source: code/trunk/morty.go@ 46

Last change on this file since 46 was 46, checked in by alex, 9 years ago
  • ignore svg and math tags (most of attributes are ignored in the current implementation)
  • add hreflang attribute as a safe one ( allows <meta rel="alternate" hreflang="x" href="" /> )
  • if the meta tag contains a http_equiv attribute:

if the value is safe, output the meta tag with the http_equiv attribute (avoid the <meta content="IE=edge">)
if the value is not safe, ignore the meta tag

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