bare-http1 3.3.1 → 3.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/README.md CHANGED
@@ -6,9 +6,9 @@ HTTP/1 library for JavaScript.
6
6
  npm i bare-http1
7
7
  ```
8
8
 
9
- Currently HTTP servers does NOT support server request bodies, but supports most other HTTP features (keep-alive, chunked encoding, etc.) and streaming server responses.
9
+ `http.Server` supports most HTTP features, e.g. keep-alive, chunked encoding, and streaming responses.
10
10
 
11
- Basic HTTP client is supported, but currently it does NOT support keep-alive and protocol negotiation.
11
+ Basic `http.ClientRequest` is implemented, temporarily it does NOT support keep-alive and protocol negotiation.
12
12
 
13
13
  ## Usage
14
14
 
@@ -20,12 +20,18 @@ module.exports = class HTTPClientConnection {
20
20
  this._read = 0
21
21
  this._buffer = null
22
22
 
23
+ this._onerror = this._onerror.bind(this)
24
+ this._onclose = this._onclose.bind(this)
25
+ this._onend = this._onend.bind(this)
26
+ this._ondata = this._ondata.bind(this)
27
+ this._ondrain = this._ondrain.bind(this)
28
+
23
29
  socket
24
- .on('error', this._onerror.bind(this))
25
- .on('close', this._onclose.bind(this))
26
- .on('end', this._onend.bind(this))
27
- .on('data', this._ondata.bind(this))
28
- .on('drain', this._ondrain.bind(this))
30
+ .on('error', this._onerror)
31
+ .on('close', this._onclose)
32
+ .on('end', this._onend)
33
+ .on('data', this._ondata)
34
+ .on('drain', this._ondrain)
29
35
  }
30
36
 
31
37
  _onerror (err) {
@@ -106,7 +112,7 @@ module.exports = class HTTPClientConnection {
106
112
  const r = data.toString().split('\r\n')
107
113
  if (r.length === 0) return this.socket.destroy()
108
114
 
109
- const [, statusCode, statusMessage] = r[0].split(' ')
115
+ const [, statusCode, ...statusMessage] = r[0].split(' ')
110
116
  if (!statusCode || !statusMessage) return this.socket.destroy()
111
117
 
112
118
  const headers = {}
@@ -116,11 +122,18 @@ module.exports = class HTTPClientConnection {
116
122
  headers[name.toLowerCase()] = value
117
123
  }
118
124
 
119
- this.res = new this._IncomingMessage(this.socket, headers, { statusCode: parseInt(statusCode, 10), statusMessage })
120
-
121
125
  this.req.on('close', () => { this.req = null })
126
+
127
+ this.res = new this._IncomingMessage(this.socket, headers, { statusCode: parseInt(statusCode, 10), statusMessage: statusMessage.join(' ') })
128
+
122
129
  this.res.on('close', () => { this.res = null; this._onreset() })
123
130
 
131
+ if (headers.connection && headers.connection.toLowerCase() === 'upgrade') {
132
+ const head = this._buffer
133
+ this._buffer = null
134
+ return this._onupgrade(head)
135
+ }
136
+
124
137
  this.req.emit('response', this.res)
125
138
 
126
139
  if (headers['transfer-encoding'] === 'chunked') {
@@ -163,6 +176,24 @@ module.exports = class HTTPClientConnection {
163
176
  if (this._read === this._length) this._onfinished()
164
177
  }
165
178
 
179
+ _onupgrade (head) {
180
+ this.socket
181
+ .off('error', this._onerror)
182
+ .off('close', this._onclose)
183
+ .off('end', this._onend)
184
+ .off('data', this._ondata)
185
+ .off('drain', this._ondrain)
186
+
187
+ const req = this.req
188
+
189
+ req.upgrade = true
190
+ req.destroy()
191
+
192
+ if (req.emit('upgrade', this.res, this.socket, head)) return
193
+
194
+ this.socket.destroy()
195
+ }
196
+
166
197
  _onfinished () {
167
198
  if (this.res) this.res.push(null)
168
199
  if (this.req) this.req._continueFinal()
@@ -178,6 +209,6 @@ module.exports = class HTTPClientConnection {
178
209
  }
179
210
 
180
211
  _ondrain () {
181
- if (this.res) this.res._continueWrite()
212
+ if (this.req) this.req._continueWrite()
182
213
  }
183
214
  }
@@ -22,9 +22,10 @@ module.exports = class HTTPClientRequest extends HTTPOutgoingMessage {
22
22
  const host = opts.host || 'localhost'
23
23
  const port = opts.port || 80
24
24
 
25
- this.headers = { host: host + ':' + port }
25
+ this.headers = { host: host + ':' + port, ...opts.headers }
26
26
 
27
27
  this._connection = connection
28
+ this._chunked = true
28
29
 
29
30
  this._pendingFinal = null
30
31
 
@@ -34,24 +35,63 @@ module.exports = class HTTPClientRequest extends HTTPOutgoingMessage {
34
35
  _header () {
35
36
  let h = `${this.method} ${this.path} HTTP/1.1\r\n`
36
37
 
38
+ let upgrade = false
39
+
37
40
  for (const name of Object.keys(this.headers)) {
38
41
  const n = name.toLowerCase()
39
42
  const v = this.headers[name]
40
43
 
44
+ if (n === 'content-length') this._chunked = false
45
+ if (n === 'connection' && v && v.toLowerCase() === 'upgrade') upgrade = true
46
+
41
47
  h += `${httpCase(n)}: ${v}\r\n`
42
48
  }
43
49
 
50
+ if (upgrade) this._chunked = false
51
+
52
+ if (this._chunked) h += 'Transfer-Encoding: chunked\r\n'
53
+
44
54
  h += '\r\n'
45
55
 
46
56
  return h
47
57
  }
48
58
 
59
+ _write (data, cb) {
60
+ if (this.headersSent === false) this.flushHeaders()
61
+
62
+ if (this._chunked) {
63
+ data = Buffer.concat([
64
+ Buffer.from('' + data.byteLength.toString(16) + '\r\n'),
65
+ data,
66
+ Buffer.from('\r\n')
67
+ ])
68
+ }
69
+
70
+ if (this.socket.write(data)) cb(null)
71
+ else this._pendingWrite = cb
72
+ }
73
+
49
74
  _final (cb) {
50
75
  if (this.headersSent === false) this.flushHeaders()
51
76
 
77
+ if (this._chunked) this.socket.write(Buffer.from('0\r\n\r\n'))
78
+
52
79
  this._pendingFinal = cb
53
80
  }
54
81
 
82
+ _predestroy () {
83
+ if (this.upgrade) return this._continueFinal()
84
+
85
+ this.socket.destroy()
86
+ }
87
+
88
+ _continueWrite () {
89
+ if (this._pendingWrite === null) return
90
+ const cb = this._pendingWrite
91
+ this._pendingWrite = null
92
+ cb(null)
93
+ }
94
+
55
95
  _continueFinal () {
56
96
  if (this._pendingFinal === null) return
57
97
  const cb = this._pendingFinal
@@ -6,6 +6,7 @@ module.exports = class HTTPIncomingMessage extends Readable {
6
6
 
7
7
  this.socket = socket
8
8
  this.headers = headers
9
+ this.upgrade = false
9
10
 
10
11
  // Server arguments
11
12
  this.method = args.method || ''
@@ -33,6 +34,6 @@ module.exports = class HTTPIncomingMessage extends Readable {
33
34
  }
34
35
 
35
36
  _predestroy () {
36
- this.socket.destroy()
37
+ if (this.upgrade === false) this.socket.destroy()
37
38
  }
38
39
  }
@@ -8,6 +8,7 @@ module.exports = class HTTPOutgoingMessage extends Writable {
8
8
  this.socket = socket
9
9
  this.headers = {}
10
10
  this.headersSent = false
11
+ this.upgrade = false
11
12
  }
12
13
 
13
14
  getHeader (name) {
@@ -38,7 +39,7 @@ module.exports = class HTTPOutgoingMessage extends Writable {
38
39
  }
39
40
 
40
41
  _predestroy () {
41
- this.socket.destroy()
42
+ if (this.upgrade === false) this.socket.destroy()
42
43
  }
43
44
  }
44
45
 
@@ -23,10 +23,14 @@ module.exports = class HTTPServerConnection {
23
23
  this._read = 0
24
24
  this._buffer = null
25
25
 
26
+ this._onerror = this._onerror.bind(this)
27
+ this._ondata = this._ondata.bind(this)
28
+ this._ondrain = this._ondrain.bind(this)
29
+
26
30
  socket
27
- .on('error', this._onerror.bind(this))
28
- .on('data', this._ondata.bind(this))
29
- .on('drain', this._ondrain.bind(this))
31
+ .on('error', this._onerror)
32
+ .on('data', this._ondata)
33
+ .on('drain', this._ondrain)
30
34
  }
31
35
 
32
36
  _onerror (err) {
@@ -34,6 +38,8 @@ module.exports = class HTTPServerConnection {
34
38
  }
35
39
 
36
40
  _ondata (data) {
41
+ if (this._state === constants.state.IN_BODY) return this._onbody(data)
42
+
37
43
  if (this._buffer !== null) {
38
44
  this._buffer = Buffer.concat([this._buffer, data])
39
45
  } else {
@@ -49,6 +55,30 @@ module.exports = class HTTPServerConnection {
49
55
  hits++
50
56
  } else if (hits === 1 && b === 10) {
51
57
  hits++
58
+
59
+ if (this._state === constants.state.BEFORE_CHUNK) {
60
+ const head = this._buffer.subarray(0, i - 1)
61
+ this._buffer = i + 1 === this._buffer.byteLength ? null : this._buffer.subarray(i + 1)
62
+ i = 0
63
+ hits = 0
64
+ this._onchunklength(head)
65
+
66
+ if (this._buffer === null) break
67
+ } else if (this._state === constants.state.IN_CHUNK) {
68
+ const chunk = this._buffer.subarray(0, i - 1)
69
+
70
+ if (chunk.byteLength !== this._length) {
71
+ hits = 0
72
+ continue
73
+ }
74
+
75
+ this._buffer = i + 1 === this._buffer.byteLength ? null : this._buffer.subarray(i + 1)
76
+ i = 0
77
+ hits = 0
78
+ this._onchunk(chunk)
79
+
80
+ if (this._buffer === null) break
81
+ }
52
82
  } else if (hits === 2 && b === 13) {
53
83
  hits++
54
84
  } else if (hits === 3 && b === 10) {
@@ -84,14 +114,77 @@ module.exports = class HTTPServerConnection {
84
114
  }
85
115
 
86
116
  this.req = new this._IncomingMessage(this.socket, headers, { method, url })
87
- this.res = new this._ServerResponse(this.socket, this.req, headers.connection === 'close')
88
117
 
89
- this.req.on('close', () => { this.req = null })
90
- this.res.on('close', () => { this.res = null; this._onreset() })
118
+ this.req.on('close', () => { this.req = null; this._onreset() })
91
119
 
92
- this.req.push(null)
120
+ if (headers.connection && headers.connection.toLowerCase() === 'upgrade') {
121
+ const head = this._buffer
122
+ this._buffer = null
123
+ return this._onupgrade(head)
124
+ }
125
+
126
+ this.res = new this._ServerResponse(this.socket, this.req, headers.connection === 'close')
127
+
128
+ this.res.on('close', () => { this.res = null })
93
129
 
94
130
  this.server.emit('request', this.req, this.res)
131
+
132
+ if (headers['transfer-encoding'] === 'chunked') {
133
+ this._state = constants.state.BEFORE_CHUNK
134
+ } else {
135
+ this._length = parseInt(headers['content-length'], 10) || 0
136
+
137
+ if (this._length === 0) return this._onfinished()
138
+
139
+ this._state = constants.state.IN_BODY
140
+
141
+ if (this._buffer) {
142
+ const body = this._buffer
143
+ this._buffer = null
144
+ this._onbody(body)
145
+ }
146
+ }
147
+ }
148
+
149
+ _onchunklength (data) {
150
+ this._length = parseInt(data.toString(), 16)
151
+
152
+ if (this._length === 0) this._onfinished()
153
+ else this._state = constants.state.IN_CHUNK
154
+ }
155
+
156
+ _onchunk (data) {
157
+ this._read += data.byteLength
158
+
159
+ this.req.push(data)
160
+
161
+ this._state = constants.state.BEFORE_CHUNK
162
+ }
163
+
164
+ _onbody (data) {
165
+ this._read += data.byteLength
166
+
167
+ this.req.push(data)
168
+
169
+ if (this._read === this._length) this._onfinished()
170
+ }
171
+
172
+ _onupgrade (head) {
173
+ this.socket
174
+ .off('error', this._onerror)
175
+ .off('data', this._ondata)
176
+ .off('drain', this._ondrain)
177
+
178
+ const req = this.req
179
+
180
+ req.upgrade = true
181
+ req.destroy()
182
+
183
+ this.server.emit('upgrade', req, this.socket, head)
184
+ }
185
+
186
+ _onfinished () {
187
+ if (this.req) this.req.push(null)
95
188
  }
96
189
 
97
190
  _onreset () {
@@ -42,7 +42,7 @@ module.exports = class HTTPServerResponse extends HTTPOutgoingMessage {
42
42
  const v = this.headers[name]
43
43
 
44
44
  if (n === 'content-length') this._chunked = false
45
- if (n === 'connection' && v === 'close') this._close = true
45
+ if (n === 'connection' && v && v.toLowerCase() === 'close') this._close = true
46
46
 
47
47
  h += httpCase(n) + ': ' + v + '\r\n'
48
48
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bare-http1",
3
- "version": "3.3.1",
3
+ "version": "3.5.0",
4
4
  "description": "Native HTTP/1 library for JavaScript",
5
5
  "exports": {
6
6
  ".": "./index.js",
@@ -31,9 +31,6 @@
31
31
  },
32
32
  "devDependencies": {
33
33
  "brittle": "^3.3.0",
34
- "mime-types": "^2.1.35",
35
- "pump": "^3.0.0",
36
- "range-parser": "^1.2.1",
37
34
  "standard": "^17.0.0"
38
35
  }
39
36
  }