From f71d810d683b5703caf9b03108c1f8d06703ff9c Mon Sep 17 00:00:00 2001 From: Brandur Date: Wed, 18 Jul 2018 14:58:08 -0700 Subject: [PATCH] Support simultaneous HTTP and HTTPS I was a little hasty in adding support for HTTPS before in that when we're developing with stripe-mock locally, we need to have a setup that supports both HTTP *and* HTTPS because some libraries will be using one and some libraries will be using the other. I've added a number of command line options that allow stripe-mock to be booted so that it's listening for both HTTP and HTTPS (and HTTP/2) simultaneously. It's added a little bit of complication, so I've added a number of checks for incompatible options and more documentation to the README on how to use them all. `12111` will remain our default port for HTTP, and `12112` will become our default port for HTTPS. Libraries that want HTTP will lock to `12111` by default and those that need HTTPS will lock to `12112`. See the README for full details, but here are some basic usages with the options: ``` sh stripe-mock stripe-mock -https stripe-mock -port 80 stripe-mock -https -unix /tmp/stripe-mock-secure.sock stripe-mock -http-port 80 -https-port 443 ``` --- Dockerfile | 3 +- README.md | 70 +++++++++++--- main.go | 268 +++++++++++++++++++++++++++++++++++++++------------ main_test.go | 163 +++++++++++++++++++++++++++++++ 4 files changed, 428 insertions(+), 76 deletions(-) diff --git a/Dockerfile b/Dockerfile index 01a066d6..4907ddfe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,5 +25,6 @@ FROM alpine:latest RUN apk --no-cache add ca-certificates WORKDIR / COPY --from=builder /go/src/github.com/stripe/stripe-mock/stripe-mock . -ENTRYPOINT /stripe-mock +ENTRYPOINT ["/stripe-mock", "-http-port", "12111", "-https-port", "12112"] EXPOSE 12111 +EXPOSE 12112 diff --git a/README.md b/README.md index 2c3fcc11..0c7dcdd6 100644 --- a/README.md +++ b/README.md @@ -12,47 +12,85 @@ the API reference][apiref]. ## Usage -Get it from Homebrew or download it [from the releases page][releases]: +If you have Go installed, you can install the basic binary with: ``` sh -brew install stripe/stripe-mock/stripe-mock +go get -u github.com/stripe/stripe-mock +``` -# start a stripe-mock service at login -brew services start stripe-mock +With no arguments, stripe-mock will listen with HTTP on its default port of +`12111`: -# upgrade if you already have it -brew upgrade stripe-mock +``` sh +stripe-mock ``` -Or if you have Go installed you can build it: +It can also be activated with HTTPS (and by extension support for HTTP/2) using +the `-https` flag (the default port changes to `12112` for HTTPS): ``` sh -go get -u github.com/stripe/stripe-mock +stripe-mock -https ``` -Run it: +For either HTTP or HTTPS, the port can be specified with either the `PORT` +environmental variable or the `-port` option (the latter is preferred if both +are present): ``` sh -stripe-mock +stripe-mock -port 12111 +``` + +It can also listen on a Unix socket: + +``` sh +stripe-mock -https -unix /tmp/stripe-mock-secure.sock +``` + +It can be configured to receive both HTTP _and_ HTTPS by using the +`-http-port`, `-http-unix`, `-https-port`, and `-https-unix` options (and note +that these cannot be mixed with any of the basic options above): + +``` sh +stripe-mock -http-port 12111 -https-port 12112 ``` -Or with docker: +### Homebrew + +Get it from Homebrew or download it [from the releases page][releases]: + +``` sh +brew install stripe/stripe-mock/stripe-mock + +# start a stripe-mock service at login +brew services start stripe-mock + +# upgrade if you already have it +brew upgrade stripe-mock +``` + +The Homebrew service listens on port `12111` for HTTP and `12112` for HTTPS and +HTTP/2. + +### Docker + ``` sh # build docker build . -t stripe-mock # run -docker run -p 12111:12111 stripe-mock +docker run -p 12111:12112 stripe-mock ``` -Then from another terminal: +The default Docker `ENTRYPOINT` listens on port `12111` for HTTP and `12112` +for HTTPS and HTTP/2. + +### Sample request + +After you've started stripe-mock, you can try a sample request against it: ``` sh curl -i http://localhost:12111/v1/charges -H "Authorization: Bearer sk_test_123" ``` -By default, stripe-mock runs on port 12111, but is configurable with the -`-port` option. - ## Development ### Testing diff --git a/main.go b/main.go index 9c24eaad..5b344fa0 100644 --- a/main.go +++ b/main.go @@ -18,7 +18,8 @@ import ( "gopkg.in/yaml.v2" ) -const defaultPort = 12111 +const defaultPortHTTP = 12111 +const defaultPortHTTPS = 12112 // verbose tracks whether the program is operating in verbose mode var verbose bool @@ -30,41 +31,45 @@ var version = "master" // --- func main() { - var https bool - var showVersion bool - var port int - var fixturesPath string - var specPath string - var unix string - - flag.BoolVar(&https, "https", false, "Run with HTTPS (which also allows HTTP/2 to be activated)") - flag.IntVar(&port, "port", 0, "Port to listen on (also respects PORT from environment)") - flag.StringVar(&fixturesPath, "fixtures", "", "Path to fixtures to use instead of bundled version") - flag.StringVar(&specPath, "spec", "", "Path to OpenAPI spec to use instead of bundled version") - flag.StringVar(&unix, "unix", "", "Unix socket to listen on") + var options options + + flag.BoolVar(&options.http, "http", false, "Run with HTTP") + flag.IntVar(&options.httpPort, "http-port", 0, "Port to listen on for HTTP") + flag.StringVar(&options.httpUnixSocket, "http-unix", "", "Unix socket to listen on for HTTP") + + flag.BoolVar(&options.https, "https", false, "Run with HTTPS (which also allows HTTP/2 to be activated)") + flag.IntVar(&options.httpsPort, "https-port", 0, "Port to listen on for HTTPS") + flag.StringVar(&options.httpsUnixSocket, "https-unix", "", "Unix socket to listen on for HTTPS") + + flag.IntVar(&options.port, "port", 0, "Port to listen on (also respects PORT from environment)") + flag.StringVar(&options.fixturesPath, "fixtures", "", "Path to fixtures to use instead of bundled version") + flag.StringVar(&options.specPath, "spec", "", "Path to OpenAPI spec to use instead of bundled version") + flag.StringVar(&options.unixSocket, "unix", "", "Unix socket to listen on") flag.BoolVar(&verbose, "verbose", false, "Enable verbose mode") - flag.BoolVar(&showVersion, "version", false, "Show version and exit") + flag.BoolVar(&options.showVersion, "version", false, "Show version and exit") + flag.Parse() fmt.Printf("stripe-mock %s\n", version) - if showVersion || len(flag.Args()) == 1 && flag.Arg(0) == "version" { + if options.showVersion || len(flag.Args()) == 1 && flag.Arg(0) == "version" { return } - if unix != "" && port != 0 { + err := options.checkConflictingOptions() + if err != nil { flag.Usage() - abort("Specify only one of -port or -unix\n") + abort(fmt.Sprintf("Invalid options: %v", err)) } // For both spec and fixtures stripe-mock will by default load data from // internal assets compiled into the binary, but either one can be // overridden with a -spec or -fixtures argument and a path to a file. - stripeSpec, err := getSpec(specPath) + stripeSpec, err := getSpec(options.specPath) if err != nil { abort(err.Error()) } - fixtures, err := getFixtures(fixturesPath) + fixtures, err := getFixtures(options.fixturesPath) if err != nil { abort(err.Error()) } @@ -77,12 +82,33 @@ func main() { http.HandleFunc("/", stub.HandleRequest) - listener, err := getListener(port, unix) + httpListener, err := options.getHTTPListener() if err != nil { abort(err.Error()) } - if https { + // Only start HTTP if requested (it's the default, but it won't start if + // HTTPS is explicitly requested instead) + if httpListener != nil { + server := http.Server{} + + // Listen in a new Goroutine that so we can start a simultaneous HTTPS + // listener if necessary. + go func() { + err := server.Serve(httpListener) + if err != nil { + abort(err.Error()) + } + }() + } + + httpsListener, err := options.getNonSecureHTTPSListener() + if err != nil { + abort(err.Error()) + } + + // Only start HTTPS if requested + if httpsListener != nil { // Our self-signed certificate is bundled up using go-bindata so that // it stays easy to distribute stripe-mock as a standalone binary with // no other dependencies. @@ -103,15 +129,141 @@ func main() { } server := http.Server{TLSConfig: tlsConfig} - tlsListener := tls.NewListener(listener, tlsConfig) - server.Serve(tlsListener) - } else { - server := http.Server{} - server.Serve(listener) + tlsListener := tls.NewListener(httpsListener, tlsConfig) + + go func() { + err := server.Serve(tlsListener) + if err != nil { + abort(err.Error()) + } + }() } + + // Block forever. The serve Goroutines above will abort the program if + // either of them fails. + select {} } -// --- +// +// Private types +// + +// options is a container for the command line options passed to stripe-mock. +type options struct { + fixturesPath string + + http bool + httpPort int + httpUnixSocket string + + https bool + httpsPort int + httpsUnixSocket string + + port int + showVersion bool + specPath string + unixSocket string +} + +func (o *options) checkConflictingOptions() error { + if o.unixSocket != "" && o.port != 0 { + return fmt.Errorf("Please specify only one of -port or -unix") + } + + // + // HTTP + // + + if o.http && (o.httpUnixSocket != "" || o.httpPort != 0) { + return fmt.Errorf("Please don't specify -http when using -http-port or -http-unix") + } + + if (o.unixSocket != "" || o.port != 0) && (o.httpUnixSocket != "" || o.httpPort != 0) { + return fmt.Errorf("Please don't specify -port or -unix when using -http-port or -http-unix") + } + + if o.httpUnixSocket != "" && o.httpPort != 0 { + return fmt.Errorf("Please specify only one of -http-port or -http-unix") + } + + // + // HTTPS + // + + if o.https && (o.httpsUnixSocket != "" || o.httpsPort != 0) { + return fmt.Errorf("Please don't specify -https when using -https-port or -https-unix") + } + + if (o.unixSocket != "" || o.port != 0) && (o.httpsUnixSocket != "" || o.httpsPort != 0) { + return fmt.Errorf("Please don't specify -port or -unix when using -https-port or -https-unix") + } + + if o.httpsUnixSocket != "" && o.httpsPort != 0 { + return fmt.Errorf("Please specify only one of -https-port or -https-unix") + } + + return nil +} + +// getHTTPListener gets a listener on a port or unix socket depending on the +// options provided. If HTTP should not be enabled, it returns nil. +func (o *options) getHTTPListener() (net.Listener, error) { + if o.httpPort != 0 { + return getPortListener(o.httpPort) + } + + if o.httpUnixSocket != "" { + return getUnixSocketListener(o.httpUnixSocket) + } + + // HTTP is active by default, but only if HTTPS is *not* active + if o.https || o.httpsPort != 0 || o.httpsUnixSocket != "" { + return nil, nil + } + + if o.port != 0 { + return getPortListener(o.port) + } + + if o.unixSocket != "" { + return getUnixSocketListener(o.unixSocket) + } + + return getPortListenerDefault(defaultPortHTTP) +} + +// getNonSecureHTTPSListener gets a basic listener on a port or unix socket +// depending on the options provided. Its return listener must still be wrapped +// in a TLSListener. If HTTPS should not be enabled, it returns nil. +func (o *options) getNonSecureHTTPSListener() (net.Listener, error) { + if o.httpsPort != 0 { + return getPortListener(o.httpsPort) + } + + if o.httpsUnixSocket != "" { + return getUnixSocketListener(o.httpsUnixSocket) + } + + // HTTPS is disabled by default + if !o.https { + return nil, nil + } + + if o.port != 0 { + return getPortListener(o.port) + } + + if o.unixSocket != "" { + return getUnixSocketListener(o.unixSocket) + } + + return getPortListenerDefault(defaultPortHTTPS) +} + +// +// Private functions +// func abort(message string) { fmt.Fprintf(os.Stderr, message) @@ -134,20 +286,6 @@ func getTLSCertificate() (tls.Certificate, error) { return tls.X509KeyPair(cert, key) } -// getEnvPortOrDefault gets a port from the environment variable `PORT` or -// falls back to the default port (`defaultPort`) if one was not present. -func getEnvPortOrDefault() (int, error) { - if os.Getenv("PORT") != "" { - port, err := strconv.Atoi(os.Getenv("PORT")) - if err != nil { - return 0, err - } - return port, nil - } - - return defaultPort, nil -} - func getFixtures(fixturesPath string) (*spec.Fixtures, error) { var data []byte var err error @@ -191,29 +329,31 @@ func getFixtures(fixturesPath string) (*spec.Fixtures, error) { return &fixtures, nil } -func getListener(port int, unix string) (net.Listener, error) { - var err error - var listener net.Listener - - if unix != "" { - listener, err = net.Listen("unix", unix) - fmt.Printf("Listening on unix socket %v\n", unix) - } else { - if port == 0 { - port, err = getEnvPortOrDefault() - if err != nil { - return nil, err - } - } - listener, err = net.Listen("tcp", ":"+strconv.Itoa(port)) - fmt.Printf("Listening on port %v\n", port) - } +func getPortListener(port int) (net.Listener, error) { + listener, err := net.Listen("tcp", ":"+strconv.Itoa(port)) if err != nil { - return nil, fmt.Errorf("Error listening on socket: %v\n", err) + return nil, fmt.Errorf("Error listening on port: %v\n", err) } + + fmt.Printf("Listening on port: %v\n", port) return listener, nil } +// getPortListenerDefault gets a port listener based on the environment +// variable `PORT`, or falls back to a listener on the default port +// (`defaultPort`) if one was not present. +func getPortListenerDefault(defaultPort int) (net.Listener, error) { + if os.Getenv("PORT") != "" { + envPort, err := strconv.Atoi(os.Getenv("PORT")) + if err != nil { + return nil, err + } + return getPortListener(envPort) + } + + return getPortListener(defaultPort) +} + func getSpec(specPath string) (*spec.Spec, error) { var data []byte var err error @@ -245,3 +385,13 @@ func getSpec(specPath string) (*spec.Spec, error) { } return &stripeSpec, nil } + +func getUnixSocketListener(unixSocket string) (net.Listener, error) { + listener, err := net.Listen("unix", unixSocket) + if err != nil { + return nil, fmt.Errorf("Error listening on socket: %v\n", err) + } + + fmt.Printf("Listening on Unix socket: %v\n", unixSocket) + return listener, nil +} diff --git a/main_test.go b/main_test.go index 92dd4f3b..84efa85e 100644 --- a/main_test.go +++ b/main_test.go @@ -2,7 +2,10 @@ package main import ( "encoding/json" + "fmt" + "testing" + assert "github.com/stretchr/testify/require" "github.com/stripe/stripe-mock/spec" ) @@ -172,3 +175,163 @@ func initTestSpec() { }, } } + +func TestCheckConflictingOptions(t *testing.T) { + // + // Valid sets of options (not exhaustive, but included quite a few standard invocations) + // + + { + options := &options{ + http: true, + } + err := options.checkConflictingOptions() + assert.NoError(t, err) + } + + { + options := &options{ + https: true, + } + err := options.checkConflictingOptions() + assert.NoError(t, err) + } + + { + options := &options{ + https: true, + port: 12111, + } + err := options.checkConflictingOptions() + assert.NoError(t, err) + } + + { + options := &options{ + httpPort: 12111, + httpsPort: 12112, + } + err := options.checkConflictingOptions() + assert.NoError(t, err) + } + + { + options := &options{ + httpUnixSocket: "/tmp/stripe-mock.sock", + httpsUnixSocket: "/tmp/stripe-mock-secure.sock", + } + err := options.checkConflictingOptions() + assert.NoError(t, err) + } + + // + // Non-specific + // + + { + options := &options{ + port: 12111, + unixSocket: "/tmp/stripe-mock.sock", + } + err := options.checkConflictingOptions() + assert.Equal(t, fmt.Errorf("Please specify only one of -port or -unix"), err) + } + + // + // HTTP + // + + { + options := &options{ + http: true, + httpPort: 12111, + } + err := options.checkConflictingOptions() + assert.Equal(t, fmt.Errorf("Please don't specify -http when using -http-port or -http-unix"), err) + } + + { + options := &options{ + http: true, + httpUnixSocket: "/tmp/stripe-mock.sock", + } + err := options.checkConflictingOptions() + assert.Equal(t, fmt.Errorf("Please don't specify -http when using -http-port or -http-unix"), err) + } + + { + options := &options{ + port: 12111, + httpPort: 12111, + } + err := options.checkConflictingOptions() + assert.Equal(t, fmt.Errorf("Please don't specify -port or -unix when using -http-port or -http-unix"), err) + } + + { + options := &options{ + unixSocket: "/tmp/stripe-mock.sock", + httpUnixSocket: "/tmp/stripe-mock.sock", + } + err := options.checkConflictingOptions() + assert.Equal(t, fmt.Errorf("Please don't specify -port or -unix when using -http-port or -http-unix"), err) + } + + { + options := &options{ + httpPort: 12111, + httpUnixSocket: "/tmp/stripe-mock.sock", + } + err := options.checkConflictingOptions() + assert.Equal(t, fmt.Errorf("Please specify only one of -http-port or -http-unix"), err) + } + + // + // HTTPS + // + + { + options := &options{ + https: true, + httpsPort: 12111, + } + err := options.checkConflictingOptions() + assert.Equal(t, fmt.Errorf("Please don't specify -https when using -https-port or -https-unix"), err) + } + + { + options := &options{ + https: true, + httpsUnixSocket: "/tmp/stripe-mock.sock", + } + err := options.checkConflictingOptions() + assert.Equal(t, fmt.Errorf("Please don't specify -https when using -https-port or -https-unix"), err) + } + + { + options := &options{ + port: 12111, + httpsPort: 12111, + } + err := options.checkConflictingOptions() + assert.Equal(t, fmt.Errorf("Please don't specify -port or -unix when using -https-port or -https-unix"), err) + } + + { + options := &options{ + unixSocket: "/tmp/stripe-mock.sock", + httpsUnixSocket: "/tmp/stripe-mock.sock", + } + err := options.checkConflictingOptions() + assert.Equal(t, fmt.Errorf("Please don't specify -port or -unix when using -https-port or -https-unix"), err) + } + + { + options := &options{ + httpsPort: 12111, + httpsUnixSocket: "/tmp/stripe-mock.sock", + } + err := options.checkConflictingOptions() + assert.Equal(t, fmt.Errorf("Please specify only one of -https-port or -https-unix"), err) + } +}