From 33fca0976f95eac7ddce424080db9e381ec7388e Mon Sep 17 00:00:00 2001
From: Nick Thomas <nick@bytemark.co.uk>
Date: Thu, 5 May 2016 13:16:32 +0100
Subject: [PATCH 1/6] Allow -listen-http, -listen-https and -listen-proxy to be
 given more than once

Per issue #13, sometimes you want to listen on more than one port for each type
of listener. This commit adds support for that.
---
 app.go               | 25 +++++++++++++------------
 app_config.go        |  6 +++---
 daemon.go            | 25 +++++++++++++++----------
 main.go              | 28 +++++++++++++++-------------
 multi_string_flag.go | 22 ++++++++++++++++++++++
 5 files changed, 68 insertions(+), 38 deletions(-)
 create mode 100644 multi_string_flag.go

diff --git a/app.go b/app.go
index aa0438f..35b661c 100644
--- a/app.go
+++ b/app.go
@@ -85,39 +85,40 @@ func (a *theApp) UpdateDomains(domains domains) {
 func (a *theApp) Run() {
 	var wg sync.WaitGroup
 
-	if a.ListenHTTP != 0 {
+	// Listen for HTTP
+	for _, fd := range a.ListenHTTP {
 		wg.Add(1)
-		go func() {
+		go func(fd uintptr) {
 			defer wg.Done()
-			err := listenAndServe(a.ListenHTTP, a.ServeHTTP, a.HTTP2, nil)
+			err := listenAndServe(fd, a.ServeHTTP, a.HTTP2, nil)
 			if err != nil {
 				log.Fatal(err)
 			}
-		}()
+		}(fd)
 	}
 
 	// Listen for HTTPS
-	if a.ListenHTTPS != 0 {
+	for _, fd := range a.ListenHTTPS {
 		wg.Add(1)
-		go func() {
+		go func(fd uintptr) {
 			defer wg.Done()
-			err := listenAndServeTLS(a.ListenHTTPS, a.RootCertificate, a.RootKey, a.ServeHTTP, a.ServeTLS, a.HTTP2)
+			err := listenAndServeTLS(fd, a.RootCertificate, a.RootKey, a.ServeHTTP, a.ServeTLS, a.HTTP2)
 			if err != nil {
 				log.Fatal(err)
 			}
-		}()
+		}(fd)
 	}
 
 	// Listen for HTTP proxy requests
-	if a.ListenProxy != 0 {
+	for _, fd := range a.ListenProxy {
 		wg.Add(1)
-		go func() {
+		go func(fd uintptr) {
 			defer wg.Done()
-			err := listenAndServe(a.ListenProxy, a.ServeProxy, a.HTTP2, nil)
+			err := listenAndServe(fd, a.ServeProxy, a.HTTP2, nil)
 			if err != nil {
 				log.Fatal(err)
 			}
-		}()
+		}(fd)
 	}
 
 	go watchDomains(a.Domain, a.UpdateDomains, time.Second)
diff --git a/app_config.go b/app_config.go
index 8da2437..7ffa1e7 100644
--- a/app_config.go
+++ b/app_config.go
@@ -6,9 +6,9 @@ type appConfig struct {
 	RootCertificate []byte
 	RootKey         []byte
 
-	ListenHTTP  uintptr
-	ListenHTTPS uintptr
-	ListenProxy uintptr
+	ListenHTTP  []uintptr
+	ListenHTTPS []uintptr
+	ListenProxy []uintptr
 
 	HTTP2        bool
 	RedirectHTTP bool
diff --git a/daemon.go b/daemon.go
index de6ce4a..69feed1 100644
--- a/daemon.go
+++ b/daemon.go
@@ -56,15 +56,20 @@ func daemonReexec(uid, gid uint, args ...string) (cmd *exec.Cmd, err error) {
 	return
 }
 
-func daemonUpdateFd(cmd *exec.Cmd, fd *uintptr) {
-	if *fd == 0 {
-		return
-	}
+func daemonUpdateFd(cmd *exec.Cmd, fd uintptr) (childFd uintptr) {
+	file := os.NewFile(fd, "[socket]")
 
-	file := os.NewFile(*fd, "[socket]")
 	// we add 3 since, we have a 3 predefined FDs
-	*fd = uintptr(3 + len(cmd.ExtraFiles))
+	childFd = uintptr(3 + len(cmd.ExtraFiles))
 	cmd.ExtraFiles = append(cmd.ExtraFiles, file)
+
+	return
+}
+
+func daemonUpdateFds(cmd *exec.Cmd, fds []uintptr) {
+	for idx, fd := range fds {
+		fds[idx] = daemonUpdateFd(cmd, fd)
+	}
 }
 
 func killProcess(cmd *exec.Cmd) {
@@ -196,10 +201,10 @@ func daemonize(config appConfig, uid, gid uint) {
 	defer configWriter.Close()
 	cmd.ExtraFiles = append(cmd.ExtraFiles, configReader)
 
-	// Create a new file and store the FD
-	daemonUpdateFd(cmd, &config.ListenHTTP)
-	daemonUpdateFd(cmd, &config.ListenHTTPS)
-	daemonUpdateFd(cmd, &config.ListenProxy)
+	// Create a new file and store the FD for each listener
+	daemonUpdateFds(cmd, config.ListenHTTP)
+	daemonUpdateFds(cmd, config.ListenHTTPS)
+	daemonUpdateFds(cmd, config.ListenProxy)
 
 	// Start the process
 	if err = cmd.Start(); err != nil {
diff --git a/main.go b/main.go
index ba7a25d..62d28e0 100644
--- a/main.go
+++ b/main.go
@@ -3,7 +3,6 @@ package main
 import (
 	"flag"
 	"log"
-	"net"
 	"os"
 	"strings"
 )
@@ -16,9 +15,12 @@ var REVISION = "HEAD"
 
 func appMain() {
 	var showVersion = flag.Bool("version", false, "Show version")
-	var listenHTTP = flag.String("listen-http", "", "The address to listen for HTTP requests")
-	var listenHTTPS = flag.String("listen-https", "", "The address to listen for HTTPS requests")
-	var listenProxy = flag.String("listen-proxy", "", "The address to listen for proxy requests")
+	var listenHTTP, listenHTTPS, listenProxy MultiStringFlag
+
+	flag.Var(&listenHTTP, "listen-http", "The address(es) to listen on for HTTP requests")
+	flag.Var(&listenHTTPS, "listen-https", "The address(es) to listen on for HTTPS requests")
+	flag.Var(&listenProxy, "listen-proxy", "The address(es) to listen on for proxy requests")
+
 	var pagesRootCert = flag.String("root-cert", "", "The default path to file certificate to serve static pages")
 	var pagesRootKey = flag.String("root-key", "", "The default path to file certificate to serve static pages")
 	var redirectHTTP = flag.Bool("redirect-http", true, "Serve the pages under HTTP")
@@ -53,22 +55,22 @@ func appMain() {
 		config.RootKey = readFile(*pagesRootKey)
 	}
 
-	if *listenHTTP != "" {
-		var l net.Listener
-		l, config.ListenHTTP = createSocket(*listenHTTP)
+	for _, addr := range listenHTTP {
+		l, fd := createSocket(addr)
 		defer l.Close()
+		config.ListenHTTP = append(config.ListenHTTP, fd)
 	}
 
-	if *listenHTTPS != "" {
-		var l net.Listener
-		l, config.ListenHTTPS = createSocket(*listenHTTPS)
+	for _, addr := range listenHTTPS {
+		l, fd := createSocket(addr)
 		defer l.Close()
+		config.ListenHTTPS = append(config.ListenHTTPS, fd)
 	}
 
-	if *listenProxy != "" {
-		var l net.Listener
-		l, config.ListenProxy = createSocket(*listenProxy)
+	for _, addr := range listenProxy {
+		l, fd := createSocket(addr)
 		defer l.Close()
+		config.ListenProxy = append(config.ListenProxy, fd)
 	}
 
 	if *daemonUID != 0 || *daemonGID != 0 {
diff --git a/multi_string_flag.go b/multi_string_flag.go
new file mode 100644
index 0000000..ba6d92e
--- /dev/null
+++ b/multi_string_flag.go
@@ -0,0 +1,22 @@
+package main
+
+import (
+	"strings"
+)
+
+// MultiStringFlag implements the flag.Value interface and allows a string flag
+// to be specified multiple times on the command line.
+//
+// e.g.: -listen-http 127.0.0.1:80 -listen-http [::1]:80
+type MultiStringFlag []string
+
+// String returns the list of parameters joined with a commas (",")
+func (s *MultiStringFlag) String() string {
+	return strings.Join(*s, ",")
+}
+
+// Set appends the value to the list of parameters
+func (s *MultiStringFlag) Set(value string) error {
+	*s = append(*s, value)
+	return nil
+}
-- 
GitLab


From 6b6eed8c9c165d7e22b07daa486a8f5496ef722b Mon Sep 17 00:00:00 2001
From: Nick Thomas <nick@bytemark.co.uk>
Date: Sat, 7 May 2016 12:36:13 +0100
Subject: [PATCH 2/6] Add acceptance tests for the gitlab-pages binary.

---
 .gitlab-ci.yml     |   1 +
 Makefile           |   3 +
 acceptance_test.go | 146 +++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 150 insertions(+)
 create mode 100644 acceptance_test.go

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 205463f..2943390 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -11,3 +11,4 @@ before_script:
 test:
   script:
   - make verify
+  - make acceptance
diff --git a/Makefile b/Makefile
index 3ad0447..3fdf749 100644
--- a/Makefile
+++ b/Makefile
@@ -41,5 +41,8 @@ test:
 	go get golang.org/x/tools/cmd/cover
 	go test ./... -cover
 
+acceptance: gitlab-pages
+	go test ./... -run-acceptance-tests
+
 docker:
 	docker run --rm -it -v ${PWD}:/go/src/pages -w /go/src/pages golang:1.5 /bin/bash
diff --git a/acceptance_test.go b/acceptance_test.go
new file mode 100644
index 0000000..37c5ea4
--- /dev/null
+++ b/acceptance_test.go
@@ -0,0 +1,146 @@
+package main
+
+import (
+	"flag"
+	"fmt"
+	"github.com/stretchr/testify/assert"
+	"net"
+	"net/http"
+	"os"
+	"os/exec"
+	"testing"
+	"time"
+)
+
+// TODO: Use TCP port 0 everywhere to avoid conflicts. The binary could output
+// the actual port (and type of listener) for us to read in place of the
+// hardcoded values below.
+type listenSpec struct {
+	Type string
+	Host string
+	Port string
+}
+
+func (l listenSpec) URL(suffix string) string {
+	scheme := "http"
+	if l.Type == "https" {
+		scheme = "https"
+	}
+
+	return fmt.Sprintf("%s://%s/%s", scheme, l.JoinHostPort(), suffix)
+}
+
+// Returns only once the TCP server is open
+func (l listenSpec) WaitUntilListening() {
+	for {
+		conn, _ := net.Dial("tcp", l.JoinHostPort())
+		if conn != nil {
+			conn.Close()
+			break
+		}
+	}
+}
+
+func (l listenSpec) JoinHostPort() string {
+	return net.JoinHostPort(l.Host, l.Port)
+}
+
+var shouldRun = flag.Bool("run-acceptance-tests", false, "Run the acceptance tests?")
+var pagesBinary = flag.String("gitlab-pages-binary", "./gitlab-pages", "Path to the gitlab-pages binary")
+
+var listenHTTP = []listenSpec{
+	{"http", "127.0.0.1", "3700"},
+	{"http", "::1", "3700"},
+}
+
+// TODO: listenHTTPS will require TLS configuration
+var listenHTTPS = []listenSpec{}
+
+var listenProxy = []listenSpec{
+	{"proxy", "127.0.0.1", "3702"},
+	{"proxy", "::1", "37002"},
+}
+
+var listeners = append(listenHTTP, append(listenHTTPS, listenProxy...)...)
+
+// TODO: start one pages process for all tests?
+func runPages(t *testing.T) *exec.Cmd {
+	if !*shouldRun {
+		t.Log("Acceptance tests disabled")
+		t.SkipNow()
+	}
+
+	if _, err := os.Stat(*pagesBinary); err != nil {
+		t.Logf("Can't find Gitlab Pages binary (%s): %s", *pagesBinary, err)
+		t.FailNow()
+	}
+
+	var args []string
+	for _, spec := range listeners {
+		args = append(args, "-listen-"+spec.Type, spec.JoinHostPort())
+	}
+
+	cmd := exec.Command(*pagesBinary, args...)
+	t.Logf("Running %s %v", *pagesBinary, args)
+	cmd.Start()
+
+	// Wait for all TCP servers to be open. Even with this, gitlab-pages
+	// will sometimes return 404 if a HTTP request comes in before it has
+	// updated its set of domains. This usually takes < 1ms, hence the sleep
+	// for now. Without it, intermittent failures occur.
+	//
+	// TODO: replace this with explicit status from the pages binary
+	// TODO: fix the first-request race
+	for _, spec := range listeners {
+		spec.WaitUntilListening()
+	}
+	time.Sleep(50 * time.Millisecond)
+
+	return cmd
+}
+
+func stopPages(cmd *exec.Cmd) {
+	cmd.Process.Kill()
+	cmd.Process.Wait()
+}
+
+func getPage(t *testing.T, spec listenSpec, host, urlsuffix string) (*http.Response, error) {
+	url := spec.URL(urlsuffix)
+	req, err := http.NewRequest("GET", url, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	req.Host = host
+
+	t.Logf("curl -H'Host: %s' %s", host, url)
+	return http.DefaultClient.Do(req)
+}
+
+func TestUnknownHostReturnsNotFound(t *testing.T) {
+	cmd := runPages(t)
+	defer stopPages(cmd)
+
+	for _, spec := range listeners {
+		rsp, err := getPage(t, spec, "invalid.invalid", "")
+
+		if assert.NoError(t, err) {
+			rsp.Body.Close()
+			assert.Equal(t, http.StatusNotFound, rsp.StatusCode)
+		}
+	}
+}
+
+func TestKnownHostReturns200(t *testing.T) {
+	cmd := runPages(t)
+	defer stopPages(cmd)
+
+	for _, spec := range listeners {
+		rsp, err := getPage(t, spec, "group.gitlab-example.com", "project/")
+
+		if assert.NoError(t, err) {
+			rsp.Body.Close()
+			assert.Equal(t, http.StatusOK, rsp.StatusCode)
+		}
+	}
+}
-- 
GitLab


From e5d0aa9a1f8301b4e4359c0fd5cbc9d7d7ca3ffe Mon Sep 17 00:00:00 2001
From: Nick Thomas <nick@bytemark.co.uk>
Date: Sat, 7 May 2016 13:01:46 +0100
Subject: [PATCH 3/6] Add documentation for providing -listen-http and friends
 multiple times.

---
 README.md | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/README.md b/README.md
index cf508cf..087a215 100644
--- a/README.md
+++ b/README.md
@@ -60,6 +60,18 @@ go build
 sudo ./gitlab-pages -listen-http ":80" -pages-root path/to/gitlab/shared/pages -pages-domain example.com -daemon-uid 1000 -daemon-gid 1000
 ```
 
+### Listen on multiple ports
+
+Each of the `listen-http`, `listen-https` and `listen-proxy` arguments can be provided multiple times. Gitlab Pages will accept connections to them all.
+
+Example:
+```
+go build
+./gitlab-pages -listen-http "10.0.0.1:8080" -listen-https "[fd00::1]:8080" -pages-root path/to/gitlab/shared/pages -pages-domain example.com
+```
+
+This is most useful in dual-stack environments (IPv4+IPv6) where both Gitlab Pages and another HTTP server have to co-exist on the same server.
+
 ### License
 
 MIT
-- 
GitLab


From 411cab5081471655a6472ca5c750f51eb03dabcf Mon Sep 17 00:00:00 2001
From: Nick Thomas <nick@bytemark.co.uk>
Date: Sat, 7 May 2016 14:40:56 +0100
Subject: [PATCH 4/6] Add a simple test for MultiStringFlag

---
 acceptance_test.go        | 128 +++++-----------------------
 domain_test.go            |  35 +-------
 helpers_test.go           | 172 ++++++++++++++++++++++++++++++++++++++
 multi_string_flag_test.go |  19 +++++
 4 files changed, 216 insertions(+), 138 deletions(-)
 create mode 100644 multi_string_flag_test.go

diff --git a/acceptance_test.go b/acceptance_test.go
index 37c5ea4..2e36acd 100644
--- a/acceptance_test.go
+++ b/acceptance_test.go
@@ -2,127 +2,42 @@ package main
 
 import (
 	"flag"
-	"fmt"
 	"github.com/stretchr/testify/assert"
-	"net"
 	"net/http"
-	"os"
-	"os/exec"
 	"testing"
-	"time"
 )
 
-// TODO: Use TCP port 0 everywhere to avoid conflicts. The binary could output
-// the actual port (and type of listener) for us to read in place of the
-// hardcoded values below.
-type listenSpec struct {
-	Type string
-	Host string
-	Port string
-}
-
-func (l listenSpec) URL(suffix string) string {
-	scheme := "http"
-	if l.Type == "https" {
-		scheme = "https"
-	}
-
-	return fmt.Sprintf("%s://%s/%s", scheme, l.JoinHostPort(), suffix)
-}
-
-// Returns only once the TCP server is open
-func (l listenSpec) WaitUntilListening() {
-	for {
-		conn, _ := net.Dial("tcp", l.JoinHostPort())
-		if conn != nil {
-			conn.Close()
-			break
-		}
-	}
-}
-
-func (l listenSpec) JoinHostPort() string {
-	return net.JoinHostPort(l.Host, l.Port)
-}
-
 var shouldRun = flag.Bool("run-acceptance-tests", false, "Run the acceptance tests?")
 var pagesBinary = flag.String("gitlab-pages-binary", "./gitlab-pages", "Path to the gitlab-pages binary")
 
-var listenHTTP = []listenSpec{
-	{"http", "127.0.0.1", "3700"},
-	{"http", "::1", "3700"},
-}
-
-// TODO: listenHTTPS will require TLS configuration
-var listenHTTPS = []listenSpec{}
-
-var listenProxy = []listenSpec{
-	{"proxy", "127.0.0.1", "3702"},
+// TODO: Use TCP port 0 everywhere to avoid conflicts. The binary could output
+// the actual port (and type of listener) for us to read in place of the
+// hardcoded values below.
+var listeners = []ListenSpec{
+	{"http", "127.0.0.1", "37000"},
+	{"http", "::1", "37000"},
+	{"https", "127.0.0.1", "37001"},
+	{"https", "::1", "37001"},
+	{"proxy", "127.0.0.1", "37002"},
 	{"proxy", "::1", "37002"},
 }
 
-var listeners = append(listenHTTP, append(listenHTTPS, listenProxy...)...)
-
-// TODO: start one pages process for all tests?
-func runPages(t *testing.T) *exec.Cmd {
-	if !*shouldRun {
-		t.Log("Acceptance tests disabled")
-		t.SkipNow()
-	}
-
-	if _, err := os.Stat(*pagesBinary); err != nil {
-		t.Logf("Can't find Gitlab Pages binary (%s): %s", *pagesBinary, err)
-		t.FailNow()
+func skipUnlessEnabled(t *testing.T) {
+	if *shouldRun {
+		return
 	}
 
-	var args []string
-	for _, spec := range listeners {
-		args = append(args, "-listen-"+spec.Type, spec.JoinHostPort())
-	}
-
-	cmd := exec.Command(*pagesBinary, args...)
-	t.Logf("Running %s %v", *pagesBinary, args)
-	cmd.Start()
-
-	// Wait for all TCP servers to be open. Even with this, gitlab-pages
-	// will sometimes return 404 if a HTTP request comes in before it has
-	// updated its set of domains. This usually takes < 1ms, hence the sleep
-	// for now. Without it, intermittent failures occur.
-	//
-	// TODO: replace this with explicit status from the pages binary
-	// TODO: fix the first-request race
-	for _, spec := range listeners {
-		spec.WaitUntilListening()
-	}
-	time.Sleep(50 * time.Millisecond)
-
-	return cmd
-}
-
-func stopPages(cmd *exec.Cmd) {
-	cmd.Process.Kill()
-	cmd.Process.Wait()
-}
-
-func getPage(t *testing.T, spec listenSpec, host, urlsuffix string) (*http.Response, error) {
-	url := spec.URL(urlsuffix)
-	req, err := http.NewRequest("GET", url, nil)
-	if err != nil {
-		return nil, err
-	}
-
-	req.Host = host
-
-	t.Logf("curl -H'Host: %s' %s", host, url)
-	return http.DefaultClient.Do(req)
+	t.Log("Acceptance tests disabled")
+	t.SkipNow()
 }
 
 func TestUnknownHostReturnsNotFound(t *testing.T) {
-	cmd := runPages(t)
-	defer stopPages(cmd)
+	skipUnlessEnabled(t)
+	teardown := RunPagesProcess(t, *pagesBinary, listeners)
+	defer teardown()
 
 	for _, spec := range listeners {
-		rsp, err := getPage(t, spec, "invalid.invalid", "")
+		rsp, err := GetPageFromListener(t, spec, "invalid.invalid", "")
 
 		if assert.NoError(t, err) {
 			rsp.Body.Close()
@@ -132,11 +47,12 @@ func TestUnknownHostReturnsNotFound(t *testing.T) {
 }
 
 func TestKnownHostReturns200(t *testing.T) {
-	cmd := runPages(t)
-	defer stopPages(cmd)
+	skipUnlessEnabled(t)
+	teardown := RunPagesProcess(t, *pagesBinary, listeners)
+	defer teardown()
 
 	for _, spec := range listeners {
-		rsp, err := getPage(t, spec, "group.gitlab-example.com", "project/")
+		rsp, err := GetPageFromListener(t, spec, "group.gitlab-example.com", "project/")
 
 		if assert.NoError(t, err) {
 			rsp.Body.Close()
diff --git a/domain_test.go b/domain_test.go
index 9032aee..7298c22 100644
--- a/domain_test.go
+++ b/domain_test.go
@@ -139,38 +139,9 @@ func TestDomainCertificate(t *testing.T) {
 		Group:   "group",
 		Project: "project2",
 		Config: &domainConfig{
-			Domain: "test.domain.com",
-			Certificate: `-----BEGIN CERTIFICATE-----
-MIICWDCCAcGgAwIBAgIJAMyzCfoGEwVNMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
-BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
-aWRnaXRzIFB0eSBMdGQwHhcNMTYwMjExMTcxNzM2WhcNMjYwMjA4MTcxNzM2WjBF
-MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
-ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB
-gQC2ZSzGIlv2zRsELkmEA1JcvIdsFv80b0NbBftewDAQRuyPlhGNifFx6v7+3O1F
-5+f+So43N0QbdrHu11K+ZuXNc6hUy0ofG/eRqXniGZEn8paUdQ98sWsbWelBDNeg
-WX4FQomynjyxbG+3IuJR5UHoLWhrJ9+pbPrT915eObbaTQIDAQABo1AwTjAdBgNV
-HQ4EFgQUGAhDu+gfckg4IkHRCQWBn4ltKV4wHwYDVR0jBBgwFoAUGAhDu+gfckg4
-IkHRCQWBn4ltKV4wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOBgQAaGx5U
-JRW5HC9dXADLf9OnmJRqWi3VNXEXWFk5XgHKc1z7KIBWMsdj+1gzm5ltRO7hkHw9
-bx6jQKZBRiUxyqTFw9Ywrk1fYFAxk8hxuqVYcGdonImJarHZTdVMBRWut9+EZBVm
-77eYbz2zASNpy++QIg85YgQum9uqREClHRBsxQ==
------END CERTIFICATE-----`,
-			Key: `-----BEGIN PRIVATE KEY-----
-MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBALZlLMYiW/bNGwQu
-SYQDUly8h2wW/zRvQ1sF+17AMBBG7I+WEY2J8XHq/v7c7UXn5/5Kjjc3RBt2se7X
-Ur5m5c1zqFTLSh8b95GpeeIZkSfylpR1D3yxaxtZ6UEM16BZfgVCibKePLFsb7ci
-4lHlQegtaGsn36ls+tP3Xl45ttpNAgMBAAECgYAAqZFmDs3isY/9jeV6c0CjUZP0
-UokOubC27eihyXTjOj61rsfVicC0tzPB3S+HZ3YyODcYAD1hFCdFRMbqJhmDiewK
-5GfATdNQeNARCfJdjYn57NKaXm7rc4C3so1YfxTL6k9QGJgTcybXiClQPDrhkZt3
-YLIeeJbY3OppLqjzgQJBAN5AzwyUqX5eQIUncQKcFY0PIjfFTku62brT7hq+TlqY
-1B6n3GUtIX+tyYg1qusy4KUUSzMslXJubHsxKanGqZ0CQQDSFwzK7KEYoZol5OMX
-mRsavc3iXmmEkkNRdNb1R4UqrlasPeeIeO1CfoD2RPcQhZCwFtR8xS8u6X9ncfC4
-qyxxAkAhpQvy6ppR7/Cyd4sLCxfUF8NlT/APVMTbHHQCBmcUHeiWj3C0vEVC78r/
-XKh4HGaXdt//ajNhdEflykZ1VgadAkB6Zh934mEA3rXWOgHsb7EQ5WAb8HF9YVGD
-FZVfFaoJ8cRhWTeZlQp14Qn1cLyYjZh8XvCxOJiCtlsZw5JBpMihAkBA6ltWb+aZ
-EBjC8ZRwZE+cAzmxaYPSs2J7JhS7X7H7Ax7ShhvHI4br3nqf00H4LkvtcHkn5d9G
-MwE1w2r4Deww
------END PRIVATE KEY-----`,
+			Domain:      "test.domain.com",
+			Certificate: CertificateFixture,
+			Key:         KeyFixture,
 		},
 	}
 
diff --git a/helpers_test.go b/helpers_test.go
index 5d56489..3bab08e 100644
--- a/helpers_test.go
+++ b/helpers_test.go
@@ -1,8 +1,17 @@
 package main
 
 import (
+	"crypto/tls"
+	"fmt"
+	"github.com/stretchr/testify/assert"
+	"io/ioutil"
 	"log"
+	"net"
+	"net/http"
 	"os"
+	"os/exec"
+	"testing"
+	"time"
 )
 
 var chdirSet = false
@@ -19,3 +28,166 @@ func setUpTests() {
 		chdirSet = true
 	}
 }
+
+// The HTTPS certificate isn't signed by anyone. This http client is set up
+// so it can talk to servers using it.
+var InsecureHTTPSClient = &http.Client{
+	Transport: &http.Transport{
+		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+	},
+}
+
+var CertificateFixture = `-----BEGIN CERTIFICATE-----
+MIICWDCCAcGgAwIBAgIJAMyzCfoGEwVNMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
+BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
+aWRnaXRzIFB0eSBMdGQwHhcNMTYwMjExMTcxNzM2WhcNMjYwMjA4MTcxNzM2WjBF
+MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
+ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB
+gQC2ZSzGIlv2zRsELkmEA1JcvIdsFv80b0NbBftewDAQRuyPlhGNifFx6v7+3O1F
+5+f+So43N0QbdrHu11K+ZuXNc6hUy0ofG/eRqXniGZEn8paUdQ98sWsbWelBDNeg
+WX4FQomynjyxbG+3IuJR5UHoLWhrJ9+pbPrT915eObbaTQIDAQABo1AwTjAdBgNV
+HQ4EFgQUGAhDu+gfckg4IkHRCQWBn4ltKV4wHwYDVR0jBBgwFoAUGAhDu+gfckg4
+IkHRCQWBn4ltKV4wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOBgQAaGx5U
+JRW5HC9dXADLf9OnmJRqWi3VNXEXWFk5XgHKc1z7KIBWMsdj+1gzm5ltRO7hkHw9
+bx6jQKZBRiUxyqTFw9Ywrk1fYFAxk8hxuqVYcGdonImJarHZTdVMBRWut9+EZBVm
+77eYbz2zASNpy++QIg85YgQum9uqREClHRBsxQ==
+-----END CERTIFICATE-----`
+
+var KeyFixture = `-----BEGIN PRIVATE KEY-----
+MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBALZlLMYiW/bNGwQu
+SYQDUly8h2wW/zRvQ1sF+17AMBBG7I+WEY2J8XHq/v7c7UXn5/5Kjjc3RBt2se7X
+Ur5m5c1zqFTLSh8b95GpeeIZkSfylpR1D3yxaxtZ6UEM16BZfgVCibKePLFsb7ci
+4lHlQegtaGsn36ls+tP3Xl45ttpNAgMBAAECgYAAqZFmDs3isY/9jeV6c0CjUZP0
+UokOubC27eihyXTjOj61rsfVicC0tzPB3S+HZ3YyODcYAD1hFCdFRMbqJhmDiewK
+5GfATdNQeNARCfJdjYn57NKaXm7rc4C3so1YfxTL6k9QGJgTcybXiClQPDrhkZt3
+YLIeeJbY3OppLqjzgQJBAN5AzwyUqX5eQIUncQKcFY0PIjfFTku62brT7hq+TlqY
+1B6n3GUtIX+tyYg1qusy4KUUSzMslXJubHsxKanGqZ0CQQDSFwzK7KEYoZol5OMX
+mRsavc3iXmmEkkNRdNb1R4UqrlasPeeIeO1CfoD2RPcQhZCwFtR8xS8u6X9ncfC4
+qyxxAkAhpQvy6ppR7/Cyd4sLCxfUF8NlT/APVMTbHHQCBmcUHeiWj3C0vEVC78r/
+XKh4HGaXdt//ajNhdEflykZ1VgadAkB6Zh934mEA3rXWOgHsb7EQ5WAb8HF9YVGD
+FZVfFaoJ8cRhWTeZlQp14Qn1cLyYjZh8XvCxOJiCtlsZw5JBpMihAkBA6ltWb+aZ
+EBjC8ZRwZE+cAzmxaYPSs2J7JhS7X7H7Ax7ShhvHI4br3nqf00H4LkvtcHkn5d9G
+MwE1w2r4Deww
+-----END PRIVATE KEY-----`
+
+func CreateHTTPSFixtureFiles(t *testing.T) (key string, cert string) {
+	keyfile, err := ioutil.TempFile("", "https-fixture")
+	assert.NoError(t, err)
+	key = keyfile.Name()
+	keyfile.Close()
+
+	certfile, err := ioutil.TempFile("", "https-fixture")
+	assert.NoError(t, err)
+	cert = certfile.Name()
+	certfile.Close()
+
+	assert.NoError(t, ioutil.WriteFile(key, []byte(KeyFixture), 0644))
+	assert.NoError(t, ioutil.WriteFile(cert, []byte(CertificateFixture), 0644))
+
+	return keyfile.Name(), certfile.Name()
+}
+
+// ListenSpec is used to point at a gitlab-pages http server, preserving the
+// type of port it is (http, https, proxy)
+type ListenSpec struct {
+	Type string
+	Host string
+	Port string
+}
+
+func (l ListenSpec) URL(suffix string) string {
+	scheme := "http"
+	if l.Type == "https" {
+		scheme = "https"
+	}
+
+	return fmt.Sprintf("%s://%s/%s", scheme, l.JoinHostPort(), suffix)
+}
+
+// Returns only once this spec points at a working TCP server
+func (l ListenSpec) WaitUntilListening() {
+	for {
+		conn, _ := net.Dial("tcp", l.JoinHostPort())
+		if conn != nil {
+			conn.Close()
+			break
+		}
+	}
+}
+
+func (l ListenSpec) JoinHostPort() string {
+	return net.JoinHostPort(l.Host, l.Port)
+}
+
+// RunPagesProcess will start a gitlab-pages process with the specified listeners
+// and return a function you can call to shut it down again. Use
+// GetPageFromProcess to do a HTTP GET against a listener.
+//
+// If run as root via sudo, the gitlab-pages process will drop privileges
+func RunPagesProcess(t *testing.T, pagesPath string, listeners []ListenSpec) (teardown func()) {
+	var tempfiles []string
+	var args []string
+	var hasHTTPS bool
+
+	_, err := os.Stat(pagesPath)
+	assert.NoError(t, err)
+
+	for _, spec := range listeners {
+		args = append(args, "-listen-"+spec.Type, spec.JoinHostPort())
+
+		if spec.Type == "https" {
+			hasHTTPS = true
+		}
+	}
+
+	if hasHTTPS {
+		key, cert := CreateHTTPSFixtureFiles(t)
+		tempfiles = []string{key, cert}
+		args = append(args, "-root-key", key, "-root-cert", cert)
+	}
+
+	if os.Geteuid() == 0 && os.Getenv("SUDO_UID") != "" && os.Getenv("SUDO_GID") != "" {
+		t.Log("Pages process will drop privileges")
+		args = append(args, "-daemon-uid", os.Getenv("SUDO_UID"), "-daemon-gid", os.Getenv("SUDO_GID"))
+	}
+
+	cmd := exec.Command(pagesPath, args...)
+	t.Logf("Running %s %v", pagesPath, args)
+	cmd.Start()
+
+	// Wait for all TCP servers to be open. Even with this, gitlab-pages
+	// will sometimes return 404 if a HTTP request comes in before it has
+	// updated its set of domains. This usually takes < 1ms, hence the sleep
+	// for now. Without it, intermittent failures occur.
+	//
+	// TODO: replace this with explicit status from the pages binary
+	// TODO: fix the first-request race
+	for _, spec := range listeners {
+		spec.WaitUntilListening()
+	}
+	time.Sleep(50 * time.Millisecond)
+
+	return func() {
+		cmd.Process.Kill()
+		cmd.Process.Wait()
+		for _, tempfile := range tempfiles {
+			os.Remove(tempfile)
+		}
+	}
+}
+
+// Does an insecure HTTP GET against the listener specified, setting a fake
+// Host: and constructing the URL from the listener and the URL suffix.
+func GetPageFromListener(t *testing.T, spec ListenSpec, host, urlsuffix string) (*http.Response, error) {
+	url := spec.URL(urlsuffix)
+	req, err := http.NewRequest("GET", url, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	req.Host = host
+
+	t.Logf("curl -H'Host: %s' %s", host, url)
+
+	return InsecureHTTPSClient.Do(req)
+}
diff --git a/multi_string_flag_test.go b/multi_string_flag_test.go
new file mode 100644
index 0000000..bbbdea3
--- /dev/null
+++ b/multi_string_flag_test.go
@@ -0,0 +1,19 @@
+package main
+
+import (
+	"flag"
+	"github.com/stretchr/testify/assert"
+	"testing"
+)
+
+func TestMultiStringFlagAppendsOnSet(t *testing.T) {
+	var concrete MultiStringFlag
+	var iface flag.Value
+
+	iface = &concrete
+
+	assert.NoError(t, iface.Set("foo"))
+	assert.NoError(t, iface.Set("bar"))
+
+	assert.Equal(t, MultiStringFlag{"foo", "bar"}, concrete)
+}
-- 
GitLab


From c2f0c476029bbe5741b16eee17d7b7781d3fcb09 Mon Sep 17 00:00:00 2001
From: Nick Thomas <nick@bytemark.co.uk>
Date: Sat, 7 May 2016 16:07:04 +0100
Subject: [PATCH 5/6] Update CHANGELOG and VERSION

[ci skip]
---
 CHANGELOG | 3 +++
 VERSION   | 2 +-
 2 files changed, 4 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG b/CHANGELOG
index f8e8304..c5becd1 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,6 @@
+v 0.2.5
+- Allow listen-http, listen-https and listen-proxy to be specified multiple times
+
 v 0.2.4
 - Fix predefined 404 page content-type
 
diff --git a/VERSION b/VERSION
index abd4105..3a4036f 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.2.4
+0.2.5
-- 
GitLab


From b8ece518d72e0e1bfef8b75cc6180298c5154ec3 Mon Sep 17 00:00:00 2001
From: Nick Thomas <me@ur.gs>
Date: Fri, 9 Sep 2016 00:02:31 +0100
Subject: [PATCH 6/6] Add the gitlab-pages binary to gitignore

---
 .gitignore | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.gitignore b/.gitignore
index a89eb3c..83058e7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
 # Created by .ignore support plugin (hsz.mobi)
 shared/pages/.update
+/gitlab-pages
-- 
GitLab