iOS Ad-hoc distribution using Amazon S3

Why?

Every iOS developer at some point in their life is challenged with a task to distribute their awesome app to a group of loyal beta testers. Apple does not make it easy. The reasons for this are clear. We all care about security and greatly appreciate the fact that iOS platform is much more secure than its main competitor, but it is still a problem that needs to be solved. There are services out there that try to automate the ad-hoc distribution process, but sometimes it is not desirable or even possible to use them. Thus, there is a need to bake an in-house distribution system.

To make the ad-hoc distribution work, one must:

  • Upload the IPA file to a place where it can be downloaded
  • Generate a special plist xml file, referring to the IPA above. This file must be accessible over https!
  • Generate an html file with a special Download link, pointing to the plist file above. This is the page users will see

Throughout this tutorial, we are going to use an application called Dishero as an example. We will host it directly on S3, without spinning up machines or configuring http servers. The result will look similar to this:

We will produce a nice webpage, hosted on S3 that will provide details and a one-click install for our application.

Prerequisites

Create an Ad-hoc distribution profile

First of all you will need to create a distribution profiles in iTunes Connect.

Ad-Hoc distribution limits the the ability to install your application only on specific devices that have to be pre-registered. Instruct your beta users to visit http://whatsmyudid.com/ and to follow the instructions there to obtain UDID (Unique Device ID) for their device and send it to you.

Follow the pictures below to create an Ad-Hoc provisioning profile, and add all the devices to it. Download it once ready and install in your local keychain by double-clicking on it.





Create a bucket on S3 and enable web hosting on it

You will also need a dedicated bucket on S3 for this. Make sure you enable Web Hosting on your bucket as shown in the picture below.
Bonus: If you want to have your download page to be at a nice URL (we are using download.dishero.com for this example), you have to do a few tricks:

  1. Name your bucket accordingly - download.dishero.com
  2. In your DNS, configure a CNAME record download.dishero.com to point to download.dishero.com.s3.amazonaws.com.

Install S3cmd

We will need s3cmd to upload the files to S3 and set the right permissions.

Installing it using homebrew is easy:

1
brew install s3cmd

So is installing it using python’s pip:

1
sudo pip install s3cmd

…or you can just follow the manual process described on s3cmd website.

Once you have it installed, run it

1
s3cmd --configure

and enter your AWS credentials when prompted. Credentials will be saved and we won’t need to state them explicitly anymore.

Where the magic happens

The script below does all the heavy lifting. It generates index.html, proper plist file and uploads them along with the IPA itself to S3.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#!/bin/bash
set -e
PROJECT="Dishero"
FILE=$1
TARGET=$2
VERSION=$3
BUILD=$4
BUILD_TIME=`stat -f "%Sm" -t "%Y-%m-%d %H:%M:%S" $1`
SECURE_TARGET="https://s3-us-west-2.amazonaws.com/${TARGET}"
echo "Generating ${PROJECT}-${VERSION}-${BUILD}.plist"
cat > ./${PROJECT}-${VERSION}-${BUILD}.plist <<PLIST_DELIM
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>items</key>
<array>
<dict>
<key>assets</key>
<array>
<dict>
<key>kind</key>
<string>software-package</string>
<key>url</key>
<string>http://${TARGET}/${PROJECT}-${VERSION}-${BUILD}.ipa</string>
</dict>
</array>
<key>metadata</key>
<dict>
<key>bundle-identifier</key>
<string>${BUNDLE}</string>
<key>bundle-version</key>
<string>${VERSION}</string>
<key>kind</key>
<string>software</string>
<key>subtitle</key>
<string>${PROJECT}</string>
<key>title</key>
<string>${PROJECT}</string>
</dict>
</dict>
</array>
</dict>
</plist>
PLIST_DELIM
echo "Generating index.html"
cat > ./index.html <<INDEX_DELIM
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
<title>${PROJECT} ${VERSION} (${BUILD})</title>
<style type="text/css">
body {background:#fff;margin:0;padding:0;font-family:arial,helvetica,sans-serif;text-align:center;padding:10px;color:#333;font-size:16px;}
#container {width:300px;margin:0 auto;}
h1 {margin:0;padding:0;font-size:14px;}
p {font-size:13px;}
.link {background:#ecf5ff;border-top:1px solid #fff;border:1px solid #dfebf8;margin-top:.5em;padding:.3em;}
.link a {text-decoration:none;font-size:15px;display:block;color:#069;}
.warning {font-size: 12px; color:#F00; font-weight:bold; margin:10px 0px;}
</style>
</head>
<body>
<div id="container">
<h1>iOS 7.0 and newer:</h1>
<p>${PROJECT} Beta ${VERSION} (${BUILD})</p>
<p>Built on ${BUILD_TIME}</p>
<div class="link"><a href="itms-services://?action=download-manifest;url=${SECURE_TARGET}/${PROJECT}-${VERSION}-${BUILD}.plist">Tap to install!</a></div>
<p><strong>Link didn't work?</strong><br />Make sure you're visiting this page on your device, not your computer.</p>
</body>
</html>
INDEX_DELIM
echo "Uploading ${PROJECT}-${VERSION}-${BUILD}.ipa to s3://${TARGET}"
s3cmd put -P $FILE s3://${TARGET}/${PROJECT}-${VERSION}-${BUILD}.ipa
echo "Uploading ${PROJECT}-${VERSION}-${BUILD}.plist to s3://${TARGET}"
s3cmd put -P ${PROJECT}-${VERSION}-${BUILD}.plist s3://${TARGET}/${PROJECT}-${VERSION}-${BUILD}.plist
echo "Uploading index.html to s3://${TARGET}"
s3cmd put -P index.html s3://${TARGET}/

Executing the script requires only a few parameters: an IPA file, a bucket path, a version and a build

1
2
# $PATH_TO_SCRIPT/upload_to_s3.sh <IPA_FILE> <TARGET_PATH> <VERSION> <BUILD>
$PATH_TO_SCRIPT/upload_to_s3.sh Dishero.ipa download.dishero.com/beta 2.5.0 1795

The command above will make the download page available at http://download.dishero.com/beta. Alternatively, if DNS is not under your control and you can’t do CNAME tricks to get a proper hostname, you will have to point your users to https://s3-us-west-2.amazonaws.com/bucketname/path/