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.

452 lines
14 KiB

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