@@ -3,7 +3,8 @@ import { logger } from '@libp2p/logger'
3
3
// @ts -expect-error no types
4
4
import toIterable from 'stream-to-it'
5
5
import { ipPortToMultiaddr as toMultiaddr } from '@libp2p/utils/ip-port-to-multiaddr'
6
- import { CLOSE_TIMEOUT } from './constants.js'
6
+ import { CLOSE_TIMEOUT , SOCKET_TIMEOUT } from './constants.js'
7
+ import errCode from 'err-code'
7
8
import type { Socket } from 'net'
8
9
import type { Multiaddr } from '@multiformats/multiaddr'
9
10
import type { MultiaddrConnection } from '@libp2p/interface-connection'
@@ -15,6 +16,8 @@ interface ToConnectionOptions {
15
16
remoteAddr ?: Multiaddr
16
17
localAddr ?: Multiaddr
17
18
signal ?: AbortSignal
19
+ socketInactivityTimeout ?: number
20
+ socketCloseTimeout ?: number
18
21
}
19
22
20
23
/**
@@ -23,6 +26,8 @@ interface ToConnectionOptions {
23
26
*/
24
27
export const toMultiaddrConnection = ( socket : Socket , options ?: ToConnectionOptions ) => {
25
28
options = options ?? { }
29
+ const inactivityTimeout = options . socketInactivityTimeout ?? SOCKET_TIMEOUT
30
+ const closeTimeout = options . socketCloseTimeout ?? CLOSE_TIMEOUT
26
31
27
32
// Check if we are connected on a unix path
28
33
if ( options . listeningAddr ?. getPath ( ) != null ) {
@@ -33,22 +38,51 @@ export const toMultiaddrConnection = (socket: Socket, options?: ToConnectionOpti
33
38
options . localAddr = options . remoteAddr
34
39
}
35
40
41
+ const remoteAddr = options . remoteAddr ?? toMultiaddr ( socket . remoteAddress ?? '' , socket . remotePort ?? '' )
42
+ const { host, port } = remoteAddr . toOptions ( )
36
43
const { sink, source } = toIterable . duplex ( socket )
37
44
45
+ // by default there is no timeout
46
+ // https://nodejs.org/dist/latest-v16.x/docs/api/net.html#socketsettimeouttimeout-callback
47
+ socket . setTimeout ( inactivityTimeout , ( ) => {
48
+ log ( '%s:%s socket read timeout' , host , port )
49
+
50
+ // only destroy with an error if the remote has not sent the FIN message
51
+ let err : Error | undefined
52
+ if ( socket . readable ) {
53
+ err = errCode ( new Error ( 'Socket read timeout' ) , 'ERR_SOCKET_READ_TIMEOUT' )
54
+ }
55
+
56
+ // if the socket times out due to inactivity we must manually close the connection
57
+ // https://nodejs.org/dist/latest-v16.x/docs/api/net.html#event-timeout
58
+ socket . destroy ( err )
59
+ } )
60
+
61
+ socket . once ( 'close' , ( ) => {
62
+ log ( '%s:%s socket closed' , host , port )
63
+
64
+ // In instances where `close` was not explicitly called,
65
+ // such as an iterable stream ending, ensure we have set the close
66
+ // timeline
67
+ if ( maConn . timeline . close == null ) {
68
+ maConn . timeline . close = Date . now ( )
69
+ }
70
+ } )
71
+
72
+ socket . once ( 'end' , ( ) => {
73
+ // the remote sent a FIN packet which means no more data will be sent
74
+ // https://nodejs.org/dist/latest-v16.x/docs/api/net.html#event-end
75
+ log ( 'socket ended' , maConn . remoteAddr . toString ( ) )
76
+ } )
77
+
38
78
const maConn : MultiaddrConnection = {
39
79
async sink ( source ) {
40
80
if ( ( options ?. signal ) != null ) {
41
81
source = abortableSource ( source , options . signal )
42
82
}
43
83
44
84
try {
45
- await sink ( ( async function * ( ) {
46
- for await ( const chunk of source ) {
47
- // Convert BufferList to Buffer
48
- // Sink in StreamMuxer define argument as Uint8Array so chunk type infers as number which can't be sliced
49
- yield Buffer . isBuffer ( chunk ) ? chunk : chunk . slice ( )
50
- }
51
- } ) ( ) )
85
+ await sink ( source )
52
86
} catch ( err : any ) {
53
87
// If aborted we can safely ignore
54
88
if ( err . type !== 'aborted' ) {
@@ -58,66 +92,84 @@ export const toMultiaddrConnection = (socket: Socket, options?: ToConnectionOpti
58
92
log ( err )
59
93
}
60
94
}
95
+
96
+ // we have finished writing, send the FIN message
97
+ socket . end ( )
61
98
} ,
62
99
63
- // Missing Type for "abortable"
64
100
source : ( options . signal != null ) ? abortableSource ( source , options . signal ) : source ,
65
101
66
102
// If the remote address was passed, use it - it may have the peer ID encapsulated
67
- remoteAddr : options . remoteAddr ?? toMultiaddr ( socket . remoteAddress ?? '' , socket . remotePort ?? '' ) ,
103
+ remoteAddr,
68
104
69
105
timeline : { open : Date . now ( ) } ,
70
106
71
107
async close ( ) {
72
- if ( socket . destroyed ) return
108
+ if ( socket . destroyed ) {
109
+ log ( '%s:%s socket was already destroyed when trying to close' , host , port )
110
+ return
111
+ }
73
112
74
- return await new Promise ( ( resolve , reject ) => {
113
+ log ( '%s:%s closing socket' , host , port )
114
+ await new Promise < void > ( ( resolve , reject ) => {
75
115
const start = Date . now ( )
76
116
77
117
// Attempt to end the socket. If it takes longer to close than the
78
118
// timeout, destroy it manually.
79
119
const timeout = setTimeout ( ( ) => {
80
- const { host, port } = maConn . remoteAddr . toOptions ( )
81
- log (
82
- 'timeout closing socket to %s:%s after %dms, destroying it manually' ,
83
- host ,
84
- port ,
85
- Date . now ( ) - start
86
- )
87
-
88
120
if ( socket . destroyed ) {
89
121
log ( '%s:%s is already destroyed' , host , port )
122
+ resolve ( )
90
123
} else {
91
- socket . destroy ( )
92
- }
124
+ log ( '%s:%s socket close timeout after %dms, destroying it manually' , host , port , Date . now ( ) - start )
93
125
94
- resolve ( )
95
- } , CLOSE_TIMEOUT ) . unref ( )
126
+ // will trigger 'error' and 'close' events that resolves promise
127
+ socket . destroy ( errCode ( new Error ( 'Socket close timeout' ) , 'ERR_SOCKET_CLOSE_TIMEOUT' ) )
128
+ }
129
+ } , closeTimeout ) . unref ( )
96
130
97
131
socket . once ( 'close' , ( ) => {
132
+ log ( '%s:%s socket closed' , host , port )
133
+ // socket completely closed
98
134
clearTimeout ( timeout )
99
135
resolve ( )
100
136
} )
101
- socket . end ( ( err ?: Error & { code ?: string } ) => {
102
- clearTimeout ( timeout )
103
- maConn . timeline . close = Date . now ( )
104
- if ( err != null ) {
105
- return reject ( err )
137
+ socket . once ( 'error' , ( err : Error ) => {
138
+ log ( '%s:%s socket error' , host , port , err )
139
+
140
+ // error closing socket
141
+ if ( maConn . timeline . close == null ) {
142
+ maConn . timeline . close = Date . now ( )
106
143
}
107
- resolve ( )
144
+
145
+ if ( socket . destroyed ) {
146
+ clearTimeout ( timeout )
147
+ }
148
+
149
+ reject ( err )
108
150
} )
151
+
152
+ // shorten inactivity timeout
153
+ socket . setTimeout ( closeTimeout )
154
+
155
+ // close writable end of the socket
156
+ socket . end ( )
157
+
158
+ if ( socket . writableLength > 0 ) {
159
+ // there are outgoing bytes waiting to be sent
160
+ socket . once ( 'drain' , ( ) => {
161
+ log ( '%s:%s socket drained' , host , port )
162
+
163
+ // all bytes have been sent we can destroy the socket (maybe) before the timeout
164
+ socket . destroy ( )
165
+ } )
166
+ } else {
167
+ // nothing to send, destroy immediately
168
+ socket . destroy ( )
169
+ }
109
170
} )
110
171
}
111
172
}
112
173
113
- socket . once ( 'close' , ( ) => {
114
- // In instances where `close` was not explicitly called,
115
- // such as an iterable stream ending, ensure we have set the close
116
- // timeline
117
- if ( maConn . timeline . close == null ) {
118
- maConn . timeline . close = Date . now ( )
119
- }
120
- } )
121
-
122
174
return maConn
123
175
}
0 commit comments