From 3b8f6cf7b89cad4490142dcc7ff08abf4d735326 Mon Sep 17 00:00:00 2001 From: nazeh Date: Sun, 14 Jul 2024 11:35:28 +0300 Subject: [PATCH 01/13] feat(homeserver): basic Axum server with nothing but / router --- Cargo.lock | 1010 +++++++++++++++++++++++++++++++++ Cargo.toml | 2 +- homeserver/Cargo.toml | 12 + homeserver/src/lib.rs | 4 + homeserver/src/main.rs | 13 + homeserver/src/routes.rs | 7 + homeserver/src/routes/root.rs | 5 + homeserver/src/server.rs | 101 ++++ 8 files changed, 1153 insertions(+), 1 deletion(-) create mode 100644 Cargo.lock create mode 100644 homeserver/Cargo.toml create mode 100644 homeserver/src/lib.rs create mode 100644 homeserver/src/main.rs create mode 100644 homeserver/src/routes.rs create mode 100644 homeserver/src/routes/root.rs create mode 100644 homeserver/src/server.rs diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..3b0764a --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1010 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + +[[package]] +name = "async-trait" +version = "0.1.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "axum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 0.1.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "backtrace" +version = "0.3.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "bytes" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" + +[[package]] +name = "cc" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e2d530f35b40a84124146478cd16f34225306a8441998836466a2e2961c950" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab92f4f49ee4fb4f997c784b7a2e0fa70050211e0b6a287f898c3c9785ca956" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pubky_homeserver" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "tokio", + "tower-http", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.4", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.204" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.204" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "syn" +version = "2.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tokio" +version = "1.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags", + "bytes", + "http", + "http-body", + "http-body-util", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml index 4e61c7e..1423834 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = [] +members = ["homeserver"] # See: https://github.com/rust-lang/rust/issues/90148#issuecomment-949194352 resolver = "2" diff --git a/homeserver/Cargo.toml b/homeserver/Cargo.toml new file mode 100644 index 0000000..e4e2bf6 --- /dev/null +++ b/homeserver/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "pubky_homeserver" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1.0.82" +axum = "0.7.5" +tokio = { version = "1.37.0", features = ["full"] } +tower-http = { version = "0.5.2", features = ["cors", "trace"] } +tracing = "0.1.40" +tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } diff --git a/homeserver/src/lib.rs b/homeserver/src/lib.rs new file mode 100644 index 0000000..b4b9f85 --- /dev/null +++ b/homeserver/src/lib.rs @@ -0,0 +1,4 @@ +mod routes; +mod server; + +pub use server::Homeserver; diff --git a/homeserver/src/main.rs b/homeserver/src/main.rs new file mode 100644 index 0000000..ab284a8 --- /dev/null +++ b/homeserver/src/main.rs @@ -0,0 +1,13 @@ +use anyhow::Result; +use pubky_homeserver::Homeserver; + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt().init(); + + let server = Homeserver::start().await?; + + server.run_until_done().await?; + + Ok(()) +} diff --git a/homeserver/src/routes.rs b/homeserver/src/routes.rs new file mode 100644 index 0000000..74bc0ee --- /dev/null +++ b/homeserver/src/routes.rs @@ -0,0 +1,7 @@ +use axum::{routing::get, Router}; + +pub mod root; + +pub fn create_app() -> Router { + Router::new().route("/", get(root::handler)) +} diff --git a/homeserver/src/routes/root.rs b/homeserver/src/routes/root.rs new file mode 100644 index 0000000..35a9482 --- /dev/null +++ b/homeserver/src/routes/root.rs @@ -0,0 +1,5 @@ +use axum::response::IntoResponse; + +pub async fn handler() -> Result { + Ok("This a Pubky homeserver.".to_string()) +} diff --git a/homeserver/src/server.rs b/homeserver/src/server.rs new file mode 100644 index 0000000..29d8d42 --- /dev/null +++ b/homeserver/src/server.rs @@ -0,0 +1,101 @@ +use std::{future::IntoFuture, net::SocketAddr}; + +use anyhow::{Error, Result}; +use tokio::{net::TcpListener, signal, task::JoinSet}; +use tracing::{info, warn}; + +#[derive(Debug)] +pub struct Homeserver { + tasks: JoinSet>, +} + +impl Homeserver { + pub async fn start() -> Result { + let app = crate::routes::create_app(); + + let mut tasks = JoinSet::new(); + + let app = app.clone(); + + let listener = TcpListener::bind(SocketAddr::from(( + [127, 0, 0, 1], + 6287, // config.port() + ))) + .await?; + + let bound_addr = listener.local_addr()?; + + // Spawn http server task + tasks.spawn( + axum::serve( + listener, + app.into_make_service_with_connect_info::(), + ) + .with_graceful_shutdown(shutdown_signal()) + .into_future(), + ); + + info!("HTTP server listening on {bound_addr}"); + + Ok(Self { tasks }) + } + + // === Public Methods === + + /// Shutdown the server and wait for all tasks to complete. + pub async fn shutdown(mut self) -> Result<()> { + self.tasks.abort_all(); + self.run_until_done().await?; + Ok(()) + } + + /// Wait for all tasks to complete. + /// + /// Runs forever unless tasks fail. + pub async fn run_until_done(mut self) -> Result<()> { + let mut final_res: Result<()> = Ok(()); + while let Some(res) = self.tasks.join_next().await { + match res { + Ok(Ok(())) => {} + Err(err) if err.is_cancelled() => {} + Ok(Err(err)) => { + warn!(?err, "task failed"); + final_res = Err(Error::from(err)); + } + Err(err) => { + warn!(?err, "task panicked"); + final_res = Err(err.into()); + } + } + } + final_res + } +} + +async fn shutdown_signal() { + let ctrl_c = async { + signal::ctrl_c() + .await + .expect("failed to install Ctrl+C handler"); + }; + + #[cfg(unix)] + let terminate = async { + signal::unix::signal(signal::unix::SignalKind::terminate()) + .expect("failed to install signal handler") + .recv() + .await; + }; + + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + fn graceful_shutdown() { + info!("Gracefully Shutting down.."); + } + + tokio::select! { + _ = ctrl_c => graceful_shutdown(), + _ = terminate => graceful_shutdown(), + } +} From 3a0ad9b028e6b2a72244235a06e21ba123e47464 Mon Sep 17 00:00:00 2001 From: nazeh Date: Sun, 14 Jul 2024 20:15:52 +0300 Subject: [PATCH 02/13] feat(common): pubky authn and authnverifier --- Cargo.lock | 645 +++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 +- common/Cargo.toml | 15 + common/src/auth.rs | 217 +++++++++++++ common/src/crypto.rs | 14 + common/src/lib.rs | 4 + common/src/namespaces.rs | 1 + common/src/timestamp.rs | 229 ++++++++++++++ 8 files changed, 1126 insertions(+), 1 deletion(-) create mode 100644 common/Cargo.toml create mode 100644 common/src/auth.rs create mode 100644 common/src/crypto.rs create mode 100644 common/src/lib.rs create mode 100644 common/src/namespaces.rs create mode 100644 common/src/timestamp.rs diff --git a/Cargo.lock b/Cargo.lock index 3b0764a..4d51cf4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,6 +32,18 @@ version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + [[package]] name = "async-trait" version = "0.1.81" @@ -119,12 +131,52 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base32" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1ce0365f4d5fb6646220bb52fe547afd51796d90f914d4063cb0b032ebee088" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "blake3" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d08263faac5cde2a4d52b513dadb80846023aade56fcd8fc99ba73ba8050e92" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + [[package]] name = "bytes" version = "1.6.1" @@ -143,6 +195,155 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "constant_time_eq" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "document-features" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0" +dependencies = [ + "litrs", +] + +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "nanorand", + "spin", +] + [[package]] name = "fnv" version = "1.0.7" @@ -158,6 +359,21 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.30" @@ -165,6 +381,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -173,6 +390,40 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + [[package]] name = "futures-task" version = "0.3.30" @@ -185,10 +436,39 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ + "futures-channel", "futures-core", + "futures-io", + "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", ] [[package]] @@ -289,6 +569,15 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -301,6 +590,12 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" + [[package]] name = "lock_api" version = "0.4.12" @@ -317,6 +612,32 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "lru" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" + +[[package]] +name = "mainline" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b751ffb57303217bcae8f490eee6044a5b40eadf6ca05ff476cad37e7b7970d" +dependencies = [ + "bytes", + "crc", + "ed25519-dalek", + "flume", + "lru", + "rand", + "serde", + "serde_bencode", + "serde_bytes", + "sha1_smol", + "thiserror", + "tracing", +] + [[package]] name = "matchers" version = "0.1.0" @@ -364,6 +685,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -466,6 +796,48 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkarr" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89f9e12544b00f5561253bbd3cb72a85ff3bc398483dc1bf82bdf095c774136b" +dependencies = [ + "bytes", + "document-features", + "dyn-clone", + "ed25519-dalek", + "flume", + "futures", + "js-sys", + "lru", + "mainline", + "rand", + "self_cell", + "simple-dns", + "thiserror", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "z32", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "proc-macro2" version = "1.0.86" @@ -475,12 +847,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pubky-common" +version = "0.1.0" +dependencies = [ + "base32", + "blake3", + "ed25519-dalek", + "once_cell", + "pkarr", + "rand", + "thiserror", +] + [[package]] name = "pubky_homeserver" version = "0.1.0" dependencies = [ "anyhow", "axum", + "pkarr", "tokio", "tower-http", "tracing", @@ -496,6 +882,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "redox_syscall" version = "0.5.2" @@ -555,6 +971,15 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "rustversion" version = "1.0.17" @@ -573,6 +998,18 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "self_cell" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a" + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + [[package]] name = "serde" version = "1.0.204" @@ -582,6 +1019,25 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde_bencode" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a70dfc7b7438b99896e7f8992363ab8e2c4ba26aa5ec675d32d1c3c2c33d413e" +dependencies = [ + "serde", + "serde_bytes", +] + +[[package]] +name = "serde_bytes" +version = "0.11.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a" +dependencies = [ + "serde", +] + [[package]] name = "serde_derive" version = "1.0.204" @@ -626,6 +1082,23 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -644,6 +1117,33 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core", +] + +[[package]] +name = "simple-dns" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01607fe2e61894468c6dc0b26103abb073fb08b79a3d9e4b6d76a1a341549958" +dependencies = [ + "bitflags", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + [[package]] name = "smallvec" version = "1.13.2" @@ -660,6 +1160,31 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.71" @@ -683,6 +1208,26 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +[[package]] +name = "thiserror" +version = "1.0.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thread_local" version = "1.1.8" @@ -830,6 +1375,12 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + [[package]] name = "unicode-ident" version = "1.0.12" @@ -842,12 +1393,94 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + +[[package]] +name = "web-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1008,3 +1641,15 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "z32" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edb37266251c28b03d08162174a91c3a092e3bd4f476f8205ee1c507b78b7bdc" + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/Cargo.toml b/Cargo.toml index 1423834..ce5c39f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["homeserver"] +members = [ "common","homeserver"] # See: https://github.com/rust-lang/rust/issues/90148#issuecomment-949194352 resolver = "2" diff --git a/common/Cargo.toml b/common/Cargo.toml new file mode 100644 index 0000000..94ca15d --- /dev/null +++ b/common/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "pubky-common" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +base32 = "0.5.0" +blake3 = "1.5.1" +ed25519-dalek = "2.1.1" +once_cell = "1.19.0" +pkarr = "2.0.3" +rand = "0.8.5" +thiserror = "1.0.60" diff --git a/common/src/auth.rs b/common/src/auth.rs new file mode 100644 index 0000000..6469d2e --- /dev/null +++ b/common/src/auth.rs @@ -0,0 +1,217 @@ +use std::sync::{Arc, Mutex}; + +use ed25519_dalek::ed25519::SignatureBytes; + +use crate::{ + crypto::{random_hash, Keypair, PublicKey, Signature}, + timestamp::Timestamp, +}; + +// 30 seconds +const TIME_INTERVAL: u64 = 30 * 1_000_000; + +#[derive(Debug, PartialEq)] +pub struct AuthnSignature(Box<[u8]>); + +impl AuthnSignature { + pub fn new(signer: &Keypair, audience: &PublicKey, token: Option<&[u8]>) -> Self { + let mut bytes = Vec::with_capacity(96); + + let time: u64 = Timestamp::now().into(); + let time_step = time / TIME_INTERVAL; + + let token_hash = token.map_or(random_hash(), |t| crate::crypto::hash(t)); + + let signature = signer + .sign(&signable( + &time_step.to_be_bytes(), + &signer.public_key(), + audience, + token_hash.as_bytes(), + )) + .to_bytes(); + + bytes.extend_from_slice(&signature); + bytes.extend_from_slice(token_hash.as_bytes()); + + Self(bytes.into()) + } + + pub fn for_token(keypair: &Keypair, audience: &PublicKey, token: &[u8]) -> Self { + AuthnSignature::new(keypair, audience, Some(token)) + } + + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } +} + +#[derive(Debug, Clone)] +pub struct AuthnVerifier { + audience: PublicKey, + inner: Arc>>, + // TODO: Support permisisons + // token_hashes: HashSet<[u8; 32]>, +} + +impl AuthnVerifier { + pub fn new(audience: PublicKey) -> Self { + Self { + audience, + inner: Arc::new(Mutex::new(Vec::new())), + } + } + + pub fn verify(&self, bytes: &[u8], signer: &PublicKey) -> Result<(), AuthnSignatureError> { + self.gc(); + + if bytes.len() != 96 { + return Err(AuthnSignatureError::InvalidLength(bytes.len())); + } + + let signature_bytes: SignatureBytes = bytes[0..64] + .try_into() + .expect("validate token length on instantiating"); + let signature = Signature::from(signature_bytes); + + let token_hash: [u8; 32] = bytes[64..].try_into().expect("should not be reachable"); + + let now = Timestamp::now().into_inner(); + let past = now - TIME_INTERVAL; + let future = now + TIME_INTERVAL; + + let result = verify_at(now, &self, &signature, signer, &token_hash); + + match result { + Ok(_) => return Ok(()), + Err(AuthnSignatureError::AlreadyUsed) => return Err(AuthnSignatureError::AlreadyUsed), + _ => {} + } + + let result = verify_at(past, &self, &signature, signer, &token_hash); + + match result { + Ok(_) => return Ok(()), + Err(AuthnSignatureError::AlreadyUsed) => return Err(AuthnSignatureError::AlreadyUsed), + _ => {} + } + + verify_at(future, &self, &signature, signer, &token_hash) + } + + // === Private Methods === + + /// Remove all tokens older than two time intervals in the past. + fn gc(&self) { + let threshold = ((Timestamp::now().into_inner() / TIME_INTERVAL) - 2).to_be_bytes(); + + let mut inner = self.inner.lock().unwrap(); + + match inner.binary_search_by(|element| element[0..8].cmp(&threshold)) { + Ok(index) | Err(index) => { + inner.drain(0..index); + } + } + } +} + +fn verify_at( + time: u64, + verifier: &AuthnVerifier, + signature: &Signature, + signer: &PublicKey, + token_hash: &[u8; 32], +) -> Result<(), AuthnSignatureError> { + let time_step = time / TIME_INTERVAL; + let time_step_bytes = time_step.to_be_bytes(); + + let result = signer.verify( + &signable(&time_step_bytes, signer, &verifier.audience, token_hash), + signature, + ); + + if result.is_ok() { + let mut inner = verifier.inner.lock().unwrap(); + + let mut candidate = [0_u8; 40]; + candidate[..8].copy_from_slice(&time_step_bytes); + candidate[8..].copy_from_slice(token_hash); + + match inner.binary_search_by(|element| element.cmp(&candidate)) { + Ok(index) | Err(index) => { + inner.insert(index, candidate); + } + }; + + return Ok(()); + } + + Err(AuthnSignatureError::InvalidSignature) +} + +fn signable( + time_step_bytes: &[u8; 8], + signer: &PublicKey, + audience: &PublicKey, + token_hash: &[u8; 32], +) -> [u8; 115] { + let mut arr = [0; 115]; + + arr[..11].copy_from_slice(crate::namespaces::PUBKY_AUTHN); + arr[11..19].copy_from_slice(time_step_bytes); + arr[19..51].copy_from_slice(signer.as_bytes()); + arr[51..83].copy_from_slice(audience.as_bytes()); + arr[83..].copy_from_slice(token_hash); + + arr +} + +#[derive(thiserror::Error, Debug)] +pub enum AuthnSignatureError { + #[error("AuthnSignature should be 96 bytes long, got {0} bytes instead")] + InvalidLength(usize), + + #[error("Invalid signature")] + InvalidSignature, + + #[error("Authn signature already used")] + AlreadyUsed, +} + +#[cfg(test)] +mod tests { + use crate::crypto::Keypair; + + use super::{AuthnSignature, AuthnVerifier}; + + #[test] + fn sign_verify() { + let keypair = Keypair::random(); + let signer = keypair.public_key(); + let audience = Keypair::random().public_key(); + + let verifier = AuthnVerifier::new(audience.clone()); + + let authn_signature = AuthnSignature::new(&keypair, &audience, None); + + verifier + .verify(authn_signature.as_bytes(), &signer) + .unwrap(); + + { + // Invalid signable + let mut invalid = authn_signature.as_bytes().to_vec(); + invalid[64..].copy_from_slice(&[0; 32]); + + assert!(!verifier.verify(&invalid, &signer).is_ok()) + } + + { + // Invalid signer + let mut invalid = authn_signature.as_bytes().to_vec(); + invalid[0..32].copy_from_slice(&[0; 32]); + + assert!(!verifier.verify(&invalid, &signer).is_ok()) + } + } +} diff --git a/common/src/crypto.rs b/common/src/crypto.rs new file mode 100644 index 0000000..8bb700a --- /dev/null +++ b/common/src/crypto.rs @@ -0,0 +1,14 @@ +use rand::prelude::Rng; + +pub use pkarr::{Keypair, PublicKey}; + +pub use ed25519_dalek::Signature; + +pub type Hash = blake3::Hash; + +pub use blake3::hash; + +pub fn random_hash() -> Hash { + let mut rng = rand::thread_rng(); + Hash::from_bytes(rng.gen()) +} diff --git a/common/src/lib.rs b/common/src/lib.rs new file mode 100644 index 0000000..90b246d --- /dev/null +++ b/common/src/lib.rs @@ -0,0 +1,4 @@ +pub mod auth; +pub mod crypto; +pub mod namespaces; +pub mod timestamp; diff --git a/common/src/namespaces.rs b/common/src/namespaces.rs new file mode 100644 index 0000000..6c951dd --- /dev/null +++ b/common/src/namespaces.rs @@ -0,0 +1 @@ +pub const PUBKY_AUTHN: &[u8; 11] = b"PUBKY:AUTHN"; diff --git a/common/src/timestamp.rs b/common/src/timestamp.rs new file mode 100644 index 0000000..f850661 --- /dev/null +++ b/common/src/timestamp.rs @@ -0,0 +1,229 @@ +//! Monotonic unix timestamp in microseconds + +use std::fmt::Display; +use std::time::SystemTime; +use std::{ + ops::{Add, Sub}, + sync::Mutex, +}; + +use once_cell::sync::Lazy; +use rand::Rng; + +/// ~4% chance of none of 10 clocks have matching id. +const CLOCK_MASK: u64 = (1 << 8) - 1; +const TIME_MASK: u64 = !0 >> 8; + +pub struct TimestampFactory { + clock_id: u64, + last_time: u64, +} + +impl TimestampFactory { + pub fn new() -> Self { + Self { + clock_id: rand::thread_rng().gen::() & CLOCK_MASK, + last_time: system_time() & TIME_MASK, + } + } + + pub fn now(&mut self) -> Timestamp { + // Ensure monotonicity. + self.last_time = (system_time() & TIME_MASK).max(self.last_time + CLOCK_MASK + 1); + + // Add clock_id to the end of the timestamp + Timestamp(self.last_time | self.clock_id) + } +} + +impl Default for TimestampFactory { + fn default() -> Self { + Self::new() + } +} + +static DEFAULT_FACTORY: Lazy> = + Lazy::new(|| Mutex::new(TimestampFactory::default())); + +/// Monotonic timestamp since [SystemTime::UNIX_EPOCH] in microseconds as u64. +/// +/// The purpose of this timestamp is to unique per "user", not globally, +/// it achieves this by: +/// 1. Override the last byte with a random `clock_id`, reducing the probability +/// of two matching timestamps across multiple machines/threads. +/// 2. Gurantee that the remaining 3 bytes are ever increasing (monotonic) within +/// the same thread regardless of the wall clock value +/// +/// This timestamp is also serialized as BE bytes to remain sortable. +/// If a `utf-8` encoding is necessary, it is encoded as [base32::Alphabet::Crockford] +/// to act as a sortable Id. +/// +/// U64 of microseconds is valid for the next 500 thousand years! +#[derive(Debug, Clone, PartialEq, PartialOrd, Hash, Eq, Ord)] +pub struct Timestamp(u64); + +impl Timestamp { + pub fn now() -> Self { + DEFAULT_FACTORY.lock().unwrap().now() + } + + /// Return big endian bytes + pub fn to_bytes(&self) -> [u8; 8] { + self.0.to_be_bytes() + } + + pub fn difference(&self, rhs: &Timestamp) -> u64 { + self.0.abs_diff(rhs.0) + } + + pub fn into_inner(&self) -> u64 { + self.0 + } +} + +impl Display for Timestamp { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let bytes: [u8; 8] = self.into(); + f.write_str(&base32::encode(base32::Alphabet::Crockford, &bytes)) + } +} + +impl TryFrom for Timestamp { + type Error = TimestampError; + + fn try_from(value: String) -> Result { + match base32::decode(base32::Alphabet::Crockford, &value) { + Some(vec) => { + let bytes: [u8; 8] = vec + .try_into() + .map_err(|_| TimestampError::InvalidEncoding)?; + + Ok(bytes.into()) + } + None => Err(TimestampError::InvalidEncoding), + } + } +} + +impl TryFrom<&[u8]> for Timestamp { + type Error = TimestampError; + + fn try_from(bytes: &[u8]) -> Result { + let bytes: [u8; 8] = bytes + .try_into() + .map_err(|_| TimestampError::InvalidBytesLength(bytes.len()))?; + + Ok(bytes.into()) + } +} + +impl From<&Timestamp> for [u8; 8] { + fn from(timestamp: &Timestamp) -> Self { + timestamp.0.to_be_bytes() + } +} + +impl From<[u8; 8]> for Timestamp { + fn from(bytes: [u8; 8]) -> Self { + Self(u64::from_be_bytes(bytes)) + } +} + +// === U64 conversion === + +impl From for u64 { + fn from(value: Timestamp) -> Self { + value.into_inner() + } +} + +impl Add for &Timestamp { + type Output = Timestamp; + + fn add(self, rhs: u64) -> Self::Output { + Timestamp(self.0 + rhs) + } +} + +impl Sub for &Timestamp { + type Output = Timestamp; + + fn sub(self, rhs: u64) -> Self::Output { + Timestamp(self.0 - rhs) + } +} + +#[cfg(not(target_arch = "wasm32"))] +/// Return the number of microseconds since [SystemTime::UNIX_EPOCH] +fn system_time() -> u64 { + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("time drift") + .as_micros() as u64 +} + +#[derive(thiserror::Error, Debug)] +pub enum TimestampError { + #[error("Invalid bytes length, Timestamp should be encoded as 8 bytes, got {0}")] + InvalidBytesLength(usize), + #[error("Invalid timestamp encoding")] + InvalidEncoding, +} + +#[cfg(test)] +mod tests { + use std::collections::HashSet; + + use super::*; + + #[test] + fn monotonic() { + const COUNT: usize = 100; + + let mut set = HashSet::with_capacity(COUNT); + let mut vec = Vec::with_capacity(COUNT); + + for _ in 0..COUNT { + let timestamp = Timestamp::now(); + + set.insert(timestamp.clone()); + vec.push(timestamp); + } + + let mut ordered = vec.clone(); + ordered.sort(); + + assert_eq!(set.len(), COUNT, "unique"); + assert_eq!(ordered, vec, "ordered"); + } + + #[test] + fn strings() { + const COUNT: usize = 100; + + let mut set = HashSet::with_capacity(COUNT); + let mut vec = Vec::with_capacity(COUNT); + + for _ in 0..COUNT { + let string = Timestamp::now().to_string(); + + set.insert(string.clone()); + vec.push(string) + } + + let mut ordered = vec.clone(); + ordered.sort(); + + assert_eq!(set.len(), COUNT, "unique"); + assert_eq!(ordered, vec, "ordered"); + } + + #[test] + fn to_from_string() { + let timestamp = Timestamp::now(); + let string = timestamp.to_string(); + let decoded: Timestamp = string.try_into().unwrap(); + + assert_eq!(decoded, timestamp) + } +} From b780a57fe9b4ea5ea78f681a47bcd8eb6fd85e3b Mon Sep 17 00:00:00 2001 From: nazeh Date: Mon, 15 Jul 2024 07:11:33 +0300 Subject: [PATCH 03/13] chore: rename crates to pubky-* --- Cargo.toml | 2 +- homeserver/src/routes.rs | 7 --- {common => pubky-common}/Cargo.toml | 0 {common => pubky-common}/src/auth.rs | 8 ++-- {common => pubky-common}/src/crypto.rs | 0 {common => pubky-common}/src/lib.rs | 0 {common => pubky-common}/src/namespaces.rs | 0 {common => pubky-common}/src/timestamp.rs | 0 {homeserver => pubky-homeserver}/Cargo.toml | 1 + pubky-homeserver/src/extractors.rs | 43 +++++++++++++++++++ {homeserver => pubky-homeserver}/src/lib.rs | 1 + {homeserver => pubky-homeserver}/src/main.rs | 4 +- pubky-homeserver/src/routes.rs | 12 ++++++ pubky-homeserver/src/routes/drive.rs | 11 +++++ .../src/routes/root.rs | 0 .../src/server.rs | 2 +- 16 files changed, 77 insertions(+), 14 deletions(-) delete mode 100644 homeserver/src/routes.rs rename {common => pubky-common}/Cargo.toml (100%) rename {common => pubky-common}/src/auth.rs (95%) rename {common => pubky-common}/src/crypto.rs (100%) rename {common => pubky-common}/src/lib.rs (100%) rename {common => pubky-common}/src/namespaces.rs (100%) rename {common => pubky-common}/src/timestamp.rs (100%) rename {homeserver => pubky-homeserver}/Cargo.toml (95%) create mode 100644 pubky-homeserver/src/extractors.rs rename {homeserver => pubky-homeserver}/src/lib.rs (76%) rename {homeserver => pubky-homeserver}/src/main.rs (63%) create mode 100644 pubky-homeserver/src/routes.rs create mode 100644 pubky-homeserver/src/routes/drive.rs rename {homeserver => pubky-homeserver}/src/routes/root.rs (100%) rename {homeserver => pubky-homeserver}/src/server.rs (98%) diff --git a/Cargo.toml b/Cargo.toml index ce5c39f..c60c23b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = [ "common","homeserver"] +members = ["pubky-*"] # See: https://github.com/rust-lang/rust/issues/90148#issuecomment-949194352 resolver = "2" diff --git a/homeserver/src/routes.rs b/homeserver/src/routes.rs deleted file mode 100644 index 74bc0ee..0000000 --- a/homeserver/src/routes.rs +++ /dev/null @@ -1,7 +0,0 @@ -use axum::{routing::get, Router}; - -pub mod root; - -pub fn create_app() -> Router { - Router::new().route("/", get(root::handler)) -} diff --git a/common/Cargo.toml b/pubky-common/Cargo.toml similarity index 100% rename from common/Cargo.toml rename to pubky-common/Cargo.toml diff --git a/common/src/auth.rs b/pubky-common/src/auth.rs similarity index 95% rename from common/src/auth.rs rename to pubky-common/src/auth.rs index 6469d2e..a344be3 100644 --- a/common/src/auth.rs +++ b/pubky-common/src/auth.rs @@ -20,7 +20,7 @@ impl AuthnSignature { let time: u64 = Timestamp::now().into(); let time_step = time / TIME_INTERVAL; - let token_hash = token.map_or(random_hash(), |t| crate::crypto::hash(t)); + let token_hash = token.map_or(random_hash(), crate::crypto::hash); let signature = signer .sign(&signable( @@ -80,7 +80,7 @@ impl AuthnVerifier { let past = now - TIME_INTERVAL; let future = now + TIME_INTERVAL; - let result = verify_at(now, &self, &signature, signer, &token_hash); + let result = verify_at(now, self, &signature, signer, &token_hash); match result { Ok(_) => return Ok(()), @@ -88,7 +88,7 @@ impl AuthnVerifier { _ => {} } - let result = verify_at(past, &self, &signature, signer, &token_hash); + let result = verify_at(past, self, &signature, signer, &token_hash); match result { Ok(_) => return Ok(()), @@ -96,7 +96,7 @@ impl AuthnVerifier { _ => {} } - verify_at(future, &self, &signature, signer, &token_hash) + verify_at(future, self, &signature, signer, &token_hash) } // === Private Methods === diff --git a/common/src/crypto.rs b/pubky-common/src/crypto.rs similarity index 100% rename from common/src/crypto.rs rename to pubky-common/src/crypto.rs diff --git a/common/src/lib.rs b/pubky-common/src/lib.rs similarity index 100% rename from common/src/lib.rs rename to pubky-common/src/lib.rs diff --git a/common/src/namespaces.rs b/pubky-common/src/namespaces.rs similarity index 100% rename from common/src/namespaces.rs rename to pubky-common/src/namespaces.rs diff --git a/common/src/timestamp.rs b/pubky-common/src/timestamp.rs similarity index 100% rename from common/src/timestamp.rs rename to pubky-common/src/timestamp.rs diff --git a/homeserver/Cargo.toml b/pubky-homeserver/Cargo.toml similarity index 95% rename from homeserver/Cargo.toml rename to pubky-homeserver/Cargo.toml index e4e2bf6..40cc287 100644 --- a/homeserver/Cargo.toml +++ b/pubky-homeserver/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [dependencies] anyhow = "1.0.82" axum = "0.7.5" +pkarr = "2.0.3" tokio = { version = "1.37.0", features = ["full"] } tower-http = { version = "0.5.2", features = ["cors", "trace"] } tracing = "0.1.40" diff --git a/pubky-homeserver/src/extractors.rs b/pubky-homeserver/src/extractors.rs new file mode 100644 index 0000000..b637b97 --- /dev/null +++ b/pubky-homeserver/src/extractors.rs @@ -0,0 +1,43 @@ +use std::collections::HashMap; + +use axum::{ + async_trait, + extract::{FromRequestParts, Path}, + http::{request::Parts, StatusCode}, + response::{IntoResponse, Response}, + RequestPartsExt, +}; + +use pkarr::PublicKey; + +#[derive(Debug)] +pub struct Pubky(PublicKey); + +impl Pubky { + pub fn public_key(&self) -> &PublicKey { + &self.0 + } +} + +#[async_trait] +impl FromRequestParts for Pubky +where + S: Send + Sync, +{ + type Rejection = Response; + + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { + let params: Path> = + parts.extract().await.map_err(IntoResponse::into_response)?; + + let pubky_id = params + .get("pubky") + .ok_or_else(|| (StatusCode::NOT_FOUND, "Pubky not found").into_response())?; + + let public_key = PublicKey::try_from(pubky_id.to_string()) + // TODO: convert Pkarr errors to app errors, in this case a params validation error + .map_err(|_| (StatusCode::BAD_REQUEST, "Invalid Pubky").into_response())?; + + Ok(Pubky(public_key)) + } +} diff --git a/homeserver/src/lib.rs b/pubky-homeserver/src/lib.rs similarity index 76% rename from homeserver/src/lib.rs rename to pubky-homeserver/src/lib.rs index b4b9f85..726545f 100644 --- a/homeserver/src/lib.rs +++ b/pubky-homeserver/src/lib.rs @@ -1,3 +1,4 @@ +mod extractors; mod routes; mod server; diff --git a/homeserver/src/main.rs b/pubky-homeserver/src/main.rs similarity index 63% rename from homeserver/src/main.rs rename to pubky-homeserver/src/main.rs index ab284a8..7d8a5c1 100644 --- a/homeserver/src/main.rs +++ b/pubky-homeserver/src/main.rs @@ -3,7 +3,9 @@ use pubky_homeserver::Homeserver; #[tokio::main] async fn main() -> Result<()> { - tracing_subscriber::fmt().init(); + tracing_subscriber::fmt() + .with_env_filter("pubky_homeserver=debug,tower_http=debug") + .init(); let server = Homeserver::start().await?; diff --git a/pubky-homeserver/src/routes.rs b/pubky-homeserver/src/routes.rs new file mode 100644 index 0000000..ecd16e4 --- /dev/null +++ b/pubky-homeserver/src/routes.rs @@ -0,0 +1,12 @@ +use axum::{routing::get, Router}; +use tower_http::trace::TraceLayer; + +pub mod drive; +pub mod root; + +pub fn create_app() -> Router { + Router::new() + .route("/", get(root::handler)) + .route("/:pubky/*key", get(drive::put)) + .layer(TraceLayer::new_for_http()) +} diff --git a/pubky-homeserver/src/routes/drive.rs b/pubky-homeserver/src/routes/drive.rs new file mode 100644 index 0000000..3050250 --- /dev/null +++ b/pubky-homeserver/src/routes/drive.rs @@ -0,0 +1,11 @@ +use axum::response::IntoResponse; + +use tracing::debug; + +use crate::extractors::Pubky; + +pub async fn put(pubky: Pubky) -> Result { + debug!(pubky=?pubky.public_key()); + + Ok("Pubky drive...".to_string()) +} diff --git a/homeserver/src/routes/root.rs b/pubky-homeserver/src/routes/root.rs similarity index 100% rename from homeserver/src/routes/root.rs rename to pubky-homeserver/src/routes/root.rs diff --git a/homeserver/src/server.rs b/pubky-homeserver/src/server.rs similarity index 98% rename from homeserver/src/server.rs rename to pubky-homeserver/src/server.rs index 29d8d42..506d5ac 100644 --- a/homeserver/src/server.rs +++ b/pubky-homeserver/src/server.rs @@ -18,7 +18,7 @@ impl Homeserver { let app = app.clone(); let listener = TcpListener::bind(SocketAddr::from(( - [127, 0, 0, 1], + [0, 0, 0, 0], 6287, // config.port() ))) .await?; From 6363a174e45084f747920cdcbee7e83617afad61 Mon Sep 17 00:00:00 2001 From: nazeh Date: Mon, 15 Jul 2024 16:07:38 +0300 Subject: [PATCH 04/13] feat(homeserver): publish server pkarr packet --- pubky-homeserver/Cargo.toml | 4 +- pubky-homeserver/src/config.rs | 105 +++++++++++++++++++++++++++++++++ pubky-homeserver/src/lib.rs | 2 + pubky-homeserver/src/main.rs | 2 +- pubky-homeserver/src/pkarr.rs | 42 +++++++++++++ 5 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 pubky-homeserver/src/config.rs create mode 100644 pubky-homeserver/src/pkarr.rs diff --git a/pubky-homeserver/Cargo.toml b/pubky-homeserver/Cargo.toml index 40cc287..4790941 100644 --- a/pubky-homeserver/Cargo.toml +++ b/pubky-homeserver/Cargo.toml @@ -6,7 +6,9 @@ edition = "2021" [dependencies] anyhow = "1.0.82" axum = "0.7.5" -pkarr = "2.0.3" +dirs-next = "2.0.0" +pkarr = { version = "2.1.0", features = ["async"] } +pubky-common = { version = "0.1.0", path = "../pubky-common" } tokio = { version = "1.37.0", features = ["full"] } tower-http = { version = "0.5.2", features = ["cors", "trace"] } tracing = "0.1.40" diff --git a/pubky-homeserver/src/config.rs b/pubky-homeserver/src/config.rs new file mode 100644 index 0000000..7c9fcfe --- /dev/null +++ b/pubky-homeserver/src/config.rs @@ -0,0 +1,105 @@ +//! Configuration for the server + +use anyhow::{anyhow, Result}; +use pkarr::Keypair; +// use serde::{Deserialize, Serialize}; +use std::{fmt::Debug, path::PathBuf}; + +use pubky_common::timestamp::Timestamp; + +const DEFAULT_HOMESERVER_PORT: u16 = 6287; +const DEFAULT_STORAGE_DIR: &str = "pubky"; + +/// Server configuration +/// +/// The config is usually loaded from a file with [`Self::load`]. +#[derive( + // Serialize, Deserialize, + Clone, +)] +pub struct Config { + port: Option, + bootstrap: Option>, + domain: String, + /// Path to the storage directory + /// + /// Defaults to a directory in the OS data directory + storage: Option, + keypair: Keypair, +} + +impl Config { + // /// Load the config from a file. + // pub async fn load(path: impl AsRef) -> Result { + // let s = tokio::fs::read_to_string(path.as_ref()) + // .await + // .with_context(|| format!("failed to read {}", path.as_ref().to_string_lossy()))?; + // let config: Config = toml::from_str(&s)?; + // Ok(config) + // } + + /// Test configurations + pub fn test(testnet: &pkarr::mainline::Testnet) -> Self { + Self { + bootstrap: Some(testnet.bootstrap.to_owned()), + storage: Some( + std::env::temp_dir() + .join(Timestamp::now().to_string()) + .join(DEFAULT_STORAGE_DIR), + ), + ..Default::default() + } + } + + pub fn port(&self) -> u16 { + self.port.unwrap_or(DEFAULT_HOMESERVER_PORT) + } + + pub fn bootstsrap(&self) -> Option> { + self.bootstrap.to_owned() + } + + pub fn domain(&self) -> &str { + &self.domain + } + + /// Get the path to the storage directory + pub fn storage(&self) -> Result { + let dir = if let Some(storage) = &self.storage { + PathBuf::from(storage) + } else { + let path = dirs_next::data_dir().ok_or_else(|| { + anyhow!("operating environment provides no directory for application data") + })?; + path.join(DEFAULT_STORAGE_DIR) + }; + + Ok(dir.join("homeserver")) + } + + pub fn keypair(&self) -> &Keypair { + &self.keypair + } +} + +impl Default for Config { + fn default() -> Self { + Self { + port: Some(0), + bootstrap: None, + domain: "localhost".to_string(), + storage: None, + keypair: Keypair::random(), + } + } +} + +impl Debug for Config { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_map() + .entry(&"port", &self.port()) + .entry(&"storage", &self.storage()) + .entry(&"public_key", &self.keypair().public_key()) + .finish() + } +} diff --git a/pubky-homeserver/src/lib.rs b/pubky-homeserver/src/lib.rs index 726545f..0ca9554 100644 --- a/pubky-homeserver/src/lib.rs +++ b/pubky-homeserver/src/lib.rs @@ -1,4 +1,6 @@ +pub mod config; mod extractors; +mod pkarr; mod routes; mod server; diff --git a/pubky-homeserver/src/main.rs b/pubky-homeserver/src/main.rs index 7d8a5c1..a54fba4 100644 --- a/pubky-homeserver/src/main.rs +++ b/pubky-homeserver/src/main.rs @@ -7,7 +7,7 @@ async fn main() -> Result<()> { .with_env_filter("pubky_homeserver=debug,tower_http=debug") .init(); - let server = Homeserver::start().await?; + let server = Homeserver::start(Default::default()).await?; server.run_until_done().await?; diff --git a/pubky-homeserver/src/pkarr.rs b/pubky-homeserver/src/pkarr.rs new file mode 100644 index 0000000..5186273 --- /dev/null +++ b/pubky-homeserver/src/pkarr.rs @@ -0,0 +1,42 @@ +//! Pkarr related task + +use pkarr::{ + dns::{rdata::SVCB, Packet}, + Keypair, PkarrClientAsync, SignedPacket, +}; + +pub async fn publish_server_packet( + pkarr_client: PkarrClientAsync, + keypair: &Keypair, + domain: &str, + port: u16, +) -> anyhow::Result<()> { + let mut packet = Packet::new_reply(0); + + let mut svcb = SVCB::new(0, domain.try_into()?); + + // Publishing port only for localhost domain, + // assuming any other domain will point to a reverse proxy + // at the conventional ports. + if domain == "localhost" { + svcb.set_port(port); + + // TODO: Add more parameteres like the signer key! + // svcb.set_param(key, value) + }; + + // TODO: announce A/AAAA records as well for Noise connections? + + packet.answers.push(pkarr::dns::ResourceRecord::new( + "pubky".try_into().unwrap(), + pkarr::dns::CLASS::IN, + 60 * 60, + pkarr::dns::rdata::RData::SVCB(svcb), + )); + + let signed_packet = SignedPacket::from_packet(keypair, &packet)?; + + pkarr_client.publish(&signed_packet).await?; + + Ok(()) +} From c41e14233b6b2264bb90968aebdcd6654c3de3af Mon Sep 17 00:00:00 2001 From: nazeh Date: Tue, 16 Jul 2024 12:36:28 +0300 Subject: [PATCH 05/13] feat(pubky): publish and resolve homeserver endpoint --- Cargo.lock | 217 ++++++++++++++++++++++++++++++- Cargo.toml | 2 +- pubky-common/Cargo.toml | 2 +- pubky-common/src/auth.rs | 9 +- pubky-homeserver/src/pkarr.rs | 4 +- pubky-homeserver/src/server.rs | 58 +++++++-- pubky/Cargo.toml | 16 +++ pubky/src/error.rs | 20 +++ pubky/src/lib.rs | 228 +++++++++++++++++++++++++++++++++ 9 files changed, 539 insertions(+), 17 deletions(-) create mode 100644 pubky/Cargo.toml create mode 100644 pubky/src/error.rs create mode 100644 pubky/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 4d51cf4..5f3b96f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -137,6 +137,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1ce0365f4d5fb6646220bb52fe547afd51796d90f914d4063cb0b032ebee088" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64ct" version = "1.6.0" @@ -231,6 +237,15 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -288,6 +303,27 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "document-features" version = "0.2.10" @@ -334,12 +370,24 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "flate2" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "flume" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" dependencies = [ + "futures-core", + "futures-sink", "nanorand", "spin", ] @@ -563,6 +611,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "itoa" version = "1.0.11" @@ -590,6 +648,16 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags", + "libc", +] + [[package]] name = "litrs" version = "0.4.1" @@ -798,9 +866,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkarr" -version = "2.0.3" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89f9e12544b00f5561253bbd3cb72a85ff3bc398483dc1bf82bdf095c774136b" +checksum = "4548c673cbf8c91b69f7a17d3a042710aa73cffe5e82351db5378f26c3be64d8" dependencies = [ "bytes", "document-features", @@ -847,6 +915,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pubky" +version = "0.1.0" +dependencies = [ + "pkarr", + "pubky-common", + "pubky_homeserver", + "thiserror", + "tokio", + "ureq", + "url", +] + [[package]] name = "pubky-common" version = "0.1.0" @@ -866,7 +947,9 @@ version = "0.1.0" dependencies = [ "anyhow", "axum", + "dirs-next", "pkarr", + "pubky-common", "tokio", "tower-http", "tracing", @@ -921,6 +1004,17 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "regex" version = "1.10.5" @@ -965,6 +1059,21 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -980,6 +1089,38 @@ dependencies = [ "semver", ] +[[package]] +name = "rustls" +version = "0.23.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4828ea528154ae444e5a642dbb7d5623354030dc9822b83fd9bb79683c7399d0" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" + +[[package]] +name = "rustls-webpki" +version = "0.102.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a6fccd794a42c2c105b513a2f62bc3fd8f3ba57a4593677ceb0bd035164d78" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.17" @@ -1238,6 +1379,21 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.38.0" @@ -1381,12 +1537,60 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72139d247e5f97a3eff96229a7ae85ead5328a39efe76f8bf5a06313d505b6ea" +dependencies = [ + "base64", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-pki-types", + "url", + "webpki-roots", +] + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + [[package]] name = "valuable" version = "0.1.0" @@ -1481,6 +1685,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index c60c23b..85e44a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["pubky-*"] +members = [ "pubky","pubky-*"] # See: https://github.com/rust-lang/rust/issues/90148#issuecomment-949194352 resolver = "2" diff --git a/pubky-common/Cargo.toml b/pubky-common/Cargo.toml index 94ca15d..8f73315 100644 --- a/pubky-common/Cargo.toml +++ b/pubky-common/Cargo.toml @@ -10,6 +10,6 @@ base32 = "0.5.0" blake3 = "1.5.1" ed25519-dalek = "2.1.1" once_cell = "1.19.0" -pkarr = "2.0.3" +pkarr = "2.1.0" rand = "0.8.5" thiserror = "1.0.60" diff --git a/pubky-common/src/auth.rs b/pubky-common/src/auth.rs index a344be3..7fc2a02 100644 --- a/pubky-common/src/auth.rs +++ b/pubky-common/src/auth.rs @@ -1,3 +1,5 @@ +//! Client-server Authentication using signed timesteps + use std::sync::{Arc, Mutex}; use ed25519_dalek::ed25519::SignatureBytes; @@ -37,8 +39,9 @@ impl AuthnSignature { Self(bytes.into()) } - pub fn for_token(keypair: &Keypair, audience: &PublicKey, token: &[u8]) -> Self { - AuthnSignature::new(keypair, audience, Some(token)) + /// Sign a randomly generated nonce + pub fn generate(keypair: &Keypair, audience: &PublicKey) -> Self { + AuthnSignature::new(keypair, audience, None) } pub fn as_bytes(&self) -> &[u8] { @@ -192,7 +195,7 @@ mod tests { let verifier = AuthnVerifier::new(audience.clone()); - let authn_signature = AuthnSignature::new(&keypair, &audience, None); + let authn_signature = AuthnSignature::generate(&keypair, &audience); verifier .verify(authn_signature.as_bytes(), &signer) diff --git a/pubky-homeserver/src/pkarr.rs b/pubky-homeserver/src/pkarr.rs index 5186273..113c598 100644 --- a/pubky-homeserver/src/pkarr.rs +++ b/pubky-homeserver/src/pkarr.rs @@ -19,6 +19,7 @@ pub async fn publish_server_packet( // assuming any other domain will point to a reverse proxy // at the conventional ports. if domain == "localhost" { + svcb.priority = 1; svcb.set_port(port); // TODO: Add more parameteres like the signer key! @@ -26,9 +27,10 @@ pub async fn publish_server_packet( }; // TODO: announce A/AAAA records as well for Noise connections? + // Or maybe Iroh's magic socket packet.answers.push(pkarr::dns::ResourceRecord::new( - "pubky".try_into().unwrap(), + "@".try_into().unwrap(), pkarr::dns::CLASS::IN, 60 * 60, pkarr::dns::rdata::RData::SVCB(svcb), diff --git a/pubky-homeserver/src/server.rs b/pubky-homeserver/src/server.rs index 506d5ac..215d67c 100644 --- a/pubky-homeserver/src/server.rs +++ b/pubky-homeserver/src/server.rs @@ -4,26 +4,31 @@ use anyhow::{Error, Result}; use tokio::{net::TcpListener, signal, task::JoinSet}; use tracing::{info, warn}; +use pkarr::{ + mainline::dht::{DhtSettings, Testnet}, + PkarrClient, PublicKey, Settings, +}; + +use crate::{config::Config, pkarr::publish_server_packet}; + #[derive(Debug)] pub struct Homeserver { + pub(crate) config: Config, + port: u16, tasks: JoinSet>, } impl Homeserver { - pub async fn start() -> Result { + pub async fn start(config: Config) -> Result { let app = crate::routes::create_app(); let mut tasks = JoinSet::new(); let app = app.clone(); - let listener = TcpListener::bind(SocketAddr::from(( - [0, 0, 0, 0], - 6287, // config.port() - ))) - .await?; + let listener = TcpListener::bind(SocketAddr::from(([0, 0, 0, 0], config.port()))).await?; - let bound_addr = listener.local_addr()?; + let port = listener.local_addr()?.port(); // Spawn http server task tasks.spawn( @@ -35,9 +40,44 @@ impl Homeserver { .into_future(), ); - info!("HTTP server listening on {bound_addr}"); + info!("Homeserver listening on http://localhost:{port}"); + + let pkarr_client = PkarrClient::new(Settings { + dht: DhtSettings { + bootstrap: config.bootstsrap(), + ..Default::default() + }, + ..Default::default() + })? + .as_async(); + + publish_server_packet(pkarr_client, config.keypair(), config.domain(), port).await?; + + info!( + "Homeserver listening on pubky://{}", + config.keypair().public_key() + ); + + Ok(Self { + tasks, + config, + port, + }) + } + + /// Test version of [Homeserver::start], using mainline Testnet, and a temporary storage. + pub async fn start_test(testnet: &Testnet) -> Result { + Homeserver::start(Config::test(testnet)).await + } + + // === Getters === + + pub fn port(&self) -> u16 { + self.port + } - Ok(Self { tasks }) + pub fn public_key(&self) -> PublicKey { + self.config.keypair().public_key() } // === Public Methods === diff --git a/pubky/Cargo.toml b/pubky/Cargo.toml new file mode 100644 index 0000000..321bea7 --- /dev/null +++ b/pubky/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "pubky" +version = "0.1.0" +edition = "2021" + +[dependencies] +pubky-common = { version = "0.1.0", path = "../pubky-common" } + +pkarr = "2.1.0" +ureq = "2.10.0" +thiserror = "1.0.62" +url = "2.5.2" + +[dev-dependencies] +pubky_homeserver = { path = "../pubky-homeserver" } +tokio = "1.37.0" diff --git a/pubky/src/error.rs b/pubky/src/error.rs new file mode 100644 index 0000000..47ec28e --- /dev/null +++ b/pubky/src/error.rs @@ -0,0 +1,20 @@ +//! Main Crate Error + +use pkarr::dns::SimpleDnsError; + +// Alias Result to be the crate Result. +pub type Result = core::result::Result; + +#[derive(thiserror::Error, Debug)] +/// Pk common Error +pub enum Error { + /// For starter, to remove as code matures. + #[error("Generic error: {0}")] + Generic(String), + + #[error(transparent)] + Dns(#[from] SimpleDnsError), + + #[error(transparent)] + Pkarr(#[from] pkarr::Error), +} diff --git a/pubky/src/lib.rs b/pubky/src/lib.rs new file mode 100644 index 0000000..74cfd41 --- /dev/null +++ b/pubky/src/lib.rs @@ -0,0 +1,228 @@ +#![allow(unused)] + +use std::{collections::HashMap, time::Duration}; + +use pkarr::{ + dns::{rdata::SVCB, Packet}, + mainline::{dht::DhtSettings, Testnet}, + Keypair, PkarrClient, PublicKey, Settings, SignedPacket, +}; +use ureq::{Agent, Response}; +use url::Url; + +use pubky_common::auth::AuthnSignature; + +mod error; + +use error::{Error, Result}; + +const MAX_RECURSIVE_PUBKY_HOMESERVER_RESOLUTION: u8 = 3; + +#[derive(Debug)] +pub struct Client { + agent: Agent, + pkarr: PkarrClient, +} + +impl Client { + pub fn new() -> Self { + Self { + agent: Agent::new(), + pkarr: PkarrClient::new(Default::default()).unwrap(), + } + } + + pub fn test(testnet: &Testnet) -> Self { + Self { + agent: Agent::new(), + pkarr: PkarrClient::new(Settings { + dht: DhtSettings { + request_timeout: Some(Duration::from_millis(10)), + bootstrap: Some(testnet.bootstrap.to_owned()), + ..DhtSettings::default() + }, + ..Settings::default() + }) + .unwrap(), + } + } + + // === Public Methods === + + // === Private Methods === + + /// Publish the SVCB record for `_pubky.`. + fn publish_pubky_homeserver(&self, keypair: &Keypair, host: &str) -> Result<()> { + let mut packet = Packet::new_reply(0); + + if let Some(existing) = self.pkarr.resolve(&keypair.public_key())? { + for answer in existing.packet().answers.iter().cloned() { + if !answer.name.to_string().starts_with("_pubky") { + packet.answers.push(answer.into_owned()) + } + } + } + + let mut svcb = SVCB::new(0, host.try_into()?); + + packet.answers.push(pkarr::dns::ResourceRecord::new( + "_pubky".try_into().unwrap(), + pkarr::dns::CLASS::IN, + 60 * 60, + pkarr::dns::rdata::RData::SVCB(svcb), + )); + + let signed_packet = SignedPacket::from_packet(keypair, &packet)?; + + self.pkarr.publish(&signed_packet)?; + + Ok(()) + } + + /// Resolve the homeserver for a pubky. + fn resolve_pubky_homeserver(&self, pubky: &PublicKey) -> Result<(PublicKey, String)> { + // TODO: cache the result of this function? + + let mut target = format!("_pubky.{}", pubky); + let mut homeserver_public_key = None; + let mut host = target.clone(); + + // PublicKey is very good at extracting the Pkarr TLD from a string. + while let Ok(public_key) = PublicKey::try_from(target.clone()) { + if let Some(signed_packet) = self.pkarr.resolve(&public_key)? { + let mut prior = None; + + for answer in signed_packet.resource_records(&target) { + if let pkarr::dns::rdata::RData::SVCB(svcb) = &answer.rdata { + if svcb.priority == 0 { + prior = Some(svcb) + } else if let Some(sofar) = prior { + if svcb.priority >= sofar.priority { + prior = Some(svcb) + } + // TODO return random if priority is the same + } else { + prior = Some(svcb) + } + } + } + + if let Some(svcb) = prior { + homeserver_public_key = Some(public_key); + target = svcb.target.to_string(); + if let Some(port) = svcb.get_param(pkarr::dns::rdata::SVCB::PORT) { + if port.len() < 2 { + // TODO: debug! Error encoding port! + } + let port = u16::from_be_bytes([port[0], port[1]]); + + host = format!("{target}:{port}"); + } else { + host.clone_from(&target); + }; + + continue; + } + }; + + break; + } + + if let Some(homeserver) = homeserver_public_key { + return Ok((homeserver, host)); + } + + Err(Error::Generic("Could not resolve homeserver".to_string())) + } + + fn fetch_direct(&self, method: HttpMethod, url: &str) -> Result { + self.agent + .request(method.into(), url) + .call() + .map_err(|_| Error::Generic("ureq error".to_string())) + } +} + +impl Default for Client { + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug, Clone)] +pub enum HttpMethod { + GET, + PUT, +} + +impl From for &str { + fn from(value: HttpMethod) -> Self { + match value { + HttpMethod::GET => "GET", + HttpMethod::PUT => "PUT", + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use pkarr::{mainline::Testnet, Keypair}; + use pubky_homeserver::Homeserver; + + #[tokio::test] + async fn resolve_homeserver() { + let testnet = Testnet::new(3); + let server = Homeserver::start_test(&testnet).await.unwrap(); + + // Publish an intermediate controller of the homeserver + let pkarr_client = PkarrClient::new(Settings { + dht: DhtSettings { + bootstrap: Some(testnet.bootstrap.clone()), + ..Default::default() + }, + ..Default::default() + }) + .unwrap() + .as_async(); + + let intermediate = Keypair::random(); + + let mut packet = Packet::new_reply(0); + + let server_tld = server.public_key().to_string(); + + let mut svcb = SVCB::new(0, server_tld.as_str().try_into().unwrap()); + + packet.answers.push(pkarr::dns::ResourceRecord::new( + "pubky".try_into().unwrap(), + pkarr::dns::CLASS::IN, + 60 * 60, + pkarr::dns::rdata::RData::SVCB(svcb), + )); + + let signed_packet = SignedPacket::from_packet(&intermediate, &packet).unwrap(); + + pkarr_client.publish(&signed_packet).await.unwrap(); + + tokio::task::spawn_blocking(move || { + let client = Client::test(&testnet); + + let pubky = Keypair::random(); + + client + .publish_pubky_homeserver(&pubky, &format!("pubky.{}", &intermediate.public_key())); + + let (public_key, host) = client + .resolve_pubky_homeserver(&pubky.public_key()) + .unwrap(); + + assert_eq!(public_key, server.public_key()); + assert!(host.starts_with("localhost")); + assert!(host.ends_with(&server.port().to_string())); + }) + .await + .expect("task failed") + } +} From e33638c6f91ec862ef1c45d1ac78cca6a688ab83 Mon Sep 17 00:00:00 2001 From: nazeh Date: Wed, 17 Jul 2024 12:14:43 +0300 Subject: [PATCH 06/13] feat(pubky): send AuthnSignature to homeserver to signup --- Cargo.lock | 2 + pubky-homeserver/Cargo.toml | 1 + pubky-homeserver/src/error.rs | 90 ++++++++++ pubky-homeserver/src/extractors.rs | 8 +- pubky-homeserver/src/lib.rs | 3 + pubky-homeserver/src/routes.rs | 16 +- pubky-homeserver/src/routes/auth.rs | 16 ++ pubky-homeserver/src/server.rs | 21 ++- pubky/Cargo.toml | 1 + pubky/src/client.rs | 262 ++++++++++++++++++++++++++++ pubky/src/client_async.rs | 31 ++++ pubky/src/error.rs | 9 + pubky/src/lib.rs | 218 ++--------------------- 13 files changed, 457 insertions(+), 221 deletions(-) create mode 100644 pubky-homeserver/src/error.rs create mode 100644 pubky-homeserver/src/routes/auth.rs create mode 100644 pubky/src/client.rs create mode 100644 pubky/src/client_async.rs diff --git a/Cargo.lock b/Cargo.lock index 5f3b96f..1ad1678 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -919,6 +919,7 @@ dependencies = [ name = "pubky" version = "0.1.0" dependencies = [ + "flume", "pkarr", "pubky-common", "pubky_homeserver", @@ -947,6 +948,7 @@ version = "0.1.0" dependencies = [ "anyhow", "axum", + "bytes", "dirs-next", "pkarr", "pubky-common", diff --git a/pubky-homeserver/Cargo.toml b/pubky-homeserver/Cargo.toml index 4790941..954daea 100644 --- a/pubky-homeserver/Cargo.toml +++ b/pubky-homeserver/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [dependencies] anyhow = "1.0.82" axum = "0.7.5" +bytes = "1.6.1" dirs-next = "2.0.0" pkarr = { version = "2.1.0", features = ["async"] } pubky-common = { version = "0.1.0", path = "../pubky-common" } diff --git a/pubky-homeserver/src/error.rs b/pubky-homeserver/src/error.rs new file mode 100644 index 0000000..2f92b7d --- /dev/null +++ b/pubky-homeserver/src/error.rs @@ -0,0 +1,90 @@ +//! Server error + +use axum::{ + extract::rejection::{ExtensionRejection, PathRejection, QueryRejection}, + http::StatusCode, + response::IntoResponse, +}; +use pubky_common::auth::AuthnSignatureError; + +pub type Result = core::result::Result; + +#[derive(Debug, Clone)] +pub struct Error { + // #[serde(with = "serde_status_code")] + status: StatusCode, + detail: Option, +} + +impl Default for Error { + fn default() -> Self { + Self { + status: StatusCode::INTERNAL_SERVER_ERROR, + detail: None, + } + } +} + +impl Error { + pub fn with_status(status: StatusCode) -> Error { + Self { + status, + detail: None, + } + } + + /// Create a new [`Error`]. + pub fn new(status_code: StatusCode, message: Option) -> Error { + Self { + status: status_code, + detail: message.map(|m| m.to_string()), + } + } +} + +impl IntoResponse for Error { + fn into_response(self) -> axum::response::Response { + match self.detail { + Some(detail) => (self.status, detail).into_response(), + _ => (self.status,).into_response(), + } + } +} + +impl From for Error { + fn from(value: QueryRejection) -> Self { + Self::new(StatusCode::BAD_REQUEST, Some(value)) + } +} + +impl From for Error { + fn from(value: ExtensionRejection) -> Self { + Self::new(StatusCode::BAD_REQUEST, Some(value)) + } +} + +impl From for Error { + fn from(value: PathRejection) -> Self { + Self::new(StatusCode::BAD_REQUEST, Some(value)) + } +} + +impl From for Error { + fn from(value: std::io::Error) -> Self { + Self::new(StatusCode::INTERNAL_SERVER_ERROR, Some(value)) + } +} + +// === Pubky specific errors === + +impl From for Error { + fn from(value: AuthnSignatureError) -> Self { + Self::new(StatusCode::BAD_REQUEST, Some(value)) + } +} + +impl From for Error { + fn from(value: pkarr::Error) -> Self { + Self::new(StatusCode::BAD_REQUEST, Some(value)) + } +} diff --git a/pubky-homeserver/src/extractors.rs b/pubky-homeserver/src/extractors.rs index b637b97..bedd827 100644 --- a/pubky-homeserver/src/extractors.rs +++ b/pubky-homeserver/src/extractors.rs @@ -10,6 +10,8 @@ use axum::{ use pkarr::PublicKey; +use crate::error::{Error, Result}; + #[derive(Debug)] pub struct Pubky(PublicKey); @@ -32,11 +34,11 @@ where let pubky_id = params .get("pubky") - .ok_or_else(|| (StatusCode::NOT_FOUND, "Pubky not found").into_response())?; + .ok_or_else(|| (StatusCode::NOT_FOUND, "pubky param missing").into_response())?; let public_key = PublicKey::try_from(pubky_id.to_string()) - // TODO: convert Pkarr errors to app errors, in this case a params validation error - .map_err(|_| (StatusCode::BAD_REQUEST, "Invalid Pubky").into_response())?; + .map_err(Error::try_from) + .map_err(IntoResponse::into_response)?; Ok(Pubky(public_key)) } diff --git a/pubky-homeserver/src/lib.rs b/pubky-homeserver/src/lib.rs index 0ca9554..eb51596 100644 --- a/pubky-homeserver/src/lib.rs +++ b/pubky-homeserver/src/lib.rs @@ -1,4 +1,7 @@ +#![allow(unused)] + pub mod config; +mod error; mod extractors; mod pkarr; mod routes; diff --git a/pubky-homeserver/src/routes.rs b/pubky-homeserver/src/routes.rs index ecd16e4..7288221 100644 --- a/pubky-homeserver/src/routes.rs +++ b/pubky-homeserver/src/routes.rs @@ -1,12 +1,20 @@ -use axum::{routing::get, Router}; +use axum::{ + routing::{get, post, put}, + Router, +}; use tower_http::trace::TraceLayer; -pub mod drive; -pub mod root; +use crate::server::AppState; -pub fn create_app() -> Router { +mod auth; +mod drive; +mod root; + +pub fn create_app(state: AppState) -> Router { Router::new() .route("/", get(root::handler)) + .route("/:pubky", put(auth::signup)) .route("/:pubky/*key", get(drive::put)) .layer(TraceLayer::new_for_http()) + .with_state(state) } diff --git a/pubky-homeserver/src/routes/auth.rs b/pubky-homeserver/src/routes/auth.rs new file mode 100644 index 0000000..0fc333e --- /dev/null +++ b/pubky-homeserver/src/routes/auth.rs @@ -0,0 +1,16 @@ +use axum::{extract::State, response::IntoResponse}; +use bytes::Bytes; + +use crate::{error::Result, extractors::Pubky, server::AppState}; + +pub async fn signup( + State(state): State, + pubky: Pubky, + body: Bytes, +) -> Result { + state.verifier.verify(&body, pubky.public_key())?; + + // TODO: store account in database. + + Ok(()) +} diff --git a/pubky-homeserver/src/server.rs b/pubky-homeserver/src/server.rs index 215d67c..12fb90a 100644 --- a/pubky-homeserver/src/server.rs +++ b/pubky-homeserver/src/server.rs @@ -1,6 +1,7 @@ use std::{future::IntoFuture, net::SocketAddr}; use anyhow::{Error, Result}; +use pubky_common::auth::AuthnVerifier; use tokio::{net::TcpListener, signal, task::JoinSet}; use tracing::{info, warn}; @@ -13,14 +14,25 @@ use crate::{config::Config, pkarr::publish_server_packet}; #[derive(Debug)] pub struct Homeserver { - pub(crate) config: Config, port: u16, + config: Config, tasks: JoinSet>, } +#[derive(Clone, Debug)] +pub(crate) struct AppState { + pub verifier: AuthnVerifier, +} + impl Homeserver { pub async fn start(config: Config) -> Result { - let app = crate::routes::create_app(); + let public_key = config.keypair().public_key(); + + let state = AppState { + verifier: AuthnVerifier::new(public_key.clone()), + }; + + let app = crate::routes::create_app(state); let mut tasks = JoinSet::new(); @@ -53,10 +65,7 @@ impl Homeserver { publish_server_packet(pkarr_client, config.keypair(), config.domain(), port).await?; - info!( - "Homeserver listening on pubky://{}", - config.keypair().public_key() - ); + info!("Homeserver listening on pubky://{public_key}"); Ok(Self { tasks, diff --git a/pubky/Cargo.toml b/pubky/Cargo.toml index 321bea7..c01a897 100644 --- a/pubky/Cargo.toml +++ b/pubky/Cargo.toml @@ -10,6 +10,7 @@ pkarr = "2.1.0" ureq = "2.10.0" thiserror = "1.0.62" url = "2.5.2" +flume = { version = "0.11.0", features = ["select", "eventual-fairness"], default-features = false } [dev-dependencies] pubky_homeserver = { path = "../pubky-homeserver" } diff --git a/pubky/src/client.rs b/pubky/src/client.rs new file mode 100644 index 0000000..da0bc4f --- /dev/null +++ b/pubky/src/client.rs @@ -0,0 +1,262 @@ +use std::{collections::HashMap, fmt::format, time::Duration}; + +use pkarr::{ + dns::{rdata::SVCB, Packet}, + mainline::{dht::DhtSettings, Testnet}, + Keypair, PkarrClient, PublicKey, Settings, SignedPacket, +}; +use ureq::{Agent, Response}; +use url::Url; + +use pubky_common::auth::AuthnSignature; + +use crate::error::{Error, Result}; + +const MAX_RECURSIVE_PUBKY_HOMESERVER_RESOLUTION: u8 = 3; + +#[derive(Debug, Clone)] +pub struct PubkyClient { + agent: Agent, + pkarr: PkarrClient, +} + +impl PubkyClient { + pub fn new() -> Self { + Self { + agent: Agent::new(), + pkarr: PkarrClient::new(Default::default()).unwrap(), + } + } + + pub fn test(testnet: &Testnet) -> Self { + Self { + agent: Agent::new(), + pkarr: PkarrClient::new(Settings { + dht: DhtSettings { + request_timeout: Some(Duration::from_millis(10)), + bootstrap: Some(testnet.bootstrap.to_owned()), + ..DhtSettings::default() + }, + ..Settings::default() + }) + .unwrap(), + } + } + + // === Public Methods === + + /// Signup to a homeserver and update Pkarr accordingly. + /// + /// The homeserver is a Pkarr domain name, where the TLD is a Pkarr public key + /// for example "pubky.o4dksfbqk85ogzdb5osziw6befigbuxmuxkuxq8434q89uj56uyy" + pub fn signup(&self, keypair: &Keypair, homeserver: &str) -> Result<()> { + let (audience, mut url) = self.resolve_endpoint(homeserver)?; + + url.set_path(&format!("/{}", keypair.public_key())); + + self.request(HttpMethod::Put, &url) + .send_bytes(AuthnSignature::generate(keypair, &audience).as_bytes()) + .map_err(Box::new)?; + + self.publish_pubky_homeserver(keypair, homeserver); + + Ok(()) + } + + // === Private Methods === + + /// Publish the SVCB record for `_pubky.`. + pub(crate) fn publish_pubky_homeserver(&self, keypair: &Keypair, host: &str) -> Result<()> { + let mut packet = Packet::new_reply(0); + + if let Some(existing) = self.pkarr.resolve(&keypair.public_key())? { + for answer in existing.packet().answers.iter().cloned() { + if !answer.name.to_string().starts_with("_pubky") { + packet.answers.push(answer.into_owned()) + } + } + } + + let svcb = SVCB::new(0, host.try_into()?); + + packet.answers.push(pkarr::dns::ResourceRecord::new( + "_pubky".try_into().unwrap(), + pkarr::dns::CLASS::IN, + 60 * 60, + pkarr::dns::rdata::RData::SVCB(svcb), + )); + + let signed_packet = SignedPacket::from_packet(keypair, &packet)?; + + self.pkarr.publish(&signed_packet)?; + + Ok(()) + } + + /// Resolve the homeserver for a pubky. + pub(crate) fn resolve_pubky_homeserver(&self, pubky: &PublicKey) -> Result<(PublicKey, Url)> { + let target = format!("_pubky.{}", pubky); + + self.resolve_endpoint(&target) + .map_err(|_| Error::Generic("Could not resolve homeserver".to_string())) + } + + /// Resolve a service's public_key and clearnet url from a Pubky domain + fn resolve_endpoint(&self, target: &str) -> Result<(PublicKey, Url)> { + // TODO: cache the result of this function? + // TODO: use MAX_RECURSIVE_PUBKY_HOMESERVER_RESOLUTION + // TODO: move to common? + + let mut target = target.to_string(); + let mut homeserver_public_key = None; + let mut host = target.clone(); + + // PublicKey is very good at extracting the Pkarr TLD from a string. + while let Ok(public_key) = PublicKey::try_from(target.clone()) { + if let Some(signed_packet) = self.pkarr.resolve(&public_key)? { + let mut prior = None; + + for answer in signed_packet.resource_records(&target) { + if let pkarr::dns::rdata::RData::SVCB(svcb) = &answer.rdata { + if svcb.priority == 0 { + prior = Some(svcb) + } else if let Some(sofar) = prior { + if svcb.priority >= sofar.priority { + prior = Some(svcb) + } + // TODO return random if priority is the same + } else { + prior = Some(svcb) + } + } + } + + if let Some(svcb) = prior { + homeserver_public_key = Some(public_key); + target = svcb.target.to_string(); + + if let Some(port) = svcb.get_param(pkarr::dns::rdata::SVCB::PORT) { + if port.len() < 2 { + // TODO: debug! Error encoding port! + } + let port = u16::from_be_bytes([port[0], port[1]]); + + host = format!("{target}:{port}"); + } else { + host.clone_from(&target); + }; + + continue; + } + }; + + break; + } + + if let Some(homeserver) = homeserver_public_key { + let url = if host.starts_with("localhost") { + format!("http://{host}") + } else { + format!("https://{host}") + }; + + return Ok((homeserver, Url::parse(&url)?)); + } + + Err(Error::Generic("Could not resolve endpoint".to_string())) + } + + fn request(&self, method: HttpMethod, url: &Url) -> ureq::Request { + self.agent.request_url(method.into(), url) + } +} + +impl Default for PubkyClient { + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug, Clone)] +pub enum HttpMethod { + Get, + Put, + Post, +} + +impl From for &str { + fn from(value: HttpMethod) -> Self { + match value { + HttpMethod::Get => "GET", + HttpMethod::Put => "PUT", + HttpMethod::Post => "POST", + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use pkarr::{ + dns::{rdata::SVCB, Packet}, + mainline::{dht::DhtSettings, Testnet}, + Keypair, PkarrClient, Settings, SignedPacket, + }; + use pubky_homeserver::Homeserver; + + #[tokio::test] + async fn resolve_homeserver() { + let testnet = Testnet::new(3); + let server = Homeserver::start_test(&testnet).await.unwrap(); + + // Publish an intermediate controller of the homeserver + let pkarr_client = PkarrClient::new(Settings { + dht: DhtSettings { + bootstrap: Some(testnet.bootstrap.clone()), + ..Default::default() + }, + ..Default::default() + }) + .unwrap() + .as_async(); + + let intermediate = Keypair::random(); + + let mut packet = Packet::new_reply(0); + + let server_tld = server.public_key().to_string(); + + let mut svcb = SVCB::new(0, server_tld.as_str().try_into().unwrap()); + + packet.answers.push(pkarr::dns::ResourceRecord::new( + "pubky".try_into().unwrap(), + pkarr::dns::CLASS::IN, + 60 * 60, + pkarr::dns::rdata::RData::SVCB(svcb), + )); + + let signed_packet = SignedPacket::from_packet(&intermediate, &packet).unwrap(); + + pkarr_client.publish(&signed_packet).await.unwrap(); + + tokio::task::spawn_blocking(move || { + let client = PubkyClient::test(&testnet); + + let pubky = Keypair::random(); + + client + .publish_pubky_homeserver(&pubky, &format!("pubky.{}", &intermediate.public_key())); + + let (public_key, url) = client + .resolve_pubky_homeserver(&pubky.public_key()) + .unwrap(); + + assert_eq!(public_key, server.public_key()); + assert_eq!(url.host_str(), Some("localhost")); + assert_eq!(url.port(), Some(server.port())); + }) + .await + .expect("task failed") + } +} diff --git a/pubky/src/client_async.rs b/pubky/src/client_async.rs new file mode 100644 index 0000000..d99c301 --- /dev/null +++ b/pubky/src/client_async.rs @@ -0,0 +1,31 @@ +use std::thread; + +use pkarr::Keypair; + +use crate::{error::Result, PubkyClient}; + +pub struct PubkyClientAsync(PubkyClient); + +impl PubkyClient { + pub fn as_async(&self) -> PubkyClientAsync { + PubkyClientAsync(self.clone()) + } +} + +impl PubkyClientAsync { + /// Async version of [PubkyClient::signup] + pub async fn signup(&self, keypair: &Keypair, homeserver: &str) -> Result<()> { + let (sender, receiver) = flume::bounded::>(1); + + let client = self.0.clone(); + let keypair = keypair.clone(); + let homeserver = homeserver.to_string(); + + thread::spawn(move || { + let result = client.signup(&keypair, &homeserver); + sender.send(result) + }); + + receiver.recv_async().await? + } +} diff --git a/pubky/src/error.rs b/pubky/src/error.rs index 47ec28e..91ef471 100644 --- a/pubky/src/error.rs +++ b/pubky/src/error.rs @@ -17,4 +17,13 @@ pub enum Error { #[error(transparent)] Pkarr(#[from] pkarr::Error), + + #[error(transparent)] + Flume(#[from] flume::RecvError), + + #[error(transparent)] + Ureq(#[from] Box), + + #[error(transparent)] + Url(#[from] url::ParseError), } diff --git a/pubky/src/lib.rs b/pubky/src/lib.rs index 74cfd41..5cf602c 100644 --- a/pubky/src/lib.rs +++ b/pubky/src/lib.rs @@ -1,168 +1,10 @@ #![allow(unused)] -use std::{collections::HashMap, time::Duration}; - -use pkarr::{ - dns::{rdata::SVCB, Packet}, - mainline::{dht::DhtSettings, Testnet}, - Keypair, PkarrClient, PublicKey, Settings, SignedPacket, -}; -use ureq::{Agent, Response}; -use url::Url; - -use pubky_common::auth::AuthnSignature; - +mod client; +mod client_async; mod error; -use error::{Error, Result}; - -const MAX_RECURSIVE_PUBKY_HOMESERVER_RESOLUTION: u8 = 3; - -#[derive(Debug)] -pub struct Client { - agent: Agent, - pkarr: PkarrClient, -} - -impl Client { - pub fn new() -> Self { - Self { - agent: Agent::new(), - pkarr: PkarrClient::new(Default::default()).unwrap(), - } - } - - pub fn test(testnet: &Testnet) -> Self { - Self { - agent: Agent::new(), - pkarr: PkarrClient::new(Settings { - dht: DhtSettings { - request_timeout: Some(Duration::from_millis(10)), - bootstrap: Some(testnet.bootstrap.to_owned()), - ..DhtSettings::default() - }, - ..Settings::default() - }) - .unwrap(), - } - } - - // === Public Methods === - - // === Private Methods === - - /// Publish the SVCB record for `_pubky.`. - fn publish_pubky_homeserver(&self, keypair: &Keypair, host: &str) -> Result<()> { - let mut packet = Packet::new_reply(0); - - if let Some(existing) = self.pkarr.resolve(&keypair.public_key())? { - for answer in existing.packet().answers.iter().cloned() { - if !answer.name.to_string().starts_with("_pubky") { - packet.answers.push(answer.into_owned()) - } - } - } - - let mut svcb = SVCB::new(0, host.try_into()?); - - packet.answers.push(pkarr::dns::ResourceRecord::new( - "_pubky".try_into().unwrap(), - pkarr::dns::CLASS::IN, - 60 * 60, - pkarr::dns::rdata::RData::SVCB(svcb), - )); - - let signed_packet = SignedPacket::from_packet(keypair, &packet)?; - - self.pkarr.publish(&signed_packet)?; - - Ok(()) - } - - /// Resolve the homeserver for a pubky. - fn resolve_pubky_homeserver(&self, pubky: &PublicKey) -> Result<(PublicKey, String)> { - // TODO: cache the result of this function? - - let mut target = format!("_pubky.{}", pubky); - let mut homeserver_public_key = None; - let mut host = target.clone(); - - // PublicKey is very good at extracting the Pkarr TLD from a string. - while let Ok(public_key) = PublicKey::try_from(target.clone()) { - if let Some(signed_packet) = self.pkarr.resolve(&public_key)? { - let mut prior = None; - - for answer in signed_packet.resource_records(&target) { - if let pkarr::dns::rdata::RData::SVCB(svcb) = &answer.rdata { - if svcb.priority == 0 { - prior = Some(svcb) - } else if let Some(sofar) = prior { - if svcb.priority >= sofar.priority { - prior = Some(svcb) - } - // TODO return random if priority is the same - } else { - prior = Some(svcb) - } - } - } - - if let Some(svcb) = prior { - homeserver_public_key = Some(public_key); - target = svcb.target.to_string(); - if let Some(port) = svcb.get_param(pkarr::dns::rdata::SVCB::PORT) { - if port.len() < 2 { - // TODO: debug! Error encoding port! - } - let port = u16::from_be_bytes([port[0], port[1]]); - - host = format!("{target}:{port}"); - } else { - host.clone_from(&target); - }; - - continue; - } - }; - - break; - } - - if let Some(homeserver) = homeserver_public_key { - return Ok((homeserver, host)); - } - - Err(Error::Generic("Could not resolve homeserver".to_string())) - } - - fn fetch_direct(&self, method: HttpMethod, url: &str) -> Result { - self.agent - .request(method.into(), url) - .call() - .map_err(|_| Error::Generic("ureq error".to_string())) - } -} - -impl Default for Client { - fn default() -> Self { - Self::new() - } -} - -#[derive(Debug, Clone)] -pub enum HttpMethod { - GET, - PUT, -} - -impl From for &str { - fn from(value: HttpMethod) -> Self { - match value { - HttpMethod::GET => "GET", - HttpMethod::PUT => "PUT", - } - } -} +use client::PubkyClient; #[cfg(test)] mod tests { @@ -172,57 +14,17 @@ mod tests { use pubky_homeserver::Homeserver; #[tokio::test] - async fn resolve_homeserver() { + async fn basic_authn() { let testnet = Testnet::new(3); let server = Homeserver::start_test(&testnet).await.unwrap(); - // Publish an intermediate controller of the homeserver - let pkarr_client = PkarrClient::new(Settings { - dht: DhtSettings { - bootstrap: Some(testnet.bootstrap.clone()), - ..Default::default() - }, - ..Default::default() - }) - .unwrap() - .as_async(); - - let intermediate = Keypair::random(); - - let mut packet = Packet::new_reply(0); - - let server_tld = server.public_key().to_string(); - - let mut svcb = SVCB::new(0, server_tld.as_str().try_into().unwrap()); - - packet.answers.push(pkarr::dns::ResourceRecord::new( - "pubky".try_into().unwrap(), - pkarr::dns::CLASS::IN, - 60 * 60, - pkarr::dns::rdata::RData::SVCB(svcb), - )); - - let signed_packet = SignedPacket::from_packet(&intermediate, &packet).unwrap(); - - pkarr_client.publish(&signed_packet).await.unwrap(); - - tokio::task::spawn_blocking(move || { - let client = Client::test(&testnet); - - let pubky = Keypair::random(); - - client - .publish_pubky_homeserver(&pubky, &format!("pubky.{}", &intermediate.public_key())); + let client = PubkyClient::test(&testnet).as_async(); - let (public_key, host) = client - .resolve_pubky_homeserver(&pubky.public_key()) - .unwrap(); + let keypair = Keypair::random(); - assert_eq!(public_key, server.public_key()); - assert!(host.starts_with("localhost")); - assert!(host.ends_with(&server.port().to_string())); - }) - .await - .expect("task failed") + client + .signup(&keypair, &server.public_key().to_string()) + .await + .unwrap() } } From 8fbe0d5ae3813a9d01a3bdd419cbcde761796163 Mon Sep 17 00:00:00 2001 From: nazeh Date: Thu, 18 Jul 2024 10:51:05 +0300 Subject: [PATCH 07/13] feat(homeserver): add database --- Cargo.lock | 159 +++++++++++++++++++++++++++++++ pubky-homeserver/Cargo.toml | 1 + pubky-homeserver/src/database.rs | 19 ++++ pubky-homeserver/src/lib.rs | 1 + pubky-homeserver/src/server.rs | 6 +- 5 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 pubky-homeserver/src/database.rs diff --git a/Cargo.lock b/Cargo.lock index 1ad1678..38919d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -149,11 +149,23 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +dependencies = [ + "serde", +] [[package]] name = "blake3" @@ -183,6 +195,12 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.6.1" @@ -246,6 +264,21 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + [[package]] name = "crypto-common" version = "0.1.6" @@ -333,6 +366,15 @@ dependencies = [ "litrs", ] +[[package]] +name = "doxygen-rs" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "415b6ec780d34dcf624666747194393603d0373b7141eef01d12ee58881507d9" +dependencies = [ + "phf", +] + [[package]] name = "dyn-clone" version = "1.0.17" @@ -525,6 +567,44 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +[[package]] +name = "heed" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bc30da4a93ff8cb98e535d595d6de42731d4719d707bc1c86f579158751a24e" +dependencies = [ + "bitflags", + "byteorder", + "heed-traits", + "heed-types", + "libc", + "lmdb-master-sys", + "once_cell", + "page_size", + "serde", + "synchronoise", + "url", +] + +[[package]] +name = "heed-traits" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3130048d404c57ce5a1ac61a903696e8fcde7e8c2991e9fcfc1f27c3ef74ff" + +[[package]] +name = "heed-types" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d3f528b053a6d700b2734eabcd0fd49cb8230647aa72958467527b0b7917114" +dependencies = [ + "bincode", + "byteorder", + "heed-traits", + "serde", + "serde_json", +] + [[package]] name = "hermit-abi" version = "0.3.9" @@ -664,6 +744,17 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" +[[package]] +name = "lmdb-master-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57640c190703d5ccf4a86aff4aeb749b2d287a8cb1723c76b51f39d77ab53b24" +dependencies = [ + "cc", + "doxygen-rs", + "libc", +] + [[package]] name = "lock_api" version = "0.4.12" @@ -803,6 +894,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "parking_lot" version = "0.12.3" @@ -832,6 +933,48 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "1.1.5" @@ -950,6 +1093,7 @@ dependencies = [ "axum", "bytes", "dirs-next", + "heed", "pkarr", "pubky-common", "tokio", @@ -1278,6 +1422,12 @@ dependencies = [ "bitflags", ] +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "slab" version = "0.4.9" @@ -1351,6 +1501,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +[[package]] +name = "synchronoise" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dbc01390fc626ce8d1cffe3376ded2b72a11bb70e1c75f404a210e4daa4def2" +dependencies = [ + "crossbeam-queue", +] + [[package]] name = "thiserror" version = "1.0.62" diff --git a/pubky-homeserver/Cargo.toml b/pubky-homeserver/Cargo.toml index 954daea..08d3b80 100644 --- a/pubky-homeserver/Cargo.toml +++ b/pubky-homeserver/Cargo.toml @@ -8,6 +8,7 @@ anyhow = "1.0.82" axum = "0.7.5" bytes = "1.6.1" dirs-next = "2.0.0" +heed = "0.20.3" pkarr = { version = "2.1.0", features = ["async"] } pubky-common = { version = "0.1.0", path = "../pubky-common" } tokio = { version = "1.37.0", features = ["full"] } diff --git a/pubky-homeserver/src/database.rs b/pubky-homeserver/src/database.rs new file mode 100644 index 0000000..6332b0f --- /dev/null +++ b/pubky-homeserver/src/database.rs @@ -0,0 +1,19 @@ +use std::fs; +use std::path::Path; + +use heed::{Env, EnvOpenOptions}; + +#[derive(Debug, Clone)] +pub struct DB { + env: Env, +} + +impl DB { + pub fn open(storage: &Path) -> anyhow::Result { + fs::create_dir_all(storage).unwrap(); + + let env = unsafe { EnvOpenOptions::new().open(storage) }?; + + Ok(DB { env }) + } +} diff --git a/pubky-homeserver/src/lib.rs b/pubky-homeserver/src/lib.rs index eb51596..51852ec 100644 --- a/pubky-homeserver/src/lib.rs +++ b/pubky-homeserver/src/lib.rs @@ -1,6 +1,7 @@ #![allow(unused)] pub mod config; +mod database; mod error; mod extractors; mod pkarr; diff --git a/pubky-homeserver/src/server.rs b/pubky-homeserver/src/server.rs index 12fb90a..f167d05 100644 --- a/pubky-homeserver/src/server.rs +++ b/pubky-homeserver/src/server.rs @@ -10,7 +10,7 @@ use pkarr::{ PkarrClient, PublicKey, Settings, }; -use crate::{config::Config, pkarr::publish_server_packet}; +use crate::{config::Config, database::DB, pkarr::publish_server_packet}; #[derive(Debug)] pub struct Homeserver { @@ -22,14 +22,18 @@ pub struct Homeserver { #[derive(Clone, Debug)] pub(crate) struct AppState { pub verifier: AuthnVerifier, + pub db: DB, } impl Homeserver { pub async fn start(config: Config) -> Result { let public_key = config.keypair().public_key(); + let db = DB::open(&config.storage()?)?; + let state = AppState { verifier: AuthnVerifier::new(public_key.clone()), + db, }; let app = crate::routes::create_app(state); From c9ccbbb77cb8fcb691880e9b564ba0d120b8bdc2 Mon Sep 17 00:00:00 2001 From: nazeh Date: Thu, 18 Jul 2024 13:18:01 +0300 Subject: [PATCH 08/13] feat(homeserver): add Users table --- Cargo.lock | 70 +++++++++++++++++++ pubky-homeserver/Cargo.toml | 2 + pubky-homeserver/src/database.rs | 27 +++++-- pubky-homeserver/src/database/migrations.rs | 11 +++ pubky-homeserver/src/database/tables.rs | 1 + pubky-homeserver/src/database/tables/users.rs | 59 ++++++++++++++++ pubky-homeserver/src/error.rs | 33 +++++---- pubky-homeserver/src/routes/auth.rs | 20 +++++- 8 files changed, 205 insertions(+), 18 deletions(-) create mode 100644 pubky-homeserver/src/database/migrations.rs create mode 100644 pubky-homeserver/src/database/tables.rs create mode 100644 pubky-homeserver/src/database/tables/users.rs diff --git a/Cargo.lock b/Cargo.lock index 38919d1..d8be522 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -55,6 +55,15 @@ dependencies = [ "syn", ] +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + [[package]] name = "autocfg" version = "1.3.0" @@ -219,6 +228,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cobs" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" + [[package]] name = "const-oid" version = "0.9.6" @@ -264,6 +279,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "critical-section" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216" + [[package]] name = "crossbeam-queue" version = "0.3.11" @@ -406,6 +427,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -567,6 +594,29 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin", + "stable_deref_trait", +] + [[package]] name = "heed" version = "0.20.3" @@ -1043,6 +1093,18 @@ dependencies = [ "spki", ] +[[package]] +name = "postcard" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55c51ee6c0db07e68448e336cf8ea4131a620edefebf9893e759b2d793420f8" +dependencies = [ + "cobs", + "embedded-io", + "heapless", + "serde", +] + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -1095,7 +1157,9 @@ dependencies = [ "dirs-next", "heed", "pkarr", + "postcard", "pubky-common", + "serde", "tokio", "tower-http", "tracing", @@ -1472,6 +1536,12 @@ dependencies = [ "der", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "subtle" version = "2.6.1" diff --git a/pubky-homeserver/Cargo.toml b/pubky-homeserver/Cargo.toml index 08d3b80..d7bde5e 100644 --- a/pubky-homeserver/Cargo.toml +++ b/pubky-homeserver/Cargo.toml @@ -10,7 +10,9 @@ bytes = "1.6.1" dirs-next = "2.0.0" heed = "0.20.3" pkarr = { version = "2.1.0", features = ["async"] } +postcard = { version = "1.0.8", features = ["alloc"] } pubky-common = { version = "0.1.0", path = "../pubky-common" } +serde = { version = "1.0.204", features = ["derive"] } tokio = { version = "1.37.0", features = ["full"] } tower-http = { version = "0.5.2", features = ["cors", "trace"] } tracing = "0.1.40" diff --git a/pubky-homeserver/src/database.rs b/pubky-homeserver/src/database.rs index 6332b0f..4e4a57e 100644 --- a/pubky-homeserver/src/database.rs +++ b/pubky-homeserver/src/database.rs @@ -1,19 +1,38 @@ use std::fs; use std::path::Path; -use heed::{Env, EnvOpenOptions}; +use heed::{types::Str, Database, Env, EnvOpenOptions, RwTxn}; + +mod migrations; +pub mod tables; + +use migrations::TABLES_COUNT; #[derive(Debug, Clone)] pub struct DB { - env: Env, + pub(crate) env: Env, } impl DB { pub fn open(storage: &Path) -> anyhow::Result { fs::create_dir_all(storage).unwrap(); - let env = unsafe { EnvOpenOptions::new().open(storage) }?; + let env = unsafe { EnvOpenOptions::new().max_dbs(TABLES_COUNT).open(storage) }?; + + let db = DB { env }; + + db.run_migrations(); + + Ok(db) + } + + fn run_migrations(&self) -> anyhow::Result<()> { + let mut wtxn = self.env.write_txn()?; + + migrations::create_users_table(&self.env, &mut wtxn); + + wtxn.commit()?; - Ok(DB { env }) + Ok(()) } } diff --git a/pubky-homeserver/src/database/migrations.rs b/pubky-homeserver/src/database/migrations.rs new file mode 100644 index 0000000..428a2f4 --- /dev/null +++ b/pubky-homeserver/src/database/migrations.rs @@ -0,0 +1,11 @@ +use heed::{types::Str, Database, Env, RwTxn}; + +use super::tables; + +pub const TABLES_COUNT: u32 = 1; + +pub fn create_users_table(env: &Env, wtxn: &mut RwTxn) -> anyhow::Result<()> { + let _: tables::users::UsersTable = env.create_database(wtxn, None)?; + + Ok(()) +} diff --git a/pubky-homeserver/src/database/tables.rs b/pubky-homeserver/src/database/tables.rs new file mode 100644 index 0000000..913bd46 --- /dev/null +++ b/pubky-homeserver/src/database/tables.rs @@ -0,0 +1 @@ +pub mod users; diff --git a/pubky-homeserver/src/database/tables/users.rs b/pubky-homeserver/src/database/tables/users.rs new file mode 100644 index 0000000..e785245 --- /dev/null +++ b/pubky-homeserver/src/database/tables/users.rs @@ -0,0 +1,59 @@ +use std::{borrow::Cow, time::SystemTime}; + +use postcard::{from_bytes, to_allocvec}; +use pubky_common::timestamp::Timestamp; +use serde::{Deserialize, Serialize}; + +use heed::{types::Str, BoxedError, BytesDecode, BytesEncode, Database}; +use pkarr::PublicKey; + +extern crate alloc; +use alloc::vec::Vec; + +/// PublicKey => User. +pub type UsersTable = Database; + +pub const USERS_TABLE: &str = "users"; + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct User { + pub created_at: u64, +} + +impl<'a> BytesEncode<'a> for User { + type EItem = Self; + + fn bytes_encode(user: &Self::EItem) -> Result, BoxedError> { + let vec = to_allocvec(user).unwrap(); + + Ok(Cow::Owned(vec)) + } +} + +impl<'a> BytesDecode<'a> for User { + type DItem = Self; + + fn bytes_decode(bytes: &'a [u8]) -> Result { + let user: User = from_bytes(bytes).unwrap(); + + Ok(user) + } +} + +pub struct PublicKeyCodec {} + +impl<'a> BytesEncode<'a> for PublicKeyCodec { + type EItem = PublicKey; + + fn bytes_encode(pubky: &Self::EItem) -> Result, BoxedError> { + Ok(Cow::Borrowed(pubky.as_bytes())) + } +} + +impl<'a> BytesDecode<'a> for PublicKeyCodec { + type DItem = PublicKey; + + fn bytes_decode(bytes: &'a [u8]) -> Result { + Ok(PublicKey::try_from(bytes)?) + } +} diff --git a/pubky-homeserver/src/error.rs b/pubky-homeserver/src/error.rs index 2f92b7d..46d37d6 100644 --- a/pubky-homeserver/src/error.rs +++ b/pubky-homeserver/src/error.rs @@ -6,6 +6,7 @@ use axum::{ response::IntoResponse, }; use pubky_common::auth::AuthnSignatureError; +use tracing::debug; pub type Result = core::result::Result; @@ -52,39 +53,47 @@ impl IntoResponse for Error { } impl From for Error { - fn from(value: QueryRejection) -> Self { - Self::new(StatusCode::BAD_REQUEST, Some(value)) + fn from(error: QueryRejection) -> Self { + Self::new(StatusCode::BAD_REQUEST, Some(error)) } } impl From for Error { - fn from(value: ExtensionRejection) -> Self { - Self::new(StatusCode::BAD_REQUEST, Some(value)) + fn from(error: ExtensionRejection) -> Self { + Self::new(StatusCode::BAD_REQUEST, Some(error)) } } impl From for Error { - fn from(value: PathRejection) -> Self { - Self::new(StatusCode::BAD_REQUEST, Some(value)) + fn from(error: PathRejection) -> Self { + Self::new(StatusCode::BAD_REQUEST, Some(error)) } } impl From for Error { - fn from(value: std::io::Error) -> Self { - Self::new(StatusCode::INTERNAL_SERVER_ERROR, Some(value)) + fn from(error: std::io::Error) -> Self { + Self::new(StatusCode::INTERNAL_SERVER_ERROR, Some(error)) } } // === Pubky specific errors === impl From for Error { - fn from(value: AuthnSignatureError) -> Self { - Self::new(StatusCode::BAD_REQUEST, Some(value)) + fn from(error: AuthnSignatureError) -> Self { + Self::new(StatusCode::BAD_REQUEST, Some(error)) } } impl From for Error { - fn from(value: pkarr::Error) -> Self { - Self::new(StatusCode::BAD_REQUEST, Some(value)) + fn from(error: pkarr::Error) -> Self { + Self::new(StatusCode::BAD_REQUEST, Some(error)) + } +} + +impl From for Error { + fn from(error: heed::Error) -> Self { + debug!(?error); + + Self::with_status(StatusCode::INTERNAL_SERVER_ERROR) } } diff --git a/pubky-homeserver/src/routes/auth.rs b/pubky-homeserver/src/routes/auth.rs index 0fc333e..fdcb81b 100644 --- a/pubky-homeserver/src/routes/auth.rs +++ b/pubky-homeserver/src/routes/auth.rs @@ -1,7 +1,14 @@ use axum::{extract::State, response::IntoResponse}; use bytes::Bytes; -use crate::{error::Result, extractors::Pubky, server::AppState}; +use pubky_common::timestamp::Timestamp; + +use crate::{ + database::tables::users::{User, UsersTable, USERS_TABLE}, + error::Result, + extractors::Pubky, + server::AppState, +}; pub async fn signup( State(state): State, @@ -10,7 +17,16 @@ pub async fn signup( ) -> Result { state.verifier.verify(&body, pubky.public_key())?; - // TODO: store account in database. + let mut wtxn = state.db.env.write_txn()?; + let users: UsersTable = state.db.env.create_database(&mut wtxn, Some(USERS_TABLE))?; + + users.put( + &mut wtxn, + pubky.public_key(), + &User { + created_at: Timestamp::now().into_inner(), + }, + )?; Ok(()) } From 4f87c7d44405e347d085d48fcba1e7591736d7e1 Mon Sep 17 00:00:00 2001 From: nazeh Date: Sat, 20 Jul 2024 13:10:54 +0300 Subject: [PATCH 09/13] feat(homeserver): check session exists --- Cargo.lock | 153 +++++++++++++++++- pubky-common/src/crypto.rs | 9 ++ pubky-homeserver/Cargo.toml | 3 + pubky-homeserver/src/database.rs | 1 + pubky-homeserver/src/database/migrations.rs | 12 +- pubky-homeserver/src/database/tables.rs | 1 + .../src/database/tables/sessions.rs | 42 +++++ pubky-homeserver/src/routes.rs | 3 + pubky-homeserver/src/routes/auth.rs | 82 +++++++++- pubky/src/client.rs | 14 ++ pubky/src/client_async.rs | 17 +- pubky/src/lib.rs | 4 +- 12 files changed, 328 insertions(+), 13 deletions(-) create mode 100644 pubky-homeserver/src/database/tables/sessions.rs diff --git a/Cargo.lock b/Cargo.lock index d8be522..009e584 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -125,6 +125,29 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-extra" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0be6ea09c9b96cb5076af0de2e383bd2bc0c18f827cf1967bdd353e0b910d733" +dependencies = [ + "axum", + "axum-core", + "bytes", + "futures-util", + "headers", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "serde", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "backtrace" version = "0.3.73" @@ -142,9 +165,15 @@ dependencies = [ [[package]] name = "base32" -version = "0.5.0" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" + +[[package]] +name = "base64" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1ce0365f4d5fb6646220bb52fe547afd51796d90f914d4063cb0b032ebee088" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64" @@ -246,6 +275,17 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + [[package]] name = "cpufeatures" version = "0.2.12" @@ -347,6 +387,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + [[package]] name = "digest" version = "0.10.7" @@ -603,6 +652,30 @@ dependencies = [ "byteorder", ] +[[package]] +name = "headers" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" +dependencies = [ + "base64 0.21.7", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http", +] + [[package]] name = "heapless" version = "0.7.17" @@ -913,6 +986,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num_cpus" version = "1.16.0" @@ -1105,6 +1184,12 @@ dependencies = [ "serde", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -1153,6 +1238,8 @@ version = "0.1.0" dependencies = [ "anyhow", "axum", + "axum-extra", + "base32", "bytes", "dirs-next", "heed", @@ -1161,6 +1248,7 @@ dependencies = [ "pubky-common", "serde", "tokio", + "tower-cookies", "tower-http", "tracing", "tracing-subscriber", @@ -1433,6 +1521,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha1_smol" version = "1.0.0" @@ -1610,6 +1709,37 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinyvec" version = "1.8.0" @@ -1671,6 +1801,23 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-cookies" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fd0118512cf0b3768f7fcccf0bef1ae41d68f2b45edc1e77432b36c97c56c6d" +dependencies = [ + "async-trait", + "axum-core", + "cookie", + "futures-util", + "http", + "parking_lot", + "pin-project-lite", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-http" version = "0.5.2" @@ -1801,7 +1948,7 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72139d247e5f97a3eff96229a7ae85ead5328a39efe76f8bf5a06313d505b6ea" dependencies = [ - "base64", + "base64 0.22.1", "flate2", "log", "once_cell", diff --git a/pubky-common/src/crypto.rs b/pubky-common/src/crypto.rs index 8bb700a..b51181e 100644 --- a/pubky-common/src/crypto.rs +++ b/pubky-common/src/crypto.rs @@ -12,3 +12,12 @@ pub fn random_hash() -> Hash { let mut rng = rand::thread_rng(); Hash::from_bytes(rng.gen()) } + +pub fn random_bytes() -> [u8; N] { + let mut rng = rand::thread_rng(); + let mut arr = [0u8; N]; + for i in 0..N { + arr[i] = rng.gen(); + } + arr +} diff --git a/pubky-homeserver/Cargo.toml b/pubky-homeserver/Cargo.toml index d7bde5e..33e18ab 100644 --- a/pubky-homeserver/Cargo.toml +++ b/pubky-homeserver/Cargo.toml @@ -6,6 +6,8 @@ edition = "2021" [dependencies] anyhow = "1.0.82" axum = "0.7.5" +axum-extra = { version = "0.9.3", features = ["typed-header"] } +base32 = "0.5.1" bytes = "1.6.1" dirs-next = "2.0.0" heed = "0.20.3" @@ -14,6 +16,7 @@ postcard = { version = "1.0.8", features = ["alloc"] } pubky-common = { version = "0.1.0", path = "../pubky-common" } serde = { version = "1.0.204", features = ["derive"] } tokio = { version = "1.37.0", features = ["full"] } +tower-cookies = "0.10.0" tower-http = { version = "0.5.2", features = ["cors", "trace"] } tracing = "0.1.40" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } diff --git a/pubky-homeserver/src/database.rs b/pubky-homeserver/src/database.rs index 4e4a57e..2f8d591 100644 --- a/pubky-homeserver/src/database.rs +++ b/pubky-homeserver/src/database.rs @@ -30,6 +30,7 @@ impl DB { let mut wtxn = self.env.write_txn()?; migrations::create_users_table(&self.env, &mut wtxn); + migrations::create_sessions_table(&self.env, &mut wtxn); wtxn.commit()?; diff --git a/pubky-homeserver/src/database/migrations.rs b/pubky-homeserver/src/database/migrations.rs index 428a2f4..93c7631 100644 --- a/pubky-homeserver/src/database/migrations.rs +++ b/pubky-homeserver/src/database/migrations.rs @@ -2,10 +2,18 @@ use heed::{types::Str, Database, Env, RwTxn}; use super::tables; -pub const TABLES_COUNT: u32 = 1; +pub const TABLES_COUNT: u32 = 2; pub fn create_users_table(env: &Env, wtxn: &mut RwTxn) -> anyhow::Result<()> { - let _: tables::users::UsersTable = env.create_database(wtxn, None)?; + let _: tables::users::UsersTable = + env.create_database(wtxn, Some(tables::users::USERS_TABLE))?; + + Ok(()) +} + +pub fn create_sessions_table(env: &Env, wtxn: &mut RwTxn) -> anyhow::Result<()> { + let _: tables::sessions::SessionsTable = + env.create_database(wtxn, Some(tables::sessions::SESSIONS_TABLE))?; Ok(()) } diff --git a/pubky-homeserver/src/database/tables.rs b/pubky-homeserver/src/database/tables.rs index 913bd46..b6e3efc 100644 --- a/pubky-homeserver/src/database/tables.rs +++ b/pubky-homeserver/src/database/tables.rs @@ -1 +1,2 @@ +pub mod sessions; pub mod users; diff --git a/pubky-homeserver/src/database/tables/sessions.rs b/pubky-homeserver/src/database/tables/sessions.rs new file mode 100644 index 0000000..bdb83d1 --- /dev/null +++ b/pubky-homeserver/src/database/tables/sessions.rs @@ -0,0 +1,42 @@ +use std::{borrow::Cow, time::SystemTime}; + +use postcard::{from_bytes, to_allocvec}; +use pubky_common::timestamp::Timestamp; +use serde::{Deserialize, Serialize}; + +use heed::{types::Bytes, BoxedError, BytesDecode, BytesEncode, Database}; +use pkarr::PublicKey; + +extern crate alloc; +use alloc::vec::Vec; + +/// session secret => Session. +pub type SessionsTable = Database; + +pub const SESSIONS_TABLE: &str = "sessions"; + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct Session { + pub created_at: u64, + pub name: String, +} + +impl<'a> BytesEncode<'a> for Session { + type EItem = Self; + + fn bytes_encode(session: &Self::EItem) -> Result, BoxedError> { + let vec = to_allocvec(session)?; + + Ok(Cow::Owned(vec)) + } +} + +impl<'a> BytesDecode<'a> for Session { + type DItem = Self; + + fn bytes_decode(bytes: &'a [u8]) -> Result { + let sesison: Session = from_bytes(bytes).unwrap(); + + Ok(sesison) + } +} diff --git a/pubky-homeserver/src/routes.rs b/pubky-homeserver/src/routes.rs index 7288221..75da602 100644 --- a/pubky-homeserver/src/routes.rs +++ b/pubky-homeserver/src/routes.rs @@ -2,6 +2,7 @@ use axum::{ routing::{get, post, put}, Router, }; +use tower_cookies::CookieManagerLayer; use tower_http::trace::TraceLayer; use crate::server::AppState; @@ -14,7 +15,9 @@ pub fn create_app(state: AppState) -> Router { Router::new() .route("/", get(root::handler)) .route("/:pubky", put(auth::signup)) + .route("/:pubky/session", get(auth::session)) .route("/:pubky/*key", get(drive::put)) .layer(TraceLayer::new_for_http()) + .layer(CookieManagerLayer::new()) .with_state(state) } diff --git a/pubky-homeserver/src/routes/auth.rs b/pubky-homeserver/src/routes/auth.rs index fdcb81b..f3c977d 100644 --- a/pubky-homeserver/src/routes/auth.rs +++ b/pubky-homeserver/src/routes/auth.rs @@ -1,32 +1,102 @@ -use axum::{extract::State, response::IntoResponse}; +use axum::{ + extract::{Request, State}, + http::{HeaderMap, StatusCode}, + response::IntoResponse, + routing::get, + Router, +}; +use axum_extra::{headers::UserAgent, TypedHeader}; use bytes::Bytes; +use tower_cookies::{Cookie, Cookies}; -use pubky_common::timestamp::Timestamp; +use pubky_common::{ + crypto::{random_bytes, random_hash}, + timestamp::Timestamp, +}; use crate::{ - database::tables::users::{User, UsersTable, USERS_TABLE}, - error::Result, + database::tables::{ + sessions::{Session, SessionsTable, SESSIONS_TABLE}, + users::{User, UsersTable, USERS_TABLE}, + }, + error::{Error, Result}, extractors::Pubky, server::AppState, }; pub async fn signup( State(state): State, + TypedHeader(user_agent): TypedHeader, + cookies: Cookies, pubky: Pubky, body: Bytes, ) -> Result { - state.verifier.verify(&body, pubky.public_key())?; + let public_key = pubky.public_key(); + + state.verifier.verify(&body, public_key)?; let mut wtxn = state.db.env.write_txn()?; let users: UsersTable = state.db.env.create_database(&mut wtxn, Some(USERS_TABLE))?; users.put( &mut wtxn, - pubky.public_key(), + public_key, &User { created_at: Timestamp::now().into_inner(), }, )?; + let session_secret = random_bytes::<16>(); + + let sessions: SessionsTable = state + .db + .env + .open_database(&wtxn, Some(SESSIONS_TABLE))? + .expect("Sessions table already created"); + + // TODO: handle not having a user agent? + let session = &Session { + created_at: Timestamp::now().into_inner(), + name: user_agent.to_string(), + }; + + sessions.put(&mut wtxn, &session_secret, session)?; + + cookies.add(Cookie::new( + public_key.to_string(), + base32::encode(base32::Alphabet::Crockford, &session_secret), + )); + + wtxn.commit()?; + Ok(()) } + +pub async fn session( + State(state): State, + TypedHeader(user_agent): TypedHeader, + cookies: Cookies, + pubky: Pubky, +) -> Result { + if let Some(cookie) = cookies.get(&pubky.public_key().to_string()) { + let rtxn = state.db.env.read_txn()?; + + let sessions: SessionsTable = state + .db + .env + .open_database(&rtxn, Some(SESSIONS_TABLE))? + .expect("Session table already created"); + + if let Some(session) = sessions.get( + &rtxn, + &base32::decode(base32::Alphabet::Crockford, cookie.value()).unwrap_or_default(), + )? { + rtxn.commit()?; + return Ok(()); + }; + + rtxn.commit()?; + }; + + Err(Error::with_status(StatusCode::NOT_FOUND)) +} diff --git a/pubky/src/client.rs b/pubky/src/client.rs index da0bc4f..e05a051 100644 --- a/pubky/src/client.rs +++ b/pubky/src/client.rs @@ -63,6 +63,20 @@ impl PubkyClient { Ok(()) } + /// Check the current sesison for a given Pubky in its homeserver. + pub fn session(&self, pubky: &PublicKey) -> Result<()> { + let (homeserver, mut url) = self.resolve_pubky_homeserver(pubky)?; + + url.set_path(&format!("/{}/sesison", pubky)); + + let response = self + .request(HttpMethod::Get, &url) + .call() + .map_err(Box::new)?; + + Ok(()) + } + // === Private Methods === /// Publish the SVCB record for `_pubky.`. diff --git a/pubky/src/client_async.rs b/pubky/src/client_async.rs index d99c301..297dd00 100644 --- a/pubky/src/client_async.rs +++ b/pubky/src/client_async.rs @@ -1,6 +1,6 @@ use std::thread; -use pkarr::Keypair; +use pkarr::{Keypair, PublicKey}; use crate::{error::Result, PubkyClient}; @@ -28,4 +28,19 @@ impl PubkyClientAsync { receiver.recv_async().await? } + + /// Async version of [PubkyClient::session] + pub async fn session(&self, pubky: &PublicKey) -> Result<()> { + let (sender, receiver) = flume::bounded::>(1); + + let client = self.0.clone(); + let pubky = pubky.clone(); + + thread::spawn(move || { + let result = client.session(&pubky); + sender.send(result) + }); + + receiver.recv_async().await? + } } diff --git a/pubky/src/lib.rs b/pubky/src/lib.rs index 5cf602c..2684bd5 100644 --- a/pubky/src/lib.rs +++ b/pubky/src/lib.rs @@ -25,6 +25,8 @@ mod tests { client .signup(&keypair, &server.public_key().to_string()) .await - .unwrap() + .unwrap(); + + let session = client.session(&keypair.public_key()).await.unwrap(); } } From 6aa64aeb928cfd13501a16deac3d6f431cae4541 Mon Sep 17 00:00:00 2001 From: nazeh Date: Sat, 20 Jul 2024 18:13:23 +0300 Subject: [PATCH 10/13] fix(homeserver): return session from /:pubky/sesison --- Cargo.lock | 41 +++++++++++++++++++ .../src/database/tables/sessions.rs | 5 ++- pubky-homeserver/src/routes/auth.rs | 11 ++++- pubky/Cargo.toml | 2 +- pubky/src/client.rs | 15 +++++-- 5 files changed, 67 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 009e584..ea7076f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -286,6 +286,23 @@ dependencies = [ "version_check", ] +[[package]] +name = "cookie_store" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4934e6b7e8419148b6ef56950d277af8561060b56afd59e2aadf98b59fce6baa" +dependencies = [ + "cookie", + "idna", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + [[package]] name = "cpufeatures" version = "0.2.12" @@ -482,6 +499,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -652,6 +675,12 @@ dependencies = [ "byteorder", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "headers" version = "0.4.0" @@ -824,6 +853,16 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "itoa" version = "1.0.11" @@ -1949,6 +1988,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72139d247e5f97a3eff96229a7ae85ead5328a39efe76f8bf5a06313d505b6ea" dependencies = [ "base64 0.22.1", + "cookie", + "cookie_store", "flate2", "log", "once_cell", diff --git a/pubky-homeserver/src/database/tables/sessions.rs b/pubky-homeserver/src/database/tables/sessions.rs index bdb83d1..f025d69 100644 --- a/pubky-homeserver/src/database/tables/sessions.rs +++ b/pubky-homeserver/src/database/tables/sessions.rs @@ -15,10 +15,13 @@ pub type SessionsTable = Database; pub const SESSIONS_TABLE: &str = "sessions"; +// TODO: add IP address? #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] pub struct Session { pub created_at: u64, - pub name: String, + /// User specified name, defaults to the user-agent. + pub name: Option, + pub user_agent: String, } impl<'a> BytesEncode<'a> for Session { diff --git a/pubky-homeserver/src/routes/auth.rs b/pubky-homeserver/src/routes/auth.rs index f3c977d..b41dcc6 100644 --- a/pubky-homeserver/src/routes/auth.rs +++ b/pubky-homeserver/src/routes/auth.rs @@ -7,6 +7,8 @@ use axum::{ }; use axum_extra::{headers::UserAgent, TypedHeader}; use bytes::Bytes; +use heed::BytesEncode; +use postcard::to_allocvec; use tower_cookies::{Cookie, Cookies}; use pubky_common::{ @@ -57,7 +59,8 @@ pub async fn signup( // TODO: handle not having a user agent? let session = &Session { created_at: Timestamp::now().into_inner(), - name: user_agent.to_string(), + user_agent: user_agent.to_string(), + name: None, }; sessions.put(&mut wtxn, &session_secret, session)?; @@ -92,7 +95,11 @@ pub async fn session( &base32::decode(base32::Alphabet::Crockford, cookie.value()).unwrap_or_default(), )? { rtxn.commit()?; - return Ok(()); + // TODO: avoid decoding then encoding sesison + let x: Vec = to_allocvec::(&session) + .map_err(|_| Error::with_status(StatusCode::INTERNAL_SERVER_ERROR))?; + + return Ok(x); }; rtxn.commit()?; diff --git a/pubky/Cargo.toml b/pubky/Cargo.toml index c01a897..6fe2f19 100644 --- a/pubky/Cargo.toml +++ b/pubky/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" pubky-common = { version = "0.1.0", path = "../pubky-common" } pkarr = "2.1.0" -ureq = "2.10.0" +ureq = { version = "2.10.0", features = ["cookies"] } thiserror = "1.0.62" url = "2.5.2" flume = { version = "0.11.0", features = ["select", "eventual-fairness"], default-features = false } diff --git a/pubky/src/client.rs b/pubky/src/client.rs index e05a051..36c887a 100644 --- a/pubky/src/client.rs +++ b/pubky/src/client.rs @@ -65,14 +65,23 @@ impl PubkyClient { /// Check the current sesison for a given Pubky in its homeserver. pub fn session(&self, pubky: &PublicKey) -> Result<()> { + // TODO: use https://crates.io/crates/user-agent-parser to parse the session + // and get more informations from the user-agent. + let (homeserver, mut url) = self.resolve_pubky_homeserver(pubky)?; - url.set_path(&format!("/{}/sesison", pubky)); + url.set_path(&format!("/{}/session", pubky)); + + let mut bytes = vec![]; - let response = self + let reader = self .request(HttpMethod::Get, &url) .call() - .map_err(Box::new)?; + .map_err(Box::new)? + .into_reader() + .read_to_end(&mut bytes); + + // TODO: return the decoded session Ok(()) } From 2c38e8e071e31720fe16e44fdf1429811ad86770 Mon Sep 17 00:00:00 2001 From: nazeh Date: Sun, 21 Jul 2024 10:23:11 +0300 Subject: [PATCH 11/13] feat(pubky): return Session struct --- Cargo.lock | 2 + pubky-common/Cargo.toml | 2 + pubky-common/src/lib.rs | 1 + pubky-common/src/session.rs | 84 +++++++++++++++++++ .../src/database/tables/sessions.rs | 44 ++-------- pubky-homeserver/src/routes/auth.rs | 31 +++---- pubky/Cargo.toml | 5 ++ pubky/src/client.rs | 11 +-- pubky/src/client_async.rs | 15 ++-- pubky/src/error.rs | 3 + pubky/src/lib.rs | 3 + 11 files changed, 124 insertions(+), 77 deletions(-) create mode 100644 pubky-common/src/session.rs diff --git a/Cargo.lock b/Cargo.lock index ea7076f..26bae45 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1267,7 +1267,9 @@ dependencies = [ "ed25519-dalek", "once_cell", "pkarr", + "postcard", "rand", + "serde", "thiserror", ] diff --git a/pubky-common/Cargo.toml b/pubky-common/Cargo.toml index 8f73315..1b7111c 100644 --- a/pubky-common/Cargo.toml +++ b/pubky-common/Cargo.toml @@ -13,3 +13,5 @@ once_cell = "1.19.0" pkarr = "2.1.0" rand = "0.8.5" thiserror = "1.0.60" +postcard = { version = "1.0.8", features = ["alloc"] } +serde = { version = "1.0.204", features = ["derive"] } diff --git a/pubky-common/src/lib.rs b/pubky-common/src/lib.rs index 90b246d..cedc227 100644 --- a/pubky-common/src/lib.rs +++ b/pubky-common/src/lib.rs @@ -1,4 +1,5 @@ pub mod auth; pub mod crypto; pub mod namespaces; +pub mod session; pub mod timestamp; diff --git a/pubky-common/src/session.rs b/pubky-common/src/session.rs new file mode 100644 index 0000000..9ef3c9d --- /dev/null +++ b/pubky-common/src/session.rs @@ -0,0 +1,84 @@ +use postcard::{from_bytes, to_allocvec}; +use serde::{Deserialize, Serialize}; + +extern crate alloc; +use alloc::vec::Vec; + +use crate::timestamp::Timestamp; + +// TODO: add IP address? +// TODO: use https://crates.io/crates/user-agent-parser to parse the session +// and get more informations from the user-agent. +#[derive(Clone, Default, Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct Session { + pub version: usize, + pub created_at: u64, + /// User specified name, defaults to the user-agent. + pub name: String, + pub user_agent: String, +} + +impl Session { + pub fn new() -> Self { + Self { + created_at: Timestamp::now().into_inner(), + ..Default::default() + } + } + + // === Setters === + + pub fn set_user_agent(&mut self, user_agent: String) -> &mut Self { + self.user_agent = user_agent; + + if self.name.is_empty() { + self.name.clone_from(&self.user_agent) + } + + self + } + + // === Public Methods === + + pub fn serialize(&self) -> Vec { + to_allocvec(self).expect("Session::serialize") + } + + pub fn deserialize(bytes: &[u8]) -> Result { + if bytes[0] > 0 { + return Err(Error::UnknownVersion); + } + + Ok(from_bytes(bytes)?) + } +} + +pub type Result = core::result::Result; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("Unknown version")] + UnknownVersion, + #[error(transparent)] + Postcard(#[from] postcard::Error), +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serialize() { + let mut session = Session::default(); + + session.user_agent = "foo".to_string(); + + let serialized = session.serialize(); + + assert_eq!(serialized, [0, 0, 0, 3, 102, 111, 111,]); + + let deseiralized = Session::deserialize(&serialized).unwrap(); + + assert_eq!(deseiralized, session) + } +} diff --git a/pubky-homeserver/src/database/tables/sessions.rs b/pubky-homeserver/src/database/tables/sessions.rs index f025d69..6e9d8f8 100644 --- a/pubky-homeserver/src/database/tables/sessions.rs +++ b/pubky-homeserver/src/database/tables/sessions.rs @@ -1,45 +1,11 @@ use std::{borrow::Cow, time::SystemTime}; -use postcard::{from_bytes, to_allocvec}; -use pubky_common::timestamp::Timestamp; -use serde::{Deserialize, Serialize}; - -use heed::{types::Bytes, BoxedError, BytesDecode, BytesEncode, Database}; -use pkarr::PublicKey; - -extern crate alloc; -use alloc::vec::Vec; +use heed::{ + types::{Bytes, Str}, + BoxedError, BytesDecode, BytesEncode, Database, +}; /// session secret => Session. -pub type SessionsTable = Database; +pub type SessionsTable = Database; pub const SESSIONS_TABLE: &str = "sessions"; - -// TODO: add IP address? -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -pub struct Session { - pub created_at: u64, - /// User specified name, defaults to the user-agent. - pub name: Option, - pub user_agent: String, -} - -impl<'a> BytesEncode<'a> for Session { - type EItem = Self; - - fn bytes_encode(session: &Self::EItem) -> Result, BoxedError> { - let vec = to_allocvec(session)?; - - Ok(Cow::Owned(vec)) - } -} - -impl<'a> BytesDecode<'a> for Session { - type DItem = Self; - - fn bytes_decode(bytes: &'a [u8]) -> Result { - let sesison: Session = from_bytes(bytes).unwrap(); - - Ok(sesison) - } -} diff --git a/pubky-homeserver/src/routes/auth.rs b/pubky-homeserver/src/routes/auth.rs index b41dcc6..fc11793 100644 --- a/pubky-homeserver/src/routes/auth.rs +++ b/pubky-homeserver/src/routes/auth.rs @@ -13,12 +13,13 @@ use tower_cookies::{Cookie, Cookies}; use pubky_common::{ crypto::{random_bytes, random_hash}, + session::Session, timestamp::Timestamp, }; use crate::{ database::tables::{ - sessions::{Session, SessionsTable, SESSIONS_TABLE}, + sessions::{SessionsTable, SESSIONS_TABLE}, users::{User, UsersTable, USERS_TABLE}, }, error::{Error, Result}, @@ -48,7 +49,7 @@ pub async fn signup( }, )?; - let session_secret = random_bytes::<16>(); + let session_secret = base32::encode(base32::Alphabet::Crockford, &random_bytes::<16>()); let sessions: SessionsTable = state .db @@ -57,18 +58,13 @@ pub async fn signup( .expect("Sessions table already created"); // TODO: handle not having a user agent? - let session = &Session { - created_at: Timestamp::now().into_inner(), - user_agent: user_agent.to_string(), - name: None, - }; + let mut session = Session::new(); + + session.set_user_agent(user_agent.to_string()); - sessions.put(&mut wtxn, &session_secret, session)?; + sessions.put(&mut wtxn, &session_secret, &session.serialize())?; - cookies.add(Cookie::new( - public_key.to_string(), - base32::encode(base32::Alphabet::Crockford, &session_secret), - )); + cookies.add(Cookie::new(public_key.to_string(), session_secret)); wtxn.commit()?; @@ -90,16 +86,11 @@ pub async fn session( .open_database(&rtxn, Some(SESSIONS_TABLE))? .expect("Session table already created"); - if let Some(session) = sessions.get( - &rtxn, - &base32::decode(base32::Alphabet::Crockford, cookie.value()).unwrap_or_default(), - )? { + if let Some(session) = sessions.get(&rtxn, cookie.value())? { + let session = session.to_owned(); rtxn.commit()?; - // TODO: avoid decoding then encoding sesison - let x: Vec = to_allocvec::(&session) - .map_err(|_| Error::with_status(StatusCode::INTERNAL_SERVER_ERROR))?; - return Ok(x); + return Ok(session); }; rtxn.commit()?; diff --git a/pubky/Cargo.toml b/pubky/Cargo.toml index 6fe2f19..e10789a 100644 --- a/pubky/Cargo.toml +++ b/pubky/Cargo.toml @@ -15,3 +15,8 @@ flume = { version = "0.11.0", features = ["select", "eventual-fairness"], defaul [dev-dependencies] pubky_homeserver = { path = "../pubky-homeserver" } tokio = "1.37.0" + +[features] +async = ["flume/async"] + +default = ["async"] diff --git a/pubky/src/client.rs b/pubky/src/client.rs index 36c887a..0ad9581 100644 --- a/pubky/src/client.rs +++ b/pubky/src/client.rs @@ -8,7 +8,7 @@ use pkarr::{ use ureq::{Agent, Response}; use url::Url; -use pubky_common::auth::AuthnSignature; +use pubky_common::{auth::AuthnSignature, session::Session}; use crate::error::{Error, Result}; @@ -64,10 +64,7 @@ impl PubkyClient { } /// Check the current sesison for a given Pubky in its homeserver. - pub fn session(&self, pubky: &PublicKey) -> Result<()> { - // TODO: use https://crates.io/crates/user-agent-parser to parse the session - // and get more informations from the user-agent. - + pub fn session(&self, pubky: &PublicKey) -> Result { let (homeserver, mut url) = self.resolve_pubky_homeserver(pubky)?; url.set_path(&format!("/{}/session", pubky)); @@ -81,9 +78,7 @@ impl PubkyClient { .into_reader() .read_to_end(&mut bytes); - // TODO: return the decoded session - - Ok(()) + Ok(Session::deserialize(&bytes)?) } // === Private Methods === diff --git a/pubky/src/client_async.rs b/pubky/src/client_async.rs index 297dd00..370c5a7 100644 --- a/pubky/src/client_async.rs +++ b/pubky/src/client_async.rs @@ -1,6 +1,7 @@ use std::thread; use pkarr::{Keypair, PublicKey}; +use pubky_common::session::Session; use crate::{error::Result, PubkyClient}; @@ -21,25 +22,19 @@ impl PubkyClientAsync { let keypair = keypair.clone(); let homeserver = homeserver.to_string(); - thread::spawn(move || { - let result = client.signup(&keypair, &homeserver); - sender.send(result) - }); + thread::spawn(move || sender.send(client.signup(&keypair, &homeserver))); receiver.recv_async().await? } /// Async version of [PubkyClient::session] - pub async fn session(&self, pubky: &PublicKey) -> Result<()> { - let (sender, receiver) = flume::bounded::>(1); + pub async fn session(&self, pubky: &PublicKey) -> Result { + let (sender, receiver) = flume::bounded::>(1); let client = self.0.clone(); let pubky = pubky.clone(); - thread::spawn(move || { - let result = client.session(&pubky); - sender.send(result) - }); + thread::spawn(move || sender.send(client.session(&pubky))); receiver.recv_async().await? } diff --git a/pubky/src/error.rs b/pubky/src/error.rs index 91ef471..13b8ab2 100644 --- a/pubky/src/error.rs +++ b/pubky/src/error.rs @@ -26,4 +26,7 @@ pub enum Error { #[error(transparent)] Url(#[from] url::ParseError), + + #[error(transparent)] + Session(#[from] pubky_common::session::Error), } diff --git a/pubky/src/lib.rs b/pubky/src/lib.rs index 2684bd5..0c389f8 100644 --- a/pubky/src/lib.rs +++ b/pubky/src/lib.rs @@ -11,6 +11,7 @@ mod tests { use super::*; use pkarr::{mainline::Testnet, Keypair}; + use pubky_common::session::Session; use pubky_homeserver::Homeserver; #[tokio::test] @@ -28,5 +29,7 @@ mod tests { .unwrap(); let session = client.session(&keypair.public_key()).await.unwrap(); + + assert_eq!(session, Session { ..session.clone() }); } } From c399a8b3be0a1ad3b4435e1ca5879230c2c9b304 Mon Sep 17 00:00:00 2001 From: nazeh Date: Sun, 21 Jul 2024 11:13:03 +0300 Subject: [PATCH 12/13] feat(homeserver): signout --- pubky-common/src/crypto.rs | 2 ++ pubky-homeserver/src/extractors.rs | 2 ++ pubky-homeserver/src/routes.rs | 3 ++- pubky-homeserver/src/routes/auth.rs | 24 ++++++++++++++++++++++++ pubky/src/client.rs | 28 ++++++++++++++++++++++------ pubky/src/client_async.rs | 12 ++++++++++++ pubky/src/error.rs | 4 ++++ pubky/src/lib.rs | 18 +++++++++++++++++- 8 files changed, 85 insertions(+), 8 deletions(-) diff --git a/pubky-common/src/crypto.rs b/pubky-common/src/crypto.rs index b51181e..2f8131c 100644 --- a/pubky-common/src/crypto.rs +++ b/pubky-common/src/crypto.rs @@ -16,6 +16,8 @@ pub fn random_hash() -> Hash { pub fn random_bytes() -> [u8; N] { let mut rng = rand::thread_rng(); let mut arr = [0u8; N]; + + #[allow(clippy::needless_range_loop)] for i in 0..N { arr[i] = rng.gen(); } diff --git a/pubky-homeserver/src/extractors.rs b/pubky-homeserver/src/extractors.rs index bedd827..be65f13 100644 --- a/pubky-homeserver/src/extractors.rs +++ b/pubky-homeserver/src/extractors.rs @@ -40,6 +40,8 @@ where .map_err(Error::try_from) .map_err(IntoResponse::into_response)?; + // TODO: return 404 if the user doesn't exist, but exclude signups. + Ok(Pubky(public_key)) } } diff --git a/pubky-homeserver/src/routes.rs b/pubky-homeserver/src/routes.rs index 75da602..dcf0b66 100644 --- a/pubky-homeserver/src/routes.rs +++ b/pubky-homeserver/src/routes.rs @@ -1,5 +1,5 @@ use axum::{ - routing::{get, post, put}, + routing::{delete, get, post, put}, Router, }; use tower_cookies::CookieManagerLayer; @@ -16,6 +16,7 @@ pub fn create_app(state: AppState) -> Router { .route("/", get(root::handler)) .route("/:pubky", put(auth::signup)) .route("/:pubky/session", get(auth::session)) + .route("/:pubky/session", delete(auth::signout)) .route("/:pubky/*key", get(drive::put)) .layer(TraceLayer::new_for_http()) .layer(CookieManagerLayer::new()) diff --git a/pubky-homeserver/src/routes/auth.rs b/pubky-homeserver/src/routes/auth.rs index fc11793..b49f862 100644 --- a/pubky-homeserver/src/routes/auth.rs +++ b/pubky-homeserver/src/routes/auth.rs @@ -98,3 +98,27 @@ pub async fn session( Err(Error::with_status(StatusCode::NOT_FOUND)) } + +pub async fn signout( + State(state): State, + cookies: Cookies, + pubky: Pubky, +) -> Result { + if let Some(cookie) = cookies.get(&pubky.public_key().to_string()) { + let mut wtxn = state.db.env.write_txn()?; + + let sessions: SessionsTable = state + .db + .env + .open_database(&wtxn, Some(SESSIONS_TABLE))? + .expect("Session table already created"); + + let _ = sessions.delete(&mut wtxn, cookie.value()); + + wtxn.commit()?; + + return Ok(()); + }; + + Err(Error::with_status(StatusCode::UNAUTHORIZED)) +} diff --git a/pubky/src/client.rs b/pubky/src/client.rs index 0ad9581..95362c4 100644 --- a/pubky/src/client.rs +++ b/pubky/src/client.rs @@ -71,16 +71,30 @@ impl PubkyClient { let mut bytes = vec![]; - let reader = self - .request(HttpMethod::Get, &url) - .call() - .map_err(Box::new)? - .into_reader() - .read_to_end(&mut bytes); + let result = self.request(HttpMethod::Get, &url).call().map_err(Box::new); + + if let Ok(reader) = result { + reader.into_reader().read_to_end(&mut bytes); + } else { + return Err(Error::NotSignedIn); + } Ok(Session::deserialize(&bytes)?) } + /// Signout from a homeserver. + pub fn signout(&self, pubky: &PublicKey) -> Result<()> { + let (homeserver, mut url) = self.resolve_pubky_homeserver(pubky)?; + + url.set_path(&format!("/{}/session", pubky)); + + self.request(HttpMethod::Delete, &url) + .call() + .map_err(Box::new)?; + + Ok(()) + } + // === Private Methods === /// Publish the SVCB record for `_pubky.`. @@ -200,6 +214,7 @@ pub enum HttpMethod { Get, Put, Post, + Delete, } impl From for &str { @@ -208,6 +223,7 @@ impl From for &str { HttpMethod::Get => "GET", HttpMethod::Put => "PUT", HttpMethod::Post => "POST", + HttpMethod::Delete => "DELETE", } } } diff --git a/pubky/src/client_async.rs b/pubky/src/client_async.rs index 370c5a7..ca0a308 100644 --- a/pubky/src/client_async.rs +++ b/pubky/src/client_async.rs @@ -38,4 +38,16 @@ impl PubkyClientAsync { receiver.recv_async().await? } + + /// Async version of [PubkyClient::signout] + pub async fn signout(&self, pubky: &PublicKey) -> Result<()> { + let (sender, receiver) = flume::bounded::>(1); + + let client = self.0.clone(); + let pubky = pubky.clone(); + + thread::spawn(move || sender.send(client.signout(&pubky))); + + receiver.recv_async().await? + } } diff --git a/pubky/src/error.rs b/pubky/src/error.rs index 13b8ab2..026382e 100644 --- a/pubky/src/error.rs +++ b/pubky/src/error.rs @@ -12,6 +12,10 @@ pub enum Error { #[error("Generic error: {0}")] Generic(String), + #[error("Not signed in")] + NotSignedIn, + + // === Transparent === #[error(transparent)] Dns(#[from] SimpleDnsError), diff --git a/pubky/src/lib.rs b/pubky/src/lib.rs index 0c389f8..1876cda 100644 --- a/pubky/src/lib.rs +++ b/pubky/src/lib.rs @@ -4,12 +4,15 @@ mod client; mod client_async; mod error; -use client::PubkyClient; +pub use client::PubkyClient; +pub use error::Error; #[cfg(test)] mod tests { use super::*; + use super::error::Error; + use pkarr::{mainline::Testnet, Keypair}; use pubky_common::session::Session; use pubky_homeserver::Homeserver; @@ -31,5 +34,18 @@ mod tests { let session = client.session(&keypair.public_key()).await.unwrap(); assert_eq!(session, Session { ..session.clone() }); + + client.signout(&keypair.public_key()).await.unwrap(); + + { + let session = client.session(&keypair.public_key()).await; + + assert!(session.is_err()); + + match session { + Err(Error::NotSignedIn) => {} + _ => assert!(false, "expected NotSignedInt error"), + } + } } } From 5a6c7ae9c5da14153365bd525f5b2a9bba6d2b2d Mon Sep 17 00:00:00 2001 From: nazeh Date: Sun, 21 Jul 2024 11:50:22 +0300 Subject: [PATCH 13/13] feat(homeserver): add signin endpoint --- pubky-homeserver/src/database/tables/users.rs | 1 + pubky-homeserver/src/routes.rs | 1 + pubky-homeserver/src/routes/auth.rs | 86 +++++++++++-------- pubky/src/client.rs | 15 ++++ pubky/src/client_async.rs | 12 +++ pubky/src/lib.rs | 8 ++ 6 files changed, 88 insertions(+), 35 deletions(-) diff --git a/pubky-homeserver/src/database/tables/users.rs b/pubky-homeserver/src/database/tables/users.rs index e785245..9666637 100644 --- a/pubky-homeserver/src/database/tables/users.rs +++ b/pubky-homeserver/src/database/tables/users.rs @@ -15,6 +15,7 @@ pub type UsersTable = Database; pub const USERS_TABLE: &str = "users"; +// TODO: add more adminstration metadata like quota, invitation links, etc.. #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] pub struct User { pub created_at: u64, diff --git a/pubky-homeserver/src/routes.rs b/pubky-homeserver/src/routes.rs index dcf0b66..86120c2 100644 --- a/pubky-homeserver/src/routes.rs +++ b/pubky-homeserver/src/routes.rs @@ -16,6 +16,7 @@ pub fn create_app(state: AppState) -> Router { .route("/", get(root::handler)) .route("/:pubky", put(auth::signup)) .route("/:pubky/session", get(auth::session)) + .route("/:pubky/session", post(auth::signin)) .route("/:pubky/session", delete(auth::signout)) .route("/:pubky/*key", get(drive::put)) .layer(TraceLayer::new_for_http()) diff --git a/pubky-homeserver/src/routes/auth.rs b/pubky-homeserver/src/routes/auth.rs index b49f862..fceb6fe 100644 --- a/pubky-homeserver/src/routes/auth.rs +++ b/pubky-homeserver/src/routes/auth.rs @@ -34,41 +34,9 @@ pub async fn signup( pubky: Pubky, body: Bytes, ) -> Result { - let public_key = pubky.public_key(); - - state.verifier.verify(&body, public_key)?; - - let mut wtxn = state.db.env.write_txn()?; - let users: UsersTable = state.db.env.create_database(&mut wtxn, Some(USERS_TABLE))?; - - users.put( - &mut wtxn, - public_key, - &User { - created_at: Timestamp::now().into_inner(), - }, - )?; - - let session_secret = base32::encode(base32::Alphabet::Crockford, &random_bytes::<16>()); - - let sessions: SessionsTable = state - .db - .env - .open_database(&wtxn, Some(SESSIONS_TABLE))? - .expect("Sessions table already created"); - - // TODO: handle not having a user agent? - let mut session = Session::new(); - - session.set_user_agent(user_agent.to_string()); - - sessions.put(&mut wtxn, &session_secret, &session.serialize())?; - - cookies.add(Cookie::new(public_key.to_string(), session_secret)); - - wtxn.commit()?; - - Ok(()) + // TODO: Verify invitation link. + // TODO: add errors in case of already axisting user. + signin(State(state), TypedHeader(user_agent), cookies, pubky, body).await } pub async fn session( @@ -122,3 +90,51 @@ pub async fn signout( Err(Error::with_status(StatusCode::UNAUTHORIZED)) } + +pub async fn signin( + State(state): State, + TypedHeader(user_agent): TypedHeader, + cookies: Cookies, + pubky: Pubky, + body: Bytes, +) -> Result { + let public_key = pubky.public_key(); + + state.verifier.verify(&body, public_key)?; + + let mut wtxn = state.db.env.write_txn()?; + let users: UsersTable = state.db.env.create_database(&mut wtxn, Some(USERS_TABLE))?; + + if let Some(existing) = users.get(&wtxn, public_key)? { + users.put(&mut wtxn, public_key, &existing)?; + } else { + users.put( + &mut wtxn, + public_key, + &User { + created_at: Timestamp::now().into_inner(), + }, + )?; + } + + let session_secret = base32::encode(base32::Alphabet::Crockford, &random_bytes::<16>()); + + let sessions: SessionsTable = state + .db + .env + .open_database(&wtxn, Some(SESSIONS_TABLE))? + .expect("Sessions table already created"); + + // TODO: handle not having a user agent? + let mut session = Session::new(); + + session.set_user_agent(user_agent.to_string()); + + sessions.put(&mut wtxn, &session_secret, &session.serialize())?; + + cookies.add(Cookie::new(public_key.to_string(), session_secret)); + + wtxn.commit()?; + + Ok(()) +} diff --git a/pubky/src/client.rs b/pubky/src/client.rs index 95362c4..e353e98 100644 --- a/pubky/src/client.rs +++ b/pubky/src/client.rs @@ -95,6 +95,21 @@ impl PubkyClient { Ok(()) } + /// Signin to a homeserver. + pub fn signin(&self, keypair: &Keypair) -> Result<()> { + let pubky = keypair.public_key(); + + let (audience, mut url) = self.resolve_pubky_homeserver(&pubky)?; + + url.set_path(&format!("/{}/session", &pubky)); + + self.request(HttpMethod::Post, &url) + .send_bytes(AuthnSignature::generate(keypair, &audience).as_bytes()) + .map_err(Box::new)?; + + Ok(()) + } + // === Private Methods === /// Publish the SVCB record for `_pubky.`. diff --git a/pubky/src/client_async.rs b/pubky/src/client_async.rs index ca0a308..de9012c 100644 --- a/pubky/src/client_async.rs +++ b/pubky/src/client_async.rs @@ -50,4 +50,16 @@ impl PubkyClientAsync { receiver.recv_async().await? } + + /// Async version of [PubkyClient::signin] + pub async fn signin(&self, keypair: &Keypair) -> Result<()> { + let (sender, receiver) = flume::bounded::>(1); + + let client = self.0.clone(); + let keypair = keypair.clone(); + + thread::spawn(move || sender.send(client.signin(&keypair))); + + receiver.recv_async().await? + } } diff --git a/pubky/src/lib.rs b/pubky/src/lib.rs index 1876cda..b05d067 100644 --- a/pubky/src/lib.rs +++ b/pubky/src/lib.rs @@ -47,5 +47,13 @@ mod tests { _ => assert!(false, "expected NotSignedInt error"), } } + + client.signin(&keypair).await.unwrap(); + + { + let session = client.session(&keypair.public_key()).await.unwrap(); + + assert_eq!(session, Session { ..session.clone() }); + } } }