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.

443 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 = _.chain(stdout.replace(/[^\d.]/g, '')).split('.').take(3).join('.').value()
  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. mongo.connect(req.body.db, {
  113. autoReconnect: false,
  114. reconnectTries: 2,
  115. reconnectInterval: 1000,
  116. connectTimeoutMS: 5000,
  117. socketTimeoutMS: 5000
  118. }, (err, db) => {
  119. if (err === null) {
  120. // Try to create a test collection
  121. db.createCollection('test', (err, results) => {
  122. if (err === null) {
  123. // Try to drop test collection
  124. db.dropCollection('test', (err, results) => {
  125. if (err === null) {
  126. res.json({ ok: true })
  127. } else {
  128. res.json({ ok: false, error: 'Unable to delete test collection. Verify permissions. ' + err.message })
  129. }
  130. db.close()
  131. })
  132. } else {
  133. res.json({ ok: false, error: 'Unable to create test collection. Verify permissions. ' + err.message })
  134. db.close()
  135. }
  136. })
  137. } else {
  138. res.json({ ok: false, error: err.message })
  139. }
  140. })
  141. })
  142. /**
  143. * Check the Git connection
  144. */
  145. app.post('/gitcheck', (req, res) => {
  146. const exec = require('execa')
  147. const url = require('url')
  148. const dataDir = path.resolve(ROOTPATH, req.body.pathData)
  149. const gitDir = path.resolve(ROOTPATH, req.body.pathRepo)
  150. let gitRemoteUrl = ''
  151. if (req.body.gitUseRemote === true) {
  152. let urlObj = url.parse(req.body.gitUrl)
  153. if (req.body.gitAuthType === 'basic') {
  154. urlObj.auth = req.body.gitAuthUser + ':' + req.body.gitAuthPass
  155. }
  156. gitRemoteUrl = url.format(urlObj)
  157. }
  158. Promise.mapSeries([
  159. () => {
  160. return fs.ensureDirAsync(dataDir).return('Data directory path is valid.')
  161. },
  162. () => {
  163. return fs.ensureDirAsync(gitDir).return('Git directory path is valid.')
  164. },
  165. () => {
  166. return exec.stdout('git', ['init'], { cwd: gitDir }).then(result => {
  167. return 'Local git repository has been initialized.'
  168. })
  169. },
  170. () => {
  171. if (req.body.gitUseRemote === false) { return false }
  172. return exec.stdout('git', ['config', '--local', 'user.name', req.body.gitSignatureName], { cwd: gitDir }).then(result => {
  173. return 'Git Signature Name has been set successfully.'
  174. })
  175. },
  176. () => {
  177. if (req.body.gitUseRemote === false) { return false }
  178. return exec.stdout('git', ['config', '--local', 'user.email', req.body.gitSignatureEmail], { cwd: gitDir }).then(result => {
  179. return 'Git Signature Name has been set successfully.'
  180. })
  181. },
  182. () => {
  183. if (req.body.gitUseRemote === false) { return false }
  184. return exec.stdout('git', ['config', '--local', '--bool', 'http.sslVerify', req.body.gitAuthSSL], { cwd: gitDir }).then(result => {
  185. return 'Git SSL Verify flag has been set successfully.'
  186. })
  187. },
  188. () => {
  189. if (req.body.gitUseRemote === false) { return false }
  190. if (req.body.gitAuthType === 'ssh') {
  191. return exec.stdout('git', ['config', '--local', 'core.sshCommand', 'ssh -i "' + req.body.gitAuthSSHKey + '" -o StrictHostKeyChecking=no'], { cwd: gitDir }).then(result => {
  192. return 'Git SSH Private Key path has been set successfully.'
  193. })
  194. } else {
  195. return false
  196. }
  197. },
  198. () => {
  199. if (req.body.gitUseRemote === false) { return false }
  200. return exec.stdout('git', ['remote', 'remove', 'origin'], { cwd: gitDir }).catch(err => {
  201. if (_.includes(err.message, 'No such remote')) {
  202. return true
  203. } else {
  204. throw err
  205. }
  206. }).then(() => {
  207. return exec.stdout('git', ['remote', 'add', 'origin', gitRemoteUrl], { cwd: gitDir }).then(result => {
  208. return 'Git Remote was added successfully.'
  209. })
  210. })
  211. },
  212. () => {
  213. if (req.body.gitUseRemote === false) { return false }
  214. return exec.stdout('git', ['pull', 'origin', req.body.gitBranch], { cwd: gitDir }).then(result => {
  215. return 'Git Pull operation successful.'
  216. })
  217. }
  218. ], step => { return step() }).then(results => {
  219. return res.json({ ok: true, results: _.without(results, false) })
  220. }).catch(err => {
  221. let errMsg = (err.stderr) ? err.stderr.replace(/(error:|warning:|fatal:)/gi, '').replace(/ \s+/g, ' ') : err.message
  222. res.json({ ok: false, error: errMsg })
  223. })
  224. })
  225. /**
  226. * Finalize
  227. */
  228. app.post('/finalize', (req, res) => {
  229. const bcrypt = require('bcryptjs-then')
  230. const crypto = Promise.promisifyAll(require('crypto'))
  231. let mongo = require('mongodb').MongoClient
  232. Promise.join(
  233. new Promise((resolve, reject) => {
  234. mongo.connect(req.body.db, {
  235. autoReconnect: false,
  236. reconnectTries: 2,
  237. reconnectInterval: 1000,
  238. connectTimeoutMS: 5000,
  239. socketTimeoutMS: 5000
  240. }, (err, db) => {
  241. if (err === null) {
  242. db.createCollection('users', { strict: false }, (err, results) => {
  243. if (err === null) {
  244. bcrypt.hash(req.body.adminPassword).then(adminPwdHash => {
  245. db.collection('users').findOneAndUpdate({
  246. provider: 'local',
  247. email: req.body.adminEmail
  248. }, {
  249. provider: 'local',
  250. email: req.body.adminEmail,
  251. name: 'Administrator',
  252. password: adminPwdHash,
  253. rights: [{
  254. role: 'admin',
  255. path: '/',
  256. exact: false,
  257. deny: false
  258. }],
  259. updatedAt: new Date(),
  260. createdAt: new Date()
  261. }, {
  262. upsert: true,
  263. returnOriginal: false
  264. }, (err, results) => {
  265. if (err === null) {
  266. resolve(true)
  267. } else {
  268. reject(err)
  269. }
  270. db.close()
  271. })
  272. })
  273. } else {
  274. reject(err)
  275. db.close()
  276. }
  277. })
  278. } else {
  279. reject(err)
  280. }
  281. })
  282. }),
  283. fs.readFileAsync('./config.yml', 'utf8').then(confRaw => {
  284. let conf = yaml.safeLoad(confRaw)
  285. conf.title = req.body.title
  286. conf.host = req.body.host
  287. conf.port = req.body.port
  288. conf.paths = {
  289. repo: req.body.pathRepo,
  290. data: req.body.pathData
  291. }
  292. conf.uploads = {
  293. maxImageFileSize: (conf.uploads && _.isNumber(conf.uploads.maxImageFileSize)) ? conf.uploads.maxImageFileSize : 3,
  294. maxOtherFileSize: (conf.uploads && _.isNumber(conf.uploads.maxOtherFileSize)) ? conf.uploads.maxOtherFileSize : 100
  295. }
  296. conf.lang = req.body.lang
  297. conf.public = (conf.public === true)
  298. if (conf.auth && conf.auth.local) {
  299. conf.auth.local = { enabled: true }
  300. } else {
  301. conf.auth = { local: { enabled: true } }
  302. }
  303. conf.db = req.body.db
  304. if (req.body.gitUseRemote === false) {
  305. conf.git = false
  306. } else {
  307. conf.git = {
  308. url: req.body.gitUrl,
  309. branch: req.body.gitBranch,
  310. auth: {
  311. type: req.body.gitAuthType,
  312. username: req.body.gitAuthUser,
  313. password: req.body.gitAuthPass,
  314. privateKey: req.body.gitAuthSSHKey,
  315. sslVerify: (req.body.gitAuthSSL === true)
  316. },
  317. signature: {
  318. name: req.body.gitSignatureName,
  319. email: req.body.gitSignatureEmail
  320. }
  321. }
  322. }
  323. return crypto.randomBytesAsync(32).then(buf => {
  324. conf.sessionSecret = buf.toString('hex')
  325. confRaw = yaml.safeDump(conf)
  326. return fs.writeFileAsync('./config.yml', confRaw)
  327. })
  328. })
  329. ).then(() => {
  330. res.json({ ok: true })
  331. }).catch(err => {
  332. res.json({ ok: false, error: err.message })
  333. })
  334. })
  335. /**
  336. * Restart in normal mode
  337. */
  338. app.post('/restart', (req, res) => {
  339. res.status(204).end()
  340. server.destroy(() => {
  341. spinner.text = 'Setup wizard terminated. Restarting in normal mode...'
  342. _.delay(() => {
  343. const exec = require('execa')
  344. exec.stdout('node', ['wiki', 'start']).then(result => {
  345. spinner.succeed('Wiki.js is now running in normal mode!')
  346. process.exit(0)
  347. })
  348. }, 1000)
  349. })
  350. })
  351. // ----------------------------------------
  352. // Error handling
  353. // ----------------------------------------
  354. app.use(function (req, res, next) {
  355. var err = new Error('Not Found')
  356. err.status = 404
  357. next(err)
  358. })
  359. app.use(function (err, req, res, next) {
  360. res.status(err.status || 500)
  361. res.send({
  362. message: err.message,
  363. error: IS_DEBUG ? err : {}
  364. })
  365. spinner.fail(err.message)
  366. process.exit(1)
  367. })
  368. // ----------------------------------------
  369. // Start HTTP server
  370. // ----------------------------------------
  371. spinner.text = 'Starting HTTP server...'
  372. app.set('port', port)
  373. server = http.createServer(app)
  374. server.listen(port)
  375. var openConnections = []
  376. server.on('connection', (conn) => {
  377. let key = conn.remoteAddress + ':' + conn.remotePort
  378. openConnections[key] = conn
  379. conn.on('close', () => {
  380. delete openConnections[key]
  381. })
  382. })
  383. server.destroy = (cb) => {
  384. server.close(cb)
  385. for (let key in openConnections) {
  386. openConnections[key].destroy()
  387. }
  388. }
  389. server.on('error', (error) => {
  390. if (error.syscall !== 'listen') {
  391. throw error
  392. }
  393. switch (error.code) {
  394. case 'EACCES':
  395. spinner.fail('Listening on port ' + port + ' requires elevated privileges!')
  396. return process.exit(1)
  397. case 'EADDRINUSE':
  398. spinner.fail('Port ' + port + ' is already in use!')
  399. return process.exit(1)
  400. default:
  401. throw error
  402. }
  403. })
  404. server.on('listening', () => {
  405. spinner.text = 'Browse to http://localhost:' + port + ' to configure Wiki.js!'
  406. })
  407. }