express = require ( ' express ' )
uuid = require " uuid "
fs = require " fs "
domain = require " domain "
app = express ( )
reportError = (err, type = "error", sync = false) ->
if err . code == " ECONNRESET "
# We're not interested in these for now, they're just aborted requests.
# TODO: Investigate whether there may also be other scenarios where an ECONNRESET is raised.
return
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