1
2
Fork 0
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:
mat 2022-10-16 22:54:54 -05:00 committed by GitHub
parent 993914d175
commit 4cef62e8e4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 1306 additions and 35 deletions

4
.gitignore vendored
View file

@ -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
View file

@ -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",
]

View file

@ -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"]}

View file

@ -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).

View 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
View 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
View 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
}

View file

@ -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::*;

View 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,
})
}
}
}

View file

@ -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)

View file

@ -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,

View file

@ -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();

View 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
}
}

View file

@ -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;

View file

@ -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()
} }

View file

@ -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.

View file

@ -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

View file

@ -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<()> {