diff --git a/.gitignore b/.gitignore index ea8c4bf..bd819bb 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +*~ \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index ced500a..7b6df71 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "aes" version = "0.6.0" @@ -45,13 +60,22 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + [[package]] name = "atty" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "hermit-abi", + "hermit-abi 0.1.19", "libc", "winapi", ] @@ -62,6 +86,21 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "base64" version = "0.13.0" @@ -80,6 +119,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + [[package]] name = "block-buffer" version = "0.9.0" @@ -110,6 +155,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -145,7 +199,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8c93436c21e4698bacadf42917db28b23017027a4deccb35dbe47a7e7840123" dependencies = [ "atty", - "bitflags", + "bitflags 1.3.2", "clap_derive", "indexmap", "lazy_static", @@ -246,6 +300,29 @@ dependencies = [ "winapi", ] +[[package]] +name = "env_logger" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "fnv" version = "1.0.7" @@ -372,6 +449,12 @@ dependencies = [ "wasi 0.10.2+wasi-snapshot-preview1", ] +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + [[package]] name = "glob" version = "0.3.0" @@ -391,7 +474,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cff78e5788be1e0ab65b04d306b2ed5092c815ec97ec70f4ebd5aee158aa55d" dependencies = [ "base64", - "bitflags", + "bitflags 1.3.2", "bytes", "headers-core", "http", @@ -424,6 +507,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + [[package]] name = "hex" version = "0.4.3" @@ -483,6 +572,12 @@ 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.18" @@ -542,6 +637,17 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi 0.3.3", + "rustix", + "windows-sys 0.48.0", +] + [[package]] name = "itoa" version = "1.0.1" @@ -556,15 +662,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.121" +version = "0.2.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efaa7b300f3b5fe8eb6bf21ce3895e1751d9665086af2d64b42f19701015ff4f" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" [[package]] name = "librespot-core" -version = "0.3.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "255e8d8d719c020895079d140baf0b0edec8447d39a7e4760708f33b7cafaafb" +checksum = "046349f25888e644bf02d9c5de0164b2a493d29aa4ce18e1ad0b756da9b55d6d" dependencies = [ "aes", "base64", @@ -603,23 +709,26 @@ dependencies = [ [[package]] name = "librespot-protocol" -version = "0.3.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41b3699b05cb4c50caa5a5b7f5b3aadb928dfcc91cf1aa632c0dabce3ccc3ee4" +checksum = "5d6d3ac6196ac0ea67bbe039f56d6730a5d8b31502ef9bce0f504ed729dcb39f" dependencies = [ "glob", "protobuf", "protobuf-codegen-pure", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" + [[package]] name = "log" -version = "0.4.16" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8" -dependencies = [ - "cfg-if", -] +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "matches" @@ -639,6 +748,15 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + [[package]] name = "minreq" version = "2.6.0" @@ -651,34 +769,13 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.2" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52da4364ffb0e4fe33a9841a98a3f3014fb964045ce4f7a45a398243c8d6b0c9" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", - "log", - "miow", - "ntapi", "wasi 0.11.0+wasi-snapshot-preview1", - "winapi", -] - -[[package]] -name = "miow" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" -dependencies = [ - "winapi", -] - -[[package]] -name = "ntapi" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" -dependencies = [ - "winapi", + "windows-sys 0.48.0", ] [[package]] @@ -712,6 +809,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.10.0" @@ -903,7 +1009,7 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8380fe0152551244f0747b1bf41737e0f8a74f97a14ccefd1148187271634f3c" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -917,6 +1023,23 @@ dependencies = [ "thiserror", ] +[[package]] +name = "regex" +version = "1.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "rpassword" version = "6.0.1" @@ -929,6 +1052,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + [[package]] name = "rustc_version" version = "0.4.0" @@ -938,6 +1067,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" +dependencies = [ + "bitflags 2.4.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + [[package]] name = "ryu" version = "1.0.9" @@ -1033,9 +1175,9 @@ checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" [[package]] name = "socket2" -version = "0.4.4" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" dependencies = [ "libc", "winapi", @@ -1044,24 +1186,37 @@ dependencies = [ [[package]] name = "spotify-connect" version = "0.1.0" +dependencies = [ + "clap", + "dirs", + "env_logger", + "librespot-core", + "librespot-protocol", + "log", + "rpassword", + "spotify-connect-client", +] + +[[package]] +name = "spotify-connect-client" +version = "0.1.0" dependencies = [ "aes", "aes-ctr", "base64", - "clap", - "dirs", "hex", "hmac 0.12.1", "librespot-core", "librespot-protocol", + "log", "minreq", "pbkdf2 0.11.0", "rand", - "rpassword", "serde", "serde_json", "sha-1 0.10.0", "tokio", + "urlencoding", ] [[package]] @@ -1149,17 +1304,18 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.17.0" +version = "1.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af73ac49756f3f7c01172e34a23e5d0216f6c32333757c2c61feb2bbff5a5ee" +checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" dependencies = [ + "autocfg", + "backtrace", "bytes", "libc", - "memchr", "mio", "pin-project-lite", "socket2", - "winapi", + "windows-sys 0.48.0", ] [[package]] @@ -1175,16 +1331,16 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.6.9" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" dependencies = [ "bytes", "futures-core", "futures-sink", - "log", "pin-project-lite", "tokio", + "tracing", ] [[package]] @@ -1266,9 +1422,9 @@ checksum = "68b90931029ab9b034b300b797048cf23723400aa757e8a2bfb9d748102f9821" [[package]] name = "uuid" -version = "0.8.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" dependencies = [ "getrandom", ] @@ -1279,7 +1435,7 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7141e445af09c8919f1d5f8a20dae0b20c3b57a45dee0d5823c6ed5d237f15a" dependencies = [ - "bitflags", + "bitflags 1.3.2", "chrono", "rustc_version", ] @@ -1342,3 +1498,135 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" diff --git a/Cargo.toml b/Cargo.toml index 9b537f9..3e8815e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,21 +3,17 @@ name = "spotify-connect" version = "0.1.0" edition = "2021" +[workspace] + +[dependencies.spotify-connect-client] +path = "lib" +version = "0.1.0" + [dependencies] -aes = "0.6.0" -aes-ctr = "0.6.0" -base64 = "0.13.0" clap = { version = "3.1.6", features = ["derive"] } -dirs = "4.0" -hex = "0.4.3" -hmac = "0.12.1" -librespot-core = "0.3.1" -librespot-protocol = "0.3.1" -minreq = { version = "2.6", default-features = false, features = ['urlencoding'] } -pbkdf2 = "0.11" -rand = "0.8.5" -serde = "1.0.136" -serde_json = "1.0.79" -sha-1 = "0.10.0" -tokio = "1.17.0" -rpassword = "6.0.1" +dirs = "4" +env_logger = "0.10" +librespot-core = "0.4.2" +librespot-protocol = "0.4.2" +log = "0.4" +rpassword = "6" diff --git a/lib/Cargo.toml b/lib/Cargo.toml new file mode 100644 index 0000000..0656fb4 --- /dev/null +++ b/lib/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "spotify-connect-client" +version = "0.1.0" +edition = "2021" + +[dependencies] +aes = "0.6.0" +aes-ctr = "0.6.0" +base64 = "0.13.0" +hex = "0.4.3" +hmac = "0.12.1" +librespot-core = "0.4.2" +librespot-protocol = "0.4.2" +log = "0.4" +minreq = { version = "2.6", default-features = false, features = ['urlencoding'] } +pbkdf2 = "0.11" +rand = "0.8.5" +serde = "1.0.136" +serde_json = "1.0.79" +sha-1 = "0.10.0" +tokio = "1.17.0" +urlencoding = "2.1" diff --git a/src/auth.rs b/lib/src/auth.rs similarity index 52% rename from src/auth.rs rename to lib/src/auth.rs index 44e5013..1b34664 100644 --- a/src/auth.rs +++ b/lib/src/auth.rs @@ -2,26 +2,6 @@ use librespot_core::{ authentication::Credentials, cache::Cache, config::SessionConfig, keymaster, session::Session, }; use librespot_protocol::authentication::AuthenticationType; -use std::io::Write; - -/// Prompt the user for its Spotify username and password -pub fn ask_user_credentials() -> Result { - // Username - print!("Spotify username: "); - std::io::stdout().flush()?; - let mut username = String::new(); - std::io::stdin().read_line(&mut username)?; - username = username.trim_end().to_string(); - - // Password - let password = rpassword::prompt_password(&format!("Password for {username}: "))?; - - Ok(Credentials { - username, - auth_type: AuthenticationType::AUTHENTICATION_USER_PASS, - auth_data: password.as_bytes().into(), - }) -} /// Create reusable credentials /// @@ -30,25 +10,27 @@ pub fn ask_user_credentials() -> Result { /// the user authenticate with the username/password couple. pub fn create_reusable_credentials( cache: Cache, + credentials: Credentials, ) -> Result> { - // Authenticate with username/password - let credentials = ask_user_credentials()?; - - let connection = tokio::runtime::Builder::new_current_thread() + let (_session, _credentials) = tokio::runtime::Builder::new_current_thread() .enable_all() - .build() - .unwrap() + .build()? .block_on(async { - Session::connect(SessionConfig::default(), credentials, Some(cache.clone())).await - }); - - connection?; + let store_credentials = true; + Session::connect( + SessionConfig::default(), + credentials, + Some(cache.clone()), + store_credentials, + ) + .await + })?; // The reusable credentials are automatically saved in the cache. Reading // them back. cache .credentials() - .ok_or_else(|| "There is no reusable credentials saved in cache".into()) + .ok_or_else(|| "There are no reusable credentials saved in cache".into()) } /// Transform existing credentials into token credentials @@ -75,17 +57,27 @@ pub fn get_token( ) -> Result> { let token = tokio::runtime::Builder::new_current_thread() .enable_all() - .build() - .unwrap() + .build()? .block_on(async { - let session = Session::connect(SessionConfig::default(), credentials, None) - .await - .expect("Impossible to create a Spotify session"); + let store_credentials = false; + let connect_result: Result<_, Box> = Session::connect( + SessionConfig::default(), + credentials, + None, + store_credentials, + ) + .await + .map_err(|e| format!("Unable to create a Spotify session: {e}").into()); - keymaster::get_token(&session, client_id, scope) - .await - .expect("Impossible to get a token from the Spotify session") - }); + match connect_result { + Ok((session, _credentials)) => keymaster::get_token(&session, client_id, scope) + .await + .map_err(|e| { + format!("Unable to get a token from the Spotify session: {e:?}").into() + }), + Err(e) => Err(e), + } + })?; Ok(token.access_token) } diff --git a/lib/src/lib.rs b/lib/src/lib.rs new file mode 100644 index 0000000..07e96e7 --- /dev/null +++ b/lib/src/lib.rs @@ -0,0 +1,109 @@ +use std::{error, fmt}; + +use librespot_core::{authentication::Credentials, diffie_hellman::DhLocalKeys}; +use log::info; + +pub mod auth; +pub mod net; +pub mod proto; + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub enum AuthType { + #[default] + Reusable, + Password, + DefaultToken, + AccessToken, +} + +#[derive(Debug)] +pub enum Error { + CouldNotGetDeviceInfo(String, Box), + CouldNotAddUser(Box), + EncryptionFailed(Box), + MissingClientId, + AccessTokenRetrievalFailure(Box), +} + +impl error::Error for Error {} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::CouldNotGetDeviceInfo(base_url, e) => f.write_fmt(format_args!( + "Could not get device information from {base_url}: {e}" + )), + Error::CouldNotAddUser(e) => f.write_fmt(format_args!( + "Authentication on the remote remote device failed: {e}" + )), + Error::EncryptionFailed(e) => { + f.write_fmt(format_args!("Encryption of credentials failed: {e}")) + } + Error::MissingClientId => f.write_str( + "To authenticate with an access token, the remote device should provide a clientID", + ), + Error::AccessTokenRetrievalFailure(e) => f.write_fmt(format_args!( + "The access token could not be retrieved from Spotify: {e}" + )), + } + } +} + +pub fn authenticate( + host_or_ip: &str, + port: u16, + path: &str, + credentials: &Credentials, + auth_type: &AuthType, +) -> Result { + let base_url = format!("http://{}:{}{path}", host_or_ip, port); + + // Get device information + let device_info = net::get_device_info(&base_url) + .map_err(|e| Error::CouldNotGetDeviceInfo(base_url.clone(), e))?; + info!("Found `{}`. Trying to connect...", device_info.remote_name); + + let (blob, my_public_key) = match auth_type { + AuthType::Reusable | AuthType::Password | AuthType::DefaultToken => { + // Generate the blob + let blob = proto::build_blob(credentials, &device_info.device_id); + + // Encrypt the blob + let local_keys = DhLocalKeys::random(&mut rand::thread_rng()); + let encrypted_blob = proto::encrypt_blob(&blob, &local_keys, &device_info.public_key) + .map_err(Error::EncryptionFailed)?; + + (encrypted_blob, base64::encode(local_keys.public_key())) + } + AuthType::AccessToken => { + let client_id = device_info + .client_id + .as_deref() + .ok_or(Error::MissingClientId)?; + let scope = device_info.scope.as_deref().unwrap_or("streaming"); + + let token = auth::get_token(credentials.clone(), client_id, scope) + .map_err(Error::AccessTokenRetrievalFailure)?; + + (token, "".to_string()) + } + }; + + let token_type = match auth_type { + AuthType::Reusable | AuthType::Password => None, + AuthType::DefaultToken => Some("default"), + AuthType::AccessToken => Some("accesstoken"), + }; + + // Send the authentication request + net::add_user( + &base_url, + &credentials.username, + &blob, + &my_public_key, + token_type, + ) + .map_err(Error::CouldNotAddUser)?; + + Ok(device_info) +} diff --git a/src/net.rs b/lib/src/net.rs similarity index 52% rename from src/net.rs rename to lib/src/net.rs index 65ab554..4b46abb 100644 --- a/src/net.rs +++ b/lib/src/net.rs @@ -1,21 +1,21 @@ use rand::Rng; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use serde_json::Value; use sha1::{Digest, Sha1}; -#[derive(Debug, Deserialize)] +// see +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct DeviceInfo { #[serde(rename = "deviceID")] pub device_id: String, - #[serde(rename = "remoteName")] pub remote_name: String, - #[serde(rename = "publicKey")] pub public_key: String, - #[serde(rename = "tokenType")] - pub token_type: Option, + pub active_user: Option, // undocumented but useful and returned by both librespot and librespot-java + pub token_type: Option, // required at least as of 2.9.0 #[serde(rename = "clientID")] - pub client_id: Option, - pub scope: Option, + pub client_id: Option, // required at least as of 2.9.0 + pub scope: Option, // required at least as of 2.9.0 } /// Get the necessary information from the remote device @@ -40,20 +40,34 @@ pub fn add_user( let device_id = hex::encode(Sha1::digest("spotify-connect".as_bytes())); let login_id = hex::encode(rand::thread_rng().gen::<[u8; 16]>()); - let mut request = minreq::post(base_url) - .with_header("Content-Type", "application/x-www-form-urlencoded") - .with_param("action", "addUser") - .with_param("userName", username) - .with_param("blob", blob) - .with_param("clientKey", my_public_key) - .with_param("deviceId", device_id) - .with_param("deviceName", "spotify-connect") - .with_param("loginId", login_id); + let params = [ + ("action", "addUser"), + ("userName", username), + ("blob", blob), + ("clientKey", my_public_key), + ("deviceId", &device_id), + ("deviceName", "spotify-connect"), + ("loginId", &login_id), + ]; + let mut body = String::with_capacity(1024); + for (i, (k, v)) in params.iter().enumerate() { + if i != 0 { + body.push('&'); + } + body.push_str(k); + body.push('='); + body.push_str(&urlencoding::encode(v)); + } if let Some(token_type) = token_type { - request = request.with_param("tokenType", token_type); + body.push_str("&tokenType="); + body.push_str(&urlencoding::encode(token_type)); } + let request = minreq::post(base_url) + .with_header("Content-Type", "application/x-www-form-urlencoded") + .with_body(body); + let response = request.send()?; let v: Value = serde_json::from_str(response.as_str()?)?; diff --git a/src/proto.rs b/lib/src/proto.rs similarity index 99% rename from src/proto.rs rename to lib/src/proto.rs index a8fa830..0a7c141 100644 --- a/src/proto.rs +++ b/lib/src/proto.rs @@ -101,7 +101,7 @@ pub fn encrypt_blob( let remote_device_key = base64::decode(remote_device_key)?; let shared_key = local_keys.shared_secret(&remote_device_key); - let base_key = Sha1::digest(&shared_key); + let base_key = Sha1::digest(shared_key); let base_key = &base_key[..16]; let checksum_key = { diff --git a/src/main.rs b/src/main.rs index 0735e34..ed251e8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,16 +1,50 @@ +use std::io::Write; + use clap::{ArgEnum, Parser}; -use librespot_core::{cache::Cache, diffie_hellman::DhLocalKeys}; +use librespot_core::{authentication::Credentials, cache::Cache}; +use librespot_protocol::authentication::AuthenticationType; + +use spotify_connect_client as client; + +// repeated here to be able to use ArgEnum without creating a library dependency on clap +#[derive(Clone, Debug, Default, ArgEnum)] +pub enum AuthType { + #[default] + Reusable, + Password, + DefaultToken, + AccessToken, +} + +impl From for client::AuthType { + fn from(value: AuthType) -> Self { + match value { + AuthType::Reusable => client::AuthType::Reusable, + AuthType::Password => client::AuthType::Password, + AuthType::DefaultToken => client::AuthType::DefaultToken, + AuthType::AccessToken => client::AuthType::AccessToken, + } + } +} -mod auth; -mod net; -mod proto; +// included here so that the two types stay in sync +impl From for AuthType { + fn from(value: client::AuthType) -> Self { + match value { + client::AuthType::Reusable => AuthType::Reusable, + client::AuthType::Password => AuthType::Password, + client::AuthType::DefaultToken => AuthType::DefaultToken, + client::AuthType::AccessToken => AuthType::AccessToken, + } + } +} /// Use the Spotify Connect feature to authenticate yourself on remote devices #[derive(Parser, Debug)] #[clap(version, about)] struct Args { - /// IP address of the remote device - ip: std::net::IpAddr, + /// Hostname or IP address of the remote device + host_or_ip: String, /// Port on which the remote device is listening port: u16, @@ -24,100 +58,85 @@ struct Args { auth_type: AuthType, } -#[derive(Clone, Debug, PartialEq, Eq, ArgEnum)] -enum AuthType { - Reusable, - Password, - DefaultToken, - AccessToken, -} - -impl Default for AuthType { - fn default() -> AuthType { - AuthType::Reusable - } +/// Prompt the user for its Spotify username and password +fn ask_user_credentials() -> Result { + // Username + print!("Spotify username: "); + std::io::stdout().flush()?; + let mut username = String::new(); + std::io::stdin().read_line(&mut username)?; + username = username.trim_end().to_string(); + + // Password + let password = rpassword::prompt_password(format!("Password for {username}: "))?; + + Ok(Credentials { + username, + auth_type: AuthenticationType::AUTHENTICATION_USER_PASS, + auth_data: password.as_bytes().into(), + }) } fn main() { + env_logger::Builder::new() + .filter_level(log::LevelFilter::Info) + .parse_default_env() + .init(); + // Parse arguments let args = Args::parse(); - let base_url = format!("http://{}:{}{}", args.ip, args.port, args.path); - let mut token_type = None; - - // Get device information - let device_info = net::get_device_info(&base_url) - .unwrap_or_else(|_| panic!("Impossible to get device information from {base_url}")); - println!("Found `{}`. Trying to connect...", device_info.remote_name); // Prepare cache let mut cache_path = dirs::cache_dir().expect("Impossible to find the user cache directory."); cache_path.push("spotify-connect"); - let cache = Cache::new(Some(cache_path), None, None) - .expect("Impossible to open cache path: {cache_path}"); + let cache = Cache::new(Some(cache_path.as_path()), None, None, None).unwrap_or_else(|e| { + panic!( + "Impossible to open cache path {}: {e}", + cache_path.display() + ) + }); // Get credentials let credentials = match args.auth_type { AuthType::Reusable | AuthType::AccessToken => { cache.credentials().unwrap_or_else(|| { // Cache is empty, authenticate to create the credentials - auth::create_reusable_credentials(cache) - .expect("Getting reusable credentials from spotify failed") + let credentials = + ask_user_credentials().expect("Getting username and password failed"); + client::auth::create_reusable_credentials(cache, credentials).unwrap_or_else(|e| { + panic!("Getting reusable credentials from spotify failed: {e}") + }) }) } - AuthType::Password => { - auth::ask_user_credentials().expect("Getting username and password failed") - } + AuthType::Password => ask_user_credentials().expect("Getting username and password failed"), AuthType::DefaultToken => { - token_type = Some("default"); - let credentials = cache.credentials().unwrap_or_else(|| { - auth::ask_user_credentials().expect("Getting username and password failed") + ask_user_credentials().expect("Getting username and password failed") }); - auth::change_to_token_credentials(credentials).expect("Token retrieval failed") + client::auth::change_to_token_credentials(credentials) + .unwrap_or_else(|e| panic!("Token retrieval failed: {e}")) } }; - let (blob, my_public_key) = match args.auth_type { - AuthType::Reusable | AuthType::Password | AuthType::DefaultToken => { - // Generate the blob - let blob = proto::build_blob(&credentials, &device_info.device_id); - - // Encrypt the blob - let local_keys = DhLocalKeys::random(&mut rand::thread_rng()); - let encrypted_blob = proto::encrypt_blob(&blob, &local_keys, &device_info.public_key) - .expect("Encryption of credentials failed"); - - (encrypted_blob, base64::encode(local_keys.public_key())) - } - AuthType::AccessToken => { - token_type = Some("accesstoken"); - - let client_id = device_info.client_id.expect( - "To authenticate with an access token, the remote device should provide a clientID", - ); - let scope = device_info.scope.unwrap_or_else(|| "streaming".into()); - - let token = auth::get_token(credentials.clone(), &client_id, &scope) - .expect("The access token could not be retrieved"); + let device_info = client::authenticate( + &args.host_or_ip, + args.port, + &args.path, + &credentials, + &args.auth_type.into(), + ) + .unwrap_or_else(|e| panic!("authentication failed: {e}")); - (token, "".to_string()) - } + let more_info = match device_info.active_user.as_deref() { + Some("") => " (no prior active user)".to_string(), + Some(username) => format!(" (was {username})"), + None => "".to_string(), }; - // Send the authentication request - net::add_user( - &base_url, - &credentials.username, - &blob, - &my_public_key, - token_type, - ) - .expect("Authentication on the remote device failed"); - println!( - "🎉 Connected as `{}` on `{}` 🎉", - credentials.username, device_info.remote_name + "🎉 Connected as `{}` on `{}`{} 🎉", + credentials.username, device_info.remote_name, more_info, ); }