Merge branch 'master' into setup-ci-workflows
Some checks failed
Testing pushed commits / compile-all (push) Has been cancelled

This commit is contained in:
lda 2023-12-09 10:10:12 -05:00
commit 38dfb2a505
51 changed files with 938 additions and 1106 deletions

View file

@ -1,7 +1,6 @@
<p align="center"><img src="https://telodendria.io/assets/Telodendria-500x500.png"></p> <h1 style="text-align: center;">Lightweight, Decentralized Chat.</h1>
<h1 align="center">Telodendria</h1>
Telodendria is an extremely powerful, yet lightweight and portable **Telodendria** is an extremely powerful, yet lightweight and portable
chat server designed to be easy to install and configure. Powered by chat server designed to be easy to install and configure. Powered by
the [Matrix](https://matrix.org) protocol, Telodendria empowers the [Matrix](https://matrix.org) protocol, Telodendria empowers
everyone to run their own chat server on ordinary hardware, including everyone to run their own chat server on ordinary hardware, including
@ -12,7 +11,7 @@ hosting a complicated, high-maintenance homeserver or joining an
existing homeserver for privacy or other reasons, then Telodendria existing homeserver for privacy or other reasons, then Telodendria
might be for you. might be for you.
> **Note:** Telodendria still in development. See [Status](#status). !!!! **Note:** Telodendria still in development. See **Status** below.
## What is Matrix? ## What is Matrix?
@ -64,12 +63,13 @@ incredibly outdated. Telodendria, on the other hand, aims to be stable.
It should *just work* for long periods of time between upgrades, and It should *just work* for long periods of time between upgrades, and
you should never feel like Telodendria is going to change significantly you should never feel like Telodendria is going to change significantly
between upgrades. 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;](docs/dev/rationale.md) [Read Technical Rationale &rightarrow;](https://git.telodendria.io/Telodendria/Telodendria/src/branch/master/docs/dev/rationale.md)
## Get Started ## Get Started
Check out the [Documentation](docs/README.md) to get started with Check out the [Documentation](https://git.telodendria.io/Telodendria/telodendria/src/branch/master/docs/README.md) to get started with
Telodendria. Telodendria.
## Status ## Status
@ -79,13 +79,13 @@ not yet deliver on all of its promises. Currently, Telodendria is not
ready for end-users yet. While it features very basic user ready for end-users yet. While it features very basic user
authentication, it does not actually work as a chat server yet. authentication, it does not actually work as a chat server yet.
We are hoping to ship Telodendria `v0.4.0` by May of 2024. This We are hoping to ship Telodendria `v1.7.0-alpha4` by January of 2025. This
release should be usable for communication between **local users** release should be usable for communication between **local users**
only. Additional features, including federation with other Matrix only. Additional features, including federation with other Matrix
homeservers will be added in future releases. homeservers will be added in future releases.
You can help speed up development by [sponsoring](#sponsorship) You can help speed up development by **sponsoring**
Telodendria or [getting involved](docs/CONTRIBUTING.md). Telodendria or [getting involved](https://git.telodendria.io/Telodendria/Telodendria/src/branch/master/docs/CONTRIBUTING.md).
## Sponsorship ## Sponsorship
@ -96,7 +96,7 @@ Telodendria's long-term success, please consider sponsoring the
project. project.
You can make a recurring donation to Telodendria using You can make a recurring donation to Telodendria using
[LiberaPay](https://bancino.net/Telodendria/donate). You can also make [LiberaPay](https://liberapay.com/Telodendria/donate). You can also make
one-time donations using one-time donations using
[Stripe](https://donate.stripe.com/8wM29AfF5bRJc48eUU). If you would [Stripe](https://donate.stripe.com/8wM29AfF5bRJc48eUU). If you would
like to make a recurring donation larger than that allowed by like to make a recurring donation larger than that allowed by
@ -108,7 +108,7 @@ LiberaPay, please contact Jordan Bancino over Matrix at
While there are no set sponsorship tiers at this time, sponsoring While there are no set sponsorship tiers at this time, sponsoring
Telodendria is a mutually beneficial relationship. Depending on the Telodendria is a mutually beneficial relationship. Depending on the
amount you donate, you can get your name, logo, and website links amount you donate, you can get your name, logo, and website links
on the [Sponsors](docs/SPONSORS.md) page, the project `README`, or the on the [Sponsors](../sponsors) page, the project `README`, or the
main website. main website.
## License ## License
@ -116,7 +116,7 @@ main website.
All of the code and documentation for Telodendria is licensed under a All of the code and documentation for Telodendria is licensed under a
modified MIT license. The MIT license is an extremely permissive modified MIT license. The MIT license is an extremely permissive
license that has very few restrictions. Please consult the license that has very few restrictions. Please consult the
[`LICENSE.txt`](LICENSE.txt) file for the actual license text. It is [`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 important to note that the Telodendria license text differs from the
original MIT license in the following ways: original MIT license in the following ways:
@ -133,4 +133,3 @@ 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 official project. If Telodendria is forked, the logo must be removed
completely from the project, and optionally replaced by a different completely from the project, and optionally replaced by a different
one. one.

View file

@ -1,4 +1,5 @@
{ {
"guard": "TELODENDRIA_SCHEMA_FILTER_H",
"header": "Schema\/Filter.h", "header": "Schema\/Filter.h",
"types": { "types": {
"FilterRoom": { "FilterRoom": {
@ -116,6 +117,5 @@
}, },
"type": "struct" "type": "struct"
} }
}, }
"guard": "TELODENDRIA_SCHEMA_FILTER_H"
} }

38
Schema/LoginRequest.json Normal file
View file

@ -0,0 +1,38 @@
{
"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"
}

49
Schema/RegToken.json Normal file
View file

@ -0,0 +1,49 @@
{
"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"
}
}
}

17
Schema/Registration.json Normal file
View file

@ -0,0 +1,17 @@
{
"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"
}

21
Schema/RequestToken.json Normal file
View file

@ -0,0 +1,21 @@
{
"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"
}

41
configure vendored
View file

@ -14,12 +14,12 @@ INCLUDE="src/include"
TOOLS="tools/src" TOOLS="tools/src"
SCHEMA="Schema" SCHEMA="Schema"
CFLAGS="-Wall -Wextra -pedantic -std=c89 -O3 -pipe -D_DEFAULT_SOURCE -I${INCLUDE}" CFLAGS="-Wall -Wextra -pedantic -std=c89 -O3 -pipe -D_DEFAULT_SOURCE -I${INCLUDE} -I${BUILD}"
LIBS="-lm -pthread -lCytoplasm" LIBS="-lm -pthread -lCytoplasm"
# Set default args for all platforms # Set default args for all platforms
SCRIPT_ARGS="--prefix=/usr/local --enable-ld-extra --bin-name=telodendria --version=0.4.0 --static $@" SCRIPT_ARGS="--cc=cc --prefix=/usr/local --enable-ld-extra --bin-name=telodendria --version=1.7.0-alpha4 --static $@"
echo "Processing options..." echo "Processing options..."
echo "Ran with arguments: $SCRIPT_ARGS" echo "Ran with arguments: $SCRIPT_ARGS"
@ -27,6 +27,9 @@ echo "Ran with arguments: $SCRIPT_ARGS"
# Process all arguments # Process all arguments
for arg in $SCRIPT_ARGS; do for arg in $SCRIPT_ARGS; do
case "$arg" in case "$arg" in
--cc=*)
CC=$(echo "$arg" | cut -d '=' -f 2-)
;;
--prefix=*) --prefix=*)
PREFIX=$(echo "$arg" | cut -d '=' -f 2-) PREFIX=$(echo "$arg" | cut -d '=' -f 2-)
;; ;;
@ -60,7 +63,7 @@ for arg in $SCRIPT_ARGS; do
STATIC="" STATIC=""
;; ;;
*) *)
echo "Invalid argument: $1" echo "Invalid argument: $arg"
exit 1 exit 1
;; ;;
esac esac
@ -112,8 +115,8 @@ compile_obj() {
src="$1" src="$1"
obj="$2" obj="$2"
pref=$(cc -I${INCLUDE} -MM -MT "${obj}" "${src}") pref=$(${CC} -I${INCLUDE} -I${BUILD} -MM -MT "${obj}" "${src}")
echo "$pref $(collect ${SCHEMA}/ .json .h ${INCLUDE}/Schema/ print_obj)" echo "$pref $(collect ${SCHEMA}/ .json .h ${BUILD}/Schema/ print_obj)"
echo "${TAB}@mkdir -p $(dirname ${obj})" echo "${TAB}@mkdir -p $(dirname ${obj})"
echo "${TAB}\$(CC) \$(CFLAGS) -fPIC -c -o \"${obj}\" \"${src}\"" echo "${TAB}\$(CC) \$(CFLAGS) -fPIC -c -o \"${obj}\" \"${src}\""
} }
@ -153,13 +156,19 @@ compile_schema() {
src="$1" src="$1"
out="$2" out="$2"
echo "${INCLUDE}/Schema/${out}.h:" obj="${BUILD}/Schema/${out}.o"
echo "${TAB}@mkdir -p ${INCLUDE}/Schema ${SRC}/Schema"
echo "${TAB}j2s -s \"${src}\" -h \"${INCLUDE}/Schema/${out}.h\" -c \"${SRC}/Schema/${out}.c\""
echo "${SRC}/Schema/${out}.c:" echo "${BUILD}/Schema/${out}.h:"
echo "${TAB}@mkdir -p ${INCLUDE}/Schema ${SRC}/Schema" echo "${TAB}@mkdir -p ${BUILD}/Schema"
echo "${TAB}j2s -s \"${src}\" -h \"${INCLUDE}/Schema/${out}.h\" -c \"${SRC}/Schema/${out}.c\"" echo "${TAB}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}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() { install_out() {
@ -185,22 +194,16 @@ uninstall_out() {
echo "Generating Makefile..." echo "Generating Makefile..."
OBJS=$(collect ${SRC}/ .c .o ${BUILD}/ print_obj) OBJS="$(collect ${SRC}/ .c .o ${BUILD}/ print_obj) $(collect ${SCHEMA}/ .json .o ${BUILD}/Schema/ print_obj)"
TAB=$(printf '\t') TAB=$(printf '\t')
# If objects don't include the schema (this is the first configure),
# then include them manually.
if ! echo "${OBJS}" | grep "Schema" > /dev/null; then
OBJS="${OBJS} $(collect ${SCHEMA}/ .json .o ${BUILD}/Schema/ print_obj)"
fi
cat << EOF > Makefile cat << EOF > Makefile
.POSIX: .POSIX:
# Generated by '$0' on $(date). # Generated by '$0' on $(date).
# This file should generally not be manually edited. # This file should generally not be manually edited.
CC = cc CC = ${CC}
PREFIX = ${PREFIX} PREFIX = ${PREFIX}
CFLAGS = ${CFLAGS} CFLAGS = ${CFLAGS}
LDFLAGS = ${LDFLAGS} LDFLAGS = ${LDFLAGS}

View file

@ -5,12 +5,22 @@ It is intended to be updated with every commit that makes a user-facing change w
reporting in the change log. As such, it changes frequently between releases. Final reporting in the change log. As such, it changes frequently between releases. Final
change log entries are published as [Releases](releases). change log entries are published as [Releases](releases).
## v0.4.0 ## v1.7.0-alpha4
**Not Released Yet.** **Not Released Yet.**
This release brings filters, rooms, and events! The core of the Matrix protocol architecture This release brings filters, rooms, and events! The core of the Matrix
is not in place. 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 ### Matrix Specification
@ -21,19 +31,30 @@ The following endpoints were added:
### Bug Fixes & General Improvements ### Bug Fixes & General Improvements
- Fixed a double-free in `RouteUserProfile()` that would cause errors with certain - Fixed a double-free in `RouteUserProfile()` that would cause errors
Matrix clients. (#35) with certain Matrix clients. (#35)
- Improved compatibility with NetBSD on various platforms. - Improved compatibility with NetBSD on various platforms.
- Moved [Cytoplasm](/Telodendria/Cytoplasm) to its own repository. - Moved [Cytoplasm](/Telodendria/Cytoplasm) to its own repository. It
- Use a `configure` script and `make` to build Telodendria instead of custom scripts. 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.
### New Features ### New Features
- Implemented `/_telodendria/admin/v1/deactivate/[localpart]` for admins to be able to - Moved all administrator API endpoints to `/_telodendria/admin/v1`,
deactivate users. because later revisions of the administrator API may break clients, so
- Moved all administrator API endpoints to `/_telodendria/admin/v1`, because later revisions we want a way to give those breaking revisions new endpoints.
of the administrator API may break clients, so we want a way to give those breaking revisions - Implemented `/_telodendria/admin/v1/deactivate/[localpart]` for admins
new endpoints. 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]`
## v0.3.0 ## v0.3.0

View file

@ -19,6 +19,7 @@ request.
- [Configuration](config.md) - [Configuration](config.md)
- [Server Statistics](stats.md) - [Server Statistics](stats.md)
- [Process Control](proc.md) - [Process Control](proc.md)
- [Registration Tokens](tokens.md)
## API Conventions ## API Conventions

View file

@ -4,11 +4,11 @@ As mentioned in [Setup](../setup.md), Telodendria's configuration is
intended to be managed via the configuration API. Consult the intended to be managed via the configuration API. Consult the
[Configuration](../config.md) document for a complete list of supported [Configuration](../config.md) document for a complete list of supported
configuration options. This document simply describes the API used to configuration options. This document simply describes the API used to
update the configuration. update the configuration described in that document.
## API Endpoints ## API Endpoints
### **GET** `/_telodendria/admin/config` ### **GET** `/_telodendria/admin/v1/config`
Retrieve the current configuration. Retrieve the current configuration.
@ -20,7 +20,7 @@ Retrieve the current configuration.
|---------------|-------------| |---------------|-------------|
| 200 | The current configuration was successfully retrieved.| | 200 | The current configuration was successfully retrieved.|
### **POST** `/_telodendria/admin/config` ### **POST** `/_telodendria/admin/v1/config`
Installs a new configuration. This endpoint validates the request body, Installs a new configuration. This endpoint validates the request body,
ensuring it is a proper configuration, then it replaces the existing ensuring it is a proper configuration, then it replaces the existing
@ -40,3 +40,23 @@ configuration with the new one.
|-------|------|-------------| |-------|------|-------------|
| `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. | `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

@ -41,7 +41,7 @@ this privilege level.
The following API endpoints are implemented for managing privileges. The following API endpoints are implemented for managing privileges.
### **GET** `/_telodendria/admin/privileges/[localpart]` ### **GET** `/_telodendria/admin/v1/privileges/[localpart]`
Retrieve the permissions for a user. If the localpart is omitted, then Retrieve the permissions for a user. If the localpart is omitted, then
retrieve the privileges for the user that owns the access token being retrieve the privileges for the user that owns the access token being
@ -62,7 +62,7 @@ used. Note that the owner of the access token must have the
|-------|------|-------------| |-------|------|-------------|
| `privileges` | `Array` | An array of privileges, as described above. The privileges are encoded as JSON strings.| | `privileges` | `Array` | An array of privileges, as described above. The privileges are encoded as JSON strings.|
### **POST** `/_telodendria/admin/privileges/[localpart]` ### **POST** `/_telodendria/admin/v1/privileges/[localpart]`
Update the privileges of a local user by replacing the privileges array 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 with the one specified in the request. Like the **GET** version of this
@ -89,7 +89,7 @@ owns the access token.
|-------|------|-------------| |-------|------|-------------|
| `privileges` | `Array` | An array of privileges, as described above. The privileges are encoded as JSON strings.| | `privileges` | `Array` | An array of privileges, as described above. The privileges are encoded as JSON strings.|
### **PUT** `/_telodendria/admin/privileges/[localpart]` ### **PUT** `/_telodendria/admin/v1/privileges/[localpart]`
Update the privileges of a local user by adding the privileges Update the privileges of a local user by adding the privileges
specified in the request to the users existing privileges. specified in the request to the users existing privileges.
@ -114,7 +114,7 @@ specified in the request to the users existing privileges.
|-------|------|-------------| |-------|------|-------------|
| `privileges` | `Array` | An array of privileges, as described above. The privileges are encoded as JSON strings.| | `privileges` | `Array` | An array of privileges, as described above. The privileges are encoded as JSON strings.|
### **DELETE** `/_telodendria/admin/privileges/[localpart]` ### **DELETE** `/_telodendria/admin/v1/privileges/[localpart]`
Update the privileges of a local user by removing the privileges Update the privileges of a local user by removing the privileges
specified in the request from the user's existing privileges. specified in the request from the user's existing privileges.

View file

@ -5,7 +5,7 @@ administrator to manage the Telodendria process itself.
## API Endpoints ## API Endpoints
### **POST** `/_telodendria/admin/restart` ### **POST** `/_telodendria/admin/v1/restart`
Restart the Telodendria daemon cleanly. This endpoint will respond Restart the Telodendria daemon cleanly. This endpoint will respond
immediately after signaling to the daemon that it should be restarted immediately after signaling to the daemon that it should be restarted
@ -26,7 +26,7 @@ starts over.
On success, this endpoint simply returns an empty JSON object. On success, this endpoint simply returns an empty JSON object.
### **POST** `/_telodendria/admin/shutdown` ### **POST** `/_telodendria/admin/v1/shutdown`
Shut down the Telodendria process cleanly. This endpoint will respond Shut down the Telodendria process cleanly. This endpoint will respond
immediately after signalling to the daemon that it should be shut immediately after signalling to the daemon that it should be shut

View file

@ -5,7 +5,7 @@ information about how the server process is performing.
## API Endpoints ## API Endpoints
### **GET** `/_telodendria/admin/stats` ### **GET** `/_telodendria/admin/v1/stats`
Retrieve basic statistics about the currently running Telodendria Retrieve basic statistics about the currently running Telodendria
process. process.

106
docs/user/admin/tokens.md Normal file
View file

@ -0,0 +1,106 @@
# 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,138 +0,0 @@
<!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.">
<meta http-equiv="onion-location" content="http://cszrjvqbeim4dbbynucd4xucpfsips4alpxyxm5s3uh2itjxsnythhyd.onion">
<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 style="font-size: x-large;" 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 and is not
yet ready for use.
Please see the <a href="/man/man7/telodendria-changelog.7.html#PROJECT_STATUS">Project Status</a>
for information about the project state, and use the links below to help fund development.
</div>
<h2 id="donate">Donate</h2>
<p>
If you would like to donate to this project, you can do so with the
following links:
</p>
<ul>
<li><a href="https://liberapay.com/Telodendria/donate">LiberaPay</a> (recurring, account required)</li>
<li><a href="https://donate.stripe.com/8wM29AfF5bRJc48eUU">Stripe</a> (one-time, no account required)</li>
</ul>
<p>
If you would like to do a recurring donation larger than what's allowed
by LiberaPay, please contact me directly on Matrix at <code>@jordan:bancino.net</code>.
</p>
<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>
<table>
<tr>
<th>Version</th>
<th>Download</th>
<th>Checksum</th>
<th>Signature</th>
</tr>
<tr>
<td>${TELODENDRIA_VERSION}</td>
<td>
<a href="/pub/v${TELODENDRIA_VERSION}/Telodendria-v${TELODENDRIA_VERSION}.tar.gz">
<code>Telodendria-v${TELODENDRIA_VERSION}.tar.gz</code>
</a>
</td>
<td>
<a href="/pub/v${TELODENDRIA_VERSION}/Telodendria-v${TELODENDRIA_VERSION}.tar.gz.sha256">
sha256
</a>
</td>
<td>
<a href="/pub/v${TELODENDRIA_VERSION}/Telodendria-v${TELODENDRIA_VERSION}.tar.gz.sig">
signify
</a>
</td>
</tr>
</table>
<p>
See the <a href="man/man7/telodendria-changelog.7.html">change log</a> for
release notes. If you are looking for older <b>Telodendria</b> versions, you
can find them <a href="/pub">here</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. Consult your operating system's
manual for how to install packages, as well as the official repository,
to see if a package is available. 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 <i>not</i> have a package or port of
<b>Telodendria</b>, please consult the
<a href="/man/man7/porting.7.html">porting(7)</a> page for guidelines
related to packaging <b>Telodendria</b> for your system.
</p>
<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>
${USER_DOCS}
<br>
<details>
<summary>Developer Documentation:</summary>
<p>
This documentation is intended primarily for developers. It details all
of the internal workings of Telodendria.
</p>
${DEV_DOCS}
</details>
<hr>
<p>
&copy; 2023 Jordan Bancino &lt;@jordan:bancino.net&gt;
<br>
Updated on ${DATE}.
</p>
</body>
</html>

View file

@ -1,368 +0,0 @@
/* $OpenBSD: mandoc.css,v 1.39 2022/07/06 14:27:55 schwarze Exp $ */
/*
* Standard style sheet for mandoc(1) -Thtml and man.cgi(8).
*
* Written by Ingo Schwarze <schwarze@openbsd.org>.
* I place this file into the public domain.
* Permission to use, copy, modify, and distribute it for any purpose
* with or without fee is hereby granted, without any conditions.
*/
/* Global defaults. */
html { max-width: 65em;
--bg: #FFFFFF;
--fg: #000000; }
body { background: var(--bg);
color: var(--fg);
font-family: Helvetica,Arial,sans-serif; }
h1, h2 { font-size: 110%; }
table { margin-top: 0em;
margin-bottom: 0em;
border-collapse: collapse; }
/* Some browsers set border-color in a browser style for tbody,
* but not for table, resulting in inconsistent border styling. */
tbody { border-color: inherit; }
tr { border-color: inherit; }
td { vertical-align: top;
padding-left: 0.2em;
padding-right: 0.2em;
border-color: inherit; }
ul, ol, dl { margin-top: 0em;
margin-bottom: 0em; }
li, dt { margin-top: 1em; }
pre { font-family: inherit; }
.permalink { border-bottom: thin dotted;
color: inherit;
font: inherit;
text-decoration: inherit; }
* { clear: both }
/* Search form and search results. */
fieldset { border: thin solid silver;
border-radius: 1em;
text-align: center; }
input[name=expr] {
width: 25%; }
table.results { margin-top: 1em;
margin-left: 2em;
font-size: smaller; }
/* Header and footer lines. */
div[role=doc-pageheader] {
display: flex;
border-bottom: 1px dotted #808080;
margin-bottom: 1em;
font-size: smaller; }
.head-ltitle { flex: 1; }
.head-vol { flex: 0 1 auto;
text-align: center; }
.head-rtitle { flex: 1;
text-align: right; }
div[role=doc-pagefooter] {
display: flex;
justify-content: space-between;
border-top: 1px dotted #808080;
margin-top: 1em;
font-size: smaller; }
.foot-left { flex: 1; }
.foot-date { flex: 0 1 auto;
text-align: center; }
.foot-os { flex: 1;
text-align: right; }
/* Sections and paragraphs. */
main { margin-left: 3.8em; }
.Nd { }
section.Sh { }
h2.Sh { margin-top: 1.2em;
margin-bottom: 0.6em;
margin-left: -3.2em; }
section.Ss { }
h3.Ss { margin-top: 1.2em;
margin-bottom: 0.6em;
margin-left: -1.2em;
font-size: 105%; }
.Pp { margin: 0.6em 0em; }
.Sx { }
.Xr { }
/* 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 td { vertical-align: middle; }
.HP { margin-left: 3.8em;
text-indent: -3.8em; }
/* Semantic markup for command line utilities. */
table.Nm { }
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. */
h2.Sh, h3.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; }
.An::before { content: "An"; }
.Ar::before { content: "Ar"; }
.Cd::before { content: "Cd"; }
.Cm::before { content: "Cm"; }
.Dv::before { content: "Dv"; }
.Em::before { content: "Em"; }
.Er::before { content: "Er"; }
.Ev::before { content: "Ev"; }
.Fa::before { content: "Fa"; }
.Fd::before { content: "Fd"; }
.Fl::before { content: "Fl"; }
.Fn::before { content: "Fn"; }
.Ft::before { content: "Ft"; }
.Ic::before { content: "Ic"; }
code.In::before { content: "In"; }
.Lb::before { content: "Lb"; }
.Lk::before { content: "Lk"; }
.Ms::before { content: "Ms"; }
.Mt::before { content: "Mt"; }
.Nd::before { content: "Nd"; }
code.Nm::before { content: "Nm"; }
.Pa::before { content: "Pa"; }
.Rs::before { content: "Rs"; }
h2.Sh::before { content: "Sh"; }
h3.Ss::before { content: "Ss"; }
.St::before { content: "St"; }
.Sx::before { content: "Sx"; }
.Sy::before { content: "Sy"; }
.Va::before { content: "Va"; }
.Vt::before { content: "Vt"; }
.Xr::before { content: "Xr"; }
.An::before, .Ar::before, .Cd::before, .Cm::before,
.Dv::before, .Em::before, .Er::before, .Ev::before,
.Fa::before, .Fd::before, .Fl::before, .Fn::before, .Ft::before,
.Ic::before, code.In::before, .Lb::before, .Lk::before,
.Ms::before, .Mt::before, .Nd::before, code.Nm::before,
.Pa::before, .Rs::before,
h2.Sh::before, h3.Ss::before, .St::before, .Sx::before, .Sy::before,
.Va::before, .Vt::before, .Xr::before {
opacity: 0;
transition: .15s ease opacity;
pointer-events: none;
position: absolute;
bottom: 100%;
box-shadow: 0 0 .35em var(--fg);
padding: .15em .25em;
white-space: nowrap;
font-family: Helvetica,Arial,sans-serif;
font-style: normal;
font-weight: bold;
background: var(--bg);
color: var(--fg); }
.An:hover::before, .Ar:hover::before, .Cd:hover::before, .Cm:hover::before,
.Dv:hover::before, .Em:hover::before, .Er:hover::before, .Ev:hover::before,
.Fa:hover::before, .Fd:hover::before, .Fl:hover::before, .Fn:hover::before,
.Ft:hover::before, .Ic:hover::before, code.In:hover::before,
.Lb:hover::before, .Lk:hover::before, .Ms:hover::before, .Mt:hover::before,
.Nd:hover::before, code.Nm:hover::before, .Pa:hover::before,
.Rs:hover::before, h2.Sh:hover::before, h3.Ss:hover::before, .St:hover::before,
.Sx:hover::before, .Sy:hover::before, .Va:hover::before, .Vt:hover::before,
.Xr:hover::before {
opacity: 1;
pointer-events: inherit; }
/* Overrides to avoid excessive margins on small devices. */
@media (max-width: 37.5em) {
main { margin-left: 0.5em; }
h2.Sh, h3.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; }
}
/* Overrides for a dark color scheme for accessibility. */
@media (prefers-color-scheme: dark) {
html { --bg: #1E1F21;
--fg: #EEEFF1; }
:link { color: #BAD7FF; }
:visited { color: #F6BAFF; }
}

View file

@ -1,114 +0,0 @@
@import "mandoc.css";
:root {
--border-radius: 10px;
--color-snippet: #161b22;
--color-link: #7b8333;
--color-bg: #0d1117;
--color-text: #c9d1d9;
--color-table-border: #30363d;
--color-table-accent: #161b22;
--color-error-bg: #5c6434;
--color-error: white;
}
html {
max-width: 100%;
}
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
}
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) !important;
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;
}
/* Mandoc overrides */
.Bd {
background-color: var(--color-snippet);
border-radius: var(--border-radius);
padding-left: 10px;
overflow: auto;
}
.Nm {
width: fit-content;
}
.Nm td, .Nm th {
border: none;
}

View file

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

View file

@ -28,5 +28,7 @@
HashMap * HashMap *
FilterApply(Filter * filter, HashMap * event) FilterApply(Filter * filter, HashMap * event)
{ {
(void) filter;
(void) event;
return NULL; return NULL;
} }

View file

@ -128,10 +128,6 @@ start:
httpServers = NULL; httpServers = NULL;
restart = 0; restart = 0;
/* For getopt() */
opterr = 1;
optind = 1;
/* Local variables */ /* Local variables */
exit = EXIT_SUCCESS; exit = EXIT_SUCCESS;
flags = 0; flags = 0;

View file

@ -30,8 +30,10 @@
#include <Cytoplasm/Json.h> #include <Cytoplasm/Json.h>
#include <Cytoplasm/Util.h> #include <Cytoplasm/Util.h>
#include <Cytoplasm/Str.h> #include <Cytoplasm/Str.h>
#include <User.h>
#include <Cytoplasm/Int64.h> #include <Cytoplasm/Int64.h>
#include <Cytoplasm/Log.h>
#include <User.h>
int int
RegTokenValid(RegTokenInfo * token) RegTokenValid(RegTokenInfo * token)
@ -99,8 +101,7 @@ RegTokenDelete(RegTokenInfo * token)
{ {
return 0; return 0;
} }
Free(token->name); RegTokenInfoFree(token);
Free(token->owner);
Free(token); Free(token);
return 1; return 1;
} }
@ -113,6 +114,8 @@ RegTokenGetInfo(Db * db, char *token)
DbRef *tokenRef; DbRef *tokenRef;
HashMap *tokenJson; HashMap *tokenJson;
char *errp = NULL;
if (!RegTokenExists(db, token)) if (!RegTokenExists(db, token))
{ {
return NULL; return NULL;
@ -126,47 +129,43 @@ RegTokenGetInfo(Db * db, char *token)
tokenJson = DbJson(tokenRef); tokenJson = DbJson(tokenRef);
ret = Malloc(sizeof(RegTokenInfo)); 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->db = db;
ret->ref = tokenRef; ret->ref = tokenRef;
ret->owner =
StrDuplicate(JsonValueAsString(HashMapGet(tokenJson, "created_by")));
ret->name = StrDuplicate(token);
ret->expires =
JsonValueAsInteger(HashMapGet(tokenJson, "expires_on"));
ret->created =
JsonValueAsInteger(HashMapGet(tokenJson, "created_on"));
ret->uses =
JsonValueAsInteger(HashMapGet(tokenJson, "uses"));
ret->used =
JsonValueAsInteger(HashMapGet(tokenJson, "used"));
ret->grants =
UserDecodePrivileges(HashMapGet(tokenJson, "grants"));
return ret; return ret;
} }
void void
RegTokenFree(RegTokenInfo * tokeninfo) RegTokenFree(RegTokenInfo *tokeninfo)
{ {
if (tokeninfo) if (tokeninfo)
{ {
Free(tokeninfo->name); RegTokenInfoFree(tokeninfo);
Free(tokeninfo->owner);
Free(tokeninfo); Free(tokeninfo);
} }
} }
int int
RegTokenClose(RegTokenInfo * tokeninfo) RegTokenClose(RegTokenInfo * tokeninfo)
{ {
HashMap *json;
if (!tokeninfo) if (!tokeninfo)
{ {
return 0; 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); return DbUnlock(tokeninfo->db, tokeninfo->ref);
} }
static int static int
@ -202,7 +201,6 @@ RegTokenInfo *
RegTokenCreate(Db * db, char *name, char *owner, UInt64 expires, Int64 uses, int privileges) RegTokenCreate(Db * db, char *name, char *owner, UInt64 expires, Int64 uses, int privileges)
{ {
RegTokenInfo *ret; RegTokenInfo *ret;
HashMap *tokenJson;
UInt64 timestamp = UtilServerTs(); UInt64 timestamp = UtilServerTs();
@ -235,26 +233,12 @@ RegTokenCreate(Db * db, char *name, char *owner, UInt64 expires, Int64 uses, int
return NULL; return NULL;
} }
ret->name = StrDuplicate(name); ret->name = StrDuplicate(name);
ret->owner = StrDuplicate(owner); ret->created_by = StrDuplicate(owner);
ret->used = Int64Create(0, 0); ret->used = Int64Create(0, 0);
ret->uses = uses; ret->uses = uses;
ret->created = timestamp; ret->created_on = timestamp;
ret->expires = expires; ret->expires_on = expires;
ret->grants = privileges; ret->grants = UserEncodePrivileges(privileges);
/* Write user info to database. */
tokenJson = DbJson(ret->ref);
HashMapSet(tokenJson, "created_by",
JsonValueString(ret->owner));
HashMapSet(tokenJson, "created_on",
JsonValueInteger(ret->created));
HashMapSet(tokenJson, "expires_on",
JsonValueInteger(ret->expires));
HashMapSet(tokenJson, "used",
JsonValueInteger(ret->used));
HashMapSet(tokenJson, "uses",
JsonValueInteger(ret->uses));
HashMapSet(tokenJson, "grants", UserEncodePrivileges(privileges));
return ret; return ret;
} }

View file

@ -42,6 +42,8 @@ struct Room
Room * Room *
RoomCreate(Db * db, RoomCreateRequest * req) RoomCreate(Db * db, RoomCreateRequest * req)
{ {
(void) db;
(void) req;
return NULL; return NULL;
} }

View file

@ -87,6 +87,8 @@ RouterBuild(void)
R("/_telodendria/admin/v1/privileges", RoutePrivileges); R("/_telodendria/admin/v1/privileges", RoutePrivileges);
R("/_telodendria/admin/v1/privileges/(.*)", RoutePrivileges); R("/_telodendria/admin/v1/privileges/(.*)", RoutePrivileges);
R("/_telodendria/admin/v1/deactivate/(.*)", RouteAdminDeactivate); R("/_telodendria/admin/v1/deactivate/(.*)", RouteAdminDeactivate);
R("/_telodendria/admin/v1/tokens/(.*)", RouteAdminTokens);
R("/_telodendria/admin/v1/tokens", RouteAdminTokens);
#undef R #undef R

View file

@ -28,7 +28,6 @@
#include <Cytoplasm/Str.h> #include <Cytoplasm/Str.h>
#include <User.h> #include <User.h>
#include <Cytoplasm/Log.h>
ROUTE_IMPL(RouteAdminDeactivate, path, argp) ROUTE_IMPL(RouteAdminDeactivate, path, argp)
{ {

View file

@ -0,0 +1,207 @@
/*
* Copyright (C) 2022-2023 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 <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

@ -65,6 +65,8 @@ ROUTE_IMPL(RouteChangePwd, path, argp)
char *token; char *token;
char *newPassword; char *newPassword;
char *msg;
Config *config = ConfigLock(db); Config *config = ConfigLock(db);
if (!config) if (!config)
@ -78,8 +80,9 @@ ROUTE_IMPL(RouteChangePwd, path, argp)
if (HttpRequestMethodGet(args->context) != HTTP_POST) if (HttpRequestMethodGet(args->context) != HTTP_POST)
{ {
msg = "Route only supports POST.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_UNRECOGNIZED, NULL); response = MatrixErrorCreate(M_UNRECOGNIZED, msg);
goto finish; goto finish;
} }
@ -118,9 +121,10 @@ ROUTE_IMPL(RouteChangePwd, path, argp)
newPassword = JsonValueAsString(HashMapGet(request, "new_password")); newPassword = JsonValueAsString(HashMapGet(request, "new_password"));
if (!newPassword) if (!newPassword)
{ {
msg = "'new_password' is unset or not a string.";
JsonFree(request); JsonFree(request);
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_BAD_JSON, NULL); response = MatrixErrorCreate(M_BAD_JSON, msg);
goto finish; goto finish;
} }

View file

@ -33,12 +33,14 @@ ROUTE_IMPL(RouteConfig, path, argp)
RouteArgs *args = argp; RouteArgs *args = argp;
HashMap *response; HashMap *response;
char *token; char *token;
char *msg;
User *user = NULL; User *user = NULL;
Config *config = NULL; Config *config = NULL;
HashMap *request = NULL; HashMap *request = NULL;
Config *newConf; Config *newConf;
HashMap *newJson = NULL;
(void) path; (void) path;
@ -58,17 +60,19 @@ ROUTE_IMPL(RouteConfig, path, argp)
if (!(UserGetPrivileges(user) & USER_CONFIG)) if (!(UserGetPrivileges(user) & USER_CONFIG))
{ {
msg = "User does not have the 'CONFIG' privilege.";
HttpResponseStatus(args->context, HTTP_FORBIDDEN); HttpResponseStatus(args->context, HTTP_FORBIDDEN);
response = MatrixErrorCreate(M_FORBIDDEN, NULL); response = MatrixErrorCreate(M_FORBIDDEN, msg);
goto finish; goto finish;
} }
config = ConfigLock(args->matrixArgs->db); config = ConfigLock(args->matrixArgs->db);
if (!config) if (!config)
{ {
msg = "Internal server error while locking configuration.";
Log(LOG_ERR, "Config endpoint failed to lock configuration."); Log(LOG_ERR, "Config endpoint failed to lock configuration.");
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR); HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
response = MatrixErrorCreate(M_UNKNOWN, NULL); response = MatrixErrorCreate(M_UNKNOWN, msg);
goto finish; goto finish;
} }
@ -89,8 +93,9 @@ ROUTE_IMPL(RouteConfig, path, argp)
newConf = ConfigParse(request); newConf = ConfigParse(request);
if (!newConf) if (!newConf)
{ {
msg = "Internal server error while parsing config.";
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR); HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
response = MatrixErrorCreate(M_UNKNOWN, NULL); response = MatrixErrorCreate(M_UNKNOWN, msg);
break; break;
} }
@ -107,8 +112,9 @@ ROUTE_IMPL(RouteConfig, path, argp)
} }
else else
{ {
msg = "Internal server error while writing the config.";
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR); HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
response = MatrixErrorCreate(M_UNKNOWN, NULL); response = MatrixErrorCreate(M_UNKNOWN, msg);
} }
} }
else else
@ -121,10 +127,59 @@ ROUTE_IMPL(RouteConfig, path, argp)
JsonFree(request); JsonFree(request);
break; break;
case HTTP_PUT: case HTTP_PUT:
/* TODO: Support incremental changes to the config */ request = JsonDecode(HttpServerStream(args->context));
default: if (!request)
{
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_UNRECOGNIZED, NULL); response = MatrixErrorCreate(M_NOT_JSON, NULL);
break;
}
newJson = JsonDuplicate(DbJson(config->ref));
JsonMerge(newJson, request);
newConf = ConfigParse(newJson);
if (!newConf)
{
msg = "Internal server error while parsing config.";
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
response = MatrixErrorCreate(M_UNKNOWN, msg);
break;
}
if (newConf->ok)
{
if (DbJsonSet(config->ref, newJson))
{
response = HashMapCreate();
/*
* TODO: Apply configuration and set this only if a main
* component was reconfigured, such as the listeners.
*/
HashMapSet(response, "restart_required", JsonValueBoolean(1));
}
else
{
msg = "Internal server error while writing the config.";
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
response = MatrixErrorCreate(M_UNKNOWN, msg);
}
}
else
{
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_BAD_JSON, newConf->err);
}
ConfigFree(newConf);
JsonFree(request);
JsonFree(newJson);
break;
default:
msg = "Unknown request method.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_UNRECOGNIZED, msg);
break; break;
} }

View file

@ -41,8 +41,9 @@ ROUTE_IMPL(RouteCreateRoom, path, argp)
if (HttpRequestMethodGet(args->context) != HTTP_POST) if (HttpRequestMethodGet(args->context) != HTTP_POST)
{ {
err = "Unknown request method.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_UNRECOGNIZED, "Unknown request method."); response = MatrixErrorCreate(M_UNRECOGNIZED, err);
goto finish; goto finish;
} }

View file

@ -47,6 +47,8 @@ ROUTE_IMPL(RouteDeactivate, path, argp)
User *user = NULL; User *user = NULL;
Config *config = ConfigLock(db); Config *config = ConfigLock(db);
char *msg;
(void) path; (void) path;
if (!config) if (!config)
@ -59,8 +61,9 @@ ROUTE_IMPL(RouteDeactivate, path, argp)
if (HttpRequestMethodGet(args->context) != HTTP_POST) if (HttpRequestMethodGet(args->context) != HTTP_POST)
{ {
msg = "Route only accepts POST.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_UNRECOGNIZED, NULL); response = MatrixErrorCreate(M_UNRECOGNIZED, msg);
goto finish; goto finish;
} }
@ -128,8 +131,9 @@ ROUTE_IMPL(RouteDeactivate, path, argp)
if (!UserDeleteTokens(user, NULL) || !UserDeactivate(user, NULL, NULL)) if (!UserDeleteTokens(user, NULL) || !UserDeactivate(user, NULL, NULL))
{ {
msg = "Internal server error: couldn't remove user properly.";
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR); HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
response = MatrixErrorCreate(M_UNKNOWN, NULL); response = MatrixErrorCreate(M_UNKNOWN, msg);
goto finish; goto finish;
} }

View file

@ -69,6 +69,8 @@ ROUTE_IMPL(RouteFilter, path, argp)
char *userParam = ArrayGet(path, 0); char *userParam = ArrayGet(path, 0);
char *msg;
if (!userParam) if (!userParam)
{ {
/* Should be impossible */ /* Should be impossible */
@ -87,15 +89,17 @@ ROUTE_IMPL(RouteFilter, path, argp)
id = UserIdParse(userParam, serverName); id = UserIdParse(userParam, serverName);
if (!id) if (!id)
{ {
msg = "Invalid user ID.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_INVALID_PARAM, NULL); response = MatrixErrorCreate(M_INVALID_PARAM, msg);
goto finish; goto finish;
} }
if (!StrEquals(id->server, serverName)) if (!StrEquals(id->server, serverName))
{ {
msg = "Cannot use /filter for non-local users.";
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED); HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
response = MatrixErrorCreate(M_UNAUTHORIZED, NULL); response = MatrixErrorCreate(M_UNAUTHORIZED, msg);
goto finish; goto finish;
} }
@ -115,8 +119,9 @@ ROUTE_IMPL(RouteFilter, path, argp)
if (!StrEquals(id->localpart, UserGetName(user))) if (!StrEquals(id->localpart, UserGetName(user)))
{ {
msg = "Unauthorized to use /filter.";
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED); HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
response = MatrixErrorCreate(M_INVALID_PARAM, NULL); response = MatrixErrorCreate(M_INVALID_PARAM, msg);
goto finish; goto finish;
} }
@ -126,8 +131,9 @@ ROUTE_IMPL(RouteFilter, path, argp)
if (!ref) if (!ref)
{ {
msg = "The filter for this user was not found.";
HttpResponseStatus(args->context, HTTP_NOT_FOUND); HttpResponseStatus(args->context, HTTP_NOT_FOUND);
response = MatrixErrorCreate(M_NOT_FOUND, NULL); response = MatrixErrorCreate(M_NOT_FOUND, msg);
goto finish; goto finish;
} }
@ -161,8 +167,9 @@ ROUTE_IMPL(RouteFilter, path, argp)
filterId = StrRandom(12); filterId = StrRandom(12);
if (!filterId) if (!filterId)
{ {
msg = "Couldn't generate random filter ID; this is unintended.";
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR); HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
response = MatrixErrorCreate(M_UNKNOWN, NULL); response = MatrixErrorCreate(M_UNKNOWN, msg);
goto finish; goto finish;
} }
@ -170,8 +177,9 @@ ROUTE_IMPL(RouteFilter, path, argp)
if (!ref) if (!ref)
{ {
Free(filterId); Free(filterId);
msg = "Couldn't write filter to the database, this is unintended.";
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR); HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
response = MatrixErrorCreate(M_UNKNOWN, NULL); response = MatrixErrorCreate(M_UNKNOWN, msg);
goto finish; goto finish;
} }

View file

@ -25,6 +25,8 @@
#include <string.h> #include <string.h>
#include <Schema/LoginRequest.h>
#include <Cytoplasm/Json.h> #include <Cytoplasm/Json.h>
#include <Cytoplasm/HashMap.h> #include <Cytoplasm/HashMap.h>
#include <Cytoplasm/Str.h> #include <Cytoplasm/Str.h>
@ -43,12 +45,9 @@ ROUTE_IMPL(RouteLogin, path, argp)
HashMap *identifier; HashMap *identifier;
char *deviceId = NULL; LoginRequest loginRequest;
char *initialDeviceDisplayName = NULL; LoginRequestUserIdentifier userIdentifier;
int refreshToken = 0;
char *password;
char *type;
UserId *userId = NULL; UserId *userId = NULL;
Db *db = args->matrixArgs->db; Db *db = args->matrixArgs->db;
@ -57,6 +56,14 @@ ROUTE_IMPL(RouteLogin, path, argp)
UserLoginInfo *loginInfo; UserLoginInfo *loginInfo;
char *fullUsername; char *fullUsername;
char *type;
char *initialDeviceDisplayName;
char *deviceId;
char *password;
int refreshToken;
char *msg;
Config *config = ConfigLock(db); Config *config = ConfigLock(db);
if (!config) if (!config)
@ -68,6 +75,9 @@ ROUTE_IMPL(RouteLogin, path, argp)
(void) path; (void) path;
memset(&loginRequest, 0, sizeof(LoginRequest));
memset(&userIdentifier, 0, sizeof(LoginRequestUserIdentifier));
switch (HttpRequestMethodGet(args->context)) switch (HttpRequestMethodGet(args->context))
{ {
case HTTP_GET: case HTTP_GET:
@ -88,49 +98,27 @@ ROUTE_IMPL(RouteLogin, path, argp)
break; break;
} }
val = HashMapGet(request, "type"); if (!LoginRequestFromJson(request, &loginRequest, &msg))
if (!val)
{ {
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_MISSING_PARAM, NULL); response = MatrixErrorCreate(M_BAD_JSON, msg);
break; break;
} }
if (JsonValueType(val) != JSON_STRING) if (loginRequest.type != REQUEST_TYPE_PASSWORD)
{ {
msg = "Unsupported login type.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_BAD_JSON, NULL); response = MatrixErrorCreate(M_UNRECOGNIZED, msg);
break; break;
} }
type = JsonValueAsString(val); identifier = loginRequest.identifier;
if (!StrEquals(type, "m.login.password"))
{
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_UNRECOGNIZED, NULL);
break;
}
val = HashMapGet(request, "identifier");
if (!val)
{
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_MISSING_PARAM, NULL);
break;
}
if (JsonValueType(val) != JSON_OBJECT)
{
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_BAD_JSON, NULL);
break;
}
identifier = JsonValueAsObject(val);
val = HashMapGet(identifier, "type"); val = HashMapGet(identifier, "type");
if (!val) if (!val)
{ {
msg = "No login identifier type set.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_MISSING_PARAM, NULL); response = MatrixErrorCreate(M_MISSING_PARAM, NULL);
break; break;
@ -138,112 +126,60 @@ ROUTE_IMPL(RouteLogin, path, argp)
if (JsonValueType(val) != JSON_STRING) if (JsonValueType(val) != JSON_STRING)
{ {
msg = "Invalid login identifier type.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_BAD_JSON, NULL); response = MatrixErrorCreate(M_BAD_JSON, msg);
break; break;
} }
type = JsonValueAsString(val); type = JsonValueAsString(val);
if (!StrEquals(type, "m.id.user")) if (!StrEquals(type, "m.id.user"))
{ {
msg = "Invalid login identifier type.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_UNRECOGNIZED, NULL); response = MatrixErrorCreate(M_UNRECOGNIZED, msg);
break; break;
} }
if (!LoginRequestUserIdentifierFromJson(identifier,
val = HashMapGet(identifier, "user"); &userIdentifier, &msg))
if (!val)
{ {
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_MISSING_PARAM, NULL); response = MatrixErrorCreate(M_BAD_JSON, msg);
break; break;
} }
if (JsonValueType(val) != JSON_STRING)
{
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_BAD_JSON, NULL);
break;
}
userId = UserIdParse(JsonValueAsString(val), config->serverName); userId = UserIdParse(userIdentifier.user, config->serverName);
if (!userId) if (!userId)
{ {
msg = "Invalid user ID.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_BAD_JSON, NULL); response = MatrixErrorCreate(M_BAD_JSON, msg);
break; break;
} }
if (!StrEquals(userId->server, config->serverName) if (!StrEquals(userId->server, config->serverName)
|| !UserExists(db, userId->localpart)) || !UserExists(db, userId->localpart))
{ {
msg = "Unknown user ID.";
HttpResponseStatus(args->context, HTTP_FORBIDDEN); HttpResponseStatus(args->context, HTTP_FORBIDDEN);
response = MatrixErrorCreate(M_FORBIDDEN, NULL); response = MatrixErrorCreate(M_FORBIDDEN, msg);
break; break;
} }
val = HashMapGet(request, "device_id"); deviceId = loginRequest.device_id;
if (val)
{
if (JsonValueType(val) != JSON_STRING)
{
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_BAD_JSON, NULL);
break;
}
deviceId = JsonValueAsString(val); initialDeviceDisplayName =loginRequest.initial_device_display_name;
} password = loginRequest.password;
refreshToken = loginRequest.refresh_token;
val = HashMapGet(request, "initial_device_display_name");
if (val)
{
if (JsonValueType(val) != JSON_STRING)
{
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_BAD_JSON, NULL);
break;
}
initialDeviceDisplayName = JsonValueAsString(val);
}
val = HashMapGet(request, "password");
if (!val)
{
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_MISSING_PARAM, NULL);
break;
}
if (JsonValueType(val) != JSON_STRING)
{
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_BAD_JSON, NULL);
break;
}
password = JsonValueAsString(val);
val = HashMapGet(request, "refresh_token");
if (val)
{
if (JsonValueType(val) != JSON_BOOLEAN)
{
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_BAD_JSON, NULL);
break;
}
refreshToken = JsonValueAsBoolean(val);
}
user = UserLock(db, userId->localpart); user = UserLock(db, userId->localpart);
if (!user) if (!user)
{ {
msg = "Couldn't lock user.";
HttpResponseStatus(args->context, HTTP_FORBIDDEN); HttpResponseStatus(args->context, HTTP_FORBIDDEN);
response = MatrixErrorCreate(M_FORBIDDEN, NULL); response = MatrixErrorCreate(M_FORBIDDEN, msg);
break; break;
} }
@ -261,10 +197,11 @@ ROUTE_IMPL(RouteLogin, path, argp)
if (!loginInfo) if (!loginInfo)
{ {
msg = "Invalid creditentials for user.";
UserUnlock(user); UserUnlock(user);
HttpResponseStatus(args->context, HTTP_FORBIDDEN); HttpResponseStatus(args->context, HTTP_FORBIDDEN);
response = MatrixErrorCreate(M_FORBIDDEN, NULL); response = MatrixErrorCreate(M_FORBIDDEN, msg);
break; break;
} }
@ -300,8 +237,9 @@ ROUTE_IMPL(RouteLogin, path, argp)
break; break;
default: default:
msg = "Route only accepts GET and POST.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_UNRECOGNIZED, NULL); response = MatrixErrorCreate(M_UNRECOGNIZED, msg);
break; break;
} }
@ -309,5 +247,8 @@ ROUTE_IMPL(RouteLogin, path, argp)
JsonFree(request); JsonFree(request);
ConfigUnlock(config); ConfigUnlock(config);
LoginRequestFree(&loginRequest);
LoginRequestUserIdentifierFree(&userIdentifier);
return response; return response;
} }

View file

@ -38,14 +38,17 @@ ROUTE_IMPL(RouteLogout, path, argp)
char *tokenstr; char *tokenstr;
char *msg;
Db *db = args->matrixArgs->db; Db *db = args->matrixArgs->db;
User *user; User *user;
if (HttpRequestMethodGet(args->context) != HTTP_POST) if (HttpRequestMethodGet(args->context) != HTTP_POST)
{ {
msg = "This route only accepts POST.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
return MatrixErrorCreate(M_UNRECOGNIZED, NULL); return MatrixErrorCreate(M_UNRECOGNIZED, msg);
} }
response = MatrixGetAccessToken(args->context, &tokenstr); response = MatrixGetAccessToken(args->context, &tokenstr);
@ -84,8 +87,9 @@ ROUTE_IMPL(RouteLogout, path, argp)
{ {
if (!UserDeleteToken(user, tokenstr)) if (!UserDeleteToken(user, tokenstr))
{ {
msg = "Internal server error: couldn't delete token.";
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR); HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
response = MatrixErrorCreate(M_UNKNOWN, NULL); response = MatrixErrorCreate(M_UNKNOWN, msg);
goto finish; goto finish;
} }

View file

@ -39,6 +39,8 @@ ROUTE_IMPL(RoutePrivileges, path, argp)
JsonValue *val; JsonValue *val;
int privileges; int privileges;
char *msg;
response = MatrixGetAccessToken(args->context, &token); response = MatrixGetAccessToken(args->context, &token);
if (response) if (response)
{ {
@ -55,8 +57,9 @@ ROUTE_IMPL(RoutePrivileges, path, argp)
if (!(UserGetPrivileges(user) & USER_GRANT_PRIVILEGES)) if (!(UserGetPrivileges(user) & USER_GRANT_PRIVILEGES))
{ {
msg = "User doesn't have the GRANT_PRIVILEGES privilege";
HttpResponseStatus(args->context, HTTP_FORBIDDEN); HttpResponseStatus(args->context, HTTP_FORBIDDEN);
response = MatrixErrorCreate(M_FORBIDDEN, NULL); response = MatrixErrorCreate(M_FORBIDDEN, msg);
goto finish; goto finish;
} }
@ -68,8 +71,9 @@ ROUTE_IMPL(RoutePrivileges, path, argp)
user = UserLock(args->matrixArgs->db, ArrayGet(path, 0)); user = UserLock(args->matrixArgs->db, ArrayGet(path, 0));
if (!user) if (!user)
{ {
msg = "Unknown user.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_INVALID_PARAM, NULL); response = MatrixErrorCreate(M_INVALID_PARAM, msg);
goto finish; goto finish;
} }
} }
@ -90,23 +94,24 @@ ROUTE_IMPL(RoutePrivileges, path, argp)
val = HashMapGet(request, "privileges"); val = HashMapGet(request, "privileges");
if (!val || JsonValueType(val) != JSON_ARRAY) if (!val || JsonValueType(val) != JSON_ARRAY)
{ {
msg = "'privileges' is unset or not an array.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_BAD_JSON, NULL); response = MatrixErrorCreate(M_BAD_JSON, msg);
break; break;
} }
switch (HttpRequestMethodGet(args->context)) switch (HttpRequestMethodGet(args->context))
{ {
case HTTP_POST: case HTTP_POST:
privileges = UserDecodePrivileges(val); privileges = UserDecodePrivileges(JsonValueAsArray(val));
break; break;
case HTTP_PUT: case HTTP_PUT:
privileges = UserGetPrivileges(user); privileges = UserGetPrivileges(user);
privileges |= UserDecodePrivileges(val); privileges |= UserDecodePrivileges(JsonValueAsArray(val));
break; break;
case HTTP_DELETE: case HTTP_DELETE:
privileges = UserGetPrivileges(user); privileges = UserGetPrivileges(user);
privileges &= ~UserDecodePrivileges(val); privileges &= ~UserDecodePrivileges(JsonValueAsArray(val));
break; break;
default: default:
/* Impossible */ /* Impossible */
@ -116,19 +121,21 @@ ROUTE_IMPL(RoutePrivileges, path, argp)
if (!UserSetPrivileges(user, privileges)) if (!UserSetPrivileges(user, privileges))
{ {
msg = "Internal server error: couldn't set privileges.";
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR); HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
response = MatrixErrorCreate(M_UNKNOWN, NULL); response = MatrixErrorCreate(M_UNKNOWN, msg);
break; break;
} }
/* Fall through */ /* Fall through */
case HTTP_GET: case HTTP_GET:
response = HashMapCreate(); response = HashMapCreate();
HashMapSet(response, "privileges", UserEncodePrivileges(UserGetPrivileges(user))); HashMapSet(response, "privileges", JsonValueArray(UserEncodePrivileges(UserGetPrivileges(user))));
break; break;
default: default:
msg = "Route only accepts POST, PUT, DELETE, and GET.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_UNRECOGNIZED, NULL); response = MatrixErrorCreate(M_UNRECOGNIZED, msg);
goto finish; goto finish;
break; break;
} }

View file

@ -37,6 +37,7 @@ ROUTE_IMPL(RouteProcControl, path, argp)
char *op = ArrayGet(path, 0); char *op = ArrayGet(path, 0);
HashMap *response; HashMap *response;
char *token; char *token;
char *msg;
User *user = NULL; User *user = NULL;
response = MatrixGetAccessToken(args->context, &token); response = MatrixGetAccessToken(args->context, &token);
@ -55,11 +56,13 @@ ROUTE_IMPL(RouteProcControl, path, argp)
if (!(UserGetPrivileges(user) & USER_PROC_CONTROL)) if (!(UserGetPrivileges(user) & USER_PROC_CONTROL))
{ {
msg = "User doesn't have PROC_CONTROL privilege.";
HttpResponseStatus(args->context, HTTP_FORBIDDEN); HttpResponseStatus(args->context, HTTP_FORBIDDEN);
response = MatrixErrorCreate(M_FORBIDDEN, NULL); response = MatrixErrorCreate(M_FORBIDDEN, msg);
goto finish; goto finish;
} }
msg = "Unknown operation.";
switch (HttpRequestMethodGet(args->context)) switch (HttpRequestMethodGet(args->context))
{ {
case HTTP_POST: case HTTP_POST:
@ -74,7 +77,7 @@ ROUTE_IMPL(RouteProcControl, path, argp)
else else
{ {
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_UNRECOGNIZED, NULL); response = MatrixErrorCreate(M_UNRECOGNIZED, msg);
goto finish; goto finish;
} }
break; break;
@ -106,12 +109,12 @@ ROUTE_IMPL(RouteProcControl, path, argp)
else else
{ {
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_UNRECOGNIZED, NULL); response = MatrixErrorCreate(M_UNRECOGNIZED, msg);
goto finish; goto finish;
} }
default: default:
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_UNRECOGNIZED, NULL); response = MatrixErrorCreate(M_UNRECOGNIZED, msg);
goto finish; goto finish;
break; break;
} }

View file

@ -45,6 +45,8 @@ ROUTE_IMPL(RouteRefresh, path, argp)
UserAccessToken *newAccessToken; UserAccessToken *newAccessToken;
char *deviceId; char *deviceId;
char *msg;
Db *db = args->matrixArgs->db; Db *db = args->matrixArgs->db;
User *user = NULL; User *user = NULL;
@ -55,8 +57,9 @@ ROUTE_IMPL(RouteRefresh, path, argp)
if (HttpRequestMethodGet(args->context) != HTTP_POST) if (HttpRequestMethodGet(args->context) != HTTP_POST)
{ {
msg = "This route only accepts POST.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
return MatrixErrorCreate(M_UNRECOGNIZED, NULL); return MatrixErrorCreate(M_UNRECOGNIZED, msg);
} }
request = JsonDecode(HttpServerStream(args->context)); request = JsonDecode(HttpServerStream(args->context));
@ -69,8 +72,9 @@ ROUTE_IMPL(RouteRefresh, path, argp)
val = HashMapGet(request, "refresh_token"); val = HashMapGet(request, "refresh_token");
if (!val || JsonValueType(val) != JSON_STRING) if (!val || JsonValueType(val) != JSON_STRING)
{ {
msg = "'refresh_token' is unset or not a string.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_BAD_JSON, NULL); response = MatrixErrorCreate(M_BAD_JSON, msg);
goto finish; goto finish;
} }

View file

@ -30,6 +30,8 @@
#include <Cytoplasm/Str.h> #include <Cytoplasm/Str.h>
#include <Cytoplasm/Memory.h> #include <Cytoplasm/Memory.h>
#include <Schema/Registration.h>
#include <User.h> #include <User.h>
#include <Uia.h> #include <Uia.h>
#include <RegToken.h> #include <RegToken.h>
@ -55,22 +57,15 @@ ROUTE_IMPL(RouteRegister, path, argp)
HashMap *request = NULL; HashMap *request = NULL;
HashMap *response = NULL; HashMap *response = NULL;
JsonValue *val; RegistrationRequest regReq;
char *kind; char *kind;
char *username = NULL;
char *password = NULL;
char *initialDeviceDisplayName = NULL;
int refreshToken = 0;
int inhibitLogin = 0;
char *deviceId = NULL;
char *fullUsername; char *fullUsername;
char *msg;
char *username;
Db *db = args->matrixArgs->db; Db *db = args->matrixArgs->db;
User *user = NULL; User *user = NULL;
Array *uiaFlows = NULL; Array *uiaFlows = NULL;
int uiaResult; int uiaResult;
@ -79,11 +74,22 @@ ROUTE_IMPL(RouteRegister, path, argp)
Config *config = ConfigLock(db); Config *config = ConfigLock(db);
regReq.username = NULL;
regReq.password = NULL;
regReq.device_id = NULL;
regReq.initial_device_display_name = NULL;
regReq.refresh_token = 0;
regReq.inhibit_login = 0;
if (!config) if (!config)
{ {
msg = "Internal server error while locking configuration.";
Log(LOG_ERR, "Registration endpoint failed to lock configuration."); Log(LOG_ERR, "Registration endpoint failed to lock configuration.");
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR); HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
return MatrixErrorCreate(M_UNKNOWN, NULL); return MatrixErrorCreate(M_UNKNOWN, msg);
} }
if (ArraySize(path) == 0) if (ArraySize(path) == 0)
@ -102,26 +108,23 @@ ROUTE_IMPL(RouteRegister, path, argp)
response = MatrixErrorCreate(M_NOT_JSON, NULL); response = MatrixErrorCreate(M_NOT_JSON, NULL);
goto end; goto end;
} }
if (!RegistrationRequestFromJson(request, &regReq, &msg))
val = HashMapGet(request, "username");
if (val)
{
if (JsonValueType(val) != JSON_STRING)
{ {
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_BAD_JSON, NULL); response = MatrixErrorCreate(M_NOT_JSON, msg);
goto finish; goto end;
} }
username = StrDuplicate(JsonValueAsString(val));
if (!UserValidate(username, config->serverName)) if (regReq.username)
{
if (!UserValidate(regReq.username, config->serverName))
{ {
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_INVALID_USERNAME, NULL); response = MatrixErrorCreate(M_INVALID_USERNAME, NULL);
goto finish; goto finish;
} }
if (UserExists(db, username)) if (UserExists(db, regReq.username))
{ {
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_USER_IN_USE, NULL); response = MatrixErrorCreate(M_USER_IN_USE, NULL);
@ -158,99 +161,44 @@ ROUTE_IMPL(RouteRegister, path, argp)
/* We don't support guest accounts yet */ /* We don't support guest accounts yet */
if (kind && !StrEquals(kind, "user")) if (kind && !StrEquals(kind, "user"))
{ {
msg = "Guest accounts are currently not supported";
HttpResponseStatus(args->context, HTTP_FORBIDDEN); HttpResponseStatus(args->context, HTTP_FORBIDDEN);
response = MatrixErrorCreate(M_INVALID_PARAM, NULL); response = MatrixErrorCreate(M_INVALID_PARAM, msg);
goto finish; goto finish;
} }
val = HashMapGet(request, "password"); if (!regReq.password)
if (!val)
{ {
msg = "'password' field is unset";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_MISSING_PARAM, NULL); response = MatrixErrorCreate(M_MISSING_PARAM, msg);
goto finish; goto finish;
} }
if (JsonValueType(val) != JSON_STRING) /* All of the other fields are optional, we don't have to check
{ * them. */
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_BAD_JSON, NULL);
goto finish;
}
password = StrDuplicate(JsonValueAsString(val)); user = UserCreate(db, regReq.username, regReq.password);
val = HashMapGet(request, "device_id");
if (val)
{
if (JsonValueType(val) != JSON_STRING)
{
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_BAD_JSON, NULL);
goto finish;
}
deviceId = StrDuplicate(JsonValueAsString(val));
}
val = HashMapGet(request, "inhibit_login");
if (val)
{
if (JsonValueType(val) != JSON_BOOLEAN)
{
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_BAD_JSON, NULL);
goto finish;
}
inhibitLogin = JsonValueAsBoolean(val);
}
val = HashMapGet(request, "initial_device_display_name");
if (val)
{
if (JsonValueType(val) != JSON_STRING)
{
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_BAD_JSON, NULL);
goto finish;
}
initialDeviceDisplayName = StrDuplicate(JsonValueAsString(val));
}
val = HashMapGet(request, "refresh_token");
if (val)
{
if (JsonValueType(val) != JSON_BOOLEAN)
{
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_BAD_JSON, NULL);
goto finish;
}
refreshToken = JsonValueAsBoolean(val);
}
user = UserCreate(db, username, password);
response = HashMapCreate(); response = HashMapCreate();
fullUsername = StrConcat(4, "@", UserGetName(user), ":", config->serverName); fullUsername = StrConcat(4,
"@", UserGetName(user), ":", config->serverName);
HashMapSet(response, "user_id", JsonValueString(fullUsername)); HashMapSet(response, "user_id", JsonValueString(fullUsername));
Free(fullUsername); Free(fullUsername);
HttpResponseStatus(args->context, HTTP_OK); HttpResponseStatus(args->context, HTTP_OK);
if (!inhibitLogin) if (!regReq.inhibit_login)
{ {
UserLoginInfo *loginInfo = UserLogin(user, password, deviceId, UserLoginInfo *loginInfo = UserLogin(user, regReq.password,
initialDeviceDisplayName, refreshToken); regReq.device_id, regReq.initial_device_display_name,
regReq.refresh_token);
HashMapSet(response, "access_token", HashMapSet(response, "access_token",
JsonValueString(loginInfo->accessToken->string)); JsonValueString(loginInfo->accessToken->string));
HashMapSet(response, "device_id", HashMapSet(response, "device_id",
JsonValueString(loginInfo->accessToken->deviceId)); JsonValueString(loginInfo->accessToken->deviceId));
if (refreshToken) if (regReq.refresh_token)
{ {
HashMapSet(response, "expires_in_ms", HashMapSet(response, "expires_in_ms",
JsonValueInteger(loginInfo->accessToken->lifetime)); JsonValueInteger(loginInfo->accessToken->lifetime));
@ -276,7 +224,7 @@ ROUTE_IMPL(RouteRegister, path, argp)
if (info) if (info)
{ {
UserSetPrivileges(user, info->grants); UserSetPrivileges(user, UserDecodePrivileges(info->grants));
RegTokenClose(info); RegTokenClose(info);
RegTokenFree(info); RegTokenFree(info);
} }
@ -294,10 +242,7 @@ ROUTE_IMPL(RouteRegister, path, argp)
UserUnlock(user); UserUnlock(user);
finish: finish:
UiaFlowsFree(uiaFlows); UiaFlowsFree(uiaFlows);
Free(username); RegistrationRequestFree(&regReq);
Free(password);
Free(deviceId);
Free(initialDeviceDisplayName);
JsonFree(request); JsonFree(request);
} }
else else
@ -310,8 +255,9 @@ finish:
if (!username) if (!username)
{ {
msg = "'username' path parameter is not set.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_MISSING_PARAM, NULL); response = MatrixErrorCreate(M_MISSING_PARAM, msg);
} }
else if (!UserValidate(username, config->serverName)) else if (!UserValidate(username, config->serverName))
{ {

View file

@ -26,19 +26,37 @@
#include <Cytoplasm/Str.h> #include <Cytoplasm/Str.h>
#include <Cytoplasm/Json.h> #include <Cytoplasm/Json.h>
#include <Schema/RequestToken.h>
ROUTE_IMPL(RouteRequestToken, path, argp) ROUTE_IMPL(RouteRequestToken, path, argp)
{ {
RouteArgs *args = argp; RouteArgs *args = argp;
char *type = ArrayGet(path, 0); char *type = ArrayGet(path, 0);
HashMap *request; HashMap *request;
HashMap *response; HashMap *response;
JsonValue *val;
char *str; char *msg;
RequestToken reqTok;
Int64 minusOne = Int64Neg(Int64Create(0, 1));
reqTok.client_secret = NULL;
reqTok.next_link = NULL;
reqTok.id_access_token = NULL;
reqTok.id_server = NULL;
reqTok.email = NULL;
reqTok.country = NULL;
reqTok.phone_number = NULL;
reqTok.send_attempt = minusOne;
if (HttpRequestMethodGet(args->context) != HTTP_POST) if (HttpRequestMethodGet(args->context) != HTTP_POST)
{ {
msg = "This route only accepts POST.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
return MatrixErrorCreate(M_UNRECOGNIZED, NULL); return MatrixErrorCreate(M_UNRECOGNIZED, msg);
} }
request = JsonDecode(HttpServerStream(args->context)); request = JsonDecode(HttpServerStream(args->context));
@ -48,87 +66,92 @@ ROUTE_IMPL(RouteRequestToken, path, argp)
return MatrixErrorCreate(M_NOT_JSON, NULL); return MatrixErrorCreate(M_NOT_JSON, NULL);
} }
val = HashMapGet(request, "client_secret"); if (!RequestTokenFromJson(request, &reqTok, &msg))
if (!val || JsonValueType(val) != JSON_STRING)
{ {
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_BAD_JSON, NULL); response = MatrixErrorCreate(M_BAD_JSON, msg);
goto finish; goto finish;
} }
str = JsonValueAsString(val); if (!reqTok.client_secret)
if (strlen(str) > 255 || StrBlank(str))
{ {
msg = "'client_secret' is not set";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_BAD_JSON, NULL); response = MatrixErrorCreate(M_BAD_JSON, msg);
goto finish; goto finish;
} }
val = HashMapGet(request, "send_attempt"); if (strlen(reqTok.client_secret) > 255 || StrBlank(reqTok.client_secret))
if (!val || JsonValueType(val) != JSON_INTEGER)
{ {
msg = "'client_secret' is blank or too long";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_BAD_JSON, NULL); response = MatrixErrorCreate(M_BAD_JSON, msg);
goto finish; goto finish;
} }
val = HashMapGet(request, "next_link"); if (Int64Eq(reqTok.send_attempt, minusOne))
if (val && JsonValueType(val) != JSON_STRING)
{ {
msg = "Invalid or inexistent 'send_attempt'";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_BAD_JSON, NULL); response = MatrixErrorCreate(M_BAD_JSON, msg);
goto finish; goto finish;
} }
val = HashMapGet(request, "id_access_token"); if (!reqTok.next_link)
if (val && JsonValueType(val) != JSON_STRING)
{ {
msg = "'next_link' is not set";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_BAD_JSON, NULL); response = MatrixErrorCreate(M_BAD_JSON, msg);
goto finish; goto finish;
} }
if (!reqTok.id_access_token)
val = HashMapGet(request, "id_server");
if (val && JsonValueType(val) != JSON_STRING)
{ {
msg = "'id_access_token' is not set";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_BAD_JSON, NULL); response = MatrixErrorCreate(M_BAD_JSON, msg);
goto finish;
}
if (!reqTok.id_server)
{
msg = "'id_server' is not set";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_BAD_JSON, msg);
goto finish; goto finish;
} }
if (StrEquals(type, "email")) if (StrEquals(type, "email"))
{ {
val = HashMapGet(request, "email"); if (!reqTok.email)
if (val && JsonValueType(val) != JSON_STRING)
{ {
msg = "Type is set to 'email' yet none was set";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_BAD_JSON, NULL); response = MatrixErrorCreate(M_BAD_JSON, msg);
goto finish; goto finish;
} }
} }
else if (StrEquals(type, "msisdn")) else if (StrEquals(type, "msisdn"))
{ {
val = HashMapGet(request, "country"); if (!reqTok.country)
if (val && JsonValueType(val) != JSON_STRING)
{ {
msg = "Type is set to 'msisdn' yet no country is set";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_BAD_JSON, NULL); response = MatrixErrorCreate(M_BAD_JSON, msg);
goto finish; goto finish;
} }
str = JsonValueAsString(val); if (strlen(reqTok.country) != 2)
if (strlen(str) != 2)
{ {
msg = "Invalid country tag, length must be 2";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_BAD_JSON, NULL); response = MatrixErrorCreate(M_BAD_JSON, msg);
goto finish; goto finish;
} }
val = HashMapGet(request, "phone_number"); if (!reqTok.phone_number)
if (val && JsonValueType(val) != JSON_STRING)
{ {
msg = "Type is set to 'msisdn' yet phone_number is unset";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_BAD_JSON, NULL); response = MatrixErrorCreate(M_BAD_JSON, msg);
goto finish; goto finish;
} }
} }
@ -145,5 +168,6 @@ ROUTE_IMPL(RouteRequestToken, path, argp)
finish: finish:
JsonFree(request); JsonFree(request);
RequestTokenFree(&reqTok);
return response; return response;
} }

View file

@ -37,6 +37,10 @@ ROUTE_IMPL(RouteRoomAliases, path, argp)
Db *db = args->matrixArgs->db; Db *db = args->matrixArgs->db;
DbRef *ref = NULL; DbRef *ref = NULL;
(void) roomId;
/* TODO: Placeholder; remove. */
goto finish;
finish: finish:
DbUnlock(db, ref); DbUnlock(db, ref);
JsonFree(request); JsonFree(request);

View file

@ -41,13 +41,15 @@ ROUTE_IMPL(RouteTokenValid, path, argp)
RegTokenInfo *info = NULL; RegTokenInfo *info = NULL;
char *tokenstr; char *tokenstr;
char *msg;
(void) path; (void) path;
if (HttpRequestMethodGet(args->context) != HTTP_GET) if (HttpRequestMethodGet(args->context) != HTTP_GET)
{ {
msg = "This route only accepts GET.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
return MatrixErrorCreate(M_UNRECOGNIZED, NULL); return MatrixErrorCreate(M_UNRECOGNIZED, msg);
} }
request = JsonDecode(HttpServerStream(args->context)); request = JsonDecode(HttpServerStream(args->context));

View file

@ -36,6 +36,8 @@ ROUTE_IMPL(RouteUiaFallback, path, argp)
char *authType = ArrayGet(path, 0); char *authType = ArrayGet(path, 0);
char *sessionId; char *sessionId;
char *msg;
if (!authType) if (!authType)
{ {
/* This should never happen */ /* This should never happen */
@ -56,9 +58,10 @@ ROUTE_IMPL(RouteUiaFallback, path, argp)
config = ConfigLock(args->matrixArgs->db); config = ConfigLock(args->matrixArgs->db);
if (!config) if (!config)
{ {
msg = "Internal server error: failed to lock configuration.";
Log(LOG_ERR, "UIA fallback failed to lock configuration."); Log(LOG_ERR, "UIA fallback failed to lock configuration.");
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR); HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
return MatrixErrorCreate(M_UNKNOWN, NULL); return MatrixErrorCreate(M_UNKNOWN, msg);
} }
request = JsonDecode(HttpServerStream(args->context)); request = JsonDecode(HttpServerStream(args->context));
@ -93,15 +96,17 @@ ROUTE_IMPL(RouteUiaFallback, path, argp)
} }
else if (HttpRequestMethodGet(args->context) != HTTP_GET) else if (HttpRequestMethodGet(args->context) != HTTP_GET)
{ {
msg = "Route only supports GET.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
return MatrixErrorCreate(M_UNRECOGNIZED, NULL); return MatrixErrorCreate(M_UNRECOGNIZED, msg);
} }
sessionId = HashMapGet(requestParams, "session"); sessionId = HashMapGet(requestParams, "session");
if (!sessionId) if (!sessionId)
{ {
msg = "'session' parameter is unset.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
return MatrixErrorCreate(M_MISSING_PARAM, NULL); return MatrixErrorCreate(M_MISSING_PARAM, msg);
} }
HttpResponseHeader(args->context, "Content-Type", "text/html"); HttpResponseHeader(args->context, "Content-Type", "text/html");

View file

@ -48,6 +48,8 @@ ROUTE_IMPL(RouteUserProfile, path, argp)
char *token = NULL; char *token = NULL;
char *value = NULL; char *value = NULL;
char *msg;
Config *config = ConfigLock(db); Config *config = ConfigLock(db);
if (!config) if (!config)
@ -63,15 +65,18 @@ ROUTE_IMPL(RouteUserProfile, path, argp)
userId = UserIdParse(username, serverName); userId = UserIdParse(username, serverName);
if (!userId) if (!userId)
{ {
msg = "Invalid user ID.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_INVALID_PARAM, NULL); response = MatrixErrorCreate(M_INVALID_PARAM, msg);
goto finish; goto finish;
} }
if (strcmp(userId->server, serverName)) if (strcmp(userId->server, serverName))
{ {
/* TODO: Implement lookup over federation. */ /* TODO: Implement lookup over federation. */
msg = "User profile endpoint currently doesn't support lookup over "
"federation.";
HttpResponseStatus(args->context, HTTP_FORBIDDEN); HttpResponseStatus(args->context, HTTP_FORBIDDEN);
response = MatrixErrorCreate(M_FORBIDDEN, NULL); response = MatrixErrorCreate(M_FORBIDDEN, msg);
goto finish; goto finish;
} }
@ -82,8 +87,9 @@ ROUTE_IMPL(RouteUserProfile, path, argp)
user = UserLock(db, userId->localpart); user = UserLock(db, userId->localpart);
if (!user) if (!user)
{ {
msg = "Couldn't lock user.";
HttpResponseStatus(args->context, HTTP_NOT_FOUND); HttpResponseStatus(args->context, HTTP_NOT_FOUND);
response = MatrixErrorCreate(M_NOT_FOUND, NULL); response = MatrixErrorCreate(M_NOT_FOUND, msg);
goto finish; goto finish;
} }
@ -138,7 +144,7 @@ ROUTE_IMPL(RouteUserProfile, path, argp)
StrEquals(entry, "avatar_url")) StrEquals(entry, "avatar_url"))
{ {
/* Check if user has privilege to do that action. */ /* Check if user has privilege to do that action. */
if (strcmp(userId->localpart, UserGetName(user)) == 0) if (StrEquals(userId->localpart, UserGetName(user)))
{ {
value = JsonValueAsString(HashMapGet(request, entry)); value = JsonValueAsString(HashMapGet(request, entry));
/* TODO: Make UserSetProfile notify other /* TODO: Make UserSetProfile notify other
@ -148,14 +154,16 @@ ROUTE_IMPL(RouteUserProfile, path, argp)
goto finish; goto finish;
} }
/* User is not allowed to carry-on the action */ /* User is not allowed to carry-on the action */
msg = "Cannot change another user's profile.";
HttpResponseStatus(args->context, HTTP_FORBIDDEN); HttpResponseStatus(args->context, HTTP_FORBIDDEN);
response = MatrixErrorCreate(M_FORBIDDEN, NULL); response = MatrixErrorCreate(M_FORBIDDEN, msg);
goto finish; goto finish;
} }
else else
{ {
msg = "Invalid property being changed.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_UNRECOGNIZED, NULL); response = MatrixErrorCreate(M_UNRECOGNIZED, msg);
goto finish; goto finish;
} }
} }
@ -166,8 +174,9 @@ ROUTE_IMPL(RouteUserProfile, path, argp)
goto finish; goto finish;
} }
default: default:
msg = "Route only accepts GET and PUT.";
HttpResponseStatus(args->context, HTTP_BAD_REQUEST); HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_UNKNOWN, NULL); response = MatrixErrorCreate(M_UNKNOWN, msg);
break; break;
} }
finish: finish:

View file

@ -35,7 +35,12 @@ ROUTE_IMPL(RouteVersions, path, argp)
(void) path; (void) path;
(void) argp; (void) argp;
ArrayAdd(versions, JsonValueString("v1.6")); #define DECLARE_SPEC_VERSION(x) ArrayAdd(versions, JsonValueString(x))
DECLARE_SPEC_VERSION("v1.7");
/* Declare additional spec version support here. */
#undef DECLARE_SPEC_VERSION
HashMapSet(response, "versions", JsonValueArray(versions)); HashMapSet(response, "versions", JsonValueArray(versions));
return response; return response;

View file

@ -37,11 +37,14 @@ ROUTE_IMPL(RouteWellKnown, path, argp)
Config *config = ConfigLock(args->matrixArgs->db); Config *config = ConfigLock(args->matrixArgs->db);
char *msg;
if (!config) if (!config)
{ {
Log(LOG_ERR, "Well-known endpoint failed to lock configuration."); Log(LOG_ERR, "Well-known endpoint failed to lock configuration.");
msg = "Internal server error: couldn't lock database.";
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR); HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
return MatrixErrorCreate(M_UNKNOWN, NULL); return MatrixErrorCreate(M_UNKNOWN, msg);
} }
if (StrEquals(ArrayGet(path, 0), "client")) if (StrEquals(ArrayGet(path, 0), "client"))

View file

@ -42,14 +42,16 @@ ROUTE_IMPL(RouteWhoami, path, argp)
char *token; char *token;
char *userID; char *userID;
char *deviceID; char *deviceID;
char *msg;
Config *config = ConfigLock(db); Config *config = ConfigLock(db);
if (!config) if (!config)
{ {
msg = "Internal server error: couldn't lock database.";
Log(LOG_ERR, "Who am I endpoint failed to lock configuration."); Log(LOG_ERR, "Who am I endpoint failed to lock configuration.");
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR); HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
return MatrixErrorCreate(M_UNKNOWN, NULL); return MatrixErrorCreate(M_UNKNOWN, msg);
} }
(void) path; (void) path;

View file

@ -33,12 +33,14 @@
static HashMap * static HashMap *
StateResolveV1(Array * states) StateResolveV1(Array * states)
{ {
(void) states;
return NULL; return NULL;
} }
static HashMap * static HashMap *
StateResolveV2(Array * states) StateResolveV2(Array * states)
{ {
(void) states;
return NULL; return NULL;
} }

View file

@ -222,6 +222,8 @@ UiaComplete(Array * flows, HttpServerContext * context, Db * db,
HashMap *dbJson; HashMap *dbJson;
int ret; int ret;
char *msg;
if (!flows) if (!flows)
{ {
return -1; return -1;
@ -242,8 +244,9 @@ UiaComplete(Array * flows, HttpServerContext * context, Db * db,
if (JsonValueType(val) != JSON_OBJECT) if (JsonValueType(val) != JSON_OBJECT)
{ {
msg = "'auth' is not an object.";
HttpResponseStatus(context, HTTP_BAD_REQUEST); HttpResponseStatus(context, HTTP_BAD_REQUEST);
*response = MatrixErrorCreate(M_BAD_JSON, NULL); *response = MatrixErrorCreate(M_BAD_JSON, msg);
return 0; return 0;
} }
@ -252,8 +255,9 @@ UiaComplete(Array * flows, HttpServerContext * context, Db * db,
if (!val || JsonValueType(val) != JSON_STRING) if (!val || JsonValueType(val) != JSON_STRING)
{ {
msg = "'auth->session' is unset or not a string.";
HttpResponseStatus(context, HTTP_BAD_REQUEST); HttpResponseStatus(context, HTTP_BAD_REQUEST);
*response = MatrixErrorCreate(M_BAD_JSON, NULL); *response = MatrixErrorCreate(M_BAD_JSON, msg);
return 0; return 0;
} }
@ -311,8 +315,9 @@ UiaComplete(Array * flows, HttpServerContext * context, Db * db,
if (!val || JsonValueType(val) != JSON_STRING) if (!val || JsonValueType(val) != JSON_STRING)
{ {
msg = "'auth->type' is unset or not a string.";
HttpResponseStatus(context, HTTP_BAD_REQUEST); HttpResponseStatus(context, HTTP_BAD_REQUEST);
*response = MatrixErrorCreate(M_BAD_JSON, NULL); *response = MatrixErrorCreate(M_BAD_JSON, msg);
ret = 0; ret = 0;
goto finish; goto finish;
} }

View file

@ -749,7 +749,7 @@ UserGetPrivileges(User * user)
return USER_NONE; return USER_NONE;
} }
return UserDecodePrivileges(HashMapGet(DbJson(user->ref), "privileges")); return UserDecodePrivileges(JsonValueAsArray(HashMapGet(DbJson(user->ref), "privileges")));
} }
int int
@ -768,7 +768,7 @@ UserSetPrivileges(User * user, int privileges)
return 1; return 1;
} }
val = UserEncodePrivileges(privileges); val = JsonValueArray(UserEncodePrivileges(privileges));
if (!val) if (!val)
{ {
return 0; return 0;
@ -779,24 +779,20 @@ UserSetPrivileges(User * user, int privileges)
} }
int int
UserDecodePrivileges(JsonValue * val) UserDecodePrivileges(Array * arr)
{ {
int privileges = USER_NONE; int privileges = USER_NONE;
size_t i; size_t i;
Array *arr;
if (!val) if (!arr)
{ {
goto finish; goto finish;
} }
if (JsonValueType(val) == JSON_ARRAY)
{
arr = JsonValueAsArray(val);
for (i = 0; i < ArraySize(arr); i++) for (i = 0; i < ArraySize(arr); i++)
{ {
val = ArrayGet(arr, i); JsonValue *val = ArrayGet(arr, i);
if (!val || JsonValueType(val) != JSON_STRING) if (!val || JsonValueType(val) != JSON_STRING)
{ {
continue; continue;
@ -804,7 +800,6 @@ UserDecodePrivileges(JsonValue * val)
privileges |= UserDecodePrivilege(JsonValueAsString(val)); privileges |= UserDecodePrivilege(JsonValueAsString(val));
} }
}
finish: finish:
return privileges; return privileges;
@ -851,7 +846,7 @@ UserDecodePrivilege(const char *p)
} }
} }
JsonValue * Array *
UserEncodePrivileges(int privileges) UserEncodePrivileges(int privileges)
{ {
Array *arr = ArrayCreate(); Array *arr = ArrayCreate();
@ -883,7 +878,7 @@ UserEncodePrivileges(int privileges)
#undef A #undef A
finish: finish:
return JsonValueArray(arr); return arr;
} }
UserId * UserId *

View file

@ -42,54 +42,7 @@
#include <Cytoplasm/Db.h> #include <Cytoplasm/Db.h>
#include <Cytoplasm/Int64.h> #include <Cytoplasm/Int64.h>
/** #include <Schema/RegToken.h>
* This structure describes a registration token that is in the
* database.
*/
typedef struct RegTokenInfo
{
Db *db;
DbRef *ref;
/*
* The token itself.
*/
char *name;
/*
* Who created this token. Note that this can be NULL if the
* token was created by Telodendria itself.
*/
char *owner;
/*
* How many times the token was used.
*/
Int64 used;
/*
* How many uses are allowed.
*/
Int64 uses;
/*
* Timestamp when this token was created.
*/
UInt64 created;
/*
* Timestamp when this token expires, or 0 if it does not
* expire.
*/
UInt64 expires;
/*
* A bit field describing the privileges this token grants. See
* the User API documentation for the privileges supported.
*/
int grants;
} RegTokenInfo;
/** /**
* ``Use'' the specified registration token by increasing the used * ``Use'' the specified registration token by increasing the used
@ -132,7 +85,6 @@ RegTokenCreate(Db *, char *, char *, UInt64, Int64, int);
* .Fn RegTokenClose . * .Fn RegTokenClose .
*/ */
extern void RegTokenFree(RegTokenInfo *); extern void RegTokenFree(RegTokenInfo *);
/** /**
* Return a boolean value indicating whether or not the specified token * Return a boolean value indicating whether or not the specified token
* is valid. A registration token is only valid if it has not expired * is valid. A registration token is only valid if it has not expired

View file

@ -105,6 +105,8 @@ ROUTE(RouteRoomAliases);
ROUTE(RouteAdminDeactivate); ROUTE(RouteAdminDeactivate);
ROUTE(RouteAdminTokens);
#undef ROUTE #undef ROUTE
#endif #endif

View file

@ -286,13 +286,13 @@ extern int UserSetPrivileges(User *, int);
* Decode the JSON that represents the user privileges into a packed * Decode the JSON that represents the user privileges into a packed
* bit field for simple manipulation. * bit field for simple manipulation.
*/ */
extern int UserDecodePrivileges(JsonValue *); extern int UserDecodePrivileges(Array *);
/** /**
* Encode the packed bit field that represents user privileges as a * Encode the packed bit field that represents user privileges as a
* JSON value. * JSON value.
*/ */
extern JsonValue *UserEncodePrivileges(int); extern Array *UserEncodePrivileges(int);
/** /**
* Convert a string privilege into its bit in the bit field. This is * Convert a string privilege into its bit in the bit field. This is