@contrast/sources 1.3.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -23,6 +23,11 @@ const NormalizedUriMapper = require('./normalized-uri-mapper');
23
23
  const { HttpSourceInfo } = require('./source-info');
24
24
 
25
25
  const componentName = 'sources';
26
+ const SourceType = {
27
+ HTTP: 'HTTP',
28
+ WEBSOCKET: 'WEBSOCKET',
29
+ };
30
+
26
31
 
27
32
  module.exports = Core.makeComponent({
28
33
  name: componentName,
@@ -48,20 +53,40 @@ class Sources {
48
53
  const { _hooks, _normalizedUriMapper, core } = this;
49
54
 
50
55
  return function (next, data) {
51
- const { args: [event, req, res] } = data;
52
-
53
- if (event !== 'request') {
54
- if (event === 'listening') {
55
- // take a snapshot of Perf.all at this point. this will get logged
56
- // at some point on the perf interval timer.
57
- core.Perf.mark('listening');
58
- core.messages.emit(Event.SERVER_LISTENING, { type: serverType, server: data.obj });
59
- }
56
+ const { args: [event, req, resOrSocket] } = data;
57
+
58
+ if (event === 'listening') {
59
+ // take a snapshot of Perf.all at this point. this will get logged
60
+ // at some point on the perf interval timer.
61
+ core.Perf.mark('listening');
62
+ core.messages.emit(Event.SERVER_LISTENING, { type: serverType, server: data.obj });
60
63
  return next();
61
64
  }
62
65
 
63
- core.Perf.requestCount += 1;
66
+ const isUpgrade = event === 'upgrade';
67
+ let protocol = serverType == 'http' ? 'http' : 'https';
68
+
69
+ if (isUpgrade) {
70
+ for (let i = 0; i < req.rawHeaders.length; i += 2) {
71
+ if (req.rawHeaders[i].toLowerCase?.() == 'upgrade') {
72
+ protocol = req.rawHeaders[i + 1]?.toLowerCase?.();
73
+ break;
74
+ }
75
+ }
76
+ }
77
+
78
+ // For HTTP servers we run the "request" and "upgrade" events in a source
79
+ // scope. We only support "websocket" upgrades currently, but this can be
80
+ // extended. Support for non-HTTP sources is expected and will require new
81
+ // instrumentation and possibly model alterations.
82
+ if (
83
+ event !== 'request' &&
84
+ (!isUpgrade || (isUpgrade && protocol !== 'websocket'))
85
+ ) {
86
+ return next();
87
+ }
64
88
 
89
+ // websocket sources are http
65
90
  const sourceInfo = new HttpSourceInfo({
66
91
  serverType,
67
92
  raw: req,
@@ -69,20 +94,38 @@ class Sources {
69
94
  });
70
95
  const store = { sourceInfo };
71
96
 
72
- onFinished(res, (/* err, req */) => {
73
- core.messages.emit(Event.RESPONSE_FINISH, store);
74
- });
97
+ if (isUpgrade) {
98
+ core.patcher.patch(resOrSocket, 'emit', {
99
+ name: `${serverType}.socket.emit`,
100
+ patchType: 'sources',
101
+ around(next) {
102
+ return core.scopes.sources.run(store, next);
103
+ },
104
+ });
105
+ } else {
106
+ onFinished(resOrSocket, (/* err, req */) => {
107
+ core.messages.emit(Event.RESPONSE_FINISH, store);
108
+ });
109
+ }
110
+
111
+ core.Perf.requestCount += 1;
75
112
 
76
113
  return core.scopes.sources.run(store, () => {
77
- if (_hooks._events.onSource) {
78
- _hooks.emit('onSource', {
79
- // future: non-http sources will have their own type
80
- sourceType: 'HTTP',
81
- store,
82
- incomingMessage: req,
83
- serverResponse: res,
84
- });
85
- }
114
+ const sourceType = protocol == 'websocket' ? SourceType.WEBSOCKET : SourceType.HTTP;
115
+ const eventArg = {
116
+ sourceType,
117
+ store,
118
+ incomingMessage: req,
119
+ };
120
+
121
+ if (sourceType == SourceType.HTTP)
122
+ eventArg.serverResponse = resOrSocket;
123
+
124
+ else if (sourceType == SourceType.WEBSOCKET)
125
+ eventArg.socket = resOrSocket;
126
+
127
+ if (_hooks._events.onSource)
128
+ _hooks.emit('onSource', eventArg);
86
129
 
87
130
  return next();
88
131
  });
package/lib/index.test.js CHANGED
@@ -8,6 +8,38 @@ const mocks = require('@contrast/test/mocks');
8
8
  const proxyquire = require('proxyquire');
9
9
 
10
10
  describe('agentify sources', function () {
11
+ let core, api, ServerMock, reqMock, resMock, onFinishedMock;
12
+
13
+ beforeEach(function () {
14
+ ({ core } = initProtectFixture());
15
+ ServerMock = function ServerMock() {
16
+ this.e = new EventEmitter();
17
+ };
18
+ ServerMock.prototype.emit = function (...args) {
19
+ this.e.emit(...args);
20
+ };
21
+ ServerMock.prototype.on = function (...args) {
22
+ this.e.on(...args);
23
+ };
24
+ api = {
25
+ Server: ServerMock,
26
+ createServer() {
27
+ return new ServerMock();
28
+ },
29
+ createSecureServer() {
30
+ return new ServerMock();
31
+ }
32
+ };
33
+ reqMock = mocks.incomingMessage();
34
+ // resMock = new EventEmitter();
35
+ onFinishedMock = sinon.stub();
36
+
37
+ core.depHooks.resolve.withArgs(sinon.match({ name: 'http' })).yields(api);
38
+ proxyquire('.', {
39
+ 'on-finished': onFinishedMock,
40
+ })(core).install();
41
+ });
42
+
11
43
  [
12
44
  {
13
45
  name: 'http',
@@ -24,41 +56,9 @@ describe('agentify sources', function () {
24
56
  protocol: 'https',
25
57
  serverType: 'https',
26
58
  },
27
- }
59
+ },
28
60
  ].forEach(({ name, method, expected }) => {
29
61
  describe(`${name} sources using ${method || 'Server'}()`, function () {
30
- let core, api, ServerMock, reqMock, resMock, onFinishedMock;
31
-
32
- beforeEach(function () {
33
- ({ core } = initProtectFixture());
34
- ServerMock = function ServerMock() {
35
- this.e = new EventEmitter();
36
- };
37
- ServerMock.prototype.emit = function (...args) {
38
- this.e.emit(...args);
39
- };
40
- ServerMock.prototype.on = function (...args) {
41
- this.e.on(...args);
42
- };
43
- api = {
44
- Server: ServerMock,
45
- createServer() {
46
- return new ServerMock();
47
- },
48
- createSecureServer() {
49
- return new ServerMock();
50
- }
51
- };
52
- reqMock = mocks.incomingMessage();
53
- // resMock = new EventEmitter();
54
- onFinishedMock = sinon.stub();
55
-
56
- core.depHooks.resolve.withArgs(sinon.match({ name: 'http' })).yields(api);
57
- proxyquire('.', {
58
- 'on-finished': onFinishedMock,
59
- })(core).install();
60
- });
61
-
62
62
  it('"request" events run in scope with correct sourceInfo', function () {
63
63
  const server = method ? api[method]() : new ServerMock();
64
64
  let store;
@@ -88,7 +88,52 @@ describe('agentify sources', function () {
88
88
 
89
89
  server.emit('foo', reqMock, resMock);
90
90
  expect(store).to.be.undefined;
91
+ expect(onFinishedMock).not.to.have.been.called;
91
92
  });
92
93
  });
93
94
  });
95
+
96
+ it('"upgrade" events run in scope with correct sourceInfo', function () {
97
+ const server = new ServerMock();
98
+ const socketMock = new EventEmitter();
99
+ let store;
100
+ let socketEventStore;
101
+
102
+ reqMock.rawHeaders.push('upgrade', 'websocket');
103
+
104
+ server.on('upgrade', function () {
105
+ store = core.scopes.sources.getStore();
106
+ });
107
+
108
+ server.emit('upgrade', reqMock, socketMock);
109
+
110
+ expect(store.sourceInfo).to.deep.include({
111
+ port: 8080,
112
+ protocol: 'http',
113
+ serverType: 'http',
114
+ });
115
+
116
+ socketMock.on('some-message', function() {
117
+ socketEventStore = core.scopes.sources.getStore();
118
+ });
119
+ socketMock.emit('some-message');
120
+
121
+ expect(store).to.equal(socketEventStore);
122
+ });
123
+
124
+ it('"upgrade" events do not run in scope if not "websocket"', function () {
125
+ const server = new ServerMock();
126
+ const socketMock = new EventEmitter();
127
+ let store;
128
+
129
+ reqMock.rawHeaders.push('upgrade', 'h2c');
130
+
131
+ server.on('upgrade', function () {
132
+ store = core.scopes.sources.getStore();
133
+ });
134
+
135
+ server.emit('upgrade', reqMock, socketMock);
136
+
137
+ expect(store).to.be.undefined;
138
+ });
94
139
  });
@@ -53,7 +53,7 @@ class HttpSourceInfo {
53
53
  this.httpVersion = raw.httpVersion;
54
54
  this.ip = raw.socket.remoteAddress ? StringPrototypeReplace.call(raw.socket.remoteAddress, /::ffff:/, '') : undefined;
55
55
  this.port = raw.socket.address?.()?.port || 0;
56
- this.protocol = serverType == 'http' ? 'http' : 'https'; // todo
56
+ this.protocol = serverType == 'http' ? 'http' : 'https';
57
57
  this.serverType = serverType;
58
58
  this.time = Date.now();
59
59
  this.method = StringPrototypeToLowerCase.call(raw.method);
@@ -62,20 +62,11 @@ class HttpSourceInfo {
62
62
  for (let i = 0; i < raw.rawHeaders.length; i += 2) {
63
63
  const iNext = i + 1;
64
64
  const headerName = StringPrototypeToLowerCase.call(raw.rawHeaders[i]);
65
-
66
65
  headerName == 'content-type' && (this.contentType = raw.rawHeaders[iNext]);
67
-
68
66
  this.rawHeaders[i] = headerName;
69
67
  this.rawHeaders[iNext] = headerName == 'content-type' ?
70
68
  StringPrototypeToLowerCase.call(raw.rawHeaders[iNext]) :
71
69
  raw.rawHeaders[iNext];
72
-
73
- if (
74
- headerName == 'upgrade' &&
75
- StringPrototypeToLowerCase.call(this.rawHeaders[iNext]) == 'websocket'
76
- ) {
77
- this.protocol = 'ws';
78
- }
79
70
  }
80
71
 
81
72
  const idx = raw.url.indexOf('?');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/sources",
3
- "version": "1.3.1",
3
+ "version": "1.5.0",
4
4
  "description": "Instruments to have incoming messages run in async-local request scope.",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -9,8 +9,8 @@
9
9
  "author": "",
10
10
  "license": "ISC",
11
11
  "dependencies": {
12
- "@contrast/common": "1.37.0",
13
- "@contrast/core": "1.57.1",
12
+ "@contrast/common": "1.38.0",
13
+ "@contrast/core": "1.59.0",
14
14
  "on-finished": "^2.4.1"
15
15
  }
16
16
  }