Changeset 69 in code for trunk


Ignore:
Timestamp:
Dec 23, 2016, 8:10:33 PM (8 years ago)
Author:
alex
Message:

[enh] for protocols others than HTTP(S) or for .onion domains, Morty serves a HTML page to the original URL with a warning.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/morty.go

    r68 r69  
    220220`
    221221
    222 var FAVICON_BYTES []byte
    223 
    224 func init() {
    225         FaviconBase64 := "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAYAAABPYyMiAAAABmJLR0T///////8JWPfcAAAACXBIWXMAAABIAAAASABGyWs+AAAAF0lEQVRIx2NgGAWjYBSMglEwCkbBSAcACBAAAeaR9cIAAAAASUVORK5CYII"
    226 
    227         FAVICON_BYTES, _ = base64.StdEncoding.DecodeString(FaviconBase64)
    228 }
    229 
    230 func (p *Proxy) RequestHandler(ctx *fasthttp.RequestCtx) {
    231 
    232         if appRequestHandler(ctx) {
    233                 return
    234         }
    235 
    236         requestHash := popRequestParam(ctx, []byte("mortyhash"))
    237 
    238         requestURI := popRequestParam(ctx, []byte("mortyurl"))
    239 
    240         if requestURI == nil {
    241                 p.serveMainPage(ctx, 200, nil)
    242                 return
    243         }
    244 
    245         if p.Key != nil {
    246                 if !verifyRequestURI(requestURI, requestHash, p.Key) {
    247                         // HTTP status code 403 : Forbidden
    248                         p.serveMainPage(ctx, 403, errors.New(`invalid "mortyhash" parameter`))
    249                         return
    250                 }
    251         }
    252 
    253         parsedURI, err := url.Parse(string(requestURI))
    254 
    255         if strings.HasSuffix(parsedURI.Host, ".onion") {
    256                 // HTTP status code 501 : Not Implemented
    257                 p.serveMainPage(ctx, 501, errors.New("Tor urls are not supported yet"))
    258                 return
    259         }
    260 
    261         if err != nil {
    262                 // HTTP status code 500 : Internal Server Error
    263                 p.serveMainPage(ctx, 500, err)
    264                 return
    265         }
    266 
    267         req := fasthttp.AcquireRequest()
    268         defer fasthttp.ReleaseRequest(req)
    269         req.SetConnectionClose()
    270 
    271         requestURIStr := string(requestURI)
    272 
    273         log.Println("getting", requestURIStr)
    274 
    275         req.SetRequestURI(requestURIStr)
    276         req.Header.SetUserAgentBytes([]byte("Mozilla/5.0 (Windows NT 10.0; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0"))
    277 
    278         resp := fasthttp.AcquireResponse()
    279         defer fasthttp.ReleaseResponse(resp)
    280 
    281         req.Header.SetMethodBytes(ctx.Method())
    282         if ctx.IsPost() || ctx.IsPut() {
    283                 req.SetBody(ctx.PostBody())
    284         }
    285 
    286         err = CLIENT.DoTimeout(req, resp, p.RequestTimeout)
    287 
    288         if err != nil {
    289                 if err == fasthttp.ErrTimeout {
    290                         // HTTP status code 504 : Gateway Time-Out
    291                         p.serveMainPage(ctx, 504, err)
    292                 } else {
    293                         // HTTP status code 500 : Internal Server Error
    294                         p.serveMainPage(ctx, 500, err)
    295                 }
    296                 return
    297         }
    298 
    299         if resp.StatusCode() != 200 {
    300                 switch resp.StatusCode() {
    301                 case 301, 302, 303, 307, 308:
    302                         loc := resp.Header.Peek("Location")
    303                         if loc != nil {
    304                                 rc := &RequestConfig{Key: p.Key, BaseURL: parsedURI}
    305                                 url, err := rc.ProxifyURI(loc)
    306                                 if err == nil {
    307                                         ctx.SetStatusCode(resp.StatusCode())
    308                                         ctx.Response.Header.Add("Location", url)
    309                                         log.Println("redirect to", string(loc))
    310                                         return
    311                                 }
    312                         }
    313                 }
    314                 error_message := fmt.Sprintf("invalid response: %d (%s)", resp.StatusCode(), requestURIStr)
    315                 p.serveMainPage(ctx, resp.StatusCode(), errors.New(error_message))
    316                 return
    317         }
    318 
    319         contentTypeBytes := resp.Header.Peek("Content-Type")
    320 
    321         if contentTypeBytes == nil {
    322                 // HTTP status code 503 : Service Unavailable
    323                 p.serveMainPage(ctx, 503, errors.New("invalid content type"))
    324                 return
    325         }
    326 
    327         contentTypeString := string(contentTypeBytes)
    328 
    329         // decode Content-Type header
    330         contentType, error := contenttype.ParseContentType(contentTypeString)
    331         if error != nil {
    332                 // HTTP status code 503 : Service Unavailable
    333                 p.serveMainPage(ctx, 503, errors.New("invalid content type"))
    334                 return
    335         }
    336 
    337         // content-disposition
    338         contentDispositionBytes := ctx.Request.Header.Peek("Content-Disposition")
    339 
    340         // check content type
    341         if !ALLOWED_CONTENTTYPE_FILTER(contentType) {
    342                 // it is not a usual content type
    343                 if ALLOWED_CONTENTTYPE_ATTACHMENT_FILTER(contentType) {
    344                         // force attachment for allowed content type
    345                         contentDispositionBytes = contentDispositionForceAttachment(contentDispositionBytes, parsedURI)
    346                 } else {
    347                         // deny access to forbidden content type
    348                         // HTTP status code 403 : Forbidden
    349                         p.serveMainPage(ctx, 403, errors.New("forbidden content type"))
    350                         return
    351                 }
    352         }
    353 
    354         // HACK : replace */xhtml by text/html
    355         if contentType.SubType == "xhtml" {
    356                 contentType.TopLevelType = "text"
    357                 contentType.SubType = "html"
    358                 contentType.Suffix = ""
    359         }
    360 
    361         // conversion to UTF-8
    362         var responseBody []byte
    363 
    364         if contentType.TopLevelType == "text" {
    365                 e, ename, _ := charset.DetermineEncoding(resp.Body(), contentTypeString)
    366                 if (e != encoding.Nop) && (!strings.EqualFold("utf-8", ename)) {
    367                         responseBody, err = e.NewDecoder().Bytes(resp.Body())
    368                         if err != nil {
    369                                 // HTTP status code 503 : Service Unavailable
    370                                 p.serveMainPage(ctx, 503, err)
    371                                 return
    372                         }
    373                 } else {
    374                         responseBody = resp.Body()
    375                 }
    376                 // update the charset or specify it
    377                 contentType.Parameters["charset"] = "UTF-8"
    378         } else {
    379                 responseBody = resp.Body()
    380         }
    381 
    382         //
    383         contentType.FilterParameters(ALLOWED_CONTENTTYPE_PARAMETERS)
    384 
    385         // set the content type
    386         ctx.SetContentType(contentType.String())
    387 
    388         // output according to MIME type
    389         switch {
    390         case contentType.SubType == "css" && contentType.Suffix == "":
    391                 sanitizeCSS(&RequestConfig{Key: p.Key, BaseURL: parsedURI}, ctx, responseBody)
    392         case contentType.SubType == "html" && contentType.Suffix == "":
    393                 sanitizeHTML(&RequestConfig{Key: p.Key, BaseURL: parsedURI}, ctx, responseBody)
    394         default:
    395                 if contentDispositionBytes != nil {
    396                         ctx.Response.Header.AddBytesV("Content-Disposition", contentDispositionBytes)
    397                 }
    398                 ctx.Write(responseBody)
    399         }
    400 }
    401 
    402 // force content-disposition to attachment
    403 func contentDispositionForceAttachment(contentDispositionBytes []byte, url *url.URL) []byte {
    404         var contentDispositionParams map[string]string
    405 
    406         if contentDispositionBytes != nil {
    407                 var err error
    408                 _, contentDispositionParams, err = mime.ParseMediaType(string(contentDispositionBytes))
    409                 if err != nil {
    410                         contentDispositionParams = make(map[string]string)
    411                 }
    412         } else {
    413                 contentDispositionParams = make(map[string]string)
    414         }
    415 
    416         _, fileNameDefined := contentDispositionParams["filename"]
    417         if !fileNameDefined {
    418                 // TODO : sanitize filename
    419                 contentDispositionParams["fileName"] = filepath.Base(url.Path)
    420         }
    421 
    422         return []byte(mime.FormatMediaType("attachment", contentDispositionParams))
    423 }
    424 
    425 func appRequestHandler(ctx *fasthttp.RequestCtx) bool {
    426         // serve robots.txt
    427         if bytes.Equal(ctx.Path(), []byte("/robots.txt")) {
    428                 ctx.SetContentType("text/plain")
    429                 ctx.Write([]byte("User-Agent: *\nDisallow: /\n"))
    430                 return true
    431         }
    432 
    433         // server favicon.ico
    434         if bytes.Equal(ctx.Path(), []byte("/favicon.ico")) {
    435                 ctx.SetContentType("image/png")
    436                 ctx.Write(FAVICON_BYTES)
    437                 return true
    438         }
    439 
    440         return false
    441 }
    442 
    443 func popRequestParam(ctx *fasthttp.RequestCtx, paramName []byte) []byte {
    444         param := ctx.QueryArgs().PeekBytes(paramName)
    445 
    446         if param == nil {
    447                 param = ctx.PostArgs().PeekBytes(paramName)
    448                 if param != nil {
    449                         ctx.PostArgs().DelBytes(paramName)
    450                 }
    451         } else {
    452                 ctx.QueryArgs().DelBytes(paramName)
    453         }
    454 
    455         return param
    456 }
    457 
    458 func sanitizeCSS(rc *RequestConfig, out io.Writer, css []byte) {
    459         // TODO
    460 
    461         urlSlices := CSS_URL_REGEXP.FindAllSubmatchIndex(css, -1)
    462 
    463         if urlSlices == nil {
    464                 out.Write(css)
    465                 return
    466         }
    467 
    468         startIndex := 0
    469 
    470         for _, s := range urlSlices {
    471                 urlStart := s[4]
    472                 urlEnd := s[5]
    473 
    474                 if uri, err := rc.ProxifyURI(css[urlStart:urlEnd]); err == nil {
    475                         out.Write(css[startIndex:urlStart])
    476                         out.Write([]byte(uri))
    477                         startIndex = urlEnd
    478                 } else {
    479                         log.Println("cannot proxify css uri:", string(css[urlStart:urlEnd]))
    480                 }
    481         }
    482         if startIndex < len(css) {
    483                 out.Write(css[startIndex:len(css)])
    484         }
    485 }
    486 
    487 func sanitizeHTML(rc *RequestConfig, out io.Writer, htmlDoc []byte) {
    488         r := bytes.NewReader(htmlDoc)
    489         decoder := html.NewTokenizer(r)
    490         decoder.AllowCDATA(true)
    491 
    492         unsafeElements := make([][]byte, 0, 8)
    493         state := STATE_DEFAULT
    494         for {
    495                 token := decoder.Next()
    496                 if token == html.ErrorToken {
    497                         err := decoder.Err()
    498                         if err != io.EOF {
    499                                 log.Println("failed to parse HTML:")
    500                         }
    501                         break
    502                 }
    503 
    504                 if len(unsafeElements) == 0 {
    505 
    506                         switch token {
    507                         case html.StartTagToken, html.SelfClosingTagToken:
    508                                 tag, hasAttrs := decoder.TagName()
    509                                 safe := !inArray(tag, UNSAFE_ELEMENTS)
    510                                 if !safe {
    511                                         if !inArray(tag, SELF_CLOSING_ELEMENTS) {
    512                                                 var unsafeTag []byte = make([]byte, len(tag))
    513                                                 copy(unsafeTag, tag)
    514                                                 unsafeElements = append(unsafeElements, unsafeTag)
    515                                         }
    516                                         break
    517                                 }
    518                                 if bytes.Equal(tag, []byte("base")) {
    519                                         for {
    520                                                 attrName, attrValue, moreAttr := decoder.TagAttr()
    521                                                 if bytes.Equal(attrName, []byte("href")) {
    522                                                         parsedURI, err := url.Parse(string(attrValue))
    523                                                         if err == nil {
    524                                                                 rc.BaseURL = parsedURI
    525                                                         }
    526                                                 }
    527                                                 if !moreAttr {
    528                                                         break
    529                                                 }
    530                                         }
    531                                         break
    532                                 }
    533                                 if bytes.Equal(tag, []byte("noscript")) {
    534                                         state = STATE_IN_NOSCRIPT
    535                                         break
    536                                 }
    537                                 var attrs [][][]byte
    538                                 if hasAttrs {
    539                                         for {
    540                                                 attrName, attrValue, moreAttr := decoder.TagAttr()
    541                                                 attrs = append(attrs, [][]byte{
    542                                                         attrName,
    543                                                         attrValue,
    544                                                         []byte(html.EscapeString(string(attrValue))),
    545                                                 })
    546                                                 if !moreAttr {
    547                                                         break
    548                                                 }
    549                                         }
    550                                 }
    551                                 if bytes.Equal(tag, []byte("link")) {
    552                                         sanitizeLinkTag(rc, out, attrs)
    553                                         break
    554                                 }
    555 
    556                                 if bytes.Equal(tag, []byte("meta")) {
    557                                         sanitizeMetaTag(rc, out, attrs)
    558                                         break
    559                                 }
    560 
    561                                 fmt.Fprintf(out, "<%s", tag)
    562 
    563                                 if hasAttrs {
    564                                         sanitizeAttrs(rc, out, attrs)
    565                                 }
    566 
    567                                 if token == html.SelfClosingTagToken {
    568                                         fmt.Fprintf(out, " />")
    569                                 } else {
    570                                         fmt.Fprintf(out, ">")
    571                                         if bytes.Equal(tag, []byte("style")) {
    572                                                 state = STATE_IN_STYLE
    573                                         }
    574                                 }
    575 
    576                                 if bytes.Equal(tag, []byte("head")) {
    577                                         fmt.Fprintf(out, HTML_HEAD_CONTENT_TYPE)
    578                                 }
    579 
    580                                 if bytes.Equal(tag, []byte("form")) {
    581                                         var formURL *url.URL
    582                                         for _, attr := range attrs {
    583                                                 if bytes.Equal(attr[0], []byte("action")) {
    584                                                         formURL, _ = url.Parse(string(attr[1]))
    585                                                         formURL = mergeURIs(rc.BaseURL, formURL)
    586                                                         break
    587                                                 }
    588                                         }
    589                                         if formURL == nil {
    590                                                 formURL = rc.BaseURL
    591                                         }
    592                                         urlStr := formURL.String()
    593                                         var key string
    594                                         if rc.Key != nil {
    595                                                 key = hash(urlStr, rc.Key)
    596                                         }
    597                                         fmt.Fprintf(out, HTML_FORM_EXTENSION, urlStr, key)
    598 
    599                                 }
    600 
    601                         case html.EndTagToken:
    602                                 tag, _ := decoder.TagName()
    603                                 writeEndTag := true
    604                                 switch string(tag) {
    605                                 case "body":
    606                                         fmt.Fprintf(out, HTML_BODY_EXTENSION, rc.BaseURL.String())
    607                                 case "style":
    608                                         state = STATE_DEFAULT
    609                                 case "noscript":
    610                                         state = STATE_DEFAULT
    611                                         writeEndTag = false
    612                                 }
    613                                 // skip noscript tags - only the tag, not the content, because javascript is sanitized
    614                                 if writeEndTag {
    615                                         fmt.Fprintf(out, "</%s>", tag)
    616                                 }
    617 
    618                         case html.TextToken:
    619                                 switch state {
    620                                 case STATE_DEFAULT:
    621                                         fmt.Fprintf(out, "%s", decoder.Raw())
    622                                 case STATE_IN_STYLE:
    623                                         sanitizeCSS(rc, out, decoder.Raw())
    624                                 case STATE_IN_NOSCRIPT:
    625                                         sanitizeHTML(rc, out, decoder.Raw())
    626                                 }
    627 
    628                         case html.CommentToken:
    629                                 // ignore comment. TODO : parse IE conditional comment
    630 
    631                         case html.DoctypeToken:
    632                                 out.Write(decoder.Raw())
    633                         }
    634                 } else {
    635                         switch token {
    636                         case html.StartTagToken:
    637                                 tag, _ := decoder.TagName()
    638                                 if inArray(tag, UNSAFE_ELEMENTS) {
    639                                         unsafeElements = append(unsafeElements, tag)
    640                                 }
    641 
    642                         case html.EndTagToken:
    643                                 tag, _ := decoder.TagName()
    644                                 if bytes.Equal(unsafeElements[len(unsafeElements)-1], tag) {
    645                                         unsafeElements = unsafeElements[:len(unsafeElements)-1]
    646                                 }
    647                         }
    648                 }
    649         }
    650 }
    651 
    652 func sanitizeLinkTag(rc *RequestConfig, out io.Writer, attrs [][][]byte) {
    653         exclude := false
    654         for _, attr := range attrs {
    655                 attrName := attr[0]
    656                 attrValue := attr[1]
    657                 if bytes.Equal(attrName, []byte("rel")) {
    658                         if !inArray(attrValue, LINK_REL_SAFE_VALUES) {
    659                                 exclude = true
    660                                 break
    661                         }
    662                 }
    663                 if bytes.Equal(attrName, []byte("as")) {
    664                         if bytes.Equal(attrValue, []byte("script")) {
    665                                 exclude = true
    666                                 break
    667                         }
    668                 }
    669         }
    670 
    671         if !exclude {
    672                 out.Write([]byte("<link"))
    673                 for _, attr := range attrs {
    674                         sanitizeAttr(rc, out, attr[0], attr[1], attr[2])
    675                 }
    676                 out.Write([]byte(">"))
    677         }
    678 }
    679 
    680 func sanitizeMetaTag(rc *RequestConfig, out io.Writer, attrs [][][]byte) {
    681         var http_equiv []byte
    682         var content []byte
    683 
    684         for _, attr := range attrs {
    685                 attrName := attr[0]
    686                 attrValue := attr[1]
    687                 if bytes.Equal(attrName, []byte("http-equiv")) {
    688                         http_equiv = bytes.ToLower(attrValue)
    689                         // exclude some <meta http-equiv="..." ..>
    690                         if !inArray(http_equiv, LINK_HTTP_EQUIV_SAFE_VALUES) {
    691                                 return
    692                         }
    693                 }
    694                 if bytes.Equal(attrName, []byte("content")) {
    695                         content = attrValue
    696                 }
    697                 if bytes.Equal(attrName, []byte("charset")) {
    698                         // exclude <meta charset="...">
    699                         return
    700                 }
    701         }
    702 
    703         out.Write([]byte("<meta"))
    704         urlIndex := bytes.Index(bytes.ToLower(content), []byte("url="))
    705         if bytes.Equal(http_equiv, []byte("refresh")) && urlIndex != -1 {
    706                 contentUrl := content[urlIndex+4:]
    707                 // special case of <meta http-equiv="refresh" content="0; url='example.com/url.with.quote.outside'">
    708                 if len(contentUrl) >= 2 && (contentUrl[0] == byte('\'') || contentUrl[0] == byte('"')) {
    709                         if contentUrl[0] == contentUrl[len(contentUrl)-1] {
    710                                 contentUrl = contentUrl[1 : len(contentUrl)-1]
    711                         }
    712                 }
    713                 // output proxify result
    714                 if uri, err := rc.ProxifyURI(contentUrl); err == nil {
    715                         fmt.Fprintf(out, ` http-equiv="refresh" content="%surl=%s"`, content[:urlIndex], uri)
    716                 }
    717         } else {
    718                 if len(http_equiv) > 0 {
    719                         fmt.Fprintf(out, ` http-equiv="%s"`, http_equiv)
    720                 }
    721                 sanitizeAttrs(rc, out, attrs)
    722         }
    723         out.Write([]byte(">"))
    724 }
    725 
    726 func sanitizeAttrs(rc *RequestConfig, out io.Writer, attrs [][][]byte) {
    727         for _, attr := range attrs {
    728                 sanitizeAttr(rc, out, attr[0], attr[1], attr[2])
    729         }
    730 }
    731 
    732 func sanitizeAttr(rc *RequestConfig, out io.Writer, attrName, attrValue, escapedAttrValue []byte) {
    733         if inArray(attrName, SAFE_ATTRIBUTES) {
    734                 fmt.Fprintf(out, " %s=\"%s\"", attrName, escapedAttrValue)
    735                 return
    736         }
    737         switch string(attrName) {
    738         case "src", "href", "action":
    739                 if uri, err := rc.ProxifyURI(attrValue); err == nil {
    740                         fmt.Fprintf(out, " %s=\"%s\"", attrName, uri)
    741                 } else {
    742                         log.Println("cannot proxify uri:", string(attrValue))
    743                 }
    744         case "style":
    745                 cssAttr := bytes.NewBuffer(nil)
    746                 sanitizeCSS(rc, cssAttr, attrValue)
    747                 fmt.Fprintf(out, " %s=\"%s\"", attrName, html.EscapeString(string(cssAttr.Bytes())))
    748         }
    749 }
    750 
    751 func mergeURIs(u1, u2 *url.URL) *url.URL {
    752         return u1.ResolveReference(u2)
    753 }
    754 
    755 // Sanitized URI : removes all runes bellow 32 (included) as the begining and end of URI, and lower case the scheme.
    756 // avoid memory allocation (except for the scheme)
    757 func sanitizeURI(uri []byte) ([]byte, string) {
    758         first_rune_index := 0
    759         first_rune_seen := false
    760         scheme_last_index := -1
    761         buffer := bytes.NewBuffer(make([]byte, 0, 10))
    762 
    763         // remove trailing space and special characters
    764         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")
    765 
    766         // loop over byte by byte
    767         for i, c := range uri {
    768                 // ignore special characters and space (c <= 32)
    769                 if c > 32 {
    770                         // append to the lower case of the rune to buffer
    771                         if c < utf8.RuneSelf && 'A' <= c && c <= 'Z' {
    772                                 c = c + 'a' - 'A'
    773                         }
    774 
    775                         buffer.WriteByte(c)
    776 
    777                         // update the first rune index that is not a special rune
    778                         if !first_rune_seen {
    779                                 first_rune_index = i
    780                                 first_rune_seen = true
    781                         }
    782 
    783                         if c == ':' {
    784                                 // colon rune found, we have found the scheme
    785                                 scheme_last_index = i
    786                                 break
    787                         } else if c == '/' || c == '?' || c == '\\' || c == '#' {
    788                                 // special case : most probably a relative URI
    789                                 break
    790                         }
    791                 }
    792         }
    793 
    794         if scheme_last_index != -1 {
    795                 // scheme found
    796                 // copy the "lower case without special runes scheme" before the ":" rune
    797                 scheme_start_index := scheme_last_index - buffer.Len() + 1
    798                 copy(uri[scheme_start_index:], buffer.Bytes())
    799                 // and return the result
    800                 return uri[scheme_start_index:], buffer.String()
    801         } else {
    802                 // scheme NOT found
    803                 return uri[first_rune_index:], ""
    804         }
    805 }
    806 
    807 func (rc *RequestConfig) ProxifyURI(uri []byte) (string, error) {
    808         // sanitize URI
    809         uri, scheme := sanitizeURI(uri)
    810 
    811         // remove javascript protocol
    812         if scheme == "javascript:" {
    813                 return "", nil
    814         }
    815 
    816         // TODO check malicious data: - e.g. data:script
    817         if scheme == "data:" {
    818                 if bytes.HasPrefix(uri, []byte("data:image/png")) ||
    819                         bytes.HasPrefix(uri, []byte("data:image/jpeg")) ||
    820                         bytes.HasPrefix(uri, []byte("data:image/pjpeg")) ||
    821                         bytes.HasPrefix(uri, []byte("data:image/gif")) ||
    822                         bytes.HasPrefix(uri, []byte("data:image/webp")) {
    823                         // should be safe
    824                         return string(uri), nil
    825                 } else {
    826                         // unsafe data
    827                         return "", nil
    828                 }
    829         }
    830 
    831         // parse the uri
    832         u, err := url.Parse(string(uri))
    833         if err != nil {
    834                 return "", err
    835         }
    836 
    837         // get the fragment (with the prefix "#")
    838         fragment := ""
    839         if len(u.Fragment) > 0 {
    840                 fragment = "#" + u.Fragment
    841         }
    842 
    843         // reset the fragment: it is not included in the mortyurl
    844         u.Fragment = ""
    845 
    846         // merge the URI with the document URI
    847         u = mergeURIs(rc.BaseURL, u)
    848 
    849         // simple internal link ?
    850         // some web pages describe the whole link https://same:auth@same.host/same.path?same.query#new.fragment
    851         if u.Scheme == rc.BaseURL.Scheme &&
    852                 (rc.BaseURL.User == nil || (u.User != nil && u.User.String() == rc.BaseURL.User.String())) &&
    853                 u.Host == rc.BaseURL.Host &&
    854                 u.Path == rc.BaseURL.Path &&
    855                 u.RawQuery == rc.BaseURL.RawQuery {
    856                 // the fragment is the only difference between the document URI and the uri parameter
    857                 return fragment, nil
    858         }
    859 
    860         // return full URI and fragment (if not empty)
    861         morty_uri := u.String()
    862 
    863         if rc.Key == nil {
    864                 return fmt.Sprintf("./?mortyurl=%s%s", url.QueryEscape(morty_uri), fragment), nil
    865         }
    866         return fmt.Sprintf("./?mortyhash=%s&mortyurl=%s%s", hash(morty_uri, rc.Key), url.QueryEscape(morty_uri), fragment), nil
    867 }
    868 
    869 func inArray(b []byte, a [][]byte) bool {
    870         for _, b2 := range a {
    871                 if bytes.Equal(b, b2) {
    872                         return true
    873                 }
    874         }
    875         return false
    876 }
    877 
    878 func hash(msg string, key []byte) string {
    879         mac := hmac.New(sha256.New, key)
    880         mac.Write([]byte(msg))
    881         return hex.EncodeToString(mac.Sum(nil))
    882 }
    883 
    884 func verifyRequestURI(uri, hashMsg, key []byte) bool {
    885         h := make([]byte, hex.DecodedLen(len(hashMsg)))
    886         _, err := hex.Decode(h, hashMsg)
    887         if err != nil {
    888                 log.Println("hmac error:", err)
    889                 return false
    890         }
    891         mac := hmac.New(sha256.New, key)
    892         mac.Write(uri)
    893         return hmac.Equal(h, mac.Sum(nil))
    894 }
    895 
    896 func (p *Proxy) serveMainPage(ctx *fasthttp.RequestCtx, statusCode int, err error) {
    897         ctx.SetContentType("text/html; charset=UTF-8")
    898         ctx.SetStatusCode(statusCode)
    899         ctx.Write([]byte(`<!doctype html>
     222var MORTY_HTML_PAGE_START string = `<!doctype html>
    900223<html>
    901224<head>
     
    918241        <div class="container">
    919242                <h1>MortyProxy</h1>
    920 `))
     243`
     244
     245var MORTY_HTML_PAGE_END string = `
     246        </div>
     247        <div class="footer">
     248                <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 />
     249                <a href="https://github.com/asciimoo/morty">view on github</a>
     250                </p>
     251        </div>
     252</body>
     253</html>`
     254
     255var FAVICON_BYTES []byte
     256
     257func init() {
     258        FaviconBase64 := "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAYAAABPYyMiAAAABmJLR0T///////8JWPfcAAAACXBIWXMAAABIAAAASABGyWs+AAAAF0lEQVRIx2NgGAWjYBSMglEwCkbBSAcACBAAAeaR9cIAAAAASUVORK5CYII"
     259
     260        FAVICON_BYTES, _ = base64.StdEncoding.DecodeString(FaviconBase64)
     261}
     262
     263func (p *Proxy) RequestHandler(ctx *fasthttp.RequestCtx) {
     264
     265        if appRequestHandler(ctx) {
     266                return
     267        }
     268
     269        requestHash := popRequestParam(ctx, []byte("mortyhash"))
     270
     271        requestURI := popRequestParam(ctx, []byte("mortyurl"))
     272
     273        if requestURI == nil {
     274                p.serveMainPage(ctx, 200, nil)
     275                return
     276        }
     277
     278        if p.Key != nil {
     279                if !verifyRequestURI(requestURI, requestHash, p.Key) {
     280                        // HTTP status code 403 : Forbidden
     281                        p.serveMainPage(ctx, 403, errors.New(`invalid "mortyhash" parameter`))
     282                        return
     283                }
     284        }
     285
     286        parsedURI, err := url.Parse(string(requestURI))
     287
     288        if err != nil {
     289                // HTTP status code 500 : Internal Server Error
     290                p.serveMainPage(ctx, 500, err)
     291                return
     292        }
     293
     294        // Serve an intermediate page for protocols other than HTTP(S)
     295        if (parsedURI.Scheme != "http" && parsedURI.Scheme != "https") || strings.HasSuffix(parsedURI.Host, ".onion") {
     296                p.serveExitMortyPage(ctx, parsedURI)
     297                return
     298        }
     299
     300        req := fasthttp.AcquireRequest()
     301        defer fasthttp.ReleaseRequest(req)
     302        req.SetConnectionClose()
     303
     304        requestURIStr := string(requestURI)
     305
     306        log.Println("getting", requestURIStr)
     307
     308        req.SetRequestURI(requestURIStr)
     309        req.Header.SetUserAgentBytes([]byte("Mozilla/5.0 (Windows NT 10.0; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0"))
     310
     311        resp := fasthttp.AcquireResponse()
     312        defer fasthttp.ReleaseResponse(resp)
     313
     314        req.Header.SetMethodBytes(ctx.Method())
     315        if ctx.IsPost() || ctx.IsPut() {
     316                req.SetBody(ctx.PostBody())
     317        }
     318
     319        err = CLIENT.DoTimeout(req, resp, p.RequestTimeout)
     320
     321        if err != nil {
     322                if err == fasthttp.ErrTimeout {
     323                        // HTTP status code 504 : Gateway Time-Out
     324                        p.serveMainPage(ctx, 504, err)
     325                } else {
     326                        // HTTP status code 500 : Internal Server Error
     327                        p.serveMainPage(ctx, 500, err)
     328                }
     329                return
     330        }
     331
     332        if resp.StatusCode() != 200 {
     333                switch resp.StatusCode() {
     334                case 301, 302, 303, 307, 308:
     335                        loc := resp.Header.Peek("Location")
     336                        if loc != nil {
     337                                rc := &RequestConfig{Key: p.Key, BaseURL: parsedURI}
     338                                url, err := rc.ProxifyURI(loc)
     339                                if err == nil {
     340                                        ctx.SetStatusCode(resp.StatusCode())
     341                                        ctx.Response.Header.Add("Location", url)
     342                                        log.Println("redirect to", string(loc))
     343                                        return
     344                                }
     345                        }
     346                }
     347                error_message := fmt.Sprintf("invalid response: %d (%s)", resp.StatusCode(), requestURIStr)
     348                p.serveMainPage(ctx, resp.StatusCode(), errors.New(error_message))
     349                return
     350        }
     351
     352        contentTypeBytes := resp.Header.Peek("Content-Type")
     353
     354        if contentTypeBytes == nil {
     355                // HTTP status code 503 : Service Unavailable
     356                p.serveMainPage(ctx, 503, errors.New("invalid content type"))
     357                return
     358        }
     359
     360        contentTypeString := string(contentTypeBytes)
     361
     362        // decode Content-Type header
     363        contentType, error := contenttype.ParseContentType(contentTypeString)
     364        if error != nil {
     365                // HTTP status code 503 : Service Unavailable
     366                p.serveMainPage(ctx, 503, errors.New("invalid content type"))
     367                return
     368        }
     369
     370        // content-disposition
     371        contentDispositionBytes := ctx.Request.Header.Peek("Content-Disposition")
     372
     373        // check content type
     374        if !ALLOWED_CONTENTTYPE_FILTER(contentType) {
     375                // it is not a usual content type
     376                if ALLOWED_CONTENTTYPE_ATTACHMENT_FILTER(contentType) {
     377                        // force attachment for allowed content type
     378                        contentDispositionBytes = contentDispositionForceAttachment(contentDispositionBytes, parsedURI)
     379                } else {
     380                        // deny access to forbidden content type
     381                        // HTTP status code 403 : Forbidden
     382                        p.serveMainPage(ctx, 403, errors.New("forbidden content type"))
     383                        return
     384                }
     385        }
     386
     387        // HACK : replace */xhtml by text/html
     388        if contentType.SubType == "xhtml" {
     389                contentType.TopLevelType = "text"
     390                contentType.SubType = "html"
     391                contentType.Suffix = ""
     392        }
     393
     394        // conversion to UTF-8
     395        var responseBody []byte
     396
     397        if contentType.TopLevelType == "text" {
     398                e, ename, _ := charset.DetermineEncoding(resp.Body(), contentTypeString)
     399                if (e != encoding.Nop) && (!strings.EqualFold("utf-8", ename)) {
     400                        responseBody, err = e.NewDecoder().Bytes(resp.Body())
     401                        if err != nil {
     402                                // HTTP status code 503 : Service Unavailable
     403                                p.serveMainPage(ctx, 503, err)
     404                                return
     405                        }
     406                } else {
     407                        responseBody = resp.Body()
     408                }
     409                // update the charset or specify it
     410                contentType.Parameters["charset"] = "UTF-8"
     411        } else {
     412                responseBody = resp.Body()
     413        }
     414
     415        //
     416        contentType.FilterParameters(ALLOWED_CONTENTTYPE_PARAMETERS)
     417
     418        // set the content type
     419        ctx.SetContentType(contentType.String())
     420
     421        // output according to MIME type
     422        switch {
     423        case contentType.SubType == "css" && contentType.Suffix == "":
     424                sanitizeCSS(&RequestConfig{Key: p.Key, BaseURL: parsedURI}, ctx, responseBody)
     425        case contentType.SubType == "html" && contentType.Suffix == "":
     426                sanitizeHTML(&RequestConfig{Key: p.Key, BaseURL: parsedURI}, ctx, responseBody)
     427        default:
     428                if contentDispositionBytes != nil {
     429                        ctx.Response.Header.AddBytesV("Content-Disposition", contentDispositionBytes)
     430                }
     431                ctx.Write(responseBody)
     432        }
     433}
     434
     435// force content-disposition to attachment
     436func contentDispositionForceAttachment(contentDispositionBytes []byte, url *url.URL) []byte {
     437        var contentDispositionParams map[string]string
     438
     439        if contentDispositionBytes != nil {
     440                var err error
     441                _, contentDispositionParams, err = mime.ParseMediaType(string(contentDispositionBytes))
     442                if err != nil {
     443                        contentDispositionParams = make(map[string]string)
     444                }
     445        } else {
     446                contentDispositionParams = make(map[string]string)
     447        }
     448
     449        _, fileNameDefined := contentDispositionParams["filename"]
     450        if !fileNameDefined {
     451                // TODO : sanitize filename
     452                contentDispositionParams["fileName"] = filepath.Base(url.Path)
     453        }
     454
     455        return []byte(mime.FormatMediaType("attachment", contentDispositionParams))
     456}
     457
     458func appRequestHandler(ctx *fasthttp.RequestCtx) bool {
     459        // serve robots.txt
     460        if bytes.Equal(ctx.Path(), []byte("/robots.txt")) {
     461                ctx.SetContentType("text/plain")
     462                ctx.Write([]byte("User-Agent: *\nDisallow: /\n"))
     463                return true
     464        }
     465
     466        // server favicon.ico
     467        if bytes.Equal(ctx.Path(), []byte("/favicon.ico")) {
     468                ctx.SetContentType("image/png")
     469                ctx.Write(FAVICON_BYTES)
     470                return true
     471        }
     472
     473        return false
     474}
     475
     476func popRequestParam(ctx *fasthttp.RequestCtx, paramName []byte) []byte {
     477        param := ctx.QueryArgs().PeekBytes(paramName)
     478
     479        if param == nil {
     480                param = ctx.PostArgs().PeekBytes(paramName)
     481                if param != nil {
     482                        ctx.PostArgs().DelBytes(paramName)
     483                }
     484        } else {
     485                ctx.QueryArgs().DelBytes(paramName)
     486        }
     487
     488        return param
     489}
     490
     491func sanitizeCSS(rc *RequestConfig, out io.Writer, css []byte) {
     492        // TODO
     493
     494        urlSlices := CSS_URL_REGEXP.FindAllSubmatchIndex(css, -1)
     495
     496        if urlSlices == nil {
     497                out.Write(css)
     498                return
     499        }
     500
     501        startIndex := 0
     502
     503        for _, s := range urlSlices {
     504                urlStart := s[4]
     505                urlEnd := s[5]
     506
     507                if uri, err := rc.ProxifyURI(css[urlStart:urlEnd]); err == nil {
     508                        out.Write(css[startIndex:urlStart])
     509                        out.Write([]byte(uri))
     510                        startIndex = urlEnd
     511                } else {
     512                        log.Println("cannot proxify css uri:", string(css[urlStart:urlEnd]))
     513                }
     514        }
     515        if startIndex < len(css) {
     516                out.Write(css[startIndex:len(css)])
     517        }
     518}
     519
     520func sanitizeHTML(rc *RequestConfig, out io.Writer, htmlDoc []byte) {
     521        r := bytes.NewReader(htmlDoc)
     522        decoder := html.NewTokenizer(r)
     523        decoder.AllowCDATA(true)
     524
     525        unsafeElements := make([][]byte, 0, 8)
     526        state := STATE_DEFAULT
     527        for {
     528                token := decoder.Next()
     529                if token == html.ErrorToken {
     530                        err := decoder.Err()
     531                        if err != io.EOF {
     532                                log.Println("failed to parse HTML:")
     533                        }
     534                        break
     535                }
     536
     537                if len(unsafeElements) == 0 {
     538
     539                        switch token {
     540                        case html.StartTagToken, html.SelfClosingTagToken:
     541                                tag, hasAttrs := decoder.TagName()
     542                                safe := !inArray(tag, UNSAFE_ELEMENTS)
     543                                if !safe {
     544                                        if !inArray(tag, SELF_CLOSING_ELEMENTS) {
     545                                                var unsafeTag []byte = make([]byte, len(tag))
     546                                                copy(unsafeTag, tag)
     547                                                unsafeElements = append(unsafeElements, unsafeTag)
     548                                        }
     549                                        break
     550                                }
     551                                if bytes.Equal(tag, []byte("base")) {
     552                                        for {
     553                                                attrName, attrValue, moreAttr := decoder.TagAttr()
     554                                                if bytes.Equal(attrName, []byte("href")) {
     555                                                        parsedURI, err := url.Parse(string(attrValue))
     556                                                        if err == nil {
     557                                                                rc.BaseURL = parsedURI
     558                                                        }
     559                                                }
     560                                                if !moreAttr {
     561                                                        break
     562                                                }
     563                                        }
     564                                        break
     565                                }
     566                                if bytes.Equal(tag, []byte("noscript")) {
     567                                        state = STATE_IN_NOSCRIPT
     568                                        break
     569                                }
     570                                var attrs [][][]byte
     571                                if hasAttrs {
     572                                        for {
     573                                                attrName, attrValue, moreAttr := decoder.TagAttr()
     574                                                attrs = append(attrs, [][]byte{
     575                                                        attrName,
     576                                                        attrValue,
     577                                                        []byte(html.EscapeString(string(attrValue))),
     578                                                })
     579                                                if !moreAttr {
     580                                                        break
     581                                                }
     582                                        }
     583                                }
     584                                if bytes.Equal(tag, []byte("link")) {
     585                                        sanitizeLinkTag(rc, out, attrs)
     586                                        break
     587                                }
     588
     589                                if bytes.Equal(tag, []byte("meta")) {
     590                                        sanitizeMetaTag(rc, out, attrs)
     591                                        break
     592                                }
     593
     594                                fmt.Fprintf(out, "<%s", tag)
     595
     596                                if hasAttrs {
     597                                        sanitizeAttrs(rc, out, attrs)
     598                                }
     599
     600                                if token == html.SelfClosingTagToken {
     601                                        fmt.Fprintf(out, " />")
     602                                } else {
     603                                        fmt.Fprintf(out, ">")
     604                                        if bytes.Equal(tag, []byte("style")) {
     605                                                state = STATE_IN_STYLE
     606                                        }
     607                                }
     608
     609                                if bytes.Equal(tag, []byte("head")) {
     610                                        fmt.Fprintf(out, HTML_HEAD_CONTENT_TYPE)
     611                                }
     612
     613                                if bytes.Equal(tag, []byte("form")) {
     614                                        var formURL *url.URL
     615                                        for _, attr := range attrs {
     616                                                if bytes.Equal(attr[0], []byte("action")) {
     617                                                        formURL, _ = url.Parse(string(attr[1]))
     618                                                        formURL = mergeURIs(rc.BaseURL, formURL)
     619                                                        break
     620                                                }
     621                                        }
     622                                        if formURL == nil {
     623                                                formURL = rc.BaseURL
     624                                        }
     625                                        urlStr := formURL.String()
     626                                        var key string
     627                                        if rc.Key != nil {
     628                                                key = hash(urlStr, rc.Key)
     629                                        }
     630                                        fmt.Fprintf(out, HTML_FORM_EXTENSION, urlStr, key)
     631
     632                                }
     633
     634                        case html.EndTagToken:
     635                                tag, _ := decoder.TagName()
     636                                writeEndTag := true
     637                                switch string(tag) {
     638                                case "body":
     639                                        fmt.Fprintf(out, HTML_BODY_EXTENSION, rc.BaseURL.String())
     640                                case "style":
     641                                        state = STATE_DEFAULT
     642                                case "noscript":
     643                                        state = STATE_DEFAULT
     644                                        writeEndTag = false
     645                                }
     646                                // skip noscript tags - only the tag, not the content, because javascript is sanitized
     647                                if writeEndTag {
     648                                        fmt.Fprintf(out, "</%s>", tag)
     649                                }
     650
     651                        case html.TextToken:
     652                                switch state {
     653                                case STATE_DEFAULT:
     654                                        fmt.Fprintf(out, "%s", decoder.Raw())
     655                                case STATE_IN_STYLE:
     656                                        sanitizeCSS(rc, out, decoder.Raw())
     657                                case STATE_IN_NOSCRIPT:
     658                                        sanitizeHTML(rc, out, decoder.Raw())
     659                                }
     660
     661                        case html.CommentToken:
     662                                // ignore comment. TODO : parse IE conditional comment
     663
     664                        case html.DoctypeToken:
     665                                out.Write(decoder.Raw())
     666                        }
     667                } else {
     668                        switch token {
     669                        case html.StartTagToken:
     670                                tag, _ := decoder.TagName()
     671                                if inArray(tag, UNSAFE_ELEMENTS) {
     672                                        unsafeElements = append(unsafeElements, tag)
     673                                }
     674
     675                        case html.EndTagToken:
     676                                tag, _ := decoder.TagName()
     677                                if bytes.Equal(unsafeElements[len(unsafeElements)-1], tag) {
     678                                        unsafeElements = unsafeElements[:len(unsafeElements)-1]
     679                                }
     680                        }
     681                }
     682        }
     683}
     684
     685func sanitizeLinkTag(rc *RequestConfig, out io.Writer, attrs [][][]byte) {
     686        exclude := false
     687        for _, attr := range attrs {
     688                attrName := attr[0]
     689                attrValue := attr[1]
     690                if bytes.Equal(attrName, []byte("rel")) {
     691                        if !inArray(attrValue, LINK_REL_SAFE_VALUES) {
     692                                exclude = true
     693                                break
     694                        }
     695                }
     696                if bytes.Equal(attrName, []byte("as")) {
     697                        if bytes.Equal(attrValue, []byte("script")) {
     698                                exclude = true
     699                                break
     700                        }
     701                }
     702        }
     703
     704        if !exclude {
     705                out.Write([]byte("<link"))
     706                for _, attr := range attrs {
     707                        sanitizeAttr(rc, out, attr[0], attr[1], attr[2])
     708                }
     709                out.Write([]byte(">"))
     710        }
     711}
     712
     713func sanitizeMetaTag(rc *RequestConfig, out io.Writer, attrs [][][]byte) {
     714        var http_equiv []byte
     715        var content []byte
     716
     717        for _, attr := range attrs {
     718                attrName := attr[0]
     719                attrValue := attr[1]
     720                if bytes.Equal(attrName, []byte("http-equiv")) {
     721                        http_equiv = bytes.ToLower(attrValue)
     722                        // exclude some <meta http-equiv="..." ..>
     723                        if !inArray(http_equiv, LINK_HTTP_EQUIV_SAFE_VALUES) {
     724                                return
     725                        }
     726                }
     727                if bytes.Equal(attrName, []byte("content")) {
     728                        content = attrValue
     729                }
     730                if bytes.Equal(attrName, []byte("charset")) {
     731                        // exclude <meta charset="...">
     732                        return
     733                }
     734        }
     735
     736        out.Write([]byte("<meta"))
     737        urlIndex := bytes.Index(bytes.ToLower(content), []byte("url="))
     738        if bytes.Equal(http_equiv, []byte("refresh")) && urlIndex != -1 {
     739                contentUrl := content[urlIndex+4:]
     740                // special case of <meta http-equiv="refresh" content="0; url='example.com/url.with.quote.outside'">
     741                if len(contentUrl) >= 2 && (contentUrl[0] == byte('\'') || contentUrl[0] == byte('"')) {
     742                        if contentUrl[0] == contentUrl[len(contentUrl)-1] {
     743                                contentUrl = contentUrl[1 : len(contentUrl)-1]
     744                        }
     745                }
     746                // output proxify result
     747                if uri, err := rc.ProxifyURI(contentUrl); err == nil {
     748                        fmt.Fprintf(out, ` http-equiv="refresh" content="%surl=%s"`, content[:urlIndex], uri)
     749                }
     750        } else {
     751                if len(http_equiv) > 0 {
     752                        fmt.Fprintf(out, ` http-equiv="%s"`, http_equiv)
     753                }
     754                sanitizeAttrs(rc, out, attrs)
     755        }
     756        out.Write([]byte(">"))
     757}
     758
     759func sanitizeAttrs(rc *RequestConfig, out io.Writer, attrs [][][]byte) {
     760        for _, attr := range attrs {
     761                sanitizeAttr(rc, out, attr[0], attr[1], attr[2])
     762        }
     763}
     764
     765func sanitizeAttr(rc *RequestConfig, out io.Writer, attrName, attrValue, escapedAttrValue []byte) {
     766        if inArray(attrName, SAFE_ATTRIBUTES) {
     767                fmt.Fprintf(out, " %s=\"%s\"", attrName, escapedAttrValue)
     768                return
     769        }
     770        switch string(attrName) {
     771        case "src", "href", "action":
     772                if uri, err := rc.ProxifyURI(attrValue); err == nil {
     773                        fmt.Fprintf(out, " %s=\"%s\"", attrName, uri)
     774                } else {
     775                        log.Println("cannot proxify uri:", string(attrValue))
     776                }
     777        case "style":
     778                cssAttr := bytes.NewBuffer(nil)
     779                sanitizeCSS(rc, cssAttr, attrValue)
     780                fmt.Fprintf(out, " %s=\"%s\"", attrName, html.EscapeString(string(cssAttr.Bytes())))
     781        }
     782}
     783
     784func mergeURIs(u1, u2 *url.URL) *url.URL {
     785        return u1.ResolveReference(u2)
     786}
     787
     788// Sanitized URI : removes all runes bellow 32 (included) as the begining and end of URI, and lower case the scheme.
     789// avoid memory allocation (except for the scheme)
     790func sanitizeURI(uri []byte) ([]byte, string) {
     791        first_rune_index := 0
     792        first_rune_seen := false
     793        scheme_last_index := -1
     794        buffer := bytes.NewBuffer(make([]byte, 0, 10))
     795
     796        // remove trailing space and special characters
     797        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")
     798
     799        // loop over byte by byte
     800        for i, c := range uri {
     801                // ignore special characters and space (c <= 32)
     802                if c > 32 {
     803                        // append to the lower case of the rune to buffer
     804                        if c < utf8.RuneSelf && 'A' <= c && c <= 'Z' {
     805                                c = c + 'a' - 'A'
     806                        }
     807
     808                        buffer.WriteByte(c)
     809
     810                        // update the first rune index that is not a special rune
     811                        if !first_rune_seen {
     812                                first_rune_index = i
     813                                first_rune_seen = true
     814                        }
     815
     816                        if c == ':' {
     817                                // colon rune found, we have found the scheme
     818                                scheme_last_index = i
     819                                break
     820                        } else if c == '/' || c == '?' || c == '\\' || c == '#' {
     821                                // special case : most probably a relative URI
     822                                break
     823                        }
     824                }
     825        }
     826
     827        if scheme_last_index != -1 {
     828                // scheme found
     829                // copy the "lower case without special runes scheme" before the ":" rune
     830                scheme_start_index := scheme_last_index - buffer.Len() + 1
     831                copy(uri[scheme_start_index:], buffer.Bytes())
     832                // and return the result
     833                return uri[scheme_start_index:], buffer.String()
     834        } else {
     835                // scheme NOT found
     836                return uri[first_rune_index:], ""
     837        }
     838}
     839
     840func (rc *RequestConfig) ProxifyURI(uri []byte) (string, error) {
     841        // sanitize URI
     842        uri, scheme := sanitizeURI(uri)
     843
     844        // remove javascript protocol
     845        if scheme == "javascript:" {
     846                return "", nil
     847        }
     848
     849        // TODO check malicious data: - e.g. data:script
     850        if scheme == "data:" {
     851                if bytes.HasPrefix(uri, []byte("data:image/png")) ||
     852                        bytes.HasPrefix(uri, []byte("data:image/jpeg")) ||
     853                        bytes.HasPrefix(uri, []byte("data:image/pjpeg")) ||
     854                        bytes.HasPrefix(uri, []byte("data:image/gif")) ||
     855                        bytes.HasPrefix(uri, []byte("data:image/webp")) {
     856                        // should be safe
     857                        return string(uri), nil
     858                } else {
     859                        // unsafe data
     860                        return "", nil
     861                }
     862        }
     863
     864        // parse the uri
     865        u, err := url.Parse(string(uri))
     866        if err != nil {
     867                return "", err
     868        }
     869
     870        // get the fragment (with the prefix "#")
     871        fragment := ""
     872        if len(u.Fragment) > 0 {
     873                fragment = "#" + u.Fragment
     874        }
     875
     876        // reset the fragment: it is not included in the mortyurl
     877        u.Fragment = ""
     878
     879        // merge the URI with the document URI
     880        u = mergeURIs(rc.BaseURL, u)
     881
     882        // simple internal link ?
     883        // some web pages describe the whole link https://same:auth@same.host/same.path?same.query#new.fragment
     884        if u.Scheme == rc.BaseURL.Scheme &&
     885                (rc.BaseURL.User == nil || (u.User != nil && u.User.String() == rc.BaseURL.User.String())) &&
     886                u.Host == rc.BaseURL.Host &&
     887                u.Path == rc.BaseURL.Path &&
     888                u.RawQuery == rc.BaseURL.RawQuery {
     889                // the fragment is the only difference between the document URI and the uri parameter
     890                return fragment, nil
     891        }
     892
     893        // return full URI and fragment (if not empty)
     894        morty_uri := u.String()
     895
     896        if rc.Key == nil {
     897                return fmt.Sprintf("./?mortyurl=%s%s", url.QueryEscape(morty_uri), fragment), nil
     898        }
     899        return fmt.Sprintf("./?mortyhash=%s&mortyurl=%s%s", hash(morty_uri, rc.Key), url.QueryEscape(morty_uri), fragment), nil
     900}
     901
     902func inArray(b []byte, a [][]byte) bool {
     903        for _, b2 := range a {
     904                if bytes.Equal(b, b2) {
     905                        return true
     906                }
     907        }
     908        return false
     909}
     910
     911func hash(msg string, key []byte) string {
     912        mac := hmac.New(sha256.New, key)
     913        mac.Write([]byte(msg))
     914        return hex.EncodeToString(mac.Sum(nil))
     915}
     916
     917func verifyRequestURI(uri, hashMsg, key []byte) bool {
     918        h := make([]byte, hex.DecodedLen(len(hashMsg)))
     919        _, err := hex.Decode(h, hashMsg)
     920        if err != nil {
     921                log.Println("hmac error:", err)
     922                return false
     923        }
     924        mac := hmac.New(sha256.New, key)
     925        mac.Write(uri)
     926        return hmac.Equal(h, mac.Sum(nil))
     927}
     928
     929func (p *Proxy) serveExitMortyPage(ctx *fasthttp.RequestCtx, uri *url.URL) {
     930        ctx.SetContentType("text/html")
     931        ctx.SetStatusCode(403)
     932        ctx.Write([]byte(MORTY_HTML_PAGE_START))
     933        ctx.Write([]byte("<h2>You are about to exit MortyProxy</h2>"))
     934        ctx.Write([]byte("<p>Following</p><p><a href=\""))
     935        ctx.Write([]byte(html.EscapeString(uri.String())))
     936        ctx.Write([]byte("\" rel=\"noreferrer\">"))
     937        ctx.Write([]byte(html.EscapeString(uri.String())))
     938        ctx.Write([]byte("</a></p><p>the content of this URL will be <b>NOT</b> sanitized.</p>"))
     939        ctx.Write([]byte(MORTY_HTML_PAGE_END))
     940}
     941
     942func (p *Proxy) serveMainPage(ctx *fasthttp.RequestCtx, statusCode int, err error) {
     943        ctx.SetContentType("text/html; charset=UTF-8")
     944        ctx.SetStatusCode(statusCode)
     945        ctx.Write([]byte(MORTY_HTML_PAGE_START))
    921946        if err != nil {
    922947                log.Println("error:", err)
     
    934959                ctx.Write([]byte(`<h3>Warning! This instance does not support direct URL opening.</h3>`))
    935960        }
    936         ctx.Write([]byte(`
    937         </div>
    938         <div class="footer">
    939                 <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 />
    940                 <a href="https://github.com/asciimoo/morty">view on github</a>
    941                 </p>
    942         </div>
    943 </body>
    944 </html>`))
     961        ctx.Write([]byte(MORTY_HTML_PAGE_END))
    945962}
    946963
Note: See TracChangeset for help on using the changeset viewer.