source: code/trunk/morty.go@ 44

Last change on this file since 44 was 44, checked in by alex, 9 years ago

[fix] fix #14. Fetched URI matched the mortyurl.
Query parameters in requested URI were parsed and set again. Unfortunately a query ?a&b were changed into ?a=b= which can lead to 404 errors.

File size: 17.5 KB
RevLine 
[1]1package main
2
3import (
4 "bytes"
5 "crypto/hmac"
6 "crypto/sha256"
7 "encoding/hex"
8 "errors"
9 "flag"
10 "fmt"
11 "io"
12 "log"
13 "net/url"
14 "regexp"
15 "strings"
[4]16 "time"
[1]17
18 "github.com/valyala/fasthttp"
19 "golang.org/x/net/html"
[44]20 "golang.org/x/text/encoding/charmap"
[1]21)
22
23const (
24 STATE_DEFAULT int = 0
25 STATE_IN_STYLE int = 1
26 STATE_IN_NOSCRIPT int = 2
27)
28
29var CLIENT *fasthttp.Client = &fasthttp.Client{
30 MaxResponseBodySize: 10 * 1024 * 1024, // 10M
31}
32
[27]33var CSS_URL_REGEXP *regexp.Regexp = regexp.MustCompile("url\\((['\"]?)[ \\t\\f]*([\u0009\u0021\u0023-\u0026\u0028\u002a-\u007E]+)(['\"]?)\\)?")
[1]34
35var UNSAFE_ELEMENTS [][]byte = [][]byte{
36 []byte("applet"),
37 []byte("canvas"),
38 []byte("embed"),
39 //[]byte("iframe"),
40 []byte("script"),
41}
42
43var SAFE_ATTRIBUTES [][]byte = [][]byte{
44 []byte("abbr"),
45 []byte("accesskey"),
46 []byte("align"),
47 []byte("alt"),
[13]48 []byte("as"),
[1]49 []byte("autocomplete"),
50 []byte("charset"),
51 []byte("checked"),
52 []byte("class"),
53 []byte("content"),
54 []byte("contenteditable"),
55 []byte("contextmenu"),
56 []byte("dir"),
57 []byte("for"),
58 []byte("height"),
59 []byte("hidden"),
60 []byte("id"),
61 []byte("lang"),
62 []byte("media"),
63 []byte("method"),
64 []byte("name"),
65 []byte("nowrap"),
66 []byte("placeholder"),
67 []byte("property"),
68 []byte("rel"),
69 []byte("spellcheck"),
70 []byte("tabindex"),
71 []byte("target"),
72 []byte("title"),
73 []byte("translate"),
74 []byte("type"),
75 []byte("value"),
76 []byte("width"),
77}
78
79var SELF_CLOSING_ELEMENTS [][]byte = [][]byte{
80 []byte("area"),
81 []byte("base"),
82 []byte("br"),
83 []byte("col"),
84 []byte("embed"),
85 []byte("hr"),
86 []byte("img"),
87 []byte("input"),
88 []byte("keygen"),
89 []byte("link"),
90 []byte("meta"),
91 []byte("param"),
92 []byte("source"),
93 []byte("track"),
94 []byte("wbr"),
95}
96
97type Proxy struct {
[4]98 Key []byte
99 RequestTimeout time.Duration
[1]100}
101
102type RequestConfig struct {
103 Key []byte
[23]104 BaseURL *url.URL
[1]105}
106
[2]107var HTML_FORM_EXTENSION string = `<input type="hidden" name="mortyurl" value="%s" /><input type="hidden" name="mortyhash" value="%s" />`
[1]108
109var HTML_BODY_EXTENSION string = `
110<div id="mortyheader">
111 <input type="checkbox" id="mortytoggle" autocomplete="off" />
[36]112 <div><p>This is a proxified and sanitized view of the page,<br />visit <a href="%s" rel="noreferrer">original site</a>.</p><p><label for="mortytoggle">hide</label></p></div>
[1]113</div>
114<style>
[36]115#mortyheader { position: fixed; padding: 12px 12px 12px 0; margin: 0; box-sizing: content-box; top: 15%%; left: 0; max-width: 140px; color: #444; overflow: hidden; z-index: 110000; font-size: 12px; line-height: normal; }
116#mortyheader a { color: #3498db; font-weight: bold; }
117#mortyheader p { padding: 0 0 0.7em 0; margin: 0; }
118#mortyheader > div { padding: 8px; font-size: 12px !important; font-family: sans !important; border-width: 4px 4px 4px 0; border-style: solid; border-color: #1abc9c; background: #FFF; line-height: 1em; }
[5]119#mortyheader label { text-align: right; cursor: pointer; display: block; color: #444; padding: 0; margin: 0; }
[1]120input[type=checkbox]#mortytoggle { display: none; }
121input[type=checkbox]#mortytoggle:checked ~ div { display: none; }
122</style>
123`
124
125func (p *Proxy) RequestHandler(ctx *fasthttp.RequestCtx) {
[10]126
127 if appRequestHandler(ctx) {
128 return
129 }
130
[1]131 requestHash := popRequestParam(ctx, []byte("mortyhash"))
132
133 requestURI := popRequestParam(ctx, []byte("mortyurl"))
134
135 if requestURI == nil {
[35]136 p.serveMainPage(ctx, 200, nil)
[1]137 return
138 }
139
140 if p.Key != nil {
141 if !verifyRequestURI(requestURI, requestHash, p.Key) {
[35]142 // HTTP status code 403 : Forbidden
143 p.serveMainPage(ctx, 403, errors.New(`invalid "mortyhash" parameter`))
[1]144 return
145 }
146 }
147
148 parsedURI, err := url.Parse(string(requestURI))
149
[18]150 if strings.HasSuffix(parsedURI.Host, ".onion") {
[35]151 // HTTP status code 501 : Not Implemented
152 p.serveMainPage(ctx, 501, errors.New("Tor urls are not supported yet"))
[18]153 return
154 }
155
[11]156 if err != nil {
[35]157 // HTTP status code 500 : Internal Server Error
158 p.serveMainPage(ctx, 500, err)
[1]159 return
160 }
161
162 req := fasthttp.AcquireRequest()
163 defer fasthttp.ReleaseRequest(req)
[12]164 req.SetConnectionClose()
[1]165
[44]166 requestURIStr := string(requestURI)
[1]167
[44]168 log.Println("getting", requestURIStr)
[1]169
[44]170 req.SetRequestURI(requestURIStr)
[1]171 req.Header.SetUserAgentBytes([]byte("Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36"))
172
173 resp := fasthttp.AcquireResponse()
174 defer fasthttp.ReleaseResponse(resp)
175
176 req.Header.SetMethodBytes(ctx.Method())
177 if ctx.IsPost() || ctx.IsPut() {
178 req.SetBody(ctx.PostBody())
179 }
180
[11]181 err = CLIENT.DoTimeout(req, resp, p.RequestTimeout)
182
183 if err != nil {
[35]184 if err == fasthttp.ErrTimeout {
185 // HTTP status code 504 : Gateway Time-Out
186 p.serveMainPage(ctx, 504, err)
187 } else {
188 // HTTP status code 500 : Internal Server Error
189 p.serveMainPage(ctx, 500, err)
190 }
[1]191 return
192 }
193
194 if resp.StatusCode() != 200 {
195 switch resp.StatusCode() {
[7]196 case 301, 302, 303, 307, 308:
[1]197 loc := resp.Header.Peek("Location")
198 if loc != nil {
[23]199 rc := &RequestConfig{Key: p.Key, BaseURL: parsedURI}
200 url, err := rc.ProxifyURI(string(loc))
[1]201 if err == nil {
202 ctx.SetStatusCode(resp.StatusCode())
203 ctx.Response.Header.Add("Location", url)
204 log.Println("redirect to", string(loc))
205 return
206 }
207 }
208 }
[44]209 error_message := fmt.Sprintf("invalid response: %d (%s)", resp.StatusCode(), requestURIStr)
[37]210 p.serveMainPage(ctx, resp.StatusCode(), errors.New(error_message))
[1]211 return
212 }
213
214 contentType := resp.Header.Peek("Content-Type")
215
216 if contentType == nil {
[35]217 // HTTP status code 503 : Service Unavailable
218 p.serveMainPage(ctx, 503, errors.New("invalid content type"))
[1]219 return
220 }
221
[17]222 if bytes.Contains(bytes.ToLower(contentType), []byte("javascript")) {
[35]223 // HTTP status code 403 : Forbidden
224 p.serveMainPage(ctx, 403, errors.New("forbidden content type"))
[17]225 return
226 }
227
[1]228 contentInfo := bytes.SplitN(contentType, []byte(";"), 2)
229
230 var responseBody []byte
231
[44]232 if len(contentInfo) == 2 && bytes.Contains(contentInfo[1], []byte("ISO-8859-2")) && bytes.Contains(contentInfo[0], []byte("text")) {
233 var err error
234 responseBody, err = charmap.ISO8859_2.NewDecoder().Bytes(resp.Body())
235 if err != nil {
236 // HTTP status code 503 : Service Unavailable
237 p.serveMainPage(ctx, 503, err)
238 return
[1]239 }
240 } else {
241 responseBody = resp.Body()
242 }
243
244 ctx.SetContentType(fmt.Sprintf("%s; charset=UTF-8", contentInfo[0]))
245
246 switch {
247 case bytes.Contains(contentType, []byte("css")):
[23]248 sanitizeCSS(&RequestConfig{Key: p.Key, BaseURL: parsedURI}, ctx, responseBody)
[1]249 case bytes.Contains(contentType, []byte("html")):
[23]250 sanitizeHTML(&RequestConfig{Key: p.Key, BaseURL: parsedURI}, ctx, responseBody)
[1]251 default:
[39]252 if ctx.Request.Header.Peek("Content-Disposition") != nil {
253 ctx.Response.Header.AddBytesV("Content-Disposition", ctx.Request.Header.Peek("Content-Disposition"))
254 }
[1]255 ctx.Write(responseBody)
256 }
257}
258
[10]259func appRequestHandler(ctx *fasthttp.RequestCtx) bool {
[11]260 // serve robots.txt
[10]261 if bytes.Equal(ctx.Path(), []byte("/robots.txt")) {
262 ctx.SetContentType("text/plain")
263 ctx.Write([]byte("User-Agent: *\nDisallow: /\n"))
264 return true
265 }
[11]266
[10]267 return false
268}
269
[1]270func popRequestParam(ctx *fasthttp.RequestCtx, paramName []byte) []byte {
271 param := ctx.QueryArgs().PeekBytes(paramName)
272
273 if param == nil {
274 param = ctx.PostArgs().PeekBytes(paramName)
275 if param != nil {
276 ctx.PostArgs().DelBytes(paramName)
277 }
278 } else {
279 ctx.QueryArgs().DelBytes(paramName)
280 }
281
282 return param
283}
284
[9]285func sanitizeCSS(rc *RequestConfig, out io.Writer, css []byte) {
[1]286 // TODO
287
288 urlSlices := CSS_URL_REGEXP.FindAllSubmatchIndex(css, -1)
289
290 if urlSlices == nil {
[9]291 out.Write(css)
[1]292 return
293 }
294
295 startIndex := 0
296
297 for _, s := range urlSlices {
[15]298 urlStart := s[4]
299 urlEnd := s[5]
[1]300
[23]301 if uri, err := rc.ProxifyURI(string(css[urlStart:urlEnd])); err == nil {
[9]302 out.Write(css[startIndex:urlStart])
303 out.Write([]byte(uri))
[1]304 startIndex = urlEnd
305 } else {
[36]306 log.Println("cannot proxify css uri:", string(css[urlStart:urlEnd]))
[1]307 }
308 }
309 if startIndex < len(css) {
[9]310 out.Write(css[startIndex:len(css)])
[1]311 }
312}
313
[9]314func sanitizeHTML(rc *RequestConfig, out io.Writer, htmlDoc []byte) {
[1]315 r := bytes.NewReader(htmlDoc)
316 decoder := html.NewTokenizer(r)
317 decoder.AllowCDATA(true)
318
319 unsafeElements := make([][]byte, 0, 8)
320 state := STATE_DEFAULT
[44]321
[1]322 for {
323 token := decoder.Next()
324 if token == html.ErrorToken {
325 err := decoder.Err()
326 if err != io.EOF {
327 log.Println("failed to parse HTML:")
328 }
329 break
330 }
331
332 if len(unsafeElements) == 0 {
333
334 switch token {
335 case html.StartTagToken, html.SelfClosingTagToken:
336 tag, hasAttrs := decoder.TagName()
337 safe := !inArray(tag, UNSAFE_ELEMENTS)
338 if !safe {
339 if !inArray(tag, SELF_CLOSING_ELEMENTS) {
340 var unsafeTag []byte = make([]byte, len(tag))
341 copy(unsafeTag, tag)
342 unsafeElements = append(unsafeElements, unsafeTag)
343 }
344 break
345 }
[38]346 if bytes.Equal(tag, []byte("base")) {
347 for {
348 attrName, attrValue, moreAttr := decoder.TagAttr()
[44]349 if !bytes.Equal(attrName, []byte("href")) {
350 continue
[38]351 }
[44]352 parsedURI, err := url.Parse(string(attrValue))
353 if err == nil {
354 rc.BaseURL = parsedURI
355 }
[38]356 if !moreAttr {
357 break
358 }
359 }
360 break
361 }
[1]362 if bytes.Equal(tag, []byte("noscript")) {
363 state = STATE_IN_NOSCRIPT
364 break
365 }
366 var attrs [][][]byte
367 if hasAttrs {
368 for {
369 attrName, attrValue, moreAttr := decoder.TagAttr()
[21]370 attrs = append(attrs, [][]byte{
371 attrName,
372 attrValue,
373 []byte(html.EscapeString(string(attrValue))),
374 })
[1]375 if !moreAttr {
376 break
377 }
378 }
[13]379 }
380 if bytes.Equal(tag, []byte("link")) {
381 sanitizeLinkTag(rc, out, attrs)
382 break
383 }
384
385 fmt.Fprintf(out, "<%s", tag)
386
387 if hasAttrs {
[44]388 if bytes.Equal(tag, []byte("meta")) {
389 sanitizeMetaAttrs(rc, out, attrs)
390 } else {
391 sanitizeAttrs(rc, out, attrs)
392 }
[1]393 }
[13]394
[1]395 if token == html.SelfClosingTagToken {
[9]396 fmt.Fprintf(out, " />")
[1]397 } else {
[9]398 fmt.Fprintf(out, ">")
[1]399 if bytes.Equal(tag, []byte("style")) {
400 state = STATE_IN_STYLE
401 }
402 }
[13]403
[1]404 if bytes.Equal(tag, []byte("form")) {
405 var formURL *url.URL
406 for _, attr := range attrs {
407 if bytes.Equal(attr[0], []byte("action")) {
408 formURL, _ = url.Parse(string(attr[1]))
[28]409 formURL = mergeURIs(rc.BaseURL, formURL)
[1]410 break
411 }
412 }
413 if formURL == nil {
[23]414 formURL = rc.BaseURL
[1]415 }
[2]416 urlStr := formURL.String()
417 var key string
418 if rc.Key != nil {
419 key = hash(urlStr, rc.Key)
420 }
[9]421 fmt.Fprintf(out, HTML_FORM_EXTENSION, urlStr, key)
[1]422
423 }
424
425 case html.EndTagToken:
426 tag, _ := decoder.TagName()
427 writeEndTag := true
428 switch string(tag) {
429 case "body":
[23]430 fmt.Fprintf(out, HTML_BODY_EXTENSION, rc.BaseURL.String())
[1]431 case "style":
432 state = STATE_DEFAULT
433 case "noscript":
434 state = STATE_DEFAULT
435 writeEndTag = false
436 }
437 // skip noscript tags - only the tag, not the content, because javascript is sanitized
438 if writeEndTag {
[9]439 fmt.Fprintf(out, "</%s>", tag)
[1]440 }
441
442 case html.TextToken:
443 switch state {
444 case STATE_DEFAULT:
[9]445 fmt.Fprintf(out, "%s", decoder.Raw())
[1]446 case STATE_IN_STYLE:
[9]447 sanitizeCSS(rc, out, decoder.Raw())
[1]448 case STATE_IN_NOSCRIPT:
[9]449 sanitizeHTML(rc, out, decoder.Raw())
[1]450 }
451
452 case html.DoctypeToken, html.CommentToken:
[9]453 out.Write(decoder.Raw())
[1]454 }
455 } else {
456 switch token {
457 case html.StartTagToken:
458 tag, _ := decoder.TagName()
459 if inArray(tag, UNSAFE_ELEMENTS) {
460 unsafeElements = append(unsafeElements, tag)
461 }
462
463 case html.EndTagToken:
464 tag, _ := decoder.TagName()
465 if bytes.Equal(unsafeElements[len(unsafeElements)-1], tag) {
466 unsafeElements = unsafeElements[:len(unsafeElements)-1]
467 }
468 }
469 }
470 }
471}
472
[13]473func sanitizeLinkTag(rc *RequestConfig, out io.Writer, attrs [][][]byte) {
474 exclude := false
475 for _, attr := range attrs {
476 attrName := attr[0]
477 attrValue := attr[1]
478 if bytes.Equal(attrName, []byte("rel")) {
479 if bytes.Equal(attrValue, []byte("dns-prefetch")) {
480 exclude = true
481 break
482 }
483 }
484 if bytes.Equal(attrName, []byte("as")) {
485 if bytes.Equal(attrValue, []byte("script")) {
486 exclude = true
487 break
488 }
489 }
490 }
491
492 if !exclude {
493 out.Write([]byte("<link"))
494 for _, attr := range attrs {
[21]495 sanitizeAttr(rc, out, attr[0], attr[1], attr[2])
[13]496 }
497 out.Write([]byte(">"))
498 }
499}
500
[44]501func sanitizeMetaAttrs(rc *RequestConfig, out io.Writer, attrs [][][]byte) {
[1]502 var http_equiv []byte
503 var content []byte
504
505 for _, attr := range attrs {
506 attrName := attr[0]
507 attrValue := attr[1]
508 if bytes.Equal(attrName, []byte("http-equiv")) {
509 http_equiv = bytes.ToLower(attrValue)
510 }
511 if bytes.Equal(attrName, []byte("content")) {
512 content = attrValue
513 }
514 }
515
[14]516 urlIndex := bytes.Index(bytes.ToLower(content), []byte("url="))
517 if bytes.Equal(http_equiv, []byte("refresh")) && urlIndex != -1 {
518 contentUrl := content[urlIndex+4:]
[36]519 // special case of <meta http-equiv="refresh" content="0; url='example.com/url.with.quote.outside'">
[37]520 if len(contentUrl) >= 2 && (contentUrl[0] == byte('\'') || contentUrl[0] == byte('"')) {
[36]521 if contentUrl[0] == contentUrl[len(contentUrl)-1] {
[37]522 contentUrl = contentUrl[1 : len(contentUrl)-1]
[36]523 }
524 }
525 // output proxify result
[23]526 if uri, err := rc.ProxifyURI(string(contentUrl)); err == nil {
[14]527 fmt.Fprintf(out, ` http-equiv="refresh" content="%surl=%s"`, content[:urlIndex], uri)
[1]528 }
529 } else {
[9]530 sanitizeAttrs(rc, out, attrs)
[1]531 }
[44]532
[1]533}
534
[9]535func sanitizeAttrs(rc *RequestConfig, out io.Writer, attrs [][][]byte) {
[1]536 for _, attr := range attrs {
[21]537 sanitizeAttr(rc, out, attr[0], attr[1], attr[2])
[1]538 }
539}
540
[21]541func sanitizeAttr(rc *RequestConfig, out io.Writer, attrName, attrValue, escapedAttrValue []byte) {
[1]542 if inArray(attrName, SAFE_ATTRIBUTES) {
[21]543 fmt.Fprintf(out, " %s=\"%s\"", attrName, escapedAttrValue)
[1]544 return
545 }
546 switch string(attrName) {
547 case "src", "href", "action":
[23]548 if uri, err := rc.ProxifyURI(string(attrValue)); err == nil {
[9]549 fmt.Fprintf(out, " %s=\"%s\"", attrName, uri)
[1]550 } else {
[36]551 log.Println("cannot proxify uri:", string(attrValue))
[1]552 }
553 case "style":
[21]554 cssAttr := bytes.NewBuffer(nil)
555 sanitizeCSS(rc, cssAttr, attrValue)
556 fmt.Fprintf(out, " %s=\"%s\"", attrName, html.EscapeString(string(cssAttr.Bytes())))
[1]557 }
558}
559
[36]560func mergeURIs(u1, u2 *url.URL) *url.URL {
[28]561 return u1.ResolveReference(u2)
[1]562}
563
[23]564func (rc *RequestConfig) ProxifyURI(uri string) (string, error) {
[28]565 // remove javascript protocol
566 if strings.HasPrefix(uri, "javascript:") {
567 return "", nil
568 }
[1]569 // TODO check malicious data: - e.g. data:script
570 if strings.HasPrefix(uri, "data:") {
571 return uri, nil
572 }
573
574 if len(uri) > 0 && uri[0] == '#' {
575 return uri, nil
576 }
577
578 u, err := url.Parse(uri)
579 if err != nil {
580 return "", err
581 }
[28]582 u = mergeURIs(rc.BaseURL, u)
[1]583
584 uri = u.String()
585
586 if rc.Key == nil {
587 return fmt.Sprintf("./?mortyurl=%s", url.QueryEscape(uri)), nil
588 }
[44]589
[1]590 return fmt.Sprintf("./?mortyhash=%s&mortyurl=%s", hash(uri, rc.Key), url.QueryEscape(uri)), nil
591}
592
593func inArray(b []byte, a [][]byte) bool {
594 for _, b2 := range a {
595 if bytes.Equal(b, b2) {
596 return true
597 }
598 }
599 return false
600}
601
602func hash(msg string, key []byte) string {
603 mac := hmac.New(sha256.New, key)
604 mac.Write([]byte(msg))
605 return hex.EncodeToString(mac.Sum(nil))
606}
607
608func verifyRequestURI(uri, hashMsg, key []byte) bool {
609 h := make([]byte, hex.DecodedLen(len(hashMsg)))
610 _, err := hex.Decode(h, hashMsg)
611 if err != nil {
612 log.Println("hmac error:", err)
613 return false
614 }
615 mac := hmac.New(sha256.New, key)
616 mac.Write(uri)
617 return hmac.Equal(h, mac.Sum(nil))
618}
619
[35]620func (p *Proxy) serveMainPage(ctx *fasthttp.RequestCtx, statusCode int, err error) {
[1]621 ctx.SetContentType("text/html")
[35]622 ctx.SetStatusCode(statusCode)
[1]623 ctx.Write([]byte(`<!doctype html>
624<head>
[11]625<title>MortyProxy</title>
[36]626<meta name="viewport" content="width=device-width, initial-scale=1 , maximum-scale=1.0, user-scalable=1" />
[11]627<style>
[36]628html { height: 100%; }
629body { min-height : 100%; display: flex; flex-direction:column; font-family: 'Garamond', 'Georgia', serif; text-align: center; color: #444; background: #FAFAFA; margin: 0; padding: 0; font-size: 1.1em; }
[11]630input { border: 1px solid #888; padding: 0.3em; color: #444; background: #FFF; font-size: 1.1em; }
[36]631input[placeholder] { width:80%; }
[11]632a { text-decoration: none; #2980b9; }
633h1, h2 { font-weight: 200; margin-bottom: 2rem; }
634h1 { font-size: 3em; }
[36]635.container { flex:1; min-height: 100%; margin-bottom: 1em; }
636.footer { margin: 1em; }
[11]637.footer p { font-size: 0.8em; }
638</style>
[1]639</head>
[11]640<body>
[36]641 <div class="container">
642 <h1>MortyProxy</h1>
643`))
[11]644 if err != nil {
645 log.Println("error:", err)
646 ctx.Write([]byte("<h2>Error: "))
647 ctx.Write([]byte(html.EscapeString(err.Error())))
648 ctx.Write([]byte("</h2>"))
649 }
[1]650 if p.Key == nil {
651 ctx.Write([]byte(`
[36]652 <form action="post">
653 Visit url: <input placeholder="https://url.." name="mortyurl" autofocus />
654 <input type="submit" value="go" />
655 </form>`))
[11]656 } else {
657 ctx.Write([]byte(`<h3>Warning! This instance does not support direct URL opening.</h3>`))
[1]658 }
659 ctx.Write([]byte(`
[36]660 </div>
661 <div class="footer">
662 <p>Morty rewrites web pages to exclude malicious HTML tags and CSS/HTML attributes. It also replaces external resource references to prevent third-party information leaks.<br />
663 <a href="https://github.com/asciimoo/morty">view on github</a>
664 </p>
665 </div>
[1]666</body>
667</html>`))
668}
669
670func main() {
671
[2]672 listen := flag.String("listen", "127.0.0.1:3000", "Listen address")
[1]673 key := flag.String("key", "", "HMAC url validation key (hexadecimal encoded) - leave blank to disable")
[24]674 ipv6 := flag.Bool("ipv6", false, "Allow IPv6 HTTP requests")
[4]675 requestTimeout := flag.Uint("timeout", 2, "Request timeout")
[1]676 flag.Parse()
677
[24]678 if *ipv6 {
679 CLIENT.Dial = fasthttp.DialDualStack
680 }
681
[4]682 p := &Proxy{RequestTimeout: time.Duration(*requestTimeout) * time.Second}
[1]683
684 if *key != "" {
685 p.Key = []byte(*key)
686 }
687
688 log.Println("listening on", *listen)
689
690 if err := fasthttp.ListenAndServe(*listen, p.RequestHandler); err != nil {
691 log.Fatal("Error in ListenAndServe:", err)
692 }
693}
Note: See TracBrowser for help on using the repository browser.