mirror of
https://github.com/mat-1/azalea.git
synced 2025-08-02 14:26:04 +00:00
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
This commit is contained in:
parent
993914d175
commit
4cef62e8e4
18 changed files with 1306 additions and 35 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -3,3 +3,7 @@
|
||||||
flamegraph.svg
|
flamegraph.svg
|
||||||
perf.data
|
perf.data
|
||||||
perf.data.old
|
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
|
||||||
|
|
468
Cargo.lock
generated
468
Cargo.lock
generated
|
@ -117,6 +117,16 @@ name = "azalea-auth"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"azalea-buf",
|
"azalea-buf",
|
||||||
|
"azalea-crypto",
|
||||||
|
"chrono",
|
||||||
|
"env_logger",
|
||||||
|
"log",
|
||||||
|
"num-bigint",
|
||||||
|
"reqwest",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -325,6 +335,12 @@ dependencies = [
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "1.3.2"
|
version = "1.3.2"
|
||||||
|
@ -391,6 +407,12 @@ dependencies = [
|
||||||
"rustc_version",
|
"rustc_version",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cc"
|
||||||
|
version = "1.0.73"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfb8"
|
name = "cfb8"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
|
@ -408,9 +430,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.19"
|
version = "0.4.22"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
|
checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"num-integer",
|
"num-integer",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
@ -437,6 +459,22 @@ dependencies = [
|
||||||
"unicode-width",
|
"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]]
|
[[package]]
|
||||||
name = "cpufeatures"
|
name = "cpufeatures"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
|
@ -590,6 +628,15 @@ version = "1.6.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
|
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]]
|
[[package]]
|
||||||
name = "enum-as-inner"
|
name = "enum-as-inner"
|
||||||
version = "0.3.4"
|
version = "0.3.4"
|
||||||
|
@ -615,6 +662,15 @@ dependencies = [
|
||||||
"termcolor",
|
"termcolor",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastrand"
|
||||||
|
version = "1.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499"
|
||||||
|
dependencies = [
|
||||||
|
"instant",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flate2"
|
name = "flate2"
|
||||||
version = "1.0.24"
|
version = "1.0.24"
|
||||||
|
@ -625,6 +681,27 @@ dependencies = [
|
||||||
"miniz_oxide",
|
"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]]
|
[[package]]
|
||||||
name = "form_urlencoded"
|
name = "form_urlencoded"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
|
@ -742,7 +819,26 @@ checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"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]]
|
[[package]]
|
||||||
|
@ -751,6 +847,12 @@ version = "1.8.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7"
|
checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.12.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
@ -777,12 +879,83 @@ dependencies = [
|
||||||
"winapi",
|
"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]]
|
[[package]]
|
||||||
name = "humantime"
|
name = "humantime"
|
||||||
version = "2.1.0"
|
version = "2.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
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]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "0.2.3"
|
version = "0.2.3"
|
||||||
|
@ -794,6 +967,16 @@ dependencies = [
|
||||||
"unicode-normalization",
|
"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]]
|
[[package]]
|
||||||
name = "inout"
|
name = "inout"
|
||||||
version = "0.1.3"
|
version = "0.1.3"
|
||||||
|
@ -821,7 +1004,7 @@ dependencies = [
|
||||||
"socket2 0.3.19",
|
"socket2 0.3.19",
|
||||||
"widestring",
|
"widestring",
|
||||||
"winapi",
|
"winapi",
|
||||||
"winreg",
|
"winreg 0.6.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -933,6 +1116,12 @@ dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mime"
|
||||||
|
version = "0.3.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
|
@ -954,6 +1143,24 @@ dependencies = [
|
||||||
"windows-sys",
|
"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]]
|
[[package]]
|
||||||
name = "nohash-hasher"
|
name = "nohash-hasher"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
@ -1059,9 +1266,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.12.0"
|
version = "1.15.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225"
|
checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oorandom"
|
name = "oorandom"
|
||||||
|
@ -1069,6 +1276,51 @@ version = "11.1.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575"
|
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]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.11.2"
|
version = "0.11.2"
|
||||||
|
@ -1135,6 +1387,12 @@ version = "0.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pkg-config"
|
||||||
|
version = "0.3.25"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "plotters"
|
name = "plotters"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
|
@ -1279,6 +1537,52 @@ version = "0.6.26"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64"
|
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]]
|
[[package]]
|
||||||
name = "resolv-conf"
|
name = "resolv-conf"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
|
@ -1324,12 +1628,45 @@ dependencies = [
|
||||||
"winapi-util",
|
"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]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
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]]
|
[[package]]
|
||||||
name = "semver"
|
name = "semver"
|
||||||
version = "1.0.10"
|
version = "1.0.10"
|
||||||
|
@ -1338,9 +1675,9 @@ checksum = "a41d061efea015927ac527063765e73601444cdc344ba855bc7bd44578b25e1c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.137"
|
version = "1.0.145"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1"
|
checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
@ -1357,9 +1694,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.137"
|
version = "1.0.145"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be"
|
checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -1368,15 +1705,27 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.81"
|
version = "1.0.86"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c"
|
checksum = "41feea4228a6f1cd09ec7a3593a682276702cd67b5273544757dae23c096f074"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa 1.0.2",
|
"itoa 1.0.2",
|
||||||
"ryu",
|
"ryu",
|
||||||
"serde",
|
"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]]
|
[[package]]
|
||||||
name = "sha-1"
|
name = "sha-1"
|
||||||
version = "0.10.0"
|
version = "0.10.0"
|
||||||
|
@ -1388,6 +1737,15 @@ dependencies = [
|
||||||
"digest",
|
"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]]
|
[[package]]
|
||||||
name = "simple_asn1"
|
name = "simple_asn1"
|
||||||
version = "0.5.4"
|
version = "0.5.4"
|
||||||
|
@ -1444,6 +1802,20 @@ dependencies = [
|
||||||
"unicode-ident",
|
"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]]
|
[[package]]
|
||||||
name = "termcolor"
|
name = "termcolor"
|
||||||
version = "1.1.3"
|
version = "1.1.3"
|
||||||
|
@ -1509,9 +1881,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.21.1"
|
version = "1.21.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0020c875007ad96677dcc890298f4b942882c5d4eb7cc8f439fc3bf813dc9c95"
|
checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
@ -1519,8 +1891,9 @@ dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
"mio",
|
"mio",
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
"once_cell",
|
"parking_lot 0.12.1",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
"signal-hook-registry",
|
||||||
"socket2 0.4.4",
|
"socket2 0.4.4",
|
||||||
"tokio-macros",
|
"tokio-macros",
|
||||||
"winapi",
|
"winapi",
|
||||||
|
@ -1537,6 +1910,16 @@ dependencies = [
|
||||||
"syn",
|
"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]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.7.4"
|
version = "0.7.4"
|
||||||
|
@ -1551,6 +1934,12 @@ dependencies = [
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower-service"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing"
|
name = "tracing"
|
||||||
version = "0.1.35"
|
version = "0.1.35"
|
||||||
|
@ -1616,6 +2005,12 @@ dependencies = [
|
||||||
"trust-dns-proto",
|
"trust-dns-proto",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "try-lock"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.15.0"
|
version = "1.15.0"
|
||||||
|
@ -1667,6 +2062,12 @@ version = "1.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f"
|
checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vcpkg"
|
||||||
|
version = "0.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version_check"
|
name = "version_check"
|
||||||
version = "0.9.4"
|
version = "0.9.4"
|
||||||
|
@ -1685,10 +2086,20 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "want"
|
||||||
version = "0.10.2+wasi-snapshot-preview1"
|
version = "0.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
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]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
|
@ -1721,6 +2132,18 @@ dependencies = [
|
||||||
"wasm-bindgen-shared",
|
"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]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro"
|
name = "wasm-bindgen-macro"
|
||||||
version = "0.2.80"
|
version = "0.2.80"
|
||||||
|
@ -1848,3 +2271,12 @@ checksum = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winreg"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
|
||||||
|
dependencies = [
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
|
@ -9,4 +9,17 @@ version = "0.1.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
azalea-buf = {path = "../azalea-buf", version = "^0.1.0"}
|
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"
|
uuid = "^1.1.2"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
env_logger = "0.9.1"
|
||||||
|
tokio = {version = "1.21.2", features = ["full"]}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
# Azalea Auth
|
# 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).
|
||||||
|
|
19
azalea-auth/examples/auth.rs
Normal file
19
azalea-auth/examples/auth.rs
Normal file
|
@ -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);
|
||||||
|
}
|
482
azalea-auth/src/auth.rs
Normal file
482
azalea-auth/src/auth.rs
Normal file
|
@ -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<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<AuthResult, AuthError> {
|
||||||
|
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<String, Vec<HashMap<String, String>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<String>,
|
||||||
|
access_token: String,
|
||||||
|
token_type: String,
|
||||||
|
expires_in: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct GameOwnershipResponse {
|
||||||
|
items: Vec<GameOwnershipItem>,
|
||||||
|
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<serde_json::Value>,
|
||||||
|
pub capes: Vec<serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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<ExpiringValue<AccessTokenResponse>, 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::<DeviceCodeResponse>()
|
||||||
|
.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::<AccessTokenResponse>()
|
||||||
|
.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<ExpiringValue<AccessTokenResponse>, 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::<AccessTokenResponse>()
|
||||||
|
.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<ExpiringValue<XboxLiveAuth>, 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::<XboxLiveAuthResponse>()
|
||||||
|
.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<String, MinecraftXstsAuthError> {
|
||||||
|
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::<XboxLiveAuthResponse>()
|
||||||
|
.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<ExpiringValue<MinecraftAuthResponse>, 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::<MinecraftAuthResponse>()
|
||||||
|
.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<bool, CheckOwnershipError> {
|
||||||
|
let res = client
|
||||||
|
.get("https://api.minecraftservices.com/entitlements/mcstore")
|
||||||
|
.header(
|
||||||
|
"Authorization",
|
||||||
|
format!("Bearer {}", minecraft_access_token),
|
||||||
|
)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.json::<GameOwnershipResponse>()
|
||||||
|
.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<ProfileResponse, GetProfileError> {
|
||||||
|
let res = client
|
||||||
|
.get("https://api.minecraftservices.com/minecraft/profile")
|
||||||
|
.header(
|
||||||
|
"Authorization",
|
||||||
|
format!("Bearer {}", minecraft_access_token),
|
||||||
|
)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.json::<ProfileResponse>()
|
||||||
|
.await?;
|
||||||
|
log::trace!("{:?}", res);
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
105
azalea-auth/src/cache.rs
Normal file
105
azalea-auth/src/cache.rs
Normal file
|
@ -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<crate::auth::AccessTokenResponse>,
|
||||||
|
/// Xbox Live auth
|
||||||
|
pub xbl: ExpiringValue<crate::auth::XboxLiveAuth>,
|
||||||
|
/// Minecraft auth
|
||||||
|
pub mca: ExpiringValue<crate::auth::MinecraftAuthResponse>,
|
||||||
|
/// The user's Minecraft profile (i.e. username, UUID, skin)
|
||||||
|
pub profile: crate::auth::ProfileResponse,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
|
pub struct ExpiringValue<T> {
|
||||||
|
/// Seconds since the UNIX epoch
|
||||||
|
pub expires_at: u64,
|
||||||
|
pub data: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> ExpiringValue<T> {
|
||||||
|
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<Vec<CachedAccount>, CacheError> {
|
||||||
|
let mut cache: Vec<CachedAccount> = 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<CachedAccount>) -> 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<CachedAccount> {
|
||||||
|
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
|
||||||
|
}
|
|
@ -1,3 +1,8 @@
|
||||||
//! Handle Minecraft authentication.
|
#![feature(let_chains)]
|
||||||
|
|
||||||
|
mod auth;
|
||||||
|
mod cache;
|
||||||
pub mod game_profile;
|
pub mod game_profile;
|
||||||
|
pub mod sessionserver;
|
||||||
|
|
||||||
|
pub use auth::*;
|
||||||
|
|
79
azalea-auth/src/sessionserver.rs
Normal file
79
azalea-auth/src/sessionserver.rs
Normal file
|
@ -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::<ForbiddenError>().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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
impl McBufWritable for u32 {
|
||||||
fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
|
fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
|
||||||
i32::write_into(&(*self as i32), buf)
|
i32::write_into(&(*self as i32), buf)
|
||||||
|
|
|
@ -1,20 +1,45 @@
|
||||||
//! Connect to Minecraft servers.
|
//! Connect to Minecraft servers.
|
||||||
|
|
||||||
use crate::{client::JoinError, Client, Event};
|
use crate::{client::JoinError, get_mc_dir, Client, Event};
|
||||||
use azalea_protocol::ServerAddress;
|
use azalea_protocol::ServerAddress;
|
||||||
use tokio::sync::mpsc::UnboundedReceiver;
|
use tokio::sync::mpsc::UnboundedReceiver;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
/// Something that can join Minecraft servers.
|
/// Something that can join Minecraft servers.
|
||||||
pub struct Account {
|
pub struct Account {
|
||||||
pub username: String,
|
pub username: String,
|
||||||
|
/// The access token for authentication. You can obtain one of these
|
||||||
|
/// manually from azalea-auth.
|
||||||
|
pub access_token: Option<String>,
|
||||||
|
/// Only required for online-mode accounts.
|
||||||
|
pub uuid: Option<uuid::Uuid>,
|
||||||
}
|
}
|
||||||
impl Account {
|
impl Account {
|
||||||
pub fn offline(username: &str) -> Self {
|
pub fn offline(username: &str) -> Self {
|
||||||
Self {
|
Self {
|
||||||
username: username.to_string(),
|
username: username.to_string(),
|
||||||
|
access_token: None,
|
||||||
|
uuid: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn microsoft(email: &str) -> Result<Self, azalea_auth::AuthError> {
|
||||||
|
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.
|
/// Joins the Minecraft server on the given address using this account.
|
||||||
pub async fn join(
|
pub async fn join(
|
||||||
&self,
|
&self,
|
||||||
|
|
|
@ -38,7 +38,6 @@ use std::{
|
||||||
};
|
};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tokio::{
|
use tokio::{
|
||||||
io::AsyncWriteExt,
|
|
||||||
sync::mpsc::{self, UnboundedReceiver, UnboundedSender},
|
sync::mpsc::{self, UnboundedReceiver, UnboundedSender},
|
||||||
task::JoinHandle,
|
task::JoinHandle,
|
||||||
time::{self},
|
time::{self},
|
||||||
|
@ -105,6 +104,8 @@ pub enum JoinError {
|
||||||
ReadPacket(#[from] azalea_protocol::read::ReadPacketError),
|
ReadPacket(#[from] azalea_protocol::read::ReadPacketError),
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
Io(#[from] io::Error),
|
Io(#[from] io::Error),
|
||||||
|
#[error("{0}")]
|
||||||
|
SessionServer(#[from] azalea_auth::sessionserver::SessionServerError),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
|
@ -159,7 +160,17 @@ impl Client {
|
||||||
debug!("Got encryption request");
|
debug!("Got encryption request");
|
||||||
let e = azalea_crypto::encrypt(&p.public_key, &p.nonce).unwrap();
|
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(
|
conn.write(
|
||||||
ServerboundKeyPacket {
|
ServerboundKeyPacket {
|
||||||
|
@ -171,6 +182,7 @@ impl Client {
|
||||||
.get(),
|
.get(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
conn.set_encryption_key(e.secret_key);
|
conn.set_encryption_key(e.secret_key);
|
||||||
}
|
}
|
||||||
ClientboundLoginPacket::LoginCompression(p) => {
|
ClientboundLoginPacket::LoginCompression(p) => {
|
||||||
|
@ -237,7 +249,7 @@ impl Client {
|
||||||
|
|
||||||
/// Disconnect from the server, ending all tasks.
|
/// Disconnect from the server, ending all tasks.
|
||||||
pub async fn shutdown(self) -> Result<(), std::io::Error> {
|
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();
|
let tasks = self.tasks.lock();
|
||||||
for task in tasks.iter() {
|
for task in tasks.iter() {
|
||||||
task.abort();
|
task.abort();
|
||||||
|
|
34
azalea-client/src/get_mc_dir.rs
Normal file
34
azalea-client/src/get_mc_dir.rs
Normal file
|
@ -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<PathBuf> {
|
||||||
|
#[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
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
mod account;
|
mod account;
|
||||||
mod client;
|
mod client;
|
||||||
|
mod get_mc_dir;
|
||||||
mod movement;
|
mod movement;
|
||||||
pub mod ping;
|
pub mod ping;
|
||||||
mod player;
|
mod player;
|
||||||
|
|
|
@ -18,8 +18,8 @@ fn generate_secret_key() -> [u8; 16] {
|
||||||
pub fn digest_data(server_id: &[u8], public_key: &[u8], private_key: &[u8]) -> Vec<u8> {
|
pub fn digest_data(server_id: &[u8], public_key: &[u8], private_key: &[u8]) -> Vec<u8> {
|
||||||
let mut digest = Sha1::new();
|
let mut digest = Sha1::new();
|
||||||
digest.update(server_id);
|
digest.update(server_id);
|
||||||
digest.update(public_key);
|
|
||||||
digest.update(private_key);
|
digest.update(private_key);
|
||||||
|
digest.update(public_key);
|
||||||
digest.finalize().to_vec()
|
digest.finalize().to_vec()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# Azalea Protocol
|
# 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.
|
The goal is to only support the latest Minecraft version in order to ease development.
|
||||||
|
|
||||||
|
|
|
@ -2,32 +2,36 @@
|
||||||
|
|
||||||
use crate::packets::game::{ClientboundGamePacket, ServerboundGamePacket};
|
use crate::packets::game::{ClientboundGamePacket, ServerboundGamePacket};
|
||||||
use crate::packets::handshake::{ClientboundHandshakePacket, ServerboundHandshakePacket};
|
use crate::packets::handshake::{ClientboundHandshakePacket, ServerboundHandshakePacket};
|
||||||
|
use crate::packets::login::clientbound_hello_packet::ClientboundHelloPacket;
|
||||||
use crate::packets::login::{ClientboundLoginPacket, ServerboundLoginPacket};
|
use crate::packets::login::{ClientboundLoginPacket, ServerboundLoginPacket};
|
||||||
use crate::packets::status::{ClientboundStatusPacket, ServerboundStatusPacket};
|
use crate::packets::status::{ClientboundStatusPacket, ServerboundStatusPacket};
|
||||||
use crate::packets::ProtocolPacket;
|
use crate::packets::ProtocolPacket;
|
||||||
use crate::read::{read_packet, ReadPacketError};
|
use crate::read::{read_packet, ReadPacketError};
|
||||||
use crate::write::write_packet;
|
use crate::write::write_packet;
|
||||||
use crate::ServerIpAddress;
|
use crate::ServerIpAddress;
|
||||||
|
use azalea_auth::sessionserver::SessionServerError;
|
||||||
use azalea_crypto::{Aes128CfbDec, Aes128CfbEnc};
|
use azalea_crypto::{Aes128CfbDec, Aes128CfbEnc};
|
||||||
use bytes::BytesMut;
|
use bytes::BytesMut;
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf};
|
use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf};
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub struct ReadConnection<R: ProtocolPacket> {
|
pub struct ReadConnection<R: ProtocolPacket> {
|
||||||
pub read_stream: OwnedReadHalf,
|
read_stream: OwnedReadHalf,
|
||||||
buffer: BytesMut,
|
buffer: BytesMut,
|
||||||
pub compression_threshold: Option<u32>,
|
compression_threshold: Option<u32>,
|
||||||
pub dec_cipher: Option<Aes128CfbDec>,
|
dec_cipher: Option<Aes128CfbDec>,
|
||||||
_reading: PhantomData<R>,
|
_reading: PhantomData<R>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct WriteConnection<W: ProtocolPacket> {
|
pub struct WriteConnection<W: ProtocolPacket> {
|
||||||
pub write_stream: OwnedWriteHalf,
|
write_stream: OwnedWriteHalf,
|
||||||
pub compression_threshold: Option<u32>,
|
compression_threshold: Option<u32>,
|
||||||
pub enc_cipher: Option<Aes128CfbEnc>,
|
enc_cipher: Option<Aes128CfbEnc>,
|
||||||
_writing: PhantomData<W>,
|
_writing: PhantomData<W>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,6 +68,10 @@ where
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn shutdown(&mut self) -> std::io::Result<()> {
|
||||||
|
self.write_stream.shutdown().await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<R, W> Connection<R, W>
|
impl<R, W> Connection<R, W>
|
||||||
|
@ -145,13 +153,54 @@ impl Connection<ClientboundLoginPacket, ServerboundLoginPacket> {
|
||||||
pub fn set_encryption_key(&mut self, key: [u8; 16]) {
|
pub fn set_encryption_key(&mut self, key: [u8; 16]) {
|
||||||
// minecraft has a cipher decoder and encoder, i don't think it matters though?
|
// minecraft has a cipher decoder and encoder, i don't think it matters though?
|
||||||
let (enc_cipher, dec_cipher) = azalea_crypto::create_cipher(&key);
|
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.reader.dec_cipher = Some(dec_cipher);
|
||||||
|
self.writer.enc_cipher = Some(enc_cipher);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn game(self) -> Connection<ClientboundGamePacket, ServerboundGamePacket> {
|
pub fn game(self) -> Connection<ClientboundGamePacket, ServerboundGamePacket> {
|
||||||
Connection::from(self)
|
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
|
// rust doesn't let us implement From because allegedly it conflicts with
|
||||||
|
|
|
@ -7,10 +7,10 @@ use std::sync::Arc;
|
||||||
struct State {}
|
struct State {}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() -> anyhow::Result<()> {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
|
|
||||||
let account = Account::offline("bot");
|
let account = Account::microsoft("example2@example.com").await?;
|
||||||
|
|
||||||
azalea::start(azalea::Options {
|
azalea::start(azalea::Options {
|
||||||
account,
|
account,
|
||||||
|
@ -21,6 +21,8 @@ async fn main() {
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle(bot: Client, event: Arc<Event>, _state: Arc<Mutex<State>>) -> anyhow::Result<()> {
|
async fn handle(bot: Client, event: Arc<Event>, _state: Arc<Mutex<State>>) -> anyhow::Result<()> {
|
||||||
|
|
Loading…
Add table
Reference in a new issue