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.

450 lines
14 KiB

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