aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormat <27899617+mat-1@users.noreply.github.com>2022-10-16 22:54:54 -0500
committerGitHub <noreply@github.com>2022-10-16 22:54:54 -0500
commit4cef62e8e4aa04e44048eb67e5091c12a73d2a09 (patch)
tree1c3b03bad262bdcab878cd42d445676290000bea
parent993914d175609e5d291e7caafc1983379642e7fe (diff)
downloadazalea-drasl-4cef62e8e4aa04e44048eb67e5091c12a73d2a09.tar.xz
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
-rw-r--r--.gitignore4
-rwxr-xr-xCargo.lock466
-rwxr-xr-xazalea-auth/Cargo.toml13
-rw-r--r--azalea-auth/README.md4
-rw-r--r--azalea-auth/examples/auth.rs19
-rw-r--r--azalea-auth/src/auth.rs482
-rw-r--r--azalea-auth/src/cache.rs105
-rwxr-xr-xazalea-auth/src/lib.rs7
-rw-r--r--azalea-auth/src/sessionserver.rs79
-rw-r--r--azalea-buf/src/write.rs7
-rw-r--r--azalea-client/src/account.rs27
-rw-r--r--azalea-client/src/client.rs18
-rw-r--r--azalea-client/src/get_mc_dir.rs34
-rwxr-xr-xazalea-client/src/lib.rs1
-rw-r--r--azalea-crypto/src/lib.rs2
-rw-r--r--azalea-protocol/README.md2
-rw-r--r--azalea-protocol/src/connect.rs63
-rw-r--r--bot/src/main.rs6
18 files changed, 1305 insertions, 34 deletions
diff --git a/.gitignore b/.gitignore
index ad9bfc78..10e5a00f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,7 @@
flamegraph.svg
perf.data
perf.data.old
+
+# created by azalea-auth/examples/auth, defined in the main .gitignore because
+# the example could be run from anywhere
+example_cache.json
diff --git a/Cargo.lock b/Cargo.lock
index 128bea90..6b7aa08c 100755
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -117,6 +117,16 @@ name = "azalea-auth"
version = "0.1.0"
dependencies = [
"azalea-buf",
+ "azalea-crypto",
+ "chrono",
+ "env_logger",
+ "log",
+ "num-bigint",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "tokio",
"uuid",
]
@@ -326,6 +336,12 @@ dependencies = [
]
[[package]]
+name = "base64"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
+
+[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -392,6 +408,12 @@ dependencies = [
]
[[package]]
+name = "cc"
+version = "1.0.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
+
+[[package]]
name = "cfb8"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -408,9 +430,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
-version = "0.4.19"
+version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
+checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1"
dependencies = [
"num-integer",
"num-traits",
@@ -438,6 +460,22 @@ dependencies = [
]
[[package]]
+name = "core-foundation"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
+
+[[package]]
name = "cpufeatures"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -591,6 +629,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
+name = "encoding_rs"
+version = "0.8.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
name = "enum-as-inner"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -616,6 +663,15 @@ dependencies = [
]
[[package]]
+name = "fastrand"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499"
+dependencies = [
+ "instant",
+]
+
+[[package]]
name = "flate2"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -626,6 +682,27 @@ dependencies = [
]
[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
name = "form_urlencoded"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -742,7 +819,26 @@ checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad"
dependencies = [
"cfg-if",
"libc",
- "wasi 0.10.2+wasi-snapshot-preview1",
+ "wasi 0.10.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "h2"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ca32592cf21ac7ccab1825cd87f6c9b3d9022c44d086172ed0966bec8af30be"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
]
[[package]]
@@ -752,6 +848,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7"
[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[package]]
name = "heck"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -778,12 +880,83 @@ dependencies = [
]
[[package]]
+name = "http"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa 1.0.2",
+]
+
+[[package]]
+name = "http-body"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
+dependencies = [
+ "bytes",
+ "http",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
+
+[[package]]
+name = "httpdate"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
+
+[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
+name = "hyper"
+version = "0.14.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa 1.0.2",
+ "pin-project-lite",
+ "socket2 0.4.4",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "want",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
+dependencies = [
+ "bytes",
+ "hyper",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+]
+
+[[package]]
name = "idna"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -795,6 +968,16 @@ dependencies = [
]
[[package]]
+name = "indexmap"
+version = "1.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
+dependencies = [
+ "autocfg",
+ "hashbrown",
+]
+
+[[package]]
name = "inout"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -821,7 +1004,7 @@ dependencies = [
"socket2 0.3.19",
"widestring",
"winapi",
- "winreg",
+ "winreg 0.6.2",
]
[[package]]
@@ -934,6 +1117,12 @@ dependencies = [
]
[[package]]
+name = "mime"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
+
+[[package]]
name = "miniz_oxide"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -955,6 +1144,24 @@ dependencies = [
]
[[package]]
+name = "native-tls"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9"
+dependencies = [
+ "lazy_static",
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
name = "nohash-hasher"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1059,9 +1266,9 @@ dependencies = [
[[package]]
name = "once_cell"
-version = "1.12.0"
+version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225"
+checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
[[package]]
name = "oorandom"
@@ -1070,6 +1277,51 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575"
[[package]]
+name = "openssl"
+version = "0.10.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12fc0523e3bd51a692c8850d075d74dc062ccf251c0110668cbd921917118a13"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.76"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5230151e44c0f05157effb743e8d517472843121cf9243e8b81393edb5acd9ce"
+dependencies = [
+ "autocfg",
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
name = "parking_lot"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1136,6 +1388,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
+name = "pkg-config"
+version = "0.3.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"
+
+[[package]]
name = "plotters"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1280,6 +1538,52 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64"
[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "reqwest"
+version = "0.11.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "431949c384f4e2ae07605ccaa56d1d9d2ecdb5cadd4f9577ccfab29f2e5149fc"
+dependencies = [
+ "base64",
+ "bytes",
+ "encoding_rs",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "hyper",
+ "hyper-tls",
+ "ipnet",
+ "js-sys",
+ "log",
+ "mime",
+ "native-tls",
+ "once_cell",
+ "percent-encoding",
+ "pin-project-lite",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "tokio",
+ "tokio-native-tls",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "winreg 0.10.1",
+]
+
+[[package]]
name = "resolv-conf"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1325,12 +1629,45 @@ dependencies = [
]
[[package]]
+name = "schannel"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2"
+dependencies = [
+ "lazy_static",
+ "windows-sys",
+]
+
+[[package]]
name = "scopeguard"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
+name = "security-framework"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
name = "semver"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1338,9 +1675,9 @@ checksum = "a41d061efea015927ac527063765e73601444cdc344ba855bc7bd44578b25e1c"
[[package]]
name = "serde"
-version = "1.0.137"
+version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1"
+checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b"
dependencies = [
"serde_derive",
]
@@ -1357,9 +1694,9 @@ dependencies = [
[[package]]
name = "serde_derive"
-version = "1.0.137"
+version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be"
+checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c"
dependencies = [
"proc-macro2",
"quote",
@@ -1368,9 +1705,9 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.81"
+version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c"
+checksum = "41feea4228a6f1cd09ec7a3593a682276702cd67b5273544757dae23c096f074"
dependencies = [
"itoa 1.0.2",
"ryu",
@@ -1378,6 +1715,18 @@ dependencies = [
]
[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa 1.0.2",
+ "ryu",
+ "serde",
+]
+
+[[package]]
name = "sha-1"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1389,6 +1738,15 @@ dependencies = [
]
[[package]]
+name = "signal-hook-registry"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
+dependencies = [
+ "libc",
+]
+
+[[package]]
name = "simple_asn1"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1445,6 +1803,20 @@ dependencies = [
]
[[package]]
+name = "tempfile"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "libc",
+ "redox_syscall",
+ "remove_dir_all",
+ "winapi",
+]
+
+[[package]]
name = "termcolor"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1509,9 +1881,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tokio"
-version = "1.21.1"
+version = "1.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0020c875007ad96677dcc890298f4b942882c5d4eb7cc8f439fc3bf813dc9c95"
+checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099"
dependencies = [
"autocfg",
"bytes",
@@ -1519,8 +1891,9 @@ dependencies = [
"memchr",
"mio",
"num_cpus",
- "once_cell",
+ "parking_lot 0.12.1",
"pin-project-lite",
+ "signal-hook-registry",
"socket2 0.4.4",
"tokio-macros",
"winapi",
@@ -1538,6 +1911,16 @@ dependencies = [
]
[[package]]
+name = "tokio-native-tls"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
name = "tokio-util"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1552,6 +1935,12 @@ dependencies = [
]
[[package]]
+name = "tower-service"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
+
+[[package]]
name = "tracing"
version = "0.1.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1617,6 +2006,12 @@ dependencies = [
]
[[package]]
+name = "try-lock"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
+
+[[package]]
name = "typenum"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1668,6 +2063,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f"
[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1685,10 +2086,20 @@ dependencies = [
]
[[package]]
+name = "want"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0"
+dependencies = [
+ "log",
+ "try-lock",
+]
+
+[[package]]
name = "wasi"
-version = "0.10.2+wasi-snapshot-preview1"
+version = "0.10.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
+checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
[[package]]
name = "wasi"
@@ -1722,6 +2133,18 @@ dependencies = [
]
[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f741de44b75e14c35df886aff5f1eb73aa114fa5d4d00dcd37b5e01259bf3b2"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
name = "wasm-bindgen-macro"
version = "0.2.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1848,3 +2271,12 @@ checksum = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9"
dependencies = [
"winapi",
]
+
+[[package]]
+name = "winreg"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
+dependencies = [
+ "winapi",
+]
diff --git a/azalea-auth/Cargo.toml b/azalea-auth/Cargo.toml
index 2f817354..34fec8f7 100755
--- a/azalea-auth/Cargo.toml
+++ b/azalea-auth/Cargo.toml
@@ -9,4 +9,17 @@ version = "0.1.0"
[dependencies]
azalea-buf = {path = "../azalea-buf", version = "^0.1.0"}
+azalea-crypto = {path = "../azalea-crypto", version = "^0.1.0"}
+chrono = {version = "0.4.22", default-features = false}
+log = "0.4.17"
+num-bigint = "0.4.3"
+reqwest = {version = "0.11.12", features = ["json"]}
+serde = {version = "1.0.145", features = ["derive"]}
+serde_json = "1.0.86"
+thiserror = "1.0.37"
+tokio = "1.21.2"
uuid = "^1.1.2"
+
+[dev-dependencies]
+env_logger = "0.9.1"
+tokio = {version = "1.21.2", features = ["full"]}
diff --git a/azalea-auth/README.md b/azalea-auth/README.md
index fa87afca..aa290c94 100644
--- a/azalea-auth/README.md
+++ b/azalea-auth/README.md
@@ -1,3 +1,5 @@
# Azalea Auth
-A port of Mojang's Authlib, except authentication isn't actually implemented yet.
+A port of Mojang's Authlib and launcher authentication.
+
+Thanks to [wiki.vg contributors](https://wiki.vg/Microsoft_Authentication_Scheme), [Overhash](https://gist.github.com/OverHash/a71b32846612ba09d8f79c9d775bfadf), and [prismarine-auth contributors](https://github.com/PrismarineJS/prismarine-auth).
diff --git a/azalea-auth/examples/auth.rs b/azalea-auth/examples/auth.rs
new file mode 100644
index 00000000..8f7cf7f9
--- /dev/null
+++ b/azalea-auth/examples/auth.rs
@@ -0,0 +1,19 @@
+use std::path::PathBuf;
+
+#[tokio::main]
+async fn main() {
+ env_logger::init();
+
+ let cache_file = PathBuf::from("example_cache.json");
+
+ let auth_result = azalea_auth::auth(
+ "example@example.com",
+ azalea_auth::AuthOpts {
+ cache_file: Some(cache_file),
+ ..Default::default()
+ },
+ )
+ .await
+ .unwrap();
+ println!("{:?}", auth_result);
+}
diff --git a/azalea-auth/src/auth.rs b/azalea-auth/src/auth.rs
new file mode 100644
index 00000000..5f96d4be
--- /dev/null
+++ b/azalea-auth/src/auth.rs
@@ -0,0 +1,482 @@
+//! Handle Minecraft (Xbox) authentication.
+
+use crate::cache::{self, CachedAccount, ExpiringValue};
+use chrono::{DateTime, Utc};
+use serde::{Deserialize, Serialize};
+use serde_json::json;
+use std::{
+ collections::HashMap,
+ path::PathBuf,
+ time::{Instant, SystemTime, UNIX_EPOCH},
+};
+use thiserror::Error;
+
+#[derive(Default)]
+pub struct AuthOpts {
+ /// Whether we should check if the user actually owns the game. This will
+ /// fail if the user has Xbox Game Pass! Note that this isn't really
+ /// necessary, since getting the user profile will check this anyways.
+ pub check_ownership: bool,
+ // /// Whether we should get the Minecraft profile data (i.e. username, uuid,
+ // /// skin, etc) for the player.
+ // pub get_profile: bool,
+ /// The directory to store the cache in. If this is not set, caching is not
+ /// done.
+ pub cache_file: Option<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)
+}
diff --git a/azalea-auth/src/cache.rs b/azalea-auth/src/cache.rs
new file mode 100644
index 00000000..8af9e171
--- /dev/null
+++ b/azalea-auth/src/cache.rs
@@ -0,0 +1,105 @@
+//! Cache auth information
+
+use serde::{Deserialize, Serialize};
+use std::path::Path;
+use std::time::{SystemTime, UNIX_EPOCH};
+use thiserror::Error;
+use tokio::fs::File;
+use tokio::io::{AsyncReadExt, AsyncWriteExt};
+
+#[derive(Debug, Error)]
+pub enum CacheError {
+ #[error("Failed to read cache file: {0}")]
+ Read(std::io::Error),
+ #[error("Failed to write cache file: {0}")]
+ Write(std::io::Error),
+ #[error("Failed to parse cache file: {0}")]
+ Parse(serde_json::Error),
+}
+
+#[derive(Deserialize, Serialize, Debug)]
+pub struct CachedAccount {
+ pub email: String,
+ /// Microsoft auth
+ pub msa: ExpiringValue<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
+}
diff --git a/azalea-auth/src/lib.rs b/azalea-auth/src/lib.rs
index 773ea1d9..03e15c71 100755
--- a/azalea-auth/src/lib.rs
+++ b/azalea-auth/src/lib.rs
@@ -1,3 +1,8 @@
-//! Handle Minecraft authentication.
+#![feature(let_chains)]
+mod auth;
+mod cache;
pub mod game_profile;
+pub mod sessionserver;
+
+pub use auth::*;
diff --git a/azalea-auth/src/sessionserver.rs b/azalea-auth/src/sessionserver.rs
new file mode 100644
index 00000000..31857bc0
--- /dev/null
+++ b/azalea-auth/src/sessionserver.rs
@@ -0,0 +1,79 @@
+//! Tell Mojang you're joining a multiplayer server.
+//!
+use serde::Deserialize;
+use serde_json::json;
+use thiserror::Error;
+use uuid::Uuid;
+
+#[derive(Debug, Error)]
+pub enum SessionServerError {
+ #[error("Error sending HTTP request to sessionserver: {0}")]
+ HttpError(#[from] reqwest::Error),
+ #[error("Multiplayer is not enabled for this account")]
+ MultiplayerDisabled,
+ #[error("This account has been banned from multiplayer")]
+ Banned,
+ #[error("Unknown sessionserver error: {0}")]
+ Unknown(String),
+ #[error("Unexpected response from sessionserver (status code {status_code}): {body}")]
+ UnexpectedResponse { status_code: u16, body: String },
+}
+
+#[derive(Deserialize)]
+pub struct ForbiddenError {
+ pub error: String,
+ pub path: String,
+}
+
+/// Tell Mojang's servers that you are going to join a multiplayer server,
+/// which is required to join online-mode servers. The server ID is an empty
+/// string.
+pub async fn join(
+ access_token: &str,
+ public_key: &[u8],
+ private_key: &[u8],
+ uuid: &Uuid,
+ server_id: &str,
+) -> Result<(), SessionServerError> {
+ let client = reqwest::Client::new();
+
+ let server_hash = azalea_crypto::hex_digest(&azalea_crypto::digest_data(
+ server_id.as_bytes(),
+ public_key,
+ private_key,
+ ));
+
+ let mut encode_buffer = Uuid::encode_buffer();
+ let undashed_uuid = uuid.simple().encode_lower(&mut encode_buffer);
+
+ let data = json!({
+ "accessToken": access_token,
+ "selectedProfile": undashed_uuid,
+ "serverId": server_hash
+ });
+ println!("data: {:?}", data);
+ let res = client
+ .post("https://sessionserver.mojang.com/session/minecraft/join")
+ .json(&data)
+ .send()
+ .await?;
+
+ match res.status() {
+ reqwest::StatusCode::NO_CONTENT => Ok(()),
+ reqwest::StatusCode::FORBIDDEN => {
+ let forbidden = res.json::<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,
+ })
+ }
+ }
+}
diff --git a/azalea-buf/src/write.rs b/azalea-buf/src/write.rs
index 9b50a1c4..e1f1ffb1 100644
--- a/azalea-buf/src/write.rs
+++ b/azalea-buf/src/write.rs
@@ -110,6 +110,13 @@ impl McBufWritable for String {
}
}
+
+impl McBufWritable for &str {
+ fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
+ write_utf_with_len(buf, self, MAX_STRING_LENGTH.into())
+ }
+}
+
impl McBufWritable for u32 {
fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
i32::write_into(&(*self as i32), buf)
diff --git a/azalea-client/src/account.rs b/azalea-client/src/account.rs
index c554908f..94e84ab8 100644
--- a/azalea-client/src/account.rs
+++ b/azalea-client/src/account.rs
@@ -1,20 +1,45 @@
//! Connect to Minecraft servers.
-use crate::{client::JoinError, Client, Event};
+use crate::{client::JoinError, get_mc_dir, Client, Event};
use azalea_protocol::ServerAddress;
use tokio::sync::mpsc::UnboundedReceiver;
+use uuid::Uuid;
/// Something that can join Minecraft servers.
pub struct Account {
pub username: String,
+ /// The access token for authentication. You can obtain one of these
+ /// manually from azalea-auth.
+ pub access_token: Option<String>,
+ /// Only required for online-mode accounts.
+ pub uuid: Option<uuid::Uuid>,
}
impl Account {
pub fn offline(username: &str) -> Self {
Self {
username: username.to_string(),
+ access_token: None,
+ uuid: None,
}
}
+ pub async fn microsoft(email: &str) -> Result<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.
pub async fn join(
&self,
diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs
index 09f68c4a..25c68c5d 100644
--- a/azalea-client/src/client.rs
+++ b/azalea-client/src/client.rs
@@ -38,7 +38,6 @@ use std::{
};
use thiserror::Error;
use tokio::{
- io::AsyncWriteExt,
sync::mpsc::{self, UnboundedReceiver, UnboundedSender},
task::JoinHandle,
time::{self},
@@ -105,6 +104,8 @@ pub enum JoinError {
ReadPacket(#[from] azalea_protocol::read::ReadPacketError),
#[error("{0}")]
Io(#[from] io::Error),
+ #[error("{0}")]
+ SessionServer(#[from] azalea_auth::sessionserver::SessionServerError),
}
#[derive(Error, Debug)]
@@ -159,7 +160,17 @@ impl Client {
debug!("Got encryption request");
let e = azalea_crypto::encrypt(&p.public_key, &p.nonce).unwrap();
- // TODO: authenticate with the server here (authenticateServer)
+ if let Some(access_token) = &account.access_token {
+ conn.authenticate(
+ access_token,
+ &account
+ .uuid
+ .expect("Uuid must be present if access token is present."),
+ e.secret_key,
+ p,
+ )
+ .await?;
+ }
conn.write(
ServerboundKeyPacket {
@@ -171,6 +182,7 @@ impl Client {
.get(),
)
.await?;
+
conn.set_encryption_key(e.secret_key);
}
ClientboundLoginPacket::LoginCompression(p) => {
@@ -237,7 +249,7 @@ impl Client {
/// Disconnect from the server, ending all tasks.
pub async fn shutdown(self) -> Result<(), std::io::Error> {
- self.write_conn.lock().await.write_stream.shutdown().await?;
+ self.write_conn.lock().await.shutdown().await?;
let tasks = self.tasks.lock();
for task in tasks.iter() {
task.abort();
diff --git a/azalea-client/src/get_mc_dir.rs b/azalea-client/src/get_mc_dir.rs
new file mode 100644
index 00000000..abc5b3c8
--- /dev/null
+++ b/azalea-client/src/get_mc_dir.rs
@@ -0,0 +1,34 @@
+//! Find out where the user's .minecraft directory is.
+//!
+//! Used for the auth cache.
+
+use std::path::PathBuf;
+
+/// Return the location of the user's .minecraft directory.
+///
+/// Windows: `%appdata%\.minecraft`\
+/// Mac: `$HOME/Library/Application Support/minecraft`\
+/// Linux: `$HOME/.minecraft`
+///
+/// Anywhere else it'll return None.
+pub fn minecraft_dir() -> Option<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
+ }
+}
diff --git a/azalea-client/src/lib.rs b/azalea-client/src/lib.rs
index a918ca32..c30ca103 100755
--- a/azalea-client/src/lib.rs
+++ b/azalea-client/src/lib.rs
@@ -2,6 +2,7 @@
mod account;
mod client;
+mod get_mc_dir;
mod movement;
pub mod ping;
mod player;
diff --git a/azalea-crypto/src/lib.rs b/azalea-crypto/src/lib.rs
index d1a86ec7..30362e5d 100644
--- a/azalea-crypto/src/lib.rs
+++ b/azalea-crypto/src/lib.rs
@@ -18,8 +18,8 @@ fn generate_secret_key() -> [u8; 16] {
pub fn digest_data(server_id: &[u8], public_key: &[u8], private_key: &[u8]) -> Vec<u8> {
let mut digest = Sha1::new();
digest.update(server_id);
- digest.update(public_key);
digest.update(private_key);
+ digest.update(public_key);
digest.finalize().to_vec()
}
diff --git a/azalea-protocol/README.md b/azalea-protocol/README.md
index a210e4a6..7bc1f4c0 100644
--- a/azalea-protocol/README.md
+++ b/azalea-protocol/README.md
@@ -1,6 +1,6 @@
# Azalea Protocol
-Send and receive Minecraft packets. You should probably use `azalea` or `azalea-client` instead.
+A low-level crate to send and receive Minecraft packets. You should probably use `azalea` or `azalea-client` instead.
The goal is to only support the latest Minecraft version in order to ease development.
diff --git a/azalea-protocol/src/connect.rs b/azalea-protocol/src/connect.rs
index d7b9bd1d..03c56471 100644
--- a/azalea-protocol/src/connect.rs
+++ b/azalea-protocol/src/connect.rs
@@ -2,32 +2,36 @@
use crate::packets::game::{ClientboundGamePacket, ServerboundGamePacket};
use crate::packets::handshake::{ClientboundHandshakePacket, ServerboundHandshakePacket};
+use crate::packets::login::clientbound_hello_packet::ClientboundHelloPacket;
use crate::packets::login::{ClientboundLoginPacket, ServerboundLoginPacket};
use crate::packets::status::{ClientboundStatusPacket, ServerboundStatusPacket};
use crate::packets::ProtocolPacket;
use crate::read::{read_packet, ReadPacketError};
use crate::write::write_packet;
use crate::ServerIpAddress;
+use azalea_auth::sessionserver::SessionServerError;
use azalea_crypto::{Aes128CfbDec, Aes128CfbEnc};
use bytes::BytesMut;
use std::fmt::Debug;
use std::marker::PhantomData;
use thiserror::Error;
+use tokio::io::AsyncWriteExt;
use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf};
use tokio::net::TcpStream;
+use uuid::Uuid;
pub struct ReadConnection<R: ProtocolPacket> {
- pub read_stream: OwnedReadHalf,
+ read_stream: OwnedReadHalf,
buffer: BytesMut,
- pub compression_threshold: Option<u32>,
- pub dec_cipher: Option<Aes128CfbDec>,
+ compression_threshold: Option<u32>,
+ dec_cipher: Option<Aes128CfbDec>,
_reading: PhantomData<R>,
}
pub struct WriteConnection<W: ProtocolPacket> {
- pub write_stream: OwnedWriteHalf,
- pub compression_threshold: Option<u32>,
- pub enc_cipher: Option<Aes128CfbEnc>,
+ write_stream: OwnedWriteHalf,
+ compression_threshold: Option<u32>,
+ enc_cipher: Option<Aes128CfbEnc>,
_writing: PhantomData<W>,
}
@@ -64,6 +68,10 @@ where
)
.await
}
+
+ pub async fn shutdown(&mut self) -> std::io::Result<()> {
+ self.write_stream.shutdown().await
+ }
}
impl<R, W> Connection<R, W>
@@ -145,13 +153,54 @@ impl Connection<ClientboundLoginPacket, ServerboundLoginPacket> {
pub fn set_encryption_key(&mut self, key: [u8; 16]) {
// minecraft has a cipher decoder and encoder, i don't think it matters though?
let (enc_cipher, dec_cipher) = azalea_crypto::create_cipher(&key);
- self.writer.enc_cipher = Some(enc_cipher);
self.reader.dec_cipher = Some(dec_cipher);
+ self.writer.enc_cipher = Some(enc_cipher);
}
pub fn game(self) -> Connection<ClientboundGamePacket, ServerboundGamePacket> {
Connection::from(self)
}
+
+ /// Authenticate with Minecraft's servers, which is required to join
+ /// online-mode servers. This must happen when you get a
+ /// `ClientboundLoginPacket::Hello` packet.
+ ///
+ /// ```no_run
+ /// let token = azalea_auth::auth(azalea_auth::AuthOpts {
+ /// ..Default::default()
+ /// })
+ /// .await;
+ /// let player_data = azalea_auth::get_profile(token).await;
+ ///
+ /// let mut connection = azalea::Connection::new(&server_address).await?;
+ ///
+ /// // transition to the login state, in a real program we would have done a handshake first
+ /// connection.login();
+ ///
+ /// match connection.read().await? {
+ /// ClientboundLoginPacket::Hello(p) => {
+ /// // tell Mojang we're joining the server
+ /// connection.authenticate(&token, player_data.uuid, p).await?;
+ /// }
+ /// _ => {}
+ /// }
+ /// ```
+ pub async fn authenticate(
+ &self,
+ access_token: &str,
+ uuid: &Uuid,
+ private_key: [u8; 16],
+ packet: ClientboundHelloPacket,
+ ) -> Result<(), SessionServerError> {
+ azalea_auth::sessionserver::join(
+ access_token,
+ &packet.public_key,
+ &private_key,
+ uuid,
+ &packet.server_id,
+ )
+ .await
+ }
}
// rust doesn't let us implement From because allegedly it conflicts with
diff --git a/bot/src/main.rs b/bot/src/main.rs
index 0a291fd8..92786ce1 100644
--- a/bot/src/main.rs
+++ b/bot/src/main.rs
@@ -7,10 +7,10 @@ use std::sync::Arc;
struct State {}
#[tokio::main]
-async fn main() {
+async fn main() -> anyhow::Result<()> {
env_logger::init();
- let account = Account::offline("bot");
+ let account = Account::microsoft("example2@example.com").await?;
azalea::start(azalea::Options {
account,
@@ -21,6 +21,8 @@ async fn main() {
})
.await
.unwrap();
+
+ Ok(())
}
async fn handle(bot: Client, event: Arc<Event>, _state: Arc<Mutex<State>>) -> anyhow::Result<()> {