Compare commits

...

496 Commits
main ... main

Author SHA1 Message Date
glyph 6cc8faa0c3 Merge pull request 'Reintroduce status and power-related templates and routes' (#140) from refactor_stats into main
Reviewed-on: PeachCloud/peach-workspace#140
2022-11-28 07:18:00 +00:00
glyph 6028e07bde single variable name change for clarity 2022-11-28 09:12:25 +02:00
glyph ebc7b9d417 remove old context code and refine status parsing 2022-11-28 09:12:01 +02:00
glyph b8ff944377 conditionally render status url based on run-mode 2022-11-28 09:11:21 +02:00
glyph 8cbb295c3a add power menu to settings menu and mount routes 2022-11-28 09:10:42 +02:00
glyph 7d5d6bcc1f add power menu template builder and mount route 2022-11-03 12:02:01 +00:00
glyph 8c3a92aa88 update lockfile 2022-11-02 15:16:21 +00:00
glyph cfe270a995 mount device status route 2022-11-02 15:16:00 +00:00
glyph 2eca779208 add peach-stats dependency 2022-11-02 15:15:42 +00:00
glyph a1b16f8d38 add refactored device status template and import module 2022-11-02 15:15:06 +00:00
glyph 3bf095e148 bump the version number and update the lockfile 2022-10-25 15:16:17 +01:00
glyph d9167a2cd6 mount the network status route 2022-10-25 15:15:13 +01:00
glyph 4e7fbd5fdf add the refactored template for network status 2022-10-25 15:14:52 +01:00
glyph 0fab57d94f uncomment vnstat_parse dependency 2022-10-25 15:14:01 +01:00
glyph 441d2a6a3b Merge pull request 'Specify network iface values as consts' (#139) from iface_config_vars into main
Reviewed-on: PeachCloud/peach-workspace#139
2022-10-18 15:02:36 +00:00
glyph 52e0aff4d1 bump version 2022-10-18 15:58:24 +01:00
glyph 24ceedbb9d replace scattered values for wlan0 and ap0 with const values 2022-10-18 15:57:40 +01:00
glyph d3ab490c05 Merge pull request 'Reintroduce networking-related templates and routes' (#138) from system_mode into main
Reviewed-on: PeachCloud/peach-workspace#138
2022-10-18 11:14:20 +00:00
glyph 1e7a54b728 remove blank line in template 2022-10-18 12:06:36 +01:00
glyph 3eab3e3687 add and mount ap detail template 2022-10-18 12:01:28 +01:00
glyph 8b0381ead1 fix template indentation 2022-10-18 12:00:40 +01:00
glyph e91c40355a add template builder and form handler for adding wifi ap 2022-10-10 10:39:29 +01:00
glyph 8cd8ee5dd6 mount routes for adding wifi ap credentials 2022-10-10 10:39:04 +01:00
glyph 24deb4601a update lockfile 2022-10-10 09:18:20 +01:00
glyph fedf2855ed add data usage template module but leave it commented out for now 2022-10-10 09:17:54 +01:00
glyph 0814eedf13 add ap list template and mount route 2022-10-03 11:40:26 +01:00
glyph 4fb4ea2f9c merge latest lockfile 2022-10-03 11:39:58 +01:00
glyph 8e283fbc6e merge upstream network api changes 2022-10-03 11:39:28 +01:00
glyph bdd3b7ab9b add wip refactored template for ap detail 2022-10-03 10:48:56 +01:00
glyph 4f36f61128 add refactored template for ap list 2022-10-03 10:48:25 +01:00
glyph acab30acce mount GET and POST routes for dns configuration 2022-09-30 15:34:19 +01:00
glyph 61ef909ed3 add dns configuration template builder and form handler 2022-09-30 15:33:36 +01:00
glyph 97030fbfbf mount GET and POST routes for modifying wifi ap password 2022-09-29 14:27:46 +01:00
glyph b6cd54142c add template builder and form handler for modifying wifi ap password 2022-09-29 14:26:56 +01:00
glyph 67f33385e5 Merge pull request 'Add method to return list of all saved and in-range access points for a given interface' (#137) from list_networks into main
Reviewed-on: PeachCloud/peach-workspace#137
2022-09-27 13:57:22 +00:00
glyph a9bcc267a2 bump minor version 2022-09-27 14:52:13 +01:00
glyph a513b7aa5b add method to return list of all saved and in-range access points for the given interface 2022-09-27 14:51:09 +01:00
glyph 1a7bd7987b add network settings menu template and route handler, along with network settings placeholder files for routes 2022-09-26 16:44:22 +01:00
glyph c5c0bb91e4 Merge pull request 'Update lockfile with kuska and golgi crate updates' (#136) from update_lockfile into main
Reviewed-on: PeachCloud/peach-workspace#136
2022-09-26 09:27:14 +00:00
glyph 5a50730435 update lockfile with kuska and golgi crate updates 2022-09-26 10:24:07 +01:00
glyph 86b4714274 Merge pull request 'Update create_history_stream to take args struct' (#135) from history_stream_args into main
Reviewed-on: PeachCloud/peach-workspace#135
2022-09-26 09:18:32 +00:00
glyph d5a2390e29 update create history stream to take args struct 2022-09-26 10:14:47 +01:00
notplants c83a22461d Merge pull request 'Add wait-for-sbot to peach-config' (#131) from wait-for-sbot into main
Reviewed-on: PeachCloud/peach-workspace#131
2022-07-25 11:17:31 +00:00
notplants 40bd1e48f1 Merge branch 'main' into wait-for-sbot
continuous-integration/drone/pr Build is passing Details
2022-07-25 10:41:20 +00:00
glyph 6407495292 Merge pull request 'Update go-sbot systemctl commands (remove --user)' (#132) from fix_systemctl_calls into main
Reviewed-on: PeachCloud/peach-workspace#132
2022-07-15 09:55:06 +00:00
notplants 03ac890793 Cargo fmt
continuous-integration/drone/pr Build is passing Details
2022-07-15 11:37:05 +02:00
notplants bc0c0fca7f Sequential match statements
continuous-integration/drone/pr Build is failing Details
2022-07-15 11:35:28 +02:00
glyph 05c1577f2a upgrade probes version to avoid precise_time error
continuous-integration/drone/pr Build is passing Details
2022-07-15 09:00:31 +01:00
glyph add169db07 bump patch version
continuous-integration/drone/pr Build is failing Details
2022-07-14 08:57:34 +01:00
glyph fcb17d6802 remove --user and add sudo for systemctl calls 2022-07-14 08:57:22 +01:00
notplants fc50bb5ee5 Cargo fmt
continuous-integration/drone/pr Build is passing Details
2022-07-12 12:29:47 +02:00
notplants 29f5ad0e84 Wait for sbot is working
continuous-integration/drone/pr Build is failing Details
2022-07-12 12:18:54 +02:00
notplants cb09d6c3e9 Wait for sbot 2022-07-12 11:51:49 +02:00
notplants 01138eef35 Merge branch 'main' of https://git.coopcloud.tech/PeachCloud/peach-workspace into main7 2022-07-11 13:17:34 +02:00
notplants 2637b28380 Bump peach-config to v0.1.26 2022-07-11 13:17:24 +02:00
notplants 03a0a51f4d Merge pull request 'Add publish-address function to peach-config' (#130) from publish-address into main
Reviewed-on: PeachCloud/peach-workspace#130
2022-07-11 10:25:09 +00:00
mycognosist eddb167c4c Remove KVT to Value map and retrieve sequence number directly from KVT
continuous-integration/drone/pr Build is passing Details
2022-07-07 15:54:19 +01:00
mycognosist 9704269c8a Remove Cargo.lock from .gitignore 2022-07-07 15:44:31 +01:00
notplants 466db8ceea Update sbot.rs to new history_stream api
continuous-integration/drone/pr Build is passing Details
2022-07-07 13:42:32 +02:00
notplants 90badbfe30 Add cargo.lock to .gitignore
continuous-integration/drone/pr Build is failing Details
2022-07-07 10:57:27 +02:00
notplants 7489916d5f Remove cargo.lock 2022-07-07 10:57:03 +02:00
notplants 7daab74b37 Fix golgi import
continuous-integration/drone/pr Build is failing Details
2022-07-07 10:51:44 +02:00
notplants 58bf306d3b Fix golgi import
continuous-integration/drone/pr Build is failing Details
2022-07-07 10:49:49 +02:00
notplants bdac23092a Change changepassword to change-password
continuous-integration/drone/pr Build is failing Details
2022-07-07 10:41:36 +02:00
notplants f1ab2caa08 Fix golgi imports
continuous-integration/drone/pr Build is failing Details
2022-07-06 12:39:55 +02:00
notplants 1fab4f3c43 Merge branch 'main' of https://git.coopcloud.tech/PeachCloud/peach-workspace into main7
continuous-integration/drone/pr Build is failing Details
2022-07-06 12:34:45 +02:00
notplants 8dcd594dd7 Publish address 2022-07-06 12:34:24 +02:00
notplants fcaa9e29c4 Merge pull request 'Add whoami command to peach-config' (#128) from who-am-i into main
Reviewed-on: PeachCloud/peach-workspace#128
2022-07-05 13:10:26 +00:00
notplants c6fc5c2992 Merge branch 'main' into who-am-i
continuous-integration/drone/pr Build is passing Details
2022-07-05 12:52:21 +00:00
notplants 1258a3697d Cargo fmt
continuous-integration/drone/pr Build is passing Details
2022-07-04 14:54:00 +02:00
notplants 7e94135839 Whoami
continuous-integration/drone/pr Build is failing Details
2022-07-04 14:35:17 +02:00
notplants f002f5cf3e Working on peach-config 2022-07-04 13:23:55 +02:00
notplants fba1e91d8b Merge pull request 'Insert domain into invite' (#125) from insert-invite into main
Reviewed-on: PeachCloud/peach-workspace#125
2022-06-30 13:00:46 +00:00
notplants 6621a09ec9 Insert domain into invite
continuous-integration/drone/pr Build is passing Details
2022-06-29 16:23:41 -04:00
glyph 170b037248 Merge pull request 'Update configuration sections of README' (#122) from update_readme into main
Reviewed-on: PeachCloud/peach-workspace#122
2022-06-28 10:37:12 +00:00
glyph 251aaf9237 merge main
continuous-integration/drone/pr Build is passing Details
2022-06-28 09:16:09 +01:00
glyph 123ebc06cc Merge pull request 'Introduce JS for improved back button behaviour' (#121) from js_back_button into main
Reviewed-on: PeachCloud/peach-workspace#121
2022-06-28 08:14:42 +00:00
glyph 9ce27d17c5 merge main
continuous-integration/drone/pr Build is passing Details
2022-06-28 09:13:54 +01:00
glyph 2a8cf4ecfb update configuration sections
continuous-integration/drone/pr Build is passing Details
2022-06-27 09:01:00 +01:00
glyph d1a55e29d7 bump patch version and update lockfile
continuous-integration/drone/pr Build is passing Details
2022-06-23 12:02:09 +01:00
glyph 4568577f81 add js to overlay back button functionality 2022-06-23 12:01:18 +01:00
glyph ab0e27c14d Merge pull request 'Improve back button behaviour' (#120) from improved_back_button into main
Reviewed-on: PeachCloud/peach-workspace#120
2022-06-23 11:00:21 +00:00
glyph 65b5f95a90 update lockfile
continuous-integration/drone/pr Build is passing Details
2022-06-21 12:14:30 +01:00
glyph a60d892e95 bump the patch version 2022-06-21 12:14:14 +01:00
glyph 5bd8a68ddf set, retrieve and reset back_url cookies 2022-06-21 12:13:36 +01:00
glyph a6f52ce384 add cookie utils module for requests and responses 2022-06-21 12:13:04 +01:00
glyph c71cc3992d Merge pull request 'Configure EBT replication setting' (#118) from enable_ebt_replication into main
Reviewed-on: PeachCloud/peach-workspace#118
2022-06-16 11:04:39 +00:00
glyph 56fafc8d67 bump manifest version and add max as author
continuous-integration/drone/pr Build is passing Details
2022-06-16 11:41:50 +01:00
glyph 414508f8ff merge changes from main 2022-06-16 11:40:35 +01:00
glyph 6ad5c620c1 Merge pull request 'URL encode SSB public keys on profile page' (#116) from fix_encoding_decoding into main
Reviewed-on: PeachCloud/peach-workspace#116
2022-06-16 10:38:11 +00:00
glyph 76d5e6a355 fix probes version
continuous-integration/drone/pr Build is passing Details
2022-06-16 11:19:04 +01:00
glyph 11e94fa421 merge changes from main
continuous-integration/drone/pr Build is failing Details
2022-06-16 11:10:32 +01:00
glyph 216b60b86a bump version in manifest 2022-06-16 11:07:44 +01:00
glyph a70f5e227d Merge pull request 'Fix link to Set New Password route' (#117) from fix_set_new_passwork_link into main
Reviewed-on: PeachCloud/peach-workspace#117
2022-06-16 10:02:12 +00:00
glyph cddcb8f9bd fix spacing in manifest
continuous-integration/drone/pr Build is passing Details
2022-06-16 09:19:59 +01:00
glyph 8b33f8c174 Merge branch 'fix_set_new_passwork_link' of ssh://git.coopcloud.tech:2222/PeachCloud/peach-workspace into fix_set_new_passwork_link 2022-06-16 09:19:04 +01:00
glyph 1b43dc8b18 bump patch version 2022-06-16 09:18:38 +01:00
glyph 2d7b74d377 Merge branch 'main' into fix_set_new_passwork_link
continuous-integration/drone/pr Build is passing Details
2022-06-16 08:14:45 +00:00
notplants 6b34864289 Merge pull request 'Change go-sbot.service name to be configurable' (#113) from go-sbot-service into main
Reviewed-on: PeachCloud/peach-workspace#113
2022-06-15 12:03:29 +00:00
notplants 87ad2439b9 Fix cargo fmt
continuous-integration/drone/pr Build is passing Details
2022-06-15 13:46:51 +02:00
notplants 5838faf128 Cargo fmt
continuous-integration/drone/pr Build is failing Details
2022-06-15 13:37:36 +02:00
notplants 9c6fa00ec7 Fix cargo.lock
continuous-integration/drone/pr Build is failing Details
2022-06-15 13:33:56 +02:00
notplants a81b8b42cf Use main golgi branch 2022-06-15 12:48:51 +02:00
notplants cdcff3475c Fix cargo.lock
continuous-integration/drone/pr Build is failing Details
2022-06-15 12:47:11 +02:00
notplants 077c2a9178 Fix clippy errors 2022-06-15 12:44:34 +02:00
notplants 8b0b872d21 Reponse to CR 2022-06-15 12:36:44 +02:00
glyph 218a70b8f8 add lockfile
continuous-integration/drone/pr Build is passing Details
2022-06-15 08:58:45 +01:00
glyph 50dcb2cf9e add checkbox to enable / disable ebt 2022-06-15 08:58:21 +01:00
glyph e1877b5024 fix link to set new password
continuous-integration/drone/pr Build is passing Details
2022-06-15 08:46:16 +01:00
mycognosist 0923c24693 update lockfile
continuous-integration/drone/pr Build is passing Details
2022-06-14 09:03:24 +01:00
mycognosist f3d4ba9fe5 url-encode the ssb id for profile buttons 2022-06-14 09:03:08 +01:00
notplants 16e6d42f87 Cargo clippy
continuous-integration/drone/pr Build is failing Details
2022-05-26 23:19:31 +02:00
notplants 3493e5adb9 Change sbot.rs to use configurable go-sbot service name 2022-05-26 23:18:22 +02:00
notplants 5147eed497 Change sbot.rs to use configurable go-sbot service name 2022-05-26 23:17:20 +02:00
notplants 9f6ba14123 Cargo fmt
continuous-integration/drone/pr Build is failing Details
2022-05-25 14:50:41 +02:00
notplants 21fb29c322 Working sbot
continuous-integration/drone/pr Build is failing Details
2022-05-25 14:49:05 +02:00
notplants 6c9e5fd3fd Merge branch 'go-sbot-service' of https://git.coopcloud.tech/PeachCloud/peach-workspace into change-paths
continuous-integration/drone/pr Build is passing Details
2022-05-25 12:26:11 +02:00
notplants 3adb226969 Bump version number 2022-05-25 12:25:54 +02:00
notplants 92f516b161 Merge pull request 'Fix lockfile' (#114) from fix-lock into go-sbot-service
continuous-integration/drone/pr Build was killed Details
Reviewed-on: PeachCloud/peach-workspace#114
2022-05-20 14:33:18 +00:00
notplants 543470b949 Fix lockfile
continuous-integration/drone/pr Build was killed Details
2022-05-20 16:28:55 +02:00
notplants 6434471599 Bump version numbers
continuous-integration/drone/pr Build is passing Details
2022-05-20 13:55:21 +02:00
notplants 56c142a387 Make go-sbot.service name configurable
continuous-integration/drone/pr Build is passing Details
2022-05-20 13:35:37 +02:00
notplants 7deaa00d6e Fix hardcoded path
continuous-integration/drone/pr Build is passing Details
2022-05-20 13:31:06 +02:00
glyph bf7f2c8e31 Merge pull request 'Update golgi init functions with keystore selector' (#107) from keystore_selector into main
Reviewed-on: PeachCloud/peach-workspace#107
2022-05-16 13:12:00 +00:00
glyph dc79833e2b merge wpactrl and golgi api updates
continuous-integration/drone/pr Build is passing Details
2022-05-13 13:15:12 +02:00
glyph 039cbcdfc4 incorporate keystore change from golgi 2022-05-13 13:09:03 +02:00
glyph 4cd2692b9a fix probes dependency version 2022-05-13 13:07:37 +02:00
glyph 3e5e7e0f7c update wpactrl dependency api usage 2022-05-13 13:07:21 +02:00
glyph b0b79fef24 fix probes:0.3.0 lockfile error
continuous-integration/drone/pr Build is failing Details
2022-05-13 12:44:51 +02:00
glyph 98497fa5ae merge latest changes from main
continuous-integration/drone/pr Build is failing Details
2022-05-13 12:35:37 +02:00
notplants e6fd9a48cf Merge pull request 'Update peach-config to use configured paths' (#111) from change-paths2 into main
Reviewed-on: PeachCloud/peach-workspace#111
2022-05-12 20:25:13 +00:00
notplants 8960df6635 Fix cargo fmt
continuous-integration/drone/pr Build is passing Details
2022-05-12 21:58:15 +02:00
notplants 781af460ae Fix clippy warning
continuous-integration/drone/pr Build is failing Details
2022-05-12 13:32:40 +02:00
notplants 4a08e4ed6d Merge pull request 'Use get_config_value for Rouille configurations' (#109) from get-config into main
Reviewed-on: PeachCloud/peach-workspace#109
2022-05-12 11:31:18 +00:00
notplants 908d265de6 Bump version numbers
continuous-integration/drone/pr Build is failing Details
2022-05-12 12:53:06 +02:00
notplants 8202d4af5f Update peach-config to use configured paths
continuous-integration/drone/pr Build is failing Details
2022-05-12 12:49:47 +02:00
notplants 5ea6a86700 Fix clippy error
continuous-integration/drone/pr Build is passing Details
2022-05-12 12:24:17 +02:00
notplants 99fd3be4ad Change RouilleConfig to ServerConfig
continuous-integration/drone/pr Build is failing Details
2022-05-12 12:21:05 +02:00
notplants e041e1c7f9 Change defaults to only be defined in config_manager.rs
continuous-integration/drone/pr Build is passing Details
2022-05-12 11:55:59 +02:00
notplants e10777a5a5 Merge pull request 'Enable nanorand featureflag for cross-compilation on mac os' (#108) from cross-compilation into main
Reviewed-on: PeachCloud/peach-workspace#108
2022-05-12 09:27:00 +00:00
notplants 288941e8a3 Change Rouille to use get_config_value
continuous-integration/drone/pr Build is failing Details
2022-05-12 11:26:23 +02:00
notplants f96c950aa6 Enable nanorand featureflag for cross-compilation on mac os
continuous-integration/drone/pr Build is passing Details
2022-05-11 17:06:12 +02:00
glyph 827ccbd4dc update lockfile
continuous-integration/drone/pr Build is failing Details
2022-05-11 16:42:32 +02:00
glyph c21e2d090c introduce keystore selector for golgi 2022-05-11 16:42:11 +02:00
notplants bab33b602a Merge pull request 'Remove unecessary .to_string' (#106) from to-string into main
Reviewed-on: PeachCloud/peach-workspace#106
2022-05-11 09:48:10 +00:00
notplants b84e470a42 Remove unecessary .to_string
continuous-integration/drone/pr Build is passing Details
2022-05-11 11:24:33 +02:00
notplants 97680f9010 Merge branch 'main' of https://git.coopcloud.tech/PeachCloud/peach-workspace into main5 2022-05-11 11:05:56 +02:00
notplants ab0401e555 Change lazy_static to 1.4 2022-05-11 11:05:49 +02:00
notplants 810a97db8a Merge pull request 'Update config_manager to check from three sources of configuration' (#105) from config into main
Reviewed-on: PeachCloud/peach-workspace#105
2022-05-11 09:03:57 +00:00
notplants 610d60d989 Fix typo
continuous-integration/drone/pr Build is passing Details
2022-05-11 10:34:53 +02:00
notplants f4c1bc1169 Fix cargo fmt in peach-web
continuous-integration/drone/pr Build is passing Details
2022-05-10 13:25:53 +02:00
notplants 3ae182caa9 Fix clippy warnings in peach-web
continuous-integration/drone/pr Build is failing Details
2022-05-10 13:23:39 +02:00
notplants 8a6ad4ad61 Fix example
continuous-integration/drone/pr Build is failing Details
2022-05-10 13:08:32 +02:00
notplants 600f9c58bf Use &str instead of String in save_config_value
continuous-integration/drone/pr Build is failing Details
2022-05-10 13:07:50 +02:00
notplants 2540a77af1 Working 3-way configuration
continuous-integration/drone/pr Build is failing Details
2022-05-10 12:59:36 +02:00
notplants 5b86f754f4 Working on refactor to use hashmaps
continuous-integration/drone/pr Build is failing Details
2022-05-09 15:53:03 +02:00
notplants 29804b0dce Get CONFIG_PATH from env_variable 2022-05-03 15:50:45 +02:00
notplants e2ac5de6e4 Merge pull request 'Fix peach-config manifest' (#103) from fix-manifest into main
Reviewed-on: PeachCloud/peach-workspace#103
2022-04-20 18:27:43 +00:00
notplants d03de8cf5d Remove extraneous quote
continuous-integration/drone/pr Build is passing Details
2022-04-20 13:55:26 -04:00
notplants 03720a7338 Remove unused import
continuous-integration/drone/pr Build is passing Details
2022-04-20 12:19:15 -04:00
notplants cf9c0c7eca Add doc comment 2022-04-20 12:18:31 -04:00
notplants f764acc2df More concise transforms 2022-04-20 12:15:38 -04:00
notplants a347e4726d Fix manifest 2022-04-20 12:07:32 -04:00
notplants 3d3006049b Merge pull request 'Change peach-web to use systemd system calls' (#102) from disc-image into main
Reviewed-on: PeachCloud/peach-workspace#102
2022-04-19 18:41:22 +00:00
notplants 2adb3006fe Cargo fmt
continuous-integration/drone/pr Build is passing Details
2022-04-19 12:57:09 -04:00
notplants 64b5929e5c Update peach-config to set file permissions correctly
continuous-integration/drone/pr Build is failing Details
2022-04-19 12:50:29 -04:00
notplants 5629a048a1 Fix conflict 2022-04-18 16:45:28 -04:00
glyph 713c3da4cc fix formatting 2022-04-15 11:08:55 +02:00
glyph 92c7d7daa9 Merge pull request 'Drone CI build pipeline' (#101) from drone_ci_test into main
Reviewed-on: PeachCloud/peach-workspace#101
2022-04-15 09:05:02 +00:00
glyph 5a95ade8b9 only run ci on pull request and add docs to readme [CI SKIP]
continuous-integration/drone/pr Build is passing Details
2022-04-15 10:38:12 +02:00
glyph 00d33c2c69 remove arm64 pipeline [CI SKIP]
continuous-integration/drone/pr Build is passing Details
2022-04-15 10:22:39 +02:00
glyph 126609a605 try arm64 ci only
continuous-integration/drone/push Build was killed Details
2022-04-15 10:16:55 +02:00
glyph e4078bd1ba specify triggers twice
continuous-integration/drone/push Build was killed Details
2022-04-15 09:48:39 +02:00
notplants 315b04a63e Update permissions for peach home dir in peach-config 2022-04-14 16:29:57 -04:00
notplants 1866e289a6 Fix clippy warning in update.rs 2022-04-14 16:11:20 -04:00
notplants bff86a490b Bump version number 2022-04-14 15:53:09 -04:00
notplants 65d5352c85 Bump version number of peach-config 2022-04-14 14:51:43 -04:00
notplants df3b4b8858 Update peach-config to install go-sbot instead of peach-go-sbot 2022-04-14 14:51:27 -04:00
notplants 2f1535fbee Update peach-web to use systemd system service 2022-04-14 14:47:43 -04:00
notplants b75aadd62d Working on peachcloud disc image 2022-04-11 10:31:45 -04:00
glyph 4662b15ba3 remove unnecessary to_owned
continuous-integration/drone/push Build was killed Details
2022-04-07 14:20:41 +02:00
glyph abde4ce1b4 only specify triggers once
continuous-integration/drone/push Build was killed Details
continuous-integration/drone/pr Build was killed Details
2022-04-06 12:07:45 +02:00
glyph c792aea2f6 add build step and arm64 pipeline
continuous-integration/drone/push Build was killed Details
2022-04-06 11:55:00 +02:00
glyph b158fba147 separate ci pipeline into discreet steps
continuous-integration/drone/push Build is failing Details
2022-04-06 11:44:14 +02:00
glyph da8d8f0ec3 add rustfmt and cargo check to ci pipeline
continuous-integration/drone/push Build is failing Details
2022-04-06 11:37:11 +02:00
glyph 271aa14322 add build status badge to root readme
continuous-integration/drone/push Build is failing Details
2022-04-06 11:33:52 +02:00
glyph d31825f688 add tests to ci and fail on clippy warnings
continuous-integration/drone/push Build is failing Details
2022-04-06 11:13:31 +02:00
glyph defb8f5f09 try to fix probes build issue again
continuous-integration/drone/push Build is passing Details
2022-04-06 10:55:41 +02:00
glyph 27e9a8295c update probes and lockfile to fix time build error
continuous-integration/drone/push Build is failing Details
2022-04-06 10:50:37 +02:00
glyph 9ad580b86f install clippy in pipeline
continuous-integration/drone/push Build is failing Details
2022-04-06 10:43:35 +02:00
glyph a76ec08da6 move drone config to repo root and add triggers
continuous-integration/drone/push Build is failing Details
2022-04-06 10:36:10 +02:00
glyph cf64cd9c76 add drone ci config with clippy for amd64 2022-04-06 10:32:12 +02:00
glyph 169149d607 Merge pull request 'Replace absolute paths with relative paths for system command calls' (#100) from fix_sys_cmd_paths into main
Reviewed-on: PeachCloud/peach-workspace#100
2022-04-06 08:17:41 +00:00
glyph f1eaa07f7b replace absolute paths with relative paths for system command calls 2022-04-06 10:13:52 +02:00
glyph 52c3e88b44 Merge pull request 'Fix Scuttlebutt Status page title' (#99) from fix_status_title into main
Reviewed-on: PeachCloud/peach-workspace#99
2022-04-06 08:04:59 +00:00
glyph e659102495 update body template comment 2022-04-06 09:59:17 +02:00
glyph 57b1a786a4 fix status page title 2022-04-06 09:57:32 +02:00
glyph fded48908d Merge pull request 'Replace Rocket and Tera with Rouille and Maud' (#88) from rouille_maud into main
Reviewed-on: PeachCloud/peach-workspace#88
2022-03-25 08:07:15 +00:00
glyph 46ded85feb resolve merge conflicts 2022-03-25 10:06:00 +02:00
glyph f29659669c add route to handle resetting default sbot config 2022-03-25 09:39:06 +02:00
glyph d6695b291d tweak no-peers element 2022-03-25 09:26:44 +02:00
glyph aefa1525fb clarify missing name and description 2022-03-25 09:19:43 +02:00
glyph 367f0307b6 autofocus password input 2022-03-25 09:19:28 +02:00
glyph b1c5c701e5 reduce specificity of peach-lib dependencies 2022-03-25 08:45:10 +02:00
glyph 2ae9cb5c48 remove ellipsis label class width 2022-03-25 08:43:32 +02:00
glyph 30aff5d7ac add clarity to empty peers list message 2022-03-25 08:43:15 +02:00
glyph 25e3a145fc fix alt attribute for logout icon 2022-03-24 13:42:02 +02:00
glyph 0bfad25d3d add follow, unfollow, block and unblock routes and sbot helper functions 2022-03-24 13:41:49 +02:00
glyph 952951515b update readme 2022-03-24 09:43:31 +02:00
glyph c65f568e40 authenticate session if disable_auth env var is true 2022-03-24 09:43:25 +02:00
glyph 979ec4eb64 reintroduce logging statements 2022-03-24 09:27:06 +02:00
glyph d9019d6a4b update git ignore and remove outdated code 2022-03-24 09:19:39 +02:00
glyph 07147f8a4f remove rocket config, context and tests 2022-03-24 09:13:25 +02:00
glyph 5fc0094146 remove tera templates 2022-03-24 09:11:26 +02:00
glyph 50afb61955 update dependencies and bump minor version 2022-03-24 09:09:08 +02:00
glyph 928afb35d3 update application configuration 2022-03-24 09:07:43 +02:00
glyph 1bdacf3632 add application configuration to replace Rocket.toml 2022-03-24 09:05:53 +02:00
glyph deaedc4428 add temporary password reset routes 2022-03-24 09:05:26 +02:00
glyph 4a94f14dc5 update golgi dependency to use git path 2022-03-23 14:59:33 +02:00
glyph b78fafe84d update dependencies 2022-03-23 14:56:44 +02:00
glyph 5d37c12913 implement authentication with separate public and private routers 2022-03-23 14:56:31 +02:00
glyph 3a05396afb mount blobstore and add theme support for all routes 2022-03-23 11:41:47 +02:00
glyph 41bd39d422 add themes, add public and private msg publishing 2022-03-23 11:41:21 +02:00
glyph 77c1ccb1c7 add form handler and helper for sbot config updates 2022-03-23 09:14:54 +02:00
glyph 7d9bc2d7cd reduce code repetition with class splices 2022-03-22 16:13:50 +02:00
glyph b20822a644 satisfy clippy 2022-03-22 16:13:34 +02:00
glyph 65f0ac7630 update flash message class width 2022-03-22 16:12:47 +02:00
glyph 703f35d8b1 add profile_update module 2022-03-22 16:12:28 +02:00
glyph 084af1b486 add profile update template and route handler 2022-03-22 16:11:52 +02:00
glyph 3e918f66cf fix error flash value 2022-03-22 16:11:13 +02:00
glyph 98121f4922 use splices for rendering and url-encode the public key link 2022-03-21 16:43:22 +02:00
glyph e19fa0f99d import profile module 2022-03-21 16:42:16 +02:00
glyph 3a7b499742 use splices for template rendering 2022-03-21 16:41:52 +02:00
glyph 85231a20c7 add profile template and route handler 2022-03-21 16:40:54 +02:00
glyph 602c6a90f1 use class splices to reduce code repetition 2022-03-21 15:17:16 +02:00
glyph 34b4cbff32 add search and invite templates and route handlers 2022-03-21 13:27:32 +02:00
glyph 112cfca67b add url comment 2022-03-21 13:26:32 +02:00
glyph a379de179d add sbot error template 2022-03-21 11:17:42 +02:00
glyph 0353586705 add friends, follows and blocks route handlers and templates 2022-03-21 11:17:30 +02:00
glyph 4e8d93c388 move routes to the router 2022-03-20 17:17:17 +02:00
glyph 6db5e7c169 add routes and helpers for starting, stopping and restarting the sbot 2022-03-20 16:38:32 +02:00
glyph 60539adf41 add max-age and date for flash cookies 2022-03-20 16:37:53 +02:00
glyph e8b9cb2cc1 remove default rouille features 2022-03-20 15:37:02 +02:00
glyph cad3fc94c8 update theme import 2022-03-20 15:36:37 +02:00
glyph 976fac973d add flash messages to admin settings and auth routes 2022-03-20 15:36:24 +02:00
glyph cd7c2bc230 move sbot helper functions to utils 2022-03-20 12:28:08 +02:00
glyph 40c4f8aaf2 implement flash cookies for auth change routes 2022-03-20 12:27:00 +02:00
glyph 70f7ad0dc6 add sbot utils, move theme utils and add flash message trait 2022-03-20 12:26:04 +02:00
glyph 31628a7155 remove old scuttlebutt routes file 2022-03-18 11:33:27 +02:00
glyph 3c49c067dd fix big circle background colour when sbot status is failed 2022-03-18 11:33:10 +02:00
glyph 729580729c add scuttlebutt peers menu and inactive template 2022-03-18 11:32:51 +02:00
glyph 59739cf6e5 further refinement of the sbot status page 2022-03-18 11:30:59 +02:00
glyph 7fe919d9a1 refine sbot status pattern matching 2022-03-18 11:25:53 +02:00
glyph 7cdf8c553d complete scuttlebutt status route 2022-03-17 16:30:26 +02:00
glyph fe04195030 update lockfile and remove old auth routes file 2022-03-17 11:02:04 +02:00
glyph 8455e8089c add base templates, settings routes and auth routes 2022-03-17 11:01:36 +02:00
glyph 97206e0573 add links for submitting bug reports and feature suggestions 2022-03-14 11:17:54 +02:00
glyph 7acf6ef395 tiny wording change to bug issue template 2022-03-14 11:11:08 +02:00
glyph 3828998769 Merge pull request 'Add feature suggestion template' (#87) from bug_report_template into main
Reviewed-on: PeachCloud/peach-workspace#87
2022-03-14 09:09:46 +00:00
glyph 440d6f9bd5 add feature suggestion template 2022-03-14 11:08:32 +02:00
glyph 59a6c7fdca Merge pull request 'Move issue template to repo root' (#86) from bug_report_template into main
Reviewed-on: PeachCloud/peach-workspace#86
2022-03-14 08:27:19 +00:00
glyph 3a4b0ffffd move issue template to repo root 2022-03-14 10:26:23 +02:00
glyph 447f81a41c Merge pull request 'Add issue template for bug reporting' (#85) from bug_report_template into main
Reviewed-on: PeachCloud/peach-workspace#85
2022-03-14 08:23:01 +00:00
glyph fadad1c30b formatting 2022-03-14 10:21:50 +02:00
glyph 6395fb05e3 add issue template for bug reporting 2022-03-14 10:20:40 +02:00
glyph d652f1a020 tiny guide typo and bold fixes 2022-03-14 10:12:44 +02:00
glyph 7c98cfcd5d add admin menu and config routes; start thinking about flash msgs 2022-03-14 09:17:31 +02:00
glyph 4a1d3e81c1 Merge pull request 'Fix and improve all login and password-related workflows' (#83) from fix_login_flows into main
Reviewed-on: PeachCloud/peach-workspace#83
2022-03-13 09:14:06 +00:00
glyph 5a07eda910 update golgi dependency path 2022-03-13 11:12:12 +02:00
glyph 580771ebf2 update notes and add sbot settings config route to router 2022-03-13 11:09:39 +02:00
glyph c794d398b8 add sbot settings config route 2022-03-13 11:09:00 +02:00
glyph 4d06eb167f incomplete sbot config route 2022-03-12 10:36:40 +02:00
glyph eba15605c2 add scuttlebutt settings menu 2022-03-11 15:33:04 +02:00
glyph 07c18ea64d create settings menu route and move home route 2022-03-11 14:28:31 +02:00
glyph ec288658f3 implement basic config 2022-03-11 14:27:40 +02:00
glyph 6b145d66f8 add basic application config and parser 2022-03-11 10:19:00 +02:00
glyph 23d6870f77 home template is working 2022-03-10 11:09:26 +02:00
glyph b7cf3c1aab fix a test and update guide links 2022-03-09 13:04:44 +02:00
glyph 5b70353d6f update readme 2022-03-08 10:53:06 +02:00
glyph 67c727716c fix dark theme styling and button widths 2022-03-07 12:02:03 +02:00
glyph 5ab47cf742 made select css classes wider 2022-03-07 11:36:49 +02:00
glyph b092f1e1c4 minor wording change for temporary password request 2022-03-07 11:36:24 +02:00
glyph 983aa0689c improve messaging for when sbot is inactive 2022-03-07 11:36:03 +02:00
glyph 1a8ac3f57f change help to guide and add text 2022-03-07 11:35:19 +02:00
glyph af34829cb0 add reset password option to admin menu 2022-03-07 11:34:10 +02:00
glyph 824cbdbc0c save default peachcloud config if file does not exist 2022-03-07 11:26:52 +02:00
glyph 84656ff251 set default password 2022-03-07 11:26:13 +02:00
glyph 6cdd6dc41b remove unnecessary serde derivations, state-based remplate rendering, styling improvements 2022-03-04 10:58:50 +02:00
glyph 3572fd4e7b remove password hash logging 2022-03-04 10:56:07 +02:00
glyph 7fdf88eaa8 fix and improve all login and password-related workflows 2022-03-04 10:53:49 +02:00
glyph 10049f0bc6 Merge pull request 'Integrate golgi into peach_web' (#81) from golgi_integration into main
Reviewed-on: PeachCloud/peach-workspace#81
2022-03-03 07:37:23 +00:00
glyph 486518002d add latest lockfile 2022-03-03 09:36:48 +02:00
glyph 3991af11c7 only render private if sbot is active, fix type in profile 2022-03-03 09:34:30 +02:00
glyph 59ef5960a4 reduce code repetition 2022-03-03 09:33:45 +02:00
glyph 69a8cc262e renamed context file 2022-03-03 08:55:58 +02:00
glyph 1479a65d59 rename sbot context module to match all scuttlebutt.rs files 2022-03-03 08:55:37 +02:00
glyph ffe190148d minor css tweaks 2022-03-02 15:24:40 +02:00
glyph b1724f6eb4 update sbot status context and add latest seq num 2022-03-02 15:24:05 +02:00
glyph 9a07ab3ac0 add query param for private msgs 2022-03-02 13:51:02 +02:00
glyph 06a55ade06 remove blob rendering for blocked peers 2022-03-02 13:35:21 +02:00
glyph 6cba477f15 add private message publishing 2022-03-02 11:57:10 +02:00
glyph 020d18731b add block list and implement (un)follow and (un)block 2022-03-02 08:58:54 +02:00
glyph a38394054d implement peer search with key validation 2022-03-01 10:53:45 +02:00
glyph 814162ce7d update friends list handler and remove followers 2022-02-28 16:25:31 +02:00
glyph 03028a2278 add context builder for friends list 2022-02-28 16:25:06 +02:00
glyph e10468c337 add basic peer lookup, improve follows and profile route handlers 2022-02-28 16:08:05 +02:00
glyph 436a516c3e add conditional rendering for profile and peers list 2022-02-28 16:06:52 +02:00
glyph 786e3f41d9 add utility to check blobstore for file 2022-02-28 16:05:53 +02:00
glyph a491892bd9 assign public key id to profile context 2022-02-25 10:49:37 +02:00
glyph 02a1078ece make profile capsule border pink 2022-02-25 09:10:09 +02:00
glyph 6d2502257d add profile image uploader and blob saver 2022-02-25 09:09:41 +02:00
glyph ebbcc35fbb add deps for base64 and temp dir creation 2022-02-25 09:09:07 +02:00
glyph e3eb3be2e3 use local blobstore path util function 2022-02-25 09:08:48 +02:00
glyph 799d9de001 add io error variant and blob writer function 2022-02-25 09:08:21 +02:00
glyph e05de8284d get profile image blob path and render in template 2022-02-23 11:54:34 +02:00
glyph 9013ccb3d6 determine blobstore path and mount blob file server 2022-02-23 11:54:08 +02:00
glyph a37288225a remove decimal from memory and blobstore size 2022-02-23 10:22:52 +02:00
glyph 4665a9e6fa fix icon permissions; remove executable 2022-02-23 10:22:21 +02:00
glyph 1a3ddccbd6 add invite creation and display 2022-02-23 10:21:58 +02:00
glyph fe1da62058 add template for profile update form 2022-02-11 10:32:59 +02:00
glyph 68c926609e routes and logic for profiles and publishing name, description and post 2022-02-11 10:32:39 +02:00
glyph 17ea3e7f44 add golgi dependency and error variant 2022-02-11 10:31:31 +02:00
glyph a174027ff5 add context builders for sbot 2022-02-11 10:31:07 +02:00
glyph f459fe47d1 add display for warning flash msgs 2022-02-11 10:30:27 +02:00
glyph 4709ec77f9 Merge pull request 'Introduce theme support (plus blobstore size)' (#80) from theme_support into main
Reviewed-on: PeachCloud/peach-workspace#80
2022-02-07 07:56:30 +00:00
glyph 4e6bb15a23 update templates to use theme variables and classes 2022-02-03 16:31:17 +02:00
glyph 62191e5509 theme icons and updated hermies icons 2022-02-03 16:30:26 +02:00
glyph da976ff4fe introduce theme attributes and variables 2022-02-03 16:29:53 +02:00
glyph 0737c435a8 add theme setter and getter, update route handlers 2022-02-03 16:29:20 +02:00
glyph 435e819648 add blobstore size lookup for sbot status 2022-02-03 16:27:20 +02:00
glyph 8f49fa55ad Merge pull request 'Add configuration reading and writing for go-sbot' (#79) from sbot_config into main
Reviewed-on: PeachCloud/peach-workspace#79
2022-02-03 08:09:03 +00:00
glyph f6292407d0 add save and restart option for config form 2022-02-02 16:32:10 +02:00
glyph dfc173d941 add clarity about need for restart after config update 2022-02-02 16:20:30 +02:00
glyph a46b58b206 add repair fs config option 2022-02-02 14:49:36 +02:00
glyph 33604ac0dc change form width for better mobile styling 2022-02-02 14:49:14 +02:00
glyph f0d972f46b add repair parameter to SbotConfig 2022-02-02 14:48:39 +02:00
glyph 89b502be25 add configuration routes for the sbot 2022-02-02 14:14:12 +02:00
glyph 90a90096f4 remove option type wrappers and implement defaults for SbotConfig 2022-02-02 14:13:02 +02:00
glyph 46926bf468 add config writer method and required error variants 2022-02-01 16:07:19 +02:00
glyph d801c957bd introduce sbot status data 2022-02-01 10:06:45 +02:00
glyph 3397e5eb75 add config file reader for go-sbot 2022-02-01 10:03:46 +02:00
glyph e474ea519f Merge pull request 'Start and stop go-sbot process' (#78) from sbot_process_start_stop into main
Reviewed-on: PeachCloud/peach-workspace#78
2022-01-27 09:24:58 +00:00
glyph f715644e25 display system startup mode 2022-01-27 11:01:12 +02:00
glyph c7cc310a32 add form element to enable / disable running go-sbot on startup 2022-01-27 10:57:21 +02:00
glyph 4470f949bd add ability to stop, start and restart go-sbot process 2022-01-27 10:56:39 +02:00
glyph 8e5c29ca6d add sbot stats to home template and set inner circle colour accordingly 2022-01-27 10:55:44 +02:00
glyph 00554706cb add go-sbot enabled / disabled check 2022-01-27 10:55:04 +02:00
glyph 6d9ced5ebc Merge pull request 'Improve Scuttlebutt status page' (#77) from add_sbot_status into main
Reviewed-on: PeachCloud/peach-workspace#77
2022-01-26 10:10:49 +00:00
glyph abda4373ae conditional font colour rendering 2022-01-26 12:10:28 +02:00
glyph e718889485 update sbot status page and add live data retrieval 2022-01-26 11:48:34 +02:00
glyph b7ec1a42be conditionally render network settings button 2022-01-26 11:47:44 +02:00
glyph 445c05e3ee style horizontal lines 2022-01-26 11:46:49 +02:00
glyph 476eaa540e add managed state for conditional template element rendering 2022-01-26 11:46:14 +02:00
glyph 6f03063f8d add managed state for conditional template element rendering 2022-01-26 11:45:59 +02:00
glyph de9b8f5d73 serialize sbot stat struct 2022-01-26 11:45:25 +02:00
glyph 51eff6a298 add sbot status-related icons 2022-01-26 11:44:25 +02:00
glyph 1c90e45f11 merge latest changes from main 2022-01-25 11:48:32 +02:00
glyph 178af281ed Merge pull request 'Retrieve go-sbot systemd process statistics' (#76) from sbot_stats into main
Reviewed-on: PeachCloud/peach-workspace#76
2022-01-25 09:46:50 +00:00
glyph e1aa7b1bb6 add sbot docs and bump version 2022-01-25 11:39:28 +02:00
glyph 816d6c8a73 add sbot process stats function, struct and error variants 2022-01-25 11:36:24 +02:00
glyph b098f73a5f Merge pull request 'Load values from Rocket.toml into managed state' (#74) from rocket_config into main
Reviewed-on: PeachCloud/peach-workspace#74
2022-01-25 09:35:39 +00:00
glyph 2bfba66dab restructure auth mode check 2022-01-18 17:12:32 +02:00
glyph 43344566de read values from managed state 2022-01-18 17:00:53 +02:00
glyph bfb53747db update rocket config file and related docs 2022-01-18 16:59:54 +02:00
glyph d0321d17d0 remove lazy_static dependency 2022-01-18 16:59:44 +02:00
glyph f3ddbcf07c set auth request guard from managed state 2022-01-18 16:59:03 +02:00
glyph 680044cba8 read config params from figment and attach managed state 2022-01-18 16:58:13 +02:00
glyph 66555f19bf Merge pull request 'Deduplicate routes and add Scuttlebutt status route & template' (#73) from add_sbot_status into main
Reviewed-on: PeachCloud/peach-workspace#73
2022-01-18 10:55:43 +00:00
glyph 792779f60f deduplicate mounting of routes 2022-01-18 12:50:06 +02:00
glyph 44b68a8b71 register scuttlebutt status routes and pass standalone var to home template 2022-01-18 12:49:47 +02:00
glyph 205dd145b4 add links to templates for sbot status page 2022-01-18 12:48:40 +02:00
glyph 7346c37c86 define route and template for sbot status 2022-01-18 12:48:11 +02:00
glyph 72fbbe83f0 Merge pull request 'Add sbot configuration route and template' (#71) from sbot_settings into main
Reviewed-on: PeachCloud/peach-workspace#71
2022-01-18 09:08:52 +00:00
glyph b4a930e774 add bottom margin for small label class 2022-01-17 15:21:11 +02:00
glyph 8c4cf6261e add sbot config template and rename menu 2022-01-17 15:20:43 +02:00
glyph 8f5b257ed1 mount routes for sbot config 2022-01-17 15:20:08 +02:00
glyph 3bb00c4eb7 add route for sbot config 2022-01-17 15:19:48 +02:00
glyph 5d75aebf0d Merge pull request 'Wide range of web improvements' (#70) from web_improvements into main
Reviewed-on: PeachCloud/peach-workspace#70
2022-01-17 09:35:29 +00:00
notplants 4d2a3771b8 remove static/js from cargo-deb assetts 2022-01-16 17:41:49 -05:00
glyph ed6da528a2 remove noscript, update urls 2022-01-14 15:32:37 +02:00
glyph aca687974a fix url for redirect 2022-01-14 15:31:40 +02:00
glyph 6e4b8faf40 improve error msg 2022-01-14 15:31:22 +02:00
glyph 552c4b419e comment-out system call-invoking function 2022-01-14 15:30:54 +02:00
glyph 6fb4a2406b add docs about standalone mode config 2022-01-14 15:30:33 +02:00
glyph 65dbc6bdd4 remove noscript template snippet 2022-01-14 15:30:16 +02:00
notplants dbab6f1762 Merge pull request 'Readd peach-menu to peach-config' (#69) from peach-menu into main
Reviewed-on: PeachCloud/peach-workspace#69
2022-01-13 17:31:19 +00:00
notplants d3ae25934c Readd peach-buttons and peach-oled 2022-01-13 12:30:46 -05:00
notplants c6f68de516 Fix version numbers 2022-01-13 12:22:38 -05:00
notplants 2ccd7e65d3 Readd peach-menu 2022-01-13 12:21:51 -05:00
notplants bf3325a41e Merge pull request 'Remove unused microservices from peach-config' (#68) from update-peach-config into main
Reviewed-on: PeachCloud/peach-workspace#68
2022-01-13 16:05:31 +00:00
notplants e4b3479417 Merge branch 'main' of https://git.coopcloud.tech/PeachCloud/peach-workspace into main3 2022-01-13 10:24:19 -05:00
notplants 0561b6a9be Remove unused microservices from peach-config 2022-01-13 10:24:10 -05:00
glyph 166f4d25ae move context objects and builders to dedicated directory 2022-01-13 15:49:12 +02:00
glyph a5f0d991fa fix template rendering for help 2022-01-13 15:48:55 +02:00
glyph 60a0d7f293 set global vars for iface names 2022-01-13 15:47:43 +02:00
glyph d8c40e0724 move context builders into dedicated directory 2022-01-13 15:47:14 +02:00
glyph f4ad230d58 remove unnecessary context objects 2022-01-13 13:16:38 +02:00
glyph b0b21ad8a0 add standalone check before mounting routes 2022-01-13 13:15:42 +02:00
glyph 08ee9cd776 cargo fmt 2022-01-12 20:21:39 +02:00
glyph cfd50ca359 cleanup paths and add whitespace 2022-01-12 20:21:05 +02:00
glyph fd94ba27ac replace snafu with custom error impl 2022-01-12 19:58:49 +02:00
glyph bb5cd0f0d3 remove unneeded dependencies 2022-01-12 19:54:30 +02:00
glyph 72b7281587 remove json api tests 2022-01-12 19:51:08 +02:00
glyph cbb4027099 Merge pull request 'Add `update` and `forget` network functions' (#67) from add_network_functions into main
Reviewed-on: PeachCloud/peach-workspace#67
2022-01-12 15:38:09 +00:00
glyph 5e1520aa3f merge latest changes from main 2022-01-12 17:36:55 +02:00
glyph a8f3730b7c Merge pull request 'Satisfy clippy warnings' (#66) from satisfy_clippy_web into main
Reviewed-on: PeachCloud/peach-workspace#66
2022-01-12 15:36:16 +00:00
glyph c1432bd29e Merge branch 'main' into satisfy_clippy_web 2022-01-12 17:35:11 +02:00
glyph eb77290a93 Merge pull request 'Remove json routes, utils and javascript' (#65) from remove_json_js into main
Reviewed-on: PeachCloud/peach-workspace#65
2022-01-12 15:33:20 +00:00
glyph 5dcba8e2ad add update and forget functions 2022-01-12 13:39:38 +02:00
glyph 69ba400b69 satisfy clippy nightly warnings 2022-01-12 13:15:04 +02:00
glyph 2a7c893d94 bump version 2022-01-12 13:08:30 +02:00
glyph 2135ab1a5b remove json routes, utils and javascript 2022-01-12 13:04:47 +02:00
glyph 6f5cefa367 Merge pull request 'Replace miniserde_support with serde_support for peach-jsonrpc-server' (#64) from fix_workspace_comp into main
Reviewed-on: PeachCloud/peach-workspace#64
2022-01-12 10:25:01 +00:00
glyph c6f8591600 replace miniserde_support with serde_support 2022-01-12 12:21:12 +02:00
glyph cd1fb697f7 Merge pull request 'Fig regression of peach-dyndns-updater' (#63) from fix-regression into main
Reviewed-on: PeachCloud/peach-workspace#63
2022-01-12 09:58:46 +00:00
glyph a5415aad99 merge direct_call_net_stats branch 2022-01-12 11:35:55 +02:00
glyph 037e5c34b6 merge replace_rust_crypto branch 2022-01-12 11:21:18 +02:00
glyph 699f2b13c9 merge update_network_args branch 2022-01-12 11:19:24 +02:00
glyph c3fbc5cd73 try operator for dyns dns domain check 2022-01-12 10:59:13 +02:00
glyph 4a27892ab6 idiomatic paths and result type for checking new dns address 2022-01-12 10:58:36 +02:00
glyph 4adf5547c9 formatting 2022-01-12 10:58:11 +02:00
notplants bdfbd7057f Remove commented out code 2022-01-11 18:10:36 -05:00
notplants 171d051710 Fix clippy warmings 2022-01-11 18:06:51 -05:00
notplants 1ea0ea2ed1 Fix regression of peach-dyndns-updater 2022-01-11 18:03:07 -05:00
glyph 42774674e5 Merge pull request 'Call peach_stats and peach_network directly (remove JSON-RPC client calls)' (#62) from direct_call_net_stats into main
Reviewed-on: PeachCloud/peach-workspace#62
2022-01-07 10:01:23 +00:00
glyph 57ed0ab66a add fullstop to docs sentence 2022-01-06 11:56:45 +02:00
glyph 49ad74595c cleanup use paths and leave network_ping note 2022-01-06 11:56:23 +02:00
glyph 17d52c771f Merge branch 'main' into direct_call_net_stats
Merge crypto library update for peach-lib.
2022-01-04 18:34:21 +02:00
glyph 6792e4702d Merge pull request 'Replace outdated crypto crate' (#61) from replace_rust_crypto into main
Reviewed-on: PeachCloud/peach-workspace#61
2022-01-04 16:33:24 +00:00
glyph 446927f587 replace outdated crypto crate 2022-01-04 15:23:41 +02:00
glyph 567b0bbc2a replace network rpc client calls with direct calls to peach_network 2022-01-04 14:55:17 +02:00
glyph 3ab3e65eb7 replace stats rpc client calls with direct calls to peach_stats 2022-01-04 14:06:57 +02:00
glyph a0e80fcda7 add deps for network and stats 2022-01-04 14:06:35 +02:00
glyph 731bc1958b Merge pull request 'Update network args and remove structs' (#60) from update_network_args into main
Reviewed-on: PeachCloud/peach-workspace#60
2022-01-04 08:38:23 +00:00
glyph 58f2ddde05 replace String with str and remove unnecessary structs 2022-01-03 11:55:37 +02:00
glyph 4b0b2626a4 update code examples 2022-01-03 11:55:20 +02:00
glyph a05e67c22f bump patch version 2022-01-03 11:55:06 +02:00
notplants c75608fb1a Merge branch 'main' of https://git.coopcloud.tech/PeachCloud/peach-workspace into main3 2021-12-22 12:19:56 -05:00
notplants 068d3430d7 Merge pull request 'Add permissions function peach-config' (#56) from permissions into main
Reviewed-on: PeachCloud/peach-workspace#56
2021-12-22 17:18:24 +00:00
notplants 62793f401e Change imports and add permissions for peach-web dir 2021-12-22 10:04:15 -05:00
notplants b8f394b901 Debugging dyndns 2021-12-22 09:59:20 -05:00
notplants 9324b3ec0b Merge pull request 'Copy Rocket.toml to /usr/share/peach-web' (#55) from copy-rocket-toml into main
Reviewed-on: PeachCloud/peach-workspace#55
2021-12-22 14:53:21 +00:00
notplants f43fbf19f5 Merge pull request 'Add changepassword function to peach-config' (#53) from change-password into main
Reviewed-on: PeachCloud/peach-workspace#53
2021-12-22 14:51:27 +00:00
notplants 29cc40be48 Fix setup of nsupdate 2021-12-18 11:24:43 -05:00
notplants 570f6a679b Change permissions to u+rwX,g+rwX 2021-12-18 10:22:50 -05:00
notplants 399af51ccc Add permissions function peach-config 2021-12-18 10:00:40 -05:00
notplants 94bac00664 Fix typo in secret_key 2021-12-18 09:22:55 -05:00
notplants c41dae8d04 Copy Rocket.toml to /usr/share/peach-web 2021-12-17 17:23:27 -05:00
notplants e34df3b656 Remove configuration of http basic auth 2021-12-17 17:19:04 -05:00
notplants 3399a3c80f Add changepassword function to peach-config 2021-12-17 16:23:47 -05:00
notplants 1c26cb70fa Merge pull request 'Bump version number for peach-config' (#51) from version-number into main
Reviewed-on: PeachCloud/peach-workspace#51
2021-12-17 17:38:14 +00:00
notplants c79bd4b19f Bump version number for peach-config 2021-12-17 12:37:43 -05:00
notplants 7743511923 Merge pull request 'Update kernel version to 4.19.0-18-arm64' (#50) from kernel-version into main
Reviewed-on: PeachCloud/peach-workspace#50
2021-12-17 17:15:36 +00:00
notplants 10833078fa Update kernel version to 4.19.0-18-arm64 2021-12-17 12:15:02 -05:00
notplants 244a2132fa Merge pull request 'Move cargo/.config to root of workspace' (#49) from workspace into main
Reviewed-on: PeachCloud/peach-workspace#49
2021-12-17 15:59:14 +00:00
notplants f737236abc Move cargo/.config to root of workspace 2021-12-16 12:55:14 -05:00
glyph b5ce677a5b Merge pull request 'Removed hardcoded interfaces from peach-network' (#47) from iface_agnostic into main
Reviewed-on: PeachCloud/peach-workspace#47
2021-12-14 20:42:40 +00:00
glyph 4d6dbd511e Merge pull request 'Remove jsonrpc from peach-network' (#46) from split_network_logic into main
Reviewed-on: PeachCloud/peach-workspace#46
2021-12-14 18:55:16 +00:00
glyph 7fe4715014 bump version and switch to rust 2021 2021-12-14 16:13:27 +02:00
glyph dd33fdd47d Merge pull request 'Create JSON-RPC server repo with stats' (#44) from jsonrpc_server into main
Reviewed-on: PeachCloud/peach-workspace#44
2021-12-14 13:05:00 +00:00
glyph 1986d31461 feature flag docs and license 2021-12-13 11:00:16 +02:00
glyph a824be53b9 update docs and remove unnecessary structs 2021-12-13 10:55:27 +02:00
glyph 287082381e remove json-rpc methods 2021-12-13 10:55:04 +02:00
glyph 9f40378fce remove json-rpc error code and improve docs 2021-12-13 10:54:51 +02:00
glyph 4f5eb3aa04 update dependencies and version 2021-12-13 10:54:38 +02:00
glyph f4113f0632 remove debian files and main 2021-12-13 10:54:24 +02:00
glyph 8032b83c41 resolve merge conflicts 2021-12-13 08:26:25 +02:00
glyph dfa1306b2d update lockfile 2021-12-13 08:20:03 +02:00
glyph 2f7c7aac8f bump version and fix wpactrl dep import 2021-12-13 08:19:55 +02:00
glyph 46b7c0fc2b update wpactrl tests 2021-12-13 08:19:25 +02:00
glyph f62c8f0b51 merge stats changes 2021-12-13 07:25:30 +02:00
glyph 06e48deb3a Merge pull request 'Remove jsonrpc from peach-stats' (#43) from separate_stats_logic into main
Reviewed-on: PeachCloud/peach-workspace#43
2021-12-13 05:20:58 +00:00
glyph fb6d0317b6 bump version 2021-12-13 07:19:14 +02:00
glyph cfbf052d27 fix example code 2021-12-13 07:19:05 +02:00
glyph d240741958 update lockfile 2021-12-10 11:02:06 +02:00
glyph 33486b4e1d jsonrpc server with peach-stats methods 2021-12-10 11:01:51 +02:00
glyph 8c3fecb875 add docs about feature flags 2021-12-09 09:54:53 +02:00
glyph 0907fbc474 remove jsonrpc from peach-stats 2021-12-09 09:44:27 +02:00
glyph b747ff6db2 Merge pull request 'Use tuple err source PeachError::JsonRpcClientCore' (#42) from fix_client_error into main
Reviewed-on: PeachCloud/peach-workspace#42
2021-12-08 08:10:07 +00:00
glyph 220c7fd540 use tuple err source 2021-12-08 10:07:58 +02:00
glyph ed7e172efb Merge pull request 'Fix manifest mess for peach-lib from nanorand PR' (#40) from nanorand_again into main
Reviewed-on: PeachCloud/peach-workspace#40
2021-12-07 12:16:47 +00:00
glyph bc0f2d595b fix pr mess for peach-lib 2021-12-07 14:14:51 +02:00
glyph 61b33d1613 trying to resolve conflict for nanorand pr 2021-12-07 14:08:21 +02:00
glyph c3fa188400 testing refactored wpactrl 2021-12-07 10:04:58 +02:00
glyph a1444cf478 Merge pull request 'Error refactor for peach-lib' (#38) from lib_error_refactor into main
Reviewed-on: PeachCloud/peach-workspace#38
2021-12-07 08:00:51 +00:00
glyph 79c94e6af0 replace snafu with custom error type 2021-12-01 14:26:30 +02:00
glyph 30f00524f4 remove unneeded dependency 2021-11-30 13:55:11 +02:00
glyph cd8e5737c4 implement custom error type 2021-11-30 13:48:16 +02:00
glyph 2429ea8fdd replace rand with nanorand and bump version 2021-11-25 15:00:19 +02:00
glyph 406206bab3 update lockfile 2021-11-25 12:20:11 +02:00
glyph c72c57c345 update lockfile 2021-11-25 09:58:49 +02:00
glyph bf9dd7f567 bump version 2021-11-25 09:58:30 +02:00
glyph e6a6fcdc89 remove snafu context and improve error handling 2021-11-25 09:58:00 +02:00
glyph b2f7747357 implement custom error type 2021-11-25 09:56:21 +02:00
glyph f17ae95b21 remove snafu 2021-11-25 09:56:10 +02:00
glyph 91180ed1fe clippy tidy-up 2021-11-25 09:55:56 +02:00
213 changed files with 12218 additions and 10789 deletions

33
.drone.yml Normal file
View File

@ -0,0 +1,33 @@
kind: pipeline
type: docker
name: test-on-amd64
platform:
arch: amd64
steps:
- name: rustfmt
image: rust:buster
commands:
- rustup component add rustfmt
- cargo fmt --check
- name: clippy
image: rust:buster
commands:
- rustup component add clippy
- cargo clippy -- -D warnings
- name: test
image: rust:buster
commands:
- cargo test
- name: build
image: rust:buster
commands:
- cargo build
trigger:
event:
- pull_request

3
.gitignore vendored
View File

@ -1,2 +1,5 @@
.idea
target
*peachdeploy.sh
*vpsdeploy.sh
*bindeploy.sh

2958
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,5 @@
[workspace]
members = [
"peach-buttons",
"peach-oled",
"peach-lib",
"peach-config",
@ -10,6 +8,7 @@ members = [
"peach-menu",
"peach-monitor",
"peach-stats",
"peach-probe",
"peach-jsonrpc-server",
"peach-dyndns-updater"
]

View File

@ -4,6 +4,8 @@ _Better [Scuttlebutt](https://scuttlebutt.nz) cloud infrastructure as a hardware
[**_Support us on OpenCollective!_**](https://opencollective.com/peachcloud)
[![Build Status](https://build.coopcloud.tech/api/badges/PeachCloud/peach-workspace/status.svg?ref=refs/heads/main)](https://build.coopcloud.tech/PeachCloud/peach-workspace)
## Background
- April 2018 project proposal: [`%HqwAsltORROCh4uyOq6iV+SsqU3OuNUevnq+5dwCqVI=.sha256`](https://viewer.scuttlebot.io/%25HqwAsltORROCh4uyOq6iV%2BSsqU3OuNUevnq%2B5dwCqVI%3D.sha256)
@ -43,6 +45,19 @@ _Better [Scuttlebutt](https://scuttlebutt.nz) cloud infrastructure as a hardware
- [peach-patterns](https://github.com/peachcloud/peach-patterns) - Pattern library for the PeachCloud UI design system
- [peach-web](https://github.com/peachcloud/peach-web) - A web interface for monitoring and interacting with the PeachCloud device
## Continuous Integration
[Drone CI](https://docs.drone.io/) is used to provide continuous integration for this workspace. The configuration file can be found in `.drone.yml` in the root of this repository. It is currently configured to run `cargo fmt`, `cargo clippy`, `cargo test` and `cargo build` on every `pull request` event. The pipeline runs on the AMD64 Debian Buster image from the official Rust Docker image repository.
The status of the current and previous CI builds can be viewed via the [Drone CI Build UI](https://build.coopcloud.tech/PeachCloud/peach-workspace) (kindly hosted by Co-op Cloud).
Adding `[CI SKIP]` to the end of a commit message results in the CI checks being skipped for the next event. For example:
```
git commit -m "update readme [CI SKIP]"
git push origin main
```
## Developer Diaries
- [@ahdinosaur](https://github.com/ahdinosaur): `@6ilZq3kN0F+dXFHAPjAwMm87JEb/VdB+LC9eIMW3sa0=.ed25519`
@ -56,4 +71,4 @@ _Better [Scuttlebutt](https://scuttlebutt.nz) cloud infrastructure as a hardware
- [GitHub](https://github.com/peachcloud)
- [Twitter](https://twitter.com/peachcloudorg)
- [Email](mailto:peachcloudorg@gmail.com)
- [OpenCollective](https://opencollective.com/peachcloud)
- [OpenCollective](https://opencollective.com/peachcloud)

View File

@ -0,0 +1,34 @@
---
name: "Bug Report Template"
about: "This template is for submitting bugs."
title: "[BUG] "
ref: "main"
labels:
- bug
- "help needed"
---
> Please fill out the sections below.
> Be kind and objective when writing in text.
> Thanks for the report! :)
**Brief description of the bug:**
**Steps to reproduce the bug:**
**Expected behaviour:**
**Technical details:**
_Is peach-web running on an x86-64 or arm64 machine?_
_What operating system distribution is it running on?_

View File

@ -0,0 +1,15 @@
---
name: "Feature Suggestion Template"
about: "This template is for submitting feature suggestions."
title: "[FEATURE] "
ref: "main"
labels:
- enhancement
---
**Brief description of the feature you'd like to suggest:**

View File

@ -1,4 +0,0 @@
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
objcopy = { path ="aarch64-linux-gnu-objcopy" }
strip = { path ="aarch64-linux-gnu-strip" }

View File

@ -1,6 +1,6 @@
[package]
name = "peach-config"
version = "0.1.10"
version = "0.1.27"
authors = ["Andrew Reid <gnomad@cryptolab.net>", "Max Fowler <max@mfowler.info>"]
edition = "2018"
description = "Command line tool for installing, updating and configuring PeachCloud"
@ -35,3 +35,7 @@ structopt = "0.3.13"
clap = "2.33.3"
log = "0.4"
lazy_static = "1.4.0"
peach-lib = { path = "../peach-lib" }
rpassword = "5.0"
golgi = { git = "https://git.coopcloud.tech/golgi-ssb/golgi.git" }
async-std = "1.10.0"

View File

@ -8,7 +8,7 @@ dtparam=i2c_arm=on
# Apply device tree overlay to enable pull-up resistors for buttons
device_tree_overlay=overlays/mygpio.dtbo
kernel=vmlinuz-4.19.0-17-arm64
kernel=vmlinuz-4.19.0-18-arm64
# For details on the initramfs directive, see
# https://www.raspberrypi.org/forums/viewtopic.php?f=63&t=10532
initramfs initrd.img-4.19.0-17-arm64
initramfs initrd.img-4.19.0-18-arm64

View File

@ -0,0 +1,35 @@
use crate::error::PeachConfigError;
use crate::ChangePasswordOpts;
use peach_lib::password_utils::set_new_password;
/// Utility function to set the admin password for peach-web from the command-line.
pub fn set_peach_web_password(opts: ChangePasswordOpts) -> Result<(), PeachConfigError> {
match opts.password {
// read password from CLI arg
Some(password) => {
set_new_password(&password)
.map_err(|err| PeachConfigError::ChangePasswordError { source: err })?;
println!(
"Your new password has been set for peach-web. You can login through the \
web interface with username admin."
);
Ok(())
}
// read password from tty
None => {
let pass1 = rpassword::read_password_from_tty(Some("New password: "))?;
let pass2 = rpassword::read_password_from_tty(Some("Confirm password: "))?;
if pass1 != pass2 {
Err(PeachConfigError::InvalidPassword)
} else {
set_new_password(&pass1)
.map_err(|err| PeachConfigError::ChangePasswordError { source: err })?;
println!(
"Your new password has been set for peach-web. You can login through the \
web interface with username admin."
);
Ok(())
}
}
}
}

View File

@ -3,17 +3,14 @@
pub const CONF: &str = "/var/lib/peachcloud/conf";
// List of package names which are installed via apt-get
pub const SERVICES: [&str; 11] = [
"peach-oled",
"peach-network",
"peach-stats",
pub const SERVICES: [&str; 8] = [
"peach-web",
"peach-probe",
"peach-menu",
"peach-buttons",
"peach-monitor",
"peach-probe",
"peach-oled",
"peach-dyndns-updater",
"peach-go-sbot",
"go-sbot",
"peach-config",
];

View File

@ -1,4 +1,6 @@
#![allow(clippy::nonstandard_macro_braces)]
use golgi::error::GolgiError;
use peach_lib::error::PeachError;
pub use snafu::ResultExt;
use snafu::Snafu;
@ -30,6 +32,18 @@ pub enum PeachConfigError {
},
#[snafu(display("Error serializing json: {}", source))]
SerdeError { source: serde_json::Error },
#[snafu(display("Error changing password: {}", source))]
ChangePasswordError { source: PeachError },
#[snafu(display("Entered passwords did not match. Please try again."))]
InvalidPassword,
#[snafu(display("Error in peach lib: {}", source))]
PeachLibError { source: PeachError },
#[snafu(display("Error in golgi: {}", source))]
Golgi { source: GolgiError },
#[snafu(display("{}", message))]
CmdInputError { message: String },
#[snafu(display("{}", message))]
WaitForSbotError { message: String },
}
impl From<std::io::Error> for PeachConfigError {
@ -46,3 +60,15 @@ impl From<serde_json::Error> for PeachConfigError {
PeachConfigError::SerdeError { source: err }
}
}
impl From<PeachError> for PeachConfigError {
fn from(err: PeachError) -> PeachConfigError {
PeachConfigError::PeachLibError { source: err }
}
}
impl From<GolgiError> for PeachConfigError {
fn from(err: GolgiError) -> PeachConfigError {
PeachConfigError::Golgi { source: err }
}
}

View File

@ -1,40 +1,32 @@
use regex::Regex;
use serde::{Deserialize, Serialize};
use snafu::ResultExt;
use std::collections::HashMap;
use std::fs;
use crate::constants::HARDWARE_CONFIG_FILE;
use crate::constants::{HARDWARE_CONFIG_FILE, SERVICES};
use crate::error::{FileReadError, FileWriteError, PeachConfigError};
use crate::utils::get_output;
use crate::RtcOption;
/// Helper function which returns the version of a package currently installed,
/// as an Ok(String) if found, and as an Err if not found
pub fn get_package_version_number(package: &str) -> Result<String, PeachConfigError> {
let version = get_output(&["dpkg-query", "--showformat=${Version}", "--show", package])?;
Ok(version)
}
/// Returns a HashMap<String, String> of all the peach-packages which are currently installed
/// mapped to their version number e.g. { "peach-probe": "1.2.0", "peach-network": "1.4.0" }
pub fn get_currently_installed_microservices() -> Result<HashMap<String, String>, PeachConfigError>
{
// gets a list of all packages currently installed with dpkg
let packages = get_output(&["dpkg", "-l"])?;
// this regex matches packages which contain the word peach in them
// and has two match groups
// 1. the first match group gets the package name
// 2. the second match group gets the version number of the package
let re: Regex = Regex::new(r"\S+\s+(\S*peach\S+)\s+(\S+).*\n").unwrap();
// the following iterator, iterates through the captures matched via the regex
// and for each capture, creates a value in the hash map,
// which maps the name of the package, to its version number
// e.g. { "peach-probe": "1.2.0", "peach-network": "1.4.0" }
let peach_packages: HashMap<String, String> = re
.captures_iter(&packages)
.filter_map(|cap| {
let groups = (cap.get(1), cap.get(2));
match groups {
(Some(package), Some(version)) => {
Some((package.as_str().to_string(), version.as_str().to_string()))
}
_ => None,
// gets a list of all packages currently installed with dpkg-query
let peach_packages: HashMap<String, String> = SERVICES
.iter()
.filter_map(|service| {
let version = get_package_version_number(service);
match version {
Ok(v) => Some((service.to_string(), v)),
Err(_) => None,
}
})
.collect();

View File

@ -1,21 +1,22 @@
mod change_password;
mod constants;
mod error;
mod generate_manifest;
mod publish_address;
mod set_permissions;
mod setup_networking;
mod setup_peach;
mod setup_peach_deb;
mod status;
mod update;
mod utils;
mod wait_for_sbot;
use clap::arg_enum;
use log::error;
use serde::{Deserialize, Serialize};
use structopt::StructOpt;
use crate::generate_manifest::generate_manifest;
use crate::setup_peach::setup_peach;
use crate::update::update;
#[derive(StructOpt, Debug)]
#[structopt(
name = "peach-config",
@ -44,6 +45,27 @@ enum PeachConfig {
/// Updates all PeachCloud microservices
#[structopt(name = "update")]
Update(UpdateOpts),
/// Changes the password for the peach-web interface
#[structopt(name = "change-password")]
ChangePassword(ChangePasswordOpts),
/// Updates file permissions on PeachCloud device
#[structopt(name = "permissions")]
SetPermissions,
/// Returns sbot id if sbot is running
#[structopt(name = "whoami")]
WhoAmI,
/// Publish domain and port.
/// It takes an address argument of the form host:port
#[structopt(name = "publish-address")]
PublishAddress(PublishAddressOpts),
/// Wait for a successful connection to sbot
#[structopt(name = "wait-for-sbot")]
WaitForSbot,
}
#[derive(StructOpt, Debug)]
@ -76,6 +98,21 @@ pub struct UpdateOpts {
list: bool,
}
#[derive(StructOpt, Debug)]
pub struct ChangePasswordOpts {
/// Optional argument to specify password as CLI argument
/// if not specified, this command asks for user input for the passwords
#[structopt(short, long)]
password: Option<String>,
}
#[derive(StructOpt, Debug)]
pub struct PublishAddressOpts {
/// Specify address in the form domain:port
#[structopt(short, long)]
address: String,
}
arg_enum! {
/// enum options for real-time clock choices
#[derive(Debug)]
@ -88,7 +125,7 @@ arg_enum! {
}
}
fn main() {
async fn run() {
// initialize the logger
env_logger::init();
@ -99,28 +136,84 @@ fn main() {
if let Some(subcommand) = opt.commands {
match subcommand {
PeachConfig::Setup(cfg) => {
match setup_peach(cfg.no_input, cfg.default_locale, cfg.i2c, cfg.rtc) {
match setup_peach::setup_peach(cfg.no_input, cfg.default_locale, cfg.i2c, cfg.rtc) {
Ok(_) => {}
Err(err) => {
error!("peach-config encountered an error: {}", err)
}
}
}
PeachConfig::Manifest => match generate_manifest() {
PeachConfig::Manifest => match generate_manifest::generate_manifest() {
Ok(_) => {}
Err(err) => {
error!(
"peach-config countered an error generating manifest: {}",
"peach-config encountered an error generating manifest: {}",
err
)
}
},
PeachConfig::Update(opts) => match update(opts) {
PeachConfig::Update(opts) => match update::update(opts) {
Ok(_) => {}
Err(err) => {
error!("peach-config encountered an error during update: {}", err)
}
},
PeachConfig::ChangePassword(opts) => {
match change_password::set_peach_web_password(opts) {
Ok(_) => {}
Err(err) => {
error!(
"peach-config encountered an error during password update: {}",
err
)
}
}
}
PeachConfig::SetPermissions => match set_permissions::set_permissions() {
Ok(_) => {}
Err(err) => {
error!(
"peach-config ecountered an error updating file permissions: {}",
err
)
}
},
PeachConfig::WhoAmI => match status::whoami().await {
Ok(sbot_id) => {
println!("{:?}", sbot_id);
{}
}
Err(err) => {
error!("sbot whoami encountered an error: {}", err)
}
},
PeachConfig::PublishAddress(opts) => {
match publish_address::publish_address(opts.address).await {
Ok(_) => {}
Err(err) => {
error!(
"peach-config encountered an error during publish address: {}",
err
)
}
}
}
PeachConfig::WaitForSbot => match wait_for_sbot::wait_for_sbot().await {
Ok(sbot_id) => {
println!("connected with sbot and found sbot_id: {:?}", sbot_id)
}
Err(err) => {
error!("peach-config did not successfully connect to sbot: {}", err)
}
},
}
}
}
// Enable an async main function and execute the `run()` function,
// catching any errors and printing them to `stderr` before exiting the
// process.
#[async_std::main]
async fn main() {
run().await;
}

View File

@ -0,0 +1,37 @@
use crate::error::PeachConfigError;
use golgi::kuska_ssb::api::dto::content::PubAddress;
use golgi::messages::SsbMessageContent;
use peach_lib::sbot::init_sbot;
/// Utility function to publish the address (domain:port) of the pub
/// publishing the address causes the domain and port to be used for invite generation,
/// and also gossips this pub address to their peers
pub async fn publish_address(address: String) -> Result<(), PeachConfigError> {
// split address into domain:port
let split: Vec<&str> = address.split(':').collect();
let (domain, port): (&str, &str) = (split[0], split[1]);
// convert port to u16
let port_as_u16: u16 = port
.parse()
.map_err(|_err| PeachConfigError::CmdInputError {
message: "Failure to parse domain and port. Address must be of the format host:port."
.to_string(),
})?;
// publish address
let mut sbot = init_sbot().await?;
let pub_id = sbot.whoami().await?;
// Compose a `pub` address type message.
let pub_address_msg = SsbMessageContent::Pub {
address: Some(PubAddress {
// Host name (can be an IP address if onboarding over WiFi).
host: Some(domain.to_string()),
// Port.
port: port_as_u16,
// Public key.
key: pub_id,
}),
};
// Publish the `pub` address message.
let _pub_msg_ref = sbot.publish(pub_address_msg).await?;
Ok(())
}

View File

@ -0,0 +1,30 @@
use lazy_static::lazy_static;
use peach_lib::config_manager;
use crate::error::PeachConfigError;
use crate::utils::cmd;
lazy_static! {
pub static ref PEACH_CONFIGDIR: String = config_manager::get_config_value("PEACH_CONFIGDIR")
.expect("Failed to load config value for PEACH_CONFIGDIR");
pub static ref PEACH_WEBDIR: String = config_manager::get_config_value("PEACH_WEBDIR")
.expect("Failed to load config value for PEACH_WEBDIR");
pub static ref PEACH_HOMEDIR: String = config_manager::get_config_value("PEACH_HOMEDIR")
.expect("Failed to load config value for PEACH_HOMEDIR");
}
/// Utility function to set correct file permissions on the PeachCloud device.
/// Accidentally changing file permissions is a fairly common thing to happen,
/// so this is a useful CLI function for quickly correcting anything that may be out of order.
pub fn set_permissions() -> Result<(), PeachConfigError> {
println!("[ UPDATING FILE PERMISSIONS ON PEACHCLOUD DEVICE ]");
cmd(&["chmod", "-R", "u+rwX,g+rwX", &PEACH_CONFIGDIR])?;
cmd(&["chown", "-R", "peach:peach", &PEACH_CONFIGDIR])?;
cmd(&["chmod", "-R", "u+rwX,g+rwX", &PEACH_WEBDIR])?;
cmd(&["chown", "-R", "peach:peach", &PEACH_WEBDIR])?;
cmd(&["chmod", "-R", "u+rwX,g+rwX", &PEACH_HOMEDIR])?;
cmd(&["chown", "-R", "peach:peach", &PEACH_HOMEDIR])?;
println!("[ PERMISSIONS SUCCESSFULLY UPDATED ]");
Ok(())
}

View File

@ -4,6 +4,7 @@ use std::fs;
use crate::error::{FileWriteError, PeachConfigError};
use crate::generate_manifest::save_hardware_config;
use crate::set_permissions::set_permissions;
use crate::setup_networking::configure_networking;
use crate::setup_peach_deb::setup_peach_deb;
use crate::update::update_microservices;
@ -68,6 +69,7 @@ pub fn setup_peach(
"libssl-dev",
"nginx",
"wget",
"dnsutils",
"-y",
])?;
@ -238,6 +240,9 @@ pub fn setup_peach(
info!("[ SAVING LOG OF HARDWARE CONFIGURATIONS ]");
save_hardware_config(i2c, rtc)?;
info!("[ SETTING FILE PERMISSIONS ]");
set_permissions()?;
info!("[ PEACHCLOUD SETUP COMPLETE ]");
info!("[ ------------------------- ]");
info!("[ please reboot your device ]");

View File

@ -0,0 +1,9 @@
use crate::error::PeachConfigError;
use peach_lib::sbot::init_sbot;
/// Utility function to check if sbot is running via the whoami method
pub async fn whoami() -> Result<String, PeachConfigError> {
let mut sbot = init_sbot().await?;
let sbot_id = sbot.whoami().await?;
Ok(sbot_id)
}

View File

@ -47,8 +47,8 @@ pub fn update_microservices() -> Result<(), PeachConfigError> {
cmd(&["apt-get", "update"])?;
// filter out peach-config from list of services
let services_to_update: Vec<&str> = SERVICES
.to_vec()
.into_iter()
.iter()
.copied()
.filter(|&x| x != "peach-config")
.collect();

View File

@ -0,0 +1,52 @@
use std::{thread, time};
use crate::error::PeachConfigError;
use peach_lib::sbot::init_sbot;
static MAX_NUM_ATTEMPTS: u8 = 10;
/// Utility function to wait for a successful whoami call with sbot
/// After each attempt to call whoami it waits 2 seconds,
/// and if after MAX_NUM_ATTEMPTS (10) there is no successful whoami call
/// it returns an Error. Otherwise it returns Ok(sbot_id).
pub async fn wait_for_sbot() -> Result<String, PeachConfigError> {
let mut num_attempts = 0;
let mut whoami = None;
while num_attempts < MAX_NUM_ATTEMPTS {
let mut sbot = None;
let sbot_res = init_sbot().await;
match sbot_res {
Ok(sbot_instance) => {
sbot = Some(sbot_instance);
}
Err(err) => {
eprintln!("failed to connect to sbot: {:?}", err);
}
}
if sbot.is_some() {
let sbot_id_res = sbot.unwrap().whoami().await;
match sbot_id_res {
Ok(sbot_id) => {
whoami = Some(sbot_id);
break;
}
Err(err) => {
eprintln!("whoami failed: {:?}", err);
}
}
}
println!("trying to connect to sbot again {:?}", num_attempts);
num_attempts += 1;
let sleep_duration = time::Duration::from_secs(2);
thread::sleep(sleep_duration);
}
whoami.ok_or(PeachConfigError::WaitForSbotError {
message: "Failed to find sbot_id after 10 attempts".to_string(),
})
}

View File

@ -1,4 +0,0 @@
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
objcopy = { path ="aarch64-linux-gnu-objcopy" }
strip = { path ="aarch64-linux-gnu-strip" }

View File

@ -1,6 +1,6 @@
[package]
name = "peach-dyndns-updater"
version = "0.1.6"
version = "0.1.8"
authors = ["Max Fowler <mfowler@commoninternet.net>"]
edition = "2018"
description = "Sytemd timer which keeps a dynamic dns subdomain up to date with the latest device IP using nsupdate."

View File

@ -0,0 +1,29 @@
#!/usr/bin/env bash
# exit when any command fails
set -e
KEYFILE=/Users/notplants/.ssh/id_rsa
SERVICE=peach-dyndns-updater
# deploy
rsync -avzh --exclude target --exclude .idea --exclude .git -e "ssh -i $KEYFILE" . rust@167.99.136.83:/srv/peachcloud/automation/peach-workspace/$SERVICE/
rsync -avzh --exclude target --exclude .idea --exclude .git -e "ssh -i $KEYFILE" ~/computer/projects/peachcloud/peach-workspace/peach-lib/ rust@167.99.136.83:/srv/peachcloud/automation/peach-workspace/peach-lib/
echo "++ cross compiling on vps"
BIN_PATH=$(ssh -i $KEYFILE rust@167.99.136.83 'cd /srv/peachcloud/automation/peach-workspace/peach-dyndns-updater; /home/rust/.cargo/bin/cargo clean -p peach-lib; /home/rust/.cargo/bin/cargo build --release --target=aarch64-unknown-linux-gnu')
echo "++ copying ${BIN_PATH} to local"
rm -f target/$SERVICE
scp -i $KEYFILE rust@167.99.136.83:/srv/peachcloud/automation/peach-workspace/target/aarch64-unknown-linux-gnu/release/peach-dyndns-updater ../target/vps-bin-$SERVICE
#echo "++ cross compiling"
BINFILE="../target/vps-bin-$SERVICE"
echo $BINFILE
echo "++ build successful"
echo "++ copying to pi"
ssh -t -i $KEYFILE peach@peach.link 'mkdir -p /srv/dev/bins'
scp -i $KEYFILE $BINFILE peach@peach.link:/srv/dev/bins/$SERVICE

View File

@ -1,6 +1,5 @@
use log::info;
use peach_lib::dyndns_client::dyndns_update_ip;
use log::{info};
fn main() {
// initalize the logger
@ -9,4 +8,4 @@ fn main() {
info!("Running peach-dyndns-updater");
let result = dyndns_update_ip();
info!("result: {:?}", result);
}
}

View File

@ -0,0 +1,25 @@
[package]
name = "peach-jsonrpc-server"
authors = ["Andrew Reid <glyph@mycelial.technology>"]
version = "0.1.0"
edition = "2021"
description = "JSON-RPC over HTTP for the PeachCloud system. Provides a JSON-RPC wrapper around the stats, network and oled libraries."
homepage = "https://opencollective.com/peachcloud"
repository = "https://git.coopcloud.tech/PeachCloud/peach-workspace"
readme = "README.md"
license = "AGPL-3.0-only"
publish = false
[badges]
maintenance = { status = "actively-developed" }
[dependencies]
env_logger = "0.9"
jsonrpc-core = "18"
jsonrpc-http-server = "18"
log = "0.4"
peach-stats = { path = "../peach-stats", features = ["serde_support"] }
serde_json = "1.0.74"
[dev-dependencies]
jsonrpc-test = "18"

View File

@ -0,0 +1,72 @@
# peach-jsonrpc-server
A JSON-RPC server for the PeachCloud system which exposes an API over HTTP.
Currently includes peach-stats capability (system statistics).
## JSON-RPC API
| Method | Description | Returns |
| --- | --- | --- |
| `cpu_stats` | CPU statistics | `user`, `system`, `nice`, `idle` |
| `cpu_stats_percent` | CPU statistics as percentages | `user`, `system`, `nice`, `idle` |
| `disk_usage` | Disk usage statistics (array of disks) | `filesystem`, `one_k_blocks`, `one_k_blocks_used`, `one_k_blocks_free`, `used_percentage`, `mountpoint` |
| `load_average` | Load average statistics | `one`, `five`, `fifteen` |
| `mem_stats` | Memory statistics | `total`, `free`, `used` |
| `ping` | Microservice status | `success` if running |
| `uptime` | System uptime | `secs` |
## Environment
The JSON-RPC HTTP server is currently hardcoded to run on "127.0.0.1:5110". Address and port configuration settings will later be exposed via CLI arguments and possibly an environment variable.
Logging is made available with `env_logger`:
`export RUST_LOG=info`
Other logging levels include `debug`, `warn` and `error`.
## Setup
Clone the peach-workspace repo:
`git clone https://git.coopcloud.tech/PeachCloud/peach-workspace`
Move into the repo peaach-jsonrpc-server directory and compile a release build:
`cd peach-jsonrpc-server`
`cargo build --release`
Run the binary:
`./peach-workspace/target/release/peach-jsonrpc-server`
## Debian Packaging
TODO.
## Example Usage
**Get CPU Statistics**
With microservice running, open a second terminal window and use `curl` to call server methods:
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "cpu_stats", "id":1 }' 127.0.0.1:5110`
Server responds with:
`{"jsonrpc":"2.0","result":"{\"user\":4661083,\"system\":1240371,\"idle\":326838290,\"nice\":0}","id":1}`
**Get System Uptime**
With microservice running, open a second terminal window and use `curl` to call server methods:
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "uptime", "id":1 }' 127.0.0.1:5110`
Server responds with:
`{"jsonrpc":"2.0","result":"{\"secs\":840968}","id":1}`
### Licensing
AGPL-3.0

View File

@ -0,0 +1,58 @@
use std::fmt;
use jsonrpc_core::{Error as JsonRpcError, ErrorCode};
use serde_json::error::Error as SerdeJsonError;
use peach_stats::StatsError;
/// Custom error type encapsulating all possible errors for a JSON-RPC server
/// and associated methods.
#[derive(Debug)]
pub enum JsonRpcServerError {
/// Failed to serialize a string from a data structure.
Serde(SerdeJsonError),
/// An error returned from the `peach-stats` library.
Stats(StatsError),
/// An expected JSON-RPC method parameter was not provided.
MissingParameter(JsonRpcError),
/// Failed to parse a provided JSON-RPC method parameter.
ParseParameter(JsonRpcError),
}
impl fmt::Display for JsonRpcServerError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
JsonRpcServerError::ParseParameter(ref source) => {
write!(f, "Failed to parse parameter: {}", source)
}
JsonRpcServerError::MissingParameter(ref source) => {
write!(f, "Missing expected parameter: {}", source)
}
JsonRpcServerError::Serde(ref source) => {
write!(f, "{}", source)
}
JsonRpcServerError::Stats(ref source) => {
write!(f, "{}", source)
}
}
}
}
impl From<JsonRpcServerError> for JsonRpcError {
fn from(err: JsonRpcServerError) -> Self {
match &err {
JsonRpcServerError::Serde(source) => JsonRpcError {
code: ErrorCode::ServerError(-32002),
message: format!("{}", source),
data: None,
},
JsonRpcServerError::Stats(source) => JsonRpcError {
code: ErrorCode::ServerError(-32001),
message: format!("{}", source),
data: None,
},
JsonRpcServerError::MissingParameter(source) => source.clone(),
JsonRpcServerError::ParseParameter(source) => source.clone(),
}
}
}

View File

@ -0,0 +1,140 @@
//! # peach-jsonrpc-server
//!
//! A JSON-RPC server which exposes an API over HTTP.
use std::env;
use std::result::Result;
use jsonrpc_core::{IoHandler, Value};
use jsonrpc_http_server::{AccessControlAllowOrigin, DomainsValidation, ServerBuilder};
use log::info;
use peach_stats::stats;
mod error;
use crate::error::JsonRpcServerError;
/// Create JSON-RPC I/O handler, add RPC methods and launch HTTP server.
pub fn run() -> Result<(), JsonRpcServerError> {
info!("Starting up.");
info!("Creating JSON-RPC I/O handler.");
let mut io = IoHandler::default();
io.add_sync_method("ping", |_| Ok(Value::String("success".to_string())));
// TODO: add blocks of methods according to provided flags
/* PEACH-STATS RPC METHODS */
io.add_sync_method("cpu_stats", move |_| {
info!("Fetching CPU statistics.");
let cpu = stats::cpu_stats().map_err(JsonRpcServerError::Stats)?;
let json_cpu = serde_json::to_string(&cpu).map_err(JsonRpcServerError::Serde)?;
Ok(Value::String(json_cpu))
});
io.add_sync_method("cpu_stats_percent", move |_| {
info!("Fetching CPU statistics as percentages.");
let cpu = stats::cpu_stats_percent().map_err(JsonRpcServerError::Stats)?;
let json_cpu = serde_json::to_string(&cpu).map_err(JsonRpcServerError::Serde)?;
Ok(Value::String(json_cpu))
});
io.add_sync_method("disk_usage", move |_| {
info!("Fetching disk usage statistics.");
let disks = stats::disk_usage().map_err(JsonRpcServerError::Stats)?;
let json_disks = serde_json::to_string(&disks).map_err(JsonRpcServerError::Serde)?;
Ok(Value::String(json_disks))
});
io.add_sync_method("load_average", move |_| {
info!("Fetching system load average statistics.");
let avg = stats::load_average().map_err(JsonRpcServerError::Stats)?;
let json_avg = serde_json::to_string(&avg).map_err(JsonRpcServerError::Serde)?;
Ok(Value::String(json_avg))
});
io.add_sync_method("mem_stats", move |_| {
info!("Fetching current memory statistics.");
let mem = stats::mem_stats().map_err(JsonRpcServerError::Stats)?;
let json_mem = serde_json::to_string(&mem).map_err(JsonRpcServerError::Serde)?;
Ok(Value::String(json_mem))
});
io.add_sync_method("uptime", move |_| {
info!("Fetching system uptime.");
let uptime = stats::uptime().map_err(JsonRpcServerError::Stats)?;
let json_uptime = serde_json::to_string(&uptime).map_err(JsonRpcServerError::Serde)?;
Ok(Value::String(json_uptime))
});
let http_server =
env::var("PEACH_JSONRPC_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string());
info!("Starting JSON-RPC server on {}.", http_server);
let server = ServerBuilder::new(io)
.cors(DomainsValidation::AllowOnly(vec![
AccessControlAllowOrigin::Null,
]))
.start_http(
&http_server
.parse()
.expect("Invalid HTTP address and port combination"),
)
.expect("Unable to start RPC server");
server.wait();
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use jsonrpc_core::{Error as JsonRpcError, ErrorCode};
use jsonrpc_test as test_rpc;
#[test]
fn rpc_success() {
let rpc = {
let mut io = IoHandler::new();
io.add_sync_method("rpc_success_response", |_| {
Ok(Value::String("success".into()))
});
test_rpc::Rpc::from(io)
};
assert_eq!(rpc.request("rpc_success_response", &()), r#""success""#);
}
#[test]
fn rpc_parse_error() {
let rpc = {
let mut io = IoHandler::new();
io.add_sync_method("rpc_parse_error", |_| {
let e = JsonRpcError {
code: ErrorCode::ParseError,
message: String::from("Parse error"),
data: None,
};
Err(JsonRpcError::from(JsonRpcServerError::MissingParameter(e)))
});
test_rpc::Rpc::from(io)
};
assert_eq!(
rpc.request("rpc_parse_error", &()),
r#"{
"code": -32700,
"message": "Parse error"
}"#
);
}
}

View File

@ -0,0 +1,34 @@
#![warn(missing_docs)]
//! # peach-jsonrpc-server
//!
//! A JSON-RPC server which exposes an over HTTP.
//!
//! Currently includes peach-stats capability (system statistics).
//!
//! ## API
//!
//! | Method | Description | Returns |
//! | --- | --- | --- |
//! | `cpu_stats` | CPU statistics | `user`, `system`, `nice`, `idle` |
//! | `cpu_stats_percent` | CPU statistics as percentages | `user`, `system`, `nice`, `idle` |
//! | `disk_usage` | Disk usage statistics (array of disks) | `filesystem`, `one_k_blocks`, `one_k_blocks_used`, `one_k_blocks_free`, `used_percentage`, `mountpoint` |
//! | `load_average` | Load average statistics | `one`, `five`, `fifteen` |
//! | `mem_stats` | Memory statistics | `total`, `free`, `used` |
//! | `ping` | Microservice status | `success` if running |
//! | `uptime` | System uptime | `secs` |
use std::process;
use log::error;
fn main() {
// initalize the logger
env_logger::init();
// handle errors returned from `run`
if let Err(e) = peach_jsonrpc_server::run() {
error!("Application error: {}", e);
process::exit(1);
}
}

View File

@ -1,24 +1,24 @@
[package]
name = "peach-lib"
version = "1.2.15"
authors = ["Andrew Reid <gnomad@cryptolab.net>"]
version = "1.3.4"
authors = ["Andrew Reid <glyph@mycelial.technology>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
log = "0.4"
async-std = "1.10"
chrono = "0.4"
dirs = "4.0"
fslock="0.1"
golgi = { git = "https://git.coopcloud.tech/golgi-ssb/golgi.git" }
jsonrpc-client-core = "0.5"
jsonrpc-client-http = "0.5"
jsonrpc-core = "8.0.1"
jsonrpc-core = "8.0"
log = "0.4"
nanorand = { version = "0.6", features = ["getrandom"] }
regex = "1"
serde = { version = "1.0", features = ["derive"] }
rust-crypto = "0.2.36"
serde_derive = "1.0"
serde_json = "1.0"
serde_yaml = "0.8"
env_logger = "0.6"
snafu = "0.6"
regex = "1"
chrono = "0.4.19"
rand="0.8.4"
fslock="0.1.6"
toml = "0.5"
sha3 = "0.10"
lazy_static = "1.4"

View File

@ -1,4 +0,0 @@
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
objcopy = { path ="aarch64-linux-gnu-objcopy" }
strip = { path ="aarch64-linux-gnu-strip" }

View File

@ -1,12 +0,0 @@
[package]
name = "debug"
version = "0.1.0"
authors = ["notplants <mfowler.email@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
peach-lib = { path = "../" }
env_logger = "0.6"
chrono = "0.4.19"

View File

@ -1,65 +0,0 @@
use peach_lib::dyndns_client::{dyndns_update_ip, register_domain, is_dns_updater_online, log_successful_nsupdate, get_num_seconds_since_successful_dns_update };
use peach_lib::password_utils::{verify_password, set_new_password, verify_temporary_password, set_new_temporary_password, send_password_reset};
use peach_lib::config_manager::{add_ssb_admin_id, delete_ssb_admin_id};
use peach_lib::sbot_client;
use std::process;
use chrono::prelude::*;
fn main() {
// initalize the logger
env_logger::init();
//
// println!("Hello, world its debug!");
// let result = set_new_password("password3");
// println!("result: {:?}", result);
//
// let result = verify_password("password1");
// println!("result should be error: {:?}", result);
//
// let result = verify_password("password3");
// println!("result should be ok: {:?}", result);
//
//
// println!("Testing temporary passwords");
// let result = set_new_temporary_password("abcd");
// println!("result: {:?}", result);
//
// let result = verify_temporary_password("password1");
// println!("result should be error: {:?}", result);
//
// let result = verify_temporary_password("abcd");
// println!("result should be ok: {:?}", result);
//
let result = send_password_reset();
println!("send password reset result should be ok: {:?}", result);
// sbot_client::post("hi cat");
// let result = sbot_client::whoami();
// let result = sbot_client::create_invite(50);
// let result = sbot_client::post("is this working");
// println!("result: {:?}", result);
// let result = sbot_client::post("nice we have contact");
// let result = sbot_client::update_pub_name("vermont-pub");
// let result = sbot_client::private_message("this is a private message", "@LZx+HP6/fcjUm7vef2eaBKAQ9gAKfzmrMVGzzdJiQtA=.ed25519");
// println!("result: {:?}", result);
// let result = send_password_reset();
// let result = add_ssb_admin_id("xyzdab");
// println!("result: {:?}", result);
// let result = delete_ssb_admin_id("xyzdab");
// println!("result: {:?}", result);
// let result = delete_ssb_admin_id("ab");
// println!("result: {:?}", result);
//// let result = log_successful_nsupdate();
//// let result = get_num_seconds_since_successful_dns_update();
// let is_online = is_dns_updater_online();
// println!("is online: {:?}", is_online);
//
//// let result = get_last_successful_dns_update();
//// println!("result: {:?}", result);
//// register_domain("newquarter299.dyn.peachcloud.org");
// let result = dyndns_update_ip();
// println!("result: {:?}", result);
}

View File

@ -0,0 +1,11 @@
use peach_lib::config_manager::{get_config_value, save_config_value};
fn main() {
println!("Running example of PeachCloud configuration management");
let v = get_config_value("ADDR").unwrap();
println!("ADDR: {}", v);
save_config_value("ADDR", "1.1.1.1");
let v = get_config_value("ADDR").unwrap();
println!("ADDR: {}", v);
}

View File

@ -1,146 +1,280 @@
//! Interfaces for writing and reading PeachCloud configurations, stored in yaml.
//!
//! Different PeachCloud microservices import peach-lib, so that they can share this interface.
//! Different PeachCloud microservices import peach-lib, so that they can share
//! this interface.
//!
//! Config values are looked up from three locations in this order by key name:
//! 1. from environmental variables
//! 2. from a configuration file
//! 3. from default values
//!
//! The configuration file is located at: "/var/lib/peachcloud/config.yml"
//! unless its path is configured by setting PEACH_CONFIG_PATH env variable.
use fslock::{LockFile};
use serde::{Deserialize, Serialize};
use std::fs;
use std::collections::{BTreeMap, HashMap};
use std::{env, fs};
use fslock::LockFile;
use lazy_static::lazy_static;
use log::debug;
use crate::error::PeachError;
use crate::error::*;
// main configuration file
pub const YAML_PATH: &str = "/var/lib/peachcloud/config.yml";
// lock file (used to avoid race conditions during config reading & writing)
pub const LOCK_FILE_PATH: &str = "/var/lib/peachcloud/config.lock";
// we make use of Serde default values in order to make PeachCloud
// robust and keep running even with a not fully complete config.yml
// main type which represents all peachcloud configurations
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct PeachConfig {
#[serde(default)]
pub external_domain: String,
#[serde(default)]
pub dyn_domain: String,
#[serde(default)]
pub dyn_dns_server_address: String,
#[serde(default)]
pub dyn_tsig_key_path: String,
#[serde(default)] // default is false
pub dyn_enabled: bool,
#[serde(default)] // default is empty vector
pub ssb_admin_ids: Vec<String>,
#[serde(default)]
pub admin_password_hash: String,
#[serde(default)]
pub temporary_password_hash: String,
// load path to main configuration file
// from PEACH_CONFIG_PATH if that environment variable is set
// or using the default value if not set
pub const DEFAULT_YAML_PATH: &str = "/var/lib/peachcloud/config.yml";
lazy_static! {
static ref CONFIG_PATH: String = {
if let Ok(val) = env::var("PEACH_CONFIG_PATH") {
val
}
else {
DEFAULT_YAML_PATH.to_string()
}
};
// lock file (used to avoid race conditions during config reading & writing)
// the lock file path is the config file path + ".lock"
static ref LOCK_FILE_PATH: String = format!("{}.lock", *CONFIG_PATH);
}
// helper functions for serializing and deserializing PeachConfig from disc
fn save_peach_config(peach_config: PeachConfig) -> Result<PeachConfig, PeachError> {
// Default values for PeachCloud configs which are used for any key which is not set
// via an environment variable or in a saved configuration file.
pub fn get_peach_config_defaults() -> HashMap<String, String> {
let peach_config_defaults: HashMap<&str, &str> = HashMap::from([
("STANDALONE_MODE", "true"),
("DISABLE_AUTH", "false"),
("ADDR", "127.0.0.1"),
("PORT", "8000"),
("EXTERNAL_DOMAIN", ""),
("DYN_DOMAIN", ""),
(
"DYN_DNS_SERVER_ADDRESS",
"http://dynserver.dyn.peachcloud.org",
),
("DYN_USE_CUSTOM_SERVER", "true"),
("DYN_TSIG_KEY_PATH", ""),
("DYN_NAMESERVER", "ns.peachcloud.org"),
("DYN_ENABLED", "false"),
("SSB_ADMIN_IDS", ""),
("ADMIN_PASSWORD_HASH", "47"),
("TEMPORARY_PASSWORD_HASH", ""),
("GO_SBOT_DATADIR", "/home/peach/.ssb-go"),
("GO_SBOT_SERVICE", "go-sbot.service"),
("PEACH_CONFIGDIR", "/var/lib/peachcloud"),
("PEACH_HOMEDIR", "/home/peach"),
("PEACH_WEBDIR", "/usr/share/peach-web"),
]);
// convert HashMap<&str, &str> to HashMap<String, String> and return
let pc_defaults: HashMap<String, String> = peach_config_defaults
.iter()
.map(|(key, val)| (key.to_string(), val.to_string()))
.collect();
pc_defaults
}
// primary interface for getting config values
// Config values are looked up from three locations in this order by key name:
// 1. from environmental variables
// 2. from a configuration file
// 3. from default values
pub fn get_config_value(key: &str) -> Result<String, PeachError> {
// first check if there is an environmental variable set
if let Ok(val) = env::var(key) {
Ok(val)
} else {
// then check if a value is set in the config file
let peach_config_on_disc = load_peach_config_from_disc()?;
let val = peach_config_on_disc.get(key);
// if no value is found in the config file, then get the default value
match val {
// return config value
Some(v) => Ok(v.to_string()),
// get default value
None => {
match get_peach_config_defaults().get(key) {
Some(v) => Ok(v.to_string()),
// if this key was not found in the defaults, then it was an invalid key
None => Err(PeachError::InvalidKey {
key: key.to_string(),
}),
}
}
}
}
}
// helper function to load PeachCloud configuration file saved to disc
pub fn load_peach_config_from_disc() -> Result<HashMap<String, String>, PeachError> {
let peach_config_exists = std::path::Path::new(CONFIG_PATH.as_str()).exists();
// if config file does not exist, return an emtpy HashMap
if !peach_config_exists {
let peach_config: HashMap<String, String> = HashMap::new();
Ok(peach_config)
}
// otherwise we load peach config from disk
else {
debug!("Loading peach config: {} exists", CONFIG_PATH.as_str());
let contents =
fs::read_to_string(CONFIG_PATH.as_str()).map_err(|source| PeachError::Read {
source,
path: CONFIG_PATH.to_string(),
})?;
let peach_config: HashMap<String, String> = serde_yaml::from_str(&contents)?;
Ok(peach_config)
}
}
// helper function to save PeachCloud configuration file to disc
// takes in a Hashmap<String, String> and saves the whole HashMap as a yaml file
// with the keys in alphabetical order
pub fn save_peach_config_to_disc(
peach_config: HashMap<String, String>,
) -> Result<HashMap<String, String>, PeachError> {
// use a file lock to avoid race conditions while saving config
let mut lock = LockFile::open(LOCK_FILE_PATH)?;
let mut lock = LockFile::open(&*LOCK_FILE_PATH).map_err(|source| PeachError::Read {
source,
path: LOCK_FILE_PATH.to_string(),
})?;
lock.lock()?;
let yaml_str = serde_yaml::to_string(&peach_config)?;
// first convert Hashmap to BTreeMap (so that keys are saved in deterministic alphabetical order)
let ordered: BTreeMap<_, _> = peach_config.iter().collect();
// then serialize BTreeMap as yaml
let yaml_str = serde_yaml::to_string(&ordered)?;
fs::write(YAML_PATH, yaml_str).context(WriteConfigError {
file: YAML_PATH.to_string(),
// write yaml to file
fs::write(CONFIG_PATH.as_str(), yaml_str).map_err(|source| PeachError::Write {
source,
path: CONFIG_PATH.to_string(),
})?;
// unlock file lock
lock.unlock()?;
// return peach_config
// return modified HashMap
Ok(peach_config)
}
pub fn load_peach_config() -> Result<PeachConfig, PeachError> {
let peach_config_exists = std::path::Path::new(YAML_PATH).exists();
// helper functions for serializing and deserializing PeachConfig values from disc
pub fn save_config_value(key: &str, value: &str) -> Result<HashMap<String, String>, PeachError> {
// get current config from disc
let mut peach_config = load_peach_config_from_disc()?;
let peach_config: PeachConfig;
// insert new key/value
peach_config.insert(key.to_string(), value.to_string());
// if this is the first time loading peach_config, we can create a default here
if !peach_config_exists {
peach_config = PeachConfig {
external_domain: "".to_string(),
dyn_domain: "".to_string(),
dyn_dns_server_address: "".to_string(),
dyn_tsig_key_path: "".to_string(),
dyn_enabled: false,
ssb_admin_ids: Vec::new(),
admin_password_hash: "".to_string(),
temporary_password_hash: "".to_string(),
};
}
// otherwise we load peach config from disk
else {
let contents = fs::read_to_string(YAML_PATH).context(ReadConfigError {
file: YAML_PATH.to_string(),
})?;
peach_config = serde_yaml::from_str(&contents)?;
}
Ok(peach_config)
// save the modified hashmap to disc
save_peach_config_to_disc(peach_config)
}
// interfaces for setting specific config values
// set all dyn configuration values at once
pub fn set_peach_dyndns_config(
dyn_domain: &str,
dyn_dns_server_address: &str,
dyn_tsig_key_path: &str,
dyn_enabled: bool,
) -> Result<PeachConfig, PeachError> {
let mut peach_config = load_peach_config()?;
peach_config.dyn_domain = dyn_domain.to_string();
peach_config.dyn_dns_server_address = dyn_dns_server_address.to_string();
peach_config.dyn_tsig_key_path = dyn_tsig_key_path.to_string();
peach_config.dyn_enabled = dyn_enabled;
save_peach_config(peach_config)
) -> Result<HashMap<String, String>, PeachError> {
let mut peach_config = load_peach_config_from_disc()?;
let dyn_enabled_str = match dyn_enabled {
true => "true",
false => "false",
};
peach_config.insert("DYN_DOMAIN".to_string(), dyn_domain.to_string());
peach_config.insert(
"DYN_DNS_SERVER_ADDRESS".to_string(),
dyn_dns_server_address.to_string(),
);
peach_config.insert(
"DYN_TSIG_KEY_PATH".to_string(),
dyn_tsig_key_path.to_string(),
);
peach_config.insert("DYN_ENABLED".to_string(), dyn_enabled_str.to_string());
save_peach_config_to_disc(peach_config)
}
pub fn set_external_domain(new_external_domain: &str) -> Result<PeachConfig, PeachError> {
let mut peach_config = load_peach_config()?;
peach_config.external_domain = new_external_domain.to_string();
save_peach_config(peach_config)
pub fn set_external_domain(
new_external_domain: &str,
) -> Result<HashMap<String, String>, PeachError> {
save_config_value("EXTERNAL_DOMAIN", new_external_domain)
}
pub fn get_peachcloud_domain() -> Result<Option<String>, PeachError> {
let peach_config = load_peach_config()?;
if !peach_config.external_domain.is_empty() {
Ok(Some(peach_config.external_domain))
} else if !peach_config.dyn_domain.is_empty() {
Ok(Some(peach_config.dyn_domain))
let external_domain = get_config_value("EXTERNAL_DOMAIN")?;
let dyn_domain = get_config_value("DYN_DOMAIN")?;
if !external_domain.is_empty() {
Ok(Some(external_domain))
} else if !dyn_domain.is_empty() {
Ok(Some(dyn_domain))
} else {
Ok(None)
}
}
pub fn set_dyndns_enabled_value(enabled_value: bool) -> Result<PeachConfig, PeachError> {
let mut peach_config = load_peach_config()?;
peach_config.dyn_enabled = enabled_value;
save_peach_config(peach_config)
pub fn get_dyndns_server_address() -> Result<String, PeachError> {
get_config_value("DYN_DNS_SERVER_ADDRESS")
}
pub fn add_ssb_admin_id(ssb_id: &str) -> Result<PeachConfig, PeachError> {
let mut peach_config = load_peach_config()?;
peach_config.ssb_admin_ids.push(ssb_id.to_string());
save_peach_config(peach_config)
pub fn set_dyndns_enabled_value(
enabled_value: bool,
) -> Result<HashMap<String, String>, PeachError> {
match enabled_value {
true => save_config_value("DYN_ENABLED", "true"),
false => save_config_value("DYN_ENABLED", "false"),
}
}
pub fn delete_ssb_admin_id(ssb_id: &str) -> Result<PeachConfig, PeachError> {
let mut peach_config = load_peach_config()?;
let mut ssb_admin_ids = peach_config.ssb_admin_ids;
pub fn get_dyndns_enabled_value() -> Result<bool, PeachError> {
let val = get_config_value("DYN_ENABLED")?;
Ok(val == "true")
}
pub fn set_admin_password_hash(
password_hash: String,
) -> Result<HashMap<String, String>, PeachError> {
save_config_value("ADMIN_PASSWORD_HASH", &password_hash)
}
pub fn get_admin_password_hash() -> Result<String, PeachError> {
let admin_password_hash = get_config_value("ADMIN_PASSWORD_HASH")?;
if !admin_password_hash.is_empty() {
Ok(admin_password_hash)
} else {
Err(PeachError::PasswordNotSet)
}
}
pub fn set_temporary_password_hash(
password_hash: String,
) -> Result<HashMap<String, String>, PeachError> {
save_config_value("TEMPORARY_PASSWORD_HASH", &password_hash)
}
pub fn get_temporary_password_hash() -> Result<String, PeachError> {
let admin_password_hash = get_config_value("TEMPORARY_PASSWORD_HASH")?;
if !admin_password_hash.is_empty() {
Ok(admin_password_hash)
} else {
Err(PeachError::PasswordNotSet)
}
}
// add ssb_id to vector of admin ids and save new value for SSB_ADMIN_IDS
pub fn add_ssb_admin_id(ssb_id: &str) -> Result<Vec<String>, PeachError> {
let mut ssb_admin_ids = get_ssb_admin_ids()?;
ssb_admin_ids.push(ssb_id.to_string());
save_ssb_admin_ids(ssb_admin_ids)
}
// remove ssb_id from vector of admin ids if found and save new value for SSB_ADMIN_IDS
// if value is not found then return an error
pub fn delete_ssb_admin_id(ssb_id: &str) -> Result<Vec<String>, PeachError> {
let mut ssb_admin_ids = get_ssb_admin_ids()?;
let index_result = ssb_admin_ids.iter().position(|x| *x == ssb_id);
match index_result {
Some(index) => {
ssb_admin_ids.remove(index);
peach_config.ssb_admin_ids = ssb_admin_ids;
save_peach_config(peach_config)
save_ssb_admin_ids(ssb_admin_ids)
}
None => Err(PeachError::SsbAdminIdNotFound {
id: ssb_id.to_string(),
@ -148,32 +282,16 @@ pub fn delete_ssb_admin_id(ssb_id: &str) -> Result<PeachConfig, PeachError> {
}
}
pub fn set_admin_password_hash(password_hash: &str) -> Result<PeachConfig, PeachError> {
let mut peach_config = load_peach_config()?;
peach_config.admin_password_hash = password_hash.to_string();
save_peach_config(peach_config)
// looks up the String value for SSB_ADMIN_IDS and converts it into a Vec<String>
pub fn get_ssb_admin_ids() -> Result<Vec<String>, PeachError> {
let ssb_admin_ids_str = get_config_value("SSB_ADMIN_IDS")?;
let ssb_admin_ids: Vec<String> = serde_json::from_str(&ssb_admin_ids_str)?;
Ok(ssb_admin_ids)
}
pub fn get_admin_password_hash() -> Result<String, PeachError> {
let peach_config = load_peach_config()?;
if !peach_config.admin_password_hash.is_empty() {
Ok(peach_config.admin_password_hash)
} else {
Err(PeachError::PasswordNotSet)
}
// takes in a Vec<String> and saves SSB_ADMIN_IDS as a json string representation of this vec
pub fn save_ssb_admin_ids(ssb_admin_ids: Vec<String>) -> Result<Vec<String>, PeachError> {
let ssb_admin_ids_as_json_str = serde_json::to_string(&ssb_admin_ids)?;
save_config_value("SSB_ADMIN_IDS", &ssb_admin_ids_as_json_str)?;
Ok(ssb_admin_ids)
}
pub fn set_temporary_password_hash(password_hash: &str) -> Result<PeachConfig, PeachError> {
let mut peach_config = load_peach_config()?;
peach_config.temporary_password_hash = password_hash.to_string();
save_peach_config(peach_config)
}
pub fn get_temporary_password_hash() -> Result<String, PeachError> {
let peach_config = load_peach_config()?;
if !peach_config.temporary_password_hash.is_empty() {
Ok(peach_config.temporary_password_hash)
} else {
Err(PeachError::PasswordNotSet)
}
}

View File

@ -9,27 +9,21 @@
//!
//! The domain for dyndns updates is stored in /var/lib/peachcloud/config.yml
//! The tsig key for authenticating the updates is stored in /var/lib/peachcloud/peach-dyndns/tsig.key
use crate::config_manager::{load_peach_config, set_peach_dyndns_config};
use crate::error::PeachError;
use crate::error::{
ChronoParseError, DecodeNsUpdateOutputError, DecodePublicIpError, GetPublicIpError,
NsCommandError, SaveDynDnsResultError, SaveTsigKeyError,
};
use std::ffi::OsStr;
use std::{fs, fs::OpenOptions, io::Write, process::Command, str::FromStr};
use chrono::prelude::*;
use jsonrpc_client_core::{expand_params, jsonrpc_client};
use jsonrpc_client_http::HttpTransport;
use log::{debug, info};
use regex::Regex;
use snafu::ResultExt;
use std::fs;
use std::fs::OpenOptions;
use std::io::Write;
use std::process::{Command, Stdio};
use std::str::FromStr;
use std::str::ParseBoolError;
use crate::config_manager::{
get_config_value, get_dyndns_enabled_value, get_dyndns_server_address,
};
use crate::{config_manager, error::PeachError};
/// constants for dyndns configuration
pub const PEACH_DYNDNS_URL: &str = "http://dynserver.dyn.peachcloud.org";
pub const TSIG_KEY_PATH: &str = "/var/lib/peachcloud/peach-dyndns/tsig.key";
pub const PEACH_DYNDNS_CONFIG_PATH: &str = "/var/lib/peachcloud/peach-dyndns";
pub const DYNDNS_LOG_PATH: &str = "/var/lib/peachcloud/peach-dyndns/latest_result.log";
@ -37,20 +31,21 @@ pub const DYNDNS_LOG_PATH: &str = "/var/lib/peachcloud/peach-dyndns/latest_resul
/// helper function which saves dyndns TSIG key returned by peach-dyndns-server to /var/lib/peachcloud/peach-dyndns/tsig.key
pub fn save_dyndns_key(key: &str) -> Result<(), PeachError> {
// create directory if it doesn't exist
fs::create_dir_all(PEACH_DYNDNS_CONFIG_PATH).context(SaveTsigKeyError {
path: PEACH_DYNDNS_CONFIG_PATH.to_string(),
})?;
fs::create_dir_all(PEACH_DYNDNS_CONFIG_PATH)?;
//.context(SaveTsigKeyError {
//path: PEACH_DYNDNS_CONFIG_PATH.to_string(),
//})?;
// write key text
let mut file = OpenOptions::new()
.write(true)
.create(true)
.open(TSIG_KEY_PATH)
.context(SaveTsigKeyError {
path: TSIG_KEY_PATH.to_string(),
})?;
writeln!(file, "{}", key).context(SaveTsigKeyError {
// TODO: consider adding context msg
.open(TSIG_KEY_PATH)?;
writeln!(file, "{}", key).map_err(|source| PeachError::Write {
source,
path: TSIG_KEY_PATH.to_string(),
})?;
Ok(())
}
@ -61,30 +56,26 @@ pub fn save_dyndns_key(key: &str) -> Result<(), PeachError> {
pub fn register_domain(domain: &str) -> std::result::Result<String, PeachError> {
debug!("Creating HTTP transport for dyndns client.");
let transport = HttpTransport::new().standalone()?;
let http_server = PEACH_DYNDNS_URL;
debug!("Creating HTTP transport handle on {}.", http_server);
let transport_handle = transport.handle(http_server)?;
let http_server = get_dyndns_server_address()?;
info!("Using dyndns http server address: {:?}", http_server);
debug!("Creating HTTP transport handle on {}.", &http_server);
let transport_handle = transport.handle(&http_server)?;
info!("Creating client for peach-dyndns service.");
let mut client = PeachDynDnsClient::new(transport_handle);
info!("Performing register_domain call to peach-dyndns-server");
let res = client.register_domain(domain).call();
match res {
Ok(key) => {
// save new TSIG key
save_dyndns_key(&key)?;
// save new configuration values
let set_config_result =
set_peach_dyndns_config(domain, PEACH_DYNDNS_URL, TSIG_KEY_PATH, true);
match set_config_result {
Ok(_) => {
let response = "success".to_string();
Ok(response)
}
Err(err) => Err(err),
}
let key = client.register_domain(domain).call()?;
// save new TSIG key
save_dyndns_key(&key)?;
// save new configuration values
let set_config_result =
config_manager::set_peach_dyndns_config(domain, &http_server, TSIG_KEY_PATH, true);
match set_config_result {
Ok(_) => {
let response = "success".to_string();
Ok(response)
}
Err(err) => Err(PeachError::JsonRpcClientCore { source: err }),
Err(err) => Err(err),
}
}
@ -92,67 +83,54 @@ pub fn register_domain(domain: &str) -> std::result::Result<String, PeachError>
pub fn is_domain_available(domain: &str) -> std::result::Result<bool, PeachError> {
debug!("Creating HTTP transport for dyndns client.");
let transport = HttpTransport::new().standalone()?;
let http_server = PEACH_DYNDNS_URL;
debug!("Creating HTTP transport handle on {}.", http_server);
let transport_handle = transport.handle(http_server)?;
let http_server = get_dyndns_server_address()?;
debug!("Creating HTTP transport handle on {}.", &http_server);
let transport_handle = transport.handle(&http_server)?;
info!("Creating client for peach_network service.");
let mut client = PeachDynDnsClient::new(transport_handle);
info!("Performing register_domain call to peach-dyndns-server");
let res = client.is_domain_available(domain).call();
info!("res: {:?}", res);
match res {
Ok(result_str) => {
let result: Result<bool, ParseBoolError> = FromStr::from_str(&result_str);
match result {
Ok(result_bool) => Ok(result_bool),
Err(err) => Err(PeachError::PeachParseBoolError { source: err }),
}
}
Err(err) => Err(PeachError::JsonRpcClientCore { source: err }),
}
let domain_availability = client.is_domain_available(domain).call()?;
info!("Domain availability: {:?}", domain_availability);
// convert availability status to a bool
let available: bool = FromStr::from_str(&domain_availability)?;
Ok(available)
}
/// Helper function to get public ip address of PeachCloud device.
fn get_public_ip_address() -> Result<String, PeachError> {
// TODO: consider other ways to get public IP address
let output = Command::new("/usr/bin/curl")
.arg("ifconfig.me")
.output()
.context(GetPublicIpError)?;
let command_output = std::str::from_utf8(&output.stdout).context(DecodePublicIpError)?;
Ok(command_output.to_string())
let output = Command::new("curl").arg("ifconfig.me").output()?;
let command_output = String::from_utf8(output.stdout)?;
Ok(command_output)
}
/// Reads dyndns configurations from config.yml
/// and then uses nsupdate to update the IP address for the configured domain
pub fn dyndns_update_ip() -> Result<bool, PeachError> {
info!("Running dyndns_update_ip");
let peach_config = load_peach_config()?;
let dyn_tsig_key_path = get_config_value("DYN_TSIG_KEY_PATH")?;
let dyn_enabled = get_dyndns_enabled_value()?;
let dyn_domain = get_config_value("DYN_DOMAIN")?;
let dyn_dns_server_address = get_config_value("DYN_DNS_SERVER_ADDRESS")?;
let dyn_nameserver = get_config_value("DYN_NAMESERVER")?;
info!(
"Using config:
dyn_tsig_key_path: {:?}
dyn_domain: {:?}
dyn_dns_server_address: {:?}
dyn_enabled: {:?}
dyn_nameserver: {:?}
",
peach_config.dyn_tsig_key_path,
peach_config.dyn_domain,
peach_config.dyn_dns_server_address,
peach_config.dyn_enabled,
dyn_tsig_key_path, dyn_domain, dyn_dns_server_address, dyn_enabled, dyn_nameserver,
);
if !peach_config.dyn_enabled {
if !dyn_enabled {
info!("dyndns is not enabled, not updating");
Ok(false)
} else {
// call nsupdate passing appropriate configs
let nsupdate_command = Command::new("/usr/bin/nsupdate")
.arg("-k")
.arg(peach_config.dyn_tsig_key_path)
.arg("-v")
.stdin(Stdio::piped())
.spawn()
.context(NsCommandError)?;
let mut nsupdate_command = Command::new("nsupdate");
nsupdate_command.arg("-k").arg(&dyn_tsig_key_path).arg("-v");
// pass nsupdate commands via stdin
let public_ip_address = get_public_ip_address()?;
info!("found public ip address: {}", public_ip_address);
@ -163,16 +141,20 @@ pub fn dyndns_update_ip() -> Result<bool, PeachError> {
update delete {DOMAIN} A
update add {DOMAIN} 30 A {PUBLIC_IP_ADDRESS}
send",
NAMESERVER = "ns.peachcloud.org",
ZONE = peach_config.dyn_domain,
DOMAIN = peach_config.dyn_domain,
NAMESERVER = dyn_nameserver,
ZONE = dyn_domain,
DOMAIN = dyn_domain,
PUBLIC_IP_ADDRESS = public_ip_address,
);
write!(nsupdate_command.stdin.as_ref().unwrap(), "{}", ns_commands).unwrap();
let nsupdate_output = nsupdate_command
.wait_with_output()
.context(NsCommandError)?;
info!("output: {:?}", nsupdate_output);
info!("ns_commands: {:?}", ns_commands);
info!("creating nsupdate temp file");
let temp_file_path = "/var/lib/peachcloud/nsupdate.sh";
// write ns_commands to temp_file
fs::write(temp_file_path, ns_commands)?;
nsupdate_command.arg(temp_file_path);
let nsupdate_output = nsupdate_command.output()?;
let args: Vec<&OsStr> = nsupdate_command.get_args().collect();
info!("nsupdate command: {:?}", args);
// We only return a successful result if nsupdate was successful
if nsupdate_output.status.success() {
info!("nsupdate succeeded, returning ok");
@ -182,9 +164,8 @@ pub fn dyndns_update_ip() -> Result<bool, PeachError> {
Ok(true)
} else {
info!("nsupdate failed, returning error");
let err_msg =
String::from_utf8(nsupdate_output.stdout).context(DecodeNsUpdateOutputError)?;
Err(PeachError::NsUpdateError { msg: err_msg })
let err_msg = String::from_utf8(nsupdate_output.stdout)?;
Err(PeachError::NsUpdate { msg: err_msg })
}
}
}
@ -195,9 +176,12 @@ pub fn log_successful_nsupdate() -> Result<bool, PeachError> {
let mut file = OpenOptions::new()
.write(true)
.create(true)
.open(DYNDNS_LOG_PATH)
.context(SaveDynDnsResultError)?;
write!(file, "{}", now_timestamp).context(SaveDynDnsResultError)?;
// TODO: possibly add a context msg here ("failed to open dynamic dns success log")
.open(DYNDNS_LOG_PATH)?;
write!(file, "{}", now_timestamp).map_err(|source| PeachError::Write {
source,
path: DYNDNS_LOG_PATH.to_string(),
})?;
Ok(true)
}
@ -207,12 +191,19 @@ pub fn get_num_seconds_since_successful_dns_update() -> Result<Option<i64>, Peac
if !log_exists {
Ok(None)
} else {
let contents =
fs::read_to_string(DYNDNS_LOG_PATH).expect("Something went wrong reading the file");
let contents = fs::read_to_string(DYNDNS_LOG_PATH).map_err(|source| PeachError::Read {
source,
path: DYNDNS_LOG_PATH.to_string(),
})?;
// replace newline if found
let contents = contents.replace("\n", "");
let time_ran_dt = DateTime::parse_from_rfc3339(&contents).context(ChronoParseError {
msg: "Error parsing dyndns time from latest_result.log".to_string(),
// TODO: maybe we can use `.trim()` instead
let contents = contents.replace('\n', "");
// TODO: consider adding additional context?
let time_ran_dt = DateTime::parse_from_rfc3339(&contents).map_err(|source| {
PeachError::ParseDateTime {
source,
path: DYNDNS_LOG_PATH.to_string(),
}
})?;
let current_time: DateTime<Utc> = Utc::now();
let duration = current_time.signed_duration_since(time_ran_dt);
@ -225,20 +216,14 @@ pub fn get_num_seconds_since_successful_dns_update() -> Result<Option<i64>, Peac
/// and has successfully run recently (in the last six minutes)
pub fn is_dns_updater_online() -> Result<bool, PeachError> {
// first check if it is enabled in peach-config
let peach_config = load_peach_config()?;
let is_enabled = peach_config.dyn_enabled;
let is_enabled = get_dyndns_enabled_value()?;
// then check if it has successfully run within the last 6 minutes (60*6 seconds)
let num_seconds_since_successful_update = get_num_seconds_since_successful_dns_update()?;
let ran_recently: bool;
match num_seconds_since_successful_update {
Some(seconds) => {
ran_recently = seconds < (60 * 6);
}
let ran_recently: bool = match num_seconds_since_successful_update {
Some(seconds) => seconds < (60 * 6),
// if the value is None, then the last time it ran successfully is unknown
None => {
ran_recently = false;
}
}
None => false,
};
// debug log
info!("is_dyndns_enabled: {:?}", is_enabled);
info!("dyndns_ran_recently: {:?}", ran_recently);
@ -260,10 +245,9 @@ pub fn get_dyndns_subdomain(dyndns_full_domain: &str) -> Option<String> {
}
// helper function which checks if a dyndns domain is new
pub fn check_is_new_dyndns_domain(dyndns_full_domain: &str) -> bool {
let peach_config = load_peach_config().unwrap();
let previous_dyndns_domain = peach_config.dyn_domain;
dyndns_full_domain != previous_dyndns_domain
pub fn check_is_new_dyndns_domain(dyndns_full_domain: &str) -> Result<bool, PeachError> {
let previous_dyndns_domain = get_config_value("DYN_DOMAIN")?;
Ok(dyndns_full_domain != previous_dyndns_domain)
}
jsonrpc_client!(pub struct PeachDynDnsClient {

View File

@ -1,136 +1,270 @@
//! Basic error handling for the network, OLED, stats and dyndns JSON-RPC clients.
pub use snafu::ResultExt;
use snafu::Snafu;
use std::error;
pub type BoxError = Box<dyn error::Error>;
#![warn(missing_docs)]
#[derive(Debug, Snafu)]
#[snafu(visibility(pub(crate)))]
//! Error handling for various aspects of the PeachCloud system, including the network, OLED, stats and dyndns JSON-RPC clients, as well as the configuration manager, sbot client and password utilities.
use golgi::GolgiError;
use std::{io, str, string};
/// This type represents all possible errors that can occur when interacting with the PeachCloud library.
#[derive(Debug)]
pub enum PeachError {
#[snafu(display("{}", source))]
JsonRpcHttp { source: jsonrpc_client_http::Error },
#[snafu(display("{}", source))]
JsonRpcClientCore { source: jsonrpc_client_core::Error },
#[snafu(display("{}", source))]
Serde { source: serde_json::error::Error },
#[snafu(display("{}", source))]
PeachParseBoolError { source: std::str::ParseBoolError },
#[snafu(display("{}", source))]
SetConfigError { source: serde_yaml::Error },
#[snafu(display("Failed to read: {}", file))]
ReadConfigError {
source: std::io::Error,
file: String,
/// Represents looking up a Config value with a non-existent key
InvalidKey {
/// the key value which was invalid
key: String,
},
#[snafu(display("Failed to save: {}", file))]
WriteConfigError {
source: std::io::Error,
file: String,
},
#[snafu(display("Failed to save tsig key: {} {}", path, source))]
SaveTsigKeyError {
source: std::io::Error,
path: String,
},
#[snafu(display("{}", msg))]
NsUpdateError { msg: String },
#[snafu(display("Failed to run nsupdate: {}", source))]
NsCommandError { source: std::io::Error },
#[snafu(display("Failed to get public IP address: {}", source))]
GetPublicIpError { source: std::io::Error },
#[snafu(display("Failed to decode public ip: {}", source))]
DecodePublicIpError { source: std::str::Utf8Error },
#[snafu(display("Failed to decode nsupdate output: {}", source))]
DecodeNsUpdateOutputError { source: std::string::FromUtf8Error },
#[snafu(display("{}", source))]
YamlError { source: serde_yaml::Error },
#[snafu(display("{:?}", err))]
JsonRpcCore { err: jsonrpc_core::Error },
#[snafu(display("Error creating regex: {}", source))]
RegexError { source: regex::Error },
#[snafu(display("Failed to decode utf8: {}", source))]
FromUtf8Error { source: std::string::FromUtf8Error },
#[snafu(display("Encountered Utf8Error: {}", source))]
Utf8Error { source: std::str::Utf8Error },
#[snafu(display("Stdio error: {}: {}", msg, source))]
StdIoError { source: std::io::Error, msg: String },
#[snafu(display("Failed to parse time from {} {}", source, msg))]
ChronoParseError {
source: chrono::ParseError,
/// Represents a failure to determine the path of the user's home directory.
HomeDir,
/// Represents all other cases of `std::io::Error`.
Io(io::Error),
/// Represents a JSON-RPC core error returned from a JSON-RPC client.
JsonRpcClientCore(jsonrpc_client_core::Error),
/// Represents a JSON-RPC core error returned from a JSON-RPC server.
JsonRpcCore(jsonrpc_core::Error),
/// Represents a JSON-RPC HTTP error returned from a JSON-RPC client.
JsonRpcHttp(jsonrpc_client_http::Error),
/// Represents a failure to update the nameserver.
NsUpdate {
/// A message describing the context of the attempted nameserver update.
msg: String,
},
#[snafu(display("Failed to save dynamic dns success log: {}", source))]
SaveDynDnsResultError { source: std::io::Error },
#[snafu(display("New passwords do not match"))]
PasswordsDoNotMatch,
#[snafu(display("No admin password is set"))]
/// Represents a failure to parse a string slice to a boolean value.
ParseBool(str::ParseBoolError),
/// Represents a failure to parse a `DateTime`. Includes the error source and the file path
/// used in the parse attempt.
ParseDateTime {
/// The underlying source of the error.
source: chrono::ParseError,
/// The file path for the parse attempt.
path: String,
},
/// Represents the submission of an incorrect admin password.
PasswordIncorrect,
/// Represents the submission of two passwords which do not match.
PasswordMismatch,
/// Represents an unset admin password (empty password hash value) in the config file.
PasswordNotSet,
#[snafu(display("The supplied password was not correct"))]
InvalidPassword,
#[snafu(display("Error saving new password: {}", msg))]
FailedToSetNewPassword { msg: String },
#[snafu(display("Error calling sbotcli: {}", msg))]
SbotCliError { msg: String },
#[snafu(display("Error deleting ssb admin id, id not found"))]
SsbAdminIdNotFound { id: String },
/// Represents a failure to read from input. Includes the error source and the file path used
/// in the read attempt.
Read {
/// The underlying source of the error.
source: io::Error,
/// The file path for the read attempt.
path: String,
},
/// Represents a failure to parse or compile a regular expression.
Regex(regex::Error),
/// Represents a failure to successfully execute an sbot command (via golgi).
Sbot(String),
/// Represents a failure to serialize or deserialize JSON.
SerdeJson(serde_json::error::Error),
/// Represents a failure to deserialize TOML.
TomlDeser(toml::de::Error),
/// Represents a failure to serialize TOML.
TomlSer(toml::ser::Error),
/// Represents a failure to serialize or deserialize YAML.
SerdeYaml(serde_yaml::Error),
/// Represents a failure to find the given SSB ID in the config file.
SsbAdminIdNotFound {
/// An SSB ID (public key).
id: String,
},
/// Represents a failure to interpret a sequence of u8 as a string slice.
Utf8ToStr(str::Utf8Error),
/// Represents a failure to interpret a sequence of u8 as a String.
Utf8ToString(string::FromUtf8Error),
/// Represents a failure to write to output. Includes the error source and the file path used
/// in the write attempt.
Write {
/// The underlying source of the error.
source: io::Error,
/// The file path for the write attempt.
path: String,
},
/// Represents a Golgi error
Golgi(GolgiError),
}
impl From<jsonrpc_client_http::Error> for PeachError {
fn from(err: jsonrpc_client_http::Error) -> PeachError {
PeachError::JsonRpcHttp { source: err }
impl std::error::Error for PeachError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match *self {
PeachError::HomeDir => None,
PeachError::InvalidKey { .. } => None,
PeachError::Io(_) => None,
PeachError::JsonRpcClientCore(_) => None,
PeachError::JsonRpcCore(_) => None,
PeachError::JsonRpcHttp(_) => None,
PeachError::NsUpdate { .. } => None,
PeachError::ParseBool(_) => None,
PeachError::ParseDateTime { ref source, .. } => Some(source),
PeachError::PasswordIncorrect => None,
PeachError::PasswordMismatch => None,
PeachError::PasswordNotSet => None,
PeachError::Read { ref source, .. } => Some(source),
PeachError::Regex(_) => None,
PeachError::Sbot(_) => None,
PeachError::SerdeJson(_) => None,
PeachError::SerdeYaml(_) => None,
PeachError::SsbAdminIdNotFound { .. } => None,
PeachError::TomlDeser(_) => None,
PeachError::TomlSer(_) => None,
PeachError::Utf8ToStr(_) => None,
PeachError::Utf8ToString(_) => None,
PeachError::Write { ref source, .. } => Some(source),
PeachError::Golgi(_) => None,
}
}
}
impl From<jsonrpc_client_core::Error> for PeachError {
fn from(err: jsonrpc_client_core::Error) -> PeachError {
PeachError::JsonRpcClientCore { source: err }
}
}
impl From<serde_json::error::Error> for PeachError {
fn from(err: serde_json::error::Error) -> PeachError {
PeachError::Serde { source: err }
}
}
impl From<serde_yaml::Error> for PeachError {
fn from(err: serde_yaml::Error) -> PeachError {
PeachError::YamlError { source: err }
impl std::fmt::Display for PeachError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match *self {
PeachError::InvalidKey { ref key } => {
write!(f, "Invalid key in config lookup for key: {}", key)
}
PeachError::HomeDir => {
write!(
f,
"Unable to determine the path of the user's home directory"
)
}
PeachError::Io(ref err) => err.fmt(f),
PeachError::JsonRpcClientCore(ref err) => err.fmt(f),
PeachError::JsonRpcCore(ref err) => {
write!(f, "{:?}", err)
}
PeachError::JsonRpcHttp(ref err) => err.fmt(f),
PeachError::NsUpdate { ref msg } => {
write!(f, "Nameserver error: {}", msg)
}
PeachError::ParseBool(ref err) => err.fmt(f),
PeachError::ParseDateTime { ref path, .. } => {
write!(f, "Date/time parse error: {}", path)
}
PeachError::PasswordIncorrect => {
write!(f, "password is incorrect")
}
PeachError::PasswordMismatch => {
write!(f, "passwords do not match")
}
PeachError::PasswordNotSet => {
write!(f, "hash value in YAML configuration file is empty")
}
PeachError::Read { ref path, .. } => {
write!(f, "Read error: {}", path)
}
PeachError::Regex(ref err) => err.fmt(f),
PeachError::Sbot(ref msg) => {
write!(f, "Sbot error: {}", msg)
}
PeachError::SerdeJson(ref err) => err.fmt(f),
PeachError::SerdeYaml(ref err) => err.fmt(f),
PeachError::SsbAdminIdNotFound { ref id } => {
write!(f, "Config error: SSB admin ID `{}` not found", id)
}
PeachError::TomlDeser(ref err) => err.fmt(f),
PeachError::TomlSer(ref err) => err.fmt(f),
PeachError::Utf8ToStr(ref err) => err.fmt(f),
PeachError::Utf8ToString(ref err) => err.fmt(f),
PeachError::Write { ref path, .. } => {
write!(f, "Write error: {}", path)
}
PeachError::Golgi(ref err) => err.fmt(f),
}
}
}
impl From<std::io::Error> for PeachError {
fn from(err: std::io::Error) -> PeachError {
PeachError::StdIoError {
source: err,
msg: "".to_string(),
}
PeachError::Io(err)
}
}
impl From<jsonrpc_client_core::Error> for PeachError {
fn from(err: jsonrpc_client_core::Error) -> PeachError {
PeachError::JsonRpcClientCore(err)
}
}
impl From<jsonrpc_client_http::Error> for PeachError {
fn from(err: jsonrpc_client_http::Error) -> PeachError {
PeachError::JsonRpcHttp(err)
}
}
impl From<str::ParseBoolError> for PeachError {
fn from(err: str::ParseBoolError) -> PeachError {
PeachError::ParseBool(err)
}
}
impl From<regex::Error> for PeachError {
fn from(err: regex::Error) -> PeachError {
PeachError::RegexError { source: err }
PeachError::Regex(err)
}
}
impl From<std::string::FromUtf8Error> for PeachError {
fn from(err: std::string::FromUtf8Error) -> PeachError {
PeachError::FromUtf8Error { source: err }
impl From<serde_json::error::Error> for PeachError {
fn from(err: serde_json::error::Error) -> PeachError {
PeachError::SerdeJson(err)
}
}
impl From<std::str::Utf8Error> for PeachError {
fn from(err: std::str::Utf8Error) -> PeachError {
PeachError::Utf8Error { source: err }
impl From<serde_yaml::Error> for PeachError {
fn from(err: serde_yaml::Error) -> PeachError {
PeachError::SerdeYaml(err)
}
}
impl From<chrono::ParseError> for PeachError {
fn from(err: chrono::ParseError) -> PeachError {
PeachError::ChronoParseError {
source: err,
msg: "".to_string(),
}
impl From<toml::de::Error> for PeachError {
fn from(err: toml::de::Error) -> PeachError {
PeachError::TomlDeser(err)
}
}
impl From<toml::ser::Error> for PeachError {
fn from(err: toml::ser::Error) -> PeachError {
PeachError::TomlSer(err)
}
}
impl From<str::Utf8Error> for PeachError {
fn from(err: str::Utf8Error) -> PeachError {
PeachError::Utf8ToStr(err)
}
}
impl From<string::FromUtf8Error> for PeachError {
fn from(err: string::FromUtf8Error) -> PeachError {
PeachError::Utf8ToString(err)
}
}
impl From<GolgiError> for PeachError {
fn from(err: GolgiError) -> PeachError {
PeachError::Golgi(err)
}
}

View File

@ -1,14 +1,10 @@
// this is to ignore a clippy warning that suggests
// to replace code with the same code that is already there (possibly a bug)
#![allow(clippy::nonstandard_macro_braces)]
pub mod config_manager;
pub mod dyndns_client;
pub mod error;
pub mod network_client;
pub mod oled_client;
pub mod password_utils;
pub mod sbot_client;
pub mod sbot;
pub mod stats_client;
// re-export error types

View File

@ -9,9 +9,6 @@
//! Several helper methods are also included here which bundle multiple client
//! calls to achieve the desired functionality.
// TODO: fix these clippy errors so this allow can be removed
#![allow(clippy::needless_borrow)]
use std::env;
use jsonrpc_client_core::{expand_params, jsonrpc_client};
@ -166,9 +163,9 @@ pub fn disable(iface: &str, ssid: &str) -> std::result::Result<String, PeachErro
let mut client = PeachNetworkClient::new(transport_handle);
info!("Performing id call to peach-network microservice.");
let id = client.id(&iface, &ssid).call()?;
let id = client.id(iface, ssid).call()?;
info!("Performing disable call to peach-network microservice.");
client.disable(&id, &iface).call()?;
client.disable(&id, iface).call()?;
let response = "success".to_string();
@ -194,12 +191,12 @@ pub fn forget(iface: &str, ssid: &str) -> std::result::Result<String, PeachError
let mut client = PeachNetworkClient::new(transport_handle);
info!("Performing id call to peach-network microservice.");
let id = client.id(&iface, &ssid).call()?;
let id = client.id(iface, ssid).call()?;
info!("Performing delete call to peach-network microservice.");
// WEIRD BUG: the parameters below are technically in the wrong order:
// it should be id first and then iface, but somehow they get twisted.
// i don't understand computers.
client.delete(&iface, &id).call()?;
client.delete(iface, &id).call()?;
info!("Performing save call to peach-network microservice.");
client.save().call()?;
@ -357,8 +354,7 @@ pub fn saved_ap(ssid: &str) -> std::result::Result<bool, PeachError> {
// retrieve a list of access points with saved credentials
let saved_aps = match client.saved_networks().call() {
Ok(ssids) => {
let networks: Vec<Networks> = serde_json::from_str(ssids.as_str())
.expect("Failed to deserialize saved_networks response");
let networks: Vec<Networks> = serde_json::from_str(ssids.as_str())?;
networks
}
// return an empty vector if there are no saved access point credentials
@ -479,7 +475,7 @@ pub fn traffic(iface: &str) -> std::result::Result<Traffic, PeachError> {
let mut client = PeachNetworkClient::new(transport_handle);
let response = client.traffic(iface).call()?;
let t: Traffic = serde_json::from_str(&response).unwrap();
let t: Traffic = serde_json::from_str(&response)?;
Ok(t)
}
@ -506,13 +502,13 @@ pub fn update(iface: &str, ssid: &str, pass: &str) -> std::result::Result<String
// get the id of the network
info!("Performing id call to peach-network microservice.");
let id = client.id(&iface, &ssid).call()?;
let id = client.id(iface, ssid).call()?;
// delete the old credentials
// WEIRD BUG: the parameters below are technically in the wrong order:
// it should be id first and then iface, but somehow they get twisted.
// i don't understand computers.
info!("Performing delete call to peach-network microservice.");
client.delete(&iface, &id).call()?;
client.delete(iface, &id).call()?;
// save the updates to wpa_supplicant.conf
info!("Performing save call to peach-network microservice.");
client.save().call()?;

View File

@ -1,23 +1,20 @@
use crate::config_manager::{get_peachcloud_domain, load_peach_config,
set_admin_password_hash, get_admin_password_hash,
get_temporary_password_hash, set_temporary_password_hash};
use crate::error::PeachError;
use crate::sbot_client;
use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng};
use std::iter;
use crypto::digest::Digest;
use crypto::sha3::Sha3;
use async_std::task;
use golgi::{sbot::Keystore, Sbot};
use log::debug;
use nanorand::{Rng, WyRand};
use sha3::{Digest, Sha3_256};
use crate::{config_manager, error::PeachError, sbot::SbotConfig};
/// Returns Ok(()) if the supplied password is correct,
/// and returns Err if the supplied password is incorrect.
pub fn verify_password(password: &str) -> Result<(), PeachError> {
let real_admin_password_hash = get_admin_password_hash()?;
let password_hash = hash_password(&password.to_string());
let real_admin_password_hash = config_manager::get_admin_password_hash()?;
let password_hash = hash_password(password);
if real_admin_password_hash == password_hash {
Ok(())
} else {
Err(PeachError::InvalidPassword)
Err(PeachError::PasswordIncorrect)
}
}
@ -29,78 +26,67 @@ pub fn validate_new_passwords(new_password1: &str, new_password2: &str) -> Resul
if new_password1 == new_password2 {
Ok(())
} else {
Err(PeachError::PasswordsDoNotMatch)
Err(PeachError::PasswordMismatch)
}
}
/// Sets a new password for the admin user
pub fn set_new_password(new_password: &str) -> Result<(), PeachError> {
let new_password_hash = hash_password(&new_password.to_string());
let result = set_admin_password_hash(&new_password_hash);
match result {
Ok(_) => {
Ok(())
},
Err(_err) => {
Err(PeachError::FailedToSetNewPassword { msg: "failed to save password hash".to_string() })
}
}
let new_password_hash = hash_password(new_password);
config_manager::set_admin_password_hash(new_password_hash)?;
Ok(())
}
/// Creates a hash from a password string
pub fn hash_password(password: &str) -> String {
let mut hasher = Sha3::sha3_256();
hasher.input_str(password);
hasher.result_str()
let mut hasher = Sha3_256::new();
// write input message
hasher.update(password);
// read hash digest
let result = hasher.finalize();
// convert `u8` to `String`
result[0].to_string()
}
/// Sets a new temporary password for the admin user
/// which can be used to reset the permanent password
pub fn set_new_temporary_password(new_password: &str) -> Result<(), PeachError> {
let new_password_hash = hash_password(&new_password.to_string());
let result = set_temporary_password_hash(&new_password_hash);
match result {
Ok(_) => {
Ok(())
},
Err(_err) => {
Err(PeachError::FailedToSetNewPassword { msg: "failed to save temporary password hash".to_string() })
}
}
let new_password_hash = hash_password(new_password);
config_manager::set_temporary_password_hash(new_password_hash)?;
Ok(())
}
/// Returns Ok(()) if the supplied temp_password is correct,
/// and returns Err if the supplied temp_password is incorrect
pub fn verify_temporary_password(password: &str) -> Result<(), PeachError> {
let temporary_admin_password_hash = get_temporary_password_hash()?;
let password_hash = hash_password(&password.to_string());
let temporary_admin_password_hash = config_manager::get_temporary_password_hash()?;
let password_hash = hash_password(password);
if temporary_admin_password_hash == password_hash {
Ok(())
} else {
Err(PeachError::InvalidPassword)
Err(PeachError::PasswordIncorrect)
}
}
/// Generates a temporary password and sends it via ssb dm
/// to the ssb id configured to be the admin of the peachcloud device
pub fn send_password_reset() -> Result<(), PeachError> {
// first generate a new random password of ascii characters
let mut rng = thread_rng();
let temporary_password: String = iter::repeat(())
.map(|()| rng.sample(Alphanumeric))
.map(char::from)
.take(10)
.collect();
// initialise random number generator
let mut rng = WyRand::new();
// generate a new password of random numbers
let temporary_password = rng.generate::<u64>().to_string();
// save this string as a new temporary password
set_new_temporary_password(&temporary_password)?;
let domain = get_peachcloud_domain()?;
let domain = config_manager::get_peachcloud_domain()?;
// then send temporary password as a private ssb message to admin
let mut msg = format!(
"Your new temporary password is: {}
If you are on the same WiFi network as your PeachCloud device you can reset your password \
using this link: http://peach.local/reset_password",
using this link: http://peach.local/auth/reset",
temporary_password
);
// if there is an external domain, then include remote link in message
@ -109,7 +95,7 @@ using this link: http://peach.local/reset_password",
Some(domain) => {
format!(
"\n\nOr if you are on a different WiFi network, you can reset your password \
using the the following link: {}/reset_password",
using the the following link: {}/auth/reset",
domain
)
}
@ -117,9 +103,41 @@ using this link: http://peach.local/reset_password",
};
msg += &remote_link;
// finally send the message to the admins
let peach_config = load_peach_config()?;
for ssb_admin_id in peach_config.ssb_admin_ids {
sbot_client::private_message(&msg, &ssb_admin_id)?;
let ssb_admin_ids = config_manager::get_ssb_admin_ids()?;
for ssb_admin_id in ssb_admin_ids {
// use golgi to send a private message on scuttlebutt
match task::block_on(publish_private_msg(&msg, &ssb_admin_id)) {
Ok(_) => (),
Err(e) => return Err(PeachError::Sbot(e)),
}
}
Ok(())
}
async fn publish_private_msg(msg: &str, recipient: &str) -> Result<(), String> {
// retrieve latest go-sbot configuration parameters
let sbot_config = SbotConfig::read().ok();
let msg = msg.to_string();
let recipient = vec![recipient.to_string()];
// initialise sbot connection with ip:port and shscap from config file
let mut sbot_client = match sbot_config {
// TODO: panics if we pass `Some(conf.shscap)` as second arg
Some(conf) => {
let ip_port = conf.lis.clone();
Sbot::init(Keystore::GoSbot, Some(ip_port), None)
.await
.map_err(|e| e.to_string())?
}
None => Sbot::init(Keystore::GoSbot, None, None)
.await
.map_err(|e| e.to_string())?,
};
debug!("Publishing a Scuttlebutt private message with temporary password");
match sbot_client.publish_private(msg, recipient).await {
Ok(_) => Ok(()),
Err(e) => Err(format!("Failed to publish private message: {}", e)),
}
}

267
peach-lib/src/sbot.rs Normal file
View File

@ -0,0 +1,267 @@
//! Data types and associated methods for monitoring and configuring go-sbot.
use std::{fs, fs::File, io, io::Write, path::PathBuf, process::Command, str};
use golgi::{sbot::Keystore, Sbot};
use log::debug;
use crate::config_manager;
use serde::{Deserialize, Serialize};
use crate::error::PeachError;
/* HELPER FUNCTIONS */
// iterate over the given directory path to determine the size of the directory
fn dir_size(path: impl Into<PathBuf>) -> io::Result<u64> {
fn dir_size(mut dir: fs::ReadDir) -> io::Result<u64> {
dir.try_fold(0, |acc, file| {
let file = file?;
let size = match file.metadata()? {
data if data.is_dir() => dir_size(fs::read_dir(file.path())?)?,
data => data.len(),
};
Ok(acc + size)
})
}
dir_size(fs::read_dir(path.into())?)
}
/* SBOT-RELATED TYPES AND METHODS */
/// go-sbot process status.
#[derive(Debug, Serialize, Deserialize)]
pub struct SbotStatus {
/// Current process state.
pub state: Option<String>,
/// Current process boot state.
pub boot_state: Option<String>,
/// Current process memory usage in bytes.
pub memory: Option<u32>,
/// Uptime for the process (if state is `active`).
pub uptime: Option<String>,
/// Downtime for the process (if state is `inactive`).
pub downtime: Option<String>,
/// Size of the blobs directory in bytes.
pub blobstore: Option<u64>,
}
/// Default builder for `SbotStatus`.
impl Default for SbotStatus {
fn default() -> Self {
Self {
state: None,
boot_state: None,
memory: None,
uptime: None,
downtime: None,
blobstore: None,
}
}
}
impl SbotStatus {
/// Retrieve statistics for the go-sbot systemd process by querying `systemctl`.
pub fn read() -> Result<Self, PeachError> {
let mut status = SbotStatus::default();
// note this command does not need to be run as sudo
// because non-privileged users are able to run systemctl show
let info_output = Command::new("systemctl")
.arg("show")
.arg(config_manager::get_config_value("GO_SBOT_SERVICE")?)
.arg("--no-page")
.output()?;
let service_info = std::str::from_utf8(&info_output.stdout)?;
for line in service_info.lines() {
if line.starts_with("ActiveState=") {
if let Some(state) = line.strip_prefix("ActiveState=") {
status.state = Some(state.to_string())
}
} else if line.starts_with("MemoryCurrent=") {
if let Some(memory) = line.strip_prefix("MemoryCurrent=") {
status.memory = memory.parse().ok()
}
}
}
// note this command does not need to be run as sudo
// because non-privileged users are able to run systemctl status
let status_output = Command::new("systemctl")
.arg("status")
.arg(config_manager::get_config_value("GO_SBOT_SERVICE")?)
.output()?;
let service_status = str::from_utf8(&status_output.stdout)?;
//.map_err(PeachError::Utf8ToStr)?;
for line in service_status.lines() {
// example of the output line we're looking for:
// `Loaded: loaded (/home/glyph/.config/systemd/user/go-sbot.service; enabled; vendor
// preset: enabled)`
if line.contains("Loaded:") {
let before_boot_state = line.find(';');
let after_boot_state = line.rfind(';');
if let (Some(start), Some(end)) = (before_boot_state, after_boot_state) {
// extract the enabled / disabled from the `Loaded: ...` line
// using the index of the first ';' + 2 and the last ';'
status.boot_state = Some(line[start + 2..end].to_string());
}
// example of the output line we're looking for here:
// `Active: active (running) since Mon 2022-01-24 16:22:51 SAST; 4min 14s ago`
} else if line.contains("Active:") {
let before_time = line.find(';');
let after_time = line.find(" ago");
if let (Some(start), Some(end)) = (before_time, after_time) {
// extract the uptime / downtime from the `Active: ...` line
// using the index of ';' + 2 and the index of " ago"
let time = Some(&line[start + 2..end]);
// if service is active then the `time` reading is uptime
if status.state == Some("active".to_string()) {
status.uptime = time.map(|t| t.to_string())
// if service is inactive then the `time` reading is downtime
} else if status.state == Some("inactive".to_string()) {
status.downtime = time.map(|t| t.to_string())
}
}
}
}
// get path to blobstore
let blobstore_path = format!(
"{}/blobs/sha256",
config_manager::get_config_value("GO_SBOT_DATADIR")?
);
// determine the size of the blobstore directory in bytes
status.blobstore = dir_size(blobstore_path).ok();
Ok(status)
}
}
/// go-sbot configuration parameters.
#[derive(Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct SbotConfig {
// TODO: maybe define as a Path type?
/// Directory path for the log and indexes.
pub repo: String,
/// Directory path for writing debug output.
pub debugdir: String,
/// Secret-handshake app-key (aka. network key).
pub shscap: String,
/// HMAC hash used to sign messages.
pub hmac: String,
/// Replication hops (1: friends, 2: friends of friends).
pub hops: u8,
/// Address to listen on.
pub lis: String,
/// Address to listen on for WebSocket connections.
pub wslis: String,
/// Address to for metrics and pprof HTTP server.
pub debuglis: String,
/// Enable sending local UDP broadcasts.
pub localadv: bool,
/// Enable listening for UDP broadcasts and connecting.
pub localdiscov: bool,
/// Enable syncing by using epidemic-broadcast-trees (EBT).
#[serde(rename(serialize = "enable_ebt", deserialize = "enable-ebt"))]
pub enable_ebt: bool,
/// Bypass graph auth and fetch remote's feed (useful for pubs that are restoring their data
/// from peer; user beware - caveats about).
pub promisc: bool,
/// Disable the UNIX socket RPC interface.
pub nounixsock: bool,
/// Attempt to repair the filesystem before starting.
pub repair: bool,
}
/// Default configuration values for go-sbot.
impl Default for SbotConfig {
fn default() -> Self {
Self {
repo: ".ssb-go".to_string(),
debugdir: "".to_string(),
shscap: "1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s=".to_string(),
hmac: "".to_string(),
hops: 1,
lis: ":8008".to_string(),
wslis: ":8989".to_string(),
debuglis: "localhost:6078".to_string(),
localadv: false,
localdiscov: false,
enable_ebt: false,
promisc: false,
nounixsock: false,
repair: false,
}
}
}
impl SbotConfig {
/// Read the go-sbot `config.toml` file from file and deserialize into `SbotConfig`.
pub fn read() -> Result<Self, PeachError> {
// determine path of user's go-sbot config.toml
let config_path = format!(
"{}/config.toml",
config_manager::get_config_value("GO_SBOT_DATADIR")?
);
let config_contents = fs::read_to_string(config_path)?;
let config: SbotConfig = toml::from_str(&config_contents)?;
Ok(config)
}
/// Write the given `SbotConfig` to the go-sbot `config.toml` file.
pub fn write(config: SbotConfig) -> Result<(), PeachError> {
let repo_comment = "# For details about go-sbot configuration, please visit the repo: https://github.com/cryptoscope/ssb\n".to_string();
// convert the provided `SbotConfig` instance to a string
let config_string = toml::to_string(&config)?;
// determine path of user's go-sbot config.toml
let config_path = format!(
"{}/config.toml",
config_manager::get_config_value("GO_SBOT_DATADIR")?
);
// open config file for writing
let mut file = File::create(config_path)?;
// write the repo comment to file
write!(file, "{}", repo_comment)?;
// write the config string to file
write!(file, "{}", config_string)?;
Ok(())
}
}
/// Initialise an sbot client
pub async fn init_sbot() -> Result<Sbot, PeachError> {
// read sbot config from config.toml
let sbot_config = SbotConfig::read().ok();
debug!("Initialising an sbot client with configuration parameters");
// initialise sbot connection with ip:port and shscap from config file
let key_path = format!(
"{}/secret",
config_manager::get_config_value("GO_SBOT_DATADIR")?
);
let sbot_client = match sbot_config {
// TODO: panics if we pass `Some(conf.shscap)` as second arg
Some(conf) => {
let ip_port = conf.lis.clone();
Sbot::init(Keystore::CustomGoSbot(key_path), Some(ip_port), None).await?
}
None => Sbot::init(Keystore::CustomGoSbot(key_path), None, None).await?,
};
Ok(sbot_client)
}

View File

@ -1,109 +0,0 @@
//! Interfaces for monitoring and configuring go-sbot using sbotcli.
//!
use crate::error::PeachError;
use serde::{Deserialize, Serialize};
use std::process::Command;
pub fn is_sbot_online() -> Result<bool, PeachError> {
let output = Command::new("/usr/bin/systemctl")
.arg("status")
.arg("peach-go-sbot")
.output()?;
let status = output.status;
// returns true if the service had an exist status of 0 (is running)
let is_running = status.success();
Ok(is_running)
}
/// currently go-sbotcli determines where the working directory is
/// using the home directory of th user that invokes it
/// this could be changed to be supplied as CLI arg
/// but for now all sbotcli commands must first become peach-go-sbot before running
/// the sudoers file is configured to allow this to happen without a password
pub fn sbotcli_command() -> Command {
let mut command = Command::new("sudo");
command
.arg("-u")
.arg("peach-go-sbot")
.arg("/usr/bin/sbotcli");
command
}
pub fn post(msg: &str) -> Result<(), PeachError> {
let mut command = sbotcli_command();
let output = command.arg("publish").arg("post").arg(msg).output()?;
if output.status.success() {
Ok(())
} else {
let stderr = std::str::from_utf8(&output.stderr)?;
Err(PeachError::SbotCliError {
msg: format!("Error making ssb post: {}", stderr),
})
}
}
#[derive(Serialize, Deserialize)]
struct WhoAmIValue {
id: String,
}
pub fn whoami() -> Result<String, PeachError> {
let mut command = sbotcli_command();
let output = command.arg("call").arg("whoami").output()?;
let text_output = std::str::from_utf8(&output.stdout)?;
let value: WhoAmIValue = serde_json::from_str(text_output)?;
let id = value.id;
Ok(id)
}
pub fn create_invite(uses: i32) -> Result<String, PeachError> {
let mut command = sbotcli_command();
let output = command
.arg("invite")
.arg("create")
.arg("--uses")
.arg(uses.to_string())
.output()?;
let text_output = std::str::from_utf8(&output.stdout)?;
let output = text_output.replace("\n", "");
Ok(output)
}
pub fn update_pub_name(new_name: &str) -> Result<(), PeachError> {
let pub_ssb_id = whoami()?;
let mut command = sbotcli_command();
let output = command
.arg("publish")
.arg("about")
.arg("--name")
.arg(new_name)
.arg(pub_ssb_id)
.output()?;
if output.status.success() {
Ok(())
} else {
let stderr = std::str::from_utf8(&output.stderr)?;
Err(PeachError::SbotCliError {
msg: format!("Error updating pub name: {}", stderr),
})
}
}
pub fn private_message(msg: &str, recipient: &str) -> Result<(), PeachError> {
let mut command = sbotcli_command();
let output = command
.arg("publish")
.arg("post")
.arg("--recps")
.arg(recipient)
.arg(msg)
.output()?;
if output.status.success() {
Ok(())
} else {
let stderr = std::str::from_utf8(&output.stderr)?;
Err(PeachError::SbotCliError {
msg: format!("Error sending ssb private message: {}", stderr),
})
}
}

View File

@ -1,4 +0,0 @@
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
objcopy = { path ="aarch64-linux-gnu-objcopy" }
strip = { path ="aarch64-linux-gnu-strip" }

View File

@ -1,4 +0,0 @@
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
objcopy = { path ="aarch64-linux-gnu-objcopy" }
strip = { path ="aarch64-linux-gnu-strip" }

View File

@ -1,4 +0,0 @@
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
objcopy = { path ="aarch64-linux-gnu-objcopy" }
strip = { path ="aarch64-linux-gnu-strip" }

View File

@ -1,44 +1,32 @@
[package]
name = "peach-network"
version = "0.2.12"
authors = ["Andrew Reid <gnomad@cryptolab.net>"]
edition = "2018"
description = "Query and configure network interfaces using JSON-RPC over HTTP."
version = "0.5.0"
authors = ["Andrew Reid <glyph@mycelial.technology>"]
edition = "2021"
description = "Query and configure network interfaces."
homepage = "https://opencollective.com/peachcloud"
repository = "https://github.com/peachcloud/peach-network"
repository = "https://git.coopcloud.tech/PeachCloud/peach-workspace/src/branch/main/peach-network"
readme = "README.md"
license = "AGPL-3.0-only"
license = "LGPL-3.0-only"
publish = false
[package.metadata.deb]
depends = "$auto"
extended-description = """\
peach-network is a microservice to query and configure network interfaces \
using JSON-RPC over HTTP."""
maintainer-scripts="debian"
systemd-units = { unit-name = "peach-network" }
assets = [
["target/release/peach-network", "usr/bin/", "755"],
["README.md", "usr/share/doc/peach-network/README", "644"],
]
[badges]
travis-ci = { repository = "peachcloud/peach-network", branch = "master" }
maintenance = { status = "actively-developed" }
[dependencies]
env_logger = "0.6"
failure = "0.1"
get_if_addrs = "0.5.3"
jsonrpc-core = "11"
jsonrpc-http-server = "11"
log = "0.4"
probes = "0.4"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
snafu = "0.6"
miniserde = { version = "0.1.15", optional = true }
probes = "0.4.1"
serde = { version = "1.0.130", features = ["derive"], optional = true }
regex = "1"
wpactrl = "0.3.1"
# replace this with crate import once latest changes have been published
wpactrl = { git = "https://github.com/sauyon/wpa-ctrl-rs.git", branch = "master" }
[dev-dependencies]
jsonrpc-test = "11"
[features]
default = []
# Provide `Serialize` and `Deserialize` traits for library structs using `miniserde`
miniserde_support = ["miniserde"]
# Provide `Serialize` and `Deserialize` traits for library structs using `serde`
serde_support = ["serde"]

View File

@ -1,178 +1,46 @@
# peach-network
[![Build Status](https://travis-ci.com/peachcloud/peach-network.svg?branch=master)](https://travis-ci.com/peachcloud/peach-network) ![Generic badge](https://img.shields.io/badge/version-0.2.12-<COLOR>.svg)
![Generic badge](https://img.shields.io/badge/version-0.4.2-<COLOR>.svg)
Networking microservice module for PeachCloud. Query and configure device interfaces using [JSON-RPC](https://www.jsonrpc.org/specification) over http.
Network interface state query and modification library.
Interaction with wireless interfaces occurs primarily through the [wpactrl crate](https://docs.rs/wpactrl/0.3.1/wpactrl/) which provides "a pure-Rust lowlevel library for controlling wpasupplicant remotely". This approach is akin to using `wpa_cli` (a WPA command line client).
_Note: This module is a work-in-progress._
## API Documentation
### JSON-RPC API
API documentation can be built and served with `cargo doc --no-deps --open`. The full set of available data structures and functions is listed in the `peach_network::network` module. A custom error type (`NetworkError`) is also publically exposed for library users; it encapsulates all possible error variants.
Methods for **retrieving data**:
## Example Usage
| Method | Parameters | Description |
| --- | --- | --- |
| `available_networks` | `iface` | List SSID, flags (security), frequency and signal level for all networks in range of given interface |
| `id` | `iface`, `ssid` | Return ID of given SSID |
| `ip` | `iface` | Return IP of given network interface |
| `ping` | | Respond with `success` if microservice is running |
| `rssi` | `iface` | Return average signal strength (dBm) for given interface |
| `rssi_percent` | `iface` | Return average signal strength (%) for given interface |
| `saved_networks` | | List all networks saved in wpasupplicant config |
| `ssid` | `iface` | Return SSID of currently-connected network for given interface |
| `state` | `iface` | Return state of given interface |
| `status` | `iface` | Return status parameters for given interface |
| `traffic` | `iface` | Return network traffic for given interface |
```rust
use peach_network::{network, NetworkError};
Methods for **modifying state**:
fn main() -> Result<(), NetworkError> {
let wlan_iface = "wlan0";
| Method | Parameters | Description |
| --- | --- | --- |
| `activate_ap` | | Activate WiFi access point (start `wpa_supplicant@ap0.service`) |
| `activate_client` | | Activate WiFi client connection (start `wpa_supplicant@wlan0.service`) |
| `add` | `ssid`, `pass` | Add WiFi credentials to `wpa_supplicant-wlan0.conf` |
| `check_iface` | | Activate WiFi access point if client mode is active without a connection |
| `connect` | `id`, `iface` | Disable other networks and attempt connection with AP represented by given id |
| `delete` | `id`, `iface` | Remove WiFi credentials for given network id and interface |
| `disable` | `id`, `iface` | Disable connection with AP represented by given id |
| `disconnect` | `iface` | Disconnect given interface |
| `modify` | `id`, `iface`, `password` | Set a new password for given network id and interface |
| `reassociate` | `iface` | Reassociate with current AP for given interface |
| `reconfigure` | | Force wpa_supplicant to re-read its configuration file |
| `reconnect` | `iface` | Disconnect and reconnect given interface |
| `save` | | Save configuration changes to `wpa_supplicant-wlan0.conf` |
let wlan_ip = network::ip(wlan_iface)?;
let wlan_ssid = network::ssid(wlan_iface)?;
### API Documentation
let ssid = "Home";
let pass = "SuperSecret";
API documentation can be built and served with `cargo doc --no-deps --open`. This set of documentation is intended for developers who wish to work on the project or better understand the API of the `src/network.rs` module.
network::add(&wlan_iface, &ssid, &pass)?;
network::save()?;
### Environment
Ok(())
}
```
The JSON-RPC HTTP server address and port can be configured with the `PEACH_NETWORK_SERVER` environment variable:
## Feature Flags
`export PEACH_NETWORK_SERVER=127.0.0.1:5000`
Feature flags are used to offer `Serialize` and `Deserialize` implementations for all `struct` data types provided by this library. These traits are not provided by default. A choice of `miniserde` and `serde` is provided.
When not set, the value defaults to `127.0.0.1:5110`.
Define the desired feature in the `Cargo.toml` manifest of your project:
Logging is made available with `env_logger`:
```toml
peach-network = { version = "0.3.0", features = ["miniserde_support"] }
```
`export RUST_LOG=info`
## License
Other logging levels include `debug`, `warn` and `error`.
### Setup
Clone this repo:
`git clone https://github.com/peachcloud/peach-network.git`
Move into the repo and compile:
`cd peach-network`
`cargo build --release`
Run the binary (sudo needed to satisfy permission requirements):
`sudo ./target/release/peach-network`
### Debian Packaging
A `systemd` service file and Debian maintainer scripts are included in the `debian` directory, allowing `peach-network` to be easily bundled as a Debian package (`.deb`). The `cargo-deb` [crate](https://crates.io/crates/cargo-deb) can be used to achieve this.
Install `cargo-deb`:
`cargo install cargo-deb`
Move into the repo:
`cd peach-network`
Build the package:
`cargo deb`
The output will be written to `target/debian/peach-network_0.2.4_arm64.deb` (or similar).
Build the package (aarch64):
`cargo deb --target aarch64-unknown-linux-gnu`
Install the package as follows:
`sudo dpkg -i target/debian/peach-network_0.2.4_arm64.deb`
The service will be automatically enabled and started.
Uninstall the service:
`sudo apt-get remove peach-network`
Remove configuration files (not removed with `apt-get remove`):
`sudo apt-get purge peach-network`
### Example Usage
**Retrieve IP address for wlan0**
With microservice running, open a second terminal window and use `curl` to call server methods:
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "ip", "params" : {"iface": "wlan0" }, "id":1 }' 127.0.0.1:5110`
Server responds with:
`{"jsonrpc":"2.0","result":"192.168.1.21","id":1}`
**Retrieve SSID of connected access point for wlan1**
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "ssid", "params" : {"iface": "wlan1" }, "id":1 }' 127.0.0.1:5110`
Server response when interface is connected:
`{"jsonrpc":"2.0","result":"Home","id":1}`
Server response when interface is not connected:
`{"jsonrpc":"2.0","error":{"code":-32003,"message":"Failed to retrieve SSID for wlan1. Interface may not be connected."},"id":1}`
**Retrieve list of SSIDs for all networks in range of wlan0**
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "available_networks", "params" : {"iface": "wlan0" }, "id":1 }' 127.0.0.1:5110`
Server response when interface is connected:
`{"jsonrpc":"2.0","result":"[{\"frequency\":\"2412\",\"signal_level\":\"-72\",\"ssid\":\"Home\",\"flags\":\"[WPA2-PSK-CCMP][ESS]\"},{\"frequency\":\"2472\",\"signal_level\":\"-56\",\"ssid\":\"podetium\",\"flags\":\"[WPA2-PSK-CCMP+TKIP][ESS]\"}]","id":1}`
Server response when interface is not connected:
`{"jsonrpc":"2.0","error":{"code":-32006,"message":"No networks found in range of wlan0"},"id":1}`
**Retrieve network traffic statistics for wlan1**
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "traffic", "params" : {"iface": "wlan1" }, "id":1 }' 127.0.0.1:5110`
Server response if interface exists:
`{"jsonrpc":"2.0","result":"{\"received\":26396361,\"transmitted\":22352530}","id":1}`
Server response when interface is not found:
`{"jsonrpc":"2.0","error":{"code":-32004,"message":"Failed to retrieve network traffic for wlan3. Interface may not be connected"},"id":1}`
**Retrieve status information for wlan0**
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "status", "params" : {"iface": "wlan0" }, "id":1 }' 127.0.0.1:5110`
Server response if interface exists:
`{"jsonrpc":"2.0","result":"{\"address\":\"b8:27:eb:9b:5d:5f\",\"bssid\":\"f4:8c:eb:cd:31:81\",\"freq\":\"2412\",\"group_cipher\":\"CCMP\",\"id\":\"0\",\"ip_address\":\"192.168.0.162\",\"key_mgmt\":\"WPA2-PSK\",\"mode\":\"station\",\"pairwise_cipher\":\"CCMP\",\"ssid\":\"Home\",\"wpa_state\":\"COMPLETED\"}","id":1}`
Server response when interface is not found:
`{"jsonrpc":"2.0","error":{"code":-32013,"message":"Failed to open control interface for wpasupplicant: No such file or directory (os error 2)"},"id":1}`
### Licensing
AGPL-3.0
LGPL-3.0.

View File

@ -1,13 +0,0 @@
[Unit]
Description=Query and configure network interfaces using JSON-RPC over HTTP.
[Service]
Type=simple
User=root
Group=netdev
Environment="RUST_LOG=error"
ExecStart=/usr/bin/peach-network
Restart=always
[Install]
WantedBy=multi-user.target

View File

@ -1,351 +1,367 @@
use std::{error, io, str};
//! Custom error type for `peach-network`.
use jsonrpc_core::{types::error::Error, ErrorCode};
use std::io;
use std::num::ParseIntError;
use io::Error as IoError;
use probes::ProbeError;
use serde_json::error::Error as SerdeError;
use snafu::Snafu;
use regex::Error as RegexError;
use wpactrl::Error as WpaError;
pub type BoxError = Box<dyn error::Error>;
#[derive(Debug, Snafu)]
#[snafu(visibility(pub(crate)))]
/// Custom error type encapsulating all possible errors when querying
/// network interfaces and modifying their state.
#[derive(Debug)]
pub enum NetworkError {
#[snafu(display("{}", err_msg))]
ActivateAp { err_msg: String },
#[snafu(display("{}", err_msg))]
ActivateClient { err_msg: String },
#[snafu(display("Failed to add network for {}", ssid))]
Add { ssid: String },
#[snafu(display("Failed to retrieve state for interface: {}", iface))]
NoState { iface: String, source: io::Error },
#[snafu(display("Failed to disable network {} for interface: {}", id, iface))]
Disable { id: String, iface: String },
#[snafu(display("Failed to disconnect {}", iface))]
Disconnect { iface: String },
#[snafu(display("Failed to generate wpa passphrase for {}: {}", ssid, source))]
GenWpaPassphrase { ssid: String, source: io::Error },
#[snafu(display("Failed to generate wpa passphrase for {}: {}", ssid, err_msg))]
GenWpaPassphraseWarning { ssid: String, err_msg: String },
#[snafu(display("No ID found for {} on interface: {}", ssid, iface))]
Id { ssid: String, iface: String },
#[snafu(display("Could not access IP address for interface: {}", iface))]
NoIp { iface: String, source: io::Error },
#[snafu(display("Could not find RSSI for interface: {}", iface))]
Rssi { iface: String },
#[snafu(display("Could not find signal quality (%) for interface: {}", iface))]
RssiPercent { iface: String },
#[snafu(display("Could not find SSID for interface: {}", iface))]
Ssid { iface: String },
#[snafu(display("No state found for interface: {}", iface))]
State { iface: String },
#[snafu(display("No status found for interface: {}", iface))]
Status { iface: String },
#[snafu(display("Could not find network traffic for interface: {}", iface))]
Traffic { iface: String },
#[snafu(display("No saved networks found for default interface"))]
/// Failed to add network.
Add {
/// SSID.
ssid: String,
},
/// Failed to retrieve network state.
NoState {
/// Interface.
iface: String,
/// Underlying error source.
source: IoError,
},
/// Failed to disable network.
Disable {
/// ID.
id: String,
/// Interface.
iface: String,
},
/// Failed to disconnect interface.
Disconnect {
/// Interface.
iface: String,
},
/// Failed to execute wpa_passphrase command.
GenWpaPassphrase {
/// SSID.
ssid: String,
/// Underlying error source.
source: IoError,
},
/// Failed to successfully generate wpa passphrase.
GenWpaPassphraseWarning {
/// SSID.
ssid: String,
/// Error message describing context.
err_msg: String,
},
/// Failed to retrieve ID for the given SSID and interface.
Id {
/// SSID.
ssid: String,
/// Interface.
iface: String,
},
/// Failed to retrieve IP address.
NoIp {
/// Inteface.
iface: String,
/// Underlying error source.
source: IoError,
},
/// Failed to retrieve RSSI.
Rssi {
/// Interface.
iface: String,
},
/// Failed to retrieve signal quality (%).
RssiPercent {
/// Interface.
iface: String,
},
/// Failed to retrieve SSID.
Ssid {
/// Interface.
iface: String,
},
/// Failed to retrieve state.
State {
/// Interface.
iface: String,
},
/// Failed to retrieve status.
Status {
/// Interface.
iface: String,
},
/// Failed to retieve network traffic.
Traffic {
/// Interface.
iface: String,
},
/// No saved network found for the default interface.
SavedNetworks,
#[snafu(display("No networks found in range of interface: {}", iface))]
AvailableNetworks { iface: String },
#[snafu(display("Missing expected parameters: {}", e))]
MissingParams { e: Error },
#[snafu(display("Failed to set new password for network {} on {}", id, iface))]
Modify { id: String, iface: String },
#[snafu(display("No IP found for interface: {}", iface))]
Ip { iface: String },
#[snafu(display("Failed to parse integer from string for RSSI value: {}", source))]
ParseString { source: std::num::ParseIntError },
#[snafu(display(
"Failed to retrieve network traffic measurement for {}: {}",
iface,
source
))]
NoTraffic { iface: String, source: ProbeError },
#[snafu(display("Failed to reassociate with WiFi network for interface: {}", iface))]
Reassociate { iface: String },
#[snafu(display("Failed to force reread of wpa_supplicant configuration file"))]
/// No networks found in range.
AvailableNetworks {
/// Interface.
iface: String,
},
/// Failed to set new password.
Modify {
/// ID.
id: String,
/// Interface.
iface: String,
},
/// Failed to retrieve IP address.
Ip {
/// Interface.
iface: String,
},
/// Failed to parse integer from string.
ParseInt(ParseIntError),
/// Failed to retrieve network traffic measurement.
NoTraffic {
/// Interface.
iface: String,
/// Underlying error source.
source: ProbeError,
},
/// Failed to reassociate with WiFi network.
Reassociate {
/// Interface.
iface: String,
},
/// Failed to force reread of wpa_supplicant configuration file.
Reconfigure,
#[snafu(display("Failed to reconnect with WiFi network for interface: {}", iface))]
Reconnect { iface: String },
#[snafu(display("Regex command failed"))]
Regex { source: regex::Error },
#[snafu(display("Failed to delete network {} for interface: {}", id, iface))]
Delete { id: String, iface: String },
#[snafu(display("Failed to retrieve state of wlan0 service: {}", source))]
WlanState { source: io::Error },
#[snafu(display("Failed to retrieve connection state of wlan0 interface: {}", source))]
WlanOperstate { source: io::Error },
#[snafu(display("Failed to save configuration changes to file"))]
Save,
#[snafu(display("Failed to connect to network {} for interface: {}", id, iface))]
Connect { id: String, iface: String },
#[snafu(display("Failed to start ap0 service: {}", source))]
StartAp0 { source: io::Error },
#[snafu(display("Failed to start wlan0 service: {}", source))]
StartWlan0 { source: io::Error },
#[snafu(display("JSON serialization failed: {}", source))]
SerdeSerialize { source: SerdeError },
#[snafu(display("Failed to open control interface for wpasupplicant"))]
WpaCtrlOpen {
#[snafu(source(from(failure::Error, std::convert::Into::into)))]
source: BoxError,
/// Failed to reconnect with WiFi network.
Reconnect {
/// Interface.
iface: String,
},
#[snafu(display("Request to wpasupplicant via wpactrl failed"))]
WpaCtrlRequest {
#[snafu(source(from(failure::Error, std::convert::Into::into)))]
source: BoxError,
/// Failed to execute Regex command.
Regex(RegexError),
/// Failed to delete network.
Delete {
/// ID.
id: String,
/// Interface.
iface: String,
},
/// Failed to retrieve state of wlan0 service.
WlanState(IoError),
/// Failed to retrieve connection state of wlan0 interface.
WlanOperstate(IoError),
/// Failed to save wpa_supplicant configuration changes to file.
Save(IoError),
/// Failed to connect to network.
Connect {
/// ID.
id: String,
/// Interface.
iface: String,
},
/// Failed to start systemctl service for a network interface.
StartInterface {
/// Underlying error source.
source: IoError,
/// Interface.
iface: String,
},
/// Failed to execute wpa-ctrl command.
WpaCtrl(WpaError),
}
impl From<NetworkError> for Error {
fn from(err: NetworkError) -> Self {
match &err {
NetworkError::ActivateAp { err_msg } => Error {
code: ErrorCode::ServerError(-32015),
message: err_msg.to_string(),
data: None,
},
NetworkError::ActivateClient { err_msg } => Error {
code: ErrorCode::ServerError(-32017),
message: err_msg.to_string(),
data: None,
},
NetworkError::Add { ssid } => Error {
code: ErrorCode::ServerError(-32000),
message: format!("Failed to add network for {}", ssid),
data: None,
},
NetworkError::NoState { iface, source } => Error {
code: ErrorCode::ServerError(-32022),
message: format!(
"Failed to retrieve interface state for {}: {}",
iface, source
),
data: None,
},
NetworkError::Disable { id, iface } => Error {
code: ErrorCode::ServerError(-32029),
message: format!("Failed to disable network {} for {}", id, iface),
data: None,
},
NetworkError::Disconnect { iface } => Error {
code: ErrorCode::ServerError(-32032),
message: format!("Failed to disconnect {}", iface),
data: None,
},
NetworkError::GenWpaPassphrase { ssid, source } => Error {
code: ErrorCode::ServerError(-32025),
message: format!("Failed to generate wpa passphrase for {}: {}", ssid, source),
data: None,
},
NetworkError::GenWpaPassphraseWarning { ssid, err_msg } => Error {
code: ErrorCode::ServerError(-32036),
message: format!(
"Failed to generate wpa passphrase for {}: {}",
ssid, err_msg
),
data: None,
},
NetworkError::Id { iface, ssid } => Error {
code: ErrorCode::ServerError(-32026),
message: format!("No ID found for {} on interface {}", ssid, iface),
data: None,
},
NetworkError::NoIp { iface, source } => Error {
code: ErrorCode::ServerError(-32001),
message: format!("Failed to retrieve IP address for {}: {}", iface, source),
data: None,
},
NetworkError::Rssi { iface } => Error {
code: ErrorCode::ServerError(-32002),
message: format!(
"Failed to retrieve RSSI for {}. Interface may not be connected",
iface
),
data: None,
},
NetworkError::RssiPercent { iface } => Error {
code: ErrorCode::ServerError(-32034),
message: format!(
"Failed to retrieve signal quality (%) for {}. Interface may not be connected",
iface
),
data: None,
},
NetworkError::Ssid { iface } => Error {
code: ErrorCode::ServerError(-32003),
message: format!(
"Failed to retrieve SSID for {}. Interface may not be connected",
iface
),
data: None,
},
NetworkError::State { iface } => Error {
code: ErrorCode::ServerError(-32023),
message: format!("No state found for {}. Interface may not exist", iface),
data: None,
},
NetworkError::Status { iface } => Error {
code: ErrorCode::ServerError(-32024),
message: format!("No status found for {}. Interface may not exist", iface),
data: None,
},
NetworkError::Traffic { iface } => Error {
code: ErrorCode::ServerError(-32004),
message: format!(
"No network traffic statistics found for {}. Interface may not exist",
iface
),
data: None,
},
NetworkError::SavedNetworks => Error {
code: ErrorCode::ServerError(-32005),
message: "No saved networks found".to_string(),
data: None,
},
NetworkError::AvailableNetworks { iface } => Error {
code: ErrorCode::ServerError(-32006),
message: format!("No networks found in range of {}", iface),
data: None,
},
NetworkError::MissingParams { e } => e.clone(),
NetworkError::Modify { id, iface } => Error {
code: ErrorCode::ServerError(-32033),
message: format!("Failed to set new password for network {} on {}", id, iface),
data: None,
},
NetworkError::Ip { iface } => Error {
code: ErrorCode::ServerError(-32007),
message: format!("No IP address found for {}", iface),
data: None,
},
NetworkError::ParseString { source } => Error {
code: ErrorCode::ServerError(-32035),
message: format!(
"Failed to parse integer from string for RSSI value: {}",
source
),
data: None,
},
NetworkError::NoTraffic { iface, source } => Error {
code: ErrorCode::ServerError(-32015),
message: format!(
"Failed to retrieve network traffic statistics for {}: {}",
iface, source
),
data: None,
},
NetworkError::Reassociate { iface } => Error {
code: ErrorCode::ServerError(-32008),
message: format!("Failed to reassociate with WiFi network for {}", iface),
data: None,
},
NetworkError::Reconfigure => Error {
code: ErrorCode::ServerError(-32030),
message: "Failed to force reread of wpa_supplicant configuration file".to_string(),
data: None,
},
NetworkError::Reconnect { iface } => Error {
code: ErrorCode::ServerError(-32009),
message: format!("Failed to reconnect with WiFi network for {}", iface),
data: None,
},
NetworkError::Regex { source } => Error {
code: ErrorCode::ServerError(-32010),
message: format!("Regex command error: {}", source),
data: None,
},
NetworkError::Delete { id, iface } => Error {
code: ErrorCode::ServerError(-32028),
message: format!("Failed to delete network {} for {}", id, iface),
data: None,
},
NetworkError::WlanState { source } => Error {
code: ErrorCode::ServerError(-32011),
message: format!("Failed to retrieve state of wlan0 service: {}", source),
data: None,
},
NetworkError::WlanOperstate { source } => Error {
code: ErrorCode::ServerError(-32021),
message: format!(
"Failed to retrieve connection state of wlan0 interface: {}",
source
),
data: None,
},
NetworkError::Save => Error {
code: ErrorCode::ServerError(-32031),
message: "Failed to save configuration changes to file".to_string(),
data: None,
},
NetworkError::Connect { id, iface } => Error {
code: ErrorCode::ServerError(-32027),
message: format!("Failed to connect to network {} for {}", id, iface),
data: None,
},
NetworkError::StartAp0 { source } => Error {
code: ErrorCode::ServerError(-32016),
message: format!("Failed to start ap0 service: {}", source),
data: None,
},
NetworkError::StartWlan0 { source } => Error {
code: ErrorCode::ServerError(-32018),
message: format!("Failed to start wlan0 service: {}", source),
data: None,
},
NetworkError::SerdeSerialize { source } => Error {
code: ErrorCode::ServerError(-32012),
message: format!("JSON serialization failed: {}", source),
data: None,
},
NetworkError::WpaCtrlOpen { source } => Error {
code: ErrorCode::ServerError(-32013),
message: format!(
"Failed to open control interface for wpasupplicant: {}",
source
),
data: None,
},
NetworkError::WpaCtrlRequest { source } => Error {
code: ErrorCode::ServerError(-32014),
message: format!("WPA supplicant request failed: {}", source),
data: None,
},
impl std::error::Error for NetworkError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match *self {
NetworkError::Add { .. } => None,
NetworkError::NoState { ref source, .. } => Some(source),
NetworkError::Disable { .. } => None,
NetworkError::Disconnect { .. } => None,
NetworkError::GenWpaPassphrase { ref source, .. } => Some(source),
NetworkError::GenWpaPassphraseWarning { .. } => None,
NetworkError::Id { .. } => None,
NetworkError::NoIp { ref source, .. } => Some(source),
NetworkError::Rssi { .. } => None,
NetworkError::RssiPercent { .. } => None,
NetworkError::Ssid { .. } => None,
NetworkError::State { .. } => None,
NetworkError::Status { .. } => None,
NetworkError::Traffic { .. } => None,
NetworkError::SavedNetworks => None,
NetworkError::AvailableNetworks { .. } => None,
NetworkError::Modify { .. } => None,
NetworkError::Ip { .. } => None,
NetworkError::ParseInt(ref source) => Some(source),
NetworkError::NoTraffic { ref source, .. } => Some(source),
NetworkError::Reassociate { .. } => None,
NetworkError::Reconfigure { .. } => None,
NetworkError::Reconnect { .. } => None,
NetworkError::Regex(ref source) => Some(source),
NetworkError::Delete { .. } => None,
NetworkError::WlanState(ref source) => Some(source),
NetworkError::WlanOperstate(ref source) => Some(source),
NetworkError::Save(ref source) => Some(source),
NetworkError::Connect { .. } => None,
NetworkError::StartInterface { ref source, .. } => Some(source),
NetworkError::WpaCtrl(ref source) => Some(source),
}
}
}
impl std::fmt::Display for NetworkError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match *self {
NetworkError::Add { ref ssid } => {
write!(f, "Failed to add network for {}", ssid)
}
NetworkError::NoState { ref iface, .. } => {
write!(f, "Failed to retrieve state for interface: {}", iface)
}
NetworkError::Disable { ref id, ref iface } => {
write!(
f,
"Failed to disable network {} for interface: {}",
id, iface
)
}
NetworkError::Disconnect { ref iface } => {
write!(f, "Failed to disconnect {}", iface)
}
NetworkError::GenWpaPassphrase { ref ssid, .. } => {
write!(f, "Failed to generate wpa passphrase for {}", ssid)
}
NetworkError::GenWpaPassphraseWarning {
ref ssid,
ref err_msg,
} => {
write!(
f,
"Failed to generate wpa passphrase for {}: {}",
ssid, err_msg
)
}
NetworkError::Id {
ref ssid,
ref iface,
} => {
write!(f, "No ID found for {} on interface: {}", ssid, iface)
}
NetworkError::NoIp { ref iface, .. } => {
write!(f, "Could not access IP address for interface: {}", iface)
}
NetworkError::Rssi { ref iface } => {
write!(f, "Could not find RSSI for interface: {}", iface)
}
NetworkError::RssiPercent { ref iface } => {
write!(
f,
"Could not find signal quality (%) for interface: {}",
iface
)
}
NetworkError::Ssid { ref iface } => {
write!(f, "Could not find SSID for interface: {}", iface)
}
NetworkError::State { ref iface } => {
write!(f, "No state found for interface: {}", iface)
}
NetworkError::Status { ref iface } => {
write!(f, "No status found for interface: {}", iface)
}
NetworkError::Traffic { ref iface } => {
write!(f, "Could not find network traffic for interface: {}", iface)
}
NetworkError::SavedNetworks => {
write!(f, "No saved networks found for default interface")
}
NetworkError::AvailableNetworks { ref iface } => {
write!(f, "No networks found in range of interface: {}", iface)
}
NetworkError::Modify { ref id, ref iface } => {
write!(
f,
"Failed to set new password for network {} on {}",
id, iface
)
}
NetworkError::Ip { ref iface } => {
write!(f, "No IP found for interface: {}", iface)
}
NetworkError::ParseInt(_) => {
write!(f, "Failed to parse integer from string for RSSI value")
}
NetworkError::NoTraffic { ref iface, .. } => {
write!(
f,
"Failed to retrieve network traffic measurement for {}",
iface
)
}
NetworkError::Reassociate { ref iface } => {
write!(
f,
"Failed to reassociate with WiFi network for interface: {}",
iface
)
}
NetworkError::Reconfigure => {
write!(
f,
"Failed to force reread of wpa_supplicant configuration file"
)
}
NetworkError::Reconnect { ref iface } => {
write!(
f,
"Failed to reconnect with WiFi network for interface: {}",
iface
)
}
NetworkError::Regex(_) => write!(f, "Regex command failed"),
NetworkError::Delete { ref id, ref iface } => {
write!(
f,
"Failed to delete network {} for interface: {}",
id, iface
)
}
NetworkError::WlanState(_) => write!(f, "Failed to retrieve state of wlan0 service"),
NetworkError::WlanOperstate(_) => {
write!(f, "Failed to retrieve connection state of wlan0 interface")
}
NetworkError::Save(ref source) => write!(
f,
"Failed to save configuration changes to file: {}",
source
),
NetworkError::Connect { ref id, ref iface } => {
write!(
f,
"Failed to connect to network {} for interface: {}",
id, iface
)
}
NetworkError::StartInterface { ref iface, .. } => write!(
f,
"Failed to start systemctl service for {} interface",
iface
),
NetworkError::WpaCtrl(_) => write!(f, "WpaCtrl command failed"),
}
}
}
impl From<WpaError> for NetworkError {
fn from(err: WpaError) -> Self {
NetworkError::WpaCtrl(err)
}
}
impl From<ParseIntError> for NetworkError {
fn from(err: ParseIntError) -> Self {
NetworkError::ParseInt(err)
}
}
impl From<RegexError> for NetworkError {
fn from(err: RegexError) -> Self {
NetworkError::Regex(err)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +0,0 @@
use std::process;
use log::error;
fn main() {
// initalize the logger
env_logger::init();
// handle errors returned from `run`
if let Err(e) = peach_network::run() {
error!("Application error: {}", e);
process::exit(1);
}
}

View File

@ -1,6 +1,6 @@
//! Retrieve network data and modify interface state.
//!
//! This module contains the core logic of the `peach-network` microservice and
//! This module contains the core logic of the `peach-network` and
//! provides convenience wrappers for a range of `wpasupplicant` commands,
//! many of which are ordinarily executed using `wpa_cli` (a WPA command line
//! client).
@ -11,9 +11,10 @@
//! Switching between client mode and access point mode is achieved by making
//! system calls to systemd (via `systemctl`). Further networking functionality
//! is provided by making system calls to retrieve interface state and write
//! access point credentials to `wpa_supplicant-wlan0.conf`.
//!
//! access point credentials to `wpa_supplicant-<wlan_iface>.conf`.
use std::{
collections::HashMap,
fs::OpenOptions,
io::prelude::*,
process::{Command, Stdio},
@ -21,72 +22,59 @@ use std::{
str,
};
use crate::error::{
GenWpaPassphrase, NetworkError, NoIp, NoState, NoTraffic, ParseString, SerdeSerialize,
StartAp0, StartWlan0, WlanState, WpaCtrlOpen, WpaCtrlRequest,
};
use probes::network;
use serde::{Deserialize, Serialize};
use snafu::ResultExt;
use wpactrl::Client as WpaClient;
#[cfg(feature = "miniserde_support")]
use miniserde::{Deserialize, Serialize};
#[cfg(feature = "serde_support")]
use serde::{Deserialize, Serialize};
use crate::error::NetworkError;
use crate::utils;
/// Network interface name.
#[derive(Debug, Deserialize)]
pub struct Iface {
pub iface: String,
}
/// Network interface name and network identifier.
#[derive(Debug, Deserialize)]
pub struct IfaceId {
pub iface: String,
pub id: String,
}
/// Network interface name, network identifier and password.
#[derive(Debug, Deserialize)]
pub struct IfaceIdPass {
pub iface: String,
pub id: String,
pub pass: String,
}
/// Network interface name and network SSID.
#[derive(Debug, Deserialize)]
pub struct IfaceSsid {
pub iface: String,
pub ssid: String,
}
/// Network SSID.
#[derive(Debug, Serialize)]
pub struct Network {
pub ssid: String,
}
/// Access point data retrieved via scan.
#[derive(Debug, Serialize)]
#[derive(Debug)]
#[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
pub struct Scan {
/// Frequency.
pub frequency: String,
/// Protocol.
pub protocol: String,
/// Signal strength.
pub signal_level: String,
/// SSID.
pub ssid: String,
}
/// Status data for a network interface.
#[derive(Debug, Serialize)]
#[derive(Debug)]
#[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
pub struct Status {
/// MAC address.
pub address: Option<String>,
/// Basic Service Set Identifier (BSSID).
pub bssid: Option<String>,
/// Frequency.
pub freq: Option<String>,
/// Group cipher.
pub group_cipher: Option<String>,
/// Local ID.
pub id: Option<String>,
/// IP address.
pub ip_address: Option<String>,
/// Key management.
pub key_mgmt: Option<String>,
/// Mode.
pub mode: Option<String>,
/// Pairwise cipher.
pub pairwise_cipher: Option<String>,
/// SSID.
pub ssid: Option<String>,
/// WPA state.
pub wpa_state: Option<String>,
}
@ -109,21 +97,96 @@ impl Status {
}
/// Received and transmitted network traffic (bytes).
#[derive(Debug, Serialize)]
#[derive(Debug)]
#[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
pub struct Traffic {
/// Total bytes received.
pub received: u64,
/// Total bytes transmitted.
pub transmitted: u64,
}
/// SSID and password for a wireless access point.
#[derive(Debug, Deserialize)]
pub struct WiFi {
pub ssid: String,
pub pass: String,
/// Access point data including state and signal strength.
#[derive(Debug)]
#[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
pub struct AccessPoint {
/// Access point data retrieved via scan.
pub detail: Option<Scan>,
/// Current state of the access point (e.g. "Available" or "Out of range").
pub state: String,
/// Signal strength of the access point as a percentage.
pub signal: Option<i32>,
}
impl AccessPoint {
fn available(detail: Option<Scan>, signal: Option<i32>) -> AccessPoint {
AccessPoint {
detail,
state: String::from("Available"),
signal,
}
}
fn saved() -> AccessPoint {
AccessPoint {
detail: None,
state: String::from("Out of range"),
signal: None,
}
}
}
/* GET - Methods for retrieving data */
/// Retrieve combined list of available (in-range) and saved wireless access
/// points for a given network interface.
///
/// # Arguments
///
/// * `iface` - A string slice holding the name of a wireless network interface
///
/// If the list results include one or more access points for the given network
/// interface, an `Ok` `Result` type is returned containing `HashMap<String,
/// AccessPoint>`.
///
/// Each entry in the returned `HashMap` contains an SSID (`String`) and
/// `AccessPoint` `struct`. If no access points are found, an empty `HashMap`
/// is returned in the `Result`. In the event of an error, a `NetworkError`
/// is returned in the `Result`.
pub fn all_networks(iface: &str) -> Result<HashMap<String, AccessPoint>, NetworkError> {
let mut wlan_networks = HashMap::new();
if let Ok(Some(networks)) = available_networks(iface) {
for ap in networks {
let ssid = ap.ssid.clone();
let rssi = ap.signal_level.clone();
// parse the string to a signed integer (for math)
let rssi_parsed = rssi.parse::<i32>().unwrap();
// perform rssi (dBm) to quality (%) conversion
let quality_percent = 2 * (rssi_parsed + 100);
let ap_detail = AccessPoint::available(Some(ap), Some(quality_percent));
wlan_networks.insert(ssid, ap_detail);
}
}
if let Ok(Some(networks)) = saved_networks() {
for saved_ssid in networks {
if !wlan_networks.contains_key(&saved_ssid) {
let ssid = saved_ssid.clone();
let ap_detail = AccessPoint::saved();
wlan_networks.insert(ssid, ap_detail);
}
}
}
Ok(wlan_networks)
}
/// Retrieve list of available wireless access points for a given network
/// interface.
///
@ -132,22 +195,15 @@ pub struct WiFi {
/// * `iface` - A string slice holding the name of a wireless network interface
///
/// If the scan results include one or more access points for the given network
/// interface, an `Ok` `Result` type is returned containing `Some(String)` -
/// where `String` is a serialized vector of `Scan` structs containing
/// data for the in-range access points. If no access points are found,
/// a `None` type is returned in the `Result`. In the event of an error, a
/// `NetworkError` is returned in the `Result`. The `NetworkError` is then
/// enumerated to a specific error type and an appropriate JSON RPC response is
/// sent to the caller.
///
pub fn available_networks(iface: &str) -> Result<Option<String>, NetworkError> {
/// interface, an `Ok` `Result` type is returned containing `Some(Vec<Scan>)`.
/// The vector of `Scan` structs contains data for the in-range access points.
/// If no access points are found, a `None` type is returned in the `Result`.
/// In the event of an error, a `NetworkError` is returned in the `Result`.
pub fn available_networks(iface: &str) -> Result<Option<Vec<Scan>>, NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = wpactrl::WpaCtrl::new()
.ctrl_path(wpa_path)
.open()
.context(WpaCtrlOpen)?;
wpa.request("SCAN").context(WpaCtrlRequest)?;
let networks = wpa.request("SCAN_RESULTS").context(WpaCtrlRequest)?;
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
wpa.request("SCAN")?;
let networks = wpa.request("SCAN_RESULTS")?;
let mut scan = Vec::new();
for network in networks.lines() {
let v: Vec<&str> = network.split('\t').collect();
@ -162,7 +218,7 @@ pub fn available_networks(iface: &str) -> Result<Option<String>, NetworkError> {
// we only want to return the auth / crypto flags
if flags_vec[0] != "[ESS]" {
// parse auth / crypto flag and assign it to protocol
protocol.push_str(flags_vec[0].replace("[", "").replace("]", "").as_str());
protocol.push_str(flags_vec[0].replace('[', "").replace(']', "").as_str());
}
let ssid = v[4].to_string();
let response = Scan {
@ -178,8 +234,7 @@ pub fn available_networks(iface: &str) -> Result<Option<String>, NetworkError> {
if scan.is_empty() {
Ok(None)
} else {
let results = serde_json::to_string(&scan).context(SerdeSerialize)?;
Ok(Some(results))
Ok(Some(scan))
}
}
@ -195,17 +250,11 @@ pub fn available_networks(iface: &str) -> Result<Option<String>, NetworkError> {
/// found in the list of saved networks, an `Ok` `Result` type is returned
/// containing `Some(String)` - where `String` is the network identifier.
/// If no match is found, a `None` type is returned in the `Result`. In the
/// event of an error, a `NetworkError` is returned in the `Result`. The
/// `NetworkError` is then enumerated to a specific error type and an
/// appropriate JSON RPC response is sent to the caller.
///
/// event of an error, a `NetworkError` is returned in the `Result`.
pub fn id(iface: &str, ssid: &str) -> Result<Option<String>, NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = wpactrl::WpaCtrl::new()
.ctrl_path(wpa_path)
.open()
.context(WpaCtrlOpen)?;
let networks = wpa.request("LIST_NETWORKS").context(WpaCtrlRequest)?;
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
let networks = wpa.request("LIST_NETWORKS")?;
let mut id = Vec::new();
for network in networks.lines() {
let v: Vec<&str> = network.split('\t').collect();
@ -233,13 +282,13 @@ pub fn id(iface: &str, ssid: &str) -> Result<Option<String>, NetworkError> {
/// an `Ok` `Result` type is returned containing `Some(String)` - where `String`
/// is the IP address of the interface. If no match is found, a `None` type is
/// returned in the `Result`. In the event of an error, a `NetworkError` is
/// returned in the `Result`. The `NetworkError` is then enumerated to a
/// specific error type and an appropriate JSON RPC response is sent to the
/// caller.
///
/// returned in the `Result`.
pub fn ip(iface: &str) -> Result<Option<String>, NetworkError> {
let net_if: String = iface.to_string();
let ifaces = get_if_addrs::get_if_addrs().context(NoIp { iface: net_if })?;
let ifaces = get_if_addrs::get_if_addrs().map_err(|source| NetworkError::NoIp {
iface: net_if,
source,
})?;
let ip = ifaces
.iter()
.find(|&i| i.name == iface)
@ -260,16 +309,11 @@ pub fn ip(iface: &str) -> Result<Option<String>, NetworkError> {
/// is the RSSI (Received Signal Strength Indicator) of the connection measured
/// in dBm. If signal strength is not found, a `None` type is returned in the
/// `Result`. In the event of an error, a `NetworkError` is returned in the
/// `Result`. The `NetworkError` is then enumerated to a specific error type and
/// an appropriate JSON RPC response is sent to the caller.
///
/// `Result`.
pub fn rssi(iface: &str) -> Result<Option<String>, NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = wpactrl::WpaCtrl::new()
.ctrl_path(wpa_path)
.open()
.context(WpaCtrlOpen)?;
let status = wpa.request("SIGNAL_POLL").context(WpaCtrlRequest)?;
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
let status = wpa.request("SIGNAL_POLL")?;
let rssi = utils::regex_finder(r"RSSI=(.*)\n", &status)?;
if rssi.is_none() {
@ -292,22 +336,17 @@ pub fn rssi(iface: &str) -> Result<Option<String>, NetworkError> {
/// is the RSSI (Received Signal Strength Indicator) of the connection measured
/// as a percentage. If signal strength is not found, a `None` type is returned
/// in the `Result`. In the event of an error, a `NetworkError` is returned in
/// the `Result`. The `NetworkError` is then enumerated to a specific error type
/// and an appropriate JSON RPC response is sent to the caller.
///
/// the `Result`.
pub fn rssi_percent(iface: &str) -> Result<Option<String>, NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = wpactrl::WpaCtrl::new()
.ctrl_path(wpa_path)
.open()
.context(WpaCtrlOpen)?;
let status = wpa.request("SIGNAL_POLL").context(WpaCtrlRequest)?;
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
let status = wpa.request("SIGNAL_POLL")?;
let rssi = utils::regex_finder(r"RSSI=(.*)\n", &status)?;
match rssi {
Some(rssi) => {
// parse the string to a signed integer (for math)
let rssi_parsed = rssi.parse::<i32>().context(ParseString)?;
let rssi_parsed = rssi.parse::<i32>()?;
// perform rssi (dBm) to quality (%) conversion
let quality_percent = 2 * (rssi_parsed + 100);
// convert signal quality integer to string
@ -327,32 +366,27 @@ pub fn rssi_percent(iface: &str) -> Result<Option<String>, NetworkError> {
///
/// If the wpasupplicant configuration file contains credentials for one or
/// more access points, an `Ok` `Result` type is returned containing
/// `Some(String)` - where `String` is a serialized vector of `Network` structs
/// containing the SSIDs of all saved networks. If no network credentials are
/// found, a `None` type is returned in the `Result`. In the event of an error,
/// a `NetworkError` is returned in the `Result`. The `NetworkError` is then
/// enumerated to a specific error type and an appropriate JSON RPC response is
/// sent to the caller.
///
pub fn saved_networks() -> Result<Option<String>, NetworkError> {
let mut wpa = wpactrl::WpaCtrl::new().open().context(WpaCtrlOpen)?;
let networks = wpa.request("LIST_NETWORKS").context(WpaCtrlRequest)?;
/// `Some(Vec<Network>)`. The vector of `Network` structs contains the SSIDs
/// of all saved networks. If no network credentials are found, a `None` type
/// is returned in the `Result`. In the event of an error, a `NetworkError` is
/// returned in the `Result`.
pub fn saved_networks() -> Result<Option<Vec<String>>, NetworkError> {
let mut wpa = WpaClient::builder().open()?;
let networks = wpa.request("LIST_NETWORKS")?;
let mut ssids = Vec::new();
for network in networks.lines() {
let v: Vec<&str> = network.split('\t').collect();
let len = v.len();
if len > 1 {
let ssid = v[1].trim().to_string();
let response = Network { ssid };
ssids.push(response)
ssids.push(ssid)
}
}
if ssids.is_empty() {
Ok(None)
} else {
let results = serde_json::to_string(&ssids).context(SerdeSerialize)?;
Ok(Some(results))
Ok(Some(ssids))
}
}
@ -366,17 +400,11 @@ pub fn saved_networks() -> Result<Option<String>, NetworkError> {
/// an `Ok` `Result` type is returned containing `Some(String)` - where `String`
/// is the SSID of the associated network. If SSID is not found, a `None` type
/// is returned in the `Result`. In the event of an error, a `NetworkError` is
/// returned in the `Result`. The `NetworkError` is then enumerated to a
/// specific error type and an appropriate JSON RPC response is sent to the
/// caller.
///
/// returned in the `Result`.
pub fn ssid(iface: &str) -> Result<Option<String>, NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = wpactrl::WpaCtrl::new()
.ctrl_path(wpa_path)
.open()
.context(WpaCtrlOpen)?;
let status = wpa.request("STATUS").context(WpaCtrlRequest)?;
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
let status = wpa.request("STATUS")?;
// pass the regex pattern and status output to the regex finder
let ssid = utils::regex_finder(r"\nssid=(.*)\n", &status)?;
@ -394,9 +422,7 @@ pub fn ssid(iface: &str) -> Result<Option<String>, NetworkError> {
/// returned containing `Some(String)` - where `String` is the state of the
/// network interface. If state is not found, a `None` type is returned in the
/// `Result`. In the event of an error, a `NetworkError` is returned in the
/// `Result`. The `NetworkError` is then enumerated to a specific error type and
/// an appropriate JSON RPC response is sent to the caller.
///
/// `Result`.
pub fn state(iface: &str) -> Result<Option<String>, NetworkError> {
// construct the interface operstate path
let iface_path: String = format!("/sys/class/net/{}/operstate", iface);
@ -404,7 +430,10 @@ pub fn state(iface: &str) -> Result<Option<String>, NetworkError> {
let output = Command::new("cat")
.arg(iface_path)
.output()
.context(NoState { iface })?;
.map_err(|source| NetworkError::NoState {
iface: iface.to_string(),
source,
})?;
if !output.stdout.is_empty() {
// unwrap the command result and convert to String
let mut state = String::from_utf8(output.stdout).unwrap();
@ -427,17 +456,11 @@ pub fn state(iface: &str) -> Result<Option<String>, NetworkError> {
/// returned containing `Some(Status)` - where `Status` is a `struct`
/// containing the aggregated interface data in named fields. If status is not
/// found, a `None` type is returned in the `Result`. In the event of an error,
/// a `NetworkError` is returned in the `Result`. The `NetworkError` is then
/// enumerated to a specific error type and an appropriate JSON RPC response is
/// sent to the caller.
///
/// a `NetworkError` is returned in the `Result`.
pub fn status(iface: &str) -> Result<Option<Status>, NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = wpactrl::WpaCtrl::new()
.ctrl_path(wpa_path)
.open()
.context(WpaCtrlOpen)?;
let wpa_status = wpa.request("STATUS").context(WpaCtrlRequest)?;
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
let wpa_status = wpa.request("STATUS")?;
// pass the regex pattern and status output to the regex finder
let state = utils::regex_finder(r"wpa_state=(.*)\n", &wpa_status)?;
@ -486,16 +509,16 @@ pub fn status(iface: &str) -> Result<Option<Status>, NetworkError> {
/// * `iface` - A string slice holding the name of a wireless network interface
///
/// If the network traffic statistics are found for the given interface, an `Ok`
/// `Result` type is returned containing `Some(String)` - where `String` is a
/// serialized `Traffic` `struct` with fields for received and transmitted
/// network data statistics. If network traffic statistics are not found for the
/// given interface, a `None` type is returned in the `Result`. In the event of
/// an error, a `NetworkError` is returned in the `Result`. The `NetworkError`
/// is then enumerated to a specific error type and an appropriate JSON RPC
/// response is sent to the caller.
///
pub fn traffic(iface: &str) -> Result<Option<String>, NetworkError> {
let network = network::read().context(NoTraffic { iface })?;
/// `Result` type is returned containing `Some(Traffic)`. The `Traffic` `struct`
/// includes fields for received and transmitted network data statistics. If
/// network traffic statistics are not found for the given interface, a `None`
/// type is returned in the `Result`. In the event of an error, a `NetworkError`
/// is returned in the `Result`.
pub fn traffic(iface: &str) -> Result<Option<Traffic>, NetworkError> {
let network = network::read().map_err(|source| NetworkError::NoTraffic {
iface: iface.to_string(),
source,
})?;
// iterate through interfaces returned in network data
for (interface, traffic) in network.interfaces {
if interface == iface {
@ -505,9 +528,7 @@ pub fn traffic(iface: &str) -> Result<Option<String>, NetworkError> {
received,
transmitted,
};
// TODO: add test for SerdeSerialize error
let t = serde_json::to_string(&traffic).context(SerdeSerialize)?;
return Ok(Some(t));
return Ok(Some(traffic));
}
}
@ -516,42 +537,25 @@ pub fn traffic(iface: &str) -> Result<Option<String>, NetworkError> {
/* SET - Methods for modifying state */
/// Activate wireless access point.
/// Start network interface service.
///
/// A `systemctl `command is invoked which starts the `ap0` interface service.
/// If the command executes successfully, an `Ok` `Result` type is returned.
/// In the event of an error, a `NetworkError` is returned in the `Result`.
/// The `NetworkError` is then enumerated to a specific error type and an
/// appropriate JSON RPC response is sent to the caller.
///
pub fn activate_ap() -> Result<(), NetworkError> {
// start the ap0 interface service
/// A `systemctl `command is invoked which starts the service for the given
/// network interface. If the command executes successfully, an `Ok` `Result`
/// type is returned. In the event of an error, a `NetworkError` is returned
/// in the `Result`.
pub fn start_iface_service(iface: &str) -> Result<(), NetworkError> {
let iface_service = format!("wpa_supplicant@{}.service", &iface);
// start the interface service
Command::new("sudo")
.arg("/usr/bin/systemctl")
.arg("start")
.arg("wpa_supplicant@ap0.service")
.arg(iface_service)
.output()
.context(StartAp0)?;
Ok(())
}
/// Activate wireless client.
///
/// A `systemctl` command is invoked which starts the `wlan0` interface service.
/// If the command executes successfully, an `Ok` `Result` type is returned.
/// In the event of an error, a `NetworkError` is returned in the `Result`.
/// The `NetworkError` is then enumerated to a specific error type and an
/// appropriate JSON RPC response is sent to the caller.
///
pub fn activate_client() -> Result<(), NetworkError> {
// start the wlan0 interface service
Command::new("sudo")
.arg("/usr/bin/systemctl")
.arg("start")
.arg("wpa_supplicant@wlan0.service")
.output()
.context(StartWlan0)?;
.map_err(|source| NetworkError::StartInterface {
source,
iface: iface.to_string(),
})?;
Ok(())
}
@ -560,81 +564,82 @@ pub fn activate_client() -> Result<(), NetworkError> {
///
/// # Arguments
///
/// * `wlan_iface` - A local wireless interface.
/// * `wifi` - An instance of the `WiFi` `struct` with fields `ssid` and `pass`
///
/// If configuration parameters are successfully generated from the provided
/// SSID and password and appended to `wpa_supplicant-wlan0.conf`, an `Ok`
/// `Result` type is returned. In the event of an error, a `NetworkError` is
/// returned in the `Result`. The `NetworkError` is then enumerated to a
/// specific error type and an appropriate JSON RPC response is sent to the
/// caller.
///
pub fn add(wifi: &WiFi) -> Result<(), NetworkError> {
/// SSID and password and appended to `wpa_supplicant-<wlan_iface>.conf` (where
/// `<wlan_iface>` is the provided interface parameter), an `Ok` `Result` type
/// is returned. In the event of an error, a `NetworkError` is returned in the
/// `Result`.
pub fn add(wlan_iface: &str, ssid: &str, pass: &str) -> Result<(), NetworkError> {
// generate configuration based on provided ssid & password
let output = Command::new("wpa_passphrase")
.arg(&wifi.ssid)
.arg(&wifi.pass)
.arg(&ssid)
.arg(&pass)
.stdout(Stdio::piped())
.output()
.context(GenWpaPassphrase { ssid: &wifi.ssid })?;
.map_err(|source| NetworkError::GenWpaPassphrase {
ssid: ssid.to_string(),
source,
})?;
// prepend newline to wpa_details to safeguard against malformed supplicant
let mut wpa_details = "\n".as_bytes().to_vec();
wpa_details.extend(&*(output.stdout));
// append wpa_passphrase output to wpa_supplicant-wlan0.conf if successful
let wlan_config = format!("/etc/wpa_supplicant/wpa_supplicant-{}.conf", wlan_iface);
// append wpa_passphrase output to wpa_supplicant-<wlan_iface>.conf if successful
if output.status.success() {
// open file in append mode
let file = OpenOptions::new()
let mut file = OpenOptions::new()
.append(true)
.open("/etc/wpa_supplicant/wpa_supplicant-wlan0.conf");
.open(wlan_config)
// TODO: create the file if it doesn't exist
.map_err(NetworkError::Save)?;
file.write(&wpa_details).map_err(NetworkError::Save)?;
let _file = match file {
// if file exists & open succeeds, write wifi configuration
Ok(mut f) => f.write(&wpa_details),
// TODO: handle this better: create file if not found
// & seed with 'ctrl_interace' & 'update_config' settings
// config file could also be copied from peach/config fs location
Err(e) => panic!("Failed to write to file: {}", e),
};
Ok(())
} else {
let err_msg = String::from_utf8_lossy(&output.stdout);
Err(NetworkError::GenWpaPassphraseWarning {
ssid: wifi.ssid.to_string(),
ssid: ssid.to_string(),
err_msg: err_msg.to_string(),
})
}
}
/// Deploy the access point if the `wlan0` interface is `up` without an active
/// Deploy an access point if the wireless interface is `up` without an active
/// connection.
///
/// The status of the `wlan0` service and the state of the `wlan0` interface
/// The status of the wireless service and the state of the wireless interface
/// are checked. If the service is active but the interface is down (ie. not
/// currently connected to an access point), then the access point is activated
/// by calling the `activate_ap()` function.
///
pub fn check_iface() -> Result<(), NetworkError> {
// returns 0 if the service is currently active
let wlan0_status = Command::new("/usr/bin/systemctl")
.arg("is-active")
.arg("wpa_supplicant@wlan0.service")
.status()
.context(WlanState)?;
pub fn check_iface(wlan_iface: &str, ap_iface: &str) -> Result<(), NetworkError> {
let wpa_service = format!("wpa_supplicant@{}.service", &wlan_iface);
// returns the current state of the wlan0 interface
let iface_state = state("wlan0")?;
// returns 0 if the service is currently active
let wlan_status = Command::new("/usr/bin/systemctl")
.arg("is-active")
.arg(wpa_service)
.status()
.map_err(NetworkError::WlanState)?;
// returns the current state of the wlan interface
let iface_state = state(wlan_iface)?;
// returns down if the interface is not currently connected to an ap
let wlan0_state = match iface_state {
let wlan_state = match iface_state {
Some(state) => state,
None => "error".to_string(),
};
// if wlan0 is active but not connected, start the ap0 service
if wlan0_status.success() && wlan0_state == "down" {
activate_ap()?
// if wlan is active but not connected, start the ap service
if wlan_status.success() && wlan_state == "down" {
start_iface_service(ap_iface)?
}
Ok(())
@ -651,18 +656,12 @@ pub fn check_iface() -> Result<(), NetworkError> {
/// If the network connection is successfully activated for the access point
/// represented by the given network identifier on the given wireless interface,
/// an `Ok` `Result`type is returned. In the event of an error, a `NetworkError`
/// is returned in the `Result`. The `NetworkError` is then enumerated to a
/// specific error type and an appropriate JSON RPC response is sent to the
/// caller.
///
/// is returned in the `Result`.
pub fn connect(id: &str, iface: &str) -> Result<(), NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = wpactrl::WpaCtrl::new()
.ctrl_path(wpa_path)
.open()
.context(WpaCtrlOpen)?;
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
let select = format!("SELECT {}", id);
wpa.request(&select).context(WpaCtrlRequest)?;
wpa.request(&select)?;
Ok(())
}
@ -676,18 +675,12 @@ pub fn connect(id: &str, iface: &str) -> Result<(), NetworkError> {
/// If the network configuration parameters are successfully deleted for
/// the access point represented by the given network identifier, an `Ok`
/// `Result`type is returned. In the event of an error, a `NetworkError` is
/// returned in the `Result`. The `NetworkError` is then enumerated to a
/// specific error type and an appropriate JSON RPC response is sent to the
/// caller.
///
/// returned in the `Result`.
pub fn delete(id: &str, iface: &str) -> Result<(), NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = wpactrl::WpaCtrl::new()
.ctrl_path(wpa_path)
.open()
.context(WpaCtrlOpen)?;
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
let remove = format!("REMOVE_NETWORK {}", id);
wpa.request(&remove).context(WpaCtrlRequest)?;
wpa.request(&remove)?;
Ok(())
}
@ -701,17 +694,12 @@ pub fn delete(id: &str, iface: &str) -> Result<(), NetworkError> {
/// If the network connection is successfully disabled for the access point
/// represented by the given network identifier, an `Ok` `Result`type is
/// returned. In the event of an error, a `NetworkError` is returned in the
/// `Result`. The `NetworkError` is then enumerated to a specific error type and
/// an appropriate JSON RPC response is sent to the caller.
///
/// `Result`.
pub fn disable(id: &str, iface: &str) -> Result<(), NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = wpactrl::WpaCtrl::new()
.ctrl_path(wpa_path)
.open()
.context(WpaCtrlOpen)?;
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
let disable = format!("DISABLE_NETWORK {}", id);
wpa.request(&disable).context(WpaCtrlRequest)?;
wpa.request(&disable)?;
Ok(())
}
@ -723,18 +711,44 @@ pub fn disable(id: &str, iface: &str) -> Result<(), NetworkError> {
///
/// If the network connection is successfully disconnected for the given
/// wireless interface, an `Ok` `Result` type is returned. In the event of an
/// error, a `NetworkError` is returned in the `Result`. The `NetworkError` is
/// then enumerated to a specific error type and an appropriate JSON RPC
/// response is sent to the caller.
///
/// error, a `NetworkError` is returned in the `Result`.
pub fn disconnect(iface: &str) -> Result<(), NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = wpactrl::WpaCtrl::new()
.ctrl_path(wpa_path)
.open()
.context(WpaCtrlOpen)?;
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
let disconnect = "DISCONNECT".to_string();
wpa.request(&disconnect).context(WpaCtrlRequest)?;
wpa.request(&disconnect)?;
Ok(())
}
/// Forget credentials for the given network SSID and interface.
/// Look up the network identified for the given SSID, delete the credentials
/// and then save.
///
/// # Arguments
///
/// * `iface` - A string slice holding the name of a wireless network interface
/// * `ssid` - A string slice holding the SSID for a wireless access point
///
/// If the credentials are successfully deleted and saved, an `Ok` `Result`
/// type is returned. In the event of an error, a `NetworkError` is returned
/// in the `Result`.
pub fn forget(iface: &str, ssid: &str) -> Result<(), NetworkError> {
// get the id of the network
let id_opt = id(iface, ssid)?;
let id = id_opt.ok_or(NetworkError::Id {
ssid: ssid.to_string(),
iface: iface.to_string(),
})?;
// delete the old credentials
// TODO: i've switched these back to the "correct" order
// WEIRD BUG: the parameters below are technically in the wrong order:
// it should be id first and then iface, but somehow they get twisted.
// i don't understand computers.
//delete(&iface, &id)?;
delete(&id, iface)?;
// save the updates to wpa_supplicant.conf
save()?;
Ok(())
}
@ -748,18 +762,12 @@ pub fn disconnect(iface: &str) -> Result<(), NetworkError> {
///
/// If the password is successfully updated for the access point represented by
/// the given network identifier, an `Ok` `Result` type is returned. In the
/// event of an error, a `NetworkError` is returned in the `Result`. The
/// `NetworkError` is then enumerated to a specific error type and an
/// appropriate JSON RPC response is sent to the caller.
///
/// event of an error, a `NetworkError` is returned in the `Result`.
pub fn modify(id: &str, iface: &str, pass: &str) -> Result<(), NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = wpactrl::WpaCtrl::new()
.ctrl_path(wpa_path)
.open()
.context(WpaCtrlOpen)?;
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
let new_pass = format!("NEW_PASSWORD {} {}", id, pass);
wpa.request(&new_pass).context(WpaCtrlRequest)?;
wpa.request(&new_pass)?;
Ok(())
}
@ -771,17 +779,11 @@ pub fn modify(id: &str, iface: &str, pass: &str) -> Result<(), NetworkError> {
///
/// If the network connection is successfully reassociated for the given
/// wireless interface, an `Ok` `Result` type is returned. In the event of an
/// error, a `NetworkError` is returned in the `Result`. The `NetworkError` is
/// then enumerated to a specific error type and an appropriate JSON RPC
/// response is sent to the caller.
///
/// error, a `NetworkError` is returned in the `Result`.
pub fn reassociate(iface: &str) -> Result<(), NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = wpactrl::WpaCtrl::new()
.ctrl_path(wpa_path)
.open()
.context(WpaCtrlOpen)?;
wpa.request("REASSOCIATE").context(WpaCtrlRequest)?;
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
wpa.request("REASSOCIATE")?;
Ok(())
}
@ -790,13 +792,10 @@ pub fn reassociate(iface: &str) -> Result<(), NetworkError> {
/// If the reconfigure command is successfully executed, indicating a reread
/// of the `wpa_supplicant.conf` file by the `wpa_supplicant` process, an `Ok`
/// `Result` type is returned. In the event of an error, a `NetworkError` is
/// returned in the `Result`. The `NetworkError` is then enumerated to a
/// specific error type and an appropriate JSON RPC response is sent to the
/// caller.
///
/// returned in the `Result`.
pub fn reconfigure() -> Result<(), NetworkError> {
let mut wpa = wpactrl::WpaCtrl::new().open().context(WpaCtrlOpen)?;
wpa.request("RECONFIGURE").context(WpaCtrlRequest)?;
let mut wpa = WpaClient::builder().open()?;
wpa.request("RECONFIGURE")?;
Ok(())
}
@ -808,31 +807,37 @@ pub fn reconfigure() -> Result<(), NetworkError> {
///
/// If the network connection is successfully disconnected and reconnected for
/// the given wireless interface, an `Ok` `Result` type is returned. In the
/// event of an error, a `NetworkError` is returned in the `Result`. The
/// `NetworkError` is then enumerated to a specific error type and an
/// appropriate JSON RPC response is sent to the caller.
///
/// event of an error, a `NetworkError` is returned in the `Result`.
pub fn reconnect(iface: &str) -> Result<(), NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = wpactrl::WpaCtrl::new()
.ctrl_path(wpa_path)
.open()
.context(WpaCtrlOpen)?;
wpa.request("DISCONNECT").context(WpaCtrlRequest)?;
wpa.request("RECONNECT").context(WpaCtrlRequest)?;
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
wpa.request("DISCONNECT")?;
wpa.request("RECONNECT")?;
Ok(())
}
/// Save configuration updates to the `wpa_supplicant` configuration file.
///
/// If wireless network configuration updates are successfully save to the
/// If wireless network configuration updates are successfully saved to the
/// `wpa_supplicant.conf` file, an `Ok` `Result` type is returned. In the
/// event of an error, a `NetworkError` is returned in the `Result`. The
/// `NetworkError` is then enumerated to a specific error type and an
/// appropriate JSON RPC response is sent to the caller.
///
/// event of an error, a `NetworkError` is returned in the `Result`.
pub fn save() -> Result<(), NetworkError> {
let mut wpa = wpactrl::WpaCtrl::new().open().context(WpaCtrlOpen)?;
wpa.request("SAVE_CONFIG").context(WpaCtrlRequest)?;
let mut wpa = WpaClient::builder().open()?;
wpa.request("SAVE_CONFIG")?;
Ok(())
}
/// Update password for an access point and save configuration updates to the
/// `wpa_supplicant` configuration file.
///
/// If wireless network configuration updates are successfully saved to the
/// `wpa_supplicant.conf` file, an `Ok` `Result` type is returned. In the
/// event of an error, a `NetworkError` is returned in the `Result`.
pub fn update(iface: &str, ssid: &str, pass: &str) -> Result<(), NetworkError> {
// delete the old credentials and save the changes
forget(iface, ssid)?;
// add the new credentials
add(iface, ssid, pass)?;
reconfigure()?;
Ok(())
}

View File

@ -1,7 +1,6 @@
use regex::Regex;
use snafu::ResultExt;
use crate::error::*;
use crate::error::NetworkError;
/// Return matches for a given Regex pattern and text
///
@ -11,7 +10,7 @@ use crate::error::*;
/// * `text` - A string slice containing the text to be matched on
///
pub fn regex_finder(pattern: &str, text: &str) -> Result<Option<String>, NetworkError> {
let re = Regex::new(pattern).context(Regex)?;
let re = Regex::new(pattern)?;
let caps = re.captures(text);
let result = caps.map(|caps| caps[1].to_string());

View File

@ -1,4 +0,0 @@
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
objcopy = { path ="aarch64-linux-gnu-objcopy" }
strip = { path ="aarch64-linux-gnu-strip" }

View File

@ -1,4 +0,0 @@
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
objcopy = { path ="aarch64-linux-gnu-objcopy" }
strip = { path ="aarch64-linux-gnu-strip" }

View File

@ -1,4 +0,0 @@
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
objcopy = { path ="aarch64-linux-gnu-objcopy" }
strip = { path ="aarch64-linux-gnu-strip" }

View File

@ -1,40 +1,31 @@
[package]
name = "peach-stats"
version = "0.1.3"
authors = ["Andrew Reid <gnomad@cryptolab.net>"]
version = "0.3.1"
authors = ["Andrew Reid <glyph@mycelial.technology>"]
edition = "2018"
description = "Query system statistics using JSON-RPC over HTTP. Provides a JSON-RPC wrapper around the probes and systemstat crates."
description = "Query system statistics. Provides a wrapper around the probes and systemstat crates."
keywords = ["peachcloud", "system stats", "system statistics", "disk", "memory"]
homepage = "https://opencollective.com/peachcloud"
repository = "https://github.com/peachcloud/peach-stats"
repository = "https://git.coopcloud.tech/PeachCloud/peach-workspace/src/branch/main/peach-stats"
readme = "README.md"
license = "AGPL-3.0-only"
license = "LGPL-3.0-only"
publish = false
[package.metadata.deb]
depends = "$auto"
extended-description = """\
peach-stats is a system statistics microservice module for PeachCloud. \
Query system statistics using JSON-RPC over HTTP. Provides a JSON-RPC \
wrapper around the probes and systemstat crates."""
maintainer-scripts="debian"
systemd-units = { unit-name = "peach-stats" }
assets = [
["target/release/peach-stats", "usr/bin/", "755"],
["README.md", "usr/share/doc/peach-stats/README", "644"],
]
[badges]
travis-ci = { repository = "peachcloud/peach-stats", branch = "master" }
maintenance = { status = "actively-developed" }
[dependencies]
env_logger = "0.9"
jsonrpc-core = "18"
jsonrpc-http-server = "18"
log = "0.4"
miniserde = "0.1.15"
miniserde = { version = "0.1.15", optional = true }
probes = "0.4.1"
serde = { version = "1.0.130", features = ["derive"], optional = true }
systemstat = "0.1.10"
[dev-dependencies]
jsonrpc-test = "18"
[features]
default = []
# Provide `Serialize` and `Deserialize` traits for library structs using `miniserde`
miniserde_support = ["miniserde"]
# Provide `Serialize` and `Deserialize` traits for library structs using `serde`
serde_support = ["serde"]

View File

@ -1,109 +1,52 @@
# peach-stats
[![Build Status](https://travis-ci.com/peachcloud/peach-stats.svg?branch=master)](https://travis-ci.com/peachcloud/peach-stats) ![Generic badge](https://img.shields.io/badge/version-0.1.3-<COLOR>.svg)
![Generic badge](https://img.shields.io/badge/version-0.3.0-<COLOR>.svg)
System statistics microservice module for PeachCloud. Provides a JSON-RPC wrapper around the [probes](https://crates.io/crates/probes) and [systemstat](https://crates.io/crates/systemstat) crates.
System statistics library for PeachCloud. Provides a wrapper around the [probes](https://crates.io/crates/probes) and [systemstat](https://crates.io/crates/systemstat) crates.
### JSON-RPC API
Currently offers the following system statistics and associated data structures:
| Method | Description | Returns |
| --- | --- | --- |
| `cpu_stats` | CPU statistics | `user`, `system`, `nice`, `idle` |
| `cpu_stats_percent` | CPU statistics as percentages | `user`, `system`, `nice`, `idle` |
| `disk_usage` | Disk usage statistics (array of disks) | `filesystem`, `one_k_blocks`, `one_k_blocks_used`, `one_k_blocks_free`, `used_percentage`, `mountpoint` |
| `load_average` | Load average statistics | `one`, `five`, `fifteen` |
| `mem_stats` | Memory statistics | `total`, `free`, `used` |
| `ping` | Microservice status | `success` if running |
| `uptime` | System uptime | `secs` |
- CPU: `user`, `system`, `nice`, `idle` (as values or percentages)
- Disk usage: `filesystem`, `one_k_blocks`, `one_k_blocks_used`,
`one_k_blocks_free`, `used_percentage`, `mountpoint`
- Load average: `one`, `five`, `fifteen`
- Memory: `total`, `free`, `used`
- Uptime: `seconds`
### Environment
As well as the following go-sbot process statistics:
The JSON-RPC HTTP server address and port can be configured with the `PEACH_STATS_SERVER` environment variable:
- Sbot: `state`, `memory`, `uptime`, `downtime`
`export PEACH_STATS_SERVER=127.0.0.1:5000`
## Example Usage
When not set, the value defaults to `127.0.0.1:5113`.
```rust
use peach_stats::{sbot, stats, StatsError};
Logging is made available with `env_logger`:
fn main() -> Result<(), StatsError> {
let cpu = stats::cpu_stats()?;
let cpu_percentages = stats::cpu_stats_percent()?;
let disks = stats::disk_usage()?;
let load = stats::load_average()?;
let mem = stats::mem_stats()?;
let uptime = stats::uptime()?;
let sbot_process = sbot::sbot_stats()?;
`export RUST_LOG=info`
// do things with the retrieved values...
Other logging levels include `debug`, `warn` and `error`.
Ok(())
}
```
### Setup
## Feature Flags
Clone this repo:
Feature flags are used to offer `Serialize` and `Deserialize` implementations for all `struct` data types provided by this library. These traits are not provided by default. A choice of `miniserde` and `serde` is provided.
`git clone https://github.com/peachcloud/peach-stats.git`
Define the desired feature in the `Cargo.toml` manifest of your project:
Move into the repo and compile a release build:
```toml
peach-stats = { version = "0.1.0", features = ["miniserde_support"] }
```
`cd peach-stats`
`cargo build --release`
Run the binary:
`./target/release/peach-stats`
### Debian Packaging
A `systemd` service file and Debian maintainer scripts are included in the `debian` directory, allowing `peach-stats` to be easily bundled as a Debian package (`.deb`). The `cargo-deb` [crate](https://crates.io/crates/cargo-deb) can be used to achieve this.
Install `cargo-deb`:
`cargo install cargo-deb`
Move into the repo:
`cd peach-stats`
Build the package:
`cargo deb`
The output will be written to `target/debian/peach-stats_0.1.0_arm64.deb` (or similar).
Build the package (aarch64):
`cargo deb --target aarch64-unknown-linux-gnu`
Install the package as follows:
`sudo dpkg -i target/debian/peach-stats_0.1.0_arm64.deb`
The service will be automatically enabled and started.
Uninstall the service:
`sudo apt-get remove peach-stats`
Remove configuration files (not removed with `apt-get remove`):
`sudo apt-get purge peach-stats`
### Example Usage
**Get CPU Statistics**
With microservice running, open a second terminal window and use `curl` to call server methods:
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "cpu_stats", "id":1 }' 127.0.0.1:5113`
Server responds with:
`{"jsonrpc":"2.0","result":"{\"user\":4661083,\"system\":1240371,\"idle\":326838290,\"nice\":0}","id":1}`
**Get System Uptime**
With microservice running, open a second terminal window and use `curl` to call server methods:
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "uptime", "id":1 }' 127.0.0.1:5113`
Server responds with:
`{"jsonrpc":"2.0","result":"{\"secs\":840968}","id":1}`
### Licensing
AGPL-3.0
## License
LGPL-3.0.

View File

@ -1,27 +0,0 @@
[Unit]
Description=Query system statistics using JSON-RPC over HTTP.
[Service]
Type=simple
User=peach-stats
Environment="RUST_LOG=error"
ExecStart=/usr/bin/peach-stats
Restart=always
CapabilityBoundingSet=~CAP_SYS_ADMIN CAP_SYS_PTRACE CAP_SYS_BOOT CAP_SYS_TIME CAP_KILL CAP_WAKE_ALARM CAP_LINUX_IMMUTABLE CAP_BLOCK_SUSPEND CAP_LEASE CAP_SYS_NICE CAP_SYS_RESOURCE CAP_RAWIO CAP_CHOWN CAP_FSETID CAP_SETFCAP CAP_DAC_* CAP_FOWNER CAP_IPC_OWNER CAP_SETUID CAP_SETGID CAP_SETPCAP CAP_AUDIT_*
InaccessibleDirectories=/home
LockPersonality=yes
NoNewPrivileges=yes
PrivateDevices=yes
PrivateTmp=yes
PrivateUsers=yes
ProtectControlGroups=yes
ProtectHome=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
ProtectSystem=yes
ReadOnlyDirectories=/var
RestrictAddressFamilies=~AF_INET6 AF_UNIX
SystemCallFilter=~@reboot @clock @debug @module @mount @swap @resources @privileged
[Install]
WantedBy=multi-user.target

View File

@ -1,69 +1,54 @@
use std::{error, fmt, io};
//! Custom error type for `peach-stats`.
use jsonrpc_core::{types::error::Error, ErrorCode};
use probes::ProbeError;
use std::{error, fmt, io::Error as IoError, str::Utf8Error};
/// Custom error type encapsulating all possible errors when retrieving system
/// statistics.
#[derive(Debug)]
pub enum StatError {
CpuStat { source: ProbeError },
DiskUsage { source: ProbeError },
LoadAvg { source: ProbeError },
MemStat { source: ProbeError },
Uptime { source: io::Error },
pub enum StatsError {
/// Failed to retrieve CPU statistics.
CpuStat(ProbeError),
/// Failed to retrieve disk usage statistics.
DiskUsage(ProbeError),
/// Failed to retrieve load average statistics.
LoadAvg(ProbeError),
/// Failed to retrieve memory usage statistics.
MemStat(ProbeError),
/// Failed to retrieve system uptime.
Uptime(IoError),
/// Systemctl command returned an error.
Systemctl(IoError),
/// Failed to interpret sequence of `u8` as a string.
Utf8String(Utf8Error),
}
impl error::Error for StatError {}
impl error::Error for StatsError {}
impl fmt::Display for StatError {
impl fmt::Display for StatsError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
StatError::CpuStat { ref source } => {
StatsError::CpuStat(ref source) => {
write!(f, "Failed to retrieve CPU statistics: {}", source)
}
StatError::DiskUsage { ref source } => {
StatsError::DiskUsage(ref source) => {
write!(f, "Failed to retrieve disk usage statistics: {}", source)
}
StatError::LoadAvg { ref source } => {
StatsError::LoadAvg(ref source) => {
write!(f, "Failed to retrieve load average statistics: {}", source)
}
StatError::MemStat { ref source } => {
StatsError::MemStat(ref source) => {
write!(f, "Failed to retrieve memory statistics: {}", source)
}
StatError::Uptime { ref source } => {
StatsError::Uptime(ref source) => {
write!(f, "Failed to retrieve system uptime: {}", source)
}
StatsError::Systemctl(ref source) => {
write!(f, "Systemctl command returned an error: {}", source)
}
StatsError::Utf8String(ref source) => {
write!(f, "Failed to convert stdout to string: {}", source)
}
}
}
}
impl From<StatError> for Error {
fn from(err: StatError) -> Self {
match &err {
StatError::CpuStat { source } => Error {
code: ErrorCode::ServerError(-32001),
message: format!("Failed to retrieve CPU statistics: {}", source),
data: None,
},
StatError::DiskUsage { source } => Error {
code: ErrorCode::ServerError(-32001),
message: format!("Failed to retrieve disk usage statistics: {}", source),
data: None,
},
StatError::LoadAvg { source } => Error {
code: ErrorCode::ServerError(-32001),
message: format!("Failed to retrieve load average statistics: {}", source),
data: None,
},
StatError::MemStat { source } => Error {
code: ErrorCode::ServerError(-32001),
message: format!("Failed to retrieve memory statistics: {}", source),
data: None,
},
StatError::Uptime { source } => Error {
code: ErrorCode::ServerError(-32001),
message: format!("Failed to retrieve system uptime: {}", source),
data: None,
},
}
}
}

View File

@ -1,103 +1,49 @@
mod error;
mod stats;
mod structs;
#![warn(missing_docs)]
use std::{env, result::Result};
//! # peach-stats
//!
//! System statistics retrieval library; designed for use with the PeachCloud platform.
//!
//! Currently offers the following statistics and associated data structures:
//!
//! - CPU: `user`, `system`, `nice`, `idle` (as values or percentages)
//! - Disk usage: `filesystem`, `one_k_blocks`, `one_k_blocks_used`,
//! `one_k_blocks_free`, `used_percentage`, `mountpoint`
//! - Load average: `one`, `five`, `fifteen`
//! - Memory: `total`, `free`, `used`
//! - Uptime: `seconds`
//!
//! ## Example Usage
//!
//! ```rust
//! use peach_stats::{stats, StatsError};
//!
//! fn main() -> Result<(), StatsError> {
//! let cpu = stats::cpu_stats()?;
//! let cpu_percentages = stats::cpu_stats_percent()?;
//! let disks = stats::disk_usage()?;
//! let load = stats::load_average()?;
//! let mem = stats::mem_stats()?;
//! let uptime = stats::uptime()?;
//!
//! Ok(())
//! }
//! ```
//!
//! ## Feature Flags
//!
//! Feature flags are used to offer `Serialize` and `Deserialize` implementations
//! for all `struct` data types provided by this library. These traits are not
//! provided by default. A choice of `miniserde` and `serde` is provided.
//!
//! Define the desired feature in the `Cargo.toml` manifest of your project:
//!
//! ```toml
//! peach-stats = { version = "0.1.0", features = ["miniserde_support"] }
//! ```
use jsonrpc_core::{IoHandler, Value};
use jsonrpc_http_server::{AccessControlAllowOrigin, DomainsValidation, ServerBuilder};
use log::info;
pub mod error;
pub mod sbot;
pub mod stats;
use crate::error::StatError;
pub fn run() -> Result<(), StatError> {
info!("Starting up.");
info!("Creating JSON-RPC I/O handler.");
let mut io = IoHandler::default();
io.add_method("cpu_stats", move |_| async {
info!("Fetching CPU statistics.");
let stats = stats::cpu_stats()?;
Ok(Value::String(stats))
});
io.add_method("cpu_stats_percent", move |_| async {
info!("Fetching CPU statistics as percentages.");
let stats = stats::cpu_stats_percent()?;
Ok(Value::String(stats))
});
io.add_method("disk_usage", move |_| async {
info!("Fetching disk usage statistics.");
let disks = stats::disk_usage()?;
Ok(Value::String(disks))
});
io.add_method("load_average", move |_| async {
info!("Fetching system load average statistics.");
let avg = stats::load_average()?;
Ok(Value::String(avg))
});
io.add_method("mem_stats", move |_| async {
info!("Fetching current memory statistics.");
let mem = stats::mem_stats()?;
Ok(Value::String(mem))
});
io.add_method("ping", |_| async {
Ok(Value::String("success".to_string()))
});
io.add_method("uptime", move |_| async {
info!("Fetching system uptime.");
let uptime = stats::uptime()?;
Ok(Value::String(uptime))
});
let http_server = env::var("PEACH_OLED_STATS").unwrap_or_else(|_| "127.0.0.1:5113".to_string());
info!("Starting JSON-RPC server on {}.", http_server);
let server = ServerBuilder::new(io)
.cors(DomainsValidation::AllowOnly(vec![
AccessControlAllowOrigin::Null,
]))
.start_http(
&http_server
.parse()
.expect("Invalid HTTP address and port combination"),
)
.expect("Unable to start RPC server");
info!("Listening for requests.");
server.wait();
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use jsonrpc_test as test_rpc;
// test to ensure correct success response
#[test]
fn rpc_success() {
let rpc = {
let mut io = IoHandler::new();
io.add_method("rpc_success_response", |_| async {
Ok(Value::String("success".into()))
});
test_rpc::Rpc::from(io)
};
assert_eq!(rpc.request("rpc_success_response", &()), r#""success""#);
}
}
pub use crate::error::StatsError;

View File

@ -1,14 +0,0 @@
use std::process;
use log::error;
fn main() {
// initialize the logger
env_logger::init();
// handle errors returned from `run`
if let Err(e) = peach_stats::run() {
error!("Application error: {}", e);
process::exit(1);
}
}

111
peach-stats/src/sbot.rs Normal file
View File

@ -0,0 +1,111 @@
//! Systemd go-sbot process statistics retrieval functions and associated data types.
use std::{process::Command, str};
#[cfg(feature = "miniserde_support")]
use miniserde::{Deserialize, Serialize};
#[cfg(feature = "serde_support")]
use serde::{Deserialize, Serialize};
use crate::StatsError;
/// go-sbot process statistics.
#[derive(Debug)]
#[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
pub struct SbotStat {
/// Current process state.
pub state: Option<String>,
/// Current process boot state.
pub boot_state: Option<String>,
/// Current process memory usage in bytes.
pub memory: Option<u32>,
/// Uptime for the process (if state is `active`).
pub uptime: Option<String>,
/// Downtime for the process (if state is `inactive`).
pub downtime: Option<String>,
}
impl SbotStat {
/// Default builder for `SbotStat`.
fn default() -> Self {
Self {
state: None,
boot_state: None,
memory: None,
uptime: None,
downtime: None,
}
}
}
/// Retrieve statistics for the go-sbot systemd process by querying `systemctl`.
pub fn sbot_stats() -> Result<SbotStat, StatsError> {
let mut status = SbotStat::default();
let info_output = Command::new("sudo")
.arg("systemctl")
.arg("show")
.arg("go-sbot.service")
.arg("--no-page")
.output()
.map_err(StatsError::Systemctl)?;
let service_info = std::str::from_utf8(&info_output.stdout).map_err(StatsError::Utf8String)?;
for line in service_info.lines() {
if line.starts_with("ActiveState=") {
if let Some(state) = line.strip_prefix("ActiveState=") {
status.state = Some(state.to_string())
}
} else if line.starts_with("MemoryCurrent=") {
if let Some(memory) = line.strip_prefix("MemoryCurrent=") {
status.memory = memory.parse().ok()
}
}
}
let status_output = Command::new("sudo")
.arg("systemctl")
.arg("status")
.arg("go-sbot.service")
.output()
.map_err(StatsError::Systemctl)?;
let service_status = str::from_utf8(&status_output.stdout).map_err(StatsError::Utf8String)?;
for line in service_status.lines() {
// example of the output line we're looking for:
// `Loaded: loaded (/home/glyph/.config/systemd/user/go-sbot.service; enabled; vendor
// preset: enabled)`
if line.contains("Loaded:") {
let before_boot_state = line.find(';');
let after_boot_state = line.rfind(';');
if let (Some(start), Some(end)) = (before_boot_state, after_boot_state) {
// extract the enabled / disabled from the `Loaded: ...` line
// using the index of the first ';' + 2 and the last ';'
status.boot_state = Some(line[start + 2..end].to_string());
}
// example of the output line we're looking for here:
// `Active: active (running) since Mon 2022-01-24 16:22:51 SAST; 4min 14s ago`
} else if line.contains("Active:") {
let before_time = line.find(';');
let after_time = line.find(" ago");
if let (Some(start), Some(end)) = (before_time, after_time) {
// extract the uptime / downtime from the `Active: ...` line
// using the index of ';' + 2 and the index of " ago"
let time = Some(&line[start + 2..end]);
// if service is active then the `time` reading is uptime
if status.state == Some("active".to_string()) {
status.uptime = time.map(|t| t.to_string())
// if service is inactive then the `time` reading is downtime
} else if status.state == Some("inactive".to_string()) {
status.downtime = time.map(|t| t.to_string())
}
}
}
}
Ok(status)
}

View File

@ -1,14 +1,96 @@
//! System statistics retrieval functions and associated data types.
use std::result::Result;
use miniserde::json;
#[cfg(feature = "miniserde_support")]
use miniserde::{Deserialize, Serialize};
#[cfg(feature = "serde_support")]
use serde::{Deserialize, Serialize};
use probes::{cpu, disk_usage, load, memory};
use systemstat::{Platform, System};
use crate::error::StatError;
use crate::structs::{CpuStat, CpuStatPercentages, DiskUsage, LoadAverage, MemStat};
use crate::error::StatsError;
pub fn cpu_stats() -> Result<String, StatError> {
let cpu_stats = cpu::proc::read().map_err(|source| StatError::CpuStat { source })?;
/// CPU statistics.
#[derive(Debug)]
#[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
pub struct CpuStat {
/// Time spent running user space (application) code.
pub user: u64,
/// Time spent running kernel code.
pub system: u64,
/// Time spent doing nothing.
pub idle: u64,
/// Time spent running user space processes which have been niced.
pub nice: u64,
}
/// CPU statistics as percentages.
#[derive(Debug)]
#[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
pub struct CpuStatPercentages {
/// Time spent running user space (application) code.
pub user: f32,
/// Time spent running kernel code.
pub system: f32,
/// Time spent doing nothing.
pub idle: f32,
/// Time spent running user space processes which have been niced.
pub nice: f32,
}
/// Disk usage statistics.
#[derive(Debug)]
#[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
pub struct DiskUsage {
/// Filesystem device path.
pub filesystem: Option<String>,
/// Total amount of disk space as a number of 1,000 kilobyte blocks.
pub one_k_blocks: u64,
/// Total amount of used disk space as a number of 1,000 kilobyte blocks.
pub one_k_blocks_used: u64,
/// Total amount of free / available disk space as a number of 1,000 kilobyte blocks.
pub one_k_blocks_free: u64,
/// Total amount of used disk space as a percentage.
pub used_percentage: u32,
/// Mountpoint of the disk / partition.
pub mountpoint: String,
}
/// Load average statistics.
#[derive(Debug)]
#[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
pub struct LoadAverage {
/// Average computational work performed over the past minute.
pub one: f32,
/// Average computational work performed over the past five minutes.
pub five: f32,
/// Average computational work performed over the past fifteen minutes.
pub fifteen: f32,
}
/// Memory statistics.
#[derive(Debug)]
#[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
pub struct MemStat {
/// Total amount of physical memory in kilobytes.
pub total: u64,
/// Total amount of free / available physical memory in kilobytes.
pub free: u64,
/// Total amount of used physical memory in kilobytes.
pub used: u64,
}
/// Retrieve the current CPU statistics.
pub fn cpu_stats() -> Result<CpuStat, StatsError> {
let cpu_stats = cpu::proc::read().map_err(StatsError::CpuStat)?;
let s = cpu_stats.stat;
let cpu = CpuStat {
user: s.user,
@ -16,13 +98,13 @@ pub fn cpu_stats() -> Result<String, StatError> {
nice: s.nice,
idle: s.idle,
};
let json_cpu = json::to_string(&cpu);
Ok(json_cpu)
Ok(cpu)
}
pub fn cpu_stats_percent() -> Result<String, StatError> {
let cpu_stats = cpu::proc::read().map_err(|source| StatError::CpuStat { source })?;
/// Retrieve the current CPU statistics as percentages.
pub fn cpu_stats_percent() -> Result<CpuStatPercentages, StatsError> {
let cpu_stats = cpu::proc::read().map_err(StatsError::CpuStat)?;
let s = cpu_stats.stat.in_percentages();
let cpu = CpuStatPercentages {
user: s.user,
@ -30,13 +112,13 @@ pub fn cpu_stats_percent() -> Result<String, StatError> {
nice: s.nice,
idle: s.idle,
};
let json_cpu = json::to_string(&cpu);
Ok(json_cpu)
Ok(cpu)
}
pub fn disk_usage() -> Result<String, StatError> {
let disks = disk_usage::read().map_err(|source| StatError::DiskUsage { source })?;
/// Retrieve the current disk usage statistics for each available disk / partition.
pub fn disk_usage() -> Result<Vec<DiskUsage>, StatsError> {
let disks = disk_usage::read().map_err(StatsError::DiskUsage)?;
let mut disk_usages = Vec::new();
for d in disks {
let disk = DiskUsage {
@ -49,42 +131,39 @@ pub fn disk_usage() -> Result<String, StatError> {
};
disk_usages.push(disk);
}
let json_disks = json::to_string(&disk_usages);
Ok(json_disks)
Ok(disk_usages)
}
pub fn load_average() -> Result<String, StatError> {
let l = load::read().map_err(|source| StatError::LoadAvg { source })?;
/// Retrieve the current load average statistics.
pub fn load_average() -> Result<LoadAverage, StatsError> {
let l = load::read().map_err(StatsError::LoadAvg)?;
let load_avg = LoadAverage {
one: l.one,
five: l.five,
fifteen: l.fifteen,
};
let json_load_avg = json::to_string(&load_avg);
Ok(json_load_avg)
Ok(load_avg)
}
pub fn mem_stats() -> Result<String, StatError> {
let m = memory::read().map_err(|source| StatError::MemStat { source })?;
/// Retrieve the current memory usage statistics.
pub fn mem_stats() -> Result<MemStat, StatsError> {
let m = memory::read().map_err(StatsError::MemStat)?;
let mem = MemStat {
total: m.total(),
free: m.free(),
used: m.used(),
};
let json_mem = json::to_string(&mem);
Ok(json_mem)
Ok(mem)
}
pub fn uptime() -> Result<String, StatError> {
/// Retrieve the system uptime in seconds.
pub fn uptime() -> Result<u64, StatsError> {
let sys = System::new();
let uptime = sys
.uptime()
.map_err(|source| StatError::Uptime { source })?;
let uptime = sys.uptime().map_err(StatsError::Uptime)?;
let uptime_secs = uptime.as_secs();
let json_uptime = json::to_string(&uptime_secs);
Ok(json_uptime)
Ok(uptime_secs)
}

View File

@ -1,41 +0,0 @@
use miniserde::Serialize;
#[derive(Debug, Serialize)]
pub struct CpuStat {
pub user: u64,
pub system: u64,
pub idle: u64,
pub nice: u64,
}
#[derive(Debug, Serialize)]
pub struct CpuStatPercentages {
pub user: f32,
pub system: f32,
pub idle: f32,
pub nice: f32,
}
#[derive(Debug, Serialize)]
pub struct DiskUsage {
pub filesystem: Option<String>,
pub one_k_blocks: u64,
pub one_k_blocks_used: u64,
pub one_k_blocks_free: u64,
pub used_percentage: u32,
pub mountpoint: String,
}
#[derive(Debug, Serialize)]
pub struct LoadAverage {
pub one: f32,
pub five: f32,
pub fifteen: f32,
}
#[derive(Debug, Serialize)]
pub struct MemStat {
pub total: u64,
pub free: u64,
pub used: u64,
}

View File

@ -1,4 +0,0 @@
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
objcopy = { path ="aarch64-linux-gnu-objcopy" }
strip = { path ="aarch64-linux-gnu-strip" }

View File

@ -1,8 +1,5 @@
*.bak
static/icons/optimized/*
api_docs.md
js_docs.md
hashmap_notes
notes
target
**/*.rs.bk
leftovers

View File

@ -1,7 +1,7 @@
[package]
name = "peach-web"
version = "0.4.12"
authors = ["Andrew Reid <gnomad@cryptolab.net>"]
version = "0.6.21"
authors = ["Andrew Reid <gnomad@cryptolab.net>", "Max Fowler <max@mfowler.info>"]
edition = "2018"
description = "peach-web is a web application which provides a web interface for monitoring and interacting with the PeachCloud device. This allows administration of the single-board computer (ie. Raspberry Pi) running PeachCloud, as well as the ssb-server and related plugins."
homepage = "https://opencollective.com/peachcloud"
@ -21,12 +21,10 @@ maintainer-scripts="debian"
systemd-units = { unit-name = "peach-web" }
assets = [
["target/release/peach-web", "/usr/bin/", "755"],
["templates/**/*", "/usr/share/peach-web/templates/", "644"],
["static/*", "/usr/share/peach-web/static/", "644"],
["static/css/*", "/usr/share/peach-web/static/css/", "644"],
["static/icons/*", "/usr/share/peach-web/static/icons/", "644"],
["static/images/*", "/usr/share/peach-web/static/images/", "644"],
["static/js/*", "/usr/share/peach-web/static/js/", "644"],
["README.md", "/usr/share/doc/peach-web/README", "644"],
]
@ -35,20 +33,20 @@ travis-ci = { repository = "peachcloud/peach-web", branch = "master" }
maintenance = { status = "actively-developed" }
[dependencies]
async-std = "1.10"
base64 = "0.13"
chrono = "0.4"
dirs = "4.0"
env_logger = "0.8"
futures = "0.3"
golgi = { git = "https://git.coopcloud.tech/golgi-ssb/golgi.git" }
lazy_static = "1.4"
log = "0.4"
nest = "1.0.0"
openssl = { version = "0.10", features = ["vendored"] }
maud = "0.23"
peach-lib = { path = "../peach-lib" }
percent-encoding = "2.1.0"
regex = "1"
rocket = { version = "0.5.0-rc.1", features = ["json", "secrets"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
snafu = "0.6"
tera = { version = "1.12.1", features = ["builtins"] }
xdg = "2.2.0"
[dependencies.rocket_dyn_templates]
version = "0.1.0-rc.1"
features = ["tera"]
peach-network = { path = "../peach-network" }
peach-stats = { path = "../peach-stats" }
rouille = { version = "3.5", default-features = false }
temporary = "0.6"
vnstat_parse = "0.1.0"
xdg = "2.2"

View File

@ -1,18 +1,27 @@
# peach-web
[![Build Status](https://travis-ci.com/peachcloud/peach-web.svg?branch=master)](https://travis-ci.com/peachcloud/peach-web) ![Generic badge](https://img.shields.io/badge/version-0.4.12-<COLOR>.svg)
![Generic badge](https://img.shields.io/badge/version-0.6.18-<COLOR>.svg)
## Web Interface for PeachCloud
**peach-web** provides a web interface for the PeachCloud device. It serves static assets and exposes a JSON API for programmatic interactions.
**peach-web** provides a web interface for the PeachCloud device.
Initial development is focused on administration of the device itself, beginning with networking functionality, with SSB-related administration to be integrated at a later stage.
The web interface is primarily designed as a means of managing a Scuttlebutt pub. As such, it exposes the following features:
The peach-web stack currently consists of [Rocket](https://rocket.rs/) (Rust web framework), [Tera](http://tera.netlify.com/) (Rust template engine), HTML, CSS and JavaScript.
- Create a Scuttlebutt profile
- Follow, unfollow, block and unblock peers
- Generate pub invite codes
- Configure the sbot (hops, log directory, LAN discovery etc.)
- Send private messages
- Stop, start and restart the sbot
Additional features are focused on administration of the device itself. This includes networking functionality and device statistics.
The peach-web stack currently consists of [Rouille](https://crates.io/crates/rouille) (Rust web framework), [Maud](https://maud.lambda.xyz/) (Rust template engine), HTML, CSS and a tiny bit of JS. Scuttlebutt functionality is provided by [golgi](http://golgi.mycelial.technology).
_Note: This is a work-in-progress._
### Setup
## Setup
Clone the `peach-workspace` repo:
@ -23,37 +32,58 @@ Move into the repo and compile:
`cd peach-workspace/peach-web`
`cargo build --release`
Run the tests:
`cargo test`
Move back to the `peach-workspace` directory:
`cd ..`
Run the binary:
`./target/release/peach-web`
`../target/release/peach-web`
_Note: Networking functionality requires peach-network microservice to be running._
## Development Setup
### Environment
In order to test `peach-web` on a development machine you will need to have a running instance of `go-sbot` (please see the [go-sbot README](https://github.com/cryptoscope/ssb) for installation details). The `GO_SBOT_DATADIR` environment variable or corresponding config variable must be set to `/home/<user>/.ssb-go` and the `PEACH_HOMEDIR` variable must be set to `/home/<user>`. See the Configuration section below for more details.
**Deployment Mode**
The `go-sbot` process must be managed by `systemd` in order for it to be controlled via the `peach-web` web interface. Here is a basic `go-sbot.service` file:
The web application deployment mode is configured with the `ROCKET_ENV` environment variable:
```
[Unit]
Description=GoSSB server.
`export ROCKET_ENV=stage`
[Service]
ExecStart=/usr/bin/go-sbot
Environment="LIBRARIAN_WRITEALL=0"
Restart=always
Other deployment modes are `dev` and `prod`. Read the [Rocket Environment Configurations docs](https://rocket.rs/v0.5-rc/guide/configuration/#environment-variables) for further information.
[Install]
WantedBy=multi-user.target
```
**Authentication**
And a `sudoers` rule must be created to allow the `go-sbot.service` state to be modified without requiring a password. Here is an example `/etc/sudoers.d/peach-web` file:
Authentication is disabled in `development` mode and enabled by default when running the application in `production` mode. It can be disabled by setting the `ROCKET_DISABLE_AUTH` environment variable to `true`:
```
# Control go-sbot service without sudo passworkd
`export ROCKET_DISABLE_AUTH=true`
<user> ALL=(ALL) NOPASSWD: /bin/systemctl start go-sbot.service, /bin/systemctl restart go-sbot.service, /bin/systemctl stop go-sbot.service, /bin/systemctl enable go-sbot.service, /bin/systemctl disable go-sbot.service
```
**Logging**
## Configuration
By default, configuration variables are stored in `/var/lib/peachcloud/config.yml`. The variables in the file are updated by `peach-web` when changes are made to configurations via the web interface. Since `peach-web` has no database, all configurations are stored in this file.
A non-default configuration directory can be defined via the `PEACH_CONFIGDIR` environment variable or corresponding key in the `config.yml` file.
### Configuration Mode
The web application can be run with a minimal set of routes and functionality (PeachPub - a simple sbot manager) or with the full-suite of capabilities, including network management and access to device statistics (PeachCloud).
The application runs in PeachPub mode by default. The complete PeachCloud mode will be available once a large refactor is complete; it is not currently in working order so it's best to stick with PeachPub for now.
The running mode can be defined by setting the `STANDALONE_MODE` environment variable (`true` for PeachPub or `false` for PeachCloud). Alternatively, the desired mode can be set by modifying the PeachCloud configuration file.
### Authentication
Authentication is enabled by default when running the application. It can be disabled by setting the `DISABLE_AUTH` environment variable to `true`:
`export DISABLE_AUTH=true`
### Logging
Logging is made available with `env_logger`:
@ -61,7 +91,15 @@ Logging is made available with `env_logger`:
Other logging levels include `debug`, `warn` and `error`.
### Debian Packaging
### Dynamic DNS Configuration
Most users will want to use the default PeachCloud dynamic dns server.
If the config dyn_use_custom_server=false, then default values will be used.
If the config dyn_use_custom_server=true, then a value must also be set for dyn_dns_server_address (e.g. "http://peachdynserver.commoninternet.net").
This value is the URL of the instance of peach-dyndns-server that requests will be sent to for domain registration.
Using a custom value can here can be useful for testing.
## Debian Packaging
A `systemd` service file and Debian maintainer scripts are included in the `debian` directory, allowing `peach-web` to be easily bundled as a Debian package (`.deb`). The `cargo-deb` [crate](https://crates.io/crates/cargo-deb) can be used to achieve this.
@ -93,10 +131,10 @@ Remove configuration files (not removed with `apt-get remove`):
`sudo apt-get purge peach-web`
### Design
## Design
`peach-web` is built on the Rocket webserver and Tera templating engine. It presents a web interface for interacting with the device. HTML is rendered server-side. Request handlers call JSON-RPC microservices and serve HTML and assets. A JSON API is exposed for remote calls and dynamic client-side content updates (via plain JavaScript following unobstructive design principles). Each Tera template is passed a context object. In the case of Rust, this object is a `struct` and must implement `Serialize`. The fields of the context object are available in the context of the template to be rendered.
`peach-web` has been designed with simplicity and resource minimalism in mind. Both the dependencies used by the project, as well as the code itself, reflect these design priorities. The Rouille micro-web-framework and Maud templating engine have been used to present a web interface for interacting with the device. HTML is rendered server-side and request handlers call `peach-` libraries and serve HTML and assets. The optimised binary for `peach-web` can be compiled on a RPi 3 B+ in approximately 30 minutes.
### Licensing
## Licensing
AGPL-3.0

View File

@ -1,7 +0,0 @@
[development]
template_dir = "templates/"
disable_auth = true
[production]
template_dir = "templates/"
disable_auth = false

View File

@ -1,14 +1,10 @@
[Unit]
Description=Rocket web application for serving the PeachCloud web interface.
Description=Rouille web application for serving the PeachCloud web interface.
[Service]
User=peach-web
Group=www-data
User=peach
Group=peach
WorkingDirectory=/usr/share/peach-web
Environment="ROCKET_ENV=prod"
Environment="ROCKET_ADDRESS=127.0.0.1"
Environment="ROCKET_PORT=3000"
Environment="ROCKET_LOG=critical"
Environment="RUST_LOG=info"
ExecStart=/usr/bin/peach-web
Restart=always

View File

@ -2,13 +2,7 @@
set -e
# create user which peach-web runs as
adduser --quiet --system peach-web
usermod -g peach peach-web
# create secret passwords folder if it doesn't already exist
mkdir -p /var/lib/peachcloud/passwords
chown -R peach-web:peach /var/lib/peachcloud/passwords
chmod -R u+rwX,go+rX,go-w /var/lib/peachcloud/passwords
id -u peach &>/dev/null || adduser --quiet peach
# create nginx config
cat <<EOF > /etc/nginx/sites-enabled/default
@ -16,51 +10,29 @@ server {
listen 80 default_server;
server_name peach.local www.peach.local;
# nginx authentication
auth_basic "If you have forgotten your password visit: http://peach.local/send_password_reset/";
auth_basic_user_file /var/lib/peachcloud/passwords/htpasswd;
# remove trailing slash if found
rewrite ^/(.*)/$ /$1 permanent;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_pass http://127.0.0.1:8000;
}
# public routes
location /send_password_reset {
auth_basic off;
proxy_pass http://127.0.0.1:3000;
}
location /reset_password {
auth_basic off;
proxy_pass http://127.0.0.1:3000;
}
location /public/ {
auth_basic off;
proxy_pass http://127.0.0.1:3000;
}
location /js/ {
auth_basic off;
proxy_pass http://127.0.0.1:3000;
}
location /css/ {
auth_basic off;
proxy_pass http://127.0.0.1:3000;
}
location /icons/ {
auth_basic off;
proxy_pass http://127.0.0.1:3000;
}
}
EOF
cat <<EOF > /etc/sudoers.d/peach-web
# allow peach-web to run commands as peach-go-sbot without a password
peach-web ALL=(peach-go-sbot) NOPASSWD:ALL
# update sudoers to allow peach-web to stop and restart go-sbot.service
mkdir -p /etc/sudoers.d/
SYSTEMCTL=/bin/systemctl
START="${SYSTEMCTL} start go-sbot.service"
RESTART="${SYSTEMCTL} restart go-sbot.service"
STOP="${SYSTEMCTL} stop go-sbot.service"
ENABLE="${SYSTEMCTL} enable go-sbot.service"
DISABLE="${SYSTEMCTL} disable go-sbot.service"
cat <<EOF > /etc/sudoers.d/peach-web
peach ALL=(ALL) NOPASSWD: $START, $STOP, $RESTART, $ENABLE, $DISABLE
EOF
chmod 0440 /etc/sudoers.d/peach-web
# cargo deb automatically replaces this token below, see https://github.com/mmstick/cargo-deb/blob/master/systemd.md
#DEBHELPER#

31
peach-web/src/config.rs Normal file
View File

@ -0,0 +1,31 @@
//! Define the configuration parameters for the web application.
//!
//! These configs are loaded using peach-lib::config_manager which checks config keys from
//! three sources:
//! 1. from environmental variables
//! 2. from a configuration file
//! 3. from default values
use crate::error::PeachWebError;
use peach_lib::config_manager::get_config_value;
pub struct ServerConfig {
pub standalone_mode: bool,
pub disable_auth: bool,
pub addr: String,
pub port: String,
}
impl ServerConfig {
pub fn new() -> Result<ServerConfig, PeachWebError> {
// define default config values
let config = ServerConfig {
standalone_mode: get_config_value("STANDALONE_MODE")?.as_str() == "true",
disable_auth: get_config_value("DISABLE_AUTH")?.as_str() == "true",
addr: get_config_value("ADDR")?,
port: get_config_value("PORT")?,
};
Ok(config)
}
}

View File

@ -1,38 +1,93 @@
//! Custom error type representing all possible error variants for peach-web.
use std::io::Error as IoError;
use golgi::GolgiError;
use peach_lib::error::PeachError;
use peach_lib::{serde_json, serde_yaml};
use snafu::Snafu;
use serde_json::error::Error as JsonError;
use serde_yaml::Error as YamlError;
#[derive(Debug, Snafu)]
/// Custom error type encapsulating all possible errors for the web application.
#[derive(Debug)]
pub enum PeachWebError {
#[snafu(display("Error loading serde json"))]
Serde { source: serde_json::error::Error },
#[snafu(display("Error loading peach-config yaml"))]
YamlError { source: serde_yaml::Error },
#[snafu(display("{}", msg))]
FailedToRegisterDynDomain { msg: String },
#[snafu(display("{}: {}", source, msg))]
PeachLibError { source: PeachError, msg: String },
FailedToRegisterDynDomain(String),
Golgi(GolgiError),
HomeDir,
Io(IoError),
Json(JsonError),
OsString,
PeachLib { source: PeachError, msg: String },
Yaml(YamlError),
}
impl From<serde_json::error::Error> for PeachWebError {
fn from(err: serde_json::error::Error) -> PeachWebError {
PeachWebError::Serde { source: err }
impl std::error::Error for PeachWebError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match *self {
PeachWebError::FailedToRegisterDynDomain(_) => None,
PeachWebError::Golgi(ref source) => Some(source),
PeachWebError::HomeDir => None,
PeachWebError::Io(ref source) => Some(source),
PeachWebError::Json(ref source) => Some(source),
PeachWebError::OsString => None,
PeachWebError::PeachLib { ref source, .. } => Some(source),
PeachWebError::Yaml(ref source) => Some(source),
}
}
}
impl From<serde_yaml::Error> for PeachWebError {
fn from(err: serde_yaml::Error) -> PeachWebError {
PeachWebError::YamlError { source: err }
impl std::fmt::Display for PeachWebError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match *self {
PeachWebError::FailedToRegisterDynDomain(ref msg) => {
write!(f, "DYN DNS error: {}", msg)
}
PeachWebError::Golgi(ref source) => write!(f, "Golgi error: {}", source),
PeachWebError::HomeDir => write!(
f,
"Filesystem error: failed to determine home directory path"
),
PeachWebError::Io(ref source) => write!(f, "IO error: {}", source),
PeachWebError::Json(ref source) => write!(f, "Serde JSON error: {}", source),
PeachWebError::OsString => write!(
f,
"Filesystem error: failed to convert OsString to String for go-ssb directory path"
),
PeachWebError::PeachLib { ref source, .. } => write!(f, "{}", source),
PeachWebError::Yaml(ref source) => write!(f, "Serde YAML error: {}", source),
}
}
}
impl From<GolgiError> for PeachWebError {
fn from(err: GolgiError) -> PeachWebError {
PeachWebError::Golgi(err)
}
}
impl From<IoError> for PeachWebError {
fn from(err: IoError) -> PeachWebError {
PeachWebError::Io(err)
}
}
impl From<JsonError> for PeachWebError {
fn from(err: JsonError) -> PeachWebError {
PeachWebError::Json(err)
}
}
impl From<PeachError> for PeachWebError {
fn from(err: PeachError) -> PeachWebError {
PeachWebError::PeachLibError {
PeachWebError::PeachLib {
source: err,
msg: "".to_string(),
}
}
}
impl From<YamlError> for PeachWebError {
fn from(err: YamlError) -> PeachWebError {
PeachWebError::Yaml(err)
}
}

View File

@ -8,177 +8,116 @@
//! ## Design
//!
//! `peach-web` is written primarily in Rust and presents a web interface for
//! interacting with the device. The stack currently consists of Rocket (Rust
//! web framework), Tera (Rust template engine inspired by Jinja2 and the Django
//! template language), HTML, CSS and JavaScript. Additional functionality is
//! provided by JSON-RPC clients for the `peach-network` and `peach-stats`
//! microservices.
//!
//! HTML is rendered server-side. Request handlers call JSON-RPC microservices
//! and serve HTML and assets. A JSON API is exposed for remote calls and
//! dynamic client-side content updates via vanilla JavaScript following
//! unobstructive design principles. Each Tera template is passed a context
//! object. In the case of Rust, this object is a `struct` and must implement
//! `Serialize`. The fields of the context object are available in the context
//! of the template to be rendered.
#![feature(proc_macro_hygiene, decl_macro)]
//! interacting with the device. The stack currently consists of Rouille (Rust
//! micro-web-framework), Maud (an HTML template engine for Rust), HTML and
//! CSS.
mod config;
pub mod error;
pub mod routes;
#[cfg(test)]
mod tests;
mod private_router;
mod public_router;
mod routes;
mod templates;
pub mod utils;
use log::{error, info};
use std::process;
use std::{
collections::HashMap,
sync::{Mutex, RwLock},
};
use rocket::{catchers, fs::FileServer, routes, Build, Rocket};
use rocket_dyn_templates::Template;
use lazy_static::lazy_static;
use log::info;
use crate::routes::authentication::*;
use crate::routes::catchers::*;
use crate::routes::index::*;
use crate::routes::scuttlebutt::*;
use crate::routes::status::device::*;
use crate::routes::status::network::*;
use crate::routes::status::ping::*;
// crate-local dependencies
use config::ServerConfig;
use utils::theme::Theme;
use crate::routes::settings::admin::*;
use crate::routes::settings::dns::*;
use crate::routes::settings::menu::*;
use crate::routes::settings::network::*;
use crate::routes::settings::scuttlebutt::*;
pub type BoxError = Box<dyn std::error::Error>;
/// Create rocket instance & mount all routes.
fn init_rocket() -> Rocket<Build> {
rocket::build()
// GENERAL HTML ROUTES
.mount(
"/",
routes![
help,
home,
login,
login_post,
logout,
reboot_cmd,
shutdown_cmd,
power_menu,
settings_menu,
],
)
// STATUS HTML ROUTES
.mount("/status", routes![device_status, network_status])
// ADMIN SETTINGS HTML ROUTES
.mount(
"/settings/admin",
routes![
admin_menu,
configure_admin,
add_admin,
add_admin_post,
delete_admin_post,
change_password,
change_password_post,
reset_password,
reset_password_post,
forgot_password_page,
send_password_reset_post,
],
)
// NETWORK SETTINGS HTML ROUTES
.mount(
"/settings/network",
routes![
add_credentials,
connect_wifi,
configure_dns,
configure_dns_post,
disconnect_wifi,
deploy_ap,
deploy_client,
forget_wifi,
network_home,
add_ssid,
add_wifi,
network_detail,
wifi_list,
wifi_password,
wifi_set_password,
wifi_usage,
wifi_usage_alerts,
wifi_usage_reset,
],
)
// SCUTTLEBUTT SETTINGS HTML ROUTES
.mount("/settings/scuttlebutt", routes![ssb_settings_menu])
// SCUTTLEBUTT SOCIAL HTML ROUTES
.mount(
"/scuttlebutt",
routes![
peers, friends, follows, followers, blocks, profile, private, follow, unfollow,
block, publish,
],
)
// GENERAL JSON API ROUTES
.mount(
"/api/v1",
routes![ping_pong, ping_network, ping_oled, ping_stats,],
)
// ADMIN JSON API ROUTES
.mount(
"/api/v1/admin",
routes![
save_password_form_endpoint,
reset_password_form_endpoint,
reboot_device,
shutdown_device,
],
)
// NETWORK JSON API ROUTES
.mount(
"/api/v1/network",
routes![
activate_ap,
activate_client,
add_wifi_credentials,
connect_ap,
disconnect_ap,
forget_ap,
modify_password,
reset_data_total,
return_ip,
return_rssi,
return_ssid,
return_state,
return_status,
scan_networks,
update_wifi_alerts,
save_dns_configuration_endpoint,
],
)
.mount("/", FileServer::from("static"))
.register("/", catchers![not_found, internal_error, forbidden])
.attach(Template::fairing())
// load the application configuration and create the theme switcher
lazy_static! {
static ref SERVER_CONFIG: ServerConfig =
ServerConfig::new().expect("Failed to load rouille configuration values on server startup");
static ref THEME: RwLock<Theme> = RwLock::new(Theme::Light);
}
/// Launch the peach-web rocket server.
#[rocket::main]
async fn main() {
/// Wireless interface identifier.
pub const WLAN_IFACE: &str = "wlan0";
/// Access point interface identifier.
pub const AP_IFACE: &str = "ap0";
/// Session data for each authenticated client.
#[derive(Debug, Clone)]
pub struct SessionData {
_login: String,
}
/// Launch the peach-web server.
fn main() {
// initialize logger
env_logger::init();
// initialize rocket
info!("Initializing Rocket");
let rocket = init_rocket();
// set ip address / hostname and port for the webserver
// defaults to "127.0.0.1:8000"
let addr_and_port = format!("{}:{}", SERVER_CONFIG.addr, SERVER_CONFIG.port);
// launch rocket
info!("Launching Rocket");
if let Err(e) = rocket.launch().await {
error!("Error in Rocket application: {}", e);
process::exit(1);
}
// store the session data for each session and a hashmap that associates
// each session id with the data
// note: we are storing this data in memory. all sessions are erased when
// the program is restarted.
let sessions_storage: Mutex<HashMap<String, SessionData>> = Mutex::new(HashMap::new());
info!("Launching web server on {}", addr_and_port);
// the `start_server` starts listening forever on the given address
rouille::start_server(addr_and_port, move |request| {
// assign a unique id to each client (appends a cookie to the response
// with a name of "SID" and a duration of one hour (3600 seconds)
rouille::session::session(request, "SID", 3600, |session| {
// if the "DISABLE_AUTH" env var is true, authenticate the session
let mut session_data = if SERVER_CONFIG.disable_auth {
Some(SessionData {
_login: "success".to_string(),
})
// if the client already has an identifier from a previous request,
// try to load the existing session data. if successful, make a
// copy of the data in order to avoid locking the session for too
// long
} else if session.client_has_sid() {
sessions_storage.lock().unwrap().get(session.id()).cloned()
} else {
None
};
// pass the request to the public router
//
// the public router includes authentication-related routes which
// do not require the user to be authenticated (ie. login and reset
// password)
//
// if the user is already authenticated, their request will be
// passed to the private router by public_router::handle_route()
//
// we pass a mutable reference to the `Option<SessionData>` so that
// the function is free to modify it
let response = public_router::handle_route(request, &mut session_data);
// since the function call to `handle_route` can modify the session
// data, we have to store it back in the `sessions_storage` after
// the request has been handled
if let Some(data) = session_data {
sessions_storage
.lock()
.unwrap()
.insert(session.id().to_owned(), data);
} else if session.client_has_sid() {
// if the content of the `Option` was erased (ie. due to
// deauthentication on logout), remove the session from the
// storage. this is only done if the client already has an
// identifier, otherwise calling `session.id()` will assign one
sessions_storage.lock().unwrap().remove(session.id());
}
response
})
});
}

View File

@ -0,0 +1,279 @@
use rouille::{router, Request, Response};
use crate::{
routes, templates,
utils::{cookie::CookieResponse, flash::FlashResponse},
SessionData,
};
// TODO: add mount_peachcloud_routes()
// https://github.com/tomaka/rouille/issues/232#issuecomment-919225104
/// Define the PeachPub router.
///
/// Takes an incoming request and matches on the defined routes,
/// returning either a template or a redirect.
///
/// All of these routes require the user to be authenticated. See the
/// `public_router` for publically-accessible, authentication-related routes.
///
/// Excludes settings and status routes related to networking and the device
/// (memory, hard disk, CPU etc.).
pub fn mount_peachpub_routes(
request: &Request,
session_data: &mut Option<SessionData>,
) -> Response {
router!(request,
(GET) (/) => {
Response::html(routes::home::build_template())
// reset the back_url cookie each time we visit the homepage
.reset_cookie("back_url")
},
(GET) (/auth/change) => {
// build the html template
Response::html(routes::authentication::change::build_template(request))
// reset the flash msg cookies in the response object
.reset_flash()
},
(POST) (/auth/change) => {
routes::authentication::change::handle_form(request)
},
(GET) (/auth/logout) => {
routes::authentication::logout::deauthenticate(session_data)
},
(GET) (/guide) => {
Response::html(routes::guide::build_template())
},
(POST) (/scuttlebutt/block) => {
routes::scuttlebutt::block::handle_form(request)
},
(GET) (/scuttlebutt/blocks) => {
Response::html(routes::scuttlebutt::blocks::build_template())
// add a back_url cookie to allow the path of the back button
// to be set correctly on the /scuttlebutt/profile page
.add_cookie("back_url=/scuttlebutt/blocks")
},
(POST) (/scuttlebutt/follow) => {
routes::scuttlebutt::follow::handle_form(request)
},
(GET) (/scuttlebutt/follows) => {
Response::html(routes::scuttlebutt::follows::build_template())
// add a back_url cookie to allow the path of the back button
// to be set correctly on the /scuttlebutt/profile page
.add_cookie("back_url=/scuttlebutt/follows")
},
(GET) (/scuttlebutt/friends) => {
Response::html(routes::scuttlebutt::friends::build_template())
// add a back_url cookie to allow the path of the back button
// to be set correctly on the /scuttlebutt/profile page
.add_cookie("back_url=/scuttlebutt/friends")
},
(GET) (/scuttlebutt/invites) => {
Response::html(routes::scuttlebutt::invites::build_template(request))
.reset_flash()
},
(POST) (/scuttlebutt/invites) => {
routes::scuttlebutt::invites::handle_form(request)
},
(GET) (/scuttlebutt/peers) => {
Response::html(routes::scuttlebutt::peers::build_template())
},
(GET) (/scuttlebutt/private) => {
Response::html(routes::scuttlebutt::private::build_template(request, None))
},
(POST) (/scuttlebutt/private) => {
routes::scuttlebutt::private::handle_form(request)
},
(GET) (/scuttlebutt/private/{ssb_id: String}) => {
Response::html(routes::scuttlebutt::private::build_template(request, Some(ssb_id)))
},
(GET) (/scuttlebutt/profile) => {
Response::html(routes::scuttlebutt::profile::build_template(request, None))
.reset_flash()
},
(GET) (/scuttlebutt/profile/update) => {
Response::html(routes::scuttlebutt::profile_update::build_template(request))
.reset_flash()
},
(POST) (/scuttlebutt/profile/update) => {
routes::scuttlebutt::profile_update::handle_form(request)
},
(GET) (/scuttlebutt/profile/{ssb_id: String}) => {
Response::html(routes::scuttlebutt::profile::build_template(request, Some(ssb_id)))
},
(POST) (/scuttlebutt/publish) => {
routes::scuttlebutt::publish::handle_form(request)
},
(GET) (/scuttlebutt/search) => {
Response::html(routes::scuttlebutt::search::build_template(request))
.reset_flash()
},
(POST) (/scuttlebutt/search) => {
routes::scuttlebutt::search::handle_form(request)
// add a back_url cookie to allow the path of the back button
// to be set correctly on the /scuttlebutt/profile page
.add_cookie("back_url=/scuttlebutt/search")
},
(POST) (/scuttlebutt/unblock) => {
routes::scuttlebutt::unblock::handle_form(request)
},
(POST) (/scuttlebutt/unfollow) => {
routes::scuttlebutt::unfollow::handle_form(request)
},
(GET) (/settings) => {
Response::html(routes::settings::menu::build_template())
},
(GET) (/settings/admin) => {
Response::html(routes::settings::admin::menu::build_template())
},
(POST) (/settings/admin/add) => {
routes::settings::admin::add::handle_form(request)
},
(GET) (/settings/admin/configure) => {
Response::html(routes::settings::admin::configure::build_template(request))
.reset_flash()
},
(POST) (/settings/admin/delete) => {
routes::settings::admin::delete::handle_form(request)
},
(GET) (/settings/power) => {
Response::html(routes::settings::power::menu::build_template(request))
},
(GET) (/settings/power/reboot) => {
routes::settings::power::reboot::handle_reboot()
},
(GET) (/settings/power/shutdown) => {
routes::settings::power::shutdown::handle_shutdown()
},
(GET) (/settings/scuttlebutt) => {
Response::html(routes::settings::scuttlebutt::menu::build_template(request))
.reset_flash()
},
(GET) (/settings/scuttlebutt/restart) => {
routes::settings::scuttlebutt::restart::restart_sbot()
},
(GET) (/settings/scuttlebutt/start) => {
routes::settings::scuttlebutt::start::start_sbot()
},
(GET) (/settings/scuttlebutt/stop) => {
routes::settings::scuttlebutt::stop::stop_sbot()
},
(GET) (/settings/scuttlebutt/configure) => {
Response::html(routes::settings::scuttlebutt::configure::build_template(request))
.reset_flash()
},
(POST) (/settings/scuttlebutt/configure) => {
routes::settings::scuttlebutt::configure::handle_form(request, false)
},
(POST) (/settings/scuttlebutt/configure/restart) => {
routes::settings::scuttlebutt::configure::handle_form(request, true)
},
(GET) (/settings/scuttlebutt/configure/default) => {
routes::settings::scuttlebutt::default::write_config()
},
(GET) (/settings/network) => {
Response::html(routes::settings::network::menu::build_template(request)).reset_flash()
},
(GET) (/settings/network/dns) => {
Response::html(routes::settings::network::configure_dns::build_template(request)).reset_flash()
},
(POST) (/settings/network/dns) => {
routes::settings::network::configure_dns::handle_form(request)
},
(GET) (/settings/network/wifi) => {
Response::html(routes::settings::network::list_aps::build_template())
},
(GET) (/settings/network/wifi/add) => {
Response::html(routes::settings::network::add_ap::build_template(request, None)).reset_flash()
},
(POST) (/settings/network/wifi/add) => {
routes::settings::network::add_ap::handle_form(request)
},
(GET) (/settings/network/wifi/add/{ssid: String}) => {
Response::html(routes::settings::network::add_ap::build_template(request, Some(ssid))).reset_flash()
},
(GET) (/settings/network/wifi/modify) => {
Response::html(routes::settings::network::modify_ap::build_template(request, None)).reset_flash()
},
(POST) (/settings/network/wifi/modify) => {
routes::settings::network::modify_ap::handle_form(request)
},
(GET) (/settings/network/wifi/modify/{ssid: String}) => {
Response::html(routes::settings::network::modify_ap::build_template(request, Some(ssid))).reset_flash()
},
(GET) (/settings/network/wifi/{ssid: String}) => {
Response::html(routes::settings::network::ap_details::build_template(request, ssid))
},
(GET) (/settings/theme/{theme: String}) => {
routes::settings::theme::set_theme(theme)
},
(GET) (/status) => {
Response::html(routes::status::device::build_template())
},
(GET) (/status/scuttlebutt) => {
Response::html(routes::status::scuttlebutt::build_template()).add_cookie("back_url=/status/scuttlebutt")
},
(GET) (/status/network) => {
Response::html(routes::status::network::build_template())
},
// render the not_found template and set a 404 status code if none of
// the other blocks matches the request
_ => Response::html(templates::not_found::build_template()).with_status_code(404)
)
}

View File

@ -0,0 +1,103 @@
use log::{error, info};
use rouille::{router, Request, Response};
use crate::{
private_router, routes,
utils::{flash::FlashResponse, sbot},
SessionData,
};
/// Request handler.
///
/// Mount the fileservers for static assets and define the
/// publically-accessible routes (including per-route handlers). Includes
/// logging of all incoming requests.
///
/// If the request is for a private route (ie. a route requiring successful
/// authentication to view), check the authentication status of the user
/// by querying the `session_data`. If the user is authenticated, pass their
/// request to the private router. Otherwise, redirect them to the login page.
pub fn handle_route(request: &Request, session_data: &mut Option<SessionData>) -> Response {
// static file server
// matches on assets in the `static` directory
let static_response = rouille::match_assets(request, "static");
if static_response.is_success() {
return static_response;
}
// set the `.ssb-go` path in order to mount the blob fileserver
let ssb_path = sbot::get_go_ssb_path().expect("define ssb-go dir path");
let blobstore = format!("{}/blobs/sha256", ssb_path);
// blobstore file server
// removes the /blob url prefix and serves blobs from blobstore
// matches on assets in the `static` directory
if let Some(request) = request.remove_prefix("/blob") {
return rouille::match_assets(&request, &blobstore);
}
// get the current time (for logging purposes)
let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S%.6f");
// define the success logger for incoming requests
let log_ok = |req: &Request, _resp: &Response, _elap: std::time::Duration| {
info!("{} {} {}", now, req.method(), req.raw_url());
};
// define the error logger for incoming requests
let log_err = |req: &Request, _elap: std::time::Duration| {
error!(
"{} Handler panicked: {} {}",
now,
req.method(),
req.raw_url()
);
};
// instantiate request logging
rouille::log_custom(request, log_ok, log_err, || {
// handle the routes which are always accessible (ie. whether logged-in
// or not)
router!(request,
(GET) (/auth/forgot) => {
Response::html(routes::authentication::forgot::build_template(request))
.reset_flash()
},
(GET) (/auth/login) => {
Response::html(routes::authentication::login::build_template(request))
.reset_flash()
},
(POST) (/auth/login) => {
routes::authentication::login::handle_form(request, session_data)
},
(GET) (/auth/reset) => {
Response::html(routes::authentication::reset::build_template(request))
.reset_flash()
},
(POST) (/auth/reset) => {
routes::authentication::reset::handle_form(request)
},
(POST) (/auth/temporary) => {
routes::authentication::temporary::handle_form()
},
_ => {
// now that we handled all the routes that are accessible in all
// circumstances, we check that the user is logged in before proceeding
if let Some(_session) = session_data.as_ref() {
// logged in:
// mount the routes which require authentication to view
private_router::mount_peachpub_routes(request, session_data)
} else {
// not logged in:
Response::redirect_303("/auth/login")
}
}
)
})
}

View File

@ -1,437 +0,0 @@
use log::info;
use rocket::form::{Form, FromForm};
use rocket::http::{Cookie, CookieJar, Status};
use rocket::request::{self, FlashMessage, FromRequest, Request};
use rocket::response::{Flash, Redirect};
use rocket::serde::{
json::{Json, Value},
Deserialize, Serialize,
};
use rocket::{get, post, Config};
use rocket_dyn_templates::Template;
use peach_lib::error::PeachError;
use peach_lib::password_utils;
use crate::error::PeachWebError;
use crate::utils::{build_json_response, TemplateOrRedirect};
// HELPERS AND STRUCTS FOR AUTHENTICATION WITH COOKIES
pub const AUTH_COOKIE_KEY: &str = "peachweb_auth";
pub const ADMIN_USERNAME: &str = "admin";
/// Note: Currently we use an empty struct for the Authenticated request guard
/// because there is only one user to be authenticated, and no data needs to be stored here.
/// In a multi-user authentication scheme, we would store the user_id in this struct,
/// and retrieve the correct user via the user_id stored in the cookie.
pub struct Authenticated;
#[derive(Debug)]
pub enum LoginError {
UserNotLoggedIn,
}
/// Request guard which returns an empty Authenticated struct from the request
/// if and only if the user has a cookie which proves they are authenticated with peach-web.
///
/// Note that cookies.get_private uses encryption, which means that this private cookie
/// cannot be inspected, tampered with, or manufactured by clients.
#[rocket::async_trait]
impl<'r> FromRequest<'r> for Authenticated {
type Error = LoginError;
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
// check for `disable_auth` config value; set to `false` if unset
// can be set via the `ROCKET_DISABLE_AUTH` environment variable
// - env var, if set, takes precedence over value defined in `Rocket.toml`
let authentication_is_disabled: bool = match Config::figment().find_value("disable_auth") {
// deserialize the boolean value; set to `false` if an error is encountered
Ok(value) => value.deserialize().unwrap_or(false),
Err(_) => false,
};
if authentication_is_disabled {
let auth = Authenticated {};
request::Outcome::Success(auth)
} else {
let authenticated = req
.cookies()
.get_private(AUTH_COOKIE_KEY)
.and_then(|cookie| cookie.value().parse().ok())
.map(|_value: String| Authenticated {});
match authenticated {
Some(auth) => request::Outcome::Success(auth),
None => request::Outcome::Failure((Status::Forbidden, LoginError::UserNotLoggedIn)),
}
}
}
}
// HELPERS AND ROUTES FOR /login
#[derive(Debug, Serialize)]
pub struct LoginContext {
pub back: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub title: Option<String>,
}
impl LoginContext {
pub fn build() -> LoginContext {
LoginContext {
back: None,
flash_name: None,
flash_msg: None,
title: None,
}
}
}
#[get("/login")]
pub fn login(flash: Option<FlashMessage>) -> Template {
let mut context = LoginContext::build();
context.back = Some("/".to_string());
context.title = Some("Login".to_string());
// check to see if there is a flash message to display
if let Some(flash) = flash {
// add flash message contents to the context object
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
};
Template::render("login", &context)
}
#[derive(Debug, Deserialize, FromForm)]
pub struct LoginForm {
pub username: String,
pub password: String,
}
/// Takes in a LoginForm and returns Ok(()) if username and password
/// are correct to authenticate with peach-web.
///
/// Note: currently there is only one user, and the username should always
/// be "admin".
pub fn verify_login_form(login_form: LoginForm) -> Result<(), PeachError> {
password_utils::verify_password(&login_form.password)
}
#[post("/login", data = "<login_form>")]
pub fn login_post(login_form: Form<LoginForm>, cookies: &CookieJar<'_>) -> TemplateOrRedirect {
let result = verify_login_form(login_form.into_inner());
match result {
Ok(_) => {
// if successful login, add a cookie indicating the user is authenticated
// and redirect to home page
// NOTE: since we currently have just one user, the value of the cookie
// is just admin (this is arbitrary).
// If we had multiple users, we could put the user_id here.
cookies.add_private(Cookie::new(AUTH_COOKIE_KEY, ADMIN_USERNAME));
TemplateOrRedirect::Redirect(Redirect::to("/"))
}
Err(_) => {
// if unsuccessful login, render /login page again
let mut context = LoginContext::build();
context.back = Some("/".to_string());
context.title = Some("Login".to_string());
context.flash_name = Some("error".to_string());
let flash_msg = "Invalid password".to_string();
context.flash_msg = Some(flash_msg);
TemplateOrRedirect::Template(Template::render("login", &context))
}
}
}
// HELPERS AND ROUTES FOR /logout
#[get("/logout")]
pub fn logout(cookies: &CookieJar<'_>) -> Flash<Redirect> {
// logout authenticated user
info!("Attempting deauthentication of user.");
cookies.remove_private(Cookie::named(AUTH_COOKIE_KEY));
Flash::success(Redirect::to("/login"), "Logged out")
}
// HELPERS AND ROUTES FOR /reset_password
#[derive(Debug, Deserialize, FromForm)]
pub struct ResetPasswordForm {
pub temporary_password: String,
pub new_password1: String,
pub new_password2: String,
}
#[derive(Debug, Serialize)]
pub struct ResetPasswordContext {
pub back: Option<String>,
pub title: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
}
impl ResetPasswordContext {
pub fn build() -> ResetPasswordContext {
ResetPasswordContext {
back: None,
title: None,
flash_name: None,
flash_msg: None,
}
}
}
#[derive(Debug, Serialize)]
pub struct ChangePasswordContext {
pub back: Option<String>,
pub title: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
}
impl ChangePasswordContext {
pub fn build() -> ChangePasswordContext {
ChangePasswordContext {
back: None,
title: None,
flash_name: None,
flash_msg: None,
}
}
}
/// Verify, validate and save the submitted password. This function is publicly exposed for users who have forgotten their password.
pub fn save_reset_password_form(password_form: ResetPasswordForm) -> Result<(), PeachWebError> {
info!(
"reset password!: {} {} {}",
password_form.temporary_password, password_form.new_password1, password_form.new_password2
);
password_utils::verify_temporary_password(&password_form.temporary_password)?;
// if the previous line did not throw an error, then the secret_link is correct
password_utils::validate_new_passwords(
&password_form.new_password1,
&password_form.new_password2,
)?;
// if the previous line did not throw an error, then the new password is valid
password_utils::set_new_password(&password_form.new_password1)?;
Ok(())
}
/// Password reset request handler. This route is used by a user who is not logged in
/// and is specifically for users who have forgotten their password.
#[get("/reset_password")]
pub fn reset_password(flash: Option<FlashMessage>) -> Template {
let mut context = ResetPasswordContext::build();
context.back = Some("/".to_string());
context.title = Some("Reset Password".to_string());
// check to see if there is a flash message to display
if let Some(flash) = flash {
// add flash message contents to the context object
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
};
Template::render("settings/admin/reset_password", &context)
}
/// Password reset form request handler. This route is used by a user who is not logged in
/// and is specifically for users who have forgotten their password.
#[post("/reset_password", data = "<reset_password_form>")]
pub fn reset_password_post(reset_password_form: Form<ResetPasswordForm>) -> Template {
let result = save_reset_password_form(reset_password_form.into_inner());
match result {
Ok(_) => {
let mut context = ChangePasswordContext::build();
context.back = Some("/".to_string());
context.title = Some("Reset Password".to_string());
context.flash_name = Some("success".to_string());
let flash_msg = "New password is now saved. Return home to login".to_string();
context.flash_msg = Some(flash_msg);
Template::render("settings/admin/reset_password", &context)
}
Err(err) => {
let mut context = ChangePasswordContext::build();
// set back icon link to network route
context.back = Some("/".to_string());
context.title = Some("Reset Password".to_string());
context.flash_name = Some("error".to_string());
context.flash_msg = Some(format!("Failed to reset password: {}", err));
Template::render("settings/admin/reset_password", &context)
}
}
}
/// JSON password reset form request handler. This route is used by a user who is not logged in
/// and is specifically for users who have forgotten their password.
#[post("/reset_password", data = "<reset_password_form>")]
pub fn reset_password_form_endpoint(reset_password_form: Json<ResetPasswordForm>) -> Value {
let result = save_reset_password_form(reset_password_form.into_inner());
match result {
Ok(_) => {
let status = "success".to_string();
let msg = "New password is now saved. Return home to login.".to_string();
build_json_response(status, None, Some(msg))
}
Err(err) => {
let status = "error".to_string();
let msg = format!("{}", err);
build_json_response(status, None, Some(msg))
}
}
}
// HELPERS AND ROUTES FOR /send_password_reset
#[derive(Debug, Serialize)]
pub struct SendPasswordResetContext {
pub back: Option<String>,
pub title: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
}
impl SendPasswordResetContext {
pub fn build() -> SendPasswordResetContext {
SendPasswordResetContext {
back: None,
title: None,
flash_name: None,
flash_msg: None,
}
}
}
/// Page for users who have forgotten their password.
/// This route is used by a user who is not logged in
/// to initiate the sending of a new password reset.
#[get("/forgot_password")]
pub fn forgot_password_page(flash: Option<FlashMessage>) -> Template {
let mut context = SendPasswordResetContext::build();
context.back = Some("/".to_string());
context.title = Some("Send Password Reset".to_string());
// check to see if there is a flash message to display
if let Some(flash) = flash {
// add flash message contents to the context object
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
};
Template::render("settings/admin/forgot_password", &context)
}
/// Send password reset request handler. This route is used by a user who is not logged in
/// and is specifically for users who have forgotten their password. A successful request results
/// in a Scuttlebutt private message being sent to the account of the device admin.
#[post("/send_password_reset")]
pub fn send_password_reset_post() -> Template {
info!("++ send password reset post");
let result = password_utils::send_password_reset();
match result {
Ok(_) => {
let mut context = ChangePasswordContext::build();
context.back = Some("/".to_string());
context.title = Some("Send Password Reset".to_string());
context.flash_name = Some("success".to_string());
let flash_msg =
"A password reset link has been sent to the admin of this device".to_string();
context.flash_msg = Some(flash_msg);
Template::render("settings/admin/forgot_password", &context)
}
Err(err) => {
let mut context = ChangePasswordContext::build();
context.back = Some("/".to_string());
context.title = Some("Send Password Reset".to_string());
context.flash_name = Some("error".to_string());
context.flash_msg = Some(format!("Failed to send password reset link: {}", err));
Template::render("settings/admin/forgot_password", &context)
}
}
}
// HELPERS AND ROUTES FOR /settings/change_password
#[derive(Debug, Deserialize, FromForm)]
pub struct PasswordForm {
pub old_password: String,
pub new_password1: String,
pub new_password2: String,
}
/// Password save form request handler. This function is for use by a user who is already logged in to change their password.
pub fn save_password_form(password_form: PasswordForm) -> Result<(), PeachWebError> {
info!(
"change password!: {} {} {}",
password_form.old_password, password_form.new_password1, password_form.new_password2
);
password_utils::verify_password(&password_form.old_password)?;
// if the previous line did not throw an error, then the old password is correct
password_utils::validate_new_passwords(
&password_form.new_password1,
&password_form.new_password2,
)?;
// if the previous line did not throw an error, then the new password is valid
password_utils::set_new_password(&password_form.new_password1)?;
Ok(())
}
/// Change password request handler. This is used by a user who is already logged in.
#[get("/change_password")]
pub fn change_password(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
let mut context = ChangePasswordContext::build();
// set back icon link to network route
context.back = Some("/settings/admin".to_string());
context.title = Some("Change Password".to_string());
// check to see if there is a flash message to display
if let Some(flash) = flash {
// add flash message contents to the context object
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
};
Template::render("settings/admin/change_password", &context)
}
/// Change password form request handler. This route is used by a user who is already logged in.
#[post("/change_password", data = "<password_form>")]
pub fn change_password_post(password_form: Form<PasswordForm>, _auth: Authenticated) -> Template {
let result = save_password_form(password_form.into_inner());
match result {
Ok(_) => {
let mut context = ChangePasswordContext::build();
// set back icon link to network route
context.back = Some("/settings/admin".to_string());
context.title = Some("Change Password".to_string());
context.flash_name = Some("success".to_string());
context.flash_msg = Some("New password is now saved".to_string());
// template_dir is set in Rocket.toml
Template::render("settings/admin/change_password", &context)
}
Err(err) => {
let mut context = ChangePasswordContext::build();
// set back icon link to network route
context.back = Some("/settings/admin".to_string());
context.title = Some("Change Password".to_string());
context.flash_name = Some("error".to_string());
context.flash_msg = Some(format!("Failed to save new password: {}", err));
Template::render("settings/admin/change_password", &context)
}
}
}
/// JSON change password form request handler.
#[post("/change_password", data = "<password_form>")]
pub fn save_password_form_endpoint(
password_form: Json<PasswordForm>,
_auth: Authenticated,
) -> Value {
let result = save_password_form(password_form.into_inner());
match result {
Ok(_) => {
let status = "success".to_string();
let msg = "Your password was successfully changed".to_string();
build_json_response(status, None, Some(msg))
}
Err(err) => {
let status = "error".to_string();
let msg = format!("{}", err);
build_json_response(status, None, Some(msg))
}
}
}

View File

@ -0,0 +1,116 @@
use log::info;
use maud::{html, PreEscaped};
use peach_lib::password_utils;
use rouille::{post_input, try_or_400, Request, Response};
use crate::{
error::PeachWebError,
templates,
utils::{
flash::{FlashRequest, FlashResponse},
theme,
},
};
// HELPER AND ROUTES FOR /auth/change (GET and POST)
/// Password change form template builder.
pub fn build_template(request: &Request) -> PreEscaped<String> {
// check for flash cookies; will be (None, None) if no flash cookies are found
let (flash_name, flash_msg) = request.retrieve_flash();
let form_template = html! {
(PreEscaped("<!-- CHANGE PASSWORD FORM -->"))
div class="card center" {
form id="changePassword" class="center" action="/auth/change" method="post" {
div style="display: flex; flex-direction: column; margin-bottom: 1rem;" {
(PreEscaped("<!-- input for current password -->"))
label for="currentPassword" class="center label-small font-gray" style="width: 80%;" { "CURRENT PASSWORD" }
input id="currentPassword" class="center input" name="current_password" type="password" title="Current password" autofocus;
(PreEscaped("<!-- input for new password -->"))
label for="newPassword" class="center label-small font-gray" style="width: 80%;" { "NEW PASSWORD" }
input id="newPassword" class="center input" name="new_password1" type="password" title="New password";
(PreEscaped("<!-- input for duplicate new password -->"))
label for="newPasswordDuplicate" class="center label-small font-gray" style="width: 80%;" { "RE-ENTER NEW PASSWORD" }
input id="newPasswordDuplicate" class="center input" name="new_password2" type="password" title="New password duplicate";
(PreEscaped("<!-- save (form submission) button -->"))
input id="savePassword" class="button button-primary center" title="Add" type="submit" value="Save";
a class="button button-secondary center" href="/settings/admin" title="Cancel"{ "Cancel" }
}
}
// render flash message if cookies were found in the request
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
(PreEscaped("<!-- FLASH MESSAGE -->"))
(templates::flash::build_template(name, msg))
}
}
};
// wrap the nav bars around the settings menu template content
// parameters are template, title and back url
let body =
templates::nav::build_template(form_template, "Change Password", Some("/settings/admin"));
// query the current theme so we can pass it into the base template builder
let theme = theme::get_theme();
// render the base template with the provided body
templates::base::build_template(body, theme)
}
/// Verify, validate and set a new password, overwriting the current password.
pub fn save_password(
current_password: &str,
new_password1: &str,
new_password2: &str,
) -> Result<(), PeachWebError> {
info!(
"Attempting password change: {} {} {}",
current_password, new_password1, new_password2
);
// check that the supplied value matches the actual current password
password_utils::verify_password(current_password)?;
// ensure that both new_password values match
password_utils::validate_new_passwords(new_password1, new_password2)?;
// hash the password and save the hash to file
password_utils::set_new_password(new_password1)?;
Ok(())
}
/// Parse current and new passwords from the submitted form, save the new
/// password hash to file (`/var/lib/peachcloud/config.yml`) and redirect
/// to the change password form URL.
pub fn handle_form(request: &Request) -> Response {
// query the request body for form data
// return a 400 error if the admin_id field is missing
let data = try_or_400!(post_input!(request, {
current_password: String,
new_password1: String,
new_password2: String,
}));
// save submitted admin id to file
// match on the result and set flash name and msg accordingly
let (flash_name, flash_msg) = match save_password(
&data.current_password,
&data.new_password1,
&data.new_password2,
) {
Ok(_) => (
// <cookie-name>=<cookie-value>
"flash_name=success".to_string(),
"flash_msg=New password has been saved".to_string(),
),
Err(err) => (
"flash_name=error".to_string(),
format!("flash_msg=Failed to save new password: {}", err),
),
};
// set the flash cookie headers and redirect to the change password page
Response::redirect_303("/auth/change").add_flash(flash_name, flash_msg)
}

View File

@ -0,0 +1,56 @@
use maud::{html, PreEscaped};
use rouille::Request;
use crate::{
templates,
utils::{flash::FlashRequest, theme},
};
// ROUTE: /auth/forgot
/// Forgot password template builder.
pub fn build_template(request: &Request) -> PreEscaped<String> {
// check for flash cookies; will be (None, None) if no flash cookies are found
let (flash_name, flash_msg) = request.retrieve_flash();
let password_reset_template = html! {
(PreEscaped("<!-- PASSWORD RESET REQUEST CARD -->"))
div class="card center" {
div class="capsule capsule-container border-info" {
p class="card-text" {
"Click the 'Send Temporary Password' button to send a new temporary password which can be used to change your device password."
}
p class="card-text" style="margin-top: 1rem;" {
"The temporary password will be sent in an SSB private message to the admin of this device."
}
p class="card-text" style="margin-top: 1rem;" {
"Once you have the temporary password, click the 'Set New Password' button to reach the password reset page."
}
}
form id="sendPasswordReset" action="/auth/temporary" method="post" {
div id="buttonDiv" {
input class="button button-primary center" style="margin-top: 1rem;" type="submit" value="Send Temporary Password" title="Send temporary password to Scuttlebutt admin(s)";
a href="/auth/reset" class="button button-primary center" title="Set a new password using the temporary password" {
"Set New Password"
}
}
}
// render flash message if cookies were found in the request
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
(PreEscaped("<!-- FLASH MESSAGE -->"))
(templates::flash::build_template(name, msg))
}
}
};
// wrap the nav bars around the settings menu template content
// parameters are template, title and back url
let body =
templates::nav::build_template(password_reset_template, "Send Password Reset", Some("/"));
// query the current theme so we can pass it into the base template builder
let theme = theme::get_theme();
// render the base template with the provided body
templates::base::build_template(body, theme)
}

View File

@ -0,0 +1,87 @@
use log::debug;
use maud::{html, PreEscaped};
use peach_lib::password_utils;
use rouille::{post_input, try_or_400, Request, Response};
use crate::{
templates,
utils::{
flash::{FlashRequest, FlashResponse},
theme,
},
SessionData,
};
// HELPER AND ROUTES FOR /auth/login (GET and POST)
/// Login form template builder.
pub fn build_template(request: &Request) -> PreEscaped<String> {
// check for flash cookies; will be (None, None) if no flash cookies are found
let (flash_name, flash_msg) = request.retrieve_flash();
let form_template = html! {
(PreEscaped("<!-- LOGIN FORM -->"))
div class="card center" {
form id="login_form" class="center" action="/auth/login" method="post" {
div style="display: flex; flex-direction: column; margin-bottom: 1rem;" {
(PreEscaped("<!-- input for password -->"))
label for="password" class="center label-small font-gray" style="width: 80%;" { "PASSWORD" }
input id="password" name="password" class="center input" type="password" title="Password for given username" autofocus;
(PreEscaped("<!-- login (form submission) button -->"))
input id="loginUser" class="button button-primary center" title="Login" type="submit" value="Login";
div class="center-text" style="margin-top: 1rem;" {
a href="/auth/forgot" class="label-small link font-gray" { "Forgot Password?" }
}
}
}
// render flash message if cookies were found in the request
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
(PreEscaped("<!-- FLASH MESSAGE -->"))
(templates::flash::build_template(name, msg))
}
}
};
// wrap the nav bars around the settings menu template content
// parameters are template, title and back url
let body = templates::nav::build_template(form_template, "Login", Some("/"));
// query the current theme so we can pass it into the base template builder
let theme = theme::get_theme();
// render the base template with the provided body
templates::base::build_template(body, theme)
}
/// Parse and verify the submitted password. If verification succeeds, set the
/// auth session cookie and redirect to the home page. If not, set a flash
/// message and redirect to the login page.
pub fn handle_form(request: &Request, session_data: &mut Option<SessionData>) -> Response {
// query the request body for form data
// return a 400 error if the admin_id field is missing
let data = try_or_400!(post_input!(request, { password: String }));
match password_utils::verify_password(&data.password) {
Ok(_) => {
debug!("Successful login attempt");
// if password verification is successful, write to `session_data`
// to authenticate the user
*session_data = Some(SessionData {
_login: "success".to_string(),
});
Response::redirect_303("/")
}
Err(err) => {
debug!("Unsuccessful login attempt");
let err_msg = format!("Invalid password: {}", err);
let (flash_name, flash_msg) = (
"flash_name=error".to_string(),
format!("flash_msg={}", err_msg),
);
// if unsuccessful login, render /login page again
Response::redirect_303("/auth/login").add_flash(flash_name, flash_msg)
}
}
}

View File

@ -0,0 +1,23 @@
use log::info;
use rouille::Response;
use crate::{utils::flash::FlashResponse, SessionData};
// ROUTE: /auth/logout (GET)
/// Deauthenticate the logged-in user by erasing the session data.
/// Redirect to the login page.
pub fn deauthenticate(session_data: &mut Option<SessionData>) -> Response {
info!("Attempting deauthentication of user.");
// erase the content of `session_data` to deauthenticate the user
*session_data = None;
let (flash_name, flash_msg) = (
"flash_name=success".to_string(),
"flash_msg=Logged out".to_string(),
);
// set the flash cookie headers and redirect to the login page
Response::redirect_303("/auth/login".to_string()).add_flash(flash_name, flash_msg)
}

View File

@ -0,0 +1,6 @@
pub mod change;
pub mod forgot;
pub mod login;
pub mod logout;
pub mod reset;
pub mod temporary;

View File

@ -0,0 +1,114 @@
use log::info;
use maud::{html, PreEscaped};
use peach_lib::password_utils;
use rouille::{post_input, try_or_400, Request, Response};
use crate::{
error::PeachWebError,
templates,
utils::{
flash::{FlashRequest, FlashResponse},
theme,
},
};
// HELPER AND ROUTES FOR /auth/reset (GET and POST)
/// Password reset form template builder.
pub fn build_template(request: &Request) -> PreEscaped<String> {
// check for flash cookies; will be (None, None) if no flash cookies are found
let (flash_name, flash_msg) = request.retrieve_flash();
let form_template = html! {
(PreEscaped("<!-- RESET PASSWORD PAGE -->"))
div class="card center" {
form id="resetPassword" class="center" action="/auth/reset" method="post" {
div style="display: flex; flex-direction: column; margin-bottom: 1rem;" {
(PreEscaped("<!-- input for temporary password -->"))
label for="temporaryPassword" class="center label-small font-gray" style="width: 80%;" { "TEMPORARY PASSWORD" }
input id="temporaryPassword" class="center input" name="temporary_password" type="password" title="Temporary password" autofocus;
(PreEscaped("<!-- input for new password1 -->"))
label for="newPassword" class="center label-small font-gray" style="width: 80%;" { "NEW PASSWORD" }
input id="newPassword" class="center input" name="new_password1" type="password" title="New password";
(PreEscaped("<!-- input for duplicate new password -->"))
label for="newPasswordDuplicate" class="center label-small font-gray" style="width: 80%;" { "RE-ENTER NEW PASSWORD" }
input id="newPasswordDuplicate" class="center input" name="new_password2" type="password" title="New password duplicate";
(PreEscaped("<!-- save (form submission) button -->"))
input id="savePassword" class="button button-primary center" title="Add" type="submit" value="Save";
a class="button button-secondary center" href="/settings/admin" title="Cancel"{ "Cancel" }
}
}
// render flash message if cookies were found in the request
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
(PreEscaped("<!-- FLASH MESSAGE -->"))
(templates::flash::build_template(name, msg))
}
}
};
// wrap the nav bars around the settings menu template content
// parameters are template, title and back url
let body =
templates::nav::build_template(form_template, "Reset Password", Some("/settings/admin"));
// query the current theme so we can pass it into the base template builder
let theme = theme::get_theme();
// render the base template with the provided body
templates::base::build_template(body, theme)
}
/// Verify, validate and set a new password, overwriting the current password.
pub fn save_password(
temporary_password: &str,
new_password1: &str,
new_password2: &str,
) -> Result<(), PeachWebError> {
info!(
"Attempting password reset: {} {} {}",
temporary_password, new_password1, new_password2
);
// check that the supplied value matches the actual temporary password
password_utils::verify_temporary_password(temporary_password)?;
// ensure that both new_password values match
password_utils::validate_new_passwords(new_password1, new_password2)?;
// hash the password and save the hash to file
password_utils::set_new_password(new_password1)?;
Ok(())
}
/// Parse temporary and new passwords from the submitted form, save the new
/// password hash to file (`/var/lib/peachcloud/config.yml`) and redirect
/// to the reset password form URL.
pub fn handle_form(request: &Request) -> Response {
// query the request body for form data
// return a 400 error if the admin_id field is missing
let data = try_or_400!(post_input!(request, {
temporary_password: String,
new_password1: String,
new_password2: String,
}));
// save submitted admin id to file
let (flash_name, flash_msg) = match save_password(
&data.temporary_password,
&data.new_password1,
&data.new_password2,
) {
Ok(_) => (
"flash_name=success".to_string(),
"flash_msg=New password has been saved. Return home to login".to_string(),
),
Err(err) => (
"flash_name=error".to_string(),
format!("flash_msg=Failed to reset password: {}", err),
),
};
// redirect to the configure admin page
Response::redirect_303("/auth/reset").add_flash(flash_name, flash_msg)
}

View File

@ -0,0 +1,42 @@
use log::debug;
use peach_lib::password_utils;
use rouille::Response;
use crate::utils::flash::FlashResponse;
// ROUTE: /auth/temporary (POST)
/// Send a temporary password as a Scuttlebutt private message to the admin(s).
///
/// This route is used by a user who is not logged in and is specifically for
/// users who have forgotten their password. A successful request results
/// in a Scuttlebutt private message being sent to the account of the device
/// admin.
///
/// Redirects to the Send Password Reset page a flash message describing the
/// outcome of the action (may be successful or unsuccessful).
pub fn handle_form() -> Response {
// save submitted admin id to file
let (flash_name, flash_msg) = match password_utils::send_password_reset() {
Ok(_) => {
debug!("Sent temporary password to device admin(s)");
(
"flash_name=success".to_string(),
"flash_msg=A temporary password has been sent to the admin(s) of this device"
.to_string(),
)
}
Err(err) => {
debug!(
"Received an error while trying to send temporary password to device admin(s): {}",
err
);
(
"error".to_string(),
format!("Failed to send temporary password: {}", err),
)
}
};
Response::redirect_303("/auth/forgot").add_flash(flash_name, flash_msg)
}

View File

@ -1,60 +0,0 @@
use log::debug;
use rocket::catch;
use rocket::response::Redirect;
use rocket_dyn_templates::Template;
use serde::Serialize;
// HELPERS AND ROUTES FOR 404 ERROR
#[derive(Debug, Serialize)]
pub struct ErrorContext {
pub back: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub title: Option<String>,
}
impl ErrorContext {
pub fn build() -> ErrorContext {
ErrorContext {
back: None,
flash_name: None,
flash_msg: None,
title: None,
}
}
}
#[catch(404)]
pub fn not_found() -> Template {
debug!("404 Page Not Found");
let mut context = ErrorContext::build();
context.back = Some("/".to_string());
context.title = Some("404: Page Not Found".to_string());
context.flash_name = Some("error".to_string());
context.flash_msg = Some("No resource found for given URL".to_string());
Template::render("catchers/not_found", context)
}
// HELPERS AND ROUTES FOR 500 ERROR
#[catch(500)]
pub fn internal_error() -> Template {
debug!("500 Internal Server Error");
let mut context = ErrorContext::build();
context.back = Some("/".to_string());
context.title = Some("500: Internal Server Error".to_string());
context.flash_name = Some("error".to_string());
context.flash_msg = Some("Internal server error".to_string());
Template::render("catchers/internal_error", context)
}
// HELPERS AND ROUTES FOR 403 FORBIDDEN
#[catch(403)]
pub fn forbidden() -> Redirect {
debug!("403 Forbidden");
Redirect::to("/login")
}

View File

@ -0,0 +1,106 @@
use maud::{html, PreEscaped};
use crate::{templates, utils::theme};
/// Guide template builder.
pub fn build_template() -> PreEscaped<String> {
// render the guide template html
let guide_template = html! {
(PreEscaped("<!-- GUIDE -->"))
div class="card card-wide center" {
div class="capsule capsule-container border-info" {
(PreEscaped("<!-- GETTING STARTED -->"))
details {
summary class="card-text link" { "Getting started" }
p class="card-text" style="margin-top: 1rem; margin-bottom: 1rem;" {
"The Scuttlebutt server (sbot) will be inactive when you first run PeachCloud. This is to allow configuration parameters to be set before it is activated for the first time. Navigate to the "
strong {
a href="/settings/scuttlebutt/configure" class="link font-gray" {
"Sbot Configuration"
}
}
" page to configure your system. The default configuration will be fine for most usecases."
}
p class="card-text" style="margin-top: 1rem; margin-bottom: 1rem;" {
"Once the configuration is set, navigate to the "
strong {
a href="/settings/scuttlebutt" class="link font-gray" {
"Scuttlebutt settings menu"
}
}
" to start the sbot. If the server starts successfully, you will see a green smiley face on the home page. If the face is orange and sleeping, that means the sbot is still inactive (ie. the process is not running). If the face is red and dead, that means the sbot failed to start - indicated an error. For now, the best way to gain insight into the problem is to check the systemd log. Open a terminal and enter: "
code { "systemctl status go-sbot.service" }
". The log output may give some clues about the source of the error."
}
}
(PreEscaped("<!-- BUG REPORTS -->"))
details {
summary class="card-text link" { "Submit a bug report" }
p class="card-text" style="margin-top: 1rem; margin-bottom: 1rem;" {
"Bug reports can be submitted by "
strong {
a href="https://git.coopcloud.tech/PeachCloud/peach-workspace/issues/new?template=BUG_TEMPLATE.md" class="link font-gray" {
"filing an issue"
}
}
" on the peach-workspace git repo. Before filing a report, first check to see if an issue already exists for the bug you've encountered. If not, you're invited to submit a new report; the template will guide you through several questions."
}
}
(PreEscaped("<!-- REQUEST SUPPORT -->"))
details {
summary class="card-text link" { "Share feedback & request support" }
p class="card-text" style="margin-top: 1rem; margin-bottom: 1rem;" {
"You're invited to share your thoughts and experiences of PeachCloud in the #peachcloud channel on Scuttlebutt. The channel is also a good place to ask for help."
}
p class="card-text" style="margin-top: 1rem; margin-bottom: 1rem;" {
"Alternatively, we have a "
strong {
a href="https://matrix.to/#/#peachcloud:matrix.org" class="link font-gray" {
"Matrix channel"
}
}
" for discussion about PeachCloud and you can also reach out to @glyph "
strong {
a href="mailto:glyph@mycelial.technology" class="link font-gray" {
"via email"
}
}
"."
}
}
(PreEscaped("<!-- CONTRIBUTE -->"))
details {
summary class="card-text link" { "Contribute to PeachCloud" }
p class="card-text" style="margin-top: 1rem; margin-bottom: 1rem;" {
"PeachCloud is free, open-source software and relies on donations and grants to fund develop. Donations can be made on our "
strong {
a href="https://opencollective.com/peachcloud" class="link font-gray" {
"Open Collective"
}
}
" page."
}
p class="card-text" style="margin-top: 1rem; margin-bottom: 1rem;" {
"Programmers, designers, artists and writers are also welcome to contribute to the project. Please visit the "
strong {
a href="https://git.coopcloud.tech/PeachCloud/peach-workspace" class="link font-gray" {
"main PeachCloud git repository"
}
}
" to find out more details or contact the team via Scuttlebutt, Matrix or email."
}
}
}
}
};
// wrap the nav bars around the home template content
// title is "" and back button link is `None` because this is the homepage
let body = templates::nav::build_template(guide_template, "Guide", Some("/"));
// query the current theme so we can pass it into the base template builder
let theme = theme::get_theme();
// render the base template with the provided body
templates::base::build_template(body, theme)
}

View File

@ -0,0 +1,119 @@
use maud::{html, PreEscaped};
use peach_lib::sbot::SbotStatus;
use crate::{templates, utils::theme, SERVER_CONFIG};
/// Read the state of the go-sbot process and define status-related
/// elements accordingly.
fn render_status_elements<'a>() -> (&'a str, &'a str, &'a str) {
// retrieve go-sbot systemd process status
let sbot_status = SbotStatus::read();
// conditionally render the center circle class, center circle text and
// status circle class color based on the go-sbot process state
if let Ok(status) = sbot_status {
if status.state == Some("active".to_string()) {
("circle-success", "^_^", "border-success")
} else if status.state == Some("inactive".to_string()) {
("circle-warning", "z_z", "border-warning")
} else {
("circle-error", "x_x", "border-danger")
}
} else {
("circle-error", "x_x", "border-danger")
}
}
/// Render the URL for the status element (icon / link).
///
/// If the application is running in standalone mode then the element links
/// directly to the Scuttlebutt status page. If not, it links to the device
/// status page.
fn render_status_url<'a>() -> &'a str {
if SERVER_CONFIG.standalone_mode {
"/status/scuttlebutt"
} else {
"/status"
}
}
/// Home template builder.
pub fn build_template() -> PreEscaped<String> {
let (circle_color, center_circle_text, circle_border) = render_status_elements();
let status_url = render_status_url();
// render the home template html
let home_template = html! {
(PreEscaped("<!-- RADIAL MENU -->"))
div class="grid" {
(PreEscaped("<!-- top-left -->"))
(PreEscaped("<!-- PEERS LINK AND ICON -->"))
a class="top-left" href="/scuttlebutt/peers" title="Scuttlebutt Peers" {
div class="circle circle-small border-circle-small border-ssb" {
img class="icon-medium" src="/icons/users.svg";
}
}
(PreEscaped("<!-- top-middle -->"))
(PreEscaped("<!-- CURRENT USER LINK AND ICON -->"))
a class="top-middle" href="/scuttlebutt/profile" title="Profile" {
div class="circle circle-small border-circle-small border-ssb" {
img class="icon-medium" src="/icons/user.svg";
}
}
(PreEscaped("<!-- top-right -->"))
(PreEscaped("<!-- MESSAGES LINK AND ICON -->"))
a class="top-right" href="/scuttlebutt/private" title="Private Messages" {
div class="circle circle-small border-circle-small border-ssb" {
img class="icon-medium" src="/icons/envelope.svg";
}
}
(PreEscaped("<!-- middle -->"))
a class="middle" {
div class={ "circle circle-large " (circle_color) } {
p style="font-size: 4rem; color: var(--near-black);" {
(center_circle_text)
}
}
}
(PreEscaped("<!-- bottom-left -->"))
(PreEscaped("<!-- SYSTEM STATUS LINK AND ICON -->"))
a class="bottom-left" href=(status_url) title="Status" {
div class={ "circle circle-small border-circle-small " (circle_border) } {
img class="icon-medium" src="/icons/heart-pulse.svg";
}
}
/*
TODO: render the path of the status circle button based on the mode
{%- if standalone_mode == true -%}
<a class="bottom-left" href="/status/scuttlebutt" title="Status">
{% else -%}
<a class="bottom-left" href="/status" title="Status">
{%- endif -%}
*/
(PreEscaped("<!-- bottom-middle -->"))
(PreEscaped("<!-- PEACHCLOUD GUIDEBOOK LINK AND ICON -->"))
a class="bottom-middle" href="/guide" title="Guide" {
div class="circle circle-small border-circle-small border-info" {
img class="icon-medium" src="/icons/book.svg";
}
}
(PreEscaped("<!-- bottom-right -->"))
(PreEscaped("<!-- SYSTEM SETTINGS LINK AND ICON -->"))
a class="bottom-right" href="/settings" title="Settings" {
div class="circle circle-small border-circle-small border-settings" {
img class="icon-medium" src="/icons/cog.svg";
}
}
}
};
// wrap the nav bars around the home template content
// title is "" and back button link is `None` because this is the homepage
let body = templates::nav::build_template(home_template, "", None);
// query the current theme so we can pass it into the base template builder
let theme = theme::get_theme();
// render the base template with the provided body
templates::base::build_template(body, theme)
}

View File

@ -1,69 +0,0 @@
use rocket::{get, request::FlashMessage};
use rocket_dyn_templates::Template;
use serde::Serialize;
use crate::routes::authentication::Authenticated;
// HELPERS AND ROUTES FOR / (HOME PAGE)
#[derive(Debug, Serialize)]
pub struct HomeContext {
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub title: Option<String>,
}
impl HomeContext {
pub fn build() -> HomeContext {
HomeContext {
flash_name: None,
flash_msg: None,
title: None,
}
}
}
#[get("/")]
pub fn home(_auth: Authenticated) -> Template {
let context = HomeContext {
flash_name: None,
flash_msg: None,
title: None,
};
Template::render("home", &context)
}
// HELPERS AND ROUTES FOR /help
#[derive(Debug, Serialize)]
pub struct HelpContext {
pub back: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub title: Option<String>,
}
impl HelpContext {
pub fn build() -> HelpContext {
HelpContext {
back: None,
flash_name: None,
flash_msg: None,
title: None,
}
}
}
#[get("/help")]
pub fn help(flash: Option<FlashMessage>) -> Template {
let mut context = HelpContext::build();
context.back = Some("/".to_string());
context.title = Some("Help".to_string());
// check to see if there is a flash message to display
if let Some(flash) = flash {
// add flash message contents to the context object
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
};
Template::render("help", &context)
}

View File

@ -1,6 +1,8 @@
pub mod authentication;
pub mod catchers;
pub mod index;
//pub mod catchers;
//pub mod index;
pub mod guide;
pub mod home;
pub mod scuttlebutt;
pub mod settings;
pub mod status;

View File

@ -1,365 +0,0 @@
//! Routes for Scuttlebutt related functionality.
use rocket::{
form::{Form, FromForm},
get, post,
request::FlashMessage,
response::{Flash, Redirect},
serde::{Deserialize, Serialize},
uri,
};
use rocket_dyn_templates::Template;
use crate::routes::authentication::Authenticated;
// HELPERS AND ROUTES FOR /private
#[derive(Debug, Serialize)]
pub struct PrivateContext {
pub back: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub title: Option<String>,
}
impl PrivateContext {
pub fn build() -> PrivateContext {
PrivateContext {
back: None,
flash_name: None,
flash_msg: None,
title: None,
}
}
}
/// A private message composition and publication page.
#[get("/private")]
pub fn private(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
let mut context = PrivateContext::build();
context.back = Some("/".to_string());
context.title = Some("Private Messages".to_string());
// check to see if there is a flash message to display
if let Some(flash) = flash {
// add flash message contents to the context object
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
};
Template::render("scuttlebutt/messages", &context)
}
// HELPERS AND ROUTES FOR /peers
#[derive(Debug, Serialize)]
pub struct PeerContext {
pub back: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub title: Option<String>,
}
impl PeerContext {
pub fn build() -> PeerContext {
PeerContext {
back: None,
flash_name: None,
flash_msg: None,
title: None,
}
}
}
/// A peer menu which allows navigating to lists of friends, follows, followers and blocks.
#[get("/peers")]
pub fn peers(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
let mut context = PeerContext::build();
context.back = Some("/".to_string());
context.title = Some("Scuttlebutt Peers".to_string());
// check to see if there is a flash message to display
if let Some(flash) = flash {
// add flash message contents to the context object
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
};
Template::render("scuttlebutt/peers", &context)
}
// HELPERS AND ROUTES FOR /post/publish
#[derive(Debug, Deserialize, FromForm)]
pub struct Post {
pub text: String,
}
/// Publish a public Scuttlebutt post. Redirects to profile page of the PeachCloud local identity with a flash message describing the outcome of the action (may be successful or unsuccessful).
#[post("/publish", data = "<post>")]
pub fn publish(
post: Form<Post>,
flash: Option<FlashMessage>,
_auth: Authenticated,
) -> Flash<Redirect> {
let post_text = &post.text;
// perform the sbotcli publish action using post_text
// if successful, redirect to home profile page and flash "success"
// if error, redirect to home profile page and flash "error"
// redirect to the profile template without public key ("home" / local profile)
let pub_key: std::option::Option<&str> = None;
let profile_url = uri!(profile(pub_key));
// consider adding the message reference to the flash message (or render it in the template for
// `profile`
Flash::success(Redirect::to(profile_url), "Published public post")
}
// HELPERS AND ROUTES FOR /follow
#[derive(Debug, Deserialize, FromForm)]
pub struct PublicKey {
pub key: String,
}
/// Follow a Scuttlebutt profile specified by the given public key. Redirects to the appropriate profile page with a flash message describing the outcome of the action (may be successful or unsuccessful).
#[post("/follow", data = "<pub_key>")]
pub fn follow(
pub_key: Form<PublicKey>,
flash: Option<FlashMessage>,
_auth: Authenticated,
) -> Flash<Redirect> {
let public_key = &pub_key.key;
// perform the sbotcli follow action using &pub_key.0
// if successful, redirect to profile page with provided public key and flash "success"
// if error, redirect to profile page with provided public key and flash "error"
// redirect to the profile template with provided public key
let profile_url = uri!(profile(Some(public_key)));
let success_msg = format!("Followed {}", public_key);
Flash::success(Redirect::to(profile_url), success_msg)
}
// HELPERS AND ROUTES FOR /unfollow
/// Unfollow a Scuttlebutt profile specified by the given public key. Redirects to the appropriate profile page with a flash message describing the outcome of the action (may be successful or unsuccessful).
#[post("/unfollow", data = "<pub_key>")]
pub fn unfollow(
pub_key: Form<PublicKey>,
flash: Option<FlashMessage>,
_auth: Authenticated,
) -> Flash<Redirect> {
let public_key = &pub_key.key;
// perform the sbotcli unfollow action using &pub_key.0
// if successful, redirect to profile page with provided public key and flash "success"
// if error, redirect to profile page with provided public key and flash "error"
// redirect to the profile template with provided public key
let profile_url = uri!(profile(Some(public_key)));
let success_msg = format!("Unfollowed {}", public_key);
Flash::success(Redirect::to(profile_url), success_msg)
}
// HELPERS AND ROUTES FOR /block
/// Block a Scuttlebutt profile specified by the given public key. Redirects to the appropriate profile page with a flash message describing the outcome of the action (may be successful or unsuccessful).
#[post("/block", data = "<pub_key>")]
pub fn block(
pub_key: Form<PublicKey>,
flash: Option<FlashMessage>,
_auth: Authenticated,
) -> Flash<Redirect> {
let public_key = &pub_key.key;
// perform the sbotcli block action using &pub_key.0
// if successful, redirect to profile page with provided public key and flash "success"
// if error, redirect to profile page with provided public key and flash "error"
// redirect to the profile template with provided public key
let profile_url = uri!(profile(Some(public_key)));
let success_msg = format!("Blocked {}", public_key);
Flash::success(Redirect::to(profile_url), success_msg)
}
// HELPERS AND ROUTES FOR /profile
#[derive(Debug, Serialize)]
pub struct ProfileContext {
pub back: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub title: Option<String>,
}
impl ProfileContext {
pub fn build() -> ProfileContext {
ProfileContext {
back: None,
flash_name: None,
flash_msg: None,
title: None,
}
}
}
/// A Scuttlebutt profile, specified by a public key. It may be our own profile or the profile of a peer. If not public key query parameter is provided, the local profile is displayed (ie. the profile of the public key associated with the local PeachCloud device).
#[get("/profile?<pub_key>")]
pub fn profile(
pub_key: Option<&str>,
flash: Option<FlashMessage>,
_auth: Authenticated,
) -> Template {
let mut context = ProfileContext::build();
context.back = Some("/".to_string());
context.title = Some("Profile".to_string());
// check to see if there is a flash message to display
if let Some(flash) = flash {
// add flash message contents to the context object
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
};
Template::render("scuttlebutt/profile", &context)
}
// HELPERS AND ROUTES FOR /friends
#[derive(Debug, Serialize)]
pub struct FriendsContext {
pub back: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub title: Option<String>,
}
impl FriendsContext {
pub fn build() -> FriendsContext {
FriendsContext {
back: None,
flash_name: None,
flash_msg: None,
title: None,
}
}
}
/// A list of friends (mutual follows), with each list item displaying the name, image and public
/// key of the peer.
#[get("/friends")]
pub fn friends(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
let mut context = FriendsContext::build();
context.back = Some("/scuttlebutt/peers".to_string());
context.title = Some("Friends".to_string());
// check to see if there is a flash message to display
if let Some(flash) = flash {
// add flash message contents to the context object
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
};
Template::render("scuttlebutt/peers_list", &context)
}
// HELPERS AND ROUTES FOR /follows
#[derive(Debug, Serialize)]
pub struct FollowsContext {
pub back: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub title: Option<String>,
}
impl FollowsContext {
pub fn build() -> FollowsContext {
FollowsContext {
back: None,
flash_name: None,
flash_msg: None,
title: None,
}
}
}
/// A list of follows (peers we follow who do not follow us), with each list item displaying the name, image and public
/// key of the peer.
#[get("/follows")]
pub fn follows(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
let mut context = FollowsContext::build();
context.back = Some("/scuttlebutt/peers".to_string());
context.title = Some("Follows".to_string());
// check to see if there is a flash message to display
if let Some(flash) = flash {
// add flash message contents to the context object
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
};
Template::render("scuttlebutt/peers_list", &context)
}
// HELPERS AND ROUTES FOR /followers
#[derive(Debug, Serialize)]
pub struct FollowersContext {
pub back: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub title: Option<String>,
}
impl FollowersContext {
pub fn build() -> FollowersContext {
FollowersContext {
back: None,
flash_name: None,
flash_msg: None,
title: None,
}
}
}
/// A list of followers (peers who follow us but who we do not follow), with each list item displaying the name, image and public
/// key of the peer.
#[get("/followers")]
pub fn followers(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
let mut context = FollowersContext::build();
context.back = Some("/scuttlebutt/peers".to_string());
context.title = Some("Followers".to_string());
// check to see if there is a flash message to display
if let Some(flash) = flash {
// add flash message contents to the context object
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
};
Template::render("scuttlebutt/peers_list", &context)
}
// HELPERS AND ROUTES FOR /blocks
#[derive(Debug, Serialize)]
pub struct BlocksContext {
pub back: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub title: Option<String>,
}
impl BlocksContext {
pub fn build() -> BlocksContext {
BlocksContext {
back: None,
flash_name: None,
flash_msg: None,
title: None,
}
}
}
/// A list of blocks (peers we've blocked previously), with each list item displaying the name, image and public
/// key of the peer.
#[get("/blocks")]
pub fn blocks(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
let mut context = BlocksContext::build();
context.back = Some("/scuttlebutt/peers".to_string());
context.title = Some("Blocks".to_string());
// check to see if there is a flash message to display
if let Some(flash) = flash {
// add flash message contents to the context object
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
};
Template::render("scuttlebutt/peers_list", &context)
}

View File

@ -0,0 +1,42 @@
use peach_lib::sbot::SbotStatus;
use rouille::{post_input, try_or_400, Request, Response};
use crate::utils::{flash::FlashResponse, sbot};
// ROUTE: /scuttlebutt/block
/// Block a Scuttlebutt profile specified by the given public key.
///
/// Parse the public key from the submitted form and publish a contact message.
/// Redirect to the appropriate profile page with a flash message describing
/// the outcome of the action (may be successful or unsuccessful).
pub fn handle_form(request: &Request) -> Response {
// query the request body for form data
// return a 400 error if the admin_id field is missing
let data = try_or_400!(post_input!(request, {
public_key: String,
}));
let (flash_name, flash_msg) = match SbotStatus::read() {
Ok(status) if status.state == Some("active".to_string()) => {
match sbot::block_peer(&data.public_key) {
Ok(success_msg) => (
"flash_name=success".to_string(),
format!("flash_msg={}", success_msg),
),
Err(error_msg) => (
"flash_name=error".to_string(),
format!("flash_msg={}", error_msg),
),
}
}
_ => (
"flash_name=warning".to_string(),
"Social interactions are unavailable.".to_string(),
),
};
let url = format!("/scuttlebutt/profile/{}", data.public_key);
Response::redirect_303(url).add_flash(flash_name, flash_msg)
}

View File

@ -0,0 +1,29 @@
use maud::PreEscaped;
use crate::{
templates,
utils::{sbot, theme},
};
// ROUTE: /scuttlebutt/blocks
/// Scuttlebutt blocks list template builder.
pub fn build_template() -> PreEscaped<String> {
// retrieve the list of blocked peers
match sbot::get_blocks_list() {
// populate the peers_list template with blocks and render it
Ok(blocks) => templates::peers_list::build_template(blocks, "Blocks"),
Err(e) => {
// render the sbot error template with the error message
let error_template = templates::error::build_template(e.to_string());
// wrap the nav bars around the error template content
let body = templates::nav::build_template(error_template, "Blocks", Some("/"));
// query the current theme so we can pass it into the base template builder
let theme = theme::get_theme();
// render the base template with the provided body
templates::base::build_template(body, theme)
}
}
}

View File

@ -0,0 +1,42 @@
use peach_lib::sbot::SbotStatus;
use rouille::{post_input, try_or_400, Request, Response};
use crate::utils::{flash::FlashResponse, sbot};
// ROUTE: /scuttlebutt/follow
/// Follow a Scuttlebutt profile specified by the given public key.
///
/// Parse the public key from the submitted form and publish a contact message.
/// Redirect to the appropriate profile page with a flash message describing
/// the outcome of the action (may be successful or unsuccessful).
pub fn handle_form(request: &Request) -> Response {
// query the request body for form data
// return a 400 error if the admin_id field is missing
let data = try_or_400!(post_input!(request, {
public_key: String,
}));
let (flash_name, flash_msg) = match SbotStatus::read() {
Ok(status) if status.state == Some("active".to_string()) => {
match sbot::follow_peer(&data.public_key) {
Ok(success_msg) => (
"flash_name=success".to_string(),
format!("flash_msg={}", success_msg),
),
Err(error_msg) => (
"flash_name=error".to_string(),
format!("flash_msg={}", error_msg),
),
}
}
_ => (
"flash_name=warning".to_string(),
"Social interactions are unavailable.".to_string(),
),
};
let url = format!("/scuttlebutt/profile/{}", data.public_key);
Response::redirect_303(url).add_flash(flash_name, flash_msg)
}

View File

@ -0,0 +1,29 @@
use maud::PreEscaped;
use crate::{
templates,
utils::{sbot, theme},
};
// ROUTE: /scuttlebutt/follows
/// Scuttlebutt follows list template builder.
pub fn build_template() -> PreEscaped<String> {
// retrieve the list of follows
match sbot::get_follows_list() {
// populate the peers_list template with follows
Ok(follows) => templates::peers_list::build_template(follows, "Follows"),
Err(e) => {
// render the sbot error template with the error message
let error_template = templates::error::build_template(e.to_string());
// wrap the nav bars around the error template content
let body = templates::nav::build_template(error_template, "Follows", Some("/"));
// query the current theme so we can pass it into the base template builder
let theme = theme::get_theme();
// render the base template with the provided body
templates::base::build_template(body, theme)
}
}
}

View File

@ -0,0 +1,29 @@
use maud::PreEscaped;
use crate::{
templates,
utils::{sbot, theme},
};
// ROUTE: /scuttlebutt/friends
/// Scuttlebutt friends list template builder.
pub fn build_template() -> PreEscaped<String> {
// retrieve the list of friends
match sbot::get_friends_list() {
// populate the peers_list template with friends and render it
Ok(friends) => templates::peers_list::build_template(friends, "Friends"),
Err(e) => {
// render the sbot error template with the error message
let error_template = templates::error::build_template(e.to_string());
// wrap the nav bars around the error template content
let body = templates::nav::build_template(error_template, "Friends", Some("/"));
// query the current theme so we can pass it into the base template builder
let theme = theme::get_theme();
// render the base template with the provided body
templates::base::build_template(body, theme)
}
}
}

View File

@ -0,0 +1,97 @@
use maud::{html, Markup, PreEscaped};
use peach_lib::sbot::SbotStatus;
use rouille::{post_input, try_or_400, Request, Response};
use crate::{
templates,
utils::{
flash::{FlashRequest, FlashResponse},
sbot, theme,
},
};
// ROUTE: /scuttlebutt/invites
/// Render the invite form template.
fn invite_form_template(
flash_name: Option<&str>,
flash_msg: Option<&str>,
invite_code: Option<&str>,
) -> Markup {
html! {
(PreEscaped("<!-- SCUTTLEBUTT INVITE FORM -->"))
div class="card center" {
form id="invites" class="center" action="/scuttlebutt/invites" method="post" {
div class="center" style="width: 80%;" {
label for="inviteUses" class="label-small font-gray" title="Number of times the invite code can be reused" { "USES" }
input type="number" id="inviteUses" name="uses" min="1" max="150" size="3" value="1";
@if let Some(code) = invite_code {
p class="card-text" style="margin-top: 1rem; user-select: all;" title="Invite code" {
(code)
}
}
}
(PreEscaped("<!-- BUTTONS -->"))
input id="createInvite" class="button button-primary center" style="margin-top: 1rem;" type="submit" title="Create a new invite code" value="Create";
a id="cancel" class="button button-secondary center" href="/scuttlebutt/peers" title="Cancel" { "Cancel" }
}
// render flash message if cookies were found in the request
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
// avoid displaying the invite code-containing flash msg
@if name != "code" {
(PreEscaped("<!-- FLASH MESSAGE -->"))
(templates::flash::build_template(name, msg))
}
}
}
}
}
/// Scuttlebutt invite template builder.
pub fn build_template(request: &Request) -> PreEscaped<String> {
// check for flash cookies; will be (None, None) if no flash cookies are found
let (flash_name, flash_msg) = request.retrieve_flash();
// if flash_name is "code" then flash_msg will be an invite code
let invite_code = if flash_name == Some("code") {
flash_msg
} else {
None
};
let invite_form_template = match SbotStatus::read() {
// only render the invite form template if the sbot is active
Ok(status) if status.state == Some("active".to_string()) => {
html! { (invite_form_template(flash_name, flash_msg, invite_code)) }
}
_ => {
// the sbot is not active; render a message instead of the invite form
templates::inactive::build_template("Invite creation is unavailable.")
}
};
let body =
templates::nav::build_template(invite_form_template, "Invites", Some("/scuttlebutt/peers"));
// query the current theme so we can pass it into the base template builder
let theme = theme::get_theme();
templates::base::build_template(body, theme)
}
/// Parse the invite uses data and attempt to generate an invite code.
pub fn handle_form(request: &Request) -> Response {
// query the request body for form data
// return a 400 error if the admin_id field is missing
let data = try_or_400!(post_input!(request, {
// the number of times the invite code can be used
uses: u16,
}));
let (flash_name, flash_msg) = match sbot::create_invite(data.uses) {
Ok(code) => ("flash_name=code".to_string(), format!("flash_msg={}", code)),
Err(e) => ("flash_name=error".to_string(), format!("flash_msg={}", e)),
};
Response::redirect_303("/scuttlebutt/invites").add_flash(flash_name, flash_msg)
}

View File

@ -0,0 +1,14 @@
pub mod block;
pub mod blocks;
pub mod follow;
pub mod follows;
pub mod friends;
pub mod invites;
pub mod peers;
pub mod private;
pub mod profile;
pub mod profile_update;
pub mod publish;
pub mod search;
pub mod unblock;
pub mod unfollow;

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