Changeset 67 in code


Ignore:
Timestamp:
Dec 23, 2016, 5:58:04 PM (8 years ago)
Author:
alex
Message:

[mod] fix HTML in the welcome page. Make sure the morty header is always visible with the same CSS style. Add an empty favicon.ico.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/morty.go

    r66 r67  
    55        "crypto/hmac"
    66        "crypto/sha256"
     7        "encoding/base64"
    78        "encoding/hex"
    89        "errors"
     
    1112        "io"
    1213        "log"
    13         "mime"
    1414        "net/url"
    15         "path/filepath"
    1615        "regexp"
    1716        "strings"
     
    2322        "golang.org/x/net/html/charset"
    2423        "golang.org/x/text/encoding"
    25 
    26         "github.com/asciimoo/morty/contenttype"
    2724)
    2825
     
    3835
    3936var CSS_URL_REGEXP *regexp.Regexp = regexp.MustCompile("url\\((['\"]?)[ \\t\\f]*([\u0009\u0021\u0023-\u0026\u0028\u002a-\u007E]+)(['\"]?)\\)?")
    40 
    41 var ALLOWED_CONTENTTYPE_FILTER contenttype.Filter = contenttype.NewFilterOr([]contenttype.Filter{
    42         // html
    43         contenttype.NewFilterEquals("text", "html", ""),
    44         contenttype.NewFilterEquals("application", "xhtml", "xml"),
    45         // css
    46         contenttype.NewFilterEquals("text", "css", ""),
    47         // images
    48         contenttype.NewFilterEquals("image", "gif", ""),
    49         contenttype.NewFilterEquals("image", "png", ""),
    50         contenttype.NewFilterEquals("image", "jpeg", ""),
    51         contenttype.NewFilterEquals("image", "pjpeg", ""),
    52         contenttype.NewFilterEquals("image", "webp", ""),
    53         contenttype.NewFilterEquals("image", "tiff", ""),
    54         contenttype.NewFilterEquals("image", "vnd.microsoft.icon", ""),
    55         contenttype.NewFilterEquals("image", "bmp", ""),
    56         contenttype.NewFilterEquals("image", "x-ms-bmp", ""),
    57         // fonts
    58         contenttype.NewFilterEquals("application", "font-otf", ""),
    59         contenttype.NewFilterEquals("application", "font-ttf", ""),
    60         contenttype.NewFilterEquals("application", "font-woff", ""),
    61         contenttype.NewFilterEquals("application", "vnd.ms-fontobject", ""),
    62 })
    63 
    64 var ALLOWED_CONTENTTYPE_ATTACHMENT_FILTER contenttype.Filter = contenttype.NewFilterOr([]contenttype.Filter{
    65         // texts
    66         contenttype.NewFilterEquals("text", "csv", ""),
    67         contenttype.NewFilterEquals("text", "tab-separated-value", ""),
    68         contenttype.NewFilterEquals("text", "plain", ""),
    69         // API
    70         contenttype.NewFilterEquals("application", "json", ""),
    71         // Documents
    72         contenttype.NewFilterEquals("application", "x-latex", ""),
    73         contenttype.NewFilterEquals("application", "pdf", ""),
    74         contenttype.NewFilterEquals("application", "vnd.oasis.opendocument.text", ""),
    75         contenttype.NewFilterEquals("application", "vnd.oasis.opendocument.spreadsheet", ""),
    76         contenttype.NewFilterEquals("application", "vnd.oasis.opendocument.presentation", ""),
    77         contenttype.NewFilterEquals("application", "vnd.oasis.opendocument.graphics", ""),
    78         // Compressed archives
    79         contenttype.NewFilterEquals("application", "zip", ""),
    80         contenttype.NewFilterEquals("application", "gzip", ""),
    81         contenttype.NewFilterEquals("application", "x-compressed", ""),
    82         contenttype.NewFilterEquals("application", "x-gtar", ""),
    83         contenttype.NewFilterEquals("application", "x-compress", ""),
    84         // Generic binary
    85         contenttype.NewFilterEquals("application", "octet-stream", ""),
    86 })
    87 
    88 var ALLOWED_CONTENTTYPE_PARAMETERS map[string]bool = map[string]bool{
    89         "charset": true,
    90 }
    9137
    9238var UNSAFE_ELEMENTS [][]byte = [][]byte{
     
    201147<div id="mortyheader">
    202148  <input type="checkbox" id="mortytoggle" autocomplete="off" />
    203   <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  <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>
    204150</div>
    205151<style>
    206 #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; }
    207 #mortyheader a { color: #3498db; font-weight: bold; }
    208 #mortyheader p { padding: 0 0 0.7em 0; margin: 0; }
    209 #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; }
    210 #mortyheader label { text-align: right; cursor: pointer; display: block; color: #444; padding: 0; margin: 0; }
     152#mortyheader { position: fixed; margin: 0; box-sizing: border-box; -webkit-box-sizing: border-box; top: 15%%; left: 0; max-width: 140px; overflow: hidden; z-index: 2147483647 !important; font-size: 12px; line-height: normal; border-width: 4px 4px 4px 0; border-style: solid; border-color: #1abc9c; background: #FFF; padding: 12px 12px 8px 8px; color: #444; }
     153#mortyheader * { box-sizing: content-box; margin: 0; border: none; padding: 0; overflow: hidden; z-index: 2147483647 !important; line-height: 1em; font-size: 12px !important; font-family: sans !important; font-weight: normal; text-align: left; text-decoration: none; }
     154#mortyheader p { padding: 0 0 0.7em 0; display: block; }
     155#mortyheader a { color: #3498db; font-weight: bold; display: inline; }
     156#mortyheader label { text-align: right; cursor: pointer; display: block; color: #444; }
    211157input[type=checkbox]#mortytoggle { display: none; }
    212158input[type=checkbox]#mortytoggle:checked ~ div { display: none; }
     
    216162var HTML_HEAD_CONTENT_TYPE string = `<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    217163<meta http-equiv="X-UA-Compatible" content="IE=edge">
     164<meta name="referrer" content="no-referrer">
    218165`
     166
     167var FAVICON_BYTES []byte
     168
     169func init() {
     170        FaviconBase64 := "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAYAAABPYyMiAAAABmJLR0T///////8JWPfcAAAACXBIWXMAAABIAAAASABGyWs+AAAAF0lEQVRIx2NgGAWjYBSMglEwCkbBSAcACBAAAeaR9cIAAAAASUVORK5CYII"
     171
     172        FAVICON_BYTES, _ = base64.StdEncoding.DecodeString(FaviconBase64)
     173}
    219174
    220175func (p *Proxy) RequestHandler(ctx *fasthttp.RequestCtx) {
     
    307262        }
    308263
    309         contentTypeBytes := resp.Header.Peek("Content-Type")
    310 
    311         if contentTypeBytes == nil {
     264        contentType := resp.Header.Peek("Content-Type")
     265
     266        if contentType == nil {
    312267                // HTTP status code 503 : Service Unavailable
    313268                p.serveMainPage(ctx, 503, errors.New("invalid content type"))
     
    315270        }
    316271
    317         contentTypeString := string(contentTypeBytes)
    318 
    319         // decode Content-Type header
    320         contentType, error := contenttype.ParseContentType(contentTypeString)
    321         if error != nil {
    322                 // HTTP status code 503 : Service Unavailable
    323                 p.serveMainPage(ctx, 503, errors.New("invalid content type"))
    324                 return
    325         }
    326 
    327         // content-disposition
    328         contentDispositionBytes := ctx.Request.Header.Peek("Content-Disposition")
    329 
    330         // check content type
    331         if !ALLOWED_CONTENTTYPE_FILTER(contentType) {
    332                 // it is not a usual content type
    333                 if ALLOWED_CONTENTTYPE_ATTACHMENT_FILTER(contentType) {
    334                         // force attachment for allowed content type
    335                         contentDispositionBytes = contentDispositionForceAttachment(contentDispositionBytes, parsedURI)
    336                 } else {
    337                         // deny access to forbidden content type
    338                         // HTTP status code 403 : Forbidden
    339                         p.serveMainPage(ctx, 403, errors.New("forbidden content type"))
    340                         return
    341                 }
    342         }
    343 
    344         // HACK : replace */xhtml by text/html
    345         if contentType.SubType == "xhtml" {
    346                 contentType.TopLevelType = "text"
    347                 contentType.SubType = "html"
    348                 contentType.Suffix = ""
    349         }
    350 
    351         // conversion to UTF-8
     272        if bytes.Contains(bytes.ToLower(contentType), []byte("javascript")) {
     273                // HTTP status code 403 : Forbidden
     274                p.serveMainPage(ctx, 403, errors.New("forbidden content type"))
     275                return
     276        }
     277
     278        contentInfo := bytes.SplitN(contentType, []byte(";"), 2)
     279
    352280        var responseBody []byte
    353281
    354         if contentType.TopLevelType == "text" {
    355                 e, ename, _ := charset.DetermineEncoding(resp.Body(), contentTypeString)
     282        if len(contentInfo) == 2 && bytes.Contains(contentInfo[0], []byte("text")) {
     283                e, ename, _ := charset.DetermineEncoding(resp.Body(), string(contentType))
    356284                if (e != encoding.Nop) && (!strings.EqualFold("utf-8", ename)) {
    357285                        responseBody, err = e.NewDecoder().Bytes(resp.Body())
     
    364292                        responseBody = resp.Body()
    365293                }
    366                 // update the charset or specify it
    367                 contentType.Parameters["charset"] = "UTF-8"
    368294        } else {
    369295                responseBody = resp.Body()
    370296        }
    371297
    372         //
    373         contentType.FilterParameters(ALLOWED_CONTENTTYPE_PARAMETERS)
    374 
    375         // set the content type
    376         ctx.SetContentType(contentType.String())
    377 
    378         // output according to MIME type
     298        if bytes.Contains(contentType, []byte("xhtml")) {
     299                ctx.SetContentType("text/html; charset=UTF-8")
     300        } else {
     301                ctx.SetContentType(fmt.Sprintf("%s; charset=UTF-8", contentInfo[0]))
     302        }
     303
    379304        switch {
    380         case contentType.SubType == "css" && contentType.Suffix == "":
     305        case bytes.Contains(contentType, []byte("css")):
    381306                sanitizeCSS(&RequestConfig{Key: p.Key, BaseURL: parsedURI}, ctx, responseBody)
    382         case contentType.SubType == "html" && contentType.Suffix == "":
     307        case bytes.Contains(contentType, []byte("html")):
    383308                sanitizeHTML(&RequestConfig{Key: p.Key, BaseURL: parsedURI}, ctx, responseBody)
    384309        default:
    385                 if contentDispositionBytes != nil {
    386                         ctx.Response.Header.AddBytesV("Content-Disposition", contentDispositionBytes)
     310                if ctx.Request.Header.Peek("Content-Disposition") != nil {
     311                        ctx.Response.Header.AddBytesV("Content-Disposition", ctx.Request.Header.Peek("Content-Disposition"))
    387312                }
    388313                ctx.Write(responseBody)
    389314        }
    390 }
    391 
    392 // force content-disposition to attachment
    393 func contentDispositionForceAttachment(contentDispositionBytes []byte, url *url.URL) []byte {
    394         var contentDispositionParams map[string]string
    395 
    396         if contentDispositionBytes != nil {
    397                 var err error
    398                 _, contentDispositionParams, err = mime.ParseMediaType(string(contentDispositionBytes))
    399                 if err != nil {
    400                         contentDispositionParams = make(map[string]string)
    401                 }
    402         } else {
    403                 contentDispositionParams = make(map[string]string)
    404         }
    405 
    406         _, fileNameDefined := contentDispositionParams["filename"]
    407         if !fileNameDefined {
    408                 // TODO : sanitize filename
    409                 contentDispositionParams["fileName"] = filepath.Base(url.Path)
    410         }
    411 
    412         return []byte(mime.FormatMediaType("attachment", contentDispositionParams))
    413315}
    414316
     
    418320                ctx.SetContentType("text/plain")
    419321                ctx.Write([]byte("User-Agent: *\nDisallow: /\n"))
     322                return true
     323        }
     324
     325        // server favicon.ico
     326        if bytes.Equal(ctx.Path(), []byte("/favicon.ico")) {
     327                ctx.SetContentType("image/png")
     328                ctx.Write(FAVICON_BYTES)
    420329                return true
    421330        }
     
    878787
    879788func (p *Proxy) serveMainPage(ctx *fasthttp.RequestCtx, statusCode int, err error) {
    880         ctx.SetContentType("text/html")
     789        ctx.SetContentType("text/html; charset=UTF-8")
    881790        ctx.SetStatusCode(statusCode)
    882791        ctx.Write([]byte(`<!doctype html>
     792<html>
    883793<head>
    884794<title>MortyProxy</title>
Note: See TracChangeset for help on using the changeset viewer.