blob: f13b7042bba9d5b605b27d98954b261079ac4e8d [file] [log] [blame]
/**
*
* Copyright (c) 2020 Silicon Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* This module provides the HTTP server functionality.
*
* @module JS API: http server
*/
const express = require('express')
const session = require('express-session')
const env = require('../util/env')
const querySession = require('../db/query-session.js')
const util = require('../util/util.js')
const webSocket = require('./ws-server.js')
const studio = require('../ide-integration/studio-rest-api')
const restApi = require('../../src-shared/rest-api.js')
const dbEnum = require('../../src-shared/db-enum.js')
const watchdog = require('../main-process/watchdog')
const initialize = require('../rest/initialize')
const dirtyFlag = require('../util/async-reporting')
const notification = require('../db/query-session-notification.js')
const restApiModules = [
require('../rest/admin.js'),
require('../rest/static-zcl.js'),
require('../rest/generation.js'),
require('../rest/file-ops.js'),
require('../rest/ide-api-handler.js'),
require('../rest/endpoint.js'),
require('../rest/user-data.js'),
require('../rest/initialize.js')
]
let httpServer = null
/**
* This function is used to register a rest module, which exports
* get/post/etc. arrays.
*
* @param {*} filename
* @param {*} db
* @param {*} app
*/
function registerRestApi(module, db, app) {
if (module.post != null)
module.post.forEach((singlePost) => {
let uri = singlePost.uri
let callback = singlePost.callback
app.post(uri, callback(db))
})
if (module.put != null)
module.put.forEach((singlePut) => {
let uri = singlePut.uri
let callback = singlePut.callback
app.put(uri, callback(db))
})
if (module.patch != null)
module.patch.forEach((singlePatch) => {
let uri = singlePatch.uri
let callback = singlePatch.callback
app.patch(uri, callback(db))
})
if (module.get != null)
module.get.forEach((singleGet) => {
let uri = singleGet.uri
let callback = singleGet.callback
app.get(uri, callback(db))
})
if (module.delete != null)
module.delete.forEach((singleDelete) => {
let uri = singleDelete.uri
let callback = singleDelete.callback
app.delete(uri, callback(db))
})
}
/**
* Register all REST modeules.
*
* @param {*} db
* @param {*} app
*/
function registerAllRestModules(db, app) {
restApiModules.forEach((module) => registerRestApi(module, db, app))
}
/**
* Promises to initialize the http server on a given port
* using a given database.
*
* @export
* @param {*} db Database object to use.
* @param {*} port Port for the HTTP server.
* @returns A promise that resolves with an express app.
*/
async function initHttpServer(
db,
port,
studioPort,
options = {
allowCors: false,
zcl: env.builtinSilabsZclMetafile(),
template: env.builtinTemplateMetafile()
}
) {
return new Promise((resolve, reject) => {
const app = express()
if (options.allowCors) {
env.logWarning('CORS is enabled. Please be careful.')
app.use(function (req, res, next) {
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader(
'Access-Control-Allow-Methods',
'GET,POST,PUT,PATCH,DELETE,OPTIONS'
)
res.setHeader(
'Access-Control-Allow-Headers',
'Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With'
)
next()
})
}
//app.use(express.urlencoded({ extended: true }))
app.use(express.json())
app.use(
session({
secret: 'Zap@Watt@SiliconLabs',
resave: true,
saveUninitialized: true
})
)
app.use(userSessionHandler(db, options))
// REST modules
registerAllRestModules(db, app)
// Static content
env.logDebug(`HTTP static content location: ${env.httpStaticContent()}`)
app.use(express.static(env.httpStaticContent()))
httpServer = app.listen(port, () => {
resolve(app)
})
process.on('uncaughtException', function (err) {
env.logWarning(`HTTP server exception.`, err)
if (err.errno === 'EADDRINUSE') {
httpServer = app.listen(0, () => {
resolve(app)
})
} else {
env.logError(err)
}
})
webSocket.initializeWebSocket(httpServer)
webSocket.onWebSocket(dbEnum.wsCategory.tick, () => {
watchdog.reset()
})
studio.initIdeIntegration(db, studioPort)
dirtyFlag.startAsyncReporting(db)
})
}
/**
* Handle user session.
*
* @param {*} db
* @param {*} options
* @returns function
*/
function userSessionHandler(db, options) {
return (req, res, next) => {
let sessionUuid = req.query[restApi.param.sessionId]
let userKey = req.session.id
if (sessionUuid == null || userKey == null) {
// This request does not carry a session along. Do nothing.
next()
} else {
let zapUserId = req.session.zapUserId
let zapSessionId
if (`zapSessionId` in req.session) {
zapSessionId = req.session.zapSessionId[sessionUuid]
} else {
req.session.zapSessionId = {}
zapSessionId = null
}
let tpk = req.query[restApi.param.templatePackageId]
let pkgArray = null
if (tpk) {
pkgArray = [tpk]
} else {
pkgArray = []
}
querySession
.getSessionInfoFromSessionKey(db, sessionUuid)
.then((result) => {
if (result) {
req.session.zapSessionId[sessionUuid] = result.sessionId
req.zapSessionId = result.sessionId
req.session.zapUserId = result.userRef
}
return result
})
.then(() => {
next()
})
.catch((err) => {
let resp = {
error: 'Could not create session: ' + err.message,
errorMessage: err
}
studio.sendSessionCreationErrorStatus(db, resp, zapSessionId)
env.logError(resp)
if (req.sessionId) {
notification.setNotification(db, 'ERROR', resp, zapSessionId, 1, 0)
}
})
}
}
}
/**
* Promises to shut down the http server.
*
* @export
* @returns Promise that resolves when server is shut down.
*/
function shutdownHttpServer() {
return new Promise((resolve, reject) => {
if (httpServer != null) {
shutdownHttpServerSync(() => resolve(null))
} else {
resolve(null)
}
})
}
/**
* Promises to shut down the http server.
*
* @export
* @returns Promise that resolves when server is shut down.
*/
function shutdownHttpServerSync(fn = null) {
if (httpServer != null) {
dirtyFlag.stopAsyncReporting()
studio.deinitIdeIntegration()
httpServer.close(() => {
env.logDebug('HTTP server shut down.')
httpServer = null
if (fn != null) fn()
})
}
}
/**
* Port http server is listening on.
*
* @export
* @returns port
*/
function httpServerPort() {
if (httpServer) {
return httpServer.address().port
} else {
return 0
}
}
/**
* Returns the URL of the server.
* @returns the server URL
*/
function httpServerUrl() {
return `http://localhost:${httpServerPort()}`
}
/**
* Returns the startup message that needs to be printed out.
*/
function httpServerStartupMessage() {
let ver = env.zapVersion()
return {
url: httpServerUrl(),
...ver
}
}
// exports
exports.initHttpServer = initHttpServer
exports.shutdownHttpServer = shutdownHttpServer
exports.shutdownHttpServerSync = shutdownHttpServerSync
exports.httpServerPort = httpServerPort
exports.httpServerStartupMessage = httpServerStartupMessage