bare-http1 3.2.1 → 3.3.1

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,7 +6,9 @@ HTTP/1 library for JavaScript.
6
6
  npm i bare-http1
7
7
  ```
8
8
 
9
- Only HTTP servers at the moment and currently does NOT support server request bodies, but supports most other HTTP features (keep-alive, chunked encoding, etc.) and streaming server responses.
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.
10
+
11
+ Basic HTTP client is supported, but currently it does NOT support keep-alive and protocol negotiation.
10
12
 
11
13
  ## Usage
12
14
 
@@ -23,7 +25,13 @@ const server = http.createServer(function (req, res) {
23
25
  })
24
26
 
25
27
  server.listen(0, function () {
26
- console.log('server is bound on', server.address().port)
28
+ const { port } = server.address()
29
+ console.log('server is bound on', port)
30
+
31
+ const client = http.request({ port }, res => {
32
+ res.on('data', (data) => console.log(data.toString()))
33
+ })
34
+ client.end()
27
35
  })
28
36
  ```
29
37
 
package/index.js CHANGED
@@ -5,6 +5,9 @@ const Server = exports.Server = require('./lib/server')
5
5
  exports.ServerResponse = require('./lib/server-response')
6
6
  exports.ServerConnection = require('./lib/server-connection')
7
7
 
8
+ const Request = exports.ClientRequest = require('./lib/client-request')
9
+ exports.ClientConnection = require('./lib/client-connection')
10
+
8
11
  exports.constants = require('./lib/constants')
9
12
 
10
13
  exports.STATUS_CODES = exports.constants.status // For Node.js compatibility
@@ -12,3 +15,37 @@ exports.STATUS_CODES = exports.constants.status // For Node.js compatibility
12
15
  exports.createServer = function createServer (opts, onrequest) {
13
16
  return new Server(opts, onrequest)
14
17
  }
18
+
19
+ exports.request = function request (url, opts, onresponse) {
20
+ if (typeof opts === 'function') {
21
+ onresponse = opts
22
+ opts = {}
23
+ }
24
+
25
+ if (typeof url === 'string') url = new URL(url)
26
+
27
+ if (URL.isURL(url)) {
28
+ opts = opts ? { ...opts } : {}
29
+
30
+ opts.host = url.hostname
31
+ opts.path = url.pathname + url.search
32
+ opts.port = url.port ? parseInt(url.port, 10) : defaultPort(url)
33
+ } else {
34
+ opts = url
35
+ }
36
+
37
+ return new Request(opts, onresponse)
38
+ }
39
+
40
+ // https://url.spec.whatwg.org/#default-port
41
+ function defaultPort (url) {
42
+ switch (url.protocol) {
43
+ case 'ftp:': return 21
44
+ case 'http':
45
+ case 'ws': return 80
46
+ case 'https':
47
+ case 'wss': return 443
48
+ }
49
+
50
+ return null
51
+ }
@@ -0,0 +1,183 @@
1
+ const HTTPIncomingMessage = require('./incoming-message')
2
+ const constants = require('./constants')
3
+ const errors = require('./errors')
4
+
5
+ module.exports = class HTTPClientConnection {
6
+ constructor (socket, opts = {}) {
7
+ const {
8
+ IncomingMessage = HTTPIncomingMessage
9
+ } = opts
10
+
11
+ this.socket = socket
12
+
13
+ this.req = null
14
+ this.res = null
15
+
16
+ this._IncomingMessage = IncomingMessage
17
+
18
+ this._state = constants.state.BEFORE_HEAD
19
+ this._length = -1
20
+ this._read = 0
21
+ this._buffer = null
22
+
23
+ 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))
29
+ }
30
+
31
+ _onerror (err) {
32
+ if (this.req) this.req.destroy(err)
33
+ }
34
+
35
+ _onclose () {
36
+ if (this.req) this.req._continueFinal()
37
+ }
38
+
39
+ _onend () {
40
+ if (this.req) this.req.destroy(errors.CONNECTION_LOST())
41
+ }
42
+
43
+ _ondata (data) {
44
+ if (this._state === constants.state.IN_BODY) return this._onbody(data)
45
+
46
+ if (this._buffer !== null) {
47
+ this._buffer = Buffer.concat([this._buffer, data])
48
+ } else {
49
+ this._buffer = data
50
+ }
51
+
52
+ let hits = 0
53
+
54
+ for (let i = 0; i < this._buffer.byteLength; i++) {
55
+ const b = this._buffer[i]
56
+
57
+ if (hits === 0 && b === 13) {
58
+ hits++
59
+ } else if (hits === 1 && b === 10) {
60
+ hits++
61
+
62
+ if (this._state === constants.state.BEFORE_CHUNK) {
63
+ const head = this._buffer.subarray(0, i - 1)
64
+ this._buffer = i + 1 === this._buffer.byteLength ? null : this._buffer.subarray(i + 1)
65
+ i = 0
66
+ hits = 0
67
+ this._onchunklength(head)
68
+
69
+ if (this._buffer === null) break
70
+ } else if (this._state === constants.state.IN_CHUNK) {
71
+ const chunk = this._buffer.subarray(0, i - 1)
72
+
73
+ if (chunk.byteLength !== this._length) {
74
+ hits = 0
75
+ continue
76
+ }
77
+
78
+ this._buffer = i + 1 === this._buffer.byteLength ? null : this._buffer.subarray(i + 1)
79
+ i = 0
80
+ hits = 0
81
+ this._onchunk(chunk)
82
+
83
+ if (this._buffer === null) break
84
+ }
85
+ } else if (hits === 2 && b === 13) {
86
+ hits++
87
+ } else if (hits === 3 && b === 10) {
88
+ if (this._state === constants.state.BEFORE_HEAD) {
89
+ const head = this._buffer.subarray(0, i - 3)
90
+ this._buffer = i + 1 === this._buffer.byteLength ? null : this._buffer.subarray(i + 1)
91
+ i = 0
92
+ hits = 0
93
+ this._onhead(head)
94
+
95
+ if (this._buffer === null) break
96
+ }
97
+ } else {
98
+ hits = 0
99
+ }
100
+ }
101
+ }
102
+
103
+ _onhead (data) {
104
+ this._state = constants.state.IN_HEAD
105
+
106
+ const r = data.toString().split('\r\n')
107
+ if (r.length === 0) return this.socket.destroy()
108
+
109
+ const [, statusCode, statusMessage] = r[0].split(' ')
110
+ if (!statusCode || !statusMessage) return this.socket.destroy()
111
+
112
+ const headers = {}
113
+
114
+ for (let i = 1; i < r.length; i++) {
115
+ const [name, value] = r[i].split(': ')
116
+ headers[name.toLowerCase()] = value
117
+ }
118
+
119
+ this.res = new this._IncomingMessage(this.socket, headers, { statusCode: parseInt(statusCode, 10), statusMessage })
120
+
121
+ this.req.on('close', () => { this.req = null })
122
+ this.res.on('close', () => { this.res = null; this._onreset() })
123
+
124
+ this.req.emit('response', this.res)
125
+
126
+ if (headers['transfer-encoding'] === 'chunked') {
127
+ this._state = constants.state.BEFORE_CHUNK
128
+ } else {
129
+ this._length = parseInt(headers['content-length'], 10) || 0
130
+
131
+ if (this._length === 0) return this._onfinished()
132
+
133
+ this._state = constants.state.IN_BODY
134
+
135
+ if (this._buffer) {
136
+ const body = this._buffer
137
+ this._buffer = null
138
+ this._onbody(body)
139
+ }
140
+ }
141
+ }
142
+
143
+ _onchunklength (data) {
144
+ this._length = parseInt(data.toString(), 16)
145
+
146
+ if (this._length === 0) this._onfinished()
147
+ else this._state = constants.state.IN_CHUNK
148
+ }
149
+
150
+ _onchunk (data) {
151
+ this._read += data.byteLength
152
+
153
+ this.res.push(data)
154
+
155
+ this._state = constants.state.BEFORE_CHUNK
156
+ }
157
+
158
+ _onbody (data) {
159
+ this._read += data.byteLength
160
+
161
+ this.res.push(data)
162
+
163
+ if (this._read === this._length) this._onfinished()
164
+ }
165
+
166
+ _onfinished () {
167
+ if (this.res) this.res.push(null)
168
+ if (this.req) this.req._continueFinal()
169
+
170
+ this.socket.end()
171
+ }
172
+
173
+ _onreset () {
174
+ this._state = constants.state.BEFORE_HEAD
175
+ this._length = -1
176
+ this._read = 0
177
+ this._buffer = null
178
+ }
179
+
180
+ _ondrain () {
181
+ if (this.res) this.res._continueWrite()
182
+ }
183
+ }
@@ -0,0 +1,69 @@
1
+ const tcp = require('bare-tcp')
2
+ const HTTPOutgoingMessage = require('./outgoing-message')
3
+ const HTTPClientConnection = require('./client-connection')
4
+
5
+ module.exports = class HTTPClientRequest extends HTTPOutgoingMessage {
6
+ constructor (opts = {}, onresponse = null) {
7
+ if (typeof opts === 'function') {
8
+ onresponse = opts
9
+ opts = {}
10
+ }
11
+
12
+ const {
13
+ connection = new HTTPClientConnection(tcp.createConnection(opts))
14
+ } = opts
15
+
16
+ super(connection.socket)
17
+
18
+ connection.req = this
19
+
20
+ this.method = opts.method || 'GET'
21
+ this.path = opts.path || '/'
22
+ const host = opts.host || 'localhost'
23
+ const port = opts.port || 80
24
+
25
+ this.headers = { host: host + ':' + port }
26
+
27
+ this._connection = connection
28
+
29
+ this._pendingFinal = null
30
+
31
+ if (onresponse) this.once('response', onresponse)
32
+ }
33
+
34
+ _header () {
35
+ let h = `${this.method} ${this.path} HTTP/1.1\r\n`
36
+
37
+ for (const name of Object.keys(this.headers)) {
38
+ const n = name.toLowerCase()
39
+ const v = this.headers[name]
40
+
41
+ h += `${httpCase(n)}: ${v}\r\n`
42
+ }
43
+
44
+ h += '\r\n'
45
+
46
+ return h
47
+ }
48
+
49
+ _final (cb) {
50
+ if (this.headersSent === false) this.flushHeaders()
51
+
52
+ this._pendingFinal = cb
53
+ }
54
+
55
+ _continueFinal () {
56
+ if (this._pendingFinal === null) return
57
+ const cb = this._pendingFinal
58
+ this._pendingFinal = null
59
+ cb(null)
60
+ }
61
+ }
62
+
63
+ function httpCase (n) {
64
+ let s = ''
65
+ for (const part of n.split('-')) {
66
+ s += (s ? '-' : '') + part.slice(0, 1).toUpperCase() + part.slice(1)
67
+ }
68
+ return s
69
+ }
package/lib/constants.js CHANGED
@@ -1,4 +1,11 @@
1
1
  module.exports = {
2
+ state: {
3
+ BEFORE_HEAD: 1,
4
+ IN_HEAD: 2,
5
+ IN_BODY: 3,
6
+ BEFORE_CHUNK: 4,
7
+ IN_CHUNK: 5
8
+ },
2
9
  status: {
3
10
  100: 'Continue',
4
11
  101: 'Switching Protocols',
package/lib/errors.js ADDED
@@ -0,0 +1,22 @@
1
+ module.exports = class HTTPError extends Error {
2
+ constructor (msg, code, fn = HTTPError) {
3
+ super(`${code}: ${msg}`)
4
+ this.code = code
5
+
6
+ if (Error.captureStackTrace) {
7
+ Error.captureStackTrace(this, fn)
8
+ }
9
+ }
10
+
11
+ get name () {
12
+ return 'HTTPError'
13
+ }
14
+
15
+ static NOT_IMPLEMENTED (msg = 'Method not implemented') {
16
+ return new HTTPError(msg, 'NOT_IMPLEMENTED', HTTPError.NOT_IMPLEMENTED)
17
+ }
18
+
19
+ static CONNECTION_LOST (msg = 'Socket hung up') {
20
+ return new HTTPError(msg, 'CONNECTION_LOST', HTTPError.CONNECTION_LOST)
21
+ }
22
+ }
@@ -1,15 +1,19 @@
1
1
  const { Readable } = require('bare-stream')
2
2
 
3
3
  module.exports = class HTTPIncomingMessage extends Readable {
4
- constructor (socket, method, url, headers) {
4
+ constructor (socket, headers, args = {}) {
5
5
  super()
6
6
 
7
7
  this.socket = socket
8
- this.method = method
9
- this.url = url
10
8
  this.headers = headers
11
9
 
12
- this.push(null)
10
+ // Server arguments
11
+ this.method = args.method || ''
12
+ this.url = args.url || ''
13
+
14
+ // Client arguments
15
+ this.statusCode = args.statusCode || 0
16
+ this.statusMessage = args.statusMessage || ''
13
17
  }
14
18
 
15
19
  get httpVersion () {
@@ -1,13 +1,11 @@
1
1
  const { Writable } = require('bare-stream')
2
- const constants = require('./constants')
2
+ const errors = require('./errors')
3
3
 
4
4
  module.exports = class HTTPOutgoingMessage extends Writable {
5
5
  constructor (socket) {
6
- super({ map: mapToBuffer })
6
+ super({ mapWritable })
7
7
 
8
8
  this.socket = socket
9
- this.statusCode = 200
10
- this.statusMessage = null
11
9
  this.headers = {}
12
10
  this.headersSent = false
13
11
  }
@@ -31,39 +29,19 @@ module.exports = class HTTPOutgoingMessage extends Writable {
31
29
  flushHeaders () {
32
30
  if (this.headersSent === true) return
33
31
 
34
- let h = 'HTTP/1.1 ' + this.statusCode + ' ' + (this.statusMessage === null ? constants.status[this.statusCode] : this.statusMessage) + '\r\n'
35
-
36
- for (const name of Object.keys(this.headers)) {
37
- const n = name.toLowerCase()
38
- const v = this.headers[name]
39
-
40
- if (n === 'content-length') this._chunked = false
41
- if (n === 'connection' && v === 'close') this._close = true
42
-
43
- h += httpCase(n) + ': ' + v + '\r\n'
44
- }
45
-
46
- if (this._chunked) h += 'Transfer-Encoding: chunked\r\n'
47
-
48
- h += '\r\n'
49
-
50
- this.socket.write(Buffer.from(h))
32
+ this.socket.write(Buffer.from(this._header()))
51
33
  this.headersSent = true
52
34
  }
53
35
 
54
- _predestroy () {
55
- this.socket.destroy()
36
+ _header () {
37
+ throw errors.NOT_IMPLEMENTED()
56
38
  }
57
- }
58
39
 
59
- function httpCase (n) {
60
- let s = ''
61
- for (const part of n.split('-')) {
62
- s += (s ? '-' : '') + part.slice(0, 1).toUpperCase() + part.slice(1)
40
+ _predestroy () {
41
+ this.socket.destroy()
63
42
  }
64
- return s
65
43
  }
66
44
 
67
- function mapToBuffer (b) {
68
- return typeof b === 'string' ? Buffer.from(b) : b
45
+ function mapWritable (data) {
46
+ return typeof data === 'string' ? Buffer.from(data) : data
69
47
  }
@@ -1,5 +1,6 @@
1
1
  const HTTPIncomingMessage = require('./incoming-message')
2
2
  const HTTPServerResponse = require('./server-response')
3
+ const constants = require('./constants')
3
4
 
4
5
  module.exports = class HTTPServerConnection {
5
6
  constructor (server, socket, opts = {}) {
@@ -8,15 +9,18 @@ module.exports = class HTTPServerConnection {
8
9
  ServerResponse = HTTPServerResponse
9
10
  } = opts
10
11
 
11
- this._server = server
12
- this._socket = socket
12
+ this.server = server
13
+ this.socket = socket
14
+
15
+ this.req = null
16
+ this.res = null
13
17
 
14
18
  this._IncomingMessage = IncomingMessage
15
19
  this._ServerResponse = ServerResponse
16
20
 
17
- this._requests = new Set()
18
- this._responses = new Set()
19
-
21
+ this._state = constants.state.BEFORE_HEAD
22
+ this._length = -1
23
+ this._read = 0
20
24
  this._buffer = null
21
25
 
22
26
  socket
@@ -26,7 +30,7 @@ module.exports = class HTTPServerConnection {
26
30
  }
27
31
 
28
32
  _onerror (err) {
29
- this._socket.destroy(err)
33
+ this.socket.destroy(err)
30
34
  }
31
35
 
32
36
  _ondata (data) {
@@ -48,25 +52,29 @@ module.exports = class HTTPServerConnection {
48
52
  } else if (hits === 2 && b === 13) {
49
53
  hits++
50
54
  } else if (hits === 3 && b === 10) {
51
- hits = 0
52
-
53
- const head = this._buffer.subarray(0, i + 1)
54
- this._buffer = i + 1 === this._buffer.byteLength ? null : this._buffer.subarray(i + 1)
55
- this._onrequest(head)
56
-
57
- if (this._buffer === null) break
55
+ if (this._state === constants.state.BEFORE_HEAD) {
56
+ const head = this._buffer.subarray(0, i - 3)
57
+ this._buffer = i + 1 === this._buffer.byteLength ? null : this._buffer.subarray(i + 1)
58
+ i = 0
59
+ hits = 0
60
+ this._onhead(head)
61
+
62
+ if (this._buffer === null) break
63
+ }
58
64
  } else {
59
65
  hits = 0
60
66
  }
61
67
  }
62
68
  }
63
69
 
64
- _onrequest (head) {
65
- const r = head.toString().trim().split('\r\n')
66
- if (r.length === 0) return this._socket.destroy()
70
+ _onhead (data) {
71
+ this._state = constants.state.IN_HEAD
72
+
73
+ const r = data.toString().split('\r\n')
74
+ if (r.length === 0) return this.socket.destroy()
67
75
 
68
76
  const [method, url] = r[0].split(' ')
69
- if (!method || !url) return this._socket.destroy()
77
+ if (!method || !url) return this.socket.destroy()
70
78
 
71
79
  const headers = {}
72
80
 
@@ -75,19 +83,25 @@ module.exports = class HTTPServerConnection {
75
83
  headers[name.toLowerCase()] = value
76
84
  }
77
85
 
78
- const req = new this._IncomingMessage(this._socket, method, url, headers)
79
- const res = new this._ServerResponse(this._socket, req, headers.connection === 'close')
86
+ this.req = new this._IncomingMessage(this.socket, headers, { method, url })
87
+ this.res = new this._ServerResponse(this.socket, this.req, headers.connection === 'close')
88
+
89
+ this.req.on('close', () => { this.req = null })
90
+ this.res.on('close', () => { this.res = null; this._onreset() })
80
91
 
81
- this._requests.add(req)
82
- this._responses.add(res)
92
+ this.req.push(null)
83
93
 
84
- req.on('close', () => this._requests.delete(req))
85
- res.on('close', () => this._responses.delete(res))
94
+ this.server.emit('request', this.req, this.res)
95
+ }
86
96
 
87
- this._server.emit('request', req, res)
97
+ _onreset () {
98
+ this._state = constants.state.BEFORE_HEAD
99
+ this._length = -1
100
+ this._read = 0
101
+ this._buffer = null
88
102
  }
89
103
 
90
104
  _ondrain () {
91
- for (const res of this._responses) res._continueWrite()
105
+ if (this.res) this.res._continueWrite()
92
106
  }
93
107
  }
@@ -1,4 +1,5 @@
1
1
  const HTTPOutgoingMessage = require('./outgoing-message')
2
+ const constants = require('./constants')
2
3
 
3
4
  module.exports = class HTTPServerResponse extends HTTPOutgoingMessage {
4
5
  constructor (socket, req, close) {
@@ -6,6 +7,9 @@ module.exports = class HTTPServerResponse extends HTTPOutgoingMessage {
6
7
 
7
8
  this.req = req
8
9
 
10
+ this.statusCode = 200
11
+ this.statusMessage = null
12
+
9
13
  this._chunked = true
10
14
  this._close = close
11
15
  this._finishing = false
@@ -30,6 +34,26 @@ module.exports = class HTTPServerResponse extends HTTPOutgoingMessage {
30
34
  this.headers = headers || {}
31
35
  }
32
36
 
37
+ _header () {
38
+ let h = 'HTTP/1.1 ' + this.statusCode + ' ' + (this.statusMessage === null ? constants.status[this.statusCode] : this.statusMessage) + '\r\n'
39
+
40
+ for (const name of Object.keys(this.headers)) {
41
+ const n = name.toLowerCase()
42
+ const v = this.headers[name]
43
+
44
+ if (n === 'content-length') this._chunked = false
45
+ if (n === 'connection' && v === 'close') this._close = true
46
+
47
+ h += httpCase(n) + ': ' + v + '\r\n'
48
+ }
49
+
50
+ if (this._chunked) h += 'Transfer-Encoding: chunked\r\n'
51
+
52
+ h += '\r\n'
53
+
54
+ return h
55
+ }
56
+
33
57
  _write (data, cb) {
34
58
  if (this.headersSent === false) {
35
59
  if (this._finishing) {
@@ -78,3 +102,11 @@ module.exports = class HTTPServerResponse extends HTTPOutgoingMessage {
78
102
  cb(null)
79
103
  }
80
104
  }
105
+
106
+ function httpCase (n) {
107
+ let s = ''
108
+ for (const part of n.split('-')) {
109
+ s += (s ? '-' : '') + part.slice(0, 1).toUpperCase() + part.slice(1)
110
+ }
111
+ return s
112
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bare-http1",
3
- "version": "3.2.1",
3
+ "version": "3.3.1",
4
4
  "description": "Native HTTP/1 library for JavaScript",
5
5
  "exports": {
6
6
  ".": "./index.js",
@@ -30,7 +30,6 @@
30
30
  "bare-tcp": "^1.1.2"
31
31
  },
32
32
  "devDependencies": {
33
- "bare-subprocess": "^2.0.0",
34
33
  "brittle": "^3.3.0",
35
34
  "mime-types": "^2.1.35",
36
35
  "pump": "^3.0.0",