source: code/trunk/morty.go@ 43

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

Merge pull request #32 from dalf/encoding

[enh] support different encodings

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()
[43]362 if bytes.Equal(attrName, []byte("href")) {
363 parsedURI, err := url.Parse(string(attrValue))
364 if err == nil {
365 rc.BaseURL = parsedURI
366 }
[38]367 }
368 if !moreAttr {
369 break
370 }
371 }
372 break
373 }
[1]374 if bytes.Equal(tag, []byte("noscript")) {
375 state = STATE_IN_NOSCRIPT
376 break
377 }
378 var attrs [][][]byte
379 if hasAttrs {
380 for {
381 attrName, attrValue, moreAttr := decoder.TagAttr()
[21]382 attrs = append(attrs, [][]byte{
383 attrName,
384 attrValue,
385 []byte(html.EscapeString(string(attrValue))),
386 })
[1]387 if !moreAttr {
388 break
389 }
390 }
[13]391 }
392 if bytes.Equal(tag, []byte("link")) {
393 sanitizeLinkTag(rc, out, attrs)
394 break
395 }
396
[42]397 if bytes.Equal(tag, []byte("meta")) {
398 sanitizeMetaTag(rc, out, attrs)
399 break
400 }
401
[13]402 fmt.Fprintf(out, "<%s", tag)
403
404 if hasAttrs {
[42]405 sanitizeAttrs(rc, out, attrs)
[1]406 }
[13]407
[1]408 if token == html.SelfClosingTagToken {
[9]409 fmt.Fprintf(out, " />")
[1]410 } else {
[9]411 fmt.Fprintf(out, ">")
[1]412 if bytes.Equal(tag, []byte("style")) {
413 state = STATE_IN_STYLE
414 }
415 }
[13]416
[42]417 if bytes.Equal(tag, []byte("head")) {
418 fmt.Fprintf(out, HTML_META_CONTENT_TYPE)
419 }
420
[1]421 if bytes.Equal(tag, []byte("form")) {
422 var formURL *url.URL
423 for _, attr := range attrs {
424 if bytes.Equal(attr[0], []byte("action")) {
425 formURL, _ = url.Parse(string(attr[1]))
[28]426 formURL = mergeURIs(rc.BaseURL, formURL)
[1]427 break
428 }
429 }
430 if formURL == nil {
[23]431 formURL = rc.BaseURL
[1]432 }
[2]433 urlStr := formURL.String()
434 var key string
435 if rc.Key != nil {
436 key = hash(urlStr, rc.Key)
437 }
[9]438 fmt.Fprintf(out, HTML_FORM_EXTENSION, urlStr, key)
[1]439
440 }
441
442 case html.EndTagToken:
443 tag, _ := decoder.TagName()
444 writeEndTag := true
445 switch string(tag) {
446 case "body":
[23]447 fmt.Fprintf(out, HTML_BODY_EXTENSION, rc.BaseURL.String())
[1]448 case "style":
449 state = STATE_DEFAULT
450 case "noscript":
451 state = STATE_DEFAULT
452 writeEndTag = false
453 }
454 // skip noscript tags - only the tag, not the content, because javascript is sanitized
455 if writeEndTag {
[9]456 fmt.Fprintf(out, "</%s>", tag)
[1]457 }
458
459 case html.TextToken:
460 switch state {
461 case STATE_DEFAULT:
[9]462 fmt.Fprintf(out, "%s", decoder.Raw())
[1]463 case STATE_IN_STYLE:
[9]464 sanitizeCSS(rc, out, decoder.Raw())
[1]465 case STATE_IN_NOSCRIPT:
[9]466 sanitizeHTML(rc, out, decoder.Raw())
[1]467 }
468
469 case html.DoctypeToken, html.CommentToken:
[9]470 out.Write(decoder.Raw())
[1]471 }
472 } else {
473 switch token {
474 case html.StartTagToken:
475 tag, _ := decoder.TagName()
476 if inArray(tag, UNSAFE_ELEMENTS) {
477 unsafeElements = append(unsafeElements, tag)
478 }
479
480 case html.EndTagToken:
481 tag, _ := decoder.TagName()
482 if bytes.Equal(unsafeElements[len(unsafeElements)-1], tag) {
483 unsafeElements = unsafeElements[:len(unsafeElements)-1]
484 }
485 }
486 }
487 }
488}
489
[13]490func sanitizeLinkTag(rc *RequestConfig, out io.Writer, attrs [][][]byte) {
491 exclude := false
492 for _, attr := range attrs {
493 attrName := attr[0]
494 attrValue := attr[1]
495 if bytes.Equal(attrName, []byte("rel")) {
496 if bytes.Equal(attrValue, []byte("dns-prefetch")) {
497 exclude = true
498 break
499 }
500 }
501 if bytes.Equal(attrName, []byte("as")) {
502 if bytes.Equal(attrValue, []byte("script")) {
503 exclude = true
504 break
505 }
506 }
507 }
508
509 if !exclude {
510 out.Write([]byte("<link"))
511 for _, attr := range attrs {
[21]512 sanitizeAttr(rc, out, attr[0], attr[1], attr[2])
[13]513 }
514 out.Write([]byte(">"))
515 }
516}
517
[42]518func sanitizeMetaTag(rc *RequestConfig, out io.Writer, attrs [][][]byte) {
[1]519 var http_equiv []byte
520 var content []byte
521
522 for _, attr := range attrs {
523 attrName := attr[0]
524 attrValue := attr[1]
525 if bytes.Equal(attrName, []byte("http-equiv")) {
526 http_equiv = bytes.ToLower(attrValue)
527 }
528 if bytes.Equal(attrName, []byte("content")) {
529 content = attrValue
530 }
[42]531 if bytes.Equal(attrName, []byte("charset")) {
532 // exclude <meta charset="...">
533 return
534 }
[1]535 }
536
[42]537 if bytes.Equal(http_equiv, []byte("content-type")) {
538 return
539 }
540
541 out.Write([]byte("<meta"))
[14]542 urlIndex := bytes.Index(bytes.ToLower(content), []byte("url="))
543 if bytes.Equal(http_equiv, []byte("refresh")) && urlIndex != -1 {
544 contentUrl := content[urlIndex+4:]
[36]545 // special case of <meta http-equiv="refresh" content="0; url='example.com/url.with.quote.outside'">
[37]546 if len(contentUrl) >= 2 && (contentUrl[0] == byte('\'') || contentUrl[0] == byte('"')) {
[36]547 if contentUrl[0] == contentUrl[len(contentUrl)-1] {
[37]548 contentUrl = contentUrl[1 : len(contentUrl)-1]
[36]549 }
550 }
551 // output proxify result
[23]552 if uri, err := rc.ProxifyURI(string(contentUrl)); err == nil {
[14]553 fmt.Fprintf(out, ` http-equiv="refresh" content="%surl=%s"`, content[:urlIndex], uri)
[1]554 }
555 } else {
[9]556 sanitizeAttrs(rc, out, attrs)
[1]557 }
[42]558 out.Write([]byte(">"))
[1]559}
560
[9]561func sanitizeAttrs(rc *RequestConfig, out io.Writer, attrs [][][]byte) {
[1]562 for _, attr := range attrs {
[21]563 sanitizeAttr(rc, out, attr[0], attr[1], attr[2])
[1]564 }
565}
566
[21]567func sanitizeAttr(rc *RequestConfig, out io.Writer, attrName, attrValue, escapedAttrValue []byte) {
[1]568 if inArray(attrName, SAFE_ATTRIBUTES) {
[21]569 fmt.Fprintf(out, " %s=\"%s\"", attrName, escapedAttrValue)
[1]570 return
571 }
572 switch string(attrName) {
573 case "src", "href", "action":
[23]574 if uri, err := rc.ProxifyURI(string(attrValue)); err == nil {
[9]575 fmt.Fprintf(out, " %s=\"%s\"", attrName, uri)
[1]576 } else {
[36]577 log.Println("cannot proxify uri:", string(attrValue))
[1]578 }
579 case "style":
[21]580 cssAttr := bytes.NewBuffer(nil)
581 sanitizeCSS(rc, cssAttr, attrValue)
582 fmt.Fprintf(out, " %s=\"%s\"", attrName, html.EscapeString(string(cssAttr.Bytes())))
[1]583 }
584}
585
[36]586func mergeURIs(u1, u2 *url.URL) *url.URL {
[28]587 return u1.ResolveReference(u2)
[1]588}
589
[23]590func (rc *RequestConfig) ProxifyURI(uri string) (string, error) {
[28]591 // remove javascript protocol
592 if strings.HasPrefix(uri, "javascript:") {
593 return "", nil
594 }
[1]595 // TODO check malicious data: - e.g. data:script
596 if strings.HasPrefix(uri, "data:") {
597 return uri, nil
598 }
599
600 if len(uri) > 0 && uri[0] == '#' {
601 return uri, nil
602 }
603
604 u, err := url.Parse(uri)
605 if err != nil {
606 return "", err
607 }
[28]608 u = mergeURIs(rc.BaseURL, u)
[1]609
610 uri = u.String()
611
612 if rc.Key == nil {
613 return fmt.Sprintf("./?mortyurl=%s", url.QueryEscape(uri)), nil
614 }
615 return fmt.Sprintf("./?mortyhash=%s&mortyurl=%s", hash(uri, rc.Key), url.QueryEscape(uri)), nil
616}
617
618func inArray(b []byte, a [][]byte) bool {
619 for _, b2 := range a {
620 if bytes.Equal(b, b2) {
621 return true
622 }
623 }
624 return false
625}
626
627func hash(msg string, key []byte) string {
628 mac := hmac.New(sha256.New, key)
629 mac.Write([]byte(msg))
630 return hex.EncodeToString(mac.Sum(nil))
631}
632
633func verifyRequestURI(uri, hashMsg, key []byte) bool {
634 h := make([]byte, hex.DecodedLen(len(hashMsg)))
635 _, err := hex.Decode(h, hashMsg)
636 if err != nil {
637 log.Println("hmac error:", err)
638 return false
639 }
640 mac := hmac.New(sha256.New, key)
641 mac.Write(uri)
642 return hmac.Equal(h, mac.Sum(nil))
643}
644
[35]645func (p *Proxy) serveMainPage(ctx *fasthttp.RequestCtx, statusCode int, err error) {
[1]646 ctx.SetContentType("text/html")
[35]647 ctx.SetStatusCode(statusCode)
[1]648 ctx.Write([]byte(`<!doctype html>
649<head>
[11]650<title>MortyProxy</title>
[36]651<meta name="viewport" content="width=device-width, initial-scale=1 , maximum-scale=1.0, user-scalable=1" />
[11]652<style>
[36]653html { height: 100%; }
654body { 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]655input { border: 1px solid #888; padding: 0.3em; color: #444; background: #FFF; font-size: 1.1em; }
[36]656input[placeholder] { width:80%; }
[11]657a { text-decoration: none; #2980b9; }
658h1, h2 { font-weight: 200; margin-bottom: 2rem; }
659h1 { font-size: 3em; }
[36]660.container { flex:1; min-height: 100%; margin-bottom: 1em; }
661.footer { margin: 1em; }
[11]662.footer p { font-size: 0.8em; }
663</style>
[1]664</head>
[11]665<body>
[36]666 <div class="container">
667 <h1>MortyProxy</h1>
668`))
[11]669 if err != nil {
670 log.Println("error:", err)
671 ctx.Write([]byte("<h2>Error: "))
672 ctx.Write([]byte(html.EscapeString(err.Error())))
673 ctx.Write([]byte("</h2>"))
674 }
[1]675 if p.Key == nil {
676 ctx.Write([]byte(`
[36]677 <form action="post">
678 Visit url: <input placeholder="https://url.." name="mortyurl" autofocus />
679 <input type="submit" value="go" />
680 </form>`))
[11]681 } else {
682 ctx.Write([]byte(`<h3>Warning! This instance does not support direct URL opening.</h3>`))
[1]683 }
684 ctx.Write([]byte(`
[36]685 </div>
686 <div class="footer">
687 <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 />
688 <a href="https://github.com/asciimoo/morty">view on github</a>
689 </p>
690 </div>
[1]691</body>
692</html>`))
693}
694
695func main() {
696
[2]697 listen := flag.String("listen", "127.0.0.1:3000", "Listen address")
[1]698 key := flag.String("key", "", "HMAC url validation key (hexadecimal encoded) - leave blank to disable")
[24]699 ipv6 := flag.Bool("ipv6", false, "Allow IPv6 HTTP requests")
[4]700 requestTimeout := flag.Uint("timeout", 2, "Request timeout")
[1]701 flag.Parse()
702
[24]703 if *ipv6 {
704 CLIENT.Dial = fasthttp.DialDualStack
705 }
706
[4]707 p := &Proxy{RequestTimeout: time.Duration(*requestTimeout) * time.Second}
[1]708
709 if *key != "" {
710 p.Key = []byte(*key)
711 }
712
713 log.Println("listening on", *listen)
714
715 if err := fasthttp.ListenAndServe(*listen, p.RequestHandler); err != nil {
716 log.Fatal("Error in ListenAndServe:", err)
717 }
718}
Note: See TracBrowser for help on using the repository browser.