diff --git a/out/production/anisette-v3-server/app.d b/out/production/anisette-v3-server/app.d new file mode 100644 index 0000000..a7fb217 --- /dev/null +++ b/out/production/anisette-v3-server/app.d @@ -0,0 +1,315 @@ +import std.algorithm.searching; +import std.array; +import std.base64; +import std.digest; +import file = std.file; +import std.format; +import std.getopt; +import std.json; +import std.math; +import std.net.curl; +import std.parallelism; +import std.path; +import std.uni; +import std.zip; + +import hunt.http; + +import slf4d; +import slf4d.default_provider; + +import provision; + +__gshared ADI adi; +__gshared Device device; +__gshared string libraryPath; + +void main(string[] args) +{ + debug { + configureLoggingProvider(new shared DefaultProvider(true, Levels.DEBUG)); + } else { + configureLoggingProvider(new shared DefaultProvider(true, Levels.INFO)); + } + + enum brandingCode = format!"anisette-v3-server v%s"(provisionVersion); + enum clientInfo = " "; + + Logger log = getLogger(); + log.info(brandingCode); + string hostname = "0.0.0.0"; + ushort port = 6969; + + string configurationPath = expandTilde("~/.config/anisette-v3"); + auto helpInformation = getopt( + args, + "n|host", format!"The hostname to bind to (default: %s)"(hostname), &hostname, + "p|port", format!"The port to bind to (default: %s)"(port), &port, + "a|adi-path", format!"Where the provisioning information should be stored on the computer for anisette-v1 backwards compat (default: %s)"(configurationPath), &configurationPath, + ); + + if (helpInformation.helpWanted) { + defaultGetoptPrinter("anisette-server with v3 support", helpInformation.options); + return; + } + + if (!file.exists(configurationPath)) { + file.mkdirRecurse(configurationPath); + } + + libraryPath = configurationPath.buildPath("lib"); + + string provisioningPathV3 = file.getcwd().buildPath("provisioning"); + + if (!file.exists(provisioningPathV3)) { + file.mkdir(provisioningPathV3); + } + + auto coreADIPath = libraryPath.buildPath("libCoreADI.so"); + auto SSCPath = libraryPath.buildPath("libstoreservicescore.so"); + + if (!(file.exists(coreADIPath) && file.exists(SSCPath))) { + auto http = HTTP(); + log.info("Downloading libraries from Apple servers..."); + auto apkData = get!(HTTP, ubyte)("https://apps.mzstatic.com/content/android-apple-music-apk/applemusic.apk", http); + log.info("Done !"); + auto apk = new ZipArchive(apkData); + auto dir = apk.directory(); + + if (!file.exists(libraryPath)) { + file.mkdirRecurse(libraryPath); + } + + version (X86_64) { + enum string architectureIdentifier = "x86_64"; + } else version (X86) { + enum string architectureIdentifier = "x86"; + } else version (AArch64) { + enum string architectureIdentifier = "arm64-v8a"; + } else version (ARM) { + enum string architectureIdentifier = "armeabi-v7a"; + } else { + static assert(false, "Architecture not supported :("); + } + + file.write(coreADIPath, apk.expand(dir["lib/" ~ architectureIdentifier ~ "/libCoreADI.so"])); + file.write(SSCPath, apk.expand(dir["lib/" ~ architectureIdentifier ~ "/libstoreservicescore.so"])); + } + + // Initializing ADI and machine if it has not already been made. + device = new Device(configurationPath.buildPath("device.json")); + adi = new ADI(libraryPath); + adi.provisioningPath = configurationPath; + + if (!device.initialized) { + log.info("Creating machine... "); + + import std.digest; + import std.random; + import std.range; + import std.uni; + import std.uuid; + device.serverFriendlyDescription = clientInfo; + device.uniqueDeviceIdentifier = randomUUID().toString().toUpper(); + device.adiIdentifier = (cast(ubyte[]) rndGen.take(2).array()).toHexString().toLower(); + device.localUserUUID = (cast(ubyte[]) rndGen.take(8).array()).toHexString().toUpper(); + + log.info("Machine creation done!"); + } + + enum dsId = -2; + + adi.identifier = device.adiIdentifier; + if (!adi.isMachineProvisioned(dsId)) { + log.info("Machine requires provisioning... "); + + ProvisioningSession provisioningSession = new ProvisioningSession(adi, device); + provisioningSession.provision(dsId); + log.info("Provisioning done!"); + } + + auto server = HttpServer.builder() + .setListener(port, hostname) + .addRoute("/", (RoutingContext context) { + import std.datetime.systime; + import std.datetime.timezone; + import core.time; + log.info("[<<] anisette-v1 request"); + auto time = Clock.currTime(); + + auto otp = adi.requestOTP(dsId); + + import std.conv; + import std.json; + + JSONValue response = [ + "X-Apple-I-Client-Time": time.toISOExtString.split('.')[0] ~ "Z", + "X-Apple-I-MD": Base64.encode(otp.oneTimePassword), + "X-Apple-I-MD-M": Base64.encode(otp.machineIdentifier), + "X-Apple-I-MD-RINFO": to!string(17106176), + "X-Apple-I-MD-LU": device.localUserUUID, + "X-Apple-I-SRL-NO": "0", + "X-MMe-Client-Info": device.serverFriendlyDescription, + "X-Apple-I-TimeZone": time.timezone.dstName, + "X-Apple-Locale": "en_US", + "X-Mme-Device-Id": device.uniqueDeviceIdentifier, + ]; + context.responseHeader(HttpHeader.CONTENT_TYPE, "application/json"); + context.responseHeader("Implementation-Version", brandingCode); + context.write(response.toString(JSONOptions.doNotEscapeSlashes)); + context.end(); + log.infoF!"[>>] 200 OK %s"(response); + }) + .addRoute("/v3/client_info", (RoutingContext context) { + log.info("[<<] anisette-v3 /v3/client_info"); + JSONValue response = [ + "client_info": clientInfo, + "user_agent": "akd/1.0 CFNetwork/1404.0.5 Darwin/22.3.0" + ]; + context.write(response.toString()); + context.end(); + }) + .addRoute("/v3/get_headers", (RoutingContext context) { + try { + log.info("[<<] anisette-v3 /v3/get_headers"); + auto json = parseJSON(context.getStringBody()); + string identifier = json["identifier"].str(); + ubyte[] adi_pb = Base64.decode(json["adi_pb"].str()); + auto provisioningPath = file.getcwd() + .buildPath("provisioning") + .buildPath(adi.identifier); + file.mkdir(provisioningPath); + file.write(provisioningPath.buildPath("adi.pb"), adi_pb); + ADI adi = new ADI(libraryPath); + adi.provisioningPath = provisioningPath; + + auto otp = adi.requestOTP(dsId); + file.rmdirRecurse(provisioningPath); + + JSONValue response = [ // Provision does no longer have a concept of 'request headers' + "result": "Headers", + "X-Apple-I-MD": Base64.encode(otp.oneTimePassword), + "X-Apple-I-MD-M": Base64.encode(otp.machineIdentifier), + "X-Apple-I-MD-RINFO": "17106176", + ]; + context.write(response.toString()); + } catch (Throwable t) { + JSONValue error = [ + "result": "GetHeadersError", + "message": typeid(t).name ~ ": " ~ t.msg + ]; + context.write(error.toString()); + } finally { + context.end(); + } + }) + .websocket("/v3/provisioning_session", new SocketProvisioningSessionHandler()).build(); + + server.start(); +} + +enum SocketProvisioningSessionState { + waitingForIdentifier, + waitingForStartProvisioningData, + waitingForEndProvisioning, +} + +struct SocketProvisioningSession { + SocketProvisioningSessionState state; + ADI adiHandle; + uint session; +} + +class SocketProvisioningSessionHandler: AbstractWebSocketMessageHandler { + SocketProvisioningSession[WebSocketConnection] sessions; + + override void onOpen(WebSocketConnection connection) { + log.info("[<<] anisette-v3 /v3/provisioning_session open"); + sessions[connection] = SocketProvisioningSession(SocketProvisioningSessionState.waitingForIdentifier, null, 0); + JSONValue giveIdentifier = [ + "result": "GiveIdentifier" + ]; + connection.sendText(giveIdentifier.toString()); + } + + override void onClosed(WebSocketConnection connection) { + scope(exit) log.info("[<<] anisette-v3 /v3/provisioning_session close"); + auto session = connection in sessions; + if (session) { + if (session.adiHandle) { + ADI adi = session.adiHandle; + if (file.exists( + file.getcwd() + .buildPath("provisioning") + .buildPath(adi.identifier) + )) { + file.rmdirRecurse( + file.getcwd() + .buildPath("provisioning") + .buildPath(adi.identifier) + ); + } + } + sessions.remove(connection); + } + } + + override void onText(WebSocketConnection connection, string text) { + try { + auto session = connection in sessions; + final switch (session.state) with (SocketProvisioningSessionState) { + case waitingForIdentifier: + auto res = parseJSON(text); + string requestedIdentifier = res["identifier"].str(); + auto adi = new ADI(libraryPath); + adi.identifier = requestedIdentifier; + adi.provisioningPath = file.getcwd().buildPath("provisioning").buildPath(requestedIdentifier); + session.adiHandle = adi; + session.state = waitingForStartProvisioningData; + break; + case waitingForStartProvisioningData: + auto res = parseJSON(text); + string spim = res["spim"].str(); + auto adi = session.adiHandle; + auto cpimAndCo = adi.startProvisioning(-2, Base64.decode(spim)); + session.session = cpimAndCo.session; + session.state = waitingForEndProvisioning; + JSONValue response = [ + "result": "GiveEndProvisioningData", + "message": Base64.encode(cpimAndCo.clientProvisioningIntermediateMetadata) + ]; + connection.sendText(response.toString()); + break; + case waitingForEndProvisioning: + auto res = parseJSON(text); + string ptm = res["ptm"].str(); + string tk = res["tk"].str(); + auto adi = session.adiHandle; + adi.endProvisioning(-2, Base64.decode(ptm), Base64.decode(tk)); + JSONValue response = [ + "result": "ProvisioningSuccess", + "message": Base64.encode( + cast(ubyte[]) file.read( + file.getcwd() + .buildPath("provisioning") + .buildPath(adi.identifier) + .buildPath("adi.pb") + ) + ) + ]; + connection.sendText(response.toString()).then((_) { + connection.close(); + }); + break; + } + } catch (Throwable t) { + JSONValue error = [ + "result": "NonStandard-" ~ typeid(t).name, + "message": t.msg + ]; + connection.sendText(error.toString()).then((_) { + connection.close(); + }); + } + } +} diff --git a/source/app.d b/source/app.d index a5a0f0a..a971aa8 100644 --- a/source/app.d +++ b/source/app.d @@ -161,6 +161,7 @@ void main(string[] args) log.infoF!"[>>] 200 OK %s"(response); }) .addRoute("/v3/client_info", (RoutingContext context) { + log.info("[<<] anisette-v3 /v3/client_info"); JSONValue response = [ "client_info": clientInfo, "user_agent": "akd/1.0 CFNetwork/1404.0.5 Darwin/22.3.0" @@ -168,35 +169,34 @@ void main(string[] args) context.write(response.toString()); context.end(); }) - .addRoute("/v3/client_info", (RoutingContext context) { + .addRoute("/v3/get_headers", (RoutingContext context) { try { - if (context.getMethod() == "POST") { - auto json = parseJSON(context.getStringBody()); - string identifier = json["identifier"].str(); - ubyte[] adi_pb = Base64.decode(json["adi_pb"].str()); - auto provisioningPath = file.getcwd() - .buildPath("provisioning") - .buildPath(adi.identifier); - file.mkdir(provisioningPath); - file.write(provisioningPath.buildPath("adi.pb"), adi_pb); - ADI adi = new ADI(libraryPath); - adi.provisioningPath = provisioningPath; + log.info("[<<] anisette-v3 /v3/get_headers"); + auto json = parseJSON(context.getStringBody()); + string identifier = json["identifier"].str(); + ubyte[] adi_pb = Base64.decode(json["adi_pb"].str()); + auto provisioningPath = file.getcwd() + .buildPath("provisioning") + .buildPath(adi.identifier); + file.mkdir(provisioningPath); + file.write(provisioningPath.buildPath("adi.pb"), adi_pb); + ADI adi = new ADI(libraryPath); + adi.provisioningPath = provisioningPath; - auto otp = adi.requestOTP(dsId); - file.rmdirRecurse(provisioningPath); + auto otp = adi.requestOTP(dsId); + file.rmdirRecurse(provisioningPath); - JSONValue response = [ // Provision does no longer have a concept of 'request headers' - "result": "Headers", - "X-Apple-I-MD": Base64.encode(otp.oneTimePassword), - "X-Apple-I-MD-M": Base64.encode(otp.machineIdentifier), - "X-Apple-I-MD-RINFO": "17106176", - ]; - context.write(response.toString()); - } + JSONValue response = [ // Provision does no longer have a concept of 'request headers' + "result": "Headers", + "X-Apple-I-MD": Base64.encode(otp.oneTimePassword), + "X-Apple-I-MD-M": Base64.encode(otp.machineIdentifier), + "X-Apple-I-MD-RINFO": "17106176", + ]; + context.write(response.toString()); } catch (Throwable t) { JSONValue error = [ - "result": "NonStandard-" ~ typeid(t).name, - "message": t.msg + "result": "GetHeadersError", + "message": typeid(t).name ~ ": " ~ t.msg ]; context.write(error.toString()); } finally { @@ -224,6 +224,7 @@ class SocketProvisioningSessionHandler: AbstractWebSocketMessageHandler { SocketProvisioningSession[WebSocketConnection] sessions; override void onOpen(WebSocketConnection connection) { + getLogger().info("[<<] anisette-v3 /v3/provisioning_session open"); sessions[connection] = SocketProvisioningSession(SocketProvisioningSessionState.waitingForIdentifier, null, 0); JSONValue giveIdentifier = [ "result": "GiveIdentifier" @@ -232,6 +233,7 @@ class SocketProvisioningSessionHandler: AbstractWebSocketMessageHandler { } override void onClosed(WebSocketConnection connection) { + scope(exit) getLogger().info("[<<] anisette-v3 /v3/provisioning_session close"); auto session = connection in sessions; if (session) { if (session.adiHandle) {