diff --git a/.cargo/config.toml b/.cargo/config.toml index 58643eb..e2f942b 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,7 +1,3 @@ -[alias] -stickbot = "run --bin stickbot" -boxbot = "run --bin boxbot" - [target.armv7-unknown-linux-gnueabihf] linker = "arm-linux-gnueabihf-gcc" diff --git a/.github/workflows/all.yml b/.github/workflows/all.yml index 8533405..26b9be8 100644 --- a/.github/workflows/all.yml +++ b/.github/workflows/all.yml @@ -101,7 +101,7 @@ jobs: - name: "apt - update" run: apt-get update - name: "apt - install libudev-dev" - run: apt-get install libudev-dev pkg-config -y + run: apt-get install libudev-dev pkg-config clang -y - name: "rust - download" run: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > /tmp/rustup-install.sh diff --git a/src/milton-web/Cargo.lock b/src/milton-web/Cargo.lock index 86a5006..4fa54ec 100644 --- a/src/milton-web/Cargo.lock +++ b/src/milton-web/Cargo.lock @@ -95,6 +95,15 @@ dependencies = [ "libc", ] +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + [[package]] name = "anyhow" version = "1.0.66" @@ -129,7 +138,7 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e14485364214912d3b19cc3435dde4df66065127f05fa0d75c712f36f12c2f28" dependencies = [ - "concurrent-queue", + "concurrent-queue 1.2.4", "event-listener", "futures-core", ] @@ -146,15 +155,15 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965" +checksum = "17adb73da160dfb475c183343c8cccd80721ea5a605d3eb57125f0a7b7a92d0b" dependencies = [ + "async-lock", "async-task", - "concurrent-queue", + "concurrent-queue 2.0.0", "fastrand", "futures-lite", - "once_cell", "slab", ] @@ -197,7 +206,7 @@ checksum = "e8121296a9f05be7f34aa4196b1747243b3b62e048bb7906f644f3fbfc490cf7" dependencies = [ "async-lock", "autocfg", - "concurrent-queue", + "concurrent-queue 1.2.4", "futures-lite", "libc", "log", @@ -367,6 +376,29 @@ dependencies = [ "serde", ] +[[package]] +name = "bindgen" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da379dbebc0b76ef63ca68d8fc6e71c0f13e59432e0987e508c1820e6ab5239" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "clap 2.34.0", + "env_logger 0.8.4", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "which", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -437,9 +469,18 @@ checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" [[package]] name = "cc" -version = "1.0.74" +version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581f5dba903aac52ea3feb5ec4810848460ee833876f1f9b0fdeab1f19091574" +checksum = "76a284da2e6fe2092f2353e51713435363112dfd60030e22add80be333fb928f" + +[[package]] +name = "cexpr" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27" +dependencies = [ + "nom", +] [[package]] name = "cfg-if" @@ -478,26 +519,52 @@ dependencies = [ "generic-array", ] +[[package]] +name = "clang-sys" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa2e27ae6ab525c3d369ded447057bca5438d86dc3a68f6faafb8269ba82ebf3" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" -version = "4.0.18" +version = "2.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335867764ed2de42325fafe6d18b8af74ba97ee0c590fa016f157535b42ab04b" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim 0.8.0", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "clap" +version = "4.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eb41c13df48950b20eb4cd0eefa618819469df1bffc49d11e8487c4ba0037e5" dependencies = [ "atty", "bitflags", "clap_derive", "clap_lex", "once_cell", - "strsim", + "strsim 0.10.0", "termcolor", ] [[package]] name = "clap_derive" -version = "4.0.18" +version = "4.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16a1b0f6422af32d5da0c58e2703320f379216ee70198241c84173a8c5ac28f3" +checksum = "0177313f9f02afc995627906bbd8967e2be069f5261954222dac78290c2b9014" dependencies = [ "heck", "proc-macro-error", @@ -534,6 +601,15 @@ dependencies = [ "cache-padded", ] +[[package]] +name = "concurrent-queue" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd7bef69dc86e3c610e4e7aed41035e2a7ed12e72dd7530f61327a6579a4390b" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "const_fn" version = "0.4.9" @@ -665,9 +741,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.80" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b7d4e43b25d3c994662706a1d4fcfc32aaa6afd287502c111b237093bb23f3a" +checksum = "97abf9f0eca9e52b7f81b945524e76710e6cb2366aead23b7d4fbf72e281f888" dependencies = [ "cc", "cxxbridge-flags", @@ -677,9 +753,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.80" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84f8829ddc213e2c1368e51a2564c552b65a8cb6a28f31e576270ac81d5e5827" +checksum = "7cc32cc5fea1d894b77d269ddb9f192110069a8a9c1f1d441195fba90553dea3" dependencies = [ "cc", "codespan-reporting", @@ -692,15 +768,15 @@ dependencies = [ [[package]] name = "cxxbridge-flags" -version = "1.0.80" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e72537424b474af1460806647c41d4b6d35d09ef7fe031c5c2fa5766047cc56a" +checksum = "8ca220e4794c934dc6b1207c3b42856ad4c302f2df1712e9f8d2eec5afaacf1f" [[package]] name = "cxxbridge-macro" -version = "1.0.80" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "309e4fb93eed90e1e14bea0da16b209f81813ba9fc7830c20ed151dd7bc0a4d7" +checksum = "b846f081361125bfc8dc9d3940c84e1fd83ba54bbca7b17cd29483c828be0704" dependencies = [ "proc-macro2", "quote", @@ -739,9 +815,22 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.9.1" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "env_logger" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c90bf5f19754d10198ccb95b70664fc925bd1fc090a0fd9a6ebc54acc8cd6272" +checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" dependencies = [ "atty", "humantime", @@ -816,6 +905,21 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.25" @@ -823,6 +927,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -831,6 +936,17 @@ version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" +[[package]] +name = "futures-executor" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.25" @@ -881,9 +997,11 @@ version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", + "futures-sink", "futures-task", "memchr", "pin-project-lite 0.2.9", @@ -933,6 +1051,12 @@ dependencies = [ "polyval", ] +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + [[package]] name = "gloo-timers" version = "0.2.4" @@ -1174,12 +1298,28 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "libc" version = "0.2.137" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if 1.0.0", + "winapi", +] + [[package]] name = "libnghttp2-sys" version = "0.1.7+1.45.0" @@ -1262,9 +1402,10 @@ version = "0.0.0" dependencies = [ "async-std", "chrono", - "clap", + "clap 4.0.23", "dotenv", - "env_logger", + "env_logger 0.9.3", + "futures", "jsonwebtoken", "kramer", "log", @@ -1275,6 +1416,7 @@ dependencies = [ "tide", "toml", "uuid", + "v4l", ] [[package]] @@ -1304,6 +1446,16 @@ dependencies = [ "libc", ] +[[package]] +name = "nom" +version = "5.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" +dependencies = [ + "memchr", + "version_check", +] + [[package]] name = "num-bigint" version = "0.2.6" @@ -1367,9 +1519,9 @@ dependencies = [ [[package]] name = "os_str_bytes" -version = "6.3.1" +version = "6.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3baf96e39c5359d2eb0dd6ccb42c62b91d9678aa68160d261b9e0ccbf9e9dea9" +checksum = "7b5bf27447411e9ee3ff51186bf7a08e16c341efdde93f4d823e8844429bed7e" [[package]] name = "parking" @@ -1377,6 +1529,12 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + [[package]] name = "pem" version = "0.8.3" @@ -1465,9 +1623,9 @@ dependencies = [ [[package]] name = "ppv-lite86" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro-error" @@ -1590,9 +1748,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" dependencies = [ "aho-corasick", "memchr", @@ -1601,9 +1759,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.27" +version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" [[package]] name = "ring" @@ -1626,6 +1784,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56770675ebc04927ded3e60633437841581c285dc6236109ea25fbf3beb7b59e" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc_version" version = "0.2.3" @@ -1785,6 +1949,12 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "shlex" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" + [[package]] name = "signal-hook" version = "0.3.14" @@ -1927,6 +2097,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + [[package]] name = "strsim" version = "0.10.0" @@ -1991,6 +2167,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + [[package]] name = "thiserror" version = "1.0.37" @@ -2229,6 +2414,26 @@ dependencies = [ "getrandom 0.2.8", ] +[[package]] +name = "v4l" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "950f4328645beeb19551e4a884600adf4e4714fe96f45c46f85eb706f4d786ce" +dependencies = [ + "bitflags", + "libc", + "v4l2-sys-mit", +] + +[[package]] +name = "v4l2-sys-mit" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c932c06df4af1dfb229f604214f2a87993784596ff33ffdadcba1b5519254e" +dependencies = [ + "bindgen", +] + [[package]] name = "value-bag" version = "1.0.0-alpha.9" @@ -2249,6 +2454,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + [[package]] name = "version_check" version = "0.9.4" @@ -2366,6 +2577,15 @@ dependencies = [ "cc", ] +[[package]] +name = "which" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724" +dependencies = [ + "libc", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/src/milton-web/Cargo.toml b/src/milton-web/Cargo.toml index f466959..0876666 100644 --- a/src/milton-web/Cargo.toml +++ b/src/milton-web/Cargo.toml @@ -2,6 +2,7 @@ name = "milton" # not using version field; this is an application for now; sha is fine. version = "0.0.0" +authors = ["Danny Hadley "] edition = "2021" publish = false @@ -29,3 +30,5 @@ dotenv = "^0.15.0" serialport = { version = "^4.2.0", default-features = false } clap = { version = "^4.0.18", features = ["derive"] } kramer = { version = "^1.3.2", features = ["async-std", "kramer-async"] } +futures = { version = "^0.3.25" } +v4l = { version = "^0.13.0", features = ["v4l2"] } diff --git a/src/milton-web/config-example.toml b/src/milton-web/config-example.toml new file mode 100644 index 0000000..6dce583 --- /dev/null +++ b/src/milton-web/config-example.toml @@ -0,0 +1,44 @@ +[lights] +# the kernel managed location of our serial device for the xiao light MCU +device="" +baud=115200 + +[oauth] +# client id + secret for the "general" auth0 application used for oauth +auth_client_id="" +auth_client_secret="" + +# client id + secret for the auth0 _management_ api +management_client_id="" +management_client_secret="" + +# the redirect uri that our auth0 application is configured for. needs to match a value in the list of +# redirect uri's allowed on their end. +redirect_uri="" + +# the auth0 application domain. +domain="" + +[server] +# octoprint general api access info +octoprint_api_url="" +octoprint_api_key="" + +# the token that octoprint can use in its 'stream url" to have access to our stream endpoint +octoprint_stream_token="" + +# where to send users after completing their oauth exchange +auth_complete_uri="" + +# a secret string used to validate jwt tokens used in our http cookie store. +jwt_secret="" + +# ... +domain="" + +# redis configuration used for our session store +redis_host="" +redis_port=6379 + +# the kernel managed device path for our streaming endpoint +video_device="" diff --git a/src/milton-web/src/bin/milton.rs b/src/milton-web/src/bin/milton.rs index ecdcfc0..3ddd00d 100644 --- a/src/milton-web/src/bin/milton.rs +++ b/src/milton-web/src/bin/milton.rs @@ -10,14 +10,17 @@ use async_std::stream::StreamExt; struct RuntimeConfiguration { #[allow(unused)] lights: milton::lights::LightConfiguration, + oauth: milton::oauth::AuthZeroConfig, + server: milton::server::Configuration, } #[derive(Deserialize, clap::Parser)] +#[command(author, version = option_env!("MILTON_VERSION").unwrap_or_else(|| "dev"), about, long_about = None)] struct CommandLineOptions { + #[arg(short = 'c', long)] config: String, - device: Option, } async fn manage_effects( @@ -103,11 +106,7 @@ fn main() -> Result<()> { log::info!("loading config from '{}'", args.config); let contents = std::fs::read_to_string(args.config)?; log::info!("loaded config from '{contents}'"); - let mut parsed = toml::from_str::(&contents)?; - - if let Some(device) = args.device { - parsed.lights.device = device; - } + let parsed = toml::from_str::(&contents)?; log::info!("starting async main thread"); async_std::task::block_on(serve(parsed))?; diff --git a/src/milton-web/src/server/auth.rs b/src/milton-web/src/server/auth.rs index 6ca2642..7270308 100644 --- a/src/milton-web/src/server/auth.rs +++ b/src/milton-web/src/server/auth.rs @@ -100,8 +100,10 @@ pub async fn identify(request: Request) -> Result { }); log::info!("attempting to identify user from claims - {:?}", claims); - let mut res = AuthIdentifyResponse::default(); - res.version = request.state().version.clone(); + let mut res = AuthIdentifyResponse { + version: request.state().version.clone(), + ..Default::default() + }; if let Some(claims) = claims { let session_data = request.state().user_from_session(&claims.oid).await.ok_or_else(|| { diff --git a/src/milton-web/src/server/control.rs b/src/milton-web/src/server/control.rs index 8e2dca3..f23c653 100644 --- a/src/milton-web/src/server/control.rs +++ b/src/milton-web/src/server/control.rs @@ -1,8 +1,12 @@ use serde::{Deserialize, Serialize}; +use std::io; use tide::{Request, Response, Result}; use crate::{octoprint::OctoprintJobResponse, server::State}; +/// The stream endpoint will use this as the http multi-part boundary for its mjpg stream. +const MJPG_BOUNDARY: &str = "mjpg-boundary"; + /// Requests to the control api will receive this type serialized as json. #[derive(Debug, Serialize)] struct ControlResponse { @@ -49,6 +53,14 @@ enum ControlQuery { BasicColor(ColorControlQuery), } +/// Accessing the snapshot endpoint is something that we'd like octoprint to be able to do, in +/// addition to users authorized through the ui via http cookies. +#[derive(Deserialize, Debug)] +struct SnapshotUrlQuery { + /// The optional authorization token we're using to access the control mjpg stream. + token: Option, +} + // TODO: this will be useful once we're able to control specific colors. blocked by firmware. // fn parse_hex(input: &String) -> Option<(u8, u8, u8)> { // let mut results = (1..input.len()) @@ -66,39 +78,67 @@ enum ControlQuery { /// ROUTE: proxy to octoprint (mjpg-streamer) snapshot url pub async fn snapshot(request: Request) -> Result { - let claims = super::claims(&request).ok_or_else(|| { - log::warn!("unauthorized attempt to query state"); - tide::Error::from_str(404, "not-found") - })?; - - if request.state().authority(&claims.oid).await.is_none() { - return Ok(tide::Response::new(404)); + // TODO: replace this with a more robust application auth token storage + validation system. + match request.query::() { + Ok(query) if query.token.is_some() && query.token == request.state().config.octoprint_stream_token => { + log::info!("authorizing stream as octoprint"); + } + _ => { + let claims = super::claims(&request).ok_or_else(|| { + log::warn!("unauthorized attempt to access camera control"); + tide::Error::from_str(404, "not-found") + })?; + + if request.state().authority(&claims.oid).await.is_none() { + return Ok(tide::Response::new(404)); + } + } } - log::info!("fetching snapshot for user '{}'", claims.oid); - - let mut response = surf::get(&request.state().config.octoprint_snapshot_url) - .await - .map_err(|error| { - log::warn!("unable to request snapshot - {}", error); - tide::Error::from_str(404, "not-found") - })?; - - log::info!("snapshot done"); - - let mime = response.content_type().ok_or_else(|| { - std::io::Error::new( - std::io::ErrorKind::Other, - "unable to determine mime type from mjpg-streamer", - ) - })?; + // Create the channel whose receiver will be used as a async reader. + let (mut writer, drain) = futures::channel::mpsc::channel::>>(1); + let buf_drain = futures::stream::TryStreamExt::into_async_read(drain); + + // Prepare the response with the correct header + let response = tide::Response::builder(200) + .content_type(format!("multipart/x-mixed-replace;boundary={MJPG_BOUNDARY}").as_str()) + .body(tide::Body::from_reader(buf_drain, None)) + .build(); + + // In a separate task, continously check our shared buffer's timestamp. If that value differs + // from the timestamp of the last message sent on our end, send a new multipart chunk. + async_std::task::spawn(async move { + let frame_reader = request.state().video_data.read().await; + let mut last_frame = (*frame_reader).0; + drop(frame_reader); + + loop { + let frame_reader = request.state().video_data.read().await; + if (*frame_reader).0 != last_frame { + last_frame = (*frame_reader).0; + + // Start the buffer that we'll send using the boundary and some multi-part http header + // context. + let mut buffer = format!( + "--{MJPG_BOUNDARY}\r\nContent-Type: image/jpeg\r\nContent-Length: {}\r\n\r\n", + frame_reader.1.len(), + ) + .into_bytes(); + + // Actually push the JPEG data into our buffer. + buffer.extend_from_slice(frame_reader.1.as_slice()); + buffer.extend_from_slice(b"\r\n"); + + if let Err(error) = writer.try_send(Ok(buffer)) { + log::warn!("unable to send received data - {error}"); + break; + } + } + drop(frame_reader); + } + }); - Ok( - tide::Response::builder(response.status()) - .content_type(mime) - .body(response.take_body()) - .build(), - ) + Ok(response) } /// ROUTE: fetches current job information from octoprint api diff --git a/src/milton-web/src/server/mod.rs b/src/milton-web/src/server/mod.rs index 4b48280..c0b1384 100644 --- a/src/milton-web/src/server/mod.rs +++ b/src/milton-web/src/server/mod.rs @@ -3,6 +3,8 @@ use std::io::{Error, ErrorKind, Result}; use async_std::channel::Sender; use serde::Deserialize; use tide::{http::Cookie, Request, Response}; +use v4l::io::traits::CaptureStream; +use v4l::video::Capture; use crate::oauth; @@ -33,9 +35,6 @@ pub struct Configuration { /// API key for octoprint. (e.g abcdef) octoprint_api_key: String, - /// mjpg-streamer url. (e.g http://192.168.2.27:8090/?action=stream) - octoprint_snapshot_url: String, - /// The location to send users _back_ to after successful oauth exchanges. auth_complete_uri: String, @@ -50,6 +49,13 @@ pub struct Configuration { /// The domain we're hosting from; used for cookies. domain: String, + + /// The kernel managed device path compatible with v4l. + video_device: Option, + + /// A special token to be used by octoprint for our mjpg stream endpoint. This should be a + /// short-lived feature and replaced with a more robust application auth token system. + octoprint_stream_token: Option, } /// The builder-pattern impl for our shared `State` type. @@ -116,6 +122,8 @@ impl StateBuilder { .ok_or_else(|| Error::new(ErrorKind::NotFound, "no version provided"))?, redis: async_std::sync::Arc::new(async_std::sync::Mutex::new(None)), + + video_data: async_std::sync::Arc::new(async_std::sync::RwLock::new((None, Vec::with_capacity(0)))), }) } } @@ -140,6 +148,9 @@ pub struct State { /// A shared tcp connection to our redis connection. This eventually should be expanded into a /// pool of available tcp connections. redis: async_std::sync::Arc>>, + + /// A shared reference to our video data, should a request need one. + video_data: async_std::sync::Arc, Vec)>>, } impl State { @@ -181,6 +192,8 @@ impl State { Ok(output) } + /// This function is responsible for taking the unique id found in our session cookie and + /// returning the user data that we have previously stored in redis. pub(crate) async fn user_from_session(&self, id: T) -> Option where T: std::fmt::Display, @@ -257,6 +270,70 @@ pub async fn listen(state: State, addr: S) -> std::io::Result<()> where S: std::convert::AsRef, { + if let Some(path) = &state.config.video_device { + let dev = v4l::Device::with_path(path)?; + let mut has_support = false; + + 'outer: for format in dev.enum_formats()? { + for framesize in dev.enum_framesizes(format.fourcc)? { + for discrete in framesize.size.to_discrete() { + if format.fourcc == v4l::format::FourCC::new(b"MJPG") { + log::info!("found mjpg compatible format on {path}"); + dev.set_format(&v4l::Format::new( + discrete.width, + discrete.width, + v4l::format::FourCC::new(b"MJPG"), + ))?; + has_support = true; + break 'outer; + } + } + } + } + + if has_support { + let clone_ref = state.video_data.clone(); + let mut stream = v4l::prelude::MmapStream::with_buffers(&dev, v4l::buffer::Type::VideoCapture, 4)?; + + async_std::task::spawn(async move { + log::info!("video data read thread active"); + let mut last_debug = std::time::Instant::now(); + + let mut current_frames = 0; + loop { + let before = std::time::Instant::now(); + + match stream.next() { + Ok((buffer, meta)) => { + let after = std::time::Instant::now(); + let seconds_since = before.duration_since(last_debug).as_secs(); + current_frames += 1; + + let mut writable_reference = clone_ref.write().await; + // log::debug!("read video device meta - {}bytes", meta.bytesused); + *writable_reference = (Some(std::time::Instant::now()), buffer.to_vec()); + drop(writable_reference); + + if seconds_since > 3 { + let frame_read_time = after.duration_since(before).as_millis(); + log::info!( + "{current_frames}f ({seconds_since}s) {frame_read_time}ms per {}bytes", + meta.bytesused + ); + last_debug = before; + current_frames = 0; + } + } + Err(error) => { + log::error!("unable to read next stream from video device - {error}"); + async_std::task::sleep(std::time::Duration::from_millis(500)).await; + } + } + } + }); + } + } + let mut app = tide::with_state(state); app.at("/control").post(control::command);