Comparing version 0.1.0-alpha.6 to 0.1.0-alpha.7
class MetacomError extends Error { | ||
constructor(message, code) { | ||
constructor({ message, code }) { | ||
super(message); | ||
@@ -55,5 +55,3 @@ this.code = code; | ||
if (packet.error) { | ||
const { message, code } = packet.error; | ||
const error = new MetacomError(message, code); | ||
reject(error); | ||
reject(new MetacomError(packet.error)); | ||
return; | ||
@@ -111,3 +109,8 @@ } | ||
const { status } = res; | ||
if (status === 200) return res.json().then(({ result }) => result); | ||
if (status === 200) { | ||
return res.json().then(packet => { | ||
if (packet.error) throw new MetacomError(packet.error); | ||
return packet.result; | ||
}); | ||
} | ||
throw new Error(`Status Code: ${status}`); | ||
@@ -114,0 +117,0 @@ }); |
'use strict'; | ||
const http = require('http'); | ||
const path = require('path'); | ||
const https = require('https'); | ||
const transport = { http, https }; | ||
const WebSocket = require('ws'); | ||
const MIME_TYPES = { | ||
html: 'text/html; charset=UTF-8', | ||
json: 'application/json; charset=UTF-8', | ||
js: 'application/javascript; charset=UTF-8', | ||
css: 'text/css', | ||
png: 'image/png', | ||
ico: 'image/x-icon', | ||
svg: 'image/svg+xml', | ||
}; | ||
class MetacomError extends Error { | ||
constructor({ message, code }) { | ||
super(message); | ||
this.code = code; | ||
} | ||
} | ||
const HEADERS = { | ||
'X-XSS-Protection': '1; mode=block', | ||
'X-Content-Type-Options': 'nosniff', | ||
'Strict-Transport-Security': 'max-age=31536000; includeSubdomains; preload', | ||
'Access-Control-Allow-Origin': '*', | ||
const fetch = (url, options) => { | ||
const dest = new URL(url); | ||
return new Promise((resolve, reject) => { | ||
const protocol = transport[dest.protocol.slice(0, -1)]; | ||
const req = protocol.request(url, options, async res => { | ||
const buffers = []; | ||
for await (const chunk of res) { | ||
buffers.push(chunk); | ||
} | ||
resolve(Buffer.concat(buffers).toString()); | ||
}); | ||
req.on('error', reject); | ||
req.write(options.body); | ||
req.end(); | ||
}); | ||
}; | ||
class Client { | ||
constructor(req, res, connection, application) { | ||
this.req = req; | ||
this.res = res; | ||
this.ip = req.socket.remoteAddress; | ||
this.connection = connection; | ||
this.application = application; | ||
module.exports = class Metacom { | ||
constructor(url) { | ||
this.url = url; | ||
this.socket = new WebSocket(url); | ||
this.api = {}; | ||
this.callId = 0; | ||
this.calls = new Map(); | ||
this.socket.addEventListener('message', ({ data }) => { | ||
try { | ||
const packet = JSON.parse(data); | ||
const { callback, event } = packet; | ||
const callId = callback || event; | ||
const promised = this.calls.get(callId); | ||
if (!promised) return; | ||
const [resolve, reject] = promised; | ||
if (packet.error) { | ||
reject(new MetacomError(packet.error)); | ||
return; | ||
} | ||
resolve(packet.result); | ||
} catch (err) { | ||
console.error(err); | ||
} | ||
}); | ||
} | ||
static() { | ||
const { req, res, ip, application } = this; | ||
const { url, method } = req; | ||
const filePath = url === '/' ? '/index.html' : url; | ||
const fileExt = path.extname(filePath).substring(1); | ||
const mimeType = MIME_TYPES[fileExt] || MIME_TYPES.html; | ||
res.writeHead(200, { ...HEADERS, 'Content-Type': mimeType }); | ||
if (res.writableEnded) return; | ||
const data = application.getStaticFile(filePath); | ||
if (data) { | ||
res.end(data); | ||
application.logger.access(`${ip}\t${method}\t${url}`); | ||
return; | ||
} | ||
this.error(404); | ||
ready() { | ||
return new Promise(resolve => { | ||
if (this.socket.readyState === WebSocket.OPEN) resolve(); | ||
else this.socket.addEventListener('open', resolve); | ||
}); | ||
} | ||
redirect(location) { | ||
const { res } = this; | ||
if (res.headersSent) return; | ||
res.writeHead(302, { Location: location }); | ||
res.end(); | ||
} | ||
error(code, err, callId = err) { | ||
const { req, res, connection, ip, application } = this; | ||
const { url, method } = req; | ||
const status = http.STATUS_CODES[code]; | ||
if (typeof err === 'number') err = undefined; | ||
const reason = err ? err.stack : status; | ||
application.logger.error(`${ip}\t${method}\t${url}\t${code}\t${reason}`); | ||
if (connection) { | ||
const packet = { callback: callId, error: { code, message: status } }; | ||
connection.send(JSON.stringify(packet)); | ||
return; | ||
async load(...interfaces) { | ||
const introspect = this.httpCall('system')('introspect'); | ||
const introspection = await introspect(interfaces); | ||
const available = Object.keys(introspection); | ||
for (const interfaceName of interfaces) { | ||
if (!available.includes(interfaceName)) continue; | ||
const methods = {}; | ||
const iface = introspection[interfaceName]; | ||
const request = this.socketCall(interfaceName); | ||
const methodNames = Object.keys(iface); | ||
for (const methodName of methodNames) { | ||
methods[methodName] = request(methodName); | ||
} | ||
this.api[interfaceName] = methods; | ||
} | ||
if (res.writableEnded) return; | ||
res.writeHead(status, { 'Content-Type': MIME_TYPES.json }); | ||
const packet = { code, error: status }; | ||
res.end(JSON.stringify(packet)); | ||
} | ||
message(data) { | ||
let packet; | ||
try { | ||
packet = JSON.parse(data); | ||
} catch (err) { | ||
this.error(500, new Error('JSON parsing error')); | ||
return; | ||
} | ||
const [callType, target] = Object.keys(packet); | ||
const callId = packet[callType]; | ||
const args = packet[target]; | ||
if (callId && args) { | ||
const [interfaceName, methodName] = target.split('/'); | ||
this.rpc(callId, interfaceName, methodName, args); | ||
return; | ||
} | ||
this.error(500, new Error('Packet structure error')); | ||
httpCall(iname, ver) { | ||
return methodName => (args = {}) => { | ||
const callId = ++this.callId; | ||
const interfaceName = ver ? `${iname}.${ver}` : iname; | ||
const target = interfaceName + '/' + methodName; | ||
const packet = { call: callId, [target]: args }; | ||
const dest = new URL(this.url); | ||
const protocol = dest.protocol === 'ws:' ? 'http' : 'https'; | ||
const url = `${protocol}://${dest.host}/api`; | ||
return fetch(url, { | ||
method: 'POST', | ||
headers: { 'Content-Type': 'application/json' }, | ||
body: JSON.stringify(packet), | ||
}).then(json => { | ||
const packet = JSON.parse(json); | ||
if (packet.error) throw new MetacomError(packet.error); | ||
return packet.result; | ||
}); | ||
}; | ||
} | ||
async rpc(callId, interfaceName, methodName, args) { | ||
const { res, connection, ip, application } = this; | ||
const { semaphore } = application.server; | ||
try { | ||
await semaphore.enter(); | ||
} catch { | ||
this.error(504, callId); | ||
return; | ||
} | ||
const [iname, ver = '*'] = interfaceName.split('.'); | ||
try { | ||
let session = await application.auth.restore(this); | ||
const proc = application.getMethod(iname, ver, methodName, session); | ||
if (!proc) { | ||
this.error(404, callId); | ||
return; | ||
} | ||
if (!session && proc.access !== 'public') { | ||
this.error(403, callId); | ||
return; | ||
} | ||
const result = await proc.method(args); | ||
const userId = result ? result.userId : undefined; | ||
if (!session && userId && proc.access === 'public') { | ||
session = application.auth.start(this, userId); | ||
result.token = session.token; | ||
} | ||
const data = JSON.stringify({ callback: callId, result }); | ||
if (connection) connection.send(data); | ||
else res.end(data); | ||
const token = session ? session.token : 'anonymous'; | ||
const record = `${ip}\t${token}\t${interfaceName}/${methodName}`; | ||
application.logger.access(record); | ||
} catch (err) { | ||
this.error(500, err, callId); | ||
} finally { | ||
semaphore.leave(); | ||
} | ||
socketCall(iname, ver) { | ||
return methodName => async (args = {}) => { | ||
const callId = ++this.callId; | ||
const interfaceName = ver ? `${iname}.${ver}` : iname; | ||
const target = interfaceName + '/' + methodName; | ||
await this.ready(); | ||
return new Promise((resolve, reject) => { | ||
this.calls.set(callId, [resolve, reject]); | ||
const packet = { call: callId, [target]: args }; | ||
this.socket.send(JSON.stringify(packet)); | ||
}); | ||
}; | ||
} | ||
} | ||
module.exports = Client; | ||
}; |
@@ -11,3 +11,3 @@ 'use strict'; | ||
const Semaphore = require('./semaphore.js'); | ||
const Client = require('./client.js'); | ||
const Channel = require('./channel.js'); | ||
@@ -36,3 +36,3 @@ const SHUTDOWN_TIMEOUT = 5000; | ||
this.application = application; | ||
this.clients = new Map(); | ||
this.channels = new Map(); | ||
const { ports, host, concurrency, queue } = config; | ||
@@ -48,5 +48,5 @@ this.semaphore = new Semaphore(concurrency, queue.size, queue.timeout); | ||
this.ws.on('connection', (connection, req) => { | ||
const client = new Client(req, null, connection, application); | ||
const channel = new Channel(req, null, connection, application); | ||
connection.on('message', data => { | ||
client.message(data); | ||
channel.message(data); | ||
}); | ||
@@ -58,7 +58,7 @@ }); | ||
listener(req, res) { | ||
const { clients } = this; | ||
const { channels } = this; | ||
let finished = false; | ||
const { method, url, connection } = req; | ||
const client = new Client(req, res, null, this.application); | ||
clients.set(connection, client); | ||
const channel = new Channel(req, res, null, this.application); | ||
channels.set(connection, channel); | ||
@@ -68,4 +68,4 @@ const timer = setTimeout(() => { | ||
finished = true; | ||
clients.delete(connection); | ||
client.error(504); | ||
channels.delete(connection); | ||
channel.error(504); | ||
}, LONG_RESPONSE); | ||
@@ -77,3 +77,3 @@ | ||
clearTimeout(timer); | ||
clients.delete(connection); | ||
channels.delete(connection); | ||
}); | ||
@@ -83,3 +83,3 @@ | ||
if (method !== 'POST') { | ||
client.error(403); | ||
channel.error(403); | ||
return; | ||
@@ -89,6 +89,6 @@ } | ||
data => { | ||
client.message(data); | ||
channel.message(data); | ||
}, | ||
err => { | ||
client.error(500, err); | ||
channel.error(500, err); | ||
} | ||
@@ -100,13 +100,13 @@ ); | ||
const port = sample(this.ports); | ||
client.redirect(`https://${host}:${port}/`); | ||
channel.redirect(`https://${host}:${port}/`); | ||
} | ||
client.static(); | ||
channel.static(); | ||
} | ||
} | ||
closeClients() { | ||
const { clients } = this; | ||
for (const [connection, client] of clients.entries()) { | ||
clients.delete(connection); | ||
client.error(503); | ||
closeChannels() { | ||
const { channels } = this; | ||
for (const [connection, channel] of channels.entries()) { | ||
channels.delete(connection); | ||
channel.error(503); | ||
connection.destroy(); | ||
@@ -121,3 +121,3 @@ } | ||
await timeout(SHUTDOWN_TIMEOUT); | ||
this.closeClients(); | ||
this.closeChannels(); | ||
} | ||
@@ -124,0 +124,0 @@ } |
'use strict'; | ||
module.exports = require('./lib/metacom.js'); | ||
module.exports = require('./lib/client.js'); | ||
module.exports.Server = require('./lib/server.js'); |
{ | ||
"name": "metacom", | ||
"version": "0.1.0-alpha.6", | ||
"version": "0.1.0-alpha.7", | ||
"author": "Timur Shemsedinov <timur.shemsedinov@gmail.com>", | ||
@@ -48,3 +48,3 @@ "description": "Communication protocol for Metarhia stack with rpc, events, binary streams, memory and db access", | ||
"devDependencies": { | ||
"eslint": "^7.7.0", | ||
"eslint": "^7.8.1", | ||
"eslint-config-metarhia": "^7.0.1", | ||
@@ -51,0 +51,0 @@ "eslint-config-prettier": "^6.11.0", |
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
18597
502