Skip to content

Commit

Permalink
feat: add Heroku deploy support
Browse files Browse the repository at this point in the history
  • Loading branch information
ViRb3 committed May 20, 2021
1 parent 283d886 commit 4fdc3f5
Show file tree
Hide file tree
Showing 7 changed files with 343 additions and 3 deletions.
16 changes: 16 additions & 0 deletions Dockerfile.heroku
@@ -0,0 +1,16 @@
FROM golang:1.16-alpine AS builder

WORKDIR /src
COPY . .

RUN go mod download && \
CGO_ENABLED=0 go build -ldflags="-s -w" -o "bin-release"

FROM alpine:3.13.5

WORKDIR /

COPY --from=builder "/src/bin-release" "/"

CMD ["/bin-release"]
EXPOSE 8080
88 changes: 88 additions & 0 deletions app.json
@@ -0,0 +1,88 @@
{
"name": "iOS Signer Service",
"description": "A self-hosted, cross-platform service to sign and install iOS apps, all without a computer.",
"repository": "https://github.com/SignTools/ios-signer-service",
"logo": "https://github.com/SignTools/ios-signer-service/raw/master/img/logo.png",
"keywords": [
"ios",
"cross-platform",
"signing",
"ci",
"apps",
"ipa",
"sideload"
],
"stack": "container",
"image": "signtools/ios-signer-service",
"env": {
"BUILDER_GITHUB_ENABLE": {
"description": "A builder is required. Currently, only GitHub Actions is supported. Make sure you have one set up before continuing: https://github.com/SignTools/ios-signer-service/blob/heroku/INSTALL-SIMPLE.md#2-builder",
"generator": "true",
"value": "true"
},
"BUILDER_GITHUB_REPO_NAME": {
"description": "The name you gave to your builder repository.",
"value": "ios-signer-ci"
},
"BUILDER_GITHUB_ORG_NAME": {
"description": "Your GitHub profile/organization name.",
"value": ""
},
"BUILDER_GITHUB_WORKFLOW_FILE_NAME": {
"description": "Your builder repository's workflow file name. Leave as default if you didn't change this.",
"value": "sign.yml"
},
"BUILDER_GITHUB_TOKEN": {
"description": "Your GitHub personal access token that you created with the builder.",
"value": ""
},
"BUILDER_GITHUB_REF": {
"description": "Your builder repository ref (branch). Leave as default if you didn't change this.",
"value": "master"
},
"SERVER_URL": {
"description": "The public address of your server. This depends on the 'App name' you chose at the top of this page. For example, if you named your Heroku app 'testapp', the address will be: https://testapp.herokuapp.com",
"value": ""
},
"BASIC_AUTH_ENABLE": {
"description": "Your app will be public, so it must be password-protected.",
"generator": "true",
"value": "true"
},
"BASIC_AUTH_USERNAME": {
"description": "A strong username to protect your service.",
"value": ""
},
"BASIC_AUTH_PASSWORD": {
"description": "A strong password to protect your service.",
"value": ""
},
"PROFILE_NAME": {
"description": "A friendly name to display your signing profile on the website.",
"value": "My iPhone 12"
},
"PROFILE_CERT_PASS": {
"description": "Your signing profile certificate's password.",
"value": ""
},
"PROFILE_CERT_BASE64": {
"description": "Your signing profile's certificate (p12). You have to encode it as base64. Use the following link: (Safari unsupported!) - https://git.io/base64z - click on 'Open file as input' in the top-right corner, and when you get a bunch of letters as 'Output', paste them here.",
"value": ""
},
"PROFILE_PROV_BASE64": {
"description": "Your signing profile's provisioning profile (mobileprovision). Not needed if you are using an Apple developer account. You have to encode it as base64. Use the following link: (Safari unsupported!) - https://git.io/base64z - click on 'Open file as input' in the top-right corner, and when you get a bunch of letters as 'Output', paste them here.",
"required": false,
"value": ""
},
"PROFILE_ACCOUNT_NAME": {
"description": "Your Apple developer account's username. Not needed if you are using a provisioning profile.",
"required": false,
"value": ""
},
"PROFILE_ACCOUNT_PASS": {
"description": "Your Apple developer account's password. Not needed if you are using a provisioning profile.",
"required": false,
"value": ""
}
}
}
5 changes: 5 additions & 0 deletions heroku.yml
@@ -0,0 +1,5 @@
build:
docker:
web: Dockerfile.heroku
run:
web: /bin-release -port "$PORT"
1 change: 1 addition & 0 deletions main_test.go
Expand Up @@ -86,6 +86,7 @@ func TestMain(m *testing.M) {
},
BuilderKey: builderKey,
PublicUrl: fmt.Sprintf("http://localhost:%d", servePort),
EnvProfile: &config.EnvProfile{},
}
storage.Load()

Expand Down
107 changes: 104 additions & 3 deletions src/config/config.go
Expand Up @@ -5,6 +5,8 @@ import (
"encoding/hex"
"github.com/knadh/koanf"
kyaml "github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/confmap"
"github.com/knadh/koanf/providers/env"
kfile "github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/providers/structs"
"github.com/pkg/errors"
Expand All @@ -13,6 +15,8 @@ import (
"ios-signer-service/src/builders"
"os"
"path/filepath"
"reflect"
"strings"
)

type BasicAuth struct {
Expand Down Expand Up @@ -88,11 +92,25 @@ func createDefaultFile() *File {
}
}

type EnvProfile struct {
Name string `yaml:"name"`
ProvBase64 string `yaml:"prov_base64"`
CertPass string `yaml:"cert_pass"`
CertBase64 string `yaml:"cert_base64"`
AccountName string `yaml:"account_name"`
AccountPass string `yaml:"account_pass"`
}

type ProfileBox struct {
EnvProfile `yaml:"profile"`
}

type Config struct {
Builder builders.Builder
BuilderKey string
PublicUrl string
*File
EnvProfile *EnvProfile
}

var Current Config
Expand All @@ -102,7 +120,8 @@ func Load(fileName string) {
if !isAllowedExt(allowedExts, fileName) {
log.Fatal().Msgf("config extension not allowed: %v\n", allowedExts)
}
fileConfig, err := getFile(fileName)
mapDelim := '.'
fileConfig, err := getFile(mapDelim, fileName)
if err != nil {
log.Fatal().Err(err).Msg("get config")
}
Expand All @@ -114,11 +133,38 @@ func Load(fileName string) {
if _, err := rand.Read(builderKey); err != nil {
log.Fatal().Err(err).Msg("init: error generating builder key")
}
profile, err := getProfileFromEnv(mapDelim)
if err != nil {
log.Fatal().Err(err).Msg("init: error checking for signing profile from envvars")
}
Current = Config{
Builder: builder,
BuilderKey: hex.EncodeToString(builderKey),
File: fileConfig,
EnvProfile: profile,
}
}

// Loads a single signing profile entirely from environment variables.
// Intended for use with Heroku without persistent storage.
func getProfileFromEnv(mapDelim rune) (*EnvProfile, error) {
k := koanf.New(string(mapDelim))
if err := k.Load(structs.Provider(ProfileBox{}, "yaml"), nil); err != nil {
return nil, errors.WithMessage(err, "load default")
}
if err := k.Load(env.Provider("", "_", func(s string) string {
return strings.ToLower(s)
}), nil); err != nil {
return nil, errors.WithMessage(err, "load envvars")
}
if err := inferDelimiters(k, k.All(), mapDelim); err != nil {
return nil, errors.WithMessage(err, "infer delimiters")
}
profile := EnvProfile{}
if err := k.UnmarshalWithConf("profile", &profile, koanf.UnmarshalConf{Tag: "yaml"}); err != nil {
return nil, errors.WithMessage(err, "unmarshal")
}
return &profile, nil
}

func isAllowedExt(allowedExts []string, fileName string) bool {
Expand All @@ -131,8 +177,7 @@ func isAllowedExt(allowedExts []string, fileName string) bool {
return false
}

func getFile(fileName string) (*File, error) {
mapDelim := '.'
func getFile(mapDelim rune, fileName string) (*File, error) {
k := koanf.New(string(mapDelim))
if err := k.Load(structs.Provider(createDefaultFile(), "yaml"), nil); err != nil {
return nil, errors.WithMessage(err, "load default")
Expand All @@ -142,6 +187,15 @@ func getFile(fileName string) (*File, error) {
} else if err != nil {
return nil, errors.WithMessage(err, "load existing")
}
// TODO: Fix entries with same path overwriting each other, e.g. PROFILE_CERT_NAME="bar" and PROFILE_CERT="foo"
if err := k.Load(env.Provider("", "_", func(s string) string {
return strings.ToLower(s)
}), nil); err != nil {
return nil, errors.WithMessage(err, "load envvars")
}
if err := inferDelimiters(k, k.All(), mapDelim); err != nil {
return nil, errors.WithMessage(err, "infer delimiters")
}
fileConfig := File{}
if err := k.UnmarshalWithConf("", &fileConfig, koanf.UnmarshalConf{Tag: "yaml"}); err != nil {
return nil, errors.WithMessage(err, "unmarshal")
Expand All @@ -156,3 +210,50 @@ func getFile(fileName string) (*File, error) {
}
return &fileConfig, nil
}

// By convention, environment variables use an underscore '_' as delimiter. YAML entries also use the same delimiter.
// This creates an ambiguity problem. For an example, take the following structure:
// builder:
// github:
// repo_name:
// The corresponding environment variable would ideally be:
// BUILDER_GITHUB_REPO_NAME
// However, a parser cannot know whether REPO and NAME are two nested entries or one entry with the name 'REPO_NAME'.
//
// inferDelimiters will take a configuration map with both properly separated entries (builder.github.repo_name) and
// greedily separated entries (builder.github.repo.name), and given the map separation delimiter, will transfer the
// values of all greedily separated entries onto the properly separated entries. This process effectively "fixes" the
// ambiguity problem and allows the configuration map to be used as usual.
//
// The input map will be modified.
func inferDelimiters(k *koanf.Koanf, data map[string]interface{}, mapDelim rune) error {
for name, val := range data {
for name2, val2 := range data {
if name == name2 {
continue
}
splitFunc := func(r rune) bool {
return r == '_' || r == mapDelim
}
split := strings.FieldsFunc(name, splitFunc)
split2 := strings.FieldsFunc(name2, splitFunc)
if reflect.DeepEqual(split, split2) {
var srcName string
var dstVal interface{}
if strings.Count(name, string(mapDelim)) > strings.Count(name2, string(mapDelim)) {
srcName = name2
dstVal = val
} else {
srcName = name
dstVal = val2
}
if err := k.Load(confmap.Provider(map[string]interface{}{srcName: dstVal}, string(mapDelim)), nil); err != nil {
return errors.WithMessage(err, "load map with new value")
}
delete(data, name)
delete(data, name2)
}
}
}
return nil
}

0 comments on commit 4fdc3f5

Please sign in to comment.