source: code/trunk/morty.go@ 42

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

[enh] support different encodings (all encoding are convert to UTF-8 as before)

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