You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

274 lines
7.9 KiB
CoffeeScript

express = require('express')
uuid = require "uuid"
fs = require "fs"
domain = require "domain"
app = express()
reportError = (err, type = "error", sync = false) ->
errorPayload = {}
Object.getOwnPropertyNames(err).forEach (key) ->
errorPayload[key] = err[key]
filename = "errors/#{type}-#{Date.now()}-#{uuid.v4()}.json"
if sync
fs.writeFileSync filename, JSON.stringify(errorPayload)
else
fs.writeFile filename, JSON.stringify(errorPayload)
# To make absolutely sure that we can log and exit on all uncaught errors.
if app.get("env") == "production"
logAndExit = (err) ->
reportError(err, "error", true)
process.exit(1)
rootDomain = domain.create()
rootDomain.on "error", logAndExit
runWrapper = (func) ->
rootDomain.run ->
try
func()
catch err
logAndExit(err)
else
# In development, we don't have to catch uncaught errors. Just run the specified function directly.
runWrapper = (func) -> func()
runWrapper ->
path = require('path')
favicon = require('serve-favicon')
logger = require('morgan')
cookieParser = require('cookie-parser')
bodyParser = require('body-parser')
fancyshelf = require "fancyshelf"
session = require "express-session"
csurf = require "csurf"
fileStreamRotator = require "file-stream-rotator"
domainMiddleware = require("express-domain-middleware")
errors = require "errors"
PrettyError = require "pretty-error"
Convert = require "ansi-to-html"
marked = require "marked"
moment = require "moment"
persist = require "./lib/persist"
useCsrf = require "./lib/use-csrf"
templateUtil = require("./lib/template-util")
sessionStore = require("./lib/persist-session")(session)
config = require "./config.json"
knexfile = require("./knexfile").development
ansiHTML = new Convert(escapeXML: true, newline: true)
# Error handling
pe = PrettyError.start()
pe.skipPackage "bluebird", "coffee-script", "express", "express-promise-router", "jade"
pe.skipNodeFiles()
errors.create
name: "UploadError"
errors.create
name: "UploadTooLarge"
parents: errors.UploadError
status: 413
errors.create
name: "InvalidFiletype"
parents: errors.UploadError
status: 415
errors.create
name: "NotAuthenticated"
status: 403
errors.create
name: "InvalidInput"
status: 422
# Database setup
shelf = fancyshelf
engine: knexfile.client
host: knexfile.connection.host
username: knexfile.connection.user
password: knexfile.connection.password
database: knexfile.connection.database
debug: (app.get("env") == "development")
# Task runner
TaskRunner = require("./lib/task-runner")
runner = new TaskRunner(app: app, db: shelf, config: config, thumbnailPath: path.join(__dirname, 'thumbnails'))
runner.addTask "mirror", require("./tasks/mirror"), maxConcurrency: 5
runner.addTask "thumbnail", require("./tasks/thumbnail")
runner.run()
runner.on "taskQueued", (taskType, task) ->
persist.increment "task:#{taskType}:queued"
runner.on "taskStarted", (taskType, task) ->
persist.decrement "task:#{taskType}:queued"
persist.increment "task:#{taskType}:running"
runner.on "taskFailed", (taskType, task) ->
persist.decrement "task:#{taskType}:running"
persist.increment "task:#{taskType}:failed"
runner.on "taskCompleted", (taskType, task) ->
persist.decrement "task:#{taskType}:running"
persist.increment "task:#{taskType}:completed"
if app.get("env") == "development"
runner.on "taskFailed", (taskType, task, err) ->
console.log err.stack
else
runner.on "taskFailed", (taskType, task, err) ->
reportError err, "taskFailed"
# Configure Express
app.set('views', path.join(__dirname, 'views'))
app.set('view engine', 'jade')
# Middleware
if app.get("env") == "development"
app.use(logger('dev'))
else
accessLogStream = fileStreamRotator.getStream frequency: (config.accessLog.frequency ? "24h"), filename: config.accessLog.filename
app.use logger (config.accessLog.format ? "combined"), stream: accessLogStream
app.use (req, res, next) ->
if config.ssl?.key?
if req.secure
res.set "Strict-Transport-Security", "max-age=63072000; includeSubDomains; preload"
next()
else
res.redirect "https://pdf.yt/"
else
next()
# Using /static/ paths to maintain backwards compatibility.
app.use("/static/thumbs", express.static(path.join(__dirname, 'thumbnails')))
app.use("/static/pdfjs", express.static(path.join(__dirname, 'public/pdfjs')))
app.use(express.static(path.join(__dirname, 'public')))
#app.use(favicon(__dirname + '/public/favicon.ico'))
app.use domainMiddleware
app.use templateUtil
app.use shelf.express
app.use session
store: new sessionStore
persist: persist
secret: config.session.signingKey
resave: true # TODO: Implement `touch` for the session store, and/or switch to a different store.
saveUninitialized: false
# Load models
require("./models")(shelf)
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: false }))
app.use(cookieParser())
app.use (req, res, next) ->
res.locals.md = marked
req.appConfig = config
req.persist = persist
jadeExports = [
"announcementVisible"
"announcementText"
"announcementLinkText"
"announcementLink"
"maintenanceMode"
"maintenanceModeText"
"donationGoal"
"donationTotal"
"showNotice"
]
for key in jadeExports
res.locals[key] = persist.getItem "var:#{key}"
res.locals.bitcoinAddress = config.donations.bitcoinAddress
res.locals.isAdmin = req.session.isAdmin
# This is for logging/reporting errors that should not occur, but that are known to not cause any adverse side-effects.
# This should NOT be used for errors that could leave the application in an undefined state!
req.reportError = reportError
req.taskRunner = runner
next()
app.use "/", require("./routes/index")
app.use "/donate", require("./routes/donate")
app.use "/d", require("./routes/document")
app.use "/upload", require("./routes/upload")
app.use "/gallery", require("./routes/gallery")
app.use "/blog", require("./routes/blog")
app.use "/admin", csurf(), useCsrf, require("./routes/admin")
# If no routes match, cause a 404
app.use (req, res, next) ->
next new errors.Http404Error("The requested page was not found.")
# Error handling middleware
app.use "/static/thumbs", (err, req, res, next) ->
# TODO: For some reason, Chrome doesn't always display images that are sent with a 404 status code. Let's stick with 200 for now...
#res.status 404
if err.status == '404'
fs.createReadStream path.join(__dirname, "public/images/no-thumbnail.png")
.pipe res
else
next(err)
app.use (err, req, res, next) ->
statusCode = err.status || 500
res.status(statusCode)
# Dump the error to disk if it's a 500, so that the error reporter can deal with it.
if app.get("env") != "development" and statusCode == 500
reportError(err)
# Display the error to the user - amount of details depending on whether the application is running in production mode or not.
if (app.get('env') == 'development')
stack = err
else
if statusCode == 500
errorMessage = "An unknown error occurred."
stack = {stack: "An administrator has been notified, and the error will be resolved as soon as possible. Apologies for the inconvenience."}
else
errorMessage = err.message
stack = {stack: err.explanation, subMessage: err.subMessage}
if req.headers["X-Requested-With"] == "XMLHttpRequest"
res.send {
message: errorMessage,
error: stack
}
else
if err instanceof errors.NotAuthenticated
res.redirect "/admin/login"
else
if app.get("env") == "development"
htmlStack = ansiHTML.toHtml(stack.stack)
.replace /#555/g, "#b5b5b5"
# TODO: It seems aborted responses will result in an attempt to send an error page/header - of course this can't succeed, as the headers have already been sent by that time, and an error is thrown.
res.render('error', {
message: errorMessage,
error: stack,
statusCode: statusCode,
htmlStack: htmlStack
})
module.exports = app