Skip to content
This repository has been archived by the owner on Jun 14, 2022. It is now read-only.

Commit

Permalink
incoming webhooks and response URLs for slash commands and interactiv…
Browse files Browse the repository at this point in the history
…e messages
  • Loading branch information
aoberoi committed Aug 23, 2017
1 parent 0fd21f6 commit 7b0e9ea
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 71 deletions.
96 changes: 48 additions & 48 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 20 additions & 5 deletions src/common.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IncomingHttpHeaders, request as httpReqFn } from 'http';
import { IncomingHttpHeaders, OutgoingHttpHeaders, request as httpReqFn } from 'http';
import { request as httpsReqFn } from 'https';
import cloneDeep = require('lodash.clonedeep');
import { ResponseInfo } from 'steno';
Expand All @@ -23,10 +23,25 @@ export function requestFunctionForTargetUrl(url: Url) {
return httpReqFn;
}

export function fixRequestHeaders(targetUrl: Url, headers: IncomingHttpHeaders): IncomingHttpHeaders {
const headersCopy = cloneDeep(headers);
if (headersCopy.host) {
headersCopy.host = (targetUrl.host as string);
export function fixRequestHeaders(hostname?: string, headers?: OutgoingHttpHeaders): IncomingHttpHeaders {
if (!headers) {
return {};
}
const headersCopy: IncomingHttpHeaders = {};
Object.keys(headers).forEach((key) => {
const val = headers[key];
if (val !== undefined) {
if (Array.isArray(val)) {
headersCopy[key] = val.slice();
} else if (typeof val === 'number') {
headersCopy[key] = '' + val;
} else {
headersCopy[key] = val.slice(0);
}
}
});
if (hostname && headersCopy.host) {
headersCopy.host = hostname;
}
return headersCopy;
}
Expand Down
30 changes: 26 additions & 4 deletions src/record/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import normalizePort = require('normalize-port');
import normalizeUrl = require('normalize-url');
import { join as pathJoin } from 'path';
import { PrintFn } from 'steno';
import { ProxyTargetConfig, ProxyTargetRule } from './http-proxy';
import { Recorder } from './recorder';

const log = Debug('steno:recordingcontroller');
Expand All @@ -22,10 +23,10 @@ export class RecordingController {
private recorder: Recorder;
private print: PrintFn;

constructor(incomingTargetUrl: string, outgoingTargetUrl: string, controlPort: string,
constructor(incomingTargetConfig: ProxyTargetConfig, outgoingTargetConfig: ProxyTargetConfig, controlPort: string,
inPort: string, outPort: string, scenarioName: string, print: PrintFn) {
this.scenarioName = scenarioName;
this.recorder = new Recorder(outgoingTargetUrl, outPort, incomingTargetUrl, inPort,
this.recorder = new Recorder(outgoingTargetConfig, outPort, incomingTargetConfig, inPort,
pathFromScenarioName(this.scenarioName), print);
this.app = this.createApp();
this.port = controlPort;
Expand Down Expand Up @@ -101,9 +102,30 @@ export function startRecordingController(
const outTargetHost = `${ outHostPrefix }slack.com`;
const outTargetUrl = `https://${ outTargetHost }`;

const incomingWebhooksPathPattern = /^\/services\//;
const slashCommandsPathPattern = /^\/commands\//;
const interactiveResponseUrlPathPattern = /^\/actions\//;
const hooksSubdomainRewriteRule: ProxyTargetRule = {
processor: (req, optsBefore) => {
if (req.url &&
(incomingWebhooksPathPattern.test(req.url) || slashCommandsPathPattern.test(req.url) ||
interactiveResponseUrlPathPattern.test(req.url))
) {
return Object.assign({}, optsBefore, {
// remove host because apparently hostname is preffered. host would have to include port, so meh.
host: null,
hostname: `hooks.${outTargetHost}`,
});
}
return optsBefore;
},
type: 'requestOptionRewrite',
};

const controller = new RecordingController(
normalizeUrl(incomingRequestTargetUrl), outTargetUrl, normalizePort(controlPort), normalizePort(inPort),
normalizePort(outPort), scenarioName, print,
{ targetUrl: normalizeUrl(incomingRequestTargetUrl) },
{ targetUrl: outTargetUrl, rules: [hooksSubdomainRewriteRule] },
normalizePort(controlPort), normalizePort(inPort), normalizePort(outPort), scenarioName, print,
);
return controller.start();
}
41 changes: 33 additions & 8 deletions src/record/http-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,41 +11,66 @@ import { fixRequestHeaders, requestFunctionForTargetUrl } from '../common';

import { RequestInfo, ResponseInfo } from 'steno';

export interface ProxyTargetRule {
type: 'requestOptionRewrite';
// NOTE: perhaps instead of exposing the original request to the rule processor, we should just expose the parsed
// RequestInfo
processor: (originalReq: IncomingMessage, reqOptions: RequestOptions) => RequestOptions;
}

export interface ProxyTargetConfig {
rules?: ProxyTargetRule[];
targetUrl: string; // stores a URL after it's passed through normalizeUrl()
}

const log = Debug('steno:http-proxy');

export class HttpProxy extends EventEmitter {

private server: Server;
private targetUrl: Url;
private requestOptionRewriteRules?: ProxyTargetRule[];
private requestFn:
(options: RequestOptions | string | URL, callback?: (res: IncomingMessage) => void) => ClientRequest;

constructor(targetUrl: string) {
constructor(targetConfig: ProxyTargetConfig) {
super();
log(`proxy init with target URL: ${targetUrl}`);
this.targetUrl = urlParse(targetUrl);
log(`proxy init with target URL: ${targetConfig.targetUrl}`);
this.targetUrl = urlParse(targetConfig.targetUrl);
if (targetConfig.rules) {
this.requestOptionRewriteRules = targetConfig.rules.filter((r) => r.type === 'requestOptionRewrite');
}
this.requestFn = requestFunctionForTargetUrl(this.targetUrl);
this.server = createServer(HttpProxy.prototype.onRequest.bind(this));
}

public onRequest(req: IncomingMessage, res: ServerResponse) {
// NOTE: cloneDeep usage here is for safety, but if this is a performance hit, we likely could remove it
const mungedHeaders = fixRequestHeaders(this.targetUrl, req.headers);
const requestInfo: RequestInfo = {
body: undefined,
headers: mungedHeaders,
headers: req.headers,
httpVersion: req.httpVersion,
id: uuid(),
method: cloneDeep(req.method as string),
trailers: undefined,
url: cloneDeep(req.url as string),
};

const proxyReqOptions = Object.assign({}, this.targetUrl, {
let proxyReqOptions: RequestOptions = Object.assign({}, this.targetUrl, {
headers: requestInfo.headers,
href: null,
method: requestInfo.method,
path: requestInfo.url,
});

if (this.requestOptionRewriteRules) {
// iteratively apply any rules
proxyReqOptions = this.requestOptionRewriteRules
.reduce((options, rule) => rule.processor(req, options), proxyReqOptions);
}

proxyReqOptions.headers = fixRequestHeaders(proxyReqOptions.hostname, proxyReqOptions.headers);

log('creating proxy request with options: %O', proxyReqOptions);
const proxyRequest = this.requestFn(proxyReqOptions);
// TODO: are response trailers really set on `req`?
Expand Down Expand Up @@ -124,6 +149,6 @@ export class HttpProxy extends EventEmitter {

}

export function createProxy(targetUrl: string): HttpProxy {
return new HttpProxy(targetUrl);
export function createProxy(targetConfig: ProxyTargetConfig): HttpProxy {
return new HttpProxy(targetConfig);
}
12 changes: 6 additions & 6 deletions src/record/recorder.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Debug = require('debug');
import { PrintFn } from 'steno';

import { createProxy, HttpProxy } from './http-proxy';
import { createProxy, HttpProxy, ProxyTargetConfig } from './http-proxy';
import { HttpSerializer } from './http-serializer';

const log = Debug('steno:recorder');
Expand All @@ -18,16 +18,16 @@ export class Recorder {
private incomingPort: string | number;
private print: PrintFn;

constructor(outgoingTargetUrl: string, outgoingPort: string | number,
incomingTargetUrl: string, incomingPort: string | number,
constructor(outgoingTargetConfig: ProxyTargetConfig, outgoingPort: string | number,
incomingTargetConfig: ProxyTargetConfig, incomingPort: string | number,
storagePath: string, print: PrintFn) {
this.serializer = new HttpSerializer(storagePath);

this.outgoingProxy = createProxy(outgoingTargetUrl);
this.outgoingProxy = createProxy(outgoingTargetConfig);
this.outgoingPort = outgoingPort;

this.incomingProxy = createProxy(incomingTargetUrl);
this.incomingTargetUrl = incomingTargetUrl;
this.incomingProxy = createProxy(incomingTargetConfig);
this.incomingTargetUrl = incomingTargetConfig.targetUrl;
this.incomingPort = incomingPort;

this.outgoingProxy.on('request', (info) => { this.serializer.onRequest(info, `${Date.now()}_outgoing`); });
Expand Down

0 comments on commit 7b0e9ea

Please sign in to comment.