source: code/trunk/morty.go@ 45

Last change on this file since 45 was 45, checked in by asciimoo, 9 years ago

Merge pull request #35 from dalf/url_encoding

Fetched URI matched the mortyurl.

File size: 18.0 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"
[45]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
[45]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
[44]169 requestURIStr := string(requestURI)
[1]170
[44]171 log.Println("getting", requestURIStr)
[1]172
[44]173 req.SetRequestURI(requestURIStr)
[1]174 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"))
175
176 resp := fasthttp.AcquireResponse()
177 defer fasthttp.ReleaseResponse(resp)
178
179 req.Header.SetMethodBytes(ctx.Method())
180 if ctx.IsPost() || ctx.IsPut() {
181 req.SetBody(ctx.PostBody())
182 }
183
[11]184 err = CLIENT.DoTimeout(req, resp, p.RequestTimeout)
185
186 if err != nil {
[35]187 if err == fasthttp.ErrTimeout {
188 // HTTP status code 504 : Gateway Time-Out
189 p.serveMainPage(ctx, 504, err)
190 } else {
191 // HTTP status code 500 : Internal Server Error
192 p.serveMainPage(ctx, 500, err)
193 }
[1]194 return
195 }
196
197 if resp.StatusCode() != 200 {
198 switch resp.StatusCode() {
[7]199 case 301, 302, 303, 307, 308:
[1]200 loc := resp.Header.Peek("Location")
201 if loc != nil {
[23]202 rc := &RequestConfig{Key: p.Key, BaseURL: parsedURI}
203 url, err := rc.ProxifyURI(string(loc))
[1]204 if err == nil {
205 ctx.SetStatusCode(resp.StatusCode())
206 ctx.Response.Header.Add("Location", url)
207 log.Println("redirect to", string(loc))
208 return
209 }
210 }
211 }
[44]212 error_message := fmt.Sprintf("invalid response: %d (%s)", resp.StatusCode(), requestURIStr)
[37]213 p.serveMainPage(ctx, resp.StatusCode(), errors.New(error_message))
[1]214 return
215 }
216
217 contentType := resp.Header.Peek("Content-Type")
218
219 if contentType == nil {
[35]220 // HTTP status code 503 : Service Unavailable
221 p.serveMainPage(ctx, 503, errors.New("invalid content type"))
[1]222 return
223 }
224
[17]225 if bytes.Contains(bytes.ToLower(contentType), []byte("javascript")) {
[35]226 // HTTP status code 403 : Forbidden
227 p.serveMainPage(ctx, 403, errors.New("forbidden content type"))
[17]228 return
229 }
230
[1]231 contentInfo := bytes.SplitN(contentType, []byte(";"), 2)
232
233 var responseBody []byte
234
[45]235 if len(contentInfo) == 2 && bytes.Contains(contentInfo[0], []byte("text")) {
236 e, ename, _ := charset.DetermineEncoding(resp.Body(), string(contentType))
237 if (e != encoding.Nop) && (!strings.EqualFold("utf-8", ename)) {
238 responseBody, err = e.NewDecoder().Bytes(resp.Body())
239 if err != nil {
240 // HTTP status code 503 : Service Unavailable
241 p.serveMainPage(ctx, 503, err)
242 return
243 }
244 } else {
245 responseBody = resp.Body()
[1]246 }
247 } else {
248 responseBody = resp.Body()
249 }
250
251 ctx.SetContentType(fmt.Sprintf("%s; charset=UTF-8", contentInfo[0]))
252
253 switch {
254 case bytes.Contains(contentType, []byte("css")):
[23]255 sanitizeCSS(&RequestConfig{Key: p.Key, BaseURL: parsedURI}, ctx, responseBody)
[1]256 case bytes.Contains(contentType, []byte("html")):
[23]257 sanitizeHTML(&RequestConfig{Key: p.Key, BaseURL: parsedURI}, ctx, responseBody)
[1]258 default:
[39]259 if ctx.Request.Header.Peek("Content-Disposition") != nil {
260 ctx.Response.Header.AddBytesV("Content-Disposition", ctx.Request.Header.Peek("Content-Disposition"))
261 }
[1]262 ctx.Write(responseBody)
263 }
264}
265
[10]266func appRequestHandler(ctx *fasthttp.RequestCtx) bool {
[11]267 // serve robots.txt
[10]268 if bytes.Equal(ctx.Path(), []byte("/robots.txt")) {
269 ctx.SetContentType("text/plain")
270 ctx.Write([]byte("User-Agent: *\nDisallow: /\n"))
271 return true
272 }
[11]273
[10]274 return false
275}
276
[1]277func popRequestParam(ctx *fasthttp.RequestCtx, paramName []byte) []byte {
278 param := ctx.QueryArgs().PeekBytes(paramName)
279
280 if param == nil {
281 param = ctx.PostArgs().PeekBytes(paramName)
282 if param != nil {
283 ctx.PostArgs().DelBytes(paramName)
284 }
285 } else {
286 ctx.QueryArgs().DelBytes(paramName)
287 }
288
289 return param
290}
291
[9]292func sanitizeCSS(rc *RequestConfig, out io.Writer, css []byte) {
[1]293 // TODO
294
295 urlSlices := CSS_URL_REGEXP.FindAllSubmatchIndex(css, -1)
296
297 if urlSlices == nil {
[9]298 out.Write(css)
[1]299 return
300 }
301
302 startIndex := 0
303
304 for _, s := range urlSlices {
[15]305 urlStart := s[4]
306 urlEnd := s[5]
[1]307
[23]308 if uri, err := rc.ProxifyURI(string(css[urlStart:urlEnd])); err == nil {
[9]309 out.Write(css[startIndex:urlStart])
310 out.Write([]byte(uri))
[1]311 startIndex = urlEnd
312 } else {
[36]313 log.Println("cannot proxify css uri:", string(css[urlStart:urlEnd]))
[1]314 }
315 }
316 if startIndex < len(css) {
[9]317 out.Write(css[startIndex:len(css)])
[1]318 }
319}
320
[9]321func sanitizeHTML(rc *RequestConfig, out io.Writer, htmlDoc []byte) {
[1]322 r := bytes.NewReader(htmlDoc)
323 decoder := html.NewTokenizer(r)
324 decoder.AllowCDATA(true)
325
326 unsafeElements := make([][]byte, 0, 8)
327 state := STATE_DEFAULT
328 for {
329 token := decoder.Next()
330 if token == html.ErrorToken {
331 err := decoder.Err()
332 if err != io.EOF {
333 log.Println("failed to parse HTML:")
334 }
335 break
336 }
337
338 if len(unsafeElements) == 0 {
339
340 switch token {
341 case html.StartTagToken, html.SelfClosingTagToken:
342 tag, hasAttrs := decoder.TagName()
343 safe := !inArray(tag, UNSAFE_ELEMENTS)
344 if !safe {
345 if !inArray(tag, SELF_CLOSING_ELEMENTS) {
346 var unsafeTag []byte = make([]byte, len(tag))
347 copy(unsafeTag, tag)
348 unsafeElements = append(unsafeElements, unsafeTag)
349 }
350 break
351 }
[38]352 if bytes.Equal(tag, []byte("base")) {
353 for {
354 attrName, attrValue, moreAttr := decoder.TagAttr()
[45]355 if bytes.Equal(attrName, []byte("href")) {
356 parsedURI, err := url.Parse(string(attrValue))
357 if err == nil {
358 rc.BaseURL = parsedURI
359 }
[38]360 }
361 if !moreAttr {
362 break
363 }
364 }
365 break
366 }
[1]367 if bytes.Equal(tag, []byte("noscript")) {
368 state = STATE_IN_NOSCRIPT
369 break
370 }
371 var attrs [][][]byte
372 if hasAttrs {
373 for {
374 attrName, attrValue, moreAttr := decoder.TagAttr()
[21]375 attrs = append(attrs, [][]byte{
376 attrName,
377 attrValue,
378 []byte(html.EscapeString(string(attrValue))),
379 })
[1]380 if !moreAttr {
381 break
382 }
383 }
[13]384 }
385 if bytes.Equal(tag, []byte("link")) {
386 sanitizeLinkTag(rc, out, attrs)
387 break
388 }
389
[45]390 if bytes.Equal(tag, []byte("meta")) {
391 sanitizeMetaTag(rc, out, attrs)
392 break
393 }
394
[13]395 fmt.Fprintf(out, "<%s", tag)
396
397 if hasAttrs {
[45]398 sanitizeAttrs(rc, out, attrs)
[1]399 }
[13]400
[1]401 if token == html.SelfClosingTagToken {
[9]402 fmt.Fprintf(out, " />")
[1]403 } else {
[9]404 fmt.Fprintf(out, ">")
[1]405 if bytes.Equal(tag, []byte("style")) {
406 state = STATE_IN_STYLE
407 }
408 }
[13]409
[45]410 if bytes.Equal(tag, []byte("head")) {
411 fmt.Fprintf(out, HTML_META_CONTENT_TYPE)
412 }
413
[1]414 if bytes.Equal(tag, []byte("form")) {
415 var formURL *url.URL
416 for _, attr := range attrs {
417 if bytes.Equal(attr[0], []byte("action")) {
418 formURL, _ = url.Parse(string(attr[1]))
[28]419 formURL = mergeURIs(rc.BaseURL, formURL)
[1]420 break
421 }
422 }
423 if formURL == nil {
[23]424 formURL = rc.BaseURL
[1]425 }
[2]426 urlStr := formURL.String()
427 var key string
428 if rc.Key != nil {
429 key = hash(urlStr, rc.Key)
430 }
[9]431 fmt.Fprintf(out, HTML_FORM_EXTENSION, urlStr, key)
[1]432
433 }
434
435 case html.EndTagToken:
436 tag, _ := decoder.TagName()
437 writeEndTag := true
438 switch string(tag) {
439 case "body":
[23]440 fmt.Fprintf(out, HTML_BODY_EXTENSION, rc.BaseURL.String())
[1]441 case "style":
442 state = STATE_DEFAULT
443 case "noscript":
444 state = STATE_DEFAULT
445 writeEndTag = false
446 }
447 // skip noscript tags - only the tag, not the content, because javascript is sanitized
448 if writeEndTag {
[9]449 fmt.Fprintf(out, "</%s>", tag)
[1]450 }
451
452 case html.TextToken:
453 switch state {
454 case STATE_DEFAULT:
[9]455 fmt.Fprintf(out, "%s", decoder.Raw())
[1]456 case STATE_IN_STYLE:
[9]457 sanitizeCSS(rc, out, decoder.Raw())
[1]458 case STATE_IN_NOSCRIPT:
[9]459 sanitizeHTML(rc, out, decoder.Raw())
[1]460 }
461
462 case html.DoctypeToken, html.CommentToken:
[9]463 out.Write(decoder.Raw())
[1]464 }
465 } else {
466 switch token {
467 case html.StartTagToken:
468 tag, _ := decoder.TagName()
469 if inArray(tag, UNSAFE_ELEMENTS) {
470 unsafeElements = append(unsafeElements, tag)
471 }
472
473 case html.EndTagToken:
474 tag, _ := decoder.TagName()
475 if bytes.Equal(unsafeElements[len(unsafeElements)-1], tag) {
476 unsafeElements = unsafeElements[:len(unsafeElements)-1]
477 }
478 }
479 }
480 }
481}
482
[13]483func sanitizeLinkTag(rc *RequestConfig, out io.Writer, attrs [][][]byte) {
484 exclude := false
485 for _, attr := range attrs {
486 attrName := attr[0]
487 attrValue := attr[1]
488 if bytes.Equal(attrName, []byte("rel")) {
489 if bytes.Equal(attrValue, []byte("dns-prefetch")) {
490 exclude = true
491 break
492 }
493 }
494 if bytes.Equal(attrName, []byte("as")) {
495 if bytes.Equal(attrValue, []byte("script")) {
496 exclude = true
497 break
498 }
499 }
500 }
501
502 if !exclude {
503 out.Write([]byte("<link"))
504 for _, attr := range attrs {
[21]505 sanitizeAttr(rc, out, attr[0], attr[1], attr[2])
[13]506 }
507 out.Write([]byte(">"))
508 }
509}
510
[45]511func sanitizeMetaTag(rc *RequestConfig, out io.Writer, attrs [][][]byte) {
[1]512 var http_equiv []byte
513 var content []byte
514
515 for _, attr := range attrs {
516 attrName := attr[0]
517 attrValue := attr[1]
518 if bytes.Equal(attrName, []byte("http-equiv")) {
519 http_equiv = bytes.ToLower(attrValue)
520 }
521 if bytes.Equal(attrName, []byte("content")) {
522 content = attrValue
523 }
[45]524 if bytes.Equal(attrName, []byte("charset")) {
525 // exclude <meta charset="...">
526 return
527 }
[1]528 }
529
[45]530 if bytes.Equal(http_equiv, []byte("content-type")) {
531 return
532 }
533
534 out.Write([]byte("<meta"))
[14]535 urlIndex := bytes.Index(bytes.ToLower(content), []byte("url="))
536 if bytes.Equal(http_equiv, []byte("refresh")) && urlIndex != -1 {
537 contentUrl := content[urlIndex+4:]
[36]538 // special case of <meta http-equiv="refresh" content="0; url='example.com/url.with.quote.outside'">
[37]539 if len(contentUrl) >= 2 && (contentUrl[0] == byte('\'') || contentUrl[0] == byte('"')) {
[36]540 if contentUrl[0] == contentUrl[len(contentUrl)-1] {
[37]541 contentUrl = contentUrl[1 : len(contentUrl)-1]
[36]542 }
543 }
544 // output proxify result
[23]545 if uri, err := rc.ProxifyURI(string(contentUrl)); err == nil {
[14]546 fmt.Fprintf(out, ` http-equiv="refresh" content="%surl=%s"`, content[:urlIndex], uri)
[1]547 }
548 } else {
[9]549 sanitizeAttrs(rc, out, attrs)
[1]550 }
[45]551 out.Write([]byte(">"))
[1]552}
553
[9]554func sanitizeAttrs(rc *RequestConfig, out io.Writer, attrs [][][]byte) {
[1]555 for _, attr := range attrs {
[21]556 sanitizeAttr(rc, out, attr[0], attr[1], attr[2])
[1]557 }
558}
559
[21]560func sanitizeAttr(rc *RequestConfig, out io.Writer, attrName, attrValue, escapedAttrValue []byte) {
[1]561 if inArray(attrName, SAFE_ATTRIBUTES) {
[21]562 fmt.Fprintf(out, " %s=\"%s\"", attrName, escapedAttrValue)
[1]563 return
564 }
565 switch string(attrName) {
566 case "src", "href", "action":
[23]567 if uri, err := rc.ProxifyURI(string(attrValue)); err == nil {
[9]568 fmt.Fprintf(out, " %s=\"%s\"", attrName, uri)
[1]569 } else {
[36]570 log.Println("cannot proxify uri:", string(attrValue))
[1]571 }
572 case "style":
[21]573 cssAttr := bytes.NewBuffer(nil)
574 sanitizeCSS(rc, cssAttr, attrValue)
575 fmt.Fprintf(out, " %s=\"%s\"", attrName, html.EscapeString(string(cssAttr.Bytes())))
[1]576 }
577}
578
[36]579func mergeURIs(u1, u2 *url.URL) *url.URL {
[28]580 return u1.ResolveReference(u2)
[1]581}
582
[23]583func (rc *RequestConfig) ProxifyURI(uri string) (string, error) {
[28]584 // remove javascript protocol
585 if strings.HasPrefix(uri, "javascript:") {
586 return "", nil
587 }
[1]588 // TODO check malicious data: - e.g. data:script
589 if strings.HasPrefix(uri, "data:") {
590 return uri, nil
591 }
592
593 if len(uri) > 0 && uri[0] == '#' {
594 return uri, nil
595 }
596
597 u, err := url.Parse(uri)
598 if err != nil {
599 return "", err
600 }
[28]601 u = mergeURIs(rc.BaseURL, u)
[1]602
603 uri = u.String()
604
605 if rc.Key == nil {
606 return fmt.Sprintf("./?mortyurl=%s", url.QueryEscape(uri)), nil
607 }
[44]608
[1]609 return fmt.Sprintf("./?mortyhash=%s&mortyurl=%s", hash(uri, rc.Key), url.QueryEscape(uri)), nil
610}
611
612func inArray(b []byte, a [][]byte) bool {
613 for _, b2 := range a {
614 if bytes.Equal(b, b2) {
615 return true
616 }
617 }
618 return false
619}
620
621func hash(msg string, key []byte) string {
622 mac := hmac.New(sha256.New, key)
623 mac.Write([]byte(msg))
624 return hex.EncodeToString(mac.Sum(nil))
625}
626
627func verifyRequestURI(uri, hashMsg, key []byte) bool {
628 h := make([]byte, hex.DecodedLen(len(hashMsg)))
629 _, err := hex.Decode(h, hashMsg)
630 if err != nil {
631 log.Println("hmac error:", err)
632 return false
633 }
634 mac := hmac.New(sha256.New, key)
635 mac.Write(uri)
636 return hmac.Equal(h, mac.Sum(nil))
637}
638
[35]639func (p *Proxy) serveMainPage(ctx *fasthttp.RequestCtx, statusCode int, err error) {
[1]640 ctx.SetContentType("text/html")
[35]641 ctx.SetStatusCode(statusCode)
[1]642 ctx.Write([]byte(`<!doctype html>
643<head>
[11]644<title>MortyProxy</title>
[36]645<meta name="viewport" content="width=device-width, initial-scale=1 , maximum-scale=1.0, user-scalable=1" />
[11]646<style>
[36]647html { height: 100%; }
648body { 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]649input { border: 1px solid #888; padding: 0.3em; color: #444; background: #FFF; font-size: 1.1em; }
[36]650input[placeholder] { width:80%; }
[11]651a { text-decoration: none; #2980b9; }
652h1, h2 { font-weight: 200; margin-bottom: 2rem; }
653h1 { font-size: 3em; }
[36]654.container { flex:1; min-height: 100%; margin-bottom: 1em; }
655.footer { margin: 1em; }
[11]656.footer p { font-size: 0.8em; }
657</style>
[1]658</head>
[11]659<body>
[36]660 <div class="container">
661 <h1>MortyProxy</h1>
662`))
[11]663 if err != nil {
664 log.Println("error:", err)
665 ctx.Write([]byte("<h2>Error: "))
666 ctx.Write([]byte(html.EscapeString(err.Error())))
667 ctx.Write([]byte("</h2>"))
668 }
[1]669 if p.Key == nil {
670 ctx.Write([]byte(`
[36]671 <form action="post">
672 Visit url: <input placeholder="https://url.." name="mortyurl" autofocus />
673 <input type="submit" value="go" />
674 </form>`))
[11]675 } else {
676 ctx.Write([]byte(`<h3>Warning! This instance does not support direct URL opening.</h3>`))
[1]677 }
678 ctx.Write([]byte(`
[36]679 </div>
680 <div class="footer">
681 <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 />
682 <a href="https://github.com/asciimoo/morty">view on github</a>
683 </p>
684 </div>
[1]685</body>
686</html>`))
687}
688
689func main() {
690
[2]691 listen := flag.String("listen", "127.0.0.1:3000", "Listen address")
[1]692 key := flag.String("key", "", "HMAC url validation key (hexadecimal encoded) - leave blank to disable")
[24]693 ipv6 := flag.Bool("ipv6", false, "Allow IPv6 HTTP requests")
[4]694 requestTimeout := flag.Uint("timeout", 2, "Request timeout")
[1]695 flag.Parse()
696
[24]697 if *ipv6 {
698 CLIENT.Dial = fasthttp.DialDualStack
699 }
700
[4]701 p := &Proxy{RequestTimeout: time.Duration(*requestTimeout) * time.Second}
[1]702
703 if *key != "" {
704 p.Key = []byte(*key)
705 }
706
707 log.Println("listening on", *listen)
708
709 if err := fasthttp.ListenAndServe(*listen, p.RequestHandler); err != nil {
710 log.Fatal("Error in ListenAndServe:", err)
711 }
712}
Note: See TracBrowser for help on using the repository browser.