From 29be2dc70016689d65fe351a73ef13477dbea607 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 21 Sep 2015 14:04:08 +0100 Subject: [PATCH] When serving output files, intelligently determine the appropriate content-type. cherry pick 6fa3fda3ed28239cf3ac9720629f9707663aa197 from datajoy. --- app.coffee | 7 +-- app/coffee/ContentTypeMapper.coffee | 26 ++++++++++ .../unit/coffee/ContentTypeMapperTests.coffee | 51 +++++++++++++++++++ 3 files changed, 79 insertions(+), 5 deletions(-) create mode 100644 app/coffee/ContentTypeMapper.coffee create mode 100644 test/unit/coffee/ContentTypeMapperTests.coffee diff --git a/app.coffee b/app.coffee index 8b4e2a9..bd0a586 100644 --- a/app.coffee +++ b/app.coffee @@ -3,6 +3,7 @@ Settings = require "settings-sharelatex" logger = require "logger-sharelatex" logger.initialize("clsi") smokeTest = require "smoke-test-sharelatex" +ContentTypeMapper = require "./app/js/ContentTypeMapper" Path = require "path" fs = require "fs" @@ -46,17 +47,13 @@ ForbidSymlinks = require "./app/js/StaticServerForbidSymlinks" # and serving the files staticServer = ForbidSymlinks express.static, Settings.path.compilesDir, setHeaders: (res, path, stat) -> if Path.basename(path) == "output.pdf" - res.set("Content-Type", "application/pdf") # Calculate an etag in the same way as nginx # https://github.com/tj/send/issues/65 etag = (path, stat) -> '"' + Math.ceil(+stat.mtime / 1000).toString(16) + '-' + Number(stat.size).toString(16) + '"' res.set("Etag", etag(path, stat)) - else - # Force plain treatment of other file types to prevent hosting of HTTP/JS files - # that could be used in same-origin/XSS attacks. - res.set("Content-Type", "text/plain") + res.set("Content-Type", ContentTypeMapper.map(path)) app.get "/project/:project_id/output/*", (req, res, next) -> if req.query?.build? && req.query.build.match(OutputCacheManager.BUILD_REGEX) diff --git a/app/coffee/ContentTypeMapper.coffee b/app/coffee/ContentTypeMapper.coffee new file mode 100644 index 0000000..4f0eb2d --- /dev/null +++ b/app/coffee/ContentTypeMapper.coffee @@ -0,0 +1,26 @@ +Path = require 'path' + +# here we coerce html, css and js to text/plain, +# otherwise choose correct mime type based on file extension, +# falling back to octet-stream +module.exports = ContentTypeMapper = + map: (path) -> + switch Path.extname(path) + when '.txt', '.html', '.js', '.css' + return 'text/plain' + when '.csv' + return 'text/csv' + when '.pdf' + return 'application/pdf' + when '.png' + return 'image/png' + when '.jpg', '.jpeg' + return 'image/jpeg' + when '.tiff' + return 'image/tiff' + when '.gif' + return 'image/gif' + when '.svg' + return 'image/svg+xml' + else + return 'application/octet-stream' diff --git a/test/unit/coffee/ContentTypeMapperTests.coffee b/test/unit/coffee/ContentTypeMapperTests.coffee new file mode 100644 index 0000000..d201b86 --- /dev/null +++ b/test/unit/coffee/ContentTypeMapperTests.coffee @@ -0,0 +1,51 @@ +SandboxedModule = require('sandboxed-module') +sinon = require('sinon') +require('chai').should() +modulePath = require('path').join __dirname, '../../../app/js/ContentTypeMapper' + +describe 'ContentTypeMapper', -> + + beforeEach -> + @ContentTypeMapper = SandboxedModule.require modulePath + + describe 'map', -> + + it 'should map .txt to text/plain', -> + content_type = @ContentTypeMapper.map('example.txt') + content_type.should.equal 'text/plain' + + it 'should map .csv to text/csv', -> + content_type = @ContentTypeMapper.map('example.csv') + content_type.should.equal 'text/csv' + + it 'should map .pdf to application/pdf', -> + content_type = @ContentTypeMapper.map('example.pdf') + content_type.should.equal 'application/pdf' + + it 'should fall back to octet-stream', -> + content_type = @ContentTypeMapper.map('example.unknown') + content_type.should.equal 'application/octet-stream' + + describe 'coercing web files to plain text', -> + + it 'should map .js to plain text', -> + content_type = @ContentTypeMapper.map('example.js') + content_type.should.equal 'text/plain' + + it 'should map .html to plain text', -> + content_type = @ContentTypeMapper.map('example.html') + content_type.should.equal 'text/plain' + + it 'should map .css to plain text', -> + content_type = @ContentTypeMapper.map('example.css') + content_type.should.equal 'text/plain' + + describe 'image files', -> + + it 'should map .png to image/png', -> + content_type = @ContentTypeMapper.map('example.png') + content_type.should.equal 'image/png' + + it 'should map .jpeg to image/jpeg', -> + content_type = @ContentTypeMapper.map('example.jpeg') + content_type.should.equal 'image/jpeg'