Compare commits
175 Commits
alpha0
..
1906431061
| Author | SHA1 | Date | |
|---|---|---|---|
| 1906431061 | |||
| 9f130a80b7 | |||
| d23ab73cf9 | |||
| f6531e344e | |||
| 32cc9ff848 | |||
| f4fb42d72e | |||
| c0dcfe997c | |||
| 5748ead926 | |||
| 14a07dcb5c | |||
| fab5818ec7 | |||
| 8836d5c591 | |||
| 0fdf5dd9c7 | |||
| 7d06f0ff3e | |||
| b722a916a9 | |||
| cd9ee54f6d | |||
| 67823237e6 | |||
| aa91d2cc0f | |||
| 66a6674a6a | |||
| a322f3fccf | |||
| e6f9bc796e | |||
| f76213d55a | |||
| aeeebf6f58 | |||
| 423c5d6d64 | |||
| eb7fdc9b03 | |||
| cfa20861c5 | |||
| e9cc5b9c76 | |||
| e6aa919b74 | |||
| c784f6f315 | |||
| cd24da25d2 | |||
| cd0864bd9a | |||
| e3c100df94 | |||
| 4fe989b5ff | |||
| b1ecd04a28 | |||
| bb3640c1c3 | |||
| 9e0751e0d0 | |||
| 353ef42752 | |||
| 63a916d18a | |||
| 7cf212fc76 | |||
| bb56b8dd9c | |||
| f498cfad1e | |||
| f8537aad6d | |||
| 8d589505e5 | |||
| 511e260157 | |||
| 839fb7c0f9 | |||
| 3af112b860 | |||
| 7e68a12adb | |||
| ec8924d05a | |||
| d2bd4deb82 | |||
| 9b6caf4e62 | |||
| 050de1a0b3 | |||
| 606c42cd5e | |||
| af55df1ff5 | |||
| c0fd74f1e6 | |||
| 24cc151938 | |||
| cc663d88c8 | |||
| 118eb29d17 | |||
| e9624e4576 | |||
| 78a8b797e6 | |||
| 9179021544 | |||
| d049f87cdc | |||
| e674a0cb33 | |||
| c1883f1524 | |||
| c284b15788 | |||
| 17c991f442 | |||
| d657e64ae4 | |||
| 2e2ebad364 | |||
| 428eda0ab7 | |||
| a3557d5183 | |||
| 5491244d01 | |||
| 04d2b0246a | |||
| 4dfec86279 | |||
| d14cd161da | |||
| 7c17a11426 | |||
| 6cfc54d943 | |||
| 5aec7b3ad4 | |||
| d709cb9454 | |||
| 05df08efcb | |||
| 813611bde7 | |||
| 7fa997d443 | |||
| 2513f0303a | |||
| 99a9aa14af | |||
| 09892709ec | |||
| 3ac6b02e56 | |||
| 8fca09d853 | |||
| aaa4d88a2f | |||
| 3bf75eb990 | |||
| 54c36c68ad | |||
| caab80f346 | |||
| dfa2b5fa83 | |||
| 2ac70d5448 | |||
| 2a246744db | |||
| b4f7071990 | |||
| 903702c719 | |||
| 54b932e9c1 | |||
| 13cfda928d | |||
| b556cd0361 | |||
| 657fdbbf48 | |||
| a65d4f1a69 | |||
| a5cfbf854d | |||
| 385c5f3298 | |||
| 31df45e771 | |||
| 0b8e3c4c90 | |||
| ce479cc5b9 | |||
| bdfa8c7bb1 | |||
| 8300a699a3 | |||
| 729ba7e02a | |||
| b15f571938 | |||
| ead810e666 | |||
| 4b412ae0f3 | |||
| e4efff1824 | |||
| 788512c391 | |||
| 6bf6fadaaa | |||
| 0070a64d5f | |||
| 1398c6040a | |||
| 12ad5ced49 | |||
| 53145f1c5e | |||
| 6867086c4f | |||
| f80411bf21 | |||
| 0a70206b11 | |||
| f40f6520d2 | |||
| b47ef2480c | |||
| cba13ad91a | |||
| 1ba84dcefc | |||
| 0c0aa6e807 | |||
| 3467ea15d9 | |||
| aa63bb745f | |||
| f20681adab | |||
| 4b3d7548bd | |||
| c4b61e16c5 | |||
| 9dcb579d93 | |||
| defafcf996 | |||
| db4c3cbbc8 | |||
| 07dfae8f0e | |||
| 05cc2ee218 | |||
| 034dcf5215 | |||
| c58199385e | |||
| b6b9dc238a | |||
| 9ec682d708 | |||
| 465a366e79 | |||
| 2969227656 | |||
| 9561531c7c | |||
| 8c8326780f | |||
| 0466b1fe05 | |||
| 69a07d77d5 | |||
| a19f228c8e | |||
| bcb3489de4 | |||
| 0fd7548ba4 | |||
| 24183ff581 | |||
| df9c6b5d46 | |||
| 423ef5c4b1 | |||
| b87c0bff3e | |||
| 978b6fdfd1 | |||
| 93e972900f | |||
| f8a1cb6f68 | |||
| 44661de993 | |||
| 48b2e78b41 | |||
| a9f3b548e5 | |||
| 379b40b2fb | |||
| cbedad7178 | |||
| 9283764f42 | |||
| 25cf1808e3 | |||
| 6788487368 | |||
| e406010374 | |||
| 289e39c677 | |||
| 6511ff6280 | |||
| 206dda0761 | |||
| 7a6c1cd085 | |||
| 04e81fcef1 | |||
| 0998845817 | |||
| 922668e2a3 | |||
| 5eb6be1415 | |||
| a3b2473eed | |||
| 043980042d | |||
| 58118bd6bb | |||
| 86d51d6dfb |
@@ -0,0 +1,103 @@
|
||||
# Details
|
||||
|
||||
Date : 2024-08-27 08:23:38
|
||||
|
||||
Directory /home/yves/Documents/code/go/meow/meowlib
|
||||
|
||||
Total : 88 files, 10488 codes, 836 comments, 1073 blanks, all 12397 lines
|
||||
|
||||
[Summary](results.md) / Details / [Diff Summary](diff.md) / [Diff Details](diff-details.md)
|
||||
|
||||
## Files
|
||||
| filename | language | code | comment | blank | total |
|
||||
| :--- | :--- | ---: | ---: | ---: | ---: |
|
||||
| [.drone.yml](/.drone.yml) | YAML | 9 | 0 | 3 | 12 |
|
||||
| [README.md](/README.md) | Markdown | 21 | 0 | 8 | 29 |
|
||||
| [asymcrypt.go](/asymcrypt.go) | Go | 237 | 36 | 41 | 314 |
|
||||
| [asymcrypt_test.go](/asymcrypt_test.go) | Go | 101 | 65 | 17 | 183 |
|
||||
| [buffer.go](/buffer.go) | Go | 62 | 0 | 5 | 67 |
|
||||
| [buffer_test.go](/buffer_test.go) | Go | 20 | 0 | 5 | 25 |
|
||||
| [clean.sh](/clean.sh) | Shell Script | 9 | 1 | 1 | 11 |
|
||||
| [client/avatar.go](/client/avatar.go) | Go | 6 | 0 | 3 | 9 |
|
||||
| [client/config.go](/client/config.go) | Go | 99 | 7 | 15 | 121 |
|
||||
| [client/config_test.go](/client/config_test.go) | Go | 17 | 0 | 4 | 21 |
|
||||
| [client/dbmessage.go](/client/dbmessage.go) | Go | 46 | 0 | 6 | 52 |
|
||||
| [client/helpers/backgroundHelper.go](/client/helpers/backgroundHelper.go) | Go | 140 | 28 | 23 | 191 |
|
||||
| [client/helpers/call.go](/client/helpers/call.go) | Go | 42 | 3 | 11 | 56 |
|
||||
| [client/helpers/contactHelper.go](/client/helpers/contactHelper.go) | Go | 1 | 0 | 1 | 2 |
|
||||
| [client/helpers/invitationAnswerHelper.go](/client/helpers/invitationAnswerHelper.go) | Go | 106 | 27 | 23 | 156 |
|
||||
| [client/helpers/invitationCheckHelper.go](/client/helpers/invitationCheckHelper.go) | Go | 52 | 65 | 10 | 127 |
|
||||
| [client/helpers/invitationCreateHelper.go](/client/helpers/invitationCreateHelper.go) | Go | 77 | 50 | 17 | 144 |
|
||||
| [client/helpers/invitationFinalizeHelper.go](/client/helpers/invitationFinalizeHelper.go) | Go | 34 | 12 | 8 | 54 |
|
||||
| [client/helpers/logger.go](/client/helpers/logger.go) | Go | 8 | 1 | 4 | 13 |
|
||||
| [client/helpers/messageHelper.go](/client/helpers/messageHelper.go) | Go | 66 | 9 | 12 | 87 |
|
||||
| [client/helpers/networkHelper.go](/client/helpers/networkHelper.go) | Go | 25 | 2 | 4 | 31 |
|
||||
| [client/helpers/serverHelper.go](/client/helpers/serverHelper.go) | Go | 1 | 0 | 1 | 2 |
|
||||
| [client/helpers/storageHelper.go](/client/helpers/storageHelper.go) | Go | 13 | 0 | 3 | 16 |
|
||||
| [client/identity.go](/client/identity.go) | Go | 254 | 81 | 27 | 362 |
|
||||
| [client/identity_test.go](/client/identity_test.go) | Go | 91 | 27 | 11 | 129 |
|
||||
| [client/internalusermessage.go](/client/internalusermessage.go) | Go | 41 | 5 | 5 | 51 |
|
||||
| [client/logger.go](/client/logger.go) | Go | 8 | 1 | 4 | 13 |
|
||||
| [client/matriochka.go](/client/matriochka.go) | Go | 44 | 4 | 4 | 52 |
|
||||
| [client/messagestorage.go](/client/messagestorage.go) | Go | 314 | 30 | 23 | 367 |
|
||||
| [client/messagestorage_test.go](/client/messagestorage_test.go) | Go | 74 | 5 | 5 | 84 |
|
||||
| [client/peer.go](/client/peer.go) | Go | 249 | 42 | 35 | 326 |
|
||||
| [client/peer_test.go](/client/peer_test.go) | Go | 26 | 0 | 4 | 30 |
|
||||
| [client/peerlist.go](/client/peerlist.go) | Go | 34 | 11 | 7 | 52 |
|
||||
| [client/peerstorage.go](/client/peerstorage.go) | Go | 209 | 20 | 20 | 249 |
|
||||
| [client/peerstorage_test.go](/client/peerstorage_test.go) | Go | 50 | 1 | 10 | 61 |
|
||||
| [client/server.go](/client/server.go) | Go | 249 | 33 | 26 | 308 |
|
||||
| [client/serverlist.go](/client/serverlist.go) | Go | 37 | 9 | 7 | 53 |
|
||||
| [client/serverstorage.go](/client/serverstorage.go) | Go | 234 | 17 | 18 | 269 |
|
||||
| [client/serverstorage_test.go](/client/serverstorage_test.go) | Go | 177 | 21 | 27 | 225 |
|
||||
| [contactcard.go](/contactcard.go) | Go | 147 | 7 | 24 | 178 |
|
||||
| [contactcard_test.go](/contactcard_test.go) | Go | 61 | 0 | 5 | 66 |
|
||||
| [crypt.go](/crypt.go) | Go | 5 | 0 | 2 | 7 |
|
||||
| [doc/act_01_send_msg.puml](/doc/act_01_send_msg.puml) | PlantUML | 21 | 0 | 1 | 22 |
|
||||
| [doc/act_02_srv_recv_msg.puml](/doc/act_02_srv_recv_msg.puml) | PlantUML | 21 | 0 | 1 | 22 |
|
||||
| [doc/act_03_srv_proc_msg.puml](/doc/act_03_srv_proc_msg.puml) | PlantUML | 20 | 0 | 0 | 20 |
|
||||
| [doc/architecture.tex](/doc/architecture.tex) | LaTeX | 0 | 0 | 1 | 1 |
|
||||
| [doc/class_messages01.puml](/doc/class_messages01.puml) | PlantUML | 70 | 0 | 14 | 84 |
|
||||
| [doc/docgen.sh](/doc/docgen.sh) | Shell Script | 13 | 1 | 4 | 18 |
|
||||
| [doc/endpoints/company_endpoint.puml](/doc/endpoints/company_endpoint.puml) | PlantUML | 7 | 0 | 0 | 7 |
|
||||
| [doc/endpoints/public_endpoint.puml](/doc/endpoints/public_endpoint.puml) | PlantUML | 10 | 0 | 0 | 10 |
|
||||
| [doc/general_deployment.puml](/doc/general_deployment.puml) | PlantUML | 19 | 0 | 3 | 22 |
|
||||
| [doc/invitation/sq_invitation.puml](/doc/invitation/sq_invitation.puml) | PlantUML | 7 | 0 | 0 | 7 |
|
||||
| [doc/invitation/sq_srvinv01.puml](/doc/invitation/sq_srvinv01.puml) | PlantUML | 12 | 0 | 0 | 12 |
|
||||
| [doc/invitation/sq_srvinv02.puml](/doc/invitation/sq_srvinv02.puml) | PlantUML | 11 | 0 | 0 | 11 |
|
||||
| [doc/invitation/sq_srvinv03.puml](/doc/invitation/sq_srvinv03.puml) | PlantUML | 11 | 0 | 1 | 12 |
|
||||
| [doc/invitation/sq_srvinv04.puml](/doc/invitation/sq_srvinv04.puml) | PlantUML | 8 | 0 | 1 | 9 |
|
||||
| [doc/meow.svg](/doc/meow.svg) | SVG | 2,814 | 1 | 2 | 2,817 |
|
||||
| [doc/meow.tex](/doc/meow.tex) | LaTeX | 175 | 0 | 50 | 225 |
|
||||
| [doc/messaging/sq_msg01.puml](/doc/messaging/sq_msg01.puml) | PlantUML | 9 | 0 | 1 | 10 |
|
||||
| [doc/messaging/wbs_messages_encapsulation.puml](/doc/messaging/wbs_messages_encapsulation.puml) | PlantUML | 13 | 0 | 2 | 15 |
|
||||
| [doc/protocol.tex](/doc/protocol.tex) | LaTeX | 60 | 0 | 24 | 84 |
|
||||
| [doc/server/server_deployment.puml](/doc/server/server_deployment.puml) | PlantUML | 33 | 0 | 3 | 36 |
|
||||
| [doc/server/server_messaging.puml](/doc/server/server_messaging.puml) | PlantUML | 18 | 0 | 6 | 24 |
|
||||
| [doc/server/sq_01_srvmessaging.puml](/doc/server/sq_01_srvmessaging.puml) | PlantUML | 18 | 0 | 2 | 20 |
|
||||
| [doc/server/sq_02_srvmessaging.puml](/doc/server/sq_02_srvmessaging.puml) | PlantUML | 27 | 0 | 3 | 30 |
|
||||
| [doc/usecase01.puml](/doc/usecase01.puml) | PlantUML | 9 | 0 | 7 | 16 |
|
||||
| [doc/usecase02.puml](/doc/usecase02.puml) | PlantUML | 10 | 0 | 6 | 16 |
|
||||
| [doc/usecase03.puml](/doc/usecase03.puml) | PlantUML | 33 | 0 | 7 | 40 |
|
||||
| [endtoend_test.go](/endtoend_test.go) | Go | 125 | 55 | 18 | 198 |
|
||||
| [go.mod](/go.mod) | Go Module File | 45 | 0 | 5 | 50 |
|
||||
| [go.sum](/go.sum) | Go Checksum File | 344 | 0 | 1 | 345 |
|
||||
| [http.go](/http.go) | Go | 41 | 0 | 4 | 45 |
|
||||
| [lokiwriter.go](/lokiwriter.go) | Go | 74 | 5 | 19 | 98 |
|
||||
| [message.go](/message.go) | Go | 37 | 0 | 7 | 44 |
|
||||
| [messages.pb.go](/messages.pb.go) | Go | 2,058 | 45 | 265 | 2,368 |
|
||||
| [pb/messages.proto](/pb/messages.proto) | Protocol Buffers | 194 | 0 | 40 | 234 |
|
||||
| [pb/messages.py](/pb/messages.py) | Python | 11 | 1 | 5 | 17 |
|
||||
| [pb/protogen.bat](/pb/protogen.bat) | Batch | 4 | 0 | 1 | 5 |
|
||||
| [pb/protogen.sh](/pb/protogen.sh) | Shell Script | 12 | 1 | 1 | 14 |
|
||||
| [proto_test.go](/proto_test.go) | Go | 33 | 0 | 5 | 38 |
|
||||
| [server/identity.go](/server/identity.go) | Go | 135 | 15 | 20 | 170 |
|
||||
| [server/invitation.go](/server/invitation.go) | Go | 62 | 1 | 8 | 71 |
|
||||
| [server/logger.go](/server/logger.go) | Go | 8 | 1 | 4 | 13 |
|
||||
| [server/router.go](/server/router.go) | Go | 271 | 54 | 21 | 346 |
|
||||
| [server/videoserver.go](/server/videoserver.go) | Go | 35 | 0 | 7 | 42 |
|
||||
| [servercard.go](/servercard.go) | Go | 10 | 0 | 3 | 13 |
|
||||
| [symcrypt.go](/symcrypt.go) | Go | 26 | 36 | 7 | 69 |
|
||||
| [symcrypt_test.go](/symcrypt_test.go) | Go | 18 | 0 | 4 | 22 |
|
||||
|
||||
[Summary](results.md) / Details / [Diff Summary](diff.md) / [Diff Details](diff-details.md)
|
||||
@@ -0,0 +1,15 @@
|
||||
# Diff Details
|
||||
|
||||
Date : 2024-08-27 08:23:38
|
||||
|
||||
Directory /home/yves/Documents/code/go/meow/meowlib
|
||||
|
||||
Total : 0 files, 0 codes, 0 comments, 0 blanks, all 0 lines
|
||||
|
||||
[Summary](results.md) / [Details](details.md) / [Diff Summary](diff.md) / Diff Details
|
||||
|
||||
## Files
|
||||
| filename | language | code | comment | blank | total |
|
||||
| :--- | :--- | ---: | ---: | ---: | ---: |
|
||||
|
||||
[Summary](results.md) / [Details](details.md) / [Diff Summary](diff.md) / Diff Details
|
||||
@@ -0,0 +1,2 @@
|
||||
"filename", "language", "", "comment", "blank", "total"
|
||||
"Total", "-", , 0, 0, 0
|
||||
|
@@ -0,0 +1,19 @@
|
||||
# Diff Summary
|
||||
|
||||
Date : 2024-08-27 08:23:38
|
||||
|
||||
Directory /home/yves/Documents/code/go/meow/meowlib
|
||||
|
||||
Total : 0 files, 0 codes, 0 comments, 0 blanks, all 0 lines
|
||||
|
||||
[Summary](results.md) / [Details](details.md) / Diff Summary / [Diff Details](diff-details.md)
|
||||
|
||||
## Languages
|
||||
| language | files | code | comment | blank | total |
|
||||
| :--- | ---: | ---: | ---: | ---: | ---: |
|
||||
|
||||
## Directories
|
||||
| path | files | code | comment | blank | total |
|
||||
| :--- | ---: | ---: | ---: | ---: | ---: |
|
||||
|
||||
[Summary](results.md) / [Details](details.md) / Diff Summary / [Diff Details](diff-details.md)
|
||||
@@ -0,0 +1,22 @@
|
||||
Date : 2024-08-27 08:23:38
|
||||
Directory : /home/yves/Documents/code/go/meow/meowlib
|
||||
Total : 0 files, 0 codes, 0 comments, 0 blanks, all 0 lines
|
||||
|
||||
Languages
|
||||
+----------+------------+------------+------------+------------+------------+
|
||||
| language | files | code | comment | blank | total |
|
||||
+----------+------------+------------+------------+------------+------------+
|
||||
+----------+------------+------------+------------+------------+------------+
|
||||
|
||||
Directories
|
||||
+------+------------+------------+------------+------------+------------+
|
||||
| path | files | code | comment | blank | total |
|
||||
+------+------------+------------+------------+------------+------------+
|
||||
+------+------------+------------+------------+------------+------------+
|
||||
|
||||
Files
|
||||
+----------+----------+------------+------------+------------+------------+
|
||||
| filename | language | code | comment | blank | total |
|
||||
+----------+----------+------------+------------+------------+------------+
|
||||
| Total | | 0 | 0 | 0 | 0 |
|
||||
+----------+----------+------------+------------+------------+------------+
|
||||
@@ -0,0 +1,90 @@
|
||||
"filename", "language", "Go", "Go Module File", "Markdown", "YAML", "Go Checksum File", "Shell Script", "Batch", "Protocol Buffers", "Python", "LaTeX", "PlantUML", "SVG", "comment", "blank", "total"
|
||||
"/home/yves/Documents/code/go/meow/meowlib/.drone.yml", "YAML", 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 12
|
||||
"/home/yves/Documents/code/go/meow/meowlib/README.md", "Markdown", 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 29
|
||||
"/home/yves/Documents/code/go/meow/meowlib/asymcrypt.go", "Go", 237, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 36, 41, 314
|
||||
"/home/yves/Documents/code/go/meow/meowlib/asymcrypt_test.go", "Go", 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 17, 183
|
||||
"/home/yves/Documents/code/go/meow/meowlib/buffer.go", "Go", 62, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 67
|
||||
"/home/yves/Documents/code/go/meow/meowlib/buffer_test.go", "Go", 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 25
|
||||
"/home/yves/Documents/code/go/meow/meowlib/clean.sh", "Shell Script", 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 1, 1, 11
|
||||
"/home/yves/Documents/code/go/meow/meowlib/client/avatar.go", "Go", 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 9
|
||||
"/home/yves/Documents/code/go/meow/meowlib/client/config.go", "Go", 99, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 15, 121
|
||||
"/home/yves/Documents/code/go/meow/meowlib/client/config_test.go", "Go", 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 21
|
||||
"/home/yves/Documents/code/go/meow/meowlib/client/dbmessage.go", "Go", 46, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 52
|
||||
"/home/yves/Documents/code/go/meow/meowlib/client/helpers/backgroundHelper.go", "Go", 140, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 23, 191
|
||||
"/home/yves/Documents/code/go/meow/meowlib/client/helpers/call.go", "Go", 42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 11, 56
|
||||
"/home/yves/Documents/code/go/meow/meowlib/client/helpers/contactHelper.go", "Go", 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2
|
||||
"/home/yves/Documents/code/go/meow/meowlib/client/helpers/invitationAnswerHelper.go", "Go", 106, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, 23, 156
|
||||
"/home/yves/Documents/code/go/meow/meowlib/client/helpers/invitationCheckHelper.go", "Go", 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 10, 127
|
||||
"/home/yves/Documents/code/go/meow/meowlib/client/helpers/invitationCreateHelper.go", "Go", 77, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 50, 17, 144
|
||||
"/home/yves/Documents/code/go/meow/meowlib/client/helpers/invitationFinalizeHelper.go", "Go", 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 8, 54
|
||||
"/home/yves/Documents/code/go/meow/meowlib/client/helpers/logger.go", "Go", 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 4, 13
|
||||
"/home/yves/Documents/code/go/meow/meowlib/client/helpers/messageHelper.go", "Go", 66, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 12, 87
|
||||
"/home/yves/Documents/code/go/meow/meowlib/client/helpers/networkHelper.go", "Go", 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 4, 31
|
||||
"/home/yves/Documents/code/go/meow/meowlib/client/helpers/serverHelper.go", "Go", 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2
|
||||
"/home/yves/Documents/code/go/meow/meowlib/client/helpers/storageHelper.go", "Go", 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 16
|
||||
"/home/yves/Documents/code/go/meow/meowlib/client/identity.go", "Go", 254, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 81, 27, 362
|
||||
"/home/yves/Documents/code/go/meow/meowlib/client/identity_test.go", "Go", 91, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, 11, 129
|
||||
"/home/yves/Documents/code/go/meow/meowlib/client/internalusermessage.go", "Go", 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 51
|
||||
"/home/yves/Documents/code/go/meow/meowlib/client/logger.go", "Go", 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 4, 13
|
||||
"/home/yves/Documents/code/go/meow/meowlib/client/matriochka.go", "Go", 44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 52
|
||||
"/home/yves/Documents/code/go/meow/meowlib/client/messagestorage.go", "Go", 314, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 30, 23, 367
|
||||
"/home/yves/Documents/code/go/meow/meowlib/client/messagestorage_test.go", "Go", 74, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 84
|
||||
"/home/yves/Documents/code/go/meow/meowlib/client/peer.go", "Go", 249, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 42, 35, 326
|
||||
"/home/yves/Documents/code/go/meow/meowlib/client/peer_test.go", "Go", 26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 30
|
||||
"/home/yves/Documents/code/go/meow/meowlib/client/peerlist.go", "Go", 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 7, 52
|
||||
"/home/yves/Documents/code/go/meow/meowlib/client/peerstorage.go", "Go", 209, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 20, 249
|
||||
"/home/yves/Documents/code/go/meow/meowlib/client/peerstorage_test.go", "Go", 50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 10, 61
|
||||
"/home/yves/Documents/code/go/meow/meowlib/client/server.go", "Go", 249, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 33, 26, 308
|
||||
"/home/yves/Documents/code/go/meow/meowlib/client/serverlist.go", "Go", 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 7, 53
|
||||
"/home/yves/Documents/code/go/meow/meowlib/client/serverstorage.go", "Go", 234, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 18, 269
|
||||
"/home/yves/Documents/code/go/meow/meowlib/client/serverstorage_test.go", "Go", 177, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 27, 225
|
||||
"/home/yves/Documents/code/go/meow/meowlib/contactcard.go", "Go", 147, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 24, 178
|
||||
"/home/yves/Documents/code/go/meow/meowlib/contactcard_test.go", "Go", 61, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 66
|
||||
"/home/yves/Documents/code/go/meow/meowlib/crypt.go", "Go", 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 7
|
||||
"/home/yves/Documents/code/go/meow/meowlib/doc/act_01_send_msg.puml", "PlantUML", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 1, 22
|
||||
"/home/yves/Documents/code/go/meow/meowlib/doc/act_02_srv_recv_msg.puml", "PlantUML", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 1, 22
|
||||
"/home/yves/Documents/code/go/meow/meowlib/doc/act_03_srv_proc_msg.puml", "PlantUML", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 0, 0, 0, 20
|
||||
"/home/yves/Documents/code/go/meow/meowlib/doc/architecture.tex", "LaTeX", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1
|
||||
"/home/yves/Documents/code/go/meow/meowlib/doc/class_messages01.puml", "PlantUML", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 70, 0, 0, 14, 84
|
||||
"/home/yves/Documents/code/go/meow/meowlib/doc/docgen.sh", "Shell Script", 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 1, 4, 18
|
||||
"/home/yves/Documents/code/go/meow/meowlib/doc/endpoints/company_endpoint.puml", "PlantUML", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 7
|
||||
"/home/yves/Documents/code/go/meow/meowlib/doc/endpoints/public_endpoint.puml", "PlantUML", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 10
|
||||
"/home/yves/Documents/code/go/meow/meowlib/doc/general_deployment.puml", "PlantUML", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 0, 0, 3, 22
|
||||
"/home/yves/Documents/code/go/meow/meowlib/doc/invitation/sq_invitation.puml", "PlantUML", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 7
|
||||
"/home/yves/Documents/code/go/meow/meowlib/doc/invitation/sq_srvinv01.puml", "PlantUML", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 12
|
||||
"/home/yves/Documents/code/go/meow/meowlib/doc/invitation/sq_srvinv02.puml", "PlantUML", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 11
|
||||
"/home/yves/Documents/code/go/meow/meowlib/doc/invitation/sq_srvinv03.puml", "PlantUML", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 1, 12
|
||||
"/home/yves/Documents/code/go/meow/meowlib/doc/invitation/sq_srvinv04.puml", "PlantUML", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 1, 9
|
||||
"/home/yves/Documents/code/go/meow/meowlib/doc/meow.svg", "SVG", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2814, 1, 2, 2817
|
||||
"/home/yves/Documents/code/go/meow/meowlib/doc/meow.tex", "LaTeX", 0, 0, 0, 0, 0, 0, 0, 0, 0, 175, 0, 0, 0, 50, 225
|
||||
"/home/yves/Documents/code/go/meow/meowlib/doc/messaging/sq_msg01.puml", "PlantUML", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 1, 10
|
||||
"/home/yves/Documents/code/go/meow/meowlib/doc/messaging/wbs_messages_encapsulation.puml", "PlantUML", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 2, 15
|
||||
"/home/yves/Documents/code/go/meow/meowlib/doc/protocol.tex", "LaTeX", 0, 0, 0, 0, 0, 0, 0, 0, 0, 60, 0, 0, 0, 24, 84
|
||||
"/home/yves/Documents/code/go/meow/meowlib/doc/server/server_deployment.puml", "PlantUML", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 33, 0, 0, 3, 36
|
||||
"/home/yves/Documents/code/go/meow/meowlib/doc/server/server_messaging.puml", "PlantUML", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 6, 24
|
||||
"/home/yves/Documents/code/go/meow/meowlib/doc/server/sq_01_srvmessaging.puml", "PlantUML", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 2, 20
|
||||
"/home/yves/Documents/code/go/meow/meowlib/doc/server/sq_02_srvmessaging.puml", "PlantUML", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, 0, 0, 3, 30
|
||||
"/home/yves/Documents/code/go/meow/meowlib/doc/usecase01.puml", "PlantUML", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 7, 16
|
||||
"/home/yves/Documents/code/go/meow/meowlib/doc/usecase02.puml", "PlantUML", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 6, 16
|
||||
"/home/yves/Documents/code/go/meow/meowlib/doc/usecase03.puml", "PlantUML", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 33, 0, 0, 7, 40
|
||||
"/home/yves/Documents/code/go/meow/meowlib/endtoend_test.go", "Go", 125, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 55, 18, 198
|
||||
"/home/yves/Documents/code/go/meow/meowlib/go.mod", "Go Module File", 0, 45, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 50
|
||||
"/home/yves/Documents/code/go/meow/meowlib/go.sum", "Go Checksum File", 0, 0, 0, 0, 344, 0, 0, 0, 0, 0, 0, 0, 0, 1, 345
|
||||
"/home/yves/Documents/code/go/meow/meowlib/http.go", "Go", 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 45
|
||||
"/home/yves/Documents/code/go/meow/meowlib/lokiwriter.go", "Go", 74, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 19, 98
|
||||
"/home/yves/Documents/code/go/meow/meowlib/message.go", "Go", 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 44
|
||||
"/home/yves/Documents/code/go/meow/meowlib/messages.pb.go", "Go", 2058, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 45, 265, 2368
|
||||
"/home/yves/Documents/code/go/meow/meowlib/pb/messages.proto", "Protocol Buffers", 0, 0, 0, 0, 0, 0, 0, 194, 0, 0, 0, 0, 0, 40, 234
|
||||
"/home/yves/Documents/code/go/meow/meowlib/pb/messages.py", "Python", 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 1, 5, 17
|
||||
"/home/yves/Documents/code/go/meow/meowlib/pb/protogen.bat", "Batch", 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 1, 5
|
||||
"/home/yves/Documents/code/go/meow/meowlib/pb/protogen.sh", "Shell Script", 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 1, 1, 14
|
||||
"/home/yves/Documents/code/go/meow/meowlib/proto_test.go", "Go", 33, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 38
|
||||
"/home/yves/Documents/code/go/meow/meowlib/server/identity.go", "Go", 135, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 20, 170
|
||||
"/home/yves/Documents/code/go/meow/meowlib/server/invitation.go", "Go", 62, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 8, 71
|
||||
"/home/yves/Documents/code/go/meow/meowlib/server/logger.go", "Go", 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 4, 13
|
||||
"/home/yves/Documents/code/go/meow/meowlib/server/router.go", "Go", 271, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 54, 21, 346
|
||||
"/home/yves/Documents/code/go/meow/meowlib/server/videoserver.go", "Go", 35, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 42
|
||||
"/home/yves/Documents/code/go/meow/meowlib/servercard.go", "Go", 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 13
|
||||
"/home/yves/Documents/code/go/meow/meowlib/symcrypt.go", "Go", 26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 36, 7, 69
|
||||
"/home/yves/Documents/code/go/meow/meowlib/symcrypt_test.go", "Go", 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 22
|
||||
"Total", "-", 6390, 45, 21, 9, 344, 34, 4, 194, 11, 235, 387, 2814, 836, 1073, 12397
|
||||
|
@@ -0,0 +1,44 @@
|
||||
# Summary
|
||||
|
||||
Date : 2024-08-27 08:23:38
|
||||
|
||||
Directory /home/yves/Documents/code/go/meow/meowlib
|
||||
|
||||
Total : 88 files, 10488 codes, 836 comments, 1073 blanks, all 12397 lines
|
||||
|
||||
Summary / [Details](details.md) / [Diff Summary](diff.md) / [Diff Details](diff-details.md)
|
||||
|
||||
## Languages
|
||||
| language | files | code | comment | blank | total |
|
||||
| :--- | ---: | ---: | ---: | ---: | ---: |
|
||||
| Go | 53 | 6,390 | 831 | 869 | 8,090 |
|
||||
| SVG | 1 | 2,814 | 1 | 2 | 2,817 |
|
||||
| PlantUML | 21 | 387 | 0 | 58 | 445 |
|
||||
| Go Checksum File | 1 | 344 | 0 | 1 | 345 |
|
||||
| LaTeX | 3 | 235 | 0 | 75 | 310 |
|
||||
| Protocol Buffers | 1 | 194 | 0 | 40 | 234 |
|
||||
| Go Module File | 1 | 45 | 0 | 5 | 50 |
|
||||
| Shell Script | 3 | 34 | 3 | 6 | 43 |
|
||||
| Markdown | 1 | 21 | 0 | 8 | 29 |
|
||||
| Python | 1 | 11 | 1 | 5 | 17 |
|
||||
| YAML | 1 | 9 | 0 | 3 | 12 |
|
||||
| Batch | 1 | 4 | 0 | 1 | 5 |
|
||||
|
||||
## Directories
|
||||
| path | files | code | comment | blank | total |
|
||||
| :--- | ---: | ---: | ---: | ---: | ---: |
|
||||
| . | 88 | 10,488 | 836 | 1,073 | 12,397 |
|
||||
| . (Files) | 21 | 3,483 | 250 | 449 | 4,182 |
|
||||
| client | 32 | 2,824 | 511 | 378 | 3,713 |
|
||||
| client (Files) | 20 | 2,259 | 314 | 261 | 2,834 |
|
||||
| client/helpers | 12 | 565 | 197 | 117 | 879 |
|
||||
| doc | 26 | 3,449 | 2 | 139 | 3,590 |
|
||||
| doc (Files) | 13 | 3,265 | 2 | 120 | 3,387 |
|
||||
| doc/endpoints | 2 | 17 | 0 | 0 | 17 |
|
||||
| doc/invitation | 5 | 49 | 0 | 2 | 51 |
|
||||
| doc/messaging | 2 | 22 | 0 | 3 | 25 |
|
||||
| doc/server | 4 | 96 | 0 | 14 | 110 |
|
||||
| pb | 4 | 221 | 2 | 47 | 270 |
|
||||
| server | 5 | 511 | 71 | 60 | 642 |
|
||||
|
||||
Summary / [Details](details.md) / [Diff Summary](diff.md) / [Diff Details](diff-details.md)
|
||||
@@ -0,0 +1,135 @@
|
||||
Date : 2024-08-27 08:23:38
|
||||
Directory : /home/yves/Documents/code/go/meow/meowlib
|
||||
Total : 88 files, 10488 codes, 836 comments, 1073 blanks, all 12397 lines
|
||||
|
||||
Languages
|
||||
+------------------+------------+------------+------------+------------+------------+
|
||||
| language | files | code | comment | blank | total |
|
||||
+------------------+------------+------------+------------+------------+------------+
|
||||
| Go | 53 | 6,390 | 831 | 869 | 8,090 |
|
||||
| SVG | 1 | 2,814 | 1 | 2 | 2,817 |
|
||||
| PlantUML | 21 | 387 | 0 | 58 | 445 |
|
||||
| Go Checksum File | 1 | 344 | 0 | 1 | 345 |
|
||||
| LaTeX | 3 | 235 | 0 | 75 | 310 |
|
||||
| Protocol Buffers | 1 | 194 | 0 | 40 | 234 |
|
||||
| Go Module File | 1 | 45 | 0 | 5 | 50 |
|
||||
| Shell Script | 3 | 34 | 3 | 6 | 43 |
|
||||
| Markdown | 1 | 21 | 0 | 8 | 29 |
|
||||
| Python | 1 | 11 | 1 | 5 | 17 |
|
||||
| YAML | 1 | 9 | 0 | 3 | 12 |
|
||||
| Batch | 1 | 4 | 0 | 1 | 5 |
|
||||
+------------------+------------+------------+------------+------------+------------+
|
||||
|
||||
Directories
|
||||
+-----------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
|
||||
| path | files | code | comment | blank | total |
|
||||
+-----------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
|
||||
| . | 88 | 10,488 | 836 | 1,073 | 12,397 |
|
||||
| . (Files) | 21 | 3,483 | 250 | 449 | 4,182 |
|
||||
| client | 32 | 2,824 | 511 | 378 | 3,713 |
|
||||
| client (Files) | 20 | 2,259 | 314 | 261 | 2,834 |
|
||||
| client/helpers | 12 | 565 | 197 | 117 | 879 |
|
||||
| doc | 26 | 3,449 | 2 | 139 | 3,590 |
|
||||
| doc (Files) | 13 | 3,265 | 2 | 120 | 3,387 |
|
||||
| doc/endpoints | 2 | 17 | 0 | 0 | 17 |
|
||||
| doc/invitation | 5 | 49 | 0 | 2 | 51 |
|
||||
| doc/messaging | 2 | 22 | 0 | 3 | 25 |
|
||||
| doc/server | 4 | 96 | 0 | 14 | 110 |
|
||||
| pb | 4 | 221 | 2 | 47 | 270 |
|
||||
| server | 5 | 511 | 71 | 60 | 642 |
|
||||
+-----------------------------------------------------------------------------------------+------------+------------+------------+------------+------------+
|
||||
|
||||
Files
|
||||
+-----------------------------------------------------------------------------------------+------------------+------------+------------+------------+------------+
|
||||
| filename | language | code | comment | blank | total |
|
||||
+-----------------------------------------------------------------------------------------+------------------+------------+------------+------------+------------+
|
||||
| /home/yves/Documents/code/go/meow/meowlib/.drone.yml | YAML | 9 | 0 | 3 | 12 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/README.md | Markdown | 21 | 0 | 8 | 29 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/asymcrypt.go | Go | 237 | 36 | 41 | 314 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/asymcrypt_test.go | Go | 101 | 65 | 17 | 183 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/buffer.go | Go | 62 | 0 | 5 | 67 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/buffer_test.go | Go | 20 | 0 | 5 | 25 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/clean.sh | Shell Script | 9 | 1 | 1 | 11 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/client/avatar.go | Go | 6 | 0 | 3 | 9 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/client/config.go | Go | 99 | 7 | 15 | 121 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/client/config_test.go | Go | 17 | 0 | 4 | 21 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/client/dbmessage.go | Go | 46 | 0 | 6 | 52 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/client/helpers/backgroundHelper.go | Go | 140 | 28 | 23 | 191 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/client/helpers/call.go | Go | 42 | 3 | 11 | 56 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/client/helpers/contactHelper.go | Go | 1 | 0 | 1 | 2 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/client/helpers/invitationAnswerHelper.go | Go | 106 | 27 | 23 | 156 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/client/helpers/invitationCheckHelper.go | Go | 52 | 65 | 10 | 127 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/client/helpers/invitationCreateHelper.go | Go | 77 | 50 | 17 | 144 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/client/helpers/invitationFinalizeHelper.go | Go | 34 | 12 | 8 | 54 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/client/helpers/logger.go | Go | 8 | 1 | 4 | 13 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/client/helpers/messageHelper.go | Go | 66 | 9 | 12 | 87 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/client/helpers/networkHelper.go | Go | 25 | 2 | 4 | 31 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/client/helpers/serverHelper.go | Go | 1 | 0 | 1 | 2 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/client/helpers/storageHelper.go | Go | 13 | 0 | 3 | 16 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/client/identity.go | Go | 254 | 81 | 27 | 362 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/client/identity_test.go | Go | 91 | 27 | 11 | 129 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/client/internalusermessage.go | Go | 41 | 5 | 5 | 51 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/client/logger.go | Go | 8 | 1 | 4 | 13 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/client/matriochka.go | Go | 44 | 4 | 4 | 52 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/client/messagestorage.go | Go | 314 | 30 | 23 | 367 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/client/messagestorage_test.go | Go | 74 | 5 | 5 | 84 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/client/peer.go | Go | 249 | 42 | 35 | 326 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/client/peer_test.go | Go | 26 | 0 | 4 | 30 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/client/peerlist.go | Go | 34 | 11 | 7 | 52 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/client/peerstorage.go | Go | 209 | 20 | 20 | 249 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/client/peerstorage_test.go | Go | 50 | 1 | 10 | 61 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/client/server.go | Go | 249 | 33 | 26 | 308 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/client/serverlist.go | Go | 37 | 9 | 7 | 53 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/client/serverstorage.go | Go | 234 | 17 | 18 | 269 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/client/serverstorage_test.go | Go | 177 | 21 | 27 | 225 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/contactcard.go | Go | 147 | 7 | 24 | 178 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/contactcard_test.go | Go | 61 | 0 | 5 | 66 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/crypt.go | Go | 5 | 0 | 2 | 7 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/doc/act_01_send_msg.puml | PlantUML | 21 | 0 | 1 | 22 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/doc/act_02_srv_recv_msg.puml | PlantUML | 21 | 0 | 1 | 22 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/doc/act_03_srv_proc_msg.puml | PlantUML | 20 | 0 | 0 | 20 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/doc/architecture.tex | LaTeX | 0 | 0 | 1 | 1 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/doc/class_messages01.puml | PlantUML | 70 | 0 | 14 | 84 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/doc/docgen.sh | Shell Script | 13 | 1 | 4 | 18 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/doc/endpoints/company_endpoint.puml | PlantUML | 7 | 0 | 0 | 7 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/doc/endpoints/public_endpoint.puml | PlantUML | 10 | 0 | 0 | 10 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/doc/general_deployment.puml | PlantUML | 19 | 0 | 3 | 22 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/doc/invitation/sq_invitation.puml | PlantUML | 7 | 0 | 0 | 7 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/doc/invitation/sq_srvinv01.puml | PlantUML | 12 | 0 | 0 | 12 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/doc/invitation/sq_srvinv02.puml | PlantUML | 11 | 0 | 0 | 11 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/doc/invitation/sq_srvinv03.puml | PlantUML | 11 | 0 | 1 | 12 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/doc/invitation/sq_srvinv04.puml | PlantUML | 8 | 0 | 1 | 9 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/doc/meow.svg | SVG | 2,814 | 1 | 2 | 2,817 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/doc/meow.tex | LaTeX | 175 | 0 | 50 | 225 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/doc/messaging/sq_msg01.puml | PlantUML | 9 | 0 | 1 | 10 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/doc/messaging/wbs_messages_encapsulation.puml | PlantUML | 13 | 0 | 2 | 15 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/doc/protocol.tex | LaTeX | 60 | 0 | 24 | 84 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/doc/server/server_deployment.puml | PlantUML | 33 | 0 | 3 | 36 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/doc/server/server_messaging.puml | PlantUML | 18 | 0 | 6 | 24 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/doc/server/sq_01_srvmessaging.puml | PlantUML | 18 | 0 | 2 | 20 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/doc/server/sq_02_srvmessaging.puml | PlantUML | 27 | 0 | 3 | 30 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/doc/usecase01.puml | PlantUML | 9 | 0 | 7 | 16 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/doc/usecase02.puml | PlantUML | 10 | 0 | 6 | 16 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/doc/usecase03.puml | PlantUML | 33 | 0 | 7 | 40 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/endtoend_test.go | Go | 125 | 55 | 18 | 198 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/go.mod | Go Module File | 45 | 0 | 5 | 50 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/go.sum | Go Checksum File | 344 | 0 | 1 | 345 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/http.go | Go | 41 | 0 | 4 | 45 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/lokiwriter.go | Go | 74 | 5 | 19 | 98 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/message.go | Go | 37 | 0 | 7 | 44 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/messages.pb.go | Go | 2,058 | 45 | 265 | 2,368 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/pb/messages.proto | Protocol Buffers | 194 | 0 | 40 | 234 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/pb/messages.py | Python | 11 | 1 | 5 | 17 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/pb/protogen.bat | Batch | 4 | 0 | 1 | 5 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/pb/protogen.sh | Shell Script | 12 | 1 | 1 | 14 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/proto_test.go | Go | 33 | 0 | 5 | 38 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/server/identity.go | Go | 135 | 15 | 20 | 170 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/server/invitation.go | Go | 62 | 1 | 8 | 71 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/server/logger.go | Go | 8 | 1 | 4 | 13 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/server/router.go | Go | 271 | 54 | 21 | 346 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/server/videoserver.go | Go | 35 | 0 | 7 | 42 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/servercard.go | Go | 10 | 0 | 3 | 13 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/symcrypt.go | Go | 26 | 36 | 7 | 69 |
|
||||
| /home/yves/Documents/code/go/meow/meowlib/symcrypt_test.go | Go | 18 | 0 | 4 | 22 |
|
||||
| Total | | 10,488 | 836 | 1,073 | 12,397 |
|
||||
+-----------------------------------------------------------------------------------------+------------------+------------+------------+------------+------------+
|
||||
@@ -15,3 +15,6 @@ client/test.cfg
|
||||
.VSCodeCouter/
|
||||
meowlib-sources.jar
|
||||
meowlib.aar
|
||||
client/test.db
|
||||
CLAUDE.md
|
||||
CODE_REVIEW.md
|
||||
|
||||
@@ -19,3 +19,10 @@ run the shell scripts
|
||||
# Tests
|
||||
|
||||
|
||||
# Design notes
|
||||
Config is written as a json file
|
||||
Identity is stored as an encrypted json file
|
||||
Message servers (messaging and my contact's messaging) are stored in an encrypted badger db with server url as key
|
||||
Received servers are stored in a sqlite db for selective searches, with storage limits
|
||||
Messages are stored in several badger? or sqlite? db per user with send/receive time as key
|
||||
|
||||
|
||||
+86
-82
@@ -2,12 +2,11 @@ package meowlib
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/gopenpgp/v2/helper"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type KeyPair struct {
|
||||
@@ -18,47 +17,50 @@ type KeyPair struct {
|
||||
|
||||
type KeysArray []KeyPair
|
||||
|
||||
func NewKeyPair() KeyPair {
|
||||
func NewKeyPair() (*KeyPair, error) { // Return error!
|
||||
var kp KeyPair
|
||||
keys, err := crypto.GenerateKey("name", "mail", "x25519", 0)
|
||||
if err != nil {
|
||||
log.Error().Msg("Key generation failed")
|
||||
return nil, fmt.Errorf("key generation failed: %w", err)
|
||||
}
|
||||
pubKey, err := keys.ToPublic()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gopenpgp: unable to extract public key: %w", err)
|
||||
}
|
||||
pubBytes, err := pubKey.Serialize()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gopenpgp: unable to serialize public key: %w", err)
|
||||
}
|
||||
privBytes, err := keys.Serialize()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to serialize private key: %w", err)
|
||||
}
|
||||
kp.Public = base64.StdEncoding.EncodeToString(pubBytes)
|
||||
kp.Private = base64.StdEncoding.EncodeToString(privBytes)
|
||||
kp.Generated = time.Now()
|
||||
pub, err := keys.GetArmoredPublicKey()
|
||||
if err != nil {
|
||||
log.Error().Msg("Public key extraction failed")
|
||||
}
|
||||
kp.Public = base64.StdEncoding.EncodeToString([]byte(pub))
|
||||
priv, err := keys.Armor()
|
||||
if err != nil {
|
||||
log.Error().Msg("Private key extraction failed")
|
||||
}
|
||||
kp.Private = base64.StdEncoding.EncodeToString([]byte(priv))
|
||||
return kp
|
||||
return &kp, nil
|
||||
}
|
||||
|
||||
func (Kp *KeyPair) GetCryptoKeyObject() *crypto.Key {
|
||||
func (Kp *KeyPair) GetCryptoKeyObject() (*crypto.Key, error) {
|
||||
priv, err := base64.StdEncoding.DecodeString(Kp.Private)
|
||||
if err != nil {
|
||||
log.Error().Msg("Create key from armoured b64 failed")
|
||||
return nil, fmt.Errorf("failed to decode private key: %w", err)
|
||||
}
|
||||
key, err := crypto.NewKeyFromArmored(string(priv))
|
||||
key, err := crypto.NewKey(priv)
|
||||
if err != nil {
|
||||
log.Error().Msg("Create key from armoured failed")
|
||||
return nil, fmt.Errorf("create key from binary failed: %w", err)
|
||||
}
|
||||
return key
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func AsymEncrypt(publicKey string, data []byte) ([]byte, error) {
|
||||
pub, err := base64.StdEncoding.DecodeString(publicKey)
|
||||
if err != nil {
|
||||
log.Error().Msg("Message encryption b64 failed")
|
||||
return nil, fmt.Errorf("Message encryption b64 failed: %w", err)
|
||||
}
|
||||
ciphertext, err := encryptMessage(string(pub), crypto.NewPlainMessage(data))
|
||||
ciphertext, err := encryptMessage(pub, crypto.NewPlainMessage(data))
|
||||
if err != nil {
|
||||
log.Error().Msg("Message encryption failed")
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("Message encryption failed: %w", err)
|
||||
}
|
||||
|
||||
return ciphertext.GetBinary(), err
|
||||
@@ -67,11 +69,11 @@ func AsymEncrypt(publicKey string, data []byte) ([]byte, error) {
|
||||
func AsymDecrypt(PrivateKey string, data []byte) ([]byte, error) {
|
||||
priv, err := base64.StdEncoding.DecodeString(PrivateKey)
|
||||
if err != nil {
|
||||
log.Error().Msg("Message decryption b64 failed")
|
||||
return nil, fmt.Errorf("Message decryption b64 failed: %w", err)
|
||||
}
|
||||
decrypted, err := decryptMessage(string(priv), nil, crypto.NewPGPMessage(data))
|
||||
decrypted, err := decryptMessage(priv, nil, crypto.NewPGPMessage(data))
|
||||
if err != nil {
|
||||
log.Error().Msg("Message decryption failed")
|
||||
return nil, fmt.Errorf("Message decryption failed: %w", err)
|
||||
}
|
||||
return decrypted.GetBinary(), err
|
||||
}
|
||||
@@ -79,11 +81,19 @@ func AsymDecrypt(PrivateKey string, data []byte) ([]byte, error) {
|
||||
func AsymEncryptArmored(PublicKey string, data []byte) ([]byte, error) {
|
||||
pub, err := base64.StdEncoding.DecodeString(PublicKey)
|
||||
if err != nil {
|
||||
log.Error().Msg("Message encryption b64 failed")
|
||||
return nil, fmt.Errorf("Message encryption b64 failed: %w", err)
|
||||
}
|
||||
armor, err := helper.EncryptBinaryMessageArmored(string(pub), data)
|
||||
pubKey, err := crypto.NewKey(pub)
|
||||
if err != nil {
|
||||
log.Error().Msg("Message encryption failed")
|
||||
return nil, fmt.Errorf("Message encryption key parse failed: %w", err)
|
||||
}
|
||||
armoredPub, err := pubKey.GetArmoredPublicKey()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Message encryption key armor failed: %w", err)
|
||||
}
|
||||
armor, err := helper.EncryptBinaryMessageArmored(armoredPub, data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Message encryption failed: %w", err)
|
||||
}
|
||||
return []byte(armor), err
|
||||
}
|
||||
@@ -91,11 +101,19 @@ func AsymEncryptArmored(PublicKey string, data []byte) ([]byte, error) {
|
||||
func AsymDecryptArmored(PrivateKey string, data []byte) ([]byte, error) {
|
||||
priv, err := base64.StdEncoding.DecodeString(PrivateKey)
|
||||
if err != nil {
|
||||
log.Error().Msg("Message decryption b64 failed")
|
||||
return nil, fmt.Errorf("Message decryption b64 failed: %w", err)
|
||||
}
|
||||
decrypted, err := helper.DecryptBinaryMessageArmored(string(priv), nil, string(data))
|
||||
privKey, err := crypto.NewKey(priv)
|
||||
if err != nil {
|
||||
log.Error().Msg("Message decryption failed")
|
||||
return nil, fmt.Errorf("Message decryption key parse failed: %w", err)
|
||||
}
|
||||
armoredPriv, err := privKey.Armor()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Message decryption key armor failed: %w", err)
|
||||
}
|
||||
decrypted, err := helper.DecryptBinaryMessageArmored(armoredPriv, nil, string(data))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Message decryption failed: %w", err)
|
||||
}
|
||||
return []byte(decrypted), err
|
||||
}
|
||||
@@ -136,7 +154,7 @@ func AsymDecryptArmored(PrivateKey string, data []byte) ([]byte, error) {
|
||||
return DecryptedMessage, err
|
||||
}
|
||||
*/
|
||||
func encryptMessage(key string, message *crypto.PlainMessage) (*crypto.PGPMessage, error) {
|
||||
func encryptMessage(key []byte, message *crypto.PlainMessage) (*crypto.PGPMessage, error) {
|
||||
publicKeyRing, err := createPublicKeyRing(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -144,54 +162,54 @@ func encryptMessage(key string, message *crypto.PlainMessage) (*crypto.PGPMessag
|
||||
|
||||
ciphertext, err := publicKeyRing.Encrypt(message, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "gopenpgp: unable to encrypt message")
|
||||
return nil, fmt.Errorf("gopenpgp: unable to encrypt message: %w", err)
|
||||
}
|
||||
|
||||
return ciphertext, nil
|
||||
}
|
||||
|
||||
func decryptMessage(privateKey string, passphrase []byte, ciphertext *crypto.PGPMessage) (*crypto.PlainMessage, error) {
|
||||
privateKeyObj, err := crypto.NewKeyFromArmored(privateKey)
|
||||
func decryptMessage(privateKey []byte, passphrase []byte, ciphertext *crypto.PGPMessage) (*crypto.PlainMessage, error) {
|
||||
privateKeyObj, err := crypto.NewKey(privateKey)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "gopenpgp: unable to parse the private key")
|
||||
return nil, fmt.Errorf("gopenpgp: unable to parse the private key: %w", err)
|
||||
}
|
||||
|
||||
privateKeyUnlocked, err := privateKeyObj.Unlock(passphrase)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "gopenpgp: unable to unlock key")
|
||||
return nil, fmt.Errorf("gopenpgp: unable to unlock key: %w", err)
|
||||
}
|
||||
|
||||
defer privateKeyUnlocked.ClearPrivateParams()
|
||||
|
||||
privateKeyRing, err := crypto.NewKeyRing(privateKeyUnlocked)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "gopenpgp: unable to create the private key ring")
|
||||
return nil, fmt.Errorf("gopenpgp: unable to create the private key ring: %w", err)
|
||||
}
|
||||
|
||||
message, err := privateKeyRing.Decrypt(ciphertext, nil, 0)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "gopenpgp: unable to decrypt message")
|
||||
return nil, fmt.Errorf("gopenpgp: unable to decrypt message: %w", err)
|
||||
}
|
||||
|
||||
return message, nil
|
||||
}
|
||||
|
||||
func createPublicKeyRing(publicKey string) (*crypto.KeyRing, error) {
|
||||
publicKeyObj, err := crypto.NewKeyFromArmored(publicKey)
|
||||
func createPublicKeyRing(publicKey []byte) (*crypto.KeyRing, error) {
|
||||
publicKeyObj, err := crypto.NewKey(publicKey)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "gopenpgp: unable to parse public key")
|
||||
return nil, fmt.Errorf("gopenpgp: unable to parse public key: %w", err)
|
||||
}
|
||||
|
||||
if publicKeyObj.IsPrivate() {
|
||||
publicKeyObj, err = publicKeyObj.ToPublic()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "gopenpgp: unable to extract public key from private key")
|
||||
return nil, fmt.Errorf("gopenpgp: unable to extract public key from private key: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
publicKeyRing, err := crypto.NewKeyRing(publicKeyObj)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "gopenpgp: unable to create new keyring")
|
||||
return nil, fmt.Errorf("gopenpgp: unable to create new keyring: %w", err)
|
||||
}
|
||||
|
||||
return publicKeyRing, nil
|
||||
@@ -201,18 +219,15 @@ func AsymEncryptAndSign(PublicEncryptionKey string, PrivateSignatureKey string,
|
||||
var enc EncryptedMessage
|
||||
pub, err := base64.StdEncoding.DecodeString(PublicEncryptionKey)
|
||||
if err != nil {
|
||||
log.Error().Msg("Message encryption and sign b64 failed")
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("Message encryption and sign b64 failed: %w", err)
|
||||
}
|
||||
priv, err := base64.StdEncoding.DecodeString(PrivateSignatureKey)
|
||||
if err != nil {
|
||||
log.Error().Msg("Message encryption and sign b64 failed")
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("Message encryption and sign b64 failed: %w", err)
|
||||
}
|
||||
ciphertext, signature, err := encryptAndSignMessage(string(pub), string(priv), crypto.NewPlainMessage(data))
|
||||
ciphertext, signature, err := encryptAndSignMessage(pub, priv, crypto.NewPlainMessage(data))
|
||||
if err != nil {
|
||||
log.Error().Msg("Message encryption failed")
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("Message encryption failed: %w", err)
|
||||
}
|
||||
enc.Data = ciphertext.GetBinary()
|
||||
enc.Signature = []byte(signature)
|
||||
@@ -222,23 +237,20 @@ func AsymEncryptAndSign(PublicEncryptionKey string, PrivateSignatureKey string,
|
||||
func AsymDecryptAndCheck(MyPrivateEncryptionKey string, MyContactPublicKey string, data []byte, Signature []byte) (DecryptedMessage []byte, err error) {
|
||||
priv, err := base64.StdEncoding.DecodeString(MyPrivateEncryptionKey)
|
||||
if err != nil {
|
||||
log.Error().Msg("Message decryption and sign b64 failed")
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("Message decryption and sign b64 failed: %w", err)
|
||||
}
|
||||
pub, err := base64.StdEncoding.DecodeString(MyContactPublicKey)
|
||||
if err != nil {
|
||||
log.Error().Msg("Message decryption and sign b64 failed")
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("Message decryption and sign b64 failed: %w", err)
|
||||
}
|
||||
DecryptedMessage, err = decryptAndCheckMessage(string(pub), string(priv), crypto.NewPGPMessage(data), crypto.NewPGPSignature(Signature))
|
||||
DecryptedMessage, err = decryptAndCheckMessage(pub, priv, crypto.NewPGPMessage(data), crypto.NewPGPSignature(Signature))
|
||||
if err != nil {
|
||||
log.Error().Msg("Message decryption and sign failed")
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("Message decryption and sign failed: %w", err)
|
||||
}
|
||||
return DecryptedMessage, err
|
||||
}
|
||||
|
||||
func encryptAndSignMessage(pub string, priv string, message *crypto.PlainMessage) (*crypto.PGPMessage, []byte, error) {
|
||||
func encryptAndSignMessage(pub []byte, priv []byte, message *crypto.PlainMessage) (*crypto.PGPMessage, []byte, error) {
|
||||
var privateKeyObj, unlockedKeyObj *crypto.Key
|
||||
var privateKeyRing *crypto.KeyRing
|
||||
publicKeyRing, err := createPublicKeyRing(pub)
|
||||
@@ -246,36 +258,32 @@ func encryptAndSignMessage(pub string, priv string, message *crypto.PlainMessage
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "gopenpgp: unable to encrypt message")
|
||||
}
|
||||
|
||||
if privateKeyObj, err = crypto.NewKeyFromArmored(priv); err != nil {
|
||||
return nil, nil, errors.Wrap(err, "gopenpgp: unable to parse private key")
|
||||
if privateKeyObj, err = crypto.NewKey(priv); err != nil {
|
||||
return nil, nil, fmt.Errorf("gopenpgp: unable to parse private key")
|
||||
}
|
||||
|
||||
if unlockedKeyObj, err = privateKeyObj.Unlock(nil); err != nil {
|
||||
return nil, nil, errors.Wrap(err, "gopenpgp: unable to unlock key")
|
||||
return nil, nil, fmt.Errorf("gopenpgp: unable to unlock key")
|
||||
}
|
||||
defer unlockedKeyObj.ClearPrivateParams()
|
||||
|
||||
if privateKeyRing, err = crypto.NewKeyRing(unlockedKeyObj); err != nil {
|
||||
return nil, nil, errors.Wrap(err, "gopenpgp: unable to create private keyring")
|
||||
return nil, nil, fmt.Errorf("gopenpgp: unable to create private keyring")
|
||||
}
|
||||
|
||||
ciphertext, err := publicKeyRing.Encrypt(message, nil)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "gopenpgp: unable to encrypt message")
|
||||
return nil, nil, fmt.Errorf("gopenpgp: unable to encrypt message")
|
||||
}
|
||||
|
||||
signature, err := privateKeyRing.SignDetached(message)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "gopenpgp: unable to encrypt message")
|
||||
return nil, nil, fmt.Errorf("gopenpgp: unable to encrypt message")
|
||||
}
|
||||
return ciphertext, signature.GetBinary(), nil
|
||||
}
|
||||
|
||||
func decryptAndCheckMessage(pub string, priv string, message *crypto.PGPMessage, signature *crypto.PGPSignature) ([]byte, error) {
|
||||
func decryptAndCheckMessage(pub []byte, priv []byte, message *crypto.PGPMessage, signature *crypto.PGPSignature) ([]byte, error) {
|
||||
var privateKeyObj, unlockedKeyObj *crypto.Key
|
||||
var privateKeyRing *crypto.KeyRing
|
||||
publicKeyRing, err := createPublicKeyRing(pub)
|
||||
@@ -283,31 +291,27 @@ func decryptAndCheckMessage(pub string, priv string, message *crypto.PGPMessage,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "gopenpgp: unable to encrypt message")
|
||||
}
|
||||
|
||||
if privateKeyObj, err = crypto.NewKeyFromArmored(priv); err != nil {
|
||||
return nil, errors.Wrap(err, "gopenpgp: unable to parse private key")
|
||||
if privateKeyObj, err = crypto.NewKey(priv); err != nil {
|
||||
return nil, fmt.Errorf("gopenpgp: unable to parse private key")
|
||||
}
|
||||
|
||||
if unlockedKeyObj, err = privateKeyObj.Unlock(nil); err != nil {
|
||||
return nil, errors.Wrap(err, "gopenpgp: unable to unlock key")
|
||||
return nil, fmt.Errorf("gopenpgp: unable to unlock key")
|
||||
}
|
||||
defer unlockedKeyObj.ClearPrivateParams()
|
||||
|
||||
if privateKeyRing, err = crypto.NewKeyRing(unlockedKeyObj); err != nil {
|
||||
return nil, errors.Wrap(err, "gopenpgp: unable to create private keyring")
|
||||
return nil, fmt.Errorf("gopenpgp: unable to create private keyring")
|
||||
}
|
||||
|
||||
plainmessage, err := privateKeyRing.Decrypt(message, nil, 0)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "gopenpgp: unable to decrypt message")
|
||||
return nil, fmt.Errorf("gopenpgp: unable to decrypt message")
|
||||
}
|
||||
|
||||
err = publicKeyRing.VerifyDetached(plainmessage, signature, crypto.GetUnixTime())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "gopenpgp: unable to check message signature")
|
||||
return nil, fmt.Errorf("gopenpgp: unable to check message signature")
|
||||
}
|
||||
return plainmessage.GetBinary(), nil
|
||||
}
|
||||
|
||||
+32
-8
@@ -42,21 +42,36 @@ WE88AQOdxtE8dAuu16suOpgLUfluDgnzCg==
|
||||
-----END PGP PUBLIC KEY BLOCK-----`
|
||||
|
||||
func TestNewKeyPair(t *testing.T) {
|
||||
kp := NewKeyPair()
|
||||
kp, err := NewKeyPair()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fmt.Println(kp.Public)
|
||||
fmt.Println(kp.Private)
|
||||
}
|
||||
|
||||
func TestGetKey(t *testing.T) {
|
||||
kp := NewKeyPair()
|
||||
kp, err := NewKeyPair()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// fmt.Println(kp.Public)
|
||||
// fmt.Println(kp.Private)
|
||||
key := kp.GetCryptoKeyObject()
|
||||
key, err := kp.GetCryptoKeyObject()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// fmt.Println(key.Armor())
|
||||
Armpubkey, _ := key.GetArmoredPublicKey()
|
||||
Armpubkey, err := key.GetArmoredPublicKey()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pubkey := base64.StdEncoding.EncodeToString([]byte(Armpubkey))
|
||||
println(len([]byte(pubkey)))
|
||||
binpubkey, _ := key.GetPublicKey()
|
||||
binpubkey, err := key.GetPublicKey()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
println(len(binpubkey))
|
||||
assert.Equal(t, kp.Public, pubkey, "The two public keys should be the same.")
|
||||
//if kp.Public != pubkey {
|
||||
@@ -65,7 +80,10 @@ func TestGetKey(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAsymEncryptDecrypt(t *testing.T) {
|
||||
kp := NewKeyPair()
|
||||
kp, err := NewKeyPair()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
foo := []byte("!#$%&()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_abcdefghijklmnopqrstuvwxyz{|}~")
|
||||
encMess, err := AsymEncrypt(kp.Public, foo)
|
||||
if err != nil {
|
||||
@@ -80,7 +98,10 @@ func TestAsymEncryptDecrypt(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAsymEncryptDecryptSigned(t *testing.T) {
|
||||
kp := NewKeyPair()
|
||||
kp, err := NewKeyPair()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
foo := "!#$%&()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_abcdefghijklmnopqrstuvwxyz{|}~"
|
||||
enc, err := AsymEncryptAndSign(kp.Public, kp.Private, []byte(foo))
|
||||
if err != nil {
|
||||
@@ -94,7 +115,10 @@ func TestAsymEncryptDecryptSigned(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAsymEncryptDecryptSigned2(t *testing.T) {
|
||||
kp := NewKeyPair()
|
||||
kp, err := NewKeyPair()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
foo := "!#$%&()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_abcdefghijklmnopqrstuvwxyz{|}~"
|
||||
enc, err := AsymEncryptAndSign(kp.Public, kp.Private, []byte(foo))
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package client
|
||||
|
||||
import "time"
|
||||
|
||||
type Avatar struct {
|
||||
File string
|
||||
Date time.Time
|
||||
}
|
||||
+110
-10
@@ -2,23 +2,32 @@ package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/awnumar/memguard"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
// UserConfig
|
||||
SavePassword bool `json:"save_password,omitempty"`
|
||||
SavedPassword string `json:"saved_password,omitempty"`
|
||||
PasswordTip string `json:"password_tip,omitempty"`
|
||||
PasswordTipUnlock string `json:"password_tip_unlock,omitempty"`
|
||||
// Technical
|
||||
IdentityFile string `json:"identity_file,omitempty"`
|
||||
StoragePath string `json:"storage_path,omitempty"`
|
||||
MaxIdsPerUser int `json:"max_ids_per_user,omitempty"`
|
||||
MsgDbRollingPeriod int `json:"msg_db_rolling_period,omitempty"`
|
||||
Chunksize int64 `json:"chunksize,omitempty"`
|
||||
ServerPollInterval int `json:"server_poll_interval,omitempty"`
|
||||
DbSize int `json:"db_size,omitempty"`
|
||||
UserAgent string `json:"user_agent,omitempty"`
|
||||
// Network
|
||||
ServerPollInterval int `json:"server_poll_interval,omitempty"`
|
||||
HttpTimeOut int `json:"http_timeout,omitempty"`
|
||||
HttpLongPoll int `json:"http_long_poll,omitempty"`
|
||||
// GUI
|
||||
LastOpenChat string `json:"last_open_chat,omitempty"`
|
||||
SoundNotificationEnable bool `json:"sound_notification_enable,omitempty"`
|
||||
@@ -42,9 +51,9 @@ type Config struct {
|
||||
DbSuffix string `json:"db_suffix,omitempty"`
|
||||
|
||||
// Inner
|
||||
memoryPassword string `json:"memory_password,omitempty"`
|
||||
additionalPasswords []string `json:"additional_passwords,omitempty"`
|
||||
me *Identity `json:"me,omitempty"`
|
||||
memoryPassword *memguard.LockedBuffer
|
||||
additionalPasswords []*memguard.LockedBuffer
|
||||
me *Identity
|
||||
}
|
||||
|
||||
var instance *Config
|
||||
@@ -57,6 +66,18 @@ func GetConfig() *Config {
|
||||
return instance
|
||||
}
|
||||
|
||||
func (c *Config) Validate() error {
|
||||
if c.StoragePath == "" {
|
||||
return errors.New("storage_path is required")
|
||||
}
|
||||
|
||||
if c.Chunksize < 1024 || c.Chunksize > 10*1024*1024 {
|
||||
return fmt.Errorf("chunksize must be between 1KB and 10MB, got %d", c.Chunksize)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) Load(filename string) error {
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
@@ -66,6 +87,18 @@ func (c *Config) Load(filename string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.Validate(); err != nil {
|
||||
return fmt.Errorf("invalid config: %w", err)
|
||||
}
|
||||
// override values if not set or wrong
|
||||
if c.HttpTimeOut <= 0 {
|
||||
c.HttpTimeOut = 10
|
||||
c.Save(filename)
|
||||
}
|
||||
if c.HttpLongPoll <= 1 {
|
||||
c.HttpLongPoll = 300
|
||||
c.Save(filename)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -74,19 +107,26 @@ func (c *Config) Save(filename string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
os.WriteFile(filename, data, 0644)
|
||||
err = os.WriteFile(filename, data, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) SetMemPass(pass string) {
|
||||
c.memoryPassword = pass
|
||||
func (c *Config) SetMemPass(pass string) error {
|
||||
if c.memoryPassword != nil {
|
||||
c.memoryPassword.Destroy()
|
||||
}
|
||||
c.memoryPassword = memguard.NewBufferFromBytes([]byte(pass))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) GetMemPass() string {
|
||||
return c.memoryPassword
|
||||
func (c *Config) GetMemPass() (string, error) {
|
||||
if c.memoryPassword == nil {
|
||||
return "", errors.New("password not set")
|
||||
}
|
||||
return string(c.memoryPassword.Bytes()), nil
|
||||
}
|
||||
|
||||
func (c *Config) GetIdentity() *Identity {
|
||||
@@ -102,5 +142,65 @@ func (c *Config) SaveIdentity() error {
|
||||
}
|
||||
|
||||
func (c *Config) Clean() {
|
||||
c.additionalPasswords = []string{}
|
||||
if c.memoryPassword != nil {
|
||||
c.memoryPassword.Destroy()
|
||||
c.memoryPassword = nil
|
||||
}
|
||||
for _, buf := range c.additionalPasswords {
|
||||
if buf != nil {
|
||||
buf.Destroy()
|
||||
}
|
||||
}
|
||||
c.additionalPasswords = []*memguard.LockedBuffer{}
|
||||
}
|
||||
|
||||
// AddAdditionalPassword securely stores an additional password in protected memory
|
||||
func (c *Config) AddAdditionalPassword(password string) {
|
||||
buf := memguard.NewBufferFromBytes([]byte(password))
|
||||
c.additionalPasswords = append(c.additionalPasswords, buf)
|
||||
}
|
||||
|
||||
// GetAdditionalPasswords returns all additional passwords as strings
|
||||
func (c *Config) GetAdditionalPasswords() ([]string, error) {
|
||||
passwords := make([]string, 0, len(c.additionalPasswords))
|
||||
for _, buf := range c.additionalPasswords {
|
||||
if buf == nil {
|
||||
continue
|
||||
}
|
||||
passwords = append(passwords, string(buf.Bytes()))
|
||||
}
|
||||
return passwords, nil
|
||||
}
|
||||
|
||||
// GetAdditionalPasswordAt returns the password at the specified index
|
||||
func (c *Config) GetAdditionalPasswordAt(index int) (string, error) {
|
||||
if index < 0 || index >= len(c.additionalPasswords) {
|
||||
return "", errors.New("index out of range")
|
||||
}
|
||||
if c.additionalPasswords[index] == nil {
|
||||
return "", errors.New("password at index is nil")
|
||||
}
|
||||
return string(c.additionalPasswords[index].Bytes()), nil
|
||||
}
|
||||
|
||||
// RemoveAdditionalPasswordAt removes and destroys the password at the specified index
|
||||
func (c *Config) RemoveAdditionalPasswordAt(index int) error {
|
||||
if index < 0 || index >= len(c.additionalPasswords) {
|
||||
return errors.New("index out of range")
|
||||
}
|
||||
if c.additionalPasswords[index] != nil {
|
||||
c.additionalPasswords[index].Destroy()
|
||||
}
|
||||
c.additionalPasswords = append(c.additionalPasswords[:index], c.additionalPasswords[index+1:]...)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearAdditionalPasswords removes and destroys all additional passwords
|
||||
func (c *Config) ClearAdditionalPasswords() {
|
||||
for _, buf := range c.additionalPasswords {
|
||||
if buf != nil {
|
||||
buf.Destroy()
|
||||
}
|
||||
}
|
||||
c.additionalPasswords = []*memguard.LockedBuffer{}
|
||||
}
|
||||
|
||||
+62
-1
@@ -6,7 +6,10 @@ import (
|
||||
|
||||
func TestConfigSave(t *testing.T) {
|
||||
c := GetConfig()
|
||||
c.memoryPassword = "hideme"
|
||||
err := c.SetMemPass("hideme")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to set password: %v", err)
|
||||
}
|
||||
c.IdentityFile = "test.id"
|
||||
c.Chunksize = 10000000
|
||||
c.SavePassword = true
|
||||
@@ -18,3 +21,61 @@ func TestConfigLoad(t *testing.T) {
|
||||
_ = GetConfig().Load("test.cfg")
|
||||
println(GetConfig().Chunksize)
|
||||
}
|
||||
|
||||
func TestAdditionalPasswords(t *testing.T) {
|
||||
c := GetConfig()
|
||||
c.ClearAdditionalPasswords()
|
||||
|
||||
// Test adding passwords
|
||||
c.AddAdditionalPassword("password1")
|
||||
c.AddAdditionalPassword("password2")
|
||||
c.AddAdditionalPassword("password3")
|
||||
|
||||
// Test getting all passwords
|
||||
passwords, err := c.GetAdditionalPasswords()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get passwords: %v", err)
|
||||
}
|
||||
if len(passwords) != 3 {
|
||||
t.Fatalf("Expected 3 passwords, got %d", len(passwords))
|
||||
}
|
||||
if passwords[0] != "password1" || passwords[1] != "password2" || passwords[2] != "password3" {
|
||||
t.Fatalf("Password values don't match expected")
|
||||
}
|
||||
|
||||
// Test getting password at index
|
||||
pass, err := c.GetAdditionalPasswordAt(1)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get password at index 1: %v", err)
|
||||
}
|
||||
if pass != "password2" {
|
||||
t.Fatalf("Expected 'password2', got '%s'", pass)
|
||||
}
|
||||
|
||||
// Test removing password at index
|
||||
err = c.RemoveAdditionalPasswordAt(1)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to remove password: %v", err)
|
||||
}
|
||||
|
||||
passwords, err = c.GetAdditionalPasswords()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get passwords after removal: %v", err)
|
||||
}
|
||||
if len(passwords) != 2 {
|
||||
t.Fatalf("Expected 2 passwords after removal, got %d", len(passwords))
|
||||
}
|
||||
if passwords[0] != "password1" || passwords[1] != "password3" {
|
||||
t.Fatalf("Password values don't match expected after removal")
|
||||
}
|
||||
|
||||
// Test clearing all passwords
|
||||
c.ClearAdditionalPasswords()
|
||||
passwords, err = c.GetAdditionalPasswords()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get passwords after clear: %v", err)
|
||||
}
|
||||
if len(passwords) != 0 {
|
||||
t.Fatalf("Expected 0 passwords after clear, got %d", len(passwords))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"forge.redroom.link/yves/meowlib"
|
||||
)
|
||||
|
||||
func DbMessageToInternalUserMessage(id int64, dbFile string, dbm *meowlib.DbMessage) *InternalUserMessage {
|
||||
var ium InternalUserMessage
|
||||
ium.Dbid = id
|
||||
ium.Dbfile = dbFile
|
||||
if dbm.Outbound {
|
||||
ium.Outbound = true
|
||||
} else {
|
||||
ium.Outbound = false
|
||||
}
|
||||
ium.Message = string(dbm.Data)
|
||||
ium.Status = dbm.Status
|
||||
ium.Contact = dbm.Contact
|
||||
ium.CurrentLocation = dbm.CurrentLocation
|
||||
ium.Messagetype = dbm.Type
|
||||
ium.Appdata = dbm.Appdata
|
||||
ium.FilePaths = dbm.FilePaths
|
||||
ium.ServerDeliveryUuid = dbm.ServerDeliveryUuid
|
||||
ium.ServerDeliveryTimestamp = dbm.ServerDeliveryTimestamp
|
||||
return &ium
|
||||
}
|
||||
|
||||
func InternalUserMessageToDbMessage(ium *InternalUserMessage) *meowlib.DbMessage {
|
||||
var dbm meowlib.DbMessage
|
||||
dbm.Outbound = ium.Outbound
|
||||
dbm.Type = ium.Messagetype
|
||||
dbm.Data = []byte(ium.Message)
|
||||
dbm.Appdata = ium.Appdata
|
||||
dbm.Contact = ium.Contact
|
||||
dbm.CurrentLocation = ium.CurrentLocation
|
||||
dbm.Status = ium.Status
|
||||
dbm.FilePaths = ium.FilePaths
|
||||
dbm.ServerDeliveryUuid = ium.ServerDeliveryUuid
|
||||
dbm.ServerDeliveryTimestamp = ium.ServerDeliveryTimestamp
|
||||
return &dbm
|
||||
}
|
||||
|
||||
func UserMessageToDbMessage(outbound bool, um *meowlib.UserMessage, filepaths []string) *meowlib.DbMessage {
|
||||
var dbm meowlib.DbMessage
|
||||
dbm.Outbound = outbound
|
||||
dbm.Type = um.Type
|
||||
dbm.Data = um.Data
|
||||
dbm.Appdata = um.Appdata
|
||||
dbm.Contact = um.Contact
|
||||
dbm.CurrentLocation = um.CurrentLocation
|
||||
dbm.Status = um.Status
|
||||
dbm.FilePaths = filepaths
|
||||
return &dbm
|
||||
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
doubleratchet "github.com/status-im/doubleratchet"
|
||||
)
|
||||
|
||||
// drLocalPair implements doubleratchet.DHPair using raw byte slices.
|
||||
type drLocalPair struct {
|
||||
priv doubleratchet.Key
|
||||
pub doubleratchet.Key
|
||||
}
|
||||
|
||||
func (p drLocalPair) PrivateKey() doubleratchet.Key { return p.priv }
|
||||
func (p drLocalPair) PublicKey() doubleratchet.Key { return p.pub }
|
||||
|
||||
// serializedDRState is an intermediate JSON-friendly representation of doubleratchet.State.
|
||||
type serializedDRState struct {
|
||||
DHrPublic []byte `json:"dhr_pub"`
|
||||
DHsPrivate []byte `json:"dhs_priv"`
|
||||
DHsPublic []byte `json:"dhs_pub"`
|
||||
RootChCK []byte `json:"root_ch_ck"`
|
||||
SendChCK []byte `json:"send_ch_ck"`
|
||||
SendChN uint32 `json:"send_ch_n"`
|
||||
RecvChCK []byte `json:"recv_ch_ck"`
|
||||
RecvChN uint32 `json:"recv_ch_n"`
|
||||
PN uint32 `json:"pn"`
|
||||
MkSkipped map[string]map[uint][]byte `json:"mk_skipped"`
|
||||
MaxSkip uint `json:"max_skip"`
|
||||
MaxKeep uint `json:"max_keep"`
|
||||
MaxMessageKeysPerSession int `json:"max_mks_per_session"`
|
||||
Step uint `json:"step"`
|
||||
KeysCount uint `json:"keys_count"`
|
||||
}
|
||||
|
||||
// drSessionStorage implements doubleratchet.SessionStorage, persisting state into peer.DrStateJson.
|
||||
type drSessionStorage struct{ peer *Peer }
|
||||
|
||||
func (s *drSessionStorage) Save(id []byte, state *doubleratchet.State) error {
|
||||
all, err := state.MkSkipped.All()
|
||||
if err != nil {
|
||||
return fmt.Errorf("drSessionStorage.Save: MkSkipped.All: %w", err)
|
||||
}
|
||||
mkSkipped := make(map[string]map[uint][]byte, len(all))
|
||||
for k, msgs := range all {
|
||||
inner := make(map[uint][]byte, len(msgs))
|
||||
for num, mk := range msgs {
|
||||
inner[num] = []byte(mk)
|
||||
}
|
||||
mkSkipped[k] = inner
|
||||
}
|
||||
|
||||
ss := serializedDRState{
|
||||
DHrPublic: []byte(state.DHr),
|
||||
DHsPrivate: []byte(state.DHs.PrivateKey()),
|
||||
DHsPublic: []byte(state.DHs.PublicKey()),
|
||||
RootChCK: []byte(state.RootCh.CK),
|
||||
SendChCK: []byte(state.SendCh.CK),
|
||||
SendChN: state.SendCh.N,
|
||||
RecvChCK: []byte(state.RecvCh.CK),
|
||||
RecvChN: state.RecvCh.N,
|
||||
PN: state.PN,
|
||||
MkSkipped: mkSkipped,
|
||||
MaxSkip: state.MaxSkip,
|
||||
MaxKeep: state.MaxKeep,
|
||||
MaxMessageKeysPerSession: state.MaxMessageKeysPerSession,
|
||||
Step: state.Step,
|
||||
KeysCount: state.KeysCount,
|
||||
}
|
||||
|
||||
b, err := json.Marshal(ss)
|
||||
if err != nil {
|
||||
return fmt.Errorf("drSessionStorage.Save: json.Marshal: %w", err)
|
||||
}
|
||||
s.peer.DrStateJson = string(b)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *drSessionStorage) Load(id []byte) (*doubleratchet.State, error) {
|
||||
if s.peer.DrStateJson == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var ss serializedDRState
|
||||
if err := json.Unmarshal([]byte(s.peer.DrStateJson), &ss); err != nil {
|
||||
return nil, fmt.Errorf("drSessionStorage.Load: json.Unmarshal: %w", err)
|
||||
}
|
||||
|
||||
c := doubleratchet.DefaultCrypto{}
|
||||
mkStorage := &doubleratchet.KeysStorageInMemory{}
|
||||
seq := uint(0)
|
||||
for k, msgs := range ss.MkSkipped {
|
||||
pubKey, err := hex.DecodeString(k)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("drSessionStorage.Load: decode skipped key hex: %w", err)
|
||||
}
|
||||
for num, mk := range msgs {
|
||||
if err := mkStorage.Put(id, doubleratchet.Key(pubKey), num, doubleratchet.Key(mk), seq); err != nil {
|
||||
return nil, fmt.Errorf("drSessionStorage.Load: Put: %w", err)
|
||||
}
|
||||
seq++
|
||||
}
|
||||
}
|
||||
|
||||
state := &doubleratchet.State{
|
||||
Crypto: c,
|
||||
DHr: doubleratchet.Key(ss.DHrPublic),
|
||||
DHs: drLocalPair{priv: doubleratchet.Key(ss.DHsPrivate), pub: doubleratchet.Key(ss.DHsPublic)},
|
||||
PN: ss.PN,
|
||||
MkSkipped: mkStorage,
|
||||
MaxSkip: ss.MaxSkip,
|
||||
MaxKeep: ss.MaxKeep,
|
||||
MaxMessageKeysPerSession: ss.MaxMessageKeysPerSession,
|
||||
Step: ss.Step,
|
||||
KeysCount: ss.KeysCount,
|
||||
}
|
||||
state.RootCh.CK = doubleratchet.Key(ss.RootChCK)
|
||||
state.RootCh.Crypto = c
|
||||
state.SendCh.CK = doubleratchet.Key(ss.SendChCK)
|
||||
state.SendCh.N = ss.SendChN
|
||||
state.SendCh.Crypto = c
|
||||
state.RecvCh.CK = doubleratchet.Key(ss.RecvChCK)
|
||||
state.RecvCh.N = ss.RecvChN
|
||||
state.RecvCh.Crypto = c
|
||||
|
||||
return state, nil
|
||||
}
|
||||
|
||||
// GetDRSession returns an active DR session for the peer, creating one if needed.
|
||||
func (p *Peer) GetDRSession() (doubleratchet.Session, error) {
|
||||
store := &drSessionStorage{peer: p}
|
||||
|
||||
// If we already have a saved state, load it
|
||||
if p.DrStateJson != "" {
|
||||
return doubleratchet.Load([]byte(p.Uid), store)
|
||||
}
|
||||
|
||||
// Initiator: has own DH keypair + root key, no state yet
|
||||
if p.DrInitiator && p.DrKpPrivate != "" {
|
||||
privBytes, err := base64.StdEncoding.DecodeString(p.DrKpPrivate)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetDRSession: decode DrKpPrivate: %w", err)
|
||||
}
|
||||
pubBytes, err := base64.StdEncoding.DecodeString(p.DrKpPublic)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetDRSession: decode DrKpPublic: %w", err)
|
||||
}
|
||||
rootKeyBytes, err := base64.StdEncoding.DecodeString(p.DrRootKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetDRSession: decode DrRootKey: %w", err)
|
||||
}
|
||||
kp := drLocalPair{priv: doubleratchet.Key(privBytes), pub: doubleratchet.Key(pubBytes)}
|
||||
return doubleratchet.New([]byte(p.Uid), doubleratchet.Key(rootKeyBytes), kp, store)
|
||||
}
|
||||
|
||||
// Responder: has remote DH public key + root key
|
||||
if !p.DrInitiator && p.ContactDrPublicKey != "" {
|
||||
remotePubBytes, err := base64.StdEncoding.DecodeString(p.ContactDrPublicKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetDRSession: decode ContactDrPublicKey: %w", err)
|
||||
}
|
||||
rootKeyBytes, err := base64.StdEncoding.DecodeString(p.DrRootKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetDRSession: decode DrRootKey: %w", err)
|
||||
}
|
||||
return doubleratchet.NewWithRemoteKey([]byte(p.Uid), doubleratchet.Key(rootKeyBytes), doubleratchet.Key(remotePubBytes), store)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("GetDRSession: peer %s has no DR keys configured", p.Uid)
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"forge.redroom.link/yves/meowlib"
|
||||
"forge.redroom.link/yves/meowlib/client"
|
||||
"github.com/google/uuid"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
type ReceivedMessage struct {
|
||||
Text string
|
||||
files []string
|
||||
Server string
|
||||
Sent uint64
|
||||
Received uint64
|
||||
LocalUuid string
|
||||
LocalSequence uint64
|
||||
AppData string
|
||||
Location meowlib.Location
|
||||
}
|
||||
|
||||
// PollServer checks for messages on a single server
|
||||
func PollServer(storage_path string, job *client.RequestsJob, timeout int, longPoll bool) (int, string, error) {
|
||||
|
||||
count := 0
|
||||
|
||||
// if folder does not exist, create it
|
||||
if _, err := os.Stat(filepath.Join(storage_path, "inbox")); os.IsNotExist(err) {
|
||||
err := os.MkdirAll(filepath.Join(storage_path, "inbox"), 0700)
|
||||
if err != nil {
|
||||
return -1, "CheckMessages: MkdirAll", err
|
||||
}
|
||||
}
|
||||
//convert server to a server object
|
||||
|
||||
var crl []*meowlib.ConversationRequest
|
||||
// build conversation requests
|
||||
if job.LookupKeys != nil {
|
||||
for _, key := range job.LookupKeys {
|
||||
var cr meowlib.ConversationRequest
|
||||
cr.LookupKey = key.Public
|
||||
cr.SendTimestamp = time.Now().UTC().Unix()
|
||||
// todo sign it
|
||||
//cr.LookupSignature =
|
||||
crl = append(crl, &cr)
|
||||
}
|
||||
// get server public key
|
||||
if job.Server.PublicKey == "" {
|
||||
key, err := meowlib.HttpGetId(job.Server.Url)
|
||||
if err != nil {
|
||||
return -1, "CheckMessages: HttpGetId", err
|
||||
}
|
||||
job.Server.PublicKey = key["publicKey"]
|
||||
}
|
||||
// build server message
|
||||
var toSrv meowlib.ToServerMessage
|
||||
toSrv.PullRequest = crl
|
||||
toSrv.From = job.Server.UserKp.Public
|
||||
|
||||
if longPoll {
|
||||
toSrv.Timeout = int64(timeout)
|
||||
}
|
||||
|
||||
data, err := job.Server.ProcessOutboundMessage(&toSrv)
|
||||
if err != nil {
|
||||
return -1, "CheckMessages: ProcessOutboundMessage", err
|
||||
}
|
||||
|
||||
response, err := meowlib.HttpPostMessage(job.Server.Url, data, timeout)
|
||||
if err != nil {
|
||||
return -1, "CheckMessages: httpPostMessage", err
|
||||
}
|
||||
fs_msg, err := job.Server.ProcessInboundServerResponse(response)
|
||||
if err != nil {
|
||||
return -1, "CheckMessages: ProcessInboundServerResponse", err
|
||||
}
|
||||
if len(fs_msg.Chat) > 0 || (fs_msg.Invitation != nil && fs_msg.Invitation.Step == 3) {
|
||||
// chat or invitation answer => save the server message
|
||||
|
||||
out, err := proto.Marshal(fs_msg)
|
||||
if err != nil {
|
||||
return -1, "CheckMessages: protobuf marshal", err
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(storage_path, "inbox", strconv.FormatInt(time.Now().UTC().UnixNano(), 10)), out, 0644); err != nil {
|
||||
return -1, "CheckMessages: WriteFile", err
|
||||
}
|
||||
}
|
||||
count = len(fs_msg.Chat)
|
||||
} else {
|
||||
// manage non uszer messages like devices or server
|
||||
}
|
||||
|
||||
return count, "", nil
|
||||
}
|
||||
|
||||
// SaveCheckJobs
|
||||
func SaveCheckJobs() (string, error) {
|
||||
me := client.GetConfig().GetIdentity()
|
||||
err := me.SaveBackgroundJob()
|
||||
if err != nil {
|
||||
|
||||
return "CheckMessages: json.Marshal", err
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// ConsumeInboxFile
|
||||
func ConsumeInboxFile(messageFilename string) ([]string, []string, string, error) {
|
||||
|
||||
messagesOverview := []string{}
|
||||
filenames := []string{}
|
||||
identity := client.GetConfig().GetIdentity()
|
||||
// read message file
|
||||
msg, err := os.ReadFile(messageFilename)
|
||||
if err != nil {
|
||||
return nil, nil, "ReadMessage: ReadFile", err
|
||||
}
|
||||
// protobuf unmarshal message
|
||||
var fromServerMessage meowlib.FromServerMessage
|
||||
err = proto.Unmarshal(msg, &fromServerMessage)
|
||||
if err != nil {
|
||||
return nil, nil, "ReadMessage: Unmarshal FromServerMessage", err
|
||||
}
|
||||
// check if invitation answer (step-2 answer waiting for the initiator)
|
||||
if fromServerMessage.Invitation != nil {
|
||||
peer, _, _, invErr := InvitationStep3ProcessAnswer(fromServerMessage.Invitation)
|
||||
if invErr == nil && peer != nil {
|
||||
// Auto-send step-3 CC to invitee's servers.
|
||||
msgs, _, sendErr := InvitationStep3Message(peer.InvitationId)
|
||||
if sendErr == nil {
|
||||
for i, bytemsg := range msgs {
|
||||
if i < len(peer.ContactPullServers) {
|
||||
meowlib.HttpPostMessage(peer.ContactPullServers[i], bytemsg, client.GetConfig().HttpTimeOut)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Chat messages
|
||||
if len(fromServerMessage.Chat) > 0 {
|
||||
for _, packedUserMessage := range fromServerMessage.Chat {
|
||||
|
||||
// find the peer with that lookup key
|
||||
peer := identity.Peers.GetFromMyLookupKey(packedUserMessage.Destination)
|
||||
if peer == nil {
|
||||
return nil, nil, "ReadMessage: GetFromMyLookupKey", errors.New("no visible peer for that message")
|
||||
}
|
||||
// Unpack the message — step-3 messages arrive before the initiator's identity
|
||||
// key is known, so skip signature verification for pending peers.
|
||||
var usermsg *meowlib.UserMessage
|
||||
if peer.InvitationPending() {
|
||||
usermsg, err = peer.ProcessInboundStep3UserMessage(packedUserMessage)
|
||||
} else {
|
||||
usermsg, err = peer.ProcessInboundUserMessage(packedUserMessage)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, nil, "ReadMessage: ProcessInboundUserMessage", err
|
||||
}
|
||||
|
||||
// Handle invitation step 3: initiator's full ContactCard arriving at the invitee.
|
||||
if usermsg.Invitation != nil && usermsg.Invitation.Step == 3 {
|
||||
finalizedPeer, _, finalErr := InvitationStep4ProcessStep3(usermsg)
|
||||
if finalErr == nil && finalizedPeer != nil {
|
||||
// Auto-send step-4 confirmation to initiator's servers.
|
||||
step4msgs, _, sendErr := InvitationStep4Message(finalizedPeer.InvitationId)
|
||||
if sendErr == nil {
|
||||
for i, bytemsg := range step4msgs {
|
||||
if i < len(finalizedPeer.ContactPullServers) {
|
||||
meowlib.HttpPostMessage(finalizedPeer.ContactPullServers[i], bytemsg, client.GetConfig().HttpTimeOut)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle invitation step 4: invitee's confirmation arriving at the initiator.
|
||||
if usermsg.Invitation != nil && usermsg.Invitation.Step == 4 {
|
||||
// Contact is fully active — nothing more to do on the initiator side.
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for received or processed already filled => it's an ack for one of our sent messages
|
||||
if len(usermsg.Data) == 0 && usermsg.Status != nil && usermsg.Status.Uuid != "" &&
|
||||
(usermsg.Status.Received != 0 || usermsg.Status.Processed != 0) {
|
||||
password, _ := client.GetConfig().GetMemPass()
|
||||
if ackErr := client.UpdateMessageAck(peer, usermsg.Status.Uuid, usermsg.Status.Received, usermsg.Status.Processed, password); ackErr != nil {
|
||||
logger.Warn().Err(ackErr).Str("uuid", usermsg.Status.Uuid).Msg("ConsumeInboxFile: UpdateMessageAck")
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
//fmt.Println("From:", usermsg.From)
|
||||
//jsonUserMessage, _ := json.Marshal(usermsg)
|
||||
//fmt.Println(string(jsonUserMessage))
|
||||
//peer = client.GetConfig().GetIdentity().Peers.GetFromPublicKey(usermsg.From)
|
||||
|
||||
// detach files
|
||||
if usermsg.Files != nil {
|
||||
// create files folder
|
||||
if _, err := os.Stat(filepath.Join(client.GetConfig().StoragePath, identity.Uuid, "files")); os.IsNotExist(err) {
|
||||
err = os.MkdirAll(filepath.Join(client.GetConfig().StoragePath, identity.Uuid, "files"), 0700)
|
||||
if err != nil {
|
||||
return nil, nil, "ReadMessage: MkdirAll", err
|
||||
}
|
||||
}
|
||||
for _, file := range usermsg.Files {
|
||||
filename := uuid.New().String() + "_" + file.Filename
|
||||
filenames = append(filenames, peer.Name+" sent: "+filename)
|
||||
// detach file
|
||||
os.WriteFile(filepath.Join(client.GetConfig().StoragePath, identity.Uuid, "files", filename), file.Data, 0600)
|
||||
}
|
||||
//? result["invitation finalized"] = peer.Name
|
||||
}
|
||||
// user message
|
||||
|
||||
messagesOverview = append(messagesOverview, peer.Name+" > "+string(usermsg.Data))
|
||||
|
||||
// stamp the received time before storing
|
||||
receivedAt := time.Now().UTC().Unix()
|
||||
if usermsg.Status == nil {
|
||||
usermsg.Status = &meowlib.ConversationStatus{}
|
||||
}
|
||||
usermsg.Status.Received = uint64(receivedAt)
|
||||
|
||||
// add message to storage
|
||||
err = peer.StoreMessage(usermsg, filenames)
|
||||
if err != nil {
|
||||
return nil, nil, "ReadMessage: StoreMessage", err
|
||||
}
|
||||
filenames = []string{}
|
||||
|
||||
// Persist peer to save updated DR state (DrStateJson)
|
||||
if peer.DrRootKey != "" {
|
||||
if storeErr := identity.Peers.StorePeer(peer); storeErr != nil {
|
||||
logger.Warn().Err(storeErr).Str("peer", peer.Uid).Msg("ConsumeInboxFile: StorePeer (DR state)")
|
||||
}
|
||||
}
|
||||
|
||||
// Send delivery ack if the peer requested it
|
||||
if peer.SendDeliveryAck && usermsg.Status.Uuid != "" {
|
||||
storagePath := filepath.Join(client.GetConfig().StoragePath, identity.Uuid)
|
||||
if ackErr := sendDeliveryAck(storagePath, peer, usermsg.Status.Uuid, receivedAt); ackErr != nil {
|
||||
logger.Warn().Err(ackErr).Str("peer", peer.Uid).Msg("ConsumeInboxFile: sendDeliveryAck")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = os.Remove(messageFilename)
|
||||
if err != nil {
|
||||
return nil, nil, "ReadMessage: Remove", err
|
||||
}
|
||||
|
||||
// list of messages & detached files
|
||||
return messagesOverview, filenames, "", nil
|
||||
}
|
||||
|
||||
// LongPollAllSerevrJobs checks for messages on a all servers defived in job file
|
||||
func LongPollAllServerJobs(storage_path string, jobs []client.RequestsJob, timeout int, longPoll bool) (int, string, error) {
|
||||
|
||||
// Channel to collect results
|
||||
resultChan := make(chan int, len(jobs))
|
||||
errChan := make(chan error, len(jobs))
|
||||
|
||||
// WaitGroup to sync goroutines
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Loop through each job (server)
|
||||
for _, job := range jobs {
|
||||
wg.Add(1)
|
||||
|
||||
go func(job client.RequestsJob) {
|
||||
defer wg.Done()
|
||||
|
||||
// Long-polling call to the server
|
||||
cnt, _, err := PollServer(storage_path, &job, timeout, true)
|
||||
|
||||
if err == nil && cnt > 0 {
|
||||
select {
|
||||
case resultChan <- cnt:
|
||||
default:
|
||||
}
|
||||
|
||||
// Close the error channel to notify all goroutines
|
||||
close(errChan)
|
||||
|
||||
}
|
||||
}(job)
|
||||
}
|
||||
|
||||
// Close the result channel when all workers are done
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultChan)
|
||||
}()
|
||||
|
||||
// Wait for the first message or all timeouts
|
||||
select {
|
||||
case cnt := <-resultChan:
|
||||
return cnt, "", nil
|
||||
case <-errChan:
|
||||
// If one fails and exitOnMessage is true
|
||||
return 0, "", nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// sendDeliveryAck builds a delivery acknowledgment for messageUuid and enqueues
|
||||
// it for sending to the peer's contact pull servers.
|
||||
func sendDeliveryAck(storagePath string, peer *client.Peer, messageUuid string, receivedAt int64) error {
|
||||
packedMsg, _, err := BuildReceivedMessage(messageUuid, peer.Uid, receivedAt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("sendDeliveryAck: BuildReceivedMessage: %w", err)
|
||||
}
|
||||
|
||||
data, err := proto.Marshal(packedMsg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("sendDeliveryAck: proto.Marshal: %w", err)
|
||||
}
|
||||
|
||||
outboxDir := filepath.Join(storagePath, "outbox")
|
||||
if err := os.MkdirAll(outboxDir, 0700); err != nil {
|
||||
return fmt.Errorf("sendDeliveryAck: MkdirAll: %w", err)
|
||||
}
|
||||
|
||||
outboxFile := filepath.Join(outboxDir, "ack_"+uuid.New().String())
|
||||
if err := os.WriteFile(outboxFile, data, 0600); err != nil {
|
||||
return fmt.Errorf("sendDeliveryAck: WriteFile: %w", err)
|
||||
}
|
||||
|
||||
var servers []client.Server
|
||||
for _, srvUid := range peer.ContactPullServers {
|
||||
srv, loadErr := client.GetConfig().GetIdentity().MessageServers.LoadServer(srvUid)
|
||||
if loadErr == nil && srv != nil {
|
||||
servers = append(servers, *srv)
|
||||
}
|
||||
}
|
||||
if len(servers) == 0 {
|
||||
os.Remove(outboxFile)
|
||||
return errors.New("sendDeliveryAck: no contact servers found")
|
||||
}
|
||||
|
||||
return client.PushSendJob(storagePath, &client.SendJob{
|
||||
Queue: peer.Uid,
|
||||
File: outboxFile,
|
||||
Servers: servers,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"forge.redroom.link/yves/meowlib"
|
||||
"forge.redroom.link/yves/meowlib/client"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const maxRetriesPerServer = 3
|
||||
const defaultSendTimeout = 3600 * 24 // seconds, used when job.Timeout is 0
|
||||
const defaultPostTimeout = 200
|
||||
|
||||
// CreateUserMessageAndSendJob is the single entry point for sending a message.
|
||||
// It creates and stores the user message, serialises the packed form to
|
||||
// storagePath/outbox/{dbFile}_{dbId}, and enqueues a SendJob in
|
||||
// storagePath/queues/{peerUid}.
|
||||
func CreateUserMessageAndSendJob(storagePath, message, peerUid, replyToUid string, filelist []string, servers []client.Server, timeout int) error {
|
||||
packedMsg, dbFile, dbId, errTxt, err := CreateAndStoreUserMessage(message, peerUid, replyToUid, filelist)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", errTxt, err)
|
||||
}
|
||||
|
||||
data, err := proto.Marshal(packedMsg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("CreateUserMessageAndSendJob: proto.Marshal: %w", err)
|
||||
}
|
||||
|
||||
outboxDir := filepath.Join(storagePath, "outbox")
|
||||
if err := os.MkdirAll(outboxDir, 0700); err != nil {
|
||||
return fmt.Errorf("CreateUserMessageAndSendJob: MkdirAll: %w", err)
|
||||
}
|
||||
|
||||
outboxFile := filepath.Join(outboxDir, fmt.Sprintf("%s_%d", dbFile, dbId))
|
||||
if err := os.WriteFile(outboxFile, data, 0600); err != nil {
|
||||
return fmt.Errorf("CreateUserMessageAndSendJob: WriteFile: %w", err)
|
||||
}
|
||||
|
||||
return client.PushSendJob(storagePath, &client.SendJob{
|
||||
Queue: peerUid,
|
||||
File: outboxFile,
|
||||
Servers: servers,
|
||||
Timeout: timeout,
|
||||
})
|
||||
}
|
||||
|
||||
// ProcessSendQueues discovers every queue DB file under storagePath/queues/
|
||||
// and processes each queue concurrently in its own goroutine.
|
||||
// Call this from the send isolate on wake-up notification or on a periodic timer.
|
||||
// It returns the total number of successfully sent messages across all queues.
|
||||
func ProcessSendQueues(storagePath string) int {
|
||||
queueDir := filepath.Join(storagePath, "queues")
|
||||
entries, err := os.ReadDir(queueDir)
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Str("dir", queueDir).Msg("ProcessSendQueues: ReadDir")
|
||||
return 0
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
counts := make(chan int, len(entries))
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
wg.Add(1)
|
||||
queue := entry.Name()
|
||||
go func(q string) {
|
||||
defer wg.Done()
|
||||
counts <- processSendQueue(storagePath, q)
|
||||
}(queue)
|
||||
}
|
||||
wg.Wait()
|
||||
close(counts)
|
||||
|
||||
total := 0
|
||||
for n := range counts {
|
||||
total += n
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// processSendQueue processes pending jobs for a single named queue sequentially.
|
||||
// It returns the number of successfully sent messages.
|
||||
//
|
||||
// For each pending job it will:
|
||||
// - immediately mark it failed if its TTL (job.Timeout) has elapsed – this is the
|
||||
// only criterion for permanent failure; retry exhaustion is never a failure cause
|
||||
// - attempt delivery, cycling through servers until one succeeds
|
||||
// - mark it sent on success
|
||||
// - stop and return when all servers fail this run (will resume on next call)
|
||||
//
|
||||
// Per-server retry counts (maxRetriesPerServer) are local to each call so that
|
||||
// past failures in previous runs never prevent future delivery attempts.
|
||||
func processSendQueue(storagePath, queue string) int {
|
||||
sent := 0
|
||||
for {
|
||||
job, _, err := client.PeekSendJob(storagePath, queue)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Str("queue", queue).Msg("processSendQueue: PeekSendJob")
|
||||
return sent
|
||||
}
|
||||
if job == nil {
|
||||
return sent // no more pending jobs
|
||||
}
|
||||
|
||||
// Hard timeout: the only criterion for permanent failure.
|
||||
// Use defaultSendTimeout when the job carries no explicit TTL.
|
||||
ttl := job.Timeout
|
||||
if ttl <= 0 {
|
||||
ttl = defaultSendTimeout
|
||||
}
|
||||
if time.Since(job.InsertedAt) > time.Duration(ttl)*time.Second {
|
||||
job.Status = client.SendStatusFailed
|
||||
if err := client.UpdateSendJob(storagePath, queue, job); err != nil {
|
||||
logger.Error().Err(err).Int64("id", job.ID).Msg("processSendQueue: UpdateSendJob timeout")
|
||||
}
|
||||
continue // try the next pending job
|
||||
}
|
||||
|
||||
// runRetries is allocated fresh every call so it never accumulates
|
||||
// across processSendQueue invocations.
|
||||
runRetries := make([]int, len(job.Servers))
|
||||
serverIdx, sendErr := attemptSendJob(job, runRetries)
|
||||
if sendErr == nil {
|
||||
now := time.Now().UTC()
|
||||
job.Status = client.SendStatusSent
|
||||
job.SentAt = &now
|
||||
job.SuccessfulServer = &serverIdx
|
||||
if err := client.UpdateSendJob(storagePath, queue, job); err != nil {
|
||||
logger.Error().Err(err).Int64("id", job.ID).Msg("processSendQueue: UpdateSendJob sent")
|
||||
}
|
||||
sent++
|
||||
continue // job delivered – look for the next one
|
||||
}
|
||||
|
||||
// All servers failed this run; stop and wait for the next poll.
|
||||
// Permanent failure is decided solely by the TTL check above.
|
||||
return sent
|
||||
}
|
||||
}
|
||||
|
||||
// attemptSendJob reads the pre-built packed message from job.File and tries
|
||||
// each server in order, skipping any server that has already reached
|
||||
// maxRetriesPerServer failures within the current run.
|
||||
// On the first successful POST it returns the server index.
|
||||
// Retry counts are tracked in the caller-supplied retries slice (run-local,
|
||||
// never persisted) so that previous runs do not influence this attempt.
|
||||
func attemptSendJob(job *client.SendJob, retries []int) (int, error) {
|
||||
data, err := os.ReadFile(job.File)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
// Ensure the retries slice is aligned with the servers slice.
|
||||
for len(retries) < len(job.Servers) {
|
||||
retries = append(retries, 0)
|
||||
}
|
||||
|
||||
for i, srv := range job.Servers {
|
||||
if retries[i] >= maxRetriesPerServer {
|
||||
continue // this server is exhausted for the current run
|
||||
}
|
||||
|
||||
// Unmarshal the stored PackedUserMessage and wrap it for this server.
|
||||
packedUsrMsg := &meowlib.PackedUserMessage{}
|
||||
if err := proto.Unmarshal(data, packedUsrMsg); err != nil {
|
||||
return -1, err
|
||||
}
|
||||
serverData, errTxt, packErr := PackMessageForServer(packedUsrMsg, srv.GetUid())
|
||||
if packErr != nil {
|
||||
logger.Error().Err(packErr).Str("errTxt", errTxt).Str("url", srv.Url).Msg("attemptSendJob: PackMessageForServer")
|
||||
retries[i]++
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = meowlib.HttpPostMessage(srv.Url, serverData, defaultPostTimeout)
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Str("url", srv.Url).Int("retry", retries[i]+1).Msg("attemptSendJob: POST failed")
|
||||
retries[i]++
|
||||
continue
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
return -1, errors.New("all servers failed or exhausted")
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"forge.redroom.link/yves/meowlib"
|
||||
"forge.redroom.link/yves/meowlib/client"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
// --- test helpers -------------------------------------------------------
|
||||
|
||||
// acceptServer starts an httptest server that counts received POST /msg requests.
|
||||
func acceptServer(t *testing.T, received *int64) *httptest.Server {
|
||||
t.Helper()
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
atomic.AddInt64(received, 1)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
}
|
||||
|
||||
// closedServerURL starts and immediately closes an httptest server so its URL
|
||||
// causes "connection refused" without any wait.
|
||||
func closedServerURL(t *testing.T) string {
|
||||
t.Helper()
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
||||
srv.Close()
|
||||
return srv.URL
|
||||
}
|
||||
|
||||
// writeMsgFile writes a valid serialised empty PackedUserMessage to a temp file
|
||||
// and returns the path. The file content satisfies proto.Unmarshal inside
|
||||
// attemptSendJob; the httptest endpoints ignore the encrypted payload.
|
||||
func writeMsgFile(t *testing.T, dir, name string) string {
|
||||
t.Helper()
|
||||
p := filepath.Join(dir, name)
|
||||
data, err := proto.Marshal(&meowlib.PackedUserMessage{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, os.WriteFile(p, data, 0600))
|
||||
return p
|
||||
}
|
||||
|
||||
// newTestServer creates a client.Server for the given URL, generates a
|
||||
// throwaway keypair so that AsymEncryptMessage succeeds, and stores the server
|
||||
// in the current identity's MessageServers so that PackMessageForServer can
|
||||
// look it up via LoadServer. Returns the registered server.
|
||||
//
|
||||
// Call setupMsgHelperConfig before this so an identity is in place.
|
||||
func newTestServer(t *testing.T, url string) client.Server {
|
||||
t.Helper()
|
||||
srv, err := client.CreateServerFromUrl(url)
|
||||
require.NoError(t, err)
|
||||
kp, err := meowlib.NewKeyPair()
|
||||
require.NoError(t, err)
|
||||
srv.PublicKey = kp.Public
|
||||
require.NoError(t, client.GetConfig().GetIdentity().MessageServers.StoreServer(srv))
|
||||
return *srv
|
||||
}
|
||||
|
||||
// pushJob is a convenience wrapper around client.PushSendJob.
|
||||
func pushJob(t *testing.T, dir, queue, file string, servers []client.Server, timeout int) {
|
||||
t.Helper()
|
||||
require.NoError(t, client.PushSendJob(dir, &client.SendJob{
|
||||
Queue: queue,
|
||||
File: file,
|
||||
Servers: servers,
|
||||
Timeout: timeout,
|
||||
}))
|
||||
}
|
||||
|
||||
// serverSlice builds a []client.Server from plain URLs.
|
||||
func serverSlice(urls ...string) []client.Server {
|
||||
out := make([]client.Server, len(urls))
|
||||
for i, u := range urls {
|
||||
out[i] = client.Server{Url: u}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// --- unit tests ---------------------------------------------------------
|
||||
|
||||
// TestAttemptSendJob_Success verifies a successful POST to the first server.
|
||||
func TestAttemptSendJob_Success(t *testing.T) {
|
||||
dir, _ := setupMsgHelperConfig(t)
|
||||
var received int64
|
||||
srv := acceptServer(t, &received)
|
||||
defer srv.Close()
|
||||
|
||||
newTestServer(t, srv.URL)
|
||||
|
||||
job := &client.SendJob{
|
||||
File: writeMsgFile(t, dir, "msg"),
|
||||
Servers: serverSlice(srv.URL),
|
||||
Timeout: 5,
|
||||
}
|
||||
retries := make([]int, len(job.Servers))
|
||||
|
||||
idx, err := attemptSendJob(job, retries)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, idx)
|
||||
assert.Equal(t, int64(1), atomic.LoadInt64(&received))
|
||||
}
|
||||
|
||||
// TestAttemptSendJob_Fallback verifies that when the first server refuses the
|
||||
// connection, the second server is tried and succeeds.
|
||||
func TestAttemptSendJob_Fallback(t *testing.T) {
|
||||
dir, _ := setupMsgHelperConfig(t)
|
||||
var received int64
|
||||
good := acceptServer(t, &received)
|
||||
defer good.Close()
|
||||
|
||||
deadURL := closedServerURL(t)
|
||||
newTestServer(t, deadURL)
|
||||
newTestServer(t, good.URL)
|
||||
|
||||
job := &client.SendJob{
|
||||
File: writeMsgFile(t, dir, "msg"),
|
||||
Servers: serverSlice(deadURL, good.URL),
|
||||
Timeout: 5,
|
||||
}
|
||||
retries := make([]int, len(job.Servers))
|
||||
|
||||
idx, err := attemptSendJob(job, retries)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, idx, "second server should have been used")
|
||||
assert.Equal(t, int64(1), atomic.LoadInt64(&received))
|
||||
assert.Equal(t, 1, retries[0], "first server retry should be incremented")
|
||||
assert.Equal(t, 0, retries[1], "second server retry must stay at zero")
|
||||
}
|
||||
|
||||
// TestAttemptSendJob_AllFail verifies that all retry counts are incremented
|
||||
// and an error is returned when every server refuses connections.
|
||||
func TestAttemptSendJob_AllFail(t *testing.T) {
|
||||
dir, _ := setupMsgHelperConfig(t)
|
||||
dead1 := closedServerURL(t)
|
||||
dead2 := closedServerURL(t)
|
||||
newTestServer(t, dead1)
|
||||
newTestServer(t, dead2)
|
||||
|
||||
job := &client.SendJob{
|
||||
File: writeMsgFile(t, dir, "msg"),
|
||||
Servers: serverSlice(dead1, dead2),
|
||||
Timeout: 5,
|
||||
}
|
||||
retries := make([]int, len(job.Servers))
|
||||
|
||||
_, err := attemptSendJob(job, retries)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, 1, retries[0])
|
||||
assert.Equal(t, 1, retries[1])
|
||||
}
|
||||
|
||||
// TestAttemptSendJob_SkipsExhaustedServer verifies that a server already at
|
||||
// maxRetriesPerServer is not contacted.
|
||||
func TestAttemptSendJob_SkipsExhaustedServer(t *testing.T) {
|
||||
dir, _ := setupMsgHelperConfig(t)
|
||||
var received int64
|
||||
good := acceptServer(t, &received)
|
||||
defer good.Close()
|
||||
|
||||
deadURL := closedServerURL(t)
|
||||
newTestServer(t, good.URL) // only good server needs to be reachable
|
||||
|
||||
job := &client.SendJob{
|
||||
File: writeMsgFile(t, dir, "msg"),
|
||||
Servers: serverSlice(
|
||||
deadURL, // exhausted – must be skipped (no need to store in identity)
|
||||
good.URL,
|
||||
),
|
||||
Timeout: 5,
|
||||
}
|
||||
retries := []int{maxRetriesPerServer, 0} // first server already exhausted this run
|
||||
|
||||
idx, err := attemptSendJob(job, retries)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, idx)
|
||||
assert.Equal(t, int64(1), atomic.LoadInt64(&received))
|
||||
}
|
||||
|
||||
// --- integration tests --------------------------------------------------
|
||||
|
||||
// TestCreateUserMessageAndSendJob verifies that the packed message is written to
|
||||
// outbox/{dbFile}_{dbId} and a pending send job is enqueued for the peer.
|
||||
func TestCreateUserMessageAndSendJob(t *testing.T) {
|
||||
dir, id := setupMsgHelperConfig(t)
|
||||
|
||||
peer := newFullyKeyedPeer(t, "peer-create-send")
|
||||
require.NoError(t, id.Peers.StorePeer(peer))
|
||||
|
||||
srv := newTestServer(t, "http://test-srv.example")
|
||||
|
||||
err := CreateUserMessageAndSendJob(
|
||||
dir,
|
||||
"hello from integration",
|
||||
"peer-create-send",
|
||||
"",
|
||||
nil,
|
||||
[]client.Server{srv},
|
||||
60,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
// A pending job must be in the queue.
|
||||
job, _, err := client.PeekSendJob(dir, "peer-create-send")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, job, "a send job must be enqueued")
|
||||
|
||||
// The outbox file must exist under storagePath/outbox/.
|
||||
assert.FileExists(t, job.File)
|
||||
assert.True(t, strings.HasPrefix(job.File, filepath.Join(dir, "outbox")),
|
||||
"outbox file must be under storagePath/outbox/")
|
||||
|
||||
// The basename must follow the {dbFile}_{dbId} naming convention.
|
||||
base := filepath.Base(job.File)
|
||||
sep := strings.LastIndex(base, "_")
|
||||
require.Greater(t, sep, 0, "filename must contain an underscore separating dbFile from dbId")
|
||||
dbId, parseErr := strconv.ParseInt(base[sep+1:], 10, 64)
|
||||
assert.NoError(t, parseErr, "suffix after underscore must be a numeric db ID")
|
||||
assert.Greater(t, dbId, int64(0), "db ID must be positive")
|
||||
}
|
||||
|
||||
// TestProcessSendQueues_Success verifies that a pending job is delivered and
|
||||
// marked as sent when the server accepts it.
|
||||
func TestProcessSendQueues_Success(t *testing.T) {
|
||||
dir, _ := setupMsgHelperConfig(t)
|
||||
var received int64
|
||||
srv := acceptServer(t, &received)
|
||||
defer srv.Close()
|
||||
|
||||
newTestServer(t, srv.URL)
|
||||
|
||||
msgPath := writeMsgFile(t, dir, "msg")
|
||||
pushJob(t, dir, "q1", msgPath, serverSlice(srv.URL), 10)
|
||||
|
||||
// grab the ID before processing so we can inspect the row afterward
|
||||
_, id, err := client.PeekSendJob(dir, "q1")
|
||||
require.NoError(t, err)
|
||||
|
||||
ProcessSendQueues(dir)
|
||||
|
||||
assert.Equal(t, int64(1), atomic.LoadInt64(&received), "server should have received exactly one message")
|
||||
|
||||
job, err := client.GetSendJob(dir, "q1", id)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, job)
|
||||
assert.Equal(t, client.SendStatusSent, job.Status)
|
||||
assert.NotNil(t, job.SentAt)
|
||||
require.NotNil(t, job.SuccessfulServer)
|
||||
assert.Equal(t, 0, *job.SuccessfulServer)
|
||||
}
|
||||
|
||||
// TestProcessSendQueues_ServerFallback verifies that when the first server is
|
||||
// unreachable, the second server is tried successfully in the same pass.
|
||||
func TestProcessSendQueues_ServerFallback(t *testing.T) {
|
||||
dir, _ := setupMsgHelperConfig(t)
|
||||
var received int64
|
||||
good := acceptServer(t, &received)
|
||||
defer good.Close()
|
||||
|
||||
deadURL := closedServerURL(t)
|
||||
newTestServer(t, deadURL)
|
||||
newTestServer(t, good.URL)
|
||||
|
||||
msgPath := writeMsgFile(t, dir, "msg")
|
||||
pushJob(t, dir, "q1", msgPath, serverSlice(deadURL, good.URL), 10)
|
||||
|
||||
_, id, err := client.PeekSendJob(dir, "q1")
|
||||
require.NoError(t, err)
|
||||
|
||||
ProcessSendQueues(dir)
|
||||
|
||||
assert.Equal(t, int64(1), atomic.LoadInt64(&received))
|
||||
|
||||
job, err := client.GetSendJob(dir, "q1", id)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, job)
|
||||
assert.Equal(t, client.SendStatusSent, job.Status)
|
||||
require.NotNil(t, job.SuccessfulServer)
|
||||
assert.Equal(t, 1, *job.SuccessfulServer, "second server should be recorded as successful")
|
||||
}
|
||||
|
||||
// TestProcessSendQueues_FailedRunsStayPending verifies that repeated delivery
|
||||
// failures do NOT mark a job as permanently failed. Only a TTL timeout can do
|
||||
// that; retry exhaustion merely stops the current run.
|
||||
func TestProcessSendQueues_FailedRunsStayPending(t *testing.T) {
|
||||
dir, _ := setupMsgHelperConfig(t)
|
||||
deadURL := closedServerURL(t)
|
||||
newTestServer(t, deadURL)
|
||||
|
||||
msgPath := writeMsgFile(t, dir, "msg")
|
||||
// timeout=0 → uses defaultSendTimeout (24 h), so the job won't expire here.
|
||||
pushJob(t, dir, "q1", msgPath, serverSlice(deadURL), 0)
|
||||
|
||||
_, id, err := client.PeekSendJob(dir, "q1")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Run several times – per-server retry counts reset each run, so the job
|
||||
// must remain pending no matter how many runs fail.
|
||||
for i := 0; i < maxRetriesPerServer+2; i++ {
|
||||
ProcessSendQueues(dir)
|
||||
}
|
||||
|
||||
job, err := client.GetSendJob(dir, "q1", id)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, job)
|
||||
assert.Equal(t, client.SendStatusPending, job.Status, "repeated failures must not cause permanent failure – only timeout does")
|
||||
}
|
||||
|
||||
// TestProcessSendQueues_JobTimeout verifies that a job whose timeout has elapsed
|
||||
// is immediately marked as failed without any send attempt.
|
||||
func TestProcessSendQueues_JobTimeout(t *testing.T) {
|
||||
dir, _ := setupMsgHelperConfig(t)
|
||||
var received int64
|
||||
srv := acceptServer(t, &received)
|
||||
defer srv.Close()
|
||||
|
||||
newTestServer(t, srv.URL)
|
||||
|
||||
msgPath := writeMsgFile(t, dir, "msg")
|
||||
// Timeout of 1 second; we will backdate inserted_at so the job looks expired.
|
||||
pushJob(t, dir, "q1", msgPath, serverSlice(srv.URL), 1)
|
||||
|
||||
_, id, err := client.PeekSendJob(dir, "q1")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Backdate inserted_at by 60 seconds directly in the DB.
|
||||
dbPath := filepath.Join(dir, "queues", "q1")
|
||||
backdateJob(t, dbPath, id, -60*time.Second)
|
||||
|
||||
ProcessSendQueues(dir)
|
||||
|
||||
assert.Equal(t, int64(0), atomic.LoadInt64(&received), "no send should be attempted for an expired job")
|
||||
|
||||
job, err := client.GetSendJob(dir, "q1", id)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, job)
|
||||
assert.Equal(t, client.SendStatusFailed, job.Status)
|
||||
}
|
||||
|
||||
// TestProcessSendQueues_MultipleQueues verifies that jobs in different queue
|
||||
// files are processed concurrently and independently.
|
||||
func TestProcessSendQueues_MultipleQueues(t *testing.T) {
|
||||
dir, _ := setupMsgHelperConfig(t)
|
||||
var received int64
|
||||
srv := acceptServer(t, &received)
|
||||
defer srv.Close()
|
||||
|
||||
newTestServer(t, srv.URL)
|
||||
|
||||
for _, q := range []string{"qa", "qb", "qc"} {
|
||||
msgPath := writeMsgFile(t, dir, "msg_"+q)
|
||||
pushJob(t, dir, q, msgPath, serverSlice(srv.URL), 10)
|
||||
}
|
||||
|
||||
// Concurrent goroutines for each queue all try to open the same BadgerDB for
|
||||
// server lookup; only one can hold the lock at a time. Jobs that lose the
|
||||
// race stay pending and are retried on the next call. Three passes guarantee
|
||||
// every queue gets at least one uncontested turn.
|
||||
for i := 0; i < 3; i++ {
|
||||
ProcessSendQueues(dir)
|
||||
}
|
||||
|
||||
assert.Equal(t, int64(3), atomic.LoadInt64(&received), "all three queues should have delivered their message")
|
||||
}
|
||||
|
||||
// backdateJob opens the SQLite file directly and shifts inserted_at by delta.
|
||||
// This lets tests simulate elapsed time without sleeping.
|
||||
func backdateJob(t *testing.T, dbPath string, id int64, delta time.Duration) {
|
||||
t.Helper()
|
||||
db, err := sql.Open("sqlite3", dbPath)
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
newTs := time.Now().Add(delta).Unix()
|
||||
_, err = db.Exec("UPDATE queue SET inserted_at = ? WHERE id = ?", newTs, id)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"forge.redroom.link/yves/meowlib"
|
||||
"forge.redroom.link/yves/meowlib/client"
|
||||
)
|
||||
|
||||
func BuildCallRequestMessage(users []string, expiry uint64, srvuid string) ([]byte, string, error) {
|
||||
// Server: get the invitation server
|
||||
server, err := client.GetConfig().GetIdentity().MessageServers.LoadServer(srvuid)
|
||||
if err != nil {
|
||||
return nil, "BuildCallRequestMessage: LoadServer", err
|
||||
}
|
||||
|
||||
toSrvMsg, err := server.BuildVideoRoomRequestMessage(users, expiry)
|
||||
if err != nil {
|
||||
return nil, "BuildCallRequestMessage: BuildVideoRoomRequestMessage", err
|
||||
}
|
||||
msg, err := server.ProcessOutboundMessage(toSrvMsg)
|
||||
if err != nil {
|
||||
return nil, "BuildCallRequestMessage: ProcessOutboundMessage", err
|
||||
}
|
||||
return msg, "", nil
|
||||
}
|
||||
|
||||
func ReadCallRequestResponseMessage(data []byte, srvuid string) (*meowlib.VideoData, string, error) {
|
||||
server, err := client.GetConfig().GetIdentity().MessageServers.LoadServer(srvuid)
|
||||
if err != nil {
|
||||
return nil, "ReadCallRequestResponseMessage: LoadServer", err
|
||||
}
|
||||
// Server inbound processing : get the invitation server
|
||||
serverMsg, err := server.ProcessInboundServerResponse(data)
|
||||
if err != nil {
|
||||
return nil, "ReadCallRequestResponseMessage: ProcessInboundServerResponse", err
|
||||
}
|
||||
return serverMsg.VideoData, "", nil
|
||||
}
|
||||
|
||||
/*
|
||||
func BuildCallMessage(videodata *meowlib.VideoData, srvuid string, peer_uid string, replyToUid string, filelist []string) ([]byte, string, error) {
|
||||
peer := client.GetConfig().GetIdentity().Peers.GetFromUid(peer_uid)
|
||||
|
||||
// Creating User message
|
||||
usermessage, err := peer.BuildSimpleUserMessage(nil)
|
||||
if err != nil {
|
||||
return nil, "BuildCallMessage : BuildSimpleUserMessage", err
|
||||
}
|
||||
|
||||
usermessage.Status.AnswerToUuid = replyToUid
|
||||
|
||||
return messageBuildPackAndStore(usermessage, srvuid, peer)
|
||||
}
|
||||
|
||||
func BuildCancelCallMessage() {
|
||||
|
||||
}
|
||||
*/
|
||||
@@ -0,0 +1 @@
|
||||
package helpers
|
||||
@@ -0,0 +1,110 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"forge.redroom.link/yves/meowlib"
|
||||
"forge.redroom.link/yves/meowlib/client"
|
||||
)
|
||||
|
||||
// InvitationStep2Answer creates the invitee's peer from an InvitationInitPayload and returns
|
||||
// the new peer (STEP_2, invitee side — in-memory, no server involved).
|
||||
func InvitationStep2Answer(payload *meowlib.InvitationInitPayload, nickname string, myNickname string, serverUids []string) (*client.Peer, string, error) {
|
||||
mynick := myNickname
|
||||
if myNickname == "" {
|
||||
mynick = client.GetConfig().GetIdentity().Nickname
|
||||
}
|
||||
peer, err := client.GetConfig().GetIdentity().InvitationStep2(mynick, nickname, serverUids, payload)
|
||||
if err != nil {
|
||||
return nil, "InvitationStep2Answer: InvitationStep2", err
|
||||
}
|
||||
client.GetConfig().GetIdentity().Save()
|
||||
return peer, "", nil
|
||||
}
|
||||
|
||||
// InvitationStep2AnswerFile reads an InvitationInitPayload from a .mwiv file and creates the
|
||||
// invitee's peer. It also writes the invitee's ContactCard response to a file (STEP_2_SEND, file variant).
|
||||
func InvitationStep2AnswerFile(invitationFile string, nickname string, myNickname string, serverUids []string) (string, error) {
|
||||
if _, err := os.Stat(invitationFile); os.IsNotExist(err) {
|
||||
return "InvitationStep2AnswerFile: os.Stat", err
|
||||
}
|
||||
if !strings.HasSuffix(invitationFile, ".mwiv") {
|
||||
return "InvitationStep2AnswerFile: unsupported format", errors.New("only .mwiv files are supported")
|
||||
}
|
||||
data, err := os.ReadFile(invitationFile)
|
||||
if err != nil {
|
||||
return "InvitationStep2AnswerFile: os.ReadFile", err
|
||||
}
|
||||
payload, err := meowlib.NewInvitationInitPayloadFromCompressed(data)
|
||||
if err != nil {
|
||||
return "InvitationStep2AnswerFile: NewInvitationInitPayloadFromCompressed", err
|
||||
}
|
||||
|
||||
mynick := myNickname
|
||||
if myNickname == "" {
|
||||
mynick = client.GetConfig().GetIdentity().Nickname
|
||||
}
|
||||
c := client.GetConfig()
|
||||
response, err := c.GetIdentity().InvitationStep2(mynick, nickname, serverUids, payload)
|
||||
if err != nil {
|
||||
return "InvitationStep2AnswerFile: InvitationStep2", err
|
||||
}
|
||||
|
||||
filename := c.StoragePath + string(os.PathSeparator) + mynick + "-" + nickname + ".mwiv"
|
||||
if err := response.GetMyContact().WriteCompressed(filename); err != nil {
|
||||
return "InvitationStep2AnswerFile: WriteCompressed", err
|
||||
}
|
||||
c.GetIdentity().Save()
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// InvitationStep2AnswerMessage builds and returns the packed server message that posts the
|
||||
// invitee's ContactCard (encrypted with the initiator's temp key) to the invitation server
|
||||
// (STEP_2_SEND, through-server variant).
|
||||
func InvitationStep2AnswerMessage(invitationId string, invitationServerUid string, timeout int) ([]byte, string, error) {
|
||||
peer := client.GetConfig().GetIdentity().Peers.GetFromInvitationId(invitationId)
|
||||
if peer == nil {
|
||||
return nil, "InvitationStep2AnswerMessage: peer not found", errors.New("no peer with that invitation id")
|
||||
}
|
||||
|
||||
answermsg, err := peer.BuildInvitationStep2Message(peer.GetMyContact())
|
||||
if err != nil {
|
||||
return nil, "InvitationStep2AnswerMessage: BuildInvitationStep2Message", err
|
||||
}
|
||||
|
||||
invitationServer, err := client.GetConfig().GetIdentity().MessageServers.LoadServer(invitationServerUid)
|
||||
if err != nil {
|
||||
return nil, "InvitationStep2AnswerMessage: LoadServer", err
|
||||
}
|
||||
|
||||
packedMsg, err := peer.ProcessOutboundUserMessage(answermsg)
|
||||
if err != nil {
|
||||
return nil, "InvitationStep2AnswerMessage: ProcessOutboundUserMessage", err
|
||||
}
|
||||
|
||||
toServerMessage, err := invitationServer.BuildToServerMessageInvitationAnswer(packedMsg, peer.MyIdentity.Public, invitationId, timeout)
|
||||
if err != nil {
|
||||
return nil, "InvitationStep2AnswerMessage: BuildToServerMessageInvitationAnswer", err
|
||||
}
|
||||
|
||||
bytemsg, err := invitationServer.ProcessOutboundMessage(toServerMessage)
|
||||
if err != nil {
|
||||
return nil, "InvitationStep2AnswerMessage: ProcessOutboundMessage", err
|
||||
}
|
||||
return bytemsg, "", nil
|
||||
}
|
||||
|
||||
// InvitationStep2AnswerMessageReadResponse reads the server acknowledgement of a Step2 answer.
|
||||
func InvitationStep2AnswerMessageReadResponse(invitationData []byte, invitationServerUid string) (*meowlib.Invitation, string, error) {
|
||||
server, err := client.GetConfig().GetIdentity().MessageServers.LoadServer(invitationServerUid)
|
||||
if err != nil {
|
||||
return nil, "InvitationStep2AnswerMessageReadResponse: LoadServer", err
|
||||
}
|
||||
serverMsg, err := server.ProcessInboundServerResponse(invitationData)
|
||||
if err != nil {
|
||||
return nil, "InvitationStep2AnswerMessageReadResponse: ProcessInboundServerResponse", err
|
||||
}
|
||||
return serverMsg.Invitation, "", nil
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"forge.redroom.link/yves/meowlib"
|
||||
"forge.redroom.link/yves/meowlib/client"
|
||||
)
|
||||
|
||||
// InvitationStep2GetMessage builds and returns the packed server message that retrieves
|
||||
// the InvitationInitPayload from the server using the shortcode URL (STEP_2, invitee side).
|
||||
func InvitationStep2GetMessage(invitationUrl string, serverPublicKey string, invitationPassword string) ([]byte, string, error) {
|
||||
meowurl := strings.Split(invitationUrl, "?")
|
||||
shortcode := meowurl[1]
|
||||
|
||||
srv, err := client.CreateServerFromMeowUrl(meowurl[0])
|
||||
if err != nil {
|
||||
return nil, "InvitationStep2GetMessage: CreateServerFromMeowUrl", err
|
||||
}
|
||||
|
||||
// Reuse the server entry if already known.
|
||||
dbsrv, err := client.GetConfig().GetIdentity().MessageServers.LoadServer(srv.Url)
|
||||
if err != nil {
|
||||
return nil, "InvitationStep2GetMessage: LoadServer", err
|
||||
}
|
||||
if dbsrv == nil {
|
||||
srv.PublicKey = serverPublicKey
|
||||
k, err := meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
return nil, "InvitationStep2GetMessage: NewKeyPair", err
|
||||
}
|
||||
srv.UserKp = k
|
||||
if err := client.GetConfig().GetIdentity().MessageServers.StoreServer(srv); err != nil {
|
||||
return nil, "InvitationStep2GetMessage: StoreServer", err
|
||||
}
|
||||
} else {
|
||||
if dbsrv.PublicKey != serverPublicKey {
|
||||
dbsrv.PublicKey = serverPublicKey
|
||||
}
|
||||
srv = dbsrv
|
||||
}
|
||||
|
||||
toSrvMsg, err := srv.BuildToServerMessageInvitationRequest(shortcode, invitationPassword)
|
||||
if err != nil {
|
||||
return nil, "InvitationStep2GetMessage: BuildToServerMessageInvitationRequest", err
|
||||
}
|
||||
bytemsg, err := srv.ProcessOutboundMessage(toSrvMsg)
|
||||
if err != nil {
|
||||
return nil, "InvitationStep2GetMessage: ProcessOutboundMessage", err
|
||||
}
|
||||
return bytemsg, "", nil
|
||||
}
|
||||
|
||||
// InvitationStep2ReadResponse decodes the server response to a Step2 get-message and returns
|
||||
// the InvitationInitPayload sent by the initiator.
|
||||
func InvitationStep2ReadResponse(invitationData []byte, invitationServerUid string) (*meowlib.InvitationInitPayload, string, error) {
|
||||
server, err := client.GetConfig().GetIdentity().MessageServers.LoadServer(invitationServerUid)
|
||||
if err != nil {
|
||||
return nil, "InvitationStep2ReadResponse: LoadServer", err
|
||||
}
|
||||
serverMsg, err := server.ProcessInboundServerResponse(invitationData)
|
||||
if err != nil {
|
||||
return nil, "InvitationStep2ReadResponse: ProcessInboundServerResponse", err
|
||||
}
|
||||
payload, err := meowlib.NewInvitationInitPayloadFromCompressed(serverMsg.Invitation.Payload)
|
||||
if err != nil {
|
||||
return nil, "InvitationStep2ReadResponse: NewInvitationInitPayloadFromCompressed", err
|
||||
}
|
||||
return payload, "", nil
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"forge.redroom.link/yves/meowlib"
|
||||
"forge.redroom.link/yves/meowlib/client"
|
||||
)
|
||||
|
||||
// InvitationStep1CreatePeer creates a minimal pending peer and returns the InvitationInitPayload
|
||||
// to be transmitted to the invitee (STEP_1).
|
||||
func InvitationStep1CreatePeer(contactName string, myNickname string, invitationMessage string, serverUids []string) (*meowlib.InvitationInitPayload, *client.Peer, string, error) {
|
||||
mynick := myNickname
|
||||
if myNickname == "" {
|
||||
mynick = client.GetConfig().GetIdentity().Nickname
|
||||
}
|
||||
payload, peer, err := client.GetConfig().GetIdentity().InvitationStep1(mynick, contactName, serverUids, invitationMessage)
|
||||
if err != nil {
|
||||
return nil, nil, "InvitationStep1CreatePeer: InvitationStep1", err
|
||||
}
|
||||
client.GetConfig().GetIdentity().Save()
|
||||
return payload, peer, "", nil
|
||||
}
|
||||
|
||||
// InvitationStep1File creates a pending peer and writes the InvitationInitPayload to a file
|
||||
// (format: "qr" for QR-code PNG, anything else for compressed binary .mwiv).
|
||||
func InvitationStep1File(contactName string, myNickname string, invitationMessage string, serverUids []string, format string) (*client.Peer, string, error) {
|
||||
payload, peer, errdata, err := InvitationStep1CreatePeer(contactName, myNickname, invitationMessage, serverUids)
|
||||
if err != nil {
|
||||
return nil, errdata, err
|
||||
}
|
||||
c := client.GetConfig()
|
||||
if format == "qr" {
|
||||
filename := c.StoragePath + string(os.PathSeparator) + peer.MyName + "-" + peer.Name + ".png"
|
||||
if err := payload.WriteQr(filename); err != nil {
|
||||
return nil, "InvitationStep1File: WriteQr", err
|
||||
}
|
||||
} else {
|
||||
filename := c.StoragePath + string(os.PathSeparator) + peer.MyName + "-" + peer.Name + ".mwiv"
|
||||
if err := payload.WriteCompressed(filename); err != nil {
|
||||
return nil, "InvitationStep1File: WriteCompressed", err
|
||||
}
|
||||
}
|
||||
return peer, "", nil
|
||||
}
|
||||
|
||||
// InvitationStep1Message builds and returns the packed server message that posts the
|
||||
// InvitationInitPayload to the invitation server (STEP_1 through-server variant).
|
||||
func InvitationStep1Message(invitationId string, invitationServerUid string, timeOut int, urlLen int, password string) ([]byte, string, error) {
|
||||
peer := client.GetConfig().GetIdentity().Peers.GetFromInvitationId(invitationId)
|
||||
if peer == nil {
|
||||
return nil, "InvitationStep1Message: peer not found", nil
|
||||
}
|
||||
if peer.InvitationKp == nil {
|
||||
return nil, "InvitationStep1Message: peer has no InvitationKp", nil
|
||||
}
|
||||
initPayload := &meowlib.InvitationInitPayload{
|
||||
Uuid: peer.InvitationId,
|
||||
Name: peer.MyName,
|
||||
PublicKey: peer.InvitationKp.Public,
|
||||
InvitationMessage: peer.InvitationMessage,
|
||||
}
|
||||
invitationServer, err := client.GetConfig().GetIdentity().MessageServers.LoadServer(invitationServerUid)
|
||||
if err != nil {
|
||||
return nil, "InvitationStep1Message: LoadServer", err
|
||||
}
|
||||
msg, err := invitationServer.BuildToServerMessageInvitationStep1(initPayload, password, timeOut, urlLen)
|
||||
if err != nil {
|
||||
return nil, "InvitationStep1Message: BuildToServerMessageInvitationStep1", err
|
||||
}
|
||||
bytemsg, err := invitationServer.ProcessOutboundMessage(msg)
|
||||
if err != nil {
|
||||
return nil, "InvitationStep1Message: ProcessOutboundMessage", err
|
||||
}
|
||||
return bytemsg, "", nil
|
||||
}
|
||||
|
||||
// InvitationStep1ReadResponse reads the server response to a Step1 message (shortcode URL + expiry).
|
||||
func InvitationStep1ReadResponse(invitationServerUid string, invitationResponse []byte) (*meowlib.Invitation, string, error) {
|
||||
server, err := client.GetConfig().GetIdentity().MessageServers.LoadServer(invitationServerUid)
|
||||
if err != nil {
|
||||
return nil, "InvitationStep1ReadResponse: LoadServer", err
|
||||
}
|
||||
serverMsg, err := server.ProcessInboundServerResponse(invitationResponse)
|
||||
if err != nil {
|
||||
return nil, "InvitationStep1ReadResponse: ProcessInboundServerResponse", err
|
||||
}
|
||||
return serverMsg.Invitation, "", nil
|
||||
}
|
||||
|
||||
// InvitationSetUrlInfo stores the shortcode URL and expiry on the pending peer.
|
||||
func InvitationSetUrlInfo(invitationId string, url string, expiry int64) {
|
||||
id := client.GetConfig().GetIdentity()
|
||||
peer := id.Peers.GetFromInvitationId(invitationId)
|
||||
if peer == nil {
|
||||
return
|
||||
}
|
||||
peer.InvitationUrl = url
|
||||
peer.InvitationExpiry = time.Unix(expiry, 0)
|
||||
id.Peers.StorePeer(peer)
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"forge.redroom.link/yves/meowlib"
|
||||
"forge.redroom.link/yves/meowlib/client"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// InvitationStep3ProcessAnswer is called by the initiator's background service when a
|
||||
// step-2 answer (invitee's ContactCard) arrives via the invitation server poll.
|
||||
// It decrypts the answer, calls InvitationStep3 to generate the initiator's full keypairs,
|
||||
// and returns the peer and the initiator's ContactCard ready for STEP_3_SEND.
|
||||
func InvitationStep3ProcessAnswer(invitation *meowlib.Invitation) (*client.Peer, *meowlib.ContactCard, string, error) {
|
||||
var invitationAnswer meowlib.PackedUserMessage
|
||||
if err := proto.Unmarshal(invitation.Payload, &invitationAnswer); err != nil {
|
||||
return nil, nil, "InvitationStep3ProcessAnswer: Unmarshal PackedUserMessage", err
|
||||
}
|
||||
|
||||
peer := client.GetConfig().GetIdentity().Peers.GetFromInvitationId(invitation.Uuid)
|
||||
if peer == nil {
|
||||
return nil, nil, "InvitationStep3ProcessAnswer: peer not found", errors.New("no peer for invitation uuid " + invitation.Uuid)
|
||||
}
|
||||
// Guard against duplicate delivery (e.g., same answer from multiple servers).
|
||||
if peer.InvitationKp == nil {
|
||||
return nil, nil, "", nil
|
||||
}
|
||||
|
||||
// Decrypt invitee's ContactCard using the initiator's temporary InvitationKp.
|
||||
usermsg, err := peer.ProcessInboundStep2UserMessage(&invitationAnswer, invitation.From)
|
||||
if err != nil {
|
||||
return nil, nil, "InvitationStep3ProcessAnswer: ProcessInboundStep2UserMessage", err
|
||||
}
|
||||
|
||||
var inviteeCC meowlib.ContactCard
|
||||
if err := proto.Unmarshal(usermsg.Invitation.Payload, &inviteeCC); err != nil {
|
||||
return nil, nil, "InvitationStep3ProcessAnswer: Unmarshal ContactCard", err
|
||||
}
|
||||
|
||||
myCC, peer, err := client.GetConfig().GetIdentity().InvitationStep3(&inviteeCC)
|
||||
if err != nil {
|
||||
return nil, nil, "InvitationStep3ProcessAnswer: InvitationStep3", err
|
||||
}
|
||||
client.GetConfig().GetIdentity().Save()
|
||||
return peer, myCC, "", nil
|
||||
}
|
||||
|
||||
// InvitationStep3Message builds and returns the packed server messages that send the
|
||||
// initiator's full ContactCard to the invitee through the invitee's servers (STEP_3_SEND).
|
||||
func InvitationStep3Message(invitationId string) ([][]byte, string, error) {
|
||||
id := client.GetConfig().GetIdentity()
|
||||
peer := id.Peers.GetFromInvitationId(invitationId)
|
||||
if peer == nil {
|
||||
return nil, "InvitationStep3Message: peer not found", errors.New("no peer for invitation id " + invitationId)
|
||||
}
|
||||
|
||||
step3msg, err := peer.BuildInvitationStep3Message(peer.GetMyContact())
|
||||
if err != nil {
|
||||
return nil, "InvitationStep3Message: BuildInvitationStep3Message", err
|
||||
}
|
||||
// Step-3 must NOT use DR or sym layers: the invitee hasn't received those
|
||||
// keys yet (they are carried inside this very message). Use plain asym only.
|
||||
serialized, err := peer.SerializeUserMessage(step3msg)
|
||||
if err != nil {
|
||||
return nil, "InvitationStep3Message: SerializeUserMessage", err
|
||||
}
|
||||
enc, err := peer.AsymEncryptMessage(serialized)
|
||||
if err != nil {
|
||||
return nil, "InvitationStep3Message: AsymEncryptMessage", err
|
||||
}
|
||||
packedMsg := peer.PackUserMessage(enc.Data, enc.Signature)
|
||||
|
||||
var results [][]byte
|
||||
for _, srvUid := range peer.ContactPullServers {
|
||||
srv, err := id.MessageServers.LoadServer(srvUid)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
toSrvMsg := srv.BuildToServerMessageFromUserMessage(packedMsg)
|
||||
bytemsg, err := srv.ProcessOutboundMessage(toSrvMsg)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
results = append(results, bytemsg)
|
||||
}
|
||||
if len(results) == 0 {
|
||||
return nil, "InvitationStep3Message: no reachable invitee server", errors.New("could not build message for any invitee server")
|
||||
}
|
||||
return results, "", nil
|
||||
}
|
||||
|
||||
// InvitationStep4ProcessStep3 is called by the invitee's message processing when a UserMessage
|
||||
// with invitation.step==3 is received. It finalizes the initiator's peer entry.
|
||||
func InvitationStep4ProcessStep3(usermsg *meowlib.UserMessage) (*client.Peer, string, error) {
|
||||
if usermsg.Invitation == nil || usermsg.Invitation.Step != 3 {
|
||||
return nil, "InvitationStep4ProcessStep3: unexpected step", errors.New("expected invitation step 3")
|
||||
}
|
||||
var initiatorCC meowlib.ContactCard
|
||||
if err := proto.Unmarshal(usermsg.Invitation.Payload, &initiatorCC); err != nil {
|
||||
return nil, "InvitationStep4ProcessStep3: Unmarshal ContactCard", err
|
||||
}
|
||||
// Patch the invitation ID from the outer message in case it was not set in the CC.
|
||||
if initiatorCC.InvitationId == "" {
|
||||
initiatorCC.InvitationId = usermsg.Invitation.Uuid
|
||||
}
|
||||
if err := client.GetConfig().GetIdentity().InvitationStep4(&initiatorCC); err != nil {
|
||||
return nil, "InvitationStep4ProcessStep3: InvitationStep4", err
|
||||
}
|
||||
client.GetConfig().GetIdentity().Save()
|
||||
peer := client.GetConfig().GetIdentity().Peers.GetFromInvitationId(initiatorCC.InvitationId)
|
||||
return peer, "", nil
|
||||
}
|
||||
|
||||
// InvitationStep4Message builds and returns the packed server messages that send the
|
||||
// invitee's confirmation to the initiator through the initiator's servers (STEP_4).
|
||||
func InvitationStep4Message(invitationId string) ([][]byte, string, error) {
|
||||
id := client.GetConfig().GetIdentity()
|
||||
peer := id.Peers.GetFromInvitationId(invitationId)
|
||||
if peer == nil {
|
||||
return nil, "InvitationStep4Message: peer not found", errors.New("no peer for invitation id " + invitationId)
|
||||
}
|
||||
|
||||
step4msg, err := peer.BuildInvitationStep4Message()
|
||||
if err != nil {
|
||||
return nil, "InvitationStep4Message: BuildInvitationStep4Message", err
|
||||
}
|
||||
packedMsg, err := peer.ProcessOutboundUserMessage(step4msg)
|
||||
if err != nil {
|
||||
return nil, "InvitationStep4Message: ProcessOutboundUserMessage", err
|
||||
}
|
||||
|
||||
var results [][]byte
|
||||
for _, srvUid := range peer.ContactPullServers {
|
||||
srv, err := id.MessageServers.LoadServer(srvUid)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
toSrvMsg := srv.BuildToServerMessageFromUserMessage(packedMsg)
|
||||
bytemsg, err := srv.ProcessOutboundMessage(toSrvMsg)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
results = append(results, bytemsg)
|
||||
}
|
||||
if len(results) == 0 {
|
||||
return nil, "InvitationStep4Message: no reachable initiator server", errors.New("could not build message for any initiator server")
|
||||
}
|
||||
return results, "", nil
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
var logger zerolog.Logger
|
||||
|
||||
// AddLogger sets the logger for the sublibrary
|
||||
func AddLogger(l zerolog.Logger) {
|
||||
logger = l
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"forge.redroom.link/yves/meowlib"
|
||||
"forge.redroom.link/yves/meowlib/client"
|
||||
"github.com/google/uuid"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func PackMessageForServer(packedMsg *meowlib.PackedUserMessage, srvuid string) ([]byte, string, error) {
|
||||
// Get the message server
|
||||
srv, err := client.GetConfig().GetIdentity().MessageServers.LoadServer(srvuid)
|
||||
if err != nil {
|
||||
return nil, "PackMessageForServer : LoadServer", err
|
||||
}
|
||||
// Fetch and persist the server public key if it was never stored
|
||||
// (servers added via invitation finalization only have a UserKp, no PublicKey)
|
||||
if srv.PublicKey == "" {
|
||||
srvdata, err := meowlib.HttpGetId(srv.Url)
|
||||
if err != nil {
|
||||
return nil, "PackMessageForServer : HttpGetId", err
|
||||
}
|
||||
srv.PublicKey = srvdata["publicKey"]
|
||||
client.GetConfig().GetIdentity().MessageServers.StoreServer(srv)
|
||||
}
|
||||
// Creating Server message for transporting the user message
|
||||
toServerMessage := srv.BuildToServerMessageFromUserMessage(packedMsg)
|
||||
data, err := srv.ProcessOutboundMessage(toServerMessage)
|
||||
if err != nil {
|
||||
return nil, "PackMessageForServer : ProcessOutboundMessage", err
|
||||
}
|
||||
return data, "", nil
|
||||
|
||||
}
|
||||
|
||||
func CreateStorePackUserMessageForServer(message string, srvuid string, peer_uid string, replyToUid string, filelist []string) ([]byte, string, error) {
|
||||
usermessage, _, _, errtxt, err := CreateAndStoreUserMessage(message, peer_uid, replyToUid, filelist)
|
||||
if err != nil {
|
||||
return nil, errtxt, err
|
||||
}
|
||||
return PackMessageForServer(usermessage, srvuid)
|
||||
}
|
||||
|
||||
// CreateAndStoreUserMessage creates, signs, and stores an outbound message for
|
||||
// peer_uid. It returns the packed (encrypted) form ready for server transport,
|
||||
// the peer DB file UUID (dbFile), the SQLite row ID (dbId), an error context
|
||||
// string, and any error.
|
||||
func CreateAndStoreUserMessage(message string, peer_uid string, replyToUid string, filelist []string) (*meowlib.PackedUserMessage, string, int64, string, error) {
|
||||
peer := client.GetConfig().GetIdentity().Peers.GetFromUid(peer_uid)
|
||||
|
||||
// Creating User message
|
||||
usermessage, err := peer.BuildSimpleUserMessage([]byte(message))
|
||||
if err != nil {
|
||||
return nil, "", 0, "PrepareServerMessage : BuildSimpleUserMessage", err
|
||||
}
|
||||
for _, file := range filelist {
|
||||
err = usermessage.AddFile(file, client.GetConfig().Chunksize)
|
||||
if err != nil {
|
||||
return nil, "", 0, "PrepareServerMessage : AddFile", err
|
||||
}
|
||||
}
|
||||
usermessage.Status.Sent = uint64(time.Now().UTC().Unix())
|
||||
usermessage.Status.ReplyToUuid = replyToUid
|
||||
|
||||
// Store message
|
||||
err = peer.StoreMessage(usermessage, nil)
|
||||
if err != nil {
|
||||
return nil, "", 0, "messageBuildPostprocess : StoreMessage", err
|
||||
}
|
||||
|
||||
dbFile := peer.LastMessage.Dbfile
|
||||
dbId := peer.LastMessage.Dbid
|
||||
|
||||
// Prepare cyphered + packed user message
|
||||
packedMsg, err := peer.ProcessOutboundUserMessage(usermessage)
|
||||
if err != nil {
|
||||
return nil, "", 0, "messageBuildPostprocess : ProcessOutboundUserMessage", err
|
||||
}
|
||||
|
||||
// Persist peer to save updated DR state (DrStateJson)
|
||||
if peer.DrRootKey != "" {
|
||||
if storeErr := client.GetConfig().GetIdentity().Peers.StorePeer(peer); storeErr != nil {
|
||||
logger.Warn().Err(storeErr).Str("peer", peer.Uid).Msg("messageBuildPostprocess: StorePeer (DR state)")
|
||||
}
|
||||
}
|
||||
|
||||
return packedMsg, dbFile, dbId, "", nil
|
||||
}
|
||||
|
||||
func BuildReceivedMessage(messageUid string, peer_uid string, received int64) (*meowlib.PackedUserMessage, string, error) {
|
||||
peer := client.GetConfig().GetIdentity().Peers.GetFromUid(peer_uid)
|
||||
// Creating User message
|
||||
usermessage, err := peer.BuildSimpleUserMessage(nil)
|
||||
if err != nil {
|
||||
return nil, "BuildReceivedMessage : BuildSimpleUserMessage", err
|
||||
}
|
||||
usermessage.Status.Uuid = messageUid
|
||||
usermessage.Status.Received = uint64(received)
|
||||
// Prepare cyphered + packed user message
|
||||
packedMsg, err := peer.ProcessOutboundUserMessage(usermessage)
|
||||
if err != nil {
|
||||
return nil, "BuildReceivedMessage : ProcessOutboundUserMessage", err
|
||||
}
|
||||
// Persist peer to save updated DR state (DrStateJson)
|
||||
if peer.DrRootKey != "" {
|
||||
client.GetConfig().GetIdentity().Peers.StorePeer(peer)
|
||||
}
|
||||
return packedMsg, "", nil
|
||||
}
|
||||
|
||||
func BuildProcessedMessage(messageUid string, peer_uid string, processed int64) (*meowlib.PackedUserMessage, string, error) {
|
||||
peer := client.GetConfig().GetIdentity().Peers.GetFromUid(peer_uid)
|
||||
// Creating User message
|
||||
usermessage, err := peer.BuildSimpleUserMessage(nil)
|
||||
if err != nil {
|
||||
return nil, "BuildProcessedMessage : BuildSimpleUserMessage", err
|
||||
}
|
||||
usermessage.Status.Uuid = messageUid
|
||||
usermessage.Status.Processed = uint64(processed)
|
||||
// Prepare cyphered + packed user message
|
||||
packedMsg, err := peer.ProcessOutboundUserMessage(usermessage)
|
||||
if err != nil {
|
||||
return nil, "BuildProcessedMessage : ProcessOutboundUserMessage", err
|
||||
}
|
||||
// Persist peer to save updated DR state (DrStateJson)
|
||||
if peer.DrRootKey != "" {
|
||||
client.GetConfig().GetIdentity().Peers.StorePeer(peer)
|
||||
}
|
||||
return packedMsg, "", nil
|
||||
}
|
||||
|
||||
func ReadAckMessageResponse() {
|
||||
//! update the status in message store
|
||||
}
|
||||
|
||||
// MarkMessageProcessed stamps the stored message with a processed timestamp of
|
||||
// now(), persists the updated record, and — if the peer has SendProcessingAck
|
||||
// enabled and the message carries a UUID — enqueues a processed acknowledgment
|
||||
// to the peer's contact pull servers.
|
||||
func MarkMessageProcessed(peerUid string, dbFile string, dbId int64) error {
|
||||
password, _ := client.GetConfig().GetMemPass()
|
||||
processedAt := time.Now().UTC().Unix()
|
||||
|
||||
dbm, err := client.GetDbMessage(dbFile, dbId, password)
|
||||
if err != nil {
|
||||
return fmt.Errorf("MarkMessageProcessed: GetDbMessage: %w", err)
|
||||
}
|
||||
if dbm.Status == nil {
|
||||
dbm.Status = &meowlib.ConversationStatus{}
|
||||
}
|
||||
dbm.Status.Processed = uint64(processedAt)
|
||||
if err := client.UpdateDbMessage(dbm, dbFile, dbId, password); err != nil {
|
||||
return fmt.Errorf("MarkMessageProcessed: UpdateDbMessage: %w", err)
|
||||
}
|
||||
|
||||
peer := client.GetConfig().GetIdentity().Peers.GetFromUid(peerUid)
|
||||
if peer == nil || !peer.SendProcessingAck || dbm.Status.Uuid == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
identity := client.GetConfig().GetIdentity()
|
||||
storagePath := filepath.Join(client.GetConfig().StoragePath, identity.Uuid)
|
||||
return sendProcessingAck(storagePath, peer, dbm.Status.Uuid, processedAt)
|
||||
}
|
||||
|
||||
// sendProcessingAck builds a processing acknowledgment for messageUuid and
|
||||
// enqueues it for sending to the peer's contact pull servers.
|
||||
func sendProcessingAck(storagePath string, peer *client.Peer, messageUuid string, processedAt int64) error {
|
||||
packedMsg, _, err := BuildProcessedMessage(messageUuid, peer.Uid, processedAt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("sendProcessingAck: BuildProcessedMessage: %w", err)
|
||||
}
|
||||
|
||||
data, err := proto.Marshal(packedMsg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("sendProcessingAck: proto.Marshal: %w", err)
|
||||
}
|
||||
|
||||
outboxDir := filepath.Join(storagePath, "outbox")
|
||||
if err := os.MkdirAll(outboxDir, 0700); err != nil {
|
||||
return fmt.Errorf("sendProcessingAck: MkdirAll: %w", err)
|
||||
}
|
||||
|
||||
outboxFile := filepath.Join(outboxDir, "ack_"+uuid.New().String())
|
||||
if err := os.WriteFile(outboxFile, data, 0600); err != nil {
|
||||
return fmt.Errorf("sendProcessingAck: WriteFile: %w", err)
|
||||
}
|
||||
|
||||
var servers []client.Server
|
||||
for _, srvUid := range peer.ContactPullServers {
|
||||
srv, loadErr := client.GetConfig().GetIdentity().MessageServers.LoadServer(srvUid)
|
||||
if loadErr == nil && srv != nil {
|
||||
servers = append(servers, *srv)
|
||||
}
|
||||
}
|
||||
if len(servers) == 0 {
|
||||
os.Remove(outboxFile)
|
||||
return errors.New("sendProcessingAck: no contact servers found")
|
||||
}
|
||||
|
||||
return client.PushSendJob(storagePath, &client.SendJob{
|
||||
Queue: peer.Uid,
|
||||
File: outboxFile,
|
||||
Servers: servers,
|
||||
})
|
||||
}
|
||||
|
||||
// ProcessSentMessages scans every send queue under storagePath/queues/, updates
|
||||
// the message storage entry with server delivery info for each sent job, then
|
||||
// removes the job from the queue. Returns the number of messages updated.
|
||||
//
|
||||
// The message DB location is recovered from the job's File basename, which must
|
||||
// follow the naming convention produced by CreateUserMessageAndSendJob:
|
||||
//
|
||||
// outbox/{dbFile}_{dbId}
|
||||
func ProcessSentMessages(storagePath string) int {
|
||||
password, _ := client.GetConfig().GetMemPass()
|
||||
queueDir := filepath.Join(storagePath, "queues")
|
||||
entries, err := os.ReadDir(queueDir)
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Str("dir", queueDir).Msg("ProcessSentMessages: ReadDir")
|
||||
return 0
|
||||
}
|
||||
|
||||
updated := 0
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
queue := entry.Name()
|
||||
|
||||
jobs, err := client.GetSentJobs(storagePath, queue)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Str("queue", queue).Msg("ProcessSentMessages: GetSentJobs")
|
||||
continue
|
||||
}
|
||||
|
||||
for _, job := range jobs {
|
||||
if job.SuccessfulServer == nil || job.SentAt == nil {
|
||||
// No delivery info – discard the job so it doesn't block the queue
|
||||
if err := client.DeleteSendJob(storagePath, queue, job.ID); err != nil {
|
||||
logger.Error().Err(err).Int64("id", job.ID).Msg("ProcessSentMessages: DeleteSendJob (incomplete)")
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Recover dbFile and dbId from the outbox filename: {dbFile}_{dbId}
|
||||
base := filepath.Base(job.File)
|
||||
sep := strings.LastIndex(base, "_")
|
||||
if sep <= 0 {
|
||||
logger.Error().Int64("id", job.ID).Str("file", job.File).
|
||||
Msg("ProcessSentMessages: cannot parse dbFile/dbId from job filename — use CreateUserMessageAndSendJob to build jobs")
|
||||
continue
|
||||
}
|
||||
dbFile := base[:sep]
|
||||
dbId, parseErr := strconv.ParseInt(base[sep+1:], 10, 64)
|
||||
if parseErr != nil || dbFile == "" || dbId == 0 {
|
||||
logger.Error().Int64("id", job.ID).Str("file", job.File).
|
||||
Msg("ProcessSentMessages: invalid dbFile/dbId in job filename")
|
||||
continue
|
||||
}
|
||||
|
||||
serverUid := job.Servers[*job.SuccessfulServer].GetUid()
|
||||
receiveTime := uint64(job.SentAt.Unix())
|
||||
|
||||
if err := client.SetMessageServerDelivery(dbFile, dbId, serverUid, receiveTime, password); err != nil {
|
||||
logger.Error().Err(err).Str("queue", queue).
|
||||
Str("dbFile", dbFile).Int64("dbId", dbId).
|
||||
Msg("ProcessSentMessages: SetMessageServerDelivery")
|
||||
continue
|
||||
}
|
||||
if err := client.DeleteSendJob(storagePath, queue, job.ID); err != nil {
|
||||
logger.Error().Err(err).Int64("id", job.ID).Msg("ProcessSentMessages: DeleteSendJob")
|
||||
}
|
||||
updated++
|
||||
}
|
||||
}
|
||||
return updated
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"forge.redroom.link/yves/meowlib"
|
||||
"forge.redroom.link/yves/meowlib/client"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
// setupMsgHelperConfig wires the global client.Config singleton to a fresh
|
||||
// temporary directory and returns it. Original values are restored in t.Cleanup.
|
||||
func setupMsgHelperConfig(t *testing.T) (dir string, id *client.Identity) {
|
||||
t.Helper()
|
||||
dir = t.TempDir()
|
||||
|
||||
cfg := client.GetConfig()
|
||||
origStorage := cfg.StoragePath
|
||||
origSuffix := cfg.DbSuffix
|
||||
origChunk := cfg.Chunksize
|
||||
|
||||
cfg.StoragePath = dir
|
||||
cfg.DbSuffix = ".sqlite"
|
||||
cfg.Chunksize = 1024 * 1024
|
||||
require.NoError(t, cfg.SetMemPass("testpassword"))
|
||||
|
||||
var err error
|
||||
id, err = client.CreateIdentity("testuser")
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
cfg.StoragePath = origStorage
|
||||
cfg.DbSuffix = origSuffix
|
||||
cfg.Chunksize = origChunk
|
||||
})
|
||||
return dir, id
|
||||
}
|
||||
|
||||
// newFullyKeyedPeer returns a Peer with all three keypairs and contact keys set,
|
||||
// ready to store messages.
|
||||
func newFullyKeyedPeer(t *testing.T, uid string) *client.Peer {
|
||||
t.Helper()
|
||||
var err error
|
||||
peer := &client.Peer{
|
||||
Uid: uid,
|
||||
Name: "TestPeer-" + uid,
|
||||
}
|
||||
peer.MyIdentity, err = meowlib.NewKeyPair()
|
||||
require.NoError(t, err)
|
||||
peer.MyEncryptionKp, err = meowlib.NewKeyPair()
|
||||
require.NoError(t, err)
|
||||
peer.MyLookupKp, err = meowlib.NewKeyPair()
|
||||
require.NoError(t, err)
|
||||
|
||||
k, err := meowlib.NewKeyPair()
|
||||
require.NoError(t, err)
|
||||
peer.ContactPublicKey = k.Public
|
||||
|
||||
k, err = meowlib.NewKeyPair()
|
||||
require.NoError(t, err)
|
||||
peer.ContactEncryption = k.Public
|
||||
|
||||
k, err = meowlib.NewKeyPair()
|
||||
require.NoError(t, err)
|
||||
peer.ContactLookupKey = k.Public
|
||||
|
||||
return peer
|
||||
}
|
||||
|
||||
// storeTestMessage stores a single outbound message for peer.
|
||||
func storeTestMessage(t *testing.T, peer *client.Peer, text string) {
|
||||
t.Helper()
|
||||
um := &meowlib.UserMessage{
|
||||
Data: []byte(text),
|
||||
From: peer.MyIdentity.Public,
|
||||
Status: &meowlib.ConversationStatus{Uuid: "uuid-" + text},
|
||||
}
|
||||
require.NoError(t, peer.StoreMessage(um, nil))
|
||||
require.NotNil(t, peer.LastMessage, "StoreMessage must set LastMessage")
|
||||
}
|
||||
|
||||
// pushAndMarkSent pushes a send job for the given peer and marks it as delivered
|
||||
// by the given server. Returns the job after the status update.
|
||||
// The outbox file is named {dbFile}_{dbId} so that ProcessSentMessages can
|
||||
// recover the message DB location from the filename, matching the convention
|
||||
// used by CreateUserMessageAndSendJob.
|
||||
func pushAndMarkSent(t *testing.T, dir string, peer *client.Peer, srv client.Server) *client.SendJob {
|
||||
t.Helper()
|
||||
|
||||
dbFile := peer.LastMessage.Dbfile
|
||||
dbId := peer.LastMessage.Dbid
|
||||
|
||||
outboxDir := filepath.Join(dir, "outbox")
|
||||
require.NoError(t, os.MkdirAll(outboxDir, 0700))
|
||||
msgFile := filepath.Join(outboxDir, fmt.Sprintf("%s_%d", dbFile, dbId))
|
||||
require.NoError(t, os.WriteFile(msgFile, []byte("packed-server-message"), 0600))
|
||||
|
||||
require.NoError(t, client.PushSendJob(dir, &client.SendJob{
|
||||
Queue: peer.Uid,
|
||||
File: msgFile,
|
||||
Servers: []client.Server{srv},
|
||||
Timeout: 60,
|
||||
}))
|
||||
|
||||
job, _, err := client.PeekSendJob(dir, peer.Uid)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, job)
|
||||
|
||||
sentAt := time.Now()
|
||||
srvIdx := 0
|
||||
job.Status = client.SendStatusSent
|
||||
job.SentAt = &sentAt
|
||||
job.SuccessfulServer = &srvIdx
|
||||
require.NoError(t, client.UpdateSendJob(dir, peer.Uid, job))
|
||||
|
||||
return job
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// TestProcessSentMessages_UpdatesDeliveryInfo is the main round-trip test.
|
||||
// It verifies that after ProcessSentMessages runs:
|
||||
// - the function returns 1 (one message updated)
|
||||
// - the send job is removed from the queue
|
||||
// - a subsequent LoadMessagesHistory returns ServerDeliveryUuid and
|
||||
// ServerDeliveryTimestamp for the message
|
||||
func TestProcessSentMessages_UpdatesDeliveryInfo(t *testing.T) {
|
||||
dir, id := setupMsgHelperConfig(t)
|
||||
|
||||
peer := newFullyKeyedPeer(t, "peer-uid-main")
|
||||
require.NoError(t, id.Peers.StorePeer(peer))
|
||||
|
||||
storeTestMessage(t, peer, "hello world")
|
||||
|
||||
srv := client.Server{Url: "http://test-server.example"}
|
||||
job := pushAndMarkSent(t, dir, peer, srv)
|
||||
|
||||
// --- call under test ---
|
||||
updated := ProcessSentMessages(dir)
|
||||
|
||||
assert.Equal(t, 1, updated, "exactly one message should be updated")
|
||||
|
||||
// The job must be removed from the queue after processing.
|
||||
jobAfter, err := client.GetSendJob(dir, peer.Uid, job.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, jobAfter, "job should be deleted after processing")
|
||||
|
||||
// Reload message history and verify delivery metadata was persisted.
|
||||
msgs, err := peer.LoadMessagesHistory(0, 0, 50)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, msgs, 1, "expected exactly one message in history")
|
||||
|
||||
assert.Equal(t, srv.GetUid(), msgs[0].ServerDeliveryUuid,
|
||||
"ServerDeliveryUuid should match the server that accepted the message")
|
||||
assert.NotZero(t, msgs[0].ServerDeliveryTimestamp,
|
||||
"ServerDeliveryTimestamp should be set after ProcessSentMessages")
|
||||
assert.Equal(t, uint64(job.SentAt.Unix()), msgs[0].ServerDeliveryTimestamp,
|
||||
"ServerDeliveryTimestamp should match job.SentAt")
|
||||
}
|
||||
|
||||
// TestProcessSentMessages_SkipsJobWithoutDeliveryInfo verifies that a Sent job
|
||||
// missing SentAt or SuccessfulServer is discarded (not counted, not updating
|
||||
// the message DB).
|
||||
func TestProcessSentMessages_SkipsJobWithoutDeliveryInfo(t *testing.T) {
|
||||
dir, id := setupMsgHelperConfig(t)
|
||||
|
||||
peer := newFullyKeyedPeer(t, "peer-uid-incomplete")
|
||||
require.NoError(t, id.Peers.StorePeer(peer))
|
||||
|
||||
storeTestMessage(t, peer, "incomplete job")
|
||||
|
||||
dbFile := peer.LastMessage.Dbfile
|
||||
dbId := peer.LastMessage.Dbid
|
||||
|
||||
outboxDir := filepath.Join(dir, "outbox")
|
||||
require.NoError(t, os.MkdirAll(outboxDir, 0700))
|
||||
msgFile := filepath.Join(outboxDir, fmt.Sprintf("%s_%d", dbFile, dbId))
|
||||
require.NoError(t, os.WriteFile(msgFile, []byte("packed"), 0600))
|
||||
|
||||
require.NoError(t, client.PushSendJob(dir, &client.SendJob{
|
||||
Queue: peer.Uid,
|
||||
File: msgFile,
|
||||
Servers: []client.Server{{Url: "http://test-server.example"}},
|
||||
Timeout: 60,
|
||||
}))
|
||||
|
||||
job, _, err := client.PeekSendJob(dir, peer.Uid)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, job)
|
||||
|
||||
// Mark as Sent but intentionally leave SentAt and SuccessfulServer nil.
|
||||
job.Status = client.SendStatusSent
|
||||
require.NoError(t, client.UpdateSendJob(dir, peer.Uid, job))
|
||||
|
||||
updated := ProcessSentMessages(dir)
|
||||
assert.Equal(t, 0, updated, "incomplete job must not be counted as updated")
|
||||
|
||||
// Message should have no delivery info.
|
||||
msgs, err := peer.LoadMessagesHistory(0, 0, 50)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, msgs, 1)
|
||||
assert.Empty(t, msgs[0].ServerDeliveryUuid, "delivery UUID must not be set")
|
||||
assert.Zero(t, msgs[0].ServerDeliveryTimestamp, "delivery timestamp must not be set")
|
||||
}
|
||||
|
||||
// TestProcessSentMessages_EmptyQueues verifies that an absent or empty queues
|
||||
// directory results in 0 updates without error.
|
||||
func TestProcessSentMessages_EmptyQueues(t *testing.T) {
|
||||
dir, _ := setupMsgHelperConfig(t)
|
||||
// queues/ directory does not exist yet.
|
||||
updated := ProcessSentMessages(dir)
|
||||
assert.Equal(t, 0, updated, "no queues → 0 updates")
|
||||
|
||||
// Also test with the directory present but empty.
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(dir, "queues"), 0700))
|
||||
updated = ProcessSentMessages(dir)
|
||||
assert.Equal(t, 0, updated, "empty queues → 0 updates")
|
||||
}
|
||||
|
||||
// TestProcessSentMessages_UnparseableFilename verifies that a job whose filename
|
||||
// does not follow the {dbFile}_{dbId} convention is skipped with a logged error
|
||||
// and not counted as updated.
|
||||
func TestProcessSentMessages_UnparseableFilename(t *testing.T) {
|
||||
dir, id := setupMsgHelperConfig(t)
|
||||
|
||||
peer := newFullyKeyedPeer(t, "peer-uid-nodbinfo")
|
||||
require.NoError(t, id.Peers.StorePeer(peer))
|
||||
|
||||
storeTestMessage(t, peer, "the real message")
|
||||
|
||||
// A filename with no underscore cannot be parsed as {dbFile}_{dbId}.
|
||||
msgFile := filepath.Join(dir, "badname.bin")
|
||||
require.NoError(t, os.WriteFile(msgFile, []byte("packed"), 0600))
|
||||
|
||||
require.NoError(t, client.PushSendJob(dir, &client.SendJob{
|
||||
Queue: peer.Uid,
|
||||
File: msgFile,
|
||||
Servers: []client.Server{{Url: "http://test-server.example"}},
|
||||
Timeout: 60,
|
||||
}))
|
||||
|
||||
job, _, err := client.PeekSendJob(dir, peer.Uid)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, job)
|
||||
|
||||
sentAt := time.Now()
|
||||
srvIdx := 0
|
||||
job.Status = client.SendStatusSent
|
||||
job.SentAt = &sentAt
|
||||
job.SuccessfulServer = &srvIdx
|
||||
require.NoError(t, client.UpdateSendJob(dir, peer.Uid, job))
|
||||
|
||||
// Must NOT count as updated; the real message row must be untouched.
|
||||
updated := ProcessSentMessages(dir)
|
||||
assert.Equal(t, 0, updated, "job without db info must not be counted as updated")
|
||||
|
||||
msgs, err := peer.LoadMessagesHistory(0, 0, 50)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, msgs, 1)
|
||||
assert.Empty(t, msgs[0].ServerDeliveryUuid, "delivery UUID must not be set")
|
||||
assert.Zero(t, msgs[0].ServerDeliveryTimestamp, "delivery timestamp must not be set")
|
||||
}
|
||||
|
||||
// TestProcessSentMessages_MultipleMessages verifies that all jobs in the same
|
||||
// queue are processed and that each message gets its own delivery info.
|
||||
func TestProcessSentMessages_MultipleMessages(t *testing.T) {
|
||||
dir, id := setupMsgHelperConfig(t)
|
||||
|
||||
peer := newFullyKeyedPeer(t, "peer-uid-multi")
|
||||
require.NoError(t, id.Peers.StorePeer(peer))
|
||||
|
||||
srv := client.Server{Url: "http://test-server.example"}
|
||||
|
||||
const n = 3
|
||||
for i := range n {
|
||||
storeTestMessage(t, peer, fmt.Sprintf("message-%d", i))
|
||||
pushAndMarkSent(t, dir, peer, srv)
|
||||
}
|
||||
|
||||
updated := ProcessSentMessages(dir)
|
||||
assert.Equal(t, n, updated, "all %d messages should be updated", n)
|
||||
|
||||
msgs, err := peer.LoadMessagesHistory(0, 0, 50)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, msgs, n)
|
||||
|
||||
for _, m := range msgs {
|
||||
assert.Equal(t, srv.GetUid(), m.ServerDeliveryUuid,
|
||||
"every message should have ServerDeliveryUuid set")
|
||||
assert.NotZero(t, m.ServerDeliveryTimestamp,
|
||||
"every message should have ServerDeliveryTimestamp set")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package helpers
|
||||
|
||||
/*
|
||||
import (
|
||||
"forge.redroom.link/yves/meowlib"
|
||||
"forge.redroom.link/yves/meowlib/client"
|
||||
)
|
||||
|
||||
func HttpSendMessage(serverUid string, message []byte, timeout int) ([]byte, error) {
|
||||
id := client.GetConfig().GetIdentity()
|
||||
srv, err := id.MessageServers.LoadServer(serverUid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// gettig server Public key if missing
|
||||
if srv.PublicKey == "" {
|
||||
srvdata, err := meowlib.HttpGetId(srv.Url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//print(srvdata["publicKey"])
|
||||
srv.PublicKey = srvdata["publicKey"]
|
||||
id.MessageServers.StoreServer(srv)
|
||||
}
|
||||
|
||||
response, err := meowlib.HttpPostMessage(srv.Url, message, timeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
*/
|
||||
@@ -0,0 +1 @@
|
||||
package helpers
|
||||
@@ -0,0 +1,15 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"forge.redroom.link/yves/meowlib/client"
|
||||
)
|
||||
|
||||
func LoadMessagesHistory(peer_uid string) ([]client.InternalUserMessage, string, error) {
|
||||
id := client.GetConfig().GetIdentity()
|
||||
peer := id.Peers.GetFromUid(peer_uid)
|
||||
msgs, err := peer.LoadMessagesHistory(0, 0, 50)
|
||||
if err != nil {
|
||||
return nil, "LoadLastMessages: LoadMessagesHistory", err
|
||||
}
|
||||
return msgs, "", nil
|
||||
}
|
||||
+342
-93
@@ -1,127 +1,258 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"math/rand"
|
||||
mrand "math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"forge.redroom.link/yves/meowlib"
|
||||
"github.com/ProtonMail/gopenpgp/v2/helper"
|
||||
"github.com/google/uuid"
|
||||
doubleratchet "github.com/status-im/doubleratchet"
|
||||
)
|
||||
|
||||
const maxHiddenCount = 30
|
||||
|
||||
// Package-level random number generator with mutex for thread-safe access
|
||||
var (
|
||||
rngMu sync.Mutex
|
||||
rng = mrand.New(mrand.NewSource(time.Now().UnixNano()))
|
||||
)
|
||||
|
||||
type Identity struct {
|
||||
Nickname string `json:"nickname,omitempty"`
|
||||
DefaultAvatarUuid string `json:"default_avatar_uuid,omitempty"`
|
||||
RootKp meowlib.KeyPair `json:"id_kp,omitempty"`
|
||||
DefaultAvatar string `json:"default_avatar,omitempty"`
|
||||
Avatars []Avatar `json:"avatars,omitempty"`
|
||||
RootKp *meowlib.KeyPair `json:"id_kp,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Peers PeerList `json:"peers,omitempty"`
|
||||
HiddenPeers [][]byte `json:"hiddend_peers,omitempty"`
|
||||
Device meowlib.KeyPair `json:"device,omitempty"`
|
||||
Peers PeerStorage `json:"peers,omitempty"`
|
||||
HiddenPeers [][]byte `json:"hidden_peers,omitempty"`
|
||||
Personae PeerList `json:"faces,omitempty"`
|
||||
Device *meowlib.KeyPair `json:"device,omitempty"`
|
||||
KnownServers ServerList `json:"known_servers,omitempty"`
|
||||
MessageServers ServerList `json:"message_servers,omitempty"`
|
||||
MessageServers ServerStorage `json:"message_servers,omitempty"`
|
||||
DefaultDbPassword string `json:"default_db_password,omitempty"`
|
||||
DbPasswordStore bool `json:"db_password_store,omitempty"`
|
||||
OwnedDevices PeerList `json:"owned_devices,omitempty"`
|
||||
StaticMtkServerPaths []ServerList `json:"static_mtk_server_paths,omitempty"`
|
||||
DynamicMtkServeRules []string `json:"dynamic_mtk_serve_rules,omitempty"`
|
||||
InvitationTimeout int `json:"invitation_timeout,omitempty"`
|
||||
Uuid string `json:"uuid,omitempty"`
|
||||
unlockedHiddenPeers PeerList
|
||||
}
|
||||
|
||||
func CreateIdentity(nickname string) *Identity {
|
||||
func CreateIdentity(nickname string) (*Identity, error) {
|
||||
var id Identity
|
||||
var err error
|
||||
id.Nickname = nickname
|
||||
id.RootKp = meowlib.NewKeyPair()
|
||||
id.Uuid = uuid.New().String()
|
||||
id.RootKp, err = meowlib.NewKeyPair()
|
||||
GetConfig().me = &id
|
||||
id.MessageServers = ServerStorage{DbFile: uuid.NewString()}
|
||||
id.generateRandomHiddenStuff()
|
||||
return &id
|
||||
err = id.CreateFolder()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &id, nil
|
||||
}
|
||||
|
||||
// Creates an invitation for a peer, returns the peer containing
|
||||
func (id *Identity) InvitePeer(MyName string, ContactName string, MessageServerIdxs []int) (*Peer, *meowlib.ContactCard, error) {
|
||||
func (id *Identity) CreateFolder() error {
|
||||
err := os.MkdirAll(filepath.Join(GetConfig().StoragePath, id.Uuid), 0700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (id *Identity) WipeFolder() error {
|
||||
err := os.RemoveAll(filepath.Join(GetConfig().StoragePath, id.Uuid))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// InvitationStep1 creates a minimal pending peer with only a temporary keypair and returns
|
||||
// the InvitationInitPayload to be transmitted to the invitee (via file, QR code, or server).
|
||||
// Full keypairs are only generated in InvitationStep3, after the invitee's answer is received.
|
||||
func (id *Identity) InvitationStep1(MyName string, ContactName string, MessageServerUids []string, InvitationMessage string) (*meowlib.InvitationInitPayload, *Peer, error) {
|
||||
var peer Peer
|
||||
var myContactCard meowlib.ContactCard
|
||||
peer.MyIdentity = meowlib.NewKeyPair()
|
||||
peer.MyEncryptionKp = meowlib.NewKeyPair()
|
||||
peer.MyLookupKp = meowlib.NewKeyPair()
|
||||
var err error
|
||||
peer.Uid = uuid.New().String()
|
||||
peer.Name = ContactName
|
||||
peer.MyName = MyName
|
||||
peer.InvitationId = uuid.New().String()
|
||||
if id.MessageServers.Servers == nil {
|
||||
return nil, nil, errors.New("no message servers defined in your identity")
|
||||
}
|
||||
for _, i := range MessageServerIdxs {
|
||||
if i > len(id.MessageServers.Servers)-1 {
|
||||
return nil, nil, errors.New("requested server out of range of defined message servers")
|
||||
}
|
||||
}
|
||||
for _, i := range MessageServerIdxs {
|
||||
srv := &id.MessageServers.Servers[i].ServerData
|
||||
myContactCard.PullServers = append(myContactCard.PullServers, srv)
|
||||
}
|
||||
myContactCard.Name = MyName
|
||||
myContactCard.ContactPublicKey = peer.MyIdentity.Public
|
||||
myContactCard.EncryptionPublicKey = peer.MyEncryptionKp.Public
|
||||
myContactCard.LookupPublicKey = peer.MyLookupKp.Public
|
||||
myContactCard.InvitationId = peer.InvitationId
|
||||
id.Peers = append(id.Peers, peer)
|
||||
peer.InvitationMessage = InvitationMessage
|
||||
peer.MyPullServers = MessageServerUids
|
||||
|
||||
return &peer, &myContactCard, nil
|
||||
// Temporary keypair: public key is sent to invitee for step-2 encryption and as
|
||||
// the server-side lookup key where the invitee will post their answer.
|
||||
peer.InvitationKp, err = meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
func (id *Identity) CheckInvitation(ReceivedContact *meowlib.ContactCard) (isAnswer bool, proposedNick string, receivedNick string) {
|
||||
for _, p := range id.Peers {
|
||||
if p.InvitationId == ReceivedContact.InvitationId {
|
||||
return true, p.Name, ReceivedContact.Name
|
||||
id.Peers.StorePeer(&peer)
|
||||
|
||||
payload := &meowlib.InvitationInitPayload{
|
||||
Uuid: peer.InvitationId,
|
||||
Name: MyName,
|
||||
PublicKey: peer.InvitationKp.Public,
|
||||
InvitationMessage: InvitationMessage,
|
||||
}
|
||||
}
|
||||
return false, "", ReceivedContact.Name
|
||||
return payload, &peer, nil
|
||||
}
|
||||
|
||||
func (id *Identity) AnswerInvitation(MyName string, ContactName string, MessageServerIdxs []int, ReceivedContact *meowlib.ContactCard) *meowlib.ContactCard {
|
||||
// InvitationStep2 creates the invitee's peer entry from the received InvitationInitPayload
|
||||
// and returns the peer. The invitee generates their full keypairs here.
|
||||
// The initiator's temporary public key (payload.PublicKey) is used both as the encryption
|
||||
// target for the step-2 answer and as the server-side lookup address.
|
||||
func (id *Identity) InvitationStep2(MyName string, ContactName string, MessageServerUids []string, payload *meowlib.InvitationInitPayload) (*Peer, error) {
|
||||
var peer Peer
|
||||
var myContactCard meowlib.ContactCard
|
||||
peer.MyIdentity = meowlib.NewKeyPair()
|
||||
peer.MyEncryptionKp = meowlib.NewKeyPair()
|
||||
peer.MyLookupKp = meowlib.NewKeyPair()
|
||||
var err error
|
||||
peer.Uid = uuid.New().String()
|
||||
peer.MyIdentity, err = meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
peer.MyEncryptionKp, err = meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
peer.MyLookupKp, err = meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ContactName != "" {
|
||||
peer.Name = ContactName
|
||||
} else {
|
||||
peer.Name = ReceivedContact.Name
|
||||
peer.Name = payload.Name
|
||||
}
|
||||
peer.Contact = *ReceivedContact
|
||||
for _, i := range MessageServerIdxs {
|
||||
srv := id.MessageServers.Servers[i].ServerData
|
||||
myContactCard.PullServers = append(myContactCard.PullServers, &srv)
|
||||
}
|
||||
myContactCard.Name = MyName
|
||||
myContactCard.ContactPublicKey = peer.MyIdentity.Public
|
||||
myContactCard.EncryptionPublicKey = peer.MyEncryptionKp.Public
|
||||
myContactCard.LookupPublicKey = peer.MyLookupKp.Public
|
||||
myContactCard.InvitationId = ReceivedContact.InvitationId
|
||||
|
||||
id.Peers = append(id.Peers, peer)
|
||||
|
||||
return &myContactCard
|
||||
// The initiator's temp key is used for both encrypting the answer and as destination.
|
||||
peer.ContactEncryption = payload.PublicKey
|
||||
peer.ContactLookupKey = payload.PublicKey
|
||||
peer.InvitationId = payload.Uuid
|
||||
peer.InvitationMessage = payload.InvitationMessage
|
||||
peer.MyPullServers = MessageServerUids
|
||||
peer.MyName = MyName
|
||||
id.Peers.StorePeer(&peer)
|
||||
return &peer, nil
|
||||
}
|
||||
|
||||
func (id *Identity) FinalizeInvitation(ReceivedContact *meowlib.ContactCard) error {
|
||||
for i, p := range id.Peers {
|
||||
if p.InvitationId == ReceivedContact.InvitationId {
|
||||
id.Peers[i].Contact = *ReceivedContact
|
||||
// InvitationStep3 is called by the initiator after receiving and decrypting the invitee's
|
||||
// ContactCard (step-2 answer). It generates the initiator's full keypairs and DR material,
|
||||
// updates the pending peer with the invitee's contact info, and returns the initiator's
|
||||
// full ContactCard to be sent to the invitee (STEP_3_SEND).
|
||||
func (id *Identity) InvitationStep3(inviteeContact *meowlib.ContactCard) (*meowlib.ContactCard, *Peer, error) {
|
||||
var err error
|
||||
peer := id.Peers.GetFromInvitationId(inviteeContact.InvitationId)
|
||||
if peer == nil {
|
||||
return nil, nil, errors.New("no pending peer found for invitation id " + inviteeContact.InvitationId)
|
||||
}
|
||||
|
||||
// Generate full keypairs now that the invitee's identity is known.
|
||||
peer.MyIdentity, err = meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
peer.MyEncryptionKp, err = meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
peer.MyLookupKp, err = meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
symKeyBytes := make([]byte, 32)
|
||||
if _, err = rand.Read(symKeyBytes); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
peer.MySymKey = base64.StdEncoding.EncodeToString(symKeyBytes)
|
||||
|
||||
// Generate DR keypair and root key.
|
||||
drKp, err := doubleratchet.DefaultCrypto{}.GenerateDH()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
peer.DrKpPrivate = base64.StdEncoding.EncodeToString(drKp.PrivateKey())
|
||||
peer.DrKpPublic = base64.StdEncoding.EncodeToString(drKp.PublicKey())
|
||||
drRootKey := make([]byte, 32)
|
||||
if _, err = rand.Read(drRootKey); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
peer.DrRootKey = base64.StdEncoding.EncodeToString(drRootKey)
|
||||
peer.DrInitiator = true
|
||||
|
||||
// Store invitee contact info.
|
||||
peer.ContactPublicKey = inviteeContact.ContactPublicKey
|
||||
peer.ContactEncryption = inviteeContact.EncryptionPublicKey
|
||||
peer.ContactLookupKey = inviteeContact.LookupPublicKey
|
||||
for _, srv := range inviteeContact.PullServers {
|
||||
peer.ContactPullServers = append(peer.ContactPullServers, srv.GetUid())
|
||||
newsrv, err := CreateServerFromUid(srv.GetUid())
|
||||
if err == nil {
|
||||
id.MessageServers.StoreServerIfNotExists(newsrv)
|
||||
}
|
||||
}
|
||||
// Drop the temporary invitation keypair — no longer needed.
|
||||
peer.InvitationKp = nil
|
||||
|
||||
id.Peers.StorePeer(peer)
|
||||
|
||||
return peer.GetMyContact(), peer, nil
|
||||
}
|
||||
|
||||
// InvitationStep4 is called by the invitee upon receiving the initiator's full ContactCard
|
||||
// (carried as a regular UserMessage with invitation.step=3). It finalizes the peer entry.
|
||||
func (id *Identity) InvitationStep4(initiatorContact *meowlib.ContactCard) error {
|
||||
var err error
|
||||
var newsrv *Server
|
||||
for _, srv := range initiatorContact.PullServers {
|
||||
newsrv, err = CreateServerFromUid(srv.GetUid())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id.MessageServers.StoreServerIfNotExists(newsrv)
|
||||
}
|
||||
peer := id.Peers.GetFromInvitationId(initiatorContact.InvitationId)
|
||||
if peer == nil {
|
||||
return errors.New("no pending peer found for invitation id " + initiatorContact.InvitationId)
|
||||
}
|
||||
peer.ContactPublicKey = initiatorContact.ContactPublicKey
|
||||
peer.ContactEncryption = initiatorContact.EncryptionPublicKey
|
||||
peer.ContactLookupKey = initiatorContact.LookupPublicKey
|
||||
peer.MySymKey = initiatorContact.SymetricKey
|
||||
peer.DrRootKey = initiatorContact.DrRootKey
|
||||
peer.ContactDrPublicKey = initiatorContact.DrPublicKey
|
||||
peer.DrInitiator = false
|
||||
for _, srv := range initiatorContact.PullServers {
|
||||
peer.ContactPullServers = append(peer.ContactPullServers, srv.GetUid())
|
||||
}
|
||||
id.Peers.StorePeer(peer)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.New("no matching contact found for invitationId " + ReceivedContact.InvitationId)
|
||||
|
||||
// CheckInvitation checks if the received ContactCard is an answer to one of our pending
|
||||
// invitations. Returns true when it is, with the proposed and received nicknames.
|
||||
func (id *Identity) CheckInvitation(ReceivedContact *meowlib.ContactCard) (isAnswer bool, proposedNick string, receivedNick string, invitationMessage string) {
|
||||
return id.Peers.CheckInvitation(ReceivedContact)
|
||||
}
|
||||
|
||||
// LoadIdentity loads an identity from an encrypted file
|
||||
func LoadIdentity(filename string, password string) (*Identity, error) {
|
||||
var id Identity
|
||||
GetConfig().memoryPassword = password
|
||||
err := GetConfig().SetMemPass(password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
GetConfig().IdentityFile = filename
|
||||
indata, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
@@ -132,7 +263,13 @@ func LoadIdentity(filename string, password string) (*Identity, error) {
|
||||
return nil, err
|
||||
}
|
||||
err = json.Unmarshal([]byte(pass), &id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
GetConfig().me = &id
|
||||
if id.Peers.DbFile != "" {
|
||||
id.Peers.LoadPeers(password)
|
||||
}
|
||||
return &id, err
|
||||
}
|
||||
|
||||
@@ -141,7 +278,11 @@ func (id *Identity) Save() error {
|
||||
return errors.New("identity filename empty")
|
||||
}
|
||||
b, _ := json.Marshal(id)
|
||||
armor, err := helper.EncryptMessageWithPassword([]byte(GetConfig().memoryPassword), string(b))
|
||||
password, err := GetConfig().GetMemPass()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
armor, err := helper.EncryptMessageWithPassword([]byte(password), string(b))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -160,7 +301,7 @@ func (id *Identity) TryUnlockHidden(password string) error {
|
||||
return err
|
||||
}
|
||||
p.dbPassword = password
|
||||
id.unlockedHiddenPeers = append(id.unlockedHiddenPeers, p)
|
||||
id.unlockedHiddenPeers = append(id.unlockedHiddenPeers, &p)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
@@ -170,6 +311,7 @@ func (id *Identity) TryUnlockHidden(password string) error {
|
||||
return errors.New("no peer found")
|
||||
}
|
||||
|
||||
/*
|
||||
func (id *Identity) HidePeer(peerIdx int, password string) error {
|
||||
serializedPeer, err := json.Marshal(id.Peers[peerIdx])
|
||||
if err != nil {
|
||||
@@ -184,39 +326,146 @@ func (id *Identity) HidePeer(peerIdx int, password string) error {
|
||||
// remove clear text peer
|
||||
id.Peers = append(id.Peers[:peerIdx], id.Peers[peerIdx+1:]...)
|
||||
return nil
|
||||
}
|
||||
}*/
|
||||
|
||||
func (id *Identity) generateRandomHiddenStuff() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
count := rand.Intn(maxHiddenCount) + 1
|
||||
func (id *Identity) generateRandomHiddenStuff() error {
|
||||
var err error
|
||||
rngMu.Lock()
|
||||
count := rng.Intn(maxHiddenCount) + 1
|
||||
rngMu.Unlock()
|
||||
for i := 1; i < count; i++ {
|
||||
var p Peer
|
||||
p.Uid = uuid.New().String()
|
||||
p.Name = randomLenString(4, 20)
|
||||
p.MyEncryptionKp = meowlib.NewKeyPair()
|
||||
p.MyIdentity = meowlib.NewKeyPair()
|
||||
p.MyLookupKp = meowlib.NewKeyPair()
|
||||
p.Contact.Name = randomLenString(4, 20)
|
||||
p.Contact.ContactPublicKey = p.MyLookupKp.Public
|
||||
p.Contact.EncryptionPublicKey = p.MyIdentity.Public
|
||||
p.Contact.LookupPublicKey = p.MyEncryptionKp.Public
|
||||
p.Contact.AddUrls([]string{randomLenString(14, 60), randomLenString(14, 60)})
|
||||
id.Peers = append(id.Peers, p)
|
||||
id.HidePeer(0, randomLenString(8, 14))
|
||||
// TODO Add conversations
|
||||
p.MyEncryptionKp, err = meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.MyIdentity, err = meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.MyLookupKp, err = meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Name = randomLenString(4, 20)
|
||||
p.ContactPublicKey = p.MyLookupKp.Public
|
||||
p.ContactEncryption = p.MyIdentity.Public
|
||||
p.ContactLookupKey = p.MyEncryptionKp.Public
|
||||
p.dbPassword = randomLenString(8, 14)
|
||||
// p.Contact.AddUrls([]string{randomLenString(14, 60), randomLenString(14, 60)}) // todo add servers
|
||||
id.Peers.StorePeer(&p)
|
||||
//id.HidePeer(0, randomLenString(8, 14))
|
||||
// TODO Add random conversations
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type BackgroundJob struct {
|
||||
RootPublic string `json:"root_public,omitempty"`
|
||||
Device *meowlib.KeyPair `json:"device,omitempty"`
|
||||
Jobs []RequestsJob `json:"jobs,omitempty"`
|
||||
}
|
||||
|
||||
type RequestsJob struct {
|
||||
Server *Server `json:"server,omitempty"`
|
||||
LookupKeys []*meowlib.KeyPair `json:"lookup_keys,omitempty"`
|
||||
}
|
||||
|
||||
func (id *Identity) GetRequestJobs() []RequestsJob {
|
||||
var list []RequestsJob
|
||||
srvs := map[string]*RequestsJob{}
|
||||
// get all servers
|
||||
servers, err := id.MessageServers.LoadAllServers()
|
||||
if err == nil {
|
||||
// build a server map
|
||||
for _, server := range servers {
|
||||
var rj RequestsJob
|
||||
rj.Server = server
|
||||
srvs[server.GetServerCard().GetUid()] = &rj
|
||||
}
|
||||
// add ids to the map
|
||||
peers, err := id.Peers.GetPeers()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
for _, peer := range peers {
|
||||
for _, server := range peer.MyPullServers {
|
||||
if srvs[server] == nil {
|
||||
continue
|
||||
}
|
||||
if peer.MyLookupKp != nil {
|
||||
// Active peer — use the permanent lookup key.
|
||||
srvs[server].LookupKeys = append(srvs[server].LookupKeys, peer.MyLookupKp)
|
||||
} else if peer.InvitationKp != nil {
|
||||
// Step-1 pending peer — poll using the temp invitation keypair so the
|
||||
// server-stored step-2 answer can be retrieved.
|
||||
srvs[server].LookupKeys = append(srvs[server].LookupKeys, peer.InvitationKp)
|
||||
}
|
||||
}
|
||||
}
|
||||
// add hidden peers
|
||||
for _, peer := range id.unlockedHiddenPeers {
|
||||
for _, server := range peer.MyPullServers {
|
||||
srvs[server].LookupKeys = append(srvs[server].LookupKeys, peer.MyLookupKp)
|
||||
}
|
||||
}
|
||||
// todo add garbage
|
||||
|
||||
// todo random reorder
|
||||
|
||||
// build list
|
||||
for _, srv := range srvs {
|
||||
if len(srv.LookupKeys) > 0 {
|
||||
list = append(list, *srv)
|
||||
}
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
func (id *Identity) SaveBackgroundJob() error {
|
||||
var bj BackgroundJob
|
||||
bj.Jobs = id.GetRequestJobs()
|
||||
bj.RootPublic = id.RootKp.Public
|
||||
bj.Device = id.Device
|
||||
jsonjobs, err := json.Marshal(bj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id.CreateFolder()
|
||||
err = os.WriteFile(filepath.Join(GetConfig().StoragePath, id.Uuid, ".jobs"), jsonjobs, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func randomLenString(min int, max int) string {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
n := rand.Intn(max-min) + min
|
||||
return randomString(n)
|
||||
rngMu.Lock()
|
||||
defer rngMu.Unlock()
|
||||
|
||||
length := rng.Intn(max-min+1) + min
|
||||
return randomStringLocked(length)
|
||||
}
|
||||
|
||||
func randomString(n int) string {
|
||||
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
|
||||
s := make([]rune, n)
|
||||
for i := range s {
|
||||
s[i] = letters[rand.Intn(len(letters))]
|
||||
rngMu.Lock()
|
||||
defer rngMu.Unlock()
|
||||
|
||||
return randomStringLocked(n)
|
||||
}
|
||||
return string(s)
|
||||
|
||||
// randomStringLocked generates a random string of length n.
|
||||
// Must be called with rngMu already locked.
|
||||
func randomStringLocked(n int) string {
|
||||
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
||||
var b strings.Builder
|
||||
b.Grow(n)
|
||||
for i := 0; i < n; i++ {
|
||||
b.WriteByte(letterBytes[rng.Intn(len(letterBytes))])
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
+90
-15
@@ -3,9 +3,11 @@ package client
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"forge.redroom.link/yves/meowlib"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -17,28 +19,59 @@ func exists(filename string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func createId() *Identity {
|
||||
func createId(t *testing.T) *Identity {
|
||||
config := GetConfig()
|
||||
config.IdentityFile = "test.id"
|
||||
config.memoryPassword = "generalPassword"
|
||||
err := config.SetMemPass("generalPassword")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to set password: %v", err)
|
||||
}
|
||||
// ! Extension to quickly open db : Debug only !
|
||||
config.DbSuffix = ".sqlite"
|
||||
id := CreateIdentity("myname")
|
||||
err := id.Save()
|
||||
id, err := CreateIdentity("myname")
|
||||
if err != nil {
|
||||
log.Fatal("CreateIdentity failed")
|
||||
}
|
||||
err = id.Save()
|
||||
if err != nil {
|
||||
log.Fatal("Save failed")
|
||||
}
|
||||
for i := range 10 {
|
||||
var p Peer
|
||||
p.Name = "testName"
|
||||
p.MyEncryptionKp = meowlib.NewKeyPair()
|
||||
p.MyIdentity = meowlib.NewKeyPair()
|
||||
p.MyLookupKp = meowlib.NewKeyPair()
|
||||
p.Contact.Name = "foo"
|
||||
p.Contact.ContactPublicKey = p.MyLookupKp.Public
|
||||
p.Contact.EncryptionPublicKey = p.MyIdentity.Public
|
||||
p.Contact.LookupPublicKey = p.MyEncryptionKp.Public
|
||||
p.Contact.AddUrls([]string{"http:/127.0.0.1/meow", "tcp://localhost:1234"})
|
||||
id.Peers = append(id.Peers, p)
|
||||
p.Uid = uuid.New().String()
|
||||
p.Name = "testName_" + strconv.Itoa(i)
|
||||
p.MyEncryptionKp, err = meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p.MyIdentity, err = meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p.MyLookupKp, err = meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p.Name = "foo_" + strconv.Itoa(i)
|
||||
k, err := meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p.ContactPublicKey = k.Public
|
||||
k, err = meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p.ContactEncryption = k.Public
|
||||
k, err = meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p.ContactLookupKey = k.Public
|
||||
p.MyPullServers = []string{"server1", "server2"}
|
||||
//p.Contact.AddUrls([]string{"http:/127.0.0.1/meow", "tcp://localhost:1234"}) //todo add servers
|
||||
id.Peers.StorePeer(&p)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
@@ -48,7 +81,10 @@ func TestLoad(t *testing.T) {
|
||||
}
|
||||
id, err := LoadIdentity("test.id", "toto")
|
||||
if err != nil {
|
||||
id := CreateIdentity("myname")
|
||||
id, err1 := CreateIdentity("myname")
|
||||
if err1 != nil {
|
||||
log.Fatal("CreateIdentity failed")
|
||||
}
|
||||
id.Save()
|
||||
} else {
|
||||
log.Println(id.Nickname)
|
||||
@@ -65,6 +101,7 @@ func TestLoad(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestHidePeer(t *testing.T) {
|
||||
/*
|
||||
id := createId()
|
||||
name := id.Peers[0].Name
|
||||
assert.Equal(t, len(id.Peers), 1)
|
||||
@@ -75,7 +112,45 @@ func TestHidePeer(t *testing.T) {
|
||||
id.TryUnlockHidden("mypassword")
|
||||
assert.Equal(t, len(id.unlockedHiddenPeers), 1)
|
||||
assert.Equal(t, id.unlockedHiddenPeers[0].Name, name)
|
||||
|
||||
if exists("test.id") {
|
||||
os.Remove("test.id")
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
// test GetRequestJobs
|
||||
func TestGetRequestJobs(t *testing.T) {
|
||||
// Create a mock Identity object
|
||||
id := createId(t)
|
||||
id.MessageServers = ServerStorage{
|
||||
DbFile: "test.db",
|
||||
}
|
||||
err := GetConfig().SetMemPass("test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to set password: %v", err)
|
||||
}
|
||||
GetConfig().SetIdentity(id)
|
||||
for i := 1; i < 10; i++ {
|
||||
// initialize a Server with name "server+i"
|
||||
srv, err := CreateServerFromUrl("server" + strconv.Itoa(i))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
id.MessageServers.StoreServer(srv)
|
||||
}
|
||||
// Call GetRequestJobs
|
||||
jobs := id.GetRequestJobs()
|
||||
|
||||
// All 10 test peers use server1 and server2, so exactly 2 jobs are expected.
|
||||
assert.Equal(t, 2, len(jobs), "Expected 2 jobs (server1 and server2)")
|
||||
|
||||
for _, job := range jobs {
|
||||
assert.Contains(t, []string{"server1", "server2"}, job.Server.GetUid(), "Unexpected server UID")
|
||||
assert.Len(t, job.LookupKeys, 10, "Expected 10 lookup keys (one per test peer) per job")
|
||||
}
|
||||
|
||||
// Clean up
|
||||
// recursively remove the test.db folder
|
||||
os.RemoveAll("test.db")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package client
|
||||
|
||||
import "forge.redroom.link/yves/meowlib"
|
||||
|
||||
type InternalUserMessage struct {
|
||||
Outbound bool `json:"outbound"`
|
||||
Messagetype string `json:"messagetype,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Status *meowlib.ConversationStatus `json:"conversation_status,omitempty"`
|
||||
Contact *meowlib.ContactCard `json:"contact,omitempty"`
|
||||
ServerDeliveryUuid string `json:"server_delivery_uuid,omitempty"`
|
||||
ServerDeliveryTimestamp uint64 `json:"server_delivery_timestamp,omitempty"`
|
||||
//Group group
|
||||
FilePaths []string `json:"file_paths,omitempty"`
|
||||
CurrentLocation *meowlib.Location `json:"current_location,omitempty"`
|
||||
Appdata []byte `json:"appdata,omitempty"`
|
||||
Dbfile string `json:"dbfile,omitempty"`
|
||||
Dbid int64 `json:"dbid,omitempty"`
|
||||
}
|
||||
|
||||
// InternalUserMessageFromUserMessage creates an InternalUserMessage from a UserMessage
|
||||
func InternalUserMessageFromUserMessage(peer *Peer, msg *meowlib.UserMessage) *InternalUserMessage {
|
||||
iu := new(InternalUserMessage)
|
||||
if peer.ContactPublicKey == msg.From {
|
||||
iu.Outbound = false
|
||||
} else {
|
||||
iu.Outbound = true
|
||||
}
|
||||
iu.Messagetype = msg.Type
|
||||
iu.Message = string(msg.Data)
|
||||
iu.Status = msg.Status
|
||||
iu.Contact = msg.Contact
|
||||
return iu
|
||||
}
|
||||
|
||||
func ProcessOutboundTextMessage(peer *Peer, text string, srv *Server) ([]byte, error) {
|
||||
// Creating User message
|
||||
usermessage, err := peer.BuildSimpleUserMessage([]byte(text))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Prepare cyphered + packed user message
|
||||
packedMsg, err := peer.ProcessOutboundUserMessage(usermessage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Persist peer to save updated DR state (DrStateJson)
|
||||
if peer.DrRootKey != "" {
|
||||
GetConfig().GetIdentity().Peers.StorePeer(peer)
|
||||
}
|
||||
// Creating Server message for transporting the user message
|
||||
toServerMessage := srv.BuildToServerMessageFromUserMessage(packedMsg)
|
||||
return srv.ProcessOutboundMessage(toServerMessage)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
-----BEGIN PGP MESSAGE-----
|
||||
Comment: https://gopenpgp.org
|
||||
Version: GopenPGP 2.8.3
|
||||
|
||||
wy4ECQMIgUuEGbIAQdTg1Y0LVbCcIFEHJ3MkTGXl7hjJ6KuaEkdm83kI3ID/mesB
|
||||
0uoB/RojNQvrAnW+1W4xFutE/1S0gG9ejWYhCWiI7sxDmLoNnB1H3Rld2N7dEYnf
|
||||
sD4baoJC3dOhfbjCUqwtA1aMEmsvJI0VsxEWAj6Uq16iTNmL7HcIaH8aDL7EA8UZ
|
||||
RTC0bQGdvkf+azASRM6uB29Cm7aIviVyt5MfF/BDoauefibHrP4Z0sYH5P0KJC2i
|
||||
AqnObuyiqeYNp9yUzVtZywSjjt2C72DkuQIwgPf0FNE3zduxOZ2Ds80tS2Zyobxx
|
||||
6e+9KUaadUEkcdv/AOOqvQOtRYSVlF5o6gWRF+A16NuwalWAnHJ41k9Y3SSIQLiz
|
||||
Ppbkw77hrHYIXqopCyxnls2FJaO4QDDjd4JGEdejpxIKognZlgJIIK03khFjUc8/
|
||||
ilM3Hgbjs6dudJ76lHT8BKaiJPfJPNPL1wf45kLhFc383OdWGJ30NB/w6TbeQKvw
|
||||
fNNyI/ksfsGbssFm6Zlc0xCpnkEjW9Q9aeHqn34n2jLiDyugwigYhYFKMD8gsQVw
|
||||
0CRcde7A13/FTa83X9sZ1/rm05FN9M24bIhvG3+8YE4B6nIX43LvYkq18tpGbRLD
|
||||
uZ33c3bHjbE4PvSf0AdXaML0vGZzxMhBHpgSvPMKt1YiBVr9Kx05txuEAAQ8xaax
|
||||
KLhhTzVUF7jo4qVeMzvgne6As02yQBdMRYSk92uKm49IWSRzaprP8bx+HktaXJCy
|
||||
tG/98FXa+05BlTceL4BPaNWrYJlYi4Vpcd3jBm6DAT30gTprJPizUVcGfTkBXII9
|
||||
sHXLYvca72ItcCzIozOJIdB+y4pV/ZWH8DQdAeZEOfaNUpYbNs9DufxuOhbgx5xQ
|
||||
JvCKBHAz6fo5O/vkJ1AatihNQ8I8R+7iJ3q4xXxKuDhv+9+V2KG1kG6L1RLKfzpy
|
||||
GZ6pnmEKbLSa0SO048g6LBhDJyk9I955LHps3HIGoFtE9Oq/2T3fBuZjJgQW0kKj
|
||||
9ddK3sDOo0/U0Ojz5tfPTkIZvYiEmDoJdfj/jBtTc4F16pf9r4chhzKnkxw9JzfR
|
||||
Ntj9KThmWOmKHNNlHlwSerxBfNmRjKjfrJ4l1nJPQRDbynTPLzCR59uKVFj5e2t4
|
||||
F6pGVBrwARQ/kX0QqyqOB6UaE2ulV2EYwnNljegOd1NoDf5kr59K5IBZNx2PvEZe
|
||||
dM+7jPIojk7pbM6sCCneVXvMG5nzG82boevlc8HJnGEP/9dJ9uWHHu+LFXf71EIQ
|
||||
npcVOrw8JXTLYhiI9ssH0Tr0C2otkAMkr3DNXcfC5BxLQ+0Ayw0Wr+MNnUbP40Dq
|
||||
vLhI5YjFdFF/X0QUeVQ9srGk/JWTTPOR1liIGYbzouGQjzzmJOBLtEPoGAdjXbhg
|
||||
QXZDkpWMTh6qwbWroyQw06Ywwiex0NkTZ+I2UDdby7Dk1V33KmL6EKYm07I3eorn
|
||||
QRyL/Qs8DpYlwjw1yvbsbj2EIF9UakNLUfFg+VAd6gsgSG2500e6+5Eyjvs8Htpa
|
||||
wdxqyKgjURK7BkDYSdC6z/eNU7AhkdhYEo0PIOf0loXu2boKKtau7oSWfrJKep9Y
|
||||
qlpKOzvgxGUx3dRNGmJKAOOLhyHVjBfl5dalzVMikpt3AXhy+an4ogiY6AZgg+gH
|
||||
bSOJ73h5V/w0xCtD/Lrc4vSDlx1+93B/4m1wXItkBXSi1C2ivjDcPY2d5gd4EfCE
|
||||
JaHak6zI+P//9zoXJLycJnl/tw0Guw5oJBrhn9ReINNV/CO1pur1H19zBEwuV9c6
|
||||
u+vx9gcwN6EJEh5nDIOXXU/NoNsMpXERwzohob1plWpYUgB7cLyW4sNsHSSdWrOH
|
||||
ipAatW+uyPJXQd0YuMm6FLB/DfkNl1BAI3QhmAyGLBxma4KesxcjDImuiGNFvWvZ
|
||||
M7D3vz4ziOzauanZ/HNDYRa/ey9XJ0iLyLIDsZ0ZrK0T1E2z7PdY4y5JWUGu3a2c
|
||||
C71RBuTfAmXIAGn/jaF9jfx7dezW91VO0PZ9fKcU7x5khA4Z9gK3oCD2RhXOkIje
|
||||
bgtYGyWnaz0qcV1JUmRSo1Zwb84NVr5jCc5n743D7+fjedGMZtLQAGCUFttgO/9u
|
||||
KZbI3UUVcTREZvUKEAyWN/EhixL3Uf7Uv4M12v3RRTydxFPhUUNPbX0+kL9flTaF
|
||||
Fph4UBuGguu5VygBq0p3YUVYdlS9L8U5WD9DGL4tKW+WJb02jAnsRyWQQcc7PDFw
|
||||
u1jGIDbaCu/JQco95wpDx0rUGtC1NOVIJFSqPcNf+NHRQaLNks6zzUa67qbJgS5p
|
||||
nvrfSEVBd7AoSGP1gAuL0qzDHR0x06Fxe9uREHg1R7eojRyAHHs6ZEuK6CmzbTrr
|
||||
Ky8vdxcfOBwfzJF/J2VHY8lkIfNULqjQMIYpJcD7bMeH12Q0Y0BV11LsYA==
|
||||
=C05v
|
||||
-----END PGP MESSAGE-----
|
||||
@@ -0,0 +1,205 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"forge.redroom.link/yves/meowlib"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// setupInvitationTest creates two independent identities with separate storage paths.
|
||||
func setupInvitationTest(t *testing.T) (initiator *Identity, invitee *Identity, cleanup func()) {
|
||||
t.Helper()
|
||||
cfg := GetConfig()
|
||||
cfg.IdentityFile = "inv_test_init.id"
|
||||
cfg.SetMemPass("testpass")
|
||||
|
||||
initiator, err := CreateIdentity("initiator")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
invitee, err = CreateIdentity("invitee")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Give each identity a pull server UID.
|
||||
srvInit, _ := CreateServerFromUrl("http://init.server/meow/")
|
||||
initiator.MessageServers.StoreServer(srvInit)
|
||||
srvInvitee, _ := CreateServerFromUrl("http://invitee.server/meow/")
|
||||
invitee.MessageServers.StoreServer(srvInvitee)
|
||||
|
||||
cleanup = func() {
|
||||
os.Remove("inv_test_init.id")
|
||||
os.RemoveAll(cfg.StoragePath + "/" + initiator.Uuid)
|
||||
os.RemoveAll(cfg.StoragePath + "/" + invitee.Uuid)
|
||||
}
|
||||
return initiator, invitee, cleanup
|
||||
}
|
||||
|
||||
// TestInvitationStep1 verifies that InvitationStep1 creates a minimal peer with only
|
||||
// InvitationKp set (no full keypairs yet) and returns a valid InvitationInitPayload.
|
||||
func TestInvitationStep1(t *testing.T) {
|
||||
initiator, _, cleanup := setupInvitationTest(t)
|
||||
defer cleanup()
|
||||
|
||||
payload, peer, err := initiator.InvitationStep1("Alice", "Bob", []string{"http://init.server/meow/"}, "Hello Bob!")
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, payload)
|
||||
assert.NotNil(t, peer)
|
||||
|
||||
assert.NotEmpty(t, payload.Uuid)
|
||||
assert.Equal(t, "Alice", payload.Name)
|
||||
assert.NotEmpty(t, payload.PublicKey)
|
||||
assert.Equal(t, "Hello Bob!", payload.InvitationMessage)
|
||||
|
||||
// Full keypairs must NOT be set yet.
|
||||
assert.Nil(t, peer.MyIdentity)
|
||||
assert.Nil(t, peer.MyEncryptionKp)
|
||||
assert.Nil(t, peer.MyLookupKp)
|
||||
// Temp keypair must be set.
|
||||
assert.NotNil(t, peer.InvitationKp)
|
||||
assert.Equal(t, payload.PublicKey, peer.InvitationKp.Public)
|
||||
}
|
||||
|
||||
// TestInvitationStep1PayloadRoundTrip verifies Compress/Decompress of InvitationInitPayload.
|
||||
func TestInvitationStep1PayloadRoundTrip(t *testing.T) {
|
||||
initiator, _, cleanup := setupInvitationTest(t)
|
||||
defer cleanup()
|
||||
|
||||
payload, _, err := initiator.InvitationStep1("Alice", "Bob", nil, "test msg")
|
||||
assert.NoError(t, err)
|
||||
|
||||
compressed, err := payload.Compress()
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, compressed)
|
||||
|
||||
restored, err := meowlib.NewInvitationInitPayloadFromCompressed(compressed)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, payload.Uuid, restored.Uuid)
|
||||
assert.Equal(t, payload.Name, restored.Name)
|
||||
assert.Equal(t, payload.PublicKey, restored.PublicKey)
|
||||
assert.Equal(t, payload.InvitationMessage, restored.InvitationMessage)
|
||||
}
|
||||
|
||||
// TestInvitationStep2 verifies that InvitationStep2 creates a peer with full keypairs and
|
||||
// sets the initiator's temp key as both ContactEncryption and ContactLookupKey.
|
||||
func TestInvitationStep2(t *testing.T) {
|
||||
initiator, invitee, cleanup := setupInvitationTest(t)
|
||||
defer cleanup()
|
||||
|
||||
payload, _, err := initiator.InvitationStep1("Alice", "Bob", nil, "Hi")
|
||||
assert.NoError(t, err)
|
||||
|
||||
peer, err := invitee.InvitationStep2("Bob", "Alice", []string{"http://invitee.server/meow/"}, payload)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, peer)
|
||||
|
||||
// Full keypairs must be set on invitee's peer.
|
||||
assert.NotNil(t, peer.MyIdentity)
|
||||
assert.NotNil(t, peer.MyEncryptionKp)
|
||||
assert.NotNil(t, peer.MyLookupKp)
|
||||
|
||||
// Contact fields must point to initiator's temp key.
|
||||
assert.Equal(t, payload.PublicKey, peer.ContactEncryption)
|
||||
assert.Equal(t, payload.PublicKey, peer.ContactLookupKey)
|
||||
assert.Equal(t, payload.Uuid, peer.InvitationId)
|
||||
}
|
||||
|
||||
// TestInvitationFullFlow exercises the complete 4-step invitation handshake end-to-end,
|
||||
// verifying that both peers end up with each other's full contact information.
|
||||
func TestInvitationFullFlow(t *testing.T) {
|
||||
initiator, invitee, cleanup := setupInvitationTest(t)
|
||||
defer cleanup()
|
||||
|
||||
// STEP_1: initiator creates init payload.
|
||||
payload, initPeer, err := initiator.InvitationStep1("Alice", "Bob", []string{"http://init.server/meow/"}, "Hello!")
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, initPeer.InvitationKp)
|
||||
assert.Nil(t, initPeer.MyIdentity)
|
||||
|
||||
// STEP_2: invitee creates their peer from the payload.
|
||||
srvCard := &meowlib.ServerCard{Name: "InviteeServer", Url: "http://invitee.server/meow/"}
|
||||
inviteePeer, err := invitee.InvitationStep2("Bob", "Alice", []string{"http://invitee.server/meow/"}, payload)
|
||||
assert.NoError(t, err)
|
||||
inviteeCC := inviteePeer.GetMyContact()
|
||||
inviteeCC.PullServers = append(inviteeCC.PullServers, srvCard)
|
||||
|
||||
// STEP_3: initiator receives invitee's CC, generates full keypairs.
|
||||
myCC, _, err := initiator.InvitationStep3(inviteeCC)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, myCC)
|
||||
assert.NotEmpty(t, myCC.ContactPublicKey)
|
||||
assert.NotEmpty(t, myCC.EncryptionPublicKey)
|
||||
assert.NotEmpty(t, myCC.LookupPublicKey)
|
||||
assert.NotEmpty(t, myCC.DrRootKey)
|
||||
assert.NotEmpty(t, myCC.DrPublicKey)
|
||||
|
||||
// After step 3, initiator's peer must have full keypairs and invitee's contact info.
|
||||
updatedInitPeer := initiator.Peers.GetFromInvitationId(payload.Uuid)
|
||||
assert.NotNil(t, updatedInitPeer.MyIdentity)
|
||||
assert.NotNil(t, updatedInitPeer.MyEncryptionKp)
|
||||
assert.NotNil(t, updatedInitPeer.MyLookupKp)
|
||||
assert.Equal(t, inviteePeer.MyIdentity.Public, updatedInitPeer.ContactPublicKey)
|
||||
assert.Equal(t, inviteePeer.MyEncryptionKp.Public, updatedInitPeer.ContactEncryption)
|
||||
assert.Nil(t, updatedInitPeer.InvitationKp) // temp key must be cleared
|
||||
|
||||
// STEP_4: invitee finalizes from initiator's full CC.
|
||||
srvCardInit := &meowlib.ServerCard{Name: "InitServer", Url: "http://init.server/meow/"}
|
||||
myCC.PullServers = append(myCC.PullServers, srvCardInit)
|
||||
err = invitee.InvitationStep4(myCC)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Both peers must now be fully finalized (ContactPublicKey set → not pending).
|
||||
finalInitPeer := initiator.Peers.GetFromInvitationId(payload.Uuid)
|
||||
assert.False(t, finalInitPeer.InvitationPending())
|
||||
|
||||
finalInviteePeer := invitee.Peers.GetFromInvitationId(payload.Uuid)
|
||||
assert.False(t, finalInviteePeer.InvitationPending())
|
||||
assert.Equal(t, updatedInitPeer.MyIdentity.Public, finalInviteePeer.ContactPublicKey)
|
||||
assert.Equal(t, updatedInitPeer.MyEncryptionKp.Public, finalInviteePeer.ContactEncryption)
|
||||
assert.Equal(t, updatedInitPeer.MyLookupKp.Public, finalInviteePeer.ContactLookupKey)
|
||||
assert.NotEmpty(t, finalInviteePeer.DrRootKey)
|
||||
}
|
||||
|
||||
// TestInvitationStep3NotFound verifies that InvitationStep3 returns an error when no
|
||||
// pending peer exists for the given invitation ID.
|
||||
func TestInvitationStep3NotFound(t *testing.T) {
|
||||
initiator, _, cleanup := setupInvitationTest(t)
|
||||
defer cleanup()
|
||||
|
||||
cc := &meowlib.ContactCard{InvitationId: "nonexistent-uuid", ContactPublicKey: "pub"}
|
||||
_, _, err := initiator.InvitationStep3(cc)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
// TestGetRequestJobsPendingPeer verifies that pending (step-1 only) peers contribute
|
||||
// their InvitationKp to GetRequestJobs instead of MyLookupKp.
|
||||
func TestGetRequestJobsPendingPeer(t *testing.T) {
|
||||
cfg := GetConfig()
|
||||
cfg.SetMemPass("testpass")
|
||||
id, err := CreateIdentity("testjobs")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(cfg.StoragePath + "/" + id.Uuid)
|
||||
|
||||
cfg.SetIdentity(id)
|
||||
id.MessageServers = ServerStorage{DbFile: "testjobs.db"}
|
||||
defer os.RemoveAll("testjobs.db")
|
||||
|
||||
srv, _ := CreateServerFromUrl("http://srv1.test/meow/")
|
||||
id.MessageServers.StoreServer(srv)
|
||||
|
||||
// Create a step-1 pending peer.
|
||||
_, _, err = id.InvitationStep1("Me", "Friend", []string{"http://srv1.test/meow/"}, "Hi")
|
||||
assert.NoError(t, err)
|
||||
|
||||
jobs := id.GetRequestJobs()
|
||||
// At least one job should have a lookup key (the InvitationKp).
|
||||
total := 0
|
||||
for _, j := range jobs {
|
||||
total += len(j.LookupKeys)
|
||||
}
|
||||
assert.Greater(t, total, 0)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
var logger zerolog.Logger
|
||||
|
||||
// AddLogger sets the logger for the sublibrary
|
||||
func AddLogger(l zerolog.Logger) {
|
||||
logger = l
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
func ProcessForOutput(usermessage *meowlib.UserMessage, peer *Peer, servers *ServerList, trackingLookupKey string) ([]byte, error) {
|
||||
lastIdx := len(servers.Servers) - 1
|
||||
// LAST SERVER : Message delivery as usual
|
||||
srv := &servers.Servers[lastIdx]
|
||||
srv := servers.Servers[lastIdx]
|
||||
// Prepare cyphered + packed user message
|
||||
packedMsg, err := peer.ProcessOutboundUserMessage(usermessage)
|
||||
if err != nil {
|
||||
@@ -23,17 +23,17 @@ func ProcessForOutput(usermessage *meowlib.UserMessage, peer *Peer, servers *Ser
|
||||
lastuuid := uuid.NewString()
|
||||
// ALL PREVIOUS SERVERS
|
||||
for i := lastIdx - 1; i >= 0; i-- {
|
||||
srv = &servers.Servers[i]
|
||||
srv = servers.Servers[i]
|
||||
var toServerMessage meowlib.ToServerMessage
|
||||
toServerMessage.MatriochkaMessage.Data = lastmsg
|
||||
toServerMessage.MatriochkaMessage.Next.Url = servers.Servers[i+1].ServerData.Url
|
||||
toServerMessage.MatriochkaMessage.Next.PublicKey = servers.Servers[i+1].ServerData.PublicKey
|
||||
toServerMessage.MatriochkaMessage.Next.Url = servers.Servers[i+1].Url
|
||||
toServerMessage.MatriochkaMessage.Next.PublicKey = servers.Servers[i+1].PublicKey
|
||||
toServerMessage.MatriochkaMessage.Next.Delay = int32(servers.Servers[i+1].AllowedDelay)
|
||||
if trackingLookupKey != "" {
|
||||
toServerMessage.MatriochkaMessage.Next.Uuid = lastuuid // change tracking uuid at each server
|
||||
if i > 0 {
|
||||
toServerMessage.MatriochkaMessage.Prev.Url = servers.Servers[i-1].ServerData.Url
|
||||
toServerMessage.MatriochkaMessage.Prev.PublicKey = servers.Servers[i+1].ServerData.PublicKey
|
||||
toServerMessage.MatriochkaMessage.Prev.Url = servers.Servers[i-1].Url
|
||||
toServerMessage.MatriochkaMessage.Prev.PublicKey = servers.Servers[i+1].PublicKey
|
||||
toServerMessage.MatriochkaMessage.Prev.Delay = int32(servers.Servers[i-1].AllowedDelay)
|
||||
toServerMessage.MatriochkaMessage.Prev.Uuid = uuid.NewString()
|
||||
lastuuid = toServerMessage.MatriochkaMessage.Prev.Uuid
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
package client
|
||||
|
||||
import "forge.redroom.link/yves/meowlib"
|
||||
|
||||
type InternalUserMessage struct {
|
||||
message *meowlib.UserMessage
|
||||
dbid int64
|
||||
}
|
||||
|
||||
func ProcessOutboundTextMessage(peer *Peer, text string, srv *Server) ([]byte, error) {
|
||||
// Creating User message
|
||||
usermessage, err := peer.BuildSimpleUserMessage([]byte(text))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Prepare cyphered + packed user message
|
||||
packedMsg, err := peer.ProcessOutboundUserMessage(usermessage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Creating Server message for transporting the user message
|
||||
toServerMessage := srv.BuildToServerMessageFromUserMessage(packedMsg)
|
||||
return srv.ProcessOutboundMessage(toServerMessage)
|
||||
}
|
||||
@@ -0,0 +1,463 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"forge.redroom.link/yves/meowlib"
|
||||
"github.com/google/uuid"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func storeMessage(peer *Peer, usermessage *meowlib.UserMessage, filenames []string, password string) error {
|
||||
var dbid string
|
||||
cfg := GetConfig()
|
||||
identity := cfg.GetIdentity()
|
||||
// If no db/no ID create DB + Tablz
|
||||
// TODO : if file size > X new db
|
||||
if len(peer.DbIds) == 0 {
|
||||
dbid = uuid.NewString()
|
||||
peer.DbIds = []string{dbid}
|
||||
|
||||
identity.Peers.StorePeer(peer)
|
||||
identity.CreateFolder()
|
||||
file, err := os.Create(filepath.Join(cfg.StoragePath, identity.Uuid, dbid+GetConfig().DbSuffix))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file.Close()
|
||||
sqliteDatabase, err := sql.Open("sqlite3", filepath.Join(cfg.StoragePath, identity.Uuid, dbid+GetConfig().DbSuffix))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer sqliteDatabase.Close()
|
||||
err = createMessageTable(sqliteDatabase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sqliteDatabase.Close()
|
||||
} else {
|
||||
dbid = peer.DbIds[len(peer.DbIds)-1]
|
||||
}
|
||||
// Open Db
|
||||
db, err := sql.Open("sqlite3", filepath.Join(cfg.StoragePath, identity.Uuid, dbid+GetConfig().DbSuffix)) // Open the created SQLite File
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.Close()
|
||||
// Detach Files
|
||||
hiddenFilenames := []string{}
|
||||
if len(usermessage.Files) > 0 {
|
||||
for _, f := range usermessage.Files {
|
||||
hiddenFilename := uuid.NewString()
|
||||
// Cypher file
|
||||
encData, err := meowlib.SymEncrypt(password, f.Data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(cfg.StoragePath, identity.Uuid, "securefiles")); os.IsNotExist(err) {
|
||||
err = os.MkdirAll(filepath.Join(cfg.StoragePath, identity.Uuid, "securefiles"), 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
os.WriteFile(filepath.Join(cfg.StoragePath, identity.Uuid, "securefiles", hiddenFilename), encData, 0600)
|
||||
hiddenFilenames = append(hiddenFilenames, filepath.Join(cfg.StoragePath, identity.Uuid, "securefiles", hiddenFilename))
|
||||
// replace f.Data by uuid filename
|
||||
f.Data = []byte(filepath.Join(cfg.StoragePath, identity.Uuid, "securefiles", hiddenFilename))
|
||||
}
|
||||
}
|
||||
outbound := true
|
||||
if usermessage.From == peer.ContactPublicKey {
|
||||
outbound = false
|
||||
}
|
||||
// Convert UserMessage to DbMessage
|
||||
dbm := UserMessageToDbMessage(outbound, usermessage, hiddenFilenames)
|
||||
// Encrypt message
|
||||
out, err := proto.Marshal(dbm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
encData, err := meowlib.SymEncrypt(password, out)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Insert message
|
||||
insertMessageSQL := `INSERT INTO message(m) VALUES (?) RETURNING ID`
|
||||
statement, err := db.Prepare(insertMessageSQL) // Prepare statement.
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result, err := statement.Exec(encData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ium := DbMessageToInternalUserMessage(id, dbid, dbm)
|
||||
peer.LastMessage = ium
|
||||
identity.Peers.StorePeer(peer)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get new messages from a peer
|
||||
func loadNewMessages(peer *Peer, lastDbId int, password string) ([]*InternalUserMessage, error) {
|
||||
var messages []*InternalUserMessage
|
||||
cfg := GetConfig()
|
||||
identity := cfg.GetIdentity()
|
||||
// handle no db yet
|
||||
if len(peer.DbIds) == 0 {
|
||||
return messages, nil
|
||||
}
|
||||
fileidx := len(peer.DbIds) - 1
|
||||
// There fileidx should provide the db that we need (unless wantMore overlaps the next DB)
|
||||
db, err := sql.Open("sqlite3", filepath.Join(cfg.StoragePath, identity.Uuid, peer.DbIds[fileidx]+GetConfig().DbSuffix)) // Open the created SQLite File
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer db.Close()
|
||||
// if it's first app query, it won't hold a lastIndex, so let's start from end
|
||||
if lastDbId == 0 {
|
||||
lastDbId = math.MaxInt64
|
||||
}
|
||||
stm, err := db.Prepare("SELECT id, m FROM message WHERE id > ? ORDER BY id DESC")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer stm.Close()
|
||||
rows, err := stm.Query(lastDbId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var ium *InternalUserMessage
|
||||
var dbm meowlib.DbMessage
|
||||
var id int64
|
||||
var m []byte
|
||||
err = rows.Scan(&id, &m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decdata, err := meowlib.SymDecrypt(password, m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = proto.Unmarshal(decdata, &dbm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ium = DbMessageToInternalUserMessage(id, peer.DbIds[fileidx], &dbm)
|
||||
ium.Dbid = id
|
||||
ium.Dbfile = peer.DbIds[fileidx]
|
||||
messages = append(messages, ium)
|
||||
}
|
||||
// TODO DB overlap
|
||||
return messages, nil
|
||||
}
|
||||
|
||||
// Get old messages from a peer
|
||||
func loadMessagesHistory(peer *Peer, inAppMsgCount int, lastDbId int, wantMore int, password string) ([]InternalUserMessage, error) {
|
||||
var messages []InternalUserMessage
|
||||
// handle no db yet
|
||||
if len(peer.DbIds) == 0 {
|
||||
return messages, nil
|
||||
}
|
||||
fileidx := len(peer.DbIds) - 1
|
||||
// initialize count with last db message count
|
||||
countStack, err := getMessageCount(peer.DbIds[fileidx])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// while the db message count < what we already have in app, step to next db file
|
||||
for inAppMsgCount > countStack {
|
||||
fileidx--
|
||||
if fileidx < 0 {
|
||||
return nil, nil
|
||||
}
|
||||
newCount, err := getMessageCount(peer.DbIds[fileidx])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
countStack += newCount
|
||||
}
|
||||
// There fileidx should provide the db that we need (unless wantMore overlaps the next DB)
|
||||
db, err := sql.Open("sqlite3", filepath.Join(GetConfig().StoragePath, GetConfig().GetIdentity().Uuid, peer.DbIds[fileidx]+GetConfig().DbSuffix)) // Open the created SQLite File
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer db.Close()
|
||||
// if it's first app query, it won't hold a lastIndex, so let's start from end
|
||||
if lastDbId == 0 {
|
||||
lastDbId = math.MaxInt64
|
||||
}
|
||||
stm, err := db.Prepare("SELECT id, m FROM message WHERE id < ? ORDER BY id DESC LIMIT ?")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer stm.Close()
|
||||
rows, err := stm.Query(lastDbId, wantMore)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var ium *InternalUserMessage
|
||||
var dbm meowlib.DbMessage
|
||||
var id int64
|
||||
var m []byte
|
||||
err = rows.Scan(&id, &m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decdata, err := meowlib.SymDecrypt(password, m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = proto.Unmarshal(decdata, &dbm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ium = DbMessageToInternalUserMessage(id, peer.DbIds[fileidx], &dbm)
|
||||
ium.Dbid = id
|
||||
ium.Dbfile = peer.DbIds[fileidx]
|
||||
|
||||
messages = append(messages, *ium)
|
||||
}
|
||||
// TODO DB overlap
|
||||
return messages, nil
|
||||
}
|
||||
|
||||
func GetDbMessage(dbFile string, dbId int64, password string) (*meowlib.DbMessage, error) {
|
||||
// There fileidx should provide the db that we need (unless wantMore overlaps the next DB)
|
||||
db, err := sql.Open("sqlite3", filepath.Join(GetConfig().StoragePath, GetConfig().GetIdentity().Uuid, dbFile+GetConfig().DbSuffix)) // Open the created SQLite dbFile
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
stm, err := db.Prepare("SELECT id, m FROM message WHERE id=?")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer stm.Close()
|
||||
rows, err := stm.Query(dbId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var dbm meowlib.DbMessage
|
||||
found := false
|
||||
for rows.Next() {
|
||||
found = true
|
||||
var id int64
|
||||
var m []byte
|
||||
err = rows.Scan(&id, &m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decdata, err := meowlib.SymDecrypt(password, m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = proto.Unmarshal(decdata, &dbm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return nil, fmt.Errorf("message row %d not found in %s", dbId, dbFile)
|
||||
}
|
||||
return &dbm, nil
|
||||
}
|
||||
|
||||
func UpdateDbMessage(dbm *meowlib.DbMessage, dbFile string, dbId int64, password string) error {
|
||||
db, err := sql.Open("sqlite3", filepath.Join(GetConfig().StoragePath, GetConfig().GetIdentity().Uuid, dbFile+GetConfig().DbSuffix)) // Open the created SQLite dbFile
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.Close()
|
||||
// Encrypt message
|
||||
out, err := proto.Marshal(dbm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
encData, err := meowlib.SymEncrypt(password, out)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Insert message
|
||||
updateMessageSQL := `UPDATE message SET m=? WHERE id=?`
|
||||
statement, err := db.Prepare(updateMessageSQL) // Prepare statement.
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = statement.Exec(encData, dbId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get old messages from a peer
|
||||
func GetMessagePreview(dbFile string, dbId int64, password string) ([]byte, error) {
|
||||
dbm, err := GetDbMessage(dbFile, dbId, password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return FilePreview(dbm.FilePaths[0], password)
|
||||
}
|
||||
|
||||
// decrypt the a file and returns the raw content
|
||||
func FilePreview(filename string, password string) ([]byte, error) {
|
||||
// get the hidden file
|
||||
encData, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// decrypt the file
|
||||
data, err := meowlib.SymDecrypt(password, encData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// return the raw content from the files content (loads the first image, or build a more complex view)
|
||||
func InternalUserMessagePreview(msg *InternalUserMessage, password string) ([]byte, error) {
|
||||
// get the hidden file name
|
||||
if len(msg.FilePaths) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return FilePreview(msg.FilePaths[0], password)
|
||||
}
|
||||
|
||||
func getMessageCount(dbid string) (int, error) {
|
||||
db, err := sql.Open("sqlite3", filepath.Join(GetConfig().StoragePath, GetConfig().GetIdentity().Uuid, dbid+GetConfig().DbSuffix)) // Open the created SQLite File
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer db.Close()
|
||||
var count int
|
||||
query := "SELECT COUNT(*) FROM message"
|
||||
err = db.QueryRow(query).Scan(&count)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// SetMessageServerDelivery updates the server delivery UUID and timestamp for an existing stored message.
|
||||
func SetMessageServerDelivery(dbFile string, dbId int64, serverUid string, receiveTime uint64, password string) error {
|
||||
dbm, err := GetDbMessage(dbFile, dbId, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dbm.ServerDeliveryUuid = serverUid
|
||||
dbm.ServerDeliveryTimestamp = receiveTime
|
||||
return UpdateDbMessage(dbm, dbFile, dbId, password)
|
||||
}
|
||||
|
||||
// FindMessageByUuid scans all DB files for a peer (newest first) and returns
|
||||
// the dbFile, row ID, and DbMessage for the message whose Status.Uuid matches.
|
||||
func FindMessageByUuid(peer *Peer, messageUuid string, password string) (string, int64, *meowlib.DbMessage, error) {
|
||||
cfg := GetConfig()
|
||||
identity := cfg.GetIdentity()
|
||||
for i := len(peer.DbIds) - 1; i >= 0; i-- {
|
||||
dbid := peer.DbIds[i]
|
||||
db, err := sql.Open("sqlite3", filepath.Join(cfg.StoragePath, identity.Uuid, dbid+GetConfig().DbSuffix))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
rows, err := db.Query("SELECT id, m FROM message ORDER BY id DESC")
|
||||
if err != nil {
|
||||
db.Close()
|
||||
continue
|
||||
}
|
||||
for rows.Next() {
|
||||
var id int64
|
||||
var m []byte
|
||||
if err := rows.Scan(&id, &m); err != nil {
|
||||
continue
|
||||
}
|
||||
decdata, err := meowlib.SymDecrypt(password, m)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
var dbm meowlib.DbMessage
|
||||
if err := proto.Unmarshal(decdata, &dbm); err != nil {
|
||||
continue
|
||||
}
|
||||
if dbm.Status != nil && dbm.Status.Uuid == messageUuid {
|
||||
rows.Close()
|
||||
db.Close()
|
||||
return dbid, id, &dbm, nil
|
||||
}
|
||||
}
|
||||
rows.Close()
|
||||
db.Close()
|
||||
}
|
||||
return "", 0, nil, fmt.Errorf("message with UUID %s not found", messageUuid)
|
||||
}
|
||||
|
||||
// UpdateMessageAck finds a stored outbound message by UUID and stamps it with
|
||||
// the received and/or processed timestamps from an inbound ACK message.
|
||||
func UpdateMessageAck(peer *Peer, messageUuid string, receivedAt uint64, processedAt uint64, password string) error {
|
||||
dbFile, dbId, dbm, err := FindMessageByUuid(peer, messageUuid, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if dbm.Status == nil {
|
||||
dbm.Status = &meowlib.ConversationStatus{}
|
||||
}
|
||||
if receivedAt != 0 {
|
||||
dbm.Status.Received = receivedAt
|
||||
}
|
||||
if processedAt != 0 {
|
||||
dbm.Status.Processed = processedAt
|
||||
}
|
||||
return UpdateDbMessage(dbm, dbFile, dbId, password)
|
||||
}
|
||||
|
||||
func createMessageTable(db *sql.DB) error {
|
||||
createMessageTableSQL := `CREATE TABLE message (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"m" BLOB);` // SQL Statement for Create Table
|
||||
statement, err := db.Prepare(createMessageTableSQL) // Prepare SQL Statement
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
statement.Exec() // Execute SQL Statements
|
||||
return nil
|
||||
}
|
||||
|
||||
func createServerTable(db *sql.DB) error {
|
||||
createServerTableSQL := `CREATE TABLE servers (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"country" varchar(2),
|
||||
"public" bool,
|
||||
"uptime" int,
|
||||
"bandwith" float,
|
||||
"load" float,
|
||||
"url" varchar(2000)
|
||||
"name" varchar(255);
|
||||
"description" varchar(5000)
|
||||
"publickey" varchar(10000)
|
||||
)` // SQL Statement for Create Table
|
||||
statement, err := db.Prepare(createServerTableSQL) // Prepare SQL Statement
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
statement.Exec() // Execute SQL Statements
|
||||
return nil
|
||||
}
|
||||
@@ -11,20 +11,28 @@ import (
|
||||
)
|
||||
|
||||
func TestStoreMessage(t *testing.T) {
|
||||
id := createId()
|
||||
var um meowlib.UserMessage
|
||||
um.Data = []byte("blabla")
|
||||
err := StoreMessage(&id.Peers[0], &um, GetConfig().memoryPassword)
|
||||
id := createId(t)
|
||||
password, err := GetConfig().GetMemPass()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
messages, err := GetLastMessages(&id.Peers[0], 0, 0, 10, GetConfig().memoryPassword)
|
||||
var um meowlib.UserMessage
|
||||
um.Data = []byte("blabla")
|
||||
peers, err := id.Peers.GetPeers()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = storeMessage(peers[0], &um, []string{}, password)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
messages, err := loadMessagesHistory(peers[0], 0, 0, 10, password)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// Checks
|
||||
assert.Equal(t, len(messages), 1, "not 1 message")
|
||||
assert.Equal(t, messages[0].message.Data, um.Data, "not 1 message")
|
||||
assert.Equal(t, messages[0].Message, string(um.Data), "not 1 message")
|
||||
// Cleanup
|
||||
if exists("test.id") {
|
||||
os.Remove("test.id")
|
||||
@@ -41,16 +49,27 @@ func TestStoreMessage(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestManyStoreMessage(t *testing.T) {
|
||||
id := createId()
|
||||
id := createId(t)
|
||||
password, err := GetConfig().GetMemPass()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
peers, err := id.Peers.GetPeers()
|
||||
// test with zero messages
|
||||
messages, err := loadMessagesHistory(peers[0], 0, 0, 10, password)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, len(messages), 0, "not 0 message")
|
||||
for i := 1; i < 100; i++ {
|
||||
var um meowlib.UserMessage
|
||||
um.Data = []byte(randomLenString(20, 200))
|
||||
err := StoreMessage(&id.Peers[0], &um, GetConfig().memoryPassword)
|
||||
err := storeMessage(peers[0], &um, []string{}, password)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
messages, err := GetLastMessages(&id.Peers[0], 0, 0, 10, GetConfig().memoryPassword)
|
||||
messages, err = loadMessagesHistory(peers[0], 0, 0, 10, password)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
+273
-36
@@ -1,12 +1,13 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"forge.redroom.link/yves/meowlib"
|
||||
doubleratchet "github.com/status-im/doubleratchet"
|
||||
"github.com/google/uuid"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
@@ -14,44 +15,116 @@ import (
|
||||
// Peer manages the peer messaging functions
|
||||
// - Building simple user messages
|
||||
// - Utility functions for packing/unpacking, encrypting/decrypting messages for peer communication
|
||||
// - Peer might be of type "contact" "group" "personnae" "channel" "device" "sensor"
|
||||
type Peer struct {
|
||||
Uid string `json:"uid,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Avatar string `json:"avatar,omitempty"`
|
||||
Avatars []Avatar `json:"avatars,omitempty"`
|
||||
MyName string `json:"my_name,omitempty"`
|
||||
MyAvatar string `json:"my_avatar,omitempty"`
|
||||
// Conversation []InternalMessage `json:"conversation,omitempty"`
|
||||
// My own keys for that peer
|
||||
MyIdentity meowlib.KeyPair `json:"my_identity,omitempty"`
|
||||
MyEncryptionKp meowlib.KeyPair `json:"my_encryption_kp,omitempty"`
|
||||
MyLookupKp meowlib.KeyPair `json:"my_lookup_kp,omitempty"`
|
||||
MyPullServers []meowlib.ServerCard `json:"my_pull_servers,omitempty"`
|
||||
MyIdentity *meowlib.KeyPair `json:"my_identity,omitempty"`
|
||||
MyEncryptionKp *meowlib.KeyPair `json:"my_encryption_kp,omitempty"`
|
||||
MyLookupKp *meowlib.KeyPair `json:"my_lookup_kp,omitempty"`
|
||||
MySymKey string `json:"my_sym_key,omitempty"`
|
||||
MyPullServers []string `json:"my_pull_servers,omitempty"`
|
||||
// Peer keys and infos
|
||||
Contact meowlib.ContactCard `json:"contact,omitempty"`
|
||||
//Contact meowlib.ContactCard `json:"contact,omitempty"` // todo : remove
|
||||
ContactPublicKey string `json:"contact_public_key,omitempty"`
|
||||
ContactLookupKey string `json:"contact_lookup_key,omitempty"`
|
||||
ContactEncryption string `json:"contact_encryption,omitempty"`
|
||||
ContactPullServers []string `json:"contact_pull_servers,omitempty"`
|
||||
InvitationId string `json:"invitation_id,omitempty"`
|
||||
InvitationUrl string `json:"invitation_url,omitempty"`
|
||||
InvitationMessage string `json:"invitation_message,omitempty"`
|
||||
InvitationExpiry time.Time `json:"invitation_expiry,omitempty"`
|
||||
LastMessage *InternalUserMessage `json:"last_message,omitempty"`
|
||||
// Internal management attributes
|
||||
Visible bool `json:"visible,omitempty"`
|
||||
SendDeliveryAck bool `json:"send_delivery_ack,omitempty"`
|
||||
SendProcessingAck bool `json:"send_processing_ack,omitempty"`
|
||||
VisiblePassword string `json:"visible_password,omitempty"`
|
||||
PasswordType string `json:"password_type,omitempty"`
|
||||
Blocked bool `json:"blocked,omitempty"`
|
||||
MessageNotification string `json:"message_notification,omitempty"`
|
||||
OnionMode bool `json:"onion_mode,omitempty"`
|
||||
LastMessage time.Time `json:"last_message,omitempty"`
|
||||
MatriochkaMode bool `json:"matriochka_mode,omitempty"`
|
||||
ServerDeliveryInfo bool `json:"server_delivery_info,omitempty"`
|
||||
CallsAllowed bool `json:"calls_allowed,omitempty"`
|
||||
DirectMode bool `json:"direct_mode,omitempty"`
|
||||
DbIds []string `json:"db_ids,omitempty"`
|
||||
AvatarUuid string `json:"avatar_uid,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
PersonnaeDbId string `json:"personnae_db_id,omitempty"`
|
||||
// Invitation temporary keypair (step 1 only — discarded after step 3)
|
||||
InvitationKp *meowlib.KeyPair `json:"invitation_kp,omitempty"`
|
||||
// Double Ratchet state
|
||||
DrKpPublic string `json:"dr_kp_public,omitempty"`
|
||||
DrKpPrivate string `json:"dr_kp_private,omitempty"`
|
||||
DrRootKey string `json:"dr_root_key,omitempty"`
|
||||
DrInitiator bool `json:"dr_initiator,omitempty"`
|
||||
ContactDrPublicKey string `json:"contact_dr_public_key,omitempty"`
|
||||
DrStateJson string `json:"dr_state_json,omitempty"`
|
||||
dbPassword string
|
||||
}
|
||||
|
||||
//
|
||||
// getters and setters
|
||||
//
|
||||
|
||||
func (p *Peer) GetMyContact() *meowlib.ContactCard {
|
||||
var c meowlib.ContactCard
|
||||
c.ContactPublicKey = p.MyIdentity.Public
|
||||
c.LookupPublicKey = p.MyLookupKp.Public
|
||||
c.EncryptionPublicKey = p.MyEncryptionKp.Public
|
||||
srvCards, err := GetConfig().GetIdentity().MessageServers.LoadServerCardsFromUids(p.MyPullServers)
|
||||
if err == nil {
|
||||
c.PullServers = srvCards
|
||||
}
|
||||
c.InvitationId = p.InvitationId
|
||||
c.InvitationMessage = p.InvitationMessage
|
||||
c.Name = p.MyName
|
||||
c.SymetricKey = p.MySymKey
|
||||
c.DrRootKey = p.DrRootKey
|
||||
c.DrPublicKey = p.DrKpPublic
|
||||
return &c
|
||||
}
|
||||
|
||||
func (p *Peer) GetContact() *meowlib.ContactCard {
|
||||
var c meowlib.ContactCard
|
||||
c.ContactPublicKey = p.ContactPublicKey
|
||||
c.LookupPublicKey = p.ContactLookupKey
|
||||
c.EncryptionPublicKey = p.ContactEncryption
|
||||
srvCards, err := GetConfig().GetIdentity().MessageServers.LoadServerCardsFromUids(p.ContactPullServers)
|
||||
if err == nil {
|
||||
c.PullServers = srvCards
|
||||
}
|
||||
c.InvitationId = p.InvitationId
|
||||
c.InvitationMessage = p.InvitationMessage
|
||||
c.Name = p.Name
|
||||
return &c
|
||||
}
|
||||
|
||||
func (p *Peer) InvitationPending() bool {
|
||||
if p.ContactPublicKey == "" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
//
|
||||
// Messages building
|
||||
//
|
||||
|
||||
func (p *Peer) BuildSimpleUserMessage(message []byte) (*meowlib.UserMessage, error) {
|
||||
var msg meowlib.UserMessage
|
||||
msg.Destination = p.Contact.LookupPublicKey
|
||||
msg.Destination = p.ContactLookupKey
|
||||
msg.From = p.MyIdentity.Public
|
||||
msg.Data = message
|
||||
msg.Type = "1"
|
||||
msg.Status = &meowlib.ConversationStatus{}
|
||||
msg.Status.LocalUuid = uuid.New().String()
|
||||
msg.Status.Uuid = uuid.New().String()
|
||||
|
||||
return &msg, nil
|
||||
}
|
||||
|
||||
@@ -82,17 +155,17 @@ func (p *Peer) BuildSingleFileMessage(filename string, message []byte) ([]meowli
|
||||
}
|
||||
var msg meowlib.UserMessage
|
||||
var file meowlib.File
|
||||
msg.Destination = p.Contact.LookupPublicKey
|
||||
msg.Destination = p.ContactLookupKey
|
||||
msg.From = p.MyIdentity.Public
|
||||
file.Filename = fi.Name()
|
||||
file.Chunk = uint32(chunk)
|
||||
file.Data = b[:readTotal]
|
||||
file.Data = append([]byte(nil), b[:readTotal]...)
|
||||
file.Size = uint64(fi.Size())
|
||||
msg.Files = append(msg.Files, &file)
|
||||
msg.Type = "2"
|
||||
msg.Type = "1"
|
||||
if chunk == 0 {
|
||||
msg.Status = &meowlib.ConversationStatus{}
|
||||
msg.Status.LocalUuid = uuid.New().String()
|
||||
msg.Status.Uuid = uuid.New().String()
|
||||
}
|
||||
msgs = append(msgs, msg)
|
||||
chunk++
|
||||
@@ -100,9 +173,28 @@ func (p *Peer) BuildSingleFileMessage(filename string, message []byte) ([]meowli
|
||||
return msgs, nil
|
||||
}
|
||||
|
||||
// Builds an invitation answer user message.
|
||||
// it takes as input a contactcard generated by Identity.AnswerInvitation
|
||||
func (p *Peer) BuildInvitationAnswserMessage(myContactCard *meowlib.ContactCard) (*meowlib.UserMessage, error) {
|
||||
// BuildInvitationStep2Message builds the invitee's answer UserMessage (STEP_2_SEND).
|
||||
// The ContactCard is encrypted with the initiator's temp public key via ProcessOutboundUserMessage.
|
||||
func (p *Peer) BuildInvitationStep2Message(myContactCard *meowlib.ContactCard) (*meowlib.UserMessage, error) {
|
||||
var msg meowlib.UserMessage
|
||||
var invitation meowlib.Invitation
|
||||
invitation.Step = 2
|
||||
out, err := proto.Marshal(myContactCard)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
invitation.Uuid = p.InvitationId
|
||||
invitation.Payload = out
|
||||
msg.Destination = p.ContactLookupKey
|
||||
msg.Invitation = &invitation
|
||||
msg.From = p.MyIdentity.Public
|
||||
msg.Type = "1"
|
||||
return &msg, nil
|
||||
}
|
||||
|
||||
// BuildInvitationStep3Message builds the initiator's full ContactCard UserMessage (STEP_3_SEND).
|
||||
// Sent through the invitee's servers after the initiator has finalized their keypairs.
|
||||
func (p *Peer) BuildInvitationStep3Message(myContactCard *meowlib.ContactCard) (*meowlib.UserMessage, error) {
|
||||
var msg meowlib.UserMessage
|
||||
var invitation meowlib.Invitation
|
||||
invitation.Step = 3
|
||||
@@ -110,13 +202,51 @@ func (p *Peer) BuildInvitationAnswserMessage(myContactCard *meowlib.ContactCard)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
invitation.Uuid = p.InvitationId
|
||||
invitation.Payload = out
|
||||
msg.Destination = p.Contact.LookupPublicKey
|
||||
msg.Destination = p.ContactLookupKey
|
||||
msg.Invitation = &invitation
|
||||
msg.From = p.MyIdentity.Public
|
||||
msg.Type = "1"
|
||||
return &msg, nil
|
||||
}
|
||||
|
||||
// BuildInvitationStep4Message builds the invitee's confirmation UserMessage (STEP_4).
|
||||
// Sent through the initiator's servers to signal the invitation is complete.
|
||||
func (p *Peer) BuildInvitationStep4Message() (*meowlib.UserMessage, error) {
|
||||
var msg meowlib.UserMessage
|
||||
var invitation meowlib.Invitation
|
||||
invitation.Step = 4
|
||||
invitation.Uuid = p.InvitationId
|
||||
msg.Destination = p.ContactLookupKey
|
||||
msg.Invitation = &invitation
|
||||
msg.From = p.MyIdentity.Public
|
||||
msg.Type = "1"
|
||||
return &msg, nil
|
||||
}
|
||||
|
||||
// ProcessInboundStep2UserMessage decrypts the invitee's step-2 answer using the
|
||||
// initiator's temporary InvitationKp private key. inviteePublicKey is the sender's
|
||||
// identity public key (carried in Invitation.From by the server).
|
||||
func (p *Peer) ProcessInboundStep2UserMessage(packed *meowlib.PackedUserMessage, inviteePublicKey string) (*meowlib.UserMessage, error) {
|
||||
dec, err := meowlib.AsymDecryptAndCheck(p.InvitationKp.Private, inviteePublicKey, packed.Payload, packed.Signature)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p.DeserializeUserMessage(dec)
|
||||
}
|
||||
|
||||
// ProcessInboundStep3UserMessage decrypts the initiator's step-3 full ContactCard using
|
||||
// the invitee's MyEncryptionKp. Signature verification is skipped because the
|
||||
// initiator's identity key is not yet known — it is extracted from the decrypted payload.
|
||||
func (p *Peer) ProcessInboundStep3UserMessage(packed *meowlib.PackedUserMessage) (*meowlib.UserMessage, error) {
|
||||
dec, err := meowlib.AsymDecrypt(p.MyEncryptionKp.Private, packed.Payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p.DeserializeUserMessage(dec)
|
||||
}
|
||||
|
||||
//
|
||||
// Messages encryption and packaging
|
||||
//
|
||||
@@ -138,12 +268,34 @@ func (p *Peer) DeserializeUserMessage(data []byte) (*meowlib.UserMessage, error)
|
||||
return &msg, nil
|
||||
}
|
||||
|
||||
// SymEncryptPayload applies the shared symmetric key over already-encrypted data.
|
||||
// If MySymKey is empty, data is returned unchanged (peer has no symkey configured).
|
||||
func (p *Peer) SymEncryptPayload(data []byte) ([]byte, error) {
|
||||
if p.MySymKey == "" {
|
||||
return data, nil
|
||||
}
|
||||
return meowlib.SymEncrypt(p.MySymKey, data)
|
||||
}
|
||||
|
||||
// SymDecryptPayload removes the outer symmetric encryption layer.
|
||||
// If MySymKey is empty, data is returned unchanged.
|
||||
func (p *Peer) SymDecryptPayload(data []byte) ([]byte, error) {
|
||||
if p.MySymKey == "" {
|
||||
return data, nil
|
||||
}
|
||||
return meowlib.SymDecrypt(p.MySymKey, data)
|
||||
}
|
||||
|
||||
// AsymEncryptMessage prepares a message to send to a specific peer contact
|
||||
func (p *Peer) AsymEncryptMessage(Message []byte) (*meowlib.EncryptedMessage, error) {
|
||||
var enc *meowlib.EncryptedMessage
|
||||
enc, err := meowlib.AsymEncryptAndSign(p.Contact.EncryptionPublicKey, p.MyIdentity.Private, Message)
|
||||
// fmt.Println("[AsymEncryptMessage] Destination is:", p.ContactLookupKey)
|
||||
// fmt.Println("[AsymEncryptMessage] Contact encryption key is:", p.ContactEncryption)
|
||||
// fmt.Println("[AsymEncryptMessage] My signing key is:", p.MyIdentity.Private)
|
||||
// fmt.Println("[AsymEncryptMessage] Signature should be verified with:", p.MyIdentity.Public)
|
||||
enc, err := meowlib.AsymEncryptAndSign(p.ContactEncryption, p.MyIdentity.Private, Message)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
logger.Error().Err(err).Msg("Peer.AsymEncryptMessage")
|
||||
return enc, err
|
||||
}
|
||||
return enc, err
|
||||
@@ -151,9 +303,12 @@ func (p *Peer) AsymEncryptMessage(Message []byte) (*meowlib.EncryptedMessage, er
|
||||
|
||||
// AsymDecryptMessage reads a message from a specific peer contact
|
||||
func (p *Peer) AsymDecryptMessage(Message []byte, Signature []byte) (DecryptedMessage []byte, err error) {
|
||||
DecryptedMessage, err = meowlib.AsymDecryptAndCheck(p.MyEncryptionKp.Private, p.Contact.ContactPublicKey, Message, Signature)
|
||||
// fmt.Println("[AsymDecryptMessage] Decrypting key is:", p.MyEncryptionKp.Private)
|
||||
// fmt.Println("[AsymDecryptMessage] Should have been encrypted with:", p.MyEncryptionKp.Public)
|
||||
// fmt.Println("[AsymDecryptMessage] Signature will be verified with:", p.ContactPublicKey)
|
||||
DecryptedMessage, err = meowlib.AsymDecryptAndCheck(p.MyEncryptionKp.Private, p.ContactPublicKey, Message, Signature)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
logger.Error().Err(err).Msg("Peer.AsymDecryptMessage")
|
||||
return nil, err
|
||||
}
|
||||
return DecryptedMessage, err
|
||||
@@ -162,9 +317,12 @@ func (p *Peer) AsymDecryptMessage(Message []byte, Signature []byte) (DecryptedMe
|
||||
// PackUserMessage will package the previously encrypted message
|
||||
func (p *Peer) PackUserMessage(message []byte, signature []byte) *meowlib.PackedUserMessage {
|
||||
var msg meowlib.PackedUserMessage
|
||||
msg.Destination = p.Contact.LookupPublicKey
|
||||
msg.Destination = p.ContactLookupKey
|
||||
msg.Payload = message
|
||||
msg.Signature = signature
|
||||
if p.ServerDeliveryInfo {
|
||||
msg.ServerDeliveryUuid = uuid.New().String()
|
||||
}
|
||||
return &msg
|
||||
}
|
||||
|
||||
@@ -188,19 +346,66 @@ func (p *Peer) ProcessOutboundUserMessage(usermessage *meowlib.UserMessage) (*me
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Encrypting it
|
||||
// Asymmetric encryption + signature (inner layer)
|
||||
enc, err := p.AsymEncryptMessage(serializedMessage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Packing it
|
||||
packedMsg := p.PackUserMessage(enc.Data, enc.Signature)
|
||||
// Symmetric encryption (middle layer, if symkey is configured)
|
||||
symEncrypted, err := p.SymEncryptPayload(enc.Data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Double Ratchet encryption (outermost layer, if DR is configured)
|
||||
if p.DrRootKey != "" {
|
||||
session, err := p.GetDRSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
drMsg, err := session.RatchetEncrypt(symEncrypted, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
headerBytes, err := json.Marshal(drMsg.Header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
packed := p.PackUserMessage(drMsg.Ciphertext, enc.Signature)
|
||||
packed.DrHeader = headerBytes
|
||||
return packed, nil
|
||||
}
|
||||
// No DR layer
|
||||
packedMsg := p.PackUserMessage(symEncrypted, enc.Signature)
|
||||
return packedMsg, nil
|
||||
}
|
||||
|
||||
// ProcessInboundUserMessage is a helper function that decrypts and deserializes a user message
|
||||
func (p *Peer) ProcessInboundUserMessage(message []byte, signature []byte) (*meowlib.UserMessage, error) {
|
||||
dec, err := p.AsymDecryptMessage(message, signature)
|
||||
func (p *Peer) ProcessInboundUserMessage(packed *meowlib.PackedUserMessage) (*meowlib.UserMessage, error) {
|
||||
payload := packed.Payload
|
||||
// Double Ratchet decryption (outermost layer), only when DR is configured and header present
|
||||
if p.DrRootKey != "" && len(packed.DrHeader) > 0 {
|
||||
session, err := p.GetDRSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var header doubleratchet.MessageHeader
|
||||
if err := json.Unmarshal(packed.DrHeader, &header); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload, err = session.RatchetDecrypt(
|
||||
doubleratchet.Message{Header: header, Ciphertext: packed.Payload},
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// Symmetric decryption (middle layer, if symkey is configured)
|
||||
symDecrypted, err := p.SymDecryptPayload(payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dec, err := p.AsymDecryptMessage(symDecrypted, packed.Signature)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -220,20 +425,52 @@ func (p *Peer) SetDbPassword(password string) {
|
||||
p.dbPassword = password
|
||||
}
|
||||
|
||||
func (p *Peer) GetDbPassword() string {
|
||||
return p.dbPassword
|
||||
func (p *Peer) GetDbPassword() (string, error) {
|
||||
if p.dbPassword == "" {
|
||||
return GetConfig().GetMemPass()
|
||||
}
|
||||
return p.dbPassword, nil
|
||||
}
|
||||
|
||||
func (p *Peer) StoreMessage(msg []byte) {
|
||||
func (p *Peer) StoreMessage(msg *meowlib.UserMessage, filenames []string) error {
|
||||
password, err := p.GetDbPassword()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return storeMessage(p, msg, filenames, password)
|
||||
}
|
||||
|
||||
func (p *Peer) GetFilePreview(filename string) ([]byte, error) {
|
||||
password, err := p.GetDbPassword()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return FilePreview(filename, password)
|
||||
}
|
||||
|
||||
func (p *Peer) UpdateMessage(msg InternalUserMessage) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Peer) LoadMessagesHistory(alreadyLoadedCount int, oldestMessageId int, qty int) ([]InternalUserMessage, error) {
|
||||
password, err := p.GetDbPassword()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return loadMessagesHistory(p, alreadyLoadedCount, oldestMessageId, qty, password)
|
||||
|
||||
}
|
||||
|
||||
func (p *Peer) LoadMessage(uid string) {
|
||||
|
||||
func (p *Peer) LoadNewMessages(lastMessageId int) ([]*InternalUserMessage, error) {
|
||||
password, err := p.GetDbPassword()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return loadNewMessages(p, lastMessageId, password)
|
||||
}
|
||||
|
||||
func (p *Peer) LoadLastMessages(qty int) {
|
||||
|
||||
func (p *Peer) LoadMessage(uid string) (*InternalUserMessage, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (p *Peer) GetLastMessageUuid(msg []byte) {
|
||||
|
||||
+620
-3
@@ -1,20 +1,637 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"forge.redroom.link/yves/meowlib"
|
||||
doubleratchet "github.com/status-im/doubleratchet"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// makePeerPair creates two peers with properly cross-wired keypairs, simulating
|
||||
// a completed invitation. Alice's contact keys point to Bob's and vice versa.
|
||||
func makePeerPair(t *testing.T) (alice *Peer, bob *Peer) {
|
||||
t.Helper()
|
||||
aliceIdentity, err := meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
aliceEncryption, err := meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
aliceLookup, err := meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
bobIdentity, err := meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bobEncryption, err := meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bobLookup, err := meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
alice = &Peer{
|
||||
Uid: "alice-uid",
|
||||
Name: "bob",
|
||||
MyName: "alice",
|
||||
MyIdentity: aliceIdentity,
|
||||
MyEncryptionKp: aliceEncryption,
|
||||
MyLookupKp: aliceLookup,
|
||||
ContactPublicKey: bobIdentity.Public,
|
||||
ContactEncryption: bobEncryption.Public,
|
||||
ContactLookupKey: bobLookup.Public,
|
||||
}
|
||||
bob = &Peer{
|
||||
Uid: "bob-uid",
|
||||
Name: "alice",
|
||||
MyName: "bob",
|
||||
MyIdentity: bobIdentity,
|
||||
MyEncryptionKp: bobEncryption,
|
||||
MyLookupKp: bobLookup,
|
||||
ContactPublicKey: aliceIdentity.Public,
|
||||
ContactEncryption: aliceEncryption.Public,
|
||||
ContactLookupKey: aliceLookup.Public,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Invitation state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestInvitationPending_True(t *testing.T) {
|
||||
p := &Peer{} // ContactPublicKey is empty
|
||||
assert.True(t, p.InvitationPending())
|
||||
}
|
||||
|
||||
func TestInvitationPending_False(t *testing.T) {
|
||||
p := &Peer{ContactPublicKey: "some-key"}
|
||||
assert.False(t, p.InvitationPending())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// BuildSimpleUserMessage
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestBuildSimpleUserMessage(t *testing.T) {
|
||||
p := &Peer{
|
||||
ContactLookupKey: "dest-lookup-key",
|
||||
MyIdentity: &meowlib.KeyPair{Public: "my-pub-key"},
|
||||
}
|
||||
msg, err := p.BuildSimpleUserMessage([]byte("hello"))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "dest-lookup-key", msg.Destination)
|
||||
assert.Equal(t, "my-pub-key", msg.From)
|
||||
assert.Equal(t, []byte("hello"), msg.Data)
|
||||
assert.Equal(t, "1", msg.Type)
|
||||
assert.NotNil(t, msg.Status)
|
||||
assert.NotEmpty(t, msg.Status.Uuid)
|
||||
}
|
||||
|
||||
func TestBuildSimpleUserMessage_EmptyData(t *testing.T) {
|
||||
p := &Peer{
|
||||
ContactLookupKey: "dest",
|
||||
MyIdentity: &meowlib.KeyPair{Public: "pub"},
|
||||
}
|
||||
msg, err := p.BuildSimpleUserMessage([]byte{})
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, msg.Data)
|
||||
assert.NotEmpty(t, msg.Status.Uuid)
|
||||
}
|
||||
|
||||
func TestBuildSimpleUserMessage_UniqueUuids(t *testing.T) {
|
||||
p := &Peer{
|
||||
ContactLookupKey: "dest",
|
||||
MyIdentity: &meowlib.KeyPair{Public: "pub"},
|
||||
}
|
||||
msg1, _ := p.BuildSimpleUserMessage([]byte("a"))
|
||||
msg2, _ := p.BuildSimpleUserMessage([]byte("b"))
|
||||
assert.NotEqual(t, msg1.Status.Uuid, msg2.Status.Uuid)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// BuildSingleFileMessage
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestBuildSingleFileMessage_FileNotFound(t *testing.T) {
|
||||
p := &Peer{
|
||||
ContactLookupKey: "dest",
|
||||
MyIdentity: &meowlib.KeyPair{Public: "pub"},
|
||||
}
|
||||
GetConfig().Chunksize = 1024
|
||||
_, err := p.BuildSingleFileMessage("/nonexistent/path/file.txt", []byte("msg"))
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestBuildSingleFileMessage_SingleChunk(t *testing.T) {
|
||||
content := []byte("small file content")
|
||||
tmpFile, err := os.CreateTemp("", "peer_test_*.txt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
tmpFile.Write(content)
|
||||
tmpFile.Close()
|
||||
|
||||
p := &Peer{
|
||||
ContactLookupKey: "dest-lookup",
|
||||
MyIdentity: &meowlib.KeyPair{Public: "my-pub"},
|
||||
}
|
||||
GetConfig().Chunksize = 1024 // larger than file
|
||||
|
||||
msgs, err := p.BuildSingleFileMessage(tmpFile.Name(), []byte("msg"))
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, msgs, 1)
|
||||
assert.Equal(t, "dest-lookup", msgs[0].Destination)
|
||||
assert.Equal(t, "my-pub", msgs[0].From)
|
||||
assert.Equal(t, "1", msgs[0].Type)
|
||||
assert.Len(t, msgs[0].Files, 1)
|
||||
assert.Equal(t, content, msgs[0].Files[0].Data)
|
||||
assert.Equal(t, uint32(0), msgs[0].Files[0].Chunk)
|
||||
assert.Equal(t, uint64(len(content)), msgs[0].Files[0].Size)
|
||||
assert.NotNil(t, msgs[0].Status)
|
||||
assert.NotEmpty(t, msgs[0].Status.Uuid)
|
||||
}
|
||||
|
||||
func TestBuildSingleFileMessage_MultipleChunks(t *testing.T) {
|
||||
// 20 bytes with chunksize 7 → chunks of [7, 7, 6], last chunk guaranteed
|
||||
// to arrive with nil error on os.File before a separate (0, EOF) read.
|
||||
content := []byte("abcdefghijklmnopqrst")
|
||||
tmpFile, err := os.CreateTemp("", "peer_test_multi_*.txt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
tmpFile.Write(content)
|
||||
tmpFile.Close()
|
||||
|
||||
p := &Peer{
|
||||
ContactLookupKey: "dest-lookup",
|
||||
MyIdentity: &meowlib.KeyPair{Public: "my-pub"},
|
||||
}
|
||||
GetConfig().Chunksize = 7
|
||||
|
||||
msgs, err := p.BuildSingleFileMessage(tmpFile.Name(), []byte("msg"))
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, msgs, 3)
|
||||
|
||||
// Verify chunk indices and reassembly
|
||||
var reassembled []byte
|
||||
for i, m := range msgs {
|
||||
assert.Equal(t, uint32(i), m.Files[0].Chunk)
|
||||
assert.Equal(t, uint64(len(content)), m.Files[0].Size)
|
||||
reassembled = append(reassembled, m.Files[0].Data...)
|
||||
}
|
||||
assert.Equal(t, content, reassembled)
|
||||
|
||||
// Only the first chunk carries a status UUID
|
||||
assert.NotNil(t, msgs[0].Status)
|
||||
assert.NotEmpty(t, msgs[0].Status.Uuid)
|
||||
assert.Nil(t, msgs[1].Status)
|
||||
assert.Nil(t, msgs[2].Status)
|
||||
}
|
||||
|
||||
func TestBuildSingleFileMessage_EmptyFile(t *testing.T) {
|
||||
tmpFile, err := os.CreateTemp("", "peer_test_empty_*.txt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
tmpFile.Close() // 0 bytes
|
||||
|
||||
p := &Peer{
|
||||
ContactLookupKey: "dest",
|
||||
MyIdentity: &meowlib.KeyPair{Public: "pub"},
|
||||
}
|
||||
GetConfig().Chunksize = 1024
|
||||
|
||||
msgs, err := p.BuildSingleFileMessage(tmpFile.Name(), []byte("msg"))
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, msgs)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// BuildInvitationAnswerMessage
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestBuildInvitationStep2Message(t *testing.T) {
|
||||
p := &Peer{
|
||||
ContactLookupKey: "dest-lookup",
|
||||
MyIdentity: &meowlib.KeyPair{Public: "my-pub"},
|
||||
InvitationId: "inv-uuid-123",
|
||||
}
|
||||
contactCard := &meowlib.ContactCard{
|
||||
Name: "Alice",
|
||||
ContactPublicKey: "alice-pub",
|
||||
}
|
||||
|
||||
msg, err := p.BuildInvitationStep2Message(contactCard)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "dest-lookup", msg.Destination)
|
||||
assert.Equal(t, "my-pub", msg.From)
|
||||
assert.Equal(t, "1", msg.Type)
|
||||
assert.NotNil(t, msg.Invitation)
|
||||
assert.Equal(t, int32(2), msg.Invitation.Step)
|
||||
assert.Equal(t, "inv-uuid-123", msg.Invitation.Uuid)
|
||||
|
||||
// Payload is the proto-serialized contact card
|
||||
var decoded meowlib.ContactCard
|
||||
err = proto.Unmarshal(msg.Invitation.Payload, &decoded)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", decoded.Name)
|
||||
assert.Equal(t, "alice-pub", decoded.ContactPublicKey)
|
||||
}
|
||||
|
||||
func TestBuildInvitationStep3Message(t *testing.T) {
|
||||
p := &Peer{
|
||||
ContactLookupKey: "dest-lookup",
|
||||
MyIdentity: &meowlib.KeyPair{Public: "my-pub"},
|
||||
InvitationId: "inv-uuid-456",
|
||||
}
|
||||
contactCard := &meowlib.ContactCard{
|
||||
Name: "Initiator",
|
||||
ContactPublicKey: "init-pub",
|
||||
}
|
||||
|
||||
msg, err := p.BuildInvitationStep3Message(contactCard)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int32(3), msg.Invitation.Step)
|
||||
assert.Equal(t, "inv-uuid-456", msg.Invitation.Uuid)
|
||||
|
||||
var decoded meowlib.ContactCard
|
||||
err = proto.Unmarshal(msg.Invitation.Payload, &decoded)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Initiator", decoded.Name)
|
||||
}
|
||||
|
||||
func TestBuildInvitationStep4Message(t *testing.T) {
|
||||
p := &Peer{
|
||||
ContactLookupKey: "dest-lookup",
|
||||
MyIdentity: &meowlib.KeyPair{Public: "my-pub"},
|
||||
InvitationId: "inv-uuid-789",
|
||||
}
|
||||
|
||||
msg, err := p.BuildInvitationStep4Message()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int32(4), msg.Invitation.Step)
|
||||
assert.Equal(t, "inv-uuid-789", msg.Invitation.Uuid)
|
||||
assert.Nil(t, msg.Invitation.Payload)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Serialize / Deserialize
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestSerializeDeserializeUserMessage(t *testing.T) {
|
||||
p := &Peer{}
|
||||
original := &meowlib.UserMessage{
|
||||
Destination: "dest-key",
|
||||
From: "from-key",
|
||||
Type: "1",
|
||||
Data: []byte("test payload"),
|
||||
Status: &meowlib.ConversationStatus{Uuid: "uuid-1"},
|
||||
}
|
||||
|
||||
serialized, err := p.SerializeUserMessage(original)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, serialized)
|
||||
|
||||
restored, err := p.DeserializeUserMessage(serialized)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, original.Destination, restored.Destination)
|
||||
assert.Equal(t, original.From, restored.From)
|
||||
assert.Equal(t, original.Type, restored.Type)
|
||||
assert.Equal(t, original.Data, restored.Data)
|
||||
assert.Equal(t, original.Status.Uuid, restored.Status.Uuid)
|
||||
}
|
||||
|
||||
func TestDeserializeUserMessage_InvalidData(t *testing.T) {
|
||||
p := &Peer{}
|
||||
// tag = field 1 wire type 2 (length-delimited), length = 10, but 0 bytes follow → EOF
|
||||
_, err := p.DeserializeUserMessage([]byte{0x0a, 0x0a})
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AsymEncryptMessage / AsymDecryptMessage
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestAsymEncryptDecryptMessage_RoundTrip(t *testing.T) {
|
||||
alice, bob := makePeerPair(t)
|
||||
plaintext := []byte("secret message from alice to bob")
|
||||
|
||||
enc, err := alice.AsymEncryptMessage(plaintext)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, enc.Data)
|
||||
assert.NotEmpty(t, enc.Signature)
|
||||
|
||||
decrypted, err := bob.AsymDecryptMessage(enc.Data, enc.Signature)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, plaintext, decrypted)
|
||||
}
|
||||
|
||||
func TestAsymEncryptDecryptMessage_Bidirectional(t *testing.T) {
|
||||
alice, bob := makePeerPair(t)
|
||||
|
||||
// Alice → Bob
|
||||
enc1, err := alice.AsymEncryptMessage([]byte("alice says hi"))
|
||||
assert.NoError(t, err)
|
||||
dec1, err := bob.AsymDecryptMessage(enc1.Data, enc1.Signature)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []byte("alice says hi"), dec1)
|
||||
|
||||
// Bob → Alice
|
||||
enc2, err := bob.AsymEncryptMessage([]byte("bob says hi"))
|
||||
assert.NoError(t, err)
|
||||
dec2, err := alice.AsymDecryptMessage(enc2.Data, enc2.Signature)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []byte("bob says hi"), dec2)
|
||||
}
|
||||
|
||||
func TestAsymDecryptMessage_WrongSignatureKey(t *testing.T) {
|
||||
alice, bob := makePeerPair(t)
|
||||
|
||||
enc, err := alice.AsymEncryptMessage([]byte("hello"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Bob verifies against a random key instead of Alice's — must fail
|
||||
eve, err := meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bobTampered := *bob
|
||||
bobTampered.ContactPublicKey = eve.Public
|
||||
|
||||
_, err = bobTampered.AsymDecryptMessage(enc.Data, enc.Signature)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestAsymEncryptMessage_InvalidKey(t *testing.T) {
|
||||
p := &Peer{
|
||||
ContactEncryption: "not-a-valid-key",
|
||||
MyIdentity: &meowlib.KeyPair{Private: "also-invalid"},
|
||||
}
|
||||
_, err := p.AsymEncryptMessage([]byte("hello"))
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PackUserMessage / UnPackUserMessage
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestPackUserMessage(t *testing.T) {
|
||||
p := &Peer{
|
||||
ContactLookupKey: "dest-key",
|
||||
ServerDeliveryInfo: false,
|
||||
}
|
||||
packed := p.PackUserMessage([]byte("payload"), []byte("sig"))
|
||||
assert.Equal(t, "dest-key", packed.Destination)
|
||||
assert.Equal(t, []byte("payload"), packed.Payload)
|
||||
assert.Equal(t, []byte("sig"), packed.Signature)
|
||||
assert.Empty(t, packed.ServerDeliveryUuid)
|
||||
}
|
||||
|
||||
func TestPackUserMessage_WithDeliveryTracking(t *testing.T) {
|
||||
p := &Peer{
|
||||
ContactLookupKey: "dest-key",
|
||||
ServerDeliveryInfo: true,
|
||||
}
|
||||
packed := p.PackUserMessage([]byte("payload"), []byte("sig"))
|
||||
assert.NotEmpty(t, packed.ServerDeliveryUuid)
|
||||
|
||||
// Two calls produce different delivery UUIDs
|
||||
packed2 := p.PackUserMessage([]byte("payload"), []byte("sig"))
|
||||
assert.NotEqual(t, packed.ServerDeliveryUuid, packed2.ServerDeliveryUuid)
|
||||
}
|
||||
|
||||
func TestUnPackUserMessage(t *testing.T) {
|
||||
p := &Peer{}
|
||||
// UnPackUserMessage unmarshals a PackedServerMessage (fields 2,3 = payload, signature)
|
||||
original := &meowlib.PackedServerMessage{
|
||||
From: "sender",
|
||||
Payload: []byte("the payload"),
|
||||
Signature: []byte("the signature"),
|
||||
}
|
||||
data, err := proto.Marshal(original)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
payload, signature, err := p.UnPackUserMessage(data)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []byte("the payload"), payload)
|
||||
assert.Equal(t, []byte("the signature"), signature)
|
||||
}
|
||||
|
||||
func TestUnPackUserMessage_InvalidData(t *testing.T) {
|
||||
p := &Peer{}
|
||||
// Truncated varint — all continuation bits set, no terminator
|
||||
_, _, err := p.UnPackUserMessage([]byte{0xff, 0xff, 0xff, 0xff, 0xff})
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ProcessOutboundUserMessage / ProcessInboundUserMessage (full pipeline)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestProcessOutboundInbound_RoundTrip(t *testing.T) {
|
||||
alice, bob := makePeerPair(t)
|
||||
|
||||
userMsg, err := alice.BuildSimpleUserMessage([]byte("end to end test"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
packed, err := alice.ProcessOutboundUserMessage(userMsg)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, packed.Payload)
|
||||
assert.NotEmpty(t, packed.Signature)
|
||||
assert.Equal(t, bob.MyLookupKp.Public, packed.Destination)
|
||||
|
||||
received, err := bob.ProcessInboundUserMessage(packed)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []byte("end to end test"), received.Data)
|
||||
assert.Equal(t, alice.MyIdentity.Public, received.From)
|
||||
}
|
||||
|
||||
func TestProcessOutboundInbound_EmptyMessage(t *testing.T) {
|
||||
alice, bob := makePeerPair(t)
|
||||
|
||||
userMsg, err := alice.BuildSimpleUserMessage([]byte{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
packed, err := alice.ProcessOutboundUserMessage(userMsg)
|
||||
assert.NoError(t, err)
|
||||
|
||||
received, err := bob.ProcessInboundUserMessage(packed)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, received.Data)
|
||||
}
|
||||
|
||||
func TestProcessOutboundUserMessage_InvalidKey(t *testing.T) {
|
||||
p := &Peer{
|
||||
ContactLookupKey: "dest",
|
||||
ContactEncryption: "invalid-key",
|
||||
MyIdentity: &meowlib.KeyPair{Public: "pub", Private: "invalid-priv"},
|
||||
}
|
||||
msg, _ := p.BuildSimpleUserMessage([]byte("test"))
|
||||
_, err := p.ProcessOutboundUserMessage(msg)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DR-encrypted round-trip
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func makeDRPeerPair(t *testing.T) (alice *Peer, bob *Peer) {
|
||||
t.Helper()
|
||||
alice, bob = makePeerPair(t)
|
||||
|
||||
// Generate DR keypair for alice (initiator)
|
||||
drKp, err := doubleratchet.DefaultCrypto{}.GenerateDH()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
drRootKeyBytes := make([]byte, 32)
|
||||
if _, err = rand.Read(drRootKeyBytes); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
drRootKey := base64.StdEncoding.EncodeToString(drRootKeyBytes)
|
||||
|
||||
alice.DrKpPrivate = base64.StdEncoding.EncodeToString(drKp.PrivateKey())
|
||||
alice.DrKpPublic = base64.StdEncoding.EncodeToString(drKp.PublicKey())
|
||||
alice.DrRootKey = drRootKey
|
||||
alice.DrInitiator = true
|
||||
|
||||
bob.DrRootKey = drRootKey
|
||||
bob.ContactDrPublicKey = alice.DrKpPublic
|
||||
bob.DrInitiator = false
|
||||
|
||||
return alice, bob
|
||||
}
|
||||
|
||||
func TestProcessOutboundInbound_DR_RoundTrip(t *testing.T) {
|
||||
alice, bob := makeDRPeerPair(t)
|
||||
|
||||
userMsg, err := alice.BuildSimpleUserMessage([]byte("dr round trip test"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
packed, err := alice.ProcessOutboundUserMessage(userMsg)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, packed.DrHeader, "DR header should be set")
|
||||
|
||||
received, err := bob.ProcessInboundUserMessage(packed)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []byte("dr round trip test"), received.Data)
|
||||
|
||||
// Verify DR state was updated
|
||||
assert.NotEmpty(t, alice.DrStateJson, "alice DR state should be persisted")
|
||||
assert.NotEmpty(t, bob.DrStateJson, "bob DR state should be persisted")
|
||||
}
|
||||
|
||||
func TestProcessOutboundInbound_DR_MultipleMessages(t *testing.T) {
|
||||
alice, bob := makeDRPeerPair(t)
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
msg := []byte("message " + strconv.Itoa(i))
|
||||
userMsg, err := alice.BuildSimpleUserMessage(msg)
|
||||
assert.NoError(t, err)
|
||||
|
||||
packed, err := alice.ProcessOutboundUserMessage(userMsg)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, packed.DrHeader)
|
||||
|
||||
received, err := bob.ProcessInboundUserMessage(packed)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, msg, received.Data)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GetConversationRequest
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestGetConversationRequest(t *testing.T) {
|
||||
p := &Peer{}
|
||||
cr := p.GetConversationRequest()
|
||||
assert.NotNil(t, cr)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SetDbPassword / GetDbPassword
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestGetDbPassword_NoPasswordSet(t *testing.T) {
|
||||
p := &Peer{} // no explicit dbPassword
|
||||
GetConfig().Clean()
|
||||
_, err := p.GetDbPassword()
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestSetGetDbPassword(t *testing.T) {
|
||||
p := &Peer{}
|
||||
p.SetDbPassword("my-secret-password")
|
||||
pw, err := p.GetDbPassword()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "my-secret-password", pw)
|
||||
}
|
||||
|
||||
func TestGetDbPassword_FallbackToMemPass(t *testing.T) {
|
||||
p := &Peer{} // dbPassword not set → falls back to config
|
||||
GetConfig().SetMemPass("config-password")
|
||||
pw, err := p.GetDbPassword()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "config-password", pw)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Stub / no-op functions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestUpdateMessage_ReturnsNil(t *testing.T) {
|
||||
p := &Peer{}
|
||||
err := p.UpdateMessage(InternalUserMessage{})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLoadMessage_ReturnsNil(t *testing.T) {
|
||||
p := &Peer{}
|
||||
msg, err := p.LoadMessage("some-uid")
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, msg)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Original test (retained)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestGetFromPublicKey(t *testing.T) {
|
||||
id := CreateIdentity("test")
|
||||
id, err := CreateIdentity("test")
|
||||
if err != nil {
|
||||
t.Fatal("CreateIdentity failed")
|
||||
}
|
||||
id.Save()
|
||||
for i := 1; i < 10; i++ {
|
||||
var p Peer
|
||||
p.Uid = uuid.New().String()
|
||||
p.Name = "test" + strconv.Itoa(i)
|
||||
p.Contact.ContactPublicKey = "stringToFind" + strconv.Itoa(i)
|
||||
id.Peers = append(id.Peers, p)
|
||||
p.ContactPublicKey = "stringToFind" + strconv.Itoa(i)
|
||||
err := id.Peers.StorePeer(&p)
|
||||
if err != nil {
|
||||
t.Fatal("StorePeer failed")
|
||||
}
|
||||
}
|
||||
p5 := id.Peers.GetFromPublicKey("stringToFind5")
|
||||
assert.Equal(t, p5.Name, "test5")
|
||||
|
||||
+18
-12
@@ -1,15 +1,20 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"forge.redroom.link/yves/meowlib"
|
||||
)
|
||||
|
||||
type PeerList []Peer
|
||||
type PeerList []*Peer
|
||||
|
||||
func (pl *PeerList) GetFromPublicKey(publickey string) *Peer {
|
||||
for _, peer := range *pl {
|
||||
if peer.Contact.ContactPublicKey == publickey {
|
||||
return &peer
|
||||
if peer.ContactPublicKey == publickey {
|
||||
return peer
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pl *PeerList) GetFromInvitationId(invitationId string) *Peer {
|
||||
for _, peer := range *pl {
|
||||
if peer.InvitationId == invitationId {
|
||||
return peer
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -18,7 +23,7 @@ func (pl *PeerList) GetFromPublicKey(publickey string) *Peer {
|
||||
func (pl *PeerList) GetFromMyLookupKey(publickey string) *Peer {
|
||||
for _, peer := range *pl {
|
||||
if peer.MyLookupKp.Public == publickey {
|
||||
return &peer
|
||||
return peer
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -26,14 +31,15 @@ func (pl *PeerList) GetFromMyLookupKey(publickey string) *Peer {
|
||||
|
||||
func (pl *PeerList) GetFromName(name string) *Peer {
|
||||
for _, peer := range *pl {
|
||||
if peer.Contact.Name == name {
|
||||
return &peer
|
||||
if peer.Name == name {
|
||||
return peer
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pl *PeerList) GetConversationRequests() []*meowlib.ConversationRequest {
|
||||
// ! Wrong implementation, does not discriminate on different servers
|
||||
/*func (pl *PeerList) GetConversationRequests() []*meowlib.ConversationRequest {
|
||||
var list []*meowlib.ConversationRequest
|
||||
for _, peer := range *pl {
|
||||
var cr meowlib.ConversationRequest
|
||||
@@ -42,4 +48,4 @@ func (pl *PeerList) GetConversationRequests() []*meowlib.ConversationRequest {
|
||||
list = append(list, &cr)
|
||||
}
|
||||
return list
|
||||
}
|
||||
}*/
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
package client
|
||||
|
||||
//
|
||||
// Storage
|
||||
//
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"forge.redroom.link/yves/meowlib"
|
||||
"github.com/dgraph-io/badger"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type PeerStorage struct {
|
||||
DbFile string `json:"db_file,omitempty"`
|
||||
db *badger.DB
|
||||
cache map[string]*Peer
|
||||
}
|
||||
|
||||
// Open the badger database from struct PeerStorage
|
||||
func (ps *PeerStorage) open() error {
|
||||
if ps.DbFile == "" {
|
||||
ps.DbFile = uuid.New().String()
|
||||
GetConfig().GetIdentity().Save()
|
||||
}
|
||||
if ps.cache == nil {
|
||||
ps.cache = make(map[string]*Peer)
|
||||
}
|
||||
opts := badger.DefaultOptions(filepath.Join(GetConfig().StoragePath, GetConfig().GetIdentity().Uuid, ps.DbFile))
|
||||
opts.Logger = nil
|
||||
var err error
|
||||
ps.db, err = badger.Open(opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Store function StorePeer stores a peer in the badger database with Peer.Uid as key
|
||||
func (ps *PeerStorage) StorePeer(peer *Peer) error {
|
||||
err := ps.open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer ps.close()
|
||||
// first marshal the Peer to bytes with protobuf
|
||||
jsonsrv, err := json.Marshal(peer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
password, err := GetConfig().GetMemPass()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if peer.dbPassword != "" {
|
||||
password = peer.dbPassword
|
||||
}
|
||||
data, err := meowlib.SymEncrypt(password, jsonsrv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
shakey := sha256.Sum256([]byte(peer.Uid))
|
||||
key := shakey[:]
|
||||
// add it to cache
|
||||
ps.cache[peer.Uid] = peer
|
||||
// then store it in the database
|
||||
return ps.db.Update(func(txn *badger.Txn) error {
|
||||
return txn.Set(key, data)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// LoadPeer function loads a Peer from the badger database with Peer.GetUid() as key
|
||||
func (ps *PeerStorage) LoadPeer(uid string, password string) (*Peer, error) {
|
||||
var peer Peer
|
||||
err := ps.open()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer ps.close()
|
||||
shakey := sha256.Sum256([]byte(uid))
|
||||
key := shakey[:]
|
||||
err = ps.db.View(func(txn *badger.Txn) error {
|
||||
item, err := txn.Get(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return item.Value(func(val []byte) error {
|
||||
jsonsrv, err := meowlib.SymDecrypt(password, val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(jsonsrv, &peer)
|
||||
})
|
||||
})
|
||||
return &peer, err
|
||||
}
|
||||
|
||||
// DeletePeer function deletes a Peer from the badger database with Peer.GetUid() as key
|
||||
func (ps *PeerStorage) DeletePeer(uid string) error {
|
||||
err := ps.open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer ps.close()
|
||||
shakey := sha256.Sum256([]byte(uid))
|
||||
key := shakey[:]
|
||||
return ps.db.Update(func(txn *badger.Txn) error {
|
||||
return txn.Delete(key)
|
||||
})
|
||||
}
|
||||
|
||||
// LoadPeers function loads Peers from the badger database with a specific password
|
||||
func (ps *PeerStorage) LoadPeers(password string) ([]*Peer, error) {
|
||||
var peers []*Peer
|
||||
err := ps.open()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer ps.close()
|
||||
err = ps.db.View(func(txn *badger.Txn) error {
|
||||
opts := badger.DefaultIteratorOptions
|
||||
opts.PrefetchSize = 10
|
||||
it := txn.NewIterator(opts)
|
||||
defer it.Close()
|
||||
for it.Rewind(); it.Valid(); it.Next() {
|
||||
item := it.Item()
|
||||
var sc Peer
|
||||
err := item.Value(func(val []byte) error {
|
||||
jsonsrv, err := meowlib.SymDecrypt(password, val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(jsonsrv, &sc)
|
||||
})
|
||||
if err == nil {
|
||||
peers = append(peers, &sc)
|
||||
ps.cache[sc.Uid] = &sc
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
// Sort peers based on peer.Name
|
||||
sort.Slice(peers, func(i, j int) bool {
|
||||
return peers[i].Name < peers[j].Name
|
||||
})
|
||||
return peers, err
|
||||
}
|
||||
|
||||
// GetPeers function returns all peers from the cache as a sorted array
|
||||
func (ps *PeerStorage) GetPeers() ([]*Peer, error) {
|
||||
peers := make([]*Peer, 0, len(ps.cache))
|
||||
for _, peer := range ps.cache {
|
||||
peers = append(peers, peer)
|
||||
}
|
||||
// Sort peers based on peer.Name
|
||||
sort.Slice(peers, func(i, j int) bool {
|
||||
return peers[i].Name < peers[j].Name
|
||||
})
|
||||
return peers, nil
|
||||
}
|
||||
|
||||
// close the badger database
|
||||
func (ps *PeerStorage) close() {
|
||||
ps.db.Close()
|
||||
}
|
||||
|
||||
func (ps *PeerStorage) GetFromPublicKey(publickey string) *Peer {
|
||||
for _, peer := range ps.cache {
|
||||
if peer.ContactPublicKey == publickey {
|
||||
return peer
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *PeerStorage) GetFromInvitationId(invitationId string) *Peer {
|
||||
for _, peer := range ps.cache {
|
||||
if peer.InvitationId == invitationId {
|
||||
return peer
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *PeerStorage) GetFromMyLookupKey(publickey string) *Peer {
|
||||
for _, peer := range ps.cache {
|
||||
if peer.MyLookupKp.Public == publickey {
|
||||
return peer
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *PeerStorage) NameExists(name string) bool {
|
||||
for _, peer := range ps.cache {
|
||||
if peer.Name == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (ps *PeerStorage) GetFromName(name string) *Peer {
|
||||
for _, peer := range ps.cache {
|
||||
if peer.Name == name {
|
||||
return peer
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *PeerStorage) GetFromUid(uid string) *Peer {
|
||||
return ps.cache[uid]
|
||||
}
|
||||
|
||||
// Checks if the received contact card is an answer to an invitation, returns true if it is, and the proposed and received nicknames
|
||||
func (ps *PeerStorage) CheckInvitation(ReceivedContact *meowlib.ContactCard) (isAnswer bool, proposedNick string, receivedNick string, invitationMessage string) {
|
||||
// invitation Id found, this is an answer to an invitation
|
||||
for _, p := range ps.cache {
|
||||
if p.InvitationId == ReceivedContact.InvitationId {
|
||||
return true, p.Name, ReceivedContact.Name, ReceivedContact.InvitationMessage
|
||||
}
|
||||
}
|
||||
// it's an invitation
|
||||
return false, "", ReceivedContact.Name, ReceivedContact.InvitationMessage
|
||||
}
|
||||
|
||||
// Finalizes an invitation, returns nil if successful
|
||||
func (ps *PeerStorage) FinalizeInvitation(ReceivedContact *meowlib.ContactCard) error {
|
||||
for i, p := range ps.cache {
|
||||
if p.InvitationId == ReceivedContact.InvitationId {
|
||||
//id.Peers[i].Name = ReceivedContact.Name
|
||||
ps.cache[i].ContactEncryption = ReceivedContact.EncryptionPublicKey
|
||||
ps.cache[i].ContactLookupKey = ReceivedContact.LookupPublicKey
|
||||
ps.cache[i].ContactPublicKey = ReceivedContact.ContactPublicKey
|
||||
if ps.cache[i].MySymKey == "" {
|
||||
ps.cache[i].MySymKey = ReceivedContact.SymetricKey
|
||||
}
|
||||
srvs := []string{}
|
||||
for srv := range ReceivedContact.PullServers {
|
||||
srvs = append(srvs, ReceivedContact.PullServers[srv].GetUid())
|
||||
}
|
||||
ps.cache[i].ContactPullServers = srvs
|
||||
ps.StorePeer(ps.cache[i])
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.New("no matching contact found for invitationId " + ReceivedContact.InvitationId)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func TestStorePeer(t *testing.T) {
|
||||
id := createId(t)
|
||||
err := GetConfig().SetMemPass("test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to set password: %v", err)
|
||||
}
|
||||
GetConfig().SetIdentity(id)
|
||||
ps := &PeerStorage{
|
||||
DbFile: "peerdb.test",
|
||||
cache: nil,
|
||||
db: nil,
|
||||
}
|
||||
|
||||
peer := &Peer{
|
||||
Uid: uuid.New().String(),
|
||||
Name: "testName",
|
||||
InvitationId: "testInvitationId",
|
||||
MyName: "testMyName",
|
||||
ContactPublicKey: "testContactPublicKey",
|
||||
}
|
||||
|
||||
err = ps.StorePeer(peer)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to store peer: %v", err)
|
||||
}
|
||||
|
||||
// load the peer from the database
|
||||
password, err := GetConfig().GetMemPass()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get password: %v", err)
|
||||
}
|
||||
peers, err := ps.LoadPeers(password)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to load peers: %v", err)
|
||||
}
|
||||
if len(peers) != 1 {
|
||||
t.Errorf("Expected 1 peer, got %d", len(peers))
|
||||
}
|
||||
if peers[0].Uid != peer.Uid {
|
||||
t.Errorf("Expected peer to have uid %s, got %s", peer.Uid, peers[0].Uid)
|
||||
}
|
||||
if peers[0].MyName != peer.MyName {
|
||||
t.Errorf("Expected peer to have MyName %s, got %s", peer.MyName, peers[0].MyName)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
// SendStatus represents the delivery state of a queued send job.
|
||||
type SendStatus int
|
||||
|
||||
const (
|
||||
SendStatusPending SendStatus = 0 // waiting to be sent
|
||||
SendStatusSent SendStatus = 1 // successfully delivered
|
||||
SendStatusFailed SendStatus = 2 // all servers exhausted or timed out
|
||||
)
|
||||
|
||||
// SendJob describes a message to send, together with its delivery tracking state.
|
||||
//
|
||||
// The File field holds the path to an outbox file written by CreateUserMessageAndSendJob.
|
||||
// It must follow the naming convention outbox/{dbFile}_{dbId} so that
|
||||
// ProcessSentMessages can recover the message DB location from the filename alone.
|
||||
// Servers is tried in order; after MaxRetriesPerServer failures on one server
|
||||
// the next one is attempted.
|
||||
//
|
||||
// Tracking fields (ID, InsertedAt, Status, SentAt, Retries, SuccessfulServer)
|
||||
// are managed by the queue functions and must not be set by the caller.
|
||||
type SendJob struct {
|
||||
// --- caller-supplied fields ---
|
||||
Queue string `json:"queue,omitempty"` // uid of destination peer, used for naming the queue sqlite db
|
||||
File string `json:"file,omitempty"` // outbox file path; basename must be {dbFile}_{dbId}
|
||||
Servers []Server `json:"servers,omitempty"`
|
||||
Timeout int `json:"timeout,omitempty"` // seconds; 0 = no timeout
|
||||
|
||||
// --- DB-managed tracking fields (not serialised by the caller) ---
|
||||
ID int64
|
||||
InsertedAt time.Time
|
||||
Status SendStatus
|
||||
SentAt *time.Time
|
||||
Retries []int // retry count per server index
|
||||
SuccessfulServer *int // index into Servers of the server that accepted
|
||||
}
|
||||
|
||||
func sendQueueDbPath(storagePath, queue string) string {
|
||||
return filepath.Join(storagePath, "queues", queue)
|
||||
}
|
||||
|
||||
func openOrCreateSendQueue(dbPath string) (*sql.DB, error) {
|
||||
dir := filepath.Dir(dbPath)
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
|
||||
f, err := os.Create(dbPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
db, err := sql.Open("sqlite3", dbPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS queue (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
file TEXT NOT NULL,
|
||||
servers TEXT NOT NULL,
|
||||
timeout INTEGER NOT NULL DEFAULT 0,
|
||||
inserted_at INTEGER NOT NULL,
|
||||
status INTEGER NOT NULL DEFAULT 0,
|
||||
sent_at INTEGER,
|
||||
retries TEXT NOT NULL DEFAULT '[]',
|
||||
successful_server INTEGER
|
||||
)`)
|
||||
if err != nil {
|
||||
db.Close()
|
||||
return nil, err
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// PushSendJob appends a SendJob to the SQLite queue identified by job.Queue inside storagePath.
|
||||
// The initial retry counters are set to zero for each server.
|
||||
func PushSendJob(storagePath string, job *SendJob) error {
|
||||
db, err := openOrCreateSendQueue(sendQueueDbPath(storagePath, job.Queue))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
serversJSON, err := json.Marshal(job.Servers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
retriesJSON, err := json.Marshal(make([]int, len(job.Servers)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = db.Exec(
|
||||
`INSERT INTO queue(file, servers, timeout, inserted_at, status, retries)
|
||||
VALUES(?,?,?,?,?,?)`,
|
||||
job.File, string(serversJSON), job.Timeout, time.Now().Unix(), SendStatusPending, string(retriesJSON),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// PeekSendJob returns the oldest pending SendJob from the named queue.
|
||||
// Returns nil, 0, nil when the queue has no pending jobs.
|
||||
func PeekSendJob(storagePath, queue string) (*SendJob, int64, error) {
|
||||
db, err := openOrCreateSendQueue(sendQueueDbPath(storagePath, queue))
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
var (
|
||||
id int64
|
||||
file string
|
||||
serversJSON string
|
||||
timeout int
|
||||
insertedAt int64
|
||||
status SendStatus
|
||||
sentAt sql.NullInt64
|
||||
retriesJSON string
|
||||
successfulServer sql.NullInt64
|
||||
)
|
||||
err = db.QueryRow(
|
||||
`SELECT id, file, servers, timeout, inserted_at, status, sent_at, retries, successful_server
|
||||
FROM queue WHERE status = ? ORDER BY id ASC LIMIT 1`,
|
||||
SendStatusPending,
|
||||
).Scan(&id, &file, &serversJSON, &timeout, &insertedAt, &status, &sentAt, &retriesJSON, &successfulServer)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, 0, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
var servers []Server
|
||||
if err := json.Unmarshal([]byte(serversJSON), &servers); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
var retries []int
|
||||
if err := json.Unmarshal([]byte(retriesJSON), &retries); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
job := &SendJob{
|
||||
ID: id,
|
||||
Queue: queue,
|
||||
File: file,
|
||||
Servers: servers,
|
||||
Timeout: timeout,
|
||||
InsertedAt: time.Unix(insertedAt, 0),
|
||||
Status: status,
|
||||
Retries: retries,
|
||||
}
|
||||
if sentAt.Valid {
|
||||
t := time.Unix(sentAt.Int64, 0)
|
||||
job.SentAt = &t
|
||||
}
|
||||
if successfulServer.Valid {
|
||||
v := int(successfulServer.Int64)
|
||||
job.SuccessfulServer = &v
|
||||
}
|
||||
return job, id, nil
|
||||
}
|
||||
|
||||
// UpdateSendJob persists the tracking fields (status, sent_at, retries, successful_server)
|
||||
// for a job that was previously returned by PeekSendJob.
|
||||
func UpdateSendJob(storagePath, queue string, job *SendJob) error {
|
||||
db, err := openOrCreateSendQueue(sendQueueDbPath(storagePath, queue))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
retriesJSON, err := json.Marshal(job.Retries)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var sentAt any
|
||||
if job.SentAt != nil {
|
||||
sentAt = job.SentAt.Unix()
|
||||
}
|
||||
var successfulServer any
|
||||
if job.SuccessfulServer != nil {
|
||||
successfulServer = *job.SuccessfulServer
|
||||
}
|
||||
_, err = db.Exec(
|
||||
`UPDATE queue SET status=?, sent_at=?, retries=?, successful_server=? WHERE id=?`,
|
||||
job.Status, sentAt, string(retriesJSON), successfulServer, job.ID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetSendJob retrieves any job by row id regardless of its status.
|
||||
// Returns nil, nil when no row with that id exists.
|
||||
func GetSendJob(storagePath, queue string, id int64) (*SendJob, error) {
|
||||
db, err := openOrCreateSendQueue(sendQueueDbPath(storagePath, queue))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
var (
|
||||
file string
|
||||
serversJSON string
|
||||
timeout int
|
||||
insertedAt int64
|
||||
status SendStatus
|
||||
sentAt sql.NullInt64
|
||||
retriesJSON string
|
||||
successfulServer sql.NullInt64
|
||||
)
|
||||
err = db.QueryRow(
|
||||
`SELECT file, servers, timeout, inserted_at, status, sent_at, retries, successful_server
|
||||
FROM queue WHERE id = ?`,
|
||||
id,
|
||||
).Scan(&file, &serversJSON, &timeout, &insertedAt, &status, &sentAt, &retriesJSON, &successfulServer)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var servers []Server
|
||||
if err := json.Unmarshal([]byte(serversJSON), &servers); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var retries []int
|
||||
if err := json.Unmarshal([]byte(retriesJSON), &retries); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
job := &SendJob{
|
||||
ID: id,
|
||||
Queue: queue,
|
||||
File: file,
|
||||
Servers: servers,
|
||||
Timeout: timeout,
|
||||
InsertedAt: time.Unix(insertedAt, 0),
|
||||
Status: status,
|
||||
Retries: retries,
|
||||
}
|
||||
if sentAt.Valid {
|
||||
t := time.Unix(sentAt.Int64, 0)
|
||||
job.SentAt = &t
|
||||
}
|
||||
if successfulServer.Valid {
|
||||
v := int(successfulServer.Int64)
|
||||
job.SuccessfulServer = &v
|
||||
}
|
||||
return job, nil
|
||||
}
|
||||
|
||||
// GetSentJobs returns all successfully-sent jobs from the named queue,
|
||||
// ordered oldest first. Use this to reconcile delivery status with the
|
||||
// message store and clean up completed entries.
|
||||
func GetSentJobs(storagePath, queue string) ([]*SendJob, error) {
|
||||
db, err := openOrCreateSendQueue(sendQueueDbPath(storagePath, queue))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
rows, err := db.Query(
|
||||
`SELECT id, file, servers, timeout, inserted_at, sent_at, retries, successful_server
|
||||
FROM queue WHERE status = ? ORDER BY id ASC`,
|
||||
SendStatusSent,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var jobs []*SendJob
|
||||
for rows.Next() {
|
||||
var (
|
||||
id int64
|
||||
file string
|
||||
serversJSON string
|
||||
timeout int
|
||||
insertedAt int64
|
||||
sentAt sql.NullInt64
|
||||
retriesJSON string
|
||||
successfulServer sql.NullInt64
|
||||
)
|
||||
if err := rows.Scan(&id, &file, &serversJSON, &timeout, &insertedAt, &sentAt, &retriesJSON, &successfulServer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var servers []Server
|
||||
if err := json.Unmarshal([]byte(serversJSON), &servers); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var retries []int
|
||||
if err := json.Unmarshal([]byte(retriesJSON), &retries); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
job := &SendJob{
|
||||
ID: id,
|
||||
Queue: queue,
|
||||
File: file,
|
||||
Servers: servers,
|
||||
Timeout: timeout,
|
||||
InsertedAt: time.Unix(insertedAt, 0),
|
||||
Status: SendStatusSent,
|
||||
Retries: retries,
|
||||
}
|
||||
if sentAt.Valid {
|
||||
t := time.Unix(sentAt.Int64, 0)
|
||||
job.SentAt = &t
|
||||
}
|
||||
if successfulServer.Valid {
|
||||
v := int(successfulServer.Int64)
|
||||
job.SuccessfulServer = &v
|
||||
}
|
||||
jobs = append(jobs, job)
|
||||
}
|
||||
return jobs, nil
|
||||
}
|
||||
|
||||
// DeleteSendJob removes a row by id from the named queue.
|
||||
// If the queue is empty after deletion, the DB file is removed.
|
||||
func DeleteSendJob(storagePath, queue string, id int64) error {
|
||||
dbPath := sendQueueDbPath(storagePath, queue)
|
||||
db, err := openOrCreateSendQueue(dbPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = db.Exec(`DELETE FROM queue WHERE id=?`, id); err != nil {
|
||||
db.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
var count int
|
||||
if err = db.QueryRow(`SELECT COUNT(*) FROM queue`).Scan(&count); err != nil {
|
||||
db.Close()
|
||||
return err
|
||||
}
|
||||
db.Close()
|
||||
|
||||
if count == 0 {
|
||||
return os.Remove(dbPath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// helpers ----------------------------------------------------------------
|
||||
|
||||
func makeServers(urls ...string) []Server {
|
||||
out := make([]Server, len(urls))
|
||||
for i, u := range urls {
|
||||
out[i] = Server{Url: u}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func pushJob(t *testing.T, dir, queue, file string, servers []Server, timeout int) {
|
||||
t.Helper()
|
||||
require.NoError(t, PushSendJob(dir, &SendJob{
|
||||
Queue: queue,
|
||||
File: file,
|
||||
Servers: servers,
|
||||
Timeout: timeout,
|
||||
}))
|
||||
}
|
||||
|
||||
// tests ------------------------------------------------------------------
|
||||
|
||||
func TestPushAndPeekSendJob(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
servers := makeServers("http://s1.example", "http://s2.example")
|
||||
pushJob(t, dir, "q1", "/tmp/msg", servers, 60)
|
||||
|
||||
got, id, err := PeekSendJob(dir, "q1")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, got)
|
||||
|
||||
assert.Greater(t, id, int64(0))
|
||||
assert.Equal(t, "/tmp/msg", got.File)
|
||||
assert.Equal(t, 60, got.Timeout)
|
||||
assert.Equal(t, SendStatusPending, got.Status)
|
||||
assert.Nil(t, got.SentAt)
|
||||
assert.Nil(t, got.SuccessfulServer)
|
||||
assert.Len(t, got.Retries, 2)
|
||||
assert.Equal(t, 0, got.Retries[0])
|
||||
assert.Equal(t, 0, got.Retries[1])
|
||||
assert.WithinDuration(t, time.Now(), got.InsertedAt, 5*time.Second)
|
||||
}
|
||||
|
||||
func TestPeekSendJob_EmptyQueue(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
got, id, err := PeekSendJob(dir, "empty")
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, got)
|
||||
assert.Equal(t, int64(0), id)
|
||||
}
|
||||
|
||||
func TestPeekSendJob_OldestFirst(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
for _, f := range []string{"/a", "/b", "/c"} {
|
||||
pushJob(t, dir, "q1", f, makeServers("http://s1"), 0)
|
||||
}
|
||||
|
||||
got, _, err := PeekSendJob(dir, "q1")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, got)
|
||||
assert.Equal(t, "/a", got.File)
|
||||
}
|
||||
|
||||
func TestPeekSendJob_SkipsNonPending(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
for _, f := range []string{"/a", "/b", "/c"} {
|
||||
pushJob(t, dir, "q1", f, makeServers("http://s1"), 0)
|
||||
}
|
||||
|
||||
// mark first as sent
|
||||
first, _, err := PeekSendJob(dir, "q1")
|
||||
require.NoError(t, err)
|
||||
first.Status = SendStatusSent
|
||||
require.NoError(t, UpdateSendJob(dir, "q1", first))
|
||||
|
||||
// mark second as failed
|
||||
second, _, err := PeekSendJob(dir, "q1")
|
||||
require.NoError(t, err)
|
||||
second.Status = SendStatusFailed
|
||||
require.NoError(t, UpdateSendJob(dir, "q1", second))
|
||||
|
||||
// only /c is still pending
|
||||
got, _, err := PeekSendJob(dir, "q1")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, got)
|
||||
assert.Equal(t, "/c", got.File)
|
||||
}
|
||||
|
||||
func TestUpdateSendJob_Sent(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
pushJob(t, dir, "q1", "/tmp/f", makeServers("http://s1"), 10)
|
||||
|
||||
job, id, err := PeekSendJob(dir, "q1")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, job)
|
||||
|
||||
now := time.Now()
|
||||
srvIdx := 0
|
||||
job.Status = SendStatusSent
|
||||
job.SentAt = &now
|
||||
job.SuccessfulServer = &srvIdx
|
||||
require.NoError(t, UpdateSendJob(dir, "q1", job))
|
||||
|
||||
// persisted correctly
|
||||
got, err := GetSendJob(dir, "q1", id)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, got)
|
||||
assert.Equal(t, SendStatusSent, got.Status)
|
||||
assert.NotNil(t, got.SentAt)
|
||||
assert.WithinDuration(t, now, *got.SentAt, time.Second)
|
||||
require.NotNil(t, got.SuccessfulServer)
|
||||
assert.Equal(t, 0, *got.SuccessfulServer)
|
||||
|
||||
// no more pending jobs
|
||||
pending, _, err := PeekSendJob(dir, "q1")
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, pending)
|
||||
}
|
||||
|
||||
func TestUpdateSendJob_Retries(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
pushJob(t, dir, "q1", "/tmp/f", makeServers("http://s1", "http://s2"), 10)
|
||||
|
||||
job, id, err := PeekSendJob(dir, "q1")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, job)
|
||||
|
||||
job.Retries[0] = 2
|
||||
require.NoError(t, UpdateSendJob(dir, "q1", job))
|
||||
|
||||
got, err := GetSendJob(dir, "q1", id)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, got)
|
||||
assert.Equal(t, SendStatusPending, got.Status) // still pending
|
||||
assert.Equal(t, 2, got.Retries[0])
|
||||
assert.Equal(t, 0, got.Retries[1])
|
||||
}
|
||||
|
||||
func TestGetSendJob_NotFound(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
pushJob(t, dir, "q1", "/tmp/f", makeServers("http://s1"), 0)
|
||||
|
||||
got, err := GetSendJob(dir, "q1", 9999)
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, got)
|
||||
}
|
||||
|
||||
func TestDeleteSendJob_KeepsDbWhenNotEmpty(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
pushJob(t, dir, "q1", "/a", makeServers("http://s1"), 0)
|
||||
pushJob(t, dir, "q1", "/b", makeServers("http://s1"), 0)
|
||||
|
||||
_, id, err := PeekSendJob(dir, "q1")
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, DeleteSendJob(dir, "q1", id))
|
||||
|
||||
// DB file must still exist (second row remains)
|
||||
_, statErr := os.Stat(filepath.Join(dir, "queues", "q1"))
|
||||
require.NoError(t, statErr)
|
||||
}
|
||||
|
||||
func TestDeleteSendJob_RemovesDbWhenEmpty(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
pushJob(t, dir, "q1", "/a", makeServers("http://s1"), 0)
|
||||
|
||||
_, id, err := PeekSendJob(dir, "q1")
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, DeleteSendJob(dir, "q1", id))
|
||||
|
||||
_, statErr := os.Stat(filepath.Join(dir, "queues", "q1"))
|
||||
assert.True(t, os.IsNotExist(statErr), "DB file should be removed when queue is empty")
|
||||
}
|
||||
+153
-22
@@ -1,7 +1,7 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"forge.redroom.link/yves/meowlib"
|
||||
@@ -17,40 +17,122 @@ import (
|
||||
// - Utility functions for packing/unpacking, encrypting/decrypting messages for server communication
|
||||
// - Server remote management if ManagerKp is available for that server
|
||||
type Server struct {
|
||||
ServerData meowlib.ServerCard `json:"server_data,omitempty"`
|
||||
//ServerCard meowlib.ServerCard `json:"server_data,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
PublicKey string `json:"public_key,omitempty"`
|
||||
Url string `json:"url,omitempty"`
|
||||
Login string `json:"login,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Presence bool `json:"presence,omitempty"`
|
||||
LastCheck time.Time `json:"last_check,omitempty"`
|
||||
Uptime time.Duration `json:"uptime,omitempty"`
|
||||
Login string `json:"login,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
UserKp meowlib.KeyPair `json:"user_kp,omitempty"`
|
||||
ManagerKp meowlib.KeyPair `json:"manager_kp,omitempty"`
|
||||
UserKp *meowlib.KeyPair `json:"user_kp,omitempty"`
|
||||
ManagerKp *meowlib.KeyPair `json:"manager_kp,omitempty"`
|
||||
Country string `json:"country,omitempty"`
|
||||
AllowedDelay int `json:"allowed_delay,omitempty"`
|
||||
Backup bool `json:"backup,omitempty"`
|
||||
WebRTC bool `json:"webrtc,omitempty"`
|
||||
}
|
||||
|
||||
// CreateServerFromUrl creates a server from a basic url, ex : https://my.meowserver.example:8443/meow/
|
||||
func CreateServerFromUrl(url string) *Server {
|
||||
func CreateServerFromUrl(url string) (*Server, error) {
|
||||
var is Server
|
||||
is.ServerData.Url = url
|
||||
return &is
|
||||
var err error
|
||||
is.Name = url
|
||||
is.Url = url
|
||||
is.UserKp, err = meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &is, nil
|
||||
}
|
||||
|
||||
// CreateServerFromUid creates a server from a uid string, ex : mylogin:mypassword@https://my.meowserver.example:8443/meow/
|
||||
func CreateServerFromUid(uid string) (*Server, error) {
|
||||
var is Server
|
||||
var err error
|
||||
uidTable := strings.Split(uid, "@") //! Weak test, use regexp
|
||||
is.Name = uid
|
||||
is.UserKp, err = meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(uidTable) == 2 {
|
||||
loginpw := strings.Split(uidTable[0], ":")
|
||||
is.Url = uidTable[1]
|
||||
is.Login = loginpw[0]
|
||||
is.Password = loginpw[1]
|
||||
} else {
|
||||
is.Url = uidTable[0]
|
||||
}
|
||||
return &is, nil
|
||||
}
|
||||
|
||||
// CreateServerFromMeowUrl creates a server from a meow url, ex : meow://mylogin:mypassword@https://my.meowserver.example:8443/meow/
|
||||
func CreateServerFromMeowUrl(meowurl string) (*Server, error) {
|
||||
uid := strings.Replace(meowurl[7:], "//", "://", 1)
|
||||
return CreateServerFromUid(uid)
|
||||
}
|
||||
|
||||
// CreateServerFromInvitationLink creates a server from a meow url, ex : meow://mylogin:mypassword@https://my.meowserver.example:8443/meow?invitationCode
|
||||
func CreateServerFromInvitationLink(meowurl string) (*Server, error) {
|
||||
// remove the invitation code, last token after a /
|
||||
meowurlTable := strings.Split(meowurl, "?")
|
||||
// join all elements with / except the last one
|
||||
meowSrvUrl := meowurlTable[0]
|
||||
return CreateServerFromMeowUrl(meowSrvUrl)
|
||||
}
|
||||
|
||||
// GetServerCard returns a server card from a server
|
||||
func (ints *Server) GetServerCard() *meowlib.ServerCard {
|
||||
var sc meowlib.ServerCard
|
||||
sc.Name = ints.Name
|
||||
sc.PublicKey = ints.PublicKey
|
||||
sc.Description = ints.Description
|
||||
sc.Url = ints.Url
|
||||
sc.Login = ints.Login
|
||||
sc.Password = ints.Password
|
||||
return &sc
|
||||
}
|
||||
|
||||
func (sc *Server) GetUid() string {
|
||||
if len(sc.Login) > 0 || len(sc.Password) > 0 {
|
||||
return sc.Login + ":" + sc.Password + "@" + sc.Url
|
||||
}
|
||||
return sc.Url
|
||||
}
|
||||
|
||||
func (sc *Server) GetMeowUrl() string {
|
||||
if len(sc.Login) > 0 || len(sc.Password) > 0 {
|
||||
return sc.Login + ":" + sc.Password + "@" + sc.Url
|
||||
}
|
||||
return "meow://" + sc.Url
|
||||
}
|
||||
|
||||
// Create a server from a server card
|
||||
func CreateServerFromServerCard(server *meowlib.ServerCard) *Server {
|
||||
func CreateServerFromServerCard(server *meowlib.ServerCard) (*Server, error) {
|
||||
var is Server
|
||||
is.ServerData = *server
|
||||
is.UserKp = meowlib.NewKeyPair()
|
||||
return &is
|
||||
var err error
|
||||
is.Name = server.Name
|
||||
is.PublicKey = server.PublicKey
|
||||
is.Description = server.Description
|
||||
is.Url = server.Url
|
||||
is.Login = server.Login
|
||||
is.Password = server.Password
|
||||
is.UserKp, err = meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &is, nil
|
||||
}
|
||||
|
||||
// AsymEncryptMessage prepares a message to send to a specific internal server
|
||||
func (ints *Server) AsymEncryptMessage(Message []byte) (*meowlib.EncryptedMessage, error) {
|
||||
var enc *meowlib.EncryptedMessage
|
||||
enc, err := meowlib.AsymEncryptAndSign(ints.ServerData.PublicKey, ints.UserKp.Private, Message)
|
||||
enc, err := meowlib.AsymEncryptAndSign(ints.PublicKey, ints.UserKp.Private, Message)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
logger.Error().Err(err).Msg("Server.AsymEncryptMessage")
|
||||
return nil, err
|
||||
}
|
||||
return enc, err
|
||||
@@ -58,9 +140,9 @@ func (ints *Server) AsymEncryptMessage(Message []byte) (*meowlib.EncryptedMessag
|
||||
|
||||
// AsymDecryptMessage reads a message from a specific internal server
|
||||
func (ints *Server) AsymDecryptMessage(Message []byte, Signature []byte) (DecryptedMessage []byte, err error) {
|
||||
DecryptedMessage, err = meowlib.AsymDecryptAndCheck(ints.UserKp.Private, ints.ServerData.PublicKey, Message, Signature)
|
||||
DecryptedMessage, err = meowlib.AsymDecryptAndCheck(ints.UserKp.Private, ints.PublicKey, Message, Signature)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
logger.Error().Err(err).Msg("Server.AsymDecryptMessage")
|
||||
return nil, err
|
||||
}
|
||||
return DecryptedMessage, err
|
||||
@@ -86,6 +168,7 @@ func (ints *Server) BuildMessageSendingMessage(usermsg *meowlib.PackedUserMessag
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ! Unfinished unused ?
|
||||
// BuildMessageRequestMessage creates a message lookup message to server and returns it as protobuf serialized byte array
|
||||
func (ints *Server) BuildMessageRequestMessage(lookupKeys []string) ([]byte, error) {
|
||||
var msg meowlib.ToServerMessage
|
||||
@@ -99,12 +182,29 @@ func (ints *Server) BuildMessageRequestMessage(lookupKeys []string) ([]byte, err
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// BuildToServerMessageInvitation creates an invitation message to server and returns it as a meowlib.ToServerMessage
|
||||
// it takes as input a contactcard generated by Identity.InvitePeer
|
||||
func (ints *Server) BuildToServerMessageInvitationCreation(invitation *meowlib.ContactCard, password string, timeout int, invitationIdLen int) (*meowlib.ToServerMessage, error) {
|
||||
// BuildVideoRoomRequestMessage creates a video room request to server and returns it as protobuf serialized byte array
|
||||
func (ints *Server) BuildVideoRoomRequestMessage(users []string, expiry uint64) (*meowlib.ToServerMessage, error) {
|
||||
var msg meowlib.ToServerMessage
|
||||
msg.Uuid = uuid.New().String()
|
||||
msg.Type = "1"
|
||||
msg.From = ints.UserKp.Public
|
||||
// declare an array of meow.VideoCredential
|
||||
videocreds := make([]*meowlib.VideoCredential, len(users))
|
||||
for idx := range users {
|
||||
videocreds[idx] = &meowlib.VideoCredential{
|
||||
Username: users[idx],
|
||||
}
|
||||
}
|
||||
msg.VideoData = &meowlib.VideoData{Credentials: videocreds}
|
||||
return &msg, nil
|
||||
}
|
||||
|
||||
// BuildToServerMessageInvitationStep1 sends the InvitationInitPayload to the server (STEP_1).
|
||||
// The server stores it and returns a shortcode URL.
|
||||
func (ints *Server) BuildToServerMessageInvitationStep1(initPayload *meowlib.InvitationInitPayload, password string, timeout int, shortCodeLen int) (*meowlib.ToServerMessage, error) {
|
||||
var msg meowlib.ToServerMessage
|
||||
var inv meowlib.Invitation
|
||||
payload, err := invitation.Compress()
|
||||
payload, err := initPayload.Compress()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -113,7 +213,7 @@ func (ints *Server) BuildToServerMessageInvitationCreation(invitation *meowlib.C
|
||||
inv.Step = 1
|
||||
inv.Password = password
|
||||
inv.Timeout = int32(timeout)
|
||||
inv.ShortcodeLen = int32(invitationIdLen)
|
||||
inv.ShortcodeLen = int32(shortCodeLen)
|
||||
inv.Payload = payload
|
||||
msg.Invitation = &inv
|
||||
return &msg, nil
|
||||
@@ -132,6 +232,37 @@ func (ints *Server) BuildToServerMessageInvitationRequest(shortcode string, pass
|
||||
return &msg, nil
|
||||
}
|
||||
|
||||
// BuildToServerMessageInvitationAnswer creates an invitation answer to server and returns it as a meowlib.ToServerMessage
|
||||
// it takes as input a contactcard generated by Identity.InvitePeer
|
||||
func (ints *Server) BuildToServerMessageInvitationAnswer(invitationAnswer *meowlib.PackedUserMessage, myPublicKeyForThatPeer string, invitation_id string, timeout int) (*meowlib.ToServerMessage, error) {
|
||||
var msg meowlib.ToServerMessage
|
||||
var inv meowlib.Invitation
|
||||
invitationPayload, err := proto.Marshal(invitationAnswer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
inv.Step = 3
|
||||
inv.Uuid = invitation_id
|
||||
msg.Type = "1"
|
||||
msg.From = ints.UserKp.Public
|
||||
inv.From = myPublicKeyForThatPeer
|
||||
inv.Payload = invitationPayload
|
||||
msg.Invitation = &inv
|
||||
return &msg, nil
|
||||
}
|
||||
|
||||
// BuildToServerMessageInvitationAnswerRequest requests invitation answer with provided id from server and returns it as a meowlib.ToServerMessage
|
||||
func (ints *Server) BuildToServerMessageInvitationAnswerRequest(invitationId string) (*meowlib.ToServerMessage, error) {
|
||||
var msg meowlib.ToServerMessage
|
||||
var inv meowlib.Invitation
|
||||
msg.Type = "1"
|
||||
msg.From = ints.UserKp.Public
|
||||
inv.Step = 4
|
||||
inv.Uuid = invitationId
|
||||
msg.Invitation = &inv
|
||||
return &msg, nil
|
||||
}
|
||||
|
||||
// PackServerMessage
|
||||
func (ints *Server) PackServerMessage(payload []byte, signature []byte) (protoPackedMessage []byte, err error) {
|
||||
var msg meowlib.PackedServerMessage
|
||||
|
||||
@@ -0,0 +1,643 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"forge.redroom.link/yves/meowlib"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// makeServerPair creates two Server structs with cross-wired keypairs,
|
||||
// simulating a client and a server. clientSrv encrypts for the server;
|
||||
// serverSrv encrypts for the client.
|
||||
func makeServerPair(t *testing.T) (clientSrv *Server, serverSrv *Server) {
|
||||
t.Helper()
|
||||
clientKp, err := meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
serverKp, err := meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
clientSrv = &Server{
|
||||
Name: "client-side",
|
||||
Url: "https://server.example.com/meow",
|
||||
PublicKey: serverKp.Public,
|
||||
UserKp: clientKp,
|
||||
}
|
||||
serverSrv = &Server{
|
||||
Name: "server-side",
|
||||
Url: "https://server.example.com/meow",
|
||||
PublicKey: clientKp.Public,
|
||||
UserKp: serverKp,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CreateServerFromUrl
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestCreateServerFromUrl(t *testing.T) {
|
||||
srv, err := CreateServerFromUrl("https://example.com/meow")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://example.com/meow", srv.Name)
|
||||
assert.Equal(t, "https://example.com/meow", srv.Url)
|
||||
assert.NotNil(t, srv.UserKp)
|
||||
assert.NotEmpty(t, srv.UserKp.Public)
|
||||
assert.NotEmpty(t, srv.UserKp.Private)
|
||||
}
|
||||
|
||||
func TestCreateServerFromUrl_UniqueUserKps(t *testing.T) {
|
||||
srv1, _ := CreateServerFromUrl("https://a.example.com/meow")
|
||||
srv2, _ := CreateServerFromUrl("https://a.example.com/meow")
|
||||
assert.NotEqual(t, srv1.UserKp.Public, srv2.UserKp.Public)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CreateServerFromUid
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestCreateServerFromUid_WithCredentials(t *testing.T) {
|
||||
srv, err := CreateServerFromUid("user:pass@https://example.com/meow")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "user", srv.Login)
|
||||
assert.Equal(t, "pass", srv.Password)
|
||||
assert.Equal(t, "https://example.com/meow", srv.Url)
|
||||
assert.NotNil(t, srv.UserKp)
|
||||
}
|
||||
|
||||
func TestCreateServerFromUid_WithoutCredentials(t *testing.T) {
|
||||
srv, err := CreateServerFromUid("https://example.com/meow")
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, srv.Login)
|
||||
assert.Empty(t, srv.Password)
|
||||
assert.Equal(t, "https://example.com/meow", srv.Url)
|
||||
}
|
||||
|
||||
func TestCreateServerFromUid_NameIsFullUid(t *testing.T) {
|
||||
uid := "admin:secret@https://example.com/meow"
|
||||
srv, err := CreateServerFromUid(uid)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, uid, srv.Name)
|
||||
}
|
||||
|
||||
func TestCreateServerFromUid_PasswordOnly(t *testing.T) {
|
||||
srv, err := CreateServerFromUid(":secret@https://example.com/meow")
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, srv.Login)
|
||||
assert.Equal(t, "secret", srv.Password)
|
||||
assert.Equal(t, "https://example.com/meow", srv.Url)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CreateServerFromMeowUrl
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestCreateServerFromMeowUrl_WithCredentials(t *testing.T) {
|
||||
srv, err := CreateServerFromMeowUrl("meow://user:pass@server.example.com/meow")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "user", srv.Login)
|
||||
assert.Equal(t, "pass", srv.Password)
|
||||
assert.Equal(t, "server.example.com/meow", srv.Url)
|
||||
}
|
||||
|
||||
func TestCreateServerFromMeowUrl_NoCredentials(t *testing.T) {
|
||||
srv, err := CreateServerFromMeowUrl("meow://server.example.com/meow")
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, srv.Login)
|
||||
assert.Equal(t, "server.example.com/meow", srv.Url)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CreateServerFromInvitationLink
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestCreateServerFromInvitationLink(t *testing.T) {
|
||||
srv, err := CreateServerFromInvitationLink("meow://user:pass@server.example.com/meow?abc123")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "user", srv.Login)
|
||||
assert.Equal(t, "pass", srv.Password)
|
||||
assert.Equal(t, "server.example.com/meow", srv.Url)
|
||||
}
|
||||
|
||||
func TestCreateServerFromInvitationLink_NoQueryParam(t *testing.T) {
|
||||
srv, err := CreateServerFromInvitationLink("meow://server.example.com/meow")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "server.example.com/meow", srv.Url)
|
||||
}
|
||||
|
||||
func TestCreateServerFromInvitationLink_MultipleQuestionMarks(t *testing.T) {
|
||||
// Only the first ? splits; everything before it is the server URL
|
||||
srv, err := CreateServerFromInvitationLink("meow://server.example.com/meow?code?extra")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "server.example.com/meow", srv.Url)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CreateServerFromServerCard
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestCreateServerFromServerCard(t *testing.T) {
|
||||
card := &meowlib.ServerCard{
|
||||
Name: "MyServer",
|
||||
PublicKey: "server-pub-key",
|
||||
Description: "A test server",
|
||||
Url: "https://example.com/meow",
|
||||
Login: "admin",
|
||||
Password: "secret",
|
||||
}
|
||||
srv, err := CreateServerFromServerCard(card)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "MyServer", srv.Name)
|
||||
assert.Equal(t, "server-pub-key", srv.PublicKey)
|
||||
assert.Equal(t, "A test server", srv.Description)
|
||||
assert.Equal(t, "https://example.com/meow", srv.Url)
|
||||
assert.Equal(t, "admin", srv.Login)
|
||||
assert.Equal(t, "secret", srv.Password)
|
||||
assert.NotNil(t, srv.UserKp)
|
||||
}
|
||||
|
||||
func TestCreateServerFromServerCard_MinimalCard(t *testing.T) {
|
||||
card := &meowlib.ServerCard{Url: "https://minimal.example.com"}
|
||||
srv, err := CreateServerFromServerCard(card)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://minimal.example.com", srv.Url)
|
||||
assert.NotNil(t, srv.UserKp)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GetServerCard
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestGetServerCard(t *testing.T) {
|
||||
srv := &Server{
|
||||
Name: "MyServer",
|
||||
PublicKey: "pub123",
|
||||
Description: "desc",
|
||||
Url: "https://example.com/meow",
|
||||
Login: "user",
|
||||
Password: "pw",
|
||||
}
|
||||
card := srv.GetServerCard()
|
||||
assert.Equal(t, srv.Name, card.Name)
|
||||
assert.Equal(t, srv.PublicKey, card.PublicKey)
|
||||
assert.Equal(t, srv.Description, card.Description)
|
||||
assert.Equal(t, srv.Url, card.Url)
|
||||
assert.Equal(t, srv.Login, card.Login)
|
||||
assert.Equal(t, srv.Password, card.Password)
|
||||
}
|
||||
|
||||
func TestGetServerCard_RoundTrip(t *testing.T) {
|
||||
card := &meowlib.ServerCard{
|
||||
Name: "RT",
|
||||
PublicKey: "pk",
|
||||
Description: "roundtrip",
|
||||
Url: "https://rt.example.com",
|
||||
Login: "l",
|
||||
Password: "p",
|
||||
}
|
||||
srv, err := CreateServerFromServerCard(card)
|
||||
assert.NoError(t, err)
|
||||
restored := srv.GetServerCard()
|
||||
assert.Equal(t, card.Name, restored.Name)
|
||||
assert.Equal(t, card.PublicKey, restored.PublicKey)
|
||||
assert.Equal(t, card.Description, restored.Description)
|
||||
assert.Equal(t, card.Url, restored.Url)
|
||||
assert.Equal(t, card.Login, restored.Login)
|
||||
assert.Equal(t, card.Password, restored.Password)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GetUid / GetMeowUrl
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestGetUid_WithCredentials(t *testing.T) {
|
||||
srv := &Server{Login: "user", Password: "pass", Url: "https://example.com/meow"}
|
||||
assert.Equal(t, "user:pass@https://example.com/meow", srv.GetUid())
|
||||
}
|
||||
|
||||
func TestGetUid_NoCredentials(t *testing.T) {
|
||||
srv := &Server{Url: "https://example.com/meow"}
|
||||
assert.Equal(t, "https://example.com/meow", srv.GetUid())
|
||||
}
|
||||
|
||||
func TestGetUid_PasswordOnly(t *testing.T) {
|
||||
srv := &Server{Password: "pass", Url: "https://example.com/meow"}
|
||||
assert.Equal(t, ":pass@https://example.com/meow", srv.GetUid())
|
||||
}
|
||||
|
||||
func TestGetMeowUrl_NoCredentials(t *testing.T) {
|
||||
srv := &Server{Url: "https://example.com/meow"}
|
||||
assert.Equal(t, "meow://https://example.com/meow", srv.GetMeowUrl())
|
||||
}
|
||||
|
||||
func TestGetMeowUrl_WithCredentials(t *testing.T) {
|
||||
srv := &Server{Login: "user", Password: "pass", Url: "https://example.com/meow"}
|
||||
// With credentials the meow:// prefix is not added — matches GetUid behaviour
|
||||
assert.Equal(t, srv.GetUid(), srv.GetMeowUrl())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AsymEncryptMessage / AsymDecryptMessage
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestServer_AsymEncryptDecrypt_RoundTrip(t *testing.T) {
|
||||
clientSrv, serverSrv := makeServerPair(t)
|
||||
plaintext := []byte("hello from client to server")
|
||||
|
||||
enc, err := clientSrv.AsymEncryptMessage(plaintext)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, enc.Data)
|
||||
assert.NotEmpty(t, enc.Signature)
|
||||
|
||||
decrypted, err := serverSrv.AsymDecryptMessage(enc.Data, enc.Signature)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, plaintext, decrypted)
|
||||
}
|
||||
|
||||
func TestServer_AsymEncryptDecrypt_Bidirectional(t *testing.T) {
|
||||
clientSrv, serverSrv := makeServerPair(t)
|
||||
|
||||
// Client → Server
|
||||
enc1, err := clientSrv.AsymEncryptMessage([]byte("client msg"))
|
||||
assert.NoError(t, err)
|
||||
dec1, err := serverSrv.AsymDecryptMessage(enc1.Data, enc1.Signature)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []byte("client msg"), dec1)
|
||||
|
||||
// Server → Client
|
||||
enc2, err := serverSrv.AsymEncryptMessage([]byte("server msg"))
|
||||
assert.NoError(t, err)
|
||||
dec2, err := clientSrv.AsymDecryptMessage(enc2.Data, enc2.Signature)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []byte("server msg"), dec2)
|
||||
}
|
||||
|
||||
func TestServer_AsymEncryptMessage_InvalidKey(t *testing.T) {
|
||||
srv := &Server{
|
||||
PublicKey: "not-a-valid-key",
|
||||
UserKp: &meowlib.KeyPair{Private: "also-invalid"},
|
||||
}
|
||||
_, err := srv.AsymEncryptMessage([]byte("test"))
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestServer_AsymDecryptMessage_WrongSignatureKey(t *testing.T) {
|
||||
clientSrv, serverSrv := makeServerPair(t)
|
||||
|
||||
enc, err := clientSrv.AsymEncryptMessage([]byte("hello"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Replace expected sender key with a random one
|
||||
eve, err := meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
serverSrv.PublicKey = eve.Public
|
||||
|
||||
_, err = serverSrv.AsymDecryptMessage(enc.Data, enc.Signature)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PackServerMessage / UnPackServerMessage
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestServer_PackUnPack_RoundTrip(t *testing.T) {
|
||||
srv, err := CreateServerFromUrl("https://example.com/meow")
|
||||
assert.NoError(t, err)
|
||||
|
||||
payload := []byte("test payload")
|
||||
signature := []byte("test sig")
|
||||
|
||||
packed, err := srv.PackServerMessage(payload, signature)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, packed)
|
||||
|
||||
gotPayload, gotSig, err := srv.UnPackServerMessage(packed)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, payload, gotPayload)
|
||||
assert.Equal(t, signature, gotSig)
|
||||
}
|
||||
|
||||
func TestServer_PackServerMessage_SetsFrom(t *testing.T) {
|
||||
srv, err := CreateServerFromUrl("https://example.com/meow")
|
||||
assert.NoError(t, err)
|
||||
|
||||
packed, err := srv.PackServerMessage([]byte("p"), []byte("s"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
msg := &meowlib.PackedServerMessage{}
|
||||
err = proto.Unmarshal(packed, msg)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, srv.UserKp.Public, msg.From)
|
||||
}
|
||||
|
||||
func TestServer_UnPackServerMessage_InvalidData(t *testing.T) {
|
||||
srv := &Server{}
|
||||
_, _, err := srv.UnPackServerMessage([]byte{0xff, 0xff, 0xff, 0xff, 0xff})
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// BuildToServerMessageFromUserMessage
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestServer_BuildToServerMessageFromUserMessage(t *testing.T) {
|
||||
srv, err := CreateServerFromUrl("https://example.com/meow")
|
||||
assert.NoError(t, err)
|
||||
|
||||
pum := &meowlib.PackedUserMessage{
|
||||
Destination: "dest-key",
|
||||
Payload: []byte("encrypted"),
|
||||
Signature: []byte("sig"),
|
||||
}
|
||||
|
||||
msg := srv.BuildToServerMessageFromUserMessage(pum)
|
||||
assert.NotEmpty(t, msg.Uuid)
|
||||
assert.Equal(t, "1", msg.Type)
|
||||
assert.Equal(t, srv.UserKp.Public, msg.From)
|
||||
assert.Len(t, msg.Messages, 1)
|
||||
assert.Equal(t, pum, msg.Messages[0])
|
||||
}
|
||||
|
||||
func TestServer_BuildToServerMessageFromUserMessage_UniqueUuids(t *testing.T) {
|
||||
srv, _ := CreateServerFromUrl("https://example.com/meow")
|
||||
pum := &meowlib.PackedUserMessage{Destination: "d"}
|
||||
|
||||
msg1 := srv.BuildToServerMessageFromUserMessage(pum)
|
||||
msg2 := srv.BuildToServerMessageFromUserMessage(pum)
|
||||
assert.NotEqual(t, msg1.Uuid, msg2.Uuid)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// BuildMessageSendingMessage
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestServer_BuildMessageSendingMessage(t *testing.T) {
|
||||
srv, err := CreateServerFromUrl("https://example.com/meow")
|
||||
assert.NoError(t, err)
|
||||
|
||||
pum := &meowlib.PackedUserMessage{
|
||||
Destination: "dest",
|
||||
Payload: []byte("payload"),
|
||||
}
|
||||
|
||||
data, err := srv.BuildMessageSendingMessage(pum)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, data)
|
||||
|
||||
var msg meowlib.ToServerMessage
|
||||
err = proto.Unmarshal(data, &msg)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "1", msg.Type)
|
||||
assert.Equal(t, srv.UserKp.Public, msg.From)
|
||||
assert.Len(t, msg.Messages, 1)
|
||||
assert.Equal(t, []byte("payload"), msg.Messages[0].Payload)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// BuildMessageRequestMessage
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestServer_BuildMessageRequestMessage(t *testing.T) {
|
||||
srv, err := CreateServerFromUrl("https://example.com/meow")
|
||||
assert.NoError(t, err)
|
||||
|
||||
data, err := srv.BuildMessageRequestMessage([]string{"key1", "key2"})
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, data)
|
||||
|
||||
var msg meowlib.ToServerMessage
|
||||
err = proto.Unmarshal(data, &msg)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "1", msg.Type)
|
||||
assert.Equal(t, srv.UserKp.Public, msg.From)
|
||||
assert.NotEmpty(t, msg.Uuid)
|
||||
// Note: lookupKeys parameter is currently unused in the message body
|
||||
assert.Empty(t, msg.PullRequest)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// BuildVideoRoomRequestMessage
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestServer_BuildVideoRoomRequestMessage(t *testing.T) {
|
||||
srv, err := CreateServerFromUrl("https://example.com/meow")
|
||||
assert.NoError(t, err)
|
||||
|
||||
users := []string{"alice", "bob", "charlie"}
|
||||
msg, err := srv.BuildVideoRoomRequestMessage(users, 3600)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, msg)
|
||||
assert.Equal(t, "1", msg.Type)
|
||||
assert.Equal(t, srv.UserKp.Public, msg.From)
|
||||
assert.NotNil(t, msg.VideoData)
|
||||
assert.Len(t, msg.VideoData.Credentials, 3)
|
||||
assert.Equal(t, "alice", msg.VideoData.Credentials[0].Username)
|
||||
assert.Equal(t, "bob", msg.VideoData.Credentials[1].Username)
|
||||
assert.Equal(t, "charlie", msg.VideoData.Credentials[2].Username)
|
||||
}
|
||||
|
||||
func TestServer_BuildVideoRoomRequestMessage_SingleUser(t *testing.T) {
|
||||
srv, _ := CreateServerFromUrl("https://example.com/meow")
|
||||
msg, err := srv.BuildVideoRoomRequestMessage([]string{"solo"}, 60)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, msg.VideoData.Credentials, 1)
|
||||
assert.Equal(t, "solo", msg.VideoData.Credentials[0].Username)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// BuildToServerMessageInvitationCreation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestServer_BuildToServerMessageInvitationStep1(t *testing.T) {
|
||||
srv, err := CreateServerFromUrl("https://example.com/meow")
|
||||
assert.NoError(t, err)
|
||||
|
||||
initPayload := &meowlib.InvitationInitPayload{
|
||||
Uuid: "test-uuid",
|
||||
Name: "Alice",
|
||||
PublicKey: "alice-temp-pub",
|
||||
InvitationMessage: "Hello!",
|
||||
}
|
||||
|
||||
msg, err := srv.BuildToServerMessageInvitationStep1(initPayload, "secret", 300, 8)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "1", msg.Type)
|
||||
assert.Equal(t, srv.UserKp.Public, msg.From)
|
||||
assert.NotNil(t, msg.Invitation)
|
||||
assert.Equal(t, int32(1), msg.Invitation.Step)
|
||||
assert.Equal(t, "secret", msg.Invitation.Password)
|
||||
assert.Equal(t, int32(300), msg.Invitation.Timeout)
|
||||
assert.Equal(t, int32(8), msg.Invitation.ShortcodeLen)
|
||||
assert.NotEmpty(t, msg.Invitation.Payload)
|
||||
|
||||
// Payload is a compressed InvitationInitPayload — decompress and verify
|
||||
restored, err := meowlib.NewInvitationInitPayloadFromCompressed(msg.Invitation.Payload)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", restored.Name)
|
||||
assert.Equal(t, "alice-temp-pub", restored.PublicKey)
|
||||
assert.Equal(t, "test-uuid", restored.Uuid)
|
||||
}
|
||||
|
||||
func TestServer_BuildToServerMessageInvitationStep1_NoPassword(t *testing.T) {
|
||||
srv, _ := CreateServerFromUrl("https://example.com/meow")
|
||||
initPayload := &meowlib.InvitationInitPayload{Name: "Bob", Uuid: "bob-uuid", PublicKey: "bob-pub"}
|
||||
|
||||
msg, err := srv.BuildToServerMessageInvitationStep1(initPayload, "", 60, 6)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, msg.Invitation.Password)
|
||||
assert.Equal(t, int32(1), msg.Invitation.Step)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// BuildToServerMessageInvitationRequest
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestServer_BuildToServerMessageInvitationRequest(t *testing.T) {
|
||||
srv, err := CreateServerFromUrl("https://example.com/meow")
|
||||
assert.NoError(t, err)
|
||||
|
||||
msg, err := srv.BuildToServerMessageInvitationRequest("SC1234", "mypassword")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "1", msg.Type)
|
||||
assert.Equal(t, srv.UserKp.Public, msg.From)
|
||||
assert.NotNil(t, msg.Invitation)
|
||||
assert.Equal(t, int32(2), msg.Invitation.Step)
|
||||
assert.Equal(t, "SC1234", msg.Invitation.Shortcode)
|
||||
assert.Equal(t, "mypassword", msg.Invitation.Password)
|
||||
}
|
||||
|
||||
func TestServer_BuildToServerMessageInvitationRequest_NoPassword(t *testing.T) {
|
||||
srv, _ := CreateServerFromUrl("https://example.com/meow")
|
||||
msg, err := srv.BuildToServerMessageInvitationRequest("CODE", "")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "CODE", msg.Invitation.Shortcode)
|
||||
assert.Empty(t, msg.Invitation.Password)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// BuildToServerMessageInvitationAnswer
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestServer_BuildToServerMessageInvitationAnswer(t *testing.T) {
|
||||
srv, err := CreateServerFromUrl("https://example.com/meow")
|
||||
assert.NoError(t, err)
|
||||
|
||||
pum := &meowlib.PackedUserMessage{
|
||||
Destination: "dest",
|
||||
Payload: []byte("answer-payload"),
|
||||
Signature: []byte("answer-sig"),
|
||||
}
|
||||
|
||||
msg, err := srv.BuildToServerMessageInvitationAnswer(pum, "my-pub-key", "inv-uuid-42", 600)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "1", msg.Type)
|
||||
assert.Equal(t, srv.UserKp.Public, msg.From)
|
||||
assert.NotNil(t, msg.Invitation)
|
||||
assert.Equal(t, int32(3), msg.Invitation.Step)
|
||||
assert.Equal(t, "inv-uuid-42", msg.Invitation.Uuid)
|
||||
assert.Equal(t, "my-pub-key", msg.Invitation.From)
|
||||
assert.NotEmpty(t, msg.Invitation.Payload)
|
||||
|
||||
// Payload is proto-serialized PackedUserMessage
|
||||
var decoded meowlib.PackedUserMessage
|
||||
err = proto.Unmarshal(msg.Invitation.Payload, &decoded)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "dest", decoded.Destination)
|
||||
assert.Equal(t, []byte("answer-payload"), decoded.Payload)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// BuildToServerMessageInvitationAnswerRequest
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestServer_BuildToServerMessageInvitationAnswerRequest(t *testing.T) {
|
||||
srv, err := CreateServerFromUrl("https://example.com/meow")
|
||||
assert.NoError(t, err)
|
||||
|
||||
msg, err := srv.BuildToServerMessageInvitationAnswerRequest("inv-uuid-99")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "1", msg.Type)
|
||||
assert.Equal(t, srv.UserKp.Public, msg.From)
|
||||
assert.NotNil(t, msg.Invitation)
|
||||
assert.Equal(t, int32(4), msg.Invitation.Step)
|
||||
assert.Equal(t, "inv-uuid-99", msg.Invitation.Uuid)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ProcessOutboundMessage / ProcessInboundServerResponse (full pipeline)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestServer_ProcessOutboundMessage(t *testing.T) {
|
||||
clientSrv, serverSrv := makeServerPair(t)
|
||||
|
||||
original := &meowlib.ToServerMessage{
|
||||
Uuid: "out-uuid",
|
||||
Type: "1",
|
||||
From: clientSrv.UserKp.Public,
|
||||
}
|
||||
|
||||
packed, err := clientSrv.ProcessOutboundMessage(original)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, packed)
|
||||
|
||||
// Verify the server side can unpack and decrypt back to the original
|
||||
payload, sig, err := serverSrv.UnPackServerMessage(packed)
|
||||
assert.NoError(t, err)
|
||||
decrypted, err := serverSrv.AsymDecryptMessage(payload, sig)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var restored meowlib.ToServerMessage
|
||||
err = proto.Unmarshal(decrypted, &restored)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "out-uuid", restored.Uuid)
|
||||
}
|
||||
|
||||
func TestServer_ProcessOutboundMessage_InvalidServerKey(t *testing.T) {
|
||||
srv := &Server{
|
||||
PublicKey: "bad-key",
|
||||
UserKp: &meowlib.KeyPair{Public: "pub", Private: "bad-priv"},
|
||||
}
|
||||
msg := &meowlib.ToServerMessage{Type: "1"}
|
||||
_, err := srv.ProcessOutboundMessage(msg)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestServer_ProcessInboundServerResponse(t *testing.T) {
|
||||
clientSrv, serverSrv := makeServerPair(t)
|
||||
|
||||
original := &meowlib.FromServerMessage{
|
||||
Chat: []*meowlib.PackedUserMessage{
|
||||
{Destination: "chat-dest", Payload: []byte("chat-payload")},
|
||||
},
|
||||
}
|
||||
originalBytes, err := proto.Marshal(original)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Simulate server packing: encrypt for the client, then pack
|
||||
enc, err := serverSrv.AsymEncryptMessage(originalBytes)
|
||||
assert.NoError(t, err)
|
||||
packedMsg, err := serverSrv.PackServerMessage(enc.Data, enc.Signature)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Client processes the inbound message
|
||||
received, err := clientSrv.ProcessInboundServerResponse(packedMsg)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, received)
|
||||
assert.Len(t, received.Chat, 1)
|
||||
assert.Equal(t, "chat-dest", received.Chat[0].Destination)
|
||||
assert.Equal(t, []byte("chat-payload"), received.Chat[0].Payload)
|
||||
}
|
||||
|
||||
func TestServer_ProcessInboundServerResponse_InvalidData(t *testing.T) {
|
||||
srv := &Server{
|
||||
UserKp: &meowlib.KeyPair{Private: "invalid"},
|
||||
}
|
||||
_, err := srv.ProcessInboundServerResponse([]byte{0xff, 0xff})
|
||||
assert.Error(t, err)
|
||||
}
|
||||
+17
-9
@@ -9,14 +9,17 @@ import "errors"
|
||||
// - Matriochka paths
|
||||
type ServerList struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Servers []Server `json:"servers,omitempty"`
|
||||
Servers []*Server `json:"servers,omitempty"`
|
||||
}
|
||||
|
||||
// FilterByIdxs returns a filtered server list filtered according to an index list
|
||||
func (sl *ServerList) FilterByIdxs(MessageServerIdxs []int) (filtered *ServerList, err error) {
|
||||
filtered.Servers = []Server{}
|
||||
filtered = &ServerList{
|
||||
Name: sl.Name,
|
||||
Servers: []*Server{},
|
||||
}
|
||||
for _, i := range MessageServerIdxs {
|
||||
if i > len(sl.Servers)-1 {
|
||||
if i < 0 || i > len(sl.Servers)-1 {
|
||||
return nil, errors.New("requested server out of range of defined message servers")
|
||||
}
|
||||
}
|
||||
@@ -28,25 +31,30 @@ func (sl *ServerList) FilterByIdxs(MessageServerIdxs []int) (filtered *ServerLis
|
||||
|
||||
// GetServerByIdx returns a server from it's index
|
||||
func (sl *ServerList) GetServerByIdx(idx int) (server *Server, err error) {
|
||||
if idx > len(sl.Servers)-1 {
|
||||
if idx < 0 || idx > len(sl.Servers)-1 {
|
||||
return nil, errors.New("requested server out of range of defined message servers")
|
||||
}
|
||||
return &sl.Servers[idx], nil
|
||||
return sl.Servers[idx], nil
|
||||
}
|
||||
|
||||
// GetServerByPubkey returns a server from it's public key
|
||||
func (sl *ServerList) GetServerByPubkey(pubkey string) (filtered *Server) {
|
||||
for _, srv := range sl.Servers {
|
||||
if srv.ServerData.PublicKey == pubkey {
|
||||
return &srv
|
||||
if srv.PublicKey == pubkey {
|
||||
return srv
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddUrls is a simple utility functon used mainly as a shortcut for testing purposes
|
||||
func (sl *ServerList) AddUrls(urls []string) {
|
||||
func (sl *ServerList) AddUrls(urls []string) error {
|
||||
for _, url := range urls {
|
||||
sl.Servers = append(sl.Servers, *CreateServerFromUrl(url))
|
||||
srvnew, err := CreateServerFromUrl(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sl.Servers = append(sl.Servers, srvnew)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,346 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"forge.redroom.link/yves/meowlib"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Helper function to create a test server list with sample servers
|
||||
func createTestServerList(t *testing.T) *ServerList {
|
||||
sl := &ServerList{
|
||||
Name: "TestServerList",
|
||||
Servers: []*Server{},
|
||||
}
|
||||
|
||||
// Create servers with different public keys
|
||||
for i := 0; i < 5; i++ {
|
||||
srv, err := CreateServerFromUrl("https://server" + string(rune('0'+i)) + ".example.com/meow")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create server: %v", err)
|
||||
}
|
||||
|
||||
// Generate unique public keys for testing
|
||||
kp, err := meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create keypair: %v", err)
|
||||
}
|
||||
srv.PublicKey = kp.Public
|
||||
srv.Name = "Server" + string(rune('0'+i))
|
||||
|
||||
sl.Servers = append(sl.Servers, srv)
|
||||
}
|
||||
|
||||
return sl
|
||||
}
|
||||
|
||||
func TestServerList_FilterByIdxs(t *testing.T) {
|
||||
sl := createTestServerList(t)
|
||||
|
||||
t.Run("Filter with valid indices", func(t *testing.T) {
|
||||
indices := []int{0, 2, 4}
|
||||
filtered, err := sl.FilterByIdxs(indices)
|
||||
|
||||
assert.NoError(t, err, "FilterByIdxs should not return error for valid indices")
|
||||
assert.NotNil(t, filtered, "Filtered list should not be nil")
|
||||
assert.Equal(t, 3, len(filtered.Servers), "Should have 3 servers after filtering")
|
||||
|
||||
// Verify the filtered servers are correct
|
||||
assert.Equal(t, sl.Servers[0], filtered.Servers[0])
|
||||
assert.Equal(t, sl.Servers[2], filtered.Servers[1])
|
||||
assert.Equal(t, sl.Servers[4], filtered.Servers[2])
|
||||
})
|
||||
|
||||
t.Run("Filter with single index", func(t *testing.T) {
|
||||
indices := []int{1}
|
||||
filtered, err := sl.FilterByIdxs(indices)
|
||||
|
||||
assert.NoError(t, err, "FilterByIdxs should not return error for valid single index")
|
||||
assert.NotNil(t, filtered, "Filtered list should not be nil")
|
||||
assert.Equal(t, 1, len(filtered.Servers), "Should have 1 server after filtering")
|
||||
assert.Equal(t, sl.Servers[1], filtered.Servers[0])
|
||||
})
|
||||
|
||||
t.Run("Filter with empty indices", func(t *testing.T) {
|
||||
indices := []int{}
|
||||
filtered, err := sl.FilterByIdxs(indices)
|
||||
|
||||
assert.NoError(t, err, "FilterByIdxs should not return error for empty indices")
|
||||
assert.NotNil(t, filtered, "Filtered list should not be nil")
|
||||
assert.Equal(t, 0, len(filtered.Servers), "Should have 0 servers after filtering with empty indices")
|
||||
})
|
||||
|
||||
t.Run("Filter with out of range index", func(t *testing.T) {
|
||||
indices := []int{0, 10}
|
||||
filtered, err := sl.FilterByIdxs(indices)
|
||||
|
||||
assert.Error(t, err, "FilterByIdxs should return error for out of range index")
|
||||
assert.Nil(t, filtered, "Filtered list should be nil on error")
|
||||
assert.Contains(t, err.Error(), "out of range", "Error message should mention out of range")
|
||||
})
|
||||
|
||||
t.Run("Filter with negative index", func(t *testing.T) {
|
||||
indices := []int{-1}
|
||||
filtered, err := sl.FilterByIdxs(indices)
|
||||
|
||||
assert.Error(t, err, "FilterByIdxs should return error for negative index")
|
||||
assert.Nil(t, filtered, "Filtered list should be nil on error")
|
||||
})
|
||||
|
||||
t.Run("Filter with duplicate indices", func(t *testing.T) {
|
||||
indices := []int{1, 1, 2}
|
||||
filtered, err := sl.FilterByIdxs(indices)
|
||||
|
||||
assert.NoError(t, err, "FilterByIdxs should not return error for duplicate indices")
|
||||
assert.NotNil(t, filtered, "Filtered list should not be nil")
|
||||
// Note: duplicates will result in duplicate servers in the list
|
||||
assert.Equal(t, 3, len(filtered.Servers), "Should have 3 servers (including duplicate)")
|
||||
})
|
||||
}
|
||||
|
||||
func TestServerList_FilterByIdxs_EmptyList(t *testing.T) {
|
||||
sl := &ServerList{
|
||||
Name: "EmptyList",
|
||||
Servers: []*Server{},
|
||||
}
|
||||
|
||||
t.Run("Filter empty list with indices", func(t *testing.T) {
|
||||
indices := []int{0}
|
||||
filtered, err := sl.FilterByIdxs(indices)
|
||||
|
||||
assert.Error(t, err, "FilterByIdxs should return error when trying to filter empty list")
|
||||
assert.Nil(t, filtered, "Filtered list should be nil on error")
|
||||
})
|
||||
|
||||
t.Run("Filter empty list with empty indices", func(t *testing.T) {
|
||||
indices := []int{}
|
||||
filtered, err := sl.FilterByIdxs(indices)
|
||||
|
||||
assert.NoError(t, err, "FilterByIdxs should not return error for empty indices on empty list")
|
||||
assert.NotNil(t, filtered, "Filtered list should not be nil")
|
||||
assert.Equal(t, 0, len(filtered.Servers), "Should have 0 servers")
|
||||
})
|
||||
}
|
||||
|
||||
func TestServerList_GetServerByIdx(t *testing.T) {
|
||||
sl := createTestServerList(t)
|
||||
|
||||
t.Run("Get server with valid index", func(t *testing.T) {
|
||||
server, err := sl.GetServerByIdx(2)
|
||||
|
||||
assert.NoError(t, err, "GetServerByIdx should not return error for valid index")
|
||||
assert.NotNil(t, server, "Server should not be nil")
|
||||
assert.Equal(t, sl.Servers[2], server, "Should return the correct server")
|
||||
})
|
||||
|
||||
t.Run("Get first server", func(t *testing.T) {
|
||||
server, err := sl.GetServerByIdx(0)
|
||||
|
||||
assert.NoError(t, err, "GetServerByIdx should not return error for index 0")
|
||||
assert.NotNil(t, server, "Server should not be nil")
|
||||
assert.Equal(t, sl.Servers[0], server, "Should return the first server")
|
||||
})
|
||||
|
||||
t.Run("Get last server", func(t *testing.T) {
|
||||
lastIdx := len(sl.Servers) - 1
|
||||
server, err := sl.GetServerByIdx(lastIdx)
|
||||
|
||||
assert.NoError(t, err, "GetServerByIdx should not return error for last index")
|
||||
assert.NotNil(t, server, "Server should not be nil")
|
||||
assert.Equal(t, sl.Servers[lastIdx], server, "Should return the last server")
|
||||
})
|
||||
|
||||
t.Run("Get server with out of range index", func(t *testing.T) {
|
||||
server, err := sl.GetServerByIdx(100)
|
||||
|
||||
assert.Error(t, err, "GetServerByIdx should return error for out of range index")
|
||||
assert.Nil(t, server, "Server should be nil on error")
|
||||
assert.Contains(t, err.Error(), "out of range", "Error message should mention out of range")
|
||||
})
|
||||
|
||||
t.Run("Get server with negative index", func(t *testing.T) {
|
||||
server, err := sl.GetServerByIdx(-1)
|
||||
|
||||
assert.Error(t, err, "GetServerByIdx should return error for negative index")
|
||||
assert.Nil(t, server, "Server should be nil on error")
|
||||
})
|
||||
}
|
||||
|
||||
func TestServerList_GetServerByIdx_EmptyList(t *testing.T) {
|
||||
sl := &ServerList{
|
||||
Name: "EmptyList",
|
||||
Servers: []*Server{},
|
||||
}
|
||||
|
||||
t.Run("Get server from empty list", func(t *testing.T) {
|
||||
server, err := sl.GetServerByIdx(0)
|
||||
|
||||
assert.Error(t, err, "GetServerByIdx should return error on empty list")
|
||||
assert.Nil(t, server, "Server should be nil on error")
|
||||
})
|
||||
}
|
||||
|
||||
func TestServerList_GetServerByPubkey(t *testing.T) {
|
||||
sl := createTestServerList(t)
|
||||
|
||||
t.Run("Get server with existing public key", func(t *testing.T) {
|
||||
// Use the public key from the second server
|
||||
targetPubkey := sl.Servers[1].PublicKey
|
||||
|
||||
server := sl.GetServerByPubkey(targetPubkey)
|
||||
|
||||
assert.NotNil(t, server, "Server should not be nil when public key exists")
|
||||
assert.Equal(t, targetPubkey, server.PublicKey, "Should return server with matching public key")
|
||||
assert.Equal(t, sl.Servers[1], server, "Should return the correct server")
|
||||
})
|
||||
|
||||
t.Run("Get server with non-existent public key", func(t *testing.T) {
|
||||
server := sl.GetServerByPubkey("nonexistent-pubkey-12345")
|
||||
|
||||
assert.Nil(t, server, "Server should be nil when public key doesn't exist")
|
||||
})
|
||||
|
||||
t.Run("Get server with empty public key", func(t *testing.T) {
|
||||
server := sl.GetServerByPubkey("")
|
||||
|
||||
assert.Nil(t, server, "Server should be nil when searching for empty public key")
|
||||
})
|
||||
|
||||
t.Run("Get first matching server", func(t *testing.T) {
|
||||
// Add a duplicate public key
|
||||
duplicatePubkey := sl.Servers[0].PublicKey
|
||||
newServer, err := CreateServerFromUrl("https://duplicate.example.com/meow")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create server: %v", err)
|
||||
}
|
||||
newServer.PublicKey = duplicatePubkey
|
||||
sl.Servers = append(sl.Servers, newServer)
|
||||
|
||||
server := sl.GetServerByPubkey(duplicatePubkey)
|
||||
|
||||
assert.NotNil(t, server, "Server should not be nil")
|
||||
assert.Equal(t, duplicatePubkey, server.PublicKey)
|
||||
// Should return the first match
|
||||
assert.Equal(t, sl.Servers[0], server, "Should return the first matching server")
|
||||
})
|
||||
}
|
||||
|
||||
func TestServerList_GetServerByPubkey_EmptyList(t *testing.T) {
|
||||
sl := &ServerList{
|
||||
Name: "EmptyList",
|
||||
Servers: []*Server{},
|
||||
}
|
||||
|
||||
t.Run("Get server from empty list", func(t *testing.T) {
|
||||
server := sl.GetServerByPubkey("any-pubkey")
|
||||
|
||||
assert.Nil(t, server, "Server should be nil when searching in empty list")
|
||||
})
|
||||
}
|
||||
|
||||
func TestServerList_AddUrls(t *testing.T) {
|
||||
t.Run("Add valid URLs", func(t *testing.T) {
|
||||
sl := &ServerList{
|
||||
Name: "TestList",
|
||||
Servers: []*Server{},
|
||||
}
|
||||
|
||||
urls := []string{
|
||||
"https://server1.example.com/meow",
|
||||
"https://server2.example.com/meow",
|
||||
"https://server3.example.com/meow",
|
||||
}
|
||||
|
||||
err := sl.AddUrls(urls)
|
||||
|
||||
assert.NoError(t, err, "AddUrls should not return error for valid URLs")
|
||||
assert.Equal(t, 3, len(sl.Servers), "Should have 3 servers after adding URLs")
|
||||
|
||||
// Verify each server was created correctly
|
||||
for i, url := range urls {
|
||||
assert.Equal(t, url, sl.Servers[i].Url, "Server URL should match")
|
||||
assert.NotNil(t, sl.Servers[i].UserKp, "Server should have UserKp")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Add empty URLs list", func(t *testing.T) {
|
||||
sl := &ServerList{
|
||||
Name: "TestList",
|
||||
Servers: []*Server{},
|
||||
}
|
||||
|
||||
urls := []string{}
|
||||
err := sl.AddUrls(urls)
|
||||
|
||||
assert.NoError(t, err, "AddUrls should not return error for empty list")
|
||||
assert.Equal(t, 0, len(sl.Servers), "Should have 0 servers after adding empty list")
|
||||
})
|
||||
|
||||
t.Run("Add URLs to existing list", func(t *testing.T) {
|
||||
sl := createTestServerList(t)
|
||||
initialCount := len(sl.Servers)
|
||||
|
||||
urls := []string{
|
||||
"https://newserver1.example.com/meow",
|
||||
"https://newserver2.example.com/meow",
|
||||
}
|
||||
|
||||
err := sl.AddUrls(urls)
|
||||
|
||||
assert.NoError(t, err, "AddUrls should not return error")
|
||||
assert.Equal(t, initialCount+2, len(sl.Servers), "Should have added 2 servers to existing list")
|
||||
})
|
||||
|
||||
t.Run("Add single URL", func(t *testing.T) {
|
||||
sl := &ServerList{
|
||||
Name: "TestList",
|
||||
Servers: []*Server{},
|
||||
}
|
||||
|
||||
urls := []string{"https://single.example.com/meow"}
|
||||
err := sl.AddUrls(urls)
|
||||
|
||||
assert.NoError(t, err, "AddUrls should not return error for single URL")
|
||||
assert.Equal(t, 1, len(sl.Servers), "Should have 1 server after adding single URL")
|
||||
})
|
||||
}
|
||||
|
||||
func TestServerList_AddUrls_NilList(t *testing.T) {
|
||||
t.Run("Add URLs to nil server list", func(t *testing.T) {
|
||||
sl := &ServerList{
|
||||
Name: "TestList",
|
||||
Servers: nil, // Explicitly nil
|
||||
}
|
||||
|
||||
urls := []string{"https://server.example.com/meow"}
|
||||
err := sl.AddUrls(urls)
|
||||
|
||||
assert.NoError(t, err, "AddUrls should not return error even with nil Servers slice")
|
||||
assert.NotNil(t, sl.Servers, "Servers slice should be initialized")
|
||||
assert.Equal(t, 1, len(sl.Servers), "Should have 1 server after adding URL")
|
||||
})
|
||||
}
|
||||
|
||||
// Test the ServerList struct itself
|
||||
func TestServerList_Structure(t *testing.T) {
|
||||
t.Run("Create empty server list", func(t *testing.T) {
|
||||
sl := &ServerList{
|
||||
Name: "TestList",
|
||||
Servers: []*Server{},
|
||||
}
|
||||
|
||||
assert.NotNil(t, sl, "ServerList should not be nil")
|
||||
assert.Equal(t, "TestList", sl.Name, "Name should be set correctly")
|
||||
assert.NotNil(t, sl.Servers, "Servers slice should not be nil")
|
||||
assert.Equal(t, 0, len(sl.Servers), "Servers slice should be empty")
|
||||
})
|
||||
|
||||
t.Run("Create server list with name", func(t *testing.T) {
|
||||
sl := &ServerList{
|
||||
Name: "MyMessageServers",
|
||||
}
|
||||
|
||||
assert.Equal(t, "MyMessageServers", sl.Name)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
package client
|
||||
|
||||
//
|
||||
// Storage
|
||||
//
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
|
||||
"forge.redroom.link/yves/meowlib"
|
||||
"github.com/dgraph-io/badger"
|
||||
)
|
||||
|
||||
type ServerStorage struct {
|
||||
DbFile string `json:"db_file,omitempty"`
|
||||
db *badger.DB
|
||||
}
|
||||
|
||||
// Open a badger database from struct ServerStorage
|
||||
func (ss *ServerStorage) open() error {
|
||||
|
||||
opts := badger.DefaultOptions(filepath.Join(GetConfig().StoragePath, GetConfig().GetIdentity().Uuid, ss.DbFile))
|
||||
opts.Logger = nil
|
||||
var err error
|
||||
ss.db, err = badger.Open(opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Store function StoreServer stores a server in a badger database with Server.GetUid() as key
|
||||
func (ss *ServerStorage) StoreServer(sc *Server) error {
|
||||
err := ss.open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer ss.close()
|
||||
// first marshal the Server to bytes with protobuf
|
||||
jsonsrv, err := json.Marshal(sc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
password, err := GetConfig().GetMemPass()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := meowlib.SymEncrypt(password, jsonsrv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
shakey := sha256.Sum256([]byte(sc.GetServerCard().GetUid()))
|
||||
key := shakey[:]
|
||||
// then store it in the database
|
||||
return ss.db.Update(func(txn *badger.Txn) error {
|
||||
return txn.Set(key, data)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// Check if a server exists in a badger database with Server.GetUid() as key
|
||||
func (ss *ServerStorage) ServerExists(sc *Server) (bool, error) {
|
||||
err := ss.open()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer ss.close()
|
||||
|
||||
shakey := sha256.Sum256([]byte(sc.GetServerCard().GetUid()))
|
||||
key := shakey[:]
|
||||
// check if key exists in badger database
|
||||
err = ss.db.View(func(txn *badger.Txn) error {
|
||||
_, err := txn.Get(key)
|
||||
return err
|
||||
}) // Add a comma here
|
||||
if err != nil { // key does not exist
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Store a server in a badger database with Server.GetUid() as key if it is not already there
|
||||
func (ss *ServerStorage) StoreServerIfNotExists(sc *Server) error {
|
||||
exists, err := ss.ServerExists(sc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
return ss.StoreServer(sc)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadServer function loads a Server from a badger database with Server.GetUid() as key
|
||||
func (ss *ServerStorage) LoadServer(uid string) (*Server, error) {
|
||||
var sc Server
|
||||
err := ss.open()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer ss.close()
|
||||
password, err := GetConfig().GetMemPass()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
shakey := sha256.Sum256([]byte(uid))
|
||||
key := shakey[:]
|
||||
err = ss.db.View(func(txn *badger.Txn) error {
|
||||
item, err := txn.Get(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return item.Value(func(val []byte) error {
|
||||
jsonsrv, err := meowlib.SymDecrypt(password, val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(jsonsrv, &sc)
|
||||
})
|
||||
})
|
||||
return &sc, err
|
||||
}
|
||||
|
||||
// DeleteServer function deletes a Server from a badger database with Server.GetUid() as key
|
||||
func (ss *ServerStorage) DeleteServer(uid string) error {
|
||||
err := ss.open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer ss.close()
|
||||
shakey := sha256.Sum256([]byte(uid))
|
||||
key := shakey[:]
|
||||
return ss.db.Update(func(txn *badger.Txn) error {
|
||||
return txn.Delete(key)
|
||||
})
|
||||
}
|
||||
|
||||
// LoadAllServers function loads all Servers from a badger database
|
||||
func (ss *ServerStorage) LoadAllServers() ([]*Server, error) {
|
||||
var scs []*Server
|
||||
err := ss.open()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer ss.close()
|
||||
password, err := GetConfig().GetMemPass()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = ss.db.View(func(txn *badger.Txn) error {
|
||||
opts := badger.DefaultIteratorOptions
|
||||
opts.PrefetchSize = 10
|
||||
it := txn.NewIterator(opts)
|
||||
defer it.Close()
|
||||
for it.Rewind(); it.Valid(); it.Next() {
|
||||
item := it.Item()
|
||||
var sc Server
|
||||
err := item.Value(func(val []byte) error {
|
||||
jsonsrv, err := meowlib.SymDecrypt(password, val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(jsonsrv, &sc)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
scs = append(scs, &sc)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return scs, err
|
||||
}
|
||||
|
||||
// LoadAllServers function loads all ServersCards from a badger database
|
||||
func (ss *ServerStorage) LoadAllServerCards() ([]*meowlib.ServerCard, error) {
|
||||
var scs []*meowlib.ServerCard
|
||||
err := ss.open()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer ss.close()
|
||||
password, err := GetConfig().GetMemPass()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = ss.db.View(func(txn *badger.Txn) error {
|
||||
opts := badger.DefaultIteratorOptions
|
||||
opts.PrefetchSize = 10
|
||||
it := txn.NewIterator(opts)
|
||||
defer it.Close()
|
||||
for it.Rewind(); it.Valid(); it.Next() {
|
||||
item := it.Item()
|
||||
var sc Server
|
||||
err := item.Value(func(val []byte) error {
|
||||
jsonsrv, err := meowlib.SymDecrypt(password, val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(jsonsrv, &sc)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
scs = append(scs, sc.GetServerCard())
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return scs, err
|
||||
}
|
||||
|
||||
// LoadServersFromUids function loads Servers with id in []Uid parameter from a badger database
|
||||
func (ss *ServerStorage) LoadServersFromUids(uids []string) ([]*Server, error) {
|
||||
var scs []*Server
|
||||
err := ss.open()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer ss.close()
|
||||
password, err := GetConfig().GetMemPass()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = ss.db.View(func(txn *badger.Txn) error {
|
||||
for _, uid := range uids {
|
||||
shakey := sha256.Sum256([]byte(uid))
|
||||
key := shakey[:]
|
||||
item, err := txn.Get(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var sc Server
|
||||
err = item.Value(func(val []byte) error {
|
||||
jsonsrv, err := meowlib.SymDecrypt(password, val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(jsonsrv, &sc)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
scs = append(scs, &sc)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return scs, err
|
||||
}
|
||||
|
||||
// LoadServersFromUids function loads Servers with id in []Uid parameter from a badger database
|
||||
func (ss *ServerStorage) LoadServerCardsFromUids(uids []string) ([]*meowlib.ServerCard, error) {
|
||||
var scs []*meowlib.ServerCard
|
||||
err := ss.open()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer ss.close()
|
||||
password, err := GetConfig().GetMemPass()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = ss.db.View(func(txn *badger.Txn) error {
|
||||
for _, uid := range uids {
|
||||
shakey := sha256.Sum256([]byte(uid))
|
||||
key := shakey[:]
|
||||
item, err := txn.Get(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var sc Server
|
||||
err = item.Value(func(val []byte) error {
|
||||
jsonsrv, err := meowlib.SymDecrypt(password, val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(jsonsrv, &sc)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
scs = append(scs, sc.GetServerCard())
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return scs, err
|
||||
}
|
||||
|
||||
// close a badger database
|
||||
func (ss *ServerStorage) close() {
|
||||
ss.db.Close()
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"forge.redroom.link/yves/meowlib"
|
||||
)
|
||||
|
||||
func TestGetUid(t *testing.T) {
|
||||
k, err := meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
srv := Server{
|
||||
Name: "test",
|
||||
Url: "http://127.0.0.1:8080",
|
||||
PublicKey: k.Public,
|
||||
}
|
||||
uid := srv.GetUid()
|
||||
if uid != "http://127.0.0.1:8080" {
|
||||
log.Fatal("uid not correct")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreServer(t *testing.T) {
|
||||
createId(t)
|
||||
ss := ServerStorage{DbFile: "test.db"}
|
||||
k, err := meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
srv := Server{
|
||||
Name: "test",
|
||||
Url: "http://127.0.0.1",
|
||||
PublicKey: k.Public,
|
||||
}
|
||||
err = ss.StoreServer(&srv)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
sout, err := ss.LoadServer(srv.GetServerCard().GetUid())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if sout == nil {
|
||||
log.Fatal("server not found")
|
||||
}
|
||||
if sout.Name != srv.Name {
|
||||
log.Fatal("name not found")
|
||||
}
|
||||
// Clean up
|
||||
// recursively remove the test.db folder
|
||||
os.RemoveAll("test.db")
|
||||
}
|
||||
|
||||
func TestLoadServersFromUids(t *testing.T) {
|
||||
createId(t)
|
||||
err := GetConfig().SetMemPass("test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to set password: %v", err)
|
||||
}
|
||||
ss := ServerStorage{DbFile: "test.db"}
|
||||
k, err := meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
srv := Server{
|
||||
Name: "test",
|
||||
Url: "http://localhost:8080",
|
||||
PublicKey: k.Public,
|
||||
}
|
||||
err = ss.StoreServer(&srv)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
sout, err := ss.LoadServersFromUids([]string{srv.GetServerCard().GetUid()})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if sout == nil {
|
||||
log.Fatal("server not found")
|
||||
}
|
||||
if sout[0].Name != srv.Name {
|
||||
log.Fatal("name not found")
|
||||
}
|
||||
// Clean up
|
||||
// recursively remove the test.db folder
|
||||
os.RemoveAll("test.db")
|
||||
}
|
||||
|
||||
func TestLoadServerCardsFromUids(t *testing.T) {
|
||||
createId(t)
|
||||
ss := ServerStorage{DbFile: "test.db"}
|
||||
k, err := meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
srv := Server{
|
||||
Name: "test",
|
||||
Url: "http://localhost:8080",
|
||||
PublicKey: k.Public,
|
||||
}
|
||||
err = ss.StoreServer(&srv)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
sout, err := ss.LoadServerCardsFromUids([]string{srv.GetServerCard().GetUid()})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if sout == nil {
|
||||
log.Fatal("server not found")
|
||||
}
|
||||
if sout[0].Name != srv.Name {
|
||||
log.Fatal("name not found")
|
||||
}
|
||||
// Clean up
|
||||
// recursively remove the test.db folder
|
||||
os.RemoveAll("test.db")
|
||||
}
|
||||
|
||||
func TestServerExists(t *testing.T) {
|
||||
createId(t)
|
||||
ss := ServerStorage{DbFile: "test.db"}
|
||||
k, err := meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
server := &Server{
|
||||
Name: "test",
|
||||
Url: "http://localhost:8080",
|
||||
PublicKey: k.Public,
|
||||
}
|
||||
|
||||
// Check if server exists before storing it
|
||||
exists, err := ss.ServerExists(server)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to check if server exists: %v", err)
|
||||
}
|
||||
if exists {
|
||||
t.Errorf("Server exists before storing it")
|
||||
}
|
||||
|
||||
// Store the server
|
||||
err = ss.StoreServer(server)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to store server: %v", err)
|
||||
}
|
||||
|
||||
// Check if server exists after storing it
|
||||
exists, err = ss.ServerExists(server)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to check if server exists: %v", err)
|
||||
}
|
||||
if !exists {
|
||||
t.Errorf("Server does not exist after storing it")
|
||||
}
|
||||
// Clean up
|
||||
// recursively remove the test.db folder
|
||||
os.RemoveAll("test.db")
|
||||
}
|
||||
|
||||
func TestStoreServerIfNotExists(t *testing.T) {
|
||||
createId(t)
|
||||
ss := ServerStorage{DbFile: "test.db"}
|
||||
k, err := meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
server := &Server{
|
||||
Name: "test",
|
||||
Url: "http://localhost:8080",
|
||||
PublicKey: k.Public,
|
||||
}
|
||||
|
||||
// Check if server exists before storing it
|
||||
exists, err := ss.ServerExists(server)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to check if server exists: %v", err)
|
||||
}
|
||||
if exists {
|
||||
t.Errorf("Server exists before storing it")
|
||||
}
|
||||
|
||||
// Store the server if it does not exist
|
||||
err = ss.StoreServerIfNotExists(server)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to store server: %v", err)
|
||||
}
|
||||
|
||||
// Check if server exists after storing it
|
||||
exists, err = ss.ServerExists(server)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to check if server exists: %v", err)
|
||||
}
|
||||
if !exists {
|
||||
t.Errorf("Server does not exist after storing it")
|
||||
}
|
||||
|
||||
// Clean up
|
||||
// recursively remove the test.db folder
|
||||
os.RemoveAll("test.db")
|
||||
}
|
||||
|
||||
func TestStoreServerIfNotExists_ServerExists(t *testing.T) {
|
||||
createId(t)
|
||||
ss := ServerStorage{DbFile: "test.db"}
|
||||
k, err := meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
server := &Server{
|
||||
Name: "test",
|
||||
Url: "http://localhost:8080",
|
||||
PublicKey: k.Public,
|
||||
}
|
||||
|
||||
// Store the server
|
||||
err = ss.StoreServer(server)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to store server: %v", err)
|
||||
}
|
||||
k, err = meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Store the server again with a different public key
|
||||
newServer := &Server{
|
||||
Name: "test",
|
||||
Url: "http://localhost:8080",
|
||||
PublicKey: k.Public,
|
||||
}
|
||||
|
||||
err = ss.StoreServerIfNotExists(newServer)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to store server: %v", err)
|
||||
}
|
||||
|
||||
// Retrieve the server and check if the public key has not changed
|
||||
storedServer, err := ss.LoadServer(server.GetServerCard().GetUid())
|
||||
if err != nil {
|
||||
t.Errorf("Failed to get server: %v", err)
|
||||
}
|
||||
|
||||
if storedServer.PublicKey != server.PublicKey {
|
||||
t.Errorf("Public key was modified")
|
||||
}
|
||||
|
||||
// Clean up
|
||||
// recursively remove the test.db folder
|
||||
os.RemoveAll("test.db")
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"forge.redroom.link/yves/meowlib"
|
||||
"github.com/google/uuid"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func StoreMessage(peer *Peer, usermessage *meowlib.UserMessage, password string) error {
|
||||
var dbid string
|
||||
// If no db/no ID create DB + Tablz
|
||||
// TODO : if file size > X new db
|
||||
if len(peer.DbIds) == 0 {
|
||||
dbid = uuid.NewString()
|
||||
file, err := os.Create(filepath.Join(GetConfig().StoragePath, dbid+GetConfig().DbSuffix))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file.Close()
|
||||
peer.DbIds = append(peer.DbIds, dbid)
|
||||
sqliteDatabase, _ := sql.Open("sqlite3", filepath.Join(GetConfig().StoragePath, dbid+GetConfig().DbSuffix)) // Open the created SQLite File
|
||||
err = createMessageTable(sqliteDatabase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sqliteDatabase.Close()
|
||||
GetConfig().me.Save()
|
||||
} else {
|
||||
dbid = peer.DbIds[len(peer.DbIds)-1]
|
||||
}
|
||||
// Open Db
|
||||
db, _ := sql.Open("sqlite3", filepath.Join(GetConfig().StoragePath, dbid+GetConfig().DbSuffix)) // Open the created SQLite File
|
||||
defer db.Close()
|
||||
// Detach Files
|
||||
if len(usermessage.Files) > 0 {
|
||||
for _, f := range usermessage.Files {
|
||||
hiddenFilename := uuid.NewString()
|
||||
// Cypher file
|
||||
encData, err := meowlib.SymEncrypt(password, f.Data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
os.WriteFile(hiddenFilename, encData, 0600)
|
||||
// replace f.Data by uuid filename
|
||||
f.Data = []byte(hiddenFilename)
|
||||
}
|
||||
}
|
||||
// Encrypt message
|
||||
out, err := proto.Marshal(usermessage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
encData, err := meowlib.SymEncrypt(password, out)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Insert message
|
||||
insertMessageSQL := `INSERT INTO message(m) VALUES (?) RETURNING ID`
|
||||
statement, err := db.Prepare(insertMessageSQL) // Prepare statement.
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = statement.Exec(encData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get last messages from a peer
|
||||
func GetLastMessages(peer *Peer, inAppMsgCount int, lastDbId int, wantMore int, password string) ([]InternalUserMessage, error) {
|
||||
var messages []InternalUserMessage
|
||||
fileidx := len(peer.DbIds) - 1
|
||||
// initialize count with last db message count
|
||||
countStack, err := getMessageCount(peer.DbIds[fileidx])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// while the db message count < what we already have in app, step to next db file
|
||||
for inAppMsgCount > countStack {
|
||||
fileidx--
|
||||
if fileidx < 0 {
|
||||
return nil, nil
|
||||
}
|
||||
newCount, err := getMessageCount(peer.DbIds[fileidx])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
countStack += newCount
|
||||
}
|
||||
// There fileidx should provide the db that we need (unless wantMore overlaps the next DB)
|
||||
db, _ := sql.Open("sqlite3", filepath.Join(GetConfig().StoragePath, peer.DbIds[fileidx]+GetConfig().DbSuffix)) // Open the created SQLite File
|
||||
defer db.Close()
|
||||
// if it's first app query, it won't hold a lastIndex, so let's start from end
|
||||
if lastDbId == 0 {
|
||||
lastDbId = math.MaxInt64
|
||||
}
|
||||
stm, err := db.Prepare("SELECT id, m FROM message WHERE id < ? ORDER BY id DESC LIMIT ?")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer stm.Close()
|
||||
rows, err := stm.Query(lastDbId, wantMore)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var ium InternalUserMessage
|
||||
var um meowlib.UserMessage
|
||||
var id int64
|
||||
var m []byte
|
||||
err = rows.Scan(&id, &m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decdata, err := meowlib.SymDecrypt(password, m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = proto.Unmarshal(decdata, &um)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ium.dbid = id
|
||||
ium.message = &um
|
||||
messages = append(messages, ium)
|
||||
}
|
||||
// TODO DB overlap
|
||||
return messages, nil
|
||||
}
|
||||
|
||||
func getMessageCount(dbid string) (int, error) {
|
||||
db, _ := sql.Open("sqlite3", filepath.Join(GetConfig().StoragePath, dbid+GetConfig().DbSuffix)) // Open the created SQLite File
|
||||
defer db.Close()
|
||||
var count int
|
||||
query := "SELECT COUNT(*) FROM message"
|
||||
err := db.QueryRow(query).Scan(&count)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func createMessageTable(db *sql.DB) error {
|
||||
createMessageTableSQL := `CREATE TABLE message (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"m" BLOB);` // SQL Statement for Create Table
|
||||
statement, err := db.Prepare(createMessageTableSQL) // Prepare SQL Statement
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
statement.Exec() // Execute SQL Statements
|
||||
return nil
|
||||
}
|
||||
|
||||
func createServerTable(db *sql.DB) error {
|
||||
createServerTableSQL := `CREATE TABLE servers (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"country" varchar(2),
|
||||
"public" bool,
|
||||
"uptime" int,
|
||||
"bandwith" float,
|
||||
"load" float,
|
||||
"url" varchar(2000)
|
||||
"name" varchar(255);
|
||||
"description" varchar(5000)
|
||||
"publickey" varchar(10000)
|
||||
)` // SQL Statement for Create Table
|
||||
statement, err := db.Prepare(createServerTableSQL) // Prepare SQL Statement
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
statement.Exec() // Execute SQL Statements
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
-----BEGIN PGP MESSAGE-----
|
||||
Comment: https://gopenpgp.org
|
||||
Version: GopenPGP 2.8.3
|
||||
|
||||
wy4ECQMIlftc5WyUrBjgI1MbXSAWh3ZqBpILi+RN79+v4HuvB/xmqoEJtZVeypwh
|
||||
0uoBc2FevnicfVu4wOUlglRjhPWLcE25+gQxlKB7RzX6cQND3+Nw3qiexvK+psrm
|
||||
mW7nOIHE/9EVXzAlRrCgMlPcZpPB+5q5X9t01BQ/tTV6OytcLS3J6byrMmefA7jG
|
||||
ki/U9oSkdwFYPosG5PKhiHCe03AIjY++s/Wgn1OMtsLWX/8/dJ6CNkzvwnX4CVti
|
||||
x8KGj7IwJefG7BGApU3eg9OcqRz8KubWI1mWfiC2uVOoFgVlnAOjP8qzUFs65LK9
|
||||
cBglhUNuG/Jc2ojCa9ndWYIaDJ2pzGpvhlGsj7kU0Fyh3AMTTzrJeRwAoqcLv8P5
|
||||
B6ERBv0rG16arkhpC4v6BFT3UekMzBMhpGSb8PPu3BmDayHmWG+Q3Lt7ufnm/UId
|
||||
naLVfnQKD6An05KkqZNqHjPsbHPg8gFcV3N87LCtCMYGGDgxbsKBDh/ig0FQwnnq
|
||||
P5Hj4VZTUcuJ25BSV/Tbbo8Z9XGKQ02OnX7h7qies+oVAan9Pq3YgjoqFB06wDTq
|
||||
hBxrSMgexfB2Dj23pioC72Ege22n1I6PBwuM5p6Ja0btZQrfhL/yY/y102MvgUXh
|
||||
Qh84zxtTKKR8b3sL3WeEckOPBcEOvbmLf+sTjWdIIcQMB0IGhDhzCvf0sGtk48eJ
|
||||
rKNruG7RMHGjBZkZnpJVArJchxmRZkuGLjwsQTRbdRPQc6vMmvPhqCuFPMhnTaL9
|
||||
nss0tnzQ2DdLOwO8JsQH41IoRi0STl6ndDT4wbGlmuh57xqMdrNjkur84zsi6G76
|
||||
wQOtGQ7A+9xCz/cnAaTPlmUUe+0Fg2vHQbGPfZy3TfERAkGYg9EsQbww/nNSOQua
|
||||
e+DbLNbBPp5egkfR6TDDbiTgwWXn6R673qLQ27MpHBY2eQ8IaJqz/jdm6/UPbuh3
|
||||
bpBF0G7HVwxfhDAPBKPObJM8doHB67d5hoxcqfINexVXsX5Dd3OzCY1mUKgn95kF
|
||||
Tzl4VGu4kIxcFRXMR49XaHC4/CQbv70c/2NiJf739fxcLkGUQ5wXA44uMKwEbzwW
|
||||
x53fhFKKjGC/AWubs9jnVVJz7EfiFX9VvhEYvXp3++emM9Nbv6BaRobq9JIKmdMl
|
||||
E69BcHrqZ7ahMDTENSpVZTlohs4AnaxeZesCPq7t75STAx2/jj3YtgfeYarE3d9I
|
||||
rn8VofS5uI41VNO4noQtj8a18YzNW5V+aGLjD2ZxvxMYfp8NfsJpEuWpXRNE5yZq
|
||||
AzeXlGlcMHc/n6+vgdTirSTbrwY2chBgxwWAdpcezimAl6VpT4gZ1pmtDxtQA5v0
|
||||
yC6LRujp+p9yPfrVEB/tuduo3DpnBJjkAcBlDtGuSew98QoIDKI/UcMUqGZW+n4U
|
||||
/QugOpd9aY7UhIFiWHZ14PnZwiUhdxZTEE4wo8TVVFRmP4L6oxLBjOByLPOH4ct+
|
||||
eNrL5cXABE0rwm3/Ywxuxy3hV07tazm+GpxdUjX4+cjBJZCwYO/JyT0OI2sPsKIY
|
||||
6WO2zkobs8fn0j3ba1ovRWGmAU0MnGCg1ZnJOiXtUn17QXoe3CnjvQu9wS15ms2F
|
||||
htQtIZwnosXuHcXUzNNtv4SFdZAFsy8tj4TYtQ3qtxYKjxyLlmPZ9yT0DD2VDcFL
|
||||
ra7II59iElBCyC0JS/q1JQxdgVPhD0ZU+x9F/koquS+35gtqjemVmeLb9W+nEWc0
|
||||
3H4W0i0k0wkSwWX4FUmGbqHczOCoVoTuKkp+ypAfzZ8L/nHybz4eK7RdGKfWeYbG
|
||||
N3zlTLaTTd2D7D1s5+df0itoM/VS0pSHPHMkNCJ/CmC8gwlIENU4cRqvvBXF2dEA
|
||||
Far8qCMJLscaoKvbQVQwhqzq9nEyra5CscJzD7nq3aiS5gwOfzy6G1qvk4KFxcaX
|
||||
PLBEAegTueaMj7KvTwDd+Yz7lnbk2fmNo4lJlGkUJMyEDLCjYg0sqLokOO7MMYyC
|
||||
V69bnJCoQPwfaE0vETiZn6TFGXG0oQg4ki87lhNzzXlT8JiTK4RMWWGtw8QBHmsv
|
||||
PWVBMuooqrPXpBEty7O7+Cxef/P0My8CxwgMOEPA2dAtWrvXrOM3wHCWoLK4FJlS
|
||||
XxHNHPwyZ49vCuEWhUJArge1oXWwZUTCpGEJLd0taUI+T9GU+5VG/VrbHprBdod4
|
||||
FjRAXxpO4Sx/Z7L/vccFjOjHeobNMKGmC4BDDmUSECCszWqT37/XFbGJrHdYqnht
|
||||
yzdRDeI11rEIpNyF65PgJR6A5hEnZk0IsSqiTvPcIodUlPkhlSVPoc+NSrYATuJa
|
||||
VviYI8AhTUxrAcZyG/unEKKQfCBB8XBn8gUTkodxOaI27GVJ/T4WgGERsPNQm/Fl
|
||||
HCbvaphsM7nszn8iuoRv5PWiWiZsetl+HvXVKWUUb4jxq6xgpIpsBJw=
|
||||
=BI6k
|
||||
-----END PGP MESSAGE-----
|
||||
+5
-6
@@ -8,7 +8,6 @@ import (
|
||||
"image/color"
|
||||
"image/png"
|
||||
"io"
|
||||
"log"
|
||||
"math"
|
||||
"os"
|
||||
|
||||
@@ -101,7 +100,7 @@ func (contact *ContactCard) WriteCompressed(filename string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (contact *ContactCard) WritePng(filename string) {
|
||||
func (contact *ContactCard) WritePng(filename string) error {
|
||||
out, err := proto.Marshal(contact)
|
||||
if err != nil {
|
||||
println(err)
|
||||
@@ -128,18 +127,18 @@ func (contact *ContactCard) WritePng(filename string) {
|
||||
|
||||
f, err := os.Create(filename)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := png.Encode(f, img); err != nil {
|
||||
f.Close()
|
||||
log.Fatal(err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := f.Close(); err != nil {
|
||||
log.Fatal(err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (Contact *ContactCard) WriteQr(filename string) error {
|
||||
|
||||
+57
-4
@@ -1,21 +1,74 @@
|
||||
package meowlib
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCompress(t *testing.T) {
|
||||
kp1 := NewKeyPair()
|
||||
kp2 := NewKeyPair()
|
||||
kp3 := NewKeyPair()
|
||||
func TestCompressAndJson(t *testing.T) {
|
||||
kp1, err := NewKeyPair()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
kp2, err := NewKeyPair()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
kp3, err := NewKeyPair()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var cc ContactCard
|
||||
cc.Name = "My Full Name And It's loooong"
|
||||
cc.ContactPublicKey = kp1.Public
|
||||
cc.EncryptionPublicKey = kp2.Public
|
||||
cc.LookupPublicKey = kp3.Public
|
||||
cc.InvitationMessage = "hello, it's me"
|
||||
cc.AddUrls([]string{"https://meow.myfirstdomain.com/services/meow:8080", "https://meow.myseconddomain.com/services/meow:8080", "http://meow.mythirddomain.com/services/meow:8080"})
|
||||
serialized, _ := cc.Serialize()
|
||||
println(len(serialized))
|
||||
compressed, _ := cc.Compress()
|
||||
println(len(compressed))
|
||||
ncc, err := NewContactCardFromCompressed(compressed)
|
||||
if err != nil {
|
||||
println(err)
|
||||
}
|
||||
assert.Equal(t, ncc.Name, cc.Name)
|
||||
if err != nil {
|
||||
println(err)
|
||||
}
|
||||
jsoncc, err := json.Marshal(cc)
|
||||
var cc1 ContactCard
|
||||
err = json.Unmarshal(jsoncc, &cc1)
|
||||
if err != nil {
|
||||
println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStaticJson(t *testing.T) {
|
||||
ccsrt := `
|
||||
{
|
||||
"contact_public_key": "LS0tLS1CRUdJTiBQR1AgUFVCTElDIEtFWSBCTE9DSy0tLS0tClZlcnNpb246IEdvcGVuUEdQIDIuNy40CkNvbW1lbnQ6IGh0dHBzOi8vZ29wZW5wZ3Aub3JnCgp4ak1FWlpST1pCWUpLd1lCQkFIYVJ3OEJBUWRBZy9iRnR4WG5hRjZLOFEzdGVWcmt3Y3YvRTV6TE94WnVZaXVvCm5MVkdtdWZOQzI1aGJXVWdQRzFoYVd3K3dvOEVFeFlJQUVFRkFtV1VUbVFKRUhmcmlVWXZsWnhyRmlFRXFFMDIKVVRmOTRTY1hJWVdjZCt1SlJpK1ZuR3NDR3dNQ0hnRUNHUUVEQ3drSEFoVUlBeFlBQWdVbkNRSUhBZ0FBYmN3QgpBT1VsTHJEbWpCM0pKeGNWUFNHaU1KTlZrem1idlhMTDVSSnh4aTNuNVVrMUFRQ2NHN29QeDYwTUdHRVNhc0V0CnBTS2VqUGFmNjNTVXhMelRoRFFacTlqOUI4NDRCR1dVVG1RU0Npc0dBUVFCbDFVQkJRRUJCMEJXeXMvaFZHSGcKRFN0V2Jid3VnbnlCdTFWdUlJbVBZMDRsKzRCQWd4QUFZQU1CQ2duQ2VBUVlGZ2dBS2dVQ1paUk9aQWtRZCt1SgpSaStWbkdzV0lRU29UVFpSTi8zaEp4Y2hoWngzNjRsR0w1V2Nhd0liREFBQWkyTUEvMTdUYksyT3FMdzZDSWZmCkE3YnlwYitxNzdHVmZlQmtmY2l3aXlCM2xRSGxBUUROZzJONisxcklEbG40cXRRc0pFSWR1OUlMMzVlMjR6cWwKbEJMSVR0YVBBQT09Cj1KeHZrCi0tLS0tRU5EIFBHUCBQVUJMSUMgS0VZIEJMT0NLLS0tLS0=",
|
||||
"encryption_public_key": "LS0tLS1CRUdJTiBQR1AgUFVCTElDIEtFWSBCTE9DSy0tLS0tCkNvbW1lbnQ6IGh0dHBzOi8vZ29wZW5wZ3Aub3JnClZlcnNpb246IEdvcGVuUEdQIDIuNy40Cgp4ak1FWlpST1pCWUpLd1lCQkFIYVJ3OEJBUWRBY3FocFM5dnFzVGE2WXJCWVEra1JaY1VDWnFSRUhVUTMrbWQyClZOdlNLYS9OQzI1aGJXVWdQRzFoYVd3K3dvOEVFeFlJQUVFRkFtV1VUbVFKRUNHb3JhR2xRMVVoRmlFRVcrZVUKRzJjZnVSaU5CVDNwSWFpdG9hVkRWU0VDR3dNQ0hnRUNHUUVEQ3drSEFoVUlBeFlBQWdVbkNRSUhBZ0FBNmRRQgpBSVN3OUFKeVphem83TWs2R2NVZzR6ZDROR1p2dVorZnNxMThoTmtNd0EwRUFRQzNaNmNUT2kraGlURjJLazVGClBtSnI4aHlQWlREVGgwa1I5NE14TzhBa0NzNDRCR1dVVG1RU0Npc0dBUVFCbDFVQkJRRUJCMEQzYWZpeS9ZT3YKZFRxMXJ0UTZhVTVLNS9COTFKTW5SaVptSGtTYW9YRDZYd01CQ2duQ2VBUVlGZ2dBS2dVQ1paUk9aQWtRSWFpdApvYVZEVlNFV0lRUmI1NVFiWngrNUdJMEZQZWtocUsyaHBVTlZJUUliREFBQVpxZ0EvMElaRTR2MmRZbFB5NURJCnNPeTlDbTNFakRJSy80SElJK1VuRm5qeGFtVENBUDQwbUJqaEJKZVRNbzV5ZWpDN2xlYXliUlNMcE1yY1NIeEcKSmpnOGJ0eDVBdz09Cj1rd1VTCi0tLS0tRU5EIFBHUCBQVUJMSUMgS0VZIEJMT0NLLS0tLS0=",
|
||||
"invitation_id": "38373ab8-f423-48bc-9136-628f2a7e0e18",
|
||||
"invitation_message": "Hi ! it's me !",
|
||||
"lookup_public_key": "LS0tLS1CRUdJTiBQR1AgUFVCTElDIEtFWSBCTE9DSy0tLS0tClZlcnNpb246IEdvcGVuUEdQIDIuNy40CkNvbW1lbnQ6IGh0dHBzOi8vZ29wZW5wZ3Aub3JnCgp4ak1FWlpST1pCWUpLd1lCQkFIYVJ3OEJBUWRBWmh2NGY3WG55aGhoU3JPQkUrc0VteGsrWkFGNFdjTjY4KythCnAwV25FU0xOQzI1aGJXVWdQRzFoYVd3K3dvOEVFeFlJQUVFRkFtV1VUbVFKRUVPTnRxN202L2h3RmlFRU9EaWMKL1cyMVJqNmJMWkdXUTQyMnJ1YnIrSEFDR3dNQ0hnRUNHUUVEQ3drSEFoVUlBeFlBQWdVbkNRSUhBZ0FBbi84QQovMWRjRHVIY3Fjd3JrNW9sclVPMUlIbVhSYWg1aC9wbm9HSDZMM0JSdlphbkFRRHozZTc0TmxZWFpnank3SVBFCittbnM5MDR6TnVqdnpFYks2SHg2dTh1VkJNNDRCR1dVVG1RU0Npc0dBUVFCbDFVQkJRRUJCMEFzVnBZd000TEMKK0JNT201WXFRWUEzRlFiTXI4alp3Wk5IelFvQ29URDRJZ01CQ2duQ2VBUVlGZ2dBS2dVQ1paUk9aQWtRUTQyMgpydWJyK0hBV0lRUTRPSno5YmJWR1Bwc3RrWlpEamJhdTV1djRjQUliREFBQUJxb0EvaUFOMGpIV0FHTXV2MTYxCkdxcXF6aGxPQXFzVjVKNW5iMy9LMk43TU9Pek1BUURzZEVaMlU5VmxXY3ljWDFuZGFoMnkzUnEvQ1QwWkJ0R2IKUzM1dU5HbDFBZz09Cj1kT2lnCi0tLS0tRU5EIFBHUCBQVUJMSUMgS0VZIEJMT0NLLS0tLS0=",
|
||||
"name": "me",
|
||||
"pull_servers": [
|
||||
{
|
||||
"name": "local",
|
||||
"public_key": "LS0tLS1CRUdJTiBQR1AgUFVCTElDIEtFWSBCTE9DSy0tLS0tClZlcnNpb246IEdvcGVuUEdQIDIuNS4wCkNvbW1lbnQ6IGh0dHBzOi8vZ29wZW5wZ3Aub3JnCgp4ak1FWSs2TUd4WUpLd1lCQkFIYVJ3OEJBUWRBVmk1ZnQyTmlmSzByVmVmYzNQd3JTRXhxNHRhVUtaTzhQeXprClB4SmNTT1BOQzI1aGJXVWdQRzFoYVd3K3dvd0VFeFlJQUQ0RkFtUHVqQnNKa0M4aTlzQjVvVjNyRmlFRWpNeEgKZldiZmxMblRhZzFRTHlMMndIbWhYZXNDR3dNQ0hnRUNHUUVEQ3drSEFoVUlBeFlBQWdJaUFRQUF4L0lCQU5kMgpVK3hZM09LQVk5elFSbmlXQXdlVEpoMWxySEpaMHd6RmVQS3JycUJvQVAwYjRISHBoT2dJWWx2TXlqajZ0TXZRCk01RTIzY3ZiWjRPZXRjNmNmeWxIQ2M0NEJHUHVqQnNTQ2lzR0FRUUJsMVVCQlFFQkIwQ2t1bWlndUpNT003Sy8KNEl1NVppYkJUYXAwSzBkNXNybkNCN2tIU2pObGV3TUJDZ25DZUFRWUZnZ0FLZ1VDWSs2TUd3bVFMeUwyd0htaApYZXNXSVFTTXpFZDladCtVdWROcURWQXZJdmJBZWFGZDZ3SWJEQUFBMlcwQS9qY2pZTUtQY3ZXcTA2QVpVKzRHClQwUmQxU2VNUXpzNndCUU9ZejEwQkVMWEFRQ3hTQ2kvN2RqRjZUWFl0SFpBSytrVUEvUHpYaW14bnRvVFpKbjMKV3l1Z0NnPT0KPTFjU20KLS0tLS1FTkQgUEdQIFBVQkxJQyBLRVkgQkxPQ0stLS0tLQ==",
|
||||
"url": "http://localhost:8080"
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
var cc1 ContactCard
|
||||
jsoncc := []byte(ccsrt)
|
||||
err := json.Unmarshal(jsoncc, &cc1)
|
||||
if err != nil {
|
||||
println(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,13 @@ go-plantuml generate -o generated/client.puml -d ../client
|
||||
sed -i 's/\.\.\/client/client/g' generated/client.puml
|
||||
go-plantuml generate -o generated/server.puml -d ../server
|
||||
sed -i 's/\.\.\/server/server/g' generated/server.puml
|
||||
cp *.puml generated/
|
||||
cd generated
|
||||
plantuml .
|
||||
plantuml -latex .
|
||||
mv *.tex tex/
|
||||
mv *.png png/
|
||||
rm *.log *.aux *.fdb_latexmk *.fls *.gz *.puml
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
@startuml Company Company
|
||||
Employee -> Company: Join company with provided credentials
|
||||
Company -> Employee: provide a full featured validated peer
|
||||
Company -> All: Publishes Employee ContactCard and info
|
||||
Company -> Employee: notify publication done, service active
|
||||
Employee -> Company: search for contacts
|
||||
@enduml
|
||||
@@ -0,0 +1,10 @@
|
||||
@startuml Public endpoint (free for chat)
|
||||
User -> Endpoint: Create & send invitation for endpoint (Generate User ContactCard and create endpoint pending contact)
|
||||
Endpoint -> User: Auto-accept invitation and answer (Generate Endpoint ContactCard and create finalized User contact)
|
||||
User -> Endpoint: [auto]validate Answer, invitation finalize (Finalize Endpoint contact and notify Endpoint that communication is possible)
|
||||
Endpoint -> User: query mandatory/optional info for profile publication
|
||||
User -> Endpoint: provide info
|
||||
Endpoint -> All: Publishes User ContactCard and info
|
||||
Endpoint -> User: notify publication done, service active
|
||||
User -> Endpoint: search for contacts
|
||||
@enduml
|
||||
@@ -0,0 +1,13 @@
|
||||
@startuml General Invitation Steps
|
||||
InitiatingUser -> InitiatingUser : STEP_1 = Create InivitedUser_Id generate a public key, invitation uid & message for InvitedUser optionnally password protected
|
||||
InitiatingUser -> InvitedUser: STEP_1_SEND= transmit step 1 data (QR Code, Bluetooth, messaging, mail, ...) optional password being tranmitted through another channel
|
||||
|
||||
InvitedUser -> InvitedUser :Create InitatingUser_Id & InvitedUser ContactCard
|
||||
InvitedUser -> InitiatingUser: STEP_2_SEND=transmit InvitedUser ContactCard (QR Codes, Bluetooth, messaging, mail, ...) encrypted with initiating user pub key
|
||||
|
||||
InitiatingUser -> InitiatingUser : STEP_3=InitiatingUser_Id Accept Invitation and create answer (Generate InitiatingUser ContactCard and create finalized InvitedUser contact)
|
||||
InitiatingUser -> InvitedUser: STEP_3_SEND=Send answer through invited user's message servers from contact card
|
||||
|
||||
InvitedUser -> InvitedUser : Finalize InitiatingUser from its ContactCard
|
||||
InvitedUser -> InitiatingUser: STEP_4= Send confirmation to InitiatingUser that communication is possible through initiating user's message servers from contact card
|
||||
@enduml
|
||||
@@ -0,0 +1,14 @@
|
||||
@startuml General Server Invitation Steps
|
||||
InitiatingUser -> InitiatingUser: STEP_1.1 Create STEP_1_data=InitiatingUser_TmpId (public key & message for InvitedUser optionnally passwords (url & payload) protected)
|
||||
InitiatingUser -> Server: STEP_1.2 Send STEP_1_data to server
|
||||
Server -> InvitedUser: STEP_1.3 Get STEP_1_data from server
|
||||
InvitedUser -> InvitedUser: STEP_2.1 Create STEP_2_data=InvitedUser_Id for InitiatingUser (Generate InvitedUser ContactCard and create InitiatingUser pending contact)
|
||||
InvitedUser -> Server: STEP_2.1 Send STEP_2_data to server
|
||||
Server -> InitiatingUser: STEP_2.3 Get STEP_2_data from server
|
||||
InitiatingUser -> InitiatingUser: STEP_3 Create STEP_3_data=InitiatingUser_Id (Generate InitiatingUser ContactCard and create finalized InvitedUser contact)
|
||||
InitiatingUser -> Server: STEP_3.1 Send STEP_3_data to server
|
||||
Server -> InvitedUser: STEP_3.3 Get STEP_3_data from server
|
||||
InvitedUser -> InvitedUser: STEP_4 Create STEP_4_data=InvitedUser_OK Review Answer, invitation finalize (Finalize InitiatingUser contact and notify InitiatingUser that communication is possible)
|
||||
InvitedUser -> Server: STEP_4.1 Send STEP_4_data to server
|
||||
Server -> InitiatingUser: STEP_4.3 Get STEP_4_data from server
|
||||
@enduml
|
||||
@@ -0,0 +1,16 @@
|
||||
@startuml Server Invitation Step 00
|
||||
InitiatingUser -> Bastet : contact name
|
||||
InitiatingUser -> Bastet : invitationMessage
|
||||
InitiatingUser -> Bastet : select invitation server
|
||||
InitiatingUser -> Bastet : optional passwords ([payload], [url])
|
||||
Bastet -> NativeLib : send invitation
|
||||
NativeLib -> NativeLib : create contact and invtation KP
|
||||
NativeLib -> Server : send invitation
|
||||
Server -> Server : create invtation URL
|
||||
Server -> Redis : store InitiatingUser pub key and message/pwd
|
||||
Server -> NativeLib : invitation URL
|
||||
NativeLib -> Bastet : invitation URL
|
||||
Bastet -> InitiatingUser : invitation URL
|
||||
InitiatingUser -> InvitedUser : invitation URL
|
||||
InitiatingUser -> InvitedUser : [invitation passwords] through another channel
|
||||
@enduml
|
||||
@@ -0,0 +1,11 @@
|
||||
@startuml Server Invitation Step 01
|
||||
InvitedUser -> Bastet : provide invitation URL
|
||||
Bastet -> NativeLib : GET InitiatingUser pub key and message
|
||||
InvitedUser -> Bastet : fill invitation
|
||||
InvitedUser -> Bastet : select servers
|
||||
Bastet -> NativeLib : get server cards for selected uids
|
||||
NativeLib -> Bastet : server cards
|
||||
Bastet -> NativeLib : invitation data & InitiatingUser pub key
|
||||
NativeLib -> Server : POST encrypted invitation
|
||||
Server -> Redis : Store invitation data
|
||||
@enduml
|
||||
@@ -0,0 +1,11 @@
|
||||
@startuml Server Invitation Step 02
|
||||
User -> Bastet : paste URL
|
||||
Bastet -> NativeLib : build invitationGetMessage
|
||||
NativeLib -> Bastet : invitationGetMessage
|
||||
Bastet -> Server : send invitationGetMessage
|
||||
Redis -> Server : retrieve invitation
|
||||
Server -> Bastet : invitation message
|
||||
Bastet -> NativeLib : decode invitation message
|
||||
NativeLib -> Bastet : invitation data
|
||||
Bastet -> User : invitation data
|
||||
@enduml
|
||||
@@ -0,0 +1,12 @@
|
||||
|
||||
@startuml Server Invitation Step 03
|
||||
User -> Bastet : select servers
|
||||
User -> Bastet : accept invitation
|
||||
Bastet -> NativeLib : accept invitation
|
||||
Bastet -> NativeLib : build accept message
|
||||
NativeLib -> Bastet : invitationGetMessage
|
||||
Bastet -> Server : send accept message
|
||||
Server -> Redis : store accept message
|
||||
Server -> Bastet : accept message ok
|
||||
Bastet -> User : accept msg sent
|
||||
@enduml
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
@startuml Server Invitation Step 03
|
||||
Bastet -> NativeLib : periodic message check
|
||||
NativeLib -> Server : get new messages
|
||||
Server -> NativeLib : send invitation message
|
||||
Server -> Redis : store accept message
|
||||
Server -> Bastet : accept message ok
|
||||
Bastet -> User : invitation sent is accepted
|
||||
@enduml
|
||||
+2816
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 467 KiB |
+10
-3
@@ -43,10 +43,17 @@ The server requires very few ressources and will run on any low cost single boar
|
||||
\textffm{Meow} also provides an anonymizing transfer service very similar to the Tor Onion protocol, we call it the Matriochka protocol.
|
||||
Any server can be used for building the transfer chain.
|
||||
Some of them might be marked as trusted.
|
||||
Random delays and random payload padding might be set for each forwarding step, making the overall message tracking much more difficult, even for organizations having capabilities of global network surveillance.
|
||||
Random delays and random size payload padding might be set for each forwarding step, making the overall message tracking much more difficult, even for organizations having capabilities of global network surveillance.
|
||||
It is strongly advised to use trusted servers as your first node and message server (the one that holds your incoming messages).
|
||||
|
||||
\subsubsection{Presence protocol for direct messaging}
|
||||
|
||||
\subsubsection{Message lookup obfuscation}
|
||||
Your device will request for messages using conversation keys on a very regular basis to your messaging(s) server(s).
|
||||
The device will check for conversation keys for all your contacts. If you check that option, it will also check for hidden contact keys.
|
||||
In case of data interception on your device link, in order to prevent statistical analysis, every request might be answered with size useful data (server's known server list).
|
||||
Moreover, some random keys will be added to your requests list.
|
||||
|
||||
\subsubsection{Presence protocol for direct messaging TBC}
|
||||
A presence service associating your conversation keys to your IP address for direct peer to peer connection is also provided.
|
||||
The presence protocol is simply activated by setting a flag in the message poll requests.
|
||||
If that flag is set, your encrypted IP will be published on the server, allowing your only your peer(s) to decrypt it and directly communicate with your terminal.
|
||||
@@ -208,7 +215,7 @@ You just have to live with the goverment decisions.
|
||||
In the best scenario that government was elected, and might represent at most 25\% of the population.
|
||||
In most case, they will vote laws to satisfy the powerful people who supported their election, and the most powerful lobbies.
|
||||
|
||||
\textffm{Meow} Nations aim to be the next lobbying power to influence real life politics, "the poor man's lobby".
|
||||
\textffm{Meow} Nations aims to be the next lobbying power to influence real life politics, "the poor man's lobby".
|
||||
|
||||
Virtual nation in that perspective will be probably quickly flagged as terrorist nation by the old world media, but well,
|
||||
one man's terrorist is another man's freedom fighter.
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
@startuml
|
||||
Client1 -> Server: Send Message (PubKeyClient2 + Payload)
|
||||
Server -> Client1: Ack Message (Server UUID + DateReceived)
|
||||
Client1 --> Server: Message delivery ? (PubKeyClient2 + Payload)
|
||||
Server --> Client1: Message delivered (Server UUID + DateReceived)
|
||||
|
||||
Client2 -> Server: Get Messages (PubKeyClient2)
|
||||
Server -> Client2: Get Messages [](PubKeyClient2 + Payload)
|
||||
Client2 --> Server: Ack Messages Processed
|
||||
@enduml
|
||||
@@ -0,0 +1,7 @@
|
||||
@startuml
|
||||
ClientFdThread -> Lib : write poll job list
|
||||
ClientFdThread -> ClientBgThread : notify job ?
|
||||
ClientBgThread -> Lib : poll for servers
|
||||
ClientBgThread -> ClientFdThread : notify message here
|
||||
ClientFdThread -> Lib : Read redeived message and update db
|
||||
@enduml
|
||||
@@ -0,0 +1,7 @@
|
||||
@startuml
|
||||
ClientFdThread -> Lib : write msg to db, encrypted msg for user to file, and job file
|
||||
ClientFdThread -> ClientBgThread : notify job
|
||||
ClientBgThread -> Lib : encrypt for server(s) and send including retries
|
||||
ClientBgThread -> Lib: notify send result
|
||||
ClientFdThread -> Lib : Read job report and update db
|
||||
@enduml
|
||||
@@ -0,0 +1,508 @@
|
||||
# Multi-Device Conversation Sync — Implementation Plan
|
||||
|
||||
## Context
|
||||
|
||||
meowlib already has scaffolding for multi-device sync:
|
||||
|
||||
| Existing artefact | Where |
|
||||
|---|---|
|
||||
| `Identity.Device *KeyPair` | `client/identity.go:35` |
|
||||
| `Identity.OwnedDevices PeerList` | `client/identity.go:40` |
|
||||
| `Peer.Type string` | `client/peer.go:52` |
|
||||
| `ToServerMessage.device_messages` (field 10) | `pb/messages.proto:75` |
|
||||
| `FromServerMessage.device_messages` (field 9) | `pb/messages.proto:99` |
|
||||
| `BackgroundJob.Device *KeyPair` | `client/identity.go:334` |
|
||||
|
||||
The server (`server/router.go`) does **not** yet implement `device_messages` routing; it goes through `messages`/`Chat` today.
|
||||
|
||||
---
|
||||
|
||||
## Chosen Sync Scheme: Event-Driven Delta Sync over Existing Message Infrastructure
|
||||
|
||||
### Rationale
|
||||
|
||||
| Approach | Pros | Cons | Verdict |
|
||||
|---|---|---|---|
|
||||
| Full DB sync | Complete history | Huge payloads, merge conflicts, wasteful | ❌ |
|
||||
| Inbox/outbox file sharing | Simple to reason about | File-level granularity, no dedup, breaks privacy model | ❌ |
|
||||
| **Event-driven delta sync** | Minimal data, no merge needed, reuses existing crypto + server stack | Requires dedup table | ✅ |
|
||||
|
||||
Each message event (received, sent, status change) is forwarded immediately to sibling devices through the **same server infrastructure** as regular peer messages. Each device maintains its own complete local DB. Convergence is eventual; dedup via `ConversationStatus.Uuid`.
|
||||
|
||||
### Key Design Decisions
|
||||
|
||||
1. **Zero server changes required.** Device sync messages are addressed to the sibling device's lookup key and travel through the existing `msg:{lookup_key}` Redis sorted-set on the server, returned in `from_server.Chat` — identical to peer messages.
|
||||
|
||||
2. **Device peers reuse the `Peer` struct** with `Type = "device"`, stored in `Identity.OwnedDevices`. They have their own three keypairs (`MyIdentity`, `MyEncryptionKp`, `MyLookupKp`) and `MyPullServers`.
|
||||
|
||||
3. **A new proto message `DeviceSyncPayload`** is added to `messages.proto`. It is serialised and placed in `UserMessage.Appdata`; the parent `UserMessage.Type` is set to `"device_sync"`. This lets the client recognise sync messages without any server-side awareness.
|
||||
|
||||
4. **`GetRequestJobs()`** is extended to include device lookup keys alongside peer lookup keys for the appropriate servers, so the background poll thread picks up device sync messages without any extra call.
|
||||
|
||||
5. **Dedup** is handled by a small SQLite table `device_sync_seen` (one table per identity folder, not per peer) keyed on `DeviceSyncPayload.DedupId`.
|
||||
|
||||
---
|
||||
|
||||
## New Protobuf Messages
|
||||
|
||||
Add to `pb/messages.proto` before re-generating:
|
||||
|
||||
```protobuf
|
||||
// Payload carried inside UserMessage.appdata for device-to-device sync.
|
||||
// The enclosing UserMessage.type MUST be "device_sync".
|
||||
message DeviceSyncPayload {
|
||||
string sync_type = 1; // "msg" | "status" | "peer_update" | "identity_update" | "server_add" | "forward"
|
||||
string peer_uid = 2; // local UID of the peer conversation on the sending device
|
||||
DbMessage db_message = 3; // the DbMessage to replicate (sync_type "msg" / "status")
|
||||
string dedup_id = 4; // globally unique ID (= DbMessage.status.uuid or generated)
|
||||
bytes peer_data = 5; // JSON-encoded Peer snapshot (sync_type "peer_update")
|
||||
bytes identity_data = 6; // JSON-encoded identity profile snapshot (sync_type "identity_update")
|
||||
bytes forward_payload = 7; // serialized UserMessage for primary to send on behalf of sibling (sync_type "forward")
|
||||
string forward_peer_uid = 8; // primary-side peer UID to forward to (sync_type "forward")
|
||||
}
|
||||
```
|
||||
|
||||
Run `cd pb && ./protogen.sh` after adding this.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1 — Device Pairing
|
||||
|
||||
**Files to touch:** `client/identity.go`, `client/helpers/` (new file `deviceHelper.go`)
|
||||
|
||||
**Goal:** Allow two app instances owned by the same user to establish a shared keypair relationship, mirroring the peer invitation flow but flagging the peer as `Type = "device"`.
|
||||
|
||||
#### 1.1 `Identity.InitDevicePairing(myDeviceName string, serverUids []string) (*Peer, error)`
|
||||
- Identical to `InvitePeer` but sets `peer.Type = "device"`.
|
||||
- Stores the resulting peer in `Identity.OwnedDevices` (not `Peers`).
|
||||
- Returns the peer so the caller can produce a `ContactCard` QR/file.
|
||||
- **Sym + DR inherited automatically**: because the implementation mirrors `InvitePeer`, the device peer will have `MySymKey`, `DrKpPublic`, `DrKpPrivate`, `DrRootKey`, and `DrInitiator = true` populated automatically. The resulting `ContactCard` will carry `dr_root_key` and `dr_public_key` so the answering device can initialise its own DR session via `AnswerDevicePairing`.
|
||||
|
||||
#### 1.2 `Identity.AnswerDevicePairing(myDeviceName string, receivedContact *meowlib.ContactCard) (*Peer, error)`
|
||||
- Mirrors `AnswerInvitation`, stores in `OwnedDevices`.
|
||||
|
||||
#### 1.3 `Identity.FinalizeDevicePairing(receivedContact *meowlib.ContactCard) error`
|
||||
- Mirrors `FinalizeInvitation`, operates on `OwnedDevices`.
|
||||
|
||||
#### 1.4 Helper functions (new file `client/helpers/deviceHelper.go`)
|
||||
```go
|
||||
// DevicePairingCreateMessage – wraps an invitation step-1 for a device peer.
|
||||
func DevicePairingCreateMessage(peer *client.Peer, serverUid string) ([]byte, string, error)
|
||||
|
||||
// DevicePairingAnswerMessage – wraps invitation step-3 answer for a device peer.
|
||||
func DevicePairingAnswerMessage(peer *client.Peer, serverUid string) ([]byte, string, error)
|
||||
```
|
||||
These reuse `invitationCreateHelper.go`/`invitationAnswerHelper.go` logic.
|
||||
|
||||
#### 1.5 Extend `PeerStorage` operations for OwnedDevices
|
||||
`OwnedDevices` is currently a `PeerList` (in-memory slice). This **must** be migrated to the same Badger-backed `PeerStorage` mechanism as `Peers` — it is no longer optional. Device peers carry a Double Ratchet session state (`DrStateJson`) that advances with every message sent or received. Without persistent storage the DR state is lost on restart, breaking the decryption of all subsequent messages. Add a `DeviceStorage PeerStorage` field to `Identity` with its own `DbFile`, and ensure `StorePeer` is called on the device peer after every outbound dispatch (in `DispatchSyncToDevices`) and after every inbound consume (in `ConsumeDeviceSyncMessage`), mirroring the pattern used in `messageHelper.go` and `bgPollHelper.go` for regular peers.
|
||||
|
||||
---
|
||||
|
||||
### Phase 2 — Sync Payload Helpers
|
||||
|
||||
**Files to touch:** `client/helpers/deviceHelper.go` (continued), `client/dbmessage.go`
|
||||
|
||||
#### 2.1 Build a sync message for one sibling device
|
||||
|
||||
```go
|
||||
// BuildDeviceSyncMessage wraps a DbMessage into a UserMessage addressed to a
|
||||
// sibling device peer. The caller then calls peer.ProcessOutboundUserMessage.
|
||||
func BuildDeviceSyncMessage(
|
||||
devicePeer *client.Peer,
|
||||
syncType string, // "msg" | "status" | "peer_event"
|
||||
peerUid string,
|
||||
dbm *meowlib.DbMessage,
|
||||
dedupId string,
|
||||
) (*meowlib.UserMessage, error)
|
||||
```
|
||||
|
||||
Implementation:
|
||||
1. Serialise `DeviceSyncPayload{SyncType, PeerUid, DbMessage, DedupId}` with `proto.Marshal`.
|
||||
2. Create a `UserMessage` with `Type = "device_sync"`, `Destination = devicePeer.ContactLookupKey`, `Appdata = serialisedPayload`.
|
||||
3. Set `Status.Uuid = dedupId`.
|
||||
|
||||
#### 2.2 Dispatch sync to all sibling devices
|
||||
|
||||
```go
|
||||
// DispatchSyncToDevices sends a DeviceSyncPayload to every device peer whose
|
||||
// pull server list overlaps with the available servers.
|
||||
// It enqueues a SendJob per device, reusing the existing bgSendHelper queue.
|
||||
func DispatchSyncToDevices(
|
||||
storagePath string,
|
||||
syncType string,
|
||||
peerUid string,
|
||||
dbm *meowlib.DbMessage,
|
||||
dedupId string,
|
||||
) error
|
||||
```
|
||||
|
||||
Iterates `identity.OwnedDevices`, builds and queues one `SendJob` per device (just like `CreateUserMessageAndSendJob` but using device peer keys and putting the message in `outbox/` with a recognisable prefix, e.g. `dev_{devPeerUid}_{dedupId}`).
|
||||
|
||||
After calling `peer.ProcessOutboundUserMessage` for each device peer, persist the updated DR state: `identity.DeviceStorage.StorePeer(devPeer)` if `devPeer.DrRootKey != ""`.
|
||||
|
||||
The message is packed into `ToServerMessage.Messages` (same field as regular chat). No server changes needed.
|
||||
|
||||
---
|
||||
|
||||
### Phase 3 — Integrate Dispatch into Send/Receive Paths
|
||||
|
||||
**Files to touch:** `client/helpers/messageHelper.go`, `client/helpers/bgPollHelper.go`
|
||||
|
||||
#### 3.1 After outbound message stored (`CreateAndStoreUserMessage`)
|
||||
|
||||
At the end of `CreateAndStoreUserMessage` (after `peer.StoreMessage`), add:
|
||||
|
||||
```go
|
||||
// Async: do not block the caller
|
||||
go DispatchSyncToDevices(storagePath, "msg", peerUid, dbm, usermessage.Status.Uuid)
|
||||
```
|
||||
|
||||
The `dbm` is obtained from `UserMessageToDbMessage(true, usermessage, nil)` (files are excluded from sync — they stay on the originating device or are re-requested).
|
||||
|
||||
#### 3.2 After inbound message stored (`ConsumeInboxFile`)
|
||||
|
||||
After `peer.StoreMessage(usermsg, filenames)` succeeds:
|
||||
|
||||
```go
|
||||
dbm := client.UserMessageToDbMessage(false, usermsg, nil)
|
||||
go DispatchSyncToDevices(storagePath, "msg", peer.Uid, dbm, usermsg.Status.Uuid)
|
||||
```
|
||||
|
||||
#### 3.3 After ACK status update (`ReadAckMessageResponse` — currently a stub)
|
||||
|
||||
When status timestamps (received/processed) are updated in the DB, dispatch a `"status"` sync with the updated `DbMessage`.
|
||||
|
||||
---
|
||||
|
||||
### Phase 4 — Receive & Consume Device Sync Messages
|
||||
|
||||
**Files to touch:** `client/helpers/bgPollHelper.go`, new `client/helpers/deviceSyncHelper.go`
|
||||
|
||||
#### 4.1 Extend `GetRequestJobs()` to include device lookup keys
|
||||
|
||||
In `identity.go:GetRequestJobs()`, after the loop over `Peers`, add a similar loop over `OwnedDevices`:
|
||||
|
||||
```go
|
||||
for _, devPeer := range id.OwnedDevices {
|
||||
for _, server := range devPeer.MyPullServers {
|
||||
if job, ok := srvs[server]; ok {
|
||||
job.LookupKeys = append(job.LookupKeys, devPeer.MyLookupKp)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Device messages will now arrive inside `from_server.Chat` alongside regular peer messages. The next step distinguishes them.
|
||||
|
||||
#### 4.2 Distinguish device vs peer messages in `ConsumeInboxFile`
|
||||
|
||||
After `identity.Peers.GetFromMyLookupKey(packedUserMessage.Destination)` returns `nil`, try:
|
||||
|
||||
```go
|
||||
devPeer := identity.OwnedDevices.GetFromMyLookupKey(packedUserMessage.Destination)
|
||||
if devPeer != nil {
|
||||
err := ConsumeDeviceSyncMessage(devPeer, packedUserMessage)
|
||||
// continue to next message
|
||||
continue
|
||||
}
|
||||
// original error path
|
||||
```
|
||||
|
||||
#### 4.3 `ConsumeDeviceSyncMessage` (new file `client/helpers/deviceSyncHelper.go`)
|
||||
|
||||
```go
|
||||
func ConsumeDeviceSyncMessage(
|
||||
devPeer *client.Peer,
|
||||
packed *meowlib.PackedUserMessage,
|
||||
) error
|
||||
```
|
||||
|
||||
Steps:
|
||||
1. Decrypt with `devPeer.ProcessInboundUserMessage(packed)` (takes the full `*PackedUserMessage` — **not** `payload, signature` separately; that API was updated when the sym-encryption + double-ratchet layer was added).
|
||||
2. Check `usermsg.Type == "device_sync"`.
|
||||
3. Deserialise `DeviceSyncPayload` from `usermsg.Appdata`.
|
||||
4. Dedup check: call `IsDeviceSyncSeen(payload.DedupId)`. If yes, skip.
|
||||
5. Mark seen: `MarkDeviceSyncSeen(payload.DedupId)`.
|
||||
6. **Persist DR state** — after decryption, if `devPeer.DrRootKey != ""`, call `identity.OwnedDevices.StorePeer(devPeer)` (or the equivalent Badger-backed store) to persist the updated `DrStateJson`. This mirrors what `ConsumeInboxFile` does for regular peers.
|
||||
7. Dispatch by `payload.SyncType`:
|
||||
- `"msg"`: find the local peer by `payload.PeerUid`, call `client.StoreDeviceSyncedMessage(peer, payload.DbMessage)`.
|
||||
- `"status"`: update the status fields in the existing DB row matched by `payload.DbMessage.Status.Uuid`.
|
||||
- `"peer_update"`: apply `payload.PeerData` to the local peer record (see Phase 6).
|
||||
- `"identity_update"`: apply `payload.IdentityData` to the local identity profile (see Phase 6).
|
||||
|
||||
#### 4.4 `StoreDeviceSyncedMessage` in `client/messagestorage.go`
|
||||
|
||||
A thin wrapper around `storeMessage` that:
|
||||
- Marks the message as synced (a new bool field `Synced` in `DbMessage`, or use a naming convention in `DbMessage.Appdata`).
|
||||
- Does **not** trigger a second round of sync dispatch (no re-broadcast).
|
||||
- Handles absent file paths gracefully (files are not synced, only metadata).
|
||||
|
||||
---
|
||||
|
||||
### Phase 6 — Peer Metadata and Identity Profile Sync
|
||||
|
||||
**Files to touch:** `client/helpers/deviceHelper.go`, `client/helpers/deviceSyncHelper.go`, `client/identity.go`
|
||||
|
||||
The goal is to propagate non-message data across sibling devices: peer names/avatars/settings and the identity profile. This is **one-directional fan-out** (whichever device makes the change dispatches to all siblings) — no merge protocol is needed because conflicts are resolved by last-write-wins (the dedupId carries a timestamp or UUID sufficient for dedup; ordering is not guaranteed but is acceptable for profile data).
|
||||
|
||||
#### 6.1 Peer metadata sync (`sync_type = "peer_update"`)
|
||||
|
||||
Dispatch a `"peer_update"` payload whenever a peer record is meaningfully mutated (name, avatar, notification settings, visibility, blocked state, etc.).
|
||||
|
||||
**Payload**: `DeviceSyncPayload.PeerData` is a JSON-encoded **full `Peer` struct**, including all private key material and DR state. This is safe because:
|
||||
- The device sync channel is E2E-encrypted with the same X25519 + sym + DR stack as peer messages.
|
||||
- The target server is user-owned; the operator is the user themselves.
|
||||
- The recipient is the same person on a different device.
|
||||
|
||||
Fields included in `PeerData`:
|
||||
- All keypairs in full: `MyIdentity`, `MyEncryptionKp`, `MyLookupKp` (private + public)
|
||||
- `MySymKey` — shared symmetric key for that peer's channel
|
||||
- `DrKpPrivate`, `DrKpPublic`, `DrRootKey`, `DrInitiator`, `ContactDrPublicKey`
|
||||
- **`DrStateJson`** — current live DR session state (see DR note below)
|
||||
- All contact keys: `ContactPublicKey`, `ContactEncryption`, `ContactLookupKey`, `ContactPullServers`
|
||||
- All metadata: `Name`, `Avatar`, `Avatars`, `MyName`, `Visible`, `Blocked`, `MessageNotification`, `SendDeliveryAck`, `SendProcessingAck`, `CallsAllowed`, server lists, etc.
|
||||
|
||||
Fields excluded from `PeerData`:
|
||||
- `dbPassword` — transient in-memory field, never serialised; the receiving device uses its own memory password.
|
||||
|
||||
The receiving device upserts the peer into its local `Peers` store. After applying the sync, the sibling device is a full participant in the conversation: it can send and receive messages using the replicated keypairs, has the same DR session state, and monitors the same lookup key queues.
|
||||
|
||||
**DR state sync (Phase 6 only)**: Syncing `DrStateJson` as part of `"peer_update"` gives sibling devices a working DR session at the point of pairing and keeps them in sync during normal single-active-device use. Phase 7 supersedes this with independent per-device DR sessions, eliminating all shared-state concerns. If Phase 7 is implemented, the `DrStateJson` field in `PeerData` can be omitted from the sync payload (each device initialises its own fresh session via the device introduction flow).
|
||||
|
||||
**New peers**: When Device A completes an invitation with a new contact, it dispatches `"peer_update"` to all siblings with the full peer record. Device B immediately becomes a full participant — same keypairs, same lookup key, same DR session start state — and can transparently send and receive messages with that contact without any secondary invitation.
|
||||
|
||||
#### 6.2 Identity profile sync (`sync_type = "identity_update"`)
|
||||
|
||||
Dispatch an `"identity_update"` whenever `Identity.Nickname`, `Identity.DefaultAvatar`, `Identity.Avatars`, or `Identity.Status` changes.
|
||||
|
||||
**Payload**: `DeviceSyncPayload.IdentityData` is a JSON-encoded subset of `Identity`:
|
||||
```go
|
||||
type IdentityProfileSnapshot struct {
|
||||
Nickname string `json:"nickname"`
|
||||
DefaultAvatar string `json:"default_avatar"`
|
||||
Avatars []Avatar `json:"avatars"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
```
|
||||
|
||||
The receiving device deserialises this and updates only the listed fields on its local `Identity`, then calls `identity.Save()`.
|
||||
|
||||
**Explicitly NOT synced** in `IdentityData`:
|
||||
- `RootKp` — the user's root signing keypair is the trust anchor; it should be established once per identity creation and never transmitted, even over a secure channel. Compromise of the root key invalidates the entire identity.
|
||||
- `Device` — device-specific keypair for server auth; each device has its own.
|
||||
- `OwnedDevices` — the device mesh itself; managed separately by the pairing flow.
|
||||
- `HiddenPeers` — sensitive by design; out of scope.
|
||||
- `DefaultDbPassword`, `DbPasswordStore` — local security preferences.
|
||||
- `MessageServers` / `Peers` — covered by their own sync types (`"server_add"`, `"peer_update"`).
|
||||
|
||||
#### 6.3 Server list sync (future — `sync_type = "server_add"`)
|
||||
|
||||
When a new `MessageServer` is added to one device's `MessageServers`, dispatch `"server_add"` so all siblings discover it. Implementation deferred; placeholder `sync_type` reserved.
|
||||
|
||||
#### 6.4 Dispatch hooks
|
||||
|
||||
- After `Identity.InvitePeer` / `FinalizeInvitation` / any peer metadata update: call `DispatchSyncToDevices(..., "peer_update", peer.Uid, nil, uuid.New().String())`.
|
||||
- After `Identity.Save()` when profile fields changed: call `DispatchSyncToDevices(..., "identity_update", "", nil, uuid.New().String())`.
|
||||
|
||||
---
|
||||
|
||||
### Phase 7 — Per-Device DR Sessions (Bullet-proof Forward Secrecy)
|
||||
|
||||
**Goal**: Eliminate the concurrent-send DR race without shared ratchet state and without leaking device count to contacts.
|
||||
|
||||
#### 7.0 Privacy constraint
|
||||
|
||||
The naive per-device DR approach (introduce all devices to all contacts) has a fundamental privacy problem: every contact learns how many devices you own and receives session material for each. This leaks metadata — device count, device rotation events, possibly device fingerprints. This is unacceptable for a privacy-first library.
|
||||
|
||||
Two architecturally sound options are described below. **Option B (primary device relay) is recommended** because it preserves complete contact-side opacity and requires no protocol extension on the contact side.
|
||||
|
||||
---
|
||||
|
||||
#### Option A — Contact-aware per-device sessions (not recommended)
|
||||
|
||||
Each device is introduced to all contacts via a `"device_introduce"` message. The contact maintains one independent DR session per device and sends a separate encrypted copy per device on every message.
|
||||
|
||||
| Property | Value |
|
||||
|---|---|
|
||||
| DR race | ❌ Eliminated |
|
||||
| Contact privacy | ❌ Contacts learn device count and session keys |
|
||||
| Contact protocol change | ✅ Required (handle `DeviceInfo` list, multi-destination send) |
|
||||
| Backward compatibility | ❌ Old clients can't participate |
|
||||
| Server changes | ✅ None |
|
||||
|
||||
This is Signal's model. It is appropriate when contacts are expected to be aware of device multiplicity (e.g. a closed ecosystem). It is **not** appropriate for meowlib's open, privacy-first design.
|
||||
|
||||
---
|
||||
|
||||
#### Option B — Primary device relay (recommended)
|
||||
|
||||
The device that owns the peer relationship (the one whose keypairs are in the `Peer` record — call it the **primary**) is the only device that ever communicates directly with a contact. Its DR session with the contact is singular, unshared, and advances normally.
|
||||
|
||||
Sibling devices that want to send a message do so by dispatching a `"forward"` device sync payload to the primary. The primary re-encrypts with the contact's keys and forwards. From the contact's perspective: one sender, one DR session, zero device awareness.
|
||||
|
||||
| Property | Value |
|
||||
|---|---|
|
||||
| DR race | ❌ Eliminated (only primary drives the DR session) |
|
||||
| Contact privacy | ✅ Contact is completely unaware of sibling devices |
|
||||
| Contact protocol change | ✅ None required |
|
||||
| Backward compatibility | ✅ Full |
|
||||
| Server changes | ✅ None |
|
||||
| Trade-off | If primary is offline, sibling outbound messages queue until it returns |
|
||||
|
||||
##### 7.1 Primary device designation
|
||||
|
||||
The device that completes the invitation flow for a peer (calls `InvitePeer` or `FinalizeInvitation`) is the primary for that peer. The `Peer` record synced to sibling devices carries a `PrimaryDeviceUid string` field (the UID of the device peer that "owns" this peer relationship):
|
||||
|
||||
```go
|
||||
// Add to Peer struct:
|
||||
PrimaryDeviceUid string `json:"primary_device_uid,omitempty"`
|
||||
// empty = this device IS the primary for this peer
|
||||
```
|
||||
|
||||
When a sibling device receives a `"peer_update"` sync, it sets `PrimaryDeviceUid` to the sender's device UID. When the primary device sends a peer update, it leaves `PrimaryDeviceUid` empty (it is the primary).
|
||||
|
||||
##### 7.2 New sync type: `"forward"`
|
||||
|
||||
Add to `DeviceSyncPayload.sync_type`:
|
||||
|
||||
```
|
||||
"forward" — sibling device requests primary to send a message to a peer on its behalf
|
||||
```
|
||||
|
||||
New fields needed in `DeviceSyncPayload`:
|
||||
|
||||
```protobuf
|
||||
bytes forward_payload = 7; // serialized UserMessage (plaintext, will be encrypted by primary)
|
||||
string forward_peer_uid = 8; // local peer UID on the primary device to forward to
|
||||
```
|
||||
|
||||
##### 7.3 Send path on a sibling device
|
||||
|
||||
When a sibling device (one where `peer.PrimaryDeviceUid != ""`) sends a message to peer P:
|
||||
|
||||
1. Build the `UserMessage` normally.
|
||||
2. **Do not** call `peer.ProcessOutboundUserMessage` — the sibling does not have a valid DR state for the contact.
|
||||
3. Serialize the `UserMessage` (plaintext proto bytes).
|
||||
4. Build a `DeviceSyncPayload{SyncType: "forward", ForwardPayload: serialized, ForwardPeerUid: peer.Uid}`.
|
||||
5. Dispatch it to the primary device via the normal device sync send path.
|
||||
6. Store the message locally with a `"pending_forward"` status so the UI reflects it immediately.
|
||||
|
||||
##### 7.4 Receive and forward path on the primary device
|
||||
|
||||
When `ConsumeDeviceSyncMessage` on the primary sees `sync_type == "forward"`:
|
||||
|
||||
1. Deserialize `ForwardPayload` into a `UserMessage`.
|
||||
2. Locate the local peer by `ForwardPeerUid`.
|
||||
3. Call `peer.ProcessOutboundUserMessage(userMessage)` — primary uses its DR session normally.
|
||||
4. Enqueue a `SendJob` to deliver to the contact's server (same path as any outbound message).
|
||||
5. Dispatch a `"msg"` sync back to all siblings with the now-stored `DbMessage` so they update the message status from `"pending_forward"` to sent.
|
||||
|
||||
##### 7.5 Offline queuing
|
||||
|
||||
If the primary device is offline when the sibling dispatches a `"forward"` sync, the sync message sits in the device sync queue on the server (same Redis sorted-set as all device messages). When the primary comes back online and polls, it picks up the forwarded message and delivers it. No message is lost; latency equals the primary's offline window.
|
||||
|
||||
##### 7.6 Result
|
||||
|
||||
- Zero contact protocol changes. Contacts cannot distinguish a primary-only device from a multi-device user.
|
||||
- No device count leakage. Device topology is fully opaque to the outside world.
|
||||
- No DR race. The primary drives a single ratchet per contact.
|
||||
- No server changes.
|
||||
- `ProcessOutboundUserMessage` signature stays `(*PackedUserMessage, error)` — no ripple through callers.
|
||||
- Trade-off is well-bounded: forward latency ≤ primary polling interval, which is already the existing long-poll timeout.
|
||||
|
||||
---
|
||||
|
||||
### Phase 5 — Dedup Store
|
||||
|
||||
**Files to touch:** new `client/devicesyncdedup.go`
|
||||
|
||||
A single SQLite DB per identity folder: `{StoragePath}/{IdentityUuid}/devicesync.db`.
|
||||
|
||||
Schema:
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS seen (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
seen_at INTEGER NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
Functions:
|
||||
```go
|
||||
func IsDeviceSyncSeen(storagePath, identityUuid, dedupId string) (bool, error)
|
||||
func MarkDeviceSyncSeen(storagePath, identityUuid, dedupId string) error
|
||||
func PruneDeviceSyncSeen(storagePath, identityUuid string, olderThan time.Duration) error
|
||||
```
|
||||
|
||||
`PruneDeviceSyncSeen` is called periodically (e.g. weekly) from the background thread to remove entries older than 30 days.
|
||||
|
||||
---
|
||||
|
||||
## File Change Summary
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `pb/messages.proto` | Add `DeviceSyncPayload` message (with `peer_data` and `identity_data` fields) |
|
||||
| `pb/protogen.sh` → re-run | Regenerate `.pb.go` |
|
||||
| `client/identity.go` | Add `InitDevicePairing`, `AnswerDevicePairing`, `FinalizeDevicePairing`; add `DeviceStorage PeerStorage` field; extend `GetRequestJobs()`; add profile-change dispatch hooks |
|
||||
| `client/peer.go` | No changes needed (Type field already exists) |
|
||||
| `client/messagestorage.go` | Add `StoreDeviceSyncedMessage` |
|
||||
| `client/devicesyncdedup.go` | **New** — dedup SQLite helpers |
|
||||
| `client/helpers/deviceHelper.go` | **New** — `BuildDeviceSyncMessage`, `DispatchSyncToDevices` (msg + peer_update + identity_update), pairing message helpers |
|
||||
| `client/helpers/deviceSyncHelper.go` | **New** — `ConsumeDeviceSyncMessage` (handles all sync types) |
|
||||
| `client/helpers/messageHelper.go` | Add `DispatchSyncToDevices` call after outbound store; detect primary vs sibling role on send |
|
||||
| `client/helpers/bgPollHelper.go` | Add device message detection in `ConsumeInboxFile` |
|
||||
| `client/peer.go` | Add `PrimaryDeviceUid string` field; sibling send path dispatches `"forward"` instead of direct send |
|
||||
| `client/helpers/deviceSyncHelper.go` | Handle `"forward"` sync type: deserialize, re-encrypt, enqueue SendJob, dispatch `"msg"` sync back |
|
||||
|
||||
Server package: **no changes required**.
|
||||
|
||||
---
|
||||
|
||||
## Sync Scope
|
||||
|
||||
| Data | Synced | Notes |
|
||||
|---|---|---|
|
||||
| Message text / data | ✅ | In `DbMessage.Data` |
|
||||
| Outbound flag | ✅ | In `DbMessage.Outbound` |
|
||||
| Message UUID | ✅ | Via `ConversationStatus.Uuid` |
|
||||
| Sent/received timestamps | ✅ | In `ConversationStatus` |
|
||||
| File content | ❌ | Not synced; only `FilePaths` metadata synced |
|
||||
| Peer full keypairs (private + public) | ✅ | Phase 6 — included in `"peer_update"` `PeerData`; channel is E2E-encrypted on user-owned server |
|
||||
| Peer symmetric key | ✅ | Phase 6 — included in `"peer_update"` `PeerData` |
|
||||
| Peer DR session state (`DrStateJson`) | ✅ | Phase 6 — synced on peer_update; Phase 7 (Option B) eliminates the need: primary drives one DR session, siblings never touch it |
|
||||
| Peer metadata (name, avatar, settings) | ✅ | Phase 6 — `"peer_update"` sync type |
|
||||
| New peer (unknown to sibling) | ✅ | Full peer record synced; sibling becomes immediate full participant |
|
||||
| Identity profile (nickname, avatar, status) | ✅ | Phase 6 — `"identity_update"` sync type |
|
||||
| Identity root keypair (`RootKp`) | ❌ | Trust anchor; never transmitted even over secure channel |
|
||||
| Known/message server list | ⚠️ | Future — `"server_add"` placeholder reserved |
|
||||
| Hidden peers | ❌ | Hidden by design; out of scope |
|
||||
| Device keypair | ❌ | Per-device; each device authenticates to servers with its own key |
|
||||
|
||||
---
|
||||
|
||||
## Privacy Properties
|
||||
|
||||
- Device sync messages are end-to-end encrypted (same X25519 + sym + DR stack as peer messages).
|
||||
- The server sees only the device lookup key as destination; it cannot distinguish sync messages from peer messages.
|
||||
- Including device lookup keys in batch pull requests does not leak which device belongs to you (same privacy model as multiple peer lookup keys per request).
|
||||
- `OwnedDevices` peers should be treated as "hidden" (not shown in contact lists) and stored in the device storage, separate from regular peers.
|
||||
- **Contacts are never made aware of device count or device identity** (Phase 7 Option B). The primary device relay model means the outside world observes exactly one sender per user identity, regardless of how many devices are active.
|
||||
- The device mesh topology (which devices exist, how many) is known only to the user's own devices, and is carried exclusively over the E2E-encrypted device sync channel on the user-owned server.
|
||||
|
||||
---
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
1. **Unit tests** for `DeviceSyncPayload` serialisation round-trip.
|
||||
2. **Unit tests** for dedup store (seen/mark/prune lifecycle).
|
||||
3. **Integration test** extending `TestEndToEnd`:
|
||||
- Create identity, two device peers (DeviceA, DeviceB).
|
||||
- Send a message on DeviceA.
|
||||
- Verify DeviceB's DB contains the synced message after `ConsumeDeviceSyncMessage`.
|
||||
- Resend the same dedup_id — verify no duplicate row created.
|
||||
4. **Integration test** for inbound sync:
|
||||
- DeviceA receives a peer message.
|
||||
- Verify DeviceB gets the sync and stores it correctly.
|
||||
@@ -0,0 +1,24 @@
|
||||
@startuml
|
||||
|
||||
|
||||
actor Sender
|
||||
actor Receiver
|
||||
component Server
|
||||
component Router
|
||||
queue msgch
|
||||
queue dvych
|
||||
|
||||
collections msg
|
||||
collections dvyrq
|
||||
collections dvy
|
||||
|
||||
UserSender -> Server : mesg
|
||||
Server -> Router : mesg
|
||||
Router -> msg : store
|
||||
Router -> dvyrq : store
|
||||
Router -> msgch : publish
|
||||
msgch -> Receiver : notifiaction
|
||||
msg -> Receiver : mesg
|
||||
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1,20 @@
|
||||
@startuml "Simple messaging"
|
||||
actor Sender as snd
|
||||
actor Receiver as rcv
|
||||
control Server as srv
|
||||
collections msg as msg
|
||||
queue msgch as msgch
|
||||
collections dvyrq as dvyrq
|
||||
collections dvy as dvy
|
||||
queue dvych as dvych
|
||||
|
||||
rcv->srv: Listen
|
||||
srv->msgch: Subscribe
|
||||
snd->srv: Send message
|
||||
srv->msg: Store message
|
||||
srv->msgch: Notify listening receivers
|
||||
msgch->srv: Notify
|
||||
msg->srv: Grab message
|
||||
srv->rcv: Send message
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1,30 @@
|
||||
@startuml "Messaging with server delivery"
|
||||
actor Sender as snd
|
||||
actor Receiver as rcv
|
||||
control Server as srv
|
||||
collections msg as msg
|
||||
queue msgch as msgch
|
||||
collections dvyrq as dvyrq
|
||||
collections dvy as dvy
|
||||
queue dvych as dvych
|
||||
|
||||
|
||||
rcv->srv: Listen for messages
|
||||
srv->msgch: Subscribe
|
||||
snd->srv: Send message with delivery uid
|
||||
snd->msg: Listen for delivery
|
||||
|
||||
group storeMessage
|
||||
srv->msg: Store message
|
||||
srv->dvyrq: Store delivery request uid=>Receiver
|
||||
srv->msgch: Notify listening receivers
|
||||
end
|
||||
msgch->srv: Notify
|
||||
msg->srv: Grab message
|
||||
srv->rcv: Cuts listening with message
|
||||
srv->dvy: Store delivery done Receiver=>uid
|
||||
srv->dvych: Notify listening Sender
|
||||
dvych->srv: Notify
|
||||
dvy->srv: Grab delivery
|
||||
srv->snd: Cuts listening with delivery
|
||||
@enduml
|
||||
@@ -1,8 +0,0 @@
|
||||
@startuml
|
||||
Client1 -> Server: Send Message (PubKeyClient2 + Payload)
|
||||
Server --> Client1: Ack Message (Server UUID + DateReceived)
|
||||
Client2 -> Server: Get Messages (PubKeyClient2 + IpPublish)
|
||||
Server --> Client2: Available Messages list (UUID list cyphered with PubKeyClient2)
|
||||
Client2 <- Server: Get Messages (decoded UUID signed with PK)
|
||||
Server --> Client2: Messages [](PubKeyClient2 + Payload)
|
||||
@enduml
|
||||
+71
-54
@@ -22,101 +22,118 @@ func TestEndToEnd(t *testing.T) {
|
||||
///////////////////////////
|
||||
// Creating New Identity //
|
||||
///////////////////////////
|
||||
Me = client.CreateIdentity("myname")
|
||||
Me, err = client.CreateIdentity("myname")
|
||||
|
||||
// define my preferences (servers)
|
||||
Me.MessageServers.Name = "Message Servers"
|
||||
Me.MessageServers.AddUrls([]string{"http://127.0.0.1/meow/", "mqtt://127.0.0.1", "meow://127.0.0.1"})
|
||||
srv := client.Server{Name: "MyServer", Url: "http://127.0.0.1/meow/"}
|
||||
Me.MessageServers.StoreServer(&srv)
|
||||
srv = client.Server{Name: "MyServer", Url: "mqtt://127.0.0.1"}
|
||||
Me.MessageServers.StoreServer(&srv)
|
||||
srv = client.Server{Name: "MyServer", Url: "meow://127.0.0.1"}
|
||||
Me.MessageServers.StoreServer(&srv)
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Create an invitation for a friend, I want him/her to know me as Bender //
|
||||
// STEP_1: Create an invitation — only a temp keypair, no full ContactCard //
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
fmt.Println("Creating an invitation for the first friend...")
|
||||
peer, myContactCard, err := Me.InvitePeer("Bender", "myfirstfriend", []int{1, 2})
|
||||
initPayload, peer, err := Me.InvitationStep1("Bender", "myfirstfriend", []string{"http://127.0.0.1/meow/", "mqtt://127.0.0.1"}, "welcome, it's me!")
|
||||
if err != nil {
|
||||
println(err)
|
||||
}
|
||||
println(peer.Name)
|
||||
// print my invitation
|
||||
a, _ := json.Marshal(myContactCard)
|
||||
fmt.Println(string(a))
|
||||
// TODO : Convert invitation to QR Code
|
||||
myContactCard.WritePng("invitation.png")
|
||||
data, err := myContactCard.Compress()
|
||||
a, _ := json.Marshal(initPayload)
|
||||
fmt.Println("InvitationInitPayload:", string(a))
|
||||
data, err := initPayload.Compress()
|
||||
if err != nil {
|
||||
println(err)
|
||||
}
|
||||
myContactCard.WriteQr("qrcode.png")
|
||||
println("Compressed contact card :", len(data))
|
||||
///////////////////////////////////////
|
||||
// Simulate peer invitation response //
|
||||
///////////////////////////////////////
|
||||
fmt.Println("Simulating first friend answer...")
|
||||
var ReceivedContact meowlib.ContactCard
|
||||
initPayload.WriteQr("qrcode.png")
|
||||
println("Compressed init payload :", len(data))
|
||||
|
||||
// Friend simulated invitation
|
||||
FirstFriendContactKp := meowlib.NewKeyPair()
|
||||
FirstFriendEncryptionKp := meowlib.NewKeyPair()
|
||||
FirstFriendLookupKp := meowlib.NewKeyPair()
|
||||
ReceivedContact.Name = "I'm the friend"
|
||||
ReceivedContact.ContactPublicKey = FirstFriendContactKp.Public
|
||||
ReceivedContact.EncryptionPublicKey = FirstFriendEncryptionKp.Public
|
||||
ReceivedContact.LookupPublicKey = FirstFriendLookupKp.Public
|
||||
ReceivedContact.InvitationId = myContactCard.InvitationId
|
||||
FriendServer1KP := meowlib.NewKeyPair()
|
||||
///////////////////////////////////////////////////////
|
||||
// STEP_2: Simulate friend receiving the payload and answering //
|
||||
///////////////////////////////////////////////////////
|
||||
fmt.Println("Simulating first friend answer (STEP_2)...")
|
||||
friendMe, err := client.CreateIdentity("friendname")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
friendPeer, err := friendMe.InvitationStep2("FriendNick", "Bender", []string{}, initPayload)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
friendCC := friendPeer.GetMyContact()
|
||||
FriendServer1KP, err := meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
FriendServer1 := meowlib.ServerCard{Name: "FriendServer1", Url: "http://myfriend.org/meow/", PublicKey: FriendServer1KP.Public, Description: "Fancy description"}
|
||||
ReceivedContact.PullServers = append(ReceivedContact.PullServers, &FriendServer1)
|
||||
friendCC.PullServers = append(friendCC.PullServers, &FriendServer1)
|
||||
|
||||
///////////////////////////////////////////////////////
|
||||
// Finalize the contact with the invitation response //
|
||||
///////////////////////////////////////////////////////
|
||||
Me.FinalizeInvitation(&ReceivedContact)
|
||||
//////////////////////////////////////////////////////////////////
|
||||
// STEP_3: Initiator receives friend's CC, generates full keypairs //
|
||||
//////////////////////////////////////////////////////////////////
|
||||
myCC, _, err := Me.InvitationStep3(friendCC)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fmt.Println("Initiator ContactCard ready:", myCC.ContactPublicKey != "")
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
// STEP_4: Friend finalizes initiator's ContactCard //
|
||||
////////////////////////////////////////////////////
|
||||
err = friendMe.InvitationStep4(myCC)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = Me.Save()
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
a, _ = json.Marshal(Me)
|
||||
os.WriteFile("id.json", a, 0644)
|
||||
fmt.Println(string(a))
|
||||
/////////////////////////////////////
|
||||
// Create a message to that friend //
|
||||
/////////////////////////////////////
|
||||
MyFirstFriend := Me.Peers[0]
|
||||
peers, _ := Me.Peers.GetPeers()
|
||||
MyFirstFriend := peers[0]
|
||||
textmessage := "Hello friend!"
|
||||
// Creating User message
|
||||
usermessage, err := MyFirstFriend.BuildSimpleUserMessage([]byte(textmessage))
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
t.Fatal(err)
|
||||
}
|
||||
serializedMessage, err := MyFirstFriend.SerializeUserMessage(usermessage)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Encrypting it
|
||||
enc, err := MyFirstFriend.AsymEncryptMessage(serializedMessage)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Packing it
|
||||
packedMsg := MyFirstFriend.PackUserMessage(enc.Data, enc.Signature)
|
||||
|
||||
srv := MyFirstFriend.Contact.PullServers[0]
|
||||
intS1 := client.CreateServerFromServerCard(srv)
|
||||
|
||||
intS1, err := Me.MessageServers.LoadServer("http://127.0.0.1/meow/")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Creating Server message for transporting the user message
|
||||
toServerMessage, err := intS1.BuildMessageSendingMessage(packedMsg)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Encrypting it
|
||||
encToServer, err := intS1.AsymEncryptMessage(toServerMessage)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Packing it
|
||||
protoPackedServerMsg, err := intS1.PackServerMessage(encToServer.Data, encToServer.Signature)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
t.Fatal(err)
|
||||
}
|
||||
///////////////////////
|
||||
// Sending to server //
|
||||
@@ -130,40 +147,40 @@ func TestEndToEnd(t *testing.T) {
|
||||
// Simulating server side processing //
|
||||
///////////////////////////////////////
|
||||
var server1 server.Identity
|
||||
server1.ServerName = intS1.ServerData.Name
|
||||
server1.ServerName = intS1.Name
|
||||
server1.ServerKp = FriendServer1KP
|
||||
server1.ServerDesc = intS1.ServerData.Description
|
||||
server1.ServerDesc = intS1.Description
|
||||
// Unpack
|
||||
srv_from, srv_encmsg, srv_signature, err := server1.UnpackReceived(protoPackedServerMsg)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Decrypt
|
||||
srv_clear, err := server1.AsymDecryptMessage(srv_from, srv_encmsg, srv_signature)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Decode msg
|
||||
srv_msg, err := server1.DeserializeToServerMessage(srv_clear)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Response : Ack received message
|
||||
srv_fromServerMessage, err := server1.BuildSimpleAckResponseMessage(srv_msg.Uuid)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
t.Fatal(err)
|
||||
}
|
||||
encoded_srv_fromServerMessage, err := server1.SerializeFromServerMessage(srv_fromServerMessage)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
t.Fatal(err)
|
||||
}
|
||||
srv_resp, err := server1.AsymEncryptMessage(srv_from, encoded_srv_fromServerMessage)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp, err := server1.PackForSending(srv_resp.Data, srv_resp.Signature)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
t.Fatal(err)
|
||||
}
|
||||
print(resp)
|
||||
//////////////////////////////////////////////
|
||||
|
||||
@@ -1,20 +1,56 @@
|
||||
module forge.redroom.link/yves/meowlib
|
||||
|
||||
go 1.16
|
||||
go 1.23.1
|
||||
|
||||
toolchain go1.24.2
|
||||
|
||||
require (
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c // indirect
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.7.3
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.8.3
|
||||
github.com/awnumar/memguard v0.23.0
|
||||
github.com/dgraph-io/badger v1.6.2
|
||||
github.com/go-redis/redis v6.15.9+incompatible
|
||||
github.com/google/uuid v1.3.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/livekit/protocol v1.16.0
|
||||
github.com/makiuchi-d/gozxing v0.1.1
|
||||
github.com/mattn/go-sqlite3 v1.14.16
|
||||
github.com/onsi/ginkgo v1.16.5 // indirect
|
||||
github.com/onsi/gomega v1.22.1 // indirect
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/rs/zerolog v1.31.0
|
||||
github.com/stretchr/testify v1.7.0
|
||||
golang.org/x/net v0.16.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
google.golang.org/protobuf v1.31.0
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
google.golang.org/protobuf v1.36.6
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.2.0 // indirect
|
||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
|
||||
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect
|
||||
github.com/alicebob/miniredis v2.5.0+incompatible // indirect
|
||||
github.com/awnumar/memcall v0.4.0 // indirect
|
||||
github.com/cespare/xxhash v1.1.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgraph-io/ristretto v0.0.2 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.3 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/gomodule/redigo v1.9.3 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/onsi/ginkgo v1.16.5 // indirect
|
||||
github.com/onsi/gomega v1.30.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/status-im/doubleratchet v3.0.0+incompatible // indirect
|
||||
github.com/twitchtv/twirp v8.1.3+incompatible // indirect
|
||||
github.com/yuin/gopher-lua v1.1.1 // indirect
|
||||
golang.org/x/crypto v0.41.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
|
||||
golang.org/x/net v0.42.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c // indirect
|
||||
google.golang.org/grpc v1.62.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -1,28 +1,75 @@
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c h1:kMFnB0vCcX7IL/m9Y5LO+KQYv+t1CQOiFe6+SV2J7bE=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M=
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/ProtonMail/go-crypto v1.2.0 h1:+PhXXn4SPGd+qk76TlEePBfOfivE0zkWFenhGhFLzWs=
|
||||
github.com/ProtonMail/go-crypto v1.2.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.7.3 h1:AJu1OI/1UWVYZl6QcCLKGu9OTngS2r52618uGlje84I=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.7.3/go.mod h1:IhkNEDaxec6NyzSI0PlxapinnwPVIESk8/76da3Ct3g=
|
||||
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
|
||||
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.8.3 h1:1jHlELwCR00qovx2B50DkL/FjYwt/P91RnlsqeOp2Hs=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.8.3/go.mod h1:LiuOTbnJit8w9ZzOoLscj0kmdALY7hfoCVh5Qlb0bcg=
|
||||
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE=
|
||||
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
|
||||
github.com/alicebob/miniredis v2.5.0+incompatible h1:yBHoLpsyjupjz3NL3MhKMVkR41j82Yjf3KFv7ApYzUI=
|
||||
github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/awnumar/memcall v0.4.0 h1:B7hgZYdfH6Ot1Goaz8jGne/7i8xD4taZie/PNSFZ29g=
|
||||
github.com/awnumar/memcall v0.4.0/go.mod h1:8xOx1YbfyuCg3Fy6TO8DK0kZUua3V42/goA5Ru47E8w=
|
||||
github.com/awnumar/memguard v0.23.0 h1:sJ3a1/SWlcuKIQ7MV+R9p0Pvo9CWsMbGZvcZQtmc68A=
|
||||
github.com/awnumar/memguard v0.23.0/go.mod h1:olVofBrsPdITtJ2HgxQKrEYEMyIBAIciVG4wNnZhW9M=
|
||||
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
|
||||
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgraph-io/badger v1.6.2 h1:mNw0qs90GVgGGWylh0umH5iag1j6n/PeJtNvL6KY/x8=
|
||||
github.com/dgraph-io/badger v1.6.2/go.mod h1:JW2yswe3V058sS0kZ2h/AXeDSqFjxnZcRrVH//y2UQE=
|
||||
github.com/dgraph-io/ristretto v0.0.2 h1:a5WaUrDa0qm0YrAAS1tUykT5El3kt62KNZZeMxQn3po=
|
||||
github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
|
||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
|
||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/eapache/channels v1.1.0 h1:F1taHcn7/F0i8DYqKXJnyhJcVpp2kgFcNePxXtnyu4k=
|
||||
github.com/eapache/channels v1.1.0/go.mod h1:jMm2qB5Ubtg9zLd+inMZd2/NUvXgzmWXsDaLyQIGfH0=
|
||||
github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=
|
||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||
github.com/frostbyte73/core v0.0.10 h1:D4DQXdPb8ICayz0n75rs4UYTXrUSdxzUfeleuNJORsU=
|
||||
github.com/frostbyte73/core v0.0.10/go.mod h1:XsOGqrqe/VEV7+8vJ+3a8qnCIXNbKsoEiu/czs7nrcU=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/gammazero/deque v0.2.1 h1:qSdsbG6pgp6nL7A0+K/B7s12mcCY/5l5SIUpMOl+dC0=
|
||||
github.com/gammazero/deque v0.2.1/go.mod h1:LFroj8x4cMYCukHJDbxFCkT+r9AndaJnFMuZDV34tuU=
|
||||
github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
|
||||
github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
|
||||
github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
@@ -30,71 +77,180 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/gomodule/redigo v1.9.3 h1:dNPSXeXv6HCq2jdyWfjgmhBdqnR6PRO3m/G05nvpPC8=
|
||||
github.com/gomodule/redigo v1.9.3/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jxskiss/base62 v1.1.0 h1:A5zbF8v8WXx2xixnAKD2w+abC+sIzYJX+nxmhA6HWFw=
|
||||
github.com/jxskiss/base62 v1.1.0/go.mod h1:HhWAlUXvxKThfOlZbcuFzsqwtF5TcqS9ru3y5GfjWAc=
|
||||
github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI=
|
||||
github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
|
||||
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lithammer/shortuuid/v4 v4.0.0 h1:QRbbVkfgNippHOS8PXDkti4NaWeyYfcBTHtw7k08o4c=
|
||||
github.com/lithammer/shortuuid/v4 v4.0.0/go.mod h1:Zs8puNcrvf2rV9rTH51ZLLcj7ZXqQI3lv67aw4KiB1Y=
|
||||
github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1 h1:jm09419p0lqTkDaKb5iXdynYrzB84ErPPO4LbRASk58=
|
||||
github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1/go.mod h1:Rs3MhFwutWhGwmY1VQsygw28z5bWcnEYmS1OG9OxjOQ=
|
||||
github.com/livekit/protocol v1.16.0 h1:TkUuirvfF1xIfpo5szXqAEEgg7QyML8d0O7+4NQpM7w=
|
||||
github.com/livekit/protocol v1.16.0/go.mod h1:pnn0Dv+/0K0OFqKHX6J6SreYO1dZxl6tDuAZ1ns8L/w=
|
||||
github.com/livekit/psrpc v0.5.3-0.20240228172457-3724cb4adbc4 h1:253WtQ2VGVHzIIzW9MUZj7vUDDILESU3zsEbiRdxYF0=
|
||||
github.com/livekit/psrpc v0.5.3-0.20240228172457-3724cb4adbc4/go.mod h1:CQUBSPfYYAaevg1TNCc6/aYsa8DJH4jSRFdCeSZk5u0=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/makiuchi-d/gozxing v0.1.1 h1:xxqijhoedi+/lZlhINteGbywIrewVdVv2wl9r5O9S1I=
|
||||
github.com/makiuchi-d/gozxing v0.1.1/go.mod h1:eRIHbOjX7QWxLIDJoQuMLhuXg9LAuw6znsUtRkNw9DU=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/nats-io/nats.go v1.31.0 h1:/WFBHEc/dOKBF6qf1TZhrdEfTmOZ5JzdJ+Y3m6Y/p7E=
|
||||
github.com/nats-io/nats.go v1.31.0/go.mod h1:di3Bm5MLsoB4Bx61CBTsxuarI36WbhAwOm8QrW39+i8=
|
||||
github.com/nats-io/nkeys v0.4.6 h1:IzVe95ru2CT6ta874rt9saQRkWfe2nFj1NtvYSLqMzY=
|
||||
github.com/nats-io/nkeys v0.4.6/go.mod h1:4DxZNzenSVd1cYQoAa8948QY3QDjrHfcfVADymtkpts=
|
||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
|
||||
github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU=
|
||||
github.com/onsi/ginkgo/v2 v2.1.6/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk=
|
||||
github.com/onsi/ginkgo/v2 v2.3.0/go.mod h1:Eew0uilEqZmIEZr8JrvYlvOM7Rr6xzTmMV8AyFNU9d0=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
|
||||
github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo=
|
||||
github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc=
|
||||
github.com/onsi/gomega v1.22.1 h1:pY8O4lBfsHKZHM/6nrxkhVPUznOlIu3quZcKP/M20KI=
|
||||
github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM=
|
||||
github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8=
|
||||
github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8=
|
||||
github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0=
|
||||
github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
|
||||
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||
github.com/pion/ice/v2 v2.3.13 h1:xOxP+4V9nSDlUaGFRf/LvAuGHDXRcjIdsbbXPK/w7c8=
|
||||
github.com/pion/ice/v2 v2.3.13/go.mod h1:KXJJcZK7E8WzrBEYnV4UtqEZsGeWfHxsNqhVcVvgjxw=
|
||||
github.com/pion/interceptor v0.1.25 h1:pwY9r7P6ToQ3+IF0bajN0xmk/fNw/suTgaTdlwTDmhc=
|
||||
github.com/pion/interceptor v0.1.25/go.mod h1:wkbPYAak5zKsfpVDYMtEfWEy8D4zL+rpxCxPImLOg3Y=
|
||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||
github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8=
|
||||
github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.12 h1:bKWiX93XKgDZENEXCijvHRU/wRifm6JV5DGcH6twtSM=
|
||||
github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||
github.com/pion/rtp v1.8.3 h1:VEHxqzSVQxCkKDSHro5/4IUUG1ea+MFdqR2R3xSpNU8=
|
||||
github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/sctp v1.8.12 h1:2VX50pedElH+is6FI+OKyRTeN5oy4mrk2HjnGa3UCmY=
|
||||
github.com/pion/sctp v1.8.12/go.mod h1:cMLT45jqw3+jiJCrtHVwfQLnfR0MGZ4rgOJwUOIqLkI=
|
||||
github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw=
|
||||
github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
|
||||
github.com/pion/srtp/v2 v2.0.18 h1:vKpAXfawO9RtTRKZJbG4y0v1b11NZxQnxRl85kGuUlo=
|
||||
github.com/pion/srtp/v2 v2.0.18/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA=
|
||||
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
|
||||
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
|
||||
github.com/pion/transport/v2 v2.2.3 h1:XcOE3/x41HOSKbl1BfyY1TF1dERx7lVvlMCbXU7kfvA=
|
||||
github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||
github.com/pion/turn/v2 v2.1.3 h1:pYxTVWG2gpC97opdRc5IGsQ1lJ9O/IlNhkzj7MMrGAA=
|
||||
github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
|
||||
github.com/pion/webrtc/v3 v3.2.28 h1:ienStxZ6HcjtH2UlmnFpMM0loENiYjaX437uIUpQSKo=
|
||||
github.com/pion/webrtc/v3 v3.2.28/go.mod h1:PNRCEuQlibrmuBhOTnol9j6KkIbUG11aHLEfNpUYey0=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
|
||||
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
|
||||
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
|
||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
|
||||
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.1.0 h1:EewKT7/LNac5SLiEblJeUu8z5eERHrmRLnMQL2d7qX4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.1.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8=
|
||||
github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/status-im/doubleratchet v3.0.0+incompatible h1:aJ1ejcSERpSzmWZBgtfYtiU2nF0Q8ZkGyuEPYETXkCY=
|
||||
github.com/status-im/doubleratchet v3.0.0+incompatible/go.mod h1:1sqR0+yhiM/bd+wrdX79AOt2csZuJOni0nUDzKNuqOU=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/twitchtv/twirp v8.1.3+incompatible h1:+F4TdErPgSUbMZMwp13Q/KgDVuI7HJXP61mNV3/7iuU=
|
||||
github.com/twitchtv/twirp v8.1.3+incompatible/go.mod h1:RRJoFSAmTEh2weEqWtpPE3vFK5YBhA6bqp2l1kfCC5A=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
||||
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
||||
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.uber.org/zap/exp v0.2.0 h1:FtGenNNeCATRB3CmB/yEUnjEFeJWpB/pMcy7e2bKPYs=
|
||||
go.uber.org/zap/exp v0.2.0/go.mod h1:t0gqAIdh1MfKv9EwN/dLwfZnJxe9ITAZN78HEWPFWDQ=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -103,80 +259,70 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos=
|
||||
golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c h1:NUsgEN92SQQqzfA+YtqYNqYmB3DMMYLlIwUZAQFVFbo=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY=
|
||||
google.golang.org/grpc v1.62.0 h1:HQKZ/fa1bXkX1oFOvSjmZEUL8wLSaZTjCcLAlmZRtdk=
|
||||
google.golang.org/grpc v1.62.0/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
@@ -185,18 +331,18 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package meowlib
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func HttpGetId(url string) (response map[string]string, err error) {
|
||||
srvId := make(map[string]string)
|
||||
resp, err := http.Get(url + "/id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = json.Unmarshal(body, &srvId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return srvId, nil
|
||||
}
|
||||
|
||||
func HttpPostMessage(url string, msg []byte, timeout int) (response []byte, err error) {
|
||||
client := http.Client{
|
||||
Timeout: time.Duration(timeout) * time.Second,
|
||||
}
|
||||
resp, err := client.Post(url+"/msg",
|
||||
"application/octet-stream", bytes.NewBuffer(msg))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package meowlib
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/makiuchi-d/gozxing"
|
||||
"github.com/makiuchi-d/gozxing/qrcode"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"image/png"
|
||||
)
|
||||
|
||||
func (p *InvitationInitPayload) Serialize() ([]byte, error) {
|
||||
return proto.Marshal(p)
|
||||
}
|
||||
|
||||
func (p *InvitationInitPayload) Compress() ([]byte, error) {
|
||||
out, err := p.Serialize()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var b bytes.Buffer
|
||||
gz, err := gzip.NewWriterLevel(&b, gzip.BestCompression)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := gz.Write(out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := gz.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
func NewInvitationInitPayloadFromCompressed(compressed []byte) (*InvitationInitPayload, error) {
|
||||
p := &InvitationInitPayload{}
|
||||
reader := bytes.NewReader(compressed)
|
||||
gzreader, err := gzip.NewReader(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
output, err := io.ReadAll(gzreader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := proto.Unmarshal(output, p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (p *InvitationInitPayload) WriteCompressed(filename string) error {
|
||||
out, err := p.Compress()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(filename, out, 0600)
|
||||
}
|
||||
|
||||
func (p *InvitationInitPayload) WriteQr(filename string) error {
|
||||
data, err := p.Compress()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
qwriter := qrcode.NewQRCodeWriter()
|
||||
code, err := qwriter.Encode(string(data), gozxing.BarcodeFormat_QR_CODE, 512, 512, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
return png.Encode(file, code)
|
||||
}
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
package meowlib
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type LokiWriter struct {
|
||||
url string
|
||||
labels map[string]string
|
||||
httpClient *http.Client
|
||||
disabled bool
|
||||
|
||||
// Circuit breaker fields
|
||||
mu sync.RWMutex
|
||||
failureCount int
|
||||
circuitOpen bool
|
||||
lastFailureTime time.Time
|
||||
lastWarningTime time.Time
|
||||
}
|
||||
|
||||
type LokiPayload struct {
|
||||
Streams []LokiStream `json:"streams"`
|
||||
}
|
||||
|
||||
type LokiStream struct {
|
||||
Stream map[string]string `json:"stream"`
|
||||
Values [][]string `json:"values"`
|
||||
}
|
||||
|
||||
const (
|
||||
// Circuit breaker configuration
|
||||
maxFailures = 3 // Open circuit after this many consecutive failures
|
||||
circuitOpenTime = 5 * time.Minute // How long to keep circuit open
|
||||
warningInterval = 1 * time.Minute // Minimum time between warning messages
|
||||
)
|
||||
|
||||
func NewLokiWriter(rawURL string, labels map[string]string) *LokiWriter {
|
||||
disabled := false
|
||||
if rawURL == "" {
|
||||
disabled = true
|
||||
} else {
|
||||
u, err := url.ParseRequestURI(rawURL)
|
||||
if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" {
|
||||
disabled = true
|
||||
}
|
||||
}
|
||||
return &LokiWriter{
|
||||
url: rawURL,
|
||||
labels: labels,
|
||||
httpClient: &http.Client{},
|
||||
disabled: disabled,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *LokiWriter) Write(p []byte) (n int, err error) {
|
||||
if w.disabled {
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// Check circuit breaker status
|
||||
w.mu.RLock()
|
||||
if w.circuitOpen {
|
||||
// Check if it's time to retry
|
||||
if time.Since(w.lastFailureTime) < circuitOpenTime {
|
||||
w.mu.RUnlock()
|
||||
// Circuit is open, silently discard log to avoid spam
|
||||
return len(p), nil
|
||||
}
|
||||
w.mu.RUnlock()
|
||||
// Time to retry - acquire write lock to close circuit
|
||||
w.mu.Lock()
|
||||
w.circuitOpen = false
|
||||
w.failureCount = 0
|
||||
w.mu.Unlock()
|
||||
} else {
|
||||
w.mu.RUnlock()
|
||||
}
|
||||
|
||||
// Use zerolog to parse the log level
|
||||
var event map[string]interface{}
|
||||
if err := json.Unmarshal(p, &event); err != nil {
|
||||
// Don't fail on unmarshal errors, just silently continue
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
level := ""
|
||||
if l, ok := event["level"].(string); ok {
|
||||
level = l
|
||||
}
|
||||
|
||||
message := ""
|
||||
if m, ok := event["message"].(string); ok {
|
||||
message = m
|
||||
}
|
||||
|
||||
// Add log level to labels
|
||||
labels := make(map[string]string)
|
||||
for k, v := range w.labels {
|
||||
labels[k] = v
|
||||
}
|
||||
labels["level"] = level
|
||||
|
||||
// Format the timestamp in nanoseconds
|
||||
timestamp := fmt.Sprintf("%d000000", time.Now().UnixNano()/int64(time.Millisecond))
|
||||
|
||||
stream := LokiStream{
|
||||
Stream: labels,
|
||||
Values: [][]string{
|
||||
{timestamp, message},
|
||||
},
|
||||
}
|
||||
|
||||
payload := LokiPayload{
|
||||
Streams: []LokiStream{stream},
|
||||
}
|
||||
|
||||
payloadBytes, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
// Don't fail on marshal errors
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", w.url, bytes.NewReader(payloadBytes))
|
||||
if err != nil {
|
||||
w.recordFailure(fmt.Sprintf("failed to create HTTP request: %v", err))
|
||||
return len(p), nil
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := w.httpClient.Do(req)
|
||||
if err != nil {
|
||||
w.recordFailure(fmt.Sprintf("failed to send log to Loki: %v", err))
|
||||
return len(p), nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
w.recordFailure(fmt.Sprintf("received non-204 response from Loki: %d", resp.StatusCode))
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// Success - reset failure count
|
||||
w.mu.Lock()
|
||||
if w.failureCount > 0 {
|
||||
// Circuit was previously failing but now recovered
|
||||
fmt.Printf("LokiWriter: connection restored to %s\n", w.url)
|
||||
w.failureCount = 0
|
||||
}
|
||||
w.mu.Unlock()
|
||||
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// recordFailure handles a Loki write failure and opens circuit if needed
|
||||
func (w *LokiWriter) recordFailure(errMsg string) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
w.failureCount++
|
||||
w.lastFailureTime = time.Now()
|
||||
|
||||
// Only print warnings if enough time has passed since last warning
|
||||
shouldWarn := time.Since(w.lastWarningTime) >= warningInterval
|
||||
|
||||
if w.failureCount >= maxFailures && !w.circuitOpen {
|
||||
w.circuitOpen = true
|
||||
if shouldWarn {
|
||||
fmt.Printf("LokiWriter: circuit breaker opened after %d failures (last error: %s). Remote logging disabled for %v.\n",
|
||||
w.failureCount, errMsg, circuitOpenTime)
|
||||
w.lastWarningTime = time.Now()
|
||||
}
|
||||
} else if !w.circuitOpen && shouldWarn {
|
||||
fmt.Printf("LokiWriter: warning - %s (failure %d/%d)\n", errMsg, w.failureCount, maxFailures)
|
||||
w.lastWarningTime = time.Now()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
package meowlib
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewLokiWriterDisabledOnEmptyURL(t *testing.T) {
|
||||
w := NewLokiWriter("", map[string]string{"app": "test"})
|
||||
assert.True(t, w.disabled)
|
||||
}
|
||||
|
||||
func TestNewLokiWriterDisabledOnInvalidURL(t *testing.T) {
|
||||
cases := []string{
|
||||
"not-a-url",
|
||||
"ftp://example.com/loki",
|
||||
"://missing-scheme",
|
||||
"http://",
|
||||
"justtext",
|
||||
}
|
||||
for _, u := range cases {
|
||||
w := NewLokiWriter(u, map[string]string{"app": "test"})
|
||||
assert.True(t, w.disabled, "expected disabled for URL: %s", u)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewLokiWriterEnabledOnValidURL(t *testing.T) {
|
||||
cases := []string{
|
||||
"http://localhost:3100/loki/api/v1/push",
|
||||
"https://log.redroom.link/loki/api/v1/push",
|
||||
}
|
||||
for _, u := range cases {
|
||||
w := NewLokiWriter(u, map[string]string{"app": "test"})
|
||||
assert.False(t, w.disabled, "expected enabled for URL: %s", u)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteDisabledReturnsLength(t *testing.T) {
|
||||
w := NewLokiWriter("", map[string]string{"app": "test"})
|
||||
msg := []byte(`{"level":"info","message":"should be discarded"}`)
|
||||
n, err := w.Write(msg)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, len(msg), n)
|
||||
}
|
||||
|
||||
func TestWriteToMockLoki(t *testing.T) {
|
||||
var received LokiPayload
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
|
||||
err := json.NewDecoder(r.Body).Decode(&received)
|
||||
assert.NoError(t, err)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
labels := map[string]string{"app": "meowlib", "env": "test"}
|
||||
w := NewLokiWriter(server.URL, labels)
|
||||
|
||||
msg := []byte(`{"level":"info","message":"hello from test"}`)
|
||||
n, err := w.Write(msg)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, len(msg), n)
|
||||
|
||||
// Verify payload structure
|
||||
assert.Len(t, received.Streams, 1)
|
||||
assert.Equal(t, "meowlib", received.Streams[0].Stream["app"])
|
||||
assert.Equal(t, "test", received.Streams[0].Stream["env"])
|
||||
assert.Equal(t, "info", received.Streams[0].Stream["level"])
|
||||
assert.Len(t, received.Streams[0].Values, 1)
|
||||
assert.Equal(t, "hello from test", received.Streams[0].Values[0][1])
|
||||
}
|
||||
|
||||
func TestWriteInvalidJSON(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("server should not be called for invalid JSON input")
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
w := NewLokiWriter(server.URL, map[string]string{"app": "test"})
|
||||
msg := []byte(`not json at all`)
|
||||
n, err := w.Write(msg)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, len(msg), n)
|
||||
}
|
||||
|
||||
func TestCircuitBreakerOpensAfterFailures(t *testing.T) {
|
||||
callCount := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
callCount++
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
w := NewLokiWriter(server.URL, map[string]string{"app": "test"})
|
||||
msg := []byte(`{"level":"error","message":"fail test"}`)
|
||||
|
||||
// Send maxFailures requests to trip the circuit breaker
|
||||
for i := 0; i < maxFailures; i++ {
|
||||
n, err := w.Write(msg)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, len(msg), n)
|
||||
}
|
||||
assert.True(t, w.circuitOpen)
|
||||
|
||||
// Next write should be silently discarded (no server call)
|
||||
prevCount := callCount
|
||||
n, err := w.Write(msg)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, len(msg), n)
|
||||
assert.Equal(t, prevCount, callCount, "no HTTP call should be made while circuit is open")
|
||||
}
|
||||
|
||||
func TestCircuitBreakerResetsAfterSuccess(t *testing.T) {
|
||||
failFirst := true
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if failFirst {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
w := NewLokiWriter(server.URL, map[string]string{"app": "test"})
|
||||
msg := []byte(`{"level":"info","message":"recovery test"}`)
|
||||
|
||||
// Cause some failures (but not enough to open circuit)
|
||||
w.Write(msg)
|
||||
w.Write(msg)
|
||||
|
||||
assert.False(t, w.circuitOpen)
|
||||
assert.Equal(t, 2, w.failureCount)
|
||||
|
||||
// Now succeed
|
||||
failFirst = false
|
||||
w.Write(msg)
|
||||
assert.Equal(t, 0, w.failureCount)
|
||||
}
|
||||
|
||||
func TestCircuitBreakerRetriesAfterTimeout(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
w := NewLokiWriter(server.URL, map[string]string{"app": "test"})
|
||||
|
||||
// Manually open the circuit with an old failure time
|
||||
w.mu.Lock()
|
||||
w.circuitOpen = true
|
||||
w.failureCount = maxFailures
|
||||
w.lastFailureTime = time.Now().Add(-circuitOpenTime - time.Second)
|
||||
w.mu.Unlock()
|
||||
|
||||
msg := []byte(`{"level":"info","message":"retry test"}`)
|
||||
n, err := w.Write(msg)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, len(msg), n)
|
||||
|
||||
// Circuit should now be closed after successful retry
|
||||
assert.False(t, w.circuitOpen)
|
||||
assert.Equal(t, 0, w.failureCount)
|
||||
}
|
||||
|
||||
func TestWriteToRealLoki(t *testing.T) {
|
||||
lokiURL := "https://log.redroom.link/loki/api/v1/push"
|
||||
|
||||
// Quick connectivity check
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
resp, err := client.Get("https://log.redroom.link/ready")
|
||||
if err != nil || resp.StatusCode != http.StatusOK {
|
||||
t.Skip("Loki not reachable, skipping live test")
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
labels := map[string]string{"app": "meowlib", "env": "test"}
|
||||
w := NewLokiWriter(lokiURL, labels)
|
||||
assert.False(t, w.disabled)
|
||||
|
||||
msg := fmt.Sprintf(`{"level":"info","message":"lokiwriter_test at %s"}`, time.Now().Format(time.RFC3339))
|
||||
n, err := w.Write([]byte(msg))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, len(msg), n)
|
||||
assert.Equal(t, 0, w.failureCount)
|
||||
}
|
||||
+3
-2
@@ -3,6 +3,7 @@ package meowlib
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
@@ -30,13 +31,13 @@ func (msg *UserMessage) AddFile(filename string, maxMessageSize int64) error {
|
||||
}
|
||||
|
||||
var file File
|
||||
file.Filename = filename
|
||||
file.Filename = filepath.Base(filename)
|
||||
file.Size = uint64(fi.Size())
|
||||
file.Data = data
|
||||
msg.Files = append(msg.Files, &file)
|
||||
|
||||
msg.Status = &ConversationStatus{}
|
||||
msg.Status.LocalUuid = uuid.New().String()
|
||||
msg.Status.Uuid = uuid.New().String()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+1072
-703
File diff suppressed because it is too large
Load Diff
+121
-49
@@ -22,25 +22,45 @@ message PackedServerMessage {
|
||||
|
||||
// structure to hold an invitation through a server
|
||||
message Invitation {
|
||||
bytes payload = 1; // invitation payload, encrypted after step 2
|
||||
bytes payload = 1; // invitation payload, optionaly encrypted with payload password(transmitted OOB) on step 1
|
||||
int32 timeout = 2; // how long do I want the invitation to remain available on the server
|
||||
int32 shortcodeLen = 3; // len of the shortcode you wish for short url transmission
|
||||
int32 shortcode_len = 3; // len of the shortcode you wish for short url transmission
|
||||
string shortcode = 4; // shortcode that the friend shall request to get the invitation
|
||||
string password = 5; // password tou set for accessin invitation (optional)
|
||||
string uuid = 6; // id that the friend gave you, that you should include to your reply to get recognized
|
||||
int64 expiry = 7; // the server allowed expiry date, it may be samller than the requested timeout according to server policy
|
||||
int32 step = 8; // progress in the inviattion process : 1=invite friend, 2=friend requests invitation, 3=friend's answer
|
||||
string password = 5; // optional password(transmitted OOB) to set for accessing invitation (server check)
|
||||
string uuid = 6; // invitation uuid
|
||||
int64 expiry = 7; // the server allowed expiry date, it may be smaller than the requested timeout according to server policy
|
||||
int32 step = 8; // progress in the invitation process : 1=initiator pub key, 2=invited data enc with pub key, 3=initator data full encrypted, 4=invited All OK !
|
||||
string from = 9; // still useful ?
|
||||
}
|
||||
|
||||
// This payload migh be used for file serialization as well as Invitation/payload in case of through server invitation
|
||||
message InvitationInitPayload {
|
||||
string uuid = 1; // uuid of the invitation, it is set here on init cause the payload might be encrypted
|
||||
string name = 2; // name of the initiator
|
||||
string public_key = 3; // public key to be used for step 2 encryption
|
||||
string invitation_message = 4; // message for the invited peer
|
||||
}
|
||||
|
||||
// structure for requesting incoming messages
|
||||
message ConversationRequest {
|
||||
string lookupKey = 1; // lookup key for a conversation
|
||||
string lastServerUuidOK = 2; // Last Server message UUID received (send me all after that one)
|
||||
bool publishOnline = 3; // ?? Publish my online status for that contact ?
|
||||
string lookupSignature = 4; // prove that I own the private key by signing that block
|
||||
string lookup_key = 1; // lookup key for a conversation
|
||||
bool delivery_request = 2; // look for for delivery tracking, key is implicit, "from" field is used
|
||||
int64 send_timestamp = 3;
|
||||
string lookup_signature = 4; // prove that I own the private key by signing that block
|
||||
}
|
||||
|
||||
message Meet {
|
||||
string public_status = 1; // Publish my online status, if the server is a meeting server
|
||||
ContactCard contact_card = 2; // mine or the requester
|
||||
string message = 3; // short description
|
||||
}
|
||||
|
||||
message Credentials {
|
||||
string login = 1; // login
|
||||
string password = 2; // password
|
||||
string public_key = 3; // public key
|
||||
string private_key = 4; // private key
|
||||
}
|
||||
|
||||
// structure defining a message for a server, that will be encrypted, then sent in a "packedmessage" payload
|
||||
message ToServerMessage {
|
||||
@@ -48,49 +68,57 @@ message ToServerMessage {
|
||||
string from = 2 ; // My pub key for the server to send me an encrypter answer
|
||||
bytes payload = 3 ; // optional payload for server
|
||||
|
||||
repeated ConversationRequest pullRequest = 4;
|
||||
repeated ConversationRequest pull_request = 4;
|
||||
|
||||
repeated PackedUserMessage messages = 5;
|
||||
|
||||
repeated ServerCard knownServers = 6;
|
||||
repeated ServerCard known_servers = 6;
|
||||
|
||||
Matriochka matriochkaMessage = 7;
|
||||
Matriochka matriochka_message = 7;
|
||||
|
||||
string uuid = 8;
|
||||
|
||||
Invitation invitation = 9; // invitation for the 2 first steps of a "through server" invitation process
|
||||
|
||||
}
|
||||
repeated PackedUserMessage device_messages = 10; // messages to another device belonging to the same user
|
||||
|
||||
message ConversationResponse {
|
||||
repeated string messageUuids = 1;
|
||||
int64 timeout = 11; // timeout expected by the client for the server to answer (long polling)
|
||||
|
||||
VideoData video_data = 12; // video call data
|
||||
|
||||
Credentials credentials = 13; // credentials for a new user or mandatory server creds
|
||||
}
|
||||
|
||||
// structure defining a from server receiver message decrypted from a "packedmessage" payload
|
||||
message FromServerMessage {
|
||||
string type = 1; // Type
|
||||
string serverPublicKey = 2 ; // Pub key from the server
|
||||
string server_public_key = 2 ; // Pub key from the server
|
||||
bytes payload = 3 ; //
|
||||
string uuidAck = 4 ; // Ack for the last received ToServerMessage Uuid
|
||||
string serverUuid = 5 ; // Provides the server uuid that replaced the client uuid
|
||||
string uuid_ack = 4 ; // Ack for the last received ToServerMessage Uuid
|
||||
string server_uuid = 5 ; // Provides the server uuid that replaced the client uuid
|
||||
|
||||
repeated PackedUserMessage chat = 6;
|
||||
|
||||
repeated ServerCard knownServers = 7;
|
||||
repeated ServerCard known_servers = 7;
|
||||
|
||||
Invitation invitation = 8; // invitation answer, for the third steps of any invitation
|
||||
|
||||
repeated PackedUserMessage device_messages = 9; // messages from other devices belonging to the same user
|
||||
|
||||
VideoData video_data = 10; // video call data
|
||||
|
||||
repeated ContactCard contact_card = 11; // contact list for a personae
|
||||
}
|
||||
|
||||
message MatriochkaServer {
|
||||
string url = 1; // Server Url
|
||||
string publicKey = 2; // Server Public Key
|
||||
string public_key = 2; // Server Public Key
|
||||
string uuid = 3 ; // Optional, uuid for delivery confirmation
|
||||
int32 delay = 4; // Max delay requested for message forwarding or delivery tracking
|
||||
}
|
||||
|
||||
message Matriochka {
|
||||
string lookupKey = 1; // Optional, only if you want delivery tracking, less stealth
|
||||
string lookup_key = 1; // Optional, only if you want delivery tracking, less stealth
|
||||
MatriochkaServer prev = 2; // Optional, like above
|
||||
MatriochkaServer next = 3; // Next server to deliver the message to
|
||||
bytes data = 4; // Matriochka data
|
||||
@@ -100,7 +128,7 @@ message Matriochka {
|
||||
message ServerCard {
|
||||
string name = 1; // friendly server name
|
||||
string description = 2; // description : owner type (company/private/university...),
|
||||
string publicKey = 3; // public key you must use to send encrypted messages to that server
|
||||
string public_key = 3; // public key you must use to send encrypted messages to that server
|
||||
string url = 4; // meow server url
|
||||
string login = 5; // required login to access the server
|
||||
string password = 6; // password associated to the login
|
||||
@@ -110,12 +138,16 @@ message ServerCard {
|
||||
// structure describing a user contact card ie the minimum set of attributes for exchanging identities
|
||||
message ContactCard {
|
||||
string name = 1; // contact nickname
|
||||
string contactPublicKey =2; // contact public key, will be used to authenticate her/his messages
|
||||
string encryptionPublicKey= 3; // public key you must use to to write encrypted messages to that contact
|
||||
string lookupPublicKey =4; // public key you will use as "destination identifier" for her/him to lookup for your messages on the servers
|
||||
repeated ServerCard pullServers =5; // list the servers where the contact will look for messages from you
|
||||
uint32 version = 6;
|
||||
string invitationId=7;
|
||||
string contact_public_key = 2; // contact public key, will be used to authenticate her/his messages
|
||||
string encryption_public_key = 3; // public key you must use to to write encrypted messages to that contact
|
||||
string lookup_public_key = 4; // public key you will use as "destination identifier" for her/him to lookup for your messages on the servers
|
||||
string symetric_key = 5; // agreed key for payload symetric encryption
|
||||
repeated ServerCard pull_servers = 6; // list the servers where the contact will look for messages from you
|
||||
uint32 version = 7;
|
||||
string invitation_id = 8;
|
||||
string invitation_message = 9;
|
||||
string dr_root_key = 10; // DR pre-shared root key (base64, 32 bytes)
|
||||
string dr_public_key = 11; // DR DH public key of the initiator (base64)
|
||||
}
|
||||
|
||||
// structure for sending a message to be forwarded to another user in protobuf format
|
||||
@@ -123,17 +155,27 @@ message PackedUserMessage {
|
||||
string destination = 1; // the peer's current conversation lookup public key
|
||||
bytes payload = 2; // the message UserMessage encrypted with the destination peer's public key
|
||||
bytes signature = 3; // the payload signature with the client identity private key
|
||||
repeated int64 serverTimestamp=4; // server time stamp, might be several in matriochka mode
|
||||
repeated int64 server_timestamp = 4; // server time stamp, might be several in matriochka mode
|
||||
string server_delivery_uuid = 5; // message uuid, for server delivery tracking, omitted if not delivery tracking desired
|
||||
bytes dr_header = 6; // serialized doubleratchet MessageHeader; empty = no DR layer
|
||||
}
|
||||
|
||||
message ConversationStatus {
|
||||
string localUuid = 1;
|
||||
uint64 localSequence = 2 ;
|
||||
uint64 sent = 3 ;
|
||||
uint64 received = 4;
|
||||
uint64 processed = 5;
|
||||
ContactCard myNextIdentity = 6;
|
||||
int32 peerNextIdentityAck = 7; // version of the new peer accepted id
|
||||
string uuid = 1; // uuid of message, or uuid of related message if uuid_action is not empty
|
||||
repeated Reaction reactions = 2; // reaction to the message per peer
|
||||
string reply_to_uuid = 3; // this message replies to the specified uuid
|
||||
uint64 local_sequence = 4 ; // seq number in local conversation for custom reordering
|
||||
uint64 sent = 5 ; // timestamp of the message sent
|
||||
uint64 received = 6; // timestamp of the message received
|
||||
uint64 processed = 7; // timestamp of the message processed
|
||||
ContactCard my_next_identity = 8;
|
||||
int32 peer_next_identity_ack = 9; // version of the new peer accepted id
|
||||
|
||||
}
|
||||
|
||||
message Reaction {
|
||||
string reaction = 1;
|
||||
string contact_uuid = 2;
|
||||
}
|
||||
|
||||
message Group{
|
||||
@@ -145,26 +187,26 @@ message Group{
|
||||
message UserMessage {
|
||||
string destination = 1; // Lookupkey
|
||||
string from = 2; // My public key for that contact
|
||||
string type = 3;
|
||||
string type = 3; // Message type
|
||||
bytes data = 4;
|
||||
|
||||
ConversationStatus Status = 5;
|
||||
|
||||
ConversationStatus status = 5;
|
||||
ContactCard contact = 6;
|
||||
|
||||
ServerCard knownServers = 7;
|
||||
|
||||
ServerCard known_servers = 7;
|
||||
Group group = 8;
|
||||
|
||||
repeated File files = 9;
|
||||
|
||||
Location currentLocation = 10;
|
||||
|
||||
Location current_location = 10;
|
||||
bytes appdata = 11;
|
||||
|
||||
Invitation invitation = 12;
|
||||
VideoData video_data = 13;
|
||||
}
|
||||
|
||||
// UserMessage types :
|
||||
// 1 : normal message (test, image, video, audio, file, etc)
|
||||
// 2 : status message (online, offline, busy, etc)
|
||||
// 3 : send avatar as file[0]
|
||||
// 4 : location request
|
||||
// 5 : location response
|
||||
|
||||
message File {
|
||||
string filename = 1; // the proposed filename
|
||||
uint64 size = 2; // the file size
|
||||
@@ -178,3 +220,33 @@ message Location {
|
||||
float longitude = 3;
|
||||
int32 altitude = 4;
|
||||
}
|
||||
|
||||
message DbMessage {
|
||||
bool outbound = 1; // direction of the message
|
||||
string type = 2;
|
||||
bytes data = 3; // text data
|
||||
ConversationStatus status = 4;
|
||||
ContactCard contact = 5;
|
||||
Group group = 6;
|
||||
repeated string file_paths = 7;
|
||||
Location current_location = 8;
|
||||
bytes appdata = 9;
|
||||
Invitation invitation = 10;
|
||||
string from = 11; // source peer uid, used when storing group conversations with more than one peer
|
||||
string server_delivery_uuid = 12; // uuid returned by the server upon delivery
|
||||
uint64 server_delivery_timestamp = 13; // timestamp of the server delivery
|
||||
}
|
||||
|
||||
message VideoData {
|
||||
string url = 1;
|
||||
string room = 2;
|
||||
uint64 duration = 3;
|
||||
repeated VideoCredential credentials = 4;
|
||||
repeated string media_query = 5;
|
||||
}
|
||||
|
||||
message VideoCredential {
|
||||
string username = 1;
|
||||
string shared_key = 2;
|
||||
string token = 3;
|
||||
}
|
||||
+3
-2
@@ -1,12 +1,13 @@
|
||||
#!/bin/bash
|
||||
echo Generating Golang
|
||||
protoc -I=. --go_out=.. messages.proto
|
||||
mv ../forge.redroom.link/yves/meowlib/messages.pb.go ../
|
||||
rm -rf ../forge.redroom.link
|
||||
|
||||
echo Generating HTML doc
|
||||
protoc --plugin=protoc-gen-doc=/usr/bin/protoc-gen-doc \
|
||||
--doc_out=../doc/generated \
|
||||
--doc_opt=html,index.html \
|
||||
*.proto
|
||||
|
||||
echo Generating UML
|
||||
protoc --plugin=protoc-gen-uml=/usr/bin/protoc-gen-uml \
|
||||
--uml_out=../doc/generated -I=. *.proto
|
||||
|
||||
+8
-3
@@ -16,19 +16,24 @@ const key = "3pw0c8#6ZG8{75b5;3?fe80$2"
|
||||
type Identity struct {
|
||||
ServerName string `json:"servername,omitempty"`
|
||||
ServerDesc string `json:"serverdesc,omitempty"`
|
||||
ServerKp meowlib.KeyPair `json:"server_kp,omitempty"`
|
||||
ServerKp *meowlib.KeyPair `json:"server_kp,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
OwnerName string `json:"owner_name,omitempty"`
|
||||
OwnerPublicKey string `json:"owner_public_key,omitempty"`
|
||||
ArchiveClients []string `json:"archive_clients,omitempty"`
|
||||
KnownServers []meowlib.ServerCard `json:"known_servers,omitempty"`
|
||||
VideoServer VideoServer `json:"video_server,omitempty"`
|
||||
}
|
||||
|
||||
func CreateIdentity(ServerName string, ServerDesc string) *Identity {
|
||||
var id Identity
|
||||
var err error
|
||||
id.ServerName = ServerName
|
||||
id.ServerDesc = ServerDesc
|
||||
id.ServerKp = meowlib.NewKeyPair()
|
||||
id.ServerKp, err = meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return &id
|
||||
}
|
||||
|
||||
@@ -52,7 +57,7 @@ func (id *Identity) Save(file string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.WriteFile(file, []byte(armor), 0644)
|
||||
err = os.WriteFile(file, []byte(armor), 0600)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
+83
-17
@@ -1,26 +1,53 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"errors"
|
||||
"math/rand"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis"
|
||||
)
|
||||
|
||||
func (r *RedisRouter) CreateInvitation(invitation []byte, timeout int, password string, serverTimeout int, urlLen int) (string, time.Time) {
|
||||
id := r.createShortId(urlLen)
|
||||
const MaxShortcodeLength = 64
|
||||
|
||||
func (r *RedisRouter) StoreInvitation(invitation []byte, timeout int, password string, serverTimeout int, urlLen int) (string, time.Time, error) {
|
||||
id, err := r.createShortId(urlLen)
|
||||
if err != nil {
|
||||
return "", time.Time{}, fmt.Errorf("failed to create invitation ID: %w", err)
|
||||
}
|
||||
if timeout > serverTimeout {
|
||||
timeout = serverTimeout
|
||||
}
|
||||
r.Client.Set("mwiv:"+id, invitation, time.Duration(timeout*1000000))
|
||||
r.Client.Set("mwiv:"+id, invitation, 0) //, time.Duration(timeout*1000000))
|
||||
if len(password) > 0 {
|
||||
r.Client.Set("mwpw:"+id, password, time.Duration(timeout*1000000))
|
||||
r.Client.Set("mwpw:"+id, password, 0) //, time.Duration(timeout*1000000))
|
||||
}
|
||||
return id, time.Now().Add(time.Duration(timeout * 1000000)).UTC()
|
||||
return id, time.Now().Add(time.Duration(timeout * 1000000)).UTC(), nil
|
||||
}
|
||||
|
||||
func (r *RedisRouter) GetInvitation(id string, password string) ([]byte, error) {
|
||||
// Check failed attempts counter
|
||||
failedAttemptsKey := "mwfa:" + id
|
||||
failedAttempts := 0 // Default when key doesn't exist
|
||||
val, err := r.Client.Get(failedAttemptsKey).Int()
|
||||
if err == nil {
|
||||
failedAttempts = val
|
||||
} else if err != redis.Nil {
|
||||
return nil, fmt.Errorf("failed to check attempts: %w", err)
|
||||
}
|
||||
// If err == redis.Nil, key doesn't exist, so failedAttempts stays 0
|
||||
|
||||
// If already hit the limit, delete invitation and fail
|
||||
if failedAttempts >= 3 {
|
||||
r.deleteInvitation(id)
|
||||
return nil, errors.New("invitation locked due to too many failed attempts")
|
||||
}
|
||||
|
||||
// Check if password is required
|
||||
passRequired := false
|
||||
expectedpass, err := r.Client.Get("mwpw:" + id).Result()
|
||||
if err != nil {
|
||||
@@ -28,17 +55,44 @@ func (r *RedisRouter) GetInvitation(id string, password string) ([]byte, error)
|
||||
} else {
|
||||
passRequired = true
|
||||
}
|
||||
if passRequired && password != expectedpass {
|
||||
|
||||
// Validate password with constant-time comparison
|
||||
if passRequired {
|
||||
if subtle.ConstantTimeCompare([]byte(password), []byte(expectedpass)) != 1 {
|
||||
// Increment failed attempts
|
||||
newCount := failedAttempts + 1
|
||||
r.Client.Set(failedAttemptsKey, newCount, 0)
|
||||
|
||||
// If this was the 3rd attempt, delete invitation
|
||||
if newCount >= 3 {
|
||||
r.deleteInvitation(id)
|
||||
return nil, errors.New("auth failed - invitation destroyed after 3 attempts")
|
||||
}
|
||||
return nil, errors.New("auth failed")
|
||||
}
|
||||
}
|
||||
|
||||
// Success - get invitation data
|
||||
mwiv, err := r.Client.Get("mwiv:" + id).Result()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Clear failed attempts counter on successful access
|
||||
r.Client.Del(failedAttemptsKey)
|
||||
|
||||
return []byte(mwiv), nil
|
||||
}
|
||||
|
||||
func (r *RedisRouter) AnswerInvitation(id string, timeout int, invitation []byte, serverTimeout int) time.Time {
|
||||
// deleteInvitation removes all invitation-related keys from Redis
|
||||
func (r *RedisRouter) deleteInvitation(id string) {
|
||||
r.Client.Del("mwiv:" + id) // invitation data
|
||||
r.Client.Del("mwpw:" + id) // password
|
||||
r.Client.Del("mwfa:" + id) // failed attempts
|
||||
r.Client.Del("mwan:" + id) // answer to invitation
|
||||
}
|
||||
|
||||
func (r *RedisRouter) StoreAnswerToInvitation(id string, timeout int, invitation []byte, serverTimeout int) time.Time {
|
||||
if timeout > serverTimeout {
|
||||
timeout = serverTimeout
|
||||
}
|
||||
@@ -46,25 +100,37 @@ func (r *RedisRouter) AnswerInvitation(id string, timeout int, invitation []byte
|
||||
return time.Now().Add(time.Duration(timeout * 1000000)).UTC()
|
||||
}
|
||||
|
||||
func (r *RedisRouter) GetInvitationAnswer(id string) ([]byte, error) {
|
||||
mwan, err := r.Client.Get("mwiv:" + id).Result()
|
||||
func (r *RedisRouter) GetAnswerToInvitation(id string) ([]byte, error) {
|
||||
mwan, err := r.Client.Get("mwan:" + id).Result()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []byte(mwan), nil
|
||||
}
|
||||
|
||||
func (r *RedisRouter) createShortId(length int) string {
|
||||
id := ""
|
||||
func (r *RedisRouter) createShortId(length int) (string, error) {
|
||||
alphabet := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
alphabetLen := big.NewInt(int64(len(alphabet)))
|
||||
|
||||
if length < 1 || length > MaxShortcodeLength {
|
||||
return "", fmt.Errorf("invalid shortcode length: %d (must be 1-%d)", length, MaxShortcodeLength)
|
||||
}
|
||||
// for not in redis
|
||||
for {
|
||||
for i := 1; i <= length; i++ {
|
||||
id += string(alphabet[rand.Intn(61)])
|
||||
var id strings.Builder
|
||||
id.Grow(length)
|
||||
|
||||
for i := 0; i < length; i++ {
|
||||
n, err := rand.Int(rand.Reader, alphabetLen)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("random generation failed: %w", err)
|
||||
}
|
||||
if r.Client.Get("mwiv:"+id).Err() == redis.Nil {
|
||||
break
|
||||
id.WriteByte(alphabet[n.Int64()])
|
||||
}
|
||||
|
||||
idStr := id.String()
|
||||
if r.Client.Get("mwiv:"+idStr).Err() == redis.Nil {
|
||||
return idStr, nil
|
||||
}
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
var logger zerolog.Logger
|
||||
|
||||
// AddLogger sets the logger for the sublibrary
|
||||
func AddLogger(l zerolog.Logger) {
|
||||
logger = l
|
||||
}
|
||||
+257
-78
@@ -2,6 +2,7 @@ package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"forge.redroom.link/yves/meowlib"
|
||||
@@ -37,78 +38,51 @@ func NewRedisRouter(server *Identity, redisUrl string, password string, db int,
|
||||
}
|
||||
|
||||
func (r *RedisRouter) Route(msg *meowlib.ToServerMessage) (*meowlib.FromServerMessage, error) {
|
||||
var from_server meowlib.FromServerMessage
|
||||
var from_server *meowlib.FromServerMessage
|
||||
// update messages counter
|
||||
err := r.Client.Incr("statistics:messages:total").Err()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// user message
|
||||
// user message => store
|
||||
if len(msg.Messages) > 0 {
|
||||
// update messages counter
|
||||
err := r.Client.Incr("statistics:messages:usermessages").Err()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for _, usrmsg := range msg.Messages {
|
||||
// serialize the message to store it as byte array into redis
|
||||
out, err := proto.Marshal(usrmsg)
|
||||
logger.Info().Msg("storing message")
|
||||
from_server, err = r.storeMessage(msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Client.ZAdd(usrmsg.Destination, redis.Z{Score: float64(time.Now().Unix()), Member: out})
|
||||
}
|
||||
from_server.UuidAck = msg.Uuid
|
||||
}
|
||||
// check for messages
|
||||
if len(msg.PullRequest) > 0 {
|
||||
// update messages counter
|
||||
err := r.Client.Incr("statistics:messages:messagelookups").Err()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for _, rq := range msg.PullRequest {
|
||||
msgcnt, err := r.Client.ZCount(rq.LookupKey, "-inf", "+inf").Result()
|
||||
logger.Info().Msg("checking for messages")
|
||||
from_server, err = r.checkForMessage(msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := r.Client.ZPopMin(rq.LookupKey, msgcnt).Result()
|
||||
if msg.Timeout > 0 {
|
||||
logger.Info().Msg("long poll, subscribing for messages")
|
||||
// set timeout for the lookup
|
||||
from_server, err = r.subscribe(msg, int(msg.Timeout))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, redismsg := range res {
|
||||
//println(redismsg.Score)
|
||||
val := redismsg.Member
|
||||
test := val.(string)
|
||||
var usrmsg meowlib.PackedUserMessage
|
||||
err := proto.Unmarshal([]byte(test), &usrmsg)
|
||||
}
|
||||
}
|
||||
// initiate video
|
||||
if msg.VideoData != nil {
|
||||
logger.Info().Msg("handling video")
|
||||
from_server, err = r.handleVideo(msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// add server timestamp
|
||||
usrmsg.ServerTimestamp = append(usrmsg.ServerTimestamp, int64(redismsg.Score))
|
||||
|
||||
from_server.Chat = append(from_server.Chat, &usrmsg)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
// manage Matriochka
|
||||
if msg.MatriochkaMessage != nil {
|
||||
// update messages counter
|
||||
err := r.Client.Incr("statistics:messages:matriochka").Err()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
out, err := proto.Marshal(msg)
|
||||
logger.Info().Msg("handling matriochka")
|
||||
from_server, err = r.handleMatriochka(msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Client.ZAdd("mtk", redis.Z{Score: float64(time.Now().Unix()), Member: out})
|
||||
if msg.MatriochkaMessage.LookupKey != "" {
|
||||
//r.Client.ZAdd("trk:" + msg.MatriochkaMessage.Next.Uuid,{})
|
||||
}
|
||||
from_server.UuidAck = msg.Uuid
|
||||
}
|
||||
// Server list exchange
|
||||
if len(msg.KnownServers) > 0 {
|
||||
@@ -116,40 +90,10 @@ func (r *RedisRouter) Route(msg *meowlib.ToServerMessage) (*meowlib.FromServerMe
|
||||
}
|
||||
// Through server invitation process
|
||||
if msg.Invitation != nil {
|
||||
// update messages counter
|
||||
err := r.Client.Incr("statistics:messages:invitation").Err()
|
||||
logger.Info().Msg("handling invitation")
|
||||
from_server, err = r.handleInvitation(msg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
switch msg.Invitation.Step {
|
||||
case 1: // create invitation
|
||||
url, expiry := r.CreateInvitation(msg.Invitation.Payload, int(msg.Invitation.Timeout), msg.Invitation.Password, r.InvitationTimeout, int(msg.Invitation.ShortcodeLen))
|
||||
from_server.Invitation.Shortcode = url
|
||||
from_server.Invitation.Expiry = expiry.UTC().Unix()
|
||||
case 2: // get invitation
|
||||
invitation, err := r.GetInvitation(msg.Invitation.Shortcode, msg.Invitation.Password)
|
||||
if err != nil {
|
||||
if err.Error() == "auth failed" {
|
||||
from_server.Invitation.Payload = []byte("authentication failure")
|
||||
} else {
|
||||
from_server.Invitation.Payload = []byte("invitation expired")
|
||||
}
|
||||
} else {
|
||||
from_server.Invitation.Payload = invitation
|
||||
}
|
||||
|
||||
/* should not happen
|
||||
case 3: // answer invitation
|
||||
expiry := r.AnswerInvitation(msg.Invitation.Id, int(msg.Invitation.Timeout), msg.Invitation.Payload, r.InvitationTimeout)
|
||||
from_server.Invitation.Expiry = expiry.UTC().Unix()
|
||||
case 4: // get answer
|
||||
answer, err := r.GetInvitationAnswer(msg.Invitation.Id)
|
||||
if err != nil {
|
||||
from_server.Invitation.Payload = []byte("invitation expired")
|
||||
} else {
|
||||
from_server.Invitation.Payload = answer
|
||||
}
|
||||
*/
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
/*
|
||||
@@ -165,5 +109,240 @@ func (r *RedisRouter) Route(msg *meowlib.ToServerMessage) (*meowlib.FromServerMe
|
||||
break
|
||||
}
|
||||
*/
|
||||
return from_server, nil
|
||||
}
|
||||
|
||||
func (r *RedisRouter) storeMessage(msg *meowlib.ToServerMessage) (*meowlib.FromServerMessage, error) {
|
||||
var from_server meowlib.FromServerMessage
|
||||
// update messages counter
|
||||
err := r.Client.Incr("statistics:messages:usermessages").Err()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, usrmsg := range msg.Messages {
|
||||
// serialize the message to store it as byte array into redis
|
||||
out, err := proto.Marshal(usrmsg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Client.ZAdd("msg:"+usrmsg.Destination, redis.Z{Score: float64(time.Now().Unix()), Member: out})
|
||||
r.Client.Publish("msgch:"+usrmsg.Destination, "!")
|
||||
// if delivery tracking resquested, store the uid for the sender's key in delivery tracking
|
||||
if usrmsg.ServerDeliveryUuid != "" {
|
||||
r.Client.SAdd("dvyrq:"+usrmsg.ServerDeliveryUuid, redis.Z{Score: float64(time.Now().Unix()), Member: msg.From}) // TODO : this probably fails !
|
||||
}
|
||||
|
||||
}
|
||||
from_server.UuidAck = msg.Uuid
|
||||
return &from_server, nil
|
||||
}
|
||||
|
||||
func (r *RedisRouter) checkForMessage(msg *meowlib.ToServerMessage) (*meowlib.FromServerMessage, error) {
|
||||
var from_server meowlib.FromServerMessage
|
||||
//dataFound := false
|
||||
// update messages counter
|
||||
err := r.Client.Incr("statistics:messages:messagelookups").Err()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// todo check pull requests signature
|
||||
// iterate over pull requests
|
||||
for _, rq := range msg.PullRequest {
|
||||
// get messages from redis
|
||||
msgcnt, err := r.Client.ZCount("msg:"+rq.LookupKey, "-inf", "+inf").Result()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := r.Client.ZPopMin("msg:"+rq.LookupKey, msgcnt).Result()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// iterate over messages
|
||||
for _, redismsg := range res {
|
||||
//println(redismsg.Score)
|
||||
val := redismsg.Member
|
||||
test := val.(string)
|
||||
var usrmsg meowlib.PackedUserMessage
|
||||
err := proto.Unmarshal([]byte(test), &usrmsg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// add server timestamp
|
||||
usrmsg.ServerTimestamp = append(usrmsg.ServerTimestamp, int64(redismsg.Score))
|
||||
from_server.Chat = append(from_server.Chat, &usrmsg)
|
||||
|
||||
// if delivery for that pick up requested, create, store and publish delivery message
|
||||
deliveryRequester, err := r.Client.SPop("msg:" + usrmsg.ServerDeliveryUuid).Result()
|
||||
if err != nil {
|
||||
if err != redis.Nil { // exit only if real error
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err != redis.Nil {
|
||||
// create a delivery record
|
||||
r.Client.ZAdd("dvy:"+deliveryRequester, redis.Z{Score: float64(time.Now().Unix()), Member: usrmsg.ServerDeliveryUuid})
|
||||
// publish it in case of listener
|
||||
r.Client.Publish("dvych:"+usrmsg.ServerDeliveryUuid, "!")
|
||||
}
|
||||
}
|
||||
// if no messages check for invitationanswer payload
|
||||
if msgcnt == 0 {
|
||||
// get invitation answer
|
||||
var answer meowlib.Invitation
|
||||
storedAnswer, _ := r.GetAnswerToInvitation(rq.LookupKey)
|
||||
if storedAnswer != nil {
|
||||
err := proto.Unmarshal(storedAnswer, &answer)
|
||||
if err != nil {
|
||||
from_server.Invitation.Payload = []byte("invitation answer corrupted")
|
||||
}
|
||||
from_server.Invitation = &answer
|
||||
// exit loop if invitation found, cannot store several in a message
|
||||
return &from_server, nil
|
||||
}
|
||||
// add invitation answer to the response
|
||||
}
|
||||
|
||||
}
|
||||
return &from_server, nil
|
||||
}
|
||||
|
||||
func goSubscribeAndListen(client *redis.Client, key string, messages chan<- string, wg *sync.WaitGroup, done <-chan struct{}) {
|
||||
defer wg.Done()
|
||||
pubsub := client.Subscribe("msgch:" + key)
|
||||
defer pubsub.Close()
|
||||
|
||||
// Create a new channel for the messages from this subscription
|
||||
myMessages := make(chan *redis.Message)
|
||||
go func() {
|
||||
for {
|
||||
msg, err := pubsub.ReceiveMessage()
|
||||
if err != nil {
|
||||
close(myMessages)
|
||||
return
|
||||
}
|
||||
myMessages <- msg
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for a message or for the done signal
|
||||
select {
|
||||
case msg := <-myMessages:
|
||||
messages <- msg.Payload
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RedisRouter) subscribe(msg *meowlib.ToServerMessage, timeout int) (*meowlib.FromServerMessage, error) {
|
||||
var from_server meowlib.FromServerMessage
|
||||
// update messages counter
|
||||
err := r.Client.Incr("statistics:messages:messagessubscription").Err()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
messages := make(chan string)
|
||||
var wg sync.WaitGroup
|
||||
done := make(chan struct{})
|
||||
// extract lookup keys and subscribe
|
||||
// iterate over pull requests
|
||||
for _, rq := range msg.PullRequest {
|
||||
wg.Add(1)
|
||||
// subscribe to the lookup key
|
||||
go goSubscribeAndListen(r.Client, rq.LookupKey, messages, &wg, done)
|
||||
}
|
||||
// wait for timeout or message
|
||||
select {
|
||||
case <-messages:
|
||||
close(done)
|
||||
return r.checkForMessage(msg)
|
||||
case <-time.After(time.Duration(timeout) * time.Second): // 10 seconds timeout
|
||||
close(done)
|
||||
}
|
||||
wg.Wait()
|
||||
return &from_server, nil
|
||||
}
|
||||
|
||||
func (r *RedisRouter) handleInvitation(msg *meowlib.ToServerMessage) (*meowlib.FromServerMessage, error) {
|
||||
var from_server meowlib.FromServerMessage
|
||||
// update messages counter
|
||||
err := r.Client.Incr("statistics:messages:invitation").Err()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch msg.Invitation.Step {
|
||||
// create invitation => provide shortcode and expiry
|
||||
case 1:
|
||||
url, expiry, err := r.StoreInvitation(msg.Invitation.Payload, int(msg.Invitation.Timeout), msg.Invitation.Password, r.InvitationTimeout, int(msg.Invitation.ShortcodeLen))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
from_server.Invitation = &meowlib.Invitation{}
|
||||
from_server.Invitation.Shortcode = url
|
||||
from_server.Invitation.Expiry = expiry.UTC().Unix()
|
||||
// get invitation => retrieve invitation from redis and send
|
||||
case 2:
|
||||
from_server.Invitation = &meowlib.Invitation{}
|
||||
invitation, err := r.GetInvitation(msg.Invitation.Shortcode, msg.Invitation.Password)
|
||||
|
||||
if err != nil {
|
||||
if err.Error() == "auth failed" {
|
||||
from_server.Invitation.Payload = []byte("authentication failure")
|
||||
} else {
|
||||
from_server.Invitation.Payload = []byte("invitation expired")
|
||||
}
|
||||
} else {
|
||||
from_server.Invitation.Payload = invitation // protobuf invitation
|
||||
}
|
||||
|
||||
// accept invitation => store accepted invitation for initiator
|
||||
case 3:
|
||||
var usermsg meowlib.PackedUserMessage
|
||||
err := proto.Unmarshal(msg.Invitation.Payload, &usermsg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := proto.Marshal(msg.Invitation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
expiry := r.StoreAnswerToInvitation(usermsg.Destination, int(msg.Invitation.Timeout), data, r.InvitationTimeout)
|
||||
from_server.Invitation = &meowlib.Invitation{}
|
||||
from_server.Invitation.Expiry = expiry.UTC().Unix()
|
||||
}
|
||||
return &from_server, nil
|
||||
}
|
||||
|
||||
func (r *RedisRouter) handleVideo(msg *meowlib.ToServerMessage) (*meowlib.FromServerMessage, error) {
|
||||
var from_server meowlib.FromServerMessage
|
||||
// update messages counter
|
||||
err := r.Client.Incr("statistics:messages:video").Err()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
videoData, err := r.ServerIdentity.VideoServer.UpdateVideoData(msg.VideoData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
from_server.VideoData = videoData
|
||||
from_server.UuidAck = msg.Uuid
|
||||
return &from_server, nil
|
||||
}
|
||||
|
||||
func (r *RedisRouter) handleMatriochka(msg *meowlib.ToServerMessage) (*meowlib.FromServerMessage, error) {
|
||||
var from_server meowlib.FromServerMessage
|
||||
// update messages counter
|
||||
err := r.Client.Incr("statistics:messages:matriochka").Err()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out, err := proto.Marshal(msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Client.ZAdd("mtk", redis.Z{Score: float64(time.Now().Unix()), Member: out})
|
||||
if msg.MatriochkaMessage.LookupKey != "" {
|
||||
//r.Client.ZAdd("trk:" + msg.MatriochkaMessage.Next.Uuid,{})
|
||||
}
|
||||
from_server.UuidAck = msg.Uuid
|
||||
return &from_server, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,663 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"forge.redroom.link/yves/meowlib"
|
||||
"github.com/alicebob/miniredis"
|
||||
"github.com/go-redis/redis"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"os"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AddLogger(zerolog.New(os.Stderr).Level(zerolog.Disabled))
|
||||
}
|
||||
|
||||
// newTestRouter spins up a miniredis instance and returns a RedisRouter wired to it.
|
||||
// The caller must call mr.Close() when done.
|
||||
func newTestRouter(t *testing.T) (*RedisRouter, *miniredis.Miniredis) {
|
||||
t.Helper()
|
||||
mr, err := miniredis.Run()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
id := CreateIdentity("TestServer", "A test server")
|
||||
router := &RedisRouter{
|
||||
Name: "TestRedis",
|
||||
ServerIdentity: id,
|
||||
Client: redis.NewClient(&redis.Options{
|
||||
Addr: mr.Addr(),
|
||||
}),
|
||||
InvitationTimeout: 3600,
|
||||
Context: nil,
|
||||
}
|
||||
// seed the statistics:start key that NewRedisRouter normally sets
|
||||
router.Client.Set("statistics:start", time.Now().UTC().Format(time.RFC3339), 0)
|
||||
return router, mr
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// storeMessage / checkForMessage round-trip
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestStoreAndCheckMessage(t *testing.T) {
|
||||
router, mr := newTestRouter(t)
|
||||
defer mr.Close()
|
||||
|
||||
dest := "lookup-key-alice"
|
||||
msg := &meowlib.ToServerMessage{
|
||||
Uuid: "msg-uuid-1",
|
||||
From: "sender-pub-key",
|
||||
Messages: []*meowlib.PackedUserMessage{
|
||||
{
|
||||
Destination: dest,
|
||||
Payload: []byte("hello alice"),
|
||||
Signature: []byte("sig1"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// store
|
||||
resp, err := router.storeMessage(msg)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "msg-uuid-1", resp.UuidAck)
|
||||
|
||||
// check: build a pull request for the same key
|
||||
pullMsg := &meowlib.ToServerMessage{
|
||||
PullRequest: []*meowlib.ConversationRequest{
|
||||
{LookupKey: dest},
|
||||
},
|
||||
}
|
||||
resp, err = router.checkForMessage(pullMsg)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, resp.Chat, 1)
|
||||
assert.Equal(t, dest, resp.Chat[0].Destination)
|
||||
assert.Equal(t, []byte("hello alice"), resp.Chat[0].Payload)
|
||||
}
|
||||
|
||||
func TestStoreMultipleMessagesAndCheck(t *testing.T) {
|
||||
router, mr := newTestRouter(t)
|
||||
defer mr.Close()
|
||||
|
||||
dest := "lookup-key-bob"
|
||||
msg := &meowlib.ToServerMessage{
|
||||
Uuid: "multi-uuid",
|
||||
Messages: []*meowlib.PackedUserMessage{
|
||||
{Destination: dest, Payload: []byte("msg-1")},
|
||||
{Destination: dest, Payload: []byte("msg-2")},
|
||||
{Destination: dest, Payload: []byte("msg-3")},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := router.storeMessage(msg)
|
||||
assert.NoError(t, err)
|
||||
|
||||
pullMsg := &meowlib.ToServerMessage{
|
||||
PullRequest: []*meowlib.ConversationRequest{
|
||||
{LookupKey: dest},
|
||||
},
|
||||
}
|
||||
resp, err := router.checkForMessage(pullMsg)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, resp.Chat, 3)
|
||||
}
|
||||
|
||||
// checkForMessage on an empty key returns an empty chat list (no error)
|
||||
func TestCheckForMessageEmpty(t *testing.T) {
|
||||
router, mr := newTestRouter(t)
|
||||
defer mr.Close()
|
||||
|
||||
pullMsg := &meowlib.ToServerMessage{
|
||||
PullRequest: []*meowlib.ConversationRequest{
|
||||
{LookupKey: "nonexistent-key"},
|
||||
},
|
||||
}
|
||||
resp, err := router.checkForMessage(pullMsg)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, resp.Chat)
|
||||
}
|
||||
|
||||
// messages are consumed (popped) — a second check returns nothing
|
||||
func TestCheckForMessageConsumes(t *testing.T) {
|
||||
router, mr := newTestRouter(t)
|
||||
defer mr.Close()
|
||||
|
||||
dest := "consume-key"
|
||||
msg := &meowlib.ToServerMessage{
|
||||
Messages: []*meowlib.PackedUserMessage{
|
||||
{Destination: dest, Payload: []byte("once")},
|
||||
},
|
||||
}
|
||||
router.storeMessage(msg)
|
||||
|
||||
pull := &meowlib.ToServerMessage{
|
||||
PullRequest: []*meowlib.ConversationRequest{{LookupKey: dest}},
|
||||
}
|
||||
resp, err := router.checkForMessage(pull)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, resp.Chat, 1)
|
||||
|
||||
// second pull — queue is drained
|
||||
resp, err = router.checkForMessage(pull)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, resp.Chat)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// storeMessage with delivery tracking
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// storeMessage calls SAdd("dvyrq:<uuid>", redis.Z{...}) when ServerDeliveryUuid
|
||||
// is set. Passing redis.Z (a sorted-set helper struct) to SAdd is a bug in
|
||||
// router.go — the member never actually lands in the set. This test documents
|
||||
// that the code path executes without error; the Redis state assertion is
|
||||
// intentionally omitted until the bug is fixed.
|
||||
func TestStoreMessageDeliveryTracking(t *testing.T) {
|
||||
router, mr := newTestRouter(t)
|
||||
defer mr.Close()
|
||||
|
||||
dest := "delivery-dest"
|
||||
msg := &meowlib.ToServerMessage{
|
||||
Uuid: "store-dvy",
|
||||
From: "sender-pub",
|
||||
Messages: []*meowlib.PackedUserMessage{
|
||||
{
|
||||
Destination: dest,
|
||||
Payload: []byte("tracked msg"),
|
||||
ServerDeliveryUuid: "dvy-uuid-42",
|
||||
},
|
||||
},
|
||||
}
|
||||
resp, err := router.storeMessage(msg)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "store-dvy", resp.UuidAck)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// storeMessage writes to multiple destinations
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestStoreMessageMultipleDestinations(t *testing.T) {
|
||||
router, mr := newTestRouter(t)
|
||||
defer mr.Close()
|
||||
|
||||
msg := &meowlib.ToServerMessage{
|
||||
Uuid: "multi-dest",
|
||||
Messages: []*meowlib.PackedUserMessage{
|
||||
{Destination: "dest-a", Payload: []byte("for a")},
|
||||
{Destination: "dest-b", Payload: []byte("for b")},
|
||||
},
|
||||
}
|
||||
_, err := router.storeMessage(msg)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// each destination has exactly one message
|
||||
cntA, _ := router.Client.ZCount("msg:dest-a", "-inf", "+inf").Result()
|
||||
cntB, _ := router.Client.ZCount("msg:dest-b", "-inf", "+inf").Result()
|
||||
assert.Equal(t, int64(1), cntA)
|
||||
assert.Equal(t, int64(1), cntB)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Route dispatcher
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestRouteDispatchesStoreAndCheck(t *testing.T) {
|
||||
router, mr := newTestRouter(t)
|
||||
defer mr.Close()
|
||||
|
||||
dest := "route-dest"
|
||||
|
||||
// first Route call: store a message
|
||||
storeReq := &meowlib.ToServerMessage{
|
||||
Uuid: "route-store-uuid",
|
||||
Messages: []*meowlib.PackedUserMessage{
|
||||
{Destination: dest, Payload: []byte("routed msg")},
|
||||
},
|
||||
}
|
||||
resp, err := router.Route(storeReq)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "route-store-uuid", resp.UuidAck)
|
||||
|
||||
// second Route call: pull that message
|
||||
pullReq := &meowlib.ToServerMessage{
|
||||
PullRequest: []*meowlib.ConversationRequest{
|
||||
{LookupKey: dest},
|
||||
},
|
||||
}
|
||||
resp, err = router.Route(pullReq)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, resp.Chat, 1)
|
||||
assert.Equal(t, []byte("routed msg"), resp.Chat[0].Payload)
|
||||
}
|
||||
|
||||
// Route with no actionable fields returns nil response and no error
|
||||
func TestRouteEmptyMessage(t *testing.T) {
|
||||
router, mr := newTestRouter(t)
|
||||
defer mr.Close()
|
||||
|
||||
resp, err := router.Route(&meowlib.ToServerMessage{})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, resp)
|
||||
}
|
||||
|
||||
// Route updates statistics counters
|
||||
func TestRouteIncrementsTotalCounter(t *testing.T) {
|
||||
router, mr := newTestRouter(t)
|
||||
defer mr.Close()
|
||||
|
||||
router.Route(&meowlib.ToServerMessage{})
|
||||
router.Route(&meowlib.ToServerMessage{})
|
||||
router.Route(&meowlib.ToServerMessage{})
|
||||
|
||||
val, err := router.Client.Get("statistics:messages:total").Int()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 3, val)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// handleMatriochka
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestHandleMatriochka(t *testing.T) {
|
||||
router, mr := newTestRouter(t)
|
||||
defer mr.Close()
|
||||
|
||||
msg := &meowlib.ToServerMessage{
|
||||
Uuid: "matriochka-uuid",
|
||||
MatriochkaMessage: &meowlib.Matriochka{
|
||||
LookupKey: "mtk-lookup",
|
||||
Data: []byte("onion layer"),
|
||||
Next: &meowlib.MatriochkaServer{
|
||||
Url: "http://next.server/meow",
|
||||
PublicKey: "next-pub-key",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := router.handleMatriochka(msg)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "matriochka-uuid", resp.UuidAck)
|
||||
|
||||
// verify something was stored in the mtk sorted set
|
||||
cnt, _ := router.Client.ZCount("mtk", "-inf", "+inf").Result()
|
||||
assert.Equal(t, int64(1), cnt)
|
||||
|
||||
// deserialize what was stored and verify it round-trips
|
||||
members, _ := router.Client.ZRange("mtk", 0, -1).Result()
|
||||
var stored meowlib.ToServerMessage
|
||||
err = proto.Unmarshal([]byte(members[0]), &stored)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "mtk-lookup", stored.MatriochkaMessage.LookupKey)
|
||||
assert.Equal(t, []byte("onion layer"), stored.MatriochkaMessage.Data)
|
||||
}
|
||||
|
||||
// multiple distinct matriochka messages accumulate in the sorted set
|
||||
func TestHandleMatriochkaMultiple(t *testing.T) {
|
||||
router, mr := newTestRouter(t)
|
||||
defer mr.Close()
|
||||
|
||||
// each message must differ so the sorted-set members are unique
|
||||
payloads := []string{"layer-1", "layer-2", "layer-3"}
|
||||
for _, p := range payloads {
|
||||
router.handleMatriochka(&meowlib.ToServerMessage{
|
||||
Uuid: "m-uuid-" + p,
|
||||
MatriochkaMessage: &meowlib.Matriochka{
|
||||
Data: []byte(p),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
cnt, _ := router.Client.ZCount("mtk", "-inf", "+inf").Result()
|
||||
assert.Equal(t, int64(3), cnt)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// handleInvitation — step 1 (create) and step 2 (retrieve)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestHandleInvitationStep1And2(t *testing.T) {
|
||||
router, mr := newTestRouter(t)
|
||||
defer mr.Close()
|
||||
|
||||
payload := []byte("invitation-data")
|
||||
|
||||
// Step 1: create invitation
|
||||
step1Msg := &meowlib.ToServerMessage{
|
||||
Invitation: &meowlib.Invitation{
|
||||
Step: 1,
|
||||
Payload: payload,
|
||||
Timeout: 60,
|
||||
ShortcodeLen: 12,
|
||||
},
|
||||
}
|
||||
resp, err := router.handleInvitation(step1Msg)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, resp.Invitation.Shortcode)
|
||||
assert.True(t, resp.Invitation.Expiry > 0)
|
||||
|
||||
shortcode := resp.Invitation.Shortcode
|
||||
|
||||
// Step 2: retrieve invitation (no password)
|
||||
step2Msg := &meowlib.ToServerMessage{
|
||||
Invitation: &meowlib.Invitation{
|
||||
Step: 2,
|
||||
Shortcode: shortcode,
|
||||
},
|
||||
}
|
||||
resp, err = router.handleInvitation(step2Msg)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, payload, resp.Invitation.Payload)
|
||||
}
|
||||
|
||||
func TestHandleInvitationStep1And2WithPassword(t *testing.T) {
|
||||
router, mr := newTestRouter(t)
|
||||
defer mr.Close()
|
||||
|
||||
payload := []byte("secret-invitation")
|
||||
password := "s3cret"
|
||||
|
||||
// Step 1: create with password
|
||||
step1Msg := &meowlib.ToServerMessage{
|
||||
Invitation: &meowlib.Invitation{
|
||||
Step: 1,
|
||||
Payload: payload,
|
||||
Timeout: 60,
|
||||
ShortcodeLen: 10,
|
||||
Password: password,
|
||||
},
|
||||
}
|
||||
resp, err := router.handleInvitation(step1Msg)
|
||||
assert.NoError(t, err)
|
||||
shortcode := resp.Invitation.Shortcode
|
||||
|
||||
// Step 2: wrong password
|
||||
step2Wrong := &meowlib.ToServerMessage{
|
||||
Invitation: &meowlib.Invitation{
|
||||
Step: 2,
|
||||
Shortcode: shortcode,
|
||||
Password: "wrong",
|
||||
},
|
||||
}
|
||||
resp, err = router.handleInvitation(step2Wrong)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []byte("authentication failure"), resp.Invitation.Payload)
|
||||
|
||||
// Step 2: correct password
|
||||
step2Correct := &meowlib.ToServerMessage{
|
||||
Invitation: &meowlib.Invitation{
|
||||
Step: 2,
|
||||
Shortcode: shortcode,
|
||||
Password: password,
|
||||
},
|
||||
}
|
||||
resp, err = router.handleInvitation(step2Correct)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, payload, resp.Invitation.Payload)
|
||||
}
|
||||
|
||||
// Step 2 on a non-existent shortcode returns "invitation expired"
|
||||
func TestHandleInvitationStep2NotFound(t *testing.T) {
|
||||
router, mr := newTestRouter(t)
|
||||
defer mr.Close()
|
||||
|
||||
step2Msg := &meowlib.ToServerMessage{
|
||||
Invitation: &meowlib.Invitation{
|
||||
Step: 2,
|
||||
Shortcode: "does-not-exist",
|
||||
},
|
||||
}
|
||||
resp, err := router.handleInvitation(step2Msg)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []byte("invitation expired"), resp.Invitation.Payload)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// handleInvitation — step 3 (store answer) + checkForMessage retrieves it
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestHandleInvitationStep3AndRetrieve(t *testing.T) {
|
||||
router, mr := newTestRouter(t)
|
||||
defer mr.Close()
|
||||
|
||||
lookupKey := "initiator-lookup-key"
|
||||
|
||||
// Build a PackedUserMessage whose Destination is the initiator's lookup key
|
||||
pum := &meowlib.PackedUserMessage{
|
||||
Destination: lookupKey,
|
||||
Payload: []byte("answer-payload"),
|
||||
}
|
||||
pumBytes, err := proto.Marshal(pum)
|
||||
assert.NoError(t, err)
|
||||
|
||||
invitationMsg := &meowlib.Invitation{
|
||||
Step: 3,
|
||||
Payload: pumBytes,
|
||||
Timeout: 60,
|
||||
}
|
||||
|
||||
step3Msg := &meowlib.ToServerMessage{
|
||||
Invitation: invitationMsg,
|
||||
}
|
||||
resp, err := router.handleInvitation(step3Msg)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, resp.Invitation.Expiry > 0)
|
||||
|
||||
// Now simulate the initiator polling: checkForMessage with the lookup key
|
||||
// and an empty message queue — should fall back to invitation answer
|
||||
pullMsg := &meowlib.ToServerMessage{
|
||||
PullRequest: []*meowlib.ConversationRequest{
|
||||
{LookupKey: lookupKey},
|
||||
},
|
||||
}
|
||||
resp, err = router.checkForMessage(pullMsg)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, resp.Invitation)
|
||||
|
||||
// The stored invitation answer should deserialize cleanly
|
||||
var storedInv meowlib.Invitation
|
||||
err = proto.Unmarshal(resp.Invitation.Payload, &storedInv)
|
||||
// payload is the re-serialized Invitation protobuf from step 3
|
||||
// just verify it's non-empty
|
||||
assert.NotEmpty(t, resp.Invitation.Payload)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// handleInvitation — password brute-force lockout (3 attempts)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestHandleInvitationPasswordLockout(t *testing.T) {
|
||||
router, mr := newTestRouter(t)
|
||||
defer mr.Close()
|
||||
|
||||
payload := []byte("locked-invitation")
|
||||
|
||||
// create invitation with password
|
||||
step1Msg := &meowlib.ToServerMessage{
|
||||
Invitation: &meowlib.Invitation{
|
||||
Step: 1,
|
||||
Payload: payload,
|
||||
Timeout: 60,
|
||||
ShortcodeLen: 8,
|
||||
Password: "correct",
|
||||
},
|
||||
}
|
||||
resp, err := router.handleInvitation(step1Msg)
|
||||
assert.NoError(t, err)
|
||||
shortcode := resp.Invitation.Shortcode
|
||||
|
||||
// 3 wrong attempts
|
||||
for range 3 {
|
||||
step2 := &meowlib.ToServerMessage{
|
||||
Invitation: &meowlib.Invitation{
|
||||
Step: 2,
|
||||
Shortcode: shortcode,
|
||||
Password: "wrong",
|
||||
},
|
||||
}
|
||||
router.handleInvitation(step2)
|
||||
}
|
||||
|
||||
// invitation should now be destroyed — even with correct password
|
||||
step2Correct := &meowlib.ToServerMessage{
|
||||
Invitation: &meowlib.Invitation{
|
||||
Step: 2,
|
||||
Shortcode: shortcode,
|
||||
Password: "correct",
|
||||
},
|
||||
}
|
||||
resp, err = router.handleInvitation(step2Correct)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []byte("invitation expired"), resp.Invitation.Payload)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// handleVideo
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestHandleVideoNoServer(t *testing.T) {
|
||||
router, mr := newTestRouter(t)
|
||||
defer mr.Close()
|
||||
|
||||
// VideoServer with no credentials configured — UpdateVideoData still works
|
||||
// (it just sets Url and returns empty credentials slice)
|
||||
msg := &meowlib.ToServerMessage{
|
||||
Uuid: "video-uuid",
|
||||
VideoData: &meowlib.VideoData{
|
||||
Room: "test-room",
|
||||
Duration: 300,
|
||||
},
|
||||
}
|
||||
resp, err := router.handleVideo(msg)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "video-uuid", resp.UuidAck)
|
||||
assert.NotNil(t, resp.VideoData)
|
||||
assert.Equal(t, "test-room", resp.VideoData.Room)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Route dispatches matriochka via top-level Route
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestRouteMatriochka(t *testing.T) {
|
||||
router, mr := newTestRouter(t)
|
||||
defer mr.Close()
|
||||
|
||||
msg := &meowlib.ToServerMessage{
|
||||
Uuid: "route-mtk",
|
||||
MatriochkaMessage: &meowlib.Matriochka{
|
||||
Data: []byte("wrapped"),
|
||||
},
|
||||
}
|
||||
resp, err := router.Route(msg)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "route-mtk", resp.UuidAck)
|
||||
|
||||
cnt, _ := router.Client.ZCount("mtk", "-inf", "+inf").Result()
|
||||
assert.Equal(t, int64(1), cnt)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Route dispatches invitation via top-level Route
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestRouteInvitation(t *testing.T) {
|
||||
router, mr := newTestRouter(t)
|
||||
defer mr.Close()
|
||||
|
||||
msg := &meowlib.ToServerMessage{
|
||||
Invitation: &meowlib.Invitation{
|
||||
Step: 1,
|
||||
Payload: []byte("via-route"),
|
||||
Timeout: 30,
|
||||
ShortcodeLen: 6,
|
||||
},
|
||||
}
|
||||
resp, err := router.Route(msg)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, resp.Invitation.Shortcode)
|
||||
assert.Len(t, resp.Invitation.Shortcode, 6)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// statistics counters
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestStatisticsCountersIncrement(t *testing.T) {
|
||||
router, mr := newTestRouter(t)
|
||||
defer mr.Close()
|
||||
|
||||
dest := "stats-dest"
|
||||
|
||||
// one store increments usermessages
|
||||
router.Route(&meowlib.ToServerMessage{
|
||||
Messages: []*meowlib.PackedUserMessage{
|
||||
{Destination: dest, Payload: []byte("x")},
|
||||
},
|
||||
})
|
||||
val, _ := router.Client.Get("statistics:messages:usermessages").Int()
|
||||
assert.Equal(t, 1, val)
|
||||
|
||||
// one pull increments messagelookups
|
||||
router.Route(&meowlib.ToServerMessage{
|
||||
PullRequest: []*meowlib.ConversationRequest{
|
||||
{LookupKey: dest},
|
||||
},
|
||||
})
|
||||
val, _ = router.Client.Get("statistics:messages:messagelookups").Int()
|
||||
assert.Equal(t, 1, val)
|
||||
|
||||
// one matriochka increments matriochka counter
|
||||
router.Route(&meowlib.ToServerMessage{
|
||||
MatriochkaMessage: &meowlib.Matriochka{Data: []byte("m")},
|
||||
})
|
||||
val, _ = router.Client.Get("statistics:messages:matriochka").Int()
|
||||
assert.Equal(t, 1, val)
|
||||
|
||||
// one invitation increments invitation counter
|
||||
router.Route(&meowlib.ToServerMessage{
|
||||
Invitation: &meowlib.Invitation{
|
||||
Step: 1,
|
||||
Payload: []byte("i"),
|
||||
Timeout: 10,
|
||||
ShortcodeLen: 4,
|
||||
},
|
||||
})
|
||||
val, _ = router.Client.Get("statistics:messages:invitation").Int()
|
||||
assert.Equal(t, 1, val)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// checkForMessage with multiple pull request keys
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestCheckForMessageMultipleKeys(t *testing.T) {
|
||||
router, mr := newTestRouter(t)
|
||||
defer mr.Close()
|
||||
|
||||
// store one message on each of two keys
|
||||
router.storeMessage(&meowlib.ToServerMessage{
|
||||
Messages: []*meowlib.PackedUserMessage{
|
||||
{Destination: "key-x", Payload: []byte("from-x")},
|
||||
},
|
||||
})
|
||||
router.storeMessage(&meowlib.ToServerMessage{
|
||||
Messages: []*meowlib.PackedUserMessage{
|
||||
{Destination: "key-y", Payload: []byte("from-y")},
|
||||
},
|
||||
})
|
||||
|
||||
pull := &meowlib.ToServerMessage{
|
||||
PullRequest: []*meowlib.ConversationRequest{
|
||||
{LookupKey: "key-x"},
|
||||
{LookupKey: "key-y"},
|
||||
},
|
||||
}
|
||||
resp, err := router.checkForMessage(pull)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, resp.Chat, 2)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"forge.redroom.link/yves/meowlib"
|
||||
"github.com/livekit/protocol/auth"
|
||||
)
|
||||
|
||||
type VideoServer struct {
|
||||
Url string `json:"url,omitempty"`
|
||||
ApiKey string `json:"api_key,omitempty"`
|
||||
ApiSecret string `json:"api_secret,omitempty"`
|
||||
}
|
||||
|
||||
func (s *VideoServer) GetJoinToken(room, username string, validity time.Duration) (string, error) {
|
||||
at := auth.NewAccessToken(s.ApiKey, s.ApiSecret)
|
||||
grant := &auth.VideoGrant{
|
||||
RoomJoin: true,
|
||||
Room: room,
|
||||
}
|
||||
at.AddGrant(grant).
|
||||
SetIdentity(username).
|
||||
SetValidFor(validity)
|
||||
|
||||
return at.ToJWT()
|
||||
}
|
||||
|
||||
func (s *VideoServer) UpdateVideoData(vd *meowlib.VideoData) (*meowlib.VideoData, error) {
|
||||
vd.Url = s.Url
|
||||
vd.Credentials = []*meowlib.VideoCredential{}
|
||||
for idx := range len(vd.Credentials) {
|
||||
token, err := s.GetJoinToken(vd.Room, vd.Credentials[idx].Username, time.Duration(vd.Duration*uint64(time.Second)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
vd.Credentials[idx].Token = token
|
||||
vd.Credentials[idx].SharedKey = s.ApiKey
|
||||
}
|
||||
return vd, nil
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package meowlib
|
||||
|
||||
func (sc *ServerCard) GetUid() string {
|
||||
if len(sc.Login) > 0 || len(sc.Password) > 0 {
|
||||
return sc.Login + ":" + sc.Password + "@" + sc.Url
|
||||
}
|
||||
return sc.Url
|
||||
}
|
||||
|
||||
func (sc *ServerCard) IsSame(sc1 *ServerCard) bool {
|
||||
return sc.GetUid() == sc1.GetUid()
|
||||
}
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
sudo apt install -y protobuf-compiler
|
||||
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
|
||||
go install github.com/pseudomuto/protoc-gen-doc/cmd/protoc-gen-doc@latest
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user