22 Commits

Author SHA1 Message Date
James Allen
81e85de169 Release version 0.1.3 2015-02-26 11:20:56 +00:00
Brian Gough
f37004cec6 update sanitizePath regex
remove accidental inclusion of , and add null char \x00
2015-02-13 11:28:43 +00:00
James Allen
1a7500f102 Allow non-latin characters in the rootResourcePath 2015-02-13 11:21:35 +00:00
James Allen
90cda12ed9 Sanitize rootResourcePath 2015-02-11 16:39:43 +00:00
James Allen
c84bd4fa3f Release version 0.1.2 2015-02-10 13:19:42 +00:00
James Allen
84f3d3061d Don't return error if directory doesn't exist yet 2014-12-09 11:25:23 +00:00
James Allen
2c4fbd10ed Add in some debugging logging 2014-12-09 11:16:16 +00:00
James Allen
ff94a76eb9 Use find -type f to get a list of output files 2014-12-09 11:08:07 +00:00
Henry Oswald
92338ab419 replaced old symlink logic with tested middlewear based on fs.realpath 2014-12-04 23:54:22 +00:00
James Allen
5b2031b84f Check file is not a symlink before returning it 2014-12-04 22:07:37 +00:00
James Allen
94397854c6 Add in missing error check 2014-12-04 21:37:09 +00:00
Brian Gough
6bf8c22d78 send a strong etag for the output.pdf file, needed for byte ranges in pdf.js 2014-12-02 14:30:24 +00:00
Henry Oswald
b4f0da0c42 err != error 2014-11-27 16:19:01 +00:00
Henry Oswald
4886620d8a Merge branch 'master' of https://github.com/sharelatex/clsi-sharelatex 2014-11-27 16:11:11 +00:00
Henry Oswald
fc674370bd respect the status code on the error if it exists 2014-11-27 16:11:00 +00:00
James Allen
b418ea201b Update acceptance tests for new knitr, and remove markdown 2014-10-29 10:59:32 +00:00
James Allen
7f9c9176a9 Force mimetype of output files to be safe 2014-10-28 12:07:26 +00:00
Henry Oswald
af86745112 increase max compile to 4 mins 2014-10-17 11:03:08 +01:00
Henry Oswald
22e8ee59af Merge branch 'master' of https://github.com/sharelatex/clsi-sharelatex 2014-10-17 10:22:27 +01:00
Henry Oswald
225a12fcd2 up timeout to 6 mins 2014-10-17 10:14:23 +01:00
James Allen
f5ce83118c Bump version to 0.1.1 2014-09-29 16:05:44 +01:00
James Allen
ae52819056 Lock down sequelize version 2014-09-23 10:52:01 +01:00
15 changed files with 184 additions and 101 deletions

View File

@@ -50,6 +50,7 @@ module.exports = (grunt) ->
unit:
options:
reporter: "spec"
grep: grunt.option("grep")
src: ["test/unit/js/**/*.js"]
acceptance:
options:

View File

@@ -4,6 +4,9 @@ logger = require "logger-sharelatex"
logger.initialize("clsi")
smokeTest = require "smoke-test-sharelatex"
Path = require "path"
fs = require "fs"
Metrics = require "metrics-sharelatex"
Metrics.initialize("clsi")
Metrics.open_sockets.monitor(logger)
@@ -21,7 +24,7 @@ app.use Metrics.http.monitor(logger)
# Compile requests can take longer than the default two
# minutes (including file download time), so bump up the
# timeout a bit.
TIMEOUT = threeMinutes = 3 * 60 * 1000
TIMEOUT = 6 * 60 * 1000
app.use (req, res, next) ->
req.setTimeout TIMEOUT
res.setTimeout TIMEOUT
@@ -33,8 +36,21 @@ app.delete "/project/:project_id", CompileController.clearCache
app.get "/project/:project_id/sync/code", CompileController.syncFromCode
app.get "/project/:project_id/sync/pdf", CompileController.syncFromPdf
staticServer = express.static(Settings.path.compilesDir)
app.get "/project/:project_id/output/*", (req, res, next) ->
staticServer = 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")
app.get "/project/:project_id/output/*", require("./app/js/SymlinkCheckerMiddlewear"), (req, res, next) ->
req.url = "/#{req.params.project_id}/#{req.params[0]}"
staticServer(req, res, next)
@@ -62,7 +78,7 @@ app.get "/health_check", (req, res)->
app.use (error, req, res, next) ->
logger.error err: error, "server error"
res.send 500
res.send error?.statusCode || 500
app.listen port = (Settings.internal?.clsi?.port or 3013), host = (Settings.internal?.clsi?.host or "localhost"), (error) ->
logger.log "CLSI listening on #{host}:#{port}"

View File

@@ -1,7 +1,8 @@
async = require "async"
fs = require "fs"
Path = require "path"
wrench = require "wrench"
spawn = require("child_process").spawn
logger = require "logger-sharelatex"
module.exports = OutputFileFinder =
findOutputFiles: (resources, directory, callback = (error, outputFiles) ->) ->
@@ -9,50 +10,48 @@ module.exports = OutputFileFinder =
for resource in resources
incomingResources[resource.path] = true
OutputFileFinder._getAllFiles directory, (error, allFiles) ->
logger.log directory: directory, "getting output files"
OutputFileFinder._getAllFiles directory, (error, allFiles = []) ->
return callback(error) if error?
jobs = []
outputFiles = []
for file in allFiles
do (file) ->
jobs.push (callback) ->
if incomingResources[file.path]
if incomingResources[file]
return callback()
else
OutputFileFinder._isDirectory Path.join(directory, file.path), (error, directory) ->
return callback(error) if error?
if !directory
outputFiles.push file
outputFiles.push {
path: file
type: file.match(/\.([^\.]+)$/)?[1]
}
callback()
async.series jobs, (error) ->
return callback(error) if error?
callback null, outputFiles
_isDirectory: (path, callback = (error, directory) ->) ->
fs.stat path, (error, stat) ->
callback error, stat?.isDirectory()
_getAllFiles: (directory, _callback = (error, outputFiles) ->) ->
callback = (error, outputFiles) ->
_callback(error, outputFiles)
_getAllFiles: (directory, _callback = (error, fileList) ->) ->
callback = (error, fileList) ->
_callback(error, fileList)
_callback = () ->
outputFiles = []
args = [directory, "-type", "f"]
logger.log args: args, "running find command"
wrench.readdirRecursive directory, (error, files) =>
if error?
if error.code == "ENOENT"
# Directory doesn't exist, which is not a problem
return callback(null, [])
else
return callback(error)
# readdirRecursive returns multiple times and finishes with a null response
if !files?
return callback(null, outputFiles)
for file in files
outputFiles.push
path: file
type: file.match(/\.([^\.]+)$/)?[1]
proc = spawn("find", args)
stdout = ""
proc.stdout.on "data", (chunk) ->
stdout += chunk.toString()
proc.on "error", callback
proc.on "close", (code) ->
if code != 0
logger.warn {directory, code}, "find returned error, directory likely doesn't exist"
return callback null, []
fileList = stdout.trim().split("\n")
fileList = fileList.map (file) ->
# Strip leading directory
path = Path.relative(directory, file)
return callback null, fileList

View File

@@ -1,6 +1,6 @@
module.exports = RequestParser =
VALID_COMPILERS: ["pdflatex", "latex", "xelatex", "lualatex"]
MAX_TIMEOUT: 60
MAX_TIMEOUT: 300
parse: (body, callback = (error, data) ->) ->
response = {}
@@ -27,10 +27,12 @@ module.exports = RequestParser =
response.timeout = response.timeout * 1000 # milliseconds
response.resources = (@_parseResource(resource) for resource in (compile.resources or []))
response.rootResourcePath = @_parseAttribute "rootResourcePath",
rootResourcePath = @_parseAttribute "rootResourcePath",
compile.rootResourcePath
default: "main.tex"
type: "string"
response.rootResourcePath = RequestParser._sanitizePath(rootResourcePath)
catch error
return callback error
@@ -72,3 +74,6 @@ module.exports = RequestParser =
throw "Default not implemented"
return attribute
_sanitizePath: (path) ->
# See http://php.net/manual/en/function.escapeshellcmd.php
path.replace(/[\#\&\;\`\|\*\?\~\<\>\^\(\)\[\]\{\}\$\\\x0A\xFF\x00]/g, "")

View File

@@ -0,0 +1,17 @@
Path = require("path")
fs = require("fs")
Settings = require("settings-sharelatex")
logger = require("logger-sharelatex")
module.exports = (req, res, next)->
basePath = Path.resolve("#{Settings.path.compilesDir}/#{req.params.project_id}")
requestedFsPath = Path.normalize("#{basePath}/#{req.params[0]}")
fs.realpath requestedFsPath, (err, realFsPath)->
if err?
return res.send(500)
else if requestedFsPath != realFsPath
logger.warn requestedFsPath:requestedFsPath, realFsPath:realFsPath, path: req.params[0], project_id: req.params.project_id, "trying to access a different file (symlink), aborting"
return res.send(404)
else
return next()

View File

@@ -1,7 +1,7 @@
{
"name": "node-clsi",
"description": "A Node.js implementation of the CLSI LaTeX web-API",
"version": "0.1.0",
"version": "0.1.3",
"repository": {
"type": "git",
"url": "https://github.com/sharelatex/clsi-sharelatex.git"
@@ -16,7 +16,7 @@
"logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.0.0",
"settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0",
"metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.0.0",
"sequelize": "~2.0.0-beta.2",
"sequelize": "2.0.0-beta.2",
"wrench": "~1.5.4",
"smoke-test-sharelatex": "git+https://github.com/sharelatex/smoke-test-sharelatex.git#v1.0.0",
"sqlite3": "~2.2.0",

View File

@@ -1,17 +0,0 @@
Section Title
-------------
* List item one
* List item two
: Sample grid table.
+---------------+---------------+--------------------+
| Fruit | Price | Advantages |
+===============+===============+====================+
| Bananas | $1.34 | - built-in wrapper |
| | | - bright color |
+---------------+---------------+--------------------+
| Oranges | $2.10 | - cures scurvy |
| | | - tasty |
+---------------+---------------+--------------------+

View File

@@ -1,9 +0,0 @@
\documentclass{article}
\usepackage{longtable}
\usepackage{booktabs, multicol, multirow}
\begin{document}
\input{chapters/chapter1}
\end{document}

View File

@@ -1,23 +0,0 @@
% Title
% Author
% Date
Chapter title
=============
Section Title
-------------
Hello world. Have a nice table:
: Sample grid table.
+---------------+---------------+--------------------+
| Fruit | Price | Advantages |
+===============+===============+====================+
| Bananas | $1.34 | - built-in wrapper |
| | | - bright color |
+---------------+---------------+--------------------+
| Oranges | $2.10 | - cures scurvy |
| | | - tasty |
+---------------+---------------+--------------------+

View File

@@ -4,29 +4,26 @@ require('chai').should()
modulePath = require('path').join __dirname, '../../../app/js/OutputFileFinder'
path = require "path"
expect = require("chai").expect
EventEmitter = require("events").EventEmitter
describe "OutputFileFinder", ->
beforeEach ->
@OutputFileFinder = SandboxedModule.require modulePath, requires:
"fs": @fs = {}
"wrench": @wrench = {}
"child_process": spawn: @spawn = sinon.stub()
"logger-sharelatex": { log: sinon.stub(), warn: sinon.stub() }
@directory = "/test/dir"
@callback = sinon.stub()
describe "findOutputFiles", ->
beforeEach ->
@resource_path = "resource/path.tex"
@output_paths = ["output.pdf", "extra", "extra/file.tex"]
@output_paths = ["output.pdf", "extra/file.tex"]
@all_paths = @output_paths.concat [@resource_path]
@resources = [
path: @resource_path = "resource/path.tex"
]
@OutputFileFinder._isDirectory = (dirPath, callback = (error, directory) ->) =>
callback null, dirPath == path.join(@directory, "extra")
@wrench.readdirRecursive = (dir, callback) =>
callback(null, [@resource_path].concat(@output_paths))
callback(null, null)
sinon.spy @wrench, "readdirRecursive"
@OutputFileFinder._getAllFiles = sinon.stub().callsArgWith(1, null, @all_paths)
@OutputFileFinder.findOutputFiles @resources, @directory, (error, @outputFiles) =>
it "should only return the output files, not directories or resource paths", ->
@@ -38,4 +35,34 @@ describe "OutputFileFinder", ->
type: "tex"
}]
describe "_getAllFiles", ->
beforeEach ->
@proc = new EventEmitter()
@proc.stdout = new EventEmitter()
@spawn.returns @proc
@directory = "/base/dir"
@OutputFileFinder._getAllFiles @directory, @callback
describe "successfully", ->
beforeEach ->
@proc.stdout.emit(
"data",
["/base/dir/main.tex", "/base/dir/chapters/chapter1.tex"].join("\n") + "\n"
)
@proc.emit "close", 0
it "should call the callback with the relative file paths", ->
@callback.calledWith(
null,
["main.tex", "chapters/chapter1.tex"]
).should.equal true
describe "when the directory doesn't exist", ->
beforeEach ->
@proc.emit "close", 1
it "should call the callback with a blank array", ->
@callback.calledWith(
null,
[]
).should.equal true

View File

@@ -204,6 +204,13 @@ describe "RequestParser", ->
@callback.calledWith("rootResourcePath attribute should be a string")
.should.equal true
describe "with a root resource path that needs escaping", ->
beforeEach ->
@validRequest.compile.rootResourcePath = "`rm -rf foo`.tex"
@RequestParser.parse @validRequest, @callback
@data = @callback.args[0][1]
it "should return the escaped resource", ->
@data.rootResourcePath.should.equal "rm -rf foo.tex"

View File

@@ -0,0 +1,60 @@
should = require('chai').should()
SandboxedModule = require('sandboxed-module')
assert = require('assert')
path = require('path')
sinon = require('sinon')
modulePath = path.join __dirname, "../../../app/js/SymlinkCheckerMiddlewear"
expect = require("chai").expect
describe "SymlinkCheckerMiddlewear", ->
beforeEach ->
@settings =
path:
compilesDir: "/compiles/here"
@fs = {}
@SymlinkCheckerMiddlewear = SandboxedModule.require modulePath, requires:
"settings-sharelatex":@settings
"logger-sharelatex":
log:->
warn:->
"fs":@fs
@req =
params:
project_id:"12345"
@res = {}
@req.params[0]= "output.pdf"
describe "sending a normal file through", ->
beforeEach ->
@fs.realpath = sinon.stub().callsArgWith(1, null, "#{@settings.path.compilesDir}/#{@req.params.project_id}/output.pdf")
it "should call next", (done)->
@SymlinkCheckerMiddlewear @req, @res, done
describe "with a symlink file", ->
beforeEach ->
@fs.realpath = sinon.stub().callsArgWith(1, null, "/etc/#{@req.params.project_id}/output.pdf")
it "should send a 404", (done)->
@res.send = (resCode)->
resCode.should.equal 404
done()
@SymlinkCheckerMiddlewear @req, @res
describe "with an error from fs.realpath", ->
beforeEach ->
@fs.realpath = sinon.stub().callsArgWith(1, "error")
it "should send a 500", (done)->
@res.send = (resCode)->
resCode.should.equal 500
done()
@SymlinkCheckerMiddlewear @req, @res