|
|
@ -0,0 +1,335 @@
|
|
|
1
|
package main
|
|
|
2
|
|
|
|
3
|
import (
|
|
|
4
|
"bytes"
|
|
|
5
|
"crypto/tls"
|
|
|
6
|
"encoding/json"
|
|
|
7
|
"flag"
|
|
|
8
|
"fmt"
|
|
|
9
|
"html/template"
|
|
|
10
|
"io"
|
|
|
11
|
"io/ioutil"
|
|
|
12
|
"log"
|
|
|
13
|
"net/http"
|
|
|
14
|
"net/http/httputil"
|
|
|
15
|
"net/url"
|
|
|
16
|
"strings"
|
|
|
17
|
)
|
|
|
18
|
|
|
|
19
|
var flags struct {
|
|
|
20
|
addr string
|
|
|
21
|
proxyURL string
|
|
|
22
|
proxyClientCert string
|
|
|
23
|
proxyClientKey string
|
|
|
24
|
}
|
|
|
25
|
|
|
|
26
|
func init() {
|
|
|
27
|
flag.StringVar(&flags.addr, "addr", "localhost:8080", "Address to listen on")
|
|
|
28
|
flag.StringVar(&flags.proxyURL, "proxy-url", "", "Proxy requests to this URL")
|
|
|
29
|
flag.StringVar(&flags.proxyClientCert, "proxy-client-cert", "", "Client certificate to use when connecting to proxy")
|
|
|
30
|
flag.StringVar(&flags.proxyClientKey, "proxy-client-key", "", "Client key to use when connecting to proxy")
|
|
|
31
|
}
|
|
|
32
|
|
|
|
33
|
var responses = []Response{
|
|
|
34
|
JSONResponse("GET", "/api", `{"kind": "APIVersions", "versions": ["v1"]}`),
|
|
|
35
|
JSONResponse("GET", "/apis", `{}`),
|
|
|
36
|
JSONResponse("GET", "/api/v1", `{"kind": "APIResourceList", "resources": [{"name": "pods", "namespaced": true, "kind": "Pod", "verbs": ["get", "list"], "categories": ["all"]}]}`),
|
|
|
37
|
JSONResponse("GET", "/api/v1/namespaces/default/pods", `{"kind": "PodList", "apiVersion": "v1", "items": [{"metadata": {"name": "oops-v1-214fbj25k"}, "status": {"phase": "Running", "conditions": [{"type": "Ready", "status": "True"}], "startTime": "2018-06-08T09:48:22Z"}}]}`),
|
|
|
38
|
}
|
|
|
39
|
|
|
|
40
|
func main() {
|
|
|
41
|
flag.Parse()
|
|
|
42
|
|
|
|
43
|
requestLog := make([]Request, 0)
|
|
|
44
|
|
|
|
45
|
http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
|
|
|
46
|
var resp *http.Response
|
|
|
47
|
if flags.proxyURL != "" {
|
|
|
48
|
resp = respondWithProxy(flags.proxyURL, w, req)
|
|
|
49
|
} else {
|
|
|
50
|
resp = respondWithStub(responses, w, req)
|
|
|
51
|
}
|
|
|
52
|
|
|
|
53
|
userAgent := req.Header.Get("User-Agent")
|
|
|
54
|
log.Printf("%s %s - %d (%s, %q)", req.Method, req.URL, resp.StatusCode, req.RemoteAddr, userAgent)
|
|
|
55
|
|
|
|
56
|
buf := new(bytes.Buffer)
|
|
|
57
|
out, err := httputil.DumpRequest(req, true)
|
|
|
58
|
if err != nil {
|
|
|
59
|
log.Printf("Error: Dumping request: %s", err)
|
|
|
60
|
return
|
|
|
61
|
}
|
|
|
62
|
buf.Write(out)
|
|
|
63
|
|
|
|
64
|
if resp.Header.Get("Content-Type") == "application/json" {
|
|
|
65
|
pretty, err := prettyfyJSON(resp.Body)
|
|
|
66
|
if err != nil {
|
|
|
67
|
log.Printf("Error: Prettyfying JSON: %s", err)
|
|
|
68
|
} else {
|
|
|
69
|
resp.Body = ioutil.NopCloser(bytes.NewReader(pretty))
|
|
|
70
|
}
|
|
|
71
|
}
|
|
|
72
|
respOut, err := httputil.DumpResponse(resp, true)
|
|
|
73
|
if err != nil {
|
|
|
74
|
log.Printf("Error: Dumping response: %s", err)
|
|
|
75
|
return
|
|
|
76
|
}
|
|
|
77
|
buf.Write([]byte("\n"))
|
|
|
78
|
buf.Write(respOut)
|
|
|
79
|
buf.Write([]byte("\n"))
|
|
|
80
|
|
|
|
81
|
requestLog = append(requestLog, buf.Bytes())
|
|
|
82
|
})
|
|
|
83
|
|
|
|
84
|
http.HandleFunc("/_log", func(w http.ResponseWriter, req *http.Request) {
|
|
|
85
|
for i := len(requestLog) - 1; i >= 0; i-- {
|
|
|
86
|
w.Write([]byte("------------------------------------------------------\n"))
|
|
|
87
|
w.Write(requestLog[i])
|
|
|
88
|
}
|
|
|
89
|
})
|
|
|
90
|
|
|
|
91
|
http.HandleFunc("/_stub", func(w http.ResponseWriter, req *http.Request) {
|
|
|
92
|
switch req.Method {
|
|
|
93
|
case "GET":
|
|
|
94
|
err := stubTmpl.Execute(w, nil)
|
|
|
95
|
if err != nil {
|
|
|
96
|
log.Printf("Error: Rendering stub template: %s", err)
|
|
|
97
|
return
|
|
|
98
|
}
|
|
|
99
|
case "POST":
|
|
|
100
|
err := req.ParseForm()
|
|
|
101
|
if err != nil {
|
|
|
102
|
log.Printf("Error: Parsing form: %s", err)
|
|
|
103
|
return
|
|
|
104
|
}
|
|
|
105
|
responses = append(responses, readResponse(req.Form))
|
|
|
106
|
default:
|
|
|
107
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
|
108
|
}
|
|
|
109
|
})
|
|
|
110
|
|
|
|
111
|
http.HandleFunc("/_stubs", func(w http.ResponseWriter, req *http.Request) {
|
|
|
112
|
w.Header().Set("Content-Type", "text/html")
|
|
|
113
|
fmt.Fprintf(w, "<!doctype html><html><head><style>pre{max-width:100vw;padding:0.5em;background-color:#eee;white-space:pre-wrap;}</style></head><body><ul>\n")
|
|
|
114
|
for _, resp := range responses {
|
|
|
115
|
fmt.Fprintf(w, "<li><pre>%s</pre></li>\n", resp.String())
|
|
|
116
|
}
|
|
|
117
|
fmt.Fprintf(w, "\n</ul></body></html>")
|
|
|
118
|
})
|
|
|
119
|
|
|
|
120
|
http.HandleFunc("/_help", func(w http.ResponseWriter, req *http.Request) {
|
|
|
121
|
urls := []string{
|
|
|
122
|
"/_log",
|
|
|
123
|
"/_stub",
|
|
|
124
|
"/_stubs",
|
|
|
125
|
"/_help",
|
|
|
126
|
}
|
|
|
127
|
fmt.Fprint(w, `<!doctype html>
|
|
|
128
|
<html>
|
|
|
129
|
<head>
|
|
|
130
|
<title>/_help</title>
|
|
|
131
|
</head>
|
|
|
132
|
<body>
|
|
|
133
|
<ul>`)
|
|
|
134
|
for _, url := range urls {
|
|
|
135
|
fmt.Fprintf(w, "<li><pre><a href=\"%s\">%s</a></pre></li>", url, url)
|
|
|
136
|
}
|
|
|
137
|
fmt.Fprint(w, `
|
|
|
138
|
</ul>
|
|
|
139
|
</body>
|
|
|
140
|
</html`)
|
|
|
141
|
})
|
|
|
142
|
|
|
|
143
|
log.Printf("Listening on http://%s", flags.addr)
|
|
|
144
|
log.Printf("See http://%s/_help", flags.addr)
|
|
|
145
|
log.Fatal(http.ListenAndServe(flags.addr, nil))
|
|
|
146
|
}
|
|
|
147
|
|
|
|
148
|
func matchResponse(req *http.Request, responses []Response) *Response {
|
|
|
149
|
for _, resp := range responses {
|
|
|
150
|
if req.Method == resp.Method && req.URL.Path == resp.Path {
|
|
|
151
|
return &resp
|
|
|
152
|
}
|
|
|
153
|
}
|
|
|
154
|
return nil
|
|
|
155
|
}
|
|
|
156
|
|
|
|
157
|
func respondWithStub(responses []Response, w http.ResponseWriter, req *http.Request) *http.Response {
|
|
|
158
|
resp := matchResponse(req, responses)
|
|
|
159
|
if resp == nil {
|
|
|
160
|
resp = &Response{Status: 404, Body: "Not found"}
|
|
|
161
|
}
|
|
|
162
|
|
|
|
163
|
for _, header := range resp.Headers {
|
|
|
164
|
w.Header().Set(header.Name, header.Value)
|
|
|
165
|
}
|
|
|
166
|
w.WriteHeader(resp.Status)
|
|
|
167
|
w.Write([]byte(resp.Body))
|
|
|
168
|
|
|
|
169
|
return resp.AsHTTP()
|
|
|
170
|
}
|
|
|
171
|
|
|
|
172
|
func respondWithProxy(proxyURL string, w http.ResponseWriter, req *http.Request) *http.Response {
|
|
|
173
|
/*proxyReq, err := http.NewRequest(req.Method, proxyURL+req.URL.Path, nil)
|
|
|
174
|
if err != nil {
|
|
|
175
|
log.Printf("Error: Creating proxy request: %s", err)
|
|
|
176
|
return nil
|
|
|
177
|
}
|
|
|
178
|
for name, vals := range req.Header {
|
|
|
179
|
proxyReq.Header[name] = vals
|
|
|
180
|
}*/
|
|
|
181
|
|
|
|
182
|
proxyTransport := &http.Transport{
|
|
|
183
|
TLSClientConfig: &tls.Config{
|
|
|
184
|
GetClientCertificate: func(info *tls.CertificateRequestInfo) (*tls.Certificate, error) {
|
|
|
185
|
log.Printf("TLS: client cert requested: %#v", info)
|
|
|
186
|
if flags.proxyClientCert != "" && flags.proxyClientKey != "" {
|
|
|
187
|
log.Printf("TLS: Loading client cert and key for proxy: %s, %s", flags.proxyClientCert, flags.proxyClientKey)
|
|
|
188
|
cert, err := tls.LoadX509KeyPair(flags.proxyClientCert, flags.proxyClientKey)
|
|
|
189
|
if err != nil {
|
|
|
190
|
return nil, err
|
|
|
191
|
}
|
|
|
192
|
return &cert, nil
|
|
|
193
|
}
|
|
|
194
|
log.Println("TLS: No client cert configured, returning empty cert")
|
|
|
195
|
return &tls.Certificate{}, nil
|
|
|
196
|
},
|
|
|
197
|
InsecureSkipVerify: true,
|
|
|
198
|
},
|
|
|
199
|
}
|
|
|
200
|
proxyClient := &http.Client{Transport: proxyTransport}
|
|
|
201
|
|
|
|
202
|
u, err := url.Parse(proxyURL)
|
|
|
203
|
if err != nil {
|
|
|
204
|
log.Printf("Error: Parsing proxy url: %s", err)
|
|
|
205
|
return nil
|
|
|
206
|
}
|
|
|
207
|
|
|
|
208
|
req.URL.Scheme = u.Scheme
|
|
|
209
|
req.URL.Host = u.Host
|
|
|
210
|
req.RequestURI = ""
|
|
|
211
|
resp, err := proxyClient.Do(req)
|
|
|
212
|
if err != nil {
|
|
|
213
|
log.Printf("Error: Proxying %s: %s", req.URL.Path, err)
|
|
|
214
|
return nil
|
|
|
215
|
}
|
|
|
216
|
defer resp.Body.Close()
|
|
|
217
|
|
|
|
218
|
for name, vals := range resp.Header {
|
|
|
219
|
w.Header()[name] = vals
|
|
|
220
|
}
|
|
|
221
|
w.WriteHeader(resp.StatusCode)
|
|
|
222
|
|
|
|
223
|
buf := new(bytes.Buffer)
|
|
|
224
|
io.Copy(buf, resp.Body)
|
|
|
225
|
resp.Body = ioutil.NopCloser(bytes.NewReader(buf.Bytes()))
|
|
|
226
|
io.Copy(w, buf)
|
|
|
227
|
|
|
|
228
|
return resp
|
|
|
229
|
}
|
|
|
230
|
|
|
|
231
|
func prettyfyJSON(r io.Reader) ([]byte, error) {
|
|
|
232
|
dec := json.NewDecoder(r)
|
|
|
233
|
var val interface{}
|
|
|
234
|
err := dec.Decode(&val)
|
|
|
235
|
if err != nil {
|
|
|
236
|
return nil, err
|
|
|
237
|
}
|
|
|
238
|
|
|
|
239
|
return json.MarshalIndent(val, "", " ")
|
|
|
240
|
}
|
|
|
241
|
|
|
|
242
|
// Request is a stored serialized HTTP request.
|
|
|
243
|
type Request []byte
|
|
|
244
|
|
|
|
245
|
// Response is a mocked HTTP response.
|
|
|
246
|
type Response struct {
|
|
|
247
|
Method string
|
|
|
248
|
Path string
|
|
|
249
|
|
|
|
250
|
Status int
|
|
|
251
|
Headers []Header
|
|
|
252
|
Body string
|
|
|
253
|
}
|
|
|
254
|
|
|
|
255
|
func (resp Response) String() string {
|
|
|
256
|
buf := new(bytes.Buffer)
|
|
|
257
|
fmt.Fprintf(buf, "%s %s\r\n", resp.Method, resp.Path)
|
|
|
258
|
for _, header := range resp.Headers {
|
|
|
259
|
fmt.Fprintf(buf, "%s: %s\r\n", header.Name, header.Value)
|
|
|
260
|
}
|
|
|
261
|
fmt.Fprintf(buf, "\r\n%s", resp.Body)
|
|
|
262
|
return buf.String()
|
|
|
263
|
}
|
|
|
264
|
|
|
|
265
|
// AsHTTP returns a http.Response representation.
|
|
|
266
|
func (resp Response) AsHTTP() *http.Response {
|
|
|
267
|
headers := make(map[string][]string)
|
|
|
268
|
for _, header := range resp.Headers {
|
|
|
269
|
h, ok := headers[header.Name]
|
|
|
270
|
if !ok {
|
|
|
271
|
h = []string{}
|
|
|
272
|
}
|
|
|
273
|
h = append(h, header.Value)
|
|
|
274
|
headers[header.Name] = h
|
|
|
275
|
}
|
|
|
276
|
return &http.Response{
|
|
|
277
|
ProtoMajor: 1,
|
|
|
278
|
ProtoMinor: 1,
|
|
|
279
|
|
|
|
280
|
StatusCode: resp.Status,
|
|
|
281
|
Header: headers,
|
|
|
282
|
Body: ioutil.NopCloser(strings.NewReader(resp.Body)),
|
|
|
283
|
}
|
|
|
284
|
}
|
|
|
285
|
|
|
|
286
|
// Header is a single-valued HTTP header name and value
|
|
|
287
|
type Header struct {
|
|
|
288
|
Name string
|
|
|
289
|
Value string
|
|
|
290
|
}
|
|
|
291
|
|
|
|
292
|
// JSONResponse creates a Response with "Content-Type: application/json".
|
|
|
293
|
func JSONResponse(method, path, body string) Response {
|
|
|
294
|
return Response{
|
|
|
295
|
Method: method,
|
|
|
296
|
Path: path,
|
|
|
297
|
Status: 200,
|
|
|
298
|
Headers: []Header{Header{Name: "Content-Type", Value: "application/json"}},
|
|
|
299
|
Body: body,
|
|
|
300
|
}
|
|
|
301
|
}
|
|
|
302
|
|
|
|
303
|
func readResponse(form url.Values) Response {
|
|
|
304
|
r := Response{}
|
|
|
305
|
r.Method = form.Get("method")
|
|
|
306
|
r.Path = form.Get("path")
|
|
|
307
|
r.Status = 200
|
|
|
308
|
headers := make([]Header, 0)
|
|
|
309
|
for i, name := range form["header"] {
|
|
|
310
|
headers = append(headers, Header{Name: name, Value: form["value"][i]})
|
|
|
311
|
}
|
|
|
312
|
r.Body = form.Get("body")
|
|
|
313
|
return r
|
|
|
314
|
}
|
|
|
315
|
|
|
|
316
|
var stubTmpl = template.Must(template.New("").Parse(`<!doctype html>
|
|
|
317
|
<html>
|
|
|
318
|
<head>
|
|
|
319
|
</head>
|
|
|
320
|
|
|
|
321
|
<body>
|
|
|
322
|
<form method="POST" action="/_stub">
|
|
|
323
|
<input type="text" name="method" placeholder="GET" />
|
|
|
324
|
<input type="text" name="path" placeholder="/request/path?query" />
|
|
|
325
|
<ul>
|
|
|
326
|
<li>
|
|
|
327
|
<input type="text" name="header" placeholder="Content-Type" />
|
|
|
328
|
<input type="text" name="value" placeholder="application/json" />
|
|
|
329
|
</li>
|
|
|
330
|
</ul>
|
|
|
331
|
<textarea name="body" placeholder="{}"></textarea>
|
|
|
332
|
<input type="submit" />
|
|
|
333
|
</form>
|
|
|
334
|
</body>
|
|
|
335
|
</html>`))
|