audiobookshelf/server/Server.js

397 lines
13 KiB
JavaScript
Raw Normal View History

2021-08-18 00:01:11 +02:00
const Path = require('path')
const Sequelize = require('sequelize')
2021-08-18 00:01:11 +02:00
const express = require('express')
const http = require('http')
2022-07-06 02:53:01 +02:00
const fs = require('./libs/fsExtra')
2022-07-07 02:10:25 +02:00
const fileUpload = require('./libs/expressFileupload')
2022-07-07 02:14:47 +02:00
const rateLimit = require('./libs/expressRateLimit')
const cookieParser = require("cookie-parser")
2021-08-18 00:01:11 +02:00
const { version } = require('../package.json')
// Utils
const fileUtils = require('./utils/fileUtils')
const Logger = require('./Logger')
2021-08-18 00:01:11 +02:00
const Auth = require('./Auth')
const Watcher = require('./Watcher')
2023-07-05 01:14:44 +02:00
const Database = require('./Database')
const SocketAuthority = require('./SocketAuthority')
const ApiRouter = require('./routers/ApiRouter')
const HlsRouter = require('./routers/HlsRouter')
2022-09-21 01:08:41 +02:00
const NotificationManager = require('./managers/NotificationManager')
const EmailManager = require('./managers/EmailManager')
const AbMergeManager = require('./managers/AbMergeManager')
const CacheManager = require('./managers/CacheManager')
const LogManager = require('./managers/LogManager')
const BackupManager = require('./managers/BackupManager')
const PlaybackSessionManager = require('./managers/PlaybackSessionManager')
const PodcastManager = require('./managers/PodcastManager')
const AudioMetadataMangaer = require('./managers/AudioMetadataManager')
const RssFeedManager = require('./managers/RssFeedManager')
const CronManager = require('./managers/CronManager')
const LibraryScanner = require('./scanner/LibraryScanner')
2023-03-24 18:21:25 +01:00
//Import the main Passport and Express-Session library
const passport = require('passport')
const expressSession = require('express-session')
2021-08-18 00:01:11 +02:00
class Server {
constructor(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH) {
2021-08-18 00:01:11 +02:00
this.Port = PORT
this.Host = HOST
global.Source = SOURCE
global.isWin = process.platform === 'win32'
global.Uid = isNaN(UID) ? undefined : Number(UID)
global.Gid = isNaN(GID) ? undefined : Number(GID)
global.ConfigPath = fileUtils.filePathToPOSIX(Path.normalize(CONFIG_PATH))
global.MetadataPath = fileUtils.filePathToPOSIX(Path.normalize(METADATA_PATH))
global.RouterBasePath = ROUTER_BASE_PATH
2023-04-29 23:05:05 +02:00
global.XAccel = process.env.USE_X_ACCEL
if (!fs.pathExistsSync(global.ConfigPath)) {
fs.mkdirSync(global.ConfigPath)
}
if (!fs.pathExistsSync(global.MetadataPath)) {
fs.mkdirSync(global.MetadataPath)
}
2021-08-18 00:01:11 +02:00
this.watcher = new Watcher()
2023-07-05 01:14:44 +02:00
this.auth = new Auth()
// Managers
2023-07-05 01:14:44 +02:00
this.notificationManager = new NotificationManager()
this.emailManager = new EmailManager()
2023-07-08 21:40:49 +02:00
this.backupManager = new BackupManager()
2023-07-05 01:14:44 +02:00
this.logManager = new LogManager()
2023-10-20 23:39:32 +02:00
this.abMergeManager = new AbMergeManager()
2023-07-05 01:14:44 +02:00
this.playbackSessionManager = new PlaybackSessionManager()
2023-10-20 23:39:32 +02:00
this.podcastManager = new PodcastManager(this.watcher, this.notificationManager)
this.audioMetadataManager = new AudioMetadataMangaer()
2023-07-05 01:14:44 +02:00
this.rssFeedManager = new RssFeedManager()
this.cronManager = new CronManager(this.podcastManager)
// Routers
this.apiRouter = new ApiRouter(this)
2023-07-05 01:14:44 +02:00
this.hlsRouter = new HlsRouter(this.auth, this.playbackSessionManager)
Logger.logManager = this.logManager
2021-08-18 00:01:11 +02:00
this.server = null
this.io = null
}
authMiddleware(req, res, next) {
2023-09-20 19:37:55 +02:00
// ask passportjs if the current request is authenticated
2023-03-24 18:21:25 +01:00
this.auth.isAuthenticated(req, res, next)
}
cancelLibraryScan(libraryId) {
LibraryScanner.setCancelLibraryScan(libraryId)
}
/**
* Initialize database, backups, logs, rss feeds, cron jobs & watcher
* Cleanup stale/invalid data
*/
2021-08-18 00:01:11 +02:00
async init() {
Logger.info('[Server] Init v' + version)
await this.playbackSessionManager.removeOrphanStreams()
2021-09-23 03:40:35 +02:00
2023-07-05 01:14:44 +02:00
await Database.init(false)
2022-07-19 00:19:16 +02:00
// Create token secret if does not exist (Added v2.1.0)
2023-07-05 01:14:44 +02:00
if (!Database.serverSettings.tokenSecret) {
2022-07-19 00:19:16 +02:00
await this.auth.initTokenSecret()
}
await this.cleanUserData() // Remove invalid user item progress
2023-09-07 00:48:50 +02:00
await CacheManager.ensureCachePaths()
2022-06-18 20:11:15 +02:00
await this.backupManager.init()
await this.logManager.init()
await this.rssFeedManager.init()
2023-08-20 20:34:03 +02:00
const libraries = await Database.libraryModel.getAllOldLibraries()
await this.cronManager.init(libraries)
2023-07-05 01:14:44 +02:00
if (Database.serverSettings.scannerDisableWatcher) {
Logger.info(`[Server] Watcher is disabled`)
this.watcher.disabled = true
} else {
this.watcher.initWatcher(libraries)
}
2021-08-18 00:01:11 +02:00
}
async start() {
Logger.info('=== Starting Server ===')
await this.init()
const app = express()
2023-03-24 18:21:25 +01:00
/**
* @temporary
* This is necessary for the ebook API endpoint in the mobile apps
* The mobile app ereader is using fetch api in Capacitor that is currently difficult to switch to native requests
* so we have to allow cors for specific origins to the /api/items/:id/ebook endpoint
* @see https://ionicframework.com/docs/troubleshooting/cors
*/
app.use((req, res, next) => {
if (req.path.match(/\/api\/items\/([a-z0-9-]{36})\/ebook(\/[0-9]+)?/)) {
const allowedOrigins = ['capacitor://localhost', 'http://localhost']
if (allowedOrigins.some(o => o === req.get('origin'))) {
res.header('Access-Control-Allow-Origin', req.get('origin'))
res.header("Access-Control-Allow-Methods", 'GET, POST, PATCH, PUT, DELETE, OPTIONS')
res.header('Access-Control-Allow-Headers', '*')
res.header('Access-Control-Allow-Credentials', true)
if (req.method === 'OPTIONS') {
return res.sendStatus(200)
}
}
}
next()
})
// parse cookies in requests
app.use(cookieParser())
2023-03-24 18:21:25 +01:00
// enable express-session
app.use(expressSession({
secret: global.ServerSettings.tokenSecret,
resave: false,
saveUninitialized: false,
cookie: {
2023-09-20 19:37:55 +02:00
// also send the cookie if were are not on https (not every use has https)
2023-03-24 18:21:25 +01:00
secure: false
},
}))
// init passport.js
app.use(passport.initialize())
// register passport in express-session
app.use(passport.session())
// config passport.js
2023-09-16 20:51:29 +02:00
await this.auth.initPassportJs()
2023-03-24 18:21:25 +01:00
const router = express.Router()
app.use(global.RouterBasePath, router)
app.disable('x-powered-by')
2021-08-18 00:01:11 +02:00
this.server = http.createServer(app)
2023-06-04 06:44:13 +02:00
router.use(fileUpload({
defCharset: 'utf8',
defParamCharset: 'utf8',
useTempFiles: true,
2023-06-06 22:40:52 +02:00
tempFileDir: Path.join(global.MetadataPath, 'tmp')
2023-06-04 06:44:13 +02:00
}))
router.use(express.urlencoded({ extended: true, limit: "5mb" }))
router.use(express.json({ limit: "5mb" }))
2021-08-18 00:01:11 +02:00
// Static path to generated nuxt
const distPath = Path.join(global.appRoot, '/client/dist')
router.use(express.static(distPath))
// Static folder
router.use(express.static(Path.join(global.appRoot, 'static')))
2021-08-18 00:01:11 +02:00
router.use('/api', this.authMiddleware.bind(this), this.apiRouter.router)
router.use('/hls', this.authMiddleware.bind(this), this.hlsRouter.router)
// RSS Feed temp route
router.get('/feed/:slug', (req, res) => {
Logger.info(`[Server] Requesting rss feed ${req.params.slug}`)
this.rssFeedManager.getFeed(req, res)
})
router.get('/feed/:slug/cover*', (req, res) => {
this.rssFeedManager.getFeedCover(req, res)
})
router.get('/feed/:slug/item/:episodeId/*', (req, res) => {
Logger.debug(`[Server] Requesting rss feed episode ${req.params.slug}/${req.params.episodeId}`)
this.rssFeedManager.getFeedItem(req, res)
})
2023-09-13 18:35:39 +02:00
// Auth routes
2023-09-20 19:37:55 +02:00
await this.auth.initAuthRoutes(router)
2023-09-13 18:35:39 +02:00
// Client dynamic routes
const dyanimicRoutes = [
'/item/:id',
'/author/:id',
'/audiobook/:id/chapters',
'/audiobook/:id/edit',
'/audiobook/:id/manage',
'/library/:library',
'/library/:library/search',
'/library/:library/bookshelf/:id?',
'/library/:library/authors',
'/library/:library/series/:id?',
2022-09-17 22:23:33 +02:00
'/library/:library/podcast/search',
'/library/:library/podcast/latest',
'/library/:library/podcast/download-queue',
'/config/users/:id',
'/config/users/:id/sessions',
'/config/item-metadata-utils/:id',
2022-11-27 21:23:28 +01:00
'/collection/:id',
'/playlist/:id'
]
dyanimicRoutes.forEach((route) => router.get(route, (req, res) => res.sendFile(Path.join(distPath, 'index.html'))))
2023-03-24 18:21:25 +01:00
// router.post('/login', passport.authenticate('local', this.auth.login), this.auth.loginResult.bind(this))
// router.post('/logout', this.authMiddleware.bind(this), this.logout.bind(this))
router.post('/init', (req, res) => {
2023-07-05 01:14:44 +02:00
if (Database.hasRootUser) {
Logger.error(`[Server] attempt to init server when server already has a root user`)
return res.sendStatus(500)
}
this.initializeServer(req, res)
})
router.get('/status', (req, res) => {
// status check for client to see if server has been initialized
// server has been initialized if a root user exists
const payload = {
app: 'audiobookshelf',
serverVersion: version,
2023-07-05 01:14:44 +02:00
isInit: Database.hasRootUser,
language: Database.serverSettings.language,
authMethods: Database.serverSettings.authActiveAuthMethods,
authFormData: Database.serverSettings.authFormData
}
if (!payload.isInit) {
payload.ConfigPath = global.ConfigPath
payload.MetadataPath = global.MetadataPath
}
res.json(payload)
})
router.get('/ping', (req, res) => {
2022-07-30 15:37:35 +02:00
Logger.info('Received ping')
2021-08-18 00:01:11 +02:00
res.json({ success: true })
})
app.get('/healthcheck', (req, res) => res.sendStatus(200))
2021-08-18 00:01:11 +02:00
this.server.listen(this.Port, this.Host, () => {
if (this.Host) Logger.info(`Listening on http://${this.Host}:${this.Port}`)
else Logger.info(`Listening on port :${this.Port}`)
2021-08-18 00:01:11 +02:00
})
// Start listening for socket connections
SocketAuthority.initialize(this)
2021-08-18 00:01:11 +02:00
}
async initializeServer(req, res) {
Logger.info(`[Server] Initializing new server`)
const newRoot = req.body.newRoot
const rootUsername = newRoot.username || 'root'
const rootPash = newRoot.password ? await this.auth.hashPass(newRoot.password) : ''
if (!rootPash) Logger.warn(`[Server] Creating root user with no password`)
await Database.createRootUser(rootUsername, rootPash, this.auth)
res.sendStatus(200)
}
/**
* Remove user media progress for items that no longer exist & remove seriesHideFrom that no longer exist
*/
async cleanUserData() {
// Get all media progress without an associated media item
2023-08-20 20:34:03 +02:00
const mediaProgressToRemove = await Database.mediaProgressModel.findAll({
where: {
'$podcastEpisode.id$': null,
'$book.id$': null
},
attributes: ['id'],
include: [
{
2023-08-20 20:34:03 +02:00
model: Database.bookModel,
attributes: ['id']
},
{
2023-08-20 20:34:03 +02:00
model: Database.podcastEpisodeModel,
attributes: ['id']
}
]
})
if (mediaProgressToRemove.length) {
// Remove media progress
2023-08-20 20:34:03 +02:00
const mediaProgressRemoved = await Database.mediaProgressModel.destroy({
where: {
id: {
[Sequelize.Op.in]: mediaProgressToRemove.map(mp => mp.id)
2023-07-05 01:14:44 +02:00
}
}
})
if (mediaProgressRemoved) {
Logger.info(`[Server] Removed ${mediaProgressRemoved} media progress for media items that no longer exist in db`)
}
}
// Remove series from hide from continue listening that no longer exist
2023-08-20 20:34:03 +02:00
const users = await Database.userModel.getOldUsers()
for (const _user of users) {
let hasUpdated = false
if (_user.seriesHideFromContinueListening.length) {
const seriesHiding = (await Database.seriesModel.findAll({
where: {
id: _user.seriesHideFromContinueListening
},
attributes: ['id'],
raw: true
})).map(se => se.id)
_user.seriesHideFromContinueListening = _user.seriesHideFromContinueListening.filter(seriesId => {
if (!seriesHiding.includes(seriesId)) { // Series removed
hasUpdated = true
return false
}
return true
})
}
if (hasUpdated) {
2023-07-05 01:14:44 +02:00
await Database.updateUser(_user)
}
}
}
// First time login rate limit is hit
loginLimitReached(req, res, options) {
Logger.error(`[Server] Login rate limit (${options.max}) was hit for ip ${req.ip}`)
options.message = 'Too many attempts. Login temporarily locked.'
}
getLoginRateLimiter() {
return rateLimit({
2023-07-05 01:14:44 +02:00
windowMs: Database.serverSettings.rateLimitLoginWindow, // 5 minutes
max: Database.serverSettings.rateLimitLoginRequests,
skipSuccessfulRequests: true,
onLimitReached: this.loginLimitReached
})
}
2021-08-18 00:01:11 +02:00
logout(req, res) {
if (req.body.socketId) {
Logger.info(`[Server] User ${req.user ? req.user.username : 'Unknown'} is logging out with socket ${req.body.socketId}`)
SocketAuthority.logout(req.body.socketId)
}
2021-08-18 00:01:11 +02:00
res.sendStatus(200)
}
async stop() {
await this.watcher.close()
Logger.info('Watcher Closed')
return new Promise((resolve) => {
this.server.close((err) => {
if (err) {
Logger.error('Failed to close server', err)
} else {
Logger.info('Server successfully closed')
}
resolve()
})
})
}
}
module.exports = Server