Merge branch 'migrate-to-matrix-sdk'
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
5
matrixgw_backend/.gitignore
vendored
Normal file
5
matrixgw_backend/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
storage
|
||||
app_storage
|
||||
.idea
|
||||
target
|
||||
static
|
||||
6118
matrixgw_backend/Cargo.lock
generated
Normal file
6118
matrixgw_backend/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
38
matrixgw_backend/Cargo.toml
Normal file
38
matrixgw_backend/Cargo.toml
Normal file
@@ -0,0 +1,38 @@
|
||||
[package]
|
||||
name = "matrixgw_backend"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
env_logger = "0.11.8"
|
||||
log = "0.4.29"
|
||||
clap = { version = "4.5.53", features = ["derive", "env"] }
|
||||
lazy_static = "1.5.0"
|
||||
anyhow = "1.0.100"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
tokio = { version = "1.48.0", features = ["full"] }
|
||||
actix-web = "4.12.1"
|
||||
actix-session = { version = "0.11.0", features = ["redis-session"] }
|
||||
actix-remote-ip = "0.1.0"
|
||||
actix-cors = "0.7.1"
|
||||
light-openid = "1.0.4"
|
||||
bytes = "1.11.0"
|
||||
sha2 = "0.10.9"
|
||||
base16ct = { version = "0.3.0", features = ["alloc"] }
|
||||
futures-util = "0.3.31"
|
||||
jwt-simple = { version = "0.12.13", default-features = false, features = ["pure-rust"] }
|
||||
thiserror = "2.0.17"
|
||||
uuid = { version = "1.19.0", features = ["v4", "serde"] }
|
||||
ipnet = { version = "2.11.0", features = ["serde"] }
|
||||
rand = "0.9.2"
|
||||
hex = "0.4.3"
|
||||
mailchecker = "6.0.19"
|
||||
matrix-sdk = { version = "0.14.0" }
|
||||
url = "2.5.7"
|
||||
ractor = "0.15.9"
|
||||
serde_json = "1.0.145"
|
||||
lazy-regex = "3.4.2"
|
||||
actix-ws = "0.3.0"
|
||||
infer = "0.19.0"
|
||||
rust-embed = "8.9.0"
|
||||
mime_guess = "2.0.5"
|
||||
10
matrixgw_backend/Dockerfile
Normal file
10
matrixgw_backend/Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y libssl3 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY matrix_gateway /usr/local/bin/matrix_gateway
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/matrix_gateway"]
|
||||
|
||||
2
matrixgw_backend/README.md
Normal file
2
matrixgw_backend/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# Matrix Gateway backend
|
||||
Backend component, written in Rust using Actix.
|
||||
102
matrixgw_backend/docker-compose.yml
Normal file
102
matrixgw_backend/docker-compose.yml
Normal file
@@ -0,0 +1,102 @@
|
||||
services:
|
||||
mas:
|
||||
image: ghcr.io/element-hq/matrix-authentication-service:main
|
||||
user: "1000"
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- masdb
|
||||
volumes:
|
||||
- ./docker/mas:/config:ro
|
||||
command: server -c /config/config.yaml
|
||||
ports:
|
||||
- "8778:8778/tcp"
|
||||
|
||||
mas_create_admin1:
|
||||
image: ghcr.io/element-hq/matrix-authentication-service:main
|
||||
user: "1000"
|
||||
restart: no
|
||||
profiles: ["create-accounts"]
|
||||
depends_on:
|
||||
- mas
|
||||
volumes:
|
||||
- ./docker/mas:/config:ro
|
||||
command: |
|
||||
manage register-user -c /config/config.yaml -y --ignore-password-complexity
|
||||
-p admin1 -e admin1@admin1.local --admin -d "Admin One" admin1
|
||||
|
||||
mas_create_user1:
|
||||
image: ghcr.io/element-hq/matrix-authentication-service:main
|
||||
user: "1000"
|
||||
restart: no
|
||||
profiles: ["create-accounts"]
|
||||
depends_on:
|
||||
- mas
|
||||
volumes:
|
||||
- ./docker/mas:/config:ro
|
||||
command: |
|
||||
manage register-user -c /config/config.yaml -y --ignore-password-complexity
|
||||
-p user1 -e user1@user1.local -d "User One" user1
|
||||
|
||||
synapse:
|
||||
image: docker.io/matrixdotorg/synapse:latest
|
||||
user: "1000"
|
||||
# Since synapse does not retry to connect to the database, restart upon
|
||||
# failure
|
||||
restart: unless-stopped
|
||||
# See the readme for a full documentation of the environment settings
|
||||
# NOTE: You must edit homeserver.yaml to use postgres, it defaults to sqlite
|
||||
environment:
|
||||
- SYNAPSE_CONFIG_PATH=/config/homeserver.yaml
|
||||
volumes:
|
||||
# You may either store all the files in a local folder
|
||||
- ./storage/synapse:/data
|
||||
- ./docker/synapse:/config:ro
|
||||
# .. or you may split this between different storage points
|
||||
# - ./files:/data
|
||||
# - /path/to/ssd:/data/uploads
|
||||
# - /path/to/large_hdd:/data/media
|
||||
# In order to expose Synapse, remove one of the following, you might for
|
||||
# instance expose the TLS port directly:
|
||||
ports:
|
||||
- "8448:8448/tcp"
|
||||
|
||||
masdb:
|
||||
image: docker.io/postgres:18-alpine
|
||||
user: "1000"
|
||||
environment:
|
||||
- POSTGRES_DB=masdb
|
||||
- POSTGRES_USER=masdb
|
||||
- POSTGRES_PASSWORD=changeme
|
||||
# ensure the database gets created correctly
|
||||
# https://element-hq.github.io/synapse/latest/postgres.html#set-up-database
|
||||
- POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C
|
||||
- PGDATA=/data
|
||||
volumes:
|
||||
# You may store the database tables in a local folder..
|
||||
- ./storage/maspostgres:/data
|
||||
# .. or store them on some high performance storage for better results
|
||||
# - /path/to/ssd/storage:/var/lib/postgresql/data
|
||||
|
||||
element:
|
||||
image: docker.io/vectorim/element-web
|
||||
ports:
|
||||
- "8080:80/tcp"
|
||||
volumes:
|
||||
- ./docker/element/config.json:/app/config.json:ro
|
||||
|
||||
oidc:
|
||||
image: dexidp/dex
|
||||
ports:
|
||||
- "9001:9001"
|
||||
volumes:
|
||||
- ./docker/dex:/conf:ro
|
||||
command: [ "dex", "serve", "/conf/dex.config.yaml" ]
|
||||
|
||||
redis:
|
||||
image: redis:alpine
|
||||
command: redis-server --requirepass ${REDIS_PASS:-secretredis}
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- ./storage/redis-data:/data
|
||||
- ./storage/redis-conf:/usr/local/etc/redis/redis.conf
|
||||
26
matrixgw_backend/docker/dex/dex.config.yaml
Normal file
26
matrixgw_backend/docker/dex/dex.config.yaml
Normal file
@@ -0,0 +1,26 @@
|
||||
issuer: http://127.0.0.1:9001/dex
|
||||
|
||||
storage:
|
||||
type: memory
|
||||
|
||||
web:
|
||||
http: 0.0.0.0:9001
|
||||
|
||||
oauth2:
|
||||
# Automate some clicking
|
||||
# Note: this might actually make some tests pass that otherwise wouldn't.
|
||||
skipApprovalScreen: false
|
||||
|
||||
connectors:
|
||||
# Note: this might actually make some tests pass that otherwise wouldn't.
|
||||
- type: mockCallback
|
||||
id: mock
|
||||
name: Example
|
||||
|
||||
# Basic OP test suite requires two clients.
|
||||
staticClients:
|
||||
- id: foo
|
||||
secret: bar
|
||||
redirectURIs:
|
||||
- http://localhost:5173/oidc_cb
|
||||
name: Project
|
||||
49
matrixgw_backend/docker/element/config.json
Normal file
49
matrixgw_backend/docker/element/config.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"default_server_config": {
|
||||
"m.homeserver": {
|
||||
"base_url": "http://localhost:8448",
|
||||
"server_name": "devserver"
|
||||
},
|
||||
"m.identity_server": {
|
||||
"base_url": "https://vector.im"
|
||||
}
|
||||
},
|
||||
"disable_custom_urls": false,
|
||||
"disable_guests": false,
|
||||
"disable_login_language_selector": false,
|
||||
"disable_3pid_login": false,
|
||||
"brand": "Element",
|
||||
"integrations_ui_url": "https://scalar.vector.im/",
|
||||
"integrations_rest_url": "https://scalar.vector.im/api",
|
||||
"integrations_widgets_urls": [
|
||||
"https://scalar.vector.im/_matrix/integrations/v1",
|
||||
"https://scalar.vector.im/api",
|
||||
"https://scalar-staging.vector.im/_matrix/integrations/v1",
|
||||
"https://scalar-staging.vector.im/api",
|
||||
"https://scalar-staging.riot.im/scalar/api"
|
||||
],
|
||||
"default_country_code": "GB",
|
||||
"show_labs_settings": false,
|
||||
"features": {},
|
||||
"default_federate": true,
|
||||
"default_theme": "light",
|
||||
"room_directory": {
|
||||
"servers": ["matrix.org"]
|
||||
},
|
||||
"enable_presence_by_hs_url": {
|
||||
"https://matrix.org": false,
|
||||
"https://matrix-client.matrix.org": false
|
||||
},
|
||||
"setting_defaults": {
|
||||
"breadcrumbs": true
|
||||
},
|
||||
"jitsi": {
|
||||
"preferred_domain": "meet.element.io"
|
||||
},
|
||||
"element_call": {
|
||||
"url": "https://call.element.io",
|
||||
"participant_limit": 8,
|
||||
"brand": "Element Call"
|
||||
},
|
||||
"map_style_url": "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx"
|
||||
}
|
||||
113
matrixgw_backend/docker/mas/config.yaml
Normal file
113
matrixgw_backend/docker/mas/config.yaml
Normal file
@@ -0,0 +1,113 @@
|
||||
http:
|
||||
listeners:
|
||||
- name: web
|
||||
resources:
|
||||
- name: discovery
|
||||
- name: human
|
||||
- name: oauth
|
||||
- name: compat
|
||||
- name: graphql
|
||||
- name: assets
|
||||
binds:
|
||||
- address: '[::]:8778'
|
||||
proxy_protocol: false
|
||||
- name: internal
|
||||
resources:
|
||||
- name: health
|
||||
binds:
|
||||
- host: localhost
|
||||
port: 8081
|
||||
proxy_protocol: false
|
||||
trusted_proxies:
|
||||
- 192.168.0.0/16
|
||||
- 172.16.0.0/12
|
||||
- 10.0.0.0/10
|
||||
- 127.0.0.1/8
|
||||
- fd00::/8
|
||||
- ::1/128
|
||||
public_base: http://localhost:8778/
|
||||
issuer: http://localhost:8778/
|
||||
database:
|
||||
uri: postgresql://masdb:changeme@masdb/masdb
|
||||
max_connections: 10
|
||||
min_connections: 0
|
||||
connect_timeout: 30
|
||||
idle_timeout: 600
|
||||
max_lifetime: 1800
|
||||
email:
|
||||
from: '"Authentication Service" <root@localhost>'
|
||||
reply_to: '"Authentication Service" <root@localhost>'
|
||||
transport: blackhole
|
||||
secrets:
|
||||
encryption: 12de9ad7bc2bacfa2ab9b1e3f7f1b3feb802195c8ebe66a8293cdb27f00be471
|
||||
keys:
|
||||
- kid: Bj2PICQ7mf
|
||||
key: |
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEogIBAAKCAQEAsCYCrrCJA7IuGbTYzP5yZN74QszbzudBUCX6MyN/+36HO2r6
|
||||
xL8x1PRJ+Klx9Y90J9pWuo+cIuEmFLqO+Yfblo9fSQgZVvkWAFpO6Xh8J4z9qg49
|
||||
M8xm0Ct8EnRDZDCEOBnwoDaAB9RTbpJGa1RPVCiamfi+xU+j47Zl4Er5jvLm81O7
|
||||
DSlH9eK8Eih8AxuKTkAbKE1zyXquImE26Mj2dmMRfjDrWV/I8oqE3WFViAKR12Av
|
||||
zw6TUyduiz8nK9pONCF3NIcQvBdHntBz1HlDXv6i0fRvlGIhjNL5LBgo6XQ3rNM1
|
||||
bW2KYOw/iFP0YbfD4/xRjkBPvK2coQ8aRzK2VwIDAQABAoH/G4XU5Xav8ePlUB7x
|
||||
wRYAycINCGL59Vos2lkUvujNFn6uopoUlKlLH/sLk87l/3hqrc9vvbayrsB/Mr3z
|
||||
mQmhReUg/khFrVE+Hs/9hH1O6N8ew3N2HKHTbrNcr4V7AiySfDGRZ3ccihyi7KPu
|
||||
XNbPjlbJ0UUMicfn06ysPl94nt0So0UAmXg+c7sDDqyzh3cY8emedYZ5FCljo/jA
|
||||
F8k40rs7CywLJYMJB9O1vtomgt1xkDRO4F8UrZrriMIcYn0iFKe7i4AH8D6nkgNu
|
||||
/v9Z43Leu8yRKrUvbpH3NaX8DlUSFWAXKpwUWr4sAQgWcLkVgjAXG1v9jCE97qW2
|
||||
f0nBAoGBAOaKrnY5rWeZ74dERnPhSCsYiqRMneQAh7eJR+Er+xu1yF/bxwkhq2tK
|
||||
/txheTK448DqhQRtr095t/v7TMZcPl3bSmybT1CQg/wiMJsgDMZqlC9tofvcq6uz
|
||||
xP8vxMFHd0YSMSP693dkny4MzNY6LuoVWDLT+HxKPJyzGs1alruzAoGBAMOZp5J2
|
||||
3ODcHQlcsGBtj1yVpQ4UXMvrSZF2ygiGK9bagL/f1iAtwACVOh5rgmbiOLSVgmR2
|
||||
n4nupTgSAXMYkjmAmDyEh0PDaRl4WWvYEKp8GMvTPVPvjc6N0dT+y8Mf9bu+LcEt
|
||||
+uZqPOZNbO5Vi+UgGeM9zZpxq/K7dpJmM/jNAoGBALsYHRGxKTsEwFEkZZCxaWIg
|
||||
HpPL4e8hRwL6FC13BeitFBpHQDX27yi5yi+Lo1I4ngz3xk+bvERhYaDLhrkML0j4
|
||||
KGQPfsTBI3vBO3UJA5Ua9XuwG19M7L0BvYPjfmfk2bUyGlM63w4zyMMUfD/3JA+w
|
||||
ls1ZHTWxAZOh/sRdGirlAoGAX16B1+XgmDp6ZeAtlzaUGd5U1eKTxFF6U1SJ+VIB
|
||||
+gYblHI84v+riB06cy6ULDnM0C+9neJAs24KXKZa0pV+Zk8O6yLrGN0kV2jYoL5+
|
||||
kcFkDa13T3+TssxvLNz22LKyi9GUWYZjuQi/nMLPg/1t8k+Oj7/Iia822WkRzRvL
|
||||
51kCgYEAwrN5Us8LR+fThm3C0vhvwv2wap6ccw0qq5+FTN+igAZAmmvKKvhow2Vi
|
||||
LnPKBkc7QvxvQSNoXkdUo4qs3zOQ7DGvJLqSG9pwxFW5X1+78pNEm5OWe8AlT1uZ
|
||||
Jz8Z1/Ae7fr/fFaucW9LkWjcuoPwPLiZ3b7ZQ6phs8qzoL+FpBI=
|
||||
-----END RSA PRIVATE KEY-----
|
||||
- kid: HcRvLHat12
|
||||
key: |
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIOCCFSnkfz1ksln6kus8enQstBTu0q62IGJVzuX0WiXPoAoGCCqGSM49
|
||||
AwEHoUQDQgAEVWPLbvSdxquLAjU3zJLcCWdaxr6QK1tPVbV1IS+87QUMv/zKiCMa
|
||||
fNpwgBXwU7dF0gY507R2yY9pcdTmRtnRug==
|
||||
-----END EC PRIVATE KEY-----
|
||||
- kid: YjMITk5VSn
|
||||
key: |
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MIGkAgEBBDCoPSjaN7qqnPz+vdzHeIy8RZCCtFOqLTkvylM1gz6xOGaVsS63VJw9
|
||||
Td9BtpolZ0egBwYFK4EEACKhZANiAAT8tH88HYBHNiQTSqZzlxElSuSDC0+Xn0O9
|
||||
ukj0xTTVBp8rUM9lCJQAlB8PjS2XK/n0YvYdzysQb3AYqszJa45/rOGvSar30YNE
|
||||
gwpJvu36xNIKZT+nHalNwg069FdjNBc=
|
||||
-----END EC PRIVATE KEY-----
|
||||
- kid: NvFzzeMRU3
|
||||
key: |
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHQCAQEEILJEmFPDGFZoBVBQf1P6h4YfasYsFiu8a6FrFxiJvKXPoAcGBSuBBAAK
|
||||
oUQDQgAE4NY5H3+D8r9GNOhrpbUn2dvLZIzi4A+SiwfqvtvPEmZkW+KDbd2tzKmx
|
||||
maydZBn52QWedVY65snGAEoh9mV1TQ==
|
||||
-----END EC PRIVATE KEY-----
|
||||
passwords:
|
||||
enabled: true
|
||||
schemes:
|
||||
- version: 1
|
||||
algorithm: argon2id
|
||||
minimum_complexity: 0
|
||||
account:
|
||||
password_registration_enabled: true
|
||||
password_registration_email_required: false
|
||||
matrix:
|
||||
kind: synapse
|
||||
homeserver: localhost
|
||||
secret: IhKoLn6jWf1qRRZWvqgaKuIdwD6H0Mvx
|
||||
endpoint: http://synapse:8448/
|
||||
|
||||
policy:
|
||||
data:
|
||||
client_registration:
|
||||
allow_insecure_uris: true
|
||||
10
matrixgw_backend/docker/matrixgw_backend/Dockerfile
Normal file
10
matrixgw_backend/docker/matrixgw_backend/Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y libcurl4 libsqlite3-0 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
COPY matrixgw_backend /usr/local/bin/matrixgw_backend
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/matrixgw_backend"]
|
||||
41
matrixgw_backend/docker/synapse/homeserver.yaml
Normal file
41
matrixgw_backend/docker/synapse/homeserver.yaml
Normal file
@@ -0,0 +1,41 @@
|
||||
# Configuration file for Synapse.
|
||||
#
|
||||
# This is a YAML file: see [1] for a quick introduction. Note in particular
|
||||
# that *indentation is important*: all the elements of a list or dictionary
|
||||
# should have the same indentation.
|
||||
#
|
||||
# [1] https://docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html
|
||||
#
|
||||
# For more information on how to configure Synapse, including a complete accounting of
|
||||
# each option, go to docs/usage/configuration/config_documentation.md or
|
||||
# https://element-hq.github.io/synapse/latest/usage/configuration/config_documentation.html
|
||||
server_name: "localhost"
|
||||
pid_file: /data/homeserver.pid
|
||||
listeners:
|
||||
- port: 8448
|
||||
tls: false
|
||||
type: http
|
||||
x_forwarded: true
|
||||
resources:
|
||||
- names: [client, federation]
|
||||
compress: false
|
||||
database:
|
||||
name: sqlite3
|
||||
args:
|
||||
database: /data/homeserver.db
|
||||
log_config: "/config/localhost.log.config"
|
||||
media_store_path: /data/media_store
|
||||
registration_shared_secret: "+oJd9zgvkQpXN-tt;95Wy,AFAdRH+FSTg&LxUXh6ZSvwMJHT;h"
|
||||
report_stats: false
|
||||
macaroon_secret_key: "d@ck1QkQLxlRg^aB#c#oZeII.oxOS6E2DX;YobP^Vm#iB5pQpd"
|
||||
form_secret: "P.uleBJUYc6AM.UOrFF1q7OKH2N5T*Ae2;fGh46;vIHLIQ#JBP"
|
||||
signing_key_path: "/config/localhost.signing.key"
|
||||
trusted_key_servers:
|
||||
- server_name: "matrix.org"
|
||||
# vim:ft=yaml
|
||||
matrix_authentication_service:
|
||||
enabled: true
|
||||
endpoint: http://mas:8778/
|
||||
secret: "IhKoLn6jWf1qRRZWvqgaKuIdwD6H0Mvx"
|
||||
# Alternatively, using a file:
|
||||
#secret_file: /path/to/secret.txt
|
||||
39
matrixgw_backend/docker/synapse/localhost.log.config
Normal file
39
matrixgw_backend/docker/synapse/localhost.log.config
Normal file
@@ -0,0 +1,39 @@
|
||||
version: 1
|
||||
|
||||
formatters:
|
||||
precise:
|
||||
|
||||
format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s'
|
||||
|
||||
|
||||
handlers:
|
||||
|
||||
|
||||
console:
|
||||
class: logging.StreamHandler
|
||||
formatter: precise
|
||||
|
||||
loggers:
|
||||
# This is just here so we can leave `loggers` in the config regardless of whether
|
||||
# we configure other loggers below (avoid empty yaml dict error).
|
||||
_placeholder:
|
||||
level: "INFO"
|
||||
|
||||
|
||||
|
||||
synapse.storage.SQL:
|
||||
# beware: increasing this to DEBUG will make synapse log sensitive
|
||||
# information such as access tokens.
|
||||
level: INFO
|
||||
|
||||
|
||||
|
||||
|
||||
root:
|
||||
level: INFO
|
||||
|
||||
|
||||
handlers: [console]
|
||||
|
||||
|
||||
disable_existing_loggers: false
|
||||
1
matrixgw_backend/docker/synapse/localhost.signing.key
Normal file
1
matrixgw_backend/docker/synapse/localhost.signing.key
Normal file
@@ -0,0 +1 @@
|
||||
ed25519 a_HEcG Q2iG1Yy5WTiZ/VIy+zHPyHCRUpqyE3qrVttGULrVQK4
|
||||
92
matrixgw_backend/examples/api_curl.rs
Normal file
92
matrixgw_backend/examples/api_curl.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
use clap::Parser;
|
||||
use jwt_simple::algorithms::HS256Key;
|
||||
use jwt_simple::prelude::{Clock, Duration, JWTClaims, MACLike};
|
||||
use matrixgw_backend::constants;
|
||||
use matrixgw_backend::extractors::auth_extractor::{MatrixJWTKID, TokenClaims};
|
||||
use matrixgw_backend::users::{APITokenID, UserEmail};
|
||||
use matrixgw_backend::utils::rand_utils::rand_string;
|
||||
use std::ops::Add;
|
||||
use std::os::unix::prelude::CommandExt;
|
||||
use std::process::Command;
|
||||
use std::str::FromStr;
|
||||
|
||||
/// cURL wrapper to query MatrixGW
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about, long_about = None)]
|
||||
struct Args {
|
||||
/// URL of Matrix GW
|
||||
#[arg(short('U'), long, env, default_value = "http://localhost:8000")]
|
||||
matrix_gw_url: String,
|
||||
|
||||
/// Token ID
|
||||
#[arg(short('i'), long, env)]
|
||||
token_id: String,
|
||||
|
||||
/// User email
|
||||
#[arg(short('u'), long, env)]
|
||||
user_mail: String,
|
||||
|
||||
/// Token secret
|
||||
#[arg(short('t'), long, env)]
|
||||
token_secret: String,
|
||||
|
||||
/// Request verb
|
||||
#[arg(short('X'), long, default_value = "GET")]
|
||||
method: String,
|
||||
|
||||
/// Payload SHA256 digest
|
||||
#[arg(short('D'), long)]
|
||||
payload_digest: Option<String>,
|
||||
|
||||
/// Request URI
|
||||
uri: String,
|
||||
|
||||
/// Command line arguments to pass to cURL
|
||||
#[clap(trailing_var_arg = true, allow_hyphen_values = true)]
|
||||
run: Vec<String>,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args: Args = Args::parse();
|
||||
|
||||
let full_url = format!("{}{}", args.matrix_gw_url, args.uri);
|
||||
log::debug!("Full URL: {full_url}");
|
||||
|
||||
let key = HS256Key::from_bytes(args.token_secret.as_bytes());
|
||||
|
||||
let claims = JWTClaims::<TokenClaims> {
|
||||
issued_at: Some(Clock::now_since_epoch()),
|
||||
expires_at: Some(Clock::now_since_epoch().add(Duration::from_mins(15))),
|
||||
invalid_before: None,
|
||||
issuer: None,
|
||||
subject: None,
|
||||
audiences: None,
|
||||
jwt_id: None,
|
||||
nonce: Some(rand_string(10)),
|
||||
custom: TokenClaims {
|
||||
method: args.method.to_string(),
|
||||
uri: args.uri,
|
||||
payload_sha256: args.payload_digest.clone(),
|
||||
},
|
||||
};
|
||||
|
||||
let jwt = key
|
||||
.with_key_id(
|
||||
&MatrixJWTKID {
|
||||
user_email: UserEmail(args.user_mail),
|
||||
id: APITokenID::from_str(args.token_id.as_str())
|
||||
.expect("Failed to decode token ID!"),
|
||||
}
|
||||
.to_string(),
|
||||
)
|
||||
.authenticate(claims)
|
||||
.expect("Failed to sign JWT!");
|
||||
|
||||
let _ = Command::new("curl")
|
||||
.args(["-X", &args.method])
|
||||
.args(["-H", &format!("{}: {jwt}", constants::API_AUTH_HEADER)])
|
||||
.args(args.run)
|
||||
.arg(full_url)
|
||||
.exec();
|
||||
panic!("Failed to run curl!")
|
||||
}
|
||||
248
matrixgw_backend/src/app_config.rs
Normal file
248
matrixgw_backend/src/app_config.rs
Normal file
@@ -0,0 +1,248 @@
|
||||
use crate::users::{APITokenID, UserEmail};
|
||||
use crate::utils::crypt_utils::sha256str;
|
||||
use clap::Parser;
|
||||
use matrix_sdk::authentication::oauth::registration::{
|
||||
ApplicationType, ClientMetadata, Localized, OAuthGrantType,
|
||||
};
|
||||
use std::path::{Path, PathBuf};
|
||||
use url::Url;
|
||||
|
||||
/// Matrix gateway backend API
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
#[clap(author, version, about, long_about = None)]
|
||||
pub struct AppConfig {
|
||||
/// Listen address
|
||||
#[clap(short, long, env, default_value = "0.0.0.0:8000")]
|
||||
pub listen_address: String,
|
||||
|
||||
/// Website origin
|
||||
#[clap(short, long, env, default_value = "http://localhost:5173")]
|
||||
pub website_origin: String,
|
||||
|
||||
/// Proxy IP, might end with a star "*"
|
||||
#[clap(short, long, env)]
|
||||
pub proxy_ip: Option<String>,
|
||||
|
||||
/// Unsecure : for development, bypass authentication, using the account with the given
|
||||
/// email address by default
|
||||
#[clap(long, env)]
|
||||
unsecure_auto_login_email: Option<String>,
|
||||
|
||||
/// Secret key, used to secure some resources. Must be randomly generated
|
||||
#[clap(short = 'S', long, env, default_value = "")]
|
||||
secret: String,
|
||||
|
||||
/// Matrix homeserver origin
|
||||
#[clap(short, long, env, default_value = "http://127.0.0.1:8448")]
|
||||
pub matrix_homeserver: String,
|
||||
|
||||
/// Redis connection hostname
|
||||
#[clap(long, env, default_value = "localhost")]
|
||||
redis_hostname: String,
|
||||
|
||||
/// Redis connection port
|
||||
#[clap(long, env, default_value_t = 6379)]
|
||||
redis_port: u16,
|
||||
|
||||
/// Redis database number
|
||||
#[clap(long, env, default_value_t = 0)]
|
||||
redis_db_number: i64,
|
||||
|
||||
/// Redis username
|
||||
#[clap(long, env)]
|
||||
redis_username: Option<String>,
|
||||
|
||||
/// Redis password
|
||||
#[clap(long, env, default_value = "secretredis")]
|
||||
redis_password: String,
|
||||
|
||||
/// URL where the OpenID configuration can be found
|
||||
#[arg(
|
||||
long,
|
||||
env,
|
||||
default_value = "http://localhost:9001/dex/.well-known/openid-configuration"
|
||||
)]
|
||||
pub oidc_configuration_url: String,
|
||||
|
||||
/// OpenID provider name
|
||||
#[arg(long, env, default_value = "3rd party provider")]
|
||||
pub oidc_provider_name: String,
|
||||
|
||||
/// OpenID client ID
|
||||
#[arg(long, env, default_value = "foo")]
|
||||
pub oidc_client_id: String,
|
||||
|
||||
/// OpenID client secret
|
||||
#[arg(long, env, default_value = "bar")]
|
||||
pub oidc_client_secret: String,
|
||||
|
||||
/// OpenID login redirect URL
|
||||
#[arg(long, env, default_value = "APP_ORIGIN/oidc_cb")]
|
||||
oidc_redirect_url: String,
|
||||
|
||||
/// Matrix oauth redirect URL
|
||||
#[arg(long, env, default_value = "APP_ORIGIN/matrix_auth_cb")]
|
||||
matrix_oauth_redirect_url: String,
|
||||
|
||||
/// Application storage path
|
||||
#[arg(long, env, default_value = "app_storage")]
|
||||
storage_path: String,
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref ARGS: AppConfig = {
|
||||
AppConfig::parse()
|
||||
};
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
/// Get parsed command line arguments
|
||||
pub fn get() -> &'static AppConfig {
|
||||
&ARGS
|
||||
}
|
||||
|
||||
/// Get auto login email (if not empty)
|
||||
pub fn unsecure_auto_login_email(&self) -> Option<UserEmail> {
|
||||
match self.unsecure_auto_login_email.as_deref() {
|
||||
None | Some("") => None,
|
||||
Some(s) => Some(UserEmail(s.to_owned())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get app secret
|
||||
pub fn secret(&self) -> &str {
|
||||
let mut secret = self.secret.as_str();
|
||||
|
||||
if cfg!(debug_assertions) && secret.is_empty() {
|
||||
secret = "DEBUGKEYDEBUGKEYDEBUGKEYDEBUGKEYDEBUGKEYDEBUGKEYDEBUGKEYDEBUGKEY";
|
||||
}
|
||||
|
||||
if secret.is_empty() {
|
||||
panic!("SECRET is undefined or too short (min 64 chars)!")
|
||||
}
|
||||
|
||||
secret
|
||||
}
|
||||
|
||||
/// Check if auth is disabled
|
||||
pub fn is_auth_disabled(&self) -> bool {
|
||||
self.unsecure_auto_login_email().is_some()
|
||||
}
|
||||
|
||||
/// Get Redis connection configuration
|
||||
pub fn redis_connection_string(&self) -> String {
|
||||
format!(
|
||||
"redis://{}:{}@{}:{}/{}",
|
||||
self.redis_username.as_deref().unwrap_or(""),
|
||||
self.redis_password,
|
||||
self.redis_hostname,
|
||||
self.redis_port,
|
||||
self.redis_db_number
|
||||
)
|
||||
}
|
||||
|
||||
/// Get OpenID providers configuration
|
||||
pub fn openid_provider(&self) -> OIDCProvider<'_> {
|
||||
OIDCProvider {
|
||||
client_id: self.oidc_client_id.as_str(),
|
||||
client_secret: self.oidc_client_secret.as_str(),
|
||||
configuration_url: self.oidc_configuration_url.as_str(),
|
||||
name: self.oidc_provider_name.as_str(),
|
||||
redirect_url: self
|
||||
.oidc_redirect_url
|
||||
.replace("APP_ORIGIN", &self.website_origin),
|
||||
}
|
||||
}
|
||||
|
||||
/// Matrix OAuth redirect URL
|
||||
pub fn matrix_oauth_redirect_url(&self) -> String {
|
||||
self.matrix_oauth_redirect_url
|
||||
.replace("APP_ORIGIN", &self.website_origin)
|
||||
}
|
||||
|
||||
/// Get Matrix client metadata information
|
||||
pub fn matrix_client_metadata(&self) -> ClientMetadata {
|
||||
let client_uri = Localized::new(
|
||||
Url::parse(&self.website_origin).expect("Invalid website origin!"),
|
||||
[],
|
||||
);
|
||||
ClientMetadata {
|
||||
application_type: ApplicationType::Native,
|
||||
grant_types: vec![OAuthGrantType::AuthorizationCode {
|
||||
redirect_uris: vec![
|
||||
Url::parse(&self.matrix_oauth_redirect_url())
|
||||
.expect("Failed to parse matrix auth redirect URI!"),
|
||||
],
|
||||
}],
|
||||
client_name: Some(Localized::new("MatrixGW".to_string(), [])),
|
||||
logo_uri: Some(Localized::new(
|
||||
Url::parse(&format!("{}/favicon.png", self.website_origin))
|
||||
.expect("Invalid website origin!"),
|
||||
[],
|
||||
)),
|
||||
policy_uri: Some(client_uri.clone()),
|
||||
tos_uri: Some(client_uri.clone()),
|
||||
client_uri,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get storage path
|
||||
pub fn storage_path(&self) -> &Path {
|
||||
Path::new(self.storage_path.as_str())
|
||||
}
|
||||
|
||||
/// User storage directory
|
||||
pub fn user_directory(&self, mail: &UserEmail) -> PathBuf {
|
||||
self.storage_path().join("users").join(sha256str(&mail.0))
|
||||
}
|
||||
|
||||
/// User metadata file
|
||||
pub fn user_metadata_file_path(&self, mail: &UserEmail) -> PathBuf {
|
||||
self.user_directory(mail).join("metadata.json")
|
||||
}
|
||||
|
||||
/// User API tokens directory
|
||||
pub fn user_api_token_directory(&self, mail: &UserEmail) -> PathBuf {
|
||||
self.user_directory(mail).join("api-tokens")
|
||||
}
|
||||
|
||||
/// User API token metadata file
|
||||
pub fn user_api_token_metadata_file(&self, mail: &UserEmail, id: &APITokenID) -> PathBuf {
|
||||
self.user_api_token_directory(mail).join(id.0.to_string())
|
||||
}
|
||||
|
||||
/// Get user Matrix database path
|
||||
pub fn user_matrix_db_path(&self, mail: &UserEmail) -> PathBuf {
|
||||
self.user_directory(mail).join("matrix-db")
|
||||
}
|
||||
|
||||
/// Get user Matrix database passphrase path
|
||||
pub fn user_matrix_passphrase_path(&self, mail: &UserEmail) -> PathBuf {
|
||||
self.user_directory(mail).join("matrix-db-passphrase")
|
||||
}
|
||||
|
||||
/// Get user Matrix session file path
|
||||
pub fn user_matrix_session_file_path(&self, mail: &UserEmail) -> PathBuf {
|
||||
self.user_directory(mail).join("matrix-session.json")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct OIDCProvider<'a> {
|
||||
pub name: &'a str,
|
||||
pub client_id: &'a str,
|
||||
pub client_secret: &'a str,
|
||||
pub configuration_url: &'a str,
|
||||
pub redirect_url: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::app_config::AppConfig;
|
||||
|
||||
#[test]
|
||||
fn verify_cli() {
|
||||
use clap::CommandFactory;
|
||||
AppConfig::command().debug_assert()
|
||||
}
|
||||
}
|
||||
43
matrixgw_backend/src/broadcast_messages.rs
Normal file
43
matrixgw_backend/src/broadcast_messages.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
use crate::matrix_connection::sync_thread::MatrixSyncTaskID;
|
||||
use crate::users::{APIToken, UserEmail};
|
||||
use matrix_sdk::Room;
|
||||
use matrix_sdk::ruma::events::reaction::OriginalSyncReactionEvent;
|
||||
use matrix_sdk::ruma::events::receipt::SyncReceiptEvent;
|
||||
use matrix_sdk::ruma::events::room::message::OriginalSyncRoomMessageEvent;
|
||||
use matrix_sdk::ruma::events::room::redaction::OriginalSyncRoomRedactionEvent;
|
||||
use matrix_sdk::ruma::events::typing::SyncTypingEvent;
|
||||
use matrix_sdk::sync::SyncResponse;
|
||||
|
||||
pub type BroadcastSender = tokio::sync::broadcast::Sender<BroadcastMessage>;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BxRoomEvent<E> {
|
||||
pub user: UserEmail,
|
||||
pub data: Box<E>,
|
||||
pub room: Room,
|
||||
}
|
||||
|
||||
/// Broadcast messages
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum BroadcastMessage {
|
||||
/// User is or has been disconnected from Matrix
|
||||
UserDisconnectedFromMatrix(UserEmail),
|
||||
/// API token has been deleted
|
||||
APITokenDeleted(APIToken),
|
||||
/// Request a Matrix sync thread to be interrupted
|
||||
StopSyncThread(MatrixSyncTaskID),
|
||||
/// Matrix sync thread has been interrupted
|
||||
SyncThreadStopped(MatrixSyncTaskID),
|
||||
/// New room message
|
||||
RoomMessageEvent(BxRoomEvent<OriginalSyncRoomMessageEvent>),
|
||||
/// New reaction message
|
||||
ReactionEvent(BxRoomEvent<OriginalSyncReactionEvent>),
|
||||
/// New room redaction
|
||||
RoomRedactionEvent(BxRoomEvent<OriginalSyncRoomRedactionEvent>),
|
||||
/// Message fully read event
|
||||
ReceiptEvent(BxRoomEvent<SyncReceiptEvent>),
|
||||
/// User is typing message event
|
||||
TypingEvent(BxRoomEvent<SyncTypingEvent>),
|
||||
/// Raw Matrix sync response
|
||||
MatrixSyncResponse { user: UserEmail, sync: SyncResponse },
|
||||
}
|
||||
28
matrixgw_backend/src/constants.rs
Normal file
28
matrixgw_backend/src/constants.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
use std::time::Duration;
|
||||
|
||||
/// Auth header
|
||||
pub const API_AUTH_HEADER: &str = "x-client-auth";
|
||||
|
||||
/// Max token validity, in seconds
|
||||
pub const API_TOKEN_JWT_MAX_DURATION: u64 = 15 * 60;
|
||||
|
||||
/// Length of generated tokens
|
||||
pub const TOKENS_LEN: usize = 50;
|
||||
|
||||
/// Session-specific constants
|
||||
pub mod sessions {
|
||||
/// OpenID auth session state key
|
||||
pub const OIDC_STATE_KEY: &str = "oidc-state";
|
||||
/// OpenID auth remote IP address
|
||||
pub const OIDC_REMOTE_IP: &str = "oidc-remote-ip";
|
||||
/// Authenticated ID
|
||||
pub const USER_ID: &str = "uid";
|
||||
}
|
||||
|
||||
/// How often heartbeat pings are sent.
|
||||
///
|
||||
/// Should be half (or less) of the acceptable client timeout.
|
||||
pub const WS_HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
|
||||
|
||||
/// How long before lack of client response causes a timeout.
|
||||
pub const WS_CLIENT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
137
matrixgw_backend/src/controllers/auth_controller.rs
Normal file
137
matrixgw_backend/src/controllers/auth_controller.rs
Normal file
@@ -0,0 +1,137 @@
|
||||
use crate::app_config::AppConfig;
|
||||
use crate::broadcast_messages::BroadcastSender;
|
||||
use crate::controllers::{HttpFailure, HttpResult};
|
||||
use crate::extractors::auth_extractor::{AuthExtractor, AuthenticatedMethod};
|
||||
use crate::extractors::matrix_client_extractor::MatrixClientExtractor;
|
||||
use crate::extractors::session_extractor::MatrixGWSession;
|
||||
use crate::users::{User, UserEmail};
|
||||
use actix_remote_ip::RemoteIP;
|
||||
use actix_web::{HttpResponse, web};
|
||||
use light_openid::primitives::OpenIDConfig;
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct StartOIDCResponse {
|
||||
url: String,
|
||||
}
|
||||
|
||||
/// Start OIDC authentication
|
||||
pub async fn start_oidc(session: MatrixGWSession, remote_ip: RemoteIP) -> HttpResult {
|
||||
let prov = AppConfig::get().openid_provider();
|
||||
|
||||
let conf = match OpenIDConfig::load_from_url(prov.configuration_url).await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
log::error!("Failed to fetch OpenID provider configuration! {e}");
|
||||
return Ok(HttpResponse::InternalServerError()
|
||||
.json("Failed to fetch OpenID provider configuration!"));
|
||||
}
|
||||
};
|
||||
|
||||
let state = match session.gen_oidc_state(remote_ip.0) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
log::error!("Failed to generate auth state! {e}");
|
||||
return Ok(HttpResponse::InternalServerError().json("Failed to generate auth state!"));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(HttpResponse::Ok().json(StartOIDCResponse {
|
||||
url: conf.gen_authorization_url(
|
||||
prov.client_id,
|
||||
&state,
|
||||
&AppConfig::get().openid_provider().redirect_url,
|
||||
),
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct FinishOpenIDLoginQuery {
|
||||
code: String,
|
||||
state: String,
|
||||
}
|
||||
|
||||
/// Finish OIDC authentication
|
||||
pub async fn finish_oidc(
|
||||
session: MatrixGWSession,
|
||||
remote_ip: RemoteIP,
|
||||
req: web::Json<FinishOpenIDLoginQuery>,
|
||||
) -> HttpResult {
|
||||
if let Err(e) = session.validate_state(&req.state, remote_ip.0) {
|
||||
log::error!("Failed to validate OIDC CB state! {e}");
|
||||
return Ok(HttpResponse::BadRequest().json("Invalid state!"));
|
||||
}
|
||||
|
||||
let prov = AppConfig::get().openid_provider();
|
||||
|
||||
let conf = OpenIDConfig::load_from_url(prov.configuration_url)
|
||||
.await
|
||||
.map_err(HttpFailure::OpenID)?;
|
||||
|
||||
let (token, _) = conf
|
||||
.request_token(
|
||||
prov.client_id,
|
||||
prov.client_secret,
|
||||
&req.code,
|
||||
&AppConfig::get().openid_provider().redirect_url,
|
||||
)
|
||||
.await
|
||||
.map_err(HttpFailure::OpenID)?;
|
||||
let (user_info, _) = conf
|
||||
.request_user_info(&token)
|
||||
.await
|
||||
.map_err(HttpFailure::OpenID)?;
|
||||
|
||||
if user_info.email_verified != Some(true) {
|
||||
log::error!("Email is not verified!");
|
||||
return Ok(HttpResponse::Unauthorized().json("Email unverified by IDP!"));
|
||||
}
|
||||
|
||||
let mail = match user_info.email {
|
||||
Some(m) => m,
|
||||
None => {
|
||||
return Ok(HttpResponse::Unauthorized().json("Email not provided by the IDP!"));
|
||||
}
|
||||
};
|
||||
|
||||
let user_name = user_info.name.unwrap_or_else(|| {
|
||||
format!(
|
||||
"{} {}",
|
||||
user_info.given_name.as_deref().unwrap_or(""),
|
||||
user_info.family_name.as_deref().unwrap_or("")
|
||||
)
|
||||
});
|
||||
|
||||
let user = User::create_or_update_user(&UserEmail(mail), &user_name).await?;
|
||||
|
||||
session.set_user(&user)?;
|
||||
|
||||
Ok(HttpResponse::Ok().finish())
|
||||
}
|
||||
|
||||
/// Get current user information
|
||||
pub async fn auth_info(client: MatrixClientExtractor) -> HttpResult {
|
||||
Ok(HttpResponse::Ok().json(client.to_extended_user_info().await?))
|
||||
}
|
||||
|
||||
/// Sign out user
|
||||
pub async fn sign_out(
|
||||
auth: AuthExtractor,
|
||||
session: MatrixGWSession,
|
||||
tx: web::Data<BroadcastSender>,
|
||||
) -> HttpResult {
|
||||
match auth.method {
|
||||
AuthenticatedMethod::Cookie => {
|
||||
session.unset_current_user()?;
|
||||
}
|
||||
|
||||
AuthenticatedMethod::Token(token) => {
|
||||
token.delete(&auth.user.email, &tx).await?;
|
||||
}
|
||||
|
||||
AuthenticatedMethod::Dev => {
|
||||
// Nothing to be done, user is always authenticated
|
||||
}
|
||||
}
|
||||
|
||||
Ok(HttpResponse::NoContent().finish())
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
use crate::controllers::HttpResult;
|
||||
use crate::controllers::matrix::matrix_media_controller;
|
||||
use crate::controllers::matrix::matrix_media_controller::MediaQuery;
|
||||
use crate::controllers::matrix::matrix_room_controller::RoomIdInPath;
|
||||
use crate::extractors::matrix_client_extractor::MatrixClientExtractor;
|
||||
use actix_web::dev::Payload;
|
||||
use actix_web::{FromRequest, HttpRequest, HttpResponse, web};
|
||||
use futures_util::{StreamExt, stream};
|
||||
use matrix_sdk::Room;
|
||||
use matrix_sdk::deserialized_responses::{TimelineEvent, TimelineEventKind};
|
||||
use matrix_sdk::media::MediaEventContent;
|
||||
use matrix_sdk::room::MessagesOptions;
|
||||
use matrix_sdk::room::edit::EditedContent;
|
||||
use matrix_sdk::ruma::api::client::filter::RoomEventFilter;
|
||||
use matrix_sdk::ruma::api::client::receipt::create_receipt::v3::ReceiptType;
|
||||
use matrix_sdk::ruma::events::reaction::ReactionEventContent;
|
||||
use matrix_sdk::ruma::events::receipt::ReceiptThread;
|
||||
use matrix_sdk::ruma::events::relation::Annotation;
|
||||
use matrix_sdk::ruma::events::room::message::{
|
||||
MessageType, RoomMessageEvent, RoomMessageEventContent, RoomMessageEventContentWithoutRelation,
|
||||
};
|
||||
use matrix_sdk::ruma::events::{AnyMessageLikeEvent, AnyTimelineEvent};
|
||||
use matrix_sdk::ruma::{MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId, RoomId, UInt};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::value::RawValue;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct APIEvent {
|
||||
pub id: OwnedEventId,
|
||||
time: MilliSecondsSinceUnixEpoch,
|
||||
sender: OwnedUserId,
|
||||
data: Box<RawValue>,
|
||||
}
|
||||
|
||||
impl APIEvent {
|
||||
pub async fn from_evt(msg: TimelineEvent, room_id: &RoomId) -> anyhow::Result<Self> {
|
||||
let (event, raw) = match &msg.kind {
|
||||
TimelineEventKind::Decrypted(d) => (d.event.deserialize()?, d.event.json()),
|
||||
TimelineEventKind::UnableToDecrypt { event, .. }
|
||||
| TimelineEventKind::PlainText { event } => (
|
||||
event.deserialize()?.into_full_event(room_id.to_owned()),
|
||||
event.json(),
|
||||
),
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
id: event.event_id().to_owned(),
|
||||
time: event.origin_server_ts(),
|
||||
sender: event.sender().to_owned(),
|
||||
data: raw.to_owned(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct APIEventsList {
|
||||
pub start: String,
|
||||
pub end: Option<String>,
|
||||
pub events: Vec<APIEvent>,
|
||||
}
|
||||
|
||||
/// Get messages for a given room
|
||||
pub(super) async fn get_events(
|
||||
room: &Room,
|
||||
limit: u32,
|
||||
from: Option<&str>,
|
||||
filter: Option<RoomEventFilter>,
|
||||
) -> anyhow::Result<APIEventsList> {
|
||||
let mut msg_opts = MessagesOptions::backward();
|
||||
msg_opts.from = from.map(str::to_string);
|
||||
msg_opts.limit = UInt::from(limit);
|
||||
if let Some(filter) = filter {
|
||||
msg_opts.filter = filter;
|
||||
}
|
||||
|
||||
let messages = room.messages(msg_opts).await?;
|
||||
Ok(APIEventsList {
|
||||
start: messages.start,
|
||||
end: messages.end,
|
||||
events: stream::iter(messages.chunk)
|
||||
.then(async |msg| APIEvent::from_evt(msg, room.room_id()).await)
|
||||
.collect::<Vec<_>>()
|
||||
.await
|
||||
.into_iter()
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct GetRoomEventsQuery {
|
||||
#[serde(default)]
|
||||
limit: Option<u32>,
|
||||
#[serde(default)]
|
||||
from: Option<String>,
|
||||
}
|
||||
|
||||
/// Get the events for a room
|
||||
pub async fn get_for_room(
|
||||
client: MatrixClientExtractor,
|
||||
path: web::Path<RoomIdInPath>,
|
||||
query: web::Query<GetRoomEventsQuery>,
|
||||
) -> HttpResult {
|
||||
let Some(room) = client.client.client.get_room(&path.room_id) else {
|
||||
return Ok(HttpResponse::NotFound().json("Room not found!"));
|
||||
};
|
||||
|
||||
Ok(HttpResponse::Ok().json(
|
||||
get_events(
|
||||
&room,
|
||||
query.limit.unwrap_or(500),
|
||||
query.from.as_deref(),
|
||||
None,
|
||||
)
|
||||
.await?,
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct SendTextMessageRequest {
|
||||
content: String,
|
||||
}
|
||||
|
||||
pub async fn send_text_message(
|
||||
client: MatrixClientExtractor,
|
||||
path: web::Path<RoomIdInPath>,
|
||||
) -> HttpResult {
|
||||
let req = client.auth.decode_json_body::<SendTextMessageRequest>()?;
|
||||
|
||||
let Some(room) = client.client.client.get_room(&path.room_id) else {
|
||||
return Ok(HttpResponse::NotFound().json("Room not found!"));
|
||||
};
|
||||
|
||||
room.send(RoomMessageEventContent::text_plain(req.content))
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Accepted().finish())
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct EventIdInPath {
|
||||
pub(crate) event_id: OwnedEventId,
|
||||
}
|
||||
|
||||
pub async fn set_text_content(
|
||||
client: MatrixClientExtractor,
|
||||
path: web::Path<RoomIdInPath>,
|
||||
event_path: web::Path<EventIdInPath>,
|
||||
) -> HttpResult {
|
||||
let req = client.auth.decode_json_body::<SendTextMessageRequest>()?;
|
||||
|
||||
let Some(room) = client.client.client.get_room(&path.room_id) else {
|
||||
return Ok(HttpResponse::NotFound().json("Room not found!"));
|
||||
};
|
||||
|
||||
let edit_event = match room
|
||||
.make_edit_event(
|
||||
&event_path.event_id,
|
||||
EditedContent::RoomMessage(RoomMessageEventContentWithoutRelation::text_plain(
|
||||
req.content,
|
||||
)),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(msg) => msg,
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"Failed to created edit message event {}: {e}",
|
||||
event_path.event_id
|
||||
);
|
||||
return Ok(HttpResponse::InternalServerError()
|
||||
.json(format!("Failed to create edit message event! {e}")));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(match room.send(edit_event).await {
|
||||
Ok(_) => HttpResponse::Accepted().finish(),
|
||||
Err(e) => {
|
||||
log::error!("Failed to edit event message {}: {e}", event_path.event_id);
|
||||
HttpResponse::InternalServerError().json(format!("Failed to edit event! {e}"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn event_file(
|
||||
req: HttpRequest,
|
||||
client: MatrixClientExtractor,
|
||||
path: web::Path<RoomIdInPath>,
|
||||
event_path: web::Path<EventIdInPath>,
|
||||
) -> HttpResult {
|
||||
let query = web::Query::<MediaQuery>::from_request(&req, &mut Payload::None).await?;
|
||||
|
||||
let Some(room) = client.client.client.get_room(&path.room_id) else {
|
||||
return Ok(HttpResponse::NotFound().json("Room not found!"));
|
||||
};
|
||||
|
||||
let event = match room.load_or_fetch_event(&event_path.event_id, None).await {
|
||||
Ok(event) => event,
|
||||
Err(e) => {
|
||||
log::error!("Failed to load event information! {e}");
|
||||
return Ok(HttpResponse::InternalServerError()
|
||||
.json(format!("Failed to load event information! {e}")));
|
||||
}
|
||||
};
|
||||
|
||||
let event = match event.kind {
|
||||
TimelineEventKind::Decrypted(dec) => dec.event.deserialize()?,
|
||||
TimelineEventKind::UnableToDecrypt { event, .. }
|
||||
| TimelineEventKind::PlainText { event } => event
|
||||
.deserialize()?
|
||||
.into_full_event(room.room_id().to_owned()),
|
||||
};
|
||||
|
||||
let AnyTimelineEvent::MessageLike(message) = event else {
|
||||
return Ok(HttpResponse::BadRequest().json("Event is not message like!"));
|
||||
};
|
||||
|
||||
let AnyMessageLikeEvent::RoomMessage(message) = message else {
|
||||
return Ok(HttpResponse::BadRequest().json("Event is not a room message!"));
|
||||
};
|
||||
|
||||
let RoomMessageEvent::Original(message) = message else {
|
||||
return Ok(HttpResponse::BadRequest().json("Event has been redacted!"));
|
||||
};
|
||||
|
||||
let (source, thumb_source) = match message.content.msgtype {
|
||||
MessageType::Audio(c) => (c.source(), c.thumbnail_source()),
|
||||
MessageType::File(c) => (c.source(), c.thumbnail_source()),
|
||||
MessageType::Image(c) => (c.source(), c.thumbnail_source()),
|
||||
MessageType::Location(c) => (c.source(), c.thumbnail_source()),
|
||||
MessageType::Video(c) => (c.source(), c.thumbnail_source()),
|
||||
_ => (None, None),
|
||||
};
|
||||
|
||||
let source = match (query.thumbnail, source, thumb_source) {
|
||||
(false, Some(s), _) => s,
|
||||
(true, _, Some(s)) => s,
|
||||
_ => return Ok(HttpResponse::NotFound().json("Requested file not available!")),
|
||||
};
|
||||
|
||||
matrix_media_controller::serve_media(req, source, false).await
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct EventReactionBody {
|
||||
key: String,
|
||||
}
|
||||
|
||||
pub async fn react_to_event(
|
||||
client: MatrixClientExtractor,
|
||||
path: web::Path<RoomIdInPath>,
|
||||
event_path: web::Path<EventIdInPath>,
|
||||
) -> HttpResult {
|
||||
let body = client.auth.decode_json_body::<EventReactionBody>()?;
|
||||
|
||||
let Some(room) = client.client.client.get_room(&path.room_id) else {
|
||||
return Ok(HttpResponse::NotFound().json("Room not found!"));
|
||||
};
|
||||
|
||||
let annotation = Annotation::new(event_path.event_id.to_owned(), body.key.to_owned());
|
||||
room.send(ReactionEventContent::from(annotation)).await?;
|
||||
|
||||
Ok(HttpResponse::Accepted().finish())
|
||||
}
|
||||
|
||||
pub async fn redact_event(
|
||||
client: MatrixClientExtractor,
|
||||
path: web::Path<RoomIdInPath>,
|
||||
event_path: web::Path<EventIdInPath>,
|
||||
) -> HttpResult {
|
||||
let Some(room) = client.client.client.get_room(&path.room_id) else {
|
||||
return Ok(HttpResponse::NotFound().json("Room not found!"));
|
||||
};
|
||||
|
||||
Ok(match room.redact(&event_path.event_id, None, None).await {
|
||||
Ok(_) => HttpResponse::Accepted().finish(),
|
||||
|
||||
Err(e) => {
|
||||
log::error!("Failed to redact event {}: {e}", event_path.event_id);
|
||||
HttpResponse::InternalServerError().json(format!("Failed to redact event! {e}"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Send receipt for event
|
||||
pub async fn receipt(
|
||||
client: MatrixClientExtractor,
|
||||
path: web::Path<RoomIdInPath>,
|
||||
event_path: web::Path<EventIdInPath>,
|
||||
) -> HttpResult {
|
||||
let Some(room) = client.client.client.get_room(&path.room_id) else {
|
||||
return Ok(HttpResponse::NotFound().json("Room not found"));
|
||||
};
|
||||
|
||||
room.send_single_receipt(
|
||||
ReceiptType::Read,
|
||||
ReceiptThread::Main,
|
||||
event_path.event_id.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Accepted().finish())
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
use crate::controllers::HttpResult;
|
||||
use crate::extractors::matrix_client_extractor::MatrixClientExtractor;
|
||||
use crate::utils::crypt_utils::sha512;
|
||||
use actix_web::dev::Payload;
|
||||
use actix_web::http::header;
|
||||
use actix_web::{FromRequest, HttpRequest, HttpResponse, web};
|
||||
use matrix_sdk::crypto::{AttachmentDecryptor, MediaEncryptionInfo};
|
||||
use matrix_sdk::media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings};
|
||||
use matrix_sdk::ruma::events::room::MediaSource;
|
||||
use matrix_sdk::ruma::{OwnedMxcUri, UInt};
|
||||
use std::io::{Cursor, Read};
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct MediaMXCInPath {
|
||||
mxc: OwnedMxcUri,
|
||||
}
|
||||
|
||||
/// Serve media resource handler
|
||||
pub async fn serve_mxc_handler(req: HttpRequest, media: web::Path<MediaMXCInPath>) -> HttpResult {
|
||||
serve_mxc_file(req, media.into_inner().mxc).await
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct MediaQuery {
|
||||
#[serde(default)]
|
||||
pub thumbnail: bool,
|
||||
}
|
||||
pub async fn serve_mxc_file(req: HttpRequest, media: OwnedMxcUri) -> HttpResult {
|
||||
let query = web::Query::<MediaQuery>::from_request(&req, &mut Payload::None).await?;
|
||||
|
||||
serve_media(req, MediaSource::Plain(media), query.thumbnail).await
|
||||
}
|
||||
|
||||
/// Serve a media file
|
||||
pub async fn serve_media(req: HttpRequest, source: MediaSource, thumbnail: bool) -> HttpResult {
|
||||
let client = MatrixClientExtractor::from_request(&req, &mut Payload::None).await?;
|
||||
|
||||
let media = client
|
||||
.client
|
||||
.client
|
||||
.media()
|
||||
.get_media_content(
|
||||
&MediaRequestParameters {
|
||||
source: source.clone(),
|
||||
format: match thumbnail {
|
||||
true => MediaFormat::Thumbnail(MediaThumbnailSettings::new(
|
||||
UInt::new(100).unwrap(),
|
||||
UInt::new(100).unwrap(),
|
||||
)),
|
||||
false => MediaFormat::File,
|
||||
},
|
||||
},
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Decrypt file if needed
|
||||
let media = if let MediaSource::Encrypted(file) = source {
|
||||
let mut cursor = Cursor::new(media);
|
||||
let mut decryptor =
|
||||
AttachmentDecryptor::new(&mut cursor, MediaEncryptionInfo::from(*file))?;
|
||||
|
||||
let mut decrypted_data = Vec::new();
|
||||
|
||||
decryptor.read_to_end(&mut decrypted_data)?;
|
||||
|
||||
decrypted_data
|
||||
} else {
|
||||
media
|
||||
};
|
||||
|
||||
let digest = sha512(&media);
|
||||
|
||||
let mime_type = infer::get(&media).map(|x| x.mime_type());
|
||||
|
||||
// Check if the browser already knows the etag
|
||||
if let Some(c) = req.headers().get(header::IF_NONE_MATCH)
|
||||
&& c.to_str().unwrap_or("") == digest
|
||||
{
|
||||
return Ok(HttpResponse::NotModified().finish());
|
||||
}
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type(mime_type.unwrap_or("application/octet-stream"))
|
||||
.insert_header(("etag", digest))
|
||||
.insert_header(("cache-control", "max-age=360000"))
|
||||
.body(media))
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
use crate::controllers::HttpResult;
|
||||
use crate::extractors::matrix_client_extractor::MatrixClientExtractor;
|
||||
use actix_web::{HttpResponse, web};
|
||||
use futures_util::{StreamExt, stream};
|
||||
use matrix_sdk::ruma::api::client::profile::{AvatarUrl, DisplayName, get_profile};
|
||||
use matrix_sdk::ruma::{OwnedMxcUri, OwnedUserId};
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct UserIDInPath {
|
||||
user_id: OwnedUserId,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct ProfileResponse {
|
||||
user_id: OwnedUserId,
|
||||
display_name: Option<String>,
|
||||
avatar: Option<OwnedMxcUri>,
|
||||
}
|
||||
|
||||
impl ProfileResponse {
|
||||
pub fn from(user_id: OwnedUserId, r: get_profile::v3::Response) -> anyhow::Result<Self> {
|
||||
Ok(Self {
|
||||
user_id,
|
||||
display_name: r.get_static::<DisplayName>()?,
|
||||
avatar: r.get_static::<AvatarUrl>()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Get user profile
|
||||
pub async fn get_profile(
|
||||
client: MatrixClientExtractor,
|
||||
path: web::Path<UserIDInPath>,
|
||||
) -> HttpResult {
|
||||
let profile = client
|
||||
.client
|
||||
.client
|
||||
.account()
|
||||
.fetch_user_profile_of(&path.user_id)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(ProfileResponse::from(path.user_id.clone(), profile)?))
|
||||
}
|
||||
|
||||
/// Get multiple users profiles
|
||||
pub async fn get_multiple(client: MatrixClientExtractor) -> HttpResult {
|
||||
let users = client.auth.decode_json_body::<Vec<OwnedUserId>>()?;
|
||||
|
||||
let list = stream::iter(users)
|
||||
.then(async |user_id| {
|
||||
client
|
||||
.client
|
||||
.client
|
||||
.account()
|
||||
.fetch_user_profile_of(&user_id)
|
||||
.await
|
||||
.map(|r| ProfileResponse::from(user_id, r))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.await
|
||||
.into_iter()
|
||||
.collect::<Result<Vec<_>, _>>()?
|
||||
.into_iter()
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(list))
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
use crate::controllers::HttpResult;
|
||||
use crate::controllers::matrix::matrix_event_controller::{APIEvent, get_events};
|
||||
use crate::controllers::matrix::matrix_media_controller;
|
||||
use crate::extractors::matrix_client_extractor::MatrixClientExtractor;
|
||||
use actix_web::{HttpRequest, HttpResponse, web};
|
||||
use futures_util::{StreamExt, stream};
|
||||
use matrix_sdk::notification_settings::{
|
||||
IsEncrypted, IsOneToOne, NotificationSettings, RoomNotificationMode,
|
||||
};
|
||||
use matrix_sdk::room::ParentSpace;
|
||||
use matrix_sdk::ruma::events::receipt::{ReceiptThread, ReceiptType};
|
||||
use matrix_sdk::ruma::{
|
||||
MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri, OwnedRoomId, OwnedUserId,
|
||||
};
|
||||
use matrix_sdk::{Room, RoomMemberships};
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct APIRoomInfo {
|
||||
id: OwnedRoomId,
|
||||
name: Option<String>,
|
||||
members: Vec<OwnedUserId>,
|
||||
avatar: Option<OwnedMxcUri>,
|
||||
is_space: bool,
|
||||
parents: Vec<OwnedRoomId>,
|
||||
number_unread_messages: u64,
|
||||
notifications: RoomNotificationMode,
|
||||
latest_event: Option<APIEvent>,
|
||||
}
|
||||
|
||||
impl APIRoomInfo {
|
||||
async fn from_room(r: &Room, notif: &NotificationSettings) -> anyhow::Result<Self> {
|
||||
// Get parent spaces
|
||||
let parent_spaces = r
|
||||
.parent_spaces()
|
||||
.await?
|
||||
.collect::<Vec<_>>()
|
||||
.await
|
||||
.into_iter()
|
||||
.collect::<Result<Vec<_>, _>>()?
|
||||
.into_iter()
|
||||
.filter_map(|d| match d {
|
||||
ParentSpace::Reciprocal(r) | ParentSpace::WithPowerlevel(r) => {
|
||||
Some(r.room_id().to_owned())
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let members = r
|
||||
.members(RoomMemberships::ACTIVE)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|r| r.user_id().to_owned())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let notifications = notif
|
||||
.get_user_defined_room_notification_mode(r.room_id())
|
||||
.await
|
||||
.unwrap_or(
|
||||
notif
|
||||
.get_default_room_notification_mode(
|
||||
IsEncrypted::from(r.encryption_state().is_encrypted()),
|
||||
IsOneToOne::from(members.len() == 2),
|
||||
)
|
||||
.await,
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
id: r.room_id().to_owned(),
|
||||
name: r.name(),
|
||||
members,
|
||||
avatar: r.avatar_url(),
|
||||
is_space: r.is_space(),
|
||||
parents: parent_spaces,
|
||||
number_unread_messages: r.unread_notification_counts().notification_count,
|
||||
notifications,
|
||||
latest_event: get_events(r, 1, None, None)
|
||||
.await?
|
||||
.events
|
||||
.into_iter()
|
||||
.next(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the list of joined rooms of the user
|
||||
pub async fn joined_rooms(client: MatrixClientExtractor) -> HttpResult {
|
||||
let notifs = client.client.client.notification_settings().await;
|
||||
let list = stream::iter(client.client.client.joined_rooms())
|
||||
.then(async |room| APIRoomInfo::from_room(&room, ¬ifs).await)
|
||||
.collect::<Vec<_>>()
|
||||
.await
|
||||
.into_iter()
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(list))
|
||||
}
|
||||
|
||||
/// Get joined spaces rooms of user
|
||||
pub async fn get_joined_spaces(client: MatrixClientExtractor) -> HttpResult {
|
||||
let notifs = client.client.client.notification_settings().await;
|
||||
|
||||
let list = stream::iter(client.client.client.joined_space_rooms())
|
||||
.then(async |room| APIRoomInfo::from_room(&room, ¬ifs).await)
|
||||
.collect::<Vec<_>>()
|
||||
.await
|
||||
.into_iter()
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(list))
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct RoomIdInPath {
|
||||
pub(crate) room_id: OwnedRoomId,
|
||||
}
|
||||
|
||||
/// Get the list of joined rooms of the user
|
||||
pub async fn single_room_info(
|
||||
client: MatrixClientExtractor,
|
||||
path: web::Path<RoomIdInPath>,
|
||||
) -> HttpResult {
|
||||
let notifs = client.client.client.notification_settings().await;
|
||||
|
||||
Ok(match client.client.client.get_room(&path.room_id) {
|
||||
None => HttpResponse::NotFound().json("Room not found"),
|
||||
Some(r) => HttpResponse::Ok().json(APIRoomInfo::from_room(&r, ¬ifs).await?),
|
||||
})
|
||||
}
|
||||
|
||||
/// Get room avatar
|
||||
pub async fn room_avatar(
|
||||
req: HttpRequest,
|
||||
client: MatrixClientExtractor,
|
||||
path: web::Path<RoomIdInPath>,
|
||||
) -> HttpResult {
|
||||
let Some(room) = client.client.client.get_room(&path.room_id) else {
|
||||
return Ok(HttpResponse::NotFound().json("Room not found"));
|
||||
};
|
||||
|
||||
let Some(uri) = room.avatar_url() else {
|
||||
return Ok(HttpResponse::NotFound().json("Room has no avatar"));
|
||||
};
|
||||
|
||||
matrix_media_controller::serve_mxc_file(req, uri).await
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct UserReceipt {
|
||||
user: OwnedUserId,
|
||||
event_id: OwnedEventId,
|
||||
ts: Option<MilliSecondsSinceUnixEpoch>,
|
||||
}
|
||||
|
||||
/// Get room receipts
|
||||
pub async fn receipts(client: MatrixClientExtractor, path: web::Path<RoomIdInPath>) -> HttpResult {
|
||||
let Some(room) = client.client.client.get_room(&path.room_id) else {
|
||||
return Ok(HttpResponse::NotFound().json("Room not found"));
|
||||
};
|
||||
|
||||
let members = room.members(RoomMemberships::ACTIVE).await?;
|
||||
|
||||
let mut receipts = Vec::new();
|
||||
for m in members {
|
||||
let Some((event_id, receipt)) = room
|
||||
.load_user_receipt(ReceiptType::Read, ReceiptThread::Main, m.user_id())
|
||||
.await?
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
receipts.push(UserReceipt {
|
||||
user: m.user_id().to_owned(),
|
||||
event_id,
|
||||
ts: receipt.ts,
|
||||
})
|
||||
}
|
||||
|
||||
Ok(HttpResponse::Ok().json(receipts))
|
||||
}
|
||||
4
matrixgw_backend/src/controllers/matrix/mod.rs
Normal file
4
matrixgw_backend/src/controllers/matrix/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod matrix_event_controller;
|
||||
pub mod matrix_media_controller;
|
||||
pub mod matrix_profile_controller;
|
||||
pub mod matrix_room_controller;
|
||||
59
matrixgw_backend/src/controllers/matrix_link_controller.rs
Normal file
59
matrixgw_backend/src/controllers/matrix_link_controller.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use crate::controllers::HttpResult;
|
||||
use crate::extractors::auth_extractor::AuthExtractor;
|
||||
use crate::extractors::matrix_client_extractor::MatrixClientExtractor;
|
||||
use crate::matrix_connection::matrix_client::FinishMatrixAuth;
|
||||
use crate::matrix_connection::matrix_manager::MatrixManagerMsg;
|
||||
use actix_web::{HttpResponse, web};
|
||||
use ractor::ActorRef;
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct StartAuthResponse {
|
||||
url: String,
|
||||
}
|
||||
|
||||
/// Start user authentication on Matrix server
|
||||
pub async fn start_auth(client: MatrixClientExtractor) -> HttpResult {
|
||||
let url = client.client.initiate_login().await?.to_string();
|
||||
Ok(HttpResponse::Ok().json(StartAuthResponse { url }))
|
||||
}
|
||||
|
||||
/// Finish user authentication on Matrix server
|
||||
pub async fn finish_auth(client: MatrixClientExtractor) -> HttpResult {
|
||||
match client
|
||||
.client
|
||||
.finish_login(client.auth.decode_json_body::<FinishMatrixAuth>()?)
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(HttpResponse::Accepted().finish()),
|
||||
Err(e) => {
|
||||
log::error!("Failed to finish Matrix authentication: {e}");
|
||||
Err(e.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Logout user from Matrix server
|
||||
pub async fn logout(
|
||||
auth: AuthExtractor,
|
||||
manager: web::Data<ActorRef<MatrixManagerMsg>>,
|
||||
) -> HttpResult {
|
||||
manager
|
||||
.cast(MatrixManagerMsg::DisconnectClient(auth.user.email))
|
||||
.expect("Failed to communicate with matrix manager!");
|
||||
|
||||
Ok(HttpResponse::Ok().finish())
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct SetRecoveryKeyRequest {
|
||||
key: String,
|
||||
}
|
||||
|
||||
/// Set recovery key of user
|
||||
pub async fn set_recovery_key(client: MatrixClientExtractor) -> HttpResult {
|
||||
let key = client.auth.decode_json_body::<SetRecoveryKeyRequest>()?.key;
|
||||
|
||||
client.client.set_recovery_key(&key).await?;
|
||||
|
||||
Ok(HttpResponse::Accepted().finish())
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
use crate::controllers::HttpResult;
|
||||
use crate::extractors::matrix_client_extractor::MatrixClientExtractor;
|
||||
use crate::matrix_connection::matrix_manager::MatrixManagerMsg;
|
||||
use actix_web::{HttpResponse, web};
|
||||
use ractor::ActorRef;
|
||||
|
||||
/// Start sync thread
|
||||
pub async fn start_sync(
|
||||
client: MatrixClientExtractor,
|
||||
manager: web::Data<ActorRef<MatrixManagerMsg>>,
|
||||
) -> HttpResult {
|
||||
match ractor::cast!(
|
||||
manager,
|
||||
MatrixManagerMsg::StartSyncThread(client.auth.user.email.clone())
|
||||
) {
|
||||
Ok(_) => Ok(HttpResponse::Accepted().finish()),
|
||||
Err(e) => {
|
||||
log::error!("Failed to start sync: {e}");
|
||||
Ok(HttpResponse::InternalServerError().finish())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop sync thread
|
||||
pub async fn stop_sync(
|
||||
client: MatrixClientExtractor,
|
||||
manager: web::Data<ActorRef<MatrixManagerMsg>>,
|
||||
) -> HttpResult {
|
||||
match ractor::cast!(
|
||||
manager,
|
||||
MatrixManagerMsg::StopSyncThread(client.auth.user.email.clone())
|
||||
) {
|
||||
Ok(_) => Ok(HttpResponse::Accepted().finish()),
|
||||
Err(e) => {
|
||||
log::error!("Failed to stop sync thread: {e}");
|
||||
Ok(HttpResponse::InternalServerError().finish())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct GetSyncStatusResponse {
|
||||
started: bool,
|
||||
}
|
||||
|
||||
/// Get sync thread status
|
||||
pub async fn status(
|
||||
client: MatrixClientExtractor,
|
||||
manager: web::Data<ActorRef<MatrixManagerMsg>>,
|
||||
) -> HttpResult {
|
||||
let started = ractor::call!(
|
||||
manager.as_ref(),
|
||||
MatrixManagerMsg::SyncThreadGetStatus,
|
||||
client.auth.user.email
|
||||
)
|
||||
.expect("RPC to Matrix Manager failed");
|
||||
|
||||
Ok(HttpResponse::Ok().json(GetSyncStatusResponse { started }))
|
||||
}
|
||||
52
matrixgw_backend/src/controllers/mod.rs
Normal file
52
matrixgw_backend/src/controllers/mod.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{HttpResponse, ResponseError};
|
||||
use std::error::Error;
|
||||
|
||||
pub mod auth_controller;
|
||||
pub mod matrix;
|
||||
pub mod matrix_link_controller;
|
||||
pub mod matrix_sync_thread_controller;
|
||||
pub mod server_controller;
|
||||
pub mod static_controller;
|
||||
pub mod tokens_controller;
|
||||
pub mod ws_controller;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum HttpFailure {
|
||||
#[error("this resource requires higher privileges")]
|
||||
Forbidden,
|
||||
#[error("this resource was not found")]
|
||||
NotFound,
|
||||
#[error("an unspecified open id error occurred: {0}")]
|
||||
OpenID(Box<dyn Error>),
|
||||
#[error("an unspecified internal error occurred: {0}")]
|
||||
InternalError(#[from] anyhow::Error),
|
||||
#[error("Actix web error: {0}")]
|
||||
ActixError(#[from] actix_web::Error),
|
||||
#[error("Matrix error: {0}")]
|
||||
MatrixError(#[from] matrix_sdk::Error),
|
||||
#[error("Matrix decryptor error: {0}")]
|
||||
MatrixDecryptorError(#[from] matrix_sdk::encryption::DecryptorError),
|
||||
#[error("Serde JSON error: {0}")]
|
||||
SerdeJSON(#[from] serde_json::Error),
|
||||
#[error("Standard library error: {0}")]
|
||||
StdLibError(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
impl ResponseError for HttpFailure {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match &self {
|
||||
Self::Forbidden => StatusCode::FORBIDDEN,
|
||||
Self::NotFound => StatusCode::NOT_FOUND,
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
HttpResponse::build(self.status_code())
|
||||
.content_type("text/plain")
|
||||
.body(self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub type HttpResult = Result<HttpResponse, HttpFailure>;
|
||||
74
matrixgw_backend/src/controllers/server_controller.rs
Normal file
74
matrixgw_backend/src/controllers/server_controller.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
use crate::app_config::AppConfig;
|
||||
use actix_web::HttpResponse;
|
||||
|
||||
/// Serve robots.txt (disallow ranking)
|
||||
pub async fn robots_txt() -> HttpResponse {
|
||||
HttpResponse::Ok()
|
||||
.content_type("text/plain")
|
||||
.body("User-agent: *\nDisallow: /\n")
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct LenConstraints {
|
||||
min: usize,
|
||||
max: usize,
|
||||
}
|
||||
|
||||
impl LenConstraints {
|
||||
pub fn new(min: usize, max: usize) -> Self {
|
||||
Self { min, max }
|
||||
}
|
||||
pub fn not_empty(max: usize) -> Self {
|
||||
Self { min: 1, max }
|
||||
}
|
||||
pub fn max_only(max: usize) -> Self {
|
||||
Self { min: 0, max }
|
||||
}
|
||||
|
||||
pub fn check_str(&self, s: &str) -> bool {
|
||||
s.len() >= self.min && s.len() <= self.max
|
||||
}
|
||||
|
||||
pub fn check_u32(&self, v: u32) -> bool {
|
||||
v >= self.min as u32 && v <= self.max as u32
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct ServerConstraints {
|
||||
pub token_name: LenConstraints,
|
||||
pub token_ip_net: LenConstraints,
|
||||
pub token_max_inactivity: LenConstraints,
|
||||
}
|
||||
|
||||
impl Default for ServerConstraints {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
token_name: LenConstraints::new(5, 255),
|
||||
token_ip_net: LenConstraints::max_only(44),
|
||||
token_max_inactivity: LenConstraints::new(3600, 3600 * 24 * 365),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct ServerConfig {
|
||||
auth_disabled: bool,
|
||||
oidc_provider_name: &'static str,
|
||||
constraints: ServerConstraints,
|
||||
}
|
||||
|
||||
impl Default for ServerConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
auth_disabled: AppConfig::get().is_auth_disabled(),
|
||||
oidc_provider_name: AppConfig::get().openid_provider().name,
|
||||
constraints: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get server static configuration
|
||||
pub async fn config() -> HttpResponse {
|
||||
HttpResponse::Ok().json(ServerConfig::default())
|
||||
}
|
||||
45
matrixgw_backend/src/controllers/static_controller.rs
Normal file
45
matrixgw_backend/src/controllers/static_controller.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
#[cfg(debug_assertions)]
|
||||
pub use serve_static_debug::{root_index, serve_static_content};
|
||||
#[cfg(not(debug_assertions))]
|
||||
pub use serve_static_release::{root_index, serve_static_content};
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
mod serve_static_debug {
|
||||
use actix_web::{HttpResponse, Responder};
|
||||
|
||||
pub async fn root_index() -> impl Responder {
|
||||
HttpResponse::Ok().body("Hello world! Debug=on for Matrix Gateway!")
|
||||
}
|
||||
|
||||
pub async fn serve_static_content() -> impl Responder {
|
||||
HttpResponse::NotFound().body("Hello world! Static assets are not served in debug mode")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
mod serve_static_release {
|
||||
use actix_web::{HttpResponse, Responder, web};
|
||||
use rust_embed::RustEmbed;
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "static/"]
|
||||
struct Asset;
|
||||
|
||||
fn handle_embedded_file(path: &str, can_fallback: bool) -> HttpResponse {
|
||||
match (Asset::get(path), can_fallback) {
|
||||
(Some(content), _) => HttpResponse::Ok()
|
||||
.content_type(mime_guess::from_path(path).first_or_octet_stream().as_ref())
|
||||
.body(content.data.into_owned()),
|
||||
(None, false) => HttpResponse::NotFound().body("404 Not Found"),
|
||||
(None, true) => handle_embedded_file("index.html", false),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn root_index() -> impl Responder {
|
||||
handle_embedded_file("index.html", false)
|
||||
}
|
||||
|
||||
pub async fn serve_static_content(path: web::Path<String>) -> impl Responder {
|
||||
handle_embedded_file(path.as_ref(), !path.as_ref().starts_with("static/"))
|
||||
}
|
||||
}
|
||||
53
matrixgw_backend/src/controllers/tokens_controller.rs
Normal file
53
matrixgw_backend/src/controllers/tokens_controller.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use crate::broadcast_messages::BroadcastSender;
|
||||
use crate::controllers::HttpResult;
|
||||
use crate::extractors::auth_extractor::{AuthExtractor, AuthenticatedMethod};
|
||||
use crate::users::{APIToken, APITokenID, BaseAPIToken};
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
/// Create a new token
|
||||
pub async fn create(auth: AuthExtractor) -> HttpResult {
|
||||
if matches!(auth.method, AuthenticatedMethod::Token(_)) {
|
||||
return Ok(HttpResponse::Forbidden()
|
||||
.json("It is not allowed to create a token using another token!"));
|
||||
}
|
||||
|
||||
let base = auth.decode_json_body::<BaseAPIToken>()?;
|
||||
|
||||
if let Some(err) = base.check() {
|
||||
return Ok(HttpResponse::BadRequest().json(err));
|
||||
}
|
||||
|
||||
let token = APIToken::create(&auth.as_ref().email, base).await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(token))
|
||||
}
|
||||
|
||||
/// Get the list of tokens of current user
|
||||
pub async fn get_list(auth: AuthExtractor) -> HttpResult {
|
||||
Ok(HttpResponse::Ok().json(
|
||||
APIToken::list_user(&auth.as_ref().email)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|mut t| {
|
||||
t.secret = String::new();
|
||||
t
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct TokenIDInPath {
|
||||
id: APITokenID,
|
||||
}
|
||||
|
||||
/// Delete an API access token
|
||||
pub async fn delete(
|
||||
auth: AuthExtractor,
|
||||
path: web::Path<TokenIDInPath>,
|
||||
tx: web::Data<BroadcastSender>,
|
||||
) -> HttpResult {
|
||||
let token = APIToken::load(&auth.user.email, &path.id).await?;
|
||||
token.delete(&auth.user.email, &tx).await?;
|
||||
Ok(HttpResponse::Accepted().finish())
|
||||
}
|
||||
307
matrixgw_backend/src/controllers/ws_controller.rs
Normal file
307
matrixgw_backend/src/controllers/ws_controller.rs
Normal file
@@ -0,0 +1,307 @@
|
||||
use crate::broadcast_messages::BroadcastMessage;
|
||||
use crate::constants;
|
||||
use crate::controllers::HttpResult;
|
||||
use crate::extractors::auth_extractor::{AuthExtractor, AuthenticatedMethod};
|
||||
use crate::extractors::matrix_client_extractor::MatrixClientExtractor;
|
||||
use crate::matrix_connection::matrix_client::MatrixClient;
|
||||
use crate::matrix_connection::matrix_manager::MatrixManagerMsg;
|
||||
use crate::users::UserEmail;
|
||||
use actix_web::dev::Payload;
|
||||
use actix_web::{FromRequest, HttpRequest, HttpResponse, web};
|
||||
use actix_ws::Message;
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk::ruma::events::reaction::ReactionEventContent;
|
||||
use matrix_sdk::ruma::events::room::message::RoomMessageEventContent;
|
||||
use matrix_sdk::ruma::events::room::redaction::RoomRedactionEventContent;
|
||||
use matrix_sdk::ruma::{MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId};
|
||||
use ractor::ActorRef;
|
||||
use std::time::Instant;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::sync::broadcast::Receiver;
|
||||
use tokio::time::interval;
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct WsRoomEvent<E> {
|
||||
pub room_id: OwnedRoomId,
|
||||
pub event_id: OwnedEventId,
|
||||
pub sender: OwnedUserId,
|
||||
pub origin_server_ts: MilliSecondsSinceUnixEpoch,
|
||||
pub data: Box<E>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct WsReceiptEntry {
|
||||
event: OwnedEventId,
|
||||
user: OwnedUserId,
|
||||
ts: Option<MilliSecondsSinceUnixEpoch>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct WsReceiptEvent {
|
||||
pub room_id: OwnedRoomId,
|
||||
pub receipts: Vec<WsReceiptEntry>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct WsTypingEvent {
|
||||
pub room_id: OwnedRoomId,
|
||||
pub user_ids: Vec<OwnedUserId>,
|
||||
}
|
||||
|
||||
/// Messages sent to the client
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum WsMessage {
|
||||
/// Room message event
|
||||
RoomMessageEvent(WsRoomEvent<RoomMessageEventContent>),
|
||||
|
||||
/// Room reaction event
|
||||
RoomReactionEvent(WsRoomEvent<ReactionEventContent>),
|
||||
|
||||
/// Room reaction event
|
||||
RoomRedactionEvent(WsRoomEvent<RoomRedactionEventContent>),
|
||||
|
||||
/// Fully read message event
|
||||
ReceiptEvent(WsReceiptEvent),
|
||||
|
||||
/// User is typing event
|
||||
TypingEvent(WsTypingEvent),
|
||||
}
|
||||
|
||||
impl WsMessage {
|
||||
pub fn from_bx_message(msg: &BroadcastMessage, user: &UserEmail) -> Option<Self> {
|
||||
match msg {
|
||||
BroadcastMessage::RoomMessageEvent(evt) if &evt.user == user => {
|
||||
Some(Self::RoomMessageEvent(WsRoomEvent {
|
||||
room_id: evt.room.room_id().to_owned(),
|
||||
event_id: evt.data.event_id.clone(),
|
||||
sender: evt.data.sender.clone(),
|
||||
origin_server_ts: evt.data.origin_server_ts,
|
||||
data: Box::new(evt.data.content.clone()),
|
||||
}))
|
||||
}
|
||||
|
||||
BroadcastMessage::ReactionEvent(evt) if &evt.user == user => {
|
||||
Some(Self::RoomReactionEvent(WsRoomEvent {
|
||||
room_id: evt.room.room_id().to_owned(),
|
||||
event_id: evt.data.event_id.clone(),
|
||||
sender: evt.data.sender.clone(),
|
||||
origin_server_ts: evt.data.origin_server_ts,
|
||||
data: Box::new(evt.data.content.clone()),
|
||||
}))
|
||||
}
|
||||
|
||||
BroadcastMessage::RoomRedactionEvent(evt) if &evt.user == user => {
|
||||
Some(Self::RoomRedactionEvent(WsRoomEvent {
|
||||
room_id: evt.room.room_id().to_owned(),
|
||||
event_id: evt.data.event_id.clone(),
|
||||
sender: evt.data.sender.clone(),
|
||||
origin_server_ts: evt.data.origin_server_ts,
|
||||
data: Box::new(evt.data.content.clone()),
|
||||
}))
|
||||
}
|
||||
|
||||
BroadcastMessage::ReceiptEvent(evt) if &evt.user == user => {
|
||||
let mut receipts = vec![];
|
||||
for (event_id, r) in &evt.data.content.0 {
|
||||
for user_receipts in r.values() {
|
||||
for (user, receipt) in user_receipts {
|
||||
receipts.push(WsReceiptEntry {
|
||||
event: event_id.clone(),
|
||||
user: user.clone(),
|
||||
ts: receipt.ts,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(Self::ReceiptEvent(WsReceiptEvent {
|
||||
room_id: evt.room.room_id().to_owned(),
|
||||
receipts,
|
||||
}))
|
||||
}
|
||||
|
||||
BroadcastMessage::TypingEvent(evt) if &evt.user == user => {
|
||||
Some(Self::TypingEvent(WsTypingEvent {
|
||||
room_id: evt.room.room_id().to_owned(),
|
||||
user_ids: evt.data.content.user_ids.clone(),
|
||||
}))
|
||||
}
|
||||
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Main WS route
|
||||
pub async fn ws(
|
||||
req: HttpRequest,
|
||||
stream: web::Payload,
|
||||
tx: web::Data<broadcast::Sender<BroadcastMessage>>,
|
||||
manager: web::Data<ActorRef<MatrixManagerMsg>>,
|
||||
) -> HttpResult {
|
||||
// Forcefully ignore request payload by manually extracting authentication information
|
||||
let client = MatrixClientExtractor::from_request(&req, &mut Payload::None).await?;
|
||||
|
||||
// Check if Matrix link has been established first
|
||||
if !client.client.is_client_connected() {
|
||||
return Ok(HttpResponse::ExpectationFailed().json("Matrix link not established yet!"));
|
||||
}
|
||||
|
||||
// Ensure sync thread is started
|
||||
ractor::cast!(
|
||||
manager,
|
||||
MatrixManagerMsg::StartSyncThread(client.auth.user.email.clone())
|
||||
)
|
||||
.expect("Failed to start sync thread prior to running WebSocket!");
|
||||
|
||||
let rx = tx.subscribe();
|
||||
|
||||
let (res, session, msg_stream) = actix_ws::handle(&req, stream)?;
|
||||
|
||||
// spawn websocket handler (and don't await it) so that the response is returned immediately
|
||||
actix_web::rt::spawn(ws_handler(
|
||||
session,
|
||||
msg_stream,
|
||||
client.auth,
|
||||
client.client,
|
||||
rx,
|
||||
));
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn ws_handler(
|
||||
mut session: actix_ws::Session,
|
||||
mut msg_stream: actix_ws::MessageStream,
|
||||
auth: AuthExtractor,
|
||||
client: MatrixClient,
|
||||
mut rx: Receiver<BroadcastMessage>,
|
||||
) {
|
||||
log::info!(
|
||||
"WS connected for user {:?} / auth method={}",
|
||||
client.email,
|
||||
auth.method.light_str()
|
||||
);
|
||||
|
||||
let mut last_heartbeat = Instant::now();
|
||||
let mut interval = interval(constants::WS_HEARTBEAT_INTERVAL);
|
||||
|
||||
let reason = loop {
|
||||
// waits for either `msg_stream` to receive a message from the client, the broadcast channel
|
||||
// to send a message, or the heartbeat interval timer to tick, yielding the value of
|
||||
// whichever one is ready first
|
||||
tokio::select! {
|
||||
ws_msg = rx.recv() => {
|
||||
let msg = match ws_msg {
|
||||
Ok(msg) => msg,
|
||||
Err(broadcast::error::RecvError::Closed) => break None,
|
||||
Err(broadcast::error::RecvError::Lagged(_)) => continue,
|
||||
};
|
||||
|
||||
match (&msg, WsMessage::from_bx_message(&msg, &auth.user.email)) {
|
||||
(BroadcastMessage::APITokenDeleted(t), _) => {
|
||||
match &auth.method{
|
||||
AuthenticatedMethod::Token(tok) if tok.id == t.id => {
|
||||
log::info!(
|
||||
"closing WS session of user {:?} as associated token was deleted {:?}",
|
||||
client.email,
|
||||
t.base.name
|
||||
);
|
||||
break None;
|
||||
}
|
||||
_=>{}
|
||||
}
|
||||
|
||||
},
|
||||
(BroadcastMessage::UserDisconnectedFromMatrix(mail), _) if mail == &auth.user.email => {
|
||||
log::info!(
|
||||
"closing WS session of user {mail:?} as user was disconnected from Matrix"
|
||||
);
|
||||
break None;
|
||||
}
|
||||
|
||||
(_, Some(message)) => {
|
||||
// Send the message to the websocket
|
||||
if let Ok(msg) = serde_json::to_string(&message)
|
||||
&& let Err(e) = session.text(msg).await {
|
||||
log::error!("Failed to send SyncEvent: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
// heartbeat interval ticked
|
||||
_tick = interval.tick() => {
|
||||
// if no heartbeat ping/pong received recently, close the connection
|
||||
if Instant::now().duration_since(last_heartbeat) > constants::WS_CLIENT_TIMEOUT {
|
||||
log::info!(
|
||||
"client has not sent heartbeat in over {:?}; disconnecting",constants::WS_CLIENT_TIMEOUT
|
||||
);
|
||||
|
||||
break None;
|
||||
}
|
||||
|
||||
// send heartbeat ping
|
||||
let _ = session.ping(b"").await;
|
||||
},
|
||||
|
||||
// Websocket messages
|
||||
msg = msg_stream.next() => {
|
||||
let msg = match msg {
|
||||
// received message from WebSocket client
|
||||
Some(Ok(msg)) => msg,
|
||||
|
||||
// client WebSocket stream error
|
||||
Some(Err(err)) => {
|
||||
log::error!("{err}");
|
||||
break None;
|
||||
}
|
||||
|
||||
// client WebSocket stream ended
|
||||
None => break None
|
||||
};
|
||||
|
||||
log::debug!("msg: {msg:?}");
|
||||
|
||||
match msg {
|
||||
Message::Text(s) => {
|
||||
log::info!("Text message from WS: {s}");
|
||||
}
|
||||
|
||||
Message::Binary(_) => {
|
||||
// drop client's binary messages
|
||||
}
|
||||
|
||||
Message::Close(reason) => {
|
||||
break reason;
|
||||
}
|
||||
|
||||
Message::Ping(bytes) => {
|
||||
last_heartbeat = Instant::now();
|
||||
let _ = session.pong(&bytes).await;
|
||||
}
|
||||
|
||||
Message::Pong(_) => {
|
||||
last_heartbeat = Instant::now();
|
||||
}
|
||||
|
||||
Message::Continuation(_) => {
|
||||
log::warn!("no support for continuation frames");
|
||||
}
|
||||
|
||||
// no-op; ignore
|
||||
Message::Nop => {}
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// attempt to close connection gracefully
|
||||
let _ = session.close(reason).await;
|
||||
|
||||
log::info!("WS disconnected for user {:?}", client.email);
|
||||
}
|
||||
334
matrixgw_backend/src/extractors/auth_extractor.rs
Normal file
334
matrixgw_backend/src/extractors/auth_extractor.rs
Normal file
@@ -0,0 +1,334 @@
|
||||
use crate::app_config::AppConfig;
|
||||
use crate::constants;
|
||||
use crate::extractors::session_extractor::MatrixGWSession;
|
||||
use crate::users::{APIToken, APITokenID, User, UserEmail};
|
||||
use crate::utils::time_utils::time_secs;
|
||||
use actix_remote_ip::RemoteIP;
|
||||
use actix_web::dev::Payload;
|
||||
use actix_web::error::ErrorPreconditionFailed;
|
||||
use actix_web::{FromRequest, HttpRequest};
|
||||
use anyhow::Context;
|
||||
use bytes::Bytes;
|
||||
use jwt_simple::common::VerificationOptions;
|
||||
use jwt_simple::prelude::{Duration, HS256Key, MACLike};
|
||||
use jwt_simple::reexports::serde_json;
|
||||
use serde::de::DeserializeOwned;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::fmt::Display;
|
||||
use std::net::IpAddr;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AuthenticatedMethod {
|
||||
/// User is authenticated using a cookie
|
||||
Cookie,
|
||||
/// User is authenticated through command line, for debugging purposes only
|
||||
Dev,
|
||||
/// User is authenticated using an API token
|
||||
Token(APIToken),
|
||||
}
|
||||
|
||||
impl AuthenticatedMethod {
|
||||
pub fn light_str(&self) -> String {
|
||||
match self {
|
||||
AuthenticatedMethod::Cookie => "Cookie".to_string(),
|
||||
AuthenticatedMethod::Dev => "DevAuthentication".to_string(),
|
||||
AuthenticatedMethod::Token(t) => format!("Token({:?} - {})", t.id, t.base.name),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AuthExtractor {
|
||||
pub user: User,
|
||||
pub method: AuthenticatedMethod,
|
||||
pub payload: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl AsRef<User> for AuthExtractor {
|
||||
fn as_ref(&self) -> &User {
|
||||
&self.user
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthExtractor {
|
||||
pub fn decode_json_body<E: DeserializeOwned + Send>(&self) -> anyhow::Result<E> {
|
||||
let payload = self
|
||||
.payload
|
||||
.as_ref()
|
||||
.context("Failed to decode request as json: missing payload!")?;
|
||||
Ok(serde_json::from_slice(payload)?)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub struct MatrixJWTKID {
|
||||
pub user_email: UserEmail,
|
||||
pub id: APITokenID,
|
||||
}
|
||||
|
||||
impl Display for MatrixJWTKID {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}#{}", self.user_email.0, self.id.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for MatrixJWTKID {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let (mail, token_id) = s
|
||||
.split_once("#")
|
||||
.context("Failed to decode KID in two parts!")?;
|
||||
|
||||
let mail = UserEmail(mail.to_string());
|
||||
|
||||
if !mail.is_valid() {
|
||||
anyhow::bail!("Given email is invalid!")
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
user_email: mail,
|
||||
id: token_id.parse().context("Failed to parse API token ID")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct TokenClaims {
|
||||
#[serde(rename = "met")]
|
||||
pub method: String,
|
||||
pub uri: String,
|
||||
#[serde(rename = "pay", skip_serializing_if = "Option::is_none")]
|
||||
pub payload_sha256: Option<String>,
|
||||
}
|
||||
|
||||
impl AuthExtractor {
|
||||
async fn extract_auth(
|
||||
req: &HttpRequest,
|
||||
remote_ip: IpAddr,
|
||||
payload_bytes: Option<Bytes>,
|
||||
) -> Result<Self, actix_web::Error> {
|
||||
// Check for authentication using API token
|
||||
if let Some(token) = req.headers().get(constants::API_AUTH_HEADER) {
|
||||
let Ok(jwt_token) = token.to_str() else {
|
||||
return Err(actix_web::error::ErrorBadRequest(
|
||||
"Failed to decode token as string!",
|
||||
));
|
||||
};
|
||||
|
||||
let metadata = match jwt_simple::token::Token::decode_metadata(jwt_token) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
log::error!("Failed to decode JWT header metadata! {e}");
|
||||
return Err(actix_web::error::ErrorBadRequest(
|
||||
"Failed to decode JWT header metadata!",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Extract token ID
|
||||
let Some(kid) = metadata.key_id() else {
|
||||
return Err(actix_web::error::ErrorBadRequest(
|
||||
"Missing key id in request!",
|
||||
));
|
||||
};
|
||||
|
||||
let jwt_kid = match MatrixJWTKID::from_str(kid) {
|
||||
Ok(i) => i,
|
||||
Err(e) => {
|
||||
log::error!("Failed to parse token id! {e}");
|
||||
return Err(actix_web::error::ErrorBadRequest(
|
||||
"Failed to parse token id!",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Get token information
|
||||
let Ok(mut token) = APIToken::load(&jwt_kid.user_email, &jwt_kid.id).await else {
|
||||
log::error!("Token not found!");
|
||||
return Err(actix_web::error::ErrorForbidden("Token not found!"));
|
||||
};
|
||||
|
||||
// Decode JWT
|
||||
let key = HS256Key::from_bytes(token.secret.as_ref());
|
||||
let verif = VerificationOptions {
|
||||
max_validity: Some(Duration::from_secs(constants::API_TOKEN_JWT_MAX_DURATION)),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let claims = match key.verify_token::<TokenClaims>(jwt_token, Some(verif)) {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
log::error!("JWT validation failed! {e}");
|
||||
return Err(actix_web::error::ErrorForbidden("JWT validation failed!"));
|
||||
}
|
||||
};
|
||||
|
||||
// Check for nonce
|
||||
if claims.nonce.is_none() {
|
||||
return Err(actix_web::error::ErrorBadRequest(
|
||||
"A nonce is required in auth JWT!",
|
||||
));
|
||||
}
|
||||
|
||||
// Check IP restriction
|
||||
if let Some(nets) = &token.base.networks
|
||||
&& !nets.is_empty()
|
||||
&& !nets.iter().any(|n| n.contains(&remote_ip))
|
||||
{
|
||||
log::error!(
|
||||
"Trying to use token {:?} from unauthorized IP address: {remote_ip:?}",
|
||||
token.id
|
||||
);
|
||||
return Err(actix_web::error::ErrorForbidden(
|
||||
"This token cannot be used from this IP address!",
|
||||
));
|
||||
}
|
||||
|
||||
// Check for write access
|
||||
if token.base.read_only && !req.method().is_safe() {
|
||||
return Err(actix_web::error::ErrorBadRequest(
|
||||
"Read only token cannot perform write operations!",
|
||||
));
|
||||
}
|
||||
|
||||
// Get user information
|
||||
let Ok(user) = User::get_by_mail(&jwt_kid.user_email).await else {
|
||||
return Err(actix_web::error::ErrorBadRequest(
|
||||
"Failed to get user information from token!",
|
||||
));
|
||||
};
|
||||
|
||||
// Update last use (if needed)
|
||||
if token.shall_update_time_used() {
|
||||
token.last_used = time_secs();
|
||||
if let Err(e) = token.write(&jwt_kid.user_email).await {
|
||||
log::error!("Failed to refresh last usage of token! {e}");
|
||||
}
|
||||
}
|
||||
|
||||
// Handle tokens expiration
|
||||
if token.is_expired() {
|
||||
log::error!("Attempted to use expired token! {token:?}");
|
||||
return Err(actix_web::error::ErrorBadRequest("Token has expired!"));
|
||||
}
|
||||
|
||||
// Check payload
|
||||
let payload = match (payload_bytes, claims.custom.payload_sha256) {
|
||||
(None, _) => None,
|
||||
(Some(_), None) => {
|
||||
return Err(actix_web::error::ErrorBadRequest(
|
||||
"A payload digest must be included in the JWT when the request has a payload!",
|
||||
));
|
||||
}
|
||||
(Some(payload), Some(provided_digest)) => {
|
||||
let computed_digest = base16ct::lower::encode_string(&Sha256::digest(&payload));
|
||||
if computed_digest != provided_digest {
|
||||
log::error!(
|
||||
"Expected digest {provided_digest} for payload but computed {computed_digest}!"
|
||||
);
|
||||
return Err(actix_web::error::ErrorBadRequest(
|
||||
"Computed digest is different from the one provided in the JWT!",
|
||||
));
|
||||
}
|
||||
|
||||
Some(payload.to_vec())
|
||||
}
|
||||
};
|
||||
|
||||
return Ok(Self {
|
||||
method: AuthenticatedMethod::Token(token),
|
||||
user,
|
||||
payload,
|
||||
});
|
||||
}
|
||||
|
||||
// Check if login is hard-coded as program argument
|
||||
if let Some(email) = &AppConfig::get().unsecure_auto_login_email() {
|
||||
let user = User::get_by_mail(email).await.map_err(|e| {
|
||||
log::error!("Failed to retrieve dev user: {e}");
|
||||
ErrorPreconditionFailed("Unable to retrieve dev user!")
|
||||
})?;
|
||||
return Ok(Self {
|
||||
method: AuthenticatedMethod::Dev,
|
||||
user,
|
||||
payload: payload_bytes.map(|bytes| bytes.to_vec()),
|
||||
});
|
||||
}
|
||||
|
||||
// Check for cookie authentication
|
||||
let session = MatrixGWSession::extract(req).await?;
|
||||
if let Some(mail) = session.current_user().map_err(|e| {
|
||||
log::error!("Failed to retrieve user id: {e}");
|
||||
ErrorPreconditionFailed("Failed to read session information!")
|
||||
})? {
|
||||
let user = User::get_by_mail(&mail).await.map_err(|e| {
|
||||
log::error!("Failed to retrieve user from cookie session: {e}");
|
||||
ErrorPreconditionFailed("Failed to retrieve user information!")
|
||||
})?;
|
||||
return Ok(Self {
|
||||
method: AuthenticatedMethod::Cookie,
|
||||
user,
|
||||
payload: payload_bytes.map(|bytes| bytes.to_vec()),
|
||||
});
|
||||
};
|
||||
|
||||
Err(ErrorPreconditionFailed("Authentication required!"))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRequest for AuthExtractor {
|
||||
type Error = actix_web::Error;
|
||||
type Future = futures_util::future::LocalBoxFuture<'static, Result<Self, Self::Error>>;
|
||||
|
||||
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
|
||||
let req = req.clone();
|
||||
|
||||
let remote_ip = match RemoteIP::from_request(&req, &mut Payload::None).into_inner() {
|
||||
Ok(ip) => ip,
|
||||
Err(e) => return Box::pin(async { Err(e) }),
|
||||
};
|
||||
|
||||
let mut payload = payload.take();
|
||||
|
||||
Box::pin(async move {
|
||||
let payload_bytes = match Bytes::from_request(&req, &mut payload).await {
|
||||
Ok(b) => {
|
||||
if b.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(b)
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to extract request payload! {e}");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
Self::extract_auth(&req, remote_ip.0, payload_bytes).await
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::extractors::auth_extractor::MatrixJWTKID;
|
||||
use crate::users::{APITokenID, UserEmail};
|
||||
use std::str::FromStr;
|
||||
|
||||
#[test]
|
||||
fn encode_decode_jwt_kid() {
|
||||
let src = MatrixJWTKID {
|
||||
user_email: UserEmail("test@mail.com".to_string()),
|
||||
id: APITokenID::default(),
|
||||
};
|
||||
let encoded = src.to_string();
|
||||
let decoded = encoded.parse::<MatrixJWTKID>().unwrap();
|
||||
assert_eq!(src, decoded);
|
||||
|
||||
MatrixJWTKID::from_str("bad").unwrap_err();
|
||||
MatrixJWTKID::from_str("ba#d").unwrap_err();
|
||||
MatrixJWTKID::from_str("test@valid.com#d").unwrap_err();
|
||||
}
|
||||
}
|
||||
54
matrixgw_backend/src/extractors/matrix_client_extractor.rs
Normal file
54
matrixgw_backend/src/extractors/matrix_client_extractor.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use crate::extractors::auth_extractor::AuthExtractor;
|
||||
use crate::matrix_connection::matrix_client::MatrixClient;
|
||||
use crate::matrix_connection::matrix_manager::MatrixManagerMsg;
|
||||
use crate::users::ExtendedUserInfo;
|
||||
use actix_web::dev::Payload;
|
||||
use actix_web::{FromRequest, HttpRequest, web};
|
||||
use ractor::ActorRef;
|
||||
|
||||
pub struct MatrixClientExtractor {
|
||||
pub auth: AuthExtractor,
|
||||
pub client: MatrixClient,
|
||||
}
|
||||
|
||||
impl MatrixClientExtractor {
|
||||
pub async fn to_extended_user_info(&self) -> anyhow::Result<ExtendedUserInfo> {
|
||||
Ok(ExtendedUserInfo {
|
||||
user: self.auth.user.clone(),
|
||||
matrix_account_connected: self.client.is_client_connected(),
|
||||
matrix_user_id: self.client.user_id().map(|id| id.to_string()),
|
||||
matrix_device_id: self.client.device_id().map(|id| id.to_string()),
|
||||
matrix_recovery_state: self.client.recovery_state(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRequest for MatrixClientExtractor {
|
||||
type Error = actix_web::Error;
|
||||
type Future = futures_util::future::LocalBoxFuture<'static, Result<Self, Self::Error>>;
|
||||
|
||||
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
|
||||
let req = req.clone();
|
||||
let mut payload = payload.take();
|
||||
Box::pin(async move {
|
||||
let auth = AuthExtractor::from_request(&req, &mut payload).await?;
|
||||
|
||||
let matrix_manager_actor =
|
||||
web::Data::<ActorRef<MatrixManagerMsg>>::from_request(&req, &mut Payload::None)
|
||||
.await?;
|
||||
let client = ractor::call!(
|
||||
matrix_manager_actor,
|
||||
MatrixManagerMsg::GetClient,
|
||||
auth.user.email.clone()
|
||||
);
|
||||
|
||||
let client = match client {
|
||||
Ok(Ok(client)) => client,
|
||||
Ok(Err(err)) => panic!("Failed to get client! {err:?}"),
|
||||
Err(err) => panic!("Failed to query manager actor! {err:#?}"),
|
||||
};
|
||||
|
||||
Ok(Self { auth, client })
|
||||
})
|
||||
}
|
||||
}
|
||||
3
matrixgw_backend/src/extractors/mod.rs
Normal file
3
matrixgw_backend/src/extractors/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod auth_extractor;
|
||||
pub mod matrix_client_extractor;
|
||||
pub mod session_extractor;
|
||||
91
matrixgw_backend/src/extractors/session_extractor.rs
Normal file
91
matrixgw_backend/src/extractors/session_extractor.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
use crate::constants;
|
||||
use crate::users::{User, UserEmail};
|
||||
use crate::utils::rand_utils::rand_string;
|
||||
use actix_session::Session;
|
||||
use actix_web::dev::Payload;
|
||||
use actix_web::{Error, FromRequest, HttpRequest};
|
||||
use futures_util::future::{Ready, ready};
|
||||
use std::net::IpAddr;
|
||||
|
||||
/// Matrix Gateway session errors
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
enum MatrixGWSessionError {
|
||||
#[error("Missing state!")]
|
||||
OIDCMissingState,
|
||||
#[error("Missing IP address!")]
|
||||
OIDCMissingIP,
|
||||
#[error("Invalid state!")]
|
||||
OIDCInvalidState,
|
||||
#[error("Invalid IP address!")]
|
||||
OIDCInvalidIP,
|
||||
}
|
||||
|
||||
/// Matrix Gateway session
|
||||
///
|
||||
/// Basic wrapper around actix-session extractor
|
||||
pub struct MatrixGWSession(Session);
|
||||
|
||||
impl MatrixGWSession {
|
||||
/// Generate OpenID state for this session
|
||||
pub fn gen_oidc_state(&self, ip: IpAddr) -> anyhow::Result<String> {
|
||||
let random_string = rand_string(50);
|
||||
self.0
|
||||
.insert(constants::sessions::OIDC_STATE_KEY, random_string.clone())?;
|
||||
self.0.insert(constants::sessions::OIDC_REMOTE_IP, ip)?;
|
||||
Ok(random_string)
|
||||
}
|
||||
|
||||
/// Validate OpenID state
|
||||
pub fn validate_state(&self, state: &str, ip: IpAddr) -> anyhow::Result<()> {
|
||||
let session_state: String = self
|
||||
.0
|
||||
.get(constants::sessions::OIDC_STATE_KEY)?
|
||||
.ok_or(MatrixGWSessionError::OIDCMissingState)?;
|
||||
|
||||
let session_ip: IpAddr = self
|
||||
.0
|
||||
.get(constants::sessions::OIDC_REMOTE_IP)?
|
||||
.ok_or(MatrixGWSessionError::OIDCMissingIP)?;
|
||||
|
||||
if session_state != state {
|
||||
return Err(anyhow::anyhow!(MatrixGWSessionError::OIDCInvalidState));
|
||||
}
|
||||
|
||||
if session_ip != ip {
|
||||
return Err(anyhow::anyhow!(MatrixGWSessionError::OIDCInvalidIP));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set current user
|
||||
pub fn set_user(&self, user: &User) -> anyhow::Result<()> {
|
||||
self.0.insert(constants::sessions::USER_ID, &user.email)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get current user
|
||||
pub fn current_user(&self) -> anyhow::Result<Option<UserEmail>> {
|
||||
Ok(self.0.get(constants::sessions::USER_ID)?)
|
||||
}
|
||||
|
||||
/// Remove defined user
|
||||
pub fn unset_current_user(&self) -> anyhow::Result<()> {
|
||||
self.0.remove(constants::sessions::USER_ID);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRequest for MatrixGWSession {
|
||||
type Error = Error;
|
||||
type Future = Ready<Result<Self, Error>>;
|
||||
|
||||
#[inline]
|
||||
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
|
||||
ready(
|
||||
Session::from_request(req, &mut Payload::None)
|
||||
.into_inner()
|
||||
.map(MatrixGWSession),
|
||||
)
|
||||
}
|
||||
}
|
||||
8
matrixgw_backend/src/lib.rs
Normal file
8
matrixgw_backend/src/lib.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
pub mod app_config;
|
||||
pub mod broadcast_messages;
|
||||
pub mod constants;
|
||||
pub mod controllers;
|
||||
pub mod extractors;
|
||||
pub mod matrix_connection;
|
||||
pub mod users;
|
||||
pub mod utils;
|
||||
222
matrixgw_backend/src/main.rs
Normal file
222
matrixgw_backend/src/main.rs
Normal file
@@ -0,0 +1,222 @@
|
||||
use actix_cors::Cors;
|
||||
use actix_remote_ip::RemoteIPConfig;
|
||||
use actix_session::SessionMiddleware;
|
||||
use actix_session::config::SessionLifecycle;
|
||||
use actix_session::storage::RedisSessionStore;
|
||||
use actix_web::cookie::Key;
|
||||
use actix_web::middleware::Logger;
|
||||
use actix_web::{App, HttpServer, web};
|
||||
use matrixgw_backend::app_config::AppConfig;
|
||||
use matrixgw_backend::broadcast_messages::BroadcastMessage;
|
||||
use matrixgw_backend::constants;
|
||||
use matrixgw_backend::controllers::matrix::{
|
||||
matrix_event_controller, matrix_media_controller, matrix_profile_controller,
|
||||
matrix_room_controller,
|
||||
};
|
||||
use matrixgw_backend::controllers::{
|
||||
auth_controller, matrix_link_controller, matrix_sync_thread_controller, server_controller,
|
||||
static_controller, tokens_controller, ws_controller,
|
||||
};
|
||||
use matrixgw_backend::matrix_connection::matrix_manager::MatrixManagerActor;
|
||||
use matrixgw_backend::users::User;
|
||||
use ractor::Actor;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
||||
|
||||
let secret_key = Key::from(AppConfig::get().secret().as_bytes());
|
||||
|
||||
log::info!("Connect to Redis session store...");
|
||||
let redis_store = RedisSessionStore::new(AppConfig::get().redis_connection_string())
|
||||
.await
|
||||
.expect("Failed to connect to Redis!");
|
||||
|
||||
let (ws_tx, _) = tokio::sync::broadcast::channel::<BroadcastMessage>(16);
|
||||
|
||||
// Auto create default account, if requested
|
||||
if let Some(mail) = &AppConfig::get().unsecure_auto_login_email() {
|
||||
User::create_or_update_user(mail, "Anonymous")
|
||||
.await
|
||||
.expect("Failed to create auto-login account!");
|
||||
}
|
||||
|
||||
// Create matrix clients manager actor
|
||||
let (manager_actor, manager_actor_handle) = Actor::spawn(
|
||||
Some("matrix-clients-manager".to_string()),
|
||||
MatrixManagerActor,
|
||||
ws_tx.clone(),
|
||||
)
|
||||
.await
|
||||
.expect("Failed to start Matrix manager actor!");
|
||||
|
||||
log::info!(
|
||||
"Starting to listen on {} for {}",
|
||||
AppConfig::get().listen_address,
|
||||
AppConfig::get().website_origin
|
||||
);
|
||||
|
||||
let manager_actor_clone = manager_actor.clone();
|
||||
HttpServer::new(move || {
|
||||
let session_mw = SessionMiddleware::builder(redis_store.clone(), secret_key.clone())
|
||||
.cookie_name("matrixgw-session".to_string())
|
||||
.session_lifecycle(SessionLifecycle::BrowserSession(Default::default()))
|
||||
.build();
|
||||
|
||||
let cors = Cors::default()
|
||||
.allowed_origin(&AppConfig::get().website_origin)
|
||||
.allowed_methods(["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"])
|
||||
.allowed_header(constants::API_AUTH_HEADER)
|
||||
.allow_any_header()
|
||||
.supports_credentials()
|
||||
.max_age(3600);
|
||||
|
||||
App::new()
|
||||
.wrap(Logger::default())
|
||||
.wrap(session_mw)
|
||||
.wrap(cors)
|
||||
.app_data(web::Data::new(manager_actor_clone.clone()))
|
||||
.app_data(web::Data::new(RemoteIPConfig {
|
||||
proxy: AppConfig::get().proxy_ip.clone(),
|
||||
}))
|
||||
.app_data(web::Data::new(ws_tx.clone()))
|
||||
// Server controller
|
||||
.route("/robots.txt", web::get().to(server_controller::robots_txt))
|
||||
.route(
|
||||
"/api/server/config",
|
||||
web::get().to(server_controller::config),
|
||||
)
|
||||
// Auth controller
|
||||
.route(
|
||||
"/api/auth/start_oidc",
|
||||
web::get().to(auth_controller::start_oidc),
|
||||
)
|
||||
.route(
|
||||
"/api/auth/finish_oidc",
|
||||
web::post().to(auth_controller::finish_oidc),
|
||||
)
|
||||
.route("/api/auth/info", web::get().to(auth_controller::auth_info))
|
||||
.route(
|
||||
"/api/auth/sign_out",
|
||||
web::get().to(auth_controller::sign_out),
|
||||
)
|
||||
// Matrix link controller
|
||||
.route(
|
||||
"/api/matrix_link/start_auth",
|
||||
web::post().to(matrix_link_controller::start_auth),
|
||||
)
|
||||
.route(
|
||||
"/api/matrix_link/finish_auth",
|
||||
web::post().to(matrix_link_controller::finish_auth),
|
||||
)
|
||||
.route(
|
||||
"/api/matrix_link/logout",
|
||||
web::post().to(matrix_link_controller::logout),
|
||||
)
|
||||
.route(
|
||||
"/api/matrix_link/set_recovery_key",
|
||||
web::post().to(matrix_link_controller::set_recovery_key),
|
||||
)
|
||||
// API Tokens controller
|
||||
.route("/api/token", web::post().to(tokens_controller::create))
|
||||
.route("/api/tokens", web::get().to(tokens_controller::get_list))
|
||||
.route(
|
||||
"/api/token/{id}",
|
||||
web::delete().to(tokens_controller::delete),
|
||||
)
|
||||
// Matrix synchronization controller
|
||||
.route(
|
||||
"/api/matrix_sync/start",
|
||||
web::post().to(matrix_sync_thread_controller::start_sync),
|
||||
)
|
||||
.route(
|
||||
"/api/matrix_sync/stop",
|
||||
web::post().to(matrix_sync_thread_controller::stop_sync),
|
||||
)
|
||||
.route(
|
||||
"/api/matrix_sync/status",
|
||||
web::get().to(matrix_sync_thread_controller::status),
|
||||
)
|
||||
.service(web::resource("/api/ws").route(web::get().to(ws_controller::ws)))
|
||||
// Matrix room controller
|
||||
.route(
|
||||
"/api/matrix/room/joined",
|
||||
web::get().to(matrix_room_controller::joined_rooms),
|
||||
)
|
||||
.route(
|
||||
"/api/matrix/room/joined_spaces",
|
||||
web::get().to(matrix_room_controller::get_joined_spaces),
|
||||
)
|
||||
.route(
|
||||
"/api/matrix/room/{room_id}",
|
||||
web::get().to(matrix_room_controller::single_room_info),
|
||||
)
|
||||
.route(
|
||||
"/api/matrix/room/{room_id}/avatar",
|
||||
web::get().to(matrix_room_controller::room_avatar),
|
||||
)
|
||||
.route(
|
||||
"/api/matrix/room/{room_id}/receipts",
|
||||
web::get().to(matrix_room_controller::receipts),
|
||||
)
|
||||
// Matrix profile controller
|
||||
.route(
|
||||
"/api/matrix/profile/{user_id}",
|
||||
web::get().to(matrix_profile_controller::get_profile),
|
||||
)
|
||||
.route(
|
||||
"/api/matrix/profile/get_multiple",
|
||||
web::post().to(matrix_profile_controller::get_multiple),
|
||||
)
|
||||
// Matrix events controller
|
||||
.route(
|
||||
"/api/matrix/room/{room_id}/events",
|
||||
web::get().to(matrix_event_controller::get_for_room),
|
||||
)
|
||||
.route(
|
||||
"/api/matrix/room/{room_id}/send_text_message",
|
||||
web::post().to(matrix_event_controller::send_text_message),
|
||||
)
|
||||
.route(
|
||||
"/api/matrix/room/{room_id}/event/{event_id}/set_text_content",
|
||||
web::post().to(matrix_event_controller::set_text_content),
|
||||
)
|
||||
.route(
|
||||
"/api/matrix/room/{room_id}/event/{event_id}/file",
|
||||
web::get().to(matrix_event_controller::event_file),
|
||||
)
|
||||
.route(
|
||||
"/api/matrix/room/{room_id}/event/{event_id}/react",
|
||||
web::post().to(matrix_event_controller::react_to_event),
|
||||
)
|
||||
.route(
|
||||
"/api/matrix/room/{room_id}/event/{event_id}",
|
||||
web::delete().to(matrix_event_controller::redact_event),
|
||||
)
|
||||
.route(
|
||||
"/api/matrix/room/{room_id}/event/{event_id}/receipt",
|
||||
web::post().to(matrix_event_controller::receipt),
|
||||
)
|
||||
// Matrix media controller
|
||||
.route(
|
||||
"/api/matrix/media/{mxc}",
|
||||
web::get().to(matrix_media_controller::serve_mxc_handler),
|
||||
)
|
||||
// Static assets
|
||||
.route("/", web::get().to(static_controller::root_index))
|
||||
.route(
|
||||
"/{tail:.*}",
|
||||
web::get().to(static_controller::serve_static_content),
|
||||
)
|
||||
})
|
||||
.workers(4)
|
||||
.bind(&AppConfig::get().listen_address)?
|
||||
.run()
|
||||
.await?;
|
||||
|
||||
// Terminate manager actor
|
||||
manager_actor.stop(None);
|
||||
manager_actor_handle.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
401
matrixgw_backend/src/matrix_connection/matrix_client.rs
Normal file
401
matrixgw_backend/src/matrix_connection/matrix_client.rs
Normal file
@@ -0,0 +1,401 @@
|
||||
use crate::app_config::AppConfig;
|
||||
use crate::matrix_connection::matrix_manager::MatrixManagerMsg;
|
||||
use crate::users::UserEmail;
|
||||
use crate::utils::rand_utils::rand_string;
|
||||
use anyhow::Context;
|
||||
use futures_util::Stream;
|
||||
use matrix_sdk::authentication::oauth::error::OAuthDiscoveryError;
|
||||
use matrix_sdk::authentication::oauth::{
|
||||
ClientId, OAuthError, OAuthSession, UrlOrQuery, UserSession,
|
||||
};
|
||||
use matrix_sdk::config::SyncSettings;
|
||||
use matrix_sdk::encryption::recovery::RecoveryState;
|
||||
use matrix_sdk::event_handler::{EventHandler, EventHandlerHandle, SyncEvent};
|
||||
use matrix_sdk::ruma::presence::PresenceState;
|
||||
use matrix_sdk::ruma::serde::Raw;
|
||||
use matrix_sdk::ruma::{DeviceId, UserId};
|
||||
use matrix_sdk::sync::SyncResponse;
|
||||
use matrix_sdk::{Client, ClientBuildError, SendOutsideWasm};
|
||||
use ractor::ActorRef;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::pin::Pin;
|
||||
use url::Url;
|
||||
|
||||
/// The full session to persist.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
struct StoredSession {
|
||||
/// The OAuth 2.0 user session.
|
||||
user_session: UserSession,
|
||||
|
||||
/// The OAuth 2.0 client ID.
|
||||
client_id: ClientId,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
|
||||
pub enum EncryptionRecoveryState {
|
||||
Unknown,
|
||||
Enabled,
|
||||
Disabled,
|
||||
Incomplete,
|
||||
}
|
||||
|
||||
/// Matrix Gateway session errors
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
enum MatrixClientError {
|
||||
#[error("Failed to destroy previous client data! {0}")]
|
||||
DestroyPreviousData(Box<MatrixClientError>),
|
||||
#[error("Failed to create Matrix database storage directory! {0}")]
|
||||
CreateMatrixDbDir(std::io::Error),
|
||||
#[error("Failed to create database passphrase! {0}")]
|
||||
CreateDbPassphrase(std::io::Error),
|
||||
#[error("Failed to read database passphrase! {0}")]
|
||||
ReadDbPassphrase(std::io::Error),
|
||||
#[error("Failed to build Matrix client! {0}")]
|
||||
BuildMatrixClient(ClientBuildError),
|
||||
#[error("Failed to clear Matrix session file! {0}")]
|
||||
ClearMatrixSessionFile(std::io::Error),
|
||||
#[error("Failed to clear Matrix database storage directory! {0}")]
|
||||
ClearMatrixDbDir(std::io::Error),
|
||||
#[error("Failed to remove database passphrase! {0}")]
|
||||
ClearDbPassphrase(std::io::Error),
|
||||
#[error("Failed to fetch server metadata! {0}")]
|
||||
FetchServerMetadata(OAuthDiscoveryError),
|
||||
#[error("Failed to load stored session! {0}")]
|
||||
LoadStoredSession(std::io::Error),
|
||||
#[error("Failed to decode stored session! {0}")]
|
||||
DecodeStoredSession(serde_json::Error),
|
||||
#[error("Failed to restore stored session! {0}")]
|
||||
RestoreSession(matrix_sdk::Error),
|
||||
#[error("Failed to parse auth redirect URL! {0}")]
|
||||
ParseAuthRedirectURL(url::ParseError),
|
||||
#[error("Failed to build auth request! {0}")]
|
||||
BuildAuthRequest(OAuthError),
|
||||
#[error("Failed to finalize authentication! {0}")]
|
||||
FinishLogin(matrix_sdk::Error),
|
||||
#[error("Failed to write session file! {0}")]
|
||||
WriteSessionFile(std::io::Error),
|
||||
#[error("Failed to rename device! {0}")]
|
||||
RenameDevice(matrix_sdk::HttpError),
|
||||
#[error("Failed to set recovery key! {0}")]
|
||||
SetRecoveryKey(matrix_sdk::encryption::recovery::RecoveryError),
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct FinishMatrixAuth {
|
||||
code: String,
|
||||
state: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MatrixClient {
|
||||
manager: ActorRef<MatrixManagerMsg>,
|
||||
pub email: UserEmail,
|
||||
pub client: Client,
|
||||
}
|
||||
|
||||
impl MatrixClient {
|
||||
/// Start to build Matrix client to initiate user authentication
|
||||
pub async fn build_client(
|
||||
manager: ActorRef<MatrixManagerMsg>,
|
||||
email: &UserEmail,
|
||||
) -> anyhow::Result<Self> {
|
||||
// Check if we are restoring a previous state
|
||||
let session_file_path = AppConfig::get().user_matrix_session_file_path(email);
|
||||
let is_restoring = session_file_path.is_file();
|
||||
if !is_restoring {
|
||||
Self::destroy_data(email).map_err(MatrixClientError::DestroyPreviousData)?;
|
||||
}
|
||||
|
||||
// Determine Matrix database path
|
||||
let db_path = AppConfig::get().user_matrix_db_path(email);
|
||||
std::fs::create_dir_all(&db_path).map_err(MatrixClientError::CreateMatrixDbDir)?;
|
||||
|
||||
// Generate or load passphrase
|
||||
let passphrase_path = AppConfig::get().user_matrix_passphrase_path(email);
|
||||
if !passphrase_path.exists() {
|
||||
std::fs::write(&passphrase_path, rand_string(32))
|
||||
.map_err(MatrixClientError::CreateDbPassphrase)?;
|
||||
}
|
||||
let passphrase = std::fs::read_to_string(passphrase_path)
|
||||
.map_err(MatrixClientError::ReadDbPassphrase)?;
|
||||
|
||||
let client = Client::builder()
|
||||
.homeserver_url(&AppConfig::get().matrix_homeserver)
|
||||
// Automatically refresh tokens if needed
|
||||
.handle_refresh_tokens()
|
||||
.sqlite_store(&db_path, Some(&passphrase))
|
||||
.build()
|
||||
.await
|
||||
.map_err(MatrixClientError::BuildMatrixClient)?;
|
||||
|
||||
let client = Self {
|
||||
manager,
|
||||
email: email.clone(),
|
||||
client,
|
||||
};
|
||||
|
||||
// Check metadata
|
||||
if !is_restoring {
|
||||
let oauth = client.client.oauth();
|
||||
let server_metadata = oauth
|
||||
.server_metadata()
|
||||
.await
|
||||
.map_err(MatrixClientError::FetchServerMetadata)?;
|
||||
log::info!("OAuth2 server issuer: {:?}", server_metadata.issuer);
|
||||
} else {
|
||||
let session: StoredSession = serde_json::from_str(
|
||||
std::fs::read_to_string(session_file_path)
|
||||
.map_err(MatrixClientError::LoadStoredSession)?
|
||||
.as_str(),
|
||||
)
|
||||
.map_err(MatrixClientError::DecodeStoredSession)?;
|
||||
|
||||
// Restore session
|
||||
client
|
||||
.client
|
||||
.restore_session(OAuthSession {
|
||||
client_id: session.client_id,
|
||||
user: session.user_session,
|
||||
})
|
||||
.await
|
||||
.map_err(MatrixClientError::RestoreSession)?;
|
||||
|
||||
// Wait for encryption tasks to complete
|
||||
client
|
||||
.client
|
||||
.encryption()
|
||||
.wait_for_e2ee_initialization_tasks()
|
||||
.await;
|
||||
|
||||
// Save stored session once
|
||||
client.save_stored_session().await?;
|
||||
}
|
||||
|
||||
// Automatically save session when token gets refreshed
|
||||
client.setup_background_session_save().await;
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
/// Destroy Matrix client related data
|
||||
fn destroy_data(email: &UserEmail) -> anyhow::Result<(), Box<MatrixClientError>> {
|
||||
let session_path = AppConfig::get().user_matrix_session_file_path(email);
|
||||
if session_path.is_file() {
|
||||
std::fs::remove_file(&session_path)
|
||||
.map_err(MatrixClientError::ClearMatrixSessionFile)?;
|
||||
}
|
||||
|
||||
let db_path = AppConfig::get().user_matrix_db_path(email);
|
||||
if db_path.is_dir() {
|
||||
std::fs::remove_dir_all(&db_path).map_err(MatrixClientError::ClearMatrixDbDir)?;
|
||||
}
|
||||
|
||||
let passphrase_path = AppConfig::get().user_matrix_passphrase_path(email);
|
||||
if passphrase_path.is_file() {
|
||||
std::fs::remove_file(passphrase_path).map_err(MatrixClientError::ClearDbPassphrase)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Initiate OAuth authentication
|
||||
pub async fn initiate_login(&self) -> anyhow::Result<Url> {
|
||||
let oauth = self.client.oauth();
|
||||
|
||||
let metadata = AppConfig::get().matrix_client_metadata();
|
||||
let client_metadata = Raw::new(&metadata).expect("Couldn't serialize client metadata");
|
||||
|
||||
let auth = oauth
|
||||
.login(
|
||||
Url::parse(&AppConfig::get().matrix_oauth_redirect_url())
|
||||
.map_err(MatrixClientError::ParseAuthRedirectURL)?,
|
||||
None,
|
||||
Some(client_metadata.into()),
|
||||
None,
|
||||
)
|
||||
.build()
|
||||
.await
|
||||
.map_err(MatrixClientError::BuildAuthRequest)?;
|
||||
|
||||
Ok(auth.url)
|
||||
}
|
||||
|
||||
/// Finish OAuth authentication
|
||||
pub async fn finish_login(&self, info: FinishMatrixAuth) -> anyhow::Result<()> {
|
||||
let oauth = self.client.oauth();
|
||||
oauth
|
||||
.finish_login(UrlOrQuery::Query(format!(
|
||||
"state={}&code={}",
|
||||
info.state, info.code
|
||||
)))
|
||||
.await
|
||||
.map_err(MatrixClientError::FinishLogin)?;
|
||||
|
||||
log::info!(
|
||||
"User successfully authenticated as {}!",
|
||||
self.client.user_id().unwrap()
|
||||
);
|
||||
|
||||
// Persist session tokens
|
||||
self.save_stored_session().await?;
|
||||
|
||||
// Rename created session to give it a more explicit name
|
||||
self.client
|
||||
.rename_device(
|
||||
self.client
|
||||
.session_meta()
|
||||
.context("Missing device ID!")?
|
||||
.device_id
|
||||
.as_ref(),
|
||||
&AppConfig::get().website_origin,
|
||||
)
|
||||
.await
|
||||
.map_err(MatrixClientError::RenameDevice)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Automatically persist session onto disk
|
||||
pub async fn setup_background_session_save(&self) {
|
||||
let this = self.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match this.client.subscribe_to_session_changes().recv().await {
|
||||
Ok(update) => match update {
|
||||
matrix_sdk::SessionChange::UnknownToken { soft_logout } => {
|
||||
log::warn!(
|
||||
"Received an unknown token error; soft logout? {soft_logout:?}"
|
||||
);
|
||||
if let Err(e) = this
|
||||
.manager
|
||||
.cast(MatrixManagerMsg::DisconnectClient(this.email))
|
||||
{
|
||||
log::warn!("Failed to propagate invalid token error: {e}");
|
||||
}
|
||||
break;
|
||||
}
|
||||
matrix_sdk::SessionChange::TokensRefreshed => {
|
||||
// The tokens have been refreshed, persist them to disk.
|
||||
if let Err(err) = this.save_stored_session().await {
|
||||
log::error!("Unable to store a session in the background: {err}");
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!("[!] Session change error: {e}");
|
||||
log::error!("Session change background service INTERRUPTED!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Update the session stored on the filesystem.
|
||||
async fn save_stored_session(&self) -> anyhow::Result<()> {
|
||||
log::debug!("Save the stored session for {:?}...", self.email);
|
||||
|
||||
let full_session = self
|
||||
.client
|
||||
.oauth()
|
||||
.full_session()
|
||||
.context("A logged in client must have a session")?;
|
||||
|
||||
let stored_session = StoredSession {
|
||||
user_session: full_session.user,
|
||||
client_id: full_session.client_id,
|
||||
};
|
||||
|
||||
let serialized_session = serde_json::to_string(&stored_session)?;
|
||||
std::fs::write(
|
||||
AppConfig::get().user_matrix_session_file_path(&self.email),
|
||||
serialized_session,
|
||||
)
|
||||
.map_err(MatrixClientError::WriteSessionFile)?;
|
||||
|
||||
log::debug!("Updating the stored session: done!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check whether a user is currently connected to this client or not
|
||||
pub fn is_client_connected(&self) -> bool {
|
||||
self.client.is_active()
|
||||
}
|
||||
|
||||
/// Disconnect user from client
|
||||
pub async fn disconnect(self) -> anyhow::Result<()> {
|
||||
if let Err(e) = self.client.logout().await {
|
||||
log::warn!("Failed to send logout request: {e}");
|
||||
}
|
||||
|
||||
// Destroy user associated data
|
||||
Self::destroy_data(&self.email)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get client Matrix device id
|
||||
pub fn device_id(&self) -> Option<&DeviceId> {
|
||||
self.client.device_id()
|
||||
}
|
||||
|
||||
/// Get client Matrix user id
|
||||
pub fn user_id(&self) -> Option<&UserId> {
|
||||
self.client.user_id()
|
||||
}
|
||||
|
||||
/// Get current encryption keys recovery state
|
||||
pub fn recovery_state(&self) -> EncryptionRecoveryState {
|
||||
match self.client.encryption().recovery().state() {
|
||||
RecoveryState::Unknown => EncryptionRecoveryState::Unknown,
|
||||
RecoveryState::Enabled => EncryptionRecoveryState::Enabled,
|
||||
RecoveryState::Disabled => EncryptionRecoveryState::Disabled,
|
||||
RecoveryState::Incomplete => EncryptionRecoveryState::Incomplete,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set new encryption key recovery key
|
||||
pub async fn set_recovery_key(&self, key: &str) -> anyhow::Result<()> {
|
||||
Ok(self
|
||||
.client
|
||||
.encryption()
|
||||
.recovery()
|
||||
.recover(key)
|
||||
.await
|
||||
.map_err(MatrixClientError::SetRecoveryKey)?)
|
||||
}
|
||||
|
||||
/// Get matrix synchronization settings to use
|
||||
fn sync_settings() -> SyncSettings {
|
||||
SyncSettings::default().set_presence(PresenceState::Offline)
|
||||
}
|
||||
|
||||
/// Perform initial synchronization
|
||||
pub async fn perform_initial_sync(&self) -> anyhow::Result<()> {
|
||||
self.client.sync_once(Self::sync_settings()).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Perform routine synchronization
|
||||
pub async fn sync_stream(
|
||||
&self,
|
||||
) -> Pin<Box<impl Stream<Item = matrix_sdk::Result<SyncResponse>>>> {
|
||||
Box::pin(self.client.sync_stream(Self::sync_settings()).await)
|
||||
}
|
||||
|
||||
/// Add new Matrix event handler
|
||||
#[must_use]
|
||||
pub fn add_event_handler<Ev, Ctx, H>(&self, handler: H) -> EventHandlerHandle
|
||||
where
|
||||
Ev: SyncEvent + DeserializeOwned + SendOutsideWasm + 'static,
|
||||
H: EventHandler<Ev, Ctx>,
|
||||
{
|
||||
self.client.add_event_handler(handler)
|
||||
}
|
||||
|
||||
/// Remove Matrix event handler
|
||||
pub fn remove_event_handler(&self, handle: EventHandlerHandle) {
|
||||
self.client.remove_event_handler(handle)
|
||||
}
|
||||
}
|
||||
164
matrixgw_backend/src/matrix_connection/matrix_manager.rs
Normal file
164
matrixgw_backend/src/matrix_connection/matrix_manager.rs
Normal file
@@ -0,0 +1,164 @@
|
||||
use crate::broadcast_messages::{BroadcastMessage, BroadcastSender};
|
||||
use crate::matrix_connection::matrix_client::MatrixClient;
|
||||
use crate::matrix_connection::sync_thread::{MatrixSyncTaskID, start_sync_thread};
|
||||
use crate::users::UserEmail;
|
||||
use ractor::{Actor, ActorProcessingErr, ActorRef, RpcReplyPort};
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub struct MatrixManagerState {
|
||||
pub broadcast_sender: BroadcastSender,
|
||||
pub clients: HashMap<UserEmail, MatrixClient>,
|
||||
pub running_sync_threads: HashMap<UserEmail, MatrixSyncTaskID>,
|
||||
}
|
||||
|
||||
pub enum MatrixManagerMsg {
|
||||
GetClient(UserEmail, RpcReplyPort<anyhow::Result<MatrixClient>>),
|
||||
DisconnectClient(UserEmail),
|
||||
StartSyncThread(UserEmail),
|
||||
StopSyncThread(UserEmail),
|
||||
SyncThreadGetStatus(UserEmail, RpcReplyPort<bool>),
|
||||
SyncThreadTerminated(UserEmail, MatrixSyncTaskID),
|
||||
}
|
||||
|
||||
pub struct MatrixManagerActor;
|
||||
|
||||
impl Actor for MatrixManagerActor {
|
||||
type Msg = MatrixManagerMsg;
|
||||
type State = MatrixManagerState;
|
||||
type Arguments = BroadcastSender;
|
||||
|
||||
async fn pre_start(
|
||||
&self,
|
||||
_myself: ActorRef<Self::Msg>,
|
||||
args: Self::Arguments,
|
||||
) -> Result<Self::State, ActorProcessingErr> {
|
||||
Ok(MatrixManagerState {
|
||||
broadcast_sender: args,
|
||||
clients: HashMap::new(),
|
||||
running_sync_threads: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn post_stop(
|
||||
&self,
|
||||
_myself: ActorRef<Self::Msg>,
|
||||
_state: &mut Self::State,
|
||||
) -> Result<(), ActorProcessingErr> {
|
||||
log::error!("[!] [!] Matrix Manager Actor stopped!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
&self,
|
||||
myself: ActorRef<Self::Msg>,
|
||||
message: Self::Msg,
|
||||
state: &mut Self::State,
|
||||
) -> Result<(), ActorProcessingErr> {
|
||||
match message {
|
||||
// Get client information
|
||||
MatrixManagerMsg::GetClient(email, port) => {
|
||||
let res = port.send(match state.clients.get(&email) {
|
||||
None => {
|
||||
// Generate client if required
|
||||
log::info!("Building new client for {:?}", &email);
|
||||
match MatrixClient::build_client(myself, &email).await {
|
||||
Ok(c) => {
|
||||
state.clients.insert(email.clone(), c.clone());
|
||||
Ok(c)
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
Some(c) => Ok(c.clone()),
|
||||
});
|
||||
|
||||
if let Err(e) = res {
|
||||
log::warn!("Failed to send client information: {e}")
|
||||
}
|
||||
}
|
||||
MatrixManagerMsg::DisconnectClient(email) => {
|
||||
if let Some(c) = state.clients.remove(&email) {
|
||||
// Stop sync thread (if running)
|
||||
if let Some(id) = state.running_sync_threads.remove(&email) {
|
||||
state
|
||||
.broadcast_sender
|
||||
.send(BroadcastMessage::StopSyncThread(id))
|
||||
.ok();
|
||||
}
|
||||
|
||||
// Disconnect client
|
||||
if let Err(e) = c.disconnect().await {
|
||||
log::error!("Failed to disconnect client: {e}");
|
||||
}
|
||||
if let Err(e) = state
|
||||
.broadcast_sender
|
||||
.send(BroadcastMessage::UserDisconnectedFromMatrix(email))
|
||||
{
|
||||
log::warn!(
|
||||
"Failed to notify that user has been disconnected from Matrix! {e}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
MatrixManagerMsg::StartSyncThread(email) => {
|
||||
// Do nothing if task is already running
|
||||
if state.running_sync_threads.contains_key(&email) {
|
||||
log::debug!("Not starting sync thread for {email:?} as it is already running");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let Some(client) = state.clients.get(&email) else {
|
||||
log::warn!(
|
||||
"Cannot start sync thread for {email:?} because client is not initialized!"
|
||||
);
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if !client.is_client_connected() {
|
||||
log::warn!(
|
||||
"Cannot start sync thread for {email:?} because Matrix account is not set!"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Start thread
|
||||
log::debug!("Starting sync thread for {email:?}");
|
||||
let thread_id =
|
||||
match start_sync_thread(client.clone(), state.broadcast_sender.clone(), myself)
|
||||
.await
|
||||
{
|
||||
Ok(thread_id) => thread_id,
|
||||
Err(e) => {
|
||||
log::error!("Failed to start sync thread! {e}");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
state.running_sync_threads.insert(email, thread_id);
|
||||
}
|
||||
MatrixManagerMsg::StopSyncThread(email) => {
|
||||
if let Some(thread_id) = state.running_sync_threads.get(&email)
|
||||
&& let Err(e) = state
|
||||
.broadcast_sender
|
||||
.send(BroadcastMessage::StopSyncThread(thread_id.clone()))
|
||||
{
|
||||
log::error!("Failed to request sync thread stop: {e}");
|
||||
}
|
||||
}
|
||||
MatrixManagerMsg::SyncThreadGetStatus(email, reply) => {
|
||||
let started = state.running_sync_threads.contains_key(&email);
|
||||
if let Err(e) = reply.send(started) {
|
||||
log::error!("Failed to send sync thread status! {e}");
|
||||
}
|
||||
}
|
||||
MatrixManagerMsg::SyncThreadTerminated(email, task_id) => {
|
||||
if state.running_sync_threads.get(&email) == Some(&task_id) {
|
||||
log::info!(
|
||||
"Sync thread {task_id:?} has been terminated, removing it from the list..."
|
||||
);
|
||||
state.running_sync_threads.remove(&email);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
3
matrixgw_backend/src/matrix_connection/mod.rs
Normal file
3
matrixgw_backend/src/matrix_connection/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod matrix_client;
|
||||
pub mod matrix_manager;
|
||||
pub mod sync_thread;
|
||||
185
matrixgw_backend/src/matrix_connection/sync_thread.rs
Normal file
185
matrixgw_backend/src/matrix_connection/sync_thread.rs
Normal file
@@ -0,0 +1,185 @@
|
||||
//! # Matrix sync thread
|
||||
//!
|
||||
//! This file contains the logic performed by the threads that synchronize with Matrix account.
|
||||
|
||||
use crate::broadcast_messages::{BroadcastMessage, BroadcastSender, BxRoomEvent};
|
||||
use crate::matrix_connection::matrix_client::MatrixClient;
|
||||
use crate::matrix_connection::matrix_manager::MatrixManagerMsg;
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk::Room;
|
||||
use matrix_sdk::ruma::events::reaction::OriginalSyncReactionEvent;
|
||||
use matrix_sdk::ruma::events::receipt::SyncReceiptEvent;
|
||||
use matrix_sdk::ruma::events::room::message::OriginalSyncRoomMessageEvent;
|
||||
use matrix_sdk::ruma::events::room::redaction::OriginalSyncRoomRedactionEvent;
|
||||
use matrix_sdk::ruma::events::typing::SyncTypingEvent;
|
||||
use ractor::ActorRef;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct MatrixSyncTaskID(uuid::Uuid);
|
||||
|
||||
/// Start synchronization thread for a given user
|
||||
pub async fn start_sync_thread(
|
||||
client: MatrixClient,
|
||||
tx: BroadcastSender,
|
||||
manager: ActorRef<MatrixManagerMsg>,
|
||||
) -> anyhow::Result<MatrixSyncTaskID> {
|
||||
// Perform initial synchronization here, so in case of error the sync task does not get registered
|
||||
log::info!("Perform initial synchronization...");
|
||||
if let Err(e) = client.perform_initial_sync().await {
|
||||
log::error!("Failed to perform initial Matrix synchronization! {e:?}");
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
let task_id = MatrixSyncTaskID(uuid::Uuid::new_v4());
|
||||
let task_id_clone = task_id.clone();
|
||||
|
||||
tokio::task::spawn(async move {
|
||||
sync_thread_task(task_id_clone, client, tx, manager).await;
|
||||
});
|
||||
|
||||
Ok(task_id)
|
||||
}
|
||||
|
||||
/// Sync thread function for a single function
|
||||
async fn sync_thread_task(
|
||||
id: MatrixSyncTaskID,
|
||||
client: MatrixClient,
|
||||
tx: BroadcastSender,
|
||||
manager: ActorRef<MatrixManagerMsg>,
|
||||
) {
|
||||
let mut rx = tx.subscribe();
|
||||
log::info!("Sync thread {id:?} started for user {:?}", client.email);
|
||||
|
||||
let mut sync_stream = client.sync_stream().await;
|
||||
|
||||
let mut handlers = vec![];
|
||||
|
||||
let tx_msg_handle = tx.clone();
|
||||
let user_msg_mail = client.email.clone();
|
||||
handlers.push(client.add_event_handler(
|
||||
async move |event: OriginalSyncRoomMessageEvent, room: Room| {
|
||||
if let Err(e) = tx_msg_handle.send(BroadcastMessage::RoomMessageEvent(BxRoomEvent {
|
||||
user: user_msg_mail.clone(),
|
||||
data: Box::new(event),
|
||||
room,
|
||||
})) {
|
||||
log::warn!("Failed to forward room message event! {e}");
|
||||
}
|
||||
},
|
||||
));
|
||||
|
||||
let tx_reac_handle = tx.clone();
|
||||
let user_reac_mail = client.email.clone();
|
||||
handlers.push(client.add_event_handler(
|
||||
async move |event: OriginalSyncReactionEvent, room: Room| {
|
||||
if let Err(e) = tx_reac_handle.send(BroadcastMessage::ReactionEvent(BxRoomEvent {
|
||||
user: user_reac_mail.clone(),
|
||||
data: Box::new(event),
|
||||
room,
|
||||
})) {
|
||||
log::warn!("Failed to forward reaction event! {e}");
|
||||
}
|
||||
},
|
||||
));
|
||||
|
||||
let tx_redac_handle = tx.clone();
|
||||
let user_redac_mail = client.email.clone();
|
||||
handlers.push(client.add_event_handler(
|
||||
async move |event: OriginalSyncRoomRedactionEvent, room: Room| {
|
||||
if let Err(e) =
|
||||
tx_redac_handle.send(BroadcastMessage::RoomRedactionEvent(BxRoomEvent {
|
||||
user: user_redac_mail.clone(),
|
||||
data: Box::new(event),
|
||||
room,
|
||||
}))
|
||||
{
|
||||
log::warn!("Failed to forward redaction event! {e}");
|
||||
}
|
||||
},
|
||||
));
|
||||
|
||||
let tx_receipt_handle = tx.clone();
|
||||
let user_receipt_mail = client.email.clone();
|
||||
handlers.push(
|
||||
client.add_event_handler(async move |event: SyncReceiptEvent, room: Room| {
|
||||
if let Err(e) = tx_receipt_handle.send(BroadcastMessage::ReceiptEvent(BxRoomEvent {
|
||||
user: user_receipt_mail.clone(),
|
||||
data: Box::new(event),
|
||||
room,
|
||||
})) {
|
||||
log::warn!("Failed to forward receipt event! {e}");
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
let tx_typing_handle = tx.clone();
|
||||
let user_typing_mail = client.email.clone();
|
||||
handlers.push(
|
||||
client.add_event_handler(async move |event: SyncTypingEvent, room: Room| {
|
||||
if let Err(e) = tx_typing_handle.send(BroadcastMessage::TypingEvent(BxRoomEvent {
|
||||
user: user_typing_mail.clone(),
|
||||
data: Box::new(event),
|
||||
room,
|
||||
})) {
|
||||
log::warn!("Failed to forward typing event! {e}");
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
// Message from tokio broadcast
|
||||
msg = rx.recv() => {
|
||||
match msg {
|
||||
Ok(BroadcastMessage::StopSyncThread(task_id)) if task_id == id => {
|
||||
log::info!("A request was received to stop sync task! {id:?} for user {:?}", client.email);
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to receive a message from broadcast! {e}");
|
||||
break;
|
||||
}
|
||||
Ok(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
res = sync_stream.next() => {
|
||||
let Some(res)= res else {
|
||||
log::error!("No more Matrix event to process, stopping now...");
|
||||
break;
|
||||
};
|
||||
|
||||
// Forward message
|
||||
match res {
|
||||
Ok(res) => {
|
||||
if let Err(e)= tx.send(BroadcastMessage::MatrixSyncResponse {
|
||||
user: client.email.clone(),
|
||||
sync: res
|
||||
}) {
|
||||
log::warn!("Failed to forward room event! {e}");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Sync error for user {:?}! {e}", client.email);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for h in handlers {
|
||||
client.remove_event_handler(h);
|
||||
}
|
||||
|
||||
// Notify manager about termination, so this thread can be removed from the list
|
||||
log::info!("Sync thread {id:?} terminated!");
|
||||
if let Err(e) = ractor::cast!(
|
||||
manager,
|
||||
MatrixManagerMsg::SyncThreadTerminated(client.email.clone(), id.clone())
|
||||
) {
|
||||
log::error!("Failed to notify Matrix manager about thread termination! {e}");
|
||||
}
|
||||
if let Err(e) = tx.send(BroadcastMessage::SyncThreadStopped(id)) {
|
||||
log::warn!("Failed to notify that synchronization thread has been interrupted! {e}")
|
||||
}
|
||||
}
|
||||
288
matrixgw_backend/src/users.rs
Normal file
288
matrixgw_backend/src/users.rs
Normal file
@@ -0,0 +1,288 @@
|
||||
use crate::app_config::AppConfig;
|
||||
use crate::broadcast_messages::{BroadcastMessage, BroadcastSender};
|
||||
use crate::constants;
|
||||
use crate::controllers::server_controller::ServerConstraints;
|
||||
use crate::matrix_connection::matrix_client::EncryptionRecoveryState;
|
||||
use crate::utils::rand_utils::rand_string;
|
||||
use crate::utils::time_utils::time_secs;
|
||||
use anyhow::Context;
|
||||
use jwt_simple::reexports::serde_json;
|
||||
use std::cmp::min;
|
||||
use std::str::FromStr;
|
||||
|
||||
/// Matrix Gateway user errors
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
enum MatrixGWUserError {
|
||||
#[error("Failed to load user metadata: {0}")]
|
||||
LoadUserMetadata(std::io::Error),
|
||||
#[error("Failed to decode user metadata: {0}")]
|
||||
DecodeUserMetadata(serde_json::Error),
|
||||
#[error("Failed to save user metadata: {0}")]
|
||||
SaveUserMetadata(std::io::Error),
|
||||
#[error("Failed to create API token directory: {0}")]
|
||||
CreateApiTokensDirectory(std::io::Error),
|
||||
#[error("Failed to delete API token: {0}")]
|
||||
DeleteToken(std::io::Error),
|
||||
#[error("Failed to load API token: {0}")]
|
||||
LoadApiToken(std::io::Error),
|
||||
#[error("Failed to decode API token: {0}")]
|
||||
DecodeApiToken(serde_json::Error),
|
||||
#[error("API Token does not exists!")]
|
||||
ApiTokenDoesNotExists,
|
||||
#[error("Failed to save API token: {0}")]
|
||||
SaveAPIToken(std::io::Error),
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct UserEmail(pub String);
|
||||
|
||||
impl UserEmail {
|
||||
pub fn is_valid(&self) -> bool {
|
||||
mailchecker::is_valid(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq)]
|
||||
pub struct APITokenID(pub uuid::Uuid);
|
||||
|
||||
impl Default for APITokenID {
|
||||
fn default() -> Self {
|
||||
Self(uuid::Uuid::new_v4())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for APITokenID {
|
||||
type Err = uuid::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(Self(uuid::Uuid::from_str(s)?))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||
pub struct User {
|
||||
pub email: UserEmail,
|
||||
pub name: String,
|
||||
pub time_create: u64,
|
||||
pub last_login: u64,
|
||||
}
|
||||
|
||||
impl User {
|
||||
/// Get a user by its mail
|
||||
pub async fn get_by_mail(mail: &UserEmail) -> anyhow::Result<Self> {
|
||||
let path = AppConfig::get().user_metadata_file_path(mail);
|
||||
let data = std::fs::read_to_string(path).map_err(MatrixGWUserError::LoadUserMetadata)?;
|
||||
Ok(serde_json::from_str(&data).map_err(MatrixGWUserError::DecodeUserMetadata)?)
|
||||
}
|
||||
|
||||
/// Update user metadata on disk
|
||||
pub async fn write(&self) -> anyhow::Result<()> {
|
||||
let path = AppConfig::get().user_metadata_file_path(&self.email);
|
||||
std::fs::write(&path, serde_json::to_string(&self)?)
|
||||
.map_err(MatrixGWUserError::SaveUserMetadata)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create or update user information
|
||||
pub async fn create_or_update_user(mail: &UserEmail, name: &str) -> anyhow::Result<User> {
|
||||
let storage_dir = AppConfig::get().user_directory(mail);
|
||||
let mut user = if !storage_dir.exists() {
|
||||
std::fs::create_dir_all(storage_dir)?;
|
||||
|
||||
User {
|
||||
email: mail.clone(),
|
||||
name: name.to_string(),
|
||||
time_create: time_secs(),
|
||||
last_login: time_secs(),
|
||||
}
|
||||
} else {
|
||||
Self::get_by_mail(mail).await?
|
||||
};
|
||||
|
||||
// Update some user information
|
||||
user.name = name.to_string();
|
||||
user.last_login = time_secs();
|
||||
user.write().await?;
|
||||
|
||||
Ok(user)
|
||||
}
|
||||
}
|
||||
|
||||
/// Base API token information
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||
pub struct BaseAPIToken {
|
||||
/// Token name
|
||||
pub name: String,
|
||||
|
||||
/// Restricted API network for token
|
||||
pub networks: Option<Vec<ipnet::IpNet>>,
|
||||
|
||||
/// Token max inactivity
|
||||
pub max_inactivity: u32,
|
||||
|
||||
/// Token expiration
|
||||
pub expiration: Option<u64>,
|
||||
|
||||
/// Read only access
|
||||
pub read_only: bool,
|
||||
}
|
||||
|
||||
impl BaseAPIToken {
|
||||
/// Check API token information validity
|
||||
pub fn check(&self) -> Option<&'static str> {
|
||||
let constraints = ServerConstraints::default();
|
||||
|
||||
if !lazy_regex::regex!("^[a-zA-Z0-9 :-]+$").is_match(&self.name) {
|
||||
return Some("Token name contains invalid characters!");
|
||||
}
|
||||
|
||||
if !constraints.token_name.check_str(&self.name) {
|
||||
return Some("Invalid token name length!");
|
||||
}
|
||||
|
||||
if !constraints
|
||||
.token_max_inactivity
|
||||
.check_u32(self.max_inactivity)
|
||||
{
|
||||
return Some("Invalid token max inactivity!");
|
||||
}
|
||||
|
||||
if let Some(expiration) = self.expiration
|
||||
&& expiration <= time_secs()
|
||||
{
|
||||
return Some("Given expiration time is in the past!");
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Single API token information
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||
pub struct APIToken {
|
||||
#[serde(flatten)]
|
||||
pub base: BaseAPIToken,
|
||||
|
||||
/// Token unique ID
|
||||
pub id: APITokenID,
|
||||
|
||||
/// Client secret
|
||||
pub secret: String,
|
||||
|
||||
/// Client creation time
|
||||
pub created: u64,
|
||||
|
||||
/// Client last usage time
|
||||
pub last_used: u64,
|
||||
}
|
||||
|
||||
impl APIToken {
|
||||
/// Get the list of tokens of a user
|
||||
pub async fn list_user(email: &UserEmail) -> anyhow::Result<Vec<Self>> {
|
||||
let tokens_dir = AppConfig::get().user_api_token_directory(email);
|
||||
|
||||
if !tokens_dir.exists() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let mut list = vec![];
|
||||
for u in std::fs::read_dir(&tokens_dir)? {
|
||||
let entry = u?;
|
||||
list.push(
|
||||
Self::load(
|
||||
email,
|
||||
&APITokenID::from_str(
|
||||
entry
|
||||
.file_name()
|
||||
.to_str()
|
||||
.context("Cannot decode API Token ID as string!")?,
|
||||
)?,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
/// Create a new token
|
||||
pub async fn create(email: &UserEmail, base: BaseAPIToken) -> anyhow::Result<Self> {
|
||||
let tokens_dir = AppConfig::get().user_api_token_directory(email);
|
||||
|
||||
if !tokens_dir.exists() {
|
||||
std::fs::create_dir_all(tokens_dir)
|
||||
.map_err(MatrixGWUserError::CreateApiTokensDirectory)?;
|
||||
}
|
||||
|
||||
let token = APIToken {
|
||||
base,
|
||||
id: Default::default(),
|
||||
secret: rand_string(constants::TOKENS_LEN),
|
||||
created: time_secs(),
|
||||
last_used: time_secs(),
|
||||
};
|
||||
|
||||
token.write(email).await?;
|
||||
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
/// Get a token information
|
||||
pub async fn load(email: &UserEmail, id: &APITokenID) -> anyhow::Result<Self> {
|
||||
let token_file = AppConfig::get().user_api_token_metadata_file(email, id);
|
||||
match token_file.exists() {
|
||||
true => Ok(serde_json::from_str::<Self>(
|
||||
&std::fs::read_to_string(&token_file).map_err(MatrixGWUserError::LoadApiToken)?,
|
||||
)
|
||||
.map_err(MatrixGWUserError::DecodeApiToken)?),
|
||||
false => Err(MatrixGWUserError::ApiTokenDoesNotExists.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Write this token information
|
||||
pub async fn write(&self, mail: &UserEmail) -> anyhow::Result<()> {
|
||||
let path = AppConfig::get().user_api_token_metadata_file(mail, &self.id);
|
||||
std::fs::write(&path, serde_json::to_string(&self)?)
|
||||
.map_err(MatrixGWUserError::SaveAPIToken)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete this token
|
||||
pub async fn delete(self, email: &UserEmail, tx: &BroadcastSender) -> anyhow::Result<()> {
|
||||
let token_file = AppConfig::get().user_api_token_metadata_file(email, &self.id);
|
||||
std::fs::remove_file(&token_file).map_err(MatrixGWUserError::DeleteToken)?;
|
||||
|
||||
if let Err(e) = tx.send(BroadcastMessage::APITokenDeleted(self)) {
|
||||
log::error!("Failed to notify API token deletion! {e}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn shall_update_time_used(&self) -> bool {
|
||||
let refresh_interval = min(600, self.base.max_inactivity / 10);
|
||||
|
||||
(self.last_used) < time_secs() - refresh_interval as u64
|
||||
}
|
||||
|
||||
pub fn is_expired(&self) -> bool {
|
||||
// Check for hard coded expiration
|
||||
if let Some(exp_time) = self.base.expiration
|
||||
&& exp_time < time_secs()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Control max token inactivity
|
||||
(self.last_used + self.base.max_inactivity as u64) < time_secs()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Debug, Clone)]
|
||||
pub struct ExtendedUserInfo {
|
||||
#[serde(flatten)]
|
||||
pub user: User,
|
||||
pub matrix_account_connected: bool,
|
||||
pub matrix_user_id: Option<String>,
|
||||
pub matrix_device_id: Option<String>,
|
||||
pub matrix_recovery_state: EncryptionRecoveryState,
|
||||
}
|
||||
11
matrixgw_backend/src/utils/crypt_utils.rs
Normal file
11
matrixgw_backend/src/utils/crypt_utils.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
use sha2::{Digest, Sha256, Sha512};
|
||||
|
||||
/// Compute SHA256sum of a given string
|
||||
pub fn sha256str(input: &str) -> String {
|
||||
hex::encode(Sha256::digest(input.as_bytes()))
|
||||
}
|
||||
|
||||
/// Compute SHA256sum of a given byte array
|
||||
pub fn sha512(input: &[u8]) -> String {
|
||||
hex::encode(Sha512::digest(input))
|
||||
}
|
||||
3
matrixgw_backend/src/utils/mod.rs
Normal file
3
matrixgw_backend/src/utils/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod crypt_utils;
|
||||
pub mod rand_utils;
|
||||
pub mod time_utils;
|
||||
6
matrixgw_backend/src/utils/rand_utils.rs
Normal file
6
matrixgw_backend/src/utils/rand_utils.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
use rand::distr::{Alphanumeric, SampleString};
|
||||
|
||||
/// Generate a random string of a given length
|
||||
pub fn rand_string(len: usize) -> String {
|
||||
Alphanumeric.sample_string(&mut rand::rng(), len)
|
||||
}
|
||||
9
matrixgw_backend/src/utils/time_utils.rs
Normal file
9
matrixgw_backend/src/utils/time_utils.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
/// Get the current time since epoch
|
||||
pub fn time_secs() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs()
|
||||
}
|
||||
Reference in New Issue
Block a user