source: code/trunk/morty.go@ 52

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

Merge pull request #37 from dalf/url_fragment

[fix] URI fragment are not encoded in the mortyurl

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