Compare commits

..

1 commit

Author SHA1 Message Date
95342f7ad1 Simplify directory structure.
Before this commit, the directory structure was a mess. So messy,
in fact, that it would be incredibly inconvenient to modify it in
the existing repo. So, here's a new repo. That shouldn't happen
again.
2022-07-23 00:19:12 +00:00
160 changed files with 10036 additions and 13038 deletions

1
.cvsignore Normal file
View file

@ -0,0 +1 @@
build

2
.exrc
View file

@ -1,2 +0,0 @@
set tabstop=4
set expandtab

View file

@ -1,49 +0,0 @@
name: Bug Report
about: File a bug report regarding Telodendria, its website, or its documentation.
body:
- type: markdown
attributes:
value: |
You are submitting a bug report. Please be sure to fill out the
title with a brief description of the bug.
- type: dropdown
attributes:
label: Type
description: Select the type of issue.
options:
- Memory Leak
- Crash
- Unexpected Error Message
- Documentation
- Website
- Other
validations:
required: true
- type: textarea
attributes:
label: Description
description: |
Please give a thorough and detailed description of the bug you
are reporting. Provide all the information you have, and do
some investigating to ensure you are providing a legitimate
issue report that is well thought out. **Include details on
how to reproduce the issue, or explicitly state that you were
unable to reproduce it.**
validations:
required: true
- type: textarea
attributes:
label: Log Output
description: |
Please copy and paste the relevant sections of the log output,
or the entire log if it is not unreasonably large. The logs
will be automatically formatted, no code block is necessary.
render: shell
validations:
required: true
- type: markdown
attributes:
value: |
Please attach any additional files that may aid in our
investigation of this issue, including screenshots, debugging
session stack traces and dumps, etc.

View file

@ -1,7 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: General Matrix Room
url: "https://matrix.to/#/#telodendria-general:bancino.net"
about: |
General discussion on Telodendria happens in this Matrix room. You
may get quicker feedback from there.

View file

@ -1,18 +0,0 @@
name: Feature Request
about: Request a new feature or enhancement be added to Telodendria.
title: "[Feature] "
body:
- type: markdown
attributes:
value: |
You are submitting a feature request. Please be sure to fill
out the title with a brief description of the feature you are
requesting.
- type: textarea
attributes:
label: Description
description: |
Please give a thorough and detailed description of the feature
you are requesting.
validations:
required: true

View file

@ -1,25 +0,0 @@
---
Please review the developer certificate of origin:
1. The contribution was created in whole or in part by me, and I have
the right to submit it under the open source licenses of the
Telodendria project; or
1. The contribution is based upon a previous work that, to the best of
my knowledge, is covered under an appropriate open source license and
I have the right under that license to submit that work with
modifications, whether created in whole or in part by me, under the
Telodendria project license; or
1. The contribution was provided directly to me by some other person
who certified (1), (2), or (3), and I have not modified it.
1. I understand and agree that this project and the contribution are
made public and that a record of the contribution—including all
personal information I submit with it—is maintained indefinitely
and may be redistributed consistent with this project or the open
source licenses involved.
- [ ] I have read the Telodendria Project development certificate of
origin, and I certify that I have permission to submit this patch
under the conditions specified in it.

View file

@ -1,27 +0,0 @@
name: Compile Telodendria
run-name: Compile Telodendria on ${{ forgejo.actor }}
on:
push:
branches:
- 'ma*'
pull_request:
types: [opened, synchronize, reopened]
jobs:
"Compile Telodendria":
strategy:
matrix:
os: [alpine]
arch: [aarch64]
runs-on: ["${{ matrix.os }}", "${{ matrix.arch }}"]
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
submodules: true
- name: Configure Telodendria
run: ./configure
- name: Configure & Build Cytoplasm
run: make cytoplasm
- name: Build Telodendria
run: make

View file

@ -1,44 +0,0 @@
name: Release Telodendria
run-name: Release Telodendria on ${{ forgejo.actor }}
on:
push:
tags:
- 'v*'
jobs:
"Release Telodendria":
strategy:
matrix:
os: [alpine]
arch: [aarch64]
runs-on: ["${{ matrix.os }}", "${{ matrix.arch }}"]
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
submodules: true
- name: Archive submodules
run: git submodule foreach --recursive 'git archive --format tar --prefix=$displaypath/ -o submodule.tar HEAD'
- name: Archive repository
run: git archive --format tar -o release.tar HEAD
- name: Produce release archive
run: |
TOPDIR=$(pwd) git submodule --quiet foreach --recursive 'cd $TOPDIR; tar --concatenate --file=release.tar $displaypath/submodule.tar; rm -fv $displaypath/submodule.tar'
gzip release.tar
mkdir release
mv release.tar.gz release/Telodendria-$GITHUB_REF_NAME.tar.gz
- name: Upload artifact
uses: actions/upload-artifact@v3
with:
name: Telodendria-$GITHUB_REF_NAME.tar.gz
path: release/Telodendria-$GITHUB_REF_NAME.tar.gz
- name: Publish release
uses: actions/forgejo-release@v2
with:
tag: $GITHUB_REF_NAME
title: "Telodendria $GITHUB_REF_NAME"
release-dir: release/
release-notes: "docs/CHANGELOG.md"
direction: upload
prerelease: true

2
.github/FUNDING.yml vendored
View file

@ -1,2 +0,0 @@
liberapay: "Telodendria"
custom: "https://donate.stripe.com/8wM29AfF5bRJc48eUU"

18
.gitignore vendored
View file

@ -1,18 +0,0 @@
# Telodendria .gitignore
build
out
data
Makefile
*-leaked.txt
.env
*.patch
*.orig
*.log
vgcore.*
*.core
contrib/.vagrant
src/Schema
src/include/Schema
man/mandoc.db

3
.gitmodules vendored
View file

@ -1,3 +0,0 @@
[submodule "Cytoplasm"]
path = Cytoplasm
url = https://git.telodendria.io/Telodendria/Cytoplasm.git

8
.idea/Telodendria.iml Normal file
View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="CPP_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml Normal file
View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/Telodendria.iml" filepath="$PROJECT_DIR$/.idea/Telodendria.iml" />
</modules>
</component>
</project>

52
.idea/workspace.xml Normal file
View file

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CMakeSettings">
<configurations>
<configuration PROFILE_NAME="Debug" ENABLED="true" CONFIG_NAME="Debug" />
</configurations>
</component>
<component name="ChangeListManager">
<list default="true" id="fb1df1f2-2a47-495d-89b9-277de6049bd1" name="Changes" comment="" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="ClangdSettings">
<option name="formatViaClangd" value="false" />
</component>
<component name="MarkdownSettingsMigration">
<option name="stateVersion" value="1" />
</component>
<component name="ProjectId" id="2CD2pGJ1okjE4N8lOPrGKmwbXGw" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;RunOnceActivity.OpenProjectViewOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.cidr.known.project.marker&quot;: &quot;true&quot;,
&quot;WebServerToolWindowFactoryState&quot;: &quot;false&quot;,
&quot;cf.first.check.clang-format&quot;: &quot;false&quot;,
&quot;cidr.known.project.marker&quot;: &quot;true&quot;
}
}</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="fb1df1f2-2a47-495d-89b9-277de6049bd1" name="Changes" comment="" />
<created>1658323016759</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1658323016759</updated>
<workItem from="1658323018463" duration="25597000" />
<workItem from="1658450495358" duration="6933000" />
</task>
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
</project>

View file

@ -1,16 +0,0 @@
N: Jordan Bancino
E: jordan@bancino.net
M: @jordan:bancino.net
W: https://bancino.net
D: Project Lead
L: United States
N: LDA
E: marie@doskel.net
E: ldasta@tedomum.fr
E: lda@freetards.xyz
M: @lda:a.freetards.xyz
M: @lda:pain.agency
M: @fourier:ari.lt
D: Developer
L: France

@ -1 +0,0 @@
Subproject commit 4f316ff7b3a955b831ca4aefb8679ddf3396a7d0

View file

@ -1,6 +1,5 @@
/* /*
* Copyright (C) 2022-2024 Jordan Bancino <@jordan:bancino.net> with * Copyright (C) 2022 Jordan Bancino <@jordan:bancino.net>
* other valuable contributors. See CONTRIBUTORS.txt for the full list.
* *
* Permission is hereby granted, free of charge, to any person * Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation files * obtaining a copy of this software and associated documentation files

136
README.md
View file

@ -1,135 +1,7 @@
<h1 style="text-align: center;">Lightweight, Decentralized Chat.</h1> # [Telodendria](https://telodendria.io)
**Telodendria** is an extremely powerful, yet lightweight and portable **Telodendria** is an open source Matrix homeserver implementation written from scratch in ANSI C and designed to be lightweight and simple, yet functional.
chat server designed to be easy to install and configure. Powered by
the [Matrix](https://matrix.org) protocol, Telodendria empowers
everyone to run their own chat server on ordinary hardware, including
old and embedded devices. Whether you want a simple chat server just
for you and your friends and family, or want to talk to users on other
Matrix homeservers but don't want to go through all the hastle of
hosting a complicated, high-maintenance homeserver or joining an
existing homeserver for privacy or other reasons, then Telodendria
might be for you.
!!!! **Note:** Telodendria still in development. See **Status** below. **Important:** This project is not developed on GitHub, or even with Git. As such, GitHub Pull Requests are not accepted. But that doesn't mean we don't want your contribution! You're more than welcome to clone this repo and use Git to make changes to the project if you'd prefer it to CVS, but when it comes time to actually submit your changes to this project, use [git format-patch](https://git-scm.com/docs/git-format-patch) to generate patch files, then submit them to the official Matrix room: [#telodendria-patches:bancino.net](https://matrix.to/#/#telodendria-patches:bancino.net).
## What is Matrix? Please see the `README.txt` file for the actual project `README`, which simply details the repository structure a little bit. All of **Telodendria**'s user and developer documentation is available as `man` pages, or online.
Matrix is an **open standard** for *interoperable*, *decentralized*,
*secure*, and *real-time* communication over the internet.
Matrix can be thought of as the successor to email, but it works
very similar to iMessage, Discord, or direct messaging on most
social media networks. The primary difference between Matrix and these
other services, however, is that Matrix doesn't depend on one central
authority, and is designed in such a way to respect your privacy.
Matrix has proven itself over the last few
years to be a reliable communication tool, and has only gotten more
user-friendly over the course of its development. Matrix is capable
enough that it can&mdash;and should&mdash;totally replace any other
means of digital communication, and it offers a much higher degree
of security, simplicity, and functionality.
Strictly speaking, Matrix itself is just the *protocol* by which
clients and servers communicate. In order to use Matrix, we need
implementations of both clients and servers. Telodendria is a server
implementation of the Matrix protocol.
## Why Telodendria?
- **Lightweight:** Written in the C programming language, Telodendria
is automatically lighter and faster than other self-hosted chat servers.
It has very few external dependencies and is as self-contained as
possible.
- **Fully-Featured:** Most lightweight chat solutions compromise on
features. Telodendria is built on the fully-featured Matrix protocol,
which provides a chat experience that most normal users are familiar
with.
- **Portable:** You can run Telodendria on just about everything,
including more traditional options like a personal home server or VPS,
but also more obscure platforms like Raspberry Pis or retro computers.
Telodendria can run on a broad number of operating systems, which means
that no matter which platform and OS you prefer, there is a good chance
you can add Telodendria without much difficulty. It is also extremely
easy to migrate a Telodendria instance between platforms; just copy the
data directory to a new device.
- **Simple:** Telodendria is designed to be a simple, no-frills
chat server. It is easy to install, easy to configure, and easy to
maintain.
- **Stable:** Other Matrix homeservers develop at the pace of the
Matrix specification itself, which is to say quite rapidly. Changes are
always being made, and a version shipped 6 months ago is already
incredibly outdated. Telodendria, on the other hand, aims to be stable.
It should *just work* for long periods of time between upgrades, and
you should never feel like Telodendria is going to change significantly
between upgrades.
- **Well-Documented:** Telodendria places as much emphasis on documentation as on code, which means you can be sure that the documentation will always remain up-to-date, accurate, and most importantly, reasonably exhaustive.
[Read Technical Rationale &rightarrow;](https://git.telodendria.io/Telodendria/Telodendria/src/branch/master/docs/dev/rationale.md)
## Get Started
Check out the [Documentation](https://git.telodendria.io/Telodendria/telodendria/src/branch/master/docs/README.md) to get started with
Telodendria.
## Status
Telodendria is in the very early stages of development. As such, it may
not yet deliver on all of its promises. Currently, Telodendria is not
ready for end-users yet. While it features very basic user
authentication, it does not actually work as a chat server yet.
We are hoping to ship Telodendria `v1.7.0-alpha4` by January of 2025. This
release should be usable for communication between **local users**
only. Additional features, including federation with other Matrix
homeservers will be added in future releases.
You can help speed up development by **sponsoring**
Telodendria or [getting involved](https://git.telodendria.io/Telodendria/Telodendria/src/branch/master/docs/CONTRIBUTING.md).
## Sponsorship
Telodendria is maintained by a loosely-knit band of volunteers. The
project currently has no sponsors and thus no source of income to
pay for infrastructure costs and developer time. To ensure
Telodendria's long-term success, please consider sponsoring the
project.
You can make a recurring donation to Telodendria using
[LiberaPay](https://liberapay.com/Telodendria/donate). You can also make
one-time donations using
[Stripe](https://donate.stripe.com/8wM29AfF5bRJc48eUU). If you would
like to make a recurring donation larger than that allowed by
LiberaPay, please contact Jordan Bancino over Matrix at
`@jordan:bancino.net` or email at `jordan@bancino.net`.
### Benefits
While there are no set sponsorship tiers at this time, sponsoring
Telodendria is a mutually beneficial relationship. Depending on the
amount you donate, you can get your name, logo, and website links
on the [Sponsors](../sponsors) page, the project `README`, or the
main website.
## License
All of the code and documentation for Telodendria is licensed under a
modified MIT license. The MIT license is an extremely permissive
license that has very few restrictions. Please consult the
[`LICENSE.txt`](https://git.telodendria.io/Telodendria/Telodendria/src/branch/master/LICENSE.txt) file for the actual license text. It is
important to note that the Telodendria license text differs from the
original MIT license in the following ways:
- Where the MIT license states that the copyright notice and permission
notice shall be included in all copies or *substantial* portions of the
software, the Telodendria requires the copyright notice and
permission notice be included with *all* portions, regardless of the
size, by omitting the word *substantial*.
The Telodendria logo in all its forms, including the ASCII
representation, belongs solely to the Telodendria project. It must be
used only to represent the official Telodendria project. You are free
to use the logo in any way as long as it represents or links to the
official project. If Telodendria is forked, the logo must be removed
completely from the project, and optionally replaced by a different
one.

27
README.txt Normal file
View file

@ -0,0 +1,27 @@
=======================================================
|_ _|__| | ___ __| | ___ _ __ __| |_ __(_) __ _
| |/ _ \ |/ _ \ / _` |/ _ \ '_ \ / _` | '__| |/ _` |
| | __/ | (_) | (_| | __/ | | | (_| | | | | (_| |
|_|\___|_|\___/ \__,_|\___|_| |_|\__,_|_| |_|\__,_|
=======================================================
Copyright (C) 2022 Jordan Bancino <@jordan:bancino.net>
This is the source code for Telodendria, a Matrix homeserver written
in C. All of the documentation is available as man pages in the
man/ directory, or online at https://telodendria.io
If information is missing from the documentation, please feel free
to reach out to #telodendria-general:bancino.net on Matrix.
This file documents the directory structure of the source code
repository.
Telodendria/
contrib/ - Supplemental files, such as example configs.
man/ - The official documentation as man pages.
site/ - The official website.
src/ - The C source code for Telodendria.
include/ - Header files for the source code.
Routes/ - Where Matrix API endpoints are implemented
tests/ - Unit and integration tests will eventually go here.
tools/ - Development environment and tools.

View file

@ -1,58 +0,0 @@
{
"guard": "TELODENDRIA_SCHEMA_CLIENTEVENT_H",
"header": "Schema/ClientEvent.h",
"types": {
"ClientEventUnsignedData": {
"type": "struct",
"fields": {
"age": {
"type": "integer"
},
"prev_content": {
"type": "object"
},
"redacted_because": {
"type": "object"
},
"transaction_id": {
"type": "string"
}
}
},
"ClientEvent": {
"type": "struct",
"fields": {
"content": {
"type": "object",
"required": true
},
"event_id": {
"type": "string",
"required": true
},
"origin_server_ts": {
"type": "integer",
"required": true
},
"room_id": {
"type": "string",
"required": true
},
"sender": {
"type": "string",
"required": true
},
"state_key": {
"type": "string"
},
"type": {
"type": "string",
"required": true
},
"_unsigned": {
"type": "ClientEventUnsignedData"
}
}
}
}
}

View file

@ -1,88 +0,0 @@
{
"guard": "TELODENDRIA_SCHEMA_CONFIG_H",
"header": "Schema\/Config.h",
"include": [ "Cytoplasm\/Db.h", "Cytoplasm/HttpServer.h" ],
"types": {
"ConfigTls": {
"fields": {
"cert": { "type": "string", "required": true },
"key": { "type": "string", "required": true }
},
"type": "struct"
},
"ConfigListener": {
"fields": {
"port": { "type": "integer", "required": true },
"threads": { "type": "integer", "required": false },
"maxConnections": { "type": "integer", "required": false },
"tls": { "type": "ConfigTls", "required": false }
},
"type": "struct"
},
"ConfigRunAs": {
"fields": {
"uid": { "type": "string", "required": false },
"gid": { "type": "string", "required": true }
},
"type": "struct"
},
"ConfigLogOutput": {
"fields": {
"stdout": { "name": "CONFIG_LOG_OUTPUT_STDOUT" },
"file": { "name": "CONFIG_LOG_OUTPUT_FILE" },
"syslog": { "name": "CONFIG_LOG_OUTPUT_SYSLOG" }
},
"type": "enum"
},
"ConfigLogLevel": {
"fields": {
"message": { "name": "CONFIG_LOG_LEVEL_MESSAGE" },
"debug": { "name": "CONFIG_LOG_LEVEL_DEBUG" },
"notice": { "name": "CONFIG_LOG_LEVEL_NOTICE" },
"warning": { "name": "CONFIG_LOG_LEVEL_WARNING" },
"error": { "name": "CONFIG_LOG_LEVEL_ERROR" }
},
"type": "enum"
},
"ConfigLogConfig": {
"fields": {
"output": { "type": "ConfigLogOutput", "required": true },
"level": { "type": "ConfigLogLevel", "required": false },
"timestampFormat":{ "type": "string", "required": false },
"color": { "type": "boolean", "required": false }
},
"type": "struct"
},
"Db *": { "type": "extern" },
"DbRef *": { "type": "extern" },
"char *": { "type": "extern" },
"Config": {
"fields": {
"db": { "type": "Db *", "ignore": true },
"ref": { "type": "DbRef *", "ignore": true },
"ok": { "type": "boolean", "ignore": true },
"err": { "type": "char *", "ignore": true },
"listen": { "type": "[ConfigListener]", "required": true },
"runAs": { "type": "ConfigRunAs", "required": false },
"log": { "type": "ConfigLogConfig", "required": true },
"serverName": { "type": "string", "required": true },
"baseUrl": { "type": "string", "required": false },
"identityServer": { "type": "string", "required": false },
"pid": { "type": "string", "required": false },
"maxCache": { "type": "integer", "required": false },
"federation": { "type": "boolean", "required": true },
"registration": { "type": "boolean", "required": true }
},
"type": "struct"
}
}
}

View file

@ -1,121 +0,0 @@
{
"guard": "TELODENDRIA_SCHEMA_FILTER_H",
"header": "Schema\/Filter.h",
"types": {
"FilterRoom": {
"fields": {
"not_rooms": {
"type": "[string]"
},
"state": {
"type": "FilterRoomEvent"
},
"include_leave": {
"type": "boolean"
},
"timeline": {
"type": "FilterRoomEvent"
},
"account_data": {
"type": "FilterRoomEvent"
},
"rooms": {
"type": "[string]"
},
"ephemeral": {
"type": "FilterRoomEvent"
}
},
"type": "struct"
},
"FilterEventFormat": {
"fields": {
"federation": {
"name": "FILTER_FORMAT_FEDERATION"
},
"client": {
"name": "FILTER_FORMANT_CLIENT"
}
},
"type": "enum"
},
"FilterEvent": {
"fields": {
"not_senders": {
"type": "[string]"
},
"limit": {
"type": "integer"
},
"senders": {
"type": "[string]"
},
"types": {
"type": "[string]"
},
"not_types": {
"type": "[string]"
}
},
"type": "struct"
},
"Filter": {
"fields": {
"event_format": {
"type": "FilterEventFormat"
},
"presence": {
"type": "FilterEvent"
},
"account_data": {
"type": "FilterEvent"
},
"room": {
"type": "FilterRoom"
},
"event_fields": {
"type": "[string]"
}
},
"type": "struct"
},
"FilterRoomEvent": {
"fields": {
"not_rooms": {
"type": "[string]"
},
"not_senders": {
"type": "[string]"
},
"limit": {
"type": "integer"
},
"senders": {
"type": "[string]"
},
"include_redundant_members": {
"type": "boolean"
},
"types": {
"type": "[string]"
},
"rooms": {
"type": "[string]"
},
"lazy_load_members": {
"type": "boolean"
},
"not_types": {
"type": "[string]"
},
"contains_url": {
"type": "boolean"
},
"unread_thread_notifications": {
"type": "boolean"
}
},
"type": "struct"
}
}
}

View file

@ -1,38 +0,0 @@
{
"header": "Schema\/LoginRequest.h",
"types": {
"LoginRequestType": {
"fields": {
"m.login.password": { "name": "REQUEST_TYPE_PASSWORD" }
},
"type": "enum"
},
"LoginRequestUserIdentifier": {
"fields": {
"type": { "type": "string" },
"user": { "type": "string" }
},
"type": "struct"
},
"LoginRequest": {
"fields": {
"type": { "type": "LoginRequestType" },
"identifier": { "type": "object" },
"password": { "type": "string" },
"address": { "type": "string" },
"user": { "type": "string" },
"device_id": { "type": "string" },
"initial_device_display_name": { "type": "string" },
"medium": { "type": "string" },
"token": { "type": "string" },
"refresh_token": { "type": "boolean" }
},
"type": "struct"
}
},
"guard": "TELODENDRIA_SCHEMA_LOGIN_REQUEST_H"
}

View file

@ -1,81 +0,0 @@
{
"guard": "TELODENDRIA_SCHEMA_PDUV1_H",
"header": "Schema/PduV1.h",
"types": {
"PduV1EventHash": {
"type": "struct",
"fields": {
"sha256": {
"type": "string",
"required": true
}
}
},
"PduV1UnsignedData": {
"type": "struct",
"fields": {
"age": {
"type": "integer"
}
}
},
"PduV1": {
"type": "struct",
"fields": {
"auth_events": {
"type": "array",
"required": true
},
"content": {
"type": "object",
"required": true
},
"depth": {
"type": "integer",
"required": true
},
"event_id": {
"type": "string",
"required": true
},
"hashes": {
"type": "PduV1EventHash",
"required": true
},
"origin_server_ts": {
"type": "integer",
"required": true
},
"prev_events": {
"type": "array",
"required": true
},
"redacts": {
"type": "string"
},
"room_id": {
"type": "string",
"required": true
},
"sender": {
"type": "string",
"required": true
},
"signatures": {
"type": "object",
"required": true
},
"state_key": {
"type": "string"
},
"type": {
"type": "string",
"required": true
},
"_unsigned": {
"type": "PduV1UnsignedData"
}
}
}
}
}

View file

@ -1,77 +0,0 @@
{
"guard": "TELODENDRIA_SCHEMA_PDUV3_H",
"header": "Schema/PduV3.h",
"types": {
"PduV3EventHash": {
"type": "struct",
"fields": {
"sha256": {
"type": "string",
"required": true
}
}
},
"PduV3UnsignedData": {
"type": "struct",
"fields": {
"age": {
"type": "integer"
}
}
},
"PduV3": {
"type": "struct",
"fields": {
"auth_events": {
"type": "[string]",
"required": true
},
"content": {
"type": "object",
"required": true
},
"depth": {
"type": "integer",
"required": true
},
"hashes": {
"type": "PduV3EventHash",
"required": true
},
"origin_server_ts": {
"type": "integer",
"required": true
},
"prev_events": {
"type": "[string]",
"required": true
},
"redacts": {
"type": "string"
},
"room_id": {
"type": "string",
"required": true
},
"sender": {
"type": "string",
"required": true
},
"signatures": {
"type": "object",
"required": true
},
"state_key": {
"type": "string"
},
"type": {
"type": "string",
"required": true
},
"_unsigned": {
"type": "PduV3UnsignedData"
}
}
}
}
}

View file

@ -1,49 +0,0 @@
{
"guard": "TELODENDRIA_SCHEMA_REGTOKEN_H",
"header": "Schema\/RegToken.h",
"include": [
"Cytoplasm\/Db.h"
],
"types": {
"Db *": {
"type": "extern"
},
"DbRef *": {
"type": "extern"
},
"RegTokenInfo": {
"fields": {
"db": {
"type": "Db *",
"ignore": true
},
"ref": {
"type": "DbRef *",
"ignore": true
},
"name": {
"type": "string"
},
"created_by": {
"type": "string"
},
"created_on": {
"type": "integer"
},
"expires_on": {
"type": "integer"
},
"used": {
"type": "integer"
},
"uses": {
"type": "integer"
},
"grants": {
"type": "array"
}
},
"type": "struct"
}
}
}

View file

@ -1,17 +0,0 @@
{
"header": "Schema\/Registration.h",
"types": {
"RegistrationRequest": {
"fields": {
"username": { "type": "string" },
"password": { "type": "string" },
"device_id": { "type": "string" },
"inhibit_login": { "type": "boolean" },
"initial_device_display_name": { "type": "string" },
"refresh_token": { "type": "boolean" }
},
"type": "struct"
}
},
"guard": "TELODENDRIA_SCHEMA_REGISTRATION_H"
}

View file

@ -1,21 +0,0 @@
{
"header": "Schema\/RequestToken.h",
"types": {
"RequestToken": {
"fields": {
"client_secret": { "type": "string" },
"send_attempt": { "type": "integer" },
"next_link": { "type": "string" },
"id_access_token": { "type": "string" },
"id_server": { "type": "string" },
"email": { "type": "string" },
"country": { "type": "string" },
"phone_number": { "type": "string" }
},
"type": "struct"
}
},
"guard": "TELODENDRIA_SCHEMA_REQUESTTOKEN_H"
}

View file

@ -1,120 +0,0 @@
{
"guard": "TELODENDRIA_SCHEMA_ROOMCREATE_H",
"header": "Schema/RoomCreateRequest.h",
"types": {
"RoomVisibility": {
"fields": {
"public": {
"name": "ROOM_PUBLIC"
},
"private": {
"name": "ROOM_PRIVATE"
}
},
"type": "enum"
},
"RoomCreateRequest": {
"fields": {
"invite": {
"type": "[string]"
},
"room_version": {
"type": "string"
},
"invite_3pid": {
"type": "[RoomInvite3Pid]"
},
"topic": {
"type": "string"
},
"visibility": {
"type": "RoomVisibility"
},
"creation_content": {
"type": "object"
},
"is_direct": {
"type": "boolean"
},
"name": {
"type": "string"
},
"initial_state": {
"type": "[RoomStateEvent]"
},
"power_level_content_override": {
"type": "object"
},
"room_alias_name": {
"type": "string"
},
"preset": {
"type": "RoomCreatePreset"
}
},
"type": "struct"
},
"RoomInvite3Pid": {
"fields": {
"id_access_token": {
"required": true,
"type": "string"
},
"address": {
"required": true,
"type": "string"
},
"medium": {
"required": true,
"type": "Room3PidMedium"
},
"id_server": {
"required": true,
"type": "string"
}
},
"type": "struct"
},
"Room3PidMedium": {
"fields": {
"msisdn": {
"name": "ROOM_3PID_MSISDN"
},
"email": {
"name": "ROOM_3PID_EMAIL"
}
},
"type": "enum"
},
"RoomCreatePreset": {
"fields": {
"public_chat": {
"name": "ROOM_CREATE_PUBLIC"
},
"trusted_private_chat": {
"name": "ROOM_CREATE_TRUSTED"
},
"private_chat": {
"name": "ROOM_CREATE_PRIVATE"
}
},
"type": "enum"
},
"RoomStateEvent": {
"fields": {
"content": {
"required": true,
"type": "object"
},
"state_key": {
"type": "string"
},
"type": {
"required": true,
"type": "string"
}
},
"type": "struct"
}
}
}

View file

@ -1,13 +0,0 @@
{
"header": "Schema\/UserDirectoryRequest.h",
"types": {
"UserDirectoryRequest": {
"fields": {
"search_term": { "type": "string" },
"limit": { "type": "integer" }
},
"type": "struct"
}
},
"guard": "TELODENDRIA_SCHEMA_USERDIRECTORYREQUEST_H"
}

141
TODO.txt Normal file
View file

@ -0,0 +1,141 @@
Telodendria To-Do List
Key:
[ ] Not Started
[x] Done
[~] In Progress
[!] Won't Fix
Phase 1: Getting off the ground
[x] Name this project
[x] Set up a CVS repository
[x] Make CVS repository public
[x] Write a build script
[x] Write a coding style guide
[x] Add a license
[x] Add support and issue reporting guide
[x] Add table of contents to website
Phase 2: Building a foundation
[x] Implement an array
[x] Implement a logging facility
[x] Implement a hash map
[x] Allow custom hash functions for each hash map
[x] Combine library code files
[x] Implement configuration file parsing using hash map
[x] Base64 encoding/decoding with padded/unpadded support
[x] Write a release script
[x] UTF-8 encoder
[x] Implement a JSON library using the hash map and array
[x] Basic encoding from HashMap/Array/strings, etc.
[x] Basic decoding to HashMap/Array/strings, etc.
[x] Proper string encoding
[x] Proper string decoding
[x] Canonical JSON
[x] Keys are sorted lexiconographically
[x] Floats are not allowed (ignore any float values)
[x] Encode as UTF-8 instead of using \u escapes
[x] Decode encoded strings to UTF-8
[x] Write a function that gets the current Unix timestamp in milliseconds
[x] Parse the Telodendria config file
[x] Add license header to all files
[!] Add documentation for all public APIs (See below)
[x] Implement a simple HTTP server
[x] Implement param parser
[x] URL encoder/decoder
[~] Design server architecture
[x] Route requests
[x] Handle requests
[~] Data abstraction layer
[ ] Database upgrades/migration path
[ ] Caching and cache control
[x] Make memory info access O(1)
[x] Error generation
[x] Properly implement the command line options as stated in telodendria(8)
[x] Remove "chroot" option, just chroot into the data dir, and make the log
file live there as well.
[x] Allow logging to the syslog
[x] Fix memory leaks
[ ] Fix bug where the socket stays open after quit.
[ ] Possibly related to not closing the connections with fclose()?
(see HttpServer.c, grep for fclose)
[ ] Figure out how to write unit tests for array/hashmap/etc
[ ] Add recipe to td script to upload patches to the Matrix room
Phase 3: Welcome to Matrix
[~] client-Server API
[x] Error responses
[x] CORS headers
[x] /_matrix/client/versions
[x] Well-known URIs
[x] Make base-url optional in config
[x] Make identity-server optional in config
[ ] Client authentication
[ ] Capabilities negotiation
[ ] Server-Server API
[ ] Application Service API
[ ] Identity Service API
[ ] Push Gateway API
[ ] Room Versions
Phase 4:
[ ] Create an OpenBSD package and get it submitted to ports
[ ] Add relayd examples to contrib/
[ ] Create a command line tool to manage Telodendria
[ ] User management
[ ] Room management
[ ] Migrate from Synapse or Dendrite, whichever is more mainstream by the time we get here
Documentation
[x] Update rationale section
[x] Update project description (no longer a CGI binary)
[x] Clean up dark mode colors (tables, background, code snippets)
[x] Add logo (possibly center title?)
[x] Update code style to not include indent or line rules, but use indent(1) instead
[x] fix typo "Subitting Patches" in Table of Contents
[x] Make a note in Getting the Code that the password for the anoncvs account is just anoncvs
[x] Add contributors list
[x] Add list of make.sh recipes and what they do
[x] Improve Google Lighthouse score on website
[!] Image elements do not have explicit width and height
[x] Background and foreground colors do not have sufficient contrast ratio (msg-error div)
[x] Lists do not contain only <li> elements
[!] Add other message divs for notes and warnings
[~] Convert documentation to man pages
[x] Clean up dark mode in man page CSS (links)
[x] Synopsis table should not be styled
[x] Make available on MANPATH in tools/env.sh
[x] Convert list of man pages into a table on home page
[~] Internal API docs
[x] Array
[x] Base64
[ ] CanonicalJson
[ ] Config
[ ] API (Config.3)
[ ] File format (Config.5)
[ ] Constants
[x] HashMap
[ ] Http
[ ] HttpServer
[ ] Json
[ ] Log
[ ] Matrix
[ ] NonPosix
[x] Queue
[ ] Routes
[ ] TelodendriaConfig
[ ] Util
[ ] Memory
[ ] Db
[x] Add onboarding documentation
[x] Building from source
[!] Writing config file (see Config.5)

62
Telodendria.css Normal file
View file

@ -0,0 +1,62 @@
body {
margin: auto;
max-width: 8.5in;
padding: 0.25in;
}
.code {
background-color: #eee;
border-radius: 5px;
display: block;
padding: 0.5em 1em 1.5em 1em;
font-family: monospace;
white-space: pre;
overflow-x: scroll;
}
kbd {
background-color: #eee;
border-radius: 3px;
border: 1px solid #b4b4b4;
box-shadow: 0 1px 1px rgba(0, 0, 0, .2), 0 2px 0 0 rgba(255, 255, 255, .7) inset;
color: #333;
display: inline-block;
font-size: .85em;
font-weight: 700;
line-height: 1;
padding: 2px 4px;
white-space: nowrap;
}
h1, h4, h5, h6 {
border-bottom: 1px dashed gray;
}
h4, h5, h6 {
width: fit-content;
}
a {
color: #0969da;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
table {
width: 100%;
border-collapse: collapse;
}
td, th {
border: 1px solid #eee;
text-align: left;
padding: 8px;
}
tr:nth-child(even) {
background-color: #eee;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #212121;
color: white;
}
.code, tr:nth-child(even) {
background-color: #333333;
}
}

541
Telodendria.html Normal file
View file

@ -0,0 +1,541 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="author" content="Jordan Bancino">
<meta name="description"
content="Telodendria, a Matrix homeserver written in ANSI C.">
<meta property="og:title"
content="Telodendria | A Matrix Homeserver written in ANSI C.">
<meta property="og:type" content="website">
<meta property="og:url"
content="https://bancino.net/pub/telodendria/telodendria.html">
<meta property="og:description"
content="Telodendria, a Matrix homeserver written in ANSI C.">
<link rel="stylesheet" href="Telodendria.css">
<title>Telodendria | A Matrix Homeserver written in ANSI C.</title>
</head>
<body>
<h1 id="telodendria">Telodendria</h1>
<p>
<b>Telodendria:</b> The terminal branches of an axon.
</p>
<p>
<b><i>Note:</i></b> <b>Telodendria</b> is under <i>heavy</i> development.
Please see the <a href="#project-status">Project Status</a>.
</p>
<p>
<b>Telodendria</b> is a Matrix homeserver implementation written from
scratch in ANSI C. It is designed to be lightweight and simple, yet
functional. <b>Telodendria</b> differentiates itself from other Matrix
homeserver implementations because it:
<ul>
<li>
Is written in C, a stable, low-level programming language with a long
history, low build and runtime overhead, and wide compatibility.
</li>
<li>
Is written with minimalism as a primary design goal. Whenever possible
and practical, no third-party libraries are pulled in to the source
code. Everything <b>Telodendria</b> needs is custom written. As a
result, <b>Telodendria</b> depends only on a standard C compiler and
library to be built, and only a web server with CGI capabilities to
run.
</li>
<li>
Uses a flat-file directory structure to store data instead of a
database. This has a number of advantages:
<ul>
<li>It makes setup and maintenance much easier.</li>
<li>
It allows <b>Telodendria</b> to run on systems with fewer resources.
</li>
<li>
It provides both runtime and data safety and stability. Since no
database is running, there's fewer things that could go wrong because
there's a lot less code running on the system.
</li>
</ul>
</li>
<li>
Runs as a CGI application. <b>Telodendria</b> is delivered as a single
small, highly-optimized binary that can be dropped in a web server's
web root to be executed. This allows it to consume very few resources
and be very easy to set up.
</li>
</ul>
<p>
<b>Telodendria</b> is on Matrix! Check out the official Matrix rooms:
</p>
<table>
<tr>
<th>Room</th>
<th>Description</th>
</tr>
<tr>
<td>
<code>#telodendria-releases:bancino.net</code>
</td>
<td>
Get notified of new releases.
</td>
</tr>
<tr>
<td>
<code>#telodendria-general:bancino.net</code>
</td>
<td>
General discussion and support for <b>Telodendria</b>.
</td>
</tr>
<tr>
<td>
<code>#telodendria-issues:bancino.net</code>
</td>
<td>
Report issues with <b>Telodendria</b>.
</td>
</tr>
<tr>
<td>
<code>#telodendria-patches:bancino.net</code>
</td>
<td>
Submit code patches to the <b>Telodendria</b> project.
</td>
</tr>
</table>
<h2 id="table-of-contents">Table of Contents</h2>
<ul>
<li>
<a href="#telodendria">Telodendria</a>
<ul>
<li><a href="#table-of-contents">Table of Contents</a></li>
<li>
<a href="#getting-started">Getting Started</a>
<ul>
<li>
<a href="#install">Install Telodendria</a>
<ul>
<li><a href="#openbsd">OpenBSD</a></li>
<li><a href="#building-from-source">Building From Source</a></li>
</ul>
</li>
<li><a href="#configure">Configure Telodendria</a></li>
</ul>
</li>
<li>
<a href="#project-status">Project Status</a>
<ul>
<li><a href="#phase-1">Phase 1: Getting Off The Ground</a></li>
<li><a href="#phase-2">Phase 2: Building A Foundation</a></li>
<li><a href="#phase-3">Phase 3: Welcome To Matrix</a></li>
<li><a href="#phase-4">Phase 4: A Real Homeserver</a></li>
</ul>
</li>
<li><a href="#rationale">Rationale</a></li>
<li><a href="#project-goals">Project Goals</a></li>
<li><a href="#getting-support">Getting Support</a></li>
<li>
<li>
<a href="#documentation-status">Documentation Status</a>
</li>
<a href="#contributing">Contributing</a>
<ul>
<li><a href="#reporting-issues">Reporting Issues</a></li>
<li>
<a href="#Developing">Developing</a>
<ul>
<li><a href="#getting-the-code">Getting The Code</a></li>
<li><a href="#code-style">Code Style</a></li>
<li><a href="#submitting-patches">Subitting Patches</a></li>
</ul>
</li>
</ul>
</li>
<li><a href="#license">License</a></li>
<li><a href="#change-log">Change Log</a></li>
</ul>
</li>
</ul>
<h2 id="getting-started">Getting Started</h2>
<h3 id="install">Install Telodendria</h3>
<p>
If your operating system has an official package or port of
<b>Telodendria</b>, you should prefer to use that. If your operating
system's package or port is too out of date for your tastes, please
contact the package's maintainers to notify them, or offer to update
the package yourself.
</p>
<p>
If your operating system does not have an official package, see below
for instructions on building from source and use them to create one.
</p>
<h4 id="openbsd">OpenBSD</h4>
<p>
<b>Telodendria</b> is available in the ports tree and as a binary
package. You can install it with the following command:
</p>
<div class="code">
$ pkg_add telodendria
</div>
<h4 id="building-from-source">Building From Source</h4>
<p>
<b>Telodendria</b> is designed to be light enough that it can be built
from source on just about any operating system. It only has the
following requirements, all of which should be already available to
you on a sufficiently complete operating system:
</p>
<ul>
<li>
A standards-compliant C compiler with the C standard library. Because
<b>Telodendria</b> is written in ANSI C, it should compile on just about
any compiler, but the following compilers are known to work:
<ul>
<li>GCC</li>
<li>Clang</li>
<li>
Tiny C Compiler (<b>Note:</b> must edit <code>make.sh</code> and remove
<code>-Wl,-static -Wl,-gc-sections</code> from <code>LDFLAGS</code>)
</li>
</ul>
Other compilers should work as well, but you may have to play with the
flags in <code>make.sh</code>.
</li>
<li>
POSIX base utilities, including <code>find</code>, <code>stat</code>,
<code>env</code>, and compliant <code>sh</code>-like shell.
</li>
</ul>
<div class="code">
$ ./make.sh
</div>
<p>
If everything went well, that will produce
<code>telodendria.cgi</code>, which you can then place under your web
root and configure your web server to execute. You'll need to make sure
<code>/.well-known/matrix</code> and <code>/_matrix</code> and all the
paths under them actually execute <code>telodendria.cgi</code>. See the
provided OpenBSD <code>httpd.conf</code> for reference. Even if you
aren't using OpenBSD's <code>httpd(8)</code>, you should find its
configuration syntax simple enough to adequately demonstrate the proper
configuration.
</p>
<h3 id="configure">Configure Telodendria</h3>
<p>
Once you get <b>Telodendria</b> built and hooked into your web server,
you will have to write a configuration file for it. The configuration
file is just JSON, and it should be called
<code>Telodendria.json</code>.
</p>
<h2 id="project-status">Project Status</h2>
<p>
<b>Telodendria</b> is a very ambitious project. There's a lot that needs
to happen yet before it is even remotely usable. At the moment, there's
nothing that even remotely resembles a Matrix homeserver here; we're still
getting off the ground and building a foundation.
</p>
<p>
Just because there's nothing here yet doesn't mean you should go away
though! We desparately need help, so you are more than welcome to help
out if you want things to go quicker. Please see the
<a href="#contributing">Contributing</a> section for details on how you
can get involved.
</p>
<h3 id="phase-1">Phase 1: Getting Off The Ground</h3>
<ul>
<li><s>Name this project</s></li>
<li><s>Set up a CVS repository</s></li>
<li><s>Make CVS repository public</s></li>
<li><s>Write a coding style guide</s></li>
<li><s>Write a build script</s></li>
<li><s>Add a license</s></li>
<li><s>Add support and issue reporting guide</s></li>
<li><s>Add table of contents to this document</s></li>
</ul>
<h3 id="phase-2">Phase 2: Building A Foundation</h3>
<ul>
<li><s>Implement an array</s></li>
<li><s>Implement a logging facility</s></li>
<li><s>Implement argument parsing (<code>-c file -Vh</code>)</s></li>
<li><s>Implement a hash map</s></li>
<li><s>Combine library code files</s></li>
<li><s>Implement configuration file parsing using the hash map</s></li>
<li>Implement a JSON library using the hash map and array</li>
<li>Figure out how to write unit tests for array/hashmap/etc</li>
<li>Implement a simple HTTP server</li>
<li>
Design the server architecture
<ul>
<li>Route requests</li>
<li>Handle requests</li>
<li>Data abstraction layer</li>
<li>Error generation</li>
</ul>
</li>
</ul>
<h3 id="phase-3">Phase 3: Welcome To Matrix</h3>
<ul>
<li>
Implement the Client-Server API
</li>
<li>
Implement the Server-Server API
</li>
<li>
Implement the other Matrix APIs
</li>
</ul>
<h3 id="phase-4">Phase 4: A Real Homeserver</h3>
<ul>
<li>
Create an OpenBSD package and get it submitted to ports
</li>
<li>
Create a command line tool to manage Telodendria
<ul>
<li>Configuration file generation</li>
<li>User management</li>
<li>Room management</li>
</ul>
</li>
<li>
Migrate from Synapse. I run a Synapse homeserver right now, so somehow
I have to get all my data into the Telodendria format.
</li>
</ul>
<h2 id="documentation-status">Documentation Status</h2>
<p>
This documentation needs just a little work. Here's the things
on my list for that:
</p>
<ul>
<li>Update Rationale section</li>
<li>Update Project description (no longer a CGI binary)</li>
<li>Update project code requirements (ANSI C, POSIX.1c)</li>
</ul>
<h2 id="rationale">Rationale</h2>
<p>
This section explains
</p>
<p>
I want a lightweight Matrix homeserver designed for OpenBSD. I want a
homeserver that can be developed in <code>vi(1)</code> and compiled
with a C compiler. I want it to function entirely on a base OpenBSD
install without having to install any extra packages whatsoever. I've
found that the existing homeserver implementations are way
over-engineered and written in such a way that many programs and
libraries have to be pulled in to use them. I also want to learn how
Matrix works, and I want to understand the code I'm running on my
server.
</p>
<p>
So I wrote Telodendria.
</p>
<p>
Telodendria is written entirely in portable ANSI C. It depends on no
third-party C libraries other than the standard C library. The only
thing you need to run it is a web server that supports executing CGI
programs, and a directory that data can be written to. Everything
Telodendria needs to run itself is compiled into a single static
binary, and the source code can be built anywhere, right out of the
box.
</p>
<p>
Telodendria doesn't use a database like all the other homeservers.
Instead, it operates more like email: it uses a flat-file data
structure similar to maildir to store data. The advantage of this is
that it saves server maintainers from also having to maintain a
database. It greatly simplifies the process of getting a Matrix
homeserver up and running, and it makes it highly portable. It also is
extremely easy to back up and restore with base tools; just
<code>tar(1)</code> up the directory, and you're good to go.
</p>
<p>
Telodendria is developed and tested on OpenBSD, but you'll find that it
should run under any web server that supports CGI. I chose to write
Telodendria as a CGI program because anyone running an existing Matrix
server is likely running a web server acting as a reverse proxy in
front of it anyway, so why not just hook the homeserver directly into
the web server? That's one less daemon to run, which means memory and
CPU savings. CGI also allows Telodendria to remain single-threaded.
Each request that comes in is handled as its own process, and
operations are entirely isolated.
</p>
<h2 id="project-goals">Project Goals</h2>
<p>
The goals of this project are as follows:
</p>
<ul>
<li>
To be a production-ready Matrix server capable of handling a lot of
users. Telodendria should have good performance in many diverse
environments.
</li>
<li>
To have as few external build and run dependencies as possible. It
should be possible to compile Telodendria on any operating system out
of the box, and have it be totally statically linked, ready to run
under a <code>chroot(8)</code>-ed web server. You'll even notice that
the documentation is written in HTML directly, not Markdown, to remove
the dependency on a Markdown parser and renderer.
</li>
<li>
To be written in clean, elegant, and well-documented code. The goal is
to build a Matrix homeserver from the ground up, not just because I
don't the way existing homeservers are implemented, but also so I can
learn how Matrix really works, and maybe even teach others along the
way.
</li>
</ul>
<h2 id="getting-support">Getting Support</h2>
<p>
<b>Telodendria</b> is designed to be fairly straightforward, but that
doesn't mean there won't be hiccups along the way. If you are struggling
to get <b>Telodendria</b> up and running, you're more than welcome to
reach out for support. Just join the
<code>#telodendria-general:bancino.net</code> Matrix channel. Before
you do though, make sure you're running the latest version of
<b>Telodendria</b> and you've thoroughly read through all the
relevant documentation.
</p>
<h2 id="contributing">Contributing</h2>
<p>
<b>Telodendria</b> is an open source project. As such, it welcomes
contributions. There are many ways you can contribute, and any way you
can is greatly appreciated.
</p>
<h3 id="reporting-issues">Reporting Issues</h3>
<p>
If&mdash;after you've reached out to
<code>#telodendria-general:bancino.net</code>&mdash;it has been
determined that there is a problem with <b>Telodendria</b>, it should
be reported to <code>#telodendria-issues:bancino.net</code>. There it
can be discussed further. The issues channel serves as the official
issue tracker of <b>Telodendria</b>; although issues may be copied
into a <code>TODO</code> file in the CVS repository just so they
don't get lost.
</p>
<h3 id="developing">Developing</h3>
<p>
The primary language used to write <b>Telodendria</b> code is ANSI C.
Yes, that's the original C standard from 1989. The reason this standard
is chosen, and the reason that it will not be changed, is because the
original C is the most portable. Other languages you'll find in the
<b>Telodendria</b> repository are shell scripts and HTML. If you have
any experience at all with any of these languages, your contributions
are valuable. Please follow the guidelines in this section to ensure
the contribution workflow goes as smoothly as possible.
</p>
<h4 id="getting-the-code">Getting The Code</h4>
<p>
There are multiple ways to get the source code for <b>Telodendria</b>.
You can download an official release tarball from
<a href="https://bancino.net/pub/telodendria">here</a> if you would like,
but the preferred way is to check out the source code from CVS. This
makes generating patches a lot easier. If you do not have CVS, consult
your operating system's package repository to install it. CVS was the
chosen version control system for this project primarily because it is
built into OpenBSD.
</p>
<div class="code">
$ export CVSROOT=anoncvs@bancino.net:/cvs
$ cvs checkout Telodendria
$ cd Telodendria
</div>
<p>
You should now have the latest <b>Telodendria</b> source code. Follow
the <a href="#code-style">Code Style</a> as you make your changes.
</p>
<h4 id="code-style">Code Style</h4>
<p>
<b>Telodendria</b>'s code style is very unique. In general, these are
the conventions used by the code base.
</p>
<ul>
<li>
All function, enumeration, structure, and header names are
<code>CamelCase</code>. This is preferred to <code>snake_case</code>
because it is more compact.
</li>
<li>
<code>enum</code>s and <code>struct</code>s are always
<code>typedef</code>-ed to their same name. The <code>typedef</code>
occurs in the public API header, and the actual declaration occurs in
the private implementation header.
</li>
<li>
Indentation is done with spaces. This ensures that files look the same
for everyone. It also makes line wrapping rules much easier because
the indentations are the same size. Please configure your editor to
make the <kbd>Tab</kbd> key insert spaces, and if it does automatic
indentation, make sure it indents with spaces. If you cannot configure
your editor to insert spaces, then you can try running the code files
you were working on through <code>expand</code>. A unit of indentation
is 4 spaces.
</li>
<li>
Lines should not exceed 72 characters, including indentations. Some
developers use <code>vi(1)</code> in an 80x24 terminal to write code.
</li>
</ul>
<p>
This guide may be subject to change. The source code is the absolute
source of truth, so as long as you make your code look like the
code surrounding it, you should be fine.
</p>
<h4 id="submitting-patches">Submitting Patches</h4>
<p>
Submitting patches is fairly easy to do if you've got the CVS sources
checked out. Once you have made your changes, just run
<code>cvs diff</code>:
</p>
<div class="code">
$ cvs diff -uNp > your-changes.patch
</div>
<p>
Then, send the resulting patches to
<code>#telodendria-patches:bancino.net</code>, where they will be
promptly reviewed by the community.
</p>
<h2 id="license">License</h2>
<p>
All of the code and documentation for <b>Telodendria</b> is licensed
under the following terms and conditions:
</p>
<div class="code">
Copyright (C) 2022 Jordan Bancino &lt;@jordan:bancino.net&gt;
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
</div>
<h2 id="change-log">Change Log</h2>
<p>
At this time, Telodendria does not have any tagged releases because it
is not yet functional as a Matrix homeserver. Please check out the <a
href="#project-status">Project Status</a> to see where things are
currently at.
</p>
</body>
</html>

321
configure vendored
View file

@ -1,321 +0,0 @@
#!/usr/bin/env sh
#
# Argument Parsing
#
echo "Build Configuration"
echo "-------------------"
BUILD="build"
OUT="out"
SRC="src"
INCLUDE="src/include"
TOOLS="tools/src"
SCHEMA="Schema"
CYTOPLASM="Cytoplasm"
CFLAGS="-O1 -D_DEFAULT_SOURCE -I${INCLUDE} -I${BUILD}"
LIBS="-lm -pthread -lCytoplasm"
# Set default args for all platforms
SCRIPT_ARGS="--prefix=/usr/local --bin-name=telodendria --version=1.7.0-alpha4"
if [ -f "${CYTOPLASM}/configure" ]; then
SCRIPT_ARGS="${SCRIPT_ARGS} --cytoplasm=${CYTOPLASM}"
else
SCRIPT_ARGS="${SCRIPT_ARGS} --cytoplasm=" # No cytoplasm path.
fi
# Set compiler depending on the platform.
case "$(uname)" in
Linux|NetBSD)
# These systems typically use GCC.
SCRIPT_ARGS="${SCRIPT_ARGS} --cc=gcc"
;;
OpenBSD|FreeBSD|Darwin)
# These systems typically use Clang.
SCRIPT_ARGS="${SCRIPT_ARGS} --cc=clang"
;;
*)
# Use default compiler which is required to be present on
# all POSIX-compliant systems.
SCRIPT_ARGS="${SCRIPT_ARGS} --cc=c99"
;;
esac
# Append any additional args specified by user
SCRIPT_ARGS="${SCRIPT_ARGS} $@"
echo "Processing options..."
echo "Ran with arguments: $SCRIPT_ARGS"
# Process all arguments
for arg in $SCRIPT_ARGS; do
case "$arg" in
--cc=*)
CC=$(echo "$arg" | cut -d '=' -f 2-)
case "${CC}" in
gcc*|clang*)
# "Fancy" compilers that support a plethora of additional flags we
# want to enable if present.
CFLAGS="-Wall -Wextra -Werror -pedantic -std=c99 -O3 ${CFLAGS}"
LDFLAGS="${LDFLAGS} -flto -fdata-sections -ffunction-sections -s -Wl,-gc-sections"
;;
esac
;;
--prefix=*)
PREFIX=$(echo "$arg" | cut -d '=' -f 2-)
;;
--bin-name=*)
BIN_NAME=$(echo "$arg" | cut -d '=' -f 2-)
;;
--version=*)
VERSION=$(echo "$arg" | cut -d '=' -f 2-)
;;
--enable-debug)
DEBUG="-O0 -g"
;;
--disable-debug)
DEBUG=""
;;
--cytoplasm=*)
CYTOPLASM=$(echo "$arg" | cut -d '=' -f 2-)
if [ -n "${CYTOPLASM}" ]; then
if [ ! -f "${CYTOPLASM}/configure" ]; then
echo "Path for Cytoplasm does not appear to actually contain Cytoplasm source:"
echo "${CYTOPLASM}"
exit 1
fi
CFLAGS="${CFLAGS} -I${CYTOPLASM}/include"
LDFLAGS="-L${CYTOPLASM}/out/lib ${LDFLAGS}"
fi
;;
*)
echo "Invalid argument: $arg"
exit 1
;;
esac
done
CFLAGS="${CFLAGS} '-DTELODENDRIA_VERSION=\"${VERSION}\"' ${DEBUG}"
LDFLAGS="${LDFLAGS} ${LIBS}"
#
# Makefile generation
#
collect() {
from="$1"
orig_ext="$2"
new_ext="$3"
prefix="$4"
exec="$5"
find "${from}" -name "*${orig_ext}" -type f | while IFS= read -r src; do
src=$(echo "$src" | sed -e "s|^${from}||g")
obj=$(echo "$src" | sed -e "s|${orig_ext}\$|${new_ext}|g")
obj="${prefix}${obj}"
src="${from}${src}"
"${exec}" "${src}" "${obj}"
done
}
prefix() {
prefix="$1"
shift
for thing in $@; do
printf "${prefix}${thing} "
done
}
cytoplasm_tool() {
tool="$1"
if [ -n "${CYTOPLASM}" ]; then
echo "LD_LIBRARY_PATH=${CYTOPLASM}/out/lib ${CYTOPLASM}/out/bin/$tool"
else
echo "$tool"
fi
}
print_src() {
printf '%s ' "$1"
}
print_obj() {
printf '%s ' "$2"
}
get_deps() {
src="$1"
${CC} -I${INCLUDE} -I${BUILD} $(if [ -n "${CYTOPLASM}" ]; then echo "-I${CYTOPLASM}/include"; fi) -E "$src" \
| grep '^#' \
| awk '{print $3}' \
| cut -d '"' -f 2 \
| sort \
| uniq \
| grep -v '^[/<]' \
| grep -e "^${SRC}/" -e "^${BUILD}/" \
| while IFS= read -r dep; do
printf "%s " "$dep"
done
}
compile_obj() {
src="$1"
obj="$2"
pref="${obj}: $(get_deps ${src})"
echo "$pref $(collect ${SCHEMA}/ .json .h ${BUILD}/Schema/ print_obj)"
echo "${TAB}@mkdir -p $(dirname ${obj})"
echo "${TAB}\$(CC) \$(CFLAGS) -fPIC -c -o \"${obj}\" \"${src}\""
}
compile_bin() {
src="$1"
out="$2"
depObjs=$(prefix ${BUILD}/ CanonicalJson.o Telodendria.o)
echo "${out}: ${src}"
echo "${TAB}@mkdir -p ${OUT}/bin"
echo "${TAB}\$(CC) \$(CFLAGS) -o \"${out}\" \"${src}\" $depObjs \$(LDFLAGS)"
}
compile_doc() {
src="$1"
out="$2"
if echo "${src}" | grep "Schema" > /dev/null; then
return
fi
echo "${out}: ${src}"
echo "${TAB}@mkdir -p ${OUT}/man/man3"
echo "${TAB}$(cytoplasm_tool hdoc) -D \"Os=${BIN_NAME}\" -i \"${src}\" -o \"${out}\""
}
print_doc() {
if echo "${src}" | grep "Schema" > /dev/null; then
return
fi
printf '%s ' "$2"
}
compile_schema() {
src="$1"
out="$2"
obj="${BUILD}/Schema/${out}.o"
echo "${BUILD}/Schema/${out}.h:"
echo "${TAB}@mkdir -p ${BUILD}/Schema"
echo "${TAB}$(cytoplasm_tool j2s) -s \"${src}\" -h \"${BUILD}/Schema/${out}.h\" -c \"${BUILD}/Schema/${out}.c\""
echo "${BUILD}/Schema/${out}.c:"
echo "${TAB}@mkdir -p ${BUILD}/Schema"
echo "${TAB}$(cytoplasm_tool j2s) -s \"${src}\" -h \"${BUILD}/Schema/${out}.h\" -c \"${BUILD}/Schema/${out}.c\""
echo "${obj}: ${src} ${BUILD}/Schema/${out}.c"
echo "${TAB}@mkdir -p ${BUILD}/Schema"
echo "${TAB}\$(CC) \$(CFLAGS) -fPIC -c -o \"${obj}\" \"${BUILD}/Schema/${out}.c\""
}
install_out() {
src="$1"
out="$2"
dir=$(dirname "$out")
echo "${TAB}mkdir -p \"$dir\""
echo "${TAB}cp \"$src\" \"$out\""
}
install_man() {
src="${OUT}/man/man3/${BIN_NAME}-$(basename $1 .h).3"
out="$2"
dir=$(dirname "$out")
echo "${TAB}mkdir -p \"$dir\""
echo "${TAB}cp \"$src\" \"$out\""
}
uninstall_out() {
src="$1"
out="$2"
echo "${TAB}rm \"$out\""
}
echo "Generating Makefile..."
OBJS="$(collect ${SRC}/ .c .o ${BUILD}/ print_obj) $(collect ${SCHEMA}/ .json .o ${BUILD}/Schema/ print_obj)"
TAB=$(printf '\t')
cat << EOF > Makefile
.POSIX:
# Generated by '$0' on $(date).
# This file should generally not be manually edited.
CC = ${CC}
PREFIX = ${PREFIX}
CFLAGS = ${CFLAGS}
LDFLAGS = ${LDFLAGS}
all: ${BIN_NAME} docs tools
docs: $(collect ${INCLUDE}/ .h .3 ${OUT}/man/man3/${BIN_NAME}- print_doc)
tools: $(collect ${TOOLS}/ .c '' ${OUT}/bin/ print_obj)
format:
${TAB}find . -name '*.c' | while IFS= read -r src; do \\
${TAB} if indent "\$\$src"; then \\
${TAB} rm \$\$(basename "\$\$src").BAK; \\
${TAB} fi \\
${TAB}done
license:
${TAB}find . -name '*.[ch]' | while IFS= read -r src; do \\
${TAB} srcHeader=\$\$(grep -n -m 1 '^ \*/' "\$\$src" | cut -d ':' -f 1); \\
${TAB} head -n\$\$srcHeader \$\$src | \\
${TAB} diff -u -p - "LICENSE.txt" | \\
${TAB} patch "\$\$src" | grep -v "^Hmm"; \\
${TAB}done
${BIN_NAME}: ${OUT}/bin/${BIN_NAME}
install: ${BIN_NAME}
${TAB}mkdir -p \$(PREFIX)/bin
${TAB}cp ${OUT}/bin/${BIN_NAME} \$(PREFIX)/bin/${BIN_NAME}
uninstall:
${TAB}rm \$(PREFIX)/bin/${BIN_NAME}
clean:
${TAB}rm -r "${BUILD}" "${OUT}"
${OUT}/bin/${BIN_NAME}: ${OBJS}
${TAB}@mkdir -p "${OUT}/bin"
${TAB}\$(CC) -o "${OUT}/bin/${BIN_NAME}" ${OBJS} \$(CFLAGS) \$(LDFLAGS)
$(collect ${SCHEMA}/ .json '' '' compile_schema)
$(collect ${SRC}/ .c .o ${BUILD}/ compile_obj)
$(collect ${TOOLS}/ .c '' ${OUT}/bin/ compile_bin)
$(collect ${INCLUDE}/ .h .3 ${OUT}/man/man3/${BIN_NAME}- compile_doc)
$(
if [ -n "${CYTOPLASM}" ]; then
echo "cytoplasm:"
echo "${TAB}cd ${CYTOPLASM} && ./configure && \$(MAKE)"
fi
)
EOF
echo "Done. Run 'make' to build ${BIN_NAME}."

View file

@ -1,6 +0,0 @@
all:
sh tools/bin/td
install:
install build/telodendria $(PREFIX)/bin/telodendria
find man -name 'telodendria*\.[1-8]' -exec install {} $(PREFIX)/{} \;

34
contrib/Vagrantfile vendored
View file

@ -1,34 +0,0 @@
Vagrant.configure("2") do |config|
config.vm.box = "generic/openbsd7"
config.vm.network "forwarded_port", guest: 80, host: 80
config.vm.network "forwarded_port", guest: 443, host: 443
config.vm.network "forwarded_port", guest: 8008, host: 8008
# NOTE: This address is not within the allowed ranges.
# To allow this address, simply allow all ranges by specifying
# this in /etc/vbox/networks.conf (if you use the Virtualbox provider, or change the configured address):
# * 0.0.0.0/0 ::/0
config.vm.network "private_network", ip: "172.17.0.101"
# File watcher which syncs the project directory to /vagrant on the vm
config.vm.synced_folder "../", "/vagrant"
config.vm.provision "shell", inline: <<-EOF
cp /vagrant/contrib/relayd.conf /etc/relayd.conf
sed -i s/127.0.0.1/0.0.0.0/ /etc/relayd.conf
mkdir -p -m 0700 /etc/ssl/private
openssl req -x509 -newkey rsa:4096 \
-days 365 -nodes \
-subj '/CN=telodendria' \
-keyout /etc/ssl/private/telodendria.key \
-out /etc/ssl/telodendria.crt
relayd -n
rcctl enable relayd
rcctl restart relayd
cat /vagrant/tools/env.sh >> /home/vagrant/.bash_profile
sed -i 's#$(pwd)#/vagrant#' /home/vagrant/.bash_profile
sed -i 's#find tools/bin#find /vagrant/tools/bin#' /home/vagrant/.bash_profile
mkdir /vagrant/data
cp /vagrant/contrib/development.conf /vagrant/contrib/development.conf.bak
sed -i 's/"localhost"/"vagrant"/' /vagrant/contrib/development.conf
### If you changed the address in the config above you might want to change it here as well:
sed -i s#http://localhost:8008#https://172.17.0.101:443# /vagrant/contrib/development.conf
EOF
end

View file

@ -1,18 +1,18 @@
{ #
"log": { # Telodendria development configuration file.
"output": "stdout", #
"color": true,
"timestampFormat": "none", server-name "localhost";
"level": "debug" base-url "http://localhost:8008";
}, id "jordan";
"listen": [ data-dir "./data";
{ federation "true";
"port": 8008, registration "true";
"tls": false log "stdout" {
} level "debug";
], timestampFormat "none";
"registration": true, color "true";
"serverName": "localhost", };
"baseUrl": "http:\/\/localhost:8008", threads "4";
"federation": true max-connections "32";
} max-cache "1k";

9
contrib/httpd.conf Normal file
View file

@ -0,0 +1,9 @@
#
# httpd.conf: An OpenBSD httpd(8) configuration file for running
# Telodendria. Note that this is a development configuration that
# should be adapted using the httpd.conf(5) man page for production
# use.
#
server "matrix" {
listen on localhost port http
}

View file

@ -1,24 +1,32 @@
{ #
"log": { # Telodendria configuration file.
"output": "file" #
}, # The following man pages document the configuration:
"listen": [ #
{ # - telodendria.conf(5)
"port": 8008, # - Config(5)
"tls": false #
}, # Alternatively, find the man pages online at the
{ # following URL:
"port": 8448, #
"tls": { # https://telodendria.io/#documentation
"cert": "telodendria.crt", #
"key": "telodendria.key"
} listen "8008";
}
], server-name "example.com";
"serverName": "example.com", base-url "https://matrix.example.com";
"identityServer": "https://identity.example.com", identity-server "https://identity.example.com";
"baseUrl": "https://matrix.example.com",
"registration": false, id "_telodendria" "_telodendria";
"federation": true, data-dir "/var/telodendria";
"maxCache": 512000000 federation "true";
} registration "false";
log "file" {
level "warning";
timestampFormat "default";
};
threads "4";
max-connections "32";
max-cache "512M";

View file

@ -1,22 +0,0 @@
table <telodendria> { localhost }
http protocol httpproxy {
pass request quick path "/_matrix/*" forward to <telodendria>
pass request quick path "/.well-known/matrix/*" forward to <telodendria>
# You'll have to generate the following for this to work:
# /etc/ssl/telodendria.crt /etc/ssl/private/telodendria.key
tls keypair "telodendria"
block
return error
}
relay proxy {
listen on 127.0.0.1 port https tls
protocol httpproxy
forward to <telodendria> port 8008
}

13
contrib/telodendria.conf Normal file
View file

@ -0,0 +1,13 @@
# Telodendria configuration file
log "/var/log/telodendria.log" {
level "message";
timestampFormat "none";
color "true";
};
threads "4";
data-dir "/var/telodendria";
federation "true";

View file

@ -1,291 +0,0 @@
# Telodendria Change Log
This document contains the complete change log for every official release of Telodendria.
It is intended to be updated with every commit that makes a user-facing change worth
reporting in the change log. As such, it changes frequently between releases. Final
change log entries are published as [Releases](releases).
## v1.7.0-alpha4
**Not Released Yet.**
This release brings filters, rooms, and events! The core of the Matrix
protocol architecture is now in place.
Note that the versioning scheme has changed from `v0.X.0` to
`v1.7.0-alphaX`. This is so that Telodendria releases correspond to the
Matrix specification that they implement, in accordance with
[this blog post](https://telodendria.io/blog/on-matrixs-release-cadence-and-state-resolution-v1).
This versioning scheme change does not indicate a drastic leap forward
in Telodendria's development&mdash;the `-alpha4` suffix indicates that
this is the 4th pre-release, with the target being a stable `v1.7.0`.
Note also that we still have a *long* way to go before we reach that
stable release.
### Matrix Specification
The following endpoints were added:
- **POST** `/_matrix/client/v3/user/{userId}/filter`
- **GET** `/_matrix/client/v3/user/{userId}/filter/{filterId}`
### Bug Fixes & General Improvements
- Use `j2s` for parsing the configuration
- Fixed a double-free in `RouteUserProfile()` that would cause errors
with certain Matrix clients. (#35)
- Improved compatibility with NetBSD on various platforms.
- Moved [Cytoplasm](/Telodendria/Cytoplasm) to its own repository. It
will now be maintained separately and have its own releases as well.
- Use a `configure` script and `make` to build Telodendria instead of
custom scripts.
- Greatly simplified some endpoint code by using Cytoplasm's `j2s` for
parsing request bodies.
- Create a parser API for grammars found in Matrix, and refactor the
User API to use it.
### New Features
- Implemented a `"pid"` option in the configuration, allowing Telodendria
to write its process ID to a specified file.
- Moved all administrator API endpoints to `/_telodendria/admin/v1`,
because later revisions of the administrator API may break clients, so
we want a way to give those breaking revisions new endpoints.
- Implemented `/_telodendria/admin/v1/deactivate/[localpart]` for admins
to be able to deactivate users.
- Added a **PUT** option to `/_telodendria/admin/v1/config` that gives
the ability to change only a subset of the configuration.
- Implemented the following APIs for managing registration tokens:
- **GET** `/_telodendria/admin/tokens`
- **GET** `/_telodendria/admin/tokens/[token]`
- **POST** `/_telodendria/admin/tokens`
- **DELETE** `/_telodendria/admin/tokens/[token]`
- **GET** `/_matrix/client/v3/directory/room/[alias]`
- **PUT** `/_matrix/client/v3/directory/room/[alias]`
- **DELETE** `/_matrix/client/v3/directory/room/[alias]`
- **GET** `/_matrix/client/v3/rooms/[id]/aliases`
## v0.3.0
**Saturday, June 10, 2023**
Introducing a new configuration API and Cytoplasm, a general-purpose C library that
supports source/sink-agnostic I/O, TLS, an HTTP client, and more! The third major
release of Telodendria packs a lot of architectural improvements on top of supporting
more of the Matrix specification.
### Matrix Specification
Added support for the following endpoints:
- `/_matrix/client/v3/account/whoami`
- `/_matrix/client/v3/account/password`
- `/_matrix/client/v3/account/deactivate`
- `/_matrix/client/v3/profile/*`
- `/_matrix/client/v3/capabilities`
- `/_matrix/client/v3/auth/*/fallback/web`
There is also support for token-based user registration. Note that there is as of
yet no admin-facing way to create these registration tokens, but the APIs are in
place.
### New Features
- Added a new `HttpClient` API for making HTTP requests. This will eventually be
used for federating with other Matrix homeservers.
- Added support for pretty-printing JSON in `Json`. Telodendria itself does not
pretty-print JSON, but this is useful for debugging and building useful tools.
- Added a handful of new development tools built on the Telodendria APIs. New
tools include `http`, a command line tool for making HTTP requests, similar to
`curl`, `json`, a command line tool for working with JSON, similar to `jq`, and
`http-debug-server`, a simple HTTP server that just prints requests out to standard
output and returns an empty JSON object. `http` and `json` are replacements for
`curl` and `jq` that build on the `HttpClient` and `Json` APIs. They exist mainly
to test those APIs, but also to reduce the number of dependencies that Telodendria
has. `http-debug-server` exists to test the `HttpServer` and `HttpClient` APIs.
- Replaced all usage of `jq` with the new `json` tool. `jq` is no longer a development
dependency.
- Replaced all usage of `curl` with the new `http` tool. `curl` is no longer a
required development dependency.
- Added a new `tt` script for easily making Matrix requests against Telodendria
in development.
- Added TLS support to both the HTTP client and server. Currently, Telodendria
supports LibreSSL and OpenSSL, but other TLS libraries should be extremely easy
to add support for.
- Added support for spinning up multiple HTTP servers. This is useful for having
a TLS port and a non-TLS port, for example.
- Moved all program configuration to the data directory and added an administrator
API endpoint to manage it. It is now no longer recommended to manually update the
configuration file. Consult the [Administrator API](user/admin/README.md) documentation
and the [Configuration](user/config.md) documentation.
- Added an administrator API endpoint for process control. Telodendria can now be
restarted or shutdown via API endpoint.
- Added an administrator API endpoint for getting statistics about the running
Telodendria process.
- Added support for user privileges, a way to have fine-grained control over what
users are allowed to do with the administrator API. Administrator APIs for setting
and getting privileges is now supported, and registration tokens have privileges
associated with them so that users created with a token will automatically be given
the specified privileges.
### Fixes & General Improvements
- Fixed a few warnings that were generated on some obscure compilers.
- Moved the `main()` function to its own file to make it easier to link other
programs with the Telodendria APIs.
- Fixed the development tools environment script. Apparently using a hyphen as a bullet
point is not very portable, because some shell implementations of `printf` interpret it
as a flag. Switched to an asterisk.
- Fixed some intermittent I/O errors that would occur as a result of race conditions in
`JsonConsomeWhitespace()`. This function, and a few others, expect I/O to be blocking,
but the `HttpServer` sets up I/O to be non-blocking, leading to occasional failures in
JSON parsing.
- Abstracted all I/O into the new `Io` and `Stream` APIs, which provide an input- and
output- agnostic stream processing interface. This allows for a simple implementation of
proxies, TLS, and other stream filters without having to change any of the existing
code.
- Remove all non-POSIX function calls, including the call to `chroot()` and, on
OpenBSD, `pledge()` and `unveil()`. This may seem like a downgrade in security, but
these are platform-specific system calls that should be patched in by package maintainers
if they are desired. They also caused problems when implementing other features, because
some library calls need to be able to access files on the filesystem.
- Fixed the build script to supply `LDFLAGS` after the object files when linking.
Apparently the order in which libraries are passed matters to some compilers.
- Added the response status of a request to the log output. This means that requests are
logged after they have completed, not before they are started.
- Memory allocations, reallocations, and frees are no longer loged when the log level
is set to debug in the configuration file. To enable the logging of memory operations,
pass the `-v` flag.
- Implemented a proper HTTP request router with POSIX regular expression support.
Previously, a series of nested `if`-statements were used to route requests, but this
approach quickly becamse very messy. While the HTTP request router incurs a small memory
and runtime speed penalty, the code is now much more maintainable and easier to follow.
- Fixed some memory bugs in `Db` that were related to caching data. Caching should
now work as expected.
- Fixed a major design flaw in `Db` that would cause deadlock when multiple threads
request access to the same object. Database locking is now in a per-thread basis,
instead of a per-reference basis.
- Telodendria now shuts down cleanly in response to `SIGTERM`.
- Did some general refactoring to make the source code more readable and easier
to maintain.
Fixed a number of memory-related issues, including switching out some unsafe
functions for safer versions, per the recommendations of the OpenBSD linker.
- Moved all code documentation into the C header files to make it more likely
that it will get updated. A simple header file parser and documentation generator
have been added to the code base. See the `hdoc` man pages for documentation.
- Updated the build script to provide static and shared libraries containing
the code for Telodendria to make it easier to statically and dynamically link to
other programs. The idea is that these libraries should be shipped with Telodendria,
or as a separate package, and can be used to provide a high-level programming
environment.
- Updated the `Json` API to calculate the length of a JSON object. This is
used to set the `Content-Length` header in HTTP requests and reponses.
- Added some string functions, including `StrEquals()`, which replaced almost all
uses of `strcmp()`, since `strcmp()` is used almost exclusively for equality
checking. `StrEquals()` provides a standard way to do so, because previously,
multiple different conventions could be found throughout the code base (for example:
`!strcmp(str1, str2)` vs `strcmp(str1, str2) == 0`).
... And many more!
## v0.2.1
**Monday, March 6, 2023**
This is a patch release that fixes a few typos and other minor issues.
## v0.2.0
**Monday, March 6, 2023**
This release is focused on providing a decent amount of the client authentication
API. You can now create accounts on a Telodendria homeserver, and log in to
get access tokens.
### New
- Added the basic form of the user registration API. If registration is enabled
in the configuration file, clients can now register for Matrix accounts.
- Added the basic form of the user login API. Clients can now log in to
their accounts and generate access tokens to be used to authenticate requests.
- Added the basic form of the user interactive authentication API, which can be used
by endpoints that the spec says requires it. Currently, it only implements the
dummy and password stages, but more stages, such as the registration token stage,
will be added in future releases.
- Added a simple landing page that allows those setting up Telodendria to
quickly verify that it is accessible where it needs to be.
- Added the static login page for clients that don't support regular login.
### Changes
- Improved HTTP request logging by removing unnecessary log entries and making
errors more specific.
- Leaked memory is now hexdump-ed out to the log if the log level is set to debug.
This greatly simplifies debugging, because developers can now see exactly what the
contents of the leaked memory are. Note that in some circumstances, this memory
may contain sensitive data, such as access tokens, usernames, or passwords. However,
Telodendria should not be leaking memory at all, so if you encounter any leaks,
please report them.
- Refactored a lot of the code and accompanying documentation to be more readable and
maintainable.
### Bug Fixes
- Fixed a memory leak that would occur when parsing an invalid JSON object.
- Fixed an edge case where HTTP response headers were being sent before they were
properly set, causing the server to report a status of 200 even when that wasn't the
desired status.
- Fixed a few memory leaks in the HTTP parameter decoder that would occur in some
edge cases.
- Fixed an "off-by-one" error in the HTTP server request parser that would prevent
`GET` parameters from being parsed.
- Fixed the database file name descriptor to prevent directory traversal attacks
by replacing special characters with safer ones.
- Fixed a memory leak that would occur when closing a database that contains
cached objects.
- Fixed a memory leak that would occur when deleting database objects.
- Fixed a few non-fatal memory warnings that would show up as a result of passing a
constant string into certain functions.
### Misc.
- Fixed a bug in `td` that caused `cvs` to be invoked in the wrong directory when
tagging a new release.
- Added support for environment variable substitution in all site files. This
makes it easier to release Telodendria versions.
- Fix whitespace issues in various shell scripts.
- Fixed the debug log output so that it only shows the file name, not the
entire file path in the repository.
- Updated the copyright year in the source code and compiled output.
- Switched the `-std=c89` flag to `-ansi`, as `-ansi` might be more supported.
- Fixed the `-v` flag. It now sets the log level to debug as soon as possible to
allow debugging configuration file parsing if necessary.
... And many more bug fixes and feature additions! Too much has changed to make a
comprehensive change log. A lot of things have been done under the hood to make
Telodendria easier to develop in the future. Please test the current functionality,
and report bugs.
The following platforms have been known to compile and run Telodendria:
- OpenBSD
- Linux (GNU and non-GNU)
- Windows (via Cygwin)
- FreeBSD
- NetBSD
- DragonFlyBSD
- Haiku OS
- Android (via Termux)
Telodendria is about being portable; if you compile it on an obscure operating system,
do let us know about it!
## v0.1.0
**Tuesday, December 13, 2022**
This is the first public release of Telodendria so there are no changes to report.
Future releases will have a complete change log entry here.
This is a symbolic release targeted at developers, so there's nothing useful to
ordinary users yet. Stay tuned for future releases though!

View file

@ -1,256 +0,0 @@
# Contributing
Telodendria is a fully open source project. As such, it welcomes
contributions. There are many ways you can contribute, and any way you
can is greatly appreciated. This document details the ways you can
contribute, and how to go about contributing.
## Sponsoring Telodendria
If you would like to sponsor Telodendria, see the
[Sponsorship](../README.md#sponsorship) section on the main project
page. Donations of any size are greatly appreciated.
## Reporting Issues
An important way to get involved is to just report issues you find with
Telodendria during experimentation or normal use. To report an issue,
go to [Issues](/Telodendria/telodendria/issues) &rightarrow;
[New Issue](/Telodendria/telodendria/issues/new/choose) and follow the
instructions.
> **Note:** GitHub issues are not accepted. Issues may only be
> submitted to the official [Gitea](https://git.telodendria.io)
> instance.
### Feature Requests
Feature requests are allowed, but note that they are low-priority in
comparison to existing issues and features. That being said, don't
hesitate to submit feature requests. Just select the "Feature Request"
option when submitting an issue.
## Developing
If you want to write code for Telodendria, either to fix an issue or
add a new feature, you're in the right place. Please follow all the
guidelines in this document to ensure the contribution workflow goes
as smoothly as possible.
### Who can develop Telodendria?
Everyone is welcome to contribute code to Telodendria, provided that
they are willing to license their contributions under the same license
as the project itself.
The primary language used to write Telodendria code is ANSI C. Other
languages you'll find in the Telodendria repository include shell
scripts, `mdoc`, a little bit of HTML and CSS, and `Makefiles`.
Experience with any of these is preferred, but if you want to use
Telodendria to learn, that's okay too! Telodendria's code base should
hopefully be a good learning tool, and if you are serious about
submitting quality work, we'll guide you through the process and
offer suggestions.
### What do I need?
You'll need a couple of things to develop Telodendria:
- A Unix-like operating system that provides standard POSIX behavior,
or the Windows Subsystem for Linux (WSL), Cygwin, or Msys2 if you are
running Windows.
- A C compiler capable of compiling ANSI C89 code (pretty much all of
them do&mdash;pick your favorite, and if you find it doesn't work,
open an issue!).
- `make` for building the project.
- `git` for managing your changes.
- [Cytoplasm](/Telodendria/Cytoplasm), a simple C library written by
the Telodendria developers for the purpose of supporting Telodendria
in a modular way.
Optionally, you may also find these tools helpful:
- `indent` for formatting code.
- `valgrind` for debugging particularly nasty issues.
### Getting The Code
Telodendria is developed using Git. The easiest way to contribute
changes is to fork the main repository, and then creating a pull
request to ask us to pull your changes into our repo.
1. If you don't have an account on the
[Gitea instance](https://git.telodendria.io), create one and sign in.
1. Fork this repository.
1. In your development environment, clone your fork:
```shell
git clone https://git.telodendria.io/[YOUR_USERNAME]/Telodendria.git
cd Telodendria
```
Please base your changes on the `master` branch. If you need help
getting started with Git, that is beyond the scope of this
document, but you can find many good tutorials on the web.
### Building &amp; Running
Telodendria uses the `make` build system. Because it aims at maximum
portability, it targets POSIX `make` and should thus run on any POSIX
system that provides a `make`, be it GNU, BSD, or something different
entirely. To facilitate this, Telodendria provides a `configure` script
which generates the `Makefile`, because the `Makefile` would be far too
verbose and tedious to maintain in a POSIX-compatible way otherwise.
This is similar to how other C programs and libraries are built, although
note that Telodendria's `configure` script is not nearly as advanced as
an `autoconf` script, for example.
Please follow the build and installation directions for
[Cytoplasm](/Telodendria/Cytoplasm) first before attempting to build
Telodendria, because Telodendria depends on Cytoplasm and assumes it is
installed in the standard location for your system. For the best results,
it is recommended to take the time to enable TLS, unless you plan on
running Telodendria behind a reverse proxy.
To build Telodendria, simply run `configure`, then `make`:
```
$ ./configure
$ make
```
You may find some of the following options for `configure` helpful:
- `--prefix=<path>`: Set the install prefix to set by default in the `Makefile`. This defaults to `/usr/local`, which should be appropriate for most Unix-like systems.
- `--(enable|disable)-ld-extra`: Control whether or not to enable additional linking flags that create a more optimized binary. For large compilers such as GCC and Clang, these flags should be enabled. However, if you are using a small or more obscure compiler, then these flags may not be supported, so you can disable them with this option.
- `--(enable|disable)-debug`: Control whether or not to enable debug mode. This sets the optimization level to 0 and builds with debug symbols. Useful for running with a debugger.
- `--static` and `--no-static`: Controls whether static binaries are built by default. On BSD systems, `--static` is perfectly acceptable, but on GNU systems, `--no-static` is often desirable to silence warnings about static binaries emitted by the GNU linker.
Telodendria can be customized with the following options:
- `--bin-name=<name>`: The output name of the server binary. This defaults to `telodendria`. Common alternatives are `matrix-telodendria` or `telodendria-server`.
- `--version=<version>`: The version string to embed in the binary. This can be used to indicate build customizations or non-release versions of Telodendria.
The following recipes are available in the generated `Makefile`:
- `all`: This is the default target. It builds everything.
- `telodendria`: Build the `telodendria` binary. If you specified an alternative `--bin-name`, then this target will be named after that.
- `docs`: Generate the header documentation as `man` pages.
- `tools`: Build the supplemental tools which may be useful for development.
- `clean`: Remove the build and output directories. Telodendria builds are out-of-tree, which greatly simplifies this recipe compared to in-tree builds.
If you're developing Telodendria, these recipes may also be helpful:
- `format`: Format the source code using `indent`. This may require a BSD `indent` because last time I tried GNU `indent`, it didn't like the flags in `indent.pro`. Your mileage may vary.
- `license`: Update the license headers in all source code files with the contents of the `LICENSE.txt`.
To install Telodendria to your system, the following recipes are available:
- `install`: This installs Telodendria under the prefix set with `./configure --prefix=<dir>` or with `make PREFIX=<dir>`. By default, the `make` `PREFIX` is set to whatever was set with `configure --prefix`.
- `uninstall`: Uninstall Telodendria from the same prefix as specified above.
After a build, you can find the object files in `build/` and the output binary in `out/bin/`.
### Pull Requests
> **Note:** Telodendria does not accept GitHub pull requests at this
> time. Please submit your pull requests via Gitea.
Telodendria follows the standard pull request procedures. Once you have
made your changes, committed them, and pushed to your fork, you should
be able to open a pull request on the main repository. When you do, you
will be prompted to write a description. Be sure to include the
related issue that you are closing in your description.
### Code Style
In general, these are the conventions used by the code base. This
guide may be slightly outdated or subject to change, but it should be
a good start. The source code itself is always the absolute source of
truth, so as long as you make your code look like the code surrounding
it, you should be fine.
- All function, enumeration, structure, and header names are
`CamelCase`. This is preferred to `snake_case` because it is more
compact.
- All variable names are `lowerCamelCase`. This is preferred to
`snake_case` because it is more compact. One exception to this rule is
if a variable name, such as a member of a struct, directly represents
a JSON key in an object specified by the Matrix specification, which
may be in `snake_case`.
- Enumerations and structures are always `typedef`-ed to their same
name. The `typedef` should occur in the public API header, and the
actual declaration should live in the implementation file, unless
the enumeration or structure is intended to be made fully public.
- A feature of the code base lives in a single C source file that has a
matching header. The header file should only export public symbols;
everything else in the C source should be static.
- Except where absolutely necessary, global variables are forbidden
to prevent problems with threads and whatnot. Every variable a
function needs should be passed to it either through a structure, or
as a separate argument.
- Anywhere that C allows curly braces to be optional, there still must
be curly braces. This makes it easier to read the code by making it
less ambiguous, and it makes it easier to add on to the code later.
As far as actually formatting the code goes, such as where to put
brackets, and whether or not to use tabs or spaces, use `indent` to
take care of that. The repository contains a `.indent.pro` that should
automatically be loaded by `indent` to set the correct rules. If you
don't have a working `indent`, then just indicate in your pull
request that I should run my `indent` on the code.
### Documentation
This project places a strong emphasis on documentation. Well-documented
code is fundamental to a successful project, so when you are writing
code, please also make sure that it is documented appropriately.
- If you are adding a header, make sure you add the necessary comments
detailing the header and the functions in it.
- If you are adding a function, make sure you add the necessary
comments to the appropriate header.
If your pull request does not also include proper documentation, it
will likely be rejected.
### Be Recognized!
If your pull request gets approved, you should be recognized for your
contributions to the project!
To have your work recognized, add your information to the `CONTRIBUTORS.txt`
file in the root of the Telodendria repository if it isn't there already.
You should do this as a part of your pull request so that when it is merged,
your information will be automatically added to the repository.
The `CONTRIBUTORS.txt` file loosely follows the Linux kernel's
[CREDITS](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/CREDITS)
file format. It is designed to be human-readable, but also parsable by
scripts.
The following fields are available:
```
(N) Name
(E) Email
(M) Matrix ID
(W) Website
(D) Description of contribution
(L) Physical location
```
Here are the rules:
* All fields are optional. If you don't want to include a field, that's
okay, simply omit it.
* All fields identify you however you wish. The goal is to recognize you for
your contribution, but if you wish to remain anonymous, you don't have to
use your real information.
* All fields can be specified multiple times. For example, if you have
multiple email addresses, websites, or Matrix IDs and you want to include
all of them, you absolutely may. Likewise, if you have made multiple
contributions, you can add multiple description entries.
* You can make up your own fields if you want. Just add their description
above.
* Leave exactly one blank like between entries in this file.

View file

@ -1,32 +0,0 @@
# Telodendria Documentation
Here you will find all of the documentation for Telodendria. If you
find that some documentation is missing or incorrect, please open an
issue, and, even better, a pull request to fix the issue.
You are also welcome to join the
[`#telodendria-general:bancino.net`](https://matrix.to/#/#telodendria-general:bancino.net)
matrix room, where you can discuss Telodnedria with others and ask
questions.
## User Documentation
- [System Requirements](user/requirements.md)
- [Install](user/install.md)
- [Usage & Running](user/usage.md)
- [Initial Set Up](user/setup.md)
- [Configuration Options](user/config.md)
- [Administrator API](user/admin/README.md)
## Developer Documentation
- [Repository Structure](dev/repo.md)
- [Contributing Guidelines](CONTRIBUTING.md)
- [Porting Guidelines](dev/ports.md)
## Miscellaneous
- [Matrix Specification](https://spec.matrix.org) ([Mirror](https://telodendria.io/spec.matrix.org))
- [Change Log](CHANGELOG.md)
- [Project Road Map](ROADMAP.md)

View file

@ -1,54 +0,0 @@
# Telodendria Matrix Specification Roadmap
This document provides a high-level overview of Telodendria's roadmap as it pertains to implementing the Matrix specification. Essentially, the Matrix specification is divided up into manageable portions amongst Telodendria releases, so that each release up until the first stable release implements a small portion of it.
**Note:** The first stable release of Telodendria will implement Matrix v1.7, no newer version. The Matrix specification changes too frequently, so I had to just pick a version in order to make this project manageable. Once v1.7 is complete, then we can move on to later specs.
This document will be updated to include more implementation details as they come up. It contains the big picture for far-out releases, and more relevant implementation details for near releases.
## Milestone v0.4.0
- [ ] Client-Server API
- [ ] **7:** Events
- [x] Compute size of JSON object in `CanonicalJson`
- [x] Rename `Sha2.h` to just `Sha.h`; add `Sha1()` function
- [x] Make `Sha256()` just return raw bytes; add function to convert to hex string.
- [ ] **8:** Rooms
- [ ] **9:** User Data
- [x] Profiles
- [ ] Directory
## Milestone v0.5.0
- [ ] Client-Server API
- [ ] Modules
- [ ] Content Repository
## Milestone v0.6.0
- [ ] Client-Server API
- [ ] Modules
- [ ] Instant Messaging
- [ ] Voice over IP
- [ ] Receipts
- [ ] Fully Read Markers
- [ ] Send-To-Device Messaging
- [ ] Security (Rate Limiting)
## Milestone v0.7.0
- [ ] Server-Server API
## Milestone v0.8.0
- [ ] Application Service API
- [ ] YAML parser?
## Milestone v0.9.0
- [ ] Identity Service API
## Milestone v1.7.0 (Stable Release!)
- [ ] Push Gateway API
- [ ] Room Versions

View file

@ -1,94 +0,0 @@
# Hosting Telodendria
These are just my own personal notes for hosting Telodendria's code infrastructure. This document is not intended to be used by normal Telodendria users or developers. It may be useful if you are *forking* Telodendria, but I sincerely hope you'll contribute to the upstream project instead. I'm writing this document solely for my own reference, but I am placing it into Telodendria's code repository in the name of transparency.
## Runners
The general sequence of steps required for setting up a CI runner is as follows:
1. Install the runner OS with all the defaults. I typically install my runners in virtual machines with 1 vcpu and 512mb RAM. Only Debian complained about this configuration, but since I didn't install a desktop environment, it worked out fine.
2. Install the packages required to build and execute the runner. These are:
- Git for checking out the source code.
- NodeJS for running `actions/checkout`, I think. Not really sure, all I know is that the runner will fail all jobs without NodeJS.
- Go for compiling the runner itself.
Run these commands to install the packages:
- **OpenBSD:** `pkg_add git go node`
- **FreeBSD:** `pkg install git go node`
- **NetBSD:** `pkgin install git go nodejs openssl mozilla-rootcerts-openssl`
(Note that the `go` executable is `go121` or whatever version was installed. and that NetBSD has no root certificates installed by default)
- **Debian:** `apt install git golang nodejs`
- **Alpine:** `apk add git go nodejs`
3. Install any development packages required to build Telodendria. For the BSDs, all development tools are built in so no additional packages are necessary. For the Linux distributions I've messed with, install these additional packages:
- **Debian:** `apt install make gcc libssl-dev`
- **Alpine:** `apk add make gcc musl-dev openssl-dev`
4. Clone `https://git.telodendria.io/Telodendria/act_runner.git`.
5. Run `go build` in the `act_runner` directory. On NetBSD, you may have to `umount /tmp` first because `/tmp` is by default very small. Otherwise, make `/tmp` larger during installation. 2GB should be plenty.
6. Run `./act_runner register` to register the runner. When prompted for the tags, follow following convention:
- **Linux Distros:** `linux`, `<distro>-v<version>`, `<arch>`
- **BSD Derivatives:** `bsd`, `<osname>-v<version>`, `<arch>`
- **Windows:** `windows`, `windows-v<version>`, `<arch>`
- **MacOS:** `macos`, `macos-v<version>`, `<arch>`
- **Others:** `other`, `<osname>-v<version>`, `<arch>`
Where `<arch>` is one of `x86` or `x64` for now. ARM runners will be a future project.
7. Run `./act_runner daemon`.
### Startup Scripts
We will obviously want `act_runner` to execute on bootup. Here are the start scripts I used:
#### Alpine
In `/etc/init.d/act_runner`:
```shell
#!/sbin/openrc-run
directory="/home/runner/act_runner"
command="/home/runner/act_runner/act_runner"
command_args="daemon"
command_user="runner:runner"
command_background="true"
pidfile="/run/act_runner.pid"
```
Don't forget to `chmod +x /etc/init.d/act_runner`.
Then just `rc-update add act_runner` and `rc-service act_runner start`.
#### Debian
In `/etc/systemd/system/act_runner.service`:
```
[Unit]
Description=Gitea Actions runner
[Service]
ExecStart=/home/runner/act_runner/act_runner daemon
ExecReload=/bin/kill -s HUP $MAINPID
WorkingDirectory=/home/runner/act_runner
TimeoutSec=0
RestartSec=10
Restart=always
User=runner
[Install]
WantedBy=multi-user.target
```
Then just `systemctl enable act_runner` and `systemctl start act_runner`.
#### Other
Eventually I got sick of writing init scripts for all the various operating systems.
Just put this in `runner`'s `crontab`:
```
@reboot cd /home/runner/act_runner && ./act_runner daemon
```
That seems to do the job good enough, and it's cross platform.

View file

@ -1,92 +0,0 @@
## Ports
Telodendria is distributed primarily as source code, and the project
itself does not offer a convenient install process such as in the form
of a shell script. This is intentional; the Telodendria project is
primarily concerned with developing Telodendria itself, not packaging
it for the hundreds of different operating systems and linux
distributions that exist. It is my firm belief that distributing an
open source project is not the job of the open source developer; that
is the reason software distributions exist: to collect and
*distribute* software.
It would be impossible to single-handedly package Telodendria for
every platform, because each platform has very different expectations
and conventions for software. Even different Linux distributions have
different conventions for where manual pages, binaries, and
configuration files go.
That being said, this document aims to assist those who want to
package Telodendria for their operating system or software
distribution.
---
Before attempting to package Telodendria, make sure that you can build
it and that it builds cleanly on your target platform. See
[Install &rightarrow; From Source](../user/install.md#from-source)
for general build instructions.
To package Telodendria, you should collect the following files, and
figure out where they should be installed on your system:
- The `telodendria` server binary itself.
- An init script. People that wish to install Telodendria on their
system using your package are going to expect it to be integrated
enough that Telodendria can easily be started at boot and otherwise
managed by the system's daemon tools, be it `systemd` or another
init system. Consult your system's documentation for writing an init
script. **Note:** Telodendria *does not* fork itself to the background;
the init script should do that.
- You may also wish to ship the `docs/` directory
so that the user can read the documentation offline, and ensure that
they are reading the correct documentation for the installed version.
You may wish to optionally create a dedicated user under which
Telodendria should run. Telodendria can be directly started as that
user, or start as root and be configured to automatically drop to that
user. Additionally, it might be helpful to provide a default
configuration, which can be placed in the samples directory on your
platform, or in a default location that Telodendria will load from.
A good default directory that you may wish to provide for configuration,
data, and logs could perhaps be `/var/telodendria` or `/var/db/telodendria` on Unix-like systems.
Once you have collected the necessary files and directories that need
to be installed, make sure your package performs the following tasks
on install:
- If necessary and depending on the configuration used, create a new
system user for the Telodnedria daemon to run as.
- If conventional for your system, enable the Telodendria init script
so that Telodendria is started on system boot.
- Instruct the user to carefully read the [Setup](../user/setup.md)
(`docs/user/setup.md`) instructions and the
[Configuration](../user/config.md) (`docs/user/config.md`) instructions
before starting Telodendria.
The goal of a package should be to get everything as ready-to-run as
possible. The user should be able to start Telodendria right away and
begin configuring it.
Remember to publicly document the setup of Telodendria on your platform
if there are additional steps required that are not mentioned in the
official Telodendria documentation. This ensures that users can get
up and running quickly and easily. If you're packaging Telodendria
for a container system such as Docker, you can omit the things that
containers typically do not have, such as the init scripts and
documentation.
Also remember that your port should feel like it belongs on your target
system. Follow all of your system's conventions when placing files
on the filesystem, so your users know what to expect. The goal is not
necessarily to have a unified experience across all operating systems,
rather, you should cater to the opinions of your operating system.
Telodendria is architected in such a way that it does not impose the
developer's opinions of where things should go, and since the
configuration lives in the database, it is fairly self contained.
If there are any changes necessary to the upstream code or build
system that would make your job in porting Telodendria easier, do not
hesitate to get involved by opening an issue and/or submitting a pull
request.

View file

@ -1,109 +0,0 @@
# Rationale
This document seeks to answer the question of "why Telodendria?" from
a technical perspective by comparing it to existing Matrix homservers.
Telodendria is written entirely from scratch in ANSI C. It is designed
to be lightweight, simple, and functional. Telodendria differentiates
itself from other homeserver implementations because it:
- Is written C, a stable, low-level programming language with a long
history, low build and runtime overhead, and wide compatibility.
- Is written with minimalism as a primary design goal. Whenever possible
and practical, no third-party libraries are pulled into the code.
Everything Telodnedria needs is custom written. As a result, Telodendria
depends only on a standard C compiler and a POSIX C library to be
built, both of which should come with any good Unix-style operating
system already, which means you shouldn't have to install anything
additional to use Telodendria.
- Uses a flat-file directory structure to store data instead of a
real database. This has a number of advantages:
- It make setup and mainenance much easier.
- It allows Telodendria to run on systems with fewer resources.
- Is packaged as a single small, statically-linked and highly-optimized
binary that can be run just about anywhere. It is designed to be
extremely easy to set up and consume as few resources as possible.
- Is permissively licensed. Telodendria is licensed under a modified
MIT license, which imposes very few restrictions on what you can do
with it.
## What about [Conduit](https://conduit.rs)?
At this point, you may be wondering why one would prefer Telodendria
over Conduit, a Matrix homeserver that could also say pretty much
everything this document has said so far. After all, Conduit is older
and thus better established, and written in Rust, a Memory Safe&trade;
programming language.
In this section, we will discuss some additional advantages of
Telodendria that Conduit lacks.
### Small Dependency Chain
Conduit's dependency chain is quite large. What this means is that
Conduit depends on a lot of code that it does not control, making it
vulnerable to supply chain attacks. A problem with Rust Crates
is that they are developer-published, so they don't go through any sort
of auditing process like a Debian package would, for example.
If any one of the dependencies is
hijacked or otherwise compromised, then Conduit itself is compromised
and it is likely that this would go unnoticed for quite a while. While
one could argue that this is extremely unlikely to happen, sometimes you
just don't want to take that risk, especially not if you're deploying a
Matrix homeserver, likely for the purpose of secure, private chat.
Telodendria doesn't pull in any packages from developer repositories, so
the risk of supply chain attacks is much lower. It
only uses its own code and code provided by the operating system it is running
on, which has been vetted by a large number of developers and can be trusted
due to the sheer scope of an operating system. A supply chain attack against
Telodendria would be a supply chain attack against the entire operating system;
at that point, end users have much bigger problems.
Minimal dependencies doesn't only mitigate supply chain attacks. It also makes
maintenance much easier. Telodendria can spend more time writing code than
Conduit because Conduit developers have to ensure dependencies stay up to date and
when they inevitably break things, Conduit must pause development to fix those.
Telodendria doesn't suffer from this problem: because most of the code is developed
along side of Telodendria, it can remain as stable or become as volatile as the
developers choose. Additionally, because Telodendria is so low-level, the code on
which it depends is extremely unlikely to be changed in any significant way,
since so many other programs depend on that code.
### Standardized
Conduit is written in Rust, which has no formal standard. This makes it less than
ideal for long-lived software projects, because it changes frequently and often
breaks existing code. Telodendria is written in C, a stable, mature, and standardized
language that will always compile the same code the same way, making it more
portable and sustainable for the future because we don't ever have to worry about
upgrading our toolchain&mdash;using standard tools built into most operating systems
will suffice.
Because the language in which Telodendria is written never changes, Telodendria can
continually optimize and improve the code, instead of having to fix breaking changes.
This ensures that Telodendria's code will last. Rust code becomes obsolete with in a
few years at best&mdash;programs written in Rust last year probably won't compile or run
properly on the latest Rust toolchain. Telodendria, on the other hand, is written in C89,
which compiled and ran the same way in 1989 as it does today and will continue to for the
foreseeable future.
### Fast Compile Times
Rust is well-known for taking an extremely long time to compile moderately-sized
programs. Since a Matrix homeserver is such a large project, the compile times would
be prohibitively large for rapid development. By writing Telodendria in C, we can take
advantage of decades worth of compiler optimizations and speed improvements, resulting
in extremely fast builds.
### Portable
One does not typically think of C as more portable than something like Rust, but
Telodendria is written in such a way that it is. Rust relies on LLVM, which doesn't
support some strange or exotic architectures in the same way that a specialized C
compiler for those architectures will. This allows users to run Telodendria on the
hardware of their choice, even if that hardware is so strange that the modern world
has totally left it behind.
Telodendria doesn't just aim at being lightweight and portable, it aims to empower
people to use common hardware that they already have, even if it is typically thought
of as underpowered.

View file

@ -1,17 +0,0 @@
# Repository Structure
This document describes the filesystem layout of the Telodendria source
code repository.
- `Telodendria/`
- `Cytoplasm/`: The source code for Cytoplasm, Telodendria's
general-purpose support library that provides core functionality.
- `contrib/`: Supplemental files, such as example configurations.
- `docs/`: All user and developer documentation as Markdown.
- `site/`: The official website source code as HTML.
- `src/': The C source code and headers for Telodendria.
- `Routes/`: Where the Matrix API endpoints are implemented.
- `Static/`: Endpoints that just generate static HTML pages.
- `include/`: Header files.
- `tools/`: Development environment and tools.

View file

@ -1,37 +0,0 @@
# Administrator API
Telodendria provides an administrator API as an extension to the
Matrix specification that allows for administrator control over the
homeserver. This includes profiling and examining the state of
running instances, as well as managing users and media.
Like Synapse, Telodendria supports designating specific local users as
administrators. However, unlike Synapse, Telodendria uses a more
fine-grained privilege model that allows a server administrator to
delegate specific administration tasks to other users while not
compromising and granting them full administrative access to the server.
To authenticate with the administrator API, simply use your login
access token just like you would authenticate any other Matrix client
request.
- [Privileges](privileges.md)
- [Configuration](config.md)
- [Server Statistics](stats.md)
- [Process Control](proc.md)
- [Registration Tokens](tokens.md)
## API Conventions
Unless otherwise indicated, HTTP response codes that are not `200 Ok`
will be accompanied by a standard Matrix API error. Consult the Matrix
specification for the format of these errors. The following error
conditions are assumed to be possible for all API endpoints listed
in the Administrator API documentation:
| Response Code | Description |
|---------------|-------------|
| 400 | The user is not authenticated, did not provide a valid JSON object, or provided a JSON object with invalid or missing parameters.|
| 403 | The user does not have the privileges necessary to carry out the requested action.|
| 500 | A fatal server error occurred. Check the logs for more information.|

View file

@ -1,62 +0,0 @@
# Administrator API: Configuration
As mentioned in [Setup](../setup.md), Telodendria's configuration is
intended to be managed via the configuration API. Consult the
[Configuration](../config.md) document for a complete list of supported
configuration options. This document simply describes the API used to
update the configuration described in that document.
## API Endpoints
### **GET** `/_telodendria/admin/v1/config`
Retrieve the current configuration.
| Requires Token | Rate Limited |
|----------------|--------------|
| Yes | Yes |
| Response Code | Description |
|---------------|-------------|
| 200 | The current configuration was successfully retrieved.|
### **POST** `/_telodendria/admin/v1/config`
Installs a new configuration. This endpoint validates the request body,
ensuring it is a proper configuration, then it replaces the existing
configuration with the new one.
| Requires Token | Rate Limited |
|----------------|--------------|
| Yes | Yes |
| Response Code | Description |
|---------------|-------------|
| 200 | The new configuration was successfully installed.|
#### 200 Response Format
| Field | Type | Description |
|-------|------|-------------|
| `restart_required` | `Boolean` | Whether or not the process needs to be restarted to finish applying the configuration. If this is `true`, then the restart endpoint should be used at a convenient time to apply the configuration.
### **PUT** `/_telodendria/admin/v1/config`
Update the currently installed configuration instead of completely replacing it. This endpoint
validates the request body, merges it on top of the current configuration, validates the resulting
configuration, then updates it in the database. This is useful when only one or two properties
in the configuration needs to be changed.
| Requires Token | Rate Limited |
|----------------|--------------|
| Yes | Yes |
| Response Code | Description |
|---------------|-------------|
| 200 | The new configuration was successfully installed.|
#### 200 Response Format
| Field | Type | Description |
|-------|------|-------------|
| `restart_required` | `Boolean` | Whether or not the process needs to be restarted to finish applying the configuration. If this is `true`, then the restart endpoint should be used at a convenient time to apply the configuration.

View file

@ -1,141 +0,0 @@
# Administrator API: Privileges
This document describes the privilege model and the API endpoints that
allow administrators to modify privileges for users.
## List Of Privileges
A local user can have any of the following privileges. Unless otherwise
indicated, these privileges only grant access to certain parts of the
administrator API; the regular Matrix API is unaffected.
- **DEACTIVATE:** Allows a user to deactivate any other local users.
- **ISSUE_TOKENS:** Allows a user to create, modify, and delete
registration tokens.
- **CONFIG:** Allows a user to modify the Telodendria server daemon's
configuration.
- **GRANT_PRIVILEGES:** Allows a user to modify his or her own
privileges or the privileges of other local users.
- **ALIAS:** Allows a user to modify and see room aliases created by
other users. By default, users can only manage their own room aliases,
but an administrator may wish to take over an alias or remove an
offensive alias.
- **PROC_CONTROL:** Allows a user to get statistics on the running
process, as well as shutdown and resetart the Telodendria daemon
itself. Typically this will pair well with **CONFIG**, because there
are certain configuration options that require the process to be
restarted to take full effect.
There is also a special "pseudo-privilege":
- **ALL:** Grants a user all of the aforementioned privileges, as well
as privileges that do not yet exist. That is, if an update to
Telodendria adds more privileges, users with this privilege will
automatically gain those new privileges in addition to having all the
existing privileges. This privilege should only be used with
fully-trusted users. It is typical for a server administrator to not
fully trust anyone else, and be the only one that holds an account with
this privilege level.
## API Endpoints
The following API endpoints are implemented for managing privileges.
### **GET** `/_telodendria/admin/v1/privileges/[localpart]`
Retrieve the permissions for a user. If the localpart is omitted, then
retrieve the privileges for the user that owns the access token being
used. Note that the owner of the access token must have the
**GRANT_PRIVILEGES** privilege to use this endpoint.
| Requires Token | Rate Limited |
|----------------|--------------|
| Yes | Yes |
| Response Code | Description |
|---------------|-------------|
| 200 | The privileges were successfully retrieved.|
#### 200 Response Format
| Field | Type | Description |
|-------|------|-------------|
| `privileges` | `Array` | An array of privileges, as described above. The privileges are encoded as JSON strings.|
### **POST** `/_telodendria/admin/v1/privileges/[localpart]`
Update the privileges of a local user by replacing the privileges array
with the one specified in the request. Like the **GET** version of this
endpoint, the localpart can be omitted to operate on the user that
owns the access token.
| Requires Token | Rate Limited |
|----------------|--------------|
| Yes | Yes |
| Response Code | Description |
|---------------|-------------|
| 200 | The privileges were successfully replaced.|
#### Request Format
| Field | Type | Description |
|-------|------|-------------|
| `privileges` | `Array` | An array of privileges, as described above. The privileges are encoded as JSON strings.|
#### 200 Response Format
| Field | Type | Description |
|-------|------|-------------|
| `privileges` | `Array` | An array of privileges, as described above. The privileges are encoded as JSON strings.|
### **PUT** `/_telodendria/admin/v1/privileges/[localpart]`
Update the privileges of a local user by adding the privileges
specified in the request to the users existing privileges.
| Requires Token | Rate Limited |
|----------------|--------------|
| Yes | Yes |
| Response Code | Description |
|---------------|-------------|
| 200 | The privileges were successfully added.|
#### Request Format
| Field | Type | Description |
|-------|------|-------------|
| `privileges` | `Array` | An array of privileges, as described above. The privileges are encoded as JSON strings.|
#### 200 Response Format
| Field | Type | Description |
|-------|------|-------------|
| `privileges` | `Array` | An array of privileges, as described above. The privileges are encoded as JSON strings.|
### **DELETE** `/_telodendria/admin/v1/privileges/[localpart]`
Update the privileges of a local user by removing the privileges
specified in the request from the user's existing privileges.
| Requires Token | Rate Limited |
|----------------|--------------|
| Yes | Yes |
| Response Code | Description |
|---------------|-------------|
| 200 | The privileges were successfully removed.|
#### Request Format
| Field | Type | Description |
|-------|------|-------------|
| `privileges` | `Array` | An array of privileges, as described above. The privileges are encoded as JSON strings.|
#### 200 Response Format
| Field | Type | Description |
|-------|------|-------------|
| `privileges` | `Array` | An array of privileges, as described above. The privileges are encoded as JSON strings.|

View file

@ -1,49 +0,0 @@
# Administrator API: Process Control
This document describes the administrator APIs that allow a server
administrator to manage the Telodendria process itself.
## API Endpoints
### **POST** `/_telodendria/admin/v1/restart`
Restart the Telodendria daemon cleanly. This endpoint will respond
immediately after signaling to the daemon that it should be restarted
as soon as possible. Note that the restart wmay not happen
instantaneously, as Telodendria will finish processing all current
requests before restarting. Also note that this is not a true restart;
the process does not exit and restart, rather, Telodendria simply tears
down all its state and then jumps back to the beginning of its code and
starts over.
| Requires Token | Rate Limited |
|----------------|--------------|
| Yes | Yes |
| Response Code | Description |
|---------------|-------------|
| 200 | The restart request was successfully sent.|
On success, this endpoint simply returns an empty JSON object.
### **POST** `/_telodendria/admin/v1/shutdown`
Shut down the Telodendria process cleanly. This endpoint will respond
immediately after signalling to the daemon that it should be shut
down as soon as possible. Note that the shutdown may not happen
instantaneously, as Telodendria will finish processing all current
requests before shutting down. Also note that once shut down, Telodendria
may be automatically restarted by the system's service manager.
Otherwise, it will have to be manually restarted. This is a true
shutdown; the Telodendria process exits as soon as possible.
| Requires Token | Rate Limited |
|----------------|--------------|
| Yes | Yes |
| Response Code | Description |
|---------------|-------------|
| 200 | The shutdown request was successfully sent.|
On success, this endpoint simply returns an empty JSON object.

View file

@ -1,26 +0,0 @@
# Administrator API: Server Statistics
The administrator API allows users with the proper privileges to get
information about how the server process is performing.
## API Endpoints
### **GET** `/_telodendria/admin/v1/stats`
Retrieve basic statistics about the currently running Telodendria
process.
| Requires Token | Rate Limited |
|----------------|--------------|
| Yes | Yes |
| Response Code | Description |
|---------------|-------------|
| 200 | The server statistics were successfully retrieved.|
#### 200 Response Format
| Field | Type | Description |
|-------|------|-------------|
| `memory_allocated` | `Integer` | The total amount of memory allocated, measured in bytes.|
| `version` | `String` | The current version of Telodendria.|

View file

@ -1,106 +0,0 @@
# Administrator API: Registration Tokens
Telodendria implements registration tokens as specified by the Matrix
specification. These tokens can be used for registration using the
`m.login.registration_token` login type. This API provides a Telodendria
administrator with a mechanism for generating and managing these tokens,
which allows controlled registration on the homeserver.
It is generally safer than completely open registration to use
registration tokens that either expire after a short period of time, or
have a limited number of uses.
## Registration Token
A registration token is represented by the following `RegToken` JSON
object:
| Field | Type | Description |
|-------|------|-------------|
| `name` | `String` | The token identifier; what is used when registering. |
| `created_by` | `String` | The localpart of the user that created this token. |
| `created_on` | `Integer` | A timestamp of when the token was created. |
| `expires_on` | `Integer` | An expiration stamp, or 0 if the token never expires. |
| `used` | `Integer` | The number of times the token has been used. |
| `uses` | `Integer` | The total number of allowed uses, or -1 for unlimited. |
| `grants` | `[String]` | An array of privileges to grant users that register with this token as described in [Privileges](privileges.md). |
All endpoints in this API will operate on some variation of this
structure. The remaining number of uses can be computed by performing
the subtraction: `uses - used`. `used` should never be greater than
`uses` or less than `0`.
Example:
```json
{
"name": "q34jgapo8uq34hg",
"created_by": "admin",
"created_on": 1699467640000,
"expires_on": 0,
"used": 3,
"uses": 5
}
```
## API Endpoints
### **GET** `/_telodendria/admin/v1/tokens`
Get a list of all registration tokens and information about them.
#### 200 Response Format
| Field | Type | Description |
|-------|------|-------------|
| `tokens` | `[RegToken]` | An array of registration tokens. |
### **GET** `/_telodendria/admin/v1/tokens/[name]`
Get information about the specified registration token.
#### Request Parameters
| Field | Type | Description |
|-------|------|-------------|
| `name` | `String` | The name of the token, as it would be used to register a user. |
#### 200 Response Format
This endpoint returns a `RegToken` object that represents the server's
record of the registration token.
### **POST** `/_telodendria/admin/v1/tokens`
Create a new registration token.
#### Request Format
This endpoint accepts a `RegToken` object, as described above. If no
`name` is provided, one will be randomly generated. Note that the fields
`created_by`, `created_on`, and `used` are ignored and set by the server
when this request is made. All other fields may be set by the request
body.
#### 200 Response Format
If the creation of the registration token was successful, a `RegToken`
that represents the server's record of it is returned.
### **DELETE** `/_telodendria/admin/v1/tokens/[name]`
Delete the specified registration token. It will no longer be usable for
the registration of users. Any users that have completed the
`m.login.registration_token` step but have not yet created their account
should still be able to do so until their user-interactive auth session
expires.
#### Request Parameters
| Field | Type | Description |
|-------|------|-------------|
| `name` | `String` | The name of the token, as it would be used to register a user. |
#### 200 Response Format
On success, this endpoint returns an empty JSON object.

View file

@ -1,248 +0,0 @@
# Configuration
Telodendria is designed to be configurable. It is configured using
JSON, which is intended to be submitted to the [Administrator
API](admin/README.md). This document details Telodendria's configuration
JSON format, which is used in both the administrator API and on-disk
in the database. The configuration file on the disk in the databsae
is `config.json`, though that file should not be edited by hand.
Use the API described in
[Administrator API &rightarrow; Configuration](admin/config.md).
## JSON Format
Telodendria's configuration is just a JSON object in the standard
key-value form:
```json
{
"serverName": "telodendria.io",
"listen": [
{
"port": 8008
}
]
/* ... */
}
```
Some keys, called *directives* in this document, have values that are
objects themselves.
## Directives
Here are the top-level directives:
- **listen:** `Array`
An array of listener description objects. Telodendria supports
listening on multiple ports, and each port is configured
independently of the others. A listener object looks like this:
- **port:** `integer`
The port to listen on. Telodendria will bind to all interfaces,
so it is recommended to configure your firewall to only allow
access on the desired interfaces. Note that Telodendria offers all
APIs over each port, including the administrator APIs; there is no
way to control which APIs are made available over which ports. If
this is a concern, a reverse-proxy such as `relayd` can be placed
in front of Telodendria to block access to undesired APIs.
- **tls:** `Object`
Telodendria can be compiled with TLS support. If it is, then a
particular listener can be set to use TLS for connections. If
**tls** is not `null` or `false`, then it can be an object with
the following directives:
- **cert:** `String`
The full path&mdash;or path relative to the data
directory&mdash;of the certificate file to load. The certificate
file should be in the format expected by the platform's TLS
library.
- **key:** `String`
Same as **cert**, but this should be the private key that matches
the certificate being used.
- **threads:** `Integer`
How many worker threads to spin up to handle requests for this
listener. This should generally be less than the total CPU core
count, to prevent overloading the system. The most efficient number
of threads ultimately depends on the configuration of the machine
running Telodendria, so you may just have to play around with
different values here to see which gives the best performance.
Note that this can be set as low as 0; in that case, the listener
will never respond to requests. Each listener needs to have at
least one thread to be useful. Also note that Telodendria may spin
up additional threads for background work, so the actual total
thread count at any given time may exceed the sum of threads
specified in the configuration.
This directive is optional. The default value is `4` in the upstream
code, but your software distribution may have patched this to be
different.
- **maxConnections:** `Integer`
The maximum number of simultanious connections to allow to the
daemon. This option prevents the daemon from allocating large
amounts of memory in the event that it undergoes a denial of
service attack. It is optional, defaults to `32`, and typically
does not need to be adjusted.
- **serverName:** `String`
Configure the domain name of your homeserver. Note that Matrix
servers cannot be migrated to other domains, so once this is set,
it should never change unless you want unexpected things to happen
or you want to start over. **serverName** should be a DNS name that
can be publicly resolved. This directive is required.
- **pid:** `String`
Configure the file Telodendria writes its PID to.
- **baseUrl:** `String`
Set the server's base URL. **baseUrl** should be a valid URL,
complete with the protocol. It does not need to be the same as the
server name; in fact, it is common for a subdomain of the server name
to be the base URL for the Matrix homeserver.
This URL is the URL at which Matrix clients will connect to the
server, and is thus served as a part of the `.well-known`
manifest.
This directive is optional. If unspecified, it is automatically
deduced from the server name.
- **identityServer:** `String`
The identity server that clients should use to perform identity
lookups. **identityServer** folows the same rules as **baseUrl**.
It also is optional, and is set to be the same as the **baseUrl**
if left unspecified.
- **runAs:** `Object`
The effective Unix user and group to drop to after binding to the
socket and completing any setup that may potentially require
elevated privileges. This directive only takes effect if
Telodendria is started as the root user, and is used as a security
mechanism. If this option is set and Telodendria is started as a
non-privileged user, then a warning is printed to the log if that
user and group do not match what's specified here. This directive
is optional, but should be used as a sanity check even if not
running as `root`, just to make sure you have your permissions
working properly.
This directive takes an object with the following directives:
- **uid:** `String`
The Unix username to switch to. If **runAs** is specified, this
directive is required.
- **gid:** `String`
The Unix group to switch to. This directive is optional; if left
unspecified, then the value of **uid** is copied.
- **federation:** `Boolean`
Whether or not to enable federation with other Matrix homeservers.
Matrix by its very nature is a federated protocol, but if you just
want to rn your own internal chat server with no contact with the
outside, then you can use this option to disable federation. It is
highly recommended to set this to `true`, however, if you wish to
be able to communicate with users on other Matrix servers. This
directive is required.
- **registration:** `Boolean`
Whether or not to enable new user registration or not. For security
and anti-spam reasons, you can set this to `false`. If you do, you
can still allow only certain users to be registered using
registration tokens, which can be managed via the administrator API.
This directive is required.
In an ideal world, everyone would run their own Matrix homeserver,
so no public registration would ever be required. Unfortunately,
not everyone has the means to run their own homeserver, especially
because of the fact that IPv4 addresses are becoming increasingly
hard to come by. If you would like to provide a service to those
that are unable to run their homeserver, then set this to `true`,
thereby allowing anyone to create an account.
Telodendria *should* be capable of handling a large amount of users
without difficulty, but it is targetted at smaller deployments.
- **log:** `Object`
The logging configuration. Telodendria uses its own logging
facility, which can output logs to standard output, a file, or the
syslog. This directive is required, and it takes an object with the
following directives:
- **output:** `Enum`
The log output destination. This can either be `stdout`, `file`,
or `syslog`. If set to `file`, Telodendria will log to
`telodendria.log` inside the data directory.
- **level:** `Enum`
The level of messages to log. Each level shows all the levels above
it. The levels are as follows:
- `error`
- `warning`
- `notice`
- `message`
- `debug`
For example, setting the level to `error` will show only errors,
while setting the level to `warning` will show both warnings
*and* errors. The `debug` level shows all messages.
- **timestampFormat:** `Enum`
If you want to customize the timestamp format shown in the log,
or disable it altogether, you can do so via this option. Acceptable
values are `none`, `default`, or a formatter string as described
by your system's `strftime()` documentation. This option only
applies if **log** is `stdout` or `file`.
- **color:** `Boolean`
Whether or not to enable colored output on TTYs. Note that ANSI
color sequences will not be written to a log file, only a real
terminal, so this option only applies if the log is being written
to a standard output which is connected to a terminal.
- **maxCache:** `Integer`
The maximum size of the cache. Telodendria relies heavily on caching
for performance reasons. The cache grows as data is loaded from the
data directory. All cache is stored in memory. This option limits the
size of the memory cache. If you have a system with a lot of memory
to spare, you'll get better performance if this option is set higher.
Otherwise, this value should be lowered on systems that have a
minimal amount of memory available.
## Examples
A number of example configuration files are shipped with Telodendria's
source code. They can be found in the `contrib/` directory if you are
viewing the source code directly. Otherwise, if you installed
Telodendria from a package, it is possible that the example
configurations were placed in the default locations for such files on
your operating system.

View file

@ -1,56 +0,0 @@
# Installation
There are multiple methods of installing Telodendria. Choose the one
best suited to your use case.
## Package Manager Or System Ports
This is the recommended way to install Telodendria. If your operating
system has an official package or port of Telodendria, you should
prefer to use that instead of the other methods documented here,
because your operating system or software distribution will have
already figured out how to best integrate Telodendria with your system.
Consult your operating system or software distribution's system
manual for instructions on how to install packages. Also consult the
official repository of your distribution to see if a package is
available. If a package exists but it is too out of date for your
tastes, please contact the package's maintainer to notify them, or
offer to update the package yourself using the
[porting instructions](../dev/ports.md).
If you are maintaining a port or package for an operating system or
software distribution, open a pull request to include your
platform-specific instructions as a subheader of this section.
Eventually, this section should contain basic instructions for the
operating systems that have packages or ports.
See [Ports](../dev/ports.md) for the project's distribution
philosophy.
## Container
At this time, Telodendria does not have any officially recommended
procedure for running in a container such as Docker or Vagrant. You
may find helpful files in the [`contrib/`](../../contrib) directory,
however.
If you are publishing container images, please open a pull request to
add your source files to `contrib/`, as well as to add documentation
under this section explaining how to get set started.
## Release Binary
At this time, Telodendria does not publish any official binaries that
can be downloaded. The tentative plan is to eventually provide binaries
with each release for a number of supported platforms. When that
happens, instructions will be provided here for dealing with the
binaries.
## From Source
If you would like to build Telodendria from source, you can download
the latest release code from the
[Releases](/Telodendria/telodendria/releases) page. After extracting
the tarball, read
[Contributing &rightarrow; Developing &rightarrow; Building &amp; Running](../CONTRIBUTING.md#building-amp-running)
for details on how to build Telodendria.

View file

@ -1,60 +0,0 @@
# Initial Set Up
While Telodendria strives to be extremely simple to deploy and run,
in most circumstances a few basic setup steps will be necessary.
Telodendria does not have a traditional configuration file like most
daemons. Instead, its configuration lives in its database; as such,
all configuration happens through the administrator API. This design
decision makes Telodendria extremely flexible, because it is possible
to re-configure Telodendria without having to manually edit files on
the filesystem, thus allowing administrators to secure their server
better.
Please follow the instructions followed here carefully in the order
they are presented for the best results.
This document assumes that you have installed Telodendria using any
of the instructions found in [Install](install.md). After installation,
follow these steps:
1. Start Telodendria. If you installed it via a package or container,
consult your operating system or container system's documentation. If
you are running Telodendria from a release binary or have built it from
source, execute the binary directly. If needed, consult the
[Usage](usage.md) page for details on how to run Telodendria.
1. Assuming that Telodendria started properly, it will spin up and
initialize its database directly with a simple&mdash;and, importantly,
safe&mdash;default configuration, as well as a randomly generated,
single-use registration token that grants a user all privileges
documented in the [Administrator API](admin/README.md) documentation.
Consult the log file for this administrator registration token. By
default, the log file is located in the data directory, and is named
`telodendria.log`.
1. Use the registration token to register for an account on the
server. This account will be the administrator account. You can do this
using the client of your choice, or using tools such as `curl` or
`http`, following the Matrix specification for registering accounts.
The administrator account behaves just like a normal local account
that an ordinary user would have registered on the server, except that
it also has all privileges granted to it, so it can make full use of
the Administrator API.
1. Using the access token granted for the administrator account via
the login process, configure Telodendria as descibed in
[Configuration](config.md). See the [Administrator API](admin/README.md)
documentation for the configuration endpoint details.
This is the recommended way to set up Telodendria. However, if you
wish to bypass the account creation step and want to configure
Telodendria by directly writing a configuration file instead of using
the administrator API, you can manually create the configuration file
in the database before starting Telodendria. Simply create `config.json`
following the description in [Configuration](config.md), then start
Telodendria.
While this alternative method may seem simpler and more convenient
to some administrators, it is only so in the short-term. Note that this
method is not supported, because it gives no access to the
administrator API whatsoever, unless you manually modify the database
further to give a user admin privileges, which is error-prone and
bypasses some of Telodendria's safety mechanisms.

View file

@ -1,64 +0,0 @@
# Usage
This document provides general documentation on how to use the
`telodendria` server binary, as well as details on how it behaves.
The details here will be useful for setting up init systems, running
Telodendria in a container, or manually executing the binary for
testing or debugging purposes.
## Command Line Options
Typically, Telodendria is controlled via the
[Administrator API](admin/README.md), but the Telodendria binary does include
a few command line options, which can be used in init scripts or for
debugging purposes.
The command line arguments are as follows:
- **`-d <dir>`** Specify the data directory to use. All persistent
storage that Telodendria requires is saved to and loaded from here.
- **`-V`** Only print the version information header and then quit
with a success exit code.
- **`-v`** Verbose mode. This overrides the configuration and sets the
log level to `debug`. It also enables additional logging of memory
operations, which can be useful for debugging.
Before proposing additional command line arguments, consider whether or
not the functionality requested can be provided via a (potentially new
and as of yet uncreated) administrator API endpoint.
## Environment
Telodendria does not read any environment variables. All configuration
should be done via the [Configuration API](config.md).
## Signals
Telodendria recognizes and responds to a number of signals:
- **`PIPE`:** This signal is ignored, because all I/O errors should
already be handled properly.
- **`USR1`:** Perform a soft restart by shutting down the HTTP servers
and resetting the program state. Note that the daemon process does
not exit.
- **`TERM`:** Perform a clean shutdown after all existing connections
are closed.
- **`INT`:** Same as `TERM`.
Any other signals are not explicitly handled, so they have the
default behavior as defind by the operating system.
## Exit Status
Telodendria exits with a non-0 exit code if the configuration file is
invalid, or one or more of required paths or files is inaccessible.
Telodendria will print an error to the log and then terminate
abnormally.
Telodendria exits with a code of 0 if the configuration file is valid,
all paths and files required are accessible, and the HTTP listener
starts as intended. If Telodendria is sent a signal that it catches
after it begins servicing requests, it will still exit normally after
it safely shuts down, because the bootstrap process completed
successfully, and by all accounts, it ran normally and exitted
normally.

57
make.sh Normal file
View file

@ -0,0 +1,57 @@
#!/usr/bin/env sh
TELODENDRIA_VERSION="0.0.1"
HEADERS="-D_POSIX_C_SOURCE=199506L -DTELODENDRIA_VERSION=\"$TELODENDRIA_VERSION\""
INCLUDES="-Isrc/include"
CC="${CC:-cc}"
CFLAGS="-Wall -Werror -pedantic -std=c89 -O3 $HEADERS $INCLUDES"
LDFLAGS="-static -flto -fdata-sections -ffunction-sections -s -Wl,-static -Wl,-gc-sections"
PROG="telodendria"
mod_time() {
if [ -n "$1" ] && [ -f "$1" ]; then
case "$(uname)" in
Linux)
stat -c %Y "$1"
;;
*BSD)
stat -f %m "$1"
;;
*)
echo "0"
;;
esac
else
echo "0"
fi
}
mkdir -p build
do_rebuild=0
objs=""
for src in $(find src -name '*.c'); do
obj=$(echo "$src" | sed -e 's/^src/build/' -e 's/\.c$/\.o/')
objs="$objs $obj"
if [ $(mod_time "$src") -gt $(mod_time "$obj") ]; then
echo "CC $obj"
obj_dir=$(dirname "$obj")
mkdir -p "$obj_dir"
if ! $CC $CFLAGS -c -o "$obj" "$src"; then
exit 1
fi
do_rebuild=1
fi
done
if [ $do_rebuild -eq 1 ] || [ ! -f "build/$PROG" ]; then
echo "LD build/$PROG"
$CC $LDFLAGS -o "build/$PROG" $objs
else
echo "Up to date."
fi
ls -lh "build/$PROG"

1
man/.cvsignore Normal file
View file

@ -0,0 +1 @@
mandoc.db

View file

@ -1,18 +0,0 @@
.Dd $Mdocdate: May 6 2023 $
.Dt HTTP-DEBUG-SERVER 1
.Os Telodendria Project
.Sh NAME
.Nm http-debug-server
.Nd A simple HTTP server that logs requests to the standard output.
.Sh DESCRIPTION
.Pp
.Nm
spins up an HTTP server, listening on port 8008, in the exact same
manner as Telodendria itself. Any request it receives is written to
the standard output, and an empty JSON object is returned to the
client.
.Pp
This command exists just to test the HTTP server API during
development. It probably serves no other practical purpose.
.Sh SEE ALSO
.Xr HttpServer 3

View file

@ -1,111 +0,0 @@
.Dd $Mdocdate: March 12 2023 $
.Dt JSON 1
.Os Telodendria Project
.Sh NAME
.Nm json
.Nd A simple command line utility for parsing and generating JSON.
.Sh SYNOPSIS
.Nm
.Op Fl s Ar query
.Op Fl e Ar str
.Sh DESCRIPTION
.Nm
is a simple command line utility for dealing with JSON. It is
somewhat inspired by
.Xr jq 1 ,
but is not compatible in any way with it.
.Nm
is designed to be much simpler than
.Xr jq 1 ,
and is built on Telodendria's own
.Xr Json 3
API. It primarily exists to ease development of Telodendria, and
to make development possible without having to install any external
tools.
.Pp
The options are as follows. Unless stated otherwise, these options
are mutually exclusive, and the last one specified takes precedence.
All positional parameters are ignored.
.Bl -tag -width Ds
.It Fl s Ar query
Use
.Va query
to query a field from a JSON object given on the standard input.
The query syntax very vaguely resembles C code, but it is much
more primitive. Multiple queries are separated by an arrow
(``->''). This makes it trivial to drill down into nested
objects and arrays.
.Pp
To select a value from an object, just specify the key. To select
an element from an array specify the key whose value is the array,
and then use the C square bracket syntax to select an element.
.Pp
A number of ``functions'' exist to make
.Nm
more versatile. Functions are called by prefacing the key with
a ``@'' symbol. Functions can appear anywhere in the query, provided
they make sense within the context of the JSON object being processed.
The available functions are as follows:
.Bl -tag -width Ds
.It keys
When applied to an object, outputs an array of keys.
.It length
When applied to an array, outputs the number of elements in the
array. When applied to a string, returns the number of bytes
needed to store the decoded version of the string.
.It decode
When applied to a string, outputs the decoded version of the
string.
.El
.Pp
When a key is prefaced with the ``^'' symbol, then instead of getting
the value at that key, it is removed from the object, and the new
object is passed along.
.It Fl e Ar str
Encode
.Va str
as a JSON string and print it to standard output. This is useful for
generating JSON with shell scripts.
.El
.Pp
If no options are specified, then the default behavior of
.Nm
is to read a JSON object given on the standard input and pretty-print
it to the standard output, or print an error to standard error if
the given input is invalid.
.Sh EXAMPLES
.Pp
Get the error string of an error returned by a Matrix API endpoint:
.Bd -literal -offset indent
json -s 'error->@decode'
.Ed
.Pp
Get the number of stages in the first flow listed in a list
of user-interactive authentication flows:
.Bd -literal -offset indent
json -s 'flows[0]->stages->@length'
.Ed
.Pp
Get the first stage of the first flow listed in a list
of user-interactive authentication flows:
.Bd -literal -offset indent
json -s 'flows[0]->stages[0]->@decode'
.Ed
.Pp
List the keys in a JSON object:
.Bd -literal -offset indent
json -s '@keys'
.Ed
.Pp
Get the number of keys in a JSON object:
.Bd -literal -offset indent
json -s '@keys->@length'
.Ed
.Sh EXIT STATUS
.Nm
exits with
.Va EXIT_SUCCESS
if all command line options were valid and the given JSON object
parses successfully. It exits with
.Va EXIT_FAILURE
in all other scenarios.

View file

@ -1,34 +0,0 @@
.Dd $Mdocdate: April 29 2023 $
.Dt TT 1
.Os Telodendria Project
.Sh NAME
.Nm tt
.Nd Make authenticated Matrix requests.
.Sh SYNPOSIS
.Nm
.Op request-path
.Sh DESCRIPTION
.Nm
is an extremely simple wrapper over
.Xr http 1
and
.Xr json 1
that automatically registers a user and continues to log in as that
user. It obtains an access token that it uses to authenticate the
given request, and then logs out when the request has completed. It
also doesn't require the whole URL to be typed; only the path name
is needed.
.Sh ENVIRONMENT
.Bl -tag
.It Ev METH
The request method to use for the user-specified request.
.It Ev DATA
The data to pass to
.Xr http 1 .
This can either be a string, or a file. Consult the
.Xr http 1
page for details.
.El
.Sh SEE ALSO
.Xr http 1 ,
.Xr json 1

114
man/man3/Array.3 Normal file
View file

@ -0,0 +1,114 @@
.Dd $Mdocdate: September 30 2022 $
.Dt ARRAY 3
.Os Telodendria Project
.Sh NAME
.Nm Array
.Nd A simple dynamic array data structure.
.Sh SYNOPSIS
.In Array.h
.Ft Array *
.Fn ArrayCreate "void"
.Ft void
.Fn ArrayFree "Array *"
.Ft int
.Fn ArrayTrim "Array *"
.Ft size_t
.Fn ArraySize "Array *"
.Ft void *
.Fn ArrayGet "Array *" "size_t"
.Ft int
.Fn ArrayInsert "Array *" "void *" "size_t"
.Ft int
.Fn ArrayAdd "Array *" "void *"
.Ft void *
.Fn ArrayDelete "Array *" "size_t"
.Ft void
.Fn ArraySort "Array *" "int (*) (void *, void *)"
.Sh DESCRIPTION
These functions implement a simple array data structure that
is automatically resized as necessary when new values are added.
This implementation does not actually store the values of the
items in it; it only stores pointers to the data. As such, you will
still have to manually maintain all your data. The advantage of this
is that these functions don't have to copy data, and thus don't care
how big the data is. Furthermore, arbitrary data can be stored in the
array.
.Pp
This array implementation is optimized for storage space and appending.
Deletions are expensive in that all the items of the list above a deletion
are moved down to fill the hole where the deletion occurred. Insertions are
also expensive in that all the elements above the given index must be shifted
up to make room for the new element.
.Pp
Due to these design choices, this array implementation is best suited to
linear writing, and then linear or random reading.
.Pp
These functions operate on an array structure which is opaque to the
caller.
.Pp
.Fn ArrayCreate
and
.Fn ArrayFree
allocate and deallocate an array, respectively.
Note that
.Fn ArrayFree
does not free any of the values stored in the array; it is the caller's
job to manage the memory for each item. Typically, the caller would
iterate over all the items in the array and free them before freeing
the array.
.Fn ArrayTrim
reduces the amount of unused memory by calling
.Xr realloc 3
on the internal structure to perfectly fit the elements in the array. It
is intended to be used by functions that return relatively read-only arrays
that will be long-lived.
.Pp
.Fn ArrayInsert
and
.Fn ArrayDelete
are the main functions used to modify the array.
.Fn ArrayAdd
is a convenience method that simply appends a value to the end of the
array. It uses
.Fn ArrayInsert .
The array can also be sorted by using
.Fn ArraySort ,
which takes a pointer to a function that compares elements. The function
should take two
.Dv void
pointers as parameters, and return an integer. The return value indicates
to the algorithm how the elements relate to each other. A return value of
0 indicates the elements are identical. A return value greater than 0
indicates that the first item is "bigger" than the second item and should
thus appear after it in the array, and a value less than zero indicates
the opposite: the second element should appear after the first in the array.
.Pp
.Fn ArrayGet
is used to get the element at the specified index.
.Sh RETURN VALUES
.Fn ArrayCreate
returns a pointer on the heap to a newly allocated array structure, or
.Dv NULL
if the allocation fails.
.Pp
.Fn ArrayGet
and
.Fn ArrayDelete
return pointers to values that were put into the array, or
.Dv NULL
if the provided array is
.Dv NULL
or the provided index was out of bounds.
.Fn ArrayDelete
returns the element at the specified index after removing it so that
it can be properly handled by the caller.
.Pp
.Fn ArrayTrim ,
.Fn ArrayInsert ,
and
.Fn ArrayAdd
return a boolean value indicating their status. They return a value of zero
on failure, and a non-zero value on success.
.Sh SEE ALSO
.Xr HashMap 3 ,
.Xr Queue 3

81
man/man3/Base64.3 Normal file
View file

@ -0,0 +1,81 @@
.Dd $Mdocdate: September 30 2022 $
.Dt BASE64 3
.Os Telodendria Project
.Sh NAME
.Nm Base64
.Nd A simple base64 encoder/decoder with "unpadded base64" support.
.Sh SYNOPSIS
.In Base64.h
.Ft size_t
.Fn Base64EncodedSize "size_t"
.Ft size_t
.Fn Base64DecodedSize "const char *" "size_t"
.Ft char *
.Fn Base64Encode "const char *" "size_t"
.Ft char *
.Fn Base64Decode "const char *" "size_t"
.Ft void
.Fn Base64Unpad "char *" "size_t"
.Ft int
.Fn Base64Pad "char **" "size_t"
.Sh DESCRIPTION
This is an efficient yet simple base64 encoding and decoding
library that supports regular base64, as well as the Matrix
specification's extension to base64, called "unpadded base64."
.Nm provides the ability to convert between the two, instead of
just implementing "unpadded base64."
.Pp
.Fn Base64EncodedSize
and
.Fn Base64DecodedSize
compute the amount of characters needed to store an encoded or
decoded message, respectively. Both functions take the size of
the message being encoded or decoded, but
.Fn Base64DecodedSize
also takes a pointer to an encoded string, because a few bytes of
the string need to be read in order to compute the size.
.Pp
.Fn Base64Encode
and
.Fn Base64Decode
do the actual work of encoding and decoding base64 data. They both
take a string and its length.
.Pp
.Fn Base64Unpad
and
.Fn Base64Pad
are used to implement Matrix unpadded base64.
.Fn Base64Unpad
takes a valid base64 string and strips off the trailing equal signs,
as per the specification.
.Fn Base64Pad
does the opposite; it takes an unpadded base64 input string, and pads
it with equal signs as necessary, so that it can be properly decoded
with
.Fn Base64Decode
if necessary. However, the Matrix specification envisons unpadded base64
as opaque; that is, once it's encoded, it never needs to be decoded.
In practice, a homeserver might need to decode an unpadded base64 string.
.Sh RETURN VALUES
.Fn Base64EncodedSize
and
.Fn Base64DecodedSize
simply return unsigned integers representing a number of bytes generated
from a simple computation.
.Pp
.Fn Base64Encode
and
.Fn Base64Decode
return pointers to new strings, allocated on the heap, or
.Dv NULL
if a heap allocation fails. These pointers must be
.Xr free 3 -ed
at some point when they are no longer needed.
.Pp
.Fn Base64Unpad
modifies the passed string in-place. It thus has no return value, because
it cannot fail. If the passed pointer is invalid, the behavior is undefined.
.Fn Base64Pad
returns a boolean value indicating whether the pad operation was successful.
In practice, this function will only fail if a bigger string is necessary, but
could not be automatically allocated on the heap.

146
man/man3/HashMap.3 Normal file
View file

@ -0,0 +1,146 @@
.Dd $Mdocdate: October 11 2022 $
.Dt HASHMAP 3
.Os Telodendria Project
.Sh NAME
.Nm HashMap
.Nd A simple hash map implementation.
.Sh SYNOPSIS
.In HashMap.h
.Ft HashMap *
.Fn HashMapCreate "void"
.Ft void
.Fn HashMapFree "HashMap *"
.Ft void
.Fn HashMapLoadSet "HashMap *" "float"
.Ft void
.Fn HashMapFunctionSet "HashMap *" "unsigned long (*) (const char *)"
.Ft void *
.Fn HashMapSet "HashMap *" "char *" "void *"
.Ft void *
.Fn HashMapGet "HashMap *" "char *"
.Ft void *
.Fn HashMapDelete "HashMap *" "char *"
.Ft int
.Fn HashMapIterate "HashMap *" "char **" "void **"
.Sh DESCRIPTION
This is the public interface for Telodendria's hash map implementation.
This hash map is designed to be simple, well documented, and generally
readable and understandable, yet also performant enough to be useful,
because it is used extensively in Telodendria.
.Pp
Fundamentally, this is an entirely generic map implementation. It can
be used for many general purposes, but it is designed to only implement
the features that Telodendria needs to function well. One example of a
Telodendria-specific feature is that keys are
.Dv NULL -terminated
strings, not any arbitrary data.
.Pp
These functions operate on an opaque
.Dv HashMap
structure, which the caller has no knowledge about.
.Pp
.Fn HashMapCreate
creates a new hash map that is ready to be used with the rest of the
hash map functions.
.Fn HashMapFree
frees this hash map, such that it becomes invalid and any future use
with the functions in this API results in undefined behavior. Note that
it does not free the keys or values stored in the hash map, since this
implementation has no way of knowing what is actually stored in it, and
where it is located. You should use
.Fn HashMapIterate
to free the values appropriately.
.Pp
.Fn HashMapMaxLoadSet
controls the maximum load of the hash map before it is expanded.
When the hash map reaches the given capacity, it is grown. You don't
want to only grow hash maps when they are full, because that makes
them perform very poorly. The maximum load value is a percentage of how
full the hash map is, and it should be between 0
and 1, where 0 means that no elements will cause the map to be expanded,
and 1 means that the hash map must be completely full before it is
expanded. The default maximum load on a new
.Dv HashMap
object is 0.75, which should be good enough for most purposes, but
if you need finer tuning, feel free to play with the max load with
this function. The changes take effect on the next insert.
.Pp
.Fn HashMapFunctionSet
allows the given hash map to use a custom hashing function. New hash
maps have a sane hashing function that should work okay for most use
cases, but if you have a better hash function, it can be specified with
this function. Do not change the hash function after keys have been
added; doing so results in undefined behavior. Only set the hash
function immediately after constructing a new hash map.
.Pp
.Fn HashMapSet
sets the given string key to the given value. Note neither the key nor the
value is copied into the hash map's own memory space; only pointers are
stored. It is the caller's job to ensure that the key and value memory
remains valid for the life of the HashMap, and are freed when they are no
longer needed.
.Fn HashMapGet
retrieves the value for the given key and .Fn HashMapDelete
removes a value from a given key.
.Pp
.Fn HashMapIterate
iterates over all the keys and values of a hash map. This function works
very similarly to
.Xr getopt 3 ,
where calls are repeatedly made in a
.Dv while
loop until there are no more items to go over. The difference is that this
function does not rely on globals; it takes pointer pointers, and stores all
necessary state inside the hash map structure itself. Note that this does not
make this function thread-safe; two threads cannot be iterating over any given
hash map at the same time, though they can be iterating over different hash
maps at the same time.
.Pp
This function can be tricky to use in some scenarios, as it continues where
it left off on each call, until there are no more elements to go through in
the hash map. If you are not iterating over the entire map in one go, and
happen to break the loop, then the next time you attempt to iterate the
hash map, you'll start somewhere in the middle. Thus, it's recommended to
always iterate over the entire hash map if you're going to use this
function. Also note that the behavior of this function is undefined if
insertions or deletions occur during the iteration. It may work fine; it may
not. That functionality has not been tested.
.Pp
.Fn HashMapIterate
takes a pointer to a string ponter, and a pointer to a value pointer. When
iterating over the keys and values of the hash map, it sets these pointers
to the current position in the map so that the caller can use them for his
own purposes.
.Sh RETURN VALUES
.Fn HashMapCreate
may return
.Dv NULL
if memory could not be allocated on the heap. Otherwise, it returns a
valid pointer to a
.Dv HashMap
structure which can be used with the other functions in this API.
.Pp
.Fn HashMapSet
returns the previous value at the passed key, or
.Dv NULL
if no such value exists. All keys must have values; you can't set a key
to
.Dv NULL .
To delete a key, use the
.Fn HashMapDelete
function.
.Pp
.Fn HashMapDelete
returns the value that was deleted from the hash map at the given key,
or
.Dv NULL
if no such value exists.
.Pp
.Fn HashMapIterate
returns 1 if there are still elements left in the current iteration of the
hash map, or 0 if no valid hash map was provided, or there are no more elements
in it for the current iteration. Note that as soon as this function returns 0
on a hash map, subsequent iterations will start from the beginning.
.Sh SEE ALSO
.Xr HashMap 3 ,
.Xr Queue 3

100
man/man3/Queue.3 Normal file
View file

@ -0,0 +1,100 @@
.Dd $Mdocdate: October 10 2022 $
.Dt QUEUE 3
.Os Telodendria Project
.Sh NAME
.Nm Array
.Nd A simple static queue data structure.
.Sh SYNOPSIS
.In Queue.h
.Ft Queue *
.Fn QueueCreate "void"
.Ft void
.Fn QueueFree "Array *"
.Ft int
.Fn QueuePush "Queue *" "void *"
.Ft void *
.Fn QueuePop "Queue *"
.Ft void *
.Fn QueuePeek "Queue *"
.Ft int
.Fn QueueFull "Queue *"
.Ft int
.Fn QueueEmpty "Queue *"
.Sh DESCRIPTION
These functions implement a simple queue data structure that
is statically sized.
This implementation does not actually store the values of the
items in it; it only stores pointers to the data. As such, you will
still have to manually maintain all your data. The advantage of this
is that these functions don't have to copy data, and thus don't care
how big the data is. Furthermore, arbitrary data can be stored in the
queue.
.Pp
This queue implementation operates on the heap. It is a circular
queue, and it does not grow as it is used. Once the size is set, the
queue never gets any bigger.
.Pp
These functions operate on a queue structure which is opaque to the
caller.
.Pp
.Fn QueueCreate
and
.Fn QueueFree
allocate and deallocate a queue, respectively.
Note that
.Fn QueueFree
does not free any of the values stored in the queue; it is the caller's
job to manage the memory for each item. Typically, the caller would
dequeue all the items in the queue and free them before freeing
the queue itself.
.Pp
.Fn QueuePush
and
.Fn QueuePop
are the main functions used to modify the array. They enqueue and dequeue
elements from the queue structure, respectively.
.Pp
.Fn QueuePeek
simply returns the pointer that is next up in the queue without actually
discarding it, such that the next call to
.Fn QueuePeek
or
.Fn QueuePop
return the same pointer.
.Pp
.Fn QueueFull
and
.Fn QueueEmpty
return a boolean value that indicates whether or not the queue is full
or empty, respectively.
.Sh RETURN VALUES
.Pp
.Fn QueueCreate
returns a queue structure, or
.Dv NULL
if there was a memory allocation error.
.Pp
.Fn QueuePush
as well as
.Fn QueueFull
and
.Fn QueueEmpty
all return boolean values. In the case of
.Fn QueuePush
whether or not the push was actually successful is returned. This will
only happen if the queue is already full, or a
.Dv NULL
pointer is passed.
.Pp
.Fn QueuePop
and
.Fn QueuePeek
both return caller-managed pointers that would have been at some point
pushed into the queue with the
.Fn QueuePush
function. They may also return
.Dv NULL
if the queue is empty.
.Sh SEE ALSO
.Xr Array 3 ,
.Xr HashMap 3

205
man/man5/telodendria.conf.5 Normal file
View file

@ -0,0 +1,205 @@
.Dd $Mdocdate: October 24 2022 $
.Dt TELODENDRIA.CONF 5
.Os Telodendria Project
.Sh NAME
.Nm telodendria.conf
.Nd Configuration file for Telodendria
.Sh DESCRIPTION
.Nm
is the configuration file for the Telodendria homeserver,
.Xr telodendria 8 .
Telodendria is designed to be extremely configurable. As such,
it has a fairly extensive configuration file. The configuration
file can be passed to the Telodendria binary with the
.Sy -f
option, and is typically located at
.Pa /etc/telodendria.conf
.sp
.Nm
uses OpenBSD-style syntax, though it is a little more rigid in its
parser. All values must be surrounded by quotes, and each directive
must be ended with a semicolon.
.Sh MACROS
Macros can be defined that will later be expanded in context.
Macro names must start with a letter, digit, or underscore, and may
contain only those characters. Macros are not expanded inside quotes.
.sp
For example:
.Bd -literal -offset indent
macro1 = "value1";
directive $macro1;
.Ed
.Sh GLOBAL OPTIONS
Here are the settings that can be set globally:
.Bl -tag -width Ds
.It Ic listen Ar port
The port to listen on. Telodendria will bind to all interfaces, but it
is recommended to configure your firewall so that it only listens on
localhost, and then configure a reverse proxy such as
.Xr relayd 8
in front of it, because Telodendria does not implement TLS. Note that
Telodendria doesn't provide multiple ports for the various services it
offers. ALl APIs are made available over the same port, so care should
be taken in
.Xr relayd.conf 5
to ensure that only the public Matrix API paths are made available over
the internet.
.Ar port
should be a decimal port number. This directive is entirely optional. If
it is omitted, then Telodendria will listen on port 8008 by default.
.It Ic server-name Ar name
Configure the domain name of your homeserver. Note that Matrix servers
cannot be migrated to other domains, so once this is set, it should never
change unless you want unexpected things to happen, or you want to start
over.
.Ar name
should be a DNS name that can be publically resolved. This directive
is required.
.It Ic base-url Ar url
Set the server's base URL.
.Ar url
should be a valid URL, complete with the protocol. It does not need to
be the same as the server name; in fact, it is common for a subdomain of
the server name to be the base URL for the Matrix homeserver.
.Pp
This URL is the URL at which Matrix clients will connect to the server,
and is thus served as a part of the
.Pa .well-known
manifest.
.Pp
This directive is optional. If it is not specified, it is automatically
deduced from the server name.
.It Ic identity-server Ar url
The identity server that clients should use to perform identity lookups.
.Pp
.Ar url
follows the same rules as
.Ic base-url .
.Pp
This directive is optional. If it is not specified, it is automatically
set to be the same as the base URL.
.It Ic id Ar uid Ar gid
The effective UNIX user and group to drop to after binding to the socket
and changing the filesystem root. This only works if Telodendria is
running as the root user, and is used as a security mechanism. If Telodendria
is started as a non-priviledged user, then a warning is printed to the log
if that user does not match what's specified here. This directive is
required, even if Telodendria is unable to switch to the given user and
group. It should be used as a sanity check to make sure the permissions are
working properly.
.It Ic data-dir Ar directory
The data directory into which Telodendria will write all user and event
information. Telodendria doesn't use a database like other Matrix homeserver
implementations; it uses a flat-file directory structure, similar to how an
SMTP server uses Maildirs to deliver email. This directive is required.
.Pp
Telodendria will
.Xr chroot 2
into this directory as soon as possible for security reasons. If the
.Ic log
directive is configured to write to a file, the log file will be written
in the data directory.
.Ar directory
should be an absolute path, under which all Telodendria data will live.
.It Ic federation Ar true|false
Whether to enable federation with other Matrix homeservers or not. Matrix is
by its very nature a federated protocol, but if you just want to run your
own internal chat server with no contact with the outside, then you can use
this option to disable federation. It is highly recommended to set this to
.Ar true ,
however, if you wish to be able to communicate with users on other Matrix
servers. This directive is required.
.It Ic registration Ar true|false
Whether or not to enable new user registration or not. For security and anti-spam
reasons, you can set this to
.Ar false .
If you do, you can still add users via the administrator API. In an ideal world,
everyone would run their own homeserver, so no public registration would ever
be required. Unfortunately, not everyone has the means to run their own homeserver,
especially because of the fact that public IPv4 addresses are becoming increasingly
harder to come by. If you would like to provide a service to those that are unable
to run their own homeserver, you can aset this to
.Ar true ,
which will allow anyone to create an account. Telodendria should be capable of handling
a large amount of users without difficulty or security issues. This directive is
required.
.It Ic log Ar stdout|file|syslog
The log configuration. Telodendria uses its own logging facility, which can output
logs to standard output, a file, or the syslog. If set to
.Ar file ,
Telodendria will log to
.Pa telodendria.log
inside the
.Ic data-dir .
.Pp
A number of child directives can
be added to this directive to customize the log output:
.Bl -tag -width Ds
.It Ic level Ar error|warning|task|message|debug
The level of messages to log at. Each level shows all the levels above it. For
example, setting the level to
.Ar error
will show only errors, while setting the level to
.Ar warning
will show warnings and errors.
.Ar task
shows tasks, warnings, and errors, and so on. The
.Ar debug
level shows all messages.
.It Ic timestampFormat Ar format|none|default
If you want to customize the timestamp format shown in the log, or disable it
altogether, you can do so via this option. Acceptable values are
.Ar none ,
.Ar default ,
or a formatter string as described by your system's
.Xr strftime 3 .
This option only applies if
.Ic log
is "stdout" or "file".
.It Ic color Ar true|false
Whether or not to enable colored output on TTYs. Note that ANSI color sequences
will not be written to a log file, only a real terminal, so this option only
applies if the log is being written to a standard output which is connected to
a terminal.
.Pp
This option only applies if
.Ic log
is "stdout".
.El
.It Ic threads Ar count
How many worker threads to spin up to handle requests. This should generally be
less than the total CPU core count, to prevent overloading the system. The most
efficient number of threads ultimately depends on the configuration of the
machine running Telodendria, so you may just have to play around with different
values here to see which gives the best performance.
.It Ic max-connections Ar count
The maximum number of simultanious connections to allow to the daemon. This option
prevents the daemon from allocating large amounts of memory in the even that it
undergoes a denial of service attack. It typically does not need to be adjusted.
.It Ic max-cache Ar bytes
The maximum size of the cache. Telodendria relies heavily on caching to speed
things up. The cache grows as data is loaded from the data directory. All cache
is stored in memory. This option limits the size of the memory cache. If you have
a system that has a lot of memory, you'll get better performance if this option
is set higher. Otherwise, this value should be lowered on systems that have
minimal memory available.
.El
.Sh FILES
.Bl -tag -width Ds
.It Pa /etc/telodendria.conf
The default
.Xr telodendria 8
configuration file.
.It Pa /var/telodendria
The recommended data directory.
.El
.Sh EXAMPLES
Please consult the default
.Nm
that ships with Telodendria for a complete example. If you installed Telodendria
as a package, then the example should be located at the default location. If you
are building from source, you can find the default config in the
.Pa contrib/
directory.
.Sh SEE ALSO
.Xr telodendria 8

252
man/man7/contributing.7 Normal file
View file

@ -0,0 +1,252 @@
.Dd $Mdocdate: September 30 2022 $
.Dt CONTRIBUTING 7
.Os Telodendria Project
.Sh NAME
.Nm contributing
.Nd Guide to contributing to Telodendria
.Sh DESCRIPTION
Telodendria is an open source project. As such, it welcomes
contributions. There are many ways you can contribute, and any
way you can is greatly appreciated. This page contains some of
the ways you can help out.
.Sh REPORTING ISSUES
Please reach out to the Matrix rooms mentioned at the top of
.Xr telodendria 7 .
All issue tracking takes place in those rooms. Start by reaching
out to the general room, and if you think there's a legitimate
problem with Telodendria itself, then stick the issue in the
issues room, where it can be discussed further. Issues usually
remain in the Matrix rooms, but severe enough issues may be put
in a
.Pa TODO
file in the
.Xr cvs 1
repository so that they don't get lost.
.Sh DEVELOPING
The primary language used to write Telodendria code is ANSI C.
Other languages you'll find in the Telodendria repository include
shell scripts,
.Xr mdoc 7 ,
and a little bit of HTML and CSS. If you have any experience with
any of these languages, your contributions are valuable! Please follow
the guidelines on this page to ensure the contribution workflow goes
as smoothly as possible.
.Ss Getting the Code
If you'd like to hack on Telodendria, you'll need the following tools
in addition to a C compiler and POSIX shell:
.Bl -tag
.It Xr cvs 1
For checking out and updating your local copy of the source code.
.It Xr indent 1
For formatting your code before generating patches.
.It Xr patch 1
For applying patches to your local copy of the source code.
.El
.sp
All of these tools are built into OpenBSD. While you don't have to
use OpenBSD to develop Telodendria, it may make the process a bit
easier. In fact, these tools where chosen precisely because they
were built into my operating system of choice.
.sp
You can download an official release tarball from the website if
you would really like, but the preferred way to get the source
code for development is to check it out from CVS. This makes generating
patches a lot easier.
.Bd -literal -offset indent
$ cvs -d anoncvs@bancino.net:/cvs checkout -P Telodendria
$ cd Telodendria
.Ed
.sp
If you already checked out the code previously, make sure you update
your local copy before you start developing:
.Bd -literal -offset indent
$ cvs -q update -dP
.Ed
.sp
You should now have the latest source code. Follow the
.Sx CODE STYLE
as you make your changes. If the
.Xr cvs 1
command fails with a "Connection refused" error message, try setting
the
.Ev CVS_RSH
environment variable to "ssh", like this:
.Bd -literal -offset indent
$ export CVS_RSH=ssh
.Ed
.sp
Then run the checkout command again. Some versions of CVS on some
systems don't use SSH to checkout by default, so if yours doesn't,
you might want to put the above line into your shell init script.
.Ss Submitting Patches
Telodendria aims at remaining as minimal as possible. This doesn't just
mean minimal code, it also means a minimal development process, which is
why Telodendria doesn't use GitHub, GitLab, or even SourceHut. Instead,
the contribution workflow operates on submitting patch files to a public
Matrix room, sort of like the OpenBSD project operates on patch files
sent to a public mailing list.
.sp
If you're not used to manually creating and submitting patches instead of
just opening a "pull request," you should be pleased to hear that submitting
patches is fairly easy to do if you've got the CVS sources checked out. In
fact, I find it easier than having to make a GitHub account, forking a
project's repository, and then making a pull request for it. Once you have
made your changes in your local copy of the code, and you've configured your
environment properly as noted in the manual for
.Xr td 8 ,
just run the patch recipe:
.Bd -literal -offset indent
$ td patch
.Ed
.sp
This will automatically generate a patch file for all your changes, and then
open it in your preferred editor. You can also generate a patch file for only
certain files and directories. To do that, set
.Ev PATCHSET ,
like this:
.Bd -literal -offset indent
# Only write a patch for README.txt and the files in docs/
$ PATCHSET="README.txt docs/" td patch
.Ed
.sp
As you'll notice, the top of the patch file should have some email-style
headers that look like this:
.Bd -literal -offset indent
From: Jordan Bancino <@jordan:bancino.net>
Date: Fri Jul 29 03:21:21 PM EDT 2022
Subject: Document Patch Procedure
.Ed
.sp
As much information should be filled out for you, such as the date. An
attempt to fill out the From header was also made by
.Xr td 8 ,
but the information there can be modifed as necessary. Consult the manual
for
.Xr td 8
for more details. The Subject should very briefly describe what the patch
is about.
.sp
You'll also notice these lines in the patch:
.Bd -literal -offset indent
[ ] I have read the Telodendria Project development certificate of
origin, and certify that I have permission to submit this patch
under the conditions specified in it.
.Ed
.sp
This is a checkbox that tells me whether or not you actually have the
rights to submit your patch, and that once you submit your patch, your
code is bound by the Telodendria project license, which you can and
should view in
.Xr telodendria 7 .
The full text of the developer certificate of origin is as follows:
.Bl -enum
.It
The contribution was created in whole or in part by me, and I have the right
to submit it under the open source licenses of the Telodendria project; or
.It
The contribution is based upon a previous work that, to the best of my knowledge,
is covered under an appropriate open source license and I have the right under
that license to submit that work with modifications, whether created in whole
or in part by me, under the Telodendria project license; or
.It
The contribution whas provided directly to me by some other person who certified
(1), (2), or (3), and I have not modifed it.
.It
I understand and agree that this project and the contribution are made public
and that a record of the contribution\(emincluding all personal information
I submit with it\(emis maintained indefinitely and may be redistributed
consistent with this project or the open source licenses involved.
.El
.sp
If you agree to the above, fill in the square brackets with an 'x', and then after
the headers, but before the checkbox, write a more thorough description of the
patch and why it was created. Then, send the resulting patch file to the public
Matrix room, as noted in
.Xr telodendria 7 ,
so it can be discussed and reviewed by the community.
.sp
Try to keep your patches on topic\(emmake one patch file per feature or bug fix
being implemented. It is okay if your patches depend on previous patches, just
indicate that in the patch description. Note that it may take a while for
patches to be committed, and some patches may not be comitted at all. In either
case, all sent patches are queued from the Matrix room into the public patch
directory, so they can be referenced easier in the future. If you want to
reference a submitted patch in a Matrix message, email, or other digital medium,
it might be a good idea to link to it in the public patch directory.
.sp
The public patch directory works as follows: when you send your patch to the
Matrix room, it is downloaded by Telodendria Bot and placed in the
.Pa ingress/
directory, named as the message ID. Then, it is assigned a patch ID and
copied to the
.Pa p/
directory as just "%d.patch", where "%d" is obviously the patch ID. This is
a permanent link that will always reference your patch. Then, your patch will
be symlinked into the
.Pa queue/
directory. I have a script that automatically ingresses patches and queues them
for me, and I use this to review patches. If your patch is accepted, the queue
symlink will be moved to
.Pa accepted/
and the submitted patch will be committed to the official CVS repository.
If your patch is rejected for some reason, its symlink will be moved to the
.Pa rejected/
directory. Regardless of the state of your patch, it will always remain
permalinked in the
.Pa p/
directory, and when it is accepted or rejected, Telodendria Bot will send a
message to the Matrix room.
.sp
You're always welcome to inquire about rejected patches, and request that they
be reviewed again, or you can use them as a starting point for future patches.
.sp
The public patch directory is located at
.Lk https://telodendria.io/patches/
.Sh CODE STYLE
In general, these are the conventions used by the code base. This guide
may be slightly outdated or subject to change, but it should be a good
start. The source code itself is always the absolute source of truth, so
as long as you make your code look like the code surrounding it, you should
be fine.
.Bl -bullet
.It
All function, enumeration, structure, and header names are CamelCase. This
is preferred to snake_case because it is more compact.
.It
All variable names are lowerCamelCase. This is preferred to snake_case
because it is more compact.
.It
enumerations and structures are always typedef-ed to their same name. The
typedef should occur in the public API header, and the actual declaration
should live in the implementation file.
.It
A feature of the code base lives in a single C source file that has a
matching header. The header file should only export public symbols;
everything else in the C source should be static.
.It
Except where absolutely necessary, global variables are forbidden to
prevent problems with threads and whatnot. Every variable a function
needs should be passed to it either through a structure, or as a
separate argument.
.It
Anywhere curly braces are optional, there still must be curly braces. This
makes it easier to add on to the code later, and just makes things a bit
less ambiguous.
.El
.sp
As far as actually formatting the code goes, such as where to put brackets,
and whether or not to use tabs or spaces, use
.Xr indent 1
to take care of all of that. The root of the CVS repository has a
.Pa .indent.pro
that should automatically be loaded by
.Xr indent 1
to set the correct rules. If you don't have a working
.Xr indent 1 ,
then just indicate in your patch that I should run my
.Xr indent 1
on the code after applying it. Although in reality, I'll likely
run my own
.Xr indent 1
on the code anyway, just to make sure the spacing is consistent, if nothing
else.

134
man/man7/onboarding.7 Normal file
View file

@ -0,0 +1,134 @@
.Dd $Mdocdate: October 11 2022 $
.Dt ONBOARDING 7
.Os Telodendria Project
.Sh NAME
.Nm onboarding
.Nd Guide to getting Telodendria running on your system.
.Sh DESCRIPTION
This page aims to provide a high-level overview of how to
get Telodendria running on your system from source. It assumes
that you're running OpenBSD, or know how to translate OpenBSD
commands into your system's commands, and that you are using an
an official distribution tarball. If you are using your system's
package manager to install Telodendria, you should refer to the
package maintainer's installation instructions instead of these
instructions.
.Pp
If you're a package maintainer, or would like to be, these
instructions might also be helpful for you as well; you can
use them to learn how the process is designed to work, so you
can mimic it on your system.
.Sh PREREQUISITES
To build Telodendria, you just need a POSIX system and C compiler.
If your system does not ship with a C compiler, please install
one before continuing with these directions.
.Pp
The build script relies on a POSIX shell implementation, as well
as the standard userspace utilities specified by POSIX.
.Pp
This should be obvious, but you'll also need a way to
download files to your machine, or otherwise copy them into place.
You'll also need
.Xr tar 1 ,
and
.Xr gzip 1
to extract the downloaded tarball.
.Sh GETTING THE CODE
You can download the source code for Telodendria by going to
the official website and downloading a tarball from the table.
Otherwise, you can download the tarball and supplemental files
directly:
.Bd -literal -offset indent
$ export VERSION=x.x.x # Set version of Telodendria you want
$ ftp https://telodendria.io/pub/v$VERSION/Telodendria-v$VERSION.tar.gz
$ ftp https://telodendria.io/pub/v$VERSION/Telodendria-v$VERSION.tar.gz.{sha256,sig}
.Ed
.Pp
Before you extract the tarball, it is a good idea to verify both its checksum
and its signature. Use
.Xr sha256 1
to verify that the download was not corrupted in transit:
.Bd -literal -offset indent
$ sha256 -C Telodendria-v$VERSION.tar.gz.sha256 Telodendria-v$VERSION.tar.gz
.Ed
.Pp
Use
.Xr signify 1
to verify that the download was legitimately published by the Telodendria
project:
.Bd -literal -offset indent
$ signify -V -p telodendria-signify.pub -m Telodendria-v$VERSION.tar.gz
.Ed
.Pp
You can obtain
.Pa telodendria-signify.pub
from the official website. While you may absolutely obtain the tarball
and checksum files from mirrors or other sources, do not obtain the public
key file from any other sources than the official website. Downloads from
other sources should be verified by the official public key only:
.Bd -literal -offset indent
$ ftp https://telodendria.io/telodendria-signify.pub
.Ed
.Pp
Finally, only when the tarball is verified, you can extract it:
.Bd -literal -offset indent
$ tar -xzf Telodendria-v$VERSION.tar.gz
$ cd Telodendria-v$VERSION
.Ed
.Sh BUILDING AND INSTALLING
In the source directory, just run the following commands:
.Bd -literal -offset indent
$ . tools/env.sh
$ td build
$ doas td install
.Ed
.Pp
This will compile the code, and install the output binary, along
with all of the documentation and configuration files, to the system.
.Pp
.Sy Packagers:
It might be beneficial to take a look at the install recipe in the
.Xr td 8
script so you know what files go where. Adapt the general procedure
for your operating system.
.Sh CONFIGURATION
A sufficiently complete configuration file should have been installed
to
.Pa /etc/telodendria.conf .
Open it with your preferred editor, and go through each line, making
sure everything is set appropriately for your setup. Consult the
.Xr telodendria.conf 5
and
.Xr Config 5
pages for a comprehensive list of all the available options and a
description for each option, as well as the general syntax of the
configuration file. You'll most certainly have to set your
server's name and base URL. Optionally, specify an identity server,
and whether or not you want to enable federation and registration.
All other options should have sane defaults for most systems.
.Sh STARTING TELODENDRIA
Once you've written the configuration file, it should just be as
easy as starting the
.Dv telodendria
daemon:
.Bd -literal -offset indent
$ rcctl enable telodendria
$ rcctl start telodendria
.Ed
.Pp
It might be a good idea to check the log file to make sure everything
started okay and is running fine.
.Pp
At this point, it would be a very good idea to configure a reverse
proxy. The
.Pa contrib/
directory of the source code has an example
.Xr relayd.conf 5
file which can be installed as-is into the typical location, or
integrated into an existing configuration. At some point in the
future, a reverse proxy may not be required for TLS, but it will
always be recommended because it provides more control and security.
.Sh SEE ALSO
.Xr td 8 ,
.Xr telodendria.conf 5 ,
.Xr telodendria 8

249
man/man7/telodendria.7 Normal file
View file

@ -0,0 +1,249 @@
.Dd $Mdocdate: October 10 2022 $
.Dt TELODENDRIA 7
.Os Telodendria Project
.Sh NAME
.Nm Telodendria
.Nd The terminal branches of an axon.
.Sh DESCRIPTION
.Nm
is an open source Matrix homeserver written entirely from scratch in ANSI C and
designed to be lightweight and simple, yet functional.
.Pp
.Sy Note:
.Nm
is under
.Sy heavy
development. Please see
.Sx PROJECT STATUS .
.Pp
.Nm
differentiates itself from the other Matrix homeserver
implementations because it:
.Bl -bullet
.It
Is written in C, a stable, low-level programming language with a
long history, low build and runtime overhead, and wide compatibility.
.It
Is written with minimalism as a primary design goal. Whenever possible
and practical, no third-party libraries are pulled into the source code.
Everything
.Nm
needs is custom written. As a result,
.Nm
depends only on a standard C compiler and a POSIX C library to be built,
both of which should come with any good Unix-style operating system already,
which means you shouldn't have to install anything extra.
.It
Uses a flat-file directory structure to store data instead of a database.
This has a number of advantages:
.Bl -bullet
.It
It makes setup and maintenance much easier.
.It
It allows
.Nm
to run on systems with fewer resources.
.El
.It
Is packaged as a single small, statically-linked and highly-optimized binary
that can be run just about anywhere. It is designed to be extremely easy to
set up and consume as few resources as possible.
.It
Is permissively licensed.
.Nm
is licensed under a modified MIT license, which imposes very few restrictions
on what you can do with it.
.El
.Pp
.Nm
is on Matrix! Check out the official Matrix rooms:
.Pp
.TS
box tab(;);
ll.
#telodendria:bancino.net;The public "space" room.
#telodendria-releases:bancino.net;Get notified of new releases.
#telodendria-general:bancino.net;General discussion and support.
#telodendria-issues:bancino.net;Report bugs and issues.
#telodendria-patches:bancino.net;Submit code patches to the project.
.TE
.Pp
.Nm
is designed to be fairly straightforward, but that doesn't mean there
won't be hiccups along the way. If you're struggling to get
.Nm
up and running, you're more than welcome to reach out for support. Just
hop into the appropriate Matrix rooms and talk to me!
.Sh RATIONALE
I want a lightweight Matrix homeserver designed specifically for OpenBSD,
and other Unix-like operating systems. I want a homeserver that can be
developed and compiled with built-in tools. I want it to function entirely
on a base OS install without having to install any packages whatsoever. I've
found that as far as these goals are concerned, the existing homeserver
implementations fall short. This project aims to prove that Matrix homeservers
can be lightweight and written in such a way that very few, if any, third-party
libraries need to be pulled in.
.Pp
I also want to learn how Matrix works, and I want to understand the code I'm
running on my server, which is why I'm writing every component from scratch,
even the HTTP server and router.
.Pp
The advantage of using a flat-file database instead of a real database is that
your data remains in a format that is incredibly easy to digest. Backups are
incredibly easy as well\(emjust
.Xr tar 1
up the data directory and you're good to go.
.Sh PROJECT GOALS
The goals of this project are generally divided into user goals and developer
goals, depending on who they impact the most. This isn't an exaustive list
of the project's goals, but it is a list of things that I want to prioritize,
because other server implementations lack them.
.Pp
The user goals are as follows:
.Bl -bullet
.It
To implement the latest Matrix specification as fully and completely as possible.
All features defined by the specification should eventually be present in
.Nm .
.It
To be as privacy-friendly as possible.
.Nm
should not store any information it does not absolutely need to function as a
Matrix homeserver. While
.Nm
strives to be feature-complete, it should not implement anything not explicitly
defined in the Matrix specification.
.It
To be a production-ready Matrix server capable of working in constrained environments,
such as embedded devices, cheap VPSs, or a peer-to-peer device.
.Nm
should also work on traditional setups as well, and be able to handle a decent
amount of users. It should work well for personal Matrix homeservers that also
host a few friends and/or family members.
.It
To be easier to get up and running, and yet also be more configurable than other
Matrix homeserver implementations. The configuration file should be flexible,
well-documented, and easy to understand and modify. An intuitive command-line
tool for administrative tasks should also be available.
.El
.Pp
The developer goals are as follows:
.Bl -bullet
.It
To have as few build and runtime dependencies as possible. It should be possible
to compile and run
.Nm
on any POSIX operating system right out of the box.
.Nm
should be fully statically-linked, so it can run under a
.Xr chroot 3 .
Furthermore, it should be possible to read all the documentation offline, and
without any overly complicated tools. You'll notice that this documentation is
a collection of
.Xr man 1
pages, not HTML or Markdown, to remove the dependency on a browser or Markdown
parser. Any Unix-like system has a manual page viewer, which makes the
documentation more accessible, even on remote systems that have no graphical
interface to read the documentation. This ensures that you can read the
documentation for the installed version of
.Nm
without having to go online.
.It
To have a simple yet elegant workflow, and not depend on any large or complex
development tools such as code forges. The entire development workflow should
be able to be successfully and efficiently completed on a base OpenBSD install.
Of course, you don't have to use OpenBSD for development, but the point is that
the workflow should require fairly standardized and simple tools.
.It
To write clean, elegant, well-tested, and well-documented code. The goal is to build
a Matrix homeserver from the ground up, not just because I don't like the way existing
homeservers are implemented, but also because I want to learn how Matrix works,
make it more accessible, and potentially teach others a little bit along the way.
.It
To foster a welcoming community. Many big communities such as Linux and OpenBSD
have hostile leaders.
.Nm
shouldn't have a hostile leader. I want to be as understanding as I can, and talk
through issues politely and in a civil manner. If I'm failing in this way, don't
be afraid to call me out!
.El
.Sh PROJECT STATUS
Telodendria is a very ambitious project. There's a lot that needs to happen yet
before it is usable. At the moment, there's nothing here that even remotely resembles
a Matrix homeserver. I'm still getting off the ground and building a foundation.
.Pp
But just because there's nothing here yet doesn't mean you should go away! I could
always use help, so you are more than welcome to help out if you want things to go
quicker. Please see the
.Xr contributing 7
page for details on how you can get involved. The CVS repository has a file called
.Pa TODO.txt ,
which contains a checklist of what has been done so far, and what's on the radar
to be done. You can take a look at it once you've checked out the source code.
If you'd like to help out, feel free to grab an item on the list that's not done
yet and start getting patches written. When you're done, enjoy the satisfaction
of crossing the item off the list yourself.
.Sh SEE ALSO
.Xr telodendria 8 ,
.Xr telodendria.conf 5 ,
.Xr td 8
.Sh STANDARDS
The installed version of
.Nm
conforms to the latest Matrix specification available at the time
of its release.
.Sh HISTORY
At this time,
.Nm
does not have any tagged releases because it is not yet functional
as a Matrix homeserver. Please checkout out
.Sx PROJECT STATUS
to see where things are currently at. When
.Nm
is mature enough to have a change log, it will go in this section.
.Sh AUTHORS
.Nm
was started by Jordan Bancino <@jordan:bancino.net>.
Contributions to the code, website, documentation, or other
components of
.Nm
were made by the following people:
.Bl -bullet
.It
Jonah Evans <@jonah:bancino.net>
.El
.Sh LICENSE
All of the code and documentation for
.Nm
is licensed under a modified MIT license. Please consult the
.Pa LICENSE.txt
file for the actual license text. The
.Nm
license text differs from the MIT license in the following ways:
.Bl -bullet
.It
Where the MIT license states that the copyright notice and permission
notice shall be included in all copies or
.Pa substantial
portions of the software, the
.Nm
license requires the copyright notice and permission notice be included
with
.Pa all
portions, regardless of the size, of the software by omitting the word
.Pa substantial .
.El
.Pp
The
.Nm
logo, however, belongs solely to the
.Nm
project. It must only be used to represent the official
.Nm
project, and may only appear in official
.Nm
media. If
.Nm
is forked, the logo must be removed completely from the project, and
optionally replaced with a different one. The logo may not be modified
in any way or for any purpose.

241
man/man8/td.8 Normal file
View file

@ -0,0 +1,241 @@
.Dd $Mdocdate: October 11 2022 $
.Dt TD 8
.Os Telodendria Project
.Sh NAME
.Nm td
.Nd Telodendria build script
.Sh SYNOPSIS
.Nm
.Op recipe
.Sh DESCRIPTION
Telodendria uses a custom build script called
.Nm .
The
.Nm
script is not only a build script, however. It does all kinds of
cool things like format the source code, and generate patch files.
.Nm
is the only supported way to develop Telodendria.
.sp
I opted to write a custom build script instead of just writing a
.Pa Makefile
because I felt that there is really no way to make a truly portable
.Pa Makefile
that could do everything I wanted, with the flexibility I wanted. I
was doing a lot of research on the differences between the GNU and BSD
versions, and I felt it just wasn't worth trying to reconsile the two
when I could write a small and relatively robust POSIX shell script that
would run on both GNU and BSD systems without any problems. I also
think that shell scripts are a lot easier to read than complex
.Pa Makefiles.
They're easier to follow because they're not so cryptic.
.sp
The
.Nm
script is fairly intuitive. It operates somewhat like
.Xr make 1
in that it has recipes that you specify on the command line. To start
using it, just run the following command in the Telodendria source
directory:
.Bd -literal -offset indent
$ . tools/env.sh
.Ed
.sp
.Sy Note:
You will have to run the above command every time you start a new
terminal session, as nothing is persisted to your system. I believe in
non-invasive, fully self-contained tooling, so it is up to you to hook the
Telodendria tools into your environment as you see fit if you want them to
persist.
.sp
If you're going to be submitting patches, you should also configure a
.Pa .env
file in the project directory root, which
.Nm
will include automatically for you. See
.Em FILES
and
.Em ENVIRONMENT .
.sp
Telodendria is designed to be light enough that it can be built from source
on just about any operating system. It only requires an ANSI C compiler and a
standard POSIX environment. To build the Telodendria binary, run
.Nm
with no arguments, or with the
.Pa build
recipe. This will produce
.Pa build/telodendria ,
which you can then install to your system and run as a daemon.
.sp
A complete list of recipes is below. Note that you can run multiple recipes
with a single invocation of
.Nm ,
but recipes are run unconditionally; that is, even if a recipe fails, all the
following recipes are still executed.
.Bl -tag
.It build
Build the source code and generate the output binary. This is the default recipe,
which means it runs if no other recipes are specified. This recipe is incremental;
it only rebuilds sources that have been modifed since the last build, making
subsequent builds faster.
.It run
Run the build binary with the development configuration in the
.Pa contrib/
directory. This recipe is used for quick testing during development. It is
.Sy not
the recommended way to run Telodendria in a production environment; it should only
be used for development.
.It clean
Remove the
.Pa build/
directory and any ephemeral files in the source tree, such as
.Pa .orig
files. The build recipe does not place anything outside of
.Pa build/ ,
so you can usually just delete that directory and get the same effect.
.It format
Make sure the source code copyright headers are up to date, and format the code
using the system's
.Xr indent 1 .
This should be run before generating patch files, to ensure that the code follows
the project conventions. Note that the provided
.Pa .indent.pro
assumes an OpenBSD indent, which may cause the GNU implementation to choke. In
that case, don't send patch files with totally different formatting; just submit
the patch as-is and they will get formatted before committing.
.It test
Run all of the unit tests and report the results. It is highly recommended to
ensure that all the tests pass before submitting a patch, because patches that
break the tests are likely to be rejected.
.It site
Deploy the Telodendria website by generating HTML files for the documentation,
and copying them along with the front page to the specified web root. This is
used to deploy the official website, but it could be used to deploy a local
development site as necessary. See
.Em ENVIRONMENT .
.It release
Generate a release tarball, checksum and sign it, and push it to the web root.
See the relevant environment variables below.
.It patch
Generate a formatted patch file. The Telodendria project isn't super picky about
how patches look as long as they apply cleanly, but this recipe generates patches
in the format we like them, and is therefore recommended. It makes patches easy
to read. This recipe will use your configured editor to open your formatted patch
so you can review and edit it as necessary before sending it off.
.It diff
Generate a temporary preview patch that is opened in the system pager. This can
be used for quickly quickly previewing your changes and the patch file you'll
be creating.
.El
.sp
.Sh ENVIRONMENT
Any of the following environment variables are used in the build recipes.
They can all be specified in your shell when invoking
.Nm ,
or they can be placed in a
.Pa .env
file. For most of these variables, if you would like to append or prepend
to the default values, do so in the
.Pa .env
file, which is sourced after the defaults are set, allowing you to reference
the default values in your new value.
.Bl -tag
.It Ev CC
The C compiler to use. This defaults to
.Pa cc,
which is usually a symlink to your system's preferred compiler. If for some
reason you want to use a diferent compiler, do so with this environment
variable.
.It Ev CFLAGS
The compiler flags used to generate object files.
.Nm
comes with reasonable defaults that shouldn't need to be changed in most
scenarios, but if you do need to change the compiler flags, you can do
so with this environment variable.
.It Ev LDFLAGS
The compiler flags used to link the object files to create an output
binary.
.Nm
comes with reasonable defaults that shouldn't need to be changed in most
scenarios, but if you need to change the linker flags, you do so with this
environment variable.
.It Ev PROG
The name of the output binary. This defaults to
.Pa build/telodendria.
.It Ev DEFINES
Global preprocessor definitions to append to
.Ev CFLAGS.
This is just kept separate to keep things organized.
.It Ev INCLUDES
Header directories to make available. This is appended to
.Ev CFLAGS,
it is just kept separate to keep things organized.
.It Ev DEBUG
If set to "1", append some debug flags to
.Ev CFLAGS
and whipe out any
.Ev LDFLAGS
that awould cause the output binary to be optimized in any way. This also
depends "-debug" to
.Ev PROG .
.It Ev TELODENDRIA_VERSION
This variable does make its way into the output binary, but it is primarily
used for generating and publishing releases. This variable affects the
.Sy release
recipe.
.It Ev TELODENDRIA_PUB
The web root where the Telodendria website lives. This is where the site
is pushed to, and where generated releases go.
.It Ev PATCHSET
This variable restricts the files that
.Nm
operates on when generating patches or diffs. If you only want to generate
a diff or patch for a certain file, directory, or collection of files and
directories, set this variable to those files and directories, separated
by a space. You can mix files and directories as necessary.
.It Ev MXID
Your Matrix ID in standard format. This is used when generating patches,
so that you can be assigned credit for your patches, as well as be contacted
about your patches.
.Nm
will automatically deduce this from your system, but it will most
likely get it wrong. Please make sure you are reachable at this ID.
.It Ev DISPLAY_NAME
The display name you want to appear on your patches. This should probably
match your Matrix display name, although it doesn't necessarily have to.
.Nm
will deduce this from your system, and if you set it up properly, you may
not even have to set this variable. If
.Nm
gets it wrong, this allows you to override your display name.
.It Ev EDITOR
Your preferred editor for writing patch file descriptions. This can be a
GUI or terminal editor. If unset, this defaults to the system's
.Xr vi 1
editor.
.It Ev PAGER
Your preferred pager for previewing patches. If left unset, this defaults
to
.Xr less 1 .
.Sh FILES
.Bl -tag
.It Pa .env
An environment file that contains lines in the form of
.Pa VARIABLE=value
with environment variables to set in the
.Nm
script. See
.Em ENVIRONMENT .
Note that
.Nm
simply sources this file, which means that any shell code in it will be
executed each time
.Nm
is invoked.
.Sh EXIT STATUS
.Sh HISTORY
.Sh BUGS
.Nm
unconditionally exits with code 0, even if errors occurred. Furthermore,
recipes are run unconditionally, regardless of whether or not any recipes
before have failed.

57
man/man8/telodendria.8 Normal file
View file

@ -0,0 +1,57 @@
.Dd $Mdocdate: September 30 2022 $
.Dt TELODENDRIA 8
.Os Telodendria Project
.Sh NAME
.Nm telodendria
.Nd Matrix homeserver daemon
.Sh SYNOPSIS
.Nm
.Op Fl nVv
.Op Fl f Ar file
.Sh DESCRIPTION
.Nm
is a Matrix homeserver written entirely from scratch in ANSI C.
It is designed to be lightweight and simple, yet functional.
.sp
The options are as follows:
.Bl -tag -width Ds
.It Fl f Ar file
Specify an alternate configuration file. The default is
.Pa /etc/telodendria.conf .
.It Fl n
Configtest mode. Only check the configuration file for validity.
.It Fl V
Only print the version information header.
.It Fl v
Verbose mode. This overrides the configuration file and sets the
log level to
.Em LOG_DEBUG
.El
.Sh ENVIRONMENT
.Nm
does not read any environment variables. All configuration should
be done via the configuration file.
.Sh FILES
Just the configuration file and the data directory; see
.Xr telodendria.conf 5
for more details.
.El
.Sh EXIT STATUS
.Nm
exits with a non-0 exit code if the configuration file is invalid, or
one or more required paths is inaccessible.
.Nm
will print an error to the log and then terminate abnormally.
.Pp
.Nm
exits with a code of 0 if the configuration file is valid, all
paths and files required are accessible, and the HTTP listener starts
as intended. If
.Nm
is sent a signal that it catches after it begins servicing requests, it
will still exit with a code of 0 after it safely shuts down, because
the bootstrap process completed successfully, and by all accounts,
it ran normally and exitted normally.
.Sh SEE ALSO
.Xr telodendria 7 ,
.Xr telodendria.conf 5

189
site/index.html Normal file
View file

@ -0,0 +1,189 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="author" content="Jordan Bancino">
<meta name="description"
content="Telodendria, a Matrix homeserver written in ANSI C.">
<meta property="og:title"
content="Telodendria | A Matrix Homeserver written in ANSI C.">
<meta property="og:type" content="website">
<meta property="og:url"
content="https://telodendria.io">
<meta property="og:description"
content="Telodendria, a Matrix homeserver written in ANSI C.">
<link rel="stylesheet" href="/style.css">
<link rel="icon" href="assets/Telodendria-196x196.png">
<title>Telodendria | A Matrix Homeserver written in ANSI C.</title>
</head>
<body>
<img id="logo" src="/assets/Telodendria-500x500.png" alt="Telodendria Logo"/>
<h1 id="telodendria">Telodendria</h1>
<p>
<b>Tel-&#601;-'den-dr&#275;-&#601;:</b> The terminal aborizations of an axon.
</p>
<p>
<b>Telodendria</b> is an open source Matrix homeserver implementation written from
scratch in ANSI C and designed to be lightweight and simple, yet
functional.
</p>
<div class="msg-error">
<b><i>Note:</i></b> <b>Telodendria</b> is under <i>heavy</i> development.
Please see the <a href="/man/man7/telodendria.7.html#PROJECT_STATUS">Project Status</a>.
</div>
<h2 id="documentation">Documentation</h2>
<p>
<b>Telodendria</b>'s documentation is distributed with the source
code as <code>man</code> pages, which contain all of the information
on what <b>Telodendria</b> is, what its goals are, how to build the source,
configure it, as well as contribute to the project. The <code>man</code>
pages are also available online for convenience:
</p>
<p>User Documentation:</p>
<table>
<tr>
<th>Man Page</th>
<th>Description</th>
</tr>
<tr>
<td><a href="man/man7/telodendria.7.html">telodendria(7)</a></td>
<td>
<b>Start here.</b> This page contains the project introduction, and provides
information about it, such as its status, how to contact the developers, and what
the source code license is.
</td>
</tr>
<tr>
<td><a href="man/man7/onboarding.7.html">onboarding(7)</a></td>
<td>
Setup instructions for OpenBSD.
</td>
</tr>
<tr>
<td><a href="man/man8/telodendria.8.html">telodendria(8)</a></td>
<td>
Command line usage for <b>Telodendria</b> administrators.
</td>
</tr>
<tr>
<td><a href="man/man5/telodendria.conf.5.html">telodendria.conf(5)</a></td>
<td>
Configuration file options.
</td>
</tr>
<tr>
<td><a href="man/man7/contributing.7.html">contributing(7)</a></td>
<td>
Contributing guide.
</td>
</tr>
</table>
<p>Developer Documentation:</p>
<table>
<tr>
<th>Man Page</th>
<th>Description</th>
</tr>
<tr>
<td><a href="man/man8/td.8.html">td(8)</a></td>
<td>
Build script and patch generation instructions.
</td>
</tr>
<tr>
<td><a href="man/man3/Array.3.html">Array(3)</a></td>
<td>
Dynamically-sized array API.
</td>
</tr>
<tr>
<td><a href="man/man3/Base64.3.html">Base64(3)</a></td>
<td>
Base64 implementation with Matrix's "unpadded base64" support.
</td>
</tr>
<tr>
<td><a href="man/man3/HashMap.3.html">HashMap(3)</a></td>
<td>
A simple hash map implementation.
</td>
</tr>
<tr>
<td><a href="man/man3/Queue.3.html">Queue(3)</a></td>
<td>
Basic fixed-size circular queue implementation.
</td>
</tr>
</table>
<h2 id="download">Download</h2>
<p>
<b>Telodendria</b> is distributed as source tarballs, in true Unix
fashion. If you want, you can verify the checksum of your download,
and check the signature. To check the signature, you'll need
<code>signify</code>, and the signify public key:
<a href="/telodendria-signify.pub">
telodendria-signify.pub</a>.
</p>
<p>
If your operating system has an official package or port of
<b>Telodendria</b>, you should prefer to use that instead of manually
downloading the source and building it. If your operating system's
package or port is too out of date for your tastes, please contact
the package's maintainers to notify them, or offer to update the
package yourself.
</p>
<table>
<tr>
<th>Version</th>
<th>Download</th>
<th>Checksum</th>
<th>Signature</th>
</tr>
<!--
<tr>
<td>v0.0.0</td>
<td>
<a href="/pub/v0.0.0/Telodendria-v0.0.0.tar.gz">
Telodendria-v0.0.0.tar.gz
</a>
</td>
<td>
<a href="/pub/v0.0.0/Telodendria-v0.0.0.tar.gz.sha256">
SHA256
</a>
</td>
<td>
<a href="/pub/v0.0.0/Telodendria-v0.0.0.tar.gz.sig">
Signify
</a>
</td>
</tr>
-->
<tr>
<td colspan="4" style="text-align: center;">
No downloads here yet. See the
<a href="/man/man7/telodendria.7.html#PROJECT_STATUS">Project Status</a> for more
information.
</td>
</tr>
</table>
<p>
You can find the change log in the documentation.
</p>
<h2 id="resources">Resources</h2>
<ul>
<li><a target="_blank" href="/pub">Old <b>Telodendria</b> Versions</a></li>
<li><a target="_blank" href="/spec.matrix.org">Matrix Spec Mirror</a>
(<a href="/matrix-spec-v1.3.tar.gz">Download matrix-spec-v1.3.tar.gz</a>)
</li>
<li><a target="_blank" href="/patches">Public Patch Directory</a></li>
</ul>
<hr>
&copy; 2022 Jordan Bancino &lt;@jordan:bancino.net&gt;
</body>
</html>

605
site/style.css Normal file
View file

@ -0,0 +1,605 @@
:root {
--border-radius: 10px;
--color-snippet: #f6f8fa;
--color-link: #0969da;
--color-bg: #ffffff;
--color-text: #24292f;
--color-table-border: #d0d7de;
--color-table-accent: #f6f8fa;
--color-error-bg: #f44336;
--color-error: white;
}
@media(prefers-color-scheme: dark) {
:root {
--color-snippet: #161b22;
--color-link: #58a6ff;
--color-bg: #0d1117;
--color-text: #c9d1d9;
--color-table-border: #30363d;
--color-table-accent: #161b22;
--color-error-bg: #b71c1c;
--color-error: white;
}
}
body {
margin: auto;
max-width: 8.5in;
padding: 0.25in;
color: var(--color-text);
background-color: var(--color-bg);
font-family: Arial;
}
.msg-error {
background-color: var(--color-error-bg);
color: var(--color-error);
border-radius: var(--border-radius);
padding: 20px;
}
.msg-error a {
color: var(--color-error);
font-weight: bold;
text-decoration: underline
}
.code {
background-color: var(--color-snippet);
border-radius: var(--border-radius);
display: block;
padding: 0.5em 1em 1.5em 1em;
font-family: monospace;
white-space: pre;
overflow: auto;
}
code {
background-color: var(--color-snippet);
border-radius: calc(var(--border-radius) / 2);
}
kbd {
background-color: #eee;
border-radius: 3px;
border: 1px solid var(--color-table-border);
box-shadow: 0 1px 1px rgba(0, 0, 0, .2), 0 2px 0 0 rgba(255, 255, 255, .7) inset;
color: #333;
display: inline-block;
font-size: .85em;
font-weight: 700;
line-height: 1;
padding: 2px 4px;
white-space: nowrap;
}
h1 {
text-align: center;
}
h1, h4, h5, h6 {
border-bottom: 1px dashed var(--color-table-border);
}
hr {
border: 0;
border-bottom: 1px dashed var(--color-table-border);
}
h4, h5, h6 {
width: fit-content;
}
a {
color: var(--color-link);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
table {
width: 100%;
border-collapse: separate;
border-spacing: 0px;
}
td, th {
border: 1px solid var(--color-table-border);
text-align: left;
padding: 8px;
}
tr:nth-child(even) {
background-color: var(--color-table-accent);
}
/* Thanks Jonah! */
#logo {
display: block;
margin: auto;
width: 50vw;
max-width: 400px;
}
/****************************************
* MAN PAGE STYLES
***************************************/
dl {
margin-top: 0em;
margin-bottom: 0em;
}
dt {
margin-top: 1em;
}
pre {
font-family: inherit;
}
.permalink {
border-bottom: thin dotted;
color: inherit;
font: inherit;
text-decoration: inherit;
}
* {
clear: both;
}
/* Header and footer lines. */
table.head {
width: 100%;
margin-bottom: 1em;
font-size: smaller;
}
td.head-vol {
text-align: center;
}
td.head-rtitle {
text-align: right;
}
.head td, .foot td {
border: none;
}
table.foot {
width: 100%;
margin-top: 1em;
font-size: smaller;
border: none;
}
td.foot-os {
text-align: right;
}
/* Sections and paragraphs. */
.manual-text {
margin-left: 3.8em;
}
.Nd {
}
section.Sh {
}
h1.Sh {
text-align: left;
border-bottom: none;
margin-top: 1.2em;
margin-bottom: 0.6em;
margin-left: -3.2em;
}
section.Ss {
}
h2.Ss {
margin-top: 1.2em;
margin-bottom: 0.6em;
margin-left: -1.2em;
font-size: 105%;
}
.Pp {
margin: 0.6em 0em;
}
.Sx {
}
.Xr {
color: var(--color-text);
font-weight: bold;
}
.Xr:hover {
text-decoration: none;
}
/* Displays and lists. */
.Bd {
}
.Bd-indent {
margin-left: 3.8em;
}
.Bl-bullet {
list-style-type: disc;
padding-left: 1em;
}
.Bl-bullet > li {
}
.Bl-dash {
list-style-type: none;
padding-left: 0em;
}
.Bl-dash > li:before {
content: "\2014 ";
}
.Bl-item {
list-style-type: none;
padding-left: 0em;
}
.Bl-item > li {
}
.Bl-compact > li {
margin-top: 0em;
}
.Bl-enum {
padding-left: 2em;
}
.Bl-enum > li {
}
.Bl-compact > li {
margin-top: 0em;
}
.Bl-diag {
}
.Bl-diag > dt {
font-style: normal;
font-weight: bold;
}
.Bl-diag > dd {
margin-left: 0em;
}
.Bl-hang {
}
.Bl-hang > dt {
}
.Bl-hang > dd {
margin-left: 5.5em;
}
.Bl-inset {
}
.Bl-inset > dt {
}
.Bl-inset > dd {
margin-left: 0em;
}
.Bl-ohang {
}
.Bl-ohang > dt {
}
.Bl-ohang > dd {
margin-left: 0em;
}
.Bl-tag {
margin-top: 0.6em;
margin-left: 5.5em;
}
.Bl-tag > dt {
float: left;
margin-top: 0em;
margin-left: -5.5em;
padding-right: 0.5em;
vertical-align: top;
}
.Bl-tag > dd {
clear: right;
column-count: 1; /* Force block formatting context. */
width: 100%;
margin-top: 0em;
margin-left: 0em;
margin-bottom: 0.6em;
vertical-align: top;
}
.Bl-compact {
margin-top: 0em;
}
.Bl-compact > dd {
margin-bottom: 0em;
}
.Bl-compact > dt {
margin-top: 0em;
}
.Bl-column {
}
.Bl-column > tbody > tr {
}
.Bl-column > tbody > tr > td {
margin-top: 1em;
}
.Bl-compact > tbody > tr > td {
margin-top: 0em;
}
.Rs {
font-style: normal;
font-weight: normal;
}
.RsA {
}
.RsB {
font-style: italic;
font-weight: normal;
}
.RsC {
}
.RsD {
}
.RsI {
font-style: italic;
font-weight: normal;
}
.RsJ {
font-style: italic;
font-weight: normal;
}
.RsN {
}
.RsO {
}
.RsP {
}
.RsQ {
}
.RsR {
}
.RsT {
text-decoration: underline;
}
.RsU {
}
.RsV {
}
.eqn {
}
.tbl {
border: none;
border-style: none !important;
}
.tbl td {
vertical-align: middle;
}
.HP {
margin-left: 3.8em;
text-indent: -3.8em;
}
/* Semantic markup for command line utilities. */
table.Nm,
table.Nm td {
border: none;
width: fit-content;
}
code.Nm {
font-style: normal;
font-weight: bold;
font-family: inherit;
}
.Fl {
font-style: normal;
font-weight: bold;
font-family: inherit;
}
.Cm {
font-style: normal;
font-weight: bold;
font-family: inherit;
}
.Ar {
font-style: italic;
font-weight: normal;
}
.Op {
display: inline flow;
}
.Ic {
font-style: normal;
font-weight: bold;
font-family: inherit;
}
.Ev {
font-style: normal;
font-weight: normal;
font-family: monospace;
}
.Pa {
font-style: italic;
font-weight: normal;
}
/* Semantic markup for function libraries. */
.Lb {
}
code.In {
font-style: normal;
font-weight: bold;
font-family: inherit;
}
a.In {
}
.Fd {
font-style: normal;
font-weight: bold;
font-family: inherit;
}
.Ft {
font-style: italic;
font-weight: normal;
}
.Fn {
font-style: normal;
font-weight: bold;
font-family: inherit;
}
.Fa {
font-style: italic;
font-weight: normal;
}
.Vt {
font-style: italic;
font-weight: normal;
}
.Va {
font-style: italic;
font-weight: normal;
}
.Dv {
font-style: normal;
font-weight: normal;
font-family: monospace;
}
.Er {
font-style: normal;
font-weight: normal;
font-family: monospace;
}
/* Various semantic markup. */
.An {
}
.Lk {
}
.Mt {
}
.Cd {
font-style: normal;
font-weight: bold;
font-family: inherit;
}
.Ad {
font-style: italic;
font-weight: normal;
}
.Ms {
font-style: normal;
font-weight: bold;
}
.St {
}
.Ux {
}
/* Physical markup. */
.Bf {
display: inline flow;
}
.No {
font-style: normal;
font-weight: normal;
}
.Em {
font-style: italic;
font-weight: normal;
}
.Sy {
font-style: normal;
font-weight: bold;
}
.Li {
font-style: normal;
font-weight: normal;
font-family: monospace;
}
/* Tooltip support. */
h1.Sh,
h2.Ss {
position: relative;
}
.An,
.Ar,
.Cd,
.Cm,
.Dv,
.Em,
.Er,
.Ev,
.Fa,
.Fd,
.Fl,
.Fn,
.Ft,
.Ic,
code.In,
.Lb,
.Lk,
.Ms,
.Mt,
.Nd,
code.Nm,
.Pa,
.Rs,
.St,
.Sx,
.Sy,
.Va,
.Vt,
.Xr {
display: inline flow;
position: relative;
}
/* Overrides to avoid excessive margins on small devices. */
@media (max-width: 37.5em) {
.manual-text {
margin-left: 0.5em;
}
h1.Sh,
h2.Ss {
margin-left: 0em;
}
.Bd-indent {
margin-left: 2em;
}
.Bl-hang > dd {
margin-left: 2em;
}
.Bl-tag {
margin-left: 2em;
}
.Bl-tag > dt {
margin-left: -2em;
}
.HP {
margin-left: 2em;
text-indent: -2em;
}
}

View file

@ -0,0 +1,2 @@
untrusted comment: signify public key
RWTPPnWvnpee8NlygSggQqk5V5oghl6Ikq99bZl5IRQwiRMLaJnq82mw

174
src/Array.c Normal file
View file

@ -0,0 +1,174 @@
#include <Array.h>
#ifndef ARRAY_BLOCK
#define ARRAY_BLOCK 16
#endif
#include <stddef.h>
#include <stdlib.h>
struct Array {
void **entries; /* An array of void pointers, to store any data */
size_t allocated; /* Elements allocated on the heap */
size_t size; /* Elements actually filled */
};
int
ArrayAdd(Array *array, void *value)
{
if (!array)
{
return 0;
}
return ArrayInsert(array, value, array->size);
}
Array *
ArrayCreate(void)
{
Array *array = malloc(sizeof(Array));
if (!array)
{
return NULL;
}
array->size = 0;
array->allocated = ARRAY_BLOCK;
array->entries = malloc(sizeof(void *) * ARRAY_BLOCK);
if (!array->entries)
{
free(array);
return NULL;
}
return array;
}
void *
ArrayDelete(Array *array, size_t index)
{
size_t i;
void *element;
if (!array)
{
return NULL;
}
element = array->entries[index];
for (i = index; i < array->size - 1; i++)
{
array->entries[i] = array->entries[i + 1];
}
array->size--;
return element;
}
void
ArrayFree(Array *array)
{
if (array)
{
free(array->entries);
free(array);
}
}
void *
ArrayGet(Array *array, size_t index)
{
if (!array)
{
return NULL;
}
if (index >= array->size)
{
return NULL;
}
return array->entries[index];
}
extern int
ArrayInsert(Array *array, void *value, size_t index)
{
size_t i;
if (!array || !value || index > array->size)
{
return 0;
}
if (array->size >= array->allocated)
{
void **tmp;
size_t newSize = array->allocated + ARRAY_BLOCK;
tmp = array->entries;
array->entries = realloc(array->entries,
sizeof(void *) * newSize);
if (!array->entries)
{
array->entries = tmp;
return 0;
}
array->allocated = newSize;
}
for (i = array->size; i > index; i--)
{
array->entries[i] = array->entries[i - 1];
}
array->size++;
array->entries[index] = value;
return 1;
}
size_t
ArraySize(Array *array)
{
if (!array)
{
return 0;
}
return array->size;
}
int
ArrayTrim(Array *array)
{
void **tmp;
if (!array)
{
return 0;
}
tmp = array->entries;
array->entries = realloc(array->entries,
sizeof(void *) * array->size);
if (!array->entries)
{
array->entries = tmp;
return 0;
}
return 1;
}

212
src/Base64.c Normal file
View file

@ -0,0 +1,212 @@
#include <Base64.h>
#include <stdlib.h>
static const char Base64EncodeMap[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
static const int Base64DecodeMap[] = {
62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58,
59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5,
6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28,
29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42,
43, 44, 45, 46, 47, 48, 49, 50, 51
};
size_t
Base64EncodedSize(size_t inputSize)
{
size_t size = inputSize;
if (inputSize % 3)
{
size += 3 - (inputSize % 3);
}
size /= 3;
size *= 4;
return size;
}
size_t
Base64DecodedSize(const char *base64, size_t len)
{
size_t ret;
size_t i;
if (!base64)
{
return 0;
}
ret = len / 4 * 3;
for (i = len; i > 0; i--) {
if (base64[i] == '=') {
ret--;
} else {
break;
}
}
return ret;
}
char *
Base64Encode(const char *input, size_t len)
{
char *out;
size_t outLen;
size_t i, j, v;
if (!input || !len)
{
return NULL;
}
outLen = Base64EncodedSize(len);
out = malloc(outLen + 1);
if (!out)
{
return NULL;
}
out[outLen] = '\0';
for (i = 0, j = 0; i < len; i += 3, j += 4)
{
v = input[i];
v = i + 1 < len ? v << 8 | input[i + 1] : v << 8;
v = i + 2 < len ? v << 8 | input[i + 2] : v << 8;
out[j] = Base64EncodeMap[(v >> 18) & 0x3F];
out[j + 1] = Base64EncodeMap[(v >> 12) & 0x3F];
if (i + 1 < len)
{
out[j + 2] = Base64EncodeMap[(v >> 6) & 0x3F];
} else {
out[j + 2] = '=';
}
if (i + 2 < len) {
out[j + 3] = Base64EncodeMap[v & 0x3F];
} else {
out[j + 3] = '=';
}
}
return out;
}
static int
Base64IsValidChar(char c)
{
return (c >= '0' && c <= '9') ||
(c >= 'A' && c <= 'Z') ||
(c >= 'a' && c <= 'z') ||
(c == '+') ||
(c == '/') ||
(c == '=');
}
char *
Base64Decode(const char *input, size_t len)
{
size_t i, j;
int v;
size_t outLen;
char *out;
if (!input)
{
return NULL;
}
outLen = Base64DecodedSize(input, len);
if (len % 4)
{
/* Invalid length; must have incorrect padding */
return NULL;
}
/* Scan for invalid characters. */
for (i = 0; i < len; i++)
{
if (!Base64IsValidChar(input[i]))
{
return NULL;
}
}
out = malloc(outLen + 1);
if (!out)
{
return NULL;
}
out[outLen] = '\0';
for (i = 0, j = 0; i < len; i += 4, j += 3) {
v = Base64DecodeMap[input[i] - 43];
v = (v << 6) | Base64DecodeMap[input[i + 1] - 43];
v = input[i + 2] == '=' ? v << 6 : (v << 6) | Base64DecodeMap[input[i + 2] - 43];
v = input[i + 3] == '=' ? v << 6 : (v << 6) | Base64DecodeMap[input[i + 3] - 43];
out[j] = (v >> 16) & 0xFF;
if (input[i + 2] != '=')
out[j + 1] = (v >> 8) & 0xFF;
if (input[i + 3] != '=')
out[j + 2] = v & 0xFF;
}
return out;
}
extern void
Base64Unpad(char *base64, size_t length)
{
if (!base64)
{
return;
}
while (base64[length - 1] == '=')
{
length--;
}
base64[length] = '\0';
}
extern int
Base64Pad(char **base64Ptr, size_t length)
{
char *tmp;
size_t newSize;
size_t i;
if (length % 4 == 0)
{
return length; /* Success: no padding needed */
}
newSize = length + (4 - (length % 4));
tmp = realloc(*base64Ptr, newSize + 100);;
if (!tmp)
{
return 0; /* Memory error */
}
*base64Ptr = tmp;
for (i = length; i < newSize; i++)
{
(*base64Ptr)[i] = '=';
}
(*base64Ptr)[newSize] = '\0';
return newSize;
}

View file

@ -1,6 +1,5 @@
/* /*
* Copyright (C) 2022-2024 Jordan Bancino <@jordan:bancino.net> with * Copyright (C) 2022 Jordan Bancino <@jordan:bancino.net>
* other valuable contributors. See CONTRIBUTORS.txt for the full list.
* *
* Permission is hereby granted, free of charge, to any person * Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation files * obtaining a copy of this software and associated documentation files
@ -24,39 +23,36 @@
*/ */
#include <CanonicalJson.h> #include <CanonicalJson.h>
#include <Cytoplasm/HashMap.h> #include <HashMap.h>
#include <Cytoplasm/Array.h> #include <Array.h>
#include <Cytoplasm/Json.h> #include <Json.h>
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
static int int
CanonicalJsonKeyCompare(void *k1, void *k2) CanonicalJsonKeyCompare(void *k1, void *k2)
{ {
return strcmp((char *) k1, (char *) k2); return strcmp((char *) k1, (char *) k2);
} }
int static void
CanonicalJsonEncodeValue(JsonValue * value, Stream * out) CanonicalJsonEncodeValue(JsonValue * value, FILE * out)
{ {
Array *arr; Array *arr;
size_t i, len; size_t i, len;
int length = 0;
/* Override object type to encode using the canonical functions */ /* Override object type to encode using the canonical functions */
switch (JsonValueType(value)) switch (JsonValueType(value))
{ {
case JSON_OBJECT: case JSON_OBJECT:
length += CanonicalJsonEncode(JsonValueAsObject(value), out); CanonicalJsonEncode(JsonValueAsObject(value), out);
break; break;
case JSON_ARRAY: case JSON_ARRAY:
arr = JsonValueAsArray(value); arr = JsonValueAsArray(value);
len = ArraySize(arr); len = ArraySize(arr);
StreamPutc(out, '['); fputc('[', out);
length++;
for (i = 0; i < len; i++) for (i = 0; i < len; i++)
{ {
@ -68,44 +64,39 @@ CanonicalJsonEncodeValue(JsonValue * value, Stream * out)
continue; continue;
} }
length += CanonicalJsonEncodeValue(aVal, out); CanonicalJsonEncodeValue(aVal, out);
if (i < len - 1) if (i < len - 1)
{ {
StreamPutc(out, ','); fputc(',', out);
length++;
} }
} }
StreamPutc(out, ']'); fputc(']', out);
length++;
break; break;
default: default:
length += JsonEncodeValue(value, out, JSON_DEFAULT); JsonEncodeValue(value, out);
break; break;
} }
return length;
} }
int int
CanonicalJsonEncode(HashMap * object, Stream * out) CanonicalJsonEncode(HashMap * object, FILE * out)
{ {
char *key; char *key;
JsonValue *value; JsonValue *value;
Array *keys; Array *keys;
size_t i; size_t i;
size_t keyCount; size_t keyCount;
int length;
if (!object) if (!object || !out)
{ {
return -1; return 0;
} }
keys = ArrayCreate(); keys = ArrayCreate();
if (!keys) if (!keys)
{ {
return -1; return 0;
} }
while (HashMapIterate(object, &key, (void **) &value)) while (HashMapIterate(object, &key, (void **) &value))
@ -115,11 +106,7 @@ CanonicalJsonEncode(HashMap * object, Stream * out)
ArraySort(keys, CanonicalJsonKeyCompare); ArraySort(keys, CanonicalJsonKeyCompare);
/* The total number of bytes written */ fputc('{', out);
length = 0;
StreamPutc(out, '{');
length++;
keyCount = ArraySize(keys); keyCount = ArraySize(keys);
for (i = 0; i < keyCount; i++) for (i = 0; i < keyCount; i++)
@ -141,21 +128,18 @@ CanonicalJsonEncode(HashMap * object, Stream * out)
continue; continue;
} }
length += JsonEncodeString(key, out); JsonEncodeString(key, out);
StreamPutc(out, ':'); fputc(':', out);
length++; CanonicalJsonEncodeValue(value, out);
length += CanonicalJsonEncodeValue(value, out);
if (i < keyCount - 1) if (i < keyCount - 1)
{ {
StreamPutc(out, ','); fputc(',', out);
length++;
} }
} }
StreamPutc(out, '}'); fputc('}', out);
length++;
ArrayFree(keys); ArrayFree(keys);
return length; return 1;
} }

View file

@ -1,232 +1,452 @@
/* #include <Config.h>
* Copyright (C) 2022-2024 Jordan Bancino <@jordan:bancino.net> with
* other valuable contributors. See CONTRIBUTORS.txt for the full list.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation files
* (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <Schema/Config.h>
#include <Cytoplasm/Memory.h>
#include <Cytoplasm/Json.h>
#include <Cytoplasm/HashMap.h>
#include <Cytoplasm/Array.h>
#include <Cytoplasm/Str.h>
#include <Cytoplasm/Db.h>
#include <Cytoplasm/Log.h>
#include <Cytoplasm/Util.h>
#include <sys/types.h>
#include <stdlib.h> #include <stdlib.h>
#include <ctype.h> #include <ctype.h>
#include <string.h> #include <string.h>
#include <limits.h>
#include <grp.h>
#include <pwd.h>
#ifndef HOST_NAME_MAX #ifndef CONFIG_BUFFER_BLOCK
#define HOST_NAME_MAX _POSIX_HOST_NAME_MAX #define CONFIG_BUFFER_BLOCK 32
#endif #endif
void struct ConfigDirective {
ConfigParse(HashMap * config, Config *tConfig) Array *values;
HashMap *children;
};
struct ConfigParseResult {
unsigned int ok : 1;
union {
size_t lineNumber;
HashMap *confMap;
} data;
};
typedef enum ConfigToken {
TOKEN_UNKNOWN,
TOKEN_NAME,
TOKEN_MACRO_ASSIGNMENT,
TOKEN_VALUE,
TOKEN_SEMICOLON,
TOKEN_BLOCK_OPEN,
TOKEN_BLOCK_CLOSE,
TOKEN_MACRO,
TOKEN_EOF
} ConfigToken;
typedef struct ConfigParserState {
FILE *stream;
unsigned int line;
char *token;
size_t tokenSize;
size_t tokenLen;
ConfigToken tokenType;
HashMap *macroMap;
} ConfigParserState;
unsigned int
ConfigParseResultOk(ConfigParseResult *result)
{ {
return result ? result->ok : 0;
}
size_t
ConfigParseResultLineNumber(ConfigParseResult *result)
{
return result && !result->ok ? result->data.lineNumber : 0;
}
HashMap *
ConfigParseResultGet(ConfigParseResult *result)
{
return result && result->ok ? result->data.confMap : NULL;
}
void
ConfigParseResultFree(ConfigParseResult *result)
{
/*
* Note that if the parse was valid, the hash map
* needs to be freed separately.
*/
free(result);
}
Array *
ConfigValuesGet(ConfigDirective *directive)
{
return directive ? directive->values : NULL;
}
HashMap *
ConfigChildrenGet(ConfigDirective *directive)
{
return directive ? directive->children : NULL;
}
/*
* Takes a void pointer because it is only used with
* HashMapIterate(), which requires a pointer to a function
* that takes a void pointer.
*/
static void
ConfigDirectiveFree(void *ptr)
{
ConfigDirective *directive = ptr;
size_t i; size_t i;
if (!config) if (!directive)
{ {
tConfig->ok = 0;
tConfig->err = "Invalid object given as config.";
return; return;
} }
memset(tConfig, 0, sizeof(Config)); for (i = 0; i < ArraySize(directive->values); i++)
tConfig->maxCache = 0;
if (!ConfigFromJson(config, tConfig, &tConfig->err))
{ {
ConfigFree(tConfig); free(ArrayGet(directive->values, i));
goto error;
} }
if (!tConfig->baseUrl)
{
size_t len = strlen(tConfig->serverName) + 10;
tConfig->baseUrl = Malloc(len); ArrayFree(directive->values);
if (!tConfig->baseUrl) ConfigFree(directive->children);
{
tConfig->err = "Couldn't allocate enough memory for 'baseUrl'.";
goto error;
}
snprintf(tConfig->baseUrl, len, "https://%s/", tConfig->serverName);
}
if (!tConfig->log.timestampFormat)
{
tConfig->log.timestampFormat = StrDuplicate("default");
}
for (i = 0; i < ArraySize(tConfig->listen); i++)
{
ConfigListener *listener = ArrayGet(tConfig->listen, i);
if (!listener->maxConnections)
{
listener->maxConnections = 32;
}
if (!listener->threads)
{
listener->threads = 4;
}
if (!listener->port)
{
listener->port = 8008;
}
}
tConfig->ok = 1;
tConfig->err = NULL;
return;
error: free(directive);
tConfig->ok = 0;
return;
}
int
ConfigExists(Db * db)
{
return DbExists(db, 1, "config");
}
int
ConfigCreateDefault(Db * db)
{
Config config;
ConfigListener *listener;
HashMap *json;
JsonValue *val;
DbRef *ref;
size_t len;
memset(&config, 0, sizeof(Config));
config.log.output = CONFIG_LOG_OUTPUT_FILE;
config.runAs.gid = StrDuplicate(getgrgid(getgid())->gr_name);
config.runAs.uid = StrDuplicate(getpwuid(getuid())->pw_name);
config.registration = 0;
config.federation = 1;
/* Create serverName and baseUrl. */
config.serverName = Malloc(HOST_NAME_MAX + 1);
memset(config.serverName, 0, HOST_NAME_MAX + 1);
gethostname(config.serverName, HOST_NAME_MAX);
len = strlen(config.serverName) + 10;
config.baseUrl = Malloc(len);
snprintf(config.baseUrl, len, "https://%s/", config.serverName);
/* Add simple listener without TLS. */
config.listen = ArrayCreate();
listener = Malloc(sizeof(ConfigListener));
listener->maxConnections = 32;
listener->port = 8008;
listener->threads = 4;
ArrayAdd(config.listen, listener);
/* Write it all out to the configuration file. */
json = ConfigToJson(&config);
val = JsonGet(json, 1, "listen");
val = ArrayGet(JsonValueAsArray(val), 0);
JsonValueFree(HashMapDelete(JsonValueAsObject(val), "tls"));
ref = DbCreate(db, 1, "config");
if (!ref)
{
ConfigFree(&config);
return 0;
}
DbJsonSet(ref, json);
DbUnlock(db, ref);
ConfigFree(&config);
JsonFree(json);
return 1;
} }
void void
ConfigLock(Db * db, Config *config) ConfigFree(HashMap *conf)
{ {
DbRef *ref = DbLock(db, 1, "config"); HashMapIterate(conf, ConfigDirectiveFree);
HashMapFree(conf);
}
if (!ref) static ConfigParserState *
ConfigParserStateCreate(FILE * stream)
{
ConfigParserState *state = malloc(sizeof(ConfigParserState));
if (!state)
{ {
config->ok = 0; return NULL;
config->err = "Couldn't lock configuration.";
} }
ConfigParse(DbJson(ref), config); state->macroMap = HashMapCreate();
if (config->ok)
if (!state->macroMap)
{ {
config->db = db; free(state);
config->ref = ref; return NULL;
}
state->stream = stream;
state->line = 1;
state->token = NULL;
state->tokenSize = 0;
state->tokenLen = 0;
state->tokenType = TOKEN_UNKNOWN;
return state;
}
static void
ConfigParserStateFree(ConfigParserState *state)
{
if (!state)
{
return;
}
free(state->token);
HashMapIterate(state->macroMap, free);
HashMapFree(state->macroMap);
free(state);
}
static int
ConfigIsNameChar(int c)
{
return isdigit(c) || isalpha(c) || (c == '-' || c == '_');
}
static char
ConfigConsumeWhitespace(ConfigParserState *state)
{
int c;
while (isspace(c = fgetc(state->stream)))
{
if (c == '\n')
{
state->line++;
}
}
return c;
}
static void
ConfigConsumeLine(ConfigParserState *state)
{
while (fgetc(state->stream) != '\n');
state->line++;
}
static void
ConfigTokenSeek(ConfigParserState *state)
{
int c;
/* If we already hit EOF, don't do anything */
if (state->tokenType == TOKEN_EOF) {
return;
}
while ((c = ConfigConsumeWhitespace(state)) == '#') {
ConfigConsumeLine(state);
}
/*
* After all whitespace and comments are consumed, identify the
* token by looking at the next character
*/
if (feof(state->stream)) {
state->tokenType = TOKEN_EOF;
return;
}
if (ConfigIsNameChar(c)) {
state->tokenLen = 0;
/* Read the key/macro into state->token */
if (!state->token) {
state->tokenSize = CONFIG_BUFFER_BLOCK;
state->token = malloc(CONFIG_BUFFER_BLOCK);
}
state->token[state->tokenLen] = c;
state->tokenLen++;
while (ConfigIsNameChar((c = fgetc(state->stream)))) {
state->token[state->tokenLen] = c;
state->tokenLen++;
if (state->tokenLen >= state->tokenSize) {
state->tokenSize += CONFIG_BUFFER_BLOCK;
state->token = realloc(state->token,
state->tokenSize);
}
}
state->token[state->tokenLen] = '\0';
state->tokenLen++;
if (!isspace(c)) {
state->tokenType = TOKEN_UNKNOWN;
} else {
state->tokenType = TOKEN_NAME;
if (c == '\n') {
state->line++;
}
}
} else {
switch (c) {
case '=':
state->tokenType = TOKEN_MACRO_ASSIGNMENT;
break;
case '"':
state->tokenLen = 0;
state->tokenType = TOKEN_VALUE;
/* read the value into state->curtok */
while ((c = fgetc(state->stream)) != '"') {
if (c == '\n') {
state->line++;
}
/*
* End of the stream reached without finding
* a closing quote
*/
if (feof(state->stream)) {
state->tokenType = TOKEN_EOF;
break;
}
state->token[state->tokenLen] = c;
state->tokenLen++;
if (state->tokenLen >= state->tokenSize) {
state->tokenSize += CONFIG_BUFFER_BLOCK;
state->token = realloc(state->token,
state->tokenSize);
}
}
state->token[state->tokenLen] = '\0';
state->tokenLen++;
break;
case ';':
state->tokenType = TOKEN_SEMICOLON;
break;
case '{':
state->tokenType = TOKEN_BLOCK_OPEN;
break;
case '}':
state->tokenType = TOKEN_BLOCK_CLOSE;
break;
case '$':
state->tokenLen = 0;
/* read the macro name into state->curtok */
while (ConfigIsNameChar(c = fgetc(state->stream))) {
state->token[state->tokenLen] = c;
state->tokenLen++;
if (state->tokenLen >= state->tokenSize) {
state->tokenSize += CONFIG_BUFFER_BLOCK;
state->token = realloc(state->token,
state->tokenSize);
}
}
state->token[state->tokenLen] = '\0';
state->tokenLen++;
state->tokenType = TOKEN_MACRO;
ungetc(c, state->stream);
break;
default:
state->tokenType = TOKEN_UNKNOWN;
break;
}
}
/* Resize curtok to only use the bytes it needs */
if (state->tokenLen) {
state->tokenSize = state->tokenLen;
state->token = realloc(state->token, state->tokenSize);
} }
} }
int static int
ConfigUnlock(Config *config) ConfigExpect(ConfigParserState *state, ConfigToken tokenType)
{ {
Db *db; return state->tokenType == tokenType;
DbRef *dbRef; }
if (!config->ok)
{ static HashMap *
return 0; ConfigParseBlock(ConfigParserState *state, int level)
{
HashMap *block = HashMapCreate();
ConfigTokenSeek(state);
while (ConfigExpect(state, TOKEN_NAME)) {
char *name = malloc(state->tokenLen + 1);
strcpy(name, state->token);
ConfigTokenSeek(state);
if (ConfigExpect(state, TOKEN_VALUE) || ConfigExpect(state, TOKEN_MACRO)) {
ConfigDirective *directive;
directive = malloc(sizeof(ConfigDirective));
directive->children = NULL;
directive->values = ArrayCreate();
while (ConfigExpect(state, TOKEN_VALUE) ||
ConfigExpect(state, TOKEN_MACRO)) {
char *dval;
char *dvalCpy;
if (ConfigExpect(state, TOKEN_VALUE)) {
dval = state->token;
} else if (ConfigExpect(state, TOKEN_MACRO)) {
dval = HashMapGet(state->macroMap, state->token);
if (!dval) {
goto error;
}
} else {
dval = NULL; /* Should never happen */
} }
db = config->db; /* dval is a pointer which is overwritten with the next token. */
dbRef = config->ref; dvalCpy = malloc(strlen(dval) + 1);
strcpy(dvalCpy, dval);
ConfigFree(config); ArrayAdd(directive->values, dvalCpy);
config->ok = 0; ConfigTokenSeek(state);
return DbUnlock(db, dbRef);
}
int
ConfigLogLevelToSyslog(ConfigLogLevel level)
{
switch (level)
{
case CONFIG_LOG_LEVEL_NOTICE:
return LOG_NOTICE;
case CONFIG_LOG_LEVEL_ERROR:
return LOG_ERR;
case CONFIG_LOG_LEVEL_MESSAGE:
return LOG_INFO;
case CONFIG_LOG_LEVEL_DEBUG:
return LOG_DEBUG;
case CONFIG_LOG_LEVEL_WARNING:
return LOG_WARNING;
} }
return LOG_INFO;
if (ConfigExpect(state, TOKEN_BLOCK_OPEN)) {
/* token_seek(state); */
directive->children = ConfigParseBlock(state, level + 1);
if (!directive->children) {
goto error;
}
}
/*
* Append this directive to the current block,
* overwriting a directive at this level with the same name.
*
* Note that if a value already exists with this name, it is
* returned by HashMapSet() and then immediately passed to
* ConfigDirectiveFree(). If the value does not exist, then
* NULL is sent to ConfigDirectiveFree(), making it a no-op.
*/
ConfigDirectiveFree(HashMapSet(block, name, directive));
} else if (ConfigExpect(state, TOKEN_MACRO_ASSIGNMENT)) {
ConfigTokenSeek(state);
if (ConfigExpect(state, TOKEN_VALUE)) {
char * valueCopy = malloc(strlen(state->token) + 1);
strcpy(valueCopy, state->token);
free(HashMapSet(state->macroMap, name, state->token));
ConfigTokenSeek(state);
} else {
goto error;
}
} else {
goto error;
}
if (!ConfigExpect(state, TOKEN_SEMICOLON)) {
goto error;
}
ConfigTokenSeek(state);
}
if (ConfigExpect(state, level ? TOKEN_BLOCK_CLOSE : TOKEN_EOF)) {
ConfigTokenSeek(state);
return block;
} else {
goto error;
}
error:
/* Only free the very top level, because this will recurse */
if (!level) {
ConfigFree(block);
}
return NULL;
} }
ConfigParseResult *
ConfigParse(FILE * stream)
{
ConfigParseResult *result;
HashMap *conf;
ConfigParserState *state;
result = malloc(sizeof(ConfigParseResult));
state = ConfigParserStateCreate(stream);
conf = ConfigParseBlock(state, 0);
if (!conf) {
result->ok = 0;
result->data.lineNumber = state->line;
} else {
result->ok = 1;
result->data.confMap = conf;
}
ConfigParserStateFree(state);
return result;
}

View file

@ -1,6 +1,5 @@
/* /*
* Copyright (C) 2022-2024 Jordan Bancino <@jordan:bancino.net> with * Copyright (C) 2022 Jordan Bancino <@jordan:bancino.net>
* other valuable contributors. See CONTRIBUTORS.txt for the full list.
* *
* Permission is hereby granted, free of charge, to any person * Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation files * obtaining a copy of this software and associated documentation files
@ -22,103 +21,120 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE. * SOFTWARE.
*/ */
#include <Db.h>
#include <Room.h> #include <Memory.h>
#include <Cytoplasm/Memory.h> #include <pthread.h>
#include <Cytoplasm/Str.h>
#include <Cytoplasm/Db.h>
#include <Schema/RoomCreateRequest.h> struct Db
struct Room
{ {
Db *db; const char *dir;
DbRef *ref; size_t cacheSize;
size_t maxCache;
pthread_mutex_t lock;
char *id; HashMap *cache;
int version;
}; };
Room * struct DbRef
RoomCreate(Db * db, RoomCreateRequest * req)
{ {
(void) db; HashMap *json;
(void) req; pthread_mutex_t lock;
return NULL;
}
Room * unsigned long ts;
RoomLock(Db * db, char *id) char *file;
{ };
DbRef *ref;
Room *room;
if (!db || !id) Db *
{ DbOpen(const char *dir, size_t cache)
return NULL;
}
ref = DbLock(db, 3, "rooms", id, "state");
if (!ref)
{
return NULL;
}
room = Malloc(sizeof(Room));
if (!room)
{
DbUnlock(db, ref);
return NULL;
}
room->db = db;
room->ref = ref;
room->id = StrDuplicate(id);
return room;
}
int
RoomUnlock(Room * room)
{ {
Db *db; Db *db;
DbRef *ref;
if (!room) if (!dir)
{ {
return 0; return NULL;
} }
db = room->db; db = Malloc(sizeof(Db));
ref = room->ref; if (!db)
{
return NULL;
}
Free(room->id); db->dir = dir;
Free(room); db->maxCache = cache;
return DbUnlock(db, ref); pthread_mutex_init(&db->lock, NULL);
if (db->maxCache)
{
db->cache = HashMapCreate();
if (!db->cache)
{
return NULL;
}
}
return db;
} }
char * void
RoomIdGet(Room * room) DbClose(Db * db)
{ {
return room ? room->id : NULL; if (!db)
{
return;
}
pthread_mutex_destroy(&db->lock);
HashMapFree(db->cache);
Free(db);
} }
int DbRef *
RoomVersionGet(Room * room) DbCreate(Db * db, char *prefix, char *key)
{ {
return room ? room->version : 0; if (!db || !prefix || !key)
{
return NULL;
}
return NULL;
}
DbRef *
DbLock(Db * db, char *prefix, char *key)
{
if (!db || !prefix || !key)
{
return NULL;
}
pthread_mutex_lock(&db->lock);
pthread_mutex_unlock(&db->lock);
return NULL;
}
void
DbUnlock(Db * db, DbRef * ref)
{
if (!db || !ref)
{
return;
}
pthread_mutex_lock(&db->lock);
pthread_mutex_unlock(&db->lock);
return;
} }
HashMap * HashMap *
RoomStateGet(Room * room) DbJson(DbRef * ref)
{ {
if (!room) return ref ? ref->json : NULL;
{
return NULL;
}
return NULL;
} }

300
src/HashMap.c Normal file
View file

@ -0,0 +1,300 @@
#include <HashMap.h>
#include <stdint.h>
#include <stddef.h>
#include <stdlib.h>
typedef struct HashMapBucket {
uint32_t hash;
void *value;
} HashMapBucket;
struct HashMap {
size_t count;
size_t capacity;
HashMapBucket **entries;
float maxLoad;
};
static uint32_t
HashMapHashKey(const char *key)
{
uint32_t hash = 2166136261u;
size_t i = 0;
while (key[i])
{
hash ^= (uint8_t) key[i];
hash *= 16777619;
i++;
}
return hash;
}
static int
HashMapGrow(HashMap *map)
{
size_t oldCapacity;
size_t i;
HashMapBucket **newEntries;
if (!map)
{
return 0;
}
oldCapacity = map->capacity;
map->capacity *= 2;
newEntries = calloc(map->capacity, sizeof(HashMapBucket *));
if (!newEntries)
{
return 0;
}
for (i = 0; i < oldCapacity; i++)
{
/* If there is a value here, and it isn't a tombstone */
if (map->entries[i] && map->entries[i]->hash)
{
/* Copy it to the new entries array */
size_t index = map->entries[i]->hash % map->capacity;
for (;;)
{
if (newEntries[index])
{
if (!newEntries[index]->hash)
{
free(newEntries[index]);
newEntries[index] = map->entries[i];
break;
}
}
else
{
newEntries[index] = map->entries[i];
break;
}
index = (index + 1) % map->capacity;
}
}
else
{
/* Either NULL or a tombstone */
free(map->entries[i]);
}
}
free(map->entries);
map->entries = newEntries;
return 1;
}
HashMap *
HashMapCreate(void)
{
HashMap *map = malloc(sizeof(HashMap));
if (!map)
{
return NULL;
}
map->maxLoad = 0.75;
map->count = 0;
map->capacity = 16;
map->entries = calloc(map->capacity, sizeof(HashMapBucket *));
if (!map->entries)
{
free(map);
return NULL;
}
return map;
}
void *
HashMapDelete(HashMap *map, const char *key)
{
uint32_t hash;
size_t index;
if (!map || !key)
{
return NULL;
}
hash = HashMapHashKey(key);
index = hash % map->capacity;
for (;;)
{
HashMapBucket *bucket = map->entries[index];
if (!bucket)
{
break;
}
if (bucket->hash == hash)
{
bucket->hash = 0;
return bucket->value;
}
index = (index + 1) % map->capacity;
}
return NULL;
}
void
HashMapFree(HashMap *map)
{
if (map)
{
size_t i;
for (i = 0; i < map->capacity; i++)
{
if (map->entries[i])
{
free(map->entries[i]);
}
}
}
free(map);
}
void *
HashMapGet(HashMap *map, const char *key)
{
uint32_t hash;
size_t index;
if (!map || !key)
{
return NULL;
}
hash = HashMapHashKey(key);
index = hash % map->capacity;
for (;;)
{
HashMapBucket *bucket = map->entries[index];
if (!bucket)
{
break;
}
if (bucket->hash == hash)
{
return bucket->value;
}
index = (index + 1) % map->capacity;
}
return NULL;
}
void
HashMapIterate(HashMap *map, void (*iteratorFunc)(void *))
{
size_t i;
if (!map)
{
return;
}
for (i = 0; i < map->capacity; i++)
{
HashMapBucket *bucket = map->entries[i];
if (bucket)
{
iteratorFunc(bucket->value);
}
}
}
void
HashMapMaxLoadSet(HashMap *map, float load)
{
if (!map)
{
return;
}
map->maxLoad = load;
}
void *
HashMapSet(HashMap *map, const char *key, void *value)
{
uint32_t hash;
size_t index;
if (!map || !key || !value)
{
return NULL;
}
if (map->count + 1 > map->capacity * map->maxLoad)
{
HashMapGrow(map);
}
hash = HashMapHashKey(key);
index = hash % map->capacity;
for (;;)
{
HashMapBucket *bucket = map->entries[index];
if (!bucket)
{
bucket = malloc(sizeof(HashMapBucket));
if (!bucket)
{
break;
}
bucket->hash = hash;
bucket->value = value;
map->entries[index] = bucket;
map->count++;
break;
}
if (!bucket->hash)
{
bucket->hash = hash;
bucket->value = value;
break;
}
if (bucket->hash == hash)
{
void *oldValue = bucket->value;
bucket->value = value;
return oldValue;
}
index = (index + 1) % map->capacity;
}
return NULL;
}

View file

@ -1,73 +0,0 @@
/*
* Copyright (C) 2022-2024 Jordan Bancino <@jordan:bancino.net> with
* other valuable contributors. See CONTRIBUTORS.txt for the full list.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation files
* (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <Html.h>
#include <stdio.h>
#include <Telodendria.h>
void
HtmlBegin(Stream * stream, char *title)
{
size_t i;
if (!stream)
{
return;
}
StreamPrintf(stream,
"<!DOCTYPE html>"
"<html>"
"<head>"
"<meta charset=\"utf-8\">"
"<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">"
"<title>%s | Telodendria</title>"
"<link rel=\"stylesheet\" href=\"/_matrix/static/telodendria.css\">"
"<script src=\"/_matrix/static/telodendria.js\"></script>"
"</head>"
"<body>"
"<pre class=\"logo\">"
,title
);
for (i = 0; i < TELODENDRIA_LOGO_HEIGHT; i++)
{
StreamPrintf(stream, "%s\n", TelodendriaLogo[i]);
}
StreamPrintf(stream,
"</pre>"
"<h1>%s</h1>"
,title);
}
void
HtmlEnd(Stream * stream)
{
StreamPuts(stream,
"</body>"
"</html>");
}

526
src/Http.c Normal file
View file

@ -0,0 +1,526 @@
/*
* Copyright (C) 2022 Jordan Bancino <@jordan:bancino.net>
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation files
* (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <Http.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <Memory.h>
#include <Constants.h>
#include <HashMap.h>
const char *
HttpRequestMethodToString(const HttpRequestMethod method)
{
switch (method)
{
case HTTP_GET:
return "GET";
case HTTP_HEAD:
return "HEAD";
case HTTP_POST:
return "POST";
case HTTP_PUT:
return "PUT";
case HTTP_DELETE:
return "DELETE";
case HTTP_CONNECT:
return "CONNECT";
case HTTP_OPTIONS:
return "OPTIONS";
case HTTP_TRACE:
return "TRACE";
case HTTP_PATCH:
return "PATCH";
default:
return NULL;
}
}
HttpRequestMethod
HttpRequestMethodFromString(const char *str)
{
if (strcmp(str, "GET") == 0)
{
return HTTP_GET;
}
if (strcmp(str, "HEAD") == 0)
{
return HTTP_HEAD;
}
if (strcmp(str, "POST") == 0)
{
return HTTP_POST;
}
if (strcmp(str, "PUT") == 0)
{
return HTTP_PUT;
}
if (strcmp(str, "DELETE") == 0)
{
return HTTP_DELETE;
}
if (strcmp(str, "CONNECT") == 0)
{
return HTTP_CONNECT;
}
if (strcmp(str, "OPTIONS") == 0)
{
return HTTP_OPTIONS;
}
if (strcmp(str, "TRACE") == 0)
{
return HTTP_TRACE;
}
if (strcmp(str, "PATCH") == 0)
{
return HTTP_PATCH;
}
return HTTP_METHOD_UNKNOWN;
}
const char *
HttpStatusToString(const HttpStatus status)
{
switch (status)
{
case HTTP_CONTINUE:
return "Continue";
case HTTP_SWITCHING_PROTOCOLS:
return "Switching Protocols";
case HTTP_EARLY_HINTS:
return "Early Hints";
case HTTP_OK:
return "Ok";
case HTTP_CREATED:
return "Created";
case HTTP_ACCEPTED:
return "Accepted";
case HTTP_NON_AUTHORITATIVE_INFORMATION:
return "Non-Authoritative Information";
case HTTP_NO_CONTENT:
return "No Content";
case HTTP_RESET_CONTENT:
return "Reset Content";
case HTTP_PARTIAL_CONTENT:
return "Partial Content";
case HTTP_MULTIPLE_CHOICES:
return "Multiple Choices";
case HTTP_MOVED_PERMANENTLY:
return "Moved Permanently";
case HTTP_FOUND:
return "Found";
case HTTP_SEE_OTHER:
return "See Other";
case HTTP_NOT_MODIFIED:
return "Not Modified";
case HTTP_TEMPORARY_REDIRECT:
return "Temporary Redirect";
case HTTP_PERMANENT_REDIRECT:
return "Permanent Redirect";
case HTTP_BAD_REQUEST:
return "Bad Request";
case HTTP_UNAUTHORIZED:
return "Unauthorized";
case HTTP_FORBIDDEN:
return "Forbidden";
case HTTP_NOT_FOUND:
return "Not Found";
case HTTP_METHOD_NOT_ALLOWED:
return "Method Not Allowed";
case HTTP_NOT_ACCEPTABLE:
return "Not Acceptable";
case HTTP_PROXY_AUTH_REQUIRED:
return "Proxy Authentication Required";
case HTTP_REQUEST_TIMEOUT:
return "Request Timeout";
case HTTP_CONFLICT:
return "Conflict";
case HTTP_GONE:
return "Gone";
case HTTP_LENGTH_REQUIRED:
return "Length Required";
case HTTP_PRECONDITION_FAILED:
return "Precondition Failed";
case HTTP_PAYLOAD_TOO_LARGE:
return "Payload Too Large";
case HTTP_URI_TOO_LONG:
return "URI Too Long";
case HTTP_UNSUPPORTED_MEDIA_TYPE:
return "Unsupported Media Type";
case HTTP_RANGE_NOT_SATISFIABLE:
return "Range Not Satisfiable";
case HTTP_EXPECTATION_FAILED:
return "Expectation Failed";
case HTTP_TEAPOT:
return "I'm a Teapot";
case HTTP_UPGRADE_REQUIRED:
return "Upgrade Required";
case HTTP_PRECONDITION_REQUIRED:
return "Precondition Required";
case HTTP_TOO_MANY_REQUESTS:
return "Too Many Requests";
case HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE:
return "Request Header Fields Too Large";
case HTTP_UNAVAILABLE_FOR_LEGAL_REASONS:
return "Unavailable For Legal Reasons";
case HTTP_INTERNAL_SERVER_ERROR:
return "Internal Server Error";
case HTTP_NOT_IMPLEMENTED:
return "Not Implemented";
case HTTP_BAD_GATEWAY:
return "Bad Gateway";
case HTTP_SERVICE_UNAVAILABLE:
return "Service Unavailable";
case HTTP_GATEWAY_TIMEOUT:
return "Gateway Timeout";
case HTTP_VERSION_NOT_SUPPORTED:
return "Version Not Supported";
case HTTP_VARIANT_ALSO_NEGOTIATES:
return "Variant Also Negotiates";
case HTTP_NOT_EXTENDED:
return "Not Extended";
case HTTP_NETWORK_AUTH_REQUIRED:
return "Network Authentication Required";
default:
return NULL;
}
}
char *
HttpUrlEncode(char *str)
{
size_t size;
size_t len;
char *encoded;
if (!str)
{
return NULL;
}
size = TELODENDRIA_STRING_CHUNK;
len = 0;
encoded = Malloc(size);
if (!encoded)
{
return NULL;
}
while (*str)
{
char c = *str;
if (len >= size - 4)
{
char *tmp;
size += TELODENDRIA_STRING_CHUNK;
tmp = Realloc(encoded, size);
if (!tmp)
{
Free(encoded);
return NULL;
}
encoded = tmp;
}
/* Control characters and extended characters */
if (c <= 0x1F || c >= 0x7F)
{
goto percentEncode;
}
/* Reserved and unsafe characters */
switch (c)
{
case '$':
case '&':
case '+':
case ',':
case '/':
case ':':
case ';':
case '=':
case '?':
case '@':
case ' ':
case '"':
case '<':
case '>':
case '#':
case '%':
case '{':
case '}':
case '|':
case '\\':
case '^':
case '~':
case '[':
case ']':
case '`':
goto percentEncode;
break;
default:
encoded[len] = c;
len++;
str++;
continue;
}
percentEncode:
encoded[len] = '%';
len++;
snprintf(encoded + len, 3, "%2X", c);
len += 2;
str++;
}
encoded[len] = '\0';
return encoded;
}
char *
HttpUrlDecode(char *str)
{
size_t i;
size_t inputLen;
char *decoded;
if (!str)
{
return NULL;
}
i = 0;
inputLen = strlen(str);
decoded = Malloc(inputLen + 1);
if (!decoded)
{
return NULL;
}
while (*str)
{
char c = *str;
if (c == '%')
{
unsigned int d;
str++;
if (sscanf(str, "%2X", &d) != 1)
{
/* Decoding error */
Free(decoded);
return NULL;
}
if (!d)
{
/* Null character given, don't put that in the string. */
continue;
}
c = (char) d;
str++;
}
decoded[i] = c;
i++;
str++;
}
decoded[i] = '\0';
return decoded;
}
HashMap *
HttpParamDecode(char *in)
{
HashMap *params;
if (!in)
{
return NULL;
}
params = HashMapCreate();
if (!params)
{
return NULL;
}
while (*in)
{
char *buf;
size_t allocated;
size_t len;
char *decKey;
char *decVal;
/* Read in key */
allocated = TELODENDRIA_STRING_CHUNK;
buf = Malloc(allocated);
len = 0;
while (*in && *in != '=')
{
if (len >= allocated - 1)
{
allocated += TELODENDRIA_STRING_CHUNK;
buf = Realloc(buf, allocated);
}
buf[len] = *in;
len++;
in++;
}
buf[len] = '\0';
/* Sanity check */
if (*in != '=')
{
/* Malformed param */
Free(buf);
HashMapFree(params);
return NULL;
}
in++;
/* Decode key */
decKey = HttpUrlDecode(buf);
Free(buf);
if (!decKey)
{
/* Decoding error */
Free(params);
return NULL;
}
/* Read in value */
allocated = TELODENDRIA_STRING_CHUNK;
buf = Malloc(allocated);
len = 0;
while (*in && *in != '&')
{
if (len >= allocated - 1)
{
allocated += TELODENDRIA_STRING_CHUNK;
buf = Realloc(buf, allocated);
}
buf[len] = *in;
len++;
in++;
}
buf[len] = '\0';
/* Decode value */
decVal = HttpUrlDecode(buf);
Free(buf);
if (!decVal)
{
/* Decoding error */
Free(params);
return NULL;
}
HashMapSet(params, decKey, decVal);
if (*in == '&')
{
in++;
continue;
}
else
{
break;
}
}
return params;
}
char *
HttpParamEncode(HashMap * params)
{
char *key;
char *val;
char *out = NULL;
if (!params || !out)
{
return NULL;
}
while (HashMapIterate(params, &key, (void *) &val))
{
char *encKey;
char *encVal;
encKey = HttpUrlEncode(key);
encVal = HttpUrlEncode(val);
if (!encKey || !encVal)
{
/* Memory error */
Free(encKey);
Free(encVal);
return NULL;
}
/* TODO */
Free(encKey);
Free(encVal);
}
return out;
}

728
src/HttpServer.c Normal file
View file

@ -0,0 +1,728 @@
/*
* Copyright (C) 2022 Jordan Bancino <@jordan:bancino.net>
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation files
* (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <HttpServer.h>
#include <Memory.h>
#include <Queue.h>
#include <Array.h>
#include <Util.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <poll.h>
#include <string.h>
#include <ctype.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
struct HttpServer
{
int sd;
unsigned int nThreads;
unsigned int maxConnections;
pthread_t socketThread;
volatile unsigned int stop:1;
volatile unsigned int isRunning:1;
HttpHandler *requestHandler;
void *handlerArgs;
Queue *connQueue;
pthread_mutex_t connQueueMutex;
Array *threadPool;
};
struct HttpServerContext
{
HashMap *requestHeaders;
HttpRequestMethod requestMethod;
char *requestPath;
HashMap *requestParams;
HashMap *responseHeaders;
HttpStatus responseStatus;
FILE *stream;
};
static HttpServerContext *
HttpServerContextCreate(HttpRequestMethod requestMethod,
char *requestPath, HashMap * requestParams, FILE * stream)
{
HttpServerContext *c;
c = Malloc(sizeof(HttpServerContext));
if (!c)
{
return NULL;
}
c->requestHeaders = HashMapCreate();
if (!c->requestHeaders)
{
Free(c);
return NULL;
}
c->responseHeaders = HashMapCreate();
if (!c->responseHeaders)
{
Free(c->requestHeaders);
Free(c);
return NULL;
}
c->requestMethod = requestMethod;
c->requestPath = requestPath;
c->requestParams = requestParams;
c->stream = stream;
c->responseStatus = HTTP_OK;
return c;
}
static void
HttpServerContextFree(HttpServerContext * c)
{
char *key;
void *val;
if (!c)
{
return;
}
while (HashMapIterate(c->requestHeaders, &key, &val))
{
/*
* These are always parsed from the request, so they should
* always be on the heap.
*/
Free(key);
Free(val);
}
HashMapFree(c->requestHeaders);
while (HashMapIterate(c->responseHeaders, &key, &val))
{
/*
* These are generated by code. As such, they may be either
* on the heap, or on the stack, depending on how they were
* added.
*
* Basically, if the memory API knows about a pointer, then
* it can be freed. If it doesn't know about a pointer, skip
* freeing it because it's probably a stack pointer.
*/
if (MemoryInfoGet(key))
{
Free(key);
}
if (MemoryInfoGet(val))
{
Free(val);
}
}
HashMapFree(c->responseHeaders);
while (HashMapIterate(c->requestParams, &key, &val))
{
Free(key);
Free(val);
}
HashMapFree(c->requestParams);
Free(c->requestPath);
#if 0
fclose(c->stream);
#endif
Free(c);
}
HashMap *
HttpRequestHeaders(HttpServerContext * c)
{
if (!c)
{
return NULL;
}
return c->requestHeaders;
}
HttpRequestMethod
HttpRequestMethodGet(HttpServerContext * c)
{
if (!c)
{
return HTTP_METHOD_UNKNOWN;
}
return c->requestMethod;
}
char *
HttpRequestPath(HttpServerContext * c)
{
if (!c)
{
return NULL;
}
return c->requestPath;
}
HashMap *
HttpRequestParams(HttpServerContext * c)
{
if (!c)
{
return NULL;
}
return c->requestParams;
}
char *
HttpResponseHeader(HttpServerContext * c, char *key, char *val)
{
if (!c)
{
return NULL;
}
return HashMapSet(c->responseHeaders, key, val);
}
void
HttpResponseStatus(HttpServerContext * c, HttpStatus status)
{
if (!c)
{
return;
}
c->responseStatus = status;
}
FILE *
HttpStream(HttpServerContext * c)
{
if (!c)
{
return NULL;
}
return c->stream;
}
void
HttpSendHeaders(HttpServerContext * c)
{
FILE *fp = c->stream;
char *key;
char *val;
fprintf(fp, "HTTP/1.0 %d %s\n", c->responseStatus, HttpStatusToString(c->responseStatus));
while (HashMapIterate(c->responseHeaders, &key, (void **) &val))
{
fprintf(fp, "%s: %s\n", key, val);
}
fprintf(fp, "\n");
}
static int
QueueConnection(HttpServer * server, int fd)
{
FILE *fp;
int result;
if (!server)
{
return 0;
}
fp = fdopen(fd, "r+");
if (!fp)
{
return 0;
}
pthread_mutex_lock(&server->connQueueMutex);
result = QueuePush(server->connQueue, fp);
pthread_mutex_unlock(&server->connQueueMutex);
return result;
}
static FILE *
DequeueConnection(HttpServer * server)
{
FILE *fp;
if (!server)
{
return NULL;
}
pthread_mutex_lock(&server->connQueueMutex);
fp = QueuePop(server->connQueue);
pthread_mutex_unlock(&server->connQueueMutex);
return fp;
}
HttpServer *
HttpServerCreate(unsigned short port, unsigned int nThreads, unsigned int maxConnections,
HttpHandler * requestHandler, void *handlerArgs)
{
HttpServer *server;
struct sockaddr_in sa = {0};
if (!requestHandler)
{
return NULL;
}
server = Malloc(sizeof(HttpServer));
if (!server)
{
goto error;
}
memset(server, 0, sizeof(HttpServer));
server->threadPool = ArrayCreate();
if (!server->threadPool)
{
goto error;
}
server->connQueue = QueueCreate(maxConnections);
if (!server->connQueue)
{
goto error;
}
if (pthread_mutex_init(&server->connQueueMutex, NULL) != 0)
{
goto error;
}
server->sd = socket(AF_INET, SOCK_STREAM, 0);
if (server->sd < 0)
{
goto error;
}
if (fcntl(server->sd, F_SETFL, O_NONBLOCK) == -1)
{
goto error;
}
sa.sin_family = AF_INET;
sa.sin_port = htons(port);
sa.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(server->sd, (struct sockaddr *) & sa, sizeof(sa)) < 0)
{
goto error;
}
if (listen(server->sd, maxConnections) < 0)
{
goto error;
}
server->nThreads = nThreads;
server->maxConnections = maxConnections;
server->requestHandler = requestHandler;
server->handlerArgs = handlerArgs;
server->stop = 0;
server->isRunning = 0;
return server;
error:
if (server)
{
if (server->connQueue)
{
QueueFree(server->connQueue);
}
pthread_mutex_destroy(&server->connQueueMutex);
if (server->threadPool)
{
ArrayFree(server->threadPool);
}
if (server->sd)
{
close(server->sd);
}
Free(server);
}
return NULL;
}
void
HttpServerFree(HttpServer * server)
{
if (!server)
{
return;
}
close(server->sd);
QueueFree(server->connQueue);
pthread_mutex_destroy(&server->connQueueMutex);
ArrayFree(server->threadPool);
Free(server);
}
static void *
HttpServerWorkerThread(void *args)
{
HttpServer *server = (HttpServer *) args;
while (!server->stop)
{
FILE *fp = DequeueConnection(server);
HttpServerContext *context;
char *line = NULL;
size_t lineSize = 0;
ssize_t lineLen = 0;
char *requestMethodPtr;
char *pathPtr;
char *requestPath;
char *requestProtocol;
HashMap *requestParams;
ssize_t requestPathLen;
ssize_t i = 0;
HttpRequestMethod requestMethod;
if (!fp)
{
/* Block for 1 millisecond before continuing so we don't
* murder the CPU */
UtilSleepMillis(1);
continue;
}
/* Get the first line of the request */
lineLen = UtilGetLine(&line, &lineSize, fp);
if (lineLen == -1)
{
goto bad_request;
}
requestMethodPtr = line;
for (i = 0; i < lineLen; i++)
{
if (line[i] == ' ')
{
line[i] = '\0';
break;
}
}
if (i == lineLen)
{
goto bad_request;
}
requestMethod = HttpRequestMethodFromString(requestMethodPtr);
if (requestMethod == HTTP_METHOD_UNKNOWN)
{
goto bad_request;
}
pathPtr = line + i + 1;
for (i = 0; i < (line + lineLen) - pathPtr; i++)
{
if (pathPtr[i] == ' ')
{
pathPtr[i] = '\0';
break;
}
}
requestPathLen = i;
requestPath = Malloc(((requestPathLen + 1) * sizeof(char)));
strcpy(requestPath, pathPtr);
requestProtocol = &pathPtr[i + 1];
line[lineLen - 2] = '\0'; /* Get rid of \r and \n */
if (strcmp(requestProtocol, "HTTP/1.1") != 0 && strcmp(requestProtocol, "HTTP/1.0") != 0)
{
goto bad_request;
}
/* Find request params */
for (i = 0; i < requestPathLen; i++)
{
if (requestPath[i] == '?')
{
break;
}
}
requestPath[i] = '\0';
requestParams = HttpParamDecode(requestPath + i);
context = HttpServerContextCreate(requestMethod, requestPath, requestParams, fp);
if (!context)
{
goto internal_error;
}
while ((lineLen = UtilGetLine(&line, &lineSize, fp)) != -1)
{
char *headerKey;
char *headerValue;
char *headerPtr;
ssize_t i;
if (strcmp(line, "\r\n") == 0)
{
break;
}
for (i = 0; i < lineLen; i++)
{
if (line[i] == ':')
{
line[i] = '\0';
break;
}
line[i] = tolower(line[i]);
}
headerKey = Malloc((i * sizeof(char)) + 1);
if (!headerKey)
{
goto internal_error;
}
strcpy(headerKey, line);
headerPtr = line + i + 1;
while (isspace((unsigned char) *headerPtr))
{
headerPtr++;
}
for (i = lineLen - 1; i > (line + lineLen) - headerPtr; i--)
{
if (!isspace((unsigned char) line[i]))
{
break;
}
line[i] = '\0';
}
headerValue = Malloc(strlen(headerPtr) + 1);
if (!headerValue)
{
goto internal_error;
}
strcpy(headerValue, headerPtr);
HashMapSet(context->requestHeaders, headerKey, headerValue);
}
server->requestHandler(context, server->handlerArgs);
HttpServerContextFree(context);
goto finish;
internal_error:
fprintf(fp, "HTTP/1.0 500 Internal Server Error\n");
fprintf(fp, "Connection: close\n");
goto finish;
bad_request:
fprintf(fp, "HTTP/1.0 400 Bad Request\n");
fprintf(fp, "Connection: close\n");
goto finish;
finish:
Free(line);
#if 0
fclose(fp);
#endif
}
return NULL;
}
static void *
HttpServerEventThread(void *args)
{
HttpServer *server = (HttpServer *) args;
struct pollfd pollFds[1];
FILE *fp;
size_t i;
server->isRunning = 1;
server->stop = 0;
pollFds[0].fd = server->sd;
pollFds[0].events = POLLIN;
for (i = 0; i < server->nThreads; i++)
{
pthread_t *workerThread = Malloc(sizeof(pthread_t));
if (!workerThread)
{
/* TODO: Make the event thread return an error to the main
* thread */
return NULL;
}
if (pthread_create(workerThread, NULL, HttpServerWorkerThread, server) != 0)
{
/* TODO: Make the event thread return an error to the main
* thread */
return NULL;
}
ArrayAdd(server->threadPool, workerThread);
}
while (!server->stop)
{
struct sockaddr_storage addr;
socklen_t addrLen = sizeof(addr);
int connFd;
int pollResult;
pollResult = poll(pollFds, 1, 500);
if (pollResult < 0)
{
/* The poll either timed out, or was interrupted. */
continue;
}
connFd = accept(server->sd, (struct sockaddr *) & addr, &addrLen);
if (connFd < 0)
{
continue;
}
QueueConnection(server, connFd);
}
for (i = 0; i < server->nThreads; i++)
{
pthread_t *workerThread = ArrayGet(server->threadPool, i);
pthread_join(*workerThread, NULL);
Free(workerThread);
}
while ((fp = DequeueConnection(server)))
{
fclose(fp);
}
server->isRunning = 0;
return NULL;
}
int
HttpServerStart(HttpServer * server)
{
if (!server)
{
return 0;
}
if (server->isRunning)
{
return 1;
}
if (pthread_create(&server->socketThread, NULL, HttpServerEventThread, server) != 0)
{
return 0;
}
return 1;
}
void
HttpServerJoin(HttpServer * server)
{
if (!server)
{
return;
}
pthread_join(server->socketThread, NULL);
}
void
HttpServerStop(HttpServer * server)
{
if (!server)
{
return;
}
server->stop = 1;
}

1043
src/Json.c Normal file

File diff suppressed because it is too large Load diff

310
src/Log.c Normal file
View file

@ -0,0 +1,310 @@
#include <Log.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
#include <ctype.h>
#include <stdarg.h>
#define LOG_TSBUFFER 64
struct LogConfig {
LogLevel level;
size_t indent;
FILE *out;
int flags;
char *tsFmt;
};
void
Log(LogConfig *config, LogLevel level, const char *msg, ...)
{
int i;
int doColor;
char indicator;
va_list argp;
/*
* Only proceed if we have a config and its log level is set to a
* value that permits us to log. This is as close as we can get
* to a no-op function if we aren't logging anything, without doing
* some crazy macro magic.
*/
if (!config || level > config->level)
{
return;
}
/* Misconfiguration */
if (!config->out)
{
return;
}
for (i = 0; i < config->indent; i++)
{
fputc(' ', config->out);
}
doColor = LogConfigFlagGet(config, LOG_FLAG_COLOR)
&& isatty(fileno(config->out));
if (doColor)
{
char *ansi;
switch (level)
{
case LOG_ERROR:
/* Bold Red */
ansi = "\033[1;31m";
break;
case LOG_WARNING:
/* Bold Yellow */
ansi = "\033[1;33m";
break;
case LOG_TASK:
/* Bold Magenta */
ansi = "\033[1;35m";
break;
case LOG_MESSAGE:
/* Bold Green */
ansi = "\033[1;32m";
break;
case LOG_DEBUG:
/* Bold Blue */
ansi = "\033[1;34m";
break;
default:
ansi = "";
break;
}
fputs(ansi, config->out);
}
fputc('[', config->out);
if (config->tsFmt)
{
time_t timer = time(NULL);
struct tm *timeInfo = localtime(&timer);
char tsBuffer[LOG_TSBUFFER];
int tsLength = strftime(tsBuffer, LOG_TSBUFFER, config->tsFmt,
timeInfo);
if (tsLength)
{
fputs(tsBuffer, config->out);
if (!isspace(tsBuffer[tsLength - 1]))
{
fputc(' ', config->out);
}
}
}
switch (level)
{
case LOG_ERROR:
indicator = 'x';
break;
case LOG_WARNING:
indicator = '!';
break;
case LOG_TASK:
indicator = '~';
break;
case LOG_MESSAGE:
indicator = '>';
break;
case LOG_DEBUG:
indicator = '*';
break;
default:
indicator = ' ';
break;
}
fprintf(config->out, "%c]", indicator);
if (doColor)
{
/* ANSI Reset */
fputs("\033[0m", config->out);
}
fputc(' ', config->out);
va_start(argp, msg);
vfprintf(config->out, msg, argp);
fputc('\n', config->out);
va_end(argp);
/* If we are debugging, there might be something that's
* going to segfault the program coming up, so flush the
* output stream immediately.
*/
if (config->level == LOG_DEBUG)
{
fflush(config->out);
}
}
LogConfig *
LogConfigCreate(void)
{
LogConfig *config;
config = calloc(1, sizeof(LogConfig));
if (!config)
{
return NULL;
}
LogConfigLevelSet(config, LOG_MESSAGE);
LogConfigIndentSet(config, 0);
LogConfigOutputSet(config, NULL); /* Will set to stdout */
LogConfigFlagSet(config, LOG_FLAG_COLOR);
LogConfigTimeStampFormatSet(config, "%y-%m-%d %H:%M:%S");
return config;
}
void
LogConfigFlagClear(LogConfig *config, int flags)
{
if (!config)
{
return;
}
config->flags &= ~flags;
}
int
LogConfigFlagGet(LogConfig *config, int flags)
{
if (!config)
{
return 0;
}
return config->flags & flags;
}
void
LogConfigFlagSet(LogConfig *config, int flags)
{
if (!config)
{
return;
}
config->flags |= flags;
}
void
LogConfigFree(LogConfig *config)
{
free(config);
}
void
LogConfigIndent(LogConfig *config)
{
if (config)
{
config->indent += 2;
}
}
size_t
LogConfigIndentGet(LogConfig *config)
{
if (!config)
{
return 0;
}
return config->indent;
}
void
LogConfigIndentSet(LogConfig *config, size_t indent)
{
if (!config)
{
return;
}
config->indent = indent;
}
LogLevel
LogConfigLevelGet(LogConfig *config)
{
if (!config)
{
return -1;
}
return config->level;
}
void
LogConfigLevelSet(LogConfig *config, LogLevel level)
{
if (!config)
{
return;
}
switch (level)
{
case LOG_ERROR:
case LOG_WARNING:
case LOG_MESSAGE:
case LOG_DEBUG:
config->level = level;
default:
break;
}
}
void
LogConfigOutputSet(LogConfig *config, FILE *out)
{
if (!config)
{
return;
}
if (out)
{
config->out = out;
}
else
{
config->out = stdout;
}
}
void
LogConfigTimeStampFormatSet(LogConfig *config, char *tsFmt)
{
if (config)
{
config->tsFmt = tsFmt;
}
}
void
LogConfigUnindent(LogConfig *config)
{
if (config && config->indent >= 2)
{
config->indent -= 2;
}
}

View file

@ -1,648 +0,0 @@
/*
* Copyright (C) 2022-2024 Jordan Bancino <@jordan:bancino.net> with
* other valuable contributors. See CONTRIBUTORS.txt for the full list.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation files
* (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <limits.h>
#include <grp.h>
#include <pwd.h>
#include <Cytoplasm/Args.h>
#include <Cytoplasm/Memory.h>
#include <Cytoplasm/Log.h>
#include <Cytoplasm/HashMap.h>
#include <Cytoplasm/Json.h>
#include <Cytoplasm/HttpServer.h>
#include <Cytoplasm/Db.h>
#include <Cytoplasm/Cron.h>
#include <Cytoplasm/Util.h>
#include <Cytoplasm/Str.h>
#include <Telodendria.h>
#include <Matrix.h>
#include <User.h>
#include <RegToken.h>
#include <Routes.h>
#include <Uia.h>
#include <Config.h>
static Array *httpServers;
static volatile int restart;
static void
SignalHandler(int signal)
{
size_t i;
switch (signal)
{
case SIGPIPE:
return;
case SIGUSR1:
restart = 1;
/* Fall through */
case SIGTERM:
case SIGINT:
if (!httpServers)
{
return;
}
for (i = 0; i < ArraySize(httpServers); i++)
{
HttpServer *server = ArrayGet(httpServers, i);
HttpServerStop(server);
}
break;
}
}
typedef enum ArgFlag
{
ARG_VERSION = (1 << 0),
ARG_VERBOSE = (1 << 2)
} ArgFlag;
int
Main(Array * args)
{
int exit;
/* Arg parsing */
ArgParseState arg;
int opt;
int flags;
char *dbPath;
/* Program configuration */
Config tConfig;
Stream *logFile;
Stream *pidFile;
char *pidPath;
/* User validation */
struct passwd *userInfo;
struct group *groupInfo;
/* HTTP server management */
size_t i;
HttpServer *server;
/* Signal handling */
struct sigaction sigAction;
MatrixHttpHandlerArgs matrixArgs;
Cron *cron;
char startDir[PATH_MAX];
char *token;
start:
/* Global variables */
httpServers = NULL;
restart = 0;
/* Local variables */
exit = EXIT_SUCCESS;
flags = 0;
dbPath = NULL;
logFile = NULL;
pidFile = NULL;
pidPath = NULL;
userInfo = NULL;
groupInfo = NULL;
cron = NULL;
token = NULL;
memset(&matrixArgs, 0, sizeof(matrixArgs));
if (!LogConfigGlobal())
{
printf("Fatal error: unable to allocate memory for logger.\n");
return EXIT_FAILURE;
}
TelodendriaPrintHeader();
ArgParseStateInit(&arg);
while ((opt = ArgParse(&arg, args, "d:Vv")) != -1)
{
switch (opt)
{
case 'd':
dbPath = arg.optArg;
break;
case 'V':
flags |= ARG_VERSION;
break;
case 'v':
flags |= ARG_VERBOSE;
break;
case '?':
exit = EXIT_FAILURE;
goto finish;
default:
break;
}
}
if (flags & ARG_VERBOSE)
{
LogConfigLevelSet(LogConfigGlobal(), LOG_DEBUG);
MemoryHook(TelodendriaMemoryHook, (void *) ARG_VERBOSE);
}
else
{
MemoryHook(TelodendriaMemoryHook, NULL);
}
if (flags & ARG_VERSION)
{
goto finish;
}
if (!dbPath)
{
Log(LOG_ERR, "No database directory specified.");
exit = EXIT_FAILURE;
goto finish;
}
if (!getcwd(startDir, PATH_MAX))
{
Log(LOG_ERR, "Unable to determine current working directory.");
exit = EXIT_FAILURE;
goto finish;
}
if (chdir(dbPath) != 0)
{
Log(LOG_ERR, "Unable to change into data directory: %s.", strerror(errno));
exit = EXIT_FAILURE;
goto finish;
}
else
{
Log(LOG_DEBUG, "Changed working directory to: %s", dbPath);
}
matrixArgs.db = DbOpen(".", 0);
if (!matrixArgs.db)
{
Log(LOG_ERR, "Unable to open data directory as a database.");
exit = EXIT_FAILURE;
goto finish;
}
else
{
Log(LOG_DEBUG, "Opened database.");
}
if (!ConfigExists(matrixArgs.db))
{
RegTokenInfo *info;
Log(LOG_NOTICE, "No configuration exists in the opened database.");
Log(LOG_NOTICE, "A default configuration will be created, and a");
Log(LOG_NOTICE, "new single-use registration token that grants all");
Log(LOG_NOTICE, "privileges will be created so an admin user can");
Log(LOG_NOTICE, "be created to configure this database using the");
Log(LOG_NOTICE, "administrator API.");
if (!ConfigCreateDefault(matrixArgs.db))
{
Log(LOG_ERR, "Unable to create default configuration.");
exit = EXIT_FAILURE;
goto finish;
}
token = StrRandom(32);
info = RegTokenCreate(matrixArgs.db, token, NULL, /* expires */ 0, /* uses */ 1, USER_ALL);
if (!info)
{
Free(token);
Log(LOG_ERR, "Unable to create admin registration token.");
exit = EXIT_FAILURE;
goto finish;
}
RegTokenClose(info);
RegTokenFree(info);
/* Don't free token, because we need to print it when logging
* is set up. */
}
Log(LOG_NOTICE, "Loading configuration...");
ConfigLock(matrixArgs.db, &tConfig);
if (!tConfig.ok)
{
Log(LOG_ERR, tConfig.err);
exit = EXIT_FAILURE;
goto finish;
}
if (!tConfig.log.timestampFormat || !StrEquals(tConfig.log.timestampFormat, "default"))
{
LogConfigTimeStampFormatSet(LogConfigGlobal(), tConfig.log.timestampFormat);
}
if (tConfig.log.color)
{
LogConfigFlagSet(LogConfigGlobal(), LOG_FLAG_COLOR);
}
else
{
LogConfigFlagClear(LogConfigGlobal(), LOG_FLAG_COLOR);
}
LogConfigLevelSet(
LogConfigGlobal(),
flags & ARG_VERBOSE ?
LOG_DEBUG :
ConfigLogLevelToSyslog(tConfig.log.level));
if (tConfig.log.output == CONFIG_LOG_OUTPUT_FILE)
{
logFile = StreamOpen("telodendria.log", "a");
if (!logFile)
{
Log(LOG_ERR, "Unable to open log file for appending.");
exit = EXIT_FAILURE;
tConfig.log.output = CONFIG_LOG_OUTPUT_STDOUT;
goto finish;
}
Log(LOG_INFO, "Logging to the log file. Check there for all future messages.");
LogConfigOutputSet(LogConfigGlobal(), logFile);
}
else if (tConfig.log.output == CONFIG_LOG_OUTPUT_STDOUT)
{
Log(LOG_DEBUG, "Already logging to standard output.");
}
else if (tConfig.log.output == CONFIG_LOG_OUTPUT_SYSLOG)
{
Log(LOG_INFO, "Logging to the syslog. Check there for all future messages.");
LogConfigFlagSet(LogConfigGlobal(), LOG_FLAG_SYSLOG);
openlog("telodendria", LOG_PID | LOG_NDELAY, LOG_DAEMON);
/* Always log everything, because the Log API will control what
* messages get passed to the syslog */
setlogmask(LOG_UPTO(LOG_DEBUG));
}
/* If a token was created with a default config, print it to the
* log */
if (token)
{
Log(LOG_NOTICE, "Admin Registration token: %s", token);
Free(token);
}
if (tConfig.pid)
{
pidFile = StreamOpen(tConfig.pid, "w+");
if (!pidFile)
{
char *msg = "Couldn't lock PID file at '%s'";
Log(LOG_ERR, msg, tConfig.pid);
exit = EXIT_FAILURE;
goto finish;
}
pidPath = StrDuplicate(tConfig.pid);
StreamPrintf(pidFile, "%ld", (long) getpid());
StreamClose(pidFile);
}
Log(LOG_DEBUG, "Configuration:");
LogConfigIndent(LogConfigGlobal());
Log(LOG_DEBUG, "Server Name: %s", tConfig.serverName);
Log(LOG_DEBUG, "Base URL: %s", tConfig.baseUrl);
Log(LOG_DEBUG, "Identity Server: %s", tConfig.identityServer);
Log(LOG_DEBUG, "Run As: %s:%s", tConfig.runAs.uid, tConfig.runAs.gid);
Log(LOG_DEBUG, "Max Cache: %ld", tConfig.maxCache);
Log(LOG_DEBUG, "Registration: %s", tConfig.registration ? "true" : "false");
Log(LOG_DEBUG, "Federation: %s", tConfig.federation ? "true" : "false");
LogConfigUnindent(LogConfigGlobal());
httpServers = ArrayCreate();
if (!httpServers)
{
Log(LOG_ERR, "Error setting up HTTP server.");
exit = EXIT_FAILURE;
goto finish;
}
/* Bind servers before possibly dropping permissions. */
for (i = 0; i < ArraySize(tConfig.listen); i++)
{
ConfigListener *serverCfg = ArrayGet(tConfig.listen, i);
HttpServerConfig args;
args.port = serverCfg->port;
args.threads = serverCfg->maxConnections;
args.maxConnections = serverCfg->maxConnections;
args.tlsCert = serverCfg->tls.cert;
args.tlsKey = serverCfg->tls.key;
args.flags = args.tlsCert && args.tlsKey ? HTTP_FLAG_TLS : HTTP_FLAG_NONE;
Log(LOG_DEBUG, "HTTP listener: %lu", i);
LogConfigIndent(LogConfigGlobal());
Log(LOG_DEBUG, "Port: %hu", serverCfg->port);
Log(LOG_DEBUG, "Threads: %u", serverCfg->threads);
Log(LOG_DEBUG, "Max Connections: %u", serverCfg->maxConnections);
Log(LOG_DEBUG, "Flags: %d", args.flags);
Log(LOG_DEBUG, "TLS Cert: %s", serverCfg->tls.cert);
Log(LOG_DEBUG, "TLS Key: %s", serverCfg->tls.key);
LogConfigUnindent(LogConfigGlobal());
args.handler = MatrixHttpHandler;
args.handlerArgs = &matrixArgs;
if (args.flags & HTTP_FLAG_TLS)
{
if (!UtilLastModified(serverCfg->tls.cert))
{
Log(LOG_ERR, "%s: %s", strerror(errno), serverCfg->tls.cert);
exit = EXIT_FAILURE;
goto finish;
}
if (UtilLastModified(serverCfg->tls.key))
{
Log(LOG_ERR, "%s: %s", strerror(errno), serverCfg->tls.key);
exit = EXIT_FAILURE;
goto finish;
}
}
server = HttpServerCreate(&args);
if (!server)
{
Log(LOG_ERR, "Unable to create HTTP server on port %d: %s",
serverCfg->port, strerror(errno));
exit = EXIT_FAILURE;
goto finish;
}
ArrayAdd(httpServers, server);
}
if (!ArraySize(httpServers))
{
Log(LOG_ERR, "No valid HTTP listeners specified in the configuration.");
exit = EXIT_FAILURE;
goto finish;
}
Log(LOG_DEBUG, "Running as uid:gid: %d:%d.", getuid(), getgid());
if (tConfig.runAs.uid && tConfig.runAs.gid)
{
userInfo = getpwnam(tConfig.runAs.uid);
groupInfo = getgrnam(tConfig.runAs.gid);
if (!userInfo || !groupInfo)
{
Log(LOG_ERR, "Unable to locate the user/group specified in the configuration.");
exit = EXIT_FAILURE;
goto finish;
}
else
{
Log(LOG_DEBUG, "Found user/group information using getpwnam() and getgrnam().");
}
}
else
{
Log(LOG_DEBUG, "No user/group info specified in the config.");
}
if (getuid() == 0)
{
if (userInfo && groupInfo)
{
if (setgid(groupInfo->gr_gid) != 0 || setuid(userInfo->pw_uid) != 0)
{
Log(LOG_ERR, "Unable to set process uid/gid.");
exit = EXIT_FAILURE;
goto finish;
}
else
{
Log(LOG_DEBUG, "Set uid/gid to %s:%s.", tConfig.runAs.uid, tConfig.runAs.gid);
}
}
else
{
Log(LOG_WARNING, "We are running as root, and we are not dropping to another user");
Log(LOG_WARNING, "because none was specified in the configuration file.");
Log(LOG_WARNING, "This is probably a security issue.");
}
}
else
{
if (tConfig.runAs.uid && tConfig.runAs.gid)
{
if (getuid() != userInfo->pw_uid || getgid() != groupInfo->gr_gid)
{
Log(LOG_WARNING, "Not running as the uid/gid specified in the configuration.");
}
else
{
Log(LOG_DEBUG, "Running as the uid/gid specified in the configuration.");
}
}
}
if (!tConfig.maxCache)
{
Log(LOG_WARNING, "Database caching is disabled.");
Log(LOG_WARNING, "If this is not what you intended, check the config file");
Log(LOG_WARNING, "and ensure that maxCache is a valid number of bytes.");
}
DbMaxCacheSet(matrixArgs.db, tConfig.maxCache);
ConfigUnlock(&tConfig);
cron = CronCreate(60 * 1000); /* 1-minute tick */
if (!cron)
{
Log(LOG_ERR, "Unable to set up job scheduler.");
exit = EXIT_FAILURE;
goto finish;
}
Log(LOG_DEBUG, "Registering jobs...");
CronEvery(cron, 30 * 60 * 1000, (JobFunc *) UiaCleanup, &matrixArgs);
Log(LOG_NOTICE, "Starting job scheduler...");
CronStart(cron);
Log(LOG_NOTICE, "Building routing tree...");
matrixArgs.router = RouterBuild();
if (!matrixArgs.router)
{
Log(LOG_ERR, "Unable to build routing tree.");
exit = EXIT_FAILURE;
goto finish;
}
Log(LOG_NOTICE, "Starting server...");
for (i = 0; i < ArraySize(httpServers); i++)
{
HttpServerConfig *serverCfg;
server = ArrayGet(httpServers, i);
serverCfg = HttpServerConfigGet(server);
if (!HttpServerStart(server))
{
Log(LOG_ERR, "Unable to start HTTP server %lu on port %hu.", i, serverCfg->port);
exit = EXIT_FAILURE;
goto finish;
}
else
{
Log(LOG_DEBUG, "Started HTTP server %lu.", i);
Log(LOG_INFO, "Listening on port: %hu", serverCfg->port);
}
}
sigAction.sa_handler = SignalHandler;
sigfillset(&sigAction.sa_mask);
sigAction.sa_flags = SA_RESTART;
#define SIGACTION(sig, act, oact) \
if (sigaction(sig, act, oact) < 0) \
{ \
Log(LOG_ERR, "Unable to install signal handler: %s", #sig); \
exit = EXIT_FAILURE; \
goto finish; \
} \
else \
{ \
Log(LOG_DEBUG, "Installed signal handler: %s", #sig); \
}
SIGACTION(SIGINT, &sigAction, NULL);
SIGACTION(SIGTERM, &sigAction, NULL);
SIGACTION(SIGPIPE, &sigAction, NULL);
SIGACTION(SIGUSR1, &sigAction, NULL);
#undef SIGACTION
/* Block this thread until the servers are terminated by a signal
* handler */
for (i = 0; i < ArraySize(httpServers); i++)
{
server = ArrayGet(httpServers, i);
HttpServerJoin(server);
Log(LOG_DEBUG, "Joined HTTP server %lu.", i);
}
finish:
Log(LOG_NOTICE, "Shutting down...");
if (httpServers)
{
for (i = 0; i < ArraySize(httpServers); i++)
{
Log(LOG_DEBUG, "Freeing HTTP server %lu...", i);
server = ArrayGet(httpServers, i);
HttpServerStop(server);
HttpServerFree(server);
Log(LOG_DEBUG, "Freed HTTP server %lu.", i);
}
ArrayFree(httpServers);
httpServers = NULL;
Log(LOG_DEBUG, "Freed HTTP servers array.");
}
if (cron)
{
Log(LOG_DEBUG, "Waiting on background jobs...");
CronStop(cron);
CronFree(cron);
Log(LOG_DEBUG, "Stopped and freed job scheduler.");
}
ConfigUnlock(&tConfig);
Log(LOG_DEBUG, "Unlocked configuration.");
DbClose(matrixArgs.db);
Log(LOG_DEBUG, "Closed database.");
HttpRouterFree(matrixArgs.router);
Log(LOG_DEBUG, "Freed routing tree.");
if (pidPath)
{
remove(pidPath);
Free(pidPath);
}
/*
* Uninstall the memory hook because it uses the Log
* API, whose configuration is being freed now, so it
* won't work anymore.
*/
MemoryHook(NULL, NULL);
StreamClose(logFile);
if (restart)
{
/*
* Change back into starting directory so initial chdir()
* call works.
*/
if (chdir(startDir) != 0)
{
/* TODO: Seems problematic, what do we do? */
}
goto start;
}
return exit;
}

View file

@ -1,6 +1,5 @@
/* /*
* Copyright (C) 2022-2024 Jordan Bancino <@jordan:bancino.net> with * Copyright (C) 2022 Jordan Bancino <@jordan:bancino.net>
* other valuable contributors. See CONTRIBUTORS.txt for the full list.
* *
* Permission is hereby granted, free of charge, to any person * Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation files * obtaining a copy of this software and associated documentation files
@ -26,43 +25,58 @@
#include <Matrix.h> #include <Matrix.h>
#include <string.h> #include <string.h>
#include <ctype.h>
#include <Cytoplasm/Memory.h> #include <Memory.h>
#include <Cytoplasm/HttpServer.h> #include <HttpServer.h>
#include <Cytoplasm/Json.h> #include <Json.h>
#include <Cytoplasm/Str.h> #include <Util.h>
#include <Cytoplasm/HttpRouter.h>
#include <Routes.h> #include <Routes.h>
void void
MatrixHttpHandler(HttpServerContext * context, void *argp) MatrixHttpHandler(HttpServerContext * context, void *argp)
{ {
MatrixHttpHandlerArgs *args = (MatrixHttpHandlerArgs *) argp; MatrixHttpHandlerArgs *args = (MatrixHttpHandlerArgs *) argp;
Stream *stream;
HashMap *response = NULL; LogConfig *lc = args->lc;
HashMap *requestHeaders = HttpRequestHeaders(context);
FILE *stream;
char *key;
char *val;
HashMap *response;
char *requestPath; char *requestPath;
RouteArgs routeArgs; Array *pathParts;
char *pathPart;
requestPath = HttpRequestPath(context); requestPath = HttpRequestPath(context);
stream = HttpServerStream(context);
Log(LOG_DEBUG, "%s %s", Log(lc, LOG_MESSAGE, "%s %s",
HttpRequestMethodToString(HttpRequestMethodGet(context)), HttpRequestMethodToString(HttpRequestMethodGet(context)),
requestPath); requestPath);
LogConfigIndent(lc);
Log(lc, LOG_DEBUG, "Request headers:");
LogConfigIndent(lc);
while (HashMapIterate(requestHeaders, &key, (void **) &val))
{
Log(lc, LOG_DEBUG, "%s: %s", key, val);
}
LogConfigUnindent(lc);
HttpResponseStatus(context, HTTP_OK); HttpResponseStatus(context, HTTP_OK);
HttpResponseHeader(context, "Server", "Telodendria/" TELODENDRIA_VERSION); HttpResponseHeader(context, "Server", "Telodendria v" TELODENDRIA_VERSION);
HttpResponseHeader(context, "Content-Type", "application/json");
/* CORS */ /* CORS */
HttpResponseHeader(context, "Access-Control-Allow-Origin", "*"); HttpResponseHeader(context, "Access-Control-Allow-Origin", "*");
HttpResponseHeader(context, "Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); HttpResponseHeader(context, "Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
HttpResponseHeader(context, "Access-Control-Allow-Headers", "X-Requested-With, Content-Type, Authorization"); HttpResponseHeader(context, "Access-Control-Allow-Headers", "X-Requested-With, Content-Type, Authorization");
HttpResponseHeader(context, "Connection", "close");
/* /*
* Web Browser Clients: Servers MUST expect that clients will approach them * Web Browser Clients: Servers MUST expect that clients will approach them
* with OPTIONS requests... the server MUST NOT perform any logic defined * with OPTIONS requests... the server MUST NOT perform any logic defined
@ -73,50 +87,71 @@ MatrixHttpHandler(HttpServerContext * context, void *argp)
HttpResponseStatus(context, HTTP_NO_CONTENT); HttpResponseStatus(context, HTTP_NO_CONTENT);
HttpSendHeaders(context); HttpSendHeaders(context);
return; goto finish;
} }
routeArgs.matrixArgs = args; pathParts = ArrayCreate();
routeArgs.context = context; key = requestPath;
if (!HttpRouterRoute(args->router, requestPath, &routeArgs, (void **) &response)) while ((pathPart = strtok_r(key, "/", &key)))
{ {
HttpResponseHeader(context, "Content-Type", "application/json"); char *decoded = HttpUrlDecode(pathPart);
HttpResponseStatus(context, HTTP_NOT_FOUND);
response = MatrixErrorCreate(M_NOT_FOUND, NULL); ArrayAdd(pathParts, decoded);
} }
pathPart = MATRIX_PATH_POP(pathParts);
if (MATRIX_PATH_EQUALS(pathPart, ".well-known"))
{
response = RouteWellKnown(args, context, pathParts);
}
else if (MATRIX_PATH_EQUALS(pathPart, "_matrix"))
{
response = RouteMatrix(args, context, pathParts);
}
else
{
HttpResponseStatus(context, HTTP_NOT_FOUND);
response = MatrixErrorCreate(M_NOT_FOUND);
}
Free(pathPart);
HttpSendHeaders(context);
stream = HttpStream(context);
if (!response)
{
Log(lc, LOG_WARNING, "A route handler returned NULL.");
HttpResponseStatus(context, HTTP_INTERNAL_SERVER_ERROR);
response = MatrixErrorCreate(M_UNKNOWN);
}
JsonEncode(response, stream);
fprintf(stream, "\n");
/* /*
* If the route handler returned a JSON object, take care * By this point, there should be no path parts remaining, but if
* of sending it here. * there are, free them up now.
*
* Otherwise, if the route handler returned NULL, assume
* that it sent its own headers and and body.
*/ */
if (response) while ((pathPart = MATRIX_PATH_POP(pathParts)) != NULL)
{ {
char *contentLen = StrInt(JsonEncode(response, NULL, JSON_DEFAULT)); Free(pathPart);
HttpResponseHeader(context, "Content-Type", "application/json");
HttpResponseHeader(context, "Content-Length", contentLen);
HttpSendHeaders(context);
Free(contentLen);
JsonEncode(response, stream, JSON_DEFAULT);
JsonFree(response);
StreamPrintf(stream, "\n");
} }
Log(LOG_INFO, "%s %s (%d %s)", ArrayFree(pathParts);
HttpRequestMethodToString(HttpRequestMethodGet(context)), JsonFree(response);
requestPath,
HttpResponseStatusGet(context), finish:
HttpStatusToString(HttpResponseStatusGet(context))); stream = HttpStream(context);
fclose(stream);
LogConfigUnindent(lc);
} }
HashMap * HashMap *
MatrixErrorCreate(MatrixError errorArg, char *msg) MatrixErrorCreate(MatrixError errorArg)
{ {
HashMap *errorObj; HashMap *errorObj;
char *errcode; char *errcode;
@ -236,10 +271,6 @@ MatrixErrorCreate(MatrixError errorArg, char *msg)
errcode = "M_MISSING_PARAM"; errcode = "M_MISSING_PARAM";
error = "A required parameter was missing from the request."; error = "A required parameter was missing from the request.";
break; break;
case M_INVALID_PARAM:
errcode = "M_INVALID_PARAM";
error = "A required parameter was invalid in some way.";
break;
case M_TOO_LARGE: case M_TOO_LARGE:
errcode = "M_TOO_LARGE"; errcode = "M_TOO_LARGE";
error = "The request or entity was too large."; error = "The request or entity was too large.";
@ -262,101 +293,14 @@ MatrixErrorCreate(MatrixError errorArg, char *msg)
return NULL; return NULL;
} }
if (msg)
{
error = msg;
}
errorObj = HashMapCreate(); errorObj = HashMapCreate();
if (!errorObj) if (!errorObj)
{ {
return NULL; return NULL;
} }
HashMapSet(errorObj, "errcode", JsonValueString(errcode)); HashMapSet(errorObj, "errcode", JsonValueString(UtilStringDuplicate(errcode)));
HashMapSet(errorObj, "error", JsonValueString(error)); HashMapSet(errorObj, "error", JsonValueString(UtilStringDuplicate(error)));
return errorObj; return errorObj;
} }
HashMap *
MatrixGetAccessToken(HttpServerContext * context, char **accessToken)
{
HashMap *params;
char *token;
params = HttpRequestHeaders(context);
token = HashMapGet(params, "authorization");
if (token)
{
/* If the header was provided but it's not given correctly,
* that's an error */
if (strncmp(token, "Bearer ", 7) != 0)
{
HttpResponseStatus(context, HTTP_UNAUTHORIZED);
return MatrixErrorCreate(M_MISSING_TOKEN, NULL);
}
/* Seek past "Bearer" */
token += 7;
/* Seek past any spaces between "Bearer" and the token */
while (*token && isspace((unsigned char) *token))
{
token++;
}
}
else
{
/* Header was not provided, we must check for ?access_token */
params = HttpRequestParams(context);
token = HashMapGet(params, "access_token");
if (!token)
{
HttpResponseStatus(context, HTTP_UNAUTHORIZED);
return MatrixErrorCreate(M_MISSING_TOKEN, NULL);
}
}
*accessToken = token;
return NULL;
}
HashMap *
MatrixRateLimit(HttpServerContext * context, Db * db)
{
/* TODO: Implement rate limiting */
(void) context;
(void) db;
return NULL;
}
HashMap *
MatrixClientWellKnown(char *base, char *identity)
{
HashMap *response;
HashMap *homeserver;
if (!base)
{
return NULL;
}
response = HashMapCreate();
homeserver = HashMapCreate();
HashMapSet(homeserver, "base_url", JsonValueString(base));
HashMapSet(response, "m.homeserver", JsonValueObject(homeserver));
if (identity)
{
HashMap *identityServer = HashMapCreate();
HashMapSet(identityServer, "base_url", JsonValueString(identity));
HashMapSet(response, "m.identity_server", JsonValueObject(identityServer));
}
return response;
}

415
src/Memory.c Normal file
View file

@ -0,0 +1,415 @@
/*
* Copyright (C) 2022 Jordan Bancino <@jordan:bancino.net>
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation files
* (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <Memory.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
struct MemoryInfo
{
size_t size;
const char *file;
int line;
void *pointer;
};
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
static void (*hook) (MemoryAction, MemoryInfo *, void *) = NULL;
static void *hookArgs = NULL;
static MemoryInfo **allocations = NULL;
static size_t allocationsSize = 0;
static size_t allocationsLen = 0;
static size_t
MemoryHash(void *p)
{
return (((size_t) p) >> 2 * 7) % allocationsSize;
}
static int
MemoryInsert(MemoryInfo * a)
{
size_t hash;
if (!allocations)
{
allocationsSize = 64;
allocations = calloc(allocationsSize, sizeof(void *));
if (!allocations)
{
return 0;
}
}
if ((allocationsLen + 1) >= (0.75 * allocationsSize))
{
size_t i;
size_t tmpAllocationsSize = allocationsSize;
MemoryInfo **tmpAllocations;
allocationsSize *= 2;
tmpAllocations = calloc(allocationsSize, sizeof(void *));
if (!tmpAllocations)
{
return 0;
}
for (i = 0; i < tmpAllocationsSize; i++)
{
if (allocations[i])
{
hash = MemoryHash(allocations[i]->pointer);
while (tmpAllocations[hash])
{
hash = (hash + 1) % allocationsSize;
}
tmpAllocations[hash] = allocations[i];
}
}
free(allocations);
allocations = tmpAllocations;
}
hash = MemoryHash(a->pointer);
while (allocations[hash])
{
hash = (hash + 1) % allocationsSize;
}
allocations[hash] = a;
allocationsLen++;
return 1;
}
static void
MemoryDelete(MemoryInfo * a)
{
size_t hash = MemoryHash(a->pointer);
size_t count = 0;
while (count <= allocationsSize)
{
if (allocations[hash] && allocations[hash] == a)
{
allocations[hash] = NULL;
allocationsLen--;
return;
}
else
{
hash = (hash + 1) % allocationsSize;
count++;
}
}
}
void *
MemoryAllocate(size_t size, const char *file, int line)
{
void *p;
MemoryInfo *a;
pthread_mutex_lock(&lock);
p = malloc(size);
if (!p)
{
pthread_mutex_unlock(&lock);
return NULL;
}
a = malloc(sizeof(MemoryInfo));
if (!a)
{
free(p);
pthread_mutex_unlock(&lock);
return NULL;
}
a->size = size;
a->file = file;
a->line = line;
a->pointer = p;
if (!MemoryInsert(a))
{
free(a);
free(p);
pthread_mutex_unlock(&lock);
return NULL;
}
if (hook)
{
hook(MEMORY_ALLOCATE, a, hookArgs);
}
pthread_mutex_unlock(&lock);
return p;
}
void *
MemoryReallocate(void *p, size_t size, const char *file, int line)
{
MemoryInfo *a;
void *new = NULL;
if (!p)
{
return MemoryAllocate(size, file, line);
}
a = MemoryInfoGet(p);
if (a)
{
pthread_mutex_lock(&lock);
new = realloc(a->pointer, size);
if (new)
{
MemoryDelete(a);
a->size = size;
a->file = file;
a->line = line;
a->pointer = new;
MemoryInsert(a);
if (hook)
{
hook(MEMORY_REALLOCATE, a, hookArgs);
}
}
pthread_mutex_unlock(&lock);
}
else if (hook)
{
a = malloc(sizeof(MemoryInfo));
if (a)
{
a->size = 0;
a->file = file;
a->line = line;
a->pointer = p;
hook(MEMORY_BAD_POINTER, a, hookArgs);
free(a);
}
}
return new;
}
void
MemoryFree(void *p, const char *file, int line)
{
MemoryInfo *a;
if (!p)
{
return;
}
a = MemoryInfoGet(p);
if (a)
{
pthread_mutex_lock(&lock);
if (hook)
{
a->file = file;
a->line = line;
hook(MEMORY_FREE, a, hookArgs);
}
MemoryDelete(a);
free(a->pointer);
free(a);
pthread_mutex_unlock(&lock);
}
else if (hook)
{
a = malloc(sizeof(MemoryInfo));
if (a)
{
a->file = file;
a->line = line;
a->size = 0;
a->pointer = p;
hook(MEMORY_BAD_POINTER, a, hookArgs);
free(a);
}
}
}
size_t
MemoryAllocated(void)
{
size_t i;
size_t total = 0;
pthread_mutex_lock(&lock);
for (i = 0; i < allocationsSize; i++)
{
if (allocations[i])
{
total += allocations[i]->size;
}
}
pthread_mutex_unlock(&lock);
return total;
}
void
MemoryFreeAll(void)
{
size_t i;
pthread_mutex_lock(&lock);
for (i = 0; i < allocationsSize; i++)
{
if (allocations[i])
{
free(allocations[i]->pointer);
free(allocations[i]);
}
}
free(allocations);
allocations = NULL;
allocationsSize = 0;
allocationsLen = 0;
pthread_mutex_unlock(&lock);
}
MemoryInfo *
MemoryInfoGet(void *p)
{
size_t hash, count;
pthread_mutex_lock(&lock);
hash = MemoryHash(p);
count = 0;
while (count <= allocationsSize)
{
if (!allocations[hash] || allocations[hash]->pointer != p)
{
hash = (hash + 1) % allocationsSize;
count++;
}
else
{
pthread_mutex_unlock(&lock);
return allocations[hash];
}
}
pthread_mutex_unlock(&lock);
return NULL;
}
size_t
MemoryInfoGetSize(MemoryInfo * a)
{
if (!a)
{
return 0;
}
return a->size;
}
const char *
MemoryInfoGetFile(MemoryInfo * a)
{
if (!a)
{
return NULL;
}
return a->file;
}
int
MemoryInfoGetLine(MemoryInfo * a)
{
if (!a)
{
return -1;
}
return a->line;
}
void *
MemoryInfoGetPointer(MemoryInfo * a)
{
if (!a)
{
return NULL;
}
return a->pointer;
}
void
MemoryIterate(void (*iterFunc) (MemoryInfo *, void *), void *args)
{
size_t i;
pthread_mutex_lock(&lock);
for (i = 0; i < allocationsSize; i++)
{
if (allocations[i])
{
iterFunc(allocations[i], args);
}
}
pthread_mutex_unlock(&lock);
}
void
MemoryHook(void (*memHook) (MemoryAction, MemoryInfo *, void *), void *args)
{
pthread_mutex_lock(&lock);
hook = memHook;
hookArgs = args;
pthread_mutex_unlock(&lock);
}

View file

@ -1,516 +0,0 @@
/*
* Copyright (C) 2022-2024 Jordan Bancino <@jordan:bancino.net> with
* other valuable contributors. See CONTRIBUTORS.txt for the full list.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation files
* (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <Parser.h>
#include <Cytoplasm/Memory.h>
#include <Cytoplasm/Str.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
/* Iterate through a char **. */
#define Iterate(s) (*(*s)++)
/* Parse an extended localpart */
static bool
ParseUserLocalpart(char **str, char **out)
{
char c;
char *start;
size_t length;
if (!str || !out)
{
return false;
}
/* An extended localpart contains every ASCII printable character,
* except an ':'. */
start = *str;
while (isascii((c = Iterate(str))) && c != ':' && c)
{
/* Do nothing */
}
length = (size_t) (*str - start) - 1;
if (length < 1)
{
*str = start;
return false;
}
if (c == ':')
{
--(*str);
}
*out = Malloc(length + 1);
memcpy(*out, start, length);
(*out)[length] = '\0';
return true;
}
/* Parses an IPv4 address. */
static int
ParseIPv4(char **str, char **out)
{
/* Be *very* careful with this buffer */
char buffer[4];
char *start;
size_t length;
char c;
int digit = 0;
int digits = 0;
memset(buffer, 0, sizeof(buffer));
start = *str;
/* An IPv4 address is made of 4 blocks between 1-3 digits, like so:
* (1-3)*DIGIT.(1-3)*DIGIT.(1-3)*DIGIT.(1-3)*DIGIT */
while ((isdigit(c = Iterate(str)) || c == '.') && c && digits < 4)
{
if (isdigit(c))
{
digit++;
continue;
}
if (digit < 1 || digit > 3)
{
/* Current digit is too long for the spec! */
*str = start;
return false;
}
memcpy(buffer, *str - digit - 1, digit);
if (atoi(buffer) > 255)
{
/* Current digit is too large for the spec! */
*str = start;
return false;
}
memset(buffer, 0, sizeof(buffer));
digit = 0;
digits++; /* We have parsed a digit. */
}
if (c == '.' || digits != 3)
{
*str = start;
return false;
}
length = (size_t) (*str - start) - 1;
*out = Malloc(length + 1);
memcpy(*out, start, length);
(*str)--;
return true;
}
static bool
IsIPv6Char(char c)
{
return (isxdigit(c) || c == ':' || c == '.');
}
static bool
ParseIPv6(char **str, char **out)
{
char *start;
size_t length;
char c;
int filled = 0;
int digit = 0;
int digits = 0;
start = *str;
length = 0;
if (Iterate(str) != '[')
{
goto fail;
}
while ((c = Iterate(str)) && IsIPv6Char(c) && digits < 8)
{
char *ipv4;
if (isxdigit(c))
{
digit++;
length++;
continue;
}
if (c == ':')
{
if (**str == ':')
{
digit = 0;
if (!filled)
{
filled = 1;
length++;
c = Iterate(str); /* Skip over the character */
continue;
}
/* RFC3513 says the following:
* > 'The "::" can only appear once in an address.' */
*str = start;
return false;
}
if (digit < 1 || digit > 4)
{
goto fail;
}
/* We do not have to check whenever the digit here is valid,
* because it has to be. */
digit = 0;
digits++;
length++;
continue;
}
/* The only remaining character being '.', we are probably dealing
* with an IPv4 literal. */
*str -= digit + 1;
length -= digit + 1;
if (ParseIPv4(str, &ipv4))
{
length += strlen(ipv4);
Free(ipv4);
c = Iterate(str);
filled = 1;
goto end;
}
}
end:
--(*str);
if (Iterate(str) != ']')
{
goto fail;
}
length = (size_t) (*str - start);
if (length < 4 || length > 47)
{
goto fail;
}
*out = Malloc(length + 1);
memset(*out, '\0', length + 1);
memcpy(*out, start, length);
return true;
fail:
*str = start;
return false;
}
static bool
ParseHostname(char **str, char **out)
{
char *start;
size_t length = 0;
char c;
start = *str;
while ((c = Iterate(str)) &&
(isalnum(c) || c == '.' || c == '-') &&
++length < 256)
{
/* Do nothing. */
}
if (length < 1 || length > 255)
{
*str = start;
return false;
}
length = (size_t) (*str - start) - 1;
*out = Malloc(length + 1);
memcpy(*out, start, length);
(*str)--;
return true;
}
static bool
ParseServerName(char **str, ServerPart *out)
{
char c;
char *start;
char *startPort;
size_t chars = 0;
char *host = NULL;
char *port = NULL;
if (!str || !out)
{
return false;
}
start = *str;
if (!host)
{
/* If we can parse an IPv4 address, use that. */
ParseIPv4(str, &host);
}
if (!host)
{
/* If we can parse an IPv6 address, use that. */
ParseIPv6(str, &host);
}
if (!host)
{
/* If we can parse an hostname, use that. */
ParseHostname(str, &host);
}
if (!host)
{
/* Can't parse a valid server name. */
return false;
}
/* Now, there's only 2 options: a ':', or the end(everything else.) */
if (**str != ':')
{
/* We're done. */
out->hostname = host;
out->port = NULL;
return true;
}
/* TODO: Separate this out */
startPort = ++(*str);
while(isdigit(c = Iterate(str)) && c && ++chars < 5)
{
/* Do nothing. */
}
if (chars < 1 || chars > 5)
{
*str = start;
Free(host);
host = NULL;
return false;
}
port = Malloc(chars + 1);
memcpy(port, startPort, chars);
port[chars] = '\0';
if (atol(port) > 65535)
{
Free(port);
Free(host);
*str = start;
return false;
}
out->hostname = host;
out->port = port;
return true;
}
bool
ParseServerPart(char *str, ServerPart *part)
{
/* This is a wrapper behind the internal ParseServerName. */
if (!str || !part)
{
return false;
}
return ParseServerName(&str, part);
}
void
ServerPartFree(ServerPart part)
{
if (part.hostname)
{
Free(part.hostname);
}
if (part.port)
{
Free(part.port);
}
}
bool
ParseCommonID(char *str, CommonID *id)
{
char sigil;
if (!str || !id)
{
return false;
}
/* There must at least be 2 chararacters: the sigil and a string.*/
if (strlen(str) < 2)
{
return false;
}
sigil = *str++;
/* Some sigils have the following restriction:
* > MUST NOT exceed 255 bytes (including the # sigil and the domain).
*/
if ((sigil == '#' || sigil == '@') && strlen(str) > 255)
{
return false;
}
id->sigil = sigil;
id->local = NULL;
id->server.hostname = NULL;
id->server.port = NULL;
switch (sigil)
{
case '$':
/* For event IDs, it depends on the version, so we're just
* accepting it all. */
if (!ParseUserLocalpart(&str, &id->local))
{
return false;
}
if (*str == ':')
{
(*str)++;
if (!ParseServerName(&str, &id->server))
{
Free(id->local);
id->local = NULL;
return false;
}
return true;
}
break;
case '!':
case '#': /* It seems like the localpart should be the same as the
user's: everything, except ':'. */
case '@':
if (!ParseUserLocalpart(&str, &id->local))
{
return false;
}
if (*str++ != ':')
{
Free(id->local);
id->local = NULL;
return false;
}
if (!ParseServerName(&str, &id->server))
{
Free(id->local);
id->local = NULL;
return false;
}
break;
}
return true;
}
void
CommonIDFree(CommonID id)
{
if (id.local)
{
Free(id.local);
}
ServerPartFree(id.server);
}
bool
ValidCommonID(char *str, char sigil)
{
CommonID id;
bool ret;
memset(&id, 0, sizeof(CommonID));
if (!str)
{
return false;
}
ret = ParseCommonID(str, &id) && id.sigil == sigil;
CommonIDFree(id);
return ret;
}
char *
ParserRecomposeServerPart(ServerPart serverPart)
{
if (serverPart.hostname && serverPart.port)
{
return StrConcat(3, serverPart.hostname, ":", serverPart.port);
}
if (serverPart.hostname)
{
return StrDuplicate(serverPart.hostname);
}
return NULL;
}
char *
ParserRecomposeCommonID(CommonID id)
{
char *ret = Malloc(2 * sizeof(char));
ret[0] = id.sigil;
ret[1] = '\0';
if (id.local)
{
char *tmp = StrConcat(2, ret, id.local);
Free(ret);
ret = tmp;
}
if (id.server.hostname)
{
char *server = ParserRecomposeServerPart(id.server);
char *tmp = StrConcat(4, "@", ret, ":", server);
Free(ret);
Free(server);
ret = tmp;
}
return ret;
}
bool
ParserServerNameEquals(ServerPart serverPart, char *str)
{
char *idServer;
bool ret;
if (!str)
{
return false;
}
idServer = ParserRecomposeServerPart(serverPart);
ret = StrEquals(idServer, str);
Free(idServer);
return ret;
}

176
src/Queue.c Normal file
View file

@ -0,0 +1,176 @@
/*
* Copyright (C) 2022 Jordan Bancino <@jordan:bancino.net>
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation files
* (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <Queue.h>
#include <Memory.h>
struct Queue
{
void **items;
size_t size;
size_t front;
size_t rear;
};
Queue *
QueueCreate(size_t size)
{
Queue *q;
if (!size)
{
/* Can't have a queue of length zero */
return NULL;
}
q = Malloc(sizeof(Queue));
if (!q)
{
return NULL;
}
q->items = Malloc(size * sizeof(void *));
if (!q->items)
{
Free(q);
return NULL;
}
q->size = size;
q->front = size + 1;
q->rear = size + 1;
return q;
}
void
QueueFree(Queue * q)
{
if (q)
{
Free(q->items);
}
Free(q);
}
int
QueueFull(Queue * q)
{
if (!q)
{
return 0;
}
return ((q->front == q->rear + 1) || (q->front == 0 && q->rear == q->size - 1));
}
int
QueueEmpty(Queue * q)
{
if (!q)
{
return 0;
}
return q->front == q->size + 1;
}
int
QueuePush(Queue * q, void *element)
{
if (!q || !element)
{
return 0;
}
if (QueueFull(q))
{
return 0;
}
if (q->front == q->size + 1)
{
q->front = 0;
}
if (q->rear == q->size + 1)
{
q->rear = 0;
}
else
{
q->rear = (q->rear + 1) % q->size;
}
q->items[q->rear] = element;
return 1;
}
void *
QueuePop(Queue * q)
{
void *element;
if (!q)
{
return NULL;
}
if (QueueEmpty(q))
{
return NULL;
}
element = q->items[q->front];
if (q->front == q->rear)
{
q->front = q->size + 1;
q->rear = q->size + 1;
}
else
{
q->front = (q->front + 1) % q->size;
}
return element;
}
void *
QueuePeek(Queue * q)
{
if (!q)
{
return NULL;
}
if (QueueEmpty(q))
{
return NULL;
}
return q->items[q->front];
}

View file

@ -1,241 +0,0 @@
/*
* Copyright (C) 2022-2024 Jordan Bancino <@jordan:bancino.net> with
* other valuable contributors. See CONTRIBUTORS.txt for the full list.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation files
* (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <RegToken.h>
#include <string.h>
#include <ctype.h>
#include <Cytoplasm/Memory.h>
#include <Cytoplasm/Json.h>
#include <Cytoplasm/Util.h>
#include <Cytoplasm/Str.h>
#include <Cytoplasm/Log.h>
#include <User.h>
int
RegTokenValid(RegTokenInfo * token)
{
HashMap *tokenJson;
int64_t uses, used;
uint64_t expiration;
if (!token || !RegTokenExists(token->db, token->name))
{
return 0;
}
tokenJson = DbJson(token->ref);
uses = JsonValueAsInteger(HashMapGet(tokenJson, "uses"));
used = JsonValueAsInteger(HashMapGet(tokenJson, "used"));
expiration = JsonValueAsInteger(HashMapGet(tokenJson, "expires_on"));
return (!expiration || (UtilTsMillis() < expiration)) && (uses == -1 || used < uses);
}
void
RegTokenUse(RegTokenInfo * token)
{
HashMap *tokenJson;
if (!token || !RegTokenExists(token->db, token->name))
{
return;
}
if (token->uses >= 0 && token->used >= token->uses)
{
return;
}
token->used++;
/* Write the information to the hashmap */
tokenJson = DbJson(token->ref);
JsonValueFree(HashMapSet(tokenJson, "used", JsonValueInteger(token->used)));
}
int
RegTokenExists(Db * db, char *token)
{
if (!token || !db)
{
return 0;
}
return DbExists(db, 3, "tokens", "registration", token);
}
int
RegTokenDelete(RegTokenInfo * token)
{
if (!token || !RegTokenClose(token))
{
return 0;
}
if (!DbDelete(token->db, 3, "tokens", "registration", token->name))
{
return 0;
}
RegTokenInfoFree(token);
Free(token);
return 1;
}
RegTokenInfo *
RegTokenGetInfo(Db * db, char *token)
{
RegTokenInfo *ret;
DbRef *tokenRef;
HashMap *tokenJson;
char *errp = NULL;
if (!RegTokenExists(db, token))
{
return NULL;
}
tokenRef = DbLock(db, 3, "tokens", "registration", token);
if (!tokenRef)
{
return NULL;
}
tokenJson = DbJson(tokenRef);
ret = Malloc(sizeof(RegTokenInfo));
if (!RegTokenInfoFromJson(tokenJson, ret, &errp))
{
Log(LOG_ERR, "RegTokenGetInfo(): Database decoding error: %s", errp);
RegTokenFree(ret);
return NULL;
}
ret->db = db;
ret->ref = tokenRef;
return ret;
}
void
RegTokenFree(RegTokenInfo *tokeninfo)
{
if (tokeninfo)
{
RegTokenInfoFree(tokeninfo);
Free(tokeninfo);
}
}
int
RegTokenClose(RegTokenInfo * tokeninfo)
{
HashMap *json;
if (!tokeninfo)
{
return 0;
}
/* Write object to database. */
json = RegTokenInfoToJson(tokeninfo);
DbJsonSet(tokeninfo->ref, json); /* Copies json into internal structure. */
JsonFree(json);
return DbUnlock(tokeninfo->db, tokeninfo->ref);
}
static int
RegTokenVerify(char *token)
{
size_t i, size;
char c;
if (!token)
{
return 0;
}
/* The spec says the following: "The token required for this
* authentication [...] is an opaque string with maximum length of
* 64 characters in the range [A-Za-z0-9._~-]." */
if ((size = strlen(token)) > 64)
{
return 0;
}
for (i = 0; i < size; i++)
{
c = token[i];
if (!(isalnum(c) || c == '0' || c == '_' || c == '~' || c == '-'))
{
return 0;
}
}
return 1;
}
RegTokenInfo *
RegTokenCreate(Db * db, char *name, char *owner, uint64_t expires, int64_t uses, int privileges)
{
RegTokenInfo *ret;
uint64_t timestamp = UtilTsMillis();
if (!db || !name)
{
return NULL;
}
/* -1 indicates infinite uses; zero and all positive values are a
* valid number of uses; althought zero would be rather useless.
* Anything less than -1 doesn't make sense. */
if (uses < -1)
{
return NULL;
}
/* Verify the token */
if (!RegTokenVerify(name) || ((expires > 0) && (expires < timestamp)))
{
return NULL;
}
ret = Malloc(sizeof(RegTokenInfo));
/* Set the token's properties */
ret->db = db;
ret->ref = DbCreate(db, 3, "tokens", "registration", name);
if (!ret->ref)
{
/* RegToken already exists or some weird fs error */
Free(ret);
return NULL;
}
ret->name = StrDuplicate(name);
ret->created_by = StrDuplicate(owner);
ret->used = 0;
ret->uses = uses;
ret->created_on = timestamp;
ret->expires_on = expires;
ret->grants = UserEncodePrivileges(privileges);
return ret;
}

View file

@ -1,98 +0,0 @@
/*
* Copyright (C) 2022-2024 Jordan Bancino <@jordan:bancino.net> with
* other valuable contributors. See CONTRIBUTORS.txt for the full list.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation files
* (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <Routes.h>
HttpRouter *
RouterBuild(void)
{
HttpRouter *router = HttpRouterCreate();
if (!router)
{
return NULL;
}
#define R(path, func) \
if (!HttpRouterAdd(router, path, func)) \
{ \
Log(LOG_ERR, "Unable to add route: %s", path); \
HttpRouterFree(router); \
return NULL; \
}
/* Matrix Specifification Routes */
R("/.well-known/matrix/(client|server)", RouteWellKnown);
R("/_matrix/client/versions", RouteVersions);
R("/_matrix/static", RouteStaticDefault);
R("/_matrix/static/telodendria\\.(js|css)", RouteStaticResources);
R("/_matrix/static/client/login", RouteStaticLogin);
R("/_matrix/client/v3/auth/(.*)/fallback/web", RouteUiaFallback);
R("/_matrix/client/v3/capabilities", RouteCapabilities);
R("/_matrix/client/v3/login", RouteLogin);
R("/_matrix/client/v3/logout", RouteLogout);
R("/_matrix/client/v3/logout/(all)", RouteLogout);
R("/_matrix/client/v3/register", RouteRegister);
R("/_matrix/client/v3/register/(available)", RouteRegister);
R("/_matrix/client/v3/refresh", RouteRefresh);
R("/_matrix/client/v3/account/whoami", RouteWhoami);
R("/_matrix/client/v3/account/password", RouteChangePwd);
R("/_matrix/client/v3/account/deactivate", RouteDeactivate);
R("/_matrix/client/v1/register/m.login.registration_token/validity", RouteTokenValid);
R("/_matrix/client/v3/account/password/(email|msisdn)/requestToken", RouteRequestToken);
R("/_matrix/client/v3/register/(email|msisdn)/requestToken", RouteRequestToken);
R("/_matrix/client/v3/profile/(.*)", RouteUserProfile);
R("/_matrix/client/v3/profile/(.*)/(avatar_url|displayname)", RouteUserProfile);
R("/_matrix/client/v3/user_directory/search", RouteUserDirectory);
R("/_matrix/client/v3/user/(.*)/filter", RouteFilter);
R("/_matrix/client/v3/user/(.*)/filter/(.*)", RouteFilter);
R("/_matrix/client/v3/createRoom", RouteCreateRoom);
R("/_matrix/client/v3/directory/room/(.*)", RouteAliasDirectory);
R("/_matrix/client/v3/rooms/(.*)/aliases", RouteRoomAliases);
/* Telodendria Admin API Routes */
R("/_telodendria/admin/v1/(restart|shutdown|stats)", RouteProcControl);
R("/_telodendria/admin/v1/config", RouteConfig);
R("/_telodendria/admin/v1/privileges", RoutePrivileges);
R("/_telodendria/admin/v1/privileges/(.*)", RoutePrivileges);
R("/_telodendria/admin/v1/deactivate/(.*)", RouteAdminDeactivate);
R("/_telodendria/admin/v1/tokens/(.*)", RouteAdminTokens);
R("/_telodendria/admin/v1/tokens", RouteAdminTokens);
#undef R
return router;
}

View file

@ -1,117 +0,0 @@
/*
* Copyright (C) 2022-2024 Jordan Bancino <@jordan:bancino.net> with
* other valuable contributors. See CONTRIBUTORS.txt for the full list.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation files
* (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <Routes.h>
#include <Cytoplasm/Json.h>
#include <Cytoplasm/HashMap.h>
#include <Cytoplasm/Str.h>
#include <User.h>
ROUTE_IMPL(RouteAdminDeactivate, path, argp)
{
RouteArgs *args = argp;
HashMap *request = NULL;
HashMap *response = NULL;
JsonValue *val;
char *reason = "Deactivated by admin";
char *removedLocalpart = ArrayGet(path, 0);
char *token;
Db *db = args->matrixArgs->db;
User *user = NULL;
User *removed = NULL;
HttpRequestMethod method = HttpRequestMethodGet(args->context);
if ((method != HTTP_DELETE) && (method != HTTP_PUT))
{
char * msg = "Route only supports DELETE and PUT as for now.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
return MatrixErrorCreate(M_UNRECOGNIZED, msg);
}
if (method == HTTP_DELETE)
{
request = JsonDecode(HttpServerStream(args->context));
if (!request)
{
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
return MatrixErrorCreate(M_NOT_JSON, NULL);
}
val = HashMapGet(request, "reason");
if (val && JsonValueType(val) == JSON_STRING)
{
reason = JsonValueAsString(val);
}
}
response = MatrixGetAccessToken(args->context, &token);
if (response)
{
goto finish;
}
user = UserAuthenticate(db, token);
removed = UserLock(db, removedLocalpart);
if (!user || !removed)
{
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_UNKNOWN_TOKEN, NULL);
goto finish;
}
if (!(UserGetPrivileges(user) & USER_DEACTIVATE))
{
char * msg = "User doesn't have the DEACTIVATE privilege.";
HttpResponseStatus(args->context, HTTP_FORBIDDEN);
response = MatrixErrorCreate(M_FORBIDDEN, msg);
goto finish;
}
if (method == HTTP_DELETE)
{
UserDeactivate(removed, UserGetName(user), reason);
response = HashMapCreate();
JsonSet(response, JsonValueString(removedLocalpart), 1, "user");
JsonSet(response, JsonValueString(reason), 1, "reason");
JsonSet(response, JsonValueString(UserGetName(user)), 1, "banned_by");
}
else
{
UserReactivate(removed);
HttpResponseStatus(args->context, HTTP_NO_CONTENT);
}
finish:
UserUnlock(user);
UserUnlock(removed);
JsonFree(request);
return response;
}

View file

@ -1,208 +0,0 @@
/*
* Copyright (C) 2022-2024 Jordan Bancino <@jordan:bancino.net> with
* other valuable contributors. See CONTRIBUTORS.txt for the full list.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation files
* (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <Routes.h>
#include <Cytoplasm/Json.h>
#include <Cytoplasm/HashMap.h>
#include <Cytoplasm/Str.h>
#include <Cytoplasm/Memory.h>
#include <RegToken.h>
#include <User.h>
ROUTE_IMPL(RouteAdminTokens, path, argp)
{
RouteArgs *args = argp;
HashMap *request = NULL;
HashMap *response = NULL;
char *token;
char *msg;
Db *db = args->matrixArgs->db;
User *user = NULL;
HttpRequestMethod method = HttpRequestMethodGet(args->context);
Array *tokensarray;
Array *tokens;
RegTokenInfo *info;
RegTokenInfo *req;
size_t i;
if (method != HTTP_GET && method != HTTP_POST && method != HTTP_DELETE)
{
msg = "Route only supports GET, POST, and DELETE";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
return MatrixErrorCreate(M_UNRECOGNIZED, msg);
}
response = MatrixGetAccessToken(args->context, &token);
if (response)
{
goto finish;
}
user = UserAuthenticate(db, token);
if (!user)
{
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_UNKNOWN_TOKEN, NULL);
goto finish;
}
if (!(UserGetPrivileges(user) & USER_ISSUE_TOKENS))
{
msg = "User doesn't have the ISSUE_TOKENS privilege.";
HttpResponseStatus(args->context, HTTP_FORBIDDEN);
response = MatrixErrorCreate(M_FORBIDDEN, msg);
goto finish;
}
switch (method)
{
case HTTP_GET:
if (ArraySize(path) == 0)
{
tokensarray = ArrayCreate();
/* Get all registration tokens */
tokens = DbList(db, 2, "tokens", "registration");
response = HashMapCreate();
for (i = 0; i < ArraySize(tokens); i++)
{
char *tokenname = ArrayGet(tokens, i);
HashMap *jsoninfo;
info = RegTokenGetInfo(db, tokenname);
jsoninfo = RegTokenInfoToJson(info);
RegTokenClose(info);
RegTokenFree(info);
ArrayAdd(tokensarray, JsonValueObject(jsoninfo));
}
JsonSet(response, JsonValueArray(tokensarray), 1, "tokens");
DbListFree(tokens);
break;
}
info = RegTokenGetInfo(db, ArrayGet(path, 0));
if (!info)
{
msg = "Token doesn't exist.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_INVALID_PARAM, msg);
goto finish;
}
response = RegTokenInfoToJson(info);
RegTokenClose(info);
RegTokenFree(info);
break;
case HTTP_POST:
request = JsonDecode(HttpServerStream(args->context));
if (!request)
{
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_NOT_JSON, NULL);
goto finish;
}
req = Malloc(sizeof(RegTokenInfo));
memset(req, 0, sizeof(RegTokenInfo));
if (!RegTokenInfoFromJson(request, req, &msg))
{
RegTokenInfoFree(req);
Free(req);
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_BAD_JSON, msg);
goto finish;
}
if (!req->name)
{
req->name = StrRandom(16);
}
/* Create the actual token that will be stored. */
info = RegTokenCreate(db, req->name, UserGetName(user),
req->expires_on, req->uses,
UserDecodePrivileges(req->grants));
if (!info)
{
RegTokenClose(info);
RegTokenFree(info);
RegTokenInfoFree(req);
Free(req);
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
msg = "Cannot create token.";
response = MatrixErrorCreate(M_INVALID_PARAM, msg);
goto finish;
}
response = RegTokenInfoToJson(info);
RegTokenClose(info);
RegTokenFree(info);
RegTokenInfoFree(req);
Free(req);
break;
case HTTP_DELETE:
if (ArraySize(path) == 0)
{
msg = "No registration token given to DELETE /tokens/[token].";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_INVALID_PARAM, msg);
goto finish;
}
info = RegTokenGetInfo(db, ArrayGet(path, 0));
RegTokenDelete(info);
response = HashMapCreate();
break;
default:
/* Should not be possible. */
break;
}
finish:
UserUnlock(user);
JsonFree(request);
return response;
}

View file

@ -1,243 +0,0 @@
/*
* Copyright (C) 2022-2024 Jordan Bancino <@jordan:bancino.net> with
* other valuable contributors. See CONTRIBUTORS.txt for the full list.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation files
* (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <Routes.h>
#include <Cytoplasm/Memory.h>
#include <Cytoplasm/Json.h>
#include <Cytoplasm/Str.h>
#include <Config.h>
#include <Parser.h>
#include <User.h>
ROUTE_IMPL(RouteAliasDirectory, path, argp)
{
RouteArgs *args = argp;
char *alias = ArrayGet(path, 0);
HashMap *request = NULL;
HashMap *response;
Db *db = args->matrixArgs->db;
DbRef *ref = NULL;
HashMap *aliases;
HashMap *idObject;
JsonValue *val;
Array *arr;
char *token;
char *msg;
User *user = NULL;
CommonID aliasID;
Config config;
aliasID.sigil = '\0';
aliasID.local = NULL;
aliasID.server.hostname = NULL;
aliasID.server.port = NULL;
ConfigLock(db, &config);
if (!ParseCommonID(alias, &aliasID) || aliasID.sigil != '#')
{
msg = "Invalid room alias.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_INVALID_PARAM, msg);
goto finish;
}
ref = DbLock(db, 1, "aliases");
if (!ref && !(ref = DbCreate(db, 1, "aliases")))
{
msg = "Unable to access alias database.",
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
response = MatrixErrorCreate(M_UNKNOWN, msg);
goto finish;
}
aliases = DbJson(ref);
switch (HttpRequestMethodGet(args->context))
{
case HTTP_GET:
val = JsonGet(aliases, 2, "alias", alias);
if (val)
{
response = HashMapCreate();
HashMapSet(response, "room_id", JsonValueDuplicate(HashMapGet(JsonValueAsObject(val), "id")));
HashMapSet(response, "servers", JsonValueDuplicate(JsonGet(aliases, 3, "alias", alias, "servers")));
}
else
{
msg = "There is no mapped room ID for this room alias.";
HttpResponseStatus(args->context, HTTP_NOT_FOUND);
response = MatrixErrorCreate(M_NOT_FOUND, msg);
}
break;
case HTTP_PUT:
case HTTP_DELETE:
response = MatrixGetAccessToken(args->context, &token);
if (response)
{
goto finish;
}
user = UserAuthenticate(db, token);
if (!user)
{
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_UNKNOWN_TOKEN, NULL);
goto finish;
}
if (HttpRequestMethodGet(args->context) == HTTP_PUT)
{
HashMap *newAlias;
char *id;
char *serverPart;
serverPart = ParserRecomposeServerPart(aliasID.server);
if (!StrEquals(serverPart, config.serverName))
{
msg = "Invalid server name.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_INVALID_PARAM, msg);
Free(serverPart);
goto finish;
}
Free(serverPart);
if (JsonGet(aliases, 2, "alias", alias))
{
HttpResponseStatus(args->context, HTTP_CONFLICT);
response = MatrixErrorCreate(M_UNKNOWN, "Room alias already exists.");
goto finish;
}
request = JsonDecode(HttpServerStream(args->context));
if (!request)
{
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_NOT_JSON, NULL);
goto finish;
}
id = JsonValueAsString(HashMapGet(request, "room_id"));
if (!id)
{
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_BAD_JSON, "Missing or invalid room_id.");
goto finish;
}
if (!ValidCommonID(id, '!'))
{
msg = "Invalid room ID.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_INVALID_PARAM, msg);
goto finish;
}
newAlias = HashMapCreate();
HashMapSet(newAlias, "createdBy", JsonValueString(UserGetName(user)));
HashMapSet(newAlias, "id", JsonValueString(id));
HashMapSet(newAlias, "servers", JsonValueArray(ArrayCreate()));
JsonSet(aliases, JsonValueObject(newAlias), 2, "alias", alias);
if (!(idObject = JsonValueAsObject(JsonGet(aliases, 2, "id", id))))
{
arr = ArrayCreate();
idObject = HashMapCreate();
HashMapSet(idObject, "aliases", JsonValueArray(arr));
JsonSet(aliases, JsonValueObject(idObject), 2, "id", id);
}
val = HashMapGet(idObject, "aliases");
arr = JsonValueAsArray(val);
ArrayAdd(arr, JsonValueString(alias));
}
else
{
HashMap *roomAlias;
char *id;
if (!(val = JsonGet(aliases, 2, "alias", alias)))
{
HttpResponseStatus(args->context, HTTP_NOT_FOUND);
response = MatrixErrorCreate(M_NOT_FOUND, "Room alias not found.");
goto finish;
}
roomAlias = JsonValueAsObject(val);
id = StrDuplicate(JsonValueAsString(HashMapGet(roomAlias, "id")));
if (!(UserGetPrivileges(user) & USER_ALIAS) && !StrEquals(UserGetName(user), JsonValueAsString(JsonGet(roomAlias, 1, "createdBy"))))
{
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
response = MatrixErrorCreate(M_UNAUTHORIZED, NULL);
Free(id);
goto finish;
}
JsonValueFree(HashMapDelete(JsonValueAsObject(HashMapGet(aliases, "alias")), alias));
idObject = JsonValueAsObject(JsonGet(aliases, 2, "id", id));
if (idObject)
{
size_t i;
val = HashMapGet(idObject, "aliases");
arr = JsonValueAsArray(val);
for (i = 0; i < ArraySize(arr); i++)
{
if (StrEquals(JsonValueAsString(ArrayGet(arr, i)), alias))
{
JsonValueFree(ArrayDelete(arr, i));
break;
}
}
}
Free(id);
}
response = HashMapCreate();
break;
default:
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_UNRECOGNIZED, "Unknown request method.");
goto finish;
}
finish:
CommonIDFree(aliasID);
ConfigUnlock(&config);
UserUnlock(user);
DbUnlock(db, ref);
JsonFree(request);
return response;
}

View file

@ -1,61 +0,0 @@
/*
* Copyright (C) 2022-2024 Jordan Bancino <@jordan:bancino.net> with
* other valuable contributors. See CONTRIBUTORS.txt for the full list.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation files
* (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <Routes.h>
#include <string.h>
#include <Cytoplasm/Memory.h>
#include <Cytoplasm/Json.h>
#include <Cytoplasm/HashMap.h>
#include <Cytoplasm/Str.h>
ROUTE_IMPL(RouteCapabilities, path, argp)
{
HashMap *response;
HashMap *capabilities;
HashMap *roomVersions;
(void) path;
(void) argp;
response = HashMapCreate();
capabilities = HashMapCreate();
roomVersions = HashMapCreate();
JsonSet(capabilities, JsonValueBoolean(1), 2, "m.change_password", "enabled");
JsonSet(capabilities, JsonValueBoolean(1), 2, "m.set_displayname", "enabled");
JsonSet(capabilities, JsonValueBoolean(1), 2, "m.set_avatar_url", "enabled");
JsonSet(capabilities, JsonValueBoolean(0), 2, "m.3pid_changes", "enabled");
/* TODO: When more room versions are implemented, add them to
* roomVersions */
HashMapSet(roomVersions, "1", JsonValueString("unstable"));
JsonSet(capabilities, JsonValueString("1"), 2, "m.room_versions", "default");
JsonSet(capabilities, JsonValueObject(roomVersions), 2, "m.room_versions", "available");
HashMapSet(response, "capabilities", JsonValueObject(capabilities));
return response;
}

View file

@ -1,165 +0,0 @@
/*
* Copyright (C) 2022-2024 Jordan Bancino <@jordan:bancino.net> with
* other valuable contributors. See CONTRIBUTORS.txt for the full list.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation files
* (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <Routes.h>
#include <string.h>
#include <Cytoplasm/Json.h>
#include <Cytoplasm/HashMap.h>
#include <Cytoplasm/Str.h>
#include <Uia.h>
#include <Cytoplasm/Memory.h>
#include <User.h>
static Array *
PasswordFlow(void)
{
Array *ret = ArrayCreate();
if (!ret)
{
return NULL;
}
ArrayAdd(ret, UiaStageBuild("m.login.password", NULL));
return ret;
}
ROUTE_IMPL(RouteChangePwd, path, argp)
{
RouteArgs *args = argp;
Db *db = args->matrixArgs->db;
User *user = NULL;
HashMap *request = NULL;
HashMap *response = NULL;
JsonValue *val = NULL;
Array *uiaFlows = NULL;
int uiaResult;
int logoutDevices = 1;
char *token;
char *newPassword;
char *msg;
Config config;
ConfigLock(db, &config);
if (!config.ok)
{
Log(LOG_ERR, "%s", config.err);
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
return MatrixErrorCreate(M_UNKNOWN, config.err);
}
(void) path;
if (HttpRequestMethodGet(args->context) != HTTP_POST)
{
msg = "Route only supports POST.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_UNRECOGNIZED, msg);
goto finish;
}
response = MatrixGetAccessToken(args->context, &token);
if (response)
{
goto finish;
}
request = JsonDecode(HttpServerStream(args->context));
if (!request)
{
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_NOT_JSON, NULL);
goto finish;
}
uiaFlows = ArrayCreate();
ArrayAdd(uiaFlows, PasswordFlow());
uiaResult = UiaComplete(uiaFlows, args->context,
db, request, &response,
config);
UiaFlowsFree(uiaFlows);
if (uiaResult < 0)
{
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
response = MatrixErrorCreate(M_UNKNOWN, NULL);
goto finish;
}
else if (!uiaResult)
{
goto finish;
}
newPassword = JsonValueAsString(HashMapGet(request, "new_password"));
if (!newPassword)
{
msg = "'new_password' is unset or not a string.";
JsonFree(request);
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_BAD_JSON, msg);
goto finish;
}
val = HashMapGet(request, "logout_devices");
if (val)
{
logoutDevices = JsonValueAsBoolean(val);
}
/* Let's authenticate the user */
user = UserAuthenticate(db, token);
if (!user)
{
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_UNKNOWN_TOKEN, NULL);
goto finish;
}
UserSetPassword(user, newPassword);
/* We might want to logout all extra devices */
if (logoutDevices)
{
/* Deletes all tokens except the passed token */
UserDeleteTokens(user, token);
}
response = HashMapCreate();
finish:
ConfigUnlock(&config);
UserUnlock(user);
JsonFree(request);
return response;
}

Some files were not shown because too many files have changed in this diff Show more