Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download
81146 views
1
// Copyright 2012 Joyent, Inc. All rights reserved.
2
3
var assert = require('assert-plus');
4
var util = require('util');
5
6
7
8
///--- Globals
9
10
var Algorithms = {
11
'rsa-sha1': true,
12
'rsa-sha256': true,
13
'rsa-sha512': true,
14
'dsa-sha1': true,
15
'hmac-sha1': true,
16
'hmac-sha256': true,
17
'hmac-sha512': true
18
};
19
20
var State = {
21
New: 0,
22
Params: 1
23
};
24
25
var ParamsState = {
26
Name: 0,
27
Quote: 1,
28
Value: 2,
29
Comma: 3
30
};
31
32
33
34
///--- Specific Errors
35
36
function HttpSignatureError(message, caller) {
37
if (Error.captureStackTrace)
38
Error.captureStackTrace(this, caller || HttpSignatureError);
39
40
this.message = message;
41
this.name = caller.name;
42
}
43
util.inherits(HttpSignatureError, Error);
44
45
function ExpiredRequestError(message) {
46
HttpSignatureError.call(this, message, ExpiredRequestError);
47
}
48
util.inherits(ExpiredRequestError, HttpSignatureError);
49
50
51
function InvalidHeaderError(message) {
52
HttpSignatureError.call(this, message, InvalidHeaderError);
53
}
54
util.inherits(InvalidHeaderError, HttpSignatureError);
55
56
57
function InvalidParamsError(message) {
58
HttpSignatureError.call(this, message, InvalidParamsError);
59
}
60
util.inherits(InvalidParamsError, HttpSignatureError);
61
62
63
function MissingHeaderError(message) {
64
HttpSignatureError.call(this, message, MissingHeaderError);
65
}
66
util.inherits(MissingHeaderError, HttpSignatureError);
67
68
69
70
///--- Exported API
71
72
module.exports = {
73
74
/**
75
* Parses the 'Authorization' header out of an http.ServerRequest object.
76
*
77
* Note that this API will fully validate the Authorization header, and throw
78
* on any error. It will not however check the signature, or the keyId format
79
* as those are specific to your environment. You can use the options object
80
* to pass in extra constraints.
81
*
82
* As a response object you can expect this:
83
*
84
* {
85
* "scheme": "Signature",
86
* "params": {
87
* "keyId": "foo",
88
* "algorithm": "rsa-sha256",
89
* "headers": [
90
* "date" or "x-date",
91
* "content-md5"
92
* ],
93
* "signature": "base64"
94
* },
95
* "signingString": "ready to be passed to crypto.verify()"
96
* }
97
*
98
* @param {Object} request an http.ServerRequest.
99
* @param {Object} options an optional options object with:
100
* - clockSkew: allowed clock skew in seconds (default 300).
101
* - headers: required header names (def: date or x-date)
102
* - algorithms: algorithms to support (default: all).
103
* @return {Object} parsed out object (see above).
104
* @throws {TypeError} on invalid input.
105
* @throws {InvalidHeaderError} on an invalid Authorization header error.
106
* @throws {InvalidParamsError} if the params in the scheme are invalid.
107
* @throws {MissingHeaderError} if the params indicate a header not present,
108
* either in the request headers from the params,
109
* or not in the params from a required header
110
* in options.
111
* @throws {ExpiredRequestError} if the value of date or x-date exceeds skew.
112
*/
113
parseRequest: function parseRequest(request, options) {
114
assert.object(request, 'request');
115
assert.object(request.headers, 'request.headers');
116
if (options === undefined) {
117
options = {};
118
}
119
if (options.headers === undefined) {
120
options.headers = [request.headers['x-date'] ? 'x-date' : 'date'];
121
}
122
assert.object(options, 'options');
123
assert.arrayOfString(options.headers, 'options.headers');
124
assert.optionalNumber(options.clockSkew, 'options.clockSkew');
125
126
if (!request.headers.authorization)
127
throw new MissingHeaderError('no authorization header present in ' +
128
'the request');
129
130
options.clockSkew = options.clockSkew || 300;
131
132
133
var i = 0;
134
var state = State.New;
135
var substate = ParamsState.Name;
136
var tmpName = '';
137
var tmpValue = '';
138
139
var parsed = {
140
scheme: '',
141
params: {},
142
signingString: '',
143
144
get algorithm() {
145
return this.params.algorithm.toUpperCase();
146
},
147
148
get keyId() {
149
return this.params.keyId;
150
}
151
152
};
153
154
var authz = request.headers.authorization;
155
for (i = 0; i < authz.length; i++) {
156
var c = authz.charAt(i);
157
158
switch (Number(state)) {
159
160
case State.New:
161
if (c !== ' ') parsed.scheme += c;
162
else state = State.Params;
163
break;
164
165
case State.Params:
166
switch (Number(substate)) {
167
168
case ParamsState.Name:
169
var code = c.charCodeAt(0);
170
// restricted name of A-Z / a-z
171
if ((code >= 0x41 && code <= 0x5a) || // A-Z
172
(code >= 0x61 && code <= 0x7a)) { // a-z
173
tmpName += c;
174
} else if (c === '=') {
175
if (tmpName.length === 0)
176
throw new InvalidHeaderError('bad param format');
177
substate = ParamsState.Quote;
178
} else {
179
throw new InvalidHeaderError('bad param format');
180
}
181
break;
182
183
case ParamsState.Quote:
184
if (c === '"') {
185
tmpValue = '';
186
substate = ParamsState.Value;
187
} else {
188
throw new InvalidHeaderError('bad param format');
189
}
190
break;
191
192
case ParamsState.Value:
193
if (c === '"') {
194
parsed.params[tmpName] = tmpValue;
195
substate = ParamsState.Comma;
196
} else {
197
tmpValue += c;
198
}
199
break;
200
201
case ParamsState.Comma:
202
if (c === ',') {
203
tmpName = '';
204
substate = ParamsState.Name;
205
} else {
206
throw new InvalidHeaderError('bad param format');
207
}
208
break;
209
210
default:
211
throw new Error('Invalid substate');
212
}
213
break;
214
215
default:
216
throw new Error('Invalid substate');
217
}
218
219
}
220
221
if (!parsed.params.headers || parsed.params.headers === '') {
222
if (request.headers['x-date']) {
223
parsed.params.headers = ['x-date'];
224
} else {
225
parsed.params.headers = ['date'];
226
}
227
} else {
228
parsed.params.headers = parsed.params.headers.split(' ');
229
}
230
231
// Minimally validate the parsed object
232
if (!parsed.scheme || parsed.scheme !== 'Signature')
233
throw new InvalidHeaderError('scheme was not "Signature"');
234
235
if (!parsed.params.keyId)
236
throw new InvalidHeaderError('keyId was not specified');
237
238
if (!parsed.params.algorithm)
239
throw new InvalidHeaderError('algorithm was not specified');
240
241
if (!parsed.params.signature)
242
throw new InvalidHeaderError('signature was not specified');
243
244
// Check the algorithm against the official list
245
parsed.params.algorithm = parsed.params.algorithm.toLowerCase();
246
if (!Algorithms[parsed.params.algorithm])
247
throw new InvalidParamsError(parsed.params.algorithm +
248
' is not supported');
249
250
// Build the signingString
251
for (i = 0; i < parsed.params.headers.length; i++) {
252
var h = parsed.params.headers[i].toLowerCase();
253
parsed.params.headers[i] = h;
254
255
if (h !== 'request-line') {
256
var value = request.headers[h];
257
if (!value)
258
throw new MissingHeaderError(h + ' was not in the request');
259
parsed.signingString += h + ': ' + value;
260
} else {
261
parsed.signingString +=
262
request.method + ' ' + request.url + ' HTTP/' + request.httpVersion;
263
}
264
265
if ((i + 1) < parsed.params.headers.length)
266
parsed.signingString += '\n';
267
}
268
269
// Check against the constraints
270
var date;
271
if (request.headers.date || request.headers['x-date']) {
272
if (request.headers['x-date']) {
273
date = new Date(request.headers['x-date']);
274
} else {
275
date = new Date(request.headers.date);
276
}
277
var now = new Date();
278
var skew = Math.abs(now.getTime() - date.getTime());
279
280
if (skew > options.clockSkew * 1000) {
281
throw new ExpiredRequestError('clock skew of ' +
282
(skew / 1000) +
283
's was greater than ' +
284
options.clockSkew + 's');
285
}
286
}
287
288
options.headers.forEach(function (hdr) {
289
// Remember that we already checked any headers in the params
290
// were in the request, so if this passes we're good.
291
if (parsed.params.headers.indexOf(hdr) < 0)
292
throw new MissingHeaderError(hdr + ' was not a signed header');
293
});
294
295
if (options.algorithms) {
296
if (options.algorithms.indexOf(parsed.params.algorithm) === -1)
297
throw new InvalidParamsError(parsed.params.algorithm +
298
' is not a supported algorithm');
299
}
300
301
return parsed;
302
}
303
304
};
305
306