├── ci
├── app
│ ├── Staticfile
│ └── index.html
├── run-tests.sh
├── create-db.sh
├── create-db.yml
├── acceptance-tests.yml
├── run-tests.yml
├── credentials.example.yml
├── acceptance-tests.sh
└── pipeline.yml
├── CODEOWNERS
├── manifest-base.yml
├── .github
└── pull_request_template.md
├── .codeclimate.yml
├── .travis.yml
├── cf
├── client.go
└── mocks
│ └── Client.go
├── manifest-broker.yml
├── manifest-cron.yml
├── .gitignore
├── healthchecks
├── postgresql.go
├── cloudfront.go
├── cloudfoundry.go
├── letsencrypt.go
├── s3.go
└── healthchecks.go
├── broker
├── broker_bind_test.go
├── broker_last_operation_test.go
├── broker_update_test.go
├── broker_provision_test.go
└── broker.go
├── utils
├── headers.go
├── headers_test.go
├── iam.go
├── certs.go
└── cloudfront.go
├── catalog.json
├── cmd
├── cdn-broker
│ ├── main_test.go
│ └── main.go
└── cdn-cron
│ └── main.go
├── SECURITY.md
├── CONTRIBUTING.md
├── LICENSE.md
├── config
└── config.go
├── go.mod
├── models
├── mocks
│ └── RouteManagerIface.go
├── models_test.go
└── models.go
├── README.md
└── go.sum
/ci/app/Staticfile:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @cloud-gov/customer-success-squad
2 |
3 |
--------------------------------------------------------------------------------
/ci/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | CDN Broker Test
4 |
5 |
6 |
--------------------------------------------------------------------------------
/manifest-base.yml:
--------------------------------------------------------------------------------
1 | ---
2 | buildpack: go_buildpack
3 | memory: 128M
4 | services:
5 | - rds-cdn-broker
6 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ## Changes proposed in this pull request:
2 | -
3 | -
4 | -
5 |
6 | ## security considerations
7 | [Note the any security considerations here, or make note of why there are none]
8 |
--------------------------------------------------------------------------------
/.codeclimate.yml:
--------------------------------------------------------------------------------
1 | engines:
2 | fixme:
3 | enabled: true
4 | gofmt:
5 | enabled: true
6 | govet:
7 | enabled: true
8 | ratings:
9 | paths:
10 | - "**.go"
11 | exclude_paths:
12 | - vendor/
13 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 |
3 | go:
4 | - "1.7"
5 | - tip
6 |
7 | # Dependencies live in /vendor/
8 | install: true
9 |
10 | # Skip vendor tests
11 | script: go test -v $(go list ./... | grep -v /vendor/)
12 |
--------------------------------------------------------------------------------
/cf/client.go:
--------------------------------------------------------------------------------
1 | package cf
2 |
3 | import "github.com/cloudfoundry-community/go-cfclient"
4 |
5 | type Client interface {
6 | GetDomainByName(name string) (cfclient.Domain, error)
7 | GetOrgByGuid(guid string) (cfclient.Org, error)
8 | }
9 |
--------------------------------------------------------------------------------
/manifest-broker.yml:
--------------------------------------------------------------------------------
1 | ---
2 | inherit: manifest-base.yml
3 | applications:
4 | - name: cdn-broker
5 | command: cdn-broker
6 | env:
7 | GO_INSTALL_PACKAGE_SPEC: "./cmd/cdn-broker"
8 | GOPACKAGENAME: "github.com/cloud-gov/cf-cdn-service-broker"
9 |
--------------------------------------------------------------------------------
/ci/run-tests.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -eux
4 |
5 | export GOPATH=$(pwd)/gopath
6 | export PATH=${PATH}:${GOPATH}/bin
7 | mkdir -p ${GOPATH}/bin
8 |
9 | pushd gopath/src/github.com/cloud-gov/cf-cdn-service-broker
10 | go test $(go list ./... | grep -v /vendor/)
11 | popd
12 |
--------------------------------------------------------------------------------
/manifest-cron.yml:
--------------------------------------------------------------------------------
1 | ---
2 | inherit: manifest-base.yml
3 | applications:
4 | - name: cdn-cron
5 | command: cdn-cron
6 | health-check-type: process
7 | no-route: true
8 | env:
9 | GO_INSTALL_PACKAGE_SPEC: "./cmd/cdn-cron"
10 | GOPACKAGENAME: "github.com/cloud-gov/cf-cdn-service-broker"
11 |
--------------------------------------------------------------------------------
/ci/create-db.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e -u
4 |
5 | cf login -a $CF_API_URL -u $CF_USERNAME -p $CF_PASSWORD -o $CF_ORGANIZATION -s $CF_SPACE
6 |
7 | # Create database service instance if not exists
8 | if ! cf service $SERVICE_NAME ; then
9 | cf create-service $SERVICE_TYPE $SERVICE_PLAN $SERVICE_NAME
10 | fi
11 |
--------------------------------------------------------------------------------
/ci/create-db.yml:
--------------------------------------------------------------------------------
1 | ---
2 | platform: linux
3 |
4 | image_resource:
5 | type: registry-image
6 | source:
7 | aws_access_key_id: ((ecr_aws_key))
8 | aws_secret_access_key: ((ecr_aws_secret))
9 | repository: general-task
10 | aws_region: us-gov-west-1
11 | tag: latest
12 |
13 | inputs:
14 | - name: broker-src
15 |
16 | run:
17 | path: broker-src/ci/create-db.sh
18 |
--------------------------------------------------------------------------------
/ci/acceptance-tests.yml:
--------------------------------------------------------------------------------
1 | ---
2 | platform: linux
3 |
4 | image_resource:
5 | type: registry-image
6 | source:
7 | aws_access_key_id: ((ecr_aws_key))
8 | aws_secret_access_key: ((ecr_aws_secret))
9 | repository: general-task
10 | aws_region: us-gov-west-1
11 | tag: latest
12 |
13 | inputs:
14 | - name: broker-src
15 |
16 | run:
17 | path: broker-src/ci/acceptance-tests.sh
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled Object files, Static and Dynamic libs (Shared Objects)
2 | *.o
3 | *.a
4 | *.so
5 |
6 | # Folders
7 | _obj
8 | _test
9 | tmp
10 |
11 | # Architecture specific extensions/prefixes
12 | *.[568vq]
13 | [568vq].out
14 |
15 | *.cgo1.go
16 | *.cgo2.c
17 | _cgo_defun.c
18 | _cgo_gotypes.go
19 | _cgo_export.*
20 |
21 | _testmain.go
22 |
23 | *.exe
24 | *.test
25 | *.prof
26 |
27 | credentials.yml
28 |
29 | vendor/
30 |
--------------------------------------------------------------------------------
/healthchecks/postgresql.go:
--------------------------------------------------------------------------------
1 | package healthchecks
2 |
3 | import (
4 | "github.com/jinzhu/gorm"
5 | _ "github.com/jinzhu/gorm/dialects/postgres"
6 |
7 | "github.com/cloud-gov/cf-cdn-service-broker/config"
8 | )
9 |
10 | func Postgresql(settings config.Settings) error {
11 | db, err := gorm.Open("postgres", settings.DatabaseUrl)
12 | defer db.Close()
13 |
14 | if err != nil {
15 | return err
16 | }
17 |
18 | return nil
19 | }
20 |
--------------------------------------------------------------------------------
/ci/run-tests.yml:
--------------------------------------------------------------------------------
1 | ---
2 | platform: linux
3 |
4 | image_resource:
5 | type: registry-image
6 | source:
7 | aws_access_key_id: ((ecr_aws_key))
8 | aws_secret_access_key: ((ecr_aws_secret))
9 | repository: general-task
10 | aws_region: us-gov-west-1
11 | tag: latest
12 |
13 | inputs:
14 | - name: broker-src
15 | path: gopath/src/github.com/cloud-gov/cf-cdn-service-broker
16 |
17 | run:
18 | path: gopath/src/github.com/cloud-gov/cf-cdn-service-broker/ci/run-tests.sh
19 |
--------------------------------------------------------------------------------
/healthchecks/cloudfront.go:
--------------------------------------------------------------------------------
1 | package healthchecks
2 |
3 | import (
4 | "github.com/aws/aws-sdk-go/aws"
5 | "github.com/aws/aws-sdk-go/aws/session"
6 | "github.com/aws/aws-sdk-go/service/cloudfront"
7 |
8 | "github.com/cloud-gov/cf-cdn-service-broker/config"
9 | )
10 |
11 | func Cloudfront(settings config.Settings) error {
12 | session := session.New(aws.NewConfig().WithRegion(settings.AwsDefaultRegion))
13 | svc := cloudfront.New(session)
14 |
15 | params := &cloudfront.ListDistributionsInput{}
16 | _, err := svc.ListDistributions(params)
17 | if err != nil {
18 | return err
19 | }
20 |
21 | return nil
22 | }
23 |
--------------------------------------------------------------------------------
/broker/broker_bind_test.go:
--------------------------------------------------------------------------------
1 | package broker_test
2 |
3 | import (
4 | "context"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 |
9 | "github.com/pivotal-cf/brokerapi"
10 |
11 | "github.com/cloud-gov/cf-cdn-service-broker/broker"
12 | )
13 |
14 | func TestBind(t *testing.T) {
15 | b := broker.CdnServiceBroker{}
16 | _, err := b.Bind(context.Background(), "", "", brokerapi.BindDetails{})
17 | assert.NotNil(t, err)
18 | }
19 |
20 | func TestUnbind(t *testing.T) {
21 | b := broker.CdnServiceBroker{}
22 | err := b.Unbind(context.Background(), "", "", brokerapi.UnbindDetails{})
23 | assert.NotNil(t, err)
24 | }
25 |
--------------------------------------------------------------------------------
/utils/headers.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "net/textproto"
5 | )
6 |
7 | type Headers map[string]bool
8 |
9 | func (h Headers) Add(header string) {
10 | canonicalHeader := textproto.CanonicalMIMEHeaderKey(header)
11 | h[canonicalHeader] = true
12 | }
13 |
14 | func (h Headers) Contains(header string) bool {
15 | canonicalHeader := textproto.CanonicalMIMEHeaderKey(header)
16 | _, isPresent := h[canonicalHeader]
17 | return isPresent
18 | }
19 |
20 | func (h Headers) Strings() []string {
21 | headers := []string{}
22 | for header, _ := range h {
23 | headers = append(headers, header)
24 | }
25 | return headers
26 | }
27 |
--------------------------------------------------------------------------------
/healthchecks/cloudfoundry.go:
--------------------------------------------------------------------------------
1 | package healthchecks
2 |
3 | import (
4 | "net/http"
5 | "time"
6 |
7 | "github.com/cloudfoundry-community/go-cfclient"
8 |
9 | "github.com/cloud-gov/cf-cdn-service-broker/config"
10 | )
11 |
12 | func Cloudfoundry(settings config.Settings) error {
13 | // We're only validating that the CF endpoint is contactable here, as
14 | // testing the authentication is tricky
15 | _, err := cfclient.NewClient(&cfclient.Config{
16 | ApiAddress: settings.APIAddress,
17 | ClientID: settings.ClientID,
18 | ClientSecret: settings.ClientSecret,
19 | HttpClient: &http.Client{
20 | Timeout: time.Second * 10,
21 | },
22 | })
23 | if err != nil {
24 | return err
25 | }
26 |
27 | return nil
28 | }
29 |
--------------------------------------------------------------------------------
/catalog.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "8478b533-2b59-4007-8494-2feec5970f94",
3 | "name": "cdn-route",
4 | "description": "Custom domains, CDN caching, and TLS certificates with automatic renewal",
5 | "bindable": true,
6 | "metadata": {
7 | "displayName": "CDN Route",
8 | "documentationUrl": "https://cloud.gov/docs/services/cdn-route/"
9 | },
10 | "plan_updateable": true,
11 | "plans": [
12 | {
13 | "id": "fc055c72-1075-44c9-9aee-bddd52e1b053",
14 | "name": "cdn-route",
15 | "description": "Custom domains, CDN caching, and TLS certificates with automatic renewal",
16 | "free": true,
17 | "metadata": {
18 | "displayName": "Content Distribution Network Route"
19 | }
20 | }
21 | ]
22 | }
23 |
--------------------------------------------------------------------------------
/healthchecks/letsencrypt.go:
--------------------------------------------------------------------------------
1 | package healthchecks
2 |
3 | import (
4 | "crypto"
5 | "github.com/xenolf/lego/acme"
6 |
7 | "github.com/cloud-gov/cf-cdn-service-broker/config"
8 | )
9 |
10 | type User struct {
11 | Email string
12 | Registration *acme.RegistrationResource
13 | key crypto.PrivateKey
14 | }
15 |
16 | func (u *User) GetEmail() string {
17 | return u.Email
18 | }
19 |
20 | func (u *User) GetRegistration() *acme.RegistrationResource {
21 | return u.Registration
22 | }
23 |
24 | func (u *User) GetPrivateKey() crypto.PrivateKey {
25 | return u.key
26 | }
27 |
28 | func LetsEncrypt(settings config.Settings) error {
29 | user := &User{key: "cheese"}
30 | _, err := acme.NewClient("https://acme-v01.api.letsencrypt.org/directory", user, acme.RSA2048)
31 | return err
32 | }
33 |
--------------------------------------------------------------------------------
/cmd/cdn-broker/main_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "net/http"
5 | "net/http/httptest"
6 | "testing"
7 |
8 | "code.cloudfoundry.org/lager"
9 | "github.com/cloud-gov/cf-cdn-service-broker/broker"
10 | "github.com/cloud-gov/cf-cdn-service-broker/config"
11 | "github.com/pivotal-cf/brokerapi"
12 | )
13 |
14 | func TestHTTPHandler(t *testing.T) {
15 | brokerAPI := brokerapi.New(
16 | &broker.CdnServiceBroker{},
17 | lager.NewLogger("main.test"),
18 | brokerapi.BrokerCredentials{},
19 | )
20 | handler := bindHTTPHandlers(brokerAPI, config.Settings{})
21 | req, err := http.NewRequest("GET", "http://example.com/healthcheck/http", nil)
22 | if err != nil {
23 | t.Error("Building new HTTP request: error should not have occurred")
24 | }
25 |
26 | w := httptest.NewRecorder()
27 | handler.ServeHTTP(w, req)
28 |
29 | if w.Code != 200 {
30 | t.Errorf("HTTP response: response code was %d, expecting 200", w.Code)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 |
2 | **Reporting Security Issues**
3 |
4 | Please refrain from reporting security vulnerabilities through public GitHub issues.
5 |
6 | Instead, kindly report them via the information provided in [cloud.gov's security.txt](https://cloud.gov/.well-known/security.txt).
7 |
8 | When reporting, include the following details (as much as possible) to help us understand the nature and extent of the potential issue:
9 |
10 | - Type of issue (e.g., buffer overflow, SQL injection, cross-site scripting, etc.)
11 | - Full paths of related source file(s)
12 | - Location of affected source code (tag/branch/commit or direct URL)
13 | - Any special configuration required to reproduce the issue
14 | - Step-by-step instructions to reproduce the issue
15 | - Proof-of-concept or exploit code (if available)
16 | - Impact of the issue, including potential exploitation by attackers
17 |
18 | Providing this information will facilitate a quicker triage of your report.
19 |
--------------------------------------------------------------------------------
/healthchecks/s3.go:
--------------------------------------------------------------------------------
1 | package healthchecks
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/aws/aws-sdk-go/aws"
7 | "github.com/aws/aws-sdk-go/aws/session"
8 | "github.com/aws/aws-sdk-go/service/s3"
9 |
10 | "github.com/cloud-gov/cf-cdn-service-broker/config"
11 | )
12 |
13 | func S3(settings config.Settings) error {
14 | bucket := settings.Bucket
15 | key := "healthcheck-test-key"
16 |
17 | session := session.New(aws.NewConfig().WithRegion(settings.AwsDefaultRegion))
18 | svc := s3.New(session)
19 |
20 | input := s3.PutObjectInput{
21 | Bucket: aws.String(bucket),
22 | Key: aws.String(key),
23 | Body: strings.NewReader("cheese"),
24 | }
25 |
26 | _, err := svc.PutObject(&input)
27 | if err != nil {
28 | return err
29 | }
30 |
31 | _, err = svc.DeleteObject(&s3.DeleteObjectInput{
32 | Bucket: aws.String(bucket),
33 | Key: aws.String(key),
34 | })
35 | if err != nil {
36 | return err
37 | }
38 |
39 | return nil
40 | }
41 |
--------------------------------------------------------------------------------
/utils/headers_test.go:
--------------------------------------------------------------------------------
1 | package utils_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/suite"
7 |
8 | . "github.com/cloud-gov/cf-cdn-service-broker/utils"
9 | )
10 |
11 | func TestHeaders(t *testing.T) {
12 | suite.Run(t, new(HeadersSuite))
13 | }
14 |
15 | type HeadersSuite struct {
16 | suite.Suite
17 | }
18 |
19 | func (h *HeadersSuite) SetupTest() {}
20 |
21 | func (h *HeadersSuite) TestAdd() {
22 | headers := Headers{}
23 | headers.Add("abc-def")
24 | h.Equal(headers, Headers{"Abc-Def": true})
25 | }
26 |
27 | func (h *HeadersSuite) TestContains() {
28 | headers := Headers{"Abc-Def": true}
29 | h.True(headers.Contains("Abc-Def"))
30 | h.False(headers.Contains("Ghi-Jkl"))
31 | }
32 |
33 | func (h *HeadersSuite) TestStrings() {
34 | headers := Headers{"Abc-Def": true, "User-Agent": true}
35 | headerStrings := headers.Strings()
36 | h.Contains(headerStrings, "Abc-Def")
37 | h.Contains(headerStrings, "User-Agent")
38 | h.Equal(len(headerStrings), 2)
39 | }
40 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | **Contribution Policy**
2 |
3 | Cloud.gov is an open source project operated by the U.S. General Services Administration (GSA) to support federal agency missions. While we value transparency and collaboration, we must balance openness with the responsibilities of operating a secure, compliant, and trusted federal platform.
4 |
5 | ✅ **Who can contribute**
6 | We welcome contributions from:
7 |
8 | - Employees of U.S. federal agencies
9 | - Contractors working under a current agreement with a U.S. government entity
10 | - GSA-approved contributors as part of official interagency collaboration
11 |
12 | ❌ **Who we cannot accept contributions from**
13 | To avoid the appearance of government endorsement, manage supply chain risk, and maintain the integrity of our compliance posture, we do **not** accept unsolicited contributions from:
14 |
15 | - Individuals unaffiliated with the U.S. government
16 | - International contributors or organizations
17 | - Unvetted accounts or first-time contributors submitting minor changes
18 |
19 | If you're unsure whether your contribution fits, feel free to open an issue first so we can discuss it.
20 |
--------------------------------------------------------------------------------
/healthchecks/healthchecks.go:
--------------------------------------------------------------------------------
1 | package healthchecks
2 |
3 | import (
4 | "fmt"
5 | "github.com/cloud-gov/cf-cdn-service-broker/config"
6 | "net/http"
7 | )
8 |
9 | var checks = map[string]func(config.Settings) error{
10 | "letsencrypt": LetsEncrypt,
11 | "s3": S3,
12 | "cloudfront": Cloudfront,
13 | "cloudfoundry": Cloudfoundry,
14 | "postgresql": Postgresql,
15 | }
16 |
17 | func Bind(mux *http.ServeMux, settings config.Settings) {
18 | mux.HandleFunc("/healthcheck", func(w http.ResponseWriter, r *http.Request) {
19 | body := ""
20 | for name, function := range checks {
21 | err := function(settings)
22 | if err != nil {
23 | body = body + fmt.Sprintf("%s error: %s\n", name, err)
24 | }
25 | }
26 | if body != "" {
27 | w.WriteHeader(http.StatusInternalServerError)
28 | fmt.Fprintf(w, "%s", body)
29 | } else {
30 | w.WriteHeader(http.StatusOK)
31 | }
32 | })
33 |
34 | mux.HandleFunc("/healthcheck/http", func(w http.ResponseWriter, r *http.Request) {
35 | w.WriteHeader(http.StatusOK)
36 | })
37 |
38 | for name, function := range checks {
39 | mux.HandleFunc("/healthcheck/"+name, func(w http.ResponseWriter, r *http.Request) {
40 | err := function(settings)
41 | if err != nil {
42 | w.WriteHeader(http.StatusInternalServerError)
43 | fmt.Fprintf(w, "%s error: %s", name, err)
44 | } else {
45 | w.WriteHeader(http.StatusOK)
46 | }
47 | })
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | As a work of the United States Government, this project is in the
2 | public domain within the United States.
3 |
4 | Additionally, we waive copyright and related rights in the work
5 | worldwide through the CC0 1.0 Universal public domain dedication.
6 |
7 | ## CC0 1.0 Universal Summary
8 |
9 | This is a human-readable summary of the [Legal Code (read the full text)](https://creativecommons.org/publicdomain/zero/1.0/legalcode).
10 |
11 | ### No Copyright
12 |
13 | The person who associated a work with this deed has dedicated the work to
14 | the public domain by waiving all of his or her rights to the work worldwide
15 | under copyright law, including all related and neighboring rights, to the
16 | extent allowed by law.
17 |
18 | You can copy, modify, distribute and perform the work, even for commercial
19 | purposes, all without asking permission.
20 |
21 | ### Other Information
22 |
23 | In no way are the patent or trademark rights of any person affected by CC0,
24 | nor are the rights that other persons may have in the work or in how the
25 | work is used, such as publicity or privacy rights.
26 |
27 | Unless expressly stated otherwise, the person who associated a work with
28 | this deed makes no warranties about the work, and disclaims liability for
29 | all uses of the work, to the fullest extent permitted by applicable law.
30 | When using or citing the work, you should not imply endorsement by the
31 | author or the affirmer.
32 |
--------------------------------------------------------------------------------
/cf/mocks/Client.go:
--------------------------------------------------------------------------------
1 | package mocks
2 |
3 | import cf "github.com/cloud-gov/cf-cdn-service-broker/cf"
4 | import cfclient "github.com/cloudfoundry-community/go-cfclient"
5 | import mock "github.com/stretchr/testify/mock"
6 |
7 | // Client is an autogenerated mock type for the Client type
8 | type Client struct {
9 | mock.Mock
10 | }
11 |
12 | // GetDomainByName provides a mock function with given fields: name
13 | func (_m *Client) GetDomainByName(name string) (cfclient.Domain, error) {
14 | ret := _m.Called(name)
15 |
16 | var r0 cfclient.Domain
17 | if rf, ok := ret.Get(0).(func(string) cfclient.Domain); ok {
18 | r0 = rf(name)
19 | } else {
20 | r0 = ret.Get(0).(cfclient.Domain)
21 | }
22 |
23 | var r1 error
24 | if rf, ok := ret.Get(1).(func(string) error); ok {
25 | r1 = rf(name)
26 | } else {
27 | r1 = ret.Error(1)
28 | }
29 |
30 | return r0, r1
31 | }
32 |
33 | // GetOrgByGuid provides a mock function with given fields: guid
34 | func (_m *Client) GetOrgByGuid(guid string) (cfclient.Org, error) {
35 | ret := _m.Called(guid)
36 |
37 | var r0 cfclient.Org
38 | if rf, ok := ret.Get(0).(func(string) cfclient.Org); ok {
39 | r0 = rf(guid)
40 | } else {
41 | r0 = ret.Get(0).(cfclient.Org)
42 | }
43 |
44 | var r1 error
45 | if rf, ok := ret.Get(1).(func(string) error); ok {
46 | r1 = rf(guid)
47 | } else {
48 | r1 = ret.Error(1)
49 | }
50 |
51 | return r0, r1
52 | }
53 |
54 | var _ cf.Client = (*Client)(nil)
55 |
--------------------------------------------------------------------------------
/ci/credentials.example.yml:
--------------------------------------------------------------------------------
1 | cf-cdn-broker-git-url: https://github.com/cloud-gov/cf-cdn-service-broker.git
2 | cf-cdn-broker-git-branch: master
3 |
4 | pipeline-tasks-git-url: https://github.com/cloud-gov/cg-pipeline-tasks
5 | pipeline-tasks-git-branch: master
6 |
7 | cdn-broker-user-staging: user
8 | cdn-broker-pass-staging: pass
9 | cdn-broker-email-staging:
10 | cdn-broker-acme-url-staging: https://acme-staging.api.letsencrypt.org/directory
11 | cdn-broker-bucket-staging:
12 | cdn-broker-iam-path-prefix-staging:
13 | cdn-broker-access-key-id-staging:
14 | cdn-broker-secret-access-key-staging:
15 | cdn-broker-region-staging:
16 | cdn-broker-client-id-staging:
17 | cdn-broker-client-secret-staging:
18 | cdn-broker-default-origin-staging:
19 |
20 | cdn-broker-user-production: user
21 | cdn-broker-pass-production: pass
22 | cdn-broker-email-production:
23 | cdn-broker-acme-url-production: https://acme-v01.api.letsencrypt.org/directory
24 | cdn-broker-bucket-production:
25 | cdn-broker-iam-path-prefix-production:
26 | cdn-broker-access-key-id-production:
27 | cdn-broker-secret-access-key-production:
28 | cdn-broker-region-production:
29 | cdn-broker-client-id-production:
30 | cdn-broker-client-secret-production:
31 | cdn-broker-default-origin-production:
32 |
33 | hosted-zone-id-staging:
34 | domain-url-staging:
35 |
36 | hosted-zone-id-production:
37 | domain-url-production:
38 |
39 | cdn-timeout:
40 |
41 | cf-api-url-staging: https://api.cloud.gov
42 | cf-deploy-username-staging:
43 | cf-deploy-password-staging:
44 | cf-organization-staging:
45 | cf-space-staging:
46 |
47 | cf-api-url-production: https://api.cloud.gov
48 | cf-deploy-username-production:
49 | cf-deploy-password-production:
50 | cf-organization-production:
51 | cf-space-production:
52 |
53 | slack-channel:
54 | slack-username:
55 | slack-webhook-url:
56 | slack-icon-url:
57 |
--------------------------------------------------------------------------------
/cmd/cdn-cron/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "math/rand"
5 | "os"
6 | "os/signal"
7 | "time"
8 |
9 | "code.cloudfoundry.org/lager"
10 | "github.com/robfig/cron"
11 |
12 | "github.com/aws/aws-sdk-go/aws"
13 | "github.com/aws/aws-sdk-go/aws/session"
14 | "github.com/aws/aws-sdk-go/service/cloudfront"
15 | "github.com/aws/aws-sdk-go/service/iam"
16 |
17 | "github.com/cloud-gov/cf-cdn-service-broker/config"
18 | "github.com/cloud-gov/cf-cdn-service-broker/models"
19 | "github.com/cloud-gov/cf-cdn-service-broker/utils"
20 | )
21 |
22 | func main() {
23 | logger := lager.NewLogger("cdn-cron")
24 | logger.RegisterSink(lager.NewWriterSink(os.Stderr, lager.INFO))
25 |
26 | rand.Seed(time.Now().UnixNano())
27 |
28 | settings, err := config.NewSettings()
29 | if err != nil {
30 | logger.Fatal("new-settings", err)
31 | }
32 |
33 | session := session.New(aws.NewConfig().WithRegion(settings.AwsDefaultRegion))
34 |
35 | db, err := config.Connect(settings)
36 | if err != nil {
37 | logger.Fatal("connect", err)
38 | }
39 |
40 | if err := db.AutoMigrate(&models.Route{}, &models.Certificate{}, &models.UserData{}).Error; err != nil {
41 | logger.Fatal("migrate", err)
42 | }
43 |
44 | manager := models.NewManager(
45 | logger,
46 | &utils.Iam{settings, iam.New(session)},
47 | &utils.Distribution{settings, cloudfront.New(session)},
48 | settings,
49 | db,
50 | )
51 |
52 | c := cron.New()
53 |
54 | c.AddFunc(settings.Schedule, func() {
55 | logger.Info("Running renew")
56 | manager.RenewAll()
57 | })
58 |
59 | c.AddFunc(settings.Schedule, func() {
60 | logger.Info("Running cert cleanup")
61 | manager.DeleteOrphanedCerts()
62 | })
63 |
64 | logger.Info("Starting cron")
65 | c.Start()
66 |
67 | waitForExit()
68 | }
69 |
70 | func waitForExit() os.Signal {
71 | c := make(chan os.Signal, 1)
72 | signal.Notify(c, os.Interrupt, os.Kill)
73 | return <-c
74 | }
75 |
--------------------------------------------------------------------------------
/utils/iam.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/aws/aws-sdk-go/aws"
7 | "github.com/aws/aws-sdk-go/service/iam"
8 |
9 | "github.com/xenolf/lego/acme"
10 |
11 | "github.com/cloud-gov/cf-cdn-service-broker/config"
12 | )
13 |
14 | type IamIface interface {
15 | UploadCertificate(name string, cert acme.CertificateResource) (string, error)
16 | DeleteCertificate(name string) error
17 | ListCertificates(callback func(iam.ServerCertificateMetadata) bool) error
18 | }
19 |
20 | type Iam struct {
21 | Settings config.Settings
22 | Service *iam.IAM
23 | }
24 |
25 | func (i *Iam) UploadCertificate(name string, cert acme.CertificateResource) (string, error) {
26 | resp, err := i.Service.UploadServerCertificate(&iam.UploadServerCertificateInput{
27 | CertificateBody: aws.String(string(cert.Certificate)),
28 | PrivateKey: aws.String(string(cert.PrivateKey)),
29 | ServerCertificateName: aws.String(name),
30 | Path: aws.String(fmt.Sprintf("/cloudfront/%s/", i.Settings.IamPathPrefix)),
31 | })
32 | if err != nil {
33 | return "", err
34 | }
35 |
36 | return *resp.ServerCertificateMetadata.ServerCertificateId, nil
37 | }
38 |
39 | func (i *Iam) ListCertificates(callback func(iam.ServerCertificateMetadata) bool) error {
40 | return i.Service.ListServerCertificatesPages(
41 | &iam.ListServerCertificatesInput{
42 | PathPrefix: aws.String(fmt.Sprintf("/cloudfront/%s/", i.Settings.IamPathPrefix)),
43 | },
44 | func(page *iam.ListServerCertificatesOutput, lastPage bool) bool {
45 | for _, v := range page.ServerCertificateMetadataList {
46 | // stop iteration if the callback tells us to
47 | if callback(*v) == false {
48 | return false
49 | }
50 | }
51 |
52 | return true
53 | },
54 | )
55 | }
56 |
57 | func (i *Iam) DeleteCertificate(name string) error {
58 | _, err := i.Service.DeleteServerCertificate(&iam.DeleteServerCertificateInput{
59 | ServerCertificateName: aws.String(name),
60 | })
61 |
62 | return err
63 | }
64 |
--------------------------------------------------------------------------------
/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "github.com/kelseyhightower/envconfig"
5 |
6 | "github.com/jinzhu/gorm"
7 | _ "github.com/jinzhu/gorm/dialects/postgres"
8 | _ "github.com/lib/pq"
9 | )
10 |
11 | type Settings struct {
12 | Port string `envconfig:"port" default:"3000"`
13 | BrokerUsername string `envconfig:"broker_username" required:"true"`
14 | BrokerPassword string `envconfig:"broker_password" required:"true"`
15 | DatabaseUrl string `envconfig:"database_url" required:"true"`
16 | Email string `envconfig:"email" required:"true"`
17 | AcmeUrl string `envconfig:"acme_url" required:"true"`
18 | Bucket string `envconfig:"bucket" required:"true"`
19 | IamPathPrefix string `envconfig:"iam_path_prefix" default:"letsencrypt"`
20 | CloudFrontPrefix string `envconfig:"cloudfront_prefix" default:""`
21 | AwsAccessKeyId string `envconfig:"aws_access_key_id" required:"true"`
22 | AwsSecretAccessKey string `envconfig:"aws_secret_access_key" required:"true"`
23 | AwsDefaultRegion string `envconfig:"aws_default_region" required:"true"`
24 | ServerSideEncryption string `envconfig:"server_side_encryption"`
25 | APIAddress string `envconfig:"api_address" required:"true"`
26 | ClientID string `envconfig:"client_id" required:"true"`
27 | ClientSecret string `envconfig:"client_secret" required:"true"`
28 | DefaultOrigin string `envconfig:"default_origin" required:"true"`
29 | Schedule string `envconfig:"schedule" default:"0 0 * * * *"`
30 | UserIdPool []string `envconfig:"user_id_pool" required:"true"`
31 | }
32 |
33 | func NewSettings() (Settings, error) {
34 | var settings Settings
35 | err := envconfig.Process("cdn", &settings)
36 | if err != nil {
37 | return Settings{}, err
38 | }
39 | return settings, nil
40 | }
41 |
42 | func Connect(settings Settings) (*gorm.DB, error) {
43 | return gorm.Open("postgres", settings.DatabaseUrl)
44 | }
45 |
--------------------------------------------------------------------------------
/cmd/cdn-broker/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "math/rand"
6 | "net/http"
7 | "os"
8 | "time"
9 |
10 | "code.cloudfoundry.org/lager"
11 | "github.com/cloudfoundry-community/go-cfclient"
12 | "github.com/pivotal-cf/brokerapi"
13 |
14 | "github.com/aws/aws-sdk-go/aws"
15 | "github.com/aws/aws-sdk-go/aws/session"
16 | "github.com/aws/aws-sdk-go/service/cloudfront"
17 | "github.com/aws/aws-sdk-go/service/iam"
18 |
19 | "github.com/cloud-gov/cf-cdn-service-broker/broker"
20 | "github.com/cloud-gov/cf-cdn-service-broker/config"
21 | "github.com/cloud-gov/cf-cdn-service-broker/healthchecks"
22 | "github.com/cloud-gov/cf-cdn-service-broker/models"
23 | "github.com/cloud-gov/cf-cdn-service-broker/utils"
24 | )
25 |
26 | func main() {
27 | logger := lager.NewLogger("cdn-service-broker")
28 | logger.RegisterSink(lager.NewWriterSink(os.Stderr, lager.INFO))
29 |
30 | rand.Seed(time.Now().UnixNano())
31 |
32 | settings, err := config.NewSettings()
33 | if err != nil {
34 | logger.Fatal("new-settings", err)
35 | }
36 |
37 | db, err := config.Connect(settings)
38 | if err != nil {
39 | logger.Fatal("connect", err)
40 | }
41 |
42 | cfClient, err := cfclient.NewClient(&cfclient.Config{
43 | ApiAddress: settings.APIAddress,
44 | ClientID: settings.ClientID,
45 | ClientSecret: settings.ClientSecret,
46 | })
47 | if err != nil {
48 | logger.Fatal("cf-client", err)
49 | }
50 |
51 | session := session.New(aws.NewConfig().WithRegion(settings.AwsDefaultRegion))
52 |
53 | if err := db.AutoMigrate(&models.Route{}, &models.Certificate{}, &models.UserData{}).Error; err != nil {
54 | logger.Fatal("migrate", err)
55 | }
56 |
57 | manager := models.NewManager(
58 | logger,
59 | &utils.Iam{settings, iam.New(session)},
60 | &utils.Distribution{settings, cloudfront.New(session)},
61 | settings,
62 | db,
63 | )
64 | broker := broker.New(
65 | &manager,
66 | cfClient,
67 | settings,
68 | logger,
69 | )
70 | credentials := brokerapi.BrokerCredentials{
71 | Username: settings.BrokerUsername,
72 | Password: settings.BrokerPassword,
73 | }
74 |
75 | brokerAPI := brokerapi.New(broker, logger, credentials)
76 | server := bindHTTPHandlers(brokerAPI, settings)
77 | http.ListenAndServe(fmt.Sprintf(":%s", settings.Port), server)
78 | }
79 |
80 | func bindHTTPHandlers(handler http.Handler, settings config.Settings) http.Handler {
81 | mux := http.NewServeMux()
82 | mux.Handle("/", handler)
83 | healthchecks.Bind(mux, settings)
84 |
85 | return mux
86 | }
87 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/cloud-gov/cf-cdn-service-broker
2 |
3 | go 1.25
4 |
5 | require (
6 | code.cloudfoundry.org/lager v1.0.1-0.20180322215153-25ee72f227fe
7 | github.com/aws/aws-sdk-go v1.34.0
8 | github.com/cloudfoundry-community/go-cfclient v0.0.0-20180323021324-b5f0f59f96d6
9 | github.com/jinzhu/gorm v1.9.1
10 | github.com/kelseyhightower/envconfig v1.3.0
11 | github.com/lib/pq v0.0.0-20180325232643-a96442e255fc
12 | github.com/pivotal-cf/brokerapi v1.0.0
13 | github.com/robfig/cron v1.0.0
14 | github.com/stretchr/testify v1.7.0
15 | github.com/xenolf/lego v0.0.0-00010101000000-000000000000
16 | )
17 |
18 | require (
19 | code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f // indirect
20 | github.com/cloudfoundry/gofileutils v0.0.0-20170111115228-4d0c80011a0f // indirect
21 | github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 // indirect
22 | github.com/davecgh/go-spew v1.1.1 // indirect
23 | github.com/denisenkom/go-mssqldb v0.12.3 // indirect
24 | github.com/drewolson/testflight v1.0.0 // indirect
25 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 // indirect
26 | github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab // indirect
27 | github.com/go-sql-driver/mysql v1.7.1 // indirect
28 | github.com/golang/protobuf v1.5.3 // indirect
29 | github.com/google/go-cmp v0.5.9 // indirect
30 | github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f // indirect
31 | github.com/gorilla/mux v1.6.1 // indirect
32 | github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a // indirect
33 | github.com/jinzhu/now v1.1.5 // indirect
34 | github.com/jmespath/go-jmespath v0.3.0 // indirect
35 | github.com/martini-contrib/render v0.0.0-20150707142108-ec18f8345a11 // indirect
36 | github.com/mattn/go-sqlite3 v1.14.16 // indirect
37 | github.com/miekg/dns v1.0.4 // indirect
38 | github.com/onsi/ginkgo v1.16.5 // indirect
39 | github.com/onsi/gomega v1.27.7 // indirect
40 | github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
41 | github.com/pborman/uuid v1.2.1 // indirect
42 | github.com/pkg/errors v0.9.1 // indirect
43 | github.com/pmezard/go-difflib v1.0.0 // indirect
44 | github.com/smartystreets/goconvey v1.8.0 // indirect
45 | github.com/stretchr/objx v0.1.0 // indirect
46 | golang.org/x/crypto v0.31.0 // indirect
47 | golang.org/x/net v0.23.0 // indirect
48 | golang.org/x/oauth2 v0.0.0-20180314180239-fdc9e635145a // indirect
49 | golang.org/x/sys v0.28.0 // indirect
50 | google.golang.org/appengine v1.0.0 // indirect
51 | google.golang.org/protobuf v1.33.0 // indirect
52 | gopkg.in/square/go-jose.v1 v1.1.1 // indirect
53 | gopkg.in/yaml.v3 v3.0.1 // indirect
54 | )
55 |
56 | replace github.com/xenolf/lego => github.com/jmcarp/lego v0.3.2-0.20170424160445-b4deb96f1082
57 |
--------------------------------------------------------------------------------
/utils/certs.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "crypto"
5 | "crypto/tls"
6 | "fmt"
7 | "io/ioutil"
8 | "net"
9 | "net/http"
10 | "path"
11 | "strings"
12 | "time"
13 |
14 | "github.com/aws/aws-sdk-go/aws"
15 | "github.com/aws/aws-sdk-go/service/s3"
16 | "github.com/xenolf/lego/acme"
17 |
18 | "github.com/cloud-gov/cf-cdn-service-broker/config"
19 | )
20 |
21 | func preCheckDNS(fqdn, value string) (bool, error) {
22 | record, err := net.LookupTXT(fqdn)
23 | if err != nil {
24 | return false, err
25 | }
26 | if len(record) == 1 && record[0] == value {
27 | return true, nil
28 | }
29 | return false, fmt.Errorf("DNS precheck failed on name %s, value %s", fqdn, value)
30 | }
31 |
32 | func init() {
33 | acme.PreCheckDNS = preCheckDNS
34 | }
35 |
36 | type User struct {
37 | Email string
38 | Registration *acme.RegistrationResource
39 | key crypto.PrivateKey
40 | }
41 |
42 | func (u *User) GetEmail() string {
43 | return u.Email
44 | }
45 |
46 | func (u *User) GetRegistration() *acme.RegistrationResource {
47 | return u.Registration
48 | }
49 |
50 | func (u *User) GetPrivateKey() crypto.PrivateKey {
51 | return u.key
52 | }
53 |
54 | func (u *User) SetPrivateKey(key crypto.PrivateKey) {
55 | u.key = key
56 | }
57 |
58 | type HTTPProvider struct {
59 | Settings config.Settings
60 | Service *s3.S3
61 | }
62 |
63 | func (p *HTTPProvider) Present(domain, token, keyAuth string) error {
64 | input := s3.PutObjectInput{
65 | Bucket: aws.String(p.Settings.Bucket),
66 | Key: aws.String(path.Join(".well-known", "acme-challenge", token)),
67 | Body: strings.NewReader(keyAuth),
68 | }
69 | if p.Settings.ServerSideEncryption != "" {
70 | input.ServerSideEncryption = aws.String(p.Settings.ServerSideEncryption)
71 | }
72 | if _, err := p.Service.PutObject(&input); err != nil {
73 | return err
74 | }
75 |
76 | insecureClient := &http.Client{
77 | Transport: &http.Transport{
78 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
79 | },
80 | }
81 |
82 | return acme.WaitFor(10*time.Second, 2*time.Second, func() (bool, error) {
83 | resp, err := insecureClient.Get("https://" + path.Join(domain, ".well-known", "acme-challenge", token))
84 | if err != nil {
85 | return false, err
86 | }
87 | defer resp.Body.Close()
88 | body, err := ioutil.ReadAll(resp.Body)
89 | if err != nil {
90 | return false, err
91 | }
92 | if string(body) == keyAuth {
93 | return true, nil
94 | }
95 | return false, fmt.Errorf("HTTP-01 token mismatch for %s: expected %s, got %s", token, keyAuth, string(body))
96 | })
97 | }
98 |
99 | func (p *HTTPProvider) CleanUp(domain, token, keyAuth string) error {
100 | _, err := p.Service.DeleteObject(&s3.DeleteObjectInput{
101 | Bucket: aws.String(p.Settings.Bucket),
102 | Key: aws.String(path.Join(".well-known", "acme-challenge", token)),
103 | })
104 | return err
105 | }
106 |
107 | type DNSProvider struct{}
108 |
109 | func (p *DNSProvider) Present(domain, token, keyAuth string) error {
110 | return nil
111 | }
112 |
113 | func (p *DNSProvider) CleanUp(domain, token, keyAuth string) error {
114 | return nil
115 | }
116 |
117 | func (p *DNSProvider) Timeout() (time.Duration, time.Duration) {
118 | return 10 * time.Second, 2 * time.Second
119 | }
120 |
121 | func NewClient(settings config.Settings, user *User, s3Service *s3.S3, excludes []acme.Challenge) (*acme.Client, error) {
122 | client, err := acme.NewClient(settings.AcmeUrl, user, acme.RSA2048)
123 | if err != nil {
124 | return &acme.Client{}, err
125 | }
126 |
127 | if user.GetRegistration() == nil {
128 | reg, err := client.Register()
129 | if err != nil {
130 | return client, err
131 | }
132 | user.Registration = reg
133 | }
134 |
135 | if err := client.AgreeToTOS(); err != nil {
136 | return client, err
137 | }
138 |
139 | client.SetChallengeProvider(acme.HTTP01, &HTTPProvider{
140 | Settings: settings,
141 | Service: s3Service,
142 | })
143 | client.SetChallengeProvider(acme.DNS01, &DNSProvider{})
144 | client.ExcludeChallenges(excludes)
145 |
146 | return client, nil
147 | }
148 |
--------------------------------------------------------------------------------
/broker/broker_last_operation_test.go:
--------------------------------------------------------------------------------
1 | package broker_test
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "strings"
7 | "testing"
8 |
9 | "github.com/stretchr/testify/suite"
10 |
11 | "code.cloudfoundry.org/lager"
12 | "github.com/pivotal-cf/brokerapi"
13 |
14 | "github.com/cloud-gov/cf-cdn-service-broker/broker"
15 | cfmock "github.com/cloud-gov/cf-cdn-service-broker/cf/mocks"
16 | "github.com/cloud-gov/cf-cdn-service-broker/config"
17 | "github.com/cloud-gov/cf-cdn-service-broker/models"
18 | "github.com/cloud-gov/cf-cdn-service-broker/models/mocks"
19 | )
20 |
21 | func TestLastOperation(t *testing.T) {
22 | suite.Run(t, new(LastOperationSuite))
23 | }
24 |
25 | type LastOperationSuite struct {
26 | suite.Suite
27 | Manager mocks.RouteManagerIface
28 | Broker *broker.CdnServiceBroker
29 | cfclient cfmock.Client
30 | settings config.Settings
31 | logger lager.Logger
32 | ctx context.Context
33 | }
34 |
35 | func (s *LastOperationSuite) SetupTest() {
36 | s.Manager = mocks.RouteManagerIface{}
37 | s.cfclient = cfmock.Client{}
38 | s.Broker = broker.New(
39 | &s.Manager,
40 | &s.cfclient,
41 | s.settings,
42 | s.logger,
43 | )
44 | s.ctx = context.Background()
45 | }
46 |
47 | func (s *LastOperationSuite) TestLastOperationMissing() {
48 | manager := mocks.RouteManagerIface{}
49 | manager.On("Get", "").Return(&models.Route{}, errors.New("not found"))
50 | b := broker.New(
51 | &manager,
52 | &s.cfclient,
53 | s.settings,
54 | s.logger,
55 | )
56 |
57 | operation, err := b.LastOperation(s.ctx, "", "")
58 | s.Equal(operation.State, brokerapi.Failed)
59 | s.Equal(operation.Description, "Service instance not found")
60 | s.Nil(err)
61 | }
62 |
63 | func (s *LastOperationSuite) TestLastOperationSucceeded() {
64 | manager := mocks.RouteManagerIface{}
65 | route := &models.Route{
66 | State: models.Provisioned,
67 | DomainExternal: "cdn.cloud.gov",
68 | DomainInternal: "abc.cloudfront.net",
69 | Origin: "cdn.apps.cloud.gov",
70 | }
71 | manager.On("Get", "123").Return(route, nil)
72 | manager.On("Poll", route).Return(nil)
73 | b := broker.New(
74 | &manager,
75 | &s.cfclient,
76 | s.settings,
77 | s.logger,
78 | )
79 |
80 | operation, err := b.LastOperation(s.ctx, "123", "")
81 | s.Equal(operation.State, brokerapi.Succeeded)
82 | s.Equal(operation.Description, "Service instance provisioned [cdn.cloud.gov => cdn.apps.cloud.gov]; CDN domain abc.cloudfront.net")
83 | s.Nil(err)
84 | }
85 |
86 | func (s *LastOperationSuite) TestLastOperationProvisioning() {
87 | manager := mocks.RouteManagerIface{}
88 | route := &models.Route{
89 | State: models.Provisioning,
90 | DomainExternal: "cdn.cloud.gov",
91 | Origin: "cdn.apps.cloud.gov",
92 | ChallengeJSON: []byte("[]"),
93 | }
94 | manager.On("Get", "123").Return(route, nil)
95 | manager.On("GetDNSInstructions", route).Return([]string{"token"}, nil)
96 | manager.On("Poll", route).Return(nil)
97 | b := broker.New(
98 | &manager,
99 | &s.cfclient,
100 | s.settings,
101 | s.logger,
102 | )
103 |
104 | operation, err := b.LastOperation(s.ctx, "123", "")
105 | s.Equal(operation.State, brokerapi.InProgress)
106 | s.True(strings.Contains(operation.Description, "Provisioning in progress [cdn.cloud.gov => cdn.apps.cloud.gov]"))
107 | s.Nil(err)
108 | }
109 |
110 | func (s *LastOperationSuite) TestLastOperationDeprovisioning() {
111 | manager := mocks.RouteManagerIface{}
112 | route := &models.Route{
113 | State: models.Deprovisioning,
114 | DomainExternal: "cdn.cloud.gov",
115 | DomainInternal: "abc.cloudfront.net",
116 | Origin: "cdn.apps.cloud.gov",
117 | }
118 | manager.On("Get", "123").Return(route, nil)
119 | manager.On("Poll", route).Return(nil)
120 | b := broker.New(
121 | &manager,
122 | &s.cfclient,
123 | s.settings,
124 | s.logger,
125 | )
126 |
127 | operation, err := b.LastOperation(s.ctx, "123", "")
128 | s.Equal(operation.State, brokerapi.InProgress)
129 | s.Equal(operation.Description, "Deprovisioning in progress [cdn.cloud.gov => cdn.apps.cloud.gov]; CDN domain abc.cloudfront.net")
130 | s.Nil(err)
131 | }
132 |
--------------------------------------------------------------------------------
/models/mocks/RouteManagerIface.go:
--------------------------------------------------------------------------------
1 | package mocks
2 |
3 | import mock "github.com/stretchr/testify/mock"
4 | import models "github.com/cloud-gov/cf-cdn-service-broker/models"
5 | import utils "github.com/cloud-gov/cf-cdn-service-broker/utils"
6 |
7 | // RouteManagerIface is an autogenerated mock type for the RouteManagerIface type
8 | type RouteManagerIface struct {
9 | mock.Mock
10 | }
11 |
12 | // Create provides a mock function with given fields: instanceId, domain, origin, path, insecureOrigin, forwardedHeaders, forwardCookies, tags
13 | func (_m *RouteManagerIface) Create(instanceId string, domain string, origin string, path string, insecureOrigin bool, forwardedHeaders utils.Headers, forwardCookies bool, tags map[string]string) (*models.Route, error) {
14 | ret := _m.Called(instanceId, domain, origin, path, insecureOrigin, forwardedHeaders, forwardCookies, tags)
15 |
16 | var r0 *models.Route
17 | if rf, ok := ret.Get(0).(func(string, string, string, string, bool, utils.Headers, bool, map[string]string) *models.Route); ok {
18 | r0 = rf(instanceId, domain, origin, path, insecureOrigin, forwardedHeaders, forwardCookies, tags)
19 | } else {
20 | if ret.Get(0) != nil {
21 | r0 = ret.Get(0).(*models.Route)
22 | }
23 | }
24 |
25 | var r1 error
26 | if rf, ok := ret.Get(1).(func(string, string, string, string, bool, utils.Headers, bool, map[string]string) error); ok {
27 | r1 = rf(instanceId, domain, origin, path, insecureOrigin, forwardedHeaders, forwardCookies, tags)
28 | } else {
29 | r1 = ret.Error(1)
30 | }
31 |
32 | return r0, r1
33 | }
34 |
35 | // DeleteOrphanedCerts provides a mock function with given fields:
36 | func (_m *RouteManagerIface) DeleteOrphanedCerts() {
37 | _m.Called()
38 | }
39 |
40 | // Disable provides a mock function with given fields: route
41 | func (_m *RouteManagerIface) Disable(route *models.Route) error {
42 | ret := _m.Called(route)
43 |
44 | var r0 error
45 | if rf, ok := ret.Get(0).(func(*models.Route) error); ok {
46 | r0 = rf(route)
47 | } else {
48 | r0 = ret.Error(0)
49 | }
50 |
51 | return r0
52 | }
53 |
54 | // Get provides a mock function with given fields: instanceId
55 | func (_m *RouteManagerIface) Get(instanceId string) (*models.Route, error) {
56 | ret := _m.Called(instanceId)
57 |
58 | var r0 *models.Route
59 | if rf, ok := ret.Get(0).(func(string) *models.Route); ok {
60 | r0 = rf(instanceId)
61 | } else {
62 | if ret.Get(0) != nil {
63 | r0 = ret.Get(0).(*models.Route)
64 | }
65 | }
66 |
67 | var r1 error
68 | if rf, ok := ret.Get(1).(func(string) error); ok {
69 | r1 = rf(instanceId)
70 | } else {
71 | r1 = ret.Error(1)
72 | }
73 |
74 | return r0, r1
75 | }
76 |
77 | // GetDNSInstructions provides a mock function with given fields: route
78 | func (_m *RouteManagerIface) GetDNSInstructions(route *models.Route) ([]string, error) {
79 | ret := _m.Called(route)
80 |
81 | var r0 []string
82 | if rf, ok := ret.Get(0).(func(*models.Route) []string); ok {
83 | r0 = rf(route)
84 | } else {
85 | if ret.Get(0) != nil {
86 | r0 = ret.Get(0).([]string)
87 | }
88 | }
89 |
90 | var r1 error
91 | if rf, ok := ret.Get(1).(func(*models.Route) error); ok {
92 | r1 = rf(route)
93 | } else {
94 | r1 = ret.Error(1)
95 | }
96 |
97 | return r0, r1
98 | }
99 |
100 | // Poll provides a mock function with given fields: route
101 | func (_m *RouteManagerIface) Poll(route *models.Route) error {
102 | ret := _m.Called(route)
103 |
104 | var r0 error
105 | if rf, ok := ret.Get(0).(func(*models.Route) error); ok {
106 | r0 = rf(route)
107 | } else {
108 | r0 = ret.Error(0)
109 | }
110 |
111 | return r0
112 | }
113 |
114 | // Renew provides a mock function with given fields: route
115 | func (_m *RouteManagerIface) Renew(route *models.Route) error {
116 | ret := _m.Called(route)
117 |
118 | var r0 error
119 | if rf, ok := ret.Get(0).(func(*models.Route) error); ok {
120 | r0 = rf(route)
121 | } else {
122 | r0 = ret.Error(0)
123 | }
124 |
125 | return r0
126 | }
127 |
128 | // RenewAll provides a mock function with given fields:
129 | func (_m *RouteManagerIface) RenewAll() {
130 | _m.Called()
131 | }
132 |
133 | // Update provides a mock function with given fields: instanceId, domain, origin, path, insecureOrigin, forwardedHeaders, forwardCookies
134 | func (_m *RouteManagerIface) Update(instanceId string, domain string, origin string, path string, insecureOrigin bool, forwardedHeaders utils.Headers, forwardCookies bool) error {
135 | ret := _m.Called(instanceId, domain, origin, path, insecureOrigin, forwardedHeaders, forwardCookies)
136 |
137 | var r0 error
138 | if rf, ok := ret.Get(0).(func(string, string, string, string, bool, utils.Headers, bool) error); ok {
139 | r0 = rf(instanceId, domain, origin, path, insecureOrigin, forwardedHeaders, forwardCookies)
140 | } else {
141 | r0 = ret.Error(0)
142 | }
143 |
144 | return r0
145 | }
146 |
147 | var _ models.RouteManagerIface = (*RouteManagerIface)(nil)
148 |
--------------------------------------------------------------------------------
/models/models_test.go:
--------------------------------------------------------------------------------
1 | package models_test
2 |
3 | import (
4 | "os"
5 | "testing"
6 | "time"
7 |
8 | "code.cloudfoundry.org/lager"
9 | "github.com/jinzhu/gorm"
10 | "github.com/xenolf/lego/acme"
11 |
12 | "github.com/aws/aws-sdk-go/aws"
13 | "github.com/aws/aws-sdk-go/aws/request"
14 | "github.com/aws/aws-sdk-go/aws/session"
15 | "github.com/aws/aws-sdk-go/service/cloudfront"
16 | "github.com/aws/aws-sdk-go/service/iam"
17 |
18 | "github.com/cloud-gov/cf-cdn-service-broker/config"
19 | "github.com/cloud-gov/cf-cdn-service-broker/models"
20 | "github.com/cloud-gov/cf-cdn-service-broker/utils"
21 |
22 | "github.com/stretchr/testify/mock"
23 | )
24 |
25 | type MockUtilsIam struct {
26 | mock.Mock
27 |
28 | Settings config.Settings
29 | Service *iam.IAM
30 | }
31 |
32 | // test doesn't execute this method
33 | func (_f MockUtilsIam) UploadCertificate(name string, cert acme.CertificateResource) (string, error) {
34 | return "", nil
35 | }
36 |
37 | // don't mock this method
38 | func (_f MockUtilsIam) ListCertificates(callback func(iam.ServerCertificateMetadata) bool) error {
39 | orig := &utils.Iam{Settings: _f.Settings, Service: _f.Service}
40 | return orig.ListCertificates(callback)
41 | }
42 |
43 | func (_f MockUtilsIam) DeleteCertificate(certName string) error {
44 | args := _f.Called(certName)
45 | return args.Error(0)
46 | }
47 |
48 | func TestDeleteOrphanedCerts(t *testing.T) {
49 | logger := lager.NewLogger("cdn-cron-test")
50 | logger.RegisterSink(lager.NewWriterSink(os.Stderr, lager.INFO))
51 |
52 | settings, _ := config.NewSettings()
53 | session := session.New(nil)
54 |
55 | //mock out the aws call to return a fixed list of certs, two of which should be deleted
56 | fakeiam := iam.New(session)
57 | fakeiam.Handlers.Clear()
58 | fakeiam.Handlers.Send.PushBack(func(r *request.Request) {
59 | //t.Log(r.Operation.Name)
60 | switch r.Operation.Name {
61 | case "ListServerCertificates":
62 | old := time.Now().AddDate(0, 0, -2)
63 | current := time.Now().AddDate(0, 0, 0)
64 |
65 | list := []*iam.ServerCertificateMetadata{
66 | &iam.ServerCertificateMetadata{
67 | Arn: aws.String("an-active-certificate"),
68 | ServerCertificateName: aws.String("an-active-certificate"),
69 | ServerCertificateId: aws.String("an-active-certificate"),
70 | UploadDate: &old,
71 | },
72 | &iam.ServerCertificateMetadata{
73 | Arn: aws.String("some-other-active-certificate"),
74 | ServerCertificateName: aws.String("some-other-active-certificate"),
75 | ServerCertificateId: aws.String("some-other-active-certificate"),
76 | UploadDate: &old,
77 | },
78 | &iam.ServerCertificateMetadata{
79 | Arn: aws.String("orphaned-but-not-old-enough"),
80 | ServerCertificateName: aws.String("orphaned-but-not-old-enough"),
81 | ServerCertificateId: aws.String("this-cert-should-not-be-deleted"),
82 | UploadDate: ¤t,
83 | },
84 | &iam.ServerCertificateMetadata{
85 | Arn: aws.String("some-orphaned-cert"),
86 | ServerCertificateName: aws.String("some-orphaned-cert"),
87 | ServerCertificateId: aws.String("this-cert-should-be-deleted"),
88 | UploadDate: &old,
89 | },
90 | &iam.ServerCertificateMetadata{
91 | Arn: aws.String("some-other-orphaned-cert"),
92 | ServerCertificateName: aws.String("some-other-orphaned-cert"),
93 | ServerCertificateId: aws.String("this-cert-should-also-be-deleted"),
94 | UploadDate: &old,
95 | },
96 | }
97 | data := r.Data.(*iam.ListServerCertificatesOutput)
98 | data.IsTruncated = aws.Bool(false)
99 | data.ServerCertificateMetadataList = list
100 | }
101 | })
102 |
103 | //mock out the aws call to return a fixed list of distributions
104 | fakecf := cloudfront.New(session)
105 | fakecf.Handlers.Clear()
106 | fakecf.Handlers.Send.PushBack(func(r *request.Request) {
107 | switch r.Operation.Name {
108 | case "ListDistributions2020_05_31":
109 | list := []*cloudfront.DistributionSummary{
110 | &cloudfront.DistributionSummary{
111 | ARN: aws.String("some-distribution"),
112 | ViewerCertificate: &cloudfront.ViewerCertificate{
113 | IAMCertificateId: aws.String("an-active-certificate"),
114 | },
115 | },
116 | &cloudfront.DistributionSummary{
117 | ARN: aws.String("some-other-distribution"),
118 | ViewerCertificate: &cloudfront.ViewerCertificate{
119 | IAMCertificateId: aws.String("some-other-active-certificate"),
120 | },
121 | },
122 | }
123 |
124 | data := r.Data.(*cloudfront.ListDistributionsOutput)
125 | data.DistributionList = &cloudfront.DistributionList{
126 | IsTruncated: aws.Bool(false),
127 | Items: list,
128 | }
129 | }
130 | })
131 |
132 | mui := new(MockUtilsIam)
133 | mui.Settings = settings
134 | mui.Service = fakeiam
135 |
136 | // expect the orphaned certs to be deleted
137 | mui.On("DeleteCertificate", "some-orphaned-cert").Return(nil)
138 | mui.On("DeleteCertificate", "some-other-orphaned-cert").Return(nil)
139 |
140 | m := models.NewManager(
141 | logger,
142 | mui,
143 | &utils.Distribution{settings, fakecf},
144 | settings,
145 | &gorm.DB{},
146 | )
147 |
148 | //run the test
149 | m.DeleteOrphanedCerts()
150 |
151 | //check our expectations
152 | mui.AssertExpectations(t)
153 |
154 | }
155 |
--------------------------------------------------------------------------------
/ci/acceptance-tests.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -eux
4 |
5 | # Set defaults
6 | TTL="${TTL:-60}"
7 | CDN_TIMEOUT="${CDN_TIMEOUT:-7200}"
8 |
9 | suffix="${RANDOM}"
10 | DOMAIN=$(printf "${DOMAIN}" "${suffix}")
11 | SERVICE_INSTANCE_NAME=$(printf "${SERVICE_INSTANCE_NAME}" "${suffix}")
12 |
13 | curl_args=""
14 | if [ -n "${CA_CERT:-}" ]; then
15 | echo "${CA_CERT}" > ca.pem
16 | curl_args="--cacert ca.pem"
17 | fi
18 |
19 | path="$(dirname $0)"
20 |
21 | # Authenticate
22 | cf api "${CF_API_URL}"
23 | (set +x; cf auth "${CF_USERNAME}" "${CF_PASSWORD}")
24 |
25 | # Target
26 | cf target -o "${CF_ORGANIZATION}" -s "${CF_SPACE}"
27 |
28 | # Create private domain
29 | cf create-domain "${CF_ORGANIZATION}" "${DOMAIN}"
30 |
31 | # sleep a little to let the domain create
32 | sleep 5
33 |
34 | # Create service
35 | opts=$(jq -n --arg domain "${DOMAIN}" '{domain: $domain}')
36 | cf create-service "${SERVICE_NAME}" "${PLAN_NAME}" "${SERVICE_INSTANCE_NAME}" -c "${opts}"
37 | service_guid=$(cf service "${SERVICE_INSTANCE_NAME}" --guid)
38 |
39 | http_regex="CNAME or ALIAS domain (.*) to (.*) or"
40 | dns_regex="name: (.*), value: (.*), ttl: (.*)"
41 |
42 | elapsed=300
43 | until [ "${elapsed}" -le 0 ]; do
44 | status=$(cf curl "/v3/service_instances/${service_guid}")
45 | description=$(echo "${status}" | jq -r '.last_operation.description')
46 | if [[ "${description}" =~ ${http_regex} ]]; then
47 | domain_external="${BASH_REMATCH[1]}"
48 | domain_internal="${BASH_REMATCH[2]}"
49 | fi
50 | if [[ "${description}" =~ ${dns_regex} ]]; then
51 | txt_name="${BASH_REMATCH[1]}"
52 | txt_value="${BASH_REMATCH[2]}"
53 | txt_ttl="${BASH_REMATCH[3]}"
54 | fi
55 | if [ -n "${domain_external:-}" ] && [ -n "${txt_name:-}" ]; then
56 | break
57 | fi
58 | let elapsed-=5
59 | sleep 5
60 | done
61 | if [ -z "${domain_internal:-}" ] || [ -z "${txt_name:-}" ]; then
62 | echo "Failed to parse description: ${description}"
63 | exit 1
64 | fi
65 |
66 | # Create DNS record(s)
67 | cat << EOF > ./create-cname.json
68 | {
69 | "Changes": [
70 | {
71 | "Action": "CREATE",
72 | "ResourceRecordSet": {
73 | "Name": "${domain_external}.",
74 | "Type": "CNAME",
75 | "TTL": ${TTL},
76 | "ResourceRecords": [
77 | {"Value": "${domain_internal}"}
78 | ]
79 | }
80 | }
81 | ]
82 | }
83 | EOF
84 |
85 | if [ "${CHALLENGE_TYPE}" = "DNS-01" ]; then
86 | cat << EOF > ./create-txt.json
87 | {
88 | "Changes": [
89 | {
90 | "Action": "CREATE",
91 | "ResourceRecordSet": {
92 | "Name": "${txt_name}",
93 | "Type": "TXT",
94 | "TTL": ${txt_ttl},
95 | "ResourceRecords": [
96 | {"Value": "\"${txt_value}\""}
97 | ]
98 | }
99 | }
100 | ]
101 | }
102 | EOF
103 | fi
104 |
105 | if [ "${CHALLENGE_TYPE}" = "HTTP-01" ]; then
106 | aws route53 change-resource-record-sets \
107 | --hosted-zone-id "${HOSTED_ZONE_ID}" \
108 | --change-batch file://./create-cname.json
109 | elif [ "${CHALLENGE_TYPE}" = "DNS-01" ]; then
110 | aws route53 change-resource-record-sets \
111 | --hosted-zone-id "${HOSTED_ZONE_ID}" \
112 | --change-batch file://./create-txt.json
113 | fi
114 |
115 | # Wait for provision to complete
116 | elapsed="${CDN_TIMEOUT}"
117 | until [ "${elapsed}" -le 0 ]; do
118 | status=$(cf curl "/v3/service_instances/${service_guid}")
119 | state=$(echo "${status}" | jq -r '.last_operation.state')
120 | if [[ "${state}" == "succeeded" ]]; then
121 | updated="true"
122 | break
123 | elif [[ "${state}" == "failed" ]]; then
124 | echo "Failed to create service"
125 | exit 1
126 | fi
127 | let elapsed-=60
128 | sleep 60
129 | done
130 | if [ "${updated}" != "true" ]; then
131 | echo "Failed to update service ${SERVICE_NAME}"
132 | exit 1
133 | fi
134 |
135 | # Create CNAME after provisioning if using DNS-01 challenge
136 | if [ "${CHALLENGE_TYPE}" = "DNS-01" ]; then
137 | aws route53 change-resource-record-sets \
138 | --hosted-zone-id "${HOSTED_ZONE_ID}" \
139 | --change-batch file://./create-cname.json
140 | fi
141 |
142 | # Push test app
143 | cat << EOF > "${path}/app/manifest.yml"
144 | ---
145 | applications:
146 | - name: cdn-broker-test-${CHALLENGE_TYPE}
147 | buildpack: staticfile_buildpack
148 | domain: ${DOMAIN}
149 | no-hostname: true
150 | EOF
151 |
152 | cf push -f "${path}/app/manifest.yml" -p "${path}/app"
153 |
154 | # Assert expected response from cdn
155 | elapsed="${CDN_TIMEOUT}"
156 | until [ "${elapsed}" -le 0 ]; do
157 | if curl ${curl_args} "https://${DOMAIN}" | grep "CDN Broker Test"; then
158 | break
159 | fi
160 | let elapsed-=60
161 | sleep 60
162 | done
163 | if [ -z "${elapsed}" ]; then
164 | echo "Failed to load ${DOMAIN}"
165 | exit 1
166 | fi
167 |
168 | # Delete private domain
169 | cf delete-domain -f "${DOMAIN}"
170 |
171 | # Delete DNS record(s)
172 | cat << EOF > ./delete-cname.json
173 | {
174 | "Changes": [
175 | {
176 | "Action": "DELETE",
177 | "ResourceRecordSet": {
178 | "Name": "${domain_external}.",
179 | "Type": "CNAME",
180 | "TTL": ${TTL},
181 | "ResourceRecords": [
182 | {"Value": "${domain_internal}"}
183 | ]
184 | }
185 | }
186 | ]
187 | }
188 | EOF
189 | if [ "${CHALLENGE_TYPE}" = "DNS-01" ]; then
190 | cat << EOF > ./delete-txt.json
191 | {
192 | "Changes": [
193 | {
194 | "Action": "DELETE",
195 | "ResourceRecordSet": {
196 | "Name": "${txt_name}.",
197 | "Type": "TXT",
198 | "TTL": ${txt_ttl},
199 | "ResourceRecords": [
200 | {"Value": "${txt_value}"}
201 | ]
202 | }
203 | }
204 | ]
205 | }
206 | EOF
207 |
208 | aws route53 change-resource-record-sets \
209 | --hosted-zone-id "${HOSTED_ZONE_ID}" \
210 | --change-batch file://./delete-cname.json
211 | elif [ "${CHALLENGE_TYPE}" = "DNS-01" ]; then
212 | aws route53 change-resource-record-sets \
213 | --hosted-zone-id "${HOSTED_ZONE_ID}" \
214 | --change-batch file://./delete-txt.json
215 | fi
216 |
217 | # Delete service
218 | cf delete-service -f "${SERVICE_INSTANCE_NAME}"
219 |
220 | # Wait for deprovision to complete
221 | elapsed="${CDN_TIMEOUT}"
222 | until [ "${elapsed}" -le 0 ]; do
223 | if ! cf service "${SERVICE_INSTANCE_NAME}"; then
224 | deleted="true"
225 | break
226 | fi
227 | let elapsed-=60
228 | sleep 60
229 | done
230 | if [ "${deleted}" != "true" ]; then
231 | echo "Failed to delete service ${SERVICE_NAME}"
232 | exit 1
233 | fi
234 |
--------------------------------------------------------------------------------
/ci/pipeline.yml:
--------------------------------------------------------------------------------
1 | ---
2 | meta:
3 | cf-creds-staging: &cf-creds-staging
4 | CF_API_URL: ((cf-api-url-staging))
5 | CF_USERNAME: ((cf-deploy-username-staging))
6 | CF_PASSWORD: ((cf-deploy-password-staging))
7 | CF_ORGANIZATION: ((cf-organization-staging))
8 | CF_SPACE: ((cf-space-staging))
9 | cf-creds-production: &cf-creds-production
10 | CF_API_URL: ((cf-api-url-production))
11 | CF_USERNAME: ((cf-deploy-username-production))
12 | CF_PASSWORD: ((cf-deploy-password-production))
13 | CF_ORGANIZATION: ((cf-organization-production))
14 | CF_SPACE: ((cf-space-production))
15 |
16 | jobs:
17 | - name: set-self
18 | plan:
19 | - get: broker-src
20 | trigger: true
21 | - set_pipeline: self
22 | file: broker-src/ci/pipeline.yml
23 |
24 | - name: test-cdn-broker
25 | plan:
26 | - get: broker-src
27 | trigger: true
28 | passed: [set-self]
29 | - task: run-tests
30 | file: broker-src/ci/run-tests.yml
31 |
32 | - name: push-cf-cdn-service-broker-staging
33 | plan:
34 | - in_parallel:
35 | - get: broker-src
36 | passed: [test-cdn-broker]
37 | trigger: true
38 | - get: pipeline-tasks
39 | - task: create-db
40 | file: broker-src/ci/create-db.yml
41 | params:
42 | # Note: Name must match service name in manifest
43 | SERVICE_TYPE: aws-rds
44 | SERVICE_NAME: rds-cdn-broker
45 | SERVICE_PLAN: shared-psql
46 | <<: *cf-creds-staging
47 | - in_parallel:
48 | - put: broker-deploy-staging
49 | params:
50 | path: broker-src
51 | manifest: broker-src/manifest-broker.yml
52 | environment_variables: &cfenv-staging
53 | BROKER_USERNAME: ((cdn-broker-user-staging))
54 | BROKER_PASSWORD: ((cdn-broker-pass-staging))
55 | EMAIL: ((cdn-broker-email-staging))
56 | ACME_URL: ((cdn-broker-acme-url-staging))
57 | BUCKET: ((cdn-broker-bucket-staging))
58 | IAM_PATH_PREFIX: ((cdn-broker-iam-path-prefix-staging))
59 | AWS_ACCESS_KEY_ID: ((cdn-broker-access-key-id-staging))
60 | AWS_SECRET_ACCESS_KEY: ((cdn-broker-secret-access-key-staging))
61 | AWS_DEFAULT_REGION: ((cdn-broker-region-staging))
62 | SERVER_SIDE_ENCRYPTION: AES256
63 | API_ADDRESS: ((cf-api-url-staging))
64 | CLIENT_ID: ((cdn-broker-client-id-staging))
65 | CLIENT_SECRET: ((cdn-broker-client-secret-staging))
66 | DEFAULT_ORIGIN: ((cdn-broker-default-origin-staging))
67 | USER_ID_POOL: ((cdn-broker-user-id-pool-staging))
68 | - put: broker-deploy-staging
69 | params:
70 | path: broker-src
71 | manifest: broker-src/manifest-cron.yml
72 | environment_variables: *cfenv-staging
73 | on_failure:
74 | put: slack
75 | params:
76 | text: |
77 | :x: FAILED to deploy cf-cdn-service-broker on "((cf-api-url-staging))"
78 | <$ATC_EXTERNAL_URL/teams/$BUILD_TEAM_NAME/pipelines/$BUILD_PIPELINE_NAME/jobs/$BUILD_JOB_NAME/builds/$BUILD_NAME|View build details>
79 | channel: ((slack-channel))
80 | username: ((slack-username))
81 | icon_url: ((slack-icon-url))
82 |
83 | - name: push-cf-cdn-service-broker-production
84 | plan:
85 | - in_parallel:
86 | - get: broker-src
87 | passed: [push-cf-cdn-service-broker-staging]
88 | trigger: true
89 | - get: pipeline-tasks
90 | - task: create-db
91 | file: broker-src/ci/create-db.yml
92 | params:
93 | # Note: Name must match service name in manifest
94 | SERVICE_TYPE: aws-rds
95 | SERVICE_NAME: rds-cdn-broker
96 | SERVICE_PLAN: shared-psql
97 | <<: *cf-creds-production
98 | - in_parallel:
99 | - put: broker-deploy-production
100 | params:
101 | path: broker-src
102 | manifest: broker-src/manifest-broker.yml
103 | environment_variables: &cfenv-production
104 | BROKER_USERNAME: ((cdn-broker-user-production))
105 | BROKER_PASSWORD: ((cdn-broker-pass-production))
106 | EMAIL: ((cdn-broker-email-production))
107 | ACME_URL: ((cdn-broker-acme-url-production))
108 | BUCKET: ((cdn-broker-bucket-production))
109 | IAM_PATH_PREFIX: ((cdn-broker-iam-path-prefix-production))
110 | AWS_ACCESS_KEY_ID: ((cdn-broker-access-key-id-production))
111 | AWS_SECRET_ACCESS_KEY: ((cdn-broker-secret-access-key-production))
112 | AWS_DEFAULT_REGION: ((cdn-broker-region-production))
113 | SERVER_SIDE_ENCRYPTION: AES256
114 | API_ADDRESS: ((cf-api-url-production))
115 | CLIENT_ID: ((cdn-broker-client-id-production))
116 | CLIENT_SECRET: ((cdn-broker-client-secret-production))
117 | DEFAULT_ORIGIN: ((cdn-broker-default-origin-production))
118 | USER_ID_POOL: ((cdn-broker-user-id-pool-production))
119 | - put: broker-deploy-production
120 | params:
121 | path: broker-src
122 | manifest: broker-src/manifest-cron.yml
123 | environment_variables: *cfenv-production
124 | on_failure:
125 | put: slack
126 | params:
127 | text: |
128 | :x: FAILED to deploy cf-cdn-service-broker on "((cf-api-url-production))"
129 | <$ATC_EXTERNAL_URL/teams/$BUILD_TEAM_NAME/pipelines/$BUILD_PIPELINE_NAME/jobs/$BUILD_JOB_NAME/builds/$BUILD_NAME|View build details>
130 | channel: ((slack-failure-channel))
131 | username: ((slack-username))
132 | icon_url: ((slack-icon-url))
133 |
134 | resources:
135 | - name: broker-src
136 | type: git
137 | source:
138 | uri: https://github.com/cloud-gov/cf-cdn-service-broker.git
139 | branch: main
140 | commit_verification_keys: ((cloud-gov-pgp-keys))
141 |
142 | - name: pipeline-tasks
143 | type: git
144 | source:
145 | uri: https://github.com/cloud-gov/cg-pipeline-tasks
146 | branch: main
147 | commit_verification_keys: ((cloud-gov-pgp-keys))
148 |
149 | - name: broker-deploy-staging
150 | type: cf
151 | source:
152 | api: ((cf-api-url-staging))
153 | username: ((cf-deploy-username-staging))
154 | password: ((cf-deploy-password-staging))
155 | organization: ((cf-organization-staging))
156 | space: ((cf-space-staging))
157 |
158 | - name: broker-deploy-production
159 | type: cf
160 | source:
161 | api: ((cf-api-url-production))
162 | username: ((cf-deploy-username-production))
163 | password: ((cf-deploy-password-production))
164 | organization: ((cf-organization-production))
165 | space: ((cf-space-production))
166 |
167 | - name: slack
168 | type: slack-notification
169 | source:
170 | url: ((slack-webhook-url))
171 |
172 | resource_types:
173 | - name: slack-notification
174 | type: registry-image
175 | source:
176 | aws_access_key_id: ((ecr_aws_key))
177 | aws_secret_access_key: ((ecr_aws_secret))
178 | repository: slack-notification-resource
179 | aws_region: us-gov-west-1
180 | tag: latest
181 |
182 |
183 | - name: git
184 | type: registry-image
185 | source:
186 | aws_access_key_id: ((ecr_aws_key))
187 | aws_secret_access_key: ((ecr_aws_secret))
188 | repository: git-resource
189 | aws_region: us-gov-west-1
190 | tag: latest
191 |
192 | - name: cf
193 | type: registry-image
194 | source:
195 | aws_access_key_id: ((ecr_aws_key))
196 | aws_secret_access_key: ((ecr_aws_secret))
197 | repository: cf-resource
198 | aws_region: us-gov-west-1
199 | tag: latest
200 |
201 | - name: registry-image
202 | type: registry-image
203 | source:
204 | aws_access_key_id: ((ecr_aws_key))
205 | aws_secret_access_key: ((ecr_aws_secret))
206 | repository: registry-image-resource
207 | aws_region: us-gov-west-1
208 | tag: latest
209 |
--------------------------------------------------------------------------------
/broker/broker_update_test.go:
--------------------------------------------------------------------------------
1 | package broker_test
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "errors"
7 | "testing"
8 |
9 | "github.com/stretchr/testify/suite"
10 |
11 | "code.cloudfoundry.org/lager"
12 | "github.com/cloudfoundry-community/go-cfclient"
13 | "github.com/pivotal-cf/brokerapi"
14 |
15 | "github.com/cloud-gov/cf-cdn-service-broker/broker"
16 | cfmock "github.com/cloud-gov/cf-cdn-service-broker/cf/mocks"
17 | "github.com/cloud-gov/cf-cdn-service-broker/config"
18 | "github.com/cloud-gov/cf-cdn-service-broker/models/mocks"
19 | "github.com/cloud-gov/cf-cdn-service-broker/utils"
20 | )
21 |
22 | func TestUpdating(t *testing.T) {
23 | suite.Run(t, new(UpdateSuite))
24 | }
25 |
26 | type UpdateSuite struct {
27 | suite.Suite
28 | Manager mocks.RouteManagerIface
29 | Broker *broker.CdnServiceBroker
30 | cfclient cfmock.Client
31 | settings config.Settings
32 | logger lager.Logger
33 | ctx context.Context
34 | }
35 |
36 | func (s *UpdateSuite) SetupTest() {
37 | s.Manager = mocks.RouteManagerIface{}
38 | s.cfclient = cfmock.Client{}
39 | s.logger = lager.NewLogger("broker.provision.test")
40 | s.settings = config.Settings{
41 | DefaultOrigin: "origin.cloud.gov",
42 | }
43 | s.Broker = broker.New(
44 | &s.Manager,
45 | &s.cfclient,
46 | s.settings,
47 | s.logger,
48 | )
49 | s.ctx = context.Background()
50 | }
51 |
52 | func (s *UpdateSuite) TestUpdateWithoutOptions() {
53 | details := brokerapi.UpdateDetails{
54 | RawParameters: json.RawMessage(`{"origin": ""}`),
55 | }
56 | _, err := s.Broker.Update(s.ctx, "", details, true)
57 | s.NotNil(err)
58 | s.Equal(err.Error(), "must pass non-empty `domain` or `origin`")
59 | }
60 |
61 | func (s *UpdateSuite) TestUpdateSuccessOnlyDomain() {
62 | details := brokerapi.UpdateDetails{
63 | RawParameters: json.RawMessage(`{"domain": "domain.gov"}`),
64 | }
65 | s.Manager.On("Update", "", "domain.gov", "origin.cloud.gov", "", false, utils.Headers{"Host": true}, true).Return(nil)
66 | s.cfclient.On("GetDomainByName", "domain.gov").Return(cfclient.Domain{}, nil)
67 | _, err := s.Broker.Update(s.ctx, "", details, true)
68 | s.Nil(err)
69 | }
70 |
71 | func (s *UpdateSuite) TestUpdateSuccessOnlyOrigin() {
72 | details := brokerapi.UpdateDetails{
73 | RawParameters: json.RawMessage(`{"origin": "origin.gov"}`),
74 | }
75 | s.Manager.On("Update", "", "", "origin.gov", "", false, utils.Headers{}, true).Return(nil)
76 | s.cfclient.On("GetDomainByName", "domain.gov").Return(cfclient.Domain{}, nil)
77 | _, err := s.Broker.Update(s.ctx, "", details, true)
78 | s.Nil(err)
79 | }
80 |
81 | func (s *UpdateSuite) TestUpdateSuccess() {
82 | details := brokerapi.UpdateDetails{
83 | RawParameters: json.RawMessage(`{
84 | "insecure_origin": true,
85 | "domain": "domain.gov",
86 | "path": "."
87 | }`),
88 | }
89 | s.Manager.On("Update", "", "domain.gov", "origin.cloud.gov", ".", true, utils.Headers{"Host": true}, true).Return(nil)
90 | s.cfclient.On("GetDomainByName", "domain.gov").Return(cfclient.Domain{}, nil)
91 | _, err := s.Broker.Update(s.ctx, "", details, true)
92 | s.Nil(err)
93 | }
94 |
95 | func (s *UpdateSuite) TestDomainNotExists() {
96 | details := brokerapi.UpdateDetails{
97 | PreviousValues: brokerapi.PreviousValues{
98 | OrgID: "dfb39134-ab7d-489e-ae59-4ed5c6f42fb5",
99 | },
100 | RawParameters: json.RawMessage(`{"domain": "domain.gov"}`),
101 | }
102 | s.Manager.On("Update", "", "domain.gov", "origin.cloud.gov", ".", true, utils.Headers{"Host": true}, true).Return(nil)
103 | s.cfclient.On("GetOrgByGuid", "dfb39134-ab7d-489e-ae59-4ed5c6f42fb5").Return(cfclient.Org{Name: "my-org"}, nil)
104 | s.cfclient.On("GetDomainByName", "domain.gov").Return(cfclient.Domain{}, errors.New("bad"))
105 | _, err := s.Broker.Update(s.ctx, "", details, true)
106 | s.NotNil(err)
107 | s.Contains(err.Error(), "cf create-domain")
108 | }
109 |
110 | func (s *UpdateSuite) setupTestOfHeaderForwarding() {
111 | s.cfclient.On("GetDomainByName", "domain.gov").Return(cfclient.Domain{}, nil)
112 | }
113 |
114 | func (s *UpdateSuite) allowUpdateWithExpectedHeaders(expectedHeaders utils.Headers) {
115 | s.Manager.On("Update", "", "domain.gov", "origin.cloud.gov", ".", true, expectedHeaders, true).Return(nil)
116 | }
117 |
118 | func (s *UpdateSuite) failOnUpdateWithExpectedHeaders(expectedHeaders utils.Headers) {
119 | s.Manager.On("Update", "", "domain.gov", "origin.cloud.gov", ".", true, expectedHeaders, true).Return(errors.New("fail"))
120 | }
121 |
122 | func (s *UpdateSuite) TestSuccessForwardingDuplicatedHostHeader() {
123 | s.setupTestOfHeaderForwarding()
124 | s.allowUpdateWithExpectedHeaders(utils.Headers{"Host": true})
125 |
126 | details := brokerapi.UpdateDetails{
127 | RawParameters: json.RawMessage(`{
128 | "insecure_origin": true,
129 | "domain": "domain.gov",
130 | "path": ".",
131 | "headers": ["Host"]
132 | }`),
133 | }
134 | _, err := s.Broker.Update(s.ctx, "", details, true)
135 | s.Nil(err)
136 | }
137 |
138 | func (s *UpdateSuite) TestSuccessForwardedSingleHeader() {
139 | s.setupTestOfHeaderForwarding()
140 | s.allowUpdateWithExpectedHeaders(utils.Headers{"User-Agent": true, "Host": true})
141 |
142 | details := brokerapi.UpdateDetails{
143 | RawParameters: json.RawMessage(`{
144 | "insecure_origin": true,
145 | "domain": "domain.gov",
146 | "path": ".",
147 | "headers": ["User-Agent"]
148 | }`),
149 | }
150 | _, err := s.Broker.Update(s.ctx, "", details, true)
151 | s.Nil(err)
152 | }
153 |
154 | func (s *UpdateSuite) TestSuccessForwardingWildcardHeader() {
155 | s.setupTestOfHeaderForwarding()
156 | s.allowUpdateWithExpectedHeaders(utils.Headers{"*": true})
157 |
158 | details := brokerapi.UpdateDetails{
159 | RawParameters: json.RawMessage(`{
160 | "insecure_origin": true,
161 | "domain": "domain.gov",
162 | "path": ".",
163 | "headers": ["*"]
164 | }`),
165 | }
166 | _, err := s.Broker.Update(s.ctx, "", details, true)
167 | s.Nil(err)
168 | }
169 |
170 | func (s *UpdateSuite) TestSuccessNineForwardedHeaders() {
171 | s.setupTestOfHeaderForwarding()
172 | s.allowUpdateWithExpectedHeaders(utils.Headers{"One": true, "Two": true, "Three": true, "Four": true, "Five": true, "Six": true, "Seven": true, "Eight": true, "Nine": true, "Host": true})
173 |
174 | details := brokerapi.UpdateDetails{
175 | RawParameters: json.RawMessage(`{
176 | "insecure_origin": true,
177 | "domain": "domain.gov",
178 | "path": ".",
179 | "headers": ["One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine"]
180 | }`),
181 | }
182 | _, err := s.Broker.Update(s.ctx, "", details, true)
183 | s.Nil(err)
184 | }
185 |
186 | func (s *UpdateSuite) TestForwardedHeadersWhitelistAndWildcard() {
187 | s.setupTestOfHeaderForwarding()
188 | s.failOnUpdateWithExpectedHeaders(utils.Headers{"*": true})
189 |
190 | details := brokerapi.UpdateDetails{
191 | RawParameters: json.RawMessage(`{
192 | "insecure_origin": true,
193 | "domain": "domain.gov",
194 | "path": ".",
195 | "headers": ["*", "User-Agent"]
196 | }`),
197 | }
198 | _, err := s.Broker.Update(s.ctx, "", details, true)
199 | s.NotNil(err)
200 | s.Contains(err.Error(), "must not pass whitelisted headers alongside wildcard")
201 | }
202 |
203 | func (s *UpdateSuite) TestForwardedHeadersMoreThanTen() {
204 | s.setupTestOfHeaderForwarding()
205 | s.failOnUpdateWithExpectedHeaders(utils.Headers{"One": true, "Two": true, "Three": true, "Four": true, "Five": true, "Six": true, "Seven": true, "Eight": true, "Nine": true, "Ten": true, "Host": true})
206 |
207 | details := brokerapi.UpdateDetails{
208 | RawParameters: json.RawMessage(`{
209 | "insecure_origin": true,
210 | "domain": "domain.gov",
211 | "path": ".",
212 | "headers": ["One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten"]
213 | }`),
214 | }
215 | _, err := s.Broker.Update(s.ctx, "", details, true)
216 | s.NotNil(err)
217 | s.Contains(err.Error(), "must not set more than 10 headers; got 11")
218 | }
219 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Cloud Foundry CDN Service Broker [](https://travis-ci.org/cloud-gov/cf-cdn-service-broker)
2 |
3 | A [Cloud Foundry](https://www.cloudfoundry.org/) [service broker](https://docs.cloudfoundry.org/services/) that uses [AWS CloudFront](https://aws.amazon.com/cloudfront/) to proxy traffic from a domain that the user controls (the domain) to an existing Cloud Foundry application or external URL. Traffic is encrypted using an SSL certificate generated by [Let's Encrypt](https://letsencrypt.org/).
4 |
5 |
6 | ## Let's Encrypt V1 End of Life
7 |
8 | The Let's Encrypt V1 endpoint is reaching end of life in June of 2020. In November of 2019, Let's Encrypt shutdown the creation of new users via the V1 API. https://community.letsencrypt.org/t/end-of-life-plan-for-acmev1/88430
9 |
10 | In response to disabling new user creation, this broker has been changed to use an existing user's credentials. This is implemented in `LoadRandomUser` in `models/models.go`. The pool of user ids to select from is configured via an environment variable `USER_ID_POOL`. This environment variable is injected via the pipeline using the `cdn-broker-user-id-pool-staging` and `cdn-broker-user-id-pool-production` variables in the `cg-deploy-cdn-broker.yml` vars file. These values should be set as a comma separated list in double quotes.
11 |
12 | `LoadRandomUser` will select a user from the pool, use the Let's Encrypt `reg` and `key` and create a new user entry in the broker database. Effectively, the user is the same in the eyes of Let's Encrypt but a different user in the broker database. This maintains the one user to one domain relationship in the broker database.
13 |
14 | The random selection of users from a pool aims to minimize the impact of the following rate limits:
15 | * - 300 Pending Authorizations per account
16 | * - Failed Validation limit of 5 failures per account, per hostname, per hour.
17 |
18 | ## Deployment
19 |
20 | ### Automated
21 |
22 | The easiest/recommended way to deploy the broker is via the [Concourse](http://concourse.ci/) pipeline.
23 |
24 | 1. Create a `ci/credentials.yml` file, and fill in the templated values from [the pipeline](ci/pipeline.yml).
25 | 1. Deploy the pipeline.
26 |
27 | ```bash
28 | fly -t lite set-pipeline -n -c ci/pipeline.yml -p deploy-cdn-broker -l ci/credentials.yml
29 | ```
30 |
31 | ### Manual
32 |
33 | 1. Clone this repository, and `cd` into it.
34 | 1. Target the space you want to deploy the broker to.
35 |
36 | ```bash
37 | $ cf target -o -s
38 | ```
39 |
40 | 1. Set the `environment_variables` listed in [the deploy pipeline](ci/pipeline.yml).
41 | 1. Deploy the broker as an application.
42 |
43 | ```bash
44 | $ cf push
45 | ```
46 |
47 | 1. [Register the broker](http://docs.cloudfoundry.org/services/managing-service-brokers.html#register-broker).
48 |
49 | ```bash
50 | $ cf create-service-broker cdn-route [username] [password] [app-url] --space-scoped
51 | ```
52 |
53 | ## Usage
54 |
55 | 1. Target the space your application is running in.
56 |
57 | ```bash
58 | $ cf target -o -s
59 | ```
60 |
61 | 1. Add your domain to your Cloud Foundry organization:
62 |
63 | ````bash
64 | $ cf create-domain my.domain.gov
65 | ```
66 |
67 | 1. Create a service instance.
68 |
69 | ```bash
70 | $ cf create-service cdn-route cdn-route my-cdn-route -c '{"domain": "my.domain.gov"}'
71 |
72 | Create in progress. Use 'cf services' or 'cf service my-cdn-route' to check operation status.
73 | ```
74 |
75 | If you have more than one domain you can pass a comma-delimited list to the `domain` parameter, just keep in mind that the broker will wait until all domains are CNAME'd:
76 |
77 | ```bash
78 | $ cf create-service cdn-route cdn-route my-cdn-route -c '{"domain": "my.domain.gov,www.my.domain.gov"}'
79 |
80 | Create in progress. Use 'cf services' or 'cf service my-cdn-route' to check operation status.
81 | ```
82 |
83 | 1. Get the DNS instructions.
84 |
85 | ```bash
86 | $ cf service my-cdn-route
87 |
88 | Last Operation
89 | Status: create in progress
90 | Message: Provisioning in progress; CNAME domain "my.domain.gov" to "d3kajwa62y9xrp.cloudfront.net."
91 | ```
92 |
93 | 1. Create/update your DNS configuration.
94 |
95 | 1. Wait up to 30 minutes for the CloudFront distribution to be provisioned and the DNS changes to propagate.
96 |
97 | 1. Visit `my.domain.gov`, and see that you have a valid certificate (i.e. that visiting your site in a modern browser doesn't give you a certificate warning).
98 |
99 | 1. Add your domain to a Cloud Foundry application:
100 |
101 | ```bash
102 | $ cf map-route my.domain.gov
103 | ```
104 |
105 | ## Custom origins
106 |
107 | If you are pointing your domain to a non-Cloud Foundry application, such as a public S3 bucket, you can pass a custom origin to the broker:
108 |
109 | ```bash
110 | $ cf create-service cdn-route cdn-route my-cdn-route \
111 | -c '{"domain": "my.domain.gov", "origin": "my-app.apps.cloud.gov"}'
112 |
113 | Create in progress. Use 'cf services' or 'cf service my-cdn-route' to check operation status.
114 | ```
115 |
116 | If you need to add a path to your origin, you can pass it in as a parameter:
117 |
118 | ```bash
119 | $ cf create-service cdn-route cdn-route my-cdn-route \
120 | -c '{"domain": "my.domain.gov", "origin": "my-app.apps.cloud.gov", "path": "/myfolder"}'
121 |
122 | Create in progress. Use 'cf services' or 'cf service my-cdn-route' to check operation status.
123 | ```
124 |
125 | If your origin is non-HTTPS, you'll need to add another parameter:
126 |
127 | ```bash
128 | $ cf create-service cdn-route cdn-route my-cdn-route \
129 | -c '{"domain": "my.domain.gov", "origin": "my-app.apps.cloud.gov", "insecure_origin": true}'
130 |
131 | Create in progress. Use 'cf services' or 'cf service my-cdn-route' to check operation status.
132 | ```
133 |
134 | ## Cookie Forwarding
135 |
136 | If you do not want cookies forwarded to your origin, you'll need to add another parameter:
137 |
138 | ```bash
139 | $ cf create-service cdn-route cdn-route my-cdn-route \
140 | -c '{"domain": "my.domain.gov", "cookies": false}'
141 |
142 | Create in progress. Use 'cf services' or 'cf service my-cdn-route' to check operation status.
143 | ```
144 |
145 | ## Header Forwarding
146 |
147 | CloudFront forwards a [limited set of headers](http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/RequestAndResponseBehaviorCustomOrigin.html#request-custom-headers-behavior) by default. If you want extra headers forwarded to your origin, you'll want to add another parameter. Here we forward both the `User-Agent` and `Referer` headers:
148 |
149 | ```bash
150 | $ cf create-service cdn-route cdn-route my-cdn-route \
151 | -c '{"domain": "my.domain.gov", "headers": ["User-Agent", "Referer"]}'
152 |
153 | Create in progress. Use 'cf services' or 'cf service my-cdn-route' to check operation status.
154 | ```
155 |
156 | CloudFront can forward up to 10 custom headers. Because this broker automatically forwards the `Host` header when not using a [custom origin](#custom-origins), you can whitelist up to nine headers by default; if using a custom origin, you can whitelist up to 10 headers. If you want to exceed this limit or forward all headers, you can use a wildcard:
157 |
158 | ```bash
159 | $ cf create-service cdn-route cdn-route my-cdn-route \
160 | -c '{"domain": "my.domain.gov", "headers": ["*"]}'
161 |
162 | Create in progress. Use 'cf services' or 'cf service my-cdn-route' to check operation status.
163 | ```
164 |
165 | When making requests to the origin, CloudFront's caching mechanism associates HTTP requests with their response. The more variation within the forwarded request, the fewer cache hits and the less effective the cache. Limiting the headers forwarded is therefore key to cache performance. Caching is disabled altogether when using a wildcard.
166 |
167 | ## Debugging
168 |
169 | By default, Cloud Controller will expire asynchronous service instances that have been pending for over one week. If your instance expires, run a dummy update
170 | to restore it to the pending state so that Cloud Controller will continue to check for updates:
171 |
172 | ```bash
173 | cf update-service my-cdn-route -c '{"timestamp": 20161001}'
174 | ```
175 |
176 | ## Tests
177 |
178 | ```bash
179 | go test -v $(go list ./... | grep -v /vendor/)
180 | ```
181 |
182 | ## Contributing
183 |
184 | See [CONTRIBUTING](CONTRIBUTING.md) for additional information.
185 |
186 | ## Public domain
187 |
188 | This project is in the worldwide [public domain](LICENSE.md). As stated in [CONTRIBUTING](CONTRIBUTING.md):
189 |
190 | > This project is in the public domain within the United States, and copyright and related rights in the work worldwide are waived through the [CC0 1.0 Universal public domain dedication](https://creativecommons.org/publicdomain/zero/1.0/).
191 | >
192 | > All contributions to this project will be released under the CC0 dedication. By submitting a pull request, you are agreeing to comply with this waiver of copyright interest.
193 |
--------------------------------------------------------------------------------
/broker/broker_provision_test.go:
--------------------------------------------------------------------------------
1 | package broker_test
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/suite"
9 |
10 | "code.cloudfoundry.org/lager"
11 | "github.com/cloudfoundry-community/go-cfclient"
12 | "github.com/pivotal-cf/brokerapi"
13 |
14 | "github.com/cloud-gov/cf-cdn-service-broker/broker"
15 | cfmock "github.com/cloud-gov/cf-cdn-service-broker/cf/mocks"
16 | "github.com/cloud-gov/cf-cdn-service-broker/config"
17 | "github.com/cloud-gov/cf-cdn-service-broker/models"
18 | "github.com/cloud-gov/cf-cdn-service-broker/models/mocks"
19 | "github.com/cloud-gov/cf-cdn-service-broker/utils"
20 | )
21 |
22 | func TestProvisioning(t *testing.T) {
23 | suite.Run(t, new(ProvisionSuite))
24 | }
25 |
26 | type ProvisionSuite struct {
27 | suite.Suite
28 | Manager mocks.RouteManagerIface
29 | Broker *broker.CdnServiceBroker
30 | cfclient cfmock.Client
31 | settings config.Settings
32 | logger lager.Logger
33 | ctx context.Context
34 | }
35 |
36 | func (s *ProvisionSuite) SetupTest() {
37 | s.Manager = mocks.RouteManagerIface{}
38 | s.cfclient = cfmock.Client{}
39 | s.logger = lager.NewLogger("broker.provision.test")
40 | s.settings = config.Settings{
41 | DefaultOrigin: "origin.cloud.gov",
42 | }
43 | s.Broker = broker.New(
44 | &s.Manager,
45 | &s.cfclient,
46 | s.settings,
47 | s.logger,
48 | )
49 | s.ctx = context.Background()
50 |
51 | s.cfclient.On("GetOrgByGuid", "dfb39134-ab7d-489e-ae59-4ed5c6f42fb5").Return(cfclient.Org{Name: "my-org"}, nil)
52 |
53 | }
54 |
55 | func (s *ProvisionSuite) TestSync() {
56 | _, err := s.Broker.Provision(s.ctx, "", brokerapi.ProvisionDetails{}, false)
57 | s.Equal(err, brokerapi.ErrAsyncRequired)
58 | }
59 |
60 | func (s *ProvisionSuite) TestWithoutDetails() {
61 | _, err := s.Broker.Provision(s.ctx, "", brokerapi.ProvisionDetails{}, true)
62 | s.NotNil(err)
63 | s.Equal(err.Error(), "must be invoked with configuration parameters")
64 | }
65 |
66 | func (s *ProvisionSuite) TestWithoutOptions() {
67 | details := brokerapi.ProvisionDetails{
68 | RawParameters: []byte(`{}`),
69 | }
70 | _, err := s.Broker.Provision(s.ctx, "", details, true)
71 | s.NotNil(err)
72 | s.Equal(err.Error(), "must pass non-empty `domain`")
73 | }
74 |
75 | func (s *ProvisionSuite) TestInstanceExists() {
76 | route := &models.Route{
77 | State: models.Provisioned,
78 | }
79 | s.cfclient.On("GetDomainByName", "domain.gov").Return(cfclient.Domain{}, nil)
80 | s.Manager.On("Get", "123").Return(route, nil)
81 |
82 | details := brokerapi.ProvisionDetails{
83 | RawParameters: []byte(`{"domain": "domain.gov"}`),
84 | }
85 | _, err := s.Broker.Provision(s.ctx, "123", details, true)
86 | s.Equal(err, brokerapi.ErrInstanceAlreadyExists)
87 | }
88 |
89 | func (s *ProvisionSuite) TestSuccess() {
90 | s.Manager.On("Get", "123").Return(&models.Route{}, errors.New("not found"))
91 | route := &models.Route{State: models.Provisioning}
92 | s.cfclient.On("GetDomainByName", "domain.gov").Return(cfclient.Domain{}, nil)
93 | s.Manager.On("Create", "123", "domain.gov", "origin.cloud.gov", "", false, utils.Headers{"Host": true}, true,
94 | map[string]string{"Organization": "", "Space": "", "Service": "", "Plan": ""}).Return(route, nil)
95 |
96 | details := brokerapi.ProvisionDetails{
97 | RawParameters: []byte(`{"domain": "domain.gov"}`),
98 | }
99 | _, err := s.Broker.Provision(s.ctx, "123", details, true)
100 | s.Nil(err)
101 | }
102 |
103 | func (s *ProvisionSuite) TestSuccessCustomOrigin() {
104 | s.Manager.On("Get", "123").Return(&models.Route{}, errors.New("not found"))
105 | route := &models.Route{State: models.Provisioning}
106 | s.Manager.On("Create", "123", "domain.gov", "custom.cloud.gov", "", false, utils.Headers{}, true,
107 | map[string]string{"Organization": "", "Space": "", "Service": "", "Plan": ""}).Return(route, nil)
108 |
109 | details := brokerapi.ProvisionDetails{
110 | RawParameters: []byte(`{"domain": "domain.gov", "origin": "custom.cloud.gov"}`),
111 | }
112 | _, err := s.Broker.Provision(s.ctx, "123", details, true)
113 | s.Nil(err)
114 | }
115 |
116 | func (s *ProvisionSuite) TestDomainNotExists() {
117 | s.cfclient.On("GetDomainByName", "domain.gov").Return(cfclient.Domain{}, errors.New("fail"))
118 | details := brokerapi.ProvisionDetails{
119 | OrganizationGUID: "dfb39134-ab7d-489e-ae59-4ed5c6f42fb5",
120 | RawParameters: []byte(`{"domain": "domain.gov"}`),
121 | }
122 | _, err := s.Broker.Provision(s.ctx, "123", details, true)
123 | s.NotNil(err)
124 | s.Contains(err.Error(), "cf create-domain")
125 | }
126 |
127 | func (s *ProvisionSuite) TestMultipleDomainsOneNotExists() {
128 | s.cfclient.On("GetDomainByName", "domain.gov").Return(cfclient.Domain{}, nil)
129 | s.cfclient.On("GetDomainByName", "domain2.gov").Return(cfclient.Domain{}, errors.New("fail"))
130 | details := brokerapi.ProvisionDetails{
131 | OrganizationGUID: "dfb39134-ab7d-489e-ae59-4ed5c6f42fb5",
132 | RawParameters: []byte(`{"domain": "domain.gov,domain2.gov"}`),
133 | }
134 | _, err := s.Broker.Provision(s.ctx, "123", details, true)
135 | s.NotNil(err)
136 | s.Contains(err.Error(), "Domain does not exist")
137 | s.NotContains(err.Error(), "domain.gov")
138 | s.Contains(err.Error(), "domain2.gov")
139 | }
140 |
141 | func (s *ProvisionSuite) TestMultipleDomainsMoreThanOneNotExists() {
142 | s.cfclient.On("GetDomainByName", "domain.gov").Return(cfclient.Domain{}, nil)
143 | s.cfclient.On("GetDomainByName", "domain2.gov").Return(cfclient.Domain{}, errors.New("fail"))
144 | s.cfclient.On("GetDomainByName", "domain3.gov").Return(cfclient.Domain{}, errors.New("fail"))
145 | details := brokerapi.ProvisionDetails{
146 | OrganizationGUID: "dfb39134-ab7d-489e-ae59-4ed5c6f42fb5",
147 | RawParameters: []byte(`{"domain": "domain.gov,domain2.gov,domain3.gov"}`),
148 | }
149 | _, err := s.Broker.Provision(s.ctx, "123", details, true)
150 | s.NotNil(err)
151 | s.Contains(err.Error(), "Multiple domains do not exist")
152 | s.NotContains(err.Error(), "domain.gov")
153 | s.Contains(err.Error(), "domain2.gov")
154 | s.Contains(err.Error(), "domain3.gov")
155 | }
156 |
157 | func (s *ProvisionSuite) setupTestOfHeaderForwarding() {
158 | s.Manager.On("Get", "123").Return(&models.Route{}, errors.New("not found"))
159 | s.cfclient.On("GetDomainByName", "domain.gov").Return(cfclient.Domain{}, nil)
160 | }
161 |
162 | func (s *ProvisionSuite) allowCreateWithExpectedHeaders(expectedHeaders utils.Headers) {
163 | route := &models.Route{State: models.Provisioning}
164 | s.Manager.On("Create", "123", "domain.gov", "origin.cloud.gov", "", false, expectedHeaders, true,
165 | map[string]string{"Organization": "", "Space": "", "Service": "", "Plan": ""}).Return(route, nil)
166 | }
167 |
168 | func (s *ProvisionSuite) failCreateWithExpectedHeaders(expectedHeaders utils.Headers) {
169 | s.Manager.On("Create", "123", "domain.gov", "origin.cloud.gov", "", false, expectedHeaders, true,
170 | map[string]string{"Organization": "", "Space": "", "Service": "", "Plan": ""}).Return(nil, errors.New("fail"))
171 | }
172 |
173 | func (s *ProvisionSuite) TestSuccessForwardingDuplicatedHostHeader() {
174 | s.setupTestOfHeaderForwarding()
175 | s.allowCreateWithExpectedHeaders(utils.Headers{"Host": true})
176 |
177 | details := brokerapi.ProvisionDetails{
178 | RawParameters: []byte(`{"domain": "domain.gov", "headers": ["Host"]}`),
179 | }
180 | _, err := s.Broker.Provision(s.ctx, "123", details, true)
181 | s.Nil(err)
182 | }
183 |
184 | func (s *ProvisionSuite) TestSuccessForwardedSingleHeader() {
185 | s.setupTestOfHeaderForwarding()
186 | s.allowCreateWithExpectedHeaders(utils.Headers{"User-Agent": true, "Host": true})
187 |
188 | details := brokerapi.ProvisionDetails{
189 | RawParameters: []byte(`{"domain": "domain.gov", "headers": ["User-Agent"]}`),
190 | }
191 | _, err := s.Broker.Provision(s.ctx, "123", details, true)
192 | s.Nil(err)
193 | }
194 |
195 | func (s *ProvisionSuite) TestSuccessForwardingWildcardHeader() {
196 | s.setupTestOfHeaderForwarding()
197 | s.allowCreateWithExpectedHeaders(utils.Headers{"*": true})
198 |
199 | details := brokerapi.ProvisionDetails{
200 | RawParameters: []byte(`{"domain": "domain.gov", "headers": ["*"]}`),
201 | }
202 | _, err := s.Broker.Provision(s.ctx, "123", details, true)
203 | s.Nil(err)
204 | }
205 |
206 | func (s *ProvisionSuite) TestSuccessNineForwardedHeaders() {
207 | s.setupTestOfHeaderForwarding()
208 | s.allowCreateWithExpectedHeaders(utils.Headers{"One": true, "Two": true, "Three": true, "Four": true, "Five": true, "Six": true, "Seven": true, "Eight": true, "Nine": true, "Host": true})
209 |
210 | details := brokerapi.ProvisionDetails{
211 | RawParameters: []byte(`{"domain": "domain.gov", "headers": ["One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine"]}`),
212 | }
213 | _, err := s.Broker.Provision(s.ctx, "123", details, true)
214 | s.Nil(err)
215 | }
216 |
217 | func (s *ProvisionSuite) TestForwardedHeadersDuplicates() {
218 | s.setupTestOfHeaderForwarding()
219 | s.failCreateWithExpectedHeaders(utils.Headers{"User-Agent": true, "Host": true})
220 |
221 | details := brokerapi.ProvisionDetails{
222 | RawParameters: []byte(`{"domain": "domain.gov", "headers": ["User-Agent", "Host", "User-Agent"]}`),
223 | }
224 | _, err := s.Broker.Provision(s.ctx, "123", details, true)
225 | s.NotNil(err)
226 | s.Contains(err.Error(), "must not pass duplicated header 'User-Agent'")
227 | }
228 |
229 | func (s *ProvisionSuite) TestForwardedHeadersWhitelistAndWildcard() {
230 | s.setupTestOfHeaderForwarding()
231 | s.failCreateWithExpectedHeaders(utils.Headers{"*": true})
232 |
233 | details := brokerapi.ProvisionDetails{
234 | RawParameters: []byte(`{"domain": "domain.gov", "headers": ["*", "User-Agent"]}`),
235 | }
236 | _, err := s.Broker.Provision(s.ctx, "123", details, true)
237 | s.NotNil(err)
238 | s.Contains(err.Error(), "must not pass whitelisted headers alongside wildcard")
239 | }
240 |
241 | func (s *ProvisionSuite) TestForwardedHeadersMoreThanTen() {
242 | s.setupTestOfHeaderForwarding()
243 | s.failCreateWithExpectedHeaders(utils.Headers{"One": true, "Two": true, "Three": true, "Four": true, "Five": true, "Six": true, "Seven": true, "Eight": true, "Nine": true, "Ten": true, "Host": true})
244 |
245 | details := brokerapi.ProvisionDetails{
246 | RawParameters: []byte(`{"domain": "domain.gov", "headers": ["One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten"]}`),
247 | }
248 | _, err := s.Broker.Provision(s.ctx, "123", details, true)
249 | s.NotNil(err)
250 | s.Contains(err.Error(), "must not set more than 10 headers; got 11")
251 | }
252 |
--------------------------------------------------------------------------------
/broker/broker.go:
--------------------------------------------------------------------------------
1 | package broker
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "errors"
7 | "fmt"
8 | "io/ioutil"
9 | "strings"
10 |
11 | "code.cloudfoundry.org/lager"
12 | "github.com/pivotal-cf/brokerapi"
13 |
14 | "github.com/cloud-gov/cf-cdn-service-broker/cf"
15 | "github.com/cloud-gov/cf-cdn-service-broker/config"
16 | "github.com/cloud-gov/cf-cdn-service-broker/models"
17 | "github.com/cloud-gov/cf-cdn-service-broker/utils"
18 | )
19 |
20 | type Options struct {
21 | Domain string `json:"domain"`
22 | Origin string `json:"origin"`
23 | Path string `json:"path"`
24 | InsecureOrigin bool `json:"insecure_origin"`
25 | Cookies bool `json:"cookies"`
26 | Headers []string `json:"headers"`
27 | }
28 |
29 | type CdnServiceBroker struct {
30 | manager models.RouteManagerIface
31 | cfclient cf.Client
32 | settings config.Settings
33 | logger lager.Logger
34 | }
35 |
36 | func New(
37 | manager models.RouteManagerIface,
38 | cfclient cf.Client,
39 | settings config.Settings,
40 | logger lager.Logger,
41 | ) *CdnServiceBroker {
42 | return &CdnServiceBroker{
43 | manager: manager,
44 | cfclient: cfclient,
45 | settings: settings,
46 | logger: logger,
47 | }
48 | }
49 |
50 | var (
51 | MAX_HEADER_COUNT = 10
52 | )
53 |
54 | func (*CdnServiceBroker) Services(context context.Context) ([]brokerapi.Service, error) {
55 | var service brokerapi.Service
56 | buf, err := ioutil.ReadFile("./catalog.json")
57 | if err != nil {
58 | return []brokerapi.Service{}, err
59 | }
60 | err = json.Unmarshal(buf, &service)
61 | if err != nil {
62 | return []brokerapi.Service{}, err
63 | }
64 | return []brokerapi.Service{service}, nil
65 | }
66 |
67 | func (b *CdnServiceBroker) Provision(
68 | context context.Context,
69 | instanceID string,
70 | details brokerapi.ProvisionDetails,
71 | asyncAllowed bool,
72 | ) (brokerapi.ProvisionedServiceSpec, error) {
73 | spec := brokerapi.ProvisionedServiceSpec{}
74 |
75 | if !asyncAllowed {
76 | return spec, brokerapi.ErrAsyncRequired
77 | }
78 |
79 | options, err := b.parseProvisionDetails(details)
80 | if err != nil {
81 | return spec, err
82 | }
83 |
84 | _, err = b.manager.Get(instanceID)
85 | if err == nil {
86 | return spec, brokerapi.ErrInstanceAlreadyExists
87 | }
88 |
89 | headers, err := b.getHeaders(options)
90 | if err != nil {
91 | return spec, err
92 | }
93 |
94 | tags := map[string]string{
95 | "Organization": details.OrganizationGUID,
96 | "Space": details.SpaceGUID,
97 | "Service": details.ServiceID,
98 | "Plan": details.PlanID,
99 | }
100 |
101 | _, err = b.manager.Create(instanceID, options.Domain, options.Origin, options.Path, options.InsecureOrigin, headers, options.Cookies, tags)
102 | if err != nil {
103 | return spec, err
104 | }
105 |
106 | return brokerapi.ProvisionedServiceSpec{IsAsync: true}, nil
107 | }
108 |
109 | func (b *CdnServiceBroker) LastOperation(
110 | context context.Context,
111 | instanceID, operationData string,
112 | ) (brokerapi.LastOperation, error) {
113 | route, err := b.manager.Get(instanceID)
114 | if err != nil {
115 | return brokerapi.LastOperation{
116 | State: brokerapi.Failed,
117 | Description: "Service instance not found",
118 | }, nil
119 | }
120 |
121 | err = b.manager.Poll(route)
122 | if err != nil {
123 | b.logger.Error("Error during update", err, lager.Data{
124 | "domain": route.DomainExternal,
125 | "state": route.State,
126 | })
127 | }
128 |
129 | switch route.State {
130 | case models.Provisioning:
131 | instructions, err := b.manager.GetDNSInstructions(route)
132 | if err != nil {
133 | return brokerapi.LastOperation{}, err
134 | }
135 | description := fmt.Sprintf(
136 | "Provisioning in progress [%s => %s]; CNAME or ALIAS domain %s to %s or create TXT record(s): \n%s",
137 | route.DomainExternal, route.Origin, route.DomainExternal, route.DomainInternal,
138 | strings.Join(instructions, "\n"),
139 | )
140 | return brokerapi.LastOperation{
141 | State: brokerapi.InProgress,
142 | Description: description,
143 | }, nil
144 | case models.Deprovisioning:
145 | return brokerapi.LastOperation{
146 | State: brokerapi.InProgress,
147 | Description: fmt.Sprintf(
148 | "Deprovisioning in progress [%s => %s]; CDN domain %s",
149 | route.DomainExternal, route.Origin, route.DomainInternal,
150 | ),
151 | }, nil
152 | case models.Failed:
153 | return brokerapi.LastOperation{
154 | State: brokerapi.Failed,
155 | Description: "Failure while provisioning instance",
156 | }, nil
157 | default:
158 | return brokerapi.LastOperation{
159 | State: brokerapi.Succeeded,
160 | Description: fmt.Sprintf(
161 | "Service instance provisioned [%s => %s]; CDN domain %s",
162 | route.DomainExternal, route.Origin, route.DomainInternal,
163 | ),
164 | }, nil
165 | }
166 | }
167 |
168 | func (b *CdnServiceBroker) Deprovision(
169 | context context.Context,
170 | instanceID string,
171 | details brokerapi.DeprovisionDetails,
172 | asyncAllowed bool,
173 | ) (brokerapi.DeprovisionServiceSpec, error) {
174 | if !asyncAllowed {
175 | return brokerapi.DeprovisionServiceSpec{}, brokerapi.ErrAsyncRequired
176 | }
177 |
178 | route, err := b.manager.Get(instanceID)
179 | if err != nil {
180 | return brokerapi.DeprovisionServiceSpec{}, err
181 | }
182 |
183 | err = b.manager.Disable(route)
184 | if err != nil {
185 | return brokerapi.DeprovisionServiceSpec{}, nil
186 | }
187 |
188 | return brokerapi.DeprovisionServiceSpec{IsAsync: true}, nil
189 | }
190 |
191 | func (b *CdnServiceBroker) Bind(
192 | context context.Context,
193 | instanceID, bindingID string,
194 | details brokerapi.BindDetails,
195 | ) (brokerapi.Binding, error) {
196 | return brokerapi.Binding{}, errors.New("service does not support bind")
197 | }
198 |
199 | func (b *CdnServiceBroker) Unbind(
200 | context context.Context,
201 | instanceID, bindingID string,
202 | details brokerapi.UnbindDetails,
203 | ) error {
204 | return errors.New("service does not support bind")
205 | }
206 |
207 | func (b *CdnServiceBroker) Update(
208 | context context.Context,
209 | instanceID string,
210 | details brokerapi.UpdateDetails,
211 | asyncAllowed bool,
212 | ) (brokerapi.UpdateServiceSpec, error) {
213 | if !asyncAllowed {
214 | return brokerapi.UpdateServiceSpec{}, brokerapi.ErrAsyncRequired
215 | }
216 |
217 | options, err := b.parseUpdateDetails(details)
218 | if err != nil {
219 | return brokerapi.UpdateServiceSpec{}, err
220 | }
221 |
222 | headers, err := b.getHeaders(options)
223 | if err != nil {
224 | return brokerapi.UpdateServiceSpec{}, err
225 | }
226 |
227 | err = b.manager.Update(instanceID, options.Domain, options.Origin, options.Path, options.InsecureOrigin, headers, options.Cookies)
228 | if err != nil {
229 | return brokerapi.UpdateServiceSpec{}, err
230 | }
231 |
232 | return brokerapi.UpdateServiceSpec{IsAsync: true}, nil
233 | }
234 |
235 | // createBrokerOptions will attempt to take raw json and convert it into the "Options" struct.
236 | func (b *CdnServiceBroker) createBrokerOptions(details []byte) (options Options, err error) {
237 | if len(details) == 0 {
238 | err = errors.New("must be invoked with configuration parameters")
239 | return
240 | }
241 | options = Options{
242 | Origin: b.settings.DefaultOrigin,
243 | Cookies: true,
244 | Headers: []string{},
245 | }
246 | err = json.Unmarshal(details, &options)
247 | if err != nil {
248 | return
249 | }
250 | return
251 | }
252 |
253 | // parseProvisionDetails will attempt to parse the update details and then verify that BOTH least "domain" and "origin"
254 | // are provided.
255 | func (b *CdnServiceBroker) parseProvisionDetails(details brokerapi.ProvisionDetails) (options Options, err error) {
256 | options, err = b.createBrokerOptions(details.RawParameters)
257 | if err != nil {
258 | return
259 | }
260 | if options.Domain == "" {
261 | err = errors.New("must pass non-empty `domain`")
262 | return
263 | }
264 | if options.Origin == b.settings.DefaultOrigin {
265 | err = b.checkDomain(options.Domain, details.OrganizationGUID)
266 | if err != nil {
267 | return
268 | }
269 | }
270 | return
271 | }
272 |
273 | // parseUpdateDetails will attempt to parse the update details and then verify that at least "domain" or "origin"
274 | // are provided.
275 | func (b *CdnServiceBroker) parseUpdateDetails(details brokerapi.UpdateDetails) (options Options, err error) {
276 | options, err = b.createBrokerOptions(details.RawParameters)
277 | if err != nil {
278 | return
279 | }
280 | if options.Domain == "" && options.Origin == "" {
281 | err = errors.New("must pass non-empty `domain` or `origin`")
282 | return
283 | }
284 | if options.Domain != "" && options.Origin == b.settings.DefaultOrigin {
285 | err = b.checkDomain(options.Domain, details.PreviousValues.OrgID)
286 | if err != nil {
287 | return
288 | }
289 | }
290 | return
291 | }
292 |
293 | func (b *CdnServiceBroker) checkDomain(domain, orgGUID string) error {
294 | // domain can be a comma separated list so we need to check each one individually
295 | domains := strings.Split(domain, ",")
296 | var errorlist []string
297 |
298 | orgName := ""
299 |
300 | for _, domain := range domains {
301 | if _, err := b.cfclient.GetDomainByName(domain); err != nil {
302 | b.logger.Error("Error checking domain", err, lager.Data{
303 | "domain": domain,
304 | "orgGUID": orgGUID,
305 | })
306 | if orgName == "" {
307 | org, err := b.cfclient.GetOrgByGuid(orgGUID)
308 | if err == nil {
309 | orgName = org.Name
310 | }
311 | }
312 | errorlist = append(errorlist, fmt.Sprintf("`cf create-domain %s %s`", orgName, domain))
313 | }
314 | }
315 |
316 | if len(errorlist) > 0 {
317 | if len(errorlist) > 1 {
318 | return fmt.Errorf("Multiple domains do not exist; create them with:\n%s", strings.Join(errorlist, "\n"))
319 | }
320 | return fmt.Errorf("Domain does not exist; create it with %s", errorlist[0])
321 | }
322 |
323 | return nil
324 | }
325 |
326 | func (b *CdnServiceBroker) getHeaders(options Options) (headers utils.Headers, err error) {
327 | headers = utils.Headers{}
328 | for _, header := range options.Headers {
329 | if headers.Contains(header) {
330 | err = fmt.Errorf("must not pass duplicated header '%s'", header)
331 | return
332 | }
333 | headers.Add(header)
334 | }
335 |
336 | // Forbid accompanying a wildcard with specific headers.
337 | if headers.Contains("*") && len(headers) > 1 {
338 | err = errors.New("must not pass whitelisted headers alongside wildcard")
339 | return
340 | }
341 |
342 | // Ensure the Host header is forwarded if using a CloudFoundry origin.
343 | if options.Origin == b.settings.DefaultOrigin && !headers.Contains("*") {
344 | headers.Add("Host")
345 | }
346 |
347 | if len(headers) > MAX_HEADER_COUNT {
348 | err = fmt.Errorf("must not set more than %d headers; got %d", MAX_HEADER_COUNT, len(headers))
349 | return
350 | }
351 |
352 | return
353 | }
354 |
--------------------------------------------------------------------------------
/utils/cloudfront.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/aws/aws-sdk-go/aws"
7 | "github.com/aws/aws-sdk-go/service/cloudfront"
8 |
9 | "github.com/cloud-gov/cf-cdn-service-broker/config"
10 | )
11 |
12 | type DistributionIface interface {
13 | Create(callerReference string, domains []string, origin, path string, insecureOrigin bool, forwardedHeaders Headers, forwardCookies bool, tags map[string]string) (*cloudfront.Distribution, error)
14 | Update(distId string, domains []string, origin, path string, insecureOrigin bool, forwardedHeaders Headers, forwardCookies bool) (*cloudfront.Distribution, error)
15 | Get(distId string) (*cloudfront.Distribution, error)
16 | SetCertificate(distId, certId string) error
17 | SetCertificateAndCname(distId, certId string, domains []string) error
18 | Disable(distId string) error
19 | Delete(distId string) (bool, error)
20 | ListDistributions(callback func(cloudfront.DistributionSummary) bool) error
21 | }
22 |
23 | type Distribution struct {
24 | Settings config.Settings
25 | Service *cloudfront.CloudFront
26 | }
27 |
28 | func (d *Distribution) getAliases(domains []string) *cloudfront.Aliases {
29 | var items []*string
30 | for _, d := range domains {
31 | items = append(items, aws.String(d))
32 | }
33 | return &cloudfront.Aliases{
34 | Quantity: aws.Int64(int64(len(domains))),
35 | Items: items,
36 | }
37 | }
38 |
39 | func (d *Distribution) getTags(tags map[string]string) *cloudfront.Tags {
40 | items := []*cloudfront.Tag{}
41 | for key, value := range tags {
42 | items = append(items, &cloudfront.Tag{
43 | Key: aws.String(key),
44 | Value: aws.String(value),
45 | })
46 | }
47 | return &cloudfront.Tags{Items: items}
48 | }
49 |
50 | func (d *Distribution) getHeaders(headers []string) *cloudfront.Headers {
51 | items := make([]*string, len(headers))
52 | for idx, header := range headers {
53 | items[idx] = aws.String(header)
54 | }
55 | return &cloudfront.Headers{
56 | Quantity: aws.Int64(int64(len(headers))),
57 | Items: items,
58 | }
59 | }
60 |
61 | // fillDistributionConfig is a wrapper function that will get all the common config settings for
62 | // "cloudfront.DistributionConfig". This function is shared between "Create" and "Update".
63 | // In order to maintain backwards compatibility with older versions of the code where the callerReference was derived
64 | // from the domain(s), the callerReference has to be explicitly passed in. This is necessary because whenever we do an
65 | // update, the domains could change but we need to treat the CallerReference like an ID because
66 | // it can't be changed like the domains and instead the callerReference which was composed of the original domains must
67 | // be passed in.
68 | func (d *Distribution) fillDistributionConfig(config *cloudfront.DistributionConfig, origin, path string,
69 | insecureOrigin bool, callerReference *string, domains []string, forwardedHeaders []string, forwardCookies bool) {
70 | config.CallerReference = callerReference
71 | config.Comment = aws.String("cdn route service")
72 | config.Enabled = aws.Bool(true)
73 | config.IsIPV6Enabled = aws.Bool(true)
74 |
75 | cookies := aws.String("all")
76 | if forwardCookies == false {
77 | cookies = aws.String("none")
78 | }
79 |
80 | config.DefaultCacheBehavior = &cloudfront.DefaultCacheBehavior{
81 | TargetOriginId: aws.String(*callerReference),
82 | ForwardedValues: &cloudfront.ForwardedValues{
83 | Headers: d.getHeaders(forwardedHeaders),
84 | Cookies: &cloudfront.CookiePreference{
85 | Forward: cookies,
86 | },
87 | QueryString: aws.Bool(true),
88 | QueryStringCacheKeys: &cloudfront.QueryStringCacheKeys{
89 | Quantity: aws.Int64(0),
90 | },
91 | },
92 | SmoothStreaming: aws.Bool(false),
93 | DefaultTTL: aws.Int64(86400),
94 | MinTTL: aws.Int64(0),
95 | MaxTTL: aws.Int64(31536000),
96 | LambdaFunctionAssociations: &cloudfront.LambdaFunctionAssociations{
97 | Quantity: aws.Int64(0),
98 | },
99 | TrustedSigners: &cloudfront.TrustedSigners{
100 | Enabled: aws.Bool(false),
101 | Quantity: aws.Int64(0),
102 | },
103 | ViewerProtocolPolicy: aws.String("redirect-to-https"),
104 | AllowedMethods: &cloudfront.AllowedMethods{
105 | CachedMethods: &cloudfront.CachedMethods{
106 | Quantity: aws.Int64(2),
107 | Items: []*string{
108 | aws.String("HEAD"),
109 | aws.String("GET"),
110 | },
111 | },
112 | Quantity: aws.Int64(7),
113 | Items: []*string{
114 | aws.String("HEAD"),
115 | aws.String("GET"),
116 | aws.String("OPTIONS"),
117 | aws.String("PUT"),
118 | aws.String("POST"),
119 | aws.String("PATCH"),
120 | aws.String("DELETE"),
121 | },
122 | },
123 | Compress: aws.Bool(false),
124 | }
125 | config.Origins = &cloudfront.Origins{
126 | Quantity: aws.Int64(2),
127 | Items: []*cloudfront.Origin{
128 | {
129 | DomainName: aws.String(origin),
130 | Id: aws.String(*callerReference),
131 | OriginPath: aws.String(path),
132 | CustomHeaders: &cloudfront.CustomHeaders{
133 | Quantity: aws.Int64(0),
134 | },
135 | CustomOriginConfig: &cloudfront.CustomOriginConfig{
136 | HTTPPort: aws.Int64(80),
137 | HTTPSPort: aws.Int64(443),
138 | OriginReadTimeout: aws.Int64(30),
139 | OriginKeepaliveTimeout: aws.Int64(5),
140 | OriginProtocolPolicy: getOriginProtocolPolicy(insecureOrigin),
141 | OriginSslProtocols: &cloudfront.OriginSslProtocols{
142 | Quantity: aws.Int64(1),
143 | Items: []*string{
144 | aws.String("TLSv1.2"),
145 | },
146 | },
147 | },
148 | },
149 | {
150 | DomainName: aws.String(fmt.Sprintf("%s.s3.amazonaws.com", d.Settings.Bucket)),
151 | Id: aws.String(fmt.Sprintf("s3-%s-%s", d.Settings.Bucket, *callerReference)),
152 | OriginPath: aws.String(""),
153 | CustomHeaders: &cloudfront.CustomHeaders{
154 | Quantity: aws.Int64(0),
155 | },
156 | S3OriginConfig: &cloudfront.S3OriginConfig{
157 | OriginAccessIdentity: aws.String(""),
158 | },
159 | },
160 | },
161 | }
162 | config.CacheBehaviors = &cloudfront.CacheBehaviors{
163 | Quantity: aws.Int64(1),
164 | Items: []*cloudfront.CacheBehavior{
165 | {
166 | AllowedMethods: &cloudfront.AllowedMethods{
167 | CachedMethods: &cloudfront.CachedMethods{
168 | Quantity: aws.Int64(2),
169 | Items: []*string{
170 | aws.String("HEAD"),
171 | aws.String("GET"),
172 | },
173 | },
174 | Items: []*string{
175 | aws.String("HEAD"),
176 | aws.String("GET"),
177 | },
178 | Quantity: aws.Int64(2),
179 | },
180 | Compress: aws.Bool(false),
181 | PathPattern: aws.String("/.well-known/acme-challenge/*"),
182 | TargetOriginId: aws.String(fmt.Sprintf("s3-%s-%s", d.Settings.Bucket, *callerReference)),
183 | ForwardedValues: &cloudfront.ForwardedValues{
184 | Headers: &cloudfront.Headers{
185 | Quantity: aws.Int64(0),
186 | },
187 | QueryString: aws.Bool(false),
188 | Cookies: &cloudfront.CookiePreference{
189 | Forward: aws.String("none"),
190 | },
191 | QueryStringCacheKeys: &cloudfront.QueryStringCacheKeys{
192 | Quantity: aws.Int64(0),
193 | },
194 | },
195 | SmoothStreaming: aws.Bool(false),
196 | DefaultTTL: aws.Int64(86400),
197 | MinTTL: aws.Int64(0),
198 | MaxTTL: aws.Int64(31536000),
199 | LambdaFunctionAssociations: &cloudfront.LambdaFunctionAssociations{
200 | Quantity: aws.Int64(0),
201 | },
202 | TrustedSigners: &cloudfront.TrustedSigners{
203 | Enabled: aws.Bool(false),
204 | Quantity: aws.Int64(0),
205 | },
206 | ViewerProtocolPolicy: aws.String("allow-all"),
207 | },
208 | },
209 | }
210 | config.Aliases = d.getAliases(domains)
211 | config.PriceClass = aws.String("PriceClass_100")
212 | }
213 |
214 | func (d *Distribution) Create(callerReference string, domains []string, origin, path string, insecureOrigin bool, forwardedHeaders Headers, forwardCookies bool, tags map[string]string) (*cloudfront.Distribution, error) {
215 | distConfig := new(cloudfront.DistributionConfig)
216 | d.fillDistributionConfig(distConfig, origin, path, insecureOrigin,
217 | aws.String(callerReference), domains, forwardedHeaders.Strings(), forwardCookies)
218 | resp, err := d.Service.CreateDistributionWithTags(&cloudfront.CreateDistributionWithTagsInput{
219 | DistributionConfigWithTags: &cloudfront.DistributionConfigWithTags{
220 | DistributionConfig: distConfig,
221 | Tags: d.getTags(tags),
222 | },
223 | })
224 |
225 | if err != nil {
226 | return &cloudfront.Distribution{}, err
227 | }
228 |
229 | return resp.Distribution, nil
230 | }
231 |
232 | func (d *Distribution) Update(distId string, domains []string, origin, path string, insecureOrigin bool, forwardedHeaders Headers, forwardCookies bool) (*cloudfront.Distribution, error) {
233 | // Get the current distribution
234 | dist, err := d.Service.GetDistributionConfig(&cloudfront.GetDistributionConfigInput{
235 | Id: aws.String(distId),
236 | })
237 | if err != nil {
238 | return nil, err
239 | }
240 | d.fillDistributionConfig(dist.DistributionConfig, origin, path, insecureOrigin,
241 | dist.DistributionConfig.CallerReference, domains, forwardedHeaders.Strings(), forwardCookies)
242 |
243 | // Call the UpdateDistribution function
244 | resp, err := d.Service.UpdateDistribution(&cloudfront.UpdateDistributionInput{
245 | Id: aws.String(distId),
246 | IfMatch: dist.ETag,
247 | DistributionConfig: dist.DistributionConfig,
248 | })
249 | if err != nil {
250 | return &cloudfront.Distribution{}, err
251 | }
252 | return resp.Distribution, nil
253 | }
254 |
255 | func (d *Distribution) Get(distId string) (*cloudfront.Distribution, error) {
256 | resp, err := d.Service.GetDistribution(&cloudfront.GetDistributionInput{
257 | Id: aws.String(distId),
258 | })
259 | if err != nil {
260 | return &cloudfront.Distribution{}, err
261 | }
262 | return resp.Distribution, nil
263 | }
264 |
265 | func (d *Distribution) SetCertificateAndCname(distId, certId string, domains []string) error {
266 | resp, err := d.Service.GetDistributionConfig(&cloudfront.GetDistributionConfigInput{
267 | Id: aws.String(distId),
268 | })
269 | if err != nil {
270 | return err
271 | }
272 |
273 | aliases := d.getAliases(domains)
274 | DistributionConfig, ETag := resp.DistributionConfig, resp.ETag
275 | DistributionConfig.Aliases = aliases
276 |
277 | DistributionConfig.ViewerCertificate.Certificate = aws.String(certId)
278 | DistributionConfig.ViewerCertificate.IAMCertificateId = aws.String(certId)
279 | DistributionConfig.ViewerCertificate.CertificateSource = aws.String("iam")
280 | DistributionConfig.ViewerCertificate.SSLSupportMethod = aws.String("sni-only")
281 | DistributionConfig.ViewerCertificate.MinimumProtocolVersion = aws.String("TLSv1.2_2018")
282 | DistributionConfig.ViewerCertificate.CloudFrontDefaultCertificate = aws.Bool(false)
283 |
284 | _, err = d.Service.UpdateDistribution(&cloudfront.UpdateDistributionInput{
285 | Id: aws.String(distId),
286 | IfMatch: ETag,
287 | DistributionConfig: DistributionConfig,
288 | })
289 |
290 | return err
291 | }
292 | func (d *Distribution) SetCertificate(distId, certId string) error {
293 | resp, err := d.Service.GetDistributionConfig(&cloudfront.GetDistributionConfigInput{
294 | Id: aws.String(distId),
295 | })
296 | if err != nil {
297 | return err
298 | }
299 |
300 | DistributionConfig, ETag := resp.DistributionConfig, resp.ETag
301 |
302 | DistributionConfig.ViewerCertificate.Certificate = aws.String(certId)
303 | DistributionConfig.ViewerCertificate.IAMCertificateId = aws.String(certId)
304 | DistributionConfig.ViewerCertificate.CertificateSource = aws.String("iam")
305 | DistributionConfig.ViewerCertificate.SSLSupportMethod = aws.String("sni-only")
306 | DistributionConfig.ViewerCertificate.MinimumProtocolVersion = aws.String("TLSv1.2_2018")
307 | DistributionConfig.ViewerCertificate.CloudFrontDefaultCertificate = aws.Bool(false)
308 |
309 | _, err = d.Service.UpdateDistribution(&cloudfront.UpdateDistributionInput{
310 | Id: aws.String(distId),
311 | IfMatch: ETag,
312 | DistributionConfig: DistributionConfig,
313 | })
314 |
315 | return err
316 | }
317 |
318 | func (d *Distribution) Disable(distId string) error {
319 | resp, err := d.Service.GetDistributionConfig(&cloudfront.GetDistributionConfigInput{
320 | Id: aws.String(distId),
321 | })
322 | if err != nil {
323 | return err
324 | }
325 |
326 | DistributionConfig, ETag := resp.DistributionConfig, resp.ETag
327 | DistributionConfig.Enabled = aws.Bool(false)
328 |
329 | _, err = d.Service.UpdateDistribution(&cloudfront.UpdateDistributionInput{
330 | Id: aws.String(distId),
331 | IfMatch: ETag,
332 | DistributionConfig: DistributionConfig,
333 | })
334 |
335 | return err
336 | }
337 |
338 | func (d *Distribution) Delete(distId string) (bool, error) {
339 | resp, err := d.Service.GetDistribution(&cloudfront.GetDistributionInput{
340 | Id: aws.String(distId),
341 | })
342 | if err != nil {
343 | return false, err
344 | }
345 |
346 | if *resp.Distribution.Status != "Deployed" {
347 | return false, nil
348 | }
349 |
350 | _, err = d.Service.DeleteDistribution(&cloudfront.DeleteDistributionInput{
351 | Id: aws.String(distId),
352 | IfMatch: resp.ETag,
353 | })
354 |
355 | return err == nil, err
356 | }
357 |
358 | func (d *Distribution) ListDistributions(callback func(cloudfront.DistributionSummary) bool) error {
359 | return d.Service.ListDistributionsPages(
360 | &cloudfront.ListDistributionsInput{},
361 | func(page *cloudfront.ListDistributionsOutput, lastPage bool) bool {
362 | for _, v := range page.DistributionList.Items {
363 | // stop iteration if the callback tells us to
364 | if callback(*v) == false {
365 | return false
366 | }
367 | }
368 | return true
369 | },
370 | )
371 | }
372 |
373 | func getOriginProtocolPolicy(insecure bool) *string {
374 | if insecure {
375 | return aws.String("http-only")
376 | }
377 | return aws.String("https-only")
378 | }
379 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f h1:UrKzEwTgeiff9vxdrfdqxibzpWjxLnuXDI5m6z3GJAk=
2 | code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f/go.mod h1:sk5LnIjB/nIEU7yP5sDQExVm62wu0pBh3yrElngUisI=
3 | code.cloudfoundry.org/lager v1.0.1-0.20180322215153-25ee72f227fe h1:ZTTFXskoxGQm9q8h8LvTHxhHZBd2tgjMduXqPyvwz58=
4 | code.cloudfoundry.org/lager v1.0.1-0.20180322215153-25ee72f227fe/go.mod h1:O2sS7gKP3HM2iemG+EnwvyNQK7pTSC6Foi4QiMp9sSk=
5 | github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw=
6 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0=
7 | github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8=
8 | github.com/aws/aws-sdk-go v1.34.0 h1:brux2dRrlwCF5JhTL7MUT3WUwo9zfDHZZp3+g3Mvlmo=
9 | github.com/aws/aws-sdk-go v1.34.0/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
10 | github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
11 | github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40/go.mod h1:8rLXio+WjiTceGBHIoTvn60HIbs7Hm7bcHjyrSqYB9c=
12 | github.com/cloudfoundry-community/go-cfclient v0.0.0-20180323021324-b5f0f59f96d6 h1:vGMGy7i30QJNYNM7IE0UR85nOWI0DNYkJl5nQN0yquk=
13 | github.com/cloudfoundry-community/go-cfclient v0.0.0-20180323021324-b5f0f59f96d6/go.mod h1:awqQBZ30j+KR+Zt6pzRZmNVZZ2Q/05LXNQbCM1+frL4=
14 | github.com/cloudfoundry/gofileutils v0.0.0-20170111115228-4d0c80011a0f h1:3WbAZFyGnNjeYKm74CsuxXVlZWYibufTaRjH5H9mNpw=
15 | github.com/cloudfoundry/gofileutils v0.0.0-20170111115228-4d0c80011a0f/go.mod h1:Zv7xtAh/T/tmfZlxpESaWWiWOdiJz2GfbBYxImuI6T4=
16 | github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 h1:sDMmm+q/3+BukdIpxwO365v/Rbspp2Nt5XntgQRXq8Q=
17 | github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
18 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
19 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
20 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
21 | github.com/denisenkom/go-mssqldb v0.12.3 h1:pBSGx9Tq67pBOTLmxNuirNTeB8Vjmf886Kx+8Y+8shw=
22 | github.com/denisenkom/go-mssqldb v0.12.3/go.mod h1:k0mtMFOnU+AihqFxPMiF05rtiDrorD1Vrm1KEz5hxDo=
23 | github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
24 | github.com/drewolson/testflight v1.0.0 h1:jgA0pHcFIPnXoBmyFzrdoR2ka4UvReMDsjYc7Jcvl80=
25 | github.com/drewolson/testflight v1.0.0/go.mod h1:t9oKuuEohRGLb80SWX+uxJHuhX98B7HnojqtW+Ryq30=
26 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
27 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
28 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
29 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
30 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
31 | github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab h1:xveKWz2iaueeTaUgdetzel+U7exyigDYBryyVfV/rZk=
32 | github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
33 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
34 | github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
35 | github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
36 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
37 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
38 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
39 | github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
40 | github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
41 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
42 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
43 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
44 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
45 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
46 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
47 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
48 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
49 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
50 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
51 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
52 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
53 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
54 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
55 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
56 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
57 | github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA=
58 | github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
59 | github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
60 | github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f h1:9oNbS1z4rVpbnkHBdPZU4jo9bSmrLpII768arSyMFgk=
61 | github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
62 | github.com/gorilla/mux v1.6.1 h1:KOwqsTYZdeuMacU7CxjMNYEKeBvLbxW+psodrbcEa3A=
63 | github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
64 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
65 | github.com/jinzhu/gorm v1.9.1 h1:lDSDtsCt5AGGSKTs8AHlSDbbgif4G4+CKJ8ETBDVHTA=
66 | github.com/jinzhu/gorm v1.9.1/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo=
67 | github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a h1:eeaG9XMUvRBYXJi4pg1ZKM7nxc5AfXfojeLLW7O5J3k=
68 | github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
69 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
70 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
71 | github.com/jmcarp/lego v0.3.2-0.20170424160445-b4deb96f1082 h1:o6TKWNAleEPbbCL/hhtOMdat46AYKTk16jo1iStW6f0=
72 | github.com/jmcarp/lego v0.3.2-0.20170424160445-b4deb96f1082/go.mod h1:cvuscyCJp5Gko4t4iPAR/o3Ao5/mmrDLw6X3YzJ7Ks8=
73 | github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc=
74 | github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
75 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
76 | github.com/kelseyhightower/envconfig v1.3.0 h1:IvRS4f2VcIQy6j4ORGIf9145T/AsUB+oY8LyvN8BXNM=
77 | github.com/kelseyhightower/envconfig v1.3.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
78 | github.com/kr/pretty v0.0.0-20160823170715-cfb55aafdaf3/go.mod h1:Bvhd+E3laJ0AVkG0c9rmtZcnhV0HQ3+c3YxxqTvc/gA=
79 | github.com/kr/text v0.0.0-20160504234017-7cafcd837844/go.mod h1:sjUstKUATFIcff4qlB53Kml0wQPtJVc/3fWrmuUmcfA=
80 | github.com/lib/pq v0.0.0-20180325232643-a96442e255fc h1:BkJw+D/mckYibrbu+tj0CLrLahkEKP9dl81zlF6a4xQ=
81 | github.com/lib/pq v0.0.0-20180325232643-a96442e255fc/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
82 | github.com/martini-contrib/render v0.0.0-20150707142108-ec18f8345a11 h1:YFh+sjyJTMQSYjKwM4dFKhJPJC/wfo98tPUc17HdoYw=
83 | github.com/martini-contrib/render v0.0.0-20150707142108-ec18f8345a11/go.mod h1:Ah2dBMoxZEqk118as2T4u4fjfXarE0pPnMJaArZQZsI=
84 | github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
85 | github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
86 | github.com/miekg/dns v1.0.4 h1:Ec3LTJwwzqT1++63P12fhtdEbQhtPE7TBdD6rlhqrMM=
87 | github.com/miekg/dns v1.0.4/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
88 | github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
89 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
90 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
91 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
92 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
93 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
94 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
95 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
96 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
97 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
98 | github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU=
99 | github.com/onsi/gomega v1.27.7/go.mod h1:1p8OOlwo2iUUDsHnOrjE5UKYJ+e3W8eQ3qSlRahPmr4=
100 | github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
101 | github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
102 | github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw=
103 | github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
104 | github.com/pivotal-cf/brokerapi v1.0.0 h1:VXskMouDTRxtARiZ4ORLIZ6cxlzTSqfGa/yi2JLA+F0=
105 | github.com/pivotal-cf/brokerapi v1.0.0/go.mod h1:P+oA8NvkCTkq2t4DohBiyqQo69Ub15RKGcm/vKNP0gg=
106 | github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
107 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
108 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
109 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
110 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
111 | github.com/robfig/cron v1.0.0 h1:slmQxIUH6U9ruw4XoJ7C2pyyx4yYeiHx8S9pNootHsM=
112 | github.com/robfig/cron v1.0.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
113 | github.com/smartystreets/assertions v1.13.1 h1:Ef7KhSmjZcK6AVf9YbJdvPYG9avaF0ZxudX+ThRdWfU=
114 | github.com/smartystreets/goconvey v1.8.0 h1:Oi49ha/2MURE0WexF052Z0m+BNSGirfjg5RL+JXWq3w=
115 | github.com/smartystreets/goconvey v1.8.0/go.mod h1:EdX8jtrTIj26jmjCOVNMVSIYAtgexqXKHOXW2Dx9JLg=
116 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
117 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
118 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
119 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
120 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
121 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
122 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
123 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
124 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
125 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
126 | golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
127 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
128 | golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
129 | golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
130 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
131 | golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
132 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
133 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
134 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
135 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
136 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
137 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
138 | golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
139 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
140 | golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
141 | golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
142 | golang.org/x/oauth2 v0.0.0-20180314180239-fdc9e635145a h1:vnrksSpEGaRXtItKmKwom9Y/vzKSeiMPjj2C5TOVUdg=
143 | golang.org/x/oauth2 v0.0.0-20180314180239-fdc9e635145a/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
144 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
145 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
146 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
147 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
148 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
149 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
150 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
151 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
152 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
153 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
154 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
155 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
156 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
157 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
158 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
159 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
160 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
161 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
162 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
163 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
164 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
165 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
166 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
167 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
168 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
169 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
170 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
171 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
172 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
173 | google.golang.org/appengine v1.0.0 h1:dN4LljjBKVChsv0XCSI+zbyzdqrkEwX5LQFUMRSGqOc=
174 | google.golang.org/appengine v1.0.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
175 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
176 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
177 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
178 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
179 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
180 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
181 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
182 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
183 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
184 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
185 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
186 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
187 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
188 | gopkg.in/square/go-jose.v1 v1.1.1 h1:pA7KxQLcwADLRJ3lpUC+vIe4LCO8oRBMoq1HJoJhA3U=
189 | gopkg.in/square/go-jose.v1 v1.1.1/go.mod h1:QpYS+a4WhS+DTlyQIi6Ka7MS3SuR9a055rgXNEe6EiA=
190 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
191 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
192 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
193 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
194 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
195 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
196 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
197 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
198 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
199 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
200 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
201 |
--------------------------------------------------------------------------------
/models/models.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "crypto"
5 | "crypto/ecdsa"
6 | "crypto/rsa"
7 | "crypto/tls"
8 | "crypto/x509"
9 | "database/sql/driver"
10 | "encoding/json"
11 | "encoding/pem"
12 | "errors"
13 | "fmt"
14 | "io/ioutil"
15 | "math/rand"
16 | "net/http"
17 | "path"
18 | "strings"
19 | "time"
20 |
21 | "code.cloudfoundry.org/lager"
22 | "github.com/jinzhu/gorm"
23 | "github.com/pivotal-cf/brokerapi"
24 | "github.com/xenolf/lego/acme"
25 |
26 | "github.com/aws/aws-sdk-go/aws"
27 | "github.com/aws/aws-sdk-go/aws/session"
28 | "github.com/aws/aws-sdk-go/service/cloudfront"
29 | "github.com/aws/aws-sdk-go/service/iam"
30 | "github.com/aws/aws-sdk-go/service/s3"
31 |
32 | "github.com/cloud-gov/cf-cdn-service-broker/config"
33 | "github.com/cloud-gov/cf-cdn-service-broker/utils"
34 | )
35 |
36 | type State string
37 |
38 | const (
39 | Provisioning State = "provisioning"
40 | Provisioned = "provisioned"
41 | Deprovisioning = "deprovisioning"
42 | Deprovisioned = "deprovisioned"
43 | Failed = "failed"
44 | )
45 |
46 | var (
47 | helperLogger = lager.NewLogger("helper-logger")
48 | )
49 |
50 | // Value Marshal a `State` to a `string` when saving to the database
51 | func (s State) Value() (driver.Value, error) {
52 | return string(s), nil
53 | }
54 |
55 | // Scan Unmarshal an `interface{}` to a `State` when reading from the database
56 | func (s *State) Scan(value interface{}) error {
57 | switch value.(type) {
58 | case string:
59 | *s = State(value.(string))
60 | case []byte:
61 | *s = State(value.([]byte))
62 | default:
63 | err := fmt.Errorf("%s-is-incompatible", value)
64 | helperLogger.Session("state-scan").Error("scan-switch", err)
65 | return err
66 | }
67 | return nil
68 | }
69 |
70 | type UserData struct {
71 | gorm.Model
72 | Email string `gorm:"not null"`
73 | Reg []byte
74 | Key []byte
75 | }
76 |
77 | /*
78 | * LoadRandomUser The Let's Encrypt v1 acme API has shut down user creation to force users to adopt v2.
79 | * In an attempt to contiune using v1 while we develop a v2 compliant broker, we are replacing
80 | * calls to create a new user for each new domain registration with a method that fetches an existing user
81 | * from a pool of ids. The random selection of users from a pool aims to minimize the impact of the following rate limits:
82 | * - 300 Pending Authorizations per account
83 | * - Failed Validation limit of 5 failures per account, per hostname, per hour.
84 | */
85 | func LoadRandomUser(db *gorm.DB, userIDPool []string) (utils.User, error) {
86 | var user utils.User
87 | defer func() {
88 | if r := recover(); r != nil {
89 | return
90 | }
91 | }()
92 | userID := userIDPool[rand.Intn(len(userIDPool))]
93 |
94 | helperLogger.Session("load-random-user").Info("random-user-id", lager.Data{
95 | "userID": userID,
96 | })
97 |
98 | var userData UserData
99 |
100 | if err := db.Where("id = ?", userID).First(&userData).Error; err != nil {
101 | helperLogger.Session("load-random-user").Error("load-user-data", err)
102 | return user, err
103 | }
104 |
105 | user, err := LoadUser(userData)
106 | if err != nil {
107 | helperLogger.Session("load-random-user").Error("load-user", err)
108 | return user, err
109 | }
110 |
111 | return user, nil
112 | }
113 |
114 | func SaveUser(db *gorm.DB, user utils.User) (UserData, error) {
115 | var err error
116 | userData := UserData{Email: user.GetEmail()}
117 |
118 | lsession := helperLogger.Session("save-user")
119 |
120 | userData.Key, err = savePrivateKey(user.GetPrivateKey())
121 | if err != nil {
122 | lsession.Error("save-private-key", err)
123 | return userData, err
124 | }
125 | userData.Reg, err = json.Marshal(user)
126 | if err != nil {
127 | lsession.Error("json-marshal-user", err)
128 | return userData, err
129 | }
130 |
131 | if err := db.Save(&userData).Error; err != nil {
132 | lsession.Error("db-save-user", err)
133 | return userData, err
134 | }
135 |
136 | return userData, nil
137 | }
138 |
139 | func LoadUser(userData UserData) (utils.User, error) {
140 | var user utils.User
141 |
142 | lsession := helperLogger.Session("load-user")
143 |
144 | if err := json.Unmarshal(userData.Reg, &user); err != nil {
145 | lsession.Error("json-unmarshal-user-data", err)
146 | return user, err
147 | }
148 | key, err := loadPrivateKey(userData.Key)
149 | if err != nil {
150 | lsession.Error("load-private-key", err)
151 | return user, err
152 | }
153 | user.SetPrivateKey(key)
154 | return user, nil
155 | }
156 |
157 | // loadPrivateKey loads a PEM-encoded ECC/RSA private key from an array of bytes.
158 | func loadPrivateKey(keyBytes []byte) (crypto.PrivateKey, error) {
159 | keyBlock, _ := pem.Decode(keyBytes)
160 |
161 | switch keyBlock.Type {
162 | case "RSA PRIVATE KEY":
163 | return x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
164 | case "EC PRIVATE KEY":
165 | return x509.ParseECPrivateKey(keyBlock.Bytes)
166 | }
167 |
168 | return nil, errors.New("unknown private key type")
169 | }
170 |
171 | // savePrivateKey saves a PEM-encoded ECC/RSA private key to an array of bytes.
172 | func savePrivateKey(key crypto.PrivateKey) ([]byte, error) {
173 | var pemType string
174 | var keyBytes []byte
175 | switch key := key.(type) {
176 | case *ecdsa.PrivateKey:
177 | var err error
178 | pemType = "EC"
179 | keyBytes, err = x509.MarshalECPrivateKey(key)
180 | if err != nil {
181 | helperLogger.Session("save-private-key").Error("marshal-ec-private-key", err)
182 | return nil, err
183 | }
184 | case *rsa.PrivateKey:
185 | pemType = "RSA"
186 | keyBytes = x509.MarshalPKCS1PrivateKey(key)
187 | }
188 |
189 | pemKey := pem.Block{Type: pemType + " PRIVATE KEY", Bytes: keyBytes}
190 | return pem.EncodeToMemory(&pemKey), nil
191 | }
192 |
193 | type Route struct {
194 | gorm.Model
195 | InstanceId string `gorm:"not null;unique_index"`
196 | State State `gorm:"not null;index"`
197 | ChallengeJSON []byte
198 | DomainExternal string
199 | DomainInternal string
200 | DistId string
201 | Origin string
202 | Path string
203 | InsecureOrigin bool
204 | Certificate Certificate
205 | UserData UserData
206 | UserDataID int
207 | }
208 |
209 | func (r *Route) GetDomains() []string {
210 | return strings.Split(r.DomainExternal, ",")
211 | }
212 |
213 | func (r *Route) loadUser(db *gorm.DB) (utils.User, error) {
214 | var userData UserData
215 | if err := db.Model(r).Related(&userData).Error; err != nil {
216 | helperLogger.Session("route-load-user").Error("load-user-data", err)
217 | return utils.User{}, err
218 | }
219 |
220 | return LoadUser(userData)
221 | }
222 |
223 | type Certificate struct {
224 | gorm.Model
225 | RouteId uint
226 | Domain string
227 | CertURL string
228 | Certificate []byte
229 | Expires time.Time `gorm:"index"`
230 | }
231 |
232 | type RouteManagerIface interface {
233 | Create(instanceId, domain, origin, path string, insecureOrigin bool, forwardedHeaders utils.Headers, forwardCookies bool, tags map[string]string) (*Route, error)
234 | Update(instanceId string, domain, origin string, path string, insecureOrigin bool, forwardedHeaders utils.Headers, forwardCookies bool) error
235 | Get(instanceId string) (*Route, error)
236 | Poll(route *Route) error
237 | Disable(route *Route) error
238 | Renew(route *Route) error
239 | RenewAll()
240 | DeleteOrphanedCerts()
241 | GetDNSInstructions(route *Route) ([]string, error)
242 | }
243 |
244 | type RouteManager struct {
245 | logger lager.Logger
246 | iam utils.IamIface
247 | cloudFront utils.DistributionIface
248 | settings config.Settings
249 | db *gorm.DB
250 | }
251 |
252 | func NewManager(
253 | logger lager.Logger,
254 | iam utils.IamIface,
255 | cloudFront utils.DistributionIface,
256 | settings config.Settings,
257 | db *gorm.DB,
258 | ) RouteManager {
259 | return RouteManager{
260 | logger: logger,
261 | iam: iam,
262 | cloudFront: cloudFront,
263 | settings: settings,
264 | db: db,
265 | }
266 | }
267 |
268 | func (m *RouteManager) Create(instanceId, domain, origin, path string, insecureOrigin bool, forwardedHeaders utils.Headers, forwardCookies bool, tags map[string]string) (*Route, error) {
269 | route := &Route{
270 | InstanceId: instanceId,
271 | State: Provisioning,
272 | DomainExternal: domain,
273 | Origin: origin,
274 | Path: path,
275 | InsecureOrigin: insecureOrigin,
276 | }
277 |
278 | lsession := m.logger.Session("route-manager-create-route", lager.Data{
279 | "instance-id": instanceId,
280 | })
281 |
282 | user, err := LoadRandomUser(m.db, m.settings.UserIdPool)
283 | if err != nil {
284 | lsession.Error("load-random-user", err)
285 | return nil, err
286 | }
287 |
288 | clients, err := m.getClients(&user, m.settings)
289 | if err != nil {
290 | lsession.Error("get-clients", err)
291 | return nil, err
292 | }
293 |
294 | userData, err := SaveUser(m.db, user)
295 | if err != nil {
296 | lsession.Error("save-user", err)
297 | return nil, err
298 | }
299 |
300 | route.UserData = userData
301 |
302 | if err := m.ensureChallenges(route, clients[acme.HTTP01], false); err != nil {
303 | lsession.Error("ensure-challenges-http-01", err)
304 | return nil, err
305 | }
306 |
307 | dist, err := m.cloudFront.Create(instanceId, make([]string, 0), origin, path, insecureOrigin, forwardedHeaders, forwardCookies, tags)
308 | if err != nil {
309 | lsession.Error("create-cloudfront-instance", err)
310 | return nil, err
311 | }
312 |
313 | route.DomainInternal = *dist.DomainName
314 | route.DistId = *dist.Id
315 |
316 | if err := m.db.Create(route).Error; err != nil {
317 | lsession.Error("db-create-route", err)
318 | return nil, err
319 | }
320 |
321 | return route, nil
322 | }
323 |
324 | func (m *RouteManager) Get(instanceId string) (*Route, error) {
325 | route := Route{}
326 |
327 | lsession := m.logger.Session("route-manager-get")
328 |
329 | result := m.db.First(&route, Route{InstanceId: instanceId})
330 | if result.Error == nil {
331 | lsession.Error("db-get-first-route", result.Error)
332 | return &route, nil
333 | } else if result.RecordNotFound() {
334 | lsession.Error("db-record-not-found", brokerapi.ErrInstanceDoesNotExist)
335 | return nil, brokerapi.ErrInstanceDoesNotExist
336 | } else {
337 | lsession.Error("db-generic-error", result.Error)
338 | return nil, result.Error
339 | }
340 | }
341 |
342 | func (m *RouteManager) Update(instanceId, domain, origin string, path string, insecureOrigin bool, forwardedHeaders utils.Headers, forwardCookies bool) error {
343 | lsession := m.logger.Session("route-manager-update", lager.Data{
344 | "instance-id": instanceId,
345 | })
346 |
347 | // Get current route
348 | route, err := m.Get(instanceId)
349 | if err != nil {
350 | lsession.Error("get-route", err)
351 | return err
352 | }
353 |
354 | // When we update the CloudFront distribution we should use the old domains
355 | // until we have a valid certificate in IAM.
356 | // CloudFront gets updated when we receive new certificates during Poll
357 | oldDomainsForCloudFront := route.GetDomains()
358 |
359 | // Override any settings that are new or different.
360 | if domain != "" {
361 | route.DomainExternal = domain
362 | }
363 | if origin != "" {
364 | route.Origin = origin
365 | }
366 | if path != route.Path {
367 | route.Path = path
368 | }
369 | if insecureOrigin != route.InsecureOrigin {
370 | route.InsecureOrigin = insecureOrigin
371 | }
372 |
373 | // Update the distribution
374 | dist, err := m.cloudFront.Update(route.DistId, oldDomainsForCloudFront,
375 | route.Origin, route.Path, route.InsecureOrigin, forwardedHeaders, forwardCookies)
376 | if err != nil {
377 | lsession.Error("cloudfront-update", err)
378 | return err
379 | }
380 | route.State = Provisioning
381 |
382 | // Get the updated domain name and dist id.
383 | route.DomainInternal = *dist.DomainName
384 | route.DistId = *dist.Id
385 |
386 | if domain != "" {
387 | user, err := route.loadUser(m.db)
388 | if err != nil {
389 | lsession.Error("load-user", err)
390 | return err
391 | }
392 |
393 | clients, err := m.getClients(&user, m.settings)
394 | if err != nil {
395 | lsession.Error("get-clients", err)
396 | return err
397 | }
398 |
399 | route.ChallengeJSON = []byte("")
400 | if err := m.ensureChallenges(route, clients[acme.HTTP01], false); err != nil {
401 | lsession.Error("ensure-challenges", err)
402 | return err
403 | }
404 | }
405 |
406 | // Save the database.
407 | result := m.db.Save(route)
408 | if result.Error != nil {
409 | lsession.Error("db-save-route", err)
410 | return result.Error
411 | }
412 | return nil
413 | }
414 |
415 | func (m *RouteManager) Poll(r *Route) error {
416 | switch r.State {
417 | case Provisioning:
418 | return m.updateProvisioning(r)
419 | case Deprovisioning:
420 | return m.updateDeprovisioning(r)
421 | default:
422 | return nil
423 | }
424 | }
425 |
426 | func (m *RouteManager) Disable(r *Route) error {
427 | lsession := m.logger.Session("route-manager-disable", lager.Data{
428 | "instance-id": r.InstanceId,
429 | })
430 |
431 | if r.DistId != "" {
432 | err := m.cloudFront.Disable(r.DistId)
433 | if err != nil {
434 | lsession.Error("cloudfront-disable", err)
435 | return err
436 | }
437 | }
438 |
439 | r.State = Deprovisioning
440 | if err := m.db.Save(r).Error; err != nil {
441 | lsession.Error("db-save-error", err)
442 | }
443 |
444 | return nil
445 | }
446 |
447 | func (m *RouteManager) stillActive(r *Route) error {
448 |
449 | lsession := m.logger.Session("route-manager-still-active", lager.Data{
450 | "instance-id": r.InstanceId,
451 | })
452 |
453 | lsession.Info("starting-canary-check", lager.Data{
454 | "route": r,
455 | "instance-id": r.InstanceId,
456 | })
457 |
458 | session := session.New(aws.NewConfig().WithRegion(m.settings.AwsDefaultRegion))
459 |
460 | s3client := s3.New(session)
461 |
462 | target := path.Join(".well-known", "acme-challenge", "canary", r.InstanceId)
463 |
464 | input := s3.PutObjectInput{
465 | Bucket: aws.String(m.settings.Bucket),
466 | Key: aws.String(target),
467 | Body: strings.NewReader(r.InstanceId),
468 | }
469 |
470 | if m.settings.ServerSideEncryption != "" {
471 | input.ServerSideEncryption = aws.String(m.settings.ServerSideEncryption)
472 | }
473 |
474 | if _, err := s3client.PutObject(&input); err != nil {
475 | lsession.Error("s3-put-object", err)
476 | return err
477 | }
478 |
479 | insecureClient := &http.Client{
480 | Transport: &http.Transport{
481 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
482 | },
483 | }
484 |
485 | for _, domain := range r.GetDomains() {
486 | resp, err := insecureClient.Get("https://" + path.Join(domain, target))
487 | if err != nil {
488 | lsession.Error("insecure-client-get", err)
489 | return err
490 | }
491 |
492 | defer resp.Body.Close()
493 | body, err := ioutil.ReadAll(resp.Body)
494 | if err != nil {
495 | lsession.Error("read-response-body", err)
496 | return err
497 | }
498 |
499 | if string(body) != r.InstanceId {
500 | err := fmt.Errorf("Canary check failed for %s; expected %s, got %s", domain, r.InstanceId, string(body))
501 | lsession.Error("", err)
502 | return err
503 | }
504 | }
505 |
506 | return nil
507 | }
508 |
509 | func (m *RouteManager) Renew(r *Route) error {
510 | lsession := m.logger.Session("route-manager-renew", lager.Data{
511 | "instance-id": r.InstanceId,
512 | })
513 |
514 | err := m.stillActive(r)
515 | if err != nil {
516 | err := fmt.Errorf("Route is not active, skipping renewal: %v", err)
517 | lsession.Error("still-active", err)
518 | return err
519 | }
520 |
521 | var certRow Certificate
522 | err = m.db.Model(r).Related(&certRow, "Certificate").Error
523 | if err != nil {
524 | lsession.Error("db-find-related-cert", err)
525 | return err
526 | }
527 |
528 | user, err := r.loadUser(m.db)
529 | if err != nil {
530 | lsession.Error("db-load-user", err)
531 | return err
532 | }
533 |
534 | clients, err := m.getClients(&user, m.settings)
535 | if err != nil {
536 | lsession.Error("get-clients", err)
537 | return err
538 | }
539 |
540 | certResource, errs := clients[acme.HTTP01].ObtainCertificate(r.GetDomains(), true, nil, false)
541 | if len(errs) > 0 {
542 | err := fmt.Errorf("Error(s) obtaining certificate: %v", errs)
543 | lsession.Error("obtain-certificate", err)
544 | return err
545 | }
546 |
547 | expires, err := acme.GetPEMCertExpiration(certResource.Certificate)
548 | if err != nil {
549 | lsession.Error("get-pem-cert-expiry", err)
550 | return err
551 | }
552 |
553 | if err := m.deployCertificate(*r, certResource); err != nil {
554 | lsession.Error("deploy-certificate", err)
555 | return err
556 | }
557 |
558 | certRow.Domain = certResource.Domain
559 | certRow.CertURL = certResource.CertURL
560 | certRow.Certificate = certResource.Certificate
561 | certRow.Expires = expires
562 | if err := m.db.Save(&certRow).Error; err != nil {
563 | lsession.Error("db-save-cert", err)
564 | return err
565 | }
566 | return nil
567 | }
568 |
569 | func (m *RouteManager) DeleteOrphanedCerts() {
570 | // iterate over all distributions and record all certificates in-use by these distributions
571 | activeCerts := make(map[string]string)
572 |
573 | m.cloudFront.ListDistributions(func(distro cloudfront.DistributionSummary) bool {
574 | if distro.ViewerCertificate.IAMCertificateId != nil {
575 | activeCerts[*distro.ViewerCertificate.IAMCertificateId] = *distro.ARN
576 | }
577 | return true
578 | })
579 |
580 | // iterate over all certificates
581 | m.iam.ListCertificates(func(cert iam.ServerCertificateMetadata) bool {
582 |
583 | // delete any certs not attached to a distribution that are older than 24 hours
584 | _, active := activeCerts[*cert.ServerCertificateId]
585 | if !active && time.Since(*cert.UploadDate).Hours() > 24 {
586 | m.logger.Info("cleaning-orphaned-certificate", lager.Data{
587 | "cert": cert,
588 | })
589 |
590 | err := m.iam.DeleteCertificate(*cert.ServerCertificateName)
591 | if err != nil {
592 | m.logger.Error("iam-delete-certificate", err, lager.Data{
593 | "cert": cert,
594 | })
595 | }
596 | }
597 |
598 | return true
599 | })
600 | }
601 |
602 | func (m *RouteManager) RenewAll() {
603 | routes := []Route{}
604 |
605 | lsession := m.logger.Session("route-manager-renew-all")
606 |
607 | m.logger.Info("Looking for routes that are expiring soon")
608 |
609 | m.db.Having(
610 | "max(expires) < now() + interval '70 days'",
611 | ).Group(
612 | "routes.id",
613 | ).Where(
614 | "state = ?", string(Provisioned),
615 | ).Joins(
616 | "join certificates on routes.id = certificates.route_id",
617 | ).Find(&routes)
618 |
619 | m.logger.Info("routes-needing-renewal", lager.Data{
620 | "num-routes": len(routes),
621 | })
622 |
623 | for _, route := range routes {
624 | err := m.Renew(&route)
625 | if err != nil {
626 | lsession.Error("renew-error", err, lager.Data{
627 | "domain": route.DomainExternal,
628 | "origin": route.Origin,
629 | })
630 | } else {
631 | lsession.Info("renew-success", lager.Data{
632 | "domain": route.DomainExternal,
633 | "origin": route.Origin,
634 | })
635 | }
636 | }
637 | }
638 |
639 | func (m *RouteManager) getClients(user *utils.User, settings config.Settings) (map[acme.Challenge]*acme.Client, error) {
640 | session := session.New(aws.NewConfig().WithRegion(settings.AwsDefaultRegion))
641 |
642 | lsession := m.logger.Session("route-manager-get-clients")
643 |
644 | var err error
645 |
646 | clients := map[acme.Challenge]*acme.Client{}
647 | clients[acme.HTTP01], err = utils.NewClient(settings, user, s3.New(session), []acme.Challenge{acme.TLSSNI01, acme.DNS01})
648 | if err != nil {
649 | lsession.Error("new-client-http-builder", err)
650 | return clients, err
651 | }
652 | clients[acme.DNS01], err = utils.NewClient(settings, user, s3.New(session), []acme.Challenge{acme.TLSSNI01, acme.HTTP01})
653 | if err != nil {
654 | lsession.Error("new-client-dns-builder", err)
655 | return clients, err
656 | }
657 |
658 | return clients, nil
659 | }
660 |
661 | func (m *RouteManager) updateProvisioning(r *Route) error {
662 | lsession := m.logger.Session("route-manager-update-provisioning", lager.Data{
663 | "instance-id": r.InstanceId,
664 | })
665 |
666 | user, err := r.loadUser(m.db)
667 | if err != nil {
668 | lsession.Error("load-user", err)
669 | return err
670 | }
671 |
672 | clients, err := m.getClients(&user, m.settings)
673 | if err != nil {
674 | lsession.Error("get-clients", err)
675 | return err
676 | }
677 |
678 | // Handle provisioning instances created before DNS challenge
679 | if err := m.ensureChallenges(r, clients[acme.HTTP01], true); err != nil {
680 | lsession.Error("ensure-challenges", err)
681 | return err
682 | }
683 |
684 | if m.checkDistribution(r) {
685 | var challenges []acme.AuthorizationResource
686 | if err := json.Unmarshal(r.ChallengeJSON, &challenges); err != nil {
687 | lsession.Error("challenge-unmarshall", err)
688 | return err
689 | }
690 | if errs := m.solveChallenges(clients, challenges); len(errs) > 0 {
691 | errstr := fmt.Errorf("Error(s) solving challenges: %v", errs)
692 | lsession.Error("solve-challenges", errstr)
693 | return errstr
694 | }
695 |
696 | cert, err := clients[acme.HTTP01].RequestCertificate(challenges, true, nil, false)
697 | if err != nil {
698 | lsession.Error("request-certificate-http-01", err)
699 | return err
700 | }
701 |
702 | expires, err := acme.GetPEMCertExpiration(cert.Certificate)
703 | if err != nil {
704 | lsession.Error("get-cert-expiry", err)
705 | return err
706 | }
707 | if err := m.deployCertificate(*r, cert); err != nil {
708 | lsession.Error("deploy-certificate", err)
709 | r.State = Failed
710 | if dbErr := m.db.Save(r).Error; dbErr != nil {
711 | newErr := fmt.Errorf("error saving state to db: %s while processing error deploying certificate: %s", dbErr, err)
712 | return newErr
713 | }
714 | return err
715 | }
716 |
717 | certRow := Certificate{
718 | Domain: cert.Domain,
719 | CertURL: cert.CertURL,
720 | Certificate: cert.Certificate,
721 | Expires: expires,
722 | }
723 | if err := m.db.Create(&certRow).Error; err != nil {
724 | lsession.Error("db-create-cert", err)
725 | return err
726 | }
727 |
728 | r.State = Provisioned
729 | r.Certificate = certRow
730 | if err := m.db.Save(r).Error; err != nil {
731 | lsession.Error("db-save-cert", err)
732 | return err
733 | }
734 | return nil
735 | }
736 |
737 | lsession.Info("distribution-provisioning")
738 | return nil
739 | }
740 |
741 | func (m *RouteManager) updateDeprovisioning(r *Route) error {
742 | lsession := m.logger.Session("route-manager-update-deprovisioning")
743 |
744 | if r.DistId != "" {
745 | deleted, err := m.cloudFront.Delete(r.DistId)
746 | if err != nil {
747 | lsession.Error("cloudfront-delete", err)
748 | return err
749 | }
750 |
751 | if deleted {
752 | r.State = Deprovisioned
753 | if err := m.db.Save(r).Error; err != nil {
754 | lsession.Error("db-save-delete-state", err)
755 | }
756 | }
757 | }
758 |
759 | r.State = Deprovisioned
760 | if err := m.db.Save(r).Error; err != nil {
761 | lsession.Error("db-save-delete-state", err)
762 | }
763 |
764 | return nil
765 | }
766 |
767 | func (m *RouteManager) checkDistribution(r *Route) bool {
768 | dist, err := m.cloudFront.Get(r.DistId)
769 | if err != nil {
770 | m.logger.Session("check-distribution").Error("cloudfront-get", err)
771 | return false
772 | }
773 |
774 | return *dist.Status == "Deployed" && *dist.DistributionConfig.Enabled
775 | }
776 |
777 | func (m *RouteManager) solveChallenges(clients map[acme.Challenge]*acme.Client, challenges []acme.AuthorizationResource) map[string]error {
778 | errs := make(chan map[string]error)
779 |
780 | for _, client := range clients {
781 | go func(client *acme.Client) {
782 | errs <- client.SolveChallenges(challenges)
783 | }(client)
784 | }
785 |
786 | var failures map[string]error
787 | for challenge := range clients {
788 | failures = <-errs
789 | m.logger.Info("solve-challenges", lager.Data{
790 | "challenge": challenge,
791 | "failures": failures,
792 | })
793 | if len(failures) == 0 {
794 | return failures
795 | }
796 | }
797 |
798 | return failures
799 | }
800 |
801 | func (m *RouteManager) deployCertificate(route Route, cert acme.CertificateResource) error {
802 | lsession := m.logger.Session("deploy-certificate", lager.Data{
803 | "instance-id": route.InstanceId,
804 | })
805 |
806 | expires, err := acme.GetPEMCertExpiration(cert.Certificate)
807 | if err != nil {
808 | lsession.Error("get-cert-expiry", err)
809 | return err
810 | }
811 |
812 | name := fmt.Sprintf("cdn-route-%s-%s", route.InstanceId, expires.Format("2006-01-02_15-04-05"))
813 |
814 | m.logger.Info("Uploading certificate to IAM", lager.Data{"name": name})
815 |
816 | certId, err := m.iam.UploadCertificate(name, cert)
817 | if err != nil {
818 | lsession.Error("iam-upload-certificate", err)
819 | return err
820 | }
821 |
822 | return m.cloudFront.SetCertificateAndCname(route.DistId, certId, route.GetDomains())
823 | }
824 |
825 | func (m *RouteManager) ensureChallenges(route *Route, client *acme.Client, update bool) error {
826 | lsession := m.logger.Session("ensure-challenges", lager.Data{
827 | "instance-id": route.InstanceId,
828 | })
829 |
830 | if len(route.ChallengeJSON) == 0 {
831 | challenges, errs := client.GetChallenges(route.GetDomains())
832 | if len(errs) > 0 {
833 | err := fmt.Errorf("Error(s) getting challenges: %v", errs)
834 | lsession.Error("get-challenges", err)
835 | return err
836 | }
837 |
838 | var err error
839 | route.ChallengeJSON, err = json.Marshal(challenges)
840 | if err != nil {
841 | lsession.Error("json-marshal-challenge", err)
842 | return err
843 | }
844 |
845 | if update {
846 | err := m.db.Save(route).Error
847 | lsession.Error("db-save-route-challenge", err)
848 | return err
849 | }
850 | return nil
851 | }
852 |
853 | return nil
854 | }
855 |
856 | func (m *RouteManager) GetDNSInstructions(route *Route) ([]string, error) {
857 | var instructions []string
858 | var challenges []acme.AuthorizationResource
859 |
860 | lsession := m.logger.Session("get-dns-instructions", lager.Data{
861 | "instance-id": route.InstanceId,
862 | })
863 |
864 | user, err := route.loadUser(m.db)
865 | if err != nil {
866 | lsession.Error("load-user", err)
867 | return instructions, err
868 | }
869 |
870 | if err := json.Unmarshal(route.ChallengeJSON, &challenges); err != nil {
871 | lsession.Error("json-unmarshal-challenge", err)
872 | return instructions, err
873 | }
874 | for _, auth := range challenges {
875 | for _, challenge := range auth.Body.Challenges {
876 | if challenge.Type == acme.DNS01 {
877 | keyAuth, err := acme.GetKeyAuthorization(challenge.Token, user.GetPrivateKey())
878 | if err != nil {
879 | lsession.Error("get-key-authorization", err)
880 | return instructions, err
881 | }
882 | fqdn, value, ttl := acme.DNS01Record(auth.Domain, keyAuth)
883 | instructions = append(instructions, fmt.Sprintf("name: %s, value: %s, ttl: %d", fqdn, value, ttl))
884 | }
885 | }
886 | }
887 | return instructions, nil
888 | }
889 |
--------------------------------------------------------------------------------