configure.js 14 KB

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