Changeset 68 in code for trunk/morty.go


Ignore:
Timestamp:
Dec 23, 2016, 7:25:28 PM (8 years ago)
Author:
asciimoo
Message:

Merge pull request #46 from dalf/html

[mod] different HTML / CSS modifications.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/morty.go

    r67 r68  
    1212        "io"
    1313        "log"
     14        "mime"
    1415        "net/url"
     16        "path/filepath"
    1517        "regexp"
    1618        "strings"
     
    2224        "golang.org/x/net/html/charset"
    2325        "golang.org/x/text/encoding"
     26
     27        "github.com/asciimoo/morty/contenttype"
    2428)
    2529
     
    3539
    3640var CSS_URL_REGEXP *regexp.Regexp = regexp.MustCompile("url\\((['\"]?)[ \\t\\f]*([\u0009\u0021\u0023-\u0026\u0028\u002a-\u007E]+)(['\"]?)\\)?")
     41
     42var ALLOWED_CONTENTTYPE_FILTER contenttype.Filter = contenttype.NewFilterOr([]contenttype.Filter{
     43        // html
     44        contenttype.NewFilterEquals("text", "html", ""),
     45        contenttype.NewFilterEquals("application", "xhtml", "xml"),
     46        // css
     47        contenttype.NewFilterEquals("text", "css", ""),
     48        // images
     49        contenttype.NewFilterEquals("image", "gif", ""),
     50        contenttype.NewFilterEquals("image", "png", ""),
     51        contenttype.NewFilterEquals("image", "jpeg", ""),
     52        contenttype.NewFilterEquals("image", "pjpeg", ""),
     53        contenttype.NewFilterEquals("image", "webp", ""),
     54        contenttype.NewFilterEquals("image", "tiff", ""),
     55        contenttype.NewFilterEquals("image", "vnd.microsoft.icon", ""),
     56        contenttype.NewFilterEquals("image", "bmp", ""),
     57        contenttype.NewFilterEquals("image", "x-ms-bmp", ""),
     58        // fonts
     59        contenttype.NewFilterEquals("application", "font-otf", ""),
     60        contenttype.NewFilterEquals("application", "font-ttf", ""),
     61        contenttype.NewFilterEquals("application", "font-woff", ""),
     62        contenttype.NewFilterEquals("application", "vnd.ms-fontobject", ""),
     63})
     64
     65var ALLOWED_CONTENTTYPE_ATTACHMENT_FILTER contenttype.Filter = contenttype.NewFilterOr([]contenttype.Filter{
     66        // texts
     67        contenttype.NewFilterEquals("text", "csv", ""),
     68        contenttype.NewFilterEquals("text", "tab-separated-value", ""),
     69        contenttype.NewFilterEquals("text", "plain", ""),
     70        // API
     71        contenttype.NewFilterEquals("application", "json", ""),
     72        // Documents
     73        contenttype.NewFilterEquals("application", "x-latex", ""),
     74        contenttype.NewFilterEquals("application", "pdf", ""),
     75        contenttype.NewFilterEquals("application", "vnd.oasis.opendocument.text", ""),
     76        contenttype.NewFilterEquals("application", "vnd.oasis.opendocument.spreadsheet", ""),
     77        contenttype.NewFilterEquals("application", "vnd.oasis.opendocument.presentation", ""),
     78        contenttype.NewFilterEquals("application", "vnd.oasis.opendocument.graphics", ""),
     79        // Compressed archives
     80        contenttype.NewFilterEquals("application", "zip", ""),
     81        contenttype.NewFilterEquals("application", "gzip", ""),
     82        contenttype.NewFilterEquals("application", "x-compressed", ""),
     83        contenttype.NewFilterEquals("application", "x-gtar", ""),
     84        contenttype.NewFilterEquals("application", "x-compress", ""),
     85        // Generic binary
     86        contenttype.NewFilterEquals("application", "octet-stream", ""),
     87})
     88
     89var ALLOWED_CONTENTTYPE_PARAMETERS map[string]bool = map[string]bool{
     90        "charset": true,
     91}
    3792
    3893var UNSAFE_ELEMENTS [][]byte = [][]byte{
     
    262317        }
    263318
    264         contentType := resp.Header.Peek("Content-Type")
    265 
    266         if contentType == nil {
     319        contentTypeBytes := resp.Header.Peek("Content-Type")
     320
     321        if contentTypeBytes == nil {
    267322                // HTTP status code 503 : Service Unavailable
    268323                p.serveMainPage(ctx, 503, errors.New("invalid content type"))
     
    270325        }
    271326
    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"))
     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"))
    275334                return
    276335        }
    277336
    278         contentInfo := bytes.SplitN(contentType, []byte(";"), 2)
    279 
     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
    280362        var responseBody []byte
    281363
    282         if len(contentInfo) == 2 && bytes.Contains(contentInfo[0], []byte("text")) {
    283                 e, ename, _ := charset.DetermineEncoding(resp.Body(), string(contentType))
     364        if contentType.TopLevelType == "text" {
     365                e, ename, _ := charset.DetermineEncoding(resp.Body(), contentTypeString)
    284366                if (e != encoding.Nop) && (!strings.EqualFold("utf-8", ename)) {
    285367                        responseBody, err = e.NewDecoder().Bytes(resp.Body())
     
    292374                        responseBody = resp.Body()
    293375                }
     376                // update the charset or specify it
     377                contentType.Parameters["charset"] = "UTF-8"
    294378        } else {
    295379                responseBody = resp.Body()
    296380        }
    297381
    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 
     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
    304389        switch {
    305         case bytes.Contains(contentType, []byte("css")):
     390        case contentType.SubType == "css" && contentType.Suffix == "":
    306391                sanitizeCSS(&RequestConfig{Key: p.Key, BaseURL: parsedURI}, ctx, responseBody)
    307         case bytes.Contains(contentType, []byte("html")):
     392        case contentType.SubType == "html" && contentType.Suffix == "":
    308393                sanitizeHTML(&RequestConfig{Key: p.Key, BaseURL: parsedURI}, ctx, responseBody)
    309394        default:
    310                 if ctx.Request.Header.Peek("Content-Disposition") != nil {
    311                         ctx.Response.Header.AddBytesV("Content-Disposition", ctx.Request.Header.Peek("Content-Disposition"))
     395                if contentDispositionBytes != nil {
     396                        ctx.Response.Header.AddBytesV("Content-Disposition", contentDispositionBytes)
    312397                }
    313398                ctx.Write(responseBody)
    314399        }
     400}
     401
     402// force content-disposition to attachment
     403func 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))
    315423}
    316424
Note: See TracChangeset for help on using the changeset viewer.