forked from stripe/stripe-mock
-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.go
415 lines (334 loc) · 11.3 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
//go:generate go-bindata cert/cert.pem cert/key.pem openapi/openapi/fixtures3.json openapi/openapi/spec3.json
package main
import (
"crypto/tls"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"net"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/stripe/stripe-mock/spec"
)
const defaultPortHTTP = 12111
const defaultPortHTTPS = 12112
// verbose tracks whether the program is operating in verbose mode
var verbose bool
// This is set to the actual version by GoReleaser (using `-ldflags "-X ..."`)
// as it's run. Versions built from source will always show master.
var version = "master"
// ---
func main() {
options := options{
httpPortDefault: defaultPortHTTP,
httpsPortDefault: defaultPortHTTPS,
}
flag.BoolVar(&options.http, "http", false, "Run with HTTP")
flag.IntVar(&options.httpPort, "http-port", -1, "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", -1, "Port to listen on for HTTPS")
flag.StringVar(&options.httpsUnixSocket, "https-unix", "", "Unix socket to listen on for HTTPS")
flag.IntVar(&options.port, "port", -1, "Port to listen on (also respects PORT from environment)")
flag.StringVar(&options.fixturesPath, "fixtures", "", "Path to fixtures to use instead of bundled version (should be JSON)")
flag.StringVar(&options.specPath, "spec", "", "Path to OpenAPI spec to use instead of bundled version (should be JSON)")
flag.BoolVar(&options.strictVersionCheck, "strict-version-check", false, "Errors if version sent in Stripe-Version doesn't match the one in OpenAPI")
flag.StringVar(&options.unixSocket, "unix", "", "Unix socket to listen on")
flag.BoolVar(&verbose, "verbose", false, "Enable verbose mode")
flag.BoolVar(&options.showVersion, "version", false, "Show version and exit")
flag.Parse()
fmt.Printf("stripe-mock %s\n", version)
if options.showVersion || len(flag.Args()) == 1 && flag.Arg(0) == "version" {
return
}
err := options.checkConflictingOptions()
if err != nil {
flag.Usage()
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(options.specPath)
if err != nil {
abort(err.Error())
}
fixtures, err := getFixtures(options.fixturesPath)
if err != nil {
abort(err.Error())
}
stub := StubServer{
fixtures: fixtures,
spec: stripeSpec,
strictVersionCheck: options.strictVersionCheck,
}
err = stub.initializeRouter()
if err != nil {
abort(fmt.Sprintf("Error initializing router: %v\n", err))
}
httpMux := http.NewServeMux()
httpMux.HandleFunc("/", stub.HandleRequest)
// Deduplicates doubled slashes in paths. e.g. `//v1/charges` becomes
// `/v1/charges`.
handler := &DoubleSlashFixHandler{httpMux}
httpListener, err := options.getHTTPListener()
if err != nil {
abort(err.Error())
}
// Only start HTTP if requested (it will activate by default with no arguments, but it won't start if
// HTTPS is explicitly requested and HTTP is not).
if httpListener != nil {
server := http.Server{
Handler: handler,
}
// 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 (it will activate by default with no
// arguments, but it won't start if HTTP is explicitly requested and HTTPS
// is not).
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.
certificate, err := getTLSCertificate()
if err != nil {
abort(err.Error())
}
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{certificate},
// h2 is HTTP/2. A server with a default config normally doesn't
// need this hint, but Go is somewhat inflexible, and we need this
// here because we're using `Serve` and reading a TLS certificate
// from memory instead of using `ServeTLS` which would've read a
// certificate from file.
NextProtos: []string{"h2"},
}
server := http.Server{
Handler: handler,
TLSConfig: tlsConfig,
}
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
httpPortDefault int // For testability -- in practice always defaultPortHTTP
httpPort int
httpUnixSocket string
https bool
httpsPortDefault int // For testability -- in practice always defaultPortHTTPS
httpsPort int
httpsUnixSocket string
port int
showVersion bool
specPath string
strictVersionCheck bool
unixSocket string
}
func (o *options) checkConflictingOptions() error {
if o.unixSocket != "" && o.port != -1 {
return fmt.Errorf("Please specify only one of -port or -unix")
}
//
// HTTP
//
if o.http && (o.httpUnixSocket != "" || o.httpPort != -1) {
return fmt.Errorf("Please don't specify -http when using -http-port or -http-unix")
}
if (o.unixSocket != "" || o.port != -1) && (o.httpUnixSocket != "" || o.httpPort != -1) {
return fmt.Errorf("Please don't specify -port or -unix when using -http-port or -http-unix")
}
if o.httpUnixSocket != "" && o.httpPort != -1 {
return fmt.Errorf("Please specify only one of -http-port or -http-unix")
}
//
// HTTPS
//
if o.https && (o.httpsUnixSocket != "" || o.httpsPort != -1) {
return fmt.Errorf("Please don't specify -https when using -https-port or -https-unix")
}
if (o.unixSocket != "" || o.port != -1) && (o.httpsUnixSocket != "" || o.httpsPort != -1) {
return fmt.Errorf("Please don't specify -port or -unix when using -https-port or -https-unix")
}
if o.httpsUnixSocket != "" && o.httpsPort != -1 {
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) {
protocol := "HTTP"
if o.httpPort != -1 {
return getPortListener(o.httpPort, protocol)
}
if o.httpUnixSocket != "" {
return getUnixSocketListener(o.httpUnixSocket, protocol)
}
// HTTPS is active by default, but only if HTTP has not been explicitly
// activated.
if o.https || o.httpsPort != -1 || o.httpsUnixSocket != "" {
return nil, nil
}
if o.port != -1 {
return getPortListener(o.port, protocol)
}
if o.unixSocket != "" {
return getUnixSocketListener(o.unixSocket, protocol)
}
return getPortListenerDefault(o.httpPortDefault, protocol)
}
// 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) {
protocol := "HTTPS"
if o.httpsPort != -1 {
return getPortListener(o.httpsPort, protocol)
}
if o.httpsUnixSocket != "" {
return getUnixSocketListener(o.httpsUnixSocket, protocol)
}
// HTTPS is active by default, but only if HTTP has not been explicitly
// activated. HTTP may be activated with `-http`, `-http-port`, or
// `-http-unix`, but also with the old backwards compatible basic `-port`
// option.
if o.http || o.httpPort != -1 || o.httpUnixSocket != "" || o.port != -1 {
return nil, nil
}
if o.port != -1 {
return getPortListener(o.port, protocol)
}
if o.unixSocket != "" {
return getUnixSocketListener(o.unixSocket, protocol)
}
return getPortListenerDefault(o.httpsPortDefault, protocol)
}
//
// Private functions
//
func abort(message string) {
fmt.Fprintf(os.Stderr, message)
os.Exit(1)
}
// getTLSCertificate reads a certificate and key from the assets built by
// go-bindata.
func getTLSCertificate() (tls.Certificate, error) {
cert, err := Asset("cert/cert.pem")
if err != nil {
return tls.Certificate{}, err
}
key, err := Asset("cert/key.pem")
if err != nil {
return tls.Certificate{}, err
}
return tls.X509KeyPair(cert, key)
}
func getFixtures(fixturesPath string) (*spec.Fixtures, error) {
var data []byte
var err error
if fixturesPath == "" {
// And do the same for fixtures
data, err = Asset("openapi/openapi/fixtures3.json")
} else {
if !isJSONFile(fixturesPath) {
return nil, fmt.Errorf("Fixtures should come from a JSON file")
}
data, err = ioutil.ReadFile(fixturesPath)
}
if err != nil {
return nil, fmt.Errorf("error loading fixtures: %v", err)
}
var fixtures spec.Fixtures
err = json.Unmarshal(data, &fixtures)
if err != nil {
return nil, fmt.Errorf("error decoding spec: %v", err)
}
return &fixtures, nil
}
func getPortListener(port int, protocol string) (net.Listener, error) {
listener, err := net.Listen("tcp", ":"+strconv.Itoa(port))
if err != nil {
return nil, fmt.Errorf("error listening on port: %v", err)
}
fmt.Printf("Listening for %s on port: %v\n", protocol, listener.Addr().(*net.TCPAddr).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, protocol string) (net.Listener, error) {
if os.Getenv("PORT") != "" {
envPort, err := strconv.Atoi(os.Getenv("PORT"))
if err != nil {
return nil, err
}
return getPortListener(envPort, protocol)
}
return getPortListener(defaultPort, protocol)
}
func getSpec(specPath string) (*spec.Spec, error) {
var data []byte
var err error
if specPath == "" {
// Load the spec information from go-bindata
data, err = Asset("openapi/openapi/spec3.json")
} else {
if !isJSONFile(specPath) {
return nil, fmt.Errorf("spec should come from a JSON file")
}
data, err = ioutil.ReadFile(specPath)
}
if err != nil {
return nil, fmt.Errorf("error loading spec: %v", err)
}
var stripeSpec spec.Spec
err = json.Unmarshal(data, &stripeSpec)
if err != nil {
return nil, fmt.Errorf("error decoding spec: %v", err)
}
return &stripeSpec, nil
}
func getUnixSocketListener(unixSocket, protocol string) (net.Listener, error) {
listener, err := net.Listen("unix", unixSocket)
if err != nil {
return nil, fmt.Errorf("error listening on socket: %v", err)
}
fmt.Printf("Listening for %s on Unix socket: %s\n", protocol, unixSocket)
return listener, nil
}
// isJSONFile judges based on a file's extension whether it's a JSON file. It's
// used to return a better error message if the user points to an unsupported
// file.
func isJSONFile(path string) bool {
return strings.ToLower(filepath.Ext(path)) == ".json"
}