1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 | const debug = require('debug')('huncwot:index');
|
15 |
|
16 | const http = require('http');
|
17 | const Stream = require('stream');
|
18 | const querystring = require('querystring');
|
19 | const { join } = require('path');
|
20 | const { parse } = require('url');
|
21 | const Busboy = require('busboy');
|
22 | const Router = require('trek-router');
|
23 | const httpstatus = require('http-status');
|
24 |
|
25 | const { serve, security } = require('./middleware');
|
26 | const { build, translate } = require('./controller');
|
27 | const { NotFound } = require('./response');
|
28 | const Logger = require('./logger');
|
29 | const HTMLifiedError = require('./error');
|
30 |
|
31 | const cwd = process.cwd();
|
32 | const handlerDir = join(cwd, '.build');
|
33 |
|
34 | const isObject = _ => !!_ && _.constructor === Object;
|
35 | const compose = (...functions) => args =>
|
36 | functions.reduceRight((arg, fn) => fn(arg), args);
|
37 |
|
38 | class Middleware extends Array {
|
39 | async next(context, last, current, done, called, func) {
|
40 | if ((done = current > this.length)) return;
|
41 |
|
42 | func = this[current] || last;
|
43 |
|
44 | return (
|
45 | func &&
|
46 | func(context, async () => {
|
47 | if (called) throw new Error('next() already called');
|
48 | called = true;
|
49 | return await this.next(context, last, current + 1);
|
50 | })
|
51 | );
|
52 | }
|
53 |
|
54 | async compose(context, last) {
|
55 | return await this.next(context, last, 0);
|
56 | }
|
57 | }
|
58 |
|
59 | class Huncwot {
|
60 | constructor({
|
61 | staticDir = join(cwd, 'static'),
|
62 | securityOptions = {
|
63 | dnsPrefetchControl: false,
|
64 | poweredBy: false
|
65 | },
|
66 | graphql = false,
|
67 | implicitControllers = true,
|
68 | _verbose = false
|
69 | } = {}) {
|
70 | this.middlewareList = new Middleware();
|
71 | this.router = new Router();
|
72 |
|
73 | if (graphql) {
|
74 | try {
|
75 | const { typeDefs, resolvers } = require(join(cwd, 'graphql'));
|
76 | const { graphql, graphiql, makeSchema } = require('./graphql');
|
77 |
|
78 | const schema = makeSchema({ typeDefs, resolvers });
|
79 |
|
80 | this.post('/graphql', graphql({ schema }));
|
81 | this.get('/graphql', graphql({ schema }));
|
82 | this.get('/graphiql', graphiql({ endpointURL: 'graphql' }));
|
83 | } catch (error) {
|
84 | switch (error.code) {
|
85 | case 'MODULE_NOT_FOUND':
|
86 | console.log('GraphQL is not set up.');
|
87 | break;
|
88 | default:
|
89 | console.error(error);
|
90 | break;
|
91 | }
|
92 | }
|
93 | }
|
94 |
|
95 | if (implicitControllers) {
|
96 | const handlers = build();
|
97 | for (let { resource, operation, path } of handlers) {
|
98 | try {
|
99 | const handler = require(join(handlerDir, path));
|
100 | let { method, route } = translate(operation, resource);
|
101 |
|
102 | console.log('-->', method, route);
|
103 |
|
104 | if (Array.isArray(handler)) {
|
105 | this.add(method, route, ...handler);
|
106 | } else {
|
107 | this.add(method, route, handler);
|
108 | }
|
109 | } catch (error) {
|
110 | console.error(error);
|
111 | }
|
112 | }
|
113 | }
|
114 |
|
115 |
|
116 |
|
117 | this.add('GET', '/', async _request => 'Hello, Huncwot');
|
118 | }
|
119 |
|
120 | async setup() {
|
121 |
|
122 | }
|
123 |
|
124 | use(middleware) {
|
125 | if (typeof middleware !== 'function')
|
126 | throw new TypeError('middleware must be a function!');
|
127 | this.middlewareList.push(middleware);
|
128 |
|
129 | return this;
|
130 | }
|
131 |
|
132 | add(method, path, ...fns) {
|
133 | const action = fns.pop();
|
134 |
|
135 |
|
136 |
|
137 |
|
138 | const pipeline = fns.length === 0 ? action : compose(...fns)(action);
|
139 |
|
140 | this.router.add(method.toUpperCase(), path, pipeline);
|
141 |
|
142 | return this;
|
143 | }
|
144 |
|
145 | start({ routes = {}, port = 5544, fn = () => {} }) {
|
146 | for (let [method, route] of Object.entries(routes)) {
|
147 | for (let [path, handler] of Object.entries(route)) {
|
148 | if (Array.isArray(handler)) {
|
149 | this.add(method, path, ...handler);
|
150 | } else {
|
151 | this.add(method, path, handler);
|
152 | }
|
153 | }
|
154 | }
|
155 |
|
156 | const RouterMiddleware = async (context, next) => {
|
157 | const method = context.request.method;
|
158 | const { pathname, query } = parse(context.request.url, true);
|
159 |
|
160 | const [handler, dynamicRoutes] = this.router.find(method, pathname);
|
161 |
|
162 | const params = {};
|
163 | for (let r of dynamicRoutes) {
|
164 | params[r.name] = r.value;
|
165 | }
|
166 |
|
167 | if (handler !== undefined) {
|
168 | await handleRequest(context);
|
169 | context.params = { ...context.params, ...query, ...params };
|
170 | return await handler(context);
|
171 | } else {
|
172 | return await next();
|
173 | }
|
174 | };
|
175 |
|
176 | this.middlewareList.push(RouterMiddleware);
|
177 |
|
178 |
|
179 |
|
180 | this.middlewareList.push(() => NotFound());
|
181 |
|
182 | const server = http.createServer((request, response) => {
|
183 | const context = { params: {}, headers: {}, request, response };
|
184 |
|
185 | this.middlewareList
|
186 | .compose(context)
|
187 | .then(handle(context))
|
188 | .then(() => Logger.printRequestResponse(context))
|
189 | .catch(error => {
|
190 | response.statusCode = 500;
|
191 | error.status = `500 ${httpstatus[500]}`;
|
192 |
|
193 |
|
194 | Logger.printRequestResponse(context);
|
195 | Logger.printError(error, 'HTTP');
|
196 |
|
197 | const htmlifiedError = new HTMLifiedError(error, request);
|
198 |
|
199 | htmlifiedError.generate().then(html => {
|
200 | response.writeHead(500, { 'Content-Type': 'text/html' }).end(html);
|
201 | });
|
202 | });
|
203 | }).on('error', error => {
|
204 | Logger.printError(error);
|
205 | process.exit(1);
|
206 | });
|
207 |
|
208 | return server.listen(port, fn);
|
209 | }
|
210 | }
|
211 |
|
212 | const handle = context => result => {
|
213 | if (null === result || undefined === result)
|
214 | throw new Error('No return statement in the handler');
|
215 |
|
216 | let { response } = context;
|
217 |
|
218 | let body, headers, type, encoding;
|
219 |
|
220 | if (typeof result === 'string' || result instanceof Stream) {
|
221 | body = result;
|
222 | } else {
|
223 | body = result.body;
|
224 | headers = result.headers;
|
225 | type = result.type;
|
226 | encoding = result.encoding;
|
227 | }
|
228 |
|
229 | response.statusCode = result.statusCode || 200;
|
230 |
|
231 | for (var key in headers) {
|
232 | response.setHeader(key, headers[key]);
|
233 | }
|
234 |
|
235 | if (encoding) response.setHeader('Content-Encoding', encoding);
|
236 |
|
237 | if (Buffer.isBuffer(body)) {
|
238 | response.setHeader('Content-Type', type || 'application/octet-stream');
|
239 | response.setHeader('Content-Length', body.length);
|
240 | response.end(body);
|
241 | return;
|
242 | }
|
243 |
|
244 | if (body instanceof Stream) {
|
245 | if (!response.getHeader('Content-Type'))
|
246 | response.setHeader('Content-Type', type || 'text/html');
|
247 |
|
248 | body.pipe(response);
|
249 | return;
|
250 | }
|
251 |
|
252 | let str = body;
|
253 |
|
254 | if (typeof body === 'object' || typeof body === 'number') {
|
255 | str = JSON.stringify(body);
|
256 | response.setHeader('Content-Type', 'application/json');
|
257 | } else {
|
258 | if (!response.getHeader('Content-Type'))
|
259 | response.setHeader('Content-Type', type || 'text/plain');
|
260 | }
|
261 |
|
262 | response.setHeader('Content-Length', Buffer.byteLength(str));
|
263 | response.end(str);
|
264 | };
|
265 |
|
266 | const handleRequest = async context => {
|
267 | const { headers } = context.request;
|
268 |
|
269 | context.headers = headers;
|
270 | context.cookies = parseCookies(headers.cookie);
|
271 |
|
272 | const buffer = await streamToString(context.request);
|
273 | if (buffer.length > 0) {
|
274 | const contentType = headers['content-type'].split(';')[0];
|
275 |
|
276 | switch (contentType) {
|
277 | case 'application/x-www-form-urlencoded':
|
278 | Object.assign(context.params, querystring.parse(buffer));
|
279 | break;
|
280 | case 'application/json': {
|
281 | const result = JSON.parse(buffer);
|
282 | if (isObject(result)) {
|
283 | Object.assign(context.params, result);
|
284 | }
|
285 | break;
|
286 | }
|
287 | case 'multipart/form-data': {
|
288 | context.files = {};
|
289 |
|
290 | const busboy = new Busboy({ headers });
|
291 |
|
292 | busboy.on('file', (fieldname, file, filename, encoding, mimetype) => {
|
293 | file.on('data', data => {
|
294 | context.files = {
|
295 | ...context.files,
|
296 | [fieldname]: {
|
297 | name: filename,
|
298 | length: data.length,
|
299 | data,
|
300 | encoding,
|
301 | mimetype
|
302 | }
|
303 | };
|
304 | });
|
305 | file.on('end', () => {});
|
306 | });
|
307 | busboy.on('field', (fieldname, val) => {
|
308 | context.params = { ...context.params, [fieldname]: val };
|
309 | });
|
310 | busboy.end(buffer);
|
311 |
|
312 | await new Promise(resolve => busboy.on('finish', resolve));
|
313 |
|
314 | break;
|
315 | }
|
316 | default:
|
317 | }
|
318 | }
|
319 | };
|
320 |
|
321 | const streamToString = async stream => {
|
322 | let chunks = '';
|
323 |
|
324 | return new Promise((resolve, reject) => {
|
325 | stream.on('data', chunk => (chunks += chunk));
|
326 | stream.on('error', reject);
|
327 | stream.on('end', () => resolve(chunks));
|
328 | });
|
329 | };
|
330 |
|
331 | const parseCookies = (cookieHeader = '') => {
|
332 | const cookies = cookieHeader.split(/; */);
|
333 | const decode = decodeURIComponent;
|
334 |
|
335 | if (cookies[0] === '') return {};
|
336 |
|
337 | const result = {};
|
338 | for (let cookie of cookies) {
|
339 | const isKeyValue = cookie.includes('=');
|
340 |
|
341 | if (!isKeyValue) {
|
342 | result[cookie.trim()] = true;
|
343 | continue;
|
344 | }
|
345 |
|
346 | let [key, value] = cookie.split('=');
|
347 |
|
348 | key.trim();
|
349 | value.trim();
|
350 |
|
351 | if ('"' === value[0]) value = value.slice(1, -1);
|
352 |
|
353 | try {
|
354 | value = decode(value);
|
355 | } catch (error) {
|
356 |
|
357 | }
|
358 |
|
359 | result[key] = value;
|
360 | }
|
361 |
|
362 | return result;
|
363 | };
|
364 |
|
365 | module.exports = Huncwot;
|