├── .gitignore ├── .npmignore ├── CHANGES ├── LICENSE ├── README.md ├── bin └── ntftp.js ├── examples ├── client │ ├── copy-remote-file.js │ └── streams.js ├── server │ ├── default-listener-deny-put.js │ ├── graceful-shutdown.js │ ├── no-pipe.js │ ├── proxy-http.js │ └── reuse-default-listener.js ├── user-extensions-authentication.js ├── user-extensions-resume.js └── user-extensions.js ├── lib ├── client.js ├── create-options.js ├── index.js ├── normalize-filename.js ├── protocol │ ├── client │ │ ├── client-request.js │ │ ├── reader.js │ │ └── writer.js │ ├── errors.js │ ├── inherit.js │ ├── known-extensions.js │ ├── opcodes.js │ ├── packets │ │ ├── ack.js │ │ ├── data.js │ │ ├── error.js │ │ ├── index.js │ │ ├── oack.js │ │ ├── read-request.js │ │ ├── read-string.js │ │ ├── rrq.js │ │ ├── write-request.js │ │ └── wrq.js │ ├── reader.js │ ├── request.js │ ├── server │ │ ├── incoming-request.js │ │ ├── reader.js │ │ └── writer.js │ └── writer.js ├── server.js └── streams │ ├── client │ ├── get-stream.js │ └── put-stream.js │ └── server │ ├── get-stream.js │ └── put-stream.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | test -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | CHANGES 2 | examples 3 | test 4 | .npmignore -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | v0.1.2 (13 Sep 2015) 2 | Fixed #14. 3 | 4 | v0.1.1 (03 Apr 2015) 5 | Fixed bug in "abort()". 6 | 7 | v0.1.0 (20 Feb 2014) 8 | Server: Added "host" and "port" public properties. 9 | Bin: Improved server logging messages. 10 | Bin: Updated status-bar dependency. 11 | 12 | v0.0.18 (18 Feb 2014) 13 | Client: DNS lookup errors now return a descriptive message. 14 | Server: The root directory is now validated when "listen()" is called. 15 | Server: Better error handling in the default "requestListener". 16 | Server: When a file is being PUTing, another file cannot GET it (ECURPUT), and 17 | vice versa (ECURGET). 18 | Bin: The "root" directory of the "--listen" option is now mandatory. 19 | Bin: Minor bugfixes. 20 | 21 | v0.0.17 (08 Feb 2014) 22 | Now the "abort" event doesn't return any error. 23 | Client: Removed "highWaterMark" option from all the functions. This option is 24 | in fact the "blockSize" option. The "highWaterMark" is only applicable in 25 | the other stream that pushes data to the PutStream, and is not very useful. 26 | Client: Convert the md5 and sha1 sums to lower case before comparing. 27 | Client: Renamed "md5sum" and "sha1sum" options to "md5" and "sha1". 28 | 29 | v0.0.16 (05 Feb 2014) 30 | Server: Major internal refactor. The only noticable change in the public API 31 | is the removal of the "stats" event in favor of a "stats" object attached to 32 | the "req" parameter. This is a way more simpler to use the API because now 33 | you have immediate access to the socket (ie. logging purposes). 34 | Removed the "file" property from the "stats" object. 35 | The "userExtensions" now defaults to an empty object instead of null. 36 | Server: Minor bugfixes and improvements. 37 | 38 | v0.0.15 (28 Jan 2014) 39 | Client: Fixed "put()". 40 | 41 | v0.0.14 (27 Jan 2014) 42 | Server: Added minor validations. 43 | Server: Minor bugfixes. 44 | Added "close()". 45 | 46 | v0.0.13 (26 Jan 2014) 47 | Server: Deny concurrent PUT requests over the same file. 48 | Server: The "stats" event is now emitted in a future tick. This allows to 49 | call the default request listener after the "stats" event and allows to 50 | create the stream chaining based on the values of the "stats" object. 51 | Server: Added "setSize()". It must be used with custom request listeners to 52 | set the size of the response when is a GET request. 53 | Better maximum size error handling. 54 | Server: Exposed error codes. 55 | Server: Now the server doesn't check for initial errors. As soon as a new 56 | request starts, it creates the streams and emits the "request" event. 57 | Minor improvements. 58 | 59 | v0.0.12 (24 Jan 2014) 60 | Client: Fixed put stream error handling with empty files. 61 | Client: Improved maximum size error handling. 62 | Improved the error checking with DATA and ACK packets. 63 | 64 | v0.0.11 (23 Jan 2014) 65 | Server: Fixed IPv6 socket. 66 | 67 | v0.0.10 (22 Jan 2014) 68 | Allowed error messages with code 0 and no description. 69 | 70 | v0.0.9 (21 Jan 2014) 71 | Client: Added DNS address resolution. 72 | Added automatic UDP socket type creation (IPv4 or IPv6). 73 | Renamed "hostname" option to "host". 74 | Server: When the request is a PUT operation "setUserExtensions()" works 75 | correctly. 76 | 77 | v0.0.8 (21 Jan 2014) 78 | Server: Allowed aborting a request without calling the default request 79 | listener. 80 | Minor internal refactor. 81 | 82 | v0.0.7 (20 Jan 2014) 83 | Bump version. 84 | 85 | v0.0.6 (20 Jan 2014) 86 | Server implemented along with a major refactor of the client. 87 | Fixed the reception of user extensions sent by the server. 88 | Added the server setup in the cli script. 89 | Changed the sockets of the stats object. 90 | Minor tweaks, checks and bugfixes. 91 | 92 | v0.0.5 (16 Jan 2014) 93 | An error message can be passed to "abort()". This is an informative message 94 | that is sent to the server. 95 | Minor bugfixes. 96 | 97 | v0.0.4 (13 Jan 2014) 98 | Added "highWaterMark" option to "get()", "put()", "createGetStream()" and 99 | "createPutStream()". 100 | 101 | v0.0.3 (11 Jan 2014) 102 | If the server sends unknown extensions now the client fails correctly. 103 | The "userExtensions" property has been moved from "createClient()" to "get()", 104 | "put()", "createGetStream()" and "createPutStream()". 105 | Fixed error while retransmitting a bad request. 106 | Properly check if the file is too big to be sent. 107 | Minor improvements. 108 | 109 | v0.0.2 (06 Jan 2014) 110 | Fixed error with the sums. 111 | Don't wait to the callback after sending a packet. 112 | The reader and the writer inherites from the request instead of using events. 113 | Speed improvement (~25% faster). 114 | 115 | v0.0.1 (05 Jan 2014) 116 | First release. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Gabriel Llamas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | tftp 2 | ==== 3 | 4 | #### Streaming TFTP client and server #### 5 | 6 | [![NPM version](https://badge.fury.io/js/tftp.png)](http://badge.fury.io/js/tftp "Fury Version Badge") 7 | [![Dependency Status](https://david-dm.org/gagle/node-tftp.png)](https://david-dm.org/gagle/node-tftp "David Dependency Manager Badge") 8 | 9 | [![NPM installation](https://nodei.co/npm/tftp.png?mini=true)](https://nodei.co/npm/tftp "NodeICO Badge") 10 | 11 | Full-featured streaming TFTP client and server. It supports most of the RFCs: 12 | 13 | - [1350 - The TFTP protocol](http://www.ietf.org/rfc/rfc1350.txt) ✓ 14 | - [2347 - Option extension](http://www.ietf.org/rfc/rfc2347.txt) ✓ 15 | - [2348 - Blocksize option](http://www.ietf.org/rfc/rfc2348.txt) ✓ 16 | - [2349 - Timeout Interval and Transfer Size Options](http://www.ietf.org/rfc/rfc2349.txt) ✓ 17 | - [2090 - Multicast option](http://www.ietf.org/rfc/rfc2090.txt) ✗ 18 | - [3617 - Uniform Resource Identifier (URI)](http://www.ietf.org/rfc/rfc3617.txt) ✓ 19 | - [7440 - Windowsize option](https://tools.ietf.org/rfc/rfc7440.txt) ✓ 20 | - [De facto - Rollover option](http://www.compuphase.com/tftp.htm) ✓ 21 | - `mail` and `netascii` transfer modes ✗ 22 | 23 | [CLIENT](#client) | [SERVER](#server) | [Error codes](#error_codes) 24 | 25 | Per se, the TFTP is a lock-step protocol built on top of UDP for transferring files between two machines. It was useful in the past but nowadays it's practically an obsolete legacy protocol useful in a very few scenarios. Without the extensions support, the RFC says that a file bigger than 32MB cannot be transferred. This limit can be incremented to 91.74MB if both machines agree to use a block size of 1468 bytes, the MTU size before IP fragmentation in Ethernet networks. Also, the transfer speed is pretty slow due to the lock-step mechanism, one acknowledgement for each packet. 26 | 27 | However, there are two de facto extensions that can boost the transfer speed and remove the size limit: the rollover and the window. 28 | 29 | This module it's perfectly integrated with Node.js, providing an streaming interface for GETting and PUTing files very easily. No configuration is needed. By default the client tries to negotiate with the server the best possible configuration. If that's not possible it simply fallbacks to the original lock-step TFTP implementation. The server also supports both the enhanced features and the classic lock-step RFCs. 30 | 31 | It can be installed locally and used programmatically, but it can be also installed globally and used directly from the console as a CLI utility. 32 | 33 | #### Special thanks #### 34 | 35 | Patrick Masotta (author of the [Serva](http://www.vercot.com/~serva/) application and the internet draft about the `windowsize` option). 36 | 37 | #### Local environment vs Internet #### 38 | 39 | TFTP runs over UDP, this means that the network packets could be lost before reaching the other side. In local controlled scenarios, the TFTP can be used in a very few cases, but don't pretend to use it over the Internet, use FTP instead. It simply doesn't work because the packets are lost with an amazing ease. 40 | 41 | TFTP is a bad protocol for transferring files because it adds some of features that TCP offers (ack's, retransmission, error detection, reordering, etc.) to the UDP but at the applicaction layer (slower!). Think why you need to use TFTP instead of FTP. In most of the cases you can use FTP and obtain better results. 42 | 43 | 44 | #### Warning! UDP packet loss in Windows #### 45 | 46 | Currently, in Windows there is a problem concerning the buffering of the received network packets ([#6696](https://github.com/joyent/node/issues/6696)). Basically, when the buffer is full, all the subsequent incoming packets are dropped, so they are never consumed by Node.js. This scenario can be reproduced by configuring a window bigger than 6 blocks with the default block size. So the advice is: do NOT increment the default window size (4) in the Windows platform until this bug is solved. 47 | 48 | --- 49 | 50 | ### CLIENT ### 51 | 52 | [_module_.createClient([options]) : Client](#createclient) 53 | 54 | #### Documentation #### 55 | 56 | - [Streams](#client_streams) 57 | - [Global installation](#client_global) 58 | 59 | #### Objects #### 60 | 61 | - [Client](#client_object) 62 | - [GetStream and PutStream](#client_getstream_putstream) 63 | 64 | --- 65 | 66 | 67 | __Streams__ 68 | 69 | For the sake of simplicity the following examples omit the error handling. See the [streams.js](https://github.com/gagle/node-tftp/blob/master/examples/client/streams.js) example or the [source code](https://github.com/gagle/node-tftp/blob/master/lib/client.js) of the [get()](#client-get) and [put()](#client-put) functions for more information. 70 | 71 | __GET remote > local__ 72 | 73 | ```javascript 74 | var get = client.createGetStream ("remote-file"); 75 | var write = fs.createWriteStream ("local-file"); 76 | 77 | get.pipe (write); 78 | ``` 79 | 80 | __PUT local > remote__ 81 | 82 | ```javascript 83 | var read = fs.createReadStream ("local-file"); 84 | var put = client.createPutStream ("remote-file", { size: totalSize }); 85 | 86 | read.pipe (put); 87 | ``` 88 | 89 | --- 90 | 91 | 92 | __Global installation__ 93 | 94 | ``` 95 | npm install tftp -g 96 | ``` 97 | 98 | Then you can access to the `ntftp` binary. 99 | 100 | There are basically two ways to use it: with or without a shell. 101 | 102 | __Without a shell__ 103 | 104 | Best for individual transfers. 105 | 106 | ``` 107 | $ ntftp get [options] [] 108 | $ ntftp put [options] [] 109 | ``` 110 | 111 | For example: 112 | 113 | ``` 114 | $ ntftp get tftp://localhost/remote-file 115 | remote-file 42.2 MiB 32.6M/s 00:12 [###·····················] 13% 116 | ``` 117 | 118 | ``` 119 | $ ntftp put my/local-file tftp://localhost/remote-file 120 | my/local-file 148.8 MiB 30.9M/s 00:07 [###########·············] 45% 121 | ``` 122 | 123 | For more information type `ntftp get|put -h`. 124 | 125 | __With a shell__ 126 | 127 | Best for multiple transfers, basically because the same server address and options are reused. 128 | 129 | ``` 130 | $ ntftp [options] [:] 131 | ``` 132 | 133 | For example: 134 | 135 | ``` 136 | $ ntftp localhost 137 | > get remote-file 138 | remote-file 42.2 MiB 32.6M/s 00:12 [###·····················] 13% 139 | > put my/local-file remote-file 140 | my/local-file 148.8 MiB 30.9M/s 00:07 [###########·············] 45% 141 | ``` 142 | 143 | For more information type `ntftp -h` and `get|put -h`. 144 | 145 | --- 146 | 147 | 148 | ___module_.createClient([options]) : Client__ 149 | 150 | Returns a new [Client](#client_object) instance. 151 | 152 | ```javascript 153 | var client = tftp.createClient ({ 154 | host: "10.10.10.10", 155 | port: 1234 156 | }); 157 | ``` 158 | 159 | Options: 160 | 161 | - __host__ - _String_ 162 | The address. Both IPv4 and IPv6 are allowed as well as a domain name. Default is `localhost` (`127.0.0.1`). 163 | - __port__ - _Number_ 164 | The port. Default is 69. 165 | - __blockSize__ - _Number_ 166 | The size of the DATA blocks. Valid range: [8, 65464]. Default is 1468, the MTU size before IP fragmentation in Ethernet networks. 167 | - __windowSize__ - _Number_ 168 | The size of each window. The window size means the number of blocks that can be sent/received without waiting an acknowledgement. Valid range: [1, 65535]. Default is 4. 169 | 170 | Comparison of transfer times: 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 |
Window sizeImprovement
1-0%
2-49%
3-64%
4-70%
5-73%
6-76%
181 | 182 | Take into account that with a bigger window more elements must be reordered (remember that UDP doesn't reorder the incoming packets). This doesn't slow down the transfer speed very much but it requires more CPU. A window size of 4 is a good trade between transfer speed and CPU usage. 183 | 184 | Right now a window size of 6 is the maximum in Windows due to the [packet loss](#udploss) issue. With a window size of 7 or greater a lot of timeouts and retransmissions begin to occur, so the recommendation is to use a window size of 4, the default value. 185 | - __retries__ - _Number_ 186 | How many retries must be done before emitting an error. Default is 3. 187 | - __timeout__ - _Number_ 188 | Milliseconds to wait before a retry. Default is 3000. 189 | 190 | --- 191 | 192 | 193 | __Client__ 194 | 195 | Each of the following methods take an `options` parameter. One option available is `userExtensions`, an object with properties that can be sent with a GET or PUT operation. For example: 196 | 197 | ```javascript 198 | var options = { 199 | userExtensions: { 200 | foo: "bar", 201 | num: 2 202 | } 203 | }; 204 | 205 | client.get ("file", options, function (){ ... }); 206 | client.put ("file", options, function (){ ... }); 207 | client.createGetStream ("file", options); 208 | client.createPutStream ("file", options); 209 | ``` 210 | 211 | The server may ignore or not these extensions. This feature is server-dependent. Please note that the TFTP algorithm cannot be modified. For example, you can implement a basic authentication; the client could send the extensions `user` and `password` and the server could validate the user and accept or deny the request. The extensions are transmitted in plain text. 212 | 213 | The extensions `timeout`, `tsize`, `blksize`, `windowsize` and `rollover` are reserved and cannot be used. 214 | 215 | __Methods__ 216 | 217 | - [Client#createGetStream(remoteFile[, options]) : GetStream](#client_creategetstream) 218 | - [Client#createPutStream(remoteFile, options) : PutStream](#client_createputstream) 219 | - [Client#get(remoteFile[, localFile][, options], callback) : undefined](#client_get) 220 | - [Client#put(localFile[, remoteFile][, options], callback) : undefined](#client_put) 221 | 222 | 223 | __Client#createGetStream(remoteFile[, options]) : GetStream__ 224 | 225 | Returns a new [GetStream](#client_getstream_putstream) instance. 226 | 227 | Options: 228 | 229 | - __md5__ - _String_ 230 | MD5 sum for validating the integrity of the file. 231 | - __sha1__ - _String_ 232 | SHA1 sum for validating the integrity of the file. 233 | - __userExtensions__ - _Object_ 234 | Custom extensions to send with the request. [More information](#client_object). 235 | 236 | ```javascript 237 | var get = client.createGetStream ("file"); 238 | ``` 239 | 240 | 241 | __Client#createPutStream(remoteFile, options) : PutStream__ 242 | 243 | Returns a new [PutStream](#client_getstream_putstream) instance. 244 | 245 | Options: 246 | 247 | - __size__ - _String_ 248 | Total size of the file to upload. This option is required. 249 | - __userExtensions__ - _Object_ 250 | Custom extensions to send with the request. [More information](#client). 251 | 252 | ```javascript 253 | var put = client.createPutStream ("file", { size: 1234 }); 254 | ``` 255 | 256 | 257 | __Client#get(remoteFile[, localFile][, options], callback) : undefined__ 258 | 259 | Downloads a file from the server. If the local filename is missing, the basename of the remote file is used. 260 | 261 | Options: 262 | 263 | - __md5__ - _String_ 264 | MD5 sum for validating the integrity of the file. 265 | - __sha1__ - _String_ 266 | SHA1 sum for validating the integrity of the file. 267 | - __userExtensions__ - _Object_ 268 | Custom extensions to send with the request. [More information](#client). 269 | 270 | ```javascript 271 | //tftp:///dir/to/remote-file -> ./file 272 | client.get ("dir/to/remote-file", function (error){ 273 | if (error) return console.error (error); 274 | ... 275 | }); 276 | ``` 277 | 278 | 279 | __Client#put(localFile[, remoteFile][, options], callback) : undefined__ 280 | 281 | Uploads a file to the server. If the remote filename is missing the basename of the local file is used. 282 | 283 | Options: 284 | 285 | - __userExtensions__ - _Object_ 286 | Custom extensions to send with the request. [More information](#client). 287 | 288 | ```javascript 289 | //./dir/to/local-file -> tftp:///file 290 | client.put ("dir/to/local-file", function (error){ 291 | if (error) return console.error (error); 292 | ... 293 | }); 294 | ``` 295 | 296 | --- 297 | 298 | 299 | __GetStream and PutStream__ 300 | 301 | The GetStream inherits from a Readable stream and the PutStream from a Writable stream. 302 | 303 | __Events__ 304 | 305 | - [abort](#client_event_abort) 306 | - [close](#client_event_close) 307 | - [end](#client_event_end) 308 | - [error](#client_event_error) 309 | - [finish](#client_event_finish) 310 | - [stats](#client_event_stats) 311 | 312 | __Methods__ 313 | 314 | - [abort([error]) : undefined](#client_getstream_putstream_abort) 315 | - [close() : undefined](#client_getstream_putstream_close) 316 | 317 | --- 318 | 319 | 320 | __abort__ 321 | 322 | Arguments: none. 323 | 324 | Emitted when the transfer has been aborted after calling to [abort()](#client_getstream_putstream_abort). 325 | 326 | 327 | __close__ 328 | 329 | Arguments: none. 330 | 331 | Emitted when the underlying socket has been closed. It is emitted __always__ and before any other event (`error`, `abort`, `end` and `finish`). 332 | 333 | 334 | __end__ 335 | 336 | Arguments: none. 337 | 338 | Emitted by the GetStream when the file download finishes. 339 | 340 | 341 | __error__ 342 | 343 | Arguments: `error`. 344 | 345 | Emitted when an error occurs. The stream is closed automatically. 346 | 347 | 348 | __finish__ 349 | 350 | Arguments: none. 351 | 352 | Emitted by the PutStream when the file upload finishes. 353 | 354 | 355 | __stats__ 356 | 357 | Arguments: `stats`. 358 | 359 | Emitted after the client has negotiated the best possible configuration. When it is emitted, the file transfer still hasn't begun. 360 | 361 | `stats` is an object similar to this: 362 | 363 | ``` 364 | { 365 | blockSize: 1468, 366 | windowSize: 4, 367 | size: 105757295, 368 | userExtensions: {}, 369 | retries: 3, 370 | timeout: 3000, 371 | localAddress: "0.0.0.0", 372 | localPort: 55146, 373 | remoteAddress: "127.0.0.1", 374 | remotePort: 55147 375 | } 376 | ``` 377 | 378 | When the GetStream emits a `stats` event, the `size` property is not guaranteed to be a Number because the server may not implement the RFC related with file size. The size of the file is obtained during the negotiation but not all the servers are able to negotiate. In these cases the `size` is null. 379 | 380 | The `userExtensions` property holds an object with the custom extensions sent by the server in response to the custom extensions sent with the request. Most of the TFTP servers don't let you respond with custom extensions when in fact this is a feature commented in the RFCs, so unless the TFTP server allows you to respond with custom extensions, this property will be always an empty object. Of course, the server provided by this module supports the user extensions. 381 | 382 | --- 383 | 384 | 385 | __abort([error]) : undefined__ 386 | 387 | Aborts the current transfer. The optional `error` can be an Error instance or any type (it is stringified). If no error message is given, it sends an [EABORT](#error_codes) error. The message is sent to the server but it is not guaranteed that it will reach the other side because TFTP is built on top of UDP and the error messages are not retransmitted, so the packet could be lost. If the message reaches the server, then the transfer is aborted immediately. 388 | 389 | --- 390 | 391 | 392 | __close() : undefined__ 393 | 394 | Closes the current transfer. It's the same as the [abort()](#client_getstream_putstream_abort) function but it doesn't send to the server any message, it just closes the local socket. Note that this will cause the server to start the timeout. The recommended way to interrupt a transfer is using [abort()](#client_getstream_putstream_abort). 395 | 396 | --- 397 | 398 | ### SERVER ### 399 | 400 | [_module_.createServer([options][, requestListener]) : Server](#createserver) 401 | 402 | #### Documentation #### 403 | 404 | - [Error handling](#error_handling) 405 | - [Graceful shutdown](#graceful_shutdown) 406 | - [Global installation](#server_global) 407 | 408 | #### Objects #### 409 | 410 | - [Server](#server_object) 411 | - [GetStream and PutStream](#server_getstream_putstream) 412 | 413 | --- 414 | 415 | 416 | __Error handling__ 417 | 418 | It's very simple. You need to attach two `error` listeners: one for the server and one for the request. If you don't attach an `error` listener, Node.js throws the error and the server just crashes. 419 | 420 | ```javascript 421 | var server = tftp.createServer (...); 422 | 423 | server.on ("error", function (error){ 424 | //Errors from the main socket 425 | //The current transfers are not aborted 426 | console.error (error); 427 | }); 428 | 429 | server.on ("request", function (req, res){ 430 | req.on ("error", function (error){ 431 | //Error from the request 432 | //The connection is already closed 433 | console.error ("[" + req.stats.remoteAddress + ":" + req.stats.remotePort + 434 | "] (" + req.file + ") " + error.message); 435 | }); 436 | }); 437 | ``` 438 | 439 | 440 | __Graceful shutdown__ 441 | 442 | When the server closes the current transfers are not aborted to allow them to finish. If you need to shutdown the server completely, you must abort all the current transfers manually. Look at [this](https://github.com/gagle/node-tftp/blob/master/examples/server/graceful-shutdown.js) example to know how to do it. 443 | 444 | --- 445 | 446 | 447 | __Global installation__ 448 | 449 | 450 | ``` 451 | npm install tftp -g 452 | ``` 453 | 454 | Then you can access to the `ntftp` binary. 455 | 456 | Use the `-l|--listen[=ROOT]` option to start the server. By default the root directory is `.`. 457 | 458 | ``` 459 | $ ntftp [options] [:] -l|--listen=ROOT 460 | ``` 461 | 462 | For example: 463 | 464 | ``` 465 | $ ntftp localhost -l . 466 | ``` 467 | 468 | This command starts a server listening on `localhost:69` and root `.`. 469 | 470 | --- 471 | 472 | 473 | ___module_.createServer([options][, requestListener]) : Server__ 474 | 475 | Returns a new [Server](#server_object) instance. 476 | 477 | ```javascript 478 | var server = tftp.createServer ({ 479 | host: "10.10.10.10", 480 | port: 1234, 481 | root: "path/to/root/dir", 482 | denyPUT: true 483 | }); 484 | ``` 485 | 486 | The `requestListener` is a function which is automatically attached to the [request](#server_event_request) event. 487 | 488 | Options: 489 | 490 | It has the same options as the [createClient()](#createclient) function with the addition of: 491 | 492 | - __root__ - _String_ 493 | The root directory. Default is `.`. 494 | - __denyGET__ - _Boolean_ 495 | Denies all the GET operations. Default is false. 496 | - __denyPUT__ - _Boolean_ 497 | Denies all the PUT operations. Default is false. 498 | 499 | Setting the options `denyGET` or `denyPUT` is more efficient than aborting the request from inside the request listener. 500 | 501 | --- 502 | 503 | 504 | __Server__ 505 | 506 | __Events__ 507 | 508 | - [close](#server_event_close) 509 | - [error](#server_event_error) 510 | - [listening](#server_event_listening) 511 | - [request](#server_event_request) 512 | 513 | __Methods__ 514 | 515 | - [close() : undefined](#server_close) 516 | - [listen() : undefined](#server_listen) 517 | - [requestListener(req, res) : undefined](#server_requestlistener) 518 | 519 | __Properties__ 520 | 521 | - [host](#server_host) 522 | - [port](#server_port) 523 | - [root](#server_root) 524 | 525 | [__Error codes__](#server_errors) 526 | 527 | --- 528 | 529 | 530 | __close__ 531 | 532 | Arguments: none. 533 | 534 | Emitted when the server closes. New requests are not accepted. Note that the current transfers are not aborted. If you need to abort them gracefully, look at [this](https://github.com/gagle/node-tftp/blob/master/examples/server/graceful-shutdown.js) example. 535 | 536 | 537 | __error__ 538 | 539 | Arguments: `error`. 540 | 541 | Emitted when an error occurs. The error is mostly caused by a bad packet reception, so almost always, if the server emits an error, it is still alive accepting new requests. 542 | 543 | 544 | __listening__ 545 | 546 | Arguments: none. 547 | 548 | Emitted when the server has been bound to the socket after calling to [listen()](#server_listen). 549 | 550 | 551 | __request__ 552 | 553 | Arguments: `req`, `res`. 554 | 555 | Emitted when a new request has been received. All the connection objects that are emitted by this event can be aborted at any time. 556 | 557 | `req` is an instance of a [GetStream](#server_getstream_putstream) and `res` is an instance of a [PutStream](#server_getstream_putstream). 558 | 559 | Requests trying to access a path outside the root directory (eg.: `../file`) are automatically denied. 560 | 561 | Note: If you don't need do anything with the `req` or `res` arguments, that is, if by any reason you don't want to _consume_ the current request, then you __must__ [abort()](#client_getstream_putstream_abort) or [close()](#client_getstream_putstream_close) the connection, otherwise you'll have an open socket for the rest of the server's lifetime. The client will timeout because it won't receive any packet, that's for sure, but the connection in the server will remain open and won't timeout. The timeout retransmissions at the server-side begin when the transfer starts but if you don't read/write/close, the connection won't timeout because it is simply waiting to the user to do something with it. 562 | 563 | --- 564 | 565 | 566 | __close() : undefined__ 567 | 568 | Closes the server and stops accepting new connections. 569 | 570 | --- 571 | 572 | 573 | __listen() : undefined__ 574 | 575 | Starts accepting new connections. 576 | 577 | --- 578 | 579 | 580 | __requestListener(req, res) : undefined__ 581 | 582 | This function must NOT be called from outside a `request` listener. This function is the default request listener, it automatically handles the GET and PUT requests. 583 | 584 | --- 585 | 586 | 587 | __host__ 588 | 589 | The address that the server is listening to. 590 | 591 | --- 592 | 593 | 594 | __port__ 595 | 596 | The port that the server is listening to. 597 | 598 | --- 599 | 600 | 601 | __root__ 602 | 603 | The root path. 604 | 605 | --- 606 | 607 | 608 | __GetStream and PutStream__ 609 | 610 | When the `request` event is emitted, a new GetStream and PutStream instances are created. These streams are similar to the [streams](#client_getstream_putstream) used in the client but with one difference, the GetStream (`req`) acts like a "connection" object. All the events from the PutStream (`res`) are forwarded to the `req` object, so you don't need to attach any event listener to the `res` object. 611 | 612 | The GetStream has two additional properties: 613 | 614 | - __file__ - _String_ 615 | The path of the file. The directories are not created recursively if they don't exist. 616 | - __method__ - _String_ 617 | The transfer's method: `GET` or `PUT`. 618 | - __stats__ - _Object_ 619 | An object holding some stats from the current request. [More information](#client_event_stats). 620 | 621 | The PutStream has two additional methods: 622 | 623 | 624 | - __setSize(size) : undefined__ 625 | 626 | Sets the size of the file to send. You need to call to this method only with GET requests when you're using a custom request listener, otherwise the request will just wait. Look at the examples [no-pipe.js](https://github.com/gagle/node-tftp/blob/master/examples/server/no-pipe.js) and [user-extensions-resume.js](https://github.com/gagle/node-tftp/blob/master/examples/user-extensions-resume.js) for more details. 627 | 628 | - __setUserExtensions(userExtensions) : undefined__ 629 | 630 | Sets the user extensions to send back to the client in response to the received ones. You cannot send extensions different from the ones that are sent by the client. This method must be called before [setSize()](#server_getstream_putstream_setsize). 631 | 632 | As said previously, the TFTP protocol doesn't have any built-in authentication mechanism but thanks to the user extensions you can implement a simple authentication as showed [here](https://github.com/gagle/node-tftp/blob/master/examples/user-extensions-authentication.js). 633 | 634 | Look at the [examples](https://github.com/gagle/node-tftp/tree/master/examples) for more details. 635 | 636 | --- 637 | 638 | 639 | ### Error codes ### 640 | 641 | The following errors are used internally but they are exposed in case you need to use any of them. 642 | 643 | The errors emitted by any `error` event of this module can contain a property named `code`. It contains the name of the error, which is one of the following: 644 | 645 | * _module_.ENOENT - File not found 646 | * _module_.EACCESS - Access violation 647 | * _module_.ENOSPC - Disk full or allocation exceeded 648 | * _module_.EBADOP - Illegal TFTP operation 649 | * _module_.ETID - Unknown transfer ID 650 | * _module_.EEXIST - File already exists 651 | * _module_.ENOUSER - No such user 652 | * _module_.EDENY - The request has been denied 653 | * _module_.ESOCKET - Invalid remote socket 654 | * _module_.EBADMSG - Malformed TFTP message 655 | * _module_.EABORT - Aborted 656 | * _module_.EFBIG - File too big 657 | * _module_.ETIME - Timed out 658 | * _module_.EBADMODE - Invalid transfer mode 659 | * _module_.EBADNAME - Invalid filename 660 | * _module_.EIO - I/O error 661 | * _module_.ENOGET - Cannot GET files 662 | * _module_.ENOPUT - Cannot PUT files 663 | * _module_.ERBIG - Request bigger than 512 bytes 664 | * _module_.ECONPUT - Concurrent PUT request over the same file 665 | * _module_.ECURPUT - The requested file is being written by another request 666 | * _module_.ECURGET - The requested file is being read by another request 667 | -------------------------------------------------------------------------------- /bin/ntftp.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | "use strict"; 4 | 5 | var fs = require ("fs"); 6 | var path = require ("path"); 7 | var readLine = require ("readline"); 8 | var url = require ("url"); 9 | var argp = require ("argp"); 10 | var statusBar = require ("status-bar"); 11 | var tftp = require ("../lib"); 12 | var normalizeFilename = require ("../lib/normalize-filename"); 13 | var errors = require ("../lib/protocol/errors"); 14 | 15 | var client; 16 | var rl; 17 | var timer; 18 | var read; 19 | var write; 20 | var filename; 21 | 22 | var renderStatusBar = function (stats){ 23 | process.stdout.write (filename + " " + 24 | this.format.storage (stats.currentSize) + " " + 25 | this.format.speed (stats.speed) + " " + 26 | this.format.time (stats.remainingTime) + " [" + 27 | this.format.progressBar (stats.percentage) + "] " + 28 | this.format.percentage (stats.percentage)); 29 | process.stdout.cursorTo (0); 30 | }; 31 | 32 | var formatFilename = function (filename){ 33 | //80 - 59 34 | var filenameMaxLength = 21; 35 | if (filename.length > filenameMaxLength){ 36 | filename = filename.slice (0, filenameMaxLength - 3) + "..."; 37 | }else{ 38 | var remaining = filenameMaxLength - filename.length; 39 | while (remaining--){ 40 | filename += " "; 41 | } 42 | } 43 | return filename; 44 | }; 45 | 46 | var parseUri = function (uri){ 47 | var o = url.parse (uri); 48 | if (o.protocol !== "tftp:"){ 49 | return { error: new Error ("The protocol must be 'tftp'") }; 50 | } 51 | if (!o.path){ 52 | return { error: new Error ("Bad uri") }; 53 | } 54 | var arr = o.path.slice (1).split (";mode="); 55 | if (arr[1] && arr[1] !== "octet"){ 56 | return this.fail (new Error ("The transfer mode must be 'octet'")); 57 | } 58 | return { 59 | host: o.host, 60 | port: o.port, 61 | file: arr[0] 62 | }; 63 | }; 64 | 65 | var notifyError = function (error, prompt){ 66 | console.error ("Error: " + error.message); 67 | if (prompt) rl.prompt (); 68 | }; 69 | 70 | var again = function (){ 71 | timer = setTimeout (function (){ 72 | timer = null; 73 | }, 3000); 74 | 75 | console.log ("\n(^C again to quit)"); 76 | rl.line = ""; 77 | rl.prompt (); 78 | }; 79 | 80 | var normalizeGetFiles = function (remote, local){ 81 | remote += ""; 82 | local = (local || remote) + ""; 83 | 84 | try{ 85 | remote = normalizeFilename (remote); 86 | }catch (error){ 87 | throw error; 88 | } 89 | 90 | return { 91 | remote: remote, 92 | local: local 93 | }; 94 | }; 95 | 96 | var normalizePutFiles = function (local, remote){ 97 | local += ""; 98 | remote = (remote || path.basename (local)) + ""; 99 | 100 | try{ 101 | remote = normalizeFilename (remote); 102 | }catch (error){ 103 | throw error; 104 | } 105 | 106 | return { 107 | remote: remote, 108 | local: local 109 | }; 110 | }; 111 | 112 | var setMainOptions = function (body){ 113 | //The default values are set inside the lib 114 | body 115 | .option ({ short: "b", long: "blksize", metavar: "SIZE", 116 | type: Number, description: "Size of each data chunk. Valid range: " + 117 | "[8, 65464]. Default is 1468, the MTU size before IP fragmentation " + 118 | "in Ethernet networks" }) 119 | .option ({ short: "r", long: "retries", metavar: "NUM", 120 | type: Number, description: "Number of retries before aborting the " + 121 | "file transfer due to an unresponsive server or a massive packet " + 122 | "loss" }) 123 | .option ({ short: "t", long: "timeout", metavar: "MILLISECONDS", 124 | type: Number, description: "Milliseconds to wait before the next " + 125 | "retry. Default is 3000ms" }) 126 | .option ({ short: "w", long: "windowsize", metavar: "SIZE", 127 | type: Number, description: "Size of the window. The window will " + 128 | "send SIZE data chunks in a row before waiting for an ack. Valid " + 129 | "range: [1, 65535]. Default is 4" }) 130 | .help () 131 | }; 132 | 133 | //Removing the module from the cache is not necessary because a second instance 134 | //will be used during the whole program lifecycle 135 | var main = argp.createParser () 136 | .main () 137 | .allowUndefinedArguments () 138 | .readPackage (__dirname + "/../package.json", { email: false }) 139 | .usages ([ 140 | "ntftp [options] [:]", 141 | "ntftp get [options] []", 142 | "ntftp put [options] [] ", 143 | ]) 144 | .on ("argument", function (argv, argument, ignore){ 145 | if (argv.server) this.fail ("Too many arguments"); 146 | argument = argument.split (":"); 147 | argv.server = { 148 | host: argument[0], 149 | port: argument[1] 150 | }; 151 | //Don't save the [:port] argument 152 | ignore (); 153 | }) 154 | .on ("end", function (argv){ 155 | if (!argv.server) this.printHelp (); 156 | if (argv.listen){ 157 | createServer (argv); 158 | }else{ 159 | createClient (argv); 160 | } 161 | }) 162 | .body () 163 | .text ("By default, the client sends to the server some de facto " + 164 | "extensions trying to achieve the best performance. If the " + 165 | "server doesn't support these extensions, it automatically " + 166 | "fallbacks to a pure RFC 1350 compliant TFTP client " + 167 | "implementation.\n\n" + 168 | 169 | "This utility can be used with the built-in shell or " + 170 | "directly with a command.\n\n" + 171 | 172 | "Shell:\n" + 173 | " Arguments:") 174 | .columns (" [:]", "The address and port of the " + 175 | "server, e.g.\n$ ntftp localhost:1234") 176 | .text ("\nOnce the shell is running, it shows a prompt and " + 177 | "recognizes the following commands:\n" + 178 | " get, put.\n\n" + 179 | 180 | "' -h' for more information.\n\n" + 181 | 182 | "To quit the program press ctrl-c two times.\n\n" + 183 | 184 | "Example:\n" + 185 | " $ ntftp localhost -w 2 --blksize 256\n" + 186 | " > get remote_file\n" + 187 | " > get remote_file local_file\n" + 188 | " > put path/to/local_file remote_file", " ") 189 | .text ("\nCommands:\n" + 190 | " get, put.\n\n" + 191 | 192 | " 'ntftp -h' for more information.\n\n" + 193 | 194 | "To start a server use the following option:") 195 | .option ({ short: "l", long: "listen", metavar: "ROOT", 196 | description: "Starts the server. The ROOT is the directory " + 197 | "from where the files are served" }) 198 | .text ("\nCommon options:"); 199 | 200 | setMainOptions (main); 201 | 202 | var setCommandBody = function (body){ 203 | body 204 | .text ("RFC 3617 uri:") 205 | .text ("tftp://[:]/[;mode=]", " ") 206 | .text ("\nTransfer mode:") 207 | .text ("The only supported mode is 'octet', that is, all the files are " + 208 | "assumed to be binary files, therefore the content is not " + 209 | "modified. Because the 'mode' parameter is optional it can just be " + 210 | "ignored.", " ") 211 | }; 212 | 213 | var command = main 214 | .command ("get", { trailing: { min: 1, max: 2 } }) 215 | .usages (["ntftp get [options] []"]) 216 | .description ("GETs a file from the server") 217 | .on ("end", function (argv){ 218 | var o = parseUri (argv.get[0] + ""); 219 | if (o.error) return this.fail (o.error); 220 | 221 | try{ 222 | var files = normalizeGetFiles (o.file, argv.get[1]); 223 | }catch (error){ 224 | return this.fail (error); 225 | } 226 | 227 | argv.server = { 228 | host: o.host, 229 | port: o.port 230 | }; 231 | 232 | createClient (argv, true); 233 | 234 | get (files.remote, files.local, function (error){ 235 | if (error) notifyError (error); 236 | process.exit (); 237 | }); 238 | }) 239 | .body (); 240 | 241 | setCommandBody (command); 242 | 243 | command 244 | .text ("\nExample:") 245 | .text ("$ ntftp get -w 2 tftp://localhost/file\n\n" + 246 | "GETs a file named 'file' from the server in 'octet' mode with " + 247 | "a window size of 2.", " ") 248 | .text ("\nOptions:") 249 | 250 | setMainOptions (command); 251 | 252 | var command = main 253 | .command ("put", { trailing: { min: 1, max: 2 } }) 254 | .usages (["ntftp put [options] [] "]) 255 | .description ("PUTs a file into the server") 256 | .on ("end", function (argv){ 257 | var o = parseUri (argv.put[argv.put.length - 1] + ""); 258 | if (o.error) return this.fail (o.error); 259 | 260 | try{ 261 | var files = normalizePutFiles ( 262 | argv.put.length === 1 ? o.file : argv.put[0], o.file); 263 | }catch (error){ 264 | return this.fail (error); 265 | } 266 | 267 | argv.server = { 268 | host: o.host, 269 | port: o.port 270 | }; 271 | 272 | createClient (argv, true); 273 | 274 | put (files.local, files.remote, function (error){ 275 | if (error) notifyError (error); 276 | process.exit (); 277 | }); 278 | }) 279 | .body (); 280 | 281 | setCommandBody (command); 282 | 283 | command 284 | .text ("\nExample:") 285 | .text ("$ ntftp put tftp://localhost/file\n\n" + 286 | "PUTs a file named 'file' into the server in 'octet' mode.", " ") 287 | .text ("\nOptions:") 288 | 289 | setMainOptions (command); 290 | 291 | //Start parsing 292 | main.argv (); 293 | 294 | //Free the parsers 295 | main = command = null; 296 | 297 | function createShellParser (){ 298 | return argp.createParser () 299 | .main () 300 | //Don't produce errors when undefined arguments and options are 301 | //introduced, they are simply ignored because anyway, if the end event 302 | //is executed, it will fail 303 | .allowUndefinedArguments () 304 | .allowUndefinedOptions () 305 | .on ("end", function (){ 306 | notifyError (new Error ("Invalid command ('get' or 'put')"), true); 307 | }) 308 | .on ("error", function (error){ 309 | notifyError (error, true); 310 | }) 311 | .command ("get", { trailing: { min: 1, max: 2 } }) 312 | .usages (["get [options] []"]) 313 | .description ("GETs a file from the server") 314 | .on ("option", function (argv, option, value, long, ignore){ 315 | //Capture the help option because the prompt needs to be displayed 316 | //after the help message 317 | if (this.options ({ 318 | short: !long, 319 | long: long 320 | })[option].id === "help"){ 321 | this.printHelp (); 322 | ignore (); 323 | rl.prompt (); 324 | } 325 | }) 326 | .on ("end", function (argv){ 327 | try{ 328 | var files = normalizeGetFiles (argv.get[0], argv.get[1]); 329 | }catch (error){ 330 | return notifyError (error, true); 331 | } 332 | 333 | get (files.remote, files.local, function (error){ 334 | if (error) return notifyError (error, true); 335 | rl.prompt (); 336 | }); 337 | }) 338 | .on ("error", function (error){ 339 | notifyError (error, true); 340 | }) 341 | .body () 342 | .text ("Options:") 343 | .help () 344 | .command ("put", { trailing: { min: 1, max: 2 } }) 345 | .usages (["put [options] []"]) 346 | .description ("PUTs a file into the server") 347 | .on ("option", function (argv, option, value, long, ignore){ 348 | //Capture the help option because the prompt needs to be displayed 349 | //after the help message 350 | if (this.options ({ 351 | short: !long, 352 | long: long 353 | })[option].id === "help"){ 354 | this.printHelp (); 355 | ignore (); 356 | rl.prompt (); 357 | } 358 | }) 359 | .on ("end", function (argv){ 360 | try{ 361 | var files = normalizePutFiles (argv.put[0], argv.put[1]); 362 | }catch (error){ 363 | return notifyError (error, true); 364 | } 365 | 366 | put (files.local, files.remote, function (error){ 367 | if (error) return notifyError (error, true); 368 | rl.prompt (); 369 | }); 370 | }) 371 | .on ("error", function (error){ 372 | notifyError (error, true); 373 | }) 374 | .body () 375 | .text ("Options:") 376 | .help (); 377 | }; 378 | 379 | function createServer (argv){ 380 | tftp.createServer ({ 381 | root: argv.listen, 382 | host: argv.server.host, 383 | port: argv.server.port, 384 | blockSize: argv.blksize, 385 | retries: argv.retries, 386 | timeout: argv.timeout, 387 | windowSize: argv.windowsize 388 | }) 389 | .on ("error", notifyError) 390 | .on ("request", function (req){ 391 | console.log ("CONNECTION [" + req.stats.remoteAddress + ":" + 392 | req.stats.remotePort + "] " + req.method + " " + req.file); 393 | req.on ("error", function (error){ 394 | console.error ("ERROR [" + req.stats.remoteAddress + ":" + 395 | req.stats.remotePort + "] " + req.method + " " + req.file + " - " + 396 | error.message); 397 | }); 398 | req.on ("close", function (){ 399 | console.log ("CLOSE [" + req.stats.remoteAddress + ":" + 400 | req.stats.remotePort + "] " + req.method + " " + req.file); 401 | }); 402 | }) 403 | .on ("listening", function (){ 404 | console.log ("Listening on " + this.host + ":" + this.port + " (root: '" + 405 | path.resolve (this.root) + "')"); 406 | }) 407 | .listen (); 408 | }; 409 | 410 | function createClient (argv, onlySigint){ 411 | client = tftp.createClient ({ 412 | host: argv.server.host, 413 | port: argv.server.port, 414 | blockSize: argv.blksize, 415 | retries: argv.retries, 416 | timeout: argv.timeout, 417 | windowSize: argv.windowsize 418 | }); 419 | 420 | createPrompt (onlySigint); 421 | }; 422 | 423 | function createPrompt (onlySigint){ 424 | if (onlySigint){ 425 | rl = readLine.createInterface ({ 426 | input: process.stdin, 427 | output: process.stdout, 428 | }); 429 | rl.on ("SIGINT", function (){ 430 | //Abort the current transfer 431 | if (read){ 432 | read.gs.abort (); 433 | }else if (write){ 434 | write.ps.abort (); 435 | }else{ 436 | process.exit (); 437 | } 438 | }); 439 | return; 440 | } 441 | 442 | var parser = createShellParser (); 443 | 444 | var completions = ["get ", "put "]; 445 | 446 | //Start prompt 447 | rl = readLine.createInterface ({ 448 | input: process.stdin, 449 | output: process.stdout, 450 | completer: function (line){ 451 | var hits = completions.filter (function (command){ 452 | return command.indexOf (line) === 0; 453 | }); 454 | return [hits.length ? hits : [], line]; 455 | } 456 | }); 457 | rl.on ("line", function (line){ 458 | if (!line) return rl.prompt (); 459 | parser.argv (line.split (" ").filter (function (word){ 460 | return word; 461 | })); 462 | }); 463 | rl.on ("SIGINT", function (){ 464 | if (timer){ 465 | console.log (); 466 | process.exit (); 467 | } 468 | 469 | //Abort the current transfer 470 | if (read){ 471 | read.gs.abort (); 472 | }else if (write){ 473 | write.ps.abort (); 474 | }else{ 475 | again (); 476 | } 477 | }); 478 | rl.prompt (); 479 | }; 480 | 481 | function get (remote, local, cb){ 482 | clearTimeout (timer); 483 | timer = null; 484 | 485 | //Check if local is a dir and prevent from starting a request 486 | fs.stat (local, function (error, stats){ 487 | if (error){ 488 | if (error.code !== "ENOENT") return cb (error); 489 | }else if (stats.isDirectory ()){ 490 | return cb (new Error ("The local file is a directory")); 491 | } 492 | 493 | filename = formatFilename (remote); 494 | 495 | var started = false; 496 | var bar = null; 497 | var noExtensionsTimer = null; 498 | 499 | read = {}; 500 | var open = false; 501 | var destroy = null; 502 | 503 | read.gs = client.createGetStream (remote) 504 | .on ("error", function (error){ 505 | if (bar) bar.cancel (); 506 | clearInterval (noExtensionsTimer); 507 | 508 | if (started) console.log (); 509 | 510 | if (open){ 511 | read.ws.on ("close", function (){ 512 | fs.unlink (local, function (){ 513 | read = null; 514 | cb (error); 515 | }); 516 | }); 517 | read.ws.destroy (); 518 | }else{ 519 | destroy = error; 520 | } 521 | }) 522 | .on ("abort", function (){ 523 | if (bar) bar.cancel (); 524 | clearInterval (noExtensionsTimer); 525 | 526 | if (started) console.log (); 527 | 528 | if (read.error){ 529 | //The error comes from the ws 530 | fs.unlink (local, function (){ 531 | var error = read.error; 532 | read = null; 533 | cb (error); 534 | }); 535 | }else{ 536 | read.ws.on ("close", function (){ 537 | read = null; 538 | fs.unlink (local, function (){ 539 | cb (); 540 | }); 541 | }); 542 | read.ws.destroy (); 543 | } 544 | }) 545 | .on ("stats", function (stats){ 546 | started = true; 547 | 548 | if (stats.size !== null){ 549 | bar = statusBar.create ({ total: stats.size }) 550 | .on ("render", renderStatusBar) 551 | this.pipe (bar); 552 | }else{ 553 | //The server doesn't support extensions 554 | var dots = "..."; 555 | var i = 1; 556 | noExtensionsTimer = setInterval (function (){ 557 | i = i%4; 558 | process.stdout.clearLine (); 559 | process.stdout.cursorTo (0); 560 | process.stdout.write (dots.slice (0, i++)); 561 | }, 200); 562 | } 563 | }); 564 | 565 | read.ws = fs.createWriteStream (local) 566 | .on ("error", function (error){ 567 | read.error = error; 568 | read.gs.abort (errors.EIO); 569 | }) 570 | .on ("open", function (){ 571 | if (destroy){ 572 | read.ws.on ("close", function (){ 573 | fs.unlink (local, function (){ 574 | cb (destroy); 575 | }); 576 | }); 577 | read.ws.destroy (); 578 | }else{ 579 | open = true; 580 | } 581 | }) 582 | .on ("finish", function (){ 583 | read = null; 584 | clearInterval (noExtensionsTimer); 585 | console.log (); 586 | cb (); 587 | }); 588 | 589 | read.gs.pipe (read.ws); 590 | }); 591 | }; 592 | 593 | function put (local, remote, cb){ 594 | clearTimeout (timer); 595 | timer = null; 596 | 597 | //Check if local is a dir or doesn't exist to prevent from starting a new 598 | //request 599 | fs.stat (local, function (error, stats){ 600 | if (error) return cb (error); 601 | if (stats.isDirectory ()){ 602 | return cb (new Error ("The local file is a directory")); 603 | } 604 | 605 | filename = formatFilename (local); 606 | 607 | var bar = statusBar.create ({ total: stats.size }) 608 | .on ("render", renderStatusBar) 609 | 610 | write = {}; 611 | 612 | write.rs = fs.createReadStream (local) 613 | .on ("error", function (error){ 614 | write.error = error; 615 | write.ps.abort (errors.EIO); 616 | }) 617 | .on ("close", function (){ 618 | write = null; 619 | }); 620 | 621 | write.ps = client.createPutStream (remote, { size: stats.size }) 622 | .on ("error", function (error){ 623 | if (bar) bar.cancel (); 624 | 625 | console.log (); 626 | 627 | if (!write){ 628 | //Empty origin file 629 | cb (error); 630 | }else{ 631 | write.rs.on ("close", function (){ 632 | write = null; 633 | cb (error); 634 | }); 635 | write.rs.destroy (); 636 | } 637 | }) 638 | .on ("abort", function (){ 639 | if (bar) bar.cancel (); 640 | console.log (); 641 | 642 | if (write.error){ 643 | //The error comes from the rs 644 | var error = write.error; 645 | write = null; 646 | cb (error); 647 | }else{ 648 | var rs = write.rs; 649 | rs.on ("close", cb); 650 | rs.destroy (); 651 | } 652 | }) 653 | .on ("finish", function (){ 654 | write = null; 655 | console.log (); 656 | cb (); 657 | }); 658 | 659 | write.rs.pipe (write.ps); 660 | write.rs.pipe (bar); 661 | }); 662 | }; -------------------------------------------------------------------------------- /examples/client/copy-remote-file.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var tftp = require ("../../lib"); 4 | 5 | //Downloads a file and at the same time uploads it again 6 | 7 | var client = tftp.createClient (); 8 | 9 | var ps; 10 | var gs = client.createGetStream ("remote-file") 11 | .on ("error", function (error){ 12 | console.error (error); 13 | if (ps) ps.abort (); 14 | }) 15 | .on ("stats", function (stats){ 16 | if (stats.size !== null){ 17 | ps = client.createPutStream ("remote-file-copy", { size: stats.size }) 18 | .on ("error", function (error){ 19 | console.error (error); 20 | gs.abort (); 21 | }); 22 | gs.pipe (ps); 23 | } 24 | }); -------------------------------------------------------------------------------- /examples/client/streams.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fs = require ("fs"); 4 | var tftp = require ("../../lib"); 5 | 6 | /* 7 | Note: Wrapping a GetStream or a PutStream in a function and use a fs.WriteStream 8 | as a destination or a fs.ReadStream as a source is not necessary. Use the 9 | functions client.get() and client.put() instead. This example only shows what is 10 | being done under the hood when using fs streams. For a simpler example with 11 | other kind of streams, see server/proxy-http.js 12 | */ 13 | 14 | var client = tftp.createClient (); 15 | 16 | var get = function (remote, local, cb){ 17 | var open = false; 18 | var destroy = null; 19 | var err = null; 20 | 21 | var gs = client.createGetStream (remote) 22 | .on ("error", function (error){ 23 | if (open){ 24 | //The file is open, destroy the stream and remove the file 25 | ws.on ("close", function (){ 26 | fs.unlink (local, function (){ 27 | cb (error); 28 | }); 29 | }); 30 | ws.destroy (); 31 | }else{ 32 | //Wait until the file is open 33 | destroy = error; 34 | } 35 | }) 36 | .on ("abort", function (){ 37 | //Remove the local file if the GET stream is aborted 38 | fs.unlink (local, function (){ 39 | //The error comes from the ws 40 | cb (err); 41 | }); 42 | }); 43 | 44 | var ws = fs.createWriteStream (local) 45 | .on ("error", function (error){ 46 | //Abort the GET stream 47 | err = error; 48 | gs.abort (tftp.EIO); 49 | }) 50 | .on ("open", function (){ 51 | if (destroy){ 52 | //There was an error in the get stream and the file must be removed 53 | ws.on ("close", function (){ 54 | fs.unlink (local, function (){ 55 | cb (error); 56 | }); 57 | }); 58 | ws.destroy (); 59 | }else{ 60 | open = true; 61 | } 62 | }) 63 | .on ("finish", function (){ 64 | //Transfer finished 65 | cb (); 66 | }); 67 | 68 | gs.pipe (ws); 69 | }; 70 | 71 | var put = function (local, remote, cb){ 72 | fs.stat (local, function (error, stats){ 73 | if (error) return cb (error); 74 | 75 | var closed = false; 76 | 77 | var rs = fs.createReadStream (local) 78 | .on ("error", function (error){ 79 | //Abort the PUT stream 80 | err = error; 81 | ps.abort (tftp.EIO); 82 | }) 83 | .on ("close", function (){ 84 | closed = true; 85 | }); 86 | 87 | var ps = new PutStream (remote, me._options, { size: stats.size }) 88 | .on ("error", function (error){ 89 | if (closed){ 90 | //Empty origin file 91 | cb (error); 92 | }else{ 93 | //Close the readable stream 94 | rs.on ("close", function (){ 95 | cb (error); 96 | }); 97 | rs.destroy (); 98 | } 99 | }) 100 | .on ("abort", function (){ 101 | //The error comes from the rs 102 | cb (err); 103 | }) 104 | .on ("finish", function (){ 105 | //Transfer finished 106 | cb (); 107 | }); 108 | 109 | rs.pipe (ps); 110 | }); 111 | }; 112 | 113 | get ("remote-file", "local-file", function (error){ 114 | if (error) return console.error (error); 115 | }); 116 | 117 | put ("local-file", "remote-file", function (error){ 118 | if (error) return console.error (error); 119 | }); -------------------------------------------------------------------------------- /examples/server/default-listener-deny-put.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var tftp = require ("../../lib"); 4 | 5 | /* 6 | socket: localhost:1234, root: ".", only GET 7 | */ 8 | 9 | var server = tftp.createServer ({ 10 | port: 1234, 11 | denyPUT: true 12 | }); 13 | 14 | server.on ("error", function (error){ 15 | //Errors from the main socket 16 | console.error (error); 17 | }); 18 | 19 | server.on ("request", function (req){ 20 | req.on ("error", function (error){ 21 | //Error from the request 22 | console.error (error); 23 | }); 24 | }); 25 | 26 | server.on ("listening", doRequest); 27 | 28 | server.listen (); 29 | 30 | function doRequest (){ 31 | tftp.createClient ({ port: 1234 }).put (__filename, function (error){ 32 | console.error (error); //[Error: (Server) Cannot PUT files] 33 | server.close (); 34 | }); 35 | } -------------------------------------------------------------------------------- /examples/server/graceful-shutdown.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var tftp = require ("../../lib"); 4 | 5 | /* 6 | This example demonstrates how to close the server and all the current 7 | connections gracefully. 8 | 9 | This is slightly different from the http server where the "connection" event 10 | returns the socket and you must call to socket.destroy() to close it. On the 11 | other hand, this tftp server doesn't have a "connection" event because the 12 | internal socket is not exposed to the public, it just has a "request" event 13 | which is fired each time the server receives a new request. The "req" argument 14 | acts like a "connection" object. When req.abort() is called it sends an error 15 | message to the client and then the socket closes, that is, it's a real graceful 16 | shutdown. Instead of killing the socket by brute force, the server informs the 17 | client that the transfer has been aborted, so the client is able to abort the 18 | transfer immediately instead of begin a timeout and then abort. 19 | */ 20 | 21 | var connections = []; 22 | 23 | var server = tftp.createServer (); 24 | 25 | server.on ("request", function (req){ 26 | req.on ("error", function (error){ 27 | //Error from the request 28 | console.error (error); 29 | }); 30 | 31 | //Save the connection 32 | connections.push (req); 33 | 34 | //The "close" event is fired when the internal socket closes, regardless 35 | //whether it is produced by an error, the socket closes naturally due to the 36 | //end of the transfer or the transfer has been aborted 37 | req.on ("close", function (){ 38 | //Remove the connection 39 | connections.splice (connections.indexOf (this), 1); 40 | if (closed && !connections.length){ 41 | //The server and all the connections have been closed 42 | console.log ("Server closed"); 43 | } 44 | }); 45 | }); 46 | 47 | server.on ("error", function (error){ 48 | //Errors from the main socket 49 | console.error (error); 50 | }); 51 | 52 | server.listen (); 53 | 54 | var closed = false; 55 | 56 | setTimeout (function (){ 57 | //Close the server after 10s 58 | server.on ("close", function (){ 59 | closed = true; 60 | 61 | if (!connections.length){ 62 | return console.log ("Server closed"); 63 | } 64 | 65 | //Abort all the current transfers 66 | for (var i=0; i> 3"); 37 | 38 | tftp.createClient () 39 | .createGetStream ("tmp1", { userExtensions: { num: 3 } }) 40 | .on ("stats", function (stats){ 41 | console.log ("<< " + stats.userExtensions.num); 42 | }) 43 | .pipe (fs.createWriteStream ("tmp2")) 44 | .on ("finish", function (){ 45 | server.close (); 46 | fs.unlinkSync ("tmp1"); 47 | fs.unlinkSync ("tmp2"); 48 | }); 49 | } -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fs = require ("fs"); 4 | var path = require ("path"); 5 | var GetStream = require ("./streams/client/get-stream"); 6 | var PutStream = require ("./streams/client/put-stream"); 7 | var normalizeFilename = require ("./normalize-filename"); 8 | var createOptions = require ("./create-options"); 9 | var errors = require ("./protocol/errors"); 10 | 11 | var Client = module.exports = function (options){ 12 | this._options = createOptions (options); 13 | }; 14 | 15 | Client.prototype.createGetStream = function (remote, options){ 16 | remote = normalizeFilename (remote); 17 | return new GetStream (remote, this._options, options); 18 | }; 19 | 20 | Client.prototype.createPutStream = function (remote, options){ 21 | remote = normalizeFilename (remote); 22 | return new PutStream (remote, this._options, options); 23 | }; 24 | 25 | Client.prototype.get = function (remote, local, options, cb){ 26 | remote = normalizeFilename (remote); 27 | 28 | var argsLength = arguments.length; 29 | if (argsLength === 2){ 30 | cb = local; 31 | local = path.basename (remote); 32 | }else if (argsLength === 3){ 33 | if (typeof local === "object"){ 34 | cb = options; 35 | options = local; 36 | local = path.basename (remote); 37 | }else if (typeof local === "string"){ 38 | cb = options; 39 | options = {}; 40 | } 41 | } 42 | 43 | var me = this; 44 | 45 | //Check if local is a dir to prevent from starting a new request 46 | fs.stat (local, function (error, stats){ 47 | if (error){ 48 | if (error.code !== "ENOENT") return cb (error); 49 | }else if (stats.isDirectory ()){ 50 | return cb (new Error ("The local file is a directory")); 51 | } 52 | 53 | var wsError; 54 | var open = false; 55 | var destroy = null; 56 | 57 | var gs = new GetStream (remote, me._options, options) 58 | .on ("error", function (error){ 59 | if (open){ 60 | ws.on ("close", function (){ 61 | fs.unlink (local, function (){ 62 | cb (error); 63 | }); 64 | }); 65 | ws.destroy (); 66 | }else{ 67 | destroy = error; 68 | } 69 | }) 70 | .on ("abort", function (){ 71 | fs.unlink (local, function (){ 72 | cb (wsError); 73 | }); 74 | }); 75 | 76 | var ws = fs.createWriteStream (local) 77 | .on ("error", function (error){ 78 | wsError = error; 79 | gs.abort (errors.EIO.message); 80 | }) 81 | .on ("open", function (){ 82 | if (destroy){ 83 | ws.on ("close", function (){ 84 | fs.unlink (local, function (){ 85 | cb (destroy); 86 | }); 87 | }); 88 | ws.destroy (); 89 | }else{ 90 | open = true; 91 | } 92 | }) 93 | .on ("finish", function (){ 94 | cb (); 95 | }); 96 | 97 | gs.pipe (ws); 98 | }); 99 | }; 100 | 101 | Client.prototype.put = function (local, remote, options, cb){ 102 | var argsLength = arguments.length; 103 | if (argsLength === 2){ 104 | cb = remote; 105 | remote = path.basename (local); 106 | options = {}; 107 | }else if (argsLength === 3){ 108 | if (typeof remote === "object"){ 109 | cb = options; 110 | options = remote; 111 | remote = path.basename (local); 112 | }else if (typeof remote === "string"){ 113 | cb = options; 114 | options = {}; 115 | } 116 | } 117 | 118 | remote = normalizeFilename (remote); 119 | 120 | var me = this; 121 | 122 | //Check if local is a dir or doesn't exist to prevent from starting a new 123 | //request 124 | fs.stat (local, function (error, stats){ 125 | if (error) return cb (error); 126 | if (stats.isDirectory ()){ 127 | return cb (new Error ("The local file is a directory")); 128 | } 129 | 130 | var rsError; 131 | var closed = false; 132 | 133 | var rs = fs.createReadStream (local) 134 | .on ("error", function (error){ 135 | rsError = error; 136 | ps.abort (errors.EIO.message); 137 | }) 138 | .on ("close", function (){ 139 | closed = true; 140 | }); 141 | 142 | options = { 143 | highWaterMark: options.highWaterMark, 144 | userExtensions: options.userExtensions, 145 | size: stats.size 146 | }; 147 | var ps = new PutStream (remote, me._options, options) 148 | .on ("error", function (error){ 149 | if (closed){ 150 | //Empty origin file 151 | cb (error); 152 | }else{ 153 | rs.on ("close", function (){ 154 | cb (error); 155 | }); 156 | rs.destroy (); 157 | } 158 | }) 159 | .on ("abort", function (){ 160 | cb (rsError); 161 | }) 162 | .on ("finish", function (){ 163 | cb (); 164 | }); 165 | 166 | rs.pipe (ps); 167 | }); 168 | }; -------------------------------------------------------------------------------- /lib/create-options.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var sanitizeNumber = function (n){ 4 | n = ~~n; 5 | return n < 1 ? 1 : n; 6 | }; 7 | 8 | module.exports = function (opts, server){ 9 | opts = opts || {}; 10 | 11 | var options = { 12 | address: opts.host || "localhost", 13 | port: sanitizeNumber (opts.port || 69), 14 | retries: sanitizeNumber (opts.retries || 3) 15 | }; 16 | 17 | if (server){ 18 | options.root = opts.root || "."; 19 | options.denyGET = opts.denyGET; 20 | options.denyPUT = opts.denyPUT; 21 | } 22 | 23 | //Default window size 4: https://github.com/joyent/node/issues/6696 24 | var windowSize = sanitizeNumber (opts.windowSize || 4); 25 | if (windowSize > 65535) windowSize = 4; 26 | 27 | //Maximum block size before IP packet fragmentation on Ethernet networks 28 | var blockSize = sanitizeNumber (opts.blockSize || 1468); 29 | if (blockSize < 8 || blockSize > 65464) blockSize = 1468; 30 | 31 | var timeout = sanitizeNumber (opts.timeout || 3000); 32 | 33 | options.extensions = { 34 | blksize: blockSize, 35 | timeout: timeout, 36 | windowsize: windowSize, 37 | //This option is not strictly required because it is not necessary when 38 | //receiving a file and it is only used to inform the server when sending a 39 | //file. Most servers won't care about it and will simply ignore it 40 | rollover: 0 41 | }; 42 | 43 | options.extensionsString = { 44 | blksize: blockSize + "", 45 | timeout: timeout + "", 46 | windowsize: windowSize + "", 47 | rollover: "0" 48 | }; 49 | 50 | options.extensionsLength = 48 + 51 | options.extensionsString.blksize.length + 52 | options.extensionsString.timeout.length + 53 | options.extensionsString.windowsize.length; 54 | 55 | return options; 56 | }; -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var Client = require ("./client"); 4 | var Server = require ("./server"); 5 | var errors = require ("./protocol/errors"); 6 | 7 | module.exports.createClient = function (options){ 8 | return new Client (options); 9 | }; 10 | 11 | module.exports.createServer = function (options, requestListener){ 12 | return new Server (options, requestListener); 13 | }; 14 | 15 | //Expose the error codes 16 | for (var p in errors){ 17 | if (p[0] !== "E") continue; 18 | module.exports[p] = errors[p].message; 19 | } -------------------------------------------------------------------------------- /lib/normalize-filename.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var path = require ("path"); 4 | 5 | module.exports = function (filename){ 6 | filename = path.normalize (filename); 7 | 8 | //Check for invalid access 9 | if (filename.indexOf ("..") === 0){ 10 | throw new Error ("The path of the filename cannot point to upper levels"); 11 | } 12 | 13 | //Multibytes characters are not allowed 14 | if (Buffer.byteLength (filename) > filename.length){ 15 | throw new Error ("The filename cannot contain multibyte characters"); 16 | } 17 | 18 | return filename; 19 | }; -------------------------------------------------------------------------------- /lib/protocol/client/client-request.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var dns = require ("dns"); 4 | var util = require ("util"); 5 | var packets = require ("../packets"); 6 | var opcodes = require ("../opcodes"); 7 | var errors = require ("../errors"); 8 | var knownExtensions = require ("../known-extensions"); 9 | var Request = require ("../request"); 10 | 11 | //States 12 | var REQ_SENT = 0; 13 | var ACK_SENT = 1; 14 | var BLK_SENT = 2; 15 | 16 | var ClientRequest = module.exports = function (args){ 17 | Request.call (this, args.globalOptions.address, args.globalOptions.port, 18 | args.globalOptions.retries, args.globalOptions.extensions.timeout); 19 | 20 | this._isRRQ = args.reader; 21 | this._ipFamily = null; 22 | this._file = args.file; 23 | this._globalOptions = args.globalOptions; 24 | this._opOptions = args.opOptions || {}; 25 | this._prefixError = "(Server) "; 26 | this._firstPacket = true; 27 | this._oackExpected = true; 28 | this._extensionsRetransmitted = false; 29 | this._extensionsEmitted = false; 30 | this._maxDataLength = 4; 31 | this._blksize = null; 32 | 33 | 34 | var me = this; 35 | me._lookup (function (){ 36 | //Delay the opening to the next tick (the address could be an ip, hence the 37 | //callback is called immediately) 38 | process.nextTick (function (){ 39 | me._open (true); 40 | }); 41 | }); 42 | }; 43 | 44 | util.inherits (ClientRequest, Request); 45 | 46 | ClientRequest.prototype._lookup = function (cb){ 47 | var me = this; 48 | dns.lookup (this._address, function (error, address, family){ 49 | if (error) return me.onError (new Error ("Cannot resolve the domain name " + 50 | "\"" + me._address + "\"")); 51 | me._address = address; 52 | me._ipFamily = family; 53 | cb (); 54 | }); 55 | }; 56 | 57 | ClientRequest.prototype._open = function (extensions){ 58 | var me = this; 59 | 60 | this._initSocket (null, function (message, rinfo){ 61 | if (me._firstPacket){ 62 | me._firstPacket = false; 63 | //Reset the timer that was started with the request 64 | me._requestTimer.reset (); 65 | 66 | //Save the remote host with the first packet 67 | me._address = rinfo.address; 68 | me._port = rinfo.port; 69 | }else if (me._address !== rinfo.address || me._port !== rinfo.port){ 70 | //A message is received from a different remote host 71 | //This could happen when the client sends a request, the server sends a 72 | //response but the client never receives it, so it timeouts and sends the 73 | //same request again. The server responds again but the client receives 74 | //the two server responses: the first is accepted but the latter produces 75 | //this error because from the point of view of the server it receives two 76 | //different requests and sends the same file from different ports 77 | return me._send (packets.error.serialize (errors.ESOCKET)); 78 | } 79 | 80 | if (message.length < 2) return me._sendErrorAndClose (errors.EBADMSG); 81 | 82 | me._onMessage (message); 83 | }); 84 | 85 | //Create and send the RRQ/WRQ message 86 | //There are 2 possible responses from the server: 87 | //- If the server doesn't support extensions the file transfer starts 88 | //- If the server supports extensions, it sends a OACK 89 | var buffer; 90 | try{ 91 | if (this._isRRQ){ 92 | buffer = extensions 93 | ? packets.rrq.serialize (this._file, this._globalOptions, 94 | this._opOptions) 95 | : packets.rrq.serialize (this._file); 96 | }else{ 97 | buffer = extensions 98 | ? packets.wrq.serialize (this._file, this._globalOptions, 99 | this._opOptions) 100 | : packets.wrq.serialize (this._file); 101 | } 102 | }catch (error){ 103 | return this._close (new Error (error.message)); 104 | } 105 | 106 | this._sendAndRetransmit (buffer); 107 | }; 108 | 109 | ClientRequest.prototype._emitDefaultExtensions = function (){ 110 | this._extensionsEmitted = true; 111 | var stats = { 112 | blockSize: 512, 113 | windowSize: 1, 114 | size: this._opOptions.size || null, 115 | userExtensions: {} 116 | }; 117 | this._setStats (stats); 118 | this._oackExpected = false; 119 | this._onReady (stats, this._globalOptions.extensions.rollover); 120 | }; 121 | 122 | ClientRequest.prototype._setStats = function (stats){ 123 | //Save max data length 124 | this._maxDataLength += stats.blockSize; 125 | this._blksize = stats.blockSize; 126 | 127 | stats.retries = this._retries; 128 | stats.timeout = this._timeout; 129 | var address = this._socket.address (); 130 | stats.localAddress = address.address; 131 | stats.localPort = address.port; 132 | stats.remoteAddress = this._address; 133 | stats.remotePort = this._port; 134 | }; 135 | 136 | ClientRequest.prototype._onMessage = function (buffer){ 137 | var op = buffer.readUInt16BE (0); 138 | 139 | if (op === opcodes.DATA){ 140 | if (this._isRRQ){ 141 | if (!this._extensionsEmitted) this._emitDefaultExtensions (); 142 | if (buffer.length < 4 || buffer.length > this._maxDataLength){ 143 | return this._sendErrorAndClose (errors.EBADMSG); 144 | } 145 | try{ 146 | this._onData (packets.data.deserialize (buffer, this._blksize)); 147 | }catch (error){ 148 | this._sendErrorAndClose (error); 149 | } 150 | }else{ 151 | this._sendErrorAndClose (errors.EBADOP); 152 | } 153 | }else if (op === opcodes.ACK){ 154 | if (!this._isRRQ){ 155 | if (!this._extensionsEmitted){ 156 | //The server doesn't return extensions, it's probably that it doesn't 157 | //rollover automatically (old server), so we can abort the transfer 158 | //prematurely 159 | //Check the size (65535x512) 160 | if (this._opOptions.size > 33553920){ 161 | return this._sendErrorAndClose (errors.EFBIG); 162 | } 163 | this._emitDefaultExtensions (); 164 | //The first ACK with block 0 is ignored 165 | if (buffer.length !== 4 || buffer.readUInt16BE (2) !== 0){ 166 | this._sendErrorAndClose (errors.EBADMSG); 167 | } 168 | }else{ 169 | try{ 170 | this._onAck (packets.ack.deserialize (buffer)); 171 | }catch (error){ 172 | this._sendErrorAndClose (error); 173 | } 174 | } 175 | }else{ 176 | this._sendErrorAndClose (errors.EBADOP); 177 | } 178 | }else if (op === opcodes.OACK){ 179 | //OACK can be only received when RRQ/WRQ is sent 180 | if (!this._oackExpected) return this._sendErrorAndClose (errors.EBADOP); 181 | this._oackExpected = false; 182 | try{ 183 | this._onOackMessage (packets.oack.deserialize (buffer)); 184 | }catch (error){ 185 | this._sendErrorAndClose (error); 186 | } 187 | }else if (op === opcodes.ERROR){ 188 | if (buffer.length < 4) return this._closeWithError (errors.EBADMSG); 189 | try{ 190 | this._onErrorMessage (packets.error.deserialize (buffer)); 191 | }catch (error){ 192 | this._closeWithError (error); 193 | } 194 | }else{ 195 | this._sendErrorAndClose (errors.EBADOP); 196 | } 197 | }; 198 | 199 | ClientRequest.prototype._onOackMessage = function (message){ 200 | var userExtensions = {}; 201 | 202 | for (var p in message){ 203 | //Fail if the OACK message contains invalid extensions 204 | if (!knownExtensions[p]){ 205 | if (!(p in this._opOptions.userExtensions)){ 206 | return this._sendErrorAndClose (errors.EDENY); 207 | }else{ 208 | userExtensions[p] = message[p]; 209 | } 210 | } 211 | } 212 | 213 | var blockSize; 214 | var transferSize; 215 | var windowSize; 216 | var rollover; 217 | 218 | if (message.timeout){ 219 | var timeout = ~~message.timeout; 220 | if (timeout > 0 && timeout <= this._timeout){ 221 | this._timeout = timeout; 222 | }else{ 223 | return this._sendErrorAndClose (errors.EDENY); 224 | } 225 | } 226 | 227 | if (message.blksize){ 228 | blockSize = ~~message.blksize; 229 | if (blockSize < 8 || blockSize > this._globalOptions.extensions.blksize){ 230 | return this._sendErrorAndClose (errors.EDENY); 231 | } 232 | } 233 | 234 | if (message.tsize){ 235 | transferSize = ~~message.tsize; 236 | if (transferSize < 0 || 237 | (this._opOptions.size !== undefined && 238 | transferSize !== this._opOptions.size)){ 239 | return this._sendErrorAndClose (errors.EDENY); 240 | } 241 | } 242 | 243 | if (message.windowsize){ 244 | windowSize = ~~message.windowsize; 245 | if (windowSize <= 0 || 246 | windowSize > this._globalOptions.extensions.windowsize){ 247 | return this._sendErrorAndClose (errors.EDENY); 248 | } 249 | } 250 | 251 | if (message.rollover){ 252 | rollover = ~~message.rollover; 253 | if (rollover < 0 || rollover > 1){ 254 | return this._sendErrorAndClose (errors.EDENY); 255 | } 256 | } 257 | 258 | this._extensionsEmitted = true; 259 | rollover = rollover !== undefined 260 | ? rollover 261 | : this._globalOptions.extensions.rollover; 262 | var stats = { 263 | blockSize: blockSize || 512, 264 | windowSize: windowSize || 1, 265 | size: transferSize !== undefined ? transferSize : null, 266 | userExtensions: userExtensions 267 | }; 268 | this._setStats (stats); 269 | 270 | if (this._isRRQ){ 271 | //Acknowledge OACK 272 | //The ACK of the block 0 is retransmitted from the reader 273 | this._sendAck (0); 274 | this._onReady (stats, rollover, true); 275 | }else{ 276 | this._onReady (stats, rollover); 277 | } 278 | }; 279 | 280 | ClientRequest.prototype._onErrorMessage = function (message){ 281 | if (this._oackExpected && message.code === 8){ 282 | if (this._extensionsRetransmitted){ 283 | //The server has returned an ERROR with code 8 after a RRQ/WRQ without 284 | //extensions. The code 8 is only used when RRQ and WRQ messages contain 285 | //extensions 286 | return this._closeWithError (errors.EBADOP); 287 | } 288 | 289 | //If the error code is 8, the server doesn't like one or more extensions 290 | //Retransmit without extensions 291 | this._extensionsRetransmitted = true; 292 | this._port = this._globalOptions.port; 293 | this._firstPacket = this._first = true; 294 | 295 | //In order to retransmit, the socket must be closed and open a new one, that 296 | //is, cannot reuse the same socket because the server closes its socket 297 | this._socket.removeListener ("close", this._onCloseFn); 298 | var me = this; 299 | this._socket.on ("close", function (){ 300 | me._open (); 301 | }); 302 | this._socket.close (); 303 | }else{ 304 | this._closeWithError (message); 305 | } 306 | }; -------------------------------------------------------------------------------- /lib/protocol/client/reader.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var Reader = require ("../reader"); 4 | var ClientRequest = require ("./client-request"); 5 | var inherit = require ("../inherit"); 6 | 7 | module.exports = inherit (Reader, ClientRequest); -------------------------------------------------------------------------------- /lib/protocol/client/writer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var Writer = require ("../writer"); 4 | var ClientRequest = require ("./client-request"); 5 | var inherit = require ("../inherit"); 6 | 7 | module.exports = inherit (Writer, ClientRequest); -------------------------------------------------------------------------------- /lib/protocol/errors.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var define = function (name, code){ 4 | var message; 5 | if (typeof code === "string"){ 6 | message = code; 7 | code = 0; 8 | }else{ 9 | message = rfc[code]; 10 | } 11 | errors[name] = { code: code, name: name, message: message }; 12 | }; 13 | 14 | var rfc = [ 15 | null, 16 | "File not found", 17 | "Access violation", 18 | "Disk full or allocation exceeded", 19 | "Illegal TFTP operation", 20 | "Unknown transfer ID", 21 | "File already exists", 22 | "No such user", 23 | "The request has been denied" 24 | ]; 25 | 26 | var errors = { 27 | wrap: function (message){ 28 | var code = 0; 29 | for (var name in this) 30 | if (this[name].message === message) 31 | code = this[name].code 32 | return { code: code, name: null, message: message } 33 | } 34 | }; 35 | 36 | define ("ENOENT", 1); 37 | define ("EACCESS", 2); 38 | define ("ENOSPC", 3); 39 | define ("EBADOP", 4); 40 | define ("ETID", 5); 41 | define ("EEXIST", 6); 42 | define ("ENOUSER", 7); 43 | define ("EDENY", 8); 44 | define ("ESOCKET", "Invalid remote socket"); 45 | define ("EBADMSG", "Malformed TFTP message"); 46 | define ("EABORT", "Aborted"); 47 | define ("EFBIG", "File too big"); 48 | define ("ETIME", "Timed out"); 49 | define ("EBADMODE", "Invalid transfer mode"); 50 | define ("EBADNAME", "Invalid filename"); 51 | define ("EIO", "I/O error"); 52 | define ("ENOGET", "Cannot GET files"); 53 | define ("ENOPUT", "Cannot PUT files"); 54 | define ("ERBIG", "Request bigger than 512 bytes (too much extensions)"); 55 | define ("ECONPUT", "Concurrent PUT request over the same file"); 56 | define ("ECURPUT", "The requested file is being written by another request"); 57 | define ("ECURGET", "The requested file is being read by another request"); 58 | 59 | module.exports = errors; -------------------------------------------------------------------------------- /lib/protocol/inherit.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var util = require ("util"); 4 | 5 | /* 6 | In order to reuse the Reader and Writer prototypes in the ClientRequest and 7 | IncomingRequest classes, this function creates a new Reader and Writer class 8 | clones: 9 | ReaderClone1 is created from a Reader and inherits from ClientRequest. 10 | ReaderClone2 is created from a Reader and inherits from IncomingRequest. 11 | WriterClone1 is created from a Writer and inherits from ClientRequest. 12 | WriterClone2 is created from a Writer and inherits from IncomingRequest. 13 | 14 | The overall class hierarchy is: 15 | - Client 16 | GetStream uses ReaderClone1 -> Reader -> ClientRequest -> Request 17 | PutStream uses WriterClone1 -> Writer -> ClientRequest -> Request 18 | - Server 19 | GetStream uses ReaderClone2 -> Reader -> IncomingRequest -> Request 20 | PutStream uses WriterClone2 -> Writer -> IncomingRequest -> Request 21 | */ 22 | module.exports = function (ctor, base){ 23 | var fn = function (){ 24 | var args = Array.prototype.slice.call (arguments); 25 | args.unshift (base); 26 | ctor.apply (this, args); 27 | }; 28 | 29 | var proto = ctor.prototype; 30 | 31 | util.inherits (fn, base); 32 | 33 | for (var p in proto){ 34 | fn.prototype[p] = proto[p]; 35 | } 36 | 37 | return fn; 38 | }; -------------------------------------------------------------------------------- /lib/protocol/known-extensions.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | timeout: true, 5 | tsize: true, 6 | blksize: true, 7 | windowsize: true, 8 | rollover: true 9 | }; -------------------------------------------------------------------------------- /lib/protocol/opcodes.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | RRQ: 1, 5 | WRQ: 2, 6 | DATA: 3, 7 | ACK: 4, 8 | ERROR: 5, 9 | OACK: 6 10 | }; -------------------------------------------------------------------------------- /lib/protocol/packets/ack.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var opcodes = require ("../opcodes"); 4 | var errors = require ("../errors"); 5 | 6 | module.exports = { 7 | serialize: function (block){ 8 | var buffer = new Buffer (4); 9 | buffer.writeUInt16BE (opcodes.ACK, 0); 10 | buffer.writeUInt16BE (block, 2); 11 | return buffer; 12 | }, 13 | deserialize: function (buffer){ 14 | var block = buffer.readUInt16BE (2); 15 | if (block < 0 || block > 65535) throw errors.EBADMSG; 16 | return { 17 | block: block 18 | }; 19 | } 20 | }; -------------------------------------------------------------------------------- /lib/protocol/packets/data.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var opcodes = require ("../opcodes"); 4 | var errors = require ("../errors"); 5 | 6 | module.exports = { 7 | serialize: function (block, data){ 8 | var buffer; 9 | if (data.length){ 10 | buffer = new Buffer (4 + data.length); 11 | buffer.writeUInt16BE (opcodes.DATA, 0); 12 | buffer.writeUInt16BE (block, 2); 13 | data.copy (buffer, 4); 14 | return buffer; 15 | }else{ 16 | buffer = new Buffer (4); 17 | buffer.writeUInt16BE (opcodes.DATA, 0); 18 | buffer.writeUInt16BE (block, 2); 19 | return buffer; 20 | } 21 | }, 22 | deserialize: function (buffer, blockSize){ 23 | var block = buffer.readUInt16BE (2); 24 | if (block < 0 || block > 65535) throw errors.EBADMSG; 25 | var data = buffer.slice (4); 26 | if (data.length > blockSize) throw errors.EBADMSG; 27 | return { 28 | block: block, 29 | data: data 30 | } 31 | } 32 | }; -------------------------------------------------------------------------------- /lib/protocol/packets/error.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var opcodes = require ("../opcodes"); 4 | var readString = require ("./read-string"); 5 | 6 | module.exports = { 7 | serialize: function (obj){ 8 | var buffer = new Buffer (obj.message.length + 5); 9 | buffer.writeUInt16BE (opcodes.ERROR, 0); 10 | buffer.writeUInt16BE (obj.code, 2); 11 | buffer.write (obj.message, 4, "ascii"); 12 | buffer[buffer.length - 1] = 0; 13 | return buffer; 14 | }, 15 | deserialize: function (buffer){ 16 | var code = buffer.readUInt16BE (2); 17 | return { 18 | code: code, 19 | message: code === 0 && buffer.length === 4 20 | //Errors with code 0 and no description 21 | ? "" 22 | : readString (buffer, { offset: 4 }) 23 | } 24 | } 25 | }; -------------------------------------------------------------------------------- /lib/protocol/packets/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; module.exports = { ack: require ("./ack"), data: require ("./data"), error: require ("./error"), oack: require ("./oack"), rrq: require ("./rrq"), wrq: require ("./wrq") }; -------------------------------------------------------------------------------- /lib/protocol/packets/oack.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var opcodes = require ("../opcodes"); 4 | var readString = require ("./read-string"); 5 | 6 | module.exports = { 7 | serialize: function (extensions){ 8 | var bytes = 2; 9 | var o = {}; 10 | var str; 11 | 12 | for (var p in extensions){ 13 | str = extensions[p] + ""; 14 | bytes += 2 + p.length + str.length; 15 | o[p] = str; 16 | } 17 | 18 | var buffer = new Buffer (bytes); 19 | buffer.writeUInt16BE (opcodes.OACK, 0); 20 | 21 | var offset = 2; 22 | for (var p in o){ 23 | buffer.write (p, offset, "ascii"); 24 | offset += p.length; 25 | buffer[offset++] = 0; 26 | buffer.write (o[p], offset, "ascii"); 27 | offset += o[p].length; 28 | buffer[offset++] = 0; 29 | }; 30 | 31 | return buffer; 32 | }, 33 | deserialize: function (buffer){ 34 | var extensions = {}; 35 | var o = { offset: 2 }; 36 | var length = buffer.length; 37 | while (o.offset < length){ 38 | extensions[readString (buffer, o)] = readString (buffer, o); 39 | } 40 | return extensions; 41 | } 42 | }; -------------------------------------------------------------------------------- /lib/protocol/packets/read-request.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var readString = require ("./read-string"); 4 | var errors = require ("../errors"); 5 | var normalizeFilename = require ("../../normalize-filename"); 6 | 7 | module.exports = function (buffer, rrq){ 8 | var o = { offset: 2 }; 9 | 10 | var file = readString (buffer, o); 11 | try{ 12 | file = normalizeFilename (file); 13 | }catch (error){ 14 | throw errors.EBADNAME; 15 | } 16 | 17 | var mode = readString (buffer, o).toLowerCase (); 18 | if (mode !== "octet" && mode !== "mail" && mode !== "netascii"){ 19 | throw errors.EBADMODE; 20 | } 21 | 22 | var extensions = null; 23 | var userExtensions = {}; 24 | var length = buffer.length; 25 | var key; 26 | var value; 27 | var blksize; 28 | var tsize; 29 | var windowsize; 30 | var rollover; 31 | 32 | while (o.offset < length){ 33 | key = readString (buffer, o); 34 | value = readString (buffer, o); 35 | 36 | blksize = key === "blksize"; 37 | tsize = key === "tsize"; 38 | windowsize = key === "windowsize"; 39 | rollover = key === "rollover"; 40 | 41 | if (blksize || tsize || windowsize || rollover){ 42 | //Validate the known extension values 43 | if (value.indexOf (".") !== -1 || isNaN ((value = Number (value))) || 44 | (blksize && (value < 8 || value > 65464)) || 45 | (tsize && ((rrq && value !== 0) || value < 0)) || 46 | (windowsize && (value < 1 || value > 65535)) || 47 | (rollover && (value !== 0 && value !== 1))){ 48 | throw errors.EDENY; 49 | } 50 | 51 | if (!extensions) extensions = {}; 52 | extensions[key] = value; 53 | }else if (key === "timeout"){ 54 | //Ignore 55 | continue; 56 | }else{ 57 | userExtensions[key] = value; 58 | } 59 | } 60 | 61 | return { 62 | file: file, 63 | extensions: extensions, 64 | userExtensions: userExtensions 65 | }; 66 | }; -------------------------------------------------------------------------------- /lib/protocol/packets/read-string.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var errors = require ("../errors"); 4 | 5 | module.exports = function (buffer, obj){ 6 | var str = ""; 7 | var bytes = []; 8 | var byte; 9 | 10 | while ((byte = buffer[obj.offset++]) !== 0){ 11 | if (byte === undefined) throw errors.EBADMSG; 12 | bytes.push (byte); 13 | } 14 | 15 | //This is faster than "str = String.fromCharCode.apply (null, bytes)" 16 | for (var i=0; i 512) throw errors.ERBIG; 25 | 26 | var buffer = new Buffer (bytes); 27 | buffer.writeUInt16BE (op, 0); 28 | buffer.write (filename, 2, "ascii"); 29 | buffer.write ("octet", length + 3, "ascii"); 30 | buffer[length + 2] = buffer[length + 8] = 0; 31 | 32 | if (!globalOptions) return buffer; 33 | 34 | var copy = function (key, value){ 35 | buffer.write (key, offset, "ascii"); 36 | offset += key.length; 37 | buffer[offset++] = 0; 38 | buffer.write (value, offset, "ascii"); 39 | offset += value.length; 40 | buffer[offset++] = 0; 41 | }; 42 | 43 | var offset = start; 44 | for (var p in globalOptions.extensionsString){ 45 | copy (p, globalOptions.extensionsString[p]); 46 | }; 47 | 48 | for (var p in userExtensions){ 49 | copy (p, userExtensions[p]); 50 | } 51 | 52 | return buffer; 53 | }; -------------------------------------------------------------------------------- /lib/protocol/packets/wrq.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var opcodes = require ("../opcodes"); 4 | var readRequest = require ("./read-request"); 5 | var writeRequest = require ("./write-request"); 6 | 7 | module.exports = { 8 | serialize: function (filename, globalOptions, opOptions){ 9 | var bytes = 0; 10 | 11 | if (globalOptions){ 12 | //tsize is size 13 | var str = opOptions.size + ""; 14 | globalOptions.extensionsString.tsize = str; 15 | bytes = globalOptions.extensionsLength + str.length; 16 | } 17 | 18 | return writeRequest (opcodes.WRQ, filename, bytes, globalOptions, 19 | opOptions); 20 | }, 21 | deserialize: readRequest 22 | }; -------------------------------------------------------------------------------- /lib/protocol/reader.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var opcodes = require ("./opcodes"); 4 | 5 | var Reader = module.exports = function (Super, args){ 6 | args.reader = true; 7 | Super.call (this, args); 8 | 9 | this._blockSize = null; 10 | this._windowStart = 1; 11 | this._windowEnd = null; 12 | this._windowSize = null; 13 | this._windowBlocksIndex = {}; 14 | this._windowBlocks = []; 15 | this._lastReceived = false; 16 | this._pending = null; 17 | this._oackReceived = null; 18 | this._firstWindow = true; 19 | this._mayRollover = false; 20 | this._rolloverFix = 0; 21 | this._windowStartRollovered = false; 22 | this._noMoreData = false; 23 | this._readerTimer = this._createRetransmitter (); 24 | 25 | var me = this; 26 | this._preFilterFn = function (e){ 27 | return e.block >= me._windowStart; 28 | }; 29 | this._postFilterFn = function (e){ 30 | return e.block < me._windowStart; 31 | }; 32 | this._sortFn = function (a, b){ 33 | return a.block - b.block; 34 | }; 35 | this._restransmitterStartFn = function (){ 36 | var block = me._blockToRetransmit (); 37 | if (block > 0){ 38 | //Update the window and emit back to the client the data 39 | me._windowStart = block === 65535 ? 1 : block + 1; 40 | //The last block could have been received 41 | me._lastReceived = false; 42 | me._notifyWindow (block); 43 | } 44 | me._sendAck (block); 45 | }; 46 | }; 47 | 48 | Reader.prototype._onClose = function (){ 49 | this._readerTimer.reset (); 50 | this.onClose (); 51 | }; 52 | 53 | Reader.prototype._onAbort = function (){ 54 | this._readerTimer.reset (); 55 | this.onAbort (); 56 | }; 57 | 58 | Reader.prototype._onError = function (error){ 59 | this._readerTimer.reset (); 60 | this.onError (error); 61 | }; 62 | 63 | Reader.prototype._onReady = function (stats, rollover, oack){ 64 | //The reader doesn't make use of the rollover option, it's not safe because 65 | //there isn't an specification for default values 66 | this._windowEnd = this._pending = this._windowSize = stats.windowSize; 67 | this._blockSize = stats.blockSize; 68 | 69 | //Start the timer for the first time 70 | this._readerTimer.start (this._restransmitterStartFn); 71 | this._oackReceived = oack; 72 | 73 | this.onStats (stats); 74 | }; 75 | 76 | Reader.prototype._blockToRetransmit = function (){ 77 | if (!this._windowBlocks.length){ 78 | //Resend ACK for the OACK 79 | if (this._oackReceived) return 0; 80 | //Rollover 81 | if (this._windowStart === 0 || 82 | (this._windowStart === 1 && !this._firstWindow)) return 65535; 83 | //First empty window will never happen (oack case treated before) 84 | //This is mostly executed by classic tftp server implementations 85 | return this._windowStart - 1; 86 | } 87 | 88 | //Sort the blocks and find the last well-received one 89 | this._sortWindow (); 90 | //-1 if rollovered to 0 91 | var last = this._windowStart - 1; 92 | 93 | for (var i=0; i [65534, 65535, 0, 1] 112 | var preRoll = this._windowBlocks.filter (this._preFilterFn); 113 | var postRoll = this._windowBlocks.filter (this._postFilterFn); 114 | preRoll.sort (this._sortFn); 115 | postRoll.sort (this._sortFn); 116 | this._windowBlocks = preRoll.concat (postRoll); 117 | }else{ 118 | this._windowBlocks.sort (this._sortFn); 119 | } 120 | }; 121 | 122 | Reader.prototype._notifyWindow = function (block){ 123 | var arr; 124 | 125 | //Emit data 126 | if (block){ 127 | //Error recovery, slow case 128 | //Two loops must be executed because a rollovered window, eg: [65535, 0, 1] 129 | var index = null; 130 | arr = []; 131 | for (var i=0; i this._windowEnd)){ 187 | return; 188 | } 189 | 190 | //Ignore duplicates 191 | if (this._windowBlocksIndex[message.block]) return; 192 | 193 | //Insert new block 194 | this._windowBlocksIndex[message.block] = true; 195 | this._windowBlocks.push (message); 196 | 197 | //Update the pending packets 198 | if (message.data.length < this._blockSize){ 199 | //Last packet 200 | this._pending = message.block - this._windowStart + 1 - 201 | this._windowBlocks.length; 202 | this._lastReceived = true; 203 | }else{ 204 | this._pending--; 205 | } 206 | 207 | if (!this._pending){ 208 | //Cancel the current timer and set it again 209 | this._readerTimer.reset (); 210 | this._readerTimer.start (this._restransmitterStartFn); 211 | 212 | //Sort the blocks 213 | if (this._windowsSize > 1){ 214 | this._sortWindow (); 215 | } 216 | 217 | //Update the window 218 | this._windowStart += this._windowSize; 219 | if (this._windowStart > 65535){ 220 | this._windowStartRollovered = true; 221 | this._windowStart -= 65535 + this._rolloverFix; 222 | } 223 | this._windowEnd = this._windowStart + this._windowSize - 1; 224 | this._mayRollover = this._windowEnd > 65535; 225 | if (this._mayRollover){ 226 | this._windowEnd -= 65535 + this._rolloverFix; 227 | } 228 | 229 | //ACK the current window 230 | this._sendAck ( 231 | this._windowBlocks[this._windowBlocks.length - 1].block); 232 | this._notifyWindow (); 233 | } 234 | }; -------------------------------------------------------------------------------- /lib/protocol/request.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var dgram = require ("dgram"); 4 | var errors = require ("./errors"); 5 | var packets = require ("./packets"); 6 | 7 | var normalizeError = function (error){ 8 | if (!error){ 9 | return errors.EABORT; 10 | }else if (error instanceof Error){ 11 | return errors.wrap (error.message || errors.EABORT.message); 12 | }else{ 13 | return errors.wrap (error + ""); 14 | } 15 | }; 16 | 17 | var Request = module.exports = function (address, port, retries, 18 | timeout){ 19 | this._address = address; 20 | this._port = port; 21 | this._retries = retries; 22 | this._timeout = timeout; 23 | this._socket = null; 24 | this._closed = false; 25 | this._closing = false; 26 | this._aborted = false; 27 | this._error = null; 28 | //The string is modified from the ClientRequest subclass 29 | this._prefixError = ""; 30 | this._requestTimer = this._createRetransmitter (); 31 | }; 32 | 33 | Request.prototype.abort = function (error){ 34 | if (this._closed || this._closing || this._aborted) return; 35 | this._aborted = true; 36 | var me = this; 37 | this._send (packets.error.serialize (normalizeError (error)), function() { me._close () }); 38 | }; 39 | 40 | Request.prototype.close = function (){ 41 | if (this._closed || this._closing || this._aborted) return; 42 | this._close (); 43 | }; 44 | 45 | Request.prototype._close = function (error){ 46 | if (this._closed || this._closing || !this._socket) return; 47 | //If multiples closes occur inside the same tick (because abort() and _close() 48 | //are called in the same tick) the socket throws the error "Not running" 49 | //because the socket is already closed when the second close occurs, this is 50 | //why there's a closing flag 51 | this._closing = true; 52 | 53 | //Store the error after the flag is set to true, otherwise the error could be 54 | //misused by another close 55 | if (error) this._error = error; 56 | 57 | var me = this; 58 | //Close in the next tick to allow sending files in the same tick 59 | process.nextTick (function (){ 60 | me._socket.close (); 61 | }); 62 | }; 63 | 64 | Request.prototype._initSocket = function (socket, onMessage){ 65 | var me = this; 66 | this._onCloseFn = function (){ 67 | me._closed = true; 68 | me._requestTimer.reset (); 69 | if (me._aborted) return me._onAbort (); 70 | if (me._error){ 71 | me._onError (me._error); 72 | }else{ 73 | //Transfer ended successfully 74 | me._onClose (); 75 | } 76 | }; 77 | this._socket = (socket || dgram.createSocket ("udp" + this._ipFamily)) 78 | .on ("error", function (error){ 79 | me._closed = true; 80 | me._requestTimer.reset (); 81 | me._onError (error); 82 | }) 83 | .on ("close", this._onCloseFn) 84 | .on ("message", onMessage); 85 | }; 86 | 87 | Request.prototype._sendAck = function (block){ 88 | this._send (packets.ack.serialize (block)); 89 | }; 90 | 91 | Request.prototype._sendBlock = function (block, buffer){ 92 | this._send (packets.data.serialize (block, buffer)); 93 | }; 94 | 95 | Request.prototype._sendErrorAndClose = function (obj){ 96 | this._send (packets.error.serialize (obj)); 97 | this._closeWithError (obj); 98 | }; 99 | 100 | Request.prototype._closeWithError = function (obj){ 101 | var error = new Error (this._prefixError + obj.message); 102 | if (obj.name) error.code = obj.name; 103 | this._close (error); 104 | }; 105 | 106 | Request.prototype._sendAndRetransmit = function (buffer){ 107 | //Return if the transfer was aborted from inside the stats event (server) 108 | if (this._aborted) return; 109 | this._send (buffer); 110 | var me = this; 111 | this._requestTimer.start (function (){ 112 | me._send (buffer); 113 | }); 114 | }; 115 | 116 | Request.prototype._send = function (buffer, cb){ 117 | if (this._closed || this._closing) return; 118 | this._socket.send (buffer, 0, buffer.length, this._port, this._address, cb); 119 | }; 120 | 121 | Request.prototype._createRetransmitter = function (){ 122 | return new Retransmitter (this); 123 | }; 124 | 125 | var Retransmitter = function (request){ 126 | this._request = request; 127 | this._timer = null; 128 | this._pending = this._request._retries; 129 | }; 130 | 131 | Retransmitter.prototype.reset = function (){ 132 | if (!this._timer) return; 133 | clearTimeout (this._timer); 134 | this._pending = this._request._retries; 135 | this._timer = null; 136 | }; 137 | 138 | Retransmitter.prototype.start = function (fn){ 139 | var me = this; 140 | this._timer = setTimeout (function (){ 141 | if (!me._pending){ 142 | //No more retries 143 | me._request._close (new Error (errors.ETIME.message)); 144 | }else{ 145 | me._pending--; 146 | fn (); 147 | //Try again 148 | me.start (fn); 149 | } 150 | }, this._request._timeout); 151 | }; 152 | 153 | Request.Helper = function (rinfo, family){ 154 | this._rinfo = rinfo; 155 | this._socket = dgram.createSocket ("udp" + family); 156 | }; 157 | 158 | Request.Helper.prototype.abort = function (error){ 159 | this.sendErrorAndClose (normalizeError (error)); 160 | }; 161 | 162 | Request.Helper.prototype.sendErrorAndClose = function (obj){ 163 | var buffer = packets.error.serialize (obj); 164 | var me = this; 165 | this._socket.send (buffer, 0, buffer.length, this._rinfo.port, 166 | this._rinfo.address, function (){ 167 | me._socket.close (); 168 | }); 169 | }; -------------------------------------------------------------------------------- /lib/protocol/server/incoming-request.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var util = require ("util"); 4 | var packets = require ("../packets"); 5 | var opcodes = require ("../opcodes"); 6 | var errors = require ("../errors"); 7 | var Request = require ("../request"); 8 | var knownExtensions = require ("../known-extensions"); 9 | 10 | var IncomingRequest = module.exports = function (args){ 11 | Request.call (this, args.helper._rinfo.address, args.helper._rinfo.port, 12 | args.globalOptions.retries, args.globalOptions.extensions.timeout); 13 | 14 | this._isRRQ = !args.reader; 15 | this._globalOptions = args.globalOptions; 16 | this._maxDataLength = 4; 17 | this._file = args.message.file; 18 | this._size = args.size || null; 19 | this._requestUserExtensions = args.message.userExtensions; 20 | this._responseUserExtensions = null; 21 | this._oackSent = false; 22 | this._firstPacket = true; 23 | this._requestExtensions = args.message.extensions; 24 | this._responseExtensions = null; 25 | 26 | var me = this; 27 | this._initSocket (args.helper._socket, function (message){ 28 | if (me._firstPacket){ 29 | me._firstPacket = false; 30 | me._requestTimer.reset (); 31 | } 32 | me._onMessage (message); 33 | }); 34 | 35 | //The socket is still not bound to an address and port because no packet has 36 | //been still sent, so the stats cannot be emitted yet (the call to address() 37 | //fails) 38 | //The socket must be manually bound 39 | this._socket.bind (0, null, function (){ 40 | if (me._requestExtensions === null){ 41 | //The client doesn't send extensions, it's probably that it doesn't 42 | //rollover automatically (old client), so we can abort the transfer 43 | //prematurely 44 | //Check the size (65535x512) 45 | if (me._isRRQ && me._size > 33553920){ 46 | return me._sendErrorAndClose (errors.EFBIG); 47 | } 48 | me._onReady (me._createStats (512, 1), 0); 49 | if (!me._isRRQ){ 50 | //The ACK of the block 0 is retransmitted from the reader 51 | me._sendAck (0); 52 | } 53 | }else{ 54 | //Send OACK 55 | me._sendOackMessage (me._requestExtensions); 56 | } 57 | }); 58 | }; 59 | 60 | util.inherits (IncomingRequest, Request); 61 | 62 | IncomingRequest.prototype.continueRequest = function (size){ 63 | this._setSize (size); 64 | 65 | if (this._requestExtensions === null){ 66 | this.onContinue (); 67 | }else{ 68 | if( this._responseExtensions.tsize !== undefined){ 69 | this._responseExtensions.tsize = size; 70 | } 71 | 72 | //Set the user extensions 73 | for (var p in this._responseUserExtensions){ 74 | //Ignore invalid extensions 75 | if (knownExtensions[p]) continue; 76 | if (this._requestUserExtensions[p] === undefined) continue; 77 | this._responseExtensions[p] = this._responseUserExtensions[p]; 78 | } 79 | 80 | this._sendAndRetransmit (packets.oack.serialize (this._responseExtensions)); 81 | } 82 | }; 83 | 84 | IncomingRequest.prototype._createStats = function (blockSize, windowSize){ 85 | //Save max data length 86 | this._maxDataLength += blockSize; 87 | 88 | var address = this._socket.address (); 89 | 90 | return { 91 | blockSize: blockSize, 92 | windowSize: windowSize, 93 | size: this._size, 94 | userExtensions: this._requestUserExtensions, 95 | retries: this._globalOptions.retries, 96 | timeout: this._timeout, 97 | localAddress: address.address, 98 | localPort: address.port, 99 | remoteAddress: this._address, 100 | remotePort: this._port, 101 | }; 102 | }; 103 | 104 | IncomingRequest.prototype._onMessage = function (buffer){ 105 | var op = buffer.readUInt16BE (0); 106 | 107 | if (op === opcodes.DATA){ 108 | if (!this._isRRQ){ 109 | if (buffer.length < 4 || buffer.length > this._maxDataLength){ 110 | return this._sendErrorAndClose (errors.EBADMSG); 111 | } 112 | try{ 113 | this._onData (packets.data.deserialize (buffer)); 114 | }catch (error){ 115 | this._sendErrorAndClose (error); 116 | } 117 | }else{ 118 | this._sendErrorAndClose (errors.EBADOP); 119 | } 120 | }else if (op === opcodes.ACK){ 121 | if (this._isRRQ){ 122 | if (buffer.length !== 4){ 123 | return this._sendErrorAndClose (errors.EBADMSG); 124 | } 125 | if (this._oackSent){ 126 | this._oackSent = false; 127 | if (buffer.readUInt16BE (2) !== 0){ 128 | this._sendErrorAndClose (errors.EBADMSG); 129 | }else{ 130 | this.onContinue (); 131 | } 132 | }else{ 133 | try{ 134 | this._onAck (packets.ack.deserialize (buffer)); 135 | }catch (error){ 136 | this._sendErrorAndClose (error); 137 | } 138 | } 139 | }else{ 140 | this._sendErrorAndClose (errors.EBADOP); 141 | } 142 | }else if (op === opcodes.ERROR){ 143 | if (buffer.length < 4) return this._closeWithError (errors.EBADMSG); 144 | try{ 145 | this._close (new Error (packets.error.deserialize (buffer).message)); 146 | }catch (error){ 147 | return this._closeWithError (error); 148 | } 149 | }else{ 150 | this._sendErrorAndClose (errors.EBADOP); 151 | } 152 | }; 153 | 154 | IncomingRequest.prototype._sendOackMessage = function (extensions){ 155 | var ext = {}; 156 | if (extensions.blksize !== undefined){ 157 | var blksize = this._globalOptions.extensions.blksize; 158 | ext.blksize = extensions.blksize > blksize ? blksize : extensions.blksize; 159 | } 160 | if (extensions.windowsize !== undefined){ 161 | var windowsize = this._globalOptions.extensions.windowsize; 162 | ext.windowsize = extensions.windowsize > windowsize 163 | ? windowsize 164 | : extensions.windowsize; 165 | } 166 | if (extensions.tsize !== undefined){ 167 | if (!this._isRRQ) this._size = extensions.tsize; 168 | ext.tsize = extensions.tsize; 169 | //ext.tsize is set later, when the request continues (RRQ) 170 | } 171 | if (extensions.rollover !== undefined){ 172 | ext.rollover = 0; 173 | } 174 | 175 | this._oackSent = true; 176 | var me = this; 177 | var ready = function (){ 178 | me._onReady (me._createStats (ext.blksize || 512, ext.windowsize || 1), 179 | 0, true); 180 | }; 181 | 182 | if (this._isRRQ){ 183 | //Save the extensions 184 | this._responseExtensions = ext; 185 | ready (); 186 | }else{ 187 | ready (); 188 | this._sendAndRetransmit (packets.oack.serialize (ext)); 189 | } 190 | }; -------------------------------------------------------------------------------- /lib/protocol/server/reader.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var Reader = require ("../reader"); 4 | var IncomingRequest = require ("./incoming-request"); 5 | var inherit = require ("../inherit"); 6 | 7 | module.exports = inherit (Reader, IncomingRequest); -------------------------------------------------------------------------------- /lib/protocol/server/writer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var Writer = require ("../writer"); 4 | var IncomingRequest = require ("./incoming-request"); 5 | var inherit = require ("../inherit"); 6 | 7 | module.exports = inherit (Writer, IncomingRequest); -------------------------------------------------------------------------------- /lib/protocol/writer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var Writer = module.exports = function (Super, args){ 4 | Super.call (this, args); 5 | 6 | this._blockMaker = null; 7 | this._window = null; 8 | this._size = args.opOptions ? args.opOptions.size : null; 9 | }; 10 | 11 | Writer.prototype._setSize = function (size){ 12 | this._size = size; 13 | if (this._blockMaker) this._blockMaker._size = size; 14 | }; 15 | 16 | Writer.prototype.send = function (buffer, cb){ 17 | //Slice the given buffer in blocks of a fixed size 18 | this._blockMaker.feed (buffer); 19 | var me = this; 20 | 21 | var next = function (){ 22 | if (me._window.isEOF ()){ 23 | me._onClose = function (){ 24 | me.onClose (); 25 | cb (); 26 | }; 27 | me._close (); 28 | }else{ 29 | newBlock (); 30 | } 31 | }; 32 | 33 | var newBlock = function (){ 34 | //Feed the window till it is full 35 | //A null block signals that no more blocks can be obtained from the buffer 36 | var block = me._blockMaker.next (); 37 | if (!block) return cb (); 38 | me._window.feed (block, next); 39 | }; 40 | 41 | newBlock (); 42 | }; 43 | 44 | Writer.prototype._onClose = function (){ 45 | if (this._window) this._window._writerTimer.reset (); 46 | this.onClose (); 47 | }; 48 | 49 | Writer.prototype._onAbort = function (){ 50 | if (this._window) this._window._writerTimer.reset (); 51 | this.onAbort (); 52 | }; 53 | 54 | Writer.prototype._onError = function (error){ 55 | if (this._window) this._window._writerTimer.reset (); 56 | this.onError (error); 57 | }; 58 | 59 | Writer.prototype._onReady = function (stats, rollover){ 60 | this._blockMaker = new BlockMaker (stats.blockSize, this._size); 61 | this._window = new Window (stats.blockSize, stats.windowSize, rollover, 62 | this); 63 | this.onStats (stats); 64 | }; 65 | 66 | Writer.prototype._onAck = function (ack){ 67 | this._window.resume (ack.block); 68 | }; 69 | 70 | var BlockMaker = function (blockSize, size){ 71 | this._current = 0; 72 | this._blockSize = blockSize; 73 | this._size = size; 74 | this._block = null; 75 | this._buffer = null; 76 | this._p = 0; 77 | this._empty = false; 78 | }; 79 | 80 | BlockMaker.prototype.feed = function (buffer){ 81 | this._buffer = buffer; 82 | }; 83 | 84 | BlockMaker.prototype.next = function (){ 85 | if (this._end) return null; 86 | 87 | if (this._empty){ 88 | this._empty = false; 89 | this._end = true; 90 | this._buffer = null; 91 | return new Buffer (0); 92 | } 93 | 94 | if (this._p === this._buffer.length){ 95 | if (this._size === 0){ 96 | //Empty file 97 | var b = this._buffer; 98 | this._buffer = null; 99 | return b; 100 | } 101 | 102 | this._p = 0; 103 | this._buffer = null; 104 | return null; 105 | } 106 | 107 | var slice; 108 | var block; 109 | 110 | if (this._block){ 111 | //end goes from 1 to blockSize - 1 112 | var end = this._blockSize - this._block.length; 113 | slice = end === this._buffer.length 114 | ? this._buffer 115 | : this._buffer.slice (0, end); 116 | block = Buffer.concat ([this._block, slice], 117 | this._block.length + slice.length); 118 | this._block = null; 119 | }else{ 120 | block = this._buffer.slice (this._p, this._p + this._blockSize); 121 | } 122 | 123 | var nextP = slice ? slice.length : block.length; 124 | this._current += nextP; 125 | 126 | //If the block has a smaller size than blockSize, it's the last block or the 127 | //buffer is smaller than a block 128 | if (block.length < this._blockSize){ 129 | if (this._current === this._size){ 130 | //Last block of the file, return it instead of saving it 131 | this._end = true; 132 | this._buffer = null; 133 | return block; 134 | } 135 | 136 | //Save the block for a later use 137 | this._block = block; 138 | this._p = 0; 139 | this._buffer = null; 140 | return null; 141 | } 142 | 143 | this._p += nextP; 144 | 145 | //The block has a length equal to blockSize 146 | if (this._current === this._size){ 147 | //Last block of the file 148 | //An empty block must be sent 149 | this._empty = true; 150 | } 151 | 152 | return block; 153 | }; 154 | 155 | var Window = function (blockSize, windowSize, rollover, request){ 156 | this._blockSize = blockSize; 157 | this._windowSize = windowSize; 158 | this._rollover = rollover; 159 | this._rolloverFix = rollover === 0 ? 1 : 0; 160 | this._block = 0; 161 | this._start = 1; 162 | this._end = windowSize; 163 | this._pending = windowSize; 164 | this._eof = false; 165 | this._mayRollover = false; 166 | this._blocks = []; 167 | 168 | this._sendFn = function (block){ 169 | request._sendBlock (block.block, block.data); 170 | }; 171 | var me = this; 172 | this._writerTimer = request._createRetransmitter (); 173 | this._restransmitterSendFn = function (){ 174 | me._blocks.forEach (me._sendFn); 175 | }; 176 | }; 177 | 178 | Window.prototype.isEOF = function (){ 179 | return this._eof; 180 | }; 181 | 182 | Window.prototype.feed = function (block, cb){ 183 | //Rollover 184 | if (++this._block === 65536){ 185 | this._block = this._rollover; 186 | } 187 | 188 | this._blocks.push ({ 189 | block: this._block, 190 | data: block 191 | }); 192 | 193 | this._eof = block.length < this._blockSize; 194 | if (this._eof) this._end = this._block; 195 | 196 | if (!--this._pending || this._eof){ 197 | //Wait for the ack 198 | this._cb = cb; 199 | 200 | //Start the timer 201 | this._writerTimer.start (this._restransmitterSendFn); 202 | 203 | //Send the window 204 | this._blocks.forEach (this._sendFn); 205 | }else{ 206 | cb (); 207 | } 208 | }; 209 | 210 | Window.prototype.resume = function (block){ 211 | //Ignore invalid acks (duplicates included) only when the window doesn't 212 | //rollover 213 | if (!this._mayRollover && 214 | (block < this._start - 1 || block > this._end)) return; 215 | 216 | this._writerTimer.reset (); 217 | 218 | if (block !== this._end){ 219 | //Not all the blocks has been received in the server 220 | if (block === this._start - 1){ 221 | //The whole window must be send again 222 | this._writerTimer.start (this._restransmitterSendFn); 223 | this._blocks.forEach (this._sendFn); 224 | }else{ 225 | //Remove the blocks already received and shift the window 226 | while (this._blocks[0].block !== block){ 227 | this._blocks.shift (); 228 | } 229 | this._blocks.shift (); 230 | } 231 | }else{ 232 | this._blocks = []; 233 | } 234 | 235 | //Update the window 236 | this._start = this._block + 1; 237 | if (this._start === 65536){ 238 | this._start = this._rollover; 239 | }else{ 240 | this._mayRollover = true; 241 | } 242 | this._end = this._block + this._windowSize; 243 | if (this._end > 65535){ 244 | this._end -= 65535 + this._rolloverFix; 245 | }else{ 246 | this._mayRollover = false; 247 | } 248 | 249 | this._pending = this._windowSize; 250 | var cb = this._cb; 251 | this._cb = null; 252 | cb (); 253 | }; -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var util = require ("util"); 4 | var events = require ("events"); 5 | var dgram = require ("dgram"); 6 | var fs = require ("fs"); 7 | var net = require ("net"); 8 | var createOptions = require ("./create-options"); 9 | var GetStream = require ("./streams/server/get-stream"); 10 | var PutStream = require ("./streams/server/put-stream"); 11 | var Helper = require ("./protocol/request").Helper; 12 | var errors = require ("./protocol/errors"); 13 | var opcodes = require ("./protocol/opcodes"); 14 | 15 | var Server = module.exports = function (options, listener){ 16 | events.EventEmitter.call (this); 17 | 18 | if (arguments.length === 0){ 19 | options = {}; 20 | }else if (typeof options === "function"){ 21 | listener = options; 22 | options = {}; 23 | } 24 | 25 | options = createOptions (options, true); 26 | this.on ("request", listener || this.requestListener); 27 | 28 | this.root = options.root; 29 | 30 | this._port = options.port; 31 | this._closed = false; 32 | this._currFiles = { 33 | get: {}, 34 | put: {} 35 | }; 36 | 37 | var address = options.address; 38 | if (options.address === "localhost"){ 39 | //IPv4 if localhost 40 | address = "127.0.0.1"; 41 | } 42 | var family = net.isIP (address); 43 | if (!family) throw new Error ("Invalid IP address (server)"); 44 | 45 | this.host = options.address; 46 | this.port = options.port; 47 | 48 | var me = this; 49 | this._socket = dgram.createSocket ("udp" + family) 50 | .on ("error", function (error){ 51 | //The current transfers are not aborted, just wait till all of them 52 | //finish (unlocking the event loop and finishing the process) 53 | //The user also can cache the requests an abort them manually 54 | me.emit ("error", error); 55 | }) 56 | .on ("close", function (){ 57 | me.emit ("close"); 58 | }) 59 | .on ("message", function (message, rinfo){ 60 | //Create a new socket for communicating with the client, the main socket 61 | //only listens to new requests 62 | var helper = new Helper (rinfo, family); 63 | 64 | if (message.length < 9 || message.length > 512){ 65 | //2 op, at least 1 filename, 4 mode mail, 2 NUL 66 | //Max 512 67 | return helper.sendErrorAndClose (errors.EBADMSG); 68 | } 69 | 70 | //Check if it's RRQ or WRQ 71 | var op = message.readUInt16BE (0); 72 | 73 | if (op === opcodes.RRQ){ 74 | if (options.denyGET){ 75 | return helper.sendErrorAndClose (errors.ENOGET); 76 | } 77 | 78 | var gs = new GetStream (); 79 | var ps = new PutStream (me._currFiles, helper, message, options, gs); 80 | ps.onReady = function (){ 81 | me.emit ("request", gs, ps); 82 | }; 83 | }else if (op === opcodes.WRQ){ 84 | if (options.denyPUT){ 85 | return helper.sendErrorAndClose (errors.ENOPUT); 86 | } 87 | 88 | var ps = new PutStream (); 89 | var gs = new GetStream (me._currFiles, helper, message, options, ps); 90 | gs.onReady = function (){ 91 | me.emit ("request", gs, ps); 92 | }; 93 | }else{ 94 | return helper.sendErrorAndClose (errors.EBADOP); 95 | } 96 | }); 97 | }; 98 | 99 | util.inherits (Server, events.EventEmitter); 100 | 101 | Server.prototype.close = function (){ 102 | if (this._closed) return; 103 | this._closed = true; 104 | //Stop the main socket from accepting new connections 105 | this._socket.close (); 106 | }; 107 | 108 | Server.prototype.listen = function (){ 109 | var me = this; 110 | //Validate the root directory 111 | fs.stat (this.root, function (error, stats){ 112 | if (error) return me.emit ("error", error); 113 | if (!stats.isDirectory ()) return me.emit ("error", new Error ("The root " + 114 | "is not a directory")); 115 | 116 | me._socket.bind (me.port, me.host, function (){ 117 | me.emit ("listening"); 118 | }); 119 | }); 120 | }; 121 | 122 | Server.prototype.requestListener = function (req, res){ 123 | if (this._closed) return; 124 | if (req._listenerCalled || req._aborted) return; 125 | req._listenerCalled = true; 126 | 127 | var filename = this.root + "/" + req.file; 128 | 129 | if (req.method === "GET"){ 130 | this._get (filename, req, res); 131 | }else{ 132 | this._put (filename, req); 133 | } 134 | }; 135 | 136 | Server.prototype._get = function (filename, req, res){ 137 | fs.stat (filename, function (error, stats){ 138 | if (error){ 139 | req.on ("abort", function (){ 140 | req.emit ("error", error); 141 | }); 142 | var msg; 143 | if (error.code === "EACCESS" || error.code === "EPERM"){ 144 | msg = errors.EACCESS.message; 145 | }else if (error.code === "ENOENT"){ 146 | msg = errors.ENOENT.message; 147 | }else{ 148 | msg = errors.EIO.message; 149 | } 150 | req.abort (msg); 151 | return; 152 | } 153 | 154 | var aborted = false; 155 | 156 | var rs = fs.createReadStream (filename) 157 | .on ("error", function (error){ 158 | req.on ("abort", function (){ 159 | aborted = true; 160 | req.emit ("error", error); 161 | }); 162 | req.abort (errors.ENOENT.message); 163 | }); 164 | 165 | req.on ("error", function (){ 166 | //Error from the rs 167 | if (aborted) return; 168 | rs.destroy (); 169 | }); 170 | 171 | res.setSize (stats.size); 172 | rs.pipe (res); 173 | }); 174 | }; 175 | 176 | Server.prototype._put = function (filename, req){ 177 | var open = false; 178 | var aborted = false; 179 | var destroy = false; 180 | 181 | req.on ("error", function (){ 182 | //Error from the ws 183 | if (aborted) return; 184 | if (open){ 185 | ws.on ("close", function (){ 186 | fs.unlink (filename, function (){}); 187 | }); 188 | ws.destroy (); 189 | }else{ 190 | destroy = true; 191 | } 192 | }); 193 | 194 | var ws = fs.createWriteStream (filename) 195 | .on ("error", function (error){ 196 | req.on ("abort", function (){ 197 | fs.unlink (filename, function (){ 198 | aborted = true; 199 | req.emit ("error", error); 200 | }); 201 | }); 202 | var msg; 203 | if (error.code === "EACCESS" || error.code === "EPERM"){ 204 | msg = errors.EACCESS.message; 205 | }else{ 206 | msg = errors.EIO.message; 207 | } 208 | req.abort (msg); 209 | }) 210 | .on ("open", function (){ 211 | if (destroy){ 212 | ws.on ("close", function (){ 213 | fs.unlink (filename, function (){}); 214 | }); 215 | ws.destroy (); 216 | }else{ 217 | open = true; 218 | } 219 | }); 220 | 221 | req.pipe (ws); 222 | }; -------------------------------------------------------------------------------- /lib/streams/client/get-stream.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var util = require ("util"); 4 | var stream = require ("stream"); 5 | var crypto = require ("crypto"); 6 | var Reader = require ("../../protocol/client/reader"); 7 | 8 | var GetStream = module.exports = function (remote, globalOptions, getOptions){ 9 | getOptions = getOptions || {}; 10 | 11 | stream.Readable.call (this); 12 | 13 | //Prefer sha1 over md5 if both sums are given 14 | var sum; 15 | if (getOptions.sha1){ 16 | sum = crypto.createHash ("sha1"); 17 | }else if (getOptions.md5){ 18 | sum = crypto.createHash ("md5"); 19 | } 20 | 21 | var me = this; 22 | this._reader = new Reader ({ 23 | file: remote, 24 | globalOptions: globalOptions, 25 | opOptions: getOptions 26 | }); 27 | this._reader.onError = function (error){ 28 | me.emit ("close"); 29 | me.emit ("error", error); 30 | }; 31 | this._reader.onAbort = function (){ 32 | me.emit ("close"); 33 | me.emit ("abort"); 34 | }; 35 | this._reader.onClose = function (){ 36 | me.emit ("close"); 37 | 38 | if (sum){ 39 | var digest = sum.digest ("hex"); 40 | if (getOptions.sha1){ 41 | if (getOptions.sha1.toLowerCase () !== digest){ 42 | return me.emit ("error", new Error ("Invalid SHA1 sum, the file " + 43 | "is corrupted")); 44 | } 45 | }else if (getOptions.md5){ 46 | if (getOptions.md5.toLowerCase () !== digest){ 47 | return me.emit ("error", new Error ("Invalid MD5 sum, the file " + 48 | "is corrupted")); 49 | } 50 | } 51 | } 52 | 53 | me.push (null); 54 | }; 55 | this._reader.onStats = function (stats){ 56 | me.emit ("stats", stats); 57 | }; 58 | this._reader.onData = function (data){ 59 | //The reader emits data chunks with the appropiate order. It guarantees 60 | //that the chunks are ready to be processed by the user 61 | //It decouples the pure implementation of the protocol and the Node.js 62 | //streaming part 63 | if (sum) sum.update (data); 64 | me.push (data); 65 | }; 66 | }; 67 | 68 | util.inherits (GetStream, stream.Readable); 69 | 70 | GetStream.prototype._read = function (){ 71 | //no-op 72 | }; 73 | 74 | GetStream.prototype.abort = function (error){ 75 | this._reader.abort (error); 76 | }; 77 | 78 | GetStream.prototype.close = function (){ 79 | this._reader.close (); 80 | }; -------------------------------------------------------------------------------- /lib/streams/client/put-stream.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var util = require ("util"); 4 | var stream = require ("stream"); 5 | var Writer = require ("../../protocol/client/writer"); 6 | 7 | var PutStream = module.exports = function (remote, globalOptions, putOptions){ 8 | if (putOptions.size === undefined || putOptions.size === null){ 9 | throw new Error ("Missing file size"); 10 | } 11 | 12 | stream.Writable.call (this); 13 | 14 | this._remote = remote; 15 | this._globalOptions = globalOptions; 16 | this._putOptions = putOptions; 17 | this._finished = false; 18 | this._writer = null; 19 | 20 | var me = this; 21 | 22 | if (putOptions.size === 0){ 23 | //Empty file 24 | 25 | //The _write() function is never called so the put request is never done 26 | //Also, the finish listener that the user attachs is automatically called 27 | //but before doing so, the put request must be sent 28 | //Note that the request is initiated when the first chunk is received 29 | //because we need to ensure that the connection with the server has been 30 | //established successfully and the server is ready to receive data, in other 31 | //words, we cannot send data when the request is still not ready. 32 | //Another approach is to establish a connection in the next tick when the 33 | //constructor is called and retain the first chunk when it is received with 34 | //the _write() function previosouly. Then, when onStats() is called, send 35 | //the buffered chunk, but anyway when the file size is 0 it must be handled 36 | //with the next piece of code, so the implemented approach is the best. 37 | 38 | var end = this.end; 39 | this.end = function (){ 40 | this._createWriter (function (){ 41 | //Send an empty buffer 42 | me._writer.send (new Buffer (0), function (){ 43 | end.apply (me, arguments); 44 | }); 45 | }); 46 | }; 47 | } 48 | 49 | this.on ("unpipe", function (){ 50 | //After a finish event the readable stream unpipes the writable stream 51 | if (this._finished) return; 52 | 53 | //The user has called manually to unpipe() 54 | //Abort file transfer 55 | if (this._writer) this._writer.abort (); 56 | }); 57 | 58 | this.on ("finish", function (){ 59 | //The finish event is emitted before unpipe 60 | //This handler is the first that is called when the finish event is emitted 61 | this._finished = true; 62 | }); 63 | }; 64 | 65 | util.inherits (PutStream, stream.Writable); 66 | 67 | PutStream.prototype._createWriter = function (cb){ 68 | var me = this; 69 | this._writer = new Writer ({ 70 | file: this._remote, 71 | globalOptions: this._globalOptions, 72 | opOptions: this._putOptions 73 | }); 74 | this._writer.onError = function (error){ 75 | me.emit ("close"); 76 | me.emit ("error", error); 77 | }; 78 | this._writer.onAbort = function (){ 79 | me.emit ("close"); 80 | me.emit ("abort"); 81 | }; 82 | this._writer.onClose = function (error){ 83 | me.emit ("close"); 84 | }; 85 | this._writer.onStats = function (stats){ 86 | me.emit ("stats", stats); 87 | cb (); 88 | }; 89 | }; 90 | 91 | PutStream.prototype._write = function (chunk, encoding, cb){ 92 | if (this._writer){ 93 | this._writer.send (chunk, cb); 94 | }else{ 95 | var me = this; 96 | this._createWriter (function (){ 97 | me._writer.send (chunk, cb); 98 | }); 99 | } 100 | }; 101 | 102 | PutStream.prototype.abort = function (error){ 103 | if (this._writer) this._writer.abort (error); 104 | }; 105 | 106 | PutStream.prototype.close = function (){ 107 | if (this._writer) this._writer.close (); 108 | }; -------------------------------------------------------------------------------- /lib/streams/server/get-stream.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var util = require ("util"); 4 | var stream = require ("stream"); 5 | var fs = require ("fs"); 6 | var errors = require ("../../protocol/errors"); 7 | var packets = require ("../../protocol/packets"); 8 | var Reader = require ("../../protocol/server/reader"); 9 | 10 | var GetStream = module.exports = function (currFiles, helper, message, 11 | globalOptions, putStream){ 12 | stream.Readable.call (this); 13 | 14 | //RRQ 15 | if (!currFiles) return; 16 | 17 | this._aborted = false; 18 | this._reader = null; 19 | this._currFiles = currFiles; 20 | 21 | //Validate the request 22 | try{ 23 | message = packets.wrq.deserialize (message); 24 | }catch (error){ 25 | return helper.sendErrorAndClose (error); 26 | } 27 | 28 | //Check whether the file can be written 29 | if (this._currFiles.put[message.file]){ 30 | return helper.sendErrorAndClose (errors.ECONPUT); 31 | } 32 | 33 | //Check whether the file can be written 34 | if (this._currFiles.get[message.file]){ 35 | return helper.sendErrorAndClose (errors.ECURGET); 36 | } 37 | 38 | this._currFiles.put[message.file] = true; 39 | 40 | this.method = "PUT"; 41 | this.file = message.file; 42 | 43 | //The put stream needs to call the get stream to pass the user extensions when 44 | //it's a WRQ 45 | putStream._gs = this; 46 | putStream._isWRQ = true; 47 | 48 | this._createReader (helper, message, globalOptions); 49 | }; 50 | 51 | util.inherits (GetStream, stream.Readable); 52 | 53 | GetStream.prototype._read = function (){ 54 | //No-op 55 | }; 56 | 57 | GetStream.prototype.abort = function (error){ 58 | if (this._aborted) return; 59 | this._aborted = true; 60 | if (this._ps){ 61 | this._ps._abort (error); 62 | }else{ 63 | this._reader.abort (error); 64 | } 65 | }; 66 | 67 | GetStream.prototype.close = function (){ 68 | if (this._aborted) return; 69 | this._aborted = true; 70 | if (this._ps){ 71 | this._ps._close (); 72 | }else{ 73 | this._reader.close (); 74 | } 75 | }; 76 | 77 | GetStream.prototype._createReader = function (helper, message, globalOptions){ 78 | var me = this; 79 | this._reader = new Reader ({ 80 | helper: helper, 81 | message: message, 82 | globalOptions: globalOptions 83 | }); 84 | this._reader.onError = function (error){ 85 | delete me._currFiles.put[me.file]; 86 | me.emit ("close"); 87 | me.emit ("error", error); 88 | }; 89 | this._reader.onAbort = function (){ 90 | delete me._currFiles.put[me.file]; 91 | me.emit ("close"); 92 | me.emit ("abort"); 93 | }; 94 | this._reader.onClose = function (){ 95 | delete me._currFiles.put[me.file]; 96 | me.emit ("close"); 97 | me.push (null); 98 | }; 99 | this._reader.onStats = function (stats){ 100 | me.stats = stats; 101 | me.onReady (); 102 | }; 103 | this._reader.onData = function (data){ 104 | //The reader emits data chunks with the appropiate order. It guarantees 105 | //that the chunks are ready to be processed by the user 106 | //It decouples the pure implementation of the protocol and the Node.js 107 | //streaming part 108 | me.push (data); 109 | }; 110 | }; -------------------------------------------------------------------------------- /lib/streams/server/put-stream.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var util = require ("util"); 4 | var stream = require ("stream"); 5 | var fs = require ("fs"); 6 | var path = require ("path"); 7 | var errors = require ("../../protocol/errors"); 8 | var packets = require ("../../protocol/packets"); 9 | var Writer = require ("../../protocol/server/writer"); 10 | 11 | var PutStream = module.exports = function (currFiles, helper, message, 12 | globalOptions, getStream){ 13 | //WRQ 14 | if (!helper) return; 15 | 16 | stream.Writable.call (this); 17 | 18 | this._isWRQ = false; 19 | this._finished = false; 20 | this._writer = null; 21 | this._size = null; 22 | this._continue = false; 23 | this._closed = false; 24 | this._sizeSet = false; 25 | this._currFiles = currFiles; 26 | 27 | //Validate the request 28 | try{ 29 | message = packets.rrq.deserialize (message); 30 | }catch (error){ 31 | return helper.sendErrorAndClose (error); 32 | } 33 | 34 | //Check whether the file can be read 35 | if (this._currFiles.put[message.file]){ 36 | return helper.sendErrorAndClose (errors.ECURPUT); 37 | } 38 | 39 | this._currFiles.get[message.file] = true; 40 | 41 | getStream.method = "GET"; 42 | getStream.file = message.file; 43 | 44 | var me = this; 45 | this.on ("unpipe", function (){ 46 | //After a finish event the readable stream unpipes the writable stream 47 | if (me._finished) return; 48 | 49 | //The user has called manually to unpipe() 50 | //Abort file transfer 51 | if (me._writer) me._writer.abort (); 52 | }); 53 | 54 | this.on ("finish", function (){ 55 | //The finish event is emitted before unpipe 56 | //This handler is the first that is called when the finish event is 57 | //emitted 58 | me._finished = true; 59 | }); 60 | 61 | //Link the streams each other. The put stream is only used to send data to the 62 | //client but the "connection" and all its related events occur in the get 63 | //stream 64 | this._gs = getStream; 65 | getStream._ps = this; 66 | 67 | this._createWriter (helper, message, globalOptions); 68 | }; 69 | 70 | util.inherits (PutStream, stream.Writable); 71 | 72 | PutStream.prototype._abort = function (error){ 73 | this._writer.abort (error); 74 | }; 75 | 76 | PutStream.prototype._close = function (){ 77 | this._writer.close (); 78 | }; 79 | 80 | PutStream.prototype._createWriter = function (helper, message, globalOptions){ 81 | var me = this; 82 | this._writer = new Writer ({ 83 | helper: helper, 84 | message: message, 85 | globalOptions: globalOptions 86 | }); 87 | //The events are emitted using the get stream 88 | this._writer.onError = function (error){ 89 | delete me._currFiles.get[me._gs.file]; 90 | me._gs.emit ("close"); 91 | me._gs.emit ("error", error); 92 | }; 93 | this._writer.onAbort = function (){ 94 | delete me._currFiles.get[me._gs.file]; 95 | me._gs.emit ("close"); 96 | me._gs.emit ("abort"); 97 | }; 98 | this._writer.onClose = function (){ 99 | delete me._currFiles.get[me._gs.file]; 100 | me._closed = true; 101 | me._gs.emit ("close"); 102 | }; 103 | this._writer.onStats = function (stats){ 104 | me._gs.stats = stats; 105 | me.onReady (); 106 | }; 107 | this._writer.onContinue = function (){ 108 | me._continue = true; 109 | }; 110 | }; 111 | 112 | PutStream.prototype._write = function (chunk, encoding, cb){ 113 | if (this._continue){ 114 | this._writer.send (chunk, cb); 115 | }else{ 116 | //Wait until the writer is ready to send data 117 | var me = this; 118 | this._writer.onContinue = function (){ 119 | me._continue = true; 120 | me._writer.send (chunk, cb); 121 | }; 122 | } 123 | }; 124 | 125 | PutStream.prototype.setSize = function (size){ 126 | if (this._isWRQ) throw new Error ("Only GET requests can set the size"); 127 | //Sanity check 128 | if (this._gs._aborted) return; 129 | if (this._sizeSet) throw new Error ("The size was previously set"); 130 | 131 | this._sizeSet = true; 132 | 133 | if (size === 0){ 134 | //Empty file 135 | //The _write() function is never called so the get request is never 136 | //answered 137 | var end = this.end; 138 | var me = this; 139 | this.end = function (){ 140 | if (me._continue){ 141 | //Send an empty buffer 142 | me._writer.send (new Buffer (0), function (){ 143 | end.call (me); 144 | }); 145 | }else{ 146 | //Wait until the writer is ready to end 147 | this._writer.onContinue = function (){ 148 | //Send an empty buffer 149 | me._writer.send (new Buffer (0), function (){ 150 | end.call (me); 151 | }); 152 | }; 153 | } 154 | }; 155 | } 156 | 157 | //The request was "paused" when the request listener was emitted. The call to 158 | //setSize() resumes it in the case of GET requests 159 | this._writer.continueRequest (size); 160 | }; 161 | 162 | PutStream.prototype.setUserExtensions = function (userExtensions){ 163 | if (this._isWRQ){ 164 | this._gs._reader._responseUserExtensions = userExtensions; 165 | }else{ 166 | this._writer._responseUserExtensions = userExtensions; 167 | } 168 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tftp", 3 | "version": "0.1.2", 4 | "description": "Streaming TFTP client and server", 5 | "keywords": ["tftp", "client", "stream"], 6 | "author": "Gabriel Llamas ", 7 | "repository": "git://github.com/gagle/node-tftp.git", 8 | "engines": { 9 | "node": ">=0.10" 10 | }, 11 | "dependencies": { 12 | "argp": "1.0.x", 13 | "status-bar": "2.0.x" 14 | }, 15 | "bin": { 16 | "ntftp": "bin/ntftp.js" 17 | }, 18 | "license": "MIT", 19 | "main": "lib" 20 | } --------------------------------------------------------------------------------