Compare commits

...

415 Commits

Author SHA1 Message Date
Sarah Jamie Lewis 9b235ba732 Merge pull request 'Gate the Import Bundle method for Managed Groups' (#564) from managed-groups into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #564
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2024-06-20 16:55:40 +00:00
Sarah Jamie Lewis 8b7cb44e44
Gate the Import Bundle method for Managed Groups
continuous-integration/drone/pr Build is passing Details
2024-06-18 12:57:21 -07:00
Dan Ballard d5145c631d createProfile return onion if possible; add attribute for private name
continuous-integration/drone/push Build is pending Details
2024-06-15 00:17:35 +00:00
Sarah Jamie Lewis 3e09a25b2d Merge pull request 'Managed Group Refinement' (#561) from managed-groups into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #561
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2024-06-13 17:48:35 +00:00
Sarah Jamie Lewis 229743c507
Managed Group Refinement
continuous-integration/drone/pr Build is passing Details
- Add NoAccessControl Fallback
- Prevent Facade Contacts from the Contact Retry Plugin
- Force Group Manager to Save History by Default
- Fix Ack'ing on Channel_Manager
2024-06-11 11:03:59 -07:00
Sarah Jamie Lewis c5aa6905a4 Merge pull request 'Managed Groups First Cut' (#558) from managed-groups into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #558
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2024-06-10 21:45:46 +00:00
Sarah Jamie Lewis 74d2aec96a
Fixup Channel Setting and Timeouts for Tests
continuous-integration/drone/pr Build is passing Details
2024-06-10 14:04:34 -07:00
Sarah Jamie Lewis 4bce08dc00
Add explicit check when sending offline to Manager Channel
continuous-integration/drone/pr Build is failing Details
2024-06-10 13:04:33 -07:00
Sarah Jamie Lewis 77c6139792
Add Comments indicating current status of hybrid groups
continuous-integration/drone/pr Build is failing Details
2024-06-10 12:15:14 -07:00
Sarah Jamie Lewis a35374f200
Revert
continuous-integration/drone/pr Build is failing Details
2024-06-10 11:08:42 -07:00
Sarah Jamie Lewis e14044e404
Add JSON Annotations
continuous-integration/drone/pr Build is passing Details
Also fix race condition in app map
2024-06-10 10:34:11 -07:00
Sarah Jamie Lewis fdec3302af
Clarify comments, add constants, clean up tests
continuous-integration/drone/pr Build is failing Details
2024-05-13 12:24:22 -07:00
Sarah Jamie Lewis d61dc30bb2
Managed Groups First Cut 2024-05-13 12:24:22 -07:00
Sarah Jamie Lewis a7b885166a Merge pull request 'Enable per-contact file sharing permissions' (#554) from ep into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #554
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2024-04-29 15:37:50 +00:00
Sarah Jamie Lewis b32b11c711
Enable per-contact file sharing permissions
continuous-integration/drone/pr Build is passing Details
2024-04-16 11:35:21 -07:00
Sarah Jamie Lewis 0e96539f22 Merge pull request 'Store Messages and Send when Online' (#553) from offline-messages into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #553
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2024-04-16 18:35:02 +00:00
Sarah Jamie Lewis e55f342324
Updating Logging -> Debug
continuous-integration/drone/pr Build is passing Details
2024-02-26 13:40:47 -08:00
Sarah Jamie Lewis 89aca91b37
Store Messages and Send when Online
continuous-integration/drone/pr Build is passing Details
2024-02-26 13:18:38 -08:00
Sarah Jamie Lewis cd918c02ea Merge pull request 'Fix Error in ACL-V1 that Prevented ShareFiles (for some)' (#552) from acl-v2 into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #552
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2024-02-26 17:26:17 +00:00
Sarah Jamie Lewis 05a198c89f
Fix Error in ACL-V1 that Prevented ShareFiles (for some)
continuous-integration/drone/pr Build is passing Details
Also aligns model.DeserializeAttributes to best practice
2024-02-24 12:51:19 -08:00
Sarah Jamie Lewis 1d9202ff93 Don't reject text messages
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is pending Details
2024-02-12 22:02:35 +00:00
Sarah Jamie Lewis 0907af57d5 Merge pull request 'Introduce Channel/Overlay Mappings' (#549) from overlays into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #549
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2024-02-11 23:10:59 +00:00
Sarah Jamie Lewis 826ac40a5c Stream check in engine
continuous-integration/drone/pr Build is pending Details
2024-02-11 14:45:11 -08:00
Sarah Jamie Lewis 1a034953df Util Functions for MW
continuous-integration/drone/pr Build is pending Details
2024-02-11 14:44:18 -08:00
Sarah Jamie Lewis 3124f7b7c4 MessageOverlay time to pointer
continuous-integration/drone/pr Build is pending Details
2024-02-11 13:56:19 -08:00
Sarah Jamie Lewis 792e79dceb Introduce Channel/Overlay Mappings
continuous-integration/drone/pr Build is failing Details
- Map channel 7 to ephemeral / no ack
- Create model methods
- Introduce optional latency measurements into Cwtch
2024-02-11 12:14:07 -08:00
Sarah Jamie Lewis 3e0680943a Prevent Duplicate Queue Subscription
continuous-integration/drone/pr Build is pending Details
continuous-integration/drone/push Build is failing Details
2024-02-09 13:16:23 -08:00
Sarah Jamie Lewis 9cb62d269e Merge pull request 'Fix non-image/preview downloads in Android' (#547) from android_file_download_fix into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #547
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2024-02-09 21:06:25 +00:00
Sarah Jamie Lewis ec71e56d23 Fix non-image/preview downloads in Android
continuous-integration/drone/pr Build is pending Details
2024-02-09 11:33:25 -08:00
Sarah Jamie Lewis aaabb12b6c Merge pull request 'First Cut of Enhanced Permissions' (#543) from enhanced-permissions into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #543
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2024-01-15 18:04:34 +00:00
Sarah Jamie Lewis b0a87ee8d0 Move comment for better understanding
continuous-integration/drone/pr Build is pending Details
2024-01-11 10:06:08 -08:00
Sarah Jamie Lewis d66beb95e5 Update APIs, Formatting
continuous-integration/drone/pr Build is pending Details
2024-01-11 10:02:27 -08:00
Sarah Jamie Lewis 41b3e20aff Remove Flakey Queued Check in Contact Retry Plugin Test
continuous-integration/drone/pr Build is passing Details
2024-01-08 13:25:53 -08:00
Sarah Jamie Lewis 1c7003fb96 First Draft of Enhanced Permissions API
continuous-integration/drone/pr Build is pending Details
2024-01-08 13:22:38 -08:00
Dan Ballard cb3b0b4c46 add new setting themeImages and fix default themeing
continuous-integration/drone/pr Build is pending Details
continuous-integration/drone/push Build is pending Details
2024-01-06 12:04:47 -08:00
Sarah Jamie Lewis a18c19bbf2 Fix Contact Retry Failure to Restart (#541)
continuous-integration/drone/push Build is pending Details
commit daea5128c0 (HEAD -> post-stable-fixes, origin/post-stable-fixes)
Author: Sarah Jamie Lewis <sarah@openprivacy.ca>
Date:   Tue Jan 2 12:45:39 2024 -0800

    Fixup Connection Test to check reconnecting status

commit 347ac3cf48
Author: Sarah Jamie Lewis <sarah@openprivacy.ca>
Date:   Tue Jan 2 12:33:31 2024 -0800

    Fixup Formatting and Quality Script

    ineffassign and misspell are no longer compatible with previous
    go workflows and the latest versions do not work. Commenting for
    now with intent to replace with better tooling.

commit d9ce7737cc
Author: Sarah Jamie Lewis <sarah@openprivacy.ca>
Date:   Tue Jan 2 12:24:33 2024 -0800

    Fix Contact Retry Failure to Restart

    When toggling between connected and disconnected, the Contact Retry plugin
    could find itself in a state where the new event would never get requeued.

    Also: Make the unsigned nature of limit in GetMessage* Apis explicit.

Reviewed-on: #541
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2024-01-02 23:17:59 +00:00
Sarah Jamie Lewis be4230d16e Merge pull request 'Small fixes pass with upgraded staticcheck and nilaway' (#539) from fixups into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #539
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2024-01-02 20:46:09 +00:00
Sarah Jamie Lewis 34957f809b Update ChunkSpec initialization
continuous-integration/drone/pr Build is failing Details
2023-11-19 14:45:08 -08:00
Sarah Jamie Lewis 456a5f5c4d Small fixes pass with upgraded staticcheck and nilaway
continuous-integration/drone/pr Build is failing Details
2023-11-18 11:51:27 -08:00
Sarah Jamie Lewis 657fb76b04 Merge pull request 'PublishServerUpdate error' (#536) from stable-blockers into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #536
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2023-09-26 20:12:48 +00:00
Sarah Jamie Lewis c0bc3b0803 PublishServerUpdate error
continuous-integration/drone/pr Build is pending Details
2023-09-26 20:07:08 +00:00
Sarah Jamie Lewis 7a962359b3 Merge pull request 'Add Contacts to Queue in the Background to Avoid Activation Blocking' (#535) from stable-blockers into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #535
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2023-09-25 18:35:48 +00:00
Sarah Jamie Lewis 935b4a1103 Add Contacts to Queue in the Background to Avoid Activation Blocking
continuous-integration/drone/pr Build is passing Details
2023-09-25 11:22:22 -07:00
Sarah Jamie Lewis 51d146fb5c Merge pull request 'Activate Peers After Purging Retries' (#534) from stable-blockers into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #534
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2023-09-20 00:01:02 +00:00
Sarah Jamie Lewis 6d9e892408 Activate Peers After Purging Retries
continuous-integration/drone/pr Build is pending Details
2023-09-19 22:38:42 +00:00
Sarah Jamie Lewis 44856003d6 Merge pull request 'Properly manage contact retries during mode switching' (#533) from stable-blockers into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #533
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2023-09-19 20:01:45 +00:00
Sarah Jamie Lewis f16eeb1922 Properly manage contact retries during mode switching
continuous-integration/drone/pr Build is passing Details
Fixes a small file shareing management issue where a file was being marked as inactive because the timestamp wasn't updated.
2023-09-19 12:22:48 -07:00
Sarah Jamie Lewis 13583f3e8c Merge pull request 'Fixup Contact Retry to Play Nicely with Appear Offline Mode' (#532) from stable-blockers into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #532
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2023-09-18 15:05:41 +00:00
Sarah Jamie Lewis 58b1008cae Fixup Contact Retry to Play Nicely with Appear Offline Mode
continuous-integration/drone/pr Build is passing Details
2023-09-18 07:47:03 -07:00
Sarah Jamie Lewis 45d6d76a7d Merge pull request 'Support Appear Offline / Disconnect from Server/Peer' (#531) from stable-blockers into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #531
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2023-09-13 18:49:09 +00:00
Sarah Jamie Lewis f42e25e926 Typo Fix
continuous-integration/drone/pr Build is pending Details
2023-09-13 11:48:47 -07:00
Sarah Jamie Lewis 7538f1a531 Enable Group Experiment in Main Test
continuous-integration/drone/pr Build is passing Details
2023-09-13 10:49:33 -07:00
Sarah Jamie Lewis a5cea1ca7b ConfigureConnections in Tests
continuous-integration/drone/pr Build was killed Details
2023-09-13 10:30:32 -07:00
Sarah Jamie Lewis e311301d72 Support Appear Offline / Disconnect from Server/Peer
continuous-integration/drone/pr Build was killed Details
2023-09-13 10:07:23 -07:00
Sarah Jamie Lewis 7464e3922d Merge pull request 'Allow force restarting of file shares regardless of timestamp.' (#530) from stable-blockers into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #530
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2023-08-31 18:51:40 +00:00
Sarah Jamie Lewis 298a8d8aea Unsub Server Functionality from Heartbeats
continuous-integration/drone/pr Build is pending Details
2023-08-29 13:01:40 -07:00
Sarah Jamie Lewis 75a3c14285 Nicer test Scheduling
continuous-integration/drone/pr Build is passing Details
2023-08-29 12:26:51 -07:00
Sarah Jamie Lewis 407902b8ee Minimize Event Noise for Server Updates / Handle Blocking Flow for ContactRetry plugin
continuous-integration/drone/pr Build is failing Details
2023-08-29 12:20:08 -07:00
Sarah Jamie Lewis 6d29ca322e Redirect JoinServer Flow. Have Servers listen to QueueJoinServer Update. Handle delete contact flow for contact retry plugin 2023-08-29 12:16:49 -07:00
Sarah Jamie Lewis fb164b104b Format
continuous-integration/drone/pr Build is pending Details
2023-08-28 13:35:54 -07:00
Sarah Jamie Lewis 048effc91a contactRetry test needs to use a valid onion
continuous-integration/drone/pr Build is passing Details
2023-08-28 13:34:24 -07:00
Sarah Jamie Lewis ca63205934 Quality Fixup
continuous-integration/drone/pr Build is failing Details
2023-08-28 13:23:25 -07:00
Sarah Jamie Lewis 0997406e51 Limit connectionRetry attempts to requested peers/servers
continuous-integration/drone/pr Build is failing Details
There is a bug where spurious PeerStateChange events from failed auth
attempts will make their way into contact retry plugin and result in
attempts that will *always* fail.

Note: This would also happen in the case of blocked peers *however* these would be short-circuit failed in engine also.
2023-08-28 13:17:55 -07:00
Sarah Jamie Lewis 602041d1c2 Allow force restarting of file shares regardless of timestamp.
continuous-integration/drone/pr Build is passing Details
Move RestartFileShare to FileSharingFunctionality where it belongs.
2023-08-28 09:48:10 -07:00
Sarah Jamie Lewis 95527f8978 Merge pull request 'Support Save History Default + Delete Server' (#529) from stable-blockers into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #529
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2023-08-22 20:22:23 +00:00
Sarah Jamie Lewis d5c3795f13 Remove Unneeded Field
continuous-integration/drone/pr Build is passing Details
2023-08-21 10:29:05 -07:00
Sarah Jamie Lewis 51f993973c Fixup Keys
continuous-integration/drone/pr Build is pending Details
2023-08-21 10:26:44 -07:00
Sarah Jamie Lewis 5b2b839865 Update Dependencies
continuous-integration/drone/pr Build is pending Details
2023-08-21 09:33:54 -07:00
Sarah Jamie Lewis 151e25b607 Rename DeleteServer to DeleteServerInfo to avoid API Clash
continuous-integration/drone/pr Build is pending Details
2023-08-21 09:32:38 -07:00
Sarah Jamie Lewis fac34ad814 Move responsibility for delete history default to Settings (where it should be)
continuous-integration/drone/pr Build is pending Details
2023-08-17 09:47:15 -07:00
Sarah Jamie Lewis aae8a7fc03 Spelling
continuous-integration/drone/pr Build is pending Details
2023-08-14 13:19:52 -07:00
Sarah Jamie Lewis e1877d69b7 Better Comments on History Keys
continuous-integration/drone/pr Build is pending Details
2023-08-14 13:18:35 -07:00
Sarah Jamie Lewis 066ed86598 Support Save History Default + Delete Server
continuous-integration/drone/pr Build is passing Details
2023-08-14 11:47:59 -07:00
Sarah Jamie Lewis 4db041f850 Register Heartbeat Event for Server Functionality
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is pending Details
2023-07-27 11:22:57 -07:00
Sarah Jamie Lewis 546180d65e Merge pull request 'Add RowIndex field to search results for more efficient UI searching' (#526) from search into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #526
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2023-07-27 18:08:20 +00:00
Sarah Jamie Lewis 9dbc398690 Add RowIndex field to search results for more efficient UI searching
continuous-integration/drone/pr Build is passing Details
2023-07-27 17:46:24 +00:00
Sarah Jamie Lewis b27229091a Merge pull request 'contact retry force disconnect internally any connecting over 2xcircut timeout' (#521) from crForceDisconn into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #521
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2023-07-25 21:22:38 +00:00
Dan Ballard 1f2617e4ae contact retry force disconnect internally any connecting over 2xcircut timeout
continuous-integration/drone/pr Build is pending Details
2023-07-25 21:22:31 +00:00
Sarah Jamie Lewis 6b212beb00 Merge pull request 'Move server handling logic back into Cwtch (from libCwtch-go / autobindings)' (#525) from server-update into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #525
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2023-07-25 19:03:20 +00:00
Sarah Jamie Lewis f2ad64fe8b Formatting / Linting
continuous-integration/drone/pr Build is passing Details
2023-07-25 11:19:23 -07:00
Sarah Jamie Lewis 8d7052bb8d Move server handling logic back into Cwtch (from libCwtch-go / autobindings)
continuous-integration/drone/pr Build is failing Details
2023-07-25 18:14:02 +00:00
Sarah Jamie Lewis a47d916eac Merge pull request 'Implement basic any-prefix/suffix matching for SearchConversations' (#524) from conversation_search into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #524
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2023-07-25 17:59:58 +00:00
Sarah Jamie Lewis 3a7d2fce05 Implement basic any-prefix/suffix matching for SearchConversations
continuous-integration/drone/pr Build is passing Details
2023-07-25 10:29:38 -07:00
Sarah Jamie Lewis 3f1e2d7a14 Merge pull request 'First cut of Conversation Search' (#518) from conversation_search into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #518
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2023-07-13 19:39:41 +00:00
Sarah Jamie Lewis 1e0cbe1dc6 Refine Connection Logic
continuous-integration/drone/pr Build is passing Details
2023-07-13 11:48:14 -07:00
Sarah Jamie Lewis 77e4e981e8 Formatting
continuous-integration/drone/pr Build is pending Details
2023-07-11 13:21:59 -07:00
Sarah Jamie Lewis b84de2aa61 Fix bug in Engine that leaked Peer Connecting Status 2023-07-11 13:21:59 -07:00
Sarah Jamie Lewis 75eb49d6ee Fix maxCount calculation 2023-07-11 13:21:59 -07:00
Sarah Jamie Lewis cfb2335c05 First cut of Conversation Search 2023-07-11 13:21:59 -07:00
Sarah Jamie Lewis 31f397e332 Merge pull request 'fix contact Retry timeout logic' (#519) from fixCR into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #519
2023-07-11 20:20:36 +00:00
Dan Ballard eb0636a229 fix contact Retry timeout logic
continuous-integration/drone/pr Build is pending Details
2023-07-07 08:32:48 -07:00
Sarah Jamie Lewis def585b23b Merge pull request 'Force cid conversation to string in DeleteContact event' (#517) from deletecontactfix into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #517
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2023-06-13 17:48:50 +00:00
Sarah Jamie Lewis 9605894463 Force Error Log if NewEventList attempts to publish an invalid field
continuous-integration/drone/pr Build is passing Details
2023-06-13 10:26:20 -07:00
Sarah Jamie Lewis 2bbe0c48d6 Force cid conversation to string in DeleteContact event
continuous-integration/drone/pr Build was killed Details
2023-06-13 10:17:52 -07:00
Sarah Jamie Lewis 655b1cf208 Merge pull request 'Add additional information to DeleteContact event' (#516) from deletecontactfix into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #516
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2023-06-13 17:07:02 +00:00
Sarah Jamie Lewis 86ae2a7c1a Add additional information to DeleteContact event
continuous-integration/drone/pr Build is passing Details
2023-06-12 11:45:54 -07:00
Sarah Jamie Lewis cff2a8cafe Merge pull request 'Fix Various Bugs Associated with Profile Start Up / Restart' (#515) from startupbugs into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #515
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2023-05-16 23:21:40 +00:00
Sarah Jamie Lewis 035c6c669f Formatting / Remove Debug
continuous-integration/drone/pr Build is passing Details
2023-05-16 15:56:07 -07:00
Sarah Jamie Lewis 462a294c93 Add ProtocolEngine test case to ContactRetry plugin
continuous-integration/drone/pr Build was killed Details
2023-05-16 15:47:49 -07:00
Sarah Jamie Lewis f982e55c4f Safety check on unreachable case
continuous-integration/drone/pr Build is pending Details
2023-05-16 15:45:56 -07:00
Sarah Jamie Lewis bc522b57c1 Close connection in unreachable case
continuous-integration/drone/pr Build is pending Details
2023-05-16 15:45:05 -07:00
Sarah Jamie Lewis 8fd6d5ead2 Fix Various Bugs Associated with Profile Start Up / Restart
continuous-integration/drone/pr Build is failing Details
2023-05-16 22:42:44 +00:00
Sarah Jamie Lewis 50cca925de Merge pull request 'Add a setting to preserve custom font scaling setting' (#514) from font-setting into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #514
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2023-05-09 19:43:51 +00:00
Sarah Jamie Lewis b81353c128 Add a setting to preserve custom font scaling setting
continuous-integration/drone/pr Build is passing Details
2023-05-09 12:19:19 -07:00
Sarah Jamie Lewis 05cc347ba2 Merge pull request 'Remove RetryPeer event, Poke token count on new group' (#513) from events into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #513
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2023-05-09 18:24:31 +00:00
Sarah Jamie Lewis 92eed46c56 Adding a Test for Contact Retry; Adding jump the queue shortcuts for priority peers
continuous-integration/drone/pr Build is passing Details
2023-05-09 10:43:07 -07:00
Sarah Jamie Lewis 2abfaf82a1 Fix Race Condition
continuous-integration/drone/pr Build is passing Details
2023-05-02 13:45:19 -07:00
Sarah Jamie Lewis f5c397876b Update Conversation Timestamp 2023-05-02 13:04:53 -07:00
Sarah Jamie Lewis 3b822393cd Remove RetryPeer event, Poke token count on new group
continuous-integration/drone/pr Build is passing Details
2023-05-02 19:28:59 +00:00
Dan Ballard 7053f4a31b remove peerlock probably left over from peerapp seperation
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build was killed Details
2023-05-01 16:13:39 -05:00
Dan Ballard e9e2a18678 fix?
continuous-integration/drone/pr Build is failing Details
2023-04-28 15:00:23 -06:00
Dan Ballard 440b7f422c move event handling for AcnStatus engine reboot from lcg into app 2023-04-28 15:00:15 -06:00
Dan Ballard 12b89966de engine shutdown now puts potentially long blocking service.close()s in goroutine; contact retry more smartly handles protocolengine start in case last ACNstatus == 100 message comes first
continuous-integration/drone/pr Build is pending Details
2023-04-27 15:16:24 -06:00
Sarah Jamie Lewis 70c335df81 Merge pull request 'Make DelteProfile and ShutdownPeer safe to call twice / with incorrect onion' (#510) from fuzzbot into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #510
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2023-04-22 01:48:10 +00:00
Sarah Jamie Lewis 8ab0e9993a Make DelteProfile and ShutdownPeer safe to call twice / with incorrect onion
continuous-integration/drone/pr Build is passing Details
2023-04-21 14:22:09 -07:00
Sarah Jamie Lewis 48e5f44f84 Merge pull request 'Add UpdatedConversationAttribute Event for the UI' (#509) from fuzzbot into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #509
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2023-04-20 22:27:11 +00:00
Sarah Jamie Lewis 79c51b0e6d Add Conversation info in UCA
continuous-integration/drone/pr Build is passing Details
2023-04-20 15:18:51 -07:00
Sarah Jamie Lewis 4e0fbbc1de Add UpdatedConversationAttribute Event for the UI
continuous-integration/drone/pr Build is pending Details
2023-04-20 15:14:09 -07:00
Sarah Jamie Lewis d9298f84b2 Merge pull request 'Enable a SendPeerMessage EngineHook for Fuzzbot' (#508) from fuzzbot into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #508
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2023-04-20 21:00:14 +00:00
Sarah Jamie Lewis 210c91f7f7 Mutex enginehooks
continuous-integration/drone/pr Build is passing Details
2023-04-20 13:38:54 -07:00
Sarah Jamie Lewis 746bfffb7c EngineHooks into enginehooks.go
continuous-integration/drone/pr Build is pending Details
2023-04-20 13:38:10 -07:00
Sarah Jamie Lewis 93c9813d96 Move EngineHooks into Protocol
continuous-integration/drone/pr Build was killed Details
2023-04-20 13:36:43 -07:00
Sarah Jamie Lewis 7255a6c71e Fixup EngineHook API
continuous-integration/drone/pr Build is pending Details
2023-04-20 13:33:55 -07:00
Sarah Jamie Lewis 5f448ac2c2 Enable a SendPeerMessage EngineHook for Fuzzbot 2023-04-20 13:33:55 -07:00
Sarah Jamie Lewis 02fe9323c4 Merge pull request 'Expose a Default Limit version of VerifyorResumeDownload' (#507) from code-fixes into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #507
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2023-04-18 20:49:55 +00:00
Sarah Jamie Lewis af0914103d Expose a Default Limit version of VerifyorResumeDownload
continuous-integration/drone/pr Build was killed Details
2023-04-18 13:25:29 -07:00
Sarah Jamie Lewis 3967cceb83 Merge pull request 'Verify File Manifest Prior to Profile Images Downloads (+remove Android specific checks)' (#506) from code-fixes into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #506
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2023-04-18 20:00:48 +00:00
Sarah Jamie Lewis 221c55868e Optimisitcally verify downloads in engine
continuous-integration/drone/pr Build is passing Details
2023-04-18 11:20:46 -07:00
Sarah Jamie Lewis cbfead7455 Remove Android guard on duplication checks
continuous-integration/drone/pr Build is passing Details
2023-04-18 11:05:36 -07:00
Sarah Jamie Lewis c4460b67a1 Merge pull request 'Small Code Fixups' (#505) from code-fixes into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #505
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2023-04-18 03:25:27 +00:00
Sarah Jamie Lewis dbac41d949 Fixup Mkdir Errors
continuous-integration/drone/pr Build is passing Details
2023-04-17 12:33:53 -07:00
Sarah Jamie Lewis f3296ffdd9 Small Code Fixups 2023-04-17 12:33:53 -07:00
Sarah Jamie Lewis 28ddbcc132 Merge pull request 'Switch to sync.Map because go maps are unsound' (#504) from fixpanic into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #504
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2023-04-06 02:45:35 +00:00
Sarah Jamie Lewis cccb97d5f0 Switch to sync.Map because go maps are unsound
continuous-integration/drone/pr Build is passing Details
2023-04-05 19:31:00 -07:00
Sarah Jamie Lewis 2e59cc43ab Merge pull request 'Support Profile Status and Profile Attributes. Auto Fetch Updates on a Heartbeat. Move Profile Image Download Checks to Cwtch' (#503) from autodownload into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #503
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2023-04-04 21:04:02 +00:00
Sarah Jamie Lewis 51f85ea619 Fix queue shutdown
continuous-integration/drone/pr Build is passing Details
2023-04-03 14:58:34 -07:00
Sarah Jamie Lewis 7107ad1eaa Close Heartbeat Queue
continuous-integration/drone/pr Build is failing Details
2023-04-03 14:49:45 -07:00
Sarah Jamie Lewis 4d81529ce2 Update Profile Extension to remove Duplication
continuous-integration/drone/pr Build is failing Details
2023-04-03 14:33:25 -07:00
Sarah Jamie Lewis 4588cbc604 Support Profile Status and Profile Attributes. Auto Fetch Updates on a Heartbeat. Move Profile Image Download Checks to Cwtch
continuous-integration/drone/pr Build is failing Details
2023-04-03 12:45:28 -07:00
Sarah Jamie Lewis e94964c583 Merge pull request 'Assert 64 bit file sizes even on 32 bit systems' (#502) from autodownload into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #502
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2023-03-16 22:05:53 +00:00
Sarah Jamie Lewis 08c6cdd858 Assert 64 bit file sizes even on 32 bit systems
continuous-integration/drone/pr Build is passing Details
2023-03-16 14:43:45 -07:00
Sarah Jamie Lewis b02d9f7fb9 Merge pull request 'Port Autodownload / Image Previews / Profile Image Experiment to Cwtch' (#501) from autodownload into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #501
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2023-03-13 20:04:36 +00:00
Sarah Jamie Lewis 264b8b9363 Ensure Settings Updates are Applied to Experiments
continuous-integration/drone/pr Build is passing Details
2023-03-13 12:49:29 -07:00
Sarah Jamie Lewis fcb07042d7 Extend Test Clean Up Time
continuous-integration/drone/pr Build is passing Details
2023-03-06 13:44:12 -08:00
Sarah Jamie Lewis de32ae240a Remove Queue Oracle 2023-03-06 13:43:57 -08:00
Sarah Jamie Lewis 186a33deb6 Autocreate Download Folder in Test
continuous-integration/drone/pr Build is failing Details
2023-03-06 13:27:45 -08:00
Sarah Jamie Lewis 0139f7a5a9 Skip processed error if an experiment *might* have flagged this event
continuous-integration/drone/pr Build is pending Details
2023-03-06 13:20:42 -08:00
Sarah Jamie Lewis d50f210e35 Port Autodownload / Image Previews / Profile Image Experiment to Cwtch
continuous-integration/drone/pr Build is failing Details
2023-03-06 13:08:29 -08:00
Sarah Jamie Lewis 7bb75e4365 Merge pull request 'Add support for "enhanced" sendinvite' (#500) from esi into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #500
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2023-03-02 20:43:18 +00:00
Sarah Jamie Lewis 0ea5cbba31 Add support for "enhanced" sendinvite
continuous-integration/drone/pr Build is passing Details
2023-03-02 10:52:15 -08:00
Sarah Jamie Lewis 456afb0262 Merge pull request 'Initial Prototype of Event Hooks' (#488) from eventhooks into master
continuous-integration/drone/push Build is failing Details
Reviewed-on: #488
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2023-02-28 18:59:49 +00:00
Sarah Jamie Lewis 243b827522 bool -> atomic.Bool to prevent "race condition"
continuous-integration/drone/pr Build is passing Details
2023-02-28 10:33:00 -08:00
Sarah Jamie Lewis a6a196a1c1 Load App Settings Tests
continuous-integration/drone/pr Build is failing Details
2023-02-28 10:13:45 -08:00
Sarah Jamie Lewis 14962e2428 Logging Fixes / InitApp -> InitAppSettings 2023-02-28 10:13:45 -08:00
Sarah Jamie Lewis aceb4adeb1 Support for Enhanced Import 2023-02-28 10:13:45 -08:00
Sarah Jamie Lewis 848d5971b6 Consolidating Profile Setup Logic 2023-02-28 10:13:45 -08:00
Sarah Jamie Lewis 9abece0f50 Reorganize Peer Init 2023-02-28 10:13:45 -08:00
Sarah Jamie Lewis 05e77604d2 Experiments Update on Load 2023-02-28 10:13:45 -08:00
Sarah Jamie Lewis 195e048410 Fix Map Panic 2023-02-28 10:13:45 -08:00
Sarah Jamie Lewis 0e49d70d65 Large API Refactor in prep for autobindings 2023-02-28 10:13:45 -08:00
Sarah Jamie Lewis 861390b11d Rename API 2023-02-28 10:13:45 -08:00
Sarah Jamie Lewis f246ea1e40 FileSharing Experiments / Move Experiment Handling to App and Cwtch Peer 2023-02-28 10:13:45 -08:00
Sarah Jamie Lewis 26c5c11216 Initial Prototype of Event Hooks 2023-02-28 10:13:45 -08:00
Sarah Jamie Lewis 697b3df54c Log Errors related to Sharing Files 2023-02-28 10:13:45 -08:00
Sarah Jamie Lewis a698f34bfa Merge pull request 'contact retry handle engine shutdown better' (#484) from contactRetryDisconn into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #484
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2022-12-14 18:52:12 +00:00
Sarah Jamie Lewis c946ff5574 Merge branch 'master' into contactRetryDisconn
continuous-integration/drone/pr Build is pending Details
2022-12-14 18:51:46 +00:00
Dan Ballard 3bb2b0988e contact retry handle engine shutdown better
continuous-integration/drone/pr Build is passing Details
2022-12-13 16:13:37 -08:00
Dan Ballard 2876fdf7f4 Merge pull request 'Remove download directory checks for Android' (#483) from priority into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #483
2022-12-12 22:47:03 +00:00
Sarah Jamie Lewis ea3ef33ac5 Remove download directory checks for Android
continuous-integration/drone/pr Build is passing Details
2022-12-12 13:17:54 -08:00
Sarah Jamie Lewis 4e2000cae4 Merge pull request 'CreateProfile takes attributes' (#482) from createProfileAttr into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #482
2022-12-11 03:42:59 +00:00
Dan Ballard 32a02b68dc CreateProfile takes attributes
continuous-integration/drone/pr Build is passing Details
2022-12-10 11:50:22 -08:00
Sarah Jamie Lewis 667fc15294 Remove Queue Breaks
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is pending Details
2022-12-07 13:23:16 -08:00
Sarah Jamie Lewis 5ef2f6f94c Make priority queue criteria a const. Remove inner loop
continuous-integration/drone/pr Build is passing Details
2022-12-07 12:55:58 -08:00
Sarah Jamie Lewis 06a2539502 Priority Queue Most Common Contact Requests
continuous-integration/drone/pr Build is passing Details
2022-12-07 11:30:11 -08:00
Sarah Jamie Lewis bfe8b1e51f Restrict Active Connections to Those Found in the Last Week 2022-12-07 11:13:37 -08:00
Sarah Jamie Lewis 7de9c21f7b Merge pull request 'for getConnectionsSortedByLastSeen, ignore accepted on servers' (#480) from serverAccept into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #480
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2022-12-06 05:28:31 +00:00
Dan Ballard 491ff6e710 GetConversationLastSeenTime use constants.SyncMostRecentMessageTime for servers; fix time parsing error in contact retry
continuous-integration/drone/pr Build is passing Details
2022-12-05 21:07:09 -08:00
Dan Ballard 6eef88fc2d for getConnectionsSortedByLastSeen, ignore accepted on servers
continuous-integration/drone/pr Build is failing Details
2022-12-05 19:25:44 -08:00
Sarah Jamie Lewis f630dedab6 Merge pull request 'info->debug fixes; rearrange integ test wait for connections for hopeful speed improvement' (#479) from connectionLogic into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #479
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2022-12-05 16:30:14 +00:00
Dan Ballard ca309096eb doubled bob download timeout
continuous-integration/drone/pr Build is pending Details
2022-12-05 16:30:07 +00:00
Dan Ballard 58921e381b increase integ test timeout, sometimes tor is slow, its slower restarting a bunch of times; poll on retvals 2022-12-05 16:30:07 +00:00
Dan Ballard 530f2d9773 integ test wait for group message to be acked 2022-12-05 16:30:07 +00:00
Dan Ballard bdb4b93f59 make ActivatePeerEngine safe to recall 2022-12-05 16:30:07 +00:00
Dan Ballard 06d402c4d7 info->debug fixes; rearrange integ test wait for connections for hopeful speed improvement 2022-12-05 16:30:07 +00:00
Sarah Jamie Lewis 321b08bfd3 Prevent Peer Queue Close from being called more than Once on Shutdown
continuous-integration/drone/push Build is failing Details
2022-12-04 07:05:40 +00:00
Dan Ballard c8a6a1b079 contactRetry has protocol engine existence awareness (prep for turning profiles on/off)
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is pending Details
2022-12-03 15:48:09 -08:00
Dan Ballard 5658e9aa9f race fixes
continuous-integration/drone/pr Build is passing Details
2022-12-03 10:39:10 -08:00
Dan Ballard 2a877ff408 remove locking/atomic from contactRetry as its single threaded
continuous-integration/drone/pr Build is failing Details
2022-12-03 10:02:13 -08:00
Dan Ballard 726fe28498 remove locking/atomic from contactRetry as its single threaded 2022-12-03 09:49:32 -08:00
Dan Ballard ad72ce6e7a add to app ActivatePeerEngine; add to peer StartConnections; order connection attempts by lastseend (track); massive connection retry rework
continuous-integration/drone/pr Build is pending Details
2022-12-03 09:26:30 -08:00
Dan Ballard 6d8f31773e add activateEngine to app to handle multiple profiles a little more gracefully; lauchPeerConnections sorts based on last message time; contactRetry slow downs and partial state tracking of circuit queue for adaptive slow downs 2022-12-02 16:40:21 -08:00
Sarah Jamie Lewis 9ef244bc80 Merge pull request 'Allow using cached tokens for local integ testing' (#470) from cached_tokens into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #470
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2022-11-30 16:51:55 +00:00
Sarah Jamie Lewis e319976832 Load->StoreCachedTokens
continuous-integration/drone/pr Build is passing Details
2022-11-30 07:58:37 -08:00
Sarah Jamie Lewis 0ba45cd59a Move Cached Token Loading into Server Join (from SMTG)
continuous-integration/drone/pr Build is passing Details
2022-11-23 08:01:22 -08:00
Sarah Jamie Lewis 4324ffae03 safely close db when cps fails
continuous-integration/drone/pr Build is failing Details
2022-10-25 14:05:32 -07:00
Sarah Jamie Lewis f2b879a9c4 Upgrade Tapir / Fix Token Acquisition
continuous-integration/drone/pr Build is failing Details
2022-10-25 20:59:50 +00:00
Sarah Jamie Lewis c66561d84f Allow using cached tokens for local integ testing
(also new TORCACHE env for integ testing to speed up bootstrapping locally)
2022-10-25 20:59:50 +00:00
Dan Ballard 8a1f9376e2 Merge pull request 'New Cwtch Tool App, Extract MakePayment into a standalone function, Fix race condition in engine.' (#468) from indents_and_tools into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #468
2022-10-03 21:03:47 +00:00
Sarah Jamie Lewis a84d627926 Merge branch 'master' into indents_and_tools
continuous-integration/drone/pr Build is passing Details
2022-10-03 20:34:46 +00:00
Sarah Jamie Lewis 120a2136b2 Merge branch 'go_update' into indents_and_tools
continuous-integration/drone/pr Build is pending Details
2022-10-03 13:33:49 -07:00
Dan Ballard b06f32b9e2 Merge pull request 'Update Go to 1.19.1' (#469) from go_update into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #469
2022-10-03 20:33:11 +00:00
Sarah Jamie Lewis 9c4ed7cc7b Update Go to 1.19.1
continuous-integration/drone/pr Build is failing Details
2022-10-03 13:15:49 -07:00
Sarah Jamie Lewis bb0246b8d9 go update
continuous-integration/drone/pr Build is failing Details
2022-10-03 13:05:42 -07:00
Sarah Jamie Lewis 7863ed2aef Fix indents 2022-10-03 13:05:42 -07:00
Sarah Jamie Lewis cf036bdee4 fix race condition in engine 2022-10-03 13:05:42 -07:00
Sarah Jamie Lewis 9c65ad4af3 Add cwtchtools, add make payment tool 2022-10-03 13:05:42 -07:00
Sarah Jamie Lewis 0e10b47c42 Merge pull request 'Fixup ProtocolEngine Shutdown' (#461) from protocl_engine_shutdown_fix into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #461
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2022-09-10 19:08:27 +00:00
Sarah Jamie Lewis 0b72a90b1f Fixup ProtocolEngine Shutdown
continuous-integration/drone/pr Build is failing Details
2022-09-10 11:57:54 -07:00
Sarah Jamie Lewis 35ca930628 Merge pull request 'timeout_fixes_tokens' (#460) from timeout_fixes_tokens into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #460
2022-09-10 18:43:58 +00:00
Sarah Jamie Lewis 8d2134c4db fix comments
continuous-integration/drone/pr Build is passing Details
2022-09-10 10:36:48 -07:00
Sarah Jamie Lewis 0f4c6de2e6 quality 2022-09-10 10:34:36 -07:00
Sarah Jamie Lewis 27cec93ad7 Adjust contact retry 2022-09-10 10:33:17 -07:00
Sarah Jamie Lewis d455eb6477 Fix Issues with Antispam triggering / Add explicit timeout calls for group servers / token aquisition and optimistic closing for peers 2022-09-10 10:18:42 -07:00
Sarah Jamie Lewis f52919271c Merge pull request 'app Shutdown uses shutdownPeer' (#459) from fixShutdown into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #459
2022-09-09 16:31:56 +00:00
Dan Ballard c8d7ec80ed app Shutdown uses shutdownPeer
continuous-integration/drone/pr Build is failing Details
2022-09-09 09:07:51 -07:00
Sarah Jamie Lewis 9554e428d2 Merge pull request 'merge app/applets; remove engine init from create/load flow; add ability to turn on/off engine' (#456) from networkAfterOnline into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #456
2022-09-08 16:41:33 +00:00
Dan Ballard ab14884bcf shutdown handle engine not being on
continuous-integration/drone/pr Build is passing Details
2022-09-08 09:19:25 -07:00
Dan Ballard bdb9ac5db4 Remove email step from drone
continuous-integration/drone/pr Build is failing Details
2022-09-08 08:53:51 -07:00
Dan Ballard c8f807ac7d amend Activate API to handle launching listen, peer and server connections
continuous-integration/drone/pr Build is failing Details
2022-09-08 08:52:49 -07:00
Dan Ballard cd37f29341 merge app/applets; remove engine init from create/load flow; add ability to turn on/off engine 2022-09-08 08:50:40 -07:00
Dan Ballard 7fe7ba72c7 Merge pull request 'Surface Token Management to UX' (#457) from surface-tokens into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #457
2022-09-07 16:39:53 +00:00
Sarah Jamie Lewis f46c717ff9 Don't update token count until after we have spent the token
continuous-integration/drone/pr Build is passing Details
2022-09-07 09:27:22 -07:00
Sarah Jamie Lewis 79bf060c2f Change ioutil -> os APIs 2022-09-07 09:27:22 -07:00
Sarah Jamie Lewis 5765cfd6c4 Surface Token Management to UX 2022-09-07 09:27:22 -07:00
Sarah Jamie Lewis 15836ad7de Merge pull request 'update .drone.yml to new format' (#458) from updateDrone into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #458
2022-09-07 03:26:34 +00:00
Dan Ballard 41f55451d4 update .drone.yml to new format
continuous-integration/drone/pr Build is passing Details
2022-09-06 19:27:51 -07:00
Dan Ballard 33bcc40206 Merge pull request 'Push locks back into storage to free up cwtch peer operations' (#454) from thread_works into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #454
2022-08-29 03:40:04 +00:00
Sarah Jamie Lewis e3efdde7b5 Merge branch 'master' into thread_works
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2022-08-26 20:57:35 +00:00
Sarah Jamie Lewis 3d49511c6c Push locks back into storage to free up cwthc peer operations
continuous-integration/drone/pr Build is pending Details
continuous-integration/drone/push Build was killed Details
2022-08-26 13:54:48 -07:00
Dan Ballard 82346e399f Merge pull request 'Add additional checks around file download directories' (#453) from file_sharing_fixes into master
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/tag Build is pending Details
Reviewed-on: #453
2022-08-22 21:07:09 +00:00
Sarah Jamie Lewis 720fb664de Add additional checks around file download directories
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2022-08-19 09:28:17 -07:00
Sarah Jamie Lewis b2efbb8843 Merge pull request 'connectivity version bump' (#452) from cbump into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #452
2022-08-19 16:28:10 +00:00
Dan Ballard bd64f708cf connectivity version bump
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2022-08-09 10:56:42 -07:00
Sarah Jamie Lewis 8b9b0906ec Merge pull request 'using new connectivity SetVersionCallback to register ACNVersion event emitting callback' (#451) from emitVer into master
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #451
2022-08-04 06:29:34 +00:00
Dan Ballard 7c753437f9 more clear errors
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is failing Details
2022-08-03 23:06:58 -07:00
Dan Ballard 2183c0b051 using new connectivity SetVersionCallback to register ACNVersion event emitting callback
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is failing Details
2022-08-03 22:20:54 -07:00
Sarah Jamie Lewis b95c2c12eb Merge pull request 'remove unused events (libcwtch-rs audit); add anti dup on import' (#449) from cleanAndNoDupImport into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #449
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2022-07-30 23:33:06 +00:00
Dan Ballard 60caa08868 readd deletecontact and wire in
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2022-07-30 16:05:39 -07:00
Dan Ballard 3dc5dbb38e fix errs
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build is pending Details
2022-07-30 00:19:16 -07:00
Dan Ballard b64229c8b7 delete engine.deleteConnection (unused)
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2022-07-29 17:37:40 -07:00
Dan Ballard 56cf2b7bf6 remove unused events (libcwtch-rs audit); add anti dup on import
continuous-integration/drone/pr Build is failing Details
continuous-integration/drone/push Build is passing Details
2022-07-29 17:24:22 -07:00
Dan Ballard 5b1ac38473 Merge pull request 'More File Sharing APIS (StopAllFileShares / GetFileShareInfo / GetSharedFiles)' (#448) from filesharing-persist into master
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/tag Build is pending Details
Reviewed-on: #448
2022-07-06 18:23:19 +00:00
Sarah Jamie Lewis 4d080a2854 More File Sharing APIS (StopAllFileShares / GetFileShareInfo / GetSharedFiles)
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2022-07-06 11:09:28 -07:00
Sarah Jamie Lewis 803d953778 Merge pull request 'Stop and Restart File Shares' (#447) from filesharing-persist into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #447
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2022-07-06 16:15:37 +00:00
Sarah Jamie Lewis 1a24e8d4b1 Rename to GetScopedZonedAttributeKeys
continuous-integration/drone/pr Build is pending Details
continuous-integration/drone/push Build is pending Details
2022-07-05 20:41:16 -07:00
Sarah Jamie Lewis fa3358cb89 Reduce nesting in ReShare Files
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build is passing Details
2022-07-05 20:29:07 -07:00
Sarah Jamie Lewis eb5a60bbb6 Use time.Since
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2022-07-05 15:38:30 -07:00
Sarah Jamie Lewis 02044e10f3 Stop and Restart File Shares
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build is failing Details
2022-07-05 15:31:44 -07:00
Sarah Jamie Lewis bc38f4ec0a Merge pull request 'Upgrade Tapir - Fix 2 small memory leaks around outbound connection handling' (#443) from tapir-gc into master
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/tag Build is pending Details
Reviewed-on: #443
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2022-04-21 23:01:28 +00:00
Sarah Jamie Lewis 45e9dfe869 Merge branch 'master' into tapir-gc
continuous-integration/drone/push Build was killed Details
continuous-integration/drone/pr Build is passing Details
2022-04-21 23:00:04 +00:00
Sarah Jamie Lewis 88ddecae56 Merge pull request 'fixing windows export/import of profiles' (#444) from winExport into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #444
2022-04-21 22:58:07 +00:00
Dan Ballard 191e287d75 fixing windows export/import of profiles
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build is pending Details
2022-04-21 15:56:33 -07:00
Sarah Jamie Lewis cade5f7793 Upgrade Tapir - Fix 2 small memory leaks around outbound connection handling
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is failing Details
2022-04-21 15:13:48 -07:00
Sarah Jamie Lewis 6fa627c1fa Merge pull request 'Upgrade Tapir/Connectivity, Fix management of server-goroutines' (#442) from perf into master
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/tag Build is pending Details
Reviewed-on: #442
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2022-04-21 00:17:14 +00:00
Sarah Jamie Lewis 1300c94d08 removing debug log
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build is passing Details
2022-04-20 17:02:57 -07:00
Sarah Jamie Lewis d02feecda0 Upgrade Tapir
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build is pending Details
2022-04-20 17:01:51 -07:00
Sarah Jamie Lewis b9d0a843fc Disable Circuit Info for now
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build is pending Details
2022-04-20 16:24:14 -07:00
Sarah Jamie Lewis 78fab87569 Upgrade Tapir/Connectivity, Fix management of server-goroutines 2022-04-20 16:24:14 -07:00
Sarah Jamie Lewis 7c25ddaf3d Merge pull request 'Fix goroutine leak in Network Check Plugin + remove simpleQueue' (#441) from plugins into master
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/tag Build is pending Details
Reviewed-on: #441
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2022-04-14 21:36:33 +00:00
Sarah Jamie Lewis 4334d3ff3f Fix event manager test
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2022-04-14 13:56:24 -07:00
Sarah Jamie Lewis 75703bf359 Fix goroutine leak in Network Check Plugin + remove simpleQueue
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is failing Details
2022-04-14 13:19:53 -07:00
Sarah Jamie Lewis 9896961b40 Merge pull request 'health check was firing way too often, fix some logic to keep it around 1min, not every 5 seconds' (#440) from healthTime into master
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/tag Build is pending Details
Reviewed-on: #440
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2022-04-09 16:51:03 +00:00
Dan Ballard d13dc5529b health check was firing way too often, fix some logic to keep it around 1min, not every 5 seconds
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2022-04-09 08:33:29 -07:00
Dan Ballard a27fd47755 Merge pull request 'attr/parseScope()' (#439) from parseScope into master
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/tag Build is pending Details
Reviewed-on: #439
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2022-04-05 00:03:19 +00:00
Dan Ballard 664a6dc198 attr/parseScope()
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2022-04-04 15:58:48 -07:00
Sarah Jamie Lewis 3fbf88d34b Merge pull request 'Close a connection if sending fails.' (#438) from send into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #438
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2022-03-29 19:16:47 +00:00
Sarah Jamie Lewis a39775d56b connection -> message
continuous-integration/drone/pr Build is pending Details
continuous-integration/drone/push Build is passing Details
2022-03-29 12:15:33 -07:00
Sarah Jamie Lewis 7fd53a3b16 Close a connection is sending fails.
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2022-03-29 11:36:21 -07:00
Sarah Jamie Lewis abfa95cddb Merge pull request 'Send[Message|File|Invite] returns message id' (#437) from sendRetId into master
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/tag Build is pending Details
Reviewed-on: #437
2022-03-23 22:38:43 +00:00
Dan Ballard 0126379436 locking in event manager to fix automated test detected race
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2022-03-23 14:57:18 -07:00
Dan Ballard dd8ed97f90 Send[Message|File|Invite] returns message id
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is failing Details
2022-03-23 14:08:30 -07:00
Sarah Jamie Lewis 8dfe391122 Merge pull request 'Simplify Network Check Plugin - Drop Self-Checks from Connectivity Plugin' (#436) from nc into master
continuous-integration/drone/push Build is failing Details
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #436
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2022-03-22 20:01:07 +00:00
Sarah Jamie Lewis cd5f461a33 Merge branch 'master' into nc
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2022-03-22 19:46:48 +00:00
Sarah Jamie Lewis dae2d358bc Formatting
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build is pending Details
2022-03-22 12:45:29 -07:00
Sarah Jamie Lewis 512a0834e0 Remove onion lookup map from NetworkCheck 2022-03-22 12:44:18 -07:00
Dan Ballard 9e506e5190 Merge pull request 'Properly remove bad profile dir' (#435) from import_export into master
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/tag Build was killed Details
Reviewed-on: #435
2022-03-09 23:58:36 +00:00
Sarah Jamie Lewis ff91300c39 Adding extra checks to import tarball profile name
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2022-03-09 15:52:24 -08:00
Sarah Jamie Lewis bf4cca631c Properly remove bad profile dir
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2022-03-09 14:32:45 -08:00
Dan Ballard b13a56d1db Merge pull request 'import_export' (#434) from import_export into master
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/tag Build is pending Details
Reviewed-on: #434
2022-03-08 23:13:12 +00:00
Sarah Jamie Lewis 5a87f835b4 First cut of profile import/export
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2022-03-08 13:45:26 -08:00
Sarah Jamie Lewis 8f138b47b0 Fixup Data-dir 2022-03-08 11:48:33 -08:00
Sarah Jamie Lewis a9ab91688b Merge pull request 'tweak reconnect plugin to have faster intervals; add group sync progress state to peer' (#433) from state into master
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/tag Build is pending Details
Reviewed-on: #433
2022-03-04 00:27:50 +00:00
Dan Ballard 93e2a25673 tweak reconnect plugin to have faster intervals; add group sync progress state to peer
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2022-03-03 16:06:44 -08:00
erinn d6a34258be Merge pull request 'Android: Change DownloadFile API to explictly use uint64' (#432) from profile_images into master
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #432
Reviewed-by: erinn <erinn@openprivacy.ca>
2022-02-04 21:23:59 +00:00
Sarah Jamie Lewis c24bb95af5 Android: Change DownloadFile API to explictly use uint64
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2022-02-04 13:20:07 -08:00
erinn d9eefd4be5 Merge pull request 'Enforce Optional File Size Limit in API' (#431) from profile_images into master
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/tag Build is pending Details
Reviewed-on: #431
Reviewed-by: erinn <erinn@openprivacy.ca>
2022-02-04 20:45:01 +00:00
Sarah Jamie Lewis 1345cf519b Enforce Optional File Size Limit in API
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2022-02-04 12:22:55 -08:00
Sarah Jamie Lewis 7d2d3979c1 Merge pull request 'Split ShareFile and SendMessage' (#430) from profile_images into master
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is passing Details
Reviewed-on: #430
Reviewed-by: erinn <erinn@openprivacy.ca>
2022-02-04 00:09:39 +00:00
Sarah Jamie Lewis 9621e294c2 Split ShareFile and SendMessage
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is failing Details
2022-02-03 16:08:05 -08:00
Sarah Jamie Lewis dec2b7182c Merge pull request 'Allow Sharing Public Profile Images' (#429) from profile_images into master
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/tag Build is pending Details
Reviewed-on: #429
Reviewed-by: erinn <erinn@openprivacy.ca>
2022-02-03 23:09:51 +00:00
Sarah Jamie Lewis f3ac8c0098 Allow Sharing Public Profile Images
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2022-02-03 14:44:29 -08:00
erinn 5dc0579075 Merge pull request 'Negotiate Lower Bandwidth / Higher Density Packets for Peers' (#428) from fastercwtch into master
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is failing Details
Reviewed-on: #428
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
Reviewed-by: erinn <erinn@openprivacy.ca>
2022-01-26 20:02:49 +00:00
Sarah Jamie Lewis ec6e025284 Version Fixups
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2022-01-25 15:55:38 -08:00
Sarah Jamie Lewis a088e588b1 Comment on Serialization Format
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build is passing Details
2022-01-25 15:44:37 -08:00
Sarah Jamie Lewis ea9cf5ca87 Make Version Strings Constant
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build is pending Details
2022-01-25 15:43:33 -08:00
Sarah Jamie Lewis ff4249e2bc Factor out serialization/parsing code into protocol.Model
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build is pending Details
2022-01-25 15:41:25 -08:00
Sarah Jamie Lewis 6bb510e39e Negotiate Lower Bandwidth / Higher Density Packets for Peers 2022-01-25 15:41:25 -08:00
erinn c6805149fa Merge pull request 'Upgrade Tapir - Expose Errors when Sending Messages' (#427) from tapir0.5 into master
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/tag Build is pending Details
Reviewed-on: #427
Reviewed-by: erinn <erinn@openprivacy.ca>
2022-01-24 23:42:25 +00:00
Sarah Jamie Lewis 45d53cb445 Upgrade Tapir - Expose Errors when Sending Messages
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2022-01-24 14:03:59 -08:00
erinn 0d60610777 Merge pull request 'upgrade connectivity' (#426) from sender_side_previews into master
continuous-integration/drone/push Build is failing Details
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #426
Reviewed-by: erinn <erinn@openprivacy.ca>
2022-01-20 21:57:30 +00:00
Sarah Jamie Lewis b8cb859044 upgrade connectivity
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2022-01-20 13:56:34 -08:00
erinn 5b10059396 Merge pull request 'Store filekey.path on the sender side to support sender side previews' (#425) from sender_side_previews into master
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is passing Details
Reviewed-on: #425
Reviewed-by: erinn <erinn@openprivacy.ca>
2022-01-20 21:52:22 +00:00
Sarah Jamie Lewis b7e0371401 Fixup data-dir locations for integ tests
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2022-01-20 13:38:46 -08:00
Sarah Jamie Lewis 92cda9fa00 Upgrade to new connectivity interface
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is failing Details
2022-01-20 13:27:35 -08:00
Sarah Jamie Lewis 12f5688af6 Store filekey.path on the sender side to support sender side previews
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is failing Details
2022-01-20 13:22:25 -08:00
Sarah Jamie Lewis cbc3bbc466 Merge pull request 'add contenthash to NewMessageFromPeer and NewMessageFromGroup' (#424) from hash into master
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #424
2022-01-20 17:35:32 +00:00
Dan Ballard d0b0752fe5 add contenthash to NewMessageFromPeer and NewMessageFromGroup
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2022-01-20 00:48:56 -05:00
erinn 36631a9111 Merge pull request 'Provide runtime information about ACN connections' (#423) from 1.7conn into master
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is passing Details
Reviewed-on: #423
Reviewed-by: erinn <erinn@openprivacy.ca>
2022-01-17 20:33:53 +00:00
Sarah Jamie Lewis 81029f1652 Provide runtime information about ACN connections
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2022-01-17 12:09:29 -08:00
erinn abe128ccf2 Merge pull request 'Upgrade Connectivity, Fixup Integ Test' (#422) from 1.6_conn into master
continuous-integration/drone/tag Build is pending Details
continuous-integration/drone/push Build is passing Details
Reviewed-on: #422
Reviewed-by: erinn <erinn@openprivacy.ca>
2022-01-12 20:54:54 +00:00
erinn 52dea28ab0 Merge branch 'master' into 1.6_conn
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2022-01-12 20:53:16 +00:00
Sarah Jamie Lewis 0c66743216 Upgrade Connectivity, Fixup Integ Test
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build is pending Details
Also removes spec dependency - we will move these to a standalone test repository
2022-01-12 12:49:13 -08:00
Sarah Jamie Lewis db05f7d51c Merge pull request 'fix logic arroudn accept/block contact and add unblock support' (#421) from fixAcceptBlock into master
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #421
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2022-01-06 21:18:39 +00:00
Sarah Jamie Lewis db8d02e842 Merge branch 'master' into fixAcceptBlock
continuous-integration/drone/pr Build is pending Details
continuous-integration/drone/push Build is passing Details
2022-01-06 21:18:08 +00:00
Dan Ballard e22bda5bc7 un/block now respect other permissions. removed uneeded serialize
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build is pending Details
2022-01-06 16:17:35 -05:00
Dan Ballard 830e479539 fix logic arroudn accept/block contact and add unblock support
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2022-01-06 12:55:26 -05:00
Dan Ballard 4d999f7914 Merge pull request 'add more safety checks around new engine ephemeral locking to avoid segfault on exit' (#420) from segExit into master
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #420
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2021-12-20 18:19:53 +00:00
Dan Ballard 42e04c17c3 init ephemeralService right away in service cache to avoid potential segfaults
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2021-12-20 13:00:23 -05:00
Sarah Jamie Lewis 069c754595 Merge pull request 'create new conversation on unknown accept' (#419) from newContact into master
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is passing Details
Reviewed-on: #419
2021-12-19 20:32:44 +00:00
Dan Ballard ee4437efe8 import legacy profile errs to debug
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2021-12-19 15:28:38 -05:00
Dan Ballard c3a830628a create new conversation on unknown accept
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2021-12-19 15:01:21 -05:00
Sarah Jamie Lewis e07b3e5259 Merge pull request 'Send Indexed Failure on Send Error' (#418) from indexederror into master
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is passing Details
Reviewed-on: #418
2021-12-19 02:09:23 +00:00
Sarah Jamie Lewis d0911eec57 Remove eventType parameter from attemptErrorConversationMessage
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2021-12-18 17:51:22 -08:00
Sarah Jamie Lewis 59d54d790d Send Indexed Failure on Send Error
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2021-12-18 17:38:48 -08:00
Sarah Jamie Lewis c035ab52bc Merge pull request 'changePassword' (#414) from changePassword into master
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is passing Details
Reviewed-on: #414
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2021-12-19 01:12:25 +00:00
Sarah Jamie Lewis 8a273d3310 Rekey comment
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2021-12-18 16:55:14 -08:00
Sarah Jamie Lewis aa98ef0e5e Add Constants
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is failing Details
2021-12-18 16:51:36 -08:00
Sarah Jamie Lewis 46f32881b9 Port Change Password to new Storage Engine
continuous-integration/drone/pr Build is pending Details
continuous-integration/drone/push Build is passing Details
2021-12-18 16:48:16 -08:00
erinn 204ff9af2a image previews wip 2021-12-18 16:48:15 -08:00
Sarah Jamie Lewis 27c2524cd8 Merge pull request 'image previews' (#413) from ipreview into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #413
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2021-12-19 00:46:31 +00:00
erinn f8ca29e552 Merge branch 'ipreview' of git.openprivacy.ca:cwtch.im/cwtch into ipreview
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2021-12-18 16:35:46 -08:00
erinn 158881ed9c quality 2021-12-18 16:34:07 -08:00
Sarah Jamie Lewis fa1729d08d Merge branch 'master' into ipreview
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is failing Details
2021-12-19 00:16:09 +00:00
erinn a392fa0cda image previews - dan comments
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build is pending Details
2021-12-18 16:15:05 -08:00
Sarah Jamie Lewis fc73ee46fc Merge pull request 'engineRefine' (#416) from engineRefine into master
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #416
2021-12-18 21:23:29 +00:00
Dan Ballard 13811def94 peerwithTokenService no longer uses Leave so as to preserve lock
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2021-12-17 22:56:37 -05:00
Dan Ballard ff012313be engine: add more granular locking around ephemeral token services 2021-12-17 22:56:37 -05:00
Dan Ballard 1d220381eb fix govet
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2021-12-17 22:55:25 -05:00
Dan Ballard 8250c04c52 refactor out appCore and add migration start and done notification events
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is failing Details
2021-12-17 15:07:02 -05:00
Sarah Jamie Lewis c22e2dd607 Merge pull request 'minor fixes for group functionality' (#412) from groupFix into master
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/tag Build is pending Details
Reviewed-on: #412
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2021-12-17 19:58:35 +00:00
Dan Ballard 113a6b617a add back passwrod errors for use; staticcheck fixes
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2021-12-17 14:46:24 -05:00
erinn 5ae269c531 message previews - safety checks
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build is pending Details
2021-12-16 16:40:28 -08:00
Dan Ballard ac05caf009 change locking on engine.ephermeralServices; logify integ test; delete unused events
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2021-12-16 19:11:10 -05:00
Dan Ballard 3efacc889d minor fixes for group functionality
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is failing Details
2021-12-16 13:11:16 -05:00
erinn 9d34b7ef57 Merge branch 'master' of git.openprivacy.ca:cwtch.im/cwtch into ipreview
continuous-integration/drone/push Build is passing Details
2021-12-14 13:21:53 -08:00
erinn d8bf2d3227 image previews wip 2021-12-14 13:21:45 -08:00
Sarah Jamie Lewis 00c8561677 Merge pull request 'add server zone' (#411) from serverZone into master
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is passing Details
Reviewed-on: #411
2021-12-11 00:18:59 +00:00
Dan Ballard 14ed0e7e0e add server zone
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2021-12-10 15:54:33 -08:00
Dan Ballard 9d23174cb6 Merge pull request 'NewMessageFromGroup Event now contains Message Body' (#410) from p2p-interim-new-storage into master
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is passing Details
Reviewed-on: #410
2021-12-08 02:44:01 +00:00
Sarah Jamie Lewis 410a7ef9c4 Merge branch 'master' into p2p-interim-new-storage
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2021-12-08 02:17:42 +00:00
Sarah Jamie Lewis 1bad9cafed NewMessageFromGroup Event now contains Message Body
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2021-12-07 18:11:57 -08:00
Dan Ballard 18280adf14 Merge pull request 'Small Fixups' (#409) from p2p-interim-new-storage into master
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #409
2021-12-08 01:30:35 +00:00
Sarah Jamie Lewis c5b61cdaf7 Formatting + Quality
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2021-12-07 17:03:20 -08:00
Sarah Jamie Lewis 93cbe0556c Add Error Checking for Non-specified Plugins (quality) 2021-12-07 17:03:20 -08:00
Sarah Jamie Lewis 9b09754f0e Remove Old Code 2021-12-07 17:03:20 -08:00
Sarah Jamie Lewis e252422463 Test Invalid Chunk Store 2021-12-07 17:03:20 -08:00
Sarah Jamie Lewis 369a0bc809 Fix Invite Construction for Groups 2021-12-07 17:03:20 -08:00
Dan Ballard b862e29d22 Merge pull request 'Adjust APIs for better UI Cache Performance. Introduce BDD Testing + Tests' (#408) from p2p-interim-new-storage into master
continuous-integration/drone/tag Build is pending Details
continuous-integration/drone/push Build is passing Details
Reviewed-on: #408
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2021-12-07 20:08:52 +00:00
Sarah Jamie Lewis aabc54739f Merge branch 'master' into p2p-interim-new-storage
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2021-12-06 20:21:47 +00:00
Sarah Jamie Lewis 359254f81e Adjust APIs for better UI Cache Performance. Introduce BDD Testing + Tests
continuous-integration/drone/push Build was killed Details
continuous-integration/drone/pr Build is passing Details
2021-12-06 12:20:38 -08:00
Dan Ballard 7eddd1c7e5 Merge pull request 'Updates to Event Handling given new Storage Engine' (#407) from p2p-interim-new-storage into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #407
2021-12-01 23:07:04 +00:00
Sarah Jamie Lewis 93c562097a Kill all Tor Connections at end of Integ Test
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2021-12-01 13:57:56 -08:00
Sarah Jamie Lewis a0ea927a08 Updates to Event Handling given new Storage Engine
continuous-integration/drone/pr Build is failing Details
continuous-integration/drone/push Build is passing Details
- AcceptConversation updates Peer Authorization and Peers with Contact
- Group and Server no longer emit New Contact Events
- SendMessageToPeer Events now contain an event Context to distinguish between get/ret vals and ui sent message errors
2021-12-01 04:13:58 -08:00
Dan Ballard 7f40ea2b51 Merge pull request 'Prevent Deadlock when Logging Protocol Engine Stopped' (#406) from p2p-interim-new-storage into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #406
2021-11-27 00:15:01 +00:00
Sarah Jamie Lewis 65ed00ca8a Merge branch 'master' into p2p-interim-new-storage
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2021-11-27 00:03:03 +00:00
Sarah Jamie Lewis 8763be1230 Prevent Deadlock when Logging Protocol Engine Stopped
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build is pending Details
2021-11-26 16:01:58 -08:00
Dan Ballard da788b9c1d Merge pull request 'Offset should apply to rownum not content hash' (#405) from p2p-interim-new-storage into master
continuous-integration/drone/push Build is failing Details
Reviewed-on: #405
2021-11-26 00:18:16 +00:00
Sarah Jamie Lewis e96d31302c Offset should apply to rownum not content hash
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2021-11-25 16:16:23 -08:00
Sarah Jamie Lewis bc8a13b707 Merge pull request 'New Storage Refactor' (#404) from p2p-interim-new-storage into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #404
Reviewed-by: erinn <erinn@openprivacy.ca>
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2021-11-25 23:56:51 +00:00
Sarah Jamie Lewis 5a3d393472 Purge on Startup + Fix SetSZA eventbus safety
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build is passing Details
2021-11-25 15:39:08 -08:00
Sarah Jamie Lewis af8322b734 ContentHash should return offset not count
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is failing Details
2021-11-25 15:30:36 -08:00
Sarah Jamie Lewis e9f986cc2e Update Server Attribute. Fix Profile Attribute Updates. Add UNIQUE constraint to type/key in profile attributes
continuous-integration/drone/pr Build is pending Details
continuous-integration/drone/push Build is passing Details
2021-11-25 14:34:47 -08:00
Sarah Jamie Lewis 1b9a9a0b72 quality
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2021-11-23 15:00:16 -08:00
Sarah Jamie Lewis e7191f5d57 comments
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build is failing Details
2021-11-23 14:54:06 -08:00
Sarah Jamie Lewis 781f4a919b Deduplicate Random ID
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is failing Details
2021-11-23 14:45:25 -08:00
Sarah Jamie Lewis 6e2e67d26f More efficient content hash fetch
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build is pending Details
2021-11-23 14:26:11 -08:00
Sarah Jamie Lewis b45aec6271 Constant Comments
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build is passing Details
2021-11-23 12:32:42 -08:00
Sarah Jamie Lewis 6ab11fc929 Purge message history for not-saved conversation on Close + other review comments
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is failing Details
2021-11-23 12:17:11 -08:00
Sarah Jamie Lewis 6101e4e031 Fix Filesharing Integ Test
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build is passing Details
2021-11-19 15:55:01 -08:00
Sarah Jamie Lewis 41dbd6da39 Fixing locking in ACN event bus interface
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build is failing Details
2021-11-19 15:10:22 -08:00
Sarah Jamie Lewis a4e62fe902 Lock app for CreateTaggedPeer
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is failing Details
2021-11-19 14:31:44 -08:00
Sarah Jamie Lewis 54e6122af7 Remove outdated tests in test script
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build is failing Details
2021-11-19 14:13:28 -08:00
Sarah Jamie Lewis 824cb3b951 Fixing small lints
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build is failing Details
2021-11-19 14:10:51 -08:00
Sarah Jamie Lewis 4f5b1fa106 Fixups for Integration Test
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build is failing Details
2021-11-19 14:04:43 -08:00
Sarah Jamie Lewis 847b04e4fc More comments + UpdateMessageAttribute public API
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build is pending Details
2021-11-19 12:27:52 -08:00
Sarah Jamie Lewis f1caca3adf Closing Down Database + Delete Peer
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build was killed Details
2021-11-19 11:49:04 -08:00
Sarah Jamie Lewis cb8960f893 Fixups from merging AddServer PR
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is failing Details
2021-11-19 00:09:19 -08:00
Sarah Jamie Lewis c1428762f8 Merge remote-tracking branch 'origin/master' into p2p-interim-new-storage
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build is pending Details
2021-11-19 00:06:44 -08:00
Sarah Jamie Lewis dfb4f7c14e Sent new message total for Group Messages
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build is pending Details
2021-11-18 23:55:16 -08:00
Sarah Jamie Lewis 72ac4099d5 Fixes from Cwtch UI Integration
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build is pending Details
2021-11-18 15:43:58 -08:00
Sarah Jamie Lewis 2caaa7eb87 More Deletions
continuous-integration/drone/push Build is pending Details
2021-11-17 16:01:25 -08:00
Sarah Jamie Lewis 0614d31366 Staticheck Pass
continuous-integration/drone/push Build is pending Details
2021-11-17 15:59:52 -08:00
Sarah Jamie Lewis cfff858fe1 First cut of Importing Legacy Profiles
continuous-integration/drone/push Build is pending Details
2021-11-17 15:34:14 -08:00
Sarah Jamie Lewis e296c30818 libcwtch-go first cut integration / message timelines etc
continuous-integration/drone/push Build is pending Details
2021-11-17 14:34:13 -08:00
Sarah Jamie Lewis 5c47dd789a Deleting Unused Profile Code
continuous-integration/drone/push Build is passing Details
2021-11-16 15:14:34 -08:00
Sarah Jamie Lewis 406d900029 First Cut of P2P and Groups using new Storage APIs!
continuous-integration/drone/push Build is pending Details
2021-11-16 15:06:30 -08:00
Sarah Jamie Lewis 62d2497843 Purging old Profile / Storage Code - Start of Group Integration
continuous-integration/drone/push Build is pending Details
2021-11-10 16:41:43 -08:00
Sarah Jamie Lewis 3d0ed3d4b0 File Sharing Integration Tests now Works with New Storage Code
continuous-integration/drone/push Build is pending Details
2021-11-10 14:28:52 -08:00
Sarah Jamie Lewis 8c80340a3d Interim Work - P2P now Works on New Storage Model
continuous-integration/drone/push Build is pending Details
2021-11-09 15:47:33 -08:00
Sarah Jamie Lewis 2c396826e7 Merge pull request 'AddServer return new server onion' (#403) from addServerRet into master
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is failing Details
Reviewed-on: #403
2021-11-05 23:26:50 +00:00
Dan Ballard 8b08cabed9 AddServer return new server onion
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build is passing Details
2021-11-05 15:25:41 -07:00
Sarah Jamie Lewis 4a9a254b48 Merge pull request 'support for file resumption' (#402) from filey into master
continuous-integration/drone/push Build is failing Details
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #402
2021-11-04 22:13:23 +00:00
erinn 01a2b7833e code type
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2021-11-04 14:41:00 -07:00
erinn 934bfe4f69 timeout downloads
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build is pending Details
2021-11-04 14:28:58 -07:00
erinn 83935ac55c Merge branch 'master' of git.openprivacy.ca:cwtch.im/cwtch into filey
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2021-11-04 14:07:54 -07:00
erinn bf1a92528a file resumption support 2021-11-04 14:07:43 -07:00
Dan Ballard 727916dcb6 Merge pull request 'Temporarily ignore timeline dedupelication for p2p messages.' (#400) from p2pdedupe into master
continuous-integration/drone/tag Build is pending Details
continuous-integration/drone/push Build is failing Details
Reviewed-on: #400
2021-11-03 19:14:31 +00:00
Sarah Jamie Lewis e2bba41a9a Remove IPC App Bridge 2021-11-03 11:40:25 -07:00
Sarah Jamie Lewis 81bd787a96 Sort Integration Timelines prior to checking 2021-11-03 11:40:25 -07:00
Sarah Jamie Lewis dc454ad849 Temporarily ignore timeline dedupelication for p2p messags. 2021-11-03 11:40:25 -07:00
erinn 5162561b33 merge 2021-11-02 15:07:24 -07:00
erinn 22ef61a8d5 wip: file retries 2021-11-02 14:30:03 -07:00
Sarah Jamie Lewis b9eb1311d1 Sort Integration Timelines prior to checking
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2021-11-02 13:42:18 -07:00
Sarah Jamie Lewis 8cb61e9c9b Merge branch 'master' into p2pdedupe
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is failing Details
2021-11-02 19:21:37 +00:00
Sarah Jamie Lewis bff922df35 Temporarily ignore timeline dedupelication for p2p messags.
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build is pending Details
2021-11-02 12:17:56 -07:00
106 changed files with 9680 additions and 5454 deletions

View File

@ -1,72 +1,89 @@
workspace:
base: /go
path: src/cwtch.im/cwtch
---
kind: pipeline
type: docker
name: linux-test
pipeline:
fetch:
image: golang
when:
repo: cwtch.im/cwtch
branch: master
event: [ push, pull_request ]
steps:
- name: fetch
image: golang:1.21.5
volumes:
- name: deps
path: /go
commands:
- wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/master/tor/tor
- wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/master/tor/torrc
- chmod a+x tor
- go get -u golang.org/x/lint/golint
- go install honnef.co/go/tools/cmd/staticcheck@latest
- go install go.uber.org/nilaway/cmd/nilaway@latest
- wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-0.4.8.9-linux-x86_64.tar.gz -O tor.tar.gz
- tar -xzf tor.tar.gz
- chmod a+x Tor/tor
- export PATH=$PWD/Tor/:$PATH
- export LD_LIBRARY_PATH=$PWD/Tor/
- tor --version
- export GO111MODULE=on
- go mod vendor
quality:
image: golang
when:
repo: cwtch.im/cwtch
branch: master
event: [ push, pull_request ]
- name: quality
image: golang:1.21.5
volumes:
- name: deps
path: /go
commands:
- go list ./... | xargs go vet
- go list ./... | xargs golint -set_exit_status
units-tests:
image: golang
when:
repo: cwtch.im/cwtch
branch: master
event: [ push, pull_request ]
- ./testing/quality.sh
- name: units-tests
image: golang:1.21.5
volumes:
- name: deps
path: /go
commands:
- export PATH=$PATH:/go/src/cwtch.im/cwtch
- export PATH=`pwd`:$PATH
- sh testing/tests.sh
integ-test:
image: golang
when:
repo: cwtch.im/cwtch
branch: master
event: [ push, pull_request ]
- name: integ-test
image: golang:1.21.5
volumes:
- name: deps
path: /go
commands:
- go test -race -v cwtch.im/cwtch/testing/
filesharing-integ-test:
image: golang
when:
repo: cwtch.im/cwtch
branch: master
event: [ push, pull_request ]
- export PATH=$PWD/Tor/:$PATH
- export LD_LIBRARY_PATH=$PWD/Tor/
- tor --version
- go test -timeout=30m -race -v cwtch.im/cwtch/testing/
- name: filesharing-integ-test
image: golang:1.21.5
volumes:
- name: deps
path: /go
commands:
- go test -race -v cwtch.im/cwtch/testing/filesharing
notify-email:
image: drillster/drone-email
host: build.openprivacy.ca
port: 25
skip_verify: true
from: drone@openprivacy.ca
when:
repo: cwtch.im/cwtch
branch: master
event: [ push, pull_request ]
status: [ failure ]
notify-gogs:
- export PATH=$PWD/Tor/:$PATH
- export LD_LIBRARY_PATH=$PWD/Tor/
- go test -timeout=20m -race -v cwtch.im/cwtch/testing/filesharing
- name: filesharing-autodownload-integ-test
image: golang:1.21.5
volumes:
- name: deps
path: /go
commands:
- export PATH=$PWD/Tor/:$PATH
- export LD_LIBRARY_PATH=$PWD/Tor/
- go test -timeout=20m -race -v cwtch.im/cwtch/testing/autodownload
- name: notify-gogs
image: openpriv/drone-gogs
pull: if-not-exists
when:
repo: cwtch.im/cwtch
branch: master
event: pull_request
status: [ success, changed, failure ]
secrets: [gogs_account_token]
gogs_url: https://git.openprivacy.ca
environment:
GOGS_ACCOUNT_TOKEN:
from_secret: gogs_account_token
settings:
gogs_url: https://git.openprivacy.ca
volumes:
# gopath where bin and pkg lives to persist across steps
- name: deps
temp: {}
trigger:
repo: cwtch.im/cwtch
branch: master
event:
- push
- pull_request
- tag

13
.gitignore vendored
View File

@ -24,4 +24,15 @@ testing/cwtch.out.png.manifest
testing/tordir/
tokens-bak.db
tokens.db
tokens1.db
tokens1.db
arch/
testing/encryptedstorage/encrypted_storage_profiles
testing/encryptedstorage/tordir
*.tar.gz
data-dir-cwtchtool/
tokens
tordir/
testing/autodownload/download_dir
testing/autodownload/storage
*.swp
testing/managerstorage/*

View File

@ -1,58 +1,78 @@
package app
import (
"os"
path "path/filepath"
"strconv"
"sync"
"cwtch.im/cwtch/app/plugins"
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/extensions"
"cwtch.im/cwtch/functionality/filesharing"
"cwtch.im/cwtch/functionality/hybrid"
"cwtch.im/cwtch/functionality/servers"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/model/constants"
"cwtch.im/cwtch/peer"
"cwtch.im/cwtch/protocol/connections"
"cwtch.im/cwtch/settings"
"cwtch.im/cwtch/storage"
"fmt"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
"git.openprivacy.ca/openprivacy/connectivity"
"git.openprivacy.ca/openprivacy/log"
"io/ioutil"
"os"
path "path/filepath"
"strconv"
"sync"
)
type applicationCore struct {
type application struct {
eventBuses map[string]event.Manager
directory string
directory string
coremutex sync.Mutex
peers map[string]peer.CwtchPeer
acn connectivity.ACN
plugins sync.Map //map[string] []plugins.Plugin
engines map[string]connections.Engine
appBus event.Manager
eventQueue event.Queue
appmutex sync.Mutex
engineHooks connections.EngineHooks
settings *settings.GlobalSettingsFile
}
type application struct {
applicationCore
appletPeers
appletACN
appletPlugins
storage map[string]storage.ProfileStore
engines map[string]connections.Engine
appBus event.Manager
appmutex sync.Mutex
func (app *application) IsFeatureEnabled(experiment string) bool {
globalSettings := app.ReadSettings()
if globalSettings.ExperimentsEnabled {
if status, exists := globalSettings.Experiments[experiment]; exists {
return status
}
}
return false
}
// Application is a full cwtch peer application. It allows management, usage and storage of multiple peers
type Application interface {
LoadProfiles(password string)
CreatePeer(name string, password string)
CreateTaggedPeer(name string, password string, tag string)
DeletePeer(onion string, currentPassword string)
CreateProfile(name string, password string, autostart bool) string
InstallEngineHooks(engineHooks connections.EngineHooks)
ImportProfile(exportedCwtchFile string, password string) (peer.CwtchPeer, error)
EnhancedImportProfile(exportedCwtchFile string, password string) string
DeleteProfile(onion string, currentPassword string)
AddPeerPlugin(onion string, pluginID plugins.PluginID)
ChangePeerPassword(onion, oldpass, newpass string)
LaunchPeers()
GetPrimaryBus() event.Manager
GetEventBus(onion string) event.Manager
QueryACNStatus()
QueryACNVersion()
ConfigureConnections(onion string, doListn, doPeers, doServers bool)
ActivatePeerEngine(onion string)
DeactivatePeerEngine(onion string)
ReadSettings() settings.GlobalSettings
UpdateSettings(settings settings.GlobalSettings)
IsFeatureEnabled(experiment string) bool
ShutdownPeer(string)
Shutdown()
@ -61,108 +81,212 @@ type Application interface {
}
// LoadProfileFn is the function signature for a function in an app that loads a profile
type LoadProfileFn func(profile *model.Profile, store storage.ProfileStore)
type LoadProfileFn func(profile peer.CwtchPeer)
func newAppCore(appDirectory string) *applicationCore {
appCore := &applicationCore{eventBuses: make(map[string]event.Manager), directory: appDirectory}
os.MkdirAll(path.Join(appCore.directory, "profiles"), 0700)
return appCore
func LoadAppSettings(appDirectory string) *settings.GlobalSettingsFile {
log.Debugf("NewApp(%v)\n", appDirectory)
os.MkdirAll(path.Join(appDirectory, "profiles"), 0700)
// Note: we basically presume this doesn't fail. If the file doesn't exist we create it, and as such the
// only plausible error conditions are related to file create e.g. low disk space. If that is the case then
// many other parts of Cwtch are likely to fail also.
globalSettingsFile, err := settings.InitGlobalSettingsFile(appDirectory, DefactoPasswordForUnencryptedProfiles)
if err != nil {
log.Errorf("error initializing global globalSettingsFile file %s. Global globalSettingsFile might not be loaded or saved", err)
}
return globalSettingsFile
}
// NewApp creates a new app with some environment awareness and initializes a Tor Manager
func NewApp(acn connectivity.ACN, appDirectory string) Application {
log.Debugf("NewApp(%v)\n", appDirectory)
app := &application{storage: make(map[string]storage.ProfileStore), engines: make(map[string]connections.Engine), applicationCore: *newAppCore(appDirectory), appBus: event.NewEventManager()}
app.appletPeers.init()
func NewApp(acn connectivity.ACN, appDirectory string, settings *settings.GlobalSettingsFile) Application {
app := &application{engines: make(map[string]connections.Engine), eventBuses: make(map[string]event.Manager), directory: appDirectory, appBus: event.NewEventManager(), settings: settings, eventQueue: event.NewQueue()}
app.peers = make(map[string]peer.CwtchPeer)
app.engineHooks = connections.DefaultEngineHooks{}
app.acn = acn
statusHandler := app.getACNStatusHandler()
acn.SetStatusCallback(statusHandler)
acn.SetVersionCallback(app.getACNVersionHandler())
prog, status := acn.GetBootstrapStatus()
statusHandler(prog, status)
app.GetPrimaryBus().Subscribe(event.ACNStatus, app.eventQueue)
go app.eventHandler()
app.appletACN.init(acn, app.getACNStatusHandler())
return app
}
// CreatePeer creates a new Peer with a given name and core required accessories (eventbus)
func (ac *applicationCore) CreatePeer(name string) (*model.Profile, error) {
log.Debugf("CreatePeer(%v)\n", name)
func (app *application) InstallEngineHooks(engineHooks connections.EngineHooks) {
app.appmutex.Lock()
defer app.appmutex.Unlock()
app.engineHooks = engineHooks
}
profile := storage.NewProfile(name)
func (app *application) ReadSettings() settings.GlobalSettings {
app.appmutex.Lock()
defer app.appmutex.Unlock()
return app.settings.ReadGlobalSettings()
}
ac.coremutex.Lock()
defer ac.coremutex.Unlock()
func (app *application) UpdateSettings(settings settings.GlobalSettings) {
// don't allow any other application changes while settings update
app.appmutex.Lock()
defer app.appmutex.Unlock()
app.settings.WriteGlobalSettings(settings)
_, exists := ac.eventBuses[profile.Onion]
if exists {
return nil, fmt.Errorf("error: profile for onion %v already exists", profile.Onion)
for _, profile := range app.peers {
profile.UpdateExperiments(settings.ExperimentsEnabled, settings.Experiments)
// Explicitly toggle blocking/unblocking of unknown connections for profiles
// that have been loaded.
if settings.BlockUnknownConnections {
profile.BlockUnknownConnections()
} else {
profile.AllowUnknownConnections()
}
profile.NotifySettingsUpdate(settings)
}
}
// ListProfiles returns a map of onions to their profile's Name
func (app *application) ListProfiles() []string {
var keys []string
app.appmutex.Lock()
defer app.appmutex.Unlock()
for handle := range app.peers {
keys = append(keys, handle)
}
return keys
}
// GetPeer returns a cwtchPeer for a given onion address
func (app *application) GetPeer(onion string) peer.CwtchPeer {
app.appmutex.Lock()
defer app.appmutex.Unlock()
if profile, ok := app.peers[onion]; ok {
return profile
}
return nil
}
func (app *application) AddPlugin(peerid string, id plugins.PluginID, bus event.Manager, acn connectivity.ACN) {
if _, exists := app.plugins.Load(peerid); !exists {
app.plugins.Store(peerid, []plugins.Plugin{})
}
pluginsinf, _ := app.plugins.Load(peerid)
peerPlugins := pluginsinf.([]plugins.Plugin)
for _, plugin := range peerPlugins {
if plugin.Id() == id {
log.Errorf("trying to add second instance of plugin %v to peer %v", id, peerid)
return
}
}
newp, err := plugins.Get(id, bus, acn, peerid)
if err == nil {
newp.Start()
peerPlugins = append(peerPlugins, newp)
log.Debugf("storing plugin for %v %v", peerid, peerPlugins)
app.plugins.Store(peerid, peerPlugins)
} else {
log.Errorf("error adding plugin: %v", err)
}
}
func (app *application) CreateProfile(name string, password string, autostart bool) string {
autostartVal := constants.True
if !autostart {
autostartVal = constants.False
}
tagVal := constants.ProfileTypeV1Password
if password == DefactoPasswordForUnencryptedProfiles {
tagVal = constants.ProfileTypeV1DefaultPassword
}
profile_id, err := app.CreatePeer(name, password, map[attr.ZonedPath]string{
attr.ProfileZone.ConstructZonedPath(constants.Tag): tagVal,
attr.ProfileZone.ConstructZonedPath(constants.PeerAutostart): autostartVal,
})
if err == nil {
return profile_id
}
return ""
}
func (app *application) setupPeer(profile peer.CwtchPeer) {
eventBus := event.NewEventManager()
ac.eventBuses[profile.Onion] = eventBus
app.eventBuses[profile.GetOnion()] = eventBus
// Initialize the Peer with the Given Event Bus
app.peers[profile.GetOnion()] = profile
profile.Init(eventBus)
// Update the Peer with the Most Recent Experiment State...
globalSettings := app.settings.ReadGlobalSettings()
profile.UpdateExperiments(globalSettings.ExperimentsEnabled, globalSettings.Experiments)
app.registerHooks(profile)
// Register the Peer With Application Plugins..
app.AddPeerPlugin(profile.GetOnion(), plugins.CONNECTIONRETRY) // Now Mandatory
app.AddPeerPlugin(profile.GetOnion(), plugins.HEARTBEAT) // Now Mandatory
return profile, nil
}
func (ac *applicationCore) DeletePeer(onion string) {
ac.coremutex.Lock()
defer ac.coremutex.Unlock()
ac.eventBuses[onion].Shutdown()
delete(ac.eventBuses, onion)
}
func (app *application) CreateTaggedPeer(name string, password string, tag string) {
profile, err := app.applicationCore.CreatePeer(name)
if err != nil {
app.appBus.Publish(event.NewEventList(event.PeerError, event.Error, err.Error()))
return
}
profileStore := storage.CreateProfileWriterStore(app.eventBuses[profile.Onion], path.Join(app.directory, "profiles", profile.LocalID), password, profile)
app.storage[profile.Onion] = profileStore
pc := app.storage[profile.Onion].GetProfileCopy(true)
p := peer.FromProfile(pc)
p.Init(app.eventBuses[profile.Onion])
peerAuthorizations := profile.ContactsAuthorizations()
// TODO: Would be nice if ProtocolEngine did not need to explicitly be given the Private Key.
identity := primitives.InitializeIdentity(profile.Name, &profile.Ed25519PrivateKey, &profile.Ed25519PublicKey)
engine := connections.NewProtocolEngine(identity, profile.Ed25519PrivateKey, app.acn, app.eventBuses[profile.Onion], peerAuthorizations)
app.peers[profile.Onion] = p
app.engines[profile.Onion] = engine
if tag != "" {
p.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Tag, tag)
}
app.appBus.Publish(event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.Onion, event.Created: event.True}))
}
// CreatePeer creates a new Peer with the given name and required accessories (eventbus, storage, protocol engine)
func (app *application) CreatePeer(name string, password string) {
app.CreateTaggedPeer(name, password, "")
}
func (app *application) DeletePeer(onion string, password string) {
log.Infof("DeletePeer called on %v\n", onion)
func (app *application) CreatePeer(name string, password string, attributes map[attr.ZonedPath]string) (string, error) {
app.appmutex.Lock()
defer app.appmutex.Unlock()
if app.storage[onion].CheckPassword(password) {
app.appletPlugins.ShutdownPeer(onion)
app.plugins.Delete(onion)
profileDirectory := path.Join(app.directory, "profiles", model.GenerateRandomID())
app.peers[onion].Shutdown()
delete(app.peers, onion)
profile, err := peer.CreateEncryptedStorePeer(profileDirectory, name, password)
if err != nil {
log.Errorf("Error Creating Peer: %v", err)
app.appBus.Publish(event.NewEventList(event.PeerError, event.Error, err.Error()))
return "", err
}
app.engines[onion].Shutdown()
delete(app.engines, onion)
app.setupPeer(profile)
app.storage[onion].Shutdown()
app.storage[onion].Delete()
delete(app.storage, onion)
for zp, val := range attributes {
zone, key := attr.ParseZone(zp.ToString())
profile.SetScopedZonedAttribute(attr.LocalScope, zone, key, val)
}
app.eventBuses[onion].Publish(event.NewEventList(event.ShutdownPeer, event.Identity, onion))
app.appBus.Publish(event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.GetOnion(), event.Created: event.True}))
return profile.GetOnion(), nil
}
app.applicationCore.DeletePeer(onion)
func (app *application) DeleteProfile(onion string, password string) {
log.Debugf("DeleteProfile called on %v\n", onion)
app.appmutex.Lock()
defer app.appmutex.Unlock()
// short circuit to prevent nil-pointer panic if this function is called twice (or incorrectly)
peer := app.peers[onion]
if peer == nil {
log.Errorf("shutdownPeer called with invalid onion %v", onion)
return
}
// allow a blank password to delete "unencrypted" accounts...
if password == "" {
password = DefactoPasswordForUnencryptedProfiles
}
if peer.CheckPassword(password) {
// soft-shutdown
peer.Shutdown()
// delete the underlying storage
peer.Delete()
// hard shutdown / remove from app
app.shutdownPeer(onion)
// Shutdown and Remove the Engine
log.Debugf("Delete peer for %v Done\n", onion)
app.appBus.Publish(event.NewEventList(event.PeerDeleted, event.Identity, onion))
return
@ -170,69 +294,175 @@ func (app *application) DeletePeer(onion string, password string) {
app.appBus.Publish(event.NewEventList(event.AppError, event.Error, event.PasswordMatchError, event.Identity, onion))
}
func (app *application) ChangePeerPassword(onion, oldpass, newpass string) {
app.eventBuses[onion].Publish(event.NewEventList(event.ChangePassword, event.Password, oldpass, event.NewPassword, newpass))
}
func (app *application) AddPeerPlugin(onion string, pluginID plugins.PluginID) {
app.AddPlugin(onion, pluginID, app.eventBuses[onion], app.acn)
}
// LoadProfiles takes a password and attempts to load any profiles it can from storage with it and create Peers for them
func (ac *applicationCore) LoadProfiles(password string, timeline bool, loadProfileFn LoadProfileFn) error {
files, err := ioutil.ReadDir(path.Join(ac.directory, "profiles"))
if err != nil {
return fmt.Errorf("error: cannot read profiles directory: %v", err)
func (app *application) ImportProfile(exportedCwtchFile string, password string) (peer.CwtchPeer, error) {
profileDirectory := path.Join(app.directory, "profiles")
profile, err := peer.ImportProfile(exportedCwtchFile, profileDirectory, password)
if profile != nil || err == nil {
app.installProfile(profile)
}
return profile, err
}
for _, file := range files {
eventBus := event.NewEventManager()
profileStore, err := storage.LoadProfileWriterStore(eventBus, path.Join(ac.directory, "profiles", file.Name()), password)
if err != nil {
continue
}
profile := profileStore.GetProfileCopy(timeline)
_, exists := ac.eventBuses[profile.Onion]
if exists {
profileStore.Shutdown()
eventBus.Shutdown()
log.Errorf("profile for onion %v already exists", profile.Onion)
continue
}
ac.coremutex.Lock()
ac.eventBuses[profile.Onion] = eventBus
ac.coremutex.Unlock()
loadProfileFn(profile, profileStore)
func (app *application) EnhancedImportProfile(exportedCwtchFile string, password string) string {
_, err := app.ImportProfile(exportedCwtchFile, password)
if err == nil {
return ""
}
return nil
return err.Error()
}
// LoadProfiles takes a password and attempts to load any profiles it can from storage with it and create Peers for them
func (app *application) LoadProfiles(password string) {
count := 0
app.applicationCore.LoadProfiles(password, true, func(profile *model.Profile, profileStore storage.ProfileStore) {
peer := peer.FromProfile(profile)
peer.Init(app.eventBuses[profile.Onion])
migrating := false
peerAuthorizations := profile.ContactsAuthorizations()
identity := primitives.InitializeIdentity(profile.Name, &profile.Ed25519PrivateKey, &profile.Ed25519PublicKey)
engine := connections.NewProtocolEngine(identity, profile.Ed25519PrivateKey, app.acn, app.eventBuses[profile.Onion], peerAuthorizations)
app.appmutex.Lock()
app.peers[profile.Onion] = peer
app.storage[profile.Onion] = profileStore
app.engines[profile.Onion] = engine
app.appmutex.Unlock()
app.appBus.Publish(event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.Onion, event.Created: event.False}))
count++
})
files, err := os.ReadDir(path.Join(app.directory, "profiles"))
if err != nil {
log.Errorf("error: cannot read profiles directory: %v", err)
return
}
for _, file := range files {
// Attempt to load an encrypted database
profileDirectory := path.Join(app.directory, "profiles", file.Name())
profile, err := peer.FromEncryptedDatabase(profileDirectory, password)
loaded := false
if err == nil {
// return the load the profile...
log.Infof("loading profile from new-type storage database...")
loaded = app.installProfile(profile)
} else { // On failure attempt to load a legacy profile
profileStore, err := storage.LoadProfileWriterStore(profileDirectory, password)
if err != nil {
continue
}
log.Infof("found legacy profile. importing to new database structure...")
legacyProfile := profileStore.GetProfileCopy(true)
if !migrating {
migrating = true
app.appBus.Publish(event.NewEventList(event.StartingStorageMiragtion))
}
cps, err := peer.CreateEncryptedStore(profileDirectory, password)
if err != nil {
log.Errorf("error creating encrypted store: %v", err)
continue
}
profile := peer.ImportLegacyProfile(legacyProfile, cps)
loaded = app.installProfile(profile)
}
if loaded {
count++
}
}
if count == 0 {
message := event.NewEventList(event.AppError, event.Error, event.AppErrLoaded0)
app.appBus.Publish(message)
}
if migrating {
app.appBus.Publish(event.NewEventList(event.DoneStorageMigration))
}
}
func (app *application) registerHooks(profile peer.CwtchPeer) {
// Register Hooks
profile.RegisterHook(extensions.ProfileValueExtension{})
profile.RegisterHook(extensions.SendWhenOnlineExtension{})
profile.RegisterHook(new(filesharing.Functionality))
profile.RegisterHook(new(filesharing.ImagePreviewsFunctionality))
profile.RegisterHook(new(servers.Functionality))
profile.RegisterHook(new(hybrid.ManagedGroupFunctionality))
profile.RegisterHook(new(hybrid.GroupManagerFunctionality)) // will only be activated if GroupManagerExperiment is enabled...
// Ensure that Profiles have the Most Up to Date Settings...
profile.NotifySettingsUpdate(app.settings.ReadGlobalSettings())
}
// installProfile takes a profile and if it isn't loaded in the app, installs it and returns true
func (app *application) installProfile(profile peer.CwtchPeer) bool {
app.appmutex.Lock()
defer app.appmutex.Unlock()
// Only attempt to finalize the profile if we don't have one loaded...
if app.peers[profile.GetOnion()] == nil {
app.setupPeer(profile)
// Finalize the Creation of Peer / Notify any Interfaces..
app.appBus.Publish(event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.GetOnion(), event.Created: event.False}))
return true
}
// Otherwise shutdown the connections
profile.Shutdown()
return false
}
// ActivatePeerEngine creates a peer engine for use with an ACN, should be called once the underlying ACN is online
func (app *application) ActivatePeerEngine(onion string) {
profile := app.GetPeer(onion)
if profile != nil {
app.appmutex.Lock()
if _, exists := app.engines[onion]; !exists {
eventBus, exists := app.eventBuses[profile.GetOnion()]
if !exists {
// todo handle this case?
log.Errorf("cannot activate peer engine without an event bus")
app.appmutex.Unlock()
return
}
engine, err := profile.GenerateProtocolEngine(app.acn, eventBus, app.engineHooks)
if err == nil {
log.Debugf("restartFlow: Creating a New Protocol Engine...")
app.engines[profile.GetOnion()] = engine
eventBus.Publish(event.NewEventList(event.ProtocolEngineCreated))
} else {
log.Errorf("corrupted profile detected for %v", onion)
}
}
app.appmutex.Unlock()
}
app.QueryACNStatus()
}
// ConfigureConnections autostarts the given kinds of connections.
func (app *application) ConfigureConnections(onion string, listen bool, peers bool, servers bool) {
profile := app.GetPeer(onion)
if profile != nil {
app.appmutex.Lock()
profileBus, exists := app.eventBuses[profile.GetOnion()]
app.appmutex.Unlock()
if exists {
// if we are making a decision to ignore
if !peers || !servers {
profileBus.Publish(event.NewEventList(event.PurgeRetries))
}
// enable the engine if it doesn't exist...
// note: this function is idempotent
app.ActivatePeerEngine(onion)
if listen {
profile.Listen()
}
profileBus.Publish(event.NewEventList(event.ResumeRetries))
// do this in the background, for large contact lists it can take a long time...
go profile.StartConnections(peers, servers)
}
} else {
log.Errorf("profile does not exist %v", onion)
}
}
// DeactivatePeerEngine shutsdown and cleans up a peer engine, should be called when an underlying ACN goes offline
func (app *application) DeactivatePeerEngine(onion string) {
if engine, exists := app.engines[onion]; exists {
engine.Shutdown()
delete(app.engines, onion)
}
}
// GetPrimaryBus returns the bus the Application uses for events that aren't peer specific
@ -241,8 +471,8 @@ func (app *application) GetPrimaryBus() event.Manager {
}
// GetEventBus returns a cwtchPeer's event bus
func (ac *applicationCore) GetEventBus(onion string) event.Manager {
if manager, ok := ac.eventBuses[onion]; ok {
func (app *application) GetEventBus(onion string) event.Manager {
if manager, ok := app.eventBuses[onion]; ok {
return manager
}
return nil
@ -251,12 +481,20 @@ func (ac *applicationCore) GetEventBus(onion string) event.Manager {
func (app *application) getACNStatusHandler() func(int, string) {
return func(progress int, status string) {
progStr := strconv.Itoa(progress)
app.peerLock.Lock()
app.appmutex.Lock()
app.appBus.Publish(event.NewEventList(event.ACNStatus, event.Progress, progStr, event.Status, status))
for _, bus := range app.eventBuses {
bus.Publish(event.NewEventList(event.ACNStatus, event.Progress, progStr, event.Status, status))
}
app.peerLock.Unlock()
app.appmutex.Unlock()
}
}
func (app *application) getACNVersionHandler() func(string) {
return func(version string) {
app.appmutex.Lock()
defer app.appmutex.Unlock()
app.appBus.Publish(event.NewEventList(event.ACNVersion, event.Data, version))
}
}
@ -270,35 +508,107 @@ func (app *application) QueryACNVersion() {
app.appBus.Publish(event.NewEventList(event.ACNVersion, event.Data, version))
}
func (app *application) eventHandler() {
acnStatus := -1
for {
e := app.eventQueue.Next()
switch e.EventType {
case event.ACNStatus:
newAcnStatus, err := strconv.Atoi(e.Data[event.Progress])
if err != nil {
break
}
if newAcnStatus == 100 {
if acnStatus != 100 {
for _, onion := range app.ListProfiles() {
profile := app.GetPeer(onion)
if profile != nil {
autostart, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.PeerAutostart)
appearOffline, appearOfflineExists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.PeerAppearOffline)
if !exists || autostart == "true" {
if appearOfflineExists && appearOffline == "true" {
// don't configure any connections...
log.Infof("peer appearing offline, not launching listen threads or connecting jobs")
app.ConfigureConnections(onion, false, false, false)
} else {
app.ConfigureConnections(onion, true, true, true)
}
}
}
}
}
} else {
if acnStatus == 100 {
// just fell offline
for _, onion := range app.ListProfiles() {
app.DeactivatePeerEngine(onion)
}
}
}
acnStatus = newAcnStatus
default:
// invalid event, signifies shutdown
if e.EventType == "" {
return
}
}
}
}
// ShutdownPeer shuts down a peer and removes it from the app's management
func (app *application) ShutdownPeer(onion string) {
app.appmutex.Lock()
defer app.appmutex.Unlock()
app.eventBuses[onion].Shutdown()
delete(app.eventBuses, onion)
app.peers[onion].Shutdown()
delete(app.peers, onion)
app.engines[onion].Shutdown()
delete(app.engines, onion)
app.storage[onion].Shutdown()
delete(app.storage, onion)
app.appletPlugins.Shutdown()
app.shutdownPeer(onion)
}
// Shutdown shutsdown all peers of an app and then the tormanager
// shutdownPeer mutex unlocked helper shutdown peer
//
//nolint:nilaway
func (app *application) shutdownPeer(onion string) {
// short circuit to prevent nil-pointer panic if this function is called twice (or incorrectly)
onionEventBus := app.eventBuses[onion]
onionPeer := app.peers[onion]
if onionEventBus == nil || onionPeer == nil {
log.Errorf("shutdownPeer called with invalid onion %v", onion)
return
}
// we are an internal locked method, app.eventBuses[onion] cannot fail...
onionEventBus.Publish(event.NewEventList(event.ShutdownPeer, event.Identity, onion))
onionEventBus.Shutdown()
delete(app.eventBuses, onion)
onionPeer.Shutdown()
delete(app.peers, onion)
if onionEngine, ok := app.engines[onion]; ok {
onionEngine.Shutdown()
delete(app.engines, onion)
}
log.Debugf("shutting down plugins for %v", onion)
pluginsI, ok := app.plugins.Load(onion)
if ok {
appPlugins := pluginsI.([]plugins.Plugin)
for _, plugin := range appPlugins {
plugin.Shutdown()
}
}
app.plugins.Delete(onion)
}
// Shutdown shutsdown all peers of an app
func (app *application) Shutdown() {
for id, peer := range app.peers {
peer.Shutdown()
app.appmutex.Lock()
defer app.appmutex.Unlock()
for id := range app.peers {
log.Debugf("Shutting Down Peer %v", id)
app.appletPlugins.ShutdownPeer(id)
log.Debugf("Shutting Down Engines for %v", id)
app.engines[id].Shutdown()
log.Debugf("Shutting Down Storage for %v", id)
app.storage[id].Shutdown()
log.Debugf("Shutting Down Bus for %v", id)
app.eventBuses[id].Shutdown()
app.shutdownPeer(id)
}
log.Debugf("Shutting Down App")
app.eventQueue.Shutdown()
app.appBus.Shutdown()
log.Debugf("Shut Down Complete")
}

View File

@ -1,39 +0,0 @@
package app
import "cwtch.im/cwtch/event"
import "git.openprivacy.ca/openprivacy/log"
const (
// DestApp should be used as a destination for IPC messages that are for the application itself an not a peer
DestApp = "app"
)
type applicationBridge struct {
applicationCore
bridge event.IPCBridge
handle func(*event.Event)
}
func (ab *applicationBridge) listen() {
log.Infoln("ab.listen()")
for {
ipcMessage, ok := ab.bridge.Read()
log.Debugf("listen() got %v for %v\n", ipcMessage.Message.EventType, ipcMessage.Dest)
if !ok {
log.Debugln("exiting appBridge.listen()")
return
}
if ipcMessage.Dest == DestApp {
ab.handle(&ipcMessage.Message)
} else {
if eventBus, exists := ab.eventBuses[ipcMessage.Dest]; exists {
eventBus.PublishLocal(ipcMessage.Message)
}
}
}
}
func (ab *applicationBridge) Shutdown() {
}

View File

@ -1,177 +0,0 @@
package app
import (
"cwtch.im/cwtch/app/plugins"
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/peer"
"cwtch.im/cwtch/storage"
"fmt"
"git.openprivacy.ca/openprivacy/log"
"path"
"strconv"
"sync"
)
type applicationClient struct {
applicationBridge
appletPeers
appBus event.Manager
acmutex sync.Mutex
}
// NewAppClient returns an Application that acts as a client to a AppService, connected by the IPCBridge supplied
func NewAppClient(appDirectory string, bridge event.IPCBridge) Application {
appClient := &applicationClient{appletPeers: appletPeers{peers: make(map[string]peer.CwtchPeer)}, applicationBridge: applicationBridge{applicationCore: *newAppCore(appDirectory), bridge: bridge}, appBus: event.NewEventManager()}
appClient.handle = appClient.handleEvent
go appClient.listen()
appClient.bridge.Write(&event.IPCMessage{Dest: DestApp, Message: event.NewEventList(event.ReloadClient)})
log.Infoln("Created new App Client")
return appClient
}
// GetPrimaryBus returns the bus the Application uses for events that aren't peer specific
func (ac *applicationClient) GetPrimaryBus() event.Manager {
return ac.appBus
}
func (ac *applicationClient) handleEvent(ev *event.Event) {
switch ev.EventType {
case event.NewPeer:
localID := ev.Data[event.Identity]
key := ev.Data[event.Key]
salt := ev.Data[event.Salt]
reload := ev.Data[event.Status] == event.StorageRunning
created := ev.Data[event.Created]
ac.newPeer(localID, key, salt, reload, created)
case event.PeerDeleted:
onion := ev.Data[event.Identity]
ac.handleDeletedPeer(onion)
case event.PeerError:
ac.appBus.Publish(*ev)
case event.AppError:
ac.appBus.Publish(*ev)
case event.ACNStatus:
ac.appBus.Publish(*ev)
case event.ACNVersion:
ac.appBus.Publish(*ev)
case event.ReloadDone:
ac.appBus.Publish(*ev)
}
}
func (ac *applicationClient) newPeer(localID, key, salt string, reload bool, created string) {
var keyBytes [32]byte
var saltBytes [128]byte
copy(keyBytes[:], key)
copy(saltBytes[:], salt)
profile, err := storage.ReadProfile(path.Join(ac.directory, "profiles", localID), keyBytes, saltBytes)
if err != nil {
log.Errorf("Could not read profile for NewPeer event: %v\n", err)
ac.appBus.Publish(event.NewEventList(event.PeerError, event.Error, fmt.Sprintf("Could not read profile for NewPeer event: %v\n", err)))
return
}
_, exists := ac.peers[profile.Onion]
if exists {
log.Errorf("profile for onion %v already exists", profile.Onion)
ac.appBus.Publish(event.NewEventList(event.PeerError, event.Error, fmt.Sprintf("profile for onion %v already exists", profile.Onion)))
return
}
eventBus := event.NewIPCEventManager(ac.bridge, profile.Onion)
peer := peer.FromProfile(profile)
peer.Init(eventBus)
ac.peerLock.Lock()
defer ac.peerLock.Unlock()
ac.peers[profile.Onion] = peer
ac.eventBuses[profile.Onion] = eventBus
npEvent := event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.Onion, event.Created: created})
if reload {
npEvent.Data[event.Status] = event.StorageRunning
}
ac.appBus.Publish(npEvent)
if reload {
ac.bridge.Write(&event.IPCMessage{Dest: DestApp, Message: event.NewEventList(event.ReloadPeer, event.Identity, profile.Onion)})
}
}
// CreatePeer messages the service to create a new Peer with the given name
func (ac *applicationClient) CreatePeer(name string, password string) {
ac.CreateTaggedPeer(name, password, "")
}
func (ac *applicationClient) CreateTaggedPeer(name, password, tag string) {
log.Infof("appClient CreatePeer %v\n", name)
message := event.IPCMessage{Dest: DestApp, Message: event.NewEvent(event.CreatePeer, map[event.Field]string{event.ProfileName: name, event.Password: password, event.Data: tag})}
ac.bridge.Write(&message)
}
// DeletePeer messages the service to delete a peer
func (ac *applicationClient) DeletePeer(onion string, password string) {
message := event.IPCMessage{Dest: DestApp, Message: event.NewEvent(event.DeletePeer, map[event.Field]string{event.Identity: onion, event.Password: password})}
ac.bridge.Write(&message)
}
func (ac *applicationClient) ChangePeerPassword(onion, oldpass, newpass string) {
message := event.IPCMessage{Dest: onion, Message: event.NewEventList(event.ChangePassword, event.Password, oldpass, event.NewPassword, newpass)}
ac.bridge.Write(&message)
}
func (ac *applicationClient) handleDeletedPeer(onion string) {
ac.acmutex.Lock()
defer ac.acmutex.Unlock()
ac.peers[onion].Shutdown()
delete(ac.peers, onion)
ac.eventBuses[onion].Publish(event.NewEventList(event.ShutdownPeer, event.Identity, onion))
ac.applicationCore.DeletePeer(onion)
ac.appBus.Publish(event.NewEventList(event.PeerDeleted, event.Identity, onion))
}
func (ac *applicationClient) AddPeerPlugin(onion string, pluginID plugins.PluginID) {
message := event.IPCMessage{Dest: DestApp, Message: event.NewEvent(event.AddPeerPlugin, map[event.Field]string{event.Identity: onion, event.Data: strconv.Itoa(int(pluginID))})}
ac.bridge.Write(&message)
}
// LoadProfiles messages the service to load any profiles for the given password
func (ac *applicationClient) LoadProfiles(password string) {
message := event.IPCMessage{Dest: DestApp, Message: event.NewEvent(event.LoadProfiles, map[event.Field]string{event.Password: password})}
ac.bridge.Write(&message)
}
func (ac *applicationClient) QueryACNStatus() {
message := event.IPCMessage{Dest: DestApp, Message: event.NewEvent(event.GetACNStatus, map[event.Field]string{})}
ac.bridge.Write(&message)
}
func (ac *applicationClient) QueryACNVersion() {
message := event.IPCMessage{Dest: DestApp, Message: event.NewEvent(event.GetACNVersion, map[event.Field]string{})}
ac.bridge.Write(&message)
}
// ShutdownPeer shuts down a peer and removes it from the app's management
func (ac *applicationClient) ShutdownPeer(onion string) {
ac.acmutex.Lock()
defer ac.acmutex.Unlock()
ac.eventBuses[onion].Shutdown()
delete(ac.eventBuses, onion)
ac.peers[onion].Shutdown()
delete(ac.peers, onion)
message := event.IPCMessage{Dest: DestApp, Message: event.NewEvent(event.ShutdownPeer, map[event.Field]string{event.Identity: onion})}
ac.bridge.Write(&message)
}
// Shutdown shuts down the application client and all front end peer components
func (ac *applicationClient) Shutdown() {
for id := range ac.peers {
ac.ShutdownPeer(id)
}
ac.applicationBridge.Shutdown()
ac.appBus.Shutdown()
}

View File

@ -1,209 +0,0 @@
package app
import (
"cwtch.im/cwtch/app/plugins"
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/protocol/connections"
"cwtch.im/cwtch/storage"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
"git.openprivacy.ca/openprivacy/connectivity"
"git.openprivacy.ca/openprivacy/log"
path "path/filepath"
"strconv"
"sync"
)
type applicationService struct {
applicationBridge
appletACN
appletPlugins
storage map[string]storage.ProfileStore
engines map[string]connections.Engine
asmutex sync.Mutex
}
// ApplicationService is the back end of an application that manages engines and writing storage and communicates to an ApplicationClient by an IPCBridge
type ApplicationService interface {
Shutdown()
}
// NewAppService returns an ApplicationService that runs the backend of an app and communicates with a client by the supplied IPCBridge
func NewAppService(acn connectivity.ACN, appDirectory string, bridge event.IPCBridge) ApplicationService {
appService := &applicationService{storage: make(map[string]storage.ProfileStore), engines: make(map[string]connections.Engine), applicationBridge: applicationBridge{applicationCore: *newAppCore(appDirectory), bridge: bridge}}
appService.appletACN.init(acn, appService.getACNStatusHandler())
appService.handle = appService.handleEvent
go appService.listen()
log.Infoln("Created new App Service")
return appService
}
func (as *applicationService) handleEvent(ev *event.Event) {
log.Infof("app Service handleEvent %v\n", ev.EventType)
switch ev.EventType {
case event.CreatePeer:
profileName := ev.Data[event.ProfileName]
password := ev.Data[event.Password]
tag := ev.Data[event.Data]
as.createPeer(profileName, password, tag)
case event.DeletePeer:
onion := ev.Data[event.Identity]
password := ev.Data[event.Password]
as.deletePeer(onion, password)
message := event.IPCMessage{Dest: DestApp, Message: *ev}
as.bridge.Write(&message)
case event.AddPeerPlugin:
onion := ev.Data[event.Identity]
pluginID, _ := strconv.Atoi(ev.Data[event.Data])
as.AddPlugin(onion, plugins.PluginID(pluginID), as.eventBuses[onion], as.acn)
case event.LoadProfiles:
password := ev.Data[event.Password]
as.loadProfiles(password)
case event.ReloadClient:
for _, storage := range as.storage {
peerMsg := *storage.GetNewPeerMessage()
peerMsg.Data[event.Status] = event.StorageRunning
peerMsg.Data[event.Created] = event.False
message := event.IPCMessage{Dest: DestApp, Message: peerMsg}
as.bridge.Write(&message)
}
message := event.IPCMessage{Dest: DestApp, Message: event.NewEventList(event.ReloadDone)}
as.bridge.Write(&message)
case event.ReloadPeer:
onion := ev.Data[event.Identity]
events := as.storage[onion].GetStatusMessages()
for _, ev := range events {
message := event.IPCMessage{Dest: onion, Message: *ev}
as.bridge.Write(&message)
}
case event.GetACNStatus:
prog, status := as.acn.GetBootstrapStatus()
as.getACNStatusHandler()(prog, status)
case event.GetACNVersion:
version := as.acn.GetVersion()
as.bridge.Write(&event.IPCMessage{Dest: DestApp, Message: event.NewEventList(event.ACNVersion, event.Data, version)})
case event.ShutdownPeer:
onion := ev.Data[event.Identity]
as.ShutdownPeer(onion)
}
}
func (as *applicationService) createPeer(name, password, tag string) {
log.Infof("app Service create peer %v %v\n", name, password)
profile, err := as.applicationCore.CreatePeer(name)
as.eventBuses[profile.Onion] = event.IPCEventManagerFrom(as.bridge, profile.Onion, as.eventBuses[profile.Onion])
if err != nil {
log.Errorf("Could not create Peer: %v\n", err)
message := event.IPCMessage{Dest: DestApp, Message: event.NewEventList(event.PeerError, event.Error, err.Error())}
as.bridge.Write(&message)
return
}
profileStore := storage.CreateProfileWriterStore(as.eventBuses[profile.Onion], path.Join(as.directory, "profiles", profile.LocalID), password, profile)
peerAuthorizations := profile.ContactsAuthorizations()
// TODO: Would be nice if ProtocolEngine did not need to explicitly be given the Private Key.
identity := primitives.InitializeIdentity(profile.Name, &profile.Ed25519PrivateKey, &profile.Ed25519PublicKey)
engine := connections.NewProtocolEngine(identity, profile.Ed25519PrivateKey, as.acn, as.eventBuses[profile.Onion], peerAuthorizations)
as.storage[profile.Onion] = profileStore
as.engines[profile.Onion] = engine
peerMsg := *profileStore.GetNewPeerMessage()
peerMsg.Data[event.Created] = event.True
peerMsg.Data[event.Status] = event.StorageNew
message := event.IPCMessage{Dest: DestApp, Message: peerMsg}
as.bridge.Write(&message)
}
func (as *applicationService) loadProfiles(password string) {
count := 0
as.applicationCore.LoadProfiles(password, false, func(profile *model.Profile, profileStore storage.ProfileStore) {
as.eventBuses[profile.Onion] = event.IPCEventManagerFrom(as.bridge, profile.Onion, as.eventBuses[profile.Onion])
peerAuthorizations := profile.ContactsAuthorizations()
identity := primitives.InitializeIdentity(profile.Name, &profile.Ed25519PrivateKey, &profile.Ed25519PublicKey)
engine := connections.NewProtocolEngine(identity, profile.Ed25519PrivateKey, as.acn, as.eventBuses[profile.Onion], peerAuthorizations)
as.asmutex.Lock()
as.storage[profile.Onion] = profileStore
as.engines[profile.Onion] = engine
as.asmutex.Unlock()
peerMsg := *profileStore.GetNewPeerMessage()
peerMsg.Data[event.Created] = event.False
peerMsg.Data[event.Status] = event.StorageNew
message := event.IPCMessage{Dest: DestApp, Message: peerMsg}
as.bridge.Write(&message)
count++
})
if count == 0 {
message := event.IPCMessage{Dest: DestApp, Message: event.NewEventList(event.AppError, event.Error, event.AppErrLoaded0)}
as.bridge.Write(&message)
}
}
func (as *applicationService) getACNStatusHandler() func(int, string) {
return func(progress int, status string) {
progStr := strconv.Itoa(progress)
as.bridge.Write(&event.IPCMessage{Dest: DestApp, Message: event.NewEventList(event.ACNStatus, event.Progress, progStr, event.Status, status)})
as.applicationCore.coremutex.Lock()
defer as.applicationCore.coremutex.Unlock()
for _, bus := range as.eventBuses {
bus.Publish(event.NewEventList(event.ACNStatus, event.Progress, progStr, event.Status, status))
}
}
}
func (as *applicationService) deletePeer(onion, password string) {
as.asmutex.Lock()
defer as.asmutex.Unlock()
if as.storage[onion].CheckPassword(password) {
as.appletPlugins.ShutdownPeer(onion)
as.plugins.Delete(onion)
as.engines[onion].Shutdown()
delete(as.engines, onion)
as.storage[onion].Shutdown()
as.storage[onion].Delete()
delete(as.storage, onion)
as.eventBuses[onion].Publish(event.NewEventList(event.ShutdownPeer, event.Identity, onion))
as.applicationCore.DeletePeer(onion)
log.Debugf("Delete peer for %v Done\n", onion)
message := event.IPCMessage{Dest: DestApp, Message: event.NewEventList(event.PeerDeleted, event.Identity, onion)}
as.bridge.Write(&message)
return
}
message := event.IPCMessage{Dest: DestApp, Message: event.NewEventList(event.AppError, event.Error, event.PasswordMatchError, event.Identity, onion)}
as.bridge.Write(&message)
}
func (as *applicationService) ShutdownPeer(onion string) {
as.engines[onion].Shutdown()
delete(as.engines, onion)
as.storage[onion].Shutdown()
delete(as.storage, onion)
as.eventBuses[onion].Shutdown()
delete(as.eventBuses, onion)
}
// Shutdown shuts down the application Service and all peer related backend parts
func (as *applicationService) Shutdown() {
log.Debugf("shutting down application service...")
as.appletPlugins.Shutdown()
for id := range as.engines {
log.Debugf("shutting down application service peer engine %v", id)
as.ShutdownPeer(id)
}
}

6
app/app_constants.go Normal file
View File

@ -0,0 +1,6 @@
package app
// DefactoPasswordForUnencryptedProfiles is used to offer "un-passworded" profiles. Our storage encrypts everything with a password. We need an agreed upon
// password to use in that case, that the app case use behind the scenes to password and unlock with
// https://docs.openprivacy.ca/cwtch-security-handbook/profile_encryption_and_storage.html
const DefactoPasswordForUnencryptedProfiles = "be gay do crime"

View File

@ -1,121 +0,0 @@
package app
import (
"cwtch.im/cwtch/event"
"git.openprivacy.ca/openprivacy/connectivity"
"git.openprivacy.ca/openprivacy/log"
"sync"
"cwtch.im/cwtch/app/plugins"
"cwtch.im/cwtch/peer"
)
type appletPeers struct {
peerLock sync.Mutex
peers map[string]peer.CwtchPeer
launched bool // bit hacky, place holder while we transition to full multi peer support and a better api
}
type appletACN struct {
acn connectivity.ACN
}
type appletPlugins struct {
plugins sync.Map //map[string] []plugins.Plugin
}
// ***** applet ACN
func (a *appletACN) init(acn connectivity.ACN, publish func(int, string)) {
a.acn = acn
acn.SetStatusCallback(publish)
prog, status := acn.GetBootstrapStatus()
publish(prog, status)
}
func (a *appletACN) Shutdown() {
a.acn.Close()
}
// ***** appletPeers
func (ap *appletPeers) init() {
ap.peers = make(map[string]peer.CwtchPeer)
ap.launched = false
}
// LaunchPeers starts each peer Listening and connecting to peers and groups
func (ap *appletPeers) LaunchPeers() {
log.Debugf("appletPeers LaunchPeers\n")
ap.peerLock.Lock()
defer ap.peerLock.Unlock()
if ap.launched {
return
}
for pid, p := range ap.peers {
log.Debugf("Launching %v\n", pid)
p.Listen()
log.Debugf("done Listen() for %v\n", pid)
p.StartPeersConnections()
log.Debugf("done StartPeersConnections() for %v\n", pid)
}
ap.launched = true
}
// ListProfiles returns a map of onions to their profile's Name
func (ap *appletPeers) ListProfiles() []string {
var keys []string
ap.peerLock.Lock()
defer ap.peerLock.Unlock()
for handle := range ap.peers {
keys = append(keys, handle)
}
return keys
}
// GetPeer returns a cwtchPeer for a given onion address
func (ap *appletPeers) GetPeer(onion string) peer.CwtchPeer {
if peer, ok := ap.peers[onion]; ok {
return peer
}
return nil
}
// ***** applet Plugins
func (ap *appletPlugins) Shutdown() {
log.Debugf("shutting down applet plugins...")
ap.plugins.Range(func(k, v interface{}) bool {
log.Debugf("shutting down plugins for %v", k)
ap.ShutdownPeer(k.(string))
return true
})
}
func (ap *appletPlugins) ShutdownPeer(peerid string) {
log.Debugf("shutting down plugins for %v", peerid)
pluginsI, ok := ap.plugins.Load(peerid)
if ok {
plugins := pluginsI.([]plugins.Plugin)
for _, plugin := range plugins {
log.Debugf("shutting down plugin: %v", plugin)
plugin.Shutdown()
}
}
}
func (ap *appletPlugins) AddPlugin(peerid string, id plugins.PluginID, bus event.Manager, acn connectivity.ACN) {
if _, exists := ap.plugins.Load(peerid); !exists {
ap.plugins.Store(peerid, []plugins.Plugin{})
}
pluginsinf, _ := ap.plugins.Load(peerid)
peerPlugins := pluginsinf.([]plugins.Plugin)
newp := plugins.Get(id, bus, acn, peerid)
newp.Start()
peerPlugins = append(peerPlugins, newp)
log.Debugf("storing plugin for %v %v", peerid, peerPlugins)
ap.plugins.Store(peerid, peerPlugins)
}

47
app/plugins/antispam.go Normal file
View File

@ -0,0 +1,47 @@
package plugins
import (
"cwtch.im/cwtch/event"
"git.openprivacy.ca/openprivacy/log"
"time"
)
const antispamTickTime = 30 * time.Second
type antispam struct {
bus event.Manager
queue event.Queue
breakChan chan bool
}
func (a *antispam) Start() {
go a.run()
}
func (a *antispam) Id() PluginID {
return ANTISPAM
}
func (a *antispam) Shutdown() {
a.breakChan <- true
}
func (a *antispam) run() {
log.Debugf("running antispam trigger plugin")
for {
select {
case <-time.After(antispamTickTime):
// no fuss, just trigger the check. Downstream will filter out superfluous actions
a.bus.Publish(event.NewEvent(event.TriggerAntispamCheck, map[event.Field]string{}))
continue
case <-a.breakChan:
return
}
}
}
// NewAntiSpam returns a Plugin that when started will trigger antispam payments on a regular interval
func NewAntiSpam(bus event.Manager) Plugin {
cr := &antispam{bus: bus, queue: event.NewQueue(), breakChan: make(chan bool, 1)}
return cr
}

View File

@ -3,13 +3,26 @@ package plugins
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/protocol/connections"
"git.openprivacy.ca/openprivacy/connectivity/tor"
"git.openprivacy.ca/openprivacy/log"
"math"
"strconv"
"sync"
"time"
)
const tickTime = 10 * time.Second
const maxBackoff int = 32 // 320 seconds or ~5 min
// Todo: Move to protocol/connections
// This Plugin is now required and it makes more sense to run more integrated in engine
const tickTimeSec = 30
const tickTime = tickTimeSec * time.Second
const circuitTimeoutSecs int = 120
const MaxBaseTimeoutSec = 5 * 60 // a max base time out of 5 min
const maxFailedBackoff = 6 // 2^6 = 64 -> 64 * [2m to 5m] = 2h8m to 5h20m
const PriorityQueueTimeSinceQualifierHours float64 = 168
type connectionType int
@ -23,28 +36,129 @@ type contact struct {
state connections.ConnectionState
ctype connectionType
ticks int
backoff int
lastAttempt time.Time
failedCount int
lastSeen time.Time
queued bool
}
// compare a to b
// returns -1 if a < b
//
// 0 if a == b
// +1 if a > b
//
// algo: sort by failedCount first favouring less attempts, then sort by lastSeen time favouring more recent connections
func (a *contact) compare(b *contact) int {
if a.failedCount < b.failedCount {
return -1
} else if a.failedCount > b.failedCount {
return +1
}
if a.lastSeen.After(b.lastSeen) {
return -1
} else if a.lastSeen.Before(b.lastSeen) {
return +1
}
return 0
}
type connectionQueue struct {
queue []*contact
}
func newConnectionQueue() *connectionQueue {
return &connectionQueue{queue: []*contact{}}
}
func (cq *connectionQueue) insert(c *contact) {
// find loc
i := 0
var b *contact
for i, b = range cq.queue {
if c.compare(b) >= 0 {
break
}
}
// insert
if len(cq.queue) == i { // nil or empty slice or after last element
cq.queue = append(cq.queue, c)
} else {
cq.queue = append(cq.queue[:i+1], cq.queue[i:]...) // index < len(a)
cq.queue[i] = c
}
c.queued = true
}
func (cq *connectionQueue) dequeue() *contact {
if len(cq.queue) == 0 {
return nil
}
c := cq.queue[0]
cq.queue = cq.queue[1:]
c.queued = false
return c
}
func (cq *connectionQueue) len() int {
return len(cq.queue)
}
type contactRetry struct {
bus event.Manager
queue event.Queue
networkUp bool
running bool
breakChan chan bool
onion string
lastCheck time.Time
bus event.Manager
queue event.Queue
ACNUp bool
ACNUpTime time.Time
protocolEngine bool
running bool
breakChan chan bool
onion string
lastCheck time.Time
acnProgress int
connections sync.Map //[string]*contact
connections sync.Map //[string]*contact
pendingQueue *connectionQueue
priorityQueue *connectionQueue
authorizedPeers sync.Map
stallRetries bool
}
// NewConnectionRetry returns a Plugin that when started will retry connecting to contacts with a backoff timing
// NewConnectionRetry returns a Plugin that when started will retry connecting to contacts with a failedCount timing
func NewConnectionRetry(bus event.Manager, onion string) Plugin {
cr := &contactRetry{bus: bus, queue: event.NewQueue(), breakChan: make(chan bool), connections: sync.Map{}, networkUp: false, onion: onion}
cr := &contactRetry{bus: bus, queue: event.NewQueue(), breakChan: make(chan bool, 1), authorizedPeers: sync.Map{}, connections: sync.Map{}, stallRetries: true, ACNUp: false, ACNUpTime: time.Now(), protocolEngine: false, onion: onion, pendingQueue: newConnectionQueue(), priorityQueue: newConnectionQueue()}
return cr
}
// maxTorCircuitsPending a function to throttle access to tor network during start up
func (cr *contactRetry) maxTorCircuitsPending() int {
timeSinceStart := time.Since(cr.ACNUpTime)
if timeSinceStart < 30*time.Second {
return 4
} else if timeSinceStart < 4*time.Minute {
return 8
} else if timeSinceStart < 8*time.Minute {
return 16
}
return connections.TorMaxPendingConns
}
func (cr *contactRetry) connectingCount() int {
connecting := 0
cr.connections.Range(func(k, v interface{}) bool {
conn := v.(*contact)
if conn.state == connections.CONNECTING {
connecting++
}
return true
})
return connecting
}
func (cr *contactRetry) Start() {
if !cr.running {
go cr.run()
@ -53,48 +167,173 @@ func (cr *contactRetry) Start() {
}
}
func (cr *contactRetry) Id() PluginID {
return CONNECTIONRETRY
}
func (cr *contactRetry) run() {
cr.running = true
cr.bus.Subscribe(event.PeerStateChange, cr.queue)
cr.bus.Subscribe(event.ACNStatus, cr.queue)
cr.bus.Subscribe(event.ServerStateChange, cr.queue)
cr.bus.Subscribe(event.QueuePeerRequest, cr.queue)
cr.bus.Subscribe(event.QueueJoinServer, cr.queue)
cr.bus.Subscribe(event.DisconnectPeerRequest, cr.queue)
cr.bus.Subscribe(event.DisconnectServerRequest, cr.queue)
cr.bus.Subscribe(event.ProtocolEngineShutdown, cr.queue)
cr.bus.Subscribe(event.ProtocolEngineCreated, cr.queue)
cr.bus.Subscribe(event.DeleteContact, cr.queue)
cr.bus.Subscribe(event.UpdateConversationAuthorization, cr.queue)
cr.bus.Subscribe(event.PurgeRetries, cr.queue)
cr.bus.Subscribe(event.ResumeRetries, cr.queue)
for {
if time.Since(cr.lastCheck) > tickTime {
cr.retryDisconnected()
// Only attempt connection if both the ACN and the Protocol Engines are Online...
log.Debugf("restartFlow checking state")
if cr.ACNUp && cr.protocolEngine && !cr.stallRetries {
log.Debugf("restartFlow time to queue!!")
cr.requeueReady()
connectingCount := cr.connectingCount()
// do priority connections first...
for connectingCount < cr.maxTorCircuitsPending() && len(cr.priorityQueue.queue) > 0 {
contact := cr.priorityQueue.dequeue()
if contact == nil {
break
}
// could have received incoming connection while in queue, make sure still disconnected before trying
if contact.state == connections.DISCONNECTED {
cr.publishConnectionRequest(contact)
connectingCount++
}
}
for connectingCount < cr.maxTorCircuitsPending() && len(cr.pendingQueue.queue) > 0 {
contact := cr.pendingQueue.dequeue()
if contact == nil {
break
}
// could have received incoming connection while in queue, make sure still disconnected before trying
if contact.state == connections.DISCONNECTED {
cr.publishConnectionRequest(contact)
connectingCount++
}
}
cr.lastCheck = time.Now()
}
// regardless of if we're up, run manual force deconnectiong of timed out connections
cr.connections.Range(func(k, v interface{}) bool {
p := v.(*contact)
if p.state == connections.CONNECTING && time.Since(p.lastAttempt) > time.Duration(circuitTimeoutSecs)*time.Second*2 {
// we have been "connecting" for twice the circuttimeout so it's failed, we just didn't learn about it, manually disconnect
cr.handleEvent(p.id, connections.DISCONNECTED, p.ctype)
log.Errorf("had to manually set peer %v of profile %v to DISCONNECTED due to assumed circuit timeout (%v) seconds", p.id, cr.onion, circuitTimeoutSecs*2)
}
return true
})
select {
case e := <-cr.queue.OutChan():
switch e.EventType {
case event.PurgeRetries:
// Purge All Authorized Peers
cr.authorizedPeers.Range(func(key interface{}, value interface{}) bool {
cr.authorizedPeers.Delete(key)
return true
})
// Purge All Connection States
cr.connections.Range(func(key interface{}, value interface{}) bool {
cr.connections.Delete(key)
return true
})
case event.ResumeRetries:
log.Infof("resuming retries...")
cr.stallRetries = false
case event.DisconnectPeerRequest:
peer := e.Data[event.RemotePeer]
cr.authorizedPeers.Delete(peer)
case event.DisconnectServerRequest:
peer := e.Data[event.GroupServer]
cr.authorizedPeers.Delete(peer)
case event.DeleteContact:
// this case covers both servers and peers (servers are peers, and go through the
// same delete conversation flow)
peer := e.Data[event.RemotePeer]
cr.authorizedPeers.Delete(peer)
case event.UpdateConversationAuthorization:
// if we update the conversation authorization then we need to check if
// we need to remove blocked conversations from the regular flow.
peer := e.Data[event.RemotePeer]
blocked := e.Data[event.Blocked]
if blocked == "true" {
cr.authorizedPeers.Delete(peer)
}
case event.PeerStateChange:
state := connections.ConnectionStateToType()[e.Data[event.ConnectionState]]
peer := e.Data[event.RemotePeer]
cr.handleEvent(peer, state, peerConn)
// only handle state change events from pre-authorized peers;
if _, exists := cr.authorizedPeers.Load(peer); exists {
cr.handleEvent(peer, state, peerConn)
}
case event.ServerStateChange:
state := connections.ConnectionStateToType()[e.Data[event.ConnectionState]]
server := e.Data[event.GroupServer]
cr.handleEvent(server, state, serverConn)
// only handle state change events from pre-authorized servers;
if _, exists := cr.authorizedPeers.Load(server); exists {
cr.handleEvent(server, state, serverConn)
}
case event.QueueJoinServer:
fallthrough
case event.QueuePeerRequest:
lastSeen, err := time.Parse(time.RFC3339Nano, e.Data[event.LastSeen])
if err != nil {
lastSeen = event.CwtchEpoch
}
id := ""
if peer, exists := e.Data[event.RemotePeer]; exists {
id = peer
cr.addConnection(peer, connections.DISCONNECTED, peerConn, lastSeen)
} else if server, exists := e.Data[event.GroupServer]; exists {
id = server
cr.addConnection(server, connections.DISCONNECTED, serverConn, lastSeen)
}
// this was an authorized event, and so we store this peer.
log.Debugf("authorizing id: %v", id)
cr.authorizedPeers.Store(id, true)
if c, ok := cr.connections.Load(id); ok {
contact := c.(*contact)
if contact.state == connections.DISCONNECTED {
// prioritize connections made in the last week
if time.Since(contact.lastSeen).Hours() < PriorityQueueTimeSinceQualifierHours {
cr.priorityQueue.insert(contact)
} else {
cr.pendingQueue.insert(contact)
}
}
}
case event.ProtocolEngineShutdown:
cr.ACNUp = false
cr.protocolEngine = false
cr.stallRetries = true
cr.connections.Range(func(k, v interface{}) bool {
p := v.(*contact)
if p.state == connections.AUTHENTICATED || p.state == connections.SYNCED {
p.lastSeen = time.Now()
}
p.state = connections.DISCONNECTED
p.failedCount = 0
return true
})
case event.ProtocolEngineCreated:
cr.protocolEngine = true
cr.processStatus()
case event.ACNStatus:
prog := e.Data[event.Progress]
if prog == "100" && !cr.networkUp {
cr.networkUp = true
cr.connections.Range(func(k, v interface{}) bool {
p := v.(*contact)
p.ticks = 0
p.backoff = 1
if p.ctype == peerConn {
cr.bus.Publish(event.NewEvent(event.RetryPeerRequest, map[event.Field]string{event.RemotePeer: p.id}))
}
if p.ctype == serverConn {
cr.bus.Publish(event.NewEvent(event.RetryServerRequest, map[event.Field]string{event.GroupServer: p.id}))
}
return true
})
} else if prog != "100" {
cr.networkUp = false
progData := e.Data[event.Progress]
if prog, err := strconv.Atoi(progData); err == nil {
cr.acnProgress = prog
cr.processStatus()
}
}
@ -108,54 +347,173 @@ func (cr *contactRetry) run() {
}
}
func (cr *contactRetry) retryDisconnected() {
func (cr *contactRetry) processStatus() {
if !cr.protocolEngine {
cr.ACNUp = false
return
}
if cr.acnProgress == 100 && !cr.ACNUp {
// ACN is up...at this point we need to completely reset our state
// as there is no guarantee that the tor daemon shares our state anymore...
cr.ACNUp = true
cr.ACNUpTime = time.Now()
// reset all of the queues...
cr.priorityQueue = newConnectionQueue()
cr.pendingQueue = newConnectionQueue()
// Loop through connections. Reset state, and requeue...
cr.connections.Range(func(k, v interface{}) bool {
p := v.(*contact)
// only reload connections if they are on the authorized peers list
if _, exists := cr.authorizedPeers.Load(p.id); exists {
p.queued = true
// prioritize connections made recently...
log.Debugf("adding %v to queue", p.id)
if time.Since(p.lastSeen).Hours() < PriorityQueueTimeSinceQualifierHours {
cr.priorityQueue.insert(p)
} else {
cr.pendingQueue.insert(p)
}
}
return true
})
} else if cr.acnProgress != 100 {
cr.ACNUp = false
cr.connections.Range(func(k, v interface{}) bool {
p := v.(*contact)
p.failedCount = 0
p.queued = false
p.state = connections.DISCONNECTED
return true
})
}
}
func (cr *contactRetry) requeueReady() {
if !cr.ACNUp {
return
}
var retryable []*contact
throughPutPerMin := int((float64(cr.maxTorCircuitsPending()) / float64(circuitTimeoutSecs)) * 60.0)
queueCount := cr.priorityQueue.len() + cr.pendingQueue.len()
// adjustedBaseTimeout = basetimeoust * (queuedItemsCount / throughPutPerMin)
// when less items are queued than through put it'll lower adjustedBaseTimeOut, but that'll be reset in the next block
// when more items are queued it will increase the timeout, to a max of MaxBaseTimeoutSec (enforced in the next block)
adjustedBaseTimeout := circuitTimeoutSecs * (queueCount / throughPutPerMin)
// circuitTimeoutSecs (120s) < adjustedBaseTimeout < MaxBaseTimeoutSec (300s)
if adjustedBaseTimeout < circuitTimeoutSecs {
adjustedBaseTimeout = circuitTimeoutSecs
} else if adjustedBaseTimeout > MaxBaseTimeoutSec {
adjustedBaseTimeout = MaxBaseTimeoutSec
}
cr.connections.Range(func(k, v interface{}) bool {
p := v.(*contact)
if p.state == connections.DISCONNECTED {
p.ticks++
if p.ticks >= p.backoff {
p.ticks = 0
if cr.networkUp {
if p.ctype == peerConn {
cr.bus.Publish(event.NewEvent(event.RetryPeerRequest, map[event.Field]string{event.RemotePeer: p.id}))
}
if p.ctype == serverConn {
cr.bus.Publish(event.NewEvent(event.RetryServerRequest, map[event.Field]string{event.GroupServer: p.id}))
}
// Don't retry anyone who isn't on the authorized peers list
if _, exists := cr.authorizedPeers.Load(p.id); exists {
if p.state == connections.DISCONNECTED && !p.queued {
timeout := time.Duration((math.Pow(2, float64(p.failedCount)))*float64(adjustedBaseTimeout /*baseTimeoutSec*/)) * time.Second
if time.Since(p.lastAttempt) > timeout {
retryable = append(retryable, p)
}
}
}
return true
})
for _, contact := range retryable {
if time.Since(contact.lastSeen).Hours() < PriorityQueueTimeSinceQualifierHours {
cr.priorityQueue.insert(contact)
} else {
cr.pendingQueue.insert(contact)
}
}
}
func (cr *contactRetry) publishConnectionRequest(contact *contact) {
log.Debugf("RestartFlow Publish Connection Request listener %v", contact)
if contact.ctype == peerConn {
cr.bus.Publish(event.NewEvent(event.PeerRequest, map[event.Field]string{event.RemotePeer: contact.id}))
}
if contact.ctype == serverConn {
cr.bus.Publish(event.NewEvent(event.RetryServerRequest, map[event.Field]string{event.GroupServer: contact.id}))
}
contact.state = connections.CONNECTING // Hacky but needed so we don't over flood waiting for PeerStateChange from engine
contact.lastAttempt = time.Now()
}
func (cr *contactRetry) addConnection(id string, state connections.ConnectionState, ctype connectionType, lastSeen time.Time) {
// don't handle contact retries for ourselves
if id == cr.onion {
return
}
if _, exists := cr.connections.Load(id); !exists {
p := &contact{id: id, state: state, failedCount: 0, lastAttempt: event.CwtchEpoch, ctype: ctype, lastSeen: lastSeen, queued: false}
cr.connections.Store(id, p)
return
} else {
// we have rerequested this connnection, probably via an explicit ask, update it's state
if c, ok := cr.connections.Load(id); ok {
contact := c.(*contact)
contact.state = state
}
}
}
func (cr *contactRetry) handleEvent(id string, state connections.ConnectionState, ctype connectionType) {
log.Debugf("cr.handleEvent state to %v on id %v", connections.ConnectionStateName[state], id)
// don't handle contact retries for ourselves
if id == cr.onion {
return
}
// reject events that contain invalid hostnames...we cannot connect to them
// and they could result in spurious connection attempts...
if !tor.IsValidHostname(id) {
return
}
if _, exists := cr.connections.Load(id); !exists {
p := &contact{id: id, state: connections.DISCONNECTED, backoff: 0, ticks: 0, ctype: ctype}
cr.connections.Store(id, p)
// We have an event for something we don't know about...
// The only reason this should happen is if a *new* Peer/Server connection has changed.
// Let's set the timeout to Now() to indicate that this is a fresh connection, and so should likely be prioritized.
cr.addConnection(id, state, ctype, time.Now())
return
}
pinf, _ := cr.connections.Load(id)
p := pinf.(*contact)
log.Debugf(" managing state change for %v %v to %v by self %v", id, connections.ConnectionStateName[p.state], connections.ConnectionStateName[state], cr.onion)
if state == connections.DISCONNECTED || state == connections.FAILED || state == connections.KILLED {
p.state = connections.DISCONNECTED
if p.backoff == 0 {
p.backoff = 1
} else if p.backoff < maxBackoff {
p.backoff *= 2
if p.state == connections.SYNCED || p.state == connections.AUTHENTICATED {
p.lastSeen = time.Now()
} else {
p.failedCount += 1
}
p.state = connections.DISCONNECTED
p.lastAttempt = time.Now()
if p.failedCount > maxFailedBackoff {
p.failedCount = maxFailedBackoff
}
p.ticks = 0
} else if state == connections.CONNECTING || state == connections.CONNECTED {
p.state = state
} else if state == connections.AUTHENTICATED {
} else if state == connections.AUTHENTICATED || state == connections.SYNCED {
p.state = state
p.backoff = 0
p.lastSeen = time.Now()
p.failedCount = 0
}
}
func (cr *contactRetry) Shutdown() {
cr.breakChan <- true
cr.queue.Shutdown()
}

View File

@ -0,0 +1,128 @@
package plugins
import (
"testing"
"time"
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/protocol/connections"
"git.openprivacy.ca/openprivacy/log"
)
// TestContactRetryQueue simulates some basic connection queueing
// NOTE: This whole test is a race condition, and does flag go's detector
// We are invasively checking the internal state of the retry plugin and accessing pointers from another
// thread.
// We could build an entire thread safe monitoring functonality, but that would dramatically expand the scope of this test.
func TestContactRetryQueue(t *testing.T) {
log.SetLevel(log.LevelDebug)
bus := event.NewEventManager()
cr := NewConnectionRetry(bus, "").(*contactRetry)
cr.ACNUp = true // fake an ACN connection...
cr.protocolEngine = true // fake protocol engine
cr.stallRetries = false // fake not being in offline mode...
go cr.run()
testOnion := "2wgvbza2mbuc72a4u6r6k4hc2blcvrmk4q26bfvlwbqxv2yq5k52fcqd"
t.Logf("contact plugin up and running..sending peer connection...")
// Assert that there is a peer connection identified as "test"
bus.Publish(event.NewEvent(event.QueuePeerRequest, map[event.Field]string{event.RemotePeer: testOnion, event.LastSeen: "test"}))
// Wait until the test actually exists, and is queued
// This is the worst part of this test setup. Ideally we would sleep, or some other yielding, but
// go test scheduling doesn't like that and even sleeping long periods won't cause the event thread to make
// progress...
setup := false
for !setup {
if _, exists := cr.connections.Load(testOnion); exists {
if _, exists := cr.authorizedPeers.Load(testOnion); exists {
t.Logf("authorized")
setup = true
}
}
}
// We should very quickly become connecting...
time.Sleep(time.Second)
pinf, _ := cr.connections.Load(testOnion)
if pinf.(*contact).state != 1 {
t.Fatalf("test connection should be in connecting after update, actually: %v", pinf.(*contact).state)
}
// Asset that "test" is authenticated
cr.handleEvent(testOnion, connections.AUTHENTICATED, peerConn)
// Assert that "test has a valid state"
pinf, _ = cr.connections.Load(testOnion)
if pinf.(*contact).state != 3 {
t.Fatalf("test connection should be in authenticated after update, actually: %v", pinf.(*contact).state)
}
// Publish an unrelated event to trigger the Plugin to go through a queuing cycle
// If we didn't do this we would have to wait 30 seconds for a check-in
bus.Publish(event.NewEvent(event.PeerStateChange, map[event.Field]string{event.RemotePeer: "test2", event.ConnectionState: "Disconnected"}))
bus.Publish(event.NewEvent(event.QueuePeerRequest, map[event.Field]string{event.RemotePeer: testOnion, event.LastSeen: time.Now().Format(time.RFC3339Nano)}))
time.Sleep(time.Second)
pinf, _ = cr.connections.Load(testOnion)
if pinf.(*contact).state != 1 {
t.Fatalf("test connection should be in connecting after update, actually: %v", pinf.(*contact).state)
}
cr.Shutdown()
}
// Takes around 4 min unless you adjust the consts for tickTimeSec and circuitTimeoutSecs
/*
func TestRetryEmission(t *testing.T) {
log.SetLevel(log.LevelDebug)
log.Infof("*** Starting TestRetryEmission! ***")
bus := event.NewEventManager()
testQueue := event.NewQueue()
bus.Subscribe(event.PeerRequest, testQueue)
cr := NewConnectionRetry(bus, "").(*contactRetry)
cr.Start()
time.Sleep(100 * time.Millisecond)
bus.Publish(event.NewEventList(event.ACNStatus, event.Progress, "100"))
bus.Publish(event.NewEventList(event.ProtocolEngineCreated))
pub, _, _ := ed25519.GenerateKey(rand.Reader)
peerAddr := tor.GetTorV3Hostname(pub)
bus.Publish(event.NewEventList(event.QueuePeerRequest, event.RemotePeer, peerAddr, event.LastSeen, time.Now().Format(time.RFC3339Nano)))
log.Infof("Fetching 1st event")
ev := testQueue.Next()
if ev.EventType != event.PeerRequest {
t.Errorf("1st event emitted was %v, expected %v", ev.EventType, event.PeerRequest)
}
log.Infof("1st event: %v", ev)
bus.Publish(event.NewEventList(event.PeerStateChange, event.RemotePeer, peerAddr, event.ConnectionState, connections.ConnectionStateName[connections.DISCONNECTED]))
log.Infof("fetching 2nd event")
ev = testQueue.Next()
log.Infof("2nd event: %v", ev)
if ev.EventType != event.PeerRequest {
t.Errorf("2nd event emitted was %v, expected %v", ev.EventType, event.PeerRequest)
}
bus.Publish(event.NewEventList(event.PeerStateChange, event.RemotePeer, peerAddr, event.ConnectionState, connections.ConnectionStateName[connections.CONNECTED]))
time.Sleep(100 * time.Millisecond)
bus.Publish(event.NewEventList(event.PeerStateChange, event.RemotePeer, peerAddr, event.ConnectionState, connections.ConnectionStateName[connections.DISCONNECTED]))
log.Infof("fetching 3rd event")
ev = testQueue.Next()
log.Infof("3nd event: %v", ev)
if ev.EventType != event.PeerRequest {
t.Errorf("3nd event emitted was %v, expected %v", ev.EventType, event.PeerRequest)
}
cr.Shutdown()
}
*/

49
app/plugins/heartbeat.go Normal file
View File

@ -0,0 +1,49 @@
package plugins
import (
"cwtch.im/cwtch/event"
"git.openprivacy.ca/openprivacy/log"
"time"
)
const heartbeatTickTime = 60 * time.Second
type heartbeat struct {
bus event.Manager
queue event.Queue
breakChan chan bool
}
func (hb *heartbeat) Start() {
go hb.run()
}
func (hb *heartbeat) Id() PluginID {
return HEARTBEAT
}
func (hb *heartbeat) Shutdown() {
hb.breakChan <- true
hb.queue.Shutdown()
}
func (hb *heartbeat) run() {
log.Debugf("running heartbeat trigger plugin")
for {
select {
case <-time.After(heartbeatTickTime):
// no fuss, just trigger the beat.
hb.bus.Publish(event.NewEvent(event.Heartbeat, map[event.Field]string{}))
continue
case <-hb.breakChan:
log.Debugf("shutting down heartbeat plugin")
return
}
}
}
// NewHeartbeat returns a Plugin that when started will trigger heartbeat checks on a regular interval
func NewHeartbeat(bus event.Manager) Plugin {
cr := &heartbeat{bus: bus, queue: event.NewQueue(), breakChan: make(chan bool, 1)}
return cr
}

View File

@ -3,7 +3,7 @@ package plugins
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/protocol/connections"
"fmt"
"cwtch.im/cwtch/utils"
"git.openprivacy.ca/openprivacy/connectivity"
"git.openprivacy.ca/openprivacy/log"
"sync"
@ -16,21 +16,23 @@ const NetworkCheckError = "Error"
// NetworkCheckSuccess is a status for when the NetworkCheck Plugin has had a successful message from a peer, indicating it is online right now
const NetworkCheckSuccess = "Success"
const NetworkCheckPeriod = time.Minute
// networkCheck is a convenience plugin for testing high level availability of onion services
type networkCheck struct {
bus event.Manager
queue event.Queue
acn connectivity.ACN
onionsToCheck sync.Map // onion:string => true:bool
breakChan chan bool
running bool
offline bool
offlineLock sync.Mutex
bus event.Manager
queue event.Queue
onion string
acn connectivity.ACN
breakChan chan bool
running bool
offline bool
offlineLock sync.Mutex
}
// NewNetworkCheck returns a Plugin that when started will attempt various network tests
func NewNetworkCheck(bus event.Manager, acn connectivity.ACN) Plugin {
nc := &networkCheck{bus: bus, acn: acn, queue: event.NewQueue(), breakChan: make(chan bool, 1)}
func NewNetworkCheck(onion string, bus event.Manager, acn connectivity.ACN) Plugin {
nc := &networkCheck{onion: onion, bus: bus, acn: acn, queue: event.NewQueue(), breakChan: make(chan bool, 1)}
return nc
}
@ -38,6 +40,10 @@ func (nc *networkCheck) Start() {
go nc.run()
}
func (nc *networkCheck) Id() PluginID {
return NETWORKCHECK
}
func (nc *networkCheck) run() {
nc.running = true
nc.offline = true
@ -49,7 +55,7 @@ func (nc *networkCheck) run() {
nc.bus.Subscribe(event.ServerStateChange, nc.queue)
nc.bus.Subscribe(event.NewGetValMessageFromPeer, nc.queue)
nc.bus.Subscribe(event.NewRetValMessageFromPeer, nc.queue)
var lastMessageReceived time.Time
var lastMessageReceived = time.Now()
for {
select {
case <-nc.breakChan:
@ -61,12 +67,13 @@ func (nc *networkCheck) run() {
// and then we will wait a minute and check the connection for the first time (the onion should be up)
// under normal operating circumstances
case event.ProtocolEngineStartListen:
if _, exists := nc.onionsToCheck.Load(e.Data[event.Onion]); !exists {
if nc.onion == (e.Data[event.Onion]) {
log.Debugf("initiating connection check for %v", e.Data[event.Onion])
nc.onionsToCheck.Store(e.Data[event.Onion], true)
if time.Since(lastMessageReceived) > time.Minute {
nc.selfTest()
}
} else {
log.Errorf("network check plugin received an event for a different profile than it was started with. Internal wiring is probably wrong.")
}
case event.PeerStateChange:
fallthrough
@ -96,10 +103,11 @@ func (nc *networkCheck) run() {
}
nc.offlineLock.Unlock()
}
case <-time.After(tickTime):
case <-time.After(NetworkCheckPeriod):
// if we haven't received an action in the last minute...kick off a set of testing
if time.Since(lastMessageReceived) > time.Minute {
nc.selfTest()
lastMessageReceived = time.Now()
}
}
}
@ -114,26 +122,22 @@ func (nc *networkCheck) Shutdown() {
}
func (nc *networkCheck) selfTest() {
nc.onionsToCheck.Range(func(key, val interface{}) bool {
go nc.checkConnection(key.(string))
return true
})
go nc.checkConnection(nc.onion)
}
//
func (nc *networkCheck) checkConnection(onion string) {
prog, _ := nc.acn.GetBootstrapStatus()
if prog != 100 {
progress, _ := nc.acn.GetBootstrapStatus()
if progress != 100 {
return
}
// we want to definitively time these actions out faster than tor will, because these onions should definitely be
// online
ClientTimeout := TimeoutPolicy(time.Second * 60)
ClientTimeout := utils.TimeoutPolicy(time.Second * 60)
err := ClientTimeout.ExecuteAction(func() error {
conn, _, err := nc.acn.Open(onion)
if err == nil {
conn.Close()
_ = conn.Close()
}
return err
})
@ -150,26 +154,3 @@ func (nc *networkCheck) checkConnection(onion string) {
nc.offline = false
}
}
// TODO we might want to reuse this, but for now it is only used by this plugin so it can live here
// TimeoutPolicy is an interface for enforcing common timeout patterns
type TimeoutPolicy time.Duration
// ExecuteAction runs a function and returns an error if it hasn't returned
// by the time specified by TimeoutPolicy
func (tp *TimeoutPolicy) ExecuteAction(action func() error) error {
c := make(chan error)
go func() {
c <- action()
}()
tick := time.NewTicker(time.Duration(*tp))
select {
case <-tick.C:
return fmt.Errorf("ActionTimedOutError")
case err := <-c:
return err
}
}

View File

@ -2,6 +2,7 @@ package plugins
import (
"cwtch.im/cwtch/event"
"fmt"
"git.openprivacy.ca/openprivacy/connectivity"
)
@ -12,22 +13,29 @@ type PluginID int
const (
CONNECTIONRETRY PluginID = iota
NETWORKCHECK
ANTISPAM
HEARTBEAT
)
// Plugin is the interface for a plugin
type Plugin interface {
Start()
Shutdown()
Id() PluginID
}
// Get is a plugin factory for the requested plugin
func Get(id PluginID, bus event.Manager, acn connectivity.ACN, onion string) Plugin {
func Get(id PluginID, bus event.Manager, acn connectivity.ACN, onion string) (Plugin, error) {
switch id {
case CONNECTIONRETRY:
return NewConnectionRetry(bus, onion)
return NewConnectionRetry(bus, onion), nil
case NETWORKCHECK:
return NewNetworkCheck(bus, acn)
return NewNetworkCheck(onion, bus, acn), nil
case ANTISPAM:
return NewAntiSpam(bus), nil
case HEARTBEAT:
return NewHeartbeat(bus), nil
}
return nil
return nil, fmt.Errorf("plugin not defined %v", id)
}

View File

@ -1,7 +1,6 @@
package utils
package app
import (
app2 "cwtch.im/cwtch/app"
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/model/constants"
"cwtch.im/cwtch/peer"
@ -12,10 +11,13 @@ import (
// Proper use of an App is to call CreatePeer and then process the NewPeer event
// however for small utility use, this function which polls the app until the peer is created
// may fill that usecase better
func WaitGetPeer(app app2.Application, name string) peer.CwtchPeer {
func WaitGetPeer(app Application, name string) peer.CwtchPeer {
for {
for _, handle := range app.ListProfiles() {
peer := app.GetPeer(handle)
if peer == nil {
continue
}
localName, _ := peer.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name)
if localName == name {
return peer

View File

@ -1,57 +0,0 @@
package bridge
import (
"cwtch.im/cwtch/event"
"sync"
)
type goChanBridge struct {
in chan event.IPCMessage
out chan event.IPCMessage
closedChan chan bool
closed bool
lock sync.Mutex
}
// MakeGoChanBridge returns a simple testing IPCBridge made from inprocess go channels
func MakeGoChanBridge() (b1, b2 event.IPCBridge) {
chan1 := make(chan event.IPCMessage)
chan2 := make(chan event.IPCMessage)
closed := make(chan bool)
a := &goChanBridge{in: chan1, out: chan2, closedChan: closed, closed: false}
b := &goChanBridge{in: chan2, out: chan1, closedChan: closed, closed: false}
go monitor(a, b)
return a, b
}
func monitor(a, b *goChanBridge) {
<-a.closedChan
a.closed = true
b.closed = true
a.closedChan <- true
}
func (pb *goChanBridge) Read() (*event.IPCMessage, bool) {
message, ok := <-pb.in
return &message, ok
}
func (pb *goChanBridge) Write(message *event.IPCMessage) {
pb.lock.Lock()
defer pb.lock.Unlock()
if !pb.closed {
pb.out <- *message
}
}
func (pb *goChanBridge) Shutdown() {
if !pb.closed {
close(pb.in)
close(pb.out)
pb.closedChan <- true
<-pb.closedChan
}
}

View File

@ -1,72 +0,0 @@
package bridge
/* Todo: When go generics ships, refactor this and event.infiniteChannel into one */
// InfiniteChannel implements the Channel interface with an infinite buffer between the input and the output.
type InfiniteChannel struct {
input, output chan interface{}
length chan int
buffer *InfiniteQueue
}
func newInfiniteChannel() *InfiniteChannel {
ch := &InfiniteChannel{
input: make(chan interface{}),
output: make(chan interface{}),
length: make(chan int),
buffer: newInfiniteQueue(),
}
go ch.infiniteBuffer()
return ch
}
// In returns the input channel
func (ch *InfiniteChannel) In() chan<- interface{} {
return ch.input
}
// Out returns the output channel
func (ch *InfiniteChannel) Out() <-chan interface{} {
return ch.output
}
// Len returns the length of items in queue
func (ch *InfiniteChannel) Len() int {
return <-ch.length
}
// Close closes the InfiniteChanel
func (ch *InfiniteChannel) Close() {
close(ch.input)
}
func (ch *InfiniteChannel) infiniteBuffer() {
var input, output chan interface{}
var next interface{}
input = ch.input
for input != nil || output != nil {
select {
case elem, open := <-input:
if open {
ch.buffer.Add(elem)
} else {
input = nil
}
case output <- next:
ch.buffer.Remove()
case ch.length <- ch.buffer.Length():
}
if ch.buffer.Length() > 0 {
output = ch.output
next = ch.buffer.Peek()
} else {
output = nil
next = nil
}
}
close(ch.output)
close(ch.length)
}

View File

@ -1,105 +0,0 @@
package bridge
/* Todo: When go generics ships, refactor this and event.infinitQueue channel into one */
/*
Package queue provides a fast, ring-buffer queue based on the version suggested by Dariusz Górecki.
Using this instead of other, simpler, queue implementations (slice+append or linked list) provides
substantial memory and time benefits, and fewer GC pauses.
The queue implemented here is as fast as it is for an additional reason: it is *not* thread-safe.
*/
// minQueueLen is smallest capacity that queue may have.
// Must be power of 2 for bitwise modulus: x % n == x & (n - 1).
const minQueueLen = 16
// InfiniteQueue represents a single instance of the queue data structure.
type InfiniteQueue struct {
buf []interface{}
head, tail, count int
}
// New constructs and returns a new Queue.
func newInfiniteQueue() *InfiniteQueue {
return &InfiniteQueue{
buf: make([]interface{}, minQueueLen),
}
}
// Length returns the number of elements currently stored in the queue.
func (q *InfiniteQueue) Length() int {
return q.count
}
// resizes the queue to fit exactly twice its current contents
// this can result in shrinking if the queue is less than half-full
func (q *InfiniteQueue) resize() {
newBuf := make([]interface{}, q.count<<1)
if q.tail > q.head {
copy(newBuf, q.buf[q.head:q.tail])
} else {
n := copy(newBuf, q.buf[q.head:])
copy(newBuf[n:], q.buf[:q.tail])
}
q.head = 0
q.tail = q.count
q.buf = newBuf
}
// Add puts an element on the end of the queue.
func (q *InfiniteQueue) Add(elem interface{}) {
if q.count == len(q.buf) {
q.resize()
}
q.buf[q.tail] = elem
// bitwise modulus
q.tail = (q.tail + 1) & (len(q.buf) - 1)
q.count++
}
// Peek returns the element at the head of the queue. This call panics
// if the queue is empty.
func (q *InfiniteQueue) Peek() interface{} {
if q.count <= 0 {
panic("queue: Peek() called on empty queue")
}
return q.buf[q.head]
}
// Get returns the element at index i in the queue. If the index is
// invalid, the call will panic. This method accepts both positive and
// negative index values. Index 0 refers to the first element, and
// index -1 refers to the last.
func (q *InfiniteQueue) Get(i int) interface{} {
// If indexing backwards, convert to positive index.
if i < 0 {
i += q.count
}
if i < 0 || i >= q.count {
panic("queue: Get() called with index out of range")
}
// bitwise modulus
return q.buf[(q.head+i)&(len(q.buf)-1)]
}
// Remove removes and returns the element from the front of the queue. If the
// queue is empty, the call will panic.
func (q *InfiniteQueue) Remove() interface{} {
if q.count <= 0 {
panic("queue: Remove() called on empty queue")
}
ret := q.buf[q.head]
q.buf[q.head] = nil
// bitwise modulus
q.head = (q.head + 1) & (len(q.buf) - 1)
q.count--
// Resize down if buffer 1/4 full.
if len(q.buf) > minQueueLen && (q.count<<2) == len(q.buf) {
q.resize()
}
return ret
}

View File

@ -1,19 +0,0 @@
// +build windows
package bridge
import (
"cwtch.im/cwtch/event"
"log"
)
func NewPipeBridgeClient(inFilename, outFilename string) event.IPCBridge {
log.Fatal("Not supported on windows")
return nil
}
// NewPipeBridgeService returns a pipe backed IPCBridge for a service
func NewPipeBridgeService(inFilename, outFilename string) event.IPCBridge {
log.Fatal("Not supported on windows")
return nil
}

View File

@ -1,357 +0,0 @@
// +build !windows
package bridge
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/protocol/connections"
"encoding/base64"
"encoding/binary"
"encoding/json"
"git.openprivacy.ca/openprivacy/log"
"os"
"sync"
"syscall"
"time"
)
/* pipeBridge creates a pair of named pipes
Needs a call to new client and service to fully successfully open
*/
const maxBufferSize = 1000
const serviceName = "service"
const clientName = "client"
const syn = "SYN"
const synack = "SYNACK"
const ack = "ACK"
type pipeBridge struct {
infile, outfile string
in, out *os.File
read chan event.IPCMessage
write *InfiniteChannel
closedChan chan bool
state connections.ConnectionState
lock sync.Mutex
threeShake func() bool
// For logging / debugging purposes
name string
}
func newPipeBridge(inFilename, outFilename string) *pipeBridge {
syscall.Mkfifo(inFilename, 0600)
syscall.Mkfifo(outFilename, 0600)
pb := &pipeBridge{infile: inFilename, outfile: outFilename, state: connections.DISCONNECTED}
pb.read = make(chan event.IPCMessage, maxBufferSize)
pb.write = newInfiniteChannel() //make(chan event.IPCMessage, maxBufferSize)
return pb
}
// NewPipeBridgeClient returns a pipe backed IPCBridge for a client
func NewPipeBridgeClient(inFilename, outFilename string) event.IPCBridge {
log.Debugf("Making new PipeBridge Client...\n")
pb := newPipeBridge(inFilename, outFilename)
pb.name = clientName
pb.threeShake = pb.threeShakeClient
go pb.connectionManager()
return pb
}
// NewPipeBridgeService returns a pipe backed IPCBridge for a service
func NewPipeBridgeService(inFilename, outFilename string) event.IPCBridge {
log.Debugf("Making new PipeBridge Service...\n")
pb := newPipeBridge(inFilename, outFilename)
pb.name = serviceName
pb.threeShake = pb.threeShakeService
go pb.connectionManager()
log.Debugf("Successfully created new PipeBridge Service!\n")
return pb
}
func (pb *pipeBridge) setState(state connections.ConnectionState) {
pb.lock.Lock()
defer pb.lock.Unlock()
pb.state = state
}
func (pb *pipeBridge) getState() connections.ConnectionState {
pb.lock.Lock()
defer pb.lock.Unlock()
return pb.state
}
func (pb *pipeBridge) connectionManager() {
for pb.getState() != connections.KILLED {
log.Debugf("clientConnManager loop start init\n")
pb.setState(connections.CONNECTING)
var err error
log.Debugf("%v open file infile\n", pb.name)
pb.in, err = os.OpenFile(pb.infile, os.O_RDWR, 0600)
if err != nil {
pb.setState(connections.DISCONNECTED)
continue
}
log.Debugf("%v open file outfile\n", pb.name)
pb.out, err = os.OpenFile(pb.outfile, os.O_RDWR, 0600)
if err != nil {
pb.setState(connections.DISCONNECTED)
continue
}
log.Debugf("Successfully connected PipeBridge %v!\n", pb.name)
pb.handleConns()
}
log.Debugf("exiting %v ConnectionManager\n", pb.name)
}
// threeShake performs a 3way handshake sync up
func (pb *pipeBridge) threeShakeService() bool {
synacked := false
for {
resp, err := pb.readString()
if err != nil {
return false
}
if string(resp) == syn {
if !synacked {
err = pb.writeString([]byte(synack))
if err != nil {
return false
}
synacked = true
}
} else if string(resp) == ack {
return true
}
}
}
func (pb *pipeBridge) synLoop(stop chan bool) {
delay := time.Duration(0)
for {
select {
case <-time.After(delay):
err := pb.writeString([]byte(syn))
if err != nil {
return
}
delay = time.Second
case <-stop:
return
}
}
}
func (pb *pipeBridge) threeShakeClient() bool {
stop := make(chan bool)
go pb.synLoop(stop)
for {
resp, err := pb.readString()
if err != nil {
return false
}
if string(resp) == synack {
stop <- true
err := pb.writeString([]byte(ack))
return err == nil
}
}
}
func (pb *pipeBridge) handleConns() {
if !pb.threeShake() {
pb.setState(connections.FAILED)
pb.closeReset()
return
}
pb.setState(connections.AUTHENTICATED)
pb.closedChan = make(chan bool, 5)
log.Debugf("handleConns authed, %v 2xgo\n", pb.name)
go pb.handleRead()
go pb.handleWrite()
<-pb.closedChan
log.Debugf("handleConns <-closedChan (%v)\n", pb.name)
if pb.getState() != connections.KILLED {
pb.setState(connections.FAILED)
}
pb.closeReset()
log.Debugf("handleConns done for %v, exit\n", pb.name)
}
func (pb *pipeBridge) closeReset() {
pb.in.Close()
pb.out.Close()
close(pb.read)
pb.write.Close()
if pb.getState() != connections.KILLED {
pb.read = make(chan event.IPCMessage, maxBufferSize)
pb.write = newInfiniteChannel()
}
}
func (pb *pipeBridge) handleWrite() {
log.Debugf("handleWrite() %v\n", pb.name)
defer log.Debugf("exiting handleWrite() %v\n", pb.name)
for {
select {
case messageInf := <-pb.write.output:
if messageInf == nil {
pb.closedChan <- true
return
}
message := messageInf.(event.IPCMessage)
if message.Message.EventType == event.EncryptedGroupMessage || message.Message.EventType == event.SendMessageToGroup || message.Message.EventType == event.NewMessageFromGroup {
log.Debugf("handleWrite <- message: %v %v ...\n", message.Dest, message.Message.EventType)
} else {
log.Debugf("handleWrite <- message: %v\n", message)
}
if pb.getState() == connections.AUTHENTICATED {
encMessage := &event.IPCMessage{Dest: message.Dest, Message: event.Event{EventType: message.Message.EventType, EventID: message.Message.EventID, Data: make(map[event.Field]string)}}
for k, v := range message.Message.Data {
encMessage.Message.Data[k] = base64.StdEncoding.EncodeToString([]byte(v))
}
messageJSON, _ := json.Marshal(encMessage)
err := pb.writeString(messageJSON)
if err != nil {
pb.closedChan <- true
return
}
} else {
return
}
}
}
}
func (pb *pipeBridge) handleRead() {
log.Debugf("handleRead() %v\n", pb.name)
defer log.Debugf("exiting handleRead() %v", pb.name)
for {
log.Debugf("Waiting to handleRead()...\n")
buffer, err := pb.readString()
if err != nil {
pb.closedChan <- true
return
}
var message event.IPCMessage
err = json.Unmarshal(buffer, &message)
if err != nil {
log.Errorf("Read error: '%v', value: '%v'", err, buffer)
pb.closedChan <- true
return // probably new connection trying to initialize
}
for k, v := range message.Message.Data {
val, _ := base64.StdEncoding.DecodeString(v)
message.Message.Data[k] = string(val)
}
if message.Message.EventType == event.EncryptedGroupMessage || message.Message.EventType == event.SendMessageToGroup || message.Message.EventType == event.NewMessageFromGroup {
log.Debugf("handleRead read<-: %v %v ...\n", message.Dest, message.Message.EventType)
} else {
log.Debugf("handleRead read<-: %v\n", message)
}
pb.read <- message
log.Debugf("handleRead wrote\n")
}
}
func (pb *pipeBridge) Read() (*event.IPCMessage, bool) {
log.Debugf("Read() %v...\n", pb.name)
var ok = false
var message event.IPCMessage
for !ok && pb.getState() != connections.KILLED {
message, ok = <-pb.read
if message.Message.EventType == event.EncryptedGroupMessage || message.Message.EventType == event.SendMessageToGroup || message.Message.EventType == event.NewMessageFromGroup {
log.Debugf("Read %v: %v %v ...\n", pb.name, message.Dest, message.Message.EventType)
} else {
log.Debugf("Read %v: %v\n", pb.name, message)
}
}
return &message, pb.getState() != connections.KILLED
}
func (pb *pipeBridge) Write(message *event.IPCMessage) {
if message.Message.EventType == event.EncryptedGroupMessage || message.Message.EventType == event.SendMessageToGroup || message.Message.EventType == event.NewMessageFromGroup {
log.Debugf("Write %v: %v %v ...\n", pb.name, message.Dest, message.Message.EventType)
} else {
log.Debugf("Write %v: %v\n", pb.name, message)
}
pb.write.input <- *message
log.Debugf("Wrote\n")
}
func (pb *pipeBridge) Shutdown() {
log.Debugf("pb.Shutdown() for %v currently in state: %v\n", pb.name, connections.ConnectionStateName[pb.getState()])
pb.state = connections.KILLED
pb.closedChan <- true
log.Debugf("Done Shutdown for %v\n", pb.name)
}
func (pb *pipeBridge) writeString(message []byte) error {
size := make([]byte, 2)
binary.LittleEndian.PutUint16(size, uint16(len(message)))
pb.out.Write(size)
for pos := 0; pos < len(message); {
n, err := pb.out.Write(message[pos:])
if err != nil {
log.Errorf("Writing out on pipeBridge: %v\n", err)
return err
}
pos += n
}
return nil
}
func (pb *pipeBridge) readString() ([]byte, error) {
var n int
size := make([]byte, 2)
var err error
n, err = pb.in.Read(size)
if err != nil || n != 2 {
log.Errorf("Could not read len int from stream: %v\n", err)
return nil, err
}
n = int(binary.LittleEndian.Uint16(size))
pos := 0
buffer := make([]byte, n)
for n > 0 {
m, err := pb.in.Read(buffer[pos:])
if err != nil {
log.Errorf("Reading into buffer from pipe: %v\n", err)
return nil, err
}
n -= m
pos += m
}
return buffer, nil
}

View File

@ -1,131 +0,0 @@
package bridge
import (
"cwtch.im/cwtch/event"
"git.openprivacy.ca/openprivacy/log"
"os"
"testing"
"time"
)
var (
clientPipe = "./client"
servicePipe = "./service"
)
func clientHelper(t *testing.T, in, out string, messageOrig *event.IPCMessage, done chan bool) {
client := NewPipeBridgeClient(in, out)
messageAfter, ok := client.Read()
if !ok {
t.Errorf("Reading from client IPCBridge failed")
done <- true
return
}
if messageOrig.Dest != messageAfter.Dest {
t.Errorf("Dest's value differs expected: %v actaul: %v", messageOrig.Dest, messageAfter.Dest)
}
if messageOrig.Message.EventType != messageAfter.Message.EventType {
t.Errorf("EventTypes's value differs expected: %v actaul: %v", messageOrig.Message.EventType, messageAfter.Message.EventType)
}
if messageOrig.Message.Data[event.Identity] != messageAfter.Message.Data[event.Identity] {
t.Errorf("Data[Identity]'s value differs expected: %v actaul: %v", messageOrig.Message.Data[event.Identity], messageAfter.Message.Data[event.Identity])
}
done <- true
}
func serviceHelper(t *testing.T, in, out string, messageOrig *event.IPCMessage, done chan bool) {
service := NewPipeBridgeService(in, out)
service.Write(messageOrig)
done <- true
}
func TestPipeBridge(t *testing.T) {
os.Remove(servicePipe)
os.Remove(clientPipe)
messageOrig := &event.IPCMessage{Dest: "ABC", Message: event.NewEventList(event.NewPeer, event.Identity, "It is I")}
serviceDone := make(chan bool)
clientDone := make(chan bool)
go clientHelper(t, clientPipe, servicePipe, messageOrig, clientDone)
go serviceHelper(t, servicePipe, clientPipe, messageOrig, serviceDone)
<-serviceDone
<-clientDone
}
func restartingClient(t *testing.T, in, out string, done chan bool) {
client := NewPipeBridgeClient(in, out)
message1 := &event.IPCMessage{Dest: "ABC", Message: event.NewEventList(event.NewPeer)}
log.Infoln("client writing message 1")
client.Write(message1)
time.Sleep(100 * time.Millisecond)
log.Infoln("client shutdown")
client.Shutdown()
log.Infoln("client new client")
client = NewPipeBridgeClient(in, out)
message2 := &event.IPCMessage{Dest: "ABC", Message: event.NewEventList(event.DeleteContact)}
log.Infoln("client2 write message2")
client.Write(message2)
done <- true
}
func stableService(t *testing.T, in, out string, done chan bool) {
service := NewPipeBridgeService(in, out)
log.Infoln("service wait read 1")
message1, ok := service.Read()
log.Infof("service read 1 %v ok:%v\n", message1, ok)
if !ok {
t.Errorf("Reading from client IPCBridge 1st time failed")
done <- true
return
}
if message1.Message.EventType != event.NewPeer {
t.Errorf("Wrong message received, expected NewPeer\n")
done <- true
return
}
log.Infoln("service wait read 2")
message2, ok := service.Read()
log.Infof("service read 2 got %v ok:%v\n", message2, ok)
if !ok {
t.Errorf("Reading from client IPCBridge 2nd time failed")
done <- true
return
}
if message2.Message.EventType != event.DeleteContact {
t.Errorf("Wrong message received, expected DeleteContact, got %v\n", message2)
done <- true
return
}
done <- true
}
func TestReconnect(t *testing.T) {
log.Infoln("TestReconnect")
os.Remove(servicePipe)
os.Remove(clientPipe)
serviceDone := make(chan bool)
clientDone := make(chan bool)
go restartingClient(t, clientPipe, servicePipe, clientDone)
go stableService(t, servicePipe, clientPipe, serviceDone)
<-serviceDone
<-clientDone
}

View File

@ -1,5 +1,9 @@
package event
import "time"
var CwtchEpoch = time.Date(2020, 6, 1, 0, 0, 0, 0, time.UTC)
// Type captures the definition of many common Cwtch application events
type Type string
@ -13,11 +17,22 @@ const (
// RemotePeer: [eg "chpr7qm6op5vfcg2pi4vllco3h6aa7exexc4rqwnlupqhoogx2zgd6qd"
PeerRequest = Type("PeerRequest")
// RetryPeerRequest
// Identical to PeerRequest, but allows Engine to make decisions regarding blocked peers
// attributes:
// RemotePeer: [eg "chpr7qm6op5vfcg2pi4vllco3h6aa7exexc4rqwnlupqhoogx2zgd6qd"
RetryPeerRequest = Type("RetryPeerRequest")
// QueuePeerRequest
// When peer has too many peers to try and wants to ease off Tor throttling, use this to notify ContactRetry plugin to schedule a peer for later try
// LastSeen: last seen time of the contact
// And one of
// RemotePeer
// GroupServer
QueuePeerRequest = Type("QueuePeerRequest")
// Disconnect*Request
// Close active connections and prevent new connections
DisconnectPeerRequest = Type("DisconnectPeerRequest")
DisconnectServerRequest = Type("DisconnectServerRequest")
// Events to Manage Retry Contacts
PurgeRetries = Type("PurgeRetries")
ResumeRetries = Type("ResumeRetries")
// RetryServerRequest
// Asks CwtchPeer to retry a server connection...
@ -25,19 +40,24 @@ const (
RetryServerRequest = Type("RetryServerRequest")
// RemotePeer
// Authorization(model.peer.Auth_...)
SetPeerAuthorization = Type("UpdatePeerAuthorization")
// ConversationID
// Accepted
// Blocked
UpdateConversationAuthorization = Type("UpdateConversationAuthorization")
// Turn on/off blocking of unknown peers (if peers aren't in the contact list then they will be autoblocked
BlockUnknownPeers = Type("BlockUnknownPeers")
AllowUnknownPeers = Type("AllowUnknownPeers")
// GroupServer
JoinServer = Type("JoinServer")
QueueJoinServer = Type("QueueJoinServer")
JoinServer = Type("JoinServer")
// attributes GroupServer - the onion of the server to leave
LeaveServer = Type("LeaveServer")
ProtocolEngineCreated = Type("ProtocolEngineCreated")
ProtocolEngineShutdown = Type("ProtocolEngineShutdown")
ProtocolEngineStartListen = Type("ProtocolEngineStartListen")
ProtocolEngineStopped = Type("ProtocolEngineStopped")
@ -55,10 +75,6 @@ const (
// GroupID: groupID (allows them to fetch from the peer)
NewGroup = Type("NewGroup")
// GroupID
AcceptGroupInvite = Type("AcceptGroupInvite")
RejectGroupInvite = Type("RejectGroupInvite")
SendMessageToGroup = Type("SendMessagetoGroup")
//Ciphertext, Signature:
@ -77,8 +93,9 @@ const (
// Error: string describing the error
SendMessageToGroupError = Type("SendMessageToGroupError")
SendMessageToPeer = Type("SendMessageToPeer")
NewMessageFromPeer = Type("NewMessageFromPeer")
SendMessageToPeer = Type("SendMessageToPeer")
NewMessageFromPeer = Type("NewMessageFromPeer")
NewMessageFromPeerEngine = Type("NewMessageFromPeerEngine")
// RemotePeer, scope, path
NewGetValMessageFromPeer = Type("NewGetValMessageFromPeer")
@ -110,12 +127,6 @@ const (
// RemotePeer: The peer associated with the acknowledgement
IndexedFailure = Type("IndexedFailure")
// UpdateMessageFlags will change the flags associated with a given message.
// Handle
// Message Index
// Flags
UpdateMessageFlags = Type("UpdateMessageFlags")
// attributes:
// RemotePeer: [eg "chpr7qm6op5vfcg2pi4vllco3h6aa7exexc4rqwnlupqhoogx2zgd6qd"]
// Error: string describing the error
@ -126,18 +137,11 @@ const (
// a peer contact has been added
// attributes:
// RemotePeer [eg ""]
// Authorization
PeerCreated = Type("PeerCreated")
ContactCreated = Type("ContactCreated")
// Password, NewPassword
ChangePassword = Type("ChangePassword")
// Error(err), EventID
ChangePasswordError = Type("ChangePasswordError")
// EventID
ChangePasswordSuccess = Type("ChangePasswordSuccess")
// a group has been successfully added or newly created
// attributes:
// Data [serialized *model.Group]
@ -146,29 +150,6 @@ const (
// RemotePeer
DeleteContact = Type("DeleteContact")
// GroupID
DeleteGroup = Type("DeleteGroup")
// request to store a profile-wide attribute (good for e.g. per-profile settings like theme prefs)
// attributes:
// Key [eg "fontcolor"]
// Data [eg "red"]
SetAttribute = Type("SetAttribute")
// request to store a per-contact attribute (e.g. display names for a peer)
// attributes:
// RemotePeer [eg ""]
// Key [eg "nick"]
// Data [eg "erinn"]
SetPeerAttribute = Type("SetPeerAttribute")
// request to store a per-cwtch-group attribute (e.g. display name for a group)
// attributes:
// GroupID [eg ""]
// Key [eg "nick"]
// Data [eg "open privacy board"]
SetGroupAttribute = Type("SetGroupAttribute")
// PeerStateChange servers as a new incoming connection message as well, and can/is consumed by frontends to alert of new p2p connections
// RemotePeer
// ConnectionState
@ -180,9 +161,6 @@ const (
/***** Application client / service messages *****/
// ProfileName, Password, Data(tag)
CreatePeer = Type("CreatePeer")
// app: Identity(onion), Created(bool)
// service -> client: Identity(localId), Password, [Status(new/default=blank || from reload='running')], Created(bool)
NewPeer = Type("NewPeer")
@ -192,20 +170,6 @@ const (
// Identity(onion)
PeerDeleted = Type("PeerDeleted")
// Identity(onion), Data(pluginID)
AddPeerPlugin = Type("AddPeerPlugin")
// Password
LoadProfiles = Type("LoadProfiles")
// Client has reloaded, triggers NewPeer s then ReloadDone
ReloadClient = Type("ReloadClient")
ReloadDone = Type("ReloadDone")
// Identity - Ask service to resend all connection states
ReloadPeer = Type("ReloadPeer")
// Identity(onion)
ShutdownPeer = Type("ShutdownPeer")
@ -218,12 +182,12 @@ const (
// Error(err)
AppError = Type("AppError")
GetACNStatus = Type("GetACNStatus")
GetACNVersion = Type("GetACNVersion")
// Progress, Status
ACNStatus = Type("ACNStatus")
// ID, Key, Data
ACNInfo = Type("ACNInfo")
// Data
ACNVersion = Type("ACNVersion")
@ -233,18 +197,13 @@ const (
// Onion: the local onion we attempt to check
NetworkStatus = Type("NetworkError")
// Notify the UI that a Server has been added
// Onion = Server Onion
ServerCreated = Type("ServerAdded")
// For debugging. Allows test to emit a Syn and get a response Ack(eventID) when the subsystem is done processing a queue
Syn = Type("Syn")
Ack = Type("Ack")
// For situations where we want to update $Identity -> $RemotePeer/$GroupID's total message count to be $Data
MessageCounterResync = Type("MessageCounterResync")
// File Handling Events
StopFileShare = Type("StopFileShare")
StopAllFileShares = Type("StopAllFileShares")
ShareManifest = Type("ShareManifest")
ManifestSizeReceived = Type("ManifestSizeReceived")
ManifestError = Type("ManifestError")
@ -253,6 +212,24 @@ const (
FileDownloadProgressUpdate = Type("FileDownloadProgressUpdate")
FileDownloaded = Type("FileDownloaded")
FileVerificationFailed = Type("FileVerificationFailed")
// Profile Attribute Event
UpdatedProfileAttribute = Type("UpdatedProfileAttribute")
// Conversation Attribute Update...
UpdatedConversationAttribute = Type("UpdatedConversationAttribute")
StartingStorageMiragtion = Type("StartingStorageMigration")
DoneStorageMigration = Type("DoneStorageMigration")
TokenManagerInfo = Type("TokenManagerInfo")
TriggerAntispamCheck = Type("TriggerAntispamCheck")
MakeAntispamPayment = Type("MakeAntispamPayment")
// Heartbeat is used to trigger actions that need to happen every so often...
Heartbeat = Type("Heartbeat")
// Conversation Search
SearchResult = Type("SearchResult")
SearchCancelled = Type("SearchCancelled")
)
// Field defines common event attributes
@ -262,22 +239,28 @@ type Field string
const (
// A peers local onion address
Onion = Field("Onion")
Onion = Field("Onion")
ProfileOnion = Field("ProfileOnion")
RemotePeer = Field("RemotePeer")
LastSeen = Field("LastSeen")
Ciphertext = Field("Ciphertext")
Signature = Field("Signature")
CachedTokens = Field("CachedTokens")
PreviousSignature = Field("PreviousSignature")
TimestampSent = Field("TimestampSent")
TimestampReceived = Field("TimestampReceived")
Identity = Field("Identity")
ConversationID = Field("ConversationID")
GroupID = Field("GroupID")
GroupServer = Field("GroupServer")
GroupName = Field("GroupName")
ServerTokenY = Field("ServerTokenY")
ServerTokenOnion = Field("ServerTokenOnion")
GroupInvite = Field("GroupInvite")
ServerTokenCount = Field("ServerTokenCount")
ProfileName = Field("ProfileName")
Password = Field("Password")
@ -301,7 +284,10 @@ const (
Status = Field("Status")
EventID = Field("EventID")
EventContext = Field("EventContext")
Channel = Field("Channel")
Index = Field("Index")
RowIndex = Field("RowIndex")
ContentHash = Field("ContentHash")
// Handle denotes a contact handle of any type.
Handle = Field("Handle")
@ -309,7 +295,8 @@ const (
// Flags denotes a set of message flags
Flags = Field("Flags")
Authorization = Field("Authorization")
Accepted = Field("Accepted")
Blocked = Field("Blocked")
KeyBundle = Field("KeyBundle")
@ -318,13 +305,16 @@ const (
Source = Field("Source")
FileKey = Field("FileKey")
FileSizeInChunks = Field("FileSizeInChunks")
ManifestSize = Field("ManifestSize")
SerializedManifest = Field("SerializedManifest")
TempFile = Field("TempFile")
FilePath = Field("FilePath")
NameSuggestion = Field("NameSuggestion")
FileKey = Field("FileKey")
FileSizeInChunks = Field("FileSizeInChunks")
ManifestSize = Field("ManifestSize")
SerializedManifest = Field("SerializedManifest")
TempFile = Field("TempFile")
FilePath = Field("FilePath")
FileDownloadFinished = Field("FileDownloadFinished")
NameSuggestion = Field("NameSuggestion")
SearchID = Field("SearchID")
)
// Defining Common errors
@ -333,18 +323,13 @@ const (
PasswordMatchError = "Password did not match"
)
// Values to be suplied in event.NewPeer for Status
const (
StorageRunning = "running"
StorageNew = "new"
)
// Defining Protocol Contexts
const (
ContextAck = "im.cwtch.acknowledgement"
ContextInvite = "im.cwtch.invite"
ContextRaw = "im.cwtch.raw"
ContextGetVal = "im.cwtch.getVal"
ContextVersion = "im.cwtch.version"
ContextRetVal = "im.cwtch.retVal"
ContextRequestManifest = "im.cwtch.file.request.manifest"
ContextSendManifest = "im.cwtch.file.send.manifest"
@ -352,19 +337,25 @@ const (
ContextSendFile = "im.cwtch.file.send.chunk"
)
// Define Default Attribute Keys
// Define Attribute Keys related to history preservation
const (
SaveHistoryKey = "SavePeerHistory"
PreserveHistoryDefaultSettingKey = "SaveHistoryDefault" // profile level default
SaveHistoryKey = "SavePeerHistory" // peer level setting
)
// Define Default Attribute Values
const (
// Save History has 3 distinct states. By default we don't save history (DefaultDeleteHistory), if the peer confirms this
// we change to DeleteHistoryConfirmed, if they confirm they want to save then this becomes SaveHistoryConfirmed
// We use this distinction between default and confirmed to drive UI
DeleteHistoryDefault = "DefaultDeleteHistory"
// Save History has 3 distinct states. By default we refer to the profile level
// attribute PreserveHistoryDefaultSettingKey ( default: false i.e. DefaultDeleteHistory),
// For each contact, if the profile owner confirms deletion we change to DeleteHistoryConfirmed,
// if the profile owner confirms they want to save history then this becomes SaveHistoryConfirmed
// These settings are set at the UI level using Get/SetScopeZoneAttribute with scoped zone: local.profile.*
SaveHistoryConfirmed = "SaveHistory"
DeleteHistoryConfirmed = "DeleteHistoryConfirmed"
// NOTE: While this says "[DeleteHistory]Default", The actual behaviour will now depend on the
// global app/profile value of PreserveHistoryDefaultSettingKey
DeleteHistoryDefault = "DefaultDeleteHistory"
)
// Bool strings

View File

@ -10,12 +10,6 @@ type queue struct {
closed bool
}
type simpleQueue struct {
eventChannel chan Event
lock sync.Mutex
closed bool
}
// Queue is a wrapper around a channel for handling Events in a consistent way across subsystems.
// The expectation is that each subsystem in Cwtch will manage a given an event.Queue fed from
// the event.Manager.
@ -33,49 +27,6 @@ func NewQueue() Queue {
return queue
}
// NewSimpleQueue initializes an event.Queue of the given buffer size.
func NewSimpleQueue(buffer int) Queue {
queue := new(simpleQueue)
queue.eventChannel = make(chan Event, buffer)
return queue
}
func (sq *simpleQueue) inChan() chan<- Event {
return sq.eventChannel
}
func (sq *simpleQueue) OutChan() <-chan Event {
return sq.eventChannel
}
// Backlog returns the length of the queue backlog
func (sq *simpleQueue) Len() int {
return len(sq.eventChannel)
}
// Next returns the next available event from the front of the queue
func (sq *simpleQueue) Next() Event {
event := <-sq.eventChannel
return event
}
// Shutdown closes our eventChannel
func (sq *simpleQueue) Shutdown() {
sq.lock.Lock()
sq.closed = true
close(sq.eventChannel)
sq.lock.Unlock()
}
// Shutdown closes our eventChannel
func (sq *simpleQueue) Publish(event Event) {
sq.lock.Lock()
if !sq.closed {
sq.inChan() <- event
}
sq.lock.Unlock()
}
func (iq *queue) inChan() chan<- Event {
return iq.infChan.In()
}
@ -84,7 +35,7 @@ func (iq *queue) OutChan() <-chan Event {
return iq.infChan.Out()
}
// Out returns the next available event from the front of the queue
// Next returns the next available event from the front of the queue
func (iq *queue) Next() Event {
event := <-iq.infChan.Out()
return event
@ -97,8 +48,10 @@ func (iq *queue) Len() int {
// Shutdown closes our eventChannel
func (iq *queue) Shutdown() {
iq.lock.Lock()
iq.closed = true
iq.infChan.Close()
if !iq.closed {
iq.closed = true
iq.infChan.Close()
}
iq.lock.Unlock()
}

View File

@ -22,7 +22,7 @@ type Event struct {
}
// GetRandNumber is a helper function which returns a random integer, this is
// currently mostly used to generate messageids
// currently mostly used to generate message IDs
func GetRandNumber() *big.Int {
num, err := rand.Int(rand.Reader, big.NewInt(math.MaxUint32))
// If we can't generate random numbers then panicking is probably
@ -46,6 +46,8 @@ func NewEventList(eventType Type, args ...interface{}) Event {
val, vok := args[i+1].(string)
if kok && vok {
data[key] = val
} else {
log.Errorf("attempted to send a field that could not be parsed to a string: %v %v", args[i], args[i+1])
}
}
return Event{EventType: eventType, EventID: GetRandNumber().String(), Data: data}
@ -56,17 +58,16 @@ type manager struct {
subscribers map[Type][]Queue
events chan []byte
mapMutex sync.Mutex
chanMutex sync.Mutex
internal chan bool
closed bool
trace bool
}
// Manager is an interface for an event bus
// FIXME this interface lends itself to race conditions around channels
type Manager interface {
Subscribe(Type, Queue)
Publish(Event)
PublishLocal(Event)
Shutdown()
}
@ -94,11 +95,18 @@ func (em *manager) initialize() {
func (em *manager) Subscribe(eventType Type, queue Queue) {
em.mapMutex.Lock()
defer em.mapMutex.Unlock()
for _, sub := range em.subscribers[eventType] {
if sub == queue {
return // don't add the same queue for the same event twice...
}
}
em.subscribers[eventType] = append(em.subscribers[eventType], queue)
}
// Publish takes an Event and sends it to the internal eventBus where it is distributed to all Subscribers
func (em *manager) Publish(event Event) {
em.chanMutex.Lock()
defer em.chanMutex.Unlock()
if event.EventType != "" && !em.closed {
// Debug Events for Tracing, locked behind an environment variable
@ -123,17 +131,12 @@ func (em *manager) Publish(event Event) {
}
}
// Publish an event only locally, not going over an IPC bridge if there is one
func (em *manager) PublishLocal(event Event) {
em.Publish(event)
}
// eventBus is an internal function that is used to distribute events to all subscribers
func (em *manager) eventBus() {
for {
eventJSON := <-em.events
// In the case on an empty event. Teardown the Queue
// In the case on an empty event. Tear down the Queue
if len(eventJSON) == 0 {
log.Errorf("Received zero length event")
break
@ -155,7 +158,10 @@ func (em *manager) eventBus() {
for _, subscriber := range subscribers {
// Deep Copy for Each Subscriber
var eventCopy Event
json.Unmarshal(eventJSON, &eventCopy)
err = json.Unmarshal(eventJSON, &eventCopy)
if err != nil {
log.Errorf("error unmarshalling event: %v ", err)
}
subscriber.Publish(eventCopy)
}
}
@ -167,7 +173,9 @@ func (em *manager) eventBus() {
// Shutdown triggers, and waits for, the internal eventBus goroutine to finish
func (em *manager) Shutdown() {
em.events <- []byte{}
em.chanMutex.Lock()
em.closed = true
em.chanMutex.Unlock()
// wait for eventBus to finish
<-em.internal
close(em.events)

View File

@ -2,7 +2,6 @@ package event
import (
"git.openprivacy.ca/openprivacy/log"
"sync"
"testing"
"time"
)
@ -12,12 +11,11 @@ func TestEventManager(t *testing.T) {
eventManager := NewEventManager()
// We need to make this buffer at least 1, otherwise we will log an error!
testChan := make(chan Event, 1)
simpleQueue := &simpleQueue{testChan, sync.Mutex{}, false}
simpleQueue := NewQueue()
eventManager.Subscribe("TEST", simpleQueue)
eventManager.Publish(Event{EventType: "TEST", Data: map[Field]string{"Value": "Hello World"}})
event := <-testChan
event := simpleQueue.Next()
if event.EventType == "TEST" && event.Data["Value"] == "Hello World" {
} else {
@ -27,17 +25,6 @@ func TestEventManager(t *testing.T) {
eventManager.Shutdown()
}
// Most basic Manager Test, Initialize, Subscribe, Publish, Receive
func TestEventManagerOverflow(t *testing.T) {
eventManager := NewEventManager()
// Explicitly setting this to 0 log an error!
testChan := make(chan Event)
simpleQueue := &simpleQueue{testChan, sync.Mutex{}, false}
eventManager.Subscribe("TEST", simpleQueue)
eventManager.Publish(Event{EventType: "TEST"})
}
func TestEventManagerMultiple(t *testing.T) {
log.SetLevel(log.LevelDebug)
eventManager := NewEventManager()
@ -56,7 +43,7 @@ func TestEventManagerMultiple(t *testing.T) {
eventManager.Publish(Event{EventType: "GroupEvent", Data: map[Field]string{"Value": "Hello World Group"}})
eventManager.Publish(Event{EventType: "PeerEvent", Data: map[Field]string{"Value": "Hello World Peer"}})
eventManager.Publish(Event{EventType: "ErrorEvent", Data: map[Field]string{"Value": "Hello World Error"}})
eventManager.Publish(Event{EventType: "NobodyIsSubscribedToThisEvent", Data: map[Field]string{"Value": "Noone should see this!"}})
eventManager.Publish(Event{EventType: "NobodyIsSubscribedToThisEvent", Data: map[Field]string{"Value": "No one should see this!"}})
assertLength := func(len int, expected int, label string) {
if len != expected {

View File

@ -1,38 +0,0 @@
package event
type ipcManager struct {
manager Manager
onion string
ipcBridge IPCBridge
}
// NewIPCEventManager returns an EvenetManager that also pipes events over and supplied IPCBridge
func NewIPCEventManager(bridge IPCBridge, onion string) Manager {
em := &ipcManager{onion: onion, ipcBridge: bridge, manager: NewEventManager()}
return em
}
// IPCEventManagerFrom returns an IPCEventManger from the supplied manager and IPCBridge
func IPCEventManagerFrom(bridge IPCBridge, onion string, manager Manager) Manager {
em := &ipcManager{onion: onion, ipcBridge: bridge, manager: manager}
return em
}
func (ipcm *ipcManager) Publish(ev Event) {
ipcm.manager.Publish(ev)
message := &IPCMessage{Dest: ipcm.onion, Message: ev}
ipcm.ipcBridge.Write(message)
}
func (ipcm *ipcManager) PublishLocal(ev Event) {
ipcm.manager.Publish(ev)
}
func (ipcm *ipcManager) Subscribe(eventType Type, queue Queue) {
ipcm.manager.Subscribe(eventType, queue)
}
func (ipcm *ipcManager) Shutdown() {
ipcm.manager.Shutdown()
}

View File

@ -1,3 +1,4 @@
// nolint:nilaway - the infiniteBuffer function causes issues with static analysis because it is very unidomatic.
package event
/*
@ -19,7 +20,7 @@ func newInfiniteChannel() *infiniteChannel {
input: make(chan Event),
output: make(chan Event),
length: make(chan int),
buffer: newInfinitQueue(),
buffer: newInfiniteQueue(),
}
go ch.infiniteBuffer()
return ch

View File

@ -24,7 +24,7 @@ type infiniteQueue struct {
}
// New constructs and returns a new Queue.
func newInfinitQueue() *infiniteQueue {
func newInfiniteQueue() *infiniteQueue {
return &infiniteQueue{
buf: make([]Event, minQueueLen),
}

View File

@ -1,14 +0,0 @@
package event
// IPCMessage is a wrapper for a regular eventMessage with a destination (onion|AppDest) so the other side of the bridge can route appropriately
type IPCMessage struct {
Dest string
Message Event
}
// IPCBridge is an interface to a IPC construct used to communicate IPCMessages
type IPCBridge interface {
Read() (*IPCMessage, bool)
Write(message *IPCMessage)
Shutdown()
}

128
extensions/profile_value.go Normal file
View File

@ -0,0 +1,128 @@
package extensions
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/model/constants"
"cwtch.im/cwtch/peer"
"cwtch.im/cwtch/protocol/connections"
"cwtch.im/cwtch/settings"
"git.openprivacy.ca/openprivacy/log"
"strconv"
)
// ProfileValueExtension implements custom Profile Names over Cwtch
type ProfileValueExtension struct {
}
func (pne ProfileValueExtension) NotifySettingsUpdate(_ settings.GlobalSettings) {
}
func (pne ProfileValueExtension) EventsToRegister() []event.Type {
return []event.Type{event.PeerStateChange, event.Heartbeat}
}
func (pne ProfileValueExtension) ExperimentsToRegister() []string {
return nil
}
func (pne ProfileValueExtension) requestProfileInfo(profile peer.CwtchPeer, ci *model.Conversation) {
profile.SendScopedZonedGetValToContact(ci.ID, attr.PublicScope, attr.ProfileZone, constants.Name)
profile.SendScopedZonedGetValToContact(ci.ID, attr.PublicScope, attr.ProfileZone, constants.ProfileStatus)
profile.SendScopedZonedGetValToContact(ci.ID, attr.PublicScope, attr.ProfileZone, constants.ProfileAttribute1)
profile.SendScopedZonedGetValToContact(ci.ID, attr.PublicScope, attr.ProfileZone, constants.ProfileAttribute2)
profile.SendScopedZonedGetValToContact(ci.ID, attr.PublicScope, attr.ProfileZone, constants.ProfileAttribute3)
}
func (pne ProfileValueExtension) OnEvent(ev event.Event, profile peer.CwtchPeer) {
switch ev.EventType {
case event.Heartbeat:
// once every heartbeat, loop through conversations and, if they are online, request an update to any long info..
conversations, err := profile.FetchConversations()
if err == nil {
for _, ci := range conversations {
if profile.GetPeerState(ci.Handle) == connections.AUTHENTICATED {
pne.requestProfileInfo(profile, ci)
}
}
}
case event.PeerStateChange:
ci, err := profile.FetchConversationInfo(ev.Data["RemotePeer"])
if err == nil {
// if we have re-authenticated with thie peer then request their profile image...
if connections.ConnectionStateToType()[ev.Data[event.ConnectionState]] == connections.AUTHENTICATED {
// Request some profile information...
pne.requestProfileInfo(profile, ci)
}
}
}
}
// OnContactReceiveValue for ProfileValueExtension handles saving specific Public Profile Values like Profile Name
func (pne ProfileValueExtension) OnContactReceiveValue(profile peer.CwtchPeer, conversation model.Conversation, szp attr.ScopedZonedPath, value string, exists bool) {
// Allow public profile parameters to be added as contact specific attributes...
scope, zone, _ := szp.GetScopeZonePath()
if exists && scope.IsPublic() && zone == attr.ProfileZone {
// Check the current value of the attribute
currentValue, err := profile.GetConversationAttribute(conversation.ID, szp)
if err == nil && currentValue == value {
// Value exists and the value is the same, short-circuit
return
}
// Save the new Attribute
err = profile.SetConversationAttribute(conversation.ID, szp, value)
if err != nil {
// Something else wen't wrong.. short-circuit
log.Errorf("error setting conversation attribute %v", err)
return
}
// Finally publish an update for listeners to react to.
scope, zone, zpath := szp.GetScopeZonePath()
profile.PublishEvent(event.NewEvent(event.UpdatedConversationAttribute, map[event.Field]string{
event.Scope: string(scope),
event.Path: string(zone.ConstructZonedPath(zpath)),
event.Data: value,
event.RemotePeer: conversation.Handle,
event.ConversationID: strconv.Itoa(conversation.ID),
}))
}
}
// OnContactRequestValue for ProfileValueExtension handles returning Public Profile Values
func (pne ProfileValueExtension) OnContactRequestValue(profile peer.CwtchPeer, conversation model.Conversation, eventID string, szp attr.ScopedZonedPath) {
scope, zone, zpath := szp.GetScopeZonePath()
log.Debugf("Looking up public | conversation scope/zone %v", szp.ToString())
if scope.IsPublic() || scope.IsConversation() {
val, exists := profile.GetScopedZonedAttribute(scope, zone, zpath)
// NOTE: Temporary Override because UI currently wipes names if it can't find them...
if !exists && zone == attr.UnknownZone && zpath == constants.Name {
val, exists = profile.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name)
}
// NOTE: Cwtch 1.15+ requires that profiles be able to restrict file downloading to specific contacts. As such we need an ACL check here
// on the fileshareing zone.
// TODO: Split this functionality into FilesharingFunctionality, and restrict this function to only considering Profile zoned attributes?
if zone == attr.FilesharingZone {
if !conversation.GetPeerAC().ShareFiles {
return
}
}
// Construct a Response
resp := event.NewEvent(event.SendRetValMessageToPeer, map[event.Field]string{event.ConversationID: strconv.Itoa(conversation.ID), event.RemotePeer: conversation.Handle, event.Exists: strconv.FormatBool(exists)})
resp.EventID = eventID
if exists {
resp.Data[event.Data] = val
} else {
resp.Data[event.Data] = ""
}
log.Debugf("Responding with SendRetValMessageToPeer exists:%v data: %v\n", exists, val)
profile.PublishEvent(resp)
}
}

View File

@ -0,0 +1,91 @@
package extensions
import (
"slices"
"strconv"
"time"
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/model/constants"
"cwtch.im/cwtch/peer"
"cwtch.im/cwtch/protocol/connections"
"cwtch.im/cwtch/settings"
"git.openprivacy.ca/openprivacy/log"
)
// SendWhenOnlineExtension implements automatic sending
// Some Considerations:
// - There are race conditions inherant in this approach e.g. a peer could go offline just after recieving a message and never sending an ack
// - In that case the next time we connect we will send a duplicate message.
// - Currently we do not include metadata like sent time in raw peer protocols (however Overlay does now have support for that information)
type SendWhenOnlineExtension struct {
}
func (soe SendWhenOnlineExtension) NotifySettingsUpdate(_ settings.GlobalSettings) {
}
func (soe SendWhenOnlineExtension) EventsToRegister() []event.Type {
return []event.Type{event.PeerStateChange}
}
func (soe SendWhenOnlineExtension) ExperimentsToRegister() []string {
return nil
}
func (soe SendWhenOnlineExtension) OnEvent(ev event.Event, profile peer.CwtchPeer) {
switch ev.EventType {
case event.PeerStateChange:
ci, err := profile.FetchConversationInfo(ev.Data["RemotePeer"])
if err == nil {
// if we have re-authenticated with thie peer then request their profile image...
if connections.ConnectionStateToType()[ev.Data[event.ConnectionState]] == connections.AUTHENTICATED {
log.Infof("Sending Offline Messages to %s", ci.Handle)
// Check the last 100 messages, if any of them are pending, then send them now...
messsages, _ := profile.GetMostRecentMessages(ci.ID, constants.CHANNEL_CHAT, 0, uint(100))
slices.Reverse(messsages)
for _, message := range messsages {
if message.Attr[constants.AttrAck] == constants.False {
sent, timeparseerr := time.Parse(time.RFC3339, message.Attr[constants.AttrSentTimestamp])
if timeparseerr != nil {
continue
}
if time.Since(sent) > time.Hour*24*7 {
continue
}
body := message.Body
ev := event.NewEvent(event.SendMessageToPeer, map[event.Field]string{event.ConversationID: strconv.Itoa(ci.ID), event.RemotePeer: ci.Handle, event.Data: body})
ev.EventID = message.Signature // we need this ensure that we correctly ack this in the db when it comes back
// TODO: The EventBus is becoming very noisy...we may want to consider a one-way shortcut to Engine i.e. profile.Engine.SendMessageToPeer
log.Infof("resending message that was sent when peer was offline")
profile.PublishEvent(ev)
}
}
if ci.HasChannel(constants.CHANNEL_MANAGER) {
messsages, _ = profile.GetMostRecentMessages(ci.ID, constants.CHANNEL_MANAGER, 0, uint(100))
slices.Reverse(messsages)
for _, message := range messsages {
if message.Attr[constants.AttrAck] == constants.False {
body := message.Body
ev := event.NewEvent(event.SendMessageToPeer, map[event.Field]string{event.ConversationID: strconv.Itoa(ci.ID), event.RemotePeer: ci.Handle, event.Data: body})
ev.EventID = message.Signature // we need this ensure that we correctly ack this in the db when it comes back
// TODO: The EventBus is becoming very noisy...we may want to consider a one-way shortcut to Engine i.e. profile.Engine.SendMessageToPeer
log.Debugf("resending message that was sent when peer was offline")
profile.PublishEvent(ev)
}
}
}
}
}
}
}
// OnContactReceiveValue is nop for SendWhenOnnlineExtension
func (soe SendWhenOnlineExtension) OnContactReceiveValue(profile peer.CwtchPeer, conversation model.Conversation, szp attr.ScopedZonedPath, value string, exists bool) {
}
// OnContactRequestValue is nop for SendWhenOnnlineExtension
func (soe SendWhenOnlineExtension) OnContactRequestValue(profile peer.CwtchPeer, conversation model.Conversation, eventID string, szp attr.ScopedZonedPath) {
}

View File

@ -2,31 +2,157 @@ package filesharing
import (
"crypto/rand"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/peer"
"cwtch.im/cwtch/protocol/files"
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/settings"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"git.openprivacy.ca/openprivacy/log"
"io"
"math"
"os"
path "path/filepath"
"regexp"
"runtime"
"strconv"
"strings"
"time"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/model/constants"
"cwtch.im/cwtch/peer"
"cwtch.im/cwtch/protocol/files"
"git.openprivacy.ca/openprivacy/log"
)
// Functionality groups some common UI triggered functions for contacts...
type Functionality struct {
}
// FunctionalityGate returns contact.Functionality always
func FunctionalityGate(experimentMap map[string]bool) (*Functionality, error) {
if experimentMap["filesharing"] == true {
func (f *Functionality) NotifySettingsUpdate(settings settings.GlobalSettings) {
}
func (f *Functionality) EventsToRegister() []event.Type {
return []event.Type{event.ProtocolEngineCreated, event.ManifestReceived, event.FileDownloaded}
}
func (f *Functionality) ExperimentsToRegister() []string {
return []string{constants.FileSharingExperiment}
}
// OnEvent handles File Sharing Hooks like Manifest Received and FileDownloaded
func (f *Functionality) OnEvent(ev event.Event, profile peer.CwtchPeer) {
if profile.IsFeatureEnabled(constants.FileSharingExperiment) {
switch ev.EventType {
case event.ProtocolEngineCreated:
f.ReShareFiles(profile)
case event.ManifestReceived:
log.Debugf("Manifest Received Event!: %v", ev)
handle := ev.Data[event.Handle]
fileKey := ev.Data[event.FileKey]
serializedManifest := ev.Data[event.SerializedManifest]
manifestFilePath, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%v.manifest", fileKey))
if exists {
downloadFilePath, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%v.path", fileKey))
if exists {
log.Debugf("downloading manifest to %v, file to %v", manifestFilePath, downloadFilePath)
var manifest files.Manifest
err := json.Unmarshal([]byte(serializedManifest), &manifest)
if err == nil {
// We only need to check the file size here, as manifest is sent to engine and the file created
// will be bound to the size advertised in manifest.
fileSizeLimitValue, fileSizeLimitExists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%v.limit", fileKey))
if fileSizeLimitExists {
fileSizeLimit, err := strconv.ParseUint(fileSizeLimitValue, 10, 64)
if err == nil {
if manifest.FileSizeInBytes >= fileSizeLimit {
log.Debugf("could not download file, size %v greater than limit %v", manifest.FileSizeInBytes, fileSizeLimitValue)
} else {
manifest.Title = manifest.FileName
manifest.FileName = downloadFilePath
log.Debugf("saving manifest")
err = manifest.Save(manifestFilePath)
if err != nil {
log.Errorf("could not save manifest: %v", err)
} else {
tempFile := ""
if runtime.GOOS == "android" {
tempFile = manifestFilePath[0 : len(manifestFilePath)-len(".manifest")]
log.Debugf("derived android temp path: %v", tempFile)
}
profile.PublishEvent(event.NewEvent(event.ManifestSaved, map[event.Field]string{
event.FileKey: fileKey,
event.Handle: handle,
event.SerializedManifest: string(manifest.Serialize()),
event.TempFile: tempFile,
event.NameSuggestion: manifest.Title,
}))
}
}
} else {
log.Errorf("error saving manifest: file size limit is incorrect: %v", err)
}
} else {
log.Errorf("error saving manifest: could not find file size limit info")
}
} else {
log.Errorf("error saving manifest: %v", err)
}
} else {
log.Errorf("found manifest path but not download path for %v", fileKey)
}
} else {
log.Errorf("no download path found for manifest: %v", fileKey)
}
case event.FileDownloaded:
fileKey := ev.Data[event.FileKey]
profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.complete", fileKey), "true")
}
} else {
log.Errorf("profile called filesharing experiment OnContactReceiveValue even though file sharing was not enabled. This is likely a programming error.")
}
}
func (f *Functionality) OnContactRequestValue(profile peer.CwtchPeer, conversation model.Conversation, eventID string, path attr.ScopedZonedPath) {
// nop
}
func (f *Functionality) OnContactReceiveValue(profile peer.CwtchPeer, conversation model.Conversation, path attr.ScopedZonedPath, value string, exists bool) {
// Profile should not call us if FileSharing is disabled
if profile.IsFeatureEnabled(constants.FileSharingExperiment) {
scope, zone, zpath := path.GetScopeZonePath()
log.Debugf("file sharing contact receive value")
if exists && scope.IsConversation() && zone == attr.FilesharingZone && strings.HasSuffix(zpath, ".manifest.size") {
fileKey := strings.Replace(zpath, ".manifest.size", "", 1)
size, err := strconv.Atoi(value)
// if size is valid and below the maximum size for a manifest
// this is to prevent malicious sharers from using large amounts of memory when distributing
// a manifest as we reconstruct this in-memory
if err == nil && size < files.MaxManifestSize {
profile.PublishEvent(event.NewEvent(event.ManifestSizeReceived, map[event.Field]string{event.FileKey: fileKey, event.ManifestSize: value, event.Handle: conversation.Handle}))
} else {
profile.PublishEvent(event.NewEvent(event.ManifestError, map[event.Field]string{event.FileKey: fileKey, event.Handle: conversation.Handle}))
}
}
} else {
log.Errorf("profile called filesharing experiment OnContactReceiveValue even though file sharing was not enabled. This is likely a programming error.")
}
}
// FunctionalityGate returns filesharing functionality - gates now happen on function calls.
func FunctionalityGate() *Functionality {
return new(Functionality)
}
// PreviewFunctionalityGate returns filesharing if image previews are enabled
func PreviewFunctionalityGate(experimentMap map[string]bool) (*Functionality, error) {
if experimentMap[constants.FileSharingExperiment] && experimentMap[constants.ImagePreviewsExperiment] {
return new(Functionality), nil
}
return nil, errors.New("filesharing is not enabled")
return nil, errors.New("image previews are not enabled")
}
// OverlayMessage presents the canonical format of the File Sharing functionality Overlay Message
@ -38,32 +164,278 @@ type OverlayMessage struct {
Size uint64 `json:"s"`
}
// FileKey is the unique reference to a file offer
func (om *OverlayMessage) FileKey() string {
return fmt.Sprintf("%s.%s", om.Hash, om.Nonce)
}
// ShouldAutoDL checks file size and file name. *DOES NOT* check user settings or contact state
func (om *OverlayMessage) ShouldAutoDL() bool {
if om.Size > constants.ImagePreviewMaxSizeInBytes {
return false
}
lname := strings.ToLower(om.Name)
for _, s := range constants.AutoDLFileExts {
if strings.HasSuffix(lname, s) {
return true
}
}
return false
}
func (f *Functionality) VerifyOrResumeDownloadDefaultLimit(profile peer.CwtchPeer, conversation int, fileKey string) error {
return f.VerifyOrResumeDownload(profile, conversation, fileKey, files.MaxManifestSize*files.DefaultChunkSize)
}
func (f *Functionality) VerifyOrResumeDownload(profile peer.CwtchPeer, conversation int, fileKey string, size uint64) error {
if manifestFilePath, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest", fileKey)); exists {
if downloadfilepath, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.path", fileKey)); exists {
manifest, err := files.LoadManifest(manifestFilePath)
if err == nil {
// Assert the filename...this is technically not necessary, but is here for completeness
manifest.FileName = downloadfilepath
if manifest.VerifyFile() == nil {
// Send a FileDownloaded Event. Usually when VerifyOrResumeDownload is triggered it's because some UI is awaiting the results of a
// Download.
profile.PublishEvent(event.NewEvent(event.FileDownloaded, map[event.Field]string{event.FileKey: fileKey, event.FilePath: downloadfilepath, event.TempFile: downloadfilepath}))
// File is verified and there is nothing else to do...
return nil
} else {
// Kick off another Download...
return f.DownloadFile(profile, conversation, downloadfilepath, manifestFilePath, fileKey, size)
}
}
}
}
return errors.New("file download metadata does not exist, or is corrupted")
}
func (f *Functionality) CheckDownloadStatus(profile peer.CwtchPeer, fileKey string) error {
path, _ := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.path", fileKey))
if value, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.complete", fileKey)); exists && value == event.True {
profile.PublishEvent(event.NewEvent(event.FileDownloaded, map[event.Field]string{
event.ProfileOnion: profile.GetOnion(),
event.FileKey: fileKey,
event.FilePath: path,
event.TempFile: "",
}))
} else {
log.Debugf("CheckDownloadStatus found .path but not .complete")
profile.PublishEvent(event.NewEvent(event.FileDownloadProgressUpdate, map[event.Field]string{
event.ProfileOnion: profile.GetOnion(),
event.FileKey: fileKey,
event.Progress: "-1",
event.FileSizeInChunks: "-1",
event.FilePath: path,
}))
}
return nil // cannot fail
}
func (f *Functionality) EnhancedShareFile(profile peer.CwtchPeer, conversationID int, sharefilepath string) string {
fileKey, overlay, err := f.ShareFile(sharefilepath, profile)
if err != nil {
log.Errorf("error sharing file: %v", err)
} else if conversationID == -1 {
// FIXME: At some point we might want to allow arbitrary public files, but for now this API will assume
// there is only one, and it is the custom profile image...
profile.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.CustomProfileImageKey, fileKey)
} else {
// Set a new attribute so we can associate this download with this conversation...
profile.SetConversationAttribute(conversationID, attr.ConversationScope.ConstructScopedZonedPath(attr.FilesharingZone.ConstructZonedPath(fileKey)), "")
id, err := profile.SendMessage(conversationID, overlay)
if err == nil {
return profile.EnhancedGetMessageById(conversationID, id)
}
}
return ""
}
// DownloadFileDefaultLimit given a profile, a conversation handle and a file sharing key, start off a download process
// to downloadFilePath with a default filesize limit
func (f *Functionality) DownloadFileDefaultLimit(profile peer.CwtchPeer, conversationID int, downloadFilePath string, manifestFilePath string, key string) error {
return f.DownloadFile(profile, conversationID, downloadFilePath, manifestFilePath, key, files.MaxManifestSize*files.DefaultChunkSize)
}
// DownloadFile given a profile, a conversation handle and a file sharing key, start off a download process
// to downloadFilePath
func (f *Functionality) DownloadFile(profile peer.CwtchPeer, handle string, downloadFilePath string, manifestFilePath string, key string) {
func (f *Functionality) DownloadFile(profile peer.CwtchPeer, conversationID int, downloadFilePath string, manifestFilePath string, key string, limit uint64) error {
// assert that we are allowed to download the file
if !profile.IsFeatureEnabled(constants.FileSharingExperiment) {
return errors.New("filesharing functionality is not enabled")
}
// Don't download files if the download or manifest path is not set
if downloadFilePath == "" || manifestFilePath == "" {
return errors.New("download path or manifest path is empty")
}
// Don't download files if the download file directory does not exist
// Unless we are on Android where the kernel wishes to keep us ignorant of the
// actual path and/or existence of the file. We handle this case further down
// the line when the manifest is received and protocol engine and the Android layer
// negotiate a temporary local file -> final file copy. We don't want to worry
// about that here...
if runtime.GOOS != "android" {
if _, err := os.Stat(path.Dir(downloadFilePath)); os.IsNotExist(err) {
return errors.New("download directory does not exist")
}
// Don't download files if the manifest file directory does not exist
if _, err := os.Stat(path.Dir(manifestFilePath)); os.IsNotExist(err) {
return errors.New("manifest directory does not exist")
}
}
// Store local.filesharing.filekey.manifest as the location of the manifest
profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest", key), manifestFilePath)
// Store local.filesharing.filekey as the location of the download
profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, key, downloadFilePath)
// Store local.filesharing.filekey.path as the location of the download
profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.path", key), downloadFilePath)
// Store local.filesharing.filekey.limit as the max file size of the download
profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.limit", key), strconv.FormatUint(limit, 10))
// Get the value of conversation.filesharing.filekey.manifest.size from `handle`
profile.SendScopedZonedGetValToContact(handle, attr.ConversationScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest.size", key))
profile.SendScopedZonedGetValToContact(conversationID, attr.ConversationScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest.size", key))
return nil
}
// startFileShare is a private method used to finalize a file share and publish it to the protocol engine for processing.
// if force is set to true, this function will ignore timestamp checks...
func (f *Functionality) startFileShare(profile peer.CwtchPeer, filekey string, manifest string, force bool) error {
tsStr, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.ts", filekey))
if exists && !force {
ts, err := strconv.ParseInt(tsStr, 10, 64)
if err != nil || ts < time.Now().Unix()-2592000 {
log.Errorf("ignoring request to download a file offered more than 30 days ago")
return err
}
}
// set the filekey status to active
profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.active", filekey), constants.True)
// reset the timestamp...
profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.ts", filekey), strconv.FormatInt(time.Now().Unix(), 10))
// share the manifest
profile.PublishEvent(event.NewEvent(event.ShareManifest, map[event.Field]string{event.FileKey: filekey, event.SerializedManifest: manifest}))
return nil
}
// RestartFileShare takes in an existing filekey and, assuming the manifest exists, restarts sharing of the manifest
// by default this function always forces a file share, even if the file has timed out.
func (f *Functionality) RestartFileShare(profile peer.CwtchPeer, filekey string) error {
return f.restartFileShareAdvanced(profile, filekey, true)
}
// RestartFileShareAdvanced takes in an existing filekey and, assuming the manifest exists, restarts sharing of the manifest in addition
// to a set of parameters
func (f *Functionality) restartFileShareAdvanced(profile peer.CwtchPeer, filekey string, force bool) error {
// assert that we are allowed to restart filesharing
if !profile.IsFeatureEnabled(constants.FileSharingExperiment) {
return errors.New("filesharing functionality is not enabled")
}
// check that a manifest exists
manifest, manifestExists := profile.GetScopedZonedAttribute(attr.ConversationScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest", filekey))
if manifestExists {
// everything is in order, so reshare this file with the engine
log.Debugf("restarting file share: %v", filekey)
return f.startFileShare(profile, filekey, manifest, force)
}
return fmt.Errorf("manifest does not exist for filekey: %v", filekey)
}
// ReShareFiles given a profile we iterate through all existing fileshares and re-share them
// if the time limit has not expired
func (f *Functionality) ReShareFiles(profile peer.CwtchPeer) error {
// assert that we are allowed to restart filesharing
if !profile.IsFeatureEnabled(constants.FileSharingExperiment) {
return errors.New("filesharing functionality is not enabled")
}
keys, err := profile.GetScopedZonedAttributeKeys(attr.LocalScope, attr.FilesharingZone)
if err != nil {
return err
}
for _, key := range keys {
// only look at timestamp keys
// this is an arbitrary choice
if strings.HasSuffix(key, ".ts") {
_, zonedpath := attr.ParseScope(key)
_, keypath := attr.ParseZone(zonedpath)
keyparts := strings.Split(keypath, ".")
// assert that the key is well-formed
if len(keyparts) == 3 && keyparts[2] == "ts" {
// fetch the timestamp key
filekey := strings.Join(keyparts[:2], ".")
sharedFile, err := f.GetFileShareInfo(profile, filekey)
// If we haven't explicitly stopped sharing the file then attempt a reshare
if err == nil && sharedFile.Active {
// this reshare can fail because we don't force sharing of files older than 30 days...
err := f.restartFileShareAdvanced(profile, filekey, false)
if err != nil {
log.Debugf("could not reshare file: %v", err)
}
} else {
log.Debugf("could not get fileshare info %v", err)
}
}
}
}
return nil
}
// GetFileShareInfo returns information related to a known fileshare.
// An error is returned if the data is incomplete
func (f *Functionality) GetFileShareInfo(profile peer.CwtchPeer, filekey string) (*SharedFile, error) {
timestampString, tsExists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.ts", filekey))
pathString, pathExists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.path", filekey))
activeString, activeExists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.active", filekey))
if tsExists && pathExists && activeExists {
timestamp, err := strconv.Atoi(timestampString)
if err == nil {
dateShared := time.Unix(int64(timestamp), 0)
expired := time.Since(dateShared) >= time.Hour*24*30
return &SharedFile{
FileKey: filekey,
Path: pathString,
DateShared: dateShared,
Active: !expired && activeString == constants.True,
Expired: expired,
}, nil
}
}
return nil, fmt.Errorf("nonexistant or malformed fileshare %v", filekey)
}
// ShareFile given a profile and a conversation handle, sets up a file sharing process to share the file
// at filepath
func (f *Functionality) ShareFile(filepath string, profile peer.CwtchPeer, handle string) error {
func (f *Functionality) ShareFile(filepath string, profile peer.CwtchPeer) (string, string, error) {
// assert that we are allowed to share files
if !profile.IsFeatureEnabled(constants.FileSharingExperiment) {
return "", "", errors.New("filesharing functionality is not enabled")
}
manifest, err := files.CreateManifest(filepath)
if err != nil {
return err
return "", "", err
}
var nonce [24]byte
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
log.Errorf("Cannot read from random: %v\n", err)
return err
return "", "", err
}
message := OverlayMessage{
@ -89,11 +461,131 @@ func (f *Functionality) ShareFile(filepath string, profile peer.CwtchPeer, handl
// manifest.FileName gets redacted in filesharing_subsystem (to remove the system-specific file hierarchy),
// but we need to *store* the full path because the sender also uses it to locate the file
lenDiff := len(filepath) - len(path.Base(filepath))
// the sender needs to know the location of the file so they can display it in a preview...
// This eventually becomes a message attribute, but we don't have access to the message identifier until
// the message gets sent.
// In the worst case, this can be obtained using CheckDownloadStatus (though in practice this lookup will be
// rare because the UI will almost always initiate the construction of a preview a file directly after sending it).
profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.path", key), filepath)
// Store the timestamp, manifest and manifest size for later.
profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.ts", key), strconv.FormatInt(time.Now().Unix(), 10))
profile.SetScopedZonedAttribute(attr.ConversationScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest", key), string(serializedManifest))
profile.SetScopedZonedAttribute(attr.ConversationScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest.size", key), strconv.Itoa(int(math.Ceil(float64(len(serializedManifest)-lenDiff)/float64(files.DefaultChunkSize)))))
profile.ShareFile(key, string(serializedManifest))
err = f.startFileShare(profile, key, string(serializedManifest), false)
profile.SendMessage(handle, string(wrapperJSON))
return nil
return key, string(wrapperJSON), err
}
// SharedFile encapsulates information about a shared file
// including the file key, file path, the original share date and the
// current sharing status
type SharedFile struct {
// The roothash.nonce identifier derived for this file share
FileKey string
// Path is the OS specific location of the file
Path string
// DateShared is the original datetime the file was shared
DateShared time.Time
// Active is true if the file is currently being shared, false otherwise
Active bool
// Expired is true if the file is not eligible to be shared (because e.g. it has been too long since the file was originally shared,
// or the file no longer exists).
Expired bool
}
func (f *Functionality) EnhancedGetSharedFiles(profile peer.CwtchPeer, conversationID int) string {
data, err := json.Marshal(f.GetSharedFiles(profile, conversationID))
if err == nil {
return string(data)
}
return ""
}
// GetSharedFiles returns all file shares associated with a given conversation
func (f *Functionality) GetSharedFiles(profile peer.CwtchPeer, conversationID int) []SharedFile {
var sharedFiles []SharedFile
ci, err := profile.GetConversationInfo(conversationID)
if err == nil {
for k := range ci.Attributes {
// when we share a file with a conversation we set a single attribute conversation.filesharing.<filekey>
if strings.HasPrefix(k, "conversation.filesharing") {
parts := strings.SplitN(k, ".", 3)
if len(parts) == 3 {
key := parts[2]
sharedFile, err := f.GetFileShareInfo(profile, key)
if err == nil {
sharedFiles = append(sharedFiles, *sharedFile)
}
}
}
}
}
return sharedFiles
}
// GenerateDownloadPath creates a file path that doesn't currently exist on the filesystem
func GenerateDownloadPath(basePath, fileName string, overwrite bool) (filePath, manifestPath string) {
// avoid all kina funky shit
re := regexp.MustCompile(`[^A-Za-z0-9._-]`)
filePath = re.ReplaceAllString(filePath, "")
// avoid hidden files on linux
for strings.HasPrefix(filePath, ".") {
filePath = strings.TrimPrefix(filePath, ".")
}
// avoid empties
if strings.TrimSpace(filePath) == "" {
filePath = "untitled"
}
// if you like it, put a / on it
if !strings.HasSuffix(basePath, string(os.PathSeparator)) {
basePath = fmt.Sprintf("%s%s", basePath, string(os.PathSeparator))
}
filePath = fmt.Sprintf("%s%s", basePath, fileName)
manifestPath = fmt.Sprintf("%s.manifest", filePath)
// if file is named "file", iterate "file", "file (2)", "file (3)", ... until DNE
// if file is named "file.ext", iterate "file.ext", "file (2).ext", "file (3).ext", ... until DNE
parts := strings.Split(fileName, ".")
fileNameBase := parts[0]
fileNameExt := ""
if len(parts) > 1 {
fileNameBase = strings.Join(parts[0:len(parts)-1], ".")
fileNameExt = fmt.Sprintf(".%s", parts[len(parts)-1])
}
if !overwrite {
for i := 2; ; i++ {
if _, err := os.Open(filePath); os.IsNotExist(err) {
if _, err := os.Open(manifestPath); os.IsNotExist(err) {
return
}
}
filePath = fmt.Sprintf("%s%s (%d)%s", basePath, fileNameBase, i, fileNameExt)
manifestPath = fmt.Sprintf("%s.manifest", filePath)
}
}
return
}
// StopFileShare sends a message to the ProtocolEngine to cease sharing a particular file
func (f *Functionality) StopFileShare(profile peer.CwtchPeer, fileKey string) error {
// Note we do not do a permissions check here, as we are *always* permitted to stop sharing files.
// set the filekey status to inactive
profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.active", fileKey), constants.False)
profile.PublishEvent(event.NewEvent(event.StopFileShare, map[event.Field]string{event.FileKey: fileKey}))
return nil // cannot fail
}
// StopAllFileShares sends a message to the ProtocolEngine to cease sharing all files
func (f *Functionality) StopAllFileShares(profile peer.CwtchPeer) {
// Note we do not do a permissions check here, as we are *always* permitted to stop sharing files.
profile.PublishEvent(event.NewEvent(event.StopAllFileShares, map[event.Field]string{}))
}

View File

@ -0,0 +1,181 @@
package filesharing
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/model/constants"
"cwtch.im/cwtch/peer"
"cwtch.im/cwtch/protocol/connections"
"cwtch.im/cwtch/settings"
"encoding/json"
"fmt"
"git.openprivacy.ca/openprivacy/log"
"os"
"strconv"
"time"
)
type ImagePreviewsFunctionality struct {
downloadFolder string
}
func (i *ImagePreviewsFunctionality) NotifySettingsUpdate(settings settings.GlobalSettings) {
i.downloadFolder = settings.DownloadPath
}
func (i *ImagePreviewsFunctionality) EventsToRegister() []event.Type {
return []event.Type{event.ProtocolEngineCreated, event.NewMessageFromPeer, event.NewMessageFromGroup, event.PeerStateChange, event.Heartbeat}
}
func (i *ImagePreviewsFunctionality) ExperimentsToRegister() []string {
return []string{constants.FileSharingExperiment, constants.ImagePreviewsExperiment}
}
func (i *ImagePreviewsFunctionality) OnEvent(ev event.Event, profile peer.CwtchPeer) {
if profile.IsFeatureEnabled(constants.FileSharingExperiment) && profile.IsFeatureEnabled(constants.ImagePreviewsExperiment) {
switch ev.EventType {
case event.NewMessageFromPeer:
ci, err := profile.FetchConversationInfo(ev.Data["RemotePeer"])
if err == nil {
if ci.GetPeerAC().RenderImages {
i.handleImagePreviews(profile, &ev, ci.ID, ci.ID)
}
}
case event.NewMessageFromGroup:
ci, err := profile.FetchConversationInfo(ev.Data["RemotePeer"])
if err == nil {
if ci.GetPeerAC().RenderImages {
i.handleImagePreviews(profile, &ev, ci.ID, ci.ID)
}
}
case event.PeerStateChange:
ci, err := profile.FetchConversationInfo(ev.Data["RemotePeer"])
if err == nil {
// if we have re-authenticated with this peer then request their profile image...
if connections.ConnectionStateToType()[ev.Data[event.ConnectionState]] == connections.AUTHENTICATED {
profile.SendScopedZonedGetValToContact(ci.ID, attr.PublicScope, attr.ProfileZone, constants.CustomProfileImageKey)
}
}
case event.Heartbeat:
conversations, err := profile.FetchConversations()
if err == nil {
for _, ci := range conversations {
if profile.GetPeerState(ci.Handle) == connections.AUTHENTICATED {
// if we have enabled file shares for this contact, then send them our profile image
// NOTE: In the past, Cwtch treated "profile image" as a public file share. As such, anyone with the file key and who is able
// to authenticate with the profile (i.e. non-blocked peers) can download the file (if the global profile images experiment is enabled)
// To better allow for fine-grained permissions (and to support hybrid group permissions), we want to enable per-conversation file
// sharing permissions. As such, profile images are now only shared with contacts with that permission enabled.
// (i.e. all previous accepted contacts, new accepted contacts, and contacts who have this toggle set explictly)
if ci.GetPeerAC().ShareFiles {
profile.SendScopedZonedGetValToContact(ci.ID, attr.PublicScope, attr.ProfileZone, constants.CustomProfileImageKey)
}
}
}
}
case event.ProtocolEngineCreated:
// Now that the Peer Engine is Activated, Reshare Profile Images
key, exists := profile.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.CustomProfileImageKey)
if exists {
serializedManifest, _ := profile.GetScopedZonedAttribute(attr.ConversationScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest", key))
// reset the share timestamp, currently file shares are hardcoded to expire after 30 days...
// we reset the profile image here so that it is always available.
profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.ts", key), strconv.FormatInt(time.Now().Unix(), 10))
log.Debugf("Custom Profile Image: %v %s", key, serializedManifest)
f := Functionality{}
f.RestartFileShare(profile, key)
}
}
}
}
func (i *ImagePreviewsFunctionality) OnContactRequestValue(profile peer.CwtchPeer, conversation model.Conversation, eventID string, path attr.ScopedZonedPath) {
}
func (i *ImagePreviewsFunctionality) OnContactReceiveValue(profile peer.CwtchPeer, conversation model.Conversation, path attr.ScopedZonedPath, value string, exists bool) {
if profile.IsFeatureEnabled(constants.FileSharingExperiment) && profile.IsFeatureEnabled(constants.ImagePreviewsExperiment) {
_, zone, path := path.GetScopeZonePath()
if exists && zone == attr.ProfileZone && path == constants.CustomProfileImageKey {
// We only download from accepted conversations
if conversation.GetPeerAC().RenderImages {
fileKey := value
basepath := i.downloadFolder
fsf := FunctionalityGate()
// We always overwrite profile image files...
fp, mp := GenerateDownloadPath(basepath, fileKey, true)
// If we have marked this file as complete...
if value, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.complete", fileKey)); exists && value == event.True {
if _, err := os.Stat(fp); err == nil {
// file is marked as completed downloaded and exists...
// Note: this will also resend the FileDownloaded event if successful...
if fsf.VerifyOrResumeDownload(profile, conversation.ID, fileKey, constants.ImagePreviewMaxSizeInBytes) == nil {
return
}
// Otherwise we fall through...
}
// Something went wrong...the file is marked as complete but either doesn't exist, or is corrupted such that we can't continue...
// So mark complete as false...
profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.complete", fileKey), event.False)
}
// If we have reached this point then we need to download the file again...
log.Debugf("Downloading Profile Image %v %v %v", fp, mp, fileKey)
fsf.DownloadFile(profile, conversation.ID, fp, mp, fileKey, constants.ImagePreviewMaxSizeInBytes)
}
}
}
}
// handleImagePreviews checks settings and, if appropriate, auto-downloads any images
func (i *ImagePreviewsFunctionality) handleImagePreviews(profile peer.CwtchPeer, ev *event.Event, conversationID, senderID int) {
if profile.IsFeatureEnabled(constants.FileSharingExperiment) && profile.IsFeatureEnabled(constants.ImagePreviewsExperiment) {
ci, err := profile.GetConversationInfo(senderID)
if err != nil {
log.Errorf("attempted to call handleImagePreviews with unknown conversation: %v", senderID)
return
}
if !ci.GetPeerAC().ShareFiles || !ci.GetPeerAC().RenderImages {
log.Infof("refusing to autodownload files from sender: %v. conversation AC does not permit image rendering", senderID)
return
}
// Short-circuit failures
// Don't auto-download images if the download path does not exist.
if i.downloadFolder == "" {
log.Errorf("download folder %v is not set", i.downloadFolder)
return
}
// Don't auto-download images if the download path does not exist.
if _, err := os.Stat(i.downloadFolder); os.IsNotExist(err) {
log.Errorf("download folder %v does not exist", i.downloadFolder)
return
}
// If file sharing is enabled then reshare all active files...
fsf := FunctionalityGate()
// Now look at the image preview experiment
var cm model.MessageWrapper
err = json.Unmarshal([]byte(ev.Data[event.Data]), &cm)
if err == nil && cm.Overlay == model.OverlayFileSharing {
log.Debugf("Received File Sharing Message")
var fm OverlayMessage
err = json.Unmarshal([]byte(cm.Data), &fm)
if err == nil {
if fm.ShouldAutoDL() {
basepath := i.downloadFolder
fp, mp := GenerateDownloadPath(basepath, fm.Name, false)
log.Debugf("autodownloading file! %v %v %v", basepath, fp, i.downloadFolder)
ev.Data["Auto"] = constants.True
mID, _ := strconv.Atoi(ev.Data["Index"])
profile.UpdateMessageAttribute(conversationID, 0, mID, constants.AttrDownloaded, constants.True)
fsf.DownloadFile(profile, senderID, fp, mp, fm.FileKey(), constants.ImagePreviewMaxSizeInBytes)
}
}
}
}
}

View File

@ -0,0 +1,107 @@
package hybrid
import (
"crypto/ed25519"
"encoding/base32"
"encoding/json"
"fmt"
"strings"
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/attr"
"git.openprivacy.ca/openprivacy/log"
)
const ManagedGroupOpen = "managed-group-open"
type GroupEventType int
const (
MemberGroupIDKey = "member_group_id_key"
MemberMessageIDKey = "member_group_messge_id"
)
const (
AddMember = GroupEventType(0x1000)
RemoveMember = GroupEventType(0x2000)
RotateKey = GroupEventType(0x3000)
NewMessage = GroupEventType(0x4000)
NewClearMessage = GroupEventType(0x5000)
SyncRequest = GroupEventType(0x6000)
)
type ManageGroupEvent struct {
EventType GroupEventType `json:"t"`
Data string `json:"d"` // json encoded data
}
type AddMemberEvent struct {
Handle string `json:"h"`
}
type RemoveMemberEvent struct {
Handle string `json:"h"`
}
type RotateKeyEvent struct {
Key []byte `json:"k"`
}
type NewMessageEvent struct {
EncryptedHybridGroupMessage []byte `json:"m"`
}
type NewClearMessageEvent struct {
HybridGroupMessage HybridGroupMessage `json:"m"`
}
type SyncRequestMessage struct {
// a map of MemberGroupID: MemberMessageID
LastSeen map[int]int `json:"l"`
}
// This file contains code for the Hybrid Group / Managed Group types..
type HybridGroupMessage struct {
Author string `json:"a"` // the authors cwtch address
MemberGroupID uint32 `json:"g"`
MemberMessageID uint32 `json:"m"`
MessageBody string `json:"b"`
Sent uint64 `json:"t"` // milliseconds since epoch
Signature []byte `json:"s"` // of json-encoded content (including empty sig)
}
// AuthenticateMessage returns true if the Author of the message produced the Signature over the message
func AuthenticateMessage(message HybridGroupMessage) bool {
messageCopy := message
messageCopy.Signature = []byte{}
// Otherwise we derive the public key from the sender and check it against that.
decodedPub, err := base32.StdEncoding.DecodeString(strings.ToUpper(message.Author))
if err == nil {
data, err := json.Marshal(messageCopy)
if err == nil && len(decodedPub) >= 32 {
return ed25519.Verify(decodedPub[:32], data, message.Signature)
}
}
log.Errorf("invalid signature on message from %s", message)
return false
}
func CheckACL(handle string, group *model.Conversation) (*model.AccessControl, error) {
if isOpen, exists := group.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath(ManagedGroupOpen)).ToString()]; !exists {
return nil, fmt.Errorf("group has not been setup correctly - ManagedGroupOpen does not exist ")
} else if isOpen == event.True {
// We don't need to do a membership check
defaultACL := group.GetPeerAC()
return &defaultACL, nil
}
// If this is a closed group. Check if we have an ACL entry for this member
// If we don't OR that member has been blocked, then close the connection.
if acl, inGroup := group.ACL[handle]; !inGroup || acl.Blocked {
log.Infof("ACL Check Failed: %v %v %v", handle, acl, inGroup)
return nil, fmt.Errorf("peer is not a member of this group")
} else {
return &acl, nil
}
}

View File

@ -0,0 +1,227 @@
// This file contains all code related to how a Group Manager operates over a group.
// Managed groups are canonically controlled by members setting
// the ManageGroup permission in the conversation ACL; allowing the manager to
// take control of how this group is structured, see OnEvent below...
// TODO: This file represents stage 1 of the roll out which de-risks most of the
// integration into cwtch peer, new interfaces, and UI integration
// The following functionality is not yet implemented:
// - group-level encryption
// - key rotation / membership ACL
// Cwtch Hybrid Groups are still very experimental functionality and should
// only be used for testing purposes.
package hybrid
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/model/constants"
"cwtch.im/cwtch/peer"
"cwtch.im/cwtch/protocol/connections"
"cwtch.im/cwtch/settings"
"encoding/json"
"fmt"
"git.openprivacy.ca/openprivacy/log"
)
// MANAGED_GROUP_HANDLE denotes the nominal name that the managed group is given, for easier handling
// Note: we could use id here, as the managed group should technically always be the first group
// But we don't want to assume that, and also allow conversations to be moved around without
// constantly referring to a magic id.
const MANAGED_GROUP_HANDLE = "managed:000"
type GroupManagerFunctionality struct {
}
func (f *GroupManagerFunctionality) NotifySettingsUpdate(settings settings.GlobalSettings) {
}
func (f *GroupManagerFunctionality) EventsToRegister() []event.Type {
return []event.Type{event.PeerStateChange, event.NewMessageFromPeerEngine}
}
func (f *GroupManagerFunctionality) ExperimentsToRegister() []string {
return []string{constants.GroupManagerExperiment, constants.GroupsExperiment}
}
// OnEvent handles File Sharing Hooks like Manifest Received and FileDownloaded
func (f *GroupManagerFunctionality) OnEvent(ev event.Event, profile peer.CwtchPeer) {
// We only want to engage this functionality if the peer is managing a group.
// In that case ALL peer connections and messages need to be routed through
// the management logic
// For now, we assume that a manager is a peer with a special management group.
// In the future we may want to make this a profile-level switch/attribute.
isManager := false
if ci, err := profile.FetchConversationInfo(MANAGED_GROUP_HANDLE); ci != nil && err == nil {
isManager = true
}
if isManager {
switch ev.EventType {
case event.PeerStateChange:
handle := ev.Data["RemotePeer"]
// check that we have authenticated with this peer
if connections.ConnectionStateToType()[ev.Data[event.ConnectionState]] == connections.AUTHENTICATED {
mg, err := f.GetManagedGroup(profile)
if err != nil {
log.Infof("group manager received peer connections but no suitable group has been found: %v %v", handle, err)
profile.DisconnectFromPeer(handle)
break
}
if _, err := CheckACL(handle, mg); err != nil {
log.Infof("received managed group connection from unauthorized peer: %v %v", handle, err)
profile.DisconnectFromPeer(handle)
break
}
}
// This is where most of the magic happens for managed groups. A few notes:
// - CwtchPeer has already taken care of storing this for us, we don't need to worry about that
// - Group Managers **only** speak overlays and **always** wrap their messages in a ManageGroupEvent anything else is fast-rejected.
case event.NewMessageFromPeerEngine:
log.Infof("received new message from peer: manager")
ci, err := f.GetManagedGroup(profile)
if err != nil {
log.Errorf("unknown conversation %v", err)
break // we don't care about unknown conversations...
}
var cm model.MessageWrapper
err = json.Unmarshal([]byte(ev.Data[event.Data]), &cm)
if err != nil {
log.Errorf("could not deserialize json %s %v", ev.Data[event.Data], err)
break
}
// The overlay type of this message **must** be ManageGroupEvent
if cm.Overlay == model.OverlayManageGroupEvent {
var mge ManageGroupEvent
err = json.Unmarshal([]byte(cm.Data), &mge)
if err == nil {
f.handleEvent(profile, *ci, mge, ev.Data[event.Data])
}
}
}
}
}
// handleEvent takes in a high level ManageGroupEvent message, transforms it into the proper type, and passes it on for handling
// assumes we are called after an event provided by an authorized peer (i.e. ManageGroup == true)
func (f *GroupManagerFunctionality) handleEvent(profile peer.CwtchPeer, conversation model.Conversation, mge ManageGroupEvent, original string) {
switch mge.EventType {
case NewClearMessage:
var nme NewClearMessageEvent
err := json.Unmarshal([]byte(mge.Data), &nme)
if err == nil {
f.handleNewMessageEvent(profile, conversation, nme, original)
}
}
}
func (f *GroupManagerFunctionality) handleNewMessageEvent(profile peer.CwtchPeer, conversation model.Conversation, nme NewClearMessageEvent, original string) {
log.Infof("handling new clear message event")
hgm := nme.HybridGroupMessage
if AuthenticateMessage(hgm) {
log.Infof("authenticated message")
group, err := f.GetManagedGroup(profile)
if err != nil {
log.Infof("received fraudulant hybrid message from group: %v", err)
return
}
if acl, err := CheckACL(hgm.Author, group); err != nil {
log.Infof("received fraudulant hybrid message from group: %v", err)
return
} else if !acl.Append {
log.Infof("received fraudulant hybrid message from group: peer does not have append privileges")
return
} else {
// TODO - Store this message locally in a format that makes it easier to
// do assurance later on
// forward the message to everyone who the server has added as a contact
// and who are represented in the ACL...
allConversations, _ := profile.FetchConversations()
for _, ci := range allConversations {
// NOTE: This check works for Open Groups too as CheckACL will return the default ACL
// for the group....
if ci.Handle != MANAGED_GROUP_HANDLE { // don't send to ourselves...
if acl, err := CheckACL(hgm.Author, group); err == nil && acl.Read {
log.Infof("forwarding group message to: %v", ci.Handle)
profile.SendMessage(ci.ID, original)
}
}
}
}
} else {
log.Errorf("received fraudulant hybrid message fom group")
}
}
// GetManagedGroup is a convieniance function that looks up the managed group
func (f *GroupManagerFunctionality) GetManagedGroup(profile peer.CwtchPeer) (*model.Conversation, error) {
return profile.FetchConversationInfo(MANAGED_GROUP_HANDLE)
}
// Establish a new Managed Group and return its conversation id
func (f *GroupManagerFunctionality) ManageNewGroup(profile peer.CwtchPeer) (int, error) {
// note: a manager can only manage one group. This will (probably) always be true and has a few benefits
// and downsides.
// The main downside is that it requires a new manager per group (and thus an onion service per group)
// However, it means that we can lean on p2p functionality like profile images / metadata / name
// etc. for group metadata and effectively get that for-free in the client.
// HOWEVER: hedging our bets here by giving this group a numeric handle...
if _, err := profile.FetchConversationInfo(MANAGED_GROUP_HANDLE); err == nil {
return -1, fmt.Errorf("manager is already managing a group")
}
ac := model.DefaultP2PAccessControl()
// by setting the ManageGroup permission in this ACL we are allowing the manager to
// take control of how this group is structured, see OnEvent above...
ac.ManageGroup = true
acl := model.AccessControlList{}
acl[profile.GetOnion()] = ac
acl[MANAGED_GROUP_HANDLE] = model.NoAccessControl()
ci, err := profile.NewConversation(MANAGED_GROUP_HANDLE, acl)
if err != nil {
return -1, err
}
profile.SetConversationAttribute(ci, attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath(ManagedGroupOpen)), event.False)
return ci, nil
}
// AddHybridContact is a wrapper arround NewContactConversation which sets the contact
// up for Hybrid Group channel messages...
// TODO this function assumes that authorization has been done at a higher level..
func (f *GroupManagerFunctionality) AddHybridContact(profile peer.CwtchPeer, handle string) error {
ac := model.DefaultP2PAccessControl()
ac.ManageGroup = false
ci, err := profile.NewContactConversation(handle, ac, true)
if err != nil {
return err
}
mg, err := f.GetManagedGroup(profile)
if err != nil {
return err
}
// Update the ACL list to add this contact...
acl := mg.ACL
acl[handle] = model.DefaultP2PAccessControl()
profile.UpdateConversationAccessControlList(mg.ID, acl)
// enable channel 2 on this conversation (hybrid groups management channel)
profile.InitChannel(ci, constants.CHANNEL_MANAGER)
key := fmt.Sprintf("channel.%d", constants.CHANNEL_MANAGER)
profile.SetConversationAttribute(ci, attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath(key)), constants.True)
// Group managers need to always save history (and manually deal with purging...)
profile.SetConversationAttribute(ci, attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(event.SaveHistoryKey)), event.SaveHistoryConfirmed)
return nil
}
func (f *GroupManagerFunctionality) OnContactRequestValue(profile peer.CwtchPeer, conversation model.Conversation, eventID string, path attr.ScopedZonedPath) {
// nop hybrid group conversations do not exchange contact requests
}
func (f *GroupManagerFunctionality) OnContactReceiveValue(profile peer.CwtchPeer, conversation model.Conversation, path attr.ScopedZonedPath, value string, exists bool) {
// nop hybrid group conversations do not exchange contact requests
}

View File

@ -0,0 +1,330 @@
package hybrid
import (
"crypto/rand"
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/model/constants"
"cwtch.im/cwtch/peer"
"cwtch.im/cwtch/settings"
"encoding/base64"
"encoding/json"
"fmt"
"git.openprivacy.ca/openprivacy/log"
"golang.org/x/crypto/nacl/secretbox"
"math"
"math/big"
"strconv"
"time"
)
type ManagedGroupFunctionality struct {
}
func (f ManagedGroupFunctionality) NotifySettingsUpdate(settings settings.GlobalSettings) {
}
func (f ManagedGroupFunctionality) EventsToRegister() []event.Type {
return []event.Type{event.NewMessageFromPeerEngine}
}
func (f ManagedGroupFunctionality) ExperimentsToRegister() []string {
return []string{constants.GroupsExperiment}
}
// OnEvent handles File Sharing Hooks like Manifest Received and FileDownloaded
func (f *ManagedGroupFunctionality) OnEvent(ev event.Event, profile peer.CwtchPeer) {
switch ev.EventType {
// This is where most of the magic happens for managed groups. A few notes:
// - CwtchPeer has already taken care of storing this for us, we don't need to worry about that
// - Group Managers **only** speak overlays and **always** wrap their messages in a ManageGroupEvent anything else is fast-rejected.
case event.NewMessageFromPeerEngine:
handle := ev.Data[event.RemotePeer]
ci, err := profile.FetchConversationInfo(handle)
if err != nil {
break // we don't care about unknown conversations...
}
// We reject managed group requests for groups not setup as managed groups...
if ci.ACL[handle].ManageGroup {
var cm model.MessageWrapper
err = json.Unmarshal([]byte(ev.Data[event.Data]), &cm)
if err != nil {
break
}
// The overlay type of this message **must** be ManageGroupEvent
if cm.Overlay == model.OverlayManageGroupEvent {
var mge ManageGroupEvent
err = json.Unmarshal([]byte(cm.Data), &mge)
if err == nil {
cid, err := profile.FetchConversationInfo(handle)
if err == nil {
f.handleEvent(profile, *cid, mge)
}
}
}
}
}
}
// handleEvent takes in a high level ManageGroupEvent message, transforms it into the proper type, and passes it on for handling
// assumes we are called after an event provided by an authorized peer (i.e. ManageGroup == true)
func (f *ManagedGroupFunctionality) handleEvent(profile peer.CwtchPeer, conversation model.Conversation, mge ManageGroupEvent) {
switch mge.EventType {
case AddMember:
var ame AddMemberEvent
err := json.Unmarshal([]byte(mge.Data), &ame)
if err == nil {
f.handleAddMemberEvent(profile, conversation, ame)
}
case RemoveMember:
var rme RemoveMemberEvent
err := json.Unmarshal([]byte(mge.Data), &rme)
if err == nil {
f.handleRemoveMemberEvent(profile, conversation, rme)
}
case NewMessage:
var nme NewMessageEvent
err := json.Unmarshal([]byte(mge.Data), &nme)
if err == nil {
f.handleNewMessageEvent(profile, conversation, nme)
}
case NewClearMessage:
var nme NewClearMessageEvent
err := json.Unmarshal([]byte(mge.Data), &nme)
if err == nil {
f.handleNewClearMessageEvent(profile, conversation, nme)
}
case RotateKey:
var rke RotateKeyEvent
err := json.Unmarshal([]byte(mge.Data), &rke)
if err == nil {
f.handleRotateKeyEvent(profile, conversation, rke)
}
}
}
// handleAddMemberEvent adds a group member to the conversation ACL
// assumes we are called after an event provided by an authorized peer (i.e. ManageGroup == true)
func (f *ManagedGroupFunctionality) handleAddMemberEvent(profile peer.CwtchPeer, conversation model.Conversation, ame AddMemberEvent) {
acl := conversation.ACL
acl[ame.Handle] = model.DefaultP2PAccessControl()
profile.UpdateConversationAccessControlList(conversation.ID, acl)
}
// handleRemoveMemberEvent removes a group member from the conversation ACL
// assumes we are called after an event provided by an authorized peer (i.e. ManageGroup == true)
func (f *ManagedGroupFunctionality) handleRemoveMemberEvent(profile peer.CwtchPeer, conversation model.Conversation, rme RemoveMemberEvent) {
acl := conversation.ACL
delete(acl, rme.Handle)
profile.UpdateConversationAccessControlList(conversation.ID, acl)
}
// handleRotateKeyEvent rotates the encryption key for a given group
// assumes we are called after an event provided by an authorized peer (i.e. ManageGroup == true)
// TODO this currently is a noop as group levle encryption is unimplemented
func (f *ManagedGroupFunctionality) handleRotateKeyEvent(profile peer.CwtchPeer, conversation model.Conversation, rke RotateKeyEvent) {
keyScope := attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath("key"))
keyB64 := base64.StdEncoding.EncodeToString(rke.Key)
profile.SetConversationAttribute(conversation.ID, keyScope, keyB64)
}
// TODO this is a sketch implementation that is not yet complete.
func (f *ManagedGroupFunctionality) handleNewMessageEvent(profile peer.CwtchPeer, conversation model.Conversation, nme NewMessageEvent) {
keyScope := attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath("key"))
if keyB64, err := profile.GetConversationAttribute(conversation.ID, keyScope); err == nil {
key, err := base64.StdEncoding.DecodeString(keyB64)
if err != nil || len(key) != 32 {
log.Errorf("hybrid group key is corrupted")
return
}
// decrypt the message with key...
hgm, err := f.decryptMessage(key, nme.EncryptedHybridGroupMessage)
if hgm == nil || err != nil {
log.Errorf("unable to decrypt hybrid group message: %v", err)
return
}
f.handleNewClearMessageEvent(profile, conversation, NewClearMessageEvent{HybridGroupMessage: *hgm})
}
}
func (f *ManagedGroupFunctionality) handleNewClearMessageEvent(profile peer.CwtchPeer, conversation model.Conversation, nme NewClearMessageEvent) {
hgm := nme.HybridGroupMessage
if AuthenticateMessage(hgm) {
// TODO Closed Group Membership Check - right now we only support open groups...
if profile.GetOnion() == hgm.Author {
// ack
signatureB64 := base64.StdEncoding.EncodeToString(hgm.Signature)
id, err := profile.GetChannelMessageBySignature(conversation.ID, constants.CHANNEL_CHAT, signatureB64)
if err == nil {
profile.UpdateMessageAttribute(conversation.ID, constants.CHANNEL_CHAT, id, constants.AttrAck, constants.True)
profile.PublishEvent(event.NewEvent(event.IndexedAcknowledgement, map[event.Field]string{event.ConversationID: strconv.Itoa(conversation.ID), event.Index: strconv.Itoa(id)}))
}
} else {
mgidstr := strconv.Itoa(int(nme.HybridGroupMessage.MemberGroupID)) // we need both MemberGroupId and MemberMessageId for attestation later on...
newmmidstr := strconv.Itoa(int(nme.HybridGroupMessage.MemberMessageID))
// Set the attributes of this message...
attr := model.Attributes{MemberGroupIDKey: mgidstr, MemberMessageIDKey: newmmidstr,
constants.AttrAuthor: hgm.Author,
constants.AttrAck: event.True,
constants.AttrSentTimestamp: time.UnixMilli(int64(hgm.Sent)).Format(time.RFC3339Nano)}
// Note: The Channel here is 0...this is the main channel that UIs understand as the default, so this message is
// becomes part of the conversation...
mid, err := profile.InternalInsertMessage(conversation.ID, constants.CHANNEL_CHAT, hgm.Author, hgm.MessageBody, attr, hgm.Signature)
contenthash := model.CalculateContentHash(hgm.Author, hgm.MessageBody)
if err == nil {
profile.PublishEvent(event.NewEvent(event.NewMessageFromGroup, map[event.Field]string{event.ConversationID: strconv.Itoa(conversation.ID), event.TimestampSent: time.UnixMilli(int64(hgm.Sent)).Format(time.RFC3339Nano), event.RemotePeer: hgm.Author, event.Index: strconv.Itoa(mid), event.Data: hgm.MessageBody, event.ContentHash: contenthash}))
}
}
// TODO need to send an event here...
} else {
log.Errorf("received fraudulant hybrid message fom group")
}
}
// todo sketch function
func (f *ManagedGroupFunctionality) decryptMessage(key []byte, ciphertext []byte) (*HybridGroupMessage, error) {
if len(ciphertext) > 24 {
var decryptNonce [24]byte
copy(decryptNonce[:], ciphertext[:24])
var fixedSizeKey [32]byte
copy(fixedSizeKey[:], key[:32])
decrypted, ok := secretbox.Open(nil, ciphertext[24:], &decryptNonce, &fixedSizeKey)
if ok {
var hgm HybridGroupMessage
err := json.Unmarshal(decrypted, &hgm)
return &hgm, err
}
}
return nil, fmt.Errorf("invalid ciphertext/key error")
}
// Define a new managed group, managed by the manager...
func (f *ManagedGroupFunctionality) NewManagedGroup(profile peer.CwtchPeer, manager string) error {
// generate a truely random member id for this group in [0..2^32)
nBig, err := rand.Int(rand.Reader, big.NewInt(math.MaxUint32))
if err != nil {
return err // if there is a problem with random we want to exit now rather than have to clean up group setup...
}
ac := model.DefaultP2PAccessControl()
ac.ManageGroup = true // by setting the ManageGroup permission in this ACL we are allowing the manager to control of how this group is structured
ci, err := profile.NewContactConversation(manager, ac, true)
if err != nil {
return err
}
// enable channel 2 on this conversation (hybrid groups management channel)
key := fmt.Sprintf("channel.%d", 2)
err = profile.SetConversationAttribute(ci, attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath(key)), constants.True)
if err != nil {
return fmt.Errorf("could not enable channel 2 on hybrid group: %v", err) // likely a catestrophic error...fail
}
err = profile.InitChannel(ci, 2)
if err != nil {
return fmt.Errorf("could not enable channel 2 on hybrid group: %v", err) // likely a catestrophic error...fail
}
// finally, set the member group id on this group...
mgidkey := attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath(MemberGroupIDKey))
err = profile.SetConversationAttributeInt(ci, mgidkey, int(nBig.Uint64()))
if err != nil {
return fmt.Errorf("could not set group id on hybrid group: %v", err) // likely a catestrophic error...fail
}
return nil
}
// SendMessageToManagedGroup acts like SendMessage(ToPeer), but with a few additional bookkeeping steps for Hybrid Groups
func (f *ManagedGroupFunctionality) SendMessageToManagedGroup(profile peer.CwtchPeer, conversation int, message string) (int, error) {
mgidkey := attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath(MemberGroupIDKey))
mgid, err := profile.GetConversationAttributeInt(conversation, mgidkey)
if err != nil {
return -1, err
}
mmidkey := attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath(MemberMessageIDKey))
mmid, err := profile.GetConversationAttributeInt(conversation, mmidkey)
if err != nil {
mmid = 0 // first message
}
mmid += 1
// Now time to package this whole thing in layers of JSON...
hgm := HybridGroupMessage{
MemberGroupID: uint32(mgid),
MemberMessageID: uint32(mmid),
Sent: uint64(time.Now().UnixMilli()),
Author: profile.GetOnion(),
MessageBody: message,
Signature: []byte{}, // Leave blank so we can sign this message...
}
data, err := json.Marshal(hgm)
if err != nil {
return -1, err
}
// Don't forget to sign the message...
sig, err := profile.SignMessage(data)
if err != nil {
return -1, err
}
hgm.Signature = sig
ncm := NewClearMessageEvent{
HybridGroupMessage: hgm,
}
signedData, err := json.Marshal(ncm)
if err != nil {
return -1, err
}
mgm := ManageGroupEvent{
EventType: NewClearMessage,
Data: string(signedData),
}
odata, err := json.Marshal(mgm)
if err != nil {
return -1, err
}
overlay := model.MessageWrapper{
Overlay: model.OverlayManageGroupEvent,
Data: string(odata),
}
ojson, err := json.Marshal(overlay)
if err != nil {
return -1, err
}
// send the message to the manager and update our message is string for tracking...
_, err = profile.SendMessage(conversation, string(ojson))
if err != nil {
return -1, err
}
profile.SetConversationAttributeInt(conversation, mmidkey, mmid)
// ok there is still one more thing we need to do...
// insert this message as part of our group log, for members of the group
// this exists in channel 0 of the conversation with the group manager...
mgidstr := strconv.Itoa(mgid) // we need both MemberGroupId and MemberMessageId for attestation later on...
newmmidstr := strconv.Itoa(mmid)
attr := model.Attributes{MemberGroupIDKey: mgidstr, MemberMessageIDKey: newmmidstr, constants.AttrAuthor: profile.GetOnion(), constants.AttrAck: event.False, constants.AttrSentTimestamp: time.Now().Format(time.RFC3339Nano)}
return profile.InternalInsertMessage(conversation, 0, hgm.Author, message, attr, hgm.Signature)
}
func (f ManagedGroupFunctionality) OnContactRequestValue(profile peer.CwtchPeer, conversation model.Conversation, eventID string, path attr.ScopedZonedPath) {
// nop hybrid group conversations do not exchange contact requests
}
func (f ManagedGroupFunctionality) OnContactReceiveValue(profile peer.CwtchPeer, conversation model.Conversation, path attr.ScopedZonedPath, value string, exists bool) {
// nop hybrid group conversations do not exchange contact requests
}

View File

@ -0,0 +1,75 @@
package inter
import (
"errors"
"strings"
"cwtch.im/cwtch/functionality/hybrid"
"cwtch.im/cwtch/model/constants"
"cwtch.im/cwtch/peer"
)
// This functionality is a little different. It's not functionality per-se. It's a wrapper around
// CwtchProfile function that combines some core-functionalities like Hybrid Groups so that
// they can be transparently exposed in autobindings.
// DEV NOTE: consider moving other cross-cutting interface functions here to simplfy CwtchPeer
type InterfaceFunctionality struct {
}
// FunctionalityGate returns filesharing functionality - gates now happen on function calls.
func FunctionalityGate() *InterfaceFunctionality {
return new(InterfaceFunctionality)
}
func (i InterfaceFunctionality) ImportBundle(profile peer.CwtchPeer, uri string) error {
// check if this is a managed group. Note: managed groups do not comply with the server bundle format.
if strings.HasPrefix(uri, "managed:") {
uri = uri[len("managed:"):]
if profile.IsFeatureEnabled(constants.GroupsExperiment) {
mgf := hybrid.ManagedGroupFunctionality{}
return mgf.NewManagedGroup(profile, uri)
} else {
return errors.New("managed groups require the group experiment to be enabled")
}
}
// DEV NOTE: we may want to eventually move Server Import code to ServerFunctionality and add a hook here...
// DEV NOTE: consider making ImportBundle a high-level functionality interface? to support different kinds of contacts?
return profile.ImportBundle(uri)
}
// EnhancedImportBundle is identical to EnhancedImportBundle in CwtchPeer but instead of wrapping CwtchPeer.ImportBundle it instead
// wraps InterfaceFunctionality.ImportBundle
func (i InterfaceFunctionality) EnhancedImportBundle(profile peer.CwtchPeer, uri string) string {
err := i.ImportBundle(profile, uri)
if err == nil {
return "importBundle.success"
}
return err.Error()
}
// SendMessage sends a message to a conversation.
// NOTE: Unlike CwtchPeer.SendMessage this interface makes no guarentees about the raw-ness of the message sent to peer contacts.
// If the conversation is a hybrid groups then the message may be wrapped in multiple layers of overlay messages / encryption
// prior to being send. To send a raw message to a peer then use peer.CwtchPeer
// DEV NOTE: Move Legacy Group message send here...
func (i InterfaceFunctionality) SendMessage(profile peer.CwtchPeer, conversation int, message string) (int, error) {
ci, err := profile.GetConversationInfo(conversation)
if err != nil {
return -1, err
}
if ci.ACL[ci.Handle].ManageGroup {
mgf := hybrid.ManagedGroupFunctionality{}
return mgf.SendMessageToManagedGroup(profile, conversation, message)
}
return profile.SendMessage(conversation, message)
}
// EnhancedSendMessage Attempts to Send a Message and Immediately Attempts to Lookup the Message in the Database
// this wraps InterfaceFunctionality.SendMessage to support HybridGroups
func (i InterfaceFunctionality) EnhancedSendMessage(profile peer.CwtchPeer, conversation int, message string) string {
mid, err := i.SendMessage(profile, conversation, message)
if err != nil {
return ""
}
return profile.EnhancedGetMessageById(conversation, mid)
}

View File

@ -0,0 +1,150 @@
package servers
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/model/constants"
"cwtch.im/cwtch/peer"
"cwtch.im/cwtch/protocol/connections"
"cwtch.im/cwtch/settings"
"encoding/json"
"errors"
"git.openprivacy.ca/openprivacy/log"
)
const (
// ServerList is a json encoded list of servers
ServerList = event.Field("ServerList")
)
const (
// UpdateServerInfo is an event containing a ProfileOnion and a ServerList
UpdateServerInfo = event.Type("UpdateServerInfo")
)
// Functionality groups some common UI triggered functions for contacts...
type Functionality struct {
}
func (f *Functionality) NotifySettingsUpdate(settings settings.GlobalSettings) {
}
func (f *Functionality) EventsToRegister() []event.Type {
return []event.Type{event.QueueJoinServer}
}
func (f *Functionality) ExperimentsToRegister() []string {
return []string{constants.GroupsExperiment}
}
// OnEvent handles File Sharing Hooks like Manifest Received and FileDownloaded
func (f *Functionality) OnEvent(ev event.Event, profile peer.CwtchPeer) {
if profile.IsFeatureEnabled(constants.GroupsExperiment) {
switch ev.EventType {
// keep the UI in sync with the current backend server updates...
// queue join server gets triggered on load and on new servers so it's a nice
// low-noise event to hook into...
case event.QueueJoinServer:
f.PublishServerUpdate(profile)
}
}
}
func (f *Functionality) OnContactRequestValue(profile peer.CwtchPeer, conversation model.Conversation, eventID string, path attr.ScopedZonedPath) {
// nop
}
func (f *Functionality) OnContactReceiveValue(profile peer.CwtchPeer, conversation model.Conversation, path attr.ScopedZonedPath, value string, exists bool) {
// nopt
}
// FunctionalityGate returns filesharing functionality - gates now happen on function calls.
func FunctionalityGate() *Functionality {
return new(Functionality)
}
// ServerKey packages up key information...
// TODO: Can this be merged with KeyBundle?
type ServerKey struct {
Type string `json:"type"`
Key string `json:"key"`
}
// SyncStatus packages up server sync information...
type SyncStatus struct {
StartTime string `json:"startTime"`
LastMessageTime string `json:"lastMessageTime"`
}
// Server encapsulates the information needed to represent a server...
type Server struct {
Onion string `json:"onion"`
Identifier int `json:"identifier"`
Status string `json:"status"`
Description string `json:"description"`
Keys []ServerKey `json:"keys"`
SyncProgress SyncStatus `json:"syncProgress"`
}
// PublishServerUpdate serializes the current list of group servers and publishes an event with this information
func (f *Functionality) PublishServerUpdate(profile peer.CwtchPeer) error {
serverListForOnion := f.GetServerInfoList(profile)
serversListBytes, err := json.Marshal(serverListForOnion)
profile.PublishEvent(event.NewEvent(UpdateServerInfo, map[event.Field]string{"ProfileOnion": profile.GetOnion(), ServerList: string(serversListBytes)}))
return err
}
// GetServerInfoList compiles all the information the UI might need regarding all servers..
func (f *Functionality) GetServerInfoList(profile peer.CwtchPeer) []Server {
var servers []Server
for _, server := range profile.GetServers() {
server, err := f.GetServerInfo(profile, server)
if err != nil {
log.Errorf("profile server list is corrupted: %v", err)
continue
}
servers = append(servers, server)
}
return servers
}
// DeleteServer purges a server and all related keys from a profile
func (f *Functionality) DeleteServerInfo(profile peer.CwtchPeer, serverOnion string) error {
// Servers are stores as special conversations
ci, err := profile.FetchConversationInfo(serverOnion)
if err != nil {
return err
}
// Purge keys...
// NOTE: This will leave some groups in the state of being unable to connect to a particular
// server.
profile.DeleteConversation(ci.ID)
f.PublishServerUpdate(profile)
return nil
}
// GetServerInfo compiles all the information the UI might need regarding a particular server including any verified
// cryptographic keys
func (f *Functionality) GetServerInfo(profile peer.CwtchPeer, serverOnion string) (Server, error) {
serverInfo, err := profile.FetchConversationInfo(serverOnion)
if err != nil {
return Server{}, errors.New("server not found")
}
keyTypes := []model.KeyType{model.KeyTypeServerOnion, model.KeyTypeTokenOnion, model.KeyTypePrivacyPass}
var serverKeys []ServerKey
for _, keyType := range keyTypes {
if key, has := serverInfo.GetAttribute(attr.PublicScope, attr.ServerKeyZone, string(keyType)); has {
serverKeys = append(serverKeys, ServerKey{Type: string(keyType), Key: key})
}
}
description, _ := serverInfo.GetAttribute(attr.LocalScope, attr.ServerZone, constants.Description)
startTimeStr := serverInfo.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.LegacyGroupZone.ConstructZonedPath(constants.SyncPreLastMessageTime)).ToString()]
recentTimeStr := serverInfo.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.LegacyGroupZone.ConstructZonedPath(constants.SyncMostRecentMessageTime)).ToString()]
syncStatus := SyncStatus{startTimeStr, recentTimeStr}
return Server{Onion: serverOnion, Identifier: serverInfo.ID, Status: connections.ConnectionStateName[profile.GetPeerState(serverInfo.Handle)], Keys: serverKeys, Description: description, SyncProgress: syncStatus}, nil
}

31
go.mod
View File

@ -1,14 +1,29 @@
module cwtch.im/cwtch
go 1.14
go 1.20
require (
git.openprivacy.ca/cwtch.im/tapir v0.4.9
git.openprivacy.ca/openprivacy/connectivity v1.5.0
git.openprivacy.ca/cwtch.im/tapir v0.6.0
git.openprivacy.ca/openprivacy/connectivity v1.11.0
git.openprivacy.ca/openprivacy/log v1.0.3
github.com/gtank/ristretto255 v0.1.2
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee
golang.org/x/sys v0.0.0-20210510120138-977fb7262007 // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
github.com/gtank/ristretto255 v0.1.3-0.20210930101514-6bb39798585c
github.com/mutecomm/go-sqlcipher/v4 v4.4.2
github.com/onsi/ginkgo/v2 v2.1.4
github.com/onsi/gomega v1.20.1
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d
)
require (
filippo.io/edwards25519 v1.0.0 // indirect
git.openprivacy.ca/openprivacy/bine v0.0.5 // indirect
github.com/google/go-cmp v0.5.8 // indirect
github.com/gtank/merlin v0.1.1 // indirect
github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b // indirect
github.com/stretchr/testify v1.7.0 // indirect
go.etcd.io/bbolt v1.3.6 // indirect
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b // indirect
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 // indirect
golang.org/x/text v0.3.7 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

73
go.sum
View File

@ -1,59 +1,70 @@
filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU=
filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
git.openprivacy.ca/cwtch.im/tapir v0.4.9 h1:LXonlztwvI1F1++0IyomIcDH1/Bxzo+oN8YjGonNvjM=
git.openprivacy.ca/cwtch.im/tapir v0.4.9/go.mod h1:p4bHo3DAO8wwimU6JAeZXbfPQ4jnoA2bV+4YvknWTNQ=
git.openprivacy.ca/openprivacy/bine v0.0.4 h1:CO7EkGyz+jegZ4ap8g5NWRuDHA/56KKvGySR6OBPW+c=
git.openprivacy.ca/openprivacy/bine v0.0.4/go.mod h1:13ZqhKyqakDsN/ZkQkIGNULsmLyqtXc46XBcnuXm/mU=
git.openprivacy.ca/openprivacy/connectivity v1.5.0 h1:ZxsR/ZaVKXIkD2x6FlajZn62ciNQjamrI4i/5xIpdoQ=
git.openprivacy.ca/openprivacy/connectivity v1.5.0/go.mod h1:UjQiGBnWbotmBzIw59B8H6efwDadjkKzm3RPT1UaIRw=
git.openprivacy.ca/openprivacy/log v1.0.2/go.mod h1:gGYK8xHtndRLDymFtmjkG26GaMQNgyhioNS82m812Iw=
filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek=
filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
git.openprivacy.ca/cwtch.im/tapir v0.6.0 h1:TtnKjxitkIDMM7Qn0n/u+mOHRLJzuQUYjYRu5n0/QFY=
git.openprivacy.ca/cwtch.im/tapir v0.6.0/go.mod h1:iQIq4y7N+DuP3CxyG66WNEC/d6vzh+wXvvOmelB+KoY=
git.openprivacy.ca/openprivacy/bine v0.0.5 h1:DJs5gqw3SkvLSgRDvroqJxZ7F+YsbxbBRg5t0rU5gYE=
git.openprivacy.ca/openprivacy/bine v0.0.5/go.mod h1:fwdeq6RO08WDkV0k7HfArsjRvurVULoUQmT//iaABZM=
git.openprivacy.ca/openprivacy/connectivity v1.11.0 h1:roASjaFtQLu+HdH5fa2wx6F00NL3YsUTlmXBJh8aLZk=
git.openprivacy.ca/openprivacy/connectivity v1.11.0/go.mod h1:OQO1+7OIz/jLxDrorEMzvZA6SEbpbDyLGpjoFqT3z1Y=
git.openprivacy.ca/openprivacy/log v1.0.3 h1:E/PMm4LY+Q9s3aDpfySfEDq/vYQontlvNj/scrPaga0=
git.openprivacy.ca/openprivacy/log v1.0.3/go.mod h1:gGYK8xHtndRLDymFtmjkG26GaMQNgyhioNS82m812Iw=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gtank/merlin v0.1.1 h1:eQ90iG7K9pOhtereWsmyRJ6RAwcP4tHTDBHXNg+u5is=
github.com/gtank/merlin v0.1.1/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s=
github.com/gtank/ristretto255 v0.1.2 h1:JEqUCPA1NvLq5DwYtuzigd7ss8fwbYay9fi4/5uMzcc=
github.com/gtank/ristretto255 v0.1.2/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/gtank/ristretto255 v0.1.3-0.20210930101514-6bb39798585c h1:gkfmnY4Rlt3VINCo4uKdpvngiibQyoENVj5Q88sxXhE=
github.com/gtank/ristretto255 v0.1.3-0.20210930101514-6bb39798585c/go.mod h1:tDPFhGdt3hJWqtKwx57i9baiB1Cj0yAg22VOPUqm5vY=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643 h1:hLDRPB66XQT/8+wG9WsDpiCvZf1yKO7sz7scAjSlBa0=
github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b h1:QrHweqAtyJ9EwCaGHBu1fghwxIPiopAHV06JlXrMHjk=
github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b/go.mod h1:xxLb2ip6sSUts3g1irPVHyk/DGslwQsNOo9I7smJfNU=
github.com/mutecomm/go-sqlcipher/v4 v4.4.2 h1:eM10bFtI4UvibIsKr10/QT7Yfz+NADfjZYh0GKrXUNc=
github.com/mutecomm/go-sqlcipher/v4 v4.4.2/go.mod h1:mF2UmIpBnzFeBdu/ypTDb/LdbS0nk0dfSN1WUsWTjMA=
github.com/onsi/ginkgo/v2 v2.1.4 h1:GNapqRSid3zijZ9H77KrgVG4/8KqiyRsxcSxe+7ApXY=
github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU=
github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q=
github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo=
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg=
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee h1:4yd7jl+vXjalO5ztz6Vc1VADv+S/80LGJmyl1ROJ2AI=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d h1:3qF+Z8Hkrw9sOhrFHti9TlB1Hkac1x+DNRkv0XQiFjo=
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b h1:ZmngSVLe/wycRns9MKikG9OWIEjGcGAkacif7oYQaUY=
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
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-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/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-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 h1:UiNENfZ8gDvpiWw7IpOMQ27spWmThO1RwwdQVbJahJM=
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
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-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
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/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -1,5 +1,10 @@
package attr
import (
"git.openprivacy.ca/openprivacy/log"
"strings"
)
/*
Scope model for peer attributes and requests
@ -18,6 +23,12 @@ type Scope string
// ScopedZonedPath typed path with a scope and a zone
type ScopedZonedPath string
func (szp ScopedZonedPath) GetScopeZonePath() (Scope, Zone, string) {
scope, path := ParseScope(string(szp))
zone, zpath := ParseZone(path)
return scope, zone, zpath
}
// scopes for attributes
const (
// on a peer, local and peer supplied data
@ -79,20 +90,15 @@ func (scope Scope) IsConversation() bool {
return scope == ConversationScope
}
// GetLocalScope takes a path and attaches the local scope to it
// Deprecated: Use ConstructScopedZonedPath
func GetLocalScope(path string) string {
return string(LocalScope) + Separator + path
}
// ParseScope takes in an untyped string and returns an explicit Scope along with the rest of the untyped path
func ParseScope(path string) (Scope, string) {
parts := strings.SplitN(path, Separator, 3)
// GetPublicScope takes a path and attaches the local scope to it
// Deprecated: Use ConstructScopedZonedPath
func GetPublicScope(path string) string {
return string(PublicScope) + Separator + path
}
log.Debugf("parsed scope: %v %v", parts, path)
// GetPeerScope takes a path and attaches the peer scope to it
// Deprecated: Use ConstructScopedZonedPath
func GetPeerScope(path string) string {
return string(PeerScope) + Separator + path
if len(parts) != 3 {
return UnknownScope, ""
}
return IntoScope(parts[0]), parts[1] + Separator + parts[2]
}

View File

@ -17,9 +17,21 @@ const (
// ProfileZone for attributes related to profile details like name and profile image
ProfileZone = Zone("profile")
// LegacyGroupZone for attributes related to legacy group experiment
LegacyGroupZone = Zone("legacygroup")
// ConversationZone for attributes related to structure of the conversation
ConversationZone = Zone("conversation")
// FilesharingZone for attributes related to file sharing
FilesharingZone = Zone("filesharing")
// ServerKeyZone for attributes related to Server Keys
ServerKeyZone = Zone("serverkey")
// ServerZone is for attributes related to the server
ServerZone = Zone("server")
// UnknownZone is a catch all useful for error handling
UnknownZone = Zone("unknown")
)
@ -31,6 +43,10 @@ func (zone Zone) ConstructZonedPath(path string) ZonedPath {
return ZonedPath(string(zone) + Separator + path)
}
func (zp ZonedPath) ToString() string {
return string(zp)
}
// ParseZone takes in an untyped string and returns an explicit Zone along with the rest of the untyped path
func ParseZone(path string) (Zone, string) {
parts := strings.SplitN(path, Separator, 2)
@ -44,8 +60,16 @@ func ParseZone(path string) (Zone, string) {
switch Zone(parts[0]) {
case ProfileZone:
return ProfileZone, parts[1]
case LegacyGroupZone:
return LegacyGroupZone, parts[1]
case FilesharingZone:
return FilesharingZone, parts[1]
case ServerKeyZone:
return ServerKeyZone, parts[1]
case ServerZone:
return ServerZone, parts[1]
case ConversationZone:
return ConversationZone, parts[1]
default:
return UnknownZone, parts[1]
}

View File

@ -3,6 +3,9 @@ package constants
// Name refers to a Profile Name
const Name = "name"
// Onion refers the Onion address of the profile
const Onion = "onion"
// Tag describes the type of a profile e.g. default password / encrypted etc.
const Tag = "tag"
@ -11,3 +14,62 @@ const ProfileTypeV1DefaultPassword = "v1-defaultPassword"
// ProfileTypeV1Password is a tag describing a profile encrypted derived from a user-provided password.
const ProfileTypeV1Password = "v1-userPassword"
// GroupID is the ID of a group
const GroupID = "groupid"
// GroupServer identifies the Server the legacy group is hosted on
const GroupServer = "groupserver"
// GroupKey is the name of the group key attribute...
const GroupKey = "groupkey"
// True - true
const True = "true"
// False - false
const False = "false"
// AttrAuthor - conversation attribute for author of the message - referenced by pub key rather than conversation id because of groups.
const AttrAuthor = "author"
// AttrAck - conversation attribute for acknowledgement status
const AttrAck = "ack"
// AttrErr - conversation attribute for errored status
const AttrErr = "error"
// AttrSentTimestamp - conversation attribute for the time the message was (nominally) sent
const AttrSentTimestamp = "sent"
// Legacy MessageFlags
// AttrRejected - conversation attribute for storing rejected prompts (for invites)
const AttrRejected = "rejected-invite"
// AttrDownloaded - conversation attribute for storing downloaded prompts (for file downloads)
const AttrDownloaded = "file-downloaded"
const CustomProfileImageKey = "custom-profile-image"
const SyncPreLastMessageTime = "SyncPreLastMessageTime"
const SyncMostRecentMessageTime = "SyncMostRecentMessageTime"
const AttrLastConnectionTime = "last-connection-time"
const PeerAutostart = "autostart"
const PeerAppearOffline = "appear-offline"
const PrivateName = "private-name"
const Archived = "archived"
const ProfileStatus = "profile-status"
const ProfileAttribute1 = "profile-attribute-1"
const ProfileAttribute2 = "profile-attribute-2"
const ProfileAttribute3 = "profile-attribute-3"
// Description is used on server contacts,
const Description = "description"
// Used to store the status of acl migrations
const ACLVersion = "acl-version"
const ACLVersionOne = "acl-v1"
const ACLVersionTwo = "acl-v2"

View File

@ -0,0 +1,13 @@
package constants
// ServerPrefix precedes a server import statement
const ServerPrefix = "server:"
// TofuBundlePrefix precedes a server and a group import statement
const TofuBundlePrefix = "tofubundle:"
// GroupPrefix precedes a group import statement
const GroupPrefix = "torv3"
// ImportBundlePrefix is an error api constant for import bundle error messages
const ImportBundlePrefix = "importBundle"

View File

@ -0,0 +1,4 @@
package constants
const CHANNEL_CHAT = 0
const CHANNEL_MANAGER = 2

View File

@ -0,0 +1,7 @@
package constants
// InvalidPasswordError is returned when an incorrect password is provided to a function that requires the current active password
const InvalidPasswordError = "invalid_password_error"
// PasswordsDoNotMatchError is returned when two passwords do not match
const PasswordsDoNotMatchError = "passwords_do_not_match"

View File

@ -0,0 +1,24 @@
package constants
const GroupsExperiment = "tapir-groups-experiment"
// FileSharingExperiment Allows file sharing
const FileSharingExperiment = "filesharing"
// ImagePreviewsExperiment Causes images (up to ImagePreviewMaxSizeInBytes, from accepted contacts) to auto-dl and preview
// requires FileSharingExperiment to be enabled
const ImagePreviewsExperiment = "filesharing-images"
// ImagePreviewMaxSizeInBytes Files up to this size will be autodownloaded using ImagePreviewsExperiment
const ImagePreviewMaxSizeInBytes = 20971520
const MessageFormattingExperiment = "message-formatting"
// AutoDLFileExts Files with these extensions will be autodownloaded using ImagePreviewsExperiment
var AutoDLFileExts = [...]string{".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"}
// BlodeuweddExperiment enables the Blodeuwedd Assistant
const BlodeuweddExperiment = "blodeuwedd"
// Enables the Hybrid Group Manager Extension
const GroupManagerExperiment = "group-manager"

181
model/conversation.go Normal file
View File

@ -0,0 +1,181 @@
package model
import (
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/model/constants"
"encoding/json"
"fmt"
"git.openprivacy.ca/openprivacy/connectivity/tor"
"git.openprivacy.ca/openprivacy/log"
"time"
)
// AccessControl is a type determining client assigned authorization to a peer
// for a given conversation
type AccessControl struct {
Blocked bool // Any attempts from this handle to connect are blocked overrides all other settings
// Basic Conversation Rights
Read bool // Allows a handle to access the conversation
Append bool // Allows a handle to append new messages to the conversation
AutoConnect bool // Profile should automatically try to connect with peer
ExchangeAttributes bool // Profile should automatically exchange attributes like Name, Profile Image, etc.
// Extension Related Permissions
ShareFiles bool // Allows a handle to share files to a conversation
RenderImages bool // Indicates that certain filetypes should be autodownloaded and rendered when shared by this contact
ManageGroup bool // Allows this conversation to be managed by hybrid groups
}
// DefaultP2PAccessControl defaults to a semi-trusted peer with no access to special extensions.
func DefaultP2PAccessControl() AccessControl {
return AccessControl{Read: true, Append: true, ExchangeAttributes: true, Blocked: false,
AutoConnect: true, ShareFiles: false, RenderImages: false}
}
// NoAccessControl defaults to a none-trusted peer with no access to special extensions.
// This is used as a fall back (if due to a software glitch a contact was setup without an access control, or for
// special contacts that should never be involvedt in external networking e.g. notes-to-self, or managed peers)
func NoAccessControl() AccessControl {
return AccessControl{Read: false, Append: false, ExchangeAttributes: false, Blocked: false,
AutoConnect: false, ShareFiles: false, RenderImages: false}
}
// AccessControlList represents an access control list for a conversation. Mapping handles to conversation
// functions
type AccessControlList map[string]AccessControl
// Serialize transforms the ACL into json.
func (acl *AccessControlList) Serialize() []byte {
data, _ := json.Marshal(acl)
return data
}
// DeserializeAccessControlList takes in JSON and returns an AccessControlList
func DeserializeAccessControlList(data []byte) (AccessControlList, error) {
var acl AccessControlList
err := json.Unmarshal(data, &acl)
return acl, err
}
// Attributes a type-driven encapsulation of an Attribute map.
type Attributes map[string]string
// Serialize transforms an Attributes map into a JSON struct
func (a *Attributes) Serialize() []byte {
data, _ := json.Marshal(a)
return data
}
// DeserializeAttributes converts a JSON struct into an Attributes map
func DeserializeAttributes(data []byte) Attributes {
attributes := make(Attributes)
err := json.Unmarshal(data, &attributes)
if err != nil {
log.Error("error deserializing attributes (this is likely a programming error): %v", err)
return make(Attributes)
}
return attributes
}
// Conversation encapsulates high-level information about a conversation, including the
// handle, any set attributes, the access control list associated with the message tree and the
// accepted status of the conversation (whether the user has consented into the conversation).
type Conversation struct {
ID int
Handle string
Attributes Attributes
ACL AccessControlList
// Deprecated, please use ACL for permissions related functions
Accepted bool
}
// GetAttribute is a helper function that fetches a conversation attribute by scope, zone and key
func (ci *Conversation) GetAttribute(scope attr.Scope, zone attr.Zone, key string) (string, bool) {
if value, exists := ci.Attributes[scope.ConstructScopedZonedPath(zone.ConstructZonedPath(key)).ToString()]; exists {
return value, true
}
return "", false
}
// GetPeerAC returns a suitable Access Control object for a the given peer conversation
// If this is called for a group conversation, this method will error and return a safe default AC.
func (ci *Conversation) GetPeerAC() AccessControl {
if acl, exists := ci.ACL[ci.Handle]; exists {
return acl
}
log.Errorf("attempted to access a Peer Access Control object from %v but peer ACL is undefined. This is likely a programming error - fallback to a NoAccess AC", ci.Handle)
return NoAccessControl()
}
// HasChannel returns true if the requested channel has been setup for this conversation
func (ci *Conversation) HasChannel(requestedChannel int) bool {
if requestedChannel == 0 {
return true
}
if requestedChannel == 1 {
return false // channel 1 is mapped to channel 0 for backwards compatibility
}
key := fmt.Sprintf("channel.%d", requestedChannel)
if value, exists := ci.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath(key)).ToString()]; exists {
return value == constants.True
}
return false
}
// IsCwtchPeer is a helper attribute that identifies whether a conversation is a cwtch peer
func (ci *Conversation) IsCwtchPeer() bool {
return tor.IsValidHostname(ci.Handle)
}
// IsGroup is a helper attribute that identifies whether a conversation is a legacy group
func (ci *Conversation) IsGroup() bool {
if _, exists := ci.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.LegacyGroupZone.ConstructZonedPath(constants.GroupID)).ToString()]; exists {
return true
}
return false
}
// IsServer is a helper attribute that identifies whether a conversation is with a server
func (ci *Conversation) IsServer() bool {
if _, exists := ci.Attributes[attr.PublicScope.ConstructScopedZonedPath(attr.ServerKeyZone.ConstructZonedPath(string(BundleType))).ToString()]; exists {
return true
}
return false
}
// ServerSyncProgress is only valid during a server being in the AUTHENTICATED state and therefor in the syncing process
// it returns a double (0-1) representing the estimated progress of the syncing
func (ci *Conversation) ServerSyncProgress() float64 {
startTimeStr, startExists := ci.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.LegacyGroupZone.ConstructZonedPath(constants.SyncPreLastMessageTime)).ToString()]
recentTimeStr, recentExists := ci.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.LegacyGroupZone.ConstructZonedPath(constants.SyncMostRecentMessageTime)).ToString()]
if !startExists || !recentExists {
return 0.0
}
startTime, err := time.Parse(startTimeStr, time.RFC3339Nano)
if err != nil {
return 0.0
}
recentTime, err := time.Parse(recentTimeStr, time.RFC3339Nano)
if err != nil {
return 0.0
}
syncRange := time.Since(startTime)
pointFromStart := startTime.Sub(recentTime)
return pointFromStart.Seconds() / syncRange.Seconds()
}
// ConversationMessage bundles an instance of a conversation message row
type ConversationMessage struct {
ID int
Body string
Attr Attributes
Signature string
ContentHash string
}

41
model/experiments.go Normal file
View File

@ -0,0 +1,41 @@
package model
import "sync"
// Experiments are optional functionality that can be enabled/disabled by an application either completely or individually.
// examples of experiments include File Sharing, Profile Images and Groups.
type Experiments struct {
enabled bool
experiments *sync.Map
}
// InitExperiments encapsulates a set of experiments separate from their storage in GlobalSettings.
func InitExperiments(enabled bool, experiments map[string]bool) Experiments {
syncExperiments := new(sync.Map)
for experiment, set := range experiments {
syncExperiments.Store(experiment, set)
}
return Experiments{
enabled: enabled,
experiments: syncExperiments,
}
}
// IsEnabled is a convenience function that takes in an experiment and returns true if it is enabled. Experiments
// are only enabled if both global experiments are turned on and if the specific experiment is also turned on.
// The one exception to this is experiments that have been promoted to default functionality which may be turned on
// even if experiments turned off globally. These experiments are defined by DefaultEnabledFunctionality.
func (e *Experiments) IsEnabled(experiment string) bool {
if !e.enabled {
// todo handle default-enabled functionality
return false
}
enabled, exists := e.experiments.Load(experiment)
if !exists {
return false
}
return enabled.(bool)
}

View File

@ -4,8 +4,6 @@ import (
"crypto/ed25519"
"crypto/rand"
"crypto/sha512"
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/model/constants"
"cwtch.im/cwtch/protocol/groups"
"encoding/base32"
"encoding/base64"
@ -13,13 +11,13 @@ import (
"encoding/json"
"errors"
"fmt"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
"git.openprivacy.ca/openprivacy/connectivity/tor"
"git.openprivacy.ca/openprivacy/log"
"golang.org/x/crypto/nacl/secretbox"
"golang.org/x/crypto/pbkdf2"
"io"
"strings"
"sync"
"time"
)
@ -33,25 +31,19 @@ const GroupInvitePrefix = "torv3"
// tied to a server under a given group key. Each group has a set of Messages.
type Group struct {
// GroupID is now derived from the GroupKey and the GroupServer
GroupID string
GroupKey [32]byte
GroupServer string
Timeline Timeline `json:"-"`
Accepted bool
IsCompromised bool
Attributes map[string]string
lock sync.Mutex
LocalID string
State string `json:"-"`
Version int
GroupID string
GroupName string
GroupKey [32]byte
GroupServer string
Attributes map[string]string //legacy to not use
Version int
Timeline Timeline `json:"-"`
LocalID string
}
// NewGroup initializes a new group associated with a given CwtchServer
func NewGroup(server string) (*Group, error) {
group := new(Group)
group.Version = CurrentGroupVersion
group.LocalID = GenerateRandomID()
group.Accepted = true // we are starting a group, so we assume we want to connect to it...
if !tor.IsValidHostname(server) {
return nil, errors.New("server is not a valid v3 onion")
}
@ -67,31 +59,26 @@ func NewGroup(server string) (*Group, error) {
// Derive Group ID from the group key and the server public key. This binds the group to a particular server
// and key.
group.GroupID = deriveGroupID(groupKey[:], server)
group.Attributes = make(map[string]string)
// By default we set the "name" of the group to a random string, we can override this later, but to simplify the
// codes around invite, we assume that this is always set.
group.Attributes[attr.GetLocalScope(constants.Name)] = group.GroupID
return group, nil
var err error
group.GroupID, err = deriveGroupID(groupKey[:], server)
return group, err
}
// CheckGroup returns true only if the ID of the group is cryptographically valid.
func (g *Group) CheckGroup() bool {
return g.GroupID == deriveGroupID(g.GroupKey[:], g.GroupServer)
id, _ := deriveGroupID(g.GroupKey[:], g.GroupServer)
return g.GroupID == id
}
// deriveGroupID hashes together the key and the hostname to create a bound identifier that can later
// be referenced and checked by profiles when they receive invites and messages.
func deriveGroupID(groupKey []byte, serverHostname string) string {
data, _ := base32.StdEncoding.DecodeString(strings.ToUpper(serverHostname))
func deriveGroupID(groupKey []byte, serverHostname string) (string, error) {
data, err := base32.StdEncoding.DecodeString(strings.ToUpper(serverHostname))
if err != nil {
return "", err
}
pubkey := data[0:ed25519.PublicKeySize]
return hex.EncodeToString(pbkdf2.Key(groupKey, pubkey, 4096, 16, sha512.New))
}
// Compromised should be called if we detect a groupkey leak
func (g *Group) Compromised() {
g.IsCompromised = true
return hex.EncodeToString(pbkdf2.Key(groupKey, pubkey, 4096, 16, sha512.New)), nil
}
// Invite generates a invitation that can be sent to a cwtch peer
@ -99,7 +86,7 @@ func (g *Group) Invite() (string, error) {
gci := &groups.GroupInvite{
GroupID: g.GroupID,
GroupName: g.Attributes[attr.GetLocalScope(constants.Name)],
GroupName: g.GroupName,
SharedKey: g.GroupKey[:],
ServerHost: g.GroupServer,
}
@ -109,76 +96,7 @@ func (g *Group) Invite() (string, error) {
return serializedInvite, err
}
// AddSentMessage takes a DecryptedGroupMessage and adds it to the Groups Timeline
func (g *Group) AddSentMessage(message *groups.DecryptedGroupMessage, sig []byte) Message {
g.lock.Lock()
defer g.lock.Unlock()
timelineMessage := Message{
Message: message.Text,
Timestamp: time.Unix(int64(message.Timestamp), 0),
Received: time.Unix(0, 0),
Signature: sig,
PeerID: message.Onion,
PreviousMessageSig: message.PreviousMessageSig,
ReceivedByServer: false,
}
g.Timeline.Insert(&timelineMessage)
return timelineMessage
}
// ErrorSentMessage removes a sent message from the unacknowledged list and sets its error flag if found, otherwise returns false
func (g *Group) ErrorSentMessage(sig []byte, error string) bool {
g.lock.Lock()
defer g.lock.Unlock()
return g.Timeline.SetSendError(sig, error)
}
// GetMessage returns the message at index `index` if it exists. Otherwise returns false.
// This routine also returns the length of the timeline
// If go has an optional type this would return Option<Message>...
func (g *Group) GetMessage(index int) (bool, Message, int) {
g.lock.Lock()
defer g.lock.Unlock()
length := len(g.Timeline.Messages)
if length > index {
return true, g.Timeline.Messages[index], length
}
return false, Message{}, length
}
// AddMessage takes a DecryptedGroupMessage and adds it to the Groups Timeline
func (g *Group) AddMessage(message *groups.DecryptedGroupMessage, sig []byte) (*Message, int) {
g.lock.Lock()
defer g.lock.Unlock()
timelineMessage := &Message{
Message: message.Text,
Timestamp: time.Unix(int64(message.Timestamp), 0),
Received: time.Now(),
Signature: sig,
PeerID: message.Onion,
PreviousMessageSig: message.PreviousMessageSig,
ReceivedByServer: true,
Error: "",
Acknowledged: true,
}
index := g.Timeline.Insert(timelineMessage)
return timelineMessage, index
}
// GetTimeline provides a safe copy of the timeline
func (g *Group) GetTimeline() (timeline []Message) {
g.lock.Lock()
defer g.lock.Unlock()
return g.Timeline.GetMessages()
}
//EncryptMessage takes a message and encrypts the message under the group key.
// EncryptMessage takes a message and encrypts the message under the group key.
func (g *Group) EncryptMessage(message *groups.DecryptedGroupMessage) ([]byte, error) {
var nonce [24]byte
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
@ -211,21 +129,6 @@ func (g *Group) DecryptMessage(ciphertext []byte) (bool, *groups.DecryptedGroupM
return false, nil
}
// SetAttribute allows applications to store arbitrary configuration info at the group level.
func (g *Group) SetAttribute(name string, value string) {
g.lock.Lock()
defer g.lock.Unlock()
g.Attributes[name] = value
}
// GetAttribute returns the value of a value set with SetAttribute. If no such value has been set exists is set to false.
func (g *Group) GetAttribute(name string) (value string, exists bool) {
g.lock.Lock()
defer g.lock.Unlock()
value, exists = g.Attributes[name]
return
}
// ValidateInvite takes in a serialized invite and returns the invite structure if it is cryptographically valid
// and an error if it is not
func ValidateInvite(invite string) (*groups.GroupInvite, error) {
@ -250,7 +153,7 @@ func ValidateInvite(invite string) (*groups.GroupInvite, error) {
// Derive the servers public key (we can ignore the error checking here because it's already been
// done by IsValidHostname, and check that we derive the same groupID...
derivedGroupID := deriveGroupID(gci.SharedKey, gci.ServerHost)
derivedGroupID, _ := deriveGroupID(gci.SharedKey, gci.ServerHost)
if derivedGroupID != gci.GroupID {
return nil, errors.New("group id is invalid")
}
@ -263,3 +166,115 @@ func ValidateInvite(invite string) (*groups.GroupInvite, error) {
}
return nil, errors.New("invite has invalid structure")
}
// AttemptDecryption takes a ciphertext and signature and attempts to decrypt it under known groups.
// If successful, adds the message to the group's timeline
func (g *Group) AttemptDecryption(ciphertext []byte, signature []byte) (bool, *groups.DecryptedGroupMessage) {
success, dgm := g.DecryptMessage(ciphertext)
// the second check here is not needed, but DecryptMessage violates the usual
// go calling convention and we want static analysis tools to pick it up
if success && dgm != nil {
// Attempt to serialize this message
serialized, err := json.Marshal(dgm)
// Someone send a message that isn't a valid Decrypted Group Message. Since we require this struct in orer
// to verify the message, we simply ignore it.
if err != nil {
return false, nil
}
// This now requires knowledge of the Sender, the Onion and the Specific Decrypted Group Message (which should only
// be derivable from the cryptographic key) which contains many unique elements such as the time and random padding
verified := g.VerifyGroupMessage(dgm.Onion, g.GroupID, base64.StdEncoding.EncodeToString(serialized), signature)
if !verified {
// An earlier version of this protocol mistakenly signed the ciphertext of the message
// instead of the serialized decrypted group message.
// This has 2 issues:
// 1. A server with knowledge of group members public keys AND the Group ID would be able to detect valid messages
// 2. It made the metadata-security of a group dependent on keeping the cryptographically derived Group ID secret.
// While not awful, it also isn't good. For Version 3 groups only we permit Cwtch to check this older signature
// structure in a backwards compatible way for the duration of the Groups Experiment.
// TODO: Delete this check when Groups are no long Experimental
if g.Version == 3 {
verified = g.VerifyGroupMessage(dgm.Onion, g.GroupID, string(ciphertext), signature)
}
}
// So we have a message that has a valid group key, but the signature can't be verified.
// The most obvious explanation for this is that the group key has been compromised (or we are in an open group and the server is being malicious)
// Either way, someone who has the private key is being detectably bad so we are just going to throw this message away and mark the group as Compromised.
if !verified {
return false, nil
}
return true, dgm
}
// If we couldn't find a group to decrypt the message with we just return false. This is an expected case
return false, nil
}
// VerifyGroupMessage confirms the authenticity of a message given an sender onion, message and signature.
// The goal of this function is 2-fold:
// 1. We confirm that the sender referenced in the group text is the actual sender of the message (or at least
// knows the senders private key)
// 2. Secondly, we confirm that the sender sent the message to a particular group id on a specific server (it doesn't
// matter if we actually received this message from the server or from a hybrid protocol, all that matters is
// that the sender and receivers agree that this message was intended for the group
//
// The 2nd point is important as it prevents an attack documented in the original Cwtch paper (and later at
// https://docs.openprivacy.ca/cwtch-security-handbook/groups.html) in which a malicious profile sets up 2 groups
// on two different servers with the same key and then forwards messages between them to convince the parties in
// each group that they are actually in one big group (with the intent to later censor and/or selectively send messages
// to each group).
func (g *Group) VerifyGroupMessage(onion string, groupID string, message string, signature []byte) bool {
// We use our group id, a known reference server and the ciphertext of the message.
m := groupID + g.GroupServer + message
// Otherwise we derive the public key from the sender and check it against that.
decodedPub, err := base32.StdEncoding.DecodeString(strings.ToUpper(onion))
if err == nil && len(decodedPub) >= 32 {
return ed25519.Verify(decodedPub[:32], []byte(m), signature)
}
return false
}
// EncryptMessageToGroup when given a message and a group, encrypts and signs the message under the group and
// profile
func EncryptMessageToGroup(message string, author primitives.Identity, group *Group, prevSig string) ([]byte, []byte, *groups.DecryptedGroupMessage, error) {
if len(message) > MaxGroupMessageLength {
return nil, nil, nil, errors.New("group message is too long")
}
timestamp := time.Now().Unix()
lenPadding := MaxGroupMessageLength - len(message)
padding := make([]byte, lenPadding)
getRandomness(&padding)
hexGroupID, err := hex.DecodeString(group.GroupID)
if err != nil {
return nil, nil, nil, err
}
prevSigBytes, err := base64.StdEncoding.DecodeString(prevSig)
if err != nil {
return nil, nil, nil, err
}
dm := &groups.DecryptedGroupMessage{
Onion: author.Hostname(),
Text: message,
SignedGroupID: hexGroupID,
Timestamp: uint64(timestamp),
PreviousMessageSig: prevSigBytes,
Padding: padding[:],
}
ciphertext, err := group.EncryptMessage(dm)
if err != nil {
return nil, nil, nil, err
}
serialized, _ := json.Marshal(dm)
signature := author.Sign([]byte(group.GroupID + group.GroupServer + base64.StdEncoding.EncodeToString(serialized)))
return ciphertext, signature, dm, nil
}

View File

@ -4,13 +4,15 @@ import (
"crypto/sha256"
"cwtch.im/cwtch/protocol/groups"
"strings"
"sync"
"testing"
"time"
)
func TestGroup(t *testing.T) {
g, _ := NewGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
g, err := NewGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
if err != nil {
t.Fatalf("Group with real group server should not fail")
}
dgm := &groups.DecryptedGroupMessage{
Onion: "onion",
Text: "Hello World!",
@ -38,15 +40,11 @@ func TestGroup(t *testing.T) {
encMessage, _ := g.EncryptMessage(dgm)
ok, message := g.DecryptMessage(encMessage)
if !ok || message.Text != "Hello World!" {
if (!ok || message == nil) || message.Text != "Hello World!" {
t.Errorf("group encryption was invalid, or returned wrong message decrypted:%v message:%v", ok, message)
return
}
g.SetAttribute("test", "test_value")
value, exists := g.GetAttribute("test")
if !exists || value != "test_value" {
t.Errorf("Custom Attribute Should have been set, instead %v %v", exists, value)
}
t.Logf("Got message %v", message)
}
@ -61,17 +59,12 @@ func TestGroupErr(t *testing.T) {
func TestGroupValidation(t *testing.T) {
group := &Group{
GroupID: "",
GroupKey: [32]byte{},
GroupServer: "",
Timeline: Timeline{},
Accepted: false,
IsCompromised: false,
Attributes: nil,
lock: sync.Mutex{},
LocalID: "",
State: "",
Version: 0,
GroupID: "",
GroupKey: [32]byte{},
GroupServer: "",
Timeline: Timeline{},
LocalID: "",
Version: 0,
}
invite, _ := group.Invite()
@ -83,7 +76,10 @@ func TestGroupValidation(t *testing.T) {
t.Logf("Error: %v", err)
// Generate a valid group but replace the group server...
group, _ = NewGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
group, err = NewGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
if err != nil {
t.Fatalf("Group with real group server should not fail")
}
group.GroupServer = "tcnkoch4nyr3cldkemejtkpqok342rbql6iclnjjs3ndgnjgufzyxvqd"
invite, _ = group.Invite()
_, err = ValidateInvite(invite)
@ -94,7 +90,10 @@ func TestGroupValidation(t *testing.T) {
t.Logf("Error: %v", err)
// Generate a valid group but replace the group key...
group, _ = NewGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
group, err = NewGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
if err != nil {
t.Fatalf("Group with real group server should not fail")
}
group.GroupKey = sha256.Sum256([]byte{})
invite, _ = group.Invite()
_, err = ValidateInvite(invite)

110
model/groups_test.go Normal file
View File

@ -0,0 +1,110 @@
package model_test
import (
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/protocol/groups"
"encoding/base64"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("group models", func() {
var (
newgroup *model.Group
anothergroup *model.Group
dgm groups.DecryptedGroupMessage
alice primitives.Identity
)
BeforeEach(func() {
newgroup, _ = model.NewGroup("iikv7tizbyxc42rsagnjxss65h3nfiwrkkoiikh7ui27r5xkav7gzuid")
anothergroup, _ = model.NewGroup("iikv7tizbyxc42rsagnjxss65h3nfiwrkkoiikh7ui27r5xkav7gzuid")
alice, _ = primitives.InitializeEphemeralIdentity()
dgm = groups.DecryptedGroupMessage{
Text: "hello world",
Onion: "some random onion",
Timestamp: 0,
SignedGroupID: nil,
PreviousMessageSig: nil,
Padding: nil,
}
})
Context("on creation of a group", func() {
It("should pass the cryptographic check", func() {
Expect(newgroup.CheckGroup()).To(Equal(true))
})
})
Context("after generating an invite", func() {
It("should validate", func() {
invite, err := newgroup.Invite()
Expect(err).NotTo(HaveOccurred())
anotherGroup, err := model.ValidateInvite(invite)
Expect(err).NotTo(HaveOccurred())
Expect(anotherGroup.GroupID).To(Equal(newgroup.GroupID))
Expect(anotherGroup.GroupName).To(Equal(newgroup.GroupName))
Expect(anotherGroup.SharedKey).To(Equal(newgroup.GroupKey[:]))
})
})
Context("when encrypting a message", func() {
Context("decrypting with the same group", func() {
It("should succeed", func() {
ciphertext, err := newgroup.EncryptMessage(&dgm)
Expect(err).NotTo(HaveOccurred())
success, decryptedMessage := newgroup.DecryptMessage(ciphertext)
Expect(success).To(Equal(true))
Expect(decryptedMessage.Text).To(Equal(dgm.Text))
Expect(decryptedMessage.Onion).To(Equal(dgm.Onion))
})
})
Context("decrypting with a different group", func() {
It("should fail", func() {
ciphertext, err := newgroup.EncryptMessage(&dgm)
Expect(err).NotTo(HaveOccurred())
success, decryptedMessage := anothergroup.DecryptMessage(ciphertext)
Expect(success).To(Equal(false))
Expect(decryptedMessage).To(BeNil())
})
})
})
Context("when alice encrypts a message to new group", func() {
It("should succeed and bob should succeed in decrypting it", func() {
ciphertext, sign, _, err := model.EncryptMessageToGroup("hello world", alice, newgroup, base64.StdEncoding.EncodeToString([]byte("hello world")))
Expect(err).NotTo(HaveOccurred())
success, dgm := newgroup.AttemptDecryption(ciphertext, sign)
Expect(success).To(BeTrue())
Expect(dgm.Text).To(Equal("hello world"))
})
})
Context("when alice encrypts a message to new group", func() {
It("should succeed and eve should fail in decrypting it", func() {
ciphertext, sign, _, err := model.EncryptMessageToGroup("hello world", alice, newgroup, base64.StdEncoding.EncodeToString([]byte("hello world")))
Expect(err).NotTo(HaveOccurred())
success, dgm := anothergroup.AttemptDecryption(ciphertext, sign)
Expect(success).To(BeFalse())
Expect(dgm).To(BeNil())
})
})
Context("when alice encrypts a message to new group", func() {
Context("and the server messes with the signature", func() {
It("bob should be unable to verify the message with the wrong signature", func() {
ciphertext, _, _, err := model.EncryptMessageToGroup("hello world", alice, newgroup, base64.StdEncoding.EncodeToString([]byte("hello world")))
Expect(err).NotTo(HaveOccurred())
success, dgm := newgroup.AttemptDecryption(ciphertext, []byte("bad signature"))
Expect(success).To(BeFalse())
Expect(dgm).To(BeNil())
})
})
})
})

View File

@ -99,7 +99,7 @@ func (t *Timeline) SetMessages(messages []Message) {
// GetMessagesByHash attempts to find messages that match the given
// content hash in the timeline. If successful it returns a list of messages as well as their local index
//, on failure it returns an error.
// , on failure it returns an error.
// We return a list of messages because content hashes are not guaranteed to be unique from a given Peer. This allows
// us to do things like: ensure that reply-to and quotes reference the last seen message from the message they are quoted
// in or detect duplicate messages from a peer.
@ -186,10 +186,18 @@ func (t *Timeline) Insert(mi *Message) int {
// check that we haven't seen this message before (this has no impact on p2p messages, but is essential for
// group messages)
idx, exists := t.signatureCache[base64.StdEncoding.EncodeToString(mi.Signature)]
if exists {
t.Messages[idx].Acknowledged = true
return idx
// FIXME: The below code now checks if the message has a signature. If it doesn't then skip duplication check.
// We do this because p2p messages right now do not have a signature, and so many p2p messages are not stored
// with a signature. In the future in hybrid groups this check will go away as all timelines will use the same
// underlying protocol.
// This is currently safe to do because p2p does not rely on signatures and groups will verify the signature of
// messages prior to generating an event to include them in the timeline.
if len(mi.Signature) != 0 {
idx, exists := t.signatureCache[base64.StdEncoding.EncodeToString(mi.Signature)]
if exists {
t.Messages[idx].Acknowledged = true
return idx
}
}
// update the message store

View File

@ -1,127 +0,0 @@
package model
import (
"strconv"
"testing"
"time"
)
func TestMessagePadding(t *testing.T) {
// Setup the Group
sarah := GenerateNewProfile("Sarah")
alice := GenerateNewProfile("Alice")
sarah.AddContact(alice.Onion, &alice.PublicProfile)
alice.AddContact(sarah.Onion, &sarah.PublicProfile)
gid, invite, _ := alice.StartGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
sarah.ProcessInvite(invite)
group := alice.GetGroup(gid)
c1, s1, err := sarah.EncryptMessageToGroup("Hello World 1", group.GroupID)
t.Logf("Length of Encrypted Message: %v %v", len(c1), err)
alice.AttemptDecryption(c1, s1)
c2, s2, _ := alice.EncryptMessageToGroup("Hello World 2", group.GroupID)
t.Logf("Length of Encrypted Message: %v", len(c2))
alice.AttemptDecryption(c2, s2)
c3, s3, _ := alice.EncryptMessageToGroup("Hello World 3", group.GroupID)
t.Logf("Length of Encrypted Message: %v", len(c3))
alice.AttemptDecryption(c3, s3)
c4, s4, _ := alice.EncryptMessageToGroup("Hello World this is a much longer message 3", group.GroupID)
t.Logf("Length of Encrypted Message: %v", len(c4))
alice.AttemptDecryption(c4, s4)
}
func TestTranscriptConsistency(t *testing.T) {
timeline := new(Timeline)
// Setup the Group
sarah := GenerateNewProfile("Sarah")
alice := GenerateNewProfile("Alice")
sarah.AddContact(alice.Onion, &alice.PublicProfile)
alice.AddContact(sarah.Onion, &sarah.PublicProfile)
// The lightest weight server entry possible (usually we would import a key bundle...)
sarah.AddContact("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd", &PublicProfile{Attributes: map[string]string{string(KeyTypeServerOnion): "2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd"}})
gid, invite, _ := alice.StartGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
sarah.ProcessInvite(invite)
group := alice.GetGroup(gid)
t.Logf("group: %v, sarah %v", group, sarah)
c1, s1, _ := alice.EncryptMessageToGroup("Hello World 1", group.GroupID)
t.Logf("Length of Encrypted Message: %v", len(c1))
alice.AttemptDecryption(c1, s1)
c2, s2, _ := alice.EncryptMessageToGroup("Hello World 2", group.GroupID)
t.Logf("Length of Encrypted Message: %v", len(c2))
alice.AttemptDecryption(c2, s2)
c3, s3, _ := alice.EncryptMessageToGroup("Hello World 3", group.GroupID)
t.Logf("Length of Encrypted Message: %v", len(c3))
alice.AttemptDecryption(c3, s3)
time.Sleep(time.Second * 1)
c4, s4, _ := alice.EncryptMessageToGroup("Hello World 4", group.GroupID)
t.Logf("Length of Encrypted Message: %v", len(c4))
alice.AttemptDecryption(c4, s4)
c5, s5, _ := alice.EncryptMessageToGroup("Hello World 5", group.GroupID)
t.Logf("Length of Encrypted Message: %v", len(c5))
_, _, m1, _ := sarah.AttemptDecryption(c1, s1)
sarah.AttemptDecryption(c1, s1) // Try a duplicate
_, _, m2, _ := sarah.AttemptDecryption(c2, s2)
_, _, m3, _ := sarah.AttemptDecryption(c3, s3)
_, _, m4, _ := sarah.AttemptDecryption(c4, s4)
_, _, m5, _ := sarah.AttemptDecryption(c5, s5)
// Now we simulate a client receiving these Messages completely out of order
timeline.Insert(m1)
timeline.Insert(m5)
timeline.Insert(m4)
timeline.Insert(m3)
timeline.Insert(m2)
for i, m := range group.GetTimeline() {
if m.Message != "Hello World "+strconv.Itoa(i+1) {
t.Fatalf("Timeline Out of Order!: %v %v", i, m)
}
t.Logf("Messages %v: %v %x %x", i, m.Message, m.Signature, m.PreviousMessageSig)
}
// Test message by hash lookup...
hash := timeline.calculateHash(*m5)
t.Logf("Looking up %v ", hash)
for key, msgs := range timeline.hashCache {
t.Logf("%v %v", key, msgs)
}
// check a real message..
msgs, err := timeline.GetMessagesByHash(hash)
if err != nil || len(msgs) != 1 {
t.Fatalf("looking up message by hash %v should have not errored: %v", hash, err)
} else if msgs[0].Message.Message != m5.Message {
t.Fatalf("%v != %v", msgs[0].Message, m5.Message)
}
// Check a non existed hash... error if there is no error
_, err = timeline.GetMessagesByHash("not a real hash")
if err == nil {
t.Fatalf("looking up message by hash %v should have errored: %v", hash, err)
}
}

25
model/message_utils.go Normal file
View File

@ -0,0 +1,25 @@
package model
import (
"crypto/sha256"
"encoding/base64"
"encoding/json"
)
// CalculateContentHash derives a hash using the author and the message body. It is intended to be
// globally referencable in the context of a single conversation
func CalculateContentHash(author string, messageBody string) string {
content := []byte(author + messageBody)
contentBasedHash := sha256.Sum256(content)
return base64.StdEncoding.EncodeToString(contentBasedHash[:])
}
func DeserializeMessage(message string) (*MessageWrapper, error) {
var cm MessageWrapper
err := json.Unmarshal([]byte(message), &cm)
if err != nil {
return nil, err
}
return &cm, err
}

13
model/model_suite_test.go Normal file
View File

@ -0,0 +1,13 @@
package model_test
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestModel(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Model Suite")
}

View File

@ -1,9 +1,41 @@
package model
import (
"time"
)
// MessageWrapper is the canonical Cwtch overlay wrapper
type MessageWrapper struct {
Overlay int `json:"o"`
Data string `json:"d"`
// when the data was assembled
SendTime *time.Time `json:"s,omitempty"`
// when the data was transmitted (by protocol engine e.g. over Tor)
TransitTime *time.Time `json:"t,omitempty"`
// when the data was received
RecvTime *time.Time `json:"r,omitempty"`
}
// Channel is defined as being the last 3 bits of the overlay id
// Channel 0 is reserved for the main conversation
// Channel 2 is reserved for conversation admin (managed groups)
// Channel 7 is reserved for streams (no ack, no store)
func (mw MessageWrapper) Channel() int {
// 1024 / 0x400 is the start of new channel overlays
if mw.Overlay > 1024 {
return mw.Overlay & 0x07
}
// for backward compatibilty all overlays less than 0x400 i.e. 1024 are
// mapped to channel 0 regardless of their channel status.
return 0
}
// If Overlay is a Stream Message it should not be ackd, or stored.
func (mw MessageWrapper) IsStream() bool {
return mw.Channel() == 0x07
}
// OverlayChat is the canonical identifier for chat overlays
@ -17,3 +49,6 @@ const OverlayInviteGroup = 101
// OverlayFileSharing is the canonical identifier for the file sharing overlay
const OverlayFileSharing = 200
// ManageGroupEvent is the canonical identifier for the manage group overlay
const OverlayManageGroupEvent = 0x402

View File

@ -2,25 +2,16 @@ package model
import (
"crypto/rand"
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/model/constants"
"cwtch.im/cwtch/protocol/groups"
"encoding/base32"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"git.openprivacy.ca/openprivacy/connectivity/tor"
"golang.org/x/crypto/ed25519"
"io"
"path/filepath"
"strings"
"sync"
"time"
)
// Authorization is a type determining client assigned authorization to a peer
// Deprecated - Only used for Importing legacy profile formats
// Still used in some APIs in UI but will be replaced prior to full deprecation
type Authorization string
const (
@ -33,6 +24,7 @@ const (
)
// PublicProfile is a local copy of a CwtchIdentity
// Deprecated - Only used for Importing legacy profile formats
type PublicProfile struct {
Name string
Ed25519PublicKey ed25519.PublicKey
@ -48,6 +40,7 @@ type PublicProfile struct {
}
// Profile encapsulates all the attributes necessary to be a Cwtch Peer.
// Deprecated - Only used for Importing legacy profile formats
type Profile struct {
PublicProfile
Contacts map[string]*PublicProfile
@ -59,475 +52,19 @@ type Profile struct {
// TODO: Should this be per server?
const MaxGroupMessageLength = 1800
func getRandomness(arr *[]byte) {
if _, err := io.ReadFull(rand.Reader, (*arr)[:]); err != nil {
// If we can't do randomness, just crash something is very very wrong and we are not going
// to resolve it here....
panic(err.Error())
}
}
// GenerateRandomID generates a random 16 byte hex id code
func GenerateRandomID() string {
randBytes := make([]byte, 16)
rand.Read(randBytes)
return filepath.Join(hex.EncodeToString(randBytes))
}
func (p *PublicProfile) init() {
if p.Attributes == nil {
p.Attributes = make(map[string]string)
}
p.UnacknowledgedMessages = make(map[string]int)
p.LocalID = GenerateRandomID()
}
// SetAttribute allows applications to store arbitrary configuration info at the profile level.
func (p *PublicProfile) SetAttribute(name string, value string) {
p.lock.Lock()
defer p.lock.Unlock()
p.Attributes[name] = value
}
// IsServer returns true if the profile is associated with a server.
func (p *PublicProfile) IsServer() (isServer bool) {
_, isServer = p.GetAttribute(string(KeyTypeServerOnion))
return
}
// GetAttribute returns the value of a value set with SetCustomAttribute. If no such value has been set exists is set to false.
func (p *PublicProfile) GetAttribute(name string) (value string, exists bool) {
p.lock.Lock()
defer p.lock.Unlock()
value, exists = p.Attributes[name]
return
}
// GenerateNewProfile creates a new profile, with new encryption and signing keys, and a profile name.
func GenerateNewProfile(name string) *Profile {
p := new(Profile)
p.init()
p.Name = name
pub, priv, _ := ed25519.GenerateKey(rand.Reader)
p.Ed25519PublicKey = pub
p.Ed25519PrivateKey = priv
p.Onion = tor.GetTorV3Hostname(pub)
p.Contacts = make(map[string]*PublicProfile)
p.Contacts[p.Onion] = &p.PublicProfile
p.Groups = make(map[string]*Group)
return p
}
// AddContact allows direct manipulation of cwtch contacts
func (p *Profile) AddContact(onion string, profile *PublicProfile) {
p.lock.Lock()
profile.init()
// We expect callers to verify addresses before we get to this point, so if this isn't a
// valid address this is a noop.
if tor.IsValidHostname(onion) {
decodedPub, err := base32.StdEncoding.DecodeString(strings.ToUpper(onion[:56]))
if err == nil {
profile.Ed25519PublicKey = ed25519.PublicKey(decodedPub[:32])
p.Contacts[onion] = profile
}
}
p.lock.Unlock()
}
// UpdateMessageFlags updates the flags stored with a message
func (p *Profile) UpdateMessageFlags(handle string, mIdx int, flags uint64) {
p.lock.Lock()
defer p.lock.Unlock()
if contact, exists := p.Contacts[handle]; exists {
if len(contact.Timeline.Messages) > mIdx {
contact.Timeline.Messages[mIdx].Flags = flags
}
} else if group, exists := p.Groups[handle]; exists {
if len(group.Timeline.Messages) > mIdx {
group.Timeline.Messages[mIdx].Flags = flags
}
}
}
// DeleteContact deletes a peer contact
func (p *Profile) DeleteContact(onion string) {
p.lock.Lock()
defer p.lock.Unlock()
delete(p.Contacts, onion)
}
// DeleteGroup deletes a group
func (p *Profile) DeleteGroup(groupID string) {
p.lock.Lock()
defer p.lock.Unlock()
delete(p.Groups, groupID)
}
// RejectInvite rejects and removes a group invite
func (p *Profile) RejectInvite(groupID string) {
p.lock.Lock()
delete(p.Groups, groupID)
p.lock.Unlock()
}
// AddSentMessageToContactTimeline allows the saving of a message sent via a direct connection chat to the profile.
func (p *Profile) AddSentMessageToContactTimeline(onion string, messageTxt string, sent time.Time, eventID string) *Message {
p.lock.Lock()
defer p.lock.Unlock()
contact, ok := p.Contacts[onion]
if ok {
now := time.Now()
sig := p.SignMessage(onion + messageTxt + sent.String() + now.String())
message := &Message{PeerID: p.Onion, Message: messageTxt, Timestamp: sent, Received: now, Signature: sig, Acknowledged: false}
if contact.UnacknowledgedMessages == nil {
contact.UnacknowledgedMessages = make(map[string]int)
}
contact.Timeline.Insert(message)
contact.UnacknowledgedMessages[eventID] = contact.Timeline.Len() - 1
return message
}
return nil
}
// AddMessageToContactTimeline allows the saving of a message sent via a direct connection chat to the profile.
func (p *Profile) AddMessageToContactTimeline(onion string, messageTxt string, sent time.Time) (message *Message) {
p.lock.Lock()
defer p.lock.Unlock()
contact, ok := p.Contacts[onion]
// We don't really need a Signature here, but we use it to maintain order
now := time.Now()
sig := p.SignMessage(onion + messageTxt + sent.String() + now.String())
if ok {
message = &Message{PeerID: onion, Message: messageTxt, Timestamp: sent, Received: now, Signature: sig, Acknowledged: true}
contact.Timeline.Insert(message)
}
return
}
// ErrorSentMessageToPeer sets a sent message's error message and removes it from the unacknowledged list
func (p *Profile) ErrorSentMessageToPeer(onion string, eventID string, error string) int {
p.lock.Lock()
defer p.lock.Unlock()
contact, ok := p.Contacts[onion]
if ok {
mIdx, ok := contact.UnacknowledgedMessages[eventID]
if ok {
contact.Timeline.Messages[mIdx].Error = error
delete(contact.UnacknowledgedMessages, eventID)
return mIdx
}
}
return -1
}
// AckSentMessageToPeer sets mesage to a peer as acknowledged
func (p *Profile) AckSentMessageToPeer(onion string, eventID string) int {
p.lock.Lock()
defer p.lock.Unlock()
contact, ok := p.Contacts[onion]
if ok {
mIdx, ok := contact.UnacknowledgedMessages[eventID]
if ok {
contact.Timeline.Messages[mIdx].Acknowledged = true
delete(contact.UnacknowledgedMessages, eventID)
return mIdx
}
}
return -1
}
// AddGroupSentMessageError searches matching groups for the message by sig and marks it as an error
func (p *Profile) AddGroupSentMessageError(groupID string, signature []byte, error string) {
p.lock.Lock()
defer p.lock.Unlock()
group, exists := p.Groups[groupID]
if exists {
group.ErrorSentMessage(signature, error)
}
}
// AcceptInvite accepts a group invite
func (p *Profile) AcceptInvite(groupID string) (err error) {
p.lock.Lock()
defer p.lock.Unlock()
group, ok := p.Groups[groupID]
if ok {
group.Accepted = true
} else {
err = errors.New("group does not exist")
}
return
}
// GetGroups returns an unordered list of group IDs associated with this profile.
func (p *Profile) GetGroups() []string {
p.lock.Lock()
defer p.lock.Unlock()
var keys []string
for onion := range p.Groups {
keys = append(keys, onion)
}
return keys
}
// GetContacts returns an unordered list of contact onions associated with this profile.
func (p *Profile) GetContacts() []string {
p.lock.Lock()
defer p.lock.Unlock()
var keys []string
for onion := range p.Contacts {
if onion != p.Onion {
keys = append(keys, onion)
}
}
return keys
}
// SetContactAuthorization sets the authoirization level of a peer
func (p *Profile) SetContactAuthorization(onion string, auth Authorization) (err error) {
p.lock.Lock()
defer p.lock.Unlock()
contact, ok := p.Contacts[onion]
if ok {
contact.Authorization = auth
} else {
err = errors.New("peer does not exist")
}
return
}
// GetContactAuthorization returns the contact's authorization level
func (p *Profile) GetContactAuthorization(onion string) Authorization {
p.lock.Lock()
defer p.lock.Unlock()
contact, ok := p.Contacts[onion]
if ok {
return contact.Authorization
}
return AuthUnknown
}
// ContactsAuthorizations calculates a list of Peers who are at the supplied auth levels
func (p *Profile) ContactsAuthorizations(authorizationFilter ...Authorization) map[string]Authorization {
authorizations := map[string]Authorization{}
for _, contact := range p.GetContacts() {
c, _ := p.GetContact(contact)
authorizations[c.Onion] = c.Authorization
}
return authorizations
}
// GetContact returns a contact if the profile has it
func (p *Profile) GetContact(onion string) (*PublicProfile, bool) {
p.lock.Lock()
defer p.lock.Unlock()
contact, ok := p.Contacts[onion]
return contact, ok
}
// VerifyGroupMessage confirms the authenticity of a message given an sender onion, message and signature.
// The goal of this function is 2-fold:
// 1. We confirm that the sender referenced in the group text is the actual sender of the message (or at least
// knows the senders private key)
// 2. Secondly, we confirm that the sender sent the message to a particular group id on a specific server (it doesn't
// matter if we actually received this message from the server or from a hybrid protocol, all that matters is
// that the sender and receivers agree that this message was intended for the group
// The 2nd point is important as it prevents an attack documented in the original Cwtch paper (and later at
// https://docs.openprivacy.ca/cwtch-security-handbook/groups.html) in which a malicious profile sets up 2 groups
// on two different servers with the same key and then forwards messages between them to convince the parties in
// each group that they are actually in one big group (with the intent to later censor and/or selectively send messages
// to each group).
func (p *Profile) VerifyGroupMessage(onion string, groupID string, message string, signature []byte) bool {
group := p.GetGroup(groupID)
if group == nil {
return false
}
// We use our group id, a known reference server and the ciphertext of the message.
m := groupID + group.GroupServer + message
// If the message is ostensibly from us then we check it against our public key...
if onion == p.Onion {
return ed25519.Verify(p.Ed25519PublicKey, []byte(m), signature)
}
// Otherwise we derive the public key from the sender and check it against that.
decodedPub, err := base32.StdEncoding.DecodeString(strings.ToUpper(onion))
if err == nil && len(decodedPub) >= 32 {
return ed25519.Verify(decodedPub[:32], []byte(m), signature)
}
return false
}
// SignMessage takes a given message and returns an Ed21159 signature
func (p *Profile) SignMessage(message string) []byte {
sig := ed25519.Sign(p.Ed25519PrivateKey, []byte(message))
return sig
}
// StartGroup when given a server, creates a new Group under this profile and returns the group id an a precomputed
// invite which can be sent on the wire.
func (p *Profile) StartGroup(server string) (groupID string, invite string, err error) {
group, err := NewGroup(server)
if err != nil {
return "", "", err
}
groupID = group.GroupID
invite, err = group.Invite()
p.lock.Lock()
defer p.lock.Unlock()
p.Groups[group.GroupID] = group
return
}
// GetGroup a pointer to a Group by the group Id, returns nil if no group found.
func (p *Profile) GetGroup(groupID string) (g *Group) {
p.lock.Lock()
defer p.lock.Unlock()
g = p.Groups[groupID]
return
}
// ProcessInvite validates a group invite and adds a new group invite to the profile if it is valid.
// returns the new group ID on success, error on fail.
func (p *Profile) ProcessInvite(invite string) (string, error) {
gci, err := ValidateInvite(invite)
if err == nil {
if server, exists := p.GetContact(gci.ServerHost); !exists || !server.IsServer() {
return "", fmt.Errorf("unknown server. a server key bundle needs to be imported before this group can be verified")
}
group := new(Group)
group.Version = CurrentGroupVersion
group.GroupID = gci.GroupID
group.LocalID = GenerateRandomID()
copy(group.GroupKey[:], gci.SharedKey[:])
group.GroupServer = gci.ServerHost
group.Accepted = false
group.Attributes = make(map[string]string)
group.Attributes[attr.GetLocalScope(constants.Name)] = gci.GroupName
p.AddGroup(group)
return gci.GroupID, nil
}
return "", err
}
// AddGroup is a convenience method for adding a group to a profile.
func (p *Profile) AddGroup(group *Group) {
p.lock.Lock()
defer p.lock.Unlock()
_, exists := p.Groups[group.GroupID]
if !exists {
p.Groups[group.GroupID] = group
}
}
// AttemptDecryption takes a ciphertext and signature and attempts to decrypt it under known groups.
// If successful, adds the message to the group's timeline
func (p *Profile) AttemptDecryption(ciphertext []byte, signature []byte) (bool, string, *Message, int) {
for _, group := range p.Groups {
success, dgm := group.DecryptMessage(ciphertext)
if success {
// Attempt to serialize this message
serialized, err := json.Marshal(dgm)
// Someone send a message that isn't a valid Decrypted Group Message. Since we require this struct in orer
// to verify the message, we simply ignore it.
if err != nil {
return false, group.GroupID, nil, -1
}
// This now requires knowledge of the Sender, the Onion and the Specific Decrypted Group Message (which should only
// be derivable from the cryptographic key) which contains many unique elements such as the time and random padding
verified := p.VerifyGroupMessage(dgm.Onion, group.GroupID, base64.StdEncoding.EncodeToString(serialized), signature)
if !verified {
// An earlier version of this protocol mistakenly signed the ciphertext of the message
// instead of the serialized decrypted group message.
// This has 2 issues:
// 1. A server with knowledge of group members public keys AND the Group ID would be able to detect valid messages
// 2. It made the metadata-security of a group dependent on keeping the cryptographically derived Group ID secret.
// While not awful, it also isn't good. For Version 3 groups only we permit Cwtch to check this older signature
// structure in a backwards compatible way for the duration of the Groups Experiment.
// TODO: Delete this check when Groups are no long Experimental
if group.Version == 3 {
verified = p.VerifyGroupMessage(dgm.Onion, group.GroupID, string(ciphertext), signature)
}
}
// So we have a message that has a valid group key, but the signature can't be verified.
// The most obvious explanation for this is that the group key has been compromised (or we are in an open group and the server is being malicious)
// Either way, someone who has the private key is being detectably bad so we are just going to throw this message away and mark the group as Compromised.
if !verified {
group.Compromised()
return false, group.GroupID, nil, -1
}
message, index := group.AddMessage(dgm, signature)
return true, group.GroupID, message, index
}
}
// If we couldn't find a group to decrypt the message with we just return false. This is an expected case
return false, "", nil, -1
}
func getRandomness(arr *[]byte) {
if _, err := io.ReadFull(rand.Reader, (*arr)[:]); err != nil {
if err != nil {
// If we can't do randomness, just crash something is very very wrong and we are not going
// to resolve it here....
panic(err.Error())
}
}
}
// EncryptMessageToGroup when given a message and a group, encrypts and signs the message under the group and
// profile
func (p *Profile) EncryptMessageToGroup(message string, groupID string) ([]byte, []byte, error) {
if len(message) > MaxGroupMessageLength {
return nil, nil, errors.New("group message is too long")
}
group := p.GetGroup(groupID)
if group != nil {
timestamp := time.Now().Unix()
// Select the latest message from the timeline as a reference point.
var prevSig []byte
if len(group.Timeline.Messages) > 0 {
prevSig = group.Timeline.Messages[len(group.Timeline.Messages)-1].Signature
} else {
prevSig = []byte(group.GroupID)
}
lenPadding := MaxGroupMessageLength - len(message)
padding := make([]byte, lenPadding)
getRandomness(&padding)
hexGroupID, err := hex.DecodeString(group.GroupID)
if err != nil {
return nil, nil, err
}
dm := &groups.DecryptedGroupMessage{
Onion: p.Onion,
Text: message,
SignedGroupID: hexGroupID,
Timestamp: uint64(timestamp),
PreviousMessageSig: prevSig,
Padding: padding[:],
}
ciphertext, err := group.EncryptMessage(dm)
if err != nil {
return nil, nil, err
}
serialized, _ := json.Marshal(dm)
signature := p.SignMessage(groupID + group.GroupServer + base64.StdEncoding.EncodeToString(serialized))
group.AddSentMessage(dm, signature)
return ciphertext, signature, nil
}
return nil, nil, errors.New("group does not exist")
return hex.EncodeToString(randBytes)
}
// GetCopy returns a full deep copy of the Profile struct and its members (timeline inclusion control by arg)
@ -541,11 +78,19 @@ func (p *Profile) GetCopy(timeline bool) *Profile {
if timeline {
for groupID := range newp.Groups {
newp.Groups[groupID].Timeline = *p.Groups[groupID].Timeline.GetCopy()
if group, exists := newp.Groups[groupID]; exists {
if pGroup, exists := p.Groups[groupID]; exists {
group.Timeline = *(pGroup).Timeline.GetCopy()
}
}
}
for peerID := range newp.Contacts {
newp.Contacts[peerID].Timeline = *p.Contacts[peerID].Timeline.GetCopy()
if peer, exists := newp.Contacts[peerID]; exists {
if pPeer, exists := p.Contacts[peerID]; exists {
peer.Timeline = *(pPeer).Timeline.GetCopy()
}
}
}
}

View File

@ -1,136 +0,0 @@
package model
import (
"testing"
)
func TestProfileIdentity(t *testing.T) {
sarah := GenerateNewProfile("Sarah")
alice := GenerateNewProfile("Alice")
alice.AddContact(sarah.Onion, &sarah.PublicProfile)
if alice.Contacts[sarah.Onion].Name != "Sarah" {
t.Errorf("alice should have added sarah as a contact %v", alice.Contacts)
}
if len(alice.GetContacts()) != 1 {
t.Errorf("alice should be only contact: %v", alice.GetContacts())
}
alice.SetAttribute("test", "hello world")
value, _ := alice.GetAttribute("test")
if value != "hello world" {
t.Errorf("value from custom attribute should have been 'hello world', instead was: %v", value)
}
t.Logf("%v", alice)
}
func TestTrustPeer(t *testing.T) {
sarah := GenerateNewProfile("Sarah")
alice := GenerateNewProfile("Alice")
sarah.AddContact(alice.Onion, &alice.PublicProfile)
alice.AddContact(sarah.Onion, &sarah.PublicProfile)
alice.SetContactAuthorization(sarah.Onion, AuthApproved)
if alice.GetContactAuthorization(sarah.Onion) != AuthApproved {
t.Errorf("peer should be approved")
}
}
func TestBlockPeer(t *testing.T) {
sarah := GenerateNewProfile("Sarah")
alice := GenerateNewProfile("Alice")
sarah.AddContact(alice.Onion, &alice.PublicProfile)
alice.AddContact(sarah.Onion, &sarah.PublicProfile)
alice.SetContactAuthorization(sarah.Onion, AuthBlocked)
if alice.GetContactAuthorization(sarah.Onion) != AuthBlocked {
t.Errorf("peer should be blocked")
}
if alice.SetContactAuthorization("", AuthUnknown) == nil {
t.Errorf("Seting Auth level of a non existent peer should error")
}
}
func TestAcceptNonExistentGroup(t *testing.T) {
sarah := GenerateNewProfile("Sarah")
sarah.AcceptInvite("doesnotexist")
}
func TestRejectGroupInvite(t *testing.T) {
sarah := GenerateNewProfile("Sarah")
alice := GenerateNewProfile("Alice")
sarah.AddContact(alice.Onion, &alice.PublicProfile)
alice.AddContact(sarah.Onion, &sarah.PublicProfile)
// The lightest weight server entry possible (usually we would import a key bundle...)
sarah.AddContact("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd", &PublicProfile{Attributes: map[string]string{string(KeyTypeServerOnion): "2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd"}})
gid, invite, _ := alice.StartGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
sarah.ProcessInvite(invite)
group := alice.GetGroup(gid)
if len(sarah.Groups) == 1 {
if sarah.GetGroup(group.GroupID).Accepted {
t.Errorf("Group should not be accepted")
}
sarah.RejectInvite(group.GroupID)
if len(sarah.Groups) != 0 {
t.Errorf("Group %v should have been deleted", group.GroupID)
}
return
}
t.Errorf("Group should exist in map")
}
func TestProfileGroup(t *testing.T) {
sarah := GenerateNewProfile("Sarah")
alice := GenerateNewProfile("Alice")
sarah.AddContact(alice.Onion, &alice.PublicProfile)
alice.AddContact(sarah.Onion, &sarah.PublicProfile)
gid, invite, _ := alice.StartGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
// The lightest weight server entry possible (usually we would import a key bundle...)
sarah.AddContact("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd", &PublicProfile{Attributes: map[string]string{string(KeyTypeServerOnion): "2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd"}})
sarah.ProcessInvite(invite)
if len(sarah.GetGroups()) != 1 {
t.Errorf("sarah should only be in 1 group instead: %v", sarah.GetGroups())
}
group := alice.GetGroup(gid)
sarah.AcceptInvite(group.GroupID)
c, s1, _ := sarah.EncryptMessageToGroup("Hello World", group.GroupID)
alice.AttemptDecryption(c, s1)
gid2, invite2, _ := alice.StartGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
sarah.ProcessInvite(invite2)
group2 := alice.GetGroup(gid2)
c2, s2, _ := sarah.EncryptMessageToGroup("Hello World", group2.GroupID)
alice.AttemptDecryption(c2, s2)
_, _, err := sarah.EncryptMessageToGroup(string(make([]byte, MaxGroupMessageLength*2)), group2.GroupID)
if err == nil {
t.Errorf("Overly long message should have returned an error")
}
bob := GenerateNewProfile("bob")
bob.AddContact(alice.Onion, &alice.PublicProfile)
// The lightest weight server entry possible (usually we would import a key bundle...)
bob.AddContact("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd", &PublicProfile{Attributes: map[string]string{string(KeyTypeServerOnion): "2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd"}})
bob.ProcessInvite(invite2)
c3, s3, err := bob.EncryptMessageToGroup("Bobs Message", group2.GroupID)
if err == nil {
ok, _, message, _ := alice.AttemptDecryption(c3, s3)
if !ok {
t.Errorf("Bobs message to the group should be decrypted %v %v", message, ok)
}
eve := GenerateNewProfile("eve")
ok, _, _, _ = eve.AttemptDecryption(c3, s3)
if ok {
t.Errorf("Eves hould not be able to decrypt Messages!")
}
} else {
t.Errorf("Bob failed to encrypt a message to the group")
}
}

File diff suppressed because it is too large Load Diff

1053
peer/cwtchprofilestorage.go Normal file

File diff suppressed because it is too large Load Diff

52
peer/hooks.go Normal file
View File

@ -0,0 +1,52 @@
package peer
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/settings"
)
type ProfileHooks interface {
// EventsToRegister returns a set of events that the extension is interested hooking
EventsToRegister() []event.Type
// ExperimentsToRegister returns a set of experiments that the extension is interested in being notified about
ExperimentsToRegister() []string
// OnEvent is called whenever an event Registered with RegisterEvents is called
OnEvent(event event.Event, profile CwtchPeer)
// OnContactRequestValue is Hooked when a contact sends a request for the given path
OnContactRequestValue(profile CwtchPeer, conversation model.Conversation, eventID string, path attr.ScopedZonedPath)
// OnContactReceiveValue is Hooked after a profile receives a response to a Get/Val Request
OnContactReceiveValue(profile CwtchPeer, conversation model.Conversation, path attr.ScopedZonedPath, value string, exists bool)
// NotifySettingsUpdate allow profile hooks to access configs e.g. download folder
NotifySettingsUpdate(settings settings.GlobalSettings)
}
type ProfileHook struct {
extension ProfileHooks
events map[event.Type]bool
experiments map[string]bool
}
func ConstructHook(extension ProfileHooks) ProfileHook {
events := make(map[event.Type]bool)
for _, e := range extension.EventsToRegister() {
events[e] = true
}
experiments := make(map[string]bool)
for _, experiment := range extension.ExperimentsToRegister() {
experiments[experiment] = true
}
return ProfileHook{
extension,
events,
experiments,
}
}

192
peer/profile_interface.go Normal file
View File

@ -0,0 +1,192 @@
package peer
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/protocol/connections"
"cwtch.im/cwtch/settings"
"git.openprivacy.ca/cwtch.im/tapir/primitives/privacypass"
"git.openprivacy.ca/openprivacy/connectivity"
)
// AccessPeeringState provides access to functions relating to the underlying connections of a peer.
type AccessPeeringState interface {
GetPeerState(string) connections.ConnectionState
}
// ModifyPeeringState is a meta-interface intended to restrict callers to modify-only access to connection peers
type ModifyPeeringState interface {
BlockUnknownConnections()
AllowUnknownConnections()
PeerWithOnion(string)
QueueJoinServer(string)
DisconnectFromPeer(string)
DisconnectFromServer(string)
}
// ModifyContactsAndPeers is a meta-interface intended to restrict a call to reading and modifying contacts
// and peers.
type ModifyContactsAndPeers interface {
ModifyPeeringState
}
// ReadServers provides access to the servers
type ReadServers interface {
GetServers() []string
}
// ModifyGroups provides write-only access add/edit/remove new groups
type ModifyGroups interface {
ImportGroup(string) (int, error)
StartGroup(string, string) (int, error)
}
// ModifyServers provides write-only access to servers
type ModifyServers interface {
AddServer(string) (string, error)
ResyncServer(onion string) error
}
// SendMessages enables a caller to sender messages to a contact
type SendMessages interface {
// SendMessage sends a raw message to the conversation.
// SendMessage is a deprecated public API. Use EnhancedSendMessage instead
SendMessage(conversation int, message string) (int, error)
// EnhancedSendMessage Attempts to Send a Message and Immediately Attempts to Lookup the Message in the Database
EnhancedSendMessage(conversation int, message string) string
SendInviteToConversation(conversationID int, inviteConversationID int) (int, error)
// EnhancedSendInviteMessage Attempts to Send an Invite and Immediately Attempts to Lookup the Message in the Database
EnhancedSendInviteMessage(conversation int, inviteConversationID int) string
SendScopedZonedGetValToContact(conversationID int, scope attr.Scope, zone attr.Zone, key string)
}
// CwtchPeer provides us with a way of testing systems built on top of cwtch without having to
// directly implement a cwtchPeer.
type CwtchPeer interface {
// Core Cwtch Peer Functions that should not be exposed to
// most functions
Init(event.Manager)
GenerateProtocolEngine(acn connectivity.ACN, bus event.Manager, engineHooks connections.EngineHooks) (connections.Engine, error)
AutoHandleEvents(events []event.Type)
Listen()
StartConnections(doPeers, doServers bool)
// Deprecated in 1.10
StartPeersConnections()
// Deprecated in 1.10
StartServerConnections()
Shutdown()
// GetOnion is deprecated. If you find yourself needing to rely on this method it is time
// to consider replacing this with a GetAddress(es) function that can fully expand cwtch beyond the boundaries
// of tor v3 onion services.
// Deprecated
GetOnion() string
// SetScopedZonedAttribute allows the setting of an attribute by scope and zone
// scope.zone.key = value
SetScopedZonedAttribute(scope attr.Scope, zone attr.Zone, key string, value string)
// GetScopedZonedAttribute allows the retrieval of an attribute by scope and zone
// scope.zone.key = value
GetScopedZonedAttribute(scope attr.Scope, zone attr.Zone, key string) (string, bool)
// GetScopedZonedAttributeKeys returns all keys associated with a given scope and zone
GetScopedZonedAttributeKeys(scope attr.Scope, zone attr.Zone) ([]string, error)
AccessPeeringState
ModifyPeeringState
ModifyGroups
ReadServers
ModifyServers
SendMessages
// Import Bundle
ImportBundle(string) error
EnhancedImportBundle(string) string
// New Unified Conversation Interfaces
NewConversation(handle string, acl model.AccessControlList) (int, error)
InitChannel(conversation int, channel int) error
NewContactConversation(handle string, acl model.AccessControl, accepted bool) (int, error)
FetchConversations() ([]*model.Conversation, error)
ArchiveConversation(conversation int)
GetConversationInfo(conversation int) (*model.Conversation, error)
FetchConversationInfo(handle string) (*model.Conversation, error)
// API-level management of conversation access control
UpdateConversationAccessControlList(id int, acl model.AccessControlList) error
EnhancedUpdateConversationAccessControlList(conversation int, acjson string) error
GetConversationAccessControlList(conversation int) (model.AccessControlList, error)
EnhancedGetConversationAccessControlList(conversation int) (string, error)
// Convieniance Functions for ACL Management
AcceptConversation(conversation int) error
BlockConversation(conversation int) error
UnblockConversation(conversation int) error
SetConversationAttribute(conversation int, path attr.ScopedZonedPath, value string) error
GetConversationAttribute(conversation int, path attr.ScopedZonedPath) (string, error)
SetConversationAttributeInt(conversation int, path attr.ScopedZonedPath, value int) error
GetConversationAttributeInt(conversation int, path attr.ScopedZonedPath) (int, error)
DeleteConversation(conversation int) error
// New Unified Conversation Channel Interfaces
GetChannelMessage(conversation int, channel int, id int) (string, model.Attributes, error)
GetChannelMessageCount(conversation int, channel int) (int, error)
GetChannelMessageByContentHash(conversation int, channel int, contenthash string) (int, error)
GetChannelMessageBySignature(conversationID int, channelID int, signature string) (int, error)
GetMostRecentMessages(conversation int, channel int, offset int, limit uint) ([]model.ConversationMessage, error)
UpdateMessageAttribute(conversation int, channel int, id int, key string, value string) error
SearchConversations(pattern string) string
// EnhancedGetMessageById returns a json-encoded enhanced message, suitable for rendering in a UI
EnhancedGetMessageById(conversation int, mid int) string
// EnhancedGetMessageByContentHash returns a json-encoded enhanced message, suitable for rendering in a UI
EnhancedGetMessageByContentHash(conversation int, hash string) string
// EnhancedGetMessages returns a set of json-encoded enhanced messages, suitable for rendering in a UI
EnhancedGetMessages(conversation int, index int, count uint) string
// Server Token APIS
// TODO move these to feature protected interfaces
StoreCachedTokens(tokenServer string, tokens []*privacypass.Token)
// Profile Management
CheckPassword(password string) bool
ChangePassword(oldpassword string, newpassword string, newpasswordAgain string) error
ExportProfile(file string) error
Delete()
PublishEvent(resp event.Event)
RegisterHook(hook ProfileHooks)
UpdateExperiments(enabled bool, experiments map[string]bool)
NotifySettingsUpdate(settings settings.GlobalSettings)
IsFeatureEnabled(featureName string) bool
SignMessage(blob []byte) ([]byte, error)
// Used for Internal Bookkeeping by Extensions, **do not expose in autobindings**
InternalInsertMessage(conversation int, channel int, author string, body string, attributes model.Attributes, signature []byte) (int, error)
}
// EnhancedMessage wraps a Cwtch model.Message with some additional data to reduce calls from the UI.
type EnhancedMessage struct {
model.Message
ID int // the actual ID of the message in the database (not the row number)
LocalIndex int // local index in the DB (row #). Can be empty (most calls supply it) but lookup by hash will fill it
ContentHash string
ContactImage string
Attributes map[string]string
}

13
peer/response.go Normal file
View File

@ -0,0 +1,13 @@
package peer
import "errors"
// Response is a wrapper to better semantically convey the response type...
type Response error
const errorSeparator = "."
// ConstructResponse is a helper function for creating Response structures.
func ConstructResponse(prefix string, error string) Response {
return errors.New(prefix + errorSeparator + error)
}

29
peer/sql_statements.go Normal file
View File

@ -0,0 +1,29 @@
package peer
import (
"database/sql"
"fmt"
)
// SQLCreateTableProfileKeyValue creates the Profile Key Value Table
const SQLCreateTableProfileKeyValue = `create table if not exists profile_kv (KeyType text, KeyName text, KeyValue blob, UNIQUE (KeyType,KeyName));`
// SQLCreateTableConversations creates the Profile Key Value Table
const SQLCreateTableConversations = `create table if not exists conversations (ID integer unique primary key autoincrement, Handle text, Attributes blob, ACL blob, Accepted bool);`
// initializeDatabase executes all the sql statements necessary to construct the base of the database.
// db must be open
func initializeDatabase(db *sql.DB) error {
_, err := db.Exec(SQLCreateTableProfileKeyValue)
if err != nil {
return fmt.Errorf("error On Executing Query: %v %v", SQLCreateTableProfileKeyValue, err)
}
_, err = db.Exec(SQLCreateTableConversations)
if err != nil {
return fmt.Errorf("error On Executing Query: %v %v", SQLCreateTableConversations, err)
}
return nil
}

329
peer/storage.go Normal file
View File

@ -0,0 +1,329 @@
package peer
import (
"archive/tar"
"compress/gzip"
"crypto/rand"
"database/sql"
"encoding/hex"
"errors"
"fmt"
"git.openprivacy.ca/openprivacy/log"
"golang.org/x/crypto/pbkdf2"
"golang.org/x/crypto/sha3"
"io"
"os"
"path"
"path/filepath"
"strings"
)
const versionFile = "VERSION"
const version = "2"
const saltFile = "SALT"
const dbFile = "db"
// CreateKeySalt derives a key and salt from a password: returns key, salt, err
func CreateKeySalt(password string) ([32]byte, [128]byte, error) {
var salt [128]byte
if _, err := io.ReadFull(rand.Reader, salt[:]); err != nil {
log.Errorf("Cannot read from random: %v\n", err)
return [32]byte{}, salt, err
}
dk := pbkdf2.Key([]byte(password), salt[:], 4096, 32, sha3.New512)
var dkr [32]byte
copy(dkr[:], dk)
return dkr, salt, nil
}
// createKey derives a key from a password and salt
func createKey(password string, salt []byte) [32]byte {
dk := pbkdf2.Key([]byte(password), salt, 4096, 32, sha3.New512)
var dkr [32]byte
copy(dkr[:], dk)
return dkr
}
func initV2Directory(directory, password string) ([32]byte, [128]byte, error) {
os.MkdirAll(directory, 0700)
key, salt, err := CreateKeySalt(password)
if err != nil {
log.Errorf("Could not create key for profile store from password: %v\n", err)
return [32]byte{}, [128]byte{}, err
}
if err = os.WriteFile(path.Join(directory, versionFile), []byte(version), 0600); err != nil {
log.Errorf("Could not write version file: %v", err)
return [32]byte{}, [128]byte{}, err
}
if err = os.WriteFile(path.Join(directory, saltFile), salt[:], 0600); err != nil {
log.Errorf("Could not write salt file: %v", err)
return [32]byte{}, [128]byte{}, err
}
return key, salt, nil
}
func openEncryptedDatabase(profileDirectory string, password string, createIfNotExists bool) (*sql.DB, error) {
salt, err := os.ReadFile(path.Join(profileDirectory, saltFile))
if err != nil {
return nil, err
}
key := createKey(password, salt)
dbPath := filepath.Join(profileDirectory, "db")
if !createIfNotExists {
if _, err := os.Stat(dbPath); errors.Is(err, os.ErrNotExist) {
return nil, err
}
}
dbname := fmt.Sprintf("%v?_pragma_key=x'%x'&_pragma_cipher_page_size=8192", dbPath, key)
db, err := sql.Open("sqlite3", dbname)
if err != nil {
log.Errorf("could not open encrypted database", err)
return nil, err
}
return db, nil
}
// CreateEncryptedStorePeer creates a *new* Cwtch Profile backed by an encrypted datastore
func CreateEncryptedStorePeer(profileDirectory string, name string, password string) (CwtchPeer, error) {
log.Debugf("Initializing Encrypted Storage Directory")
_, _, err := initV2Directory(profileDirectory, password)
if err != nil {
return nil, err
}
log.Debugf("Opening Encrypted Database")
db, err := openEncryptedDatabase(profileDirectory, password, true)
if db == nil || err != nil {
return nil, fmt.Errorf("unable to open encrypted database: error: %v", err)
}
log.Debugf("Initializing Database")
err = initializeDatabase(db)
if err != nil {
db.Close()
return nil, err
}
log.Debugf("Creating Cwtch Profile Backed By Encrypted Database")
cps, err := NewCwtchProfileStorage(db, profileDirectory)
if err != nil {
db.Close()
return nil, err
}
return NewProfileWithEncryptedStorage(name, cps), nil
}
// CreateEncryptedStore creates a encrypted datastore
func CreateEncryptedStore(profileDirectory string, password string) (*CwtchProfileStorage, error) {
log.Debugf("Creating Encrypted Database")
db, err := openEncryptedDatabase(profileDirectory, password, true)
if db == nil || err != nil {
return nil, fmt.Errorf("unable to open encrypted database: error: %v", err)
}
log.Debugf("Initializing Database")
err = initializeDatabase(db)
if err != nil {
db.Close()
return nil, err
}
log.Debugf("Creating Cwtch Profile Backed By Encrypted Database")
cps, err := NewCwtchProfileStorage(db, profileDirectory)
if err != nil {
db.Close()
return nil, err
}
return cps, nil
}
// FromEncryptedDatabase constructs a Cwtch Profile from an existing Encrypted Database
func FromEncryptedDatabase(profileDirectory string, password string) (CwtchPeer, error) {
log.Debugf("Loading Encrypted Profile: %v", profileDirectory)
db, err := openEncryptedDatabase(profileDirectory, password, false)
if db == nil || err != nil {
return nil, fmt.Errorf("unable to open encrypted database: error: %v", err)
}
log.Debugf("Initializing Profile from Encrypted Storage")
cps, err := NewCwtchProfileStorage(db, profileDirectory)
if err != nil {
db.Close()
return nil, err
}
return FromEncryptedStorage(cps), nil
}
func ImportProfile(exportedCwtchFile string, profilesDir string, password string) (CwtchPeer, error) {
profileID, err := checkCwtchProfileBackupFile(exportedCwtchFile)
if profileID == "" || err != nil {
log.Errorf("%s is an invalid cwtch backup file: %s", profileID, err)
return nil, err
}
log.Debugf("%s is a valid cwtch backup file", profileID)
profileDBFile := filepath.Join(profilesDir, profileID, dbFile)
log.Debugf("checking %v", profileDBFile)
if _, err := os.Stat(profileDBFile); errors.Is(err, os.ErrNotExist) {
// backup is valid and the profile hasn't been imported yet, time to extract and check the password
profileDir := filepath.Join(profilesDir, profileID)
os.MkdirAll(profileDir, 0700)
err := importCwtchProfileBackupFile(exportedCwtchFile, profilesDir)
if err == nil {
profile, err := FromEncryptedDatabase(profileDir, password)
if err == nil {
return profile, err
}
// Otherwise purge
log.Errorf("error importing profile: %v. removing %s", err, profileDir)
os.RemoveAll(profileDir)
return nil, err
}
return nil, err
}
return nil, fmt.Errorf("%s is already a profile for this app", profileID)
}
func checkCwtchProfileBackupFile(srcFile string) (string, error) {
f, err := os.Open(srcFile)
if err != nil {
return "", err
}
defer f.Close()
gzf, err := gzip.NewReader(f)
if err != nil {
return "", err
}
tarReader := tar.NewReader(gzf)
profileName := ""
for {
header, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
return "", err
}
switch header.Typeflag {
case tar.TypeDir:
return "", errors.New("invalid cwtch backup file")
case tar.TypeReg:
parts := strings.Split(header.Name, "/")
if len(parts) != 2 {
return "", errors.New("invalid header name")
}
dir := parts[0]
profileFileType := parts[1]
_, hexErr := hex.DecodeString(dir)
if dir == "." || dir == ".." || len(dir) != 32 || hexErr != nil {
return "", errors.New("invalid profile name")
}
if profileName == "" {
profileName = dir
}
if dir != profileName {
return "", errors.New("invalid cwtch backup file")
}
if profileFileType != dbFile && profileFileType != saltFile && profileFileType != versionFile {
return "", errors.New("invalid cwtch backup file")
}
default:
return "", errors.New("invalid cwtch backup file")
}
}
return profileName, nil
}
func importCwtchProfileBackupFile(srcFile string, profilesDir string) error {
f, err := os.Open(srcFile)
if err != nil {
return err
}
defer f.Close()
gzf, err := gzip.NewReader(f)
if err != nil {
return err
}
tarReader := tar.NewReader(gzf)
profileName := ""
for {
header, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
switch header.Typeflag {
case tar.TypeDir:
return errors.New("invalid cwtch backup file")
case tar.TypeReg:
// using split here because we deliberately construct these paths in a cross-platform consistent way
parts := strings.Split(header.Name, "/")
if len(parts) != 2 {
return errors.New("invalid header name")
}
dir := parts[0]
base := parts[1]
_, hexErr := hex.DecodeString(dir)
if dir == "." || dir == ".." || len(dir) != 32 || hexErr != nil {
return errors.New("invalid profile name")
}
if profileName == "" {
profileName = dir
}
if dir != profileName {
return errors.New("invalid cwtch backup file")
}
// here we use filepath.Join to construct a valid directory path
outFile, err := os.Create(filepath.Join(profilesDir, dir, base))
if err != nil {
return fmt.Errorf("error importing cwtch profile file: %s", err)
}
defer outFile.Close()
if _, err := io.Copy(outFile, tarReader); err != nil {
return fmt.Errorf("error importing cwtch profile file: %s", err)
}
default:
return errors.New("invalid cwtch backup file")
}
}
return nil
}

View File

@ -4,8 +4,11 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
"git.openprivacy.ca/cwtch.im/tapir/primitives/privacypass"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"cwtch.im/cwtch/event"
@ -24,6 +27,18 @@ import (
"golang.org/x/crypto/ed25519"
)
// 32 from tor/src/app/config/config.c MaxClientCircuitsPending
// we lower a bit because there's a lot of spillage
// - just cus we get a SOCKS timeout doesn't mean tor has stopped trying as a huge sorce
// - potential multiple profiles as a huge source
// - second order connections like token service's second servers aren't tracked in our system adding a few extra periodically
const TorMaxPendingConns = 28
type connectionLockedService struct {
service tapir.Service
connectingLock sync.Mutex
}
type engine struct {
queue event.Queue
@ -35,7 +50,7 @@ type engine struct {
authorizations sync.Map // string(onion) => model.Authorization
// Block Unknown Contacts
blockUnknownContacts bool
blockUnknownContacts atomic.Bool
// Pointer to the Global Event Manager
eventManager event.Manager
@ -46,7 +61,8 @@ type engine struct {
getValRequests sync.Map // [string]string eventID:Data
// Nextgen Tapir Service
ephemeralServices sync.Map // string(onion) => tapir.Service
ephemeralServices map[string]*connectionLockedService //sync.Map // string(onion) => tapir.Service
ephemeralServicesLock sync.Mutex
// Required for listen(), inaccessible from identity
privateKey ed25519.PrivateKey
@ -54,7 +70,10 @@ type engine struct {
// file sharing subsystem is responsible for maintaining active shares and downloads
filesharingSubSystem files.FileSharingSubSystem
shuttingDown bool
tokenManagers sync.Map // [tokenService][]TokenManager
shuttingDown atomic.Bool
onSendMessage func(connection tapir.Connection, message []byte) error
}
// Engine (ProtocolEngine) encapsulates the logic necessary to make and receive Cwtch connections.
@ -68,11 +87,16 @@ type Engine interface {
}
// NewProtocolEngine initializes a new engine that runs Cwtch using the given parameters
func NewProtocolEngine(identity primitives.Identity, privateKey ed25519.PrivateKey, acn connectivity.ACN, eventManager event.Manager, peerAuthorizations map[string]model.Authorization) Engine {
func NewProtocolEngine(identity primitives.Identity, privateKey ed25519.PrivateKey, acn connectivity.ACN, eventManager event.Manager, peerAuthorizations map[string]model.Authorization, engineHooks EngineHooks) Engine {
engine := new(engine)
engine.identity = identity
engine.privateKey = privateKey
engine.ephemeralServices = make(map[string]*connectionLockedService)
engine.queue = event.NewQueue()
// the standard send message function
engine.onSendMessage = engineHooks.SendPeerMessage
go engine.eventHandler()
engine.acn = acn
@ -84,8 +108,8 @@ func NewProtocolEngine(identity primitives.Identity, privateKey ed25519.PrivateK
engine.eventManager = eventManager
engine.eventManager.Subscribe(event.ProtocolEngineStartListen, engine.queue)
engine.eventManager.Subscribe(event.ProtocolEngineShutdown, engine.queue)
engine.eventManager.Subscribe(event.PeerRequest, engine.queue)
engine.eventManager.Subscribe(event.RetryPeerRequest, engine.queue)
engine.eventManager.Subscribe(event.InvitePeerToGroup, engine.queue)
engine.eventManager.Subscribe(event.JoinServer, engine.queue)
engine.eventManager.Subscribe(event.LeaveServer, engine.queue)
@ -94,17 +118,23 @@ func NewProtocolEngine(identity primitives.Identity, privateKey ed25519.PrivateK
engine.eventManager.Subscribe(event.SendGetValMessageToPeer, engine.queue)
engine.eventManager.Subscribe(event.SendRetValMessageToPeer, engine.queue)
engine.eventManager.Subscribe(event.DeleteContact, engine.queue)
engine.eventManager.Subscribe(event.DeleteGroup, engine.queue)
engine.eventManager.Subscribe(event.SetPeerAuthorization, engine.queue)
engine.eventManager.Subscribe(event.UpdateConversationAuthorization, engine.queue)
engine.eventManager.Subscribe(event.BlockUnknownPeers, engine.queue)
engine.eventManager.Subscribe(event.AllowUnknownPeers, engine.queue)
engine.eventManager.Subscribe(event.DisconnectPeerRequest, engine.queue)
engine.eventManager.Subscribe(event.DisconnectServerRequest, engine.queue)
// File Handling
engine.eventManager.Subscribe(event.ShareManifest, engine.queue)
engine.eventManager.Subscribe(event.StopFileShare, engine.queue)
engine.eventManager.Subscribe(event.StopAllFileShares, engine.queue)
engine.eventManager.Subscribe(event.ManifestSizeReceived, engine.queue)
engine.eventManager.Subscribe(event.ManifestSaved, engine.queue)
// Token Server
engine.eventManager.Subscribe(event.MakeAntispamPayment, engine.queue)
for peer, authorization := range peerAuthorizations {
engine.authorizations.Store(peer, authorization)
}
@ -121,26 +151,25 @@ func (e *engine) EventManager() event.Manager {
// eventHandler process events from other subsystems
func (e *engine) eventHandler() {
log.Debugf("restartFlow Launching ProtocolEngine listener")
for {
ev := e.queue.Next()
// optimistic shutdown...
if e.shuttingDown.Load() {
return
}
switch ev.EventType {
case event.StatusRequest:
e.eventManager.Publish(event.Event{EventType: event.ProtocolEngineStatus, EventID: ev.EventID})
case event.PeerRequest:
log.Debugf("restartFlow Handling Peer Request")
if torProvider.IsValidHostname(ev.Data[event.RemotePeer]) {
go e.peerWithOnion(ev.Data[event.RemotePeer])
}
case event.RetryPeerRequest:
// This event allows engine to treat (automated) retry peering requests differently to user-specified
// peer events
if torProvider.IsValidHostname(ev.Data[event.RemotePeer]) {
log.Debugf("Retrying Peer Request: %v", ev.Data[event.RemotePeer])
go e.peerWithOnion(ev.Data[event.RemotePeer])
}
case event.InvitePeerToGroup:
err := e.sendPeerMessage(ev.Data[event.RemotePeer], pmodel.PeerMessage{ID: ev.EventID, Context: event.ContextInvite, Data: []byte(ev.Data[event.GroupInvite])})
if err != nil {
e.eventManager.Publish(event.NewEvent(event.SendMessageToPeerError, map[event.Field]string{event.EventContext: string(event.InvitePeerToGroup), event.RemotePeer: ev.Data[event.RemotePeer], event.EventID: ev.EventID, event.Error: "peer is offline or the connection has yet to finalize"}))
}
case event.JoinServer:
signature, err := base64.StdEncoding.DecodeString(ev.Data[event.Signature])
@ -148,7 +177,18 @@ func (e *engine) eventHandler() {
// will result in a full sync
signature = []byte{}
}
go e.peerWithTokenServer(ev.Data[event.GroupServer], ev.Data[event.ServerTokenOnion], ev.Data[event.ServerTokenY], signature)
// if we have been sent cached tokens, also deserialize them
cachedTokensJson := ev.Data[event.CachedTokens]
var cachedTokens []*privacypass.Token
if len(cachedTokensJson) != 0 {
json.Unmarshal([]byte(cachedTokensJson), &cachedTokens)
}
// create a new token handler...
e.NewTokenHandler(ev.Data[event.ServerTokenOnion], cachedTokens)
go e.peerWithTokenServer(ev.Data[event.GroupServer], ev.Data[event.ServerTokenOnion], ev.Data[event.ServerTokenY], signature, cachedTokens)
case event.MakeAntispamPayment:
go e.makeAntispamPayment(ev.Data[event.GroupServer])
case event.LeaveServer:
e.leaveServer(ev.Data[event.GroupServer])
case event.DeleteContact:
@ -156,11 +196,15 @@ func (e *engine) eventHandler() {
// We remove this peer from out blocklist which will prevent them from contacting us if we have "block unknown peers" turned on.
e.authorizations.Delete(ev.Data[event.RemotePeer])
e.deleteConnection(onion)
case event.DeleteGroup:
// TODO: There isn't a way here to determine if other Groups are using a server connection...
case event.DisconnectPeerRequest:
e.deleteConnection(ev.Data[event.RemotePeer])
case event.DisconnectServerRequest:
e.leaveServer(ev.Data[event.GroupServer])
case event.SendMessageToGroup:
ciphertext, _ := base64.StdEncoding.DecodeString(ev.Data[event.Ciphertext])
signature, _ := base64.StdEncoding.DecodeString(ev.Data[event.Signature])
// launch a goroutine to post to the server
go e.sendMessageToGroup(ev.Data[event.GroupID], ev.Data[event.GroupServer], ciphertext, signature, 0)
case event.SendMessageToPeer:
// TODO: remove this passthrough once the UI is integrated.
@ -169,18 +213,25 @@ func (e *engine) eventHandler() {
context = event.ContextRaw
}
if err := e.sendPeerMessage(ev.Data[event.RemotePeer], pmodel.PeerMessage{ID: ev.EventID, Context: context, Data: []byte(ev.Data[event.Data])}); err != nil {
e.eventManager.Publish(event.NewEvent(event.SendMessageToPeerError, map[event.Field]string{event.RemotePeer: ev.Data[event.RemotePeer], event.EventID: ev.EventID, event.Error: "peer is offline or the connection has yet to finalize"}))
e.eventManager.Publish(event.NewEvent(event.SendMessageToPeerError, map[event.Field]string{event.EventContext: string(event.SendMessageToPeer), event.RemotePeer: ev.Data[event.RemotePeer], event.EventID: ev.EventID, event.Error: "peer is offline or the connection has yet to finalize"}))
}
case event.SendGetValMessageToPeer:
if err := e.sendGetValToPeer(ev.EventID, ev.Data[event.RemotePeer], ev.Data[event.Scope], ev.Data[event.Path]); err != nil {
e.eventManager.Publish(event.NewEvent(event.SendMessageToPeerError, map[event.Field]string{event.RemotePeer: ev.Data[event.RemotePeer], event.EventID: ev.EventID, event.Error: err.Error()}))
e.eventManager.Publish(event.NewEvent(event.SendMessageToPeerError, map[event.Field]string{event.EventContext: string(event.SendGetValMessageToPeer), event.RemotePeer: ev.Data[event.RemotePeer], event.EventID: ev.EventID, event.Error: err.Error()}))
}
case event.SendRetValMessageToPeer:
if err := e.sendRetValToPeer(ev.EventID, ev.Data[event.RemotePeer], ev.Data[event.Data], ev.Data[event.Exists]); err != nil {
e.eventManager.Publish(event.NewEvent(event.SendMessageToPeerError, map[event.Field]string{event.RemotePeer: ev.Data[event.RemotePeer], event.EventID: ev.EventID, event.Error: err.Error()}))
e.eventManager.Publish(event.NewEvent(event.SendMessageToPeerError, map[event.Field]string{event.EventContext: string(event.SendRetValMessageToPeer), event.RemotePeer: ev.Data[event.RemotePeer], event.EventID: ev.EventID, event.Error: err.Error()}))
}
case event.UpdateConversationAuthorization:
accepted, _ := strconv.ParseBool(ev.Data[event.Accepted])
blocked, _ := strconv.ParseBool(ev.Data[event.Blocked])
auth := model.AuthUnknown
if blocked {
auth = model.AuthBlocked
} else if accepted {
auth = model.AuthApproved
}
case event.SetPeerAuthorization:
auth := model.Authorization(ev.Data[event.Authorization])
e.authorizations.Store(ev.Data[event.RemotePeer], auth)
if auth == model.AuthBlocked {
connection, err := e.service.GetConnection(ev.Data[event.RemotePeer])
@ -194,14 +245,18 @@ func (e *engine) eventHandler() {
}
case event.AllowUnknownPeers:
log.Debugf("%v now allows unknown connections", e.identity.Hostname())
e.blockUnknownContacts = false
e.blockUnknownContacts.Store(false)
case event.BlockUnknownPeers:
log.Debugf("%v now forbids unknown connections", e.identity.Hostname())
e.blockUnknownContacts = true
e.blockUnknownContacts.Store(true)
case event.ProtocolEngineStartListen:
go e.listenFn()
case event.ShareManifest:
e.filesharingSubSystem.ShareFile(ev.Data[event.FileKey], ev.Data[event.SerializedManifest])
case event.StopFileShare:
e.filesharingSubSystem.StopFileShare(ev.Data[event.FileKey])
case event.StopAllFileShares:
e.filesharingSubSystem.StopAllFileShares()
case event.ManifestSizeReceived:
handle := ev.Data[event.Handle]
key := ev.Data[event.FileKey]
@ -215,14 +270,25 @@ func (e *engine) eventHandler() {
serializedManifest := ev.Data[event.SerializedManifest]
tempFile := ev.Data[event.TempFile]
title := ev.Data[event.NameSuggestion]
// NOTE: for now there will probably only ever be a single chunk request. When we enable group
// sharing and rehosting then this loop will serve as a a way of splitting the request among multiple
// contacts
for _, message := range e.filesharingSubSystem.CompileChunkRequests(key, serializedManifest, tempFile, title) {
if err := e.sendPeerMessage(handle, message); err != nil {
e.eventManager.Publish(event.NewEvent(event.SendMessageToPeerError, map[event.Field]string{event.RemotePeer: ev.Data[event.RemotePeer], event.EventID: ev.EventID, event.Error: err.Error()}))
// Another optimistic check here. Technically Cwtch profile should not request manifest on a download files
// but if they do then we should check if it exists up front. If it does then announce that the download
// is complete.
if _, filePath, success := e.filesharingSubSystem.VerifyFile(key); success {
log.Debugf("file verified and downloaded!")
e.eventManager.Publish(event.NewEvent(event.FileDownloaded, map[event.Field]string{event.FileKey: key, event.FilePath: filePath, event.TempFile: tempFile}))
} else {
// NOTE: for now there will probably only ever be a single chunk request. When we enable group
// sharing and rehosting then this loop will serve as a a way of splitting the request among multiple
// contacts
for _, message := range e.filesharingSubSystem.CompileChunkRequests(key, serializedManifest, tempFile, title) {
if err := e.sendPeerMessage(handle, message); err != nil {
e.eventManager.Publish(event.NewEvent(event.SendMessageToPeerError, map[event.Field]string{event.RemotePeer: ev.Data[event.RemotePeer], event.EventID: ev.EventID, event.Error: err.Error()}))
}
}
}
case event.ProtocolEngineShutdown:
return
default:
return
}
@ -233,7 +299,7 @@ func (e *engine) isBlocked(onion string) bool {
authorization, known := e.authorizations.Load(onion)
if !known {
// if we block unknown peers we will block this contact
return e.blockUnknownContacts
return e.blockUnknownContacts.Load()
}
return authorization.(model.Authorization) == model.AuthBlocked
}
@ -244,7 +310,7 @@ func (e *engine) isAllowed(onion string) bool {
log.Errorf("attempted to lookup authorization of onion not in map...that should never happen")
return false
}
if e.blockUnknownContacts {
if e.blockUnknownContacts.Load() {
return authorization.(model.Authorization) == model.AuthApproved
}
return authorization.(model.Authorization) != model.AuthBlocked
@ -259,21 +325,39 @@ func (e *engine) createPeerTemplate() *PeerApp {
peerAppTemplate.OnAuth = e.ignoreOnShutdown(e.peerAuthed)
peerAppTemplate.OnConnecting = e.ignoreOnShutdown(e.peerConnecting)
peerAppTemplate.OnClose = e.ignoreOnShutdown(e.peerDisconnected)
peerAppTemplate.OnSendMessage = e.onSendMessage
return peerAppTemplate
}
// Listen sets up an onion listener to process incoming cwtch messages
func (e *engine) listenFn() {
err := e.service.Listen(e.createPeerTemplate())
if !e.shuttingDown {
if !e.shuttingDown.Load() {
e.eventManager.Publish(event.NewEvent(event.ProtocolEngineStopped, map[event.Field]string{event.Identity: e.identity.Hostname(), event.Error: err.Error()}))
}
}
// Shutdown tears down the eventHandler goroutine
func (e *engine) Shutdown() {
e.shuttingDown = true
// don't accept any more events...
e.queue.Publish(event.NewEvent(event.ProtocolEngineShutdown, map[event.Field]string{}))
e.eventManager.Publish(event.NewEvent(event.ProtocolEngineShutdown, map[event.Field]string{}))
e.service.Shutdown()
e.shuttingDown.Store(true)
e.ephemeralServicesLock.Lock()
defer e.ephemeralServicesLock.Unlock()
for _, connection := range e.ephemeralServices {
log.Infof("shutting down ephemeral service")
// work around: service.shutdown() can block for a long time if it is Open()ing a new connection, putting it in a
// goroutine means we can perform this operation and let the per service shutdown in their own time or until the app exits
conn := connection // don't capture loop variable
go func() {
conn.connectingLock.Lock()
conn.service.Shutdown()
conn.connectingLock.Unlock()
}()
}
e.queue.Shutdown()
}
@ -284,61 +368,94 @@ func (e *engine) peerWithOnion(onion string) {
if !e.isBlocked(onion) {
e.ignoreOnShutdown(e.peerConnecting)(onion)
connected, err := e.service.Connect(onion, e.createPeerTemplate())
if connected && err == nil {
// on success CwtchPeer will handle Auth and other status updates
// early exit from this function...
return
}
// If we are already connected...check if we are authed and issue an auth event
// (This allows the ui to be stateless)
if connected && err != nil {
conn, err := e.service.GetConnection(onion)
conn, err := e.service.WaitForCapabilityOrClose(onion, cwtchCapability)
if err == nil {
if conn.HasCapability(cwtchCapability) {
e.ignoreOnShutdown(e.peerAuthed)(onion)
return
}
log.Errorf("PeerWithOnion something went very wrong...%v %v", onion, err)
if conn != nil {
conn.Close()
}
e.ignoreOnShutdown(e.peerDisconnected)(onion)
} else {
e.ignoreOnShutdown(e.peerDisconnected)(onion)
}
}
}
e.ignoreOnShutdown(e.peerDisconnected)(onion)
}
// Only issue a disconnected error if we are disconnected (Connect will fail if a connection already exists)
if !connected && err != nil {
e.ignoreOnShutdown(e.peerDisconnected)(onion)
func (e *engine) makeAntispamPayment(onion string) {
log.Debugf("making antispam payment")
e.ephemeralServicesLock.Lock()
ephemeralService, ok := e.ephemeralServices[onion]
e.ephemeralServicesLock.Unlock()
if ephemeralService == nil || !ok {
log.Debugf("could not find associated group for antispam payment")
return
}
// Before doing anything, send and event with the current number of token
// This may unblock downstream processes who don't have an accurate token count
e.PokeTokenCount(onion)
conn, err := ephemeralService.service.GetConnection(onion)
if err == nil {
tokenApp, ok := (conn.App()).(*TokenBoardClient)
if ok {
tokenManagerPointer, _ := e.tokenManagers.LoadOrStore(tokenApp.tokenServiceOnion, NewTokenManager())
tokenManager := tokenManagerPointer.(*TokenManager)
log.Debugf("checking antispam tokens %v", tokenManager.NumTokens())
if tokenManager.NumTokens() < 5 {
go tokenApp.PurchaseTokens()
}
}
}
}
// peerWithTokenServer is the entry point for cwtchPeer - server relationships
// needs to be run in a goroutine as will block on Open.
func (e *engine) peerWithTokenServer(onion string, tokenServerOnion string, tokenServerY string, lastKnownSignature []byte) {
func (e *engine) peerWithTokenServer(onion string, tokenServerOnion string, tokenServerY string, lastKnownSignature []byte, cachedTokens []*privacypass.Token) {
e.ephemeralServicesLock.Lock()
_, exists := e.ephemeralServices[onion]
service, exists := e.ephemeralServices.Load(onion)
if exists {
connection := service.(*tor.BaseOnionService)
if conn, err := connection.GetConnection(onion); err == nil {
// We are already peered and synced so return...
// This will only not-trigger it lastKnownSignature has been wiped, which only happens when ResyncServer is called
// in CwtchPeer.
if !conn.IsClosed() && len(lastKnownSignature) != 0 {
return
}
// Otherwise...we are going to rebuild the connection(which will result in a bandwidth heavy resync)...
e.leaveServer(onion)
}
// Otherwise...let's reconnect
e.ephemeralServicesLock.Unlock()
log.Debugf("attempted to join a server with an active connection")
return
}
connectionService := &connectionLockedService{service: new(tor.BaseOnionService)}
e.ephemeralServices[onion] = connectionService
connectionService.connectingLock.Lock()
defer connectionService.connectingLock.Unlock()
e.ephemeralServicesLock.Unlock()
log.Debugf("Peering with Token Server %v %v", onion, tokenServerOnion)
e.ignoreOnShutdown(e.serverConnecting)(onion)
// Create a new ephemeral service for this connection
ephemeralService := new(tor.BaseOnionService)
eid, epk := primitives.InitializeEphemeralIdentity()
ephemeralService.Init(e.acn, epk, &eid)
connectionService.service.Init(e.acn, epk, &eid)
Y := ristretto255.NewElement()
Y := new(ristretto255.Element)
Y.UnmarshalText([]byte(tokenServerY))
connected, err := ephemeralService.Connect(onion, NewTokenBoardClient(e.acn, Y, tokenServerOnion, lastKnownSignature, e.receiveGroupMessage, e.serverAuthed, e.serverSynced, e.ignoreOnShutdown(e.serverDisconnected)))
e.ephemeralServices.Store(onion, ephemeralService)
connected, err := connectionService.service.Connect(onion, NewTokenBoardClient(e.acn, Y, tokenServerOnion, lastKnownSignature, e))
// If we are already connected...check if we are authed and issue an auth event
// (This allows the ui to be stateless)
if connected && err != nil {
conn, err := ephemeralService.GetConnection(onion)
conn, err := connectionService.service.GetConnection(onion)
if err == nil {
// If the server is synced, resend the synced status update
@ -353,6 +470,10 @@ func (e *engine) peerWithTokenServer(onion string, tokenServerOnion string, toke
e.ignoreOnShutdown(e.serverAuthed)(onion)
return
}
// if we are not authed or synced then we are stuck...
e.ignoreOnShutdown(e.serverConnecting)(onion)
log.Errorf("server connection attempt issued to active connection")
}
}
@ -364,7 +485,7 @@ func (e *engine) peerWithTokenServer(onion string, tokenServerOnion string, toke
func (e *engine) ignoreOnShutdown(f func(string)) func(string) {
return func(x string) {
if !e.shuttingDown {
if !e.shuttingDown.Load() {
f(x)
}
}
@ -372,7 +493,7 @@ func (e *engine) ignoreOnShutdown(f func(string)) func(string) {
func (e *engine) ignoreOnShutdown2(f func(string, string)) func(string, string) {
return func(x, y string) {
if !e.shuttingDown {
if !e.shuttingDown.Load() {
f(x, y)
}
}
@ -383,6 +504,26 @@ func (e *engine) peerAuthed(onion string) {
if !known {
e.authorizations.Store(onion, model.AuthUnknown)
}
// FIXME: This call uses WAY too much memory, and was responsible for the vast majority
// of allocations in the UI
// This is because Bine ends up reading the entire response into memory and then passes that back
// into Connectivity which eventually extracts just what it needs.
// Ideally we would just read from the control stream directly into reusable buffers.
//details, err := e.acn.GetInfo(onion)
//if err == nil {
// if hops, exists := details["circuit"]; exists {
// e.eventManager.Publish(event.NewEvent(event.ACNInfo, map[event.Field]string{
// event.Handle: onion,
// event.Key: "circuit",
// event.Data: hops,
// }))
// }
//} else {
// log.Errorf("error getting info for onion %v", err)
//}
e.eventManager.Publish(event.NewEvent(event.PeerStateChange, map[event.Field]string{
event.RemotePeer: string(onion),
event.ConnectionState: ConnectionStateName[AUTHENTICATED],
@ -391,22 +532,15 @@ func (e *engine) peerAuthed(onion string) {
func (e *engine) peerConnecting(onion string) {
e.eventManager.Publish(event.NewEvent(event.PeerStateChange, map[event.Field]string{
event.RemotePeer: string(onion),
event.RemotePeer: onion,
event.ConnectionState: ConnectionStateName[CONNECTING],
}))
}
func (e *engine) serverConnecting(onion string) {
e.eventManager.Publish(event.NewEvent(event.ServerStateChange, map[event.Field]string{
event.GroupServer: string(onion),
event.ConnectionState: ConnectionStateName[CONNECTING],
}))
}
func (e *engine) serverConnected(onion string) {
e.eventManager.Publish(event.NewEvent(event.ServerStateChange, map[event.Field]string{
event.GroupServer: onion,
event.ConnectionState: ConnectionStateName[CONNECTED],
event.ConnectionState: ConnectionStateName[CONNECTING],
}))
}
@ -425,6 +559,8 @@ func (e *engine) serverSynced(onion string) {
}
func (e *engine) serverDisconnected(onion string) {
e.leaveServer(onion)
e.eventManager.Publish(event.NewEvent(event.ServerStateChange, map[event.Field]string{
event.GroupServer: onion,
event.ConnectionState: ConnectionStateName[DISCONNECTED],
@ -439,6 +575,23 @@ func (e *engine) peerAck(onion string, eventID string) {
}
func (e *engine) peerDisconnected(onion string) {
// Clean up any existing get value requests...
e.getValRequests.Range(func(key, value interface{}) bool {
keyString := key.(string)
if strings.HasPrefix(keyString, onion) {
e.getValRequests.Delete(keyString)
}
return true
})
// Purge circuit information...
e.eventManager.Publish(event.NewEvent(event.ACNInfo, map[event.Field]string{
event.Handle: onion,
event.Key: "circuit",
event.Data: "",
}))
e.eventManager.Publish(event.NewEvent(event.PeerStateChange, map[event.Field]string{
event.RemotePeer: string(onion),
event.ConnectionState: ConnectionStateName[DISCONNECTED],
@ -453,8 +606,13 @@ func (e *engine) sendGetValToPeer(eventID, onion, scope, path string) error {
return err
}
e.getValRequests.Store(onion+eventID, message)
return e.sendPeerMessage(onion, pmodel.PeerMessage{ID: eventID, Context: event.ContextGetVal, Data: message})
key := onion + eventID
e.getValRequests.Store(key, message)
err = e.sendPeerMessage(onion, pmodel.PeerMessage{ID: eventID, Context: event.ContextGetVal, Data: message})
if err != nil {
e.getValRequests.Delete(key)
}
return err
}
func (e *engine) sendRetValToPeer(eventID, onion, val, existsStr string) error {
@ -484,7 +642,6 @@ func (e *engine) receiveGroupMessage(server string, gm *groups.EncryptedGroupMes
// sendMessageToGroup attempts to sent the given message to the given group id.
func (e *engine) sendMessageToGroup(groupID string, server string, ct []byte, sig []byte, attempts int) {
// sending to groups can fail for a few reasons (slow server, not enough tokens, etc.)
// rather than trying to keep all that logic in method we simply back-off and try again
// but if we fail more than 5 times then we report back to the client so they can investigate other options.
@ -496,33 +653,38 @@ func (e *engine) sendMessageToGroup(groupID string, server string, ct []byte, si
return
}
es, ok := e.ephemeralServices.Load(server)
if es == nil || !ok {
e.ephemeralServicesLock.Lock()
ephemeralService, ok := e.ephemeralServices[server]
e.ephemeralServicesLock.Unlock()
if ephemeralService == nil || !ok {
log.Debugf("could not send message to group: serve not found")
e.eventManager.Publish(event.NewEvent(event.SendMessageToGroupError, map[event.Field]string{event.GroupID: groupID, event.GroupServer: server, event.Error: "server-not-found", event.Signature: base64.StdEncoding.EncodeToString(sig)}))
return
}
ephemeralService := es.(tapir.Service)
conn, err := ephemeralService.WaitForCapabilityOrClose(server, groups.CwtchServerSyncedCapability)
conn, err := ephemeralService.service.WaitForCapabilityOrClose(server, groups.CwtchServerSyncedCapability)
if err == nil {
tokenApp, ok := (conn.App()).(*TokenBoardClient)
if ok {
if spent, numtokens := tokenApp.Post(ct, sig); !spent {
if spent, numtokens := tokenApp.Post(groupID, ct, sig); !spent {
// we failed to post, probably because we ran out of tokens... so make a payment
go tokenApp.MakePayment()
go tokenApp.PurchaseTokens()
// backoff
time.Sleep(time.Second * 5)
// try again
log.Debugf("sending message to group error attempt: %v", attempts)
e.sendMessageToGroup(groupID, server, ct, sig, attempts+1)
} else {
if numtokens < 5 {
go tokenApp.MakePayment()
go tokenApp.PurchaseTokens()
}
}
// regardless we return....
return
}
}
log.Debugf("could not send message to group")
e.eventManager.Publish(event.NewEvent(event.SendMessageToGroupError, map[event.Field]string{event.GroupID: groupID, event.GroupServer: server, event.Error: "server-connection-not-valid", event.Signature: base64.StdEncoding.EncodeToString(sig)}))
}
@ -585,10 +747,20 @@ func (e *engine) handlePeerMessage(hostname string, eventID string, context stri
}
} else {
// Fall through handler for the default text conversation.
e.eventManager.Publish(event.NewEvent(event.NewMessageFromPeer, map[event.Field]string{event.TimestampReceived: time.Now().Format(time.RFC3339Nano), event.RemotePeer: hostname, event.Data: string(message)}))
e.eventManager.Publish(event.NewEvent(event.NewMessageFromPeerEngine, map[event.Field]string{event.TimestampReceived: time.Now().Format(time.RFC3339Nano), event.RemotePeer: hostname, event.Data: string(message)}))
// Don't ack messages in channel 7
// Note: this code explictly doesn't care about malformed messages, we deal with them
// later on...we still want to ack the original send...(as some "malformed" messages
// may be future-ok)
if cm, err := model.DeserializeMessage(string(message)); err == nil {
if cm.IsStream() {
return
}
}
// Send an explicit acknowledgement
// Every other protocol should have a explicit acknowledgement message e.g. value lookups have responses, and file handling has an explicit flow
// Every other protocol should have an explicit acknowledgement message e.g. value lookups have responses, and file handling has an explicit flow
if err := e.sendPeerMessage(hostname, pmodel.PeerMessage{ID: eventID, Context: event.ContextAck, Data: []byte{}}); err != nil {
e.eventManager.Publish(event.NewEvent(event.SendMessageToPeerError, map[event.Field]string{event.RemotePeer: hostname, event.EventID: eventID, event.Error: err.Error()}))
}
@ -613,12 +785,14 @@ func (e *engine) handlePeerRetVal(hostname string, getValData, retValData []byte
e.eventManager.Publish(event.NewEventList(event.NewRetValMessageFromPeer, event.RemotePeer, hostname, event.Scope, getVal.Scope, event.Path, getVal.Path, event.Exists, strconv.FormatBool(retVal.Exists), event.Data, retVal.Val))
}
// leaveServer disconnects from a server and deletes the ephemeral service
func (e *engine) leaveServer(server string) {
es, ok := e.ephemeralServices.Load(server)
e.ephemeralServicesLock.Lock()
defer e.ephemeralServicesLock.Unlock()
ephemeralService, ok := e.ephemeralServices[server]
if ok {
ephemeralService := es.(tapir.Service)
ephemeralService.Shutdown()
e.ephemeralServices.Delete(server)
ephemeralService.service.Shutdown()
delete(e.ephemeralServices, server)
}
}

View File

@ -0,0 +1,59 @@
package connections
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/protocol/groups"
"encoding/base64"
"git.openprivacy.ca/cwtch.im/tapir/primitives/privacypass"
"strconv"
)
// Implement Token Service Handler for Engine
// GroupMessageHandler receives a server and an encrypted group message
func (e *engine) GroupMessageHandler(server string, gm *groups.EncryptedGroupMessage) {
e.receiveGroupMessage(server, gm)
}
// PostingFailed notifies a peer that a message failed to post
func (e *engine) PostingFailed(group string, sig []byte) {
e.eventManager.Publish(event.NewEvent(event.SendMessageToGroupError, map[event.Field]string{event.GroupID: group, event.Error: "failed to post message", event.Signature: base64.StdEncoding.EncodeToString(sig)}))
}
// ServerAuthedHandler is notified when a server has successfully authed
func (e *engine) ServerAuthedHandler(server string) {
e.serverAuthed(server)
}
// ServerSyncedHandler is notified when a server has successfully synced
func (e *engine) ServerSyncedHandler(server string) {
e.serverSynced(server)
}
// ServerClosedHandler is notified when a server connection has closed, the result is ignored during shutdown...
func (e *engine) ServerClosedHandler(server string) {
e.ignoreOnShutdown(e.serverDisconnected)(server)
}
// NewTokenHandler is notified after a successful token acquisition
func (e *engine) NewTokenHandler(tokenService string, tokens []*privacypass.Token) {
tokenManagerPointer, _ := e.tokenManagers.LoadOrStore(tokenService, NewTokenManager())
tokenManager := tokenManagerPointer.(*TokenManager)
tokenManager.StoreNewTokens(tokens)
e.eventManager.Publish(event.NewEvent(event.TokenManagerInfo, map[event.Field]string{event.ServerTokenOnion: tokenService, event.ServerTokenCount: strconv.Itoa(tokenManager.NumTokens())}))
}
// FetchToken is notified when a server requires a new token from the client
func (e *engine) FetchToken(tokenService string) (*privacypass.Token, int, error) {
tokenManagerPointer, _ := e.tokenManagers.LoadOrStore(tokenService, NewTokenManager())
tokenManager := tokenManagerPointer.(*TokenManager)
token, numTokens, err := tokenManager.FetchToken()
e.eventManager.Publish(event.NewEvent(event.TokenManagerInfo, map[event.Field]string{event.ServerTokenOnion: tokenService, event.ServerTokenCount: strconv.Itoa(numTokens)}))
return token, numTokens, err
}
func (e *engine) PokeTokenCount(tokenService string) {
tokenManagerPointer, _ := e.tokenManagers.LoadOrStore(tokenService, NewTokenManager())
tokenManager := tokenManagerPointer.(*TokenManager)
e.eventManager.Publish(event.NewEvent(event.TokenManagerInfo, map[event.Field]string{event.ServerTokenOnion: tokenService, event.ServerTokenCount: strconv.Itoa(tokenManager.NumTokens())}))
}

View File

@ -0,0 +1,14 @@
package connections
import "git.openprivacy.ca/cwtch.im/tapir"
type EngineHooks interface {
SendPeerMessage(connection tapir.Connection, message []byte) error
}
type DefaultEngineHooks struct {
}
func (deh DefaultEngineHooks) SendPeerMessage(connection tapir.Connection, message []byte) error {
return connection.Send(message)
}

View File

@ -0,0 +1,59 @@
package connections
import (
"cwtch.im/cwtch/utils"
"git.openprivacy.ca/cwtch.im/tapir/applications"
"git.openprivacy.ca/cwtch.im/tapir/networks/tor"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
"git.openprivacy.ca/cwtch.im/tapir/primitives/privacypass"
"git.openprivacy.ca/openprivacy/connectivity"
"git.openprivacy.ca/openprivacy/log"
"reflect"
"time"
)
// MakePayment uses the PoW based token protocol to obtain more tokens
func MakePayment(tokenServiceOnion string, tokenService *privacypass.TokenServer, acn connectivity.ACN, handler TokenBoardHandler) error {
log.Debugf("making a payment")
id, sk := primitives.InitializeEphemeralIdentity()
client := new(tor.BaseOnionService)
client.Init(acn, sk, &id)
defer client.Shutdown()
tokenApplication := new(applications.TokenApplication)
tokenApplication.TokenService = tokenService
powTokenApp := new(applications.ApplicationChain).
ChainApplication(new(applications.ProofOfWorkApplication), applications.SuccessfulProofOfWorkCapability).
ChainApplication(tokenApplication, applications.HasTokensCapability)
log.Debugf("waiting for successful PoW auth...")
tp := utils.TimeoutPolicy(time.Second * 30)
err := tp.ExecuteAction(func() error {
connected, err := client.Connect(tokenServiceOnion, powTokenApp)
if connected && err == nil {
log.Debugf("waiting for successful token acquisition...")
conn, err := client.WaitForCapabilityOrClose(tokenServiceOnion, applications.HasTokensCapability)
if err == nil {
powtapp, ok := conn.App().(*applications.TokenApplication)
if ok {
log.Debugf("updating tokens")
handler.NewTokenHandler(tokenServiceOnion, powtapp.Tokens)
log.Debugf("transcript: %v", powtapp.Transcript().OutputTranscriptToAudit())
conn.Close()
return nil
}
log.Errorf("invalid cast of powapp. this should never happen %v %v", powtapp, reflect.TypeOf(conn.App()))
return nil
}
return nil
}
return err
})
// we timed out
if err != nil {
log.Debugf("make payment timeout...")
return err
}
return err
}

View File

@ -1,11 +1,15 @@
package connections
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
model2 "cwtch.im/cwtch/protocol/model"
"encoding/json"
"git.openprivacy.ca/cwtch.im/tapir"
"git.openprivacy.ca/cwtch.im/tapir/applications"
"git.openprivacy.ca/openprivacy/log"
"sync/atomic"
"time"
)
const cwtchCapability = tapir.Capability("cwtchCapability")
@ -21,6 +25,8 @@ type PeerApp struct {
OnAuth func(string)
OnClose func(string)
OnConnecting func(string)
OnSendMessage func(connection tapir.Connection, message []byte) error
version atomic.Value
}
type peerGetVal struct {
@ -32,6 +38,9 @@ type peerRetVal struct {
Exists bool
}
const Version1 = 0x01
const Version2 = 0x02
// NewInstance should always return a new instantiation of the application.
func (pa *PeerApp) NewInstance() tapir.Application {
newApp := new(PeerApp)
@ -42,6 +51,8 @@ func (pa *PeerApp) NewInstance() tapir.Application {
newApp.OnAuth = pa.OnAuth
newApp.OnClose = pa.OnClose
newApp.OnConnecting = pa.OnConnecting
newApp.OnSendMessage = pa.OnSendMessage
newApp.version.Store(Version1)
return newApp
}
@ -59,11 +70,28 @@ func (pa *PeerApp) Init(connection tapir.Connection) {
pa.connection.Close()
pa.OnClose(connection.Hostname())
} else {
// we are authenticated
// attempt to negotiate a more efficient packet format...
// we are abusing the context here slightly by sending a "malformed" GetVal request.
// as a rule cwtch ignores getval requests that it cannot deserialize so older clients will ignore this
// message.
// version *must* be the first message sent to prevent race conditions for other events fired after-auth
// (e.g. getVal requests)
// as such, we send this message before we update the rest of the system
_ = pa.SendMessage(model2.PeerMessage{
ID: event.ContextVersion,
Context: event.ContextGetVal,
Data: []byte{Version2},
})
pa.OnAuth(connection.Hostname())
go pa.listen()
}
} else {
// The auth protocol wasn't completed, we can safely shutdown the connection
// send an onclose here because we *may* have triggered this and we want to retry later...
pa.OnClose(connection.Hostname())
connection.Close()
}
}
@ -76,11 +104,47 @@ func (pa *PeerApp) listen() {
pa.OnClose(pa.connection.Hostname())
return
}
var peerMessage model2.PeerMessage
err := json.Unmarshal(message, &peerMessage)
var packet model2.PeerMessage
var err error
if pa.version.Load() == Version1 {
err = json.Unmarshal(message, &packet)
} else if pa.version.Load() == Version2 {
parsePacket, parseErr := model2.ParsePeerMessage(message)
// if all else fails...attempt to process this message as a version 1 message
if parseErr != nil {
err = json.Unmarshal(message, &packet)
} else {
packet = *parsePacket
}
} else {
log.Errorf("invalid version")
pa.OnClose(pa.connection.Hostname())
return
}
if err == nil {
if pa.IsAllowed(pa.connection.Hostname()) {
pa.MessageHandler(pa.connection.Hostname(), peerMessage.ID, peerMessage.Context, peerMessage.Data)
// we don't expose im.cwtch.version messages outside of PeerApp (ideally at some point in the future we
// can remove this check all together)
if packet.ID == event.ContextVersion {
if pa.version.Load() == Version1 && len(packet.Data) == 1 && packet.Data[0] == Version2 {
log.Debugf("switching to protocol version 2")
pa.version.Store(Version2)
}
} else {
if cm, err := model.DeserializeMessage(string(packet.Data)); err == nil {
if cm.TransitTime != nil {
rt := time.Now().UTC()
cm.RecvTime = &rt
data, _ := json.Marshal(cm)
packet.Data = data
}
}
pa.MessageHandler(pa.connection.Hostname(), packet.ID, packet.Context, packet.Data)
}
}
} else {
log.Errorf("Error unmarshalling PeerMessage package: %x %v", message, err)
@ -91,10 +155,41 @@ func (pa *PeerApp) listen() {
// SendMessage sends the peer a preformatted message
// NOTE: This is a stub, we will likely want to extend this to better reflect the desired protocol
func (pa *PeerApp) SendMessage(message model2.PeerMessage) error {
serialized, err := json.Marshal(message)
var serialized []byte
var err error
if cm, err := model.DeserializeMessage(string(message.Data)); err == nil {
if cm.SendTime != nil {
tt := time.Now().UTC()
cm.TransitTime = &tt
data, _ := json.Marshal(cm)
message.Data = data
}
}
if pa.version.Load() == Version2 {
// treat data as a pre-serialized string, not as a byte array (which will be base64 encoded and bloat the packet size)
serialized = message.Serialize()
} else {
serialized, err = json.Marshal(message)
}
if err == nil {
pa.connection.Send(serialized)
return nil
err = pa.OnSendMessage(pa.connection, serialized)
// at this point we have tried to send a message to a peer only to find that something went wrong.
// we don't know *what* went wrong - the most likely explanation is the peer went offline in the time between
// sending the message and it arriving in the engine to be sent. Other explanations include problems with Tor,
// a dropped wifi connection.
// Regardless, we error out this message and close this peer app assuming it cannot be used again.
// We expect that cwtch will eventually recreate this connection and the app.
if err != nil {
// close any associated sockets
pa.connection.Close()
// tell cwtch this connection is no longer valid
pa.OnClose(err.Error())
}
return err
}
return err
}

View File

@ -0,0 +1,54 @@
package connections
import (
"encoding/json"
"errors"
"git.openprivacy.ca/cwtch.im/tapir/primitives/privacypass"
"git.openprivacy.ca/openprivacy/log"
"sync"
)
// TokenManager maintains a list of tokens associated with a single TokenServer
type TokenManager struct {
lock sync.Mutex
tokens map[string]*privacypass.Token
}
func NewTokenManager() *TokenManager {
tm := new(TokenManager)
tm.tokens = make(map[string]*privacypass.Token)
return tm
}
// StoreNewTokens adds tokens to the internal list managed by this TokenManager
func (tm *TokenManager) StoreNewTokens(tokens []*privacypass.Token) {
tm.lock.Lock()
defer tm.lock.Unlock()
log.Debugf("acquired %v new tokens", tokens)
for _, token := range tokens {
serialized, _ := json.Marshal(token)
tm.tokens[string(serialized)] = token
}
}
// NumTokens returns the current number of tokens
func (tm *TokenManager) NumTokens() int {
tm.lock.Lock()
defer tm.lock.Unlock()
return len(tm.tokens)
}
// FetchToken removes a token from the internal list and returns it, along with a count of the remaining tokens.
// Errors if no tokens available.
func (tm *TokenManager) FetchToken() (*privacypass.Token, int, error) {
tm.lock.Lock()
defer tm.lock.Unlock()
if len(tm.tokens) == 0 {
return nil, 0, errors.New("no more tokens")
}
for serializedToken, token := range tm.tokens {
delete(tm.tokens, serializedToken)
return token, len(tm.tokens), nil
}
return nil, 0, errors.New("no more tokens")
}

View File

@ -3,31 +3,35 @@ package connections
import (
"cwtch.im/cwtch/protocol/groups"
"encoding/json"
"errors"
"git.openprivacy.ca/cwtch.im/tapir"
"git.openprivacy.ca/cwtch.im/tapir/applications"
"git.openprivacy.ca/cwtch.im/tapir/networks/tor"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
"git.openprivacy.ca/cwtch.im/tapir/primitives/privacypass"
"git.openprivacy.ca/openprivacy/connectivity"
"git.openprivacy.ca/openprivacy/log"
"github.com/gtank/ristretto255"
"reflect"
"sync"
"time"
)
// TokenBoardHandler encapsulates all the various handlers a client needs to interact with a token board
// this includes handlers to receive new messages, as well as handlers to manage tokens.
type TokenBoardHandler interface {
GroupMessageHandler(server string, gm *groups.EncryptedGroupMessage)
ServerAuthedHandler(server string)
ServerSyncedHandler(server string)
ServerClosedHandler(server string)
NewTokenHandler(tokenService string, tokens []*privacypass.Token)
PostingFailed(server string, sig []byte)
FetchToken(tokenService string) (*privacypass.Token, int, error)
}
// NewTokenBoardClient generates a new Client for Token Board
func NewTokenBoardClient(acn connectivity.ACN, Y *ristretto255.Element, tokenServiceOnion string, lastKnownSignature []byte, groupMessageHandler func(server string, gm *groups.EncryptedGroupMessage), serverAuthedHandler func(server string), serverSyncedHandler func(server string), serverClosedHandler func(server string)) tapir.Application {
func NewTokenBoardClient(acn connectivity.ACN, Y *ristretto255.Element, tokenServiceOnion string, lastKnownSignature []byte, tokenBoardHandler TokenBoardHandler) tapir.Application {
tba := new(TokenBoardClient)
tba.acn = acn
tba.tokenService = privacypass.NewTokenServer()
tba.tokenService.Y = Y
tba.tokenServiceOnion = tokenServiceOnion
tba.receiveGroupMessageHandler = groupMessageHandler
tba.serverAuthedHandler = serverAuthedHandler
tba.serverSyncedHandler = serverSyncedHandler
tba.serverClosedHandler = serverClosedHandler
tba.tokenBoardHandler = tokenBoardHandler
tba.lastKnownSignature = lastKnownSignature
return tba
}
@ -35,28 +39,24 @@ func NewTokenBoardClient(acn connectivity.ACN, Y *ristretto255.Element, tokenSer
// TokenBoardClient defines a client for the TokenBoard server
type TokenBoardClient struct {
applications.AuthApp
connection tapir.Connection
receiveGroupMessageHandler func(server string, gm *groups.EncryptedGroupMessage)
serverAuthedHandler func(server string)
serverSyncedHandler func(server string)
serverClosedHandler func(server string)
connection tapir.Connection
tokenBoardHandler TokenBoardHandler
// Token service handling
acn connectivity.ACN
tokens []*privacypass.Token
tokenLock sync.Mutex
acn connectivity.ACN
tokenService *privacypass.TokenServer
tokenServiceOnion string
lastKnownSignature []byte
postLock sync.Mutex
postQueue []groups.CachedEncryptedGroupMessage
}
// NewInstance Client a new TokenBoardApp
func (ta *TokenBoardClient) NewInstance() tapir.Application {
tba := new(TokenBoardClient)
tba.serverAuthedHandler = ta.serverAuthedHandler
tba.serverSyncedHandler = ta.serverSyncedHandler
tba.serverClosedHandler = ta.serverClosedHandler
tba.receiveGroupMessageHandler = ta.receiveGroupMessageHandler
tba.tokenBoardHandler = ta.tokenBoardHandler
tba.acn = ta.acn
tba.tokenService = ta.tokenService
tba.tokenServiceOnion = ta.tokenServiceOnion
@ -66,17 +66,22 @@ func (ta *TokenBoardClient) NewInstance() tapir.Application {
// Init initializes the cryptographic TokenBoardApp
func (ta *TokenBoardClient) Init(connection tapir.Connection) {
// connection.Hostname is always valid because we are ALWAYS the initiating party
log.Debugf("connecting to server: %v", connection.Hostname())
ta.AuthApp.Init(connection)
log.Debugf("server protocol complete: %v", connection.Hostname())
if connection.HasCapability(applications.AuthCapability) {
log.Debugf("Successfully Initialized Connection to %v", connection.Hostname())
ta.connection = connection
ta.serverAuthedHandler(ta.connection.Hostname())
log.Debugf("Successfully Initialized Connection")
ta.tokenBoardHandler.ServerAuthedHandler(connection.Hostname())
go ta.Listen()
// Optimistically acquire many tokens for this server...
go ta.MakePayment()
go ta.MakePayment()
go ta.PurchaseTokens()
go ta.PurchaseTokens()
ta.Replay()
} else {
log.Debugf("Error Connecting to %v", connection.Hostname())
ta.tokenBoardHandler.ServerClosedHandler(connection.Hostname())
connection.Close()
}
}
@ -88,7 +93,7 @@ func (ta *TokenBoardClient) Listen() {
data := ta.connection.Expect()
if len(data) == 0 {
log.Debugf("Server closed the connection...")
ta.serverClosedHandler(ta.connection.Hostname())
ta.tokenBoardHandler.ServerClosedHandler(ta.connection.Hostname())
return // connection is closed
}
@ -96,7 +101,7 @@ func (ta *TokenBoardClient) Listen() {
var message groups.Message
if err := json.Unmarshal(data, &message); err != nil {
log.Debugf("Server sent an unexpected message, closing the connection: %v", err)
ta.serverClosedHandler(ta.connection.Hostname())
ta.tokenBoardHandler.ServerClosedHandler(ta.connection.Hostname())
ta.connection.Close()
return
}
@ -104,15 +109,28 @@ func (ta *TokenBoardClient) Listen() {
switch message.MessageType {
case groups.NewMessageMessage:
if message.NewMessage != nil {
ta.receiveGroupMessageHandler(ta.connection.Hostname(), &message.NewMessage.EGM)
ta.tokenBoardHandler.GroupMessageHandler(ta.connection.Hostname(), &message.NewMessage.EGM)
} else {
log.Debugf("Server sent an unexpected NewMessage, closing the connection: %s", data)
ta.serverClosedHandler(ta.connection.Hostname())
ta.tokenBoardHandler.ServerClosedHandler(ta.connection.Hostname())
ta.connection.Close()
return
}
case groups.PostResultMessage:
// TODO handle failure
ta.postLock.Lock()
egm := ta.postQueue[0]
ta.postQueue = ta.postQueue[1:]
ta.postLock.Unlock()
if !message.PostResult.Success {
log.Debugf("post result message: %v", message.PostResult)
// Retry using another token
posted, _ := ta.Post(egm.Group, egm.Ciphertext, egm.Signature)
// if posting failed...
if !posted {
log.Errorf("error posting message")
ta.tokenBoardHandler.PostingFailed(egm.Group, egm.Signature)
}
}
case groups.ReplayResultMessage:
if message.ReplayResult != nil {
log.Debugf("Replaying %v Messages...", message.ReplayResult.NumMessages)
@ -121,23 +139,23 @@ func (ta *TokenBoardClient) Listen() {
if len(data) == 0 {
log.Debugf("Server sent an unexpected EncryptedGroupMessage, closing the connection")
ta.serverClosedHandler(ta.connection.Hostname())
ta.tokenBoardHandler.ServerClosedHandler(ta.connection.Hostname())
ta.connection.Close()
return
}
egm := &groups.EncryptedGroupMessage{}
if err := json.Unmarshal(data, egm); err == nil {
ta.receiveGroupMessageHandler(ta.connection.Hostname(), egm)
ta.tokenBoardHandler.GroupMessageHandler(ta.connection.Hostname(), egm)
ta.lastKnownSignature = egm.Signature
} else {
log.Debugf("Server sent an unexpected EncryptedGroupMessage, closing the connection: %v", err)
ta.serverClosedHandler(ta.connection.Hostname())
ta.tokenBoardHandler.ServerClosedHandler(ta.connection.Hostname())
ta.connection.Close()
return
}
}
ta.serverSyncedHandler(ta.connection.Hostname())
ta.tokenBoardHandler.ServerSyncedHandler(ta.connection.Hostname())
ta.connection.SetCapability(groups.CwtchServerSyncedCapability)
}
}
@ -152,79 +170,35 @@ func (ta *TokenBoardClient) Replay() {
// PurchaseTokens purchases the given number of tokens from the server (using the provided payment handler)
func (ta *TokenBoardClient) PurchaseTokens() {
ta.MakePayment()
MakePayment(ta.tokenServiceOnion, ta.tokenService, ta.acn, ta.tokenBoardHandler)
}
// Post sends a Post Request to the server
func (ta *TokenBoardClient) Post(ct []byte, sig []byte) (bool, int) {
func (ta *TokenBoardClient) Post(group string, ct []byte, sig []byte) (bool, int) {
egm := groups.EncryptedGroupMessage{Ciphertext: ct, Signature: sig}
token, numTokens, err := ta.NextToken(egm.ToBytes(), ta.connection.Hostname())
if err == nil {
data, _ := json.Marshal(groups.Message{MessageType: groups.PostRequestMessage, PostRequest: &groups.PostRequest{EGM: egm, Token: token}})
ta.postLock.Lock()
// ONLY put group in the EGM as a cache / for error reporting...
ta.postQueue = append(ta.postQueue, groups.CachedEncryptedGroupMessage{Group: group, EncryptedGroupMessage: egm})
log.Debugf("Message Length: %s %v", data, len(data))
ta.connection.Send(data)
err := ta.connection.Send(data)
ta.postLock.Unlock()
if err != nil {
return false, numTokens
}
return true, numTokens
}
log.Debugf("No Valid Tokens: %v", err)
return false, numTokens
}
// MakePayment uses the PoW based token protocol to obtain more tokens
func (ta *TokenBoardClient) MakePayment() error {
log.Debugf("Making a Payment")
id, sk := primitives.InitializeEphemeralIdentity()
client := new(tor.BaseOnionService)
client.Init(ta.acn, sk, &id)
tokenApplication := new(applications.TokenApplication)
tokenApplication.TokenService = ta.tokenService
powTokenApp := new(applications.ApplicationChain).
ChainApplication(new(applications.ProofOfWorkApplication), applications.SuccessfulProofOfWorkCapability).
ChainApplication(tokenApplication, applications.HasTokensCapability)
log.Debugf("Waiting for successful PoW Auth...")
connected, err := client.Connect(ta.tokenServiceOnion, powTokenApp)
if connected == true && err == nil {
log.Debugf("Waiting for successful Token Acquisition...")
conn, err := client.WaitForCapabilityOrClose(ta.tokenServiceOnion, applications.HasTokensCapability)
if err == nil {
powtapp, ok := conn.App().(*applications.TokenApplication)
if ok {
// Update tokens...we need a lock here to prevent SpendToken from modifying the tokens
// during this process..
log.Debugf("Updating Tokens")
ta.tokenLock.Lock()
ta.tokens = append(ta.tokens, powtapp.Tokens...)
ta.tokenLock.Unlock()
log.Debugf("Transcript: %v", powtapp.Transcript().OutputTranscriptToAudit())
conn.Close()
return nil
}
log.Errorf("invalid cast of powapp. this should never happen %v %v", powtapp, reflect.TypeOf(conn.App()))
return errors.New("invalid cast of powapp. this should never happen")
}
log.Debugf("could not connect to payment server %v..trying again")
return ta.MakePayment()
} else if connected && err != nil {
log.Debugf("inexplicable error: %v", err)
}
log.Debugf("failed to make a connection. trying again...")
// it doesn't actually take that long to make a payment, so waiting a small amount of time should suffice
time.Sleep(time.Second)
return ta.MakePayment()
}
// NextToken retrieves the next token
func (ta *TokenBoardClient) NextToken(data []byte, hostname string) (privacypass.SpentToken, int, error) {
// Taken the first new token, we need a lock here because tokens can be appended by MakePayment
// which could result in weird behaviour...
ta.tokenLock.Lock()
defer ta.tokenLock.Unlock()
if len(ta.tokens) == 0 {
return privacypass.SpentToken{}, len(ta.tokens), errors.New("no more tokens")
token, numtokens, err := ta.tokenBoardHandler.FetchToken(ta.tokenServiceOnion)
if err != nil {
return privacypass.SpentToken{}, numtokens, err
}
token := ta.tokens[0]
ta.tokens = ta.tokens[1:]
return token.SpendToken(append(data, hostname...)), len(ta.tokens), nil
return token.SpendToken(append(data, hostname...)), numtokens, nil
}

View File

@ -13,7 +13,7 @@ type ChunkSpec []uint64
// CreateChunkSpec given a full list of chunks with their downloaded status (true for downloaded, false otherwise)
// derives a list of identifiers of chunks that have not been downloaded yet
func CreateChunkSpec(progress []bool) ChunkSpec {
var chunks ChunkSpec
chunks := ChunkSpec{}
for i, p := range progress {
if !p {
chunks = append(chunks, uint64(i))

View File

@ -15,7 +15,6 @@ import (
)
// FileSharingSubSystem encapsulates the functionality necessary to share and download files via Cwtch
//
type FileSharingSubSystem struct {
// for sharing files
@ -35,9 +34,24 @@ func (fsss *FileSharingSubSystem) ShareFile(fileKey string, serializedManifest s
log.Errorf("could not share file %v", err)
return
}
log.Debugf("sharing file: %v %v", fileKey, serializedManifest)
fsss.activeShares.Store(fileKey, &manifest)
}
// StopFileShare given a file key removes the serialized manifest from consideration by the file sharing
// subsystem. Future requests on this manifest will fail, as will any in-progress chunk requests.
func (fsss *FileSharingSubSystem) StopFileShare(fileKey string) {
fsss.activeShares.Delete(fileKey)
}
// StopAllFileShares removes all active file shares from consideration
func (fsss *FileSharingSubSystem) StopAllFileShares() {
fsss.activeShares.Range(func(key, value interface{}) bool {
fsss.activeShares.Delete(key)
return true
})
}
// FetchManifest given a file key and knowledge of the manifest size in chunks (obtained via an attribute lookup)
// construct a request to download the manifest.
func (fsss *FileSharingSubSystem) FetchManifest(fileKey string, manifestSize uint64) model.PeerMessage {
@ -82,7 +96,7 @@ func (fsss *FileSharingSubSystem) RequestManifestParts(fileKey string) []model.P
if exists {
oldManifest := manifestI.(*Manifest)
serializedOldManifest := oldManifest.Serialize()
log.Debugf("found serialized manifest: %s", serializedOldManifest)
log.Debugf("found serialized manifest")
// copy so we dont get threading issues by modifying the original
// and then redact the file path before sending
@ -130,20 +144,22 @@ func (fsss *FileSharingSubSystem) ReceiveManifestPart(manifestKey string, part [
log.Debugf("storing manifest part %v %v", offset, end)
serializedManifestBytes := []byte(serializedManifest)
copy(serializedManifestBytes[offset:end], part[:])
if len(serializedManifestBytes) > offset && len(serializedManifestBytes) >= end {
copy(serializedManifestBytes[offset:end], part[:])
if len(part) < DefaultChunkSize {
serializedManifestBytes = serializedManifestBytes[0 : len(serializedManifestBytes)-(DefaultChunkSize-len(part))]
}
if len(part) < DefaultChunkSize {
serializedManifestBytes = serializedManifestBytes[0 : len(serializedManifestBytes)-(DefaultChunkSize-len(part))]
}
serializedManifest = string(serializedManifestBytes)
fsss.prospectiveManifests.Store(fileKey, serializedManifest)
log.Debugf("current manifest: [%s]", serializedManifest)
var manifest Manifest
err := json.Unmarshal([]byte(serializedManifest), &manifest)
if err == nil && hex.EncodeToString(manifest.RootHash) == fileKeyParts[0] {
log.Debugf("valid manifest received! %x", manifest.RootHash)
return fileKey, serializedManifest
serializedManifest = string(serializedManifestBytes)
fsss.prospectiveManifests.Store(fileKey, serializedManifest)
log.Debugf("current manifest: [%s]", serializedManifest)
var manifest Manifest
err := json.Unmarshal([]byte(serializedManifest), &manifest)
if err == nil && hex.EncodeToString(manifest.RootHash) == fileKeyParts[0] {
log.Debugf("valid manifest received! %x", manifest.RootHash)
return fileKey, serializedManifest
}
}
}
}

View File

@ -8,8 +8,8 @@ import (
"encoding/json"
"errors"
"fmt"
"git.openprivacy.ca/openprivacy/log"
"io"
"io/ioutil"
"os"
"sync"
)
@ -122,7 +122,7 @@ func (m *Manifest) GetChunkBytes(id uint64) ([]byte, error) {
// LoadManifest reads in a json serialized Manifest from a file
func LoadManifest(filename string) (*Manifest, error) {
bytes, err := ioutil.ReadFile(filename)
bytes, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
@ -201,7 +201,7 @@ func (m *Manifest) StoreChunk(id uint64, contents []byte) (uint64, error) {
// Write the contents of the chunk to the file
_, err = m.openFd.Write(contents)
if err == nil && m.chunkComplete[id] == false {
if err == nil && !m.chunkComplete[id] {
m.chunkComplete[id] = true
m.progress++
}
@ -232,12 +232,15 @@ func (m *Manifest) GetChunkRequest() ChunkSpec {
}
// PrepareDownload creates an empty file of the expected size of the file described by the manifest
// If the file already exists it assume it is the correct file and that it is resuming from when it left off.
// If the file already exists it assumes it is the correct file and that it is resuming from when it left off.
func (m *Manifest) PrepareDownload() error {
m.lock.Lock()
defer m.lock.Unlock()
m.chunkComplete = make([]bool, len(m.Chunks))
if m.ChunkSizeInBytes == 0 || m.FileSizeInBytes == 0 {
return fmt.Errorf("manifest is invalid")
}
if info, err := os.Stat(m.FileName); os.IsNotExist(err) {
useFileName := m.FileName
@ -294,6 +297,12 @@ func (m *Manifest) PrepareDownload() error {
}
break
}
if chunkI >= len(m.Chunks) {
log.Errorf("file is larger than the number of chunks assigned. Assuming manifest was corrupted.")
return fmt.Errorf("file is larger than the number of chunks assigned. Assuming manifest was corrupted")
}
hash := sha512.New()
hash.Write(buf[0:n])
chunkHash := hash.Sum(nil)
@ -319,7 +328,7 @@ func (m *Manifest) Close() {
// Save writes a JSON encoded byte array version of the manifest to path
func (m *Manifest) Save(path string) error {
return ioutil.WriteFile(path, m.Serialize(), 0600)
return os.WriteFile(path, m.Serialize(), 0600)
}
// Serialize returns the manifest as a JSON encoded byte array

View File

@ -3,8 +3,8 @@ package files
import (
"encoding/hex"
"encoding/json"
"io/ioutil"
"math"
"os"
"testing"
)
@ -28,17 +28,22 @@ func TestManifest(t *testing.T) {
t.Logf("%v", manifest)
// Try to tread the chunk
contents, err := manifest.GetChunkBytes(1)
// Try to read the chunk
_, err = manifest.GetChunkBytes(1)
if err == nil {
t.Fatalf("chunk fetch should have thrown an error")
}
contents, err = manifest.GetChunkBytes(0)
_, err = manifest.GetChunkBytes(0)
if err != nil {
t.Fatalf("chunk fetch error: %v", err)
}
contents, err = manifest.GetChunkBytes(0)
_, err = manifest.GetChunkBytes(0)
if err != nil {
t.Fatalf("chunk fetch error: %v", err)
}
_, err = manifest.GetChunkBytes(0)
if err != nil {
t.Fatalf("chunk fetch error: %v", err)
}
@ -46,7 +51,6 @@ func TestManifest(t *testing.T) {
json, _ := json.Marshal(manifest)
t.Logf("%s", json)
t.Logf("%s", contents)
}
func TestManifestLarge(t *testing.T) {
@ -73,7 +77,7 @@ func TestManifestLarge(t *testing.T) {
t.Logf("%v %s", len(json), json)
// Pretend we downloaded the manifest
ioutil.WriteFile("testdata/cwtch.png.manifest", json, 0600)
os.WriteFile("testdata/cwtch.png.manifest", json, 0600)
// Load the manifest from a file
cwtchPngManifest, err := LoadManifest("testdata/cwtch.png.manifest")
@ -89,7 +93,12 @@ func TestManifestLarge(t *testing.T) {
}
// Prepare Download
cwtchPngOutManifest, _ := LoadManifest("testdata/cwtch.png.manifest")
cwtchPngOutManifest, err := LoadManifest("testdata/cwtch.png.manifest")
if err != nil {
t.Fatalf("could not prepare download %v", err)
}
cwtchPngOutManifest.FileName = "testdata/cwtch.out.png"
defer cwtchPngOutManifest.Close()
@ -113,7 +122,20 @@ func TestManifestLarge(t *testing.T) {
t.Fatalf("could not store chunk %v %v", i, err)
}
// Attempt to store the chunk in an invalid position...
_, err = cwtchPngOutManifest.StoreChunk(uint64(i+1), contents)
if err == nil {
t.Fatalf("incorrect chunk store")
}
}
// Attempt to store an invalid chunk...should trigger an error
_, err = cwtchPngOutManifest.StoreChunk(uint64(len(cwtchPngManifest.Chunks)), []byte{0xff})
if err == nil {
t.Fatalf("incorrect chunk store")
}
err = cwtchPngOutManifest.VerifyFile()
if err != nil {
t.Fatalf("could not verify file %v", err)

View File

@ -39,6 +39,12 @@ type EncryptedGroupMessage struct {
Signature []byte
}
// CachedEncryptedGroupMessage provides an encapsulation of the encrypted group message for local caching / error reporting
type CachedEncryptedGroupMessage struct {
EncryptedGroupMessage
Group string
}
// ToBytes converts the encrypted group message to a set of bytes for serialization
func (egm EncryptedGroupMessage) ToBytes() []byte {
data, _ := json.Marshal(egm)

View File

@ -0,0 +1,53 @@
package model
import (
"bytes"
"errors"
)
// PeerMessage is an encapsulation that can be used by higher level applications
type PeerMessage struct {
// ID **must** only contain alphanumeric characters separated by period.
ID string // A unique Message ID (primarily used for acknowledgments)
// Context **must** only contain alphanumeric characters separated by period.
Context string // A unique context identifier i.e. im.cwtch.chat
// Data can contain anything
Data []byte // A data packet.
}
// Serialize constructs an efficient serialized representation
// Format: [ID String] | [Context String] | Binary Data
func (m *PeerMessage) Serialize() []byte {
return append(append([]byte(m.ID+"|"), []byte(m.Context+"|")...), m.Data...)
}
// ParsePeerMessage returns either a deserialized PeerMessage or an error if it is malformed
func ParsePeerMessage(message []byte) (*PeerMessage, error) {
// find the identifier prefix
idTerminator := bytes.IndexByte(message, '|')
if idTerminator != -1 && idTerminator+1 < len(message) {
// find the context terminator prefix
contextbegin := idTerminator + 1
contextTerminator := bytes.IndexByte(message[contextbegin:], '|')
if contextTerminator != -1 {
// check that we have data
dataBegin := contextbegin + contextTerminator + 1
var data []byte
if dataBegin < len(message) {
data = message[dataBegin:]
}
// compile the message
return &PeerMessage{
ID: string(message[0:idTerminator]),
Context: string(message[contextbegin : contextbegin+contextTerminator]),
Data: data,
}, nil
}
}
return nil, errors.New("invalid message")
}

View File

@ -1,8 +0,0 @@
package model
// PeerMessage is an encapsulation that can be used by higher level applications
type PeerMessage struct {
ID string // A unique Message ID (primarily used for acknowledgments)
Context string // A unique context identifier i.e. im.cwtch.chat
Data []byte // The serialized data packet.
}

160
settings/settings.go Normal file
View File

@ -0,0 +1,160 @@
package settings
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model/constants"
"cwtch.im/cwtch/storage/v1"
"encoding/json"
"git.openprivacy.ca/openprivacy/log"
"os"
path "path/filepath"
)
const (
CwtchStarted = event.Type("CwtchStarted")
CwtchStartError = event.Type("CwtchStartError")
UpdateGlobalSettings = event.Type("UpdateGlobalSettings")
)
const GlobalSettingsFilename = "ui.globals"
const saltFile = "SALT"
type NotificationPolicy string
const (
NotificationPolicyMute = NotificationPolicy("NotificationPolicy.Mute")
NotificationPolicyOptIn = NotificationPolicy("NotificationPolicy.OptIn")
NotificationPolicyDefaultAll = NotificationPolicy("NotificationPolicy.DefaultAll")
)
type GlobalSettingsFile struct {
v1.FileStore
}
type GlobalSettings struct {
Locale string
Theme string
ThemeMode string
ThemeImages bool
PreviousPid int64
ExperimentsEnabled bool
Experiments map[string]bool
BlockUnknownConnections bool
NotificationPolicy NotificationPolicy
NotificationContent string
StreamerMode bool
StateRootPane int
FirstTime bool
UIColumnModePortrait string
UIColumnModeLandscape string
DownloadPath string
AllowAdvancedTorConfig bool
CustomTorrc string
UseCustomTorrc bool
UseExternalTor bool
CustomSocksPort int
CustomControlPort int
UseTorCache bool
TorCacheDir string
BlodeuweddPath string
FontScaling float64
DefaultSaveHistory bool
}
var DefaultGlobalSettings = GlobalSettings{
Locale: "en",
Theme: "cwtch",
ThemeMode: "dark",
ThemeImages: false,
PreviousPid: -1,
ExperimentsEnabled: false,
Experiments: map[string]bool{constants.MessageFormattingExperiment: true},
StateRootPane: 0,
FirstTime: true,
BlockUnknownConnections: false,
StreamerMode: false,
UIColumnModePortrait: "DualpaneMode.Single",
UIColumnModeLandscape: "DualpaneMode.CopyPortrait",
NotificationPolicy: "NotificationPolicy.Mute",
NotificationContent: "NotificationContent.SimpleEvent",
DownloadPath: "",
AllowAdvancedTorConfig: false,
CustomTorrc: "",
UseCustomTorrc: false,
CustomSocksPort: -1,
CustomControlPort: -1,
UseTorCache: false,
TorCacheDir: "",
BlodeuweddPath: "",
FontScaling: 1.0, // use the system pixel scaling default
DefaultSaveHistory: false,
}
func InitGlobalSettingsFile(directory string, password string) (*GlobalSettingsFile, error) {
var key [32]byte
salt, err := os.ReadFile(path.Join(directory, saltFile))
if err != nil {
log.Infof("Could not find salt file: %v (creating a new settings file)", err)
var newSalt [128]byte
key, newSalt, err = v1.CreateKeySalt(password)
if err != nil {
log.Errorf("Could not initialize salt: %v", err)
return nil, err
}
err := os.MkdirAll(directory, 0700)
if err != nil {
return nil, err
}
err = os.WriteFile(path.Join(directory, saltFile), newSalt[:], 0600)
if err != nil {
log.Errorf("Could not write salt file: %v", err)
return nil, err
}
} else {
key = v1.CreateKey(password, salt)
}
gsFile := v1.NewFileStore(directory, GlobalSettingsFilename, key)
log.Infof("initialized global settings file: %v", gsFile)
globalSettingsFile := GlobalSettingsFile{
gsFile,
}
return &globalSettingsFile, nil
}
func (globalSettingsFile *GlobalSettingsFile) ReadGlobalSettings() GlobalSettings {
settings := DefaultGlobalSettings
if globalSettingsFile == nil {
log.Errorf("Global Settings File was not Initialized Properly")
return settings
}
settingsBytes, err := globalSettingsFile.Read()
if err != nil {
log.Infof("Could not read global ui settings: %v (assuming this is a first time app deployment...)", err)
return settings //firstTime = true
}
// note: by giving json.Unmarshal settings we are providing it defacto defaults
// from DefaultGlobalSettings
err = json.Unmarshal(settingsBytes, &settings)
if err != nil {
log.Errorf("Could not parse global ui settings: %v\n", err)
// TODO if settings is corrupted, we probably want to alert the UI.
return settings //firstTime = true
}
log.Debugf("Settings: %#v", settings)
return settings
}
func (globalSettingsFile *GlobalSettingsFile) WriteGlobalSettings(globalSettings GlobalSettings) {
bytes, _ := json.Marshal(globalSettings)
// override first time setting
globalSettings.FirstTime = true
err := globalSettingsFile.Write(bytes)
if err != nil {
log.Errorf("Could not write global ui settings: %v\n", err)
}
}

View File

@ -1,94 +1,17 @@
package storage
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/storage/v0"
"cwtch.im/cwtch/storage/v1"
"git.openprivacy.ca/openprivacy/log"
"io/ioutil"
"path"
"strconv"
)
const profileFilename = "profile"
const versionFile = "VERSION"
const currentVersion = 1
// ProfileStore is an interface to managing the storage of Cwtch Profiles
type ProfileStore interface {
Shutdown()
Delete()
GetProfileCopy(timeline bool) *model.Profile
GetNewPeerMessage() *event.Event
GetStatusMessages() []*event.Event
CheckPassword(string) bool
}
// CreateProfileWriterStore creates a profile store backed by a filestore listening for events and saving them
// directory should be $appDir/profiles/$rand
func CreateProfileWriterStore(eventManager event.Manager, directory, password string, profile *model.Profile) ProfileStore {
return v1.CreateProfileWriterStore(eventManager, directory, password, profile)
}
// LoadProfileWriterStore loads a profile store from filestore listening for events and saving them
// directory should be $appDir/profiles/$rand
func LoadProfileWriterStore(eventManager event.Manager, directory, password string) (ProfileStore, error) {
versionCheckUpgrade(directory, password)
return v1.LoadProfileWriterStore(eventManager, directory, password)
}
// ReadProfile reads a profile from storage and returns the profile
// Should only be called for cache refresh of the profile after a ProfileWriterStore has opened
// (and upgraded) the store, and thus supplied the key/salt
func ReadProfile(directory string, key [32]byte, salt [128]byte) (*model.Profile, error) {
return v1.ReadProfile(directory, key, salt)
}
// NewProfile creates a new profile for use in the profile store.
func NewProfile(name string) *model.Profile {
profile := model.GenerateNewProfile(name)
return profile
}
// ********* Versioning and upgrade **********
func detectVersion(directory string) int {
vnumberStr, err := ioutil.ReadFile(path.Join(directory, versionFile))
if err != nil {
return 0
}
vnumber, err := strconv.Atoi(string(vnumberStr))
if err != nil {
log.Errorf("Could not parse VERSION file contents: '%v' - %v\n", vnumber, err)
return -1
}
return vnumber
}
func upgradeV0ToV1(directory, password string) error {
log.Debugln("Attempting storage v0 to v1: Reading v0 profile...")
profile, err := v0.ReadProfile(directory, password)
if err != nil {
return err
}
log.Debugln("Attempting storage v0 to v1: Writing v1 profile...")
return v1.UpgradeV0Profile(profile, directory, password)
}
func versionCheckUpgrade(directory, password string) {
version := detectVersion(directory)
log.Debugf("versionCheck: %v\n", version)
if version == -1 {
return
}
if version == 0 {
err := upgradeV0ToV1(directory, password)
if err != nil {
return
}
//version = 1
}
func LoadProfileWriterStore(directory, password string) (ProfileStore, error) {
return v1.LoadProfileWriterStore(directory, password)
}

View File

@ -1,76 +0,0 @@
// Known race issue with event bus channel closure
package storage
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/storage/v0"
"fmt"
"git.openprivacy.ca/openprivacy/log"
"os"
"testing"
"time"
)
const testingDir = "./testing"
const filenameBase = "testStream"
const password = "asdfqwer"
const line1 = "Hello from storage!"
const testProfileName = "Alice"
const testKey = "key"
const testVal = "value"
const testInitialMessage = "howdy"
const testMessage = "Hello from storage"
func TestProfileStoreUpgradeV0toV1(t *testing.T) {
log.SetLevel(log.LevelDebug)
os.RemoveAll(testingDir)
eventBus := event.NewEventManager()
queue := event.NewQueue()
eventBus.Subscribe(event.ChangePasswordSuccess, queue)
fmt.Println("Creating and initializing v0 profile and store...")
profile := NewProfile(testProfileName)
profile.AddContact("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd", &model.PublicProfile{Attributes: map[string]string{string(model.KeyTypeServerOnion): "2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd"}})
ps1 := v0.NewProfileWriterStore(eventBus, testingDir, password, profile)
groupid, invite, err := profile.StartGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
if err != nil {
t.Errorf("Creating group: %v\n", err)
}
if err != nil {
t.Errorf("Creating group invite: %v\n", err)
}
ps1.AddGroup(invite)
fmt.Println("Sending 200 messages...")
for i := 0; i < 200; i++ {
ps1.AddGroupMessage(groupid, time.Now().Format(time.RFC3339Nano), time.Now().Format(time.RFC3339Nano), profile.Onion, testMessage, []byte{byte(i)})
}
fmt.Println("Shutdown v0 profile store...")
ps1.Shutdown()
fmt.Println("New v1 Profile store...")
ps2, err := LoadProfileWriterStore(eventBus, testingDir, password)
if err != nil {
t.Errorf("Error createing new profileStore with new password: %v\n", err)
return
}
profile2 := ps2.GetProfileCopy(true)
if profile2.Groups[groupid] == nil {
t.Errorf("Failed to load group %v\n", groupid)
return
}
if len(profile2.Groups[groupid].Timeline.Messages) != 200 {
t.Errorf("Failed to load group's 200 messages, instead got %v\n", len(profile2.Groups[groupid].Timeline.Messages))
}
}

View File

@ -1,70 +0,0 @@
package v0
import (
"crypto/rand"
"errors"
"git.openprivacy.ca/openprivacy/log"
"golang.org/x/crypto/nacl/secretbox"
"golang.org/x/crypto/pbkdf2"
"golang.org/x/crypto/sha3"
"io"
"io/ioutil"
"path"
)
// createKey derives a key from a password
func createKey(password string) ([32]byte, [128]byte, error) {
var salt [128]byte
if _, err := io.ReadFull(rand.Reader, salt[:]); err != nil {
log.Errorf("Cannot read from random: %v\n", err)
return [32]byte{}, salt, err
}
dk := pbkdf2.Key([]byte(password), salt[:], 4096, 32, sha3.New512)
var dkr [32]byte
copy(dkr[:], dk)
return dkr, salt, nil
}
//encryptFileData encrypts the cwtchPeer via the specified key.
func encryptFileData(data []byte, key [32]byte) ([]byte, error) {
var nonce [24]byte
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
log.Errorf("Cannot read from random: %v\n", err)
return nil, err
}
encrypted := secretbox.Seal(nonce[:], data, &nonce, &key)
return encrypted, nil
}
//decryptFile decrypts the passed ciphertext into a cwtchPeer via the specified key.
func decryptFile(ciphertext []byte, key [32]byte) ([]byte, error) {
var decryptNonce [24]byte
copy(decryptNonce[:], ciphertext[:24])
decrypted, ok := secretbox.Open(nil, ciphertext[24:], &decryptNonce, &key)
if ok {
return decrypted, nil
}
return nil, errors.New("Failed to decrypt")
}
// Load instantiates a cwtchPeer from the file store
func readEncryptedFile(directory, filename, password string) ([]byte, error) {
encryptedbytes, err := ioutil.ReadFile(path.Join(directory, filename))
if err == nil && len(encryptedbytes) > 128 {
var dkr [32]byte
//Separate the salt from the encrypted bytes, then generate the derived key
salt, encryptedbytes := encryptedbytes[0:128], encryptedbytes[128:]
dk := pbkdf2.Key([]byte(password), salt, 4096, 32, sha3.New512)
copy(dkr[:], dk)
data, err := decryptFile(encryptedbytes, dkr)
if err == nil {
return data, nil
}
return nil, err
}
return nil, err
}

View File

@ -1,46 +0,0 @@
package v0
import (
"io/ioutil"
"path"
)
// fileStore stores a cwtchPeer in an encrypted file
type fileStore struct {
directory string
filename string
password string
}
// FileStore is a primitive around storing encrypted files
type FileStore interface {
Read() ([]byte, error)
Write(data []byte) error
}
// NewFileStore instantiates a fileStore given a filename and a password
func NewFileStore(directory string, filename string, password string) FileStore {
filestore := new(fileStore)
filestore.password = password
filestore.filename = filename
filestore.directory = directory
return filestore
}
func (fps *fileStore) Read() ([]byte, error) {
return readEncryptedFile(fps.directory, fps.filename, fps.password)
}
// write serializes a cwtchPeer to a file
func (fps *fileStore) Write(data []byte) error {
key, salt, _ := createKey(fps.password)
encryptedbytes, err := encryptFileData(data, key)
if err != nil {
return err
}
// the salt for the derived key is appended to the front of the file
encryptedbytes = append(salt[:], encryptedbytes...)
err = ioutil.WriteFile(path.Join(fps.directory, fps.filename), encryptedbytes, 0600)
return err
}

View File

@ -1,120 +0,0 @@
package v0
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"encoding/json"
"fmt"
"os"
"time"
)
const groupIDLen = 32
const peerIDLen = 56
const profileFilename = "profile"
// ProfileStoreV0 is a legacy profile store used now for upgrading legacy profile stores to newer versions
type ProfileStoreV0 struct {
fs FileStore
streamStores map[string]StreamStore // map [groupId|onion] StreamStore
directory string
password string
profile *model.Profile
}
// NewProfileWriterStore returns a profile store backed by a filestore listening for events and saving them
// directory should be $appDir/profiles/$rand
func NewProfileWriterStore(eventManager event.Manager, directory, password string, profile *model.Profile) *ProfileStoreV0 {
os.Mkdir(directory, 0700)
ps := &ProfileStoreV0{fs: NewFileStore(directory, profileFilename, password), password: password, directory: directory, profile: profile, streamStores: map[string]StreamStore{}}
if profile != nil {
ps.save()
}
return ps
}
// ReadProfile reads a profile from storqage and returns the profile
// directory should be $appDir/profiles/$rand
func ReadProfile(directory, password string) (*model.Profile, error) {
os.Mkdir(directory, 0700)
ps := &ProfileStoreV0{fs: NewFileStore(directory, profileFilename, password), password: password, directory: directory, profile: nil, streamStores: map[string]StreamStore{}}
err := ps.Load()
if err != nil {
return nil, err
}
profile := ps.getProfileCopy(true)
return profile, nil
}
/********************************************************************************************/
// AddGroup For testing, adds a group to the profile (and starts a stream store)
func (ps *ProfileStoreV0) AddGroup(invite string) {
gid, err := ps.profile.ProcessInvite(invite)
if err == nil {
ps.save()
group := ps.profile.Groups[gid]
ps.streamStores[group.GroupID] = NewStreamStore(ps.directory, group.LocalID, ps.password)
}
}
// AddGroupMessage for testing, adds a group message
func (ps *ProfileStoreV0) AddGroupMessage(groupid string, timeSent, timeRecvied string, remotePeer, data string, signature []byte) {
received, _ := time.Parse(time.RFC3339Nano, timeRecvied)
sent, _ := time.Parse(time.RFC3339Nano, timeSent)
message := model.Message{Received: received, Timestamp: sent, Message: data, PeerID: remotePeer, Signature: signature, PreviousMessageSig: []byte("PreviousSignature")}
ss, exists := ps.streamStores[groupid]
if exists {
ss.Write(message)
} else {
fmt.Println("ERROR")
}
}
// GetNewPeerMessage is for AppService to call on Reload events, to reseed the AppClient with the loaded peers
func (ps *ProfileStoreV0) GetNewPeerMessage() *event.Event {
message := event.NewEventList(event.NewPeer, event.Identity, ps.profile.LocalID, event.Password, ps.password, event.Status, "running")
return &message
}
// Load instantiates a cwtchPeer from the file store
func (ps *ProfileStoreV0) Load() error {
decrypted, err := ps.fs.Read()
if err != nil {
return err
}
cp := new(model.Profile)
err = json.Unmarshal(decrypted, &cp)
if err == nil {
ps.profile = cp
for gid, group := range cp.Groups {
ss := NewStreamStore(ps.directory, group.LocalID, ps.password)
cp.Groups[gid].Timeline.SetMessages(ss.Read())
ps.streamStores[group.GroupID] = ss
}
}
return err
}
func (ps *ProfileStoreV0) getProfileCopy(timeline bool) *model.Profile {
return ps.profile.GetCopy(timeline)
}
// Shutdown saves the storage system
func (ps *ProfileStoreV0) Shutdown() {
ps.save()
}
/************* Writing *************/
func (ps *ProfileStoreV0) save() error {
bytes, _ := json.Marshal(ps.profile)
return ps.fs.Write(bytes)
}

View File

@ -1,70 +0,0 @@
// Known race issue with event bus channel closure
package v0
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"log"
"os"
"testing"
"time"
)
const testProfileName = "Alice"
const testKey = "key"
const testVal = "value"
const testInitialMessage = "howdy"
const testMessage = "Hello from storage"
// NewProfile creates a new profile for use in the profile store.
func NewProfile(name string) *model.Profile {
profile := model.GenerateNewProfile(name)
return profile
}
func TestProfileStoreWriteRead(t *testing.T) {
log.Println("profile store test!")
os.RemoveAll(testingDir)
eventBus := event.NewEventManager()
profile := NewProfile(testProfileName)
ps1 := NewProfileWriterStore(eventBus, testingDir, password, profile)
profile.SetAttribute(testKey, testVal)
groupid, invite, err := profile.StartGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
if err != nil {
t.Errorf("Creating group: %v\n", err)
}
if err != nil {
t.Errorf("Creating group invite: %v\n", err)
}
ps1.AddGroup(invite)
ps1.AddGroupMessage(groupid, time.Now().Format(time.RFC3339Nano), time.Now().Format(time.RFC3339Nano), ps1.getProfileCopy(true).Onion, testMessage, []byte{byte(0x01)})
ps1.Shutdown()
ps2 := NewProfileWriterStore(eventBus, testingDir, password, nil)
err = ps2.Load()
if err != nil {
t.Errorf("Error createing ProfileStoreV0: %v\n", err)
}
profile = ps2.getProfileCopy(true)
if profile.Name != testProfileName {
t.Errorf("Profile name from loaded profile incorrect. Expected: '%v' Actual: '%v'\n", testProfileName, profile.Name)
}
v, _ := profile.GetAttribute(testKey)
if v != testVal {
t.Errorf("Profile attribute '%v' incorrect. Expected: '%v' Actual: '%v'\n", testKey, testVal, v)
}
group2 := ps2.getProfileCopy(true).Groups[groupid]
if group2 == nil {
t.Errorf("Group not loaded\n")
}
}

View File

@ -1,145 +0,0 @@
package v0
import (
"cwtch.im/cwtch/model"
"encoding/json"
"fmt"
"git.openprivacy.ca/openprivacy/log"
"io/ioutil"
"os"
"path"
"sync"
)
const (
fileStorePartitions = 16
bytesPerFile = 15 * 1024
)
// streamStore is a file-backed implementation of StreamStore using an in memory buffer of ~16KB and a rotating set of files
type streamStore struct {
password string
storeDirectory string
filenameBase string
lock sync.Mutex
// Buffer is used just for current file to write to
messages []model.Message
bufferByteCount int
}
// StreamStore provides a stream like interface to encrypted storage
type StreamStore interface {
Read() []model.Message
Write(m model.Message)
}
// NewStreamStore returns an initialized StreamStore ready for reading and writing
func NewStreamStore(directory string, filenameBase string, password string) (store StreamStore) {
ss := &streamStore{storeDirectory: directory, filenameBase: filenameBase, password: password}
os.Mkdir(ss.storeDirectory, 0700)
ss.initBuffer()
return ss
}
// Read returns all messages from the backing file (not the buffer, for writing to the current file)
func (ss *streamStore) Read() (messages []model.Message) {
ss.lock.Lock()
defer ss.lock.Unlock()
resp := []model.Message{}
for i := fileStorePartitions - 1; i >= 0; i-- {
filename := fmt.Sprintf("%s.%d", ss.filenameBase, i)
bytes, err := readEncryptedFile(ss.storeDirectory, filename, ss.password)
if err != nil {
continue
}
msgs := []model.Message{}
json.Unmarshal([]byte(bytes), &msgs)
resp = append(resp, msgs...)
}
// 2019.10.10 "Acknowledged" & "ReceivedByServer" are added to the struct, populate it as true for old ones without
for i := 0; i < len(resp) && (resp[i].Acknowledged == false && resp[i].ReceivedByServer == false); i++ {
resp[i].Acknowledged = true
resp[i].ReceivedByServer = true
}
return resp
}
// ****** Writing *******/
func (ss *streamStore) WriteN(messages []model.Message) {
ss.lock.Lock()
defer ss.lock.Unlock()
for _, m := range messages {
ss.updateBuffer(m)
if ss.bufferByteCount > bytesPerFile {
ss.updateFile()
log.Debugf("rotating log file")
ss.rotateFileStore()
ss.initBuffer()
}
}
}
// Write adds a GroupMessage to the store
func (ss *streamStore) Write(m model.Message) {
ss.lock.Lock()
defer ss.lock.Unlock()
ss.updateBuffer(m)
ss.updateFile()
if ss.bufferByteCount > bytesPerFile {
log.Debugf("rotating log file")
ss.rotateFileStore()
ss.initBuffer()
}
}
func (ss *streamStore) initBuffer() {
ss.messages = []model.Message{}
ss.bufferByteCount = 0
}
func (ss *streamStore) updateBuffer(m model.Message) {
ss.messages = append(ss.messages, m)
ss.bufferByteCount += (104 * 1.5) + len(m.Message)
}
func (ss *streamStore) updateFile() error {
msgs, err := json.Marshal(ss.messages)
if err != nil {
log.Errorf("Failed to marshal group messages %v\n", err)
}
// ENCRYPT
key, salt, _ := createKey(ss.password)
encryptedMsgs, err := encryptFileData(msgs, key)
if err != nil {
log.Errorf("Failed to encrypt messages: %v\n", err)
return err
}
encryptedMsgs = append(salt[:], encryptedMsgs...)
ioutil.WriteFile(path.Join(ss.storeDirectory, fmt.Sprintf("%s.%d", ss.filenameBase, 0)), encryptedMsgs, 0700)
return nil
}
func (ss *streamStore) rotateFileStore() {
os.Remove(path.Join(ss.storeDirectory, fmt.Sprintf("%s.%d", ss.filenameBase, fileStorePartitions-1)))
for i := fileStorePartitions - 2; i >= 0; i-- {
os.Rename(path.Join(ss.storeDirectory, fmt.Sprintf("%s.%d", ss.filenameBase, i)), path.Join(ss.storeDirectory, fmt.Sprintf("%s.%d", ss.filenameBase, i+1)))
}
}

View File

@ -1,50 +0,0 @@
package v0
import (
"cwtch.im/cwtch/model"
"os"
"testing"
)
const testingDir = "./testing"
const filenameBase = "testStream"
const password = "asdfqwer"
const line1 = "Hello from storage!"
func TestStreamStoreWriteRead(t *testing.T) {
os.Remove(".test.json")
os.RemoveAll(testingDir)
os.Mkdir(testingDir, 0777)
ss1 := NewStreamStore(testingDir, filenameBase, password)
m := model.Message{Message: line1}
ss1.Write(m)
ss2 := NewStreamStore(testingDir, filenameBase, password)
messages := ss2.Read()
if len(messages) != 1 {
t.Errorf("Read messages has wrong length. Expected: 1 Actual: %d\n", len(messages))
}
if messages[0].Message != line1 {
t.Errorf("Read message has wrong content. Expected: '%v' Actual: '%v'\n", line1, messages[0].Message)
}
}
func TestStreamStoreWriteReadRotate(t *testing.T) {
os.Remove(".test.json")
os.RemoveAll(testingDir)
os.Mkdir(testingDir, 0777)
ss1 := NewStreamStore(testingDir, filenameBase, password)
m := model.Message{Message: line1}
for i := 0; i < 400; i++ {
ss1.Write(m)
}
ss2 := NewStreamStore(testingDir, filenameBase, password)
messages := ss2.Read()
if len(messages) != 400 {
t.Errorf("Read messages has wrong length. Expected: 400 Actual: %d\n", len(messages))
}
if messages[0].Message != line1 {
t.Errorf("Read message has wrong content. Expected: '%v' Actual: '%v'\n", line1, messages[0].Message)
}
}

View File

@ -8,8 +8,8 @@ import (
"golang.org/x/crypto/pbkdf2"
"golang.org/x/crypto/sha3"
"io"
"io/ioutil"
"path"
"os"
path "path/filepath"
)
// CreateKeySalt derives a key and salt from a password: returns key, salt, err
@ -35,7 +35,7 @@ func CreateKey(password string, salt []byte) [32]byte {
return dkr
}
//EncryptFileData encrypts the data with the supplied key
// EncryptFileData encrypts the data with the supplied key
func EncryptFileData(data []byte, key [32]byte) ([]byte, error) {
var nonce [24]byte
@ -48,7 +48,7 @@ func EncryptFileData(data []byte, key [32]byte) ([]byte, error) {
return encrypted, nil
}
//DecryptFile decrypts the passed ciphertext with the supplied key.
// DecryptFile decrypts the passed ciphertext with the supplied key.
func DecryptFile(ciphertext []byte, key [32]byte) ([]byte, error) {
var decryptNonce [24]byte
copy(decryptNonce[:], ciphertext[:24])
@ -56,18 +56,14 @@ func DecryptFile(ciphertext []byte, key [32]byte) ([]byte, error) {
if ok {
return decrypted, nil
}
return nil, errors.New("Failed to decrypt")
return nil, errors.New("failed to decrypt")
}
// ReadEncryptedFile reads data from an encrypted file in directory with key
func ReadEncryptedFile(directory, filename string, key [32]byte) ([]byte, error) {
encryptedbytes, err := ioutil.ReadFile(path.Join(directory, filename))
encryptedbytes, err := os.ReadFile(path.Join(directory, filename))
if err == nil {
data, err := DecryptFile(encryptedbytes, key)
if err == nil {
return data, nil
}
return nil, err
return DecryptFile(encryptedbytes, key)
}
return nil, err
}

View File

@ -2,7 +2,6 @@ package v1
import (
"git.openprivacy.ca/openprivacy/log"
"io/ioutil"
"os"
"path"
)
@ -38,7 +37,7 @@ func (fps *fileStore) Write(data []byte) error {
return err
}
err = ioutil.WriteFile(path.Join(fps.directory, fps.filename), encryptedbytes, 0600)
err = os.WriteFile(path.Join(fps.directory, fps.filename), encryptedbytes, 0600)
return err
}

View File

@ -3,117 +3,35 @@ package v1
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"encoding/base64"
"encoding/json"
"git.openprivacy.ca/openprivacy/log"
"io/ioutil"
"os"
"path"
"strconv"
"time"
)
const groupIDLen = 32
const peerIDLen = 56
const profileFilename = "profile"
const version = "1"
const versionFile = "VERSION"
const saltFile = "SALT"
//ProfileStoreV1 storage for profiles and message streams that uses in memory key and fs stored salt instead of in memory password
// ProfileStoreV1 storage for profiles and message streams that uses in memory key and fs stored salt instead of in memory password
type ProfileStoreV1 struct {
fs FileStore
streamStores map[string]StreamStore // map [groupId|onion] StreamStore
directory string
profile *model.Profile
key [32]byte
salt [128]byte
eventManager event.Manager
queue event.Queue
writer bool
}
// CheckPassword returns true if the given password produces the same key as the current stored key, otherwise false.
func (ps *ProfileStoreV1) CheckPassword(checkpass string) bool {
oldkey := CreateKey(checkpass, ps.salt[:])
return oldkey == ps.key
}
// InitV1Directory generates a key and salt from a password, writes a SALT and VERSION file and returns the key and salt
func InitV1Directory(directory, password string) ([32]byte, [128]byte, error) {
os.Mkdir(directory, 0700)
key, salt, err := CreateKeySalt(password)
if err != nil {
log.Errorf("Could not create key for profile store from password: %v\n", err)
return [32]byte{}, [128]byte{}, err
}
if err = ioutil.WriteFile(path.Join(directory, versionFile), []byte(version), 0600); err != nil {
log.Errorf("Could not write version file: %v", err)
return [32]byte{}, [128]byte{}, err
}
if err = ioutil.WriteFile(path.Join(directory, saltFile), salt[:], 0600); err != nil {
log.Errorf("Could not write salt file: %v", err)
return [32]byte{}, [128]byte{}, err
}
return key, salt, nil
}
// CreateProfileWriterStore creates a profile store backed by a filestore listening for events and saving them
// directory should be $appDir/profiles/$rand
func CreateProfileWriterStore(eventManager event.Manager, directory, password string, profile *model.Profile) *ProfileStoreV1 {
key, salt, err := InitV1Directory(directory, password)
if err != nil {
return nil
}
ps := &ProfileStoreV1{fs: NewFileStore(directory, profileFilename, key), key: key, salt: salt, directory: directory, profile: profile, eventManager: eventManager, streamStores: map[string]StreamStore{}, writer: true}
ps.save()
ps.initProfileWriterStore()
return ps
}
func (ps *ProfileStoreV1) initProfileWriterStore() {
ps.queue = event.NewQueue()
go ps.eventHandler()
ps.eventManager.Subscribe(event.SetPeerAuthorization, ps.queue)
ps.eventManager.Subscribe(event.PeerCreated, ps.queue)
ps.eventManager.Subscribe(event.GroupCreated, ps.queue)
ps.eventManager.Subscribe(event.SetAttribute, ps.queue)
ps.eventManager.Subscribe(event.SetPeerAttribute, ps.queue)
ps.eventManager.Subscribe(event.SetGroupAttribute, ps.queue)
ps.eventManager.Subscribe(event.AcceptGroupInvite, ps.queue)
ps.eventManager.Subscribe(event.RejectGroupInvite, ps.queue)
ps.eventManager.Subscribe(event.NewGroup, ps.queue)
ps.eventManager.Subscribe(event.NewMessageFromGroup, ps.queue)
ps.eventManager.Subscribe(event.SendMessageToPeer, ps.queue)
ps.eventManager.Subscribe(event.PeerAcknowledgement, ps.queue)
ps.eventManager.Subscribe(event.NewMessageFromPeer, ps.queue)
ps.eventManager.Subscribe(event.PeerStateChange, ps.queue)
ps.eventManager.Subscribe(event.ServerStateChange, ps.queue)
ps.eventManager.Subscribe(event.DeleteContact, ps.queue)
ps.eventManager.Subscribe(event.DeleteGroup, ps.queue)
ps.eventManager.Subscribe(event.ChangePassword, ps.queue)
ps.eventManager.Subscribe(event.UpdateMessageFlags, ps.queue)
fs FileStore
directory string
profile *model.Profile
key [32]byte
salt [128]byte
}
// LoadProfileWriterStore loads a profile store from filestore listening for events and saving them
// directory should be $appDir/profiles/$rand
func LoadProfileWriterStore(eventManager event.Manager, directory, password string) (*ProfileStoreV1, error) {
salt, err := ioutil.ReadFile(path.Join(directory, saltFile))
func LoadProfileWriterStore(directory, password string) (*ProfileStoreV1, error) {
salt, err := os.ReadFile(path.Join(directory, saltFile))
if err != nil {
return nil, err
}
key := CreateKey(password, salt)
ps := &ProfileStoreV1{fs: NewFileStore(directory, profileFilename, key), key: key, directory: directory, profile: nil, eventManager: eventManager, streamStores: map[string]StreamStore{}, writer: true}
ps := &ProfileStoreV1{fs: NewFileStore(directory, profileFilename, key), key: key, directory: directory, profile: nil}
copy(ps.salt[:], salt)
err = ps.load()
@ -121,163 +39,9 @@ func LoadProfileWriterStore(eventManager event.Manager, directory, password stri
return nil, err
}
ps.initProfileWriterStore()
return ps, nil
}
// ReadProfile reads a profile from storqage and returns the profile
// directory should be $appDir/profiles/$rand
func ReadProfile(directory string, key [32]byte, salt [128]byte) (*model.Profile, error) {
os.Mkdir(directory, 0700)
ps := &ProfileStoreV1{fs: NewFileStore(directory, profileFilename, key), key: key, salt: salt, directory: directory, profile: nil, eventManager: nil, streamStores: map[string]StreamStore{}, writer: true}
err := ps.load()
if err != nil {
return nil, err
}
profile := ps.GetProfileCopy(true)
return profile, nil
}
// UpgradeV0Profile takes a profile (presumably from a V0 store) and creates and writes a V1 store
func UpgradeV0Profile(profile *model.Profile, directory, password string) error {
key, salt, err := InitV1Directory(directory, password)
if err != nil {
return err
}
ps := &ProfileStoreV1{fs: NewFileStore(directory, profileFilename, key), key: key, salt: salt, directory: directory, profile: profile, eventManager: nil, streamStores: map[string]StreamStore{}, writer: true}
ps.save()
for gid, group := range ps.profile.Groups {
ss := NewStreamStore(ps.directory, group.LocalID, ps.key)
ss.WriteN(ps.profile.Groups[gid].Timeline.Messages)
}
return nil
}
// NewProfile creates a new profile for use in the profile store.
func NewProfile(name string) *model.Profile {
profile := model.GenerateNewProfile(name)
return profile
}
// GetNewPeerMessage is for AppService to call on Reload events, to reseed the AppClient with the loaded peers
func (ps *ProfileStoreV1) GetNewPeerMessage() *event.Event {
message := event.NewEventList(event.NewPeer, event.Identity, ps.profile.LocalID, event.Key, string(ps.key[:]), event.Salt, string(ps.salt[:]))
return &message
}
// GetStatusMessages creates an array of status messages for all peers and group servers from current information
func (ps *ProfileStoreV1) GetStatusMessages() []*event.Event {
messages := []*event.Event{}
for _, contact := range ps.profile.Contacts {
message := event.NewEvent(event.PeerStateChange, map[event.Field]string{
event.RemotePeer: string(contact.Onion),
event.ConnectionState: contact.State,
})
messages = append(messages, &message)
}
doneServers := make(map[string]bool)
for _, group := range ps.profile.Groups {
if _, exists := doneServers[group.GroupServer]; !exists {
message := event.NewEvent(event.ServerStateChange, map[event.Field]string{
event.GroupServer: string(group.GroupServer),
event.ConnectionState: group.State,
})
messages = append(messages, &message)
doneServers[group.GroupServer] = true
}
}
return messages
}
// ChangePassword restores all data under a new password's encryption
func (ps *ProfileStoreV1) ChangePassword(oldpass, newpass, eventID string) {
oldkey := CreateKey(oldpass, ps.salt[:])
if oldkey != ps.key {
ps.eventManager.Publish(event.NewEventList(event.ChangePasswordError, event.Error, "Supplied current password does not match", event.EventID, eventID))
return
}
newkey := CreateKey(newpass, ps.salt[:])
newStreamStores := map[string]StreamStore{}
idToNewLocalID := map[string]string{}
// Generate all new StreamStores with the new password and write all the old StreamStore data into these ones
for ssid, ss := range ps.streamStores {
// New ss with new pass and new localID
newlocalID := model.GenerateRandomID()
idToNewLocalID[ssid] = newlocalID
newSS := NewStreamStore(ps.directory, newlocalID, newkey)
newStreamStores[ssid] = newSS
// write whole store
messages := ss.Read()
newSS.WriteN(messages)
}
// Switch over
oldStreamStores := ps.streamStores
ps.streamStores = newStreamStores
for ssid, newLocalID := range idToNewLocalID {
if len(ssid) == groupIDLen {
ps.profile.Groups[ssid].LocalID = newLocalID
} else {
if ps.profile.Contacts[ssid] != nil {
ps.profile.Contacts[ssid].LocalID = newLocalID
} else {
log.Errorf("Unknown Contact: %v. This is probably the result of corrupted development data from fuzzing. This contact will not appear in the new profile.", ssid)
}
}
}
ps.key = newkey
ps.fs.ChangeKey(newkey)
ps.save()
// Clean up
for _, oldss := range oldStreamStores {
oldss.Delete()
}
ps.eventManager.Publish(event.NewEventList(event.ChangePasswordSuccess, event.EventID, eventID))
return
}
func (ps *ProfileStoreV1) save() error {
if ps.writer {
bytes, _ := json.Marshal(ps.profile)
return ps.fs.Write(bytes)
}
return nil
}
func (ps *ProfileStoreV1) regenStreamStore(messages []model.Message, contact string) {
oldss := ps.streamStores[contact]
newLocalID := model.GenerateRandomID()
newSS := NewStreamStore(ps.directory, newLocalID, ps.key)
newSS.WriteN(messages)
if len(contact) == groupIDLen {
ps.profile.Groups[contact].LocalID = newLocalID
} else {
// We can assume this exists as regen stream store should only happen to *update* a message
ps.profile.Contacts[contact].LocalID = newLocalID
}
ps.streamStores[contact] = newSS
ps.save()
oldss.Delete()
}
// load instantiates a cwtchPeer from the file store
func (ps *ProfileStoreV1) load() error {
decrypted, err := ps.fs.Read()
@ -301,34 +65,26 @@ func (ps *ProfileStoreV1) load() error {
}
}
// Check if there is any saved history...
saveHistory, keyExists := contact.GetAttribute(event.SaveHistoryKey)
if !keyExists {
contact.SetAttribute(event.SaveHistoryKey, event.DeleteHistoryDefault)
}
if saveHistory == event.SaveHistoryConfirmed {
if contact.Attributes[event.SaveHistoryKey] == event.SaveHistoryConfirmed {
ss := NewStreamStore(ps.directory, contact.LocalID, ps.key)
cp.Contacts[contact.Onion].Timeline.SetMessages(ss.Read())
ps.streamStores[contact.Onion] = ss
if contact, exists := cp.Contacts[contact.Onion]; exists {
contact.Timeline.SetMessages(ss.Read())
}
}
}
for gid, group := range cp.Groups {
if group.Version == 0 {
log.Infof("group %v is of unsupported version 0. dropping group...\n", group.GroupID)
log.Debugf("group %v is of unsupported version 0. dropping group...\n", group.GroupID)
delete(cp.Groups, gid)
continue
}
ss := NewStreamStore(ps.directory, group.LocalID, ps.key)
cp.Groups[gid].Timeline.SetMessages(ss.Read())
cp.Groups[gid].Timeline.Sort()
ps.streamStores[group.GroupID] = ss
if group, exists := cp.Groups[gid]; exists {
group.Timeline.SetMessages(ss.Read())
group.Timeline.Sort()
}
}
ps.save()
}
return err
@ -338,238 +94,3 @@ func (ps *ProfileStoreV1) load() error {
func (ps *ProfileStoreV1) GetProfileCopy(timeline bool) *model.Profile {
return ps.profile.GetCopy(timeline)
}
func (ps *ProfileStoreV1) eventHandler() {
for {
ev := ps.queue.Next()
log.Debugf("eventHandler event %v %v\n", ev.EventType, ev.EventID)
switch ev.EventType {
case event.SetPeerAuthorization:
err := ps.profile.SetContactAuthorization(ev.Data[event.RemotePeer], model.Authorization(ev.Data[event.Authorization]))
if err == nil {
ps.save()
}
case event.PeerCreated:
var pp *model.PublicProfile
json.Unmarshal([]byte(ev.Data[event.Data]), &pp)
ps.profile.AddContact(ev.Data[event.RemotePeer], pp)
case event.GroupCreated:
var group *model.Group
json.Unmarshal([]byte(ev.Data[event.Data]), &group)
ps.profile.AddGroup(group)
ps.streamStores[group.GroupID] = NewStreamStore(ps.directory, group.LocalID, ps.key)
ps.save()
case event.SetAttribute:
ps.profile.SetAttribute(ev.Data[event.Key], ev.Data[event.Data])
ps.save()
case event.SetPeerAttribute:
contact, exists := ps.profile.GetContact(ev.Data[event.RemotePeer])
if exists {
contact.SetAttribute(ev.Data[event.Key], ev.Data[event.Data])
ps.save()
switch ev.Data[event.Key] {
case event.SaveHistoryKey:
if event.DeleteHistoryConfirmed == ev.Data[event.Data] {
ss, exists := ps.streamStores[ev.Data[event.RemotePeer]]
if exists {
ss.Delete()
delete(ps.streamStores, ev.Data[event.RemotePeer])
}
} else if event.SaveHistoryConfirmed == ev.Data[event.Data] {
_, exists := ps.streamStores[ev.Data[event.RemotePeer]]
if !exists {
ss := NewStreamStore(ps.directory, contact.LocalID, ps.key)
ps.streamStores[ev.Data[event.RemotePeer]] = ss
}
}
default:
{
}
}
} else {
log.Errorf("error setting attribute on peer %v peer does not exist", ev)
}
case event.SetGroupAttribute:
group := ps.profile.GetGroup(ev.Data[event.GroupID])
if group != nil {
group.SetAttribute(ev.Data[event.Key], ev.Data[event.Data])
ps.save()
} else {
log.Errorf("error setting attribute on group %v group does not exist", ev)
}
case event.AcceptGroupInvite:
err := ps.profile.AcceptInvite(ev.Data[event.GroupID])
if err == nil {
ps.save()
} else {
log.Errorf("error accepting group invite")
}
case event.RejectGroupInvite:
ps.profile.RejectInvite(ev.Data[event.GroupID])
ps.save()
case event.NewGroup:
gid, err := ps.profile.ProcessInvite(ev.Data[event.GroupInvite])
if err == nil {
ps.save()
group := ps.profile.Groups[gid]
ps.streamStores[group.GroupID] = NewStreamStore(ps.directory, group.LocalID, ps.key)
} else {
log.Errorf("error storing new group invite: %v (%v)", err, ev)
}
case event.SendMessageToPeer: // cache the message till an ack, then it's given to stream store.
// stream store doesn't support updates, so we don't want to commit it till ack'd
ps.profile.AddSentMessageToContactTimeline(ev.Data[event.RemotePeer], ev.Data[event.Data], time.Now(), ev.EventID)
case event.NewMessageFromPeer:
ps.profile.AddMessageToContactTimeline(ev.Data[event.RemotePeer], ev.Data[event.Data], time.Now())
ps.attemptSavePeerMessage(ev.Data[event.RemotePeer], ev.Data[event.Data], ev.Data[event.TimestampReceived], true)
case event.PeerAcknowledgement:
onion := ev.Data[event.RemotePeer]
eventID := ev.Data[event.EventID]
contact, ok := ps.profile.Contacts[onion]
if ok {
mIdx, ok := contact.UnacknowledgedMessages[eventID]
if ok {
message := contact.Timeline.Messages[mIdx]
ps.attemptSavePeerMessage(onion, message.Message, message.Timestamp.Format(time.RFC3339Nano), false)
}
}
ps.profile.AckSentMessageToPeer(ev.Data[event.RemotePeer], ev.Data[event.EventID])
case event.NewMessageFromGroup:
groupid := ev.Data[event.GroupID]
received, _ := time.Parse(time.RFC3339Nano, ev.Data[event.TimestampReceived])
sent, _ := time.Parse(time.RFC3339Nano, ev.Data[event.TimestampSent])
sig, _ := base64.StdEncoding.DecodeString(ev.Data[event.Signature])
prevsig, _ := base64.StdEncoding.DecodeString(ev.Data[event.PreviousSignature])
message := model.Message{Received: received, Timestamp: sent, Message: ev.Data[event.Data], PeerID: ev.Data[event.RemotePeer], Signature: sig, PreviousMessageSig: prevsig, Acknowledged: true}
ss, exists := ps.streamStores[groupid]
if exists {
// We need to store a local copy of the message...
ps.profile.GetGroup(groupid).Timeline.Insert(&message)
ss.Write(message)
} else {
log.Errorf("error storing new group message: %v stream store does not exist", ev)
}
case event.PeerStateChange:
if _, exists := ps.profile.Contacts[ev.Data[event.RemotePeer]]; exists {
ps.profile.Contacts[ev.Data[event.RemotePeer]].State = ev.Data[event.ConnectionState]
}
case event.ServerStateChange:
for _, group := range ps.profile.Groups {
if group.GroupServer == ev.Data[event.GroupServer] {
group.State = ev.Data[event.ConnectionState]
}
}
case event.DeleteContact:
onion := ev.Data[event.RemotePeer]
ps.profile.DeleteContact(onion)
ps.save()
ss, exists := ps.streamStores[onion]
if exists {
ss.Delete()
delete(ps.streamStores, onion)
}
case event.DeleteGroup:
groupID := ev.Data[event.GroupID]
ps.profile.DeleteGroup(groupID)
ps.save()
ss, exists := ps.streamStores[groupID]
if exists {
ss.Delete()
delete(ps.streamStores, groupID)
}
case event.ChangePassword:
oldpass := ev.Data[event.Password]
newpass := ev.Data[event.NewPassword]
ps.ChangePassword(oldpass, newpass, ev.EventID)
case event.UpdateMessageFlags:
handle := ev.Data[event.Handle]
mIx, err := strconv.Atoi(ev.Data[event.Index])
if err != nil {
log.Errorf("Invalid Message Index: %v", err)
return
}
flags, err := strconv.ParseUint(ev.Data[event.Flags], 2, 64)
if err != nil {
log.Errorf("Invalid Message Flags: %v", err)
return
}
ps.profile.UpdateMessageFlags(handle, mIx, flags)
if len(handle) == groupIDLen {
ps.regenStreamStore(ps.profile.GetGroup(handle).Timeline.Messages, handle)
} else if contact, exists := ps.profile.GetContact(handle); exists {
if exists {
val, _ := contact.GetAttribute(event.SaveHistoryKey)
if val == event.SaveHistoryConfirmed {
ps.regenStreamStore(contact.Timeline.Messages, handle)
}
}
}
default:
log.Debugf("shutting down profile store: %v", ev)
return
}
}
}
// attemptSavePeerMessage checks if the peer has been configured to save history from this peer
// and if so the peer saves the message into history. fromPeer is used to control if the message is saved
// as coming from the remote peer or if it was sent by out profile.
func (ps *ProfileStoreV1) attemptSavePeerMessage(peerID, messageData, timestampeReceived string, fromPeer bool) {
contact, exists := ps.profile.GetContact(peerID)
if exists {
val, _ := contact.GetAttribute(event.SaveHistoryKey)
switch val {
case event.SaveHistoryConfirmed:
{
peerID := peerID
var received time.Time
var message model.Message
if fromPeer {
received, _ = time.Parse(time.RFC3339Nano, timestampeReceived)
message = model.Message{Received: received, Timestamp: received, Message: messageData, PeerID: peerID, Signature: []byte{}, PreviousMessageSig: []byte{}}
} else {
received := time.Now()
message = model.Message{Received: received, Timestamp: received, Message: messageData, PeerID: ps.profile.Onion, Signature: []byte{}, PreviousMessageSig: []byte{}, Acknowledged: true}
}
ss, exists := ps.streamStores[peerID]
if exists {
ss.Write(message)
} else {
log.Errorf("error storing new peer message: %v stream store does not exist", peerID)
}
}
default:
{
}
}
} else {
log.Errorf("error saving message for peer that doesn't exist: %v", peerID)
}
}
// Shutdown shuts down the queue / thread
func (ps *ProfileStoreV1) Shutdown() {
if ps.queue != nil {
ps.queue.Shutdown()
}
}
// Delete removes all stored files for this stored profile
func (ps *ProfileStoreV1) Delete() {
log.Debugf("Delete ProfileStore for %v\n", ps.profile.Onion)
for _, ss := range ps.streamStores {
ss.Delete()
}
ps.fs.Delete()
err := os.RemoveAll(ps.directory)
if err != nil {
log.Errorf("ProfileStore Delete error on RemoveAll on %v was %v\n", ps.directory, err)
}
}

View File

@ -1,159 +0,0 @@
// Known race issue with event bus channel closure
package v1
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"encoding/base64"
"fmt"
"log"
"os"
"testing"
"time"
)
const testProfileName = "Alice"
const testKey = "key"
const testVal = "value"
const testInitialMessage = "howdy"
const testMessage = "Hello from storage"
func TestProfileStoreWriteRead(t *testing.T) {
log.Println("profile store test!")
os.RemoveAll(testingDir)
eventBus := event.NewEventManager()
profile := NewProfile(testProfileName)
// The lightest weight server entry possible (usually we would import a key bundle...)
profile.AddContact("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd", &model.PublicProfile{Attributes: map[string]string{string(model.KeyTypeServerOnion): "2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd"}})
ps1 := CreateProfileWriterStore(eventBus, testingDir, password, profile)
eventBus.Publish(event.NewEvent(event.SetAttribute, map[event.Field]string{event.Key: testKey, event.Data: testVal}))
time.Sleep(1 * time.Second)
groupid, invite, err := profile.StartGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
if err != nil {
t.Errorf("Creating group: %v\n", err)
}
if err != nil {
t.Errorf("Creating group invite: %v\n", err)
}
eventBus.Publish(event.NewEvent(event.NewGroup, map[event.Field]string{event.TimestampReceived: time.Now().Format(time.RFC3339Nano), event.RemotePeer: ps1.GetProfileCopy(true).Onion, event.GroupInvite: string(invite)}))
time.Sleep(1 * time.Second)
eventBus.Publish(event.NewEvent(event.NewMessageFromGroup, map[event.Field]string{
event.GroupID: groupid,
event.TimestampSent: time.Now().Format(time.RFC3339Nano),
event.TimestampReceived: time.Now().Format(time.RFC3339Nano),
event.RemotePeer: ps1.GetProfileCopy(true).Onion,
event.Data: testMessage,
}))
time.Sleep(1 * time.Second)
ps1.Shutdown()
ps2, err := LoadProfileWriterStore(eventBus, testingDir, password)
if err != nil {
t.Errorf("Error createing ProfileStoreV1: %v\n", err)
}
profile = ps2.GetProfileCopy(true)
if profile.Name != testProfileName {
t.Errorf("Profile name from loaded profile incorrect. Expected: '%v' Actual: '%v'\n", testProfileName, profile.Name)
}
v, _ := profile.GetAttribute(testKey)
if v != testVal {
t.Errorf("Profile attribute '%v' inccorect. Expected: '%v' Actual: '%v'\n", testKey, testVal, v)
}
group2 := ps2.GetProfileCopy(true).Groups[groupid]
if group2 == nil {
t.Errorf("Group not loaded\n")
}
}
func TestProfileStoreChangePassword(t *testing.T) {
os.RemoveAll(testingDir)
eventBus := event.NewEventManager()
queue := event.NewQueue()
eventBus.Subscribe(event.ChangePasswordSuccess, queue)
profile := NewProfile(testProfileName)
profile.AddContact("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd", &model.PublicProfile{Attributes: map[string]string{string(model.KeyTypeServerOnion): "2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd"}})
ps1 := CreateProfileWriterStore(eventBus, testingDir, password, profile)
groupid, invite, err := profile.StartGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
if err != nil {
t.Errorf("Creating group: %v\n", err)
}
if err != nil {
t.Errorf("Creating group invite: %v\n", err)
}
eventBus.Publish(event.NewEvent(event.NewGroup, map[event.Field]string{event.TimestampReceived: time.Now().Format(time.RFC3339Nano), event.RemotePeer: ps1.GetProfileCopy(true).Onion, event.GroupInvite: string(invite)}))
time.Sleep(1 * time.Second)
fmt.Println("Sending 200 messages...")
for i := 0; i < 200; i++ {
eventBus.Publish(event.NewEvent(event.NewMessageFromGroup, map[event.Field]string{
event.GroupID: groupid,
event.TimestampSent: time.Now().Format(time.RFC3339Nano),
event.TimestampReceived: time.Now().Format(time.RFC3339Nano),
event.RemotePeer: profile.Onion,
event.Data: testMessage,
event.Signature: base64.StdEncoding.EncodeToString([]byte{byte(i)}),
}))
}
newPass := "qwerty123"
fmt.Println("Sending Change Passwords event...")
eventBus.Publish(event.NewEventList(event.ChangePassword, event.Password, password, event.NewPassword, newPass))
ev := queue.Next()
if ev.EventType != event.ChangePasswordSuccess {
t.Errorf("Unexpected event response detected %v\n", ev.EventType)
return
}
fmt.Println("Sending 10 more messages...")
for i := 0; i < 10; i++ {
eventBus.Publish(event.NewEvent(event.NewMessageFromGroup, map[event.Field]string{
event.GroupID: groupid,
event.TimestampSent: time.Now().Format(time.RFC3339Nano),
event.TimestampReceived: time.Now().Format(time.RFC3339Nano),
event.RemotePeer: profile.Onion,
event.Data: testMessage,
event.Signature: base64.StdEncoding.EncodeToString([]byte{0x01, byte(i)}),
}))
}
time.Sleep(3 * time.Second)
fmt.Println("Shutdown profile store...")
ps1.Shutdown()
fmt.Println("New Profile store...")
ps2, err := LoadProfileWriterStore(eventBus, testingDir, newPass)
if err != nil {
t.Errorf("Error createing new ProfileStoreV1 with new password: %v\n", err)
return
}
profile2 := ps2.GetProfileCopy(true)
if profile2.Groups[groupid] == nil {
t.Errorf("Failed to load group %v\n", groupid)
return
}
if len(profile2.Groups[groupid].Timeline.Messages) != 210 {
t.Errorf("Failed to load group's 210 messages, instead got %v\n", len(profile2.Groups[groupid].Timeline.Messages))
}
}

View File

@ -5,7 +5,6 @@ import (
"encoding/json"
"fmt"
"git.openprivacy.ca/openprivacy/log"
"io/ioutil"
"math"
"os"
"path"
@ -15,7 +14,6 @@ import (
// This number is larger that the recommend chunk size of libsodium secretbox by an order of magnitude.
// Since this code is not performance-sensitive (and is unlikely to gain any significant performance benefit from
// cache-efficient chunking) this size isnt currently a concern.
// TODO: revise and evaluate better storage options after beta”
const (
fileStorePartitions = 128
bytesPerFile = 128 * 1024
@ -94,7 +92,7 @@ func (ss *streamStore) updateFile() error {
return err
}
ioutil.WriteFile(path.Join(ss.storeDirectory, fmt.Sprintf("%s.%d", ss.filenameBase, 0)), encryptedMsgs, 0600)
os.WriteFile(path.Join(ss.storeDirectory, fmt.Sprintf("%s.%d", ss.filenameBase, 0)), encryptedMsgs, 0600)
return nil
}
@ -154,7 +152,7 @@ func (ss *streamStore) WriteN(messages []model.Message) {
ss.lock.Lock()
defer ss.lock.Unlock()
log.Infof("WriteN %v messages\n", len(messages))
log.Debugf("WriteN %v messages\n", len(messages))
i := 0
for _, m := range messages {
ss.updateBuffer(m)

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

View File

@ -0,0 +1,201 @@
package filesharing
import (
"crypto/rand"
app2 "cwtch.im/cwtch/app"
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/functionality/filesharing"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/model/constants"
"cwtch.im/cwtch/peer"
"cwtch.im/cwtch/protocol/connections"
"encoding/base64"
"errors"
"fmt"
"git.openprivacy.ca/openprivacy/connectivity/tor"
"git.openprivacy.ca/openprivacy/log"
"path/filepath"
// Import SQL Cipher
mrand "math/rand"
"os"
"os/user"
"path"
"runtime"
"runtime/pprof"
"testing"
"time"
_ "github.com/mutecomm/go-sqlcipher/v4"
)
func waitForPeerPeerConnection(t *testing.T, peera peer.CwtchPeer, peerb peer.CwtchPeer) {
for {
state := peera.GetPeerState(peerb.GetOnion())
if state == connections.FAILED {
t.Fatalf("%v could not connect to %v", peera.GetOnion(), peerb.GetOnion())
}
if state != connections.AUTHENTICATED {
fmt.Printf("peer %v waiting connect to peer %v, currently: %v\n", peera.GetOnion(), peerb.GetOnion(), connections.ConnectionStateName[state])
time.Sleep(time.Second * 5)
continue
} else {
peerAName, _ := peera.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name)
peerBName, _ := peerb.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name)
fmt.Printf("%v CONNECTED and AUTHED to %v\n", peerAName, peerBName)
break
}
}
}
func TestFileSharing(t *testing.T) {
numGoRoutinesStart := runtime.NumGoroutine()
os.RemoveAll("cwtch.out.png")
os.RemoveAll("cwtch.out.png.manifest")
os.RemoveAll("storage")
os.RemoveAll("tordir")
os.RemoveAll("./download_dir")
log.SetLevel(log.LevelInfo)
os.Mkdir("tordir", 0700)
dataDir := path.Join("tordir", "tor")
os.MkdirAll(dataDir, 0700)
// we don't need real randomness for the port, just to avoid a possible conflict...
socksPort := mrand.Intn(1000) + 9051
controlPort := mrand.Intn(1000) + 9052
// generate a random password
key := make([]byte, 64)
_, err := rand.Read(key)
if err != nil {
panic(err)
}
useCache := os.Getenv("TORCACHE") == "true"
torDataDir := ""
if useCache {
log.Infof("using tor cache")
torDataDir = filepath.Join(dataDir, "data-dir-torcache")
os.MkdirAll(torDataDir, 0700)
} else {
log.Infof("using clean tor data dir")
if torDataDir, err = os.MkdirTemp(dataDir, "data-dir-"); err != nil {
t.Fatalf("could not create data dir")
}
}
tor.NewTorrc().WithSocksPort(socksPort).WithOnionTrafficOnly().WithHashedPassword(base64.StdEncoding.EncodeToString(key)).WithControlPort(controlPort).Build("tordir/tor/torrc")
acn, err := tor.NewTorACNWithAuth("./tordir", path.Join("..", "tor"), torDataDir, controlPort, tor.HashedPasswordAuthenticator{Password: base64.StdEncoding.EncodeToString(key)})
if err != nil {
t.Fatalf("Could not start Tor: %v", err)
}
acn.WaitTillBootstrapped()
defer acn.Close()
app := app2.NewApp(acn, "./storage", app2.LoadAppSettings("./storage"))
usr, err := user.Current()
if err != nil {
t.Fatalf("current user is undefined")
}
cwtchDir := path.Join(usr.HomeDir, ".cwtch")
os.Mkdir(cwtchDir, 0700)
os.RemoveAll(path.Join(cwtchDir, "testing"))
os.Mkdir(path.Join(cwtchDir, "testing"), 0700)
t.Logf("Creating Alice...")
app.CreateProfile("alice", "asdfasdf", true)
t.Logf("Creating Bob...")
app.CreateProfile("bob", "asdfasdf", true)
t.Logf("** Waiting for Alice, Bob...")
alice := app2.WaitGetPeer(app, "alice")
app.ActivatePeerEngine(alice.GetOnion())
app.ConfigureConnections(alice.GetOnion(), true, true, true)
bob := app2.WaitGetPeer(app, "bob")
app.ActivatePeerEngine(bob.GetOnion())
app.ConfigureConnections(bob.GetOnion(), true, true, true)
alice.AutoHandleEvents([]event.Type{event.PeerStateChange, event.NewRetValMessageFromPeer})
bob.AutoHandleEvents([]event.Type{event.PeerStateChange, event.NewRetValMessageFromPeer})
// Turn on File Sharing Experiment...
settings := app.ReadSettings()
settings.ExperimentsEnabled = true
settings.DownloadPath = "./download_dir"
os.RemoveAll(path.Join(settings.DownloadPath, "cwtch.png"))
os.RemoveAll(path.Join(settings.DownloadPath, "cwtch.png.manifest"))
os.MkdirAll(settings.DownloadPath, 0700)
settings.Experiments[constants.FileSharingExperiment] = true
// Turn Auto Downloading On... (Part of the Image Previews / Profile Images Experiment)
settings.Experiments[constants.ImagePreviewsExperiment] = true
app.UpdateSettings(settings)
t.Logf("** Launching Peers...")
waitTime := time.Duration(30) * time.Second
t.Logf("** Waiting for Alice, Bob to connect with onion network... (%v)\n", waitTime)
time.Sleep(waitTime)
bob.NewContactConversation(alice.GetOnion(), model.DefaultP2PAccessControl(), true)
alice.NewContactConversation(bob.GetOnion(), model.DefaultP2PAccessControl(), true)
alice.PeerWithOnion(bob.GetOnion())
json, err := alice.EnhancedGetConversationAccessControlList(1)
if err != nil {
t.Fatalf("Error!: %v", err)
}
t.Logf("alice<->bob ACL: %s", json)
t.Logf("Waiting for alice and Bob to peer...")
waitForPeerPeerConnection(t, alice, bob)
err = alice.AcceptConversation(1)
if err != nil {
t.Fatalf("Error!: %v", err)
}
err = bob.AcceptConversation(1)
if err != nil {
t.Fatalf("Error!: %v", err)
}
t.Logf("Alice and Bob are Connected!!")
filesharingFunctionality := filesharing.FunctionalityGate()
_, fileSharingMessage, err := filesharingFunctionality.ShareFile("cwtch.png", alice)
alice.SendMessage(1, fileSharingMessage)
if err != nil {
t.Fatalf("Error!: %v", err)
}
// test that bob can download and verify the file
// The main difference here is that bob doesn't need to do anything...
// testBobDownloadFile(t, bob, filesharingFunctionality, queueOracle)
// Wait for say...
time.Sleep(30 * time.Second)
if _, err := os.Stat(path.Join(settings.DownloadPath, "cwtch.png")); errors.Is(err, os.ErrNotExist) {
// path/to/whatever does not exist
t.Fatalf("cwtch.png should have been automatically downloaded...")
}
app.Shutdown()
acn.Close()
time.Sleep(20 * time.Second)
numGoRoutinesPostACN := runtime.NumGoroutine()
// Printing out the current goroutines
// Very useful if we are leaking any.
pprof.Lookup("goroutine").WriteTo(os.Stdout, 1)
if numGoRoutinesStart != numGoRoutinesPostACN {
t.Errorf("Number of GoRoutines at start (%v) does not match number of goRoutines after cleanup of peers and servers (%v), clean up failed, leak detected!", numGoRoutinesStart, numGoRoutinesPostACN)
}
}

View File

@ -3,25 +3,27 @@ package testing
import (
"crypto/rand"
app2 "cwtch.im/cwtch/app"
"cwtch.im/cwtch/app/utils"
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/event/bridge"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/model/constants"
"cwtch.im/cwtch/peer"
"cwtch.im/cwtch/protocol/connections"
"encoding/base64"
"encoding/json"
"fmt"
"git.openprivacy.ca/cwtch.im/tapir/primitives/privacypass"
"git.openprivacy.ca/openprivacy/connectivity/tor"
"git.openprivacy.ca/openprivacy/log"
_ "github.com/mutecomm/go-sqlcipher/v4"
mrand "math/rand"
"os"
"os/user"
"path"
"path/filepath"
"runtime"
"runtime/pprof"
"strings"
"strconv"
"testing"
"time"
)
@ -32,63 +34,31 @@ var (
carolLines = []string{"Howdy, thanks!"}
)
func printAndCountVerifedTimeline(t *testing.T, timeline []model.Message) int {
numVerified := 0
for _, message := range timeline {
fmt.Printf("%v %v> %s\n", message.Timestamp, message.PeerID, message.Message)
numVerified++
func waitForRetVal(peer peer.CwtchPeer, convId int, szp attr.ScopedZonedPath) {
for {
_, err := peer.GetConversationAttribute(convId, szp)
if err == nil {
return
}
time.Sleep(time.Second * 5)
}
return numVerified
}
func waitForPeerGroupConnection(t *testing.T, peer peer.CwtchPeer, groupID string) {
peerName, _ := peer.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Name)
for {
fmt.Printf("%v checking group connection...\n", peerName)
state, ok := peer.GetGroupState(groupID)
if ok {
fmt.Printf("Waiting for Peer %v to join group %v - state: %v\n", peerName, groupID, state)
if state == connections.FAILED {
t.Fatalf("%v could not connect to %v", peer.GetOnion(), groupID)
}
if state != connections.SYNCED {
fmt.Printf("peer %v %v waiting connect to group %v, currently: %v\n", peerName, peer.GetOnion(), groupID, connections.ConnectionStateName[state])
time.Sleep(time.Second * 5)
continue
} else {
fmt.Printf("peer %v %v CONNECTED to group %v\n", peerName, peer.GetOnion(), groupID)
break
}
}
time.Sleep(time.Second * 2)
}
return
}
func waitForPeerPeerConnection(t *testing.T, peera peer.CwtchPeer, peerb peer.CwtchPeer) {
for {
state, ok := peera.GetPeerState(peerb.GetOnion())
if ok {
//log.Infof("Waiting for Peer %v to peer with peer: %v - state: %v\n", peera.GetProfile().Name, peerb.GetProfile().Name, state)
if state == connections.FAILED {
t.Fatalf("%v could not connect to %v", peera.GetOnion(), peerb.GetOnion())
}
if state != connections.AUTHENTICATED {
fmt.Printf("peer %v waiting connect to peer %v, currently: %v\n", peera.GetOnion(), peerb.GetOnion(), connections.ConnectionStateName[state])
time.Sleep(time.Second * 5)
continue
} else {
peerAName, _ := peera.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Name)
peerBName, _ := peerb.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Name)
fmt.Printf("%v CONNECTED and AUTHED to %v\n", peerAName, peerBName)
break
}
func checkAndLoadTokens() []*privacypass.Token {
var tokens []*privacypass.Token
data, err := os.ReadFile("../tokens")
if err == nil {
err := json.Unmarshal(data, &tokens)
if err != nil {
log.Errorf("could not load tokens from file")
}
}
return
return tokens
}
func TestCwtchPeerIntegration(t *testing.T) {
// Goroutine Monitoring Start..
numGoRoutinesStart := runtime.NumGoroutine()
log.AddEverythingFromPattern("connectivity")
@ -96,14 +66,19 @@ func TestCwtchPeerIntegration(t *testing.T) {
log.ExcludeFromPattern("connection/connection")
log.ExcludeFromPattern("outbound/3dhauthchannel")
log.ExcludeFromPattern("event/eventmanager")
log.ExcludeFromPattern("pipeBridge")
log.ExcludeFromPattern("tapir")
// checking if we should use the token cache
cachedTokens := checkAndLoadTokens()
if len(cachedTokens) > 7 {
log.Infof("using cached tokens")
}
os.Mkdir("tordir", 0700)
dataDir := path.Join("tordir", "tor")
os.MkdirAll(dataDir, 0700)
// we don't need real randomness for the port, just to avoid a possible conflict...
mrand.Seed(int64(time.Now().Nanosecond()))
socksPort := mrand.Intn(1000) + 9051
controlPort := mrand.Intn(1000) + 9052
@ -114,13 +89,28 @@ func TestCwtchPeerIntegration(t *testing.T) {
panic(err)
}
useCache := os.Getenv("TORCACHE") == "true"
torDataDir := ""
if useCache {
log.Infof("using tor cache")
torDataDir = filepath.Join(dataDir, "data-dir-torcache")
os.MkdirAll(torDataDir, 0700)
} else {
log.Infof("using clean tor data dir")
if torDataDir, err = os.MkdirTemp(dataDir, "data-dir-"); err != nil {
t.Fatalf("could not create data dir")
}
}
tor.NewTorrc().WithSocksPort(socksPort).WithOnionTrafficOnly().WithHashedPassword(base64.StdEncoding.EncodeToString(key)).WithControlPort(controlPort).Build("tordir/tor/torrc")
acn, err := tor.NewTorACNWithAuth("./tordir", path.Join("..", "tor"), controlPort, tor.HashedPasswordAuthenticator{Password: base64.StdEncoding.EncodeToString(key)})
acn, err := tor.NewTorACNWithAuth("./tordir", path.Join("..", "tor"), torDataDir, controlPort, tor.HashedPasswordAuthenticator{Password: base64.StdEncoding.EncodeToString(key)})
if err != nil {
t.Fatalf("Could not start Tor: %v", err)
}
pid, _ := acn.GetPID()
t.Logf("Tor pid: %v", pid)
log.Infof("Waiting for tor to bootstrap...")
acn.WaitTillBootstrapped()
defer acn.Close()
// ***** Cwtch Server management *****
@ -128,337 +118,334 @@ func TestCwtchPeerIntegration(t *testing.T) {
const ServerAddr = "nfhxzvzxinripgdh4t2m4xcy3crf6p4cbhectgckuj3idsjsaotgowad"
serverKeyBundle, _ := base64.StdEncoding.DecodeString(ServerKeyBundleBase64)
app := app2.NewApp(acn, "./storage")
app := app2.NewApp(acn, "./storage", app2.LoadAppSettings("./storage"))
usr, _ := user.Current()
cwtchDir := path.Join(usr.HomeDir, ".cwtch")
os.Mkdir(cwtchDir, 0700)
os.RemoveAll(path.Join(cwtchDir, "testing"))
os.Mkdir(path.Join(cwtchDir, "testing"), 0700)
bridgeClient := bridge.NewPipeBridgeClient(path.Join(cwtchDir, "testing/clientPipe"), path.Join(cwtchDir, "testing/servicePipe"))
bridgeService := bridge.NewPipeBridgeService(path.Join(cwtchDir, "testing/servicePipe"), path.Join(cwtchDir, "testing/clientPipe"))
appClient := app2.NewAppClient("./storage", bridgeClient)
appService := app2.NewAppService(acn, "./storage", bridgeService)
numGoRoutinesPostAppStart := runtime.NumGoroutine()
// ***** cwtchPeer setup *****
// Turn on Groups Experiment...
settings := app.ReadSettings()
settings.ExperimentsEnabled = true
settings.Experiments[constants.GroupsExperiment] = true
app.UpdateSettings(settings)
fmt.Println("Creating Alice...")
app.CreatePeer("alice", "asdfasdf")
log.Infoln("Creating Alice...")
app.CreateProfile("Alice", "asdfasdf", true)
fmt.Println("Creating Bob...")
app.CreatePeer("bob", "asdfasdf")
log.Infoln("Creating Bob...")
app.CreateProfile("Bob", "asdfasdf", true)
fmt.Println("Creating Carol...")
appClient.CreatePeer("carol", "asdfasdf")
log.Infoln("Creating Carol...")
app.CreateProfile("Carol", "asdfasdf", true)
alice := utils.WaitGetPeer(app, "alice")
fmt.Println("Alice created:", alice.GetOnion())
alice.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, "Alice")
alice := app2.WaitGetPeer(app, "Alice")
aliceBus := app.GetEventBus(alice.GetOnion())
app.ActivatePeerEngine(alice.GetOnion())
app.ConfigureConnections(alice.GetOnion(), true, true, true)
log.Infoln("Alice created:", alice.GetOnion())
// alice.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, "Alice") <- This is now done automatically by ProfileValueExtension, keeping this here for clarity
alice.AutoHandleEvents([]event.Type{event.PeerStateChange, event.ServerStateChange, event.NewGroupInvite, event.NewRetValMessageFromPeer})
bob := utils.WaitGetPeer(app, "bob")
fmt.Println("Bob created:", bob.GetOnion())
bob.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, "Bob")
bob := app2.WaitGetPeer(app, "Bob")
bobBus := app.GetEventBus(bob.GetOnion())
app.ActivatePeerEngine(bob.GetOnion())
app.ConfigureConnections(bob.GetOnion(), true, true, true)
log.Infoln("Bob created:", bob.GetOnion())
// bob.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, "Bob") <- This is now done automatically by ProfileValueExtension, keeping this here for clarity
bob.AutoHandleEvents([]event.Type{event.PeerStateChange, event.ServerStateChange, event.NewGroupInvite, event.NewRetValMessageFromPeer})
carol := utils.WaitGetPeer(appClient, "carol")
fmt.Println("Carol created:", carol.GetOnion())
carol.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, "Carol")
carol := app2.WaitGetPeer(app, "Carol")
carolBus := app.GetEventBus(carol.GetOnion())
app.ActivatePeerEngine(carol.GetOnion())
app.ConfigureConnections(carol.GetOnion(), true, true, true)
log.Infoln("Carol created:", carol.GetOnion())
// carol.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, "Carol") <- This is now done automatically by ProfileValueExtension, keeping this here for clarity
carol.AutoHandleEvents([]event.Type{event.PeerStateChange, event.ServerStateChange, event.NewGroupInvite, event.NewRetValMessageFromPeer})
app.LaunchPeers()
appClient.LaunchPeers()
waitTime := time.Duration(60) * time.Second
t.Logf("** Waiting for Alice, Bob, and Carol to connect with onion network... (%v)\n", waitTime)
log.Infof("** Waiting for Alice, Bob, and Carol to register their onion hidden service on the network... (%v)\n", waitTime)
time.Sleep(waitTime)
numGoRoutinesPostPeerStart := runtime.NumGoroutine()
fmt.Println("** Wait Done!")
log.Infof("** Wait Done!")
// ***** Peering, server joining, group creation / invite *****
fmt.Println("Alice joining server...")
if err := alice.AddServer(string(serverKeyBundle)); err != nil {
t.Fatalf("Failed to Add Server Bundle %v", err)
log.Infoln("Alice and Bob creating conversations...")
// Simulate Alice Adding Bob
log.Infof(" alice.NewConvo(bob)...")
alice2bobConversationID, err := alice.NewContactConversation(bob.GetOnion(), model.DefaultP2PAccessControl(), true)
if err != nil {
t.Fatalf("error adding conversaiton %v", alice2bobConversationID)
}
log.Infof(" bob.NewConvo(alice)...")
bob2aliceConversationID, err := bob.NewContactConversation(alice.GetOnion(), model.DefaultP2PAccessControl(), true)
if err != nil {
t.Fatalf("error adding conversaiton %v", bob2aliceConversationID)
}
alice.JoinServer(ServerAddr)
fmt.Println("Alice peering with Bob...")
log.Infof("Alice and Carol creating conversations...")
// Simulate Alice Adding Carol
alice2carolConversationID, err := alice.NewContactConversation(carol.GetOnion(), model.DefaultP2PAccessControl(), true)
if err != nil {
t.Fatalf("error adding conversaiton %v", alice2carolConversationID)
}
carol2aliceConversationID, err := carol.NewContactConversation(alice.GetOnion(), model.DefaultP2PAccessControl(), true)
if err != nil {
t.Fatalf("error adding conversaiton %v", carol2aliceConversationID)
}
log.Infof("Alice peering with Bob...")
alice.PeerWithOnion(bob.GetOnion())
fmt.Println("Alice peering with Carol...")
log.Infof("Alice Peering with Carol...")
alice.PeerWithOnion(carol.GetOnion())
fmt.Println("Creating group on ", ServerAddr, "...")
groupID, _, err := alice.StartGroup(ServerAddr)
fmt.Printf("Created group: %v!\n", groupID)
// Test that we can rekey alice without issues...
err = alice.ChangePassword("asdfasdf", "password 1 2 3", "password 1 2 3")
if err != nil {
t.Errorf("Failed to init group: %v", err)
return
t.Fatalf("error changing password for Alice: %v", err)
}
fmt.Println("Waiting for alice to join server...")
waitForPeerGroupConnection(t, alice, groupID)
if !alice.CheckPassword("password 1 2 3") {
t.Fatalf("Alice password did not change...")
}
fmt.Println("Waiting for alice and Bob to peer...")
waitForPeerPeerConnection(t, alice, bob)
// Need to add contact else SetContactAuth fails on peer peer doesnt exist
// Normal flow would be Bob app monitors for the new connection (a new connection state change to Auth
// and the adds the user to peer, and then approves or blocks it
bob.AddContact("alice?", alice.GetOnion(), model.AuthApproved)
bob.AddServer(string(serverKeyBundle))
bob.SetContactAuthorization(alice.GetOnion(), model.AuthApproved)
WaitForConnection(t, alice, bob.GetOnion(), connections.AUTHENTICATED)
WaitForConnection(t, alice, carol.GetOnion(), connections.AUTHENTICATED)
WaitForConnection(t, bob, alice.GetOnion(), connections.AUTHENTICATED)
WaitForConnection(t, carol, alice.GetOnion(), connections.AUTHENTICATED)
waitForPeerPeerConnection(t, alice, carol)
carol.AddContact("alice?", alice.GetOnion(), model.AuthApproved)
carol.AddServer(string(serverKeyBundle))
carol.SetContactAuthorization(alice.GetOnion(), model.AuthApproved)
log.Infof("Alice and Bob getVal public.name...")
fmt.Println("Alice and Bob getVal public.name...")
alice.SendScopedZonedGetValToContact(alice2bobConversationID, attr.PublicScope, attr.ProfileZone, constants.Name)
bob.SendScopedZonedGetValToContact(bob2aliceConversationID, attr.PublicScope, attr.ProfileZone, constants.Name)
alice.SendScopedZonedGetValToContact(bob.GetOnion(), attr.PublicScope, attr.ProfileZone, constants.Name)
bob.SendScopedZonedGetValToContact(alice.GetOnion(), attr.PublicScope, attr.ProfileZone, constants.Name)
alice.SendScopedZonedGetValToContact(carol.GetOnion(), attr.PublicScope, attr.ProfileZone, constants.Name)
carol.SendScopedZonedGetValToContact(alice.GetOnion(), attr.PublicScope, attr.ProfileZone, constants.Name)
alice.SendScopedZonedGetValToContact(alice2carolConversationID, attr.PublicScope, attr.ProfileZone, constants.Name)
carol.SendScopedZonedGetValToContact(carol2aliceConversationID, attr.PublicScope, attr.ProfileZone, constants.Name)
// This used to be 10, but increasing it to 30 because this is now causing frequent issues
// Probably related to latency/throughput problems in the underlying tor network.
time.Sleep(30 * time.Second)
aliceName, exists := bob.GetContactAttribute(alice.GetOnion(), attr.GetPeerScope(constants.Name))
if !exists || aliceName != "Alice" {
t.Fatalf("Bob: alice GetKeyVal error on alice peer.name %v\n", exists)
waitForRetVal(bob, bob2aliceConversationID, attr.PublicScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Name)))
aliceName, err := bob.GetConversationAttribute(bob2aliceConversationID, attr.PublicScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Name)))
if err != nil || aliceName != "Alice" {
t.Fatalf("Bob: alice GetKeyVal error on alice peer.name %v: %v\n", aliceName, err)
}
fmt.Printf("Bob has alice's name as '%v'\n", aliceName)
log.Infof("Bob has alice's name as '%v'\n", aliceName)
bobName, exists := alice.GetContactAttribute(bob.GetOnion(), attr.GetPeerScope(constants.Name))
if !exists || bobName != "Bob" {
t.Fatalf("Alice: bob GetKeyVal error on bob peer.name\n")
waitForRetVal(alice, alice2bobConversationID, attr.PublicScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Name)))
bobName, err := alice.GetConversationAttribute(alice2bobConversationID, attr.PublicScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Name)))
if err != nil || bobName != "Bob" {
t.Fatalf("Alice: bob GetKeyVal error on bob peer.name %v: %v \n", bobName, err)
}
fmt.Printf("Alice has bob's name as '%v'\n", bobName)
log.Infof("Alice has bob's name as '%v'\n", bobName)
aliceName, exists = carol.GetContactAttribute(alice.GetOnion(), attr.GetPeerScope(constants.Name))
if !exists || aliceName != "Alice" {
t.Fatalf("carol GetKeyVal error for alice peer.name %v\n", exists)
waitForRetVal(carol, carol2aliceConversationID, attr.PublicScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Name)))
aliceName, err = carol.GetConversationAttribute(carol2aliceConversationID, attr.PublicScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Name)))
if err != nil || aliceName != "Alice" {
t.Fatalf("carol GetKeyVal error for alice peer.name %v: %v\n", aliceName, err)
}
carolName, exists := alice.GetContactAttribute(carol.GetOnion(), attr.GetPeerScope(constants.Name))
if !exists || carolName != "Carol" {
t.Fatalf("alice GetKeyVal error, carol peer.name\n")
waitForRetVal(alice, alice2carolConversationID, attr.PublicScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Name)))
carolName, err := alice.GetConversationAttribute(alice2carolConversationID, attr.PublicScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Name)))
if err != nil || carolName != "Carol" {
t.Fatalf("alice GetKeyVal error, carol peer.name: %v: %v\n", carolName, err)
}
fmt.Printf("Alice has carol's name as '%v'\n", carolName)
log.Infof("Alice has carol's name as '%v'\n", carolName)
fmt.Println("Alice inviting Bob to group...")
err = alice.InviteOnionToGroup(bob.GetOnion(), groupID)
// Group Testing
usedTokens := len(aliceLines)
// Simulate Alice Creating a Group
log.Infoln("Alice joining server...")
if serverOnion, err := alice.AddServer(string(serverKeyBundle)); err != nil {
if len(cachedTokens) > len(aliceLines) {
alice.StoreCachedTokens(serverOnion, cachedTokens[0:len(aliceLines)])
}
t.Fatalf("Failed to Add Server Bundle %v", err)
}
// Creating a Group
log.Infof("Creating group on %v...", ServerAddr)
aliceGroupConversationID, err := alice.StartGroup("Our Cool Testing Group", ServerAddr)
log.Infof("Created group: %v!\n", aliceGroupConversationID)
if err != nil {
t.Errorf("Failed to init group: %v", err)
return
}
// Invites
log.Infoln("Alice inviting Bob to group...")
_, err = alice.SendInviteToConversation(alice2bobConversationID, aliceGroupConversationID)
if err != nil {
t.Fatalf("Error for Alice inviting Bob to group: %v", err)
}
time.Sleep(time.Second * 5)
fmt.Println("Bob examining groups and accepting invites...")
for _, message := range bob.GetContact(alice.GetOnion()).Timeline.GetMessages() {
fmt.Printf("Found message from Alice: %v", message.Message)
if strings.HasPrefix(message.Message, "torv3") {
gid, err := bob.ImportGroup(message.Message)
if err == nil {
fmt.Printf("Bob found invite...now accepting %v...", gid)
bob.AcceptInvite(gid)
} else {
t.Fatalf("Bob could not accept invite...%v", gid)
}
}
// Alice invites Bob to the Group...
message, _, err := bob.GetChannelMessage(bob2aliceConversationID, 0, 1)
log.Infof("Alice message to Bob %v %v", message, err)
var overlayMessage model.MessageWrapper
json.Unmarshal([]byte(message), &overlayMessage)
log.Infof("Parsed Overlay Message: %v", overlayMessage)
err = bob.ImportBundle(overlayMessage.Data)
log.Infof("Result of Bob Importing the Bundle from Alice: %v", err)
if len(cachedTokens) > (usedTokens + len(bobLines)) {
bob.StoreCachedTokens(ServerAddr, cachedTokens[usedTokens:usedTokens+len(bobLines)])
usedTokens += len(bobLines)
}
fmt.Println("Waiting for Bob to join connect to group server...")
waitForPeerGroupConnection(t, bob, groupID)
log.Infof("Waiting for alice to join server...")
WaitForConnection(t, alice, ServerAddr, connections.SYNCED)
log.Infof("Waiting for Bob to join connect to group server...")
WaitForConnection(t, bob, ServerAddr, connections.SYNCED)
// 1 = Alice
// 2 = Server
// 3 = Group...
bobGroupConversationID := 3
numGoRoutinesPostServerConnect := runtime.NumGoroutine()
// ***** Conversation *****
log.Infof("Starting conversation in group...")
checkSendMessageToGroup(t, alice, aliceBus, aliceGroupConversationID, aliceLines[0])
checkSendMessageToGroup(t, bob, bobBus, bobGroupConversationID, bobLines[0])
checkSendMessageToGroup(t, alice, aliceBus, aliceGroupConversationID, aliceLines[1])
checkSendMessageToGroup(t, bob, bobBus, bobGroupConversationID, bobLines[1])
fmt.Println("Starting conversation in group...")
// Conversation
fmt.Printf("%v> %v\n", aliceName, aliceLines[0])
err = alice.SendMessage(groupID, aliceLines[0])
if err != nil {
t.Fatalf("Alice failed to send a message to the group: %v", err)
// Pretend that Carol Acquires the Overlay Message through some other means...
json.Unmarshal([]byte(message), &overlayMessage)
log.Infof("Parsed Overlay Message: %v", overlayMessage)
err = carol.ImportBundle(overlayMessage.Data)
log.Infof("Result of Carol Importing the Bundle from Alice: %v", err)
log.Infof("Waiting for Carol to join connect to group server...")
carolGroupConversationID := 3
if len(cachedTokens) > (usedTokens + len(carolLines)) {
carol.StoreCachedTokens(ServerAddr, cachedTokens[usedTokens:usedTokens+len(carolLines)])
}
time.Sleep(time.Second * 10)
WaitForConnection(t, carol, ServerAddr, connections.SYNCED)
numGoRoutinesPostCarolConnect := runtime.NumGoroutine()
fmt.Printf("%v> %v\n", bobName, bobLines[0])
err = bob.SendMessage(groupID, bobLines[0])
if err != nil {
t.Fatalf("Bob failed to send a message to the group: %v", err)
}
time.Sleep(time.Second * 10)
// Check Alice Timeline
log.Infof("Checking Alice's Timeline...")
checkMessage(t, alice, aliceGroupConversationID, 1, aliceLines[0])
checkMessage(t, alice, aliceGroupConversationID, 2, bobLines[0])
checkMessage(t, alice, aliceGroupConversationID, 3, aliceLines[1])
checkMessage(t, alice, aliceGroupConversationID, 4, bobLines[1])
fmt.Printf("%v> %v\n", aliceName, aliceLines[1])
alice.SendMessage(groupID, aliceLines[1])
time.Sleep(time.Second * 10)
fmt.Printf("%v> %v\n", bobName, bobLines[1])
bob.SendMessage(groupID, bobLines[1])
time.Sleep(time.Second * 10)
fmt.Println("Alice inviting Carol to group...")
err = alice.InviteOnionToGroup(carol.GetOnion(), groupID)
if err != nil {
t.Fatalf("Error for Alice inviting Carol to group: %v", err)
}
time.Sleep(time.Second * 60) // Account for some token acquisition in Alice and Bob flows.
fmt.Println("Carol examining groups and accepting invites...")
for _, message := range carol.GetContact(alice.GetOnion()).Timeline.GetMessages() {
fmt.Printf("Found message from Alice: %v", message.Message)
if strings.HasPrefix(message.Message, "torv3") {
gid, err := carol.ImportGroup(message.Message)
if err == nil {
fmt.Printf("Carol found invite...now accepting %v...", gid)
carol.AcceptInvite(gid)
} else {
t.Fatalf("Carol could not accept invite...%v", gid)
}
}
}
fmt.Println("Shutting down Alice...")
log.Infof("Shutting down Alice...")
app.ShutdownPeer(alice.GetOnion())
time.Sleep(time.Second * 5)
time.Sleep(time.Second * 3)
numGoRoutinesPostAlice := runtime.NumGoroutine()
fmt.Println("Carol joining server...")
carol.JoinServer(ServerAddr)
waitForPeerGroupConnection(t, carol, groupID)
numGoRotinesPostCarolConnect := runtime.NumGoroutine()
checkSendMessageToGroup(t, carol, carolBus, carolGroupConversationID, carolLines[0])
checkSendMessageToGroup(t, bob, bobBus, bobGroupConversationID, bobLines[2])
fmt.Printf("%v> %v", bobName, bobLines[2])
bob.SendMessage(groupID, bobLines[2])
// Bob should have enough tokens so we don't need to account for
// token acquisition here...
// Time to Sync
time.Sleep(time.Second * 10)
fmt.Printf("%v> %v", carolName, carolLines[0])
carol.SendMessage(groupID, carolLines[0])
time.Sleep(time.Second * 30) // we need to account for spam-based token acquisition, but everything should
// be warmed-up and delays should be pretty small.
// Check Bob Timeline
log.Infof("Checking Bob's Timeline...")
checkMessage(t, bob, bobGroupConversationID, 1, aliceLines[0])
checkMessage(t, bob, bobGroupConversationID, 2, bobLines[0])
checkMessage(t, bob, bobGroupConversationID, 3, aliceLines[1])
checkMessage(t, bob, bobGroupConversationID, 4, bobLines[1])
checkMessage(t, bob, bobGroupConversationID, 5, carolLines[0])
checkMessage(t, bob, bobGroupConversationID, 6, bobLines[2])
// ***** Verify Test *****
// Check Carol Timeline
log.Infof("Checking Carols's Timeline...")
checkMessage(t, carol, carolGroupConversationID, 1, aliceLines[0])
checkMessage(t, carol, carolGroupConversationID, 2, bobLines[0])
checkMessage(t, carol, carolGroupConversationID, 3, aliceLines[1])
checkMessage(t, carol, carolGroupConversationID, 4, bobLines[1])
checkMessage(t, carol, carolGroupConversationID, 5, carolLines[0])
checkMessage(t, carol, carolGroupConversationID, 6, bobLines[2])
fmt.Println("Final syncing time...")
time.Sleep(time.Second * 30)
// Have bob clean up some conversations...
log.Infof("Bob cleanup conversation")
bob.DeleteConversation(1)
alicesGroup := alice.GetGroup(groupID)
if alicesGroup == nil {
t.Error("aliceGroup == nil")
return
}
fmt.Printf("Alice's TimeLine:\n")
aliceVerified := printAndCountVerifedTimeline(t, alicesGroup.GetTimeline())
if aliceVerified != 4 {
t.Errorf("Alice did not have 4 verified messages")
}
bobsGroup := bob.GetGroup(groupID)
if bobsGroup == nil {
t.Error("bobGroup == nil")
return
}
fmt.Printf("Bob's TimeLine:\n")
bobVerified := printAndCountVerifedTimeline(t, bobsGroup.GetTimeline())
if bobVerified != 6 {
t.Errorf("Bob did not have 6 verified messages")
}
carolsGroup := carol.GetGroup(groupID)
fmt.Printf("Carol's TimeLine:\n")
carolVerified := printAndCountVerifedTimeline(t, carolsGroup.GetTimeline())
if carolVerified != 6 {
t.Errorf("Carol did not have 6 verified messages")
}
if len(alicesGroup.GetTimeline()) != 4 {
t.Errorf("Alice's timeline does not have all messages")
} else {
// check message 0,1,2,3
aliceGroupTimeline := alicesGroup.GetTimeline()
if aliceGroupTimeline[0].Message != aliceLines[0] || aliceGroupTimeline[1].Message != bobLines[0] ||
aliceGroupTimeline[2].Message != aliceLines[1] || aliceGroupTimeline[3].Message != bobLines[1] {
t.Errorf("Some of Alice's timeline messages did not have the expected content!")
}
}
if len(bobsGroup.GetTimeline()) != 6 {
t.Errorf("Bob's timeline does not have all messages")
} else {
// check message 0,1,2,3,4,5
bobGroupTimeline := bobsGroup.GetTimeline()
if bobGroupTimeline[0].Message != aliceLines[0] || bobGroupTimeline[1].Message != bobLines[0] ||
bobGroupTimeline[2].Message != aliceLines[1] || bobGroupTimeline[3].Message != bobLines[1] ||
bobGroupTimeline[4].Message != bobLines[2] || bobGroupTimeline[5].Message != carolLines[0] {
t.Errorf("Some of Bob's timeline messages did not have the expected content!")
}
}
if len(carolsGroup.GetTimeline()) != 6 {
t.Errorf("Carol's timeline does not have all messages")
} else {
// check message 0,1,2,3,4,5
carolGroupTimeline := carolsGroup.GetTimeline()
if carolGroupTimeline[0].Message != aliceLines[0] || carolGroupTimeline[1].Message != bobLines[0] ||
carolGroupTimeline[2].Message != aliceLines[1] || carolGroupTimeline[3].Message != bobLines[1] ||
carolGroupTimeline[4].Message != carolLines[0] || carolGroupTimeline[5].Message != bobLines[2] {
t.Errorf("Some of Carol's timeline messages did not have the expected content!")
}
}
fmt.Println("Shutting down Bob...")
log.Infof("Shutting down Bob...")
app.ShutdownPeer(bob.GetOnion())
time.Sleep(time.Second * 3)
numGoRoutinesPostBob := runtime.NumGoroutine()
fmt.Println("Shutting down Carol...")
appClient.ShutdownPeer(carol.GetOnion())
log.Infof("Shutting down Carol...")
app.ShutdownPeer(carol.GetOnion())
time.Sleep(time.Second * 3)
numGoRoutinesPostCarol := runtime.NumGoroutine()
fmt.Println("Shutting down apps...")
fmt.Printf("app Shutdown: %v\n", runtime.NumGoroutine())
log.Infof("Shutting down apps...")
log.Infof("app Shutdown: %v\n", runtime.NumGoroutine())
app.Shutdown()
fmt.Printf("appClientShutdown: %v\n", runtime.NumGoroutine())
appClient.Shutdown()
fmt.Printf("appServiceShutdown: %v\n", runtime.NumGoroutine())
appService.Shutdown()
fmt.Printf("bridgeClientShutdown: %v\n", runtime.NumGoroutine())
bridgeClient.Shutdown()
time.Sleep(2 * time.Second)
fmt.Printf("brideServiceShutdown: %v\n", runtime.NumGoroutine())
bridgeService.Shutdown()
time.Sleep(2 * time.Second)
log.Infof("Done shutdown: %v\n", runtime.NumGoroutine())
fmt.Printf("Done shutdown: %v\n", runtime.NumGoroutine())
numGoRoutinesPostAppShutdown := runtime.NumGoroutine()
fmt.Println("Shutting down ACN...")
log.Infof("Shutting down ACN...")
acn.Close()
time.Sleep(time.Second * 2) // Server ^^ has a 5 second loop attempting reconnect before exiting
time.Sleep(time.Second * 30) // the network status plugin might keep goroutines alive for a minute before killing them
numGoRoutinesPostACN := runtime.NumGoroutine()
time.Sleep(time.Second * 60) // the network status / heartbeat plugin might keep goroutines alive for a minute before killing them
numGoRoutinesPostAppShutdown := runtime.NumGoroutine()
// Printing out the current goroutines
// Very useful if we are leaking any.
pprof.Lookup("goroutine").WriteTo(os.Stdout, 1)
fmt.Printf("numGoRoutinesStart: %v\nnumGoRoutinesPostAppStart: %v\nnumGoRoutinesPostPeerStart: %v\nnumGoRoutinesPostPeerAndServerConnect: %v\n"+
"numGoRoutinesPostAlice: %v\nnumGoRotinesPostCarolConnect: %v\nnumGoRoutinesPostBob: %v\nnumGoRoutinesPostCarol: %v\nnumGoRoutinesPostAppShutdown: %v\nnumGoRoutinesPostACN: %v\n",
fmt.Println("")
log.Infof("numGoRoutinesStart: %v\nnumGoRoutinesPostAppStart: %v\nnumGoRoutinesPostPeerStart: %v\nnumGoRoutinesPostPeerAndServerConnect: %v\n"+
"numGoRoutinesPostAlice: %v\nnumGoRoutinesPostCarolConnect: %v\nnumGoRoutinesPostBob: %v\nnumGoRoutinesPostCarol: %v\nnumGoRoutinesPostAppShutdown: %v",
numGoRoutinesStart, numGoRoutinesPostAppStart, numGoRoutinesPostPeerStart, numGoRoutinesPostServerConnect,
numGoRoutinesPostAlice, numGoRotinesPostCarolConnect, numGoRoutinesPostBob, numGoRoutinesPostCarol, numGoRoutinesPostAppShutdown, numGoRoutinesPostACN)
numGoRoutinesPostAlice, numGoRoutinesPostCarolConnect, numGoRoutinesPostBob, numGoRoutinesPostCarol, numGoRoutinesPostAppShutdown)
if numGoRoutinesStart != numGoRoutinesPostACN {
t.Errorf("Number of GoRoutines at start (%v) does not match number of goRoutines after cleanup of peers and servers (%v), clean up failed, leak detected!", numGoRoutinesStart, numGoRoutinesPostACN)
if numGoRoutinesStart != numGoRoutinesPostAppShutdown {
t.Errorf("Number of GoRoutines at start (%v) does not match number of goRoutines after cleanup of peers and servers (%v), clean up failed, v detected!", numGoRoutinesStart, numGoRoutinesPostAppShutdown)
}
}
// Utility function for sending a message from a peer to a group
func checkSendMessageToGroup(t *testing.T, profile peer.CwtchPeer, bus event.Manager, id int, message string) {
name, _ := profile.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name)
log.Infof("%v> %v\n", name, message)
queue := event.NewQueue()
bus.Subscribe(event.IndexedAcknowledgement, queue)
mid, err := profile.SendMessage(id, message)
if err != nil {
log.Errorf("Alice failed to send a message to the group: %v", err)
t.Fatalf("Alice failed to send a message to the group: %v\n", err)
}
log.Infof("Sent message with mid: %v, waiting for ack...", mid)
ev := queue.Next()
switch ev.EventType {
case event.IndexedAcknowledgement:
if evid, err := strconv.Atoi(ev.Data[event.Index]); err == nil && evid == mid {
log.Infof("Message mid acked!")
break
}
}
queue.Shutdown()
time.Sleep(time.Second * 10)
}
// Utility function for testing that a message in a conversation is as expected
func checkMessage(t *testing.T, profile peer.CwtchPeer, id int, messageID int, expected string) {
message, _, err := profile.GetChannelMessage(id, 0, messageID)
log.Debugf(" checking if expected: %v is actual: %v", expected, message)
if err != nil {
log.Errorf("unexpected message %v expected: %v got error: %v", profile.GetOnion(), expected, err)
t.Fatalf("unexpected message %v expected: %v got error: %v\n", profile.GetOnion(), expected, err)
}
if message != expected {
log.Errorf("unexpected message %v expected: %v got: [%v]", profile.GetOnion(), expected, message)
t.Fatalf("unexpected message %v expected: %v got: [%v]\n", profile.GetOnion(), expected, message)
}
}

View File

@ -0,0 +1,202 @@
package encryptedstorage
import (
// Import SQL Cipher
"crypto/rand"
app2 "cwtch.im/cwtch/app"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/constants"
"cwtch.im/cwtch/peer"
"encoding/base64"
"fmt"
"git.openprivacy.ca/openprivacy/connectivity/tor"
"git.openprivacy.ca/openprivacy/log"
_ "github.com/mutecomm/go-sqlcipher/v4"
mrand "math/rand"
"os"
"path"
"path/filepath"
"testing"
"time"
)
func TestEncryptedStorage(t *testing.T) {
log.SetLevel(log.LevelDebug)
os.Mkdir("tordir", 0700)
dataDir := filepath.Join("tordir", "tor")
os.MkdirAll(dataDir, 0700)
// we don't need real randomness for the port, just to avoid a possible conflict...
socksPort := mrand.Intn(1000) + 9051
controlPort := mrand.Intn(1000) + 9052
// generate a random password
key := make([]byte, 64)
_, err := rand.Read(key)
if err != nil {
panic(err)
}
torDataDir := ""
if torDataDir, err = os.MkdirTemp(dataDir, "data-dir-"); err != nil {
t.Fatalf("could not create data dir")
}
tor.NewTorrc().WithSocksPort(socksPort).WithOnionTrafficOnly().WithHashedPassword(base64.StdEncoding.EncodeToString(key)).WithControlPort(controlPort).Build("tordir/tor/torrc")
acn, err := tor.NewTorACNWithAuth("./tordir", path.Join("..", "..", "tor"), torDataDir, controlPort, tor.HashedPasswordAuthenticator{Password: base64.StdEncoding.EncodeToString(key)})
if err != nil {
t.Fatalf("Could not start Tor: %v", err)
}
cwtchDir := path.Join(".", "encrypted_storage_profiles")
os.RemoveAll(cwtchDir)
os.Mkdir(cwtchDir, 0700)
fmt.Println("Creating Alice...")
defer acn.Close()
acn.WaitTillBootstrapped()
app := app2.NewApp(acn, cwtchDir, app2.LoadAppSettings(cwtchDir))
app.CreateProfile("alice", "password", true)
app.CreateProfile("bob", "password", true)
alice := app2.WaitGetPeer(app, "alice")
bob := app2.WaitGetPeer(app, "bob")
alice.Listen()
bob.Listen()
// To keep this large test organized, we will break it down into sub tests...
subTestAliceAddAndDeleteBob(t, alice, bob)
conversations, err := alice.FetchConversations()
if err != nil || len(conversations) != 1 {
t.Fatalf("unexpected issue when fetching all of alices conversations. Expected 1 got : %v %v", conversations, err)
}
aliceOnion := alice.GetOnion()
alice.PeerWithOnion(bob.GetOnion())
time.Sleep(time.Second * 40)
alice.SendMessage(2, "Hello Bob")
if err != nil {
t.Fatalf("alice should have been able to fetch her own message")
}
_, attr, _ := alice.GetChannelMessage(2, 0, 1)
if attr[constants.AttrAck] != "false" {
t.Fatalf("Alices message should have been acknowledged...yet")
}
time.Sleep(time.Second * 30)
ci, err := bob.FetchConversationInfo(alice.GetOnion())
for err != nil {
time.Sleep(time.Second * 5)
ci, err = bob.FetchConversationInfo(alice.GetOnion())
}
if ci == nil {
t.Fatalf("could not fetch bobs conversation")
}
body, _, err := bob.GetChannelMessage(ci.ID, 0, 1)
if body != "Hello Bob" || err != nil {
t.Fatalf("unexpected message in conversation channel %v %v", body, err)
} else {
t.Logf("succesfully found message in conversation channel %v", body)
}
// Check that we received an ACk...
_, attr, err = alice.GetChannelMessage(2, 0, 1)
if err != nil {
t.Fatalf("alice should have been able to fetch her own message")
}
if attr[constants.AttrAck] != "true" {
t.Fatalf("Alices message should have been acknowledged.")
}
if count, err := alice.GetChannelMessageCount(2, 0); err != nil || count != 1 {
t.Fatalf("Channel should have a single message in it. Instead returned %v %v", count, err)
}
messages, err := alice.GetMostRecentMessages(2, 0, 0, 10)
if err != nil {
t.Fatalf("fetching messages over offset should not result in error: %v", err)
}
if len(messages) != 1 || len(messages) > 0 && messages[0].Body != "Hello Bob" {
t.Fatalf("expeced GetMostRecentMessages to return 1, instead returned: %v %v", len(messages), messages)
}
err = alice.ExportProfile("alice.tar.gz")
if err != nil {
t.Fatalf("could not export profile: %v", err)
}
_, err = app.ImportProfile("alice.tar.gz", "password")
if err == nil {
t.Fatal("profile is already imported...this should fail")
}
app.DeleteProfile(alice.GetOnion(), "password")
alice, err = app.ImportProfile("alice.tar.gz", "password")
if err != nil {
t.Fatalf("profile should have successfully imported: %s", err)
}
if alice.GetOnion() != aliceOnion {
t.Fatalf("profile is not Alice...%s != %s", aliceOnion, alice.GetOnion())
}
app.Shutdown()
}
// Sub Test testing that Alice can add Bob, delete the conversation associated with Bob, and then add Bob again
// Under a different conversation identifier.
func subTestAliceAddAndDeleteBob(t *testing.T, alice peer.CwtchPeer, bob peer.CwtchPeer) {
t.Logf("Starting Sub Test AliceAddAndDeleteBob")
alice.NewContactConversation(bob.GetOnion(), model.AccessControl{Read: true, Append: true, Blocked: false}, true)
// Test Basic Fetching
bobCI, err := alice.FetchConversationInfo(bob.GetOnion())
if bobCI == nil || err != nil {
t.Fatalf("alice should have been able to fetch bobs conversationf info ci:%v err:%v", bobCI, err)
} else {
t.Logf("Bobs Conversation Info fetched successfully: %v", bobCI)
}
oldID := bobCI.ID
alice.DeleteConversation(oldID)
// Test Basic Fetching
bobCI, err = alice.FetchConversationInfo(bob.GetOnion())
if bobCI != nil {
t.Fatalf("alice should **not** have been able to fetch bobs conversationf info ci:%v err:%v", bobCI, err)
} else {
t.Logf("expected error fetching deleted conversation info: %v", err)
}
alice.NewContactConversation(bob.GetOnion(), model.AccessControl{Read: true, Append: true, Blocked: false}, true)
// Test Basic Fetching
bobCI, err = alice.FetchConversationInfo(bob.GetOnion())
if bobCI == nil || err != nil {
t.Fatalf("alice should have been able to fetch bobs conversationf info ci:%v err:%v", bobCI, err)
} else {
t.Logf("Bobs Conversation Info fetched successfully: %v", bobCI)
}
if oldID == bobCI.ID {
t.Fatalf("bob should have a different conversation ID. Instead it is the same as the old conversation id, meaning something has gone wrong in the storage engine.")
}
}

View File

@ -2,8 +2,13 @@ package filesharing
import (
"crypto/rand"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"path/filepath"
app2 "cwtch.im/cwtch/app"
"cwtch.im/cwtch/app/utils"
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/functionality/filesharing"
"cwtch.im/cwtch/model"
@ -12,12 +17,11 @@ import (
"cwtch.im/cwtch/peer"
"cwtch.im/cwtch/protocol/connections"
"cwtch.im/cwtch/protocol/files"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
utils2 "cwtch.im/cwtch/utils"
"git.openprivacy.ca/openprivacy/connectivity/tor"
"git.openprivacy.ca/openprivacy/log"
// Import SQL Cipher
mrand "math/rand"
"os"
"os/user"
@ -26,46 +30,42 @@ import (
"runtime/pprof"
"testing"
"time"
_ "github.com/mutecomm/go-sqlcipher/v4"
)
func waitForPeerPeerConnection(t *testing.T, peera peer.CwtchPeer, peerb peer.CwtchPeer) {
for {
state, ok := peera.GetPeerState(peerb.GetOnion())
if ok {
//log.Infof("Waiting for Peer %v to peer with peer: %v - state: %v\n", peera.GetProfile().Name, peerb.GetProfile().Name, state)
if state == connections.FAILED {
t.Fatalf("%v could not connect to %v", peera.GetOnion(), peerb.GetOnion())
}
if state != connections.AUTHENTICATED {
fmt.Printf("peer %v waiting connect to peer %v, currently: %v\n", peera.GetOnion(), peerb.GetOnion(), connections.ConnectionStateName[state])
time.Sleep(time.Second * 5)
continue
} else {
peerAName, _ := peera.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Name)
peerBName, _ := peerb.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Name)
fmt.Printf("%v CONNECTED and AUTHED to %v\n", peerAName, peerBName)
break
}
state := peera.GetPeerState(peerb.GetOnion())
if state == connections.FAILED {
t.Fatalf("%v could not connect to %v", peera.GetOnion(), peerb.GetOnion())
}
if state != connections.AUTHENTICATED {
fmt.Printf("peer %v waiting connect to peer %v, currently: %v\n", peera.GetOnion(), peerb.GetOnion(), connections.ConnectionStateName[state])
time.Sleep(time.Second * 5)
continue
} else {
peerAName, _ := peera.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name)
peerBName, _ := peerb.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name)
fmt.Printf("%v CONNECTED and AUTHED to %v\n", peerAName, peerBName)
break
}
}
return
}
func TestFileSharing(t *testing.T) {
numGoRoutinesStart := runtime.NumGoroutine()
os.RemoveAll("cwtch.out.png")
os.RemoveAll("cwtch.out.png.manifest")
log.SetLevel(log.LevelDebug)
log.ExcludeFromPattern("tapir")
os.Mkdir("tordir", 0700)
dataDir := path.Join("tordir", "tor")
os.MkdirAll(dataDir, 0700)
// we don't need real randomness for the port, just to avoid a possible conflict...
mrand.Seed(int64(time.Now().Nanosecond()))
socksPort := mrand.Intn(1000) + 9051
controlPort := mrand.Intn(1000) + 9052
@ -76,92 +76,144 @@ func TestFileSharing(t *testing.T) {
panic(err)
}
useCache := os.Getenv("TORCACHE") == "true"
torDataDir := ""
if useCache {
log.Infof("using tor cache")
torDataDir = filepath.Join(dataDir, "data-dir-torcache")
os.MkdirAll(torDataDir, 0700)
} else {
log.Infof("using clean tor data dir")
if torDataDir, err = os.MkdirTemp(dataDir, "data-dir-"); err != nil {
t.Fatalf("could not create data dir")
}
}
tor.NewTorrc().WithSocksPort(socksPort).WithOnionTrafficOnly().WithHashedPassword(base64.StdEncoding.EncodeToString(key)).WithControlPort(controlPort).Build("tordir/tor/torrc")
acn, err := tor.NewTorACNWithAuth("./tordir", path.Join("..", "..", "tor"), controlPort, tor.HashedPasswordAuthenticator{Password: base64.StdEncoding.EncodeToString(key)})
acn, err := tor.NewTorACNWithAuth("./tordir", path.Join("..", "tor"), torDataDir, controlPort, tor.HashedPasswordAuthenticator{Password: base64.StdEncoding.EncodeToString(key)})
if err != nil {
t.Fatalf("Could not start Tor: %v", err)
}
acn.WaitTillBootstrapped()
defer acn.Close()
app := app2.NewApp(acn, "./storage")
app := app2.NewApp(acn, "./storage", app2.LoadAppSettings("./storage"))
usr, _ := user.Current()
usr, err := user.Current()
if err != nil {
t.Fatalf("current user is undefined")
}
cwtchDir := path.Join(usr.HomeDir, ".cwtch")
os.Mkdir(cwtchDir, 0700)
os.RemoveAll(path.Join(cwtchDir, "testing"))
os.Mkdir(path.Join(cwtchDir, "testing"), 0700)
fmt.Println("Creating Alice...")
app.CreatePeer("alice", "asdfasdf")
t.Logf("Creating Alice...")
app.CreateProfile("alice", "asdfasdf", true)
fmt.Println("Creating Bob...")
app.CreatePeer("bob", "asdfasdf")
alice := utils.WaitGetPeer(app, "alice")
bob := utils.WaitGetPeer(app, "bob")
t.Logf("Creating Bob...")
app.CreateProfile("bob", "asdfasdf", true)
t.Logf("** Waiting for Alice, Bob...")
alice := app2.WaitGetPeer(app, "alice")
app.ActivatePeerEngine(alice.GetOnion())
app.ConfigureConnections(alice.GetOnion(), true, true, true)
bob := app2.WaitGetPeer(app, "bob")
app.ActivatePeerEngine(bob.GetOnion())
app.ConfigureConnections(bob.GetOnion(), true, true, true)
alice.AutoHandleEvents([]event.Type{event.PeerStateChange, event.NewRetValMessageFromPeer})
bob.AutoHandleEvents([]event.Type{event.PeerStateChange, event.NewRetValMessageFromPeer, event.ManifestReceived})
bob.AutoHandleEvents([]event.Type{event.PeerStateChange, event.NewRetValMessageFromPeer})
aliceQueueOracle := event.NewQueue()
aliceEb := app.GetEventBus(alice.GetOnion())
if aliceEb == nil {
t.Fatalf("alice's eventbus is undefined")
}
aliceEb.Subscribe(event.SearchResult, aliceQueueOracle)
queueOracle := event.NewQueue()
app.GetEventBus(bob.GetOnion()).Subscribe(event.FileDownloaded, queueOracle)
bobEb := app.GetEventBus(bob.GetOnion())
if bobEb == nil {
t.Fatalf("bob's eventbus is undefined")
}
bobEb.Subscribe(event.FileDownloaded, queueOracle)
app.LaunchPeers()
// Turn on File Sharing Experiment...
settings := app.ReadSettings()
settings.ExperimentsEnabled = true
settings.Experiments[constants.FileSharingExperiment] = true
app.UpdateSettings(settings)
t.Logf("** Launching Peers...")
waitTime := time.Duration(30) * time.Second
t.Logf("** Waiting for Alice, Bob to connect with onion network... (%v)\n", waitTime)
time.Sleep(waitTime)
bob.AddContact("alice?", alice.GetOnion(), model.AuthApproved)
alice.PeerWithOnion(bob.GetOnion())
bob.NewContactConversation(alice.GetOnion(), model.DefaultP2PAccessControl(), true)
alice.NewContactConversation(bob.GetOnion(), model.DefaultP2PAccessControl(), true)
fmt.Println("Waiting for alice and Bob to peer...")
waitForPeerPeerConnection(t, alice, bob)
filesharingFunctionality := filesharing.FunctionalityGate()
fmt.Println("Alice and Bob are Connected!!")
_, fileSharingMessage, err := filesharingFunctionality.ShareFile("cwtch.png", alice)
if err != nil {
t.Fatalf("Error!: %v", err)
}
filesharingFunctionality, _ := filesharing.FunctionalityGate(map[string]bool{"filesharing": true})
alice.SendMessage(1, fileSharingMessage)
err = filesharingFunctionality.ShareFile("cwtch.png", alice, bob.GetOnion())
// Ok this is fun...we just Sent a Message we may not have a connection yet...
// so this test will only pass if sending offline works...
waitForPeerPeerConnection(t, bob, alice)
bob.SendMessage(1, "this is a test message")
bob.SendMessage(1, "this is another test message")
// Wait for the messages to arrive...
time.Sleep(time.Second * 20)
alice.SearchConversations("test")
results := 0
for {
ev := aliceQueueOracle.Next()
if ev.EventType != event.SearchResult {
t.Fatalf("Expected a search result vent")
}
results += 1
t.Logf("found search result (%d)....%v", results, ev)
if results == 2 {
break
}
}
// test that bob can download and verify the file
testBobDownloadFile(t, bob, filesharingFunctionality, queueOracle)
// Test stopping and restarting file shares
t.Logf("Stopping File Share")
filesharingFunctionality.StopAllFileShares(alice)
// Allow time for the stop request to filter through Engine
time.Sleep(time.Second * 5)
// Restart
t.Logf("Restarting File Share")
err = filesharingFunctionality.ReShareFiles(alice)
if err != nil {
t.Fatalf("Error!: %v", err)
}
// Wait for the messages to arrive...
time.Sleep(time.Second * 10)
// run the same download test again...to check that we can actually download the file
testBobDownloadFile(t, bob, filesharingFunctionality, queueOracle)
for _, message := range bob.GetContact(alice.GetOnion()).Timeline.GetMessages() {
var messageWrapper model.MessageWrapper
json.Unmarshal([]byte(message.Message), &messageWrapper)
if messageWrapper.Overlay == model.OverlayFileSharing {
var fileMessageOverlay filesharing.OverlayMessage
err := json.Unmarshal([]byte(messageWrapper.Data), &fileMessageOverlay)
if err == nil {
filesharingFunctionality.DownloadFile(bob, alice.GetOnion(), "cwtch.out.png", "cwtch.out.png.manifest", fmt.Sprintf("%s.%s", fileMessageOverlay.Hash, fileMessageOverlay.Nonce))
}
}
fmt.Printf("Found message from Alice: %v", message.Message)
}
// Wait for the file downloaded event
ev := queueOracle.Next()
if ev.EventType != event.FileDownloaded {
t.Fatalf("Expected file download event")
}
manifest, err := files.CreateManifest("cwtch.out.png")
if hex.EncodeToString(manifest.RootHash) != "8f0ed73bbb30db45b6a740b1251cae02945f48e4f991464d5f3607685c45dcd136a325dab2e5f6429ce2b715e602b20b5b16bf7438fb6235fefe912adcedb5fd" {
t.Fatalf("file hash does not match expected %x: ", manifest.RootHash)
}
// test that we can delete bob...
app.DeleteProfile(bob.GetOnion(), "asdfasdf")
aliceQueueOracle.Shutdown()
queueOracle.Shutdown()
app.Shutdown()
acn.Close()
time.Sleep(5 * time.Second)
numGoRoutinesPostACN := runtime.NumGoroutine()
// Printing out the current goroutines
@ -173,3 +225,61 @@ func TestFileSharing(t *testing.T) {
}
}
func testBobDownloadFile(t *testing.T, bob peer.CwtchPeer, filesharingFunctionality *filesharing.Functionality, queueOracle event.Queue) {
os.RemoveAll("cwtch.out.png")
os.RemoveAll("cwtch.out.png.manifest")
message, _, err := bob.GetChannelMessage(1, 0, 1)
if err != nil {
t.Fatalf("could not find file sharing message: %v", err)
}
var messageWrapper model.MessageWrapper
json.Unmarshal([]byte(message), &messageWrapper)
if messageWrapper.Overlay == model.OverlayFileSharing {
var fileMessageOverlay filesharing.OverlayMessage
err := json.Unmarshal([]byte(messageWrapper.Data), &fileMessageOverlay)
if err == nil {
t.Logf("bob attempting to download file with invalid download")
// try downloading with invalid download dir
err = filesharingFunctionality.DownloadFile(bob, 1, "/do/not/download/this/file/cwtch.out.png", "./cwtch.out.png.manifest", fmt.Sprintf("%s.%s", fileMessageOverlay.Hash, fileMessageOverlay.Nonce), constants.ImagePreviewMaxSizeInBytes)
if err == nil {
t.Fatalf("should not download file with invalid download dir")
}
t.Logf("bob attempting to download file with invalid manifest")
// try downloading with invalid manifest dir
err = filesharingFunctionality.DownloadFile(bob, 1, "./cwtch.out.png", "/do/not/download/this/file/cwtch.out.png.manifest", fmt.Sprintf("%s.%s", fileMessageOverlay.Hash, fileMessageOverlay.Nonce), constants.ImagePreviewMaxSizeInBytes)
if err == nil {
t.Fatalf("should not download file with invalid manifest dir")
}
t.Logf("bob attempting to download file")
err = filesharingFunctionality.DownloadFile(bob, 1, "./cwtch.out.png", "./cwtch.out.png.manifest", fmt.Sprintf("%s.%s", fileMessageOverlay.Hash, fileMessageOverlay.Nonce), constants.ImagePreviewMaxSizeInBytes)
if err != nil {
t.Fatalf("could not download file: %v", err)
}
}
}
// Wait for the file downloaded event
ClientTimeout := utils2.TimeoutPolicy(time.Second * 120)
err = ClientTimeout.ExecuteAction(func() error {
ev := queueOracle.Next()
if ev.EventType != event.FileDownloaded {
t.Fatalf("Expected file download event")
}
manifest, _ := files.CreateManifest("cwtch.out.png")
if hex.EncodeToString(manifest.RootHash) != "8f0ed73bbb30db45b6a740b1251cae02945f48e4f991464d5f3607685c45dcd136a325dab2e5f6429ce2b715e602b20b5b16bf7438fb6235fefe912adcedb5fd" {
t.Fatalf("file hash does not match expected %x: ", manifest.RootHash)
}
return nil
})
if err != nil {
t.Fatalf("timeout when attempting to download a file")
}
}

View File

@ -0,0 +1,214 @@
package testing
import (
"crypto/rand"
"encoding/base64"
"fmt"
mrand "math/rand"
"os"
"path"
"path/filepath"
"runtime"
"runtime/pprof"
"testing"
"time"
app2 "cwtch.im/cwtch/app"
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/functionality/hybrid"
"cwtch.im/cwtch/functionality/inter"
"cwtch.im/cwtch/model/constants"
"cwtch.im/cwtch/peer"
"cwtch.im/cwtch/protocol/connections"
"git.openprivacy.ca/openprivacy/connectivity/tor"
"git.openprivacy.ca/openprivacy/log"
_ "github.com/mutecomm/go-sqlcipher/v4"
)
func TestHyrbidGroupIntegration(t *testing.T) {
t.Logf("Starting Hybrid Groups Test")
os.RemoveAll("./storage")
os.RemoveAll("./managerstorage")
// Goroutine Monitoring Start..
numGoRoutinesStart := runtime.NumGoroutine()
log.AddEverythingFromPattern("connectivity")
log.SetLevel(log.LevelInfo)
log.ExcludeFromPattern("connection/connection")
log.ExcludeFromPattern("outbound/3dhauthchannel")
log.ExcludeFromPattern("event/eventmanager")
log.ExcludeFromPattern("tapir")
os.Mkdir("tordir", 0700)
dataDir := path.Join("tordir", "tor")
os.MkdirAll(dataDir, 0700)
// we don't need real randomness for the port, just to avoid a possible conflict...
socksPort := mrand.Intn(1000) + 9051
controlPort := mrand.Intn(1000) + 9052
// generate a random password
key := make([]byte, 64)
_, err := rand.Read(key)
if err != nil {
panic(err)
}
useCache := os.Getenv("TORCACHE") == "true"
torDataDir := ""
if useCache {
log.Infof("using tor cache")
torDataDir = filepath.Join(dataDir, "data-dir-torcache")
os.MkdirAll(torDataDir, 0700)
} else {
log.Infof("using clean tor data dir")
if torDataDir, err = os.MkdirTemp(dataDir, "data-dir-"); err != nil {
t.Fatalf("could not create data dir")
}
}
tor.NewTorrc().WithSocksPort(socksPort).WithOnionTrafficOnly().WithHashedPassword(base64.StdEncoding.EncodeToString(key)).WithControlPort(controlPort).Build("tordir/tor/torrc")
acn, err := tor.NewTorACNWithAuth("./tordir", path.Join("..", "tor"), torDataDir, controlPort, tor.HashedPasswordAuthenticator{Password: base64.StdEncoding.EncodeToString(key)})
if err != nil {
t.Fatalf("Could not start Tor: %v", err)
}
log.Infof("Waiting for tor to bootstrap...")
acn.WaitTillBootstrapped()
defer acn.Close()
// ***** Cwtch Server management *****
app := app2.NewApp(acn, "./storage", app2.LoadAppSettings("./storage"))
// ***** cwtchPeer setup *****
// Turn on Groups Experiment...
settings := app.ReadSettings()
settings.ExperimentsEnabled = true
settings.Experiments[constants.GroupsExperiment] = true
settings.Experiments[constants.GroupManagerExperiment] = true
app.UpdateSettings(settings)
alice := MakeProfile(app, "Alice")
bob := MakeProfile(app, "Bob")
manager := MakeProfile(app, "Manager")
waitTime := time.Duration(60) * time.Second
log.Infof("** Waiting for Alice, Bob, and Carol to register their onion hidden service on the network... (%v)\n", waitTime)
time.Sleep(waitTime)
log.Infof("** Wait Done!")
// Ok Lets Start By Creating a Hybrid Group...
hgmf := hybrid.GroupManagerFunctionality{}
ci, err := hgmf.ManageNewGroup(manager)
if err != nil {
t.Fatalf("could not create hybrid group: %v", err)
}
log.Infof("created a hybrid group: %d. moving onto adding hybrid contacts...", ci)
err = hgmf.AddHybridContact(manager, alice.GetOnion())
if err != nil {
t.Fatalf("could not create hybrid contact (alice): %v", err)
}
err = hgmf.AddHybridContact(manager, bob.GetOnion())
if err != nil {
t.Fatalf("could not create hybrid contact (bob): %v", err)
}
// Now we can allow alice, bob and carol to create a new hybrid group...
log.Infof("now we can allow alice bob and carol to join the hybrid group")
inter := inter.InterfaceFunctionality{}
err = inter.ImportBundle(alice, "managed:"+manager.GetOnion())
if err != nil {
t.Fatalf("could not create hybrid group contact (carol): %v", err)
}
alice.PeerWithOnion(manager.GetOnion()) // explictly trigger a peer request
err = inter.ImportBundle(bob, "managed:"+manager.GetOnion())
if err != nil {
t.Fatalf("could not create hybrid group contact (carol): %v", err)
}
bob.PeerWithOnion(manager.GetOnion())
log.Infof("waiting for alice and manager to connect")
WaitForConnection(t, alice, manager.GetOnion(), connections.AUTHENTICATED)
log.Infof("waiting for bob and manager to connect")
WaitForConnection(t, bob, manager.GetOnion(), connections.AUTHENTICATED)
// at this pont we should be able to send messages to the group, and receive them in the timeline
log.Infof("sending message to group")
_, err = inter.SendMessage(alice, 1, "hello everyone!!!")
if err != nil {
t.Fatalf("hybrid group sending failed... %v", err)
}
// Note: From this point onwards there are no managed-group specific calls. Everything happens
// transparently with respect to the receiver.
time.Sleep(time.Second * 10)
bobMessages, err := bob.GetMostRecentMessages(1, constants.CHANNEL_CHAT, 0, 1)
if err != nil || len(bobMessages) != 1 {
t.Fatalf("hybrid group receipt failed... %v %v ", err, len(bobMessages))
}
if bobMessages[0].Body != "hello everyone!!!" {
t.Fatalf("hybrid group receipt failed...message does not match")
}
aliceMessages, err := alice.GetMostRecentMessages(1, constants.CHANNEL_CHAT, 0, 1)
if err != nil || len(aliceMessages) != 1 {
t.Fatalf("hybrid group receipt failed... %v", err)
}
if aliceMessages[0].Attr[constants.AttrAck] != constants.True {
t.Fatalf("hybrid group receipt failed...alice's message was not ack'd")
}
// Time to Clean Up....
log.Infof("Shutting down Alice...")
app.ShutdownPeer(alice.GetOnion())
time.Sleep(time.Second * 3)
log.Infof("Shutting down Bob...")
app.ShutdownPeer(bob.GetOnion())
time.Sleep(time.Second * 3)
log.Infof("Shutting fown Manager...")
app.ShutdownPeer(manager.GetOnion())
time.Sleep(time.Second * 3)
log.Infof("Shutting down apps...")
log.Infof("app Shutdown: %v\n", runtime.NumGoroutine())
app.Shutdown()
time.Sleep(2 * time.Second)
log.Infof("Done shutdown: %v\n", runtime.NumGoroutine())
log.Infof("Shutting down ACN...")
acn.Close()
time.Sleep(time.Second * 60) // the network status / heartbeat plugin might keep goroutines alive for a minute before killing them
numGoRoutinesPostAppShutdown := runtime.NumGoroutine()
// Printing out the current goroutines
// Very useful if we are leaking any.
pprof.Lookup("goroutine").WriteTo(os.Stdout, 1)
fmt.Println("")
if numGoRoutinesStart != numGoRoutinesPostAppShutdown {
t.Errorf("Number of GoRoutines at start (%v) does not match number of goRoutines after cleanup of peers and servers (%v), clean up failed, v detected!", numGoRoutinesStart, numGoRoutinesPostAppShutdown)
}
}
func MakeProfile(application app2.Application, name string) peer.CwtchPeer {
application.CreateProfile(name, "asdfasdf", true)
p := app2.WaitGetPeer(application, name)
application.ConfigureConnections(p.GetOnion(), true, true, false)
log.Infof("%s created: %s", name, p.GetOnion())
// bob.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, "Bob") <- This is now done automatically by ProfileValueExtension, keeping this here for clarity
p.AutoHandleEvents([]event.Type{event.PeerStateChange, event.ServerStateChange, event.NewGroupInvite, event.NewRetValMessageFromPeer})
return p
}

Some files were not shown because too many files have changed in this diff Show More