@alertlogic/al-collector-js 3.0.5 → 3.0.7
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/README.md +11 -1
- package/al_util.js +6 -1
- package/collector_statusc.js +42 -0
- package/index.js +2 -1
- package/package.json +1 -1
- package/test/al_mock.js +23 -0
- package/test/al_util_test.js +46 -0
- package/test/collector_statusc_test.js +164 -0
- package/test/request_retry_test.js +30 -0
package/README.md
CHANGED
|
@@ -25,7 +25,9 @@ const {
|
|
|
25
25
|
AzcollectC,
|
|
26
26
|
EndpointsC,
|
|
27
27
|
AlLog,
|
|
28
|
-
Parse
|
|
28
|
+
Parse ,
|
|
29
|
+
RestServiceClient,
|
|
30
|
+
CollectorStatusC
|
|
29
31
|
} = require('@alertlogic/al-collector-js');
|
|
30
32
|
```
|
|
31
33
|
|
|
@@ -90,6 +92,14 @@ const azCollectClient = new AzcollectC(apiEndpoint, aimsCreds, collectorType, se
|
|
|
90
92
|
```javascript
|
|
91
93
|
const alEndpointsClient = EndpointsC(apiEndpoint, aimsCreds, retryOption);
|
|
92
94
|
```
|
|
95
|
+
## CollectorStatusC
|
|
96
|
+
* @param {string} apiEndpoint - Alert Logic API hostname.
|
|
97
|
+
* @param {Object} aimsCreds - Alert Logic API credentials object, refer to AimsC.
|
|
98
|
+
* @param {*} retryOptions.
|
|
99
|
+
|
|
100
|
+
```javascript
|
|
101
|
+
const alCollectorStatusClient = CollectorStatusC(apiEndpoint, aimsCreds, retryOption);
|
|
102
|
+
```
|
|
93
103
|
|
|
94
104
|
## AlLog
|
|
95
105
|
* @param hostId - host uuid obtained at collector registration
|
package/al_util.js
CHANGED
|
@@ -15,6 +15,7 @@ let MAX_CONNS_PER_SERVICE = 128;
|
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* @default Refer to https://www.npmjs.com/package/retry
|
|
18
|
+
* set maxRetryTime to 180seconds(3min) to avoid lambda timeout.
|
|
18
19
|
*/
|
|
19
20
|
let DEFAULT_RETRY = {
|
|
20
21
|
// Default values
|
|
@@ -22,7 +23,8 @@ let DEFAULT_RETRY = {
|
|
|
22
23
|
factor: 7,
|
|
23
24
|
minTimeout: 300,
|
|
24
25
|
retries: 2,
|
|
25
|
-
maxTimeout: 10000
|
|
26
|
+
maxTimeout: 10000,
|
|
27
|
+
maxRetryTime: 180000 // Maximum time to spend retrying in milliseconds.
|
|
26
28
|
};
|
|
27
29
|
|
|
28
30
|
/**
|
|
@@ -124,6 +126,9 @@ class RestServiceClient {
|
|
|
124
126
|
deleteRequest(path, extraOptions) {
|
|
125
127
|
return this.request('DELETE', path, extraOptions);
|
|
126
128
|
}
|
|
129
|
+
put(path, extraOptions) {
|
|
130
|
+
return this.request('PUT', path, extraOptions);
|
|
131
|
+
}
|
|
127
132
|
get host() {
|
|
128
133
|
return this._host;
|
|
129
134
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/* -----------------------------------------------------------------------------
|
|
2
|
+
* @copyright (C) 2023, Alert Logic, Inc
|
|
3
|
+
* @doc
|
|
4
|
+
*
|
|
5
|
+
* HTTP client for Collector status service.
|
|
6
|
+
*
|
|
7
|
+
* @end
|
|
8
|
+
* -----------------------------------------------------------------------------
|
|
9
|
+
*/
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const AlServiceC = require('./al_servicec').AlServiceC;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @class
|
|
16
|
+
* HTTPS client for Alert Logic Collector_status service.
|
|
17
|
+
*
|
|
18
|
+
* @constructor
|
|
19
|
+
* @param {string} apiEndpoint - Alert Logic API hostname.
|
|
20
|
+
* @param {Object} aimsCreds - Alert Logic API credentials object, refer to AimsC.
|
|
21
|
+
* @param {*} retryOptions
|
|
22
|
+
*/
|
|
23
|
+
class CollectorStatusC extends AlServiceC {
|
|
24
|
+
constructor(apiEndpoint, aimsCreds, retryOptions) {
|
|
25
|
+
super(apiEndpoint, 'collectors_status', 'v1', aimsCreds, retryOptions);
|
|
26
|
+
}
|
|
27
|
+
sendStatus(statusId, stream, data) {
|
|
28
|
+
let payload = {
|
|
29
|
+
headers: {
|
|
30
|
+
'Content-Type': 'application/json'
|
|
31
|
+
},
|
|
32
|
+
body: data
|
|
33
|
+
};
|
|
34
|
+
// Few collector stream contain `/` which not excepted by collectors_status service. so Encode the stream before making the api call.It will encodes special characters including: , / ? : @ & = + $ #
|
|
35
|
+
const encodedStream = encodeURIComponent(stream);
|
|
36
|
+
return this.put(`/statuses/${statusId}/streams/${encodedStream}`, payload);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = {
|
|
41
|
+
CollectorStatusC: CollectorStatusC
|
|
42
|
+
};
|
package/index.js
CHANGED
|
@@ -16,6 +16,7 @@ module.exports = {
|
|
|
16
16
|
EndpointsC : require('./al_servicec').EndpointsC,
|
|
17
17
|
AlLog : require('./al_log'),
|
|
18
18
|
Parse: require('./parse'),
|
|
19
|
-
RestServiceClient: require('./al_util').RestServiceClient
|
|
19
|
+
RestServiceClient: require('./al_util').RestServiceClient,
|
|
20
|
+
CollectorStatusC: require('./collector_statusc').CollectorStatusC
|
|
20
21
|
};
|
|
21
22
|
|
package/package.json
CHANGED
package/test/al_mock.js
CHANGED
|
@@ -28,6 +28,7 @@ const CID = '12345678';
|
|
|
28
28
|
|
|
29
29
|
const AL_API = 'al-api-endpoint.alertlogic.com';
|
|
30
30
|
const INGEST_API = 'ingest-api-endpoint.alertlogic.com';
|
|
31
|
+
const COLLECTOR_STATUS_API = 'collector-status-api-endpoint.alertlogic.com';
|
|
31
32
|
|
|
32
33
|
const AWS_CHECKIN_URL = '/aws/cwe/checkin/1234567890/us-east-1/test-function';
|
|
33
34
|
|
|
@@ -94,6 +95,21 @@ const AZCOLLECT_CHECKIN_QUERY_COMPRESSED = {
|
|
|
94
95
|
body : COMPRESSED_CHECKIN_BODY
|
|
95
96
|
};
|
|
96
97
|
|
|
98
|
+
const SEND_COLLECTOR_STATUS_BODY_DATA = {
|
|
99
|
+
status: "error",
|
|
100
|
+
inst_type: "collector",
|
|
101
|
+
stream: "Audit.Exchange",
|
|
102
|
+
status_id: "FC561097-E51D-4CB6-AB86-2A90CFFE60C7",
|
|
103
|
+
timestamp: 1685377308,
|
|
104
|
+
reported_by: "paws",
|
|
105
|
+
collection_type: "o365",
|
|
106
|
+
errorinfo: {
|
|
107
|
+
code: "500",
|
|
108
|
+
description: "server error",
|
|
109
|
+
details: "failed to send logmsgs"
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
97
113
|
const AIMS_RESPONSE_200 = {
|
|
98
114
|
'authentication': {
|
|
99
115
|
'user': {
|
|
@@ -143,6 +159,10 @@ const AIMS_RESPONSE_200 = {
|
|
|
143
159
|
}
|
|
144
160
|
};
|
|
145
161
|
|
|
162
|
+
const SERVER_ERROR_500 = {
|
|
163
|
+
statusCode: 500,
|
|
164
|
+
message: "Internal Server Error"
|
|
165
|
+
};
|
|
146
166
|
function gen_auth_response() {
|
|
147
167
|
return {
|
|
148
168
|
authentication : {
|
|
@@ -170,6 +190,9 @@ module.exports = {
|
|
|
170
190
|
AIMS_RESPONSE_200: AIMS_RESPONSE_200,
|
|
171
191
|
AZURE_REGISTER_VALUES: AZURE_REGISTER_VALUES,
|
|
172
192
|
AZURE_CHECKIN_VALUES: AZURE_CHECKIN_VALUES,
|
|
193
|
+
SEND_COLLECTOR_STATUS_BODY_DATA: SEND_COLLECTOR_STATUS_BODY_DATA,
|
|
194
|
+
COLLECTOR_STATUS_API: COLLECTOR_STATUS_API,
|
|
195
|
+
SERVER_ERROR_500: SERVER_ERROR_500,
|
|
173
196
|
|
|
174
197
|
gen_auth_response : gen_auth_response
|
|
175
198
|
};
|
package/test/al_util_test.js
CHANGED
|
@@ -132,5 +132,51 @@ describe('Unit Tests', function() {
|
|
|
132
132
|
done();
|
|
133
133
|
});
|
|
134
134
|
|
|
135
|
+
it('check it should return server error after retry', function (done) {
|
|
136
|
+
var restC = new RestServiceClient(m_alMock.AL_API);
|
|
137
|
+
|
|
138
|
+
nock('https://' + m_alMock.AL_API, {
|
|
139
|
+
reqheaders: {
|
|
140
|
+
'accept': 'application/json',
|
|
141
|
+
'host': m_alMock.AL_API,
|
|
142
|
+
'some_header': 'some_value'
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
.post(TEST_PATH, TEST_BODY)
|
|
146
|
+
.reply(500, m_alMock.SERVER_ERROR_500);
|
|
147
|
+
|
|
148
|
+
restC.post(TEST_PATH, {
|
|
149
|
+
headers: { 'some_header': 'some_value' },
|
|
150
|
+
body: TEST_BODY
|
|
151
|
+
})
|
|
152
|
+
.catch(err => {
|
|
153
|
+
console.log(`err ${err}`);
|
|
154
|
+
done();
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('check it should success if last response is successful after server error 500', function (done) {
|
|
159
|
+
var restC = new RestServiceClient(m_alMock.AL_API);
|
|
160
|
+
var restMock = nock('https://' + m_alMock.AL_API, {
|
|
161
|
+
reqheaders: {
|
|
162
|
+
'accept': 'application/json',
|
|
163
|
+
'host': m_alMock.AL_API,
|
|
164
|
+
'content-type': 'application/json',
|
|
165
|
+
'some_header': 'some_value'
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
.post(TEST_PATH, TEST_BODY)
|
|
169
|
+
.reply(500, m_alMock.SERVER_ERROR_500)
|
|
170
|
+
.post(TEST_PATH, TEST_BODY)
|
|
171
|
+
.reply(200, m_alMock.AIMS_RESPONSE_200);
|
|
172
|
+
|
|
173
|
+
restC.post(TEST_PATH, {
|
|
174
|
+
headers: { 'some_header': 'some_value' },
|
|
175
|
+
body: TEST_BODY
|
|
176
|
+
}).then(res => {
|
|
177
|
+
assert.ok(restMock.isDone());
|
|
178
|
+
done();
|
|
179
|
+
});
|
|
180
|
+
});
|
|
135
181
|
});
|
|
136
182
|
});
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/* -----------------------------------------------------------------------------
|
|
2
|
+
* @copyright (C) 2023, Alert Logic, Inc
|
|
3
|
+
* @doc
|
|
4
|
+
*
|
|
5
|
+
* Tests for base Alert Logic Collectors Status client
|
|
6
|
+
*
|
|
7
|
+
* @end
|
|
8
|
+
* -----------------------------------------------------------------------------
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const sinon = require('sinon');
|
|
13
|
+
const assert = require('assert');
|
|
14
|
+
const AimsC = require('../al_servicec').AimsC;
|
|
15
|
+
const AlServiceC = require('../al_servicec').AlServiceC;
|
|
16
|
+
const m_alMock = require('./al_mock');
|
|
17
|
+
const CollectorStatusC = require('../collector_statusc').CollectorStatusC;
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
describe('Unit Tests', function () {
|
|
21
|
+
|
|
22
|
+
describe('Collector_statusc', function () {
|
|
23
|
+
var fakeAuth;
|
|
24
|
+
let fakePut;
|
|
25
|
+
beforeEach(function () {
|
|
26
|
+
fakeAuth = sinon.stub(AimsC.prototype, 'authenticate').callsFake(
|
|
27
|
+
function fakeFn() {
|
|
28
|
+
return new Promise(function (resolve, reject) {
|
|
29
|
+
resolve(m_alMock.gen_auth_response());
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
afterEach(function (done) {
|
|
34
|
+
fakeAuth.restore();
|
|
35
|
+
fakePut.restore();
|
|
36
|
+
fs.unlink(m_alMock.CACHE_FILENAME, function (err) {
|
|
37
|
+
done();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('Verify send Status called with correct parameter', function (done) {
|
|
42
|
+
|
|
43
|
+
fakePut = sinon.stub(AlServiceC.prototype, 'put').callsFake(
|
|
44
|
+
function fakeFn(path, extraOptions) {
|
|
45
|
+
assert.equal(extraOptions.body, m_alMock.SEND_COLLECTOR_STATUS_BODY_DATA);
|
|
46
|
+
assert.equal(path, `/statuses/${m_alMock.SEND_COLLECTOR_STATUS_BODY_DATA.status_id}/streams/${m_alMock.SEND_COLLECTOR_STATUS_BODY_DATA.stream}`);
|
|
47
|
+
done();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
var aimsc = new AimsC(m_alMock.AL_API, m_alMock.AIMS_CREDS);
|
|
51
|
+
var collectorStatus = new CollectorStatusC(m_alMock.COLLECTOR_STATUS_API, aimsc);
|
|
52
|
+
collectorStatus.sendStatus(m_alMock.SEND_COLLECTOR_STATUS_BODY_DATA.status_id, m_alMock.SEND_COLLECTOR_STATUS_BODY_DATA.stream, m_alMock.SEND_COLLECTOR_STATUS_BODY_DATA).then(res => {
|
|
53
|
+
fakePut.restore();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('it should encode the stream if it contain special char', function (done) {
|
|
58
|
+
let stream = 'projects/appliance-builds';
|
|
59
|
+
const encodedStream = encodeURIComponent(stream);
|
|
60
|
+
fakePut = sinon.stub(AlServiceC.prototype, 'put').callsFake(
|
|
61
|
+
function fakeFn(path, extraOptions) {
|
|
62
|
+
assert.equal(extraOptions.body, m_alMock.SEND_COLLECTOR_STATUS_BODY_DATA);
|
|
63
|
+
assert.equal(path, `/statuses/${m_alMock.SEND_COLLECTOR_STATUS_BODY_DATA.status_id}/streams/${encodedStream}`);
|
|
64
|
+
done();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
var aimsc = new AimsC(m_alMock.AL_API, m_alMock.AIMS_CREDS);
|
|
68
|
+
var collectorStatus = new CollectorStatusC(m_alMock.COLLECTOR_STATUS_API, aimsc);
|
|
69
|
+
m_alMock.SEND_COLLECTOR_STATUS_BODY_DATA.stream = stream;
|
|
70
|
+
collectorStatus.sendStatus(m_alMock.SEND_COLLECTOR_STATUS_BODY_DATA.status_id, m_alMock.SEND_COLLECTOR_STATUS_BODY_DATA.stream, m_alMock.SEND_COLLECTOR_STATUS_BODY_DATA).then(res => {
|
|
71
|
+
fakePut.restore();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('If sequence of parameter is not correct then api throw the error', function (done) {
|
|
76
|
+
const error = {
|
|
77
|
+
"errorinfo": {
|
|
78
|
+
"error_id": "246A66D0-2910-46E1-9C1F-24359B6714A0",
|
|
79
|
+
"description": "Stream does not match payload",
|
|
80
|
+
"code": "stream_mismatch"
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
fakePut = sinon.stub(AlServiceC.prototype, 'put').callsFake(
|
|
84
|
+
function fakeFn(path, extraOptions) {
|
|
85
|
+
return new Promise(function (resolve, reject) {
|
|
86
|
+
reject(error);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
var aimsc = new AimsC(m_alMock.AL_API, m_alMock.AIMS_CREDS);
|
|
91
|
+
var collectorStatus = new CollectorStatusC(m_alMock.COLLECTOR_STATUS_API, aimsc);
|
|
92
|
+
collectorStatus.sendStatus(m_alMock.SEND_COLLECTOR_STATUS_BODY_DATA.stream, m_alMock.SEND_COLLECTOR_STATUS_BODY_DATA.status_id, m_alMock.SEND_COLLECTOR_STATUS_BODY_DATA).then(res => {
|
|
93
|
+
}).catch(err => {
|
|
94
|
+
assert.deepEqual(err, error);
|
|
95
|
+
done();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('collection_type and inst_type value should be within the defined enum else throw error', function (done) {
|
|
100
|
+
const error = {
|
|
101
|
+
"errorinfo": {
|
|
102
|
+
"error_id": "5BC87CAB-8CC7-4B36-9B09-3E26B3088F77",
|
|
103
|
+
"details": {
|
|
104
|
+
"schema_validation_error": [
|
|
105
|
+
{
|
|
106
|
+
"invalid": "data",
|
|
107
|
+
"schema": {
|
|
108
|
+
"type": "string",
|
|
109
|
+
"enum": [
|
|
110
|
+
"o365",
|
|
111
|
+
"auth0",
|
|
112
|
+
"Carbonblack",
|
|
113
|
+
"Ciscoamp",
|
|
114
|
+
"Ciscoduo",
|
|
115
|
+
"crowdstrike",
|
|
116
|
+
"googlestackdriver",
|
|
117
|
+
"gsuite",
|
|
118
|
+
"Mimecast",
|
|
119
|
+
"salesforce",
|
|
120
|
+
"aws_elb_classic",
|
|
121
|
+
"s3_audit_logs",
|
|
122
|
+
"redshift_connection_logs",
|
|
123
|
+
"redshift_user_activity_logs",
|
|
124
|
+
"redshift_user_logs",
|
|
125
|
+
"vpc_flow_logs_v2",
|
|
126
|
+
"aws_elb_application",
|
|
127
|
+
"aws_elb_network",
|
|
128
|
+
"aws_network_firewall",
|
|
129
|
+
"carbon_black_edr",
|
|
130
|
+
"aws_eks_log_cwl_export",
|
|
131
|
+
"crowdstrike_fdr",
|
|
132
|
+
"aws_waf"
|
|
133
|
+
]
|
|
134
|
+
},
|
|
135
|
+
"error": "not_in_enum",
|
|
136
|
+
"data": "CiscoMeraki",
|
|
137
|
+
"path": [
|
|
138
|
+
"collection_type"
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
]
|
|
142
|
+
},
|
|
143
|
+
"description": "JSON Schema Validation error",
|
|
144
|
+
"code": "schema_validation_error"
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
fakePut = sinon.stub(AlServiceC.prototype, 'put').callsFake(
|
|
148
|
+
function fakeFn(path, extraOptions) {
|
|
149
|
+
return new Promise(function (resolve, reject) {
|
|
150
|
+
reject(error);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
var aimsc = new AimsC(m_alMock.AL_API, m_alMock.AIMS_CREDS);
|
|
155
|
+
var collectorStatus = new CollectorStatusC(m_alMock.COLLECTOR_STATUS_API, aimsc);
|
|
156
|
+
m_alMock.SEND_COLLECTOR_STATUS_BODY_DATA.collection_type = 'CiscoMeraki';
|
|
157
|
+
collectorStatus.sendStatus(m_alMock.SEND_COLLECTOR_STATUS_BODY_DATA.stream, m_alMock.SEND_COLLECTOR_STATUS_BODY_DATA.status_id, m_alMock.SEND_COLLECTOR_STATUS_BODY_DATA).then(res => {
|
|
158
|
+
}).catch(err => {
|
|
159
|
+
assert.deepEqual(err.code, error.code);
|
|
160
|
+
done();
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -14,6 +14,7 @@ const nock = require('nock');
|
|
|
14
14
|
const m_alMock = require('./al_mock');
|
|
15
15
|
const m_alService = require('../al_servicec');
|
|
16
16
|
const EndpointsC = require('../al_servicec').EndpointsC;
|
|
17
|
+
const moment = require('moment');
|
|
17
18
|
|
|
18
19
|
describe('HTTP request retry tests', function() {
|
|
19
20
|
beforeEach(function(done) {
|
|
@@ -202,4 +203,33 @@ describe('HTTP request retry tests', function() {
|
|
|
202
203
|
return done();
|
|
203
204
|
});
|
|
204
205
|
});
|
|
206
|
+
|
|
207
|
+
it('Retry 500 with custom retry config with maxRetryTime', function (done) {
|
|
208
|
+
const maxRetryTime = 1500;
|
|
209
|
+
const retryOptions = {
|
|
210
|
+
retries: 3,
|
|
211
|
+
factor: 2,
|
|
212
|
+
minTimeout: 300,
|
|
213
|
+
maxTimeout: 1000,
|
|
214
|
+
maxRetryTime: maxRetryTime,
|
|
215
|
+
};
|
|
216
|
+
nock('https://' + m_alMock.AL_API)
|
|
217
|
+
.post('/aims/v1/authenticate')
|
|
218
|
+
.times(4)
|
|
219
|
+
.reply(500, m_alMock.SERVER_ERROR_500);
|
|
220
|
+
var startTime = moment();
|
|
221
|
+
var aimsc = new m_alService.AimsC(
|
|
222
|
+
m_alMock.AL_API, m_alMock.AIMS_CREDS, '/tmp', retryOptions);
|
|
223
|
+
aimsc.authenticate()
|
|
224
|
+
.catch(err => {
|
|
225
|
+
assert.equal(err.statusCode, 500);
|
|
226
|
+
var nowMoment = moment();
|
|
227
|
+
const elapsedTime = nowMoment.diff(startTime, 'milliseconds');
|
|
228
|
+
assert.ok(
|
|
229
|
+
elapsedTime >= maxRetryTime && elapsedTime <= maxRetryTime + retryOptions.maxTimeout,
|
|
230
|
+
'Test case failed: Request did not complete within maxRetryTime.'
|
|
231
|
+
);
|
|
232
|
+
return done();
|
|
233
|
+
});
|
|
234
|
+
});
|
|
205
235
|
});
|