Update Rust crate rustls to v0.23.43 - autoclosed #4

Closed
rasensprenger wants to merge 0 commits from renovate/rustls-0.x-lockfile into main
51 changed files with 2327 additions and 5224 deletions

View file

@ -1,44 +0,0 @@
Dear Snap Data Protection Officer,
I am writing to exercise my right to erasure under Article 17 of the GDPR in relation to two Snapchat accounts that belong to me:
alexemmet
alex_emmet
I previously contacted Snapchat Support through X, formerly Twitter, because I was unable to identify a functioning general support email address or reach a human support representative through the available support channels.
In my message through X, I explained that I am requesting deletion of both accounts and the personal data associated with them.
I still have access to the login credentials for alexemmet and can verify ownership of that account through the normal account-access process.
For alex_emmet, I no longer have access to the old device, email address, or phone number that may have been associated with the account. The account was created when I was under 18.
Snapchat Support responded through X that it could not take action where a user does not know, or no longer has access to, the email address or phone number associated with an account.
I would like to clarify that I am not requesting account recovery, a password reset, or restored access to alex_emmet. I am making a data-subject request for erasure of my personal data.
I understand that Snap is entitled to verify my identity before acting on this request. Under Article 12(6) GDPR, where there are reasonable doubts concerning the identity of the person making a request, the controller may request additional information necessary to confirm the person's identity.
I am therefore asking Snap to provide an alternative means of verifying that I am the data subject associated with alex_emmet, given that I no longer have access to the original device, email address, or phone number. I am willing to provide reasonable information necessary for verification through a secure channel.
My request covers:
deletion of the Snapchat account alexemmet;
deletion of the Snapchat account alex_emmet;
erasure of personal data associated with both accounts;
erasure of personal data contained in previous support correspondence, support tickets, attachments, account-recovery records, and associated metadata concerning me.
If Snap considers that any of this data must be retained, please identify the categories of data being retained, the legal basis for retention, and the applicable retention period.
If Snap considers that it cannot comply with the erasure request for alex_emmet, please provide the specific reason for that decision and explain why no alternative method of verifying my identity is available.
I would also appreciate confirmation that this request is being handled as a GDPR data-subject request rather than as an account-recovery request.
I can provide screenshots of my correspondence with Snapchat Support through X if required.
Kind regards,
Alex

1
.gitignore vendored
View file

@ -27,7 +27,6 @@ target
# Added by cargo
/target
/certs
*.mk
*.mpkb

1417
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -4,31 +4,33 @@ version = "0.1.0"
edition = "2024"
[dependencies]
mtp = { git = "https://git.methanium.net/Methanium/mtp.git", features = [
mtp = { git = "https://git.methanium.net/methanium/mtp", features = [
"crypto",
"files",
"raw",
"web-server",
] }
ansi_term = "0.12.1"
base64 = "0.23.1"
base64 = "0.22.1"
bytes = "1"
dashmap = "6.2.1"
dotenv = "0.15.0"
http = "1"
once_cell = "1.21.4"
rand = "0.10.2"
rustls = { version = "0.23.43", default-features = false, features = [
reqwest = "0.13.4"
rustls = { version = "0.23.42", default-features = false, features = [
"std",
"tls12",
"aws-lc-rs",
"prefer-post-quantum",
] }
sqlx = { version = "0.9.0", features = ["mysql", "runtime-tokio", "migrate"] }
sqlx = { version = "0.8.6", features = ["mysql", "runtime-tokio", "migrate"] }
strum = "0.28.0"
strum_macros = "0.28.0"
tokio = { version = "*", features = ["full"] }
tokio-util = { version = "0.7.19", features = ["rt"] }
uuid = { version = "1.26.0", features = ["v4", "v7"] }
thiserror = "2.0.20"
uuid = { version = "1.24.0", features = ["v4", "v7"] }
zip = "8.6.0"
thiserror = "2.0.19"
serde = { version = "1.0.229", features = ["derive"] }
serde_json = "1.0.151"

View file

@ -1,15 +1,2 @@
# Omega
The Omega is Tensamin's central Server. It maintains the centralized user Registry & manages Omikron useage.
On first start Omega creates an unencrypted private keyring at `./omega.mk` and the matching public bundle at `./omega.mpkb`. Existing identity files are loaded fail-closed, so a malformed keyring or mismatched public bundle stops startup. A missing public bundle is rebuilt from a valid private keyring without generating a new identity.
## MTP routing contract
Omega preserves the MTP distinction between an absent routing field and a
present zero value on the wire. For Omega request and response operations,
both forms are rejected:
- request IDs are required and nonzero for correlation;
- authenticated sender and receiver IDs are required and nonzero;
- fields that are optional for a particular notification remain `Option` and
are not converted to zero.

29
certs/server_cert.pem Normal file
View file

@ -0,0 +1,29 @@
-----BEGIN CERTIFICATE-----
MIIFCTCCAvGgAwIBAgIUFzlX/gHUsBzyka/qanwJ7SaH++gwDQYJKoZIhvcNAQEL
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDcwNDE3MjM0MVoXDTI3MDcw
NDE3MjM0MVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF
AAOCAg8AMIICCgKCAgEAkbTCZUXRNWmomFxNwI6xElPDGSX5xM05GWoOAYqW3teu
QV2dvBgX2DSywlVURnb6E3T+W+pZNEO95cL3R50+qPpVNHsW2VUbpaPBlFJ6F6ni
Pecx9mHSdawUkUwQsWd0DvTJbC557KH8kIrlc9oJOpneu6sLOoaBHyl1sucFI3vb
2Gl6C7MUGuh8olfseKmi/u32wohF6nuDTZvdmCIyrcvD2JEAgSfTqszr+T4JyEpF
DotzviPZFVQ2aVBQhjPUyio5vpvrgq6cELCGn7DZ95HCrU752Gb4VABUs/pEYRaZ
jY6+3OXoMrUolsGKlsupXnWz53OTjTqLb7XxjKoyfA6RgdMhF0C43Bl4uYIQgLhA
dJ1XCj1E/m28dmHvc1oSDzaYwBRCyYhG5/5B1GIU5+zEHsu/MSsmXZXe7PJmoWDm
kaFoYjdVYwV3EMF4VnrWGS6eTPSu62afBQAXogdwLf1YiUJppGBGZVl5PkPjQC9D
jAK0o9cudabBnV/KkjM7pZO5BOho4jpZ0ob9C+au+ocgjIyaQ8SPKJEh7ViN5VMg
B8wwf3f0oOt7Ok1T07Gqv8J75f4ouOfIoGgPr43Z2kHz63ga8yLEdFI5n9gEjLJb
jEgFyvI/1lVPPcO76OhF+UpeiQFf6eUjAyGMGia3E5YS0mNA3ANuhnNxhLg7Z8EC
AwEAAaNTMFEwHQYDVR0OBBYEFKyeBGfcdE4UJ0LH90q361b8Tk7bMB8GA1UdIwQY
MBaAFKyeBGfcdE4UJ0LH90q361b8Tk7bMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI
hvcNAQELBQADggIBAAhDfGdBEe72cbtsULnQ19zXB9xMhoG7PQiZFCG+aNl4sJ6q
Ep+ULXyyN/02IRnvrTTVp0bixMPZw7y2lkP5NJA5f/cXVDPXhNtLtua1EPHH98AL
SXJoLOPJ1EDuj4u7x4jxWDCZdbZicab9b+kzfEPHx+bwcHvxKCOGTHfu/MzCAiye
aYoop4nguOtgxpVhLODOd1kJp06ieI74tySdhbc+q5VfgRXSl3UyOgrgXVYuteW+
kt+CoJrlKaZ+OK9N9jov+pNyriaccvXhrc5bc0s8QMwz4a3w8AJP0F4ubK4gYeUw
T3H1e0jqnWBy5yodhCjfyl3N7FVUHpMdBFdKvs3LUGiAdK7EhgpJd3pKdv8eGEB/
cli/LQgf57kEjp7OJgapMNcEEwTM8XUgTsDzjxH3enh40mqbGhCSiwoMGPF0fldq
z5hWydIe0SQv39EbpCqQ+MPqK8Dk3vSXwh+bvh9dmvnUrPls2bQ1WbBsUz7VnwP0
/RxgwhHCwDuNary6EinCLZZ1ZxDB4PMGfxcA8Zt4K7OOGfrCoP60HXbBcg2XJlrB
m2ThIXYDz6xX2FYXEfMIRbuEi717KuyKEnJl6+GFDNCKbpOVy4RXUcqwB/fCCyjg
b7oX2mrfCMCbMJiZl819eYFxaGcxv/tvFZa4rDnHkaWvKZRhu0pzIsJc6D6Q
-----END CERTIFICATE-----

52
certs/server_key.pem Normal file
View file

@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQCRtMJlRdE1aaiY
XE3AjrESU8MZJfnEzTkZag4Bipbe165BXZ28GBfYNLLCVVRGdvoTdP5b6lk0Q73l
wvdHnT6o+lU0exbZVRulo8GUUnoXqeI95zH2YdJ1rBSRTBCxZ3QO9MlsLnnsofyQ
iuVz2gk6md67qws6hoEfKXWy5wUje9vYaXoLsxQa6HyiV+x4qaL+7fbCiEXqe4NN
m92YIjKty8PYkQCBJ9OqzOv5PgnISkUOi3O+I9kVVDZpUFCGM9TKKjm+m+uCrpwQ
sIafsNn3kcKtTvnYZvhUAFSz+kRhFpmNjr7c5egytSiWwYqWy6ledbPnc5ONOotv
tfGMqjJ8DpGB0yEXQLjcGXi5ghCAuEB0nVcKPUT+bbx2Ye9zWhIPNpjAFELJiEbn
/kHUYhTn7MQey78xKyZdld7s8mahYOaRoWhiN1VjBXcQwXhWetYZLp5M9K7rZp8F
ABeiB3At/ViJQmmkYEZlWXk+Q+NAL0OMArSj1y51psGdX8qSMzulk7kE6GjiOlnS
hv0L5q76hyCMjJpDxI8okSHtWI3lUyAHzDB/d/Sg63s6TVPTsaq/wnvl/ii458ig
aA+vjdnaQfPreBrzIsR0Ujmf2ASMsluMSAXK8j/WVU89w7vo6EX5Sl6JAV/p5SMD
IYwaJrcTlhLSY0DcA26Gc3GEuDtnwQIDAQABAoICAAq2ehZ9GCEvuAwfuT2/oAhD
bVRDhQYxupGCSS5W1LnkrLS0n9zM/q/QTBZatLwLkl5s2yjB5bYghu1GNO0ohAtj
gHE/3ZG0HrjCfgCcQcrMnExF29gMIC+zq711FMMeYpuY5thT4HYEAPBVrgIjFUug
NY6hyE0Qx5yVbUhukOOnfyX9t1H15DQIROWgxkum2UuaDmsGHvHbC+/gVl4RoZ1C
no8lNh7I4VitAWqNY6W8xDAKE8P2mcbmLqv2kj0TwYDtstovV5T/pozZlcRW+eeo
aWJZrKf7BiYS8idbbryer0KHrMPMCZL029udtkVZCwZdbBFH36DiHY9klrJrbxOw
gG2PmHgCAwXT4CQ2AOUXeSOWgjk1gH0q1baVWSPE4CbPC0Gl0Qlut8PgfrP5Mb/Q
3xLRjYcnNOVAZn0SLtX5Gg6TLcYg4HdAUdiP0ES9vkSWol+Fd1JVXUxVVvmK4AJI
Ynpm00X7YA89Mgd3NUE5M5ei26dyE9xWA99rmcJ8dQL0c9XAVKjPv7sMEf12KKDO
bLcR869zsVQuqNociHQEz48hYquGDkMmsmC28OET9eb9IO455my9AoQQZ00nJs4D
+nG0kDFtuq6lSp9EVq2ArJDqowrh8jWqi6yCCND/aFtiWLqeJJkK+ori2KB4LITi
wj03E6uGFwmXMD5VNVp5AoIBAQDNVoDmZdONaBmtrAm/63hxDGuTi1tCIQZe+E5A
O7SAjHMEeTcjVFC7ckdb52Q9kifAG7lw/mPiPcKa8fAFWCWyaOoyQotW2QVEJ7cR
5gBRVUrXozbfJEizRtqLmBwcucrFe72okgeQYa5QmnF1hUcqYmz/Mlole6LrN3Bc
zHznY7VPbSir1/SVGwDlVChl72Y5cdvwuSy4YgSM3sdLER3KlmvlkS1ItBSAR5Ow
/s7iANATSyZdzwyCqMw/KV86EqBtHXUao0G4hSo0PiVXpTME/rntu+KUegieJlRZ
CJ2zwcVvSmyo/Ubv0v/8HbW25jsCLD0yK2n/tB22/z1+OiCNAoIBAQC1p86+WUNo
xkMGMVxP8G4UWNLW2vkVWVPfipHarxcs1OvaCFWJDi0od4O+uDz8OjrJ7JpX8gqH
kjh2zw0zZ1vjMVoenOklJkO7x4iQNU44Abs+yGSyR3cSds992+S0yML89KKENakM
4f/VdETIza3e+2ASgJkitpmSpW8ofeEczFx3C5cpIDVfuXNP8i2HmOCDeVKP/c+m
NSt0jDZsIe1EZWDgwyJvwvfYzfDeEt9qzN75SFGbKYgVyjVIdxYMN18WB1S1MC6y
QdHcrFBYlQTCUXKGSAVE0uudAKMyQrZu6ZU6yJ3uwRbaqf4hqI0k01A1YNwMZ5Oo
Xs5OUdeUVxkFAoIBAFQs6/aHM6w6SpyJdA27B8rAHrGDZUu8ohH4lIt8bkhsSxJA
ZrwS6m2NMXeZadxbt/Pm6aRKhMPQMaC3IX/vXLTAkZE8+kYF/UStSz0JHoQ5BM9V
Vz8ubqcfmqOzHYMhHuDKEzuxcWxcvsrIJU7Z7gyQAMK074OCJY1rUqKYKXPpyf/f
bdbUVq1xPnLEhzMSasONy23pW03S0K62/NfnEmFkPKMFWpz9RZI4xXC/ZCv9zU67
deTq5eQCMoiJ+0GhNSbF9qWO23g7DswP1dsSG9ImYi304bOgOzwp1lYRVCTGgJtY
1SaJfbT+uHWv8DgkNXh0jjms1m0+dxf3lkvSpFkCggEAagKiLoedRTnex3Bq9OPU
+d7UPDrR/dmBjwUpFTv1BF7FvRn8AN3bELCem+CdBn1c7ZY8hHJmHrQAa6VtiMAa
6b1VYGQl014/TQim87GIEjtAcOU1jq0SGh5n/IUlNTOsRz7k0M8X9sK1MHaH5b/S
ZjzA1ktiEtYJTYFqG3FKvCmvFHIZJxD61QA/ATrNw0I+U7urqQhv9Ak3cO6anAQ4
mR+op8J5CEv5o/abSJ4u7/7baRMBPNHB7yVwbvfAUCTIQuzITGzv82th86xDv05H
1rb9ZMwG9yE4CwniM2SnXpbGVeXZ9QmOMtHd0vqwU/tV5bC++iZAD5Q0RZSRgKpL
mQKCAQA8ktcefAkcKTYOHbcHS/ARkIpjZeT2LA71pdv5Xa3FkW7pVAAg1e63qwSE
i/HVFMwzgClyjp+WMDirjm2rmpQ+qBzz0GuVMUjD+1kQd+KJ8vtGojYNKpJg2Aug
+C2uYVDsaOlIXVyuyhkJ5yfO4lb4JXLWYRdbQTnRX7oWpiy7kauFuUwKXo9sHqGD
5FHC/g4TQmV+uNPxWK4gnEiuVq2HLu4oVRhegwd3hZiDqPFj7wziIjMWZinSJ43C
616glIG/5F81ACaZeZJGOw+vkVzkuMeZMkhgPffYVD+TIFxl++fj7CRrH0yAKNpn
23BF0xaeMGypYNeF29nvifeI0XJF
-----END PRIVATE KEY-----

29
certs/transport_cert.pem Normal file
View file

@ -0,0 +1,29 @@
-----BEGIN CERTIFICATE-----
MIIFCTCCAvGgAwIBAgIUHjVeqHhryj/hji8tNx6fFCnYZkMwDQYJKoZIhvcNAQEL
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDcwNDE3MjM0NFoXDTI3MDcw
NDE3MjM0NFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF
AAOCAg8AMIICCgKCAgEAqyiBICCnWqU+80udT0dm/+zixAehbp8mlDOWeQJzizCT
qVGPdrgn6HxZKuSCQ+AYdCvMrTjkv0fTMnwwk1Dlwzt/WYn5QSXYFaoMJZF3vEka
mOCFgVlahYx0Krz6wO0n8xfeDd1qSwAyGCk7JXnlTTnbht8tfdbm2RQn2dq0oQkw
DLtdfNTB1FcnjAy1UpYJOow6okuKFyngGU/U/sd+cIPQP9+7TRtSrnkRYSbpVApM
rQgqXDMSxEu7k6dd9K3zgp23zwAzCuLJ1L+aNCKyMP6Kh3qlZYIcBVzKFAmkJniy
XfRTyCdvsA6Lb6HyaKt8TLsxcdBkYuy7xdvFlLwVe9Afag1VDuw/UNwviJZVHMLi
cox02u+ICCPssxmt7bzc0v70WPBF8OsdU1zpE5MP1wtO2o1XlEH+DszkyfZk1pPs
ouWEJEa/imrsjrch0x7dvMzdo3vJT9izfaDwR9gkDdNSkeHYOwUPdgUPJlICPhVy
Ny6FA+2V1ktZE5MdbvNw9EjjIZueIc9Ilp90UHUDMSvS6zD3vABh2LfaEG1GaT/I
2x1PT5weVdHGH01SnV95k2tMBH8lK6v8alzoQUwqdmTCLIs4N2oZQgS3D564v/UX
iEtTEmyPPa7bdazIRwRuRK0Q59+qvAAHXZzzw5dw2q3Y9+AyOQvXYreRQqluZCcC
AwEAAaNTMFEwHQYDVR0OBBYEFDRbK+KovS24EIioPNSY1zr4KdwkMB8GA1UdIwQY
MBaAFDRbK+KovS24EIioPNSY1zr4KdwkMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI
hvcNAQELBQADggIBAAwa+2LD5a/g9aw9S9oCQT0y2v8tBAhlfDtaD4QhN5lSItEC
3ReagJ5w2ISH164PCpMU4keFDeh1dK/j/7hf0muH2KF6j8ByfMua2h/njWymvOE4
hksFzfJmBlUImXdzUci0Dii68iAzwt2aNHFUCHyQ/uZ6PKN5cSATcOC4GdSImOkT
7Via1kop+8NGWdbtbKzenpD+6vMET3+9rDWafRmIj42vMyXE+f795KCWAlXYo6T7
hI4PWtVqJZX+k6LDB7PMGokNDCusjNrYLoMk8pWM2GAScMmoXakGyNqMAOPQDAZW
UQ6LUn/oooC/F9oE5ZUoSaq+iM7B7TVX13EI+ccY411CRS95PZzG+zCI6sCuGNh0
OJ7WbvqR/snkrK/hu3mKZr2FuYtKJQO7KMSqDwkv80mowQDektVIqj226z6wVjvU
0uObwtRUMWqpfiY4tYqpyI6AXogTwAytHqltGL/thcBK7EW7Wi78kSwBDu3qXTzs
Iwbq+kOuRci5qGMeUerHsRWDDvwict8Fqf2xKw8RNvOkkWg4UxEZsemCpHoCbAPb
Piw5+HSDAqBRM03bzi6T1Gf847+uq6MA7l5qJZjEaRvmJoBlaS80bztYg5MOFGxL
WhVM9TwUHcGaysnd0urArsED1L1AJ4f9VlnwTnjUNZdnW/wd4airkP4FW4Ol
-----END CERTIFICATE-----

52
certs/transport_key.pem Normal file
View file

@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQCrKIEgIKdapT7z
S51PR2b/7OLEB6FunyaUM5Z5AnOLMJOpUY92uCfofFkq5IJD4Bh0K8ytOOS/R9My
fDCTUOXDO39ZiflBJdgVqgwlkXe8SRqY4IWBWVqFjHQqvPrA7SfzF94N3WpLADIY
KTsleeVNOduG3y191ubZFCfZ2rShCTAMu1181MHUVyeMDLVSlgk6jDqiS4oXKeAZ
T9T+x35wg9A/37tNG1KueRFhJulUCkytCCpcMxLES7uTp130rfOCnbfPADMK4snU
v5o0IrIw/oqHeqVlghwFXMoUCaQmeLJd9FPIJ2+wDotvofJoq3xMuzFx0GRi7LvF
28WUvBV70B9qDVUO7D9Q3C+IllUcwuJyjHTa74gII+yzGa3tvNzS/vRY8EXw6x1T
XOkTkw/XC07ajVeUQf4OzOTJ9mTWk+yi5YQkRr+KauyOtyHTHt28zN2je8lP2LN9
oPBH2CQN01KR4dg7BQ92BQ8mUgI+FXI3LoUD7ZXWS1kTkx1u83D0SOMhm54hz0iW
n3RQdQMxK9LrMPe8AGHYt9oQbUZpP8jbHU9PnB5V0cYfTVKdX3mTa0wEfyUrq/xq
XOhBTCp2ZMIsizg3ahlCBLcPnri/9ReIS1MSbI89rtt1rMhHBG5ErRDn36q8AAdd
nPPDl3Dardj34DI5C9dit5FCqW5kJwIDAQABAoICABp/REeW1oxuwvPuXlWnfldw
3bBZurusnCUwZq39hlQOOqXpUFZMRIgLjCWwR5xoxSW4TCZQWwAOh1ssT7wsDM3w
gugijI8cjpCCtAEIPVWi/vZGN/+M4jnaQyJaAaXmxnBYtGdIF7cHD4+e/nRFo+KU
lPA90nCbWZSnYqipi5zXQR7kziB6XYwudpsKB/OCR755IXCo/3ao5HPMC0jq1cpn
FaZ10AIsEIed4DtpRzuxS9Uzy5Rc0chy2FYPqzjVyeThiX2x5bUHOWtxbYF/EkAJ
1U9hBNmrTq7j17hKFSVhj8Is24rufq4Y/6wnZvxI+WQgTL4hHrwT3dlP4I2En/yQ
cWeblqclTB04O7L4fy518bZ9TQj+u07UeccWW4ZJvGEejlwWZzY5oLDDeQ7LUb2r
K/U/XGn3mYArKfW0GB7JpiALkIi9KU4Zz9LXObkFxYQug1WTSHH1Ge10W88D2cDM
w8zZanq1TudCLEeWghosXglSvyPzNmJg/g4l6tfTYL9n5ISS09sZWUit54zIS83h
TQtdCjcO9jXSovzLPnHhjtyV9s4WX0wNTbDmpJ41vWVVpYYII5b0ljLMFUGkj6bU
/LC6K/VV1u08OK2A3tZ9ZxLVt8kXEISiOkMfdnNImdtoNEWZY0jglW99bt5uBZ6u
l86o3yqGPIHK31ByBjblAoIBAQDj5Jxe4+xGQaTtsgNaaISbp+qPGo6HoOEmpDo6
cWGcZRLKuQhyLIg9ANJg33aISkIYjkKs08DegjAST9m0QmgXeOQzWf9yZhSA/HZN
Znk1jyNiO3d8ZEBSRcIxBgKF546KCbCdIZGIwwRS2rNHRMYvA3u/OC8T+S2MZ2Bb
S7wysBP0z6vPBmmo491OufmovGD2mrPFFFAKAvfvbnUXVe+NQNZPAbZgL6rq7RfL
8Y4rr7TTY1wy6ocAhr8opsl5kMOJ2dPu9e+gpR42kYKAFcDFS7DzyUVkOTQTrpIH
JjcuGnGED6zdEDiybfzoe6LlOVgg5BaaE8Qpa/gHEb6jUnirAoIBAQDARJNLburc
k/uL9hYpdGgme+8vIolfUhNgfzc17T9xvo0j5hB7DjG5f1Cy62zMHZZWlXg0+8dp
VZUCYWjduDuh8eZxuWZEnkdPvzAIfX0W0xtChLLY5xzzmzlgKeerJJ2RhpLXgEEj
vnUkfyJCL9lXwcY+Zr+Jcg+ckB2n5mltYosai3GLB2xjzG5YoeFa6lIjOP+tc6qk
iDr6iTOVixTdrAaEg8mKYsQrSajyxXt9OUVxOjyKTzafzG6aMAXXcABPS0NOL7Jk
pjgnxedPc7t6mFLxLnc+RE6Y0ZuUIf6i1oJ++47qTtxO6FKdvjJCqi4hqFlbZwJ8
IM4OZXwtM7p1AoIBACVpl35Afr598lyPV9uUwwhmxawemP2NHMliNqj3iiXJtg8P
12IfC3KwWtlKdkQjpLwxRqLm1Ify6X9oGn5sVHheAWCoyrX2KjGOwMEuLIHtcMqk
ppYGQ0T+bMXtjaZ8ADuLIbJ6G1FPartLahkuIjb/pGYdDCWER8UAB6KVSV0s6OnM
2hDPpbLzTZaqE2FvI5/4f8C/9+/c0uU8xf5BvmT7GvO3fUBHMxEEQV7a1hM38kCt
1CoLtIQSmvlKgFLzwBY2a4OUXnWKBe7BbqImcBIzveYXycuNVI5Sn2iLDo4EC1S5
2hAkpJouUlG8Ul69TkmACHHdAtwl/H70m85mMKcCggEBALLCD0BfC7WxUHq8fbZl
KEA/kg38ykP5h1J8dNSKlCYoyAjMah4/ADQ7DsiK4kiUaaZuqah5PtnMXBIuhtU9
6ROUWHD7sv2xOxQF6Z67LYX/GKUrwyyvE9faReoGjAIutOEMgZIjCG03MqqQGUX4
Pta8rpht5z7e8WzbAPeN3g95Q2OWqiZc7IWNGDsHpwJkQaHF6NFXbRw0HMG6tl8f
RHMk8Zpa/RLQ6RpmDdJJkZrHmKzBPHBLXboM0/g/Fe9Ljr4qd5VF+1JgyG0SLxFL
xM8iuFrMnMkjsP+cQk3jnV/AmOt+700hrOsgTU+j+g7JMsLQRuQYTdMinPH9oQpV
wdECggEBAJGFsteTj6E9D926+URGP07jYmdZJ7Vln8cKiIOWEEsCmjWBLILtomMO
XhdlJvR992reHBabNJa4ma3LwA1A7sbaMEIr0u+XzmU7E7HiBcBb8OA/RXy5Objo
4jVisvx331LMKSAv0HMnDVDFhyC3r3VxgQUEptolqS0QHDM5i6/N0CgWFlYUJAzY
R4/CXpZt8mLgCk9HJT3+6aAfSlkcBjruevX03xVmzBBFoOIwHRjxqNCi/4gEl4Os
w41FMqoV6JhksXN11aLRwRKLtNEkBxR3g4BEBidk+2bC+uJFqlWlFGFIUq2RKURB
tXVzHj9k6OcU/WVyVApnTjE757+2yJ4=
-----END PRIVATE KEY-----

View file

@ -144,7 +144,7 @@
bindAddress = lib.mkOption {
type = lib.types.str;
default = "0.0.0.0";
description = "IP address to bind the MTP/QUIC server to.";
description = "IP address to bind the HTTP/API server to.";
};
openFirewall = lib.mkOption {

View file

@ -15,7 +15,6 @@ CREATE TABLE IF NOT EXISTS users (
sub_end BIGINT NOT NULL DEFAULT 0,
public_key BLOB NOT NULL,
token BLOB NOT NULL,
created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
UNIQUE KEY uk_users_username (username),
KEY idx_users_iota_id (iota_id),

View file

@ -1,22 +0,0 @@
ALTER TABLE users
ADD COLUMN presence_preference VARBINARY(32) NOT NULL DEFAULT 'user_online';
UPDATE users
SET presence_preference = 'user_online'
WHERE presence_preference NOT IN (
'user_online',
'user_idle',
'user_dnd',
'user_wc',
'user_invisible'
);
ALTER TABLE users
ADD CONSTRAINT chk_users_presence_preference
CHECK (presence_preference IN (
'user_online',
'user_idle',
'user_dnd',
'user_wc',
'user_invisible'
));

View file

@ -1,11 +0,0 @@
ALTER TABLE users
DROP FOREIGN KEY fk_users_iota;
ALTER TABLE users
MODIFY iota_id BIGINT NULL;
ALTER TABLE users
ADD CONSTRAINT fk_users_iota
FOREIGN KEY (iota_id)
REFERENCES iotas (id)
ON DELETE SET NULL;

View file

@ -1,8 +0,0 @@
CREATE TABLE pending_iota_user_erasure (
user_id BIGINT NOT NULL,
iota_id BIGINT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (user_id, iota_id),
CONSTRAINT fk_pending_iota_user_erasure_iota
FOREIGN KEY (iota_id) REFERENCES iotas(id) ON DELETE CASCADE
);

View file

@ -1,4 +0,0 @@
CREATE TABLE iota_snapshot_outbox (
iota_id BIGINT NOT NULL PRIMARY KEY,
updated_at DATETIME NOT NULL
);

View file

@ -1,2 +0,0 @@
ALTER TABLE users
ADD COLUMN IF NOT EXISTS created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3);

@ -1 +1 @@
Subproject commit f4e45aa3a3ad0e3c3a257f66857b904a1af7901c
Subproject commit ece6e2c3b4e925f3cefe46f4a048fbfc8f823093

View file

@ -28,7 +28,7 @@ pub struct UserResponse {
pub username: String,
pub public_key: String,
pub user_id: i64,
pub iota_id: Option<i64>,
pub iota_id: i64,
pub sub_level: i32,
pub sub_end: i64,
#[serde(skip_serializing_if = "Option::is_none")]
@ -47,7 +47,7 @@ pub struct UsernameResponse {
pub username: String,
pub public_key: String,
pub user_id: i64,
pub iota_id: Option<i64>,
pub iota_id: i64,
pub sub_level: i32,
pub sub_end: i64,
}

View file

@ -1,5 +1,4 @@
use std::{env, time::Duration};
use thiserror::Error;
#[derive(Clone, Debug)]
pub struct RateLimitConfig {
@ -10,26 +9,6 @@ pub struct RateLimitConfig {
pub transport_connections_per_ip: usize,
}
#[derive(Clone, Debug)]
pub struct DispatchConfig {
pub omikron_handler_concurrency: usize,
pub global_handler_concurrency: usize,
}
#[derive(Clone, Debug, Default)]
pub struct OmegaConfig {
pub rate_limits: RateLimitConfig,
pub dispatch: DispatchConfig,
}
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("invalid configuration value for {name}: {value}")]
InvalidValue { name: String, value: String },
#[error("configuration value for {name} is not valid Unicode")]
InvalidUnicode { name: String },
}
pub const fn cors_origin() -> &'static str {
"*"
}
@ -47,75 +26,40 @@ impl Default for RateLimitConfig {
}
impl RateLimitConfig {
pub fn from_env() -> Result<Self, ConfigError> {
pub fn from_env() -> Self {
let defaults = Self::default();
Ok(Self {
window: env_duration("RATE_LIMIT_WINDOW_SECONDS", defaults.window)?,
general_requests: env_usize("RATE_LIMIT_GENERAL_REQUESTS", defaults.general_requests)?,
Self {
window: env_duration("RATE_LIMIT_WINDOW_SECONDS", defaults.window),
general_requests: env_usize("RATE_LIMIT_GENERAL_REQUESTS", defaults.general_requests),
registration_requests: env_usize(
"RATE_LIMIT_REGISTRATION_REQUESTS",
defaults.registration_requests,
)?,
),
transport_connections: env_usize(
"RATE_LIMIT_TRANSPORT_CONNECTIONS",
defaults.transport_connections,
)?,
),
transport_connections_per_ip: env_usize(
"RATE_LIMIT_TRANSPORT_CONNECTIONS_PER_IP",
defaults.transport_connections_per_ip,
)?,
})
}
pub fn from_env_or_default() -> Self {
Self::from_env().unwrap_or_default()
}
}
impl Default for DispatchConfig {
fn default() -> Self {
Self {
omikron_handler_concurrency: 32,
global_handler_concurrency: 256,
),
}
}
}
impl OmegaConfig {
pub fn from_env() -> Result<Self, ConfigError> {
let rate_limits = RateLimitConfig::from_env()?;
let defaults = DispatchConfig::default();
Ok(Self {
rate_limits,
dispatch: DispatchConfig {
omikron_handler_concurrency: env_usize(
"OMEGA_OMIKRON_HANDLER_CONCURRENCY",
defaults.omikron_handler_concurrency,
)?,
global_handler_concurrency: env_usize(
"OMEGA_GLOBAL_HANDLER_CONCURRENCY",
defaults.global_handler_concurrency,
)?,
},
})
}
}
fn env_usize(name: &str, fallback: usize) -> Result<usize, ConfigError> {
match env::var(name) {
Ok(value) => value
.parse::<usize>()
fn env_usize(name: &str, fallback: usize) -> usize {
env::var(name)
.ok()
.and_then(|value| value.parse::<usize>().ok())
.filter(|value| *value > 0)
.ok_or_else(|| ConfigError::InvalidValue {
name: name.into(),
value,
}),
Err(env::VarError::NotPresent) => Ok(fallback),
Err(env::VarError::NotUnicode(_)) => Err(ConfigError::InvalidUnicode { name: name.into() }),
}
.unwrap_or(fallback)
}
fn env_duration(name: &str, fallback: Duration) -> Result<Duration, ConfigError> {
env_usize(name, fallback.as_secs() as usize).map(|value| Duration::from_secs(value as u64))
fn env_duration(name: &str, fallback: Duration) -> Duration {
env::var(name)
.ok()
.and_then(|value| value.parse::<u64>().ok())
.filter(|value| *value > 0)
.map(Duration::from_secs)
.unwrap_or(fallback)
}

View file

@ -22,7 +22,7 @@ pub async fn get_iota_by_id(id: IotaId) -> Result<Iota> {
pub async fn create_new_iota(public_key: PublicKeyBundle) -> Result<IotaId> {
for _ in 0..16 {
let id = crate::db::user_repo::generate_protocol_id();
let id = crate::db::user_repo::get_register_id().await?;
let iota_id = IotaId::from(id.0);
match register_complete_iota(iota_id, public_key.clone()).await {
Ok(()) => return Ok(iota_id),
@ -48,12 +48,20 @@ pub async fn register_complete_iota(id: IotaId, public_key: PublicKeyBundle) ->
}
sqlx::query("INSERT INTO iotas (id, public_key) VALUES (?, ?)")
.bind(id.0)
.bind(public_key.try_as_bytes()?)
.bind(public_key.as_bytes())
.execute(&pool().await?)
.await?;
Ok(())
}
pub async fn change_iota_key(id: IotaId, key: PublicKeyBundle) -> Result<()> {
sqlx::query("UPDATE iotas SET public_key = ? WHERE id = ?")
.bind(key.as_bytes())
.bind(id.0)
.execute(&pool().await?)
.await?;
Ok(())
}
pub async fn delete_iota(id: IotaId) -> Result<()> {
sqlx::query("DELETE FROM iotas WHERE id = ?")
.bind(id.0)

View file

@ -2,22 +2,29 @@ use crate::{
db::pool,
error::{OmegaError, Result},
models::{IotaId, User, UserId},
sql::connection_status::UserStatus,
};
use mtp::crypto::PublicKeyBundle;
use sqlx::{FromRow, MySql, QueryBuilder, Row};
use std::collections::HashMap;
use sqlx::{FromRow, Row};
pub const MAX_PROTOCOL_ID: i64 = (1_i64 << 48) - 1;
const ID_ALLOCATION_ATTEMPTS: usize = 16;
pub fn generate_protocol_id() -> UserId {
loop {
let value = rand::random::<u64>() & ((1_u64 << 48) - 1);
if value != 0 {
return UserId::from(value as i64);
}
pub async fn get_register_id() -> Result<UserId> {
use std::time::{SystemTime, UNIX_EPOCH};
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let ts = timestamp as i64;
if ts >= 1 && ts <= MAX_PROTOCOL_ID {
return Ok(UserId::from(ts));
}
// Fall back to random if the timestamp is outside the 48-bit range.
let id = (rand::random::<u64>() & ((1_u64 << 48) - 1)) as i64;
Ok(UserId::from(id.max(1)))
}
pub fn valid_protocol_id(id: i64) -> bool {
@ -33,24 +40,8 @@ pub async fn allocate_registration(iota_id: IotaId, request_id: u32) -> Result<(
));
}
let database = pool().await?;
sqlx::query(
"UPDATE registration_leases SET request_id = NULL \
WHERE request_id IS NOT NULL AND expires_at < UTC_TIMESTAMP()",
)
.execute(&database)
.await?;
for _ in 0..ID_ALLOCATION_ATTEMPTS {
let id = generate_protocol_id();
let user_exists = sqlx::query("SELECT 1 FROM users WHERE id = ? LIMIT 1")
.bind(id.0)
.fetch_optional(&database)
.await?
.is_some();
if user_exists {
continue;
}
let id = get_register_id().await?;
let token = uuid::Uuid::new_v4().to_string();
let result = sqlx::query(
"INSERT INTO registration_leases (token, user_id, iota_id, request_id, expires_at) \
@ -60,7 +51,7 @@ pub async fn allocate_registration(iota_id: IotaId, request_id: u32) -> Result<(
.bind(id.0)
.bind(iota_id.0)
.bind(request_id)
.execute(&database)
.execute(&pool().await?)
.await;
match result {
Ok(_) => return Ok((id, token)),
@ -71,7 +62,7 @@ pub async fn allocate_registration(iota_id: IotaId, request_id: u32) -> Result<(
)
.bind(iota_id.0)
.bind(request_id)
.fetch_optional(&database)
.fetch_optional(&pool().await?)
.await?;
if let Some(existing) = existing {
let current: i8 = existing.get("current");
@ -101,25 +92,23 @@ pub(crate) fn is_duplicate_key(error: &sqlx::Error) -> bool {
})
}
const USER_BY_USERNAME_QUERY: &str = "SELECT id, iota_id, username, display, status, presence_preference, about, avatar, sub_level, sub_end, public_key, token, CAST(UNIX_TIMESTAMP(created_at) * 1000 AS SIGNED) AS created_at FROM users WHERE username = ?";
const USER_BY_ID_QUERY: &str = "SELECT id, iota_id, username, display, status, presence_preference, about, avatar, sub_level, sub_end, public_key, token, CAST(UNIX_TIMESTAMP(created_at) * 1000 AS SIGNED) AS created_at FROM users WHERE id = ?";
const USER_COLUMNS: &str = "SELECT id, iota_id, username, display, status, presence_preference, about, avatar, sub_level, sub_end, public_key, token, CAST(UNIX_TIMESTAMP(created_at) * 1000 AS SIGNED) AS created_at FROM users";
const USER_BY_USERNAME_QUERY: &str = "SELECT id, iota_id, username, display, status, about, avatar, sub_level, sub_end, public_key, token FROM users WHERE username = ?";
const USER_BY_ID_QUERY: &str = "SELECT id, iota_id, username, display, status, about, avatar, sub_level, sub_end, public_key, token FROM users WHERE id = ?";
const USERS_BY_IOTA_ID_QUERY: &str = "SELECT id, iota_id, username, display, status, about, avatar, sub_level, sub_end, public_key, token FROM users WHERE iota_id = ?";
#[derive(FromRow)]
struct UserRow {
id: i64,
iota_id: Option<i64>,
iota_id: i64,
username: Vec<u8>,
display: Option<Vec<u8>>,
status: Option<Vec<u8>>,
presence_preference: Vec<u8>,
about: Option<Vec<u8>>,
avatar: Option<Vec<u8>>,
sub_level: i32,
sub_end: i64,
public_key: Vec<u8>,
token: Vec<u8>,
created_at: i64,
}
impl TryFrom<UserRow> for User {
@ -132,18 +121,16 @@ impl TryFrom<UserRow> for User {
|value| String::from_utf8(value).map_err(|error| sqlx::Error::Decode(Box::new(error)));
Ok(User {
id: row.id.into(),
iota_id: row.iota_id.map(IotaId::from),
iota_id: row.iota_id.into(),
username: decode(row.username)?,
display: row.display.map(decode).transpose()?,
status: row.status.map(decode).transpose()?,
presence_preference: decode(row.presence_preference)?,
about: row.about.map(decode).transpose()?,
avatar: row.avatar,
sub_level: row.sub_level,
sub_end: row.sub_end,
public_key,
token: decode(row.token)?,
created_at: row.created_at,
})
}
}
@ -167,127 +154,13 @@ pub async fn get_by_user_id(id: UserId) -> Result<User> {
}
pub async fn get_users_by_iota_id(id: IotaId) -> Result<Vec<User>> {
get_users_by_iota_ids(&[id.0]).await
}
fn normalized_ids(ids: &[i64]) -> Vec<i64> {
let mut ids = ids
.iter()
.copied()
.filter(|id| valid_protocol_id(*id))
.collect::<Vec<_>>();
ids.sort_unstable();
ids.dedup();
ids
}
fn append_in_clause(query: &mut QueryBuilder<MySql>, ids: &[i64]) {
query.push("(");
for (index, id) in ids.iter().enumerate() {
if index > 0 {
query.push(", ");
}
query.push_bind(*id);
}
query.push(")");
}
async fn fetch_users(mut query: QueryBuilder<MySql>) -> Result<Vec<User>> {
query
.build_query_as::<UserRow>()
.fetch_all(&pool().await?)
.await?
.into_iter()
.map(|row| row.try_into().map_err(OmegaError::from))
.collect()
}
pub async fn get_users_by_ids(ids: &[i64]) -> Result<Vec<User>> {
let ids = normalized_ids(ids);
if ids.is_empty() {
return Ok(Vec::new());
}
let mut query = QueryBuilder::<MySql>::new(USER_COLUMNS);
query.push(" WHERE id IN ");
append_in_clause(&mut query, &ids);
fetch_users(query).await
}
pub async fn get_users_by_iota_ids(ids: &[i64]) -> Result<Vec<User>> {
let ids = normalized_ids(ids);
if ids.is_empty() {
return Ok(Vec::new());
}
let mut query = QueryBuilder::<MySql>::new(USER_COLUMNS);
query.push(" WHERE iota_id IN ");
append_in_clause(&mut query, &ids);
fetch_users(query).await
}
pub async fn get_users_by_ids_and_iota_ids(
user_ids: &[i64],
iota_ids: &[i64],
) -> Result<Vec<User>> {
let user_ids = normalized_ids(user_ids);
let iota_ids = normalized_ids(iota_ids);
if user_ids.is_empty() && iota_ids.is_empty() {
return Ok(Vec::new());
}
let mut query = QueryBuilder::<MySql>::new(USER_COLUMNS);
query.push(" WHERE ");
if !user_ids.is_empty() {
query.push("id IN ");
append_in_clause(&mut query, &user_ids);
}
if !iota_ids.is_empty() {
if !user_ids.is_empty() {
query.push(" OR ");
}
query.push("iota_id IN ");
append_in_clause(&mut query, &iota_ids);
}
fetch_users(query).await
}
#[derive(FromRow)]
struct PresencePreferenceRow {
id: i64,
presence_preference: Vec<u8>,
}
pub async fn get_presence_preferences(ids: &[i64]) -> Result<HashMap<i64, UserStatus>> {
let ids = normalized_ids(ids);
if ids.is_empty() {
return Ok(HashMap::new());
}
let mut query =
QueryBuilder::<MySql>::new("SELECT id, presence_preference FROM users WHERE id IN ");
append_in_clause(&mut query, &ids);
let rows = query
.build_query_as::<PresencePreferenceRow>()
let rows = sqlx::query_as::<_, UserRow>(USERS_BY_IOTA_ID_QUERY)
.bind(id.0)
.fetch_all(&pool().await?)
.await?;
let mut preferences = HashMap::with_capacity(rows.len());
for row in rows {
let status = String::from_utf8(row.presence_preference)
.ok()
.and_then(|value| UserStatus::from_client_preference(&value));
let status = match status {
Some(status) => status,
None => {
crate::log_in!(
crate::util::logger::PrintType::General,
"Invalid persisted presence preference for user {}, using user_online",
row.id
);
UserStatus::user_online
}
};
preferences.insert(row.id, status);
}
Ok(preferences)
rows.into_iter()
.map(|row| row.try_into().map_err(OmegaError::from))
.collect()
}
async fn update(
@ -344,117 +217,33 @@ pub async fn change_status(id: UserId, value: String) -> Result<()> {
.await
}
pub async fn change_iota_id(id: UserId, value: Option<IotaId>) -> Result<()> {
let mut transaction = pool().await?.begin().await?;
let previous = sqlx::query("SELECT iota_id FROM users WHERE id = ? FOR UPDATE")
.bind(id.0)
.fetch_optional(&mut *transaction)
.await?
.ok_or(OmegaError::NotFound)?;
let previous_iota_id: Option<i64> = previous.get("iota_id");
pub async fn change_iota_id(id: UserId, value: IotaId) -> Result<()> {
sqlx::query("UPDATE users SET iota_id = ? WHERE id = ?")
.bind(value.map(|id| id.0))
.bind(value.0)
.bind(id.0)
.execute(&mut *transaction)
.await?;
if let Some(iota_id) = previous_iota_id {
enqueue_iota_snapshot(&mut transaction, iota_id).await?;
}
if let Some(iota_id) = value {
enqueue_iota_snapshot(&mut transaction, iota_id.0).await?;
}
transaction.commit().await?;
Ok(())
}
async fn enqueue_iota_snapshot(
transaction: &mut sqlx::Transaction<'_, MySql>,
iota_id: i64,
) -> Result<()> {
sqlx::query(
"INSERT INTO iota_snapshot_outbox (iota_id, updated_at) VALUES (?, UTC_TIMESTAMP()) \
ON DUPLICATE KEY UPDATE updated_at = VALUES(updated_at)",
)
.bind(iota_id)
.execute(&mut **transaction)
.await?;
Ok(())
}
pub async fn pending_iota_snapshots() -> Result<Vec<IotaId>> {
let rows = sqlx::query("SELECT iota_id FROM iota_snapshot_outbox ORDER BY updated_at")
.fetch_all(&pool().await?)
.await?;
Ok(rows
.into_iter()
.map(|row| IotaId::from(row.get::<i64, _>("iota_id")))
.collect())
}
pub async fn complete_iota_snapshot(iota_id: IotaId) -> Result<()> {
sqlx::query("DELETE FROM iota_snapshot_outbox WHERE iota_id = ?")
.bind(iota_id.0)
.execute(&pool().await?)
.await?;
Ok(())
}
/// Delete the central identity while retaining a durable instruction for the
/// last hosting Iota. The pending row is intentionally independent of users:
/// it must outlive the account row.
pub async fn delete_user_with_pending_erasure(id: UserId) -> Result<Option<IotaId>> {
let mut tx = pool().await?.begin().await?;
let row = sqlx::query("SELECT iota_id FROM users WHERE id = ? FOR UPDATE")
.bind(id.0)
.fetch_optional(&mut *tx)
.await?
.ok_or(OmegaError::NotFound)?;
let iota_id: Option<i64> = row.get("iota_id");
if let Some(iota_id) = iota_id {
sqlx::query(
"INSERT IGNORE INTO pending_iota_user_erasure (user_id, iota_id) VALUES (?, ?)",
pub async fn change_token(id: UserId, value: String) -> Result<()> {
update(
id,
"UPDATE users SET token = ? WHERE id = ?",
value.into_bytes(),
)
.bind(id.0)
.bind(iota_id)
.execute(&mut *tx)
.await?;
}
sqlx::query("DELETE FROM registration_leases WHERE user_id = ?")
.bind(id.0)
.execute(&mut *tx)
.await?;
.await
}
pub async fn delete_user(id: UserId) -> Result<()> {
sqlx::query("DELETE FROM users WHERE id = ?")
.bind(id.0)
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok(iota_id.map(IotaId::from))
}
pub async fn pending_erasures_for_iota(iota_id: IotaId) -> Result<Vec<UserId>> {
let rows = sqlx::query("SELECT user_id FROM pending_iota_user_erasure WHERE iota_id = ?")
.bind(iota_id.0)
.fetch_all(&pool().await?)
.await?;
Ok(rows
.into_iter()
.map(|row| UserId::from(row.get::<i64, _>("user_id")))
.collect())
}
pub async fn acknowledge_pending_erasure(user_id: UserId, iota_id: IotaId) -> Result<bool> {
let result =
sqlx::query("DELETE FROM pending_iota_user_erasure WHERE user_id = ? AND iota_id = ?")
.bind(user_id.0)
.bind(iota_id.0)
.execute(&pool().await?)
.await?;
Ok(result.rows_affected() == 1)
Ok(())
}
pub async fn change_keys(id: UserId, public_key: PublicKeyBundle) -> Result<()> {
let public_key = public_key.try_as_bytes()?;
sqlx::query("UPDATE users SET public_key = ? WHERE id = ?")
.bind(public_key)
.bind(public_key.as_bytes())
.bind(id.0)
.execute(&pool().await?)
.await?;
@ -477,7 +266,6 @@ pub async fn register_complete_user(
if !valid_username(&username) {
return Err(OmegaError::Validation("invalid username".into()));
}
let public_key_bytes = public_key.try_as_bytes()?;
let mut transaction = pool().await?.begin().await?;
let lease = sqlx::query(
@ -504,7 +292,7 @@ pub async fn register_complete_user(
)
.bind(id.0)
.bind(username.as_bytes())
.bind(&public_key_bytes)
.bind(public_key.as_bytes())
.bind(iota_id.0)
.bind(token.as_bytes())
.execute(&mut *transaction)
@ -517,25 +305,24 @@ pub async fn register_complete_user(
.bind(id.0)
.fetch_optional(&mut *transaction)
.await?;
let existing_matches = match existing {
Some(existing) => {
let existing = User::try_from(existing).map_err(OmegaError::from)?;
existing.iota_id == Some(iota_id)
match existing
.map(User::try_from)
.transpose()
.map_err(OmegaError::from)?
{
Some(existing)
if existing.iota_id == iota_id
&& existing.username == username
&& existing.public_key.try_as_bytes()? == public_key_bytes
&& existing.token == token
}
None => false,
};
if existing_matches {
&& existing.public_key.as_bytes() == public_key.as_bytes()
&& existing.token == token =>
{
Ok(())
} else {
Err(insert_error.into())
}
_ => Err(insert_error.into()),
}
}
};
result?;
enqueue_iota_snapshot(&mut transaction, iota_id.0).await?;
sqlx::query("UPDATE registration_leases SET completed_at = UTC_TIMESTAMP() WHERE token = ?")
.bind(&registration_token)
.execute(&mut *transaction)
@ -546,20 +333,19 @@ pub async fn register_complete_user(
fn valid_username(username: &str) -> bool {
!username.is_empty()
&& username.len() <= 15
&& username
.bytes()
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit())
&& username.chars().count() <= 15
&& !username.chars().any(char::is_control)
&& !username.contains(['/', '\\'])
}
#[cfg(test)]
mod tests {
use super::{MAX_PROTOCOL_ID, generate_protocol_id, valid_protocol_id};
use super::{MAX_PROTOCOL_ID, get_register_id, valid_protocol_id};
#[tokio::test]
async fn generated_registration_ids_fit_the_mtp_wire_range() {
for _ in 0..128 {
assert!(valid_protocol_id(generate_protocol_id().0));
assert!(valid_protocol_id(get_register_id().await.unwrap().0));
}
assert!(!valid_protocol_id(0));
assert!(valid_protocol_id(MAX_PROTOCOL_ID));

View file

@ -1,34 +1,11 @@
use std::path::PathBuf;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum IdentityError {
#[error("identity storage error at {path}: {source}")]
Storage {
path: PathBuf,
#[source]
source: mtp::files::FileError,
},
#[error("identity I/O error at {path}: {source}")]
Io {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("public key bundle at {path} does not match the private keyring")]
PublicBundleMismatch { path: PathBuf },
}
#[derive(Debug, Error)]
pub enum OmegaError {
#[error("database pool is not initialized")]
DatabaseNotInitialized,
#[error("database error: {0}")]
Database(sqlx::Error),
#[error("cryptographic error: {0}")]
Crypto(#[from] mtp::crypto::CryptoError),
#[error("identity error: {0}")]
Identity(#[from] IdentityError),
#[error("invalid input: {0}")]
Validation(String),
#[error("resource not found")]
@ -68,8 +45,6 @@ impl OmegaError {
Self::NotFound => http::StatusCode::NOT_FOUND,
Self::DatabaseNotInitialized
| Self::Database(_)
| Self::Crypto(_)
| Self::Identity(_)
| Self::Transport(_)
| Self::NotConnected
| Self::NotAuthenticated

View file

@ -1,297 +0,0 @@
use crate::error::{IdentityError, Result};
use mtp::crypto::{Keyring, PublicKeyBundle};
use mtp::files::{
FileError, load_keyring_raw, load_public_key_bundle, save_keyring_raw, save_public_key_bundle,
};
use std::{
fs,
path::{Path, PathBuf},
sync::{
Arc,
atomic::{AtomicU64, Ordering},
},
};
pub const KEYRING_PATH: &str = "./omega.mk";
pub const PUBLIC_KEY_PATH: &str = "./omega.mpkb";
pub struct OmegaIdentity {
keyring: Arc<Keyring>,
}
static PUBLIC_BUNDLE_TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
impl OmegaIdentity {
pub fn load_or_create() -> Result<Self> {
Self::load_or_create_at(Path::new(KEYRING_PATH), Path::new(PUBLIC_KEY_PATH))
}
pub(crate) fn load_or_create_at(
keyring_path: impl AsRef<Path>,
public_key_path: impl AsRef<Path>,
) -> Result<Self> {
let keyring_path = keyring_path.as_ref();
let public_key_path = public_key_path.as_ref();
let keyring = match load_keyring_raw(keyring_path) {
Ok(keyring) => keyring,
Err(FileError::Io(error)) if error.kind() == std::io::ErrorKind::NotFound => {
return Self::create_at(keyring_path, public_key_path);
}
Err(error) => {
return Err(IdentityError::Storage {
path: keyring_path.to_path_buf(),
source: error,
}
.into());
}
};
let identity = Self {
keyring: Arc::new(keyring),
};
match load_public_key_bundle(public_key_path) {
Ok(persisted_bundle) => {
identity.verify_public_bundle(public_key_path, &persisted_bundle)?
}
Err(FileError::Io(error)) if error.kind() == std::io::ErrorKind::NotFound => {
Self::persist_public_bundle(&identity.public_key_bundle(), public_key_path)?;
}
Err(error) => {
return Err(IdentityError::Storage {
path: public_key_path.to_path_buf(),
source: error,
}
.into());
}
}
Ok(identity)
}
fn create_at(keyring_path: &Path, public_key_path: &Path) -> Result<Self> {
let keyring = Keyring::generate();
save_keyring_raw(&keyring, keyring_path).map_err(|error| IdentityError::Storage {
path: keyring_path.to_path_buf(),
source: error,
})?;
Self::persist_public_bundle(&keyring.public_key_bundle(), public_key_path)?;
Ok(Self {
keyring: Arc::new(keyring),
})
}
fn persist_public_bundle(bundle: &PublicKeyBundle, path: &Path) -> Result<()> {
let temporary_path = temporary_path(path)?;
let result = save_public_key_bundle(bundle, &temporary_path).map_err(|source| {
IdentityError::Storage {
path: path.to_path_buf(),
source,
}
});
if let Err(error) = result {
let _ = fs::remove_file(&temporary_path);
return Err(error.into());
}
if let Err(source) = fs::File::open(&temporary_path).and_then(|file| file.sync_all()) {
let _ = fs::remove_file(&temporary_path);
return Err(IdentityError::Io {
path: path.to_path_buf(),
source,
}
.into());
}
if let Err(source) = fs::rename(&temporary_path, path) {
let _ = fs::remove_file(&temporary_path);
return Err(IdentityError::Io {
path: path.to_path_buf(),
source,
}
.into());
}
Ok(())
}
fn verify_public_bundle(&self, path: &Path, persisted_bundle: &PublicKeyBundle) -> Result<()> {
let expected = self.keyring.public_key_bundle().try_as_bytes()?;
let actual = persisted_bundle.try_as_bytes()?;
if expected != actual {
return Err(IdentityError::PublicBundleMismatch {
path: path.to_path_buf(),
}
.into());
}
Ok(())
}
pub fn keyring(&self) -> &Keyring {
&self.keyring
}
pub fn public_key_bundle(&self) -> PublicKeyBundle {
self.keyring().public_key_bundle()
}
pub fn clone_keyring(&self) -> Result<Keyring> {
let bytes = self.keyring.try_to_bytes()?;
Ok(Keyring::from_bytes(&bytes)?)
}
#[cfg(test)]
pub(crate) fn from_keyring(keyring: Keyring) -> Self {
Self {
keyring: Arc::new(keyring),
}
}
}
fn temporary_path(path: &Path) -> Result<PathBuf> {
let parent = path.parent().unwrap_or_else(|| Path::new("."));
let file_name = path.file_name().ok_or_else(|| IdentityError::Io {
path: path.to_path_buf(),
source: std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"identity path has no file name",
),
})?;
let mut temporary_name = file_name.to_os_string();
temporary_name.push(format!(
".tmp-{}-{}",
std::process::id(),
PUBLIC_BUNDLE_TEMP_COUNTER.fetch_add(1, Ordering::Relaxed)
));
Ok(parent.join(temporary_name))
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::sync::atomic::{AtomicU64, Ordering};
fn test_directory() -> std::path::PathBuf {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let id = COUNTER.fetch_add(1, Ordering::Relaxed);
let path =
std::env::temp_dir().join(format!("omega-identity-test-{}-{id}", std::process::id()));
fs::create_dir_all(&path).expect("create test directory");
path
}
#[test]
fn generated_identity_survives_restart_with_separate_file_formats() {
let directory = test_directory();
let keyring_path = directory.join("omega.mk");
let public_key_path = directory.join("omega.mpkb");
let first = OmegaIdentity::load_or_create_at(&keyring_path, &public_key_path)
.expect("create identity");
let first_bundle = first.public_key_bundle().try_as_bytes().expect("bundle");
let keyring_bytes = fs::read(&keyring_path).expect("read keyring");
let bundle_bytes = fs::read(&public_key_path).expect("read bundle");
assert_eq!(&keyring_bytes[..4], b"MTMK");
assert_eq!(&bundle_bytes[..4], b"MPKB");
let restarted = OmegaIdentity::load_or_create_at(&keyring_path, &public_key_path)
.expect("reload identity");
assert_eq!(
restarted
.public_key_bundle()
.try_as_bytes()
.expect("bundle"),
first_bundle
);
assert_eq!(
load_public_key_bundle(&public_key_path)
.expect("load public bundle")
.try_as_bytes()
.expect("bundle"),
first_bundle
);
fs::remove_dir_all(directory).expect("remove test directory");
}
#[test]
fn invalid_existing_keyring_does_not_create_a_new_identity() {
let directory = test_directory();
let keyring_path = directory.join("omega.mk");
let public_key_path = directory.join("omega.mpkb");
fs::write(&keyring_path, b"not-a-keyring").expect("write invalid keyring");
let result = OmegaIdentity::load_or_create_at(&keyring_path, &public_key_path);
assert!(result.is_err());
assert!(!public_key_path.exists());
fs::remove_dir_all(directory).expect("remove test directory");
}
#[test]
fn missing_public_bundle_is_repaired_without_changing_keyring() {
let directory = test_directory();
let keyring_path = directory.join("omega.mk");
let public_key_path = directory.join("omega.mpkb");
OmegaIdentity::load_or_create_at(&keyring_path, &public_key_path).expect("create identity");
let original_keyring = fs::read(&keyring_path).expect("read keyring");
fs::remove_file(&public_key_path).expect("remove bundle");
let repaired = OmegaIdentity::load_or_create_at(&keyring_path, &public_key_path)
.expect("repair bundle");
assert_eq!(
fs::read(&keyring_path).expect("read keyring"),
original_keyring
);
assert_eq!(
repaired.public_key_bundle().try_as_bytes().expect("bundle"),
load_public_key_bundle(&public_key_path)
.expect("load bundle")
.try_as_bytes()
.expect("bundle")
);
fs::remove_dir_all(directory).expect("remove test directory");
}
#[test]
fn mismatched_public_bundle_stops_startup() {
let directory = test_directory();
let keyring_path = directory.join("omega.mk");
let public_key_path = directory.join("omega.mpkb");
OmegaIdentity::load_or_create_at(&keyring_path, &public_key_path).expect("create identity");
let other_keyring = Keyring::generate();
save_public_key_bundle(&other_keyring.public_key_bundle(), &public_key_path)
.expect("save mismatched bundle");
assert!(matches!(
OmegaIdentity::load_or_create_at(&keyring_path, &public_key_path),
Err(crate::OmegaError::Identity(
IdentityError::PublicBundleMismatch { .. }
))
));
fs::remove_dir_all(directory).expect("remove test directory");
}
#[test]
fn existing_raw_keyring_is_reloaded_without_a_passphrase() {
let directory = test_directory();
let keyring_path = directory.join("omega.mk");
let public_key_path = directory.join("omega.mpkb");
let first = OmegaIdentity::load_or_create_at(&keyring_path, &public_key_path)
.expect("create identity");
let original_keyring = fs::read(&keyring_path).expect("read keyring");
let reloaded = OmegaIdentity::load_or_create_at(&keyring_path, &public_key_path)
.expect("reload identity");
assert_eq!(
fs::read(&keyring_path).expect("read keyring"),
original_keyring
);
assert_eq!(
reloaded.public_key_bundle().try_as_bytes().expect("bundle"),
first.public_key_bundle().try_as_bytes().expect("bundle")
);
fs::remove_dir_all(directory).expect("remove test directory");
}
}

View file

@ -2,34 +2,58 @@ mod api;
mod config;
mod db;
pub mod error;
mod identity;
mod models;
mod server;
mod sql;
mod state;
mod transport;
mod util;
pub use error::{OmegaError, Result};
use crate::db::initialize;
use crate::state::OmegaState;
use crate::transport::omikron_connection;
use crate::transport::omikron_manager;
use crate::util::file_util::get_directory;
use crate::util::logger::PrintType;
use crate::util::logger::startup;
use crate::{config::OmegaConfig, server::middleware};
use dotenv::from_path;
use mtp::crypto::Keyring;
use mtp::files::{load_keyring_raw, save_keyring_raw, save_public_key_bundle};
use once_cell::sync::Lazy;
use rustls::crypto::aws_lc_rs::default_provider;
use std::env;
use std::path::Path;
use std::time::Duration;
use tokio::time::interval;
const KEYRING_PATH: &str = "./omega.mk";
static KEYRING: Lazy<Keyring> = Lazy::new(|| {
load_keyring_raw(KEYRING_PATH).unwrap_or_else(|_| {
let kr = Keyring::generate();
if let Err(error) = save_keyring_raw(&kr, KEYRING_PATH) {
eprintln!("Failed to save generated keyring: {error}");
}
if let Err(error) = save_public_key_bundle(&kr.public_key_bundle(), KEYRING_PATH) {
eprintln!("Failed to save generated public key bundle: {error}");
}
eprintln!("Generated new keyring at {}", KEYRING_PATH);
kr
})
});
pub fn get_keyring() -> &'static Keyring {
&KEYRING
}
pub fn load_keyring() -> Keyring {
Keyring::from_bytes(&KEYRING.to_bytes()).unwrap_or_else(|error| {
eprintln!("Failed to clone keyring: {error}");
Keyring::generate()
})
}
#[tokio::main]
async fn main() {
if default_provider().install_default().is_err() {
if let Err(_) = default_provider().install_default() {
println!("Error loading Provider");
return;
}
@ -38,26 +62,6 @@ async fn main() {
log_in!("Incoming messages");
log_out!("Outgoing messages");
let config = match OmegaConfig::from_env() {
Ok(config) => config,
Err(error) => {
log!("[FATAL] Omega configuration is invalid: {}", error);
return;
}
};
if middleware::initialize_config(config.rate_limits.clone()).is_err() {
log!("[FATAL] Omega rate-limit configuration was initialized more than once");
return;
}
let identity = match identity::OmegaIdentity::load_or_create() {
Ok(identity) => identity,
Err(error) => {
log!("[FATAL] Omega identity initialization failed: {}", error);
return;
}
};
let state = OmegaState::new(identity, config);
log!("Started");
log!(" .env");
if let Err(e) = initialize().await {
@ -84,14 +88,13 @@ async fn main() {
}
}
});
let snapshot_outbox_worker = omikron_manager::spawn_iota_snapshot_outbox_worker();
let port: u16 = env::var("PORT")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(443);
tokio::select! {
result = omikron_connection::start(port, state) => {
result = omikron_connection::start(port) => {
if let Err(e) = result {
log_err!(0, PrintType::General, "Server error: {:?}", e);
}
@ -102,5 +105,4 @@ async fn main() {
}
rate_limit_cleanup.abort();
short_link_cleanup.abort();
snapshot_outbox_worker.abort();
}

View file

@ -11,8 +11,7 @@ fn serialize_public_key<S>(
where
S: serde::Serializer,
{
let encoded = key.try_to_base64().map_err(serde::ser::Error::custom)?;
serializer.serialize_str(&encoded)
serializer.serialize_str(&key.to_base64())
}
pub use ids::{IotaId, OmikronId, UserId};

View file

@ -4,11 +4,10 @@ use mtp::crypto::PublicKeyBundle;
#[derive(Clone, Debug, serde::Serialize)]
pub struct User {
pub id: UserId,
pub iota_id: Option<IotaId>,
pub iota_id: IotaId,
pub username: String,
pub display: Option<String>,
pub status: Option<String>,
pub presence_preference: String,
pub about: Option<String>,
pub avatar: Option<Vec<u8>>,
pub sub_level: i32,
@ -17,5 +16,4 @@ pub struct User {
pub public_key: PublicKeyBundle,
#[serde(skip_serializing)]
pub token: String,
pub created_at: i64,
}

View file

@ -8,24 +8,20 @@ use crate::db::{
user_repo::{get_by_user_id, get_by_username},
};
use crate::error::{OmegaError, Result};
use crate::identity::OmegaIdentity;
use crate::load_keyring;
use crate::models::UserId;
use crate::server::{
middleware,
validation::{parse_positive_id, validate_non_empty},
with_cors,
};
use crate::transport::omikron_manager::{
get_all_connections, get_connected_omikron, get_iota_primary_omikron_connection,
get_random_omikron,
};
use crate::sql::user_online_tracker::{get_all_connections, get_iota_primary_omikron_connection};
use crate::transport::omikron_manager::{get_connected_omikron, get_random_omikron};
use crate::util::file_util::get_directory;
use base64::Engine as _;
use bytes::Bytes;
use http::{Method, StatusCode};
use mtp::webserver::{HttpRequest, HttpResponse, RouteParams};
use std::collections::BTreeMap;
use std::sync::Arc;
fn error_body(error: &OmegaError) -> String {
json(&StatusResponse {
@ -37,13 +33,13 @@ fn error_body(error: &OmegaError) -> String {
})
}
fn user_response(user: crate::models::User) -> Result<UserResponse> {
Ok(UserResponse {
fn user_response(user: crate::models::User) -> UserResponse {
UserResponse {
status: "success",
username: user.username,
public_key: user.public_key.try_to_base64()?,
public_key: user.public_key.to_base64(),
user_id: user.id.0,
iota_id: user.iota_id.map(|id| id.0),
iota_id: user.iota_id.0,
sub_level: user.sub_level,
sub_end: user.sub_end,
display: user.display,
@ -52,10 +48,10 @@ fn user_response(user: crate::models::User) -> Result<UserResponse> {
avatar: user
.avatar
.map(|value| base64::engine::general_purpose::STANDARD.encode(value)),
})
}
}
async fn route(path_parts: &[&str], identity: &OmegaIdentity) -> Result<(StatusCode, String)> {
async fn route(path_parts: &[&str]) -> Result<(StatusCode, String)> {
match path_parts {
["api", "get", "omikron"] => {
let connection = get_random_omikron()
@ -71,7 +67,7 @@ async fn route(path_parts: &[&str], identity: &OmegaIdentity) -> Result<(StatusC
json(&OmikronResponse {
status: "success",
id,
public_key: omikron.public_key.try_to_base64()?,
public_key: omikron.public_key.to_base64(),
ip_address: omikron.ip_address,
port: omikron.port,
}),
@ -85,17 +81,7 @@ async fn route(path_parts: &[&str], identity: &OmegaIdentity) -> Result<(StatusC
omikron_id
} else {
let user = get_by_user_id(UserId::from(id)).await?;
match user.iota_id {
Some(iota_id) => get_iota_primary_omikron_connection(iota_id.0),
None => {
get_random_omikron()
.await
.map_err(|_| OmegaError::NotFound)?
.get_omikron_id()
.await
}
}
.ok_or(OmegaError::NotFound)?
get_iota_primary_omikron_connection(user.iota_id.0).ok_or(OmegaError::NotFound)?
};
// Database rows describe registered Omikrons. The public discovery
@ -107,7 +93,7 @@ async fn route(path_parts: &[&str], identity: &OmegaIdentity) -> Result<(StatusC
json(&OmikronResponse {
status: "success",
id: omikron.id.0,
public_key: omikron.public_key.try_to_base64()?,
public_key: omikron.public_key.to_base64(),
ip_address: omikron.ip_address,
port: omikron.port,
}),
@ -122,7 +108,12 @@ async fn route(path_parts: &[&str], identity: &OmegaIdentity) -> Result<(StatusC
.map(|(omikron_id, iotas)| {
let iotas = iotas
.into_iter()
.map(|(iota_id, users)| (iota_id.to_string(), users.into_iter().collect()))
.map(|(iota_id, users)| {
(
iota_id.to_string(),
users.into_iter().map(i64::from).collect(),
)
})
.collect();
(omikron_id.to_string(), iotas)
})
@ -143,7 +134,7 @@ async fn route(path_parts: &[&str], identity: &OmegaIdentity) -> Result<(StatusC
json(&IotaResponse {
status: "success",
iota_id: iota.id.0,
public_key: iota.public_key.try_to_base64()?,
public_key: iota.public_key.to_base64(),
}),
))
}
@ -155,9 +146,9 @@ async fn route(path_parts: &[&str], identity: &OmegaIdentity) -> Result<(StatusC
json(&UsernameResponse {
status: "success",
username: user.username,
public_key: user.public_key.try_to_base64()?,
public_key: user.public_key.to_base64(),
user_id: user.id.0,
iota_id: user.iota_id.map(|id| id.0),
iota_id: user.iota_id.0,
sub_level: user.sub_level,
sub_end: user.sub_end,
}),
@ -165,7 +156,7 @@ async fn route(path_parts: &[&str], identity: &OmegaIdentity) -> Result<(StatusC
}
["api", "get", "public_key"] => {
let public_key = base64::engine::general_purpose::STANDARD
.encode(identity.public_key_bundle().try_as_bytes()?);
.encode(load_keyring().public_key_bundle().as_bytes());
Ok((
StatusCode::OK,
json(&PublicKeyResponse {
@ -177,7 +168,7 @@ async fn route(path_parts: &[&str], identity: &OmegaIdentity) -> Result<(StatusC
["api", "get", "user", id] => {
let id = parse_positive_id(id)?;
let user = get_by_user_id(UserId::from(id)).await?;
Ok((StatusCode::OK, json(&user_response(user)?)))
Ok((StatusCode::OK, json(&user_response(user))))
}
_ => Ok((
StatusCode::INTERNAL_SERVER_ERROR,
@ -186,42 +177,43 @@ async fn route(path_parts: &[&str], identity: &OmegaIdentity) -> Result<(StatusC
}
}
pub async fn handle(
request: HttpRequest,
response: HttpResponse,
identity: Arc<OmegaIdentity>,
) -> HttpResponse {
pub async fn handle(request: HttpRequest, response: HttpResponse) -> HttpResponse {
let method = request.method;
let path = request.uri.path().to_string();
if method != Method::OPTIONS && !middleware::allow(request.remote_addr.ip(), &path) {
return with_cors(response.status(StatusCode::TOO_MANY_REQUESTS).body(json(
&StatusResponse {
return response
.status(StatusCode::TOO_MANY_REQUESTS)
.header("access-control-allow-origin", &crate::config::cors_origin())
.body(json(&StatusResponse {
status: "error_rate_limited",
},
)));
}));
}
if method == Method::OPTIONS {
return with_cors(response.status(StatusCode::NO_CONTENT));
return response
.status(StatusCode::OK)
.header("access-control-allow-origin", &crate::config::cors_origin())
.header("access-control-allow-methods", "GET, POST, OPTIONS")
.header("access-control-allow-headers", "*");
}
let path_parts: Vec<&str> = path.split('/').filter(|part| !part.is_empty()).collect();
if let ["api", "download", "iota_frontend"] = path_parts.as_slice() {
let file_path = format!("{}/downloads/iota_frontend.zip", get_directory());
return match std::fs::read(file_path) {
Ok(bytes) => with_cors(
response
Ok(bytes) => response
.status(StatusCode::OK)
.header("access-control-allow-origin", &crate::config::cors_origin())
.header("content-type", "application/zip")
.header(
"content-disposition",
"attachment; filename=\"iota_frontend.zip\"",
)
.body(Bytes::from(bytes)),
),
Err(_) => with_cors(response.status(StatusCode::NOT_FOUND).body(json(
&StatusResponse {
Err(_) => response
.status(StatusCode::NOT_FOUND)
.header("access-control-allow-origin", &crate::config::cors_origin())
.body(json(&StatusResponse {
status: "error_not_found",
},
))),
})),
};
}
if let ["direct", short @ ..] = path_parts.as_slice() {
@ -229,23 +221,25 @@ pub async fn handle(
let location = crate::server::short_link::get_short_link(&short)
.await
.unwrap_or_else(|_| "https://tensamin.net".to_string());
return with_cors(
response
return response
.status(StatusCode::TEMPORARY_REDIRECT)
.header("location", &location),
);
.header("location", &location);
}
let (status, body) = route(&path_parts, &identity)
let (status, body) = route(&path_parts)
.await
.unwrap_or_else(|error| (error.status_code(), error_body(&error)));
with_cors(response.status(status).body(body))
response
.status(status)
.header("access-control-allow-origin", &crate::config::cors_origin())
.header("access-control-allow-headers", "*")
.header("access-control-allow-methods", "GET, POST, OPTIONS")
.body(body)
}
pub async fn handle_pattern(
request: HttpRequest,
response: HttpResponse,
_params: RouteParams,
identity: Arc<OmegaIdentity>,
) -> HttpResponse {
handle(request, response, identity).await
handle(request, response).await
}

View file

@ -1,8 +1,6 @@
use http::StatusCode;
use mtp::webserver::HttpResponse;
use crate::server::with_cors;
pub fn index_handler(response: HttpResponse) -> HttpResponse {
let documentation = r#"
Omega API Server
@ -35,10 +33,8 @@ live in-memory state; it is empty after an Omega restart until Omikrons sync.
All other routes will return this documentation.
"#;
with_cors(
response
.status(StatusCode::OK)
.header("content-type", "text/plain; charset=utf-8")
.body(documentation),
)
.body(documentation)
}

View file

@ -1,25 +1,14 @@
use dashmap::DashMap;
use once_cell::sync::Lazy;
use std::net::IpAddr;
use std::{collections::VecDeque, sync::OnceLock, time::Instant};
use std::{collections::VecDeque, time::Instant};
use tokio::time::interval;
static REQUESTS: Lazy<DashMap<(IpAddr, String), VecDeque<Instant>>> = Lazy::new(DashMap::new);
static CONFIG: OnceLock<crate::config::RateLimitConfig> = OnceLock::new();
static CONFIG: Lazy<crate::config::RateLimitConfig> =
Lazy::new(crate::config::RateLimitConfig::from_env);
const MAX_TRACKED_CLIENT_BUCKETS: usize = 100_000;
pub(crate) fn initialize_config(
config: crate::config::RateLimitConfig,
) -> Result<(), crate::config::RateLimitConfig> {
CONFIG.set(config)
}
fn config() -> &'static crate::config::RateLimitConfig {
static FALLBACK: Lazy<crate::config::RateLimitConfig> =
Lazy::new(crate::config::RateLimitConfig::from_env_or_default);
CONFIG.get().unwrap_or(&FALLBACK)
}
pub fn allow(remote_addr: IpAddr, path: &str) -> bool {
let key = if path.contains("register") {
"registration"
@ -34,15 +23,15 @@ pub fn allow(remote_addr: IpAddr, path: &str) -> bool {
}
}
let limit = if key == "registration" {
config().registration_requests
CONFIG.registration_requests
} else {
config().general_requests
CONFIG.general_requests
};
let now = Instant::now();
let mut entries = REQUESTS.entry(map_key).or_default();
while entries
.front()
.is_some_and(|time| now.duration_since(*time) >= config().window)
.is_some_and(|time| now.duration_since(*time) >= CONFIG.window)
{
entries.pop_front();
}
@ -55,7 +44,7 @@ pub fn allow(remote_addr: IpAddr, path: &str) -> bool {
pub fn spawn_cleanup_task() -> tokio::task::JoinHandle<()> {
tokio::spawn(async {
let mut ticker = interval(config().window);
let mut ticker = interval(CONFIG.window);
loop {
ticker.tick().await;
cleanup_expired();
@ -68,7 +57,7 @@ fn cleanup_expired() {
REQUESTS.retain(|_, entries| {
while entries
.front()
.is_some_and(|time| now.duration_since(*time) >= config().window)
.is_some_and(|time| now.duration_since(*time) >= CONFIG.window)
{
entries.pop_front();
}

View file

@ -1,32 +1,6 @@
pub mod api;
pub mod index;
pub mod middleware;
pub mod server;
pub mod short_link;
pub mod validation;
pub mod web;
use mtp::webserver::HttpResponse;
pub(crate) fn with_cors(response: HttpResponse) -> HttpResponse {
response
.header("access-control-allow-origin", crate::config::cors_origin())
.header("access-control-allow-methods", "GET, POST, OPTIONS")
.header("access-control-allow-headers", "*")
}
#[cfg(test)]
mod tests {
use super::with_cors;
use mtp::webserver::HttpResponse;
#[test]
fn web_responses_allow_all_origins() {
let response = with_cors(HttpResponse::default());
assert_eq!(response.headers["access-control-allow-origin"], "*");
assert_eq!(
response.headers["access-control-allow-methods"],
"GET, POST, OPTIONS"
);
assert_eq!(response.headers["access-control-allow-headers"], "*");
}
}

16
src/server/server.rs Normal file
View file

@ -0,0 +1,16 @@
use crate::server::{api, index::index_handler};
use mtp::webserver::WebServerConfig;
pub fn build_web_config() -> Result<WebServerConfig, mtp::webserver::RouterError> {
WebServerConfig::new()
.route("/api/download/iota_frontend", api::handle)?
.route("/api/get/omikron", api::handle)?
.route("/api/get/connections", api::handle)?
.route("/api/get/public_key", api::handle)?
.route_pattern("/api/get/omikron/{id}", api::handle_pattern)?
.route_pattern("/api/get/iota/{id}", api::handle_pattern)?
.route_pattern("/api/get/id/{username}", api::handle_pattern)?
.route_pattern("/api/get/user/{id}", api::handle_pattern)?
.route_pattern("/direct/{short}", api::handle_pattern)?
.fallback(|_request, response| async move { index_handler(response) })
}

View file

@ -34,7 +34,7 @@ pub async fn get_short_link(short: &str) -> Result<String, ()> {
short
};
let frag = short.replace(key, "");
let normalized = normalize_short(key);
let normalized = normalize_short(&key);
let target = short_link_repo::get(&normalized)
.await

View file

@ -1,54 +0,0 @@
use crate::identity::OmegaIdentity;
use crate::server::{api, index::index_handler};
use http::{Method, StatusCode};
use mtp::webserver::{HttpRequest, HttpResponse, RouteParams, WebServerConfig};
use std::{future::Future, pin::Pin, sync::Arc};
type RouteFuture = Pin<Box<dyn Future<Output = HttpResponse> + Send>>;
fn api_handler(
identity: Arc<OmegaIdentity>,
) -> impl Fn(HttpRequest, HttpResponse) -> RouteFuture + Send + Sync + 'static {
move |request, response| Box::pin(api::handle(request, response, identity.clone()))
}
fn api_pattern_handler(
identity: Arc<OmegaIdentity>,
) -> impl Fn(HttpRequest, HttpResponse, RouteParams) -> RouteFuture + Send + Sync + 'static {
move |request, response, params| {
Box::pin(api::handle_pattern(
request,
response,
params,
identity.clone(),
))
}
}
pub fn build_web_config(
identity: Arc<OmegaIdentity>,
) -> Result<WebServerConfig, mtp::webserver::RouterError> {
WebServerConfig::new()
.route("/api/download/iota_frontend", api_handler(identity.clone()))?
.route("/api/get/omikron", api_handler(identity.clone()))?
.route("/api/get/connections", api_handler(identity.clone()))?
.route("/api/get/public_key", api_handler(identity.clone()))?
.route_pattern(
"/api/get/omikron/{id}",
api_pattern_handler(identity.clone()),
)?
.route_pattern("/api/get/iota/{id}", api_pattern_handler(identity.clone()))?
.route_pattern(
"/api/get/id/{username}",
api_pattern_handler(identity.clone()),
)?
.route_pattern("/api/get/user/{id}", api_pattern_handler(identity.clone()))?
.route_pattern("/direct/{short}", api_pattern_handler(identity))?
.fallback(|request, response| async move {
if request.method == Method::OPTIONS {
crate::server::with_cors(response.status(StatusCode::NO_CONTENT))
} else {
index_handler(response)
}
})
}

View file

@ -1,4 +1,7 @@
#[derive(Debug, Clone, PartialEq, Eq)]
use strum::IntoEnumIterator;
use strum_macros::EnumIter;
#[derive(Debug, Clone, PartialEq, EnumIter, Eq)]
#[allow(unused, non_camel_case_types)]
pub enum UserStatus {
user_offline,
@ -12,78 +15,17 @@ pub enum UserStatus {
iota_online,
iota_borked,
}
#[allow(unused)]
impl UserStatus {
pub fn from_client_preference(s: &str) -> Option<Self> {
match s {
"user_online" => Some(Self::user_online),
"user_idle" => Some(Self::user_idle),
"user_dnd" => Some(Self::user_dnd),
"user_wc" => Some(Self::user_wc),
"user_invisible" => Some(Self::user_invisible),
_ => None,
pub fn to_string(&self) -> String {
format!("{:?}", self)
}
pub fn from_str(s: &str) -> Option<UserStatus> {
for sel in UserStatus::iter() {
if &sel.to_string() == s {
return Some(sel);
}
}
pub fn public_value(&self) -> Self {
match self {
Self::user_invisible => Self::user_offline,
value => value.clone(),
}
}
/// Parse a value received from a client or persisted as an account
/// preference. Derived connectivity and diagnostic states are never valid
/// preferences.
#[allow(dead_code)]
pub fn from_str(s: &str) -> Option<Self> {
Self::from_client_preference(s)
}
}
impl std::fmt::Display for UserStatus {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "{self:?}")
}
}
#[cfg(test)]
mod tests {
use super::UserStatus;
#[test]
fn accepts_only_client_preferences() {
for value in [
"user_online",
"user_idle",
"user_dnd",
"user_wc",
"user_invisible",
] {
assert!(
UserStatus::from_client_preference(value).is_some(),
"{value}"
);
}
for value in [
"user_offline",
"iota_offline",
"iota_online",
"user_borked",
"iota_borked",
"unknown",
] {
assert_eq!(UserStatus::from_client_preference(value), None, "{value}");
assert_eq!(UserStatus::from_str(value), None, "{value}");
}
}
#[test]
fn invisible_is_publicly_offline() {
assert_eq!(
UserStatus::user_invisible.public_value(),
UserStatus::user_offline
);
assert_eq!(UserStatus::user_dnd.public_value(), UserStatus::user_dnd);
None
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,143 +0,0 @@
use crate::{
config::OmegaConfig, identity::OmegaIdentity, sql::user_online_tracker::PresenceTracker,
};
use dashmap::DashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::Semaphore;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum AccountChallengeOperation {
Attach,
Delete,
}
#[derive(Clone, Debug)]
pub struct AccountChallenge {
pub nonce: u64,
pub created_at: Instant,
}
pub struct OmegaState {
pub presence: Arc<PresenceTracker>,
pub identity: Arc<OmegaIdentity>,
pub(crate) config: Arc<OmegaConfig>,
pub(crate) global_handler_limit: Arc<Semaphore>,
pub(crate) omikron_handler_concurrency: usize,
challenges: DashMap<(AccountChallengeOperation, i64, i64), AccountChallenge>,
}
impl OmegaState {
fn with_handler_limits(identity: OmegaIdentity, config: OmegaConfig) -> Self {
let omikron_handler_concurrency = config.dispatch.omikron_handler_concurrency;
let global_handler_concurrency = config.dispatch.global_handler_concurrency;
Self {
presence: Arc::new(PresenceTracker::default()),
identity: Arc::new(identity),
config: Arc::new(config),
global_handler_limit: Arc::new(Semaphore::new(global_handler_concurrency)),
omikron_handler_concurrency,
challenges: DashMap::new(),
}
}
pub fn new(identity: OmegaIdentity, config: OmegaConfig) -> Arc<Self> {
Arc::new(Self::with_handler_limits(identity, config))
}
#[cfg(test)]
fn test_state() -> Arc<Self> {
use mtp::crypto::Keyring;
let mut config = OmegaConfig::default();
config.dispatch.omikron_handler_concurrency = 4;
config.dispatch.global_handler_concurrency = 8;
Arc::new(Self::with_handler_limits(
OmegaIdentity::from_keyring(Keyring::generate()),
config,
))
}
pub fn issue_challenge(
&self,
operation: AccountChallengeOperation,
user_id: i64,
requester_iota_id: i64,
) -> u64 {
let nonce = rand::random::<u64>();
self.challenges.insert(
(operation, user_id, requester_iota_id),
AccountChallenge {
nonce,
created_at: Instant::now(),
},
);
nonce
}
pub fn consume_challenge(
&self,
operation: AccountChallengeOperation,
user_id: i64,
requester_iota_id: i64,
nonce: u64,
) -> bool {
self.challenges
.remove(&(operation, user_id, requester_iota_id))
.is_some_and(|(_, value)| {
value.nonce == nonce && value.created_at.elapsed() <= Duration::from_secs(120)
})
}
}
#[cfg(test)]
mod tests {
use super::OmegaState;
use crate::sql::connection_status::UserStatus;
#[test]
fn state_instances_have_independent_presence_trackers() {
let first = OmegaState::test_state();
let second = OmegaState::test_state();
first.presence.connect_iota(11, 42);
assert!(first.presence.has_iota_route(11));
assert!(!second.presence.has_iota_route(11));
assert_eq!(first.presence.primary_iota_route(11), Some(42));
assert_eq!(second.presence.primary_iota_route(11), None);
}
#[test]
fn two_session_private_and_public_presence_flow_is_authoritative() {
let state = OmegaState::test_state();
state.presence.set_preference(7, UserStatus::user_online);
state.presence.set_preference(8, UserStatus::user_online);
state.presence.connect_iota(11, 42);
state.presence.track_session(7, 100, 42, 11);
state.presence.track_session(7, 101, 42, 11);
state.presence.replace_subscription(7, 100, 42, vec![8]);
assert_eq!(
state.presence.resolve_public_state(8, 11),
UserStatus::user_offline
);
state.presence.set_preference(8, UserStatus::user_invisible);
assert_eq!(
state.presence.resolve_public_state(8, 11),
UserStatus::user_offline
);
assert_eq!(
state.presence.resolve_private_state(8),
UserStatus::user_invisible
);
state.presence.remove_session(7, 100, 42);
assert!(state.presence.owns_session(7, 101, 42));
state.presence.remove_session(7, 101, 42);
assert_eq!(
state.presence.resolve_public_state(7, 11),
UserStatus::user_offline
);
}
}

View file

@ -1,147 +0,0 @@
use std::collections::BTreeSet;
const OMIKRON_PREFIX: &str = "omikron;caps=";
const OMEGA_PREFIX: &str = "omega;caps=";
const SET_USER_STATE: &str = "set_user_state_v1";
const STATE_SUBSCRIBE: &str = "state_subscribe_v1";
const SESSION_SNAPSHOT: &str = "session_snapshot_v1";
const CLIENT_STATE_PUSH: &str = "client_state_push_v1";
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct PeerCapabilities {
pub set_user_state_v1: bool,
pub state_subscribe_v1: bool,
pub session_snapshot_v1: bool,
pub client_state_push_v1: bool,
}
impl PeerCapabilities {
/// A missing descriptor is the legacy protocol: tuple route snapshots,
/// GetStates-only subscription refreshes, and ClientChanged pushes.
pub fn from_identification_description(description: Option<&str>) -> Result<Self, ()> {
parse_capabilities(description, OMIKRON_PREFIX)
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct OmegaCapabilities {
pub set_user_state_v1: bool,
pub state_subscribe_v1: bool,
pub session_snapshot_v1: bool,
pub client_state_push_v1: bool,
}
impl OmegaCapabilities {
pub fn current() -> Self {
Self {
set_user_state_v1: true,
state_subscribe_v1: true,
session_snapshot_v1: true,
client_state_push_v1: true,
}
}
pub fn identification_description(&self) -> String {
let mut names = Vec::new();
if self.set_user_state_v1 {
names.push(SET_USER_STATE);
}
if self.state_subscribe_v1 {
names.push(STATE_SUBSCRIBE);
}
if self.session_snapshot_v1 {
names.push(SESSION_SNAPSHOT);
}
if self.client_state_push_v1 {
names.push(CLIENT_STATE_PUSH);
}
format!("{OMEGA_PREFIX}{}", names.join(","))
}
}
fn parse_capabilities(description: Option<&str>, prefix: &str) -> Result<PeerCapabilities, ()> {
let Some(description) = description else {
return Ok(PeerCapabilities::default());
};
if description == "omikron" {
return Ok(PeerCapabilities::default());
}
let Some(capabilities) = description.strip_prefix(prefix) else {
return Err(());
};
let mut seen = BTreeSet::new();
for capability in capabilities.split(',') {
if capability.is_empty() || !seen.insert(capability) {
return Err(());
}
}
if seen.iter().any(|capability| {
!matches!(
*capability,
SET_USER_STATE | STATE_SUBSCRIBE | SESSION_SNAPSHOT | CLIENT_STATE_PUSH
)
}) {
return Err(());
}
Ok(PeerCapabilities {
set_user_state_v1: seen.contains(SET_USER_STATE),
state_subscribe_v1: seen.contains(STATE_SUBSCRIBE),
session_snapshot_v1: seen.contains(SESSION_SNAPSHOT),
client_state_push_v1: seen.contains(CLIENT_STATE_PUSH),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn advertised_capabilities_are_parsed() {
let capabilities = PeerCapabilities::from_identification_description(Some(
"omikron;caps=set_user_state_v1,state_subscribe_v1,session_snapshot_v1,client_state_push_v1",
)).unwrap();
assert!(capabilities.set_user_state_v1);
assert!(capabilities.state_subscribe_v1);
assert!(capabilities.session_snapshot_v1);
assert!(capabilities.client_state_push_v1);
}
#[test]
fn invalid_capability_values_fail_identification() {
assert!(
PeerCapabilities::from_identification_description(Some("omikron;caps=unsupported"))
.is_err()
);
}
#[test]
fn legacy_peer_has_no_version_specific_features() {
let capabilities = PeerCapabilities::from_identification_description(None).unwrap();
assert!(!capabilities.set_user_state_v1);
assert!(!capabilities.state_subscribe_v1);
assert!(!capabilities.session_snapshot_v1);
assert!(!capabilities.client_state_push_v1);
assert_eq!(
PeerCapabilities::from_identification_description(Some("omikron")),
Ok(PeerCapabilities::default())
);
}
#[test]
fn reconnecting_with_the_same_identification_is_stable() {
let description = Some(
"omikron;caps=set_user_state_v1,state_subscribe_v1,session_snapshot_v1,client_state_push_v1",
);
assert_eq!(
PeerCapabilities::from_identification_description(description),
PeerCapabilities::from_identification_description(description)
);
}
#[test]
fn omega_capability_description_is_distinct_from_omikron_capabilities() {
let description = OmegaCapabilities::current().identification_description();
assert!(description.starts_with(OMEGA_PREFIX));
assert!(PeerCapabilities::from_identification_description(Some(&description)).is_err());
}
}

View file

@ -1,178 +1 @@
pub(crate) use super::omikron_connection::{OmikronConnection, OmikronResult};
use mtp::codec::{CommunicationValue, DataValue};
pub(crate) trait RequiredMtpFields {
/// Require a nonzero request correlation ID. `None` and `Some(0)` are
/// distinct MTP wire states, but both are invalid for Omega requests.
fn require_id(&self) -> OmikronResult<u32>;
/// Require a nonzero authenticated peer identity.
fn require_sender(&self) -> OmikronResult<u64>;
/// Require a nonzero application routing target.
fn require_receiver(&self) -> OmikronResult<u64>;
fn require_sender_i64(&self) -> OmikronResult<i64>;
fn require_receiver_i64(&self) -> OmikronResult<i64>;
}
impl RequiredMtpFields for CommunicationValue {
fn require_id(&self) -> OmikronResult<u32> {
self.id().filter(|id| *id != 0).ok_or_else(|| {
crate::OmegaError::Validation("MTP message is missing request id".into())
})
}
fn require_sender(&self) -> OmikronResult<u64> {
self.sender()
.filter(|sender| *sender != 0)
.ok_or_else(|| crate::OmegaError::Validation("MTP message is missing sender".into()))
}
fn require_receiver(&self) -> OmikronResult<u64> {
self.receiver()
.filter(|receiver| *receiver != 0)
.ok_or_else(|| crate::OmegaError::Validation("MTP message is missing receiver".into()))
}
fn require_sender_i64(&self) -> OmikronResult<i64> {
positive_i64(self.require_sender()?, "sender")
}
fn require_receiver_i64(&self) -> OmikronResult<i64> {
positive_i64(self.require_receiver()?, "receiver")
}
}
pub(crate) fn positive_i64(value: impl TryInto<i128>, field: &str) -> OmikronResult<i64> {
let value = value
.try_into()
.map_err(|_| crate::OmegaError::Validation(format!("invalid {field}")))?;
let value = i64::try_from(value)
.map_err(|_| crate::OmegaError::Validation(format!("invalid {field}")))?;
if value <= 0 {
return Err(crate::OmegaError::Validation(format!("invalid {field}")));
}
Ok(value)
}
pub(crate) fn validate_dispatch_fields(value: &CommunicationValue) -> OmikronResult<()> {
let Some(message_type) = value.get_comm_type_enum() else {
return Err(crate::OmegaError::Validation(
"MTP message has an unknown communication type".into(),
));
};
if !matches!(
message_type,
mtp::codec::CommunicationType::ClientChanged
| mtp::codec::CommunicationType::PushNotification
) {
value.require_id()?;
}
if matches!(
message_type,
mtp::codec::CommunicationType::GetUserData
| mtp::codec::CommunicationType::ChangeUserData
| mtp::codec::CommunicationType::DeleteUser
| mtp::codec::CommunicationType::AttachUserBegin
| mtp::codec::CommunicationType::AttachUserComplete
| mtp::codec::CommunicationType::DeleteUserCredentialBegin
| mtp::codec::CommunicationType::DeleteUserCredentialComplete
| mtp::codec::CommunicationType::EraseHostedUserDataAck
| mtp::codec::CommunicationType::ReleaseUserFromIota
| mtp::codec::CommunicationType::DeleteIota
| mtp::codec::CommunicationType::GetNotifications
| mtp::codec::CommunicationType::ReadNotification
| mtp::codec::CommunicationType::StateSubscribe
) {
value.require_sender()?;
}
Ok(())
}
pub(crate) trait OptionalDataValueCompat {
fn as_number(&self) -> Option<i128>;
fn as_signed_number(&self) -> Option<i128>;
fn as_str(&self) -> Option<&str>;
fn as_bytes(&self) -> Option<Vec<u8>>;
}
impl OptionalDataValueCompat for Option<&DataValue> {
fn as_number(&self) -> Option<i128> {
self.and_then(|value| value.as_number())
}
fn as_signed_number(&self) -> Option<i128> {
self.and_then(|value| value.as_signed_number())
}
fn as_str(&self) -> Option<&str> {
self.and_then(|value| value.as_str())
}
fn as_bytes(&self) -> Option<Vec<u8>> {
self.and_then(|value| value.as_bytes())
}
}
#[cfg(test)]
mod tests {
use super::{RequiredMtpFields, validate_dispatch_fields};
use mtp::codec::{CommunicationType, CommunicationValue};
#[test]
fn missing_request_id_is_rejected_without_a_zero_fallback() {
let value = CommunicationValue::new(CommunicationType::GetUserData).without_id();
assert!(value.require_id().is_err());
assert!(validate_dispatch_fields(&value).is_err());
}
#[test]
fn zero_routing_values_are_rejected_by_omega_contract() {
let value = CommunicationValue::new(CommunicationType::GetUserData)
.with_id(0)
.with_sender(0)
.with_receiver(0);
assert!(value.require_id().is_err());
assert!(value.require_sender().is_err());
assert!(value.require_receiver().is_err());
assert!(validate_dispatch_fields(&value).is_err());
}
#[test]
fn security_sensitive_messages_require_sender() {
for message_type in [
CommunicationType::GetUserData,
CommunicationType::ChangeUserData,
CommunicationType::DeleteUser,
CommunicationType::AttachUserBegin,
CommunicationType::AttachUserComplete,
CommunicationType::DeleteUserCredentialBegin,
CommunicationType::DeleteUserCredentialComplete,
CommunicationType::EraseHostedUserDataAck,
CommunicationType::ReleaseUserFromIota,
CommunicationType::DeleteIota,
CommunicationType::GetNotifications,
CommunicationType::ReadNotification,
CommunicationType::StateSubscribe,
] {
let value = CommunicationValue::new(message_type).with_id(1);
assert!(value.require_sender().is_err());
assert!(validate_dispatch_fields(&value).is_err());
}
}
#[test]
fn optional_client_changed_id_remains_optional() {
let value = CommunicationValue::new(CommunicationType::ClientChanged).without_id();
assert!(validate_dispatch_fields(&value).is_ok());
}
#[test]
fn push_notification_uses_its_logical_sender_field() {
let value = CommunicationValue::new(CommunicationType::PushNotification);
assert!(validate_dispatch_fields(&value).is_ok());
}
}
pub(crate) use super::omikron_connection::OmikronConnection;

View file

@ -1,15 +1,9 @@
use super::super::connection::{
OmikronConnection, OmikronResult, OptionalDataValueCompat, RequiredMtpFields,
};
use super::super::omikron_connection::{OmikronConnection, OmikronResult};
use crate::{
db::{iota_repo, user_repo},
models::{IotaId, UserId},
state::AccountChallengeOperation,
};
use mtp::{
codec::{CommunicationType, CommunicationValue, DataType, DataValue},
crypto::{verify_ed25519, verify_ml_dsa},
};
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
use std::sync::Arc;
async fn delete(
@ -22,417 +16,28 @@ async fn delete(
Err(error) => CommunicationValue::new(CommunicationType::ErrorInternal)
.add_typed_default(DataType::ErrorType, DataValue::Str(error.to_string())),
};
connection
.send(&response.with_id(value.require_id()?))
.await
connection.send(&response.with_id(value.get_id())).await
}
pub async fn user(
connection: Arc<OmikronConnection>,
value: CommunicationValue,
) -> OmikronResult<()> {
value.require_id()?;
value.require_sender_i64()?;
let user_id = UserId::from(value.require_sender_i64()?);
complete_delete(connection, value, user_id).await
delete(
connection,
value.clone(),
user_repo::delete_user(UserId::from(value.get_sender() as i64)),
)
.await
}
pub async fn iota(
connection: Arc<OmikronConnection>,
value: CommunicationValue,
) -> OmikronResult<()> {
value.require_id()?;
value.require_sender_i64()?;
delete(
connection,
value.clone(),
iota_repo::delete_iota(IotaId::from(value.require_sender_i64()?)),
iota_repo::delete_iota(IotaId::from(value.get_sender() as i64)),
)
.await
}
pub async fn release_from_iota(
connection: Arc<OmikronConnection>,
value: CommunicationValue,
) -> OmikronResult<()> {
value.require_id()?;
value.require_sender_i64()?;
let Some(user_id) = value
.get_data(DataType::UserId)
.as_signed_number()
.and_then(|id| i64::try_from(id).ok())
.filter(|id| *id > 0)
else {
return connection
.send_error_response(value.require_id()?, CommunicationType::ErrorInvalidUserId)
.await;
};
let requester = IotaId::from(value.require_sender_i64()?);
let Ok(user) = user_repo::get_by_user_id(UserId::from(user_id)).await else {
return connection
.send_error_response(value.require_id()?, CommunicationType::ErrorNotFound)
.await;
};
if user.iota_id != Some(requester) {
return connection
.send_error_response(
value.require_id()?,
CommunicationType::ErrorNotAuthenticated,
)
.await;
}
let previous_iota = user.iota_id;
match user_repo::change_iota_id(user.id, None).await {
Ok(()) => {
if let Some(iota) = previous_iota {
let _ = crate::transport::omikron_manager::publish_iota_user_snapshot(iota.0).await;
}
connection
.send(
&CommunicationValue::new(CommunicationType::Success)
.with_id(value.require_id()?),
)
.await
}
Err(error) => {
connection
.send(
&CommunicationValue::new(CommunicationType::ErrorInternal)
.with_id(value.require_id()?)
.add_typed_default(DataType::ErrorType, DataValue::Str(error.to_string())),
)
.await
}
}
}
fn lifecycle_payload(domain: &[u8], user_id: i64, iota_id: i64, nonce: u64) -> Vec<u8> {
let mut payload = Vec::with_capacity(domain.len() + 32);
payload.extend_from_slice(domain);
payload.extend_from_slice(&user_id.to_be_bytes());
payload.extend_from_slice(&iota_id.to_be_bytes());
payload.extend_from_slice(&nonce.to_be_bytes());
payload
}
pub async fn attach_begin(
connection: Arc<OmikronConnection>,
value: CommunicationValue,
) -> OmikronResult<()> {
value.require_id()?;
value.require_sender_i64()?;
let Some(user_id) = value
.get_data(DataType::UserId)
.as_signed_number()
.and_then(|v| i64::try_from(v).ok())
.filter(|v| *v > 0)
else {
return connection
.send_error_response(value.require_id()?, CommunicationType::ErrorInvalidUserId)
.await;
};
if user_repo::get_by_user_id(UserId::from(user_id))
.await
.is_err()
{
return connection
.send_error_response(value.require_id()?, CommunicationType::ErrorNotFound)
.await;
}
let requester = value.require_sender_i64()?;
let nonce =
connection
.state()
.issue_challenge(AccountChallengeOperation::Attach, user_id, requester);
connection
.send(
&CommunicationValue::new(CommunicationType::AttachUserChallenge)
.with_id(value.require_id()?)
.add_typed_default(DataType::UserId, DataValue::SignedNumber(user_id.into()))
.add_typed_default(DataType::ServerNonce, DataValue::SignedNumber(nonce.into())),
)
.await
}
pub async fn attach_complete(
connection: Arc<OmikronConnection>,
value: CommunicationValue,
) -> OmikronResult<()> {
value.require_id()?;
value.require_sender_i64()?;
let Some(user_id) = value
.get_data(DataType::UserId)
.as_signed_number()
.and_then(|v| i64::try_from(v).ok())
.filter(|v| *v > 0)
else {
return connection
.send_error_response(value.require_id()?, CommunicationType::ErrorInvalidUserId)
.await;
};
let requester = value.require_sender_i64()?;
let Some(nonce) = value
.get_data(DataType::ServerNonce)
.as_signed_number()
.and_then(|v| u64::try_from(v).ok())
else {
return connection
.send_error_response(
value.require_id()?,
CommunicationType::ErrorInvalidChallenge,
)
.await;
};
let signature = value.get_data(DataType::Signature).as_bytes();
let pq_signature = value.get_data(DataType::PqSignature).as_bytes();
let (Some(signature), Some(pq_signature)) = (signature, pq_signature) else {
return connection
.send_error_response(
value.require_id()?,
CommunicationType::ErrorInvalidChallenge,
)
.await;
};
if !connection.state().consume_challenge(
AccountChallengeOperation::Attach,
user_id,
requester,
nonce,
) {
return connection
.send_error_response(
value.require_id()?,
CommunicationType::ErrorInvalidChallenge,
)
.await;
}
let Ok(user) = user_repo::get_by_user_id(UserId::from(user_id)).await else {
return connection
.send_error_response(value.require_id()?, CommunicationType::ErrorNotFound)
.await;
};
let payload = lifecycle_payload(b"tensamin:user-attach:v1\0", user_id, requester, nonce);
if verify_ed25519(&user.public_key.sig_cl_public_key, &payload, &signature).is_err()
|| verify_ml_dsa(&user.public_key.sig_pq_public_key, &payload, &pq_signature).is_err()
{
return connection
.send_error_response(
value.require_id()?,
CommunicationType::ErrorNotAuthenticated,
)
.await;
}
let previous_iota = user.iota_id;
match user_repo::change_iota_id(user.id, Some(IotaId::from(requester))).await {
Ok(()) => {
if let Some(iota) = previous_iota.filter(|id| id.0 != requester) {
let _ = crate::transport::omikron_manager::publish_iota_user_snapshot(iota.0).await;
}
let _ = crate::transport::omikron_manager::publish_iota_user_snapshot(requester).await;
connection
.send(
&CommunicationValue::new(CommunicationType::Success)
.with_id(value.require_id()?),
)
.await
}
Err(_) => {
connection
.send_error_response(value.require_id()?, CommunicationType::ErrorInternal)
.await
}
}
}
async fn complete_delete(
connection: Arc<OmikronConnection>,
value: CommunicationValue,
user_id: UserId,
) -> OmikronResult<()> {
match user_repo::delete_user_with_pending_erasure(user_id).await {
Ok(iota_id) => {
let cleanup_pending = iota_id.is_some();
if let Some(iota_id) = iota_id {
let _ =
crate::transport::omikron_manager::publish_iota_user_snapshot(iota_id.0).await;
crate::transport::omikron_manager::deliver_pending_erasures(iota_id.0).await;
}
connection
.send(
&CommunicationValue::new(CommunicationType::Success)
.with_id(value.require_id()?)
.add_typed_default(
DataType::CleanupPending,
DataValue::Bool(cleanup_pending),
),
)
.await
}
Err(crate::error::OmegaError::NotFound) => {
connection
.send_error_response(value.require_id()?, CommunicationType::ErrorNotFound)
.await
}
Err(error) => {
connection
.send(
&CommunicationValue::new(CommunicationType::ErrorInternal)
.with_id(value.require_id()?)
.add_typed_default(DataType::ErrorType, DataValue::Str(error.to_string())),
)
.await
}
}
}
pub async fn delete_credential_begin(
connection: Arc<OmikronConnection>,
value: CommunicationValue,
) -> OmikronResult<()> {
value.require_id()?;
value.require_sender_i64()?;
let Some(user_id) = value
.get_data(DataType::UserId)
.as_signed_number()
.and_then(|v| i64::try_from(v).ok())
.filter(|v| *v > 0)
else {
return connection
.send_error_response(value.require_id()?, CommunicationType::ErrorInvalidUserId)
.await;
};
if user_repo::get_by_user_id(UserId::from(user_id))
.await
.is_err()
{
return connection
.send_error_response(value.require_id()?, CommunicationType::ErrorNotFound)
.await;
}
let requester = value.require_sender_i64()?;
let nonce =
connection
.state()
.issue_challenge(AccountChallengeOperation::Delete, user_id, requester);
connection
.send(
&CommunicationValue::new(CommunicationType::DeleteUserCredentialChallenge)
.with_id(value.require_id()?)
.add_typed_default(DataType::UserId, DataValue::SignedNumber(user_id.into()))
.add_typed_default(DataType::ServerNonce, DataValue::SignedNumber(nonce.into())),
)
.await
}
pub async fn delete_credential_complete(
connection: Arc<OmikronConnection>,
value: CommunicationValue,
) -> OmikronResult<()> {
value.require_id()?;
value.require_sender_i64()?;
let Some(user_id) = value
.get_data(DataType::UserId)
.as_signed_number()
.and_then(|v| i64::try_from(v).ok())
.filter(|v| *v > 0)
else {
return connection
.send_error_response(value.require_id()?, CommunicationType::ErrorInvalidUserId)
.await;
};
let requester = value.require_sender_i64()?;
let Some(nonce) = value
.get_data(DataType::ServerNonce)
.as_signed_number()
.and_then(|v| u64::try_from(v).ok())
else {
return connection
.send_error_response(
value.require_id()?,
CommunicationType::ErrorInvalidChallenge,
)
.await;
};
let (Some(signature), Some(pq_signature)) = (
value.get_data(DataType::Signature).as_bytes(),
value.get_data(DataType::PqSignature).as_bytes(),
) else {
return connection
.send_error_response(
value.require_id()?,
CommunicationType::ErrorInvalidChallenge,
)
.await;
};
if !connection.state().consume_challenge(
AccountChallengeOperation::Delete,
user_id,
requester,
nonce,
) {
return connection
.send_error_response(
value.require_id()?,
CommunicationType::ErrorInvalidChallenge,
)
.await;
}
let Ok(user) = user_repo::get_by_user_id(UserId::from(user_id)).await else {
return connection
.send_error_response(value.require_id()?, CommunicationType::ErrorNotFound)
.await;
};
let payload = lifecycle_payload(b"tensamin:user-delete:v1\0", user_id, requester, nonce);
if verify_ed25519(&user.public_key.sig_cl_public_key, &payload, &signature).is_err()
|| verify_ml_dsa(&user.public_key.sig_pq_public_key, &payload, &pq_signature).is_err()
{
return connection
.send_error_response(
value.require_id()?,
CommunicationType::ErrorNotAuthenticated,
)
.await;
}
complete_delete(connection, value, user.id).await
}
pub async fn erase_hosted_user_data_ack(
connection: Arc<OmikronConnection>,
value: CommunicationValue,
) -> OmikronResult<()> {
value.require_id()?;
value.require_sender_i64()?;
let Some(user_id) = value
.get_data(DataType::UserId)
.as_signed_number()
.and_then(|v| i64::try_from(v).ok())
.filter(|v| *v > 0)
else {
return connection
.send_error_response(value.require_id()?, CommunicationType::ErrorInvalidUserId)
.await;
};
let iota_id = IotaId::from(value.require_sender_i64()?);
match user_repo::acknowledge_pending_erasure(UserId::from(user_id), iota_id).await {
Ok(true) => {
connection
.send(
&CommunicationValue::new(CommunicationType::Success)
.with_id(value.require_id()?),
)
.await
}
Ok(false) => {
connection
.send_error_response(
value.require_id()?,
CommunicationType::ErrorNotAuthenticated,
)
.await
}
Err(_) => {
connection
.send_error_response(value.require_id()?, CommunicationType::ErrorInternal)
.await
}
}
}

View file

@ -1,6 +1,4 @@
use super::super::connection::{
OmikronConnection, OmikronResult, OptionalDataValueCompat, RequiredMtpFields,
};
use super::super::omikron_connection::{OmikronConnection, OmikronResult};
use crate::server::short_link::add_short_link;
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
use std::sync::Arc;
@ -9,15 +7,15 @@ pub async fn shorten(
connection: Arc<OmikronConnection>,
value: CommunicationValue,
) -> OmikronResult<()> {
let link_data = value.get_data(DataType::Link);
let link = link_data
let link = value
.get_data(DataType::Link)
.as_str()
.ok_or(crate::error::OmegaError::InvalidResponse)?;
let short = add_short_link(link)
.await
.map_err(|_| crate::error::OmegaError::Transport("short link error".to_string()))?;
let response = CommunicationValue::new(CommunicationType::ShortenLink)
.with_id(value.require_id()?)
.with_id(value.get_id())
.add_typed_default(DataType::Link, DataValue::Str(short));
connection.send(&response).await
}

View file

@ -1,6 +1,4 @@
use super::super::connection::{
OmikronConnection, OmikronResult, OptionalDataValueCompat, RequiredMtpFields,
};
use super::super::omikron_connection::{OmikronConnection, OmikronResult};
use crate::{db::notification_repo, log, models::UserId};
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
use mtp::type_map::TypeMap;
@ -10,9 +8,8 @@ pub async fn get(
connection: Arc<OmikronConnection>,
value: CommunicationValue,
) -> OmikronResult<()> {
let request_id = value.require_id()?;
let sender = value.require_sender_i64()?;
let notifications = match notification_repo::get_notifications(UserId::from(sender)).await {
let notifications =
match notification_repo::get_notifications(UserId::from(value.get_sender() as i64)).await {
Ok(items) => items
.into_iter()
.map(|item| {
@ -39,7 +36,7 @@ pub async fn get(
}
};
let response = CommunicationValue::new(CommunicationType::GetNotifications)
.with_id(request_id)
.with_id(value.get_id())
.add_typed_default(DataType::Notifications, DataValue::Array(notifications));
connection.send(&response).await
}
@ -48,17 +45,19 @@ pub async fn read(
connection: Arc<OmikronConnection>,
value: CommunicationValue,
) -> OmikronResult<()> {
let request_id = value.require_id()?;
let receiver = value.require_sender_i64()?;
let receiver = match value.get_sender() {
sender if sender > 0 => sender as i64,
_ => match value.get_data(DataType::ReceiverId).as_number() {
Some(id) => id as i64,
None => return Ok(()),
},
};
let Some(other) = value
.get_data(DataType::SenderId)
.as_number()
.and_then(|id| i64::try_from(id).ok())
.filter(|id| *id > 0)
.map(|id| id as i64)
else {
return connection
.send_error_response(request_id, CommunicationType::ErrorInvalidData)
.await;
return Ok(());
};
if let Err(error) =
notification_repo::read_notification(UserId::from(receiver), UserId::from(other)).await
@ -70,7 +69,7 @@ pub async fn read(
);
} else {
let response =
CommunicationValue::new(CommunicationType::ReadNotification).with_id(request_id);
CommunicationValue::new(CommunicationType::ReadNotification).with_id(value.get_id());
let _ = connection.send(&response).await;
let sync = CommunicationValue::new(CommunicationType::ReadNotification)
.with_receiver(receiver as u64)
@ -84,54 +83,18 @@ pub async fn push(
connection: Arc<OmikronConnection>,
value: CommunicationValue,
) -> OmikronResult<()> {
let request_id = value.id().filter(|id| *id != 0);
let receiver = value.require_receiver_i64().ok().or_else(|| {
value
.get_data(DataType::ReceiverId)
.as_number()
.and_then(|id| i64::try_from(id).ok())
.filter(|id| *id > 0)
});
let Some(receiver) = receiver else {
if let Some(request_id) = request_id {
return connection
.send_error_response(request_id, CommunicationType::ErrorInvalidData)
.await;
}
return Ok(());
let receiver = match value.get_receiver() {
receiver if receiver > 0 => receiver as i64,
_ => match value.get_data(DataType::ReceiverId).as_number() {
Some(id) => id as i64,
None => return Ok(()),
},
};
let Some(sender) = value
let sender = value
.get_data(DataType::SenderId)
.as_number()
.and_then(|sender| i64::try_from(sender).ok())
.filter(|sender| *sender > 0)
else {
if let Some(request_id) = request_id {
return connection
.send_error_response(request_id, CommunicationType::ErrorInvalidData)
.await;
}
return Ok(());
};
let source_omikron = connection
.clone()
.get_omikron_id()
.await
.ok_or(crate::OmegaError::NotConnected)?;
if !connection
.state()
.presence
.routes_for_user(sender)
.iter()
.any(|route| route.omikron_id == source_omikron)
{
if let Some(request_id) = request_id {
return connection
.send_error_response(request_id, CommunicationType::ErrorNotAuthenticated)
.await;
}
return Ok(());
}
.map(|id| id as i64)
.unwrap_or(value.get_sender() as i64);
if let Err(error) =
notification_repo::add_notification(UserId::from(receiver), UserId::from(sender)).await
{
@ -141,11 +104,9 @@ pub async fn push(
error
);
} else {
if let Some(request_id) = request_id {
let response =
CommunicationValue::new(CommunicationType::PushNotification).with_id(request_id);
CommunicationValue::new(CommunicationType::PushNotification).with_id(value.get_id());
let _ = connection.send(&response).await;
}
let push = CommunicationValue::new(CommunicationType::PushNotification)
.with_receiver(receiver as u64)
.add_typed_default(DataType::SenderId, DataValue::SignedNumber(sender.into()));

View file

@ -1,175 +1,69 @@
use super::super::connection::{
OmikronConnection, OmikronResult, OptionalDataValueCompat, RequiredMtpFields,
};
use super::super::omikron_connection::{OmikronConnection, OmikronResult};
use crate::{
db::user_repo, log_in, models::IotaId, sql::connection_status::UserStatus, state::OmegaState,
db::user_repo,
log_in,
models::IotaId,
sql::{connection_status::UserStatus, user_online_tracker},
};
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
use std::{
collections::{BTreeMap, HashMap, HashSet},
sync::Arc,
};
use std::sync::Arc;
fn parse_subscription(value: &CommunicationValue) -> Result<(i64, i64, Vec<i64>), &'static str> {
let Some(sender) = value.sender() else {
return Err("user_id");
};
let user_id = i64::try_from(sender)
.ok()
.filter(|id| *id > 0)
.ok_or("user_id")?;
let session_id = value
pub async fn user_connected(
_connection: Arc<OmikronConnection>,
value: CommunicationValue,
omikron_id: i64,
) -> OmikronResult<()> {
log_in!(crate::util::logger::PrintType::Omega, "User connected");
if let Some(user_id) = value.get_data(DataType::UserId).as_number() {
let status = value
.get_data(DataType::UserState)
.as_str()
.and_then(UserStatus::from_str)
.unwrap_or(UserStatus::user_online);
if let Ok(user_id) = i64::try_from(user_id) {
if let Some(session_id) = value
.get_data(DataType::SessionId)
.as_number()
.and_then(|id| i64::try_from(id).ok())
.filter(|id| *id > 0)
.ok_or("session_id")?;
let Some(DataValue::Array(values)) = value.get_data(DataType::UserIds) else {
return Err("user_ids");
};
let mut user_ids = Vec::with_capacity(values.len());
for value in values {
let DataValue::SignedNumber(user_id) = value else {
return Err("user_ids");
};
let Ok(user_id) = i64::try_from(*user_id) else {
return Err("user_ids");
};
if user_id <= 0 {
return Err("user_ids");
}
if !user_ids.contains(&user_id) {
user_ids.push(user_id);
}
}
Ok((user_id, session_id, user_ids))
}
fn apply_preferences(state: &OmegaState, preferences: HashMap<i64, UserStatus>) {
state.presence.set_preferences(preferences);
}
fn states_for_users(state: &OmegaState, users: &[crate::models::User]) -> HashMap<i64, UserStatus> {
users
.iter()
.map(|user| {
let status = user
.iota_id
.map(|iota_id| state.presence.resolve_public_state(user.id.0, iota_id.0))
.unwrap_or(UserStatus::user_offline);
(user.id.0, status)
})
.collect()
}
fn changed_states(
state: &OmegaState,
before: &HashMap<i64, UserStatus>,
users: &[crate::models::User],
) -> Vec<(i64, UserStatus)> {
let mut changes = users
.iter()
.filter_map(|user| {
let after = user
.iota_id
.map(|iota_id| state.presence.resolve_public_state(user.id.0, iota_id.0))
.unwrap_or(UserStatus::user_offline);
(before.get(&user.id.0) != Some(&after)).then_some((user.id.0, after))
})
.collect::<Vec<_>>();
changes.sort_by_key(|(user_id, _)| *user_id);
changes.dedup_by_key(|(user_id, _)| *user_id);
changes
}
fn state_notification(
subscriber: &crate::sql::user_online_tracker::PresenceSubscriber,
user_id: i64,
user_state: &UserStatus,
) -> CommunicationValue {
CommunicationValue::new(CommunicationType::ClientChanged)
.with_receiver(subscriber.user_id as u64)
.add_typed_default(
DataType::SessionId,
DataValue::SignedNumber(subscriber.session_id.into()),
)
.add_typed_default(DataType::UserId, DataValue::SignedNumber(user_id.into()))
.add_typed_default(DataType::UserState, DataValue::Str(user_state.to_string()))
}
async fn publish_state_changes(state: &OmegaState, changes: &[(i64, UserStatus)]) {
let mut grouped = BTreeMap::<i64, Vec<CommunicationValue>>::new();
for (user_id, user_state) in changes {
for subscriber in state.presence.subscribers(*user_id) {
grouped
.entry(subscriber.omikron_id)
.or_default()
.push(state_notification(&subscriber, *user_id, user_state));
}
}
for (omikron_id, notifications) in grouped {
if let Err(error) =
crate::transport::omikron_manager::send_state_batch(omikron_id, notifications).await
{
log_in!(
crate::util::logger::PrintType::General,
"Failed to deliver presence state batch to Omikron {}: {}",
omikron_id,
error
user_online_tracker::track_user_session_status(
user_id, session_id, status, omikron_id,
);
} else {
user_online_tracker::track_user_status(user_id, status, omikron_id);
}
}
}
Ok(())
}
async fn publish_changed_states(
state: &OmegaState,
before: &HashMap<i64, UserStatus>,
users: &[crate::models::User],
) {
publish_state_changes(state, &changed_states(state, before, users)).await;
}
pub async fn state_subscribe(
state: Arc<OmegaState>,
connection: Arc<OmikronConnection>,
pub async fn user_disconnected(
_: Arc<OmikronConnection>,
value: CommunicationValue,
omikron_id: i64,
) -> OmikronResult<()> {
let (user_id, session_id, user_ids) = match parse_subscription(&value) {
Ok(subscription) => subscription,
Err("user_id") => {
return connection
.send_error_response(value.require_id()?, CommunicationType::ErrorNoUserId)
.await;
log_in!(crate::util::logger::PrintType::Omega, "User disconnected");
if let Some(user_id) = value.get_data(DataType::UserId).as_number() {
if let Some(session_id) = value
.get_data(DataType::SessionId)
.as_number()
.and_then(|id| i64::try_from(id).ok())
.filter(|id| *id > 0)
{
user_online_tracker::untrack_user_session_status(
user_id as i64,
session_id,
omikron_id,
);
} else {
user_online_tracker::untrack_user_status(user_id as i64, omikron_id);
}
Err(detail) => {
return connection
.send_error_response_with_detail(
value.require_id()?,
CommunicationType::ErrorInvalidData,
detail,
)
.await;
}
};
if !state.presence.owns_session(user_id, session_id, omikron_id) {
return connection
.send_error_response(value.require_id()?, CommunicationType::ErrorNoIota)
.await;
}
state
.presence
.replace_subscription(user_id, session_id, omikron_id, user_ids);
connection
.send(&CommunicationValue::new(CommunicationType::Success).with_id(value.require_id()?))
.await
Ok(())
}
/// Legacy state-change contract used by Omegas that predate SetUserState.
/// The payload is ClientChanged with UserId and UserState only.
pub async fn client_changed_legacy(
state: Arc<OmegaState>,
pub async fn client_changed(
_: Arc<OmikronConnection>,
value: CommunicationValue,
_: i64,
@ -178,136 +72,29 @@ pub async fn client_changed_legacy(
.get_data(DataType::UserId)
.as_number()
.and_then(|id| i64::try_from(id).ok())
.filter(|id| *id > 0)
else {
return Ok(());
};
let Some(status) = value
.get_data(DataType::UserState)
.as_str()
.and_then(UserStatus::from_client_preference)
.and_then(UserStatus::from_str)
else {
return Ok(());
};
state.presence.set_preference(user_id, status);
// Connectivity is derived from routes. Clients may choose only public
// presence preferences, never server/offline states.
if matches!(
status,
UserStatus::user_offline | UserStatus::iota_offline | UserStatus::iota_online
) {
return Ok(());
}
user_online_tracker::update_user_session_status(user_id, status);
Ok(())
}
pub async fn user_connected(
state: Arc<OmegaState>,
connection: Arc<OmikronConnection>,
value: CommunicationValue,
omikron_id: i64,
) -> OmikronResult<()> {
log_in!(crate::util::logger::PrintType::Omega, "User connected");
let Some(user_id) = value
.get_data(DataType::UserId)
.as_number()
.and_then(|id| i64::try_from(id).ok())
.filter(|id| *id > 0)
else {
return connection
.send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData)
.await;
};
let Some(session_id) = value
.get_data(DataType::SessionId)
.as_number()
.and_then(|id| i64::try_from(id).ok())
.filter(|id| *id > 0)
else {
return connection
.send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData)
.await;
};
let Some(iota_id) = value
.get_data(DataType::IotaId)
.as_number()
.and_then(|id| i64::try_from(id).ok())
.filter(|id| *id > 0)
else {
return connection
.send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData)
.await;
};
let user = match user_repo::get_by_user_id(user_id.into()).await {
Ok(user) => user,
Err(crate::error::OmegaError::NotFound) => {
return connection
.send_error_response(value.require_id()?, CommunicationType::ErrorNotFound)
.await;
}
Err(error) => return Err(error),
};
let preferences = match user_repo::get_presence_preferences(&[user_id]).await {
Ok(preferences) => preferences,
Err(error) => return Err(error),
};
if user.iota_id.map(|id| id.0) != Some(iota_id) || !state.presence.has_iota_route(iota_id) {
return connection
.send_error_response(value.require_id()?, CommunicationType::ErrorNoIota)
.await;
}
apply_preferences(&state, preferences);
let users = [user];
let before = states_for_users(&state, &users);
state
.presence
.track_session(user_id, session_id, omikron_id, iota_id);
publish_changed_states(&state, &before, &users).await;
connection
.send(&CommunicationValue::new(CommunicationType::Success).with_id(value.require_id()?))
.await
}
pub async fn user_disconnected(
state: Arc<OmegaState>,
connection: Arc<OmikronConnection>,
value: CommunicationValue,
omikron_id: i64,
) -> OmikronResult<()> {
log_in!(crate::util::logger::PrintType::Omega, "User disconnected");
let Some(user_id) = value
.get_data(DataType::UserId)
.as_number()
.and_then(|id| i64::try_from(id).ok())
.filter(|id| *id > 0)
else {
return connection
.send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData)
.await;
};
let Some(session_id) = value
.get_data(DataType::SessionId)
.as_number()
.and_then(|id| i64::try_from(id).ok())
.filter(|id| *id > 0)
else {
return connection
.send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData)
.await;
};
if let Ok(user) = user_repo::get_by_user_id(user_id.into()).await {
let preferences = user_repo::get_presence_preferences(&[user_id]).await?;
apply_preferences(&state, preferences);
let users = [user];
let before = states_for_users(&state, &users);
state
.presence
.remove_session(user_id, session_id, omikron_id);
publish_changed_states(&state, &before, &users).await;
} else {
state
.presence
.remove_session(user_id, session_id, omikron_id);
}
connection
.send(&CommunicationValue::new(CommunicationType::Success).with_id(value.require_id()?))
.await
}
pub async fn iota_connected(
state: Arc<OmegaState>,
connection: Arc<OmikronConnection>,
value: CommunicationValue,
omikron_id: i64,
@ -316,36 +103,38 @@ pub async fn iota_connected(
let Some(iota_id) = value
.get_data(DataType::IotaId)
.as_number()
.and_then(|id| i64::try_from(id).ok())
.filter(|id| *id > 0)
.map(|id| id as i64)
else {
return connection
.send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData)
.await;
return Ok(());
};
let users = user_repo::get_users_by_iota_id(IotaId::from(iota_id)).await?;
let ids = users.iter().map(|user| user.id.0).collect::<Vec<_>>();
apply_preferences(&state, user_repo::get_presence_preferences(&ids).await?);
let before = states_for_users(&state, &users);
state.presence.connect_iota(iota_id, omikron_id);
let user_ids = users
.iter()
.map(|user| DataValue::SignedNumber(user.id.0.into()))
.collect();
user_online_tracker::track_iota_connection(iota_id, omikron_id, true);
let mut user_ids = Vec::new();
match user_repo::get_users_by_iota_id(IotaId::from(iota_id)).await {
Ok(users) => {
for user in users {
user_ids.push(DataValue::SignedNumber(user.id.0.into()));
user_online_tracker::track_user_status(
user.id.0,
UserStatus::user_offline,
omikron_id,
);
}
}
Err(_) => log_in!(
crate::util::logger::PrintType::General,
"SQL error loading users for IOTA"
),
}
let response = CommunicationValue::new(CommunicationType::IotaUserData)
.with_id(value.get_id())
.add_typed_default(DataType::IotaId, DataValue::SignedNumber(iota_id.into()))
.add_typed_default(DataType::UserIds, DataValue::Array(user_ids));
connection.clone().send(&response).await?;
crate::transport::omikron_manager::deliver_pending_erasures(iota_id).await;
publish_changed_states(&state, &before, &users).await;
connection
.send(&CommunicationValue::new(CommunicationType::Success).with_id(value.require_id()?))
.await
let _ = connection.send(&response).await;
Ok(())
}
pub async fn iota_disconnected(
state: Arc<OmegaState>,
connection: Arc<OmikronConnection>,
_: Arc<OmikronConnection>,
value: CommunicationValue,
omikron_id: i64,
) -> OmikronResult<()> {
@ -353,329 +142,42 @@ pub async fn iota_disconnected(
let Some(iota_id) = value
.get_data(DataType::IotaId)
.as_number()
.and_then(|id| i64::try_from(id).ok())
.filter(|id| *id > 0)
.map(|id| id as i64)
else {
return connection
.send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData)
.await;
return Ok(());
};
let users = user_repo::get_users_by_iota_id(IotaId::from(iota_id)).await?;
let ids = users.iter().map(|user| user.id.0).collect::<Vec<_>>();
apply_preferences(&state, user_repo::get_presence_preferences(&ids).await?);
let before = states_for_users(&state, &users);
state.presence.untrack_iota_connection(iota_id, omikron_id);
publish_changed_states(&state, &before, &users).await;
connection
.send(&CommunicationValue::new(CommunicationType::Success).with_id(value.require_id()?))
.await
if user_online_tracker::untrack_iota_connection(iota_id, omikron_id) {
if let Ok(users) = user_repo::get_users_by_iota_id(IotaId::from(iota_id)).await {
user_online_tracker::untrack_many_users(
&users.iter().map(|user| user.id.0).collect::<Vec<_>>(),
);
}
}
Ok(())
}
pub async fn sync_status(
state: Arc<OmegaState>,
connection: Arc<OmikronConnection>,
_: Arc<OmikronConnection>,
value: CommunicationValue,
omikron_id: i64,
) -> OmikronResult<()> {
let request_id = value.require_id()?;
let Some(DataValue::Array(iota_values)) = value.get_data(DataType::IotaIds) else {
return connection
.send_error_response(request_id, CommunicationType::ErrorInvalidData)
.await;
};
let Some(DataValue::Array(session_values)) = value.get_data(DataType::UserStates) else {
return connection
.send_error_response(request_id, CommunicationType::ErrorInvalidData)
.await;
};
let mut iota_ids = Vec::with_capacity(iota_values.len());
for item in iota_values {
let DataValue::SignedNumber(id) = item else {
return connection
.send_error_response(request_id, CommunicationType::ErrorInvalidData)
.await;
};
let Ok(id) = i64::try_from(*id) else {
return connection
.send_error_response(request_id, CommunicationType::ErrorInvalidData)
.await;
};
if id <= 0 {
return connection
.send_error_response(request_id, CommunicationType::ErrorInvalidData)
.await;
}
if !iota_ids.contains(&id) {
iota_ids.push(id);
}
}
if !connection.peer_capabilities().session_snapshot_v1 {
let Some(DataValue::Array(user_values)) = value.get_data(DataType::UserIds) else {
return connection
.send_error_response(request_id, CommunicationType::ErrorInvalidData)
.await;
};
let mut user_ids = Vec::with_capacity(user_values.len());
for item in user_values {
let DataValue::SignedNumber(user_id) = item else {
return connection
.send_error_response(request_id, CommunicationType::ErrorInvalidData)
.await;
};
let Ok(user_id) = i64::try_from(*user_id) else {
return connection
.send_error_response(request_id, CommunicationType::ErrorInvalidData)
.await;
};
if user_id <= 0 {
return connection
.send_error_response(request_id, CommunicationType::ErrorInvalidData)
.await;
}
if !user_ids.contains(&user_id) {
user_ids.push(user_id);
}
}
let previous_iota_ids = state.presence.iota_ids_owned_by(omikron_id);
let affected_iota_ids = previous_iota_ids
.iter()
.chain(iota_ids.iter())
.copied()
.collect::<HashSet<_>>();
let users = user_repo::get_users_by_ids(&user_ids).await?;
let returned_user_ids = users.iter().map(|user| user.id.0).collect::<Vec<_>>();
apply_preferences(
&state,
user_repo::get_presence_preferences(&returned_user_ids).await?,
);
let before = states_for_users(&state, &users);
state
.presence
.replace_omikron_snapshot(omikron_id, &iota_ids, &[]);
let affected_users = user_repo::get_users_by_ids_and_iota_ids(
&returned_user_ids,
&affected_iota_ids.iter().copied().collect::<Vec<_>>(),
)
.await?;
publish_changed_states(&state, &before, &affected_users).await;
return connection
.send(&CommunicationValue::new(CommunicationType::Success).with_id(request_id))
.await;
}
let tm = mtp::type_map::TypeMap::latest();
let mut sessions = Vec::with_capacity(session_values.len());
for item in session_values {
let (user_id, session_id, iota_id) = if connection.peer_capabilities().session_snapshot_v1 {
let DataValue::Container(entries) = item else {
return connection
.send_error_response(request_id, CommunicationType::ErrorInvalidData)
.await;
};
let find = |kind| {
entries.iter().find_map(|(key, value)| {
(Some(*key) == DataType::try_to_id(kind, &tm)).then_some(value)
})
};
let (
Some(DataValue::SignedNumber(user_id)),
Some(DataValue::SignedNumber(session_id)),
Some(DataValue::SignedNumber(iota_id)),
) = (
find(DataType::UserId),
find(DataType::SessionId),
find(DataType::IotaId),
)
else {
return connection
.send_error_response(request_id, CommunicationType::ErrorInvalidData)
.await;
};
(*user_id, *session_id, *iota_id)
} else {
let DataValue::Array(values) = item else {
return connection
.send_error_response(request_id, CommunicationType::ErrorInvalidData)
.await;
};
let [
DataValue::SignedNumber(user_id),
DataValue::SignedNumber(session_id),
DataValue::SignedNumber(iota_id),
] = values.as_slice()
else {
return connection
.send_error_response(request_id, CommunicationType::ErrorInvalidData)
.await;
};
(*user_id, *session_id, *iota_id)
};
let (Ok(user_id), Ok(session_id), Ok(iota_id)) = (
i64::try_from(user_id),
i64::try_from(session_id),
i64::try_from(iota_id),
) else {
return connection
.send_error_response(request_id, CommunicationType::ErrorInvalidData)
.await;
};
if user_id <= 0 || session_id <= 0 || iota_id <= 0 || !iota_ids.contains(&iota_id) {
return connection
.send_error_response(request_id, CommunicationType::ErrorInvalidData)
.await;
}
if sessions.iter().any(|(existing_user, existing_session, _)| {
*existing_user == user_id && *existing_session == session_id
}) {
return connection
.send_error_response(request_id, CommunicationType::ErrorInvalidData)
.await;
}
sessions.push((user_id, session_id, iota_id));
}
let previous_iota_ids = state.presence.iota_ids_owned_by(omikron_id);
let previous_session_user_ids = state
.presence
.sessions_owned_by(omikron_id)
.into_iter()
.map(|(user_id, _, _)| user_id)
.collect::<HashSet<_>>();
let new_session_user_ids = sessions
.iter()
.map(|(user_id, _, _)| *user_id)
.collect::<HashSet<_>>();
let affected_iota_ids = previous_iota_ids
.iter()
.chain(iota_ids.iter())
.copied()
.collect::<HashSet<_>>();
let users = user_repo::get_users_by_ids_and_iota_ids(
&previous_session_user_ids
.iter()
.chain(new_session_user_ids.iter())
.copied()
.collect::<Vec<_>>(),
&affected_iota_ids.iter().copied().collect::<Vec<_>>(),
)
.await?;
if sessions.iter().any(|(user_id, _, iota_id)| {
!users
.iter()
.any(|user| user.id.0 == *user_id && user.iota_id == Some(IotaId::from(*iota_id)))
}) {
return connection
.send_error_response(request_id, CommunicationType::ErrorInvalidData)
.await;
}
let user_ids = users.iter().map(|user| user.id.0).collect::<Vec<_>>();
apply_preferences(
&state,
user_repo::get_presence_preferences(&user_ids).await?,
);
let before = states_for_users(&state, &users);
state
.presence
.replace_omikron_snapshot(omikron_id, &iota_ids, &sessions);
publish_changed_states(&state, &before, &users).await;
connection
.send(&CommunicationValue::new(CommunicationType::Success).with_id(request_id))
.await
}
pub async fn omikron_disconnected(state: Arc<OmegaState>, omikron_id: i64) {
let iota_ids = state.presence.iota_ids_owned_by(omikron_id);
let session_user_ids = state
.presence
.sessions_owned_by(omikron_id)
.into_iter()
.map(|(user_id, _, _)| user_id)
.collect::<Vec<_>>();
let users = match user_repo::get_users_by_ids_and_iota_ids(&session_user_ids, &iota_ids).await {
Ok(users) => users,
Err(error) => {
log_in!(
crate::util::logger::PrintType::General,
"Failed to load users before Omikron {} cleanup: {}",
if let DataValue::Array(ids) = value.get_data(DataType::UserIds) {
for id in ids {
if let DataValue::SignedNumber(id) = id {
user_online_tracker::track_user_status(
*id as i64,
UserStatus::user_offline,
omikron_id,
error
);
return;
}
};
let user_ids = users.iter().map(|user| user.id.0).collect::<Vec<_>>();
if let Err(error) = user_repo::get_presence_preferences(&user_ids)
.await
.map(|preferences| apply_preferences(&state, preferences))
{
log_in!(
crate::util::logger::PrintType::General,
"Failed to load preferences before Omikron {} cleanup: {}",
omikron_id,
error
);
return;
}
let before = states_for_users(&state, &users);
let removed = state.presence.remove_omikron(omikron_id);
debug_assert_eq!(removed.iota_ids, {
let mut ids = iota_ids.clone();
ids.sort_unstable();
ids.dedup();
ids
});
publish_changed_states(&state, &before, &users).await;
}
#[cfg(test)]
mod tests {
use super::parse_subscription;
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
fn request(user_ids: DataValue) -> CommunicationValue {
CommunicationValue::new(CommunicationType::StateSubscribe)
.with_sender(7)
.add_typed_default(DataType::SessionId, DataValue::SignedNumber(11))
.add_typed_default(DataType::UserIds, user_ids)
}
#[test]
fn subscription_parser_deduplicates_valid_targets() {
let parsed = parse_subscription(&request(DataValue::Array(vec![
DataValue::SignedNumber(20),
DataValue::SignedNumber(21),
DataValue::SignedNumber(20),
])))
.unwrap();
assert_eq!(parsed, (7, 11, vec![20, 21]));
}
#[test]
fn subscription_parser_rejects_missing_or_malformed_fields() {
let missing_users = CommunicationValue::new(CommunicationType::StateSubscribe)
.with_sender(7)
.add_typed_default(DataType::SessionId, DataValue::SignedNumber(11));
assert_eq!(parse_subscription(&missing_users), Err("user_ids"));
let malformed_users = request(DataValue::Array(vec![DataValue::Str("bad".into())]));
assert_eq!(parse_subscription(&malformed_users), Err("user_ids"));
let invalid_session = CommunicationValue::new(CommunicationType::StateSubscribe)
.with_sender(7)
.add_typed_default(DataType::SessionId, DataValue::SignedNumber(0))
.add_typed_default(
DataType::UserIds,
DataValue::Array(vec![DataValue::SignedNumber(20)]),
);
assert_eq!(parse_subscription(&invalid_session), Err("session_id"));
}
#[test]
fn empty_subscription_is_valid_and_authoritative() {
let parsed = parse_subscription(&request(DataValue::Array(Vec::new()))).unwrap();
assert_eq!(parsed, (7, 11, Vec::new()));
}
}
if let DataValue::Array(ids) = value.get_data(DataType::IotaIds) {
for id in ids {
if let DataValue::SignedNumber(id) = id {
user_online_tracker::track_iota_connection(*id as i64, omikron_id, true);
}
}
}
Ok(())
}

View file

@ -1,6 +1,4 @@
use super::super::connection::{
OmikronConnection, OmikronResult, OptionalDataValueCompat, RequiredMtpFields,
};
use super::super::omikron_connection::{OmikronConnection, OmikronResult};
use crate::{
db::{iota_repo, user_repo},
models::{IotaId, UserId},
@ -22,13 +20,13 @@ pub async fn get_register(
.filter(|id| user_repo::valid_protocol_id(*id));
let Some(iota_id) = iota_id else {
return connection
.send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData)
.send_error_response(value.get_id(), CommunicationType::ErrorInvalidData)
.await;
};
let (register_id, registration_token) =
user_repo::allocate_registration(IotaId::from(iota_id), value.require_id()?).await?;
user_repo::allocate_registration(IotaId::from(iota_id), value.get_id()).await?;
let response = CommunicationValue::new(CommunicationType::GetRegister)
.with_id(value.require_id()?)
.with_id(value.get_id())
.add_typed_default(
DataType::UserId,
DataValue::SignedNumber(register_id.0.into()),
@ -47,7 +45,7 @@ pub async fn complete_iota(
.and_then(|key| PublicKeyBundle::from_base64(key).ok());
let Some(public_key) = public_key else {
return connection
.send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData)
.send_error_response(value.get_id(), CommunicationType::ErrorInvalidData)
.await;
};
match iota_repo::create_new_iota(public_key).await {
@ -55,7 +53,7 @@ pub async fn complete_iota(
connection
.send(
&CommunicationValue::new(CommunicationType::CompleteRegisterIota)
.with_id(value.require_id()?)
.with_id(value.get_id())
.add_typed_default(DataType::IotaId, DataValue::SignedNumber(id.0.into())),
)
.await
@ -64,7 +62,7 @@ pub async fn complete_iota(
connection
.send(
&CommunicationValue::new(CommunicationType::ErrorInternal)
.with_id(value.require_id()?)
.with_id(value.get_id())
.add_typed_default(DataType::ErrorType, DataValue::Str(error.to_string())),
)
.await
@ -108,7 +106,7 @@ pub async fn complete_user(
})
else {
return connection
.send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData)
.send_error_response(value.get_id(), CommunicationType::ErrorInvalidData)
.await;
};
// Omikron supplies the authenticated Iota ID in the payload. The lease
@ -120,7 +118,7 @@ pub async fn complete_user(
.filter(|id| user_repo::valid_protocol_id(*id));
let Some(iota_id) = iota_id else {
return connection
.send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData)
.send_error_response(value.get_id(), CommunicationType::ErrorInvalidData)
.await;
};
@ -135,29 +133,15 @@ pub async fn complete_user(
.await
{
Ok(()) => {
if let Err(error) =
crate::transport::omikron_manager::publish_iota_user_snapshot(iota_id).await
{
crate::log_in!(
crate::util::logger::PrintType::General,
"Could not publish Iota snapshot after registering user {} on Iota {}: {}",
user_id,
iota_id,
error
);
}
connection
.send(
&CommunicationValue::new(CommunicationType::Success)
.with_id(value.require_id()?),
)
.send(&CommunicationValue::new(CommunicationType::Success).with_id(value.get_id()))
.await
}
Err(error) => {
connection
.send(
&CommunicationValue::new(CommunicationType::ErrorInternal)
.with_id(value.require_id()?)
.with_id(value.get_id())
.add_typed_default(DataType::ErrorType, DataValue::Str(error.to_string())),
)
.await

View file

@ -1,149 +1,46 @@
use super::super::connection::{
OmikronConnection, OmikronResult, OptionalDataValueCompat, RequiredMtpFields,
};
use crate::db::user_repo;
use super::super::omikron_connection::{OmikronConnection, OmikronResult};
use crate::sql::{connection_status::UserStatus, user_online_tracker};
use mtp::{
codec::{CommunicationType, CommunicationValue, DataType, DataValue},
type_map::TypeMap,
};
use std::{
collections::{HashMap, HashSet},
sync::Arc,
};
async fn send_error(
connection: Arc<OmikronConnection>,
request_id: u32,
error_type: CommunicationType,
session_id: Option<i128>,
) -> OmikronResult<()> {
let mut response = CommunicationValue::new(error_type).with_id(request_id);
if let Some(session_id) = session_id {
response =
response.add_typed_default(DataType::SessionId, DataValue::SignedNumber(session_id));
}
connection.send(&response).await
}
use std::sync::Arc;
pub async fn get(
connection: Arc<OmikronConnection>,
value: CommunicationValue,
) -> OmikronResult<()> {
let state = connection.state();
let legacy_peer = !connection.peer_capabilities().client_state_push_v1;
let Some(DataValue::Array(ids)) = value.get_data(DataType::UserIds) else {
return send_error(
connection,
value.require_id()?,
CommunicationType::ErrorInvalidData,
None,
)
.await;
let DataValue::Array(ids) = value.get_data(DataType::UserIds) else {
return Ok(());
};
let session_id = value
.get_data(DataType::SessionId)
.as_number()
.filter(|id| *id > 0);
if session_id.is_none() && !legacy_peer {
return send_error(
connection,
value.require_id()?,
CommunicationType::ErrorInvalidData,
None,
)
.await;
}
let tm = TypeMap::latest();
let mut requested_user_ids = Vec::new();
let mut requested_set = HashSet::new();
for id in ids {
let states = ids
.iter()
.filter_map(|id| {
let DataValue::SignedNumber(id) = id else {
return send_error(
connection,
value.require_id()?,
CommunicationType::ErrorInvalidData,
session_id,
)
.await;
return None;
};
let Ok(user_id) = i64::try_from(*id) else {
return send_error(
connection,
value.require_id()?,
CommunicationType::ErrorInvalidData,
session_id,
)
.await;
};
if user_id <= 0 {
return send_error(
connection,
value.require_id()?,
CommunicationType::ErrorInvalidData,
session_id,
)
.await;
let status = user_online_tracker::get_user_status(*id as i64)
.map(|status| {
if status.connection_type == UserStatus::user_invisible {
UserStatus::user_offline.to_string()
} else {
status.connection_type.to_string()
}
if !requested_set.insert(user_id) {
continue;
}
requested_user_ids.push(user_id);
}
let users = match user_repo::get_users_by_ids(&requested_user_ids).await {
Ok(users) => users,
Err(_) => {
return send_error(
connection,
value.require_id()?,
CommunicationType::ErrorInternal,
session_id,
)
.await;
}
};
let users_by_id: HashMap<_, _> = users.into_iter().map(|user| (user.id.0, user)).collect();
let mut states = Vec::new();
let mut missing_user_ids = Vec::new();
for user_id in requested_user_ids {
let Some(user) = users_by_id.get(&user_id) else {
missing_user_ids.push(user_id);
continue;
};
let status = user
.iota_id
.map(|iota_id| state.presence.resolve_public_state(user_id, iota_id.0))
.unwrap_or(crate::sql::connection_status::UserStatus::user_offline)
.to_string();
})
.unwrap_or_else(|| UserStatus::iota_offline.to_string());
let mut map = Vec::new();
if let Some(kind) = DataType::UserId.try_to_id(&tm) {
map.push((kind, DataValue::SignedNumber(user_id.into())));
map.push((kind, DataValue::SignedNumber((*id as i64).into())));
}
if let Some(kind) = DataType::UserState.try_to_id(&tm) {
map.push((kind, DataValue::Str(status)));
}
states.push(DataValue::Container(map));
}
Some(DataValue::Container(map))
})
.collect();
let response = CommunicationValue::new(CommunicationType::GetStates)
.with_id(value.require_id()?)
.with_id(value.get_id())
.add_typed_default(DataType::UserStates, DataValue::Array(states));
let response = if let Some(session_id) = session_id {
response.add_typed_default(DataType::SessionId, DataValue::SignedNumber(session_id))
} else {
response
};
let response = if legacy_peer {
response
} else {
response.add_typed_default(
DataType::MissingUserIds,
DataValue::Array(
missing_user_ids
.into_iter()
.map(|id| DataValue::SignedNumber(id.into()))
.collect(),
),
)
};
connection.send(&response).await
}

View file

@ -1,9 +1,8 @@
use super::super::connection::{
OmikronConnection, OmikronResult, OptionalDataValueCompat, RequiredMtpFields,
};
use super::super::omikron_connection::{OmikronConnection, OmikronResult};
use crate::{
db::{iota_repo, user_repo},
models::{IotaId, UserId},
sql::{connection_status::UserStatus, user_online_tracker},
};
use base64::{Engine as _, engine::general_purpose::STANDARD};
use mtp::{
@ -12,12 +11,9 @@ use mtp::{
};
use std::sync::Arc;
fn connections(connection: &OmikronConnection, iota_id: i64) -> DataValue {
fn connections(iota_id: i64) -> DataValue {
DataValue::Array(
connection
.state()
.presence
.iota_connections(iota_id)
user_online_tracker::get_iota_omikron_connections(iota_id)
.unwrap_or_default()
.into_iter()
.map(|id| DataValue::SignedNumber(id.into()))
@ -29,16 +25,10 @@ pub async fn get_user(
connection: Arc<OmikronConnection>,
value: CommunicationValue,
) -> OmikronResult<()> {
let request_id = value.require_id()?;
let sender = value.require_sender_i64()?;
let state = connection.state();
let user = if let Some(id) = value
.get_data(DataType::UserId)
.as_number()
.and_then(|id| i64::try_from(id).ok())
.filter(|id| *id > 0)
{
user_repo::get_by_user_id(UserId::from(id)).await.ok()
let user = if let Some(id) = value.get_data(DataType::UserId).as_number() {
user_repo::get_by_user_id(UserId::from(id as i64))
.await
.ok()
} else if let Some(name) = value.get_data(DataType::Username).as_str() {
user_repo::get_by_username(name).await.ok()
} else {
@ -46,28 +36,25 @@ pub async fn get_user(
};
let Some(user) = user else {
return connection
.send_error_response(request_id, CommunicationType::ErrorNotFound)
.send_error_response(value.get_id(), CommunicationType::ErrorNotFound)
.await;
};
let id = user.id.0;
let iota_id = user.iota_id.map(|id| id.0);
let iota_id = user.iota_id.0;
let username = user.username.clone();
let display = user
.display
.filter(|name| !name.is_empty())
.unwrap_or_else(|| username.clone());
let mut response = CommunicationValue::new(CommunicationType::GetUserData)
.with_id(request_id)
.with_id(value.get_id())
.add_typed_default(DataType::Username, DataValue::Str(username))
.add_typed_default(
DataType::PublicKey,
DataValue::Str(user.public_key.try_to_base64()?),
DataValue::Str(user.public_key.to_base64()),
)
.add_typed_default(DataType::UserId, DataValue::SignedNumber(id.into()))
.add_typed_default(
DataType::CreatedAt,
DataValue::SignedNumber(user.created_at.into()),
)
.add_typed_default(DataType::IotaId, DataValue::SignedNumber(iota_id.into()))
.add_typed_default(DataType::Display, DataValue::Str(display))
.add_typed_default(
DataType::SubLevel,
@ -87,44 +74,28 @@ pub async fn get_user(
response =
response.add_typed_default(DataType::Avatar, DataValue::Str(STANDARD.encode(avatar)));
}
let route = state.presence.user_route(id);
let private_request = sender == id;
let resolved_status = if private_request {
if !state
.presence
.load_preference(id, &user.presence_preference)
{
crate::log_in!(
crate::util::logger::PrintType::General,
"Invalid persisted presence preference for user {}, using user_online",
id
);
}
state.presence.resolve_private_state(id)
} else {
iota_id
.map(|iota_id| state.presence.resolve_public_state(id, iota_id))
.unwrap_or(crate::sql::connection_status::UserStatus::user_offline)
};
let online = user_online_tracker::get_user_status(id);
response = response
.add_typed_default(
DataType::OnlineStatus,
DataValue::Str(resolved_status.to_string()),
)
.add_typed_default(
DataType::OmikronConnections,
iota_id
.map(|iota_id| connections(&connection, iota_id))
.unwrap_or_else(|| DataValue::Array(Vec::new())),
);
if let Some(iota_id) = iota_id {
response =
response.add_typed_default(DataType::IotaId, DataValue::SignedNumber(iota_id.into()));
DataValue::Str(
online
.as_ref()
.map(|status| {
if status.connection_type == UserStatus::user_invisible {
UserStatus::user_offline.to_string()
} else {
status.connection_type.to_string()
}
if let Some(route) = route {
})
.unwrap_or_else(|| UserStatus::iota_offline.to_string()),
),
)
.add_typed_default(DataType::OmikronConnections, connections(iota_id));
if let Some(status) = online {
response = response.add_typed_default(
DataType::OmikronId,
DataValue::SignedNumber(route.omikron_id.into()),
DataValue::SignedNumber(status.omikron_id.into()),
);
}
connection.send(&response).await
@ -134,44 +105,33 @@ pub async fn get_iota(
connection: Arc<OmikronConnection>,
value: CommunicationValue,
) -> OmikronResult<()> {
let request_id = value.require_id()?;
let found = if let Some(id) = value
.get_data(DataType::IotaId)
.as_number()
.and_then(|id| i64::try_from(id).ok())
.filter(|id| *id > 0)
{
iota_repo::get_iota_by_id(IotaId::from(id))
let found = if let Some(id) = value.get_data(DataType::IotaId).as_number() {
iota_repo::get_iota_by_id(IotaId::from(id as i64))
.await
.ok()
.map(|iota| (iota.id.0, iota.public_key, None, None))
} else if let Some(id) = value.get_data(DataType::UserId).as_number() {
if let Some(id) = i64::try_from(id).ok().filter(|id| *id > 0)
&& let Ok(user) = user_repo::get_by_user_id(UserId::from(id)).await
{
match user.iota_id {
Some(iota_id) => iota_repo::get_iota_by_id(iota_id)
if let Ok(user) = user_repo::get_by_user_id(UserId::from(id as i64)).await {
iota_repo::get_iota_by_id(user.iota_id)
.await
.ok()
.map(|iota| (iota.id.0, iota.public_key, Some(user.id.0), None)),
None => None,
}
.map(|iota| (iota.id.0, iota.public_key, Some(user.id.0), None))
} else {
None
}
} else if let Some(name) = value.get_data(DataType::Username).as_str() {
if let Ok(user) = user_repo::get_by_username(name).await {
match user.iota_id {
Some(iota_id) => iota_repo::get_iota_by_id(iota_id).await.ok().map(|iota| {
iota_repo::get_iota_by_id(user.iota_id)
.await
.ok()
.map(|iota| {
(
iota.id.0,
iota.public_key,
Some(user.id.0),
Some(name.to_owned()),
)
}),
None => None,
}
})
} else {
None
}
@ -180,14 +140,14 @@ pub async fn get_iota(
};
let Some((id, key, user_id, username)) = found else {
return connection
.send_error_response(request_id, CommunicationType::ErrorNotFound)
.send_error_response(value.get_id(), CommunicationType::ErrorNotFound)
.await;
};
let mut response = CommunicationValue::new(CommunicationType::GetIotaData)
.with_id(request_id)
.add_typed_default(DataType::PublicKey, DataValue::Str(key.try_to_base64()?))
.with_id(value.get_id())
.add_typed_default(DataType::PublicKey, DataValue::Str(key.to_base64()))
.add_typed_default(DataType::IotaId, DataValue::SignedNumber(id.into()))
.add_typed_default(DataType::OmikronConnections, connections(&connection, id));
.add_typed_default(DataType::OmikronConnections, connections(id));
if let Some(user_id) = user_id {
response =
response.add_typed_default(DataType::UserId, DataValue::SignedNumber(user_id.into()));
@ -202,8 +162,7 @@ async fn update_user(
connection: Arc<OmikronConnection>,
value: CommunicationValue,
) -> OmikronResult<()> {
let request_id = value.require_id()?;
let id = UserId::from(value.require_sender_i64()?);
let id = UserId::from(value.get_sender() as i64);
let mut error = None;
if let Some(name) = value.get_data(DataType::Username).as_str() {
error = user_repo::change_username(id, name.to_owned())
@ -211,40 +170,40 @@ async fn update_user(
.err()
.map(|e| e.to_string());
}
if error.is_none()
&& let Some(name) = value.get_data(DataType::Display).as_str()
{
if error.is_none() {
if let Some(name) = value.get_data(DataType::Display).as_str() {
error = user_repo::change_display_name(id, name.to_owned())
.await
.err()
.map(|e| e.to_string());
}
if error.is_none()
&& let Some(avatar) = value.get_data(DataType::Avatar).as_str()
{
}
if error.is_none() {
if let Some(avatar) = value.get_data(DataType::Avatar).as_str() {
error = user_repo::change_avatar(id, avatar.to_owned())
.await
.err()
.map(|e| e.to_string());
}
if error.is_none()
&& let Some(about) = value.get_data(DataType::About).as_str()
{
}
if error.is_none() {
if let Some(about) = value.get_data(DataType::About).as_str() {
error = user_repo::change_about(id, about.to_owned())
.await
.err()
.map(|e| e.to_string());
}
if error.is_none()
&& let Some(status) = value.get_data(DataType::Status).as_str()
{
}
if error.is_none() {
if let Some(status) = value.get_data(DataType::Status).as_str() {
error = user_repo::change_status(id, status.to_owned())
.await
.err()
.map(|e| e.to_string());
}
if error.is_none()
&& let Some(key) = value
}
if error.is_none() {
if let Some(key) = value
.get_data(DataType::PublicKey)
.as_str()
.and_then(|key| PublicKeyBundle::from_base64(key).ok())
@ -254,12 +213,13 @@ async fn update_user(
.err()
.map(|e| e.to_string());
}
}
let response = match error {
None => CommunicationValue::new(CommunicationType::Success),
Some(error) => CommunicationValue::new(CommunicationType::ErrorInternal)
.add_typed_default(DataType::ErrorType, DataValue::Str(error)),
};
connection.send(&response.with_id(request_id)).await
connection.send(&response.with_id(value.get_id())).await
}
pub async fn change_user(
@ -268,3 +228,44 @@ pub async fn change_user(
) -> OmikronResult<()> {
update_user(connection, value).await
}
pub async fn change_iota(
connection: Arc<OmikronConnection>,
value: CommunicationValue,
) -> OmikronResult<()> {
let Some(reset) = value.get_data(DataType::ResetToken).as_str() else {
return connection
.send_error_response(value.get_id(), CommunicationType::ErrorInvalidData)
.await;
};
let Some(new_token) = value.get_data(DataType::NewToken).as_str() else {
return connection
.send_error_response(value.get_id(), CommunicationType::ErrorInvalidData)
.await;
};
let user_id = UserId::from(value.get_sender() as i64);
let user = match user_repo::get_by_user_id(user_id).await {
Ok(user) => user,
Err(_) => {
return connection
.send_error_response(value.get_id(), CommunicationType::ErrorNotFound)
.await;
}
};
if user.token != reset {
return connection
.send_error_response(value.get_id(), CommunicationType::ErrorInvalidChallenge)
.await;
}
let result =
match user_repo::change_iota_id(user_id, IotaId::from(value.get_sender() as i64)).await {
Ok(()) => user_repo::change_token(user_id, new_token.to_owned()).await,
Err(error) => Err(error),
};
let response = match result {
Ok(()) => CommunicationValue::new(CommunicationType::Success),
Err(error) => CommunicationValue::new(CommunicationType::ErrorInternal)
.add_typed_default(DataType::ErrorType, DataValue::Str(error.to_string())),
};
connection.send(&response.with_id(value.get_id())).await
}

View file

@ -1,6 +1,4 @@
pub mod capabilities;
pub mod connection;
pub mod handlers;
pub mod omikron_connection;
pub mod omikron_manager;
pub mod relay_router;

View file

@ -1,48 +1,32 @@
use super::capabilities::{OmegaCapabilities, PeerCapabilities};
use crate::models::OmikronId;
use crate::{
log, log_cv_in, log_cv_out, log_err, log_in, server,
state::OmegaState,
transport::connection::{RequiredMtpFields, validate_dispatch_fields},
load_keyring, log, log_cv_in, log_cv_out, log_err, log_in, server,
transport::omikron_manager,
util::{file_util::load_file_vec, logger::PrintType},
};
use dashmap::{DashMap, mapref::entry::Entry};
use dashmap::DashMap;
use mtp::{
codec::{CommunicationType, CommunicationValue},
crypto::PublicKeyBundle,
host::{AuthState, AuthenticationPolicy, HostConfig, Policy, SendMode},
host::{AuthenticationPolicy, HostConfig, Policy, SendMode},
webserver::{MTPWebServer, WebMtpReceiver, WebMtpSender},
};
use std::{
net::IpAddr,
net::{IpAddr, Ipv4Addr},
sync::{
Arc, Mutex as StdMutex,
atomic::{AtomicU32, AtomicUsize, Ordering},
Arc,
atomic::{AtomicUsize, Ordering},
},
time::{Duration, Instant},
};
use tokio::{
sync::{Mutex, Semaphore, mpsc, oneshot},
time::interval,
};
use tokio_util::{sync::CancellationToken, task::TaskTracker};
use tokio::{sync::Mutex, time::interval};
const CLEANUP_INTERVAL: Duration = Duration::from_secs(30);
const MAX_WAITING_AGE: Duration = Duration::from_secs(60);
const DEFAULT_BIND_ADDRESS: &str = "0.0.0.0";
static ACTIVE_CONNECTIONS: AtomicUsize = AtomicUsize::new(0);
static NEXT_CORRELATION_ID: AtomicU32 = AtomicU32::new(1);
static ACTIVE_CONNECTIONS_BY_IP: once_cell::sync::Lazy<DashMap<IpAddr, usize>> =
once_cell::sync::Lazy::new(DashMap::new);
fn parse_bind_address(value: Option<&str>) -> Result<IpAddr, std::net::AddrParseError> {
value
.unwrap_or(DEFAULT_BIND_ADDRESS)
.trim()
.parse::<IpAddr>()
}
struct ConnectionLimitGuard(Option<IpAddr>);
impl Drop for ConnectionLimitGuard {
fn drop(&mut self) {
@ -58,181 +42,32 @@ impl Drop for ConnectionLimitGuard {
pub type OmikronResult<T> = crate::error::Result<T>;
pub struct WaitingTask {
pub sender: oneshot::Sender<CommunicationValue>,
pub task: Box<dyn Fn(Arc<OmikronConnection>, CommunicationValue) -> bool + Send + Sync>,
pub inserted_at: Instant,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum DispatchClass {
Concurrent,
Ordered,
}
pub struct OmikronConnection {
id: u64,
state: Arc<OmegaState>,
sender: Mutex<Option<WebMtpSender>>,
waiting_tasks: DashMap<u32, WaitingTask>,
cleanup_handle: std::sync::Mutex<Option<tokio::task::JoinHandle<()>>>,
handler_tasks: TaskTracker,
handler_cancel: CancellationToken,
handler_limit: Arc<Semaphore>,
ordered_sender: mpsc::Sender<CommunicationValue>,
ordered_receiver: StdMutex<Option<mpsc::Receiver<CommunicationValue>>>,
peer_capabilities: PeerCapabilities,
}
impl Drop for OmikronConnection {
fn drop(&mut self) {
if let Some(handle) = self.cleanup_handle.lock().unwrap().take() {
handle.abort();
}
self.handler_cancel.cancel();
self.handler_tasks.close();
self.waiting_tasks.clear();
}
}
impl OmikronConnection {
pub fn new(
sender: WebMtpSender,
id: u64,
description: Option<&str>,
state: Arc<OmegaState>,
authenticated: bool,
) -> Option<Arc<Self>> {
if !authenticated || id == 0 || i64::try_from(id).is_err() {
return None;
}
let peer_capabilities =
PeerCapabilities::from_identification_description(description).ok()?;
let handler_concurrency = state.omikron_handler_concurrency;
let (ordered_sender, ordered_receiver) = mpsc::channel(handler_concurrency);
Some(Arc::new(Self {
pub fn new(sender: WebMtpSender, id: u64) -> Arc<Self> {
Arc::new(Self {
id,
state,
sender: Mutex::new(Some(sender)),
waiting_tasks: DashMap::new(),
cleanup_handle: std::sync::Mutex::new(None),
handler_tasks: TaskTracker::new(),
handler_cancel: CancellationToken::new(),
handler_limit: Arc::new(Semaphore::new(handler_concurrency)),
ordered_sender,
ordered_receiver: StdMutex::new(Some(ordered_receiver)),
peer_capabilities,
}))
}
pub fn peer_capabilities(&self) -> &PeerCapabilities {
&self.peer_capabilities
}
fn track_handler<F>(&self, task: F)
where
F: std::future::Future<Output = ()> + Send + 'static,
{
let handle = self.handler_tasks.spawn(task);
let id = self.id as i64;
tokio::spawn(async move {
if let Err(error) = handle.await {
log_err!(
id,
PrintType::Omega,
"Omikron handler task failed: {}",
error
);
}
});
}
fn start_ordered_worker(self: &Arc<Self>) {
let Some(mut receiver) = self.ordered_receiver.lock().unwrap().take() else {
return;
};
let connection = self.clone();
let cancellation = self.handler_cancel.clone();
self.track_handler(async move {
loop {
let value = tokio::select! {
_ = cancellation.cancelled() => break,
value = receiver.recv() => match value {
Some(value) => value,
None => break,
},
};
let global_permit = tokio::select! {
_ = cancellation.cancelled() => break,
permit = connection.state.global_handler_limit.clone().acquire_owned() => {
match permit {
Ok(permit) => permit,
Err(_) => break,
}
}
};
let permit = tokio::select! {
_ = cancellation.cancelled() => break,
permit = connection.handler_limit.clone().acquire_owned() => {
match permit {
Ok(permit) => permit,
Err(_) => break,
}
}
};
let result = tokio::select! {
_ = cancellation.cancelled() => break,
result = connection.clone().dispatch(value) => result,
};
drop(permit);
drop(global_permit);
if let Err(error) = result {
log_err!(
connection.id as i64,
PrintType::Omega,
"Error processing ordered Omikron message: {}",
error
);
}
}
});
}
async fn stop_handlers(&self) {
self.handler_cancel.cancel();
self.handler_tasks.close();
let _ = tokio::time::timeout(Duration::from_secs(1), self.handler_tasks.wait()).await;
self.waiting_tasks.clear();
}
fn correlation_response(value: &CommunicationValue) -> bool {
matches!(
value.get_comm_type_enum(),
Some(CommunicationType::Success)
| Some(CommunicationType::ErrorProtocol)
| Some(CommunicationType::ErrorAnonymous)
| Some(CommunicationType::ErrorInternal)
| Some(CommunicationType::ErrorInvalidData)
| Some(CommunicationType::ErrorInvalidUserId)
| Some(CommunicationType::ErrorInvalidOmikronId)
| Some(CommunicationType::ErrorNotFound)
| Some(CommunicationType::ErrorNotAuthenticated)
| Some(CommunicationType::ErrorNoIota)
| Some(CommunicationType::ErrorInvalidChallenge)
| Some(CommunicationType::ErrorInvalidSecret)
| Some(CommunicationType::ErrorInvalidPrivateKey)
| Some(CommunicationType::ErrorInvalidPublicKey)
| Some(CommunicationType::ErrorNoUserId)
| Some(CommunicationType::ErrorNoCallId)
| Some(CommunicationType::ErrorInvalidCallId)
)
}
fn next_correlation_id(&self) -> Option<u32> {
for _ in 0..u32::MAX {
let id = NEXT_CORRELATION_ID.fetch_add(1, Ordering::Relaxed);
if id != 0 && !self.waiting_tasks.contains_key(&id) {
return Some(id);
}
}
None
})
}
pub async fn handle(self: Arc<Self>, receiver: &mut WebMtpReceiver) {
@ -241,33 +76,12 @@ impl OmikronConnection {
PrintType::Omega,
"Omikron connection started"
);
let capabilities = CommunicationValue::new(CommunicationType::IdentificationResponse)
.add_typed_default(
mtp::codec::DataType::Description,
mtp::codec::DataValue::Str(
OmegaCapabilities::current().identification_description(),
),
);
if let Err(error) = self.clone().send(&capabilities).await {
log_err!(
self.id as i64,
PrintType::Omega,
"Failed to send Omega capabilities: {}",
error
);
self.clone().cleanup().await;
return;
}
self.start_ordered_worker();
let cleanup_conn = Arc::downgrade(&self);
let cleanup_conn = self.clone();
*self.cleanup_handle.lock().unwrap() = Some(tokio::spawn(async move {
let mut ticker = interval(CLEANUP_INTERVAL);
loop {
ticker.tick().await;
let Some(connection) = cleanup_conn.upgrade() else {
break;
};
connection
cleanup_conn
.waiting_tasks
.retain(|_, task| task.inserted_at.elapsed() < MAX_WAITING_AGE);
}
@ -309,136 +123,36 @@ impl OmikronConnection {
async fn process_message(self: Arc<Self>, value: CommunicationValue) -> OmikronResult<()> {
log_cv_in!(PrintType::Omikron, &value);
if Self::correlation_response(&value)
&& let Some(message_id) = value.id()
&& let Some((_, task)) = self.waiting_tasks.remove(&message_id)
{
let _ = task.sender.send(value);
if let Some((_, task)) = self.waiting_tasks.remove(&value.get_id()) {
let _ = (task.task)(self.clone(), value);
return Ok(());
}
if !value.is_type(CommunicationType::Relay) {
validate_dispatch_fields(&value)?;
}
let dispatch_class = Self::dispatch_class(&value);
if dispatch_class == DispatchClass::Ordered {
tokio::select! {
_ = self.handler_cancel.cancelled() => {
return Err(crate::error::OmegaError::NotConnected);
}
result = self.ordered_sender.send(value) => {
result.map_err(|_| crate::error::OmegaError::NotConnected)?;
}
}
return Ok(());
}
let global_permit = tokio::select! {
_ = self.handler_cancel.cancelled() => {
return Err(crate::error::OmegaError::NotConnected);
}
permit = self.state.global_handler_limit.clone().acquire_owned() => {
permit.map_err(|_| crate::error::OmegaError::NotConnected)?
}
};
let permit = tokio::select! {
_ = self.handler_cancel.cancelled() => {
drop(global_permit);
return Err(crate::error::OmegaError::NotConnected);
}
permit = self.handler_limit.clone().acquire_owned() => {
permit.map_err(|_| crate::error::OmegaError::NotConnected)?
}
};
let connection = self.clone();
let cancellation = self.handler_cancel.clone();
self.track_handler(async move {
tokio::select! {
_ = cancellation.cancelled() => {}
result = connection.clone().dispatch(value) => {
if let Err(error) = result {
log_err!(
connection.id as i64,
PrintType::Omega,
"Error processing Omikron message: {}",
error
);
}
}
}
drop(permit);
drop(global_permit);
});
Ok(())
}
fn dispatch_class(value: &CommunicationValue) -> DispatchClass {
match value.get_comm_type_enum() {
Some(CommunicationType::UserConnected)
| Some(CommunicationType::UserDisconnected)
| Some(CommunicationType::IotaConnected)
| Some(CommunicationType::IotaDisconnected)
| Some(CommunicationType::SyncClientIotaStatus)
| Some(CommunicationType::StateSubscribe)
| Some(CommunicationType::ClientChanged) => DispatchClass::Ordered,
_ => DispatchClass::Concurrent,
}
self.dispatch(value).await
}
async fn dispatch(self: Arc<Self>, value: CommunicationValue) -> OmikronResult<()> {
let id = self.id as i64;
let state = self.state.clone();
if value.is_type(CommunicationType::Relay) {
let value = crate::transport::relay_router::ensure_relay_frame_id(value);
let request_id = value.id();
let result = crate::transport::relay_router::route_from_omikron(id, value).await;
let response = match &result {
Ok(response) => request_id.map(|request_id| response.clone().with_id(request_id)),
Err(error) => {
log_err!(id, PrintType::Omega, "Relay routing failed: {}", error);
request_id.map(|request_id| {
CommunicationValue::new(
crate::transport::relay_router::error_response_type(error),
)
.with_id(request_id)
})
}
};
if let Some(response) = response
&& let Err(send_error) = self.clone().send(&response).await
{
log_err!(
id,
PrintType::Omega,
"Relay routing response failed: {}",
send_error
);
}
return result
.map(|_| ())
.map_err(|error| crate::error::OmegaError::Transport(error.to_string()));
}
match value.get_comm_type_enum() {
Some(CommunicationType::ShortenLink) => {
crate::transport::handlers::links::shorten(self, value).await
}
Some(CommunicationType::UserConnected) => {
crate::transport::handlers::presence::user_connected(state, self, value, id).await
crate::transport::handlers::presence::user_connected(self, value, id).await
}
Some(CommunicationType::UserDisconnected) => {
crate::transport::handlers::presence::user_disconnected(state, self, value, id)
.await
crate::transport::handlers::presence::user_disconnected(self, value, id).await
}
Some(CommunicationType::ClientChanged) => {
crate::transport::handlers::presence::client_changed(self, value, id).await
}
Some(CommunicationType::IotaConnected) => {
crate::transport::handlers::presence::iota_connected(state, self, value, id).await
crate::transport::handlers::presence::iota_connected(self, value, id).await
}
Some(CommunicationType::IotaDisconnected) => {
crate::transport::handlers::presence::iota_disconnected(state, self, value, id)
.await
crate::transport::handlers::presence::iota_disconnected(self, value, id).await
}
Some(CommunicationType::SyncClientIotaStatus) => {
crate::transport::handlers::presence::sync_status(state, self, value, id).await
crate::transport::handlers::presence::sync_status(self, value, id).await
}
Some(CommunicationType::GetUserData) => {
crate::transport::handlers::user_data::get_user(self, value).await
@ -449,6 +163,9 @@ impl OmikronConnection {
Some(CommunicationType::ChangeUserData) => {
crate::transport::handlers::user_data::change_user(self, value).await
}
Some(CommunicationType::ChangeIotaData) => {
crate::transport::handlers::user_data::change_iota(self, value).await
}
Some(CommunicationType::GetRegister) => {
crate::transport::handlers::register::get_register(self, value).await
}
@ -461,24 +178,6 @@ impl OmikronConnection {
Some(CommunicationType::DeleteUser) => {
crate::transport::handlers::account::user(self, value).await
}
Some(CommunicationType::AttachUserBegin) => {
crate::transport::handlers::account::attach_begin(self, value).await
}
Some(CommunicationType::AttachUserComplete) => {
crate::transport::handlers::account::attach_complete(self, value).await
}
Some(CommunicationType::DeleteUserCredentialBegin) => {
crate::transport::handlers::account::delete_credential_begin(self, value).await
}
Some(CommunicationType::DeleteUserCredentialComplete) => {
crate::transport::handlers::account::delete_credential_complete(self, value).await
}
Some(CommunicationType::EraseHostedUserDataAck) => {
crate::transport::handlers::account::erase_hosted_user_data_ack(self, value).await
}
Some(CommunicationType::ReleaseUserFromIota) => {
crate::transport::handlers::account::release_from_iota(self, value).await
}
Some(CommunicationType::DeleteIota) => {
crate::transport::handlers::account::iota(self, value).await
}
@ -494,13 +193,6 @@ impl OmikronConnection {
Some(CommunicationType::GetStates) => {
crate::transport::handlers::states::get(self, value).await
}
Some(CommunicationType::StateSubscribe) => {
crate::transport::handlers::presence::state_subscribe(state, self, value, id).await
}
Some(CommunicationType::ClientChanged) => {
crate::transport::handlers::presence::client_changed_legacy(state, self, value, id)
.await
}
_ => {
log_err!(
0,
@ -524,71 +216,6 @@ impl OmikronConnection {
.await
.map_err(|error| crate::error::OmegaError::SendError(error.to_string()))
}
pub(crate) async fn send_messages(
self: Arc<Self>,
values: &[CommunicationValue],
) -> OmikronResult<()> {
let guard = self.sender.lock().await;
let sender = guard
.as_ref()
.ok_or(crate::error::OmegaError::NotConnected)?;
for value in values {
log_cv_out!(PrintType::Omikron, value);
sender
.send(value)
.await
.map_err(|error| crate::error::OmegaError::SendError(error.to_string()))?;
}
Ok(())
}
pub(crate) async fn await_response(
self: Arc<Self>,
value: &CommunicationValue,
timeout: Duration,
) -> OmikronResult<CommunicationValue> {
value.require_id()?;
let (tx, rx) = oneshot::channel();
let message_id = self.next_correlation_id().ok_or_else(|| {
crate::OmegaError::Transport("no relay correlation id available".into())
})?;
match self.waiting_tasks.entry(message_id) {
Entry::Vacant(entry) => {
entry.insert(WaitingTask {
sender: tx,
inserted_at: Instant::now(),
});
}
Entry::Occupied(_) => {
return Err(crate::OmegaError::Transport(
"relay correlation id collision".into(),
));
}
}
let outbound = value.clone().with_id(message_id);
if let Err(error) = self.clone().send(&outbound).await {
self.waiting_tasks.remove(&message_id);
return Err(error);
}
match tokio::time::timeout(timeout, rx).await {
Ok(Ok(response)) => Ok(response),
Ok(Err(_)) => {
self.waiting_tasks.remove(&message_id);
Err(crate::error::OmegaError::Transport(
"Relay response channel closed".into(),
))
}
Err(_) => {
self.waiting_tasks.remove(&message_id);
Err(crate::error::OmegaError::Transport(
"Relay response timed out".into(),
))
}
}
}
pub(crate) async fn send_error_response(
self: Arc<Self>,
message_id: u32,
@ -597,22 +224,6 @@ impl OmikronConnection {
self.send(&CommunicationValue::new(error_type).with_id(message_id))
.await
}
pub(crate) async fn send_error_response_with_detail(
self: Arc<Self>,
message_id: u32,
error_type: CommunicationType,
detail: &'static str,
) -> OmikronResult<()> {
self.send(
&CommunicationValue::new(error_type)
.with_id(message_id)
.add_typed_default(
mtp::codec::DataType::ErrorType,
mtp::codec::DataValue::Str(detail.to_string()),
),
)
.await
}
pub async fn close(self: Arc<Self>) {
log_in!(
self.id as i64,
@ -624,15 +235,10 @@ impl OmikronConnection {
}
}
async fn cleanup(self: Arc<Self>) {
self.stop_handlers().await;
if self.id != 0 {
log_in!(self.id as i64, PrintType::Omega, "Omikron disconnected");
if omikron_manager::remove_omikron(self.id as i64, &self).await {
crate::transport::handlers::presence::omikron_disconnected(
self.state.clone(),
self.id as i64,
)
.await;
crate::sql::user_online_tracker::untrack_omikron(self.id as i64).await;
}
}
if let Some(handle) = self.cleanup_handle.lock().unwrap().take() {
@ -642,21 +248,13 @@ impl OmikronConnection {
pub async fn get_omikron_id(self: Arc<Self>) -> Option<i64> {
Some(self.id as i64)
}
pub fn state(&self) -> Arc<OmegaState> {
self.state.clone()
}
pub async fn send_message(self: Arc<Self>, value: &CommunicationValue) -> OmikronResult<()> {
self.send(value).await
}
}
pub async fn get_by_omikron_id(
omikron_id: u64,
description: Option<String>,
) -> Option<PublicKeyBundle> {
PeerCapabilities::from_identification_description(description.as_deref()).ok()?;
let omikron_id = i64::try_from(omikron_id).ok().filter(|id| *id > 0)?;
crate::db::omikron_repo::get_omikron_by_id(OmikronId::from(omikron_id))
pub async fn get_by_omikron_id(omikron_id: u64, _: Option<String>) -> Option<PublicKeyBundle> {
crate::db::omikron_repo::get_omikron_by_id(OmikronId::from(omikron_id as i64))
.await
.ok()
.map(|omikron| omikron.public_key)
@ -665,13 +263,13 @@ pub async fn complete_register(_: PublicKeyBundle, _: Option<String>) -> u64 {
0
}
pub async fn start(port: u16, state: Arc<OmegaState>) -> Result<(), Box<dyn std::error::Error>> {
pub async fn start(port: u16) -> Result<(), Box<dyn std::error::Error>> {
let cert_pem = load_file_vec("certs", "cert.pem")?;
let key_pem = load_file_vec("certs", "key.pem")?;
let web_config = server::web::build_web_config(state.identity.clone())?
let web_config = server::server::build_web_config()?
.serve_tcp_https(true)
.max_tcp_connections(256);
let ip = parse_bind_address(std::env::var("BIND_ADDRESS").ok().as_deref())?;
let ip = IpAddr::from(Ipv4Addr::new(0, 0, 0, 0));
let host_config = HostConfig::new(ip, port, cert_pem, key_pem)
.with_policy(Policy {
send_mode: SendMode::SingleStreamPerMessage,
@ -693,7 +291,7 @@ pub async fn start(port: u16, state: Arc<OmegaState>) -> Result<(), Box<dyn std:
max_frames_per_stream: None,
})
.with_authentication(
state.identity.clone_keyring()?,
load_keyring(),
Box::new(|id, description| Box::pin(get_by_omikron_id(id, description))),
Box::new(|key, description| Box::pin(complete_register(key, description))),
)
@ -714,7 +312,7 @@ pub async fn start(port: u16, state: Arc<OmegaState>) -> Result<(), Box<dyn std:
continue;
}
};
let config = &state.config.rate_limits;
let config = crate::config::RateLimitConfig::from_env();
let peer_ip = conn.remote_addr.map(|address| address.ip());
let active = ACTIVE_CONNECTIONS.fetch_add(1, Ordering::AcqRel) + 1;
let peer_active = peer_ip.map(|ip| {
@ -722,11 +320,10 @@ pub async fn start(port: u16, state: Arc<OmegaState>) -> Result<(), Box<dyn std:
*count += 1;
*count
});
let connection_limit_guard = ConnectionLimitGuard(peer_ip);
if active > config.transport_connections
|| peer_active.is_some_and(|count| count > config.transport_connections_per_ip)
{
drop(connection_limit_guard);
drop(ConnectionLimitGuard(peer_ip));
log_err!(
0,
PrintType::Omega,
@ -734,90 +331,12 @@ pub async fn start(port: u16, state: Arc<OmegaState>) -> Result<(), Box<dyn std:
);
continue;
}
let authenticated = matches!(&conn.auth_state, AuthState::Authenticated);
let Some(connection) = OmikronConnection::new(
conn.sender,
conn.client_id,
conn.description.as_deref(),
state.clone(),
authenticated,
) else {
drop(connection_limit_guard);
log_err!(
0,
PrintType::Omega,
"Rejected Omikron connection without authenticated transport state or valid capabilities"
);
continue;
};
let connection = OmikronConnection::new(conn.sender, conn.client_id);
tokio::spawn(async move {
let _guard = connection_limit_guard;
let _guard = ConnectionLimitGuard(peer_ip);
omikron_manager::add_omikron(connection.clone()).await;
connection.handle(&mut conn.receiver).await;
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::{DispatchClass, OmikronConnection, parse_bind_address};
use mtp::codec::{CommunicationType, CommunicationValue};
use std::net::{IpAddr, Ipv4Addr};
#[test]
fn relay_dispatch_is_not_on_the_ordered_state_lane() {
let value = CommunicationValue::new(CommunicationType::Relay);
assert_eq!(
OmikronConnection::dispatch_class(&value),
DispatchClass::Concurrent
);
}
#[test]
fn parses_configured_bind_address() {
assert_eq!(
parse_bind_address(Some("10.200.2.0")),
Ok(IpAddr::V4(Ipv4Addr::new(10, 200, 2, 0)))
);
}
#[test]
fn presence_lifecycle_dispatch_is_ordered() {
let value = CommunicationValue::new(CommunicationType::UserConnected);
assert_eq!(
OmikronConnection::dispatch_class(&value),
DispatchClass::Ordered
);
}
#[test]
fn client_presence_changes_are_ordered() {
let value = CommunicationValue::new(CommunicationType::ClientChanged);
assert_eq!(
OmikronConnection::dispatch_class(&value),
DispatchClass::Ordered
);
}
#[test]
fn only_protocol_responses_match_waiting_tasks() {
assert!(OmikronConnection::correlation_response(
&CommunicationValue::new(CommunicationType::Success).with_id(1)
));
assert!(OmikronConnection::correlation_response(
&CommunicationValue::new(CommunicationType::ErrorInvalidData).with_id(1)
));
assert!(!OmikronConnection::correlation_response(
&CommunicationValue::new(CommunicationType::GetUserData).with_id(1)
));
}
#[test]
fn defaults_bind_address_to_all_interfaces() {
assert_eq!(
parse_bind_address(None),
Ok(IpAddr::V4(Ipv4Addr::UNSPECIFIED))
);
}
}

View file

@ -1,18 +1,12 @@
use crate::db::user_repo;
use crate::state::OmegaState;
use crate::transport::connection::OmikronConnection;
use crate::transport::omikron_connection::OmikronResult;
use dashmap::DashMap;
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
use mtp::codec::CommunicationValue;
use once_cell::sync::Lazy;
use rand::prelude::IteratorRandom;
use std::sync::Arc;
use std::time::Duration;
use tokio::task::JoinHandle;
use tokio::time::interval;
pub static OMIKRON_CONNECTIONS: Lazy<DashMap<i64, Arc<OmikronConnection>>> =
Lazy::new(DashMap::new);
Lazy::new(|| DashMap::new());
pub async fn add_omikron(conn: Arc<OmikronConnection>) {
let id = match conn.clone().get_omikron_id().await {
@ -26,7 +20,6 @@ pub async fn add_omikron(conn: Arc<OmikronConnection>) {
if let Some(old) = OMIKRON_CONNECTIONS.insert(id, conn.clone()) {
old.close().await;
}
let _ = flush_iota_snapshot_outbox().await;
}
pub async fn remove_omikron(omikron_id: i64, connection: &Arc<OmikronConnection>) -> bool {
@ -41,175 +34,22 @@ pub fn get_connected_omikron(omikron_id: i64) -> Option<Arc<OmikronConnection>>
.map(|connection| connection.clone())
}
pub fn get_state() -> Option<Arc<OmegaState>> {
OMIKRON_CONNECTIONS
.iter()
.next()
.map(|connection| connection.value().state())
}
pub fn get_iota_primary_omikron_connection(iota_id: i64) -> Option<i64> {
get_state().and_then(|state| state.presence.primary_iota_route(iota_id))
}
pub async fn get_all_connections()
-> Result<std::collections::HashMap<i64, std::collections::HashMap<i64, Vec<i64>>>, ()> {
match get_state() {
Some(state) => {
let mut result = state.presence.connection_routes();
let iota_ids = state
.presence
.all_iota_routes()
.keys()
.copied()
.collect::<Vec<_>>();
let users = user_repo::get_users_by_iota_ids(&iota_ids)
.await
.map_err(|_| ())?;
for user in users {
if let Some(iota_id) = user.iota_id {
for omikron_id in state
.presence
.iota_connections(iota_id.0)
.unwrap_or_default()
{
if let Some(users) = result
.get_mut(&omikron_id)
.and_then(|iotas| iotas.get_mut(&iota_id.0))
{
users.push(user.id.0);
}
}
}
}
for iotas in result.values_mut() {
for users in iotas.values_mut() {
users.sort_unstable();
users.dedup();
}
}
Ok(result)
}
None => Ok(std::collections::HashMap::new()),
}
}
pub async fn send_state_batch(
omikron_id: i64,
notifications: Vec<CommunicationValue>,
) -> OmikronResult<()> {
let connection =
get_connected_omikron(omikron_id).ok_or(crate::error::OmegaError::NotConnected)?;
connection.send_messages(&notifications).await
}
pub async fn get_random_omikron() -> Result<Arc<OmikronConnection>, ()> {
let keys: Vec<_> = OMIKRON_CONNECTIONS.iter().map(|e| *e.key()).collect();
if let Some(key) = keys.into_iter().choose(&mut rand::rng())
&& let Some(connection) = get_connected_omikron(key)
{
if let Some(key) = keys.into_iter().choose(&mut rand::rng()) {
if let Some(connection) = get_connected_omikron(key) {
return Ok(connection);
}
}
Err(())
}
pub async fn send_to_user(user_id: i64, cv: &CommunicationValue) {
if let Some(state) = get_state() {
for user_route in state.presence.routes_for_user(user_id) {
if let Some(omikron_conn) = OMIKRON_CONNECTIONS.get(&user_route.omikron_id) {
if let Some(user_conn) = crate::sql::user_online_tracker::get_user_status(user_id) {
if let Some(omikron_conn) = OMIKRON_CONNECTIONS.get(&user_conn.omikron_id) {
let _ = omikron_conn.value().clone().send_message(cv).await;
}
}
}
}
/*
* Publish each Iota membership snapshot to every live relay route. Each
* Omikron keeps a local authorization index, so sending only a primary route
* leaves the remaining relays stale after registration or migration.
*/
pub async fn publish_iota_user_snapshot(iota_id: i64) -> OmikronResult<()> {
let state = get_state().ok_or(crate::error::OmegaError::NotConnected)?;
let omikron_ids = state
.presence
.iota_connections(iota_id)
.ok_or(crate::error::OmegaError::NotConnected)?;
let users = user_repo::get_users_by_iota_id(crate::models::IotaId::from(iota_id)).await?;
let user_ids = users
.into_iter()
.map(|user| DataValue::SignedNumber(user.id.0.into()))
.collect();
let snapshot = CommunicationValue::new(CommunicationType::IotaUserData)
.add_typed_default(DataType::IotaId, DataValue::SignedNumber(iota_id.into()))
.add_typed_default(DataType::UserIds, DataValue::Array(user_ids));
for omikron_id in omikron_ids {
let connection =
get_connected_omikron(omikron_id).ok_or(crate::error::OmegaError::NotConnected)?;
connection.send(&snapshot).await?;
}
Ok(())
}
pub async fn flush_iota_snapshot_outbox() -> OmikronResult<()> {
for iota_id in user_repo::pending_iota_snapshots().await? {
match publish_iota_user_snapshot(iota_id.0).await {
Ok(()) => {
if let Err(error) = user_repo::complete_iota_snapshot(iota_id).await {
crate::log_in!(
crate::util::logger::PrintType::General,
"Could not complete Iota snapshot outbox entry for {}: {}",
iota_id.0,
error
);
}
}
Err(error) => {
crate::log_in!(
crate::util::logger::PrintType::General,
"Could not publish Iota snapshot for {}: {}",
iota_id.0,
error
);
}
}
}
Ok(())
}
pub fn spawn_iota_snapshot_outbox_worker() -> JoinHandle<()> {
tokio::spawn(async {
let mut retry = interval(Duration::from_secs(30));
loop {
retry.tick().await;
if let Err(error) = flush_iota_snapshot_outbox().await {
crate::log_in!(
crate::util::logger::PrintType::General,
"Could not load Iota snapshot outbox: {}",
error
);
}
}
})
}
pub async fn deliver_pending_erasures(iota_id: i64) {
let Ok(users) =
user_repo::pending_erasures_for_iota(crate::models::IotaId::from(iota_id)).await
else {
return;
};
let Some(omikron_id) = get_iota_primary_omikron_connection(iota_id) else {
return;
};
let Some(connection) = get_connected_omikron(omikron_id) else {
return;
};
for user_id in users {
let request = CommunicationValue::new(CommunicationType::EraseHostedUserData)
.add_typed_default(DataType::UserId, DataValue::SignedNumber(user_id.0.into()))
.add_typed_default(DataType::IotaId, DataValue::SignedNumber(iota_id.into()));
let _ = connection.clone().send(&request).await;
}
}

View file

@ -1,230 +0,0 @@
use super::omikron_manager;
use crate::{log_err, util::logger::PrintType};
use mtp::codec::{CommunicationType, CommunicationValue, RelayError, forward_relay_frame};
use std::{
convert::TryFrom,
sync::atomic::{AtomicU32, Ordering},
time::Duration,
};
use thiserror::Error;
const TARGET_KIND_MASK: u64 = 0xC000_0000_0000_0000;
const TARGET_ID_MASK: u64 = (1_u64 << 48) - 1;
const USER_TARGET_KIND: u64 = 0x4000_0000_0000_0000;
const IOTA_TARGET_KIND: u64 = 0x8000_0000_0000_0000;
static NEXT_RELAY_FRAME_ID: AtomicU32 = AtomicU32::new(1);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RouteTarget {
User(u64),
Iota(u64),
}
impl RouteTarget {
pub fn from_wire_id(value: u64) -> Option<Self> {
let id = value & TARGET_ID_MASK;
if id == 0 || value & !(TARGET_KIND_MASK | TARGET_ID_MASK) != 0 {
return None;
}
match value & TARGET_KIND_MASK {
USER_TARGET_KIND => Some(Self::User(id)),
IOTA_TARGET_KIND => Some(Self::Iota(id)),
_ => None,
}
}
}
#[derive(Debug, Error)]
pub enum RelayRouteError {
#[error("relay has no destination Iota")]
MissingDestinationIota,
#[error("relay has an outer sender")]
OuterSenderPresent,
#[error("relay has invalid route target {0}")]
InvalidDestinationTarget(u64),
#[error("relay destination Iota is outside Omega's ID range")]
DestinationIotaOutOfRange,
#[error("destination Iota is offline")]
IotaOffline,
#[error("destination Omikron is offline")]
OmikronOffline,
#[error("relay route resolves back to source Omikron")]
RouteLoop,
#[error(transparent)]
Relay(#[from] RelayError),
#[error("sending relay to destination Omikron failed: {0}")]
Send(String),
}
pub fn ensure_relay_frame_id(frame: CommunicationValue) -> CommunicationValue {
if frame.id().is_some_and(|id| id != 0) {
return frame;
}
let id = NEXT_RELAY_FRAME_ID.fetch_add(1, Ordering::Relaxed).max(1);
frame.with_id(id)
}
pub fn error_response_type(error: &RelayRouteError) -> CommunicationType {
match error {
RelayRouteError::IotaOffline | RelayRouteError::OmikronOffline => {
CommunicationType::ErrorNoIota
}
RelayRouteError::Send(_) => CommunicationType::ErrorInternal,
RelayRouteError::MissingDestinationIota
| RelayRouteError::OuterSenderPresent
| RelayRouteError::InvalidDestinationTarget(_)
| RelayRouteError::DestinationIotaOutOfRange
| RelayRouteError::RouteLoop
| RelayRouteError::Relay(_) => CommunicationType::ErrorInvalidData,
}
}
pub async fn route_from_omikron(
source_omikron_id: i64,
frame: CommunicationValue,
) -> Result<CommunicationValue, RelayRouteError> {
let frame = ensure_relay_frame_id(frame);
if !frame.is_type(CommunicationType::Relay) {
return Err(RelayRouteError::Relay(RelayError::NotRelay));
}
if frame.sender().is_some() {
return Err(RelayRouteError::OuterSenderPresent);
}
let Some(destination_wire_id) = frame.receiver() else {
log_err!(
source_omikron_id,
PrintType::Omega,
"Relay routing failed: missing destination Iota"
);
return Err(RelayRouteError::MissingDestinationIota);
};
let Some(RouteTarget::Iota(destination_iota)) = RouteTarget::from_wire_id(destination_wire_id)
else {
return Err(RelayRouteError::InvalidDestinationTarget(
destination_wire_id,
));
};
let destination_iota_i64 =
i64::try_from(destination_iota).map_err(|_| RelayRouteError::DestinationIotaOutOfRange)?;
let frame = forward_relay_frame(&frame, destination_wire_id)?;
let Some(destination_omikron) =
omikron_manager::get_iota_primary_omikron_connection(destination_iota_i64)
else {
log_err!(
source_omikron_id,
PrintType::Omega,
"Relay destination Iota {} is offline",
destination_iota
);
return Err(RelayRouteError::IotaOffline);
};
if destination_omikron == source_omikron_id {
log_err!(
source_omikron_id,
PrintType::Omega,
"Relay route loop for destination Iota {} and Omikron {}",
destination_iota,
destination_omikron
);
return Err(RelayRouteError::RouteLoop);
}
let Some(connection) = omikron_manager::get_connected_omikron(destination_omikron) else {
log_err!(
source_omikron_id,
PrintType::Omega,
"Relay destination Iota {} resolves to disconnected Omikron {}",
destination_iota,
destination_omikron
);
return Err(RelayRouteError::OmikronOffline);
};
let response = connection
.await_response(&frame, Duration::from_secs(20))
.await
.map_err(|error| {
log_err!(
source_omikron_id,
PrintType::Omega,
"Relay send to destination Iota {} via Omikron {} failed: {}",
destination_iota,
destination_omikron,
error
);
RelayRouteError::Send(error.to_string())
})?;
if response.is_type(CommunicationType::Success) {
Ok(response)
} else {
Err(RelayRouteError::Send(format!(
"destination Omikron rejected the Relay with {}",
response.get_type()
)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use mtp::codec::DataValue;
fn wire(target: RouteTarget) -> u64 {
let (kind, id) = match target {
RouteTarget::User(id) => (USER_TARGET_KIND, id),
RouteTarget::Iota(id) => (IOTA_TARGET_KIND, id),
};
kind | id
}
fn relay_frame() -> CommunicationValue {
CommunicationValue::new(CommunicationType::Relay)
.without_sender()
.with_receiver(wire(RouteTarget::Iota(42)))
.with_payload(DataValue::Bytes(vec![1, 2, 3, 4]))
}
#[test]
fn forwarding_preserves_relay_payload_and_next_hop() {
let frame = relay_frame().with_receiver(wire(RouteTarget::Iota(7)));
let result = forward_relay_frame(&frame, wire(RouteTarget::Iota(42)));
assert!(result.is_ok());
let Ok(forwarded) = result else { return };
assert_eq!(forwarded.receiver(), Some(wire(RouteTarget::Iota(42))));
assert_eq!(forwarded.sender(), None);
assert_eq!(forwarded.payload(), frame.payload());
assert_eq!(forwarded.id(), frame.id());
}
#[tokio::test]
async fn outer_sender_is_rejected_before_route_lookup() {
let frame = relay_frame().with_sender(9);
let result = route_from_omikron(1, frame).await;
assert!(matches!(result, Err(RelayRouteError::OuterSenderPresent)));
}
#[tokio::test]
async fn missing_destination_is_rejected_before_route_lookup() {
let frame = relay_frame().without_receiver();
let result = route_from_omikron(1, frame).await;
assert!(matches!(
result,
Err(RelayRouteError::MissingDestinationIota)
));
}
#[tokio::test]
async fn user_route_target_is_rejected_by_opaque_omega_router() {
let frame = relay_frame().with_receiver(wire(RouteTarget::User(42)));
let result = route_from_omikron(1, frame).await;
assert!(matches!(
result,
Err(RelayRouteError::InvalidDestinationTarget(_))
));
}
#[tokio::test]
async fn unavailable_destination_is_reported_as_iota_offline() {
let result = route_from_omikron(1, relay_frame()).await;
assert!(matches!(result, Err(RelayRouteError::IotaOffline)));
}
}

View file

@ -1,15 +1,293 @@
use once_cell::sync::Lazy;
use std::path::PathBuf;
use std::fs::{self, File};
use std::io::{self, BufReader, Read};
use std::path::{Path, PathBuf};
use tokio::io::AsyncWriteExt;
use uuid::Uuid;
use zip::ZipArchive;
use crate::log;
static WORKING_DIR: Lazy<PathBuf> =
Lazy::new(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
pub fn load_file_vec(path: &str, name: &str) -> Result<Vec<u8>, std::io::Error> {
let dir = PathBuf::from(get_directory()).join(path);
pub fn delete_file(path: &str, name: &str) -> bool {
let dir = Path::new(&get_directory()).join(path);
let file = dir.join(name);
if !file.exists() {
return false;
}
fs::remove_file(file).is_ok()
}
#[allow(dead_code)]
pub fn delete_directory(path: &str) -> bool {
let dir = Path::new(&get_directory()).join(path);
delete_dir_recursive(&dir)
}
#[allow(dead_code)]
fn delete_dir_recursive(directory: &Path) -> bool {
if !directory.exists() {
return false;
}
if let Err(e) = fs::remove_dir_all(directory) {
log!(
"[IMPORTANT] Couldn't delete directory {}: {}",
directory.display(),
e
);
return false;
}
true
}
#[allow(dead_code)]
pub fn delete_user_directory(user_id: Uuid) {
let user_dir = Path::new(&get_directory())
.join("users")
.join(user_id.to_string());
let _ = delete_dir_recursive(&user_dir);
}
pub fn load_file_buf(path: &str, name: &str) -> io::Result<BufReader<File>> {
let dir = Path::new(&get_directory()).join(path);
let file_path = dir.join(name);
// Ensure the directory exists, create if necessary
if !dir.exists() {
if let Err(_) = fs::create_dir_all(&dir) {
return Err(io::Error::new(
io::ErrorKind::NotFound,
"Directory creation failed",
));
}
}
// Create the file if it doesn't exist
if !file_path.exists() {
return Err(io::Error::new(
io::ErrorKind::NotFound,
"File creation failed",
));
}
// Open the file and return a BufReader for efficient reading
let file = File::open(&file_path)?;
Ok(BufReader::new(file))
}
pub fn has_file(path: &str, name: &str) -> bool {
let dir = Path::new(&get_directory()).join(path);
let file_path = dir.join(name);
if !dir.exists() {
return false;
}
if !file_path.exists() {
return false;
}
true
}
pub fn has_dir(path: &str) -> bool {
let dir = Path::new(&get_directory()).join(path);
if !dir.exists() {
return false;
}
true
}
pub fn load_file(path: &str, name: &str) -> String {
let dir = Path::new(&get_directory()).join(path);
let file_path = dir.join(name);
if !dir.exists() {
if let Err(e) = fs::create_dir_all(&dir) {
log!("[IMPORTANT] Couldn't create directories: {}", e);
return String::new();
}
return String::new();
}
if !file_path.exists() {
if let Err(e) = File::create(&file_path) {
log!("[IMPORTANT] Couldn't create file: {}", e);
}
return String::new();
}
let mut content = String::new();
if let Ok(mut f) = File::open(&file_path) {
let _ = f.read_to_string(&mut content);
}
content
}
pub fn load_file_vec(path: &str, name: &str) -> Result<Vec<u8>, std::io::Error> {
let dir = Path::new(&get_directory()).join(path);
let file_path = dir.join(name);
std::fs::read(file_path)
}
pub fn save_file(path: &str, name: &str, value: &str) {
let dir = Path::new(&get_directory()).join(path);
let file_path = dir.join(name);
if !dir.exists() {
if let Err(e) = fs::create_dir_all(&dir) {
log!("[IMPORTANT] Couldn't create directories: {}", e);
return;
}
}
if let Err(e) = fs::write(&file_path, value) {
log!(
"[IMPORTANT] Couldn't write file {}: {}",
file_path.display(),
e
);
}
}
pub fn get_children(path: &str) -> Vec<String> {
let dir = Path::new(&get_directory()).join(path);
let mut children = Vec::new();
if let Ok(entries) = fs::read_dir(&dir) {
for entry in entries {
if let Ok(entry) = entry {
children.push(entry.file_name().to_string_lossy().to_string());
}
}
}
children
}
pub fn get_directory() -> String {
WORKING_DIR.to_string_lossy().to_string()
}
pub fn working_dir() -> &'static Path {
&WORKING_DIR
}
// Helper to download the zip file content to a file on disk
#[allow(dead_code)]
async fn download_zip(url: &str, as_name: &Path) -> Result<(), Box<dyn std::error::Error>> {
let response = reqwest::get(url).await?;
// Check for successful response status
if !response.status().is_success() {
return Err(format!("Failed to download file: Status {}", response.status()).into());
}
let mut zip_file = tokio::fs::File::create(as_name).await?;
let body = response.bytes().await?;
zip_file.write_all(&body).await?;
Ok(())
}
#[allow(dead_code, deprecated)]
fn extract_zip_contents_to_folder(
zip_path: &Path,
target_dir: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let file = File::open(zip_path)?;
let mut archive = ZipArchive::new(file)?;
let staging_dir = target_dir.with_extension("staging");
let _ = fs::remove_dir_all(&staging_dir);
fs::create_dir_all(&staging_dir)?;
let mut first_item_name: Option<PathBuf> = None;
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
let entry_path = staging_dir.join(file.sanitized_name());
if i == 0 {
if file.name().ends_with('/') || file.sanitized_name().components().count() == 1 {
first_item_name = Some(file.sanitized_name());
}
}
if file.name().ends_with('/') {
fs::create_dir_all(&entry_path)?;
} else {
if let Some(parent) = entry_path.parent() {
fs::create_dir_all(parent)?;
}
let mut out_file = File::create(entry_path)?;
io::copy(&mut file, &mut out_file)?;
}
}
if let Some(root_path) = first_item_name {
let root_dir = staging_dir.join(&root_path);
if root_dir.is_dir() {
let root_contents_count = fs::read_dir(&staging_dir)?.count();
if root_contents_count == 1
|| (root_contents_count > 1 && fs::metadata(&root_dir).is_ok())
{
let _ = fs::remove_dir_all(target_dir);
fs::create_dir_all(target_dir)?;
for entry in fs::read_dir(root_dir)? {
let entry = entry?;
let src = entry.path();
let dest = target_dir.join(entry.file_name());
if let Err(_) = fs::rename(&src, &dest) {
if src.is_file() {
fs::copy(&src, &dest)?;
} else {
if entry.path().is_dir() {
fs::rename(&src, &dest)?;
}
}
}
}
let _ = fs::remove_dir_all(&staging_dir);
return Ok(());
}
}
}
log!("Extracting directly (no single root folder detected).");
let _ = fs::remove_dir_all(target_dir);
fs::rename(&staging_dir, target_dir)?;
Ok(())
}
#[allow(dead_code)]
pub async fn download_and_extract_zip(url: &str, as_name: &str) {
let base_dir = PathBuf::from(get_directory());
let zip_filename = format!("{}.zip", Uuid::new_v4());
let zip_path = base_dir.join(&zip_filename);
let target_dir = base_dir.join(as_name);
if let Err(e) = download_zip(url, &zip_path).await {
log!("Error downloading file: {}", e);
return;
}
let zip_path_clone = zip_path.clone();
let target_dir_clone = target_dir.clone();
let extract_result = extract_zip_contents_to_folder(&zip_path_clone, &target_dir_clone);
if let Err(e) = extract_result {
log!("Panic during ZIP extraction: {}", e);
}
if let Err(e) = tokio::fs::remove_file(&zip_path).await {
log!("Error cleaning up ZIP file {}: {}", zip_path.display(), e);
}
}

View file

@ -222,7 +222,7 @@ pub fn log_cv_internal(
let formatted = format_cv(cv);
log_internal(
cv.sender().and_then(|sender| i64::try_from(sender).ok()),
Some(cv.get_sender() as i64),
print_type.unwrap_or(PrintType::General),
prefix,
false,
@ -233,14 +233,14 @@ pub fn log_cv_internal(
pub fn format_cv(cv: &CommunicationValue) -> String {
let mut parts = Vec::new();
let sender = cv.sender().filter(|sender| *sender != 0);
let receiver = cv.receiver().filter(|receiver| *receiver != 0);
let sender = cv.get_sender();
let receiver = cv.get_receiver();
if let (Some(sender), Some(receiver)) = (sender, receiver) {
if sender > 0 && receiver > 0 {
parts.push(format!("{} > {}", sender, receiver));
} else if let Some(sender) = sender {
} else if sender > 0 {
parts.push(format!("{}", sender));
} else if let Some(receiver) = receiver {
} else if receiver > 0 {
parts.push(format!("> {}", receiver));
}
@ -248,20 +248,16 @@ pub fn format_cv(cv: &CommunicationValue) -> String {
.get_comm_type_enum()
.map(|kind| kind.to_string())
.unwrap_or_else(|| cv.get_type().to_string());
let id = cv
.id()
.map(|id| id.to_string())
.unwrap_or_else(|| "missing".to_string());
parts.push(format!("{} (id={})", comm_type, id));
parts.push(format!("{} (id={})", comm_type, cv.get_id()));
let data = cv.data().unwrap_or(&[]);
let data = cv.data();
let formated_data = format_data_container(
data.iter().map(|(k, v)| (*k, v.clone())).collect(),
Version(3, 0),
Version(1, 0),
);
parts.push(formated_data.to_string());
parts.push(format!("{}", formated_data));
parts.join(": ")
}
@ -273,7 +269,7 @@ fn format_data_container(data: Vec<(DataTypeId, DataValue)>, version: Version) -
let key_str = key.to_string();
match value {
DataValue::Str(s) => format!("{}=\"{}\"", key_str, abbreviate_string(&s)),
DataValue::Str(s) => format!("{}=\"{}\"", key_str, s),
DataValue::Container(inner) => {
let inner_formatted = format_data_container(inner, version.clone());
@ -304,7 +300,7 @@ fn format_array(arr: Vec<DataValue>, version: Version) -> String {
let parts: Vec<String> = arr
.into_iter()
.map(|value| match value {
DataValue::Str(s) => format!("\"{}\"", abbreviate_string(&s)),
DataValue::Str(s) => format!("\"{}\"", s),
DataValue::Container(inner) => {
let inner_formatted = format_data_container(inner, version.clone());
@ -329,32 +325,6 @@ fn format_array(arr: Vec<DataValue>, version: Version) -> String {
parts.join(", ")
}
fn abbreviate_string(value: &str) -> String {
const EDGE_LENGTH: usize = 4;
let chars: Vec<char> = value.chars().collect();
if chars.len() <= EDGE_LENGTH * 2 {
return value.to_string();
}
let prefix: String = chars.iter().take(EDGE_LENGTH).collect();
let suffix: String = chars.iter().rev().take(EDGE_LENGTH).rev().collect();
format!("{prefix}...{suffix}")
}
#[cfg(test)]
mod tests {
use super::abbreviate_string;
#[test]
fn abbreviates_only_strings_longer_than_eight_characters() {
assert_eq!(abbreviate_string("12345678"), "12345678");
assert_eq!(abbreviate_string("123456789"), "1234...6789");
assert_eq!(abbreviate_string("YWJjZGVmZ2hpag=="), "YWJj...ag==");
}
}
#[macro_export]
macro_rules! log_cv {
($kind:expr, $cv:expr) => {