diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ee44a96 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea +target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..4b26642 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3834 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a2e47a1fbe209ee101dd6d61285226744c6c8d3c21c8dc878ba6cb9f467f3a" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aead" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fc95d1bdb8e6666b2b217308eeeb09f2d6728d104be3e31916cc74d15420331" +dependencies = [ + "generic-array 0.14.4", +] + +[[package]] +name = "aes" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884391ef1066acaa41e766ba8f596341b96e93ce34f9a43e7d24bf0a0eaf0561" +dependencies = [ + "aes-soft", + "aesni", + "cipher", +] + +[[package]] +name = "aes-gcm" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5278b5fabbb9bd46e24aa69b2fdea62c99088e0a950a9be40e3e0101298f88da" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aes-soft" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be14c7498ea50828a38d0e24a765ed2effe92a705885b57d029cd67d45744072" +dependencies = [ + "cipher", + "opaque-debug 0.3.0", +] + +[[package]] +name = "aesni" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2e11f5e94c2f7d386164cc2aa1f97823fed6f259e486940a71c174dd01b0ce" +dependencies = [ + "cipher", + "opaque-debug 0.3.0", +] + +[[package]] +name = "aho-corasick" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" +dependencies = [ + "memchr", +] + +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "atomicwrites" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a2baf2feb820299c53c7ad1cc4f5914a220a1cb76d7ce321d2522a94b54651f" +dependencies = [ + "nix 0.14.1", + "tempdir", + "winapi 0.3.9", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "autocfg" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "backtrace" +version = "0.3.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4717cfcbfaa661a0fd48f8453951837ae7e8f81e481fbb136e3202d72805a744" +dependencies = [ + "addr2line", + "cc", + "cfg-if 1.0.0", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "489d6c0ed21b11d038c31b6ceccca973e65d73ba3bd8ecb9a2babf5546164643" +dependencies = [ + "byteorder", + "safemem", +] + +[[package]] +name = "base64" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" +dependencies = [ + "byteorder", +] + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "bitvec" +version = "0.19.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8942c8d352ae1838c9dda0b0ca2ab657696ef2232a20147cf1b30ae1a9cb4321" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" +dependencies = [ + "block-padding", + "byte-tools", + "byteorder", + "generic-array 0.12.4", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array 0.14.4", +] + +[[package]] +name = "block-padding" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" +dependencies = [ + "byte-tools", +] + +[[package]] +name = "bstr" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a40b47ad93e1a5404e6c18dec46b628214fee441c70f4ab5d6942142cc268a3d" +dependencies = [ + "memchr", +] + +[[package]] +name = "byte-tools" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" +dependencies = [ + "byteorder", + "either", + "iovec", +] + +[[package]] +name = "bytesize" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c58ec36aac5066d5ca17df51b3e70279f5670a72102f5752cb7e7c856adfc70" + +[[package]] +name = "c_linked_list" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4964518bd3b4a8190e832886cdc0da9794f12e8e6c1613a9e90ff331c4c8724b" + +[[package]] +name = "cc" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits 0.2.14", + "time 0.1.44", + "winapi 0.3.9", +] + +[[package]] +name = "chrono-tz" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2554a3155fec064362507487171dcc4edc3df60cb10f3a1fb10ed8094822b120" +dependencies = [ + "chrono", + "parse-zoneinfo", +] + +[[package]] +name = "cipher" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801" +dependencies = [ + "generic-array 0.14.4", +] + +[[package]] +name = "clap" +version = "2.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags", +] + +[[package]] +name = "const_format" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f4087b5a1164f92255f8b301c88fc8627e5abf5e25b5476f84b02e4b47795d" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c36c619c422113552db4eb28cddba8faa757e33f758cc3415bd2885977b591" +dependencies = [ + "proc-macro2 1.0.28", + "quote 1.0.9", + "unicode-xid 0.2.2", +] + +[[package]] +name = "cookie" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80f6044740a4a516b8aac14c140cdf35c1a640b1bd6b98b6224e49143b2f1566" +dependencies = [ + "aes-gcm", + "base64 0.13.0", + "hkdf", + "hmac", + "percent-encoding 2.1.0", + "rand 0.8.4", + "sha2", + "time 0.1.44", +] + +[[package]] +name = "core-foundation" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" + +[[package]] +name = "cpufeatures" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef" +dependencies = [ + "libc", +] + +[[package]] +name = "cpuid-bool" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba" + +[[package]] +name = "crossbeam-channel" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ec7fcd21571dc78f96cc96243cab8d8f035247c3efd16c687be154c3fa9efa" +dependencies = [ + "crossbeam-utils 0.6.6", +] + +[[package]] +name = "crossbeam-deque" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20ff29ded3204c5106278a81a38f4b482636ed4fa1e6cfbeef193291beb29ed" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils 0.7.2", + "maybe-uninit", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" +dependencies = [ + "autocfg 1.0.1", + "cfg-if 0.1.10", + "crossbeam-utils 0.7.2", + "lazy_static", + "maybe-uninit", + "memoffset", + "scopeguard 1.1.0", +] + +[[package]] +name = "crossbeam-queue" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "774ba60a54c213d409d5353bda12d49cd68d14e45036a285234c8d6f91f92570" +dependencies = [ + "cfg-if 0.1.10", + "crossbeam-utils 0.7.2", + "maybe-uninit", +] + +[[package]] +name = "crossbeam-utils" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04973fa96e96579258a5091af6003abde64af786b860f18622b82e026cca60e6" +dependencies = [ + "cfg-if 0.1.10", + "lazy_static", +] + +[[package]] +name = "crossbeam-utils" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" +dependencies = [ + "autocfg 1.0.1", + "cfg-if 0.1.10", + "lazy_static", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db" +dependencies = [ + "cfg-if 1.0.0", + "lazy_static", +] + +[[package]] +name = "crypto-mac" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff07008ec701e8028e2ceb8f83f0e4274ee62bd2dbdc4fefff2e9a91824081a" +dependencies = [ + "generic-array 0.14.4", + "subtle", +] + +[[package]] +name = "ctr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb4a30d54f7443bf3d6191dcd486aca19e67cb3c49fa7a06a319966346707e7f" +dependencies = [ + "cipher", +] + +[[package]] +name = "ctrlc" +version = "3.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "232295399409a8b7ae41276757b5a1cc21032848d42bff2352261f958b3ca29a" +dependencies = [ + "nix 0.20.0", + "winapi 0.3.9", +] + +[[package]] +name = "deunicode" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "850878694b7933ca4c9569d30a34b55031b9b139ee1fc7b94a527c4ef960d690" + +[[package]] +name = "devise" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74e04ba2d03c5fa0d954c061fc8c9c288badadffc272ebb87679a89846de3ed3" +dependencies = [ + "devise_codegen", + "devise_core", +] + +[[package]] +name = "devise_codegen" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "066ceb7928ca93a9bedc6d0e612a8a0424048b0ab1f75971b203d01420c055d7" +dependencies = [ + "devise_core", + "quote 0.6.13", +] + +[[package]] +name = "devise_core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf41c59b22b5e3ec0ea55c7847e5f358d340f3a8d6d53a5cf4f1564967f96487" +dependencies = [ + "bitflags", + "proc-macro2 0.4.30", + "quote 0.6.13", + "syn 0.15.44", +] + +[[package]] +name = "digest" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +dependencies = [ + "generic-array 0.12.4", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array 0.14.4", +] + +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "dtoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "env_logger" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aafcde04e90a5226a6443b7aabdb016ba2f8307c847d524724bd9b346dd1a2d3" +dependencies = [ + "atty", + "humantime 1.3.0", + "log 0.4.14", + "regex", + "termcolor", +] + +[[package]] +name = "env_logger" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" +dependencies = [ + "atty", + "humantime 2.1.0", + "log 0.4.14", + "regex", + "termcolor", +] + +[[package]] +name = "error-chain" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" +dependencies = [ + "backtrace", + "version_check 0.9.3", +] + +[[package]] +name = "failure" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" +dependencies = [ + "backtrace", + "failure_derive", +] + +[[package]] +name = "failure_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" +dependencies = [ + "proc-macro2 1.0.28", + "quote 1.0.9", + "syn 1.0.74", + "synstructure", +] + +[[package]] +name = "fake-simd" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" + +[[package]] +name = "filetime" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall 0.2.10", + "winapi 0.3.9", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "fsevent" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab7d1bd1bd33cc98b0889831b72da23c0aa4df9cec7e0702f46ecea04b35db6" +dependencies = [ + "bitflags", + "fsevent-sys", +] + +[[package]] +name = "fsevent-sys" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f41b048a94555da0f42f1d632e2e19510084fb8e303b0daa2816e733fb3644a0" +dependencies = [ + "libc", +] + +[[package]] +name = "fslock" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b14c83e47c73f7d62d907ae24a1a98e9132df3c33eb6c54fcf4bce0dbc41d5af" +dependencies = [ + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + +[[package]] +name = "fuchsia-zircon" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +dependencies = [ + "bitflags", + "fuchsia-zircon-sys", +] + +[[package]] +name = "fuchsia-zircon-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" + +[[package]] +name = "funty" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" + +[[package]] +name = "futures" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678" + +[[package]] +name = "futures-cpupool" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab90cde24b3319636588d0c35fe03b1333857621051837ed769faefb4c2162e4" +dependencies = [ + "futures", + "num_cpus", +] + +[[package]] +name = "gcc" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" + +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +dependencies = [ + "typenum", + "version_check 0.9.3", +] + +[[package]] +name = "get_if_addrs" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abddb55a898d32925f3148bd281174a68eeb68bbfd9a5938a57b18f506ee4ef7" +dependencies = [ + "c_linked_list", + "get_if_addrs-sys", + "libc", + "winapi 0.2.8", +] + +[[package]] +name = "get_if_addrs-sys" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d04f9fb746cf36b191c00f3ede8bde9c8e64f9f4b05ae2694a9ccf5e3f5ab48" +dependencies = [ + "gcc", + "libc", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", +] + +[[package]] +name = "ghash" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97304e4cd182c3846f7575ced3890c53012ce534ad9114046b0a9e00bb30a375" +dependencies = [ + "opaque-debug 0.3.0", + "polyval", +] + +[[package]] +name = "gimli" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4075386626662786ddb0ec9081e7c7eeb1ba31951f447ca780ef9f5d568189" + +[[package]] +name = "glob" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb" + +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + +[[package]] +name = "globset" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10463d9ff00a2a068db14231982f5132edebad0d7660cd956a1c30292dbcbfbd" +dependencies = [ + "aho-corasick", + "bstr", + "fnv", + "log 0.4.14", + "regex", +] + +[[package]] +name = "globwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" +dependencies = [ + "bitflags", + "ignore", + "walkdir", +] + +[[package]] +name = "h2" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5b34c246847f938a410a03c5458c7fee2274436675e76d8b903c08efc29c462" +dependencies = [ + "byteorder", + "bytes", + "fnv", + "futures", + "http", + "indexmap", + "log 0.4.14", + "slab 0.4.4", + "string", + "tokio-io", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hkdf" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51ab2f639c231793c5f6114bdb9bbe50a7dbbfcd7c7c6bd8475dec2d991e964f" +dependencies = [ + "digest 0.9.0", + "hmac", +] + +[[package]] +name = "hmac" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" +dependencies = [ + "crypto-mac", + "digest 0.9.0", +] + +[[package]] +name = "http" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6ccf5ede3a895d8856620237b2f02972c1bbc78d2965ad7fe8838d4a0ed41f0" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6741c859c1b2463a423a1dbce98d418e6c3c3fc720fb0d45528657320920292d" +dependencies = [ + "bytes", + "futures", + "http", + "tokio-buf", +] + +[[package]] +name = "httparse" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a87b616e37e93c22fb19bcd386f02f3af5ea98a25670ad0fce773de23c5e68" + +[[package]] +name = "humansize" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02296996cb8796d7c6e3bc2d9211b7802812d36999a51bb754123ead7d37d026" + +[[package]] +name = "humantime" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" +dependencies = [ + "quick-error", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "0.10.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a0652d9a2609a968c14be1a9ea00bf4b1d64e2e1f53a1b51b6fff3a6e829273" +dependencies = [ + "base64 0.9.3", + "httparse", + "language-tags", + "log 0.3.9", + "mime 0.2.6", + "num_cpus", + "time 0.1.44", + "traitobject", + "typeable", + "unicase 1.4.2", + "url", +] + +[[package]] +name = "hyper" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34a590ca09d341e94cddf8e5af0bbccde205d5fbc2fa3c09dd67c7f85cea59d7" +dependencies = [ + "base64 0.9.3", + "bytes", + "futures", + "futures-cpupool", + "httparse", + "iovec", + "language-tags", + "log 0.4.14", + "mime 0.3.16", + "net2", + "percent-encoding 1.0.1", + "relay", + "time 0.1.44", + "tokio-core", + "tokio-io", + "tokio-proto", + "tokio-service", + "unicase 2.6.0", + "want 0.0.4", +] + +[[package]] +name = "hyper" +version = "0.12.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c843caf6296fc1f93444735205af9ed4e109a539005abb2564ae1d6fad34c52" +dependencies = [ + "bytes", + "futures", + "futures-cpupool", + "h2", + "http", + "http-body", + "httparse", + "iovec", + "itoa", + "log 0.4.14", + "net2", + "rustc_version", + "time 0.1.44", + "tokio", + "tokio-buf", + "tokio-executor", + "tokio-io", + "tokio-reactor", + "tokio-tcp", + "tokio-threadpool", + "tokio-timer", + "want 0.2.0", +] + +[[package]] +name = "idna" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "ignore" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d" +dependencies = [ + "crossbeam-utils 0.8.5", + "globset", + "lazy_static", + "log 0.4.14", + "memchr", + "regex", + "same-file", + "thread_local", + "walkdir", + "winapi-util", +] + +[[package]] +name = "indexmap" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +dependencies = [ + "autocfg 1.0.1", + "hashbrown", +] + +[[package]] +name = "inotify" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4816c66d2c8ae673df83366c18341538f234a26d65a9ecea5c348b453ac1d02f" +dependencies = [ + "bitflags", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +dependencies = [ + "libc", +] + +[[package]] +name = "itoa" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" + +[[package]] +name = "jsonrpc-client-core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29cb249837420fb0cee7fb0fbf1d22679e121b160e71bb5e0d90b9df241c23e" +dependencies = [ + "error-chain", + "futures", + "jsonrpc-core 8.0.1", + "log 0.4.14", + "serde 1.0.127", + "serde_json", +] + +[[package]] +name = "jsonrpc-client-http" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e642eb74423b9dfcb4512fda167148746b76f788a823cd712fadf409f31d302" +dependencies = [ + "error-chain", + "futures", + "hyper 0.11.27", + "jsonrpc-client-core", + "log 0.4.14", + "tokio-core", +] + +[[package]] +name = "jsonrpc-client-transports" +version = "14.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2773fa94a2a1fd51efb89a8f45b8861023dbb415d18d3c9235ae9388d780f9ec" +dependencies = [ + "failure", + "futures", + "jsonrpc-core 14.2.0", + "jsonrpc-pubsub 14.2.0", + "log 0.4.14", + "serde 1.0.127", + "serde_json", + "url", +] + +[[package]] +name = "jsonrpc-core" +version = "8.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf83704f4e79979a424d1082dd2c1e52683058056c9280efa19ac5f6bc9033c" +dependencies = [ + "futures", + "log 0.3.9", + "serde 1.0.127", + "serde_derive", + "serde_json", +] + +[[package]] +name = "jsonrpc-core" +version = "11.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b83fdc5e0218128d0d270f2f2e7a5ea716f3240c8518a58bc89e6716ba8581" +dependencies = [ + "futures", + "log 0.4.14", + "serde 1.0.127", + "serde_derive", + "serde_json", +] + +[[package]] +name = "jsonrpc-core" +version = "14.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0747307121ffb9703afd93afbd0fb4f854c38fb873f2c8b90e0e902f27c7b62" +dependencies = [ + "futures", + "log 0.4.14", + "serde 1.0.127", + "serde_derive", + "serde_json", +] + +[[package]] +name = "jsonrpc-core-client" +version = "11.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c889ca27072f038496a62f38356e8f827acf194d7276030120265362b2974eab" +dependencies = [ + "failure", + "futures", + "jsonrpc-core 11.0.0", + "log 0.4.14", + "serde 1.0.127", + "serde_json", +] + +[[package]] +name = "jsonrpc-core-client" +version = "14.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34221123bc79b66279a3fde2d3363553835b43092d629b34f2e760c44dc94713" +dependencies = [ + "jsonrpc-client-transports", +] + +[[package]] +name = "jsonrpc-http-server" +version = "11.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "541257be6c8f75a41812575150dfa1120d3ee3a852601f2ca8ac9bcb73575c4e" +dependencies = [ + "hyper 0.12.36", + "jsonrpc-core 11.0.0", + "jsonrpc-server-utils", + "log 0.4.14", + "net2", + "unicase 2.6.0", +] + +[[package]] +name = "jsonrpc-pubsub" +version = "11.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c233c4570183a45f7bde14cd7d23446d6c236de6df9442e53a60951adae9fd34" +dependencies = [ + "jsonrpc-core 11.0.0", + "log 0.4.14", + "parking_lot 0.7.1", + "serde 1.0.127", +] + +[[package]] +name = "jsonrpc-pubsub" +version = "14.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d44f5602a11d657946aac09357956d2841299ed422035edf140c552cb057986" +dependencies = [ + "jsonrpc-core 14.2.0", + "log 0.4.14", + "parking_lot 0.10.2", + "rand 0.7.3", + "serde 1.0.127", +] + +[[package]] +name = "jsonrpc-server-utils" +version = "11.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3372b3248a53abcca8f61924f188052bb0c4cd80b482b2b4eaf9f8667efb9f4" +dependencies = [ + "bytes", + "globset", + "jsonrpc-core 11.0.0", + "lazy_static", + "log 0.4.14", + "num_cpus", + "tokio", + "tokio-codec", + "unicase 2.6.0", +] + +[[package]] +name = "jsonrpc-test" +version = "11.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aedad254cd8faba2bf8d1fe00c52fe9d9944f1a8c3f789916b68a61a3414c87" +dependencies = [ + "jsonrpc-core 11.0.0", + "jsonrpc-core-client 11.0.0", + "jsonrpc-pubsub 11.0.0", + "log 0.4.14", + "serde 1.0.127", + "serde_json", +] + +[[package]] +name = "jsonrpc-test" +version = "14.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fdbfe280021d220b3392e38b34781647f8268eef6d71564074a870a08404899" +dependencies = [ + "jsonrpc-core 14.2.0", + "jsonrpc-core-client 14.2.0", + "jsonrpc-pubsub 14.2.0", + "log 0.4.14", + "serde 1.0.127", + "serde_json", +] + +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "language-tags" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "lexical-core" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" +dependencies = [ + "arrayvec", + "bitflags", + "cfg-if 1.0.0", + "ryu", + "static_assertions", +] + +[[package]] +name = "libc" +version = "0.2.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790" + +[[package]] +name = "linked-hash-map" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d262045c5b87c0861b3f004610afd0e2c851e2908d08b6c870cbb9d5f494ecd" +dependencies = [ + "serde 0.8.23", + "serde_test", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" + +[[package]] +name = "lock_api" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ebf1391f6acad60e5c8b43706dde4582df75c06698ab44511d15016bc2442c" +dependencies = [ + "owning_ref", + "scopeguard 0.3.3", +] + +[[package]] +name = "lock_api" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" +dependencies = [ + "scopeguard 1.1.0", +] + +[[package]] +name = "log" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" +dependencies = [ + "log 0.4.14", +] + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "matches" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" + +[[package]] +name = "maybe-uninit" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" + +[[package]] +name = "memchr" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" + +[[package]] +name = "memoffset" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "043175f069eda7b85febe4a74abbaeff828d9f8b448515d3151a14a3542811aa" +dependencies = [ + "autocfg 1.0.1", +] + +[[package]] +name = "mime" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba626b8a6de5da682e1caa06bdb42a335aee5a84db8e5046a3e8ab17ba0a3ae0" +dependencies = [ + "log 0.3.9", +] + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg 1.0.1", +] + +[[package]] +name = "mio" +version = "0.6.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" +dependencies = [ + "cfg-if 0.1.10", + "fuchsia-zircon", + "fuchsia-zircon-sys", + "iovec", + "kernel32-sys", + "libc", + "log 0.4.14", + "miow", + "net2", + "slab 0.4.4", + "winapi 0.2.8", +] + +[[package]] +name = "mio-extras" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19" +dependencies = [ + "lazycell", + "log 0.4.14", + "mio", + "slab 0.4.4", +] + +[[package]] +name = "mio-uds" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0" +dependencies = [ + "iovec", + "libc", + "mio", +] + +[[package]] +name = "miow" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" +dependencies = [ + "kernel32-sys", + "net2", + "winapi 0.2.8", + "ws2_32-sys", +] + +[[package]] +name = "mkdirp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9796ba90f2e7187d7837ea05033ed6dff5320cbf2944fe2dc1da53569396ca07" + +[[package]] +name = "native-tls" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8d96b2e1c8da3957d58100b09f102c6d9cfdfced01b7ec5a8974044bb09dbd4" +dependencies = [ + "lazy_static", + "libc", + "log 0.4.14", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nest" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb122918e2e32ae9ee84b7b489dd20fec746cdad3e734095687fc2700da788a" +dependencies = [ + "atomicwrites", + "indexmap", + "lazy_static", + "log 0.4.14", + "mkdirp", + "objekt", + "serde-hjson", + "serde_json", + "serde_yaml", + "snafu 0.2.3", + "toml 0.5.8", +] + +[[package]] +name = "net2" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "nix" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7fd5681d13fda646462cfbd4e5f2051279a89a544d50eb98c365b507246839f" +dependencies = [ + "bitflags", + "bytes", + "cfg-if 0.1.10", + "gcc", + "libc", + "void", +] + +[[package]] +name = "nix" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c722bee1037d430d0f8e687bbdbf222f27cc6e4e68d5caf630857bb2b6dbdce" +dependencies = [ + "bitflags", + "cc", + "cfg-if 0.1.10", + "libc", + "void", +] + +[[package]] +name = "nix" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a" +dependencies = [ + "bitflags", + "cc", + "cfg-if 1.0.0", + "libc", +] + +[[package]] +name = "nom" +version = "6.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c5c51b9083a3c620fa67a2a635d1ce7d95b897e957d6b28ff9a5da960a103a6" +dependencies = [ + "bitvec", + "funty", + "lexical-core", + "memchr", + "version_check 0.9.3", +] + +[[package]] +name = "notify" +version = "4.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae03c8c853dba7bfd23e571ff0cff7bc9dceb40a4cd684cd1681824183f45257" +dependencies = [ + "bitflags", + "filetime", + "fsevent", + "fsevent-sys", + "inotify", + "libc", + "mio", + "mio-extras", + "walkdir", + "winapi 0.3.9", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg 1.0.1", + "num-traits 0.2.14", +] + +[[package]] +name = "num-traits" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" +dependencies = [ + "num-traits 0.2.14", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg 1.0.1", +] + +[[package]] +name = "num_cpus" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5b3dd1c072ee7963717671d1ca129f1048fda25edea6b752bfc71ac8854170" + +[[package]] +name = "objekt" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2069a3ae3dad97a4ae47754e8f47e5d2f1fd32ab7ad8a84bb31d051faa59cc3c" + +[[package]] +name = "once_cell" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" + +[[package]] +name = "opaque-debug" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "openssl" +version = "0.10.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "549430950c79ae24e6d02e0b7404534ecf311d94cc9f861e9e4020187d13d885" +dependencies = [ + "bitflags", + "cfg-if 1.0.0", + "foreign-types", + "libc", + "once_cell", + "openssl-sys", +] + +[[package]] +name = "openssl-probe" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" + +[[package]] +name = "openssl-src" +version = "111.15.0+1.1.1k" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a5f6ae2ac04393b217ea9f700cd04fa9bf3d93fae2872069f3d15d908af70a" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a7907e3bfa08bb85105209cdfcb6c63d109f8f6c1ed6ca318fff5c1853fbc1d" +dependencies = [ + "autocfg 1.0.1", + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "owning_ref" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ff55baddef9e4ad00f88b6c743a2a8062d4c6ade126c2a528644b8e444d52ce" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "parking_lot" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab41b4aed082705d1056416ae4468b6ea99d52599ecf3169b00088d43113e337" +dependencies = [ + "lock_api 0.1.5", + "parking_lot_core 0.4.0", +] + +[[package]] +name = "parking_lot" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f842b1982eb6c2fe34036a4fbfb06dd185a3f5c8edfaacdf7d1ea10b07de6252" +dependencies = [ + "lock_api 0.3.4", + "parking_lot_core 0.6.2", + "rustc_version", +] + +[[package]] +name = "parking_lot" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e" +dependencies = [ + "lock_api 0.3.4", + "parking_lot_core 0.7.2", +] + +[[package]] +name = "parking_lot_core" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94c8c7923936b28d546dfd14d4472eaf34c99b14e1c973a32b3e6d4eb04298c9" +dependencies = [ + "libc", + "rand 0.6.5", + "rustc_version", + "smallvec 0.6.14", + "winapi 0.3.9", +] + +[[package]] +name = "parking_lot_core" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b876b1b9e7ac6e1a74a6da34d25c42e17e8862aa409cbbbdcfc8d86c6f3bc62b" +dependencies = [ + "cfg-if 0.1.10", + "cloudabi", + "libc", + "redox_syscall 0.1.57", + "rustc_version", + "smallvec 0.6.14", + "winapi 0.3.9", +] + +[[package]] +name = "parking_lot_core" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d58c7c768d4ba344e3e8d72518ac13e259d7c7ade24167003b8488e10b6740a3" +dependencies = [ + "cfg-if 0.1.10", + "cloudabi", + "libc", + "redox_syscall 0.1.57", + "smallvec 1.6.1", + "winapi 0.3.9", +] + +[[package]] +name = "parse-zoneinfo" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" +dependencies = [ + "regex", +] + +[[package]] +name = "peach-config" +version = "0.1.10" +dependencies = [ + "clap", + "env_logger 0.6.2", + "lazy_static", + "log 0.4.14", + "regex", + "serde 1.0.127", + "serde_json", + "snafu 0.6.10", + "structopt", +] + +[[package]] +name = "peach-dyndns-updater" +version = "0.1.6" +dependencies = [ + "env_logger 0.6.2", + "log 0.4.14", + "peach-lib 1.2.15", +] + +[[package]] +name = "peach-lib" +version = "1.2.15" +dependencies = [ + "chrono", + "env_logger 0.6.2", + "fslock", + "jsonrpc-client-core", + "jsonrpc-client-http", + "jsonrpc-core 8.0.1", + "log 0.4.14", + "rand 0.8.4", + "regex", + "serde 1.0.127", + "serde_derive", + "serde_json", + "serde_yaml", + "snafu 0.6.10", +] + +[[package]] +name = "peach-lib" +version = "1.2.15" +source = "git+https://github.com/peachcloud/peach-lib?branch=main#c8918019926461404aca8614f42da1d9ab204c4c" +dependencies = [ + "chrono", + "env_logger 0.6.2", + "fslock", + "jsonrpc-client-core", + "jsonrpc-client-http", + "jsonrpc-core 8.0.1", + "log 0.4.14", + "rand 0.8.4", + "regex", + "serde 1.0.127", + "serde_derive", + "serde_json", + "serde_yaml", + "snafu 0.6.10", +] + +[[package]] +name = "peach-menu" +version = "0.2.7" +dependencies = [ + "chrono", + "crossbeam-channel", + "env_logger 0.6.2", + "jsonrpc-client-core", + "jsonrpc-client-http", + "jsonrpc-http-server", + "jsonrpc-test 11.0.0", + "log 0.4.14", + "peach-lib 1.2.15 (git+https://github.com/peachcloud/peach-lib?branch=main)", + "serde 1.0.127", + "serde_json", + "ws", +] + +[[package]] +name = "peach-monitor" +version = "0.1.1" +dependencies = [ + "ctrlc", + "nest", + "probes 0.3.0", + "serde_json", + "structopt", + "xdg", +] + +[[package]] +name = "peach-network" +version = "0.2.12" +dependencies = [ + "env_logger 0.6.2", + "failure", + "get_if_addrs", + "jsonrpc-core 11.0.0", + "jsonrpc-http-server", + "jsonrpc-test 11.0.0", + "log 0.4.14", + "probes 0.4.1", + "regex", + "serde 1.0.127", + "serde_json", + "snafu 0.6.10", + "wpactrl", +] + +[[package]] +name = "peach-probe" +version = "0.1.2" +dependencies = [ + "clap", + "const_format", + "env_logger 0.6.2", + "jsonrpc-client-core", + "jsonrpc-client-http", + "jsonrpc-core 14.2.0", + "jsonrpc-test 14.2.0", + "log 0.4.14", + "peach-lib 1.2.15 (git+https://github.com/peachcloud/peach-lib?branch=main)", + "regex", + "serde 1.0.127", + "serde_derive", + "serde_json", + "snafu 0.4.4", + "structopt", +] + +[[package]] +name = "peach-stats" +version = "0.1.3" +dependencies = [ + "env_logger 0.6.2", + "jsonrpc-core 11.0.0", + "jsonrpc-http-server", + "jsonrpc-test 11.0.0", + "log 0.4.14", + "probes 0.3.0", + "serde 1.0.127", + "serde_json", + "snafu 0.4.4", + "systemstat", +] + +[[package]] +name = "peach-web" +version = "0.4.11" +dependencies = [ + "env_logger 0.8.4", + "log 0.4.14", + "nest", + "openssl", + "peach-lib 1.2.15", + "percent-encoding 2.1.0", + "regex", + "rocket", + "rocket_contrib", + "serde 1.0.127", + "serde_json", + "snafu 0.6.10", + "tera 1.12.1", + "websocket", + "xdg", +] + +[[package]] +name = "pear" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5320f212db967792b67cfe12bd469d08afd6318a249bd917d5c19bc92200ab8a" +dependencies = [ + "pear_codegen", +] + +[[package]] +name = "pear_codegen" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfc1c836fdc3d1ef87c348b237b5b5c4dff922156fb2d968f57734f9669768ca" +dependencies = [ + "proc-macro2 0.4.30", + "quote 0.6.13", + "syn 0.15.44", + "version_check 0.9.3", + "yansi", +] + +[[package]] +name = "percent-encoding" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "pest" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" +dependencies = [ + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2 1.0.28", + "quote 1.0.9", + "syn 1.0.74", +] + +[[package]] +name = "pest_meta" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d" +dependencies = [ + "maplit", + "pest", + "sha-1", +] + +[[package]] +name = "pkg-config" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" + +[[package]] +name = "polyval" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebcc4aa140b9abd2bc40d9c3f7ccec842679cd79045ac3a7ac698c1a064b7cd" +dependencies = [ + "cpuid-bool", + "opaque-debug 0.3.0", + "universal-hash", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" + +[[package]] +name = "probes" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f77e66f6d6d898cbbd4a09c48fd3507cfc210b7c83055de02a38b5f7a1e6d216" +dependencies = [ + "libc", + "time 0.3.0", +] + +[[package]] +name = "probes" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb02a28631f195f482c19529ec82bec8e4ffa2d96159e67eb1ae9f5c5c902d8" +dependencies = [ + "libc", + "time 0.1.44", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2 1.0.28", + "quote 1.0.9", + "syn 1.0.74", + "version_check 0.9.3", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2 1.0.28", + "quote 1.0.9", + "version_check 0.9.3", +] + +[[package]] +name = "proc-macro2" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" +dependencies = [ + "unicode-xid 0.1.0", +] + +[[package]] +name = "proc-macro2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612" +dependencies = [ + "unicode-xid 0.2.2", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" +dependencies = [ + "proc-macro2 0.4.30", +] + +[[package]] +name = "quote" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2 1.0.28", +] + +[[package]] +name = "radium" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" + +[[package]] +name = "rand" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c" +dependencies = [ + "libc", + "rand 0.4.6", +] + +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi 0.3.9", +] + +[[package]] +name = "rand" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" +dependencies = [ + "autocfg 0.1.7", + "libc", + "rand_chacha 0.1.1", + "rand_core 0.4.2", + "rand_hc 0.1.0", + "rand_isaac", + "rand_jitter", + "rand_os", + "rand_pcg", + "rand_xorshift", + "winapi 0.3.9", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc 0.2.0", +] + +[[package]] +name = "rand" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.3", + "rand_hc 0.3.1", +] + +[[package]] +name = "rand_chacha" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" +dependencies = [ + "autocfg 0.1.7", + "rand_core 0.3.1", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.3", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom 0.2.3", +] + +[[package]] +name = "rand_hc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_hc" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" +dependencies = [ + "rand_core 0.6.3", +] + +[[package]] +name = "rand_isaac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_jitter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" +dependencies = [ + "libc", + "rand_core 0.4.2", + "winapi 0.3.9", +] + +[[package]] +name = "rand_os" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" +dependencies = [ + "cloudabi", + "fuchsia-cprng", + "libc", + "rand_core 0.4.2", + "rdrand", + "winapi 0.3.9", +] + +[[package]] +name = "rand_pcg" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" +dependencies = [ + "autocfg 0.1.7", + "rand_core 0.4.2", +] + +[[package]] +name = "rand_xorshift" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + +[[package]] +name = "redox_syscall" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a26af418b574bd56588335b3a3659a65725d4e636eb1016c2f9e3b38c7cc759" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "relay" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1576e382688d7e9deecea24417e350d3062d97e32e45d70b1cde65994ff1489a" +dependencies = [ + "futures", +] + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "rocket" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a7ab1dfdc75bb8bd2be381f37796b1b300c45a3c9145b34d86715e8dd90bf28" +dependencies = [ + "atty", + "base64 0.13.0", + "log 0.4.14", + "memchr", + "num_cpus", + "pear", + "rocket_codegen", + "rocket_http", + "state", + "time 0.1.44", + "toml 0.4.10", + "version_check 0.9.3", + "yansi", +] + +[[package]] +name = "rocket_codegen" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729e687d6d2cf434d174da84fb948f7fef4fac22d20ce94ca61c28b72dbcf9f" +dependencies = [ + "devise", + "glob 0.3.0", + "indexmap", + "quote 0.6.13", + "rocket_http", + "version_check 0.9.3", + "yansi", +] + +[[package]] +name = "rocket_contrib" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b6303dccab46dce6c7ac26c9b9d8d8cde1b19614b027c3f913be6611bff6d9b" +dependencies = [ + "glob 0.3.0", + "log 0.4.14", + "notify", + "rocket", + "serde 1.0.127", + "serde_json", + "tera 0.11.20", +] + +[[package]] +name = "rocket_http" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6131e6e6d38a9817f4a494ff5da95971451c2eb56a53915579fc9c80f6ef0117" +dependencies = [ + "cookie", + "hyper 0.10.16", + "indexmap", + "pear", + "percent-encoding 1.0.1", + "smallvec 1.6.1", + "state", + "time 0.1.44", + "unicode-xid 0.1.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dead70b0b5e03e9c814bcb6b01e03e68f7c57a80aa48c72ec92152ab3e818d49" + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver", +] + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +dependencies = [ + "lazy_static", + "winapi 0.3.9", +] + +[[package]] +name = "scoped-tls" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "332ffa32bf586782a3efaeb58f127980944bbc8c4d6913a86107ac2a5ab24b28" + +[[package]] +name = "scopeguard" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94258f53601af11e6a49f722422f6e3425c52b06245a5cf9bc09908b174f5e27" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "security-framework" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23a2ac85147a3a11d77ecf1bc7166ec0b92febfa4461c37944e180f319ece467" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e4effb91b4b8b6fb7732e670b6cee160278ff8e6bf485c7805d9e319d76e284" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "serde" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dad3f759919b92c3068c696c15c3d17238234498bbdcc80f2c469606f948ac8" + +[[package]] +name = "serde" +version = "1.0.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f03b9878abf6d14e6779d3f24f07b2cfa90352cfec4acc5aab8f1ac7f146fae8" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-hjson" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a3a4e0ea8a88553209f6cc6cfe8724ecad22e1acf372793c27d995290fe74f8" +dependencies = [ + "lazy_static", + "linked-hash-map 0.3.0", + "num-traits 0.1.43", + "regex", + "serde 0.8.23", +] + +[[package]] +name = "serde_derive" +version = "1.0.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a024926d3432516606328597e0f224a51355a493b49fdd67e9209187cbe55ecc" +dependencies = [ + "proc-macro2 1.0.28", + "quote 1.0.9", + "syn 1.0.74", +] + +[[package]] +name = "serde_json" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "336b10da19a12ad094b59d870ebde26a45402e5b470add4b5fd03c5048a32127" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde 1.0.127", +] + +[[package]] +name = "serde_test" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "110b3dbdf8607ec493c22d5d947753282f3bae73c0f56d322af1e8c78e4c23d5" +dependencies = [ + "serde 0.8.23", +] + +[[package]] +name = "serde_yaml" +version = "0.8.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15654ed4ab61726bf918a39cb8d98a2e2995b002387807fa6ba58fdf7f59bb23" +dependencies = [ + "dtoa", + "linked-hash-map 0.5.4", + "serde 1.0.127", + "yaml-rust", +] + +[[package]] +name = "sha-1" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" +dependencies = [ + "block-buffer 0.7.3", + "digest 0.8.1", + "fake-simd", + "opaque-debug 0.2.3", +] + +[[package]] +name = "sha2" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b362ae5752fd2137731f9fa25fd4d9058af34666ca1966fb969119cc35719f12" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if 1.0.0", + "cpufeatures", + "digest 0.9.0", + "opaque-debug 0.3.0", +] + +[[package]] +name = "slab" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b4fcaed89ab08ef143da37bc52adbcc04d4a69014f4c1208d6b51f0c47bc23" + +[[package]] +name = "slab" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590" + +[[package]] +name = "slug" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3bc762e6a4b6c6fcaade73e77f9ebc6991b676f88bb2358bddb56560f073373" +dependencies = [ + "deunicode", +] + +[[package]] +name = "smallvec" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8cbcd6df1e117c2210e13ab5109635ad68a929fcbb8964dc965b76cb5ee013" + +[[package]] +name = "smallvec" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97fcaeba89edba30f044a10c6a3cc39df9c3f17d7cd829dd1446cab35f890e0" +dependencies = [ + "maybe-uninit", +] + +[[package]] +name = "smallvec" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" + +[[package]] +name = "snafu" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a2e16c9b74a09d4bc84ea6e5eb68fef70b4cb14e8ccd26f2de76af2e9c2e8a" +dependencies = [ + "backtrace", + "snafu-derive 0.2.3", +] + +[[package]] +name = "snafu" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b028158eb06caa8345bee10cccfb25fa632beccf0ef5308832b4fd4b78a7db48" +dependencies = [ + "backtrace", + "doc-comment", + "snafu-derive 0.4.4", +] + +[[package]] +name = "snafu" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eab12d3c261b2308b0d80c26fffb58d17eba81a4be97890101f416b478c79ca7" +dependencies = [ + "doc-comment", + "snafu-derive 0.6.10", +] + +[[package]] +name = "snafu-derive" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf96650c2b31fa949780f792025f4ca79fbe87bd723a8b3573dce37be7193b08" +dependencies = [ + "proc-macro2 0.4.30", + "quote 0.6.13", + "syn 0.15.44", +] + +[[package]] +name = "snafu-derive" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf50aaef500c248a590e2696e8bf8c7620ca2235b9bb90a70363d82dd1abec6a" +dependencies = [ + "proc-macro2 0.4.30", + "quote 0.6.13", + "syn 0.15.44", +] + +[[package]] +name = "snafu-derive" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1508efa03c362e23817f96cde18abed596a25219a8b2c66e8db33c03543d315b" +dependencies = [ + "proc-macro2 1.0.28", + "quote 1.0.9", + "syn 1.0.74", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "state" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3015a7d0a5fd5105c91c3710d42f9ccf0abfb287d62206484dcc67f9569a6483" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "string" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24114bfcceb867ca7f71a0d3fe45d45619ec47a6fbfa98cb14e14250bfa5d6d" +dependencies = [ + "bytes", +] + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "structopt" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b041cdcb67226aca307e6e7be44c8806423d83e018bd662360a93dabce4d71" +dependencies = [ + "clap", + "lazy_static", + "structopt-derive", +] + +[[package]] +name = "structopt-derive" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7813934aecf5f51a54775e00068c237de98489463968231a51746bbbc03f9c10" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2 1.0.28", + "quote 1.0.9", + "syn 1.0.74", +] + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "syn" +version = "0.15.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" +dependencies = [ + "proc-macro2 0.4.30", + "quote 0.6.13", + "unicode-xid 0.1.0", +] + +[[package]] +name = "syn" +version = "1.0.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c" +dependencies = [ + "proc-macro2 1.0.28", + "quote 1.0.9", + "unicode-xid 0.2.2", +] + +[[package]] +name = "synstructure" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "474aaa926faa1603c40b7885a9eaea29b444d1cb2850cb7c0e37bb1a4182f4fa" +dependencies = [ + "proc-macro2 1.0.28", + "quote 1.0.9", + "syn 1.0.74", + "unicode-xid 0.2.2", +] + +[[package]] +name = "systemstat" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a934f8fe2f893260080fdde71e840b35308f48bf3bd3b261cb24e668c4b48db3" +dependencies = [ + "bytesize", + "chrono", + "lazy_static", + "libc", + "nom", + "time 0.1.44", + "winapi 0.3.9", +] + +[[package]] +name = "take" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b157868d8ac1f56b64604539990685fa7611d8fa9e5476cf0c02cf34d32917c5" + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempdir" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" +dependencies = [ + "rand 0.4.6", + "remove_dir_all", +] + +[[package]] +name = "tempfile" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "rand 0.8.4", + "redox_syscall 0.2.10", + "remove_dir_all", + "winapi 0.3.9", +] + +[[package]] +name = "tera" +version = "0.11.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b505279e19d8f7d24b1a9dc58327c9c36174b1a2c7ebdeac70792d017cb64f3" +dependencies = [ + "chrono", + "error-chain", + "glob 0.2.11", + "humansize", + "lazy_static", + "pest", + "pest_derive", + "regex", + "serde 1.0.127", + "serde_json", + "slug", + "unic-segment 0.7.0", + "url", +] + +[[package]] +name = "tera" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95b0d8a46da5fe3ea119394a6c7f1e745f9de359081641c99946e2bf55d4f2" +dependencies = [ + "chrono", + "chrono-tz", + "globwalk", + "humansize", + "lazy_static", + "percent-encoding 2.1.0", + "pest", + "pest_derive", + "rand 0.8.4", + "regex", + "serde 1.0.127", + "serde_json", + "slug", + "unic-segment 0.9.0", +] + +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thread_local" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd" +dependencies = [ + "once_cell", +] + +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi 0.3.9", +] + +[[package]] +name = "time" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cf2535c6456e772ad756a0854ec907ede55d73d0b5a34855d808cb2d2f0942e" +dependencies = [ + "libc", +] + +[[package]] +name = "tinyvec" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "848a1e1181b9f6753b5e96a092749e29b11d19ede67dfbbd6c7dc7e0f49b5338" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "tokio" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a09c0b5bb588872ab2f09afa13ee6e9dac11e10a0ec9e8e3ba39a5a5d530af6" +dependencies = [ + "bytes", + "futures", + "mio", + "num_cpus", + "tokio-codec", + "tokio-current-thread", + "tokio-executor", + "tokio-fs", + "tokio-io", + "tokio-reactor", + "tokio-sync", + "tokio-tcp", + "tokio-threadpool", + "tokio-timer", + "tokio-udp", + "tokio-uds", +] + +[[package]] +name = "tokio-buf" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fb220f46c53859a4b7ec083e41dec9778ff0b1851c0942b211edb89e0ccdc46" +dependencies = [ + "bytes", + "either", + "futures", +] + +[[package]] +name = "tokio-codec" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25b2998660ba0e70d18684de5d06b70b70a3a747469af9dea7618cc59e75976b" +dependencies = [ + "bytes", + "futures", + "tokio-io", +] + +[[package]] +name = "tokio-core" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87b1395334443abca552f63d4f61d0486f12377c2ba8b368e523f89e828cffd4" +dependencies = [ + "bytes", + "futures", + "iovec", + "log 0.4.14", + "mio", + "scoped-tls", + "tokio", + "tokio-executor", + "tokio-io", + "tokio-reactor", + "tokio-timer", +] + +[[package]] +name = "tokio-current-thread" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1de0e32a83f131e002238d7ccde18211c0a5397f60cbfffcb112868c2e0e20e" +dependencies = [ + "futures", + "tokio-executor", +] + +[[package]] +name = "tokio-executor" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb2d1b8f4548dbf5e1f7818512e9c406860678f29c300cdf0ebac72d1a3a1671" +dependencies = [ + "crossbeam-utils 0.7.2", + "futures", +] + +[[package]] +name = "tokio-fs" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297a1206e0ca6302a0eed35b700d292b275256f596e2f3fea7729d5e629b6ff4" +dependencies = [ + "futures", + "tokio-io", + "tokio-threadpool", +] + +[[package]] +name = "tokio-io" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57fc868aae093479e3131e3d165c93b1c7474109d13c90ec0dda2a1bbfff0674" +dependencies = [ + "bytes", + "futures", + "log 0.4.14", +] + +[[package]] +name = "tokio-proto" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fbb47ae81353c63c487030659494b295f6cb6576242f907f203473b191b0389" +dependencies = [ + "futures", + "log 0.3.9", + "net2", + "rand 0.3.23", + "slab 0.3.0", + "smallvec 0.2.1", + "take", + "tokio-core", + "tokio-io", + "tokio-service", +] + +[[package]] +name = "tokio-reactor" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09bc590ec4ba8ba87652da2068d150dcada2cfa2e07faae270a5e0409aa51351" +dependencies = [ + "crossbeam-utils 0.7.2", + "futures", + "lazy_static", + "log 0.4.14", + "mio", + "num_cpus", + "parking_lot 0.9.0", + "slab 0.4.4", + "tokio-executor", + "tokio-io", + "tokio-sync", +] + +[[package]] +name = "tokio-service" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24da22d077e0f15f55162bdbdc661228c1581892f52074fb242678d015b45162" +dependencies = [ + "futures", +] + +[[package]] +name = "tokio-sync" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfe50152bc8164fcc456dab7891fa9bf8beaf01c5ee7e1dd43a397c3cf87dee" +dependencies = [ + "fnv", + "futures", +] + +[[package]] +name = "tokio-tcp" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98df18ed66e3b72e742f185882a9e201892407957e45fbff8da17ae7a7c51f72" +dependencies = [ + "bytes", + "futures", + "iovec", + "mio", + "tokio-io", + "tokio-reactor", +] + +[[package]] +name = "tokio-threadpool" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df720b6581784c118f0eb4310796b12b1d242a7eb95f716a8367855325c25f89" +dependencies = [ + "crossbeam-deque", + "crossbeam-queue", + "crossbeam-utils 0.7.2", + "futures", + "lazy_static", + "log 0.4.14", + "num_cpus", + "slab 0.4.4", + "tokio-executor", +] + +[[package]] +name = "tokio-timer" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93044f2d313c95ff1cb7809ce9a7a05735b012288a888b62d4434fd58c94f296" +dependencies = [ + "crossbeam-utils 0.7.2", + "futures", + "slab 0.4.4", + "tokio-executor", +] + +[[package]] +name = "tokio-tls" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "354b8cd83825b3c20217a9dc174d6a0c67441a2fae5c41bcb1ea6679f6ae0f7c" +dependencies = [ + "futures", + "native-tls", + "tokio-io", +] + +[[package]] +name = "tokio-udp" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2a0b10e610b39c38b031a2fcab08e4b82f16ece36504988dcbd81dbba650d82" +dependencies = [ + "bytes", + "futures", + "log 0.4.14", + "mio", + "tokio-codec", + "tokio-io", + "tokio-reactor", +] + +[[package]] +name = "tokio-uds" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab57a4ac4111c8c9dbcf70779f6fc8bc35ae4b2454809febac840ad19bd7e4e0" +dependencies = [ + "bytes", + "futures", + "iovec", + "libc", + "log 0.4.14", + "mio", + "mio-uds", + "tokio-codec", + "tokio-io", + "tokio-reactor", +] + +[[package]] +name = "toml" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758664fc71a3a69038656bee8b6be6477d2a6c315a6b81f7081f591bffa4111f" +dependencies = [ + "serde 1.0.127", +] + +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "indexmap", + "serde 1.0.127", +] + +[[package]] +name = "traitobject" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efd1f82c56340fdf16f2a953d7bda4f8fdffba13d93b00844c25572110b26079" + +[[package]] +name = "try-lock" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee2aa4715743892880f70885373966c83d73ef1b0838a664ef0c76fffd35e7c2" + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]] +name = "typeable" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1410f6f91f21d1612654e7cc69193b0334f909dcf2c790c4826254fbb86f8887" + +[[package]] +name = "typenum" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" + +[[package]] +name = "ucd-trie" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" + +[[package]] +name = "unic-char-property" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36d3f7ce754afdbccccf8ff0dd0134e50fb44aaae579f96218856e9e5dbd1e" +dependencies = [ + "unic-char-range 0.7.0", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range 0.9.0", +] + +[[package]] +name = "unic-char-range" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9ab85fab42ad1b26cafc03bf891f69cb4d6e15f491030e89a0122197baa8ae8" + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff8d4a7ade929ef7d971e16ced21a8cd56a63869aa6032dfb8cb083cf7d077bf" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-segment" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9ca47cbb09fb5fcd066b5867d11dc528302fa465277882797d6a836e1ee6f9e" +dependencies = [ + "unic-ucd-segment 0.7.0", +] + +[[package]] +name = "unic-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23" +dependencies = [ + "unic-ucd-segment 0.9.0", +] + +[[package]] +name = "unic-ucd-segment" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f1a08ce0409a9e391b88d1930118eec48af12742fc538bcec55f775865776e" +dependencies = [ + "unic-char-property 0.7.0", + "unic-char-range 0.7.0", + "unic-ucd-version 0.7.0", +] + +[[package]] +name = "unic-ucd-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700" +dependencies = [ + "unic-char-property 0.9.0", + "unic-char-range 0.9.0", + "unic-ucd-version 0.9.0", +] + +[[package]] +name = "unic-ucd-version" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1f5e6c6c53c2d0ece4a5964bc55fcff8602153063cb4fab20958ff32998ff6" +dependencies = [ + "unic-common 0.7.0", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common 0.9.0", +] + +[[package]] +name = "unicase" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4765f83163b74f957c797ad9253caf97f103fb064d3999aea9568d09fc8a33" +dependencies = [ + "version_check 0.1.5", +] + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check 0.9.3", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeb8be209bb1c96b7c177c7420d26e04eccacb0eeae6b980e35fcb74678107e0" +dependencies = [ + "matches", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" + +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + +[[package]] +name = "unicode-xid" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "universal-hash" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" +dependencies = [ + "generic-array 0.14.4", + "subtle", +] + +[[package]] +name = "url" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" +dependencies = [ + "idna", + "matches", + "percent-encoding 1.0.1", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" + +[[package]] +name = "version_check" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi 0.3.9", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a05d9d966753fa4b5c8db73fcab5eed4549cfe0e1e4e66911e5564a0085c35d1" +dependencies = [ + "futures", + "log 0.4.14", + "try-lock 0.1.0", +] + +[[package]] +name = "want" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6395efa4784b027708f7451087e647ec73cc74f5d9bc2e418404248d679a230" +dependencies = [ + "futures", + "log 0.4.14", + "try-lock 0.2.3", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "websocket" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "723abe6b75286edc51d8ecabb38a2353f62a9e9b0588998b59111474f1dcd637" +dependencies = [ + "bytes", + "futures", + "hyper 0.10.16", + "native-tls", + "rand 0.6.5", + "tokio-codec", + "tokio-io", + "tokio-reactor", + "tokio-tcp", + "tokio-tls", + "unicase 1.4.2", + "url", + "websocket-base", +] + +[[package]] +name = "websocket-base" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403f3fd505ff930da84156389639932955fb09705b3dccd1a3d60c8e7ff62776" +dependencies = [ + "base64 0.10.1", + "bitflags", + "byteorder", + "bytes", + "futures", + "native-tls", + "rand 0.6.5", + "sha-1", + "tokio-codec", + "tokio-io", + "tokio-tcp", + "tokio-tls", +] + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "wpactrl" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f487ac0a84d67974aa9ca3a7a284cb4821869d677376768f5c303fa786635c13" +dependencies = [ + "failure", + "log 0.4.14", + "nix 0.10.0", +] + +[[package]] +name = "ws" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec91ea61b83ce033c43c06c52ddc7532f465c0153281610d44c58b74083aee1a" +dependencies = [ + "byteorder", + "bytes", + "httparse", + "log 0.4.14", + "mio", + "mio-extras", + "rand 0.6.5", + "sha-1", + "slab 0.4.4", + "url", +] + +[[package]] +name = "ws2_32-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "wyz" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" + +[[package]] +name = "xdg" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d089681aa106a86fade1b0128fb5daf07d5867a509ab036d99988dec80429a57" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map 0.5.4", +] + +[[package]] +name = "yansi" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc79f4a1e39857fc00c3f662cbf2651c771f00e9c15fe2abc341806bd46bd71" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..cd17e5e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[workspace] + +members = [ + "peach-lib", + "peach-config", + "peach-network", + "peach-web", + "peach-menu", + "peach-monitor", + "peach-stats", + "peach-probe", + "peach-dyndns-updater" +] diff --git a/peach-config/.cargo/config b/peach-config/.cargo/config new file mode 100644 index 0000000..4b6f460 --- /dev/null +++ b/peach-config/.cargo/config @@ -0,0 +1,4 @@ +[target.aarch64-unknown-linux-gnu] +linker = "aarch64-linux-gnu-gcc" +objcopy = { path ="aarch64-linux-gnu-objcopy" } +strip = { path ="aarch64-linux-gnu-strip" } diff --git a/peach-config/.gitignore b/peach-config/.gitignore new file mode 100644 index 0000000..eb5a316 --- /dev/null +++ b/peach-config/.gitignore @@ -0,0 +1 @@ +target diff --git a/peach-config/Cargo.toml b/peach-config/Cargo.toml new file mode 100644 index 0000000..d36b1f4 --- /dev/null +++ b/peach-config/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "peach-config" +version = "0.1.10" +authors = ["Andrew Reid ", "Max Fowler "] +edition = "2018" +description = "Command line tool for installing, updating and configuring PeachCloud" +homepage = "https://opencollective.com/peachcloud" +repository = "https://github.com/peachcloud/peach-config" +readme = "README.md" +license = "AGPL-3.0-only" +publish = false + +[package.metadata.deb] +depends = "$auto" +extended-description = """\ +peach-config is a command line tool for installing, updating and configuring PeachCloud""" +maintainer-scripts="debian" +assets = [ + ["target/release/peach-config", "usr/bin/", "755"], + ["conf/**/*", "/var/lib/peachcloud/conf/", "644"], + ["README.md", "usr/share/doc/peach-config/README", "644"], +] + +[badges] +travis-ci = { repository = "peachcloud/peach-config", branch = "main" } +maintenance = { status = "actively-developed" } + +[dependencies] +env_logger = "0.6" +serde = { version = "1", features = ["derive"] } +serde_json = "1.0.64" +snafu = "0.6" +regex = "1" +structopt = "0.3.13" +clap = "2.33.3" +log = "0.4" +lazy_static = "1.4.0" diff --git a/peach-config/README.md b/peach-config/README.md new file mode 100644 index 0000000..79355d4 --- /dev/null +++ b/peach-config/README.md @@ -0,0 +1,94 @@ +# peach-config + +[![Build Status](https://travis-ci.com/peachcloud/peach-config.svg?branch=main)](https://travis-ci.com/peachcloud/peach-config) +![Generic badge](https://img.shields.io/badge/version-0.1.10-.svg) + +Rust crate which provides a CLI tool for installing and updating PeachCloud. + + + +## Installation From PeachCloud Disc Image + +The recommended way to install PeachCloud is to download the latest PeachCloud disc image from http://releases.peachcloud.org, +and flash it to an SD card. peach-config is included as part of this disc image, and can then +be used as a tool for updating PeachCloud as needed. + +You can find detailed instructions on setting up PeachCloud from a PeachCloud disc image [here](docs/installation-from-peach-disc-image.md). + + +## Installation From Debian Disc Image + +You can find a guide for installing plain Debian onto a Raspberry pi [here](docs/installation-from-debian-disc-image.md). + +Once you have Debian running on your pi, you can install peach-config by adding the PeachCloud apt repository and using apt. + +To add the PeachCloud Debian package archive as an apt source, run the following commands from your Pi: + +``` bash +echo "deb http://apt.peachcloud.org/ buster main" > /etc/apt/sources.list.d/peach.list +wget -O - http://apt.peachcloud.org/pubkey.gpg | sudo apt-key add - +``` + +You can then install peach-config with apt: + +``` bash +sudo apt-get update +sudo apt-get install python3-peach-config +``` + +Alternatively you can run the following one-liner, which does all of the above: +> curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/peachcloud/peach-config-rust/main/install.sh | sh + +peach-config has only been tested on a Raspberry Pi 3 B+ running Debian 10. + + +## Usage + +The peach-config debian module installs a command-line tool to `/usr/bin/peach-config`. + +`peach-config` is a tool for installing PeachCloud and for updating it. + +`peach-config -h` shows the help menu: + +```bash +USAGE: + peach-config [FLAGS] [SUBCOMMAND] + +FLAGS: + -h, --help Prints help information + -V, --version Prints version information + -v, --verbose + +SUBCOMMANDS: + help Prints this message or the help of the given subcommand(s) + manifest Prints json manifest of peach configurations + setup Idempotent setup of PeachCloud + update Updates all PeachCloud microservices +``` + +The setup command takes a few different parameters to customize configuration. +```bash +USAGE: + peach-config setup [FLAGS] [OPTIONS] + +FLAGS: + -d, --default-locale Use the default en_US.UTF-8 locale for compatability + -h, --help Prints help information + -i, --i2c Setup i2c configurations + -n, --no-input Run peach-config in non-interactive mode + -V, --version Prints version information + +OPTIONS: + -r, --rtc Optionally select which model of real-time-clock is being used {ds1307, ds3231} +``` + +I2C configuration is necessary for the OLED display and physical interface to work correctly. RTC configuration is required for the real-time clock to work correctly. When passing the `-r` flag, the type of real-time clock module must be included (either ds1307 or ds3231). Selecting real-time clock configuration will not work if the I2C flag is not selected (in other words, the real-time clock requires I2C). + +Run the script as follows for a full installation and configuration with I2C and the ds3231 RTC module: + +`peach-config setup -i -r ds3231 -n -d` + + +## Licensing + +AGPL-3.0 \ No newline at end of file diff --git a/peach-config/build.sh b/peach-config/build.sh new file mode 100755 index 0000000..c1a70ae --- /dev/null +++ b/peach-config/build.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +sudo RUST_LOG=info cargo build diff --git a/peach-config/conf/50-gpio.rules b/peach-config/conf/50-gpio.rules new file mode 100644 index 0000000..89069ef --- /dev/null +++ b/peach-config/conf/50-gpio.rules @@ -0,0 +1 @@ +SUBSYSTEM=="gpio", KERNEL=="gpiochip*", ACTION=="add", PROGRAM="/bin/sh -c 'chown -R root:gpio-user /dev/gpiochip* ; chmod -R 770 /dev/gpiochip*'" diff --git a/peach-config/conf/README.md b/peach-config/conf/README.md new file mode 100644 index 0000000..f725b06 --- /dev/null +++ b/peach-config/conf/README.md @@ -0,0 +1,20 @@ +# Configuration File Paths + +```bash +/boot/firmware/bcm2710-rpi-3-b.dtb +/boot/firmware/config.txt +/boot/firmware/overlays/mygpio.dtbo +/etc/default/hostapd +/etc/dhcpd.conf +/etc/dnsmasq.conf +/etc/hostapd/hostapd.conf +/etc/hostname +/etc/hosts +/etc/modules +/etc/network/interfaces +/etc/nginx/sites-available/peach.conf +/etc/systemd/system/activate-rtc.service +/etc/udev/rules.d/00-accesspoint.rules +/etc/wpa_supplicant/wpa_supplicant.conf +``` + diff --git a/peach-config/conf/activate-rtc.service b/peach-config/conf/activate-rtc.service new file mode 100644 index 0000000..8283ebd --- /dev/null +++ b/peach-config/conf/activate-rtc.service @@ -0,0 +1,10 @@ +[Unit] +Description=Add RTC as new I2C device + +[Service] +Type=oneshot +RemainAfterExit=no +ExecStart=/usr/local/bin/activate_rtc + +[Install] +WantedBy=multi-user.target diff --git a/peach-config/conf/activate_rtc.sh b/peach-config/conf/activate_rtc.sh new file mode 100755 index 0000000..19e505b --- /dev/null +++ b/peach-config/conf/activate_rtc.sh @@ -0,0 +1,2 @@ +#!/bin/bash +echo ds1307 0x68 > /sys/class/i2c-adapter/i2c-1/new_device \ No newline at end of file diff --git a/peach-config/conf/ap_auto_deploy.sh b/peach-config/conf/ap_auto_deploy.sh new file mode 100755 index 0000000..af868ea --- /dev/null +++ b/peach-config/conf/ap_auto_deploy.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +# Start the ap0 service (access point) if wlan0 is active but not connected + +# returns "active" or "inactive" +wlan_active=$(/usr/bin/systemctl is-active wpa_supplicant@wlan0.service) + +# returns "up" or "down" +wlan_state=$(cat /sys/class/net/wlan0/operstate) + +if [ $wlan_active = "active" ] && [ $wlan_state = "down" ]; then + echo "Starting ap0 service" + /usr/bin/systemctl start wpa_supplicant@ap0.service +fi diff --git a/peach-config/conf/backup/00-accesspoint.rules b/peach-config/conf/backup/00-accesspoint.rules new file mode 100644 index 0000000..335aec9 --- /dev/null +++ b/peach-config/conf/backup/00-accesspoint.rules @@ -0,0 +1,2 @@ +SUBSYSTEM=="net", ACTION=="add", RUN+="/usr/sbin/iw dev wlan0 interface add ap0 type __ap" +SUBSYSTEM=="net", ACTION=="add", RUN+="/usr/bin/ip address add 11.11.11.10/24 brd + dev ap0" diff --git a/peach-config/conf/backup/README b/peach-config/conf/backup/README new file mode 100644 index 0000000..ce11f65 --- /dev/null +++ b/peach-config/conf/backup/README @@ -0,0 +1,3 @@ +# Backup + +This directory contains all the legacy configuration files for PeachCloud networking. These files have been deprecated by the transition to using systemd-networkd for networking. They are being kept here as a backup but will eventually be removed entirely. diff --git a/peach-config/conf/backup/dhcpd.conf b/peach-config/conf/backup/dhcpd.conf new file mode 100644 index 0000000..5c2b99a --- /dev/null +++ b/peach-config/conf/backup/dhcpd.conf @@ -0,0 +1,3 @@ +interface ap0 + static ip_address=11.11.11.10/24 + nohook wpa_supplicant diff --git a/peach-config/conf/backup/dnsmasq.conf b/peach-config/conf/backup/dnsmasq.conf new file mode 100644 index 0000000..15c2921 --- /dev/null +++ b/peach-config/conf/backup/dnsmasq.conf @@ -0,0 +1,8 @@ +interface=ap0 +listen-address=11.11.11.10 +bind-dynamic +server=208.67.222.222 +server=208.67.220.220 +domain-needed +bogus-priv +dhcp-range=11.11.11.11,11.11.11.30,255.255.255.0,24h diff --git a/peach-config/conf/backup/hostapd b/peach-config/conf/backup/hostapd new file mode 100644 index 0000000..ec2c39e --- /dev/null +++ b/peach-config/conf/backup/hostapd @@ -0,0 +1,22 @@ +# Defaults for hostapd initscript +# +# WARNING: The DAEMON_CONF setting has been deprecated and will be removed +# in future package releases. +# +# See /usr/share/doc/hostapd/README.Debian for information about alternative +# methods of managing hostapd. +# +# Uncomment and set DAEMON_CONF to the absolute path of a hostapd configuration +# file and hostapd will be started during system boot. An example configuration +# file can be found at /usr/share/doc/hostapd/examples/hostapd.conf.gz +# +DAEMON_CONF="/etc/hostapd/hostapd.conf" +# Additional daemon options to be appended to hostapd command:- +# -d show more debug messages (-dd for even more) +# -K include key data in debug messages +# -t include timestamps in some debug messages +# +# Note that -B (daemon mode) and -P (pidfile) options are automatically +# configured by the init.d script and must not be added to DAEMON_OPTS. +# +#DAEMON_OPTS="" diff --git a/peach-config/conf/backup/hostapd.conf b/peach-config/conf/backup/hostapd.conf new file mode 100644 index 0000000..e4be7d1 --- /dev/null +++ b/peach-config/conf/backup/hostapd.conf @@ -0,0 +1,15 @@ +interface=ap0 +hw_mode=g +channel=8 +wmm_enabled=0 +macaddr_acl=0 +beacon_int=100 +auth_algs=3 +wmm_enabled=1 +ignore_broadcast_ssid=0 +wpa=2 +wpa_key_mgmt=WPA-PSK +wpa_pairwise=TKIP +rsn_pairwise=CCMP +ssid=peach +wpa_passphrase=cloudless diff --git a/peach-config/conf/backup/interfaces b/peach-config/conf/backup/interfaces new file mode 100644 index 0000000..1d5c2e6 --- /dev/null +++ b/peach-config/conf/backup/interfaces @@ -0,0 +1,22 @@ +# interfaces(5) file used by ifup(8) and ifdown(8) +# Include files from /etc/network/interfaces.d: +# source-directory /etc/network/interfaces.d + +# Loopback +auto lo +iface lo inet loopback + +# Ethernet +iface eth0 inet dhcp + +# Wireless +auto wlan0 +iface wlan0 inet dhcp + wpa-conf /etc/wpa_supplicant/wpa_supplicant.conf + +# Access Point +iface ap0 inet static + address 11.11.11.10 + netmask 255.255.255.0 + network 11.11.11.0 + broadcast 11.11.11.255 diff --git a/peach-config/conf/backup/network b/peach-config/conf/backup/network new file mode 100644 index 0000000..9652221 --- /dev/null +++ b/peach-config/conf/backup/network @@ -0,0 +1,17 @@ +# +# Allow peach-network user to execute activate_ap and +# activate_client scripts without needing to enter +# a password for sudo'd command. +# + +# User alias for PeachCloud microservices which control networking +User_Alias PEACH_NTWK = peach-network + +# Command alias for activate_ap and activate_client scripts +Cmnd_Alias SCRIPTS = /usr/local/bin/activate_ap, /usr/local/bin/activate_client + +# Command alias for network-related actions +Cmnd_Alias SERVICE = /usr/bin/systemctl unmask hostapd, /usr/bin/systemctl start hostapd, /usr/bin/systemctl stop hostapd, /usr/bin/systemctl stop dnsmasq, /usr/bin/systemctl start dnsmasq, /usr/bin/systemctl start wpa_supplicant, /usr/bin/systemctl stop wpa_supplicant, /usr/sbin/ifup wlan0, /usr/sbin/ifdown wlan0, /bin/ip link set wlan0 mode default + +# Allow PEACH_NTWK users to execute SCRIPTS & SERVICE commands without password +PEACH_NTWK ALL=(ALL) NOPASSWD: SCRIPTS, SERVICE diff --git a/peach-config/conf/backup/wpa_supplicant.conf b/peach-config/conf/backup/wpa_supplicant.conf new file mode 100644 index 0000000..94a9ae0 --- /dev/null +++ b/peach-config/conf/backup/wpa_supplicant.conf @@ -0,0 +1,8 @@ +ctrl_interface=/run/wpa_supplicant +update_config=1 + +network={ + ssid="YOUR_SSID" + #psk="YOUR_PASS" + psk=7f4f1a9d128f9fc2741f4229d57e3e7c355b1760b27aa8c3816c461497f5cd2a +} diff --git a/peach-config/conf/bcm2710-rpi-3-b.dtb b/peach-config/conf/bcm2710-rpi-3-b.dtb new file mode 100755 index 0000000..55009c8 Binary files /dev/null and b/peach-config/conf/bcm2710-rpi-3-b.dtb differ diff --git a/peach-config/conf/config.txt b/peach-config/conf/config.txt new file mode 100755 index 0000000..f2392c7 --- /dev/null +++ b/peach-config/conf/config.txt @@ -0,0 +1,9 @@ +# Switch the CPU from ARMv7 into ARMv8 (aarch64) mode +arm_control=0x200 + +enable_uart=1 + +kernel=vmlinuz-4.19.0-10-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-10-arm64 diff --git a/peach-config/conf/config.txt_ds1307 b/peach-config/conf/config.txt_ds1307 new file mode 100755 index 0000000..55bb814 --- /dev/null +++ b/peach-config/conf/config.txt_ds1307 @@ -0,0 +1,16 @@ +# Switch the CPU from ARMv7 into ARMv8 (aarch64) mode +arm_control=0x200 + +enable_uart=1 +upstream_kernel=1 +# Activate I2C +dtparam=i2c_arm=on +# Activate DS1307 RTC module +dtoverlay=i2c-rtc,ds1307 +# Apply device tree overlay to enable pull-up resistors for buttons +device_tree_overlay=overlays/mygpio.dtbo + +kernel=vmlinuz-4.19.0-10-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-10-arm64 diff --git a/peach-config/conf/config.txt_ds3231 b/peach-config/conf/config.txt_ds3231 new file mode 100755 index 0000000..69de18f --- /dev/null +++ b/peach-config/conf/config.txt_ds3231 @@ -0,0 +1,16 @@ +# Switch the CPU from ARMv7 into ARMv8 (aarch64) mode +arm_control=0x200 + +enable_uart=1 +upstream_kernel=1 +# Activate I2C +dtparam=i2c_arm=on +# Activate DS3231 RTC module +dtoverlay=i2c-rtc,ds3231 +# Apply device tree overlay to enable pull-up resistors for buttons +device_tree_overlay=overlays/mygpio.dtbo + +kernel=vmlinuz-4.19.0-10-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-10-arm64 diff --git a/peach-config/conf/config.txt_i2c b/peach-config/conf/config.txt_i2c new file mode 100755 index 0000000..e23a6f5 --- /dev/null +++ b/peach-config/conf/config.txt_i2c @@ -0,0 +1,14 @@ +# Switch the CPU from ARMv7 into ARMv8 (aarch64) mode +arm_control=0x200 + +enable_uart=1 +upstream_kernel=1 +# Activate I2C +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 +# 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 diff --git a/peach-config/conf/hostname b/peach-config/conf/hostname new file mode 100644 index 0000000..d9da5dc --- /dev/null +++ b/peach-config/conf/hostname @@ -0,0 +1 @@ +peach diff --git a/peach-config/conf/hosts b/peach-config/conf/hosts new file mode 100644 index 0000000..c90d86b --- /dev/null +++ b/peach-config/conf/hosts @@ -0,0 +1,5 @@ +127.0.0.1 localhost +127.0.1.1 peach +::1 localhost ip6-localhost ip6-loopback +ff02::1 ip6-allnodes +ff02::2 ip6-allrouters diff --git a/peach-config/conf/modules b/peach-config/conf/modules new file mode 100644 index 0000000..f81858a --- /dev/null +++ b/peach-config/conf/modules @@ -0,0 +1,6 @@ +# /etc/modules: kernel modules to load at boot time. +# +# This file contains the names of kernel modules that should be loaded +# at boot time, one per line. Lines beginning with "#" are ignored. +i2c-dev +i2c-bcm2835 diff --git a/peach-config/conf/modules_rtc b/peach-config/conf/modules_rtc new file mode 100644 index 0000000..8fc6d85 --- /dev/null +++ b/peach-config/conf/modules_rtc @@ -0,0 +1,7 @@ +# /etc/modules: kernel modules to load at boot time. +# +# This file contains the names of kernel modules that should be loaded +# at boot time, one per line. Lines beginning with "#" are ignored. +i2c-dev +i2c-bcm2835 +rtc-ds1307 diff --git a/peach-config/conf/mygpio.dtbo b/peach-config/conf/mygpio.dtbo new file mode 100755 index 0000000..92fc8ea Binary files /dev/null and b/peach-config/conf/mygpio.dtbo differ diff --git a/peach-config/conf/network/04-wired.network b/peach-config/conf/network/04-wired.network new file mode 100644 index 0000000..6090684 --- /dev/null +++ b/peach-config/conf/network/04-wired.network @@ -0,0 +1,22 @@ +[Match] +Name=e* + +[Network] +## Uncomment only one option block +# Option: using a DHCP server and multicast DNS +LLMNR=no +LinkLocalAddressing=no +MulticastDNS=yes +DHCP=ipv4 + +# Option: using link-local ip addresses and multicast DNS +#LLMNR=no +#LinkLocalAddressing=yes +#MulticastDNS=yes + +# Option: using static ip address and multicast DNS +# (example, use your settings) +#Address=192.168.50.60/24 +#Gateway=192.168.50.1 +#DNS=84.200.69.80 1.1.1.1 +#MulticastDNS=yes diff --git a/peach-config/conf/network/08-wlan0.network b/peach-config/conf/network/08-wlan0.network new file mode 100644 index 0000000..24ca2e2 --- /dev/null +++ b/peach-config/conf/network/08-wlan0.network @@ -0,0 +1,4 @@ +[Match] +Name=wlan0 +[Network] +DHCP=yes diff --git a/peach-config/conf/network/12-ap0.network b/peach-config/conf/network/12-ap0.network new file mode 100644 index 0000000..f7ab3f6 --- /dev/null +++ b/peach-config/conf/network/12-ap0.network @@ -0,0 +1,11 @@ +[Match] +Name=ap0 +[Network] +Address=11.11.11.1/24 +# IPMasquerade is doing NAT +# Uncomment the two lines below to share internet over ap0 +#IPMasquerade=yes +#IPForward=yes +DHCPServer=yes +[DHCPServer] +DNS=84.200.69.80 1.1.1.1 diff --git a/peach-config/conf/network/ap-auto-deploy.service b/peach-config/conf/network/ap-auto-deploy.service new file mode 100644 index 0000000..43a122c --- /dev/null +++ b/peach-config/conf/network/ap-auto-deploy.service @@ -0,0 +1,10 @@ +[Unit] +Description=Start the ap0 service (access point) if the wlan0 service is active but not connected to any access point + +[Service] +Type=oneshot +RemainAfterExit=no +ExecStart=/usr/local/bin/ap_auto_deploy + +[Install] +WantedBy=multi-user.target diff --git a/peach-config/conf/network/ap-auto-deploy.timer b/peach-config/conf/network/ap-auto-deploy.timer new file mode 100644 index 0000000..610fcf3 --- /dev/null +++ b/peach-config/conf/network/ap-auto-deploy.timer @@ -0,0 +1,9 @@ +[Unit] +Description=Determine when and how often the ap_auto_deploy script is run + +[Timer] +OnBootSec=60s +OnUnitActiveSec=180s + +[Install] +WantedBy=timers.target diff --git a/peach-config/conf/network/copy-wlan.service b/peach-config/conf/network/copy-wlan.service new file mode 100644 index 0000000..c31f6fb --- /dev/null +++ b/peach-config/conf/network/copy-wlan.service @@ -0,0 +1,8 @@ +[Unit] +Before=network.target + +[Service] +ExecStart=/usr/local/bin/copy-wlan.sh + +[Install] +WantedBy=default.target \ No newline at end of file diff --git a/peach-config/conf/network/copy-wlan.sh b/peach-config/conf/network/copy-wlan.sh new file mode 100644 index 0000000..227f7ca --- /dev/null +++ b/peach-config/conf/network/copy-wlan.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +FILE=/boot/firmware/wpa_supplicant.conf +if test -f "$FILE"; then + cp $FILE /etc/wpa_supplicant/wpa_supplicant-wlan0.conf + chown root:netdev /etc/wpa_supplicant/wpa_supplicant-wlan0.conf + rm $FILE +fi \ No newline at end of file diff --git a/peach-config/conf/network/wpa_supplicant-ap0.conf b/peach-config/conf/network/wpa_supplicant-ap0.conf new file mode 100644 index 0000000..ec140fa --- /dev/null +++ b/peach-config/conf/network/wpa_supplicant-ap0.conf @@ -0,0 +1,11 @@ +ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev +update_config=1 + +network={ + ssid="peach" + mode=2 + key_mgmt=WPA-PSK + proto=RSN WPA + psk="cloudless" + frequency=2412 +} diff --git a/peach-config/conf/network/wpa_supplicant-wlan0.conf b/peach-config/conf/network/wpa_supplicant-wlan0.conf new file mode 100644 index 0000000..9f5bb95 --- /dev/null +++ b/peach-config/conf/network/wpa_supplicant-wlan0.conf @@ -0,0 +1,6 @@ +ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev +update_config=1 +network={ + ssid="YourRouterSsid" + psk="password_goes_here" +} diff --git a/peach-config/conf/network/wpa_supplicant@ap0.service b/peach-config/conf/network/wpa_supplicant@ap0.service new file mode 100644 index 0000000..50baf76 --- /dev/null +++ b/peach-config/conf/network/wpa_supplicant@ap0.service @@ -0,0 +1,18 @@ +[Unit] +Description=WPA supplicant daemon (interface-specific version) +Requires=sys-subsystem-net-devices-wlan0.device +After=sys-subsystem-net-devices-wlan0.device +Conflicts=wpa_supplicant@wlan0.service +Before=network.target +Wants=network.target + +# NetworkManager users will probably want the dbus version instead. + +[Service] +Type=simple +ExecStartPre=/sbin/iw dev wlan0 interface add ap0 type __ap +ExecStart=/sbin/wpa_supplicant -c/etc/wpa_supplicant/wpa_supplicant-%I.conf -Dnl80211,wext -i%I +ExecStopPost=/sbin/iw dev ap0 del + +[Install] +Alias=multi-user.target.wants/wpa_supplicant@%i.service diff --git a/peach-config/conf/peach.conf b/peach-config/conf/peach.conf new file mode 100644 index 0000000..9000236 --- /dev/null +++ b/peach-config/conf/peach.conf @@ -0,0 +1,7 @@ +server { + listen 80; + server_name peach.local www.peach.local; + location / { + proxy_pass http://127.0.0.1:3000; + } +} diff --git a/peach-config/conf/peach.list b/peach-config/conf/peach.list new file mode 100644 index 0000000..a8b1607 --- /dev/null +++ b/peach-config/conf/peach.list @@ -0,0 +1 @@ +deb http://apt.peachcloud.org/ buster main \ No newline at end of file diff --git a/peach-config/conf/reduce-timeout.conf b/peach-config/conf/reduce-timeout.conf new file mode 100644 index 0000000..b9d8016 --- /dev/null +++ b/peach-config/conf/reduce-timeout.conf @@ -0,0 +1,2 @@ +[Service] +TimeoutStartSec=5 diff --git a/peach-config/conf/shutdown b/peach-config/conf/shutdown new file mode 100644 index 0000000..a3dfeef --- /dev/null +++ b/peach-config/conf/shutdown @@ -0,0 +1,13 @@ +# +# Allow peach microservices to initiate reboot / shutdown +# without needing to enter a password for sudo'd command. +# + +# User alias for PeachCloud microservices which initiate shutdown +User_Alias PEACH_CTRL = peach-menu, peach-web + +# Command alias for reboot and shutdown +Cmnd_Alias SHUTDOWN = /sbin/reboot, /sbin/shutdown + +# Allow PEACH_CTRL users to execute SHUTDOWN commands without password +PEACH_CTRL ALL=(ALL) NOPASSWD: SHUTDOWN diff --git a/peach-config/debian/peach-network.service b/peach-config/debian/peach-network.service new file mode 100644 index 0000000..d81099c --- /dev/null +++ b/peach-config/debian/peach-network.service @@ -0,0 +1,13 @@ +[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 diff --git a/peach-config/docs/direct_ethernet_setup.md b/peach-config/docs/direct_ethernet_setup.md new file mode 100644 index 0000000..359a7fa --- /dev/null +++ b/peach-config/docs/direct_ethernet_setup.md @@ -0,0 +1,64 @@ + +# Direct Ethernet Setup + +This file contains documentation for two different ways of working with PeachCloud using a direct ethernet connection (useful for development purposes). + +## Method 1 + +If you are close to a router, you can plug your Pi into the router via ethernet and you should be able to SSH into the Pi from any laptop connected to the same router via WiFi or ethernet. + +## Method 2 + +Using a DHCP server on your laptop (instructions for a laptop running Debian). + +**Install the DHCP server:** + +`sudo apt-get install isc-dhcp-server` + +**Configure the DHCP server:** + +In `/etc/dhcp/dhcpd.conf`, add the following section: + +```plaintext +subnet 10.0.2.0 netmask 255.255.255.240 { + range 10.0.2.2 10.0.2.14; + option routers 10.0.2.1; + host peach { + hardware ethernet b8:27:eb:b1:b1:4e; + fixed-address 10.0.2.4; + } +} +``` + +Note that `b8:27:eb:b1:b1:4e` may need to be replaced with the address of your Pi's ethernet interface, which you can look up by running `ip a` on the Pi. This address should be static. The `fixed-address` section of the config tells the DHCP server to always give the specified client (`peach`) the `10.0.2.4` IP address. + + +In `/etc/default/isc-dhcp-server`, add the following section with the name of your ethernet interface (in this case `ens9`): + +```plaintext +INTERFACESv4="ens9" +INTERFACESv6="" +``` + +In `/etc/network/interfaces`, set a static IP for your ethernet interface by adding this section: + +```plaintext +auto ens9 +iface ens9 inet static + address 10.0.2.1 +``` + +**Start the DHCP server:** + +`sudo systemctl start isc-dhcp-server` + +**Connect the ethernet cable:** + +Connect your Pi to the laptop via an ethernet cable. You should then be able to SSH into the Pi using the following command: + +`ssh peach@10.0.2.4` + +_Note:_ On the Pi, internet traffic will still need to go through `wlan0` interface. + +On Mac OS you don't need to change the network config on your laptop. Simply enable internet sharing over ethernet and you should be able to connect to the Pi. + diff --git a/peach-config/docs/installation-from-debian-disc-image.md b/peach-config/docs/installation-from-debian-disc-image.md new file mode 100644 index 0000000..7d4bb46 --- /dev/null +++ b/peach-config/docs/installation-from-debian-disc-image.md @@ -0,0 +1,147 @@ +## Installation From Debian Disc Image + +You can also setup PeachCloud by installing Debian onto an sd card, and then installing and running peach-config. +This is essentially how the PeachCloud disc image is created (see [peach-img-builder](https://github.com/peachcloud/peach-img-builder)). + +Here are the steps for installing peach-config on Debian. + +#### Step 1: Flash The SD Card + +Download the latest Debian Buster preview image for RPi3 and flash it to an SD card. + +_Note:_ Be sure to use the correct device location in the `dd` command, otherwise you risk wiping another connected USB device. `sudo dmesg | tail` can be run after plugging in the SD card to determine the correct device location: + +```bash +wget https://raspi.debian.net/verified/20200831_raspi_3.img.xz +xzcat 20200831_raspi_3.img.xz | sudo dd of=/dev/sdb bs=64k oflag=dsync status=progress +``` + +On Mac OS, use the following command to flash the SD card: + +`xzcat 20200831_raspi_3.img.xz | sudo dd of=/dev/sdcarddisc` + +Alternatively, use [Etcher](https://www.balena.io/etcher/). + +_Note:_ If the above image link stops working, you can find the complete list of Raspberry Pi Debian images [here](https://raspi.debian.net/tested-images/). + + +#### Step 2: Connect To The Internet + +Use the following commands to connect to a local WiFi network over the `wlan0` interface (assuming `eth0` connection is not possible): + +```bash +# username +root +# password (by default raspberry debian requires no password, so we set the password for root here) +passwd +# set interface up (run command twice if you receive 'link is not ready' error on first try) +ip link set wlan0 up +# append ssid and password for wifi access point +wpa_passphrase > /etc/wpa_supplicant/wpa_supplicant.conf +# open wpa_supplicant.conf +nano /etc/wpa_supplicant/wpa_supplicant.conf +``` + +[ Add the following two lines to top of file ] + +```plaintext +ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev +update_config=1 +``` + +[ Save and exit ] + +```bash +# open network interfaces config +nano /etc/network/interfaces +``` + +[ Add the following lines to the file ] + +```plaintext +auto lo +iface lo inet loopback + +auto eth0 +allow-hotplug eth0 +iface eth0 inet dhcp + +auto wlan0 +allow-hotplug wlan0 +iface wlan0 inet dhcp + wpa-conf /etc/wpa_supplicant/wpa_supplicant.conf +``` + + +[ Save and exit ] + +`reboot now` + +[ Pi should now be connected to the WiFi network ] + +#### Step 3: Install PeachCloud + + +You can run the following one-liner to install peach-cloud (TODO: make this install peachcloud instead of rust): +``` +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +``` + +Alternatively, you can run the commands in the install script manually. + + +## Connecting + +Once the setup script has been run, connect to the system remotely over the local network using ssh or mosh: + +`ssh user@peach.local` or `mosh user@peach.local` + +There is a file with detailed instructions on how to connect via a direct ethernet cable located in `docs/direct-ethernet-setup.md` + + +## Network + +Networking is handled by `wpa_supplicant` and `systemd-networkd`. + +The RPi connects to other networks with the `wlan0` interface and deploys an access point on the `ap0` interface. Only one of these modes is active at a time (client or access point). The RPi boots in client mode by default. + +To switch to access point mode: + +`sudo systemctl start wpa_supplicant@ap0.service` + +To switch to client mode: + +`sudo systemctl start wpa_supplicant@wlan0.service` + +_Note:_ No stopping of services or rebooting is required. + +To enable access point mode on boot: + +```bash +sudo systemctl disable wpa_supplicant@wlan0.service +sudo systemctl enable wpa_supplicant@ap0.service +``` + +A standalone networking configuration script is included in this repository (`scripts/setup_networking.py`). Network-related documentation can also be found in this repository (`docs`). + +This repository also contains a script for automatically starting an access point on `ap0` if the `wlan0` service is active but not connected (`scripts/ap_auto_deploy.sh`). The executable script is installed at `/usr/local/bin/ap_auto_deploy` and can either be run once-off or scheduled for repeated execution using a `systemd` service file and timer file (`conf/network/ap-auto-deploy.service` and `conf/network/ap-auto-deploy.timer`). When the timer is enabled for repeated execution, the script is automatically run 60 seconds after boot and every 180 seconds after that. + +To stop and disable the access point auto deploy service: + +```bash +sudo systemctl stop ap-auto-deploy.timer +sudo systemctl disable ap-auto-deploy.timer +``` + + +## Troubleshooting + +You may encounter DNS issues if your system time is inaccurate. Please refer to this [StackExchange answer](https://unix.stackexchange.com/a/570382/450882) for details. The steps to remedy the situation are offered here in brief: + +```bash +sudo -Es +timedatectl set-ntp 0 +# edit this line according to your current date & time +timedatectl set-time "2021-01-13 11:37:10" +timedatectl set-ntp 1 +exit diff --git a/peach-config/docs/installation-from-peach-disc-image.md b/peach-config/docs/installation-from-peach-disc-image.md new file mode 100644 index 0000000..bb53461 --- /dev/null +++ b/peach-config/docs/installation-from-peach-disc-image.md @@ -0,0 +1,38 @@ +## Installation From PeachCloud Disc Image + +#### Step 1: Flash The SD Card + +Download the latest PeachCloud image from http://releases.peachcloud.org and flash it to an SD card. + +_Note:_ Be sure to use the correct device location in the `dd` command, otherwise you risk wiping another connected USB device. `sudo dmesg | tail` can be run after plugging in the SD card to determine the correct device location: + +```bash +wget http://releases.peachcloud.org/peach-imgs/20210225/20210225_peach_raspi3.img +sudo dd 20210225_peach_raspi3.img of=/dev/sdb bs=64k oflag=dsync status=progress +``` + +On Mac OS, use the following command to flash the SD card: + +`xzcat 20200831_raspi_3.img.xz | sudo dd of=/dev/sdcarddisc` + +Alternatively, use [Etcher](https://www.balena.io/etcher/). + +_Note:_ If the above image link stops working, you can find the latest image [here](http://releases.peachcloud.org). + +Your SD card now has a complete PeachCloud installation on it and is ready to use. + + +#### Step 2: Connecting To The Internet + +## Via peach.local + +TODO: write this documentation + +## Via a Screen + +TODO: write this documentation + + +#### Step 3: Getting Started + +TODO: write this documentation \ No newline at end of file diff --git a/peach-config/docs/networkd_dual_iface_setup b/peach-config/docs/networkd_dual_iface_setup new file mode 100644 index 0000000..b7bf69a --- /dev/null +++ b/peach-config/docs/networkd_dual_iface_setup @@ -0,0 +1,116 @@ +install drivers for usb wifi adapter (RT5370 chipset): + +apt install firmware-ralink + + +----- + +quick-setup (https://raspberrypi.stackexchange.com/a/108593): + + +# create interface file for a wired connection + +sudo -Es +cat > /etc/systemd/network/04-wired.network < /etc/wpa_supplicant/wpa_supplicant-wlan0.conf < /etc/wpa_supplicant/wpa_supplicant-wlan1.conf < /etc/systemd/network/08-wlan1.network < /etc/systemd/network/12-wlan0.network < /etc/systemd/network/04-wired.network < /etc/wpa_supplicant/wpa_supplicant-wlan0.conf < /etc/wpa_supplicant/wpa_supplicant-ap0.conf < /etc/systemd/network/08-wlan0.network < /etc/systemd/network/12-ap0.network < /etc/apt/sources.list.d/peach.list +wget -O - http://apt.peachcloud.org/pubkey.gpg | sudo apt-key add - +apt-get update +apt-get install -y peach-config +RUST_LOG=info peach-config setup -i -n -d \ No newline at end of file diff --git a/peach-config/src/constants.rs b/peach-config/src/constants.rs new file mode 100644 index 0000000..8bd3235 --- /dev/null +++ b/peach-config/src/constants.rs @@ -0,0 +1,25 @@ +// Directory on peachcloud device where CONF files are store +// before they are copied to their eventual locations +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", + "peach-web", + "peach-menu", + "peach-buttons", + "peach-monitor", + "peach-probe", + "peach-dyndns-updater", + "peach-go-sbot", + "peach-config", +]; + +// File path to where current hardware configurations are stored +// note: this is stored separately from /var/lib/peachcloud/config.yml +// because it is not a configuration which should be manually edited +// the values in the hardware_config.json are a log of what peach-config configured +// whereas the values in config.yml can be manually modified if needed +pub const HARDWARE_CONFIG_FILE: &str = "/var/lib/peachcloud/hardware_config.json"; diff --git a/peach-config/src/error.rs b/peach-config/src/error.rs new file mode 100644 index 0000000..e501ccc --- /dev/null +++ b/peach-config/src/error.rs @@ -0,0 +1,48 @@ +#![allow(clippy::nonstandard_macro_braces)] +pub use snafu::ResultExt; +use snafu::Snafu; + +#[derive(Debug, Snafu)] +#[allow(clippy::enum_variant_names)] +#[snafu(visibility(pub(crate)))] +pub enum PeachConfigError { + #[snafu(display("Command not found: \"{}\"", command))] + CmdIoError { + source: std::io::Error, + command: String, + }, + #[snafu(display("\"{}\" returned an error. {}", command, msg))] + CmdError { msg: String, command: String }, + #[snafu(display("Command could not parse stdout: \"{}\"", command))] + CmdParseOutputError { + source: std::str::Utf8Error, + command: String, + }, + #[snafu(display("Failed to write file: {}", file))] + FileWriteError { + file: String, + source: std::io::Error, + }, + #[snafu(display("Failed to read file: {}", file))] + FileReadError { + file: String, + source: std::io::Error, + }, + #[snafu(display("Error serializing json: {}", source))] + SerdeError { source: serde_json::Error }, +} + +impl From for PeachConfigError { + fn from(err: std::io::Error) -> PeachConfigError { + PeachConfigError::CmdIoError { + source: err, + command: "unknown".to_string(), + } + } +} + +impl From for PeachConfigError { + fn from(err: serde_json::Error) -> PeachConfigError { + PeachConfigError::SerdeError { source: err } + } +} diff --git a/peach-config/src/generate_manifest.rs b/peach-config/src/generate_manifest.rs new file mode 100644 index 0000000..78f0d87 --- /dev/null +++ b/peach-config/src/generate_manifest.rs @@ -0,0 +1,119 @@ +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::error::{FileReadError, FileWriteError, PeachConfigError}; +use crate::utils::get_output; +use crate::RtcOption; + +/// Returns a HashMap 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, 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 = 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, + } + }) + .collect(); + + // finally the hashmap of packages and version numbers is returned + Ok(peach_packages) +} + +/// Output form of manifest +#[derive(Debug, Serialize, Deserialize)] +pub struct Manifest { + // packages is a map of {package_name: version} + packages: HashMap, + hardware: Option, +} + +/// The form that hardware configs are saved in when peach-config setup runs successfully +#[derive(Debug, Serialize, Deserialize)] +pub struct HardwareConfig { + // packages is a map of {package_name: version} + i2c: bool, + rtc: Option, +} + +/// Log which hardware settings were configured to a .json file +/// # Arguments +/// +/// * `i2c` - a boolean flag, if true i2c will be configured +/// * `rtc` - an optional enum, if supplied indicates which real-time-clock model +/// is being used +/// +/// Any error results in a PeachConfigError, otherwise the saved HardwareConfig object +/// is returned. +pub fn save_hardware_config( + i2c: bool, + rtc: Option, +) -> Result { + let hardware_config = HardwareConfig { i2c, rtc }; + + let json_str = serde_json::to_string(&hardware_config)?; + + fs::write(HARDWARE_CONFIG_FILE, json_str).context(FileWriteError { + file: HARDWARE_CONFIG_FILE.to_string(), + })?; + + Ok(hardware_config) +} + +/// Load the hardware configs that were saved from the last successful run of peach-config setup +/// +/// Returns an Ok(Some) containing the configuration if one is found, +/// and returns Ok(None) if no hardware configuration was found. +fn load_hardware_config() -> Result, PeachConfigError> { + // if there is no hardware_config, return None + let hardware_config_exists = std::path::Path::new(HARDWARE_CONFIG_FILE).exists(); + if !hardware_config_exists { + Ok(None) + } + // otherwise we load hardware_config from json + else { + let contents = fs::read_to_string(HARDWARE_CONFIG_FILE).context(FileReadError { + file: HARDWARE_CONFIG_FILE.to_string(), + })?; + let hardware_config: HardwareConfig = serde_json::from_str(&contents)?; + Ok(Some(hardware_config)) + } +} + +/// Outputs a Manifest in json form to stdout +/// which contains the currently installed peach packages +/// as well as the hardware configuration of the last run of peach-config setup. +pub fn generate_manifest() -> Result<(), PeachConfigError> { + let packages = get_currently_installed_microservices()?; + let hardware_config_option = load_hardware_config()?; + let manifest = Manifest { + packages, + hardware: hardware_config_option, + }; + let output = serde_json::to_string(&manifest)?; + println!("{}", output); + Ok(()) +} diff --git a/peach-config/src/main.rs b/peach-config/src/main.rs new file mode 100644 index 0000000..98a16af --- /dev/null +++ b/peach-config/src/main.rs @@ -0,0 +1,126 @@ +mod constants; +mod error; +mod generate_manifest; +mod setup_networking; +mod setup_peach; +mod setup_peach_deb; +mod update; +mod utils; + +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", + about = "a CLI tool for updating, installing and configuring PeachCloud" +)] +struct Opt { + #[structopt(short, long)] + verbose: bool, + + // SUBCOMMANDS + #[structopt(subcommand)] + commands: Option, +} + +#[derive(StructOpt, Debug)] +#[structopt(name = "peach-config", about = "about")] +enum PeachConfig { + /// Prints json manifest of peach configurations + #[structopt(name = "manifest")] + Manifest, + + /// Idempotent setup of PeachCloud + #[structopt(name = "setup")] + Setup(SetupOpts), + + /// Updates all PeachCloud microservices + #[structopt(name = "update")] + Update(UpdateOpts), +} + +#[derive(StructOpt, Debug)] +struct SetupOpts { + /// Setup i2c configurations + #[structopt(short, long)] + i2c: bool, + /// Optionally select which model of real-time-clock is being used, + /// {ds1307, ds3231} + #[structopt(short, long)] + rtc: Option, + /// Run peach-config in non-interactive mode + #[structopt(short, long)] + no_input: bool, + /// Use the default en_US.UTF-8 locale for compatability + #[structopt(short, long)] + default_locale: bool, +} + +#[derive(StructOpt, Debug)] +pub struct UpdateOpts { + /// Only update other microservices and not peach-config + #[structopt(short, long)] + microservices: bool, + /// Only update peach-config and not other microservices + #[structopt(short, long = "--self")] + self_only: bool, + /// List microservices which are available for updating + #[structopt(short, long)] + list: bool, +} + +arg_enum! { + /// enum options for real-time clock choices + #[derive(Debug)] + #[allow(non_camel_case_types)] + #[allow(clippy::enum_variant_names)] + #[derive(Serialize, Deserialize)] + pub enum RtcOption { + DS1307, + DS3231 + } +} + +fn main() { + // initialize the logger + env_logger::init(); + + // parse cli arguments + let opt = Opt::from_args(); + + // switch based on subcommand + if let Some(subcommand) = opt.commands { + match subcommand { + PeachConfig::Setup(cfg) => { + match 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() { + Ok(_) => {} + Err(err) => { + error!( + "peach-config countered an error generating manifest: {}", + err + ) + } + }, + PeachConfig::Update(opts) => match update(opts) { + Ok(_) => {} + Err(err) => { + error!("peach-config encountered an error during update: {}", err) + } + }, + } + } +} diff --git a/peach-config/src/setup_networking.rs b/peach-config/src/setup_networking.rs new file mode 100644 index 0000000..f82f6f6 --- /dev/null +++ b/peach-config/src/setup_networking.rs @@ -0,0 +1,147 @@ +use log::info; +use std::path::Path; + +use crate::error::PeachConfigError; +use crate::utils::{cmd, conf}; + +/// Idempotent script to configure a Debian installation to use +/// systemd-networkd for general networking. The script configures the eth0, +/// wlan0 and ap0 interfaces. This configuration allows switching between +/// wireless client mode (wlan0) and wireless access point mode (ap0) +pub fn configure_networking() -> Result<(), PeachConfigError> { + info!("[ INSTALLING SYSTEM REQUIREMENTS ]"); + cmd(&["apt", "install", "-y", "libnss-resolve"])?; + + info!("[ SETTING HOST ]"); + cmd(&["cp", &conf("hostname"), "/etc/hostname"])?; + cmd(&["cp", &conf("hosts"), "/etc/hosts"])?; + + info!("[ DEINSTALLING CLASSIC NETWORKING ]"); + cmd(&[ + "apt-get", + "autoremove", + "-y", + "ifupdown", + "dhcpcd5", + "isc-dhcp-client", + "isc-dhcp-common", + "rsyslog", + ])?; + cmd(&[ + "apt-mark", + "hold", + "ifupdown", + "dhcpcd5", + "isc-dhcp-client", + "isc-dhcp-common", + "rsyslog", + "openresolv", + ])?; + cmd(&["rm", "-rf", "/etc/network", "/etc/dhcp"])?; + + info!("[ SETTING UP SYSTEMD-RESOLVED & SYSTEMD-NETWORKD ]"); + cmd(&["apt-get", "autoremove", "-y", "avahi-daemon"])?; + cmd(&["apt-mark", "hold", "avahi-daemon", "libnss-mdns"])?; + cmd(&[ + "ln", + "-sf", + "/run/systemd/resolve/stub-resolv.conf", + "/etc/resolv.conf", + ])?; + cmd(&[ + "systemctl", + "enable", + "systemd-networkd.service", + "systemd-resolved.service", + ])?; + + info!("[ CREATING INTERFACE FILE FOR WIRED CONNECTION ]"); + cmd(&[ + "cp", + &conf("network/04-wired.network"), + "/etc/systemd/network/04-wired.network", + ])?; + + info!("[ SETTING UP WPA_SUPPLICANT AS WIFI CLIENT WITH WLAN0 ]"); + // to avoid overwriting previous credentials, only copy file if it doesn't already exist + let wlan0 = "/etc/wpa_supplicant/wpa_supplicant-wlan0.conf"; + if !Path::new(wlan0).exists() { + cmd(&["cp", &conf("network/wpa_supplicant-wlan0.conf"), wlan0])?; + cmd(&["chmod", "660", wlan0])?; + cmd(&["chown", "root:netdev", wlan0])?; + } + cmd(&["systemctl", "disable", "wpa_supplicant.service"])?; + cmd(&["systemctl", "enable", "wpa_supplicant@wlan0.service"])?; + + info!("[ CREATING BOOT SCRIPT TO COPY NETWORK CONFIGS ]"); + cmd(&[ + "cp", + &conf("network/copy-wlan.sh"), + "/usr/local/bin/copy-wlan.sh", + ])?; + cmd(&["chmod", "770", "/usr/local/bin/copy-wlan.sh"])?; + cmd(&[ + "cp", + &conf("network/copy-wlan.service"), + "/etc/systemd/system/copy-wlan.service", + ])?; + cmd(&["systemctl", "enable", "copy-wlan.service"])?; + + info!("[ SETTING UP WPA_SUPPLICANT AS ACCESS POINT WITH AP0 ]"); + cmd(&[ + "cp", + &conf("network/wpa_supplicant-ap0.conf"), + "/etc/wpa_supplicant/wpa_supplicant-ap0.conf", + ])?; + cmd(&[ + "chmod", + "600", + "/etc/wpa_supplicant/wpa_supplicant-ap0.conf", + ])?; + + info!("[ CONFIGURING INTERFACES ]"); + cmd(&[ + "cp", + &conf("network/08-wlan0.network"), + "/etc/systemd/network/08-wlan0.network", + ])?; + cmd(&[ + "cp", + &conf("network/12-ap0.network"), + "/etc/systemd/network/12-ap0.network", + ])?; + + info!("[ MODIFYING SERVICE FOR ACCESS POINT TO USE AP0 ]"); + cmd(&["systemctl", "disable", "wpa_supplicant@ap0.service"])?; + cmd(&[ + "cp", + &conf("network/wpa_supplicant@ap0.service"), + "/etc/systemd/system/wpa_supplicant@ap0.service", + ])?; + + info!("[ SETTING WLAN0 TO RUN AS CLIENT ON STARTUP ]"); + cmd(&["systemctl", "enable", "wpa_supplicant@wlan0.service"])?; + cmd(&["systemctl", "disable", "wpa_supplicant@ap0.service"])?; + + info!("[ CREATING ACCESS POINT AUTO-DEPLOY SCRIPT ]"); + cmd(&[ + "cp", + &conf("ap_auto_deploy.sh"), + "/usr/local/bin/ap_auto_deploy", + ])?; + + info!("[ CONFIGURING ACCESS POINT AUTO-DEPLOY SERVICE ]"); + cmd(&[ + "cp", + &conf("network/ap-auto-deploy.service"), + "/etc/systemd/system/ap-auto-deploy.service", + ])?; + cmd(&[ + "cp", + &conf("network/ap-auto-deploy.timer"), + "/etc/systemd/system/ap-auto-deploy.timer", + ])?; + + info!("[ NETWORKING HAS BEEN CONFIGURED ]"); + Ok(()) +} diff --git a/peach-config/src/setup_peach.rs b/peach-config/src/setup_peach.rs new file mode 100644 index 0000000..fd70b3e --- /dev/null +++ b/peach-config/src/setup_peach.rs @@ -0,0 +1,245 @@ +use log::info; +use snafu::ResultExt; +use std::fs; + +use crate::error::{FileWriteError, PeachConfigError}; +use crate::generate_manifest::save_hardware_config; +use crate::setup_networking::configure_networking; +use crate::setup_peach_deb::setup_peach_deb; +use crate::update::update_microservices; +use crate::utils::{cmd, conf, create_group_if_doesnt_exist, does_user_exist, get_output}; +use crate::RtcOption; + +/// Idempotent setup of PeachCloud device which sets up networking configuration, +/// configures the peachcloud apt repository, installs system dependencies, +/// installs microservices, and creates necessary system groups and users. +/// +/// # Arguments +/// +/// * `no_input` - a bool, if true, runs the script without requiring user interaction +/// * `default_locale` - a bool, if true, sets the default locale of the device to en_US.UTF-8 +/// * `i2c` - a bool, if true, setup i2c configurations for peach-menu +/// * `rtc` - an optional enum, which if provided indicates the model number of the real-time +/// clock being used +/// +/// If any command in the script returns an error (non-zero exit status) a PeachConfigError +/// is returned, otherwise an Ok is returned. +pub fn setup_peach( + no_input: bool, + default_locale: bool, + i2c: bool, + rtc: Option, +) -> Result<(), PeachConfigError> { + info!("[ RUNNING SETUP PEACH ]"); + + // list of system users for (micro)services + let users = [ + "peach-buttons", + "peach-menu", + "peach-monitor", + "peach-network", + "peach-oled", + "peach-stats", + "peach-web", + ]; + + // Update Pi and install requirements + info!("[ UPDATING OPERATING SYSTEM ]"); + // cmd(&["apt-get", "update", "-y"])?; + // cmd(&["apt-get", "upgrade", "-y"])?; + + info!("[ INSTALLING SYSTEM REQUIREMENTS ]"); + cmd(&[ + "apt-get", + "install", + "vim", + "man-db", + "locales", + "iw", + "git", + "python-smbus", + "i2c-tools", + "build-essential", + "curl", + "libnss-resolve", + "mosh", + "sudo", + "pkg-config", + "libssl-dev", + "nginx", + "wget", + "-y", + ])?; + + // Create system groups first + info!("[ CREATING SYSTEM GROUPS ]"); + create_group_if_doesnt_exist("peach")?; + create_group_if_doesnt_exist("gpio-user")?; + + // Add the system users + info!("[ ADDING SYSTEM USER ]"); + if no_input { + // if no input, then peach user starts with password peachcloud + let default_password = "peachcloud"; + let enc_password = get_output(&["openssl", "passwd", "-crypt", default_password])?; + info!("[ CREATING SYSTEM USER WITH DEFAULT PASSWORD ]"); + if !(does_user_exist("peach")?) { + cmd(&[ + "/usr/sbin/useradd", + "-m", + "-p", + &enc_password, + "-g", + "peach", + "-s", + "/bin/bash", + "peach", + ])?; + } + } else { + cmd(&["/usr/sbin/adduser", "peach"])?; + } + cmd(&["usermod", "-aG", "sudo", "peach"])?; + cmd(&["usermod", "-aG", "peach", "peach"])?; + + info!("[ CREATING SYSTEM USERS ]"); + // Peachcloud microservice users + for user in users { + // Create new system user without home directory and add to `peach` group + cmd(&[ + "/usr/sbin/adduser", + "--system", + "--no-create-home", + "--ingroup", + "peach", + user, + ])?; + } + + info!("[ ASSIGNING GROUP MEMBERSHIP ]"); + cmd(&[ + "/usr/sbin/usermod", + "-a", + "-G", + "gpio-user", + "peach-buttons", + ])?; + cmd(&["/usr/sbin/usermod", "-a", "-G", "netdev", "peach-network"])?; + cmd(&["/usr/sbin/usermod", "-a", "-G", "i2c", "peach-oled"])?; + + // Overwrite configuration files + info!("[ CONFIGURING OPERATING SYSTEM ]"); + info!("[ CONFIGURING GPIO ]"); + cmd(&[ + "cp", + &conf("50-gpio.rules"), + "/etc/udev/rules.d/50-gpio.rules", + ])?; + + if i2c { + info!("[ CONFIGURING I2C ]"); + cmd(&["mkdir", "-p", "/boot/firmware/overlays"])?; + cmd(&[ + "cp", + &conf("mygpio.dtbo"), + "/boot/firmware/overlays/mygpio.dtbo", + ])?; + cmd(&["cp", &conf("config.txt_i2c"), "/boot/firmware/config.txt"])?; + cmd(&["cp", &conf("modules"), "/etc/modules"])?; + } + + if let Some(rtc_model) = &rtc { + if i2c { + match rtc_model { + RtcOption::DS1307 => { + info!("[ CONFIGURING DS1307 RTC MODULE ]"); + cmd(&[ + "cp", + &conf("config.txt_ds1307"), + "/boot/firmware/config.txt", + ])?; + } + RtcOption::DS3231 => { + info!("[ CONFIGURING DS3231 RTC MODULE ]"); + cmd(&[ + "cp", + &conf("config.txt_ds3231"), + "/boot/firmware/config.txt", + ])?; + } + } + cmd(&["cp", &conf("modules_rtc"), "/etc/modules"])?; + cmd(&[ + "cp", + &conf("activate_rtc.sh"), + "/usr/local/bin/activate_rtc", + ])?; + cmd(&[ + "cp", + &conf("activate-rtc.service"), + "/etc/systemd/system/activate-rtc.service", + ])?; + cmd(&["systemctl", "daemon-reload"])?; + cmd(&["systemctl", "enable", "activate-rtc"])?; + } + } + + info!("[ CONFIGURING NGINX ]"); + cmd(&[ + "cp", + &conf("peach.conf"), + "/etc/nginx/sites-available/peach.conf", + ])?; + cmd(&[ + "ln", + "-sf", + "/etc/nginx/sites-available/peach.conf", + "/etc/nginx/sites-enabled/", + ])?; + + if !no_input { + info!("[ CONFIGURING LOCALE ]"); + cmd(&["dpkg-reconfigure", "locales"])?; + // this is specified as an argument, so a user can run this script in no-input mode without updating their locale + // if they have already set it + if default_locale { + info!("[ SETTING DEFAULT LOCALE TO en_US.UTF-8 FOR COMPATIBILITY ]"); + cmd(&[ + "sed", + "-i", + "-e", + "s/// en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/", + "/etc/locale.gen", + ])?; + fs::write("/etc/default/locale", "LANG=\"en_US.UTF-8\"").context(FileWriteError { + file: "/etc/default/locale".to_string(), + })?; + cmd(&["dpkg-reconfigure", "--frontend=noninteractive", "locales"])?; + } + } + + info!("[ CONFIGURING CONSOLE LOG-LEVEL PRINTING ]"); + // TODO: for now commenting this out, because its throwing an error + // cmd(&["sysctl", "-w", "kernel.printk=4 4 1 7"])?; + + info!("[ CONFIGURING SUDOERS ]"); + cmd(&["mkdir", "-p", "/etc/sudoers.d"])?; + cmd(&["cp", &conf("shutdown"), "/etc/sudoers.d/shutdown"])?; + + info!("[ CONFIGURING PEACH APT REPO ]"); + setup_peach_deb()?; + + info!("[ INSTALLING PEACH MICROSERVICES ]"); + update_microservices()?; + + info!("[ CONFIGURING NETWORKING ]"); + configure_networking()?; + + info!("[ SAVING LOG OF HARDWARE CONFIGURATIONS ]"); + save_hardware_config(i2c, rtc)?; + + info!("[ PEACHCLOUD SETUP COMPLETE ]"); + info!("[ ------------------------- ]"); + info!("[ please reboot your device ]"); + Ok(()) +} diff --git a/peach-config/src/setup_peach_deb.rs b/peach-config/src/setup_peach_deb.rs new file mode 100644 index 0000000..ae2f7c0 --- /dev/null +++ b/peach-config/src/setup_peach_deb.rs @@ -0,0 +1,20 @@ +use crate::error::PeachConfigError; +use crate::utils::{cmd, conf}; + +/// Adds apt.peachcloud.org to the list of debian apt sources and sets the public key appropriately +pub fn setup_peach_deb() -> Result<(), PeachConfigError> { + cmd(&[ + "cp", + &conf("peach.list"), + "/etc/apt/sources.list.d/peach.list", + ])?; + cmd(&[ + "wget", + "-O", + "/tmp/pubkey.gpg", + "http://apt.peachcloud.org/pubkey.gpg", + ])?; + cmd(&["apt-key", "add", "/tmp/pubkey.gpg"])?; + cmd(&["rm", "/tmp/pubkey.gpg"])?; + Ok(()) +} diff --git a/peach-config/src/update.rs b/peach-config/src/update.rs new file mode 100644 index 0000000..7b404d9 --- /dev/null +++ b/peach-config/src/update.rs @@ -0,0 +1,84 @@ +use crate::constants::SERVICES; +use crate::error::PeachConfigError; +use crate::utils::{cmd, get_output}; +use crate::UpdateOpts; +use serde::{Deserialize, Serialize}; + +/// Parses update subcommand CLI arguments and calls correct methods. +/// +/// If no options are passed, it runs a full update +/// - first updating peach-config +/// - and then re-running peach-config to update all the other microservices +/// +/// # Arguments +/// +/// * `opts` - an UpdateOpts object containing parsed CLI args +/// +/// Any error results in a PeachConfigError, otherwise an Ok is returned. +pub fn update(opts: UpdateOpts) -> Result<(), PeachConfigError> { + if opts.self_only { + run_update_self() + } else if opts.microservices { + update_microservices() + } else if opts.list { + list_available_updates() + } + // otherwise no options were passed, and we do a full update: + // - first updating peach-config + // - and then re-running peach-config to update all the other microservices + else { + run_update_self()?; + cmd(&["/usr/bin/peach-config", "update", "--microservices"])?; + Ok(()) + } +} + +/// Updates peach-config using apt-get +pub fn run_update_self() -> Result<(), PeachConfigError> { + cmd(&["apt-get", "update"])?; + cmd(&["apt-get", "install", "-y", "peach-config"])?; + Ok(()) +} + +/// Installs all peach microservices or updates them to the latest version +/// except for peach-config +pub fn update_microservices() -> Result<(), PeachConfigError> { + // update apt + cmd(&["apt-get", "update"])?; + // filter out peach-config from list of services + let services_to_update: Vec<&str> = SERVICES + .to_vec() + .into_iter() + .filter(|&x| x != "peach-config") + .collect(); + + // apt-get install all services + let mut update_cmd = ["apt-get", "install", "-y"].to_vec(); + update_cmd.extend(services_to_update); + cmd(&update_cmd)?; + Ok(()) +} + +/// Output form of list_available_updates +#[derive(Debug, Serialize, Deserialize)] +pub struct ListAvailableUpdatesOutput { + // packages is a list of package names + upgradeable: Vec, +} + +/// Checks if there are any PeachCloud updates available and displays them +pub fn list_available_updates() -> Result<(), PeachConfigError> { + cmd(&["apt-get", "update"])?; + let output = get_output(&["apt", "list", "--upgradable"])?; + let lines = output.split('\n'); + // filter down to just lines which are one of the services + let upgradeable: Vec = lines + .into_iter() + .filter(|x| SERVICES.iter().any(|s| x.contains(s))) + .map(|x| x.to_string()) + .collect(); + let list_available_updates_output = ListAvailableUpdatesOutput { upgradeable }; + let output = serde_json::to_string(&list_available_updates_output)?; + println!("{}", output); + Ok(()) +} diff --git a/peach-config/src/utils.rs b/peach-config/src/utils.rs new file mode 100644 index 0000000..113864d --- /dev/null +++ b/peach-config/src/utils.rs @@ -0,0 +1,91 @@ +use log::{debug, info}; +use snafu::ResultExt; +use std::process::{Command, Output}; + +use crate::constants::CONF; +use crate::error::PeachConfigError; +use crate::error::{CmdIoError, CmdParseOutputError}; + +/// Utility function which takes in a vector of &str and executes them as a bash command. +/// This function is intended to make scripted bash via rust more ergonomic. +/// +/// The first item in the vector is used as the command, +/// and the following items, if supplied, are used as arguments for the command. +/// +/// Returns a std::process::Output if successful and a PeachConfigError otherwise. +pub fn cmd(args: &[&str]) -> Result { + info!("command: {:?}", args); + let output = Command::new(args[0]) + .args(&args[1..args.len()]) + .output() + .context(CmdIoError { + command: format!("{:?}", args), + })?; + debug!("output: {:?}", output); + if output.status.success() { + Ok(output) + } else { + let err_msg = String::from_utf8(output.stderr).expect("failed to read stderr"); + Err(PeachConfigError::CmdError { + msg: err_msg, + command: format!("{:?}", args), + }) + } +} + +/// Utility function which calls calls cmd (above) but converts the Output to a String +/// before returning. +pub fn get_output(args: &[&str]) -> Result { + let output = cmd(args)?; + let std_out = std::str::from_utf8(&output.stdout).context(CmdParseOutputError { + command: format!("{:?}", args), + })?; + let mut std_out = std_out.to_string(); + if std_out.ends_with('\n') { + std_out.pop(); + } + Ok(std_out) +} + +/// Takes in a relative path from the conf dir and returns the absolute path to the file +pub fn conf(path: &str) -> String { + let full_path = format!("{}/{}", CONF, path); + full_path +} + +/// Creates a linux group with the given name if it doesn't already exist +pub fn create_group_if_doesnt_exist(group: &str) -> Result<(), PeachConfigError> { + let output = Command::new("getent") + .arg("group") + .arg(group) + .output() + .context(CmdIoError { + command: format!("getent group {}", group), + })?; + if output.status.success() { + // then group already exists + Ok(()) + } else { + // otherwise create group + cmd(&["/usr/sbin/groupadd", group])?; + Ok(()) + } +} + +/// Creates a linux user with the given username if it doesn't already exist +pub fn does_user_exist(user: &str) -> Result { + let output = Command::new("getent") + .arg("passwd") + .arg(user) + .output() + .context(CmdIoError { + command: format!("getent passwd {}", user), + })?; + if output.status.success() { + // then user already exists + Ok(true) + } else { + // otherwise user does not exist + Ok(false) + } +} diff --git a/peach-dyndns-updater/.cargo/config b/peach-dyndns-updater/.cargo/config new file mode 100644 index 0000000..4b6f460 --- /dev/null +++ b/peach-dyndns-updater/.cargo/config @@ -0,0 +1,4 @@ +[target.aarch64-unknown-linux-gnu] +linker = "aarch64-linux-gnu-gcc" +objcopy = { path ="aarch64-linux-gnu-objcopy" } +strip = { path ="aarch64-linux-gnu-strip" } diff --git a/peach-dyndns-updater/.gitignore b/peach-dyndns-updater/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/peach-dyndns-updater/.gitignore @@ -0,0 +1 @@ +/target diff --git a/peach-dyndns-updater/Cargo.toml b/peach-dyndns-updater/Cargo.toml new file mode 100644 index 0000000..994481d --- /dev/null +++ b/peach-dyndns-updater/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "peach-dyndns-updater" +version = "0.1.6" +authors = ["Max Fowler "] +edition = "2018" +description = "Sytemd timer which keeps a dynamic dns subdomain up to date with the latest device IP using nsupdate." +homepage = "https://opencollective.com/peachcloud" +repository = "https://github.com/peachcloud/peach-dyndns-updater" +readme = "README.md" +license = "AGPL-3.0-only" +publish = false + +[package.metadata.deb] +depends = "$auto" +extended-description = """\ +Cron job which keeps a dynamic dns subdomain up to date with the latest device IP using nsupdate. +""" +maintainer-scripts="debian" +systemd-units = { unit-name = "peach-dyndns-updater" } +assets = [ + ["target/release/peach-dyndns-updater", "usr/bin/", "755"], + ["README.md", "usr/share/doc/peach-dyndns-updater/README", "644"], +] + +[badges] +travis-ci = { repository = "peachcloud/peach-dyndns-updater", branch = "main" } +maintenance = { status = "actively-developed" } + +[dependencies] +peach-lib = { path = "../peach-lib" } +env_logger = "0.6" +log = "0.4" + diff --git a/peach-dyndns-updater/README.md b/peach-dyndns-updater/README.md new file mode 100644 index 0000000..81f5600 --- /dev/null +++ b/peach-dyndns-updater/README.md @@ -0,0 +1,25 @@ +# peach-dyndns-updater + +This is a debian service which uses a systemd timer and nsudpate to keep the IP address of a dynamic dns record up to date. + +It is a simple wrapper for the function peach_lib::dyndns_client::dyndns_update_ip(), +which reads the PeachCloud configurations from disc, and then if it finds +that dyndns is enabled, it uses nsupdate to update the IP address of the configured domain records. + +The nsupdate requests use the subdomain, dyndns_server_address and a path to a TSIG key (for authentication), +as provided by the PeachCloud configurations. + + +## setup + +peach-dyndns-udpater is packaged as a debian service, so it can be installed and automatically enabled via: +``` bash +apt-get install peach-dyndns-updater +``` + +After being installed, it uses a system timer to run the script every five minutes. + +You can see that it is running properly by running: +``` bash +journalctl -u peach-dyndns-udpater +``` \ No newline at end of file diff --git a/peach-dyndns-updater/debian/peach-dyndns-updater.service b/peach-dyndns-updater/debian/peach-dyndns-updater.service new file mode 100644 index 0000000..109a343 --- /dev/null +++ b/peach-dyndns-updater/debian/peach-dyndns-updater.service @@ -0,0 +1,12 @@ +[Unit] +Description=Systemd timer which keeps a dynamic dns subdomain up to date with the latest device IP using nsupdate. + +[Service] +Type=oneshot +User=peach-dyndns-updater +Group=peach +Environment="RUST_LOG=info" +ExecStart=/usr/bin/peach-dyndns-updater + +[Install] +WantedBy=multi-user.target diff --git a/peach-dyndns-updater/debian/peach-dyndns-updater.timer b/peach-dyndns-updater/debian/peach-dyndns-updater.timer new file mode 100644 index 0000000..08c5563 --- /dev/null +++ b/peach-dyndns-updater/debian/peach-dyndns-updater.timer @@ -0,0 +1,11 @@ +[Unit] +Description=Systemd timer which keeps a dynamic dns subdomain up to date with the latest device IP using nsupdate. + +[Timer] +Unit=peach-dyndns-updater.service +OnCalendar=*:0/5 +Persistent=true +AccuracySec=30s + +[Install] +WantedBy=timers.target diff --git a/peach-dyndns-updater/debian/postinst b/peach-dyndns-updater/debian/postinst new file mode 100644 index 0000000..660ee8c --- /dev/null +++ b/peach-dyndns-updater/debian/postinst @@ -0,0 +1,12 @@ +#!/bin/sh +set -e + +# create user which peach-dyndns-updater runs as +adduser --quiet --system peach-dyndns-updater +usermod -g peach peach-dyndns-updater + +# set permissions +chown peach-dyndns-updater /usr/bin/peach-dyndns-updater + +# cargo deb automatically replaces this token below, see https://github.com/mmstick/cargo-deb/blob/master/systemd.md +#DEBHELPER# \ No newline at end of file diff --git a/peach-dyndns-updater/src/main.rs b/peach-dyndns-updater/src/main.rs new file mode 100644 index 0000000..fbbc33a --- /dev/null +++ b/peach-dyndns-updater/src/main.rs @@ -0,0 +1,12 @@ +use peach_lib::dyndns_client::dyndns_update_ip; +use log::{info}; + + +fn main() { + // initalize the logger + env_logger::init(); + + info!("Running peach-dyndns-updater"); + let result = dyndns_update_ip(); + info!("result: {:?}", result); +} \ No newline at end of file diff --git a/peach-lib/.gitignore b/peach-lib/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/peach-lib/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/peach-lib/Cargo.toml b/peach-lib/Cargo.toml new file mode 100644 index 0000000..5d56455 --- /dev/null +++ b/peach-lib/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "peach-lib" +version = "1.2.15" +authors = ["Andrew Reid "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +log = "0.4" +jsonrpc-client-core = "0.5" +jsonrpc-client-http = "0.5" +jsonrpc-core = "8.0.1" +serde = { version = "1.0", features = ["derive"] } +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" diff --git a/peach-lib/README.md b/peach-lib/README.md new file mode 100644 index 0000000..7c05a99 --- /dev/null +++ b/peach-lib/README.md @@ -0,0 +1,37 @@ +# peach-lib + +![Generic badge](https://img.shields.io/badge/version-1.2.9-.svg) + +JSON-RPC client library for the PeachCloud ecosystem. + +`peach-lib` offers the ability to programmatically interact with the `peach-network`, `peach-oled` and `peach-stats` microservices. + +## Overview + +The `peach-lib` crate bundles JSON-RPC client code for making requests to the three PeachCloud microservices which expose JSON-RPC servers (`peach-network`, `peach-oled` and `peach-menu`). The full list of available RPC APIs can be found in the READMEs of the respective microservices ([peach-network](https://github.com/peachcloud/peach-network), [peach-oled](https://github.com/peachcloud/peach-oled), [peach-menu](https://github.com/peachcloud/peach-menu)), or in the [developer documentation for PeachCloud](http://docs.peachcloud.org/software/microservices/index.html). + +The library also includes a custom error type, `PeachError`, which bundles the underlying error types into three variants: `JsonRpcHttp`, `JsonRpcCore` and `Serde`. When used as the returned error type in a `Result` function response, this allows convenient use of the `?` operator (as illustrated in the example usage code below). + +## Usage + +Define the dependency in your `Cargo.toml` file: + +`peach-lib = { git = "https://github.com/peachcloud/peach-lib", branch = "main" }` + +Import the required client from the library: + +```rust +use peach_lib::network_client; +``` + +Call one of the exposed methods: + +```rust +network_client::ip("wlan0")?; +``` + +Further example usage can be found in the [`peach-menu`](https://github.com/peachcloud/peach-menu) code (see `src/states.rs`). + +## Licensing + +AGPL-3.0 diff --git a/peach-lib/debug/.cargo/config b/peach-lib/debug/.cargo/config new file mode 100644 index 0000000..4b6f460 --- /dev/null +++ b/peach-lib/debug/.cargo/config @@ -0,0 +1,4 @@ +[target.aarch64-unknown-linux-gnu] +linker = "aarch64-linux-gnu-gcc" +objcopy = { path ="aarch64-linux-gnu-objcopy" } +strip = { path ="aarch64-linux-gnu-strip" } diff --git a/peach-lib/debug/Cargo.toml b/peach-lib/debug/Cargo.toml new file mode 100644 index 0000000..2c9f53e --- /dev/null +++ b/peach-lib/debug/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "debug" +version = "0.1.0" +authors = ["notplants "] +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" \ No newline at end of file diff --git a/peach-lib/debug/src/main.rs b/peach-lib/debug/src/main.rs new file mode 100644 index 0000000..19de0b3 --- /dev/null +++ b/peach-lib/debug/src/main.rs @@ -0,0 +1,65 @@ +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); +} diff --git a/peach-lib/src/config_manager.rs b/peach-lib/src/config_manager.rs new file mode 100644 index 0000000..735866a --- /dev/null +++ b/peach-lib/src/config_manager.rs @@ -0,0 +1,143 @@ +//! Interfaces for writing and reading PeachCloud configurations, stored in yaml. +//! +//! Different PeachCloud microservices import peach-lib, so that they can share this interface. +//! +//! The configuration file is located at: "/var/lib/peachcloud/config.yml" + +use fslock::LockFile; +use serde::{Deserialize, Serialize}; +use std::fs; + +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, +} + +// helper functions for serializing and deserializing PeachConfig from disc +fn save_peach_config(peach_config: PeachConfig) -> Result { + // use a file lock to avoid race conditions while saving config + let mut lock = LockFile::open(LOCK_FILE_PATH)?; + lock.lock()?; + + let yaml_str = serde_yaml::to_string(&peach_config)?; + + fs::write(YAML_PATH, yaml_str).context(WriteConfigError { + file: YAML_PATH.to_string(), + })?; + + // unlock file lock + lock.unlock()?; + + // return peach_config + Ok(peach_config) +} + +pub fn load_peach_config() -> Result { + let peach_config_exists = std::path::Path::new(YAML_PATH).exists(); + + let peach_config: PeachConfig; + + // 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(), + }; + } + // 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) +} + +// interfaces for setting specific config values +pub fn set_peach_dyndns_config( + dyn_domain: &str, + dyn_dns_server_address: &str, + dyn_tsig_key_path: &str, + dyn_enabled: bool, +) -> Result { + 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) +} + +pub fn set_external_domain(new_external_domain: &str) -> Result { + let mut peach_config = load_peach_config()?; + peach_config.external_domain = new_external_domain.to_string(); + save_peach_config(peach_config) +} + +pub fn get_peachcloud_domain() -> Result, 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)) + } else { + Ok(None) + } +} + +pub fn set_dyndns_enabled_value(enabled_value: bool) -> Result { + let mut peach_config = load_peach_config()?; + peach_config.dyn_enabled = enabled_value; + save_peach_config(peach_config) +} + +pub fn add_ssb_admin_id(ssb_id: &str) -> Result { + let mut peach_config = load_peach_config()?; + peach_config.ssb_admin_ids.push(ssb_id.to_string()); + save_peach_config(peach_config) +} + +pub fn delete_ssb_admin_id(ssb_id: &str) -> Result { + let mut peach_config = load_peach_config()?; + let mut ssb_admin_ids = peach_config.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) + } + None => Err(PeachError::SsbAdminIdNotFound { + id: ssb_id.to_string(), + }), + } +} diff --git a/peach-lib/src/dyndns_client.rs b/peach-lib/src/dyndns_client.rs new file mode 100644 index 0000000..df58947 --- /dev/null +++ b/peach-lib/src/dyndns_client.rs @@ -0,0 +1,272 @@ +//! Client which makes jsonrpc requests via HTTP to the `peach-dyndns-server` API which runs on the peach-vps. +//! Note this is the one service in peach-lib which makes requests to an external server off of the local device. +//! +//! If the requests are successful, dyndns configurations are saved locally on the PeachCloud device, +//! which are then used by the peach-dyndns-cronjob to update the dynamic IP using nsupdate. +//! +//! There is also one function in this file, dyndns_update_ip, which doesn't interact with the jsonrpc server. +//! This function uses nsupdate to actually update dns records directly. +//! +//! 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 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; + +/// 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"; + +/// 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(), + })?; + // 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 { + path: TSIG_KEY_PATH.to_string(), + })?; + Ok(()) +} + +/// Makes a post request to register a new domain with peach-dyns-server +/// if the post is successful, the domain is registered with peach-dyndns-server +/// a unique TSIG key is returned and saved to disk, +/// and peachcloud is configured to start updating the IP of this domain using nsupdate +pub fn register_domain(domain: &str) -> std::result::Result { + 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)?; + 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), + } + } + Err(err) => Err(PeachError::JsonRpcClientCore { source: err }), + } +} + +/// Makes a post request to check if a domain is available +pub fn is_domain_available(domain: &str) -> std::result::Result { + 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)?; + 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 = 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 }), + } +} + +/// Helper function to get public ip address of PeachCloud device. +fn get_public_ip_address() -> Result { + // 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()) +} + +/// 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 { + info!("Running dyndns_update_ip"); + let peach_config = load_peach_config()?; + info!( + "Using config: + dyn_tsig_key_path: {:?} + dyn_domain: {:?} + dyn_dns_server_address: {:?} + dyn_enabled: {:?} + ", + peach_config.dyn_tsig_key_path, + peach_config.dyn_domain, + peach_config.dyn_dns_server_address, + peach_config.dyn_enabled, + ); + if !peach_config.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)?; + // pass nsupdate commands via stdin + let public_ip_address = get_public_ip_address()?; + info!("found public ip address: {}", public_ip_address); + let ns_commands = format!( + " + server {NAMESERVER} + zone {ZONE} + 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, + 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); + // We only return a successful result if nsupdate was successful + if nsupdate_output.status.success() { + info!("nsupdate succeeded, returning ok"); + // log a timestamp that the update was successful + log_successful_nsupdate()?; + // return true + 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 }) + } + } +} + +// Helper function to log a timestamp of the latest successful nsupdate +pub fn log_successful_nsupdate() -> Result { + let now_timestamp = chrono::offset::Utc::now().to_rfc3339(); + let mut file = OpenOptions::new() + .write(true) + .create(true) + .open(DYNDNS_LOG_PATH) + .context(SaveDynDnsResultError)?; + write!(file, "{}", now_timestamp).context(SaveDynDnsResultError)?; + Ok(true) +} + +/// Helper function to return how many seconds since peach-dyndns-updater successfully ran +pub fn get_num_seconds_since_successful_dns_update() -> Result, PeachError> { + let log_exists = std::path::Path::new(DYNDNS_LOG_PATH).exists(); + if !log_exists { + Ok(None) + } else { + let contents = + fs::read_to_string(DYNDNS_LOG_PATH).expect("Something went wrong reading the file"); + // 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(), + })?; + let current_time: DateTime = Utc::now(); + let duration = current_time.signed_duration_since(time_ran_dt); + let duration_in_seconds = duration.num_seconds(); + Ok(Some(duration_in_seconds)) + } +} + +/// helper function which returns a true result if peach-dyndns-updater is enabled +/// and has successfully run recently (in the last six minutes) +pub fn is_dns_updater_online() -> Result { + // first check if it is enabled in peach-config + let peach_config = load_peach_config()?; + let is_enabled = peach_config.dyn_enabled; + // 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); + } + // if the value is None, then the last time it ran successfully is unknown + None => { + ran_recently = false; + } + } + // debug log + info!("is_dyndns_enabled: {:?}", is_enabled); + info!("dyndns_ran_recently: {:?}", ran_recently); + // if both are true, then return true + Ok(is_enabled && ran_recently) +} + +/// helper function which builds a full dynamic dns domain from a subdomain +pub fn get_full_dynamic_domain(subdomain: &str) -> String { + format!("{}.dyn.peachcloud.org", subdomain) +} + +/// helper function to get a dyndns subdomain from a dyndns full domain +pub fn get_dyndns_subdomain(dyndns_full_domain: &str) -> Option { + let re = Regex::new(r"(.*)\.dyn\.peachcloud\.org").ok()?; + let caps = re.captures(dyndns_full_domain)?; + let subdomain = caps.get(1).map_or("", |m| m.as_str()); + Some(subdomain.to_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 +} + +jsonrpc_client!(pub struct PeachDynDnsClient { + pub fn register_domain(&mut self, domain: &str) -> RpcRequest; + pub fn is_domain_available(&mut self, domain: &str) -> RpcRequest; +}); diff --git a/peach-lib/src/error.rs b/peach-lib/src/error.rs new file mode 100644 index 0000000..0068263 --- /dev/null +++ b/peach-lib/src/error.rs @@ -0,0 +1,134 @@ +//! 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; + +#[derive(Debug, Snafu)] +#[snafu(visibility(pub(crate)))] +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, + }, + #[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, + 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("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 }, +} + +impl From for PeachError { + fn from(err: jsonrpc_client_http::Error) -> PeachError { + PeachError::JsonRpcHttp { source: err } + } +} + +impl From for PeachError { + fn from(err: jsonrpc_client_core::Error) -> PeachError { + PeachError::JsonRpcClientCore { source: err } + } +} + +impl From for PeachError { + fn from(err: serde_json::error::Error) -> PeachError { + PeachError::Serde { source: err } + } +} + +impl From for PeachError { + fn from(err: serde_yaml::Error) -> PeachError { + PeachError::YamlError { source: err } + } +} + +impl From for PeachError { + fn from(err: std::io::Error) -> PeachError { + PeachError::StdIoError { + source: err, + msg: "".to_string(), + } + } +} + +impl From for PeachError { + fn from(err: regex::Error) -> PeachError { + PeachError::RegexError { source: err } + } +} + +impl From for PeachError { + fn from(err: std::string::FromUtf8Error) -> PeachError { + PeachError::FromUtf8Error { source: err } + } +} + +impl From for PeachError { + fn from(err: std::str::Utf8Error) -> PeachError { + PeachError::Utf8Error { source: err } + } +} + +impl From for PeachError { + fn from(err: chrono::ParseError) -> PeachError { + PeachError::ChronoParseError { + source: err, + msg: "".to_string(), + } + } +} diff --git a/peach-lib/src/lib.rs b/peach-lib/src/lib.rs new file mode 100644 index 0000000..2c68442 --- /dev/null +++ b/peach-lib/src/lib.rs @@ -0,0 +1,18 @@ +// 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 stats_client; + +// re-export error types +pub use jsonrpc_client_core; +pub use jsonrpc_core; +pub use serde_json; +pub use serde_yaml; diff --git a/peach-lib/src/network_client.rs b/peach-lib/src/network_client.rs new file mode 100644 index 0000000..cf22f2f --- /dev/null +++ b/peach-lib/src/network_client.rs @@ -0,0 +1,597 @@ +//! Perform JSON-RPC calls to the `peach-network` microservice. +//! +//! This module contains a JSON-RPC client and associated data structures for +//! making calls to the `peach-network` microservice. Each RPC has a +//! corresponding method which creates an HTTP transport, makes the call to the +//! RPC microservice and returns the response to the caller. These convenience +//! methods simplify the process of performing RPC calls from other modules. +//! +//! 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}; +use jsonrpc_client_http::HttpTransport; +use log::{debug, info}; +use serde::{Deserialize, Serialize}; + +use crate::error::PeachError; +use crate::stats_client::Traffic; + +#[derive(Debug, Deserialize, Serialize)] +pub struct AccessPoint { + pub detail: Option, + pub signal: Option, + pub state: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Networks { + pub ssid: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Scan { + pub protocol: String, + pub frequency: String, + pub signal_level: String, + pub ssid: String, +} + +/// Creates a JSON-RPC client with http transport and calls the `peach-network` +/// `activate_ap` method. +pub fn activate_ap() -> std::result::Result { + debug!("Creating HTTP transport for network client."); + let transport = HttpTransport::new().standalone()?; + let http_addr = + env::var("PEACH_NETWORK_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string()); + let http_server = format!("http://{}", http_addr); + 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 = PeachNetworkClient::new(transport_handle); + + let response = client.activate_ap().call()?; + + Ok(response) +} + +/// Creates a JSON-RPC client with http transport and calls the `peach-network` +/// `activate_client` method. +pub fn activate_client() -> std::result::Result { + debug!("Creating HTTP transport for network client."); + let transport = HttpTransport::new().standalone()?; + let http_addr = + env::var("PEACH_NETWORK_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string()); + let http_server = format!("http://{}", http_addr); + 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 = PeachNetworkClient::new(transport_handle); + + let response = client.activate_client().call()?; + + Ok(response) +} + +/// Creates a JSON-RPC client with http transport and calls the `peach-network` +/// `add_wifi` method. +/// +/// # Arguments +/// +/// * `ssid` - A string slice containing the SSID of an access point. +/// * `pass` - A string slice containing the password for an access point. +pub fn add(ssid: &str, pass: &str) -> std::result::Result { + debug!("Creating HTTP transport for network client."); + let transport = HttpTransport::new().standalone()?; + let http_addr = + env::var("PEACH_NETWORK_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string()); + let http_server = format!("http://{}", http_addr); + 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 = PeachNetworkClient::new(transport_handle); + + let response = client.add(ssid, pass).call()?; + + Ok(response) +} + +/// Creates a JSON-RPC client with http transport and calls the `peach-network` +/// `available_networks` method, which returns a list of in-range access points. +/// +/// # Arguments +/// +/// * `iface` - A string slice containing the network interface identifier. +pub fn available_networks(iface: &str) -> std::result::Result { + debug!("Creating HTTP transport for network client."); + let transport = HttpTransport::new().standalone()?; + let http_addr = + env::var("PEACH_NETWORK_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string()); + let http_server = format!("http://{}", http_addr); + 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 = PeachNetworkClient::new(transport_handle); + + let response = client.available_networks(iface).call()?; + + Ok(response) +} + +/// Creates a JSON-RPC client with http transport and calls the `peach-network` +/// `connect` method, which disables other network connections and enables the +/// connection for the chosen network, identified by ID and interface. +/// +/// # Arguments +/// +/// * `id` - A string slice containing a network identifier. +/// * `iface` - A string slice containing the network interface identifier. +pub fn connect(id: &str, iface: &str) -> std::result::Result { + debug!("Creating HTTP transport for network client."); + let transport = HttpTransport::new().standalone()?; + let http_addr = + env::var("PEACH_NETWORK_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string()); + let http_server = format!("http://{}", http_addr); + 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 = PeachNetworkClient::new(transport_handle); + + let response = client.connect(id, iface).call()?; + + Ok(response) +} + +/// Creates a JSON-RPC client with http transport and calls the `peach-network` +/// `id` and `disable` methods. +/// +/// # Arguments +/// +/// * `iface` - A string slice containing the network interface identifier. +/// * `ssid` - A string slice containing the SSID of a network. +pub fn disable(iface: &str, ssid: &str) -> std::result::Result { + debug!("Creating HTTP transport for network client."); + let transport = HttpTransport::new().standalone()?; + let http_addr = + env::var("PEACH_NETWORK_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string()); + let http_server = format!("http://{}", http_addr); + 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 = PeachNetworkClient::new(transport_handle); + + info!("Performing id call to peach-network microservice."); + let id = client.id(&iface, &ssid).call()?; + info!("Performing disable call to peach-network microservice."); + client.disable(&id, &iface).call()?; + + let response = "success".to_string(); + + Ok(response) +} + +/// Creates a JSON-RPC client with http transport and calls the `peach-network` +/// `id`, `delete` and `save` methods. +/// +/// # Arguments +/// +/// * `iface` - A string slice containing the network interface identifier. +/// * `ssid` - A string slice containing the SSID of a network. +pub fn forget(iface: &str, ssid: &str) -> std::result::Result { + debug!("Creating HTTP transport for network client."); + let transport = HttpTransport::new().standalone()?; + let http_addr = + env::var("PEACH_NETWORK_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string()); + let http_server = format!("http://{}", http_addr); + 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 = PeachNetworkClient::new(transport_handle); + + info!("Performing id call to peach-network microservice."); + 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()?; + info!("Performing save call to peach-network microservice."); + client.save().call()?; + + let response = "success".to_string(); + + Ok(response) +} + +/// Creates a JSON-RPC client with http transport and calls the `peach-network` +/// `id` method. +/// +/// # Arguments +/// +/// * `iface` - A string slice containing the network interface identifier. +/// * `ssid` - A string slice containing the SSID of a network. +pub fn id(iface: &str, ssid: &str) -> std::result::Result { + debug!("Creating HTTP transport for network client."); + let transport = HttpTransport::new().standalone()?; + let http_addr = + env::var("PEACH_NETWORK_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string()); + let http_server = format!("http://{}", http_addr); + 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 = PeachNetworkClient::new(transport_handle); + + let response = client.id(iface, ssid).call()?; + + Ok(response) +} + +/// Creates a JSON-RPC client with http transport and calls the `peach-network` +/// `ip` method. +/// +/// # Arguments +/// +/// * `iface` - A string slice containing the network interface identifier. +pub fn ip(iface: &str) -> std::result::Result { + debug!("Creating HTTP transport for network client."); + let transport = HttpTransport::new().standalone()?; + let http_addr = + env::var("PEACH_NETWORK_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string()); + let http_server = format!("http://{}", http_addr); + 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 = PeachNetworkClient::new(transport_handle); + + let response = client.ip(iface).call()?; + + Ok(response) +} + +/// Creates a JSON-RPC client with http transport and calls the `peach-network` +/// `ping` method, which serves as a means of determining availability of the +/// microservice (ie. there will be no response if `peach-network` is not +/// running). +pub fn ping() -> std::result::Result { + debug!("Creating HTTP transport for network client."); + let transport = HttpTransport::new().standalone()?; + let http_addr = + env::var("PEACH_NETWORK_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string()); + let http_server = format!("http://{}", http_addr); + 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 = PeachNetworkClient::new(transport_handle); + let response = client.ping().call()?; + + Ok(response) +} + +/// Creates a JSON-RPC client with http transport and calls the `peach-network` +/// `reconfigure` method. +pub fn reconfigure() -> std::result::Result { + debug!("Creating HTTP transport for network client."); + let transport = HttpTransport::new().standalone()?; + let http_addr = + env::var("PEACH_NETWORK_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string()); + let http_server = format!("http://{}", http_addr); + 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 = PeachNetworkClient::new(transport_handle); + + let response = client.reconfigure().call()?; + + Ok(response) +} + +/// Creates a JSON-RPC client with http transport and calls the `peach-network` +/// `rssi` method. +/// +/// # Arguments +/// +/// * `iface` - A string slice containing the network interface identifier. +pub fn rssi(iface: &str) -> std::result::Result { + debug!("Creating HTTP transport for network client."); + let transport = HttpTransport::new().standalone()?; + let http_addr = + env::var("PEACH_NETWORK_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string()); + let http_server = format!("http://{}", http_addr); + 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 = PeachNetworkClient::new(transport_handle); + + let response = client.rssi(iface).call()?; + + Ok(response) +} + +/// Creates a JSON-RPC client with http transport and calls the `peach-network` +/// `rssi_percent` method. +/// +/// # Arguments +/// +/// * `iface` - A string slice containing the network interface identifier. +pub fn rssi_percent(iface: &str) -> std::result::Result { + debug!("Creating HTTP transport for network client."); + let transport = HttpTransport::new().standalone()?; + let http_addr = + env::var("PEACH_NETWORK_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string()); + let http_server = format!("http://{}", http_addr); + 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 = PeachNetworkClient::new(transport_handle); + + let response = client.rssi_percent(iface).call()?; + + Ok(response) +} + +/// Helper function to determine if a given SSID already exists in the +/// `wpa_supplicant.conf` file, indicating that network credentials have already +/// been added for that access point. Creates a JSON-RPC client with http +/// transport and calls the `peach-network` `saved_networks` method. Returns a +/// boolean expression inside a Result type. +/// +/// # Arguments +/// +/// * `ssid` - A string slice containing the SSID of a network. +pub fn saved_ap(ssid: &str) -> std::result::Result { + debug!("Creating HTTP transport for network client."); + let transport = HttpTransport::new().standalone()?; + let http_addr = + env::var("PEACH_NETWORK_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string()); + let http_server = format!("http://{}", http_addr); + 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 = PeachNetworkClient::new(transport_handle); + + // retrieve a list of access points with saved credentials + let saved_aps = match client.saved_networks().call() { + Ok(ssids) => { + let networks: Vec = serde_json::from_str(ssids.as_str()) + .expect("Failed to deserialize saved_networks response"); + networks + } + // return an empty vector if there are no saved access point credentials + Err(_) => Vec::new(), + }; + + // loop through the access points in the list + for network in saved_aps { + // return true if the access point ssid matches the given ssid + if network.ssid == ssid { + return Ok(true); + } + } + + // return false if no matches are found + Ok(false) +} + +/// Creates a JSON-RPC client with http transport and calls the `peach-network` +/// `saved_networks` method, which returns a list of networks saved in +/// `wpa_supplicant.conf`. +pub fn saved_networks() -> std::result::Result { + debug!("Creating HTTP transport for network client."); + let transport = HttpTransport::new().standalone()?; + let http_addr = + env::var("PEACH_NETWORK_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string()); + let http_server = format!("http://{}", http_addr); + 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 = PeachNetworkClient::new(transport_handle); + let response = client.saved_networks().call()?; + + Ok(response) +} + +/// Creates a JSON-RPC client with http transport and calls the `peach-network` +/// `ssid` method. +/// +/// # Arguments +/// +/// * `iface` - A string slice containing the network interface identifier. +pub fn ssid(iface: &str) -> std::result::Result { + debug!("Creating HTTP transport for network client."); + let transport = HttpTransport::new().standalone()?; + let http_addr = + env::var("PEACH_NETWORK_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string()); + let http_server = format!("http://{}", http_addr); + 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 = PeachNetworkClient::new(transport_handle); + + let response = client.ssid(iface).call()?; + + Ok(response) +} + +/// Creates a JSON-RPC client with http transport and calls the `peach-network` +/// `state` method. +/// +/// # Arguments +/// +/// * `iface` - A string slice containing the network interface identifier. +pub fn state(iface: &str) -> std::result::Result { + debug!("Creating HTTP transport for network client."); + let transport = HttpTransport::new().standalone()?; + let http_addr = + env::var("PEACH_NETWORK_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string()); + let http_server = format!("http://{}", http_addr); + 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 = PeachNetworkClient::new(transport_handle); + + let response = client.state(iface).call()?; + + Ok(response) +} + +/// Creates a JSON-RPC client with http transport and calls the `peach-network` +/// `status` method. +/// +/// # Arguments +/// +/// * `iface` - A string slice containing the network interface identifier. +pub fn status(iface: &str) -> std::result::Result { + debug!("Creating HTTP transport for network client."); + let transport = HttpTransport::new().standalone()?; + let http_addr = + env::var("PEACH_NETWORK_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string()); + let http_server = format!("http://{}", http_addr); + 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 = PeachNetworkClient::new(transport_handle); + + let response = client.status(iface).call()?; + + Ok(response) +} + +/// Creates a JSON-RPC client with http transport and calls the `peach-network` +/// `traffic` method. +/// +/// # Arguments +/// +/// * `iface` - A string slice containing the network interface identifier. +pub fn traffic(iface: &str) -> std::result::Result { + debug!("Creating HTTP transport for network client."); + let transport = HttpTransport::new().standalone()?; + let http_addr = + env::var("PEACH_NETWORK_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string()); + let http_server = format!("http://{}", http_addr); + 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 = PeachNetworkClient::new(transport_handle); + + let response = client.traffic(iface).call()?; + let t: Traffic = serde_json::from_str(&response).unwrap(); + + Ok(t) +} + +/// Creates a JSON-RPC client with http transport and calls the `peach-network` +/// `id`, `delete`, `save` and `add` methods. These combined calls allow the +/// saved password for an access point to be updated. +/// +/// # Arguments +/// +/// * `iface` - A string slice containing the network interface identifier. +/// * `ssid` - A string slice containing the SSID of a network. +/// * `pass` - A string slice containing the password for a network. +pub fn update(iface: &str, ssid: &str, pass: &str) -> std::result::Result { + debug!("Creating HTTP transport for network client."); + let transport = HttpTransport::new().standalone()?; + let http_addr = + env::var("PEACH_NETWORK_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string()); + let http_server = format!("http://{}", http_addr); + 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 = PeachNetworkClient::new(transport_handle); + + // get the id of the network + info!("Performing id call to peach-network microservice."); + 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()?; + // save the updates to wpa_supplicant.conf + info!("Performing save call to peach-network microservice."); + client.save().call()?; + // add the new credentials + info!("Performing add call to peach-network microservice."); + client.add(ssid, pass).call()?; + // reconfigure wpa_supplicant with latest addition to config + info!("Performing reconfigure call to peach-network microservice."); + client.reconfigure().call()?; + + let response = "success".to_string(); + + Ok(response) +} + +jsonrpc_client!(pub struct PeachNetworkClient { + /// JSON-RPC request to activate the access point. + pub fn activate_ap(&mut self) -> RpcRequest; + + /// JSON-RPC request to activate the wireless client (wlan0). + pub fn activate_client(&mut self) -> RpcRequest; + + /// JSON-RPC request to add credentials for an access point. + pub fn add(&mut self, ssid: &str, pass: &str) -> RpcRequest; + + /// JSON-RPC request to list all networks in range of the given interface. + pub fn available_networks(&mut self, iface: &str) -> RpcRequest; + + /// JSON-RPC request to connect the network for the given interface and ID. + pub fn connect(&mut self, id: &str, iface: &str) -> RpcRequest; + + /// JSON-RPC request to delete the credentials for the given network from the wpa_supplicant config. + pub fn delete(&mut self, id: &str, iface: &str) -> RpcRequest; + + /// JSON-RPC request to disable the network for the given interface and ID. + pub fn disable(&mut self, id: &str, iface: &str) -> RpcRequest; + + /// JSON-RPC request to disconnect the network for the given interface. + //pub fn disconnect(&mut self, iface: &str) -> RpcRequest; + + /// JSON-RPC request to get the ID for the given interface and SSID. + pub fn id(&mut self, iface: &str, ssid: &str) -> RpcRequest; + + /// JSON-RPC request to get the IP address for the given interface. + pub fn ip(&mut self, iface: &str) -> RpcRequest; + + /// JSON-RPC request to set a new network password for the given interface and ID. + //pub fn modify(&mut self, id: &str, iface: &str, pass: &str) -> RpcRequest; + + /// JSON-RPC request to check peach-network availability. + pub fn ping(&mut self) -> RpcRequest; + + /// JSON-RPC request to reread the wpa_supplicant config for the given interface. + pub fn reconfigure(&mut self) -> RpcRequest; + + /// JSON-RPC request to reconnect WiFi for the given interface. + //pub fn reconnect(&mut self, iface: &str) -> RpcRequest; + + /// JSON-RPC request to get the average signal strength (dBm) for the given interface. + pub fn rssi(&mut self, iface: &str) -> RpcRequest; + + /// JSON-RPC request to get the average signal quality (%) for the given interface. + pub fn rssi_percent(&mut self, iface: &str) -> RpcRequest; + + /// JSON-RPC request to save network configuration updates to file. + pub fn save(&mut self) -> RpcRequest; + + /// JSON-RPC request to list all networks saved in `wpa_supplicant.conf`. + pub fn saved_networks(&mut self) -> RpcRequest; + + /// JSON-RPC request to get the SSID of the currently-connected network for the given interface. + pub fn ssid(&mut self, iface: &str) -> RpcRequest; + + /// JSON-RPC request to get the state for the given interface. + pub fn state(&mut self, iface: &str) -> RpcRequest; + + /// JSON-RPC request to get the status of the given interface. + pub fn status(&mut self, iface: &str) -> RpcRequest; + + /// JSON-RPC request to get the network traffic for the given interface. + pub fn traffic(&mut self, iface: &str) -> RpcRequest; +}); diff --git a/peach-lib/src/oled_client.rs b/peach-lib/src/oled_client.rs new file mode 100644 index 0000000..9809843 --- /dev/null +++ b/peach-lib/src/oled_client.rs @@ -0,0 +1,165 @@ +use std::env; + +use jsonrpc_client_core::{expand_params, jsonrpc_client}; +use jsonrpc_client_http::HttpTransport; +use log::{debug, info}; + +use crate::error::PeachError; + +/// Creates a JSON-RPC client with http transport and calls the `peach-oled` +/// `clear` method. +pub fn clear() -> std::result::Result<(), PeachError> { + debug!("Creating HTTP transport for OLED client."); + let transport = HttpTransport::new().standalone()?; + let http_addr = env::var("PEACH_OLED_SERVER").unwrap_or_else(|_| "127.0.0.1:5112".to_string()); + let http_server = format!("http://{}", http_addr); + debug!("Creating HTTP transport handle on {}.", http_server); + let transport_handle = transport.handle(&http_server)?; + info!("Creating client for peach_oled service."); + let mut client = PeachOledClient::new(transport_handle); + + client.clear().call()?; + debug!("Cleared the OLED display."); + + Ok(()) +} + +/// Creates a JSON-RPC client with http transport and calls the `peach-oled` +/// `draw` method. +/// +/// # Arguments +/// +/// * `bytes` - A Vec of 8 byte unsigned int. +/// * `width` - A 32 byte unsigned int. +/// * `height` - A 32 byte unsigned int. +/// * `x_coord` - A 32 byte signed int. +/// * `y_coord` - A 32 byte signed int. +pub fn draw( + bytes: Vec, + width: u32, + height: u32, + x_coord: i32, + y_coord: i32, +) -> std::result::Result { + debug!("Creating HTTP transport for OLED client."); + let transport = HttpTransport::new().standalone()?; + let http_addr = env::var("PEACH_OLED_SERVER").unwrap_or_else(|_| "127.0.0.1:5112".to_string()); + let http_server = format!("http://{}", http_addr); + debug!("Creating HTTP transport handle on {}.", http_server); + let transport_handle = transport.handle(&http_server)?; + info!("Creating client for peach_oled service."); + let mut client = PeachOledClient::new(transport_handle); + + client.draw(bytes, width, height, x_coord, y_coord).call()?; + debug!("Drew to the OLED display."); + + Ok("success".to_string()) +} + +/// Creates a JSON-RPC client with http transport and calls the `peach-oled` +/// `flush` method. +pub fn flush() -> std::result::Result<(), PeachError> { + debug!("Creating HTTP transport for OLED client."); + let transport = HttpTransport::new().standalone()?; + let http_addr = env::var("PEACH_OLED_SERVER").unwrap_or_else(|_| "127.0.0.1:5112".to_string()); + let http_server = format!("http://{}", http_addr); + debug!("Creating HTTP transport handle on {}.", http_server); + let transport_handle = transport.handle(&http_server)?; + info!("Creating client for peach_oled service."); + let mut client = PeachOledClient::new(transport_handle); + + client.flush().call()?; + debug!("Flushed the OLED display."); + + Ok(()) +} + +/// Creates a JSON-RPC client with http transport and calls the `peach-oled` +/// `ping` method. +pub fn ping() -> std::result::Result<(), PeachError> { + debug!("Creating HTTP transport for OLED client."); + let transport = HttpTransport::new().standalone()?; + let http_addr = env::var("PEACH_OLED_SERVER").unwrap_or_else(|_| "127.0.0.1:5112".to_string()); + let http_server = format!("http://{}", http_addr); + debug!("Creating HTTP transport handle on {}.", http_server); + let transport_handle = transport.handle(&http_server)?; + info!("Creating client for peach_oled service."); + let mut client = PeachOledClient::new(transport_handle); + + client.ping().call()?; + debug!("Pinged the OLED microservice."); + + Ok(()) +} + +/// Creates a JSON-RPC client with http transport and calls the `peach-oled` +/// `power` method. +/// +/// # Arguments +/// +/// * `power` - A boolean expression +pub fn power(on: bool) -> std::result::Result<(), PeachError> { + debug!("Creating HTTP transport for OLED client."); + let transport = HttpTransport::new().standalone()?; + let http_addr = env::var("PEACH_OLED_SERVER").unwrap_or_else(|_| "127.0.0.1:5112".to_string()); + let http_server = format!("http://{}", http_addr); + debug!("Creating HTTP transport handle on {}.", http_server); + let transport_handle = transport.handle(&http_server)?; + info!("Creating client for peach_oled service."); + let mut client = PeachOledClient::new(transport_handle); + + client.power(on).call()?; + debug!("Toggled the OLED display power."); + + Ok(()) +} + +/// Creates a JSON-RPC client with http transport and calls the `peach-oled` +/// `draw` method. +/// +/// # Arguments +/// +/// * `x_coord` - A 32 byte signed int. +/// * `y_coord` - A 32 byte signed int. +/// * `string` - A reference to a string slice +/// * `font_size` - A reference to a string slice +pub fn write( + x_coord: i32, + y_coord: i32, + string: &str, + font_size: &str, +) -> std::result::Result { + debug!("Creating HTTP transport for OLED client."); + let transport = HttpTransport::new().standalone()?; + let http_addr = env::var("PEACH_OLED_SERVER").unwrap_or_else(|_| "127.0.0.1:5112".to_string()); + let http_server = format!("http://{}", http_addr); + debug!("Creating HTTP transport handle on {}.", http_server); + let transport_handle = transport.handle(&http_server)?; + info!("Creating client for peach_oled service."); + let mut client = PeachOledClient::new(transport_handle); + + client.write(x_coord, y_coord, string, font_size).call()?; + debug!("Wrote to the OLED display."); + + Ok("success".to_string()) +} + +jsonrpc_client!(pub struct PeachOledClient { + /// Creates a JSON-RPC request to clear the OLED display. + pub fn clear(&mut self) -> RpcRequest; + + /// Creates a JSON-RPC request to draw to the OLED display. + pub fn draw(&mut self, bytes: Vec, width: u32, height: u32, x_coord: i32, y_coord: i32) -> RpcRequest; + + /// Creates a JSON-RPC request to flush the OLED display. + pub fn flush(&mut self) -> RpcRequest; + + /// Creates a JSON-RPC request to ping the OLED microservice. + pub fn ping(&mut self) -> RpcRequest; + +/// Creates a JSON-RPC request to toggle the power of the OLED display. + pub fn power(&mut self, on: bool) -> RpcRequest; + + /// Creates a JSON-RPC request to write to the OLED display. + pub fn write(&mut self, x_coord: i32, y_coord: i32, string: &str, font_size: &str) -> RpcRequest; +}); diff --git a/peach-lib/src/password_utils.rs b/peach-lib/src/password_utils.rs new file mode 100644 index 0000000..c35edbd --- /dev/null +++ b/peach-lib/src/password_utils.rs @@ -0,0 +1,150 @@ +use crate::config_manager::{get_peachcloud_domain, load_peach_config}; +use crate::error::PeachError; +use crate::error::StdIoError; +use crate::sbot_client; +use rand::distributions::Alphanumeric; +use rand::{thread_rng, Rng}; +use snafu::ResultExt; +use std::iter; +use std::process::Command; + +/// filepath where nginx basic auth passwords are stored +pub const HTPASSWD_FILE: &str = "/var/lib/peachcloud/passwords/htpasswd"; +/// filepath where random temporary password is stored for password resets +pub const HTPASSWD_TEMPORARY_PASSWORD_FILE: &str = + "/var/lib/peachcloud/passwords/temporary_password"; +/// the username of the user for nginx basic auth +pub const PEACHCLOUD_AUTH_USER: &str = "admin"; + +/// 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 output = Command::new("/usr/bin/htpasswd") + .arg("-vb") + .arg(HTPASSWD_FILE) + .arg(PEACHCLOUD_AUTH_USER) + .arg(password) + .output() + .context(StdIoError { + msg: "htpasswd is not installed", + })?; + if output.status.success() { + Ok(()) + } else { + Err(PeachError::InvalidPassword) + } +} + +/// Checks if the given passwords are valid, and returns Ok() if they are and +/// a PeachError otherwise. +/// Currently this just checks that the passwords are the same, +/// but could be extended to test if they are strong enough. +pub fn validate_new_passwords(new_password1: &str, new_password2: &str) -> Result<(), PeachError> { + if new_password1 == new_password2 { + Ok(()) + } else { + Err(PeachError::PasswordsDoNotMatch) + } +} + +/// Uses htpasswd to set a new password for the admin user +pub fn set_new_password(new_password: &str) -> Result<(), PeachError> { + let output = Command::new("/usr/bin/htpasswd") + .arg("-cb") + .arg(HTPASSWD_FILE) + .arg(PEACHCLOUD_AUTH_USER) + .arg(new_password) + .output() + .context(StdIoError { + msg: "htpasswd is not installed", + })?; + if output.status.success() { + Ok(()) + } else { + let err_output = String::from_utf8(output.stderr)?; + Err(PeachError::FailedToSetNewPassword { msg: err_output }) + } +} + +/// Uses htpasswd to set 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 output = Command::new("/usr/bin/htpasswd") + .arg("-cb") + .arg(HTPASSWD_TEMPORARY_PASSWORD_FILE) + .arg(PEACHCLOUD_AUTH_USER) + .arg(new_password) + .output() + .context(StdIoError { + msg: "htpasswd is not installed", + })?; + if output.status.success() { + Ok(()) + } else { + let err_output = String::from_utf8(output.stderr)?; + Err(PeachError::FailedToSetNewPassword { msg: err_output }) + } +} + +/// 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> { + // TODO: confirm temporary password has not expired + let output = Command::new("/usr/bin/htpasswd") + .arg("-vb") + .arg(HTPASSWD_TEMPORARY_PASSWORD_FILE) + .arg(PEACHCLOUD_AUTH_USER) + .arg(password) + .output() + .context(StdIoError { + msg: "htpasswd is not installed", + })?; + if output.status.success() { + Ok(()) + } else { + Err(PeachError::InvalidPassword) + } +} + +/// 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(); + // save this string as a new temporary password + set_new_temporary_password(&temporary_password)?; + let domain = 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", + temporary_password + ); + // if there is an external domain, then include remote link in message + // otherwise dont include it + let remote_link = match domain { + 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", + domain + ) + } + None => "".to_string(), + }; + 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)?; + } + Ok(()) +} diff --git a/peach-lib/src/sbot_client.rs b/peach-lib/src/sbot_client.rs new file mode 100644 index 0000000..86dbc3f --- /dev/null +++ b/peach-lib/src/sbot_client.rs @@ -0,0 +1,109 @@ +//! 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 { + 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 { + 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 { + 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), + }) + } +} diff --git a/peach-lib/src/stats_client.rs b/peach-lib/src/stats_client.rs new file mode 100644 index 0000000..3dfe20c --- /dev/null +++ b/peach-lib/src/stats_client.rs @@ -0,0 +1,198 @@ +//! Perform JSON-RPC calls to the `peach-stats` microservice. +//! +//! This module contains a JSON-RPC client and associated data structures for +//! making calls to the `peach-stats` microservice. Each RPC has a corresponding +//! method which creates an HTTP transport, makes the call to the RPC +//! microservice and returns the response to the caller. These convenience +//! methods simplify the process of performing RPC calls from other modules. + +use std::env; + +use jsonrpc_client_core::{expand_params, jsonrpc_client}; +use jsonrpc_client_http::HttpTransport; +use log::{debug, info}; +use serde::{Deserialize, Serialize}; + +use crate::error::PeachError; + +#[derive(Debug, Deserialize, Serialize)] +pub struct CpuStat { + pub user: u64, + pub system: u64, + pub idle: u64, + pub nice: u64, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct CpuStatPercentages { + pub user: f32, + pub system: f32, + pub idle: f32, + pub nice: f32, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct DiskUsage { + pub filesystem: Option, + 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, Deserialize, Serialize)] +pub struct LoadAverage { + pub one: f32, + pub five: f32, + pub fifteen: f32, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct MemStat { + pub total: u64, + pub free: u64, + pub used: u64, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Traffic { + pub received: u64, + pub transmitted: u64, + pub rx_unit: Option, + pub tx_unit: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Uptime { + pub secs: u64, + pub nanos: u32, +} + +/// Creates a JSON-RPC client with http transport and calls the `peach-stats` +/// `cpu_stats_percent` method. +pub fn cpu_stats_percent() -> std::result::Result { + debug!("Creating HTTP transport for stats client."); + let transport = HttpTransport::new().standalone()?; + let http_addr = env::var("PEACH_STATS_SERVER").unwrap_or_else(|_| "127.0.0.1:5113".to_string()); + let http_server = format!("http://{}", http_addr); + debug!("Creating HTTP transport handle on {}.", http_server); + let transport_handle = transport.handle(&http_server)?; + info!("Creating client for peach_stats service."); + let mut client = PeachStatsClient::new(transport_handle); + + let response = client.cpu_stats_percent().call()?; + let c: CpuStatPercentages = serde_json::from_str(&response)?; + + Ok(c) +} + +/// Creates a JSON-RPC client with http transport and calls the `peach-stats` +/// `disk_usage` method. +pub fn disk_usage() -> std::result::Result { + debug!("Creating HTTP transport for stats client."); + let transport = HttpTransport::new().standalone()?; + let http_addr = env::var("PEACH_STATS_SERVER").unwrap_or_else(|_| "127.0.0.1:5113".to_string()); + let http_server = format!("http://{}", http_addr); + debug!("Creating HTTP transport handle on {}.", http_server); + let transport_handle = transport.handle(&http_server)?; + info!("Creating client for peach_stats service."); + let mut client = PeachStatsClient::new(transport_handle); + + let response = client.disk_usage().call()?; + + Ok(response) +} + +/// Creates a JSON-RPC client with http transport and calls the `peach-stats` +/// `cpu_stats_percent` method. +pub fn load_average() -> std::result::Result { + debug!("Creating HTTP transport for stats client."); + let transport = HttpTransport::new().standalone()?; + let http_addr = env::var("PEACH_STATS_SERVER").unwrap_or_else(|_| "127.0.0.1:5113".to_string()); + let http_server = format!("http://{}", http_addr); + debug!("Creating HTTP transport handle on {}.", http_server); + let transport_handle = transport.handle(&http_server)?; + info!("Creating client for peach_stats service."); + let mut client = PeachStatsClient::new(transport_handle); + + let response = client.load_average().call()?; + let l: LoadAverage = serde_json::from_str(&response)?; + + Ok(l) +} + +/// Creates a JSON-RPC client with http transport and calls the `peach-stats` +/// `cpu_stats_percent` method. +pub fn mem_stats() -> std::result::Result { + debug!("Creating HTTP transport for stats client."); + let transport = HttpTransport::new().standalone()?; + let http_addr = env::var("PEACH_STATS_SERVER").unwrap_or_else(|_| "127.0.0.1:5113".to_string()); + let http_server = format!("http://{}", http_addr); + debug!("Creating HTTP transport handle on {}.", http_server); + let transport_handle = transport.handle(&http_server)?; + info!("Creating client for peach_stats service."); + let mut client = PeachStatsClient::new(transport_handle); + + let response = client.mem_stats().call()?; + let m: MemStat = serde_json::from_str(&response)?; + + Ok(m) +} + +/// Creates a JSON-RPC client with http transport and calls the `peach-stats` +/// `ping` method. +pub fn ping() -> std::result::Result { + debug!("Creating HTTP transport for stats client."); + let transport = HttpTransport::new().standalone()?; + let http_addr = env::var("PEACH_STATS_SERVER").unwrap_or_else(|_| "127.0.0.1:5113".to_string()); + let http_server = format!("http://{}", http_addr); + debug!("Creating HTTP transport handle on {}.", http_server); + let transport_handle = transport.handle(&http_server)?; + info!("Creating client for peach_stats service."); + let mut client = PeachStatsClient::new(transport_handle); + + let response = client.ping().call()?; + + Ok(response) +} + +/// Creates a JSON-RPC client with http transport and calls the `peach-stats` +/// `uptime` method. If a successful response is returned, the uptime value (in +/// seconds) is converted to minutes before being returned to the caller. +pub fn uptime() -> std::result::Result { + debug!("Creating HTTP transport for stats client."); + let transport = HttpTransport::new().standalone()?; + let http_addr = env::var("PEACH_STATS_SERVER").unwrap_or_else(|_| "127.0.0.1:5113".to_string()); + let http_server = format!("http://{}", http_addr); + debug!("Creating HTTP transport handle on {}.", http_server); + let transport_handle = transport.handle(&http_server)?; + info!("Creating client for peach_stats service."); + let mut client = PeachStatsClient::new(transport_handle); + + let response = client.uptime().call()?; + let u: Uptime = serde_json::from_str(&response)?; + let minutes = (u.secs / 60).to_string(); + + Ok(minutes) +} + +jsonrpc_client!(pub struct PeachStatsClient { + /// JSON-RPC request to get measurement of current CPU statistics. + pub fn cpu_stats_percent(&mut self) -> RpcRequest; + + /// JSON-RPC request to get measurement of current disk usage statistics. + pub fn disk_usage(&mut self) -> RpcRequest; + + /// JSON-RPC request to get measurement of current load average statistics. + pub fn load_average(&mut self) -> RpcRequest; + + /// JSON-RPC request to get measurement of current memory statistics. + pub fn mem_stats(&mut self) -> RpcRequest; + + /// JSON-RPC request to check availability of the `peach-stats` microservice. + pub fn ping(&mut self) -> RpcRequest; + + /// JSON-RPC request to get system uptime. + pub fn uptime(&mut self) -> RpcRequest; +}); diff --git a/peach-menu/.cargo/config b/peach-menu/.cargo/config new file mode 100644 index 0000000..4b6f460 --- /dev/null +++ b/peach-menu/.cargo/config @@ -0,0 +1,4 @@ +[target.aarch64-unknown-linux-gnu] +linker = "aarch64-linux-gnu-gcc" +objcopy = { path ="aarch64-linux-gnu-objcopy" } +strip = { path ="aarch64-linux-gnu-strip" } diff --git a/peach-menu/.gitignore b/peach-menu/.gitignore new file mode 100644 index 0000000..53eaa21 --- /dev/null +++ b/peach-menu/.gitignore @@ -0,0 +1,2 @@ +/target +**/*.rs.bk diff --git a/peach-menu/.travis.yml b/peach-menu/.travis.yml new file mode 100644 index 0000000..f2536c6 --- /dev/null +++ b/peach-menu/.travis.yml @@ -0,0 +1,7 @@ +language: rust +rust: + - nightly +before_script: + - rustup component add clippy +script: + - cargo clippy -- -D warnings diff --git a/peach-menu/Cargo.toml b/peach-menu/Cargo.toml new file mode 100644 index 0000000..f877a1d --- /dev/null +++ b/peach-menu/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "peach-menu" +version = "0.2.7" +authors = ["Andrew Reid "] +edition = "2018" +description = "Menu for monitoring and interacting with the PeachCloud device. A state machine which listens for GPIO events (button presses) by subscribing to peach-buttons over websockets and makes JSON-RPC calls to relevant PeachCloud microservices." +homepage = "https://opencollective.com/peachcloud" +repository = "https://github.com/peachcloud/peach-menu" +readme = "README.md" +license = "AGPL-3.0-only" +publish = false + +[package.metadata.deb] +depends = "$auto" +extended-description = """\ +Menu for monitoring and interacting with the PeachCloud device. \ +A state machine which listens for GPIO events (button presses) by \ +subscribing to peach-buttons over websockets and makes JSON-RPC calls \ +to relevant PeachCloud microservices.""" +maintainer-scripts="debian" +systemd-units = { unit-name = "peach-menu" } +assets = [ + ["target/release/peach-menu", "usr/bin/", "755"], + ["README.md", "usr/share/doc/peach-menu/README", "644"], +] + +[badges] +travis-ci = { repository = "peachcloud/peach-menu", branch = "master" } +maintenance = { status = "actively-developed" } + +[dependencies] +chrono = "0.4" +crossbeam-channel = "0.3" +env_logger = "0.6" +jsonrpc-client-core = "0.5.0" +jsonrpc-client-http = "0.5.0" +jsonrpc-http-server = "11" +jsonrpc-test = "11" +log = "0.4" +peach-lib = { git = "https://github.com/peachcloud/peach-lib", branch = "main" } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +ws = "0.8" diff --git a/peach-menu/README.md b/peach-menu/README.md new file mode 100644 index 0000000..8645514 --- /dev/null +++ b/peach-menu/README.md @@ -0,0 +1,108 @@ +# peach-menu + +[![Build Status](https://travis-ci.com/peachcloud/peach-menu.svg?branch=master)](https://travis-ci.com/peachcloud/peach-menu) ![Generic badge](https://img.shields.io/badge/version-0.2.7-.svg) + +OLED menu microservice module for PeachCloud. A state machine which listens for GPIO events (button presses) by subscribing to `peach-buttons` over websockets and makes [JSON-RPC](https://www.jsonrpc.org/specification) calls to relevant PeachCloud microservices (`peach-network`, `peach-oled`, `peach-stats`). + +_Note: This module is a work-in-progress._ + +### Button Code Mappings + +``` +0 => Center, +1 => Left, +2 => Right, +3 => Up, +4 => Down, +5 => A, +6 => B +``` + +### States + +``` +Home(u8), +Logo, +Network, +NetworkConf(u8), +NetworkMode(u8), +OledPower(u8), +Reboot, +Shutdown, +Stats, +``` + +### Environment + +The JSON-RPC HTTP server address and port for the OLED microservice can be configured with the `PEACH_OLED_SERVER` environment variable: + +`export PEACH_OLED_SERVER=127.0.0.1:5000` + +When not set, the value defaults to `127.0.0.1:5112`. + +Logging is made available with `env_logger`: + +`export RUST_LOG=info` + +Other logging levels include `debug`, `warn` and `error`. + +### Setup + +Clone this repo: + +`git clone https://github.com/peachcloud/peach-menu.git` + +Move into the repo and compile: + +`cd peach-menu` +`cargo build --release` + +Run the binary: + +`./target/target/peach-menu` + +_Note: Will currently panic if `peach_buttons` is not running (connection to ws server fails)._ + +### Debian Packaging + +A `systemd` service file and Debian maintainer scripts are included in the `debian` directory, allowing `peach-menu` 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-menu` + +Build the package: + +`cargo deb` + +The output will be written to `target/debian/peach-menu_0.2.1_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-menu_0.2.1_arm64.deb` + +The service will be automatically enabled and started. + +Uninstall the service: + +`sudo apt-get remove peach-menu` + +Remove configuration files (not removed with `apt-get remove`): + +`sudo apt-get purge peach-menu` + +### Resources + +This work was made much, much easier by the awesome blog post titled [Pretty State Machine Patterns in Rust](https://hoverbear.org/2016/10/12/rust-state-machine-pattern/) by [hoverbear](https://hoverbear.org/about/). Thanks hoverbear! + +### Licensing + +AGPL-3.0 diff --git a/peach-menu/debian/peach-menu.service b/peach-menu/debian/peach-menu.service new file mode 100644 index 0000000..b830aa0 --- /dev/null +++ b/peach-menu/debian/peach-menu.service @@ -0,0 +1,15 @@ +[Unit] +Description=Menu for monitoring and interacting with the PeachCloud device. + +[Service] +Type=simple +User=peach-menu +Environment="RUST_LOG=error" +ExecStart=/usr/bin/peach-menu +Restart=always +Wants=peach-network.service peach-stats.service +Requires=peach-buttons.service peach-oled.service +After=peach-buttons.service peach-oled.service peach-network.service peach-stats.service + +[Install] +WantedBy=multi-user.target diff --git a/peach-menu/src/buttons.rs b/peach-menu/src/buttons.rs new file mode 100644 index 0000000..2a9971d --- /dev/null +++ b/peach-menu/src/buttons.rs @@ -0,0 +1,82 @@ +use std::process; + +use log::{debug, error, info, warn}; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use ws::{CloseCode, Error, Handler, Handshake, Message, Sender}; + +#[derive(Debug, Deserialize)] +pub struct Press { + pub button_code: u8, +} + +#[derive(Serialize, Deserialize)] +struct ButtonMsg { + jsonrpc: String, + method: String, + params: Vec, +} + +/// Websocket client for `peach_buttons`. +#[derive(Debug)] +pub struct Client<'a> { + pub out: Sender, + pub s: &'a crossbeam_channel::Sender, +} + +impl<'a> Handler for Client<'a> { + /// Sends request to `peach_buttons` to subscribe to emitted events. + fn on_open(&mut self, _: Handshake) -> ws::Result<()> { + info!("Subscribing to peach_buttons microservice over ws."); + let subscribe = json!({ + "id":1, + "jsonrpc":"2.0", + "method":"subscribe_buttons" + }); + let data = subscribe.to_string(); + self.out.send(data) + } + + /// Displays JSON-RPC request from `peach_buttons`. + fn on_message(&mut self, msg: Message) -> ws::Result<()> { + info!("Received ws message from peach_buttons."); + // button_code must be extracted from the request and passed to + // state_changer + let m: String = msg.into_text()?; + // distinguish button_press events from other received jsonrpc requests + if m.contains(r"params") { + // serialize msg string into a struct + let bm: ButtonMsg = serde_json::from_str(&m).unwrap_or_else(|err| { + error!("Problem serializing button_code msg: {}", err); + process::exit(1); + }); + debug!("Sending button code to state_changer."); + // send the button_code parameter to state_changer + self.s.send(bm.params[0]).unwrap_or_else(|err| { + error!("Problem sending button_code over channel: {}", err); + process::exit(1); + }); + } + Ok(()) + } + + /// Handles disconnection from websocket and displays debug data. + fn on_close(&mut self, code: CloseCode, reason: &str) { + match code { + CloseCode::Normal => { + info!("The client is done with the connection."); + } + CloseCode::Away => { + info!("The client is leaving the site."); + } + CloseCode::Abnormal => { + warn!("Closing handshake failed! Unable to obtain closing status from client."); + } + _ => error!("The client encountered an error: {}", reason), + } + } + + fn on_error(&mut self, err: Error) { + error!("The server encountered an error: {:?}", err); + } +} diff --git a/peach-menu/src/lib.rs b/peach-menu/src/lib.rs new file mode 100644 index 0000000..7caa092 --- /dev/null +++ b/peach-menu/src/lib.rs @@ -0,0 +1,47 @@ +//! # peach-menu +//! +//! `peach_menu` is a collection of utilities and data structures for running +//! a menu state machine. I/O takes place using JSON-RPC 2.0 over websockets, +//! with `peach-buttons` providing GPIO input data and `peach-oled` receiving +//! output data for display. +//! +pub mod buttons; +pub mod state_machine; +mod states; +mod structs; + +use std::env; + +use crossbeam_channel::unbounded; +use log::{debug, info}; +use ws::connect; + +use crate::buttons::*; +use crate::state_machine::*; + +/// Configures channels for message passing, launches the state machine +/// changer thread and connects to the `peach-buttons` JSON-RPC pubsub +/// service over websockets. +/// +/// A Receiver is passed into `state_changer` and the corresponding Sender +/// is passed into the websockets client. This allows the `button_code` to +/// be extracted from the received websocket message and passed to the +/// state machine. +/// +pub fn run() -> std::result::Result<(), Box> { + info!("Starting up."); + + debug!("Creating unbounded channel for message passing."); + let (s, r) = unbounded(); + + debug!("Spawning state-machine thread."); + state_changer(r); + + let ws_addr = env::var("PEACH_BUTTONS_SERVER").unwrap_or_else(|_| "127.0.0.1:5111".to_string()); + + let ws_server = format!("ws://{}", ws_addr); + + connect(ws_server, |out| Client { out, s: &s })?; + + Ok(()) +} diff --git a/peach-menu/src/main.rs b/peach-menu/src/main.rs new file mode 100644 index 0000000..c3b3ca0 --- /dev/null +++ b/peach-menu/src/main.rs @@ -0,0 +1,14 @@ +use std::process; + +use log::error; + +fn main() { + // initialize the logger + env_logger::init(); + + // handle errors returned from `run` + if let Err(e) = peach_menu::run() { + error!("Application error: {:?}", e); + process::exit(1); + } +} diff --git a/peach-menu/src/state_machine.rs b/peach-menu/src/state_machine.rs new file mode 100644 index 0000000..21fbdfa --- /dev/null +++ b/peach-menu/src/state_machine.rs @@ -0,0 +1,245 @@ +use std::{process, thread}; + +use crossbeam_channel::*; +use log::{error, info, warn}; + +use peach_lib::error::PeachError; +use peach_lib::oled_client; + +use crate::states::*; + +#[derive(Debug, Clone, Copy)] +/// The button press events. +pub enum Event { + Center, + Left, + Right, + Down, + Up, + A, + B, + Unknown, +} + +#[derive(Debug, PartialEq)] +/// The states of the state machine. +pub enum State { + Home(u8), + Logo, + Network, + NetworkConf(u8), + NetworkMode(u8), + OledPower(u8), + Reboot, + Shutdown, + Stats, +} + +/// Initializes the state machine, listens for button events and drives +/// corresponding state changes. +/// +/// # Arguments +/// +/// * `r` - An unbounded `crossbeam_channel::Receiver` for unsigned 8 byte int. +/// +pub fn state_changer(r: Receiver) { + thread::spawn(move || { + info!("Initializing the state machine."); + let mut state = State::Logo; + match state.run() { + Ok(_) => (), + Err(e) => warn!("State machine error: {:?}", e), + }; + + loop { + let button_code = r.recv().unwrap_or_else(|err| { + error!("Problem receiving button code from server: {}", err); + process::exit(1); + }); + let event = match button_code { + 0 => Event::Center, + 1 => Event::Left, + 2 => Event::Right, + 3 => Event::Up, + 4 => Event::Down, + 5 => Event::A, + 6 => Event::B, + _ => Event::Unknown, + }; + state = state.next(event); + match state.run() { + Ok(_) => (), + Err(e) => warn!("State machine error: {:?}", e), + }; + } + }); +} + +// 0 - Home +// 1 - Networking +// 2 - System Stats +// 3 - Display Off +// 4 - Reboot +// 5 - Shutdown +// 0 - NetworkConf +// 1 - Client Mode +// 2 - Access Point +// NetworkMode +// 0 - Client Mode +// 1 - Access Point Mode +// OledPower +// 0 - Off +// 1 - On + +impl State { + /// Determines the next state based on current state and event. + pub fn next(self, event: Event) -> State { + match (self, event) { + (State::Logo, Event::A) => State::Home(0), + (State::Home(_), Event::B) => State::Logo, + (State::Home(0), Event::Down) => State::Home(2), + (State::Home(0), Event::Up) => State::Home(5), + (State::Home(0), Event::A) => State::Network, + (State::Home(1), Event::Down) => State::Home(2), + (State::Home(1), Event::Up) => State::Home(5), + (State::Home(1), Event::A) => State::Network, + (State::Home(2), Event::Down) => State::Home(3), + (State::Home(2), Event::Up) => State::Home(1), + (State::Home(2), Event::A) => State::Stats, + (State::Home(3), Event::Down) => State::Home(4), + (State::Home(3), Event::Up) => State::Home(2), + (State::Home(3), Event::A) => State::OledPower(0), + (State::Home(4), Event::Down) => State::Home(5), + (State::Home(4), Event::Up) => State::Home(3), + (State::Home(4), Event::A) => State::Reboot, + (State::Home(5), Event::Down) => State::Home(1), + (State::Home(5), Event::Up) => State::Home(4), + (State::Home(5), Event::A) => State::Shutdown, + (State::Network, Event::A) => State::NetworkConf(0), + (State::Network, Event::B) => State::Home(0), + (State::NetworkConf(0), Event::A) => State::NetworkMode(0), + (State::NetworkConf(0), Event::B) => State::Network, + (State::NetworkConf(0), Event::Down) => State::NetworkConf(2), + (State::NetworkConf(0), Event::Up) => State::NetworkConf(2), + (State::NetworkConf(1), Event::A) => State::NetworkMode(0), + (State::NetworkConf(1), Event::B) => State::Network, + (State::NetworkConf(1), Event::Down) => State::NetworkConf(2), + (State::NetworkConf(1), Event::Up) => State::NetworkConf(2), + (State::NetworkConf(2), Event::A) => State::NetworkMode(1), + (State::NetworkConf(2), Event::B) => State::Network, + (State::NetworkConf(2), Event::Down) => State::NetworkConf(1), + (State::NetworkConf(2), Event::Up) => State::NetworkConf(1), + (State::NetworkMode(1), Event::B) => State::Network, + (State::NetworkMode(1), Event::Down) => State::NetworkConf(1), + (State::NetworkMode(1), Event::Up) => State::NetworkConf(1), + (State::NetworkMode(0), Event::B) => State::Network, + (State::NetworkMode(0), Event::Down) => State::NetworkConf(2), + (State::NetworkMode(0), Event::Up) => State::NetworkConf(2), + (State::OledPower(0), _) => State::OledPower(1), + (State::OledPower(1), Event::Down) => State::Home(4), + (State::OledPower(1), Event::Up) => State::Home(2), + (State::OledPower(1), Event::A) => State::OledPower(0), + (State::Stats, Event::B) => State::Home(0), + // return current state if combination is unmatched + (s, _) => s, + } + } + + /// Executes state-specific logic for current state. + pub fn run(&self) -> Result<(), PeachError> { + match *self { + // home: root + State::Home(0) => { + info!("State changed to: Home 0."); + state_home(0)?; + } + // home: networking + State::Home(1) => { + info!("State changed to: Home 1."); + state_home(1)?; + } + // home: system stats + State::Home(2) => { + info!("State changed to: Home 2."); + state_home(2)?; + } + // home: display off + State::Home(3) => { + info!("State changed to: Home 3."); + state_home(3)?; + } + // home: reboot + State::Home(4) => { + info!("State changed to: Home 4."); + state_home(4)?; + } + // home: shutdown + State::Home(5) => { + info!("State changed to: Home 5."); + state_home(5)?; + } + // home: unknown + State::Home(_) => { + info!("State changed to: Home _."); + } + State::Logo => { + info!("State changed to: Logo."); + state_logo()?; + } + State::Network => { + info!("State changed to: Network."); + state_network()?; + } + State::NetworkConf(0) => { + info!("State changed to: NetworkConf 0."); + state_network_conf(0)?; + } + State::NetworkConf(1) => { + info!("State changed to: NetworkConf 1."); + state_network_conf(1)?; + } + State::NetworkConf(2) => { + info!("State changed to: NetworkConf 2."); + state_network_conf(2)?; + } + State::NetworkConf(_) => { + info!("State changed to: NetworkConf _."); + } + State::NetworkMode(0) => { + info!("State changed to: NetworkMode 0."); + state_network_mode(0)?; + } + State::NetworkMode(1) => { + info!("State changed to: NetworkMode 1."); + state_network_mode(1)?; + } + State::NetworkMode(_) => { + info!("State changed to: NetworkMode _."); + } + State::OledPower(0) => { + info!("State changed to: OledPower 0."); + oled_client::power(false)?; + } + State::OledPower(1) => { + info!("State changed to: OledPower 1."); + oled_client::power(true)?; + } + State::OledPower(_) => { + info!("State changed to: OledPower _."); + } + State::Reboot => { + info!("State changed to: Reboot."); + state_reboot()?; + } + State::Shutdown => { + info!("State changed to: Shutdown."); + state_shutdown()?; + } + State::Stats => { + info!("State changed to: Stats."); + state_stats()?; + } + } + Ok(()) + } +} diff --git a/peach-menu/src/states.rs b/peach-menu/src/states.rs new file mode 100644 index 0000000..90eb567 --- /dev/null +++ b/peach-menu/src/states.rs @@ -0,0 +1,337 @@ +use std::{process, thread, time}; + +use chrono::{DateTime, Local}; +use log::info; + +use peach_lib::error::PeachError; +use peach_lib::network_client; +use peach_lib::oled_client; +use peach_lib::stats_client; + +pub fn state_network_mode(mode: u8) -> Result<(), PeachError> { + match mode { + 0 => { + oled_client::clear()?; + oled_client::write(24, 16, "ACTIVATING", "6x8")?; + oled_client::write(24, 27, "WIRELESS", "6x8")?; + oled_client::write(24, 38, "CONNECTION...", "6x8")?; + oled_client::flush()?; + + network_client::activate_client()?; + + oled_client::clear()?; + oled_client::write(0, 0, "> Client mode", "6x8")?; + oled_client::write(0, 9, " Access point mode", "6x8")?; + oled_client::flush()?; + + Ok(()) + } + 1 => { + oled_client::clear()?; + oled_client::write(27, 16, "DEPLOYING", "6x8")?; + oled_client::write(27, 27, "ACCESS", "6x8")?; + oled_client::write(27, 38, "POINT...", "6x8")?; + oled_client::flush()?; + + network_client::activate_ap()?; + + oled_client::clear()?; + oled_client::write(0, 0, " Client mode", "6x8")?; + oled_client::write(0, 9, "> Access point mode", "6x8")?; + oled_client::flush()?; + + Ok(()) + } + _ => Ok(()), + } +} + +pub fn state_home(selected: u8) -> Result<(), PeachError> { + // match on `selected` + match selected { + // Home: root + 0 => { + let dt: DateTime = Local::now(); + let t = format!("{}", dt.time().format("%H:%M")); + + oled_client::clear()?; + oled_client::write(96, 0, &t, "6x8")?; + oled_client::write(0, 0, "PeachCloud", "6x8")?; + oled_client::write(0, 18, "> Networking", "6x8")?; + oled_client::write(0, 27, " System Stats", "6x8")?; + oled_client::write(0, 36, " Display Off", "6x8")?; + oled_client::write(0, 45, " Reboot", "6x8")?; + oled_client::write(0, 54, " Shutdown", "6x8")?; + oled_client::write(100, 54, "v0.2", "6x8")?; + oled_client::flush()?; + + Ok(()) + } + // Home: networking + 1 => { + oled_client::write(0, 18, "> ", "6x8")?; + oled_client::write(0, 27, " ", "6x8")?; + oled_client::write(0, 36, " ", "6x8")?; + oled_client::write(0, 45, " ", "6x8")?; + oled_client::write(0, 54, " ", "6x8")?; + oled_client::flush()?; + + Ok(()) + } + // Home: system stats + 2 => { + oled_client::write(0, 18, " ", "6x8")?; + oled_client::write(0, 27, "> ", "6x8")?; + oled_client::write(0, 36, " ", "6x8")?; + oled_client::write(0, 45, " ", "6x8")?; + oled_client::write(0, 54, " ", "6x8")?; + oled_client::flush()?; + + Ok(()) + } + // Home: display off + 3 => { + oled_client::write(0, 18, " ", "6x8")?; + oled_client::write(0, 27, " ", "6x8")?; + oled_client::write(0, 36, "> ", "6x8")?; + oled_client::write(0, 45, " ", "6x8")?; + oled_client::write(0, 54, " ", "6x8")?; + oled_client::flush()?; + + Ok(()) + } + // Home: reboot + 4 => { + oled_client::write(0, 18, " ", "6x8")?; + oled_client::write(0, 27, " ", "6x8")?; + oled_client::write(0, 36, " ", "6x8")?; + oled_client::write(0, 45, "> ", "6x8")?; + oled_client::write(0, 54, " ", "6x8")?; + oled_client::flush()?; + + Ok(()) + } + // Home: shutdown + 5 => { + oled_client::write(0, 18, " ", "6x8")?; + oled_client::write(0, 27, " ", "6x8")?; + oled_client::write(0, 36, " ", "6x8")?; + oled_client::write(0, 45, " ", "6x8")?; + oled_client::write(0, 54, "> ", "6x8")?; + oled_client::flush()?; + + Ok(()) + } + // outlier + _ => Ok(()), + } +} + +pub fn state_logo() -> Result<(), PeachError> { + let bytes = PEACH_LOGO.to_vec(); + oled_client::clear()?; + oled_client::draw(bytes, 64, 64, 32, 0)?; + oled_client::flush()?; + + Ok(()) +} + +pub fn state_network() -> Result<(), PeachError> { + let status = match network_client::state("wlan0") { + Ok(state) => state, + Err(_) => "Error".to_string(), + }; + match status.as_ref() { + // wlan0 is up or dormant + // Network: Client mode + "up" | "dormant" => { + let show_status = format!("STATUS {}", status); + let ip = match network_client::ip("wlan0") { + Ok(ip) => ip, + Err(_) => "x.x.x.x".to_string(), + }; + let show_ip = format!("IP {}", ip); + let ssid = match network_client::ssid("wlan0") { + Ok(ssid) => ssid, + Err(_) => "Not connected".to_string(), + }; + let show_ssid = format!("NETWORK {}", ssid); + let rssi = match network_client::rssi("wlan0") { + Ok(rssi) => rssi, + Err(_) => "_".to_string(), + }; + let show_rssi = format!("SIGNAL {}dBm", rssi); + let config = "> Configuration"; + + oled_client::clear()?; + oled_client::write(0, 0, "MODE Client", "6x8")?; + oled_client::write(0, 9, &show_status, "6x8")?; + oled_client::write(0, 18, &show_ssid, "6x8")?; + oled_client::write(0, 27, &show_ip, "6x8")?; + oled_client::write(0, 36, &show_rssi, "6x8")?; + oled_client::write(0, 54, config, "6x8")?; + oled_client::flush()?; + + Ok(()) + } + // wlan0 is down + // Network: AP mode + "down" => { + let status = match network_client::state("ap0") { + Ok(state) => state, + Err(_) => "Error".to_string(), + }; + let show_status = format!("STATUS {}", status); + let ip = match network_client::ip("ap0") { + Ok(ip) => ip, + Err(_) => "x.x.x.x".to_string(), + }; + let show_ip = format!("IP {}", ip); + let ssid = "peach"; + let show_ssid = format!("NETWORK {}", ssid); + let config = "> Configuration"; + + oled_client::clear()?; + oled_client::write(0, 0, "MODE Access Point", "6x8")?; + oled_client::write(0, 9, &show_status, "6x8")?; + oled_client::write(0, 18, &show_ssid, "6x8")?; + oled_client::write(0, 27, &show_ip, "6x8")?; + oled_client::write(0, 54, config, "6x8")?; + oled_client::flush()?; + + Ok(()) + } + // outlier + // TODO: account for iface states other than 'up' and 'down' + _ => Ok(()), + } +} + +pub fn state_network_conf(selected: u8) -> Result<(), PeachError> { + // match on `selected` + match selected { + // NetworkConf: root + 0 => { + oled_client::clear()?; + oled_client::write(0, 0, "> Client Mode", "6x8")?; + oled_client::write(0, 9, " Access Point Mode", "6x8")?; + oled_client::flush()?; + + Ok(()) + } + // NetworkConf: client + 1 => { + oled_client::write(0, 0, "> ", "6x8")?; + oled_client::write(0, 9, " ", "6x8")?; + oled_client::flush()?; + + Ok(()) + } + // NetworkConf: ap + 2 => { + oled_client::write(0, 0, " ", "6x8")?; + oled_client::write(0, 9, "> ", "6x8")?; + oled_client::flush()?; + + Ok(()) + } + // outlier + _ => Ok(()), + } +} + +pub fn state_reboot() -> Result<(), PeachError> { + oled_client::clear()?; + oled_client::write(27, 16, "REBOOTING", "6x8")?; + oled_client::write(27, 27, "DEVICE...", "6x8")?; + oled_client::flush()?; + + let three_secs = time::Duration::from_millis(3000); + thread::sleep(three_secs); + + oled_client::power(false)?; + info!("Rebooting device"); + process::Command::new("sudo") + .arg("/sbin/shutdown") + .arg("-r") + .arg("now") + .output() + .expect("Failed to reboot"); + + Ok(()) +} + +pub fn state_shutdown() -> Result<(), PeachError> { + oled_client::clear()?; + oled_client::write(27, 16, "SHUTTING", "6x8")?; + oled_client::write(27, 27, "DOWN", "6x8")?; + oled_client::write(27, 38, "DEVICE...", "6x8")?; + oled_client::flush()?; + + let three_secs = time::Duration::from_millis(3000); + thread::sleep(three_secs); + + oled_client::power(false)?; + info!("Shutting down device"); + process::Command::new("sudo") + .arg("/sbin/shutdown") + .arg("now") + .output() + .expect("Failed to shutdown"); + + Ok(()) +} + +pub fn state_stats() -> Result<(), PeachError> { + let cpu = stats_client::cpu_stats_percent()?; + let cpu_stats = format!( + "CPU {} us {} sy {} id", + cpu.user.round(), + cpu.system.round(), + cpu.idle.round() + ); + let mem = stats_client::mem_stats()?; + let mem_stats = format!("MEM {}MB f {}MB u", mem.free / 1024, mem.used / 1024); + let load = stats_client::load_average()?; + let load_stats = format!("LOAD {} {} {}", load.one, load.five, load.fifteen); + let uptime = stats_client::uptime()?; + let uptime_stats = format!("UPTIME {} mins", uptime); + let traffic = network_client::traffic("wlan0")?; + let rx = traffic.received / 1024 / 1024; + let rx_stats = format!("DATA RX {}MB", rx); + let tx = traffic.transmitted / 1024 / 1024; + let tx_stats = format!("DATA TX {}MB", tx); + + oled_client::clear()?; + oled_client::write(0, 0, &cpu_stats, "6x8")?; + oled_client::write(0, 9, &mem_stats, "6x8")?; + oled_client::write(0, 18, &load_stats, "6x8")?; + oled_client::write(0, 27, &uptime_stats, "6x8")?; + oled_client::write(0, 36, &rx_stats, "6x8")?; + oled_client::write(0, 45, &tx_stats, "6x8")?; + oled_client::flush()?; + + Ok(()) +} + +const PEACH_LOGO: [u8; 512] = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 0, 0, 0, 0, 0, + 0, 3, 248, 14, 0, 0, 7, 0, 0, 15, 252, 63, 128, 0, 31, 192, 0, 63, 254, 127, 192, 0, 63, 224, + 0, 127, 255, 127, 224, 0, 127, 240, 0, 63, 255, 255, 128, 0, 255, 240, 0, 31, 255, 255, 192, + 31, 255, 248, 0, 15, 252, 64, 112, 63, 255, 248, 0, 24, 240, 96, 24, 127, 255, 255, 192, 48, 0, + 48, 12, 127, 255, 255, 224, 96, 0, 24, 12, 255, 255, 255, 240, 64, 0, 8, 6, 255, 255, 255, 248, + 64, 0, 12, 2, 255, 255, 255, 252, 192, 0, 4, 2, 255, 227, 255, 252, 192, 0, 4, 2, 127, 128, + 255, 252, 128, 0, 4, 2, 63, 0, 127, 252, 128, 0, 6, 2, 126, 0, 63, 252, 128, 0, 6, 3, 252, 0, + 63, 248, 128, 0, 6, 6, 0, 0, 1, 240, 192, 0, 6, 12, 0, 0, 0, 192, 192, 0, 6, 8, 0, 0, 0, 96, + 64, 0, 4, 24, 0, 0, 0, 32, 64, 0, 4, 24, 0, 0, 0, 48, 96, 0, 4, 16, 0, 0, 0, 16, 32, 0, 4, 16, + 0, 0, 0, 16, 48, 0, 12, 24, 0, 0, 0, 16, 24, 0, 8, 56, 0, 0, 0, 16, 12, 0, 24, 104, 0, 0, 0, + 48, 7, 0, 0, 204, 0, 0, 0, 96, 1, 128, 3, 134, 0, 0, 0, 192, 0, 240, 6, 3, 128, 0, 1, 128, 0, + 63, 28, 1, 255, 255, 255, 0, 0, 3, 240, 0, 31, 255, 252, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +]; diff --git a/peach-menu/src/structs.rs b/peach-menu/src/structs.rs new file mode 100644 index 0000000..7c3ac16 --- /dev/null +++ b/peach-menu/src/structs.rs @@ -0,0 +1,53 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize)] +pub struct CpuStat { + pub user: u64, + pub system: u64, + pub idle: u64, + pub nice: u64, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct CpuStatPercentages { + pub user: f32, + pub system: f32, + pub idle: f32, + pub nice: f32, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct DiskUsage { + pub filesystem: Option, + 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, Deserialize, Serialize)] +pub struct LoadAverage { + pub one: f32, + pub five: f32, + pub fifteen: f32, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct MemStat { + pub total: u64, + pub free: u64, + pub used: u64, +} + +#[derive(Debug, Deserialize)] +pub struct Traffic { + pub received: u64, + pub transmitted: u64, +} + +#[derive(Debug, Deserialize)] +pub struct Uptime { + pub secs: u64, + pub nanos: u32, +} diff --git a/peach-menu/view_mockups b/peach-menu/view_mockups new file mode 100644 index 0000000..e3aaff5 --- /dev/null +++ b/peach-menu/view_mockups @@ -0,0 +1,81 @@ + --------------------- --------------------- + |PeachCloud 21:27| |PeachCloud 11:11| + | | | | + |> Networking | | Networking | + | System Stats | |> System Stats | + | Preferences | | Preferences | + | Shutdown | | Shutdown | + | v0.1 | | v0.1 | + --------------------- --------------------- + + [ A ] [ A ] + + --------------------- --------------------- + |MODE Client | |CPU % 0.4 0.2 99.7 | + |STATUS Active | |LOAD 0.14 0.28 0.12 | + |NETWORK Home | |MEM MB 918 355 95 | + |IP 192.168.0.103 | |UPTIME 7532 HR | + |SIGNAL -48 dBm | | | + | | | | + | > Configuration | | | + --------------------- --------------------- + + [ A ] + + --------------------- + | > Activate AP | + | Scan for Networks | + | Forget Network | + | | + | | + | | + | | + --------------------- + +----- + + --------------------- --------------------- + |PeachCloud 21:27| |PeachCloud 11:11| + | | | | + |> Networking | | Networking | + | System Stats | |> System Stats | + | Shutdown | | Shutdown | + | | | | + |A - Select | B - Help| |A - Select | B - Help| + --------------------- --------------------- + + [ A ] [ A ] + + --------------------- --------------------- + |MODE Client | |SDA0 | + |STATUS Active | | USED 11.1 GB | + |NETWORK Home | | FREE 6.4 GB | + |IP 192.168.0.103 | |SDA1 | + |SIGNAL -48 dBm | | USED 1.2 GB | + | | | FREE 21.7 | + |A - Config | B - Back| |A - Config | B - Back| + --------------------- --------------------- + + [ A ] + + --------------------- + |> Activate AP | + | Scan for Networks | + | Forget Network | + | | + | | + | | + |A - Config | B - Back| + --------------------- + +----- + + --------------------- + |BATTERY 83 % | + |CPU TEMP 45 C | + |UPTIME 1d 12m 33s | + | | + | | + | | + |A - Config | B - Back| + --------------------- diff --git a/peach-meta/.gitattributes b/peach-meta/.gitattributes new file mode 100644 index 0000000..beb3964 --- /dev/null +++ b/peach-meta/.gitattributes @@ -0,0 +1,4 @@ +*.png filter=lfs diff=lfs merge=lfs -text +*.jpg filter=lfs diff=lfs merge=lfs -text +*.webm filter=lfs diff=lfs merge=lfs -text +*.svg filter=lfs diff=lfs merge=lfs -text diff --git a/peach-meta/comms/april-2018-grant.md b/peach-meta/comms/april-2018-grant.md new file mode 100644 index 0000000..20f59a8 --- /dev/null +++ b/peach-meta/comms/april-2018-grant.md @@ -0,0 +1,146 @@ +# ButtCloud :partly_sunny: :rainbow: + +_an April 2018 #ssbc-grants proposal_ + +i'd like to work on better pub infrastructure and a hosted pub-as-a-service product. + +## team + +me! :smiley_cat: + +## goals + +- we build an [open source business](http://blog.dinosaur.is/workers-of-open-source-unite/) on Scuttlebutt! +- a pub is [your personal cloud device](%Gwqklkj0b2CBT5tPiz5170NWsPp3xiuLbOImEaG/e+4=.sha256) that is always available and publicly accessible +- a pub should be affordable (start pricing at ~$7/month per pub, try to get to ~$1/month per pub) +- we support open source infrastructure providers, like [OVH](https://www.ovh.com/world/public-cloud/) and [Catalyst](http://catalyst.net.nz/catalyst-cloud) +- the infrastructure code should easy for others to contribute and maintain + +## the story so far + +pubs are important ([1](%f6ZRXO2t0rwUw/lq5FpWCHtuHc9406Q37TB+lF9bUbc=.sha256), [2](https://writtenby.adriengiboire.com/2018/03/14/my-first-week-experience-with-scuttlebutt-and-patchwork/), [3](https://twitter.com/nicolasini/status/974364249219727360)), but [public pubs are a stop-gap we've used for too long](%MZCHPVkh8sNhTsevWgSVXNlL6dYgSnzBvB3hJcJZ/7k=.sha256), we need [private pubs for everyone](%Gwqklkj0b2CBT5tPiz5170NWsPp3xiuLbOImEaG/e+4=.sha256)! + +[over the holidays](%1TVZigDql9VAQaZZVX/QegKan18urBuXQikOWE1uTMk=.sha256), i got maybe 80% of the way towards automated Scuttlebutt pub infrastructure using [Salt Stack](https://docs.saltstack.com/), hosted on [Open Stack](https://docs.openstack.org) using [OVH public cloud](https://www.ovh.com/world/public-cloud/). i've also been able to achieve _mostly_ reliable pubs using an [`ssb-pub`](https://github.com/ahdinosaur/ssb-pub) Docker image. + +i want to throw away all the [Salt Stack](https://docs.saltstack.com/) work i did and start over with [Docker Swarm](https://docs.docker.com/engine/swarm/). + +i expect this will take [longer than a month of full-time work to complete](%9ZzHJ2F0MHncqLLInC47Tp/OuHEUcHyRfWYierUpUKc=.sha256). + +## sub-projects + +there are three main sub-projects: + +- provider service +- hub swarm +- pub service + +## architecture + +- provider service + - web app + - join + - land + - sign in + - create pub + - pay for pub + - start pub service + - monitor + - land + - sign in + - view pub + - see stats + - command + - land + - sign in + - view pub + - run command + - web api + - users + - id + - name + - email + - pub + - bots + - id + - userId + - name + - status (up, down, none) + - stats + - stream Docker stats + - commands + - relay commands to pub services + - orchestrator + - on schedule, check what pubs are up + - have 1 pub per 1 GB memory, 1 hub per 15 GB memory + - queue worker jobs to ensure correct swarm + - payment + - products + - plans + - customers + - subscriptions + - swarm worker + - manage hub [machines](https://docs.docker.com/machine/drivers/openstack/) + - create hub + - destroy hub + - manage pub [services](https://docs.docker.com/engine/swarm/swarm-tutorial/deploy-service/) + - ensure pub service is up + - ensure pub service is down + - mailer worker +- pub service + +## stack + +- provider service + - web api + - [@feathersjs/socketio](https://github.com/feathersjs/socketio) + - [@feathersjs/authentication](https://github.com/feathersjs/authentication) + - [@feathersjs/authentication-jwt](https://github.com/feathersjs/authentication-jwt) + - [feathers-stripe](https://github.com/feathersjs-ecosystem/feathers-stripe) + - [node-resque](https://github.com/taskrabbit/node-resque) + - [docker-remote-api](https://github.com/mafintosh/docker-remote-api) + - web app + - [next.js](https://github.com/zeit/next.js/) + - [ramda](http://ramdajs.com/docs/) + - [@feathersjs/socketio-client](https://github.com/feathersjs/socketio-client) + - [@feathersjs/authentication-client](https://github.com/feathersjs/authentication-client) + - [react](https://facebook.github.io/react) + - [react-hyperscript](https://github.com/mlmorg/react-hyperscript) + - [recompose](https://github.com/acdlite/recompose) + - [fela](https://github.com/rofrischmann/fela) + - [material-ui](https://material-ui.com/) + - [react-stripe-elements](https://github.com/stripe/react-stripe-elements) + - swarm worker + - [node-resque](https://github.com/taskrabbit/node-resque) + - [docker-remote-api](https://github.com/mafintosh/docker-remote-api) + - mailer worker + - [node-resque](https://github.com/taskrabbit/node-resque) + - [nodemailer](https://github.com/nodemailer/nodemailer) + - third-party: [sendgrid](https://sendgrid.com/) + - dev tool: [maildev](https://github.com/djfarrelly/maildev) +- [pub service](http://github.com/ahdinosaur/ssb-pub) + - [scuttlebot](https://github.com/ssbc/scuttlebot) + - [ssb-viewer](%MeCTQrz9uszf9EZoTnKCeFeIedhnKWuB3JHW2l1g9NA=.sha256) + - [git-ssb-web](%q5d5Du+9WkaSdjc8aJPZm+jMrqgo0tmfR+RcX5ZZ6H4=.sha256) + +## roadmap + +_rough draft, subject to change_ + +- upgrade pub service + - update `ssb-pub` to use docker-compose (so we can host multiple Scuttlebutt processes in the same service) + - add `ssb-viewer` + - add `git-ssb-web` +- prototype hub swarm + - setup single "hub manager" to be docker swarm manager + - run mock provider service on manager + - setup many "hub worker"s to be docker swarm workers +- create provider service + - upgrade mock provider service to include postgres and redis databases + - scaffold web api, web app, swarm worker, and mailer worker + - implement provider web app user flows +- automate swarm + - implement swarm orchestration functionality +- start business + - understand costs, determine prices, forecast profit/loss + - decide on company jurisdiction and legal structure + - [copy](https://getterms.io/) or create (with help) a Terms of Service & Privacy Policy diff --git a/peach-meta/diary.md b/peach-meta/diary.md new file mode 100644 index 0000000..1daa794 --- /dev/null +++ b/peach-meta/diary.md @@ -0,0 +1,271 @@ +# dev diary + +## 2018-03-29 + +@dinosaur + +- setup [ButtCloud.org](http://buttcloud.org) and various linked services (GitHub, Docker Cloud, Twitter, Gmail) +- started [`buttcloud/meta`](https://github.com/buttcloud/meta) repo to store any meta information (including [these dev diary entries](https://github.com/buttcloud/meta/blob/master/diary.md)) +- started [`buttcloud/butt-landing](https://github.com/buttcloud/butt-landing) to be a simple public landing page for pub servers + - working towards a multi-service pub using Docker Compose, i realized `ssb-viewer` and `git-ssb-web` were non-trivial to install in typical environments + - the plan is to use this as a simple secondary service to get a multi-service pub working, then later can focus on the other secondary services + +## 2018-03-30 + +@dinosaur + +- made [Docker image](https://hub.docker.com/r/buttcloud/butt-landing) for `buttcloud/butt-landing` +- extracted minimal peer server and client code from `scuttlebot` into [`buttcloud/butt-peer`](https://github.com/buttcloud/butt-peer) + - added custom logging plugin which uses [`pino`](https://github.com/pinojs/pino), to try and have a consistent logging system across services + - made [two Docker images](https://hub.docker.com/r/buttcloud/butt-peer), one for `butt-peer-server` and one for `butt-peer-client` + - i combined them so i could use the client code in the server healthcheck + +## 2018-04-03 + +@dinosaur + +- started `buttcloud/butt` as `docker-compose.yml` of peer server, landing server, and nginx proxy +- update `butt-peer-server` and `butt-peer-client` to not use `node` user, easier to start with default `root` user + - why? i ran into an error with volume data permissions, would rather punt to later + - UPDATE: changed this back, got it working with `node` user, needed to create volume in `Dockerfile` then mount external volume at same path +- update `butt-landing` to auto re-connect to sbot, so it doesn't error when sbot is not yet up + - why? this is the recommended way to have docker services depend on each other, `docker-compose.yml` v3 not longer supports `depends_on: condition: healthy` +- setup Docker Hub automated builds to build tagged images based on git version tags + - match tag name: `/^v[0-9.]+$/`, docker tag is same + +## 2018-04-04 + +@dinosaur + +- got minimal `buttcloud/butt` working! +- iron out some kinks... + +## 2018-04-08 + +@dinosaur + +- switch to focus on Docker Swarm +- change to use Traefik instead of Nginx Proxy + +## 2018-04-09 + +@dinosaur + +- keep trying to get the Swarm setup to work, grr...! + +## 2018-04-13 + +@dinosaur + +- take a break from Swarm for meow +- first pass at scaffolding [`buttcloud/buttcloud-provider`](https://github.com/buttcloud/buttcloud-provider) + - have a working web server, browser app, and worker, but not yet complete to start feature development + +## 2018-04-14 + +@dinosaur + +- more progress toward provider app scaffolding + +![buttcloud diary](./images/2018-04-14-buttcloud-landing.jpg) + +## 2018-04-15 to 2018-04-17 + +@dinosaur + +provider app has landed! + +![provider app landing](./images/2018-04-17-buttcloud-landing.webm) + +- integrate `redux-bundler` +- add emojis +- found seamless background image to tile on landing page +- start onboarding workflow +- implement start step of onboarding + - validate forms on client + - validate service calls on server, show errors in form + - show success or failure messages as snackbar + - after form submission (which creates the user) + - generate json web token that identifies user + - send welcome email to next page of onboarding with token + - emails are sent by queuing a delayed job to a worker (`node-resque`) + - setup decent email templates with `mjml` + - store user in local storage in case they refresh page before progressing + - show help text on page + - allow user to resend onboarding email + +## 2018-04-18 + +@ahdinosaur + +back to the infra side, during breakfast this morning i figured out why `buttcloud/butt` was failing! + +i had a hunch that it had to do with the address that `sbot` was binding to. i configured `host` as `example_butt-peer-server`, since that's how the Docker service was to be identified within the Docker network. but still, the health checker inside the service couldn't find it. i changed this to `0.0.0.0` and it works! + +did the same for the landing service. now the stack comes online, you can `curl -H "Host: example.butt.nz" localhost` and get the output from the landing page associated with `example.butt.nz` (proxied by `traefik`). + +next i added a [custom plugin to `butt-peer-server`](https://github.com/buttcloud/butt-peer/blob/3c4390907eebe18f98e5f5d9c839161b9d1e001e/server/plugins/address.js) that allows you to configure `externalHost`, in case it differs from `host`. this means we can bind to `host` (like `0.0.0.0`) but advertise our public multiserver address as `example.butt.nz` (like for invites). + +then, on a whim from @mischa, i went [back to `buttcloud-provider` to swap `redux-form` for `final-form`](https://github.com/buttcloud/buttcloud-provider/pull/4), easy as. + +made up some issues, want to step back to think about the next steps from here. + +also made the ButtCloud logo! + +![ButtCloud logo](./images/logo.png) + +## 2018-04-19 + +@ahdinosaur + +- setup contributor license agreement: https://github.com/buttcloud/meta/issues/6 +- setup kanbans + - [dev](https://github.com/orgs/buttcloud/projects/1) + - [ops](https://github.com/orgs/buttcloud/projects/2) + - [biz](https://github.com/orgs/buttcloud/projects/3) + +## 2018-04-26 + +@ahdinosaur + +- start to separate pub and hub stacks in swarm setup: https://github.com/buttcloud/butt/commit/426deb39b9880100fe82ba5960da3d43fe74c452 +- worked on deploy for web app demo: https://github.com/buttcloud/buttcloud-provider/pull/9 + - browser code is up at: (using netlify for free) + - api server is up at (using heroku for free) + +## 2018-04-27 + +@ahdinosaur + +- discovered and documented bug with `tinyify`: https://github.com/browserify/tinyify/issues/10 +- add standard style setup to web app: https://github.com/buttcloud/buttcloud-provider/pull/10 + +## 2018-04-30 + +@ahdinosaur + +- demo is now live! [demo.buttcloud.org](https://demo.buttcloud.org) :sheep: +- renamed sub-projects to either `buttpub*` or `butthub*`, to standardize language: https://github.com/buttcloud/meta/issues/7 +- setup continuous integration for `butthub-provider`: https://github.com/buttcloud/butthub-provider/pull/11 +- setup ButtCloud account with OVH + - apply for their startup support program (for maybe $1k cloud credit): https://www.ovh.com/world/dlp/ +- play with `docker-machine` to create a local swarm across many machines + - get the swarm scripts from `buttpub` working, now across multiple virtual machines + +## 2018-05-01 + +@ahdinosaur + +started [`docker-up`](https://github.com/buttcloud/docker-up): opinionated glue to manage our Docker swarm + +## 2018-05-02 + +@ahdinosaur + +continued with `docker-up` + +- ended up making a fun little continuable (`cb => {}`) based async flow control library in `./util/async.js`, maybe will publish as `flowstep` +- realized that the Docker API doesn't handle the `docker stack *` functionality, that's implemented in the Docker CLI + - i learned that a "stack" is really a set of networks, volumes, and services each with a label "com.docker.stack.namespace" to reference the stack name + - have to decide whether + - a) to continue using the Docker API and implement that functionality ourselves + - b) to move to using the Docker CLI + - for now, will go with option a) ! + - reading the Docker CLI code, it's not scary or complex + - this way we have more low-level control of the Docker Swarm + - this way we can focus on exactly what we need for ButtCloud + +## 2018-05-03 + +more `docker-up`, getting close to `v1`! :balloon: + +- add executable cli +- clean up the api +- fractal stacks! + - top-level config is a stack, with stacks all the way down + - each stack has services, networks, volumes, AND NESTED STACKS + - each stack _may_ have a name to namespace associated services +- pretty configurable logging + +next up (notes to self): + +- better cli (take in resource type) +- use explicit docker version in api requests +- add "com.docker.stack.namespace" label to be legit docker stack + +gotta work with @Mischa on another contract meow, then Art~Hack! + +## 2018-05-04 + +@ahdinosaur + +- reviewed @austin's sweet pull request to `butthub-provider`: https://github.com/buttcloud/butthub-provider/pull/13 +- completed the "next up" features for `docker-up` in [the previous entry](%p6giuIpqWY242inxUqUdMi1RqVKU0JCPZZWJYjL1i8Q=.sha256) + +## 2018-05-07 + +@ahdinosaur + +- published `docker-up/util/async.js` as [`callstep`](https://github.com/ahdinosaur/callstep), wrote up a splash of documentation :walking_man: + +## 2018-05-08 + +@ahdinosaur + +- `docker-up`: add basic integration and unit tests using `ava`, clean up log and config wrappers using composable callsteps: https://github.com/buttcloud/docker-up/commit/29655b561b781331fc4b3a2455ea15e7360bb111 + - cc @ike +- `butthub-provider`: look into adding integration tests using `codecept` +- not ButtCloud, but got sponsored by TickTack to improve `ssb-pub`: https://github.com/ahdinosaur/ssb-pub/pull/10 + +## 2018-05-09 + +@ahdinosaur + +- `butthub-provider`: end-to-end acceptance testing with codecept: https://github.com/buttcloud/butthub-provider/pull/14 + +## 2018-05-10 + +@ahdinosaur + +- `butthub-provider`: battled some end-to-end testing dragons :dragon: : https://github.com/buttcloud/butthub-provider/pull/14 + - integrated the entire web app stack (api server, asset server, worker, and mailer) in the codecept process + - at the end of the tests, the process was hanging, who was still running? + - with some help from `why-is-node-running` and heaps of reading dependency internals, started the journey to find every remaining handle, gotta catch 'em all! :racehorse: + +## 2018-05-11 + +@ahdinosaur + +- `butthub-provider`: won the end-to-end test war: https://github.com/buttcloud/butthub-provider/pull/14 + - cleaned up every last handle, so the test process cleanly exists meow + - which means continuous integration now includes end-to-end server + browser tests! :raised_hands: + - travis gives us a running postgres and redis database, how nice +- `butthub-provider`: finish a boring dependency upgrade: https://github.com/buttcloud/butthub-provider/pull/12 +- `docker-up`: https://github.com/buttcloud/docker-up/pull/4 + - the plan was to add update for services, but along the way i realized many things were broken: + - each resource uses a different identifier! + - `down` should check if resource exists before `remove` + - `up` returns output of inspect + - [generic resource creator](https://github.com/buttcloud/docker-up/blob/55cb293d42e9ec8cf7d15394ad9115db2cb17f26/resources/generic.js) now takes: + - `name` + - `hasUpdate`: true for `service`, false otherwise + - `idField`: network uses `Id, volume only has `Name`, service uses `ID` +- `docker-up`: add continuous integration tests: https://github.com/buttcloud/docker-up/pull/5 :white_check_mark: + - travis even gives us running docker to play with! + +## 2018-05-15 - 2018-06-09 + +@ahdinosaur + +- am starting to finally understand some advanced fp (functional programming) concepts like monads +- `docker-up`: investigate using `sanctuary` +- `docker-up`: port code to use `ramda`, `folktale`, `folktale-validations`, and `fluture` +- `docker-up`: re-architect how everything works + - given next config, translate into docker api config + - fetch all current docker api config + - diff docker api current and next configs + - show diff to sysadmin + - if acceptable, translate the diff into docker api commands and run those +- rename `docker-up` to `gyne` +- rename ButtCloud to PeachCloud +- get example swarm working across 3 virtual machines using `docker-machine` and `gyne` diff --git a/peach-meta/images/2018-04-14-buttcloud-landing.jpg b/peach-meta/images/2018-04-14-buttcloud-landing.jpg new file mode 100644 index 0000000..8133206 --- /dev/null +++ b/peach-meta/images/2018-04-14-buttcloud-landing.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b1f6b3c3edda9a0208155587af991924b174e34303f1b146ff6d3134f906a50 +size 78050 diff --git a/peach-meta/images/2018-04-17-buttcloud-landing.webm b/peach-meta/images/2018-04-17-buttcloud-landing.webm new file mode 100644 index 0000000..10cbc5f --- /dev/null +++ b/peach-meta/images/2018-04-17-buttcloud-landing.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:245f952d53518713ecb90521b30131ae449edeb6226f679b837e7eb02f9d84d5 +size 704447 diff --git a/peach-meta/images/logo-square.png b/peach-meta/images/logo-square.png new file mode 100644 index 0000000..f2437cc --- /dev/null +++ b/peach-meta/images/logo-square.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0fa0ec60fa75b8347bd56d6a5eb5bd54d838b6f3f038e3d3b1a2082a38339604 +size 38577 diff --git a/peach-meta/images/logo-square.svg b/peach-meta/images/logo-square.svg new file mode 100644 index 0000000..c96d0a2 --- /dev/null +++ b/peach-meta/images/logo-square.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:704c4826e9d1a045dc7d3a066e2bb850d3ae9f4a2d005b08d73462ae298ac352 +size 5831 diff --git a/peach-meta/images/logo.png b/peach-meta/images/logo.png new file mode 100644 index 0000000..004882e --- /dev/null +++ b/peach-meta/images/logo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6bd71ddde0172abdcb3c5953f23e7f1324dc4f7dd3d4e9b720308d865cce2859 +size 34537 diff --git a/peach-meta/images/logo.svg b/peach-meta/images/logo.svg new file mode 100644 index 0000000..6d78ce6 --- /dev/null +++ b/peach-meta/images/logo.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb27730252530f3e7a8db2817842a4883d36eddd7492511d592d42c2bea571ac +size 5741 diff --git a/peach-meta/processes/cla.md b/peach-meta/processes/cla.md new file mode 100644 index 0000000..367b9a9 --- /dev/null +++ b/peach-meta/processes/cla.md @@ -0,0 +1,19 @@ +# Contributor License Agreement + +to contribute to ButtCloud, you must first sign a Contributor License Agreeement. + +## why? + +@ahdinosaur, the steward of ButtCloud, is committed to open source, at the moment all the code is licensed under the AGPL-3.0 copyleft licence. + +but we don't the ButtCloud project to be locked in to a legacy license, when promising new licenses such as a [License Zero](http://licensezero.com/) explore new legal territories, where we may in the future want to move to a peer production, copyfarleft, or reciprocity license. + +a Contributor License Agreement, as implemented here, allows contributors to grant a broad license to @ahdinosaur, the steward of ButtCloud. + +## how? + +once you contribute something significant, @ahdinosaur will contact you and setup a digital signature process using [HelloSign](https://hellosign.com). + +the document to sign is [this Individual Contributor License Agreement](https://drive.google.com/file/d/11bR0SIPjCjdBLi-p-XZo2CBPBeguCdAS/view?usp=sharing). + +after you sign, the document will be uploaded to a [drive](https://drive.google.com/drive/u/4/folders/1BZ0JezJ_9DhJ2HgpoyDV4qirpjcAXIyi) for safe storage. diff --git a/peach-monitor/.cargo/config b/peach-monitor/.cargo/config new file mode 100644 index 0000000..4b6f460 --- /dev/null +++ b/peach-monitor/.cargo/config @@ -0,0 +1,4 @@ +[target.aarch64-unknown-linux-gnu] +linker = "aarch64-linux-gnu-gcc" +objcopy = { path ="aarch64-linux-gnu-objcopy" } +strip = { path ="aarch64-linux-gnu-strip" } diff --git a/peach-monitor/.gitignore b/peach-monitor/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/peach-monitor/.gitignore @@ -0,0 +1 @@ +/target diff --git a/peach-monitor/Cargo.toml b/peach-monitor/Cargo.toml new file mode 100644 index 0000000..2772fdf --- /dev/null +++ b/peach-monitor/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "peach-monitor" +version = "0.1.1" +authors = ["mycognosist "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +ctrlc = "3.1.6" +nest = "1.0.0" +probes = "0.3" +serde_json = "1.0.57" +structopt = "0.3" +xdg = "2.2.0" diff --git a/peach-monitor/LICENSE b/peach-monitor/LICENSE new file mode 100644 index 0000000..0ad25db --- /dev/null +++ b/peach-monitor/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/peach-monitor/README.md b/peach-monitor/README.md new file mode 100644 index 0000000..21de87d --- /dev/null +++ b/peach-monitor/README.md @@ -0,0 +1,92 @@ +# peach-monitor + +![Generic badge](https://img.shields.io/badge/version-0.1.1-.svg) + +Monitor network data usage and set alert flags based on user-defined thresholds. + +`peach-monitor` is a CLI tool capable of running as a one-shot data store updater or as a daemon for continually updating data usage alert flags. + +The utility is intended to be run with the `--save` flag prior to each system reboot or shutdown. This allows network transmission totals (upload and download) to be persisted to the filesystem in the form of a JSON data store. + +When the `--update` flag is set, `peach-monitor` retrieves user-defined alert thresholds from the data store, calculates the latest data usage statistics and sets alert flags accordingly. These flag values can be accessed from other parts of the PeachCloud system to alert the user (for example, by `peach-web` for web application display). + +The `--daemon` flag executes the `--update` functionality in a loop and is intended to be run as a background process for convenient alert flag updates. The optional `--interval` argument defines the frequency with which the alert flags are updated. The default update frequency is once every 60 seconds. + +The `--iface` argument is used to define the network interface from which to retrieve network traffic data statistics. This defaults to `wlan0` if not defined. + +### Usage + +`peach-monitor [FLAGS] [OPTIONS]` + +```bash +FLAGS: + -d, --daemon Run daemon + -h, --help Prints help information + -s, --save Save latest usage totals to file + -u, --update Update alert flags + -V, --version Prints version information + +OPTIONS: + -i, --iface Define network interface [default: wlan0] + -t, --interval Define time interval for updating alert flags (seconds) [default: 60] +``` + +### Data Store + +`~/.local/share/peachcloud` + +``` +. +└── net + ├── alert.json // programatically-defined alert flags + ├── notify.json // user-defined alert thresholds + └── traffic.json // network transmission totals +``` + +### Alert Types + +`peach-monitor` defines warning and critical thresholds and corresponding alert flags for total network data traffic. The critical threshold may allow a disable-network feature in future implementations of `peach-monitor`. + +### Debian Packaging + +A `systemd` service file and Debian maintainer scripts are included in the `debian` directory, allowing `peach-monitor` 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-monitor` + +Build the package: + +`cargo deb` + +The output will be written to `target/debian/peach-monitor_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-monitor_0.1.0_arm64.deb` + +The service will be automatically enabled and started. + +Uninstall the service: + +`sudo apt-get remove peach-monitor` + +Remove configuration files (not removed with `apt-get remove`): + +`sudo apt-get purge peach-monitor` + +### Roadmap + +- Add disk-usage tracking and alerts + +### Licensing + +AGPL-3.0 diff --git a/peach-monitor/debian/peach-monitor.service b/peach-monitor/debian/peach-monitor.service new file mode 100644 index 0000000..92ae7bf --- /dev/null +++ b/peach-monitor/debian/peach-monitor.service @@ -0,0 +1,27 @@ +[Unit] +Description=Monitor network data usage and set alert flags based on user-defined thresholds. + +[Service] +Type=simple +User=peach-monitor +Environment="RUST_LOG=error" +ExecStart=/usr/bin/peach-monitor +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 diff --git a/peach-monitor/debian/postinst b/peach-monitor/debian/postinst new file mode 100644 index 0000000..4c765b2 --- /dev/null +++ b/peach-monitor/debian/postinst @@ -0,0 +1,17 @@ +#!/bin/sh + +# This will only remove masks created by d-s-h on package removal. +deb-systemd-helper unmask peach-monitor.service > /dev/null || true + +# was-enabled defaults to true, so new installations run enable. +if deb-systemd-helper --quiet was-enabled peach-monitor.service +then + # Enables the unit on first installation, creates new + # symlinks on upgrades if the unit file has changed. + deb-systemd-helper enable peach-monitor.service > /dev/null || true + deb-systemd-invoke start peach-monitor +else + # Update the statefile to add new symlinks (if any), which need to be + # cleaned up on purge. Also remove old symlinks. + deb-systemd-helper update-state peach-monitor.service > /dev/null || true +fi diff --git a/peach-monitor/debian/postrm b/peach-monitor/debian/postrm new file mode 100644 index 0000000..f9007f9 --- /dev/null +++ b/peach-monitor/debian/postrm @@ -0,0 +1,13 @@ +#!/bin/sh + +# In case this system is running systemd, we make systemd reload the unit files +# to pick up changes. +if [ -d /run/systemd/system ] ; then + systemctl --system daemon-reload > /dev/null || true +fi + +if [ "$1" = "remove" ]; then + if [ -x "/usr/bin/deb-systemd-helper" ]; then + deb-systemd-helper mask peach-monitor.service > /dev/null + fi +fi diff --git a/peach-monitor/debian/prerm b/peach-monitor/debian/prerm new file mode 100644 index 0000000..2b25c31 --- /dev/null +++ b/peach-monitor/debian/prerm @@ -0,0 +1,3 @@ +#!/bin/sh + +deb-systemd-invoke stop peach-monitor > /dev/null || true diff --git a/peach-monitor/src/error.rs b/peach-monitor/src/error.rs new file mode 100644 index 0000000..d18dc0c --- /dev/null +++ b/peach-monitor/src/error.rs @@ -0,0 +1,15 @@ +//! Basic error handling for network and nest. + +use std::error; + +pub type BoxError = Box; + +#[derive(Debug)] +pub enum NetworkError { + +} + +#[derive(Debug)] +pub enum NestError { + +} diff --git a/peach-monitor/src/main.rs b/peach-monitor/src/main.rs new file mode 100644 index 0000000..ab7e3e8 --- /dev/null +++ b/peach-monitor/src/main.rs @@ -0,0 +1,202 @@ +use std::convert::TryInto; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::{thread, time}; + +use nest::{Error, Store, Value}; +use probes::network; +use serde_json::json; +use structopt::StructOpt; + +#[derive(StructOpt, Debug)] +#[structopt( + name = "peach-monitor", + about = "Monitor data usage and set alert flags" +)] +struct Opt { + /// Run daemon + #[structopt(short, long)] + daemon: bool, + + /// Define network interface + #[structopt(short, long, default_value = "wlan0")] + iface: String, + + /// Save latest usage totals to file + #[structopt(short, long)] + save: bool, + + /// Define time interval for updating alert flags (seconds) + #[structopt(short = "t", long, default_value = "60")] + interval: u64, + + /// Update alert flags + #[structopt(short, long)] + update: bool, +} + +/// Network traffic total (bytes) +#[derive(Debug)] +struct Traffic { + total: u64, // bytes +} + +impl Traffic { + /// Retrieve latest statistics for traffic + fn get(iface: &str) -> Option { + let network = network::read().expect("IO error when executing network command"); + for (interface, data) in network.interfaces { + if interface == iface { + let rx = data.received; + let tx = data.transmitted; + let total = rx + tx; + let t = Traffic { total }; + return Some(t); + }; + } + None + } +} + +/// Warning and cutoff network traffic threshold (bytes) +struct Threshold { + warn: u64, // warning threshold (bytes) + cut: u64, // cutoff threshold (bytes) +} + +impl Threshold { + /// Retrieve latest alert threshold from the data store + fn get(store: &Store) -> Threshold { + let mut threshold = Vec::new(); + + let warn_val = store + .get(&["net", "notify", "warn"]) + .unwrap_or(Value::Uint(0)); + if let Value::Uint(val) = warn_val { + threshold.push(val); + }; + + let cut_val = store + .get(&["net", "notify", "cut"]) + .unwrap_or(Value::Uint(0)); + if let Value::Uint(val) = cut_val { + threshold.push(val); + }; + + Threshold { + warn: threshold[0], + cut: threshold[1], + } + } +} + +/// Convert a megabyte value to bytes +fn to_bytes(val: u64) -> u64 { + (val * 1024) * 1024 +} + +/// Evaluate traffic values against alert thresholds and set flags +fn set_alert_flags(store: &Store, threshold: &Threshold) -> Result<(), Error> { + let stored_total = store.get(&["net", "traffic", "total"])?; + if let Value::Uint(total) = stored_total { + // total is in bytes while warn is in megabytes + if total > to_bytes(threshold.warn) { + store.set(&["net", "alert", "warn_alert"], &Value::Bool(true))?; + } else { + store.set(&["net", "alert", "warn_alert"], &Value::Bool(false))?; + } + if total > to_bytes(threshold.cut) { + store.set(&["net", "alert", "cut_alert"], &Value::Bool(true))?; + } else { + store.set(&["net", "alert", "cut_alert"], &Value::Bool(false))?; + } + } + + Ok(()) +} + +/// Calculate and store the latest network transmission totals +fn update_transmission_totals(iface: &str, store: &Store) -> Result<(), Error> { + // retrieve previous network traffic statistics + let stored_total = match store.get(&["net", "traffic", "total"]) { + Ok(total) => total, + // return 0 if no value exists + Err(_) => Value::Uint(u64::MIN), + }; + + // retrieve latest network traffic statistics + let traffic = Traffic::get(iface).expect("Error while retrieving network traffic statistics"); + + // store updated network traffic statistics (totals) + if let Value::Uint(total) = stored_total { + let updated_total = total + traffic.total; + let total_value = Value::Uint(updated_total); + store.set(&["net", "traffic", "total"], &total_value)?; + }; + + Ok(()) +} + +fn main() -> Result<(), Error> { + // parse cli arguments + let opt = Opt::from_args(); + + // define the path + let path = xdg::BaseDirectories::new() + .unwrap() + .create_data_directory("peachcloud") + .unwrap(); + + // define the schema + let schema = json!({ + "net": { + "traffic": "json", + "notify": "json", + "alert": "json" + } + }) + .try_into()?; + + // create the data store + let store = Store::new(path, schema); + + // update network transmission totals + if opt.save { + update_transmission_totals(&opt.iface, &store).unwrap(); + } + + // update alert flags + if opt.update { + // retrieve alert thresholds + let threshold = Threshold::get(&store); + + // test transmission totals against alert thresholds and set flags + set_alert_flags(&store, &threshold)?; + } + + if opt.daemon { + let running = Arc::new(AtomicBool::new(true)); + let r = running.clone(); + ctrlc::set_handler(move || { + r.store(false, Ordering::SeqCst); + }) + .expect("Error setting Ctrl-C handler"); + + let interval = time::Duration::from_secs(opt.interval); + + // run loop until SIGINT or SIGTERM is received + while running.load(Ordering::SeqCst) { + // retrieve alert thresholds + let threshold = Threshold::get(&store); + + // test transmission totals against alert threshold and set flags + set_alert_flags(&store, &threshold)?; + + thread::sleep(interval); + } + + println!("Terminating gracefully..."); + } + + Ok(()) +} diff --git a/peach-network/.cargo/config b/peach-network/.cargo/config new file mode 100644 index 0000000..4b6f460 --- /dev/null +++ b/peach-network/.cargo/config @@ -0,0 +1,4 @@ +[target.aarch64-unknown-linux-gnu] +linker = "aarch64-linux-gnu-gcc" +objcopy = { path ="aarch64-linux-gnu-objcopy" } +strip = { path ="aarch64-linux-gnu-strip" } diff --git a/peach-network/.gitignore b/peach-network/.gitignore new file mode 100644 index 0000000..ad6c6e6 --- /dev/null +++ b/peach-network/.gitignore @@ -0,0 +1,4 @@ +target +**/*.rs.bk +AP_STUFF +notes diff --git a/peach-network/.travis.yml b/peach-network/.travis.yml new file mode 100644 index 0000000..f2536c6 --- /dev/null +++ b/peach-network/.travis.yml @@ -0,0 +1,7 @@ +language: rust +rust: + - nightly +before_script: + - rustup component add clippy +script: + - cargo clippy -- -D warnings diff --git a/peach-network/Cargo.toml b/peach-network/Cargo.toml new file mode 100644 index 0000000..4e3e96d --- /dev/null +++ b/peach-network/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "peach-network" +version = "0.2.12" +authors = ["Andrew Reid "] +edition = "2018" +description = "Query and configure network interfaces using JSON-RPC over HTTP." +homepage = "https://opencollective.com/peachcloud" +repository = "https://github.com/peachcloud/peach-network" +readme = "README.md" +license = "AGPL-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" +jsonrpc-test = "11" +log = "0.4" +probes = "0.4" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +snafu = "0.6" +regex = "1" +wpactrl = "0.3.1" diff --git a/peach-network/README.md b/peach-network/README.md new file mode 100644 index 0000000..1642d93 --- /dev/null +++ b/peach-network/README.md @@ -0,0 +1,178 @@ +# 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-.svg) + +Networking microservice module for PeachCloud. Query and configure device interfaces using [JSON-RPC](https://www.jsonrpc.org/specification) over http. + +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._ + +### JSON-RPC API + +Methods for **retrieving data**: + +| 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 | + +Methods for **modifying state**: + +| 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` | + +### API Documentation + +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. + +### Environment + +The JSON-RPC HTTP server address and port can be configured with the `PEACH_NETWORK_SERVER` environment variable: + +`export PEACH_NETWORK_SERVER=127.0.0.1:5000` + +When not set, the value defaults to `127.0.0.1:5110`. + +Logging is made available with `env_logger`: + +`export RUST_LOG=info` + +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 diff --git a/peach-network/conf/04-wired.network b/peach-network/conf/04-wired.network new file mode 100644 index 0000000..6090684 --- /dev/null +++ b/peach-network/conf/04-wired.network @@ -0,0 +1,22 @@ +[Match] +Name=e* + +[Network] +## Uncomment only one option block +# Option: using a DHCP server and multicast DNS +LLMNR=no +LinkLocalAddressing=no +MulticastDNS=yes +DHCP=ipv4 + +# Option: using link-local ip addresses and multicast DNS +#LLMNR=no +#LinkLocalAddressing=yes +#MulticastDNS=yes + +# Option: using static ip address and multicast DNS +# (example, use your settings) +#Address=192.168.50.60/24 +#Gateway=192.168.50.1 +#DNS=84.200.69.80 1.1.1.1 +#MulticastDNS=yes diff --git a/peach-network/conf/08-wlan0.network b/peach-network/conf/08-wlan0.network new file mode 100644 index 0000000..24ca2e2 --- /dev/null +++ b/peach-network/conf/08-wlan0.network @@ -0,0 +1,4 @@ +[Match] +Name=wlan0 +[Network] +DHCP=yes diff --git a/peach-network/conf/12-ap0.network b/peach-network/conf/12-ap0.network new file mode 100644 index 0000000..f7ab3f6 --- /dev/null +++ b/peach-network/conf/12-ap0.network @@ -0,0 +1,11 @@ +[Match] +Name=ap0 +[Network] +Address=11.11.11.1/24 +# IPMasquerade is doing NAT +# Uncomment the two lines below to share internet over ap0 +#IPMasquerade=yes +#IPForward=yes +DHCPServer=yes +[DHCPServer] +DNS=84.200.69.80 1.1.1.1 diff --git a/peach-network/conf/ap-auto-deploy.service b/peach-network/conf/ap-auto-deploy.service new file mode 100644 index 0000000..43a122c --- /dev/null +++ b/peach-network/conf/ap-auto-deploy.service @@ -0,0 +1,10 @@ +[Unit] +Description=Start the ap0 service (access point) if the wlan0 service is active but not connected to any access point + +[Service] +Type=oneshot +RemainAfterExit=no +ExecStart=/usr/local/bin/ap_auto_deploy + +[Install] +WantedBy=multi-user.target diff --git a/peach-network/conf/ap-auto-deploy.timer b/peach-network/conf/ap-auto-deploy.timer new file mode 100644 index 0000000..610fcf3 --- /dev/null +++ b/peach-network/conf/ap-auto-deploy.timer @@ -0,0 +1,9 @@ +[Unit] +Description=Determine when and how often the ap_auto_deploy script is run + +[Timer] +OnBootSec=60s +OnUnitActiveSec=180s + +[Install] +WantedBy=timers.target diff --git a/peach-network/conf/copy-wlan.service b/peach-network/conf/copy-wlan.service new file mode 100644 index 0000000..c31f6fb --- /dev/null +++ b/peach-network/conf/copy-wlan.service @@ -0,0 +1,8 @@ +[Unit] +Before=network.target + +[Service] +ExecStart=/usr/local/bin/copy-wlan.sh + +[Install] +WantedBy=default.target \ No newline at end of file diff --git a/peach-network/conf/copy-wlan.sh b/peach-network/conf/copy-wlan.sh new file mode 100644 index 0000000..227f7ca --- /dev/null +++ b/peach-network/conf/copy-wlan.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +FILE=/boot/firmware/wpa_supplicant.conf +if test -f "$FILE"; then + cp $FILE /etc/wpa_supplicant/wpa_supplicant-wlan0.conf + chown root:netdev /etc/wpa_supplicant/wpa_supplicant-wlan0.conf + rm $FILE +fi \ No newline at end of file diff --git a/peach-network/conf/wpa_supplicant-ap0.conf b/peach-network/conf/wpa_supplicant-ap0.conf new file mode 100644 index 0000000..ec140fa --- /dev/null +++ b/peach-network/conf/wpa_supplicant-ap0.conf @@ -0,0 +1,11 @@ +ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev +update_config=1 + +network={ + ssid="peach" + mode=2 + key_mgmt=WPA-PSK + proto=RSN WPA + psk="cloudless" + frequency=2412 +} diff --git a/peach-network/conf/wpa_supplicant-wlan0.conf b/peach-network/conf/wpa_supplicant-wlan0.conf new file mode 100644 index 0000000..9f5bb95 --- /dev/null +++ b/peach-network/conf/wpa_supplicant-wlan0.conf @@ -0,0 +1,6 @@ +ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev +update_config=1 +network={ + ssid="YourRouterSsid" + psk="password_goes_here" +} diff --git a/peach-network/conf/wpa_supplicant@ap0.service b/peach-network/conf/wpa_supplicant@ap0.service new file mode 100644 index 0000000..50baf76 --- /dev/null +++ b/peach-network/conf/wpa_supplicant@ap0.service @@ -0,0 +1,18 @@ +[Unit] +Description=WPA supplicant daemon (interface-specific version) +Requires=sys-subsystem-net-devices-wlan0.device +After=sys-subsystem-net-devices-wlan0.device +Conflicts=wpa_supplicant@wlan0.service +Before=network.target +Wants=network.target + +# NetworkManager users will probably want the dbus version instead. + +[Service] +Type=simple +ExecStartPre=/sbin/iw dev wlan0 interface add ap0 type __ap +ExecStart=/sbin/wpa_supplicant -c/etc/wpa_supplicant/wpa_supplicant-%I.conf -Dnl80211,wext -i%I +ExecStopPost=/sbin/iw dev ap0 del + +[Install] +Alias=multi-user.target.wants/wpa_supplicant@%i.service diff --git a/peach-network/debian/peach-network.service b/peach-network/debian/peach-network.service new file mode 100644 index 0000000..d81099c --- /dev/null +++ b/peach-network/debian/peach-network.service @@ -0,0 +1,13 @@ +[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 diff --git a/peach-network/notes.txt b/peach-network/notes.txt new file mode 100755 index 0000000..d4a06e3 --- /dev/null +++ b/peach-network/notes.txt @@ -0,0 +1,5 @@ +curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "ssid", "params" : {"iface": "wlan0" }, "id":1 }' 127.0.0.1:5110 + +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 + +curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "add", "params" : {"ssid": "testss", "pass": "tptp" }, "id":1 }' 127.0.0.1:5110 diff --git a/peach-network/src/error.rs b/peach-network/src/error.rs new file mode 100644 index 0000000..808f208 --- /dev/null +++ b/peach-network/src/error.rs @@ -0,0 +1,351 @@ +use std::{error, io, str}; + +use jsonrpc_core::{types::error::Error, ErrorCode}; +use probes::ProbeError; +use serde_json::error::Error as SerdeError; +use snafu::Snafu; + +pub type BoxError = Box; + +#[derive(Debug, Snafu)] +#[snafu(visibility(pub(crate)))] +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"))] + 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"))] + 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, + }, + + #[snafu(display("Request to wpasupplicant via wpactrl failed"))] + WpaCtrlRequest { + #[snafu(source(from(failure::Error, std::convert::Into::into)))] + source: BoxError, + }, +} + +impl From 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, + }, + } + } +} diff --git a/peach-network/src/lib.rs b/peach-network/src/lib.rs new file mode 100644 index 0000000..14308cf --- /dev/null +++ b/peach-network/src/lib.rs @@ -0,0 +1,986 @@ +//! # peach-network +//! +//! `peach-network` is a networking microservice module for PeachCloud. It +//! exposes a JSON-RPC API over HTTP which allows querying of network interface +//! data and modification of interface state. +//! +//! The `src/network.rs` module contains the core networking logic and data +//! types for interacting with the `wpa_supplicant` process and related parts of +//! the operating system, while the `src/error.rs` module contains +//! error-handling data types and methods. +//! +//! `src/main.rs` initializes the logger, starts the application and catches +//! application errors, while `src/lib.rs` contains the JSON-RPC server, RPC +//! methods, HTTP server and tests. +//! +mod error; +pub mod network; +mod utils; + +use std::env; +use std::result::Result; + +use jsonrpc_core::{types::error::Error, IoHandler, Params, Value}; +use jsonrpc_http_server::{AccessControlAllowOrigin, DomainsValidation, ServerBuilder}; +#[allow(unused_imports)] +use jsonrpc_test as test; +use log::info; +use serde_json::json; + +use crate::error::{BoxError, NetworkError}; +use crate::network::{Iface, IfaceId, IfaceIdPass, IfaceSsid, WiFi}; + +/// Create JSON-RPC I/O handler, add RPC methods and launch HTTP server. +pub fn run() -> Result<(), BoxError> { + info!("Starting up."); + + info!("Creating JSON-RPC I/O handler."); + let mut io = IoHandler::default(); + + /* GET - All RPC methods for retrieving data */ + + io.add_method("available_networks", move |params: Params| { + let i: Result = params.parse(); + match i { + Ok(i) => { + let iface = i.iface; + match network::available_networks(&iface)? { + Some(list) => Ok(Value::String(list)), + None => Err(Error::from(NetworkError::AvailableNetworks { iface })), + } + } + Err(e) => Err(Error::from(NetworkError::MissingParams { e })), + } + }); + + io.add_method("id", move |params: Params| { + let i: Result = params.parse(); + match i { + Ok(i) => { + let iface = i.iface; + let ssid = i.ssid; + match network::id(&iface, &ssid)? { + Some(id) => Ok(Value::String(id)), + None => Err(Error::from(NetworkError::Id { iface, ssid })), + } + } + Err(e) => Err(Error::from(NetworkError::MissingParams { e })), + } + }); + + io.add_method("ip", move |params: Params| { + let i: Result = params.parse(); + match i { + Ok(i) => { + let iface = i.iface; + match network::ip(&iface)? { + Some(ip) => Ok(Value::String(ip)), + None => Err(Error::from(NetworkError::Ip { iface })), + } + } + Err(e) => Err(Error::from(NetworkError::MissingParams { e })), + } + }); + + io.add_method("ping", |_| Ok(Value::String("success".to_string()))); + + io.add_method("rssi", move |params: Params| { + let i: Result = params.parse(); + match i { + Ok(i) => { + let iface = i.iface; + match network::rssi(&iface)? { + Some(rssi) => Ok(Value::String(rssi)), + None => Err(Error::from(NetworkError::Rssi { iface })), + } + } + Err(e) => Err(Error::from(NetworkError::MissingParams { e })), + } + }); + + io.add_method("rssi_percent", move |params: Params| { + let i: Result = params.parse(); + match i { + Ok(i) => { + let iface = i.iface; + match network::rssi_percent(&iface)? { + Some(rssi) => Ok(Value::String(rssi)), + None => Err(Error::from(NetworkError::RssiPercent { iface })), + } + } + Err(e) => Err(Error::from(NetworkError::MissingParams { e })), + } + }); + + io.add_method("saved_networks", move |_| { + let list = network::saved_networks()?; + match list { + Some(list) => Ok(Value::String(list)), + None => Err(Error::from(NetworkError::SavedNetworks)), + } + }); + + io.add_method("ssid", move |params: Params| { + let i: Result = params.parse(); + match i { + Ok(i) => { + let iface = i.iface; + match network::ssid(&iface)? { + Some(ip) => Ok(Value::String(ip)), + None => Err(Error::from(NetworkError::Ssid { iface })), + } + } + Err(e) => Err(Error::from(NetworkError::MissingParams { e })), + } + }); + + io.add_method("state", move |params: Params| { + let i: Result = params.parse(); + match i { + Ok(i) => { + let iface = i.iface; + match network::state(&iface)? { + Some(state) => Ok(Value::String(state)), + None => Err(Error::from(NetworkError::State { iface })), + } + } + Err(e) => Err(Error::from(NetworkError::MissingParams { e })), + } + }); + + io.add_method("status", move |params: Params| { + let i: Result = params.parse(); + match i { + Ok(i) => { + let iface = i.iface; + match network::status(&iface)? { + Some(status) => { + let json_status = json!(status); + Ok(Value::String(json_status.to_string())) + } + None => Err(Error::from(NetworkError::Status { iface })), + } + } + Err(e) => Err(Error::from(NetworkError::MissingParams { e })), + } + }); + + io.add_method("traffic", move |params: Params| { + let i: Result = params.parse(); + match i { + Ok(i) => { + let iface = i.iface; + match network::traffic(&iface)? { + Some(traffic) => Ok(Value::String(traffic)), + None => Err(Error::from(NetworkError::Traffic { iface })), + } + } + Err(e) => Err(Error::from(NetworkError::MissingParams { e })), + } + }); + + /* SET - All RPC methods for modifying state */ + + io.add_method("activate_ap", move |_| { + network::activate_ap()?; + + Ok(Value::String("success".to_string())) + }); + + io.add_method("activate_client", move |_| { + network::activate_client()?; + + Ok(Value::String("success".to_string())) + }); + + io.add_method("add", move |params: Params| { + let w: Result = params.parse(); + match w { + Ok(w) => match network::add(&w) { + Ok(_) => Ok(Value::String("success".to_string())), + Err(e) => Err(Error::from(e)), + }, + Err(e) => Err(Error::from(NetworkError::MissingParams { e })), + } + }); + + io.add_method("check_iface", move |_| { + network::check_iface()?; + + Ok(Value::String("success".to_string())) + }); + + io.add_method("delete", move |params: Params| { + let i: Result = params.parse(); + match i { + Ok(i) => { + let id = i.id; + let iface = i.iface; + match network::delete(&id, &iface) { + Ok(_) => Ok(Value::String("success".to_string())), + Err(_) => Err(Error::from(NetworkError::Delete { id, iface })), + } + } + Err(e) => Err(Error::from(NetworkError::MissingParams { e })), + } + }); + + io.add_method("disable", move |params: Params| { + let i: Result = params.parse(); + match i { + Ok(i) => { + let id = i.id; + let iface = i.iface; + match network::disable(&id, &iface) { + Ok(_) => Ok(Value::String("success".to_string())), + Err(_) => Err(Error::from(NetworkError::Disable { id, iface })), + } + } + Err(e) => Err(Error::from(NetworkError::MissingParams { e })), + } + }); + + io.add_method("disconnect", move |params: Params| { + let i: Result = params.parse(); + match i { + Ok(i) => { + let iface = i.iface; + match network::disconnect(&iface) { + Ok(_) => Ok(Value::String("success".to_string())), + Err(_) => Err(Error::from(NetworkError::Disconnect { iface })), + } + } + Err(e) => Err(Error::from(NetworkError::MissingParams { e })), + } + }); + + io.add_method("modify", move |params: Params| { + let i: Result = params.parse(); + match i { + Ok(i) => { + let iface = i.iface; + let id = i.id; + let pass = i.pass; + match network::modify(&iface, &id, &pass) { + Ok(_) => Ok(Value::String("success".to_string())), + Err(_) => Err(Error::from(NetworkError::Modify { iface, id })), + } + } + Err(e) => Err(Error::from(NetworkError::MissingParams { e })), + } + }); + + io.add_method("reassociate", move |params: Params| { + let i: Result = params.parse(); + match i { + Ok(i) => { + let iface = i.iface; + match network::reassociate(&iface) { + Ok(_) => Ok(Value::String("success".to_string())), + Err(_) => Err(Error::from(NetworkError::Reassociate { iface })), + } + } + Err(e) => Err(Error::from(NetworkError::MissingParams { e })), + } + }); + + io.add_method("reconfigure", move |_| match network::reconfigure() { + Ok(_) => Ok(Value::String("success".to_string())), + Err(_) => Err(Error::from(NetworkError::Reconfigure)), + }); + + io.add_method("reconnect", move |params: Params| { + let i: Result = params.parse(); + match i { + Ok(i) => { + let iface = i.iface; + match network::reconnect(&iface) { + Ok(_) => Ok(Value::String("success".to_string())), + Err(_) => Err(Error::from(NetworkError::Reconnect { iface })), + } + } + Err(e) => Err(Error::from(NetworkError::MissingParams { e })), + } + }); + + io.add_method("save", move |_| match network::save() { + Ok(_) => Ok(Value::String("success".to_string())), + Err(_) => Err(Error::from(NetworkError::Save)), + }); + + io.add_method("connect", move |params: Params| { + let i: Result = params.parse(); + match i { + Ok(i) => { + let id = i.id; + let iface = i.iface; + match network::connect(&id, &iface) { + Ok(_) => Ok(Value::String("success".to_string())), + Err(_) => Err(Error::from(NetworkError::Connect { id, iface })), + } + } + Err(e) => Err(Error::from(NetworkError::MissingParams { e })), + } + }); + + let http_server = + env::var("PEACH_NETWORK_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::ErrorCode; + use std::io::Error as IoError; + use std::io::ErrorKind; + + #[test] + fn rpc_success() { + let rpc = { + let mut io = IoHandler::new(); + io.add_method("rpc_success_response", |_| { + Ok(Value::String("success".into())) + }); + test::Rpc::from(io) + }; + + assert_eq!(rpc.request("rpc_success_response", &()), r#""success""#); + } + + // test to ensure correct MissingParams parse error + #[test] + fn rpc_parse_error() { + let rpc = { + let mut io = IoHandler::new(); + io.add_method("rpc_parse_error", |_| { + let e = Error { + code: ErrorCode::ParseError, + message: String::from("Parse error"), + data: None, + }; + Err(Error::from(NetworkError::MissingParams { e })) + }); + test::Rpc::from(io) + }; + + assert_eq!( + rpc.request("rpc_parse_error", &()), + r#"{ + "code": -32700, + "message": "Parse error" +}"# + ); + } + + // test to ensure correct Add error response + #[test] + fn rpc_add_error() { + let rpc = { + let mut io = IoHandler::new(); + io.add_method("rpc_add_error", |_| { + Err(Error::from(NetworkError::Add { + ssid: "Home".to_string(), + })) + }); + test::Rpc::from(io) + }; + + assert_eq!( + rpc.request("rpc_add_error", &()), + r#"{ + "code": -32000, + "message": "Failed to add network for Home" +}"# + ); + } + + // test to ensure correct Disable error response + #[test] + fn rpc_disable_error() { + let rpc = { + let mut io = IoHandler::new(); + io.add_method("rpc_disable_error", |_| { + Err(Error::from(NetworkError::Disable { + id: "0".to_string(), + iface: "wlan0".to_string(), + })) + }); + test::Rpc::from(io) + }; + + assert_eq!( + rpc.request("rpc_disable_error", &()), + r#"{ + "code": -32029, + "message": "Failed to disable network 0 for wlan0" +}"# + ); + } + + // test to ensure correct Disconnect error response + #[test] + fn rpc_disconnect_error() { + let rpc = { + let mut io = IoHandler::new(); + io.add_method("rpc_disconnect_error", |_| { + Err(Error::from(NetworkError::Disconnect { + iface: "wlan0".to_string(), + })) + }); + test::Rpc::from(io) + }; + + assert_eq!( + rpc.request("rpc_disconnect_error", &()), + r#"{ + "code": -32032, + "message": "Failed to disconnect wlan0" +}"# + ); + } + + // test to ensure correct GenWpaPassphrase error response + #[test] + fn rpc_genwpapassphrase_error() { + let rpc = { + let mut io = IoHandler::new(); + io.add_method("rpc_genwpapassphrase_error", |_| { + Err(Error::from(NetworkError::GenWpaPassphrase { + ssid: "HomeWifi".to_string(), + source: IoError::new(ErrorKind::NotFound, "oh no!"), + })) + }); + test::Rpc::from(io) + }; + + assert_eq!( + rpc.request("rpc_genwpapassphrase_error", &()), + r#"{ + "code": -32025, + "message": "Failed to generate wpa passphrase for HomeWifi: oh no!" +}"# + ); + } + + // test to ensure correct Id error response + #[test] + fn rpc_id_error() { + let rpc = { + let mut io = IoHandler::new(); + io.add_method("rpc_id_error", |_| { + Err(Error::from(NetworkError::Id { + iface: "wlan0".to_string(), + ssid: "Home".to_string(), + })) + }); + test::Rpc::from(io) + }; + + assert_eq!( + rpc.request("rpc_id_error", &()), + r#"{ + "code": -32026, + "message": "No ID found for Home on interface wlan0" +}"# + ); + } + + // test to ensure correct NoIp error response + #[test] + fn rpc_noip_error() { + let rpc = { + let mut io = IoHandler::new(); + io.add_method("rpc_noip_error", |_| { + Err(Error::from(NetworkError::NoIp { + iface: "wlan7".to_string(), + source: IoError::new(ErrorKind::AddrNotAvailable, "oh no!"), + })) + }); + test::Rpc::from(io) + }; + + assert_eq!( + rpc.request("rpc_noip_error", &()), + r#"{ + "code": -32001, + "message": "Failed to retrieve IP address for wlan7: oh no!" +}"# + ); + } + + // test to ensure correct Rssi error response + #[test] + fn rpc_rssi_error() { + let rpc = { + let mut io = IoHandler::new(); + io.add_method("rpc_rssi_error", |_| { + Err(Error::from(NetworkError::Rssi { + iface: "wlan0".to_string(), + })) + }); + test::Rpc::from(io) + }; + + assert_eq!( + rpc.request("rpc_rssi_error", &()), + r#"{ + "code": -32002, + "message": "Failed to retrieve RSSI for wlan0. Interface may not be connected" +}"# + ); + } + + // test to ensure correct RssiPercent error response + #[test] + fn rpc_rssipercent_error() { + let rpc = { + let mut io = IoHandler::new(); + io.add_method("rpc_rssipercent_error", |_| { + Err(Error::from(NetworkError::RssiPercent { + iface: "wlan0".to_string(), + })) + }); + test::Rpc::from(io) + }; + + assert_eq!( + rpc.request("rpc_rssipercent_error", &()), + r#"{ + "code": -32034, + "message": "Failed to retrieve signal quality (%) for wlan0. Interface may not be connected" +}"# + ); + } + + // test to ensure correct Ssid error response + #[test] + fn rpc_ssid_error() { + let rpc = { + let mut io = IoHandler::new(); + io.add_method("rpc_ssid_error", |_| { + Err(Error::from(NetworkError::Ssid { + iface: "wlan0".to_string(), + })) + }); + test::Rpc::from(io) + }; + + assert_eq!( + rpc.request("rpc_ssid_error", &()), + r#"{ + "code": -32003, + "message": "Failed to retrieve SSID for wlan0. Interface may not be connected" +}"# + ); + } + + // test to ensure correct State error response + #[test] + fn rpc_state_error() { + let rpc = { + let mut io = IoHandler::new(); + io.add_method("rpc_state_error", |_| { + Err(Error::from(NetworkError::State { + iface: "wlan1".to_string(), + })) + }); + test::Rpc::from(io) + }; + + assert_eq!( + rpc.request("rpc_state_error", &()), + r#"{ + "code": -32023, + "message": "No state found for wlan1. Interface may not exist" +}"# + ); + } + + // test to ensure correct Traffic error response + #[test] + fn rpc_traffic_error() { + let rpc = { + let mut io = IoHandler::new(); + io.add_method("rpc_traffic_error", |_| { + Err(Error::from(NetworkError::Traffic { + iface: "wlan0".to_string(), + })) + }); + test::Rpc::from(io) + }; + + assert_eq!( + rpc.request("rpc_traffic_error", &()), + r#"{ + "code": -32004, + "message": "No network traffic statistics found for wlan0. Interface may not exist" +}"# + ); + } + + // test to ensure correct SavedNetworks error response + #[test] + fn rpc_savednetworks_error() { + let rpc = { + let mut io = IoHandler::new(); + io.add_method("rpc_savednetworks_error", |_| { + Err(Error::from(NetworkError::SavedNetworks)) + }); + test::Rpc::from(io) + }; + + assert_eq!( + rpc.request("rpc_savednetworks_error", &()), + r#"{ + "code": -32005, + "message": "No saved networks found" +}"# + ); + } + + // test to ensure correct AvailableNetworks error response + #[test] + fn rpc_availablenetworks_error() { + let rpc = { + let mut io = IoHandler::new(); + io.add_method("rpc_availablenetworks_error", |_| { + Err(Error::from(NetworkError::AvailableNetworks { + iface: "wlan0".to_string(), + })) + }); + test::Rpc::from(io) + }; + + assert_eq!( + rpc.request("rpc_availablenetworks_error", &()), + r#"{ + "code": -32006, + "message": "No networks found in range of wlan0" +}"# + ); + } + + // test to ensure correct MissingParams error response + #[test] + fn rpc_missingparams_error() { + let rpc = { + let mut io = IoHandler::new(); + io.add_method("rpc_missingparams_error", |_| { + let e = Error { + code: ErrorCode::InvalidParams, + message: String::from( + "Invalid params: invalid type: null, expected struct Iface.", + ), + data: None, + }; + Err(Error::from(NetworkError::MissingParams { e })) + }); + test::Rpc::from(io) + }; + + assert_eq!( + rpc.request("rpc_missingparams_error", &()), + r#"{ + "code": -32602, + "message": "Invalid params: invalid type: null, expected struct Iface." +}"# + ); + } + + // test to ensure correct Modify error response + #[test] + fn rpc_modify_error() { + let rpc = { + let mut io = IoHandler::new(); + io.add_method("rpc_modify_error", |_| { + Err(Error::from(NetworkError::Modify { + id: "1".to_string(), + iface: "wlan0".to_string(), + })) + }); + test::Rpc::from(io) + }; + + assert_eq!( + rpc.request("rpc_modify_error", &()), + r#"{ + "code": -32033, + "message": "Failed to set new password for network 1 on wlan0" +}"# + ); + } + + // test to ensure correct Ip error response + #[test] + fn rpc_ip_error() { + let rpc = { + let mut io = IoHandler::new(); + io.add_method("rpc_ip_error", |_| { + Err(Error::from(NetworkError::Ip { + iface: "wlan0".to_string(), + })) + }); + test::Rpc::from(io) + }; + + assert_eq!( + rpc.request("rpc_ip_error", &()), + r#"{ + "code": -32007, + "message": "No IP address found for wlan0" +}"# + ); + } + + // test to ensure correct Reassociate error response + #[test] + fn rpc_reassociate_error() { + let rpc = { + let mut io = IoHandler::new(); + io.add_method("rpc_reassociate_error", |_| { + Err(Error::from(NetworkError::Reassociate { + iface: "wlan0".to_string(), + })) + }); + test::Rpc::from(io) + }; + + assert_eq!( + rpc.request("rpc_reassociate_error", &()), + r#"{ + "code": -32008, + "message": "Failed to reassociate with WiFi network for wlan0" +}"# + ); + } + + // test to ensure correct Reconfigure error response + #[test] + fn rpc_reconfigure_error() { + let rpc = { + let mut io = IoHandler::new(); + io.add_method("rpc_reconfigure_error", |_| { + Err(Error::from(NetworkError::Reconfigure)) + }); + test::Rpc::from(io) + }; + + assert_eq!( + rpc.request("rpc_reconfigure_error", &()), + r#"{ + "code": -32030, + "message": "Failed to force reread of wpa_supplicant configuration file" +}"# + ); + } + + // test to ensure correct Connect error response + #[test] + fn rpc_connect_error() { + let rpc = { + let mut io = IoHandler::new(); + io.add_method("rpc_connect_error", |_| { + Err(Error::from(NetworkError::Connect { + id: "0".to_string(), + iface: "wlan0".to_string(), + })) + }); + test::Rpc::from(io) + }; + + assert_eq!( + rpc.request("rpc_connect_error", &()), + r#"{ + "code": -32027, + "message": "Failed to connect to network 0 for wlan0" +}"# + ); + } + + // test to ensure correct Reconnect error response + #[test] + fn rpc_reconnect_error() { + let rpc = { + let mut io = IoHandler::new(); + io.add_method("rpc_reconnect_error", |_| { + Err(Error::from(NetworkError::Reconnect { + iface: "wlan0".to_string(), + })) + }); + test::Rpc::from(io) + }; + + assert_eq!( + rpc.request("rpc_reconnect_error", &()), + r#"{ + "code": -32009, + "message": "Failed to reconnect with WiFi network for wlan0" +}"# + ); + } + + // test to ensure correct Regex error response + #[test] + fn rpc_regex_error() { + let rpc = { + let mut io = IoHandler::new(); + io.add_method("rpc_regex_error", |_| { + let source = regex::Error::Syntax("oh no!".to_string()); + Err(Error::from(NetworkError::Regex { source })) + }); + test::Rpc::from(io) + }; + + assert_eq!( + rpc.request("rpc_regex_error", &()), + r#"{ + "code": -32010, + "message": "Regex command error: oh no!" +}"# + ); + } + + // test to ensure correct Delete error response + #[test] + fn rpc_delete_error() { + let rpc = { + let mut io = IoHandler::new(); + io.add_method("rpc_delete_error", |_| { + Err(Error::from(NetworkError::Delete { + id: "0".to_string(), + iface: "wlan0".to_string(), + })) + }); + test::Rpc::from(io) + }; + + assert_eq!( + rpc.request("rpc_delete_error", &()), + r#"{ + "code": -32028, + "message": "Failed to delete network 0 for wlan0" +}"# + ); + } + + // test to ensure correct WlanState error response + #[test] + fn rpc_wlanstate_error() { + let rpc = { + let mut io = IoHandler::new(); + io.add_method("rpc_wlanstate_error", |_| { + let source = IoError::new(ErrorKind::PermissionDenied, "oh no!"); + Err(Error::from(NetworkError::WlanState { source })) + }); + test::Rpc::from(io) + }; + + assert_eq!( + rpc.request("rpc_wlanstate_error", &()), + r#"{ + "code": -32011, + "message": "Failed to retrieve state of wlan0 service: oh no!" +}"# + ); + } + + // test to ensure correct WlanOperstate error response + #[test] + fn rpc_wlanoperstate_error() { + let rpc = { + let mut io = IoHandler::new(); + io.add_method("rpc_wlanoperstate_error", |_| { + let source = IoError::new(ErrorKind::PermissionDenied, "oh no!"); + Err(Error::from(NetworkError::WlanOperstate { source })) + }); + test::Rpc::from(io) + }; + + assert_eq!( + rpc.request("rpc_wlanoperstate_error", &()), + r#"{ + "code": -32021, + "message": "Failed to retrieve connection state of wlan0 interface: oh no!" +}"# + ); + } + + // test to ensure correct Save error response + #[test] + fn rpc_save_error() { + let rpc = { + let mut io = IoHandler::new(); + io.add_method("rpc_save_error", |_| Err(Error::from(NetworkError::Save))); + test::Rpc::from(io) + }; + + assert_eq!( + rpc.request("rpc_save_error", &()), + r#"{ + "code": -32031, + "message": "Failed to save configuration changes to file" +}"# + ); + } + + // test to ensure correct WpaCtrlOpen error response + #[test] + fn rpc_wpactrlopen_error() { + let rpc = { + let mut io = IoHandler::new(); + io.add_method("rpc_wpactrlopen_error", |_| { + let fail_err = failure::err_msg("Permission denied (os error 13)").compat(); + let source = Box::new(fail_err); + Err(Error::from(NetworkError::WpaCtrlOpen { source })) + }); + test::Rpc::from(io) + }; + + assert_eq!( + rpc.request("rpc_wpactrlopen_error", &()), + r#"{ + "code": -32013, + "message": "Failed to open control interface for wpasupplicant: Permission denied (os error 13)" +}"# + ); + } + + // test to ensure correct WpaCtrlRequest error response + #[test] + fn rpc_wpactrlrequest_error() { + let rpc = { + let mut io = IoHandler::new(); + io.add_method("rpc_wpactrlrequest_error", |_| { + let fail_err = failure::err_msg("oh no!").compat(); + let source = Box::new(fail_err); + Err(Error::from(NetworkError::WpaCtrlRequest { source })) + }); + test::Rpc::from(io) + }; + + assert_eq!( + rpc.request("rpc_wpactrlrequest_error", &()), + r#"{ + "code": -32014, + "message": "WPA supplicant request failed: oh no!" +}"# + ); + } +} diff --git a/peach-network/src/main.rs b/peach-network/src/main.rs new file mode 100644 index 0000000..b54cc87 --- /dev/null +++ b/peach-network/src/main.rs @@ -0,0 +1,14 @@ +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); + } +} diff --git a/peach-network/src/network.rs b/peach-network/src/network.rs new file mode 100644 index 0000000..e52c1b2 --- /dev/null +++ b/peach-network/src/network.rs @@ -0,0 +1,838 @@ +//! Retrieve network data and modify interface state. +//! +//! This module contains the core logic of the `peach-network` microservice and +//! provides convenience wrappers for a range of `wpasupplicant` commands, +//! many of which are ordinarily executed using `wpa_cli` (a WPA command line +//! client). +//! +//! The `wpactrl` crate ([docs](https://docs.rs/wpactrl/0.3.1/wpactrl/)) +//! is used to interact with the `wpasupplicant` process. +//! +//! 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`. +//! +use std::{ + fs::OpenOptions, + io::prelude::*, + process::{Command, Stdio}, + result::Result, + 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 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)] +pub struct Scan { + pub frequency: String, + pub protocol: String, + pub signal_level: String, + pub ssid: String, +} + +/// Status data for a network interface. +#[derive(Debug, Serialize)] +pub struct Status { + pub address: Option, + pub bssid: Option, + pub freq: Option, + pub group_cipher: Option, + pub id: Option, + pub ip_address: Option, + pub key_mgmt: Option, + pub mode: Option, + pub pairwise_cipher: Option, + pub ssid: Option, + pub wpa_state: Option, +} + +impl Status { + fn new() -> Status { + Status { + address: None, + bssid: None, + freq: None, + group_cipher: None, + id: None, + ip_address: None, + key_mgmt: None, + mode: None, + pairwise_cipher: None, + ssid: None, + wpa_state: None, + } + } +} + +/// Received and transmitted network traffic (bytes). +#[derive(Debug, Serialize)] +pub struct Traffic { + pub received: u64, + pub transmitted: u64, +} + +/// SSID and password for a wireless access point. +#[derive(Debug, Deserialize)] +pub struct WiFi { + pub ssid: String, + pub pass: String, +} + +/* GET - Methods for retrieving data */ + +/// Retrieve list of available wireless access points for a given network +/// interface. +/// +/// # Arguments +/// +/// * `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, 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 scan = Vec::new(); + for network in networks.lines() { + let v: Vec<&str> = network.split('\t').collect(); + let len = v.len(); + if len > 1 { + let frequency = v[1].to_string(); + let signal_level = v[2].to_string(); + let flags = v[3].to_string(); + let flags_vec: Vec<&str> = flags.split("][").collect(); + let mut protocol = String::new(); + // an open access point (no auth) will only have [ESS] in flags + // 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()); + } + let ssid = v[4].to_string(); + let response = Scan { + frequency, + protocol, + signal_level, + ssid, + }; + scan.push(response) + } + } + + if scan.is_empty() { + Ok(None) + } else { + let results = serde_json::to_string(&scan).context(SerdeSerialize)?; + Ok(Some(results)) + } +} + +/// Retrieve network identifier for the network specified by a given interface +/// and SSID. +/// +/// # Arguments +/// +/// * `iface` - A string slice holding the name of a wireless network interface +/// * `ssid` - A string slice holding the SSID of a wireless access point +/// +/// If the identifier corresponding to the given interface and SSID is +/// 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. +/// +pub fn id(iface: &str, ssid: &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 networks = wpa.request("LIST_NETWORKS").context(WpaCtrlRequest)?; + let mut id = Vec::new(); + for network in networks.lines() { + let v: Vec<&str> = network.split('\t').collect(); + let len = v.len(); + if len > 1 && v[1] == ssid { + id.push(v[0].trim()) + } + } + + if id.is_empty() { + Ok(None) + } else { + let network_id: String = id[0].to_string(); + Ok(Some(network_id)) + } +} + +/// Retrieve IP address for a given interface. +/// +/// # Arguments +/// +/// * `iface` - A string slice holding the name of a wireless network interface +/// +/// If the given interface is found in the list of available interfaces, +/// 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. +/// +pub fn ip(iface: &str) -> Result, NetworkError> { + let net_if: String = iface.to_string(); + let ifaces = get_if_addrs::get_if_addrs().context(NoIp { iface: net_if })?; + let ip = ifaces + .iter() + .find(|&i| i.name == iface) + .map(|iface| iface.ip().to_string()); + + Ok(ip) +} + +/// Retrieve average signal strength (dBm) for the network associated with +/// a given interface. +/// +/// # Arguments +/// +/// * `iface` - A string slice holding the name of a wireless network interface +/// +/// If the signal strength is found for the given interface after polling, +/// an `Ok` `Result` type is returned containing `Some(String)` - where `String` +/// 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. +/// +pub fn rssi(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 status = wpa.request("SIGNAL_POLL").context(WpaCtrlRequest)?; + let rssi = utils::regex_finder(r"RSSI=(.*)\n", &status)?; + + if rssi.is_none() { + let iface = iface.to_string(); + Err(NetworkError::Rssi { iface }) + } else { + Ok(rssi) + } +} + +/// Retrieve average signal strength (%) for the network associated with +/// a given interface. +/// +/// # Arguments +/// +/// * `iface` - A string slice holding the name of a wireless network interface +/// +/// If the signal strength is found for the given interface after polling, +/// an `Ok` `Result` type is returned containing `Some(String)` - where `String` +/// 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. +/// +pub fn rssi_percent(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 status = wpa.request("SIGNAL_POLL").context(WpaCtrlRequest)?; + 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::().context(ParseString)?; + // perform rssi (dBm) to quality (%) conversion + let quality_percent = 2 * (rssi_parsed + 100); + // convert signal quality integer to string + let quality = quality_percent.to_string(); + + Ok(Some(quality)) + } + None => { + let iface = iface.to_string(); + Err(NetworkError::RssiPercent { iface }) + } + } +} + +/// Retrieve list of all access points with credentials saved in the +/// wpasupplicant configuration file. +/// +/// 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, NetworkError> { + let mut wpa = wpactrl::WpaCtrl::new().open().context(WpaCtrlOpen)?; + let networks = wpa.request("LIST_NETWORKS").context(WpaCtrlRequest)?; + 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) + } + } + + if ssids.is_empty() { + Ok(None) + } else { + let results = serde_json::to_string(&ssids).context(SerdeSerialize)?; + Ok(Some(results)) + } +} + +/// Retrieve SSID for the network associated with a given interface. +/// +/// # Arguments +/// +/// * `iface` - A string slice holding the name of a wireless network interface +/// +/// If the SSID is found in the status output for the given interface, +/// 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. +/// +pub fn ssid(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 status = wpa.request("STATUS").context(WpaCtrlRequest)?; + + // pass the regex pattern and status output to the regex finder + let ssid = utils::regex_finder(r"\nssid=(.*)\n", &status)?; + + Ok(ssid) +} + +/// Retrieve state for a given interface. +/// +/// # Arguments +/// +/// * `iface` - A string slice holding the name of a wireless network interface +/// +/// If the state is found for the given interface, an `Ok` `Result` type is +/// 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. +/// +pub fn state(iface: &str) -> Result, NetworkError> { + // construct the interface operstate path + let iface_path: String = format!("/sys/class/net/{}/operstate", iface); + // execute the cat command and save output, catching any errors + let output = Command::new("cat") + .arg(iface_path) + .output() + .context(NoState { iface })?; + if !output.stdout.is_empty() { + // unwrap the command result and convert to String + let mut state = String::from_utf8(output.stdout).unwrap(); + // remove trailing newline character + let len = state.len(); + state.truncate(len - 1); + return Ok(Some(state)); + } + + Ok(None) +} + +/// Retrieve status for a given interface. +/// +/// # Arguments +/// +/// * `iface` - A string slice holding the name of a wireless network interface +/// +/// If the status is found for the given interface, an `Ok` `Result` type is +/// 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. +/// +pub fn status(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 wpa_status = wpa.request("STATUS").context(WpaCtrlRequest)?; + + // pass the regex pattern and status output to the regex finder + let state = utils::regex_finder(r"wpa_state=(.*)\n", &wpa_status)?; + // regex_finder returns an Option type, unwrap or replace None with ERROR + let wpa_state = state.unwrap_or_else(|| "ERROR".to_string()); + + // create new Status object (all fields are None type by default) + let mut status = Status::new(); + // match on wpa_state and set Status fields accordingly + match wpa_state.as_ref() { + "ERROR" => status.wpa_state = Some("ERROR".to_string()), + "UNKNOWN" => status.wpa_state = Some("UNKNOWN".to_string()), + "INTERFACE_DISABLED" => status.wpa_state = Some("DISABLED".to_string()), + "INACTIVE" => status.wpa_state = Some("INACTIVE".to_string()), + "DISCONNECTED" => status.wpa_state = Some("DISCONNECTED".to_string()), + "SCANNING" => status.wpa_state = Some("SCANNING".to_string()), + "ASSOCIATING" => status.wpa_state = Some("ASSOCIATING".to_string()), + "ASSOCIATED" => status.wpa_state = Some("ASSOCIATED".to_string()), + "AUTHENTICATING" => status.wpa_state = Some("AUTHENTICATING".to_string()), + "4WAY_HANDSHAKE" => status.wpa_state = Some("4WAY_HANDSHAKE".to_string()), + "GROUP_HANDSHAKE" => status.wpa_state = Some("GROUP_HANDSHAKE".to_string()), + // retrieve additional status fields only if wpa_state is COMPLETED + "COMPLETED" => { + status.address = utils::regex_finder(r"\naddress=(.*)\n", &wpa_status)?; + status.bssid = utils::regex_finder(r"\nbssid=(.*)\n", &wpa_status)?; + status.freq = utils::regex_finder(r"\nfreq=(.*)\n", &wpa_status)?; + status.group_cipher = utils::regex_finder(r"\ngroup_cipher=(.*)\n", &wpa_status)?; + status.id = utils::regex_finder(r"\nid=(.*)\n", &wpa_status)?; + status.ip_address = utils::regex_finder(r"\nip_address=(.*)\n", &wpa_status)?; + status.key_mgmt = utils::regex_finder(r"\nkey_mgmt=(.*)\n", &wpa_status)?; + status.mode = utils::regex_finder(r"\nmode=(.*)\n", &wpa_status)?; + status.pairwise_cipher = utils::regex_finder(r"\npairwise_cipher=(.*)\n", &wpa_status)?; + status.ssid = utils::regex_finder(r"\nssid=(.*)\n", &wpa_status)?; + status.wpa_state = utils::regex_finder(r"\nwpa_state=(.*)\n", &wpa_status)?; + } + _ => (), + } + + Ok(Some(status)) +} + +/// Retrieve network traffic statistics for a given interface. +/// +/// # Arguments +/// +/// * `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, NetworkError> { + let network = network::read().context(NoTraffic { iface })?; + // iterate through interfaces returned in network data + for (interface, traffic) in network.interfaces { + if interface == iface { + let received = traffic.received; + let transmitted = traffic.transmitted; + let traffic = Traffic { + received, + transmitted, + }; + // TODO: add test for SerdeSerialize error + let t = serde_json::to_string(&traffic).context(SerdeSerialize)?; + return Ok(Some(t)); + } + } + + Ok(None) +} + +/* SET - Methods for modifying state */ + +/// Activate wireless access point. +/// +/// 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 + Command::new("sudo") + .arg("/usr/bin/systemctl") + .arg("start") + .arg("wpa_supplicant@ap0.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)?; + + Ok(()) +} + +/// Add network credentials for a given wireless access point. +/// +/// # Arguments +/// +/// * `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> { + // generate configuration based on provided ssid & password + let output = Command::new("wpa_passphrase") + .arg(&wifi.ssid) + .arg(&wifi.pass) + .stdout(Stdio::piped()) + .output() + .context(GenWpaPassphrase { ssid: &wifi.ssid })?; + + // 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 + if output.status.success() { + // open file in append mode + let file = OpenOptions::new() + .append(true) + .open("/etc/wpa_supplicant/wpa_supplicant-wlan0.conf"); + + 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(), + err_msg: err_msg.to_string(), + }) + } +} + +/// Deploy the access point if the `wlan0` interface is `up` without an active +/// connection. +/// +/// The status of the `wlan0` service and the state of the `wlan0` 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)?; + + // returns the current state of the wlan0 interface + let iface_state = state("wlan0")?; + + // returns down if the interface is not currently connected to an ap + let wlan0_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()? + } + + Ok(()) +} + +/// Connect with an access point for a given network identifier and interface. +/// Results in connections with other access points being disabled. +/// +/// # Arguments +/// +/// * `id` - A string slice holding the network identifier of an access point +/// * `iface` - A string slice holding the name of a wireless network interface +/// +/// 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. +/// +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 select = format!("SELECT {}", id); + wpa.request(&select).context(WpaCtrlRequest)?; + Ok(()) +} + +/// Delete network credentials for a given network identifier and interface. +/// +/// # Arguments +/// +/// * `id` - A string slice holding the network identifier of an access point +/// * `iface` - A string slice holding the name of a wireless network interface +/// +/// 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. +/// +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 remove = format!("REMOVE_NETWORK {}", id); + wpa.request(&remove).context(WpaCtrlRequest)?; + Ok(()) +} + +/// Disable network connection for a given network identifier and interface. +/// +/// # Arguments +/// +/// * `id` - A string slice holding the network identifier of an access point +/// * `iface` - A string slice holding the name of a wireless network interface +/// +/// 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. +/// +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 disable = format!("DISABLE_NETWORK {}", id); + wpa.request(&disable).context(WpaCtrlRequest)?; + Ok(()) +} + +/// Disconnect network connection for a given wireless interface. +/// +/// # Arguments +/// +/// * `iface` - A string slice holding the name of a wireless network interface +/// +/// 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. +/// +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 disconnect = "DISCONNECT".to_string(); + wpa.request(&disconnect).context(WpaCtrlRequest)?; + Ok(()) +} + +/// Modify password for a given network identifier and interface. +/// +/// # Arguments +/// +/// * `id` - A string slice holding the network identifier of an access point +/// * `iface` - A string slice holding the name of a wireless network interface +/// * `pass` - A string slice holding the password for a wireless access point +/// +/// 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. +/// +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 new_pass = format!("NEW_PASSWORD {} {}", id, pass); + wpa.request(&new_pass).context(WpaCtrlRequest)?; + Ok(()) +} + +/// Reassociate with an access point for a given wireless interface. +/// +/// # Arguments +/// +/// * `iface` - A string slice holding the name of a wireless network interface +/// +/// 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. +/// +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)?; + Ok(()) +} + +/// Reconfigure `wpa_supplicant` by forcing a reread of the configuration file. +/// +/// 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. +/// +pub fn reconfigure() -> Result<(), NetworkError> { + let mut wpa = wpactrl::WpaCtrl::new().open().context(WpaCtrlOpen)?; + wpa.request("RECONFIGURE").context(WpaCtrlRequest)?; + Ok(()) +} + +/// Reconnect network connection for a given wireless interface. +/// +/// # Arguments +/// +/// * `iface` - A string slice holding the name of a wireless network interface +/// +/// 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. +/// +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)?; + Ok(()) +} + +/// Save configuration updates to the `wpa_supplicant` configuration file. +/// +/// If wireless network configuration updates are successfully save 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. +/// +pub fn save() -> Result<(), NetworkError> { + let mut wpa = wpactrl::WpaCtrl::new().open().context(WpaCtrlOpen)?; + wpa.request("SAVE_CONFIG").context(WpaCtrlRequest)?; + Ok(()) +} diff --git a/peach-network/src/utils.rs b/peach-network/src/utils.rs new file mode 100644 index 0000000..e82b7aa --- /dev/null +++ b/peach-network/src/utils.rs @@ -0,0 +1,19 @@ +use regex::Regex; +use snafu::ResultExt; + +use crate::error::*; + +/// Return matches for a given Regex pattern and text +/// +/// # Arguments +/// +/// * `pattern` - A string slice containing a regular expression +/// * `text` - A string slice containing the text to be matched on +/// +pub fn regex_finder(pattern: &str, text: &str) -> Result, NetworkError> { + let re = Regex::new(pattern).context(Regex)?; + let caps = re.captures(text); + let result = caps.map(|caps| caps[1].to_string()); + + Ok(result) +} diff --git a/peach-probe/.cargo/config b/peach-probe/.cargo/config new file mode 100644 index 0000000..4b6f460 --- /dev/null +++ b/peach-probe/.cargo/config @@ -0,0 +1,4 @@ +[target.aarch64-unknown-linux-gnu] +linker = "aarch64-linux-gnu-gcc" +objcopy = { path ="aarch64-linux-gnu-objcopy" } +strip = { path ="aarch64-linux-gnu-strip" } diff --git a/peach-probe/.gitignore b/peach-probe/.gitignore new file mode 100644 index 0000000..09d9410 --- /dev/null +++ b/peach-probe/.gitignore @@ -0,0 +1,6 @@ +/target +deploy.sh +build.sh +crosscompile.sh +run.sh +/tmp \ No newline at end of file diff --git a/peach-probe/Cargo.toml b/peach-probe/Cargo.toml new file mode 100644 index 0000000..eb46c9b --- /dev/null +++ b/peach-probe/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "peach-probe" +version = "0.1.2" +authors = ["Andrew Reid "] +edition = "2018" +description = "Diagnostic tool for probing PeachCloud microservices to evaluate their state and ensure correct API responses" +homepage = "https://opencollective.com/peachcloud" +repository = "https://github.com/peachcloud/peach-probe" +readme = "README.md" +license = "AGPL-3.0-only" +publish = false + +[package.metadata.deb] +depends = "$auto" +extended-description = """\ +peach-probe is a diagnostic tool for probing PeachCloud microservices to evaluate their state" \ +and ensure correct API responses" \ +""" +assets = [ + ["target/release/peach-probe", "usr/bin/", "755"], + ["README.md", "usr/share/doc/peach-probe/README", "644"], +] + +[dependencies] +structopt = "0.3.13" +env_logger = "0.6.1" +log = "0.4.0" +jsonrpc-core = "14.2" +jsonrpc-client-http = "0.5" +jsonrpc-client-core = "0.5" +jsonrpc-test = "14.2" +snafu = "0.4.1" +serde = "1.0" +serde_json = "1.0" +serde_derive = "1.0" +peach-lib = { git = "https://github.com/peachcloud/peach-lib", branch = "main" } +clap = "2.33.3" +const_format = "0.2.10" +regex = "1" + + + diff --git a/peach-probe/README.md b/peach-probe/README.md new file mode 100644 index 0000000..9078f7a --- /dev/null +++ b/peach-probe/README.md @@ -0,0 +1,51 @@ +# peach-probe + +![Generic badge](https://img.shields.io/badge/version-0.1.2-.svg) + +Probe PeachCloud microservices to evaluate their state and ensure correct API responses. + +`peach-probe` is a CLI tool for contract testing of the public API's exposed by PeachCloud microservices. +It is composed of JSON-RPC clients which make calls to the methods of their respective servers and +generates a report with the results. + +`peach-probe` also makes use of `systemctl status` commands to test the status of all PeachCloud microservices. + +This utility is intended to provide a rapid means of testing a deployed PeachCloud system and allow informed trouble-shooting in the case of errors. + +## Installation + +After adding releases.peachcloud.org to /etc/sources.list, as described [here](https://github.com/peachcloud/peach-vps/blob/main/README.md), +peach-probe can be installed by running: +`sudo apt-get install peach-probe` + +## Usage + +```bash +USAGE: + peach-probe [FLAGS] [services]... + +FLAGS: + -h, --help Prints help information + -V, --version Prints version information + -v, --verbose prints successful endpoint calls in addition to errors + +ARGS: + ... [possible values: peach_oled, peach_Network, peach_stats, peach_menu, peach_web, + peach_Buttons, peach_monitor] +``` + +If no service arguments are provided, peach-probe will query all services. + +## Custom Port Numbers + +If peach-microservices are running on ports other than the default ports, +this can be specified using environmental variables as documented [here](https://github.com/peachcloud/peach-lib/blob/main/README.md). + +## Todo + + - On detecting certain errors, suggest possible fixes + - Finish querying of all peach-network endpoints + +## Licensing + +AGPL-3.0 diff --git a/peach-probe/src/error.rs b/peach-probe/src/error.rs new file mode 100644 index 0000000..f8889e9 --- /dev/null +++ b/peach-probe/src/error.rs @@ -0,0 +1,32 @@ +#[derive(Debug)] +pub enum ProbeError { + GetServiceVersionRegexError(regex::Error), + GetServiceVersionRegexMatchError, + GetServiceVersionParseError(core::str::Utf8Error), + GetServiceLogParseError(std::string::FromUtf8Error), + GetServiceVersionAptError(std::io::Error), +} + +impl From for ProbeError { + fn from(err: regex::Error) -> ProbeError { + ProbeError::GetServiceVersionRegexError(err) + } +} + +impl From for ProbeError { + fn from(err: core::str::Utf8Error) -> ProbeError { + ProbeError::GetServiceVersionParseError(err) + } +} + +impl From for ProbeError { + fn from(err: std::string::FromUtf8Error) -> ProbeError { + ProbeError::GetServiceLogParseError(err) + } +} + +impl From for ProbeError { + fn from(err: std::io::Error) -> ProbeError { + ProbeError::GetServiceVersionAptError(err) + } +} diff --git a/peach-probe/src/main.rs b/peach-probe/src/main.rs new file mode 100644 index 0000000..6f01ffe --- /dev/null +++ b/peach-probe/src/main.rs @@ -0,0 +1,133 @@ +use log::info; + +use clap::arg_enum; +use structopt::StructOpt; + +mod error; +mod probe; +mod vars; + +use crate::probe::PeachProbe; + +#[derive(StructOpt, Debug)] +#[structopt( + name = "peach-probe", + about = "a CLI tool for contract testing of the public API's exposed by PeachCloud microservices" +)] +struct Opt { + #[structopt(short, long)] + verbose: bool, + #[structopt(possible_values = &Microservice::variants(), case_insensitive = true)] + services: Vec, +} + +arg_enum! { + #[derive(Debug)] + #[allow(non_camel_case_types)] + #[allow(clippy::enum_variant_names)] + pub enum Microservice { + Peach_Oled, + Peach_Network, + Peach_Stats, + Peach_Menu, + Peach_Web, + Peach_Buttons + } +} + +impl Microservice { + /// get_package_name converts the microservice enum to a string representation + /// which can be used by systemctl and other tools which reference the package by name + /// we can't use std::fmt::Display because this is already used by arg_enum! + pub fn get_package_name(service: &Microservice) -> String { + let s = match service { + Microservice::Peach_Oled => "peach-oled", + Microservice::Peach_Network => "peach-network", + Microservice::Peach_Stats => "peach-stats", + Microservice::Peach_Menu => "peach-menu", + Microservice::Peach_Web => "peach-web", + Microservice::Peach_Buttons => "peach-buttons", + }; + s.to_string() + } +} + +fn main() { + // initialize the logger + env_logger::init(); + + // parse cli arguments + let opt = Opt::from_args(); + + // debugging what was parsed + info!("probing services: {:?}", opt.services); + if opt.verbose { + info!("using verbose mode") + } + + let services; + // if not arguments were provided, then we probe all services + if opt.services.is_empty() { + services = vec![ + Microservice::Peach_Network, + Microservice::Peach_Oled, + Microservice::Peach_Stats, + Microservice::Peach_Web, + Microservice::Peach_Buttons, + Microservice::Peach_Menu, + ] + } else { + services = opt.services; + } + + // instantiate the probe + let mut probe: PeachProbe = PeachProbe::new(opt.verbose); + + // iterate through services and run probe tests on them + for service in services { + probe.probe_service(service); + } + + // final report of how many microservices returned successes and failures + println!("[ generating report ]"); + for result in probe.results { + let num_failures = result.failures.len(); + let report; + // if service is running according to systemctl status + if result.is_running { + if num_failures == 0 { + report = format!( + "- {} [version: {}] is online.", + result.microservice, result.version + ); + println!("{}", report); + } + // even if its running, some endpoints could still return errors + else { + report = format!( + "- {} [version: {}] is online but {} endpoints returned errors: {:?}", + result.microservice, result.version, num_failures, result.failures + ); + eprintln!("{}", report); + } + } + // if service is not running according to systemctl status, print the service log + else { + match result.service_log { + Some(service_log) => { + report = format!( + "- {} [version: {}] is offline, with log:\n {}", + result.microservice, result.version, service_log + ); + } + None => { + report = format!( + "- {} [version: {}] is offline, log not found", + result.microservice, result.version + ); + } + }; + eprintln!("{}", report); + } + } +} diff --git a/peach-probe/src/probe.rs b/peach-probe/src/probe.rs new file mode 100644 index 0000000..1cac739 --- /dev/null +++ b/peach-probe/src/probe.rs @@ -0,0 +1,355 @@ +use peach_lib::error::PeachError; +use peach_lib::network_client; +use peach_lib::oled_client; +use peach_lib::stats_client; + +use log::info; +use regex::Regex; +use std::process::Command; + +use crate::error::ProbeError; +use crate::vars::PEACH_LOGO; +use crate::Microservice; + +/// ProbeResult stores the results of probing a particular microservice +pub struct ProbeResult { + // string of the name of the service + pub microservice: String, + // string of the version of this service currently installed + pub version: String, + // vector of names of endpoints which had errors + pub failures: Vec, + // vector of names of endpoints which returned successfully + pub successes: Vec, + // bool which stores true if the service is running + pub is_running: bool, + // string which stores the tail of the log from journalctl -u service + pub service_log: Option, +} + +impl ProbeResult { + fn new(microservice: &str) -> ProbeResult { + ProbeResult { + microservice: microservice.to_string(), + failures: Vec::new(), + successes: Vec::new(), + is_running: false, + version: "".to_string(), + service_log: None, + } + } +} + +/// PeachProbe implements probes for all microservices and data structures +/// for storing the results of all probes +pub struct PeachProbe { + pub results: Vec, + pub verbose: bool, +} + +impl PeachProbe { + pub fn new(verbose: bool) -> PeachProbe { + PeachProbe { + results: Vec::new(), + verbose, + } + } + + /// probe any microservice, using systemctl status to see if the service is running + /// and testing endpoints for services which support this (peach-stats, peach-network, peach-oled) + /// for all other microservices this function just checks if the service is running + pub fn probe_service(&mut self, service: Microservice) { + // get package name from enum + let service_name = Microservice::get_package_name(&service); + println!("[ probing {} ]", service_name); + + // instantiate ProbeResult + let mut result = ProbeResult::new(&service_name); + + // get version of service + result.version = PeachProbe::get_service_version(&service_name); + + // check status of service + let status_result = PeachProbe::get_service_status(&service_name); + match status_result { + Ok(is_running) => { + result.is_running = is_running; + // if the service is not running, get the journalctl log of the service + if !is_running { + let log_result = PeachProbe::get_service_log(&service_name); + match log_result { + Ok(log) => { + result.service_log = Some(log); + } + Err(err) => { + eprintln!("error getting log for {}: {:#?}", service_name, err); + } + } + } + } + Err(err) => { + result.is_running = false; + eprintln!( + "error retrieving service status of {}: {:#?}", + service_name, err + ); + } + } + + // probe endpoints for the serivce if applicable + let result = match service { + Microservice::Peach_Stats => self.peach_stats(result), + Microservice::Peach_Oled => self.peach_oled(result), + Microservice::Peach_Network => self.peach_network(result), + _ => { + info!("probing endpoints not implemented for this service"); + result + } + }; + + // save result + self.results.push(result); + } + + /// helper function which gets the version of the microservice running using apt-get + fn get_service_version_result(service: &str) -> Result { + let output = Command::new("/usr/bin/apt") + .arg("list") + .arg(service) + .output()?; + let command_output = std::str::from_utf8(&output.stdout)?; + // use a regex to get the version number from the string + let re = Regex::new(r".*buster,now (\d+\.\d+\.\d+) arm64.*")?; + let cap = re.captures(command_output); + match cap { + Some(c) => { + let version = &c[1]; + Ok(version.to_string()) + } + None => Err(ProbeError::GetServiceVersionRegexMatchError), + } + } + + /// helper function to call systemctl status for service + pub fn get_service_status(service: &str) -> Result { + let output = Command::new("/usr/bin/systemctl") + .arg("status") + .arg(service) + .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) + } + + /// helper function to get last 2 lines of journalctl log for service + pub fn get_service_log(service: &str) -> Result { + let output = Command::new("/usr/bin/journalctl") + .arg("-u") + .arg(service) + .arg("-t") + .arg(service) + .arg("-n") + .arg("3") + .output()?; + let log_output = String::from_utf8(output.stdout)?; + Ok(log_output) + } + + /// helper function which gets the version of the microservice running using apt-get as a string + /// if there is an error getting the version, it returns the string "Unknown" + fn get_service_version(service: &str) -> String { + let version_result = PeachProbe::get_service_version_result(service); + match version_result { + Ok(version) => version, + Err(_) => "Unknown".to_string(), + } + } + + /// helper function for probing an endpoint on a peach microservice and collecting errors for a final report + fn probe_peach_endpoint( + &mut self, + endpoint_result: Result, + endpoint_name: &str, + result: &mut ProbeResult, + ) { + match endpoint_result { + Ok(_) => { + if self.verbose { + println!("++ {} endpoint is online", endpoint_name); + } + result.successes.push(endpoint_name.to_string()); + } + Err(e) => { + eprintln!("++ {} endpoint is offline", endpoint_name); + match e { + PeachError::JsonRpcHttp(e) => { + eprintln!("Returned JsonRpcHTTP error: {:#?}\n", e) + } + PeachError::JsonRpcCore(e) => { + eprintln!("Returned JsonRpcCore error: {:#?}\n", e) + } + PeachError::Serde(_) => eprintln!("Returned Serde Json serialization error\n"), + } + result.failures.push(endpoint_name.to_string()); + } + } + } + + /// helper function for probing an endpoint on a peach microservice which expects a particular JsonRPCCore Error + fn probe_assert_error_endpoint( + &mut self, + endpoint_result: Result, + endpoint_name: &str, + expected_error_code: i64, + result: &mut ProbeResult, + ) { + match endpoint_result { + Ok(_) => { + eprintln!("++ this endpoint should not return successfully during peach-probe, something is strange"); + result.failures.push(endpoint_name.to_string()); + } + Err(e) => { + match e { + PeachError::JsonRpcCore(e) => { + match e.kind() { + // this is the expected error, all other errors are unexpected + jsonrpc_client_core::ErrorKind::JsonRpcError(err) => { + if err.code.code() == expected_error_code { + if self.verbose { + println!("++ {} endpoint is online", endpoint_name); + } + result.successes.push(endpoint_name.to_string()); + } else { + eprintln!("++ {} endpoint is offline", endpoint_name); + eprintln!("Returned JsonRpcCore error with unexpected code or message: {:#?}\n", e); + result.failures.push(endpoint_name.to_string()); + } + } + _ => { + eprintln!("++ {} endpoint is offline", endpoint_name); + eprintln!("Returned unexpected JsonRpcCore error: {:#?}\n", e); + result.failures.push(endpoint_name.to_string()); + } + } + } + PeachError::JsonRpcHttp(e) => { + eprintln!("++ {} endpoint is offline", endpoint_name); + eprintln!("Returned JsonRpcHTTP error: {:#?}\n", e); + result.failures.push(endpoint_name.to_string()); + } + PeachError::Serde(_) => { + eprintln!("++ {} endpoint is offline", endpoint_name); + eprintln!("Returned Serde Json serialization error\n"); + result.failures.push(endpoint_name.to_string()); + } + } + } + } + } + + /// probes all endpoints on the peach-stats microservice + pub fn peach_stats(&mut self, mut result: ProbeResult) -> ProbeResult { + // probe endpoints + self.probe_peach_endpoint( + stats_client::cpu_stats_percent(), + "cpu_stats_percent", + &mut result, + ); + self.probe_peach_endpoint(stats_client::load_average(), "load_average", &mut result); + self.probe_peach_endpoint(stats_client::disk_usage(), "disk_usage", &mut result); + self.probe_peach_endpoint(stats_client::mem_stats(), "mem_stats", &mut result); + self.probe_peach_endpoint(stats_client::ping(), "ping", &mut result); + self.probe_peach_endpoint(stats_client::uptime(), "uptime", &mut result); + + // save result + result + } + + /// probes all endpoints on peach-network microservice + pub fn peach_network(&mut self, mut result: ProbeResult) -> ProbeResult { + // probe endpoints which should successfully return if online + self.probe_peach_endpoint( + network_client::add("peach-probe-test-ssid", "peach-probe-test-pass"), + "add", + &mut result, + ); + self.probe_peach_endpoint( + network_client::available_networks("wlan0"), + "available_networks", + &mut result, + ); + self.probe_peach_endpoint( + network_client::id("wlan0", "peach-probe-test-ssid"), + "id", + &mut result, + ); + self.probe_peach_endpoint(network_client::ip("wlan0"), "ip", &mut result); + self.probe_peach_endpoint(network_client::ssid("wlan0"), "ssid", &mut result); + self.probe_peach_endpoint(network_client::ping(), "ping", &mut result); + self.probe_peach_endpoint(network_client::reconfigure(), "reconfigure", &mut result); + self.probe_peach_endpoint( + network_client::saved_networks(), + "saved_networks", + &mut result, + ); + self.probe_peach_endpoint(network_client::state("wlan0"), "state", &mut result); + self.probe_peach_endpoint(network_client::traffic("wlan0"), "traffic", &mut result); + self.probe_peach_endpoint( + network_client::forget("wlan0", "peach-probe-test-ssid"), + "forget", + &mut result, + ); + + // if online, the following functions should return an error which we should catch and confirm + self.probe_assert_error_endpoint( + network_client::connect("peach-probe-test-ssid", "wlan0"), + "connect", + -32027, + &mut result, + ); + + // probe switching between ap and client mode + self.probe_peach_endpoint(network_client::activate_ap(), "activate_ap", &mut result); + self.probe_peach_endpoint( + network_client::activate_client(), + "activate_client", + &mut result, + ); + + // return result + result + } + + /// probes all endpoints on the peach-oled microservice + pub fn peach_oled(&mut self, mut result: ProbeResult) -> ProbeResult { + // probe endpoints + self.probe_peach_endpoint(oled_client::ping(), "ping", &mut result); + + // probe clear and flush + self.probe_peach_endpoint(oled_client::clear(), "clear", &mut result); + self.probe_peach_endpoint( + oled_client::write(0, 0, "peach-probe success", "6x8"), + "write", + &mut result, + ); + + // probe draw endpoint + let bytes = PEACH_LOGO.to_vec(); + self.probe_peach_endpoint( + oled_client::draw(bytes, 64, 64, 32, 10), + "draw", + &mut result, + ); + + // just clear at the end without flush so that state of peach-oled is not changed + self.probe_peach_endpoint(oled_client::flush(), "flush", &mut result); + + // test power off endpoint + self.probe_peach_endpoint(oled_client::power(false), "power-off", &mut result); + self.probe_peach_endpoint(oled_client::power(true), "power-on", &mut result); + + // return result + result + } +} diff --git a/peach-probe/src/vars.rs b/peach-probe/src/vars.rs new file mode 100644 index 0000000..8606cdc --- /dev/null +++ b/peach-probe/src/vars.rs @@ -0,0 +1,21 @@ +pub const PEACH_LOGO: [u8; 512] = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 0, 0, 0, 0, 0, + 0, 3, 248, 14, 0, 0, 7, 0, 0, 15, 252, 63, 128, 0, 31, 192, 0, 63, 254, 127, 192, 0, 63, 224, + 0, 127, 255, 127, 224, 0, 127, 240, 0, 63, 255, 255, 128, 0, 255, 240, 0, 31, 255, 255, 192, + 31, 255, 248, 0, 15, 252, 64, 112, 63, 255, 248, 0, 24, 240, 96, 24, 127, 255, 255, 192, 48, 0, + 48, 12, 127, 255, 255, 224, 96, 0, 24, 12, 255, 255, 255, 240, 64, 0, 8, 6, 255, 255, 255, 248, + 64, 0, 12, 2, 255, 255, 255, 252, 192, 0, 4, 2, 255, 227, 255, 252, 192, 0, 4, 2, 127, 128, + 255, 252, 128, 0, 4, 2, 63, 0, 127, 252, 128, 0, 6, 2, 126, 0, 63, 252, 128, 0, 6, 3, 252, 0, + 63, 248, 128, 0, 6, 6, 0, 0, 1, 240, 192, 0, 6, 12, 0, 0, 0, 192, 192, 0, 6, 8, 0, 0, 0, 96, + 64, 0, 4, 24, 0, 0, 0, 32, 64, 0, 4, 24, 0, 0, 0, 48, 96, 0, 4, 16, 0, 0, 0, 16, 32, 0, 4, 16, + 0, 0, 0, 16, 48, 0, 12, 24, 0, 0, 0, 16, 24, 0, 8, 56, 0, 0, 0, 16, 12, 0, 24, 104, 0, 0, 0, + 48, 7, 0, 0, 204, 0, 0, 0, 96, 1, 128, 3, 134, 0, 0, 0, 192, 0, 240, 6, 3, 128, 0, 1, 128, 0, + 63, 28, 1, 255, 255, 255, 0, 0, 3, 240, 0, 31, 255, 252, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +]; diff --git a/peach-stats/.cargo/config b/peach-stats/.cargo/config new file mode 100644 index 0000000..4b6f460 --- /dev/null +++ b/peach-stats/.cargo/config @@ -0,0 +1,4 @@ +[target.aarch64-unknown-linux-gnu] +linker = "aarch64-linux-gnu-gcc" +objcopy = { path ="aarch64-linux-gnu-objcopy" } +strip = { path ="aarch64-linux-gnu-strip" } diff --git a/peach-stats/.gitignore b/peach-stats/.gitignore new file mode 100644 index 0000000..53eaa21 --- /dev/null +++ b/peach-stats/.gitignore @@ -0,0 +1,2 @@ +/target +**/*.rs.bk diff --git a/peach-stats/.travis.yml b/peach-stats/.travis.yml new file mode 100644 index 0000000..f2536c6 --- /dev/null +++ b/peach-stats/.travis.yml @@ -0,0 +1,7 @@ +language: rust +rust: + - nightly +before_script: + - rustup component add clippy +script: + - cargo clippy -- -D warnings diff --git a/peach-stats/Cargo.toml b/peach-stats/Cargo.toml new file mode 100644 index 0000000..b603d20 --- /dev/null +++ b/peach-stats/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "peach-stats" +version = "0.1.3" +authors = ["Andrew Reid "] +edition = "2018" +description = "Query system statistics using JSON-RPC over HTTP. Provides a JSON-RPC wrapper around the probes and systemstat crates." +homepage = "https://opencollective.com/peachcloud" +repository = "https://github.com/peachcloud/peach-stats" +readme = "README.md" +license = "AGPL-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.6" +jsonrpc-core = "11" +jsonrpc-http-server = "11" +jsonrpc-test = "11" +log = "0.4" +probes = "0.3" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +snafu = "0.4" +systemstat = "0.1" diff --git a/peach-stats/README.md b/peach-stats/README.md new file mode 100644 index 0000000..f187a53 --- /dev/null +++ b/peach-stats/README.md @@ -0,0 +1,109 @@ +# 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-.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. + +### 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`, `nanos` | + +### Environment + +The JSON-RPC HTTP server address and port can be configured with the `PEACH_STATS_SERVER` environment variable: + +`export PEACH_STATS_SERVER=127.0.0.1:5000` + +When not set, the value defaults to `127.0.0.1:5113`. + +Logging is made available with `env_logger`: + +`export RUST_LOG=info` + +Other logging levels include `debug`, `warn` and `error`. + +### Setup + +Clone this repo: + +`git clone https://github.com/peachcloud/peach-stats.git` + +Move into the repo and compile a release build: + +`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,\"nanos\":0}","id":1}` + +### Licensing + +AGPL-3.0 + diff --git a/peach-stats/debian/peach-stats.service b/peach-stats/debian/peach-stats.service new file mode 100644 index 0000000..a32c506 --- /dev/null +++ b/peach-stats/debian/peach-stats.service @@ -0,0 +1,27 @@ +[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 diff --git a/peach-stats/src/error.rs b/peach-stats/src/error.rs new file mode 100644 index 0000000..12ce99d --- /dev/null +++ b/peach-stats/src/error.rs @@ -0,0 +1,67 @@ +use std::{error, io}; + +use jsonrpc_core::{types::error::Error, ErrorCode}; +use probes::ProbeError; +use serde_json::Error as SerdeError; +use snafu::Snafu; + +pub type BoxError = Box; + +#[derive(Debug, Snafu)] +#[snafu(visibility(pub(crate)))] +pub enum StatError { + #[snafu(display("Failed to retrieve CPU statistics: {}", source))] + ReadCpuStat { source: ProbeError }, + + #[snafu(display("Failed to retrieve disk usage statistics: {}", source))] + ReadDiskUsage { source: ProbeError }, + + #[snafu(display("Failed to retrieve load average statistics: {}", source))] + ReadLoadAvg { source: ProbeError }, + + #[snafu(display("Failed to retrieve memory statistics: {}", source))] + ReadMemStat { source: ProbeError }, + + #[snafu(display("Failed to retrieve system uptime: {}", source))] + ReadUptime { source: io::Error }, + + #[snafu(display("JSON serialization failed: {}", source))] + SerdeSerialize { source: SerdeError }, +} + +impl From for Error { + fn from(err: StatError) -> Self { + match &err { + StatError::ReadCpuStat { source } => Error { + code: ErrorCode::ServerError(-32001), + message: format!("Failed to retrieve CPU statistics: {}", source), + data: None, + }, + StatError::ReadDiskUsage { source } => Error { + code: ErrorCode::ServerError(-32001), + message: format!("Failed to retrieve disk usage statistics: {}", source), + data: None, + }, + StatError::ReadLoadAvg { source } => Error { + code: ErrorCode::ServerError(-32001), + message: format!("Failed to retrieve load average statistics: {}", source), + data: None, + }, + StatError::ReadMemStat { source } => Error { + code: ErrorCode::ServerError(-32001), + message: format!("Failed to retrieve memory statistics: {}", source), + data: None, + }, + StatError::ReadUptime { source } => Error { + code: ErrorCode::ServerError(-32001), + message: format!("Failed to retrieve system uptime: {}", source), + data: None, + }, + StatError::SerdeSerialize { source } => Error { + code: ErrorCode::ServerError(-32002), + message: format!("JSON serialization failed: {}", source), + data: None, + }, + } + } +} diff --git a/peach-stats/src/lib.rs b/peach-stats/src/lib.rs new file mode 100644 index 0000000..68eff8d --- /dev/null +++ b/peach-stats/src/lib.rs @@ -0,0 +1,102 @@ +mod error; +mod stats; +mod structs; + +use std::{env, result::Result}; + +use jsonrpc_core::{IoHandler, Value}; +use jsonrpc_http_server::{AccessControlAllowOrigin, DomainsValidation, ServerBuilder}; +#[allow(unused_imports)] +use jsonrpc_test as test; +use log::info; + +use crate::error::BoxError; + +pub fn run() -> Result<(), BoxError> { + info!("Starting up."); + + info!("Creating JSON-RPC I/O handler."); + let mut io = IoHandler::default(); + + io.add_method("cpu_stats", move |_| { + info!("Fetching CPU statistics."); + let stats = stats::cpu_stats()?; + + Ok(Value::String(stats)) + }); + + io.add_method("cpu_stats_percent", move |_| { + info!("Fetching CPU statistics as percentages."); + let stats = stats::cpu_stats_percent()?; + + Ok(Value::String(stats)) + }); + + io.add_method("disk_usage", move |_| { + info!("Fetching disk usage statistics."); + let disks = stats::disk_usage()?; + + Ok(Value::String(disks)) + }); + + io.add_method("load_average", move |_| { + info!("Fetching system load average statistics."); + let avg = stats::load_average()?; + + Ok(Value::String(avg)) + }); + + io.add_method("mem_stats", move |_| { + info!("Fetching current memory statistics."); + let mem = stats::mem_stats()?; + + Ok(Value::String(mem)) + }); + + io.add_method("ping", |_| Ok(Value::String("success".to_string()))); + + io.add_method("uptime", move |_| { + 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::*; + + // test to ensure correct success response + #[test] + fn rpc_success() { + let rpc = { + let mut io = IoHandler::new(); + io.add_method("rpc_success_response", |_| { + Ok(Value::String("success".into())) + }); + test::Rpc::from(io) + }; + + assert_eq!(rpc.request("rpc_success_response", &()), r#""success""#); + } +} diff --git a/peach-stats/src/main.rs b/peach-stats/src/main.rs new file mode 100644 index 0000000..55a11ec --- /dev/null +++ b/peach-stats/src/main.rs @@ -0,0 +1,14 @@ +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); + } +} diff --git a/peach-stats/src/stats.rs b/peach-stats/src/stats.rs new file mode 100644 index 0000000..a3c42bb --- /dev/null +++ b/peach-stats/src/stats.rs @@ -0,0 +1,87 @@ +use std::result::Result; + +use probes::{cpu, disk_usage, load, memory}; +use snafu::ResultExt; +use systemstat::{Platform, System}; + +use crate::error::*; +use crate::structs::{CpuStat, CpuStatPercentages, DiskUsage, LoadAverage, MemStat}; + +pub fn cpu_stats() -> Result { + let cpu_stats = cpu::proc::read().context(ReadCpuStat)?; + let s = cpu_stats.stat; + let cpu = CpuStat { + user: s.user, + system: s.system, + nice: s.nice, + idle: s.idle, + }; + let json_cpu = serde_json::to_string(&cpu).context(SerdeSerialize)?; + + Ok(json_cpu) +} + +pub fn cpu_stats_percent() -> Result { + let cpu_stats = cpu::proc::read().context(ReadCpuStat)?; + let s = cpu_stats.stat.in_percentages(); + let cpu = CpuStatPercentages { + user: s.user, + system: s.system, + nice: s.nice, + idle: s.idle, + }; + let json_cpu = serde_json::to_string(&cpu).context(SerdeSerialize)?; + + Ok(json_cpu) +} + +pub fn disk_usage() -> Result { + let disks = disk_usage::read().context(ReadDiskUsage)?; + let mut disk_usages = Vec::new(); + for d in disks { + let disk = DiskUsage { + filesystem: d.filesystem, + one_k_blocks: d.one_k_blocks, + one_k_blocks_used: d.one_k_blocks_used, + one_k_blocks_free: d.one_k_blocks_free, + used_percentage: d.used_percentage, + mountpoint: d.mountpoint, + }; + disk_usages.push(disk); + } + let json_disks = serde_json::to_string(&disk_usages).context(SerdeSerialize)?; + + Ok(json_disks) +} + +pub fn load_average() -> Result { + let l = load::read().context(ReadLoadAvg)?; + let load_avg = LoadAverage { + one: l.one, + five: l.five, + fifteen: l.fifteen, + }; + let json_load_avg = serde_json::to_string(&load_avg).context(SerdeSerialize)?; + + Ok(json_load_avg) +} + +pub fn mem_stats() -> Result { + let m = memory::read().context(ReadMemStat)?; + let mem = MemStat { + total: m.total(), + free: m.free(), + used: m.used(), + }; + let json_mem = serde_json::to_string(&mem).context(SerdeSerialize)?; + + Ok(json_mem) +} + +pub fn uptime() -> Result { + let sys = System::new(); + let uptime = sys.uptime().context(ReadUptime)?; + let json_uptime = serde_json::to_string(&uptime).context(SerdeSerialize)?; + + Ok(json_uptime) +} diff --git a/peach-stats/src/structs.rs b/peach-stats/src/structs.rs new file mode 100644 index 0000000..1fe12ee --- /dev/null +++ b/peach-stats/src/structs.rs @@ -0,0 +1,41 @@ +use serde::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, + 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, +} diff --git a/peach-web/.cargo/config b/peach-web/.cargo/config new file mode 100644 index 0000000..4b6f460 --- /dev/null +++ b/peach-web/.cargo/config @@ -0,0 +1,4 @@ +[target.aarch64-unknown-linux-gnu] +linker = "aarch64-linux-gnu-gcc" +objcopy = { path ="aarch64-linux-gnu-objcopy" } +strip = { path ="aarch64-linux-gnu-strip" } diff --git a/peach-web/.gitignore b/peach-web/.gitignore new file mode 100644 index 0000000..8f0bd8f --- /dev/null +++ b/peach-web/.gitignore @@ -0,0 +1,8 @@ +*.bak +static/icons/optimized/* +api_docs.md +js_docs.md +hashmap_notes +notes +target +**/*.rs.bk diff --git a/peach-web/.travis.yml b/peach-web/.travis.yml new file mode 100644 index 0000000..f2536c6 --- /dev/null +++ b/peach-web/.travis.yml @@ -0,0 +1,7 @@ +language: rust +rust: + - nightly +before_script: + - rustup component add clippy +script: + - cargo clippy -- -D warnings diff --git a/peach-web/Cargo.toml b/peach-web/Cargo.toml new file mode 100644 index 0000000..9cf3abc --- /dev/null +++ b/peach-web/Cargo.toml @@ -0,0 +1,56 @@ +[package] +name = "peach-web" +version = "0.4.11" +authors = ["Andrew Reid "] +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" +repository = "https://github.com/peachcloud/peach-web" +readme = "README.md" +license = "AGPL-3.0-only" +publish = false + +[package.metadata.deb] +depends = "apache2-utils" +extended-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.""" +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"], +] + +[badges] +travis-ci = { repository = "peachcloud/peach-web", branch = "master" } +maintenance = { status = "actively-developed" } + +[dependencies] +env_logger = "0.8" +log = "0.4" +nest = "1.0.0" +peach-lib = { path = "../peach-lib" } +percent-encoding = "2.1.0" +rocket = "0.4.6" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +snafu = "0.6" +tera = { version = "1.12.1", features = ["builtins"] } +websocket = "0.26" +regex = "1" +xdg = "2.2.0" +openssl = { version = "0.10", features = ["vendored"] } + +[dependencies.rocket_contrib] +version = "0.4.10" +default-features = false +features = ["json", "tera_templates"] diff --git a/peach-web/README.md b/peach-web/README.md new file mode 100644 index 0000000..e8956cf --- /dev/null +++ b/peach-web/README.md @@ -0,0 +1,151 @@ +# 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.6-.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. + +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 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. + +_Note: This is a work-in-progress._ + +### WEB ROUTES (`src/routes.rs`) + +| Endpoint | Method | Parameters | Description | +| --- | --- | --- | --- | +| `/` | GET | | Home | +| `/device` | GET | | Device status overview | +| `/device/reboot` | GET | | Reboot device | +| `/device/shutdown` | GET | | Shutdown device | +| `/login` | GET | | Login form | +| `/network` | GET | | Network status overview | +| `/network/ap/activate` | GET | | Activate WiFi access point mode | +| `/network/wifi` | GET | | List of networks | +| `/network/wifi?` | GET | `ssid` | Details of a single network | +| `/network/wifi/activate` | GET | | Activate WiFi client mode | +| `/network/wifi/add` | GET | `ssid` (optional - prepopulation value of SSID in form) | Add a WiFi network | +| `/network/wifi/add` | POST | `ssid` & `pass` | Submit form to add a WiFi network | +| `/network/wifi/connect` | POST | `ssid` | Connect to the given WiFi network | +| `/network/wifi/disconnect` | POST | `ssid` | Disconnect from currently associated WiFi network | +| `/network/wifi/forget` | POST | `ssid` | Submit form to forget a saved WiFi network | +| `/network/wifi/modify?` | GET | `ssid` | Form for updating a WiFi network password | +| `/network/wifi/modify` | POST | `ssid` & `pass` | Submit form to update a WiFi network password | +| `/network/wifi/usage` | GET | | Network data usage values and a form to update alert thresholds | +| `/network/wifi/usage` | POST | `rx_warn`, `rx_cut`, `tx_warn`, `tx_cut`, `rx_warn_flag`, `rx_cut_flag`, `tx_warn_flag`, `tx_cut_flag` | Submit form to update alert thresholds & set flags | +| `/network/wifi/usage/reset` | GET | | Reset the stored network data usage total to zero | +| `/network/dns` | GET | | View current DNS configurations | +| `/network/dns` | POST | | Modify DNS configurations | +| `/shutdown` | GET | | Shutdown menu | + +### JSON API (`src/json_api.rs`) + +All JSON API calls are prefixed by `/api/v1/`. This has been excluded from the table below to keep the table compact. + +| Endpoint | Method | Parameters | Description | +| --- | --- | --- | --- | +| `device/reboot` | POST | | Reboot device | +| `device/shutdown` | POST | | Shutdown device | +| `network/activate_ap` | POST | | Activate WiFi access point mode | +| `network/activate_client` | POST | | Activate WiFi client mode | +| `network/ip` | GET | | Returns IP address values for wlan0 & ap0 interfaces | +| `network/rssi` | GET | | Returns RSSI for connected WiFi network | +| `network/ssid` | GET | | Returns SSID for connected WiFi network | +| `network/state` | GET | | Returns state of wlan0 & ap0 interfaces | +| `network/status` | GET | | Returns status object for connected WiFi network | +| `network/wifi` | GET | | Returns scan results for in-range access-points | +| `network/wifi` | POST | `ssid` & `pass` | Submit SSID & password to create new WiFi connection | +| `network/wifi/connect` | POST | `ssid` | Submit SSID to connect to a given WiFi network | +| `network/wifi/disconnect` | POST | `ssid` | Disconnect from the currently associated WiFi network | +| `network/wifi/forget` | POST | `ssid` | Submit SSID to delete credentials for given WiFi network | +| `network/wifi/modify` | POST | `ssid` & `pass` | Submit SSID & password to update the credentials for given WiFi network | +| `/network/wifi/usage` | POST | `rx_warn`, `rx_cut`, `tx_warn`, `tx_cut`, `rx_warn_flag`, `rx_cut_flag`, `tx_warn_flag`, `tx_cut_flag` | Submit form to update alert thresholds & set flags | +| `/network/wifi/usage/reset` | POST | | Reset network data usage total | +| `ping` | GET | | Returns `pong!` if `peach-web` is running | +| `ping/network` | GET | | Returns `pong!` if `peach-network` microservice is running | +| `ping/oled` | GET | | Returns `pong!` if `peach-oled` microservice is running | +| `ping/stats` | GET | | Returns `pong!` if `peach-stats` microservice is running | +| `dns/configure` | POST | | Modify dns configurations | + +### Environment + +The web application deployment mode is configured with the `ROCKET_ENV` environment variable: + +`export ROCKET_ENV=stage` + +Other deployment modes are `dev` and `prod`. Read the [Rocket Environment Configurations docs](https://rocket.rs/v0.4/guide/configuration/#environment) for further information. + +The WebSocket server port can be configured with `PEACH_WEB_WS` environment variable: + +`export PEACH_WEB_WS=2333` + +When not set, the value defaults to `5115`. + +Logging is made available with `env_logger`: + +`export RUST_LOG=info` + +Other logging levels include `debug`, `warn` and `error`. + +### Setup + +Clone this repo: + +`git clone https://github.com/peachcloud/peach-web.git` + +Move into the repo and compile: + +`cd peach-web` +`cargo build --release` + +Run the tests: + +`cargo test` + +Run the binary: + +`./target/release/peach-web` + +_Note: Networking functionality requires peach-network microservice to be running._ + +### 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. + +Install `cargo-deb`: + +`cargo install cargo-deb` + +Move into the repo: + +`cd peach-web` + +Build the package: + +`cargo deb` + +The output will be written to `target/debian/peach-web_0.3.0_arm64.deb` (or similar). + +Install the package as follows: + +`sudo dpkg -i target/debian/peach-web_0.3.0_arm64.deb` + +The service will be automatically enabled and started. + +Uninstall the service: + +`sudo apt-get remove peach-web` + +Remove configuration files (not removed with `apt-get remove`): + +`sudo apt-get purge peach-web` + +### 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 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. + +### Licensing + +AGPL-3.0 diff --git a/peach-web/Rocket.toml b/peach-web/Rocket.toml new file mode 100644 index 0000000..d8fe73c --- /dev/null +++ b/peach-web/Rocket.toml @@ -0,0 +1,5 @@ +[development] +template_dir = "templates/" + +[production] +template_dir = "templates/" diff --git a/peach-web/debian/peach-web.service b/peach-web/debian/peach-web.service new file mode 100644 index 0000000..0891180 --- /dev/null +++ b/peach-web/debian/peach-web.service @@ -0,0 +1,17 @@ +[Unit] +Description=Rocket web application for serving the PeachCloud web interface. + +[Service] +User=peach-web +Group=www-data +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 + +[Install] +WantedBy=multi-user.target diff --git a/peach-web/debian/postinst b/peach-web/debian/postinst new file mode 100644 index 0000000..0a09b6a --- /dev/null +++ b/peach-web/debian/postinst @@ -0,0 +1,66 @@ +#!/bin/sh +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 + +# create nginx config +cat < /etc/nginx/sites-enabled/default +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; + } + + # 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 < /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 + +EOF + +# cargo deb automatically replaces this token below, see https://github.com/mmstick/cargo-deb/blob/master/systemd.md +#DEBHELPER# \ No newline at end of file diff --git a/peach-web/docs/FEATURES.md b/peach-web/docs/FEATURES.md new file mode 100644 index 0000000..ef00257 --- /dev/null +++ b/peach-web/docs/FEATURES.md @@ -0,0 +1,146 @@ +# peach-web + +## Features List + +A first draft of desirable features for the PeachCloud web admin interface. + +_Note: This is a work-in-progress. Expect changes._ + +_First-pass at organizing features categorically_ + +**Summary:** + +- Profile +- Peers +- Invites +- System Status +- Configuration +- Documentation +- Network + - _Not sure if this one is made redundant by System Status & Configuration_ + - _Could also be 'Monitoring' (showing graphs of SSB network acitivty_ + +**Detail:** + +- Profile + - Display avatar + - Set avatar (upload file) + - Display bio + - Update bio +- Peers + - List friends + - List followers + - List follows + - List locally-connected peers + - Follow + - Unfollow + - Block + - Mute (private block) + - Private-message a peer +- Invites + - Create an invite + - Text-based (hash) + - Image-based (QR-code) + - Audio-based (possible?) + - Share an invite + - Send to a peer within SSB (private message) + - Share publically within SSB (public post) + - Send via email + - Accept an invite + - Monitor an invite + - Check if the invite has been accepted + - For multi-use invites, show number of used & unused invite-slots + - Cancel an invite (_not sure if this is currently possible_) +- Documentation + - Browse + - Scuttlebot + - Scuttlebutt + - PeachCloud + - _Link to external docs or host locally for offline-first viewing?_ + - Search + - Notes + - Add personal notes to document specific workflows etc. + - Display notes + - Delete notes +- System status + - Hardware + - CPU usage + - Memory usage + - Storage usage + - Disk I/O + - Software + - Version info of PeachCloud, sbot, plugins + - Scripts + - Plugins + - Power + - Power source (mains, battery, solar panels) + - Battery level and status (ie. 60% - charging) + - Network + - Display network mode (AP or client) + - If AP, list connected devices + - Display current connection(s) + - Ethernet + - WiFi + - Bluetooth + - NFC + - LoRa + - Display signal strength + - Display bandwidth usage + - Display hostname & external IP + - Display internal IP + - Logs + - Display system logs + - Errors + - List errors + - Report a bug / error + - Via SSB message + - Via email +- Configuration + - Access control + - Change user password + - Change administrator password + - Manage guest account + - Enable / disable SSH + - Blob management + - Prune blobs + - By size + - By date + - By author + - Network + - Set network mode (AP or client) + - List available networks + - Connect to a network + - Disconnect from a network + - Routing + - Select routing over IPv4, IPv6 + - Enable / disable routing over CJDNS / Yggdrail + - Updates + - Check for available updates + - Download updates + - Install / apply update + - Backups + - Create backup + - Secret key + - Configuration (device settings) + - Export backup + - External storage (USB) + - Dark Crystal + - IPFS / Dat + - List backup history + - Schedule backups + - Delete previous backups / backup history + - Alerts + - Set alerts based on CPU, memory, disk, bandwith-usage thresholds + - List previously-defined alerts + - Delete alerts + - Plugins + - List available plugins / extensions + - _E.g. ssb-web viewer, git-ssb viewer_ + - Activate a plugin + - Deactivate a plugin + - Define plugin settings + - Miscellaneous + - List current datetime + - Set datetime + - Display current timezone + - Set timezone diff --git a/peach-web/docs/RESOURCES.md b/peach-web/docs/RESOURCES.md new file mode 100644 index 0000000..a7daf8a --- /dev/null +++ b/peach-web/docs/RESOURCES.md @@ -0,0 +1,49 @@ +# peach-web + +## Resources + +### Raspberry Pi + +[Installing Debian ARM64 on Raspberry Pi 3 with WiFi](https://quantum2.xyz/2017/10/27/installing-debian-arm64-on-raspberry-pi-3-with-wifi/) + +[Debian buster on the Raspberry Pi 3 (update) (2018-01-08)](https://people.debian.org/~stapelberg/2018/01/08/raspberry-pi-3) + +**Networking** + +[Setting up a Raspberry Pi as a WiFi access point](https://learn.adafruit.com/setting-up-a-raspberry-pi-as-a-wifi-access-point/overview) + +[Switching Between Client Mode and Access Point Mode on Raspberry Pi](http://shortcircuitsandinfiniteloops.blogspot.com/2018/02/switching-between-client-mode-and.html) + + - No need for additional / external WiFi adapter + +[Using your new Raspberry Pi 3 as a WiFi access point with hostapd](https://frillip.com/using-your-raspberry-pi-3-as-a-wifi-access-point-with-hostapd/) + + - _Note: Successfully used this to configure my Pi 3 as a WiFi access point (including DHCP)_ + +[Starting hostapd when WiFi goes down (bash script)](http://sirlagz.net/2013/01/22/script-starting-hostapd-when-wifi-goes-down/) + +[Raspberry Pi Zero W Simultaneous AP and Managed Mode Wifi](https://blog.thewalr.us/2017/09/26/raspberry-pi-zero-w-simultaneous-ap-and-managed-mode-wifi/) + + - Could be helpful when it comes to switching between managed and AP mode + +[RPi Switch Between AP and Client Mode](https://raspberrypi.stackexchange.com/questions/44184/switch-between-ap-and-client-mode) + +[Using a Raspberry Pi 3 as a WiFi Client and Access Point simultaneously](http://www.marrold.co.uk/2017/03/using-raspberry-pi-3-as-wifi-client-and.html) + +**GPIO** + +rppal ([Crate](https://crates.io/crates/rppal), [Repo](https://github.com/golemparts/rppal), [Docs](https://docs.golemparts.com/rppal)) + + - Interface for the Raspberry Pi's GPIO, I2C, PWM and SPI peripherals (Rust) + +[How to Setup an I2C LCD on the Raspberry Pi](http://www.circuitbasics.com/raspberry-pi-i2c-lcd-set-up-and-programming/) (Python) + +### UI / UX + +**Fantasy UI** + +[Mark Coleran](http://coleran.com/) - large portfolio of FUI projects + +[A Conversation About Fantasy User Interfaces](https://www.subtraction.com/2016/06/02/a-conversation-about-fantasy-user-interfaces/) - interview of Kirill Grouchnikov by Khoi Vinh + +[Pushing Pixels](https://www.pushing-pixels.org/fui/) - FUI interviews diff --git a/peach-web/docs/research/pub_questions.md b/peach-web/docs/research/pub_questions.md new file mode 100644 index 0000000..642b31a --- /dev/null +++ b/peach-web/docs/research/pub_questions.md @@ -0,0 +1,74 @@ +These questions were drawn from the #pub-help channel, viewed from the perspective of [@mycognosist](@HEqy940T6uB+T+d9Jaa58aNfRzLx9eRWqkZljBmnkmk=.ed25519) and spanning approximately 2 years. + +_Note: list is incomplete and loosely grouped for now_ + +### Hardware + +- What are the minimum hardware requirements for running a pub? +- Disk space + - How much is required? + - At what rate will usage increase? + - When can usage expect to stabilize? +- CPU & memory usage + - How much is being used? + +### Peers + +- Local peers + - Who am I connected to? + - Which pubs am I syncing with? +- Replication + - How many hops away will my pub replicate? + - How can I increase / reduce the replication reach? +- Users / peers + - How many people follow my pub? + - How many people is my pub following? + - How can I unfollow / block a peer / account? +- Discovery + - Once my pub is running, how can I publish / share discovery details? +- Sync + - How often will my pub sync with peers? + - How can I be sure my pub is syncing successfully? + +### Networking + +- Public IP & port + - How can I check / configure these details? + - Is it possible to proxy a pub to be able to listen to a port that is common with an HTTP server? +- Connections + - How many incoming & outgoing connections is my pub experiencing? + - Who / where / what am I connected to? +- NAT + - How can I ensure local network users are able to reach my pub behind a NAT? +- Bandwidth + - How much is required? + - Is there a per-user average? +- CJDNS / Yggdrasil + - How can I configure my pub to route in this way? + - How can I limit routing to IPv4 / IPv6 / CJDNS / Yggdrasil? + +### Testing + +- How can I be sure my pub is working properly? + +### Errors + +- Slow response + - How many peers is too many? + - Why else might my pub be responding slowly? +- Crashes and error reporting + - What's the best way to report a crash or error? + - What info should I submit? + - Who can I contact to request assistance? + +### Backups + +- Backups + - What files / info should I backup to allow recovery from hardware failure? + +### Documentation + +- Sbot commands + - Is there a place they're all documented? +- GDPR + - What do users of my pub need to be informed about to be compliant? diff --git a/peach-web/src/common.rs b/peach-web/src/common.rs new file mode 100644 index 0000000..b608500 --- /dev/null +++ b/peach-web/src/common.rs @@ -0,0 +1,105 @@ +//! This module contains core api functions shared by json_api.rs and by routes.rs +//! +//! These functions return Results which are then handled by the json api or the html routes +//! and turned into a rocket response appropriately. +use log::info; + +use crate::error::PeachWebError; +use crate::forms::{AddAdminForm, DnsForm, PasswordForm, ResetPasswordForm}; +use peach_lib::config_manager; +use peach_lib::dyndns_client; +use peach_lib::dyndns_client::{check_is_new_dyndns_domain, get_full_dynamic_domain}; +use peach_lib::error::PeachError; +use peach_lib::jsonrpc_client_core::{Error, ErrorKind}; +use peach_lib::jsonrpc_core::types::error::ErrorCode; +use peach_lib::password_utils; + +pub fn save_dns_configuration(dns_form: DnsForm) -> Result<(), PeachWebError> { + // first save local configurations + config_manager::set_external_domain(&dns_form.external_domain)?; + config_manager::set_dyndns_enabled_value(dns_form.enable_dyndns)?; + // if dynamic dns is enabled and this is a new domain name, then register it + if dns_form.enable_dyndns { + let full_dynamic_domain = get_full_dynamic_domain(&dns_form.dynamic_domain); + // check if this is a new domain or if its already registered + let is_new_domain = check_is_new_dyndns_domain(&full_dynamic_domain); + if is_new_domain { + match dyndns_client::register_domain(&full_dynamic_domain) { + Ok(_) => { + info!("Registered new dyndns domain"); + // successful update + Ok(()) + } + Err(err) => { + info!("Failed to register dyndns domain: {:?}", err); + // json response for failed update + let msg: String = match err { + PeachError::JsonRpcClientCore { source } => { + match source { + Error(ErrorKind::JsonRpcError(err), _state) => match err.code { + ErrorCode::ServerError(-32030) => { + format!("Error registering domain: {} was previously registered", full_dynamic_domain) + } + _ => { + format!("Failed to register dyndns domain {:?}", err) + } + }, + _ => { + format!("Failed to register dyndns domain: {:?}", source) + } + } + } + _ => "Failed to register dyndns domain".to_string(), + }; + Err(PeachWebError::FailedToRegisterDynDomain { msg }) + } + } + } + // if the domain is already registered, then dont re-register, and just return success + else { + Ok(()) + } + } else { + Ok(()) + } +} + +/// 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(()) +} + +/// 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(()) +} + +pub fn save_add_admin_form(admin_form: AddAdminForm) -> Result<(), PeachWebError> { + let _result = config_manager::add_ssb_admin_id(&admin_form.ssb_id)?; + // if the previous line didn't throw an error then it was a success + Ok(()) +} diff --git a/peach-web/src/context.rs b/peach-web/src/context.rs new file mode 100644 index 0000000..6bbeae5 --- /dev/null +++ b/peach-web/src/context.rs @@ -0,0 +1,890 @@ +//! Build context objects for inclusion in Tera templates. +//! +//! Each context object is represented by a struct which implements a build +//! method. Context objects provide the means by which application and device +//! state are made available for rendering in the templates. + +// Context object struct names: +// +// DeviceContext +// ErrorContext +// FlashContext +// HelpContext +// HomeContext +// LoginContext +// MessageContext +// NetworkContext +// NetworkAddContext +// NetworkAlertContext +// NetworkDetailContext +// NetworkListContext +// PeerContext +// ProfileContext +// ShutdownContext + +use std::collections::HashMap; + +use serde::Serialize; + +use peach_lib::config_manager::load_peach_config; +use peach_lib::dyndns_client; +use peach_lib::dyndns_client::{get_dyndns_subdomain, is_dns_updater_online}; +use peach_lib::network_client; +use peach_lib::network_client::{AccessPoint, Networks, Scan}; +use peach_lib::oled_client; +use peach_lib::sbot_client; +use peach_lib::stats_client; +use peach_lib::stats_client::{CpuStatPercentages, DiskUsage, LoadAverage, MemStat, Traffic}; + +use crate::monitor; +use crate::monitor::{Alert, Data, Threshold}; + +// used in /device for system statistics +#[derive(Debug, Serialize)] +pub struct DeviceContext { + pub back: Option, + pub cpu_stat_percent: Option, + pub disk_stats: Vec, + pub flash_name: Option, + pub flash_msg: Option, + pub load_average: Option, + pub mem_stats: Option, + pub network_ping: String, + pub oled_ping: String, + pub stats_ping: String, + pub dyndns_enabled: bool, + pub dyndns_is_online: bool, + pub config_is_valid: bool, + pub sbot_is_online: bool, + pub title: Option, + pub uptime: Option, +} + +impl DeviceContext { + pub fn build() -> DeviceContext { + // convert result to Option, discard any error + let cpu_stat_percent = stats_client::cpu_stats_percent().ok(); + let load_average = stats_client::load_average().ok(); + let mem_stats = stats_client::mem_stats().ok(); + let network_ping = match network_client::ping() { + Ok(_) => "ONLINE".to_string(), + Err(_) => "OFFLINE".to_string(), + }; + let oled_ping = match oled_client::ping() { + Ok(_) => "ONLINE".to_string(), + Err(_) => "OFFLINE".to_string(), + }; + let stats_ping = match stats_client::ping() { + Ok(_) => "ONLINE".to_string(), + Err(_) => "OFFLINE".to_string(), + }; + let uptime = match stats_client::uptime() { + Ok(mins) => mins, + Err(_) => "Unavailable".to_string(), + }; + + // serialize disk usage data into Vec + let disk_usage_stats = match stats_client::disk_usage() { + Ok(disks) => { + let partitions: Vec = serde_json::from_str(disks.as_str()) + .expect("Failed to deserialize disk_usage response"); + partitions + } + Err(_) => Vec::new(), + }; + + let mut disk_stats = Vec::new(); + // select only the partition we're interested in: /dev/mmcblk0p2 ("/") + for disk in disk_usage_stats { + if disk.mountpoint == "/" { + disk_stats.push(disk); + } + } + + // parse the uptime string to a signed integer (for math) + let uptime_parsed = uptime.parse::().ok(); + + // dyndns_is_online & config_is_valid + let dyndns_enabled: bool; + let dyndns_is_online: bool; + let config_is_valid: bool; + let load_peach_config_result = load_peach_config(); + match load_peach_config_result { + Ok(peach_config) => { + dyndns_enabled = peach_config.dyn_enabled; + config_is_valid = true; + if dyndns_enabled { + let is_dyndns_online_result = dyndns_client::is_dns_updater_online(); + match is_dyndns_online_result { + Ok(is_online) => { + dyndns_is_online = is_online; + } + Err(_err) => { + dyndns_is_online = false; + } + } + } else { + dyndns_is_online = false; + } + } + Err(_err) => { + dyndns_enabled = false; + dyndns_is_online = false; + config_is_valid = false; + } + } + + // test if go-sbot is running + let sbot_is_online: bool; + let sbot_is_online_result = sbot_client::is_sbot_online(); + match sbot_is_online_result { + Ok(val) => { + sbot_is_online = val; + } + Err(_err) => { + sbot_is_online = false; + } + } + + DeviceContext { + back: None, + cpu_stat_percent, + disk_stats, + flash_name: None, + flash_msg: None, + load_average, + mem_stats, + network_ping, + oled_ping, + stats_ping, + dyndns_enabled, + dyndns_is_online, + config_is_valid, + sbot_is_online, + title: None, + uptime: uptime_parsed, + } + } +} + +#[derive(Debug, Serialize)] +pub struct ErrorContext { + pub back: Option, + pub flash_name: Option, + pub flash_msg: Option, + pub title: Option, +} + +impl ErrorContext { + pub fn build() -> ErrorContext { + ErrorContext { + back: None, + flash_name: None, + flash_msg: None, + title: None, + } + } +} + +#[derive(Debug, Serialize)] +pub struct FlashContext { + pub flash_name: Option, + pub flash_msg: Option, +} + +#[derive(Debug, Serialize)] +pub struct HelpContext { + pub back: Option, + pub flash_name: Option, + pub flash_msg: Option, + pub title: Option, +} + +impl HelpContext { + pub fn build() -> HelpContext { + HelpContext { + back: None, + flash_name: None, + flash_msg: None, + title: None, + } + } +} + +#[derive(Debug, Serialize)] +pub struct HomeContext { + pub flash_name: Option, + pub flash_msg: Option, + pub title: Option, +} + +impl HomeContext { + pub fn build() -> HomeContext { + HomeContext { + flash_name: None, + flash_msg: None, + title: None, + } + } +} + +#[derive(Debug, Serialize)] +pub struct LoginContext { + pub back: Option, + pub flash_name: Option, + pub flash_msg: Option, + pub title: Option, +} + +impl LoginContext { + pub fn build() -> LoginContext { + LoginContext { + back: None, + flash_name: None, + flash_msg: None, + title: None, + } + } +} + +#[derive(Debug, Serialize)] +pub struct MessageContext { + pub back: Option, + pub flash_name: Option, + pub flash_msg: Option, + pub title: Option, +} + +impl MessageContext { + pub fn build() -> MessageContext { + MessageContext { + back: None, + flash_name: None, + flash_msg: None, + title: None, + } + } +} + +#[derive(Debug, Serialize)] +pub struct ConfigureDNSContext { + pub external_domain: String, + pub dyndns_subdomain: String, + pub enable_dyndns: bool, + pub is_dyndns_online: bool, + pub back: Option, + pub title: Option, + pub flash_name: Option, + pub flash_msg: Option, +} + +impl ConfigureDNSContext { + pub fn build() -> ConfigureDNSContext { + let peach_config = load_peach_config().unwrap(); + let dyndns_fulldomain = peach_config.dyn_domain; + let is_dyndns_online = is_dns_updater_online().unwrap(); + let dyndns_subdomain = + get_dyndns_subdomain(&dyndns_fulldomain).unwrap_or(dyndns_fulldomain); + ConfigureDNSContext { + external_domain: peach_config.external_domain, + dyndns_subdomain, + enable_dyndns: peach_config.dyn_enabled, + is_dyndns_online, + back: None, + title: None, + flash_name: None, + flash_msg: None, + } + } +} + +#[derive(Debug, Serialize)] +pub struct ChangePasswordContext { + pub back: Option, + pub title: Option, + pub flash_name: Option, + pub flash_msg: Option, +} + +impl ChangePasswordContext { + pub fn build() -> ChangePasswordContext { + ChangePasswordContext { + back: None, + title: None, + flash_name: None, + flash_msg: None, + } + } +} + +#[derive(Debug, Serialize)] +pub struct ConfigureAdminContext { + pub ssb_admin_ids: Vec, + pub back: Option, + pub title: Option, + pub flash_name: Option, + pub flash_msg: Option, +} + +impl ConfigureAdminContext { + pub fn build() -> ConfigureAdminContext { + let peach_config = load_peach_config().unwrap(); + let ssb_admin_ids = peach_config.ssb_admin_ids; + ConfigureAdminContext { + ssb_admin_ids, + back: None, + title: None, + flash_name: None, + flash_msg: None, + } + } +} + +#[derive(Debug, Serialize)] +pub struct AddAdminContext { + pub back: Option, + pub title: Option, + pub flash_name: Option, + pub flash_msg: Option, +} + +impl AddAdminContext { + pub fn build() -> AddAdminContext { + AddAdminContext { + back: None, + title: None, + flash_name: None, + flash_msg: None, + } + } +} + +#[derive(Debug, Serialize)] +pub struct ResetPasswordContext { + pub back: Option, + pub title: Option, + pub flash_name: Option, + pub flash_msg: Option, +} + +impl ResetPasswordContext { + pub fn build() -> ResetPasswordContext { + ResetPasswordContext { + back: None, + title: None, + flash_name: None, + flash_msg: None, + } + } +} + +#[derive(Debug, Serialize)] +pub struct SendPasswordResetContext { + pub back: Option, + pub title: Option, + pub flash_name: Option, + pub flash_msg: Option, +} + +impl SendPasswordResetContext { + pub fn build() -> SendPasswordResetContext { + SendPasswordResetContext { + back: None, + title: None, + flash_name: None, + flash_msg: None, + } + } +} + +#[derive(Debug, Serialize)] +pub struct NetworkContext { + pub ap_ip: String, + pub ap_ssid: String, + pub ap_state: String, + pub ap_traffic: Option, + pub wlan_ip: String, + pub wlan_rssi: Option, + pub wlan_scan: Option>, + pub wlan_ssid: String, + pub wlan_state: String, + pub wlan_status: String, + pub wlan_traffic: Option, + pub flash_name: Option, + pub flash_msg: Option, + // allows for passing in the ssid of a chosen access point + // this is used in the network_detail template + pub selected: Option, + // page title for header in navbar + pub title: Option, + // url for back-arrow link + pub back: Option, +} + +impl NetworkContext { + pub fn build() -> NetworkContext { + let ap_ip = match network_client::ip("ap0") { + Ok(ip) => ip, + Err(_) => "x.x.x.x".to_string(), + }; + let ap_ssid = match network_client::ssid("ap0") { + Ok(ssid) => ssid, + Err(_) => "Not currently activated".to_string(), + }; + let ap_state = match network_client::state("ap0") { + Ok(state) => state, + Err(_) => "Interface unavailable".to_string(), + }; + let ap_traffic = match network_client::traffic("ap0") { + Ok(traffic) => { + let mut t = traffic; + // modify traffic values & assign measurement unit + // based on received and transmitted values + // if received > 999 MB, convert it to GB + if t.received > 1_047_527_424 { + t.received /= 1_073_741_824; + t.rx_unit = Some("GB".to_string()); + } else if t.received > 0 { + // otherwise, convert it to MB + t.received = (t.received / 1024) / 1024; + t.rx_unit = Some("MB".to_string()); + } else { + t.received = 0; + t.rx_unit = Some("MB".to_string()); + } + + if t.transmitted > 1_047_527_424 { + t.transmitted /= 1_073_741_824; + t.tx_unit = Some("GB".to_string()); + } else if t.transmitted > 0 { + t.transmitted = (t.transmitted / 1024) / 1024; + t.tx_unit = Some("MB".to_string()); + } else { + t.transmitted = 0; + t.tx_unit = Some("MB".to_string()); + } + Some(t) + } + Err(_) => None, + }; + let wlan_ip = match network_client::ip("wlan0") { + Ok(ip) => ip, + Err(_) => "x.x.x.x".to_string(), + }; + let wlan_rssi = match network_client::rssi_percent("wlan0") { + Ok(rssi) => Some(rssi), + Err(_) => None, + }; + let wlan_scan = match network_client::available_networks("wlan0") { + Ok(networks) => { + let scan: Vec = serde_json::from_str(networks.as_str()) + .expect("Failed to deserialize scan_networks response"); + Some(scan) + } + Err(_) => None, + }; + let wlan_ssid = match network_client::ssid("wlan0") { + Ok(ssid) => ssid, + Err(_) => "Not connected".to_string(), + }; + let wlan_state = match network_client::state("wlan0") { + Ok(state) => state, + Err(_) => "Interface unavailable".to_string(), + }; + let wlan_status = match network_client::status("wlan0") { + Ok(status) => status, + Err(_) => "Interface unavailable".to_string(), + }; + let wlan_traffic = match network_client::traffic("wlan0") { + Ok(traffic) => { + let mut t = traffic; + // modify traffic values & assign measurement unit + // based on received and transmitted values + // if received > 999 MB, convert it to GB + if t.received > 1_047_527_424 { + t.received /= 1_073_741_824; + t.rx_unit = Some("GB".to_string()); + } else if t.received > 0 { + // otherwise, convert it to MB + t.received = (t.received / 1024) / 1024; + t.rx_unit = Some("MB".to_string()); + } else { + t.received = 0; + t.rx_unit = Some("MB".to_string()); + } + + if t.transmitted > 1_047_527_424 { + t.transmitted /= 1_073_741_824; + t.tx_unit = Some("GB".to_string()); + } else if t.transmitted > 0 { + t.transmitted = (t.transmitted / 1024) / 1024; + t.tx_unit = Some("MB".to_string()); + } else { + t.transmitted = 0; + t.tx_unit = Some("MB".to_string()); + } + Some(t) + } + Err(_) => None, + }; + + NetworkContext { + ap_ip, + ap_ssid, + ap_state, + ap_traffic, + wlan_ip, + wlan_rssi, + wlan_scan, + wlan_ssid, + wlan_state, + wlan_status, + wlan_traffic, + flash_name: None, + flash_msg: None, + selected: None, + title: None, + back: None, + } + } +} + +// used in /network/wifi/add? +#[derive(Debug, Serialize)] +pub struct NetworkAddContext { + pub back: Option, + pub flash_name: Option, + pub flash_msg: Option, + pub selected: Option, + pub title: Option, +} + +impl NetworkAddContext { + pub fn build() -> NetworkAddContext { + NetworkAddContext { + back: None, + flash_name: None, + flash_msg: None, + selected: None, + title: None, + } + } +} + +// used in /network/wifi/alert for traffic alerts +#[derive(Debug, Serialize)] +pub struct NetworkAlertContext { + pub alert: Alert, + pub back: Option, + pub data_total: Data, // combined stored and current wifi traffic in bytes + pub flash_name: Option, + pub flash_msg: Option, + pub threshold: Threshold, + pub title: Option, + pub traffic: Traffic, // current wifi traffic in bytes (since boot) +} + +impl NetworkAlertContext { + pub fn build() -> NetworkAlertContext { + let alert = monitor::get_alerts().unwrap(); + // stored wifi data values as bytes + let stored_traffic = monitor::get_data().unwrap(); + let threshold = monitor::get_thresholds().unwrap(); + // current wifi traffic values as bytes + let traffic = match network_client::traffic("wlan0") { + Ok(t) => t, + Err(_) => Traffic { + received: 0, + transmitted: 0, + rx_unit: None, + tx_unit: None, + }, + }; + + let current_traffic = traffic.received + traffic.transmitted; + let total = stored_traffic.total + current_traffic; + let data_total = Data { total }; + + NetworkAlertContext { + alert, + back: None, + data_total, + flash_name: None, + flash_msg: None, + threshold, + title: None, + traffic, + } + } +} + +#[derive(Debug, Serialize)] +pub struct NetworkDetailContext { + pub back: Option, + pub flash_name: Option, + pub flash_msg: Option, + pub saved_aps: Vec, + pub selected: Option, + pub title: Option, + pub wlan_ip: String, + pub wlan_networks: HashMap, + pub wlan_rssi: Option, + pub wlan_ssid: String, + pub wlan_state: String, + pub wlan_status: String, + pub wlan_traffic: Option, +} + +impl NetworkDetailContext { + pub fn build() -> NetworkDetailContext { + let wlan_ip = match network_client::ip("wlan0") { + Ok(ip) => ip, + Err(_) => "x.x.x.x".to_string(), + }; + // list of networks saved in wpa_supplicant.conf + let wlan_list = match network_client::saved_networks() { + Ok(ssids) => { + let networks: Vec = serde_json::from_str(ssids.as_str()) + .expect("Failed to deserialize scan_list response"); + networks + } + Err(_) => Vec::new(), + }; + // list of networks saved in wpa_supplicant.conf + // HACK: we're running the same function twice (wlan_list) + // see if we can implement clone for Vec instead + let saved_aps = match network_client::saved_networks() { + Ok(ssids) => { + let networks: Vec = serde_json::from_str(ssids.as_str()) + .expect("Failed to deserialize scan_list response"); + networks + } + Err(_) => Vec::new(), + }; + let wlan_rssi = match network_client::rssi_percent("wlan0") { + Ok(rssi) => Some(rssi), + Err(_) => None, + }; + // list of networks currently in range (online & accessible) + let wlan_scan = match network_client::available_networks("wlan0") { + Ok(networks) => { + let scan: Vec = serde_json::from_str(networks.as_str()) + .expect("Failed to deserialize scan_networks response"); + scan + } + Err(_) => Vec::new(), + }; + let wlan_ssid = match network_client::ssid("wlan0") { + Ok(ssid) => ssid, + Err(_) => "Not connected".to_string(), + }; + let wlan_state = match network_client::state("wlan0") { + Ok(state) => state, + Err(_) => "Interface unavailable".to_string(), + }; + let wlan_status = match network_client::status("wlan0") { + Ok(status) => status, + Err(_) => "Interface unavailable".to_string(), + }; + let wlan_traffic = match network_client::traffic("wlan0") { + Ok(traffic) => { + let mut t = traffic; + // modify traffic values & assign measurement unit + // based on received and transmitted values + // if received > 999 MB, convert it to GB + if t.received > 1_047_527_424 { + t.received /= 1_073_741_824; + t.rx_unit = Some("GB".to_string()); + } else if t.received > 0 { + // otherwise, convert it to MB + t.received = (t.received / 1024) / 1024; + t.rx_unit = Some("MB".to_string()); + } else { + t.received = 0; + t.rx_unit = Some("MB".to_string()); + } + + if t.transmitted > 1_047_527_424 { + t.transmitted /= 1_073_741_824; + t.tx_unit = Some("GB".to_string()); + } else if t.transmitted > 0 { + t.transmitted = (t.transmitted / 1024) / 1024; + t.tx_unit = Some("MB".to_string()); + } else { + t.transmitted = 0; + t.tx_unit = Some("MB".to_string()); + } + Some(t) + } + Err(_) => None, + }; + // create a hashmap to combine wlan_list & wlan_scan without repetition + let mut wlan_networks = HashMap::new(); + for ap in wlan_scan { + 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::().unwrap(); + // perform rssi (dBm) to quality (%) conversion + let quality_percent = 2 * (rssi_parsed + 100); + let ap_detail = AccessPoint { + detail: Some(ap), + state: "Available".to_string(), + signal: Some(quality_percent), + }; + wlan_networks.insert(ssid, ap_detail); + } + for network in wlan_list { + // avoid repetition by checking that ssid is not already in list + if !wlan_networks.contains_key(&network.ssid) { + let ssid = network.ssid.clone(); + let net_detail = AccessPoint { + detail: None, + state: "Not in range".to_string(), + signal: None, + }; + wlan_networks.insert(ssid, net_detail); + } + } + + NetworkDetailContext { + back: None, + flash_name: None, + flash_msg: None, + saved_aps, + selected: None, + title: None, + wlan_ip, + wlan_networks, + wlan_rssi, + wlan_ssid, + wlan_state, + wlan_status, + wlan_traffic, + } + } +} + +#[derive(Debug, Serialize)] +pub struct NetworkListContext { + pub ap_state: String, + pub back: Option, + pub flash_name: Option, + pub flash_msg: Option, + pub title: Option, + pub wlan_networks: HashMap, + pub wlan_ssid: String, +} + +impl NetworkListContext { + pub fn build() -> NetworkListContext { + // list of networks saved in the wpa_supplicant.conf + let wlan_list = match network_client::saved_networks() { + Ok(ssids) => { + let networks: Vec = serde_json::from_str(ssids.as_str()) + .expect("Failed to deserialize scan_list response"); + networks + } + Err(_) => Vec::new(), + }; + + // list of networks currently in range (online & accessible) + let wlan_scan = match network_client::available_networks("wlan0") { + Ok(networks) => { + let scan: Vec = serde_json::from_str(networks.as_str()) + .expect("Failed to deserialize scan_networks response"); + scan + } + Err(_) => Vec::new(), + }; + + let wlan_ssid = match network_client::ssid("wlan0") { + Ok(ssid) => ssid, + Err(_) => "Not connected".to_string(), + }; + + // create a hashmap to combine wlan_list & wlan_scan without repetition + let mut wlan_networks = HashMap::new(); + for ap in wlan_scan { + wlan_networks.insert(ap.ssid, "Available".to_string()); + } + for network in wlan_list { + // insert ssid (with state) only if it doesn't already exist + wlan_networks + .entry(network.ssid) + .or_insert_with(|| "Not in range".to_string()); + } + + let ap_state = match network_client::state("ap0") { + Ok(state) => state, + Err(_) => "Interface unavailable".to_string(), + }; + + NetworkListContext { + ap_state, + back: None, + flash_msg: None, + flash_name: None, + title: None, + wlan_networks, + wlan_ssid, + } + } +} + +#[derive(Debug, Serialize)] +pub struct PeerContext { + pub back: Option, + pub flash_name: Option, + pub flash_msg: Option, + pub title: Option, +} + +impl PeerContext { + pub fn build() -> PeerContext { + PeerContext { + back: None, + flash_name: None, + flash_msg: None, + title: None, + } + } +} + +#[derive(Debug, Serialize)] +pub struct ProfileContext { + pub back: Option, + pub flash_name: Option, + pub flash_msg: Option, + pub title: Option, +} + +impl ProfileContext { + pub fn build() -> ProfileContext { + ProfileContext { + back: None, + flash_name: None, + flash_msg: None, + title: None, + } + } +} + +#[derive(Debug, Serialize)] +pub struct ShutdownContext { + pub back: Option, + pub flash_name: Option, + pub flash_msg: Option, + pub title: Option, +} + +impl ShutdownContext { + pub fn build() -> ShutdownContext { + ShutdownContext { + back: None, + flash_name: None, + flash_msg: None, + title: None, + } + } +} diff --git a/peach-web/src/device.rs b/peach-web/src/device.rs new file mode 100644 index 0000000..5b715d9 --- /dev/null +++ b/peach-web/src/device.rs @@ -0,0 +1,28 @@ +//! System calls for modifying the state of the PeachCloud device. + +use std::io; +use std::process::{Command, Output}; + +use log::info; + +/// Executes a system command to reboot the device immediately. +pub fn reboot() -> io::Result { + info!("Rebooting the device"); + // ideally, we'd like to reboot after 5 seconds to allow time for JSON + // response but this is not possible with the `shutdown` command alone. + // TODO: send "rebooting..." message to `peach-oled` for display + Command::new("sudo") + .arg("shutdown") + .arg("-r") + .arg("now") + .output() +} + +/// Executes a system command to shutdown the device immediately. +pub fn shutdown() -> io::Result { + info!("Shutting down the device"); + // ideally, we'd like to reboot after 5 seconds to allow time for JSON + // response but this is not possible with the `shutdown` command alone. + // TODO: send "shutting down..." message to `peach-oled` for display + Command::new("sudo").arg("shutdown").arg("now").output() +} diff --git a/peach-web/src/error.rs b/peach-web/src/error.rs new file mode 100644 index 0000000..e140586 --- /dev/null +++ b/peach-web/src/error.rs @@ -0,0 +1,38 @@ +//!! different types of PeachWebError + +use peach_lib::error::PeachError; +use peach_lib::{serde_json, serde_yaml}; +use snafu::Snafu; + +#[derive(Debug, Snafu)] +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 }, +} + +impl From for PeachWebError { + fn from(err: serde_json::error::Error) -> PeachWebError { + PeachWebError::Serde { source: err } + } +} + +impl From for PeachWebError { + fn from(err: serde_yaml::Error) -> PeachWebError { + PeachWebError::YamlError { source: err } + } +} + +impl From for PeachWebError { + fn from(err: PeachError) -> PeachWebError { + PeachWebError::PeachLibError { + source: err, + msg: "".to_string(), + } + } +} diff --git a/peach-web/src/forms.rs b/peach-web/src/forms.rs new file mode 100644 index 0000000..3983bb5 --- /dev/null +++ b/peach-web/src/forms.rs @@ -0,0 +1,47 @@ +//! Provides data structures which are used to parse forms from post requests. +//! +use rocket::request::FromForm; +use rocket::UriDisplayQuery; +use serde::Deserialize; + +#[derive(Debug, Deserialize, FromForm)] +pub struct DnsForm { + pub external_domain: String, + pub enable_dyndns: bool, + pub dynamic_domain: String, +} + +#[derive(Debug, Deserialize, FromForm)] +pub struct PasswordForm { + pub old_password: String, + pub new_password1: String, + pub new_password2: String, +} + +#[derive(Debug, Deserialize, FromForm)] +pub struct ResetPasswordForm { + pub temporary_password: String, + pub new_password1: String, + pub new_password2: String, +} + +#[derive(Debug, Deserialize, FromForm, UriDisplayQuery)] +pub struct Ssid { + pub ssid: String, +} + +#[derive(Debug, Deserialize, FromForm)] +pub struct WiFi { + pub ssid: String, + pub pass: String, +} + +#[derive(Debug, Deserialize, FromForm)] +pub struct AddAdminForm { + pub ssb_id: String, +} + +#[derive(Debug, Deserialize, FromForm)] +pub struct DeleteAdminForm { + pub ssb_id: String, +} diff --git a/peach-web/src/json_api.rs b/peach-web/src/json_api.rs new file mode 100644 index 0000000..54da07a --- /dev/null +++ b/peach-web/src/json_api.rs @@ -0,0 +1,539 @@ +//! JSON API routes for PeachCloud. +//! +//! This module contains handlers which allow retrieval and modification of +//! device state via JSON. +//! +//! API ROUTES +//! +//! | Method | URL | Description | +//! | ------ | -------------------------------- | ----------------------------- | +//! | POST | /api/v1/device/reboot | Reboot device | +//! | POST | /api/v1/device/shutdown | Shutdown device | +//! | POST | /api/v1/network/activate_ap | | +//! | POST | /api/v1/network/activate_client | | +//! | GET | /api/v1/network/ip | | +//! | GET | /api/v1/network/rssi | | +//! | GET | /api/v1/network/ssid | | +//! | GET | /api/v1/network/state | | +//! | GET | /api/v1/network/status | | +//! | GET | /api/v1/network/wifi | Retrieve available networks | +//! | POST | /api/v1/network/wifi | Add WiFi AP credentials | +//! | POST | /api/v1/network/wifi/connect | Connect to WiFi access point | +//! | POST | /api/v1/network/wifi/disconnect | Disconnect WiFi access point | +//! | POST | /api/v1/network/wifi/forget | Forget / remove network | +//! | POST | /api/v1/network/wifi/modify | Modify network password | +//! | POST | /api/v1/network/wifi/usage | Update alert thresholds | +//! | POST | /api/v1/network/wifi/usage/reset | Reset stored data usage total | +//! | GET | /api/v1/ping | | +//! | GET | /api/v1/ping/network | Ping `peach-network` | +//! | GET | /api/v1/ping/oled | Ping `peach-oled` | +//! | GET | /api/v1/ping/stats | Ping `peach-stats` | +//! | POST | /api/v1/dns/configure | Modify dns configurations | +//! | POST | /api/v1/settings/change_password | Change password (logged in) | +//! | POST | /public/api/v1/reset_password | Change password (public) | + +use log::{debug, warn}; +use rocket::{get, post}; +use rocket_contrib::json; +use rocket_contrib::json::{Json, JsonValue}; +use serde::Serialize; + +use peach_lib::dyndns_client::is_dns_updater_online; +use peach_lib::network_client; +use peach_lib::oled_client; +use peach_lib::stats_client; +use peach_lib::stats_client::Traffic; + +use crate::common::{save_dns_configuration, save_password_form, save_reset_password_form}; +use crate::device; +use crate::forms::{DnsForm, PasswordForm, ResetPasswordForm, Ssid, WiFi}; +use crate::monitor; +use crate::monitor::Threshold; + +#[derive(Serialize)] +pub struct JsonResponse { + pub status: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub msg: Option, +} + +// reboot the device +#[post("/api/v1/device/reboot")] +pub fn reboot_device() -> Json { + match device::reboot() { + Ok(_) => { + debug!("Going down for reboot..."); + let status = "success".to_string(); + let msg = "Going down for reboot.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + Err(_) => { + warn!("Reboot failed"); + let status = "error".to_string(); + let msg = "Failed to reboot the device.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + } +} + +// shutdown the device +#[post("/api/v1/device/shutdown")] +pub fn shutdown_device() -> Json { + match device::shutdown() { + Ok(_) => { + debug!("Going down for shutdown..."); + let status = "success".to_string(); + let msg = "Going down for shutdown.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + Err(_) => { + warn!("Shutdown failed"); + let status = "error".to_string(); + let msg = "Failed to shutdown the device.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + } +} + +#[post("/api/v1/network/activate_ap")] +pub fn activate_ap() -> Json { + // activate the wireless access point + debug!("Activating WiFi access point."); + match network_client::activate_ap() { + Ok(_) => { + let status = "success".to_string(); + Json(build_json_response(status, None, None)) + } + Err(_) => { + let status = "error".to_string(); + let msg = "Failed to activate WiFi access point.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + } +} + +#[post("/api/v1/network/activate_client")] +pub fn activate_client() -> Json { + // activate the wireless client + debug!("Activating WiFi client mode."); + match network_client::activate_client() { + Ok(_) => { + let status = "success".to_string(); + Json(build_json_response(status, None, None)) + } + Err(_) => { + let status = "error".to_string(); + let msg = "Failed to activate WiFi client mode.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + } +} + +#[get("/api/v1/network/ip")] +pub fn return_ip() -> Json { + // retrieve ip for wlan0 or set to x.x.x.x if not found + let wlan_ip = match network_client::ip("wlan0") { + Ok(ip) => ip, + Err(_) => "x.x.x.x".to_string(), + }; + // retrieve ip for ap0 or set to x.x.x.x if not found + let ap_ip = match network_client::ip("ap0") { + Ok(ip) => ip, + Err(_) => "x.x.x.x".to_string(), + }; + let data = json!({ + "wlan0": wlan_ip, + "ap0": ap_ip + }); + let status = "success".to_string(); + Json(build_json_response(status, Some(data), None)) +} + +#[get("/api/v1/network/rssi")] +pub fn return_rssi() -> Json { + // retrieve rssi for connected network + match network_client::rssi("wlan0") { + Ok(rssi) => { + let status = "success".to_string(); + let data = json!(rssi); + Json(build_json_response(status, Some(data), None)) + } + Err(_) => { + let status = "success".to_string(); + let msg = "Not currently connected to an access point.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + } +} + +#[get("/api/v1/network/ssid")] +pub fn return_ssid() -> Json { + // retrieve ssid for connected network + match network_client::ssid("wlan0") { + Ok(network) => { + let status = "success".to_string(); + let data = json!(network); + Json(build_json_response(status, Some(data), None)) + } + Err(_) => { + let status = "success".to_string(); + let msg = "Not currently connected to an access point.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + } +} + +#[get("/api/v1/network/state")] +pub fn return_state() -> Json { + // retrieve state of wlan0 or set to x.x.x.x if not found + let wlan_state = match network_client::state("wlan0") { + Ok(state) => state, + Err(_) => "unavailable".to_string(), + }; + // retrieve state for ap0 or set to x.x.x.x if not found + let ap_state = match network_client::state("ap0") { + Ok(state) => state, + Err(_) => "unavailable".to_string(), + }; + let data = json!({ + "wlan0": wlan_state, + "ap0": ap_state + }); + let status = "success".to_string(); + Json(build_json_response(status, Some(data), None)) +} + +#[get("/api/v1/network/status")] +pub fn return_status() -> Json { + // retrieve status info for wlan0 interface + match network_client::status("wlan0") { + Ok(network) => { + let status = "success".to_string(); + let data = json!(network); + Json(build_json_response(status, Some(data), None)) + } + Err(_) => { + let status = "success".to_string(); + let msg = "Not currently connected to an access point.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + } +} + +#[get("/api/v1/network/wifi")] +pub fn scan_networks() -> Json { + // retrieve scan results for access-points within range of wlan0 + match network_client::available_networks("wlan0") { + Ok(networks) => { + let status = "success".to_string(); + let data = json!(networks); + Json(build_json_response(status, Some(data), None)) + } + Err(_) => { + let status = "success".to_string(); + let msg = "Unable to scan for networks. Interface may be deactivated.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + } +} + +#[post("/api/v1/network/wifi", data = "")] +pub fn add_wifi(wifi: Json) -> Json { + // generate and write wifi config to wpa_supplicant + match network_client::add(&wifi.ssid, &wifi.pass) { + Ok(_) => { + debug!("Added WiFi credentials."); + // force reread of wpa_supplicant.conf file with new credentials + match network_client::reconfigure() { + Ok(_) => debug!("Successfully reconfigured wpa_supplicant."), + Err(_) => warn!("Failed to reconfigure wpa_supplicant."), + } + // json response for successful update + let status = "success".to_string(); + let msg = "WiFi credentials added.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + Err(_) => { + debug!("Failed to add WiFi credentials."); + // json response for failed update + let status = "error".to_string(); + let msg = "Failed to add WiFi credentials.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + } +} + +#[post("/api/v1/network/wifi/connect", data = "")] +pub fn connect_ap(ssid: Json) -> Json { + // retrieve the id for the given network ssid + match network_client::id("wlan0", &ssid.ssid) { + // attempt connection with the given network + Ok(id) => match network_client::connect(&id, "wlan0") { + Ok(_) => { + let status = "success".to_string(); + let msg = "Connected to chosen network.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + Err(_) => { + let status = "error".to_string(); + let msg = "Failed to connect to chosen network.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + }, + Err(_) => { + let status = "error".to_string(); + let msg = "Failed to retrieve the network ID.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + } +} + +#[post("/api/v1/network/wifi/disconnect", data = "")] +pub fn disconnect_ap(ssid: Json) -> Json { + // attempt to disable the current network for wlan0 interface + match network_client::disable("wlan0", &ssid.ssid) { + Ok(_) => { + let status = "success".to_string(); + let msg = "Disconnected from WiFi network.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + Err(_) => { + let status = "error".to_string(); + let msg = "Failed to disconnect from WiFi network.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + } +} + +#[post("/api/v1/network/wifi/forget", data = "")] +pub fn forget_ap(network: Json) -> Json { + let ssid = &network.ssid; + match network_client::forget("wlan0", ssid) { + Ok(_) => { + debug!("Removed WiFi credentials for chosen network."); + let status = "success".to_string(); + let msg = "WiFi network credentials removed.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + Err(_) => { + warn!("Failed to remove WiFi credentials."); + let status = "error".to_string(); + let msg = "Failed to remove WiFi network credentials.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + } +} + +#[post("/api/v1/network/wifi/modify", data = "")] +pub fn modify_password(wifi: Json) -> Json { + let ssid = &wifi.ssid; + let pass = &wifi.pass; + // we are using a helper function (`update`) to delete the old + // credentials and add the new ones. this is because the wpa_cli method + // for updating the password does not work. + match network_client::update("wlan0", ssid, pass) { + Ok(_) => { + debug!("WiFi password updated for chosen network."); + let status = "success".to_string(); + let msg = "WiFi password updated.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + Err(_) => { + warn!("Failed to update WiFi password."); + let status = "error".to_string(); + let msg = "Failed to update WiFi password.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + } +} + +#[post("/api/v1/network/wifi/usage", data = "")] +pub fn update_wifi_alerts(thresholds: Json) -> Json { + match monitor::update_store(thresholds.into_inner()) { + Ok(_) => { + debug!("WiFi data usage thresholds updated."); + let status = "success".to_string(); + let msg = "Updated alert threshold and flags.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + Err(_) => { + warn!("Failed to update WiFi data usage thresholds."); + let status = "error".to_string(); + let msg = "Failed to update WiFi data usage thresholds.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + } +} + +#[post("/api/v1/network/wifi/usage/reset")] +pub fn reset_data_total() -> Json { + match monitor::reset_data() { + Ok(_) => { + debug!("Reset network data usage total."); + let traffic = match network_client::traffic("wlan0") { + Ok(t) => t, + Err(_) => Traffic { + received: 0, + transmitted: 0, + rx_unit: None, + tx_unit: None, + }, + }; + // current wifi traffic values as bytes + let current_traffic = traffic.received + traffic.transmitted; + let data = json!(current_traffic); + let status = "success".to_string(); + let msg = "Reset network data usage total.".to_string(); + Json(build_json_response(status, Some(data), Some(msg))) + } + Err(_) => { + warn!("Failed to reset network data usage total."); + let status = "error".to_string(); + let msg = "Failed to reset network data usage total.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + } +} + +// status route: useful for checking connectivity from web client +#[get("/api/v1/ping")] +pub fn ping_pong() -> Json { + // ping pong + let status = "success".to_string(); + let msg = "pong!".to_string(); + Json(build_json_response(status, None, Some(msg))) +} + +// test route: useful for ad hoc testing +#[get("/api/v1/test")] +pub fn test_route() -> Json { + let val = is_dns_updater_online().unwrap(); + let status = "success".to_string(); + let msg = val.to_string(); + Json(build_json_response(status, None, Some(msg))) +} + +// status route: check availability of `peach-network` microservice +#[get("/api/v1/ping/network")] +pub fn ping_network() -> Json { + match network_client::ping() { + Ok(_) => { + debug!("peach-network responded successfully"); + let status = "success".to_string(); + let msg = "peach-network is available.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + Err(_) => { + warn!("peach-network failed to respond"); + let status = "error".to_string(); + let msg = "peach-network is unavailable.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + } +} + +// status route: check availability of `peach-oled` microservice +#[get("/api/v1/ping/oled")] +pub fn ping_oled() -> Json { + match oled_client::ping() { + Ok(_) => { + debug!("peach-oled responded successfully"); + let status = "success".to_string(); + let msg = "peach-oled is available.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + Err(_) => { + warn!("peach-oled failed to respond"); + let status = "error".to_string(); + let msg = "peach-oled is unavailable.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + } +} + +// status route: check availability of `peach-stats` microservice +#[get("/api/v1/ping/stats")] +pub fn ping_stats() -> Json { + match stats_client::ping() { + Ok(_) => { + debug!("peach-stats responded successfully"); + let status = "success".to_string(); + let msg = "peach-stats is available.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + Err(_) => { + warn!("peach-stats failed to respond"); + let status = "error".to_string(); + let msg = "peach-stats is unavailable.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + } +} + +#[post("/api/v1/dns/configure", data = "")] +pub fn save_dns_configuration_endpoint(dns_form: Json) -> Json { + let result = save_dns_configuration(dns_form.into_inner()); + match result { + Ok(_) => { + let status = "success".to_string(); + let msg = "New dynamic dns configuration is now enabled".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + Err(err) => { + let status = "error".to_string(); + let msg = format!("{}", err); + Json(build_json_response(status, None, Some(msg))) + } + } +} + +#[post("/api/v1/settings/change_password", data = "")] +pub fn save_password_form_endpoint(password_form: Json) -> Json { + 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(); + Json(build_json_response(status, None, Some(msg))) + } + Err(err) => { + let status = "error".to_string(); + let msg = format!("{}", err); + Json(build_json_response(status, None, Some(msg))) + } + } +} + +/// this reset password route is used by a user who is not logged in +/// and is specifically for users who have forgotten their password +/// all routes under /public/* are excluded from nginx basic auth via the nginx config +#[post("/public/api/v1/reset_password", data = "")] +pub fn reset_password_form_endpoint( + reset_password_form: Json, +) -> Json { + 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(); + Json(build_json_response(status, None, Some(msg))) + } + Err(err) => { + let status = "error".to_string(); + let msg = format!("{}", err); + Json(build_json_response(status, None, Some(msg))) + } + } +} + +// HELPER FUNCTIONS + +pub fn build_json_response( + status: String, + data: Option, + msg: Option, +) -> JsonResponse { + JsonResponse { status, data, msg } +} diff --git a/peach-web/src/lib.rs b/peach-web/src/lib.rs new file mode 100644 index 0000000..a9ec234 --- /dev/null +++ b/peach-web/src/lib.rs @@ -0,0 +1,150 @@ +//! # peach-web +//! +//! `peach-web` 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. +//! +//! ## 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. A basic Websockets server is included, +//! though is not currently utilised. 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)] +// 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 common; +pub mod context; +pub mod device; +pub mod error; +pub mod forms; +pub mod json_api; +pub mod monitor; +pub mod routes; +#[cfg(test)] +mod tests; +mod ws; + +use std::{env, thread}; + +use log::{debug, error, info}; + +use rocket::{catchers, routes}; +use rocket_contrib::templates::Template; + +use crate::json_api::*; +use crate::routes::*; +use crate::ws::*; + +pub type BoxError = Box; + +// create rocket instance & mount web & json routes (makes testing easier) +fn rocket() -> rocket::Rocket { + rocket::ignite() + .mount( + "/", + routes![ + add_credentials, // WEB ROUTE + connect_wifi, // WEB ROUTE + disconnect_wifi, // WEB ROUTE + deploy_ap, // WEB ROUTE + deploy_client, // WEB ROUTE + device_stats, // WEB ROUTE + files, // WEB ROUTE + forget_wifi, // WEB ROUTE + help, // WEB ROUTE + index, // WEB ROUTE + login, // WEB ROUTE + logout, // WEB ROUTE + messages, // WEB ROUTE + network_home, // WEB ROUTE + network_add_ssid, // WEB ROUTE + network_add_wifi, // WEB ROUTE + network_detail, // WEB ROUTE + peers, // WEB ROUTE + profile, // WEB ROUTE + reboot_cmd, // WEB ROUTE + shutdown_cmd, // WEB ROUTE + shutdown_menu, // WEB ROUTE + wifi_list, // WEB ROUTE + wifi_password, // WEB ROUTE + wifi_set_password, // WEB ROUTE + wifi_usage, // WEB ROUTE + wifi_usage_alerts, // WEB ROUTE + wifi_usage_reset, // WEB ROUTE + configure_dns, // WEB ROUTE + configure_dns_post, // WEB ROUTE + change_password, // WEB ROUTE + reset_password, // WEB ROUTE + reset_password_post, // WEB ROUTE + send_password_reset_page, // WEB ROUTE + send_password_reset_post, // WEB ROUTE + configure_admin, // WEB ROUTE + add_admin, // WEB ROUTE + add_admin_post, // WEB ROUTE + delete_admin_post, // WEB ROUTE + activate_ap, // JSON API + activate_client, // JSON API + add_wifi, // JSON API + connect_ap, // JSON API + disconnect_ap, // JSON API + forget_ap, // JSON API + modify_password, // JSON API + ping_pong, // JSON API + test_route, // JSON API + ping_network, // JSON API + ping_oled, // JSON API + ping_stats, // JSON API + reset_data_total, // JSON API + return_ip, // JSON API + return_rssi, // JSON API + return_ssid, // JSON API + return_state, // JSON API + return_status, // JSON API + reboot_device, // JSON API + scan_networks, // JSON API + shutdown_device, // JSON API + update_wifi_alerts, // JSON API + save_dns_configuration_endpoint, // JSON API + save_password_form_endpoint, // JSON API + reset_password_form_endpoint, // JSON API + ], + ) + .register(catchers![not_found, internal_error]) + .attach(Template::fairing()) +} + +// launch the rocket server +pub fn run() -> Result<(), BoxError> { + info!("Starting up."); + + // spawn a separate thread for rocket to prevent blocking websockets + thread::spawn(|| { + info!("Launching Rocket server."); + rocket().launch(); + }); + + // NOTE: websockets are not currently in use (may be in the future) + let ws_addr = env::var("PEACH_WEB_WS").unwrap_or_else(|_| "0.0.0.0:5115".to_string()); + match websocket_server(ws_addr) { + Ok(_) => debug!("Websocket server terminated without error."), + Err(e) => error!("Error starting the websocket server: {}", e), + }; + + Ok(()) +} diff --git a/peach-web/src/main.rs b/peach-web/src/main.rs new file mode 100644 index 0000000..4588e5d --- /dev/null +++ b/peach-web/src/main.rs @@ -0,0 +1,16 @@ +//! Initialize the logger and run the application, catching any errors. + +use std::process; + +use log::error; + +fn main() { + // initialize the logger + env_logger::init(); + + // handle errors returned from `run` + if let Err(e) = peach_web::run() { + error!("Application error: {}", e); + process::exit(1); + } +} diff --git a/peach-web/src/monitor.rs b/peach-web/src/monitor.rs new file mode 100644 index 0000000..e09d2db --- /dev/null +++ b/peach-web/src/monitor.rs @@ -0,0 +1,198 @@ +// Monitor data transmission totals, set thresholds and check alert flags + +use std::convert::TryInto; + +use nest::{Error, Store, Value}; +use rocket::request::FromForm; +use serde::{Deserialize, Serialize}; +use serde_json::json; + +/// Network traffic data total +#[derive(Debug, Serialize)] +pub struct Data { + pub total: u64, // total traffic in bytes +} + +impl Data { + /// Retrieve network traffic data values from the store + fn get(store: &Store) -> Data { + // retrieve previous network traffic statistics + let data_stored = match store.get(&["net", "traffic", "total"]) { + Ok(total) => total, + // return 0 if no value exists + Err(_) => Value::Uint(u64::MIN), + }; + + let mut data = Vec::new(); + // retrieve u64 from Value type + if let Value::Uint(total) = data_stored { + data.push(total); + }; + + Data { total: data[0] } + } +} + +/// Network traffic notification thresholds and flags (user-defined) +#[derive(Debug, Deserialize, Serialize, FromForm)] +pub struct Threshold { + warn: u64, // traffic warning threshold + cut: u64, // traffic cutoff threshold + warn_flag: bool, // traffic warning notification flag + cut_flag: bool, // traffic cutoff notification flag +} + +impl Threshold { + /// Retrieve notification thresholds and flags from the store + fn get(store: &Store) -> Threshold { + let mut threshold = Vec::new(); + + let warn_val = store + .get(&["net", "notify", "warn"]) + .unwrap_or(Value::Uint(0)); + if let Value::Uint(val) = warn_val { + threshold.push(val); + }; + + let cut_val = store + .get(&["net", "notify", "cut"]) + .unwrap_or(Value::Uint(0)); + if let Value::Uint(val) = cut_val { + threshold.push(val); + }; + + let mut flag = Vec::new(); + + let warn_flag = store + .get(&["net", "notify", "warn_flag"]) + .unwrap_or(Value::Bool(false)); + if let Value::Bool(state) = warn_flag { + flag.push(state); + } + + let cut_flag = store + .get(&["net", "notify", "cut_flag"]) + .unwrap_or(Value::Bool(false)); + if let Value::Bool(state) = cut_flag { + flag.push(state); + } + + Threshold { + warn: threshold[0], + cut: threshold[1], + warn_flag: flag[0], + cut_flag: flag[1], + } + } + + /// Store notification flags from user data + fn set(self, store: &Store) { + store + .set(&["net", "notify", "warn"], &Value::Uint(self.warn)) + .unwrap(); + store + .set(&["net", "notify", "cut"], &Value::Uint(self.cut)) + .unwrap(); + store + .set( + &["net", "notify", "warn_flag"], + &Value::Bool(self.warn_flag), + ) + .unwrap(); + store + .set(&["net", "notify", "cut_flag"], &Value::Bool(self.cut_flag)) + .unwrap(); + } +} + +/// Warning and cutoff network traffic alert flags (programatically-defined) +#[derive(Debug, Serialize)] +pub struct Alert { + warn: bool, + cut: bool, +} + +impl Alert { + /// Retrieve latest alert flags from the store + fn get(store: &Store) -> Alert { + let mut alert = Vec::new(); + + let warn_flag = store + .get(&["net", "alert", "warn"]) + .unwrap_or(Value::Bool(false)); + if let Value::Bool(flag) = warn_flag { + alert.push(flag); + } + + let cut_flag = store + .get(&["net", "alert", "cut"]) + .unwrap_or(Value::Bool(false)); + if let Value::Bool(flag) = cut_flag { + alert.push(flag); + } + + Alert { + warn: alert[0], + cut: alert[1], + } + } +} + +fn create_store() -> std::result::Result { + // define the path + let path = xdg::BaseDirectories::new() + .unwrap() + .create_data_directory("peachcloud") + .unwrap(); + + // define the schema + let schema = json!({ + "net": { + "traffic": "json", + "alert": "json", + "notify": "json", + } + }) + .try_into()?; + + // create the data store + let store = Store::new(path, schema); + + Ok(store) +} + +pub fn get_alerts() -> std::result::Result { + let store = create_store()?; + let alerts = Alert::get(&store); + + Ok(alerts) +} + +pub fn get_data() -> std::result::Result { + let store = create_store()?; + let data = Data::get(&store); + + Ok(data) +} + +pub fn get_thresholds() -> std::result::Result { + let store = create_store()?; + let thresholds = Threshold::get(&store); + + Ok(thresholds) +} + +// set stored traffic total to 0 +pub fn reset_data() -> std::result::Result<(), Error> { + let store = create_store()?; + store.set(&["net", "traffic", "total"], &Value::Uint(0))?; + + Ok(()) +} + +pub fn update_store(threshold: Threshold) -> std::result::Result<(), Error> { + let store = create_store()?; + Threshold::set(threshold, &store); + + Ok(()) +} diff --git a/peach-web/src/routes.rs b/peach-web/src/routes.rs new file mode 100644 index 0000000..53feb98 --- /dev/null +++ b/peach-web/src/routes.rs @@ -0,0 +1,744 @@ +//! Route handlers for PeachCloud web routes. +//! +//! This module contains handlers which serve templates and static assests, +//! generate flash messages, catch errors and handle redirects for PeachCloud. +//! +//! WEB ROUTES +//! +//! | Method | URL | Description | +//! | ------ | --------------------------- | --------------------------------- | +//! | GET | / | Home | +//! | GET | /device | Device statistics | +//! | GET | /device/reboot | Reboot device | +//! | GET | /device/shutdown | Shutdown device | +//! | GET | /help | Help and usage guidelines | +//! | GET | /login | Login form | +//! | POST | /login | Login form submission | +//! | POST | /logout | Logout authenticated user | +//! | GET | /network | Network overview | +//! | GET | /network/ap/activate | Activate WiFi access point mode | +//! | GET | /network/wifi | List of networks | +//! | GET | /network/wifi? | Details of single network | +//! | GET | /network/wifi/activate | Activate WiFi client mode | +//! | GET | /network/wifi/add | Add WiFi form | +//! | POST | /network/wifi/add | WiFi form submission | +//! | GET | /network/wifi/add? | Add WiFi form (SSID populated) | +//! | POST | /network/wifi/connect | Connect to WiFi access point | +//! | POST | /network/wifi/disconnect | Disconnect from WiFi access point | +//! | POST | /network/wifi/forget | Remove WiFi | +//! | GET | /network/wifi/modify? | Modify WiFi password form | +//! | POST | /network/wifi/modify | Modify network password | +//! | GET | /network/wifi/usage | WiFi data usage form | +//! | POST | /network/wifi/usage | WiFi data usage form submission | +//! | GET | /network/wifi/usage/reset | Reset stored data usage total | +//! | GET | /messages | Private Scuttlebutt messages | +//! | GET | /peers | Scuttlebutt peers overview | +//! | GET | /profile | Scuttlebutt user profile | +//! | GET | /shutdown | Shutdown menu | +//! | GET | /network/dns | View DNS configurations | +//! | POST | /network/dns | Modify DNS configurations | +//! | GET | /settings/change_password | View password settings form | +//! | POST | /settings/change_password | Change admin password | +//! | GET | /reset_password | Change password using temp pass | +//! | POST | /reset_password | Rhange password using temp pass | +//! | GET | /send_password_reset | Send new password reset link | +//! | POST | /send_password_reset | Send new password reset link | + +use std::path::{Path, PathBuf}; + +use log::{debug, info, warn}; +use percent_encoding::percent_decode; +use rocket::http::RawStr; +use rocket::request::{FlashMessage, Form}; +use rocket::response::{Flash, NamedFile, Redirect}; +use rocket::{catch, get, post, uri}; +use rocket_contrib::templates::Template; + +use peach_lib::config_manager; +use peach_lib::network_client; +use peach_lib::password_utils; + +use crate::common::{ + save_add_admin_form, save_dns_configuration, save_password_form, save_reset_password_form, +}; +use crate::context::{ + AddAdminContext, ChangePasswordContext, ConfigureAdminContext, ConfigureDNSContext, + DeviceContext, ErrorContext, HelpContext, HomeContext, LoginContext, MessageContext, + NetworkAddContext, NetworkAlertContext, NetworkContext, NetworkDetailContext, + NetworkListContext, PeerContext, ProfileContext, ResetPasswordContext, + SendPasswordResetContext, ShutdownContext, +}; +use crate::device; +use crate::forms::{ + AddAdminForm, DeleteAdminForm, DnsForm, PasswordForm, ResetPasswordForm, Ssid, WiFi, +}; +use crate::monitor; +use crate::monitor::Threshold; + +#[get("/")] +pub fn index() -> Template { + let context = HomeContext { + flash_name: None, + flash_msg: None, + title: None, + }; + Template::render("index", &context) +} + +#[get("/device")] +pub fn device_stats(flash: Option) -> Template { + // assign context through context_builder call + let mut context = DeviceContext::build(); + context.back = Some("/".to_string()); + context.title = Some("Device Status".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.name().to_string()); + context.flash_msg = Some(flash.msg().to_string()); + }; + // template_dir is set in Rocket.toml + Template::render("device", &context) +} + +#[get("/device/reboot")] +pub fn reboot_cmd() -> Flash { + match device::reboot() { + Ok(_) => Flash::success(Redirect::to("/shutdown"), "Rebooting the device"), + Err(_) => Flash::error(Redirect::to("/shutdown"), "Failed to reboot the device"), + } +} + +#[get("/device/shutdown")] +pub fn shutdown_cmd() -> Flash { + match device::shutdown() { + Ok(_) => Flash::success(Redirect::to("/shutdown"), "Shutting down the device"), + Err(_) => Flash::error(Redirect::to("/shutdown"), "Failed to shutdown the device"), + } +} + +#[get("/help")] +pub fn help(flash: Option) -> 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.name().to_string()); + context.flash_msg = Some(flash.msg().to_string()); + }; + Template::render("help", &context) +} + +#[get("/login")] +pub fn login(flash: Option) -> 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.name().to_string()); + context.flash_msg = Some(flash.msg().to_string()); + }; + Template::render("login", &context) +} + +#[post("/logout")] +pub fn logout() -> Flash { + // logout authenticated user + debug!("Attempting deauthentication of user."); + /* + match logout_user() { + Ok(_) => Flash::success(Redirect::to("/"), "Logout success"), + Err(_) => Flash::error( + Redirect::to("/"), + "Failed to logout", + ), + } + */ + Flash::success(Redirect::to("/"), "Logged out") +} + +#[get("/network")] +pub fn network_home(flash: Option) -> Template { + // assign context through context_builder call + let mut context = NetworkContext::build(); + // set back button (nav) url + context.back = Some("/".to_string()); + // set page title + context.title = Some("Network Configuration".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.name().to_string()); + context.flash_msg = Some(flash.msg().to_string()); + }; + // template_dir is set in Rocket.toml + Template::render("network_card", &context) +} + +#[get("/network/ap/activate")] +pub fn deploy_ap() -> Flash { + // activate the wireless access point + debug!("Activating WiFi access point."); + match network_client::activate_ap() { + Ok(_) => Flash::success(Redirect::to("/network"), "Activated WiFi access point"), + Err(_) => Flash::error( + Redirect::to("/network"), + "Failed to activate WiFi access point", + ), + } +} + +#[get("/network/wifi")] +pub fn wifi_list(flash: Option) -> Template { + // assign context through context_builder call + let mut context = NetworkListContext::build(); + context.back = Some("/network".to_string()); + context.title = Some("WiFi Networks".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.name().to_string()); + context.flash_msg = Some(flash.msg().to_string()); + }; + // template_dir is set in Rocket.toml + Template::render("network_list", &context) +} + +#[get("/network/wifi?")] +pub fn network_detail(ssid: &RawStr, flash: Option) -> Template { + // assign context through context_builder call + let mut context = NetworkDetailContext::build(); + context.back = Some("/network/wifi".to_string()); + context.title = Some("WiFi Network".to_string()); + // decode ssid from url + let decoded_ssid = percent_decode(ssid.as_bytes()).decode_utf8().unwrap(); + context.selected = Some(decoded_ssid.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.name().to_string()); + context.flash_msg = Some(flash.msg().to_string()); + }; + // template_dir is set in Rocket.toml + Template::render("network_detail", &context) +} + +#[get("/network/wifi/activate")] +pub fn deploy_client() -> Flash { + // activate the wireless client + debug!("Activating WiFi client mode."); + match network_client::activate_client() { + Ok(_) => Flash::success(Redirect::to("/network"), "Activated WiFi client"), + Err(_) => Flash::error(Redirect::to("/network"), "Failed to activate WiFi client"), + } +} + +#[get("/network/wifi/add")] +pub fn network_add_wifi(flash: Option) -> Template { + let mut context = NetworkContext::build(); + // set back icon link to network route + context.back = Some("/network".to_string()); + context.title = Some("Add WiFi Network".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.name().to_string()); + context.flash_msg = Some(flash.msg().to_string()); + }; + // template_dir is set in Rocket.toml + Template::render("network_add", &context) +} + +#[get("/network/wifi/add?")] +pub fn network_add_ssid(ssid: &RawStr, flash: Option) -> Template { + // decode ssid from url + let decoded_ssid = percent_decode(ssid.as_bytes()).decode_utf8().unwrap(); + let mut context = NetworkAddContext::build(); + context.back = Some("/network/wifi".to_string()); + context.selected = Some(decoded_ssid.to_string()); + context.title = Some("Add WiFi Network".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.name().to_string()); + context.flash_msg = Some(flash.msg().to_string()); + }; + // template_dir is set in Rocket.toml + Template::render("network_add", &context) +} + +#[post("/network/wifi/add", data = "")] +pub fn add_credentials(wifi: Form) -> Template { + // check if the credentials already exist for this access point + // note: this is nicer but it's an unstable feature: + // if check_saved_aps(&wifi.ssid).contains(true) + // use unwrap_or instead, set value to false if err is returned + let creds_exist = network_client::saved_ap(&wifi.ssid).unwrap_or(false); + if creds_exist { + let mut context = NetworkAddContext::build(); + context.back = Some("/network".to_string()); + context.flash_name = Some("error".to_string()); + context.flash_msg = + Some("Network credentials already exist for this access point".to_string()); + context.title = Some("Add WiFi Network".to_string()); + // return early from handler with "creds already exist" message + return Template::render("network_add", &context); + }; + + // if credentials not found, generate and write wifi config to wpa_supplicant + match network_client::add(&wifi.ssid, &wifi.pass) { + Ok(_) => { + debug!("Added WiFi credentials."); + // force reread of wpa_supplicant.conf file with new credentials + match network_client::reconfigure() { + Ok(_) => debug!("Successfully reconfigured wpa_supplicant"), + Err(_) => warn!("Failed to reconfigure wpa_supplicant"), + } + let mut context = NetworkAddContext::build(); + context.back = Some("/network".to_string()); + context.flash_name = Some("success".to_string()); + context.flash_msg = Some("Added WiFi credentials".to_string()); + context.title = Some("Add WiFi Network".to_string()); + Template::render("network_add", &context) + } + Err(_) => { + debug!("Failed to add WiFi credentials."); + let mut context = NetworkAddContext::build(); + context.back = Some("/network".to_string()); + context.flash_name = Some("error".to_string()); + context.flash_msg = Some("Failed to add WiFi credentials".to_string()); + context.title = Some("Add WiFi Network".to_string()); + Template::render("network_add", &context) + } + } +} + +#[get("/network/wifi/usage")] +pub fn wifi_usage(flash: Option) -> Template { + let mut context = NetworkAlertContext::build(); + // set back icon link to network route + context.back = Some("/network".to_string()); + context.title = Some("Network Data Usage".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.name().to_string()); + context.flash_msg = Some(flash.msg().to_string()); + }; + // template_dir is set in Rocket.toml + Template::render("network_usage", &context) +} + +#[post("/network/wifi/usage", data = "")] +pub fn wifi_usage_alerts(thresholds: Form) -> Flash { + match monitor::update_store(thresholds.into_inner()) { + Ok(_) => { + debug!("WiFi data usage thresholds updated."); + Flash::success( + Redirect::to("/network/wifi/usage"), + "Updated alert thresholds and flags", + ) + } + Err(_) => { + warn!("Failed to update WiFi data usage thresholds."); + Flash::error( + Redirect::to("/network/wifi/usage"), + "Failed to update alert thresholds and flags", + ) + } + } +} + +#[get("/network/dns")] +pub fn configure_dns(flash: Option) -> Template { + let mut context = ConfigureDNSContext::build(); + // set back icon link to network route + context.back = Some("/network".to_string()); + context.title = Some("Configure DNS".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.name().to_string()); + context.flash_msg = Some(flash.msg().to_string()); + }; + Template::render("configure_dns", &context) +} + +#[post("/network/dns", data = "")] +pub fn configure_dns_post(dns: Form) -> Template { + let result = save_dns_configuration(dns.into_inner()); + match result { + Ok(_) => { + let mut context = ConfigureDNSContext::build(); + // set back icon link to network route + context.back = Some("/network".to_string()); + context.title = Some("Configure DNS".to_string()); + context.flash_name = Some("success".to_string()); + context.flash_msg = Some("New dynamic dns configuration is now enabled".to_string()); + Template::render("configure_dns", &context) + } + Err(err) => { + let mut context = ConfigureDNSContext::build(); + // set back icon link to network route + context.back = Some("/network".to_string()); + context.title = Some("Configure DNS".to_string()); + context.flash_name = Some("error".to_string()); + context.flash_msg = Some(format!("Failed to save dns configurations: {}", err)); + Template::render("configure_dns", &context) + } + } +} + +/// this change password route is used by a user who is already logged in +#[get("/settings/change_password")] +pub fn change_password(flash: Option) -> Template { + let mut context = ChangePasswordContext::build(); + // set back icon link to network route + context.back = Some("/network".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.name().to_string()); + context.flash_msg = Some(flash.msg().to_string()); + }; + Template::render("password/change_password", &context) +} + +/// this change password route is used by a user who is already logged in +#[post("/settings/change_password", data = "")] +pub fn change_password_post(password_form: Form) -> 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("/network".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("password/change_password", &context) + } + Err(err) => { + let mut context = ChangePasswordContext::build(); + // set back icon link to network route + context.back = Some("/network".to_string()); + context.title = Some("Configure DNS".to_string()); + context.flash_name = Some("error".to_string()); + context.flash_msg = Some(format!("Failed to save new password: {}", err)); + Template::render("password/change_password", &context) + } + } +} + +/// this reset password route is used by a user who is not logged in +/// and is specifically for users who have forgotten their password +/// all routes under /public/* are excluded from nginx basic auth via the nginx config +#[get("/reset_password")] +pub fn reset_password(flash: Option) -> 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.name().to_string()); + context.flash_msg = Some(flash.msg().to_string()); + }; + Template::render("password/reset_password", &context) +} + +/// this reset password route is used by a user who is not logged in +/// and is specifically for users who have forgotten their password +/// and is excluded from nginx basic auth via the nginx config +#[post("/reset_password", data = "")] +pub fn reset_password_post(reset_password_form: Form) -> 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("password/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("password/reset_password", &context) + } + } +} + +/// this route is used by a user who is not logged in to send a new password reset link +#[get("/send_password_reset")] +pub fn send_password_reset_page(flash: Option) -> 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.name().to_string()); + context.flash_msg = Some(flash.msg().to_string()); + }; + Template::render("password/send_password_reset", &context) +} + +/// this send_password_reset route is used by a user who is not logged in +/// and is specifically for users who have forgotten their password +#[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("password/send_password_reset", &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("password/send_password_reset", &context) + } + } +} + +/// this is a route for viewing and deleting currently configured admin +#[get("/settings/configure_admin")] +pub fn configure_admin(flash: Option) -> Template { + let mut context = ConfigureAdminContext::build(); + // set back icon link to network route + context.back = Some("/network".to_string()); + context.title = Some("Configure Admin".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.name().to_string()); + context.flash_msg = Some(flash.msg().to_string()); + }; + Template::render("admin/configure_admin", &context) +} + +#[get("/settings/admin/add")] +pub fn add_admin(flash: Option) -> Template { + let mut context = AddAdminContext::build(); + context.back = Some("/settings/configure_admin".to_string()); + context.title = Some("Add Admin".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.name().to_string()); + context.flash_msg = Some(flash.msg().to_string()); + }; + // template_dir is set in Rocket.toml + Template::render("admin/add_admin", &context) +} + +#[post("/settings/admin/add", data = "")] +pub fn add_admin_post(add_admin_form: Form) -> Flash { + let result = save_add_admin_form(add_admin_form.into_inner()); + let url = uri!(configure_admin); + match result { + Ok(_) => Flash::success(Redirect::to(url), "Successfully added new admin"), + Err(_) => Flash::error(Redirect::to(url), "Failed to add new admin"), + } +} + +#[post("/settings/admin/delete", data = "")] +pub fn delete_admin_post(delete_admin_form: Form) -> Flash { + let result = config_manager::delete_ssb_admin_id(&delete_admin_form.ssb_id); + let url = uri!(configure_admin); + match result { + Ok(_) => Flash::success(Redirect::to(url), "Successfully removed admin id"), + Err(_) => Flash::error(Redirect::to(url), "Failed to remove admin id"), + } +} + +#[get("/network/wifi/usage/reset")] +pub fn wifi_usage_reset() -> Flash { + let url = uri!(wifi_usage); + match monitor::reset_data() { + Ok(_) => Flash::success(Redirect::to(url), "Reset stored network traffic total"), + Err(_) => Flash::error( + Redirect::to(url), + "Failed to reset stored network traffic total", + ), + } +} + +#[post("/network/wifi/connect", data = "")] +pub fn connect_wifi(network: Form) -> Flash { + let ssid = &network.ssid; + let url = uri!(network_detail: ssid); + match network_client::id("wlan0", ssid) { + Ok(id) => match network_client::connect(&id, "wlan0") { + Ok(_) => Flash::success(Redirect::to(url), "Connected to chosen network"), + Err(_) => Flash::error(Redirect::to(url), "Failed to connect to chosen network"), + }, + Err(_) => Flash::error(Redirect::to(url), "Failed to retrieve the network ID"), + } +} + +#[post("/network/wifi/disconnect", data = "")] +pub fn disconnect_wifi(network: Form) -> Flash { + let ssid = &network.ssid; + let url = uri!(network_home); + match network_client::disable("wlan0", ssid) { + Ok(_) => Flash::success(Redirect::to(url), "Disconnected from WiFi network"), + Err(_) => Flash::error(Redirect::to(url), "Failed to disconnect from WiFi network"), + } +} + +#[post("/network/wifi/forget", data = "")] +pub fn forget_wifi(network: Form) -> Flash { + let ssid = &network.ssid; + let url = uri!(network_home); + match network_client::forget("wlan0", ssid) { + Ok(_) => Flash::success(Redirect::to(url), "WiFi credentials removed"), + Err(_) => Flash::error( + Redirect::to(url), + "Failed to remove WiFi credentials".to_string(), + ), + } +} + +#[get("/network/wifi/modify?")] +pub fn wifi_password(ssid: &RawStr, flash: Option) -> Template { + // decode ssid from url + let decoded_ssid = percent_decode(ssid.as_bytes()).decode_utf8().unwrap(); + let mut context = NetworkAddContext { + back: Some("/network/wifi".to_string()), + flash_name: None, + flash_msg: None, + selected: Some(decoded_ssid.to_string()), + title: Some("Update WiFi 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.name().to_string()); + context.flash_msg = Some(flash.msg().to_string()); + }; + // template_dir is set in Rocket.toml + Template::render("network_modify", &context) +} + +#[post("/network/wifi/modify", data = "")] +pub fn wifi_set_password(wifi: Form) -> Flash { + let ssid = &wifi.ssid; + let pass = &wifi.pass; + let url = uri!(network_detail: ssid); + match network_client::update("wlan0", ssid, pass) { + Ok(_) => Flash::success(Redirect::to(url), "WiFi password updated".to_string()), + Err(_) => Flash::error( + Redirect::to(url), + "Failed to update WiFi password".to_string(), + ), + } +} + +#[get("/messages")] +pub fn messages(flash: Option) -> Template { + let mut context = MessageContext::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.name().to_string()); + context.flash_msg = Some(flash.msg().to_string()); + }; + Template::render("messages", &context) +} + +#[get("/peers")] +pub fn peers(flash: Option) -> 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.name().to_string()); + context.flash_msg = Some(flash.msg().to_string()); + }; + Template::render("peers", &context) +} + +#[get("/profile")] +pub fn profile(flash: Option) -> 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.name().to_string()); + context.flash_msg = Some(flash.msg().to_string()); + }; + Template::render("profile", &context) +} + +#[get("/shutdown")] +pub fn shutdown_menu(flash: Option) -> Template { + let mut context = ShutdownContext::build(); + context.back = Some("/".to_string()); + context.title = Some("Shutdown Device".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.name().to_string()); + context.flash_msg = Some(flash.msg().to_string()); + }; + Template::render("shutdown", &context) +} + +#[get("/", rank = 2)] +pub fn files(file: PathBuf) -> Option { + NamedFile::open(Path::new("static/").join(file)).ok() +} + +#[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("not_found", context) +} + +#[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("internal_error", context) +} diff --git a/peach-web/src/tests.rs b/peach-web/src/tests.rs new file mode 100644 index 0000000..02a3c65 --- /dev/null +++ b/peach-web/src/tests.rs @@ -0,0 +1,484 @@ +use std::fs::File; +use std::io::Read; + +use rocket::http::{ContentType, Status}; +use rocket::local::Client; + +use super::rocket; +use crate::json_api::build_json_response; + +// helper function to test correct retrieval and content of a file +fn test_query_file(path: &str, file: T, status: Status) +where + T: Into>, +{ + let client = Client::new(rocket()).unwrap(); + let mut response = client.get(path).dispatch(); + assert_eq!(response.status(), status); + + let body_data = response.body().and_then(|body| body.into_bytes()); + if let Some(filename) = file.into() { + let expected_data = read_file_content(filename); + assert!(body_data.map_or(false, |s| s == expected_data)); + } +} + +// helper function to return the content of a file, given a path +fn read_file_content(path: &str) -> Vec { + let mut fp = File::open(&path).expect(&format!("Can't open {}", path)); + let mut file_content = vec![]; + + fp.read_to_end(&mut file_content) + .expect(&format!("Reading {} failed.", path)); + file_content +} + +// WEB PAGE ROUTES + +#[test] +fn index_html() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let mut response = client.get("/").dispatch(); + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.content_type(), Some(ContentType::HTML)); + let body = response.body_string().unwrap(); + assert!(body.contains("/peers")); + assert!(body.contains("/profile")); + assert!(body.contains("/messages")); + assert!(body.contains("/device")); + assert!(body.contains("/help")); + assert!(body.contains("/network")); +} + +#[test] +fn network_card_html() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let mut response = client.get("/network").dispatch(); + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.content_type(), Some(ContentType::HTML)); + let body = response.body_string().unwrap(); + assert!(body.contains("MODE")); + assert!(body.contains("SSID")); + assert!(body.contains("IP")); + assert!(body.contains("Add WiFi Network")); + assert!(body.contains("Deploy Access Point")); + assert!(body.contains("List WiFi Networks")); + assert!(body.contains("SIGNAL")); + assert!(body.contains("DOWNLOAD")); + assert!(body.contains("UPLOAD")); +} + +#[test] +fn network_list_html() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let mut response = client.get("/network/wifi").dispatch(); + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.content_type(), Some(ContentType::HTML)); + let body = response.body_string().unwrap(); + assert!(body.contains("WiFi Networks")); + assert!(body.contains("No saved or available networks found.")); +} + +// TODO: needs further testing once template has been refactored +#[test] +fn network_detail_html() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let response = client.get("/network/wifi?ssid=Home").dispatch(); + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.content_type(), Some(ContentType::HTML)); + //let body = response.body_string().unwrap(); + //assert!(body.contains("Network not found")); +} + +#[test] +fn network_add_html() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let mut response = client.get("/network/wifi/add").dispatch(); + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.content_type(), Some(ContentType::HTML)); + let body = response.body_string().unwrap(); + assert!(body.contains("Add WiFi Network")); + assert!(body.contains("SSID")); + assert!(body.contains("Password")); + assert!(body.contains("Add")); + assert!(body.contains("Cancel")); +} + +#[test] +fn network_add_ssid_html() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let mut response = client.get("/network/wifi/add?ssid=Home").dispatch(); + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.content_type(), Some(ContentType::HTML)); + let body = response.body_string().unwrap(); + assert!(body.contains("Add WiFi Network")); + assert!(body.contains("Home")); + assert!(body.contains("Password")); + assert!(body.contains("Add")); + assert!(body.contains("Cancel")); +} + +#[test] +fn device_html() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let mut response = client.get("/device").dispatch(); + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.content_type(), Some(ContentType::HTML)); + let body = response.body_string().unwrap(); + assert!(body.contains("Device Status")); + assert!(body.contains("Networking")); + assert!(body.contains("Display")); + assert!(body.contains("Statistics")); +} + +#[test] +fn help_html() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let mut response = client.get("/help").dispatch(); + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.content_type(), Some(ContentType::HTML)); + let body = response.body_string().unwrap(); + assert!(body.contains("Help")); +} + +#[test] +fn login_html() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let mut response = client.get("/login").dispatch(); + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.content_type(), Some(ContentType::HTML)); + let body = response.body_string().unwrap(); + assert!(body.contains("Login")); +} + +#[test] +fn messages_html() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let mut response = client.get("/messages").dispatch(); + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.content_type(), Some(ContentType::HTML)); + let body = response.body_string().unwrap(); + assert!(body.contains("Private Messages")); +} + +#[test] +fn peers_html() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let mut response = client.get("/peers").dispatch(); + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.content_type(), Some(ContentType::HTML)); + let body = response.body_string().unwrap(); + assert!(body.contains("Scuttlebutt Peers")); +} + +#[test] +fn profile_html() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let mut response = client.get("/profile").dispatch(); + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.content_type(), Some(ContentType::HTML)); + let body = response.body_string().unwrap(); + assert!(body.contains("Profile")); +} + +#[test] +fn shutdown_html() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let mut response = client.get("/shutdown").dispatch(); + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.content_type(), Some(ContentType::HTML)); + let body = response.body_string().unwrap(); + assert!(body.contains("Shutdown Device")); +} + +#[test] +fn network_usage_html() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let mut response = client.get("/network/wifi/usage").dispatch(); + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.content_type(), Some(ContentType::HTML)); + let body = response.body_string().unwrap(); + assert!(body.contains("Network Data Usage")); + assert!(body.contains("WARNING THRESHOLD")); + assert!(body.contains("Update")); + assert!(body.contains("Cancel")); +} + +#[test] +fn add_credentials() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let response = client + .post("/network/wifi/add") + .header(ContentType::Form) + .body("ssid=Home&pass=Password") + .dispatch(); + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.content_type(), Some(ContentType::HTML)); +} + +#[test] +fn forget_wifi() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let response = client + .post("/network/wifi/forget") + .header(ContentType::Form) + .body("ssid=Home") + .dispatch(); + assert_eq!(response.status(), Status::SeeOther); + assert_eq!(response.content_type(), None); +} + +#[test] +fn modify_password() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let response = client + .post("/network/wifi/modify") + .header(ContentType::Form) + .body("ssid=Home&pass=Password") + .dispatch(); + assert_eq!(response.status(), Status::SeeOther); + assert_eq!(response.content_type(), None); +} + +#[test] +fn deploy_ap() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let response = client.get("/network/ap/activate").dispatch(); + // check for 303 status (redirect) + assert_eq!(response.status(), Status::SeeOther); + assert_eq!(response.content_type(), None); +} + +#[test] +fn deploy_client() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let response = client.get("/network/wifi/activate").dispatch(); + // check for 303 status (redirect) + assert_eq!(response.status(), Status::SeeOther); + assert_eq!(response.content_type(), None); +} + +// JSON API ROUTES + +#[test] +fn activate_ap() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let response = client + .post("/api/v1/network/activate_ap") + .header(ContentType::JSON) + .dispatch(); + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.content_type(), Some(ContentType::JSON)); +} + +#[test] +fn activate_client() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let response = client + .post("/api/v1/network/activate_client") + .header(ContentType::JSON) + .dispatch(); + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.content_type(), Some(ContentType::JSON)); +} + +#[test] +fn return_ip() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let mut response = client + .get("/api/v1/network/ip") + .header(ContentType::JSON) + .dispatch(); + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.content_type(), Some(ContentType::JSON)); + let body = response.body_string().unwrap(); + assert!(body.contains("wlan0")); + assert!(body.contains("ap0")); +} + +#[test] +fn return_rssi() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let mut response = client + .get("/api/v1/network/rssi") + .header(ContentType::JSON) + .dispatch(); + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.content_type(), Some(ContentType::JSON)); + let body = response.body_string().unwrap(); + assert!(body.contains("Not currently connected to an access point.")); +} + +#[test] +fn return_ssid() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let mut response = client + .get("/api/v1/network/ssid") + .header(ContentType::JSON) + .dispatch(); + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.content_type(), Some(ContentType::JSON)); + let body = response.body_string().unwrap(); + assert!(body.contains("Not currently connected to an access point.")); +} + +#[test] +fn return_state() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let mut response = client + .get("/api/v1/network/state") + .header(ContentType::JSON) + .dispatch(); + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.content_type(), Some(ContentType::JSON)); + let body = response.body_string().unwrap(); + assert!(body.contains("ap0")); + assert!(body.contains("wlan0")); + assert!(body.contains("unavailable")); +} + +#[test] +fn return_status() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let mut response = client + .get("/api/v1/network/status") + .header(ContentType::JSON) + .dispatch(); + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.content_type(), Some(ContentType::JSON)); + let body = response.body_string().unwrap(); + assert!(body.contains("Not currently connected to an access point.")); +} + +#[test] +fn scan_networks() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let mut response = client + .get("/api/v1/network/wifi") + .header(ContentType::JSON) + .dispatch(); + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.content_type(), Some(ContentType::JSON)); + let body = response.body_string().unwrap(); + assert!(body.contains("Unable to scan for networks. Interface may be deactivated.")); +} + +#[test] +fn add_wifi() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let mut response = client + .post("/api/v1/network/wifi") + .header(ContentType::JSON) + .body(r#"{ "ssid": "Home", "pass": "Password" }"#) + .dispatch(); + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.content_type(), Some(ContentType::JSON)); + let body = response.body_string().unwrap(); + assert!(body.contains("Failed to add WiFi credentials.")); +} + +#[test] +fn remove_wifi() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let mut response = client + .post("/api/v1/network/wifi/forget") + .header(ContentType::JSON) + .body(r#"{ "ssid": "Home" }"#) + .dispatch(); + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.content_type(), Some(ContentType::JSON)); + let body = response.body_string().unwrap(); + assert!(body.contains("Failed to remove WiFi network credentials.")); +} + +#[test] +fn new_password() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let mut response = client + .post("/api/v1/network/wifi/modify") + .header(ContentType::JSON) + .body(r#"{ "ssid": "Home", "pass": "Password" }"#) + .dispatch(); + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.content_type(), Some(ContentType::JSON)); + let body = response.body_string().unwrap(); + assert!(body.contains("Failed to update WiFi password.")); +} + +#[test] +fn ping_pong() { + let client = Client::new(rocket()).expect("valid rocket instance"); + let mut response = client + .get("/api/v1/ping") + .header(ContentType::JSON) + .dispatch(); + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.content_type(), Some(ContentType::JSON)); + let body = response.body_string().unwrap(); + assert!(body.contains("pong!")); +} + +// HELPER FUNCTION TESTS + +#[test] +fn test_build_json_response() { + let status = "success".to_string(); + let data = json!("WiFi credentials added.".to_string()); + let json = build_json_response(status, Some(data), None); + assert_eq!(json.status, "success"); + assert_eq!(json.data, Some(json!("WiFi credentials added."))); + assert_eq!(json.msg, None); +} + +// FILE TESTS + +#[test] +fn nested_file() { + test_query_file( + "/images/placeholder.txt", + "static/images/placeholder.txt", + Status::Ok, + ); + test_query_file( + "/images/placeholder.txt?v=1", + "static/images/placeholder.txt", + Status::Ok, + ); + test_query_file( + "/images/placeholder.txt?v=1&a=b", + "static/images/placeholder.txt", + Status::Ok, + ); +} + +#[test] +fn icon_file() { + test_query_file( + "/icons/peach-icon.png", + "static/icons/peach-icon.png", + Status::Ok, + ); +} + +#[test] +fn invalid_path() { + test_query_file("/thou_shalt_not_exist", None, Status::NotFound); + test_query_file("/thou_shalt_not_exist", None, Status::NotFound); + test_query_file("/thou/shalt/not/exist?a=b&c=d", None, Status::NotFound); +} + +#[test] +fn invalid_get_request() { + let client = Client::new(rocket()).unwrap(); + + // try to get a path that doesn't exist + let mut res = client + .get("/message/99") + .header(ContentType::JSON) + .dispatch(); + assert_eq!(res.status(), Status::NotFound); + + let body = res.body_string().unwrap(); + assert!(body.contains("404: Page Not Found")); + assert!(body.contains("No PeachCloud resource exists for this URL.")); +} diff --git a/peach-web/src/ws.rs b/peach-web/src/ws.rs new file mode 100644 index 0000000..50591a0 --- /dev/null +++ b/peach-web/src/ws.rs @@ -0,0 +1,65 @@ +// NOTE: websockets are not currently in use for PeachCloud but may be in the +// future. + +use std::io; +use std::thread; + +use log::{debug, info}; +use websocket::sync::Server; +use websocket::{Message, OwnedMessage}; + +pub fn websocket_server(address: String) -> io::Result<()> { + // Start listening for WebSocket connections + let ws_server = Server::bind(address)?; + + info!("Listening for WebSocket connections."); + for connection in ws_server.filter_map(Result::ok) { + // Spawn a new thread for each connection. + thread::spawn(move || { + if !connection + .protocols() + .contains(&"rust-websocket".to_string()) + { + connection.reject().unwrap(); + return; + } + + let mut client = connection.use_protocol("rust-websocket").accept().unwrap(); + + let client_ip = client.peer_addr().unwrap(); + + debug!("Websocket connection from {}.", client_ip); + + let msg_text = "Websocket successfully connected.".to_string(); + let message = Message::text(msg_text); + client.send_message(&message).unwrap(); + + let (mut receiver, mut sender) = client.split().unwrap(); + + for message in receiver.incoming_messages() { + let message = message.unwrap(); + + match message { + OwnedMessage::Close(_) => { + debug!("Received close message."); + let message = Message::close(); + sender.send_message(&message).unwrap(); + debug!("Websocket client {} disconnected.", client_ip); + return; + } + OwnedMessage::Ping(data) => { + debug!("Received ping message."); + let message = Message::pong(data); + sender.send_message(&message).unwrap(); + } + _ => { + sender.send_message(&message).unwrap(); + debug!("Received unknown message: {:?}", message); + } + } + } + }); + } + + Ok(()) +} diff --git a/peach-web/static/css/_variables.css b/peach-web/static/css/_variables.css new file mode 100644 index 0000000..45be819 --- /dev/null +++ b/peach-web/static/css/_variables.css @@ -0,0 +1,177 @@ +/* + + VARIABLES + +*/ + +@custom-media --breakpoint-not-small screen and (min-width: 30em); +@custom-media --breakpoint-medium screen and (min-width: 30em) and (max-width: 60em); +@custom-media --breakpoint-large screen and (min-width: 60em); + +:root { + + --sans-serif: -apple-system, BlinkMacSystemFont, 'avenir next', avenir, helvetica, 'helvetica neue', ubuntu, roboto, noto, 'segoe ui', arial, sans-serif; + --serif: georgia, serif; + --code: consolas, monaco, monospace; + + --font-size-headline: 6rem; + --font-size-subheadline: 5rem; + --font-size-1: 3rem; + --font-size-2: 2.25rem; + --font-size-3: 1.5rem; + --font-size-4: 1.25rem; + --font-size-5: 1rem; + --font-size-6: .875rem; + --font-size-7: .75rem; + + --letter-spacing-tight:-.05em; + --letter-spacing-1:.1em; + --letter-spacing-2:.25em; + + --line-height-solid: 1; + --line-height-title: 1.25; + --line-height-copy: 1.5; + + --measure: 30em; + --measure-narrow: 20em; + --measure-wide: 34em; + + --spacing-none: 0; + --spacing-extra-small: .25rem; + --spacing-small: .5rem; + --spacing-medium: 1rem; + --spacing-large: 2rem; + --spacing-extra-large: 4rem; + --spacing-extra-extra-large: 8rem; + --spacing-extra-extra-extra-large: 16rem; + --spacing-copy-separator: 1.5em; + + --height-1: 1rem; + --height-2: 2rem; + --height-3: 4rem; + --height-4: 8rem; + --height-5: 16rem; + + --width-1: 1rem; + --width-2: 2rem; + --width-3: 4rem; + --width-4: 8rem; + --width-5: 16rem; + + --max-width-1: 1rem; + --max-width-2: 2rem; + --max-width-3: 4rem; + --max-width-4: 8rem; + --max-width-5: 16rem; + --max-width-6: 32rem; + --max-width-7: 48rem; + --max-width-8: 64rem; + --max-width-9: 96rem; + + --border-radius-none: 0; + --border-radius-1: .125rem; + --border-radius-2: .25rem; + --border-radius-3: .5rem; + --border-radius-4: 1rem; + --border-radius-circle: 100%; + --border-radius-pill: 9999px; + + --border-width-none: 0; + --border-width-1: .125rem; + --border-width-2: .25rem; + --border-width-3: .5rem; + --border-width-4: 1rem; + --border-width-5: 2rem; + + --box-shadow-1: 0px 0px 4px 2px rgba( 0, 0, 0, 0.2 ); + --box-shadow-2: 0px 0px 8px 2px rgba( 0, 0, 0, 0.2 ); + --box-shadow-3: 2px 2px 4px 2px rgba( 0, 0, 0, 0.2 ); + --box-shadow-4: 2px 2px 8px 0px rgba( 0, 0, 0, 0.2 ); + --box-shadow-5: 4px 4px 8px 0px rgba( 0, 0, 0, 0.2 ); + + --black: #000; + --near-black: #111; + --dark-gray:#333; + --mid-gray:#555; + --gray: #777; + --silver: #999; + --light-silver: #aaa; + --moon-gray: #ccc; + --light-gray: #eee; + --near-white: #f4f4f4; + --white: #fff; + + --transparent: transparent; + + --black-90: rgba(0,0,0,.9); + --black-80: rgba(0,0,0,.8); + --black-70: rgba(0,0,0,.7); + --black-60: rgba(0,0,0,.6); + --black-50: rgba(0,0,0,.5); + --black-40: rgba(0,0,0,.4); + --black-30: rgba(0,0,0,.3); + --black-20: rgba(0,0,0,.2); + --black-10: rgba(0,0,0,.1); + --black-05: rgba(0,0,0,.05); + --black-025: rgba(0,0,0,.025); + --black-0125: rgba(0,0,0,.0125); + + --white-90: rgba(255,255,255,.9); + --white-80: rgba(255,255,255,.8); + --white-70: rgba(255,255,255,.7); + --white-60: rgba(255,255,255,.6); + --white-50: rgba(255,255,255,.5); + --white-40: rgba(255,255,255,.4); + --white-30: rgba(255,255,255,.3); + --white-20: rgba(255,255,255,.2); + --white-10: rgba(255,255,255,.1); + --white-05: rgba(255,255,255,.05); + --white-025: rgba(255,255,255,.025); + --white-0125: rgba(255,255,255,.0125); + + --dark-red: #e7040f; + --red: #ff4136; + --light-red: #ff725c; + --orange: #ff6300; + --gold: #ffb700; + --yellow: #ffd700; + --light-yellow: #fbf1a9; + --purple: #5e2ca5; + --light-purple: #a463f2; + --dark-pink: #d5008f; + --hot-pink: #ff41b4; + --pink: #ff80cc; + --light-pink: #ffa3d7; + --dark-green: #137752; + --green: #19a974; + --light-green: #9eebcf; + --navy: #001b44; + --dark-blue: #00449e; + --blue: #357edd; + --light-blue: #96ccff; + --lightest-blue: #cdecff; + --washed-blue: #f6fffe; + --washed-green: #e8fdf5; + --washed-yellow: #fffceb; + --washed-red: #ffdfdf; + +/* PEACHCLOUD-SPECIFIC VARIABLES */ + + --primary: var(--light-green); + --secondary: var(--near-white); + --success: var(--green); + --info: var(--blue); + --warning: var(--orange); + --danger: var(--red); + --light: var(--light-gray); + --dark: var(--near-black); + +/* we need to add shades for each accent colour + * + * --info-100 + * --info-200 + * --info-300 -> var(--blue) + * --info-400 + * --info-500 + */ +} diff --git a/peach-web/static/css/peachcloud.css b/peach-web/static/css/peachcloud.css new file mode 100644 index 0000000..1b2e722 --- /dev/null +++ b/peach-web/static/css/peachcloud.css @@ -0,0 +1,908 @@ +@import url('_variables.css'); + +/* ------------------------------ *\ + * peachcloud.css + * + * Index + * - ALIGNMENT + * - BUTTONS + * - CARDS + * - CAPSULES + * - CIRCLES + * - COLORS + * - GRIDS + * - HTML + * - FLASH MESSAGE + * - FONTS + * - ICONS + * - INPUTS + * - LABELS + * - LINKS + * - LISTS + * - METERS + * - NAVIGATION + * - RADIAL MENU + * - SWITCHES / SLIDERS + * - PARAGRAPHS + * +\* ------------------------------ */ + +/* + * ALIGNMENT + */ + +.center { + display: block; + margin-left: auto; + margin-right: auto; +} + +.center-text { + text-align: center; +} + +.center-vert { + position: absolute; + top: 50%; + transform: translateY(-50%); +} + +.push-right { + margin-left: auto; + padding-right: 0; +} + +.top-left { + /* place-self combines align-self and justify-self */ + place-self: end center; +} + +@media only screen and (min-width: 600px) { + .top-left { + place-self: end; + } +} + +.top-right { + place-self: end center; +} + +@media only screen and (min-width: 600px) { + .top-right { + place-self: end start; + } +} + +.top-middle { + place-self: center; +} + +@media only screen and (min-width: 600px) { + .top-middle { + padding-bottom: 2rem; + place-self: center; + } +} + +.middle { + place-self: center; + grid-column-start: 1; + grid-column-end: 4; +} + +.bottom-middle { + place-self: center; +} + +@media only screen and (min-width: 600px) { + .bottom-middle { + padding-top: 2rem; + place-self: center; + } +} + +.bottom-left { + place-self: start center; +} + +@media only screen and (min-width: 600px) { + .bottom-left { + place-self: start end; + } +} + +.bottom-right { + place-self: start center; +} + +@media only screen and (min-width: 600px) { + .bottom-right { + place-self: start; + } +} + +/* + * BUTTONS + */ + +.button { + border: 1px solid var(--near-black); + border-radius: var(--border-radius-2); + /* Needed to render inputs & buttons of equal width */ + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; + color: var(--near-black); + cursor: pointer; + padding: 10px; + text-align: center; + text-decoration: none; + font-size: var(--font-size-5); + font-family: var(--sans-serif); + width: 80%; + margin-top: 5px; + margin-bottom: 5px; +} + +.button.full-width { + width: 100%; +} + +.button-div { + grid-column-start: 1; + grid-column-end: 4; + margin-bottom: 1rem; +} + +.button-primary { + background-color: var(--light-gray); +} + +.button-primary:hover { + background-color: var(--primary); +} + +.button-primary:focus { + background-color: var(--primary); + outline: none; +} + +.button-secondary { + background-color: var(--light-gray); +} + +.button-secondary:hover { + background-color: var(--light-silver); +} + +.button-secondary:focus { + background-color: var(--light-silver); + outline: none; +} + +.button-warning { + background-color: var(--light-gray); +} + +.button-warning:hover { + background-color: var(--light-red); +} + +.button-warning:focus { + background-color: var(--light-red); + outline: none; +} + +/* + * CAPSULES + */ + +.capsule { + padding: 1rem; + border: var(--border-width-1) solid; + border-radius: var(--border-radius-3); + background-color: var(--light-gray); + margin-top: 1rem; + margin-bottom: 1rem; +} + +/* + * CARDS + */ + +.card { + min-height: 50vh; + max-height: 90vh; + position: relative; + width: 100%; +} + +@media only screen and (min-width: 600px) { + .card { + min-height: 50vh; + max-height: 90vh; + width: 20rem; + } +} + +.card-container { + justify-content: center; + padding-top: 1rem; + padding-bottom: 1rem; +} + +.form-container { + justify-content: center; + padding-top: 1rem; + padding-bottom: 1rem; + width: 80%; + margin: auto; +} + +.text-container { + width: 80%; + margin: auto; +} + +.card-text { + margin: 0; + font-size: var(--font-size-5); + padding-bottom: 0.3rem; +} + +.container { + display: grid; + grid-template-columns: 2fr 5fr 2fr; + grid-template-rows: auto; + grid-row-gap: 1rem; + align-items: center; + justify-items: center; + margin-bottom: 1rem; + margin-top: 1rem; +} + +/* + * CIRCLES + */ + +.circle { + align-items: center; + border-radius: 50%; + box-shadow: var(--box-shadow-3); + display: flex; + justify-content: center; + position: relative; +} + +.circle-small { + height: 5rem; + width: 5rem; +} + +.circle-medium { + height: 8rem; + width: 8rem; +} + +.circle-large { + height: 13rem; + width: 13rem; +} + +.circle-success { + background-color: var(--success); + color: var(--white); + font-size: var(--font-size-4); +} + +.circle-warning { + background-color: var(--warning); + color: var(--white); + font-size: var(--font-size-4); +} + +.circle-error { + background-color: var(--danger); + color: var(--white); + font-size: var(--font-size-4); +} + +/* quartered-circle: circle for the center of radial-menu */ + +.quartered-circle { + width: 100px; + height: 100px; +} + +.quarter { + width: 50%; + height: 50%; +} + +.quarter-link { + left: 50%; + margin: -2em; + top: 50%; +} + +.quarter-icon { + position: absolute; + bottom: 1em; + left: 1.5em; +} + +/* + * COLORS + */ + +.primary-bg { + background-color: var(--primary); +} + +.secondary-bg { + background-color: var(--secondary); +} + +.success-bg { + background-color: var(--success); +} + +.info-bg { + background-color: var(--info); +} + +.warning-bg { + background-color: var(--warning); +} + +.danger-bg { + background-color: var(--danger); +} + +.light-bg { + background-color: var(--light); +} + +.primary-border { + border-color: var(--primary); +} + +.success-border { + border-color: var(--success); +} + +.info-border { + border-color: var(--info); +} + +.warning-border { + border-color: var(--warning); +} + +.danger-border { + border-color: var(--danger); +} + +.dark-gray-border { + border-color: var(--dark-gray); +} + +/* + * GRIDS + */ + +.grid { + display: grid; + grid-template-columns: 2fr 1fr 2fr; + grid-template-rows: 2fr 1fr 2fr; + height: 80vh; +} + +.flex-grid { + display: flex; + align-content: space-between; + align-items: baseline; + justify-content: center; +} + +.two-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + grid-template-rows: auto; + align-items: center; + justify-content: center; + justify-items: center; + margin-right: 2rem; + margin-left: 2rem; + padding-top: 1.5rem; + padding-bottom: 1rem; +} + +.three-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-template-rows: auto; + grid-gap: 10px; + align-items: center; + justify-content: center; +} + +.stack { + display: grid; + align-items: flex-end; + justify-items: center; + justify-content: center; +} + +.three-grid-icon-1 { + align-self: center; + grid-column: 1; + grid-row: 1; + justify-self: center; + margin-bottom: 10px; + max-width: 55%; + text-align: center; +} + +.three-grid-icon-2 { + align-self: center; + grid-column: 2; + grid-row: 1; + justify-self: center; + margin-bottom: 10px; + max-width: 55%; + text-align: center; +} + +.three-grid-icon-3 { + align-self: center; + grid-column: 3; + grid-row: 1; + justify-self: center; + margin-bottom: 10px; + max-width: 55%; + text-align: center; +} + +.three-grid-label-1 { + align-self: center; + grid-column: 1; + grid-row: 1; + justify-self: center; + text-align: center; +} + +.three-grid-label-2 { + align-self: center; + grid-column: 2; + grid-row: 1; + justify-self: center; + text-align: center; +} + +.three-grid-label-3 { + align-self: center; + grid-column: 3; + grid-row: 1; + justify-self: center; + text-align: center; +} + +.grid-column-1 { + grid-column: 1; +} + +.grid-column-2 { + grid-column: 2; + justify-self: left; +} + +.grid-column-3 { + grid-column: 3; +} + +/* + * HTML + */ + +/* Push bottom nav bar to bottom of screen on larger displays */ +@media only screen and (min-width: 600px) { + html { + height: 100%; + } +} + +/* + * FLASH MESSAGE + */ + +.flash-message { + font-family: var(--sans-serif); + font-size: var(--font-size-6); + margin-left: 2rem; + margin-right: 2rem; +} + +/* + * FONTS + */ + +.font-near-black { + color: var(--near-black); +} + +.font-gray { + color: var(--mid-gray); +} + +.font-light-gray { + color: var(--silver); +} + +.font-success { + color: var(--success); +} + +.font-warning { + color: var(--warning); +} + +.font-failure { + color: var(--danger); +} + +/* + * ICONS + */ + +.icon { + width: 3rem; +} + +.icon-small { + width: 1rem; +} + +.icon-medium { + width: 2rem; +} + +.icon-large { + width: 3rem; +} + +.icon-100 { + width: 100%; +} + +/* icon-active: sets color of icon svg to near-black */ +.icon-active { + filter: invert(0%) sepia(1%) saturate(4171%) hue-rotate(79deg) brightness(86%) contrast(87%); +} + +/* icon-inactive: sets color of icon svg to gray */ +.icon-inactive { + filter: invert(72%) sepia(8%) saturate(14%) hue-rotate(316deg) brightness(93%) contrast(92%); +} + +/* + * INPUTS + */ + +.input { + /* Needed to render inputs & buttons of equal width */ + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; + margin-top: 0.5rem; + margin-bottom: 1rem; + padding-left: 5px; + line-height: 1.5rem; + width: 80%; +} + +.form-input { + margin-bottom: 0; + margin-left: 0px; + border: 0px; + padding-left: 5px; + line-height: 1.5rem; + width: 100%; +} + +.alert-input { + /* Needed to render inputs & buttons of equal width */ + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; + margin-right: 0.25rem; + padding-right: 0.25rem; + text-align: right; + width: 7rem; +} + +.input-wrapper { + margin-bottom: 15px; +} + +/* + * LABELS + */ + +.label-small { + font-family: var(--sans-serif); + font-size: var(--font-size-7); + display: block; +} + +.label-medium { + font-size: var(--font-size-3); + display: block; +} + +.label-large { + font-size: var(--font-size-2); + display: block; +} + +.input-label { + margin-bottom: 0.4rem; +} + +/* + * LINKS + */ + +.link { + text-decoration: none; + color: var(--font-near-black); +} + +/* + * LISTS + */ + +.list { + padding-left: 0; + margin-left: 0; + max-width: var(--max-width-6); + border: 1px solid var(--light-silver); + border-radius: var(--border-radius-2); + list-style-type: none; + font-family: var(--sans-serif); +} + +.list-container { + width: var(--max-width-5); +} + +.list-icon { + align-self: center; + justify-self: right; + grid-column: 2; + grid-row: 1/3; +} + +.list-item { + display: grid; + padding: 1rem; + border-bottom-color: var(--light-silver); + border-bottom-style: solid; + border-bottom-width: 1px; +} + +.list-text { + justify-self: left; + grid-column: 1; + grid-row: 1; + margin: 0; + font-size: var(--font-size-5); +} + +.list-label { + justify-self: left; + grid-column: 1; + grid-row: 2; +} + +/* + * METERS + */ + +meter { + border: 1px solid #ccc; + border-radius: 3px; + display: block; + /* height: 1rem; */ + margin: 0 auto; + margin-bottom: 1rem; + width: 100%; + /* remove default styling */ + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + + /* Firefox */ + background: none; /* remove default background */ + background-color: var(--near-white); + box-shadow: 0 5px 5px -5px #333 inset; +} + +meter::-webkit-meter-bar { + background: none; /* remove default background */ + background-color: var(--near-white); + box-shadow: 0 5px 5px -5px #333 inset; +} + +meter::-webkit-meter-optimum-value { + background-size: 100% 100%; + box-shadow: 0 5px 5px -5px #999 inset; + transition: width .5s; +} + +/* Firefox styling */ +meter::-moz-meter-bar { + background: var(--mid-gray); + background-size: 100% 100%; + box-shadow: 0 5px 5px -5px #999 inset; +} + +.meter-gauge { + background-color: var(--near-white); + border: 1px solid #ccc; + border-radius: 3px; + box-shadow: 0 5px 5px -5px #333 inset; + display: block; +} + +/* Chrome styling */ +.meter-gauge > span { + background: var(--mid-gray); + background-size: 100% 100%; + box-shadow: 0 5px 5px -5px #999 inset; + display: block; + height: inherit; + text-indent: -9999px; +} + +/* + * NAVIGATION + */ + +.nav-bar { + background-color: var(--moon-gray); + display: flex; + align-items: center; + width: 100%; + height: 2em; + padding-top: 1rem; + padding-bottom: 1rem; + justify-content: space-between; +} + +.nav-title { + font-family: var(--sans-serif); + font-size: var(--font-size-4); + font-weight: normal; + margin: 0; +} + +.nav-icon { + width: auto; + height: 90%; + cursor: pointer; +} + +.nav-icon-left { + float: left; + padding-left: 10px; +} + +.nav-icon-right { + float: right; + padding-right: 10px; +} + +.nav-item { + display: inline-block; + list-style-type: none; +} + +/* + * SWITCHES / SLIDERS + */ + +/* switch: the box around the slider */ +.switch { + display: inline-block; + height: 34px; + position: relative; + width: 60px; +} + +/* hide default HTML checkbox */ +.switch input { + height: 0; + opacity: 0; + width: 0; +} + +.switch-icon-left { + align-self: center; + grid-column: 1; + grid-row: 1; + justify-self: center; +} + +.switch-icon-right { + align-self: center; + grid-column: 3; + grid-row: 1; + justify-self: center; +} + +.slider { + background-color: var(--moon-gray); + bottom: 0; + cursor: pointer; + left: 0; + position: absolute; + right: 0; + top: 0; + transition: .4s; + -webkit-transition: .4s; +} + +.slider:before { + background-color: var(--white); + bottom: 4px; + content: ""; + height: 26px; + left: 4px; + position: absolute; + transition: .4s; + -webkit-transition: .4s; + width: 26px; +} + +input:checked + .slider { + background-color: var(--near-black); +} + +input:focus + .slider { + box-shadow: 0 0 1px var(--near-black); +} + +input:checked + .slider:before { + -ms-transform: translateX(26px); + transform: translateX(26px); + -webkit-transform: translateX(26px); +} + +.slider.round { + border-radius: 34px; +} + +.slider.round:before { + border-radius: 50%; +} + +/* + * TITLES + */ + +.title-medium { + font-size: var(--font-size-4); + font-family: var(--sans-serif); + max-width: var(--max-width-6); +} + +/* + * PARAGRAPHS + */ + +p { + font-family: var(--sans-serif); + overflow-wrap: anywhere; +} + diff --git a/peach-web/static/favicon.ico b/peach-web/static/favicon.ico new file mode 100644 index 0000000..a7f0efe Binary files /dev/null and b/peach-web/static/favicon.ico differ diff --git a/peach-web/static/icons/alert.svg b/peach-web/static/icons/alert.svg new file mode 100644 index 0000000..af2e185 --- /dev/null +++ b/peach-web/static/icons/alert.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/peach-web/static/icons/back.svg b/peach-web/static/icons/back.svg new file mode 100644 index 0000000..4d26923 --- /dev/null +++ b/peach-web/static/icons/back.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/peach-web/static/icons/book.svg b/peach-web/static/icons/book.svg new file mode 100644 index 0000000..cc4892e --- /dev/null +++ b/peach-web/static/icons/book.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/peach-web/static/icons/chart.svg b/peach-web/static/icons/chart.svg new file mode 100644 index 0000000..15b0f54 --- /dev/null +++ b/peach-web/static/icons/chart.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/peach-web/static/icons/cloud-disconnected.svg b/peach-web/static/icons/cloud-disconnected.svg new file mode 100644 index 0000000..6500a87 --- /dev/null +++ b/peach-web/static/icons/cloud-disconnected.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/peach-web/static/icons/cloud.svg b/peach-web/static/icons/cloud.svg new file mode 100644 index 0000000..3caeae6 --- /dev/null +++ b/peach-web/static/icons/cloud.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/peach-web/static/icons/cog.svg b/peach-web/static/icons/cog.svg new file mode 100644 index 0000000..4b4948a --- /dev/null +++ b/peach-web/static/icons/cog.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/peach-web/static/icons/devices.svg b/peach-web/static/icons/devices.svg new file mode 100644 index 0000000..8cbf252 --- /dev/null +++ b/peach-web/static/icons/devices.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/peach-web/static/icons/down-arrow.svg b/peach-web/static/icons/down-arrow.svg new file mode 100644 index 0000000..76e238b --- /dev/null +++ b/peach-web/static/icons/down-arrow.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/peach-web/static/icons/enter.svg b/peach-web/static/icons/enter.svg new file mode 100644 index 0000000..21ddd12 --- /dev/null +++ b/peach-web/static/icons/enter.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/peach-web/static/icons/envelope.svg b/peach-web/static/icons/envelope.svg new file mode 100644 index 0000000..f030873 --- /dev/null +++ b/peach-web/static/icons/envelope.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/peach-web/static/icons/exit.svg b/peach-web/static/icons/exit.svg new file mode 100644 index 0000000..b54f4b5 --- /dev/null +++ b/peach-web/static/icons/exit.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/peach-web/static/icons/glyph.png b/peach-web/static/icons/glyph.png new file mode 100644 index 0000000..dac03ff Binary files /dev/null and b/peach-web/static/icons/glyph.png differ diff --git a/peach-web/static/icons/heart-pulse.svg b/peach-web/static/icons/heart-pulse.svg new file mode 100644 index 0000000..62e0c08 --- /dev/null +++ b/peach-web/static/icons/heart-pulse.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/peach-web/static/icons/hermies.png b/peach-web/static/icons/hermies.png new file mode 100644 index 0000000..6f8cbd8 Binary files /dev/null and b/peach-web/static/icons/hermies.png differ diff --git a/peach-web/static/icons/lcd.svg b/peach-web/static/icons/lcd.svg new file mode 100644 index 0000000..3af28bf --- /dev/null +++ b/peach-web/static/icons/lcd.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/peach-web/static/icons/low-signal.svg b/peach-web/static/icons/low-signal.svg new file mode 100644 index 0000000..9744065 --- /dev/null +++ b/peach-web/static/icons/low-signal.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/peach-web/static/icons/peach-icon.png b/peach-web/static/icons/peach-icon.png new file mode 100644 index 0000000..c8da1e6 Binary files /dev/null and b/peach-web/static/icons/peach-icon.png differ diff --git a/peach-web/static/icons/power-switch.svg b/peach-web/static/icons/power-switch.svg new file mode 100644 index 0000000..9f47372 --- /dev/null +++ b/peach-web/static/icons/power-switch.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/peach-web/static/icons/question-circle.svg b/peach-web/static/icons/question-circle.svg new file mode 100644 index 0000000..45e5929 --- /dev/null +++ b/peach-web/static/icons/question-circle.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/peach-web/static/icons/router.svg b/peach-web/static/icons/router.svg new file mode 100644 index 0000000..ed05d32 --- /dev/null +++ b/peach-web/static/icons/router.svg @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/peach-web/static/icons/scissor.svg b/peach-web/static/icons/scissor.svg new file mode 100644 index 0000000..5a1610c --- /dev/null +++ b/peach-web/static/icons/scissor.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/peach-web/static/icons/signal.svg b/peach-web/static/icons/signal.svg new file mode 100644 index 0000000..9744065 --- /dev/null +++ b/peach-web/static/icons/signal.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/peach-web/static/icons/smile.svg b/peach-web/static/icons/smile.svg new file mode 100644 index 0000000..13026dc --- /dev/null +++ b/peach-web/static/icons/smile.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/peach-web/static/icons/up-arrow.svg b/peach-web/static/icons/up-arrow.svg new file mode 100644 index 0000000..3e9f3c5 --- /dev/null +++ b/peach-web/static/icons/up-arrow.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/peach-web/static/icons/users.svg b/peach-web/static/icons/users.svg new file mode 100644 index 0000000..743ef13 --- /dev/null +++ b/peach-web/static/icons/users.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/peach-web/static/icons/wifi.svg b/peach-web/static/icons/wifi.svg new file mode 100644 index 0000000..3e8ab2e --- /dev/null +++ b/peach-web/static/icons/wifi.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/peach-web/static/images/placeholder.txt b/peach-web/static/images/placeholder.txt new file mode 100644 index 0000000..6fb1526 --- /dev/null +++ b/peach-web/static/images/placeholder.txt @@ -0,0 +1 @@ +This file is a temporary placeholder which is currently being used to allow testing of nested file retrieval. diff --git a/peach-web/static/index.html b/peach-web/static/index.html new file mode 100644 index 0000000..ca5cf11 --- /dev/null +++ b/peach-web/static/index.html @@ -0,0 +1,39 @@ + + + + PeachCloud + + + + + + +
+

Welcome to PeachCloud!

+
+ +
+
+
+ Configure WiFi +
+ + +
+
+ + +
+
+ +
+
+
+
+ +
+

+

+

+
+ diff --git a/peach-web/static/js/change_password.js b/peach-web/static/js/change_password.js new file mode 100644 index 0000000..9361d56 --- /dev/null +++ b/peach-web/static/js/change_password.js @@ -0,0 +1,45 @@ +/* +* behavioural layer for the `change_password.html.tera` template, + */ + +// catch click of 'Save' button and make POST request +PEACH.add = function() { + document.addEventListener('DOMContentLoaded', function() { + document.body.addEventListener('submit', function(e) { + // prevent redirect on button press (default behavior) + e.preventDefault(); + // capture form data + var formElement = document.querySelector("form"); + // create form data object from the wifiCreds form element + var formData = new FormData(formElement); + var object = {}; + // assign values from form + formData.forEach(function(value, key){ + object[key] = value; + }); + // perform json serialization + console.log(object); + var jsonData = JSON.stringify(object); + // write in-progress status message to ui + PEACH.flashMsg("info", "Saving new password."); + // send add_wifi POST request + fetch("/api/v1/settings/change_password", { + method: "post", + headers: { + 'Content-Type': 'application/json', + }, + body: jsonData + }) + .then( (response) => { + return response.json() + }) + .then( (jsonData) => { + // write json response message to ui + PEACH.flashMsg(jsonData.status, jsonData.msg); + }) + }, false); + }); +} + +var addInstance = PEACH; +addInstance.add(); diff --git a/peach-web/static/js/common.js b/peach-web/static/js/common.js new file mode 100644 index 0000000..9a5d8a4 --- /dev/null +++ b/peach-web/static/js/common.js @@ -0,0 +1,56 @@ +/* +* +* Common javascript functions shared by multiple pages: +* - flashMsg +* - logout +* +*/ + +var PEACH = {}; + +// display a message by appending a paragraph element +PEACH.flashMsg = function(status, msg) { + // set the class of the element according to status + var elementClass; + if (status === "success") { + elementClass = "capsule center-text flash-message font-success"; + } else if (status === "info") { + elementClass = "capsule center-text flash-message font-info"; + } else { + elementClass = "capsule center-text flash-message font-failure"; + }; + + var flashElement = document.getElementById("flashMsg"); + // if flashElement exists, update the class & text + if (flashElement) { + flashElement.className = elementClass; + flashElement.innerText = msg; + // if flashElement does not exist, create it, set id, class, text & append + } else { + // create new div for flash message + var flashDiv = document.createElement("DIV"); + // set div attributes + flashDiv.id = "flashMsg"; + flashDiv.className = elementClass; + // add json response message to flash message div + var flashMsg = document.createTextNode(msg); + flashDiv.appendChild(flashMsg); + // insert the flash message div below the button div + var buttonDiv = document.getElementById("buttonDiv"); + // flashDiv will be added to the end since buttonDiv is the last + // child within the parent element (card-container div) + buttonDiv.parentNode.insertBefore(flashDiv, buttonDiv.nextSibling); + } +} + +// add click event to logout button which logs out of http basic auth +// by "trying to login" with invalid credentials (user@logout) +document.getElementById('logoutButton').onclick = function(e){ + e.preventDefault(); + var logoutUrl = "http://user:logout@" + window.location.hostname + window.location = logoutUrl; +} + +var addInstance = PEACH; +addInstance.add(); +addInstance.logout(); diff --git a/peach-web/static/js/configure_dns.js b/peach-web/static/js/configure_dns.js new file mode 100644 index 0000000..835be63 --- /dev/null +++ b/peach-web/static/js/configure_dns.js @@ -0,0 +1,98 @@ +/* + +behavioural layer for the `configure_dns.html.tera` template, +corresponding to the web route `/network/dns` + + - intercept button click for add (form submission of credentials) + - perform json api call + - update the dom + +*/ + +var PEACH_DNS = {}; + +// catch click of 'Add' button and make POST request +PEACH_DNS.add = function() { + document.addEventListener('DOMContentLoaded', function() { + document.body.addEventListener('submit', function(e) { + // prevent redirect on button press (default behavior) + e.preventDefault(); + // capture form data + var formElement = document.querySelector("form"); + // create form data object from the wifiCreds form element + var formData = new FormData(formElement); + var object = {}; + // set checkbox to false (the value is only passed to formData if it is "on") + object["enable_dyndns"] = false; + // assign values from form + formData.forEach(function(value, key){ + // convert checkbox to bool + if (key === "enable_dyndns") { + value = (value === "on"); + } + object[key] = value; + }); + // perform json serialization + console.log(object); + var jsonData = JSON.stringify(object); + // write in-progress status message to ui + PEACH_DNS.flashMsg("info", "Saving new DNS configurations"); + // send add_wifi POST request + fetch("/api/v1/dns/configure", { + method: "post", + headers: { + 'Content-Type': 'application/json', + }, + body: jsonData + }) + .then( (response) => { + return response.json() + }) + .then( (jsonData) => { + // write json response message to ui + PEACH_DNS.flashMsg(jsonData.status, jsonData.msg); + let statusIndicator = document.getElementById("dyndns-status-indicator"); + statusIndicator.remove(); + + }) + }, false); + }); +} + +// display a message by appending a paragraph element +PEACH_DNS.flashMsg = function(status, msg) { + // set the class of the element according to status + var elementClass; + if (status === "success") { + elementClass = "capsule center-text flash-message font-success"; + } else if (status === "info") { + elementClass = "capsule center-text flash-message font-info"; + } else { + elementClass = "capsule center-text flash-message font-failure"; + }; + + var flashElement = document.getElementById("flashMsg"); + // if flashElement exists, update the class & text + if (flashElement) { + flashElement.className = elementClass; + flashElement.innerText = msg; + // if flashElement does not exist, create it, set id, class, text & append + } else { + // create new div for flash message + var flashDiv = document.createElement("DIV"); + // set div attributes + flashDiv.id = "flashMsg"; + flashDiv.className = elementClass; + // add json response message to flash message div + var flashMsg = document.createTextNode(msg); + flashDiv.appendChild(flashMsg); + // insert the flash message div below the button div + var buttonDiv = document.getElementById("buttonDiv"); + // flashDiv will be added to the end since buttonDiv is the last + // child within the parent element (card-container div) + buttonDiv.parentNode.insertBefore(flashDiv, buttonDiv.nextSibling); + } +} + +var addInstance = PEACH_DNS; +addInstance.add(); diff --git a/peach-web/static/js/network_add.js b/peach-web/static/js/network_add.js new file mode 100644 index 0000000..ccf68a1 --- /dev/null +++ b/peach-web/static/js/network_add.js @@ -0,0 +1,93 @@ +/* + +behavioural layer for the `network_add.html.tera` template, +corresponding to the web route `/network/wifi/add` + + - intercept button click for add (form submission of credentials) + - perform json api call + - update the dom + +methods: + + PEACH_NETWORK.add(); + PEACH_NETWORK.flashMsg(status, msg); + +*/ + +var PEACH_NETWORK = {}; + +// catch click of 'Add' button and make POST request +PEACH_NETWORK.add = function() { + document.addEventListener('DOMContentLoaded', function() { + document.body.addEventListener('submit', function(e) { + // prevent redirect on button press (default behavior) + e.preventDefault(); + // capture form data + var formElement = document.querySelector("form"); + // create form data object from the wifiCreds form element + var formData = new FormData(formElement); + var object = {}; + // assign ssid and pass from form + formData.forEach(function(value, key){ + object[key] = value; + }); + // perform json serialization + var jsonData = JSON.stringify(object); + // write in-progress status message to ui + PEACH_NETWORK.flashMsg("info", "Adding WiFi credentials..."); + // send add_wifi POST request + fetch("/api/v1/network/wifi", { + method: "post", + headers: { + 'Content-Type': 'application/json', + }, + body: jsonData + }) + .then( (response) => { + return response.json() + }) + .then( (jsonData) => { + // write json response message to ui + PEACH_NETWORK.flashMsg(jsonData.status, jsonData.msg); + }) + }, false); + }); +} + +// display a message by appending a paragraph element +PEACH_NETWORK.flashMsg = function(status, msg) { + // set the class of the element according to status + var elementClass; + if (status === "success") { + elementClass = "capsule center-text flash-message font-success"; + } else if (status === "info") { + elementClass = "capsule center-text flash-message font-info"; + } else { + elementClass = "capsule center-text flash-message font-failure"; + }; + + var flashElement = document.getElementById("flashMsg"); + // if flashElement exists, update the class & text + if (flashElement) { + flashElement.className = elementClass; + flashElement.innerText = msg; + // if flashElement does not exist, create it, set id, class, text & append + } else { + // create new div for flash message + var flashDiv = document.createElement("DIV"); + // set div attributes + flashDiv.id = "flashMsg"; + flashDiv.className = elementClass; + // add json response message to flash message div + var flashMsg = document.createTextNode(msg); + flashDiv.appendChild(flashMsg); + // insert the flash message div below the button div + var buttonDiv = document.getElementById("buttonDiv"); + // flashDiv will be added to the end since buttonDiv is the last + // child within the parent element (card-container div) + buttonDiv.parentNode.insertBefore(flashDiv, buttonDiv.nextSibling); + } +} + +var addInstance = PEACH_NETWORK; +addInstance.add(); diff --git a/peach-web/static/js/network_card.js b/peach-web/static/js/network_card.js new file mode 100644 index 0000000..79fa72a --- /dev/null +++ b/peach-web/static/js/network_card.js @@ -0,0 +1,201 @@ +/* + +behavioural layer for the `network_card.html.tera` template, +corresponding to the web route `/network` + + - intercept form submissions + - perform json api calls + - update the dom + +methods: + + PEACH_NETWORK.activateAp(); + PEACH_NETWORK.activateClient(); + PEACH_NETWORK.apOnline(); + PEACH_NETWORK.clientOffline(); + PEACH_NETWORK.clientOnline(); + PEACH_NETWORK.flashMsg(status, msg); + +*/ + +var PEACH_NETWORK = {}; + +// catch click of 'Deploy Access Point' and make POST request +PEACH_NETWORK.activateAp = function() { + document.addEventListener('DOMContentLoaded', function() { + var deployAP = document.getElementById('deployAccessPoint'); + if (deployAP) { + deployAP.addEventListener('click', function(e) { + // prevent form submission (default behavior) + e.preventDefault(); + // write in-progress status message to ui + PEACH_NETWORK.flashMsg("info", "Deploying access point..."); + // send activate_ap POST request + fetch("/api/v1/network/activate_ap", { + method: "post", + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + }) + .then( (response) => { + return response.json() + }) + .then( (jsonData) => { + console.log(jsonData.msg); + // write json response message to ui + PEACH_NETWORK.flashMsg(jsonData.status, jsonData.msg); + // if ap activation is successful, update the ui + if (jsonData.status === "success") { + PEACH_NETWORK.apOnline(); + } + }) + }, false); + } + }); +} + +// catch click of 'Enable WiFi' and make POST request +PEACH_NETWORK.activateClient = function() { + document.addEventListener('DOMContentLoaded', function() { + var enableWifi = document.getElementById('connectWifi'); + if (enableWifi) { + enableWifi.addEventListener('click', function(e) { + // prevent form submission (default behavior) + e.preventDefault(); + // write in-progress status message to ui + PEACH_NETWORK.flashMsg("info", "Enabling WiFi client..."); + // send activate_ap POST request + fetch("/api/v1/network/activate_client", { + method: "post", + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + }) + .then( (response) => { + return response.json() + }) + .then( (jsonData) => { + console.log(jsonData.msg); + // write json response message to ui + PEACH_NETWORK.flashMsg(jsonData.status, jsonData.msg); + // if client activation is successful, update the ui + if (jsonData.status === "success") { + PEACH_NETWORK.clientOnline(); + } + }) + }, false); + } + }); +} + +// update ui for access point mode (status: online) +PEACH_NETWORK.apOnline = function() { + console.log('Activating AP Mode'); + + // update network mode and status (icon & label) + let i = document.getElementById("netModeIcon"); + i.className = "center icon icon-active"; + i.src = "icons/router.svg"; + let l = document.getElementById("netModeLabel"); + l.textContent = "ONLINE"; + + // create Enable WiFi button and add it to button div + var wifiButton = document.createElement("A"); + wifiButton.className = "button center"; + wifiButton.href = "/network/wifi/activate"; + wifiButton.id = "connectWifi"; + var label = "Enable WiFi"; + var buttonText = document.createTextNode(label); + wifiButton.appendChild(buttonText); + + // append the new button to the buttons div + let buttons = document.getElementById("buttons"); + buttons.appendChild(wifiButton); + + // remove the old 'Activate Access Point' button + let apButton = document.getElementById("deployAccessPoint"); + apButton.style = "display: none;"; +} + +// update ui for wifi client mode (status: online) +PEACH_NETWORK.clientOnline = function() { + console.log('Activating Client Mode'); + + // update network mode and status (icon & label) + let i = document.getElementById("netModeIcon"); + i.className = "center icon icon-active"; + i.src = "icons/wifi.svg"; + let l = document.getElementById("netModeLabel"); + l.textContent = "ONLINE"; + + // TODO: think about updates for buttons (transition from ap mode) +} + +// update ui for wifi client mode (status: offline) +PEACH_NETWORK.clientOffline = function() { + console.log('Activating Client Mode'); + + // update network mode and status (icon & label) + let i = document.getElementById("netModeIcon"); + i.className = "center icon icon-inactive"; + i.src = "icons/wifi.svg"; + let l = document.getElementById("netModeLabel"); + l.textContent = "OFFLINE"; + + // TODO: think about updates for buttons (transition from ap mode) +} + +// display a message by appending a paragraph element +PEACH_NETWORK.flashMsg = function(status, msg) { + // set the class of the element according to status + var elementClass; + if (status === "success") { + elementClass = "capsule center-text flash-message font-success"; + } else if (status === "info") { + elementClass = "capsule center-text flash-message font-info"; + } else { + elementClass = "capsule center-text flash-message font-failure"; + }; + + var flashElement = document.getElementById("flashMsg"); + // if flashElement exists, update the class & text + if (flashElement) { + flashElement.className = elementClass; + flashElement.innerText = msg; + // if flashElement does not exist, create it, set id, class, text & append + } else { + // create new div for flash message + var flashDiv = document.createElement("DIV"); + // set div attributes + flashDiv.id = "flashMsg"; + flashDiv.className = elementClass; + // add json response message to flash message div + var flashMsg = document.createTextNode(msg); + flashDiv.appendChild(flashMsg); + // insert the flash message div above the three icon grid div + var gridDiv = document.getElementById("gridDiv"); + gridDiv.parentNode.insertBefore(flashDiv, gridDiv); + } +} + +var networkInstance = PEACH_NETWORK; +networkInstance.activateAp(); +networkInstance.activateClient(); + +/* + +async function exampleFetch() { + const response = await fetch('/api/v1/network/state'); + const myJson = await response.json(); + //const jsonData = JSON.parse(myJson); + console.log(myJson.data.wlan0); + //var state = document.createElement("P"); + //state.innerText = ""jsonData.wlan0; + //document.body.appendChild(state); +} + +exampleFetch() + +*/ diff --git a/peach-web/static/js/network_detail.js b/peach-web/static/js/network_detail.js new file mode 100644 index 0000000..6d294ae --- /dev/null +++ b/peach-web/static/js/network_detail.js @@ -0,0 +1,167 @@ +/* + +behavioural layer for the `network_detail.html.tera` template, +corresponding to the web route `/network/wifi?` + + - intercept button clicks for connect, disconnect and forget + - perform json api call + - update the dom + +methods: + + PEACH_NETWORK.connect(); + PEACH_NETWORK.disconnect(); + PEACH_NETWORK.forget(); + PEACH_NETWORK.flashMsg(status, msg); + +*/ + +var PEACH_NETWORK = {}; + +// catch click of 'Connect' button (form) and make POST request +PEACH_NETWORK.connect = function() { + document.addEventListener('DOMContentLoaded', function() { + var connectWifi = document.getElementById('connectWifi'); + if (connectWifi) { + connectWifi.addEventListener('click', function(e) { + // prevent form submission (default behavior) + e.preventDefault(); + // retrieve ssid value and append to form data object + var ssid = document.getElementById('connectSsid').value; + // create key:value pair + var ssidData = { ssid: ssid }; + // perform json serialization + var jsonData = JSON.stringify(ssidData); + // write in-progress status message to ui + PEACH_NETWORK.flashMsg("info", "Connecting to access point..."); + // send add_wifi POST request + fetch("/api/v1/network/wifi/connect", { + method: "post", + headers: { + 'Content-Type': 'application/json', + }, + body: jsonData + }) + .then( (response) => { + return response.json() + }) + .then( (jsonData) => { + // write json response message to ui + PEACH_NETWORK.flashMsg(jsonData.status, jsonData.msg); + }) + }, false); + }; + }); +} + +// catch click of 'Disconnect' button and make POST request +PEACH_NETWORK.disconnect = function() { + document.addEventListener('DOMContentLoaded', function() { + var disconnectWifi = document.getElementById('disconnectWifi'); + if (disconnectWifi) { + disconnectWifi.addEventListener('click', function(e) { + // prevent form submission (default behavior) + e.preventDefault(); + // retrieve ssid value and append to form data object + var ssid = document.getElementById('disconnectSsid').value; + // create key:value pair + var ssidData = { ssid: ssid }; + // perform json serialization + var jsonData = JSON.stringify(ssidData); + // write in-progress status message to ui + PEACH_NETWORK.flashMsg("info", "Disconnecting from access point..."); + // send disconnect_wifi POST request + fetch("/api/v1/network/wifi/disconnect", { + method: "post", + headers: { + 'Content-Type': 'application/json', + }, + body: jsonData + }) + .then( (response) => { + return response.json() + }) + .then( (jsonData) => { + // write json response message to ui + PEACH_NETWORK.flashMsg(jsonData.status, jsonData.msg); + }) + }, false); + }; + }); +} + +// catch click of 'Forget' button (form) and make POST request +PEACH_NETWORK.forget = function() { + document.addEventListener('DOMContentLoaded', function() { + var forgetWifi = document.getElementById('forgetWifi'); + if (forgetWifi) { + forgetWifi.addEventListener('click', function(e) { + // prevent form submission (default behavior) + e.preventDefault(); + // retrieve ssid value + var ssid = document.getElementById('forgetSsid').value; + // create key:value pair + var ssidData = { ssid: ssid }; + // perform json serialization + var jsonData = JSON.stringify(ssidData); + // write in-progress status message to ui + PEACH_NETWORK.flashMsg("info", "Removing credentials for access point..."); + // send forget_ap POST request + fetch("/api/v1/network/wifi/forget", { + method: "post", + headers: { + 'Content-Type': 'application/json', + }, + body: jsonData + }) + .then( (response) => { + return response.json() + }) + .then( (jsonData) => { + // write json response message to ui + PEACH_NETWORK.flashMsg(jsonData.status, jsonData.msg); + }) + }, false); + }; + }); +} + +// display a message by appending a paragraph element +PEACH_NETWORK.flashMsg = function(status, msg) { + // set the class of the element according to status + var elementClass; + if (status === "success") { + elementClass = "capsule center-text flash-message font-success"; + } else if (status === "info") { + elementClass = "capsule center-text flash-message font-info"; + } else { + elementClass = "capsule center-text flash-message font-failure"; + }; + + var flashElement = document.getElementById("flashMsg"); + // if flashElement exists, update the class & text + if (flashElement) { + flashElement.className = elementClass; + flashElement.innerText = msg; + // if flashElement does not exist, create it, set id, class, text & append + } else { + // create new div for flash message + var flashDiv = document.createElement("DIV"); + // set div attributes + flashDiv.id = "flashMsg"; + flashDiv.className = elementClass; + // add json response message to flash message div + var flashMsg = document.createTextNode(msg); + flashDiv.appendChild(flashMsg); + // insert the flash message div below the button div + var buttonDiv = document.getElementById("buttonDiv"); + // flashDiv will be added to the end since buttonDiv is the last + // child within the parent element (card-container div) + buttonDiv.parentNode.insertBefore(flashDiv, buttonDiv.nextSibling); + } +} + +var detailInstance = PEACH_NETWORK; +detailInstance.connect(); +detailInstance.disconnect(); +detailInstance.forget(); diff --git a/peach-web/static/js/network_modify.js b/peach-web/static/js/network_modify.js new file mode 100644 index 0000000..4dce16b --- /dev/null +++ b/peach-web/static/js/network_modify.js @@ -0,0 +1,92 @@ +/* + +behavioural layer for the `network_modify.html.tera` template + + - intercept button click for modify (form submission of credentials) + - perform json api call + - update the dom + +methods: + + PEACH_NETWORK.modify(); + PEACH_NETWORK.flashMsg(status, msg); + +*/ + +var PEACH_NETWORK = {}; + +// catch click of 'Save' button and make POST request +PEACH_NETWORK.modify = function() { + document.addEventListener('DOMContentLoaded', function() { + document.body.addEventListener('submit', function(e) { + // prevent redirect on button press (default behavior) + e.preventDefault(); + // capture form data + var formElement = document.querySelector("form"); + // create form data object from the wifiModify form element + var formData = new FormData(formElement); + var object = {}; + // assign ssid and pass from form + formData.forEach(function(value, key){ + object[key] = value; + }); + // perform json serialization + var jsonData = JSON.stringify(object); + // write in-progress status message to ui + PEACH_NETWORK.flashMsg("info", "Updating WiFi password..."); + // send new_password POST request + fetch("/api/v1/network/wifi/modify", { + method: "post", + headers: { + 'Content-Type': 'application/json', + }, + body: jsonData + }) + .then( (response) => { + return response.json() + }) + .then( (jsonData) => { + // write json response message to ui + PEACH_NETWORK.flashMsg(jsonData.status, jsonData.msg); + }) + }, false); + }); +} + +// display a message by appending a paragraph element +PEACH_NETWORK.flashMsg = function(status, msg) { + // set the class of the element according to status + var elementClass; + if (status === "success") { + elementClass = "capsule center-text flash-message font-success"; + } else if (status === "info") { + elementClass = "capsule center-text flash-message font-info"; + } else { + elementClass = "capsule center-text flash-message font-failure"; + }; + + var flashElement = document.getElementById("flashMsg"); + // if flashElement exists, update the class & text + if (flashElement) { + flashElement.className = elementClass; + flashElement.innerText = msg; + // if flashElement does not exist, create it, set id, class, text & append + } else { + // create new div for flash message + var flashDiv = document.createElement("DIV"); + // set div attributes + flashDiv.id = "flashMsg"; + flashDiv.className = elementClass; + // add json response message to flash message div + var flashMsg = document.createTextNode(msg); + flashDiv.appendChild(flashMsg); + // insert the flash message div below the button div + var buttonDiv = document.getElementById("buttonDiv"); + // flashDiv will be added to the end since buttonDiv is the last + // child within the parent element (card-container div) + buttonDiv.parentNode.insertBefore(flashDiv, buttonDiv.nextSibling); + } +} + +var modifyInstance = PEACH_NETWORK; +modifyInstance.modify(); diff --git a/peach-web/static/js/network_usage.js b/peach-web/static/js/network_usage.js new file mode 100644 index 0000000..236a2d3 --- /dev/null +++ b/peach-web/static/js/network_usage.js @@ -0,0 +1,173 @@ +/* + +behavioural layer for the `network_usage.html.tera` template, +corresponding to the web route `/network/wifi/usage` + + - intercept form submissions + - perform json api calls + - update the dom + +methods: + + PEACH_NETWORK.updateAlerts(); + PEACH_NETWORK.resetUsage(); + PEACH_NETWORK.toggleWarning(); + PEACH_NETWORK.toggleCutoff(); + PEACH_NETWORK.flashMsg(status, msg); + +*/ + +var PEACH_NETWORK = {}; + +// catch click of 'Update' and make POST request +PEACH_NETWORK.updateAlerts = function() { + document.addEventListener('DOMContentLoaded', function() { + document.body.addEventListener('submit', function(e) { + // prevent redirect on button press (default behavior) + e.preventDefault(); + // capture form data + var formElement = document.querySelector("form"); + let warn = formElement.elements.warn.value; + let cut = formElement.elements.cut.value; + let warn_flag = formElement.elements.warn_flag.checked; + let cut_flag = formElement.elements.cut_flag.checked; + // perform json serialization + var jsonData = JSON.stringify({ + "warn": parseFloat(warn), + "cut": parseFloat(cut), + "warn_flag": warn_flag, + "cut_flag": cut_flag, + }); + // send update_alerts POST request + fetch("/api/v1/network/wifi/usage", { + method: "post", + headers: { + 'Content-Type': 'application/json', + }, + body: jsonData + }) + .then( (response) => { + return response.json() + }) + .then( (jsonData) => { + // write json response message to ui + PEACH_NETWORK.flashMsg(jsonData.status, jsonData.msg); + }) + }, false); + }); +} + +// catch click of 'Reset' and make POST request +PEACH_NETWORK.resetUsage = function() { + document.addEventListener('DOMContentLoaded', function() { + var resetBtn = document.getElementById('resetTotal'); + if (resetBtn) { + resetBtn.addEventListener('click', function(e) { + // prevent form submission (default behavior) + e.preventDefault(); + // send reset_data_usage POST request + fetch("/api/v1/network/wifi/usage/reset", { + method: "post", + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + }) + .then( (response) => { + return response.json() + }) + .then( (jsonData) => { + console.log(jsonData.msg); + // write json response message to ui + PEACH_NETWORK.flashMsg(jsonData.status, jsonData.msg); + // if reset is successful, update the ui + if (jsonData.status === "success") { + console.log(jsonData.data); + PEACH_NETWORK.updateTotal(jsonData.data); + } + }) + }, false); + } + }); +} + +// update data usage total in ui +PEACH_NETWORK.updateTotal = function(data) { + document.addEventListener('DOMContentLoaded', function() { + console.log(data); + let label = document.getElementById("dataTotal"); + // take usage total as bytes, convert to MB and round to nearest integer + label.textContent = (data / 1024 / 1024).round(); + }); +}; + +// update ui for warning +PEACH_NETWORK.toggleWarning = function() { + document.addEventListener('DOMContentLoaded', function() { + let i = document.getElementById("warnIcon"); + let warnCheck = document.getElementById("warnCheck"); + warnCheck.addEventListener('click', function(e) { + console.log('Toggling warning icon state'); + if (warnCheck.checked) { + i.className = "icon"; + } else { + i.className = "icon icon-inactive"; + } + }); + }); +}; + +// update ui for cutoff +PEACH_NETWORK.toggleCutoff = function() { + document.addEventListener('DOMContentLoaded', function() { + let i = document.getElementById("cutIcon"); + let cutCheck = document.getElementById("cutCheck"); + cutCheck.addEventListener('click', function(e) { + console.log('Toggling cutoff icon state'); + if (cutCheck.checked) { + i.className = "icon"; + } else { + i.className = "icon icon-inactive"; + } + }); + }); +}; + +// display a message by appending a paragraph element +PEACH_NETWORK.flashMsg = function(status, msg) { + // set the class of the element according to status + var elementClass; + if (status === "success") { + elementClass = "capsule center-text flash-message font-success"; + } else if (status === "info") { + elementClass = "capsule center-text flash-message font-info"; + } else { + elementClass = "capsule center-text flash-message font-failure"; + }; + + var flashElement = document.getElementById("flashMsg"); + // if flashElement exists, update the class & text + if (flashElement) { + flashElement.className = elementClass; + flashElement.innerText = msg; + // if flashElement does not exist, create it, set id, class, text & append + } else { + // create new div for flash message + var flashDiv = document.createElement("DIV"); + // set div attributes + flashDiv.id = "flashMsg"; + flashDiv.className = elementClass; + // add json response message to flash message div + var flashMsg = document.createTextNode(msg); + flashDiv.appendChild(flashMsg); + // insert the flash message div below the button div + var buttonDiv = document.getElementById("buttonDiv"); + buttonDiv.parentNode.insertBefore(flashDiv, buttonDiv.nextSibling); + } +} + +var usageInstance = PEACH_NETWORK; +usageInstance.resetUsage(); +usageInstance.toggleWarning(); +usageInstance.toggleCutoff(); +usageInstance.updateAlerts(); diff --git a/peach-web/static/js/reset_password.js b/peach-web/static/js/reset_password.js new file mode 100644 index 0000000..1b4eb09 --- /dev/null +++ b/peach-web/static/js/reset_password.js @@ -0,0 +1,45 @@ +/* +* behavioural layer for the `reset_password.html.tera` template, + */ + +// catch click of 'Save' button and make POST request +PEACH.add = function() { + document.addEventListener('DOMContentLoaded', function() { + document.body.addEventListener('submit', function(e) { + // prevent redirect on button press (default behavior) + e.preventDefault(); + // capture form data + var formElement = document.querySelector("form"); + // create form data object from the wifiCreds form element + var formData = new FormData(formElement); + var object = {}; + // assign values from form + formData.forEach(function(value, key){ + object[key] = value; + }); + // perform json serialization + console.log(object); + var jsonData = JSON.stringify(object); + // write in-progress status message to ui + PEACH.flashMsg("info", "Saving new password."); + // send add_wifi POST request + fetch("/public/api/v1/reset_password", { + method: "post", + headers: { + 'Content-Type': 'application/json', + }, + body: jsonData + }) + .then( (response) => { + return response.json() + }) + .then( (jsonData) => { + // write json response message to ui + PEACH.flashMsg(jsonData.status, jsonData.msg); + }) + }, false); + }); +} + +var addInstance = PEACH; +addInstance.add(); diff --git a/peach-web/static/js/shutdown_menu.js b/peach-web/static/js/shutdown_menu.js new file mode 100644 index 0000000..0d40dff --- /dev/null +++ b/peach-web/static/js/shutdown_menu.js @@ -0,0 +1,119 @@ +/* + +behavioural layer for the `shutdown.html.tera` template, +corresponding to the web route `/shutdown` + + - intercept button clicks for reboot & shutdown + - perform json api calls + - update the dom + +methods: + + PEACH_DEVICE.reboot(); + PEACH_DEVICE.shutdown(); + PEACH_DEVICE.flashMsg(status, msg); + +*/ + +var PEACH_DEVICE = {}; + +// catch click of 'Reboot' button and make POST request +PEACH_DEVICE.reboot = function() { + document.addEventListener('DOMContentLoaded', function() { + var rebootDevice = document.getElementById('rebootBtn'); + if (rebootDevice) { + rebootDevice.addEventListener('click', function(e) { + // prevent redirect on button press (default behavior) + e.preventDefault(); + // write reboot flash message + PEACH_DEVICE.flashMsg("success", "Rebooting the device..."); + // send reboot_device POST request + fetch("/api/v1/device/reboot", { + method: "post", + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + }) + .then( (response) => { + return response.json() + }) + .then( (jsonData) => { + console.log(jsonData.msg); + // write json response message to ui + PEACH_DEVICE.flashMsg(jsonData.status, jsonData.msg); + }) + }, false); + } + }); +} + +// catch click of 'Shutdown' button and make POST request +PEACH_DEVICE.shutdown = function() { + document.addEventListener('DOMContentLoaded', function() { + var shutdownDevice = document.getElementById('shutdownBtn'); + if (shutdownDevice) { + shutdownDevice.addEventListener('click', function(e) { + // prevent form submission (default behavior) + e.preventDefault(); + // write shutdown flash message + PEACH_DEVICE.flashMsg("success", "Shutting down the device..."); + // send shutdown_device POST request + fetch("/api/v1/device/shutdown", { + method: "post", + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + }) + .then( (response) => { + return response.json() + }) + .then( (jsonData) => { + console.log(jsonData.msg); + // write json response message to ui + PEACH_DEVICE.flashMsg(jsonData.status, jsonData.msg); + }) + }, false); + } + }); +} + +// display a message by appending a paragraph element +PEACH_DEVICE.flashMsg = function(status, msg) { + // set the class of the element according to status + var elementClass; + if (status === "success") { + elementClass = "capsule center-text flash-message font-success"; + } else if (status === "info") { + elementClass = "capsule center-text flash-message font-info"; + } else { + elementClass = "capsule center-text flash-message font-failure"; + }; + + var flashElement = document.getElementById("flashMsg"); + // if flashElement exists, update the class & text + if (flashElement) { + flashElement.className = elementClass; + flashElement.innerText = msg; + // if flashElement does not exist, create it, set id, class, text & append + } else { + // create new div for flash message + var flashDiv = document.createElement("DIV"); + // set div attributes + flashDiv.id = "flashMsg"; + flashDiv.className = elementClass; + // add json response message to flash message div + var flashMsg = document.createTextNode(msg); + flashDiv.appendChild(flashMsg); + // insert the flash message div below the button div + var buttonDiv = document.getElementById("buttonDiv"); + // flashDiv will be added to the end since buttonDiv is the last + // child within the parent element (card-container div) + buttonDiv.parentNode.insertBefore(flashDiv, buttonDiv.nextSibling); + } +} + +var deviceInstance = PEACH_DEVICE; +deviceInstance.reboot(); +deviceInstance.shutdown(); diff --git a/peach-web/templates/admin/add_admin.html.tera b/peach-web/templates/admin/add_admin.html.tera new file mode 100644 index 0000000..a99badb --- /dev/null +++ b/peach-web/templates/admin/add_admin.html.tera @@ -0,0 +1,22 @@ +{%- extends "nav" -%} +{%- block card %} + +
+
+
+ +
+ + Cancel +
+
+ + + {% include "snippets/flash_message" %} + + + {% include "snippets/noscript" %} + +
+
+{%- endblock card -%} diff --git a/peach-web/templates/admin/configure_admin.html.tera b/peach-web/templates/admin/configure_admin.html.tera new file mode 100644 index 0000000..a9b5ed4 --- /dev/null +++ b/peach-web/templates/admin/configure_admin.html.tera @@ -0,0 +1,34 @@ +{%- extends "nav" -%} +{%- block card %} + +
+
+

Current Admins

+ + {% if not ssb_admin_ids %} +
+ There are no currently configured admins. +
+ {% else %} + {% for admin in ssb_admin_ids %} +
+
+ + {{ admin }} +
+
+ {% endfor %} + {% endif %} + Add Admin +
+ + + + {% include "snippets/flash_message" %} + + + {% include "snippets/noscript" %} + +
+ +{%- endblock card -%} diff --git a/peach-web/templates/base.html.tera b/peach-web/templates/base.html.tera new file mode 100644 index 0000000..6d228aa --- /dev/null +++ b/peach-web/templates/base.html.tera @@ -0,0 +1,17 @@ + + + + + + PeachCloud + + + + + + + + {% block nav %}{% endblock nav %} + + + diff --git a/peach-web/templates/configure_dns.html.tera b/peach-web/templates/configure_dns.html.tera new file mode 100644 index 0000000..019bc5b --- /dev/null +++ b/peach-web/templates/configure_dns.html.tera @@ -0,0 +1,75 @@ +{%- extends "nav" -%} +{%- block card %} + +
+ +
+ + {% if enable_dyndns %} + +
+
+ {% if is_dyndns_online %} + + {% else %} + + {% endif %} +
+
+ {% endif %} + +
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ +
+ +
+ + + + + {% if flash_msg and flash_name == "success" %} + +
{{ flash_msg }}.
+ {%- elif flash_msg and flash_name == "info" %} + +
{{ flash_msg }}.
+ {%- elif flash_msg and flash_name == "error" %} + +
{{ flash_msg }}.
+ {%- endif -%} + + + +
+ + +{%- endblock card -%} diff --git a/peach-web/templates/device.html.tera b/peach-web/templates/device.html.tera new file mode 100644 index 0000000..35556ca --- /dev/null +++ b/peach-web/templates/device.html.tera @@ -0,0 +1,139 @@ +{%- extends "nav" -%} +{%- block card -%} + {# ASSIGN VARIABLES #} + {# ---------------- #} + {%- if mem_stats -%} + {% set mem_usage_percent = mem_stats.used / mem_stats.total * 100 | round -%} + {% set mem_used = mem_stats.used / 1024 | round -%} + {% set mem_free = mem_stats.free / 1024 | round -%} + {% set mem_total = mem_stats.total / 1024 | round -%} + {% endif -%} + {% if cpu_stat_percent -%} + {% set cpu_usage_percent = cpu_stat_percent.nice + cpu_stat_percent.system + cpu_stat_percent.user | round -%} + {%- endif -%} + {%- if disk_stats -%} + {%- for disk in disk_stats -%} + {%- set_global disk_usage_percent = disk.used_percentage -%} + {# Calculate free disk space in megabytes #} + {%- set_global disk_free = disk.one_k_blocks_free / 1024 | round -%} + {%- endfor -%} + {%- endif -%} + +
+
+ {# Display microservice status for network, oled & stats #} +
+ +
+ Network +
+ + +
+
+ +
+ Display +
+ + +
+
+ +
+ Stats +
+ + +
+
+
+
+ +
+ Dyndns +
+ + +
+
+ +
+ Config +
+ + +
+
+ +
+ Sbot +
+ + +
+
+
+ {# Display CPU usage meter #} + {%- if cpu_stat_percent -%} +
+ CPU + {{ cpu_usage_percent }}% +
+ +
+ CPU Usage +
+
+ {%- else -%} +

CPU usage data unavailable

+ {% endif -%} + {# Display memory usage meter #} + {%- if mem_stats %} +
+ Memory + {{ mem_usage_percent }}% ({{ mem_free }} MB free) +
+ +
+ Memory Usage +
+
+ {%- else -%} +

Memory usage data unavailable

+ {% endif -%} + {# Display disk usage meter #} + {%- if disk_stats %} +
+ Disk + {{ disk_usage_percent }}% ({% if disk_free > 1024 %}{{ disk_free / 1024 | round }} GB{% else %}{{ disk_free }} MB{% endif %} free) +
+ +
+ Disk Usage +
+
+ {%- else -%} +

Disk usage data unavailable

+ {%- endif %} + {# Display system uptime in minutes #} + {%- if uptime and uptime < 60 %} +

Uptime: {{ uptime }} minutes

+ {# Display system uptime in hours & minutes #} + {%- elif uptime and uptime > 60 -%} +

Uptime: {{ uptime / 60 | round(method="floor") }} hours, {{ uptime % 60 }} minutes

+ {%- else -%} +

Uptime data unavailable

+ {%- endif %} + + + {%- if flash_msg and flash_name == "success" -%} + +
{{ flash_msg }}.
+ {%- elif flash_msg and flash_name == "error" -%} + +
{{ flash_msg }}.
+ {%- endif %} +
+
+{%- endblock card %} diff --git a/peach-web/templates/help.html.tera b/peach-web/templates/help.html.tera new file mode 100644 index 0000000..8167f67 --- /dev/null +++ b/peach-web/templates/help.html.tera @@ -0,0 +1,21 @@ +{%- extends "nav" -%} +{%- block card %} + +
+
+ + + {%- if flash_msg and flash_name == "success" %} + +
{{ flash_msg }}.
+ {%- elif flash_msg and flash_name == "error" %} + +
{{ flash_msg }}.
+ {%- endif %} + + +
+
+{%- endblock card -%} diff --git a/peach-web/templates/index.html.tera b/peach-web/templates/index.html.tera new file mode 100644 index 0000000..212c26e --- /dev/null +++ b/peach-web/templates/index.html.tera @@ -0,0 +1,53 @@ +{%- extends "nav" -%} +{%- block card %} + + +{%- endblock card %} diff --git a/peach-web/templates/internal_error.html.tera b/peach-web/templates/internal_error.html.tera new file mode 100644 index 0000000..d2c4f15 --- /dev/null +++ b/peach-web/templates/internal_error.html.tera @@ -0,0 +1,9 @@ +{%- extends "nav" -%} +{%- block card %} +
+
+

PeachCloud has encountered an internal error. This may indicate that one or several software components are misconfigured or malfunctioning. Please try to repeat your desired actions. If the problem persists, a system reset is recommended - either via the Shutdown menu or the OLED menu on the physical device.

+

Click the back arrow in the top-left or the PeachCloud logo at the bottom of your screen to return Home.

+
+
+{%- endblock card -%} diff --git a/peach-web/templates/login.html.tera b/peach-web/templates/login.html.tera new file mode 100644 index 0000000..39c1093 --- /dev/null +++ b/peach-web/templates/login.html.tera @@ -0,0 +1,31 @@ +{%- extends "nav" -%} +{%- block card %} + +
+
+
+ + + + +
+ + Cancel +
+
+ + + {% if flash_msg and flash_name == "success" %} + +
{{ flash_msg }}.
+ {%- elif flash_msg and flash_name == "info" %} + +
{{ flash_msg }}.
+ {%- elif flash_msg and flash_name == "error" %} + +
{{ flash_msg }}.
+ {%- endif -%} +
+
+ +{%- endblock card -%} diff --git a/peach-web/templates/messages.html.tera b/peach-web/templates/messages.html.tera new file mode 100644 index 0000000..36f6fed --- /dev/null +++ b/peach-web/templates/messages.html.tera @@ -0,0 +1,21 @@ +{%- extends "nav" -%} +{%- block card %} + +
+
+ + + {%- if flash_msg and flash_name == "success" %} + +
{{ flash_msg }}.
+ {%- elif flash_msg and flash_name == "error" %} + +
{{ flash_msg }}.
+ {%- endif %} + + +
+
+{%- endblock card -%} diff --git a/peach-web/templates/nav.html.tera b/peach-web/templates/nav.html.tera new file mode 100644 index 0000000..004ca7b --- /dev/null +++ b/peach-web/templates/nav.html.tera @@ -0,0 +1,29 @@ +{%- extends "base" -%} +{%- block nav -%} + + + +
+ {%- block card -%}{%- endblock card %} +
+ + +{%- endblock nav -%} diff --git a/peach-web/templates/network_add.html.tera b/peach-web/templates/network_add.html.tera new file mode 100644 index 0000000..e73a5d2 --- /dev/null +++ b/peach-web/templates/network_add.html.tera @@ -0,0 +1,37 @@ +{%- extends "nav" -%} +{%- block card %} + +
+
+
+ + + + +
+ + Cancel +
+
+ + + {% if flash_msg and flash_name == "success" %} + +
{{ flash_msg }}.
+ {%- elif flash_msg and flash_name == "info" %} + +
{{ flash_msg }}.
+ {%- elif flash_msg and flash_name == "error" %} + +
{{ flash_msg }}.
+ {%- endif -%} + + +
+
+ +{%- endblock card -%} diff --git a/peach-web/templates/network_card.html.tera b/peach-web/templates/network_card.html.tera new file mode 100644 index 0000000..2fdf6fc --- /dev/null +++ b/peach-web/templates/network_card.html.tera @@ -0,0 +1,184 @@ +{%- extends "nav" -%} + +{%- block card %} + + {%- if ap_state == "up" %} + +
+ +
+ + + +
+ WiFi router + +
+ + +
+ +

Access Point

+ +

peach

+ +

{{ ap_ip }}

+
+
+ + + + + {%- if flash_msg and flash_name == "success" %} + +
{{ flash_msg }}.
+ {%- elif flash_msg and flash_name == "info" %} + +
{{ flash_msg }}.
+ {%- elif flash_msg and flash_name == "error" %} + +
{{ flash_msg }}
+ {%- endif %} + +
+
+
+ Digital devices +
+ +
+ +
+
+ Download +
+ {%- if ap_traffic -%} + + + {%- else -%} + + + {%- endif -%} +
+ +
+
+ Upload +
+ {%- if ap_traffic -%} + + + {%- else -%} + + + {%- endif -%} +
+ +
+
+
+
+ {%- else %} + +
+ + {%- if wlan_state == "up" %} +
+ + + +
+ WiFi online + + {%- else %} +
+
+ WiFi offline + + {%- endif %} +
+
+ + + +

WiFi Client

+ +

{{ wlan_ssid }}

+ +

{{ wlan_ip }}

+
+
+ + + + + {%- if flash_msg and flash_name == "success" %} + +
{{ flash_msg }}
+ {%- elif flash_msg and flash_name == "info" %} + +
{{ flash_msg }}
+ {%- elif flash_msg and flash_name == "error" %} + +
{{ flash_msg }}
+ {%- endif %} +
+ + +
+
+ Signal +
+ +
+ +
+
+ Download +
+ {%- if wlan_traffic %} + + + + {%- else %} + + + + {%- endif %} +
+ +
+
+ Upload +
+ {%- if wlan_traffic %} + + + + {%- else %} + + + + {%- endif %} +
+ +
+
+
+
+ + {%- endif -%} +{%- endblock card -%} diff --git a/peach-web/templates/network_detail.html.tera b/peach-web/templates/network_detail.html.tera new file mode 100644 index 0000000..89b8493 --- /dev/null +++ b/peach-web/templates/network_detail.html.tera @@ -0,0 +1,86 @@ +{%- extends "nav" -%} +{%- block card -%} + {%- if wlan_networks -%} + {%- for ssid, ap in wlan_networks -%} + {# select only the access point we are interested in #} + {%- if ssid == selected %} + +
+ +
+ + +
+ WiFi icon + +
+ + +
+ +

{{ ssid }}

+ +

{% if ap.detail %}{% if ap.detail.protocol != "" %}{{ ap.detail.protocol }}{% else %}None{% endif %}{% else %}Unknown{% endif %}

+ +

{% if ap.signal %}{{ ap.signal }}%{% else %}Unknown{% endif %}

+
+
+ +
+
+ {%- if wlan_ssid == selected -%} +
+ + + +
+ {%- endif -%} + {%- if saved_aps -%} + {# Loop through the list of AP's with saved credentials #} + {%- for ap in saved_aps -%} + {# If the selected access point appears in the list, #} + {# display the Modify and Forget buttons. #} + {%- if ap.ssid == selected -%} + {# Set 'in_list' to true to allow correct Add button display #} + {% set_global in_list = true %} + {%- if wlan_ssid != selected and ap.state == "Available" -%} +
+ + + +
+ {%- endif -%} + Modify +
+ + + +
+ {%- endif -%} + {%- endfor -%} + {%- endif -%} + {%- if in_list == false -%} + {# Display the Add button if AP creds not already in saved networks list #} + Add + {%- endif -%} + Cancel +
+ + + {%- if flash_msg and flash_name == "success" %} + +
{{ flash_msg }}
+ {%- elif flash_msg and flash_name == "info" %} + +
{{ flash_msg }}
+ {%- elif flash_msg and flash_name == "error" %} + +
{{ flash_msg }}
+ {%- endif -%} +
+
+ + {%- endif -%} + {%- endfor -%} + {%- endif -%} +{%- endblock card -%} diff --git a/peach-web/templates/network_list.html.tera b/peach-web/templates/network_list.html.tera new file mode 100644 index 0000000..45d8227 --- /dev/null +++ b/peach-web/templates/network_list.html.tera @@ -0,0 +1,38 @@ +{%- extends "nav" -%} +{%- block card %} +
+
+ +
+
+{%- endblock card -%} diff --git a/peach-web/templates/network_modify.html.tera b/peach-web/templates/network_modify.html.tera new file mode 100644 index 0000000..4c9e5ef --- /dev/null +++ b/peach-web/templates/network_modify.html.tera @@ -0,0 +1,31 @@ +{%- extends "nav" -%} +{%- block card %} + +
+
+
+ + + + +
+ + Cancel +
+
+ + + {%- if flash_msg and flash_name == "success" %} + +
{{ flash_msg }}.
+ {%- elif flash_msg and flash_name == "info" %} + +
{{ flash_msg }}.
+ {%- elif flash_msg and flash_name == "error" %} + +
{{ flash_msg }}.
+ {%- endif -%} +
+
+ +{%- endblock card -%} diff --git a/peach-web/templates/network_usage.html.tera b/peach-web/templates/network_usage.html.tera new file mode 100644 index 0000000..db87620 --- /dev/null +++ b/peach-web/templates/network_usage.html.tera @@ -0,0 +1,57 @@ +{%- extends "nav" -%} +{%- block card -%} + +
+
+
+ + +
+ +
+
+
+ Warning +
+
+ + + +
+
+ + +
+
+ Cutoff +
+
+ + + +
+
+ + +
+
+
+ + Reset + Cancel +
+ + + {% if flash_msg and flash_name == "success" %} + +
{{ flash_msg }}.
+ {%- elif flash_msg and flash_name == "info" %} + +
{{ flash_msg }}.
+ {%- elif flash_msg and flash_name == "error" %} + +
{{ flash_msg }}.
+ {%- endif -%} +
+ +{%- endblock card %} diff --git a/peach-web/templates/not_found.html.tera b/peach-web/templates/not_found.html.tera new file mode 100644 index 0000000..2d0e80f --- /dev/null +++ b/peach-web/templates/not_found.html.tera @@ -0,0 +1,9 @@ +{%- extends "nav" -%} +{%- block card %} +
+
+

No PeachCloud resource exists for this URL. Please ensure that the URL in the address bar is correct.

+

Click the back arrow in the top-left or the PeachCloud logo at the bottom of your screen to return Home.

+
+
+{%- endblock card -%} diff --git a/peach-web/templates/password/change_password.html.tera b/peach-web/templates/password/change_password.html.tera new file mode 100644 index 0000000..10a0195 --- /dev/null +++ b/peach-web/templates/password/change_password.html.tera @@ -0,0 +1,52 @@ +{%- extends "nav" -%} +{%- block card %} + +
+ +
+ +
+
+ + + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ +
+ Cancel + + + + {% include "snippets/flash_message" %} + + + {% include "snippets/noscript" %} + +
+
+ + +{%- endblock card -%} diff --git a/peach-web/templates/password/reset_password.html.tera b/peach-web/templates/password/reset_password.html.tera new file mode 100644 index 0000000..0d37b29 --- /dev/null +++ b/peach-web/templates/password/reset_password.html.tera @@ -0,0 +1,50 @@ +{%- extends "nav" -%} +{%- block card %} + +
+ +
+ +
+
+ + + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ +
+ + + + {% include "snippets/flash_message" %} + + + {% include "snippets/noscript" %} + +
+
+ +{%- endblock card -%} diff --git a/peach-web/templates/password/send_password_reset.html.tera b/peach-web/templates/password/send_password_reset.html.tera new file mode 100644 index 0000000..fdf66f0 --- /dev/null +++ b/peach-web/templates/password/send_password_reset.html.tera @@ -0,0 +1,27 @@ +{%- extends "nav" -%} +{%- block card %} + +
+ +

+ Click the button below to send a new temporary password which can be used to change your device password. +

+ The temporary password will be sent in an SSB private message to the admin of this device. +

+ +
+
+ +
+
+ + + {% include "snippets/flash_message" %} + + + {% include "snippets/noscript" %} + + +
+ +{%- endblock card -%} diff --git a/peach-web/templates/peers.html.tera b/peach-web/templates/peers.html.tera new file mode 100644 index 0000000..4624fd9 --- /dev/null +++ b/peach-web/templates/peers.html.tera @@ -0,0 +1,21 @@ +{%- extends "nav" -%} +{%- block card %} + +
+
+ + + {%- if flash_msg and flash_name == "success" %} + +
{{ flash_msg }}.
+ {%- elif flash_msg and flash_name == "error" %} + +
{{ flash_msg }}.
+ {%- endif %} + + +
+
+{%- endblock card -%} diff --git a/peach-web/templates/profile.html.tera b/peach-web/templates/profile.html.tera new file mode 100644 index 0000000..406c1ee --- /dev/null +++ b/peach-web/templates/profile.html.tera @@ -0,0 +1,21 @@ +{%- extends "nav" -%} +{%- block card %} + +
+
+ + + {%- if flash_msg and flash_name == "success" %} + +
{{ flash_msg }}.
+ {%- elif flash_msg and flash_name == "error" %} + +
{{ flash_msg }}.
+ {%- endif %} + + +
+
+{%- endblock card -%} diff --git a/peach-web/templates/shutdown.html.tera b/peach-web/templates/shutdown.html.tera new file mode 100644 index 0000000..f2c8c50 --- /dev/null +++ b/peach-web/templates/shutdown.html.tera @@ -0,0 +1,30 @@ +{%- extends "nav" -%} +{%- block card %} + +
+
+ + + + + {%- if flash_msg and flash_name == "success" %} + +
{{ flash_msg }}.
+ {%- elif flash_msg and flash_name == "error" %} + +
{{ flash_msg }}.
+ {%- endif %} + + +
+
+ +{%- endblock card -%} diff --git a/peach-web/templates/snippets/flash_message.html.tera b/peach-web/templates/snippets/flash_message.html.tera new file mode 100644 index 0000000..0c967ce --- /dev/null +++ b/peach-web/templates/snippets/flash_message.html.tera @@ -0,0 +1,11 @@ + +{% if flash_msg and flash_name == "success" %} + +
{{ flash_msg }}.
+{%- elif flash_msg and flash_name == "info" %} + +
{{ flash_msg }}.
+{%- elif flash_msg and flash_name == "error" %} + +
{{ flash_msg }}.
+{%- endif -%} \ No newline at end of file diff --git a/peach-web/templates/snippets/noscript.html.tera b/peach-web/templates/snippets/noscript.html.tera new file mode 100644 index 0000000..5ae7793 --- /dev/null +++ b/peach-web/templates/snippets/noscript.html.tera @@ -0,0 +1,6 @@ + + \ No newline at end of file