ios-uploader
Version:
Easy to use, cross-platform tool to upload an iOS app to itunes-connect.
384 lines (335 loc) • 10.5 kB
JavaScript
const axios = require('axios');
const path = require('path');
const utility = require('./utility');
const SOFTWARE_SERVICE_URL = 'https://contentdelivery.itunes.apple.com/WebObjects/MZLabelService.woa/json/MZITunesSoftwareService';
const PRODUCER_SERVICE_URL = 'https://contentdelivery.itunes.apple.com/WebObjects/MZLabelService.woa/json/MZITunesProducerService';
const USER_AGENT = 'iTMSTransporter/2.0.0';
const MAX_BODY_LENGTH = 1024 ** 3;
/**
* Construct error message using application error string and response object.
* @param {String} message Application error message
* @param {Object|undefined} response Response object from remote request,
* used to extract error message if any.
* @returns {Error} An error that can be thrown.
*/
function constructError(message, response) {
let errorMessage = message;
if (response && response.ErrorMessage) {
errorMessage += '\n' + response.ErrorMessage;
}
return new Error(errorMessage);
}
async function generateMetadata(ctx) {
let metaText = await utility.readFile(path.join(__dirname, '../assets/metadata_template.xml'));
const fileStats = await utility.getFileStats(ctx.fileHandle);
ctx.fileName = path.basename(ctx.filePath).replace(/[: ]/g, '_');
ctx.fileChecksum = await utility.getFileMD5(ctx.fileHandle);
ctx.fileSize = fileStats.size;
ctx.fileModifiedTime = Math.round(fileStats.mtimeMs);
metaText = metaText
.replace('APPLE_ID', ctx.appleId)
.replace('BUNDLE_SHORT_VERSION', ctx.bundleShortVersion)
.replace('BUNDLE_VERSION', ctx.bundleVersion)
.replace('BUNDLE_IDENTIFIER', ctx.bundleId)
.replace('FILE_SIZE', ctx.fileSize)
.replace('FILE_NAME', ctx.fileName)
.replace('MD5', ctx.fileChecksum);
ctx.metadataSize = metaText.length;
ctx.metadataChecksum = utility.getStringMD5(metaText);
ctx.metadataBuffer = Buffer.from(metaText, 'utf-8');
ctx.metadataCompressed = await utility.bufferToGZBase64(ctx.metadataBuffer);
}
async function makeSoftwareServiceRequest(ctx, method, params) {
const requestId = utility.generateIDString();
const request = {
jsonrpc: '2.0',
method,
id: requestId,
params,
};
const headers = {
'User-Agent': USER_AGENT,
'Content-Type': 'application/json',
};
const json = JSON.stringify(request);
const jsonChecksum = utility.getStringMD5Buffer(json);
if (ctx.sessionId) {
headers['x-request-id'] = requestId;
headers['x-session-digest'] = utility.makeSessionDigest(ctx.sessionId, jsonChecksum, requestId, ctx.sharedSecret);
headers['x-session-id'] = ctx.sessionId;
headers['x-session-version'] = '2';
}
let res = await axios.post(
SOFTWARE_SERVICE_URL,
json,
{ headers },
);
return res.data.result;
}
async function makeProducerServiceRequest(ctx, method, params) {
const requestId = utility.generateIDString();
const request = {
jsonrpc: '2.0',
method,
id: requestId,
params,
};
const headers = {
'User-Agent': USER_AGENT,
'Content-Type': 'application/json',
};
const json = JSON.stringify(request);
const jsonChecksum = utility.getStringMD5Buffer(json);
if (ctx.sessionId) {
headers['x-request-id'] = requestId;
headers['x-session-digest'] = utility.makeSessionDigest(ctx.sessionId, jsonChecksum, requestId, ctx.sharedSecret);
headers['x-session-id'] = ctx.sessionId;
headers['x-session-version'] = '2';
}
let res = await axios.post(
PRODUCER_SERVICE_URL,
json,
{ headers },
);
return res.data.result;
}
async function authenticateForSession(ctx) {
let res = await makeProducerServiceRequest(ctx, 'authenticateForSession', {
Username: ctx.username,
Password: ctx.password,
});
if (res.SessionId && res.SharedSecret) {
ctx.sessionId = res.SessionId;
ctx.sharedSecret = res.SharedSecret;
}
else {
throw constructError('Authentication failed!', res);
}
}
async function lookupSoftwareForBundleId(ctx) {
let res = await makeSoftwareServiceRequest(ctx, 'lookupSoftwareForBundleId', {
Application: 'altool',
ApplicationBundleId: 'com.apple.itunes.altool',
BundleId: ctx.bundleId,
Version: '4.0.1 (1182)',
});
if (!res.Success || res.Attributes.length < 1) {
throw constructError('Application lookup failed!', res);
}
ctx.appleId = res.Attributes[0].AppleID;
ctx.appName = res.Attributes[0].Application;
ctx.appIconUrl = res.Attributes[0].IconURL;
}
async function validateMetadata(ctx) {
let res = await makeProducerServiceRequest(ctx, 'validateMetadata', {
Application: 'iTMSTransporter',
BaseVersion: '2.0.0',
Files: [
ctx.fileName,
'metadata.xml',
],
iTMSTransporterMode: 'upload',
MetadataChecksum: ctx.metadataChecksum,
MetadataCompressed: ctx.metadataCompressed,
MetadataInfo: {
app_platform: 'ios',
apple_id: ctx.appleId,
asset_types: [
'bundle',
],
bundle_identifier: ctx.bundleId,
bundle_short_version_string: ctx.bundleShortVersion,
bundle_version: ctx.bundleVersion,
device_id: '',
packageVersion: 'software5.4',
primary_bundle_identifier: '',
},
PackageName: ctx.packageName,
PackageSize: ctx.fileSize + ctx.metadataSize,
Username: ctx.username,
Version: '2.0.0',
});
if (!res.Success) {
throw constructError('Metadata validation failed!', res);
}
}
async function validateAssets(ctx) {
let res = await makeProducerServiceRequest(ctx, 'validateAssets', {
Application: 'iTMSTransporter',
BaseVersion: '2.0.0',
AssetDescriptionsCompressed: [],
Files: [
ctx.fileName,
'metadata.xml',
],
iTMSTransporterMode: 'upload',
MetadataChecksum: ctx.metadataChecksum,
MetadataCompressed: ctx.metadataCompressed,
MetadataInfo: {
app_platform: 'ios',
apple_id: ctx.appleId,
asset_types: [
'bundle',
],
bundle_identifier: ctx.bundleId,
bundle_short_version_string: ctx.bundleShortVersion,
bundle_version: ctx.bundleVersion,
device_id: '',
packageVersion: 'software5.4',
primary_bundle_identifier: '',
},
PackageName: ctx.packageName,
PackageSize: ctx.fileSize + ctx.metadataSize,
StreamingInfoList: [],
Transport: 'HTTP',
Username: ctx.username,
Version: '2.0.0',
});
if (!res.Success) {
throw constructError('Asset validation failed!', res);
}
// validateAssets returns a new package name.
ctx.packageName = res.NewPackageName;
}
async function clientChecksumCompleted(ctx) {
let res = await makeProducerServiceRequest(ctx, 'clientChecksumCompleted', {
Application: 'iTMSTransporter',
BaseVersion: '2.0.0',
iTMSTransporterMode: 'upload',
NewPackageName: ctx.packageName,
Username: ctx.username,
Version: '2.0.0',
});
if (!res.Success) {
throw constructError('Client checksum failed!', res);
}
}
async function createReservation(ctx) {
let res = await makeProducerServiceRequest(ctx, 'createReservation', {
Application: 'iTMSTransporter',
BaseVersion: '2.0.0',
fileDescriptions: [
{
checksum: ctx.metadataChecksum,
checksumAlgorithm: 'MD5',
contentType: 'application/xml',
fileName: 'metadata.xml',
fileSize: ctx.metadataSize,
},
{
checksum: ctx.fileChecksum,
checksumAlgorithm: 'MD5',
contentType: 'application/octet-stream',
fileName: ctx.fileName,
fileSize: ctx.fileSize,
uti: 'com.apple.ipa',
},
],
iTMSTransporterMode: 'upload',
NewPackageName: ctx.packageName,
Username: ctx.username,
Version: '2.0.0',
});
if (!res.Success) {
throw constructError('Create reservation failed!', res);
}
return res.Reservations;
}
async function executeOperation({ ctx, reservation, operation }) {
let data;
if (reservation.file === 'metadata.xml') {
data = ctx.metadataBuffer.slice(operation.offset, operation.offset + operation.length);
}
else if (reservation.file === ctx.fileName) {
data = await utility.getFilePart(ctx.fileHandle, operation.offset, operation.length);
}
else {
// Unknown file
return;
}
let res;
try {
res = await axios({
url: operation.uri,
method: operation.method,
headers: Object.assign({
'User-Agent': USER_AGENT,
}, operation.headers),
validateStatus: null,
maxBodyLength: MAX_BODY_LENGTH,
data,
});
}
catch (err) {
throw new Error('Upload failed!\n' + err.message);
}
if (res.status != 200) {
throw new Error('Upload failed! (' + res.status + ')');
}
ctx.bytesSent += operation.length;
}
async function commitReservation(ctx, reservation) {
let res = await makeProducerServiceRequest(ctx, 'commitReservation', {
Application: 'iTMSTransporter',
BaseVersion: '2.0.0',
iTMSTransporterMode: 'upload',
NewPackageName: ctx.packageName,
reservations: [
reservation.id,
],
Username: ctx.username,
Version: '2.0.0',
});
if (!res.Success) {
throw constructError('Commit reservation failed!', res);
}
}
async function uploadDoneWithArguments(ctx) {
let res = await makeProducerServiceRequest(ctx, 'uploadDoneWithArguments', {
Application: 'iTMSTransporter',
BaseVersion: '2.0.0',
FileSizeInfo: {
[ctx.fileName]: ctx.fileSize,
'metadata.xml': ctx.metadataSize,
},
ClientChecksumInfo: [
{
CalculatedChecksum: ctx.fileChecksum,
CalculationTime: 100,
FileLastModified: ctx.fileModifiedTime,
Filename: ctx.fileName,
fileSize: ctx.fileSize,
},
],
StatisticsArray: [],
StreamingInfoList: [],
iTMSTransporterMode: 'upload',
PackagePathWithoutBase: null,
NewPackageName: ctx.packageName,
Transport: 'HTTP',
TransferTime: ctx.transferTime,
NumberBytesTransferred: ctx.fileSize + ctx.metadataSize,
Username: ctx.username,
Version: '2.0.0',
});
if (!res.Success) {
throw constructError('Upload completion failed!', res);
}
}
module.exports = {
SOFTWARE_SERVICE_URL,
PRODUCER_SERVICE_URL,
constructError,
generateMetadata,
makeSoftwareServiceRequest,
makeProducerServiceRequest,
authenticateForSession,
lookupSoftwareForBundleId,
validateMetadata,
validateAssets,
clientChecksumCompleted,
createReservation,
executeOperation,
commitReservation,
uploadDoneWithArguments,
};