Projekt

Obecné

Profil

Stáhnout (11.4 KB) Statistiky
| Větev: | Revize:
1
/*jshint node:true */
2

    
3
var assert = require('assert');
4

    
5
exports.HTTPParser = HTTPParser;
6
function HTTPParser(type) {
7
  assert.ok(type === HTTPParser.REQUEST || type === HTTPParser.RESPONSE);
8
  this.type = type;
9
  this.state = type + '_LINE';
10
  this.info = {
11
    headers: [],
12
    upgrade: false
13
  };
14
  this.trailers = [];
15
  this.line = '';
16
  this.isChunked = false;
17
  this.connection = '';
18
  this.headerSize = 0; // for preventing too big headers
19
  this.body_bytes = null;
20
  this.isUserCall = false;
21
  this.hadError = false;
22
}
23
HTTPParser.maxHeaderSize = 80 * 1024; // maxHeaderSize (in bytes) is configurable, but 80kb by default;
24
HTTPParser.REQUEST = 'REQUEST';
25
HTTPParser.RESPONSE = 'RESPONSE';
26
var kOnHeaders = HTTPParser.kOnHeaders = 0;
27
var kOnHeadersComplete = HTTPParser.kOnHeadersComplete = 1;
28
var kOnBody = HTTPParser.kOnBody = 2;
29
var kOnMessageComplete = HTTPParser.kOnMessageComplete = 3;
30

    
31
// Some handler stubs, needed for compatibility
32
HTTPParser.prototype[kOnHeaders] =
33
HTTPParser.prototype[kOnHeadersComplete] =
34
HTTPParser.prototype[kOnBody] =
35
HTTPParser.prototype[kOnMessageComplete] = function () {};
36

    
37
var compatMode0_12 = true;
38
Object.defineProperty(HTTPParser, 'kOnExecute', {
39
    get: function () {
40
      // hack for backward compatibility
41
      compatMode0_12 = false;
42
      return 4;
43
    }
44
  });
45

    
46
var methods = exports.methods = HTTPParser.methods = [
47
  'DELETE',
48
  'GET',
49
  'HEAD',
50
  'POST',
51
  'PUT',
52
  'CONNECT',
53
  'OPTIONS',
54
  'TRACE',
55
  'COPY',
56
  'LOCK',
57
  'MKCOL',
58
  'MOVE',
59
  'PROPFIND',
60
  'PROPPATCH',
61
  'SEARCH',
62
  'UNLOCK',
63
  'BIND',
64
  'REBIND',
65
  'UNBIND',
66
  'ACL',
67
  'REPORT',
68
  'MKACTIVITY',
69
  'CHECKOUT',
70
  'MERGE',
71
  'M-SEARCH',
72
  'NOTIFY',
73
  'SUBSCRIBE',
74
  'UNSUBSCRIBE',
75
  'PATCH',
76
  'PURGE',
77
  'MKCALENDAR',
78
  'LINK',
79
  'UNLINK'
80
];
81
HTTPParser.prototype.reinitialize = HTTPParser;
82
HTTPParser.prototype.close =
83
HTTPParser.prototype.pause =
84
HTTPParser.prototype.resume =
85
HTTPParser.prototype.free = function () {};
86
HTTPParser.prototype._compatMode0_11 = false;
87
HTTPParser.prototype.getAsyncId = function() { return 0; };
88

    
89
var headerState = {
90
  REQUEST_LINE: true,
91
  RESPONSE_LINE: true,
92
  HEADER: true
93
};
94
HTTPParser.prototype.execute = function (chunk, start, length) {
95
  if (!(this instanceof HTTPParser)) {
96
    throw new TypeError('not a HTTPParser');
97
  }
98

    
99
  // backward compat to node < 0.11.4
100
  // Note: the start and length params were removed in newer version
101
  start = start || 0;
102
  length = typeof length === 'number' ? length : chunk.length;
103

    
104
  this.chunk = chunk;
105
  this.offset = start;
106
  var end = this.end = start + length;
107
  try {
108
    while (this.offset < end) {
109
      if (this[this.state]()) {
110
        break;
111
      }
112
    }
113
  } catch (err) {
114
    if (this.isUserCall) {
115
      throw err;
116
    }
117
    this.hadError = true;
118
    return err;
119
  }
120
  this.chunk = null;
121
  length = this.offset - start;
122
  if (headerState[this.state]) {
123
    this.headerSize += length;
124
    if (this.headerSize > HTTPParser.maxHeaderSize) {
125
      return new Error('max header size exceeded');
126
    }
127
  }
128
  return length;
129
};
130

    
131
var stateFinishAllowed = {
132
  REQUEST_LINE: true,
133
  RESPONSE_LINE: true,
134
  BODY_RAW: true
135
};
136
HTTPParser.prototype.finish = function () {
137
  if (this.hadError) {
138
    return;
139
  }
140
  if (!stateFinishAllowed[this.state]) {
141
    return new Error('invalid state for EOF');
142
  }
143
  if (this.state === 'BODY_RAW') {
144
    this.userCall()(this[kOnMessageComplete]());
145
  }
146
};
147

    
148
// These three methods are used for an internal speed optimization, and it also
149
// works if theses are noops. Basically consume() asks us to read the bytes
150
// ourselves, but if we don't do it we get them through execute().
151
HTTPParser.prototype.consume =
152
HTTPParser.prototype.unconsume =
153
HTTPParser.prototype.getCurrentBuffer = function () {};
154

    
155
//For correct error handling - see HTTPParser#execute
156
//Usage: this.userCall()(userFunction('arg'));
157
HTTPParser.prototype.userCall = function () {
158
  this.isUserCall = true;
159
  var self = this;
160
  return function (ret) {
161
    self.isUserCall = false;
162
    return ret;
163
  };
164
};
165

    
166
HTTPParser.prototype.nextRequest = function () {
167
  this.userCall()(this[kOnMessageComplete]());
168
  this.reinitialize(this.type);
169
};
170

    
171
HTTPParser.prototype.consumeLine = function () {
172
  var end = this.end,
173
      chunk = this.chunk;
174
  for (var i = this.offset; i < end; i++) {
175
    if (chunk[i] === 0x0a) { // \n
176
      var line = this.line + chunk.toString('ascii', this.offset, i);
177
      if (line.charAt(line.length - 1) === '\r') {
178
        line = line.substr(0, line.length - 1);
179
      }
180
      this.line = '';
181
      this.offset = i + 1;
182
      return line;
183
    }
184
  }
185
  //line split over multiple chunks
186
  this.line += chunk.toString('ascii', this.offset, this.end);
187
  this.offset = this.end;
188
};
189

    
190
var headerExp = /^([^: \t]+):[ \t]*((?:.*[^ \t])|)/;
191
var headerContinueExp = /^[ \t]+(.*[^ \t])/;
192
HTTPParser.prototype.parseHeader = function (line, headers) {
193
  if (line.indexOf('\r') !== -1) {
194
    throw parseErrorCode('HPE_LF_EXPECTED');
195
  }
196

    
197
  var match = headerExp.exec(line);
198
  var k = match && match[1];
199
  if (k) { // skip empty string (malformed header)
200
    headers.push(k);
201
    headers.push(match[2]);
202
  } else {
203
    var matchContinue = headerContinueExp.exec(line);
204
    if (matchContinue && headers.length) {
205
      if (headers[headers.length - 1]) {
206
        headers[headers.length - 1] += ' ';
207
      }
208
      headers[headers.length - 1] += matchContinue[1];
209
    }
210
  }
211
};
212

    
213
var requestExp = /^([A-Z-]+) ([^ ]+) HTTP\/(\d)\.(\d)$/;
214
HTTPParser.prototype.REQUEST_LINE = function () {
215
  var line = this.consumeLine();
216
  if (!line) {
217
    return;
218
  }
219
  var match = requestExp.exec(line);
220
  if (match === null) {
221
    throw parseErrorCode('HPE_INVALID_CONSTANT');
222
  }
223
  this.info.method = this._compatMode0_11 ? match[1] : methods.indexOf(match[1]);
224
  if (this.info.method === -1) {
225
    throw new Error('invalid request method');
226
  }
227
  if (match[1] === 'CONNECT') {
228
    this.info.upgrade = true;
229
  }
230
  this.info.url = match[2];
231
  this.info.versionMajor = +match[3];
232
  this.info.versionMinor = +match[4];
233
  this.body_bytes = 0;
234
  this.state = 'HEADER';
235
};
236

    
237
var responseExp = /^HTTP\/(\d)\.(\d) (\d{3}) ?(.*)$/;
238
HTTPParser.prototype.RESPONSE_LINE = function () {
239
  var line = this.consumeLine();
240
  if (!line) {
241
    return;
242
  }
243
  var match = responseExp.exec(line);
244
  if (match === null) {
245
    throw parseErrorCode('HPE_INVALID_CONSTANT');
246
  }
247
  this.info.versionMajor = +match[1];
248
  this.info.versionMinor = +match[2];
249
  var statusCode = this.info.statusCode = +match[3];
250
  this.info.statusMessage = match[4];
251
  // Implied zero length.
252
  if ((statusCode / 100 | 0) === 1 || statusCode === 204 || statusCode === 304) {
253
    this.body_bytes = 0;
254
  }
255
  this.state = 'HEADER';
256
};
257

    
258
HTTPParser.prototype.shouldKeepAlive = function () {
259
  if (this.info.versionMajor > 0 && this.info.versionMinor > 0) {
260
    if (this.connection.indexOf('close') !== -1) {
261
      return false;
262
    }
263
  } else if (this.connection.indexOf('keep-alive') === -1) {
264
    return false;
265
  }
266
  if (this.body_bytes !== null || this.isChunked) { // || skipBody
267
    return true;
268
  }
269
  return false;
270
};
271

    
272
HTTPParser.prototype.HEADER = function () {
273
  var line = this.consumeLine();
274
  if (line === undefined) {
275
    return;
276
  }
277
  var info = this.info;
278
  if (line) {
279
    this.parseHeader(line, info.headers);
280
  } else {
281
    var headers = info.headers;
282
    var hasContentLength = false;
283
    var currentContentLengthValue;
284
    for (var i = 0; i < headers.length; i += 2) {
285
      switch (headers[i].toLowerCase()) {
286
        case 'transfer-encoding':
287
          this.isChunked = headers[i + 1].toLowerCase() === 'chunked';
288
          break;
289
        case 'content-length':
290
          currentContentLengthValue = +headers[i + 1];
291
          if (hasContentLength) {
292
            // Fix duplicate Content-Length header with same values.
293
            // Throw error only if values are different.
294
            // Known issues:
295
            // https://github.com/request/request/issues/2091#issuecomment-328715113
296
            // https://github.com/nodejs/node/issues/6517#issuecomment-216263771
297
            if (currentContentLengthValue !== this.body_bytes) {
298
              throw parseErrorCode('HPE_UNEXPECTED_CONTENT_LENGTH');
299
            }
300
          } else {
301
            hasContentLength = true;
302
            this.body_bytes = currentContentLengthValue;
303
          }
304
          break;
305
        case 'connection':
306
          this.connection += headers[i + 1].toLowerCase();
307
          break;
308
        case 'upgrade':
309
          info.upgrade = true;
310
          break;
311
      }
312
    }
313

    
314
    if (this.isChunked && hasContentLength) {
315
      throw parseErrorCode('HPE_UNEXPECTED_CONTENT_LENGTH');
316
    }
317

    
318
    info.shouldKeepAlive = this.shouldKeepAlive();
319
    //problem which also exists in original node: we should know skipBody before calling onHeadersComplete
320
    var skipBody;
321
    if (compatMode0_12) {
322
      skipBody = this.userCall()(this[kOnHeadersComplete](info));
323
    } else {
324
      skipBody = this.userCall()(this[kOnHeadersComplete](info.versionMajor,
325
          info.versionMinor, info.headers, info.method, info.url, info.statusCode,
326
          info.statusMessage, info.upgrade, info.shouldKeepAlive));
327
    }
328
    if (info.upgrade || skipBody === 2) {
329
      this.nextRequest();
330
      return true;
331
    } else if (this.isChunked && !skipBody) {
332
      this.state = 'BODY_CHUNKHEAD';
333
    } else if (skipBody || this.body_bytes === 0) {
334
      this.nextRequest();
335
    } else if (this.body_bytes === null) {
336
      this.state = 'BODY_RAW';
337
    } else {
338
      this.state = 'BODY_SIZED';
339
    }
340
  }
341
};
342

    
343
HTTPParser.prototype.BODY_CHUNKHEAD = function () {
344
  var line = this.consumeLine();
345
  if (line === undefined) {
346
    return;
347
  }
348
  this.body_bytes = parseInt(line, 16);
349
  if (!this.body_bytes) {
350
    this.state = 'BODY_CHUNKTRAILERS';
351
  } else {
352
    this.state = 'BODY_CHUNK';
353
  }
354
};
355

    
356
HTTPParser.prototype.BODY_CHUNK = function () {
357
  var length = Math.min(this.end - this.offset, this.body_bytes);
358
  this.userCall()(this[kOnBody](this.chunk, this.offset, length));
359
  this.offset += length;
360
  this.body_bytes -= length;
361
  if (!this.body_bytes) {
362
    this.state = 'BODY_CHUNKEMPTYLINE';
363
  }
364
};
365

    
366
HTTPParser.prototype.BODY_CHUNKEMPTYLINE = function () {
367
  var line = this.consumeLine();
368
  if (line === undefined) {
369
    return;
370
  }
371
  assert.equal(line, '');
372
  this.state = 'BODY_CHUNKHEAD';
373
};
374

    
375
HTTPParser.prototype.BODY_CHUNKTRAILERS = function () {
376
  var line = this.consumeLine();
377
  if (line === undefined) {
378
    return;
379
  }
380
  if (line) {
381
    this.parseHeader(line, this.trailers);
382
  } else {
383
    if (this.trailers.length) {
384
      this.userCall()(this[kOnHeaders](this.trailers, ''));
385
    }
386
    this.nextRequest();
387
  }
388
};
389

    
390
HTTPParser.prototype.BODY_RAW = function () {
391
  var length = this.end - this.offset;
392
  this.userCall()(this[kOnBody](this.chunk, this.offset, length));
393
  this.offset = this.end;
394
};
395

    
396
HTTPParser.prototype.BODY_SIZED = function () {
397
  var length = Math.min(this.end - this.offset, this.body_bytes);
398
  this.userCall()(this[kOnBody](this.chunk, this.offset, length));
399
  this.offset += length;
400
  this.body_bytes -= length;
401
  if (!this.body_bytes) {
402
    this.nextRequest();
403
  }
404
};
405

    
406
// backward compat to node < 0.11.6
407
['Headers', 'HeadersComplete', 'Body', 'MessageComplete'].forEach(function (name) {
408
  var k = HTTPParser['kOn' + name];
409
  Object.defineProperty(HTTPParser.prototype, 'on' + name, {
410
    get: function () {
411
      return this[k];
412
    },
413
    set: function (to) {
414
      // hack for backward compatibility
415
      this._compatMode0_11 = true;
416
      return (this[k] = to);
417
    }
418
  });
419
});
420

    
421
function parseErrorCode(code) {
422
  var err = new Error('Parse Error');
423
  err.code = code;
424
  return err;
425
}
(4-4/5)