├── .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 | [](http://badge.fury.io/js/tftp "Fury Version Badge")
7 | [](https://david-dm.org/gagle/node-tftp "David Dependency Manager Badge")
8 |
9 | [](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 | Window size | Improvement |
174 | 1 | -0% |
175 | 2 | -49% |
176 | 3 | -64% |
177 | 4 | -70% |
178 | 5 | -73% |
179 | 6 | -76% |
180 |
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 | }
--------------------------------------------------------------------------------