Compare commits
152 Commits
alpha0
...
eb7fdc9b03
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
103
.VSCodeCounter/2024-08-27_08-23-38/details.md
Normal file
103
.VSCodeCounter/2024-08-27_08-23-38/details.md
Normal file
@@ -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)
|
||||||
15
.VSCodeCounter/2024-08-27_08-23-38/diff-details.md
Normal file
15
.VSCodeCounter/2024-08-27_08-23-38/diff-details.md
Normal file
@@ -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
|
||||||
2
.VSCodeCounter/2024-08-27_08-23-38/diff.csv
Normal file
2
.VSCodeCounter/2024-08-27_08-23-38/diff.csv
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
"filename", "language", "", "comment", "blank", "total"
|
||||||
|
"Total", "-", , 0, 0, 0
|
||||||
|
19
.VSCodeCounter/2024-08-27_08-23-38/diff.md
Normal file
19
.VSCodeCounter/2024-08-27_08-23-38/diff.md
Normal file
@@ -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)
|
||||||
22
.VSCodeCounter/2024-08-27_08-23-38/diff.txt
Normal file
22
.VSCodeCounter/2024-08-27_08-23-38/diff.txt
Normal file
@@ -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 |
|
||||||
|
+----------+----------+------------+------------+------------+------------+
|
||||||
90
.VSCodeCounter/2024-08-27_08-23-38/results.csv
Normal file
90
.VSCodeCounter/2024-08-27_08-23-38/results.csv
Normal file
@@ -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
|
||||||
|
44
.VSCodeCounter/2024-08-27_08-23-38/results.md
Normal file
44
.VSCodeCounter/2024-08-27_08-23-38/results.md
Normal file
@@ -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)
|
||||||
135
.VSCodeCounter/2024-08-27_08-23-38/results.txt
Normal file
135
.VSCodeCounter/2024-08-27_08-23-38/results.txt
Normal file
@@ -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 |
|
||||||
|
+-----------------------------------------------------------------------------------------+------------------+------------+------------+------------+------------+
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -15,3 +15,6 @@ client/test.cfg
|
|||||||
.VSCodeCouter/
|
.VSCodeCouter/
|
||||||
meowlib-sources.jar
|
meowlib-sources.jar
|
||||||
meowlib.aar
|
meowlib.aar
|
||||||
|
client/test.db
|
||||||
|
CLAUDE.md
|
||||||
|
CODE_REVIEW.md
|
||||||
|
|||||||
@@ -19,3 +19,10 @@ run the shell scripts
|
|||||||
# Tests
|
# 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
|
||||||
|
|
||||||
|
|||||||
100
asymcrypt.go
100
asymcrypt.go
@@ -2,12 +2,11 @@ package meowlib
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||||
"github.com/ProtonMail/gopenpgp/v2/helper"
|
"github.com/ProtonMail/gopenpgp/v2/helper"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type KeyPair struct {
|
type KeyPair struct {
|
||||||
@@ -18,47 +17,46 @@ type KeyPair struct {
|
|||||||
|
|
||||||
type KeysArray []KeyPair
|
type KeysArray []KeyPair
|
||||||
|
|
||||||
func NewKeyPair() KeyPair {
|
func NewKeyPair() (*KeyPair, error) { // Return error!
|
||||||
var kp KeyPair
|
var kp KeyPair
|
||||||
keys, err := crypto.GenerateKey("name", "mail", "x25519", 0)
|
keys, err := crypto.GenerateKey("name", "mail", "x25519", 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Msg("Key generation failed")
|
return nil, fmt.Errorf("key generation failed: %w", err)
|
||||||
}
|
}
|
||||||
kp.Generated = time.Now()
|
|
||||||
pub, err := keys.GetArmoredPublicKey()
|
pub, err := keys.GetArmoredPublicKey()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Msg("Public key extraction failed")
|
return nil, fmt.Errorf("gopenpgp: unable to get public key: %w", err)
|
||||||
}
|
}
|
||||||
kp.Public = base64.StdEncoding.EncodeToString([]byte(pub))
|
|
||||||
priv, err := keys.Armor()
|
priv, err := keys.Armor()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Msg("Private key extraction failed")
|
return nil, fmt.Errorf("failed to armor private key: %w", err)
|
||||||
}
|
}
|
||||||
|
kp.Public = base64.StdEncoding.EncodeToString([]byte(pub))
|
||||||
kp.Private = base64.StdEncoding.EncodeToString([]byte(priv))
|
kp.Private = base64.StdEncoding.EncodeToString([]byte(priv))
|
||||||
return kp
|
kp.Generated = time.Now()
|
||||||
|
return &kp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (Kp *KeyPair) GetCryptoKeyObject() *crypto.Key {
|
func (Kp *KeyPair) GetCryptoKeyObject() (*crypto.Key, error) {
|
||||||
priv, err := base64.StdEncoding.DecodeString(Kp.Private)
|
priv, err := base64.StdEncoding.DecodeString(Kp.Private)
|
||||||
if err != nil {
|
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.NewKeyFromArmored(string(priv))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Msg("Create key from armoured failed")
|
return nil, fmt.Errorf("Ccreate key from armoured failed: %w", err)
|
||||||
}
|
}
|
||||||
return key
|
return key, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func AsymEncrypt(publicKey string, data []byte) ([]byte, error) {
|
func AsymEncrypt(publicKey string, data []byte) ([]byte, error) {
|
||||||
pub, err := base64.StdEncoding.DecodeString(publicKey)
|
pub, err := base64.StdEncoding.DecodeString(publicKey)
|
||||||
if err != nil {
|
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(string(pub), crypto.NewPlainMessage(data))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Msg("Message encryption failed")
|
return nil, fmt.Errorf("Message encryption failed: %w", err)
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ciphertext.GetBinary(), err
|
return ciphertext.GetBinary(), err
|
||||||
@@ -67,11 +65,11 @@ func AsymEncrypt(publicKey string, data []byte) ([]byte, error) {
|
|||||||
func AsymDecrypt(PrivateKey string, data []byte) ([]byte, error) {
|
func AsymDecrypt(PrivateKey string, data []byte) ([]byte, error) {
|
||||||
priv, err := base64.StdEncoding.DecodeString(PrivateKey)
|
priv, err := base64.StdEncoding.DecodeString(PrivateKey)
|
||||||
if err != nil {
|
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(string(priv), nil, crypto.NewPGPMessage(data))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Msg("Message decryption failed")
|
return nil, fmt.Errorf("Message decryption failed: %w", err)
|
||||||
}
|
}
|
||||||
return decrypted.GetBinary(), err
|
return decrypted.GetBinary(), err
|
||||||
}
|
}
|
||||||
@@ -79,11 +77,11 @@ func AsymDecrypt(PrivateKey string, data []byte) ([]byte, error) {
|
|||||||
func AsymEncryptArmored(PublicKey string, data []byte) ([]byte, error) {
|
func AsymEncryptArmored(PublicKey string, data []byte) ([]byte, error) {
|
||||||
pub, err := base64.StdEncoding.DecodeString(PublicKey)
|
pub, err := base64.StdEncoding.DecodeString(PublicKey)
|
||||||
if err != nil {
|
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)
|
armor, err := helper.EncryptBinaryMessageArmored(string(pub), data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Msg("Message encryption failed")
|
return nil, fmt.Errorf("Message encryption failed: %w", err)
|
||||||
}
|
}
|
||||||
return []byte(armor), err
|
return []byte(armor), err
|
||||||
}
|
}
|
||||||
@@ -91,11 +89,11 @@ func AsymEncryptArmored(PublicKey string, data []byte) ([]byte, error) {
|
|||||||
func AsymDecryptArmored(PrivateKey string, data []byte) ([]byte, error) {
|
func AsymDecryptArmored(PrivateKey string, data []byte) ([]byte, error) {
|
||||||
priv, err := base64.StdEncoding.DecodeString(PrivateKey)
|
priv, err := base64.StdEncoding.DecodeString(PrivateKey)
|
||||||
if err != nil {
|
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))
|
decrypted, err := helper.DecryptBinaryMessageArmored(string(priv), nil, string(data))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Msg("Message decryption failed")
|
return nil, fmt.Errorf("Message decryption failed: %w", err)
|
||||||
}
|
}
|
||||||
return []byte(decrypted), err
|
return []byte(decrypted), err
|
||||||
}
|
}
|
||||||
@@ -144,7 +142,7 @@ func encryptMessage(key string, message *crypto.PlainMessage) (*crypto.PGPMessag
|
|||||||
|
|
||||||
ciphertext, err := publicKeyRing.Encrypt(message, nil)
|
ciphertext, err := publicKeyRing.Encrypt(message, nil)
|
||||||
if err != 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
|
return ciphertext, nil
|
||||||
@@ -153,24 +151,24 @@ func encryptMessage(key string, message *crypto.PlainMessage) (*crypto.PGPMessag
|
|||||||
func decryptMessage(privateKey string, passphrase []byte, ciphertext *crypto.PGPMessage) (*crypto.PlainMessage, error) {
|
func decryptMessage(privateKey string, passphrase []byte, ciphertext *crypto.PGPMessage) (*crypto.PlainMessage, error) {
|
||||||
privateKeyObj, err := crypto.NewKeyFromArmored(privateKey)
|
privateKeyObj, err := crypto.NewKeyFromArmored(privateKey)
|
||||||
if err != nil {
|
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)
|
privateKeyUnlocked, err := privateKeyObj.Unlock(passphrase)
|
||||||
if err != nil {
|
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()
|
defer privateKeyUnlocked.ClearPrivateParams()
|
||||||
|
|
||||||
privateKeyRing, err := crypto.NewKeyRing(privateKeyUnlocked)
|
privateKeyRing, err := crypto.NewKeyRing(privateKeyUnlocked)
|
||||||
if err != nil {
|
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)
|
message, err := privateKeyRing.Decrypt(ciphertext, nil, 0)
|
||||||
if err != nil {
|
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
|
return message, nil
|
||||||
@@ -179,19 +177,19 @@ func decryptMessage(privateKey string, passphrase []byte, ciphertext *crypto.PGP
|
|||||||
func createPublicKeyRing(publicKey string) (*crypto.KeyRing, error) {
|
func createPublicKeyRing(publicKey string) (*crypto.KeyRing, error) {
|
||||||
publicKeyObj, err := crypto.NewKeyFromArmored(publicKey)
|
publicKeyObj, err := crypto.NewKeyFromArmored(publicKey)
|
||||||
if err != nil {
|
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() {
|
if publicKeyObj.IsPrivate() {
|
||||||
publicKeyObj, err = publicKeyObj.ToPublic()
|
publicKeyObj, err = publicKeyObj.ToPublic()
|
||||||
if err != nil {
|
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)
|
publicKeyRing, err := crypto.NewKeyRing(publicKeyObj)
|
||||||
if err != nil {
|
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
|
return publicKeyRing, nil
|
||||||
@@ -201,18 +199,15 @@ func AsymEncryptAndSign(PublicEncryptionKey string, PrivateSignatureKey string,
|
|||||||
var enc EncryptedMessage
|
var enc EncryptedMessage
|
||||||
pub, err := base64.StdEncoding.DecodeString(PublicEncryptionKey)
|
pub, err := base64.StdEncoding.DecodeString(PublicEncryptionKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Msg("Message encryption and sign b64 failed")
|
return nil, fmt.Errorf("Message encryption and sign b64 failed: %w", err)
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
priv, err := base64.StdEncoding.DecodeString(PrivateSignatureKey)
|
priv, err := base64.StdEncoding.DecodeString(PrivateSignatureKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Msg("Message encryption and sign b64 failed")
|
return nil, fmt.Errorf("Message encryption and sign b64 failed: %w", err)
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
ciphertext, signature, err := encryptAndSignMessage(string(pub), string(priv), crypto.NewPlainMessage(data))
|
ciphertext, signature, err := encryptAndSignMessage(string(pub), string(priv), crypto.NewPlainMessage(data))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Msg("Message encryption failed")
|
return nil, fmt.Errorf("Message encryption failed: %w", err)
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
enc.Data = ciphertext.GetBinary()
|
enc.Data = ciphertext.GetBinary()
|
||||||
enc.Signature = []byte(signature)
|
enc.Signature = []byte(signature)
|
||||||
@@ -222,18 +217,15 @@ func AsymEncryptAndSign(PublicEncryptionKey string, PrivateSignatureKey string,
|
|||||||
func AsymDecryptAndCheck(MyPrivateEncryptionKey string, MyContactPublicKey string, data []byte, Signature []byte) (DecryptedMessage []byte, err error) {
|
func AsymDecryptAndCheck(MyPrivateEncryptionKey string, MyContactPublicKey string, data []byte, Signature []byte) (DecryptedMessage []byte, err error) {
|
||||||
priv, err := base64.StdEncoding.DecodeString(MyPrivateEncryptionKey)
|
priv, err := base64.StdEncoding.DecodeString(MyPrivateEncryptionKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Msg("Message decryption and sign b64 failed")
|
return nil, fmt.Errorf("Message decryption and sign b64 failed: %w", err)
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
pub, err := base64.StdEncoding.DecodeString(MyContactPublicKey)
|
pub, err := base64.StdEncoding.DecodeString(MyContactPublicKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Msg("Message decryption and sign b64 failed")
|
return nil, fmt.Errorf("Message decryption and sign b64 failed: %w", err)
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
DecryptedMessage, err = decryptAndCheckMessage(string(pub), string(priv), crypto.NewPGPMessage(data), crypto.NewPGPSignature(Signature))
|
DecryptedMessage, err = decryptAndCheckMessage(string(pub), string(priv), crypto.NewPGPMessage(data), crypto.NewPGPSignature(Signature))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Msg("Message decryption and sign failed")
|
return nil, fmt.Errorf("Message decryption and sign failed: %w", err)
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
return DecryptedMessage, err
|
return DecryptedMessage, err
|
||||||
}
|
}
|
||||||
@@ -247,30 +239,30 @@ func encryptAndSignMessage(pub string, priv string, message *crypto.PlainMessage
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err != 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")
|
||||||
}
|
}
|
||||||
|
|
||||||
if privateKeyObj, err = crypto.NewKeyFromArmored(priv); err != nil {
|
if privateKeyObj, err = crypto.NewKeyFromArmored(priv); err != nil {
|
||||||
return nil, nil, errors.Wrap(err, "gopenpgp: unable to parse private key")
|
return nil, nil, fmt.Errorf("gopenpgp: unable to parse private key")
|
||||||
}
|
}
|
||||||
|
|
||||||
if unlockedKeyObj, err = privateKeyObj.Unlock(nil); err != nil {
|
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()
|
defer unlockedKeyObj.ClearPrivateParams()
|
||||||
|
|
||||||
if privateKeyRing, err = crypto.NewKeyRing(unlockedKeyObj); err != nil {
|
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)
|
ciphertext, err := publicKeyRing.Encrypt(message, nil)
|
||||||
if err != 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)
|
signature, err := privateKeyRing.SignDetached(message)
|
||||||
if err != 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")
|
||||||
}
|
}
|
||||||
return ciphertext, signature.GetBinary(), nil
|
return ciphertext, signature.GetBinary(), nil
|
||||||
}
|
}
|
||||||
@@ -284,30 +276,30 @@ func decryptAndCheckMessage(pub string, priv string, message *crypto.PGPMessage,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "gopenpgp: unable to encrypt message")
|
return nil, fmt.Errorf("gopenpgp: unable to encrypt message")
|
||||||
}
|
}
|
||||||
|
|
||||||
if privateKeyObj, err = crypto.NewKeyFromArmored(priv); err != nil {
|
if privateKeyObj, err = crypto.NewKeyFromArmored(priv); err != nil {
|
||||||
return nil, errors.Wrap(err, "gopenpgp: unable to parse private key")
|
return nil, fmt.Errorf("gopenpgp: unable to parse private key")
|
||||||
}
|
}
|
||||||
|
|
||||||
if unlockedKeyObj, err = privateKeyObj.Unlock(nil); err != nil {
|
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()
|
defer unlockedKeyObj.ClearPrivateParams()
|
||||||
|
|
||||||
if privateKeyRing, err = crypto.NewKeyRing(unlockedKeyObj); err != nil {
|
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)
|
plainmessage, err := privateKeyRing.Decrypt(message, nil, 0)
|
||||||
if err != nil {
|
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())
|
err = publicKeyRing.VerifyDetached(plainmessage, signature, crypto.GetUnixTime())
|
||||||
if err != nil {
|
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
|
return plainmessage.GetBinary(), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,21 +42,36 @@ WE88AQOdxtE8dAuu16suOpgLUfluDgnzCg==
|
|||||||
-----END PGP PUBLIC KEY BLOCK-----`
|
-----END PGP PUBLIC KEY BLOCK-----`
|
||||||
|
|
||||||
func TestNewKeyPair(t *testing.T) {
|
func TestNewKeyPair(t *testing.T) {
|
||||||
kp := NewKeyPair()
|
kp, err := NewKeyPair()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
fmt.Println(kp.Public)
|
fmt.Println(kp.Public)
|
||||||
fmt.Println(kp.Private)
|
fmt.Println(kp.Private)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetKey(t *testing.T) {
|
func TestGetKey(t *testing.T) {
|
||||||
kp := NewKeyPair()
|
kp, err := NewKeyPair()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
// fmt.Println(kp.Public)
|
// fmt.Println(kp.Public)
|
||||||
// fmt.Println(kp.Private)
|
// fmt.Println(kp.Private)
|
||||||
key := kp.GetCryptoKeyObject()
|
key, err := kp.GetCryptoKeyObject()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
// fmt.Println(key.Armor())
|
// fmt.Println(key.Armor())
|
||||||
Armpubkey, _ := key.GetArmoredPublicKey()
|
Armpubkey, err := key.GetArmoredPublicKey()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
pubkey := base64.StdEncoding.EncodeToString([]byte(Armpubkey))
|
pubkey := base64.StdEncoding.EncodeToString([]byte(Armpubkey))
|
||||||
println(len([]byte(pubkey)))
|
println(len([]byte(pubkey)))
|
||||||
binpubkey, _ := key.GetPublicKey()
|
binpubkey, err := key.GetPublicKey()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
println(len(binpubkey))
|
println(len(binpubkey))
|
||||||
assert.Equal(t, kp.Public, pubkey, "The two public keys should be the same.")
|
assert.Equal(t, kp.Public, pubkey, "The two public keys should be the same.")
|
||||||
//if kp.Public != pubkey {
|
//if kp.Public != pubkey {
|
||||||
@@ -65,7 +80,10 @@ func TestGetKey(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAsymEncryptDecrypt(t *testing.T) {
|
func TestAsymEncryptDecrypt(t *testing.T) {
|
||||||
kp := NewKeyPair()
|
kp, err := NewKeyPair()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
foo := []byte("!#$%&()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_abcdefghijklmnopqrstuvwxyz{|}~")
|
foo := []byte("!#$%&()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_abcdefghijklmnopqrstuvwxyz{|}~")
|
||||||
encMess, err := AsymEncrypt(kp.Public, foo)
|
encMess, err := AsymEncrypt(kp.Public, foo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -80,7 +98,10 @@ func TestAsymEncryptDecrypt(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAsymEncryptDecryptSigned(t *testing.T) {
|
func TestAsymEncryptDecryptSigned(t *testing.T) {
|
||||||
kp := NewKeyPair()
|
kp, err := NewKeyPair()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
foo := "!#$%&()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_abcdefghijklmnopqrstuvwxyz{|}~"
|
foo := "!#$%&()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_abcdefghijklmnopqrstuvwxyz{|}~"
|
||||||
enc, err := AsymEncryptAndSign(kp.Public, kp.Private, []byte(foo))
|
enc, err := AsymEncryptAndSign(kp.Public, kp.Private, []byte(foo))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -94,7 +115,10 @@ func TestAsymEncryptDecryptSigned(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAsymEncryptDecryptSigned2(t *testing.T) {
|
func TestAsymEncryptDecryptSigned2(t *testing.T) {
|
||||||
kp := NewKeyPair()
|
kp, err := NewKeyPair()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
foo := "!#$%&()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_abcdefghijklmnopqrstuvwxyz{|}~"
|
foo := "!#$%&()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_abcdefghijklmnopqrstuvwxyz{|}~"
|
||||||
enc, err := AsymEncryptAndSign(kp.Public, kp.Private, []byte(foo))
|
enc, err := AsymEncryptAndSign(kp.Public, kp.Private, []byte(foo))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
8
client/avatar.go
Normal file
8
client/avatar.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Avatar struct {
|
||||||
|
File string
|
||||||
|
Date time.Time
|
||||||
|
}
|
||||||
124
client/config.go
124
client/config.go
@@ -2,23 +2,32 @@ package client
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/awnumar/memguard"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
// UserConfig
|
// UserConfig
|
||||||
SavePassword bool `json:"save_password,omitempty"`
|
SavePassword bool `json:"save_password,omitempty"`
|
||||||
SavedPassword string `json:"saved_password,omitempty"`
|
SavedPassword string `json:"saved_password,omitempty"`
|
||||||
|
PasswordTip string `json:"password_tip,omitempty"`
|
||||||
|
PasswordTipUnlock string `json:"password_tip_unlock,omitempty"`
|
||||||
// Technical
|
// Technical
|
||||||
IdentityFile string `json:"identity_file,omitempty"`
|
IdentityFile string `json:"identity_file,omitempty"`
|
||||||
StoragePath string `json:"storage_path,omitempty"`
|
StoragePath string `json:"storage_path,omitempty"`
|
||||||
MaxIdsPerUser int `json:"max_ids_per_user,omitempty"`
|
MaxIdsPerUser int `json:"max_ids_per_user,omitempty"`
|
||||||
MsgDbRollingPeriod int `json:"msg_db_rolling_period,omitempty"`
|
MsgDbRollingPeriod int `json:"msg_db_rolling_period,omitempty"`
|
||||||
Chunksize int64 `json:"chunksize,omitempty"`
|
Chunksize int64 `json:"chunksize,omitempty"`
|
||||||
ServerPollInterval int `json:"server_poll_interval,omitempty"`
|
|
||||||
DbSize int `json:"db_size,omitempty"`
|
DbSize int `json:"db_size,omitempty"`
|
||||||
UserAgent string `json:"user_agent,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
|
// GUI
|
||||||
LastOpenChat string `json:"last_open_chat,omitempty"`
|
LastOpenChat string `json:"last_open_chat,omitempty"`
|
||||||
SoundNotificationEnable bool `json:"sound_notification_enable,omitempty"`
|
SoundNotificationEnable bool `json:"sound_notification_enable,omitempty"`
|
||||||
@@ -42,9 +51,9 @@ type Config struct {
|
|||||||
DbSuffix string `json:"db_suffix,omitempty"`
|
DbSuffix string `json:"db_suffix,omitempty"`
|
||||||
|
|
||||||
// Inner
|
// Inner
|
||||||
memoryPassword string `json:"memory_password,omitempty"`
|
memoryPassword *memguard.LockedBuffer
|
||||||
additionalPasswords []string `json:"additional_passwords,omitempty"`
|
additionalPasswords []*memguard.LockedBuffer
|
||||||
me *Identity `json:"me,omitempty"`
|
me *Identity
|
||||||
}
|
}
|
||||||
|
|
||||||
var instance *Config
|
var instance *Config
|
||||||
@@ -57,6 +66,18 @@ func GetConfig() *Config {
|
|||||||
return instance
|
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 {
|
func (c *Config) Load(filename string) error {
|
||||||
data, err := os.ReadFile(filename)
|
data, err := os.ReadFile(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -66,6 +87,18 @@ func (c *Config) Load(filename string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,19 +107,26 @@ func (c *Config) Save(filename string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
os.WriteFile(filename, data, 0644)
|
err = os.WriteFile(filename, data, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) SetMemPass(pass string) {
|
func (c *Config) SetMemPass(pass string) error {
|
||||||
c.memoryPassword = pass
|
if c.memoryPassword != nil {
|
||||||
|
c.memoryPassword.Destroy()
|
||||||
|
}
|
||||||
|
c.memoryPassword = memguard.NewBufferFromBytes([]byte(pass))
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) GetMemPass() string {
|
func (c *Config) GetMemPass() (string, error) {
|
||||||
return c.memoryPassword
|
if c.memoryPassword == nil {
|
||||||
|
return "", errors.New("password not set")
|
||||||
|
}
|
||||||
|
return string(c.memoryPassword.Bytes()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) GetIdentity() *Identity {
|
func (c *Config) GetIdentity() *Identity {
|
||||||
@@ -102,5 +142,65 @@ func (c *Config) SaveIdentity() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) Clean() {
|
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{}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import (
|
|||||||
|
|
||||||
func TestConfigSave(t *testing.T) {
|
func TestConfigSave(t *testing.T) {
|
||||||
c := GetConfig()
|
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.IdentityFile = "test.id"
|
||||||
c.Chunksize = 10000000
|
c.Chunksize = 10000000
|
||||||
c.SavePassword = true
|
c.SavePassword = true
|
||||||
@@ -18,3 +21,61 @@ func TestConfigLoad(t *testing.T) {
|
|||||||
_ = GetConfig().Load("test.cfg")
|
_ = GetConfig().Load("test.cfg")
|
||||||
println(GetConfig().Chunksize)
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
51
client/dbmessage.go
Normal file
51
client/dbmessage.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
|
||||||
|
}
|
||||||
242
client/helpers/bgPollHelper.go
Normal file
242
client/helpers/bgPollHelper.go
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
package helpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"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
|
||||||
|
if fromServerMessage.Invitation != nil {
|
||||||
|
invitationGetAnswerReadResponse(fromServerMessage.Invitation)
|
||||||
|
}
|
||||||
|
// 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
|
||||||
|
usermsg, err := peer.ProcessInboundUserMessage(packedUserMessage.Payload, packedUserMessage.Signature)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, "ReadMessage: ProcessInboundUserMessage", err
|
||||||
|
}
|
||||||
|
|
||||||
|
//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))
|
||||||
|
// add message to storage
|
||||||
|
err = peer.StoreMessage(usermsg, filenames)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, "ReadMessage: StoreMessage", err
|
||||||
|
}
|
||||||
|
filenames = []string{}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
155
client/helpers/bgSendHelper.go
Normal file
155
client/helpers/bgSendHelper.go
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
package helpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"forge.redroom.link/yves/meowlib"
|
||||||
|
"forge.redroom.link/yves/meowlib/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
const maxRetriesPerServer = 3
|
||||||
|
const defaultSendTimeout = 3600 * 24 // seconds, used when job.Timeout is 0
|
||||||
|
|
||||||
|
// WriteSendJob enqueues a SendJob from the main Flutter isolate.
|
||||||
|
// It is a thin wrapper over client.PushSendJob and is safe to call
|
||||||
|
// concurrently with ProcessSendQueues.
|
||||||
|
func WriteSendJob(storagePath string, job *client.SendJob) error {
|
||||||
|
return client.PushSendJob(storagePath, job)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
func ProcessSendQueues(storagePath string) {
|
||||||
|
queueDir := filepath.Join(storagePath, "queues")
|
||||||
|
entries, err := os.ReadDir(queueDir)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn().Err(err).Str("dir", queueDir).Msg("ProcessSendQueues: ReadDir")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
wg.Add(1)
|
||||||
|
queue := entry.Name()
|
||||||
|
go func(q string) {
|
||||||
|
defer wg.Done()
|
||||||
|
processSendQueue(storagePath, q)
|
||||||
|
}(queue)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// processSendQueue processes pending jobs for a single named queue sequentially.
|
||||||
|
//
|
||||||
|
// For each pending job it will:
|
||||||
|
// - immediately mark it failed if its timeout has elapsed
|
||||||
|
// - attempt delivery, cycling through servers until one succeeds
|
||||||
|
// - mark it sent on success or failed when all servers are exhausted
|
||||||
|
// - stop and return when a job still has retries left (will resume on next call)
|
||||||
|
func processSendQueue(storagePath, queue string) {
|
||||||
|
for {
|
||||||
|
job, _, err := client.PeekSendJob(storagePath, queue)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error().Err(err).Str("queue", queue).Msg("processSendQueue: PeekSendJob")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if job == nil {
|
||||||
|
return // no more pending jobs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hard timeout: job has been sitting too long
|
||||||
|
if job.Timeout > 0 && time.Since(job.InsertedAt) > time.Duration(job.Timeout)*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
|
||||||
|
}
|
||||||
|
|
||||||
|
serverIdx, sendErr := attemptSendJob(job)
|
||||||
|
if sendErr == nil {
|
||||||
|
now := time.Now()
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
continue // job delivered – look for the next one
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist updated retry counts regardless of outcome
|
||||||
|
if err := client.UpdateSendJob(storagePath, queue, job); err != nil {
|
||||||
|
logger.Error().Err(err).Int64("id", job.ID).Msg("processSendQueue: UpdateSendJob retries")
|
||||||
|
}
|
||||||
|
|
||||||
|
if allServersExhausted(job) {
|
||||||
|
job.Status = client.SendStatusFailed
|
||||||
|
if err := client.UpdateSendJob(storagePath, queue, job); err != nil {
|
||||||
|
logger.Error().Err(err).Int64("id", job.ID).Msg("processSendQueue: UpdateSendJob failed")
|
||||||
|
}
|
||||||
|
continue // all servers dead for this job – try the next one
|
||||||
|
}
|
||||||
|
|
||||||
|
// Job still has remaining retries on some server; stop and wait for the next poll
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
// On the first successful POST it returns the server index.
|
||||||
|
// All retry counts are incremented in-place inside job.Retries.
|
||||||
|
func attemptSendJob(job *client.SendJob) (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(job.Retries) < len(job.Servers) {
|
||||||
|
job.Retries = append(job.Retries, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout := job.Timeout
|
||||||
|
if timeout <= 0 {
|
||||||
|
timeout = defaultSendTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, srv := range job.Servers {
|
||||||
|
if job.Retries[i] >= maxRetriesPerServer {
|
||||||
|
continue // this server is exhausted
|
||||||
|
}
|
||||||
|
_, err := meowlib.HttpPostMessage(srv.Url, data, timeout)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn().Err(err).Str("url", srv.Url).Int("retry", job.Retries[i]+1).Msg("attemptSendJob: POST failed")
|
||||||
|
job.Retries[i]++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
return -1, errors.New("all servers failed or exhausted")
|
||||||
|
}
|
||||||
|
|
||||||
|
// allServersExhausted returns true when every server in the job has been tried
|
||||||
|
// maxRetriesPerServer times without success.
|
||||||
|
func allServersExhausted(job *client.SendJob) bool {
|
||||||
|
if len(job.Servers) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for i := range job.Servers {
|
||||||
|
if i >= len(job.Retries) || job.Retries[i] < maxRetriesPerServer {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
337
client/helpers/bgSendHelper_test.go
Normal file
337
client/helpers/bgSendHelper_test.go
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
package helpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"forge.redroom.link/yves/meowlib/client"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
_ "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 dummy bytes to a temp file and returns the path.
|
||||||
|
func writeMsgFile(t *testing.T, dir, name string) string {
|
||||||
|
t.Helper()
|
||||||
|
p := filepath.Join(dir, name)
|
||||||
|
require.NoError(t, os.WriteFile(p, []byte("packed-server-message"), 0600))
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 ---------------------------------------------------------
|
||||||
|
|
||||||
|
func TestAllServersExhausted_NoServers(t *testing.T) {
|
||||||
|
job := &client.SendJob{}
|
||||||
|
assert.True(t, allServersExhausted(job))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAllServersExhausted_NoneExhausted(t *testing.T) {
|
||||||
|
job := &client.SendJob{
|
||||||
|
Servers: serverSlice("http://s1", "http://s2"),
|
||||||
|
Retries: []int{0, 0},
|
||||||
|
}
|
||||||
|
assert.False(t, allServersExhausted(job))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAllServersExhausted_PartiallyExhausted(t *testing.T) {
|
||||||
|
job := &client.SendJob{
|
||||||
|
Servers: serverSlice("http://s1", "http://s2"),
|
||||||
|
Retries: []int{maxRetriesPerServer, 0},
|
||||||
|
}
|
||||||
|
assert.False(t, allServersExhausted(job))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAllServersExhausted_AllExhausted(t *testing.T) {
|
||||||
|
job := &client.SendJob{
|
||||||
|
Servers: serverSlice("http://s1", "http://s2"),
|
||||||
|
Retries: []int{maxRetriesPerServer, maxRetriesPerServer},
|
||||||
|
}
|
||||||
|
assert.True(t, allServersExhausted(job))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAttemptSendJob_Success verifies a successful POST to the first server.
|
||||||
|
func TestAttemptSendJob_Success(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
var received int64
|
||||||
|
srv := acceptServer(t, &received)
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
job := &client.SendJob{
|
||||||
|
File: writeMsgFile(t, dir, "msg"),
|
||||||
|
Servers: serverSlice(srv.URL),
|
||||||
|
Timeout: 5,
|
||||||
|
Retries: []int{0},
|
||||||
|
}
|
||||||
|
|
||||||
|
idx, err := attemptSendJob(job)
|
||||||
|
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 := t.TempDir()
|
||||||
|
var received int64
|
||||||
|
good := acceptServer(t, &received)
|
||||||
|
defer good.Close()
|
||||||
|
|
||||||
|
job := &client.SendJob{
|
||||||
|
File: writeMsgFile(t, dir, "msg"),
|
||||||
|
Servers: serverSlice(closedServerURL(t), good.URL),
|
||||||
|
Timeout: 5,
|
||||||
|
Retries: []int{0, 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
idx, err := attemptSendJob(job)
|
||||||
|
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, job.Retries[0], "first server retry should be incremented")
|
||||||
|
assert.Equal(t, 0, job.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 := t.TempDir()
|
||||||
|
job := &client.SendJob{
|
||||||
|
File: writeMsgFile(t, dir, "msg"),
|
||||||
|
Servers: serverSlice(closedServerURL(t), closedServerURL(t)),
|
||||||
|
Timeout: 5,
|
||||||
|
Retries: []int{0, 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := attemptSendJob(job)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Equal(t, 1, job.Retries[0])
|
||||||
|
assert.Equal(t, 1, job.Retries[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAttemptSendJob_SkipsExhaustedServer verifies that a server already at
|
||||||
|
// maxRetriesPerServer is not contacted.
|
||||||
|
func TestAttemptSendJob_SkipsExhaustedServer(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
var received int64
|
||||||
|
good := acceptServer(t, &received)
|
||||||
|
defer good.Close()
|
||||||
|
|
||||||
|
job := &client.SendJob{
|
||||||
|
File: writeMsgFile(t, dir, "msg"),
|
||||||
|
Servers: serverSlice(
|
||||||
|
closedServerURL(t), // exhausted – must be skipped
|
||||||
|
good.URL,
|
||||||
|
),
|
||||||
|
Timeout: 5,
|
||||||
|
Retries: []int{maxRetriesPerServer, 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
idx, err := attemptSendJob(job)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 1, idx)
|
||||||
|
assert.Equal(t, int64(1), atomic.LoadInt64(&received))
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- integration tests --------------------------------------------------
|
||||||
|
|
||||||
|
// TestWriteSendJob verifies the thin WriteSendJob wrapper enqueues the job.
|
||||||
|
func TestWriteSendJob(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
err := WriteSendJob(dir, &client.SendJob{
|
||||||
|
Queue: "q1",
|
||||||
|
File: "/tmp/f",
|
||||||
|
Servers: serverSlice("http://s1"),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
got, _, err := client.PeekSendJob(dir, "q1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, got)
|
||||||
|
assert.Equal(t, "/tmp/f", got.File)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 := t.TempDir()
|
||||||
|
var received int64
|
||||||
|
srv := acceptServer(t, &received)
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
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 := t.TempDir()
|
||||||
|
var received int64
|
||||||
|
good := acceptServer(t, &received)
|
||||||
|
defer good.Close()
|
||||||
|
|
||||||
|
msgPath := writeMsgFile(t, dir, "msg")
|
||||||
|
pushJob(t, dir, "q1", msgPath, serverSlice(closedServerURL(t), 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_AllServersExhausted verifies that after maxRetriesPerServer
|
||||||
|
// failed attempts per server the job is marked as failed.
|
||||||
|
func TestProcessSendQueues_AllServersExhausted(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
deadURL := closedServerURL(t)
|
||||||
|
|
||||||
|
msgPath := writeMsgFile(t, dir, "msg")
|
||||||
|
pushJob(t, dir, "q1", msgPath, serverSlice(deadURL), 0)
|
||||||
|
|
||||||
|
_, id, err := client.PeekSendJob(dir, "q1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Each call to ProcessSendQueues increments the retry counter by 1.
|
||||||
|
// After maxRetriesPerServer calls, all servers are exhausted → failed.
|
||||||
|
for i := 0; i < maxRetriesPerServer; i++ {
|
||||||
|
ProcessSendQueues(dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
job, err := client.GetSendJob(dir, "q1", id)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, job)
|
||||||
|
assert.Equal(t, client.SendStatusFailed, job.Status)
|
||||||
|
assert.Equal(t, maxRetriesPerServer, job.Retries[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 := t.TempDir()
|
||||||
|
var received int64
|
||||||
|
srv := acceptServer(t, &received)
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
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 := t.TempDir()
|
||||||
|
var received int64
|
||||||
|
srv := acceptServer(t, &received)
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
for _, q := range []string{"qa", "qb", "qc"} {
|
||||||
|
msgPath := writeMsgFile(t, dir, "msg_"+q)
|
||||||
|
pushJob(t, dir, q, msgPath, serverSlice(srv.URL), 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
55
client/helpers/call.go
Normal file
55
client/helpers/call.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
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() {
|
||||||
|
|
||||||
|
}
|
||||||
1
client/helpers/contactHelper.go
Normal file
1
client/helpers/contactHelper.go
Normal file
@@ -0,0 +1 @@
|
|||||||
|
package helpers
|
||||||
161
client/helpers/invitationAnswerHelper.go
Normal file
161
client/helpers/invitationAnswerHelper.go
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
package helpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"C"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"forge.redroom.link/yves/meowlib"
|
||||||
|
"forge.redroom.link/yves/meowlib/client"
|
||||||
|
)
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InvitationAnswer
|
||||||
|
func InvitationAnswer(cc *meowlib.ContactCard, nickname string, myNickname string, serverUids []string) (*client.Peer, string, error) {
|
||||||
|
|
||||||
|
mynick := myNickname
|
||||||
|
// my nickname for that contact
|
||||||
|
|
||||||
|
if myNickname == "" {
|
||||||
|
mynick = client.GetConfig().GetIdentity().Nickname
|
||||||
|
}
|
||||||
|
|
||||||
|
// build my contact card for that friend
|
||||||
|
peer, err := client.GetConfig().GetIdentity().AnswerInvitation(mynick, nickname, serverUids, cc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "InvitationAnswer: AnswerInvitation", err
|
||||||
|
}
|
||||||
|
|
||||||
|
//peerstr, err := json.Marshal(peer)
|
||||||
|
//fmt.Println("InvitationAnswer: " + string(peerstr))
|
||||||
|
c := client.GetConfig()
|
||||||
|
c.GetIdentity().Save()
|
||||||
|
|
||||||
|
return peer, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InvitationAnswerFile
|
||||||
|
func InvitationAnswerFile(invitationFile string, nickname string, myNickname string, serverUids []string) (string, error) {
|
||||||
|
format := "qr"
|
||||||
|
var filename string = ""
|
||||||
|
var cc *meowlib.ContactCard
|
||||||
|
c := client.GetConfig()
|
||||||
|
if _, err := os.Stat(invitationFile); os.IsNotExist(err) {
|
||||||
|
return "InvitationAnswerFile : os.Stat", err
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(invitationFile, ".mwiv") {
|
||||||
|
format = "mwiv"
|
||||||
|
data, err := os.ReadFile(invitationFile)
|
||||||
|
if err != nil {
|
||||||
|
return "InvitationAnswerFile : os.ReadFile", err
|
||||||
|
}
|
||||||
|
cc, err = meowlib.NewContactCardFromCompressed(data)
|
||||||
|
if err != nil {
|
||||||
|
return "InvitationAnswerFile : NewContactCardFromCompressed", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
identity := client.GetConfig().GetIdentity()
|
||||||
|
if cc != nil {
|
||||||
|
isAnswer, proposed, received, _ := identity.CheckInvitation(cc)
|
||||||
|
if isAnswer {
|
||||||
|
fmt.Fprintln(os.Stdout, "This is already a response "+proposed+" to your invitation.")
|
||||||
|
fmt.Fprintln(os.Stdout, "You cannot answer again.")
|
||||||
|
fmt.Fprintln(os.Stdout, "You should finalize it by importing "+proposed+" contact card to your meow.")
|
||||||
|
fmt.Fprintln(os.Stdout, "Use : 'meow invitation finalize "+invitationFile+"' to do it.")
|
||||||
|
|
||||||
|
} else {
|
||||||
|
mynick := myNickname
|
||||||
|
// my nickname for that contact
|
||||||
|
|
||||||
|
if myNickname == "" {
|
||||||
|
mynick = client.GetConfig().GetIdentity().Nickname
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := identity.AnswerInvitation(mynick, nickname, serverUids, cc)
|
||||||
|
if err != nil {
|
||||||
|
return "InvitationAnswerFile : AnswerInvitation", err
|
||||||
|
}
|
||||||
|
fmt.Fprintln(os.Stdout, "Invitation sent by "+received)
|
||||||
|
if format == "qr" {
|
||||||
|
filename = c.StoragePath + string(os.PathSeparator) + mynick + "-" + nickname + ".png"
|
||||||
|
response.GetMyContact().WriteQr(filename)
|
||||||
|
} else {
|
||||||
|
filename = c.StoragePath + string(os.PathSeparator) + mynick + "-" + nickname + ".mwiv"
|
||||||
|
response.GetMyContact().WriteCompressed(filename)
|
||||||
|
}
|
||||||
|
client.GetConfig().GetIdentity().Save()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InvitationAnswerMessage
|
||||||
|
func InvitationAnswerMessage(invitationId string, invitationServerUid string, timeout int) ([]byte, string, error) {
|
||||||
|
|
||||||
|
// find the peer with that invitation id
|
||||||
|
/*var peer *client.Peer
|
||||||
|
for i := len(client.GetConfig().GetIdentity().Peers) - 1; i >= 0; i-- { //! to allow self invitation : testing only, findinc the received peer before myself
|
||||||
|
// for i := 0; i < len(client.GetConfig().GetIdentity().Peers); i++ {
|
||||||
|
if client.GetConfig().GetIdentity().Peers[i].InvitationId == invitationId {
|
||||||
|
peer = client.GetConfig().GetIdentity().Peers[i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
peer := client.GetConfig().GetIdentity().Peers.GetFromInvitationId(invitationId)
|
||||||
|
|
||||||
|
if peer == nil {
|
||||||
|
// declare a custom go error for no peer found
|
||||||
|
return nil, "InvitationAnswerMessage: loop for peer", errors.New("no peer with that invitation id")
|
||||||
|
}
|
||||||
|
answermsg, err := peer.BuildInvitationAnswerMessage(peer.GetMyContact())
|
||||||
|
if err != nil {
|
||||||
|
return nil, "InvitationAnswerMessage: BuildInvitationAnswserMessage", err
|
||||||
|
}
|
||||||
|
// Server: get the invitation server
|
||||||
|
invitationServer, err := client.GetConfig().GetIdentity().MessageServers.LoadServer(invitationServerUid)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "InvitationAnswerMessage: LoadServer", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// this will be the invitation's payload
|
||||||
|
packedMsg, err := peer.ProcessOutboundUserMessage(answermsg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "InvitationAnswerMessage: ProcessOutboundUserMessage", err
|
||||||
|
}
|
||||||
|
// Creating Server message for transporting the user message
|
||||||
|
toServerMessage, err := invitationServer.BuildToServerMessageInvitationAnswer(packedMsg, peer.MyIdentity.Public, invitationId, timeout)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "InvitationAnswerMessage: BuildToServerMessageInvitationAnswer", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server outbound processing
|
||||||
|
bytemsg, err := invitationServer.ProcessOutboundMessage(toServerMessage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "InvitationAnswerMessage: ProcessOutboundMessage", err
|
||||||
|
}
|
||||||
|
return bytemsg, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InvitationAnswerMessageReadResponse
|
||||||
|
// Called by the invitation receiver
|
||||||
|
// invitationData: the data received from the server
|
||||||
|
// invitationServerUid: the uid of the server holding the invitation
|
||||||
|
func InvitationAnswerMessageReadResponse(invitationData []byte, invitationServerUid string) (*meowlib.Invitation, string, error) {
|
||||||
|
|
||||||
|
server, err := client.GetConfig().GetIdentity().MessageServers.LoadServer(invitationServerUid)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "InvitationAnswerMessageReadResponse: LoadServer", err
|
||||||
|
}
|
||||||
|
// Server inbound processing : get the invitation server
|
||||||
|
serverMsg, err := server.ProcessInboundServerResponse(invitationData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "InvitationAnswerMessageReadResponse: ProcessInboundServerResponse", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return serverMsg.Invitation, "", nil
|
||||||
|
|
||||||
|
}
|
||||||
133
client/helpers/invitationCheckHelper.go
Normal file
133
client/helpers/invitationCheckHelper.go
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
package helpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"forge.redroom.link/yves/meowlib"
|
||||||
|
"forge.redroom.link/yves/meowlib/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InvitationCheck
|
||||||
|
// todo
|
||||||
|
/*
|
||||||
|
func InvitationCheck(invitationdata []byte) *C.char {
|
||||||
|
var jsoninv map[string]interface{}
|
||||||
|
err := json.Unmarshal([]byte(C.GoString(invitationdata)), &jsoninv)
|
||||||
|
if err != nil {
|
||||||
|
return C.CString(errorToJson(err, "InvitationCheck: "))
|
||||||
|
}
|
||||||
|
var cc *meowlib.ContactCard
|
||||||
|
if _, err := os.Stat(jsoninv["filename"].(string)); os.IsNotExist(err) {
|
||||||
|
return C.CString(errorToJson(err, "InvitationCheck: "))
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(jsoninv["filename"].(string), ".mwiv") {
|
||||||
|
data, err := os.ReadFile(jsoninv["filename"].(string))
|
||||||
|
if err != nil {
|
||||||
|
return C.CString(errorToJson(err, "InvitationCheck: "))
|
||||||
|
}
|
||||||
|
cc, err = meowlib.NewContactCardFromCompressed(data)
|
||||||
|
if err != nil {
|
||||||
|
return C.CString(errorToJson(err, "InvitationCheck: "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
identity := client.GetConfig().GetIdentity()
|
||||||
|
result := map[string]string{}
|
||||||
|
if cc != nil {
|
||||||
|
isAnswer, proposed, received, invitationMessage := identity.CheckInvitation(cc)
|
||||||
|
if isAnswer { // answer to infitation
|
||||||
|
result["type"] = "answer"
|
||||||
|
result["to"] = proposed
|
||||||
|
result["from"] = received
|
||||||
|
result["invitation_message"] = invitationMessage
|
||||||
|
//fmt.Fprintln(os.Stdout, "Invitation sent to "+proposed+" received with "+received+" as suggested nickname")
|
||||||
|
} else { // finalization message
|
||||||
|
result["type"] = "finalize"
|
||||||
|
result["from"] = received
|
||||||
|
//fmt.Fprintln(os.Stdout, "Invitation sent by "+received)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
val, err := json.Marshal(result)
|
||||||
|
if err != nil {
|
||||||
|
return C.CString(errorToJson(err, "InvitationCheck: "))
|
||||||
|
}
|
||||||
|
return C.CString(string(val))
|
||||||
|
}*/
|
||||||
|
|
||||||
|
// InvitationGetMessage
|
||||||
|
// Called by the invitation receiver
|
||||||
|
// invitationUrl: the url of server holding the invitation
|
||||||
|
// serverPublicKey: the public key of the server holding the invitation
|
||||||
|
// invitationPassword: the password of the invitation
|
||||||
|
func InvitationGetMessage(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, "InvitationGetMessage: CreateServerFromMeowUrl", err
|
||||||
|
}
|
||||||
|
// check if already in msg servers
|
||||||
|
dbsrv, err := client.GetConfig().GetIdentity().MessageServers.LoadServer(srv.Url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "InvitationGetMessage: LoadServer", err
|
||||||
|
}
|
||||||
|
if dbsrv == nil {
|
||||||
|
// create a server object with url & pubkey
|
||||||
|
srv.PublicKey = serverPublicKey
|
||||||
|
k, err := meowlib.NewKeyPair()
|
||||||
|
if err != nil {
|
||||||
|
return nil, "InvitationGetMessage: NewKeyPair", err
|
||||||
|
}
|
||||||
|
srv.UserKp = k
|
||||||
|
// save it
|
||||||
|
err = client.GetConfig().GetIdentity().MessageServers.StoreServer(srv)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "InvitationGetMessage: StoreServer", err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if dbsrv.PublicKey != serverPublicKey {
|
||||||
|
dbsrv.PublicKey = serverPublicKey
|
||||||
|
}
|
||||||
|
srv = dbsrv
|
||||||
|
}
|
||||||
|
// buildserver message
|
||||||
|
toSrvMsg, err := srv.BuildToServerMessageInvitationRequest(shortcode, invitationPassword)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "InvitationGetMessage: BuildToServerMessageInvitationRequest", err
|
||||||
|
}
|
||||||
|
// processoutbound
|
||||||
|
bytemsg, err := srv.ProcessOutboundMessage(toSrvMsg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "InvitationGetMessage: ProcessOutboundMessage", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytemsg, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InvitationGetMessageReadResponse
|
||||||
|
// Called by the invitation receiver
|
||||||
|
// invitationData: the data received from the server
|
||||||
|
// invitationServerUid: the uid of the server holding the invitation
|
||||||
|
func InvitationGetMessageReadResponse(invitationData []byte, invitationServerUid string) (*meowlib.ContactCard, string, error) {
|
||||||
|
|
||||||
|
server, err := client.GetConfig().GetIdentity().MessageServers.LoadServer(invitationServerUid)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "InvitationGetMessageReadResponse: LoadServer", err
|
||||||
|
}
|
||||||
|
// Server inbound processing : get the invitation server
|
||||||
|
serverMsg, err := server.ProcessInboundServerResponse(invitationData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "InvitationGetMessageReadResponse: ProcessInboundServerResponse", err
|
||||||
|
}
|
||||||
|
// fmt.Println("Inbound OK, Invitation Step: ", serverMsg.Invitation.Step, len(serverMsg.Invitation.Payload))
|
||||||
|
// fmt.Println("Invitation Check")
|
||||||
|
// fmt.Println(hex.EncodeToString(serverMsg.Invitation.Payload))
|
||||||
|
// contactCard decode
|
||||||
|
cc, err := meowlib.NewContactCardFromCompressed(serverMsg.Invitation.Payload)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "InvitationGetMessageReadResponse: NewContactCardFromCompressed", err
|
||||||
|
}
|
||||||
|
return cc, "", nil
|
||||||
|
}
|
||||||
143
client/helpers/invitationCreateHelper.go
Normal file
143
client/helpers/invitationCreateHelper.go
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
package helpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"forge.redroom.link/yves/meowlib"
|
||||||
|
"forge.redroom.link/yves/meowlib/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InvitationCreatePeer creates a new peer and returns it
|
||||||
|
// Called by invitation initiator
|
||||||
|
// name: the name of the peer
|
||||||
|
// myNickname: my nickname for that peer
|
||||||
|
// invitationMessage: the message to send to the peer
|
||||||
|
// serverUids: the list of server uids
|
||||||
|
func InvitationCreatePeer(name string, myNickname string, invitationMessage string, serverUids []string) (*client.Peer, string, error) {
|
||||||
|
|
||||||
|
mynick := myNickname
|
||||||
|
if myNickname == "" {
|
||||||
|
mynick = client.GetConfig().GetIdentity().Nickname
|
||||||
|
}
|
||||||
|
|
||||||
|
// build my contact card for that friend
|
||||||
|
peer, err := client.GetConfig().GetIdentity().InvitePeer(mynick, name, serverUids, invitationMessage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "InvitationCreate: InvitePeer", err
|
||||||
|
}
|
||||||
|
client.GetConfig().GetIdentity().Save()
|
||||||
|
|
||||||
|
return peer, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InvitationCreateFile creates a new peer and writes the invitation to a file
|
||||||
|
// Called by invitation initiator
|
||||||
|
// name: the name of the peer
|
||||||
|
// myNickname: my nickname for that peer
|
||||||
|
// invitationMessage: the message to send to the peer
|
||||||
|
// serverUids: the list of server uids
|
||||||
|
// format: the format of the file (qr or mwiv)
|
||||||
|
func InvitationCreateFile(name string, myNickname string, invitationMessage string, serverUids []string, format string) (*client.Peer, string, error) {
|
||||||
|
|
||||||
|
peer, errdata, err := InvitationCreatePeer(name, myNickname, invitationMessage, serverUids)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errdata, err
|
||||||
|
}
|
||||||
|
c := client.GetConfig()
|
||||||
|
var filename string = ""
|
||||||
|
if format == "qr" {
|
||||||
|
filename = c.StoragePath + string(os.PathSeparator) + peer.MyName + "-" + peer.Name + ".png"
|
||||||
|
err := peer.GetMyContact().WriteQr(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "InvitationCreateFile: WriteQr", err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
filename = c.StoragePath + string(os.PathSeparator) + peer.MyName + "-" + peer.Name + ".mwiv"
|
||||||
|
err := peer.GetMyContact().WriteCompressed(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "InvitationCreateFile: WriteCompressed", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return peer, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InvitationCreateMessage creates a new invitation message for an invited peer
|
||||||
|
// Called by invitation initiator
|
||||||
|
// invitationId: the invitation id of the peer
|
||||||
|
// invitationServerUid: the uid of the server for sending the invitation
|
||||||
|
// timeOut: the timeout for the invitation
|
||||||
|
// urlLen: the length of the invitation url
|
||||||
|
// password: the password for the invitation
|
||||||
|
func InvitationCreateMessage(invitationId string, invitationServerUid string, timeOut int, urlLen int, password string) ([]byte, string, error) {
|
||||||
|
|
||||||
|
// lookup for peer with "invitation_id"
|
||||||
|
var myContact *meowlib.ContactCard
|
||||||
|
/* for i := 0; i < len(client.GetConfig().GetIdentity().Peers); i++ {
|
||||||
|
if client.GetConfig().GetIdentity().Peers[i].InvitationId == invitationId {
|
||||||
|
myContact = client.GetConfig().GetIdentity().Peers[i].GetMyContact()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
peer := client.GetConfig().GetIdentity().Peers.GetFromInvitationId(invitationId)
|
||||||
|
myContact = peer.GetMyContact()
|
||||||
|
// todo handle not found !!
|
||||||
|
// lookup for message server with "invitation_server"
|
||||||
|
invitationServer, err := client.GetConfig().GetIdentity().MessageServers.LoadServer(invitationServerUid) //.GetServerByIdx(int(jsoninv["invitation_server"].(float64)))
|
||||||
|
if err != nil {
|
||||||
|
return nil, "InvitationCreateMessage: LoadServer", err
|
||||||
|
}
|
||||||
|
// call server.buildinviattion
|
||||||
|
msg, err := invitationServer.BuildToServerMessageInvitationCreation(myContact, password, timeOut, urlLen)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "InvitationCreateMessage: BuildToServerMessageInvitationCreation", err
|
||||||
|
}
|
||||||
|
// fmt.Println("Invitation Create")
|
||||||
|
// fmt.Println(hex.EncodeToString(msg.Invitation.Payload))
|
||||||
|
bytemsg, err := invitationServer.ProcessOutboundMessage(msg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "InvitationCreateMessage: ProcessOutboundMessage", err
|
||||||
|
}
|
||||||
|
return bytemsg, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InvitationCreateReadResponse reads the response of an invitation creation (url, expiry)
|
||||||
|
// Called by invitation initiator
|
||||||
|
// invitationServerUid: the uid of the server where we sent the invitation
|
||||||
|
// invitationResponse: the response we got from the server
|
||||||
|
func InvitationCreateReadResponse(invitationServerUid string, invitationResponse []byte) (*meowlib.Invitation, string, error) {
|
||||||
|
|
||||||
|
server, err := client.GetConfig().GetIdentity().MessageServers.LoadServer(invitationServerUid)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "InvitationCreateReadResponse: LoadServer", err
|
||||||
|
}
|
||||||
|
serverMsg, err := server.ProcessInboundServerResponse(invitationResponse)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "InvitationCreateReadResponse: ProcessInboundServerResponse", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return serverMsg.Invitation, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InvitationSetUrlInfo sets the url info for an invitation
|
||||||
|
// Called by invitation initiator
|
||||||
|
// invitationId: the invitation id of the peer
|
||||||
|
// url: the url of the invitation we got from the server
|
||||||
|
func InvitationSetUrlInfo(invitationId string, url string, expiry int64) {
|
||||||
|
id := client.GetConfig().GetIdentity()
|
||||||
|
// lookup for peer with "invitation_id"
|
||||||
|
peer := id.Peers.GetFromInvitationId(invitationId)
|
||||||
|
peer.InvitationUrl = url
|
||||||
|
peer.InvitationExpiry = time.Unix(expiry, 0)
|
||||||
|
id.Peers.StorePeer(peer)
|
||||||
|
|
||||||
|
/* for i := 0; i < len(client.GetConfig().GetIdentity().Peers); i++ {
|
||||||
|
if client.GetConfig().GetIdentity().Peers[i].InvitationId == invitationId {
|
||||||
|
client.GetConfig().GetIdentity().Peers[i].InvitationUrl = url
|
||||||
|
client.GetConfig().GetIdentity().Peers[i].InvitationExpiry = time.Unix(expiry, 0)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
client.GetConfig().GetIdentity().Save()*/
|
||||||
|
|
||||||
|
}
|
||||||
53
client/helpers/invitationFinalizeHelper.go
Normal file
53
client/helpers/invitationFinalizeHelper.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package helpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"forge.redroom.link/yves/meowlib"
|
||||||
|
"forge.redroom.link/yves/meowlib/client"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Got it by the message background check
|
||||||
|
// => noInvitationGetAnswer
|
||||||
|
|
||||||
|
// invitationGetAnswerReadResponse
|
||||||
|
// Called by the initiator's background service only
|
||||||
|
// invitationAnswerData: the data received from the server
|
||||||
|
// invitationServerUid: the uid of the server holding the invitation
|
||||||
|
func invitationGetAnswerReadResponse(invitation *meowlib.Invitation) (*client.Peer, string, error) {
|
||||||
|
|
||||||
|
// decode the payload
|
||||||
|
var invitationAnswer meowlib.PackedUserMessage
|
||||||
|
err := proto.Unmarshal(invitation.Payload, &invitationAnswer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "InvitationGetAnswerReadResponse: Unmarshal", err
|
||||||
|
}
|
||||||
|
// retreive user public key to check usermessage signature
|
||||||
|
// contactPublikKey := serverMsg.Invitation.From
|
||||||
|
peer := client.GetConfig().GetIdentity().Peers.GetFromInvitationId(invitation.Uuid)
|
||||||
|
peer.ContactPublicKey = invitation.From
|
||||||
|
if peer != nil {
|
||||||
|
|
||||||
|
// process the packed user message
|
||||||
|
usermsg, err := peer.ProcessInboundUserMessage(invitationAnswer.Payload, invitationAnswer.Signature)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "InvitationGetAnswerReadResponse: ProcessInboundUserMessage", err
|
||||||
|
}
|
||||||
|
decodedInvitation := usermsg.Invitation
|
||||||
|
var cc meowlib.ContactCard
|
||||||
|
err = proto.Unmarshal(decodedInvitation.Payload, &cc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "InvitationGetAnswerReadResponse: Unmarshal", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// finalize the invitation
|
||||||
|
// id := client.GetConfig().GetIdentity()
|
||||||
|
peer.ContactLookupKey = cc.ContactPublicKey
|
||||||
|
peer.ContactEncryption = cc.EncryptionPublicKey
|
||||||
|
for _, server := range cc.PullServers {
|
||||||
|
peer.ContactPullServers = append(peer.ContactPullServers, server.GetUid())
|
||||||
|
}
|
||||||
|
client.GetConfig().GetIdentity().Save()
|
||||||
|
|
||||||
|
}
|
||||||
|
return peer, "", nil
|
||||||
|
}
|
||||||
12
client/helpers/logger.go
Normal file
12
client/helpers/logger.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
86
client/helpers/messageHelper.go
Normal file
86
client/helpers/messageHelper.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
package helpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"forge.redroom.link/yves/meowlib"
|
||||||
|
"forge.redroom.link/yves/meowlib/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
func messageBuildPackAndStore(msg *meowlib.UserMessage, srvuid string, peer *client.Peer) ([]byte, string, error) {
|
||||||
|
// Get the message server
|
||||||
|
srv, err := client.GetConfig().GetIdentity().MessageServers.LoadServer(srvuid)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "messageBuildPostprocess : LoadServer", err
|
||||||
|
}
|
||||||
|
// Prepare cyphered + packed user message
|
||||||
|
packedMsg, err := peer.ProcessOutboundUserMessage(msg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "messageBuildPostprocess : ProcessOutboundUserMessage", err
|
||||||
|
}
|
||||||
|
// Creating Server message for transporting the user message
|
||||||
|
toServerMessage := srv.BuildToServerMessageFromUserMessage(packedMsg)
|
||||||
|
data, err := srv.ProcessOutboundMessage(toServerMessage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "messageBuildPostprocess : ProcessOutboundMessage", err
|
||||||
|
}
|
||||||
|
// Store message
|
||||||
|
err = peer.StoreMessage(msg, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "messageBuildPostprocess : StoreMessage", err
|
||||||
|
}
|
||||||
|
return data, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateUserMessage(message string, 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([]byte(message))
|
||||||
|
if err != nil {
|
||||||
|
return nil, "PrepareServerMessage : BuildSimpleUserMessage", err
|
||||||
|
}
|
||||||
|
for _, file := range filelist {
|
||||||
|
err = usermessage.AddFile(file, client.GetConfig().Chunksize)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "PrepareServerMessage : AddFile", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
usermessage.Status.AnswerToUuid = replyToUid
|
||||||
|
|
||||||
|
return messageBuildPackAndStore(usermessage, srvuid, peer)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildAckMessage(messageUid string, srvuid string, peer_uid string, received int64, processed int64) ([]byte, string, error) {
|
||||||
|
|
||||||
|
peer := client.GetConfig().GetIdentity().Peers.GetFromUid(peer_uid)
|
||||||
|
srv, err := client.GetConfig().GetIdentity().MessageServers.LoadServer(srvuid)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "PrepareServerMessage : LoadServer", err
|
||||||
|
}
|
||||||
|
// Creating User message
|
||||||
|
usermessage, err := peer.BuildSimpleUserMessage(nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "PrepareServerMessage : BuildSimpleUserMessage", err
|
||||||
|
}
|
||||||
|
usermessage.Status.Uuid = messageUid
|
||||||
|
usermessage.Status.Received = uint64(received)
|
||||||
|
usermessage.Status.Processed = uint64(processed)
|
||||||
|
// Prepare cyphered + packed user message
|
||||||
|
packedMsg, err := peer.ProcessOutboundUserMessage(usermessage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "PrepareServerMessage : ProcessOutboundUserMessage", err
|
||||||
|
}
|
||||||
|
// Creating Server message for transporting the user message
|
||||||
|
toServerMessage := srv.BuildToServerMessageFromUserMessage(packedMsg)
|
||||||
|
data, err := srv.ProcessOutboundMessage(toServerMessage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "PrepareServerMessage : ProcessOutboundMessage", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadAckMessageResponse() {
|
||||||
|
//! update the status in message store
|
||||||
|
}
|
||||||
30
client/helpers/networkHelper.go
Normal file
30
client/helpers/networkHelper.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
1
client/helpers/serverHelper.go
Normal file
1
client/helpers/serverHelper.go
Normal file
@@ -0,0 +1 @@
|
|||||||
|
package helpers
|
||||||
15
client/helpers/storageHelper.go
Normal file
15
client/helpers/storageHelper.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -5,6 +5,9 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"forge.redroom.link/yves/meowlib"
|
"forge.redroom.link/yves/meowlib"
|
||||||
@@ -14,114 +17,209 @@ import (
|
|||||||
|
|
||||||
const maxHiddenCount = 30
|
const maxHiddenCount = 30
|
||||||
|
|
||||||
|
// Package-level random number generator with mutex for thread-safe access
|
||||||
|
var (
|
||||||
|
rngMu sync.Mutex
|
||||||
|
rng = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
)
|
||||||
|
|
||||||
type Identity struct {
|
type Identity struct {
|
||||||
Nickname string `json:"nickname,omitempty"`
|
Nickname string `json:"nickname,omitempty"`
|
||||||
DefaultAvatarUuid string `json:"default_avatar_uuid,omitempty"`
|
DefaultAvatar string `json:"default_avatar,omitempty"`
|
||||||
RootKp meowlib.KeyPair `json:"id_kp,omitempty"`
|
Avatars []Avatar `json:"avatars,omitempty"`
|
||||||
Status string `json:"status,omitempty"`
|
RootKp *meowlib.KeyPair `json:"id_kp,omitempty"`
|
||||||
Peers PeerList `json:"peers,omitempty"`
|
Status string `json:"status,omitempty"`
|
||||||
HiddenPeers [][]byte `json:"hiddend_peers,omitempty"`
|
Peers PeerStorage `json:"peers,omitempty"`
|
||||||
Device meowlib.KeyPair `json:"device,omitempty"`
|
HiddenPeers [][]byte `json:"hidden_peers,omitempty"`
|
||||||
KnownServers ServerList `json:"known_servers,omitempty"`
|
Personae PeerList `json:"faces,omitempty"`
|
||||||
MessageServers ServerList `json:"message_servers,omitempty"`
|
Device *meowlib.KeyPair `json:"device,omitempty"`
|
||||||
DefaultDbPassword string `json:"default_db_password,omitempty"`
|
KnownServers ServerList `json:"known_servers,omitempty"`
|
||||||
DbPasswordStore bool `json:"db_password_store,omitempty"`
|
MessageServers ServerStorage `json:"message_servers,omitempty"`
|
||||||
OwnedDevices PeerList `json:"owned_devices,omitempty"`
|
DefaultDbPassword string `json:"default_db_password,omitempty"`
|
||||||
StaticMtkServerPaths []ServerList `json:"static_mtk_server_paths,omitempty"`
|
DbPasswordStore bool `json:"db_password_store,omitempty"`
|
||||||
DynamicMtkServeRules []string `json:"dynamic_mtk_serve_rules,omitempty"`
|
OwnedDevices PeerList `json:"owned_devices,omitempty"`
|
||||||
InvitationTimeout int `json:"invitation_timeout,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
|
unlockedHiddenPeers PeerList
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateIdentity(nickname string) *Identity {
|
func CreateIdentity(nickname string) (*Identity, error) {
|
||||||
var id Identity
|
var id Identity
|
||||||
|
var err error
|
||||||
id.Nickname = nickname
|
id.Nickname = nickname
|
||||||
id.RootKp = meowlib.NewKeyPair()
|
id.Uuid = uuid.New().String()
|
||||||
|
id.RootKp, err = meowlib.NewKeyPair()
|
||||||
GetConfig().me = &id
|
GetConfig().me = &id
|
||||||
|
id.MessageServers = ServerStorage{DbFile: uuid.NewString()}
|
||||||
id.generateRandomHiddenStuff()
|
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) CreateFolder() error {
|
||||||
func (id *Identity) InvitePeer(MyName string, ContactName string, MessageServerIdxs []int) (*Peer, *meowlib.ContactCard, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates an invitation for a peer, returns the newly created peer including infos to provide a ContactCard
|
||||||
|
func (id *Identity) InvitePeer(MyName string, ContactName string, MessageServerUids []string, InvitationMessage string) (*Peer, error) {
|
||||||
var peer Peer
|
var peer Peer
|
||||||
var myContactCard meowlib.ContactCard
|
var err error
|
||||||
peer.MyIdentity = meowlib.NewKeyPair()
|
peer.Uid = uuid.New().String()
|
||||||
peer.MyEncryptionKp = meowlib.NewKeyPair()
|
peer.MyIdentity, err = meowlib.NewKeyPair()
|
||||||
peer.MyLookupKp = 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
|
||||||
|
}
|
||||||
peer.Name = ContactName
|
peer.Name = ContactName
|
||||||
peer.InvitationId = uuid.New().String()
|
peer.InvitationId = uuid.New().String() // todo as param to identify then update url
|
||||||
if id.MessageServers.Servers == nil {
|
/* if id.MessageServers.Servers == nil {
|
||||||
return nil, nil, errors.New("no message servers defined in your identity")
|
return 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 {
|
||||||
for _, i := range MessageServerIdxs {
|
if i > len(id.MessageServers.Servers)-1 {
|
||||||
srv := &id.MessageServers.Servers[i].ServerData
|
return nil, errors.New("requested server out of range of defined message servers")
|
||||||
myContactCard.PullServers = append(myContactCard.PullServers, srv)
|
}
|
||||||
}
|
}
|
||||||
myContactCard.Name = MyName
|
for _, i := range MessageServerIdxs {
|
||||||
myContactCard.ContactPublicKey = peer.MyIdentity.Public
|
srv := id.MessageServers.Servers[i].GetServerCard()
|
||||||
myContactCard.EncryptionPublicKey = peer.MyEncryptionKp.Public
|
peer.MyContact.PullServers = append(peer.MyContact.PullServers, srv)
|
||||||
myContactCard.LookupPublicKey = peer.MyLookupKp.Public
|
}*/
|
||||||
myContactCard.InvitationId = peer.InvitationId
|
/* pullServers, err := id.MessageServers.LoadServerCardsFromUids(MessageServerUids)
|
||||||
id.Peers = append(id.Peers, peer)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}*/
|
||||||
|
peer.MyPullServers = MessageServerUids
|
||||||
|
peer.MyName = MyName
|
||||||
|
peer.InvitationMessage = InvitationMessage
|
||||||
|
id.Peers.StorePeer(&peer)
|
||||||
|
|
||||||
return &peer, &myContactCard, nil
|
return &peer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (id *Identity) CheckInvitation(ReceivedContact *meowlib.ContactCard) (isAnswer bool, proposedNick string, receivedNick string) {
|
// Checks if the received contact card is an answer to an invitation, returns true if it is, and the proposed and received nicknames
|
||||||
for _, p := range id.Peers {
|
func (id *Identity) 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 id.Peers {
|
||||||
if p.InvitationId == ReceivedContact.InvitationId {
|
if p.InvitationId == ReceivedContact.InvitationId {
|
||||||
return true, p.Name, ReceivedContact.Name
|
return true, p.Name, ReceivedContact.Name, ReceivedContact.InvitationMessage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false, "", ReceivedContact.Name
|
|
||||||
|
// it's an invitation
|
||||||
|
return false, "", ReceivedContact.Name, ReceivedContact.InvitationMessage*/
|
||||||
|
return id.Peers.CheckInvitation(ReceivedContact)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (id *Identity) AnswerInvitation(MyName string, ContactName string, MessageServerIdxs []int, ReceivedContact *meowlib.ContactCard) *meowlib.ContactCard {
|
// Answers an invitation, returns the newly created peer including infos to provide a ContactCard
|
||||||
|
func (id *Identity) AnswerInvitation(MyName string, ContactName string, MessageServerIdxs []string, ReceivedContact *meowlib.ContactCard) (*Peer, error) {
|
||||||
var peer Peer
|
var peer Peer
|
||||||
var myContactCard meowlib.ContactCard
|
var err error
|
||||||
peer.MyIdentity = meowlib.NewKeyPair()
|
var newsrv *Server
|
||||||
peer.MyEncryptionKp = meowlib.NewKeyPair()
|
//var myContactCard meowlib.ContactCard
|
||||||
peer.MyLookupKp = meowlib.NewKeyPair()
|
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 != "" {
|
if ContactName != "" {
|
||||||
peer.Name = ContactName
|
peer.Name = ContactName
|
||||||
} else {
|
} else {
|
||||||
peer.Name = ReceivedContact.Name
|
peer.Name = ReceivedContact.Name
|
||||||
}
|
}
|
||||||
peer.Contact = *ReceivedContact
|
peer.ContactEncryption = ReceivedContact.EncryptionPublicKey
|
||||||
for _, i := range MessageServerIdxs {
|
peer.ContactLookupKey = ReceivedContact.LookupPublicKey
|
||||||
srv := id.MessageServers.Servers[i].ServerData
|
peer.ContactPublicKey = ReceivedContact.ContactPublicKey
|
||||||
myContactCard.PullServers = append(myContactCard.PullServers, &srv)
|
peer.InvitationId = ReceivedContact.InvitationId
|
||||||
|
peer.InvitationMessage = ReceivedContact.InvitationMessage
|
||||||
|
for srv := range ReceivedContact.PullServers {
|
||||||
|
peer.ContactPullServers = append(peer.ContactPullServers, ReceivedContact.PullServers[srv].GetUid())
|
||||||
|
newsrv, err = CreateServerFromUid(ReceivedContact.PullServers[srv].GetUid())
|
||||||
|
id.MessageServers.StoreServerIfNotExists(newsrv)
|
||||||
}
|
}
|
||||||
myContactCard.Name = MyName
|
/* for _, i := range MessageServerIdxs {
|
||||||
myContactCard.ContactPublicKey = peer.MyIdentity.Public
|
srv := id.MessageServers.Servers[i].GetServerCard()
|
||||||
myContactCard.EncryptionPublicKey = peer.MyEncryptionKp.Public
|
peer.MyContact.PullServers = append(peer.MyContact.PullServers, srv)
|
||||||
myContactCard.LookupPublicKey = peer.MyLookupKp.Public
|
}*/
|
||||||
myContactCard.InvitationId = ReceivedContact.InvitationId
|
/* srvCards, err := GetConfig().GetIdentity().MessageServers.LoadServerCardsFromUids(MessageServerIdxs)
|
||||||
|
if err != nil {
|
||||||
|
peer.MyContact.PullServers = srvCards
|
||||||
|
}*/
|
||||||
|
peer.MyPullServers = MessageServerIdxs
|
||||||
|
peer.MyName = MyName
|
||||||
|
peer.InvitationId = ReceivedContact.InvitationId
|
||||||
|
id.Peers.StorePeer(&peer)
|
||||||
|
|
||||||
id.Peers = append(id.Peers, peer)
|
return &peer, nil
|
||||||
|
|
||||||
return &myContactCard
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Finalizes an invitation, returns nil if successful
|
||||||
func (id *Identity) FinalizeInvitation(ReceivedContact *meowlib.ContactCard) error {
|
func (id *Identity) FinalizeInvitation(ReceivedContact *meowlib.ContactCard) error {
|
||||||
for i, p := range id.Peers {
|
var err error
|
||||||
|
var newsrv *Server
|
||||||
|
/*for i, p := range id.Peers {
|
||||||
if p.InvitationId == ReceivedContact.InvitationId {
|
if p.InvitationId == ReceivedContact.InvitationId {
|
||||||
id.Peers[i].Contact = *ReceivedContact
|
//id.Peers[i].Name = ReceivedContact.Name
|
||||||
|
id.Peers[i].ContactEncryption = ReceivedContact.EncryptionPublicKey
|
||||||
|
id.Peers[i].ContactLookupKey = ReceivedContact.LookupPublicKey
|
||||||
|
id.Peers[i].ContactPublicKey = ReceivedContact.ContactPublicKey
|
||||||
|
srvs := []string{}
|
||||||
|
for srv := range ReceivedContact.PullServers {
|
||||||
|
srvs = append(srvs, ReceivedContact.PullServers[srv].GetUid())
|
||||||
|
}
|
||||||
|
id.Peers[i].ContactPullServers = srvs
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return errors.New("no matching contact found for invitationId " + ReceivedContact.InvitationId)
|
return errors.New("no matching contact found for invitationId " + ReceivedContact.InvitationId)*/
|
||||||
|
for srv := range ReceivedContact.PullServers {
|
||||||
|
newsrv, err = CreateServerFromUid(ReceivedContact.PullServers[srv].GetUid())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
id.MessageServers.StoreServerIfNotExists(newsrv)
|
||||||
|
}
|
||||||
|
return id.Peers.FinalizeInvitation(ReceivedContact)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoadIdentity loads an identity from an encrypted file
|
||||||
func LoadIdentity(filename string, password string) (*Identity, error) {
|
func LoadIdentity(filename string, password string) (*Identity, error) {
|
||||||
var id Identity
|
var id Identity
|
||||||
GetConfig().memoryPassword = password
|
err := GetConfig().SetMemPass(password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
GetConfig().IdentityFile = filename
|
GetConfig().IdentityFile = filename
|
||||||
indata, err := os.ReadFile(filename)
|
indata, err := os.ReadFile(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -132,7 +230,13 @@ func LoadIdentity(filename string, password string) (*Identity, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
err = json.Unmarshal([]byte(pass), &id)
|
err = json.Unmarshal([]byte(pass), &id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
GetConfig().me = &id
|
GetConfig().me = &id
|
||||||
|
if id.Peers.DbFile != "" {
|
||||||
|
id.Peers.LoadPeers(password)
|
||||||
|
}
|
||||||
return &id, err
|
return &id, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,7 +245,11 @@ func (id *Identity) Save() error {
|
|||||||
return errors.New("identity filename empty")
|
return errors.New("identity filename empty")
|
||||||
}
|
}
|
||||||
b, _ := json.Marshal(id)
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -160,7 +268,7 @@ func (id *Identity) TryUnlockHidden(password string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
p.dbPassword = password
|
p.dbPassword = password
|
||||||
id.unlockedHiddenPeers = append(id.unlockedHiddenPeers, p)
|
id.unlockedHiddenPeers = append(id.unlockedHiddenPeers, &p)
|
||||||
found = true
|
found = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,6 +278,7 @@ func (id *Identity) TryUnlockHidden(password string) error {
|
|||||||
return errors.New("no peer found")
|
return errors.New("no peer found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
func (id *Identity) HidePeer(peerIdx int, password string) error {
|
func (id *Identity) HidePeer(peerIdx int, password string) error {
|
||||||
serializedPeer, err := json.Marshal(id.Peers[peerIdx])
|
serializedPeer, err := json.Marshal(id.Peers[peerIdx])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -184,39 +293,137 @@ func (id *Identity) HidePeer(peerIdx int, password string) error {
|
|||||||
// remove clear text peer
|
// remove clear text peer
|
||||||
id.Peers = append(id.Peers[:peerIdx], id.Peers[peerIdx+1:]...)
|
id.Peers = append(id.Peers[:peerIdx], id.Peers[peerIdx+1:]...)
|
||||||
return nil
|
return nil
|
||||||
}
|
}*/
|
||||||
|
|
||||||
func (id *Identity) generateRandomHiddenStuff() {
|
func (id *Identity) generateRandomHiddenStuff() error {
|
||||||
rand.Seed(time.Now().UnixNano())
|
var err error
|
||||||
count := rand.Intn(maxHiddenCount) + 1
|
rngMu.Lock()
|
||||||
|
count := rng.Intn(maxHiddenCount) + 1
|
||||||
|
rngMu.Unlock()
|
||||||
for i := 1; i < count; i++ {
|
for i := 1; i < count; i++ {
|
||||||
var p Peer
|
var p Peer
|
||||||
|
p.Uid = uuid.New().String()
|
||||||
p.Name = randomLenString(4, 20)
|
p.Name = randomLenString(4, 20)
|
||||||
p.MyEncryptionKp = meowlib.NewKeyPair()
|
p.MyEncryptionKp, err = meowlib.NewKeyPair()
|
||||||
p.MyIdentity = meowlib.NewKeyPair()
|
if err != nil {
|
||||||
p.MyLookupKp = meowlib.NewKeyPair()
|
return err
|
||||||
p.Contact.Name = randomLenString(4, 20)
|
}
|
||||||
p.Contact.ContactPublicKey = p.MyLookupKp.Public
|
p.MyIdentity, err = meowlib.NewKeyPair()
|
||||||
p.Contact.EncryptionPublicKey = p.MyIdentity.Public
|
if err != nil {
|
||||||
p.Contact.LookupPublicKey = p.MyEncryptionKp.Public
|
return err
|
||||||
p.Contact.AddUrls([]string{randomLenString(14, 60), randomLenString(14, 60)})
|
}
|
||||||
id.Peers = append(id.Peers, p)
|
p.MyLookupKp, err = meowlib.NewKeyPair()
|
||||||
id.HidePeer(0, randomLenString(8, 14))
|
if err != nil {
|
||||||
// TODO Add conversations
|
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 {
|
||||||
|
// check if peer inviation is accepted
|
||||||
|
for _, server := range peer.MyPullServers {
|
||||||
|
srvs[server].LookupKeys = append(srvs[server].LookupKeys, peer.MyLookupKp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 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 {
|
func randomLenString(min int, max int) string {
|
||||||
rand.Seed(time.Now().UnixNano())
|
rngMu.Lock()
|
||||||
n := rand.Intn(max-min) + min
|
defer rngMu.Unlock()
|
||||||
return randomString(n)
|
|
||||||
|
length := rng.Intn(max-min+1) + min
|
||||||
|
return randomStringLocked(length)
|
||||||
}
|
}
|
||||||
|
|
||||||
func randomString(n int) string {
|
func randomString(n int) string {
|
||||||
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
|
rngMu.Lock()
|
||||||
s := make([]rune, n)
|
defer rngMu.Unlock()
|
||||||
for i := range s {
|
|
||||||
s[i] = letters[rand.Intn(len(letters))]
|
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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ package client
|
|||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"forge.redroom.link/yves/meowlib"
|
"forge.redroom.link/yves/meowlib"
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/stretchr/testify/assert"
|
"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 := GetConfig()
|
||||||
config.IdentityFile = "test.id"
|
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 !
|
// ! Extension to quickly open db : Debug only !
|
||||||
config.DbSuffix = ".sqlite"
|
config.DbSuffix = ".sqlite"
|
||||||
id := CreateIdentity("myname")
|
id, err := CreateIdentity("myname")
|
||||||
err := id.Save()
|
if err != nil {
|
||||||
|
log.Fatal("CreateIdentity failed")
|
||||||
|
}
|
||||||
|
err = id.Save()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("Save failed")
|
log.Fatal("Save failed")
|
||||||
}
|
}
|
||||||
var p Peer
|
for i := range 10 {
|
||||||
p.Name = "testName"
|
var p Peer
|
||||||
p.MyEncryptionKp = meowlib.NewKeyPair()
|
p.Uid = uuid.New().String()
|
||||||
p.MyIdentity = meowlib.NewKeyPair()
|
p.Name = "testName_" + strconv.Itoa(i)
|
||||||
p.MyLookupKp = meowlib.NewKeyPair()
|
p.MyEncryptionKp, err = meowlib.NewKeyPair()
|
||||||
p.Contact.Name = "foo"
|
if err != nil {
|
||||||
p.Contact.ContactPublicKey = p.MyLookupKp.Public
|
t.Fatal(err)
|
||||||
p.Contact.EncryptionPublicKey = p.MyIdentity.Public
|
}
|
||||||
p.Contact.LookupPublicKey = p.MyEncryptionKp.Public
|
p.MyIdentity, err = meowlib.NewKeyPair()
|
||||||
p.Contact.AddUrls([]string{"http:/127.0.0.1/meow", "tcp://localhost:1234"})
|
if err != nil {
|
||||||
id.Peers = append(id.Peers, p)
|
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
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,7 +81,10 @@ func TestLoad(t *testing.T) {
|
|||||||
}
|
}
|
||||||
id, err := LoadIdentity("test.id", "toto")
|
id, err := LoadIdentity("test.id", "toto")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
id := CreateIdentity("myname")
|
id, err1 := CreateIdentity("myname")
|
||||||
|
if err1 != nil {
|
||||||
|
log.Fatal("CreateIdentity failed")
|
||||||
|
}
|
||||||
id.Save()
|
id.Save()
|
||||||
} else {
|
} else {
|
||||||
log.Println(id.Nickname)
|
log.Println(id.Nickname)
|
||||||
@@ -65,17 +101,58 @@ func TestLoad(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHidePeer(t *testing.T) {
|
func TestHidePeer(t *testing.T) {
|
||||||
id := createId()
|
/*
|
||||||
name := id.Peers[0].Name
|
id := createId()
|
||||||
assert.Equal(t, len(id.Peers), 1)
|
name := id.Peers[0].Name
|
||||||
h := len(id.HiddenPeers)
|
assert.Equal(t, len(id.Peers), 1)
|
||||||
id.HidePeer(0, "mypassword")
|
h := len(id.HiddenPeers)
|
||||||
assert.Equal(t, len(id.Peers), 0)
|
id.HidePeer(0, "mypassword")
|
||||||
assert.Equal(t, len(id.HiddenPeers), h+1)
|
assert.Equal(t, len(id.Peers), 0)
|
||||||
id.TryUnlockHidden("mypassword")
|
assert.Equal(t, len(id.HiddenPeers), h+1)
|
||||||
assert.Equal(t, len(id.unlockedHiddenPeers), 1)
|
id.TryUnlockHidden("mypassword")
|
||||||
assert.Equal(t, id.unlockedHiddenPeers[0].Name, name)
|
assert.Equal(t, len(id.unlockedHiddenPeers), 1)
|
||||||
if exists("test.id") {
|
assert.Equal(t, id.unlockedHiddenPeers[0].Name, name)
|
||||||
os.Remove("test.id")
|
|
||||||
}
|
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()
|
||||||
|
|
||||||
|
// Check that the returned list is as expected
|
||||||
|
assert.Equal(t, 6, len(jobs), "Expected 6 jobs")
|
||||||
|
|
||||||
|
// Check that each job has the correct server and lookup keys
|
||||||
|
for _, job := range jobs {
|
||||||
|
//fmt.Println(job.Server.GetUid(), job.LookupKeys)
|
||||||
|
assert.Contains(t, []string{"server1", "server2", "server3", "server4", "server5", "server6"}, job.Server.GetUid(), "Unexpected server UID")
|
||||||
|
assert.Len(t, job.LookupKeys, 1, "Expected 1 lookup key per job")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
// recursively remove the test.db folder
|
||||||
|
os.RemoveAll("test.db")
|
||||||
}
|
}
|
||||||
|
|||||||
50
client/internalusermessage.go
Normal file
50
client/internalusermessage.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
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 int64 `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
|
||||||
|
}
|
||||||
|
// Creating Server message for transporting the user message
|
||||||
|
toServerMessage := srv.BuildToServerMessageFromUserMessage(packedMsg)
|
||||||
|
return srv.ProcessOutboundMessage(toServerMessage)
|
||||||
|
}
|
||||||
12
client/logger.go
Normal file
12
client/logger.go
Normal file
@@ -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) {
|
func ProcessForOutput(usermessage *meowlib.UserMessage, peer *Peer, servers *ServerList, trackingLookupKey string) ([]byte, error) {
|
||||||
lastIdx := len(servers.Servers) - 1
|
lastIdx := len(servers.Servers) - 1
|
||||||
// LAST SERVER : Message delivery as usual
|
// LAST SERVER : Message delivery as usual
|
||||||
srv := &servers.Servers[lastIdx]
|
srv := servers.Servers[lastIdx]
|
||||||
// Prepare cyphered + packed user message
|
// Prepare cyphered + packed user message
|
||||||
packedMsg, err := peer.ProcessOutboundUserMessage(usermessage)
|
packedMsg, err := peer.ProcessOutboundUserMessage(usermessage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -23,17 +23,17 @@ func ProcessForOutput(usermessage *meowlib.UserMessage, peer *Peer, servers *Ser
|
|||||||
lastuuid := uuid.NewString()
|
lastuuid := uuid.NewString()
|
||||||
// ALL PREVIOUS SERVERS
|
// ALL PREVIOUS SERVERS
|
||||||
for i := lastIdx - 1; i >= 0; i-- {
|
for i := lastIdx - 1; i >= 0; i-- {
|
||||||
srv = &servers.Servers[i]
|
srv = servers.Servers[i]
|
||||||
var toServerMessage meowlib.ToServerMessage
|
var toServerMessage meowlib.ToServerMessage
|
||||||
toServerMessage.MatriochkaMessage.Data = lastmsg
|
toServerMessage.MatriochkaMessage.Data = lastmsg
|
||||||
toServerMessage.MatriochkaMessage.Next.Url = servers.Servers[i+1].ServerData.Url
|
toServerMessage.MatriochkaMessage.Next.Url = servers.Servers[i+1].Url
|
||||||
toServerMessage.MatriochkaMessage.Next.PublicKey = servers.Servers[i+1].ServerData.PublicKey
|
toServerMessage.MatriochkaMessage.Next.PublicKey = servers.Servers[i+1].PublicKey
|
||||||
toServerMessage.MatriochkaMessage.Next.Delay = int32(servers.Servers[i+1].AllowedDelay)
|
toServerMessage.MatriochkaMessage.Next.Delay = int32(servers.Servers[i+1].AllowedDelay)
|
||||||
if trackingLookupKey != "" {
|
if trackingLookupKey != "" {
|
||||||
toServerMessage.MatriochkaMessage.Next.Uuid = lastuuid // change tracking uuid at each server
|
toServerMessage.MatriochkaMessage.Next.Uuid = lastuuid // change tracking uuid at each server
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
toServerMessage.MatriochkaMessage.Prev.Url = servers.Servers[i-1].ServerData.Url
|
toServerMessage.MatriochkaMessage.Prev.Url = servers.Servers[i-1].Url
|
||||||
toServerMessage.MatriochkaMessage.Prev.PublicKey = servers.Servers[i+1].ServerData.PublicKey
|
toServerMessage.MatriochkaMessage.Prev.PublicKey = servers.Servers[i+1].PublicKey
|
||||||
toServerMessage.MatriochkaMessage.Prev.Delay = int32(servers.Servers[i-1].AllowedDelay)
|
toServerMessage.MatriochkaMessage.Prev.Delay = int32(servers.Servers[i-1].AllowedDelay)
|
||||||
toServerMessage.MatriochkaMessage.Prev.Uuid = uuid.NewString()
|
toServerMessage.MatriochkaMessage.Prev.Uuid = uuid.NewString()
|
||||||
lastuuid = toServerMessage.MatriochkaMessage.Prev.Uuid
|
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)
|
|
||||||
}
|
|
||||||
387
client/messagestorage.go
Normal file
387
client/messagestorage.go
Normal file
@@ -0,0 +1,387 @@
|
|||||||
|
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, 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
|
||||||
|
for rows.Next() {
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
func TestStoreMessage(t *testing.T) {
|
||||||
id := createId()
|
id := createId(t)
|
||||||
var um meowlib.UserMessage
|
password, err := GetConfig().GetMemPass()
|
||||||
um.Data = []byte("blabla")
|
|
||||||
err := StoreMessage(&id.Peers[0], &um, GetConfig().memoryPassword)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
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 {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
// Checks
|
// Checks
|
||||||
assert.Equal(t, len(messages), 1, "not 1 message")
|
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
|
// Cleanup
|
||||||
if exists("test.id") {
|
if exists("test.id") {
|
||||||
os.Remove("test.id")
|
os.Remove("test.id")
|
||||||
@@ -41,16 +49,27 @@ func TestStoreMessage(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestManyStoreMessage(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++ {
|
for i := 1; i < 100; i++ {
|
||||||
var um meowlib.UserMessage
|
var um meowlib.UserMessage
|
||||||
um.Data = []byte(randomLenString(20, 200))
|
um.Data = []byte(randomLenString(20, 200))
|
||||||
err := StoreMessage(&id.Peers[0], &um, GetConfig().memoryPassword)
|
err := storeMessage(peers[0], &um, []string{}, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
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 {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
178
client/peer.go
178
client/peer.go
@@ -1,7 +1,6 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
@@ -14,44 +13,101 @@ import (
|
|||||||
// Peer manages the peer messaging functions
|
// Peer manages the peer messaging functions
|
||||||
// - Building simple user messages
|
// - Building simple user messages
|
||||||
// - Utility functions for packing/unpacking, encrypting/decrypting messages for peer communication
|
// - 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 {
|
type Peer struct {
|
||||||
Name string `json:"name,omitempty"`
|
Uid string `json:"uid,omitempty"`
|
||||||
MyName string `json:"my_name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
MyAvatar string `json:"my_avatar,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"`
|
// Conversation []InternalMessage `json:"conversation,omitempty"`
|
||||||
// My own keys for that peer
|
// My own keys for that peer
|
||||||
MyIdentity meowlib.KeyPair `json:"my_identity,omitempty"`
|
MyIdentity *meowlib.KeyPair `json:"my_identity,omitempty"`
|
||||||
MyEncryptionKp meowlib.KeyPair `json:"my_encryption_kp,omitempty"`
|
MyEncryptionKp *meowlib.KeyPair `json:"my_encryption_kp,omitempty"`
|
||||||
MyLookupKp meowlib.KeyPair `json:"my_lookup_kp,omitempty"`
|
MyLookupKp *meowlib.KeyPair `json:"my_lookup_kp,omitempty"`
|
||||||
MyPullServers []meowlib.ServerCard `json:"my_pull_servers,omitempty"`
|
MyPullServers []string `json:"my_pull_servers,omitempty"`
|
||||||
// Peer keys and infos
|
// Peer keys and infos
|
||||||
Contact meowlib.ContactCard `json:"contact,omitempty"`
|
//Contact meowlib.ContactCard `json:"contact,omitempty"` // todo : remove
|
||||||
InvitationId string `json:"invitation_id,omitempty"`
|
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
|
// Internal management attributes
|
||||||
Visible bool `json:"visible,omitempty"`
|
Visible bool `json:"visible,omitempty"`
|
||||||
VisiblePassword string `json:"visible_password,omitempty"`
|
VisiblePassword string `json:"visible_password,omitempty"`
|
||||||
PasswordType string `json:"password_type,omitempty"`
|
PasswordType string `json:"password_type,omitempty"`
|
||||||
Blocked bool `json:"blocked,omitempty"`
|
Blocked bool `json:"blocked,omitempty"`
|
||||||
MessageNotification string `json:"message_notification,omitempty"`
|
MessageNotification string `json:"message_notification,omitempty"`
|
||||||
OnionMode bool `json:"onion_mode,omitempty"`
|
MatriochkaMode bool `json:"matriochka_mode,omitempty"`
|
||||||
LastMessage time.Time `json:"last_message,omitempty"`
|
ServerDeliveryInfo bool `json:"server_delivery_info,omitempty"`
|
||||||
DbIds []string `json:"db_ids,omitempty"`
|
CallsAllowed bool `json:"calls_allowed,omitempty"`
|
||||||
AvatarUuid string `json:"avatar_uid,omitempty"`
|
DirectMode bool `json:"direct_mode,omitempty"`
|
||||||
|
DbIds []string `json:"db_ids,omitempty"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
PersonnaeDbId string `json:"personnae_db_id,omitempty"`
|
||||||
dbPassword string
|
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
|
||||||
|
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
|
// Messages building
|
||||||
//
|
//
|
||||||
|
|
||||||
func (p *Peer) BuildSimpleUserMessage(message []byte) (*meowlib.UserMessage, error) {
|
func (p *Peer) BuildSimpleUserMessage(message []byte) (*meowlib.UserMessage, error) {
|
||||||
var msg meowlib.UserMessage
|
var msg meowlib.UserMessage
|
||||||
msg.Destination = p.Contact.LookupPublicKey
|
msg.Destination = p.ContactLookupKey
|
||||||
msg.From = p.MyIdentity.Public
|
msg.From = p.MyIdentity.Public
|
||||||
msg.Data = message
|
msg.Data = message
|
||||||
msg.Type = "1"
|
msg.Type = "1"
|
||||||
msg.Status = &meowlib.ConversationStatus{}
|
msg.Status = &meowlib.ConversationStatus{}
|
||||||
msg.Status.LocalUuid = uuid.New().String()
|
msg.Status.Uuid = uuid.New().String()
|
||||||
|
|
||||||
return &msg, nil
|
return &msg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,17 +138,17 @@ func (p *Peer) BuildSingleFileMessage(filename string, message []byte) ([]meowli
|
|||||||
}
|
}
|
||||||
var msg meowlib.UserMessage
|
var msg meowlib.UserMessage
|
||||||
var file meowlib.File
|
var file meowlib.File
|
||||||
msg.Destination = p.Contact.LookupPublicKey
|
msg.Destination = p.ContactLookupKey
|
||||||
msg.From = p.MyIdentity.Public
|
msg.From = p.MyIdentity.Public
|
||||||
file.Filename = fi.Name()
|
file.Filename = fi.Name()
|
||||||
file.Chunk = uint32(chunk)
|
file.Chunk = uint32(chunk)
|
||||||
file.Data = b[:readTotal]
|
file.Data = append([]byte(nil), b[:readTotal]...)
|
||||||
file.Size = uint64(fi.Size())
|
file.Size = uint64(fi.Size())
|
||||||
msg.Files = append(msg.Files, &file)
|
msg.Files = append(msg.Files, &file)
|
||||||
msg.Type = "2"
|
msg.Type = "1"
|
||||||
if chunk == 0 {
|
if chunk == 0 {
|
||||||
msg.Status = &meowlib.ConversationStatus{}
|
msg.Status = &meowlib.ConversationStatus{}
|
||||||
msg.Status.LocalUuid = uuid.New().String()
|
msg.Status.Uuid = uuid.New().String()
|
||||||
}
|
}
|
||||||
msgs = append(msgs, msg)
|
msgs = append(msgs, msg)
|
||||||
chunk++
|
chunk++
|
||||||
@@ -102,7 +158,7 @@ func (p *Peer) BuildSingleFileMessage(filename string, message []byte) ([]meowli
|
|||||||
|
|
||||||
// Builds an invitation answer user message.
|
// Builds an invitation answer user message.
|
||||||
// it takes as input a contactcard generated by Identity.AnswerInvitation
|
// it takes as input a contactcard generated by Identity.AnswerInvitation
|
||||||
func (p *Peer) BuildInvitationAnswserMessage(myContactCard *meowlib.ContactCard) (*meowlib.UserMessage, error) {
|
func (p *Peer) BuildInvitationAnswerMessage(myContactCard *meowlib.ContactCard) (*meowlib.UserMessage, error) {
|
||||||
var msg meowlib.UserMessage
|
var msg meowlib.UserMessage
|
||||||
var invitation meowlib.Invitation
|
var invitation meowlib.Invitation
|
||||||
invitation.Step = 3
|
invitation.Step = 3
|
||||||
@@ -110,8 +166,10 @@ func (p *Peer) BuildInvitationAnswserMessage(myContactCard *meowlib.ContactCard)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
invitation.Uuid = p.InvitationId
|
||||||
invitation.Payload = out
|
invitation.Payload = out
|
||||||
msg.Destination = p.Contact.LookupPublicKey
|
msg.Destination = p.ContactLookupKey
|
||||||
|
msg.Invitation = &invitation
|
||||||
msg.From = p.MyIdentity.Public
|
msg.From = p.MyIdentity.Public
|
||||||
msg.Type = "1"
|
msg.Type = "1"
|
||||||
return &msg, nil
|
return &msg, nil
|
||||||
@@ -141,9 +199,13 @@ func (p *Peer) DeserializeUserMessage(data []byte) (*meowlib.UserMessage, error)
|
|||||||
// AsymEncryptMessage prepares a message to send to a specific peer contact
|
// AsymEncryptMessage prepares a message to send to a specific peer contact
|
||||||
func (p *Peer) AsymEncryptMessage(Message []byte) (*meowlib.EncryptedMessage, error) {
|
func (p *Peer) AsymEncryptMessage(Message []byte) (*meowlib.EncryptedMessage, error) {
|
||||||
var enc *meowlib.EncryptedMessage
|
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 {
|
if err != nil {
|
||||||
fmt.Println(err.Error())
|
logger.Error().Err(err).Msg("Peer.AsymEncryptMessage")
|
||||||
return enc, err
|
return enc, err
|
||||||
}
|
}
|
||||||
return enc, err
|
return enc, err
|
||||||
@@ -151,9 +213,12 @@ func (p *Peer) AsymEncryptMessage(Message []byte) (*meowlib.EncryptedMessage, er
|
|||||||
|
|
||||||
// AsymDecryptMessage reads a message from a specific peer contact
|
// AsymDecryptMessage reads a message from a specific peer contact
|
||||||
func (p *Peer) AsymDecryptMessage(Message []byte, Signature []byte) (DecryptedMessage []byte, err error) {
|
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 {
|
if err != nil {
|
||||||
fmt.Println(err.Error())
|
logger.Error().Err(err).Msg("Peer.AsymDecryptMessage")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return DecryptedMessage, err
|
return DecryptedMessage, err
|
||||||
@@ -162,9 +227,12 @@ func (p *Peer) AsymDecryptMessage(Message []byte, Signature []byte) (DecryptedMe
|
|||||||
// PackUserMessage will package the previously encrypted message
|
// PackUserMessage will package the previously encrypted message
|
||||||
func (p *Peer) PackUserMessage(message []byte, signature []byte) *meowlib.PackedUserMessage {
|
func (p *Peer) PackUserMessage(message []byte, signature []byte) *meowlib.PackedUserMessage {
|
||||||
var msg meowlib.PackedUserMessage
|
var msg meowlib.PackedUserMessage
|
||||||
msg.Destination = p.Contact.LookupPublicKey
|
msg.Destination = p.ContactLookupKey
|
||||||
msg.Payload = message
|
msg.Payload = message
|
||||||
msg.Signature = signature
|
msg.Signature = signature
|
||||||
|
if p.ServerDeliveryInfo {
|
||||||
|
msg.ServerDeliveryUuid = uuid.New().String()
|
||||||
|
}
|
||||||
return &msg
|
return &msg
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,20 +288,52 @@ func (p *Peer) SetDbPassword(password string) {
|
|||||||
p.dbPassword = password
|
p.dbPassword = password
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Peer) GetDbPassword() string {
|
func (p *Peer) GetDbPassword() (string, error) {
|
||||||
return p.dbPassword
|
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) {
|
func (p *Peer) GetLastMessageUuid(msg []byte) {
|
||||||
|
|||||||
@@ -1,20 +1,530 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"forge.redroom.link/yves/meowlib"
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/stretchr/testify/assert"
|
"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 TestBuildInvitationAnswerMessage(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.BuildInvitationAnswerMessage(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(3), 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 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.Payload, packed.Signature)
|
||||||
|
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.Payload, packed.Signature)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 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) {
|
func TestGetFromPublicKey(t *testing.T) {
|
||||||
id := CreateIdentity("test")
|
id, err := CreateIdentity("test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("CreateIdentity failed")
|
||||||
|
}
|
||||||
id.Save()
|
id.Save()
|
||||||
for i := 1; i < 10; i++ {
|
for i := 1; i < 10; i++ {
|
||||||
var p Peer
|
var p Peer
|
||||||
|
p.Uid = uuid.New().String()
|
||||||
p.Name = "test" + strconv.Itoa(i)
|
p.Name = "test" + strconv.Itoa(i)
|
||||||
p.Contact.ContactPublicKey = "stringToFind" + strconv.Itoa(i)
|
p.ContactPublicKey = "stringToFind" + strconv.Itoa(i)
|
||||||
id.Peers = append(id.Peers, p)
|
err := id.Peers.StorePeer(&p)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("StorePeer failed")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
p5 := id.Peers.GetFromPublicKey("stringToFind5")
|
p5 := id.Peers.GetFromPublicKey("stringToFind5")
|
||||||
assert.Equal(t, p5.Name, "test5")
|
assert.Equal(t, p5.Name, "test5")
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
type PeerList []*Peer
|
||||||
"forge.redroom.link/yves/meowlib"
|
|
||||||
)
|
|
||||||
|
|
||||||
type PeerList []Peer
|
|
||||||
|
|
||||||
func (pl *PeerList) GetFromPublicKey(publickey string) *Peer {
|
func (pl *PeerList) GetFromPublicKey(publickey string) *Peer {
|
||||||
for _, peer := range *pl {
|
for _, peer := range *pl {
|
||||||
if peer.Contact.ContactPublicKey == publickey {
|
if peer.ContactPublicKey == publickey {
|
||||||
return &peer
|
return peer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pl *PeerList) GetFromInvitationId(invitationId string) *Peer {
|
||||||
|
for _, peer := range *pl {
|
||||||
|
if peer.InvitationId == invitationId {
|
||||||
|
return peer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -18,7 +23,7 @@ func (pl *PeerList) GetFromPublicKey(publickey string) *Peer {
|
|||||||
func (pl *PeerList) GetFromMyLookupKey(publickey string) *Peer {
|
func (pl *PeerList) GetFromMyLookupKey(publickey string) *Peer {
|
||||||
for _, peer := range *pl {
|
for _, peer := range *pl {
|
||||||
if peer.MyLookupKp.Public == publickey {
|
if peer.MyLookupKp.Public == publickey {
|
||||||
return &peer
|
return peer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -26,14 +31,15 @@ func (pl *PeerList) GetFromMyLookupKey(publickey string) *Peer {
|
|||||||
|
|
||||||
func (pl *PeerList) GetFromName(name string) *Peer {
|
func (pl *PeerList) GetFromName(name string) *Peer {
|
||||||
for _, peer := range *pl {
|
for _, peer := range *pl {
|
||||||
if peer.Contact.Name == name {
|
if peer.Name == name {
|
||||||
return &peer
|
return peer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
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
|
var list []*meowlib.ConversationRequest
|
||||||
for _, peer := range *pl {
|
for _, peer := range *pl {
|
||||||
var cr meowlib.ConversationRequest
|
var cr meowlib.ConversationRequest
|
||||||
@@ -42,4 +48,4 @@ func (pl *PeerList) GetConversationRequests() []*meowlib.ConversationRequest {
|
|||||||
list = append(list, &cr)
|
list = append(list, &cr)
|
||||||
}
|
}
|
||||||
return list
|
return list
|
||||||
}
|
}*/
|
||||||
|
|||||||
251
client/peerstorage.go
Normal file
251
client/peerstorage.go
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
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
|
||||||
|
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)
|
||||||
|
}
|
||||||
54
client/peerstorage_test.go
Normal file
54
client/peerstorage_test.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
286
client/sendjobs.go
Normal file
286
client/sendjobs.go
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
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 of a pre-built packed server message (binary).
|
||||||
|
// 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"`
|
||||||
|
File string `json:"file,omitempty"`
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
184
client/sendjobs_test.go
Normal file
184
client/sendjobs_test.go
Normal file
@@ -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")
|
||||||
|
}
|
||||||
181
client/server.go
181
client/server.go
@@ -1,7 +1,7 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"forge.redroom.link/yves/meowlib"
|
"forge.redroom.link/yves/meowlib"
|
||||||
@@ -17,40 +17,122 @@ import (
|
|||||||
// - Utility functions for packing/unpacking, encrypting/decrypting messages for server communication
|
// - Utility functions for packing/unpacking, encrypting/decrypting messages for server communication
|
||||||
// - Server remote management if ManagerKp is available for that server
|
// - Server remote management if ManagerKp is available for that server
|
||||||
type Server struct {
|
type Server struct {
|
||||||
ServerData meowlib.ServerCard `json:"server_data,omitempty"`
|
//ServerCard meowlib.ServerCard `json:"server_data,omitempty"`
|
||||||
Presence bool `json:"presence,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
LastCheck time.Time `json:"last_check,omitempty"`
|
Description string `json:"description,omitempty"`
|
||||||
Uptime time.Duration `json:"uptime,omitempty"`
|
PublicKey string `json:"public_key,omitempty"`
|
||||||
Login string `json:"login,omitempty"`
|
Url string `json:"url,omitempty"`
|
||||||
Password string `json:"password,omitempty"`
|
Login string `json:"login,omitempty"`
|
||||||
UserKp meowlib.KeyPair `json:"user_kp,omitempty"`
|
Password string `json:"password,omitempty"`
|
||||||
ManagerKp meowlib.KeyPair `json:"manager_kp,omitempty"`
|
Presence bool `json:"presence,omitempty"`
|
||||||
Country string `json:"country,omitempty"`
|
LastCheck time.Time `json:"last_check,omitempty"`
|
||||||
AllowedDelay int `json:"allowed_delay,omitempty"`
|
Uptime time.Duration `json:"uptime,omitempty"`
|
||||||
Backup bool `json:"backup,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/
|
// 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
|
var is Server
|
||||||
is.ServerData.Url = url
|
var err error
|
||||||
return &is
|
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
|
// Create a server from a server card
|
||||||
func CreateServerFromServerCard(server *meowlib.ServerCard) *Server {
|
func CreateServerFromServerCard(server *meowlib.ServerCard) (*Server, error) {
|
||||||
var is Server
|
var is Server
|
||||||
is.ServerData = *server
|
var err error
|
||||||
is.UserKp = meowlib.NewKeyPair()
|
is.Name = server.Name
|
||||||
return &is
|
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
|
// AsymEncryptMessage prepares a message to send to a specific internal server
|
||||||
func (ints *Server) AsymEncryptMessage(Message []byte) (*meowlib.EncryptedMessage, error) {
|
func (ints *Server) AsymEncryptMessage(Message []byte) (*meowlib.EncryptedMessage, error) {
|
||||||
var enc *meowlib.EncryptedMessage
|
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 {
|
if err != nil {
|
||||||
fmt.Println(err.Error())
|
logger.Error().Err(err).Msg("Server.AsymEncryptMessage")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return enc, 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
|
// AsymDecryptMessage reads a message from a specific internal server
|
||||||
func (ints *Server) AsymDecryptMessage(Message []byte, Signature []byte) (DecryptedMessage []byte, err error) {
|
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 {
|
if err != nil {
|
||||||
fmt.Println(err.Error())
|
logger.Error().Err(err).Msg("Server.AsymDecryptMessage")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return DecryptedMessage, err
|
return DecryptedMessage, err
|
||||||
@@ -86,6 +168,7 @@ func (ints *Server) BuildMessageSendingMessage(usermsg *meowlib.PackedUserMessag
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ! Unfinished unused ?
|
||||||
// BuildMessageRequestMessage creates a message lookup message to server and returns it as protobuf serialized byte array
|
// BuildMessageRequestMessage creates a message lookup message to server and returns it as protobuf serialized byte array
|
||||||
func (ints *Server) BuildMessageRequestMessage(lookupKeys []string) ([]byte, error) {
|
func (ints *Server) BuildMessageRequestMessage(lookupKeys []string) ([]byte, error) {
|
||||||
var msg meowlib.ToServerMessage
|
var msg meowlib.ToServerMessage
|
||||||
@@ -99,9 +182,26 @@ func (ints *Server) BuildMessageRequestMessage(lookupKeys []string) ([]byte, err
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
// BuildToServerMessageInvitation creates an invitation message to server and returns it as a meowlib.ToServerMessage
|
// BuildToServerMessageInvitation creates an invitation message to server and returns it as a meowlib.ToServerMessage
|
||||||
// it takes as input a contactcard generated by Identity.InvitePeer
|
// 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) {
|
func (ints *Server) BuildToServerMessageInvitationCreation(invitation *meowlib.ContactCard, password string, timeout int, shortCodeLen int) (*meowlib.ToServerMessage, error) {
|
||||||
var msg meowlib.ToServerMessage
|
var msg meowlib.ToServerMessage
|
||||||
var inv meowlib.Invitation
|
var inv meowlib.Invitation
|
||||||
payload, err := invitation.Compress()
|
payload, err := invitation.Compress()
|
||||||
@@ -113,7 +213,7 @@ func (ints *Server) BuildToServerMessageInvitationCreation(invitation *meowlib.C
|
|||||||
inv.Step = 1
|
inv.Step = 1
|
||||||
inv.Password = password
|
inv.Password = password
|
||||||
inv.Timeout = int32(timeout)
|
inv.Timeout = int32(timeout)
|
||||||
inv.ShortcodeLen = int32(invitationIdLen)
|
inv.ShortcodeLen = int32(shortCodeLen)
|
||||||
inv.Payload = payload
|
inv.Payload = payload
|
||||||
msg.Invitation = &inv
|
msg.Invitation = &inv
|
||||||
return &msg, nil
|
return &msg, nil
|
||||||
@@ -132,6 +232,37 @@ func (ints *Server) BuildToServerMessageInvitationRequest(shortcode string, pass
|
|||||||
return &msg, nil
|
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
|
// PackServerMessage
|
||||||
func (ints *Server) PackServerMessage(payload []byte, signature []byte) (protoPackedMessage []byte, err error) {
|
func (ints *Server) PackServerMessage(payload []byte, signature []byte) (protoPackedMessage []byte, err error) {
|
||||||
var msg meowlib.PackedServerMessage
|
var msg meowlib.PackedServerMessage
|
||||||
|
|||||||
640
client/server_test.go
Normal file
640
client/server_test.go
Normal file
@@ -0,0 +1,640 @@
|
|||||||
|
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_BuildToServerMessageInvitationCreation(t *testing.T) {
|
||||||
|
srv, err := CreateServerFromUrl("https://example.com/meow")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
cc := &meowlib.ContactCard{
|
||||||
|
Name: "Alice",
|
||||||
|
ContactPublicKey: "alice-pub",
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, err := srv.BuildToServerMessageInvitationCreation(cc, "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 ContactCard — decompress and verify
|
||||||
|
restored, err := meowlib.NewContactCardFromCompressed(msg.Invitation.Payload)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "Alice", restored.Name)
|
||||||
|
assert.Equal(t, "alice-pub", restored.ContactPublicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_BuildToServerMessageInvitationCreation_NoPassword(t *testing.T) {
|
||||||
|
srv, _ := CreateServerFromUrl("https://example.com/meow")
|
||||||
|
cc := &meowlib.ContactCard{Name: "Bob"}
|
||||||
|
|
||||||
|
msg, err := srv.BuildToServerMessageInvitationCreation(cc, "", 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)
|
||||||
|
}
|
||||||
@@ -8,15 +8,18 @@ import "errors"
|
|||||||
// - Owned servers lists
|
// - Owned servers lists
|
||||||
// - Matriochka paths
|
// - Matriochka paths
|
||||||
type ServerList struct {
|
type ServerList struct {
|
||||||
Name string `json:"name,omitempty"`
|
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
|
// FilterByIdxs returns a filtered server list filtered according to an index list
|
||||||
func (sl *ServerList) FilterByIdxs(MessageServerIdxs []int) (filtered *ServerList, err error) {
|
func (sl *ServerList) FilterByIdxs(MessageServerIdxs []int) (filtered *ServerList, err error) {
|
||||||
filtered.Servers = []Server{}
|
filtered = &ServerList{
|
||||||
|
Name: sl.Name,
|
||||||
|
Servers: []*Server{},
|
||||||
|
}
|
||||||
for _, i := range MessageServerIdxs {
|
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")
|
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
|
// GetServerByIdx returns a server from it's index
|
||||||
func (sl *ServerList) GetServerByIdx(idx int) (server *Server, err error) {
|
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 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
|
// GetServerByPubkey returns a server from it's public key
|
||||||
func (sl *ServerList) GetServerByPubkey(pubkey string) (filtered *Server) {
|
func (sl *ServerList) GetServerByPubkey(pubkey string) (filtered *Server) {
|
||||||
for _, srv := range sl.Servers {
|
for _, srv := range sl.Servers {
|
||||||
if srv.ServerData.PublicKey == pubkey {
|
if srv.PublicKey == pubkey {
|
||||||
return &srv
|
return srv
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddUrls is a simple utility functon used mainly as a shortcut for testing purposes
|
// 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 {
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
346
client/serverlist_test.go
Normal file
346
client/serverlist_test.go
Normal file
@@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
292
client/serverstorage.go
Normal file
292
client/serverstorage.go
Normal file
@@ -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()
|
||||||
|
}
|
||||||
255
client/serverstorage_test.go
Normal file
255
client/serverstorage_test.go
Normal file
@@ -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
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"image/color"
|
"image/color"
|
||||||
"image/png"
|
"image/png"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
@@ -101,7 +100,7 @@ func (contact *ContactCard) WriteCompressed(filename string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (contact *ContactCard) WritePng(filename string) {
|
func (contact *ContactCard) WritePng(filename string) error {
|
||||||
out, err := proto.Marshal(contact)
|
out, err := proto.Marshal(contact)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
println(err)
|
println(err)
|
||||||
@@ -128,18 +127,18 @@ func (contact *ContactCard) WritePng(filename string) {
|
|||||||
|
|
||||||
f, err := os.Create(filename)
|
f, err := os.Create(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := png.Encode(f, img); err != nil {
|
if err := png.Encode(f, img); err != nil {
|
||||||
f.Close()
|
f.Close()
|
||||||
log.Fatal(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := f.Close(); err != nil {
|
if err := f.Close(); err != nil {
|
||||||
log.Fatal(err)
|
return err
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (Contact *ContactCard) WriteQr(filename string) error {
|
func (Contact *ContactCard) WriteQr(filename string) error {
|
||||||
|
|||||||
@@ -1,21 +1,74 @@
|
|||||||
package meowlib
|
package meowlib
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCompress(t *testing.T) {
|
func TestCompressAndJson(t *testing.T) {
|
||||||
kp1 := NewKeyPair()
|
kp1, err := NewKeyPair()
|
||||||
kp2 := NewKeyPair()
|
if err != nil {
|
||||||
kp3 := NewKeyPair()
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
kp2, err := NewKeyPair()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
kp3, err := NewKeyPair()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
var cc ContactCard
|
var cc ContactCard
|
||||||
cc.Name = "My Full Name And It's loooong"
|
cc.Name = "My Full Name And It's loooong"
|
||||||
cc.ContactPublicKey = kp1.Public
|
cc.ContactPublicKey = kp1.Public
|
||||||
cc.EncryptionPublicKey = kp2.Public
|
cc.EncryptionPublicKey = kp2.Public
|
||||||
cc.LookupPublicKey = kp3.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"})
|
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()
|
serialized, _ := cc.Serialize()
|
||||||
println(len(serialized))
|
println(len(serialized))
|
||||||
compressed, _ := cc.Compress()
|
compressed, _ := cc.Compress()
|
||||||
println(len(compressed))
|
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
|
sed -i 's/\.\.\/client/client/g' generated/client.puml
|
||||||
go-plantuml generate -o generated/server.puml -d ../server
|
go-plantuml generate -o generated/server.puml -d ../server
|
||||||
sed -i 's/\.\.\/server/server/g' generated/server.puml
|
sed -i 's/\.\.\/server/server/g' generated/server.puml
|
||||||
|
cp *.puml generated/
|
||||||
cd generated
|
cd generated
|
||||||
plantuml .
|
plantuml .
|
||||||
plantuml -latex .
|
plantuml -latex .
|
||||||
|
mv *.tex tex/
|
||||||
|
mv *.png png/
|
||||||
|
rm *.log *.aux *.fdb_latexmk *.fls *.gz *.puml
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
7
doc/endpoints/company_endpoint.puml
Normal file
7
doc/endpoints/company_endpoint.puml
Normal file
@@ -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
|
||||||
10
doc/endpoints/public_endpoint.puml
Normal file
10
doc/endpoints/public_endpoint.puml
Normal file
@@ -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
|
||||||
7
doc/invitation/sq_invitation.puml
Normal file
7
doc/invitation/sq_invitation.puml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
@startuml Server Invitation Step 01
|
||||||
|
Bob -> MeowBob: Create invitation for alice (Generate Bob ContactCard and create Alice pending contact)
|
||||||
|
Bob -> Alice: Send invitation (Bob ContactCard)
|
||||||
|
Alice -> MeowAlice: Accept Invitation and create answer (Generate Alice ContactCard and create finalized Bob contact)
|
||||||
|
Alice -> Bob: Send answer (Alice ContactCard)
|
||||||
|
Bob -> MeowBob: Review Answer, invitation finalize (Finalize Alice contact and notify Alice that communication is possible)
|
||||||
|
@enduml
|
||||||
12
doc/invitation/sq_srvinv01.puml
Normal file
12
doc/invitation/sq_srvinv01.puml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
@startuml Server Invitation Step 01
|
||||||
|
User -> Bastet : fill invitation
|
||||||
|
User -> Bastet : select servers
|
||||||
|
Bastet -> NativeLib : get server cards for selected uids
|
||||||
|
NativeLib -> Bastet : server cards
|
||||||
|
Bastet -> NativeLib : invitationCreateMessage
|
||||||
|
NativeLib -> Bastet : invitationMessage
|
||||||
|
Bastet -> Server : send invitation
|
||||||
|
Server -> Redis : Store invitation
|
||||||
|
Server -> Bastet : invitation URL
|
||||||
|
Bastet -> User : invitation URL
|
||||||
|
@enduml
|
||||||
11
doc/invitation/sq_srvinv02.puml
Normal file
11
doc/invitation/sq_srvinv02.puml
Normal file
@@ -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
|
||||||
12
doc/invitation/sq_srvinv03.puml
Normal file
12
doc/invitation/sq_srvinv03.puml
Normal file
@@ -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
|
||||||
9
doc/invitation/sq_srvinv04.puml
Normal file
9
doc/invitation/sq_srvinv04.puml
Normal file
@@ -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
doc/meow.svg
Normal file
2816
doc/meow.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 467 KiB |
13
doc/meow.tex
13
doc/meow.tex
@@ -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.
|
\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.
|
Any server can be used for building the transfer chain.
|
||||||
Some of them might be marked as trusted.
|
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).
|
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.
|
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.
|
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.
|
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 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.
|
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,
|
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.
|
one man's terrorist is another man's freedom fighter.
|
||||||
|
|||||||
10
doc/messaging/sq_msg01.puml
Normal file
10
doc/messaging/sq_msg01.puml
Normal file
@@ -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
|
||||||
24
doc/server/server_messaging.puml
Normal file
24
doc/server/server_messaging.puml
Normal file
@@ -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
|
||||||
20
doc/server/sq_01_srvmessaging.puml
Normal file
20
doc/server/sq_01_srvmessaging.puml
Normal file
@@ -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
|
||||||
30
doc/server/sq_02_srvmessaging.puml
Normal file
30
doc/server/sq_02_srvmessaging.puml
Normal file
@@ -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
|
|
||||||
@@ -22,29 +22,34 @@ func TestEndToEnd(t *testing.T) {
|
|||||||
///////////////////////////
|
///////////////////////////
|
||||||
// Creating New Identity //
|
// Creating New Identity //
|
||||||
///////////////////////////
|
///////////////////////////
|
||||||
Me = client.CreateIdentity("myname")
|
Me, err = client.CreateIdentity("myname")
|
||||||
|
|
||||||
// define my preferences (servers)
|
// define my preferences (servers)
|
||||||
Me.MessageServers.Name = "Message Servers"
|
srv := client.Server{Name: "MyServer", Url: "http://127.0.0.1/meow/"}
|
||||||
Me.MessageServers.AddUrls([]string{"http://127.0.0.1/meow/", "mqtt://127.0.0.1", "meow://127.0.0.1"})
|
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 //
|
// Create an invitation for a friend, I want him/her to know me as Bender //
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
fmt.Println("Creating an invitation for the first friend...")
|
fmt.Println("Creating an invitation for the first friend...")
|
||||||
peer, myContactCard, err := Me.InvitePeer("Bender", "myfirstfriend", []int{1, 2})
|
peer, err := Me.InvitePeer("Bender", "myfirstfriend", []string{"http://127.0.0.1/meow/", "mqtt://127.0.0.1"}, "welcome, it's me!")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
println(err)
|
println(err)
|
||||||
}
|
}
|
||||||
println(peer.Name)
|
println(peer.Name)
|
||||||
// print my invitation
|
// print my invitation
|
||||||
a, _ := json.Marshal(myContactCard)
|
a, _ := json.Marshal(peer.GetMyContact())
|
||||||
fmt.Println(string(a))
|
fmt.Println(string(a))
|
||||||
// TODO : Convert invitation to QR Code
|
// TODO : Convert invitation to QR Code
|
||||||
myContactCard.WritePng("invitation.png")
|
peer.GetMyContact().WritePng("invitation.png")
|
||||||
data, err := myContactCard.Compress()
|
data, err := peer.GetMyContact().Compress()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
println(err)
|
println(err)
|
||||||
}
|
}
|
||||||
myContactCard.WriteQr("qrcode.png")
|
peer.GetMyContact().WriteQr("qrcode.png")
|
||||||
println("Compressed contact card :", len(data))
|
println("Compressed contact card :", len(data))
|
||||||
///////////////////////////////////////
|
///////////////////////////////////////
|
||||||
// Simulate peer invitation response //
|
// Simulate peer invitation response //
|
||||||
@@ -53,15 +58,25 @@ func TestEndToEnd(t *testing.T) {
|
|||||||
var ReceivedContact meowlib.ContactCard
|
var ReceivedContact meowlib.ContactCard
|
||||||
|
|
||||||
// Friend simulated invitation
|
// Friend simulated invitation
|
||||||
FirstFriendContactKp := meowlib.NewKeyPair()
|
FirstFriendContactKp, err := meowlib.NewKeyPair()
|
||||||
FirstFriendEncryptionKp := meowlib.NewKeyPair()
|
if err != nil {
|
||||||
FirstFriendLookupKp := meowlib.NewKeyPair()
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
FirstFriendEncryptionKp, err := meowlib.NewKeyPair()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
FirstFriendLookupKp, err := meowlib.NewKeyPair()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
ReceivedContact.Name = "I'm the friend"
|
ReceivedContact.Name = "I'm the friend"
|
||||||
ReceivedContact.ContactPublicKey = FirstFriendContactKp.Public
|
ReceivedContact.ContactPublicKey = FirstFriendContactKp.Public
|
||||||
ReceivedContact.EncryptionPublicKey = FirstFriendEncryptionKp.Public
|
ReceivedContact.EncryptionPublicKey = FirstFriendEncryptionKp.Public
|
||||||
ReceivedContact.LookupPublicKey = FirstFriendLookupKp.Public
|
ReceivedContact.LookupPublicKey = FirstFriendLookupKp.Public
|
||||||
ReceivedContact.InvitationId = myContactCard.InvitationId
|
ReceivedContact.InvitationId = peer.GetMyContact().InvitationId
|
||||||
FriendServer1KP := meowlib.NewKeyPair()
|
FriendServer1KP, err := meowlib.NewKeyPair()
|
||||||
|
|
||||||
FriendServer1 := meowlib.ServerCard{Name: "FriendServer1", Url: "http://myfriend.org/meow/", PublicKey: FriendServer1KP.Public, Description: "Fancy description"}
|
FriendServer1 := meowlib.ServerCard{Name: "FriendServer1", Url: "http://myfriend.org/meow/", PublicKey: FriendServer1KP.Public, Description: "Fancy description"}
|
||||||
ReceivedContact.PullServers = append(ReceivedContact.PullServers, &FriendServer1)
|
ReceivedContact.PullServers = append(ReceivedContact.PullServers, &FriendServer1)
|
||||||
|
|
||||||
@@ -71,52 +86,53 @@ func TestEndToEnd(t *testing.T) {
|
|||||||
Me.FinalizeInvitation(&ReceivedContact)
|
Me.FinalizeInvitation(&ReceivedContact)
|
||||||
err = Me.Save()
|
err = Me.Save()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err.Error())
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
a, _ = json.Marshal(Me)
|
a, _ = json.Marshal(Me)
|
||||||
os.WriteFile("id.json", a, 0644)
|
os.WriteFile("id.json", a, 0644)
|
||||||
fmt.Println(string(a))
|
fmt.Println(string(a))
|
||||||
/////////////////////////////////////
|
/////////////////////////////////////
|
||||||
// Create a message to that friend //
|
// Create a message to that friend //
|
||||||
/////////////////////////////////////
|
/////////////////////////////////////
|
||||||
MyFirstFriend := Me.Peers[0]
|
peers, _ := Me.Peers.GetPeers()
|
||||||
|
MyFirstFriend := peers[0]
|
||||||
textmessage := "Hello friend!"
|
textmessage := "Hello friend!"
|
||||||
// Creating User message
|
// Creating User message
|
||||||
usermessage, err := MyFirstFriend.BuildSimpleUserMessage([]byte(textmessage))
|
usermessage, err := MyFirstFriend.BuildSimpleUserMessage([]byte(textmessage))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err.Error())
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
serializedMessage, err := MyFirstFriend.SerializeUserMessage(usermessage)
|
serializedMessage, err := MyFirstFriend.SerializeUserMessage(usermessage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err.Error())
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
// Encrypting it
|
// Encrypting it
|
||||||
enc, err := MyFirstFriend.AsymEncryptMessage(serializedMessage)
|
enc, err := MyFirstFriend.AsymEncryptMessage(serializedMessage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err.Error())
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Packing it
|
// Packing it
|
||||||
packedMsg := MyFirstFriend.PackUserMessage(enc.Data, enc.Signature)
|
packedMsg := MyFirstFriend.PackUserMessage(enc.Data, enc.Signature)
|
||||||
|
|
||||||
srv := MyFirstFriend.Contact.PullServers[0]
|
intS1, err := Me.MessageServers.LoadServer("http://127.0.0.1/meow/")
|
||||||
intS1 := client.CreateServerFromServerCard(srv)
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
// Creating Server message for transporting the user message
|
// Creating Server message for transporting the user message
|
||||||
toServerMessage, err := intS1.BuildMessageSendingMessage(packedMsg)
|
toServerMessage, err := intS1.BuildMessageSendingMessage(packedMsg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err.Error())
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
// Encrypting it
|
// Encrypting it
|
||||||
encToServer, err := intS1.AsymEncryptMessage(toServerMessage)
|
encToServer, err := intS1.AsymEncryptMessage(toServerMessage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err.Error())
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
// Packing it
|
// Packing it
|
||||||
protoPackedServerMsg, err := intS1.PackServerMessage(encToServer.Data, encToServer.Signature)
|
protoPackedServerMsg, err := intS1.PackServerMessage(encToServer.Data, encToServer.Signature)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err.Error())
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
///////////////////////
|
///////////////////////
|
||||||
// Sending to server //
|
// Sending to server //
|
||||||
@@ -130,40 +146,40 @@ func TestEndToEnd(t *testing.T) {
|
|||||||
// Simulating server side processing //
|
// Simulating server side processing //
|
||||||
///////////////////////////////////////
|
///////////////////////////////////////
|
||||||
var server1 server.Identity
|
var server1 server.Identity
|
||||||
server1.ServerName = intS1.ServerData.Name
|
server1.ServerName = intS1.Name
|
||||||
server1.ServerKp = FriendServer1KP
|
server1.ServerKp = FriendServer1KP
|
||||||
server1.ServerDesc = intS1.ServerData.Description
|
server1.ServerDesc = intS1.Description
|
||||||
// Unpack
|
// Unpack
|
||||||
srv_from, srv_encmsg, srv_signature, err := server1.UnpackReceived(protoPackedServerMsg)
|
srv_from, srv_encmsg, srv_signature, err := server1.UnpackReceived(protoPackedServerMsg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err.Error())
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
// Decrypt
|
// Decrypt
|
||||||
srv_clear, err := server1.AsymDecryptMessage(srv_from, srv_encmsg, srv_signature)
|
srv_clear, err := server1.AsymDecryptMessage(srv_from, srv_encmsg, srv_signature)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err.Error())
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
// Decode msg
|
// Decode msg
|
||||||
srv_msg, err := server1.DeserializeToServerMessage(srv_clear)
|
srv_msg, err := server1.DeserializeToServerMessage(srv_clear)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err.Error())
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
// Response : Ack received message
|
// Response : Ack received message
|
||||||
srv_fromServerMessage, err := server1.BuildSimpleAckResponseMessage(srv_msg.Uuid)
|
srv_fromServerMessage, err := server1.BuildSimpleAckResponseMessage(srv_msg.Uuid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err.Error())
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
encoded_srv_fromServerMessage, err := server1.SerializeFromServerMessage(srv_fromServerMessage)
|
encoded_srv_fromServerMessage, err := server1.SerializeFromServerMessage(srv_fromServerMessage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err.Error())
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
srv_resp, err := server1.AsymEncryptMessage(srv_from, encoded_srv_fromServerMessage)
|
srv_resp, err := server1.AsymEncryptMessage(srv_from, encoded_srv_fromServerMessage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err.Error())
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
resp, err := server1.PackForSending(srv_resp.Data, srv_resp.Signature)
|
resp, err := server1.PackForSending(srv_resp.Data, srv_resp.Signature)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err.Error())
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
print(resp)
|
print(resp)
|
||||||
//////////////////////////////////////////////
|
//////////////////////////////////////////////
|
||||||
|
|||||||
57
go.mod
57
go.mod
@@ -1,20 +1,55 @@
|
|||||||
module forge.redroom.link/yves/meowlib
|
module forge.redroom.link/yves/meowlib
|
||||||
|
|
||||||
go 1.16
|
go 1.23.1
|
||||||
|
|
||||||
|
toolchain go1.24.2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c // indirect
|
github.com/ProtonMail/gopenpgp/v2 v2.8.3
|
||||||
github.com/ProtonMail/gopenpgp/v2 v2.7.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/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/makiuchi-d/gozxing v0.1.1
|
||||||
github.com/mattn/go-sqlite3 v1.14.16
|
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/pkg/errors v0.9.1
|
||||||
github.com/rs/zerolog v1.31.0
|
github.com/rs/zerolog v1.34.0
|
||||||
github.com/stretchr/testify v1.7.0
|
github.com/stretchr/testify v1.9.0
|
||||||
golang.org/x/net v0.16.0 // indirect
|
google.golang.org/protobuf v1.36.6
|
||||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
)
|
||||||
google.golang.org/protobuf v1.31.0
|
|
||||||
|
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/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
|
||||||
)
|
)
|
||||||
|
|||||||
296
go.sum
296
go.sum
@@ -1,28 +1,75 @@
|
|||||||
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c h1:kMFnB0vCcX7IL/m9Y5LO+KQYv+t1CQOiFe6+SV2J7bE=
|
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
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 h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
|
||||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
|
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.8.3 h1:1jHlELwCR00qovx2B50DkL/FjYwt/P91RnlsqeOp2Hs=
|
||||||
github.com/ProtonMail/gopenpgp/v2 v2.7.3/go.mod h1:IhkNEDaxec6NyzSI0PlxapinnwPVIESk8/76da3Ct3g=
|
github.com/ProtonMail/gopenpgp/v2 v2.8.3/go.mod h1:LiuOTbnJit8w9ZzOoLscj0kmdALY7hfoCVh5Qlb0bcg=
|
||||||
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE=
|
||||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
|
||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
github.com/alicebob/miniredis v2.5.0+incompatible h1:yBHoLpsyjupjz3NL3MhKMVkR41j82Yjf3KFv7ApYzUI=
|
||||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk=
|
||||||
github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
|
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||||
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
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/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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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.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.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 h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
|
||||||
github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
|
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/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/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.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/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.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||||
@@ -30,71 +77,178 @@ 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.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
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.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.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.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.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.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.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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/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 h1:xxqijhoedi+/lZlhINteGbywIrewVdVv2wl9r5O9S1I=
|
||||||
github.com/makiuchi-d/gozxing v0.1.1/go.mod h1:eRIHbOjX7QWxLIDJoQuMLhuXg9LAuw6znsUtRkNw9DU=
|
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.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.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.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 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
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.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
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.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.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 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
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.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.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.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8=
|
||||||
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
|
github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
|
||||||
github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo=
|
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||||
github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc=
|
github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8=
|
||||||
github.com/onsi/gomega v1.22.1 h1:pY8O4lBfsHKZHM/6nrxkhVPUznOlIu3quZcKP/M20KI=
|
github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0=
|
||||||
github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM=
|
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 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
|
||||||
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
|
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
|
||||||
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
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.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.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.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/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-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-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-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.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.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||||
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
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.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.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/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
@@ -103,80 +257,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-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-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-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.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.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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos=
|
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||||
golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
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-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-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-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.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.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-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-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-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-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-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-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-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-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-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-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-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-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-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.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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.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.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
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-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.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.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.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.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.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.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.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.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.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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
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-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-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.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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
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-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-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-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-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-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
|
||||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
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-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-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||||
@@ -185,18 +329,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.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-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.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.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||||
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=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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/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 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
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.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.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.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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
44
http.go
Normal file
44
http.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
182
lokiwriter.go
Normal file
182
lokiwriter.go
Normal file
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
191
lokiwriter_test.go
Normal file
191
lokiwriter_test.go
Normal file
@@ -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,6 +3,7 @@ package meowlib
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
@@ -30,13 +31,13 @@ func (msg *UserMessage) AddFile(filename string, maxMessageSize int64) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var file File
|
var file File
|
||||||
file.Filename = filename
|
file.Filename = filepath.Base(filename)
|
||||||
file.Size = uint64(fi.Size())
|
file.Size = uint64(fi.Size())
|
||||||
file.Data = data
|
file.Data = data
|
||||||
msg.Files = append(msg.Files, &file)
|
msg.Files = append(msg.Files, &file)
|
||||||
|
|
||||||
msg.Status = &ConversationStatus{}
|
msg.Status = &ConversationStatus{}
|
||||||
msg.Status.LocalUuid = uuid.New().String()
|
msg.Status.Uuid = uuid.New().String()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
1708
messages.pb.go
1708
messages.pb.go
File diff suppressed because it is too large
Load Diff
@@ -26,21 +26,33 @@ message Invitation {
|
|||||||
int32 timeout = 2; // how long do I want the invitation to remain available on the server
|
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 shortcodeLen = 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 shortcode = 4; // shortcode that the friend shall request to get the invitation
|
||||||
string password = 5; // password tou set for accessin invitation (optional)
|
string password = 5; // password to set for accessing invitation (optional)
|
||||||
string uuid = 6; // id that the friend gave you, that you should include to your reply to get recognized
|
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
|
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
|
int32 step = 8; // progress in the inviattion process : 1=invite friend, 2=friend requests invitation, 3=friend's answer
|
||||||
|
string from=9; // used in step 3 the answer public key to check the signature in user message
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// structure for requesting incoming messages
|
// structure for requesting incoming messages
|
||||||
message ConversationRequest {
|
message ConversationRequest {
|
||||||
string lookupKey = 1; // lookup key for a conversation
|
string lookup_key = 1; // lookup key for a conversation
|
||||||
string lastServerUuidOK = 2; // Last Server message UUID received (send me all after that one)
|
bool delivery_request = 2; // look for for delivery tracking, key is implicit, "from" field is used
|
||||||
bool publishOnline = 3; // ?? Publish my online status for that contact ?
|
int64 send_timestamp = 3;
|
||||||
string lookupSignature = 4; // prove that I own the private key by signing that block
|
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
|
// structure defining a message for a server, that will be encrypted, then sent in a "packedmessage" payload
|
||||||
message ToServerMessage {
|
message ToServerMessage {
|
||||||
@@ -48,38 +60,47 @@ message ToServerMessage {
|
|||||||
string from = 2 ; // My pub key for the server to send me an encrypter answer
|
string from = 2 ; // My pub key for the server to send me an encrypter answer
|
||||||
bytes payload = 3 ; // optional payload for server
|
bytes payload = 3 ; // optional payload for server
|
||||||
|
|
||||||
repeated ConversationRequest pullRequest = 4;
|
repeated ConversationRequest pull_request = 4;
|
||||||
|
|
||||||
repeated PackedUserMessage messages = 5;
|
repeated PackedUserMessage messages = 5;
|
||||||
|
|
||||||
repeated ServerCard knownServers = 6;
|
repeated ServerCard known_servers = 6;
|
||||||
|
|
||||||
Matriochka matriochkaMessage = 7;
|
Matriochka matriochka_message = 7;
|
||||||
|
|
||||||
string uuid = 8;
|
string uuid = 8;
|
||||||
|
|
||||||
Invitation invitation = 9; // invitation for the 2 first steps of a "through server" invitation process
|
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
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
message ConversationResponse {
|
|
||||||
repeated string messageUuids = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// structure defining a from server receiver message decrypted from a "packedmessage" payload
|
// structure defining a from server receiver message decrypted from a "packedmessage" payload
|
||||||
message FromServerMessage {
|
message FromServerMessage {
|
||||||
string type = 1; // Type
|
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 ; //
|
bytes payload = 3 ; //
|
||||||
string uuidAck = 4 ; // Ack for the last received ToServerMessage Uuid
|
string uuid_ack = 4 ; // Ack for the last received ToServerMessage Uuid
|
||||||
string serverUuid = 5 ; // Provides the server uuid that replaced the client uuid
|
string server_uuid = 5 ; // Provides the server uuid that replaced the client uuid
|
||||||
|
|
||||||
repeated PackedUserMessage chat = 6;
|
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
|
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 {
|
message MatriochkaServer {
|
||||||
@@ -100,7 +121,7 @@ message Matriochka {
|
|||||||
message ServerCard {
|
message ServerCard {
|
||||||
string name = 1; // friendly server name
|
string name = 1; // friendly server name
|
||||||
string description=2; // description : owner type (company/private/university...),
|
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 url = 4; // meow server url
|
||||||
string login = 5; // required login to access the server
|
string login = 5; // required login to access the server
|
||||||
string password = 6; // password associated to the login
|
string password = 6; // password associated to the login
|
||||||
@@ -110,12 +131,13 @@ message ServerCard {
|
|||||||
// structure describing a user contact card ie the minimum set of attributes for exchanging identities
|
// structure describing a user contact card ie the minimum set of attributes for exchanging identities
|
||||||
message ContactCard {
|
message ContactCard {
|
||||||
string name=1; // contact nickname
|
string name=1; // contact nickname
|
||||||
string contactPublicKey =2; // contact public key, will be used to authenticate her/his messages
|
string contact_public_key =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 encryption_public_key= 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
|
string lookup_public_key =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
|
repeated ServerCard pull_servers =5; // list the servers where the contact will look for messages from you
|
||||||
uint32 version = 6;
|
uint32 version = 6;
|
||||||
string invitationId=7;
|
string invitation_id=7;
|
||||||
|
string invitation_message=8;
|
||||||
}
|
}
|
||||||
|
|
||||||
// structure for sending a message to be forwarded to another user in protobuf format
|
// structure for sending a message to be forwarded to another user in protobuf format
|
||||||
@@ -124,16 +146,18 @@ message PackedUserMessage {
|
|||||||
bytes payload=2; // the message UserMessage encrypted with the destination peer's 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
|
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 serverTimestamp=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
|
||||||
}
|
}
|
||||||
|
|
||||||
message ConversationStatus {
|
message ConversationStatus {
|
||||||
string localUuid = 1;
|
string uuid = 1;
|
||||||
uint64 localSequence = 2 ;
|
string answer_to_uuid=2; // message is an answer to another one, specify uuid here
|
||||||
uint64 sent = 3 ;
|
uint64 localSequence = 3 ; // seq number in local conversation for custom reordering
|
||||||
uint64 received = 4;
|
uint64 sent = 4 ; // timestamp of the message sent
|
||||||
uint64 processed = 5;
|
uint64 received = 5; // timestamp of the message received
|
||||||
ContactCard myNextIdentity = 6;
|
uint64 processed = 6; // timestamp of the message processed
|
||||||
int32 peerNextIdentityAck = 7; // version of the new peer accepted id
|
ContactCard my_next_identity = 7;
|
||||||
|
int32 peer_next_identityAck = 8; // version of the new peer accepted id
|
||||||
}
|
}
|
||||||
|
|
||||||
message Group{
|
message Group{
|
||||||
@@ -141,30 +165,32 @@ message Group{
|
|||||||
repeated ContactCard members = 2;
|
repeated ContactCard members = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// structure defining information that might be exchanged between two peers.
|
// structure defining information that might be exchanged between two peers.
|
||||||
message UserMessage {
|
message UserMessage {
|
||||||
string destination = 1; // Lookupkey
|
string destination = 1; // Lookupkey
|
||||||
string from = 2; // My public key for that contact
|
string from = 2; // My public key for that contact
|
||||||
string type = 3;
|
string type = 3; // Message type
|
||||||
bytes data = 4;
|
bytes data = 4;
|
||||||
|
ConversationStatus status = 5;
|
||||||
ConversationStatus Status = 5;
|
|
||||||
|
|
||||||
ContactCard contact = 6;
|
ContactCard contact = 6;
|
||||||
|
|
||||||
ServerCard knownServers = 7;
|
ServerCard knownServers = 7;
|
||||||
|
|
||||||
Group group = 8;
|
Group group = 8;
|
||||||
|
|
||||||
repeated File files = 9;
|
repeated File files = 9;
|
||||||
|
Location current_location = 10;
|
||||||
Location currentLocation = 10;
|
|
||||||
|
|
||||||
bytes appdata = 11;
|
bytes appdata = 11;
|
||||||
|
|
||||||
Invitation invitation = 12;
|
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 {
|
message File {
|
||||||
string filename=1; // the proposed filename
|
string filename=1; // the proposed filename
|
||||||
uint64 size=2; // the file size
|
uint64 size=2; // the file size
|
||||||
@@ -178,3 +204,31 @@ message Location {
|
|||||||
float longitude=3;
|
float longitude=3;
|
||||||
int32 altitude=4;
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
echo Generating Golang
|
||||||
protoc -I=. --go_out=.. messages.proto
|
protoc -I=. --go_out=.. messages.proto
|
||||||
mv ../forge.redroom.link/yves/meowlib/messages.pb.go ../
|
mv ../forge.redroom.link/yves/meowlib/messages.pb.go ../
|
||||||
rm -rf ../forge.redroom.link
|
rm -rf ../forge.redroom.link
|
||||||
|
echo Generating HTML doc
|
||||||
protoc --plugin=protoc-gen-doc=/usr/bin/protoc-gen-doc \
|
protoc --plugin=protoc-gen-doc=/usr/bin/protoc-gen-doc \
|
||||||
--doc_out=../doc/generated \
|
--doc_out=../doc/generated \
|
||||||
--doc_opt=html,index.html \
|
--doc_opt=html,index.html \
|
||||||
*.proto
|
*.proto
|
||||||
|
echo Generating UML
|
||||||
protoc --plugin=protoc-gen-uml=/usr/bin/protoc-gen-uml \
|
protoc --plugin=protoc-gen-uml=/usr/bin/protoc-gen-uml \
|
||||||
--uml_out=../doc/generated -I=. *.proto
|
--uml_out=../doc/generated -I=. *.proto
|
||||||
|
|||||||
@@ -16,19 +16,24 @@ const key = "3pw0c8#6ZG8{75b5;3?fe80$2"
|
|||||||
type Identity struct {
|
type Identity struct {
|
||||||
ServerName string `json:"servername,omitempty"`
|
ServerName string `json:"servername,omitempty"`
|
||||||
ServerDesc string `json:"serverdesc,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"`
|
Status string `json:"status,omitempty"`
|
||||||
OwnerName string `json:"owner_name,omitempty"`
|
OwnerName string `json:"owner_name,omitempty"`
|
||||||
OwnerPublicKey string `json:"owner_public_key,omitempty"`
|
OwnerPublicKey string `json:"owner_public_key,omitempty"`
|
||||||
ArchiveClients []string `json:"archive_clients,omitempty"`
|
ArchiveClients []string `json:"archive_clients,omitempty"`
|
||||||
KnownServers []meowlib.ServerCard `json:"known_servers,omitempty"`
|
KnownServers []meowlib.ServerCard `json:"known_servers,omitempty"`
|
||||||
|
VideoServer VideoServer `json:"video_server,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateIdentity(ServerName string, ServerDesc string) *Identity {
|
func CreateIdentity(ServerName string, ServerDesc string) *Identity {
|
||||||
var id Identity
|
var id Identity
|
||||||
|
var err error
|
||||||
id.ServerName = ServerName
|
id.ServerName = ServerName
|
||||||
id.ServerDesc = ServerDesc
|
id.ServerDesc = ServerDesc
|
||||||
id.ServerKp = meowlib.NewKeyPair()
|
id.ServerKp, err = meowlib.NewKeyPair()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return &id
|
return &id
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +57,7 @@ func (id *Identity) Save(file string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = os.WriteFile(file, []byte(armor), 0644)
|
err = os.WriteFile(file, []byte(armor), 0600)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,53 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/subtle"
|
||||||
"errors"
|
"errors"
|
||||||
"math/rand"
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-redis/redis"
|
"github.com/go-redis/redis"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r *RedisRouter) CreateInvitation(invitation []byte, timeout int, password string, serverTimeout int, urlLen int) (string, time.Time) {
|
const MaxShortcodeLength = 64
|
||||||
id := r.createShortId(urlLen)
|
|
||||||
|
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 {
|
if timeout > serverTimeout {
|
||||||
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 {
|
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) {
|
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
|
passRequired := false
|
||||||
expectedpass, err := r.Client.Get("mwpw:" + id).Result()
|
expectedpass, err := r.Client.Get("mwpw:" + id).Result()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -28,17 +55,44 @@ func (r *RedisRouter) GetInvitation(id string, password string) ([]byte, error)
|
|||||||
} else {
|
} else {
|
||||||
passRequired = true
|
passRequired = true
|
||||||
}
|
}
|
||||||
if passRequired && password != expectedpass {
|
|
||||||
return nil, errors.New("auth failed")
|
// 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()
|
mwiv, err := r.Client.Get("mwiv:" + id).Result()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear failed attempts counter on successful access
|
||||||
|
r.Client.Del(failedAttemptsKey)
|
||||||
|
|
||||||
return []byte(mwiv), nil
|
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 {
|
if timeout > serverTimeout {
|
||||||
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()
|
return time.Now().Add(time.Duration(timeout * 1000000)).UTC()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RedisRouter) GetInvitationAnswer(id string) ([]byte, error) {
|
func (r *RedisRouter) GetAnswerToInvitation(id string) ([]byte, error) {
|
||||||
mwan, err := r.Client.Get("mwiv:" + id).Result()
|
mwan, err := r.Client.Get("mwan:" + id).Result()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return []byte(mwan), nil
|
return []byte(mwan), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RedisRouter) createShortId(length int) string {
|
func (r *RedisRouter) createShortId(length int) (string, error) {
|
||||||
id := ""
|
|
||||||
alphabet := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
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 not in redis
|
||||||
for {
|
for {
|
||||||
for i := 1; i <= length; i++ {
|
var id strings.Builder
|
||||||
id += string(alphabet[rand.Intn(61)])
|
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)
|
||||||
|
}
|
||||||
|
id.WriteByte(alphabet[n.Int64()])
|
||||||
}
|
}
|
||||||
if r.Client.Get("mwiv:"+id).Err() == redis.Nil {
|
|
||||||
break
|
idStr := id.String()
|
||||||
|
if r.Client.Get("mwiv:"+idStr).Err() == redis.Nil {
|
||||||
|
return idStr, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return id
|
|
||||||
}
|
}
|
||||||
|
|||||||
12
server/logger.go
Normal file
12
server/logger.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
343
server/router.go
343
server/router.go
@@ -2,6 +2,7 @@ package server
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"forge.redroom.link/yves/meowlib"
|
"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) {
|
func (r *RedisRouter) Route(msg *meowlib.ToServerMessage) (*meowlib.FromServerMessage, error) {
|
||||||
var from_server meowlib.FromServerMessage
|
var from_server *meowlib.FromServerMessage
|
||||||
// update messages counter
|
// update messages counter
|
||||||
err := r.Client.Incr("statistics:messages:total").Err()
|
err := r.Client.Incr("statistics:messages:total").Err()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
// user message
|
// user message => store
|
||||||
if len(msg.Messages) > 0 {
|
if len(msg.Messages) > 0 {
|
||||||
// update messages counter
|
logger.Info().Msg("storing message")
|
||||||
err := r.Client.Incr("statistics:messages:usermessages").Err()
|
from_server, err = r.storeMessage(msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
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(usrmsg.Destination, redis.Z{Score: float64(time.Now().Unix()), Member: out})
|
|
||||||
}
|
|
||||||
from_server.UuidAck = msg.Uuid
|
|
||||||
}
|
}
|
||||||
// check for messages
|
// check for messages
|
||||||
if len(msg.PullRequest) > 0 {
|
if len(msg.PullRequest) > 0 {
|
||||||
// update messages counter
|
logger.Info().Msg("checking for messages")
|
||||||
err := r.Client.Incr("statistics:messages:messagelookups").Err()
|
from_server, err = r.checkForMessage(msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
for _, rq := range msg.PullRequest {
|
if msg.Timeout > 0 {
|
||||||
msgcnt, err := r.Client.ZCount(rq.LookupKey, "-inf", "+inf").Result()
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
res, err := r.Client.ZPopMin(rq.LookupKey, msgcnt).Result()
|
}
|
||||||
if err != nil {
|
}
|
||||||
return nil, err
|
// initiate video
|
||||||
}
|
if msg.VideoData != nil {
|
||||||
for _, redismsg := range res {
|
logger.Info().Msg("handling video")
|
||||||
//println(redismsg.Score)
|
from_server, err = r.handleVideo(msg)
|
||||||
val := redismsg.Member
|
if err != nil {
|
||||||
test := val.(string)
|
return nil, err
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// manage Matriochka
|
// manage Matriochka
|
||||||
if msg.MatriochkaMessage != nil {
|
if msg.MatriochkaMessage != nil {
|
||||||
// update messages counter
|
logger.Info().Msg("handling matriochka")
|
||||||
err := r.Client.Incr("statistics:messages:matriochka").Err()
|
from_server, err = r.handleMatriochka(msg)
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
out, err := proto.Marshal(msg)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
// Server list exchange
|
||||||
if len(msg.KnownServers) > 0 {
|
if len(msg.KnownServers) > 0 {
|
||||||
@@ -116,40 +90,10 @@ func (r *RedisRouter) Route(msg *meowlib.ToServerMessage) (*meowlib.FromServerMe
|
|||||||
}
|
}
|
||||||
// Through server invitation process
|
// Through server invitation process
|
||||||
if msg.Invitation != nil {
|
if msg.Invitation != nil {
|
||||||
// update messages counter
|
logger.Info().Msg("handling invitation")
|
||||||
err := r.Client.Incr("statistics:messages:invitation").Err()
|
from_server, err = r.handleInvitation(msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return nil, 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
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
@@ -165,5 +109,240 @@ func (r *RedisRouter) Route(msg *meowlib.ToServerMessage) (*meowlib.FromServerMe
|
|||||||
break
|
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
|
return &from_server, nil
|
||||||
}
|
}
|
||||||
|
|||||||
663
server/router_test.go
Normal file
663
server/router_test.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
41
server/videoserver.go
Normal file
41
server/videoserver.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
12
servercard.go
Normal file
12
servercard.go
Normal file
@@ -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()
|
||||||
|
}
|
||||||
4
setup_req.sh
Executable file
4
setup_req.sh
Executable file
@@ -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
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
package meowlib
|
package meowlib
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func SymEncrypt(password string, data []byte) ([]byte, error) {
|
func SymEncrypt(password string, data []byte) ([]byte, error) {
|
||||||
@@ -12,7 +13,7 @@ func SymEncrypt(password string, data []byte) ([]byte, error) {
|
|||||||
|
|
||||||
pgpMessage, err = crypto.EncryptMessageWithPassword(message, []byte(password))
|
pgpMessage, err = crypto.EncryptMessageWithPassword(message, []byte(password))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "gopenpgp: unable to encrypt message with password")
|
return nil, fmt.Errorf("gopenpgp: unable to encrypt message with password: %w", err)
|
||||||
}
|
}
|
||||||
return pgpMessage.GetBinary(), nil
|
return pgpMessage.GetBinary(), nil
|
||||||
}
|
}
|
||||||
@@ -25,7 +26,7 @@ func SymDecrypt(password string, data []byte) ([]byte, error) {
|
|||||||
pgpMessage = crypto.NewPGPMessage(data)
|
pgpMessage = crypto.NewPGPMessage(data)
|
||||||
message, err = crypto.DecryptMessageWithPassword(pgpMessage, []byte(password))
|
message, err = crypto.DecryptMessageWithPassword(pgpMessage, []byte(password))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "gopenpgp: unable to decrypt message with password")
|
return nil, fmt.Errorf("gopenpgp: unable to decrypt message with password: %w", err)
|
||||||
}
|
}
|
||||||
return message.GetBinary(), nil
|
return message.GetBinary(), nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user