ilp-protocol-stream
Version:
Interledger Transport Protocol for sending multiple streams of money and data over ILP.
231 lines • 9.67 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createServer = exports.Server = void 0;
const events_1 = require("events");
const IlpPacket = __importStar(require("ilp-packet"));
const ILDCP = __importStar(require("ilp-protocol-ildcp"));
const ilp_logger_1 = __importDefault(require("ilp-logger"));
const cryptoHelper = __importStar(require("./crypto"));
const pool_1 = require("./pool");
const oer_utils_1 = require("oer-utils");
const DEFAULT_DISCONNECT_DELAY = 100;
class Server extends events_1.EventEmitter {
constructor(opts) {
super();
this.pendingRequests = Promise.resolve();
this.serverSecret = opts.serverSecret || cryptoHelper.randomBytes(32);
this.plugin = opts.plugin;
this.log = (0, ilp_logger_1.default)('ilp-protocol-stream:Server');
this.connectionOpts = Object.assign({}, opts, {
serverSecret: undefined,
});
this.disconnectDelay = opts.disconnectDelay || DEFAULT_DISCONNECT_DELAY;
}
get connected() {
return Boolean(this.pool);
}
async listen() {
if (this.connected && this.plugin.isConnected()) {
return;
}
this.plugin.registerDataHandler((data) => {
this.emit('_incoming_prepare');
const request = this.handleData(data);
this.pendingRequests = this.pendingRequests.then(() => request.finally());
return request;
});
await this.plugin.connect();
const { clientAddress, assetCode, assetScale } = await ILDCP.fetch(this.plugin.sendData.bind(this.plugin));
this.pool = new pool_1.ServerConnectionPool(this.serverSecret, Object.assign(Object.assign({}, this.connectionOpts), { isServer: true, plugin: this.plugin, sourceAccount: clientAddress, assetCode,
assetScale }), (connection) => {
this.emit('connection', connection);
});
}
async close() {
if (!this.pool) {
return;
}
const serverAccount = this.pool.getServerAccount();
this.plugin.deregisterDataHandler();
this.plugin.registerDataHandler(async () => IlpPacket.serializeIlpReject({
code: IlpPacket.Errors.codes.T99_APPLICATION_ERROR,
triggeredBy: serverAccount,
message: 'Shutting down server',
data: Buffer.alloc(0),
}));
await this.pendingRequests;
await new Promise((r) => setTimeout(r, this.disconnectDelay));
await this.pool.close();
this.plugin.deregisterDataHandler();
await this.plugin.disconnect();
this.emit('_close');
this.pool = undefined;
}
async acceptConnection() {
await this.listen();
return new Promise((resolve, reject) => {
const done = (connection) => {
this.removeListener('connection', done);
this.removeListener('_close', done);
if (connection)
resolve(connection);
else
reject(new Error('server closed'));
};
this.once('connection', done);
this.once('_close', done);
});
}
generateAddressAndSecret(opts) {
if (!this.pool) {
throw new Error('Server must be connected to generate address and secret');
}
let connectionTag = Buffer.alloc(0);
let receiptNonce = Buffer.alloc(0);
let receiptSecret = Buffer.alloc(0);
let receiptsEnabled = false;
if (opts) {
if (typeof opts === 'object') {
if (opts.connectionTag) {
connectionTag = Buffer.from(opts.connectionTag, 'ascii');
}
if (!opts.receiptNonce !== !opts.receiptSecret) {
throw new Error('receiptNonce and receiptSecret must accompany each other');
}
if (opts.receiptNonce) {
if (opts.receiptNonce.length !== 16) {
throw new Error('receiptNonce must be 16 bytes');
}
receiptsEnabled = true;
receiptNonce = opts.receiptNonce;
}
if (opts.receiptSecret) {
if (opts.receiptSecret.length !== 32) {
throw new Error('receiptSecret must be 32 bytes');
}
receiptSecret = opts.receiptSecret;
}
}
else {
connectionTag = Buffer.from(opts, 'ascii');
}
}
const tokenNonce = cryptoHelper.generateTokenNonce();
const predictor = new oer_utils_1.Predictor();
predictor.writeOctetString(tokenNonce, cryptoHelper.TOKEN_NONCE_LENGTH);
predictor.writeVarOctetString(connectionTag);
predictor.writeVarOctetString(receiptNonce);
predictor.writeVarOctetString(receiptSecret);
const writer = new oer_utils_1.Writer(predictor.length);
writer.writeOctetString(tokenNonce, cryptoHelper.TOKEN_NONCE_LENGTH);
writer.writeVarOctetString(connectionTag);
writer.writeVarOctetString(receiptNonce);
writer.writeVarOctetString(receiptSecret);
const token = cryptoHelper.encryptConnectionAddressToken(this.serverSecret, writer.getBuffer());
const sharedSecret = cryptoHelper.generateSharedSecretFromToken(this.serverSecret, token);
return {
destinationAccount: `${this.pool.getServerAccount()}.${base64url(token)}`,
sharedSecret,
receiptsEnabled,
};
}
get assetCode() {
if (!this.pool) {
throw new Error('Server must be connected to get asset code.');
}
return this.pool.getAssetCode();
}
get assetScale() {
if (!this.pool) {
throw new Error('Server must be connected to get asset scale.');
}
return this.pool.getAssetScale();
}
async handleData(data) {
if (!this.pool) {
throw new Error('Unexpected call to handleData - server is not connected');
}
try {
let prepare;
try {
prepare = IlpPacket.deserializeIlpPrepare(data);
}
catch (err) {
this.log.error('got data that is not an ILP Prepare packet: %h', data);
return IlpPacket.serializeIlpReject({
code: 'F00',
message: `Expected an ILP Prepare packet (type 12), but got packet with type: ${data[0]}`,
data: Buffer.alloc(0),
triggeredBy: this.pool.getServerAccount(),
});
}
const localAddressParts = prepare.destination
.replace(`${this.pool.getServerAccount()}.`, '')
.split('.');
if (localAddressParts.length === 0 || !localAddressParts[0]) {
this.log.error('destination in ILP Prepare packet does not have a Connection ID: %s', prepare.destination);
throw new IlpPacket.Errors.UnreachableError('');
}
const connectionId = localAddressParts[0];
const connection = await this.pool
.getConnection(connectionId, prepare)
.catch((_err) => {
throw new IlpPacket.Errors.UnreachableError('');
});
const fulfill = await connection.handlePrepare(prepare);
return IlpPacket.serializeIlpFulfill(fulfill);
}
catch (err) {
const triggeredBy = this.pool.getServerAccount();
if (IlpPacket.isIlpError(err)) {
return IlpPacket.errorToReject(triggeredBy, err);
}
else {
this.log.error('error handling prepare:', err);
return IlpPacket.serializeIlpReject({
code: 'F00',
message: '',
data: Buffer.alloc(0),
triggeredBy,
});
}
}
}
}
exports.Server = Server;
async function createServer(opts) {
const server = new Server(opts);
await server.listen();
return server;
}
exports.createServer = createServer;
function base64url(buffer) {
return buffer.toString('base64').replace(/=+$/, '').replace(/\+/g, '-').replace(/\//g, '_');
}
//# sourceMappingURL=server.js.map