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

459 lines
15 KiB

  1. 'use strict'
  2. module.exports = (port, spinner) => {
  3. const path = require('path')
  4. const ROOTPATH = process.cwd()
  5. const SERVERPATH = path.join(ROOTPATH, 'server')
  6. const IS_DEBUG = process.env.NODE_ENV === 'development'
  7. // ----------------------------------------
  8. // Load modules
  9. // ----------------------------------------
  10. const bodyParser = require('body-parser')
  11. const compression = require('compression')
  12. const express = require('express')
  13. const favicon = require('serve-favicon')
  14. const http = require('http')
  15. const Promise = require('bluebird')
  16. const fs = Promise.promisifyAll(require('fs-extra'))
  17. const yaml = require('js-yaml')
  18. const _ = require('lodash')
  19. const cfgHelper = require('./helpers/config')
  20. // ----------------------------------------
  21. // Define Express App
  22. // ----------------------------------------
  23. var app = express()
  24. app.use(compression())
  25. var server
  26. // ----------------------------------------
  27. // Public Assets
  28. // ----------------------------------------
  29. app.use(favicon(path.join(ROOTPATH, 'assets', 'favicon.ico')))
  30. app.use(express.static(path.join(ROOTPATH, 'assets')))
  31. // ----------------------------------------
  32. // View Engine Setup
  33. // ----------------------------------------
  34. app.set('views', path.join(SERVERPATH, 'views'))
  35. app.set('view engine', 'pug')
  36. app.use(bodyParser.json())
  37. app.use(bodyParser.urlencoded({ extended: false }))
  38. app.locals._ = require('lodash')
  39. // ----------------------------------------
  40. // Controllers
  41. // ----------------------------------------
  42. app.get('*', (req, res) => {
  43. let langs = []
  44. let conf = {}
  45. try {
  46. langs = yaml.safeLoad(fs.readFileSync(path.join(SERVERPATH, 'app/data.yml'), 'utf8')).langs
  47. conf = yaml.safeLoad(fs.readFileSync(path.join(ROOTPATH, 'config.yml'), 'utf8'))
  48. } catch (err) {
  49. console.error(err)
  50. }
  51. res.render('configure/index', {
  52. langs,
  53. conf,
  54. runmode: {
  55. staticPort: (process.env.WIKI_JS_HEROKU || process.env.WIKI_JS_DOCKER),
  56. staticMongo: (!_.isNil(process.env.WIKI_JS_HEROKU))
  57. }
  58. })
  59. })
  60. /**
  61. * Perform basic system checks
  62. */
  63. app.post('/syscheck', (req, res) => {
  64. Promise.mapSeries([
  65. () => {
  66. const semver = require('semver')
  67. if (!semver.satisfies(semver.clean(process.version), '>=6.9.0')) {
  68. throw new Error('Node.js version is too old. Minimum is v6.6.0.')
  69. }
  70. return 'Node.js ' + process.version + ' detected. Minimum is v6.9.0.'
  71. },
  72. () => {
  73. return Promise.try(() => {
  74. require('crypto')
  75. }).catch(err => { // eslint-disable-line handle-callback-err
  76. throw new Error('Crypto Node.js module is not available.')
  77. }).return('Node.js Crypto module is available.')
  78. },
  79. () => {
  80. const exec = require('child_process').exec
  81. const semver = require('semver')
  82. return new Promise((resolve, reject) => {
  83. exec('git --version', (err, stdout, stderr) => {
  84. if (err || stdout.length < 3) {
  85. reject(new Error('Git is not installed or not reachable from PATH.'))
  86. }
  87. let gitver = _.head(stdout.match(/[\d]+\.[\d]+(\.[\d]+)?/gi))
  88. if (!gitver || !semver.satisfies(semver.clean(gitver), '>=2.7.4')) {
  89. reject(new Error('Git version is too old. Minimum is v2.7.4.'))
  90. }
  91. resolve('Git v' + gitver + ' detected. Minimum is v2.7.4.')
  92. })
  93. })
  94. },
  95. () => {
  96. const os = require('os')
  97. if (os.totalmem() < 1000 * 1000 * 768) {
  98. throw new Error('Not enough memory. Minimum is 768 MB.')
  99. }
  100. return _.round(os.totalmem() / (1024 * 1024)) + ' MB of system memory available. Minimum is 768 MB.'
  101. },
  102. () => {
  103. let fs = require('fs')
  104. return Promise.try(() => {
  105. fs.accessSync(path.join(ROOTPATH, 'config.yml'), (fs.constants || fs).W_OK)
  106. }).catch(err => { // eslint-disable-line handle-callback-err
  107. throw new Error('config.yml file is not writable by Node.js process or was not created properly.')
  108. }).return('config.yml is writable by the setup process.')
  109. }
  110. ], test => { return test() }).then(results => {
  111. res.json({ ok: true, results })
  112. }).catch(err => {
  113. res.json({ ok: false, error: err.message })
  114. })
  115. })
  116. /**
  117. * Check the DB connection
  118. */
  119. app.post('/dbcheck', (req, res) => {
  120. let mongo = require('mongodb').MongoClient
  121. let mongoURI = cfgHelper.parseConfigValue(req.body.db)
  122. mongo.connect(mongoURI, {
  123. autoReconnect: false,
  124. reconnectTries: 2,
  125. reconnectInterval: 1000,
  126. connectTimeoutMS: 5000,
  127. socketTimeoutMS: 5000
  128. }, (err, db) => {
  129. if (err === null) {
  130. // Try to create a test collection
  131. db.createCollection('test', (err, results) => {
  132. if (err === null) {
  133. // Try to drop test collection
  134. db.dropCollection('test', (err, results) => {
  135. if (err === null) {
  136. res.json({ ok: true })
  137. } else {
  138. res.json({ ok: false, error: 'Unable to delete test collection. Verify permissions. ' + err.message })
  139. }
  140. db.close()
  141. })
  142. } else {
  143. res.json({ ok: false, error: 'Unable to create test collection. Verify permissions. ' + err.message })
  144. db.close()
  145. }
  146. })
  147. } else {
  148. res.json({ ok: false, error: err.message })
  149. }
  150. })
  151. })
  152. /**
  153. * Check the Git connection
  154. */
  155. app.post('/gitcheck', (req, res) => {
  156. const exec = require('execa')
  157. const url = require('url')
  158. const dataDir = path.resolve(ROOTPATH, cfgHelper.parseConfigValue(req.body.pathData))
  159. const gitDir = path.resolve(ROOTPATH, cfgHelper.parseConfigValue(req.body.pathRepo))
  160. let gitRemoteUrl = ''
  161. if (req.body.gitUseRemote === true) {
  162. let urlObj = url.parse(cfgHelper.parseConfigValue(req.body.gitUrl))
  163. if (req.body.gitAuthType === 'basic') {
  164. urlObj.auth = req.body.gitAuthUser + ':' + req.body.gitAuthPass
  165. }
  166. gitRemoteUrl = url.format(urlObj)
  167. }
  168. Promise.mapSeries([
  169. () => {
  170. return fs.ensureDirAsync(dataDir).return('Data directory path is valid.')
  171. },
  172. () => {
  173. return fs.ensureDirAsync(gitDir).return('Git directory path is valid.')
  174. },
  175. () => {
  176. return exec.stdout('git', ['init'], { cwd: gitDir }).then(result => {
  177. return 'Local git repository has been initialized.'
  178. })
  179. },
  180. () => {
  181. if (req.body.gitUseRemote === false) { return false }
  182. return exec.stdout('git', ['config', '--local', 'user.name', 'Wiki'], { cwd: gitDir }).then(result => {
  183. return 'Git Signature Name has been set successfully.'
  184. })
  185. },
  186. () => {
  187. if (req.body.gitUseRemote === false) { return false }
  188. return exec.stdout('git', ['config', '--local', 'user.email', req.body.gitServerEmail], { cwd: gitDir }).then(result => {
  189. return 'Git Signature Name has been set successfully.'
  190. })
  191. },
  192. () => {
  193. if (req.body.gitUseRemote === false) { return false }
  194. return exec.stdout('git', ['config', '--local', '--bool', 'http.sslVerify', req.body.gitAuthSSL], { cwd: gitDir }).then(result => {
  195. return 'Git SSL Verify flag has been set successfully.'
  196. })
  197. },
  198. () => {
  199. if (req.body.gitUseRemote === false) { return false }
  200. if (req.body.gitAuthType === 'ssh') {
  201. return exec.stdout('git', ['config', '--local', 'core.sshCommand', 'ssh -i "' + req.body.gitAuthSSHKey + '" -o StrictHostKeyChecking=no'], { cwd: gitDir }).then(result => {
  202. return 'Git SSH Private Key path has been set successfully.'
  203. })
  204. } else {
  205. return false
  206. }
  207. },
  208. () => {
  209. if (req.body.gitUseRemote === false) { return false }
  210. return exec.stdout('git', ['remote', 'rm', 'origin'], { cwd: gitDir }).catch(err => {
  211. if (_.includes(err.message, 'No such remote') || _.includes(err.message, 'Could not remove')) {
  212. return true
  213. } else {
  214. throw err
  215. }
  216. }).then(() => {
  217. return exec.stdout('git', ['remote', 'add', 'origin', gitRemoteUrl], { cwd: gitDir }).then(result => {
  218. return 'Git Remote was added successfully.'
  219. })
  220. })
  221. },
  222. () => {
  223. if (req.body.gitUseRemote === false) { return false }
  224. return exec.stdout('git', ['pull', 'origin', req.body.gitBranch], { cwd: gitDir }).then(result => {
  225. return 'Git Pull operation successful.'
  226. })
  227. }
  228. ], step => { return step() }).then(results => {
  229. return res.json({ ok: true, results: _.without(results, false) })
  230. }).catch(err => {
  231. let errMsg = (err.stderr) ? err.stderr.replace(/(error:|warning:|fatal:)/gi, '').replace(/ \s+/g, ' ') : err.message
  232. res.json({ ok: false, error: errMsg })
  233. })
  234. })
  235. /**
  236. * Finalize
  237. */
  238. app.post('/finalize', (req, res) => {
  239. const bcrypt = require('bcryptjs-then')
  240. const crypto = Promise.promisifyAll(require('crypto'))
  241. let mongo = require('mongodb').MongoClient
  242. let parsedMongoConStr = cfgHelper.parseConfigValue(req.body.db)
  243. Promise.join(
  244. new Promise((resolve, reject) => {
  245. mongo.connect(parsedMongoConStr, {
  246. autoReconnect: false,
  247. reconnectTries: 2,
  248. reconnectInterval: 1000,
  249. connectTimeoutMS: 5000,
  250. socketTimeoutMS: 5000
  251. }, (err, db) => {
  252. if (err === null) {
  253. db.createCollection('users', { strict: false }, (err, results) => {
  254. if (err === null) {
  255. bcrypt.hash(req.body.adminPassword).then(adminPwdHash => {
  256. db.collection('users').findOneAndUpdate({
  257. provider: 'local',
  258. email: req.body.adminEmail
  259. }, {
  260. provider: 'local',
  261. email: req.body.adminEmail,
  262. name: 'Administrator',
  263. password: adminPwdHash,
  264. rights: [{
  265. role: 'admin',
  266. path: '/',
  267. exact: false,
  268. deny: false
  269. }],
  270. updatedAt: new Date(),
  271. createdAt: new Date()
  272. }, {
  273. upsert: true,
  274. returnOriginal: false
  275. }, (err, results) => {
  276. if (err === null) {
  277. resolve(true)
  278. } else {
  279. reject(err)
  280. }
  281. db.close()
  282. })
  283. })
  284. } else {
  285. reject(err)
  286. db.close()
  287. }
  288. })
  289. } else {
  290. reject(err)
  291. }
  292. })
  293. }),
  294. fs.readFileAsync(path.join(ROOTPATH, 'config.yml'), 'utf8').then(confRaw => {
  295. let conf = yaml.safeLoad(confRaw)
  296. conf.title = req.body.title
  297. conf.host = req.body.host
  298. conf.port = req.body.port
  299. conf.paths = {
  300. repo: req.body.pathRepo,
  301. data: req.body.pathData
  302. }
  303. conf.uploads = {
  304. maxImageFileSize: (conf.uploads && _.isNumber(conf.uploads.maxImageFileSize)) ? conf.uploads.maxImageFileSize : 3,
  305. maxOtherFileSize: (conf.uploads && _.isNumber(conf.uploads.maxOtherFileSize)) ? conf.uploads.maxOtherFileSize : 100
  306. }
  307. conf.lang = req.body.lang
  308. conf.public = (req.body.public === true)
  309. if (conf.auth && conf.auth.local) {
  310. conf.auth.local = { enabled: true }
  311. } else {
  312. conf.auth = { local: { enabled: true } }
  313. }
  314. conf.db = req.body.db
  315. if (req.body.gitUseRemote === false) {
  316. conf.git = false
  317. } else {
  318. conf.git = {
  319. url: req.body.gitUrl,
  320. branch: req.body.gitBranch,
  321. auth: {
  322. type: req.body.gitAuthType,
  323. username: req.body.gitAuthUser,
  324. password: req.body.gitAuthPass,
  325. privateKey: req.body.gitAuthSSHKey,
  326. sslVerify: (req.body.gitAuthSSL === true)
  327. },
  328. showUserEmail: (req.body.gitShowUserEmail === true),
  329. serverEmail: req.body.gitServerEmail
  330. }
  331. }
  332. return crypto.randomBytesAsync(32).then(buf => {
  333. conf.sessionSecret = buf.toString('hex')
  334. confRaw = yaml.safeDump(conf)
  335. return fs.writeFileAsync(path.join(ROOTPATH, 'config.yml'), confRaw)
  336. })
  337. })
  338. ).then(() => {
  339. if (process.env.IS_HEROKU) {
  340. return fs.outputJsonAsync(path.join(SERVERPATH, 'app/heroku.json'), { configured: true })
  341. } else {
  342. return true
  343. }
  344. }).then(() => {
  345. res.json({ ok: true })
  346. }).catch(err => {
  347. res.json({ ok: false, error: err.message })
  348. })
  349. })
  350. /**
  351. * Restart in normal mode
  352. */
  353. app.post('/restart', (req, res) => {
  354. res.status(204).end()
  355. server.destroy(() => {
  356. spinner.text = 'Setup wizard terminated. Restarting in normal mode...'
  357. _.delay(() => {
  358. const exec = require('execa')
  359. exec.stdout('node', ['wiki', 'start']).then(result => {
  360. spinner.succeed('Wiki.js is now running in normal mode!')
  361. process.exit(0)
  362. })
  363. }, 1000)
  364. })
  365. })
  366. // ----------------------------------------
  367. // Error handling
  368. // ----------------------------------------
  369. app.use(function (req, res, next) {
  370. var err = new Error('Not Found')
  371. err.status = 404
  372. next(err)
  373. })
  374. app.use(function (err, req, res, next) {
  375. res.status(err.status || 500)
  376. res.send({
  377. message: err.message,
  378. error: IS_DEBUG ? err : {}
  379. })
  380. spinner.fail(err.message)
  381. process.exit(1)
  382. })
  383. // ----------------------------------------
  384. // Start HTTP server
  385. // ----------------------------------------
  386. spinner.text = 'Starting HTTP server...'
  387. app.set('port', port)
  388. server = http.createServer(app)
  389. server.listen(port)
  390. var openConnections = []
  391. server.on('connection', (conn) => {
  392. let key = conn.remoteAddress + ':' + conn.remotePort
  393. openConnections[key] = conn
  394. conn.on('close', () => {
  395. delete openConnections[key]
  396. })
  397. })
  398. server.destroy = (cb) => {
  399. server.close(cb)
  400. for (let key in openConnections) {
  401. openConnections[key].destroy()
  402. }
  403. }
  404. server.on('error', (error) => {
  405. if (error.syscall !== 'listen') {
  406. throw error
  407. }
  408. switch (error.code) {
  409. case 'EACCES':
  410. spinner.fail('Listening on port ' + port + ' requires elevated privileges!')
  411. return process.exit(1)
  412. case 'EADDRINUSE':
  413. spinner.fail('Port ' + port + ' is already in use!')
  414. return process.exit(1)
  415. default:
  416. throw error
  417. }
  418. })
  419. server.on('listening', () => {
  420. spinner.text = 'Browse to http://localhost:' + port + ' to configure Wiki.js!'
  421. })
  422. }