1 | 'use strict'
|
2 |
|
3 | const fs = require('fs')
|
4 | const path = require('path')
|
5 | const util = require('util')
|
6 | const IConfiguration = require('./iconfiguration')
|
7 | const { check } = require('./mapping')
|
8 | const checkMethod = require('./checkMethod')
|
9 | const { parse } = require('./schema')
|
10 | const {
|
11 | $configurationInterface,
|
12 | $configurationRequests,
|
13 | $handlerMethod,
|
14 | $handlerSchema
|
15 | } = require('./symbols')
|
16 |
|
17 | const readFileAsync = util.promisify(fs.readFile)
|
18 | const statAsync = util.promisify(fs.stat)
|
19 |
|
20 | const defaultHandlers = {
|
21 | custom: require('./handlers/custom'),
|
22 | file: require('./handlers/file'),
|
23 | status: require('./handlers/status'),
|
24 | url: require('./handlers/url'),
|
25 | use: require('./handlers/use')
|
26 | }
|
27 |
|
28 | const defaults = {
|
29 | hostname: undefined,
|
30 | port: 5000,
|
31 | 'max-redirect': 10,
|
32 | listeners: [],
|
33 | mappings: [{
|
34 | match: /^\/proxy\/(https?)\/(.*)/,
|
35 | url: '$1://$2',
|
36 | 'unsecure-cookies': true
|
37 | }, {
|
38 | match: '(.*)',
|
39 | file: './$1'
|
40 | }]
|
41 | }
|
42 |
|
43 | function applyDefaults (configuration) {
|
44 | Object.keys(defaults).forEach(property => {
|
45 | if (!Object.prototype.hasOwnProperty.call(configuration, property)) {
|
46 | configuration[property] = defaults[property]
|
47 | }
|
48 | })
|
49 | }
|
50 |
|
51 | function getHandler (handlers, types, mapping) {
|
52 | for (let index = 0; index < types.length; ++index) {
|
53 | const type = types[index]
|
54 | const redirect = mapping[type]
|
55 | if (redirect !== undefined) {
|
56 | return {
|
57 | handler: handlers[type],
|
58 | redirect,
|
59 | type
|
60 | }
|
61 | }
|
62 | }
|
63 | return {}
|
64 | }
|
65 |
|
66 | function checkHandler (handler, type) {
|
67 | if (handler.schema) {
|
68 | handler[$handlerSchema] = parse(handler.schema)
|
69 | delete handler.schema
|
70 | }
|
71 | if (handler.method) {
|
72 | checkMethod(handler, $handlerMethod)
|
73 | delete handler.method
|
74 | }
|
75 | if (typeof handler.redirect !== 'function') {
|
76 | throw new Error('Invalid "' + type + '" handler: redirect is not a function')
|
77 | }
|
78 | }
|
79 |
|
80 | function validateHandler (type) {
|
81 | const handlers = this.handlers
|
82 | let handler = handlers[type]
|
83 | if (typeof handler === 'string') {
|
84 | handler = require(handler)
|
85 | handlers[type] = handler
|
86 | }
|
87 | checkHandler(handler, type)
|
88 | Object.freeze(handler)
|
89 | }
|
90 |
|
91 | function setHandlers (configuration) {
|
92 | if (configuration.handlers) {
|
93 |
|
94 | configuration.handlers = Object.assign({}, configuration.handlers, defaultHandlers)
|
95 | } else {
|
96 | configuration.handlers = Object.assign({}, defaultHandlers)
|
97 | }
|
98 | Object.keys(configuration.handlers).forEach(validateHandler.bind(configuration))
|
99 | configuration.handler = getHandler.bind(null, configuration.handlers, Object.keys(configuration.handlers))
|
100 | }
|
101 |
|
102 | function invalidListeners () {
|
103 | throw new Error('Invalid listeners member, must be an array of functions')
|
104 | }
|
105 |
|
106 | function checkListeners (configuration) {
|
107 | const listeners = configuration.listeners
|
108 | if (!Array.isArray(listeners)) {
|
109 | invalidListeners()
|
110 | }
|
111 | configuration.listeners = listeners.map(register => {
|
112 | let registerType = typeof register
|
113 | if (registerType === 'string') {
|
114 | register = require(path.join(configuration.cwd || process.cwd(), register))
|
115 | registerType = typeof register
|
116 | }
|
117 | if (registerType !== 'function') {
|
118 | invalidListeners()
|
119 | }
|
120 | return register
|
121 | })
|
122 | }
|
123 |
|
124 | async function readSslFile (configuration, filePath) {
|
125 | if (path.isAbsolute(filePath)) {
|
126 | return (await readFileAsync(filePath)).toString()
|
127 | }
|
128 | return (await readFileAsync(path.join(configuration.ssl.cwd, filePath))).toString()
|
129 | }
|
130 |
|
131 | async function checkProtocol (configuration) {
|
132 | if (configuration.ssl) {
|
133 | configuration.ssl.cert = await readSslFile(configuration, configuration.ssl.cert)
|
134 | configuration.ssl.key = await readSslFile(configuration, configuration.ssl.key)
|
135 | configuration.protocol = 'https'
|
136 | } else {
|
137 | configuration.protocol = 'http'
|
138 | }
|
139 | if (![true, false, undefined].includes(configuration.http2)) {
|
140 | throw new Error('Invalid http2 setting')
|
141 | }
|
142 | if (!configuration.http2) {
|
143 | configuration.http2 = false
|
144 | }
|
145 | }
|
146 |
|
147 | async function checkMappings (configuration) {
|
148 | const configurationInterface = new IConfiguration(configuration)
|
149 | configuration[$configurationInterface] = configurationInterface
|
150 | for await (const mapping of configuration.mappings) {
|
151 | await check(configuration, mapping)
|
152 | }
|
153 | }
|
154 |
|
155 | function setCwd (folderPath, configuration) {
|
156 | if (configuration.handlers) {
|
157 | Object.keys(configuration.handlers).forEach(prefix => {
|
158 | const handler = configuration.handlers[prefix]
|
159 | if (typeof handler === 'string' && handler.match(/^\.\.?\//)) {
|
160 | configuration.handlers[prefix] = path.join(folderPath, handler)
|
161 | }
|
162 | })
|
163 | }
|
164 | if (configuration.mappings) {
|
165 | configuration.mappings.forEach(mapping => {
|
166 | if (!mapping.cwd) {
|
167 | mapping.cwd = folderPath
|
168 | }
|
169 | })
|
170 | }
|
171 | if (configuration.ssl && !configuration.ssl.cwd) {
|
172 | configuration.ssl.cwd = folderPath
|
173 | }
|
174 | configuration.cwd = folderPath
|
175 | }
|
176 |
|
177 | function extend (filePath, configuration) {
|
178 | const folderPath = path.dirname(filePath)
|
179 | setCwd(folderPath, configuration)
|
180 | if (configuration.extend) {
|
181 | const basefilePath = path.join(folderPath, configuration.extend)
|
182 | delete configuration.extend
|
183 | return readFileAsync(basefilePath)
|
184 | .then(buffer => JSON.parse(buffer.toString()))
|
185 | .then(baseConfiguration => {
|
186 |
|
187 | const baseMappings = baseConfiguration.mappings
|
188 | const mergedConfiguration = Object.assign(baseConfiguration, configuration)
|
189 | if (baseMappings && baseMappings !== mergedConfiguration.mappings) {
|
190 | mergedConfiguration.mappings = [...configuration.mappings, ...baseMappings]
|
191 | }
|
192 | return extend(basefilePath, mergedConfiguration)
|
193 | })
|
194 | }
|
195 | return configuration
|
196 | }
|
197 |
|
198 | module.exports = {
|
199 | async check (configuration) {
|
200 | if (typeof configuration !== 'object' || configuration === null) {
|
201 | throw new Error('Configuration must be an object')
|
202 | }
|
203 | const checkedConfiguration = Object.assign({}, configuration)
|
204 | applyDefaults(checkedConfiguration)
|
205 | setHandlers(checkedConfiguration)
|
206 | checkListeners(checkedConfiguration)
|
207 | await checkProtocol(checkedConfiguration)
|
208 | await checkMappings(checkedConfiguration)
|
209 | checkedConfiguration[$configurationRequests] = {
|
210 | lastId: 0,
|
211 | holding: Promise.resolve(),
|
212 | contexts: []
|
213 | }
|
214 | return checkedConfiguration
|
215 | },
|
216 |
|
217 | async read (fileName) {
|
218 | let filePath
|
219 | if (path.isAbsolute(fileName)) {
|
220 | filePath = fileName
|
221 | } else {
|
222 | filePath = path.join(process.cwd(), fileName)
|
223 | }
|
224 | return statAsync(filePath)
|
225 | .then(() => readFileAsync(filePath).then(buffer => JSON.parse(buffer.toString())))
|
226 | .then(configuration => extend(filePath, configuration))
|
227 | }
|
228 | }
|