diff --git a/Cargo.lock b/Cargo.lock index c3b6f89fa..f6fe87dbd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] @@ -44,11 +44,17 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + [[package]] name = "backtrace" -version = "0.3.71" +version = "0.3.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" dependencies = [ "addr2line", "cc", @@ -61,9 +67,9 @@ dependencies = [ [[package]] name = "base64" -version = "0.22.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" @@ -73,9 +79,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "blake3" @@ -103,9 +109,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.95" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b" +checksum = "eaff6f8ce506b9773fa786672d63fc7a191ffea1be33f72bbd4aeacefca9ffc8" [[package]] name = "cfg-if" @@ -146,11 +152,27 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + [[package]] name = "crc32fast" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] @@ -166,15 +188,15 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "either" -version = "1.11.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "encode_unicode" @@ -184,9 +206,9 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "errno" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys", @@ -206,9 +228,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.29" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4556222738635b7a3417ae6130d8f52201e45a0c4d1a907f0826383adb5f85e7" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", "miniz_oxide", @@ -240,9 +262,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -251,9 +273,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] name = "idna" @@ -304,21 +326,21 @@ dependencies = [ [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.153" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "linux-raw-sys" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "llvm-sys" @@ -336,30 +358,58 @@ dependencies = [ [[package]] name = "log" -version = "0.4.21" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "memchr" -version = "2.7.2" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "miniz_oxide" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", ] +[[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", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "object" -version = "0.32.2" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" dependencies = [ "memchr", ] @@ -370,6 +420,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + [[package]] name = "percent-encoding" version = "2.3.1" @@ -384,9 +440,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.81" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] @@ -471,20 +527,29 @@ name = "rt" version = "0.15.0" dependencies = [ "backtrace", + "core-foundation", + "core-foundation-sys", "crossbeam-queue", "crossbeam-utils", "libc", + "once_cell", "rand", "rustix", + "rustls", + "rustls-native-certs", + "rustls-pemfile", + "rustls-webpki", + "security-framework", + "security-framework-sys", "socket2", "unicode-segmentation", ] [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustix" @@ -492,7 +557,7 @@ version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", @@ -501,11 +566,12 @@ dependencies = [ [[package]] name = "rustls" -version = "0.22.4" +version = "0.23.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +checksum = "4828ea528154ae444e5a642dbb7d5623354030dc9822b83fd9bb79683c7399d0" dependencies = [ "log", + "once_cell", "ring", "rustls-pki-types", "rustls-webpki", @@ -513,28 +579,84 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88d6d420651b496bdd98684116959239430022a115c1240e6c3993be0b15fba" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" +dependencies = [ + "base64", + "rustls-pki-types", +] + [[package]] name = "rustls-pki-types" -version = "1.5.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "beb461507cee2c2ff151784c52762cf4d9ff6a61f3e80968600ed24fa837fa54" +checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" [[package]] name = "rustls-webpki" -version = "0.102.3" +version = "0.102.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3bce581c0dd41bce533ce695a1437fa16a7ab5ac3ccfa99fe1a620a7885eabf" +checksum = "f9a6fccd794a42c2c105b513a2f62bc3fd8f3ba57a4593677ceb0bd035164d78" dependencies = [ "ring", "rustls-pki-types", "untrusted", ] +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "security-framework" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" +dependencies = [ + "bitflags 2.6.0", + "core-foundation", + "core-foundation-sys", + "libc", + "num-bigint", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "similar" @@ -558,9 +680,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.6" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", "windows-sys", @@ -574,15 +696,15 @@ checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "subtle" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.60" +version = "2.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +checksum = "2f0209b68b3613b093e0ec905354eccaedcfe83b8cb37cbdeae64026c3064c16" dependencies = [ "proc-macro2", "quote", @@ -591,9 +713,9 @@ dependencies = [ [[package]] name = "tar" -version = "0.4.40" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb" +checksum = "cb797dad5fb5b76fcf519e702f4a589483b5ef06567f160c392832c1f5e44909" dependencies = [ "filetime", "libc", @@ -601,18 +723,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.59" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.59" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", @@ -621,9 +743,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" dependencies = [ "tinyvec_macros", ] @@ -667,9 +789,9 @@ checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unicode-width" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" [[package]] name = "untrusted" @@ -679,25 +801,24 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "2.9.7" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d11a831e3c0b56e438a28308e7c810799e3c118417f342d30ecec080105395cd" +checksum = "72139d247e5f97a3eff96229a7ae85ead5328a39efe76f8bf5a06313d505b6ea" dependencies = [ "base64", "log", "once_cell", "rustls", "rustls-pki-types", - "rustls-webpki", "url", "webpki-roots", ] [[package]] name = "url" -version = "2.5.0" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", "idna", @@ -712,9 +833,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "webpki-roots" -version = "0.26.1" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3de34ae270483955a94f4b21bdaaeb83d508bb84a01435f393818edb0012009" +checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" dependencies = [ "rustls-pki-types", ] @@ -730,9 +851,9 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", @@ -746,54 +867,54 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "zeroize" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/compiler/src/linker.rs b/compiler/src/linker.rs index 39d20ceca..6ee66e134 100644 --- a/compiler/src/linker.rs +++ b/compiler/src/linker.rs @@ -256,7 +256,13 @@ pub(crate) fn link( cmd.arg("-lm"); cmd.arg("-lpthread"); } - _ => {} + OperatingSystem::Mac => { + // This is needed for TLS support. + for name in ["Security", "CoreFoundation"] { + cmd.arg("-framework"); + cmd.arg(name); + } + } } let mut static_linking = state.config.static_linking; diff --git a/rt/Cargo.toml b/rt/Cargo.toml index 9853cb5aa..e5e72df94 100644 --- a/rt/Cargo.toml +++ b/rt/Cargo.toml @@ -23,6 +23,35 @@ unicode-segmentation = "^1.10" backtrace = "^0.3" rustix = { version = "^0.38", features = ["fs", "mm", "param", "process", "net", "std", "time", "event"], default-features = false } +# The dependencies needed for TLS support. +# +# We use ring instead of the default aws-lc-sys because: +# +# 1. aws-lc-sys requires cmake to be installed when building on FreeBSD (and +# potentially other platforms), as aws-lc-sys only provides generated +# bindings for a limited set of platforms +# 2. aws-lc-sys increases compile times quite a bit +# 3. We don't care about FIPS compliance at the time of writing +rustls = { version = "^0.23", features = ["ring", "tls12", "std"], default-features = false } +rustls-pemfile = "^2.1" + +# These dependencies are used by the customized version of +# rustls-platform-modifier. We include a custom version so we can deal with the +# platform verification process being potentially slow. See +# https://github.com/rustls/rustls/issues/850 and +# https://github.com/inko-lang/inko/issues/329 for more details. +once_cell = "1.9" + +[target.'cfg(all(unix, not(target_os = "macos"), not(target_os = "ios"), not(target_os = "tvos")))'.dependencies] +rustls-native-certs = "0.7" +webpki = { package = "rustls-webpki", version = "0.102", default-features = false } + +[target.'cfg(any(target_os = "macos", target_os = "ios", target_os = "tvos"))'.dependencies] +core-foundation = "0.9" +core-foundation-sys = "0.8" +security-framework = { version = "2.10", features = ["OSX_10_14"] } +security-framework-sys = { version = "2.10", features = ["OSX_10_14"] } + [dependencies.socket2] version = "^0.5" features = ["all"] diff --git a/rt/src/lib.rs b/rt/src/lib.rs index 800f8c47c..ca3dd4c15 100644 --- a/rt/src/lib.rs +++ b/rt/src/lib.rs @@ -3,21 +3,22 @@ #![allow(clippy::missing_safety_doc)] #![allow(clippy::too_many_arguments)] -pub mod macros; +mod macros; -pub mod arc_without_weak; -pub mod config; -pub mod context; -pub mod mem; -pub mod memory_map; -pub mod network_poller; -pub mod process; -pub mod result; -pub mod runtime; -pub mod scheduler; -pub mod socket; -pub mod stack; -pub mod state; +mod arc_without_weak; +mod config; +mod context; +mod mem; +mod memory_map; +mod network_poller; +mod process; +mod result; +mod runtime; +mod rustls_platform_verifier; +mod scheduler; +mod socket; +mod stack; +mod state; #[cfg(test)] pub mod test; diff --git a/rt/src/network_poller.rs b/rt/src/network_poller.rs index e28c278d1..68c61aa7e 100644 --- a/rt/src/network_poller.rs +++ b/rt/src/network_poller.rs @@ -25,6 +25,7 @@ const CAPACITY: usize = 1024; pub(crate) type NetworkPoller = sys::Poller; /// The type of event a poller should wait for. +#[derive(Debug)] pub(crate) enum Interest { Read, Write, diff --git a/rt/src/network_poller/kqueue.rs b/rt/src/network_poller/kqueue.rs index 4d3790277..d71a08eb5 100644 --- a/rt/src/network_poller/kqueue.rs +++ b/rt/src/network_poller/kqueue.rs @@ -40,18 +40,20 @@ impl Poller { source: impl AsFd, interest: Interest, ) { - let fd = source.as_fd().as_raw_fd(); - let (add, del) = match interest { - Interest::Read => (EventFilter::Read(fd), EventFilter::Write(fd)), - Interest::Write => (EventFilter::Write(fd), EventFilter::Read(fd)), - }; let id = process.identifier() as isize; + let fd = source.as_fd().as_raw_fd(); let flags = EventFlags::CLEAR | EventFlags::ONESHOT | EventFlags::RECEIPT; - let events = [ - Event::new(add, EventFlags::ADD | flags, id), - Event::new(del, EventFlags::DELETE, 0), - ]; + let events = match interest { + Interest::Read => [ + Event::new(EventFilter::Read(fd), EventFlags::ADD | flags, id), + Event::new(EventFilter::Write(fd), EventFlags::DELETE, 0), + ], + Interest::Write => [ + Event::new(EventFilter::Write(fd), EventFlags::ADD | flags, id), + Event::new(EventFilter::Read(fd), EventFlags::DELETE, 0), + ], + }; self.apply(&events); } diff --git a/rt/src/process.rs b/rt/src/process.rs index 20d91659d..c6c8dd9ec 100644 --- a/rt/src/process.rs +++ b/rt/src/process.rs @@ -4,7 +4,6 @@ use crate::scheduler::process::Thread; use crate::scheduler::timeouts::Timeout; use crate::stack::Stack; use crate::state::State; -use backtrace; use std::alloc::{alloc, dealloc, handle_alloc_error, Layout}; use std::cell::UnsafeCell; use std::collections::VecDeque; diff --git a/rt/src/result.rs b/rt/src/result.rs index d72a8b71d..836b7d6a4 100644 --- a/rt/src/result.rs +++ b/rt/src/result.rs @@ -13,7 +13,11 @@ pub(crate) fn error_to_int(error: io::Error) -> i64 { // raw_os_error() above returns a None. Errno::TIMEDOUT.raw_os_error() } else { - -1 + match error.kind() { + io::ErrorKind::InvalidData => -2, + io::ErrorKind::UnexpectedEof => -3, + _ => -1, + } }; code as i64 @@ -60,7 +64,7 @@ impl Result { } pub(crate) fn io_error(error: io::Error) -> Result { - Self::error({ error_to_int(error) } as _) + Self::error(error_to_int(error) as _) } } diff --git a/rt/src/runtime.rs b/rt/src/runtime.rs index f027d53eb..1ae7b84fc 100644 --- a/rt/src/runtime.rs +++ b/rt/src/runtime.rs @@ -14,6 +14,7 @@ mod stdio; mod string; mod sys; mod time; +mod tls; use crate::config::Config; use crate::mem::ClassPointer; @@ -67,6 +68,12 @@ pub unsafe extern "system" fn inko_runtime_new( // does for us when compiling an executable. signal_sched::block_all(); + // Configure the TLS provider. This must be done once before we start the + // program. + rustls::crypto::ring::default_provider() + .install_default() + .expect("failed to set up the default TLS cryptography provider"); + Box::into_raw(Box::new(Runtime::new(&*counts, args))) } diff --git a/rt/src/runtime/env.rs b/rt/src/runtime/env.rs index dfb9dbd51..1f8bbb43f 100644 --- a/rt/src/runtime/env.rs +++ b/rt/src/runtime/env.rs @@ -40,26 +40,6 @@ pub unsafe extern "system" fn inko_env_size(state: *const State) -> i64 { (*state).environment.len() as _ } -#[no_mangle] -pub unsafe extern "system" fn inko_env_home_directory( - state: *const State, -) -> InkoResult { - let state = &*state; - - // Rather than performing all sorts of magical incantations to get the home - // directory, we're just going to require that HOME is set. - // - // If the home is explicitly set to an empty string we still ignore it, - // because there's no scenario in which Some("") is useful. - state - .environment - .get("HOME") - .filter(|&path| !path.is_empty()) - .cloned() - .map(|v| InkoResult::ok(InkoString::alloc(state.string_class, v) as _)) - .unwrap_or_else(InkoResult::none) -} - #[no_mangle] pub unsafe extern "system" fn inko_env_temp_directory( state: *const State, diff --git a/rt/src/runtime/helpers.rs b/rt/src/runtime/helpers.rs index 178fcbce5..38b4f924e 100644 --- a/rt/src/runtime/helpers.rs +++ b/rt/src/runtime/helpers.rs @@ -1,3 +1,9 @@ +use crate::context; +use crate::network_poller::Interest; +use crate::process::ProcessPointer; +use crate::scheduler::timeouts::Timeout; +use crate::socket::Socket; +use crate::state::State; use std::io::{self, Read}; /// Reads a number of bytes from a buffer into a Vec. @@ -14,3 +20,48 @@ pub(crate) fn read_into( Ok(read as i64) } + +pub(crate) fn poll( + state: &State, + mut process: ProcessPointer, + socket: &mut Socket, + interest: Interest, + deadline: i64, +) -> io::Result<()> { + let poll_id = unsafe { process.thread() }.network_poller; + + // We must keep the process' state lock open until everything is registered, + // otherwise a timeout thread may reschedule the process (i.e. the timeout + // is very short) before we finish registering the socket with a poller. + { + let mut proc_state = process.state(); + + // A deadline of -1 signals that we should wait indefinitely. + if deadline >= 0 { + let time = Timeout::until(deadline as u64); + + proc_state.waiting_for_io(Some(time.clone())); + state.timeout_worker.suspend(process, time); + } else { + proc_state.waiting_for_io(None); + } + + socket.register(state, process, poll_id, interest); + } + + // Safety: the current thread is holding on to the process' run lock, so if + // the process gets rescheduled onto a different thread, said thread won't + // be able to use it until we finish this context switch. + unsafe { context::switch(process) }; + + if process.timeout_expired() { + // The socket is still registered at this point, so we have to + // deregister first. If we don't and suspend for another IO operation, + // the poller could end up rescheduling the process multiple times (as + // there are multiple events still in flight for the process). + socket.deregister(state); + return Err(io::Error::from(io::ErrorKind::TimedOut)); + } + + Ok(()) +} diff --git a/rt/src/runtime/socket.rs b/rt/src/runtime/socket.rs index 496b51200..51ee49b41 100644 --- a/rt/src/runtime/socket.rs +++ b/rt/src/runtime/socket.rs @@ -1,10 +1,9 @@ -use crate::context; use crate::mem::{ByteArray, String as InkoString}; use crate::network_poller::Interest; use crate::process::ProcessPointer; use crate::result::{error_to_int, Result}; -use crate::scheduler::timeouts::Timeout; -use crate::socket::Socket; +use crate::runtime::helpers::poll; +use crate::socket::{read_from, Socket}; use crate::state::State; use std::io::{self, Write}; use std::ptr::{drop_in_place, write}; @@ -24,66 +23,33 @@ impl RawAddress { } } -fn blocking( +fn run( state: &State, - mut process: ProcessPointer, + process: ProcessPointer, socket: &mut Socket, interest: Interest, deadline: i64, mut func: impl FnMut(&mut Socket) -> io::Result, ) -> io::Result { match func(socket) { - Err(err) if err.kind() == io::ErrorKind::WouldBlock => {} - val => return val, - } - - let poll_id = unsafe { process.thread() }.network_poller; - - // We must keep the process' state lock open until everything is registered, - // otherwise a timeout thread may reschedule the process (i.e. the timeout - // is very short) before we finish registering the socket with a poller. - { - let mut proc_state = process.state(); - - // A deadline of -1 signals that we should wait indefinitely. - if deadline >= 0 { - let time = Timeout::until(deadline as u64); - - proc_state.waiting_for_io(Some(time.clone())); - state.timeout_worker.suspend(process, time); - } else { - proc_state.waiting_for_io(None); + Err(e) if e.kind() == io::ErrorKind::WouldBlock => { + poll(state, process, socket, interest, deadline) + .and_then(|_| func(socket)) } - - socket.register(state, process, poll_id, interest); + val => val, } - - // Safety: the current thread is holding on to the process' run lock, so if - // the process gets rescheduled onto a different thread, said thread won't - // be able to use it until we finish this context switch. - unsafe { context::switch(process) }; - - if process.timeout_expired() { - // The socket is still registered at this point, so we have to - // deregister first. If we don't and suspend for another IO operation, - // the poller could end up rescheduling the process multiple times (as - // there are multiple events still in flight for the process). - socket.deregister(state); - return Err(io::Error::from(io::ErrorKind::TimedOut)); - } - - func(socket) } #[no_mangle] pub(crate) unsafe extern "system" fn inko_socket_new( - proto: i64, + domain: i64, kind: i64, + proto: i64, out: *mut Socket, ) -> i64 { - let sock = match proto { - 0 => Socket::ipv4(kind), - 1 => Socket::ipv6(kind), + let sock = match domain { + 0 => Socket::ipv4(kind, proto), + 1 => Socket::ipv6(kind, proto), _ => Socket::unix(kind), }; @@ -108,7 +74,7 @@ pub(crate) unsafe extern "system" fn inko_socket_write( let state = &*state; let slice = std::slice::from_raw_parts(data, size as _); - blocking(state, process, &mut *socket, Interest::Write, deadline, |sock| { + run(state, process, &mut *socket, Interest::Write, deadline, |sock| { sock.write(slice) }) .map(|v| Result::ok(v as _)) @@ -126,8 +92,8 @@ pub unsafe extern "system" fn inko_socket_read( ) -> Result { let state = &*state; - blocking(state, process, &mut *socket, Interest::Read, deadline, |sock| { - sock.read(&mut (*buffer).value, amount as usize) + run(state, process, &mut *socket, Interest::Read, deadline, |sock| { + read_from(sock, &mut (*buffer).value, amount as usize) }) .map(|size| Result::ok(size as _)) .unwrap_or_else(Result::io_error) @@ -169,7 +135,7 @@ pub unsafe extern "system" fn inko_socket_connect( ) -> Result { let state = &*state; - blocking(state, process, &mut *socket, Interest::Write, deadline, |sock| { + run(state, process, &mut *socket, Interest::Write, deadline, |sock| { sock.connect(InkoString::read(address), port as u16) }) .map(|_| Result::none()) @@ -184,14 +150,10 @@ pub unsafe extern "system" fn inko_socket_accept( deadline: i64, out: *mut Socket, ) -> i64 { - let res = blocking( - &*state, - process, - &mut *socket, - Interest::Read, - deadline, - |sock| sock.accept(), - ); + let res = + run(&*state, process, &mut *socket, Interest::Read, deadline, |sock| { + sock.accept() + }); match res { Ok(val) => { @@ -213,14 +175,10 @@ pub unsafe extern "system" fn inko_socket_receive_from( out: *mut RawAddress, ) -> i64 { let state = &*state; - let res = blocking( - state, - process, - &mut *socket, - Interest::Read, - deadline, - |sock| sock.recv_from(&mut (*buffer).value, amount as _), - ); + let res = + run(state, process, &mut *socket, Interest::Read, deadline, |sock| { + sock.recv_from(&mut (*buffer).value, amount as _) + }); match res { Ok((addr, port)) => { @@ -244,7 +202,7 @@ pub unsafe extern "system" fn inko_socket_send_bytes_to( let state = &*state; let addr = InkoString::read(address); - blocking(state, process, &mut *socket, Interest::Write, deadline, |sock| { + run(state, process, &mut *socket, Interest::Write, deadline, |sock| { sock.send_to(&(*buffer).value, addr, port as _) }) .map(|size| Result::ok(size as _)) @@ -264,7 +222,7 @@ pub unsafe extern "system" fn inko_socket_send_string_to( let state = &*state; let addr = InkoString::read(address); - blocking(state, process, &mut *socket, Interest::Write, deadline, |sock| { + run(state, process, &mut *socket, Interest::Write, deadline, |sock| { sock.send_to(InkoString::read(buffer).as_bytes(), addr, port as _) }) .map(|size| Result::ok(size as _)) diff --git a/rt/src/runtime/tls.rs b/rt/src/runtime/tls.rs new file mode 100644 index 000000000..c309b346c --- /dev/null +++ b/rt/src/runtime/tls.rs @@ -0,0 +1,356 @@ +use crate::mem::{ByteArray, String as InkoString}; +use crate::network_poller::Interest; +use crate::process::ProcessPointer; +use crate::result::Result; +use crate::runtime::helpers::poll; +use crate::rustls_platform_verifier::tls_config; +use crate::socket::{read_from, Socket}; +use crate::state::State; +use rustls::pki_types::ServerName; +use rustls::{ + ClientConfig, ClientConnection, Error as TlsError, RootCertStore, + ServerConfig, ServerConnection, SideData, Stream, +}; +use rustls_pemfile::{private_key, read_all, Item}; +use std::fs::File; +use std::io::{self, BufReader, Write}; +use std::ops::{Deref, DerefMut}; +use std::sync::Arc; + +/// The error code produced when a TLS certificate is invalid. +const INVALID_CERT: isize = -1; + +/// The error code produced when a TLS private key is invalid. +const INVALID_KEY: isize = -2; + +unsafe fn run< + C: Deref> + DerefMut, + R, + S: SideData, +>( + state: *const State, + process: ProcessPointer, + socket: *mut Socket, + con: *mut C, + deadline: i64, + mut func: impl FnMut(&mut Stream) -> io::Result, +) -> io::Result { + let state = &*state; + let mut stream = Stream::new(&mut *con, &mut *socket); + + loop { + match func(&mut stream) { + Err(e) if e.kind() == io::ErrorKind::WouldBlock => { + let interest = if stream.conn.wants_write() { + Interest::Write + } else { + Interest::Read + }; + + poll(state, process, stream.sock, interest, deadline)?; + } + val => return val, + } + } +} + +unsafe fn tls_close< + C: Deref> + DerefMut, + S: SideData, +>( + state: *const State, + proc: ProcessPointer, + sock: *mut Socket, + con: *mut C, + deadline: i64, +) -> io::Result<()> { + (*con).send_close_notify(); + + while (*con).wants_write() { + run(state, proc, sock, con, deadline, |s| s.conn.write_tls(s.sock))?; + } + + Ok(()) +} + +#[no_mangle] +pub unsafe extern "system" fn inko_tls_client_config_new() -> *mut ClientConfig +{ + Arc::into_raw(Arc::new(tls_config())) as *mut _ +} + +#[no_mangle] +pub unsafe extern "system" fn inko_tls_client_config_with_certificate( + path: *const InkoString, +) -> Result { + let mut store = RootCertStore::empty(); + let mut reader = match File::open(InkoString::read(path)) { + Ok(f) => BufReader::new(f), + Err(e) => return Result::io_error(e), + }; + + for item in read_all(&mut reader) { + match item { + Ok(Item::X509Certificate(cert)) => { + // We don't want to expose a bunch of error messages/cases for + // the different reasons for a certificate being invalid, as + // it's not clear users actually care about that. As such, at + // least for the time being we just use a single opaque error + // for invalid certificates. + if store.add(cert).is_err() { + return Result::error(INVALID_CERT as _); + } + } + Ok(_) => return Result::error(INVALID_CERT as _), + Err(e) => return Result::io_error(e), + } + } + + // read_all() doesn't yield anything if the certificate file is empty, so we + // have to handle this case explicitly. + if store.is_empty() { + return Result::error(INVALID_CERT as _); + } + + let conf = Arc::new( + ClientConfig::builder() + .with_root_certificates(store) + .with_no_client_auth(), + ); + + Result::ok(Arc::into_raw(conf) as *mut _) +} + +#[no_mangle] +pub unsafe extern "system" fn inko_tls_client_config_clone( + config: *const ClientConfig, +) -> *const ClientConfig { + Arc::increment_strong_count(config); + config +} + +#[no_mangle] +pub unsafe extern "system" fn inko_tls_client_config_drop( + config: *const ClientConfig, +) { + drop(Arc::from_raw(config)); +} + +#[no_mangle] +pub unsafe extern "system" fn inko_tls_client_connection_new( + config: *const ClientConfig, + server: *const InkoString, +) -> Result { + let name = match ServerName::try_from(InkoString::read(server)) { + Ok(v) => v, + Err(_) => return Result::none(), + }; + + Arc::increment_strong_count(config); + + // ClientConnection::new() _can_ in theory fail, but based on the source + // code it seems this only happens when certain settings are adjusted, which + // we don't allow at this time. + let con = ClientConnection::new(Arc::from_raw(config), name) + .expect("failed to set up the TLS client connection"); + + Result::ok_boxed(con) +} + +#[no_mangle] +pub unsafe extern "system" fn inko_tls_client_connection_drop( + state: *mut ClientConnection, +) { + drop(Box::from_raw(state)); +} + +#[no_mangle] +pub unsafe extern "system" fn inko_tls_server_config_new( + cert_path: *const InkoString, + key_path: *const InkoString, +) -> Result { + let mut chain = Vec::new(); + let mut cert_reader = match File::open(InkoString::read(cert_path)) { + Ok(f) => BufReader::new(f), + Err(e) => return Result::io_error(e), + }; + + for item in read_all(&mut cert_reader) { + match item { + Ok(Item::X509Certificate(cert)) => chain.push(cert), + Ok(_) => return Result::error(INVALID_CERT as _), + Err(e) => return Result::io_error(e), + } + } + + // If the certificate file (and thus the chain) is empty there's no point in + // running the rest of the code, so we abort early. + if chain.is_empty() { + return Result::error(INVALID_CERT as _); + } + + let mut key_reader = match File::open(InkoString::read(key_path)) { + Ok(f) => BufReader::new(f), + Err(e) => return Result::io_error(e), + }; + let key = match private_key(&mut key_reader) { + Ok(Some(v)) => v, + Ok(None) => return Result::error(INVALID_KEY as _), + Err(e) => return Result::io_error(e), + }; + + let conf = match ServerConfig::builder() + .with_no_client_auth() + .with_single_cert(chain, key) + { + Ok(v) => v, + Err( + TlsError::NoCertificatesPresented + | TlsError::InvalidCertificate(_) + | TlsError::UnsupportedNameType + | TlsError::InvalidCertRevocationList(_), + ) => return Result::error(INVALID_CERT as _), + // For private key errors (and potentially others), rustls produces a + // `Error::General`, and in the future possibly other errors. The "one + // error type to rule them all" approach of rustls makes handling + // specific cases painful, so we just treat all remaining errors as + // private key errors. Given we already handle invalid certificates + // above, this should be correct (enough). + Err(_) => return Result::error(INVALID_KEY as _), + }; + + Result::ok(Arc::into_raw(Arc::new(conf)) as *mut _) +} + +#[no_mangle] +pub unsafe extern "system" fn inko_tls_server_config_clone( + config: *const ServerConfig, +) -> *const ServerConfig { + Arc::increment_strong_count(config); + config +} + +#[no_mangle] +pub unsafe extern "system" fn inko_tls_server_config_drop( + config: *const ServerConfig, +) { + drop(Arc::from_raw(config)); +} + +#[no_mangle] +pub unsafe extern "system" fn inko_tls_server_connection_new( + config: *const ServerConfig, +) -> *mut ServerConnection { + Arc::increment_strong_count(config); + + // ServerConnection::new() _can_ in theory fail, but based on the source + // code it seems this only happens when certain settings are adjusted, which + // we don't allow at this time. + let con = ServerConnection::new(Arc::from_raw(config)) + .expect("failed to set up the TLS server connection"); + + Box::into_raw(Box::new(con)) +} + +#[no_mangle] +pub unsafe extern "system" fn inko_tls_server_connection_drop( + state: *mut ServerConnection, +) { + drop(Box::from_raw(state)); +} + +#[no_mangle] +pub unsafe extern "system" fn inko_tls_client_write( + state: *const State, + proc: ProcessPointer, + sock: *mut Socket, + con: *mut ClientConnection, + data: *mut u8, + size: i64, + deadline: i64, +) -> Result { + let buf = std::slice::from_raw_parts(data, size as _); + + run(state, proc, sock, con, deadline, |s| s.write(buf)) + .map(|v| Result::ok(v as _)) + .unwrap_or_else(Result::io_error) +} + +#[no_mangle] +pub unsafe extern "system" fn inko_tls_client_read( + state: *const State, + proc: ProcessPointer, + sock: *mut Socket, + con: *mut ClientConnection, + buffer: *mut ByteArray, + amount: i64, + deadline: i64, +) -> Result { + let buf = &mut (*buffer).value; + let len = amount as usize; + + run(state, proc, sock, con, deadline, |s| read_from(s, buf, len)) + .map(|v| Result::ok(v as _)) + .unwrap_or_else(Result::io_error) +} + +#[no_mangle] +pub unsafe extern "system" fn inko_tls_client_close( + state: *const State, + proc: ProcessPointer, + sock: *mut Socket, + con: *mut ClientConnection, + deadline: i64, +) -> Result { + tls_close(state, proc, sock, con, deadline) + .map(|_| Result::none()) + .unwrap_or_else(Result::io_error) +} + +#[no_mangle] +pub unsafe extern "system" fn inko_tls_server_write( + state: *const State, + proc: ProcessPointer, + sock: *mut Socket, + con: *mut ServerConnection, + data: *mut u8, + size: i64, + deadline: i64, +) -> Result { + let buf = std::slice::from_raw_parts(data, size as _); + + run(state, proc, sock, con, deadline, |s| s.write(buf)) + .map(|v| Result::ok(v as _)) + .unwrap_or_else(Result::io_error) +} + +#[no_mangle] +pub unsafe extern "system" fn inko_tls_server_read( + state: *const State, + proc: ProcessPointer, + sock: *mut Socket, + con: *mut ServerConnection, + buffer: *mut ByteArray, + amount: i64, + deadline: i64, +) -> Result { + let buf = &mut (*buffer).value; + let len = amount as usize; + + run(state, proc, sock, con, deadline, |s| read_from(s, buf, len)) + .map(|v| Result::ok(v as _)) + .unwrap_or_else(Result::io_error) +} + +#[no_mangle] +pub unsafe extern "system" fn inko_tls_server_close( + state: *const State, + proc: ProcessPointer, + sock: *mut Socket, + con: *mut ServerConnection, + deadline: i64, +) -> Result { + tls_close(state, proc, sock, con, deadline) + .map(|_| Result::none()) + .unwrap_or_else(Result::io_error) +} diff --git a/rt/src/rustls_platform_verifier/LICENSE b/rt/src/rustls_platform_verifier/LICENSE new file mode 100644 index 000000000..996412264 --- /dev/null +++ b/rt/src/rustls_platform_verifier/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 1Password + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/rt/src/rustls_platform_verifier/mod.rs b/rt/src/rustls_platform_verifier/mod.rs new file mode 100644 index 000000000..dcd8f7bdb --- /dev/null +++ b/rt/src/rustls_platform_verifier/mod.rs @@ -0,0 +1,41 @@ +use rustls::ClientConfig; +use std::sync::Arc; + +mod verification; +pub use verification::Verifier; + +/// Creates and returns a `rustls` configuration that verifies TLS +/// certificates in the best way for the underlying OS platform, using +/// safe defaults for the `rustls` configuration. +/// +/// # Example +/// +/// This example shows how to use the custom verifier with the `reqwest` crate: +/// ```ignore +/// # use reqwest::ClientBuilder; +/// #[tokio::main] +/// async fn main() { +/// let client = ClientBuilder::new() +/// .use_preconfigured_tls(rustls_platform_verifier::tls_config()) +/// .build() +/// .expect("nothing should fail"); +/// +/// let _response = client.get("https://example.com").send().await; +/// } +/// ``` +/// +/// **Important:** You must ensure that your `reqwest` version is using the same Rustls +/// version as this crate or it will panic when downcasting the `&dyn Any` verifier. +/// +/// If you require more control over the rustls `ClientConfig`, you can +/// instantiate a [Verifier] with [Verifier::default] and then use it +/// with [`DangerousClientConfigBuilder::with_custom_certificate_verifier`][rustls::client::danger::DangerousClientConfigBuilder::with_custom_certificate_verifier]. +/// +/// Refer to the crate level documentation to see what platforms +/// are currently supported. +pub fn tls_config() -> ClientConfig { + ClientConfig::builder() + .dangerous() + .with_custom_certificate_verifier(Arc::new(Verifier::new())) + .with_no_client_auth() +} diff --git a/rt/src/rustls_platform_verifier/verification/apple.rs b/rt/src/rustls_platform_verifier/verification/apple.rs new file mode 100644 index 000000000..933c0f590 --- /dev/null +++ b/rt/src/rustls_platform_verifier/verification/apple.rs @@ -0,0 +1,238 @@ +use super::log_server_cert; +use crate::rustls_platform_verifier::verification::invalid_certificate; +use core_foundation::date::CFDate; +use core_foundation_sys::date::kCFAbsoluteTimeIntervalSince1970; +use once_cell::sync::OnceCell; +use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerifier}; +use rustls::crypto::{ + verify_tls12_signature, verify_tls13_signature, CryptoProvider, +}; +use rustls::pki_types; +use rustls::{ + CertificateError, DigitallySignedStruct, Error as TlsError, OtherError, + SignatureScheme, +}; +use security_framework::{ + certificate::SecCertificate, policy::SecPolicy, + secure_transport::SslProtocolSide, trust::SecTrust, +}; +use std::sync::Arc; + +use crate::process::ProcessPointer; +use crate::scheduler::process::CURRENT_PROCESS; + +mod errors { + pub(super) use security_framework_sys::base::{ + errSecCertificateRevoked, errSecCreateChainFailed, + errSecHostNameMismatch, errSecInvalidExtendedKeyUsage, + }; +} + +#[allow(clippy::as_conversions)] +fn system_time_to_cfdate( + time: pki_types::UnixTime, +) -> Result { + // SAFETY: The interval is defined by macOS externally, but is always present and never modified at runtime + // since its a global variable. + // + // See https://developer.apple.com/documentation/corefoundation/kcfabsolutetimeintervalsince1970. + let unix_adjustment = unsafe { kCFAbsoluteTimeIntervalSince1970 as u64 }; + + // Convert a system timestamp based off the UNIX epoch into the + // Apple epoch used by all `CFAbsoluteTime` values. + // Subtracting Durations with sub() will panic on overflow + time.as_secs() + .checked_sub(unix_adjustment) + .ok_or(TlsError::FailedToGetCurrentTime) + .map(|epoch| CFDate::new(epoch as f64)) +} + +/// A TLS certificate verifier that utilizes the Apple platform certificate facilities. +#[derive(Debug)] +pub struct Verifier { + pub(super) crypto_provider: OnceCell>, +} + +impl Verifier { + /// Creates a new instance of a TLS certificate verifier that utilizes the macOS certificate + /// facilities. + /// + /// A [`CryptoProvider`] must be set with + /// [`set_provider`][Verifier::set_provider]/[`with_provider`][Verifier::with_provider] or + /// [`CryptoProvider::install_default`] before the verifier can be used. + pub fn new() -> Self { + Self { crypto_provider: OnceCell::new() } + } + + fn verify_certificate( + &self, + end_entity: &pki_types::CertificateDer<'_>, + intermediates: &[pki_types::CertificateDer<'_>], + server_name: &str, + ocsp_response: Option<&[u8]>, + now: pki_types::UnixTime, + ) -> Result<(), TlsError> { + let certificates: Vec = + std::iter::once(end_entity.as_ref()) + .chain(intermediates.iter().map(|cert| cert.as_ref())) + .map(|cert| { + SecCertificate::from_der(cert).map_err(|_| { + TlsError::InvalidCertificate( + CertificateError::BadEncoding, + ) + }) + }) + .collect::, _>>()?; + + // Create our verification policy suitable for verifying TLS chains. + // This uses the "default" verification engine and parameters, the same as Windows. + // + // The protocol side should be set to `server` for a client to verify server TLS + // certificates. + // + // The server name will be required to match what the end-entity certificate reports + // + // Ref: https://developer.apple.com/documentation/security/1392592-secpolicycreatessl + let policy = + SecPolicy::create_ssl(SslProtocolSide::SERVER, Some(server_name)); + + // Create our trust evaluation context/chain. + // + // Apple requires that the certificate to be verified is always first in the array, and we + // always place the end-entity certificate at the start. + // + // Ref: https://developer.apple.com/documentation/security/1401555-sectrustcreatewithcertificates + let mut trust_evaluation = + SecTrust::create_with_certificates(&certificates, &[policy]) + .map_err(|e| TlsError::General(e.to_string()))?; + + // Tell the system that we want to consider the certificates evaluation at the point + // in time that `rustls` provided. + let now = system_time_to_cfdate(now)?; + trust_evaluation + .set_trust_verify_date(&now) + .map_err(|e| invalid_certificate(e.to_string()))?; + + // If we have OCSP response data, make sure the system makes use of it. + if let Some(ocsp_response) = ocsp_response { + trust_evaluation + .set_trust_ocsp_response(std::iter::once(ocsp_response)) + .map_err(|e| invalid_certificate(e.to_string()))?; + } + + // Safety: well, technically none, but due to the way the runtime uses + // the verifier this should never misbehave. + let process = unsafe { ProcessPointer::new(CURRENT_PROCESS.get()) }; + let trust_error = + match process.blocking(|| trust_evaluation.evaluate_with_error()) { + Ok(()) => return Ok(()), + Err(e) => e, + }; + + let err_code = trust_error.code(); + + let err = err_code + .try_into() + .map_err(|_| ()) + .and_then(|code| { + // Only map the errors we need for tests. + match code { + errors::errSecHostNameMismatch => Ok(TlsError::InvalidCertificate( + CertificateError::NotValidForName, + )), + errors::errSecCreateChainFailed => Ok(TlsError::InvalidCertificate( + CertificateError::UnknownIssuer, + )), + errors::errSecInvalidExtendedKeyUsage => Ok(TlsError::InvalidCertificate( + CertificateError::Other(OtherError(std::sync::Arc::new(super::EkuError))), + )), + errors::errSecCertificateRevoked => { + Ok(TlsError::InvalidCertificate(CertificateError::Revoked)) + } + _ => Err(()), + } + }) + // Fallback to an error containing the description and specific error code so that + // the exact error cause can be looked up easily. + .unwrap_or_else(|_| invalid_certificate(format!("{}: {}", trust_error, err_code))); + + Err(err) + } +} + +impl ServerCertVerifier for Verifier { + fn verify_server_cert( + &self, + end_entity: &pki_types::CertificateDer<'_>, + intermediates: &[pki_types::CertificateDer<'_>], + server_name: &pki_types::ServerName, + ocsp_response: &[u8], + now: pki_types::UnixTime, + ) -> Result { + log_server_cert(end_entity); + + // Convert IP addresses to name strings to ensure match check on leaf certificate. + // Ref: https://developer.apple.com/documentation/security/1392592-secpolicycreatessl + let server = server_name.to_str(); + + let ocsp_data = + if !ocsp_response.is_empty() { Some(ocsp_response) } else { None }; + + match self.verify_certificate( + end_entity, + intermediates, + &server, + ocsp_data, + now, + ) { + Ok(()) => { + Ok(rustls::client::danger::ServerCertVerified::assertion()) + } + Err(e) => { + // This error only tells us what the system errored with, so it doesn't leak anything + // sensitive. + Err(e) + } + } + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &pki_types::CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + verify_tls12_signature( + message, + cert, + dss, + &self.get_provider().signature_verification_algorithms, + ) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &pki_types::CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + verify_tls13_signature( + message, + cert, + dss, + &self.get_provider().signature_verification_algorithms, + ) + } + + fn supported_verify_schemes(&self) -> Vec { + self.get_provider() + .signature_verification_algorithms + .supported_schemes() + } +} + +impl Default for Verifier { + fn default() -> Self { + Self::new() + } +} diff --git a/rt/src/rustls_platform_verifier/verification/mod.rs b/rt/src/rustls_platform_verifier/verification/mod.rs new file mode 100644 index 000000000..4469a3453 --- /dev/null +++ b/rt/src/rustls_platform_verifier/verification/mod.rs @@ -0,0 +1,59 @@ +use rustls::crypto::CryptoProvider; +use std::sync::Arc; + +#[cfg(all( + not(target_os = "macos"), + not(target_os = "ios"), + not(target_os = "tvos") +))] +mod others; + +#[cfg(all( + not(target_os = "macos"), + not(target_os = "ios"), + not(target_os = "tvos") +))] +pub use others::Verifier; + +#[cfg(any(target_os = "macos", target_os = "ios", target_os = "tvos"))] +mod apple; + +#[cfg(any(target_os = "macos", target_os = "ios", target_os = "tvos"))] +pub use apple::Verifier; + +/// An EKU was invalid for the use case of verifying a server certificate. +/// +/// This error is used primarily for tests. +#[derive(Debug, PartialEq)] +pub(crate) struct EkuError; + +impl std::fmt::Display for EkuError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("certificate had invalid extensions") + } +} + +impl std::error::Error for EkuError {} + +// Log the certificate we are verifying so that we can try and find what may be wrong with it +// if we need to debug a user's situation. +fn log_server_cert(_end_entity: &rustls::pki_types::CertificateDer<'_>) {} + +// Unknown certificate error shorthand. Used when we need to construct an "Other" certificate +// error with a platform specific error message. +#[cfg(any(target_os = "macos", target_os = "ios", target_os = "tvos"))] +fn invalid_certificate(reason: impl Into) -> rustls::Error { + rustls::Error::InvalidCertificate(rustls::CertificateError::Other( + rustls::OtherError(Arc::from(Box::from(reason.into()))), + )) +} + +impl Verifier { + fn get_provider(&self) -> &Arc { + self.crypto_provider.get_or_init(|| { + CryptoProvider::get_default() + .expect("rustls default CryptoProvider not set") + .clone() + }) + } +} diff --git a/rt/src/rustls_platform_verifier/verification/others.rs b/rt/src/rustls_platform_verifier/verification/others.rs new file mode 100644 index 000000000..3b247d936 --- /dev/null +++ b/rt/src/rustls_platform_verifier/verification/others.rs @@ -0,0 +1,166 @@ +use super::log_server_cert; +use once_cell::sync::OnceCell; +use rustls::client::danger::{ + HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier, +}; +use rustls::client::WebPkiServerVerifier; +use rustls::pki_types; +use rustls::{ + crypto::CryptoProvider, CertificateError, DigitallySignedStruct, + Error as TlsError, OtherError, SignatureScheme, +}; +use std::fmt::Debug; +use std::sync::{Arc, Mutex}; + +/// A TLS certificate verifier that uses the system's root store and WebPKI. +#[derive(Debug)] +pub struct Verifier { + // We use a `OnceCell` so we only need + // to try loading native root certs once per verifier. + // + // We currently keep one set of certificates per-verifier so that + // locking and unlocking the application will pull fresh root + // certificates from disk, picking up on any changes + // that might have been made since. + inner: OnceCell>, + + // Extra trust anchors to add to the verifier above and beyond those provided by the + // platform via rustls-native-certs. + extra_roots: Mutex>>, + + pub(super) crypto_provider: OnceCell>, +} + +impl Verifier { + /// Creates a new verifier whose certificate validation is provided by + /// WebPKI, using root certificates provided by the platform. + /// + /// A [`CryptoProvider`] must be set with + /// [`set_provider`][Verifier::set_provider]/[`with_provider`][Verifier::with_provider] or + /// [`CryptoProvider::install_default`] before the verifier can be used. + pub fn new() -> Self { + Self { + inner: OnceCell::new(), + extra_roots: Vec::new().into(), + crypto_provider: OnceCell::new(), + } + } + + fn get_or_init_verifier( + &self, + ) -> Result<&Arc, TlsError> { + self.inner.get_or_try_init(|| self.init_verifier()) + } + + // Attempt to load CA root certificates present on system, fallback to WebPKI roots if error + fn init_verifier(&self) -> Result, TlsError> { + let mut root_store = rustls::RootCertStore::empty(); + + // Safety: There's no way for the mutex to be locked multiple times, so this is + // an infallible operation. + let mut extra_roots = self.extra_roots.try_lock().unwrap(); + if !extra_roots.is_empty() { + root_store.extend(extra_roots.drain(..)); + } + + #[cfg(all( + unix, + not(target_os = "macos"), + not(target_os = "ios"), + not(target_os = "tvos"), + ))] + match rustls_native_certs::load_native_certs() { + Ok(certs) => { + root_store.add_parsable_certificates(certs); + } + Err(err) => { + // This only contains a path to a system directory: + // https://github.com/rustls/rustls-native-certs/blob/bc13b9a6bfc2e1eec881597055ca49accddd972a/src/lib.rs#L91-L94 + const MSG: &str = "failed to load system root certificates: "; + + // Don't return an error if this fails when other roots have already been loaded via + // `new_with_extra_roots`. It leads to extra failure cases where connections would otherwise still work. + if root_store.is_empty() { + return Err(rustls::Error::General(format!("{MSG}{err}"))); + } + } + }; + + WebPkiServerVerifier::builder_with_provider( + root_store.into(), + Arc::clone(self.get_provider()), + ) + .build() + .map_err(|e| TlsError::Other(OtherError(Arc::new(e)))) + } +} + +impl ServerCertVerifier for Verifier { + fn verify_server_cert( + &self, + end_entity: &pki_types::CertificateDer<'_>, + intermediates: &[pki_types::CertificateDer<'_>], + server_name: &pki_types::ServerName, + ocsp_response: &[u8], + now: pki_types::UnixTime, + ) -> Result { + log_server_cert(end_entity); + + self.get_or_init_verifier()? + .verify_server_cert( + end_entity, + intermediates, + server_name, + ocsp_response, + now, + ) + .map_err(map_webpki_errors) + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &pki_types::CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + self.get_or_init_verifier()?.verify_tls12_signature(message, cert, dss) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &pki_types::CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + self.get_or_init_verifier()?.verify_tls13_signature(message, cert, dss) + } + + fn supported_verify_schemes(&self) -> Vec { + match self.get_or_init_verifier() { + Ok(v) => v.supported_verify_schemes(), + Err(_) => Vec::default(), + } + } +} + +impl Default for Verifier { + fn default() -> Self { + Self::new() + } +} + +fn map_webpki_errors(err: TlsError) -> TlsError { + if let TlsError::InvalidCertificate(CertificateError::Other(other_err)) = + &err + { + if let Some(webpki::Error::RequiredEkuNotFound) = + other_err.0.downcast_ref::() + { + return TlsError::InvalidCertificate(CertificateError::Other( + OtherError(Arc::new(super::EkuError)), + )); + } + } + + err +} diff --git a/rt/src/scheduler/process.rs b/rt/src/scheduler/process.rs index 5a2cdc4ec..31394f386 100644 --- a/rt/src/scheduler/process.rs +++ b/rt/src/scheduler/process.rs @@ -10,10 +10,12 @@ use crossbeam_utils::atomic::AtomicCell; use crossbeam_utils::thread::scope; use rand::rngs::ThreadRng; use rand::thread_rng; +use std::cell::Cell; use std::cmp::min; use std::collections::VecDeque; use std::mem::{size_of, swap}; use std::ops::Drop; +use std::ptr::null_mut; use std::sync::atomic::{AtomicBool, AtomicU16, AtomicU64, Ordering}; use std::sync::{Condvar, Mutex}; use std::thread::sleep; @@ -86,6 +88,18 @@ const MONITOR_INTERVAL: u64 = 100; /// we perform a number of regular cycles before entering a deep sleep. const MAX_IDLE_CYCLES: u64 = 1_000_000 / MONITOR_INTERVAL; +thread_local! { + /// The process that's currently running. + /// + /// This threat-local should only be used when access to the current process + /// is needed, but the process can't be passed in as an argument. An example + /// is the patched version of rustls-platform-verifier: it needs access to + /// the current process, but the rustls API doesn't make this possible. + pub(crate) static CURRENT_PROCESS: Cell<*mut Process> = const { + Cell::new(null_mut()) + }; +} + pub(crate) fn epoch_loop(state: &State) { while state.scheduler.pool.is_alive() { sleep(Duration::from_millis(EPOCH_INTERVAL)); @@ -533,15 +547,19 @@ impl Thread { match process.next_task() { Task::Resume => { + CURRENT_PROCESS.set(process.as_ptr()); process.resume(state, self); unsafe { context::switch(process) } } Task::Start(func, args) => { + CURRENT_PROCESS.set(process.as_ptr()); process.resume(state, self); unsafe { context::start(process, func, args) } } Task::Wait => return, } + + CURRENT_PROCESS.set(null_mut()); } match self.action.take() { diff --git a/rt/src/socket.rs b/rt/src/socket.rs index 5f881cf23..801bfc7a9 100644 --- a/rt/src/socket.rs +++ b/rt/src/socket.rs @@ -5,7 +5,7 @@ use crate::process::ProcessPointer; use crate::socket::socket_address::SocketAddress; use crate::state::State; use rustix::io::Errno; -use socket2::{Domain, SockAddr, Socket as RawSocket, Type}; +use socket2::{Domain, Protocol, SockAddr, Socket as RawSocket, Type}; use std::io::{self, Read}; use std::mem::transmute; use std::net::Shutdown; @@ -62,42 +62,22 @@ fn encode_sockaddr( /// The slice has enough space to store up to `bytes` of data. fn socket_output_slice(buffer: &mut Vec, bytes: usize) -> &mut [u8] { let len = buffer.len(); - let available = buffer.capacity() - len; - - if bytes > available { - let to_reserve = bytes - available; - - if to_reserve > 0 { - // Only increasing capacity when needed is done for two reasons: - // - // 1. It saves us from increasing capacity when there is enough - // space. - // - // 2. Due to sockets being non-blocking, a socket operation may - // fail. This will result in this code being called multiple - // times. If we were to simply increase capacity every time we'd - // end up growing the buffer much more than necessary. - buffer.reserve_exact(to_reserve); - } - } + buffer.reserve_exact(bytes); unsafe { slice::from_raw_parts_mut(buffer.as_mut_ptr().add(len), bytes) } } -fn update_buffer_length_and_capacity(buffer: &mut Vec, read: usize) { +fn update_buffer_length(buffer: &mut Vec, read: usize) { unsafe { buffer.set_len(buffer.len() + read); } - - buffer.shrink_to_fit(); } fn socket_type(kind: i64) -> io::Result { match kind { 0 => Ok(Type::STREAM), 1 => Ok(Type::DGRAM), - 2 => Ok(Type::SEQPACKET), - 3 => Ok(Type::RAW), + 2 => Ok(Type::RAW), _ => Err(io::Error::new( io::ErrorKind::Other, format!("{} is not a valid socket type", kind), @@ -105,6 +85,37 @@ fn socket_type(kind: i64) -> io::Result { } } +fn socket_protocol(value: i64) -> Option { + if value == 0 { + None + } else { + Some(Protocol::from(value as i32)) + } +} + +pub(crate) fn read_from( + reader: &mut R, + into: &mut Vec, + amount: usize, +) -> io::Result { + if amount > 0 { + // We don't use take(), because that only terminates if: + // + // 1. We hit EOF, or + // 2. We have read the desired number of bytes + // + // For files this is fine, but for sockets EOF is not triggered + // until the socket is closed; which is almost always too late. + let slice = socket_output_slice(into, amount); + let read = reader.read(slice)?; + + update_buffer_length(into, read); + Ok(read) + } else { + Ok(reader.read_to_end(into)?) + } +} + /// A nonblocking socket that can be registered with a `NetworkPoller`. /// /// When changing the layout of this type, don't forget to also update its @@ -133,12 +144,12 @@ impl Socket { pub(crate) fn new( domain: Domain, kind: Type, + protocol: Option, unix: bool, ) -> io::Result { - let socket = RawSocket::new(domain, kind, None)?; + let socket = RawSocket::new(domain, kind, protocol)?; socket.set_nonblocking(true)?; - Ok(Socket { inner: socket, registered: AtomicI8::new(NOT_REGISTERED), @@ -146,17 +157,27 @@ impl Socket { }) } - pub(crate) fn ipv4(kind_int: i64) -> io::Result { - Self::new(Domain::IPV4, socket_type(kind_int)?, false) + pub(crate) fn ipv4(kind_int: i64, protocol: i64) -> io::Result { + Self::new( + Domain::IPV4, + socket_type(kind_int)?, + socket_protocol(protocol), + false, + ) } - pub(crate) fn ipv6(kind_int: i64) -> io::Result { - Self::new(Domain::IPV6, socket_type(kind_int)?, false) + pub(crate) fn ipv6(kind_int: i64, protocol: i64) -> io::Result { + Self::new( + Domain::IPV6, + socket_type(kind_int)?, + socket_protocol(protocol), + false, + ) } #[cfg(unix)] pub(crate) fn unix(kind_int: i64) -> io::Result { - Self::new(Domain::UNIX, socket_type(kind_int)?, true) + Self::new(Domain::UNIX, socket_type(kind_int)?, None, true) } #[cfg(not(unix))] @@ -260,29 +281,6 @@ impl Socket { }) } - pub(crate) fn read( - &self, - buffer: &mut Vec, - amount: usize, - ) -> io::Result { - if amount > 0 { - // We don't use take(), because that only terminates if: - // - // 1. We hit EOF, or - // 2. We have read the desired number of bytes - // - // For files this is fine, but for sockets EOF is not triggered - // until the socket is closed; which is almost always too late. - let slice = socket_output_slice(buffer, amount); - let read = self.inner.recv(unsafe { transmute(slice) })?; - - update_buffer_length_and_capacity(buffer, read); - Ok(read) - } else { - Ok((&self.inner).read_to_end(buffer)?) - } - } - pub(crate) fn recv_from( &self, buffer: &mut Vec, @@ -292,8 +290,7 @@ impl Socket { let (read, sockaddr) = self.inner.recv_from(unsafe { transmute(slice) })?; - update_buffer_length_and_capacity(buffer, read); - + update_buffer_length(buffer, read); decode_sockaddr(sockaddr, self.unix) .map_err(|err| io::Error::new(io::ErrorKind::Other, err)) } @@ -358,7 +355,7 @@ impl io::Write for Socket { impl io::Read for Socket { fn read(&mut self, buf: &mut [u8]) -> io::Result { - self.inner.recv(unsafe { transmute(buf) }) + self.inner.read(buf) } } @@ -369,7 +366,7 @@ mod tests { #[test] fn test_try_clone() { - let socket1 = Socket::ipv4(0).unwrap(); + let socket1 = Socket::ipv4(0, 0).unwrap(); socket1.registered.store(2, Ordering::Release); diff --git a/std/fixtures/tls/README.md b/std/fixtures/tls/README.md new file mode 100644 index 000000000..8164aff0d --- /dev/null +++ b/std/fixtures/tls/README.md @@ -0,0 +1,12 @@ +This directory contains an X.509 certificate and private key, and a +configuration file used to generate them. These files are used for running the +tests of std.net.tls. Under no circumstance should you use these certificates +anywhere else. I repeat: + +DO NOT USE THESE CERTIFICATES, THEY ARE FOR TESTING PURPOSES ONLY. + +The certificate and private key are generated as follows: + +```bash +openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout test.key -out test.pem -config test.cnf -sha25 +``` diff --git a/std/fixtures/tls/empty.key b/std/fixtures/tls/empty.key new file mode 100644 index 000000000..e69de29bb diff --git a/std/fixtures/tls/empty.pem b/std/fixtures/tls/empty.pem new file mode 100644 index 000000000..e69de29bb diff --git a/std/fixtures/tls/invalid.key b/std/fixtures/tls/invalid.key new file mode 100644 index 000000000..d45f833be --- /dev/null +++ b/std/fixtures/tls/invalid.key @@ -0,0 +1,24 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCyU87LWHj+r1Dj +dZQtA0EWy5TmBjf8Rr5tv4eiSolFYAD1kfyelutxj6mMzgk1cvePpC8npPzZRhwO +/8vGZshRw2D0Vdgq8XN8B6G35S4PhhdgHsvGMSX6s5EBk4Xj730YKR9NaSpyHSIa +LEYcmM3Ryv+iKEwnEPw+/oFHeW56J197dWiLIHEDM8oC82lX5MT/cYm2QG4CsC/7 +TCDDT1sEVhkdPB60Scks/Ln5ACsWYInH67AvXWDOaBSebBRR+wFGckgu2kldahIc +e0ejawKa5xgvOX00JmQyigDwqm3NMxjdGOg7y27aNWw8Vbo/oRDif775DvtNIn9C +1nJB8wNjAgMBAAECggEAFuAJo2u7yE6HQvOmvdLF4IgzQgHjNaEs4AQqgHGCRAbJ +fgwrinu+mQh+OI8yKYvlYM+FXaxcOPzgMDZflpmXBxICgmVD/6zjDQfSQWDh3zZA +EmGbmdayfK3YeIogKeSN40cHJRV2pJZtyktf9Ql5ls4CVnPyjNewxoiRidsfBlvc +IoCRjiTD6+MHOQjp4AzwVvbXH1Sr7OsngA4glQJjFlXllyVYQNXBr1sWTVl3TS2L +OQfqYzHWlRtty88z7ExK3D03Jz0PD7qWtTwgJkq1ON+PjgCf7rMvZbVmCoUgX759 +LLxY1NE6ogdoRlPyZG3fvSfmxo2PWOqawboZMvirAQKBgQDVwkYwlbY00XuHA4UF +bGwTyk5Yp0/DVSR4jZyAJN6J5xln0JUWYKeps8kxLfDlPgmn6qIaine9Ewf1D9qt +DGr19mzEHzP07OBRo0l34XE5WUMvV4ter3swwmI5w/ysgds7Mz7xA9a69ukF/7SW +C+2RiVW7hJs1pydQw7+NY9YIYwKBgQDVkRv4tvmITtfPpVjAkw0gPIQ2WLw4uIvk +PIX/A58dg952ga+C4MZ7OFtcKI7CF7anr0gCNdGQS6I3SA18YS49U/zycuzPh9v7 +lcIMV+R0Wvo2B6QIGJpt7FzfZBXGdv/ft5l+MII3jpoGqGu1K3Ifj/zUrDlUJDQq +ivrkH+CJAQKBgQDFcbCRugfWW9TlDhw1uUNPOGQLwWeMvr10WSHAv82KxZsS6Hh9 +dgQIXZeuRIgpx5b1smXPbC1TyRtlgiJ0C29VCCzJLyU3zAEbh18aS3PhDBFhzlRe +vmpkzHgccWqYEU5mLVyrFOeoRN9S+jFdE2F6N8en8MHI2kAXeugZeqk9jwKBgCmV +pMWsEzCIcZs8DekJeR/SyMewRY4h2RNq+YhrUxszJykaHWu1itBJa/io6QtABM/n +4HSVuCWJpJ9xBzc10QQeC33GBPhv8tStF2jB4HkLkfbdTAJLkB5hTMAuw9KuLyqH +-----END PRIVATE KEY----- diff --git a/std/fixtures/tls/invalid.pem b/std/fixtures/tls/invalid.pem new file mode 100644 index 000000000..208614514 --- /dev/null +++ b/std/fixtures/tls/invalid.pem @@ -0,0 +1,10 @@ +-----BEGIN CERTIFICATE----- +MIIDvDCCAqSgAwIBAgIUBwbzzsn/P9HmSv1o2tAoIH6ZYE0wDQYJKoZIhvcNAQEL +BQAwaTELMAkGA1UEBhMCTkwxEDAOBgNVBAgMB0V4YW1wbGUxEDAOBgNVBAcMB0V4 +YW1wbGUxEDAOBgNVBAoMB0V4YW1wbGUxEDAOBgNVBAsMB2V4YW1wbGUxEjAQBgNV +BAMMCWxvY2FsaG9zdDAeFw0yNDA3MTcxMzA3MDVaFw0yNTA3MTcxMzA3MDVaMGkx +CzAJBgNVBAYTAk5MMRAwDgYDVQQIDAdFeGFtcGxlMRAwDgYDVQQHDAdFeGFtcGxl +MRAwDgYDVQQKDAdFeGFtcGxlMRAwDgYDVQQLDAdleGFtcGxlMRIwEAYDVQQDDAls +b2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCyU87LWHj+ +r1DjdZQtA0EWy5TmBjf8Rr5tv4eiSolFYAD1kfyelutxj6mMzgk1cvePpC8npPzZ +-----END CERTIFICATE----- diff --git a/std/fixtures/tls/test.cnf b/std/fixtures/tls/test.cnf new file mode 100644 index 000000000..554df61c3 --- /dev/null +++ b/std/fixtures/tls/test.cnf @@ -0,0 +1,17 @@ +[req] +distinguished_name = req_distinguished_name +x509_extensions = v3_req +prompt = no +[req_distinguished_name] +C = NL +ST = Example +L = Example +O = Example +OU = example +CN = localhost +[v3_req] +keyUsage = critical, digitalSignature, keyAgreement +extendedKeyUsage = serverAuth +subjectAltName = @alt_names +[alt_names] +DNS.1 = localhost diff --git a/std/fixtures/tls/test.key b/std/fixtures/tls/test.key new file mode 100644 index 000000000..4dfecb289 --- /dev/null +++ b/std/fixtures/tls/test.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCyU87LWHj+r1Dj +dZQtA0EWy5TmBjf8Rr5tv4eiSolFYAD1kfyelutxj6mMzgk1cvePpC8npPzZRhwO +/8vGZshRw2D0Vdgq8XN8B6G35S4PhhdgHsvGMSX6s5EBk4Xj730YKR9NaSpyHSIa +LEYcmM3Ryv+iKEwnEPw+/oFHeW56J197dWiLIHEDM8oC82lX5MT/cYm2QG4CsC/7 +TCDDT1sEVhkdPB60Scks/Ln5ACsWYInH67AvXWDOaBSebBRR+wFGckgu2kldahIc +e0ejawKa5xgvOX00JmQyigDwqm3NMxjdGOg7y27aNWw8Vbo/oRDif775DvtNIn9C +1nJB8wNjAgMBAAECggEAFuAJo2u7yE6HQvOmvdLF4IgzQgHjNaEs4AQqgHGCRAbJ +fgwrinu+mQh+OI8yKYvlYM+FXaxcOPzgMDZflpmXBxICgmVD/6zjDQfSQWDh3zZA +EmGbmdayfK3YeIogKeSN40cHJRV2pJZtyktf9Ql5ls4CVnPyjNewxoiRidsfBlvc +IoCRjiTD6+MHOQjp4AzwVvbXH1Sr7OsngA4glQJjFlXllyVYQNXBr1sWTVl3TS2L +OQfqYzHWlRtty88z7ExK3D03Jz0PD7qWtTwgJkq1ON+PjgCf7rMvZbVmCoUgX759 +LLxY1NE6ogdoRlPyZG3fvSfmxo2PWOqawboZMvirAQKBgQDVwkYwlbY00XuHA4UF +bGwTyk5Yp0/DVSR4jZyAJN6J5xln0JUWYKeps8kxLfDlPgmn6qIaine9Ewf1D9qt +DGr19mzEHzP07OBRo0l34XE5WUMvV4ter3swwmI5w/ysgds7Mz7xA9a69ukF/7SW +C+2RiVW7hJs1pydQw7+NY9YIYwKBgQDVkRv4tvmITtfPpVjAkw0gPIQ2WLw4uIvk +PIX/A58dg952ga+C4MZ7OFtcKI7CF7anr0gCNdGQS6I3SA18YS49U/zycuzPh9v7 +lcIMV+R0Wvo2B6QIGJpt7FzfZBXGdv/ft5l+MII3jpoGqGu1K3Ifj/zUrDlUJDQq +ivrkH+CJAQKBgQDFcbCRugfWW9TlDhw1uUNPOGQLwWeMvr10WSHAv82KxZsS6Hh9 +dgQIXZeuRIgpx5b1smXPbC1TyRtlgiJ0C29VCCzJLyU3zAEbh18aS3PhDBFhzlRe +vmpkzHgccWqYEU5mLVyrFOeoRN9S+jFdE2F6N8en8MHI2kAXeugZeqk9jwKBgCmV +pMWsEzCIcZs8DekJeR/SyMewRY4h2RNq+YhrUxszJykaHWu1itBJa/io6QtABM/n +4HSVuCWJpJ9xBzc10QQeC33GBPhv8tStF2jB4HkLkfbdTAJLkB5hTMAuw9KuLyqH +nHqmxWQ9/x3Ww4o2WHVu2wMqOct5dTLnduzejCEBAoGATsOUSv3+Gm5TdhyB64Y9 +eCk+GwSiZuZUsWKLs68wmF4fmKM53rgJK2qZzW8gEpl6hhehhr/XJbJc7jY1Hmmk +567RIPm2hyj7npLo5sCL2moo21j2XJfqvVikHaXPg782e3nqqdnNZmV7+D32tl6M +AwI2G5eWOxC5PQckr7blIpM= +-----END PRIVATE KEY----- diff --git a/std/fixtures/tls/test.pem b/std/fixtures/tls/test.pem new file mode 100644 index 000000000..b1a3a96fb --- /dev/null +++ b/std/fixtures/tls/test.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDvDCCAqSgAwIBAgIUBwbzzsn/P9HmSv1o2tAoIH6ZYE0wDQYJKoZIhvcNAQEL +BQAwaTELMAkGA1UEBhMCTkwxEDAOBgNVBAgMB0V4YW1wbGUxEDAOBgNVBAcMB0V4 +YW1wbGUxEDAOBgNVBAoMB0V4YW1wbGUxEDAOBgNVBAsMB2V4YW1wbGUxEjAQBgNV +BAMMCWxvY2FsaG9zdDAeFw0yNDA3MTcxMzA3MDVaFw0yNTA3MTcxMzA3MDVaMGkx +CzAJBgNVBAYTAk5MMRAwDgYDVQQIDAdFeGFtcGxlMRAwDgYDVQQHDAdFeGFtcGxl +MRAwDgYDVQQKDAdFeGFtcGxlMRAwDgYDVQQLDAdleGFtcGxlMRIwEAYDVQQDDAls +b2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCyU87LWHj+ +r1DjdZQtA0EWy5TmBjf8Rr5tv4eiSolFYAD1kfyelutxj6mMzgk1cvePpC8npPzZ +RhwO/8vGZshRw2D0Vdgq8XN8B6G35S4PhhdgHsvGMSX6s5EBk4Xj730YKR9NaSpy +HSIaLEYcmM3Ryv+iKEwnEPw+/oFHeW56J197dWiLIHEDM8oC82lX5MT/cYm2QG4C +sC/7TCDDT1sEVhkdPB60Scks/Ln5ACsWYInH67AvXWDOaBSebBRR+wFGckgu2kld +ahIce0ejawKa5xgvOX00JmQyigDwqm3NMxjdGOg7y27aNWw8Vbo/oRDif775DvtN +In9C1nJB8wNjAgMBAAGjXDBaMA4GA1UdDwEB/wQEAwIDiDATBgNVHSUEDDAKBggr +BgEFBQcDATAUBgNVHREEDTALgglsb2NhbGhvc3QwHQYDVR0OBBYEFAMp7neOF3LS +LEM8S3tQfXZeaxbvMA0GCSqGSIb3DQEBCwUAA4IBAQAAlLAzA+qDahYG8TvC1GYY +GO61UWwom/PTNHkr7M0ByNjz5XlPEq5zIWJZpcvHbr29ayh70xiZ2lr0a3xQstQF +8lNo0QgV0rQDqWkTXujMW5qos0NkLHhz/wNa5CLeGLHOxez4Yb3lmHg5n071bqQU +F/AhrLg17klvLB+I9QRpJ5RuGMeml+pJtrdQOXKjttZ+eX6vfI8iHG+5dVn50wYB +dSDVknR4MhaqJfLeXAz1JS1a6OJbSD4J8JglGiXvEMQdeXsmkYMRwIZOoiGFiJpu +OzLLhs1TsfEwTGLiapfCFXO610FVPsVynY4Ylr6LRodiFCzIlx2k9O3p6GRAkKy7 +-----END CERTIFICATE----- diff --git a/std/src/std/env.inko b/std/src/std/env.inko index a6b85c756..e8d486783 100644 --- a/std/src/std/env.inko +++ b/std/src/std/env.inko @@ -37,8 +37,6 @@ fn extern inko_env_get(state: Pointer[UInt8], name: String) -> AnyResult fn extern inko_env_get_working_directory(state: Pointer[UInt8]) -> AnyResult -fn extern inko_env_home_directory(state: Pointer[UInt8]) -> AnyResult - fn extern inko_env_set_working_directory(path: String) -> AnyResult fn extern inko_env_temp_directory(state: Pointer[UInt8]) -> String @@ -119,10 +117,17 @@ fn pub variables -> Map[String, String] { # env.home_directory # => Option.Some('/home/alice') # ``` fn pub home_directory -> Option[Path] { - match inko_env_home_directory(_INKO.state) { - case { @tag = 0, @value = val } -> Option.Some(Path.new(val as String)) - case _ -> Option.None + # Rather than performing all sorts of magical incantations to get the home + # directory, we're just going to require that HOME is set. + # + # If the home is explicitly set to an empty string we still ignore it, because + # there's no scenario in which Some("") is useful. + let val = match inko_env_get(_INKO.state, 'HOME') { + case { @tag = 0, @value = val } -> val as String + case _ -> return Option.None } + + if val.size > 0 { Option.Some(Path.new(val)) } else { Option.None } } # Returns the path to the temporary directory. diff --git a/std/src/std/fs/path.inko b/std/src/std/fs/path.inko index 2ba9f9916..da38eca26 100644 --- a/std/src/std/fs/path.inko +++ b/std/src/std/fs/path.inko @@ -1,6 +1,7 @@ # Cross-platform path manipulation. import std.clone (Clone) import std.cmp (Equal) +import std.env (home_directory) import std.fmt (Format, Formatter) import std.fs (DirectoryEntry) import std.hash (Hash, Hasher) @@ -85,6 +86,13 @@ let pub SEPARATOR = '/' # The byte used to represent the path separator. let SEPARATOR_BYTE = 47 +# The character used to signal the user's home directory. +let HOME = '~' + +# The prefix of a path that indicates a path relative to the user's home +# directory. +let HOME_WITH_SEPARATOR = HOME + SEPARATOR + # Returns the number of bytes leading up to the last path separator. # # If no separator could be found, `-1` is returned. @@ -623,11 +631,20 @@ class pub Path { # Returns the canonical, absolute version of `self`. # + # # Resolving home directories + # + # If `self` is equal to `~`, this method returns the path to the user's home + # directory. If `self` starts with `~/`, this prefix is replaced with the path + # to the user's home directory (e.g. `~/foo` becomes `/var/home/alice/foo`). + # # # Errors # - # This method may return an `Error` for cases such as when `self` doesn't - # exist, or when a component that isn't the last component is _not_ a - # directory. + # This method may return an `Error` for cases such as: + # + # - `self` doesn't exist + # - a component that isn't the last component is _not_ a directory + # - `self` is equal to `~` or starts with `~/`, but the home directory can't + # be found (e.g. it doesn't exist) # # # Examples # @@ -635,9 +652,30 @@ class pub Path { # import std.fs.path (Path) # # Path.new('/foo/../bar').expand.get # => Path.new('/bar') + # Path.new('~').expand.get # => '/var/home/...' + # Path.new('~/').expand.get # => '/var/home/...' # ``` fn pub expand -> Result[Path, Error] { - match inko_path_expand(_INKO.state, @path) { + if @path == HOME { + return match home_directory { + case Some(v) -> Result.Ok(v) + case _ -> Result.Error(Error.NotFound) + } + } + + let mut target = @path + + match @path.strip_prefix(HOME_WITH_SEPARATOR) { + case Some(tail) -> { + target = match home_directory { + case Some(v) -> join_strings(v.path, tail) + case _ -> throw Error.NotFound + } + } + case _ -> {} + } + + match inko_path_expand(_INKO.state, target) { case { @tag = 0, @value = v } -> Result.Ok(Path.new(v as String)) case { @tag = _, @value = e } -> { Result.Error(Error.from_os_error(e as Int)) diff --git a/std/src/std/io.inko b/std/src/std/io.inko index 0a074b4a1..05b4674e2 100644 --- a/std/src/std/io.inko +++ b/std/src/std/io.inko @@ -19,6 +19,15 @@ let MAX_READ_ALL_SIZE = 1024 * 1024 # The default size of the buffer maintained by `BufferedReader`. let DEFAULT_BUFFER_SIZE = 8 * 1024 +# The error code used to signal invalid data. +# +# This error code isn't produced by libc, instead it's specific to the runtime +# library. +let INVALID_DATA = -2 + +# The error code used when encountering an unexpected end of the input. +let UNEXPECTED_EOF = -3 + fn extern inko_last_error -> Int32 # An error type for I/O operations. @@ -121,6 +130,18 @@ class pub enum Error { # A memory address used (e.g. as an argument) is in an invalid range. case BadAddress + # The data provided for the operation is invalid, such as when using an + # invalid TLS certificate or when a TLS socket encountered invalid TLS data + # (e.g. an invalid handshake message). + case InvalidData + + # The operation encountered the end of the input stream, but more input is + # required. + # + # An example of where this error is encountered is when reading from a TLS + # socket that was closed without sending the `close_notify` message. + case EndOfInput + # An error not covered by the other variants. # # The wrapped `Int` is the raw error code. @@ -170,6 +191,8 @@ class pub enum Error { case errors.EHOSTUNREACH -> Error.HostUnreachable case errors.EINPROGRESS -> Error.InProgress case errors.EFAULT -> Error.BadAddress + case INVALID_DATA -> Error.InvalidData + case UNEXPECTED_EOF -> Error.EndOfInput case val -> Error.Other(val) } } @@ -214,6 +237,10 @@ impl ToString for Error { case TimedOut -> 'the operation timed out' case WouldBlock -> 'the operation would block' case BadAddress -> 'a memory address is in an invalid range' + case InvalidData -> "the data provided isn't valid for the operation" + case EndOfInput -> { + 'the end of the input stream is reached, but more input is required' + } case Other(code) -> 'an other error with code ${code} occurred' } } @@ -253,6 +280,8 @@ impl Format for Error { case TimedOut -> 'TimedOut' case WouldBlock -> 'WouldBlock' case BadAddress -> 'BadAddress' + case InvalidData -> 'InvalidData' + case EndOfInput -> 'EndOfInput' case Other(code) -> { formatter.tuple('Other').field(code).finish return @@ -297,6 +326,8 @@ impl Equal[ref Error] for Error { case (TimedOut, TimedOut) -> true case (WouldBlock, WouldBlock) -> true case (Other(a), Other(b)) -> a == b + case (InvalidData, InvalidData) -> true + case (EndOfInput, EndOfInput) -> true case _ -> false } } diff --git a/std/src/std/net/socket.inko b/std/src/std/net/socket.inko index 42171d2c5..ee4af50ce 100644 --- a/std/src/std/net/socket.inko +++ b/std/src/std/net/socket.inko @@ -71,31 +71,22 @@ import std.net.ip (IpAddress) import std.string (ToString) import std.time (Duration, ToInstant) -class extern Linger { - let @l_onoff: Int32 - let @l_linger: Int32 -} - -class extern RawSocket { - let @inner: Int32 - let @registered: UInt8 - let @unix: UInt8 -} - -class extern RawAddress { - let @address: String - let @port: Int -} - -class extern AnyResult { - let @tag: Int - let @value: UInt64 -} +# The maximum value valid for a listen() call. +# +# Linux and FreeBSD do not allow for values greater than this as they internally +# use an u16, so we'll limit the backlog to this value. We don't use SOMAXCONN +# because it might be hardcoded. This means that setting `net.core.somaxconn` on +# Linux (for example) would have no effect. +let MAXIMUM_LISTEN_BACKLOG = 65_535 -class extern IntResult { - let @tag: Int - let @value: Int -} +# A value that signals the lack of a socket deadline. +let NO_DEADLINE = -1 +let IPV4 = 0 +let IPV6 = 1 +let UNIX = 2 +let STREAM = 0 +let DGRAM = 1 +let RAW = 2 fn extern setsockopt( socket: Int32, @@ -113,9 +104,18 @@ fn extern getsockopt( size: Pointer[Int32], ) -> Int32 +fn extern inko_socket_poll( + state: Pointer[UInt8], + process: Pointer[UInt8], + socket: Pointer[RawSocket], + write: Bool, + deadline: Int, +) -> IntResult + fn extern inko_socket_new( - proto: Int, + domain: Int, kind: Int, + protocol: Int, out: Pointer[RawSocket], ) -> Int64 @@ -222,130 +222,157 @@ fn extern inko_socket_shutdown_read_write( socket: Pointer[RawSocket], ) -> IntResult -# The maximum value valid for a listen() call. -# -# Linux and FreeBSD do not allow for values greater than this as they internally -# use an u16, so we'll limit the backlog to this value. We don't use SOMAXCONN -# because it might be hardcoded. This means that setting `net.core.somaxconn` on -# Linux (for example) would have no effect. -let MAXIMUM_LISTEN_BACKLOG = 65_535 +fn poll( + socket: Pointer[RawSocket], + deadline: Int, + write: Bool, +) -> Result[Nil, Error] { + match inko_socket_poll(_INKO.state, _INKO.process, socket, write, deadline) { + case { @tag = 1 } -> Result.Ok(nil) + case { @value = v } -> Result.Error(Error.from_os_error(v)) + } +} -# A value that signals the lack of a socket deadline. -let NO_DEADLINE = -1 -let IPV4 = 0 -let IPV6 = 1 -let UNIX = 2 +class extern Linger { + let @l_onoff: Int32 + let @l_linger: Int32 +} -# The type of a socket. -class pub enum Type { - # The type corresponding to `SOCK_STREAM`. - case STREAM +class extern RawSocket { + let @inner: Int32 + let @registered: UInt8 + let @unix: UInt8 +} - # The type corresponding to `SOCK_DGRAM`. - case DGRAM +class extern RawAddress { + let @address: String + let @port: Int +} - # The type corresponding to `SOCK_SEQPACKET`. - case SEQPACKET +class extern IntResult { + let @tag: Int + let @value: Int +} - # The type corresponding to `SOCK_RAW`. - case RAW +trait RawSocketOperations { + fn mut raw_socket -> Pointer[RawSocket] - # Converts a `Type` into the underlying `SOCK_*` integer. - fn pub move into_int -> Int { - match self { - case STREAM -> 0 - case DGRAM -> 1 - case SEQPACKET -> 2 - case RAW -> 3 - } - } + fn raw_deadline -> Int } # An IPv4 or IPv6 socket address. class pub SocketAddress { # The IPv4/IPv6 address of this socket address. - # - # This is stored as a `String` so we don't need to parse the address every - # time a `SocketAddress` is created. - let pub @address: String + let pub @ip: IpAddress # The port number of this socket address. let pub @port: Int - fn pub static new(address: String, port: Int) -> SocketAddress { - SocketAddress(address: address, port: port) - } + fn static from_raw(raw: ref RawAddress) -> SocketAddress { + # The address passed to this method is one filled in by the system. Assuming + # our IP address parsing logic is complete (which it should be), + # encountering an address we can't parse is a bug and should terminate the + # program. + let ip = match IpAddress.parse(raw.address) { + case Some(v) -> v + case _ -> panic("IpAddress.parse doesn't support '${raw.address}'") + } - # Returns the IPv4/IPv6 address associated with `self`. - fn pub ip -> Option[IpAddress] { - IpAddress.parse(@address) + SocketAddress(ip, raw.port) } } impl Equal[ref SocketAddress] for SocketAddress { # Returns `true` if `self` and `other` are the same. fn pub ==(other: ref SocketAddress) -> Bool { - @address == other.address and @port == other.port + @ip == other.ip and @port == other.port } } impl Format for SocketAddress { fn pub fmt(formatter: mut Formatter) { - formatter.write('${@address}:${@port}') + formatter.write('${@ip}:${@port}') } } # A low-level, non-blocking IPv4 or IPv6 socket. class pub Socket { - let @raw: RawSocket + let @socket: RawSocket # A point in time after which socket operations time out. # # We use an `Int` to remove the need for using `Option[Instant]`. let @deadline: Int - # Creates a new IPv4 socket. - # - # # Examples - # - # ```inko - # import std.net.socket (Type, Socket) - # - # Socket.ipv4(Type.DGRAM).get - # ``` - fn pub static ipv4(type: Type) -> Result[Socket, Error] { + fn static new( + domain: Int, + type: Int, + protocol: Int, + ) -> Result[Socket, Error] { let sock = RawSocket( inner: 0 as Int32, registered: 0 as UInt8, unix: 0 as UInt8, ) - match inko_socket_new(IPV4, type.into_int, mut sock) as Int { - case 0 -> Result.Ok(Socket(raw: sock, deadline: NO_DEADLINE)) + match inko_socket_new(domain, type, protocol, mut sock) as Int { + case 0 -> Result.Ok(Socket(socket: sock, deadline: NO_DEADLINE)) case e -> Result.Error(Error.from_os_error(e)) } } - # Creates a new IPv6 socket. + # Returns a new `Socket` configured as a stream socket. + # + # The `ipv6` argument specifies if the socket is an IPv4 socket (`false`) or + # an IPv6 socket (`true`). # # # Examples # # ```inko - # import std.net.socket (Type, Socket) + # import std.net.socket (Socket) # - # Socket.ipv6(Type.DGRAM).get + # Socket.stream(ipv6: false) # ``` - fn pub static ipv6(type: Type) -> Result[Socket, Error] { - let sock = RawSocket( - inner: 0 as Int32, - registered: 0 as UInt8, - unix: 0 as UInt8, - ) + fn pub static stream(ipv6: Bool) -> Result[Socket, Error] { + Socket.new(ipv6.to_int, STREAM, protocol: 0) + } - match inko_socket_new(IPV6, type.into_int, mut sock) as Int { - case 0 -> Result.Ok(Socket(raw: sock, deadline: NO_DEADLINE)) - case e -> Result.Error(Error.from_os_error(e)) - } + # Returns a new `Socket` configured as a datagram socket. + # + # The `ipv6` argument specifies if the socket is an IPv4 socket (`false`) or + # an IPv6 socket (`true`). + # + # # Examples + # + # ```inko + # import std.net.socket (Socket) + # + # Socket.datagram(ipv6: false) + # ``` + fn pub static datagram(ipv6: Bool) -> Result[Socket, Error] { + Socket.new(ipv6.to_int, DGRAM, protocol: 0) + } + + # Returns a new `Socket` configured as a raw socket. + # + # The `ipv6` argument specifies if the socket is an IPv4 socket (`false`) or + # an IPv6 socket (`true`). + # + # The `protocol` argument must specify a valid IANA IP protocol as defined in + # RFC 1700. + # + # Note that on certain platforms (e.g. Linux, and probably most other Unix + # systems) you'll need root privileges in order to create a raw socket. + # + # # Examples + # + # ```inko + # import std.net.socket (Socket) + # + # Socket.raw(ipv6: false, protocol: 1) + # ``` + fn pub static raw(ipv6: Bool, protocol: Int) -> Result[Socket, Error] { + Socket.new(ipv6.to_int, RAW, protocol) } # Sets the point in time after which socket operations must time out, known as @@ -357,10 +384,10 @@ class pub Socket { # after which operations time out: # # ```inko - # import std.net.socket (Socket, Type) + # import std.net.socket (Socket) # import std.time (Duration) # - # let socket = Socket.ipv4(Type.DGRAM) + # let socket = Socket.datagram(ipv6: false) # # socket.timeout_after = Duration.from_secs(5) # ``` @@ -368,10 +395,10 @@ class pub Socket { # We can also use an `Instant`: # # ```inko - # import std.net.socket (Socket, Type) + # import std.net.socket (Socket) # import std.time (Duration, Instant) # - # let socket = Socket.ipv4(Type.DGRAM) + # let socket = Socket.datagram(ipv6: false) # # socket.timeout_after = Instant.new + Duration.from_secs(5) # ``` @@ -391,15 +418,15 @@ class pub Socket { # Binding a socket: # # ```inko - # import std.net.socket (Socket, Type) + # import std.net.socket (Socket) # import std.net.ip (IpAddress) # - # let socket = Socket.ipv4(Type.DGRAM).get + # let socket = Socket.datagram(ipv6: false).get # # socket.bind(ip: IpAddress.v4(0, 0, 0, 0), port: 9999).get # ``` fn pub mut bind(ip: ref IpAddress, port: Int) -> Result[Nil, Error] { - match inko_socket_bind(@raw, ip.to_string, port) { + match inko_socket_bind(@socket, ip.to_string, port) { case { @tag = 1, @value = _ } -> Result.Ok(nil) case { @tag = _, @value = e } -> Result.Error(Error.from_os_error(e)) } @@ -412,11 +439,11 @@ class pub Socket { # Connecting a socket: # # ```inko - # import std.net.socket (Socket, Type) + # import std.net.socket (Socket) # import std.net.ip (IpAddress) # - # let listener = Socket.ipv4(Type.STREAM).get - # let client = Socket.ipv4(Type.STREAM).get + # let listener = Socket.stream(ipv6: false).get + # let client = Socket.stream(ipv6: false).get # # socket.bind(ip: IpAddress.v4(0, 0, 0, 0), port: 9999).get # socket.listen.get @@ -427,7 +454,7 @@ class pub Socket { inko_socket_connect( _INKO.state, _INKO.process, - @raw, + @socket, ip.to_string, port, @deadline, @@ -446,16 +473,16 @@ class pub Socket { # Marking a socket as a listener: # # ```inko - # import std.net.socket (Socket, Type) + # import std.net.socket (Socket) # import std.net.ip (IpAddress) # - # let socket = Socket.ipv4(Type.STREAM).get + # let socket = Socket.stream(ipv6: false).get # # socket.bind(ip: IpAddress.v4(0, 0, 0, 0), port: 9999).get # socket.listen.get # ``` fn pub mut listen -> Result[Nil, Error] { - match inko_socket_listen(@raw, MAXIMUM_LISTEN_BACKLOG) { + match inko_socket_listen(@socket, MAXIMUM_LISTEN_BACKLOG) { case { @tag = 1, @value = _ } -> Result.Ok(nil) case { @tag = _, @value = e } -> Result.Error(Error.from_os_error(e)) } @@ -470,11 +497,11 @@ class pub Socket { # Accepting a connection and reading data from the connection: # # ```inko - # import std.net.socket (Socket, Type) + # import std.net.socket (Socket) # import std.net.ip (IpAddress) # - # let listener = Socket.ipv4(Type.STREAM).get - # let stream = Socket.ipv4(Type.STREAM).get + # let listener = Socket.stream(ipv6: false).get + # let stream = Socket.stream(ipv6: false).get # # listener.bind(ip: IpAddress.v4(0, 0, 0, 0), port: 9999).get # listener.listen.get @@ -497,10 +524,16 @@ class pub Socket { ) match - inko_socket_accept(_INKO.state, _INKO.process, @raw, @deadline, mut sock) + inko_socket_accept( + _INKO.state, + _INKO.process, + @socket, + @deadline, + mut sock, + ) as Int { - case 0 -> Result.Ok(Socket(raw: sock, deadline: NO_DEADLINE)) + case 0 -> Result.Ok(Socket(socket: sock, deadline: NO_DEADLINE)) case e -> Result.Error(Error.from_os_error(e)) } } @@ -512,10 +545,10 @@ class pub Socket { # # Examples # # ```inko - # import std.net.socket (Socket, Type) + # import std.net.socket (Socket) # import std.net.ip (IpAddress) # - # let socket = Socket.ipv4(Type.DGRAM).get + # let socket = Socket.datagram(ipv6: false).get # # socket.bind(ip: IpAddress.v4(0, 0, 0, 0), port: 9999).get # socket @@ -535,7 +568,7 @@ class pub Socket { inko_socket_send_string_to( _INKO.state, _INKO.process, - @raw, + @socket, string, ip.to_string, port, @@ -554,10 +587,10 @@ class pub Socket { # # Examples # # ```inko - # import std.net.socket (Socket, Type) + # import std.net.socket (Socket) # import std.net.ip (IpAddress) # - # let socket = Socket.ipv4(Type.DGRAM).get + # let socket = Socket.datagram(ipv6: false).get # let bytes = 'hello'.to_byte_array # # socket.bind(ip: IpAddress.v4(0, 0, 0, 0), port: 9999).get @@ -578,7 +611,7 @@ class pub Socket { inko_socket_send_bytes_to( _INKO.state, _INKO.process, - @raw, + @socket, bytes, ip.to_string, port, @@ -601,10 +634,10 @@ class pub Socket { # Sending a message to ourselves and receiving it: # # ```inko - # import std.net.socket (Socket, Type) + # import std.net.socket (Socket) # import std.net.ip (IpAddress) # - # let socket = Socket.ipv4(Type.DGRAM).get + # let socket = Socket.datagram(ipv6: false).get # let bytes = ByteArray.new # # socket @@ -631,7 +664,7 @@ class pub Socket { inko_socket_receive_from( _INKO.state, _INKO.process, - @raw, + @socket, bytes, size, @deadline, @@ -639,7 +672,7 @@ class pub Socket { ) as Int { - case 0 -> Result.Ok(SocketAddress.new(raw.address, raw.port)) + case 0 -> Result.Ok(SocketAddress.from_raw(raw)) case e -> Result.Error(Error.from_os_error(e)) } } @@ -648,8 +681,8 @@ class pub Socket { fn pub local_address -> Result[SocketAddress, Error] { let raw = RawAddress(address: '', port: 0) - match inko_socket_local_address(_INKO.state, @raw, mut raw) as Int { - case 0 -> Result.Ok(SocketAddress.new(raw.address, raw.port)) + match inko_socket_local_address(_INKO.state, @socket, mut raw) as Int { + case 0 -> Result.Ok(SocketAddress.from_raw(raw)) case e -> Result.Error(Error.from_os_error(e)) } } @@ -658,8 +691,8 @@ class pub Socket { fn pub peer_address -> Result[SocketAddress, Error] { let raw = RawAddress(address: '', port: 0) - match inko_socket_peer_address(_INKO.state, @raw, mut raw) as Int { - case 0 -> Result.Ok(SocketAddress.new(raw.address, raw.port)) + match inko_socket_peer_address(_INKO.state, @socket, mut raw) as Int { + case 0 -> Result.Ok(SocketAddress.from_raw(raw)) case e -> Result.Error(Error.from_os_error(e)) } } @@ -710,7 +743,7 @@ class pub Socket { } let res = setsockopt( - @raw.inner, + @socket.inner, const.SOL_SOCKET as Int32, const.SO_LINGER as Int32, (mut linger) as Pointer[UInt8], @@ -726,7 +759,7 @@ class pub Socket { let linger = Linger(l_onoff: 0 as Int32, l_linger: 0 as Int32) let size = 8 as Int32 let res = getsockopt( - @raw.inner, + @socket.inner, const.SOL_SOCKET as Int32, const.SO_LINGER as Int32, (mut linger) as Pointer[UInt8], @@ -775,7 +808,7 @@ class pub Socket { # Shuts down the reading half of this socket. fn pub mut shutdown_read -> Result[Nil, Error] { - match inko_socket_shutdown_read(@raw) { + match inko_socket_shutdown_read(@socket) { case { @tag = 1, @value = _ } -> Result.Ok(nil) case { @tag = _, @value = e } -> Result.Error(Error.from_os_error(e)) } @@ -783,7 +816,7 @@ class pub Socket { # Shuts down the writing half of this socket. fn pub mut shutdown_write -> Result[Nil, Error] { - match inko_socket_shutdown_write(@raw) { + match inko_socket_shutdown_write(@socket) { case { @tag = 1, @value = _ } -> Result.Ok(nil) case { @tag = _, @value = e } -> Result.Error(Error.from_os_error(e)) } @@ -791,7 +824,7 @@ class pub Socket { # Shuts down both the reading and writing half of this socket. fn pub mut shutdown -> Result[Nil, Error] { - match inko_socket_shutdown_read_write(@raw) { + match inko_socket_shutdown_read_write(@socket) { case { @tag = 1, @value = _ } -> Result.Ok(nil) case { @tag = _, @value = e } -> Result.Error(Error.from_os_error(e)) } @@ -808,8 +841,8 @@ class pub Socket { unix: 0 as UInt8, ) - match inko_socket_try_clone(@raw, mut sock) as Int { - case 0 -> Result.Ok(Socket(raw: sock, deadline: NO_DEADLINE)) + match inko_socket_try_clone(@socket, mut sock) as Int { + case 0 -> Result.Ok(Socket(socket: sock, deadline: NO_DEADLINE)) case e -> Result.Error(Error.from_os_error(e)) } } @@ -817,7 +850,7 @@ class pub Socket { fn mut set_option(level: Int, option: Int, value: Int) -> Result[Nil, Error] { let val = value as Int32 let res = setsockopt( - @raw.inner, + @socket.inner, level as Int32, option as Int32, (mut val) as Pointer[UInt8], @@ -832,7 +865,7 @@ class pub Socket { let size = 4 as Int32 let val = 0 as Int32 let res = getsockopt( - @raw.inner, + @socket.inner, level as Int32, option as Int32, (mut val) as Pointer[UInt8], @@ -850,18 +883,68 @@ class pub Socket { panic('getsockopt(2) failed: ${Error.last_os_error}') } } + + # Waits for the socket to be writable. + # + # This method returns once data can be written to the socket without + # triggering the equivalent of EAGAIN. + # + # You don't need to call this method yourself when using sockets, as Inko's + # runtime library takes care of this automatically. This method instead exists + # to make it possible to interact with C libraries that may perform operations + # on non-blocking sockets and require you to manually poll sockets for + # readiness. + # + # # Errors + # + # This method returns a `Error.TimedOut` if waiting for the socket to become + # writable exceeded the configured deadline. + # + # TODO: do we _actually_ want to expose this? + fn pub wait_until_writable -> Result[Nil, Error] { + poll(@socket, @deadline, write: true) + } + + # Waits for the socket to be readable. + # + # Refer to the documentation of `Socket.wait_until_writable` for more details. + # + # # Errors + # + # This method returns a `Error.TimedOut` if waiting for the socket to become + # readable exceeded the configured deadline. + fn pub wait_until_readable -> Result[Nil, Error] { + poll(@socket, @deadline, write: false) + } +} + +impl RawSocketOperations for Socket { + fn mut raw_socket -> Pointer[RawSocket] { + @socket + } + + fn raw_deadline -> Int { + @deadline + } } impl Drop for Socket { fn mut drop { - inko_socket_drop(@raw) + inko_socket_drop(@socket) } } impl Read for Socket { fn pub mut read(into: mut ByteArray, size: Int) -> Result[Int, Error] { match - inko_socket_read(_INKO.state, _INKO.process, @raw, into, size, @deadline) + inko_socket_read( + _INKO.state, + _INKO.process, + @socket, + into, + size, + @deadline, + ) { case { @tag = 0, @value = v } -> Result.Ok(v) case { @tag = _, @value = e } -> Result.Error(Error.from_os_error(e)) @@ -874,7 +957,7 @@ impl WriteInternal for Socket { let state = _INKO.state let proc = _INKO.process - match inko_socket_write(state, proc, @raw, data, size, @deadline) { + match inko_socket_write(state, proc, @socket, data, size, @deadline) { case { @tag = 0, @value = n } -> Result.Ok(n) case { @value = e } -> Result.Error(Error.from_os_error(e)) } @@ -913,16 +996,10 @@ class pub UdpSocket { # import std.net.socket (UdpSocket) # import std.net.ip (IpAddress) # - # let ip = IpAddress.parse('0.0.0.0').get - # - # UdpSocket.new(ip, port: 0).get + # UdpSocket.new(IpAddress.v4(0, 0, 0, 0), port: 0).get # ``` fn pub static new(ip: ref IpAddress, port: Int) -> Result[UdpSocket, Error] { - let socket = if ip.v6? { - try Socket.ipv6(Type.DGRAM) - } else { - try Socket.ipv4(Type.DGRAM) - } + let socket = try Socket.datagram(ip.v6?) try socket.bind(ip, port) Result.Ok(UdpSocket(socket)) @@ -1039,6 +1116,16 @@ class pub UdpSocket { } } +impl RawSocketOperations for UdpSocket { + fn mut raw_socket -> Pointer[RawSocket] { + @socket.socket + } + + fn raw_deadline -> Int { + @socket.deadline + } +} + impl Read for UdpSocket { fn pub mut read(into: mut ByteArray, size: Int) -> Result[Int, Error] { @socket.read(into, size) @@ -1084,16 +1171,10 @@ class pub TcpClient { # import std.net.socket (TcpClient) # import std.net.ip (IpAddress) # - # let ip = IpAddress.parse('127.0.0.1').get - # - # TcpClient.new(ip, port: 40_000).get + # TcpClient.new(IpAddress.v4(127, 0, 0, 1), port: 40_000).get # ``` fn pub static new(ip: ref IpAddress, port: Int) -> Result[TcpClient, Error] { - let socket = if ip.v6? { - try Socket.ipv6(Type.STREAM) - } else { - try Socket.ipv4(Type.STREAM) - } + let socket = try Socket.stream(ip.v6?) try socket.connect(ip, port) from(socket) @@ -1125,13 +1206,9 @@ class pub TcpClient { fn pub static with_timeout[T: ToInstant]( ip: ref IpAddress, port: Int, - timeout_after: T, + timeout_after: ref T, ) -> Result[TcpClient, Error] { - let socket = if ip.v6? { - try Socket.ipv6(Type.STREAM) - } else { - try Socket.ipv4(Type.STREAM) - } + let socket = try Socket.stream(ip.v6?) socket.timeout_after = timeout_after try socket.connect(ip, port) @@ -1177,6 +1254,16 @@ class pub TcpClient { } } +impl RawSocketOperations for TcpClient { + fn mut raw_socket -> Pointer[RawSocket] { + @socket.socket + } + + fn raw_deadline -> Int { + @socket.deadline + } +} + impl Read for TcpClient { fn pub mut read(into: mut ByteArray, size: Int) -> Result[Int, Error] { @socket.read(into, size) @@ -1209,26 +1296,16 @@ class pub TcpServer { # rebinding of sockets. `SO_REUSEPORT` is only used on platforms that support # it. # - # The `only_ipv6` argument is ignored when binding to an IPv4 address. - # # # Examples # - # Creating a `TcpServer`: - # # ```inko - # import std.net.socket (TcpServer) # import std.net.ip (IpAddress) + # import std.net.socket (TcpServer) # - # let ip = IpAddress.parse('0.0.0.0').get - # - # TcpServer.new(ip, port: 40_000).get + # TcpServer.new(IpAddress.v4(0, 0, 0, 0), port: 40_000).get # ``` fn pub static new(ip: ref IpAddress, port: Int) -> Result[TcpServer, Error] { - let socket = if ip.v6? { - try Socket.ipv6(Type.STREAM) - } else { - try Socket.ipv4(Type.STREAM) - } + let socket = try Socket.stream(ip.v6?) try socket.no_delay = true try socket.reuse_address = true @@ -1284,6 +1361,16 @@ class pub TcpServer { } } +impl RawSocketOperations for TcpServer { + fn mut raw_socket -> Pointer[RawSocket] { + @socket.socket + } + + fn raw_deadline -> Int { + @socket.deadline + } +} + # A Unix domain socket address. class pub UnixAddress { # The path or name of the address. @@ -1292,37 +1379,6 @@ class pub UnixAddress { # and unnamed addresses. let pub @address: String - # Creates a new `UnixAddress` from the given path or name. - # - # # Examples - # - # Creating a `UnixAddress` that uses a path: - # - # ```inko - # import std.net.socket (UnixAddress) - # - # UnixAddress.new('/tmp/test.sock'.to_path) - # ``` - # - # Creating a `UnixAddress` that uses an unnamed address: - # - # ```inko - # import std.net.socket (UnixAddress) - # - # UnixAddress.new(''.to_path) - # ``` - # - # Creating a `UnixAddress` that uses an abstract address: - # - # ```inko - # import std.net.socket (UnixAddress) - # - # UnixAddress.new("\0example".to_path) - # ``` - fn pub static new(address: ref Path) -> UnixAddress { - UnixAddress(address.to_string) - } - # Returns the path of this address. # # If the address is unnamed or an abstract address, None is returned. @@ -1419,37 +1475,52 @@ impl ToString for UnixAddress { # A low-level, non-blocking Unix domain socket. class pub UnixSocket { - let @raw: RawSocket + let @socket: RawSocket # A point in time after which socket operations time out. # # We use an `Int` to remove the need for using `Option[Instant]`. let @deadline: Int - # Creates a new Unix domain socket. - # - # # Examples - # - # Creating a new socket: - # - # ```inko - # import std.net.socket (Type, UnixSocket) - # - # UnixSocket.new(Type.DGRAM).get - # ``` - fn pub static new(type: Type) -> Result[UnixSocket, Error] { + fn static new(type: Int) -> Result[UnixSocket, Error] { let sock = RawSocket( inner: 0 as Int32, registered: 0 as UInt8, unix: 1 as UInt8, ) - match inko_socket_new(UNIX, type.into_int, mut sock) as Int { - case 0 -> Result.Ok(UnixSocket(raw: sock, deadline: NO_DEADLINE)) + match inko_socket_new(UNIX, type, 0, mut sock) as Int { + case 0 -> Result.Ok(UnixSocket(socket: sock, deadline: NO_DEADLINE)) case e -> Result.Error(Error.from_os_error(e)) } } + # Returns a new `UnixSocket` configured as a stream socket. + # + # # Examples + # + # ```inko + # import std.net.socket (UnixSocket) + # + # UnixSocket.stream + # ``` + fn pub static stream -> Result[UnixSocket, Error] { + UnixSocket.new(STREAM) + } + + # Returns a new `UnixSocket` configured as a datagram socket. + # + # # Examples + # + # ```inko + # import std.net.socket (UnixSocket) + # + # UnixSocket.datagram + # ``` + fn pub static datagram -> Result[UnixSocket, Error] { + UnixSocket.new(DGRAM) + } + # Sets the point in time after which socket operations must time out, known as # a "deadline". # @@ -1459,10 +1530,10 @@ class pub UnixSocket { # after which operations time out: # # ```inko - # import std.net.socket (UnixSocket, Type) + # import std.net.socket (UnixSocket) # import std.time (Duration) # - # let socket = UnixSocket.new(Type.DGRAM) + # let socket = UnixSocket.datagram # # socket.timeout_after = Duration.from_secs(5) # ``` @@ -1470,10 +1541,10 @@ class pub UnixSocket { # We can also use an `Instant`: # # ```inko - # import std.net.socket (UnixSocket, Type) + # import std.net.socket (UnixSocket) # import std.time (Duration, Instant) # - # let socket = UnixSocket.new(Type.DGRAM) + # let socket = UnixSocket.datagram # # socket.timeout_after = Instant.new + Duration.from_secs(5) # ``` @@ -1493,14 +1564,14 @@ class pub UnixSocket { # Binding a Unix socket to a path: # # ```inko - # import std.net.socket (Type, UnixSocket) + # import std.net.socket (UnixSocket) # - # let socket = UnixSocket.new(Type.DGRAM).get + # let socket = UnixSocket.datagram.get # # socket.bind('/tmp/test.sock'.to_path).get # ``` fn pub mut bind(path: ref Path) -> Result[Nil, Error] { - match inko_socket_bind(@raw, path.to_string, 0) { + match inko_socket_bind(@socket, path.to_string, 0) { case { @tag = 1, @value = _ } -> Result.Ok(nil) case { @tag = _, @value = e } -> Result.Error(Error.from_os_error(e)) } @@ -1513,10 +1584,10 @@ class pub UnixSocket { # Connecting a Unix socket: # # ```inko - # import std.net.socket (Type, UnixSocket) + # import std.net.socket (UnixSocket) # - # let listener = UnixSocket.new(Type.STREAM).get - # let stream = UnixSocket.new(Type.STREAM).get + # let listener = UnixSocket.stream.get + # let stream = UnixSocket.stream.get # # listener.bind('/tmp/test.sock'.to_path).get # listener.listen.get @@ -1528,7 +1599,7 @@ class pub UnixSocket { inko_socket_connect( _INKO.state, _INKO.process, - @raw, + @socket, path.to_string, 0, @deadline, @@ -1547,15 +1618,15 @@ class pub UnixSocket { # Marking a socket as a listener: # # ```inko - # import std.net.socket (Type, UnixSocket) + # import std.net.socket (UnixSocket) # - # let socket = UnixSocket.new(Type.STREAM).get + # let socket = UnixSocket.stream.get # # socket.bind('/tmp/test.sock'.to_path).get # socket.listen.get # ``` fn pub mut listen -> Result[Nil, Error] { - match inko_socket_listen(@raw, MAXIMUM_LISTEN_BACKLOG) { + match inko_socket_listen(@socket, MAXIMUM_LISTEN_BACKLOG) { case { @tag = 1, @value = _ } -> Result.Ok(nil) case { @tag = _, @value = e } -> Result.Error(Error.from_os_error(e)) } @@ -1570,10 +1641,10 @@ class pub UnixSocket { # Accepting a connection and reading data from the connection: # # ```inko - # import std.net.socket (Type, UnixSocket) + # import std.net.socket (UnixSocket) # - # let listener = UnixSocket.new(Type.STREAM).get - # let stream = UnixSocket.new(Type.STREAM).get + # let listener = UnixSocket.stream.get + # let stream = UnixSocket.stream.get # # listener.bind('/tmp/test.sock'.to_path).get # listener.listen.get @@ -1596,10 +1667,16 @@ class pub UnixSocket { ) match - inko_socket_accept(_INKO.state, _INKO.process, @raw, @deadline, mut sock) + inko_socket_accept( + _INKO.state, + _INKO.process, + @socket, + @deadline, + mut sock, + ) as Int { - case 0 -> Result.Ok(UnixSocket(raw: sock, deadline: NO_DEADLINE)) + case 0 -> Result.Ok(UnixSocket(socket: sock, deadline: NO_DEADLINE)) case e -> Result.Error(Error.from_os_error(e)) } } @@ -1611,9 +1688,9 @@ class pub UnixSocket { # # Examples # # ```inko - # import std.net.socket (Type, UnixSocket) + # import std.net.socket (UnixSocket) # - # let socket = UnixSocket.new(Type.DGRAM).get + # let socket = UnixSocket.datagram.get # # socket.bind('/tmp/test.sock'.to_path).get # socket @@ -1630,7 +1707,7 @@ class pub UnixSocket { inko_socket_send_string_to( _INKO.state, _INKO.process, - @raw, + @socket, string, addr, 0, @@ -1649,9 +1726,9 @@ class pub UnixSocket { # # Examples # # ```inko - # import std.net.socket (Type, UnixSocket) + # import std.net.socket (UnixSocket) # - # let socket = UnixSocket.new(Type.DGRAM).get + # let socket = UnixSocket.datagram.get # let bytes = 'hello'.to_byte_array # # socket.bind('/tmp/test.sock'.to_path).get @@ -1669,7 +1746,7 @@ class pub UnixSocket { inko_socket_send_bytes_to( _INKO.state, _INKO.process, - @raw, + @socket, bytes, addr, 0, @@ -1692,9 +1769,9 @@ class pub UnixSocket { # Sending a message to ourselves and receiving it: # # ```inko - # import std.net.socket (Type, UnixSocket) + # import std.net.socket (UnixSocket) # - # let socket = UnixSocket.new(Type.DGRAM).get + # let socket = UnixSocket.datagram.get # let bytes = ByteArray.new # # socket.send_string_to('hello', address: '/tmp/test.sock'.to_path).get @@ -1714,7 +1791,7 @@ class pub UnixSocket { inko_socket_receive_from( _INKO.state, _INKO.process, - @raw, + @socket, bytes, size, @deadline, @@ -1722,7 +1799,7 @@ class pub UnixSocket { ) as Int { - case 0 -> Result.Ok(UnixAddress.new(raw.address.to_path)) + case 0 -> Result.Ok(UnixAddress(raw.address)) case e -> Result.Error(Error.from_os_error(e)) } } @@ -1731,8 +1808,8 @@ class pub UnixSocket { fn pub local_address -> Result[UnixAddress, Error] { let raw = RawAddress(address: '', port: 0) - match inko_socket_local_address(_INKO.state, @raw, mut raw) as Int { - case 0 -> Result.Ok(UnixAddress.new(raw.address.to_path)) + match inko_socket_local_address(_INKO.state, @socket, mut raw) as Int { + case 0 -> Result.Ok(UnixAddress(raw.address)) case e -> Result.Error(Error.from_os_error(e)) } } @@ -1741,8 +1818,8 @@ class pub UnixSocket { fn pub peer_address -> Result[UnixAddress, Error] { let raw = RawAddress(address: '', port: 0) - match inko_socket_peer_address(_INKO.state, @raw, mut raw) as Int { - case 0 -> Result.Ok(UnixAddress.new(raw.address.to_path)) + match inko_socket_peer_address(_INKO.state, @socket, mut raw) as Int { + case 0 -> Result.Ok(UnixAddress(raw.address)) case e -> Result.Error(Error.from_os_error(e)) } } @@ -1759,7 +1836,7 @@ class pub UnixSocket { # Shuts down the reading half of this socket. fn pub mut shutdown_read -> Result[Nil, Error] { - match inko_socket_shutdown_read(@raw) { + match inko_socket_shutdown_read(@socket) { case { @tag = 1, @value = _ } -> Result.Ok(nil) case { @tag = _, @value = e } -> Result.Error(Error.from_os_error(e)) } @@ -1767,7 +1844,7 @@ class pub UnixSocket { # Shuts down the writing half of this socket. fn pub mut shutdown_write -> Result[Nil, Error] { - match inko_socket_shutdown_write(@raw) { + match inko_socket_shutdown_write(@socket) { case { @tag = 1, @value = _ } -> Result.Ok(nil) case { @tag = _, @value = e } -> Result.Error(Error.from_os_error(e)) } @@ -1775,7 +1852,7 @@ class pub UnixSocket { # Shuts down both the reading and writing half of this socket. fn pub mut shutdown -> Result[Nil, Error] { - match inko_socket_shutdown_read_write(@raw) { + match inko_socket_shutdown_read_write(@socket) { case { @tag = 1, @value = _ } -> Result.Ok(nil) case { @tag = _, @value = e } -> Result.Error(Error.from_os_error(e)) } @@ -1792,16 +1869,40 @@ class pub UnixSocket { unix: 1 as UInt8, ) - match inko_socket_try_clone(@raw, mut sock) as Int { - case 0 -> Result.Ok(UnixSocket(raw: sock, deadline: NO_DEADLINE)) + match inko_socket_try_clone(@socket, mut sock) as Int { + case 0 -> Result.Ok(UnixSocket(socket: sock, deadline: NO_DEADLINE)) case e -> Result.Error(Error.from_os_error(e)) } } + # Waits for the socket to be writable. + # + # Refer to the documentation of `Socket.wait_until_writable` for more details. + # + # # Errors + # + # This method returns a `Error.TimedOut` if waiting for the socket to become + # writable exceeded the configured deadline. + fn pub wait_until_writable -> Result[Nil, Error] { + poll(@socket, @deadline, write: true) + } + + # Waits for the socket to be readable. + # + # Refer to the documentation of `Socket.wait_until_writable` for more details. + # + # # Errors + # + # This method returns a `Error.TimedOut` if waiting for the socket to become + # readable exceeded the configured deadline. + fn pub wait_until_readable -> Result[Nil, Error] { + poll(@socket, @deadline, write: false) + } + fn mut set_option(level: Int, option: Int, value: Int) -> Result[Nil, Error] { let val = value as Int32 let res = setsockopt( - @raw.inner, + @socket.inner, level as Int32, option as Int32, (mut val) as Pointer[UInt8], @@ -1813,16 +1914,33 @@ class pub UnixSocket { } } +impl RawSocketOperations for UnixSocket { + fn mut raw_socket -> Pointer[RawSocket] { + @socket + } + + fn raw_deadline -> Int { + @deadline + } +} + impl Drop for UnixSocket { fn mut drop { - inko_socket_drop(@raw) + inko_socket_drop(@socket) } } impl Read for UnixSocket { fn pub mut read(into: mut ByteArray, size: Int) -> Result[Int, Error] { match - inko_socket_read(_INKO.state, _INKO.process, @raw, into, size, @deadline) + inko_socket_read( + _INKO.state, + _INKO.process, + @socket, + into, + size, + @deadline, + ) { case { @tag = 0, @value = v } -> Result.Ok(v) case { @tag = _, @value = e } -> Result.Error(Error.from_os_error(e)) @@ -1835,7 +1953,7 @@ impl WriteInternal for UnixSocket { let state = _INKO.state let proc = _INKO.process - match inko_socket_write(state, proc, @raw, data, size, @deadline) { + match inko_socket_write(state, proc, @socket, data, size, @deadline) { case { @tag = 0, @value = n } -> Result.Ok(n) case { @value = e } -> Result.Error(Error.from_os_error(e)) } @@ -1873,7 +1991,7 @@ class pub UnixDatagram { # UnixDatagram.new('/tmp/test.sock'.to_path).get # ``` fn pub static new(address: ref Path) -> Result[UnixDatagram, Error] { - let socket = try UnixSocket.new(Type.DGRAM) + let socket = try UnixSocket.datagram try socket.bind(address) Result.Ok(UnixDatagram(socket)) @@ -1973,6 +2091,16 @@ class pub UnixDatagram { } } +impl RawSocketOperations for UnixDatagram { + fn mut raw_socket -> Pointer[RawSocket] { + @socket.socket + } + + fn raw_deadline -> Int { + @socket.deadline + } +} + impl Read for UnixDatagram { fn pub mut read(into: mut ByteArray, size: Int) -> Result[Int, Error] { @socket.read(into, size) @@ -2016,7 +2144,7 @@ class pub UnixClient { # UnixClient.new('/tmp/test.sock'.to_path).get # ``` fn pub static new(address: ref Path) -> Result[UnixClient, Error] { - let socket = try UnixSocket.new(Type.STREAM) + let socket = try UnixSocket.stream try socket.connect(address) Result.Ok(UnixClient(socket)) @@ -2043,11 +2171,11 @@ class pub UnixClient { # ) # .get # ``` - fn pub static with_timeout[I: ToInstant]( + fn pub static with_timeout[T: ToInstant]( address: ref Path, - timeout_after: I, + timeout_after: ref T, ) -> Result[UnixClient, Error] { - let socket = try UnixSocket.new(Type.STREAM) + let socket = try UnixSocket.stream socket.timeout_after = timeout_after try socket.connect(address) @@ -2093,6 +2221,16 @@ class pub UnixClient { } } +impl RawSocketOperations for UnixClient { + fn mut raw_socket -> Pointer[RawSocket] { + @socket.socket + } + + fn raw_deadline -> Int { + @socket.deadline + } +} + impl Read for UnixClient { fn pub mut read(into: mut ByteArray, size: Int) -> Result[Int, Error] { @socket.read(into, size) @@ -2132,7 +2270,7 @@ class pub UnixServer { # UnixServer.new('/tmp/test.sock'.to_path).get # ``` fn pub static new(address: ref Path) -> Result[UnixServer, Error] { - let socket = try UnixSocket.new(Type.STREAM) + let socket = try UnixSocket.stream try socket.bind(address) try socket.listen @@ -2181,3 +2319,13 @@ class pub UnixServer { @socket.try_clone.map(fn (sock) { UnixServer(sock) }) } } + +impl RawSocketOperations for UnixServer { + fn mut raw_socket -> Pointer[RawSocket] { + @socket.socket + } + + fn raw_deadline -> Int { + @socket.deadline + } +} diff --git a/std/src/std/net/tls.inko b/std/src/std/net/tls.inko new file mode 100644 index 000000000..70cf0edf5 --- /dev/null +++ b/std/src/std/net/tls.inko @@ -0,0 +1,684 @@ +# TLS support for sockets. +# +# This module provides socket support for TLS 1.2 and TLS 1.3. +# +# The two main socket types are `Client` and `Server`, both acting as wrappers +# around existing socket types (e.g. `std.net.socket.TcpClient`) that +# transparently handle TLS encryption and decryption. +# +# For more details on how to set up a client and/or server socket, refer to the +# documentation of `Client.new` and `Server.new`. +# +# # Handling closing of connections +# +# The TLS specification states that clients _should_ send the `close_notify` +# message when they disconnect, but not every TLS implementation/user sends it. +# The `Client` and `Server` types provided by this module automatically send the +# `close_notify` message when they're dropped. +# +# When performing an IO operation on a socket closed without an explicit +# `close_notify` message being sent first, an `Error.EndOfInput` error is +# produced. +# +# When receiving a `close_notify` message during or after an IO operation (e.g. +# a write), a `Error.InvalidData` or `Error.BrokenPipe` error may be produced. +import std.clone (Clone) +import std.cmp (Equal) +import std.drop (Drop) +import std.fmt (Format, Formatter) +import std.fs.path (Path) +import std.io (Error, Read, Write, WriteInternal) +import std.net.socket (RawSocket, RawSocketOperations) +import std.string (ToString) + +# The error code produced when a TLS certificate is invalid. +let INVALID_CERT = -1 + +# The error code produced when a TLS private key is invalid. +let INVALID_KEY = -2 + +fn extern inko_tls_client_config_new -> Pointer[UInt8] + +fn extern inko_tls_client_config_clone(config: Pointer[UInt8]) -> Pointer[UInt8] + +fn extern inko_tls_client_config_drop(config: Pointer[UInt8]) + +fn extern inko_tls_client_config_with_certificate(path: String) -> AnyResult + +fn extern inko_tls_client_connection_new( + config: Pointer[UInt8], + name: String, +) -> AnyResult + +fn extern inko_tls_client_connection_drop(connection: Pointer[UInt8]) + +fn extern inko_tls_server_config_new( + certificate: String, + key: String, +) -> AnyResult + +fn extern inko_tls_server_config_clone(config: Pointer[UInt8]) -> Pointer[UInt8] + +fn extern inko_tls_server_config_drop(config: Pointer[UInt8]) + +fn extern inko_tls_server_connection_new( + config: Pointer[UInt8], +) -> Pointer[UInt8] + +fn extern inko_tls_server_connection_drop(connection: Pointer[UInt8]) + +fn extern inko_tls_client_read( + state: Pointer[UInt8], + process: Pointer[UInt8], + socket: Pointer[RawSocket], + connection: Pointer[UInt8], + buffer: mut ByteArray, + amount: Int, + deadline: Int, +) -> IntResult + +fn extern inko_tls_client_write( + state: Pointer[UInt8], + process: Pointer[UInt8], + socket: Pointer[RawSocket], + connection: Pointer[UInt8], + data: Pointer[UInt8], + size: Int, + deadline: Int, +) -> IntResult + +fn extern inko_tls_client_flush( + state: Pointer[UInt8], + process: Pointer[UInt8], + socket: Pointer[RawSocket], + connection: Pointer[UInt8], +) -> IntResult + +fn extern inko_tls_client_close( + state: Pointer[UInt8], + process: Pointer[UInt8], + socket: Pointer[RawSocket], + connection: Pointer[UInt8], + deadline: Int, +) -> IntResult + +fn extern inko_tls_server_read( + state: Pointer[UInt8], + process: Pointer[UInt8], + socket: Pointer[RawSocket], + connection: Pointer[UInt8], + buffer: mut ByteArray, + amount: Int, + deadline: Int, +) -> IntResult + +fn extern inko_tls_server_write( + state: Pointer[UInt8], + process: Pointer[UInt8], + socket: Pointer[RawSocket], + connection: Pointer[UInt8], + data: Pointer[UInt8], + size: Int, + deadline: Int, +) -> IntResult + +fn extern inko_tls_server_flush( + state: Pointer[UInt8], + process: Pointer[UInt8], + socket: Pointer[RawSocket], + connection: Pointer[UInt8], +) -> IntResult + +fn extern inko_tls_server_close( + state: Pointer[UInt8], + process: Pointer[UInt8], + socket: Pointer[RawSocket], + connection: Pointer[UInt8], + deadline: Int, +) -> IntResult + +class extern AnyResult { + let @tag: Int + let @value: UInt64 +} + +class extern IntResult { + let @tag: Int + let @value: Int +} + +# An error produced when creating a `ClientConfig`. +class pub enum ClientConfigError { + # An IO error occurred when reading the certificate. + case Read(Error) + + # The certificate exists but is invalid, such as when it's revoked or not + # encoded correctly. + case InvalidCertificate +} + +impl Equal[ref ClientConfigError] for ClientConfigError { + fn pub ==(other: ref ClientConfigError) -> Bool { + match (self, other) { + case (Read(a), Read(b)) -> a == b + case (InvalidCertificate, InvalidCertificate) -> true + case _ -> false + } + } +} + +impl ToString for ClientConfigError { + fn pub to_string -> String { + match self { + case Read(e) -> 'failed to read the certificate: ${e}' + case InvalidCertificate -> 'the certificate is invalid' + } + } +} + +impl Format for ClientConfigError { + fn pub fmt(formatter: mut Formatter) { + match self { + case Read(e) -> formatter.tuple('Read').field(e).finish + case InvalidCertificate -> formatter.tuple('InvalidCertificate').finish + } + } +} + +# An error produced when creating a `ServerConfig`. +class pub enum ServerConfigError { + # An IO error occurred when reading the certificate or private key. + case Read(Error) + + # The certificate exists but is invalid, such as when it's revoked or not + # encoded correctly. + case InvalidCertificate + + # The private key exists but is invalid. + case InvalidPrivateKey +} + +impl Equal[ref ServerConfigError] for ServerConfigError { + fn pub ==(other: ref ServerConfigError) -> Bool { + match (self, other) { + case (Read(a), Read(b)) -> a == b + case (InvalidCertificate, InvalidCertificate) -> true + case (InvalidPrivateKey, InvalidPrivateKey) -> true + case _ -> false + } + } +} + +impl ToString for ServerConfigError { + fn pub to_string -> String { + match self { + case Read(e) -> 'failed to read the certificate or private key: ${e}' + case InvalidCertificate -> 'the certificate is invalid' + case InvalidPrivateKey -> 'the private key is invalid' + } + } +} + +impl Format for ServerConfigError { + fn pub fmt(formatter: mut Formatter) { + match self { + case Read(e) -> formatter.tuple('Read').field(e).finish + case InvalidCertificate -> formatter.tuple('InvalidCertificate').finish + case InvalidPrivateKey -> formatter.tuple('InvalidPrivateKey').finish + } + } +} + +# A type storing the configuration details for TLS clients. +# +# To configure a `Server`, use `ServerConfig` instead. +# +# Creating a `ClientConfig` is potentially expensive, depending on the amount of +# certificates that need to be processed. As such, it's recommended to only +# create a `ClientConfig` once and use `ClientConfig.clone` to clone it whenever +# necessary (e.g. when sharing a `ClientConfig` between processes), as cloning a +# `ClientConfig` is cheap. +class pub ClientConfig { + let @raw: Pointer[UInt8] + + # Returns a new `ClientConfig` that uses the system's certificate store. + # + # # Examples + # + # ```inko + # import std.net.tls (ClientConfig) + # + # ClientConfig.new + # ``` + fn pub static new -> ClientConfig { + ClientConfig(inko_tls_client_config_new) + } + + # Returns a new `ClientConfig` using the specified X.509 certificate. + # + # # Errors + # + # If the certificate at `path` doesn't exist or is invalid, a + # `ClientConfigError` error is returned. + # + # # Examples + # + # ```inko + # import std.net.tls (ClientConfig) + # + # ClientConfig + # .with_certificate('/etc/ssl/example.pem'.to_path) + # .or_panic('failed to create the configuration') + # ``` + fn pub static with_certificate( + path: ref Path, + ) -> Result[ClientConfig, ClientConfigError] { + match inko_tls_client_config_with_certificate(path.to_string) { + case { @tag = 0, @value = v } -> { + Result.Ok(ClientConfig(v as Pointer[UInt8])) + } + case { @tag = _, @value = e } -> { + match e as Int { + case INVALID_CERT -> { + Result.Error(ClientConfigError.InvalidCertificate) + } + case e -> Result.Error(ClientConfigError.Read(Error.from_os_error(e))) + } + } + } + } +} + +impl Drop for ClientConfig { + fn mut drop { + inko_tls_client_config_drop(@raw) + } +} + +impl Clone[ClientConfig] for ClientConfig { + fn pub clone -> ClientConfig { + ClientConfig(inko_tls_client_config_clone(@raw)) + } +} + +# A type that acts as the client in a TLS session. +# +# `Client` values wrap existing sockets such as `std.net.socket.TcpClient` and +# apply TLS encryption/decryption to IO operations. +# +# # Closing TLS connections +# +# When a `Client` is dropped the TLS connection is closed by sending the TLS +# `close_notify` message. +# +# # Examples +# +# ```inko +# import std.net.ip (IpAddress) +# import std.net.socket (TcpClient) +# import std.net.tls (Client, ClientConfig) +# +# let conf = ClientConfig.new +# let sock = TcpClient +# .new(ip: IpAddress.v4(127, 0, 0, 1), port: 9000) +# .or_panic('failed to connect to the server') +# let client = Client +# .new(socket, conf, name: 'localhost') +# .or_panic('the server name is invalid') +# +# client.write_string('ping').or_panic('failed to write the message') +# +# let response = ByteArray.new +# +# client.read_all(response).or_panic('failed to read the response') +# ``` +class pub Client[T: mut + RawSocketOperations] { + # The socket wrapped by this `Client`. + let pub @socket: T + + # The TLS connection state. + let @state: Pointer[UInt8] + + # Returns a `Client` acting as the client in a TLS session. + # + # The `socket` argument is the socket (e.g. `std.net.socket.TcpClient`) to + # wrap. This can be either an owned socket or a mutable borrow of a socket. + # + # The `name` argument is the DNS name to use for Server Name Indication (SNI). + # Setting this to an IP address disables the use of SNI. In most cases you'll + # want to set this to the DNS name of the server the socket is connecting to. + # + # The `config` argument is a `ClientConfig` instance to use for configuring + # the TLS connection. + # + # # Errors + # + # This method returns an `Option.None` if the `name` argument contains an + # invalid value. + # + # # Examples + # + # ```inko + # import std.net.ip (IpAddress) + # import std.net.socket (TcpClient) + # import std.net.tls (Client, ClientConfig) + # + # let conf = ClientConfig.new + # let sock = TcpClient + # .new(ip: IpAddress.v4(127, 0, 0, 1), port: 9000) + # .or_panic('failed to connect to the server') + # + # Client + # .new(sock, conf, name: 'localhost') + # .or_panic('the server name is invalid') + # ``` + fn pub static new( + socket: T, + config: ref ClientConfig, + name: String, + ) -> Option[Client[T]] { + let state = match inko_tls_client_connection_new(config.raw, name) { + case { @tag = 0, @value = v } -> v as Pointer[UInt8] + case _ -> return Option.None + } + + Option.Some(Client(socket, state)) + } + + # Sends the TLS `close_notify` message to the socket, informing the peer that + # the connection is being closed. + fn pub mut close -> Result[Nil, Error] { + match + inko_tls_client_close( + _INKO.state, + _INKO.process, + @socket.raw_socket, + @state, + @socket.raw_deadline, + ) + { + case { @tag = 1 } -> Result.Ok(nil) + case { @value = e } -> Result.Error(Error.from_os_error(e)) + } + } +} + +impl Drop for Client { + fn mut drop { + # Per the TLS specification, the connection _should_ be closed explicitly + # when discarding the socket. + let _ = close + + inko_tls_client_connection_drop(@state) + } +} + +impl Read for Client { + fn pub mut read(into: mut ByteArray, size: Int) -> Result[Int, Error] { + match + inko_tls_client_read( + _INKO.state, + _INKO.process, + @socket.raw_socket, + @state, + into, + size, + @socket.raw_deadline, + ) + { + case { @tag = 0, @value = v } -> Result.Ok(v) + case { @tag = _, @value = e } -> Result.Error(Error.from_os_error(e)) + } + } +} + +impl WriteInternal for Client { + fn mut write_internal(data: Pointer[UInt8], size: Int) -> Result[Int, Error] { + match + inko_tls_client_write( + _INKO.state, + _INKO.process, + @socket.raw_socket, + @state, + data, + size, + @socket.raw_deadline, + ) + { + case { @tag = 0, @value = v } -> Result.Ok(v) + case { @value = e } -> Result.Error(Error.from_os_error(e)) + } + } +} + +impl Write for Client { + fn pub mut write_bytes(bytes: ref ByteArray) -> Result[Nil, Error] { + write_all_internal(bytes.to_pointer, bytes.size) + } + + fn pub mut write_string(string: String) -> Result[Nil, Error] { + write_all_internal(string.to_pointer, string.size) + } + + fn pub mut flush -> Result[Nil, Never] { + Result.Ok(nil) + } +} + +# A type storing the configuration details for TLS servers. +# +# To configure a `Client`, use `ClientConfig` instead. +# +# Creating a `ServerConfig` is potentially expensive, depending on the +# certificate and private key that are used. As such, it's recommended to only +# create a `ServerConfig` once and use `ServerConfig.clone` to clone it whenever +# necessary, as cloning a `ServerConfig` is cheap. +class pub ServerConfig { + let @raw: Pointer[UInt8] + + # Returns a new `ClientConfig` using the specified X.509 certificate and + # private key. + # + # # Errors + # + # A `ServerConfigError` is returned if any of the following is true: + # + # - The certificate and/or private key couldn't be read (e.g. they don't exist + # or the user has insufficient permissions) + # - The certificate and/or private key is invalid (e.g. the used format isn't + # correct) + # + # # Examples + # + # ```inko + # import std.net.tls (ServerConfig) + # + # ServerConfig + # .new(certificate: 'cert.pem'.to_path, key: 'cert.key'.to_path) + # .or_panic('failed to create the configuration') + # ``` + fn pub static new( + certificate: ref Path, + key: ref Path, + ) -> Result[ServerConfig, ServerConfigError] { + match inko_tls_server_config_new(certificate.to_string, key.to_string) { + case { @tag = 0, @value = v } -> { + Result.Ok(ServerConfig(v as Pointer[UInt8])) + } + case { @tag = _, @value = e } -> { + match e as Int { + case INVALID_CERT -> { + Result.Error(ServerConfigError.InvalidCertificate) + } + case INVALID_KEY -> Result.Error(ServerConfigError.InvalidPrivateKey) + case e -> Result.Error(ServerConfigError.Read(Error.from_os_error(e))) + } + } + } + } +} + +impl Drop for ServerConfig { + fn mut drop { + inko_tls_server_config_drop(@raw) + } +} + +impl Clone[ServerConfig] for ServerConfig { + fn pub clone -> ServerConfig { + ServerConfig(inko_tls_server_config_clone(@raw)) + } +} + +# A type that acts as the server in a TLS session. +# +# `Server` values wrap existing sockets such as `std.net.socket.TcpClient` and +# apply TLS encryption/decryption to IO operations. +# +# # Closing TLS connections +# +# When a `Client` is dropped the TLS connection is closed by sending the TLS +# `close_notify` message. +# +# # Examples +# +# ```inko +# import std.net.ip (IpAddress) +# import std.net.socket (TcpServer) +# import std.net.tls (Server, ServerConfig) +# +# let conf = ServerConfig +# .new(certificate: 'cert.pem'.to_path, key: 'cert.key'.to_path) +# .or_panic('failed to create the server configuration') +# +# let server = TcpServer +# .new(ip: IpAddress.v4(0, 0, 0, 0), port: 9000) +# .or_panic('failed to start the server') +# +# let con = server +# .accept +# .map(fn (sock) { Server.new(sock, conf) }) +# .or_panic('failed to accept the new connection') +# +# let bytes = ByteArray.new +# +# con.read(into: bytes, size: 32).or_panic('failed to read the data') +# ``` +class pub Server[T: mut + RawSocketOperations] { + # The socket wrapped by this `Server`. + let pub @socket: T + + # The TLS connection state. + let @state: Pointer[UInt8] + + # Returns a `Server` acting as the server in a TLS session. + # + # The `socket` argument is the socket (e.g. `std.net.socket.TcpClient`) to + # wrap. This can be either an owned socket or a mutable borrow of a socket. + # + # The `config` argument is a `ServerConfig` instance to use for configuring + # the TLS connection. + # + # # Examples + # + # ```inko + # import std.net.ip (IpAddress) + # import std.net.socket (TcpServer) + # import std.net.tls (Server, ServerConfig) + # + # let conf = ServerConfig + # .new(certificate: 'cert.pem'.to_path, key: 'cert.key'.to_path) + # .or_panic('failed to create the server configuration') + # + # let server = TcpServer + # .new(ip: IpAddress.v4(0, 0, 0, 0), port: 9000) + # .or_panic('failed to start the server') + # + # server + # .accept + # .map(fn (sock) { Server.new(sock, conf) }) + # .or_panic('failed to accept the new connection') + # ``` + fn pub static new(socket: T, config: ref ServerConfig) -> Server[T] { + Server(socket, inko_tls_server_connection_new(config.raw)) + } + + # Sends the TLS `close_notify` message to the socket, informing the peer that + # the connection is being closed. + fn pub mut close -> Result[Nil, Error] { + match + inko_tls_server_close( + _INKO.state, + _INKO.process, + @socket.raw_socket, + @state, + @socket.raw_deadline, + ) + { + case { @tag = 1 } -> Result.Ok(nil) + case { @value = e } -> Result.Error(Error.from_os_error(e)) + } + } +} + +impl Drop for Server { + fn mut drop { + # Per the TLS specification, the connection _should_ be closed explicitly + # when discarding the socket. + let _ = close + + inko_tls_server_connection_drop(@state) + } +} + +impl Read for Server { + fn pub mut read(into: mut ByteArray, size: Int) -> Result[Int, Error] { + match + inko_tls_server_read( + _INKO.state, + _INKO.process, + @socket.raw_socket, + @state, + into, + size, + @socket.raw_deadline, + ) + { + case { @tag = 0, @value = v } -> Result.Ok(v) + case { @tag = _, @value = e } -> Result.Error(Error.from_os_error(e)) + } + } +} + +impl WriteInternal for Server { + fn mut write_internal(data: Pointer[UInt8], size: Int) -> Result[Int, Error] { + match + inko_tls_server_write( + _INKO.state, + _INKO.process, + @socket.raw_socket, + @state, + data, + size, + @socket.raw_deadline, + ) + { + case { @tag = 0, @value = v } -> Result.Ok(v) + case { @value = e } -> Result.Error(Error.from_os_error(e)) + } + } +} + +impl Write for Server { + fn pub mut write_bytes(bytes: ref ByteArray) -> Result[Nil, Error] { + write_all_internal(bytes.to_pointer, bytes.size) + } + + fn pub mut write_string(string: String) -> Result[Nil, Error] { + write_all_internal(string.to_pointer, string.size) + } + + fn pub mut flush -> Result[Nil, Never] { + Result.Ok(nil) + } +} diff --git a/std/src/std/string.inko b/std/src/std/string.inko index 682dcfd0d..a55cf1383 100644 --- a/std/src/std/string.inko +++ b/std/src/std/string.inko @@ -482,28 +482,38 @@ class builtin String { # Returns a new `String` without the given prefix. # + # If `self` starts with the prefix, a `Option.Some` is returned containing the + # substring after the prefix. If `self` doesn't start with the prefix, an + # `Option.None` is returned. + # # # Examples # # ```inko - # 'xhellox'.strip_prefix('x') # => 'hellox' + # 'xhellox'.strip_prefix('x') # => Option.Some('hellox') + # 'xhellox'.strip_prefix('y') # => Option.None # ``` - fn pub strip_prefix(prefix: String) -> String { - if starts_with?(prefix).false? { return clone } + fn pub strip_prefix(prefix: String) -> Option[String] { + if starts_with?(prefix).false? { return Option.None } - slice(start: prefix.size, size: size - prefix.size).into_string + Option.Some(slice(start: prefix.size, size: size - prefix.size).into_string) } # Returns a new `String` without the given suffix. # + # If `self` ends with the suffix, a `Option.Some` is returned containing the + # substring before the prefix. If `self` doesn't end with the suffix, an + # `Option.None` is returned. + # # # Examples # # ```inko - # 'xhellox'.strip_suffix('x') # => 'xhello' + # 'xhellox'.strip_suffix('x') # => Option.Some('xhello') + # 'xhellox'.strip_suffix('y') # => Option.None # ``` - fn pub strip_suffix(suffix: String) -> String { - if ends_with?(suffix).false? { return clone } + fn pub strip_suffix(suffix: String) -> Option[String] { + if ends_with?(suffix).false? { return Option.None } - slice(start: 0, size: size - suffix.size).into_string + Option.Some(slice(start: 0, size: size - suffix.size).into_string) } # Returns a new `String` without any leading whitespace. diff --git a/std/test/compiler/test_diagnostics.inko b/std/test/compiler/test_diagnostics.inko index 6df68cb01..1a0336817 100644 --- a/std/test/compiler/test_diagnostics.inko +++ b/std/test/compiler/test_diagnostics.inko @@ -215,7 +215,7 @@ class Diagnostic { # We remove the directory leading up to the file, that way the diagnostic # lines in the test file don't need to specify the full file paths, and # debugging failing tests is a little less annoying due to noisy output. - let file = (try string(map, 'file')).strip_prefix('${directory}/') + let file = (try string(map, 'file')).strip_prefix('${directory}/').get let line = try location(map, 'lines') let column = try location(map, 'columns') let message = try string(map, 'message') @@ -277,7 +277,7 @@ fn pub tests(t: mut Tests) { case Error(e) -> panic('failed to read the diagnostics directory: ${e}') } - let name = test_file.tail.strip_suffix('.inko') + let name = test_file.tail.strip_suffix('.inko').get t.test('inko check ${name}', fn move (t) { let file = ReadOnlyFile.new(test_file.clone).or_panic( diff --git a/std/test/std/fs/test_path.inko b/std/test/std/fs/test_path.inko index bb0bcbdf0..6e31649e9 100644 --- a/std/test/std/fs/test_path.inko +++ b/std/test/std/fs/test_path.inko @@ -5,6 +5,7 @@ import std.fs (DirectoryEntry, FileType) import std.fs.file (self, ReadOnlyFile, WriteOnlyFile) import std.fs.path (self, Path) import std.io (Error) +import std.stdio (STDOUT) import std.sys import std.test (Tests) import std.time (DateTime) @@ -153,17 +154,37 @@ fn pub tests(t: mut Tests) { t.test('Path.fmt', fn (t) { t.equal(fmt(Path.new('foo')), '"foo"') }) t.test('Path.expand', fn (t) { - let temp = env.temporary_directory - let bar = temp.join('foo').join('bar') + with_directory(t.id, fn (temp) { + let bar = temp.join('foo').join('bar') - bar.create_directory_all.get + bar.create_directory_all.get - let expanded = bar.join('..').join('..').expand + let expanded = bar.join('..').join('..').expand - t.equal(expanded, Result.Ok(temp)) - bar.remove_directory_all + t.equal(expanded, Result.Ok(temp.clone)) + }) + + t.equal(Path.new('~').expand.ok, env.home_directory) + t.equal(Path.new('~/').expand.ok, env.home_directory) + t.true(Path.new('~foo').expand.error?) + t.true(Path.new('/~').expand.error?) + t.true(Path.new('~/this-directory-should-not-exist').expand.error?) }) + t.fork( + 'Path.expand with a missing home directory', + child: fn { + let out = STDOUT.new + let res = Path.new('~').expand.map(fn (v) { v.to_string }).or('ERROR') + + out.write_string(res) + }, + test: fn (test, proc) { + proc.variable('HOME', '') + test.equal(proc.spawn.stdout, 'ERROR') + }, + ) + t.test('Path.tail', fn (t) { t.equal(Path.new('foo').tail, 'foo') t.equal(Path.new('foo/').tail, 'foo') diff --git a/std/test/std/net/test_socket.inko b/std/test/std/net/test_socket.inko index d1c212940..1d05a12dc 100644 --- a/std/test/std/net/test_socket.inko +++ b/std/test/std/net/test_socket.inko @@ -6,7 +6,7 @@ import std.fs.path (Path) import std.io (Error) import std.net.ip (IpAddress, Ipv4Address, Ipv6Address) import std.net.socket ( - NO_DEADLINE, Socket, SocketAddress, TcpClient, TcpServer, Type, UdpSocket, + NO_DEADLINE, Socket, SocketAddress, TcpClient, TcpServer, UdpSocket, UnixAddress, UnixClient, UnixDatagram, UnixServer, UnixSocket, ) import std.string (ToString) @@ -47,15 +47,15 @@ impl Drop for SocketPath { fn pub tests(t: mut Tests) { t.test('SocketAddress.new', fn (t) { - let addr = SocketAddress.new(address: '127.0.0.1', port: 1234) + let addr = SocketAddress(ip: IpAddress.v4(127, 0, 0, 1), port: 1234) - t.equal(addr.ip, Option.Some(IpAddress.V4(Ipv4Address.new(127, 0, 0, 1)))) + t.equal(addr.ip, IpAddress.V4(Ipv4Address.new(127, 0, 0, 1))) t.equal(addr.port, 1234) }) t.test('SocketAddress.==', fn (t) { - let addr1 = SocketAddress.new(address: '127.0.0.1', port: 1234) - let addr2 = SocketAddress.new(address: '127.0.0.1', port: 4567) + let addr1 = SocketAddress(ip: IpAddress.v4(127, 0, 0, 1), port: 1234) + let addr2 = SocketAddress(ip: IpAddress.v4(127, 0, 0, 1), port: 4567) t.equal(addr1, addr1) t.not_equal(addr1, addr2) @@ -63,47 +63,47 @@ fn pub tests(t: mut Tests) { t.test('SocketAddress.fmt', fn (t) { t.equal( - fmt(SocketAddress.new(address: '127.0.0.1', port: 1234)), + fmt(SocketAddress(ip: IpAddress.v4(127, 0, 0, 1), port: 1234)), '127.0.0.1:1234', ) }) - t.test('Socket.ipv4', fn (t) { - t.true(Socket.ipv4(Type.STREAM).ok?) - t.true(Socket.ipv4(Type.DGRAM).ok?) + t.test('Socket.stream', fn (t) { + t.true(Socket.stream(ipv6: false).ok?) + t.true(Socket.stream(ipv6: true).ok?) }) - t.test('Socket.ipv6', fn (t) { - t.true(Socket.ipv6(Type.STREAM).ok?) - t.true(Socket.ipv6(Type.DGRAM).ok?) + t.test('Socket.datagram', fn (t) { + t.true(Socket.datagram(ipv6: false).ok?) + t.true(Socket.datagram(ipv6: true).ok?) }) t.test('Socket.bind', fn (t) { { - let sock = Socket.ipv4(Type.STREAM).get + let sock = Socket.stream(ipv6: false).get t.true(sock.bind(ip: IpAddress.v4(-1, -1, -1, -1), port: 0).error?) } { - let sock = Socket.ipv4(Type.STREAM).get + let sock = Socket.stream(ipv6: false).get t.true(sock.bind(ip: IpAddress.v4(0, 0, 0, 0), port: 0).ok?) } }) t.test('Socket.connect', fn (t) { - let listener = Socket.ipv4(Type.STREAM).get - let stream1 = Socket.ipv4(Type.STREAM).get + let listener = Socket.stream(ipv6: false).get + let stream1 = Socket.stream(ipv6: false).get listener.bind(ip: IpAddress.v4(127, 0, 0, 1), port: 0).get listener.listen.get let addr = listener.local_address.get - t.true(stream1.connect(addr.ip.get, addr.port).ok?) + t.true(stream1.connect(addr.ip, addr.port).ok?) - let stream2 = Socket.ipv4(Type.STREAM).get + let stream2 = Socket.stream(ipv6: false).get # connect() may not immediately raise a "connection refused" error, due to # connect() being non-blocking. In this case the "connection refused" error @@ -118,22 +118,22 @@ fn pub tests(t: mut Tests) { }) t.test('Socket.listen', fn (t) { - let socket = Socket.ipv4(Type.STREAM).get + let socket = Socket.stream(ipv6: false).get socket.bind(ip: IpAddress.v4(0, 0, 0, 0), port: 0).get t.true(socket.listen.ok?) }) t.test('Socket.accept', fn (t) { - let server = Socket.ipv4(Type.STREAM).get - let client = Socket.ipv4(Type.STREAM).get + let server = Socket.stream(ipv6: false).get + let client = Socket.stream(ipv6: false).get server.bind(ip: IpAddress.v4(127, 0, 0, 1), port: 0).get server.listen.get let addr = server.local_address.get - t.equal(client.connect(addr.ip.get, addr.port), Result.Ok(nil)) + t.equal(client.connect(addr.ip, addr.port), Result.Ok(nil)) let connection = server.accept.get @@ -141,7 +141,7 @@ fn pub tests(t: mut Tests) { }) t.test('Socket.send_string_to', fn (t) { - let socket = Socket.ipv4(Type.DGRAM).get + let socket = Socket.datagram(ipv6: false).get socket.bind(ip: IpAddress.v4(127, 0, 0, 1), port: 0).get @@ -149,7 +149,7 @@ fn pub tests(t: mut Tests) { let buffer = ByteArray.new t.equal( - socket.send_string_to('ping', send_to.ip.get, send_to.port), + socket.send_string_to('ping', send_to.ip, send_to.port), Result.Ok(4), ) t.equal(socket.read(into: buffer, size: 4), Result.Ok(4)) @@ -157,7 +157,7 @@ fn pub tests(t: mut Tests) { }) t.test('Socket.send_bytes_to', fn (t) { - let socket = Socket.ipv4(Type.DGRAM).get + let socket = Socket.datagram(ipv6: false).get socket.bind(ip: IpAddress.v4(127, 0, 0, 1), port: 0).get @@ -165,11 +165,7 @@ fn pub tests(t: mut Tests) { let buffer = ByteArray.new t.equal( - socket.send_bytes_to( - 'ping'.to_byte_array, - send_to.ip.get, - send_to.port.clone, - ), + socket.send_bytes_to('ping'.to_byte_array, send_to.ip, send_to.port.clone), Result.Ok(4), ) t.equal(socket.read(into: buffer, size: 4), Result.Ok(4)) @@ -177,8 +173,8 @@ fn pub tests(t: mut Tests) { }) t.test('Socket.receive_from', fn (t) { - let listener = Socket.ipv4(Type.DGRAM).get - let client = Socket.ipv4(Type.DGRAM).get + let listener = Socket.datagram(ipv6: false).get + let client = Socket.datagram(ipv6: false).get listener.bind(ip: IpAddress.v4(127, 0, 0, 1), port: 0).get client.bind(ip: IpAddress.v4(127, 0, 0, 1), port: 0).get @@ -186,7 +182,7 @@ fn pub tests(t: mut Tests) { let send_to = listener.local_address.get t.equal( - client.send_string_to('ping', send_to.ip.get, send_to.port.clone), + client.send_string_to('ping', send_to.ip, send_to.port.clone), Result.Ok(4), ) @@ -197,70 +193,70 @@ fn pub tests(t: mut Tests) { }) t.test('Socket.local_address with an unbound socket', fn (t) { - let socket = Socket.ipv4(Type.DGRAM).get + let socket = Socket.datagram(ipv6: false).get let address = socket.local_address.get - t.equal(address.address, '0.0.0.0') + t.equal(address.ip, IpAddress.v4(0, 0, 0, 0)) t.equal(address.port, 0) }) t.test('Socket.local_address with a bound socket', fn (t) { - let socket = Socket.ipv4(Type.DGRAM).get + let socket = Socket.datagram(ipv6: false).get socket.bind(ip: IpAddress.v4(127, 0, 0, 1), port: 0).get let local_address = socket.local_address.get - t.equal(local_address.address, '127.0.0.1') + t.equal(local_address.ip, IpAddress.v4(127, 0, 0, 1)) t.true(local_address.port > 0) }) t.test('Socket.peer_address with a disconnected socket', fn (t) { - let socket = Socket.ipv4(Type.DGRAM).get + let socket = Socket.datagram(ipv6: false).get t.true(socket.peer_address.error?) }) t.test('Socket.peer_address with a connected socket', fn (t) { - let listener = Socket.ipv4(Type.STREAM).get - let client = Socket.ipv4(Type.STREAM).get + let listener = Socket.stream(ipv6: false).get + let client = Socket.stream(ipv6: false).get listener.bind(ip: IpAddress.v4(127, 0, 0, 1), port: 0).get listener.listen.get let addr = listener.local_address.get - t.equal(client.connect(addr.ip.get, addr.port), Result.Ok(nil)) + t.equal(client.connect(addr.ip, addr.port), Result.Ok(nil)) t.equal(client.peer_address, Result.Ok(addr)) }) t.test('Socket.ttl=', fn (t) { - let socket = Socket.ipv4(Type.STREAM).get + let socket = Socket.stream(ipv6: false).get t.true((socket.ttl = 10).ok?) }) t.test('Socket.only_ipv6=', fn (t) { - let socket = Socket.ipv6(Type.STREAM).get + let socket = Socket.stream(ipv6: true).get t.true((socket.only_ipv6 = true).ok?) }) t.test('Socket.no_delay=', fn (t) { - let socket = Socket.ipv4(Type.STREAM).get + let socket = Socket.stream(ipv6: false).get t.true((socket.no_delay = true).ok?) t.true(socket.no_delay?) }) t.test('Socket.broadcast=', fn (t) { - let socket = Socket.ipv4(Type.DGRAM).get + let socket = Socket.datagram(ipv6: false).get t.true((socket.broadcast = true).ok?) }) t.test('Socket.linger=', fn (t) { - let socket = Socket.ipv4(Type.STREAM).get + let socket = Socket.stream(ipv6: false).get let duration = Duration.from_secs(5) t.true((socket.linger = Option.Some(duration)).ok?) @@ -271,45 +267,45 @@ fn pub tests(t: mut Tests) { }) t.test('Socket.receive_buffer_size=', fn (t) { - let socket = Socket.ipv4(Type.STREAM).get + let socket = Socket.stream(ipv6: false).get t.true((socket.receive_buffer_size = 256).ok?) }) t.test('Socket.send_buffer_size=', fn (t) { - let socket = Socket.ipv4(Type.STREAM).get + let socket = Socket.stream(ipv6: false).get t.true((socket.send_buffer_size = 256).ok?) }) t.test('Socket.keepalive=', fn (t) { - let socket = Socket.ipv4(Type.STREAM).get + let socket = Socket.stream(ipv6: false).get t.true((socket.keepalive = true).ok?) }) t.test('Socket.reuse_adress=', fn (t) { - let socket = Socket.ipv6(Type.DGRAM).get + let socket = Socket.datagram(ipv6: true).get t.true((socket.reuse_address = true).ok?) }) t.test('Socket.reuse_port=', fn (t) { - let socket = Socket.ipv6(Type.DGRAM).get + let socket = Socket.datagram(ipv6: true).get t.true((socket.reuse_port = true).ok?) }) t.test('Socket.shutdown_read', fn (t) { - let listener = Socket.ipv4(Type.STREAM).get - let stream = Socket.ipv4(Type.STREAM).get + let listener = Socket.stream(ipv6: false).get + let stream = Socket.stream(ipv6: false).get listener.bind(ip: IpAddress.v4(127, 0, 0, 1), port: 0).get listener.listen.get let addr = listener.local_address.get - t.equal(stream.connect(addr.ip.get, addr.port), Result.Ok(nil)) + t.equal(stream.connect(addr.ip, addr.port), Result.Ok(nil)) stream.shutdown_read.get let bytes = ByteArray.new @@ -319,45 +315,45 @@ fn pub tests(t: mut Tests) { }) t.test('Socket.shutdown_write', fn (t) { - let listener = Socket.ipv4(Type.STREAM).get - let stream = Socket.ipv4(Type.STREAM).get + let listener = Socket.stream(ipv6: false).get + let stream = Socket.stream(ipv6: false).get listener.bind(ip: IpAddress.v4(127, 0, 0, 1), port: 0).get listener.listen.get let addr = listener.local_address.get - t.equal(stream.connect(addr.ip.get, addr.port), Result.Ok(nil)) + t.equal(stream.connect(addr.ip, addr.port), Result.Ok(nil)) stream.shutdown_write.get t.true(stream.write_string('ping').error?) }) t.test('Socket.shutdown shuts down the writing half', fn (t) { - let listener = Socket.ipv4(Type.STREAM).get - let stream = Socket.ipv4(Type.STREAM).get + let listener = Socket.stream(ipv6: false).get + let stream = Socket.stream(ipv6: false).get listener.bind(ip: IpAddress.v4(127, 0, 0, 1), port: 0).get listener.listen.get let addr = listener.local_address.get - t.equal(stream.connect(addr.ip.get, addr.port), Result.Ok(nil)) + t.equal(stream.connect(addr.ip, addr.port), Result.Ok(nil)) stream.shutdown.get t.true(stream.write_string('ping').error?) }) t.test('Socket.shutdown shuts down the reading half', fn (t) { - let listener = Socket.ipv4(Type.STREAM).get - let stream = Socket.ipv4(Type.STREAM).get + let listener = Socket.stream(ipv6: false).get + let stream = Socket.stream(ipv6: false).get listener.bind(ip: IpAddress.v4(127, 0, 0, 1), port: 0).get listener.listen.get let addr = listener.local_address.get - t.equal(stream.connect(addr.ip.get, addr.port), Result.Ok(nil)) + t.equal(stream.connect(addr.ip, addr.port), Result.Ok(nil)) stream.shutdown.get let bytes = ByteArray.new @@ -367,34 +363,34 @@ fn pub tests(t: mut Tests) { }) t.test('Socket.try_clone', fn (t) { - let socket = Socket.ipv4(Type.STREAM).get + let socket = Socket.stream(ipv6: false).get t.true(socket.try_clone.ok?) }) t.test('Socket.read', fn (t) { - let socket = Socket.ipv4(Type.DGRAM).get + let socket = Socket.datagram(ipv6: false).get socket.bind(ip: IpAddress.v4(127, 0, 0, 1), port: 0).get let addr = socket.local_address.get let bytes = ByteArray.new - t.equal(socket.send_string_to('ping', addr.ip.get, addr.port), Result.Ok(4)) + t.equal(socket.send_string_to('ping', addr.ip, addr.port), Result.Ok(4)) t.equal(socket.read(into: bytes, size: 4), Result.Ok(4)) t.equal(bytes.into_string, 'ping') }) t.test('Socket.write_bytes', fn (t) { - let listener = Socket.ipv4(Type.STREAM).get - let stream = Socket.ipv4(Type.STREAM).get + let listener = Socket.stream(ipv6: false).get + let stream = Socket.stream(ipv6: false).get listener.bind(ip: IpAddress.v4(127, 0, 0, 1), port: 0).get listener.listen.get let addr = listener.local_address.get - t.equal(stream.connect(addr.ip.get, addr.port), Result.Ok(nil)) + t.equal(stream.connect(addr.ip, addr.port), Result.Ok(nil)) stream.write_bytes('ping'.to_byte_array).get let connection = listener.accept.get @@ -405,15 +401,15 @@ fn pub tests(t: mut Tests) { }) t.test('Socket.write_string', fn (t) { - let listener = Socket.ipv4(Type.STREAM).get - let stream = Socket.ipv4(Type.STREAM).get + let listener = Socket.stream(ipv6: false).get + let stream = Socket.stream(ipv6: false).get listener.bind(ip: IpAddress.v4(127, 0, 0, 1), port: 0).get listener.listen.get let addr = listener.local_address.get - t.equal(stream.connect(addr.ip.get, addr.port), Result.Ok(nil)) + t.equal(stream.connect(addr.ip, addr.port), Result.Ok(nil)) stream.write_string('ping').get let connection = listener.accept.get @@ -424,13 +420,13 @@ fn pub tests(t: mut Tests) { }) t.test('Socket.flush', fn (t) { - let socket = Socket.ipv4(Type.STREAM).get + let socket = Socket.stream(ipv6: false).get t.equal(socket.flush, Result.Ok(nil)) }) t.test('Socket.timeout_after=', fn (t) { - let server = Socket.ipv4(Type.STREAM).get + let server = Socket.stream(ipv6: false).get server.bind(ip: IpAddress.v4(127, 0, 0, 1), port: 0).get server.listen.get @@ -440,7 +436,7 @@ fn pub tests(t: mut Tests) { }) t.test('Socket.timeout_after= with a read after a timeout', fn (t) { - let server = Socket.ipv4(Type.STREAM).get + let server = Socket.stream(ipv6: false).get let bytes = ByteArray.new server.timeout_after = Duration.from_secs(0) @@ -450,7 +446,7 @@ fn pub tests(t: mut Tests) { }) t.test('Socket.reset_deadline', fn (t) { - let socket = Socket.ipv4(Type.STREAM).get + let socket = Socket.stream(ipv6: false).get socket.timeout_after = Duration.from_secs(10) t.true(socket.deadline > 0) @@ -477,7 +473,7 @@ fn pub tests(t: mut Tests) { let socket2 = UdpSocket.new(ip: ip, port: 0).get let addr = socket2.local_address.get - t.true(socket1.connect(addr.ip.get, addr.port).ok?) + t.true(socket1.connect(addr.ip, addr.port).ok?) }) t.test('UdpSocket.send_string_to', fn (t) { @@ -487,7 +483,7 @@ fn pub tests(t: mut Tests) { let addr = socket.local_address.get - t.equal(socket.send_string_to('ping', addr.ip.get, addr.port), Result.Ok(4)) + t.equal(socket.send_string_to('ping', addr.ip, addr.port), Result.Ok(4)) let bytes = ByteArray.new @@ -500,7 +496,7 @@ fn pub tests(t: mut Tests) { let socket = UdpSocket.new(ip: ip, port: 0).get let addr = socket.local_address.get - socket.send_bytes_to('ping'.to_byte_array, addr.ip.get, addr.port).get + socket.send_bytes_to('ping'.to_byte_array, addr.ip, addr.port).get let bytes = ByteArray.new @@ -514,7 +510,7 @@ fn pub tests(t: mut Tests) { let client = UdpSocket.new(ip: ip, port: 0).get let addr = listener.local_address.get - client.send_string_to('ping', addr.ip.get, addr.port).get + client.send_string_to('ping', addr.ip, addr.port).get let bytes = ByteArray.new @@ -527,7 +523,7 @@ fn pub tests(t: mut Tests) { let socket = UdpSocket.new(ip: ip, port: 0).get let local_address = socket.local_address.get - t.equal(local_address.address, '127.0.0.1') + t.equal(local_address.ip, IpAddress.v4(127, 0, 0, 1)) t.true(local_address.port > 0) }) @@ -543,7 +539,7 @@ fn pub tests(t: mut Tests) { let socket = UdpSocket.new(ip: ip, port: 0).get let addr = socket.local_address.get - t.equal(socket.send_string_to('ping', addr.ip.get, addr.port), Result.Ok(4)) + t.equal(socket.send_string_to('ping', addr.ip, addr.port), Result.Ok(4)) let bytes = ByteArray.new @@ -557,7 +553,7 @@ fn pub tests(t: mut Tests) { let client_socket = UdpSocket.new(ip: ip, port: 0).get let addr = server_socket.local_address.get - t.equal(client_socket.connect(addr.ip.get, addr.port), Result.Ok(nil)) + t.equal(client_socket.connect(addr.ip, addr.port), Result.Ok(nil)) client_socket.write_bytes('ping'.to_byte_array).get let bytes = ByteArray.new @@ -574,20 +570,20 @@ fn pub tests(t: mut Tests) { }) t.ok('TcpClient.new', fn (t) { - let listener = Socket.ipv4(Type.STREAM).get + let listener = Socket.stream(ipv6: false).get listener.bind(ip: IpAddress.v4(127, 0, 0, 1), port: 0).get listener.listen.get let addr = listener.local_address.get - let client = try TcpClient.new(addr.ip.get, addr.port) + let client = try TcpClient.new(addr.ip, addr.port) t.true(client.socket.no_delay?) Result.Ok(nil) }) t.ok('TcpClient.with_timeout', fn (t) { - let listener = Socket.ipv4(Type.STREAM).get + let listener = Socket.stream(ipv6: false).get listener.bind(ip: IpAddress.v4(127, 0, 0, 1), port: 0).get listener.listen.get @@ -596,7 +592,7 @@ fn pub tests(t: mut Tests) { { let client = try TcpClient.with_timeout( - addr.ip.get, + addr.ip, addr.port, timeout_after: Duration.from_secs(2), ) @@ -611,48 +607,56 @@ fn pub tests(t: mut Tests) { timeout_after: Duration.from_micros(500), ) - t.equal(timed_out.error, Option.Some(Error.TimedOut)) + # If no internet connection is available the error is NetworkUnreachable + # instead, so we have to account for that. + t.true( + match timed_out { + case Error(TimedOut or NetworkUnreachable) -> true + case _ -> false + }, + ) + Result.Ok(nil) }) t.ok('TcpClient.local_address', fn (t) { - let listener = try Socket.ipv4(Type.STREAM) + let listener = try Socket.stream(ipv6: false) try listener.bind(ip: IpAddress.v4(127, 0, 0, 1), port: 0) try listener.listen let addr = try listener.local_address - let stream = try TcpClient.new(addr.ip.get, addr.port) + let stream = try TcpClient.new(addr.ip, addr.port) let local_addr = try stream.local_address - t.equal(local_addr.address, '127.0.0.1') + t.equal(local_addr.ip, IpAddress.v4(127, 0, 0, 1)) t.true(local_addr.port > 0) Result.Ok(nil) }) t.ok('TcpClient.peer_address', fn (t) { - let listener = try Socket.ipv4(Type.STREAM) + let listener = try Socket.stream(ipv6: false) try listener.bind(ip: IpAddress.v4(127, 0, 0, 1), port: 0) try listener.listen let addr = try listener.local_address - let stream = try TcpClient.new(addr.ip.get, addr.port) + let stream = try TcpClient.new(addr.ip, addr.port) let peer_addr = try stream.peer_address - t.equal(peer_addr.address, addr.address) + t.equal(peer_addr.ip, addr.ip) t.equal(peer_addr.port, addr.port) Result.Ok(nil) }) t.ok('TcpClient.read', fn (t) { - let listener = try Socket.ipv4(Type.STREAM) + let listener = try Socket.stream(ipv6: false) try listener.bind(ip: IpAddress.v4(127, 0, 0, 1), port: 0) try listener.listen let addr = try listener.local_address - let stream = try TcpClient.new(addr.ip.get, addr.port) + let stream = try TcpClient.new(addr.ip, addr.port) let bytes = ByteArray.new let client = try listener.accept @@ -664,13 +668,13 @@ fn pub tests(t: mut Tests) { }) t.ok('TcpClient.write_bytes', fn (t) { - let listener = try Socket.ipv4(Type.STREAM) + let listener = try Socket.stream(ipv6: false) try listener.bind(ip: IpAddress.v4(127, 0, 0, 1), port: 0) try listener.listen let addr = try listener.local_address - let stream = try TcpClient.new(addr.ip.get, addr.port) + let stream = try TcpClient.new(addr.ip, addr.port) let connection = try listener.accept let bytes = ByteArray.new @@ -681,13 +685,13 @@ fn pub tests(t: mut Tests) { }) t.ok('TcpClient.write_string', fn (t) { - let listener = try Socket.ipv4(Type.STREAM) + let listener = try Socket.stream(ipv6: false) try listener.bind(ip: IpAddress.v4(127, 0, 0, 1), port: 0) try listener.listen let addr = try listener.local_address - let stream = try TcpClient.new(addr.ip.get, addr.port) + let stream = try TcpClient.new(addr.ip, addr.port) let connection = try listener.accept let bytes = ByteArray.new @@ -698,25 +702,25 @@ fn pub tests(t: mut Tests) { }) t.ok('TcpClient.flush', fn (t) { - let listener = try Socket.ipv4(Type.STREAM) + let listener = try Socket.stream(ipv6: false) try listener.bind(ip: IpAddress.v4(127, 0, 0, 1), port: 0) try listener.listen let addr = try listener.local_address - let stream = try TcpClient.new(addr.ip.get, addr.port) + let stream = try TcpClient.new(addr.ip, addr.port) stream.flush }) t.ok('TcpClient.shutdown_read', fn (t) { - let listener = try Socket.ipv4(Type.STREAM) + let listener = try Socket.stream(ipv6: false) try listener.bind(ip: IpAddress.v4(127, 0, 0, 1), port: 0) try listener.listen let addr = try listener.local_address - let stream = try TcpClient.new(addr.ip.get, addr.port) + let stream = try TcpClient.new(addr.ip, addr.port) t.equal(stream.shutdown_read, Result.Ok(nil)) @@ -728,13 +732,13 @@ fn pub tests(t: mut Tests) { }) t.ok('TcpClient.shutdown_write', fn (t) { - let listener = try Socket.ipv4(Type.STREAM) + let listener = try Socket.stream(ipv6: false) try listener.bind(ip: IpAddress.v4(127, 0, 0, 1), port: 0) try listener.listen let addr = try listener.local_address - let stream = try TcpClient.new(addr.ip.get, addr.port) + let stream = try TcpClient.new(addr.ip, addr.port) t.equal(stream.shutdown_write, Result.Ok(nil)) t.true(stream.write_string('ping').error?) @@ -742,13 +746,13 @@ fn pub tests(t: mut Tests) { }) t.ok('TcpClient.shutdown shuts down the writing half', fn (t) { - let listener = try Socket.ipv4(Type.STREAM) + let listener = try Socket.stream(ipv6: false) try listener.bind(ip: IpAddress.v4(127, 0, 0, 1), port: 0) try listener.listen let addr = try listener.local_address - let stream = try TcpClient.new(addr.ip.get, addr.port) + let stream = try TcpClient.new(addr.ip, addr.port) t.equal(stream.shutdown, Result.Ok(nil)) t.true(stream.write_string('ping').error?) @@ -756,13 +760,13 @@ fn pub tests(t: mut Tests) { }) t.ok('TcpClient.shutdown shuts down the reading half', fn (t) { - let listener = try Socket.ipv4(Type.STREAM) + let listener = try Socket.stream(ipv6: false) try listener.bind(ip: IpAddress.v4(127, 0, 0, 1), port: 0) try listener.listen let addr = try listener.local_address - let stream = try TcpClient.new(addr.ip.get, addr.port) + let stream = try TcpClient.new(addr.ip, addr.port) t.equal(stream.shutdown, Result.Ok(nil)) @@ -774,13 +778,13 @@ fn pub tests(t: mut Tests) { }) t.ok('TcpClient.try_clone', fn (t) { - let listener = Socket.ipv4(Type.STREAM).get + let listener = Socket.stream(ipv6: false).get try listener.bind(ip: IpAddress.v4(127, 0, 0, 1), port: 0) try listener.listen let addr = try listener.local_address - let client = try TcpClient.new(addr.ip.get, addr.port) + let client = try TcpClient.new(addr.ip, addr.port) let clone = try client.try_clone t.true(clone.socket.no_delay?) @@ -795,14 +799,14 @@ fn pub tests(t: mut Tests) { let listener = TcpServer.new(ip: ip, port: 0).get let addr = listener.local_address.get - t.true(TcpServer.new(addr.ip.get, addr.port).ok?) + t.true(TcpServer.new(addr.ip, addr.port).ok?) }) t.ok('TcpServer.accept', fn (t) { let ip = IpAddress.V4(Ipv4Address.new(127, 0, 0, 1)) let listener = try TcpServer.new(ip: ip, port: 0) let addr = try listener.local_address - let stream = try TcpClient.new(addr.ip.get, addr.port) + let stream = try TcpClient.new(addr.ip, addr.port) let connection = try listener.accept t.equal(connection.local_address, stream.peer_address) @@ -815,7 +819,7 @@ fn pub tests(t: mut Tests) { let listener = TcpServer.new(ip: ip, port: 0).get let addr = listener.local_address.get - t.equal(addr.address, '127.0.0.1') + t.equal(addr.ip, IpAddress.v4(127, 0, 0, 1)) t.true(addr.port > 0) }) @@ -830,78 +834,69 @@ fn pub tests(t: mut Tests) { }) t.test('UnixAddress.to_path', fn (t) { - t.equal( - UnixAddress.new('foo.sock'.to_path).to_path, - Option.Some('foo.sock'.to_path), - ) - t.true(UnixAddress.new('\0foo'.to_path).to_path.none?) - t.true(UnixAddress.new(''.to_path).to_path.none?) + t.equal(UnixAddress('foo.sock').to_path, Option.Some('foo.sock'.to_path)) + t.true(UnixAddress('\0foo').to_path.none?) + t.true(UnixAddress('').to_path.none?) }) t.test('UnixAddress.to_string', fn (t) { - t.equal(UnixAddress.new('foo.sock'.to_path).to_string, 'foo.sock') - t.equal(UnixAddress.new('\0foo'.to_path).to_string, '\0foo') - t.equal(UnixAddress.new(''.to_path).to_string, '') + t.equal(UnixAddress('foo.sock').to_string, 'foo.sock') + t.equal(UnixAddress('\0foo').to_string, '\0foo') + t.equal(UnixAddress('').to_string, '') }) t.test('UnixAddress.abstract?', fn (t) { - t.false(UnixAddress.new(''.to_path).abstract?) - t.false(UnixAddress.new('foo.sock'.to_path).abstract?) - t.true(UnixAddress.new('\0foo'.to_path).abstract?) + t.false(UnixAddress('').abstract?) + t.false(UnixAddress('foo.sock').abstract?) + t.true(UnixAddress('\0foo').abstract?) }) t.test('UnixAddress.unnamed?', fn (t) { - t.false(UnixAddress.new('foo.sock'.to_path).unnamed?) - t.false(UnixAddress.new('\0foo'.to_path).unnamed?) - t.true(UnixAddress.new(''.to_path).unnamed?) + t.false(UnixAddress('foo.sock').unnamed?) + t.false(UnixAddress('\0foo').unnamed?) + t.true(UnixAddress('').unnamed?) }) t.test('UnixAddress.fmt', fn (t) { - t.equal(fmt(UnixAddress.new('foo.sock'.to_path)), 'foo.sock') - t.equal(fmt(UnixAddress.new('\0foo'.to_path)), '@foo') - t.equal(fmt(UnixAddress.new(''.to_path)), 'unnamed') + t.equal(fmt(UnixAddress('foo.sock')), 'foo.sock') + t.equal(fmt(UnixAddress('\0foo')), '@foo') + t.equal(fmt(UnixAddress('')), 'unnamed') }) t.test('UnixAddress.==', fn (t) { - t.equal( - UnixAddress.new('a.sock'.to_path), - UnixAddress.new('a.sock'.to_path), - ) - t.not_equal( - UnixAddress.new('a.sock'.to_path), - UnixAddress.new('b.sock'.to_path), - ) + t.equal(UnixAddress('a.sock'), UnixAddress('a.sock')) + t.not_equal(UnixAddress('a.sock'), UnixAddress('b.sock')) }) t.test('UnixAddress.to_string', fn (t) { - t.equal(UnixAddress.new('foo.sock'.to_path).to_string, 'foo.sock') - t.equal(UnixAddress.new('\0foo'.to_path).to_string, '\0foo') - t.equal(UnixAddress.new(''.to_path).to_string, '') + t.equal(UnixAddress('foo.sock').to_string, 'foo.sock') + t.equal(UnixAddress('\0foo').to_string, '\0foo') + t.equal(UnixAddress('').to_string, '') }) t.test('UnixSocket.new', fn (t) { - t.true(UnixSocket.new(Type.DGRAM).ok?) - t.true(UnixSocket.new(Type.STREAM).ok?) + t.true(UnixSocket.datagram.ok?) + t.true(UnixSocket.stream.ok?) }) t.test('UnixSocket.bind', fn (t) { - let socket1 = UnixSocket.new(Type.STREAM).get - let socket2 = UnixSocket.new(Type.STREAM).get + let socket1 = UnixSocket.stream.get + let socket2 = UnixSocket.stream.get let path = SocketPath.new(t.id) t.true(socket1.bind(path.path).ok?) t.true(socket2.bind(path.path).error?) if env.OS == 'linux' { - let socket = UnixSocket.new(Type.STREAM).get + let socket = UnixSocket.stream.get t.true(socket.bind('\0inko-test-${t.id}'.to_path).ok?) } }) t.test('UnixSocket.connect', fn (t) { - let listener = UnixSocket.new(Type.STREAM).get - let stream = UnixSocket.new(Type.STREAM).get + let listener = UnixSocket.stream.get + let stream = UnixSocket.stream.get let path = SocketPath.new(t.id) t.true(stream.connect(path.path).error?) @@ -912,8 +907,8 @@ fn pub tests(t: mut Tests) { t.true(stream.connect(path.path).ok?) if env.OS == 'linux' { - let listener = UnixSocket.new(Type.STREAM).get - let stream = UnixSocket.new(Type.STREAM).get + let listener = UnixSocket.stream.get + let stream = UnixSocket.stream.get let addr = '\0inko-test-${t.id}' listener.bind(addr.to_path).get @@ -925,7 +920,7 @@ fn pub tests(t: mut Tests) { t.test('UnixSocket.listen', fn (t) { let path = SocketPath.new(t.id) - let socket = UnixSocket.new(Type.STREAM).get + let socket = UnixSocket.stream.get socket.bind(path.path).get @@ -934,8 +929,8 @@ fn pub tests(t: mut Tests) { t.test('UnixSocket.accept', fn (t) { let path = SocketPath.new(t.id) - let listener = UnixSocket.new(Type.STREAM).get - let stream = UnixSocket.new(Type.STREAM).get + let listener = UnixSocket.stream.get + let stream = UnixSocket.stream.get listener.bind(path.path).get listener.listen.get @@ -948,7 +943,7 @@ fn pub tests(t: mut Tests) { t.test('UnixSocket.send_string_to', fn (t) { let path = SocketPath.new(t.id) - let socket = UnixSocket.new(Type.DGRAM).get + let socket = UnixSocket.datagram.get t.equal(socket.bind(path.path), Result.Ok(nil)) t.equal(socket.send_string_to('ping', path.path), Result.Ok(4)) @@ -961,7 +956,7 @@ fn pub tests(t: mut Tests) { t.test('UnixSocket.send_bytes_to', fn (t) { let path = SocketPath.new(t.id) - let socket = UnixSocket.new(Type.DGRAM).get + let socket = UnixSocket.datagram.get t.equal(socket.bind(path.path), Result.Ok(nil)) t.equal(socket.send_bytes_to('ping'.to_byte_array, path.path), Result.Ok(4)) @@ -974,8 +969,8 @@ fn pub tests(t: mut Tests) { t.test('UnixSocket.receive_from', fn (t) { let pair = SocketPath.pair(t.id) - let listener = UnixSocket.new(Type.DGRAM).get - let client = UnixSocket.new(Type.DGRAM).get + let listener = UnixSocket.datagram.get + let client = UnixSocket.datagram.get t.equal(listener.bind(pair.0.path), Result.Ok(nil)) t.equal(client.bind(pair.1.path), Result.Ok(nil)) @@ -988,31 +983,31 @@ fn pub tests(t: mut Tests) { }) t.test('UnixSocket.local_address with an unbound socket', fn (t) { - let socket = UnixSocket.new(Type.DGRAM).get + let socket = UnixSocket.datagram.get let address = socket.local_address.get - t.equal(address, UnixAddress.new(''.to_path)) + t.equal(address, UnixAddress('')) }) t.test('UnixSocket.local_address with a bound socket', fn (t) { let path = SocketPath.new(t.id) - let socket = UnixSocket.new(Type.DGRAM).get + let socket = UnixSocket.datagram.get socket.bind(path.path).get - t.equal(socket.local_address, Result.Ok(UnixAddress.new(path.path))) + t.equal(socket.local_address, Result.Ok(UnixAddress(path.to_string))) }) t.test('UnixSocket.peer_address with a disconnected socket', fn (t) { - let socket = UnixSocket.new(Type.DGRAM).get + let socket = UnixSocket.datagram.get t.true(socket.peer_address.error?) }) t.test('UnixSocket.peer_address with a connected socket', fn (t) { let path = SocketPath.new(t.id) - let listener = UnixSocket.new(Type.STREAM).get - let client = UnixSocket.new(Type.STREAM).get + let listener = UnixSocket.stream.get + let client = UnixSocket.stream.get listener.bind(path.path).get listener.listen.get @@ -1023,7 +1018,7 @@ fn pub tests(t: mut Tests) { t.test('UnixSocket.read', fn (t) { let path = SocketPath.new(t.id) - let socket = UnixSocket.new(Type.DGRAM).get + let socket = UnixSocket.datagram.get t.equal(socket.bind(path.path), Result.Ok(nil)) t.equal(socket.send_string_to('ping', path.path), Result.Ok(4)) @@ -1036,8 +1031,8 @@ fn pub tests(t: mut Tests) { t.test('UnixSocket.write_bytes', fn (t) { let path = SocketPath.new(t.id) - let listener = UnixSocket.new(Type.STREAM).get - let stream = UnixSocket.new(Type.STREAM).get + let listener = UnixSocket.stream.get + let stream = UnixSocket.stream.get listener.bind(path.path).get listener.listen.get @@ -1053,8 +1048,8 @@ fn pub tests(t: mut Tests) { t.test('UnixSocket.write_string', fn (t) { let path = SocketPath.new(t.id) - let listener = UnixSocket.new(Type.STREAM).get - let stream = UnixSocket.new(Type.STREAM).get + let listener = UnixSocket.stream.get + let stream = UnixSocket.stream.get listener.bind(path.path).get listener.listen.get @@ -1069,27 +1064,27 @@ fn pub tests(t: mut Tests) { }) t.test('UnixSocket.flush', fn (t) { - let socket = UnixSocket.new(Type.STREAM).get + let socket = UnixSocket.stream.get t.equal(socket.flush, Result.Ok(nil)) }) t.test('UnixSocket.receive_buffer_size', fn (t) { - let socket = UnixSocket.new(Type.STREAM).get + let socket = UnixSocket.stream.get t.true((socket.receive_buffer_size = 256).ok?) }) t.test('UnixSocket.send_buffer_size', fn (t) { - let socket = UnixSocket.new(Type.STREAM).get + let socket = UnixSocket.stream.get t.true((socket.send_buffer_size = 256).ok?) }) t.test('UnixSocket.shutdown_read', fn (t) { let path = SocketPath.new(t.id) - let listener = UnixSocket.new(Type.STREAM).get - let stream = UnixSocket.new(Type.STREAM).get + let listener = UnixSocket.stream.get + let stream = UnixSocket.stream.get listener.bind(path.path).get listener.listen.get @@ -1104,8 +1099,8 @@ fn pub tests(t: mut Tests) { t.test('UnixSocket.shutdown_write', fn (t) { let path = SocketPath.new(t.id) - let listener = UnixSocket.new(Type.STREAM).get - let stream = UnixSocket.new(Type.STREAM).get + let listener = UnixSocket.stream.get + let stream = UnixSocket.stream.get listener.bind(path.path).get listener.listen.get @@ -1117,8 +1112,8 @@ fn pub tests(t: mut Tests) { t.test('UnixSocket.shutdown shuts down the writing half', fn (t) { let path = SocketPath.new(t.id) - let listener = UnixSocket.new(Type.STREAM).get - let stream = UnixSocket.new(Type.STREAM).get + let listener = UnixSocket.stream.get + let stream = UnixSocket.stream.get listener.bind(path.path).get listener.listen.get @@ -1130,8 +1125,8 @@ fn pub tests(t: mut Tests) { t.test('UnixSocket.shutdown shuts down the reading half', fn (t) { let path = SocketPath.new(t.id) - let listener = UnixSocket.new(Type.STREAM).get - let stream = UnixSocket.new(Type.STREAM).get + let listener = UnixSocket.stream.get + let stream = UnixSocket.stream.get listener.bind(path.path).get listener.listen.get @@ -1145,14 +1140,14 @@ fn pub tests(t: mut Tests) { }) t.test('UnixSocket.try_clone', fn (t) { - let socket = UnixSocket.new(Type.STREAM).get + let socket = UnixSocket.stream.get t.true(socket.try_clone.ok?) }) t.test('UnixSocket.timeout_after=', fn (t) { let path = SocketPath.new(t.id) - let server = UnixSocket.new(Type.STREAM).get + let server = UnixSocket.stream.get server.bind(path.path).get server.listen @@ -1162,7 +1157,7 @@ fn pub tests(t: mut Tests) { }) t.test('UnixSocket.reset_deadline', fn (t) { - let socket = UnixSocket.new(Type.STREAM).get + let socket = UnixSocket.stream.get socket.timeout_after = Duration.from_secs(10) t.true(socket.deadline > 0) @@ -1215,7 +1210,7 @@ fn pub tests(t: mut Tests) { let path = SocketPath.new(t.id) let socket = UnixDatagram.new(path.path).get - t.equal(socket.local_address, Result.Ok(UnixAddress.new(path.path))) + t.equal(socket.local_address, Result.Ok(UnixAddress(path.to_string))) }) t.test('UnixDatagram.try_clone', fn (t) { @@ -1260,7 +1255,7 @@ fn pub tests(t: mut Tests) { t.test('UnixClient.new', fn (t) { let path = SocketPath.new(t.id) - let listener = UnixSocket.new(Type.STREAM).get + let listener = UnixSocket.stream.get listener.bind(path.path).get listener.listen.get @@ -1270,7 +1265,7 @@ fn pub tests(t: mut Tests) { t.ok('UnixClient.with_timeout', fn (t) { let path = SocketPath.new(t.id) - let listener = UnixSocket.new(Type.STREAM).get + let listener = UnixSocket.stream.get listener.bind(path.path).get listener.listen.get @@ -1286,33 +1281,33 @@ fn pub tests(t: mut Tests) { t.ok('UnixClient.local_address', fn (t) { let path = SocketPath.new(t.id) - let listener = UnixSocket.new(Type.STREAM).get + let listener = UnixSocket.stream.get try listener.bind(path.path) try listener.listen let stream = try UnixClient.new(path.path) - t.equal(stream.local_address, Result.Ok(UnixAddress.new(''.to_path))) + t.equal(stream.local_address, Result.Ok(UnixAddress(''))) Result.Ok(nil) }) t.ok('UnixClient.peer_address', fn (t) { let path = SocketPath.new(t.id) - let listener = UnixSocket.new(Type.STREAM).get + let listener = UnixSocket.stream.get try listener.bind(path.path) try listener.listen let stream = try UnixClient.new(path.path) - t.equal(stream.peer_address, Result.Ok(UnixAddress.new(path.path))) + t.equal(stream.peer_address, Result.Ok(UnixAddress(path.to_string))) Result.Ok(nil) }) t.ok('UnixClient.read', fn (t) { let path = SocketPath.new(t.id) - let listener = UnixSocket.new(Type.STREAM).get + let listener = UnixSocket.stream.get try listener.bind(path.path) try listener.listen @@ -1331,7 +1326,7 @@ fn pub tests(t: mut Tests) { t.ok('UnixClient.write_bytes', fn (t) { let path = SocketPath.new(t.id) - let listener = UnixSocket.new(Type.STREAM).get + let listener = UnixSocket.stream.get try listener.bind(path.path) try listener.listen @@ -1348,7 +1343,7 @@ fn pub tests(t: mut Tests) { t.ok('UnixClient.write_string', fn (t) { let path = SocketPath.new(t.id) - let listener = UnixSocket.new(Type.STREAM).get + let listener = UnixSocket.stream.get try listener.bind(path.path) try listener.listen @@ -1365,7 +1360,7 @@ fn pub tests(t: mut Tests) { t.ok('UnixClient.flush', fn (t) { let path = SocketPath.new(t.id) - let listener = UnixSocket.new(Type.STREAM).get + let listener = UnixSocket.stream.get try listener.bind(path.path) try listener.listen @@ -1375,7 +1370,7 @@ fn pub tests(t: mut Tests) { t.ok('UnixClient.shutdown_read', fn (t) { let path = SocketPath.new(t.id) - let listener = UnixSocket.new(Type.STREAM).get + let listener = UnixSocket.stream.get try listener.bind(path.path) try listener.listen @@ -1393,7 +1388,7 @@ fn pub tests(t: mut Tests) { t.ok('UnixClient.shutdown_write', fn (t) { let path = SocketPath.new(t.id) - let listener = UnixSocket.new(Type.STREAM).get + let listener = UnixSocket.stream.get try listener.bind(path.path) try listener.listen @@ -1407,7 +1402,7 @@ fn pub tests(t: mut Tests) { t.ok('UnixClient.shutdown shuts down the writing half', fn (t) { let path = SocketPath.new(t.id) - let listener = UnixSocket.new(Type.STREAM).get + let listener = UnixSocket.stream.get try listener.bind(path.path) try listener.listen @@ -1421,7 +1416,7 @@ fn pub tests(t: mut Tests) { t.ok('UnixClient.shutdown shuts down the reading half', fn (t) { let path = SocketPath.new(t.id) - let listener = UnixSocket.new(Type.STREAM).get + let listener = UnixSocket.stream.get try listener.bind(path.path) try listener.listen @@ -1439,7 +1434,7 @@ fn pub tests(t: mut Tests) { t.ok('UnixClient.try_clone', fn (t) { let path = SocketPath.new(t.id) - let listener = UnixSocket.new(Type.STREAM).get + let listener = UnixSocket.stream.get try listener.bind(path.path) try listener.listen @@ -1468,7 +1463,7 @@ fn pub tests(t: mut Tests) { let listener = UnixServer.new(path.path).get let addr = listener.local_address.get - t.equal(addr, UnixAddress.new(path.path)) + t.equal(addr, UnixAddress(path.to_string)) }) t.test('UnixServer.try_clone', fn (t) { diff --git a/std/test/std/net/test_tls.inko b/std/test/std/net/test_tls.inko new file mode 100644 index 000000000..bc6f7ef0f --- /dev/null +++ b/std/test/std/net/test_tls.inko @@ -0,0 +1,356 @@ +import std.env +import std.fmt (fmt) +import std.fs.path (Path) +import std.io (Error) +import std.net.ip (IpAddress) +import std.net.socket (Socket, TcpClient, TcpServer) +import std.net.tls ( + Client, ClientConfig, ClientConfigError, Server, ServerConfig, + ServerConfigError, +) +import std.test (Tests) + +class async HelloServer { + let @socket: TcpServer + let @config: ServerConfig + + fn static new -> Result[(HelloServer, Int), String] { + let conf = recover { + try ServerConfig + .new(certificate: fixture('test.pem'), key: fixture('test.key')) + .map_error(fn (e) { 'failed to create the server config: ${e}' }) + } + + # We bind to port zero so we get a random port number, such that we don't + # accidentally pick one that's already in use. + let sock = recover { + try TcpServer.new(ip: IpAddress.v4(0, 0, 0, 0), port: 0).map_error( + fn (e) { 'failed to start the server: ${e}' }, + ) + } + let addr = try sock.local_address.map_error(fn (e) { + 'failed to get the server address: ${e}' + }) + + Result.Ok((HelloServer(sock, conf), addr.port)) + } + + fn async hello { + # We may encounter an error such as when the client closes the connection. + # We want to ignore those so we don't terminate the test suite. + let _ = @socket.accept.then(fn (sock) { + Server.new(sock, @config).write_string('hello') + }) + } + + fn async reply { + # We may encounter an error such as when the client closes the connection. + # We want to ignore those so we don't terminate the test suite. + let _ = @socket.accept.then(fn (sock) { + let con = Server.new(sock, @config) + let bytes = ByteArray.new + + try con.read(into: bytes, size: 32) + con.write_bytes(bytes) + }) + } +} + +fn connect(port: Int) -> Result[TcpClient, String] { + TcpClient.new(IpAddress.v4(127, 0, 0, 1), port).map_error(fn (e) { + 'failed to connect to the server: ${e}' + }) +} + +fn fixture(name: String) -> Path { + env + .working_directory + .or_else(fn (_) { '.'.to_path }) + .join('fixtures') + .join('tls') + .join(name) +} + +fn client_config -> Result[ClientConfig, String] { + ClientConfig.with_certificate(fixture('test.pem')).map_error(fn (e) { + e.to_string + }) +} + +fn dummy_socket -> Result[Socket, String] { + Socket.stream(ipv6: false).map_error(fn (e) { e.to_string }) +} + +fn pub tests(t: mut Tests) { + t.test('ClientConfigError.to_string', fn (t) { + t.true( + ClientConfigError.Read(Error.BrokenPipe).to_string.contains?( + Error.BrokenPipe.to_string, + ), + ) + }) + + t.test('ClientConfigError.fmt', fn (t) { + t.equal(fmt(ClientConfigError.Read(Error.BrokenPipe)), 'Read(BrokenPipe)') + t.equal(fmt(ClientConfigError.InvalidCertificate), 'InvalidCertificate') + }) + + t.test('ClientConfigError.==', fn (t) { + t.equal( + ClientConfigError.Read(Error.BrokenPipe), + ClientConfigError.Read(Error.BrokenPipe), + ) + t.equal( + ClientConfigError.InvalidCertificate, + ClientConfigError.InvalidCertificate, + ) + t.not_equal( + ClientConfigError.Read(Error.BrokenPipe), + ClientConfigError.Read(Error.Other(42)), + ) + t.not_equal( + ClientConfigError.Read(Error.BrokenPipe), + ClientConfigError.InvalidCertificate, + ) + }) + + t.test('ServerConfigError.to_string', fn (t) { + t.true( + ServerConfigError.Read(Error.BrokenPipe).to_string.contains?( + Error.BrokenPipe.to_string, + ), + ) + }) + + t.test('ServerConfigError.fmt', fn (t) { + t.equal(fmt(ServerConfigError.Read(Error.BrokenPipe)), 'Read(BrokenPipe)') + t.equal(fmt(ServerConfigError.InvalidCertificate), 'InvalidCertificate') + t.equal(fmt(ServerConfigError.InvalidPrivateKey), 'InvalidPrivateKey') + }) + + t.test('ServerConfigError.==', fn (t) { + t.equal( + ServerConfigError.Read(Error.BrokenPipe), + ServerConfigError.Read(Error.BrokenPipe), + ) + t.equal( + ServerConfigError.InvalidCertificate, + ServerConfigError.InvalidCertificate, + ) + t.equal( + ServerConfigError.InvalidPrivateKey, + ServerConfigError.InvalidPrivateKey, + ) + t.not_equal( + ServerConfigError.Read(Error.BrokenPipe), + ServerConfigError.Read(Error.Other(42)), + ) + t.not_equal( + ServerConfigError.Read(Error.BrokenPipe), + ServerConfigError.InvalidCertificate, + ) + t.not_equal( + ServerConfigError.Read(Error.BrokenPipe), + ServerConfigError.InvalidPrivateKey, + ) + }) + + t.test('ClientConfig.with_certificate with a valid certificate', fn (t) { + t.true(ClientConfig.with_certificate(fixture('test.pem')).ok?) + }) + + t.test('ClientConfig.with_certificate with an invalid certificate', fn (t) { + t.equal( + ClientConfig.with_certificate(fixture('empty.pem')).error, + Option.Some(ClientConfigError.InvalidCertificate), + ) + t.equal( + ClientConfig.with_certificate(fixture('invalid.pem')).error, + Option.Some(ClientConfigError.InvalidCertificate), + ) + t.equal( + ClientConfig.with_certificate(fixture('missing.pem')).error, + Option.Some(ClientConfigError.Read(Error.NotFound)), + ) + }) + + t.ok('ClientConfig.clone', fn (t) { + let a = try ClientConfig.with_certificate(fixture('test.pem')) + let b = a.clone + + t.equal(a.raw as Int, b.raw as Int) + Result.Ok(nil) + }) + + t.test('ServerConfig.new with a valid certificate and private key', fn (t) { + let cert = fixture('test.pem') + let key = fixture('test.key') + + t.true(ServerConfig.new(cert, key).ok?) + }) + + t.test('ServerConfig with an invalid certificate', fn (t) { + let key = fixture('test.key') + + t.equal( + ServerConfig.new(fixture('empty.pem'), key).error, + Option.Some(ServerConfigError.InvalidCertificate), + ) + t.equal( + ServerConfig.new(fixture('invalid.pem'), key).error, + Option.Some(ServerConfigError.InvalidCertificate), + ) + t.equal( + ServerConfig.new(fixture('missing.pem'), key).error, + Option.Some(ServerConfigError.Read(Error.NotFound)), + ) + }) + + t.test('ServerConfig with an invalid private key', fn (t) { + let cert = fixture('test.pem') + + t.equal( + ServerConfig.new(cert, fixture('empty.key')).error, + Option.Some(ServerConfigError.InvalidPrivateKey), + ) + t.equal( + ServerConfig.new(cert, fixture('invalid.key')).error, + Option.Some(ServerConfigError.InvalidPrivateKey), + ) + t.equal( + ServerConfig.new(cert, fixture('missing.key')).error, + Option.Some(ServerConfigError.Read(Error.NotFound)), + ) + }) + + t.ok('ServerConfig.clone', fn (t) { + let a = try ServerConfig.new(fixture('test.pem'), fixture('test.key')) + let b = a.clone + + t.equal(b.raw as Int, a.raw as Int) + Result.Ok(nil) + }) + + t.ok('Client.new with a valid DNS name as the server name', fn (t) { + let conf = try client_config + let sock = try dummy_socket + + t.true(Client.new(sock, conf, name: 'localhost').some?) + Result.Ok(nil) + }) + + t.ok('Client.new with a valid IP address as the server name', fn (t) { + let conf = try client_config + let sock = try dummy_socket + + t.true(Client.new(sock, conf, name: '127.0.0.1').some?) + Result.Ok(nil) + }) + + t.ok('Client.new with an invalid DNS name as the server name', fn (t) { + let conf = try client_config + let sock = try dummy_socket + + t.true(Client.new(sock, conf, name: 'what?!').none?) + Result.Ok(nil) + }) + + t.ok('Client.new with an invalid IP address as the server name', fn (t) { + let conf = try client_config + let sock = try dummy_socket + + t.true(Client.new(sock, conf, name: '1.2.3.4.5').none?) + Result.Ok(nil) + }) + + t.ok('Client.read', fn (t) { + let port = match HelloServer.new { + case Ok((server, port)) -> { + server.hello + port + } + case Error(e) -> throw e + } + + let conf = try client_config + let sock = try connect(port) + let client = Client.new(sock, conf, name: 'localhost').get + let bytes = ByteArray.new + + t.equal(client.read(into: bytes, size: 5), Result.Ok(5)) + t.equal(bytes.into_string, 'hello') + Result.Ok(nil) + }) + + t.ok('Client.write_bytes', fn (t) { + let port = match HelloServer.new { + case Ok((server, port)) -> { + server.reply + port + } + case Error(e) -> throw e + } + + let conf = try client_config + let sock = try connect(port) + let client = Client.new(sock, conf, name: 'localhost').get + let bytes = ByteArray.new + + t.equal(client.write_bytes('ping'.to_byte_array), Result.Ok(nil)) + t.equal(client.read(into: bytes, size: 4), Result.Ok(4)) + t.equal(bytes.into_string, 'ping') + Result.Ok(nil) + }) + + t.ok('Client.write_string', fn (t) { + let port = match HelloServer.new { + case Ok((server, port)) -> { + server.reply + port + } + case Error(e) -> throw e + } + + let conf = try client_config + let sock = try connect(port) + let client = Client.new(sock, conf, name: 'localhost').get + let bytes = ByteArray.new + + t.equal(client.write_string('ping'), Result.Ok(nil)) + t.equal(client.read(into: bytes, size: 4), Result.Ok(4)) + t.equal(bytes.into_string, 'ping') + Result.Ok(nil) + }) + + t.ok('Client.close', fn (t) { + let port = match HelloServer.new { + case Ok((server, port)) -> { + server.reply + port + } + case Error(e) -> throw e + } + + let conf = try client_config + let sock = try connect(port) + let client = Client.new(sock, conf, name: 'localhost').get + let bytes = ByteArray.new + + t.equal(client.close, Result.Ok(nil)) + + # The exact errors reported here are timing sensitive. + t.true(client.write_string('ping').error?) + t.true(client.read(into: bytes, size: 4).error?) + t.true(bytes.empty?) + Result.Ok(nil) + }) + + t.ok('Client.flush', fn (t) { + let conf = try client_config + let sock = try dummy_socket + let client = Client.new(sock, conf, name: 'localhost').get + + t.equal(client.flush, Result.Ok(nil)) + Result.Ok(nil) + }) +} diff --git a/std/test/std/test_env.inko b/std/test/std/test_env.inko index f704802b0..0afadfb3e 100644 --- a/std/test/std/test_env.inko +++ b/std/test/std/test_env.inko @@ -44,6 +44,20 @@ fn pub tests(t: mut Tests) { } }) + t.fork( + 'env.home_directory with a missing home directory', + child: fn { + let out = STDOUT.new + let res = env.home_directory.map(fn (v) { v.to_string }).or('ERROR') + + out.write_string(res) + }, + test: fn (test, proc) { + proc.variable('HOME', '') + test.equal(proc.spawn.stdout, 'ERROR') + }, + ) + t.test('env.working_directory', fn (t) { let path = env.working_directory.get diff --git a/std/test/std/test_io.inko b/std/test/std/test_io.inko index 0612d8228..35183558c 100644 --- a/std/test/std/test_io.inko +++ b/std/test/std/test_io.inko @@ -1,5 +1,8 @@ import std.fmt (fmt) -import std.io (Buffer, BufferedReader, DEFAULT_BUFFER_SIZE, Error, Read, Write) +import std.io ( + Buffer, BufferedReader, DEFAULT_BUFFER_SIZE, Error, INVALID_DATA, Read, + UNEXPECTED_EOF, Write, +) import std.libc.bsd.errors if bsd import std.libc.linux.errors if linux import std.libc.mac.errors if mac @@ -123,6 +126,8 @@ fn pub tests(t: mut Tests) { t.equal(Error.from_os_error(errors.ECONNREFUSED), Error.ConnectionRefused) t.equal(Error.from_os_error(errors.EHOSTUNREACH), Error.HostUnreachable) t.equal(Error.from_os_error(errors.EINPROGRESS), Error.InProgress) + t.equal(Error.from_os_error(INVALID_DATA), Error.InvalidData) + t.equal(Error.from_os_error(UNEXPECTED_EOF), Error.EndOfInput) t.equal(Error.from_os_error(999), Error.Other(999)) }) @@ -157,6 +162,8 @@ fn pub tests(t: mut Tests) { t.equal(fmt(Error.StorageFull), 'StorageFull') t.equal(fmt(Error.TimedOut), 'TimedOut') t.equal(fmt(Error.WouldBlock), 'WouldBlock') + t.equal(fmt(Error.InvalidData), 'InvalidData') + t.equal(fmt(Error.EndOfInput), 'EndOfInput') t.equal(fmt(Error.Other(999)), 'Other(999)') }) diff --git a/std/test/std/test_optparse.inko b/std/test/std/test_optparse.inko index d918a35f5..2bdafd9f1 100644 --- a/std/test/std/test_optparse.inko +++ b/std/test/std/test_optparse.inko @@ -535,7 +535,8 @@ fn pub tests(t: mut Tests) { lorem ipsum --example -x Foo' - .strip_prefix('\n'), + .strip_prefix('\n') + .get, ) opts.single('o', 'option-with-a-much-longer-name', '', 'Example') @@ -552,7 +553,8 @@ fn pub tests(t: mut Tests) { --example -x Foo -o, --option-with-a-much-longer-name Example' - .strip_prefix('\n'), + .strip_prefix('\n') + .get, ) }) diff --git a/std/test/std/test_string.inko b/std/test/std/test_string.inko index a83446645..9483547ef 100644 --- a/std/test/std/test_string.inko +++ b/std/test/std/test_string.inko @@ -258,27 +258,27 @@ fn pub tests(t: mut Tests) { }) t.test('String.strip_prefix', fn (t) { - t.equal('hello'.strip_prefix('xxxxxxxxx'), 'hello') - t.equal('hello'.strip_prefix('x'), 'hello') - t.equal('hello'.strip_prefix(''), 'hello') - t.equal('XhelloX'.strip_prefix('x'), 'XhelloX') - t.equal('xhellox'.strip_prefix('xy'), 'xhellox') - t.equal('xhellox'.strip_prefix('y'), 'xhellox') - t.equal('xhellox'.strip_prefix('x'), 'hellox') - t.equal('xxhelloxx'.strip_prefix('xx'), 'helloxx') - t.equal('๐Ÿ˜ƒhello๐Ÿ˜ƒ'.strip_prefix('๐Ÿ˜ƒ'), 'hello๐Ÿ˜ƒ') + t.equal('hello'.strip_prefix('xxxxxxxxx'), Option.None) + t.equal('hello'.strip_prefix('x'), Option.None) + t.equal('hello'.strip_prefix(''), Option.None) + t.equal('XhelloX'.strip_prefix('x'), Option.None) + t.equal('xhellox'.strip_prefix('xy'), Option.None) + t.equal('xhellox'.strip_prefix('y'), Option.None) + t.equal('xhellox'.strip_prefix('x'), Option.Some('hellox')) + t.equal('xxhelloxx'.strip_prefix('xx'), Option.Some('helloxx')) + t.equal('๐Ÿ˜ƒhello๐Ÿ˜ƒ'.strip_prefix('๐Ÿ˜ƒ'), Option.Some('hello๐Ÿ˜ƒ')) }) t.test('String.strip_suffix', fn (t) { - t.equal('hello'.strip_suffix('xxxxxxxxx'), 'hello') - t.equal('hello'.strip_suffix('x'), 'hello') - t.equal('hello'.strip_suffix(''), 'hello') - t.equal('XhelloX'.strip_suffix('x'), 'XhelloX') - t.equal('xhellox'.strip_suffix('xy'), 'xhellox') - t.equal('xhellox'.strip_suffix('y'), 'xhellox') - t.equal('xhellox'.strip_suffix('x'), 'xhello') - t.equal('xxhelloxx'.strip_suffix('xx'), 'xxhello') - t.equal('๐Ÿ˜ƒhello๐Ÿ˜ƒ'.strip_suffix('๐Ÿ˜ƒ'), '๐Ÿ˜ƒhello') + t.equal('hello'.strip_suffix('xxxxxxxxx'), Option.None) + t.equal('hello'.strip_suffix('x'), Option.None) + t.equal('hello'.strip_suffix(''), Option.None) + t.equal('XhelloX'.strip_suffix('x'), Option.None) + t.equal('xhellox'.strip_suffix('xy'), Option.None) + t.equal('xhellox'.strip_suffix('y'), Option.None) + t.equal('xhellox'.strip_suffix('x'), Option.Some('xhello')) + t.equal('xxhelloxx'.strip_suffix('xx'), Option.Some('xxhello')) + t.equal('๐Ÿ˜ƒhello๐Ÿ˜ƒ'.strip_suffix('๐Ÿ˜ƒ'), Option.Some('๐Ÿ˜ƒhello')) }) t.test('String.trim_start', fn (t) {