Comparing version 2.0.1 to 2.0.2
@@ -5,2 +5,10 @@ # Changelog | ||
## [2.0.2][] - 2021-09-11 | ||
- Rework Channel and Server | ||
- Decompose Channel to WsChannel and HttpChannel | ||
- Move event handlers from Server to WsChannel and HttpChannel | ||
- Return after error to avoid double reply and logging | ||
- Update typings | ||
## [2.0.1][] - 2021-09-03 | ||
@@ -146,3 +154,4 @@ | ||
[unreleased]: https://github.com/metarhia/metacom/compare/v2.0.1...HEAD | ||
[unreleased]: https://github.com/metarhia/metacom/compare/v2.0.2...HEAD | ||
[2.0.2]: https://github.com/metarhia/metacom/compare/v2.0.1...v2.0.2 | ||
[2.0.1]: https://github.com/metarhia/metacom/compare/v2.0.0...v2.0.1 | ||
@@ -149,0 +158,0 @@ [2.0.0]: https://github.com/metarhia/metacom/compare/v1.8.1...v2.0.0 |
'use strict'; | ||
const http = require('http'); | ||
const metautil = require('metautil'); | ||
const transport = require('./transport.js'); | ||
const { MIME_TYPES, HEADERS } = transport.http; | ||
const { COOKIE_DELETE, COOKIE_HOST, TOKEN } = transport.http; | ||
@@ -59,3 +55,3 @@ const EMPTY_PACKET = Buffer.from('{}'); | ||
} | ||
channel.connection.send(JSON.stringify(packet)); | ||
channel.send(packet); | ||
} | ||
@@ -81,11 +77,9 @@ | ||
class Channel { | ||
constructor(req, res, connection, application) { | ||
constructor(application, req, res) { | ||
this.application = application; | ||
this.req = req; | ||
this.res = res; | ||
this.ip = req.socket.remoteAddress; | ||
this.connection = connection; | ||
this.application = application; | ||
const client = new Client(); | ||
this.client = client; | ||
channels.set(client, this); | ||
this.client = new Client(); | ||
channels.set(this.client, this); | ||
this.session = null; | ||
@@ -96,45 +90,6 @@ this.restoreSession(); | ||
get token() { | ||
if (this.session === null) return 'anonymous'; | ||
if (this.session === null) return ''; | ||
return this.session.token; | ||
} | ||
redirect(location) { | ||
const { res } = this; | ||
if (res.headersSent) return; | ||
res.writeHead(302, { Location: location, ...HEADERS }); | ||
res.end(); | ||
} | ||
send(ext, data) { | ||
const { res } = this; | ||
const mimeType = MIME_TYPES[ext] || MIME_TYPES.html; | ||
res.writeHead(200, { ...HEADERS, 'Content-Type': mimeType }); | ||
res.end(data); | ||
} | ||
options() { | ||
const { res } = this; | ||
if (res.headersSent) return; | ||
res.writeHead(200, HEADERS); | ||
res.end(); | ||
} | ||
error(code, err = null, callId) { | ||
const { req, res, connection, ip, application } = this; | ||
const { url, method } = req; | ||
const status = http.STATUS_CODES[code]; | ||
const reason = err !== null ? err.stack : status; | ||
application.console.error(`${ip}\t${method}\t${url}\t${code}\t${reason}`); | ||
const message = status || err.message; | ||
const error = { message, code }; | ||
if (connection) { | ||
connection.send(JSON.stringify({ callback: callId, error })); | ||
return; | ||
} | ||
if (res.writableEnded) return; | ||
const httpCode = status ? code : 500; | ||
res.writeHead(httpCode, { 'Content-Type': MIME_TYPES.json, ...HEADERS }); | ||
res.end(JSON.stringify({ error })); | ||
} | ||
message(data) { | ||
@@ -188,29 +143,11 @@ if (Buffer.compare(EMPTY_PACKET, data) === 0) { | ||
this.error(code, err, callId); | ||
return; | ||
} finally { | ||
proc.leave(); | ||
} | ||
this.reply(callId, result); | ||
const record = `${this.ip}\t${interfaceName}/${methodName}`; | ||
application.console.log(record); | ||
} | ||
async hook(proc, interfaceName, methodName, args) { | ||
const { application, client, res } = this; | ||
const callId = -1; | ||
if (!proc) { | ||
this.error(404, null, callId); | ||
if (typeof result === 'object' && result.constructor.name === 'Error') { | ||
this.error(result.code, result, callId); | ||
return; | ||
} | ||
const context = { client }; | ||
let result = null; | ||
try { | ||
result = await proc.invoke(context, { method: methodName, args }); | ||
} catch (err) { | ||
this.error(500, err, callId); | ||
} | ||
const data = JSON.stringify(result); | ||
if (!res.writableEnded) { | ||
res.writeHead(200, { 'Content-Type': MIME_TYPES.json, ...HEADERS }); | ||
res.end(data); | ||
} | ||
this.send({ callback: callId, result }); | ||
const record = `${this.ip}\t${interfaceName}/${methodName}`; | ||
@@ -220,26 +157,2 @@ application.console.log(record); | ||
reply(callId, result) { | ||
const { res, connection } = this; | ||
if (typeof result === 'object' && result.constructor.name === 'Error') { | ||
this.error(result.code, result, callId); | ||
return; | ||
} | ||
const data = JSON.stringify({ callback: callId, result }); | ||
if (connection) { | ||
connection.send(data); | ||
} else { | ||
res.writeHead(200, { 'Content-Type': MIME_TYPES.json, ...HEADERS }); | ||
res.end(data); | ||
} | ||
} | ||
startSession() { | ||
const token = this.application.auth.generateToken(); | ||
const host = metautil.parseHost(this.req.headers.host); | ||
const cookie = `${TOKEN}=${token}; ${COOKIE_HOST}=${host}`; | ||
const session = this.application.auth.startSession(); | ||
if (this.res) this.res.setHeader('Set-Cookie', cookie); | ||
return session; | ||
} | ||
async restoreSession() { | ||
@@ -256,10 +169,2 @@ const { cookie } = this.req.headers; | ||
deleteSession() { | ||
const { token } = this; | ||
if (token === 'anonymous') return; | ||
const host = metautil.parseHost(this.req.headers.host); | ||
this.res.setHeader('Set-Cookie', COOKIE_DELETE + host); | ||
this.application.auth.deleteSession(token); | ||
} | ||
destroy() { | ||
@@ -266,0 +171,0 @@ channels.delete(this.client); |
'use strict'; | ||
const http = require('http'); | ||
const metautil = require('metautil'); | ||
const { Channel } = require('./channel.js'); | ||
const MIME_TYPES = { | ||
@@ -29,2 +33,91 @@ html: 'text/html; charset=UTF-8', | ||
module.exports = { MIME_TYPES, HEADERS, COOKIE_DELETE, COOKIE_HOST, TOKEN }; | ||
class HttpChannel extends Channel { | ||
constructor(application, req, res) { | ||
super(application, req, res); | ||
res.on('close', () => { | ||
this.destroy(); | ||
}); | ||
} | ||
write(data, httpCode = 200, ext = 'json') { | ||
const { res } = this; | ||
if (res.writableEnded) return; | ||
const mimeType = MIME_TYPES[ext] || MIME_TYPES.html; | ||
res.writeHead(httpCode, { ...HEADERS, 'Content-Type': mimeType }); | ||
res.end(data); | ||
} | ||
send(obj, httpCode = 200) { | ||
const data = JSON.stringify(obj); | ||
this.write(data, httpCode, 'json'); | ||
} | ||
error(code, err = null, callId) { | ||
const { req, ip, application } = this; | ||
const { url, method } = req; | ||
const status = http.STATUS_CODES[code]; | ||
const reason = err ? err.stack : status; | ||
application.console.error(`${ip}\t${method}\t${url}\t${code}\t${reason}`); | ||
const message = status || 'Unknown error'; | ||
const httpCode = status ? code : 500; | ||
const error = { message, code }; | ||
if (callId) this.send({ callback: callId, error }); | ||
else this.send({ error }, httpCode); | ||
} | ||
redirect(location) { | ||
const { res } = this; | ||
if (res.headersSent) return; | ||
res.writeHead(302, { Location: location, ...HEADERS }); | ||
res.end(); | ||
} | ||
options() { | ||
const { res } = this; | ||
if (res.headersSent) return; | ||
res.writeHead(200, HEADERS); | ||
res.end(); | ||
} | ||
async hook(proc, interfaceName, methodName, args) { | ||
const { application, client } = this; | ||
const callId = -1; | ||
if (!proc) { | ||
this.error(404, null, callId); | ||
return; | ||
} | ||
const context = { client }; | ||
let result = null; | ||
try { | ||
result = await proc.invoke(context, { method: methodName, args }); | ||
} catch (err) { | ||
this.error(500, err, callId); | ||
return; | ||
} | ||
this.send(result); | ||
const record = `${this.ip}\t${interfaceName}/${methodName}`; | ||
application.console.log(record); | ||
} | ||
startSession() { | ||
const token = this.application.auth.generateToken(); | ||
const host = metautil.parseHost(this.req.headers.host); | ||
const cookie = `${TOKEN}=${token}; ${COOKIE_HOST}=${host}`; | ||
const session = this.application.auth.startSession(); | ||
if (this.res) this.res.setHeader('Set-Cookie', cookie); | ||
return session; | ||
} | ||
deleteSession() { | ||
const { token } = this; | ||
if (token === 'anonymous') return; | ||
const host = metautil.parseHost(this.req.headers.host); | ||
this.res.setHeader('Set-Cookie', COOKIE_DELETE + host); | ||
this.application.auth.deleteSession(token); | ||
} | ||
} | ||
const createChannel = (application, req, res) => | ||
new HttpChannel(application, req, res); | ||
module.exports = { createChannel }; |
@@ -9,4 +9,5 @@ 'use strict'; | ||
const ws = require('ws'); | ||
const { Channel, channels } = require('./channel.js'); | ||
const { channels } = require('./channel.js'); | ||
const { serveStatic } = require('./static.js'); | ||
const transport = require('./transport.js'); | ||
@@ -46,5 +47,5 @@ const SHORT_TIMEOUT = 500; | ||
const { protocol, timeouts, nagle = true } = config; | ||
const transport = protocol === 'http' || this.balancer ? http : https; | ||
const proto = protocol === 'http' || this.balancer ? http : https; | ||
const listener = this.listener.bind(this); | ||
this.server = transport.createServer({ ...application.cert }, listener); | ||
this.server = proto.createServer({ ...application.cert }, listener); | ||
if (!nagle) { | ||
@@ -59,10 +60,4 @@ this.server.on('connection', (socket) => { | ||
this.ws = new ws.Server({ server: this.server }); | ||
this.ws.on('connection', async (connection, req) => { | ||
const channel = await new Channel(req, null, connection, application); | ||
connection.on('message', (data) => { | ||
channel.message(data); | ||
}); | ||
connection.on('close', () => { | ||
channel.destroy(); | ||
}); | ||
this.ws.on('connection', (connection, req) => { | ||
transport.ws.createChannel(application, req, connection); | ||
}); | ||
@@ -79,13 +74,5 @@ this.ws.on('error', (err) => { | ||
async listener(req, res) { | ||
let finished = false; | ||
listener(req, res) { | ||
const { url } = req; | ||
const channel = await new Channel(req, res, null, this.application); | ||
res.on('close', () => { | ||
if (finished) return; | ||
finished = true; | ||
channel.destroy(); | ||
}); | ||
const channel = transport.http.createChannel(this.application, req, res); | ||
if (this.balancer) { | ||
@@ -98,3 +85,2 @@ const host = metautil.parseHost(req.headers.host); | ||
} | ||
if (url.startsWith('/api')) this.request(channel); | ||
@@ -101,0 +87,0 @@ else serveStatic(channel); |
@@ -6,10 +6,9 @@ 'use strict'; | ||
const serveStatic = (channel) => { | ||
const { req, res, ip, application } = channel; | ||
const { req, res, application } = channel; | ||
if (res.writableEnded) return; | ||
const { url, method } = req; | ||
application.console.log(`${ip}\t${method}\t${url}`); | ||
const { url } = req; | ||
const filePath = url === '/' ? '/index.html' : url; | ||
const fileExt = path.extname(filePath).substring(1); | ||
const data = application.getStaticFile(filePath); | ||
if (data) channel.send(fileExt, data); | ||
if (data) channel.write(data, 200, fileExt); | ||
else channel.error(404); | ||
@@ -16,0 +15,0 @@ }; |
@@ -5,2 +5,3 @@ 'use strict'; | ||
http: require('./http.js'), | ||
ws: require('./ws.js'), | ||
}; |
import { EventEmitter } from 'events'; | ||
import { ClientRequest, ServerResponse } from 'http'; | ||
import WebSocket from 'ws'; | ||
import { Semaphore } from 'metautil'; | ||
@@ -26,1 +29,84 @@ export interface MetacomError extends Error { | ||
} | ||
export interface ServerConfig { | ||
concurrency: number; | ||
host: string; | ||
balancer: boolean; | ||
protocol: string; | ||
ports: Array<number>; | ||
queue: object; | ||
} | ||
export class Session { | ||
constructor(token: string, channel: Channel, data: any); | ||
} | ||
export class Channel { | ||
application: object; | ||
req: ClientRequest; | ||
res: ServerResponse; | ||
ip: string; | ||
client: Metacom; | ||
session?: Session; | ||
constructor(application: object, req: ClientRequest, res: ServerResponse); | ||
message(data: string): void; | ||
prc( | ||
callId: number, | ||
interfaceName: string, | ||
methodName: string, | ||
args: [] | ||
): Promise<void>; | ||
restoreSession(): Promise<Session | null>; | ||
destroy(): void; | ||
error(code: number, err?: boolean, callId?: number): void; | ||
} | ||
export class HttpChannel extends Channel { | ||
write(data: any, httpCode?: number, ext?: string): void; | ||
send(obj: object, httpCode?: number): void; | ||
redirect(location: string): void; | ||
options(): void; | ||
hook( | ||
proc: object, | ||
interfaceName: string, | ||
methodName: string, | ||
args: Array<any> | ||
): Promise<void>; | ||
startSession(): Session; | ||
deleteSession(): void; | ||
} | ||
export class WsChannel extends Channel { | ||
connection: WebSocket; | ||
constructor(application: object, req: ClientRequest, connection: WebSocket); | ||
write(data: any): void; | ||
send(obj: object): void; | ||
redirect(location: string): void; | ||
options(): void; | ||
hook( | ||
proc: object, | ||
interfaceName: string, | ||
methodName: string, | ||
args: Array<any> | ||
): Promise<void>; | ||
startSession(): Session; | ||
deleteSession(): void; | ||
} | ||
export class Server { | ||
config: ServerConfig; | ||
application: object; | ||
semaphore: Semaphore; | ||
balancer: boolean; | ||
port: number; | ||
server?: any; | ||
ws?: any; | ||
protocol: string; | ||
host: string; | ||
constructor(config: ServerConfig, application: object); | ||
bind(): void; | ||
listener(req: ClientRequest, res: ServerResponse): void; | ||
request(channel: Channel): void; | ||
closeChannels(): void; | ||
close(): Promise<void>; | ||
} |
{ | ||
"name": "metacom", | ||
"version": "2.0.1", | ||
"version": "2.0.2", | ||
"author": "Timur Shemsedinov <timur.shemsedinov@gmail.com>", | ||
@@ -56,7 +56,7 @@ "description": "Communication protocol for Metarhia stack with rpc, events, binary streams, memory and db access", | ||
"dependencies": { | ||
"metautil": "^3.5.9", | ||
"ws": "^8.2.0" | ||
"metautil": "^3.5.11", | ||
"ws": "^8.2.2" | ||
}, | ||
"devDependencies": { | ||
"@types/node": "^16.6.2", | ||
"@types/node": "^16.9.1", | ||
"@types/ws": "^7.4.7", | ||
@@ -66,8 +66,8 @@ "eslint": "^7.31.0", | ||
"eslint-config-prettier": "^8.3.0", | ||
"eslint-plugin-import": "^2.24.0", | ||
"eslint-plugin-prettier": "^3.4.0", | ||
"eslint-plugin-import": "^2.24.2", | ||
"eslint-plugin-prettier": "^4.0.0", | ||
"metatests": "^0.7.2", | ||
"prettier": "^2.3.2", | ||
"typescript": "^4.3.5" | ||
"prettier": "^2.4.0", | ||
"typescript": "^4.4.3" | ||
} | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
38067
16
920
8
Updatedmetautil@^3.5.11
Updatedws@^8.2.2