source: code/trunk/morty.go@ 61

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

[mod] data scheme URL : allow only images (png, jpeg, pjpeg, gif, webp)

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