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
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"
[42]20 "golang.org/x/net/html/charset"
21 "golang.org/x/text/encoding"
[1]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
[27]34var CSS_URL_REGEXP *regexp.Regexp = regexp.MustCompile("url\\((['\"]?)[ \\t\\f]*([\u0009\u0021\u0023-\u0026\u0028\u002a-\u007E]+)(['\"]?)\\)?")
[1]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"),
[13]49 []byte("as"),
[1]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 {
[4]99 Key []byte
100 RequestTimeout time.Duration
[1]101}
102
103type RequestConfig struct {
104 Key []byte
[23]105 BaseURL *url.URL
[1]106}
107
[2]108var HTML_FORM_EXTENSION string = `<input type="hidden" name="mortyurl" value="%s" /><input type="hidden" name="mortyhash" value="%s" />`
[1]109
110var HTML_BODY_EXTENSION string = `
111<div id="mortyheader">
112 <input type="checkbox" id="mortytoggle" autocomplete="off" />
[36]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>
[1]114</div>
115<style>
[36]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; }
[5]120#mortyheader label { text-align: right; cursor: pointer; display: block; color: #444; padding: 0; margin: 0; }
[1]121input[type=checkbox]#mortytoggle { display: none; }
122input[type=checkbox]#mortytoggle:checked ~ div { display: none; }
123</style>
124`
125
[42]126var HTML_META_CONTENT_TYPE string = "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">"
127
[1]128func (p *Proxy) RequestHandler(ctx *fasthttp.RequestCtx) {
[10]129
130 if appRequestHandler(ctx) {
131 return
132 }
133
[1]134 requestHash := popRequestParam(ctx, []byte("mortyhash"))
135
136 requestURI := popRequestParam(ctx, []byte("mortyurl"))
137
138 if requestURI == nil {
[35]139 p.serveMainPage(ctx, 200, nil)
[1]140 return
141 }
142
143 if p.Key != nil {
144 if !verifyRequestURI(requestURI, requestHash, p.Key) {
[35]145 // HTTP status code 403 : Forbidden
146 p.serveMainPage(ctx, 403, errors.New(`invalid "mortyhash" parameter`))
[1]147 return
148 }
149 }
150
151 parsedURI, err := url.Parse(string(requestURI))
152
[18]153 if strings.HasSuffix(parsedURI.Host, ".onion") {
[35]154 // HTTP status code 501 : Not Implemented
155 p.serveMainPage(ctx, 501, errors.New("Tor urls are not supported yet"))
[18]156 return
157 }
158
[11]159 if err != nil {
[35]160 // HTTP status code 500 : Internal Server Error
161 p.serveMainPage(ctx, 500, err)
[1]162 return
163 }
164
165 req := fasthttp.AcquireRequest()
166 defer fasthttp.ReleaseRequest(req)
[12]167 req.SetConnectionClose()
[1]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
[11]191 err = CLIENT.DoTimeout(req, resp, p.RequestTimeout)
192
193 if err != nil {
[35]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 }
[1]201 return
202 }
203
204 if resp.StatusCode() != 200 {
205 switch resp.StatusCode() {
[7]206 case 301, 302, 303, 307, 308:
[1]207 loc := resp.Header.Peek("Location")
208 if loc != nil {
[23]209 rc := &RequestConfig{Key: p.Key, BaseURL: parsedURI}
210 url, err := rc.ProxifyURI(string(loc))
[1]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 }
[37]219 error_message := fmt.Sprintf("invalid response: %d", resp.StatusCode())
220 p.serveMainPage(ctx, resp.StatusCode(), errors.New(error_message))
[1]221 return
222 }
223
224 contentType := resp.Header.Peek("Content-Type")
225
226 if contentType == nil {
[35]227 // HTTP status code 503 : Service Unavailable
228 p.serveMainPage(ctx, 503, errors.New("invalid content type"))
[1]229 return
230 }
231
[17]232 if bytes.Contains(bytes.ToLower(contentType), []byte("javascript")) {
[35]233 // HTTP status code 403 : Forbidden
234 p.serveMainPage(ctx, 403, errors.New("forbidden content type"))
[17]235 return
236 }
237
[1]238 contentInfo := bytes.SplitN(contentType, []byte(";"), 2)
239
240 var responseBody []byte
241
[42]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()
[1]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")):
[23]262 sanitizeCSS(&RequestConfig{Key: p.Key, BaseURL: parsedURI}, ctx, responseBody)
[1]263 case bytes.Contains(contentType, []byte("html")):
[23]264 sanitizeHTML(&RequestConfig{Key: p.Key, BaseURL: parsedURI}, ctx, responseBody)
[1]265 default:
[39]266 if ctx.Request.Header.Peek("Content-Disposition") != nil {
267 ctx.Response.Header.AddBytesV("Content-Disposition", ctx.Request.Header.Peek("Content-Disposition"))
268 }
[1]269 ctx.Write(responseBody)
270 }
271}
272
[10]273func appRequestHandler(ctx *fasthttp.RequestCtx) bool {
[11]274 // serve robots.txt
[10]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 }
[11]280
[10]281 return false
282}
283
[1]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
[9]299func sanitizeCSS(rc *RequestConfig, out io.Writer, css []byte) {
[1]300 // TODO
301
302 urlSlices := CSS_URL_REGEXP.FindAllSubmatchIndex(css, -1)
303
304 if urlSlices == nil {
[9]305 out.Write(css)
[1]306 return
307 }
308
309 startIndex := 0
310
311 for _, s := range urlSlices {
[15]312 urlStart := s[4]
313 urlEnd := s[5]
[1]314
[23]315 if uri, err := rc.ProxifyURI(string(css[urlStart:urlEnd])); err == nil {
[9]316 out.Write(css[startIndex:urlStart])
317 out.Write([]byte(uri))
[1]318 startIndex = urlEnd
319 } else {
[36]320 log.Println("cannot proxify css uri:", string(css[urlStart:urlEnd]))
[1]321 }
322 }
323 if startIndex < len(css) {
[9]324 out.Write(css[startIndex:len(css)])
[1]325 }
326}
327
[9]328func sanitizeHTML(rc *RequestConfig, out io.Writer, htmlDoc []byte) {
[1]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 }
[38]359 if bytes.Equal(tag, []byte("base")) {
360 for {
361 attrName, attrValue, moreAttr := decoder.TagAttr()
[42]362 if !bytes.Equal(attrName, []byte("href")) {
363 continue
[38]364 }
[42]365 parsedURI, err := url.Parse(string(attrValue))
366 if err == nil {
367 rc.BaseURL = parsedURI
368 }
[38]369 if !moreAttr {
370 break
371 }
372 }
373 break
374 }
[1]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()
[21]383 attrs = append(attrs, [][]byte{
384 attrName,
385 attrValue,
386 []byte(html.EscapeString(string(attrValue))),
387 })
[1]388 if !moreAttr {
389 break
390 }
391 }
[13]392 }
393 if bytes.Equal(tag, []byte("link")) {
394 sanitizeLinkTag(rc, out, attrs)
395 break
396 }
397
[42]398 if bytes.Equal(tag, []byte("meta")) {
399 sanitizeMetaTag(rc, out, attrs)
400 break
401 }
402
[13]403 fmt.Fprintf(out, "<%s", tag)
404
405 if hasAttrs {
[42]406 sanitizeAttrs(rc, out, attrs)
[1]407 }
[13]408
[1]409 if token == html.SelfClosingTagToken {
[9]410 fmt.Fprintf(out, " />")
[1]411 } else {
[9]412 fmt.Fprintf(out, ">")
[1]413 if bytes.Equal(tag, []byte("style")) {
414 state = STATE_IN_STYLE
415 }
416 }
[13]417
[42]418 if bytes.Equal(tag, []byte("head")) {
419 fmt.Fprintf(out, HTML_META_CONTENT_TYPE)
420 }
421
[1]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]))
[28]427 formURL = mergeURIs(rc.BaseURL, formURL)
[1]428 break
429 }
430 }
431 if formURL == nil {
[23]432 formURL = rc.BaseURL
[1]433 }
[2]434 urlStr := formURL.String()
435 var key string
436 if rc.Key != nil {
437 key = hash(urlStr, rc.Key)
438 }
[9]439 fmt.Fprintf(out, HTML_FORM_EXTENSION, urlStr, key)
[1]440
441 }
442
443 case html.EndTagToken:
444 tag, _ := decoder.TagName()
445 writeEndTag := true
446 switch string(tag) {
447 case "body":
[23]448 fmt.Fprintf(out, HTML_BODY_EXTENSION, rc.BaseURL.String())
[1]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 {
[9]457 fmt.Fprintf(out, "</%s>", tag)
[1]458 }
459
460 case html.TextToken:
461 switch state {
462 case STATE_DEFAULT:
[9]463 fmt.Fprintf(out, "%s", decoder.Raw())
[1]464 case STATE_IN_STYLE:
[9]465 sanitizeCSS(rc, out, decoder.Raw())
[1]466 case STATE_IN_NOSCRIPT:
[9]467 sanitizeHTML(rc, out, decoder.Raw())
[1]468 }
469
470 case html.DoctypeToken, html.CommentToken:
[9]471 out.Write(decoder.Raw())
[1]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
[13]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 {
[21]513 sanitizeAttr(rc, out, attr[0], attr[1], attr[2])
[13]514 }
515 out.Write([]byte(">"))
516 }
517}
518
[42]519func sanitizeMetaTag(rc *RequestConfig, out io.Writer, attrs [][][]byte) {
[1]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 }
[42]532 if bytes.Equal(attrName, []byte("charset")) {
533 // exclude <meta charset="...">
534 return
535 }
[1]536 }
537
[42]538 if bytes.Equal(http_equiv, []byte("content-type")) {
539 return
540 }
541
542 out.Write([]byte("<meta"))
[14]543 urlIndex := bytes.Index(bytes.ToLower(content), []byte("url="))
544 if bytes.Equal(http_equiv, []byte("refresh")) && urlIndex != -1 {
545 contentUrl := content[urlIndex+4:]
[36]546 // special case of <meta http-equiv="refresh" content="0; url='example.com/url.with.quote.outside'">
[37]547 if len(contentUrl) >= 2 && (contentUrl[0] == byte('\'') || contentUrl[0] == byte('"')) {
[36]548 if contentUrl[0] == contentUrl[len(contentUrl)-1] {
[37]549 contentUrl = contentUrl[1 : len(contentUrl)-1]
[36]550 }
551 }
552 // output proxify result
[23]553 if uri, err := rc.ProxifyURI(string(contentUrl)); err == nil {
[14]554 fmt.Fprintf(out, ` http-equiv="refresh" content="%surl=%s"`, content[:urlIndex], uri)
[1]555 }
556 } else {
[9]557 sanitizeAttrs(rc, out, attrs)
[1]558 }
[42]559 out.Write([]byte(">"))
[1]560}
561
[9]562func sanitizeAttrs(rc *RequestConfig, out io.Writer, attrs [][][]byte) {
[1]563 for _, attr := range attrs {
[21]564 sanitizeAttr(rc, out, attr[0], attr[1], attr[2])
[1]565 }
566}
567
[21]568func sanitizeAttr(rc *RequestConfig, out io.Writer, attrName, attrValue, escapedAttrValue []byte) {
[1]569 if inArray(attrName, SAFE_ATTRIBUTES) {
[21]570 fmt.Fprintf(out, " %s=\"%s\"", attrName, escapedAttrValue)
[1]571 return
572 }
573 switch string(attrName) {
574 case "src", "href", "action":
[23]575 if uri, err := rc.ProxifyURI(string(attrValue)); err == nil {
[9]576 fmt.Fprintf(out, " %s=\"%s\"", attrName, uri)
[1]577 } else {
[36]578 log.Println("cannot proxify uri:", string(attrValue))
[1]579 }
580 case "style":
[21]581 cssAttr := bytes.NewBuffer(nil)
582 sanitizeCSS(rc, cssAttr, attrValue)
583 fmt.Fprintf(out, " %s=\"%s\"", attrName, html.EscapeString(string(cssAttr.Bytes())))
[1]584 }
585}
586
[36]587func mergeURIs(u1, u2 *url.URL) *url.URL {
[28]588 return u1.ResolveReference(u2)
[1]589}
590
[23]591func (rc *RequestConfig) ProxifyURI(uri string) (string, error) {
[28]592 // remove javascript protocol
593 if strings.HasPrefix(uri, "javascript:") {
594 return "", nil
595 }
[1]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 }
[28]609 u = mergeURIs(rc.BaseURL, u)
[1]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
[35]646func (p *Proxy) serveMainPage(ctx *fasthttp.RequestCtx, statusCode int, err error) {
[1]647 ctx.SetContentType("text/html")
[35]648 ctx.SetStatusCode(statusCode)
[1]649 ctx.Write([]byte(`<!doctype html>
650<head>
[11]651<title>MortyProxy</title>
[36]652<meta name="viewport" content="width=device-width, initial-scale=1 , maximum-scale=1.0, user-scalable=1" />
[11]653<style>
[36]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; }
[11]656input { border: 1px solid #888; padding: 0.3em; color: #444; background: #FFF; font-size: 1.1em; }
[36]657input[placeholder] { width:80%; }
[11]658a { text-decoration: none; #2980b9; }
659h1, h2 { font-weight: 200; margin-bottom: 2rem; }
660h1 { font-size: 3em; }
[36]661.container { flex:1; min-height: 100%; margin-bottom: 1em; }
662.footer { margin: 1em; }
[11]663.footer p { font-size: 0.8em; }
664</style>
[1]665</head>
[11]666<body>
[36]667 <div class="container">
668 <h1>MortyProxy</h1>
669`))
[11]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 }
[1]676 if p.Key == nil {
677 ctx.Write([]byte(`
[36]678 <form action="post">
679 Visit url: <input placeholder="https://url.." name="mortyurl" autofocus />
680 <input type="submit" value="go" />
681 </form>`))
[11]682 } else {
683 ctx.Write([]byte(`<h3>Warning! This instance does not support direct URL opening.</h3>`))
[1]684 }
685 ctx.Write([]byte(`
[36]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>
[1]692</body>
693</html>`))
694}
695
696func main() {
697
[2]698 listen := flag.String("listen", "127.0.0.1:3000", "Listen address")
[1]699 key := flag.String("key", "", "HMAC url validation key (hexadecimal encoded) - leave blank to disable")
[24]700 ipv6 := flag.Bool("ipv6", false, "Allow IPv6 HTTP requests")
[4]701 requestTimeout := flag.Uint("timeout", 2, "Request timeout")
[1]702 flag.Parse()
703
[24]704 if *ipv6 {
705 CLIENT.Dial = fasthttp.DialDualStack
706 }
707
[4]708 p := &Proxy{RequestTimeout: time.Duration(*requestTimeout) * time.Second}
[1]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.