11import { once } from 'node:events'
2- import { isIP , isIPv6 , Socket } from 'node:net'
2+ import { request as httpRequest } from 'node:http'
3+ import { request as httpsRequest } from 'node:https'
4+ import { isIPv6 , Socket } from 'node:net'
35import { connect } from 'node:tls'
46import { spawn } from 'node:child_process'
57import AbstractNbdClient from './AbstractNbdClient.mjs'
@@ -14,6 +16,7 @@ import { NBD_DEFAULT_PORT, NBD_OPT_STARTTLS } from './constants.mjs'
1416export default class NbdTcpClient extends AbstractNbdClient {
1517 #httpProxy
1618 #proxyRejectUnauthorized
19+ #proxyTimeout
1720 #serverAddress
1821 #serverCert
1922 #serverPort
@@ -26,18 +29,37 @@ export default class NbdTcpClient extends AbstractNbdClient {
2629 * @param {string } [settings.cert] - PEM certificate, enables TLS when set
2730 * @param {string } [settings.httpProxy] - URL of an HTTP(S) proxy, the connection is tunneled through it with `CONNECT`
2831 * @param {boolean } [settings.proxyRejectUnauthorized=true] - whether to check the certificate of an HTTPS proxy
32+ * @param {number } [settings.proxyTimeout=6e4] - delay in ms after which an unanswered `CONNECT` is aborted
2933 * @param {object } [options] - see {@link AbstractNbdClient}
3034 */
3135 constructor (
32- { address, port = NBD_DEFAULT_PORT , exportname, cert, httpProxy, proxyRejectUnauthorized = true } ,
36+ {
37+ address,
38+ port = NBD_DEFAULT_PORT ,
39+ exportname,
40+ cert,
41+ httpProxy,
42+ proxyRejectUnauthorized = true ,
43+ proxyTimeout = 6e4 ,
44+ } ,
3345 options
3446 ) {
3547 super ( { exportname } , options )
48+ // other proxy protocols (e.g. SOCKS) are not supported for now
49+ if ( httpProxy !== undefined ) {
50+ const { protocol } = new URL ( httpProxy )
51+ if ( protocol !== 'http:' && protocol !== 'https:' ) {
52+ const error = new Error ( `unsupported proxy protocol ${ protocol } , only http: and https: are supported` )
53+ error . code = 'NBD_PROXY_UNSUPPORTED_PROTOCOL'
54+ throw error
55+ }
56+ }
3657 this . #serverAddress = address
3758 this . #serverPort = port
3859 this . #serverCert = cert
3960 this . #httpProxy = httpProxy
4061 this . #proxyRejectUnauthorized = proxyRejectUnauthorized
62+ this . #proxyTimeout = proxyTimeout
4163 }
4264
4365 /**
@@ -50,81 +72,48 @@ export default class NbdTcpClient extends AbstractNbdClient {
5072 async #connectThroughHttpProxy( ) {
5173 const proxy = new URL ( this . #httpProxy)
5274 const isHttps = proxy . protocol === 'https:'
53- // URL keeps the brackets around IPv6 addresses
54- const proxyHost = proxy . hostname . replace ( / ^ \[ ( .* ) \] $ / , '$1' )
55- const proxyPort = Number ( proxy . port ) || ( isHttps ? 443 : 80 )
5675
57- let socket
58- if ( isHttps ) {
59- socket = connect ( {
60- host : proxyHost ,
61- port : proxyPort ,
62- rejectUnauthorized : this . #proxyRejectUnauthorized,
63- // SNI does not support IP addresses
64- servername : isIP ( proxyHost ) === 0 ? proxyHost : undefined ,
65- } )
66- await once ( socket , 'secureConnect' )
67- } else {
68- socket = new Socket ( )
69- socket . connect ( proxyPort , proxyHost )
70- await once ( socket , 'connect' )
76+ const address = this . #serverAddress
77+ const target = `${ isIPv6 ( address ) ? `[${ address } ]` : address } :${ this . #serverPort} `
78+ const headers = { host : target }
79+ if ( proxy . username !== '' || proxy . password !== '' ) {
80+ const credentials = `${ decodeURIComponent ( proxy . username ) } :${ decodeURIComponent ( proxy . password ) } `
81+ headers [ 'proxy-authorization' ] = `Basic ${ Buffer . from ( credentials ) . toString ( 'base64' ) } `
7182 }
7283
73- try {
74- const address = this . #serverAddress
75- const target = `${ isIPv6 ( address ) ? `[${ address } ]` : address } :${ this . #serverPort} `
76- let request = `CONNECT ${ target } HTTP/1.1\r\nHost: ${ target } \r\n`
77- if ( proxy . username !== '' || proxy . password !== '' ) {
78- const credentials = `${ decodeURIComponent ( proxy . username ) } :${ decodeURIComponent ( proxy . password ) } `
79- request += `Proxy-Authorization: Basic ${ Buffer . from ( credentials ) . toString ( 'base64' ) } \r\n`
80- }
81- socket . write ( request + '\r\n' )
82-
83- // read the proxy response headers, the NBD server may already have sent
84- // some data after them
85- const { head, rest } = await new Promise ( ( resolve , reject ) => {
86- let buffer = Buffer . alloc ( 0 )
87- const cleanUp = ( ) => {
88- socket . removeListener ( 'data' , onData )
89- socket . removeListener ( 'end' , onEnd )
90- socket . removeListener ( 'error' , reject )
91- }
92- const onData = chunk => {
93- buffer = Buffer . concat ( [ buffer , chunk ] )
94- const index = buffer . indexOf ( '\r\n\r\n' )
95- if ( index !== - 1 ) {
96- cleanUp ( )
97- socket . pause ( )
98- resolve ( { head : buffer . subarray ( 0 , index ) . toString ( ) , rest : buffer . subarray ( index + 4 ) } )
99- } else if ( buffer . length > 16 * 1024 ) {
100- cleanUp ( )
101- reject ( new Error ( 'HTTP proxy response headers too large' ) )
102- }
103- }
104- const onEnd = ( ) => {
105- cleanUp ( )
106- reject ( new Error ( 'HTTP proxy closed the connection before answering' ) )
107- }
108- socket . on ( 'data' , onData )
109- socket . once ( 'end' , onEnd )
110- socket . once ( 'error' , reject )
111- } )
84+ const req = ( isHttps ? httpsRequest : httpRequest ) ( {
85+ agent : false ,
86+ headers,
87+ // URL keeps the brackets around IPv6 addresses
88+ hostname : proxy . hostname . replace ( / ^ \[ ( .* ) \] $ / , '$1' ) ,
89+ method : 'CONNECT' ,
90+ path : target ,
91+ port : proxy . port ,
92+ rejectUnauthorized : this . #proxyRejectUnauthorized,
93+ timeout : this . #proxyTimeout,
94+ } )
95+ // `timeout` only emits an event, the request must be aborted explicitly
96+ req . on ( 'timeout' , ( ) => {
97+ const error = new Error ( `HTTP proxy did not answer the CONNECT to ${ target } in time` )
98+ error . code = 'NBD_PROXY_CONNECT_TIMEOUT'
99+ req . destroy ( error )
100+ } )
101+ req . end ( )
112102
113- const statusLine = head . split ( '\r\n' , 1 ) [ 0 ]
114- const statusCode = Number ( statusLine . split ( ' ' ) [ 1 ] )
115- if ( statusCode !== 200 ) {
116- const error = new Error ( `HTTP proxy refused to connect to ${ target } : ${ statusLine } ` )
117- error . code = 'NBD_PROXY_CONNECT_FAILED'
118- throw error
119- }
120- if ( rest . length !== 0 ) {
121- socket . unshift ( rest )
122- }
123- return socket
124- } catch ( error ) {
103+ // the NBD server may already have sent some data, it is in `head`
104+ const [ res , socket , head ] = await once ( req , 'connect' )
105+ // the tunnel is established, NBD has its own timeouts
106+ socket . setTimeout ( 0 )
107+ if ( res . statusCode !== 200 ) {
125108 socket . destroy ( )
109+ const error = new Error ( `HTTP proxy refused to connect to ${ target } : ${ res . statusCode } ${ res . statusMessage } ` )
110+ error . code = 'NBD_PROXY_CONNECT_FAILED'
126111 throw error
127112 }
113+ if ( head . length !== 0 ) {
114+ socket . unshift ( head )
115+ }
116+ return socket
128117 }
129118
130119 // mandatory , at least to start the handshake: the connection always starts
0 commit comments