diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 5c325abd..e7bfecad 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -41,8 +41,8 @@ jobs: run: cargo test -p dicom-pixeldata --no-default-features # test dicom-ul with async feature - if: matrix.rust == 'stable' || matrix.rust == 'beta' - run: cargo test -p dicom-ul --features async - # test/check library projects with minimum rust version + run: cargo test -p dicom-ul --features async-tls + # test library projects with minimum rust version - if: matrix.rust == '1.72.0' run: | cargo test \ diff --git a/.gitignore b/.gitignore index 48a4f8fd..c3f53d8d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,10 @@ target/ *.rs.bk .idea/ -.marscode/ \ No newline at end of file +.marscode/ +*.crt +*.key +*.csr +*.srl +*.pem +certs/ diff --git a/Cargo.lock b/Cargo.lock index 5d30afc4..ebf97f55 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,15 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "addr2line" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" -dependencies = [ - "gimli", -] - [[package]] name = "adler2" version = "2.0.1" @@ -26,15 +17,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "aligned-vec" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" -dependencies = [ - "equator", -] - [[package]] name = "android_system_properties" version = "0.1.5" @@ -100,29 +82,6 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" -[[package]] -name = "arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" - -[[package]] -name = "arg_enum_proc_macro" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - [[package]] name = "autocfg" version = "1.5.0" @@ -130,41 +89,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] -name = "av1-grain" -version = "0.2.4" +name = "aws-lc-rs" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f3efb2ca85bc610acfa917b5aaa36f3fcbebed5b3182d7f877b02531c4b80c8" +checksum = "879b6c89592deb404ba4dc0ae6b58ffd1795c78991cbb5b8bc441c48a070440d" dependencies = [ - "anyhow", - "arrayvec", - "log", - "nom", - "num-rational", - "v_frame", -] - -[[package]] -name = "avif-serialize" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47c8fbc0f831f4519fe8b810b6a7a91410ec83031b8233f730a0480029f6a23f" -dependencies = [ - "arrayvec", + "aws-lc-sys", + "zeroize", ] [[package]] -name = "backtrace" -version = "0.3.76" +name = "aws-lc-sys" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +checksum = "107a4e9d9cab9963e04e84bb8dee0e25f2a987f9a8bad5ed054abd439caa8f8c" dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-link", + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", ] [[package]] @@ -173,6 +117,26 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.9.4", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + [[package]] name = "bit_field" version = "0.10.3" @@ -191,12 +155,6 @@ version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" -[[package]] -name = "bitstream-io" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" - [[package]] name = "block-buffer" version = "0.10.4" @@ -207,10 +165,24 @@ dependencies = [ ] [[package]] -name = "built" -version = "0.7.7" +name = "bpaf" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "473976d7a8620bb1e06dcdd184407c2363fe4fec8e983ee03ed9197222634a31" +dependencies = [ + "bpaf_derive", +] + +[[package]] +name = "bpaf_derive" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" +checksum = "fefb4feeec9a091705938922f26081aad77c64cd2e76cd1c4a9ece8e42e1618a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "bumpalo" @@ -253,9 +225,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.40" +version = "1.2.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d05d92f4b1fd76aad469d46cdd858ca761576082cd37df81416691e50199fb" +checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" dependencies = [ "find-msvc-tools", "jobserver", @@ -264,20 +236,19 @@ dependencies = [ ] [[package]] -name = "cfg-expr" -version = "0.15.8" +name = "cexpr" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ - "smallvec", - "target-lexicon", + "nom", ] [[package]] name = "cfg-if" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "charls" @@ -309,11 +280,22 @@ dependencies = [ "windows-link", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" -version = "4.5.48" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" +checksum = "f4512b90fa68d3a9932cea5184017c5d200f5921df706d45e853537dea51508f" dependencies = [ "clap_builder", "clap_derive", @@ -321,21 +303,22 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.48" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" +checksum = "0025e98baa12e766c67ba13ff4695a887a1eba19569aad00a472546795bd6730" dependencies = [ "anstream", "anstyle", "clap_lex", "strsim", + "terminal_size", ] [[package]] name = "clap_derive" -version = "4.5.47" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ "heck", "proc-macro2", @@ -345,9 +328,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "cmake" @@ -377,6 +360,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -442,6 +435,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "deranged" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" +dependencies = [ + "powerfmt", +] + [[package]] name = "dicom" version = "0.9.0" @@ -457,6 +459,17 @@ dependencies = [ "dicom-ul", ] +[[package]] +name = "dicom-app-common" +version = "0.1.0" +dependencies = [ + "clap", + "rustls", + "rustls-native-certs", + "snafu", + "tracing", +] + [[package]] name = "dicom-core" version = "0.9.0" @@ -631,6 +644,7 @@ dependencies = [ "ndarray", "num-traits", "rayon", + "rayon-core", "rstest", "snafu", "tracing", @@ -654,6 +668,7 @@ name = "dicom-storescp" version = "0.9.0" dependencies = [ "clap", + "dicom-app-common", "dicom-core", "dicom-dictionary-std", "dicom-encoding", @@ -671,6 +686,7 @@ name = "dicom-storescu" version = "0.9.0" dependencies = [ "clap", + "dicom-app-common", "dicom-core", "dicom-dictionary-std", "dicom-dump", @@ -680,8 +696,10 @@ dependencies = [ "dicom-transfer-syntax-registry", "dicom-ul", "indicatif", + "rustls", "snafu", "tokio", + "tokio-rustls", "tracing", "tracing-subscriber", "walkdir", @@ -740,15 +758,21 @@ version = "0.9.0" dependencies = [ "byteordered", "bytes", + "cfg-if", "dicom-core", "dicom-dictionary-std", "dicom-encoding", "dicom-object", "dicom-transfer-syntax-registry", "matches", + "rcgen", "rstest", + "rustls", + "rustls-cert-gen", "snafu", + "time", "tokio", + "tokio-rustls", "tracing", ] @@ -768,6 +792,12 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "either" version = "1.15.0" @@ -844,26 +874,6 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" -[[package]] -name = "equator" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" -dependencies = [ - "equator-macro", -] - -[[package]] -name = "equator-macro" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -877,7 +887,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -922,15 +932,15 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" [[package]] name = "flate2" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" dependencies = [ "crc32fast", "miniz_oxide", @@ -942,6 +952,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures-core" version = "0.3.31" @@ -1001,9 +1017,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.7" +version = "0.14.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" dependencies = [ "typenum", "version_check", @@ -1017,27 +1033,21 @@ checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.7+wasi-0.2.4", + "wasip2", ] -[[package]] -name = "gimli" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" - [[package]] name = "glob" version = "0.3.3" @@ -1046,12 +1056,13 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "half" -version = "2.6.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "cfg-if", "crunchy", + "zerocopy", ] [[package]] @@ -1115,9 +1126,9 @@ dependencies = [ [[package]] name = "image" -version = "0.25.6" +version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" +checksum = "bc144d44a31d753b02ce64093d532f55ff8dc4ebf2ffb8a63c0dda691385acae" dependencies = [ "bytemuck", "byteorder-lite", @@ -1125,7 +1136,6 @@ dependencies = [ "image-webp", "num-traits", "png", - "ravif", "rayon", "tiff", "zune-core", @@ -1142,12 +1152,6 @@ dependencies = [ "quick-error 2.0.1", ] -[[package]] -name = "imgref" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" - [[package]] name = "indenter" version = "0.3.4" @@ -1177,17 +1181,6 @@ dependencies = [ "web-time", ] -[[package]] -name = "interpolate_name" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "inventory" version = "0.3.21" @@ -1197,17 +1190,6 @@ dependencies = [ "rustversion", ] -[[package]] -name = "io-uring" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" -dependencies = [ - "bitflags 2.9.4", - "cfg-if", - "libc", -] - [[package]] name = "is-terminal" version = "0.4.16" @@ -1233,9 +1215,9 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itertools" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] @@ -1261,7 +1243,7 @@ version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "libc", ] @@ -1474,18 +1456,18 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" [[package]] name = "libc" -version = "0.2.176" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] -name = "libfuzzer-sys" -version = "0.4.10" +name = "libloading" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ - "arbitrary", - "cc", + "cfg-if", + "windows-link", ] [[package]] @@ -1510,12 +1492,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] -name = "loop9" -version = "0.1.5" +name = "matchers" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "imgref", + "regex-automata", ] [[package]] @@ -1534,16 +1516,6 @@ dependencies = [ "rawpointer", ] -[[package]] -name = "maybe-rayon" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" -dependencies = [ - "cfg-if", - "rayon", -] - [[package]] name = "memchr" version = "2.7.6" @@ -1568,13 +1540,13 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" dependencies = [ "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "wasi", + "windows-sys 0.61.2", ] [[package]] @@ -1592,12 +1564,6 @@ dependencies = [ "rawpointer", ] -[[package]] -name = "new_debug_unreachable" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" - [[package]] name = "nom" version = "7.1.3" @@ -1608,29 +1574,13 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "noop_proc_macro" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" - [[package]] name = "nu-ansi-term" -version = "0.50.1" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.52.0", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", + "windows-sys 0.61.2", ] [[package]] @@ -1643,15 +1593,10 @@ dependencies = [ ] [[package]] -name = "num-derive" -version = "0.4.2" +name = "num-conv" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "num-integer" @@ -1662,17 +1607,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-rational" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -1688,15 +1622,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] - [[package]] name = "once_cell" version = "1.21.3" @@ -1732,6 +1657,12 @@ dependencies = [ "libc", ] +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + [[package]] name = "owo-colors" version = "4.2.3" @@ -1766,10 +1697,14 @@ dependencies = [ ] [[package]] -name = "paste" -version = "1.0.15" +name = "pem" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] [[package]] name = "percent-encoding" @@ -1830,13 +1765,10 @@ dependencies = [ ] [[package]] -name = "ppv-lite86" -version = "0.2.21" +name = "powerfmt" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "pretty_assertions" @@ -1848,13 +1780,23 @@ dependencies = [ "yansi", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit 0.23.6", + "toml_edit", ] [[package]] @@ -1866,25 +1808,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "profiling" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" -dependencies = [ - "profiling-procmacros", -] - -[[package]] -name = "profiling-procmacros" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" -dependencies = [ - "quote", - "syn", -] - [[package]] name = "quick-error" version = "1.2.3" @@ -1912,86 +1835,6 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.16", -] - -[[package]] -name = "rav1e" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" -dependencies = [ - "arbitrary", - "arg_enum_proc_macro", - "arrayvec", - "av1-grain", - "bitstream-io", - "built", - "cfg-if", - "interpolate_name", - "itertools 0.12.1", - "libc", - "libfuzzer-sys", - "log", - "maybe-rayon", - "new_debug_unreachable", - "noop_proc_macro", - "num-derive", - "num-traits", - "once_cell", - "paste", - "profiling", - "rand", - "rand_chacha", - "simd_helpers", - "system-deps", - "thiserror", - "v_frame", - "wasm-bindgen", -] - -[[package]] -name = "ravif" -version = "0.11.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5825c26fddd16ab9f515930d49028a630efec172e903483c94796cfe31893e6b" -dependencies = [ - "avif-serialize", - "imgref", - "loop9", - "quick-error 2.0.1", - "rav1e", - "rayon", - "rgb", -] - [[package]] name = "rawpointer" version = "0.2.1" @@ -2000,9 +1843,9 @@ checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" [[package]] name = "rayon" -version = "1.10.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051" dependencies = [ "either", "rayon-core", @@ -2018,20 +1861,33 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rcgen" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fae430c6b28f1ad601274e78b7dffa0546de0b73b4cd32f46723c0c2a16f7a5" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "yasna", +] + [[package]] name = "redox_syscall" -version = "0.5.17" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags 2.9.4", ] [[package]] name = "regex" -version = "1.11.3" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", @@ -2041,9 +1897,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.11" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", @@ -2052,9 +1908,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "relative-path" @@ -2062,12 +1918,6 @@ version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" -[[package]] -name = "rgb" -version = "0.8.52" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" - [[package]] name = "ring" version = "0.17.14" @@ -2112,10 +1962,10 @@ dependencies = [ ] [[package]] -name = "rustc-demangle" -version = "0.1.26" +name = "rustc-hash" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustc_version" @@ -2136,15 +1986,16 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.32" +version = "0.23.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" +checksum = "751e04a496ca00bb97a5e043158d23d66b5aabf2e1d5aa2a0aaebb1aafe6f82c" dependencies = [ + "aws-lc-rs", "log", "once_cell", "ring", @@ -2154,6 +2005,32 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-cert-gen" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0207d48722efec7effe77a631b6482bb8eaff6fb5d56ca334c32dfb59a046170" +dependencies = [ + "anyhow", + "bpaf", + "pem", + "rcgen", + "ring", + "rustls-pki-types", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pemfile" version = "2.2.0" @@ -2178,6 +2055,7 @@ version = "0.103.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -2210,12 +2088,44 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +dependencies = [ + "bitflags 2.9.4", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.27" @@ -2266,15 +2176,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - [[package]] name = "sha2" version = "0.10.9" @@ -2316,15 +2217,6 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" -[[package]] -name = "simd_helpers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" -dependencies = [ - "quote", -] - [[package]] name = "slab" version = "0.4.11" @@ -2360,12 +2252,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -2456,25 +2348,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "system-deps" -version = "6.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" -dependencies = [ - "cfg-expr", - "heck", - "pkg-config", - "toml", - "version-compare", -] - -[[package]] -name = "target-lexicon" -version = "0.12.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" - [[package]] name = "tempfile" version = "3.23.0" @@ -2482,10 +2355,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -2538,31 +2411,47 @@ dependencies = [ "weezl", ] +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + [[package]] name = "tokio" -version = "1.47.1" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "backtrace", "bytes", - "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", - "slab", "socket2", "tokio-macros", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", @@ -2570,65 +2459,41 @@ dependencies = [ ] [[package]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime 0.6.11", - "toml_edit 0.22.27", -] - -[[package]] -name = "toml_datetime" -version = "0.6.11" +name = "tokio-rustls" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "serde", + "rustls", + "tokio", ] [[package]] name = "toml_datetime" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime 0.6.11", - "winnow", -] - -[[package]] -name = "toml_edit" -version = "0.23.6" +version = "0.23.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" dependencies = [ "indexmap", - "toml_datetime 0.7.2", + "toml_datetime", "toml_parser", "winnow", ] [[package]] name = "toml_parser" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" dependencies = [ "winnow", ] @@ -2682,10 +2547,14 @@ version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ + "matchers", "nu-ansi-term", + "once_cell", + "regex-automata", "sharded-slab", "smallvec", "thread_local", + "tracing", "tracing-core", "tracing-log", ] @@ -2710,9 +2579,9 @@ checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-width" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "untrusted" @@ -2762,17 +2631,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "v_frame" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" -dependencies = [ - "aligned-vec", - "num-traits", - "wasm-bindgen", -] - [[package]] name = "valuable" version = "0.1.1" @@ -2785,12 +2643,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "version-compare" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" - [[package]] name = "version_check" version = "0.9.5" @@ -2813,15 +2665,6 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasi" -version = "0.14.7+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" -dependencies = [ - "wasip2", -] - [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" @@ -2902,9 +2745,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8" dependencies = [ "rustls-pki-types", ] @@ -2921,14 +2764,14 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] name = "windows-core" -version = "0.62.1" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6844ee5416b285084d3d3fffd743b925a6c9385455f64f6d4fa3031c4c2749a9" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", @@ -2939,9 +2782,9 @@ dependencies = [ [[package]] name = "windows-implement" -version = "0.60.1" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb307e42a74fb6de9bf3a02d9712678b22399c87e6fa869d6dfcd8c1b7754e0" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", @@ -2950,9 +2793,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.59.2" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0abd1ddbc6964ac14db11c7213d6532ef34bd9aa042c2e5935f59d7908b46a5" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", @@ -2961,24 +2804,24 @@ dependencies = [ [[package]] name = "windows-link" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-result" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ "windows-link", ] @@ -3007,14 +2850,14 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.4", + "windows-targets 0.53.5", ] [[package]] name = "windows-sys" -version = "0.61.1" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] @@ -3037,19 +2880,19 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.4" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -3060,9 +2903,9 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -3072,9 +2915,9 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -3084,9 +2927,9 @@ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" @@ -3096,9 +2939,9 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -3108,9 +2951,9 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -3120,9 +2963,9 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -3132,9 +2975,9 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -3144,9 +2987,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" @@ -3169,6 +3012,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + [[package]] name = "zerocopy" version = "0.8.27" diff --git a/Cargo.toml b/Cargo.toml index b5af76a0..87add00f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,9 @@ members = [ "storescp", "storescu", "toimage", + "transfer-syntax-registry", + "ul", + "app-common", ] # use edition 2021 resolver diff --git a/app-common/Cargo.toml b/app-common/Cargo.toml new file mode 100644 index 00000000..83bee32a --- /dev/null +++ b/app-common/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "dicom-app-common" +description = "Helper library for command-line applications in the DICOM-rs project" +authors = ["Eduardo Pinho ", "Nate Richman "] +keywords = ["dicom", "cli"] +version = "0.1.0" +edition = "2021" + +[dependencies] +clap = { version = "4.5.47", features = ["derive", "wrap_help"] } +rustls = "0.23.31" +rustls-native-certs = "0.8.1" +snafu = "0.8.9" +tracing = "0.1.41" diff --git a/app-common/README.md b/app-common/README.md new file mode 100644 index 00000000..135b4848 --- /dev/null +++ b/app-common/README.md @@ -0,0 +1,6 @@ +# DICOM-rs `app-common` + +[![crates.io](https://img.shields.io/crates/v/dicom-app-common.svg)](https://crates.io/crates/dicom-app-common) +[![Documentation](https://docs.rs/dicom-app-common/badge.svg)](https://docs.rs/dicom-app-common) + +This is a helper library for command-line applications in the [DICOM-rs](https://github.com/Enet4/dicom-rs) project. \ No newline at end of file diff --git a/app-common/src/lib.rs b/app-common/src/lib.rs new file mode 100644 index 00000000..416c8098 --- /dev/null +++ b/app-common/src/lib.rs @@ -0,0 +1,266 @@ +use clap::Args; +use rustls::{ClientConfig, ServerConfig, SupportedProtocolVersion, pki_types::{CertificateDer, CertificateRevocationListDer, PrivateKeyDer, pem::PemObject}, server::WebPkiClientVerifier}; +use tracing::{debug, info}; +use std::{path::PathBuf, sync::Arc}; +use snafu::{Snafu, ResultExt}; + +#[derive(Snafu, Debug)] +pub enum MissingPemObject { + #[snafu(display("Missing Certificate"))] + Certificate, + #[snafu(display("Missing Private Key"))] + PrivateKey, +} + +#[derive(Snafu, Debug)] +pub enum TlsError { + #[snafu(display("IO error"))] + Io { source: std::io::Error }, + #[snafu(display("PEM parse error in path: {}", path.as_ref().map(|p| p.display().to_string()).unwrap_or("unknown".into())))] + PemParse { + source: rustls::pki_types::pem::Error, + path: Option, + }, + #[snafu(display("Rustls error"))] + Rustls { source: rustls::Error }, + + #[snafu(display("Certificate verifier error"))] + CertificateVerifier { source: rustls::client::VerifierBuilderError}, + + #[snafu(display("Config error: {missing}"))] + Config { missing: MissingPemObject }, +} + + +#[derive(Args, Debug)] +pub struct TlsOptions { + /// Enables mTLS (TLS for DICOM connections) + #[arg(long = "tls", default_value = "false")] + pub enabled: bool, + + /// Crypto provider to use, see documentation (https://docs.rs/rustls/latest/rustls/index.html) for details + #[arg(long, value_enum, default_value_t = CryptoProvider::AwsLC, value_name = "provider")] + pub crypto_provider: CryptoProvider, + + /// List of cipher suites to use. If not specified, the default cipher suites for the selected crypto provider will be used. + #[arg(long, value_name = "cipher1,...")] + pub cipher_suites: Option>, + + /// TLS protocol versions to enable + #[arg(long, value_enum, value_name = "version,...", default_values_t = vec![TLSProtocolVersion::TLS1_2, TLSProtocolVersion::TLS1_3])] + pub protocol_versions: Vec, + + /// Path to private key file in PEM format + #[arg(long, value_name = "/path/to/key.pem,...")] + pub key: Option, + + /// Path to certificate file in PEM format + #[arg(long, value_name = "/path/to/cert.pem,...")] + pub cert: Option, + + /// Path to additional CA certificates (comma separated) in PEM format to add to the root store + #[arg(long, value_name = "/path/to/cert.pem,...")] + pub add_certs: Option>, + + /// Add Certificate Revocation Lists (CRLs) to the server's certificate verifier + #[arg(long, value_name = "/path/to/crl.pem,...")] + pub add_crls: Option>, + + /// Load certitificates from the system root store + #[arg(long, action = clap::ArgAction::SetFalse)] + pub system_roots: bool, + + /// How to handle peer certificates + #[arg(long, value_enum, value_name = "opt", default_value_t = PeerCertOption::Require)] + pub peer_cert: PeerCertOption, + +} + +#[derive(Args, Debug)] +pub struct TlsAcceptorOptions { + /// Allow unauthenticated clients (only valid for server) + #[arg(long)] + pub allow_unauthenticated: bool, +} + +/// Crypto provider options +/// +/// See rustls +/// [Cryptograpy providers](https://docs.rs/rustls/latest/rustls/#cryptography-providers) +/// for more details +/// +/// Currently only AWS-LC is supported +#[non_exhaustive] +#[derive(clap::ValueEnum, Clone, Debug)] +pub enum CryptoProvider { + AwsLC, + //RING +} + +/// TLS protocol version options +/// +/// Subset of rustls +/// [ProtocolVersions](https://docs.rs/rustls/latest/rustls/enum.ProtocolVersion.html#variants) +/// supported +#[derive(clap::ValueEnum, Clone, Debug)] +pub enum TLSProtocolVersion { + TLS1_2, + TLS1_3, +} + +/// Peer certificate handling options +/// +/// Defines how the TLS connection should handle peer certificates +#[derive(clap::ValueEnum, Clone, Debug)] +pub enum PeerCertOption { + /// Require the peer to present a valid certificate + Require, + /// Do not verify the peer certificate + Ignore, +} + +/// Show the supported cipher suites for the default crypto provider +pub fn show_cipher_suites(){ + let provider = rustls::crypto::CryptoProvider::get_default().expect("No default crypto provider found"); + println!("Supported cipher suites: "); + for suite in &provider.cipher_suites { + println!("{:?}", suite.suite()); + } +} + +impl TlsOptions{ + /// Build a root cert store from system roots and any additional certs + fn root_cert_store(&self) -> Result { + let mut root_store = rustls::RootCertStore::empty(); + // Load system roots unless disabled + if self.system_roots{ + let system_roots = rustls_native_certs::load_native_certs(); + root_store.add_parsable_certificates(system_roots.certs); + } + // Add any extra certs + if let Some(certs) = &self.add_certs{ + let mut loaded_certs = Vec::new(); + for path in certs { + let cert = CertificateDer::from_pem_file(path) + .with_context(|_| PemParseSnafu{path: path.clone()})?; + loaded_certs.push(cert); + } + root_store.add_parsable_certificates(loaded_certs); + } + Ok(root_store) + } + + /// Load client certs if provided + fn certs(&self) -> Result>>, TlsError> { + // If a certificate is provided, load it as a cert chain + match self.cert.as_ref() { + Some(path) => { + let certs = CertificateDer::pem_file_iter(path) + .with_context(|_| PemParseSnafu{path: path.clone()})? + .collect::, _>>() + .with_context(|_| PemParseSnafu{path: path.clone()})?; + Ok(Some(certs)) + } + None => Ok(None), + } + } + + /// Load CRLs if provided + fn crls(&self) -> Result>>, TlsError> { + match self.add_crls.as_ref() { + Some(crls) => { + let mut loaded_crls = Vec::new(); + for path in crls { + let crl = CertificateRevocationListDer::from_pem_file(path) + .with_context(|_| PemParseSnafu{path: path.clone()})?; + loaded_crls.push(crl); + } + Ok(Some(loaded_crls)) + } + None => Ok(None), + } + } + + /// Map selected protocol versions to rustls types + fn protocol_versions(&self) -> Vec<&'static SupportedProtocolVersion> { + self.protocol_versions.iter().map(|v| match v { + TLSProtocolVersion::TLS1_2 => &rustls::version::TLS12, + TLSProtocolVersion::TLS1_3 => &rustls::version::TLS13 + }).collect() + } + + /// Consume the options to create a client config + pub fn client_config(&self) -> Result { + debug!("Building client config with options: {:?}", self); + // Get the crypto provider + let provider = match self.crypto_provider { + CryptoProvider::AwsLC => rustls::crypto::aws_lc_rs::default_provider(), + }; + let builder = ClientConfig::builder_with_provider(provider.into()) + .with_protocol_versions(self.protocol_versions().as_slice()) + .context(RustlsSnafu)? + .with_root_certificates(self.root_cert_store()?); + match (self.certs()?, &self.key) { + (Some(certs), Some(key)) => { + info!("Using client certificate authentication"); + let key = PrivateKeyDer::from_pem_file(key) + .with_context(|_| PemParseSnafu{path: key.clone()})?; + let config = builder.with_client_auth_cert(certs, key) + .context(RustlsSnafu)?; + Ok(config) + } + (Some(_), None) => { + ConfigSnafu{ missing: MissingPemObject::PrivateKey }.fail() + } + (None, _) => { + let config = builder.with_no_client_auth(); + info!("Using client without certificate authentication"); + Ok(config) + } + } + } + + /// Consume the options to create a server config + pub fn server_config(&self, acceptor_options: &TlsAcceptorOptions) -> Result { + // Get the crypto provider + let provider = match self.crypto_provider { + CryptoProvider::AwsLC => Arc::new(rustls::crypto::aws_lc_rs::default_provider()), + }; + let builder = ServerConfig::builder_with_provider(provider.clone()) + .with_protocol_versions(self.protocol_versions().as_slice()) + .context(RustlsSnafu)?; + let builder = if let PeerCertOption::Ignore = self.peer_cert { + builder.with_no_client_auth() + } else { + let mut cert_verifier = WebPkiClientVerifier::builder_with_provider( + self.root_cert_store()?.into(), provider + ); + if let Some(crl_paths) = self.crls()? { + cert_verifier = cert_verifier.with_crls(crl_paths); + } + if acceptor_options.allow_unauthenticated { + info!("Allowing unauthenticated clients"); + cert_verifier = cert_verifier.allow_unauthenticated(); + } + let cert_verifier = cert_verifier.build() + .context(CertificateVerifierSnafu)?; + builder.with_client_cert_verifier(cert_verifier) + }; + match (self.certs()?, &self.key) { + (Some(certs), Some(key)) => { + let key = PrivateKeyDer::from_pem_file(key) + .with_context(|_| PemParseSnafu{path: key.clone()})?; + let config = builder.with_single_cert(certs, key) + .context(RustlsSnafu)?; + Ok(config) + } + (Some(_), None) => { + ConfigSnafu{ missing: MissingPemObject::PrivateKey }.fail() + } + (None, _) => { + ConfigSnafu{ missing: MissingPemObject::Certificate }.fail() + } + } + } + +} diff --git a/echoscu/src/main.rs b/echoscu/src/main.rs index 4685a1b0..09303f29 100644 --- a/echoscu/src/main.rs +++ b/echoscu/src/main.rs @@ -3,7 +3,7 @@ use dicom_core::{dicom_value, DataElement, VR}; use dicom_dictionary_std::{tags, uids}; use dicom_object::{mem::InMemDicomObject, StandardDataDictionary}; use dicom_ul::{ - association::client::ClientAssociationOptions, + association::{Association, SyncAssociation, client::ClientAssociationOptions}, pdu::{self, PDataValueType, Pdu}, }; use pdu::PDataValue; diff --git a/findscu/src/main.rs b/findscu/src/main.rs index d43b7537..452e8d82 100644 --- a/findscu/src/main.rs +++ b/findscu/src/main.rs @@ -6,6 +6,7 @@ use dicom_dump::DumpOptions; use dicom_encoding::transfer_syntax; use dicom_object::{mem::InMemDicomObject, open_file, StandardDataDictionary}; use dicom_transfer_syntax_registry::{entries, TransferSyntaxRegistry}; +use dicom_ul::association::{Association, SyncAssociation}; use dicom_ul::pdu::Pdu; use dicom_ul::{ association::ClientAssociationOptions, diff --git a/pixeldata/Cargo.toml b/pixeldata/Cargo.toml index e242e066..3a8dc225 100644 --- a/pixeldata/Cargo.toml +++ b/pixeldata/Cargo.toml @@ -25,13 +25,14 @@ dicom-dictionary-std = { path = "../dictionary-std", version = "0.9" } snafu = "0.8" byteorder = "1.4.3" gdcm-rs = { version = "0.6", optional = true } -rayon = { version = "1.5.0", optional = true } +rayon = { version = ">= 1.5, < 1.11.0", optional = true } +rayon-core = { version = "~1, < 1.13.0", optional = true } ndarray = { version = "0.16.1", optional = true } num-traits = "0.2.12" tracing = "0.1.34" [dependencies.image] -version = "0.25.1" +version = ">=0.25.1, <0.25.8" default-features = false features = ["jpeg", "png", "pnm", "tiff", "webp", "bmp", "exr"] optional = true diff --git a/storescp/Cargo.toml b/storescp/Cargo.toml index 75458d27..dfff1cef 100644 --- a/storescp/Cargo.toml +++ b/storescp/Cargo.toml @@ -13,13 +13,14 @@ readme = "README.md" [dependencies] clap = { version = "4.0.18", features = ["derive"] } dicom-core = { path = '../core', version = "0.9" } -dicom-ul = { path = '../ul', version = "0.9", features = ["async"] } +dicom-ul = { path = '../ul', version = "0.9", features = ["async-tls"] } dicom-object = { path = '../object', version = "0.9" } dicom-encoding = { path = "../encoding/", version = "0.9" } dicom-dictionary-std = { path = "../dictionary-std/", version = "0.9" } dicom-transfer-syntax-registry = { path = "../transfer-syntax-registry/", version = "0.9" } snafu = "0.8" tracing = "0.1.36" -tracing-subscriber = "0.3.15" +tracing-subscriber = { version = "0.3.15", features = ["env-filter"] } tokio = { version = "1.38.0", features = ["full"] } +dicom-app-common = { version = "0.1.0", path = "../app-common" } diff --git a/storescp/src/main.rs b/storescp/src/main.rs index 5118c073..bf66bec4 100644 --- a/storescp/src/main.rs +++ b/storescp/src/main.rs @@ -1,13 +1,13 @@ use std::{ - net::{Ipv4Addr, SocketAddrV4}, - path::PathBuf, + net::{Ipv4Addr, SocketAddrV4}, path::PathBuf }; +use dicom_app_common::{TlsOptions, TlsAcceptorOptions}; use clap::Parser; use dicom_core::{dicom_value, DataElement, VR}; use dicom_dictionary_std::tags; use dicom_object::{InMemDicomObject, StandardDataDictionary}; -use snafu::Report; +use snafu::{Report, ResultExt, Whatever}; use tracing::{error, info, Level}; mod store_async; @@ -15,6 +15,7 @@ mod store_sync; mod transfer; use store_async::run_store_async; use store_sync::run_store_sync; +use tracing_subscriber::EnvFilter; /// DICOM C-STORE SCP #[derive(Debug, Parser)] @@ -52,6 +53,11 @@ struct App { /// Run in non-blocking mode (spins up an async task to handle each incoming stream) #[arg(short, long)] non_blocking: bool, + /// TLS options + #[command(flatten, next_help_heading = "TLS Options")] + tls: TlsOptions, + #[command(flatten)] + tls_acceptor: TlsAcceptorOptions, } fn create_cstore_response( @@ -104,6 +110,20 @@ fn create_cecho_response(message_id: u16) -> InMemDicomObject Result<(), Box> { use std::sync::Arc; let args = Arc::new(args); - tracing::subscriber::set_global_default( - tracing_subscriber::FmtSubscriber::builder() - .with_max_level(if args.verbose { - Level::DEBUG - } else { - Level::INFO - }) - .finish(), - ) - .unwrap_or_else(|e| { - eprintln!( - "Could not set up global logger: {}", - snafu::Report::from_error(e) - ); - }); - std::fs::create_dir_all(&args.out_dir).unwrap_or_else(|e| { error!("Could not create output directory: {}", e); std::process::exit(-2); @@ -166,22 +170,6 @@ async fn run_async(args: App) -> Result<(), Box> { } fn run_sync(args: App) -> Result<(), Box> { - tracing::subscriber::set_global_default( - tracing_subscriber::FmtSubscriber::builder() - .with_max_level(if args.verbose { - Level::DEBUG - } else { - Level::INFO - }) - .finish(), - ) - .unwrap_or_else(|e| { - eprintln!( - "Could not set up global logger: {}", - snafu::Report::from_error(e) - ); - }); - std::fs::create_dir_all(&args.out_dir).unwrap_or_else(|e| { error!("Could not create output directory: {}", e); std::process::exit(-2); diff --git a/storescp/src/store_async.rs b/storescp/src/store_async.rs index cb0a0195..d792c7f0 100644 --- a/storescp/src/store_async.rs +++ b/storescp/src/store_async.rs @@ -1,8 +1,10 @@ +use std::path::Path; + use dicom_dictionary_std::tags; use dicom_encoding::transfer_syntax::TransferSyntaxIndex; use dicom_object::{FileMetaTableBuilder, InMemDicomObject}; use dicom_transfer_syntax_registry::TransferSyntaxRegistry; -use dicom_ul::{Pdu, pdu::{PDataValueType, PresentationContextResultReason}}; +use dicom_ul::{Pdu, association::{Association, AsyncAssociation, AsyncServerAssociation}, pdu::{PDataValueType, PresentationContextResultReason}}; use snafu::{OptionExt, Report, ResultExt, Whatever}; use tracing::{debug, info, warn}; @@ -21,13 +23,10 @@ pub async fn run_store_async( out_dir, port: _, non_blocking: _, + tls, + tls_acceptor } = args; - let verbose = *verbose; - let mut instance_buffer: Vec = Vec::with_capacity(1024 * 1024); - let mut msgid = 1; - let mut sop_class_uid = "".to_string(); - let mut sop_instance_uid = "".to_string(); let mut options = dicom_ul::association::ServerAssociationOptions::new() .accept_any() @@ -51,29 +50,79 @@ pub async fn run_store_async( for uid in ABSTRACT_SYNTAXES { options = options.with_abstract_syntax(*uid); } - - let mut association = options - .establish_async(scu_stream) - .await - .whatever_context("could not establish association")?; - - info!("New association from {}", association.client_ae_title()); - if args.verbose { + let (peer_addr, peer_title) = if tls.enabled { + let config = tls.server_config(tls_acceptor).whatever_context("Could not create TLS config")?; + options = options.tls_config(config); + let peer_addr = scu_stream.peer_addr().ok(); + let association = options + .establish_tls_async(scu_stream) + .await + .whatever_context("could not establish association")?; + info!("New association from {}", association.peer_ae_title()); + if args.verbose { + debug!( + "> Presentation contexts: {:?}", + association.presentation_contexts() + ); + } + debug!( + "#accepted_presentation_contexts={}, acceptor_max_pdu_length={}, requestor_max_pdu_length={}", + association.presentation_contexts() + .iter() + .filter(|pc| pc.reason == PresentationContextResultReason::Acceptance) + .count(), + association.acceptor_max_pdu_length(), + association.requestor_max_pdu_length(), + ); + let peer_title = association.peer_ae_title().to_string(); + inner(association, *verbose, out_dir).await?; + (peer_addr, peer_title) + } else { + let peer_addr = scu_stream.peer_addr().ok(); + let association = options + .establish_async(scu_stream) + .await + .whatever_context("could not establish association")?; + info!("New association from {}", association.peer_ae_title()); + if args.verbose { + debug!( + "> Presentation contexts: {:?}", + association.presentation_contexts() + ); + } debug!( - "> Presentation contexts: {:?}", + "#accepted_presentation_contexts={}, acceptor_max_pdu_length={}, requestor_max_pdu_length={}", association.presentation_contexts() + .iter() + .filter(|pc| pc.reason == PresentationContextResultReason::Acceptance) + .count(), + association.acceptor_max_pdu_length(), + association.requestor_max_pdu_length(), ); + let peer_title = association.peer_ae_title().to_string(); + inner(association, *verbose, out_dir).await?; + (peer_addr, peer_title) + }; + + if let Some(peer_addr) = peer_addr { + info!( + "Dropping connection with {} ({})", + peer_title, + peer_addr + ); + } else { + info!("Dropping connection with {}", peer_title); } - debug!( - "#accepted_presentation_contexts={}, acceptor_max_pdu_length={}, requestor_max_pdu_length={}", - association.presentation_contexts() - .iter() - .filter(|pc| pc.reason == PresentationContextResultReason::Acceptance) - .count(), - association.acceptor_max_pdu_length(), - association.requestor_max_pdu_length(), - ); + Ok(()) + +} +async fn inner(mut association: AsyncServerAssociation, verbose: bool, out_dir: &Path) -> Result<(), Whatever> +where T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static{ + let mut instance_buffer: Vec = Vec::with_capacity(1024 * 1024); + let mut msgid = 1; + let mut sop_class_uid = "".to_string(); + let mut sop_instance_uid = "".to_string(); loop { match association.receive().await { Ok(mut pdu) => { @@ -194,7 +243,7 @@ pub async fn run_store_async( let file_obj = obj.with_exact_meta(file_meta); // write the files to the current directory with their SOPInstanceUID as filenames - let mut file_path = out_dir.clone(); + let mut file_path = out_dir.to_path_buf(); file_path.push( sop_instance_uid.trim_end_matches('\0').to_string() + ".dcm", ); @@ -244,7 +293,7 @@ pub async fn run_store_async( }); info!( "Released association with {}", - association.client_ae_title() + association.peer_ae_title() ); break; } @@ -269,16 +318,5 @@ pub async fn run_store_async( } } } - - if let Ok(peer_addr) = association.inner_stream().peer_addr() { - info!( - "Dropping connection with {} ({})", - association.client_ae_title(), - peer_addr - ); - } else { - info!("Dropping connection with {}", association.client_ae_title()); - } - Ok(()) } diff --git a/storescp/src/store_sync.rs b/storescp/src/store_sync.rs index 9f75452b..debe343d 100644 --- a/storescp/src/store_sync.rs +++ b/storescp/src/store_sync.rs @@ -1,10 +1,11 @@ use std::net::TcpStream; +use std::path::Path; use dicom_dictionary_std::tags; use dicom_encoding::transfer_syntax::TransferSyntaxIndex; use dicom_object::{FileMetaTableBuilder, InMemDicomObject}; use dicom_transfer_syntax_registry::TransferSyntaxRegistry; -use dicom_ul::{Pdu, pdu::{PDataValueType, PresentationContextResultReason}}; +use dicom_ul::{Pdu, ServerAssociation, association::{Association, CloseSocket, SyncAssociation}, pdu::{PDataValueType, PresentationContextResultReason}}; use snafu::{OptionExt, Report, ResultExt, Whatever}; use tracing::{debug, info, warn}; @@ -20,13 +21,10 @@ pub fn run_store_sync(scu_stream: TcpStream, args: &App) -> Result<(), Whatever> out_dir, port: _, non_blocking: _, - } = args; - let verbose = *verbose; + tls, + tls_acceptor, + } = &args; - let mut instance_buffer: Vec = Vec::with_capacity(1024 * 1024); - let mut msgid = 1; - let mut sop_class_uid = "".to_string(); - let mut sop_instance_uid = "".to_string(); let mut options = dicom_ul::association::ServerAssociationOptions::new() .accept_any() @@ -50,27 +48,80 @@ pub fn run_store_sync(scu_stream: TcpStream, args: &App) -> Result<(), Whatever> for uid in ABSTRACT_SYNTAXES { options = options.with_abstract_syntax(*uid); } - - let mut association = options - .establish(scu_stream) - .whatever_context("could not establish association")?; - - info!("New association from {}", association.client_ae_title()); - if args.verbose { + let (peer_addr, peer_title) = if tls.enabled { + let config = tls.server_config(tls_acceptor).whatever_context("Could not create TLS config")?; + options = options.tls_config(config); + let peer_addr = scu_stream.peer_addr().ok(); + let association = options + .establish_tls(scu_stream) + .whatever_context("could not establish association")?; + info!("New association from {}", association.peer_ae_title()); + info!("New association from {}", association.peer_ae_title()); + if args.verbose { + debug!( + "> Presentation contexts: {:?}", + association.presentation_contexts() + ); + } debug!( - "> Presentation contexts: {:?}", + "#accepted_presentation_contexts={}, acceptor_max_pdu_length={}, requestor_max_pdu_length={}", association.presentation_contexts() + .iter() + .filter(|pc| pc.reason == PresentationContextResultReason::Acceptance) + .count(), + association.acceptor_max_pdu_length(), + association.requestor_max_pdu_length(), ); + let peer_title = association.peer_ae_title().to_string(); + inner(association, *verbose, out_dir)?; + (peer_addr, peer_title) + } else { + let peer_addr = scu_stream.peer_addr().ok(); + let association = options + .establish(scu_stream) + .whatever_context("could not establish association")?; + info!("New association from {}", association.peer_ae_title()); + if args.verbose { + debug!( + "> Presentation contexts: {:?}", + association.presentation_contexts() + ); + } + debug!( + "#accepted_presentation_contexts={}, acceptor_max_pdu_length={}, requestor_max_pdu_length={}", + association.presentation_contexts() + .iter() + .filter(|pc| pc.reason == PresentationContextResultReason::Acceptance) + .count(), + association.acceptor_max_pdu_length(), + association.requestor_max_pdu_length(), + ); + let peer_title = association.peer_ae_title().to_string(); + inner(association, *verbose, out_dir)?; + (peer_addr, peer_title) + }; + + if let Some(peer_addr) = peer_addr { + info!( + "Dropping connection with {} ({})", + peer_title, + peer_addr + ); + } else { + info!("Dropping connection with {}", peer_title); } - debug!( - "#accepted_presentation_contexts={}, acceptor_max_pdu_length={}, requestor_max_pdu_length={}", - association.presentation_contexts() - .iter() - .filter(|pc| pc.reason == PresentationContextResultReason::Acceptance) - .count(), - association.acceptor_max_pdu_length(), - association.requestor_max_pdu_length(), - ); + Ok(()) + +} + +fn inner(mut association: ServerAssociation, verbose: bool, out_dir: &Path) -> Result<(), Whatever> +where + T: std::io::Read + std::io::Write + CloseSocket, +{ + let mut instance_buffer: Vec = Vec::with_capacity(1024 * 1024); + let mut msgid = 1; + let mut sop_class_uid = "".to_string(); + let mut sop_instance_uid = "".to_string(); loop { match association.receive() { @@ -192,7 +243,7 @@ pub fn run_store_sync(scu_stream: TcpStream, args: &App) -> Result<(), Whatever> let file_obj = obj.with_exact_meta(file_meta); // write the files to the current directory with their SOPInstanceUID as filenames - let mut file_path = out_dir.clone(); + let mut file_path = out_dir.to_path_buf(); file_path.push( sop_instance_uid.trim_end_matches('\0').to_string() + ".dcm", ); @@ -241,7 +292,7 @@ pub fn run_store_sync(scu_stream: TcpStream, args: &App) -> Result<(), Whatever> }); info!( "Released association with {}", - association.client_ae_title() + association.peer_ae_title() ); break; } @@ -266,16 +317,6 @@ pub fn run_store_sync(scu_stream: TcpStream, args: &App) -> Result<(), Whatever> } } } - - if let Ok(peer_addr) = association.inner_stream().peer_addr() { - info!( - "Dropping connection with {} ({})", - association.client_ae_title(), - peer_addr - ); - } else { - info!("Dropping connection with {}", association.client_ae_title()); - } - Ok(()) -} + +} \ No newline at end of file diff --git a/storescu/Cargo.toml b/storescu/Cargo.toml index 72c1e79d..99d452c8 100644 --- a/storescu/Cargo.toml +++ b/storescu/Cargo.toml @@ -24,12 +24,15 @@ dicom-encoding = { path = "../encoding/", version = "0.9" } dicom-object = { path = '../object', version = "0.9" } dicom-pixeldata = { path = "../pixeldata", version = "0.9", optional = true } dicom-transfer-syntax-registry = { path = "../transfer-syntax-registry/", version = "0.9" } -dicom-ul = { path = '../ul', version = "0.9", features = ["async"] } +dicom-ul = { path = '../ul', version = "0.9", features = ["async-tls"] } walkdir = "2.3.2" indicatif = "0.17.0" tracing = "0.1.34" -tracing-subscriber = "0.3.11" +tracing-subscriber = { version = "0.3.11", features = ["env-filter"] } snafu = "0.8" +dicom-app-common = { version = "0.1.0", path = "../app-common" } +rustls = "0.23.31" +tokio-rustls = "0.26.3" [dependencies.tokio] version = "1.38.0" diff --git a/storescu/README.md b/storescu/README.md index 925ed0a5..8470b2ce 100644 --- a/storescu/README.md +++ b/storescu/README.md @@ -16,35 +16,80 @@ for `storescu` tools in other DICOM software projects. ```none DICOM C-STORE SCU -USAGE: - dicom-storescu [FLAGS] [OPTIONS] [files]... - -FLAGS: - --fail-first fail if not all DICOM files can be transferred - -h, --help Prints help information - -V, --version Prints version information - -v, --verbose verbose mode - -OPTIONS: - --called-ae-title - the called Application Entity title, overrides AE title in address if present [default: ANY-SCP] - - --calling-ae-title the calling Application Entity title [default: STORE-SCU] - --max-pdu-length the maximum PDU length accepted by the SCU [default: 16384] - -m, --message-id the C-STORE message ID [default: 1] - --username user identity username - --password user identity password - --kerberos-service-ticket user identity Kerberos service ticket - --saml-assertion user identity SAML assertion - --jwt user identity JWT - -ARGS: - socket address to Store SCP, optionally with AE title (example: "STORE-SCP@127.0.0.1:104") - ... the DICOM file(s) to store +Usage: dicom-storescu [OPTIONS] ... + +Arguments: + socket address to Store SCP, optionally with AE title (example: "STORE-SCP@127.0.0.1:104") + ... the DICOM file(s) to store + +Options: + -v, --verbose verbose mode + --calling-ae-title the calling Application Entity title [default: STORE-SCU] + --called-ae-title the called Application Entity title, overrides AE title in address if present [default: ANY-SCP] + --max-pdu-length the maximum PDU length accepted by the SCU [default: 16384] + --fail-first fail if not all DICOM files can be transferred + --never-transcode fail file transfer if it cannot be done without transcoding + --username User Identity username + --password User Identity password + --kerberos-service-ticket User Identity Kerberos service ticket + --saml-assertion User Identity SAML assertion + --jwt User Identity JWT + -c, --concurrency Dispatch these many service users to send files in parallel + -h, --help Print help (see more with '--help') + -V, --version Print version + +TLS Options: + --tls Enables mTLS (TLS for DICOM connections) + --crypto-provider Crypto provider to use, see documentation (https://docs.rs/rustls/latest/rustls/index.html) for details [default: aws-lc] [possible values: + aws-lc] + --cipher-suites List of cipher suites to use. If not specified, the default cipher suites for the selected crypto provider will be used + --protocol-versions TLS protocol versions to enable [default: tls1-2 tls1-3] [possible values: tls1-2, tls1-3] + --key Path to private key file in PEM format + --cert Path to certificate file in PEM format + --add-certs Path to additional CA certificates (comma separated) in PEM format to add to the root store + --add-crls Add Certificate Revocation Lists (CRLs) to the server's certificate verifier + --system-roots Load certitificates from the system root store + --peer-cert How to handle peer certificates [default: require] [possible values: require, ignore] + --allow-unauthenticated Allow unauthenticated clients (only valid for server) +dd ``` -Example: +## Examples + +### Send two files to remote ```sh dicom-storescu MAIN-STORAGE@192.168.1.99:104 xray1.dcm xray2.dcm ``` + +### Use a TLS connection + +The following example assumes you have a TLS enabled dicom server running on the destination server. + +The destination will need to be configured for the dicom modality as well as the cert of this client. + +If the server has a self-signed cert, make sure the CA that signed it is passed in via `add-certs` + +```sh +dicom-storescu --tls \ + --calling-ae-title TLS-CLIENT \ + --cert /opt/client.pem \ + --key /opt/client.key.pem \ + --add-certs /opt/ca.pem \ + MAIN-STORAGE@192.168.1.99:104 xray1.dcm xray2.dcm +``` + +### Use an anonymous TLS connection + +The following example assumes you have a TLS enabled dicom server running on the destination server. + +The destination will need to be configured for the dicom modality and allow anonymous TLS + +If the server has a self-signed cert, make sure the CA that signed it is passed in via `add-certs` + +```sh +dicom-storescu --tls \ + --calling-ae-title TLS-CLIENT \ + --add-certs /opt/ca.pem \ + MAIN-STORAGE@192.168.1.99:104 xray1.dcm xray2.dcm +``` \ No newline at end of file diff --git a/storescu/out.json b/storescu/out.json deleted file mode 100644 index e69de29b..00000000 diff --git a/storescu/src/main.rs b/storescu/src/main.rs index d7a342d0..cc3cc5aa 100644 --- a/storescu/src/main.rs +++ b/storescu/src/main.rs @@ -5,19 +5,22 @@ use dicom_encoding::transfer_syntax; use dicom_encoding::TransferSyntax; use dicom_object::{mem::InMemDicomObject, DefaultDicomObject, StandardDataDictionary}; use dicom_transfer_syntax_registry::TransferSyntaxRegistry; -use dicom_ul::pdu::PresentationContextResultReason; +use dicom_ul::ClientAssociationOptions; use indicatif::{ProgressBar, ProgressStyle}; use snafu::prelude::*; use snafu::{Report, Whatever}; +use tracing::debug; use std::collections::HashSet; use std::ffi::OsStr; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::Duration; use tokio::sync::Mutex; -use tracing::{debug, error, info, warn, Level}; +use tracing::{error, info, warn, Level}; +use tracing_subscriber::filter::EnvFilter; use transfer_syntax::TransferSyntaxIndex; use walkdir::WalkDir; +use dicom_app_common::TlsOptions; mod store_async; mod store_sync; @@ -36,9 +39,6 @@ struct App { /// verbose mode #[arg(short = 'v', long = "verbose")] verbose: bool, - /// the C-STORE message ID - #[arg(short = 'm', long = "message-id", default_value = "1")] - message_id: u16, /// the calling Application Entity title #[arg(long = "calling-ae-title", default_value = "STORE-SCU")] calling_ae_title: String, @@ -102,6 +102,9 @@ struct App { /// Dispatch these many service users to send files in parallel #[arg(short = 'c', long = "concurrency")] concurrency: Option, + + #[command(flatten, next_help_heading = "TLS Options")] + tls: TlsOptions } #[derive(Debug)] @@ -172,10 +175,78 @@ enum Error { WriteIO { source: std::io::Error, }, + + #[snafu(display("TLS error: {}", source))] + Tls { + source: dicom_app_common::TlsError, + } +} + +#[allow(clippy::too_many_arguments)] +pub fn get_scu_options<'a>( + calling_ae_title: String, + called_ae_title: Option, + max_pdu_length: u32, + username: Option, + password: Option, + kerberos_service_ticket: Option, + saml_assertion: Option, + jwt: Option, + presentation_contexts: &'a HashSet<(String, String)>, + tls_options: rustls::ClientConfig, +) -> ClientAssociationOptions<'a> { + let mut scu_init = ClientAssociationOptions::new() + .calling_ae_title(calling_ae_title) + .max_pdu_length(max_pdu_length) + .server_name("localhost") + .tls_config(tls_options); + + for (storage_sop_class_uid, transfer_syntax) in presentation_contexts { + scu_init = scu_init.with_presentation_context(storage_sop_class_uid, vec![transfer_syntax]); + } + + if let Some(called_ae_title) = called_ae_title { + scu_init = scu_init.called_ae_title(called_ae_title); + } + + if let Some(username) = username { + scu_init = scu_init.username(username); + } + + if let Some(password) = password { + scu_init = scu_init.password(password); + } + + if let Some(kerberos_service_ticket) = kerberos_service_ticket { + scu_init = scu_init.kerberos_service_ticket(kerberos_service_ticket); + } + + if let Some(saml_assertion) = saml_assertion { + scu_init = scu_init.saml_assertion(saml_assertion); + } + + if let Some(jwt) = jwt { + scu_init = scu_init.jwt(jwt); + } + scu_init } fn main() { let app = App::parse(); + tracing::subscriber::set_global_default( + tracing_subscriber::FmtSubscriber::builder() + .with_max_level(Level::INFO) + .with_env_filter( + EnvFilter::from_default_env() + .add_directive("dicom_app_common=info".parse().unwrap()) + .add_directive(if app.verbose { "dicom_storescu=debug".parse().unwrap() } else { "dicom_storescu=info".parse().unwrap() }) + ) + .finish(), + ) + .whatever_context("Could not set up global logging subscriber") + .unwrap_or_else(|e: Whatever| { + eprintln!("[ERROR] {}", Report::from_error(e)); + }); match app.concurrency { Some(0) | None => { run(app).unwrap_or_else(|e| { @@ -263,12 +334,10 @@ fn check_files( } fn run(app: App) -> Result<(), Error> { - use crate::store_sync::{get_scu, send_file}; let App { addr, files, verbose, - message_id, calling_ae_title, called_ae_title, max_pdu_length, @@ -281,30 +350,23 @@ fn run(app: App) -> Result<(), Error> { saml_assertion, jwt, concurrency: _, + tls, } = app; // never transcode if the feature is disabled if cfg!(not(feature = "transcode")) { never_transcode = true; } - - tracing::subscriber::set_global_default( - tracing_subscriber::FmtSubscriber::builder() - .with_max_level(if verbose { Level::DEBUG } else { Level::INFO }) - .finish(), - ) - .whatever_context("Could not set up global logging subscriber") - .unwrap_or_else(|e: Whatever| { - eprintln!("[ERROR] {}", Report::from_error(e)); - }); + let tls_enabled = tls.enabled; + let config = tls.client_config() + .context(TlsSnafu)?; if verbose { info!("Establishing association with '{}'...", &addr); } - let (mut dicom_files, presentation_contexts) = check_files(files, verbose, never_transcode); + let (dicom_files, presentation_contexts) = check_files(files, verbose, never_transcode); - let mut scu = get_scu( - addr, + let scu_options = get_scu_options( calling_ae_title, called_ae_title, max_pdu_length, @@ -313,52 +375,9 @@ fn run(app: App) -> Result<(), Error> { kerberos_service_ticket, saml_assertion, jwt, - presentation_contexts, - )?; - - if verbose { - info!("Association established"); - debug!( - "#accepted_presentation_contexts={},requestor_max_pdu_length={}, acceptor_max_pdu_length={}", - scu.presentation_contexts() - .iter() - .filter(|pc| pc.reason == PresentationContextResultReason::Acceptance) - .count(), - scu.requestor_max_pdu_length(), - scu.acceptor_max_pdu_length(), - ); - debug!( - "Presentation contexts: {:?}", - scu.presentation_contexts() - ); - } - - for file in &mut dicom_files { - // identify the right transfer syntax to use - let r: Result<_, Error> = - check_presentation_contexts(file, scu.presentation_contexts(), ignore_sop_class, never_transcode); - match r { - Ok((pc, ts)) => { - if verbose { - debug!( - "{}: Selected presentation context: {:?}", - file.file.display(), - pc - ); - } - file.pc_selected = Some(pc); - file.ts_selected = Some(ts); - } - Err(e) => { - error!("{}", Report::from_error(e)); - if fail_first { - let _ = scu.abort(); - std::process::exit(-2); - } - } - } - } - + &presentation_contexts, + config + ); let progress_bar; if !verbose { progress_bar = Some(ProgressBar::new(dicom_files.len() as u64)); @@ -374,32 +393,23 @@ fn run(app: App) -> Result<(), Error> { progress_bar = None; } - for file in dicom_files { - scu = send_file( - scu, - file, - message_id, - progress_bar.as_ref(), - verbose, - fail_first, - )?; - } + if tls_enabled { + let scu = scu_options.establish_with_tls(&addr).map_err(Box::from).context(ScuSnafu)?; + store_sync::inner(scu, dicom_files, &progress_bar, fail_first, verbose, never_transcode, ignore_sop_class)?; - if let Some(pb) = progress_bar { - pb.finish_with_message("done") - }; - - scu.release().map_err(Box::from).context(ScuSnafu)?; + } else { + let scu = scu_options.establish(&addr).map_err(Box::from).context(ScuSnafu)?; + store_sync::inner(scu, dicom_files, &progress_bar, fail_first, verbose, never_transcode, ignore_sop_class)?; + } Ok(()) } + async fn run_async() -> Result<(), Error> { - use crate::store_async::{get_scu, send_file}; let App { addr, files, verbose, - message_id, calling_ae_title, called_ae_title, max_pdu_length, @@ -412,6 +422,7 @@ async fn run_async() -> Result<(), Error> { saml_assertion, jwt, concurrency, + tls } = App::parse(); // never transcode if the feature is disabled @@ -419,15 +430,9 @@ async fn run_async() -> Result<(), Error> { never_transcode = true; } - tracing::subscriber::set_global_default( - tracing_subscriber::FmtSubscriber::builder() - .with_max_level(if verbose { Level::DEBUG } else { Level::INFO }) - .finish(), - ) - .whatever_context("Could not set up global logging subscriber") - .unwrap_or_else(|e: Whatever| { - eprintln!("[ERROR] {}", Report::from_error(e)); - }); + let tls_enabled = tls.enabled; + let config = tls.client_config() + .context(TlsSnafu)?; if verbose { info!("Establishing association with '{}'...", &addr); @@ -468,9 +473,9 @@ async fn run_async() -> Result<(), Error> { let password = password.clone(); let called_ae_title = called_ae_title.clone(); let calling_ae_title = calling_ae_title.clone(); + let tls_config_clone = config.clone(); tasks.spawn(async move { - let mut scu = get_scu( - addr, + let scu_options = get_scu_options( calling_ae_title, called_ae_title, max_pdu_length, @@ -479,48 +484,42 @@ async fn run_async() -> Result<(), Error> { kerberos_service_ticket, saml_assertion, jwt, - pc, - ) - .await?; - loop { - let file = { - let mut files = d_files.lock().await; - files.pop() - }; - let mut file = match file { - Some(file) => file, - None => break, - }; - let r: Result<_, Error> = check_presentation_contexts( - &file, - scu.presentation_contexts(), + &pc, + tls_config_clone + ); + if tls_enabled { + let scu = scu_options + .establish_with_async_tls(&addr) + .await + .map_err(Box::from) + .context(ScuSnafu)?; + store_async::inner( + scu, + d_files, + pbx, + never_transcode, + fail_first, + verbose, ignore_sop_class, + ) + .await + } else { + let scu = scu_options + .establish_async(&addr) + .await + .map_err(Box::from) + .context(ScuSnafu)?; + store_async::inner( + scu, + d_files, + pbx, never_transcode, - ); - match r { - Ok((pc, ts)) => { - if verbose { - debug!( - "{}: Selected presentation context: {:?}", - file.file.display(), - pc - ); - } - file.pc_selected = Some(pc); - file.ts_selected = Some(ts); - } - Err(e) => { - error!("{}", Report::from_error(e)); - if fail_first { - let _ = scu.abort().await; - std::process::exit(-2); - } - } - } - scu = send_file(scu, file, message_id, pbx.as_ref(), verbose, fail_first).await?; + fail_first, + verbose, + ignore_sop_class, + ) + .await } - let _ = scu.release().await; - Ok::<(), Error>(()) }); } while let Some(result) = tasks.join_next().await { diff --git a/storescu/src/store_async.rs b/storescu/src/store_async.rs index 9e01ef8f..7bbd1efc 100644 --- a/storescu/src/store_async.rs +++ b/storescu/src/store_async.rs @@ -1,84 +1,30 @@ -use std::{collections::HashSet, io::stderr, sync::Arc}; +use std::{io::stderr, sync::Arc}; use dicom_dictionary_std::tags; use dicom_encoding::TransferSyntaxIndex; use dicom_object::{open_file, InMemDicomObject}; use dicom_transfer_syntax_registry::TransferSyntaxRegistry; use dicom_ul::{ - pdu::{PDataValue, PDataValueType}, - ClientAssociation, ClientAssociationOptions, Pdu, + Pdu, association::{Association, AsyncAssociation, client::AsyncClientAssociation}, pdu::{PDataValue, PDataValueType} }; use indicatif::ProgressBar; -use snafu::{OptionExt, ResultExt}; -use tokio::{io::AsyncWriteExt, net::TcpStream}; +use snafu::{OptionExt, Report, ResultExt}; +use tokio::{io::AsyncWriteExt, sync::Mutex}; use tracing::{debug, error, info, warn}; use crate::{ - into_ts, store_req_command, ConvertFieldSnafu, CreateCommandSnafu, DicomFile, Error, - MissingAttributeSnafu, ReadDatasetSnafu, ReadFilePathSnafu, ScuSnafu, - UnsupportedFileTransferSyntaxSnafu, WriteDatasetSnafu, + ConvertFieldSnafu, CreateCommandSnafu, DicomFile, Error, MissingAttributeSnafu, ReadDatasetSnafu, ReadFilePathSnafu, ScuSnafu, UnsupportedFileTransferSyntaxSnafu, WriteDatasetSnafu, check_presentation_contexts, into_ts, store_req_command }; -#[allow(clippy::too_many_arguments)] -pub async fn get_scu( - addr: String, - calling_ae_title: String, - called_ae_title: Option, - max_pdu_length: u32, - username: Option, - password: Option, - kerberos_service_ticket: Option, - saml_assertion: Option, - jwt: Option, - presentation_contexts: HashSet<(String, String)>, -) -> Result, Error> { - let mut scu_init = ClientAssociationOptions::new() - .calling_ae_title(calling_ae_title) - .max_pdu_length(max_pdu_length); - - for (storage_sop_class_uid, transfer_syntax) in &presentation_contexts { - scu_init = scu_init.with_presentation_context(storage_sop_class_uid, vec![transfer_syntax]); - } - - if let Some(called_ae_title) = called_ae_title { - scu_init = scu_init.called_ae_title(called_ae_title); - } - - if let Some(username) = username { - scu_init = scu_init.username(username); - } - - if let Some(password) = password { - scu_init = scu_init.password(password); - } - - if let Some(kerberos_service_ticket) = kerberos_service_ticket { - scu_init = scu_init.kerberos_service_ticket(kerberos_service_ticket); - } - - if let Some(saml_assertion) = saml_assertion { - scu_init = scu_init.saml_assertion(saml_assertion); - } - - if let Some(jwt) = jwt { - scu_init = scu_init.jwt(jwt); - } - - scu_init - .establish_with_async(&addr) - .await - .map_err(Box::from) - .context(ScuSnafu) -} - -pub async fn send_file( - mut scu: ClientAssociation, +pub async fn send_file( + mut scu: AsyncClientAssociation, file: DicomFile, message_id: u16, progress_bar: Option<&Arc>>, verbose: bool, fail_first: bool, -) -> Result, Error> { +) -> Result, Error> +where T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static{ if let (Some(pc_selected), Some(ts_uid_selected)) = (file.pc_selected, file.ts_selected) { let cmd = store_req_command(&file.sop_class_uid, &file.sop_instance_uid, message_id); @@ -156,7 +102,7 @@ pub async fn send_file( scu.send(&pdu).await.map_err(Box::from).context(ScuSnafu)?; { - let mut pdata = scu.send_pdata(pc_selected.id).await; + let mut pdata = scu.send_pdata(pc_selected.id); pdata.write_all(&object_data).await.unwrap(); //.whatever_context("Failed to send C-STORE-RQ P-Data")?; } @@ -251,3 +197,58 @@ pub async fn send_file( }; Ok(scu) } + + +pub async fn inner( + mut scu: AsyncClientAssociation, + d_files: Arc>>, + pbx: Option>>, + never_transcode: bool, + fail_first: bool, + verbose: bool, + ignore_sop_class: bool, +) -> Result<(), Error> + where T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static { + let mut message_id = 1; + loop { + let file = { + let mut files = d_files.lock().await; + files.pop() + }; + let mut file = match file { + Some(file) => file, + None => break, + }; + let r: Result<_, Error> = check_presentation_contexts( + &file, + scu.presentation_contexts(), + ignore_sop_class, + never_transcode, + ); + match r { + Ok((pc, ts)) => { + if verbose { + debug!( + "{}: Selected presentation context: {:?}", + file.file.display(), + pc + ); + } + file.pc_selected = Some(pc); + file.ts_selected = Some(ts); + } + Err(e) => { + error!("{}", Report::from_error(e)); + if fail_first { + let _ = scu.abort().await; + std::process::exit(-2); + } + } + } + scu = send_file(scu, file, message_id, pbx.as_ref(), verbose, fail_first).await?; + message_id += 1; + } + let _ = scu.release().await; + Ok(()) + +} \ No newline at end of file diff --git a/storescu/src/store_sync.rs b/storescu/src/store_sync.rs index a4ed0b7f..cb7e4095 100644 --- a/storescu/src/store_sync.rs +++ b/storescu/src/store_sync.rs @@ -1,7 +1,5 @@ use std::{ - collections::HashSet, io::{stderr, Write}, - net::TcpStream, }; use dicom_dictionary_std::tags; @@ -9,78 +7,25 @@ use dicom_encoding::TransferSyntaxIndex; use dicom_object::{open_file, InMemDicomObject}; use dicom_transfer_syntax_registry::TransferSyntaxRegistry; use dicom_ul::{ - pdu::{PDataValue, PDataValueType}, - ClientAssociation, ClientAssociationOptions, Pdu, + ClientAssociation, Pdu, association::{Association, CloseSocket, SyncAssociation}, pdu::{PDataValue, PDataValueType} }; use indicatif::ProgressBar; -use snafu::{OptionExt, ResultExt}; +use snafu::{OptionExt, Report, ResultExt}; use tracing::{debug, error, info, warn}; use crate::{ - into_ts, store_req_command, ConvertFieldSnafu, CreateCommandSnafu, DicomFile, Error, - MissingAttributeSnafu, ReadDatasetSnafu, ReadFilePathSnafu, ScuSnafu, - UnsupportedFileTransferSyntaxSnafu, WriteDatasetSnafu, WriteIOSnafu, + ConvertFieldSnafu, CreateCommandSnafu, DicomFile, Error, MissingAttributeSnafu, ReadDatasetSnafu, ReadFilePathSnafu, ScuSnafu, UnsupportedFileTransferSyntaxSnafu, WriteDatasetSnafu, WriteIOSnafu, check_presentation_contexts, into_ts, store_req_command }; -#[allow(clippy::too_many_arguments)] -pub fn get_scu( - addr: String, - calling_ae_title: String, - called_ae_title: Option, - max_pdu_length: u32, - username: Option, - password: Option, - kerberos_service_ticket: Option, - saml_assertion: Option, - jwt: Option, - presentation_contexts: HashSet<(String, String)>, -) -> Result, Error> { - let mut scu_init = ClientAssociationOptions::new() - .calling_ae_title(calling_ae_title) - .max_pdu_length(max_pdu_length); - - for (storage_sop_class_uid, transfer_syntax) in &presentation_contexts { - scu_init = scu_init.with_presentation_context(storage_sop_class_uid, vec![transfer_syntax]); - } - - if let Some(called_ae_title) = called_ae_title { - scu_init = scu_init.called_ae_title(called_ae_title); - } - - if let Some(username) = username { - scu_init = scu_init.username(username); - } - - if let Some(password) = password { - scu_init = scu_init.password(password); - } - - if let Some(kerberos_service_ticket) = kerberos_service_ticket { - scu_init = scu_init.kerberos_service_ticket(kerberos_service_ticket); - } - - if let Some(saml_assertion) = saml_assertion { - scu_init = scu_init.saml_assertion(saml_assertion); - } - - if let Some(jwt) = jwt { - scu_init = scu_init.jwt(jwt); - } - - scu_init - .establish_with(&addr) - .map_err(Box::from) - .context(ScuSnafu) -} - -pub fn send_file( - mut scu: ClientAssociation, +pub fn send_file( + mut scu: ClientAssociation, file: DicomFile, message_id: u16, progress_bar: Option<&ProgressBar>, verbose: bool, fail_first: bool, -) -> Result, Error> { +) -> Result, Error> +where T: std::io::Read + std::io::Write + CloseSocket{ if let (Some(pc_selected), Some(ts_uid_selected)) = (file.pc_selected, file.ts_selected) { if let Some(pb) = &progress_bar { pb.set_message(file.sop_instance_uid.clone()); @@ -255,3 +200,56 @@ pub fn send_file( }; Ok(scu) } + + +pub fn inner( + mut scu: ClientAssociation, + d_files: Vec, + pbx: &Option, + fail_first: bool, + verbose: bool, + never_transcode: bool, + ignore_sop_class: bool, +) -> Result<(), Error> +where T: std::io::Read + std::io::Write + CloseSocket{ + let mut message_id = 1; + for mut file in d_files { + // identify the right transfer syntax to use + let r: Result<_, Error> = + check_presentation_contexts(&file, scu.presentation_contexts(), ignore_sop_class, never_transcode); + match r { + Ok((pc, ts)) => { + if verbose { + debug!( + "{}: Selected presentation context: {:?}", + file.file.display(), + pc + ); + } + file.pc_selected = Some(pc); + file.ts_selected = Some(ts); + } + Err(e) => { + error!("{}", Report::from_error(e)); + if fail_first { + let _ = scu.abort(); + std::process::exit(-2); + } + } + } + scu = send_file( + scu, + file, + message_id, + pbx.as_ref(), + verbose, + fail_first, + )?; + message_id += 1; + } + scu.release().map_err(Box::from).context(ScuSnafu)?; + if let Some(pb) = pbx { + pb.finish_with_message("done") + }; + Ok(()) +} \ No newline at end of file diff --git a/ul/Cargo.toml b/ul/Cargo.toml index a7b8f5f1..ee21f712 100644 --- a/ul/Cargo.toml +++ b/ul/Cargo.toml @@ -14,9 +14,12 @@ readme = "README.md" [dependencies] byteordered = "0.6" bytes = "^1.6" +cfg-if = "1.0.3" dicom-encoding = { path = "../encoding/", version = "0.9" } dicom-transfer-syntax-registry = { path = "../transfer-syntax-registry/", version = "0.9", default-features = false } +rustls = {version = "0.23.31", optional = true } snafu = "0.8" +tokio-rustls = {version = "0.26.2", optional = true } tracing = "0.1.34" [dependencies.tokio] @@ -37,10 +40,16 @@ rstest = "0.26.1" tokio = { version = "^1.38", features = ["io-util", "macros", "net", "rt", "rt-multi-thread"] } dicom-core = { path = '../core' } dicom-object = { path = '../object' } +time = "0.3" +rustls-cert-gen = "0.2.0" +rcgen = "0.14.4" [features] async = ["dep:tokio"] +sync-tls = ["dep:rustls"] +async-tls=["async", "sync-tls", "dep:tokio-rustls"] default = [] +full = ["async-tls"] [package.metadata.docs.rs] -features = ["async"] +features = ["async", "sync-tls", "async-tls"] diff --git a/ul/README.md b/ul/README.md index 53be7a3b..9bffd380 100644 --- a/ul/README.md +++ b/ul/README.md @@ -4,3 +4,21 @@ [![Documentation](https://docs.rs/dicom-ul/badge.svg)](https://docs.rs/dicom-ul) This is an implementation of the DICOM upper layer protocol. + +## Testing + +### TLS + +TLS testing requires a Certificate authority, and signed client/server key pairs + +A function is provided within `tests/association.rs` which: + +1. Creates a CA with a `US` country code +2. Creates certificate signing requests for two "clients" (one client and one server) +3. Signs the certificates using the CA + +When finished, there should be a `.pem`, and a `.key.pem` for the client, server and CA client and server, + +You can change the country or the IP/DNS of the configured +client/server +by modifying `country_name` and/or `organization_name` in the test code. \ No newline at end of file diff --git a/ul/src/association/client.rs b/ul/src/association/client.rs index 3e58a42d..0f5dc2c8 100644 --- a/ul/src/association/client.rs +++ b/ul/src/association/client.rs @@ -8,33 +8,83 @@ use bytes::BytesMut; use std::{ borrow::Cow, convert::TryInto, - io::Write, net::{TcpStream, ToSocketAddrs}, time::Duration, }; use crate::{ - association::{ - read_pdu_from_wire, ConnectSnafu, MissingAbstractSyntaxSnafu, NegotiatedOptions, - NoAcceptedPresentationContextsSnafu, ProtocolVersionMismatchSnafu, RejectedSnafu, - SendPduSnafu, SendTooLongPduSnafu, SetReadTimeoutSnafu, SetWriteTimeoutSnafu, - ToAddressSnafu, UnexpectedPduSnafu, UnknownPduSnafu, WireSendSnafu, - }, - pdu::{ - write_pdu, AbortRQSource, AssociationAC, AssociationRQ, Pdu, PresentationContextNegotiated, - PresentationContextProposed, PresentationContextResultReason, UserIdentity, - UserIdentityType, UserVariableItem, DEFAULT_MAX_PDU, LARGE_PDU_SIZE, PDU_HEADER_SIZE, - }, - AeAddr, IMPLEMENTATION_CLASS_UID, IMPLEMENTATION_VERSION_NAME, + AeAddr, IMPLEMENTATION_CLASS_UID, IMPLEMENTATION_VERSION_NAME, association::{ + Association, CloseSocket, NegotiatedOptions, SocketOptions, SyncAssociation, encode_pdu, private::SyncAssociationSealed, read_pdu_from_wire + }, pdu::{ + AbortRQSource, AssociationAC, AssociationRQ, DEFAULT_MAX_PDU, LARGE_PDU_SIZE, PDU_HEADER_SIZE, Pdu, PresentationContextNegotiated, PresentationContextProposed, PresentationContextResultReason, UserIdentity, UserIdentityType, UserVariableItem, write_pdu + } }; use snafu::{ensure, ResultExt}; use super::{ - pdata::{PDataReader, PDataWriter}, uid::trim_uid, Result, }; +#[cfg(feature = "sync-tls")] +pub type TlsStream = rustls::StreamOwned; +#[cfg(feature = "async-tls")] +pub type AsyncTlsStream = tokio_rustls::client::TlsStream; + +/// Helper function to establish a TCP client connection +fn tcp_connection( + ae_address: &AeAddr, + opts: &SocketOptions, +) -> Result where T: ToSocketAddrs +{ + // NOTE: TcpStream::connect_timeout needs a single SocketAddr, whereas TcpStream::connect can + // take multiple + let conn_result: Result = if let Some(timeout) = opts.connection_timeout { + let addresses = ae_address.to_socket_addrs().context(super::ToAddressSnafu)?; + let mut result = Result::Err(std::io::Error::from(std::io::ErrorKind::AddrNotAvailable)); + for address in addresses { + result = TcpStream::connect_timeout(&address, timeout); + if result.is_ok() { + break; + } + } + result.context(super::ConnectSnafu) + } else { + TcpStream::connect(ae_address).context(super::ConnectSnafu) + }; + + let socket = conn_result?; + socket + .set_read_timeout(opts.read_timeout) + .context(super::SetReadTimeoutSnafu)?; + socket + .set_write_timeout(opts.write_timeout) + .context(super::SetWriteTimeoutSnafu)?; + + Ok(socket) + +} + +/// Helper function to establish a TLS client connection +#[cfg(feature = "sync-tls")] +fn tls_connection( + ae_address: &AeAddr, + server_name: &str, + opts: &SocketOptions, + tls_config: std::sync::Arc, +) -> Result where T: ToSocketAddrs{ + use std::convert::TryFrom; + + let socket = tcp_connection(ae_address, opts)?; + let server_name = rustls::pki_types::ServerName::try_from(server_name.to_string()) + .context(super::InvalidServerNameSnafu)?; + + let conn = rustls::ClientConnection::new(tls_config.clone(), server_name) + .context(super::TlsConnectionSnafu)?; + + Ok(rustls::StreamOwned::new(conn, socket)) +} + /// A DICOM association builder for a client node. /// The final outcome is a [`ClientAssociation`]. /// @@ -47,7 +97,7 @@ use super::{ /// /// > **⚠️ Warning:** It is highly recommended to set `read_timeout` and `write_timeout` to a reasonable /// > value for the async client since there is _no_ default timeout on -/// > [`tokio::net::TcpStream`] +/// > [`TcpStream`] /// /// ## Basic usage /// @@ -67,12 +117,14 @@ use super::{ /// ``` /// /// ### Async +/// +/// * Make sure you include the `async` feature in your `Cargo.toml` /// -/// ```no_run +/// ```ignore /// # use dicom_ul::association::client::ClientAssociationOptions; /// # use std::time::Duration; -/// #[cfg(feature = "async")] -/// #[tokio::main] +/// # #[cfg(feature = "async")] +/// # #[tokio::main] /// # async fn main() -> Result<(), Box> { /// let association = ClientAssociationOptions::new() /// .with_presentation_context("1.2.840.10008.1.1", vec!["1.2.840.10008.1.2.1", "1.2.840.10008.1.2"]) @@ -82,11 +134,65 @@ use super::{ /// .await?; /// # Ok(()) /// # } -/// #[cfg(not(feature = "async"))] -/// fn main() {} +/// ``` +/// +/// ## TLS Support +/// +/// ### Sync TLS +/// +/// * Make sure you include the `tls` feature in your `Cargo.toml` +/// +/// ### Async TLS +/// +/// * Make sure you include the `async-tls` feature in your `Cargo.toml` +/// +/// > **⚠️ Warning:** Just including the `async` and `tls` features will _not_ work! +/// +/// ### Example +/// ``` +/// # use dicom_ul::association::client::ClientAssociationOptions; +/// # use std::time::Duration; +/// # use std::sync::Arc; +/// # #[cfg(feature = "tls")] +/// # fn run() -> Result<(), Box> { +/// use rustls::{ +/// ClientConfig, RootCertStore, +/// pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject}, +/// }; +/// // Using a self-signed certificate for demonstration purposes only. +/// let ca_cert = CertificateDer::from_pem_slice(include_bytes!("../../assets/ca.crt").as_ref()) +/// .expect("Failed to load client cert"); +/// +/// // Server certificate -- signed by CA +/// let server_cert = CertificateDer::from_pem_slice(include_bytes!("../../assets/server.crt").as_ref()) +/// .expect("Failed to load server cert"); +/// +/// // Client cert and private key -- signed by CA +/// let client_cert = CertificateDer::from_pem_slice(include_bytes!("../../assets/client.crt").as_ref()) +/// .expect("Failed to load client cert"); +/// let client_private_key = PrivateKeyDer::from_pem_slice(include_bytes!("../../assets/client.key").as_ref()) +/// .expect("Failed to load client private key"); +/// +/// // Create a root cert store for the client which includes the server certificate +/// let mut certs = RootCertStore::empty(); +/// certs.add_parsable_certificates(vec![ca_cert.clone()]); +/// +/// let config = ClientConfig::builder() +/// .with_root_certificates(certs) +/// .with_client_auth_cert(vec![client_cert, ca_cert], client_private_key) +/// .expect("Failed to create client TLS config"); +/// +/// let association = ClientAssociationOptions::new() +/// .with_presentation_context("1.2.840.10008.1.1", vec!["1.2.840.10008.1.2.1", "1.2.840.10008.1.2"]) +/// .tls_config(config) +/// .read_timeout(Duration::from_secs(60)) +/// .write_timeout(Duration::from_secs(60)) +/// .establish("129.168.0.5:104")?; +/// # Ok(()) +/// # } /// ``` /// -/// ### Presentation contexts +/// ## Presentation contexts /// /// At least one presentation context must be specified, /// using the method [`with_presentation_context`](Self::with_presentation_context) @@ -132,12 +238,14 @@ pub struct ClientAssociationOptions<'a> { saml_assertion: Option>, /// User identity JWT jwt: Option>, - /// TCP read timeout - read_timeout: Option, - /// TCP write timeout - write_timeout: Option, - /// TCP connection timeout - connection_timeout: Option, + /// Socket options for TCP connections + socket_options: SocketOptions, + /// TLS configuration to use for the connection + #[cfg(feature = "sync-tls")] + tls_config: Option>, + /// Server name for TLS + #[cfg(feature = "sync-tls")] + server_name: Option, } impl Default for ClientAssociationOptions<'_> { @@ -159,9 +267,15 @@ impl Default for ClientAssociationOptions<'_> { kerberos_service_ticket: None, saml_assertion: None, jwt: None, - read_timeout: None, - write_timeout: None, - connection_timeout: None, + socket_options: SocketOptions { + read_timeout: None, + write_timeout: None, + connection_timeout: None, + }, + #[cfg(feature = "sync-tls")] + tls_config: None, + #[cfg(feature = "sync-tls")] + server_name: None, } } } @@ -171,7 +285,6 @@ impl<'a> ClientAssociationOptions<'a> { pub fn new() -> Self { Self::default() } - /// Define the calling application entity title for the association, /// which refers to this DICOM node. /// @@ -358,14 +471,50 @@ impl<'a> ClientAssociationOptions<'a> { self } - /// Initiate the TCP connection to the given address + /// Set the TLS configuration to use for the connection + #[cfg(feature = "sync-tls")] + pub fn tls_config(mut self, config: impl Into>) -> Self { + self.tls_config = Some(config.into()); + self + } + + /// Set the server name to use for the TLS connection + #[cfg(feature = "sync-tls")] + pub fn server_name(mut self, server_name: &str) -> Self { + self.server_name = Some(server_name.to_string()); + self + } + + /// Initiate simple TCP connection to the given address /// and request a new DICOM association, /// negotiating the presentation contexts in the process. pub fn establish( self, address: A, - ) -> Result> { - self.establish_impl(AeAddr::new_socket_addr(address)) + ) -> Result> + { + let addr = AeAddr::new_socket_addr(address); + let socket = tcp_connection(&addr, &self.socket_options)?; + self.establish_impl(addr, socket) + } + + /// Initiate simple TCP connection to the given address + /// and request a new DICOM association, + /// negotiating the presentation contexts in the process. + #[cfg(feature = "sync-tls")] + pub fn establish_tls( + self, address: A + ) -> Result> { + match (&self.tls_config, &self.server_name) { + (Some(tls_config), Some(server_name)) => { + let addr = AeAddr::new_socket_addr(address); + let socket = tls_connection( + &addr, server_name, &self.socket_options, tls_config.clone() + )?; + self.establish_impl(addr, socket) + }, + _ => super::TlsConfigMissingSnafu.fail()? + } } /// Initiate the TCP connection to the given address @@ -395,10 +544,70 @@ impl<'a> ClientAssociationOptions<'a> { pub fn establish_with( self, ae_address: &str, - ) -> Result> { + ) -> Result> { match ae_address.try_into() { - Ok(ae_address) => self.establish_impl(ae_address), - Err(_) => self.establish_impl(AeAddr::new_socket_addr(ae_address)), + Ok(ae_address) => { + let socket = tcp_connection(&ae_address, &self.socket_options)?; + self.establish_impl(ae_address, socket) + }, + Err(_) => { + let addr = AeAddr::new_socket_addr(ae_address); + let socket = tcp_connection(&addr, &self.socket_options)?; + self.establish_impl(addr, socket) + }, + } + } + + + /// Initiate TLS connection to the given address + /// and request a new DICOM association, + /// negotiating the presentation contexts in the process. + /// + /// This method allows you to specify the called AE title + /// alongside with the socket address. + /// See [AeAddr](`crate::AeAddr`) for more details. + /// However, the AE title in this parameter + /// is overridden by any `called_ae_title` option + /// previously received. + /// + /// # Example + /// + /// ```no_run + /// # use dicom_ul::association::client::ClientAssociationOptions; + /// # fn run() -> Result<(), Box> { + /// let association = ClientAssociationOptions::new() + /// .with_abstract_syntax("1.2.840.10008.1.1") + /// // called AE title in address + /// .establish_with("MY-STORAGE@10.0.0.100:104")?; + /// # Ok(()) + /// # } + /// ``` + #[allow(unreachable_patterns)] + #[cfg(feature = "sync-tls")] + pub fn establish_with_tls( + self, + ae_address: &str, + ) -> Result> { + match (&self.tls_config, &self.server_name) { + (Some(tls_config), Some(server_name)) => { + match ae_address.try_into() { + Ok(ae_address) => { + let socket = tls_connection( + &ae_address, server_name, &self.socket_options, tls_config.clone() + )?; + self.establish_impl(ae_address, socket) + }, + Err(_) => { + let addr = AeAddr::new_socket_addr(ae_address); + let socket = tls_connection( + &addr, server_name, &self.socket_options, tls_config.clone() + )?; + self.establish_impl(addr, socket) + }, + } + + }, + _ => super::TlsConfigMissingSnafu.fail()? } } @@ -407,7 +616,11 @@ impl<'a> ClientAssociationOptions<'a> { /// This is used to set both the read and write timeout. pub fn read_timeout(self, timeout: Duration) -> Self { Self { - read_timeout: Some(timeout), + socket_options: SocketOptions { + read_timeout: Some(timeout), + write_timeout: self.socket_options.write_timeout, + connection_timeout: self.socket_options.connection_timeout, + }, ..self } } @@ -415,7 +628,11 @@ impl<'a> ClientAssociationOptions<'a> { /// Set the write timeout for the underlying TCP socket pub fn write_timeout(self, timeout: Duration) -> Self { Self { - write_timeout: Some(timeout), + socket_options: SocketOptions { + read_timeout: self.socket_options.read_timeout, + write_timeout: Some(timeout), + connection_timeout: self.socket_options.connection_timeout, + }, ..self } } @@ -423,7 +640,11 @@ impl<'a> ClientAssociationOptions<'a> { /// Set the connection timeout for the underlying TCP socket pub fn connection_timeout(self, timeout: Duration) -> Self { Self { - connection_timeout: Some(timeout), + socket_options: SocketOptions { + read_timeout: self.socket_options.read_timeout, + write_timeout: self.socket_options.write_timeout, + connection_timeout: Some(timeout), + }, ..self } } @@ -451,7 +672,7 @@ impl<'a> ClientAssociationOptions<'a> { // should not be omitted by the user ensure!( !presentation_contexts.is_empty(), - MissingAbstractSyntaxSnafu + crate::association::MissingAbstractSyntaxSnafu ); // choose called AE title @@ -526,12 +747,12 @@ impl<'a> ClientAssociationOptions<'a> { application_context_name: _, presentation_contexts: presentation_contexts_scp, calling_ae_title: _, - called_ae_title: _, + called_ae_title, user_variables, }) => { ensure!( self.protocol_version == protocol_version_scp, - ProtocolVersionMismatchSnafu { + crate::association::ProtocolVersionMismatchSnafu { expected: self.protocol_version, got: protocol_version_scp, } @@ -572,70 +793,40 @@ impl<'a> ClientAssociationOptions<'a> { }) .collect(); if presentation_contexts.is_empty() { - return NoAcceptedPresentationContextsSnafu.fail(); + return crate::association::NoAcceptedPresentationContextsSnafu.fail(); } Ok(NegotiatedOptions { presentation_contexts, peer_max_pdu_length: acceptor_max_pdu_length, user_variables, + peer_ae_title: called_ae_title, }) } - Pdu::AssociationRJ(association_rj) => RejectedSnafu { association_rj }.fail(), + Pdu::AssociationRJ(association_rj) => crate::association::RejectedSnafu { association_rj }.fail(), pdu @ Pdu::AbortRQ { .. } | pdu @ Pdu::ReleaseRQ | pdu @ Pdu::AssociationRQ { .. } | pdu @ Pdu::PData { .. } - | pdu @ Pdu::ReleaseRP => UnexpectedPduSnafu { pdu }.fail(), - pdu @ Pdu::Unknown { .. } => UnknownPduSnafu { pdu }.fail(), + | pdu @ Pdu::ReleaseRP => crate::association::UnexpectedPduSnafu { pdu }.fail(), + pdu @ Pdu::Unknown { .. } => crate::association::UnknownPduSnafu { pdu }.fail() } } - fn simple_tcp_connection(&self, ae_address: AeAddr) -> Result - where - T: ToSocketAddrs, - { - let conn_result: Result = if let Some(timeout) = self.connection_timeout { - let addresses = ae_address.to_socket_addrs().context(ToAddressSnafu)?; - - let mut result: Result = - Result::Err(std::io::Error::from(std::io::ErrorKind::AddrNotAvailable)); - - for address in addresses { - result = std::net::TcpStream::connect_timeout(&address, timeout); - if result.is_ok() { - break; - } - } - result.context(ConnectSnafu) - } else { - std::net::TcpStream::connect(ae_address).context(ConnectSnafu) - }; - - let socket = conn_result?; - socket - .set_read_timeout(self.read_timeout) - .context(SetReadTimeoutSnafu)?; - socket - .set_write_timeout(self.write_timeout) - .context(SetWriteTimeoutSnafu)?; - - Ok(socket) - } - /// Establish the association with the given AE address. - fn establish_impl( + fn establish_impl( self, ae_address: AeAddr, - ) -> Result> + mut socket: S + ) -> Result> where T: ToSocketAddrs, + S: CloseSocket + std::io::Read + std::io::Write, { let (pc_proposed, a_associate) = self.create_a_associate_req(ae_address.ae_title())?; - let mut socket = self.simple_tcp_connection(ae_address)?; - let mut buffer: Vec = Vec::with_capacity(DEFAULT_MAX_PDU as usize); - // send request - write_pdu(&mut buffer, &a_associate).context(SendPduSnafu)?; - socket.write_all(&buffer).context(WireSendSnafu)?; + let mut buffer: Vec = Vec::with_capacity(self.max_pdu_length as usize); + + write_pdu(&mut buffer, &a_associate).context(super::SendPduSnafu)?; + socket.write_all(&buffer).context(super::WireSendSnafu)?; buffer.clear(); let mut buf = BytesMut::with_capacity( @@ -655,24 +846,21 @@ impl<'a> ClientAssociationOptions<'a> { let _ = socket.write_all(&buffer); buffer.clear(); Err(e) - } - Ok(NegotiatedOptions { - presentation_contexts, - peer_max_pdu_length, - user_variables, - }) => { + }, + Ok(NegotiatedOptions{presentation_contexts, peer_max_pdu_length, user_variables, peer_ae_title}) => { Ok(ClientAssociation { presentation_contexts, requestor_max_pdu_length: self.max_pdu_length, acceptor_max_pdu_length: peer_max_pdu_length, socket, - buffer, + write_buffer: buffer, strict: self.strict, // Fixes #589, instead of creating a new buffer, we pass the existing buffer into the Association object. read_buffer: buf, - read_timeout: self.read_timeout, - write_timeout: self.write_timeout, + read_timeout: self.socket_options.read_timeout, + write_timeout: self.socket_options.write_timeout, user_variables, + peer_ae_title, }) } } @@ -737,28 +925,6 @@ impl<'a> ClientAssociationOptions<'a> { } } -/// Trait to close underlying socket -pub trait CloseSocket { - fn close(&mut self) -> std::io::Result<()>; -} - -impl CloseSocket for std::net::TcpStream { - fn close(&mut self) -> std::io::Result<()> { - self.shutdown(std::net::Shutdown::Both) - } -} - -/// Trait to release association -pub trait Release { - fn release(&mut self) -> Result<()>; -} - -impl Release for ClientAssociation { - fn release(&mut self) -> Result<()> { - self.release_impl() - } -} - /// A DICOM upper level association from the perspective /// of a requesting application entity. /// @@ -777,9 +943,7 @@ impl Release for ClientAssociation { /// establish the association. #[derive(Debug)] pub struct ClientAssociation -where - S: CloseSocket, - ClientAssociation: Release, +where S: CloseSocket + std::io::Read + std::io::Write, { /// The presentation contexts accorded with the acceptor application entity, /// without the rejected ones. @@ -790,8 +954,8 @@ where acceptor_max_pdu_length: u32, /// The TCP stream to the other DICOM node socket: S, - /// Buffer to assemble PDU before sending it on wire - buffer: Vec, + /// Buffer to write PDUs to the wire, prevents needing to allocate on every send + write_buffer: Vec, /// whether to receive PDUs in strict mode strict: bool, /// Timeout for individual socket Reads @@ -802,523 +966,461 @@ where read_buffer: BytesMut, /// User variables that were taken from the server user_variables: Vec, + /// The AE title of the peer + peer_ae_title: String, } -impl ClientAssociation -where - ClientAssociation: Release, +impl Association for ClientAssociation +where S: CloseSocket + std::io::Read + std::io::Write, { - /// Retrieve read timeout for the association - pub fn read_timeout(&self) -> Option { - self.read_timeout - } - - /// Retrieve write timeout for the association - pub fn write_timeout(&self) -> Option { - self.write_timeout + fn peer_ae_title(&self) -> &str { + &self.peer_ae_title } - /// Retrieve the list of negotiated presentation contexts. - pub fn presentation_contexts(&self) -> &[PresentationContextNegotiated] { - &self.presentation_contexts + fn requestor_max_pdu_length(&self) -> u32 { + self.requestor_max_pdu_length } - /// Retrieve the maximum PDU length - /// admitted by the association acceptor. - pub fn acceptor_max_pdu_length(&self) -> u32 { + fn acceptor_max_pdu_length(&self) -> u32 { self.acceptor_max_pdu_length } - /// Retrieve the maximum PDU length - /// that this application entity is expecting to receive. - /// - /// The current implementation is not required to fail - /// and/or abort the association - /// if a larger PDU is received. - pub fn requestor_max_pdu_length(&self) -> u32 { - self.requestor_max_pdu_length + fn presentation_contexts(&self) -> &[PresentationContextNegotiated] { + &self.presentation_contexts } - /// Retrieve the user variables that were taken from the server. - /// - /// It usually contains the maximum PDU length, - /// the implementation class UID, and the implementation version name. - pub fn user_variables(&self) -> &[UserVariableItem] { + fn user_variables(&self) -> &[UserVariableItem] { &self.user_variables } } -impl ClientAssociation -where - ClientAssociation: Release, +impl ClientAssociation +where S: CloseSocket + std::io::Read + std::io::Write, { - /// Send a PDU message to the other intervenient. - pub fn send(&mut self, msg: &Pdu) -> Result<()> { - self.buffer.clear(); - write_pdu(&mut self.buffer, msg).context(SendPduSnafu)?; - if self.buffer.len() > (self.acceptor_max_pdu_length + PDU_HEADER_SIZE) as usize { - return SendTooLongPduSnafu { - length: self.buffer.len(), - } - .fail(); - } - self.socket.write_all(&self.buffer).context(WireSendSnafu) - } - - /// Read a PDU message from the other intervenient. - pub fn receive(&mut self) -> Result { - read_pdu_from_wire( - &mut self.socket, - &mut self.read_buffer, - self.requestor_max_pdu_length, - self.strict, - ) + /// Retrieve read timeout for the association + pub fn read_timeout(&self) -> Option { + self.read_timeout } - /// Gracefully terminate the association by exchanging release messages - /// and then shutting down the TCP connection. - pub fn release(mut self) -> Result<()> { - let out = self.release_impl(); - let _ = self.socket.shutdown(std::net::Shutdown::Both); - out + /// Retrieve write timeout for the association + pub fn write_timeout(&self) -> Option { + self.write_timeout } +} - /// Send an abort message and shut down the TCP connection, - /// terminating the association. - pub fn abort(mut self) -> Result<()> { - let pdu = Pdu::AbortRQ { - source: AbortRQSource::ServiceUser, - }; - let out = self.send(&pdu); - let _ = self.socket.shutdown(std::net::Shutdown::Both); - out +impl SyncAssociationSealed for ClientAssociation +where S: CloseSocket + std::io::Read + std::io::Write{ + /// Send a PDU message to the other intervenient. + fn send(&mut self, pdu: &Pdu) -> Result<()> { + self.write_buffer.clear(); + encode_pdu(&mut self.write_buffer, pdu, self.acceptor_max_pdu_length + PDU_HEADER_SIZE)?; + self.socket.write_all(&self.write_buffer).context(super::WireSendSnafu) } - /// Obtain access to the inner TCP stream - /// connected to the association acceptor. - /// - /// This can be used to send the PDU in semantic fragments of the message, - /// thus using less memory. - /// - /// **Note:** reading and writing should be done with care - /// to avoid inconsistencies in the association state. - /// Do not call `send` and `receive` while not in a PDU boundary. - pub fn inner_stream(&mut self) -> &mut std::net::TcpStream { - &mut self.socket + /// Read a PDU message from the other intervenient. + fn receive(&mut self) -> Result { + read_pdu_from_wire(&mut self.socket, &mut self.read_buffer, self.requestor_max_pdu_length, self.strict) } - /// Prepare a P-Data writer for sending - /// one or more data items. - /// - /// Returns a writer which automatically - /// splits the inner data into separate PDUs if necessary. - pub fn send_pdata( - &mut self, - presentation_context_id: u8, - ) -> PDataWriter<&mut std::net::TcpStream> { - PDataWriter::new( - &mut self.socket, - presentation_context_id, - self.acceptor_max_pdu_length, - ) + fn close(&mut self) -> std::io::Result<()> { + self.socket.close() } +} - /// Prepare a P-Data reader for receiving - /// one or more data item PDUs. - /// - /// Returns a reader which automatically - /// receives more data PDUs once the bytes collected are consumed. - pub fn receive_pdata(&mut self) -> PDataReader<'_, &mut std::net::TcpStream> { - PDataReader::new( - &mut self.socket, - self.requestor_max_pdu_length, - &mut self.read_buffer, - ) +impl SyncAssociation for ClientAssociation +where S: CloseSocket + std::io::Read + std::io::Write{ + fn inner_stream(&mut self) -> &mut S { + &mut self.socket } - /// Release implementation function, - /// which tries to send a release request and receive a release response. - /// This is in a separate private function because - /// terminating a connection should still close the connection - /// if the exchange fails. - fn release_impl(&mut self) -> Result<()> { - let pdu = Pdu::ReleaseRQ; - self.send(&pdu)?; - let pdu = self.receive()?; - - match pdu { - Pdu::ReleaseRP => {} - pdu @ Pdu::AbortRQ { .. } - | pdu @ Pdu::AssociationAC { .. } - | pdu @ Pdu::AssociationRJ { .. } - | pdu @ Pdu::AssociationRQ { .. } - | pdu @ Pdu::PData { .. } - | pdu @ Pdu::ReleaseRQ => return UnexpectedPduSnafu { pdu }.fail(), - pdu @ Pdu::Unknown { .. } => return UnknownPduSnafu { pdu }.fail(), - } - Ok(()) + fn get_mut(&mut self) -> (&mut S, &mut BytesMut) { + let Self { socket, read_buffer, .. } = self; + (socket, read_buffer) } } /// Automatically release the association and shut down the connection. -impl Drop for ClientAssociation -where - T: CloseSocket, - ClientAssociation: Release, +impl Drop for ClientAssociation +where S: CloseSocket + std::io::Read + std::io::Write, { fn drop(&mut self) { - let _ = self.release(); - let _ = self.socket.close(); + let _ = SyncAssociationSealed::release(self); } } #[cfg(feature = "async")] -pub mod non_blocking { - use std::{convert::TryInto, future::Future, time::Duration}; - - use super::{CloseSocket, Release, Result}; - use crate::association::{ - ClientAssociation, ClientAssociationOptions, SendTooLongPduSnafu, TimeoutSnafu, - }; - use crate::pdu::{DEFAULT_MAX_PDU, LARGE_PDU_SIZE, PDU_HEADER_SIZE}; - use crate::{ - association::{ - client::{ConnectSnafu, NegotiatedOptions, ToAddressSnafu, WireSendSnafu}, - pdata::non_blocking::{AsyncPDataWriter, PDataReader}, - read_pdu_from_wire_async, SendPduSnafu, UnexpectedPduSnafu, UnknownPduSnafu, - }, - pdu::AbortRQSource, - write_pdu, AeAddr, Pdu, - }; +/// Initiate simple TCP connection to the given address +pub async fn async_connection( + ae_address: &AeAddr, + opts: &SocketOptions, +) -> Result where T: tokio::net::ToSocketAddrs{ + super::timeout(opts.connection_timeout, async { + tokio::net::TcpStream::connect(ae_address.socket_addr()) + .await + .context(crate::association::ConnectSnafu) + }).await +} - use bytes::BytesMut; - use snafu::ResultExt; - use tokio::io::AsyncWriteExt; - - // Helper function to perform an operation with timeout - async fn timeout( - timeout: Option, - block: impl Future>, - ) -> Result { - if let Some(timeout) = timeout { - tokio::time::timeout(timeout, block) - .await - .map_err(|_| std::io::Error::from(std::io::ErrorKind::TimedOut)) - .context(TimeoutSnafu)? - } else { - block.await - } - } +/// Initiate TLS connection to the given address +#[cfg(feature = "async-tls")] +pub(crate) async fn async_tls_connection( + ae_address: &AeAddr, + server_name: &str, + opts: &SocketOptions, + tls_config: std::sync::Arc, +) -> Result +where + T: tokio::net::ToSocketAddrs, +{ + use std::convert::TryFrom; + use rustls::pki_types::ServerName; + + let tcp_stream = async_connection(ae_address, opts).await?; + let connector = tokio_rustls::TlsConnector::from(tls_config); + let domain = ServerName::try_from(server_name.to_string()) + .context(crate::association::InvalidServerNameSnafu)?; + // NOTE: When tokio-rustls is updated to return a rustls::Error instead of std::io::Error, + // switch to `crate::association::TlsConnectionSnafu` for context. + let tls_stream = connector + .connect(domain, tcp_stream) + .await + .context(crate::association::ConnectSnafu)?; + Ok(tls_stream) +} - impl<'a> ClientAssociationOptions<'a> { - pub(crate) async fn async_simple_tcp_connection( - &self, - ae_address: AeAddr, - ) -> Result - where - T: tokio::net::ToSocketAddrs, - { - let conn_result: Result = - if let Some(timeout) = self.connection_timeout { - let addresses = tokio::net::lookup_host(ae_address.socket_addr()) - .await - .context(ToAddressSnafu)?; - - let mut result: Result = - Result::Err(std::io::Error::from(std::io::ErrorKind::AddrNotAvailable)); - - for address in addresses { - result = match tokio::time::timeout( - timeout, - tokio::net::TcpStream::connect(&address), - ) - .await - { - Ok(inner) => inner, - Err(_) => result, - }; - if result.is_ok() { - break; - } - } - result.context(ConnectSnafu) - } else { - tokio::net::TcpStream::connect(ae_address.socket_addr()) - .await - .context(ConnectSnafu) - }; +#[cfg(feature = "async")] +#[derive(Debug)] +pub struct AsyncClientAssociation +where S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send, +{ + /// The presentation contexts accorded with the acceptor application entity, + /// without the rejected ones. + presentation_contexts: Vec, + /// The maximum PDU length that this application entity is expecting to receive + requestor_max_pdu_length: u32, + /// The maximum PDU length that the remote application entity accepts + acceptor_max_pdu_length: u32, + /// The TCP stream to the other DICOM node + socket: S, + /// Buffer to assemble PDU before sending it on wire + write_buffer: Vec, + /// whether to receive PDUs in strict mode + strict: bool, + /// Timeout for individual socket Reads + read_timeout: Option, + /// Timeout for individual socket Writes. + write_timeout: Option, + /// Buffer to assemble PDU before parsing + read_buffer: BytesMut, + /// User variables that were taken from the server + user_variables: Vec, + /// The AE title of the peer + peer_ae_title: String, +} - conn_result - } +#[cfg(feature = "async")] +impl<'a> ClientAssociationOptions<'a> { + async fn establish_impl_async( + self, + ae_address: AeAddr, + mut socket: S + ) -> Result> + where + T: tokio::net::ToSocketAddrs, + S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send, + { + use tokio::io::AsyncWriteExt; + let (pc_proposed, a_associate) = self.create_a_associate_req(ae_address.ae_title())?; + let mut write_buffer: Vec = Vec::with_capacity(DEFAULT_MAX_PDU as usize); - async fn establish_impl_async( - self, - ae_address: AeAddr, - ) -> Result> - where - T: tokio::net::ToSocketAddrs, - { - let (pc_proposed, a_associate) = self.create_a_associate_req(ae_address.ae_title())?; - let mut socket = self.async_simple_tcp_connection(ae_address).await?; - let mut write_buffer: Vec = Vec::with_capacity(DEFAULT_MAX_PDU as usize); + // send request + write_pdu(&mut write_buffer, &a_associate) + .context(crate::association::SendPduSnafu)?; + super::timeout(self.socket_options.write_timeout, async { + socket.write_all(&write_buffer) + .await + .context(crate::association::WireSendSnafu)?; + Ok(()) + }).await?; + write_buffer.clear(); - // send request - write_pdu(&mut write_buffer, &a_associate).context(SendPduSnafu)?; - timeout(self.write_timeout, async { - socket - .write_all(&write_buffer) + // read buffer is prepared according to the requestor's max pdu length + let mut read_buffer = BytesMut::with_capacity( + (self.max_pdu_length.min(LARGE_PDU_SIZE) + PDU_HEADER_SIZE) as usize, + ); + let resp = super::timeout(self.socket_options.read_timeout, async { + super::read_pdu_from_wire_async(&mut socket, &mut read_buffer, self.max_pdu_length, self.strict).await + }) + .await?; + let negotiated_options = self.process_a_association_resp(resp, &pc_proposed); + match negotiated_options { + Err(e) => { + // abort connection + let _ = write_pdu( + &mut write_buffer, + &Pdu::AbortRQ { + source: AbortRQSource::ServiceUser, + }, + ); + socket.write_all(&write_buffer) .await - .context(WireSendSnafu)?; - Ok(()) - }) - .await?; - write_buffer.clear(); - - // read buffer is prepared according to the requestor's max pdu length - let mut read_buffer = BytesMut::with_capacity( - (self.max_pdu_length.min(LARGE_PDU_SIZE) + PDU_HEADER_SIZE) as usize, - ); - let resp = timeout(self.read_timeout, async { - read_pdu_from_wire_async( - &mut socket, - &mut read_buffer, - self.max_pdu_length, - self.strict, - ) - .await - }) - .await?; - let negotiated_options = self.process_a_association_resp(resp, &pc_proposed); - match negotiated_options { - Err(e) => { - // abort connection - let _ = write_pdu( - &mut write_buffer, - &Pdu::AbortRQ { - source: AbortRQSource::ServiceUser, - }, - ); - socket - .write_all(&write_buffer) - .await - .context(WireSendSnafu)?; - write_buffer.clear(); - Err(e) - } - Ok(NegotiatedOptions { + .context(crate::association::WireSendSnafu)?; + write_buffer.clear(); + Err(e) + }, + Ok(NegotiatedOptions{presentation_contexts, peer_max_pdu_length, user_variables, peer_ae_title}) => { + Ok(AsyncClientAssociation { presentation_contexts, - peer_max_pdu_length, + requestor_max_pdu_length: self.max_pdu_length, + acceptor_max_pdu_length: peer_max_pdu_length, + socket, + write_buffer, + strict: self.strict, + // Fixes #589, instead of creating a new buffer, we pass the existing buffer into the Association object. + read_buffer, + read_timeout: self.socket_options.read_timeout, + write_timeout: self.socket_options.write_timeout, user_variables, - }) => { - Ok(ClientAssociation { - presentation_contexts, - requestor_max_pdu_length: self.max_pdu_length, - acceptor_max_pdu_length: peer_max_pdu_length, - socket, - buffer: write_buffer, - strict: self.strict, - // retain the existing read buffer so that no PDUs are lost - read_buffer, - read_timeout: self.read_timeout, - write_timeout: self.write_timeout, - user_variables, - }) - } + peer_ae_title + }) } } + } - /// Initiate the TCP connection to the given address - /// and request a new DICOM association, - /// negotiating the presentation contexts in the process. - pub async fn establish_async( - self, - address: A, - ) -> Result> { - self.establish_impl_async(AeAddr::new_socket_addr(address)) - .await + /// Initiate the TCP connection to the given address + /// and request a new DICOM association, + /// negotiating the presentation contexts in the process. + pub async fn establish_async( + self, + address: A, + ) -> Result> { + let addr = AeAddr::new_socket_addr(address); + let socket = async_connection(&addr, &self.socket_options).await?; + self.establish_impl_async(addr, socket) + .await + } + + /// Initiate the TCP connection to the given address + /// and request a new DICOM association, + /// negotiating the presentation contexts in the process. + #[cfg(feature = "async-tls")] + pub async fn establish_tls_async( + self, + address: A, + ) -> Result> { + match (&self.tls_config, &self.server_name) { + (Some(tls_config), Some(server_name)) => { + let addr = AeAddr::new_socket_addr(address); + let socket = async_tls_connection( + &addr, server_name, &self.socket_options, tls_config.clone() + ).await?; + self.establish_impl_async(addr, socket) + .await + }, + _ => crate::association::TlsConfigMissingSnafu.fail()? } + } - /// Initiate the TCP connection to the given address - /// and request a new DICOM association, - /// negotiating the presentation contexts in the process. - /// - /// This method allows you to specify the called AE title - /// alongside with the socket address. - /// See [AeAddr](`crate::AeAddr`) for more details. - /// However, the AE title in this parameter - /// is overridden by any `called_ae_title` option - /// previously received. - /// - /// # Example - /// - /// ```no_run - /// # use dicom_ul::association::client::ClientAssociationOptions; - /// #[tokio::main] - /// # async fn run() -> Result<(), Box> { - /// let association = ClientAssociationOptions::new() - /// .with_abstract_syntax("1.2.840.10008.1.1") - /// // called AE title in address - /// .establish_with_async("MY-STORAGE@10.0.0.100:104") - /// .await?; - /// # Ok(()) - /// # } - /// ``` - #[allow(unreachable_patterns)] - pub async fn establish_with_async( - self, - ae_address: &str, - ) -> Result> { - match ae_address.try_into() { - Ok(ae_address) => self.establish_impl_async(ae_address).await, - Err(_) => { - self.establish_impl_async(AeAddr::new_socket_addr(ae_address)) - .await - } + /// Initiate async TCP connection to the given address + /// and request a new DICOM association, + /// negotiating the presentation contexts in the process. + /// + /// This method allows you to specify the called AE title + /// alongside with the socket address. + /// See [AeAddr](`crate::AeAddr`) for more details. + /// However, the AE title in this parameter + /// is overridden by any `called_ae_title` option + /// previously received. + /// + /// # Example + /// + /// ```no_run + /// # use dicom_ul::association::client::ClientAssociationOptions; + /// #[tokio::main] + /// # async fn run() -> Result<(), Box> { + /// let association = ClientAssociationOptions::new() + /// .with_abstract_syntax("1.2.840.10008.1.1") + /// // called AE title in address + /// .establish_with_async("MY-STORAGE@10.0.0.100:104") + /// .await?; + /// # Ok(()) + /// # } + /// ``` + #[allow(unreachable_patterns)] + pub async fn establish_with_async( + self, + ae_address: &str, + ) -> Result> { + match ae_address.try_into() { + Ok(ae_address) => { + let socket = async_connection(&ae_address, &self.socket_options).await?; + self.establish_impl_async(ae_address, socket).await + }, + Err(_) => { + let addr = AeAddr::new_socket_addr(ae_address); + let socket = async_connection(&addr, &self.socket_options).await?; + self.establish_impl_async(addr, socket) + .await } } } - impl ClientAssociation - where - ClientAssociation: Release, - { - /// Send a PDU message to the other intervenient. - pub async fn send(&mut self, msg: &Pdu) -> Result<()> { - self.buffer.clear(); - write_pdu(&mut self.buffer, msg).context(SendPduSnafu)?; - if self.buffer.len() > (self.acceptor_max_pdu_length + PDU_HEADER_SIZE) as usize { - return SendTooLongPduSnafu { - length: self.buffer.len(), + /// Initiate async TLS connection to the given address + /// and request a new DICOM association, + /// negotiating the presentation contexts in the process. + /// + /// This method allows you to specify the called AE title + /// alongside with the socket address. + /// See [AeAddr](`crate::AeAddr`) for more details. + /// However, the AE title in this parameter + /// is overridden by any `called_ae_title` option + /// previously received. + /// + /// # Example + /// + /// ```no_run + /// # use dicom_ul::association::client::ClientAssociationOptions; + /// #[tokio::main] + /// # async fn run() -> Result<(), Box> { + /// let association = ClientAssociationOptions::new() + /// .with_abstract_syntax("1.2.840.10008.1.1") + /// // called AE title in address + /// .establish_with_async_tls("MY-STORAGE@10.0.0.100:104") + /// .await?; + /// # Ok(()) + /// # } + /// ``` + #[cfg(feature = "async-tls")] + #[allow(unreachable_patterns)] + pub async fn establish_with_async_tls( + self, + ae_address: &str, + ) -> Result> { + match (&self.tls_config, &self.server_name) { + (Some(tls_config), Some(server_name)) => { + match ae_address.try_into() { + Ok(ae_address) => { + let socket = async_tls_connection( + &ae_address, server_name, &self.socket_options, tls_config.clone() + ).await?; + self.establish_impl_async(ae_address, socket).await + }, + Err(_) => { + let addr = AeAddr::new_socket_addr(ae_address); + let socket = async_tls_connection( + &addr, server_name, &self.socket_options, tls_config.clone() + ).await?; + self.establish_impl_async(addr, socket).await + }, } - .fail(); - } - timeout(self.write_timeout, async { - self.socket - .write_all(&self.buffer) - .await - .context(WireSendSnafu) - }) - .await - } - /// Read a PDU message from the other intervenient. - pub async fn receive(&mut self) -> Result { - timeout(self.read_timeout, async { - read_pdu_from_wire_async( - &mut self.socket, - &mut self.read_buffer, - self.requestor_max_pdu_length, - self.strict, - ) - .await - }) - .await + }, + _ => crate::association::TlsConfigMissingSnafu.fail()? } + } +} - /// Gracefully terminate the association by exchanging release messages - /// and then shutting down the TCP connection. - pub async fn release(mut self) -> Result<()> { - timeout(self.write_timeout, async { - let out = self.release_impl().await; - let _ = self.socket.shutdown().await; - out - }) - .await - } +#[cfg(feature = "async")] +impl Association for AsyncClientAssociation +where S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send, +{ + fn peer_ae_title(&self) -> &str { + &self.peer_ae_title + } - /// Send an abort message and shut down the TCP connection, - /// terminating the association. - pub async fn abort(mut self) -> Result<()> { - timeout(self.write_timeout, async { - let pdu = Pdu::AbortRQ { - source: AbortRQSource::ServiceUser, - }; - let out = self.send(&pdu).await; - let _ = self.socket.shutdown().await; - out - }) - .await - } + fn acceptor_max_pdu_length(&self) -> u32 { + self.acceptor_max_pdu_length + } - /// Prepare a P-Data writer for sending - /// one or more data items. - /// - /// Returns a writer which automatically - /// splits the inner data into separate PDUs if necessary. - pub async fn send_pdata( - &mut self, - presentation_context_id: u8, - ) -> AsyncPDataWriter<&mut tokio::net::TcpStream> { - AsyncPDataWriter::new( - &mut self.socket, - presentation_context_id, - self.acceptor_max_pdu_length, - ) - } + fn requestor_max_pdu_length(&self) -> u32 { + self.requestor_max_pdu_length + } - /// Prepare a P-Data reader for receiving - /// one or more data item PDUs. - /// - /// Returns a reader which automatically - /// receives more data PDUs once the bytes collected are consumed. - #[cfg(feature = "async")] - pub fn receive_pdata(&mut self) -> PDataReader<'_, &mut tokio::net::TcpStream> { - PDataReader::new( + fn presentation_contexts(&self) -> &[PresentationContextNegotiated] { + &self.presentation_contexts + } + + fn user_variables(&self) -> &[UserVariableItem] { + &self.user_variables + } +} + +#[cfg(feature = "async")] +impl AsyncClientAssociation +where S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send, +{ + /// Retrieve read timeout for the association + pub fn read_timeout(&self) -> Option { + self.read_timeout + } + + /// Retrieve write timeout for the association + pub fn write_timeout(&self) -> Option { + self.write_timeout + } +} + +#[cfg(feature = "async")] +impl super::private::AsyncAssociationSealed for AsyncClientAssociation +where S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send, +{ + async fn send(&mut self, msg: &Pdu) -> Result<()> { + use tokio::io::AsyncWriteExt; + + self.write_buffer.clear(); + encode_pdu(&mut self.write_buffer, msg, self.acceptor_max_pdu_length + PDU_HEADER_SIZE)?; + super::timeout(self.write_timeout, async { + self.socket + .write_all(&self.write_buffer) + .await + .context(crate::association::WireSendSnafu) + }) + .await + } + + async fn receive(&mut self) -> Result { + use crate::association::read_pdu_from_wire_async; + super::timeout(self.read_timeout, async { + read_pdu_from_wire_async( &mut self.socket, - self.requestor_max_pdu_length, &mut self.read_buffer, - ) - } + self.requestor_max_pdu_length, + self.strict + ).await + }) + .await + } - /// Release implementation function, - /// which tries to send a release request and receive a release response. - /// This is in a separate private function because - /// terminating a connection should still close the connection - /// if the exchange fails. - async fn release_impl(&mut self) -> Result<()> { - let pdu = Pdu::ReleaseRQ; - self.send(&pdu).await?; - let pdu = self.receive().await?; - match pdu { - Pdu::ReleaseRP => {} - pdu @ Pdu::AbortRQ { .. } - | pdu @ Pdu::AssociationAC { .. } - | pdu @ Pdu::AssociationRJ { .. } - | pdu @ Pdu::AssociationRQ { .. } - | pdu @ Pdu::PData { .. } - | pdu @ Pdu::ReleaseRQ => return UnexpectedPduSnafu { pdu }.fail(), - pdu @ Pdu::Unknown { .. } => return UnknownPduSnafu { pdu }.fail(), - } - Ok(()) - } - /// Obtain access to the inner TCP stream - /// connected to the association acceptor. - /// - /// This can be used to send the PDU in semantic fragments of the message, - /// thus using less memory. - /// - /// **Note:** reading and writing should be done with care - /// to avoid inconsistencies in the association state. - /// Do not call `send` and `receive` while not in a PDU boundary. - pub fn inner_stream(&mut self) -> &mut tokio::net::TcpStream { - &mut self.socket - } + async fn close(&mut self) -> std::io::Result<()> { + use tokio::io::AsyncWriteExt; + self.socket.shutdown().await } +} - impl Release for ClientAssociation { - fn release(&mut self) -> super::Result<()> { - tokio::task::block_in_place(move || { - tokio::runtime::Handle::current().block_on(async move { self.release_impl().await }) - }) - } +#[cfg(feature = "async")] +impl super::AsyncAssociation for AsyncClientAssociation +where S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send{ + + fn inner_stream(&mut self) -> &mut S { + &mut self.socket + } + + fn get_mut(&mut self) -> (&mut S, &mut BytesMut) { + let Self { socket, read_buffer, .. } = self; + (socket, read_buffer) } - /// Automatically release the association and shut down the connection. - impl CloseSocket for tokio::net::TcpStream { - fn close(&mut self) -> std::io::Result<()> { - tokio::task::block_in_place(move || { - tokio::runtime::Handle::current().block_on(async move { self.shutdown().await }) +} + +#[cfg(feature = "async")] +impl Drop for AsyncClientAssociation +where S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send, +{ + fn drop(&mut self) { + tokio::task::block_in_place(move || { + tokio::runtime::Handle::current().block_on(async move { + let _ = crate::association::private::AsyncAssociationSealed::release(self).await; }) - } + }); } } @@ -1327,9 +1429,8 @@ mod tests { use super::*; #[cfg(feature = "async")] use crate::association::read_pdu_from_wire_async; - use crate::pdu::LARGE_PDU_SIZE; - #[cfg(feature = "async")] - use tokio::io::AsyncWriteExt; + use std::io::Write; + impl<'a> ClientAssociationOptions<'a> { pub(crate) fn establish_with_extra_pdus( @@ -1341,15 +1442,15 @@ mod tests { T: ToSocketAddrs, { let (pc_proposed, a_associate) = self.create_a_associate_req(ae_address.ae_title())?; - let mut socket = self.simple_tcp_connection(ae_address)?; + let mut socket = tcp_connection(&ae_address, &self.socket_options)?; let mut write_buffer: Vec = Vec::with_capacity(DEFAULT_MAX_PDU as usize); // send request - write_pdu(&mut write_buffer, &a_associate).context(SendPduSnafu)?; + write_pdu(&mut write_buffer, &a_associate).context(crate::association::SendPduSnafu)?; for pdu in extra_pdus { - write_pdu(&mut write_buffer, &pdu).context(SendPduSnafu)?; + write_pdu(&mut write_buffer, &pdu).context(crate::association::SendPduSnafu)?; } - socket.write_all(&write_buffer).context(WireSendSnafu)?; + socket.write_all(&write_buffer).context(crate::association::WireSendSnafu)?; write_buffer.clear(); let mut read_buffer = BytesMut::with_capacity( @@ -1365,6 +1466,7 @@ mod tests { presentation_contexts, peer_max_pdu_length, user_variables, + peer_ae_title } = self .process_a_association_resp(resp, &pc_proposed) .expect("Failed to process a associate response"); @@ -1373,13 +1475,14 @@ mod tests { requestor_max_pdu_length: self.max_pdu_length, acceptor_max_pdu_length: peer_max_pdu_length, socket, - buffer: write_buffer, + write_buffer, strict: self.strict, // Fixes #589, instead of creating a new buffer, we pass the existing buffer into the Association object. read_buffer, - read_timeout: self.read_timeout, - write_timeout: self.write_timeout, + read_timeout: self.socket_options.read_timeout, + write_timeout: self.socket_options.write_timeout, user_variables, + peer_ae_title }) } @@ -1388,20 +1491,22 @@ mod tests { &self, ae_address: AeAddr, extra_pdus: Vec, - ) -> Result> + ) -> Result> where T: tokio::net::ToSocketAddrs, { + use tokio::io::AsyncWriteExt; + let (pc_proposed, a_associate) = self.create_a_associate_req(ae_address.ae_title())?; - let mut socket = self.async_simple_tcp_connection(ae_address).await?; + let mut socket = async_connection(&ae_address, &self.socket_options).await?; let mut buffer: Vec = Vec::with_capacity(DEFAULT_MAX_PDU as usize); // send request - write_pdu(&mut buffer, &a_associate).context(SendPduSnafu)?; + write_pdu(&mut buffer, &a_associate).context(crate::association::SendPduSnafu)?; for pdu in extra_pdus { - write_pdu(&mut buffer, &pdu).context(SendPduSnafu)?; + write_pdu(&mut buffer, &pdu).context(crate::association::SendPduSnafu)?; } - socket.write_all(&buffer).await.context(WireSendSnafu)?; + socket.write_all(&buffer).await.context(crate::association::WireSendSnafu)?; buffer.clear(); let mut buf = BytesMut::with_capacity( @@ -1414,21 +1519,23 @@ mod tests { presentation_contexts, peer_max_pdu_length, user_variables, + peer_ae_title } = self .process_a_association_resp(resp, &pc_proposed) .expect("Failed to process a associate response"); - Ok(ClientAssociation { + Ok(AsyncClientAssociation { presentation_contexts, requestor_max_pdu_length: self.max_pdu_length, acceptor_max_pdu_length: peer_max_pdu_length, socket, - buffer, + write_buffer: buffer, strict: self.strict, // Fixes #589, instead of creating a new buffer, we pass the existing buffer into the Association object. read_buffer: buf, - read_timeout: self.read_timeout, - write_timeout: self.write_timeout, + read_timeout: self.socket_options.read_timeout, + write_timeout: self.socket_options.write_timeout, user_variables, + peer_ae_title }) } @@ -1441,11 +1548,11 @@ mod tests { T: ToSocketAddrs, { let (pc_proposed, a_associate) = self.create_a_associate_req(ae_address.ae_title())?; - let mut socket = self.simple_tcp_connection(ae_address)?; + let mut socket = tcp_connection(&ae_address, &self.socket_options)?; let mut buffer: Vec = Vec::with_capacity(DEFAULT_MAX_PDU as usize); // send request - write_pdu(&mut buffer, &a_associate).context(SendPduSnafu)?; - socket.write_all(&buffer).context(WireSendSnafu)?; + write_pdu(&mut buffer, &a_associate).context(crate::association::SendPduSnafu)?; + socket.write_all(&buffer).context(crate::association::WireSendSnafu)?; buffer.clear(); let mut buf = BytesMut::with_capacity( @@ -1456,6 +1563,7 @@ mod tests { presentation_contexts, peer_max_pdu_length, user_variables, + peer_ae_title } = self .process_a_association_resp(resp, &pc_proposed) .expect("Failed to process a associate response"); @@ -1464,14 +1572,15 @@ mod tests { requestor_max_pdu_length: self.max_pdu_length, acceptor_max_pdu_length: peer_max_pdu_length, socket, - buffer, + write_buffer: buffer, strict: self.strict, read_buffer: BytesMut::with_capacity( (self.max_pdu_length.min(LARGE_PDU_SIZE) + PDU_HEADER_SIZE) as usize, ), - read_timeout: self.read_timeout, - write_timeout: self.write_timeout, + read_timeout: self.socket_options.read_timeout, + write_timeout: self.socket_options.write_timeout, user_variables, + peer_ae_title }) } @@ -1480,16 +1589,18 @@ mod tests { pub async fn broken_establish_async( &self, ae_address: AeAddr, - ) -> Result> + ) -> Result> where T: tokio::net::ToSocketAddrs, { + use tokio::io::AsyncWriteExt; + let (pc_proposed, a_associate) = self.create_a_associate_req(ae_address.ae_title())?; - let mut socket = self.async_simple_tcp_connection(ae_address).await?; + let mut socket = async_connection(&ae_address, &self.socket_options).await?; let mut buffer: Vec = Vec::with_capacity(DEFAULT_MAX_PDU as usize); // send request - write_pdu(&mut buffer, &a_associate).context(SendPduSnafu)?; - socket.write_all(&buffer).await.context(WireSendSnafu)?; + write_pdu(&mut buffer, &a_associate).context(crate::association::SendPduSnafu)?; + socket.write_all(&buffer).await.context(crate::association::WireSendSnafu)?; buffer.clear(); let mut buf = BytesMut::with_capacity( @@ -1502,22 +1613,24 @@ mod tests { presentation_contexts, peer_max_pdu_length, user_variables, + peer_ae_title } = self .process_a_association_resp(resp, &pc_proposed) .expect("Failed to process a associate response"); - Ok(ClientAssociation { + Ok(AsyncClientAssociation { presentation_contexts, requestor_max_pdu_length: self.max_pdu_length, acceptor_max_pdu_length: peer_max_pdu_length, socket, - buffer, + write_buffer: buffer, strict: self.strict, read_buffer: BytesMut::with_capacity( (self.max_pdu_length.min(LARGE_PDU_SIZE) + PDU_HEADER_SIZE) as usize, ), - read_timeout: self.read_timeout, - write_timeout: self.write_timeout, + read_timeout: self.socket_options.read_timeout, + write_timeout: self.socket_options.write_timeout, user_variables, + peer_ae_title }) } } diff --git a/ul/src/association/mod.rs b/ul/src/association/mod.rs index a9e5c353..e3882e29 100644 --- a/ul/src/association/mod.rs +++ b/ul/src/association/mod.rs @@ -27,27 +27,22 @@ pub(crate) mod pdata; use std::{ backtrace::Backtrace, - io::{BufRead, BufReader, Cursor, Read}, + io::{BufRead, BufReader, Cursor, Read}, time::Duration, }; use bytes::{Buf, BytesMut}; +pub use pdata::{PDataReader, PDataWriter}; pub use client::{ClientAssociation, ClientAssociationOptions}; +pub use server::{ServerAssociation, ServerAssociationOptions}; #[cfg(feature = "async")] pub use pdata::non_blocking::AsyncPDataWriter; -pub use pdata::{PDataReader, PDataWriter}; -pub use server::{ServerAssociation, ServerAssociationOptions}; +#[cfg(feature = "async")] +pub use client::AsyncClientAssociation; +#[cfg(feature = "async")] +pub use server::AsyncServerAssociation; use snafu::{ensure, ResultExt, Snafu}; -use crate::{ - pdu::{self, AssociationRJ, PresentationContextNegotiated, ReadPduSnafu, UserVariableItem}, - Pdu, -}; - -pub(crate) struct NegotiatedOptions { - peer_max_pdu_length: u32, - user_variables: Vec, - presentation_contexts: Vec, -} +use crate::{Pdu, pdu::{self, AssociationRJ, PresentationContextNegotiated, ReadPduSnafu, UserVariableItem}, write_pdu}; type Result = std::result::Result; @@ -150,6 +145,12 @@ pub enum Error { backtrace: Backtrace, }, + #[snafu(display("failed close connection: {}", source))] + Close { + source: std::io::Error, + backtrace: Backtrace + }, + #[snafu(display( "PDU is too large ({} bytes) to be sent to the remote application entity", length @@ -159,6 +160,386 @@ pub enum Error { #[snafu(display("Connection closed by peer"))] ConnectionClosed, + + /// TLS configuration is missing + #[cfg(feature = "sync-tls")] + #[snafu(display("TLS configuration is required but not provided"))] + TlsConfigMissing { backtrace: Backtrace }, + + /// Invalid server name for TLS + #[cfg(feature = "sync-tls")] + #[snafu(display("Invalid server name for TLS connection"))] + InvalidServerName { + source: rustls::pki_types::InvalidDnsNameError, + backtrace: Backtrace + }, + + /// Failed to establish TLS connection + #[cfg(feature = "sync-tls")] + #[snafu(display("Failed to establish TLS connection: {:?}", source))] + TlsConnection { + source: rustls::Error, + backtrace: Backtrace + }, +} +/// Struct to hold negotiated options after association is accepted +pub(crate) struct NegotiatedOptions{ + /// Maximum PDU length the peer can handle + peer_max_pdu_length: u32, + /// User variables accepted by the peer + user_variables: Vec, + /// Presentation contexts accepted by the peer + presentation_contexts: Vec, + /// The peer's AE title + peer_ae_title: String +} + +/// Socket configuration for associations +#[derive(Debug, Clone, Copy, Default)] +pub struct SocketOptions { + /// Timeout for individual read operations + read_timeout: Option, + /// Timeout for individual send operations + write_timeout: Option, + /// Timeout for connection establishment + connection_timeout: Option, +} +/// Trait to close underlying socket +pub trait CloseSocket { + fn close(&mut self) -> std::io::Result<()>; +} + +impl CloseSocket for std::net::TcpStream { + fn close(&mut self) -> std::io::Result<()> { + self.shutdown(std::net::Shutdown::Both) + } +} + +#[cfg(feature = "sync-tls")] +impl CloseSocket for rustls::StreamOwned{ + fn close(&mut self) -> std::io::Result<()> { + self.get_mut().shutdown(std::net::Shutdown::Both) + } +} + +#[cfg(feature = "sync-tls")] +impl CloseSocket for rustls::StreamOwned{ + fn close(&mut self) -> std::io::Result<()> { + self.get_mut().shutdown(std::net::Shutdown::Both) + } +} + +/// Trait that represents common properties of an association +pub trait Association { + /// Obtain the remote DICOM node's application entity title. + fn peer_ae_title(&self) -> &str; + + /// Retrieve the maximum PDU length + /// admitted by the association acceptor. + fn acceptor_max_pdu_length(&self) -> u32; + + /// Retrieve the maximum PDU length + /// that this application entity is expecting to receive. + /// + /// The current implementation is not required to fail + /// and/or abort the association + /// if a larger PDU is received. + fn requestor_max_pdu_length(&self) -> u32; + + /// Obtain a view of the negotiated presentation contexts. + fn presentation_contexts(&self) -> &[PresentationContextNegotiated]; + + /// Retrieve the user variables that were taken from the server. + /// + /// It usually contains the maximum PDU length, + /// the implementation class UID, and the implementation version name. + fn user_variables(&self) -> &[UserVariableItem]; +} + +mod private { + use crate::{Pdu, pdu::{AbortRQServiceProviderReason, AbortRQSource}}; + use snafu::{ResultExt}; + + /// Private trait which exposes "unsafe" methods that should not be called by the user + /// + /// `close` and `release` _should_ take ownership, and in the public interface, they + /// do. However, in order to implement `Drop` we need to expose a version of these + /// methods that don't take ownership. + /// + /// `send` and `receive` implementations are needed in order to provide + /// the implementation for `release` + pub trait SyncAssociationSealed { + + fn close(&mut self) -> std::io::Result<()>; + fn send(&mut self, pdu: &Pdu) -> super::Result<()>; + fn receive(&mut self) -> super::Result; + fn release(&mut self) -> super::Result<()>{ + let pdu = Pdu::ReleaseRQ; + self.send(&pdu)?; + let pdu = self.receive()?; + + match pdu { + Pdu::ReleaseRP => {} + pdu @ Pdu::AbortRQ { .. } + | pdu @ Pdu::AssociationAC { .. } + | pdu @ Pdu::AssociationRJ { .. } + | pdu @ Pdu::AssociationRQ { .. } + | pdu @ Pdu::PData { .. } + | pdu @ Pdu::ReleaseRQ => return super::UnexpectedPduSnafu { pdu }.fail(), + pdu @ Pdu::Unknown { .. } => return super::UnknownPduSnafu { pdu }.fail(), + } + self.close() + .context(super::CloseSnafu)?; + Ok(()) + } + + fn abort(&mut self) -> super::Result<()> where Self: Sized { + let pdu = Pdu::AbortRQ { + source: AbortRQSource::ServiceProvider( + AbortRQServiceProviderReason::ReasonNotSpecified, + ), + }; + let out = self.send(&pdu); + let _ = self.close(); + out + } + } + + /// Private trait which exposes "unsafe" methods that should not be called by the user + /// + /// `close` and `release` _should_ take ownership, and in the public interface, they + /// do. However, in order to implement `Drop` we need to expose a version of these + /// methods that don't take ownership. + /// + /// `send` and `receive` implementations are needed in order to provide + /// the implementation for `release` + #[cfg(feature = "async")] + pub trait AsyncAssociationSealed { + fn close(&mut self) -> impl std::future::Future> + Send + where Self: Send; + fn send(&mut self, pdu: &Pdu) -> impl std::future::Future> + Send + where Self: Send; + fn receive(&mut self) -> impl std::future::Future> + Send + where Self: Send; + fn release(&mut self) -> impl std::future::Future> + Send + where Self: Send { + async move { + let pdu = Pdu::ReleaseRQ; + self.send(&pdu).await?; + let pdu = self.receive().await?; + + match pdu { + Pdu::ReleaseRP => {} + pdu @ Pdu::AbortRQ { .. } + | pdu @ Pdu::AssociationAC { .. } + | pdu @ Pdu::AssociationRJ { .. } + | pdu @ Pdu::AssociationRQ { .. } + | pdu @ Pdu::PData { .. } + | pdu @ Pdu::ReleaseRQ => return super::UnexpectedPduSnafu { pdu }.fail(), + pdu @ Pdu::Unknown { .. } => return super::UnknownPduSnafu { pdu }.fail(), + } + self.close() + .await + .context(super::CloseSnafu)?; + Ok(()) + } + } + + fn abort(&mut self) -> impl std::future::Future> + Send + where Self: Sized + Send { + let pdu = Pdu::AbortRQ { + source: AbortRQSource::ServiceProvider( + AbortRQServiceProviderReason::ReasonNotSpecified, + ), + }; + async move { + let out = self.send(&pdu).await; + let _ = self.close().await; + out + } + } + } +} + +/// Trait that represents methods that can be made on a synchronous association. +pub trait SyncAssociation: private::SyncAssociationSealed + Association { + + /// Obtain access to the inner stream + /// connected to the association acceptor. + /// + /// This can be used to send the PDU in semantic fragments of the message, + /// thus using less memory. + /// + /// **Note:** reading and writing should be done with care + /// to avoid inconsistencies in the association state. + /// Do not call `send` and `receive` while not in a PDU boundary. + fn inner_stream(&mut self) -> &mut S; + + /// Obtain mutable access to the inner stream and read buffer + fn get_mut(&mut self) -> (&mut S, &mut BytesMut); + + /// Send a PDU message to the other intervenient. + fn send(&mut self, pdu: &Pdu) -> Result<()>{ + private::SyncAssociationSealed::send(self, pdu) + } + + /// Read a PDU message from the other intervenient. + fn receive(&mut self) -> Result{ + private::SyncAssociationSealed::receive(self) + } + + /// Send a provider initiated abort message + /// and shut down the TCP connection, + /// terminating the association. + fn abort(mut self) -> Result<()> where Self: Sized { + private::SyncAssociationSealed::abort(&mut self) + } + + /// Iniate a graceful release of the association + fn release(mut self) -> Result<()> where Self: Sized{ + private::SyncAssociationSealed::release(&mut self) + } + + /// Prepare a P-Data writer for sending + /// one or more data item PDUs. + /// + /// Returns a writer which automatically + /// splits the inner data into separate PDUs if necessary. + fn send_pdata(&mut self, presentation_context_id: u8) -> PDataWriter<&mut S>{ + let max_pdu_length = self.acceptor_max_pdu_length(); + PDataWriter::new( + self.inner_stream(), + presentation_context_id, + max_pdu_length, + ) + + } + + /// Prepare a P-Data reader for receiving + /// one or more data item PDUs. + /// + /// Returns a reader which automatically + /// receives more data PDUs once the bytes collected are consumed. + fn receive_pdata(&mut self) -> PDataReader<'_, &mut S>{ + let max_pdu_length = self.requestor_max_pdu_length(); + let (socket, read_buffer) = self.get_mut(); + PDataReader::new( + socket, + max_pdu_length, + read_buffer, + ) + } +} + +#[cfg(feature = "async")] +/// Trait that represents methods that can be made on an asynchronous association. +pub trait AsyncAssociation: private::AsyncAssociationSealed + Association { + + /// Obtain access to the inner stream + /// connected to the association acceptor. + /// + /// This can be used to send the PDU in semantic fragments of the message, + /// thus using less memory. + /// + /// **Note:** reading and writing should be done with care + /// to avoid inconsistencies in the association state. + /// Do not call `send` and `receive` while not in a PDU boundary. + fn inner_stream(&mut self) -> &mut S; + + /// Obtain mutable access to the inner stream and read buffer + fn get_mut(&mut self) -> (&mut S, &mut BytesMut); + + /// Send a PDU message to the other intervenient. + fn send(&mut self, pdu: &Pdu) -> impl std::future::Future> + Send + where Self: Send { + async move{ + private::AsyncAssociationSealed::send(self, pdu).await + } + } + + /// Read a PDU message from the other intervenient. + fn receive(&mut self) -> impl std::future::Future> + Send + where Self: Send { + async move { + private::AsyncAssociationSealed::receive(self).await + } + } + + /// Send a provider initiated abort message + /// and shut down the TCP connection, + /// terminating the association. + fn abort(mut self) -> impl std::future::Future> + Send + where Self: Sized + Send { + async move { + private::AsyncAssociationSealed::abort(&mut self).await + } + } + + /// Iniate a graceful release of the association + fn release(mut self) -> impl std::future::Future> + Send + where Self: Sized + Send { + async move { + private::AsyncAssociationSealed::release(&mut self).await + } + } + + /// Prepare a P-Data writer for sending + /// one or more data item PDUs. + /// + /// Returns a writer which automatically + /// splits the inner data into separate PDUs if necessary. + fn send_pdata(&mut self, presentation_context_id: u8) -> AsyncPDataWriter<&mut S>{ + let max_pdu_length = self.acceptor_max_pdu_length(); + AsyncPDataWriter::new( + self.inner_stream(), + presentation_context_id, + max_pdu_length, + ) + + } + + /// Prepare a P-Data reader for receiving + /// one or more data item PDUs. + /// + /// Returns a reader which automatically + /// receives more data PDUs once the bytes collected are consumed. + fn receive_pdata(&mut self) -> PDataReader<'_, &mut S>{ + let max_pdu_length = self.requestor_max_pdu_length(); + let (socket, read_buffer) = self.get_mut(); + PDataReader::new( + socket, + max_pdu_length, + read_buffer, + ) + } +} + +// Helper function to perform an operation with timeout +#[cfg(feature = "async")] +async fn timeout( + timeout: Option, + block: impl std::future::Future>, +) -> Result { + if let Some(timeout) = timeout { + tokio::time::timeout(timeout, block) + .await + .map_err(|_| std::io::Error::from(std::io::ErrorKind::TimedOut)) + .context(crate::association::TimeoutSnafu)? + } else { + block.await + } +} + +/// Encode a PDU into the provided buffer +pub fn encode_pdu(buffer: &mut Vec, pdu: &Pdu, peer_max_pdu_length: u32) -> Result<()> { + write_pdu( buffer, pdu).context(SendPduSnafu)?; + if buffer.len() > peer_max_pdu_length as usize { + return SendTooLongPduSnafu { + length: buffer.len(), + } + .fail(); + } + Ok(()) } /// Helper function to get a PDU from a reader. @@ -202,21 +583,19 @@ where Ok(msg) } -#[cfg(feature = "async")] -use tokio::io::{AsyncRead, AsyncReadExt}; - /// Helper function to get a PDU from an async reader. /// /// Chunks of data are read into `read_buffer`, /// which should be passed in subsequent calls /// to receive more PDUs from the same stream. #[cfg(feature = "async")] -pub async fn read_pdu_from_wire_async( +pub async fn read_pdu_from_wire_async( reader: &mut R, read_buffer: &mut BytesMut, max_pdu_length: u32, strict: bool, ) -> Result { + use tokio::io::AsyncReadExt; // receive response let msg = loop { diff --git a/ul/src/association/pdata.rs b/ul/src/association/pdata.rs index 77dbaf49..b061a5e1 100644 --- a/ul/src/association/pdata.rs +++ b/ul/src/association/pdata.rs @@ -55,7 +55,7 @@ fn setup_pdata_header(buffer: &mut [u8], is_last: bool) { /// /// ```no_run /// # use std::io::Write; -/// # use dicom_ul::association::ClientAssociationOptions; +/// # use dicom_ul::association::{ClientAssociationOptions, Association, SyncAssociation}; /// # use dicom_ul::pdu::{Pdu, PDataValue, PDataValueType}; /// # fn command_data() -> Vec { unimplemented!() } /// # fn dicom_data() -> &'static [u8] { unimplemented!() } @@ -223,7 +223,7 @@ where /// /// ```no_run /// # use std::io::Read; -/// # use dicom_ul::association::ClientAssociationOptions; +/// # use dicom_ul::association::{ClientAssociationOptions, SyncAssociation}; /// # use dicom_ul::pdu::{Pdu, PDataValue, PDataValueType}; /// # fn command_data() -> Vec { unimplemented!() } /// # fn dicom_data() -> &'static [u8] { unimplemented!() } @@ -394,7 +394,7 @@ pub mod non_blocking { /// ```no_run /// # use std::io::Write; /// use tokio::io::AsyncWriteExt; - /// # use dicom_ul::association::ClientAssociationOptions; + /// # use dicom_ul::association::{ClientAssociationOptions, Association, AsyncAssociation}; /// # use dicom_ul::pdu::{Pdu, PDataValue, PDataValueType}; /// # fn command_data() -> Vec { unimplemented!() } /// # fn dicom_data() -> &'static [u8] { unimplemented!() } @@ -417,7 +417,7 @@ pub mod non_blocking { /// }).await; /// /// // then send a DICOM object which may be split into multiple PDUs - /// let mut pdata = association.send_pdata(presentation_context_id).await; + /// let mut pdata = association.send_pdata(presentation_context_id); /// pdata.write_all(dicom_data()).await?; /// pdata.finish().await?; /// diff --git a/ul/src/association/server.rs b/ul/src/association/server.rs index 865f126d..244ff039 100644 --- a/ul/src/association/server.rs +++ b/ul/src/association/server.rs @@ -9,14 +9,16 @@ use std::borrow::Cow; use std::time::Duration; use std::{io::Write, net::TcpStream}; +use dicom_encoding::transfer_syntax::TransferSyntaxIndex; +use dicom_transfer_syntax_registry::TransferSyntaxRegistry; +use snafu::{ensure, ResultExt}; +use crate::association::private::SyncAssociationSealed; use crate::association::{ + Association, CloseSocket, SocketOptions, SyncAssociation, encode_pdu, read_pdu_from_wire, AbortedSnafu, MissingAbstractSyntaxSnafu, RejectedSnafu, SendPduSnafu, - SendTooLongPduSnafu, SetReadTimeoutSnafu, SetWriteTimeoutSnafu, UnexpectedPduSnafu, + UnexpectedPduSnafu, UnknownPduSnafu, WireSendSnafu, }; -use dicom_encoding::transfer_syntax::TransferSyntaxIndex; -use dicom_transfer_syntax_registry::TransferSyntaxRegistry; -use snafu::{ensure, ResultExt}; use crate::association::NegotiatedOptions; use crate::pdu::{PresentationContextNegotiated, LARGE_PDU_SIZE}; @@ -31,12 +33,14 @@ use crate::{ }; use super::{ - pdata::{PDataReader, PDataWriter}, uid::trim_uid, - Error, + Error, Result }; -pub type Result = std::result::Result; +#[cfg(feature = "sync-tls")] +pub type TlsStream = rustls::StreamOwned; +#[cfg(feature = "async-tls")] +pub type AsyncTlsStream = tokio_rustls::server::TlsStream; /// Common interface for application entity access control policies. /// @@ -104,6 +108,31 @@ impl AccessControl for AcceptCalledAeTitle { /// a value of this type can be reused for multiple connections. /// /// [`ClientAssociationOptions`]: crate::association::ClientAssociationOptions +/// +/// The SCP will by default accept all transfer syntaxes +/// supported by the main [transfer syntax registry][1], +/// unless one or more transfer syntaxes are explicitly indicated +/// through calls to [`with_transfer_syntax`][2]. +/// +/// Access control logic is also available, +/// enabling application entities to decide on +/// whether to accept or reject the association request +/// based on the _called_ and _calling_ AE titles. +/// +/// - By default, the application will accept requests from anyone +/// ([`AcceptAny`]) +/// - To only accept requests with a matching _called_ AE title, +/// add a call to [`accept_called_ae_title`] +/// ([`AcceptCalledAeTitle`]). +/// - Any other policy can be implemented through the [`AccessControl`] trait. +/// +/// [`accept_called_ae_title`]: Self::accept_called_ae_title +/// [`AcceptAny`]: AcceptAny +/// [`AcceptCalledAeTitle`]: AcceptCalledAeTitle +/// [`AccessControl`]: AccessControl +/// +/// [1]: dicom_transfer_syntax_registry +/// [2]: ServerAssociationOptions::with_transfer_syntax /// /// ## Basic Usage /// @@ -124,17 +153,18 @@ impl AccessControl for AcceptCalledAeTitle { /// # Ok(()) /// # } /// ``` -/// +/// /// ### Async /// /// Spawn an async task for each incoming association request. /// /// ```no_run /// # use std::net::{Ipv4Addr, SocketAddrV4}; -/// # use dicom_ul::association::server::ServerAssociationOptions; +/// # use dicom_ul::association::{server::ServerAssociationOptions}; /// # #[cfg(feature = "async")] /// # #[tokio::main] /// # async fn main() -> Result<(), Box> { +/// # use dicom_ul::association::AsyncAssociation; /// let listen_addr = SocketAddrV4::new(Ipv4Addr::from(0), 11111); /// let listener = tokio::net::TcpListener::bind(listen_addr).await?; /// loop { @@ -174,31 +204,73 @@ impl AccessControl for AcceptCalledAeTitle { /// # #[cfg(not(feature = "async"))] /// fn main() {} /// ``` +/// +/// ## TLS Support +/// +/// ### Sync TLS +/// +/// * Make sure you include the `tls` feature in your `Cargo.toml` +/// +/// ### Async TLS +/// +/// * Make sure you include the `async-tls` feature in your `Cargo.toml` +/// +/// > **⚠️ Warning:** Just including the `async` and `tls` features will _not_ work! +/// +/// ### Example +/// ```no_compile +/// # // NOTE: cannot run on CI as assets are missing +/// # use dicom_ul::association::client::ClientAssociationOptions; +/// # use std::time::Duration; +/// # use std::sync::Arc; +/// # #[cfg(feature = "tls")] +/// # fn run() -> Result<(), Box> { +/// use rustls::{ +/// ClientConfig, RootCertStore, +/// pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject}, +/// }; +/// # let tcp_listener: TcpListener = unimplemented!(); +/// // Using a self-signed certificate for demonstration purposes only. +/// let ca_cert = CertificateDer::from_pem_slice(include_bytes!("../../assets/ca.crt").as_ref()) +/// .expect("Failed to load client cert"); +/// +/// // Server certificate -- signed by CA +/// let server_cert = CertificateDer::from_pem_slice(include_bytes!("../../assets/server.crt").as_ref()) +/// .expect("Failed to load server cert"); /// -/// The SCP will by default accept all transfer syntaxes -/// supported by the main [transfer syntax registry][1], -/// unless one or more transfer syntaxes are explicitly indicated -/// through calls to [`with_transfer_syntax`][2]. -/// -/// Access control logic is also available, -/// enabling application entities to decide on -/// whether to accept or reject the association request -/// based on the _called_ and _calling_ AE titles. -/// -/// - By default, the application will accept requests from anyone -/// ([`AcceptAny`]) -/// - To only accept requests with a matching _called_ AE title, -/// add a call to [`accept_called_ae_title`] -/// ([`AcceptCalledAeTitle`]). -/// - Any other policy can be implemented through the [`AccessControl`] trait. +/// // Client cert and private key -- signed by CA +/// let client_cert = CertificateDer::from_pem_slice(include_bytes!("../../assets/client.crt").as_ref()) +/// .expect("Failed to load client cert"); +/// let client_private_key = PrivateKeyDer::from_pem_slice(include_bytes!("../../assets/client.key").as_ref()) +/// .expect("Failed to load client private key"); +/// +/// // Create a root cert store for the client which includes the server certificate +/// let mut certs = RootCertStore::empty(); +/// certs.add_parsable_certificates(vec![ca_cert.clone()]); /// -/// [`accept_called_ae_title`]: Self::accept_called_ae_title -/// [`AcceptAny`]: AcceptAny -/// [`AcceptCalledAeTitle`]: AcceptCalledAeTitle -/// [`AccessControl`]: AccessControl +/// // Server configuration. +/// // Creates a server config that requires client authentication (mutual TLS) using +/// // webpki for certificate verification. +/// let server_config = ServerConfig::builder() +/// .with_client_cert_verifier( +/// WebPkiClientVerifier::builder(certs.clone()) +/// .build() +/// .expect("Failed to create client cert verifier") +/// ) +/// .with_single_cert(vec![server_cert.clone(), ca_cert.clone()], server_private_key) +/// .expect("Failed to create server TLS config"); +/// +/// let (stream, _address) = tcp_listener.accept()?; /// -/// [1]: dicom_transfer_syntax_registry -/// [2]: ServerAssociationOptions::with_transfer_syntax +/// let association = ServerAssociationOptions::new() +/// .accept_called_ae_title() +/// .ae_title("TLS-SCP") +/// .with_abstract_syntax(VERIFICATION) +/// .tls_config((*server_tls_config).clone()); +/// // .establish_tls(stream); +/// # Ok(()) +/// # } +/// ``` #[derive(Debug, Clone)] pub struct ServerAssociationOptions<'a, A> { /// the application entity access control policy @@ -219,10 +291,11 @@ pub struct ServerAssociationOptions<'a, A> { strict: bool, /// whether to accept unknown abstract syntaxes promiscuous: bool, - /// TCP read timeout - read_timeout: Option, - /// TCP write timeout - write_timeout: Option, + /// Options for the underlying TCP socket + socket_options: SocketOptions, + /// TLS configuration for the underlying TCP socket + #[cfg(feature = "sync-tls")] + tls_config: Option>, } impl Default for ServerAssociationOptions<'_, AcceptAny> { @@ -237,8 +310,9 @@ impl Default for ServerAssociationOptions<'_, AcceptAny> { max_pdu_length: DEFAULT_MAX_PDU, strict: true, promiscuous: false, - read_timeout: None, - write_timeout: None, + socket_options: SocketOptions::default(), + #[cfg(feature = "sync-tls")] + tls_config: None, } } } @@ -289,8 +363,9 @@ where strict, promiscuous, ae_access_control: _, - read_timeout, - write_timeout, + socket_options, + #[cfg(feature = "sync-tls")] + tls_config } = self; ServerAssociationOptions { @@ -303,8 +378,9 @@ where max_pdu_length, strict, promiscuous, - read_timeout, - write_timeout, + socket_options, + #[cfg(feature = "sync-tls")] + tls_config } } @@ -366,7 +442,11 @@ where /// This is used to set both the read and write timeout. pub fn read_timeout(self, timeout: Duration) -> Self { Self { - read_timeout: Some(timeout), + socket_options: SocketOptions { + read_timeout: Some(timeout), + write_timeout: self.socket_options.write_timeout, + connection_timeout: self.socket_options.connection_timeout, + }, ..self } } @@ -374,11 +454,22 @@ where /// Set the write timeout for the underlying TCP socket pub fn write_timeout(self, timeout: Duration) -> Self { Self { - write_timeout: Some(timeout), + socket_options: SocketOptions { + read_timeout: self.socket_options.read_timeout, + write_timeout: Some(timeout), + connection_timeout: self.socket_options.connection_timeout, + }, ..self } } + /// Set the TLS configuration for the underlying TCP socket + #[cfg(feature = "sync-tls")] + pub fn tls_config(mut self, config: impl Into>) -> Self { + self.tls_config = Some(config.into()); + self + } + /// Process an association request PDU /// /// In the success case, returns @@ -393,7 +484,7 @@ where fn process_a_association_rq( &self, msg: Pdu, - ) -> std::result::Result<(Pdu, NegotiatedOptions, String), (Pdu, Error)> { + ) -> std::result::Result<(Pdu, NegotiatedOptions), (Pdu, Error)> { match msg { Pdu::AssociationRQ(AssociationRQ { protocol_version, @@ -523,16 +614,13 @@ where ), ], }); - Ok(( - pdu, - NegotiatedOptions { - peer_max_pdu_length: requestor_max_pdu_length, - user_variables, - presentation_contexts: presentation_contexts_negotiated, - }, - calling_ae_title, - )) - } + Ok((pdu, NegotiatedOptions{ + peer_max_pdu_length: requestor_max_pdu_length, + user_variables, + presentation_contexts: presentation_contexts_negotiated, + peer_ae_title: calling_ae_title + })) + }, Pdu::ReleaseRQ => Err((Pdu::ReleaseRP, AbortedSnafu.build())), pdu @ Pdu::AssociationAC { .. } | pdu @ Pdu::AssociationRJ { .. } @@ -558,53 +646,40 @@ where } /// Negotiate an association with the given TCP stream. - pub fn establish(&self, mut socket: TcpStream) -> Result> { + pub fn establish(&self, mut socket: TcpStream) -> Result> + { ensure!( !self.abstract_syntax_uids.is_empty() || self.promiscuous, MissingAbstractSyntaxSnafu ); - let max_pdu_length = self.max_pdu_length; socket - .set_read_timeout(self.read_timeout) - .context(SetReadTimeoutSnafu)?; + .set_read_timeout(self.socket_options.read_timeout) + .context(super::SetReadTimeoutSnafu)?; socket - .set_write_timeout(self.write_timeout) - .context(SetWriteTimeoutSnafu)?; + .set_write_timeout(self.socket_options.write_timeout) + .context(super::SetWriteTimeoutSnafu)?; + let mut read_buffer = BytesMut::with_capacity( (self.max_pdu_length.min(LARGE_PDU_SIZE) + PDU_HEADER_SIZE) as usize, ); - let msg = read_pdu_from_wire( - &mut socket, - &mut read_buffer, - self.max_pdu_length, - self.strict, - )?; - let mut write_buffer: Vec = Vec::with_capacity(DEFAULT_MAX_PDU as usize); + let msg = read_pdu_from_wire(&mut socket, &mut read_buffer, self.max_pdu_length, self.strict)?; + let mut write_buffer: Vec = Vec::with_capacity(self.max_pdu_length as usize); match self.process_a_association_rq(msg) { - Ok(( - pdu, - NegotiatedOptions { - user_variables: _, - presentation_contexts, - peer_max_pdu_length, - }, - calling_ae_title, - )) => { + Ok((pdu, NegotiatedOptions{ user_variables, presentation_contexts , peer_max_pdu_length, peer_ae_title })) => { write_pdu(&mut write_buffer, &pdu).context(SendPduSnafu)?; socket.write_all(&write_buffer).context(WireSendSnafu)?; - Ok(ServerAssociation { + Ok(ServerAssociation { presentation_contexts, requestor_max_pdu_length: peer_max_pdu_length, - acceptor_max_pdu_length: max_pdu_length, + acceptor_max_pdu_length: self.max_pdu_length, socket, - client_ae_title: calling_ae_title, - buffer: write_buffer, + client_ae_title: peer_ae_title, + write_buffer, strict: self.strict, read_buffer, - read_timeout: self.read_timeout, - write_timeout: self.write_timeout, + user_variables, }) } Err((pdu, err)) => { @@ -616,6 +691,58 @@ where } } + /// Negotiate an association with the given TCP stream using TLS. + #[cfg(feature = "sync-tls")] + pub fn establish_tls(&self, socket: TcpStream) -> Result> { + ensure!( + !self.abstract_syntax_uids.is_empty() || self.promiscuous, + MissingAbstractSyntaxSnafu + ); + let tls_config = self.tls_config.as_ref().ok_or_else(|| { + super::TlsConfigMissingSnafu {}.build() + })?; + + socket + .set_read_timeout(self.socket_options.read_timeout) + .context(super::SetReadTimeoutSnafu)?; + socket + .set_write_timeout(self.socket_options.write_timeout) + .context(super::SetWriteTimeoutSnafu)?; + + let conn = rustls::ServerConnection::new(tls_config.clone()) + .context(super::TlsConnectionSnafu)?; + let mut tls_stream = rustls::StreamOwned::new(conn, socket); + let mut read_buffer = BytesMut::with_capacity( + (self.max_pdu_length.min(LARGE_PDU_SIZE) + PDU_HEADER_SIZE) as usize, + ); + + let msg = read_pdu_from_wire(&mut tls_stream, &mut read_buffer, self.max_pdu_length, self.strict)?; + let mut write_buffer: Vec = Vec::with_capacity(self.max_pdu_length as usize); + match self.process_a_association_rq(msg) { + Ok((pdu, NegotiatedOptions{ user_variables, presentation_contexts , peer_max_pdu_length, peer_ae_title })) => { + write_pdu(&mut write_buffer, &pdu).context(SendPduSnafu)?; + tls_stream.write_all(&write_buffer).context(WireSendSnafu)?; + Ok(ServerAssociation { + presentation_contexts, + requestor_max_pdu_length: peer_max_pdu_length, + acceptor_max_pdu_length: self.max_pdu_length, + socket: tls_stream, + client_ae_title: peer_ae_title, + write_buffer, + strict: self.strict, + read_buffer, + user_variables, + }) + }, + Err((pdu, err)) => { + // send the rejection/abort PDU + write_pdu(&mut write_buffer, &pdu).context(SendPduSnafu)?; + tls_stream.write_all(&write_buffer).context(WireSendSnafu)?; + Err(err) + } + } + } + /// From a sequence of transfer syntaxes, /// choose the first transfer syntax to /// - be on the options' list of transfer syntaxes, and @@ -647,15 +774,16 @@ where /// of an accepting application entity. /// /// The most common operations of an established association are -/// [`send`](Self::send) -/// and [`receive`](Self::receive). +/// [`send`](SyncAssociation::send) +/// and [`receive`](SyncAssociation::receive). /// Sending large P-Data fragments may be easier through the P-Data sender -/// abstraction (see [`send_pdata`](Self::send_pdata)). +/// abstraction (see [`send_pdata`](SyncAssociation::send_pdata)). /// /// When the value falls out of scope, /// the program will shut down the underlying TCP connection. #[derive(Debug)] -pub struct ServerAssociation { +pub struct ServerAssociation +where S: std::io::Read + std::io::Write + CloseSocket{ /// The accorded presentation contexts presentation_contexts: Vec, /// The maximum PDU length that the remote application entity accepts @@ -666,117 +794,81 @@ pub struct ServerAssociation { socket: S, /// The application entity title of the other DICOM node client_ae_title: String, - /// write buffer to send fully assembled PDUs on wire - buffer: Vec, + /// Reusable buffer used for sending PDUs on the wire + /// prevents reallocation on each send + write_buffer: Vec, /// whether to receive PDUs in strict mode strict: bool, /// Read buffer from the socket read_buffer: bytes::BytesMut, - /// Timeout for individual receive operations - read_timeout: Option, - /// Timeout for individual send operations - write_timeout: Option, + /// User variables received from the peer + user_variables: Vec, } -impl ServerAssociation { +impl Association for ServerAssociation +where S: std::io::Read + std::io::Write + CloseSocket{ + /// Obtain a view of the negotiated presentation contexts. - pub fn presentation_contexts(&self) -> &[PresentationContextNegotiated] { + fn presentation_contexts(&self) -> &[PresentationContextNegotiated] { &self.presentation_contexts } /// Retrieve the maximum PDU length /// admitted by this application entity. - pub fn acceptor_max_pdu_length(&self) -> u32 { + fn acceptor_max_pdu_length(&self) -> u32 { self.acceptor_max_pdu_length } /// Retrieve the maximum PDU length /// that the requestor is expecting to receive. - pub fn requestor_max_pdu_length(&self) -> u32 { + fn requestor_max_pdu_length(&self) -> u32 { self.requestor_max_pdu_length } /// Obtain the remote DICOM node's application entity title. - pub fn client_ae_title(&self) -> &str { + fn peer_ae_title(&self) -> &str { &self.client_ae_title } + + fn user_variables(&self) -> &[UserVariableItem] { + &self.user_variables + } } -impl ServerAssociation { - /// Send a PDU message to the other intervenient. - pub fn send(&mut self, msg: &Pdu) -> Result<()> { - self.buffer.clear(); - write_pdu(&mut self.buffer, msg).context(SendPduSnafu)?; - if self.buffer.len() > (self.requestor_max_pdu_length + PDU_HEADER_SIZE) as usize { - return SendTooLongPduSnafu { - length: self.buffer.len(), - } - .fail(); - } - self.socket.write_all(&self.buffer).context(WireSendSnafu) +impl SyncAssociationSealed for ServerAssociation + where S: std::io::Read + std::io::Write + CloseSocket { + fn send(&mut self, pdu: &Pdu) -> Result<()> { + self.write_buffer.clear(); + encode_pdu(&mut self.write_buffer, pdu, self.requestor_max_pdu_length + PDU_HEADER_SIZE)?; + self.socket.write_all(&self.write_buffer).context(WireSendSnafu) } - /// Read a PDU message from the other intervenient. - pub fn receive(&mut self) -> Result { - read_pdu_from_wire( - &mut self.socket, - &mut self.read_buffer, - self.acceptor_max_pdu_length, - self.strict, - ) + fn receive(&mut self) -> Result { + read_pdu_from_wire(&mut self.socket, &mut self.read_buffer, self.acceptor_max_pdu_length, self.strict) } - /// Send a provider initiated abort message - /// and shut down the TCP connection, - /// terminating the association. - pub fn abort(mut self) -> Result<()> { - let pdu = Pdu::AbortRQ { - source: AbortRQSource::ServiceProvider( - AbortRQServiceProviderReason::ReasonNotSpecified, - ), - }; - let out = self.send(&pdu); - let _ = self.socket.shutdown(std::net::Shutdown::Both); - out + fn close(&mut self) -> std::io::Result<()>{ + self.socket.close() } +} - /// Prepare a P-Data writer for sending - /// one or more data item PDUs. - /// - /// Returns a writer which automatically - /// splits the inner data into separate PDUs if necessary. - pub fn send_pdata(&mut self, presentation_context_id: u8) -> PDataWriter<&mut TcpStream> { - PDataWriter::new( - &mut self.socket, - presentation_context_id, - self.requestor_max_pdu_length, - ) +impl SyncAssociation for ServerAssociation +where S: std::io::Read + std::io::Write + CloseSocket,{ + fn inner_stream(&mut self) -> &mut S { + &mut self.socket } - /// Prepare a P-Data reader for receiving - /// one or more data item PDUs. - /// - /// Returns a reader which automatically - /// receives more data PDUs once the bytes collected are consumed. - pub fn receive_pdata(&mut self) -> PDataReader<'_, &mut TcpStream> { - PDataReader::new( - &mut self.socket, - self.acceptor_max_pdu_length, - &mut self.read_buffer, - ) + fn get_mut(&mut self) -> (&mut S, &mut BytesMut) { + let Self { socket, read_buffer, .. } = self; + (socket, read_buffer) } - /// Obtain access to the inner TCP stream - /// connected to the association acceptor. - /// - /// This can be used to send the PDU in semantic fragments of the message, - /// thus using less memory. - /// - /// **Note:** reading and writing should be done with care - /// to avoid inconsistencies in the association state. - /// Do not call `send` and `receive` while not in a PDU boundary. - pub fn inner_stream(&mut self) -> &mut TcpStream { - &mut self.socket +} + +impl Drop for ServerAssociation +where S: std::io::Read + std::io::Write + CloseSocket{ + fn drop(&mut self) { + let _ = SyncAssociationSealed::abort(self); } } @@ -837,186 +929,234 @@ where it.into_iter().find(|ts| is_supported(ts.as_ref())) } + #[cfg(feature = "async")] -pub mod non_blocking { - - use bytes::BytesMut; - use snafu::{ensure, ResultExt}; - use tokio::{io::AsyncWriteExt, net::TcpStream}; - - use super::{ - AccessControl, Result, SendTooLongPduSnafu, ServerAssociation, ServerAssociationOptions, - WireSendSnafu, - }; - use crate::{ - association::{ - read_pdu_from_wire_async, server::MissingAbstractSyntaxSnafu, NegotiatedOptions, - ReceivePduSnafu, SendPduSnafu, TimeoutSnafu, - }, - pdu::{ - AbortRQServiceProviderReason, AbortRQSource, ReadPduSnafu, DEFAULT_MAX_PDU, - LARGE_PDU_SIZE, PDU_HEADER_SIZE, - }, - write_pdu, Pdu, - }; - - impl ServerAssociationOptions<'_, A> - where - A: AccessControl, - { - /// Negotiate an association with the given TCP stream. - pub async fn establish_async( - &self, - mut socket: TcpStream, - ) -> Result> { - ensure!( - !self.abstract_syntax_uids.is_empty() || self.promiscuous, - MissingAbstractSyntaxSnafu +impl ServerAssociationOptions<'_, A> +where + A: AccessControl, +{ + /// Negotiate an association with the given TCP stream. + pub async fn establish_async(&self, mut socket: tokio::net::TcpStream) -> Result> { + use tokio::io::AsyncWriteExt; + ensure!( + !self.abstract_syntax_uids.is_empty() || self.promiscuous, + MissingAbstractSyntaxSnafu + ); + let read_timeout = self.socket_options.read_timeout; + let task = async { + let mut read_buffer = BytesMut::with_capacity( + (self.max_pdu_length.min(LARGE_PDU_SIZE) + PDU_HEADER_SIZE) as usize, ); - let read_timeout = self.read_timeout; - let task = async { - let mut read_buffer = BytesMut::with_capacity( - (self.max_pdu_length.min(LARGE_PDU_SIZE) + PDU_HEADER_SIZE) as usize, - ); - let pdu = read_pdu_from_wire_async( - &mut socket, - &mut read_buffer, - self.max_pdu_length, - self.strict, - ) - .await?; - - let mut write_buffer: Vec = - Vec::with_capacity((DEFAULT_MAX_PDU + PDU_HEADER_SIZE) as usize); - match self.process_a_association_rq(pdu) { - Ok(( - pdu, - NegotiatedOptions { - user_variables: _, - presentation_contexts, - peer_max_pdu_length, - }, - calling_ae_title, - )) => { - write_pdu(&mut write_buffer, &pdu).context(SendPduSnafu)?; - socket - .write_all(&write_buffer) - .await - .context(WireSendSnafu)?; - Ok(ServerAssociation { - presentation_contexts, - requestor_max_pdu_length: peer_max_pdu_length, - acceptor_max_pdu_length: self.max_pdu_length, - socket, - client_ae_title: calling_ae_title, - buffer: write_buffer, - strict: self.strict, - read_buffer, - read_timeout: self.read_timeout, - write_timeout: self.write_timeout, - }) - } - Err((pdu, err)) => { - // send the rejection/abort PDU - write_pdu(&mut write_buffer, &pdu).context(SendPduSnafu)?; - socket - .write_all(&write_buffer) - .await - .context(WireSendSnafu)?; - Err(err) - } + let pdu = super::read_pdu_from_wire_async(&mut socket, &mut read_buffer, self.max_pdu_length, self.strict).await?; + + let mut write_buffer: Vec = + Vec::with_capacity((DEFAULT_MAX_PDU + PDU_HEADER_SIZE) as usize); + match self.process_a_association_rq(pdu) { + Ok((pdu, NegotiatedOptions{ user_variables, presentation_contexts , peer_max_pdu_length, peer_ae_title})) => { + write_pdu(&mut write_buffer, &pdu).context(SendPduSnafu)?; + socket.write_all(&write_buffer).await.context(WireSendSnafu)?; + Ok(AsyncServerAssociation { + presentation_contexts, + requestor_max_pdu_length: peer_max_pdu_length, + acceptor_max_pdu_length: self.max_pdu_length, + socket, + client_ae_title: peer_ae_title, + write_buffer, + strict: self.strict, + read_buffer, + read_timeout: self.socket_options.read_timeout, + write_timeout: self.socket_options.write_timeout, + user_variables + }) + }, + Err((pdu, err)) => { + // send the rejection/abort PDU + write_pdu(&mut write_buffer, &pdu).context(SendPduSnafu)?; + socket.write_all(&write_buffer).await.context(WireSendSnafu)?; + Err(err) } - }; - if let Some(timeout) = read_timeout { - tokio::time::timeout(timeout, task) - .await - .map_err(|err| std::io::Error::new(std::io::ErrorKind::TimedOut, err)) - .context(TimeoutSnafu)? - } else { - task.await } - } + + }; + super::timeout(read_timeout, task).await } - impl ServerAssociation { - /// Send a PDU message to the other intervenient. - pub async fn send(&mut self, msg: &Pdu) -> Result<()> { - let timeout = self.write_timeout; - let task = async { - self.buffer.clear(); - write_pdu(&mut self.buffer, msg).context(SendPduSnafu)?; - if self.buffer.len() > (self.requestor_max_pdu_length + PDU_HEADER_SIZE) as usize { - return SendTooLongPduSnafu { - length: self.buffer.len(), - } - .fail(); + /// Negotiate an association with the given TCP stream. + #[cfg(feature = "async-tls")] + pub async fn establish_tls_async(&self, socket: tokio::net::TcpStream) -> Result> { + use tokio_rustls::TlsAcceptor; + use tokio::io::AsyncWriteExt; + + ensure!( + !self.abstract_syntax_uids.is_empty() || self.promiscuous, + MissingAbstractSyntaxSnafu + ); + let tls_config = self.tls_config.as_ref().ok_or_else(|| { + crate::association::TlsConfigMissingSnafu {}.build() + })?; + let acceptor = TlsAcceptor::from(tls_config.clone()); + let mut socket = acceptor.accept(socket).await.context(crate::association::ConnectSnafu)?; + let read_timeout = self.socket_options.read_timeout; + let task = async { + let mut read_buffer = BytesMut::with_capacity( + (self.max_pdu_length.min(LARGE_PDU_SIZE) + PDU_HEADER_SIZE) as usize, + ); + let pdu = super::read_pdu_from_wire_async(&mut socket, &mut read_buffer, self.max_pdu_length, self.strict).await?; + + let mut write_buffer: Vec = + Vec::with_capacity((DEFAULT_MAX_PDU + PDU_HEADER_SIZE) as usize); + match self.process_a_association_rq(pdu) { + Ok((pdu, NegotiatedOptions{ user_variables, presentation_contexts , peer_max_pdu_length, peer_ae_title})) => { + write_pdu(&mut write_buffer, &pdu).context(SendPduSnafu)?; + socket.write_all(&write_buffer).await.context(WireSendSnafu)?; + Ok(AsyncServerAssociation { + presentation_contexts, + requestor_max_pdu_length: peer_max_pdu_length, + acceptor_max_pdu_length: self.max_pdu_length, + socket, + client_ae_title: peer_ae_title, + write_buffer, + strict: self.strict, + read_buffer, + read_timeout: self.socket_options.read_timeout, + write_timeout: self.socket_options.write_timeout, + user_variables + }) + }, + Err((pdu, err)) => { + // send the rejection/abort PDU + write_pdu(&mut write_buffer, &pdu).context(SendPduSnafu)?; + socket.write_all(&write_buffer).await.context(WireSendSnafu)?; + Err(err) } - self.socket - .write_all(&self.buffer) - .await - .context(WireSendSnafu) - }; - if let Some(timeout) = timeout { - tokio::time::timeout(timeout, task) - .await - .map_err(|err| std::io::Error::new(std::io::ErrorKind::TimedOut, err)) - .context(WireSendSnafu)? - } else { - task.await } - } + }; + super::timeout(read_timeout, task).await + } +} + +/// An async DICOM upper level association from the perspective +/// of an accepting application entity. +/// +/// The most common operations of an established association are +/// [`send`](crate::association::AsyncAssociation::send) +/// and [`receive`](crate::association::AsyncAssociation::receive). +/// Sending large P-Data fragments may be easier through the P-Data sender +/// abstraction (see [`send_pdata`](crate::association::AsyncAssociation::send_pdata)). +/// +/// When the value falls out of scope, +/// the program will shut down the underlying TCP connection. +#[cfg(feature = "async")] +#[derive(Debug)] +pub struct AsyncServerAssociation +where S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send{ + /// The accorded presentation contexts + presentation_contexts: Vec, + /// The maximum PDU length that the remote application entity accepts + requestor_max_pdu_length: u32, + /// The maximum PDU length that this application entity is expecting to receive + acceptor_max_pdu_length: u32, + /// The TCP stream to the other DICOM node + socket: S, + /// The application entity title of the other DICOM node + client_ae_title: String, + /// write buffer to send fully assembled PDUs on wire + write_buffer: Vec, + /// whether to receive PDUs in strict mode + strict: bool, + /// Read buffer from the socket + read_buffer: bytes::BytesMut, + /// Timeout for individual receive operations + read_timeout: Option, + /// Timeout for individual send operations + write_timeout: Option, + /// User variables received from the peer + user_variables: Vec, +} + +#[cfg(feature = "async")] +impl Association for AsyncServerAssociation +where S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send{ + + fn acceptor_max_pdu_length(&self) -> u32 { + self.acceptor_max_pdu_length + } + + fn requestor_max_pdu_length(&self) -> u32 { + self.requestor_max_pdu_length + } + + /// Obtain a view of the negotiated presentation contexts. + fn presentation_contexts(&self) -> &[PresentationContextNegotiated] { + &self.presentation_contexts + } + + /// Obtain the remote DICOM node's application entity title. + fn peer_ae_title(&self) -> &str { + &self.client_ae_title + } - /// Read a PDU message from the other intervenient. - pub async fn receive(&mut self) -> Result { - let timeout = self.read_timeout; - let task = async { - read_pdu_from_wire_async( - &mut self.socket, - &mut self.read_buffer, - self.acceptor_max_pdu_length, - self.strict, - ) + fn user_variables(&self) -> &[UserVariableItem] { + &self.user_variables + } +} + +#[cfg(feature = "async")] +impl crate::association::private::AsyncAssociationSealed for AsyncServerAssociation +where S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send{ + /// Send a PDU message to the other intervenient. + async fn send(&mut self, msg: &Pdu) -> Result<()> { + use tokio::io::AsyncWriteExt; + self.write_buffer.clear(); + super::timeout(self.write_timeout, async { + encode_pdu(&mut self.write_buffer, msg, self.requestor_max_pdu_length + PDU_HEADER_SIZE)?; + self.socket + .write_all(&self.write_buffer) .await - }; - if let Some(timeout) = timeout { - tokio::time::timeout(timeout, task) - .await - .map_err(|err| std::io::Error::new(std::io::ErrorKind::TimedOut, err)) - .context(ReadPduSnafu) - .context(ReceivePduSnafu)? - } else { - task.await - } - } + .context(WireSendSnafu) + }).await + } - /// Send a provider initiated abort message - /// and shut down the TCP connection, - /// terminating the association. - pub async fn abort(mut self) -> Result<()> { - let timeout = self.write_timeout; - let task = async { - let pdu = Pdu::AbortRQ { - source: AbortRQSource::ServiceProvider( - AbortRQServiceProviderReason::ReasonNotSpecified, - ), - }; - let out = self.send(&pdu).await; - let _ = self.socket.shutdown().await; - out - }; - if let Some(timeout) = timeout { - tokio::time::timeout(timeout, task) - .await - .map_err(|err| std::io::Error::new(std::io::ErrorKind::TimedOut, err)) - .context(WireSendSnafu)? - } else { - task.await - } - } + /// Read a PDU message from the other intervenient. + async fn receive(&mut self) -> Result { + super::timeout(self.read_timeout,async { + super::read_pdu_from_wire_async( + &mut self.socket, + &mut self.read_buffer, + self.acceptor_max_pdu_length, + self.strict + ).await + }).await + } - pub fn inner_stream(&mut self) -> &mut TcpStream { - &mut self.socket - } + async fn close(&mut self) -> std::io::Result<()> { + use tokio::io::AsyncWriteExt; + self.socket.shutdown().await + } +} + +#[cfg(feature = "async")] +impl crate::association::AsyncAssociation for AsyncServerAssociation +where S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send{ + fn inner_stream(&mut self) -> &mut S { + &mut self.socket + } + + fn get_mut(&mut self) -> (&mut S, &mut bytes::BytesMut) { + let Self { socket, read_buffer, .. } = self; + (socket, read_buffer) + } +} + +#[cfg(feature = "async")] +impl Drop for AsyncServerAssociation +where S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send{ + fn drop(&mut self) { + tokio::task::block_in_place(move || { + tokio::runtime::Handle::current().block_on(async move { + let _ = crate::association::private::AsyncAssociationSealed::abort(self).await; + }) + }); } } @@ -1065,14 +1205,8 @@ mod tests { )?; let ( pdu, - NegotiatedOptions { - user_variables: _, - presentation_contexts, - peer_max_pdu_length, - }, - calling_ae_title, - ) = self - .process_a_association_rq(pdu) + NegotiatedOptions{ user_variables, presentation_contexts , peer_max_pdu_length, peer_ae_title} + ) = self.process_a_association_rq(pdu) .expect("Could not parse association req"); let mut write_buffer: Vec = @@ -1088,14 +1222,11 @@ mod tests { requestor_max_pdu_length: peer_max_pdu_length, acceptor_max_pdu_length: self.max_pdu_length, socket, - client_ae_title: calling_ae_title, - buffer: write_buffer, + client_ae_title: peer_ae_title, + write_buffer, + read_buffer, strict: self.strict, - read_buffer: BytesMut::with_capacity( - (self.max_pdu_length.min(LARGE_PDU_SIZE) + PDU_HEADER_SIZE) as usize, - ), - read_timeout: self.read_timeout, - write_timeout: self.write_timeout, + user_variables, }) } @@ -1105,7 +1236,7 @@ mod tests { &self, mut socket: tokio::net::TcpStream, extra_pdus: Vec, - ) -> Result> { + ) -> Result> { use tokio::io::AsyncWriteExt; use crate::association::read_pdu_from_wire_async; @@ -1122,14 +1253,8 @@ mod tests { .await?; let ( pdu, - NegotiatedOptions { - user_variables: _, - presentation_contexts, - peer_max_pdu_length, - }, - calling_ae_title, - ) = self - .process_a_association_rq(pdu) + NegotiatedOptions{ user_variables, presentation_contexts , peer_max_pdu_length, peer_ae_title} + ) = self.process_a_association_rq(pdu) .expect("Could not parse association req"); let mut buffer: Vec = Vec::with_capacity( @@ -1141,19 +1266,20 @@ mod tests { } socket.write_all(&buffer).await.context(WireSendSnafu)?; - Ok(ServerAssociation { + Ok(AsyncServerAssociation { presentation_contexts, requestor_max_pdu_length: peer_max_pdu_length, acceptor_max_pdu_length: self.max_pdu_length, socket, - client_ae_title: calling_ae_title, - buffer, + client_ae_title: peer_ae_title, + write_buffer: buffer, strict: self.strict, read_buffer: BytesMut::with_capacity( (self.max_pdu_length.min(LARGE_PDU_SIZE) + PDU_HEADER_SIZE) as usize, ), - read_timeout: self.read_timeout, - write_timeout: self.write_timeout, + user_variables, + read_timeout: self.socket_options.read_timeout, + write_timeout: self.socket_options.write_timeout, }) } @@ -1173,32 +1299,25 @@ mod tests { )?; let ( pdu, - NegotiatedOptions { - user_variables: _, - presentation_contexts, - peer_max_pdu_length, - }, - calling_ae_title, - ) = self - .process_a_association_rq(msg) - .expect("Could not parse association req"); - let mut write_buffer: Vec = - Vec::with_capacity((peer_max_pdu_length + PDU_HEADER_SIZE) as usize); - write_pdu(&mut write_buffer, &pdu).context(SendPduSnafu)?; - socket.write_all(&write_buffer).context(WireSendSnafu)?; - Ok(ServerAssociation { + NegotiatedOptions{user_variables, presentation_contexts , peer_max_pdu_length, peer_ae_title} + ) = self.process_a_association_rq(msg).expect("Could not parse association req"); + let mut buffer: Vec = Vec::with_capacity( + (peer_max_pdu_length.min(LARGE_PDU_SIZE) + PDU_HEADER_SIZE) as usize, + ); + write_pdu(&mut buffer, &pdu).context(SendPduSnafu)?; + socket.write_all(&buffer).context(WireSendSnafu)?; + Ok(ServerAssociation { presentation_contexts, requestor_max_pdu_length: peer_max_pdu_length, acceptor_max_pdu_length: self.max_pdu_length, socket, - client_ae_title: calling_ae_title, - buffer: write_buffer, + client_ae_title: peer_ae_title, + write_buffer: buffer, strict: self.strict, read_buffer: BytesMut::with_capacity( (self.max_pdu_length.min(LARGE_PDU_SIZE) + PDU_HEADER_SIZE) as usize, ), - read_timeout: self.read_timeout, - write_timeout: self.write_timeout, + user_variables }) } @@ -1207,7 +1326,7 @@ mod tests { pub async fn broken_establish_async( &self, mut socket: tokio::net::TcpStream, - ) -> Result> { + ) -> Result> { use tokio::io::AsyncWriteExt; use crate::association::read_pdu_from_wire_async; @@ -1224,35 +1343,27 @@ mod tests { .await?; let ( pdu, - NegotiatedOptions { - user_variables: _, - presentation_contexts, - peer_max_pdu_length, - }, - calling_ae_title, - ) = self - .process_a_association_rq(msg) - .expect("Could not parse association req"); - let mut write_buffer: Vec = - Vec::with_capacity((peer_max_pdu_length + PDU_HEADER_SIZE) as usize); - write_pdu(&mut write_buffer, &pdu).context(SendPduSnafu)?; - socket - .write_all(&write_buffer) - .await - .context(WireSendSnafu)?; - Ok(ServerAssociation { + NegotiatedOptions{user_variables, presentation_contexts , peer_max_pdu_length, peer_ae_title}, + ) = self.process_a_association_rq(msg).expect("Could not parse association req"); + let mut buffer: Vec = Vec::with_capacity( + (peer_max_pdu_length.min(LARGE_PDU_SIZE) + PDU_HEADER_SIZE) as usize, + ); + write_pdu(&mut buffer, &pdu).context(SendPduSnafu)?; + socket.write_all(&buffer).await.context(WireSendSnafu)?; + Ok(AsyncServerAssociation { presentation_contexts, requestor_max_pdu_length: peer_max_pdu_length, acceptor_max_pdu_length: self.max_pdu_length, socket, - client_ae_title: calling_ae_title, - buffer: write_buffer, + client_ae_title: peer_ae_title, + write_buffer: buffer, strict: self.strict, read_buffer: BytesMut::with_capacity( (self.max_pdu_length.min(LARGE_PDU_SIZE) + PDU_HEADER_SIZE) as usize, ), - read_timeout: self.read_timeout, - write_timeout: self.write_timeout, + read_timeout: self.socket_options.read_timeout, + write_timeout: self.socket_options.write_timeout, + user_variables }) } } diff --git a/ul/src/association/tests.rs b/ul/src/association/tests.rs index 996736d8..5713ce7a 100644 --- a/ul/src/association/tests.rs +++ b/ul/src/association/tests.rs @@ -2,6 +2,9 @@ use dicom_core::{dicom_value, DataElement, VR}; use dicom_dictionary_std::{tags, uids::VERIFICATION}; use dicom_object::InMemDicomObject; use dicom_transfer_syntax_registry::entries::IMPLICIT_VR_LITTLE_ENDIAN; +use crate::association::SyncAssociation; +#[cfg(feature = "async")] +use crate::association::AsyncAssociation; // Helper funtion to create a C-ECHO command fn create_c_echo_command(message_id: u16) -> Vec { let obj = InMemDicomObject::command_from_element_iter([ @@ -27,13 +30,11 @@ fn create_c_echo_command(message_id: u16) -> Vec { data } mod successive_pdus_during_client_association { - use super::*; - use crate::{ - pdu::{PDataValue, PDataValueType}, - ClientAssociationOptions, Pdu, - }; use std::net::TcpListener; + use super::*; + use crate::{ClientAssociationOptions, Pdu, pdu::{AbortRQServiceProviderReason, AbortRQSource, PDataValue, PDataValueType}}; + use crate::association::server::*; #[test] @@ -70,6 +71,7 @@ mod successive_pdus_during_client_association { // Send the second PDU (C-ECHO command) immediately after establishment association.send(&server_pdu).unwrap(); + association }); // Give server time to start @@ -84,20 +86,18 @@ mod successive_pdus_during_client_association { // This should succeed in establishing the association despite multiple PDUs let mut association = scu_options.establish(server_addr).unwrap(); - // Client should be able to receive the release request that was sent consecutively let received_pdu = association.receive().unwrap(); assert_eq!(received_pdu, echo_pdu); - // Clean shutdown - drop(association); server_handle.join().unwrap(); + drop(association); } // Tests edge case where the server sends an extra PDU during association // client should be able to handle this gracefully. #[test] - fn test_association_sends_extra_pdu_fails() { + fn test_association_sends_extra_pdu() { // During association, the server sends a C-ECHO command // This will be received by the client @@ -125,10 +125,9 @@ mod successive_pdus_during_client_association { .accept_any() .with_abstract_syntax(VERIFICATION) .ae_title("THIS-SCP"); - - server_options - .establish_with_extra_pdus(stream, vec![server_pdu]) - .unwrap(); + + let association = server_options.establish_with_extra_pdus(stream, vec![server_pdu]).unwrap(); + association }); // Give server time to start @@ -149,8 +148,8 @@ mod successive_pdus_during_client_association { assert_eq!(received_pdu, echo_pdu); // Clean shutdown - drop(association); server_handle.join().unwrap(); + drop(association); } #[cfg(feature = "async")] @@ -188,6 +187,7 @@ mod successive_pdus_during_client_association { // Send the second PDU (C-ECHO command) immediately after establishment association.send(&server_pdu).await.unwrap(); + association }); // Give server time to start @@ -208,15 +208,15 @@ mod successive_pdus_during_client_association { assert_eq!(received_pdu, echo_pdu); // Clean shutdown - drop(association); server_handle.await.unwrap(); + drop(association); } // Tests edge case where the server sends an extra PDU during association // client should be able to handle this gracefully. #[cfg(feature = "async")] #[tokio::test(flavor = "multi_thread")] - async fn test_association_sends_extra_pdu_fails_async() { + async fn test_association_sends_extra_pdu_async() { // During association, the server sends a C-ECHO command // This will be received by the client @@ -244,11 +244,9 @@ mod successive_pdus_during_client_association { .accept_any() .with_abstract_syntax(VERIFICATION) .ae_title("THIS-SCP"); - - server_options - .establish_with_extra_pdus_async(stream, vec![server_pdu]) - .await - .unwrap(); + + let association = server_options.establish_with_extra_pdus_async(stream, vec![server_pdu]).await.unwrap(); + association }); // Give server time to start @@ -269,8 +267,8 @@ mod successive_pdus_during_client_association { assert_eq!(received_pdu, echo_pdu); // Clean shutdown - drop(association); server_handle.await.unwrap(); + drop(association); } // Tests edge case where the client sends an extra PDU during association @@ -303,10 +301,9 @@ mod successive_pdus_during_client_association { .accept_any() .with_abstract_syntax(VERIFICATION) .ae_title("THIS-SCP"); - - server_options - .establish_with_extra_pdus(stream, vec![echo_pdu]) - .unwrap(); + + let association = server_options.establish_with_extra_pdus(stream, vec![echo_pdu]).unwrap(); + association }); // Give server time to start std::thread::sleep(std::time::Duration::from_millis(10)); @@ -321,14 +318,16 @@ mod successive_pdus_during_client_association { // This should succeed in establishing the association despite multiple PDUs let mut association = scu_options.broken_establish(server_addr.into()).unwrap(); - // Client should not have anything to receive - let received_pdu = association.receive(); - assert!(received_pdu.is_err()); - + // Initiate abort server side + server_handle.join().unwrap(); + + // Client should not have anything to receive, only receives abort Rq + let received_pdu = association.receive().unwrap(); + assert_eq!(received_pdu, Pdu::AbortRQ { source: AbortRQSource::ServiceProvider(AbortRQServiceProviderReason::ReasonNotSpecified) }); + // Client cannot receive the PDU that was sent during association // Clean shutdown drop(association); - server_handle.join().unwrap(); } #[cfg(feature = "async")] @@ -360,11 +359,9 @@ mod successive_pdus_during_client_association { .accept_any() .with_abstract_syntax(VERIFICATION) .ae_title("THIS-SCP"); - - server_options - .establish_with_extra_pdus_async(stream, vec![echo_pdu]) - .await - .unwrap(); + + let association = server_options.establish_with_extra_pdus_async(stream, vec![echo_pdu]).await.unwrap(); + association }); // Give server time to start @@ -378,19 +375,19 @@ mod successive_pdus_during_client_association { .read_timeout(std::time::Duration::from_secs(5)); // Client's broken implementation will miss the extra PDU from server - let mut association = scu_options - .broken_establish_async(server_addr.into()) - .await - .unwrap(); + let mut association = scu_options.broken_establish_async( + server_addr.into() + ).await.unwrap(); + // Initiate abort server-side + server_handle.await.unwrap(); // Client should be able to receive the release request that was sent consecutively - let received_pdu = association.receive().await; - assert!(received_pdu.is_err()); - + let received_pdu = association.receive().await.expect("Could not receive abort PDU"); + assert_eq!(received_pdu, Pdu::AbortRQ { source: AbortRQSource::ServiceProvider(AbortRQServiceProviderReason::ReasonNotSpecified) }); + // Client cannot receive the PDU that was sent during association // Clean shutdown drop(association); - server_handle.await.unwrap(); } } @@ -438,6 +435,7 @@ mod successive_pdus_during_server_association { // Server should be able to receive the PDU sent by client after association let received_pdu = association.receive().unwrap(); assert_eq!(received_pdu, echo_pdu); + association }); // Give server time to start @@ -457,8 +455,8 @@ mod successive_pdus_during_server_association { association.send(&client_pdu).unwrap(); // Clean shutdown - drop(association); server_handle.join().unwrap(); + drop(association); } #[test] diff --git a/ul/src/lib.rs b/ul/src/lib.rs index 836a665f..48ff00bc 100644 --- a/ul/src/lib.rs +++ b/ul/src/lib.rs @@ -20,6 +20,10 @@ //! ## Features //! * `async`: Enables a fully async implementation of the upper layer protocol. //! See [`ClientAssociationOptions`] and [`ServerAssociationOptions`] for details +//! * `sync-tls`: Enables TLS support for synchronous associations. +//! * `async-tls`: Enables TLS support for asynchronous associations. +//! Implies `async` and `sync-tls`. +//! * `full`: Enables all capabilities: `async-tls` pub mod address; pub mod association; diff --git a/ul/test.json b/ul/test.json deleted file mode 100644 index 7379f69f..00000000 --- a/ul/test.json +++ /dev/null @@ -1 +0,0 @@ -{"_id": "66d9defb7a74b2ded8614922", "label": "deid-export 2024-09-05 11:40:27", "parent": {"id": "6650dd2f995b66671b4a52a5", "type": "subject"}, "parents": {"group": "susannah", "project": "66464b2e6d2adbc8c94a519e", "subject": "6650dd2f995b66671b4a52a5", "session": null, "acquisition": null}, "created": "2024-09-05T16:40:27.338000Z", "modified": "2024-09-05T16:40:27.410000Z", "timestamp": null, "revision": 2, "inputs": [{"_id": "d3ec367e-5d63-4397-bade-3a706bece949", "name": "deid_dicom_zip.yaml", "type": "source code", "file_id": "6650dda1995b66671b4a52a6", "version": 2, "mimetype": "application/octet-stream", "modality": null, "deid_log_id": null, "deid_log_skip_reason": null, "classification": {}, "tags": [], "provider_id": "63e56e1bc78fb19740fd2499", "parent_ref": {"id": "66d9defb7a74b2ded8614922", "type": "analysis"}, "parents": {"group": "susannah", "project": "66464b2e6d2adbc8c94a519e", "subject": null, "session": null, "acquisition": null, "analysis": null}, "restored_from": null, "restored_by": null, "path": "d3/ec/d3ec367e-5d63-4397-bade-3a706bece949", "reference": null, "origin": {"type": "user", "id": "naterichman@flywheel.io"}, "virus_scan": null, "created": "2024-09-05T16:40:27.350000Z", "modified": "2024-09-05T16:40:27.350000Z", "replaced": null, "deleted": null, "size": 1486, "hash": "bb61057bec02e88980613cd5f3f71bb7ccdd86a752bcd85e84945523b8d8e866b6e4528990968485bbe59e19e89921d7", "client_hash": null, "info": {}, "info_exists": false, "zip_member_count": null, "gear_info": {"name": "deid-export", "version": "1.5.3-rc.2", "id": "66630801e6c7518991f26065"}, "copy_of": null, "original_copy_of": null}], "description": "", "info": {}, "files": [], "notes": [], "tags": [], "job": "66d9defb7a74b2ded8614924", "gear_info": {"id": "66630801e6c7518991f26065", "category": "analysis", "name": "deid-export", "version": "1.5.3-rc.2", "capabilities": null}, "compute_provider_id": null, "join-origin": null, "copy_of": null, "original_copy_of": null, "container_type": "analysis"} diff --git a/ul/tests/association.rs b/ul/tests/association.rs index e5cd11c5..c1d7698e 100644 --- a/ul/tests/association.rs +++ b/ul/tests/association.rs @@ -2,9 +2,253 @@ use dicom_dictionary_std::uids::VERIFICATION; use dicom_ul::ClientAssociationOptions; use rstest::rstest; use std::time::Instant; +#[cfg(feature = "sync-tls")] +use dicom_ul::association::Association; +#[cfg(feature = "sync-tls")] +use std::sync::Arc; + +#[cfg(feature = "sync-tls")] +fn ensure_test_certs() -> Result<(), Box> { + use rustls_cert_gen::CertificateBuilder; + use rcgen::SanType; + use std::{convert::TryInto, net::IpAddr, str::FromStr, path::PathBuf}; + + let out_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("certs"); + let cert_names = vec!["ca.pem", "ca.key.pem", "client.pem", "client.key.pem", "server.pem", "server.key.pem"]; + if cert_names.iter().all(|path| out_dir.join(path).exists()){ + println!("All certs exist, exiting"); + return Ok(()); + } + if out_dir.exists() { + std::fs::remove_dir_all(&out_dir).expect("Could not remove certs dir"); + } + + // Create output directory + std::fs::create_dir_all(&out_dir)?; + + // Generate Certificate Authority (CA) + let ca = CertificateBuilder::new() + .certificate_authority() + .country_name(&"US")? + .organization_name(&"DICOM-RS-CA") + .build()?; + + // Write CA certificate and private key to `../certs/ca.pem` and `../certs/ca.key.pem` + ca.serialize_pem().write(&out_dir, "ca")?; + println!("Created certs/ca.pem and certs/ca.key.pem"); + + // Generate Client keypair + let mut client = CertificateBuilder::new() + .end_entity() + .common_name(&"DICOM-RS-CLIENT") + .subject_alternative_names(vec![SanType::IpAddress(IpAddr::from_str("127.0.0.1")?), SanType::DnsName("localhost".try_into()?)]); + client.client_auth(); + + client + .build(&ca)? + .serialize_pem().write(&out_dir, "client")?; + println!("Created certs/client.pem and certs/client.key.pem"); + + // Generate Server keypair + let mut server = CertificateBuilder::new() + .end_entity() + .common_name(&"DICOM-RS-SERVER") + .subject_alternative_names(vec![SanType::IpAddress(IpAddr::from_str("127.0.0.1")?), SanType::DnsName("localhost".try_into()?)]); + server.server_auth(); + + server + .build(&ca)? + .serialize_pem().write(&out_dir, "server")?; + println!("Created certs/server.pem and certs/server.key.pem"); + + Ok(()) +} const TIMEOUT_TOLERANCE: u64 = 25; +#[cfg(feature = "sync-tls")] +/// Create a test TLS server configuration +fn create_test_config() -> Result<(Arc, Arc), Box> { + use rustls::{ClientConfig, RootCertStore, ServerConfig, pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject}, server::WebPkiClientVerifier}; + use std::path::PathBuf; + ensure_test_certs()?; + + + let ca_cert_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("certs/ca.pem"); + let ca_cert = CertificateDer::from_pem_slice(&std::fs::read(ca_cert_path)?) + .expect("Failed to load CA cert"); + + let client_cert_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("certs/client.pem"); + let client_cert = CertificateDer::from_pem_slice(&std::fs::read(client_cert_path)?) + .expect("Failed to load client cert"); + + let client_key_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("certs/client.key.pem"); + let client_private_key = PrivateKeyDer::from_pem_slice(&std::fs::read(client_key_path)?) + .expect("Failed to load client private key"); + + let server_cert_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("certs/server.pem"); + let server_cert = CertificateDer::from_pem_slice(&std::fs::read(server_cert_path)?) + .expect("Failed to load server cert"); + + let server_key_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("certs/server.key.pem"); + let server_private_key = PrivateKeyDer::from_pem_slice(&std::fs::read(server_key_path)?) + .expect("Failed to load server private key"); + + // Create a root cert store for the client which includes the server certificate + let mut certs = RootCertStore::empty(); + certs.add_parsable_certificates(vec![ca_cert.clone()]); + let certs = Arc::new(certs); + + // Server configuration. + // Creates a server config that requires client authentication (mutual TLS) using + // webpki for certificate verification. + let server_config = ServerConfig::builder() + .with_client_cert_verifier( + WebPkiClientVerifier::builder(certs.clone()) + .build() + .expect("Failed to create client cert verifier") + ) + .with_single_cert(vec![server_cert.clone(), ca_cert.clone()], server_private_key) + .expect("Failed to create server TLS config"); + + let config = ClientConfig::builder() + .with_root_certificates(certs) + .with_client_auth_cert(vec![client_cert, ca_cert], client_private_key) + .expect("Failed to create client TLS config"); + + Ok((Arc::new(server_config), Arc::new(config))) +} + +#[cfg(feature = "sync-tls")] +#[test] +fn test_tls_connection_sync() { + // set up crypto provider -- Just use the default provider which is aws_lc_rs + + use dicom_ul::{ServerAssociationOptions, association::SyncAssociation}; + let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); + + let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind listener"); + let server_addr = listener.local_addr().expect("Failed to get local address"); + + // Server configuration + let (server_tls_config, client_tls_config) = create_test_config().expect("Failed to create test config"); + let server_options = ServerAssociationOptions::new() + .accept_called_ae_title() + .ae_title("TLS-SCP") + .with_abstract_syntax(VERIFICATION) + .tls_config((*server_tls_config).clone()); + + // Spawn server thread + let server_handle = std::thread::spawn(move || { + use dicom_ul::association::SyncAssociation; + + let (stream, _) = listener.accept().expect("Failed to accept connection"); + let mut association = server_options.establish_tls(stream) + .expect("Failed to establish TLS association"); + + // Verify we can access association properties + assert_eq!(association.peer_ae_title(), "TLS-SCU"); + assert!(!association.presentation_contexts().is_empty()); + + // Wait for a release request + let pdu = association.receive().expect("Failed to receive PDU"); + if let dicom_ul::Pdu::ReleaseRQ = pdu { + association.send(&dicom_ul::Pdu::ReleaseRP).expect("Failed to send ReleaseRP"); + } + association + }); + + // Give server time to start + std::thread::sleep(std::time::Duration::from_millis(50)); + + // Client configuration + let client_options = ClientAssociationOptions::new() + .calling_ae_title("TLS-SCU") + .called_ae_title("TLS-SCP") + .with_abstract_syntax(VERIFICATION) + .server_name("localhost") + .tls_config((*client_tls_config).clone()); + + // Establish TLS connection + let association = client_options.establish_tls(server_addr) + .expect("Failed to establish TLS association"); + println!("{:?}", association); + println!("{:?}", server_handle); + + // Verify association properties + assert_eq!(association.peer_ae_title(), "TLS-SCP"); + assert!(!association.presentation_contexts().is_empty()); + + // Release the association + association.release().expect("Failed to release association"); + + // Wait for server to complete + server_handle.join().expect("Server thread failed"); +} + +#[cfg(feature = "async-tls")] +#[tokio::test(flavor = "multi_thread")] +async fn test_tls_connection_async() -> Result<(), Box> { + use dicom_ul::{ServerAssociationOptions, association::AsyncAssociation}; + // set up crypto provider -- Just use the default provider which is aws_lc_rs + let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); + + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?; + let server_addr = listener.local_addr()?; + + // Server configuration + let (server_tls_config, client_tls_config) = create_test_config().expect("Failed to create test config"); + let server_options = ServerAssociationOptions::new() + .accept_called_ae_title() + .ae_title("ASYNC-TLS-SCP") + .with_abstract_syntax(VERIFICATION) + .tls_config((*server_tls_config).clone()); + + // Spawn server task + let server_handle = tokio::spawn(async move { + let (stream, _) = listener.accept().await.map_err(|e| Box::new(e) as Box)?; + let mut association = server_options.establish_tls_async(stream).await.map_err(|e| Box::new(e) as Box)?; + + // Verify we can access association properties + assert_eq!(association.peer_ae_title(), "ASYNC-TLS-SCU"); + assert!(!association.presentation_contexts().is_empty()); + + // Wait for a release request + let pdu = association.receive().await.map_err(|e| Box::new(e) as Box)?; + if let dicom_ul::Pdu::ReleaseRQ = pdu { + association.send(&dicom_ul::Pdu::ReleaseRP).await.map_err(|e| Box::new(e) as Box)?; + } + + Ok::<(), Box>(()) + }); + + // Give server time to start + tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; + + // Client configuration + let client_options = ClientAssociationOptions::new() + .calling_ae_title("ASYNC-TLS-SCU") + .called_ae_title("ASYNC-TLS-SCP") + .with_abstract_syntax(VERIFICATION) + .server_name("localhost") + .tls_config((*client_tls_config).clone()); + + // Establish TLS connection + let association = client_options.establish_tls_async(server_addr).await?; + + // Verify association properties + assert_eq!(association.peer_ae_title(), "ASYNC-TLS-SCP"); + assert!(!association.presentation_contexts().is_empty()); + + // Release the association + association.release().await?; + + // Wait for server to complete + server_handle.await??; + + Ok(()) +} + #[rstest] #[case(100)] #[case(500)] diff --git a/ul/tests/association_echo.rs b/ul/tests/association_echo.rs index 7c6fdb7c..db9fe303 100644 --- a/ul/tests/association_echo.rs +++ b/ul/tests/association_echo.rs @@ -1,10 +1,11 @@ use dicom_ul::{ - association::{client::ClientAssociationOptions, server::ServerAssociationOptions, Error}, - pdu::{ + ServerAssociation, association::{Association, Error, SyncAssociation, client::ClientAssociationOptions, server::ServerAssociationOptions}, pdu::{ PDataValue, PDataValueType, Pdu, PresentationContextNegotiated, PresentationContextResultReason, - }, + } }; +#[cfg(feature = "async")] +use dicom_ul::association::AsyncServerAssociation; use std::net::SocketAddr; @@ -41,7 +42,7 @@ fn bogus_packet(len: usize) -> Pdu { fn spawn_scp( max_server_pdu_len: usize, max_client_pdu_len: usize, -) -> Result<(std::thread::JoinHandle>, SocketAddr)> { +) -> Result<(std::thread::JoinHandle>>, SocketAddr)> { let listener = std::net::TcpListener::bind("localhost:0")?; let addr = listener.local_addr()?; let scp = ServerAssociationOptions::new() @@ -50,7 +51,7 @@ fn spawn_scp( .max_pdu_length(max_server_pdu_len as u32) .with_abstract_syntax(VERIFICATION_SOP_CLASS); - let h = std::thread::spawn(move || -> Result<()> { + let h = std::thread::spawn(move || -> Result<_> { let (stream, _addr) = listener.accept()?; let mut association = scp.establish(stream)?; @@ -113,7 +114,7 @@ fn spawn_scp( assert_eq!(pdu, Pdu::ReleaseRQ); association.send(&Pdu::ReleaseRP)?; - Ok(()) + Ok(association) }); Ok((h, addr)) } @@ -122,7 +123,7 @@ fn spawn_scp( async fn spawn_scp_async( max_server_pdu_len: usize, max_client_pdu_len: usize, -) -> Result<(tokio::task::JoinHandle>, SocketAddr)> { +) -> Result<(tokio::task::JoinHandle>>, SocketAddr)> { let listener = tokio::net::TcpListener::bind("localhost:0").await?; let addr = listener.local_addr()?; let scp = ServerAssociationOptions::new() @@ -132,6 +133,8 @@ async fn spawn_scp_async( .with_abstract_syntax(VERIFICATION_SOP_CLASS); let h = tokio::spawn(async move { + use dicom_ul::association::AsyncAssociation; + let (stream, _addr) = listener.accept().await?; let mut association = scp.establish_async(stream).await?; @@ -197,7 +200,7 @@ async fn spawn_scp_async( assert_eq!(pdu, Pdu::ReleaseRQ); association.send(&Pdu::ReleaseRP).await?; - Ok(()) + Ok(association) }); Ok((h, addr)) } @@ -281,6 +284,8 @@ async fn scu_scp_association_test_async() { #[cfg(feature = "async")] async fn run_scu_scp_association_test_async(max_is_client: bool) { + use dicom_ul::association::AsyncAssociation; + let (max_client_pdu_len, max_server_pdu_len) = if max_is_client { (HI_PDU_LEN, LO_PDU_LEN) } else { diff --git a/ul/tests/association_promiscuous.rs b/ul/tests/association_promiscuous.rs index 49355a6f..e1ff8cb8 100644 --- a/ul/tests/association_promiscuous.rs +++ b/ul/tests/association_promiscuous.rs @@ -1,14 +1,15 @@ use std::net::SocketAddr; -use dicom_ul::association::Error::NoAcceptedPresentationContexts; +use dicom_ul::association::{Association, SyncAssociation, Error::NoAcceptedPresentationContexts}; +#[cfg(feature = "async")] +use dicom_ul::association::{AsyncAssociation, AsyncServerAssociation}; use dicom_ul::pdu::PresentationContextResultReason::Acceptance; use dicom_ul::pdu::{ PresentationContextNegotiated, PresentationContextResultReason, UserVariableItem, DEFAULT_MAX_PDU, }; use dicom_ul::{ - ClientAssociationOptions, Pdu, ServerAssociationOptions, IMPLEMENTATION_CLASS_UID, - IMPLEMENTATION_VERSION_NAME, + ClientAssociationOptions, IMPLEMENTATION_CLASS_UID, IMPLEMENTATION_VERSION_NAME, Pdu, ServerAssociation, ServerAssociationOptions }; type Result = std::result::Result>; @@ -24,7 +25,7 @@ const ULTRASOUND_IMAGE_STORAGE_RAW: &str = "1.2.840.10008.5.1.4.1.1.6.1\0"; fn spawn_scp( abstract_syntax_uids: &'static [&str], promiscuous: bool, -) -> Result<(std::thread::JoinHandle>, SocketAddr)> { +) -> Result<(std::thread::JoinHandle>>, SocketAddr)> { let listener = std::net::TcpListener::bind("localhost:0")?; let addr = listener.local_addr()?; let mut options = ServerAssociationOptions::new() @@ -53,7 +54,7 @@ fn spawn_scp( assert_eq!(pdu, Pdu::ReleaseRQ); association.send(&Pdu::ReleaseRP)?; - Ok(()) + Ok(association) }); Ok((handle, addr)) @@ -63,7 +64,7 @@ fn spawn_scp( async fn spawn_scp_async( abstract_syntax_uids: &'static [&str], promiscuous: bool, -) -> Result<(tokio::task::JoinHandle>, SocketAddr)> { +) -> Result<(tokio::task::JoinHandle>>, SocketAddr)> { let listener = tokio::net::TcpListener::bind("localhost:0").await?; let addr = listener.local_addr()?; let mut options = ServerAssociationOptions::new() @@ -92,7 +93,7 @@ async fn spawn_scp_async( assert_eq!(pdu, Pdu::ReleaseRQ); association.send(&Pdu::ReleaseRP).await?; - Ok(()) + Ok(association) }); Ok((handle, addr)) diff --git a/ul/tests/association_store.rs b/ul/tests/association_store.rs index 5c133b05..a085f8a1 100644 --- a/ul/tests/association_store.rs +++ b/ul/tests/association_store.rs @@ -1,7 +1,8 @@ use dicom_ul::{ - association::client::ClientAssociationOptions, - pdu::{Pdu, PresentationContextNegotiated, PresentationContextResultReason}, + ServerAssociation, association::{Association, SyncAssociation, client::ClientAssociationOptions}, pdu::{Pdu, PresentationContextNegotiated, PresentationContextResultReason} }; +#[cfg(feature = "async")] +use dicom_ul::association::{AsyncAssociation, AsyncServerAssociation}; use std::net::SocketAddr; use dicom_ul::association::server::ServerAssociationOptions; @@ -20,7 +21,7 @@ static MR_IMAGE_STORAGE: &str = "1.2.840.10008.5.1.4.1.1.4"; static DIGITAL_MG_STORAGE_SOP_CLASS_RAW: &str = "1.2.840.10008.5.1.4.1.1.1.2\0"; static DIGITAL_MG_STORAGE_SOP_CLASS: &str = "1.2.840.10008.5.1.4.1.1.1.2"; -fn spawn_scp() -> Result<(std::thread::JoinHandle>, SocketAddr)> { +fn spawn_scp() -> Result<(std::thread::JoinHandle>>, SocketAddr)> { let listener = std::net::TcpListener::bind("localhost:0")?; let addr = listener.local_addr()?; let scp = ServerAssociationOptions::new() @@ -29,7 +30,7 @@ fn spawn_scp() -> Result<(std::thread::JoinHandle>, SocketAddr)> { .with_abstract_syntax(MR_IMAGE_STORAGE) .with_abstract_syntax(DIGITAL_MG_STORAGE_SOP_CLASS); - let h = std::thread::spawn(move || -> Result<()> { + let h = std::thread::spawn(move || -> Result<_> { let (stream, _addr) = listener.accept()?; let mut association = scp.establish(stream)?; @@ -56,13 +57,13 @@ fn spawn_scp() -> Result<(std::thread::JoinHandle>, SocketAddr)> { assert_eq!(pdu, Pdu::ReleaseRQ); association.send(&Pdu::ReleaseRP)?; - Ok(()) + Ok(association) }); Ok((h, addr)) } #[cfg(feature = "async")] -async fn spawn_scp_async() -> Result<(tokio::task::JoinHandle>, SocketAddr)> { +async fn spawn_scp_async() -> Result<(tokio::task::JoinHandle>>, SocketAddr)> { let listener = tokio::net::TcpListener::bind("localhost:0").await?; let addr = listener.local_addr()?; let scp = ServerAssociationOptions::new() @@ -98,7 +99,7 @@ async fn spawn_scp_async() -> Result<(tokio::task::JoinHandle>, Socke assert_eq!(pdu, Pdu::ReleaseRQ); association.send(&Pdu::ReleaseRP).await?; - Ok(()) + Ok(association) }); Ok((h, addr)) } diff --git a/ul/tests/association_store_uncompressed.rs b/ul/tests/association_store_uncompressed.rs index 7762cf5f..65c783a0 100644 --- a/ul/tests/association_store_uncompressed.rs +++ b/ul/tests/association_store_uncompressed.rs @@ -2,9 +2,10 @@ //! which only accepts uncompressed transfer syntaxes use dicom_ul::{ - association::client::ClientAssociationOptions, - pdu::{Pdu, PresentationContextNegotiated, PresentationContextResultReason}, + ServerAssociation, association::{Association, SyncAssociation, client::ClientAssociationOptions}, pdu::{Pdu, PresentationContextNegotiated, PresentationContextResultReason} }; +#[cfg(feature = "async")] +use dicom_ul::association::{AsyncAssociation, AsyncServerAssociation}; use std::net::SocketAddr; use dicom_ul::association::server::ServerAssociationOptions; @@ -24,7 +25,7 @@ static MR_IMAGE_STORAGE: &str = "1.2.840.10008.5.1.4.1.1.4"; static DIGITAL_MG_STORAGE_SOP_CLASS_RAW: &str = "1.2.840.10008.5.1.4.1.1.1.2\0"; static DIGITAL_MG_STORAGE_SOP_CLASS: &str = "1.2.840.10008.5.1.4.1.1.1.2"; -fn spawn_scp() -> Result<(std::thread::JoinHandle>, SocketAddr)> { +fn spawn_scp() -> Result<(std::thread::JoinHandle>>, SocketAddr)> { let listener = std::net::TcpListener::bind("localhost:0")?; let addr = listener.local_addr()?; let scp = ServerAssociationOptions::new() @@ -35,7 +36,7 @@ fn spawn_scp() -> Result<(std::thread::JoinHandle>, SocketAddr)> { .with_transfer_syntax(EXPLICIT_VR_LE) .with_transfer_syntax(IMPLICIT_VR_LE); - let h = std::thread::spawn(move || -> Result<()> { + let h = std::thread::spawn(move || -> Result<_> { let (stream, _addr) = listener.accept()?; let mut association = scp.establish(stream)?; @@ -64,13 +65,13 @@ fn spawn_scp() -> Result<(std::thread::JoinHandle>, SocketAddr)> { assert_eq!(pdu, Pdu::ReleaseRQ); association.send(&Pdu::ReleaseRP)?; - Ok(()) + Ok(association) }); Ok((h, addr)) } #[cfg(feature = "async")] -async fn spawn_scp_async() -> Result<(tokio::task::JoinHandle>, SocketAddr)> { +async fn spawn_scp_async() -> Result<(tokio::task::JoinHandle>>, SocketAddr)> { let listener = tokio::net::TcpListener::bind("localhost:0").await?; let addr = listener.local_addr()?; let scp = ServerAssociationOptions::new() @@ -110,7 +111,7 @@ async fn spawn_scp_async() -> Result<(tokio::task::JoinHandle>, Socke assert_eq!(pdu, Pdu::ReleaseRQ); association.send(&Pdu::ReleaseRP).await?; - Ok(()) + Ok(association) }); Ok((h, addr)) }