diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..c201793 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1165 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +dependencies = [ + "anstyle", + "once_cell", + "windows-sys 0.59.0", +] + +[[package]] +name = "anyhow" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" +dependencies = [ + "backtrace", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytes_parser" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e37a46f3e15e6d40e650dbf90dba0097f30c65c62ff3b97a00ea845d14fb9d61" +dependencies = [ + "thiserror", +] + +[[package]] +name = "cc" +version = "1.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets", +] + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + +[[package]] +name = "flate2" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + +[[package]] +name = "log" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "miniz_oxide" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" +dependencies = [ + "adler2", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "proc-macro2" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.37.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustls" +version = "0.23.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "srrust" +version = "0.1.0" +dependencies = [ + "anstyle", + "anyhow", + "bincode", + "bytes_parser", + "chrono", + "env_logger", + "log", + "nix", + "quick-xml", + "serde", + "socket2", + "ureq", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "url", + "webpki-roots", +] + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "webpki-roots" +version = "0.26.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..e46e711 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "srrust" +version = "0.1.0" +edition = "2021" + +[dependencies] +anstyle = "1.0.8" +anyhow = { version = "1.0.89", features = ["backtrace"] } +bincode = "1.3.3" +bytes_parser = "0.1.5" +chrono = "0.4.38" +env_logger = "0.11.5" +log = "0.4.22" +nix = { version = "0.29.0", features = ["event"] } +quick-xml = "0.37.1" +serde = { version = "1.0.210", features = ["alloc", "derive"] } +socket2 = { version = "0.5.7", features = ["all"] } +ureq = { version = "2.12.1", features = ["native-certs"] } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5983338 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Régis Hanna + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 499fa99..089fa47 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,10 @@ # srrust -Serveur skyreacher en Rust +SkyReacher, aircraft position server. + +This server provides the position of aircraft that are close to client applications like [SkyReacher for Android](https://github.com/regishanna/srandroid). + +Client applications connect to the server and send their approximate location. This allows the server to only send the position of aircraft close to the client in order to optimize bandwidth. Aircraft positions are sent to clients using the GDL90 protocol. + +The server retrieves the position of aircraft using the following networks: +* [OGN](https://www.glidernet.org/) for glider positions (mainly via the FLARM protocol) and for aircraft positions using the SafeSky application +* [ADSBHub](https://www.adsbhub.org/) for aircraft equipped with ADS-B transponders diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..e25af91 --- /dev/null +++ b/src/client.rs @@ -0,0 +1,134 @@ +use crate::{dgramostream, gdl90, traffic_infos::TrafficInfos}; + +use std::{net::{SocketAddr, TcpStream}, os::fd::{AsFd, BorrowedFd}, time::Duration}; + + +// client 2D position +#[derive(Clone)] +pub struct Position { + pub latitude: f64, + pub longitude: f64, +} + + +pub struct Client { + socket: TcpStream, + address: SocketAddr, + position: Option, + recv_dgram: dgramostream::RecvDgram, +} + + +impl Client { + /// Creation of a new client + pub fn new(socket: TcpStream) -> Self { + // Set the client socket to be able to detect a connectivity problem as quickly as possible + Self::set_sock_options(&socket); + + // Get the address of the connected client + let address = socket.peer_addr().unwrap(); + + Self { + socket, + address, + position: None, + recv_dgram: dgramostream::RecvDgram::new(16), + } + } + + + /// Get the address of the client + pub fn address(&self) -> SocketAddr { + self.address + } + + + /// Receive the position of the client + pub fn recv_position(&mut self) -> anyhow::Result> { + // Reading the position datagram from the client + match self.recv_dgram.recv(&self.socket)? { + None => Ok(None), // The datagram is not yet reconstituted, nothing to do + Some(position_dgram) => { // The datagram is reconstituted, we parse it + self.position = Some(Self::parse_client_position_msg(position_dgram)?); + Ok(self.position.clone()) + } + } + } + + + /// Send traffic information to the client, only if it is nearby + pub fn send_traffic(&self, traffic_infos: &TrafficInfos) -> anyhow::Result<()> { + if self.traffic_close(traffic_infos) { + // The traffic is close to the client, we send it the information + + // Prepare the message in GDL90 format + let mut buffer = [0u8; 100]; + let len = gdl90::make_traffic_report_message(traffic_infos, &mut buffer).unwrap(); + + // Send the message as a datagram + dgramostream::send(&self.socket, &buffer[..len])?; + } + + Ok(()) + } + + + fn set_sock_options(socket: &TcpStream) { + let sock = socket2::SockRef::from(socket); + + // Setting TCP timeout + sock.set_tcp_user_timeout(Some(Duration::from_secs(10))).unwrap(); + + // Setting TCP keepalive + let keepalive = socket2::TcpKeepalive::new() + .with_time(Duration::from_secs(30)) + .with_interval(Duration::from_secs(5)) + .with_retries(2); + sock.set_tcp_keepalive(&keepalive).unwrap(); + } + + + fn parse_client_position_msg(msg: &[u8]) -> anyhow::Result { + let mut parser = bytes_parser::BytesParser::from(msg); + + let latitude = f64::from(parser.parse_i32()?) / 1_000_000.0; + anyhow::ensure!((-90.0..=90.0).contains(&latitude), "Latitude out of bounds"); + + let longitude = f64::from(parser.parse_i32()?) / 1_000_000.0; + anyhow::ensure!((-180.0..=180.0).contains(&longitude), "Longitude out of bounds"); + + Ok(Position { + latitude, + longitude, + }) + } + + + fn traffic_close(&self, traffic_infos: &TrafficInfos) -> bool { + let mut traffic_close = false; + + match &self.position { + None => (), // The client's position is not known, we consider that the traffic is not close + Some(position) => { + // Traffic must be in a square centered on the customer's position to be considered close + const MAX_DELTA_LATITUDE: f64 = 1.0; // In degrees + const MAX_DELTA_LONGITUDE: f64 = 1.0; // In degrees + if ((traffic_infos.latitude - position.latitude).abs() < MAX_DELTA_LATITUDE) && + ((traffic_infos.longitude - position.longitude).abs() < MAX_DELTA_LONGITUDE) { + traffic_close = true; + } + } + } + + traffic_close + } + + +} + + +impl AsFd for Client { + fn as_fd(&self) -> BorrowedFd<'_> { + self.socket.as_fd() + } +} diff --git a/src/client_pool.rs b/src/client_pool.rs new file mode 100644 index 0000000..e214078 --- /dev/null +++ b/src/client_pool.rs @@ -0,0 +1,248 @@ +use crate::{client, internal_com}; + +use nix::sys::epoll::{Epoll, EpollCreateFlags, EpollEvent, EpollFlags}; +use std::{net::TcpStream, os::fd::AsFd, sync::{atomic::{AtomicUsize, Ordering}, mpsc, Arc}, thread}; + + +// Maximum number of clients connected at the same time for the pool +const CLIENTS_MAX_NB: usize = 200; + + +// Event identifier for use in epoll data field + +const EVENT_TYPE_CLIENT: u32 = 1; +const EVENT_TYPE_TRAFFIC_RECV: u32 = 2; + +struct EventId(u64); + +impl EventId { + fn new(event_type: u32, event_number: u32) -> Self { + Self(u64::from(event_type) << 32 | u64::from(event_number)) + } + + fn event_type(&self) -> u32 { + (self.0 >> 32) as u32 + } + + fn event_number(&self) -> u32 { + (self.0 & 0x00000000_ffffffffu64) as u32 + } +} + +impl From for EventId { + fn from(event_id: u64) -> EventId { + Self(event_id) + } +} + +impl From for u64 { + fn from(event_id: EventId) -> u64 { + event_id.0 + } +} + + +pub struct ClientPool { + new_client_tx: mpsc::SyncSender, + nb_clients: Arc +} + + +impl ClientPool { + /// Creation of the client pool + pub fn new() -> Self { + // Creation of the channel to receive new clients + let (new_client_tx, new_client_rx) = mpsc::sync_channel(0); + + // Initialization of the current number of clients + let nb_clients = Arc::new(AtomicUsize::new(0)); + let nb_clients_thread = nb_clients.clone(); + + // Creation of the thread that will handle the client pool + thread::spawn(|| { + Self::work_thread(new_client_rx, nb_clients_thread); + }); + + Self {new_client_tx, nb_clients} + } + + + /// Add a new client to the pool + pub fn add_new_client(&self, socket: TcpStream) { + self.new_client_tx.send(socket).unwrap(); + } + + + /// Get current number of clients in the pool + pub fn get_nb_clients(&self) -> usize { + self.nb_clients.load(Ordering::Relaxed) + } + + + fn work_thread(new_client_rx: mpsc::Receiver, nb_clients: Arc) { + // Clients list + let mut clients = Vec::new(); + let mut free_clients = Vec::new(); // Index of free clients (None) in clients Vec + let mut clients_to_delete = Vec::new(); // Index of clients to delete in clients Vec + + // Create the epoll instance + let epoll = Epoll::new(EpollCreateFlags::empty()).unwrap(); + + // Create the traffic receiver and register it in epoll + let traffic_recv = internal_com::Receiver::new(true /* nonblocking */); + epoll.add(traffic_recv.as_fd(), + EpollEvent::new(EpollFlags::EPOLLIN, + EventId::new(EVENT_TYPE_TRAFFIC_RECV, 0).into())).unwrap(); + + let mut epoll_events = [EpollEvent::empty(); 100]; + loop { + // Wait for events + let nb_events = epoll.wait(&mut epoll_events, 100u16 /* milliseconds */).unwrap(); + + // Read the events + for epoll_event in epoll_events.iter().take(nb_events) { + let event_id: EventId = epoll_event.data().into(); + match event_id.event_type() { + EVENT_TYPE_CLIENT => { + // Process the client event + let client_index = event_id.event_number() as usize; + Self::process_client_event(client_index, &epoll, &mut clients, &mut free_clients, &nb_clients); + } + + EVENT_TYPE_TRAFFIC_RECV => { + // Process the traffic receiver event + Self::process_traffic_event(&traffic_recv, &epoll, &mut clients, &mut free_clients, &mut clients_to_delete, &nb_clients); + } + + event_type => panic!("Unknown event type : {event_type}"), + } + } + + // Check if there are new clients + Self::check_new_client(&new_client_rx, &epoll, &mut clients, &mut free_clients, &nb_clients); + } + } + + + fn process_client_event(client_index: usize, epoll: &Epoll, clients: &mut [Option], free_clients: &mut Vec, nb_clients: &Arc) { + if let Some(client) = &mut clients[client_index] { + match client.recv_position() { + Ok(Some(position)) => { + log::info!("New position received ({}, {}) from client {}", + position.latitude, position.longitude, client.address()); + } + Ok(None) => { + // Nothing to do + } + Err(e) => { + // Error while receiving the client position + log::warn!("Receive error ({}) from client {}", e, client.address()); + Self::delete_client(client_index, epoll, clients, free_clients, nb_clients); + } + } + } + } + + + fn process_traffic_event(traffic_recv: &internal_com::Receiver, epoll: &Epoll, + clients: &mut [Option], free_clients: &mut Vec, clients_to_delete: &mut Vec, + nb_clients: &Arc) { + + // Loop until there is no more traffic information to receive, to optimize the number of epoll.wait calls + loop { + match traffic_recv.recv() { + Err(e) => { + // Exit the loop if an error occurs + // A WouldBlock error is normal because we are in non-blocking mode + // and indicates that there is no more traffic information to receive + match e.downcast_ref::() { + Some(err) if err.kind() == std::io::ErrorKind::WouldBlock => (), + Some(_) | None => log::warn!("Traffic receive error : {}", e), + } + break; + } + + Ok(infos) => { + // Send the traffic information to all clients + for (i, client_opt) in clients.iter().enumerate() { + if let Some(client) = client_opt { + if let Err(e) = client.send_traffic(&infos) { + log::warn!("Send error ({}) to client {}", e, client.address()); + // Add the client to the delete list + clients_to_delete.push(i); + } + } + } + + // Delete clients that must be deleted + while let Some(i) = clients_to_delete.pop() { + Self::delete_client(i, epoll, clients, free_clients, nb_clients); + } + } + } + } + } + + + fn add_client(client: client::Client, epoll: &Epoll, clients: &mut Vec>, free_clients: &mut Vec, nb_clients: &Arc) { + // If the maximum number of clients is reached, we refuse the new client + let current_nb_clients = nb_clients.fetch_add(1, Ordering::Relaxed); + if current_nb_clients >= CLIENTS_MAX_NB { + nb_clients.fetch_sub(1, Ordering::Relaxed); + log::warn!("Unable to connect new client {} : maximum number of clients ({}) for the pool is reached", client.address(), CLIENTS_MAX_NB); + } + else { + log::info!("New client connected : {}, {}th in the pool", client.address(), current_nb_clients + 1); + + // Add the new client to the list + let client_index; + + // If there are free clients, we reuse one + if let Some(i) = free_clients.pop() { + assert!(clients[i].is_none()); + clients[i] = Some(client); + client_index = i; + } + // Otherwise we add a new client + else { + clients.push(Some(client)); + client_index = clients.len() - 1; + } + + // Register the event in epoll + epoll.add(clients[client_index].as_ref().unwrap().as_fd(), + EpollEvent::new(EpollFlags::EPOLLIN, + EventId::new(EVENT_TYPE_CLIENT, client_index.try_into().unwrap()).into())).unwrap(); + } + } + + + fn delete_client(client_index: usize, epoll: &Epoll, clients: &mut [Option], free_clients: &mut Vec, nb_clients: &Arc) { + let client = &mut clients[client_index]; + + log::info!("Client {} is disconnected", client.as_ref().unwrap().address()); + + // Unregister the event in epoll + epoll.delete(client.as_ref().unwrap().as_fd()).unwrap(); + + // Free the client + *client = None; + free_clients.push(client_index); + + // Decrement the number of clients + nb_clients.fetch_sub(1, Ordering::Relaxed); + } + + + fn check_new_client(new_client_rx: &mpsc::Receiver, epoll: &Epoll, + clients: &mut Vec>, free_clients: &mut Vec, nb_clients: &Arc) { + + // While there are new clients, we add them to the pool + while let Ok(socket) = new_client_rx.try_recv() { + socket.set_nonblocking(true).unwrap(); // We can't block all clients because of one blocking client + Self::add_client(client::Client::new(socket), epoll, clients, free_clients, nb_clients); + } + } + + +} \ No newline at end of file diff --git a/src/dgramostream.rs b/src/dgramostream.rs new file mode 100644 index 0000000..3be3e5f --- /dev/null +++ b/src/dgramostream.rs @@ -0,0 +1,96 @@ +//! Datagram over Stream: sending and receiving datagrams over a blocking TCP socket. +//! A header representing the size of the datagram in 16-bit big endian is +//! inserted in front of each datagram to allow its reception in stream mode +//! + +use std::{io::{Read, Write}, net::TcpStream}; + + +/// Send a datagram +pub fn send(mut sock: &TcpStream, buf: &[u8]) -> anyhow::Result<()> { + // Sending the header containing the size of the buffer in big endian + let buf_len_bytes = u16::try_from(buf.len())?.to_be_bytes(); + sock.write_all(&buf_len_bytes)?; + + // Sending the buffer + sock.write_all(buf)?; + + Ok(()) +} + + +/// Allows the reconstruction of a datagram from reading a TCP socket +pub struct RecvDgram { + datagram: Vec, // Buffer containing the datagram + datagram_cur_len: usize, // Current buffer size + expected_len: Option, // Expected size of the buffer (known thanks to the header) + header_buf: [u8; 2], // Buffer containing the header + header_buf_cur_len: usize, // Current header buffer size +} + +impl RecvDgram { + pub fn new(datagram_max_len: u16) -> Self { + Self { + datagram: vec![0; datagram_max_len as usize], + datagram_cur_len: 0, + expected_len: None, + header_buf: [0; 2], + header_buf_cur_len: 0, + } + } + + /// Deletes a datagram being received + pub fn clear(&mut self) { + self.expected_len = None; + self.header_buf_cur_len = 0; + } + + /// Receives a datagram + /// Warning: in the event of an error, you must call "clear" to receive a new datagram + pub fn recv(&mut self, mut sock: &TcpStream) -> anyhow::Result> { + // Should we receive the header or the buffer? + match self.expected_len { + None => { + // We have not completely received the header, we continue + let nb = sock.read(&mut self.header_buf[self.header_buf_cur_len..])?; + if nb == 0 { + Err(anyhow::anyhow!("Connection closed by remote")) + } + else { + self.header_buf_cur_len += nb; + // Have we received all the header? + if self.header_buf_cur_len >= self.header_buf.len() { + // Yes, we read the expected size of the datagram + let len = u16::from_be_bytes(self.header_buf) as usize; + anyhow::ensure!(len <= self.datagram.len(), + "Expected size of the datagram ({}) greater than the size of the buffer ({})", len, self.datagram.len()); + self.expected_len = Some(len); + self.datagram_cur_len = 0; + } + Ok(None) + } + }, + Some(expct_len) => { + // We have already received the header, we receive the buffer (or we continue to receive it) + let nb = sock.read(&mut self.datagram[self.datagram_cur_len..expct_len])?; + if nb == 0 { + Err(anyhow::anyhow!("Connection closed by remote")) + } + else { + self.datagram_cur_len += nb; + // Have we received the entire buffer? + if self.datagram_cur_len >= expct_len { + // Yes, this is the end of datagram reception + self.clear(); + Ok(Some(&self.datagram[..expct_len])) + } + else { + // No, we will have to recall the method + Ok(None) + } + } + } + } + } + +} diff --git a/src/gdl90.rs b/src/gdl90.rs new file mode 100644 index 0000000..096b384 --- /dev/null +++ b/src/gdl90.rs @@ -0,0 +1,245 @@ +//! GDL90 message formatting +//! See +//! + +use crate::traffic_infos::{TrafficInfos, AddressType}; + + +// Structure of a message + +const HEAD_LEN: usize = 2; +const TAIL_LEN: usize = 3; +const FLAG_BYTE: u8 = 0x7e; +const CONTROL_ESCAPE_CHAR: u8 = 0x7d; + + +// TRAFFIC REPORT message + +const TRAFFIC_REPORT_MESSAGE_ID: u8 = 20; +const TRAFFIC_REPORT_LEN: usize = 27; + +const TRAFFIC_REPORT_ADDRESS_OFFSET: usize = 0; +const TRAFFIC_REPORT_LATITUDE_OFFSET: usize = 4; +const TRAFFIC_REPORT_LONGITUDE_OFFSET: usize = 7; +const TRAFFIC_REPORT_ALTITUDE_OFFSET: usize = 10; +const TRAFFIC_REPORT_MISC_INDICATOR_OFFSET: usize = 11; +const TRAFFIC_REPORT_HORIZONTAL_VELOCITY_OFFSET: usize = 13; +const TRAFFIC_REPORT_VERTICAL_VELOCITY_OFFSET: usize = 14; +const TRAFFIC_REPORT_TRACK_OFFSET: usize = 16; +const TRAFFIC_REPORT_CALLSIGN_OFFSET: usize = 18; + + +// CRC table +const CRC_ARRAY: [u16; 256] = [ + 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7, + 0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF, + 0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6, + 0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE, + 0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485, + 0xA56A, 0xB54B, 0x8528, 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D, + 0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4, + 0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC, + 0x48C4, 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823, + 0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, 0x9969, 0xA90A, 0xB92B, + 0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12, + 0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A, + 0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41, + 0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49, + 0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70, + 0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78, + 0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, 0xE16F, + 0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, 0x6067, + 0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E, + 0x02B1, 0x1290, 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256, + 0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D, + 0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, + 0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C, + 0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634, + 0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, 0xB98A, 0xA9AB, + 0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3, + 0xCB7D, 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A, + 0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92, + 0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9, + 0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1, + 0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8, + 0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0 +]; + + +/// Formats a TRAFFIC REPORT message in a provided buffer +/// Returns the used size of the buffer +pub fn make_traffic_report_message(infos: &TrafficInfos, buffer: &mut [u8]) -> anyhow::Result { + let mut buf = [0u8; HEAD_LEN + TRAFFIC_REPORT_LEN + TAIL_LEN]; + + // Address + { + let offset = HEAD_LEN + TRAFFIC_REPORT_ADDRESS_OFFSET; + buf[offset] = u8::from(&(infos.addr_type)); + buf[offset + 1] = ((infos.address >> 16) & 0xff) as u8; + buf[offset + 2] = ((infos.address >> 8) & 0xff) as u8; + buf[offset + 3] = (infos.address & 0xff) as u8; + } + + // Latitude on 24 signed bits + { + let mut latitude = ((infos.latitude * f64::from(0x0080_0000)) / 180.0) as i32; + latitude = latitude.clamp(-0x0040_0000, 0x003f_ffff); + let offset = HEAD_LEN + TRAFFIC_REPORT_LATITUDE_OFFSET; + buf[offset] = ((latitude >> 16) & 0xff) as u8; + buf[offset + 1] = ((latitude >> 8) & 0xff) as u8; + buf[offset + 2] = (latitude & 0xff) as u8; + } + + // Longitude on 24 signed bits + { + let mut longitude = ((infos.longitude * f64::from(0x0080_0000)) / 180.0) as i32; + longitude = longitude.clamp(-0x0080_0000, 0x007f_ffff); + let offset = HEAD_LEN + TRAFFIC_REPORT_LONGITUDE_OFFSET; + buf[offset] = ((longitude >> 16) & 0xff) as u8; + buf[offset + 1] = ((longitude >> 8) & 0xff) as u8; + buf[offset + 2] = (longitude & 0xff) as u8; + } + + // Altitude on 12 bits, 1000 ft offset + { + let mut altitude = ((if infos.altitude < -1000 { -1000 } else {infos.altitude}) + 1000) / 25; + if altitude > 0xffe { + altitude = 0xffe; + } + let offset = HEAD_LEN + TRAFFIC_REPORT_ALTITUDE_OFFSET; + buf[offset] = ((altitude >> 4) & 0xff) as u8; + buf[offset + 1] |= ((altitude << 4) & 0xf0) as u8; + } + + // Miscellanous indicators + { + let misc_indicator = (if infos.track.is_some() { 1u8 } else { 0 }) | 8; + let offset = HEAD_LEN + TRAFFIC_REPORT_MISC_INDICATOR_OFFSET; + buf[offset] |= misc_indicator; + } + + // Ground speed on 12 bits + { + let ground_speed = match infos.ground_speed { + None => 0xfffu32, + Some(gs) => { + if gs < 0 { + 0 + } + else if gs > 0xffe { + 0xffe + } + else { + gs as u32 + } + } + }; + let offset = HEAD_LEN + TRAFFIC_REPORT_HORIZONTAL_VELOCITY_OFFSET; + buf[offset] = ((ground_speed >> 4) & 0xff) as u8; + buf[offset + 1] |= ((ground_speed << 4) & 0xf0) as u8; + } + + // Vertical speed on 12 bits + { + let vertical_speed = match infos.vertical_speed { + None => -0x800i32, + Some(vs) => vs.clamp(-32640, 32640) / 64 + }; + let offset = HEAD_LEN + TRAFFIC_REPORT_VERTICAL_VELOCITY_OFFSET; + buf[offset] |= ((vertical_speed >> 8) & 0x0f) as u8; + buf[offset + 1] = (vertical_speed & 0xff) as u8; + } + + // Track on 8 bits + { + let mut track = match infos.track { + None => 0, + Some(tr) => (tr * 256) / 360 + }; + if track > 255 { + track = 255; + } + let offset = HEAD_LEN + TRAFFIC_REPORT_TRACK_OFFSET; + buf[offset] = track as u8; + } + + // Callsign on 8 characters + { + let offset = HEAD_LEN + TRAFFIC_REPORT_CALLSIGN_OFFSET; + let size_callsign = infos.callsign.chars().count(); + for i in 0usize..8 { + let c_to_append = if i < size_callsign { infos.callsign.chars().nth(i).unwrap() } else { ' ' }; + buf[offset + 1] = if c_to_append.is_ascii() { c_to_append as u8 } else { b'?' }; + } + } + + // Filling header and tail fields + fill_header_and_tail(TRAFFIC_REPORT_MESSAGE_ID, &mut buf); + + // Application of byte-stuffing + byte_stuff(&buf, buffer) +} + + +impl From<&AddressType> for u8 { + fn from(value: &AddressType) -> Self { + match value { + AddressType::AdsbIcao => 0, + AddressType::Ogn => 6 + } + } +} + + +fn fill_header_and_tail(message_id: u8, msg_buf: &mut [u8]) { + // Flag byte at the beginning and at the end + msg_buf[0] = FLAG_BYTE; + msg_buf[msg_buf.len() - 1] = FLAG_BYTE; + + // Message id + msg_buf[1] = message_id; + + // CRC on the message id and message data fields + let crc = compute_crc(&msg_buf[1..(msg_buf.len() - 3)]); + msg_buf[msg_buf.len() - 3] = (crc & 0xff) as u8; // LSB first + msg_buf[msg_buf.len() - 2] = ((crc >> 8) & 0xff) as u8; +} + + +/// CRC CRC-CCITT +/// The following buffer: 0x00 0x81 0x41 0xDB 0xD0 0x08 0x02 +/// gives a CRC of 0x8BB3 +fn compute_crc(buffer: &[u8]) -> u16 { + let mut crc = 0u16; + + for val in buffer { + crc = CRC_ARRAY[(crc >> 8) as usize] ^ (crc << 8) ^ u16::from(*val); + } + + crc +} + + +fn byte_stuff(message: &[u8], buffer: &mut [u8]) -> anyhow::Result { + let mut cur_len = 0usize; + + for (i, &val) in message.iter().enumerate() { + if (i > 0) && (i < (message.len() - 1)) && // Start and end flags are excluded from replacement + ((val == FLAG_BYTE) || (val == CONTROL_ESCAPE_CHAR)) { + // Inserting a control escape character + anyhow::ensure!(buffer.len() >= cur_len + 2, "Insufficient buffer size"); // Verification that the buffer size is large enough + buffer[cur_len] = CONTROL_ESCAPE_CHAR; + cur_len += 1; + buffer[cur_len] = val ^ 0x20; + cur_len += 1; + } + else { + // No insert + anyhow::ensure!(buffer.len() > cur_len, "Insufficient buffer size"); // Verification that the buffer size is large enough + buffer[cur_len] = val; + cur_len += 1; + } + } + + Ok(cur_len) +} diff --git a/src/internal_com.rs b/src/internal_com.rs new file mode 100644 index 0000000..aef81cd --- /dev/null +++ b/src/internal_com.rs @@ -0,0 +1,87 @@ +use crate::traffic_infos::TrafficInfos; + +use socket2::{Socket, Domain, Type}; +use std::{net::{Ipv4Addr, SocketAddr, SocketAddrV4, UdpSocket}, os::fd::AsFd}; + + +// Multicast address and port to use +const MULTICAST_ADDR_V4: Ipv4Addr = Ipv4Addr::new(224,0,0,64); +const MULTICAST_PORT: u16 = 1665; + + +/// Receiving traffic information from sources +pub struct Receiver { + socket: UdpSocket, +} + +impl Receiver { + pub fn new(nonblocking: bool) -> Self { + // We use the socket2 crate because UdpSocket does not allow setting the SO_REUSEPORT option + // necessary to have several receivers listening on the same multicast port + let sock = Socket::new(Domain::IPV4, Type::DGRAM, None).unwrap(); + sock.set_reuse_port(true).unwrap(); + + // Bind the socket to the listening multicast address and port + sock.bind(&SocketAddrV4::new(MULTICAST_ADDR_V4, MULTICAST_PORT).into()).unwrap(); + + // Now we can convert to UdpSocket + let socket: UdpSocket = sock.into(); + + // We set the socket to non-blocking mode if asked + socket.set_nonblocking(nonblocking).unwrap(); + + // We subscribe to the local multicast address + socket.join_multicast_v4(&MULTICAST_ADDR_V4, &Ipv4Addr::LOCALHOST).unwrap(); + + // We expect to receive local frames but no filtering on the remote port + socket.connect(SocketAddr::from((Ipv4Addr::LOCALHOST, 0))).unwrap(); + + Self {socket} + } + + /// Reading of traffic information from sources + pub fn recv(&self) -> anyhow::Result { + // Reading on multicast socket + let mut buf = [0; 100]; + let recv_size = self.socket.recv(&mut buf)?; + + // Deserialization to reconstruct traffic information + let traffic_infos: TrafficInfos = bincode::deserialize(&buf[..recv_size])?; + Ok(traffic_infos) + } + +} + +impl AsFd for Receiver { + fn as_fd(&self) -> std::os::unix::prelude::BorrowedFd<'_> { + self.socket.as_fd() + } +} + + +/// Transmission of traffic information to all clients +pub struct Sender { + socket: UdpSocket, +} + +impl Sender { + pub fn new() -> Self { + // Bind the socket to the local address without imposing a transmission port + let socket = UdpSocket::bind(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0)).unwrap(); + + // We will send the frames to the address and the multicast port + socket.connect(SocketAddr::from((MULTICAST_ADDR_V4, MULTICAST_PORT))).unwrap(); + + Sender {socket} + } + + /// Sending information on traffic to all clients + pub fn send(&self, traffic_infos: &TrafficInfos) { + // Serialization of traffic information in a buffer to be able to send it + let buf = bincode::serialize(traffic_infos).unwrap(); + + // Sending the buffer on the multicast socket + self.socket.send(&buf).unwrap(); + } + +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..1d73b82 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,50 @@ +use server::Server; +use src_ogn::SrcOgn; +use src_adsbhub::SrcAdsbhub; +use std::io::Write; + +mod traffic_infos; +mod dgramostream; +mod gdl90; +mod internal_com; +mod server; +mod client_pool; +mod client; +mod src_ogn; +mod src_adsbhub; + +fn main() { + // Init and customization of the trace system + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")) + .format(|buf, record| { + let level_color = match record.level() { + log::Level::Error => Some(anstyle::Color::from(anstyle::AnsiColor::Red)), + log::Level::Warn => Some(anstyle::Color::from(anstyle::AnsiColor::Yellow)), + _ => None + }; + let level_style = anstyle::Style::new().fg_color(level_color); + writeln!( + buf, + "[{}-{}{}{:#}-{}:{}] {}", + chrono::Local::now().format("%H:%M:%S%.6f"), + level_style, + record.level(), + level_style, + record.file().unwrap_or("unknown"), + record.line().unwrap_or(0), + record.args() + ) + }) + .init(); + + log::info!("Start {} v{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")); + + // Launch of reception of OGN traffic + SrcOgn::start_receive(); + + // Launch of reception of ADSBHub traffic + SrcAdsbhub::start_receive(); + + // Listening and processing client connections (blocking) + Server::new().listen_connections(); +} diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 0000000..7260255 --- /dev/null +++ b/src/server.rs @@ -0,0 +1,53 @@ +use crate::client_pool::ClientPool; + +use std::{net::TcpListener, thread}; + + +pub struct Server { + client_pools: Vec +} + + +impl Server { + /// Creation of a new server + pub fn new() -> Self { + // Create a pool of clients for each CPU + let nb_cpus = thread::available_parallelism().unwrap().get(); + let mut client_pools = Vec::new(); + for _ in 0..nb_cpus { + client_pools.push(ClientPool::new()); + } + log::info!("{} pools of clients created", nb_cpus); + + Self {client_pools} + } + + + /// Listening and processing connections from clients + /// => This method is blocking + pub fn listen_connections(&self) { + let listener = TcpListener::bind("0.0.0.0:1664").unwrap(); + loop { + // Wait for a new client connection + let (socket, _) = listener.accept().unwrap(); + + // Add the new client to the least populated pool + self.least_polpulated_pool().add_new_client(socket); + } + } + + + fn least_polpulated_pool(&self) -> &ClientPool { + let mut min = self.client_pools[0].get_nb_clients(); + let mut idx = 0; + for i in 1..self.client_pools.len() { + let nb_clients = self.client_pools[i].get_nb_clients(); + if nb_clients < min { + min = nb_clients; + idx = i; + } + } + &self.client_pools[idx] + } + +} diff --git a/src/src_adsbhub.rs b/src/src_adsbhub.rs new file mode 100644 index 0000000..3af6d28 --- /dev/null +++ b/src/src_adsbhub.rs @@ -0,0 +1,309 @@ +//! Get aircraft informations from `ADSBHub` network with SBS formatting +//! See for SBS message specification +//! + +use crate::{internal_com, traffic_infos::{AddressType, TrafficInfos}}; + +use anyhow::{anyhow, Context}; +use core::str; +use std::{io::Read, net::TcpStream, thread, time::Duration, str::FromStr}; + + +const ADSBHUB_ADDR: &str = "data.adsbhub.org:5002"; + +// Subtype for SBS MSG messages +const SBS_MSG_TYPE_NONE: u32 = 0; +const SBS_MSG_TYPE_ES_IDENTIFICATION_AND_CATEGORY: u32 = 1; +const SBS_MSG_TYPE_ES_AIRBORNE_POSITION_MESSAGE: u32 = 3; +const SBS_MSG_TYPE_ES_AIRBORNE_VELOCITY_MESSAGE: u32 = 4; + +// Position of data fields of an SBS MSG message +const SBS_FIELD_POS_MESSAGE_TYPE: usize = 0; +const SBS_FIELD_POS_TRANSMISSION_TYPE: usize = 1; +const SBS_FIELD_POS_HEX_IDENT: usize = 4; +const SBS_FIELD_POS_CALLSIGN: usize = 10; +const SBS_FIELD_POS_ALTITUDE: usize = 11; +const SBS_FIELD_POS_GROUND_SPEED: usize = 12; +const SBS_FIELD_POS_TRACK: usize = 13; +const SBS_FIELD_POS_LATITUDE: usize = 14; +const SBS_FIELD_POS_LONGITUDE: usize = 15; +const SBS_FIELD_POS_VERTICAL_RATE: usize = 16; +const SBS_FIELD_POS_IS_ON_GROUND: usize = 21; // Last position + +const BODY_FIELD_FIRST_POSITION: usize = SBS_FIELD_POS_CALLSIGN; // Position of the first field of the message body + + +pub struct SrcAdsbhub { + sender: internal_com::Sender, +} + +impl SrcAdsbhub { + /// Starts reception of Adsbhub traffic + pub fn start_receive() { + thread::spawn(|| { + Self::work_thread(); + }); + } + + + fn new() -> SrcAdsbhub { + SrcAdsbhub { + sender: internal_com::Sender::new(), + } + } + + + fn work_thread() { + let adsbhub = Self::new(); + loop { + if let Err(e) = adsbhub.get_and_send_positions() { + log::warn!("{:#}", e); + } + thread::sleep(Duration::from_secs(5)); + } + } + + + fn get_and_send_positions(&self) -> anyhow::Result<()> { + // variable for get_message + let mut rx_buf = [0u8; 100_000]; + let mut rx_buf_current_size = 0usize; + let mut msg_begin_offset = 0usize; + + // variable for parse_message + let mut traffic_infos = TrafficInfos::default(); + let mut last_message_type = SBS_MSG_TYPE_NONE; + let mut last_hex_ident = 0u32; + + // Connection to ADSBHub network + let mut sock = TcpStream::connect(ADSBHUB_ADDR).context("Failed to connect to ADSBHub")?; + + // Setting the socket to quickly detect a silent disconnection from the remote + Self::set_sock_options(&sock); + + // Infinite message reading and processing loop + loop { + // Get one SBS message + let msg = Self::get_message(&mut sock, &mut rx_buf, &mut rx_buf_current_size, &mut msg_begin_offset)?; + //println!("SBS msg = {}", str::from_utf8(msg)?); + + // Parse the SBS message + if let Some(()) = Self::parse_message(msg, &mut traffic_infos, &mut last_message_type, &mut last_hex_ident)? { + // A complete sequence of messages has been received, the traffic information is valid + //println!("{:?}", traffic_infos); + + // Sending traffic information to clients + self.sender.send(&traffic_infos); + } + } + } + + + fn set_sock_options(socket: &TcpStream) { + let sock = socket2::SockRef::from(socket); + + // Setting TCP keepalive + let keepalive = socket2::TcpKeepalive::new() + .with_time(Duration::from_secs(30)) + .with_interval(Duration::from_secs(5)) + .with_retries(2); + sock.set_tcp_keepalive(&keepalive).unwrap(); + } + + + fn get_message<'a>(socket: &mut TcpStream, rx_buf: &'a mut [u8], rx_buf_current_size: &mut usize, begin_offset: &mut usize) + -> anyhow::Result<&'a [u8]> { + const MSG_END_VALUE: u8 = b'\n'; + let mut current_offset = *begin_offset; + + // Look for the end of the SBS message + 'msg_end_search: loop { + // If we have processed all the data received, we retrieve new data + if current_offset >= *rx_buf_current_size { + // If there is no more space in the rx buffer, + // we shift the message to the beginning of the buffer to be able to fill it + if *rx_buf_current_size >= rx_buf.len() { + rx_buf.copy_within(*begin_offset..*rx_buf_current_size, 0usize); + *rx_buf_current_size -= *begin_offset; + *begin_offset = 0; + current_offset = *rx_buf_current_size; + + // If the buffer is still full, it means that the message is larger than + // the reception buffer, this is not normal, we return an error + anyhow::ensure!(*rx_buf_current_size < rx_buf.len(), "SBS message too long"); + } + + // Retrieve new data + let nb = socket.read(&mut rx_buf[*rx_buf_current_size..]).context("Failed to read data from ADSBHub")?; + anyhow::ensure!(nb > 0, "Connection closed by ADSBHub"); + *rx_buf_current_size += nb; + } + + // Look for the next end of message in the received data + while current_offset < *rx_buf_current_size { + // If we found the end of message, we leave the big loop + if rx_buf[current_offset] == MSG_END_VALUE { + break 'msg_end_search; + } + current_offset += 1; + } + } + + // Found the end of message, we set the beginning of the next message + let current_begin_offset = *begin_offset; + *begin_offset = current_offset + 1; + + Ok(&rx_buf[current_begin_offset..current_offset]) + } + + + fn parse_message(message: &[u8], traffic_infos: &mut TrafficInfos, last_message_type: &mut u32, last_hex_ident: &mut u32) + -> anyhow::Result> { + let msg = str::from_utf8(message).context("Invalid character in ADSBHub message")?; + + // Browses all message fields + let mut last_position = 0; + for (i, field) in msg.split(',').enumerate() { + last_position = i; + if i < BODY_FIELD_FIRST_POSITION { + // Field of message header + Self::parse_message_header_field(i, field, traffic_infos, last_message_type, last_hex_ident)?; + } + else { + // Field of message body + if *last_message_type == SBS_MSG_TYPE_ES_IDENTIFICATION_AND_CATEGORY { + Self::parse_message1_body_field(i, field, traffic_infos)?; + } + else if *last_message_type == SBS_MSG_TYPE_ES_AIRBORNE_POSITION_MESSAGE { + Self::parse_message3_body_field(i, field, traffic_infos)?; + } + else if *last_message_type == SBS_MSG_TYPE_ES_AIRBORNE_VELOCITY_MESSAGE { + Self::parse_message4_body_field(i, field, traffic_infos)?; + } + else { + return Err(anyhow!("Unexpected SBS MSG type")); + } + } + } + + anyhow::ensure!(last_position == SBS_FIELD_POS_IS_ON_GROUND, "Wrong number of fields in SBS message"); + + // If it is the last message in the sequence, we return that parse is completed + if *last_message_type == SBS_MSG_TYPE_ES_AIRBORNE_VELOCITY_MESSAGE { + Ok(Some(())) + } + else { + Ok(None) + } + } + + + fn parse_message_header_field(field_position: usize, field: &str, traffic_infos: &mut TrafficInfos, last_message_type: &mut u32, last_hex_ident: &mut u32) + -> anyhow::Result<()> { + match field_position { + SBS_FIELD_POS_MESSAGE_TYPE => { // The first field is constant + anyhow::ensure!(field == "MSG", "First SBS field is not 'MSG'"); + }, + + SBS_FIELD_POS_TRANSMISSION_TYPE => { // We check the sequence of messages : ES_IDENTIFICATION_AND_CATEGORY then ES_AIRBORNE_POSITION_MESSAGE then ES_AIRBORNE_VELOCITY_MESSAGE + let msg_type = u32::from_str(field).context("Failed to parse SBS MSG subtype")?; + + if msg_type == SBS_MSG_TYPE_ES_IDENTIFICATION_AND_CATEGORY { + if (*last_message_type == SBS_MSG_TYPE_NONE) || (*last_message_type == SBS_MSG_TYPE_ES_AIRBORNE_VELOCITY_MESSAGE) { + // First message in the sequence, we reset the information structure + *traffic_infos = TrafficInfos::default(); + } + else { + return Err(anyhow!("Invalid SBS MSG sequence")); + } + } + else if msg_type == SBS_MSG_TYPE_ES_AIRBORNE_POSITION_MESSAGE { + anyhow::ensure!(*last_message_type == SBS_MSG_TYPE_ES_IDENTIFICATION_AND_CATEGORY, "Invalid SBS MSG sequence"); + } + else if msg_type == SBS_MSG_TYPE_ES_AIRBORNE_VELOCITY_MESSAGE { + anyhow::ensure!(*last_message_type == SBS_MSG_TYPE_ES_AIRBORNE_POSITION_MESSAGE, "Invalid SBS MSG sequence"); + } + else { + return Err(anyhow!("Unexpected SBS MSG subtype {}", msg_type)); + } + + *last_message_type = msg_type; + }, + + SBS_FIELD_POS_HEX_IDENT => { // Hex identifier of the mode S transponder + let hex_ident = u32::from_str_radix(field, 16).context("Failed to parse SBS MSG hex identifier")?; + + if *last_message_type == SBS_MSG_TYPE_ES_IDENTIFICATION_AND_CATEGORY { + // For the first message of the sequence, the identifier must be different from the previous one + anyhow::ensure!(hex_ident != *last_hex_ident, "SBS hex identifier must be different from last sequence"); + } + else { + // For other messages, we must keep the same identifier + anyhow::ensure!(hex_ident == *last_hex_ident, "SBS hex identifier must be the same for all messages of the sequence"); + traffic_infos.addr_type = AddressType::AdsbIcao; + traffic_infos.address = hex_ident; + } + + *last_hex_ident = hex_ident; + }, + + _ => () // Other fields are not used and are therefore considered valid + } + + Ok(()) + } + + + fn parse_message1_body_field(field_position: usize, field: &str, traffic_infos: &mut TrafficInfos) + -> anyhow::Result<()> { + if field_position == SBS_FIELD_POS_CALLSIGN { + anyhow::ensure!(!field.is_empty(), "Callsign in SBS message is empty"); + traffic_infos.callsign = String::from(field); + } + + Ok(()) + } + + + fn parse_message3_body_field(field_position: usize, field: &str, traffic_infos: &mut TrafficInfos) + -> anyhow::Result<()> { + match field_position { + SBS_FIELD_POS_ALTITUDE => traffic_infos.altitude = i32::from_str(field).context("Failed to parse SBS MSG altitude")?, + SBS_FIELD_POS_LATITUDE => traffic_infos.latitude = f64::from_str(field).context("Failed to parse SBS MSG latitude")?, + SBS_FIELD_POS_LONGITUDE => traffic_infos.longitude = f64::from_str(field).context("Failed to parse SBS MSG longitude")?, + _ => () // Other fields are not used and are therefore considered valid + } + + Ok(()) + } + + + fn parse_message4_body_field(field_position: usize, field: &str, traffic_infos: &mut TrafficInfos) + -> anyhow::Result<()> { + match field_position { + SBS_FIELD_POS_GROUND_SPEED => { + traffic_infos.ground_speed = if field.is_empty() { None } else { Some(f64::from_str(field).context("Failed to parse SBS MSG ground speed")? as i32) }; + }, + + SBS_FIELD_POS_TRACK => { + if field.is_empty() { + traffic_infos.track = None; + } + else { + let track = f64::from_str(field).context("Failed to parse SBS MSG track")?; + anyhow::ensure!((0.0..=360.0).contains(&track), "SBS MSG track out of range"); + traffic_infos.track = Some(track as u32); + } + }, + + SBS_FIELD_POS_VERTICAL_RATE => { + traffic_infos.vertical_speed = if field.is_empty() { None } else { Some(i32::from_str(field).context("Failed to parse SBS MSG vertical rate")?) }; + }, + + _ => () // Other fields are not used and are therefore considered valid + } + + Ok(()) + } + +} \ No newline at end of file diff --git a/src/src_ogn.rs b/src/src_ogn.rs new file mode 100644 index 0000000..0ef5750 --- /dev/null +++ b/src/src_ogn.rs @@ -0,0 +1,138 @@ +use crate::{internal_com, traffic_infos::{AddressType, TrafficInfos}}; + +use quick_xml::{events::Event, Reader}; +use std::{thread, time, str::FromStr}; + + +pub struct SrcOgn { + sender: internal_com::Sender, +} + +impl SrcOgn { + /// Starts reception of OGN traffic + pub fn start_receive() { + thread::spawn(|| { + Self::work_thread(); + }); + } + + + fn new() -> SrcOgn { + SrcOgn { + sender: internal_com::Sender::new(), + } + } + + + fn work_thread() { + let ogn = Self::new(); + loop { + if let Err(e) = ogn.get_and_send_positions() { + log::warn!("{:?}", e); + } + thread::sleep(time::Duration::from_secs(5)); + } + } + + + fn get_and_send_positions(&self) -> anyhow::Result<()> { + let ogn_string = Self::get_ogn_string()?; + self.parse_ogn_string(&ogn_string)?; + Ok(()) + } + + + fn get_ogn_string() -> anyhow::Result { + // We retrieve traffic information on France + let ogn_string = ureq::get("https://live.glidernet.org/lxml.php?\ + a=0\ + &b=51.3\ + &c=42.1\ + &d=8.4\ + &e=-5.1") + .call()? + .into_string()?; + Ok(ogn_string) + } + + + fn parse_ogn_string(&self, ogn_string: &str) -> anyhow::Result<()> { + // Parse the XML string with quick-xml + let mut reader = Reader::from_str(ogn_string); + loop { + match reader.read_event()? { + Event::Empty(element) => { + // OGN traffic is contained in empty XML elements with name "m" + if element.local_name().as_ref() == b"m" { + // Browse element attributes + for attribute in element.attributes() { + match attribute { + Err(e) => return Err(anyhow::anyhow!("Incorrect attribute : {}", e)), + Ok(attr) => { + // The attribute containing the traffic information is "a" + if attr.key.local_name().as_ref() == b"a" { + // We recover its value + let traffic_string = &(attr.unescape_value()?); + + // Analysis of the traffic chain + let traffic_infos = Self::parse_traffic(traffic_string)?; + //println!("{:?}", traffic_infos); + + // Sending traffic information to clients + self.sender.send(&traffic_infos); + } + } + } + } + } + }, + Event::Eof => break, // End of the XML chain, we exit the loop + _ => () // Other events do not interest us + } + } + + Ok(()) + } + + + fn parse_traffic(traffic_string: &str) -> anyhow::Result { + let mut traffic_infos = TrafficInfos::default(); + + // Breaking down and parsing each field in the traffic chain + let traffic_fields = traffic_string.split(','); + for (i, traffic_field) in traffic_fields.enumerate() { + match i { + 0 => traffic_infos.latitude = f64::from_str(traffic_field)?, + 1 => traffic_infos.longitude = f64::from_str(traffic_field)?, + 2 => traffic_infos.callsign = traffic_field.to_string(), + 4 => traffic_infos.altitude = Self::meter_to_feet(i32::from_str(traffic_field)?), + 7 => traffic_infos.track = Some(u32::from_str(traffic_field)?), + 8 => traffic_infos.ground_speed = Some(Self::kmh_to_kt(i32::from_str(traffic_field)?)), + 9 => traffic_infos.vertical_speed = Some(Self::mps_to_fpm(f64::from_str(traffic_field)?)), + 13 => { + let address = u32::from_str_radix(traffic_field, 16)?; + traffic_infos.address = address & 0x00ff_ffff; // We only keep the 24 least significant bits + traffic_infos.addr_type = AddressType::Ogn; + }, + _ => () // The other fields are not used and are considered valid + } + } + Ok(traffic_infos) + } + + + fn meter_to_feet(meter: i32) -> i32 { + (f64::from(meter) * 3.28084) as i32 + } + + + fn kmh_to_kt(kmh: i32) -> i32 { + (f64::from(kmh) * 0.539_957) as i32 + } + + + fn mps_to_fpm(mps: f64) -> i32{ + (mps * 196.850_394) as i32 + } + +} \ No newline at end of file diff --git a/src/traffic_infos.rs b/src/traffic_infos.rs new file mode 100644 index 0000000..76c6a3e --- /dev/null +++ b/src/traffic_infos.rs @@ -0,0 +1,23 @@ +use serde::{Serialize, Deserialize}; + +/// Address type as defined by GDL90, “Address Type” field +#[derive(Default, Debug, Serialize, Deserialize)] +pub enum AddressType { + #[default] + AdsbIcao, + Ogn +} + +/// Information regarding traffic +#[derive(Default, Debug, Serialize, Deserialize)] +pub struct TrafficInfos { + pub addr_type: AddressType, + pub address: u32, // on 24 bits + pub callsign: String, + pub altitude: i32, // in ft with QNH of 1013 hPa + pub latitude: f64, // in degrees + pub longitude: f64, // in degrees + pub track: Option, // in degrees + pub ground_speed: Option, // in kt + pub vertical_speed: Option, // in fpm +} diff --git a/tests/clients_sim.py b/tests/clients_sim.py new file mode 100644 index 0000000..a0738c4 --- /dev/null +++ b/tests/clients_sim.py @@ -0,0 +1,50 @@ +import socket +import threading +import argparse +import time + + +def create_connection(id, target_ip, target_port): + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect((target_ip, target_port)) + print(f"Id {id}: connected to {target_ip}:{target_port}") + + # Send a position message: (lat = 46.344877, long = -1.466214) + pos_msg = b"\x00\x08\x02\xc3\x2a\xad\xff\xe9\xa0\x9a" + sock.sendall(pos_msg) + + # Receive aircraft positions until peer closes the connection + while True: + data = sock.recv(1024) + if not data: + break + + sock.close() + print(f"Id {id}: disconnected to {target_ip}:{target_port}") + + except Exception as e: + print(f"Id {id}: failed to connect to {target_ip}:{target_port} - {e}") + + +def main(): + parser = argparse.ArgumentParser(description="Client connection simulator.") + parser.add_argument("--ip", type=str, required=True, help="Target address") + parser.add_argument("--port", type=int, default=1664, help="Target port") + parser.add_argument("--connections", type=int, default=10, help="Number of connections to create") + args = parser.parse_args() + + threads = [] + for i in range(args.connections): + thread = threading.Thread(target=create_connection, args=(i, args.ip, args.port)) + threads.append(thread) + thread.start() + if i % 10 == 0: + time.sleep(0.2) + + for thread in threads: + thread.join() + + +if __name__ == "__main__": + main() \ No newline at end of file