diff --git a/.cargo/config.toml b/.cargo/config.toml index 9dc3894..a4139ed 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -4,7 +4,8 @@ [alias] # No default features avoids selecting the default target (esp32c6) which would conflict with other target features. - +# -Z only works on nightly +#build-esp32 = "build --release --target xtensa-esp32-none-elf --no-default-features --features esp32" build-esp32 = "build --release --target xtensa-esp32-none-elf --no-default-features --features esp32 -Z build-std=core,alloc" build-esp32c2 = "build --release --target riscv32imc-unknown-none-elf --no-default-features --features esp32c2 " build-esp32c3 = "build --release --target riscv32imc-unknown-none-elf --no-default-features --features esp32c3 " @@ -35,21 +36,20 @@ test-ota = "test --package ota --target x86_64-unknown-linux-gnu" build-ota-packer = "build --package ota --bin ota-packer --target x86_64-unknown-linux-gnu" ota-packer = "run --package ota --bin ota-packer --target x86_64-unknown-linux-gnu" - -[target.xtensa-esp32-none-elf] +[target.xtensa-esp32-none-elf] # ESP32 runner = "espflash flash --baud=921600 --monitor --chip esp32" rustflags = ["-C", "link-arg=-nostartfiles", '--cfg=feature="esp32"'] -[target.riscv32imc-unknown-none-elf] +[target.riscv32imc-unknown-none-elf] # ESP32-C2 / ESP32-C3 runner = "espflash flash --baud=921600 --monitor" rustflags = ["-C", "force-frame-pointers"] -[target.riscv32imac-unknown-none-elf] +[target.riscv32imac-unknown-none-elf] # ESP32C6 runner = "espflash flash --baud=921600 --partition-table partitions.csv --monitor" rustflags = ["-C", "force-frame-pointers"] -[target.xtensa-esp32s2-none-elf] +[target.xtensa-esp32s2-none-elf] # ESP32-S2 runner = "espflash flash --baud=921600 --monitor --chip esp32s2" rustflags = ["-C", "link-arg=-nostartfiles", '--cfg=feature="esp32s2"'] -[target.xtensa-esp32s3-none-elf] +[target.xtensa-esp32s3-none-elf] # ESP32-S3 runner = "espflash flash --baud=921600 --monitor --chip esp32s3" rustflags = ["-C", "link-arg=-nostartfiles", '--cfg=feature="esp32s3"'] @@ -70,6 +70,9 @@ target = "riscv32imac-unknown-none-elf" # target = "x86_64-unknown-linux-gnu" [unstable] -build-std = [] -# build-std core and alloc has been moved as per alias parameter since -# `unstable` section MUST be global and cannot be definde per target. +build-std = ["core","alloc"] +# build-std = [] +# TODO Resolve build-std for different targets on stable channel. +# build-std = ["core","alloc"] # For ESP32, ESP32-S2, ESP32-S3: When using stable channel enable this. -Z option only works on nightly channel. +# build-std core and alloc has been moved as per alias parameter since +# `unstable` section MUST be global and cannot be defined per target. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e4afb33..f8d8654 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -52,7 +52,7 @@ jobs: - name: Check lints and format if: ${{ contains(fromJson('["esp32c6"]'), matrix.device.soc) }} run: | - cargo +${{ matrix.device.toolchain }} clippy --features ${{ matrix.device.soc }} --target riscv32imac-unknown-none-elf -- -D warnings + cargo +${{ matrix.device.toolchain }} clippy --release --features ${{ matrix.device.soc }} --target riscv32imac-unknown-none-elf -- -D warnings cargo +${{ matrix.device.toolchain }} fmt -- --check ota-packer: name: OTA Packer @@ -68,4 +68,4 @@ jobs: target: riscv32imac-unknown-none-elf toolchain: stable - name: Build utility - run: cargo build-ota-packer \ No newline at end of file + run: cargo build-ota-packer diff --git a/.gitignore b/.gitignore index 911255d..dc43000 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,6 @@ target/ # VSCode workspace(s) *.code-workspace + +# Temporarily ignore book from this branch +docs/book diff --git a/.vscode/settings.json b/.vscode/settings.json index 979d509..c4db1c1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ { - "rust-analyzer.check.allTargets": false, + "rust-analyzer.check.allTargets": false, } diff --git a/Cargo.lock b/Cargo.lock index 1bd82e2..80f1827 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -134,7 +134,7 @@ checksum = "f48d6ace212fdf1b45fd6b566bb40808415344642b76c3224c07c8df9da81e97" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.114", ] [[package]] @@ -145,9 +145,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "block-buffer" @@ -210,18 +210,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.59" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5caf74d17c3aec5495110c34cc3f78644bfa89af6c8993ed4de2790e49b6499" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.59" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "370daa45065b80218950227371916a1633217ae42b2715b2287b606dcd618e24" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" dependencies = [ "anstream", "anstyle", @@ -306,7 +306,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.114", ] [[package]] @@ -350,7 +350,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.116", + "syn 2.0.114", ] [[package]] @@ -363,7 +363,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.114", ] [[package]] @@ -376,7 +376,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.116", + "syn 2.0.114", ] [[package]] @@ -387,7 +387,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn 2.0.116", + "syn 2.0.114", ] [[package]] @@ -398,7 +398,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core 0.21.3", "quote", - "syn 2.0.116", + "syn 2.0.114", ] [[package]] @@ -409,7 +409,7 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core 0.23.0", "quote", - "syn 2.0.116", + "syn 2.0.114", ] [[package]] @@ -420,7 +420,7 @@ checksum = "780eb241654bf097afb00fc5f054a09b687dad862e485fdcf8399bb056565370" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.114", ] [[package]] @@ -493,9 +493,9 @@ dependencies = [ [[package]] name = "edge-nal-embassy" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "252f89adf4f0016631977bec3ba50d768263a3a9fa9f023b4087088a619568ce" +checksum = "fb09ea0c604bbcb694ecf9cf6596bc5f59bbb5360b68c3e471b61ae9a55a8533" dependencies = [ "edge-nal", "embassy-futures", @@ -583,7 +583,7 @@ dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.114", ] [[package]] @@ -845,7 +845,7 @@ dependencies = [ "darling 0.21.3", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.114", ] [[package]] @@ -924,7 +924,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54786287c0a61ca0f78cb0c338a39427551d1be229103b4444591796c579e093" dependencies = [ "bitfield", - "bitflags 2.11.0", + "bitflags 2.10.0", "bytemuck", "cfg-if", "critical-section", @@ -986,7 +986,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.114", "termcolor", ] @@ -1252,31 +1252,32 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", "futures-task", "pin-project-lite", + "pin-utils", ] [[package]] @@ -1406,7 +1407,7 @@ dependencies = [ "indoc", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.114", ] [[package]] @@ -1423,9 +1424,9 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jiff" -version = "0.2.20" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c867c356cc096b33f4981825ab281ecba3db0acefe60329f044c1789d94c6543" +checksum = "d89a5b5e10d5a9ad6e5d1f4bd58225f655d6fe9767575a5e8ac5a6fe64e04495" dependencies = [ "jiff-static", "log", @@ -1436,20 +1437,20 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.20" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7946b4325269738f270bb55b3c19ab5c5040525f83fd625259422a9d25d9be5" +checksum = "ff7a39c8862fc1369215ccf0a8f12dd4598c7f6484704359f0351bd617034dbf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.114", ] [[package]] name = "libc" -version = "0.2.182" +version = "0.2.181" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" [[package]] name = "linked_list_allocator" @@ -1504,7 +1505,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.114", ] [[package]] @@ -1534,7 +1535,7 @@ checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.114", ] [[package]] @@ -1600,6 +1601,12 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "poly1305" version = "0.8.0" @@ -1644,7 +1651,7 @@ checksum = "a33fa6ec7f2047f572d49317cca19c87195de99c6e5b6ee492da701cfe02b053" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.114", ] [[package]] @@ -1722,7 +1729,7 @@ checksum = "7d323d13972c1b104aa036bc692cd08b822c8bbf23d79a27c526095856499799" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.114", ] [[package]] @@ -1751,7 +1758,7 @@ checksum = "def519ddeeb5e43c2b4fc3952c27b3a86782fc05192f322b2309125cd85b1fc3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.114", ] [[package]] @@ -1839,7 +1846,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.114", ] [[package]] @@ -1914,7 +1921,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.114", ] [[package]] @@ -1971,7 +1978,7 @@ dependencies = [ [[package]] name = "ssh-stamp" -version = "0.1.0" +version = "0.2.0" dependencies = [ "bcrypt", "cfg-if", @@ -2002,7 +2009,6 @@ dependencies = [ "hex", "hmac", "log", - "once_cell", "ota", "paste", "portable-atomic", @@ -2017,6 +2023,7 @@ dependencies = [ "subtle", "sunset", "sunset-async", + "sunset-sftp", "sunset-sshwire-derive", ] @@ -2072,7 +2079,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.114", ] [[package]] @@ -2083,8 +2090,8 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sunset" -version = "0.4.0" -source = "git+https://github.com/mkj/sunset#6b4e77f5210fd375f7ba7bf49940d7f1ac80c230" +version = "0.3.0" +source = "git+https://github.com/jubeormk1/sunset?branch=dev%2Fsftp-start#9d76de8a261736e8b4b8bfc35b61d6feccbf63a8" dependencies = [ "aes", "ascii", @@ -2114,8 +2121,8 @@ dependencies = [ [[package]] name = "sunset-async" -version = "0.4.0" -source = "git+https://github.com/mkj/sunset#6b4e77f5210fd375f7ba7bf49940d7f1ac80c230" +version = "0.3.0" +source = "git+https://github.com/jubeormk1/sunset?branch=dev%2Fsftp-start#9d76de8a261736e8b4b8bfc35b61d6feccbf63a8" dependencies = [ "embassy-futures", "embassy-sync 0.7.2", @@ -2125,10 +2132,26 @@ dependencies = [ "sunset", ] +[[package]] +name = "sunset-sftp" +version = "0.1.2" +source = "git+https://github.com/jubeormk1/sunset?branch=dev%2Fsftp-start#9d76de8a261736e8b4b8bfc35b61d6feccbf63a8" +dependencies = [ + "embassy-futures", + "embassy-sync 0.7.2", + "embedded-io-async 0.6.1", + "log", + "num_enum", + "paste", + "sunset", + "sunset-async", + "sunset-sshwire-derive", +] + [[package]] name = "sunset-sshwire-derive" -version = "0.2.1" -source = "git+https://github.com/mkj/sunset#6b4e77f5210fd375f7ba7bf49940d7f1ac80c230" +version = "0.2.0" +source = "git+https://github.com/jubeormk1/sunset?branch=dev%2Fsftp-start#9d76de8a261736e8b4b8bfc35b61d6feccbf63a8" dependencies = [ "virtue", ] @@ -2159,9 +2182,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.116" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", @@ -2200,9 +2223,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.9+spec-1.1.0" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" dependencies = [ "winnow", ] @@ -2221,9 +2244,9 @@ checksum = "e87a2ed6b42ec5e28cc3b94c09982969e9227600b2e3dcbc1db927a84c06bd69" [[package]] name = "unicode-ident" -version = "1.0.24" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" [[package]] name = "unicode-width" @@ -2365,7 +2388,7 @@ checksum = "96fb42cd29c42f8744c74276e9f5bee7b06685bbe5b88df891516d72cb320450" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.114", ] [[package]] @@ -2385,5 +2408,5 @@ checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.114", ] diff --git a/Cargo.toml b/Cargo.toml index 50b81ef..e299705 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,8 +4,10 @@ [package] name = "ssh-stamp" -version = "0.1.0" -authors = ["Roman Valls Guimera "] +version = "0.2.0" +authors = [ "Julio Beltran Ortega ", + "Anthony Tambasco ", + "Roman Valls Guimera "] edition = "2024" license = "MIT OR Apache-2.0" @@ -13,21 +15,21 @@ license = "MIT OR Apache-2.0" members = ["storage", "ota"] [workspace.dependencies] - -sunset = { git = "https://github.com/mkj/sunset", default-features = false, features = [ +sunset = { git = "https://github.com/jubeormk1/sunset", branch = "dev/sftp-start", default-features = false, features = [ "openssh-key", "embedded-io", ] } -sunset-async = { git = "https://github.com/mkj/sunset", default-features = false, features = [ +sunset-async = { git = "https://github.com/jubeormk1/sunset", branch = "dev/sftp-start", default-features = false, features = [ "multi-thread", ] } -sunset-sshwire-derive = { git = "https://github.com/mkj/sunset", default-features = false } +sunset-sshwire-derive = { git = "https://github.com/jubeormk1/sunset", branch = "dev/sftp-start", default-features = false } +sunset-sftp = { git = "https://github.com/jubeormk1/sunset", branch = "dev/sftp-start", default-features = false } sha2 = { version = "0.10", default-features = false } -rustc-hash = { version = "2.1", default-features = false } +rustc-hash = { version = "2.1", default-features = false } # For hash in the OTA SFTP server TODO: For the optimisation task: Remove this dependency and use sha2 instead? log = "0.4" -esp-hal = { version = "1.0.0", features = ["unstable", "log-04"] } +esp-hal = { version = "1", features = ["unstable", "log-04"] } embedded-storage = "0.3.1" esp-storage = { version = "0.8.0" } esp-bootloader-esp-idf = { version = "0.4.0" } @@ -39,22 +41,11 @@ once_cell = { version = "1", features = [ ], default-features = false } [dependencies] - storage = { path = "storage" } ota = { path = "ota" } -sunset = { workspace = true } -sunset-async = { workspace = true } -sunset-sshwire-derive = { workspace = true } sha2 = { workspace = true } rustc-hash = { workspace = true } -log = { workspace = true } -esp-hal = { workspace = true } -embedded-storage = { workspace = true } -esp-storage = { workspace = true } -esp-bootloader-esp-idf = { workspace = true } -static_cell = { workspace = true } -once_cell = { workspace = true } cfg-if = "1" ed25519-dalek = { version = "2", default-features = false } @@ -71,23 +62,30 @@ smoltcp = { version = "0.12", default-features = false, features = [ ] } embassy-time = { version = "0.5.0" } embedded-io-async = "0.6.1" -esp-alloc = "0.9.0" +esp-alloc = { version ="0.9.0"} esp-backtrace = { version = "0.18.0", features = ["panic-handler", "println"] } - +esp-hal = { workspace = true } esp-rtos = { version = "0.2.0", features = ["embassy", "log-04", "esp-radio"] } esp-radio = { version = "0.17.0", features = ["wifi", "log-04"] } esp-println = { version = "0.16", features = ["log-04"] } hex = { version = "0.4", default-features = false } - +log = { version = "0.4" } +static_cell = { workspace = true } ssh-key = { version = "0.6", default-features = false, features = ["ed25519"] } +sunset = { workspace = true } +sunset-async = { workspace = true } +sunset-sshwire-derive = { workspace = true } +sunset-sftp = { workspace = true } getrandom = { version = "0.2.10", features = ["custom"] } embassy-sync = "0.7" heapless = "0.8" embassy-futures = "0.1.2" edge-dhcp = "0.6" edge-nal = "0.5" -edge-nal-embassy = "0.6" +edge-nal-embassy = "0.7" #sequential-storage = { version = "4", features = ["heapless"] } +esp-storage = { workspace = true } +embedded-storage = { workspace = true } embassy-embedded-hal = "0.3" bcrypt = { version = "0.17", default-features = false } subtle = { version = "2", default-features = false } @@ -99,6 +97,7 @@ digest = { version = "0.10", default-features = false, features = [ ] } embedded-storage-async = "0.4" portable-atomic = "1" +esp-bootloader-esp-idf = { workspace = true } snafu = { version = "0.8", default-features = false } paste = "1" pretty-hex = { version = "0.4", default-features = false } @@ -131,8 +130,9 @@ opt-level = "s" [features] ipv6 = [] + +# Enables the SFTP OTA Subsystem. Use ota-packer to pack a binary and PUT it over sftp sftp-ota = [] -default = ["esp32c6"] # MCU options esp32 = [ @@ -166,7 +166,7 @@ esp32c3 = [ "esp-bootloader-esp-idf/esp32c3", ] #esp32c5 = [ -# "esp-hal/esp32c5", +# "esp-hal/esp32c5", # "esp-alloc/esp32c5", # "esp-backtrace/esp32c5", # "esp-radio/esp32c5", diff --git a/README.md b/README.md index 2e8a073..aff59ca 100644 --- a/README.md +++ b/README.md @@ -39,59 +39,94 @@ A "low level to SSH Swiss army knife". Rust versions are controlled via `rust-toolchain.toml` and the equivalent defined on the CI workflow. -Required for all targets: +On a fresh system the following should be enough to build and run on the relevant ESP32 dev boards. + +## Required for all targets: ``` rustup toolchain install stable --component rust-src cargo install espflash ``` -On a fresh system the following should be enough to build and run on an ESP32-C6 dev board. +## ESP32-C6 + + ``` rustup target add riscv32imac-unknown-none-elf cargo build-esp32c6 cargo run-esp32c6 ``` -Build and run for ESP32-C2 / ESP32-C3: +## ESP32-C2 / ESP32-C3 +``` +rustup target add riscv32imc-unknown-none-elf` +``` +### ESP32-C2 ``` -rustup target add riscv32imc-unknown-none-elf cargo build-esp32c2 -cargo build-esp32c3 cargo run-esp32c2 +``` +### ESP32-C3 +``` +cargo build-esp32c2 cargo run-esp32c3 ``` -Build for ESP32 / ESP32-S2 / ESP32-S3 (Xtensa Cores) - + +## ESP32 / ESP32-S2 / ESP32-S3 (Xtensa Cores) Install esp toolchain first: https://github.com/esp-rs/espup ``` -cargo install espup -espup install -$HOME/export-esp.sh -rustup override set esp +cargo install espup +espup install +source $HOME/export-esp.sh +``` + +### ESP32 +``` +cargo +esp build-esp32 +cargo +esp run-esp32 +``` +### ESP32-S2 +``` +cargo +esp build-esp32s2 +cargo +esp run-esp32s2 +``` +### ESP32-S3 +``` +cargo +esp build-esp32s3 +cargo +esp run-esp32s3 +``` + +### Using rustup toolchain override (Doesn't require `+esp`) +To set rustup override: +``` +rustup override set esp +``` +To remove rustup override: +``` +cargo override unset +``` +Build: +``` cargo build-esp32 cargo build-esp32s2 cargo build-esp32s3 ``` -Running on the target: +Run: ``` cargo run-esp32 cargo run-esp32s2 cargo run-esp32s3 ``` -Alternatively to not use rustup override: -``` -cargo +esp build-esp32 -cargo +esp build-esp32s2 -cargo +esp build-esp32s3 -``` -Running on the target: -``` -cargo +esp run-esp32 -cargo +esp run-esp32s2 -cargo +esp run-esp32s3 -``` +# Default UART Pins +| Target | RX | TX | +| ---- | -- | -- | +| ESP32 | 13 | 14 | +| ESP32S2 | 11 | 10 | +| ESP32C2 | 9 | 10 | +| ESP32C3 | 20 | 21 | +| ESP32C6 | 11 | 10 | # Example usecases @@ -117,3 +152,11 @@ cargo cyclonedx -f json --manifest-path ./docs/ [nlnet-grant]: https://nlnet.nl/project/SSH-Stamp/ [openwrt_mediatek_no_monitor]: https://github.com/openwrt/openwrt/issues/16279 [nlnet_zero_commons]: ./docs/nlnet/zero_commons_logo.svg + +/dev/ttyUSB0 is ESP32 + +USB to UART +sudo minicom --device /dev/ttyUSB1 + + +https://docs.espressif.com/projects/esp-matter/en/latest/esp32/optimizations.html diff --git a/docs/nlnet/mou.md b/docs/nlnet/mou.md new file mode 100644 index 0000000..38bc0ea --- /dev/null +++ b/docs/nlnet/mou.md @@ -0,0 +1,369 @@ +takentaal v1.0 + + + +# SSH Stamp + + + +The aim of this project (available at https://github.com/brainstorm/ssh-stamp) is to write software that executes code on a microprocessor and allows transferring data between the internet and various electronic interfaces. In other words: a logic "bridge" between the internet and low level electronic interfaces. + + + +In its conception, the SSH Stamp is a secure bridge between wireless and a serial port (ubiquitous interface also known as "UART"), implemented in the Rust programming language. The Rust programming language offers convenient memory safety guarantees that are meaningful for this project from a systems security perspective. + + + +From a practical standpoint, SSH Stamp allows its users (from different levels of expertise) to audit, monitor, defend, operate, understand and maintain a variety of low level electronics remotely (and securely) through the internet at large. + + + +## {240} Password-based authentication (GH issue #20) + + + +Currently, SSH Stamp accepts ANY arbitrary password when logging in via the SSH protocol. This is a well known and big security issue that requires attention to be put on the password authentication handler. + + + +On the server side, there should be a persistent setting that completely disables password-based authentication (only accepting safer key-based authentication method (see related GH issues #21 and #23)). + + + +- {120} Implement password authentication server-side toggle. + +- {120} Implement handler business logic and connect it with GH issue #23. + + + +## {400} Have the server generate a unique server keypair and store it persistently (on boot) (GH issue #38) + + + +Currently, SSH Stamp accepts any (hardcoded) private key, as implied in the top-level documentation. This is a well known and big security issue that requires attention to be put on the public/private key authentication handler. + + + +Server keys are used by the client to identify the server in a secure way. Ideally those keys should be generated at first boot and optionally re-generated if the user desires to do so. + + + +- {400} Implement in-device private key generation and storage, test and audit its logic. + + + +## {200} Public key-based authentication (GH issue #21) + + + +The user should be able to provide concatenate public keys as "authorized_keys" via environment variable(s) (see related GH issue #23). + + + +- {200} Implement pubkey authentication environment variable provisioning. + + + +## {1000} Provisioning based on (SSH) environment variables (GH issue #23) + + + +Many SSH clients pass environment variables to the server for many purposes. + + + +This task is meant to exploit this commonly used pattern to simplify provisioning of new SSH Stamps. Environment variables will be used to, among other functions: + + + +- {200} Define and persist the UART bridge parameters such as BAUD speed, stop bits. + +- {200} Compile a spreadsheet of commercially available "stamp boards" and set a default, unused UART physical pins pair. Allow the user to alter that default via SSH env settings (see GH issue #23). + +- {200} Pass the password and store it for subsequent sessions, disallowing un-authenticated password changes after first initialization. + +- {200} Pass the private key(s) and/or trigger in-device private key generation. + +- {200} Have an environment variable (or command) that instructs the processor to factory reset all non-volatile storage but keeps the firmware intact. + + + +This approach avoids having complicated device-programming-time deployment(s) of sensitive cryptographic material. + + + +Furthermore, provisioning the device this way yields a superior user experience: a device can be shipped on an unprovisioned state and subsequently be **NON INTERACTIVELY** provisioned via SSH env vars. + + + +The emphasis on non-interactivity (and idempotency) is due to time tested patterns on the IT automation industry (see: Ansible, Puppet, Chef, SaltStack, etc...). + + + +## {1700} Firmware update via SFTP (GH issue #24) + + + +Instead of regular "out of band" firmware OTA update mechanisms, SSH Stamp firmware will be updated via its own secure protocol: SCP/SFTP. + + + +The reason behind this approach is that we can leverage the existing SSH server AAA mechanisms to deploy new firmware securely. + + + +Ideally, this process shouldn't have to involve the bootloader since this should happen at a "userspace + reboot" level. + + + +- {1300} Implement SCP/SFTP as per IETF RFCs. Only the functionality needed to support firmware update. + +- {300} Implement OTA mechanism on device. + +- {100} Audit and thoroughly check that new firmware cannot be altered pre-auth. + + + +The changes described above will most likely need to be applied to SSH Stamp's main current "core" dependency: "sunset". This will therefore constitute an external (to SSH Stamp) OSS contribution that will be used in our software. + + + +## {2000} FSMs + SansIO refactor (GH issue #25) + + + +This is a relatively big refactor but needed pre-requisite to introduce other low level I/O (protocols) beside UART such as SPI/I2C/CAN... it is also a research task since there's uncertainty on whether such a bridge between a (relatively slow) network stack and (generally faster) electrical protocols will tolerate strict timing demands. + + + +Moving to compile-time validated finite state machines (FSMs) will hopefully make pre-auth attacks less likely to be successful on SSH Stamp. Such attacks have been unfortunately common in recent SSH server explorations, see: + + + +https://threatprotect.qualys.com/2025/04/21/erlang-otp-ssh-server-remote-code-execution-vulnerability-cve-2025-32433/ + + + +https://www.runzero.com/blog/sshamble-unexpected-exposures-in-the-secure-shell/ + + + +The trigger of this refactoring idea is https://www.firezone.dev/blog/sans-io + + + +This approach should ideally be combined with some kind of performance profiling to ensure we don't regress after incorporating those changes. + + + +- {1100} Implement a PoC state machine on std outside SSH Stamp, validate corner cases there. + +- {500} Adapt PoC for no_std usage and structure. + +- {400} Refactor codebase for SansIO + FSM. + + + +## {900} General performance engineering (GH issue #28) + + + +Profiling and optimisation are tangentially mentioned towards the end of issue #25, but better tooling/process is needed to maximise the performance of this firmware on different fronts: + + + +- {100} Choose appropriate overall and per-task heap size allocations, i.e: bisect heap size until finding a clear performance degradation under different loads. + +- {100} Perform benchmarks for different RX/TX buffer sizes on both SSH network stack and UART packet queues. + +- {100} Minimise the build for size as much as possible without discarding security checks (overflow protections). + +- {300} Add CI/CD instrumentation to track all of the above over time, avoiding future regressions. + +- {300} Profile hot spots across the app, including cryptographic primitives, and optimise them. + + + +Those performance engineering techniques should be appropriately documented (preferably with examples) and ideally implemented in CI to monitor regressions. + + + +## {1000} Documentation (GH issue #30) + + + +Mid project, when the code based reaches relative maturity and stability, we should make a concerted effort to write and review docs at all levels: user, dev, architecture.md + + + +- {200} Make sure onboarding (and quickstart) is fully documented and flawless. + +- {200} Refer and implement an ARCHITECTURE.md document. Advice taken from Matklad's blogpost: https://matklad.github.io/2021/02/06/ARCHITECTURE.md.html + +- {120} List and document all the effects of environment variables from GH issue #23. + +- {120} List and document both supported and unsupported devices and boards. + +- {120} List and document ways to debug/profile/tweak firmware for different feature flags and scenarios. + +- {120} Have at least one real world scenario documented. + +- {120} Define clearly what's our threat model, i.e: we'll not protect against any physical attacks. OTOH, pre-auth attacks (exploit bad code logic that Rust doesn't protect against) can be the worst case scenario for SSH Stamp. + + + +## {600} Testing (GH issue #37) + + + +Test that the firmware compiles, has good coverage and there are no performance regressions as mentioned in other tasks. This task is a more focused effort so that there is comprehensive testing in both software and hardware. + + + +- {200} Leverage embedded-test Rust crate for hardware testing (HIL). + +- {200} Physically build a test harness with most of the supported targets so that they can be easily tested (ideally via CI/CD hooks). + +- {200} Test that a IPV6 address is served over DHCPv6. ULA, DHCP-PD and other common scenarios work as they should. + + + +## {500} Compile project for all Espressif ESP32 WiFi targets (GH issue #18) + + + +The current project only compiles for the ESP32-C6, as in indicated in various feature flags and linker options. + + + +Find a non-intrusive way to compile for as many Espressif targets as possible. + + + +- {400} Implement cargo build targets for each Espressif microcontroller. + +- {100} Resolve compilation issues that arise for each target. + + + +## {800} Implement firmware support for alternative wifi enabled microcontrollers (other than Espressif) (GH issue #19) + + + +Implementing and testing support for other microcontrollers from a completely different manufacturer. + + + +- {200} Compile and flash SSH Stamp into this target with UART support. + +- {600} Try to support WiFi (currently not supported at the upstream HAL). + + + +## {2000} Add mlkem768x25519 (to sunset?) and integrate into SSH-Stamp (GH issue #34) + + + +Add mlkem768x25519 (post Quantum resistant crypto) to sunset dependency and integrate into SSH-Stamp. + + + +- {1500} Implement and test MLKEM upstream in sunset. + +- {500} Test in SSH Stamp. + + + +## {1080} Bootloader audit and UX improvements (GH issue #35) + + + +Some users will expect that the bridge is established against their own dev board USB2TTL converted UART0 port instead of custom discrete pins. + + + +The current approach is to use UART1 and custom pins (soon to be GPIO9 and GPIO10 by default) so that boot messages and early SSH2UART are not intermixed. + +This task will have to: + + + +- {600} Explore how the default-shipped ESP-IDF 5.x bootloader is packaged in esp-hal's firmware. + +- {240} Conditionally modify the bootloader accordingly so no boot messages are displayed when powering up the board and no println!/debug messages from the user level firmware itself are shown either. In other words: Only bridged SSH2UART messages should be conveyed. + +- {120} Make sure those bootloader modifications are not an impediment to basic operations such as re-flashing the IC (i.e: perhaps espflash expects some bootloader output to trigger a flashing operation?). + +- {120} Document a path towards a secure bootloader that is compatible with the HAL/device **and** respects the aforementioned points. + + + +This exploration into the bootloader inner workings can also pave the way to assess whether improvements in secure boot are interesting to pursue and how. + + + +## {840} Implement WiFi STA mode (GH issue #36) + + + +Currently SSH Stamp boots up in AP (Access Point) mode and expects DHCP clients. This tasks would put SSH Stamp in STA (Station mode) or acting as a WiFi client instead. + + + +This is attractive on more permanent installations where SSH Stamp can be part of a larger local wireless network. + + + +- {240} Make appropriate changes to device HAL initialization. + +- {240} Verify that those changes do not break prior assumptions. + +- {240} Align onboarding process for that mode's particularities. + +- {120} Make the setting configurable (see GH issue #23). + + + +## {200} Licensing (GH issue #22) + + + +There's some borrowed code from SSH Stamp's main dependency: sunset. Clarify this properly and/or find the right way to compose LICENSE wording. + + + +- {200} Process feedback and apply changes from NLNet licensing experts. + + + +## {2000} Implement bridging support for other electrical protocols such as: CAN, SPI and I2C. + + + +Encapsulate transaction-oriented protocols (CAN, SPI, I2C) over a plain byte stream. This task assumes that the the processor's timing characteristics are suitable. At a protocol level, for CAN packet encapsulation there's "slcan" and "GVRET" among other more exotic encapsulation protocols. + + + +Equivalents for SPI-over-Network or I2C-over-Network are a much more experimental and uncharted territory, perhaps the Bus Pirate command protocol would be the closest match. + + + +- {1000} Implement slcan and/or GVRET for SSH_to_CAN support. + +- {1000} Investigate and implement proof of concept for SPI and/or I2C. + + + +## {600} Final release + + + +Final review on all the work done and refinement, optimization and security audit handover. + + + +- {120} Stable release. + +- {240} Process feedback from security audit and accessibility scan. + +- {240} Final release with updated documentation. diff --git a/docs/nlnet/plan.md b/docs/nlnet/plan.md new file mode 100644 index 0000000..989acea --- /dev/null +++ b/docs/nlnet/plan.md @@ -0,0 +1,34 @@ +```mermaid +gantt + title SSH Stamp (a.k.a esp-ssh-rs) development plan under NLNet grant + dateFormat YYYY-MM-DD + excludes weekends + tickInterval 1day + weekday monday + todayMarker off + axisFormat %e + section Production + Fix password auth : passwd, 2025-05-01, 24h + Fix pubkey auth : pubkey, after passwd, 24h + UART perf : uart_intr, after pubkey, 12h + section Provisioning + Provisioning : prov, after uart_intr, 16h + OTA updates : ota, after prov, 12h + section Docs + usage docs : usage_docs, after ota, 4h + dev docs : dev_docs, after ota, 2h + section Robustness + #forbid(unsafe) : no_unsafe, after ota, 12h + sans-io refactor : sans_io, after uart_intr, 16h + section Multi-target + Espressif chips : all_espressif, after no_unsafe, 12h + Other chip1 : chip1, after all_espressif, 20h + Other chip2 : chip2, after chip1, 18h + section Testing + CI/CD : ci, after chip2, 16h + hardware in test loop : HIL, after ci, 21h + Users test : user_tests, after dev_docs, 9h + section Security + Self audit : self_sec_audit, after all_espressif, 10h + NLNet security audit? : nlnet_sec_audit, after all_espressif, 45h +``` diff --git a/ota/Cargo.toml b/ota/Cargo.toml index 70d4edd..131e4f7 100644 --- a/ota/Cargo.toml +++ b/ota/Cargo.toml @@ -20,4 +20,4 @@ clap = "4.5" [[bin]] name = "ota-packer" -path = "src/bin/ota-packer.rs" +path = "src/bin/ota-packer.rs" \ No newline at end of file diff --git a/partitions.csv b/partitions.csv index 085821e..34340f6 100644 --- a/partitions.csv +++ b/partitions.csv @@ -1,6 +1,9 @@ # ESP-IDF Partition Table (wont fit in 4MB targets) # Name, Type, SubType, Offset, Size, Flags app_config, data, nvs, 0x9000, 0x2000, +# between app_config and otadata there is0x2000 bytes of free space otadata, data, ota, 0xd000, 0x2000, phy_init, data, phy, 0xf000, 0x1000, -factory, app, factory, 0x10000, 0x1e0000, \ No newline at end of file +ota_0, app, ota_0, 0x010000, 0x1e0000, +ota_1, app, ota_1, 0x1f0000, 0x1e0000, +extra_data, data, nvs, 0x3d0000, 64K, diff --git a/src/config.rs b/src/config.rs index e5bc69d..bcd1521 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,7 +4,7 @@ use core::net::Ipv6Addr; use embassy_net::{Ipv4Cidr, StaticConfigV4}; #[cfg(feature = "ipv6")] use embassy_net::{Ipv6Cidr, StaticConfigV6}; -use heapless::{String, Vec}; +use heapless::String; use esp_println::dbg; @@ -20,8 +20,7 @@ use sunset::{ sshwire::{SSHDecode, SSHEncode, SSHSink, SSHSource, WireError, WireResult}, }; -use crate::pins::SerdePinConfig; -use crate::settings::{DEFAULT_SSID, KEY_SLOTS}; +use crate::settings::{DEFAULT_SSID, DEFAULT_UART_RX_PIN, DEFAULT_UART_TX_PIN, KEY_SLOTS}; #[derive(Debug, PartialEq)] pub struct SSHStampConfig { @@ -35,7 +34,7 @@ pub struct SSHStampConfig { /// WiFi pub wifi_ssid: String<32>, - pub wifi_pw: Option>, // TODO: Why not 64? + pub wifi_pw: Option>, // Max 64 characters including null-terminator? /// Networking /// TODO: Populate this field from esp's hardware info or just refer it from HAL? @@ -46,9 +45,26 @@ pub struct SSHStampConfig { #[cfg(feature = "ipv6")] pub ipv6_static: Option, /// UART - pub uart_pins: SerdePinConfig, + pub uart_pins: UartPins, } +#[derive(Debug, PartialEq)] +pub struct UartPins { + pub rx: u8, + pub tx: u8, +} + +impl Default for UartPins { + fn default() -> Self { + // sensible defaults for UART pins; adjust if your board uses different pins + UartPins { + rx: DEFAULT_UART_RX_PIN, + tx: DEFAULT_UART_TX_PIN, + } + } +} +use esp_println::println; + impl SSHStampConfig { /// Bump this when the format changes pub const CURRENT_VERSION: u8 = 6; @@ -60,12 +76,15 @@ impl SSHStampConfig { let hostkey = SignKey::generate(KeyType::Ed25519, None)?; // TODO: Those env events come from system's std::env / core::env (if any)... so it shouldn't be unsafe() - let wifi_ssid_str = String::try_from(DEFAULT_SSID).unwrap(); - let wifi_ssid: String<32> = wifi_ssid_str; + let wifi_ssid = Self::default_ssid(); let mac = random_mac()?; let wifi_pw = None; - let uart_pins = SerdePinConfig::default(); + let uart_pins = UartPins::default(); + println!( + "SSH Stamp Config new() - RX Pin: {} TX Pin: {}", + uart_pins.rx, uart_pins.tx + ); Ok(SSHStampConfig { hostkey, @@ -95,6 +114,12 @@ impl SSHStampConfig { } } + pub(crate) fn default_ssid() -> String<32> { + let mut s = String::<32>::new(); + s.push_str(DEFAULT_SSID).unwrap(); + s + } + // pub fn config_change(&mut self, conf: SSHConfig) -> Result<()> { // ServEvent::ConfigChange(); // } @@ -139,6 +164,18 @@ where bool::dec(s)?.then(|| SSHDecode::dec(s)).transpose() } +// encode Option> as a bool then the &str contents (heapless::String doesn't implement SSHEncode) +pub(crate) fn enc_option_str( + v: &Option>, + s: &mut dyn SSHSink, +) -> WireResult<()> { + v.is_some().enc(s)?; + if let Some(st) = v { + st.as_str().enc(s)?; + } + Ok(()) +} + fn enc_ipv4_config(v: &Option, s: &mut dyn SSHSink) -> WireResult<()> { v.is_some().enc(s)?; if let Some(v) = v { @@ -178,12 +215,11 @@ where return Err(WireError::PacketWrong); } let gw: Option = dec_option(s)?; - let gateway = gw.map(Ipv4Addr::from_bits); Ok(StaticConfigV4 { address: Ipv4Cidr::new(ad, prefix), gateway, - dns_servers: Vec::new(), + dns_servers: Default::default(), }) }) .transpose() @@ -227,15 +263,16 @@ impl SSHEncode for SSHStampConfig { } self.wifi_ssid.as_str().enc(s)?; - enc_option(&self.wifi_pw, s)?; + enc_option_str::<63>(&self.wifi_pw, s)?; self.mac.enc(s)?; enc_ipv4_config(&self.ipv4_static, s)?; #[cfg(feature = "ipv6")] enc_ipv6_config(&self.ipv6_static, s)?; - // Encode PinConfig - self.uart_pins.enc(s)?; + // Encode UartPins + self.uart_pins.rx.enc(s)?; + self.uart_pins.tx.enc(s)?; Ok(()) } @@ -267,7 +304,9 @@ impl<'de> SSHDecode<'de> for SSHStampConfig { // Not supported by sshwire-derive nor virtue (no Option support) // let uart_pins = SSHDecode::dec(s)?; - let uart_pins = SSHDecode::dec(s)?; + let rx: u8 = SSHDecode::dec(s)?; + let tx: u8 = SSHDecode::dec(s)?; + let uart_pins = UartPins { rx, tx }; Ok(Self { hostkey, diff --git a/src/espressif/buffered_uart.rs b/src/espressif/buffered_uart.rs index 366a3d8..413c899 100644 --- a/src/espressif/buffered_uart.rs +++ b/src/espressif/buffered_uart.rs @@ -2,18 +2,27 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -use portable_atomic::{AtomicUsize, Ordering}; - -use embassy_futures::select::select; -use embassy_sync::pipe::TryWriteError; /// Wrapper around bidirectional embassy-sync Pipes, in order to handle UART /// RX/RX happening in an InterruptExecutor at higher priority. /// /// Doesn't implement the InterruptExecutor, in the task in the app should await /// the 'run' async function. +/// +use crate::config::SSHStampConfig; +use embassy_futures::select::select; +use embassy_sync::pipe::TryWriteError; +use embassy_sync::signal::Signal; use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, pipe::Pipe}; use esp_hal::Async; -use esp_hal::uart::Uart; +use esp_hal::gpio::AnyPin; +use esp_hal::peripherals::UART1; +use esp_hal::system::software_reset; +use esp_hal::uart::{Config, RxConfig, Uart}; +use esp_println::dbg; +use esp_println::println; +use portable_atomic::{AtomicUsize, Ordering}; +use static_cell::StaticCell; +use sunset_async::SunsetMutex; // Sizes of the software buffers. Inward is more // important as an overrun here drops bytes. A full outward @@ -31,7 +40,7 @@ pub struct BufferedUart { dropped_rx_bytes: AtomicUsize, } -pub struct Config {} +pub struct UartConfig {} impl BufferedUart { pub fn new() -> Self { @@ -55,6 +64,7 @@ impl BufferedUart { let rd_from = async { loop { let n = uart_rx.read_async(&mut uart_rx_buf).await.unwrap(); + let mut rx_slice = &uart_rx_buf[..n]; // Write rx_slice to 'inward' pipe, dropping bytes rather than blocking if @@ -106,7 +116,7 @@ impl BufferedUart { self.dropped_rx_bytes.swap(0, Ordering::Relaxed) } - pub fn reconfigure(&self, _config: Config) { + pub fn reconfigure(&self, _config: &'static SunsetMutex) { todo!(); } } @@ -116,3 +126,152 @@ impl Default for BufferedUart { Self::new() } } + +pub async fn uart_buffer_disable() -> () { + // disable uart buffer + println!("UART buffer disabled"); + // TODO: Correctly disable/restart UART buffer and/or send messsage to user over SSH + software_reset(); +} +// use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; + +pub async fn uart_disable() -> () { + // disable uart + println!("UART disabled"); + // TODO: Correctly disable/restart UART and/or send messsage to user over SSH + software_reset(); +} + +#[derive(Default)] +pub struct GPIOS<'a> { + pub gpio9: Option>, + pub gpio10: Option>, + pub gpio11: Option>, + pub gpio12: Option>, + pub gpio13: Option>, + pub gpio14: Option>, + pub gpio20: Option>, + pub gpio21: Option>, +} + +pub static UART_BUF: StaticCell = StaticCell::new(); +pub static UART_SIGNAL: Signal = Signal::new(); + +pub async fn uart_buffer_wait_for_initialisation() -> &'static BufferedUart { + UART_BUF.init_with(BufferedUart::new) +} + +#[embassy_executor::task] +pub async fn uart_task( + uart_buf: &'static BufferedUart, + uart1: UART1<'static>, + _config: &'static SunsetMutex, + gpios: GPIOS<'static>, +) -> () { + dbg!("UART task started"); + + // Wait until ssh shell complete + UART_SIGNAL.wait().await; + dbg!("UART signal recieved"); + + // Temporarily hardcoded pin numbers. Restore once ServEvent::SessionEnv properly updates config + // let config_lock = config.lock().await; + // let rx: u8 = config_lock.uart_pins.rx; + // let tx: u8 = config_lock.uart_pins.tx; + cfg_if::cfg_if!( + if #[cfg(feature = "esp32")] { + let rx: u8 = 13; + let tx: u8 = 14; + } else if #[cfg(feature = "esp32c2")] { + let rx: u8 = 9; + let tx: u8 = 10; + } else if #[cfg(feature = "esp32c3")] { + let rx: u8 = 20; + let tx: u8 = 21; + } else { + let rx: u8 = 10; + let tx: u8 = 11; + } + ); + + println!("Config Read - RX Pin: {} TX Pin: {}", rx, tx); + if rx != tx { + let mut _holder9 = Some(gpios.gpio9); + let mut _holder10 = Some(gpios.gpio10); + let mut _holder11 = Some(gpios.gpio11); + let mut _holder13 = Some(gpios.gpio13); + let mut _holder14 = Some(gpios.gpio14); + let mut _holder20 = Some(gpios.gpio20); + let mut _holder21 = Some(gpios.gpio21); + // Not every GPIO is available for every target. + // TODO: Merge all targets to only match on available GPIO + cfg_if::cfg_if!( + if #[cfg(feature = "esp32")] { + let rx_pin = match rx { + 13 => _holder13.take().unwrap().unwrap(), + 14 => _holder14.take().unwrap().unwrap(), + _ => _holder13.take().unwrap().unwrap(), + }; + let tx_pin = match tx { + 13 => _holder13.take().unwrap().unwrap(), + 14 => _holder14.take().unwrap().unwrap(), + _ => _holder13.take().unwrap().unwrap(), + }; + } else if #[cfg(feature = "esp32c2")] { + let rx_pin = match rx { + 9 => _holder9.take().unwrap().unwrap(), + 10 => _holder10.take().unwrap().unwrap(), + _ => _holder9.take().unwrap().unwrap(), + }; + let tx_pin = match tx { + 9 => _holder9.take().unwrap().unwrap(), + 10 => _holder10.take().unwrap().unwrap(), + _ => _holder10.take().unwrap().unwrap(), + }; + } else if #[cfg(feature = "esp32c3")] { + let rx_pin = match rx { + 20 => _holder20.take().unwrap().unwrap(), + 21 => _holder21.take().unwrap().unwrap(), + _ => _holder20.take().unwrap().unwrap(), + }; + let tx_pin = match tx { + 20 => _holder20.take().unwrap().unwrap(), + 21 => _holder21.take().unwrap().unwrap(), + _ => _holder21.take().unwrap().unwrap(), + }; + } else { + let rx_pin = match rx { + 10 => _holder10.take().unwrap().unwrap(), + 11 => _holder11.take().unwrap().unwrap(), + _ => _holder10.take().unwrap().unwrap(), + }; + let tx_pin = match tx { + 10 => _holder10.take().unwrap().unwrap(), + 11 => _holder11.take().unwrap().unwrap(), + _ => _holder11.take().unwrap().unwrap(), + }; + } + ); + + // Hardware UART setup + dbg!("UART config"); + let uart_config = Config::default().with_rx( + RxConfig::default() + .with_fifo_full_threshold(16) + .with_timeout(1), + ); + + dbg!("UART setup pins"); + let uart = Uart::new(uart1, uart_config) + .unwrap() + .with_rx(rx_pin) + .with_tx(tx_pin) + .into_async(); + + // Run the main buffered TX/RX loop + dbg!("uart_task running UART"); + uart_buf.run(uart).await; + } + // TODO: Pin config error + dbg!("uart_task Pin config error! Using the same pin number for RX and TX!"); +} diff --git a/src/espressif/hash.rs b/src/espressif/hash.rs new file mode 100644 index 0000000..8672bdc --- /dev/null +++ b/src/espressif/hash.rs @@ -0,0 +1,28 @@ +use esp_hal::hmac::Hmac; +use hmac::Hmac; +use sha2::Sha256; + +pub trait EspressifHmac { + fn new_from_slice(key: &[u8]) -> Result + where + Self: Sized; + fn update(&mut self, data: &[u8]); + fn finalize(self) -> [u8; 32]; +} + +impl EspressifHmac for Hmac { + fn new_from_slice(key: &[u8]) -> Result { + Hmac::new_from_slice(key).map_err(|_| ()) + } + + fn update(&mut self, data: &[u8]) { + self.update(data); + } + + fn finalize(self) -> [u8; 32] { + let result = self.finalize(); + let mut arr = [0u8; 32]; + arr.copy_from_slice(&result.into_bytes()); + arr + } +} \ No newline at end of file diff --git a/src/espressif/mod.rs b/src/espressif/mod.rs index 73ab369..4370f04 100644 --- a/src/espressif/mod.rs +++ b/src/espressif/mod.rs @@ -5,3 +5,6 @@ pub mod buffered_uart; pub mod net; pub mod rng; +// TODO: Specialise for Espressif, tricky since it seems to require burning eFuses?: +// https://github.com/esp-rs/esp-hal/blob/main/examples/src/bin/hmac.rs +//pub mod hash; diff --git a/src/espressif/net.rs b/src/espressif/net.rs index b59773a..aba202f 100644 --- a/src/espressif/net.rs +++ b/src/espressif/net.rs @@ -2,40 +2,31 @@ // // SPDX-License-Identifier: GPL-3.0-or-later +use crate::config::SSHStampConfig; +use crate::settings::{DEFAULT_IP, DEFAULT_SSID}; use core::net::Ipv4Addr; +use core::net::SocketAddrV4; use core::str::FromStr; - +use edge_dhcp; +use edge_dhcp::{ + io::{self, DEFAULT_SERVER_PORT}, + server::{Server, ServerOptions}, +}; +use edge_nal::UdpBind; +use edge_nal_embassy::{Udp, UdpBuffers}; use embassy_executor::Spawner; use embassy_net::{IpListenEndpoint, Ipv4Cidr, Runner, StaticConfigV4}; use embassy_net::{Stack, StackResources, tcp::TcpSocket}; use embassy_time::{Duration, Timer}; - use esp_hal::peripherals::WIFI; - use esp_hal::rng::Rng; +use esp_hal::system::software_reset; use esp_println::{dbg, println}; - use esp_radio::Controller; -use esp_radio::wifi::{AccessPointConfig, ModeConfig, WifiController, WifiDevice, WifiEvent}; - +use esp_radio::wifi::WifiEvent; +use esp_radio::wifi::{AccessPointConfig, ModeConfig, WifiApState, WifiController}; +use heapless::String; use sunset_async::SunsetMutex; - -use core::net::SocketAddrV4; -use edge_dhcp; - -use edge_dhcp::{ - io::{self, DEFAULT_SERVER_PORT}, - server::{Server, ServerOptions}, -}; -use edge_nal::UdpBind; -use edge_nal_embassy::{Udp, UdpBuffers}; - -use crate::config::SSHStampConfig; - -use super::buffered_uart::BufferedUart; - -const GW_IP_ADDR_ENV: Option<&'static str> = option_env!("GATEWAY_IP"); - // When you are okay with using a nightly compiler it's better to use https://docs.rs/static_cell/2.1.0/static_cell/macro.make_static.html macro_rules! mk_static { ($t:ty,$val:expr) => {{ @@ -48,19 +39,21 @@ macro_rules! mk_static { pub async fn if_up( spawner: Spawner, - wifi_controller: Controller<'static>, + controller: Controller<'static>, wifi: WIFI<'static>, - rng: &mut Rng, + rng: Rng, config: &'static SunsetMutex, ) -> Result, sunset::Error> { - let wifi_init = &*mk_static!(Controller<'static>, wifi_controller); - - let (controller, interfaces) = + let wifi_init = &*mk_static!(Controller<'static>, controller); + let (mut wifi_controller, interfaces) = esp_radio::wifi::new(wifi_init, wifi, Default::default()).unwrap(); - let gw_ip_addr_str = GW_IP_ADDR_ENV.unwrap_or("192.168.4.1"); + let ap_config = + ModeConfig::AccessPoint(AccessPointConfig::default().with_ssid(DEFAULT_SSID.into())); + let res = wifi_controller.set_config(&ap_config); + println!("wifi_set_configuration returned {:?}", res); - let gw_ip_addr_ipv4 = Ipv4Addr::from_str(gw_ip_addr_str).expect("failed to parse gateway ip"); + let gw_ip_addr_ipv4 = *DEFAULT_IP; let net_config = embassy_net::Config::ipv4_static(StaticConfigV4 { address: Ipv4Cidr::new(gw_ip_addr_ipv4, 24), @@ -78,7 +71,7 @@ pub async fn if_up( seed, ); - spawner.spawn(wifi_up(controller, config)).ok(); + spawner.spawn(wifi_up(wifi_controller, config)).ok(); spawner.spawn(net_up(runner)).ok(); spawner.spawn(dhcp_server(ap_stack, gw_ip_addr_ipv4)).ok(); @@ -99,43 +92,50 @@ pub async fn if_up( Ok(ap_stack) } -pub async fn accept_requests(stack: Stack<'static>, uart: &BufferedUart) -> ! { - let rx_buffer = mk_static!([u8; 1536], [0; 1536]); - let tx_buffer = mk_static!([u8; 1536], [0; 1536]); +pub async fn ap_stack_disable() -> () { + // drop ap_stack + println!("AP Stack disabled"); + // TODO: Correctly disable/restart AP Stack and/or send messsage to user over SSH + software_reset(); +} - loop { - let mut socket = TcpSocket::new(stack, rx_buffer, tx_buffer); - - println!("Waiting for SSH client..."); - - if let Err(e) = socket - .accept(IpListenEndpoint { - addr: None, - port: 22, - }) - .await - { - println!("connect error: {:?}", e); - continue; - } +pub async fn tcp_socket_disable() -> () { + // drop tcp stack + println!("TCP socket disabled"); + // TODO: Correctly disable/restart tcp socket and/or send messsage to user over SSH + software_reset(); +} - println!("Connected, port 22"); - match crate::serve::handle_ssh_session(&mut socket, uart).await { - Ok(_) => (), - Err(e) => { - println!("SSH client fatal error: {}", e); - } - }; +pub async fn accept_requests<'a>( + tcp_stack: Stack<'a>, + rx_buffer: &'a mut [u8], + tx_buffer: &'a mut [u8], +) -> TcpSocket<'a> { + let mut tcp_socket = TcpSocket::new(tcp_stack, rx_buffer, tx_buffer); + + println!("Waiting for SSH client..."); + if let Err(e) = tcp_socket + .accept(IpListenEndpoint { + addr: None, + port: 22, + }) + .await + { + println!("connect error: {:?}", e); + // continue; + tcp_socket_disable().await; } + println!("Connected, port 22"); + + tcp_socket } #[embassy_executor::task] -async fn wifi_up( - mut controller: WifiController<'static>, +pub async fn wifi_up( + mut wifi_controller: WifiController<'static>, config: &'static SunsetMutex, ) { - println!("Device capabilities: {:?}", controller.capabilities()); - + println!("Device capabilities: {:?}", wifi_controller.capabilities()); let wifi_ssid = { let guard = config.lock().await; guard.wifi_ssid.clone() @@ -144,25 +144,39 @@ async fn wifi_up( // TODO: No wifi password(s) yet... //let wifi_password = config.lock().await.wifi_pw; + println!("Starting Wifi"); + loop { - if matches!(controller.is_connected(), Ok(true)) { + if esp_radio::wifi::ap_state() == WifiApState::Started { // wait until we're no longer connected - controller.wait_for_event(WifiEvent::StaDisconnected).await; + wifi_controller.wait_for_event(WifiEvent::ApStop).await; Timer::after(Duration::from_millis(5000)).await } - if !matches!(controller.is_started(), Ok(true)) { - let client_config = ModeConfig::AccessPoint( - AccessPointConfig::default().with_ssid(wifi_ssid.as_str().into()), // Ugly - ); - controller.set_config(&client_config).unwrap(); + if !matches!(wifi_controller.is_started(), Ok(true)) { + let ssid_string = String::<63>::from_str(&wifi_ssid) + .unwrap() + .to_ascii_lowercase(); + let client_config = + ModeConfig::AccessPoint(AccessPointConfig::default().with_ssid(ssid_string)); + wifi_controller.set_config(&client_config).unwrap(); println!("Starting wifi"); - controller.start_async().await.unwrap(); + wifi_controller.start_async().await.unwrap(); println!("Wifi started!"); } Timer::after(Duration::from_millis(10)).await; } } +pub async fn wifi_controller_disable() -> () { + // TODO: Correctly disable wifi controller + // pub async fn wifi_disable(wifi_controller: EspWifiController<'_>) -> (){ + // drop wifi controller + // esp_wifi::deinit_unchecked() + // wifi_controller.deinit_unchecked() + software_reset(); +} + +use esp_radio::wifi::WifiDevice; #[embassy_executor::task] async fn net_up(mut runner: Runner<'static, WifiDevice<'static>>) { println!("Bringing up network stack...\n"); diff --git a/src/keys.rs b/src/keys.rs deleted file mode 100644 index 6189d1d..0000000 --- a/src/keys.rs +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Roman Valls, 2025 -// -// SPDX-License-Identifier: GPL-3.0-or-later - -// FIXME: For demo purposes, there should be a key handler/generator on first connection. -pub(crate) const HOST_SECRET_KEY: &[u8; 400] = b" ------BEGIN OPENSSH PRIVATE KEY----- -b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW -QyNTUxOQAAACD/HyNyMDvZkVWgMRzpbK6VgVk+/b627AamAjoO8T4uSAAAAJCzAcYdswHG -HQAAAAtzc2gtZWQyNTUxOQAAACD/HyNyMDvZkVWgMRzpbK6VgVk+/b627AamAjoO8T4uSA -AAAEAZYxnkyw7+ehro8oDJ2PBAO8OpJrBAezD3PLOw9CdLCP8fI3IwO9mRVaAxHOlsrpWB -WT79vrbsBqYCOg7xPi5IAAAAC2d1c0B0aGVzZXVzAQI= ------END OPENSSH PRIVATE KEY----- -"; diff --git a/src/lib.rs b/src/lib.rs index 4069e75..9fed909 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,10 +9,7 @@ pub mod config; pub mod errors; pub mod espressif; -pub mod keys; - -pub mod pins; pub mod serial; pub mod serve; pub mod settings; -pub mod storage; +pub mod store; diff --git a/src/main.rs b/src/main.rs index fa4fe2d..1ad9cbd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,68 +1,131 @@ #![no_std] #![no_main] -// SPDX-FileCopyrightText: 2025 Roman Valls, 2025 +// SPDX-FileCopyrightText: 2025 Julio Beltran Ortega, Anthony Tambasco, Roman Valls Guimera, 2025 // // SPDX-License-Identifier: GPL-3.0-or-later -use core::marker::Sized; -use esp_alloc as _; -use esp_backtrace as _; - -#[cfg(feature = "esp32")] -use esp_hal::timer::timg::TimerGroup; -use esp_hal::{ - gpio::Pin, - interrupt::{Priority, software::SoftwareInterruptControl}, - peripherals::UART1, - rng::Rng, - uart::{Config, RxConfig, Uart}, -}; -use esp_println::dbg; -use esp_rtos::embassy::InterruptExecutor; - +use core::result::Result; +use core::result::Result::Err; +use core::result::Result::Ok; +// use core::error::Error; +use core::future::Future; use embassy_executor::Spawner; - -use log::info; - -use ssh_stamp::pins; -use ssh_stamp::pins::GPIOConfig; -use ssh_stamp::pins::PinChannel; +use embassy_futures::select::{Either3, select3}; +use embassy_net::{Stack, tcp::TcpSocket}; +use embassy_sync::{blocking_mutex::raw::NoopRawMutex, channel::Channel}; +use esp_hal::interrupt::{Priority, software::SoftwareInterruptControl}; +use esp_hal::system::software_reset; +use esp_hal::{peripherals::WIFI, rng::Rng}; +use esp_println::println; +use esp_radio::Controller; +use esp_rtos::embassy::InterruptExecutor; use ssh_stamp::{ config::SSHStampConfig, espressif::{ - buffered_uart::BufferedUart, - net::{accept_requests, if_up}, - rng, + buffered_uart::{BufferedUart, GPIOS, UART_BUF, uart_task}, + net, rng, }, + serve, + settings::UART_BUFFER_SIZE, }; - +use static_cell::StaticCell; use storage::flash; +use sunset_async::{SSHServer, SunsetMutex}; -use static_cell::StaticCell; -use sunset_async::SunsetMutex; +cfg_if::cfg_if! { + if #[cfg(feature = "esp32")] { + use esp_hal::timer::timg::TimerGroup; + } +} + +pub async fn peripherals_disable() -> () { + // drop peripherals + software_reset(); +} +pub struct SshStampInit<'a> { + pub rng: Rng, + pub wifi: WIFI<'a>, + pub config: &'a SunsetMutex, + pub uart_buf: &'a BufferedUart, + pub spawner: Spawner, +} +static INT_EXECUTOR: StaticCell> = StaticCell::new(); // 0 is used for esp_rtos #[esp_rtos::main] async fn main(spawner: Spawner) -> ! { + println!("HSM: main"); cfg_if::cfg_if!( if #[cfg(feature = "esp32s2")] { // TODO: This heap size will crash at runtime (only for the ESP32S2), we need to fix this // applying ideas from https://github.com/brainstorm/ssh-stamp/pull/41#issuecomment-2964775170 esp_alloc::heap_allocator!(#[esp_hal::ram(reclaimed)] size: 72 * 1024); - } else { esp_alloc::heap_allocator!(size: 72 * 1024); } ); esp_bootloader_esp_idf::esp_app_desc!(); esp_println::logger::init_logger_from_env(); + println!("HSM: Initialising peripherals "); // System init let peripherals = esp_hal::init(esp_hal::Config::default()); - let mut rng = Rng::new(); - // let timg0 = TimerGroup::new(peripherals.TIMG0); dhcp example uses it for esp_rtos as timg0.timer0 - + let rng = Rng::new(); rng::register_custom_rng(rng); + + println!("Initialising flash "); + // Read SSH configuration from Flash (if it exists) + flash::init(peripherals.FLASH); + + println!("Loading config "); + let flash_config = { + let Some(flash_storage_guard) = flash::get_flash_n_buffer() else { + panic!("Could not acquire flash storage lock"); + }; + let mut flash_storage = flash_storage_guard.lock().await; + // TODO: Migrate this function/test to embedded-test. + // Quick roundtrip test for SSHStampConfig + // ssh_stamp::config::roundtrip_config(); + ssh_stamp::store::load_or_create(&mut flash_storage).await + } + .expect("Could not load or create SSHStampConfig"); + + println!("Initialising config "); + static CONFIG: StaticCell> = StaticCell::new(); + let config: &SunsetMutex = CONFIG.init(SunsetMutex::new(flash_config)); + + println!("Initialising gpio "); + // Only certain GPIO are available for each target. + // TODO: Confirm working pins on every target. + cfg_if::cfg_if!( + if #[cfg(any(feature = "esp32"))]{ + let gpios = GPIOS { + gpio13: Some(peripherals.GPIO13.into()), + gpio14: Some(peripherals.GPIO14.into()), + .. Default::default() + }; + } else if #[cfg(feature = "esp32c2")] { + let gpios = GPIOS { + gpio9: Some(peripherals.GPIO9.into()), + gpio10: Some(peripherals.GPIO10.into()), + .. Default::default() + }; + } else if #[cfg(feature = "esp32c3")] { + let gpios = GPIOS { + gpio20: Some(peripherals.GPIO20.into()), + gpio21: Some(peripherals.GPIO21.into()), + .. Default::default() + }; + } else { + let gpios = GPIOS { + gpio10: Some(peripherals.GPIO10.into()), + gpio11: Some(peripherals.GPIO11.into()), + .. Default::default() + }; + } + ); + + println!("Initialising timers "); let sw_int = SoftwareInterruptControl::new(peripherals.SW_INTERRUPT); cfg_if::cfg_if! { if #[cfg(feature = "esp32")] { @@ -81,41 +144,6 @@ async fn main(spawner: Spawner) -> ! { } } - // TODO: Migrate this function/test to embedded-test. - // Quick roundtrip test for SSHStampConfig - // ssh_stamp::config::roundtrip_config(); - - flash::init(peripherals.FLASH); - - #[cfg(feature = "sftp-ota")] - { - // TODO: Add OTA validation on new Application partition after OTA update and reboot - } - - // Read SSH configuration from Flash (if it exists) - let config = { - // let rrr = flash::get_flash_n_buffer(); - let Some(flash_storage_guard) = flash::get_flash_n_buffer() else { - panic!("Could not acquire flash storage lock"); - }; - let mut flash_storage = flash_storage_guard.lock().await; - // TODO: Migrate this function/test to embedded-test. - // Quick roundtrip test for SSHStampConfig - // ssh_stamp::config::roundtrip_config(); - ssh_stamp::storage::load_or_create(&mut flash_storage).await - } - .expect("Could not load or create SSHStampConfig"); - - static CONFIG: StaticCell> = StaticCell::new(); - let config = CONFIG.init(SunsetMutex::new(config)); - let wifi_controller = esp_radio::init().unwrap(); - - // Bring up the network interface and start accepting SSH connections. - // Clone the reference to config to avoid borrow checker issues. - let tcp_stack = if_up(spawner, wifi_controller, peripherals.WIFI, &mut rng, config) - .await - .unwrap(); - // Set up software buffered UART to run in a higher priority InterruptExecutor // Must be higher priority than esp_rtos (Priority1) let uart_buf = UART_BUF.init_with(BufferedUart::new); @@ -129,102 +157,283 @@ async fn main(spawner: Spawner) -> ! { } } - let serde_pin_config = { - let guard = config.lock().await; - guard.uart_pins.clone() - }; + // Use the same config reference for UART task. + // Pass GPIO peripherals which can then be selected from config values + interrupt_spawner + .spawn(uart_task(uart_buf, peripherals.UART1, config, gpios)) + .unwrap(); - cfg_if::cfg_if! { - if #[cfg(any(feature = "esp32c2", feature = "esp32c3"))] { - // TODO: ESPC2/C3: GPIO pin configuration not yet implemented - let available_gpios = GPIOConfig { - gpio10: Some(peripherals.GPIO10.degrade()), - gpio11: Some(peripherals.GPIO9.degrade()), - }; - }else if #[cfg(feature = "esp32c6")] { + let peripherals_enabled_struct = SshStampInit { + rng, + wifi: peripherals.WIFI, + config, + spawner, + uart_buf, + }; - let available_gpios = GPIOConfig { - gpio10: Some(peripherals.GPIO10.degrade()), - gpio11: Some(peripherals.GPIO11.degrade()), - }; - } else if #[cfg(feature = "esp32")]{ - // TODO: ESP32: GPIO pin configuration not yet implemented - let available_gpios = GPIOConfig { - gpio10: Some(peripherals.GPIO12.degrade()), - gpio11: Some(peripherals.GPIO13.degrade()), - }; - } else if #[cfg(any( feature = "esp32s2", feature = "esp32s3"))]{ - // TODO: ESP32/S2/S3: GPIO pin configuration not yet implemented - let available_gpios = GPIOConfig { - gpio10: Some(peripherals.GPIO10.degrade()), - gpio11: Some(peripherals.GPIO11.degrade()), - }; - } else { - // No fallback - panic!("Unsupported target for PinChannel GPIOConfig initialization"); + match peripherals_enabled(peripherals_enabled_struct).await { + Ok(_) => (), + Err(e) => { + println!("Peripheral error: {}", e); } + } + + peripherals_disable().await; + // loop{} + software_reset(); +} + +pub struct PeripheralsEnabled<'a> { + pub rng: Rng, + pub wifi: WIFI<'a>, + pub config: &'a SunsetMutex, + pub controller: Controller<'static>, + pub uart_buf: &'a BufferedUart, + pub spawner: Spawner, +} + +async fn peripherals_enabled(s: SshStampInit<'static>) -> Result<(), sunset::Error> { + println!("HSM: peripherals_enabled"); + let controller = esp_radio::init().unwrap(); + + let peripherals_enabled_struct = PeripheralsEnabled { + rng: s.rng, + wifi: s.wifi, + config: s.config, + controller, + uart_buf: s.uart_buf, + spawner: s.spawner, }; + match wifi_controller_enabled(peripherals_enabled_struct).await { + Ok(_) => (), + Err(e) => { + println!("Wifi controller error: {}", e); + } + } - // Initialize the global pin channel and keep the &'static reference so we can - // pass it to tasks that need to mutate pins (no unsafe globals). - let pin_channel_ref = - pins::init_global_channel(PinChannel::new(serde_pin_config, available_gpios)); + net::wifi_controller_disable().await; + Ok(()) // todo!() return relevant value +} - // Grab UART1, typically not connected to dev board's TTL2USB IC nor builtin JTAG functionality - let uart1 = peripherals.UART1; +pub struct WifiControllerEnabled<'a> { + pub rng: Rng, + pub config: &'a SunsetMutex, + pub uart_buf: &'a BufferedUart, + pub tcp_stack: Stack<'a>, +} - // Use the same config reference for UART task. - // Pass pin_channel_ref into the UART task so it can acquire/release pins. - interrupt_spawner - .spawn(uart_task(uart_buf, uart1, pin_channel_ref)) +pub async fn wifi_controller_enabled(s: PeripheralsEnabled<'static>) -> Result<(), sunset::Error> { + println!("HSM: wifi_controller_enabled"); + let tcp_stack = net::if_up(s.spawner, s.controller, s.wifi, s.rng, s.config) + .await .unwrap(); - // Optional version details logging - if let Some(build_date) = option_env!("BUILD_DATE") { - info!( - "Initialization done. Starting SSH server... version : v{}, build date: {}", - env!("CARGO_PKG_VERSION"), - build_date - ); + + let wifi_controller_enabled_stack = WifiControllerEnabled { + config: s.config, + rng: s.rng, + uart_buf: s.uart_buf, + tcp_stack, + }; + match tcp_enabled(wifi_controller_enabled_stack).await { + Ok(_) => (), + Err(e) => { + println!("AP Stack error: {}", e); + } } + net::ap_stack_disable().await; + Ok(()) // todo!() return relevant value +} + +pub struct TCPEnabled<'a> { + pub config: &'a SunsetMutex, + pub tcp_socket: TcpSocket<'a>, + pub uart_buf: &'a BufferedUart, +} - // Pass pin_channel_ref into accept_requests (so SSH handlers can use it). - // NOTE: accept_requests signature must accept this arg; if it doesn't, - // thread the reference into whatever code spawns handle_ssh_client. - accept_requests(tcp_stack, uart_buf).await; -} - -static UART_BUF: StaticCell = StaticCell::new(); -static INT_EXECUTOR: StaticCell> = StaticCell::new(); // 0 is used for esp_rtos. - -#[embassy_executor::task()] -async fn uart_task( - buffer: &'static BufferedUart, - uart_periph: UART1<'static>, - pin_channel_ref: &'static SunsetMutex, -) { - dbg!("Spawning UART task..."); - // Hardware UART setup - let uart_config = Config::default().with_rx( - RxConfig::default() - .with_fifo_full_threshold(16) - .with_timeout(1), +cfg_if::cfg_if!(if #[cfg(feature = "esp32")] {use embassy_net::IpListenEndpoint;}); +async fn tcp_enabled<'a>(s: WifiControllerEnabled<'a>) -> Result<(), sunset::Error> { + println!("HSM: tcp_enabled"); + + let mut rx_buffer = [0u8; 1536]; + let mut tx_buffer = [0u8; 1536]; + + // TO FIX: ESP32 target needs tcp_socket.accept in main.rs - Gets blocked at bridge connection? + cfg_if::cfg_if!( + if #[cfg(feature = "esp32")] { + let mut tcp_socket = TcpSocket::new(s.tcp_stack, &mut rx_buffer, &mut tx_buffer); + println!("Waiting for SSH client..."); + if let Err(e) = tcp_socket + .accept(IpListenEndpoint { + addr: None, + port: 22, + }) + .await + { + println!("connect error: {:?}", e); + net::tcp_socket_disable().await; + } + println!("Connected, port 22"); + } else { + let tcp_socket = net::accept_requests(s.tcp_stack, &mut rx_buffer, &mut tx_buffer).await; + } ); + let tcp_enabled_struct = TCPEnabled { + config: s.config, + tcp_socket, + uart_buf: s.uart_buf, + }; + match socket_enabled(tcp_enabled_struct).await { + Ok(_) => (), + Err(e) => { + println!("TCP socket error: {}", e); + } + } + net::tcp_socket_disable().await; + Ok(()) // todo!() return relevant value +} - // Use the pinned reference passed in from main. - let mut pin_chan = pin_channel_ref.lock().await; - - // Sync pin config via channels - pin_chan - .with_channel(async |rx, tx| { - let uart = Uart::new(uart_periph, uart_config) - .unwrap() - .with_rx(rx) - .with_tx(tx) - .into_async(); - - // Run the main buffered TX/RX loop - buffer.run(uart).await; - }) - .await - .unwrap(); +pub struct SocketEnabled<'a> { + pub config: &'a SunsetMutex, + pub tcp_socket: TcpSocket<'a>, + pub ssh_server: SSHServer<'a>, + pub uart_buf: &'a BufferedUart, +} + +async fn socket_enabled<'a>(s: TCPEnabled<'a>) -> Result<(), sunset::Error> { + println!("HSM: socket_enabled"); + // loop { + // Spawn network tasks to handle incoming connections with demo_common::session() + let mut inbuf = [0u8; UART_BUFFER_SIZE]; + let mut outbuf = [0u8; UART_BUFFER_SIZE]; + println!("HSM: Starting ssh_server"); + let ssh_server = serve::ssh_wait_for_initialisation(&mut inbuf, &mut outbuf).await; + println!("HSM: Started ssh_server"); + + let socket_enabled_struct = SocketEnabled { + config: s.config, + tcp_socket: s.tcp_socket, + ssh_server, + uart_buf: s.uart_buf, + }; + match ssh_enabled(socket_enabled_struct).await { + Ok(_) => (), + Err(e) => { + println!("SSH server error: {}", e); + } + } + + serve::ssh_disable().await; + // } + Ok(()) // todo!() return relevant value +} + +pub struct SshEnabled<'a, 'b, CL> +where + CL: Future>, +{ + pub tcp_socket: TcpSocket<'a>, + pub ssh_server: &'b SSHServer<'a>, + pub uart_buf: &'a BufferedUart, + pub config: &'a SunsetMutex, + pub chan_pipe: &'b Channel, + pub connection_loop: CL, +} + +async fn ssh_enabled<'a>(s: SocketEnabled<'a>) -> Result<(), sunset::Error> { + println!("HSM: ssh_enabled"); + // loop { + println!("HSM: Starting channel pipe"); + let chan_pipe = Channel::::new(); + println!("HSM: Started channel pipe. Calling connection_loop from ssh_enabled"); + let connection = serve::connection_loop(&s.ssh_server, &chan_pipe, s.config); + println!("HSM: Started connection loop"); + + let ssh_enabled_struct = SshEnabled { + tcp_socket: s.tcp_socket, + ssh_server: &s.ssh_server, + config: s.config, + uart_buf: s.uart_buf, + chan_pipe: &chan_pipe, + connection_loop: connection, + }; + match client_connected(ssh_enabled_struct).await { + Ok(_) => (), + Err(e) => { + println!("Client connection error: {}", e); + } + } + + serve::connection_disable().await; + // } + Ok(()) // todo!() return relevant value +} + +pub struct ClientConnected<'a, 'b, CL, BR> +where + CL: Future>, + BR: Future>, +{ + pub ssh_server: &'b SSHServer<'a>, + pub bridge: BR, + pub connection_loop: CL, + pub tcp_socket: TcpSocket<'a>, +} + +async fn client_connected<'a, 'b, CL>(s: SshEnabled<'a, 'b, CL>) -> Result<(), sunset::Error> +where + CL: Future>, + 'a: 'b, +{ + println!("HSM: client_connected"); + + // // loop { + println!("HSM: Setting up serial bridge"); + let bridge = serve::handle_ssh_client(s.uart_buf, s.ssh_server, s.chan_pipe); + + let uart_enabled_struct = ClientConnected { + ssh_server: s.ssh_server, + bridge, + connection_loop: s.connection_loop, + tcp_socket: s.tcp_socket, + }; + match bridge_connected(uart_enabled_struct).await { + Ok(_) => (), + Err(e) => { + println!("Bridge error: {}", e); + } + } + + serve::bridge_disable().await; + // } + + Ok(()) // todo!() return relevant value +} + +async fn bridge_connected<'a, 'b, CL, BR>( + s: ClientConnected<'a, 'b, CL, BR>, +) -> Result<(), sunset::Error> +where + CL: Future>, + BR: Future>, + 'a: 'b, +{ + println!("HSM: bridge_connected"); + let mut tcp_socket = s.tcp_socket; + let (mut rsock, mut wsock) = tcp_socket.split(); + println!("HSM: Running server from bridge_connected()"); + let server = s.ssh_server.run(&mut rsock, &mut wsock); + let connection_loop = s.connection_loop; + let bridge = s.bridge; + println!("HSM: Main select() in bridge_connected()"); + match select3(server, connection_loop, bridge).await { + Either3::First(r) => r, + Either3::Second(r) => r, + Either3::Third(r) => r, + }?; + Result::Ok(()) +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} } diff --git a/src/pins.rs b/src/pins.rs deleted file mode 100644 index be371b5..0000000 --- a/src/pins.rs +++ /dev/null @@ -1,272 +0,0 @@ -use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, channel::Channel}; -use esp_hal::gpio::AnyPin; -use esp_hal::peripherals; -use static_cell::StaticCell; -use sunset::sshwire::{SSHDecode, SSHEncode, SSHSink, SSHSource, WireResult}; -use sunset_async::SunsetMutex; - -use crate::{ - config::{dec_option, enc_option}, - errors, -}; - -#[derive(Debug, Clone, PartialEq)] -pub struct SerdePinConfig { - pub tx: u8, - pub rx: u8, - pub rts: Option, - pub cts: Option, -} - -impl SerdePinConfig { - /// Create a new SerdePinConfig with default values. - pub fn new() -> Self { - Self::default() - } - - /// Set the TX pin, returning an updated SerdePinConfig (builder style). - pub fn with_tx(mut self, tx: u8) -> Self { - self.tx = tx; - self - } - - /// Set the RX pin, returning an updated SerdePinConfig (builder style). - pub fn with_rx(mut self, rx: u8) -> Self { - self.rx = rx; - self - } - - /// Set the RTS pin (optional), returning an updated SerdePinConfig. - pub fn with_rts(mut self, rts: Option) -> Self { - self.rts = rts; - self - } - - /// Set the CTS pin (optional), returning an updated SerdePinConfig. - pub fn with_cts(mut self, cts: Option) -> Self { - self.cts = cts; - self - } -} - -impl Default for SerdePinConfig { - fn default() -> Self { - Self { - tx: 10, - rx: 11, - rts: None, - cts: None, - } - } -} - -impl SSHEncode for SerdePinConfig { - fn enc(&self, s: &mut dyn SSHSink) -> WireResult<()> { - self.tx.enc(s)?; - self.rx.enc(s)?; - enc_option(&self.rts, s)?; - enc_option(&self.cts, s) - } -} - -impl<'de> SSHDecode<'de> for SerdePinConfig { - fn dec(s: &mut S) -> WireResult - where - S: SSHSource<'de>, - { - // Decoding Options is problematic since encode only writes them if they exist. - let pin_config = SerdePinConfig { - tx: u8::dec(s)?, - rx: u8::dec(s)?, - rts: dec_option(s)?, - cts: dec_option(s)?, - }; - - Ok(pin_config) - } -} - -#[derive(Default)] -pub struct GPIOConfig { - pub gpio10: Option>, - pub gpio11: Option>, -} - -pub struct PinChannel { - pub config: SerdePinConfig, - pub gpios: GPIOConfig, - pub tx: Channel, - pub rx: Channel, - // TODO: cts/rts pins -} - -impl PinChannel { - pub fn new(config: SerdePinConfig, gpios: GPIOConfig) -> Self { - Self { - config, - gpios, - tx: Channel::::new(), - rx: Channel::::new(), - } - } - - pub async fn recv_tx(&mut self) -> errors::Result> { - // tx needs to lock here. - //self.tx.receive().await; - - Ok(match self.config.tx { - 10 => self.gpios.gpio10.take().ok_or(errors::Error::InvalidPin)?, - 11 => self.gpios.gpio11.take().ok_or(errors::Error::InvalidPin)?, - _ => return Err(errors::Error::InvalidPin), - }) - } - - pub async fn send_tx(&mut self, pin: AnyPin<'static>) -> errors::Result<()> { - match self.config.tx { - 10 => self.gpios.gpio10 = Some(pin), - 11 => self.gpios.gpio11 = Some(pin), - _ => return Err(errors::Error::InvalidPin), - }; - - // tx lock needs to be released. - self.tx.send(()).await; - Ok(()) - } - - pub async fn recv_rx(&mut self) -> errors::Result> { - // rx needs to lock here. - // dbg!("recv_rx: before rx.receive.await"); - // self.rx.receive().await; - // dbg!("recv_rx: after rx.receive.await"); - - match self.config.rx { - 10 => self.gpios.gpio10.take().ok_or(errors::Error::InvalidPin), - 11 => self.gpios.gpio11.take().ok_or(errors::Error::InvalidPin), - _ => Err(errors::Error::InvalidPin), - } - } - - pub async fn send_rx(&mut self, pin: AnyPin<'static>) -> errors::Result<()> { - match self.config.rx { - 10 => self.gpios.gpio10 = Some(pin), - 11 => self.gpios.gpio11 = Some(pin), - _ => return Err(errors::Error::InvalidPin), - }; - - // rx lock needs to be released. - self.rx.send(()).await; - Ok(()) - } - - pub async fn with_channel(&mut self, f: F) -> errors::Result<()> - where - F: for<'a> AsyncFnOnce(AnyPin<'a>, AnyPin<'a>), - { - let mut rx = self.recv_rx().await?; - let mut tx = self.recv_tx().await?; - - f(rx.reborrow(), tx.reborrow()).await; - - self.send_rx(rx).await.unwrap(); - self.send_tx(tx).await.unwrap(); - - Ok(()) - } - - // Update the runtime config's TX pin number. This only changes the - // u8 config; actual AnyPin movement happens when the uart task next - // reacquires pins via recv_tx/recv_rx. - pub fn set_tx_pin(&mut self, tx: u8) -> errors::Result<()> { - if tx == self.config.rx { - return Err(errors::Error::InvalidPin); - } - match tx { - 10 | 11 => { - self.config.tx = tx; - Ok(()) - } - _ => Err(errors::Error::InvalidPin), - } - } - - pub fn set_rx_pin(&mut self, rx: u8) -> errors::Result<()> { - if rx == self.config.tx { - return Err(errors::Error::InvalidPin); - } - match rx { - 10 | 11 => { - self.config.rx = rx; - Ok(()) - } - _ => Err(errors::Error::InvalidPin), - } - } -} - -// Global PinChannel holder: initialize from main() and access from other modules. -// We keep a StaticCell but avoid any unsafe global pointer; callers receive -// the &'static SunsetMutex returned by init_global_channel and must retain it. -static GLOBAL_PIN_CHANNEL: StaticCell> = StaticCell::new(); - -pub fn init_global_channel(ch: PinChannel) -> &'static SunsetMutex { - // Initialize the StaticCell and return the &'static reference. - GLOBAL_PIN_CHANNEL.init(SunsetMutex::new(ch)) -} - -pub struct PinConfig { - pub tx: AnyPin<'static>, - pub rx: AnyPin<'static>, -} - -pub struct PinConfigAlt { - pub peripherals: peripherals::Peripherals, -} - -impl PinConfigAlt { - pub fn new(peripherals: peripherals::Peripherals) -> Self { - Self { peripherals } - } - - pub fn take_pin<'a>(&'a mut self, pin: u8) -> AnyPin<'a> { - match pin { - 0 => self.peripherals.GPIO0.reborrow().into(), - 1 => self.peripherals.GPIO1.reborrow().into(), - _ => panic!(), - } - } -} - -impl PinConfig { - pub fn new(mut gpio_config: GPIOConfig, config_inner: SerdePinConfig) -> errors::Result { - if config_inner.rx == config_inner.tx { - return Err(errors::Error::InvalidPin); - } - - // SAFETY: Safe because moved in peripherals. - Ok(Self { - rx: match config_inner.rx { - 10 => gpio_config.gpio10.take().unwrap(), - 11 => gpio_config.gpio11.take().unwrap(), - _ => return Err(errors::Error::InvalidPin), - }, - tx: match config_inner.tx { - 10 => gpio_config.gpio10.take().unwrap(), - 11 => gpio_config.gpio11.take().unwrap(), - _ => return Err(errors::Error::InvalidPin), - }, - }) - } - - /// Resolves a u8 pin number into an AnyPin GPIO type. - /// Returns None if the pin number is invalid or unsupported. - pub fn initialize_pin( - peripherals: peripherals::Peripherals, - pin_number: u8, - ) -> errors::Result> { - match pin_number { - 0 => Ok(peripherals.GPIO0.into()), - - _ => Err(errors::Error::InvalidPin), - } - } -} diff --git a/src/serial.rs b/src/serial.rs index f25ae39..5e6550d 100644 --- a/src/serial.rs +++ b/src/serial.rs @@ -11,7 +11,7 @@ use esp_println::println; /// Forwards an incoming SSH connection to/from the local UART, until /// the connection drops -pub(crate) async fn serial_bridge( +pub async fn serial_bridge( chanr: impl Read, chanw: impl Write, uart: &BufferedUart, @@ -34,7 +34,6 @@ async fn uart_to_ssh( // TODO: should this also go to the SSH client? println!("UART RX dropped {} bytes", dropped); } - let n = uart_buf.read(&mut ssh_tx_buf).await; chanw.write_all(&ssh_tx_buf[..n]).await?; } diff --git a/src/serve.rs b/src/serve.rs index 13957b2..e8011e0 100644 --- a/src/serve.rs +++ b/src/serve.rs @@ -2,73 +2,113 @@ // // SPDX-License-Identifier: GPL-3.0-or-later +use crate::config::SSHStampConfig; +use crate::settings::UART_BUFFER_SIZE; +use crate::store; use core::option::Option::{self, None, Some}; use core::result::Result; - -use crate::espressif::buffered_uart::BufferedUart; -use crate::keys; -use crate::serial::serial_bridge; - +use storage::flash; // Embassy -use embassy_futures::select::{Either3, select3}; -use embassy_net::tcp::TcpSocket; use embassy_sync::blocking_mutex::raw::NoopRawMutex; use embassy_sync::channel::Channel; use embassy_sync::mutex::Mutex; - +// use embedded_storage::Storage; +use esp_hal::system::software_reset; use heapless::String; -use sunset::{ChanHandle, ServEvent, SignKey, error}; -use sunset_async::{ProgressHolder, SSHServer}; - +use sunset_async::SunsetMutex; +// use sunset::sshwire::SSHEncode; +use crate::espressif::buffered_uart::UART_SIGNAL; use esp_println::{dbg, println}; +use sunset::{ChanHandle, ServEvent, error}; +use sunset_async::{ProgressHolder, SSHServer}; -enum SessionType { +pub enum SessionType { Bridge(ChanHandle), - #[cfg(feature = "sftp-ota")] Sftp(ChanHandle), } -async fn connection_loop( +pub async fn connection_loop( serv: &SSHServer<'_>, chan_pipe: &Channel, + config: &SunsetMutex, ) -> Result<(), sunset::Error> { let username = Mutex::::new(String::<20>::new()); let mut session: Option = None; println!("Entering connection_loop and prog_loop is next..."); - + let mut config_changed: bool = false; loop { let mut ph = ProgressHolder::new(); + // dbg!("Waiting for ssh server event"); let ev = serv.progress(&mut ph).await?; - dbg!(&ev); + // dbg!(&ev); #[allow(unreachable_patterns)] match ev { + // #[cfg(feature = "sftp-ota")] + ServEvent::SessionSubsystem(a) => { + println!("ServEvent::SessionSubsystem"); + if a.command()?.to_lowercase().as_str() == "sftp" { + if let Some(ch) = session.take() { + debug_assert!(ch.num() == a.channel()); + + a.succeed()?; + dbg!("We got SFTP subsystem"); + let _ = chan_pipe.try_send(SessionType::Sftp(ch)); + } else { + a.fail()?; + } + } else { + a.fail()?; + } + } ServEvent::SessionShell(a) => { + println!("ServEvent::SessionShell"); if let Some(ch) = session.take() { + // Save config after connection successful (SessionEnv completed) + if config_changed { + let config_guard = config.lock().await; + let Some(flash_storage_guard) = flash::get_flash_n_buffer() else { + panic!("Could not acquire flash storage lock"); + }; + let mut flash_storage = flash_storage_guard.lock().await; + // TODO: Migrate this function/test to embedded-test. + // Quick roundtrip test for SSHStampConfig + // ssh_stamp::config::roundtrip_config(); + let _result = store::save(&mut flash_storage, &config_guard).await; + } + debug_assert!(ch.num() == a.channel()); a.succeed()?; dbg!("We got shell"); + // Signal for uart task to configure pins and run. Value is irrelevant. + UART_SIGNAL.signal(1); + println!("Connection loop: UART_SIGNAL sent"); let _ = chan_pipe.try_send(SessionType::Bridge(ch)); } else { a.fail()?; } } ServEvent::FirstAuth(ref a) => { + println!("ServEvent::FirstAuth"); // record the username if username.lock().await.push_str(a.username()?).is_err() { println!("Too long username") } } ServEvent::Hostkeys(h) => { - let signkey: SignKey = SignKey::from_openssh(keys::HOST_SECRET_KEY)?; - h.hostkeys(&[&signkey])?; + println!("ServEvent::Hostkeys"); + let config_guard = config.lock().await; + h.hostkeys(&[&config_guard.hostkey])?; } ServEvent::PasswordAuth(a) => { + println!("ServEvent::PasswordAuth"); a.allow()?; } ServEvent::PubkeyAuth(a) => { + println!("ServEvent::PubkeyAuth"); a.allow()?; } ServEvent::OpenSession(a) => { + println!("ServEvent::OpenSession"); match session { Some(_) => { todo!("Can't have two sessions"); @@ -79,7 +119,61 @@ async fn connection_loop( } } } + ServEvent::SessionEnv(a) => { + dbg!("Got ENV request"); + dbg!(a.name()?); + dbg!(a.value()?); + + // TODO: Logic to serialise/validate env vars? I.e: + // a.name.validate(); // Checks the input variable, sanitizes, assigns a target subsystem + // + // config.change(c): Apply the config change to the relevant subsystem. + // i.e: if UART_TX_PIN or UART_RX_PIN, we update the PinChannel with with_channel() to change pins live. + match a.name()? { + "SAVE_CONFIG" => { + if a.value()? == "1" { + dbg!("Triggering config save..."); + todo!("Implement config save to flash"); + } + } + // If the env var is UART_TX_PIN or UART_RX_PIN + "UART_TX_PIN" => { + let val = a.value()?; + dbg!("Updating UART TX pin to ", val); + if let Ok(pin_num) = val.parse::() { + let mut config_lock = config.lock().await; + config_lock.uart_pins.tx = pin_num; + config_changed = true; + dbg!("TX pin updated"); + } else { + dbg!("Invalid TX pin value"); + } + } + "UART_RX_PIN" => { + let val = a.value()?; + dbg!("Updating UART RX pin to ", val); + if let Ok(pin_num) = val.parse::() { + let mut config_lock = config.lock().await; + config_lock.uart_pins.rx = pin_num; + config_changed = true; + dbg!("RX pin updated"); + } else { + dbg!("Invalid RX pin value"); + } + } + _ => { + dbg!("Unknown/unsupported ENV var"); + } + } + + // config.save(a): Potentially an optional special environment variable SAVE_CONFIG=1 + // that serialises current config to flash + // Only save once all ENV requests have been recorded? + + a.succeed()?; + } ServEvent::SessionPty(a) => { + println!("ServEvent::SessionPty"); a.succeed()?; } ServEvent::SessionExec(a) => { @@ -89,84 +183,68 @@ async fn connection_loop( println!("Expected caller to handle event"); error::BadUsage.fail()? } - ServEvent::PollAgain => (), - ServEvent::SessionSubsystem(a) => { - #[cfg(feature = "sftp-ota")] - { - if a.command()?.to_lowercase().as_str() == "sftp" { - if let Some(ch) = session.take() { - debug_assert!(ch.num() == a.channel()); - - a.succeed()?; - dbg!("We got SFTP subsystem"); - let _ = chan_pipe.try_send(SessionType::Sftp(ch)); - } else { - a.fail()?; - } - } else { - a.fail()?; - } - } - #[cfg(not(feature = "sftp-ota"))] - { - a.fail()?; - } - } - ServEvent::SessionEnv(a) => { - dbg!("Got ENV request"); - dbg!(a.name()?); - dbg!(a.value()?); - a.succeed()?; - } - _ => { - println!("Unexpected event: {:?}", ev); + ServEvent::PollAgain => { + // println!("ServEvent::PollAgain"); } + _ => (), } } } -pub(crate) async fn handle_ssh_session( - stream: &mut TcpSocket<'_>, - uart: &BufferedUart, -) -> Result<(), sunset::Error> { - // Spawn network tasks to handle incoming connections with demo_common::session() - let mut inbuf = [0u8; 4096]; - let mut outbuf = [0u8; 4096]; - - let ssh_server = SSHServer::new(&mut inbuf, &mut outbuf); - let (mut rsock, mut wsock) = stream.split(); +pub async fn connection_disable() -> () { + // disable connection loop + println!("Connection loop disabled"); + // TODO: Correctly disable/restart Conection loop and/or send messsage to user over SSH + software_reset(); +} - let chan_pipe = Channel::::new(); +pub async fn ssh_wait_for_initialisation<'server>( + inbuf: &'server mut [u8; UART_BUFFER_SIZE], + outbuf: &'server mut [u8; UART_BUFFER_SIZE], +) -> SSHServer<'server> { + SSHServer::new(inbuf, outbuf) +} - println!("Calling connection_loop from handle_ssh_client"); - let conn_loop = connection_loop(&ssh_server, &chan_pipe); - println!("Running server from handle_ssh_client()"); - let server = ssh_server.run(&mut rsock, &mut wsock); +pub async fn ssh_disable() -> () { + // drop ssh server + println!("SSH Server disabled"); + // TODO: Correctly disable/restart SSH Server and/or send messsage to user over SSH + software_reset(); +} - // TODO: Maybe loop forever here and/or handle disconnection/terminations gracefully? - let session = async { - let session_type = chan_pipe.receive().await; +// use crate::serve::SessionType; +use crate::espressif::buffered_uart::BufferedUart; +use crate::serial::serial_bridge; +use sunset_async::ChanInOut; - match session_type { - SessionType::Bridge(ch) => { - println!("Setting up serial bridge"); - let (chan_read, chan_write) = ssh_server.stdio(ch).await?.split(); - serial_bridge(chan_read, chan_write, uart).await? - } - #[cfg(feature = "sftp-ota")] - SessionType::Sftp(_ch) => { - // TODO: create a new SFTP Subsystem session to handle ota - } - }; - Ok(()) +pub async fn handle_ssh_client<'a, 'b>( + uart_buff: &'a BufferedUart, + ssh_server: &'b SSHServer<'a>, + chan_pipe: &'b Channel, +) -> Result<(), sunset::Error> { + dbg!("Preparing bridge"); + let session_type = chan_pipe.receive().await; + dbg!("Checking bridge session type"); + match session_type { + SessionType::Bridge(ch) => { + dbg!("Handling bridge session"); + let stdio: ChanInOut<'_> = ssh_server.stdio(ch).await?; + let stdio2 = stdio.clone(); + dbg!("Starting bridge"); + serial_bridge(stdio, stdio2, uart_buff).await? + } + SessionType::Sftp(_ch) => { + dbg!("Handling SFTP session"); + // Handle SFTP session + // todo!() + } }; - - println!("Main select() in handle_ssh_client()"); - match select3(conn_loop, server, session).await { - Either3::First(r) => r, - Either3::Second(r) => r, - Either3::Third(r) => r, - }?; - Ok(()) } + +pub async fn bridge_disable() -> () { + // disable bridge + println!("Bridge disabled"); + // TODO: Correctly disable/restart bridge and/or send messsage to user over SSH + software_reset(); +} diff --git a/src/settings.rs b/src/settings.rs index 3bfb964..0b66f87 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,9 +1,7 @@ -// SPDX-FileCopyrightText: 2025 Roman Valls, 2025 -// -// SPDX-License-Identifier: GPL-3.0-or-later - // Static settings +use core::net::Ipv4Addr; + // SSH server settings //pub(crate) const MTU: usize = 1536; //pub(crate) const PORT: u16 = 22; @@ -11,7 +9,25 @@ pub(crate) const DEFAULT_SSID: &str = "ssh-stamp"; //pub(crate) const SSH_SERVER_ID: &str = "SSH-2.0-ssh-stamp-0.1"; pub(crate) const KEY_SLOTS: usize = 1; // TODO: Document whether this a "reasonable default"? Justify why? //pub(crate) const PASSWORD_AUTHENTICATION: bool = true; +pub(crate) const DEFAULT_IP: &Ipv4Addr = &Ipv4Addr::new(192, 168, 4, 1); +pub const UART_BUFFER_SIZE: usize = 4096; // UART settings //pub(crate) const BAUD_RATE: u32 = 115200; //pub(crate) const UART_SETTINGS: &str = "8N1"; +cfg_if::cfg_if!( + // if #[cfg(feature = "esp32")] { + if #[cfg(feature = "esp32")] { + pub(crate) const DEFAULT_UART_TX_PIN: u8 = 14; + pub(crate) const DEFAULT_UART_RX_PIN: u8 = 13; + } else if #[cfg(feature = "esp32c2")] { + pub(crate) const DEFAULT_UART_TX_PIN: u8 = 10; + pub(crate) const DEFAULT_UART_RX_PIN: u8 = 9; + } else if #[cfg(feature = "esp32c3")] { + pub(crate) const DEFAULT_UART_TX_PIN: u8 = 21; + pub(crate) const DEFAULT_UART_RX_PIN: u8 = 20; + } else { + pub(crate) const DEFAULT_UART_TX_PIN: u8 = 10; + pub(crate) const DEFAULT_UART_RX_PIN: u8 = 11; + } +); diff --git a/src/storage.rs b/src/store.rs similarity index 100% rename from src/storage.rs rename to src/store.rs diff --git a/storage/src/lib.rs b/storage/src/lib.rs index 7b758ee..16fc0b1 100644 --- a/storage/src/lib.rs +++ b/storage/src/lib.rs @@ -3,7 +3,15 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -// TODO: When the time comes, generalize the flash so it can be used with all supported targets +/// Module defining the ESP32-specific storage traits implementations for OTA updates +#[cfg(any( + feature = "esp32", + feature = "esp32s2", + feature = "esp32s3", + feature = "esp32c3", + feature = "esp32c6" +))] +// TODO: When the time comes, generalise the flash so it can be used with all supported targets /// [[flash]] is a packet to provide safe access to the Flash storage used by SSH-Stamp /// /// It does so by storing the FlashStorage and a buffer for read/write operations in a single structure