2023-06-17 10:40:20 +00:00
import core.time ;
2023-06-16 18:47:30 +00:00
import core.memory ;
2023-04-30 23:22:51 +00:00
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 ;
2023-06-14 19:53:52 +00:00
import std.uuid ;
2023-04-30 23:22:51 +00:00
import std.zip ;
2023-06-17 10:40:20 +00:00
import vibe.core.core ;
import vibe.http.websockets ;
import vibe.http.server ;
import vibe.http.router ;
2023-08-09 17:42:53 +00:00
import vibe.stream.tls ;
2023-06-17 10:40:20 +00:00
import vibe.web.web ;
2023-04-30 23:22:51 +00:00
import slf4d ;
2023-06-16 18:19:18 +00:00
import slf4d : Logger ;
2023-04-30 23:22:51 +00:00
import slf4d.default_provider ;
import provision ;
2023-06-16 18:47:30 +00:00
import provision.androidlibrary ;
2023-04-30 23:22:51 +00:00
__gshared string libraryPath ;
2023-06-14 19:53:52 +00:00
enum brandingCode = format ! "anisette-v3-server v%s" ( provisionVersion ) ;
enum clientInfo = "<MacBookPro13,2> <macOS;13.1;22C65> <com.apple.AuthKit/1 (com.apple.dt.Xcode/3594.4.19)>" ;
enum dsId = - 2 ;
__gshared ADI v1Adi ;
__gshared Device v1Device ;
2023-08-12 02:11:52 +00:00
__gshared Duration timeout ;
2023-06-16 18:19:18 +00:00
int main ( string [ ] args ) {
2023-04-30 23:22:51 +00:00
debug {
configureLoggingProvider ( new shared DefaultProvider ( true , Levels . DEBUG ) ) ;
} else {
configureLoggingProvider ( new shared DefaultProvider ( true , Levels . INFO ) ) ;
}
Logger log = getLogger ( ) ;
log . info ( brandingCode ) ;
string hostname = "0.0.0.0" ;
ushort port = 6969 ;
string configurationPath = expandTilde ( "~/.config/anisette-v3" ) ;
2023-08-09 17:42:53 +00:00
string certificateChainPath = null ;
string privateKeyPath = null ;
2023-08-12 02:11:52 +00:00
long timeoutMsecs = 3000 ;
2023-04-30 23:22:51 +00:00
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 ,
2023-08-12 02:11:52 +00:00
"timeout" , format ! "Timeout duration for Anisette V3 in milliseconds (default: %d)" ( timeoutMsecs ) , & timeoutMsecs ,
2023-08-09 17:42:53 +00:00
"private-key" , "Path to the PEM-formatted private key file for HTTPS support (requires --cert-chain)" , & certificateChainPath ,
"cert-chain" , "Path to the PEM-formatted certificate chain file for HTTPS support (requires --private-key)" , & privateKeyPath ,
2023-04-30 23:22:51 +00:00
) ;
2023-08-12 02:11:52 +00:00
timeout = dur ! "msecs" ( timeoutMsecs ) ;
2023-08-09 17:42:53 +00:00
if ( ( certificateChainPath & & ! privateKeyPath ) | | ( ! certificateChainPath & & privateKeyPath ) ) {
log . error ( "--certificate-chain and --private-key must both be specified for HTTPS support (they can be both be in the same file though)." ) ;
return 1 ;
}
2023-04-30 23:22:51 +00:00
if ( helpInformation . helpWanted ) {
defaultGetoptPrinter ( "anisette-server with v3 support" , helpInformation . options ) ;
2023-06-16 18:19:18 +00:00
return 0 ;
2023-04-30 23:22:51 +00:00
}
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.
2023-06-14 19:53:52 +00:00
v1Device = new Device ( configurationPath . buildPath ( "device.json" ) ) ;
2023-06-16 18:56:23 +00:00
v1Adi = new ADI ( libraryPath ) ;
2023-06-14 19:53:52 +00:00
v1Adi . provisioningPath = configurationPath ;
2023-04-30 23:22:51 +00:00
2023-06-14 19:53:52 +00:00
if ( ! v1Device . initialized ) {
2023-04-30 23:22:51 +00:00
log . info ( "Creating machine... " ) ;
import std.random ;
import std.range ;
2023-06-14 19:53:52 +00:00
v1Device . serverFriendlyDescription = clientInfo ;
v1Device . uniqueDeviceIdentifier = randomUUID ( ) . toString ( ) . toUpper ( ) ;
v1Device . adiIdentifier = ( cast ( ubyte [ ] ) rndGen . take ( 2 ) . array ( ) ) . toHexString ( ) . toLower ( ) ;
v1Device . localUserUUID = ( cast ( ubyte [ ] ) rndGen . take ( 8 ) . array ( ) ) . toHexString ( ) . toUpper ( ) ;
2023-04-30 23:22:51 +00:00
log . info ( "Machine creation done!" ) ;
}
2023-06-14 19:53:52 +00:00
v1Adi . identifier = v1Device . adiIdentifier ;
if ( ! v1Adi . isMachineProvisioned ( dsId ) ) {
2023-04-30 23:22:51 +00:00
log . info ( "Machine requires provisioning... " ) ;
2023-06-14 19:53:52 +00:00
ProvisioningSession provisioningSession = new ProvisioningSession ( v1Adi , v1Device ) ;
2023-04-30 23:22:51 +00:00
provisioningSession . provision ( dsId ) ;
log . info ( "Provisioning done!" ) ;
}
2023-06-16 18:19:18 +00:00
// Create the router that will map the incoming requests to request handlers
auto router = new URLRouter ( ) ;
// Register SampleService as a web service
router . registerWebInterface ( new AnisetteService ( ) ) ;
// Start up the HTTP server.
auto settings = new HTTPServerSettings ;
settings . port = port ;
settings . bindAddresses = [ hostname ] ;
settings . sessionStore = new MemorySessionStore ;
2023-08-09 17:42:53 +00:00
if ( certificateChainPath ) {
settings . tlsContext = createTLSContext ( TLSContextKind . server ) ;
settings . tlsContext . useCertificateChainFile ( certificateChainPath ) ;
settings . tlsContext . usePrivateKeyFile ( privateKeyPath ) ;
}
2023-06-16 18:19:18 +00:00
auto listener = listenHTTP ( settings , router ) ;
return runApplication ( & args ) ;
2023-06-14 19:53:52 +00:00
}
2023-06-16 18:19:18 +00:00
class AnisetteService {
@method ( HTTPMethod . GET )
@path ( "/" )
void handleV1Request ( HTTPServerRequest req , HTTPServerResponse res ) {
2023-04-30 23:22:51 +00:00
import std.datetime.systime ;
import std.datetime.timezone ;
import core.time ;
2023-06-14 19:53:52 +00:00
auto log = getLogger ( ) ;
2023-04-30 23:22:51 +00:00
log . info ( "[<<] anisette-v1 request" ) ;
auto time = Clock . currTime ( ) ;
2023-06-14 19:53:52 +00:00
auto otp = v1Adi . requestOTP ( dsId ) ;
2023-04-30 23:22:51 +00:00
import std.conv ;
import std.json ;
2023-06-14 19:53:52 +00:00
JSONValue responseJson = [
2023-04-30 23:22:51 +00:00
"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 ) ,
2023-06-14 19:53:52 +00:00
"X-Apple-I-MD-LU" : v1Device . localUserUUID ,
2023-04-30 23:22:51 +00:00
"X-Apple-I-SRL-NO" : "0" ,
2023-06-14 19:53:52 +00:00
"X-MMe-Client-Info" : v1Device . serverFriendlyDescription ,
2023-04-30 23:22:51 +00:00
"X-Apple-I-TimeZone" : time . timezone . dstName ,
"X-Apple-Locale" : "en_US" ,
2023-06-14 19:53:52 +00:00
"X-Mme-Device-Id" : v1Device . uniqueDeviceIdentifier ,
2023-04-30 23:22:51 +00:00
] ;
2023-06-14 19:53:52 +00:00
2023-06-17 10:40:20 +00:00
res . headers [ "Implementation-Version" ] = brandingCode ;
2023-06-16 18:19:18 +00:00
res . writeBody ( responseJson . toString ( JSONOptions . doNotEscapeSlashes ) , "application/json" ) ;
2023-06-14 19:53:52 +00:00
log . infoF ! "[>>] 200 OK %s" ( responseJson ) ;
}
2023-06-16 18:19:18 +00:00
@method ( HTTPMethod . GET )
@path ( "/v3/client_info" )
void getClientInfo ( HTTPServerRequest req , HTTPServerResponse res ) {
2023-06-14 19:53:52 +00:00
auto log = getLogger ( ) ;
2023-05-01 09:37:09 +00:00
log . info ( "[<<] anisette-v3 /v3/client_info" ) ;
2023-06-14 19:53:52 +00:00
JSONValue responseJson = [
2023-04-30 23:22:51 +00:00
"client_info" : clientInfo ,
2023-06-14 19:53:52 +00:00
"user_agent" : "akd/1.0 CFNetwork/808.1.4"
2023-04-30 23:22:51 +00:00
] ;
2023-06-14 19:53:52 +00:00
2023-06-17 10:40:20 +00:00
res . headers [ "Implementation-Version" ] = brandingCode ;
res . writeBody ( responseJson . toString ( JSONOptions . doNotEscapeSlashes ) , "application/json" ) ;
2023-06-14 19:53:52 +00:00
}
2023-06-16 18:19:18 +00:00
@method ( HTTPMethod . POST )
@path ( "/v3/get_headers" )
void getHeaders ( HTTPServerRequest req , HTTPServerResponse res ) {
2023-06-14 19:53:52 +00:00
auto log = getLogger ( ) ;
log . info ( "[<<] anisette-v3 /v3/get_headers" ) ;
string identifier = "(null)" ;
2023-04-30 23:22:51 +00:00
try {
2023-06-14 19:53:52 +00:00
import std.uuid ;
2023-06-16 18:19:18 +00:00
auto json = req . json ( ) ;
ubyte [ ] identifierBytes = Base64 . decode ( json [ "identifier" ] . to ! string ( ) ) ;
ubyte [ ] adi_pb = Base64 . decode ( json [ "adi_pb" ] . to ! string ( ) ) ;
2023-06-14 19:53:52 +00:00
identifier = UUID ( identifierBytes [ 0. . 16 ] ) . toString ( ) ;
2023-06-16 18:19:18 +00:00
2023-05-01 09:37:09 +00:00
auto provisioningPath = file . getcwd ( )
2023-06-16 18:19:18 +00:00
. buildPath ( "provisioning" )
. buildPath ( identifier ) ;
if ( file . exists ( provisioningPath ) ) {
file . rmdirRecurse ( provisioningPath ) ;
}
2023-05-01 09:37:09 +00:00
file . mkdir ( provisioningPath ) ;
file . write ( provisioningPath . buildPath ( "adi.pb" ) , adi_pb ) ;
2023-06-16 18:56:23 +00:00
GC . disable ( ) ; // garbage collector can deallocate ADI parts since it can't find the pointers.
2023-08-11 22:43:14 +00:00
scope ( exit ) {
GC . enable ( ) ;
GC . collect ( ) ;
}
2023-06-16 18:56:23 +00:00
2023-06-16 18:47:30 +00:00
ADI adi = makeGarbageCollectedADI ( libraryPath ) ;
2023-05-01 09:37:09 +00:00
adi . provisioningPath = provisioningPath ;
2023-06-14 19:53:52 +00:00
adi . identifier = identifier . toUpper ( ) [ 0. . 16 ] ;
2023-05-01 09:37:09 +00:00
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" ,
] ;
2023-06-14 19:53:52 +00:00
res . headers [ "Implementation-Version" ] = brandingCode ;
2023-06-16 18:19:18 +00:00
res . writeBody ( response . toString ( JSONOptions . doNotEscapeSlashes ) , "application/json" ) ;
log . info ( "[>>] anisette-v3 /v3/get_headers OK." ) ;
2023-04-30 23:22:51 +00:00
} catch ( Throwable t ) {
JSONValue error = [
2023-05-01 09:37:09 +00:00
"result" : "GetHeadersError" ,
"message" : typeid ( t ) . name ~ ": " ~ t . msg
2023-04-30 23:22:51 +00:00
] ;
2023-06-14 19:53:52 +00:00
res . headers [ "Implementation-Version" ] = brandingCode ;
2023-06-16 18:19:18 +00:00
log . info ( "[>>] anisette-v3 /v3/get_headers error." ) ;
res . writeBody ( error . toString ( JSONOptions . doNotEscapeSlashes ) , "application/json" ) ;
2023-04-30 23:22:51 +00:00
} finally {
2023-06-14 19:53:52 +00:00
if ( file . exists (
file . getcwd ( )
. buildPath ( "provisioning" )
. buildPath ( identifier )
) ) {
file . rmdirRecurse (
file . getcwd ( )
. buildPath ( "provisioning" )
. buildPath ( identifier )
) ;
}
2023-04-30 23:22:51 +00:00
}
2023-06-14 19:53:52 +00:00
}
2023-04-30 23:22:51 +00:00
2023-06-16 18:19:18 +00:00
@method ( HTTPMethod . GET )
@path ( "/v3/provisioning_session" )
void provisionSession ( scope WebSocket socket ) {
auto log = getLogger ( ) ;
2023-06-17 10:40:20 +00:00
scope ( exit ) socket . close ( ) ;
2023-04-30 23:22:51 +00:00
2023-06-17 10:40:20 +00:00
auto requestUUID = randomUUID ( ) . toString ( ) ; // Assign a random UUID to the request to make it easier to track.
log . infoF ! "[<< %s] anisette-v3 /v3/provisionSession connected." ( requestUUID ) ;
2023-06-16 18:19:18 +00:00
2023-06-17 10:40:20 +00:00
JSONValue giveIdentifier = [
"result" : "GiveIdentifier"
] ;
socket . send ( giveIdentifier . toString ( JSONOptions . doNotEscapeSlashes ) ) ;
2023-06-16 18:19:18 +00:00
2023-06-17 10:40:20 +00:00
log . infoF ! "[>> %s] Asking for identifier." ( requestUUID ) ;
if ( ! socket . waitForData ( timeout ) ) {
JSONValue timeoutJs = [
"result" : "Timeout"
] ;
log . infoF ! "[>> %s] Timeout!" ( requestUUID ) ;
socket . send ( timeoutJs . toString ( JSONOptions . doNotEscapeSlashes ) ) ;
socket . close ( ) ;
return ;
}
2023-04-30 23:22:51 +00:00
2023-08-09 17:24:36 +00:00
string identifier ;
try {
auto res = parseJSON ( socket . receiveText ( ) ) ;
ubyte [ ] requestedIdentifier = Base64 . decode ( res [ "identifier" ] . str ( ) ) ;
log . infoF ! "[>> %s] Got it." ( requestUUID ) ;
2023-06-16 18:19:18 +00:00
2023-08-09 17:24:36 +00:00
identifier = UUID ( requestedIdentifier [ 0. . 16 ] ) . toString ( ) ;
} catch ( Exception ex ) {
2023-06-16 18:19:18 +00:00
JSONValue response = [
2023-06-17 10:40:20 +00:00
"result" : "InvalidIdentifier"
2023-06-16 18:19:18 +00:00
] ;
2023-08-09 17:24:36 +00:00
log . infoF ! "[>> %s] It is invalid: %s" ( requestUUID , ex ) ;
2023-06-16 18:19:18 +00:00
socket . send ( response . toString ( JSONOptions . doNotEscapeSlashes ) ) ;
2023-06-17 10:40:20 +00:00
socket . close ( ) ;
return ;
}
2023-06-16 18:19:18 +00:00
2023-06-17 10:40:20 +00:00
log . infoF ! ( "[<< %s] Correct identifier (%s)." ) ( requestUUID , identifier ) ;
GC . disable ( ) ; // garbage collector can deallocate ADI parts since it can't find the pointers.
2023-08-11 22:43:14 +00:00
scope ( exit ) {
GC . enable ( ) ;
GC . collect ( ) ;
}
2023-06-17 10:40:20 +00:00
ADI adi = makeGarbageCollectedADI ( libraryPath ) ;
auto provisioningPath = file . getcwd ( )
. buildPath ( "provisioning" )
. buildPath ( identifier ) ;
adi . provisioningPath = provisioningPath ;
scope ( exit ) {
if ( file . exists ( provisioningPath ) ) {
file . rmdirRecurse ( provisioningPath ) ;
2023-06-16 18:19:18 +00:00
}
2023-06-17 10:40:20 +00:00
}
adi . identifier = identifier . toUpper ( ) [ 0. . 16 ] ;
JSONValue response = [
"result" : "GiveStartProvisioningData"
] ;
log . infoF ! "[>> %s] Okay asking for spim now." ( requestUUID ) ;
socket . send ( response . toString ( JSONOptions . doNotEscapeSlashes ) ) ;
if ( ! socket . waitForData ( timeout ) ) {
JSONValue timeoutJs = [
"result" : "Timeout"
] ;
log . infoF ! "[>> %s] Timeout!" ( requestUUID ) ;
socket . send ( timeoutJs . toString ( JSONOptions . doNotEscapeSlashes ) ) ;
socket . close ( ) ;
return ;
}
uint session ;
try {
2023-08-09 17:24:36 +00:00
auto res = parseJSON ( socket . receiveText ( ) ) ;
2023-06-16 18:19:18 +00:00
string spim = res [ "spim" ] . str ( ) ;
2023-06-17 10:40:20 +00:00
log . infoF ! "[<< %s] Received SPIM." ( requestUUID ) ;
2023-06-16 18:19:18 +00:00
auto cpimAndCo = adi . startProvisioning ( - 2 , Base64 . decode ( spim ) ) ;
2023-06-17 10:40:20 +00:00
session = cpimAndCo . session ;
2023-06-16 18:19:18 +00:00
response = [
"result" : "GiveEndProvisioningData" ,
"cpim" : Base64 . encode ( cpimAndCo . clientProvisioningIntermediateMetadata )
] ;
2023-06-17 10:40:20 +00:00
log . infoF ! "[>> %s] Okay gimme ptm tk." ( requestUUID ) ;
2023-06-16 18:19:18 +00:00
socket . send ( response . toString ( JSONOptions . doNotEscapeSlashes ) ) ;
2023-06-17 10:40:20 +00:00
} catch ( Exception ex ) {
JSONValue error = [
"result" : "StartProvisioningError" ,
"message" : format ! "%s (request id: %s)" ( ex . msg , requestUUID )
] ;
log . errorF ! "[>> %s] anisette-v3 error: %s" ( requestUUID , ex ) ;
socket . send ( error . toString ( ) ) ;
return ;
}
2023-06-16 18:19:18 +00:00
2023-06-17 10:40:20 +00:00
if ( ! socket . waitForData ( timeout ) ) {
JSONValue timeoutJs = [
"result" : "Timeout"
] ;
log . infoF ! "[>> %s] Timeout!" ( requestUUID ) ;
socket . send ( timeoutJs . toString ( JSONOptions . doNotEscapeSlashes ) ) ;
socket . close ( ) ;
return ;
}
try {
2023-08-09 17:24:36 +00:00
auto res = parseJSON ( socket . receiveText ( ) ) ;
2023-06-16 18:19:18 +00:00
string ptm = res [ "ptm" ] . str ( ) ;
string tk = res [ "tk" ] . str ( ) ;
2023-06-17 10:40:20 +00:00
log . infoF ! "[<< %s] Received PTM and TK." ( requestUUID ) ;
2023-06-16 18:19:18 +00:00
adi . endProvisioning ( session , Base64 . decode ( ptm ) , Base64 . decode ( tk ) ) ;
response = [
"result" : "ProvisioningSuccess" ,
"adi_pb" : Base64 . encode (
cast ( ubyte [ ] ) file . read (
adi . provisioningPath ( )
. buildPath ( "adi.pb" )
)
)
] ;
2023-06-17 10:40:20 +00:00
} catch ( Exception ex ) {
2023-06-16 18:19:18 +00:00
JSONValue error = [
2023-06-17 10:40:20 +00:00
"result" : "EndProvisioningError" ,
"message" : format ! "%s (request id: %s)" ( ex . msg , requestUUID )
2023-06-16 18:19:18 +00:00
] ;
2023-06-17 10:40:20 +00:00
log . errorF ! "[>> %s] anisette-v3 error: %s" ( requestUUID , ex ) ;
2023-06-16 18:19:18 +00:00
socket . send ( error . toString ( ) ) ;
2023-06-17 10:40:20 +00:00
return ;
2023-04-30 23:22:51 +00:00
}
2023-06-17 10:40:20 +00:00
log . infoF ! "[>> %s] Okay all right here is your provisioning data." ( requestUUID ) ;
socket . send ( response . toString ( JSONOptions . doNotEscapeSlashes ) ) ;
2023-04-30 23:22:51 +00:00
}
}
2023-06-16 18:47:30 +00:00
private ADI makeGarbageCollectedADI ( string libraryPath ) {
extern ( C ) void * malloc_GC ( size_t sz ) {
2023-08-12 01:06:41 +00:00
return GC . malloc ( sz , GC . BlkAttr . NO_MOVE ) ;
2023-06-16 18:47:30 +00:00
}
extern ( C ) void free_GC ( void * ptr ) {
GC . free ( ptr ) ;
}
AndroidLibrary storeServicesCore = new AndroidLibrary ( libraryPath . buildPath ( "libstoreservicescore.so" ) , [
"malloc" : cast ( void * ) & malloc_GC ,
"free" : cast ( void * ) & free_GC
] ) ;
return new ADI ( libraryPath , storeServicesCore ) ;
}