From 4cef62e8e4aa04e44048eb67e5091c12a73d2a09 Mon Sep 17 00:00:00 2001 From: mat <27899617+mat-1@users.noreply.github.com> Date: Sun, 16 Oct 2022 22:54:54 -0500 Subject: [PATCH] Microsoft Authentication (#29) * a * try to do more work on auth signing (untested) * well auth works when i remove the d= so * auth stuff * sessionserver stuff * add auth in azalea-protocol/client * caching* refreshing microsoft auth tokens isn't implemented yet, also i haven't tested it * how did i not notice that i had the code duplicated * fix cache * add refreshing msa token * replace some printlns with log::trace * auth works! * Update main.rs * fix clippy warnings --- .gitignore | 4 + Cargo.lock | 468 ++++++++++++++++++++++++++++-- azalea-auth/Cargo.toml | 13 + azalea-auth/README.md | 4 +- azalea-auth/examples/auth.rs | 19 ++ azalea-auth/src/auth.rs | 482 +++++++++++++++++++++++++++++++ azalea-auth/src/cache.rs | 105 +++++++ azalea-auth/src/lib.rs | 7 +- azalea-auth/src/sessionserver.rs | 79 +++++ azalea-buf/src/write.rs | 7 + azalea-client/src/account.rs | 27 +- azalea-client/src/client.rs | 18 +- azalea-client/src/get_mc_dir.rs | 34 +++ azalea-client/src/lib.rs | 1 + azalea-crypto/src/lib.rs | 2 +- azalea-protocol/README.md | 2 +- azalea-protocol/src/connect.rs | 63 +++- bot/src/main.rs | 6 +- 18 files changed, 1306 insertions(+), 35 deletions(-) create mode 100644 azalea-auth/examples/auth.rs create mode 100644 azalea-auth/src/auth.rs create mode 100644 azalea-auth/src/cache.rs create mode 100644 azalea-auth/src/sessionserver.rs create mode 100644 azalea-client/src/get_mc_dir.rs diff --git a/.gitignore b/.gitignore index ad9bfc78..10e5a00f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,7 @@ flamegraph.svg perf.data perf.data.old + +# created by azalea-auth/examples/auth, defined in the main .gitignore because +# the example could be run from anywhere +example_cache.json diff --git a/Cargo.lock b/Cargo.lock index 128bea90..6b7aa08c 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -117,6 +117,16 @@ name = "azalea-auth" version = "0.1.0" dependencies = [ "azalea-buf", + "azalea-crypto", + "chrono", + "env_logger", + "log", + "num-bigint", + "reqwest", + "serde", + "serde_json", + "thiserror", + "tokio", "uuid", ] @@ -325,6 +335,12 @@ dependencies = [ "uuid", ] +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + [[package]] name = "bitflags" version = "1.3.2" @@ -391,6 +407,12 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" + [[package]] name = "cfb8" version = "0.8.1" @@ -408,9 +430,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.19" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" dependencies = [ "num-integer", "num-traits", @@ -437,6 +459,22 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + [[package]] name = "cpufeatures" version = "0.2.2" @@ -590,6 +628,15 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +[[package]] +name = "encoding_rs" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" +dependencies = [ + "cfg-if", +] + [[package]] name = "enum-as-inner" version = "0.3.4" @@ -615,6 +662,15 @@ dependencies = [ "termcolor", ] +[[package]] +name = "fastrand" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +dependencies = [ + "instant", +] + [[package]] name = "flate2" version = "1.0.24" @@ -625,6 +681,27 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.0.1" @@ -742,7 +819,26 @@ checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" dependencies = [ "cfg-if", "libc", - "wasi 0.10.2+wasi-snapshot-preview1", + "wasi 0.10.0+wasi-snapshot-preview1", +] + +[[package]] +name = "h2" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca32592cf21ac7ccab1825cd87f6c9b3d9022c44d086172ed0966bec8af30be" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", ] [[package]] @@ -751,6 +847,12 @@ version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "heck" version = "0.4.0" @@ -777,12 +879,83 @@ dependencies = [ "winapi", ] +[[package]] +name = "http" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" +dependencies = [ + "bytes", + "fnv", + "itoa 1.0.2", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + [[package]] name = "humantime" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "hyper" +version = "0.14.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa 1.0.2", + "pin-project-lite", + "socket2 0.4.4", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "idna" version = "0.2.3" @@ -794,6 +967,16 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "indexmap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +dependencies = [ + "autocfg", + "hashbrown", +] + [[package]] name = "inout" version = "0.1.3" @@ -821,7 +1004,7 @@ dependencies = [ "socket2 0.3.19", "widestring", "winapi", - "winreg", + "winreg 0.6.2", ] [[package]] @@ -933,6 +1116,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + [[package]] name = "miniz_oxide" version = "0.5.3" @@ -954,6 +1143,24 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "native-tls" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nohash-hasher" version = "0.2.0" @@ -1059,9 +1266,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.12.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225" +checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" [[package]] name = "oorandom" @@ -1069,6 +1276,51 @@ version = "11.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" +[[package]] +name = "openssl" +version = "0.10.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12fc0523e3bd51a692c8850d075d74dc062ccf251c0110668cbd921917118a13" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5230151e44c0f05157effb743e8d517472843121cf9243e8b81393edb5acd9ce" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "parking_lot" version = "0.11.2" @@ -1135,6 +1387,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" + [[package]] name = "plotters" version = "0.3.1" @@ -1279,6 +1537,52 @@ version = "0.6.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "reqwest" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "431949c384f4e2ae07605ccaa56d1d9d2ecdb5cadd4f9577ccfab29f2e5149fc" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg 0.10.1", +] + [[package]] name = "resolv-conf" version = "0.7.0" @@ -1324,12 +1628,45 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" +dependencies = [ + "lazy_static", + "windows-sys", +] + [[package]] name = "scopeguard" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "security-framework" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.10" @@ -1338,9 +1675,9 @@ checksum = "a41d061efea015927ac527063765e73601444cdc344ba855bc7bd44578b25e1c" [[package]] name = "serde" -version = "1.0.137" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" +checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b" dependencies = [ "serde_derive", ] @@ -1357,9 +1694,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.137" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" +checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c" dependencies = [ "proc-macro2", "quote", @@ -1368,15 +1705,27 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.81" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c" +checksum = "41feea4228a6f1cd09ec7a3593a682276702cd67b5273544757dae23c096f074" dependencies = [ "itoa 1.0.2", "ryu", "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 1.0.2", + "ryu", + "serde", +] + [[package]] name = "sha-1" version = "0.10.0" @@ -1388,6 +1737,15 @@ dependencies = [ "digest", ] +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + [[package]] name = "simple_asn1" version = "0.5.4" @@ -1444,6 +1802,20 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + [[package]] name = "termcolor" version = "1.1.3" @@ -1509,9 +1881,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.21.1" +version = "1.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0020c875007ad96677dcc890298f4b942882c5d4eb7cc8f439fc3bf813dc9c95" +checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099" dependencies = [ "autocfg", "bytes", @@ -1519,8 +1891,9 @@ dependencies = [ "memchr", "mio", "num_cpus", - "once_cell", + "parking_lot 0.12.1", "pin-project-lite", + "signal-hook-registry", "socket2 0.4.4", "tokio-macros", "winapi", @@ -1537,6 +1910,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.4" @@ -1551,6 +1934,12 @@ dependencies = [ "tracing", ] +[[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.35" @@ -1616,6 +2005,12 @@ dependencies = [ "trust-dns-proto", ] +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + [[package]] name = "typenum" version = "1.15.0" @@ -1667,6 +2062,12 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.4" @@ -1685,10 +2086,20 @@ dependencies = [ ] [[package]] -name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" +name = "want" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" [[package]] name = "wasi" @@ -1721,6 +2132,18 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f741de44b75e14c35df886aff5f1eb73aa114fa5d4d00dcd37b5e01259bf3b2" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.80" @@ -1848,3 +2271,12 @@ checksum = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9" dependencies = [ "winapi", ] + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] diff --git a/azalea-auth/Cargo.toml b/azalea-auth/Cargo.toml index 2f817354..34fec8f7 100755 --- a/azalea-auth/Cargo.toml +++ b/azalea-auth/Cargo.toml @@ -9,4 +9,17 @@ version = "0.1.0" [dependencies] azalea-buf = {path = "../azalea-buf", version = "^0.1.0"} +azalea-crypto = {path = "../azalea-crypto", version = "^0.1.0"} +chrono = {version = "0.4.22", default-features = false} +log = "0.4.17" +num-bigint = "0.4.3" +reqwest = {version = "0.11.12", features = ["json"]} +serde = {version = "1.0.145", features = ["derive"]} +serde_json = "1.0.86" +thiserror = "1.0.37" +tokio = "1.21.2" uuid = "^1.1.2" + +[dev-dependencies] +env_logger = "0.9.1" +tokio = {version = "1.21.2", features = ["full"]} diff --git a/azalea-auth/README.md b/azalea-auth/README.md index fa87afca..aa290c94 100644 --- a/azalea-auth/README.md +++ b/azalea-auth/README.md @@ -1,3 +1,5 @@ # Azalea Auth -A port of Mojang's Authlib, except authentication isn't actually implemented yet. +A port of Mojang's Authlib and launcher authentication. + +Thanks to [wiki.vg contributors](https://wiki.vg/Microsoft_Authentication_Scheme), [Overhash](https://gist.github.com/OverHash/a71b32846612ba09d8f79c9d775bfadf), and [prismarine-auth contributors](https://github.com/PrismarineJS/prismarine-auth). diff --git a/azalea-auth/examples/auth.rs b/azalea-auth/examples/auth.rs new file mode 100644 index 00000000..8f7cf7f9 --- /dev/null +++ b/azalea-auth/examples/auth.rs @@ -0,0 +1,19 @@ +use std::path::PathBuf; + +#[tokio::main] +async fn main() { + env_logger::init(); + + let cache_file = PathBuf::from("example_cache.json"); + + let auth_result = azalea_auth::auth( + "example@example.com", + azalea_auth::AuthOpts { + cache_file: Some(cache_file), + ..Default::default() + }, + ) + .await + .unwrap(); + println!("{:?}", auth_result); +} diff --git a/azalea-auth/src/auth.rs b/azalea-auth/src/auth.rs new file mode 100644 index 00000000..5f96d4be --- /dev/null +++ b/azalea-auth/src/auth.rs @@ -0,0 +1,482 @@ +//! Handle Minecraft (Xbox) authentication. + +use crate::cache::{self, CachedAccount, ExpiringValue}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use std::{ + collections::HashMap, + path::PathBuf, + time::{Instant, SystemTime, UNIX_EPOCH}, +}; +use thiserror::Error; + +#[derive(Default)] +pub struct AuthOpts { + /// Whether we should check if the user actually owns the game. This will + /// fail if the user has Xbox Game Pass! Note that this isn't really + /// necessary, since getting the user profile will check this anyways. + pub check_ownership: bool, + // /// Whether we should get the Minecraft profile data (i.e. username, uuid, + // /// skin, etc) for the player. + // pub get_profile: bool, + /// The directory to store the cache in. If this is not set, caching is not + /// done. + pub cache_file: Option, +} + +#[derive(Debug, Error)] +pub enum AuthError { + #[error( + "The Minecraft API is indicating that you don't own the game. \ + If you're using Xbox Game Pass, set `check_ownership` to false in the auth options." + )] + DoesNotOwnGame, + #[error("Error getting Microsoft auth token: {0}")] + GetMicrosoftAuthToken(#[from] GetMicrosoftAuthTokenError), + #[error("Error refreshing Microsoft auth token: {0}")] + RefreshMicrosoftAuthToken(#[from] RefreshMicrosoftAuthTokenError), + #[error("Error getting Xbox Live auth token: {0}")] + GetXboxLiveAuthToken(#[from] MinecraftXstsAuthError), + #[error("Error getting Minecraft profile: {0}")] + GetMinecraftProfile(#[from] GetProfileError), + #[error("Error checking ownership: {0}")] + CheckOwnership(#[from] CheckOwnershipError), + #[error("Error getting Minecraft auth token: {0}")] + GetMinecraftAuthToken(#[from] MinecraftAuthError), + #[error("Error authenticating with Xbox Live: {0}")] + GetXboxLiveAuth(#[from] XboxLiveAuthError), +} + +/// Authenticate with authenticate with Microsoft. If the data isn't cached, +/// they'll be asked to go to log into Microsoft in a web page. +/// +/// The email is technically only used as a cache key, so it *could* be +/// anything. You should just have it be the actual email so it's not confusing +/// though, and in case the Microsoft API does start providing the real email. +pub async fn auth(email: &str, opts: AuthOpts) -> Result { + let cached_account = if let Some(cache_file) = &opts.cache_file && let Some(account) = cache::get_account_in_cache(cache_file, email).await { + Some(account) + } else { None }; + + // these two MUST be set by the end, since we return them in AuthResult + let profile: ProfileResponse; + let minecraft_access_token: String; + + if let Some(account) = &cached_account && !account.mca.is_expired() { + // the minecraft auth data is cached and not expired, so we can just + // use that instead of doing auth all over again :) + profile = account.profile.clone(); + minecraft_access_token = account.mca.data.access_token.clone(); + } else { + let client = reqwest::Client::new(); + let mut msa = if let Some(account) = cached_account { + account.msa + } else { + interactive_get_ms_auth_token(&client).await? + }; + if msa.is_expired() { + log::trace!("refreshing Microsoft auth token"); + msa = refresh_ms_auth_token(&client, &msa.data.refresh_token).await?; + } + let ms_access_token = &msa.data.access_token; + log::trace!("Got access token: {}", ms_access_token); + + let xbl_auth = auth_with_xbox_live(&client, ms_access_token).await?; + + let xsts_token = obtain_xsts_for_minecraft( + &client, + &xbl_auth + .get() + .expect("Xbox Live auth token shouldn't have expired yet") + .token, + ) + .await?; + + // Minecraft auth + let mca = auth_with_minecraft(&client, &xbl_auth.data.user_hash, &xsts_token).await?; + + minecraft_access_token = mca + .get() + .expect("Minecraft auth shouldn't have expired yet") + .access_token + .to_string(); + + if opts.check_ownership { + let has_game = check_ownership(&client, &minecraft_access_token).await?; + if !has_game { + return Err(AuthError::DoesNotOwnGame); + } + } + + profile = get_profile(&client, &minecraft_access_token).await?; + + if let Some(cache_file) = opts.cache_file { + if let Err(e) = cache::set_account_in_cache( + &cache_file, + email, + CachedAccount { + email: email.to_string(), + mca, + msa, + xbl: xbl_auth, + profile: profile.clone(), + }, + ) + .await { + log::warn!("Error while caching auth data: {}", e); + } + } + } + + Ok(AuthResult { + access_token: minecraft_access_token, + profile, + }) +} + +#[derive(Debug)] +pub struct AuthResult { + pub access_token: String, + pub profile: ProfileResponse, +} + +#[derive(Debug, Deserialize)] +pub struct DeviceCodeResponse { + user_code: String, + device_code: String, + verification_uri: String, + expires_in: u64, + interval: u64, +} + +#[allow(unused)] +#[derive(Debug, Deserialize, Serialize)] +pub struct AccessTokenResponse { + token_type: String, + expires_in: u64, + scope: String, + access_token: String, + refresh_token: String, + user_id: String, +} + +#[allow(unused)] +#[derive(Debug, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct XboxLiveAuthResponse { + issue_instant: String, + not_after: String, + token: String, + display_claims: HashMap>>, +} + +/// Just the important data +#[derive(Serialize, Deserialize, Debug)] +pub struct XboxLiveAuth { + token: String, + user_hash: String, +} + +#[allow(unused)] +#[derive(Debug, Deserialize, Serialize)] +pub struct MinecraftAuthResponse { + username: String, + roles: Vec, + access_token: String, + token_type: String, + expires_in: u64, +} + +#[allow(unused)] +#[derive(Debug, Deserialize)] +pub struct GameOwnershipResponse { + items: Vec, + signature: String, + key_id: String, +} + +#[allow(unused)] +#[derive(Debug, Deserialize)] +pub struct GameOwnershipItem { + name: String, + signature: String, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct ProfileResponse { + pub id: String, + pub name: String, + pub skins: Vec, + pub capes: Vec, +} + +// nintendo switch (so it works for accounts that are under 18 years old) +const CLIENT_ID: &str = "00000000441cc96b"; + +#[derive(Debug, Error)] +pub enum GetMicrosoftAuthTokenError { + #[error("Http error: {0}")] + Http(#[from] reqwest::Error), + #[error("Authentication timed out")] + Timeout, +} + +/// Asks the user to go to a webpage and log in with Microsoft. +async fn interactive_get_ms_auth_token( + client: &reqwest::Client, +) -> Result, GetMicrosoftAuthTokenError> { + let res = client + .post("https://login.live.com/oauth20_connect.srf") + .form(&vec![ + ("scope", "service::user.auth.xboxlive.com::MBI_SSL"), + ("client_id", CLIENT_ID), + ("response_type", "device_code"), + ]) + .send() + .await? + .json::() + .await?; + log::trace!("Device code response: {:?}", res); + println!( + "Go to \x1b[1m{}\x1b[m and enter the code \x1b[1m{}\x1b[m", + res.verification_uri, res.user_code + ); + + let login_expires_at = Instant::now() + std::time::Duration::from_secs(res.expires_in); + + while Instant::now() < login_expires_at { + tokio::time::sleep(std::time::Duration::from_secs(res.interval)).await; + + log::trace!("Polling to check if user has logged in..."); + if let Ok(access_token_response) = client + .post(format!( + "https://login.live.com/oauth20_token.srf?client_id={}", + CLIENT_ID + )) + .form(&vec![ + ("client_id", CLIENT_ID), + ("device_code", &res.device_code), + ("grant_type", "urn:ietf:params:oauth:grant-type:device_code"), + ]) + .send() + .await? + .json::() + .await + { + log::trace!("access_token_response: {:?}", access_token_response); + let expires_at = SystemTime::now() + + std::time::Duration::from_secs(access_token_response.expires_in); + return Ok(ExpiringValue { + data: access_token_response, + expires_at: expires_at + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(), + }); + } + } + + Err(GetMicrosoftAuthTokenError::Timeout) +} + +#[derive(Debug, Error)] +pub enum RefreshMicrosoftAuthTokenError { + #[error("Http error: {0}")] + Http(#[from] reqwest::Error), +} + +async fn refresh_ms_auth_token( + client: &reqwest::Client, + refresh_token: &str, +) -> Result, RefreshMicrosoftAuthTokenError> { + let access_token_response = client + .post("https://login.live.com/oauth20_token.srf") + .form(&vec![ + ("scope", "service::user.auth.xboxlive.com::MBI_SSL"), + ("client_id", CLIENT_ID), + ("grant_type", "refresh_token"), + ("refresh_token", refresh_token), + ]) + .send() + .await? + .json::() + .await?; + + let expires_at = + SystemTime::now() + std::time::Duration::from_secs(access_token_response.expires_in); + Ok(ExpiringValue { + data: access_token_response, + expires_at: expires_at + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(), + }) +} + +#[derive(Debug, Error)] +pub enum XboxLiveAuthError { + #[error("Http error: {0}")] + Http(#[from] reqwest::Error), + #[error("Invalid expiry date: {0}")] + InvalidExpiryDate(String), +} + +async fn auth_with_xbox_live( + client: &reqwest::Client, + access_token: &str, +) -> Result, XboxLiveAuthError> { + let auth_json = json!({ + "Properties": { + "AuthMethod": "RPS", + "SiteName": "user.auth.xboxlive.com", + // i thought this was supposed to be d={} but it doesn't work for + // me when i add it ?????? + "RpsTicket": format!("{}", access_token) + }, + "RelyingParty": "http://auth.xboxlive.com", + "TokenType": "JWT" + }); + let payload = auth_json.to_string(); + log::trace!("auth_json: {:#?}", auth_json); + let res = client + .post("https://user.auth.xboxlive.com/user/authenticate") + .header("Content-Type", "application/json") + .header("Accept", "application/json") + .header("x-xbl-contract-version", "1") + // .header("Cache-Control", "no-store, must-revalidate, no-cache") + // .header("Signature", base64::encode(signature)) + .body(payload) + .send() + .await? + .json::() + .await?; + log::trace!("Xbox Live auth response: {:?}", res); + + // not_after looks like 2020-12-21T19:52:08.4463796Z + let expires_at = DateTime::parse_from_rfc3339(&res.not_after) + .map_err(|e| XboxLiveAuthError::InvalidExpiryDate(format!("{}: {}", res.not_after, e)))? + .with_timezone(&Utc) + .timestamp() as u64; + Ok(ExpiringValue { + data: XboxLiveAuth { + token: res.token, + user_hash: res.display_claims["xui"].get(0).unwrap()["uhs"].clone(), + }, + expires_at, + }) +} + +#[derive(Debug, Error)] +pub enum MinecraftXstsAuthError { + #[error("Http error: {0}")] + Http(#[from] reqwest::Error), +} + +async fn obtain_xsts_for_minecraft( + client: &reqwest::Client, + xbl_auth_token: &str, +) -> Result { + let res = client + .post("https://xsts.auth.xboxlive.com/xsts/authorize") + .header("Accept", "application/json") + .json(&json!({ + "Properties": { + "SandboxId": "RETAIL", + "UserTokens": [xbl_auth_token.to_string()] + }, + "RelyingParty": "rp://api.minecraftservices.com/", + "TokenType": "JWT" + })) + .send() + .await? + .json::() + .await?; + log::trace!("Xbox Live auth response (for XSTS): {:?}", res); + + Ok(res.token) +} + +#[derive(Debug, Error)] +pub enum MinecraftAuthError { + #[error("Http error: {0}")] + Http(#[from] reqwest::Error), +} + +async fn auth_with_minecraft( + client: &reqwest::Client, + user_hash: &str, + xsts_token: &str, +) -> Result, MinecraftAuthError> { + let res = client + .post("https://api.minecraftservices.com/authentication/login_with_xbox") + .header("Accept", "application/json") + .json(&json!({ + "identityToken": format!("XBL3.0 x={};{}", user_hash, xsts_token) + })) + .send() + .await? + .json::() + .await?; + log::trace!("{:?}", res); + + let expires_at = SystemTime::now() + std::time::Duration::from_secs(res.expires_in); + Ok(ExpiringValue { + data: res, + // to seconds since epoch + expires_at: expires_at.duration_since(UNIX_EPOCH).unwrap().as_secs(), + }) +} + +#[derive(Debug, Error)] +pub enum CheckOwnershipError { + #[error("Http error: {0}")] + Http(#[from] reqwest::Error), +} + +async fn check_ownership( + client: &reqwest::Client, + minecraft_access_token: &str, +) -> Result { + let res = client + .get("https://api.minecraftservices.com/entitlements/mcstore") + .header( + "Authorization", + format!("Bearer {}", minecraft_access_token), + ) + .send() + .await? + .json::() + .await?; + log::trace!("{:?}", res); + + // vanilla checks here to make sure the signatures are right, but it's not + // actually required so we just don't + + Ok(!res.items.is_empty()) +} + +#[derive(Debug, Error)] +pub enum GetProfileError { + #[error("Http error: {0}")] + Http(#[from] reqwest::Error), +} + +async fn get_profile( + client: &reqwest::Client, + minecraft_access_token: &str, +) -> Result { + let res = client + .get("https://api.minecraftservices.com/minecraft/profile") + .header( + "Authorization", + format!("Bearer {}", minecraft_access_token), + ) + .send() + .await? + .json::() + .await?; + log::trace!("{:?}", res); + + Ok(res) +} diff --git a/azalea-auth/src/cache.rs b/azalea-auth/src/cache.rs new file mode 100644 index 00000000..8af9e171 --- /dev/null +++ b/azalea-auth/src/cache.rs @@ -0,0 +1,105 @@ +//! Cache auth information + +use serde::{Deserialize, Serialize}; +use std::path::Path; +use std::time::{SystemTime, UNIX_EPOCH}; +use thiserror::Error; +use tokio::fs::File; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; + +#[derive(Debug, Error)] +pub enum CacheError { + #[error("Failed to read cache file: {0}")] + Read(std::io::Error), + #[error("Failed to write cache file: {0}")] + Write(std::io::Error), + #[error("Failed to parse cache file: {0}")] + Parse(serde_json::Error), +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct CachedAccount { + pub email: String, + /// Microsoft auth + pub msa: ExpiringValue, + /// Xbox Live auth + pub xbl: ExpiringValue, + /// Minecraft auth + pub mca: ExpiringValue, + /// The user's Minecraft profile (i.e. username, UUID, skin) + pub profile: crate::auth::ProfileResponse, +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct ExpiringValue { + /// Seconds since the UNIX epoch + pub expires_at: u64, + pub data: T, +} + +impl ExpiringValue { + pub fn is_expired(&self) -> bool { + self.expires_at + < SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() + } + + /// Return the data if it's not expired, otherwise return `None` + pub fn get(&self) -> Option<&T> { + if self.is_expired() { + None + } else { + Some(&self.data) + } + } +} + +async fn get_entire_cache(cache_file: &Path) -> Result, CacheError> { + let mut cache: Vec = Vec::new(); + if cache_file.exists() { + let mut cache_file = File::open(cache_file).await.map_err(CacheError::Read)?; + // read the file into a string + let mut contents = String::new(); + cache_file + .read_to_string(&mut contents) + .await + .map_err(CacheError::Read)?; + cache = serde_json::from_str(&contents).map_err(CacheError::Parse)?; + } + Ok(cache) +} +async fn set_entire_cache(cache_file: &Path, cache: Vec) -> Result<(), CacheError> { + println!("saving cache: {:?}", cache); + + let mut cache_file = File::create(cache_file).await.map_err(CacheError::Write)?; + let cache = serde_json::to_string_pretty(&cache).map_err(CacheError::Parse)?; + cache_file + .write_all(cache.as_bytes()) + .await + .map_err(CacheError::Write)?; + + Ok(()) +} + +/// Gets cached data for the given email. +/// +/// Technically it doesn't actually have to be an email since it's only the +/// cache key. I considered using usernames or UUIDs as the cache key, but +/// usernames change and no one has their UUID memorized. +pub async fn get_account_in_cache(cache_file: &Path, email: &str) -> Option { + let cache = get_entire_cache(cache_file).await.unwrap_or_default(); + cache.into_iter().find(|account| account.email == email) +} + +pub async fn set_account_in_cache( + cache_file: &Path, + email: &str, + account: CachedAccount, +) -> Result<(), CacheError> { + let mut cache = get_entire_cache(cache_file).await.unwrap_or_default(); + cache.retain(|account| account.email != email); + cache.push(account); + set_entire_cache(cache_file, cache).await +} diff --git a/azalea-auth/src/lib.rs b/azalea-auth/src/lib.rs index 773ea1d9..03e15c71 100755 --- a/azalea-auth/src/lib.rs +++ b/azalea-auth/src/lib.rs @@ -1,3 +1,8 @@ -//! Handle Minecraft authentication. +#![feature(let_chains)] +mod auth; +mod cache; pub mod game_profile; +pub mod sessionserver; + +pub use auth::*; diff --git a/azalea-auth/src/sessionserver.rs b/azalea-auth/src/sessionserver.rs new file mode 100644 index 00000000..31857bc0 --- /dev/null +++ b/azalea-auth/src/sessionserver.rs @@ -0,0 +1,79 @@ +//! Tell Mojang you're joining a multiplayer server. +//! +use serde::Deserialize; +use serde_json::json; +use thiserror::Error; +use uuid::Uuid; + +#[derive(Debug, Error)] +pub enum SessionServerError { + #[error("Error sending HTTP request to sessionserver: {0}")] + HttpError(#[from] reqwest::Error), + #[error("Multiplayer is not enabled for this account")] + MultiplayerDisabled, + #[error("This account has been banned from multiplayer")] + Banned, + #[error("Unknown sessionserver error: {0}")] + Unknown(String), + #[error("Unexpected response from sessionserver (status code {status_code}): {body}")] + UnexpectedResponse { status_code: u16, body: String }, +} + +#[derive(Deserialize)] +pub struct ForbiddenError { + pub error: String, + pub path: String, +} + +/// Tell Mojang's servers that you are going to join a multiplayer server, +/// which is required to join online-mode servers. The server ID is an empty +/// string. +pub async fn join( + access_token: &str, + public_key: &[u8], + private_key: &[u8], + uuid: &Uuid, + server_id: &str, +) -> Result<(), SessionServerError> { + let client = reqwest::Client::new(); + + let server_hash = azalea_crypto::hex_digest(&azalea_crypto::digest_data( + server_id.as_bytes(), + public_key, + private_key, + )); + + let mut encode_buffer = Uuid::encode_buffer(); + let undashed_uuid = uuid.simple().encode_lower(&mut encode_buffer); + + let data = json!({ + "accessToken": access_token, + "selectedProfile": undashed_uuid, + "serverId": server_hash + }); + println!("data: {:?}", data); + let res = client + .post("https://sessionserver.mojang.com/session/minecraft/join") + .json(&data) + .send() + .await?; + + match res.status() { + reqwest::StatusCode::NO_CONTENT => Ok(()), + reqwest::StatusCode::FORBIDDEN => { + let forbidden = res.json::().await?; + match forbidden.error.as_str() { + "InsufficientPrivilegesException" => Err(SessionServerError::MultiplayerDisabled), + "UserBannedException" => Err(SessionServerError::Banned), + _ => Err(SessionServerError::Unknown(forbidden.error)), + } + } + status_code => { + let body = res.text().await?; + Err(SessionServerError::UnexpectedResponse { + status_code: status_code.as_u16(), + body, + }) + } + } +} diff --git a/azalea-buf/src/write.rs b/azalea-buf/src/write.rs index 9b50a1c4..e1f1ffb1 100644 --- a/azalea-buf/src/write.rs +++ b/azalea-buf/src/write.rs @@ -110,6 +110,13 @@ impl McBufWritable for String { } } + +impl McBufWritable for &str { + fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> { + write_utf_with_len(buf, self, MAX_STRING_LENGTH.into()) + } +} + impl McBufWritable for u32 { fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> { i32::write_into(&(*self as i32), buf) diff --git a/azalea-client/src/account.rs b/azalea-client/src/account.rs index c554908f..94e84ab8 100644 --- a/azalea-client/src/account.rs +++ b/azalea-client/src/account.rs @@ -1,20 +1,45 @@ //! Connect to Minecraft servers. -use crate::{client::JoinError, Client, Event}; +use crate::{client::JoinError, get_mc_dir, Client, Event}; use azalea_protocol::ServerAddress; use tokio::sync::mpsc::UnboundedReceiver; +use uuid::Uuid; /// Something that can join Minecraft servers. pub struct Account { pub username: String, + /// The access token for authentication. You can obtain one of these + /// manually from azalea-auth. + pub access_token: Option, + /// Only required for online-mode accounts. + pub uuid: Option, } impl Account { pub fn offline(username: &str) -> Self { Self { username: username.to_string(), + access_token: None, + uuid: None, } } + pub async fn microsoft(email: &str) -> Result { + let minecraft_dir = get_mc_dir::minecraft_dir().unwrap(); + let auth_result = azalea_auth::auth( + email, + azalea_auth::AuthOpts { + cache_file: Some(minecraft_dir.join("azalea-auth.json")), + ..Default::default() + }, + ) + .await?; + Ok(Self { + username: auth_result.profile.name, + access_token: Some(auth_result.access_token), + uuid: Some(Uuid::parse_str(&auth_result.profile.id).expect("Invalid UUID")), + }) + } + /// Joins the Minecraft server on the given address using this account. pub async fn join( &self, diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs index 09f68c4a..25c68c5d 100644 --- a/azalea-client/src/client.rs +++ b/azalea-client/src/client.rs @@ -38,7 +38,6 @@ use std::{ }; use thiserror::Error; use tokio::{ - io::AsyncWriteExt, sync::mpsc::{self, UnboundedReceiver, UnboundedSender}, task::JoinHandle, time::{self}, @@ -105,6 +104,8 @@ pub enum JoinError { ReadPacket(#[from] azalea_protocol::read::ReadPacketError), #[error("{0}")] Io(#[from] io::Error), + #[error("{0}")] + SessionServer(#[from] azalea_auth::sessionserver::SessionServerError), } #[derive(Error, Debug)] @@ -159,7 +160,17 @@ impl Client { debug!("Got encryption request"); let e = azalea_crypto::encrypt(&p.public_key, &p.nonce).unwrap(); - // TODO: authenticate with the server here (authenticateServer) + if let Some(access_token) = &account.access_token { + conn.authenticate( + access_token, + &account + .uuid + .expect("Uuid must be present if access token is present."), + e.secret_key, + p, + ) + .await?; + } conn.write( ServerboundKeyPacket { @@ -171,6 +182,7 @@ impl Client { .get(), ) .await?; + conn.set_encryption_key(e.secret_key); } ClientboundLoginPacket::LoginCompression(p) => { @@ -237,7 +249,7 @@ impl Client { /// Disconnect from the server, ending all tasks. pub async fn shutdown(self) -> Result<(), std::io::Error> { - self.write_conn.lock().await.write_stream.shutdown().await?; + self.write_conn.lock().await.shutdown().await?; let tasks = self.tasks.lock(); for task in tasks.iter() { task.abort(); diff --git a/azalea-client/src/get_mc_dir.rs b/azalea-client/src/get_mc_dir.rs new file mode 100644 index 00000000..abc5b3c8 --- /dev/null +++ b/azalea-client/src/get_mc_dir.rs @@ -0,0 +1,34 @@ +//! Find out where the user's .minecraft directory is. +//! +//! Used for the auth cache. + +use std::path::PathBuf; + +/// Return the location of the user's .minecraft directory. +/// +/// Windows: `%appdata%\.minecraft`\ +/// Mac: `$HOME/Library/Application Support/minecraft`\ +/// Linux: `$HOME/.minecraft` +/// +/// Anywhere else it'll return None. +pub fn minecraft_dir() -> Option { + #[cfg(target_os = "windows")] + { + let appdata = std::env::var("APPDATA").ok()?; + Some(PathBuf::from(appdata).join(".minecraft")) + } + #[cfg(target_os = "macos")] + { + let home = std::env::var("HOME").ok()?; + Some(PathBuf::from(home).join("Library/Application Support/minecraft")) + } + #[cfg(target_os = "linux")] + { + let home = std::env::var("HOME").ok()?; + Some(PathBuf::from(home).join(".minecraft")) + } + #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] + { + None + } +} diff --git a/azalea-client/src/lib.rs b/azalea-client/src/lib.rs index a918ca32..c30ca103 100755 --- a/azalea-client/src/lib.rs +++ b/azalea-client/src/lib.rs @@ -2,6 +2,7 @@ mod account; mod client; +mod get_mc_dir; mod movement; pub mod ping; mod player; diff --git a/azalea-crypto/src/lib.rs b/azalea-crypto/src/lib.rs index d1a86ec7..30362e5d 100644 --- a/azalea-crypto/src/lib.rs +++ b/azalea-crypto/src/lib.rs @@ -18,8 +18,8 @@ fn generate_secret_key() -> [u8; 16] { pub fn digest_data(server_id: &[u8], public_key: &[u8], private_key: &[u8]) -> Vec { let mut digest = Sha1::new(); digest.update(server_id); - digest.update(public_key); digest.update(private_key); + digest.update(public_key); digest.finalize().to_vec() } diff --git a/azalea-protocol/README.md b/azalea-protocol/README.md index a210e4a6..7bc1f4c0 100644 --- a/azalea-protocol/README.md +++ b/azalea-protocol/README.md @@ -1,6 +1,6 @@ # Azalea Protocol -Send and receive Minecraft packets. You should probably use `azalea` or `azalea-client` instead. +A low-level crate to send and receive Minecraft packets. You should probably use `azalea` or `azalea-client` instead. The goal is to only support the latest Minecraft version in order to ease development. diff --git a/azalea-protocol/src/connect.rs b/azalea-protocol/src/connect.rs index d7b9bd1d..03c56471 100644 --- a/azalea-protocol/src/connect.rs +++ b/azalea-protocol/src/connect.rs @@ -2,32 +2,36 @@ use crate::packets::game::{ClientboundGamePacket, ServerboundGamePacket}; use crate::packets::handshake::{ClientboundHandshakePacket, ServerboundHandshakePacket}; +use crate::packets::login::clientbound_hello_packet::ClientboundHelloPacket; use crate::packets::login::{ClientboundLoginPacket, ServerboundLoginPacket}; use crate::packets::status::{ClientboundStatusPacket, ServerboundStatusPacket}; use crate::packets::ProtocolPacket; use crate::read::{read_packet, ReadPacketError}; use crate::write::write_packet; use crate::ServerIpAddress; +use azalea_auth::sessionserver::SessionServerError; use azalea_crypto::{Aes128CfbDec, Aes128CfbEnc}; use bytes::BytesMut; use std::fmt::Debug; use std::marker::PhantomData; use thiserror::Error; +use tokio::io::AsyncWriteExt; use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf}; use tokio::net::TcpStream; +use uuid::Uuid; pub struct ReadConnection { - pub read_stream: OwnedReadHalf, + read_stream: OwnedReadHalf, buffer: BytesMut, - pub compression_threshold: Option, - pub dec_cipher: Option, + compression_threshold: Option, + dec_cipher: Option, _reading: PhantomData, } pub struct WriteConnection { - pub write_stream: OwnedWriteHalf, - pub compression_threshold: Option, - pub enc_cipher: Option, + write_stream: OwnedWriteHalf, + compression_threshold: Option, + enc_cipher: Option, _writing: PhantomData, } @@ -64,6 +68,10 @@ where ) .await } + + pub async fn shutdown(&mut self) -> std::io::Result<()> { + self.write_stream.shutdown().await + } } impl Connection @@ -145,13 +153,54 @@ impl Connection { pub fn set_encryption_key(&mut self, key: [u8; 16]) { // minecraft has a cipher decoder and encoder, i don't think it matters though? let (enc_cipher, dec_cipher) = azalea_crypto::create_cipher(&key); - self.writer.enc_cipher = Some(enc_cipher); self.reader.dec_cipher = Some(dec_cipher); + self.writer.enc_cipher = Some(enc_cipher); } pub fn game(self) -> Connection { Connection::from(self) } + + /// Authenticate with Minecraft's servers, which is required to join + /// online-mode servers. This must happen when you get a + /// `ClientboundLoginPacket::Hello` packet. + /// + /// ```no_run + /// let token = azalea_auth::auth(azalea_auth::AuthOpts { + /// ..Default::default() + /// }) + /// .await; + /// let player_data = azalea_auth::get_profile(token).await; + /// + /// let mut connection = azalea::Connection::new(&server_address).await?; + /// + /// // transition to the login state, in a real program we would have done a handshake first + /// connection.login(); + /// + /// match connection.read().await? { + /// ClientboundLoginPacket::Hello(p) => { + /// // tell Mojang we're joining the server + /// connection.authenticate(&token, player_data.uuid, p).await?; + /// } + /// _ => {} + /// } + /// ``` + pub async fn authenticate( + &self, + access_token: &str, + uuid: &Uuid, + private_key: [u8; 16], + packet: ClientboundHelloPacket, + ) -> Result<(), SessionServerError> { + azalea_auth::sessionserver::join( + access_token, + &packet.public_key, + &private_key, + uuid, + &packet.server_id, + ) + .await + } } // rust doesn't let us implement From because allegedly it conflicts with diff --git a/bot/src/main.rs b/bot/src/main.rs index 0a291fd8..92786ce1 100644 --- a/bot/src/main.rs +++ b/bot/src/main.rs @@ -7,10 +7,10 @@ use std::sync::Arc; struct State {} #[tokio::main] -async fn main() { +async fn main() -> anyhow::Result<()> { env_logger::init(); - let account = Account::offline("bot"); + let account = Account::microsoft("example2@example.com").await?; azalea::start(azalea::Options { account, @@ -21,6 +21,8 @@ async fn main() { }) .await .unwrap(); + + Ok(()) } async fn handle(bot: Client, event: Arc, _state: Arc>) -> anyhow::Result<()> {