md-fileserver
Version:
Locally view markdown files in a browser
251 lines (224 loc) • 6.16 kB
JavaScript
const fs = require('fs')
const path = require('path')
const async = require('asyncc')
const serveIndex = require('serve-index')
const serveStatic = require('serve-static')
const cookie = require('cookie')
const config = require('../config.js')
const { renderRequest } = require('./markd.js')
const { renderConfig, updateConfig } = require('./renderConfig.js')
const {
newError,
homedir,
drive,
uri2filename,
filename2uri,
urlWithoutDrive
} = require('./utils.js')
const MARKDOWNEXT = /\.(md|mdown|markdown)$/
const FILETEXT = /[/\\]([A-Za-z]+)$/
const FILENOEXT = /[/\\]([^.]+)$/
const FORBIDDEN_RE =
/^\/(etc|bin|sbin|usr|boot|opt|var|lib|[Cc]:\/[Ww]indows\/[Ss]ystem32)(\/|$)/
/**
* middlewares
*/
const M = {
// only allow access from localhost
forbiddenRemote: function (req, _res, next) {
if (
['127.0.0.1', '::ffff:127.0.0.1', '::1'].indexOf(
req.connection.remoteAddress
) === -1
) {
console.error('Access forbidden for ' + req.connection.remoteAddress)
next(newError(600))
} else {
next()
}
},
// forbid access to system paths
forbiddenPaths: function (req, _res, next) {
const reqPath = uri2filename(req.url)
if (FORBIDDEN_RE.test(reqPath)) {
next(newError(403))
return
}
next()
},
// check session
session: (token) => (req, res, next) => {
const cookies = cookie.parse(req.headers.cookie || '')
req.url = req._parsedUrl.pathname
if (token && req.query.session === token) {
// const domain = req._parsedUrl.hostname
res.setHeader(
'Set-Cookie',
cookie.serialize('session', token, { httpOnly: true, path: '/' })
)
res.redirect(req.url)
} else if (token && cookies.session === token) {
next()
} else {
next(newError(403))
}
},
// load config
config: (appConfig) => (req, res, next) => {
req._config = Object.assign({}, config, appConfig.config)
next()
},
renderConfig,
updateConfig,
// render cheatsheet
cheatsheet: (req, _res, next) => {
const file = path.resolve(__dirname, '..', 'test', 'cheatsheet.md')
req.url = filename2uri(file)
next()
},
// redirect to homeDir
home: (req, res) => {
res.redirect(homedir())
},
// unescape an escaped URL
unescape: function (req, _res, next) {
req.url = decodeURIComponent(req.url)
next()
},
// check if file exists and if is directory
stat: function (req, _res, next) {
const filename = uri2filename(req.url)
fs.stat(filename, (err, stats) => {
if (err) {
next(newError(404))
} else {
if (stats && stats.isDirectory()) req.isDirectory = true
next()
}
})
},
// try to autocorrect markdown links (e.g. for gitlab)
noextfile: function (req, res, next) {
let path
const filename = uri2filename(req.url)
if (!req.isDirectory && FILENOEXT.test(req.url)) {
async.eachSeries(
['', '.md', '.markdown'], // order of processing extensions
function (ext, cb) {
path = filename + ext
fs.stat(path, function (err) {
if (!err) {
return cb(new Error('found'))
}
cb()
})
},
function (/* err */) {
if (path && path !== filename) {
res.redirect(filename2uri(path))
} else {
next()
}
}
)
} else {
next()
}
},
// serve files without extension as plaintext files
plaintext: function (req, res, next) {
if (!req.isDirectory && FILETEXT.test(req.url)) {
const filename = uri2filename(req.url)
fs.readFile(filename, 'utf8', function (err, data) {
if (err) {
next(newError(404))
return
}
res.writeHead(200, { 'Content-Type': 'text/plain' })
res.write(data)
res.end()
})
} else {
next()
}
},
// make markdown conversion
markdown: function (req, res, next) {
// transform markdown pages
if (!req.isDirectory && MARKDOWNEXT.test(req.url)) {
const filename = uri2filename(req.url)
renderRequest(filename, req, res, next)
} else {
next()
}
},
// show index for directories
serveIndex: function (req, res, next) {
if (!req.isDirectory) {
next()
return
}
const originalUrl = req.url
const _drive = drive(req.url)
req.url = urlWithoutDrive(req.url)
serveIndex(_drive, {
icons: true,
filter: function (f) {
if (!req._config.filter) {
return f
} else {
// filter only markdown files
const stats = fs.statSync(uri2filename(originalUrl + '/' + f))
if (!stats.isFile() || MARKDOWNEXT.test(f)) {
return f
}
}
}
})(req, res, function (err) {
req.url = originalUrl
next(err)
})
},
// serve static files
serveStatic: function (path, options) {
return function (req, res, next) {
path = path || drive(req.url)
req.url = urlWithoutDrive(req.url)
serveStatic(path, options)(req, res, function (err) {
// called only if file cannot be served
if (!err) {
// `err` is undefined if file not found - so set
err = newError(404)
}
next(err)
})
}
},
// final 404 handler
four0four: function (req, res, next) {
next(newError(404))
},
// error handler
error: function (err, req, res, _next) {
if (!err.status) {
// Error objects
err = { status: 500, message: err.toString() }
}
const final = () => {
res.writeHead(err.status, { 'Content-Type': 'text/plain' })
res.write(err.message)
res.end()
}
if (err.status === 600) {
// remote access is forbidden
res.destroy()
} else if ([403, 404].includes(err.status)) {
const filename = path.resolve(__dirname, 'pages', `error${err.status}.md`)
renderRequest(filename, req, res, final, err.status)
} else {
final()
}
}
}
M.MARKDOWNEXT = MARKDOWNEXT
module.exports = M