UNPKG

tftp

Version:

Streaming TFTP client and server

662 lines (583 loc) 17.8 kB
#!/usr/bin/env node "use strict"; var fs = require ("fs"); var path = require ("path"); var readLine = require ("readline"); var url = require ("url"); var argp = require ("argp"); var statusBar = require ("status-bar"); var tftp = require ("../lib"); var normalizeFilename = require ("../lib/normalize-filename"); var errors = require ("../lib/protocol/errors"); var client; var rl; var timer; var read; var write; var filename; var renderStatusBar = function (stats){ process.stdout.write (filename + " " + this.format.storage (stats.currentSize) + " " + this.format.speed (stats.speed) + " " + this.format.time (stats.remainingTime) + " [" + this.format.progressBar (stats.percentage) + "] " + this.format.percentage (stats.percentage)); process.stdout.cursorTo (0); }; var formatFilename = function (filename){ //80 - 59 var filenameMaxLength = 21; if (filename.length > filenameMaxLength){ filename = filename.slice (0, filenameMaxLength - 3) + "..."; }else{ var remaining = filenameMaxLength - filename.length; while (remaining--){ filename += " "; } } return filename; }; var parseUri = function (uri){ var o = url.parse (uri); if (o.protocol !== "tftp:"){ return { error: new Error ("The protocol must be 'tftp'") }; } if (!o.path){ return { error: new Error ("Bad uri") }; } var arr = o.path.slice (1).split (";mode="); if (arr[1] && arr[1] !== "octet"){ return this.fail (new Error ("The transfer mode must be 'octet'")); } return { host: o.host, port: o.port, file: arr[0] }; }; var notifyError = function (error, prompt){ console.error ("Error: " + error.message); if (prompt) rl.prompt (); }; var again = function (){ timer = setTimeout (function (){ timer = null; }, 3000); console.log ("\n(^C again to quit)"); rl.line = ""; rl.prompt (); }; var normalizeGetFiles = function (remote, local){ remote += ""; local = (local || remote) + ""; try{ remote = normalizeFilename (remote); }catch (error){ throw error; } return { remote: remote, local: local }; }; var normalizePutFiles = function (local, remote){ local += ""; remote = (remote || path.basename (local)) + ""; try{ remote = normalizeFilename (remote); }catch (error){ throw error; } return { remote: remote, local: local }; }; var setMainOptions = function (body){ //The default values are set inside the lib body .option ({ short: "b", long: "blksize", metavar: "SIZE", type: Number, description: "Size of each data chunk. Valid range: " + "[8, 65464]. Default is 1468, the MTU size before IP fragmentation " + "in Ethernet networks" }) .option ({ short: "r", long: "retries", metavar: "NUM", type: Number, description: "Number of retries before aborting the " + "file transfer due to an unresponsive server or a massive packet " + "loss" }) .option ({ short: "t", long: "timeout", metavar: "MILLISECONDS", type: Number, description: "Milliseconds to wait before the next " + "retry. Default is 3000ms" }) .option ({ short: "w", long: "windowsize", metavar: "SIZE", type: Number, description: "Size of the window. The window will " + "send SIZE data chunks in a row before waiting for an ack. Valid " + "range: [1, 65535]. Default is 4" }) .help () }; //Removing the module from the cache is not necessary because a second instance //will be used during the whole program lifecycle var main = argp.createParser () .main () .allowUndefinedArguments () .readPackage (__dirname + "/../package.json", { email: false }) .usages ([ "ntftp [options] <host>[:<port>]", "ntftp get [options] <rfc3617_uri> [<local>]", "ntftp put [options] [<local>] <rfc3617_uri>", ]) .on ("argument", function (argv, argument, ignore){ if (argv.server) this.fail ("Too many arguments"); argument = argument.split (":"); argv.server = { host: argument[0], port: argument[1] }; //Don't save the <host>[:port] argument ignore (); }) .on ("end", function (argv){ if (!argv.server) this.printHelp (); if (argv.listen){ createServer (argv); }else{ createClient (argv); } }) .body () .text ("By default, the client sends to the server some de facto " + "extensions trying to achieve the best performance. If the " + "server doesn't support these extensions, it automatically " + "fallbacks to a pure RFC 1350 compliant TFTP client " + "implementation.\n\n" + "This utility can be used with the built-in shell or " + "directly with a command.\n\n" + "Shell:\n" + " Arguments:") .columns (" <host>[:<port>]", "The address and port of the " + "server, e.g.\n$ ntftp localhost:1234") .text ("\nOnce the shell is running, it shows a prompt and " + "recognizes the following commands:\n" + " get, put.\n\n" + "'<command> -h' for more information.\n\n" + "To quit the program press ctrl-c two times.\n\n" + "Example:\n" + " $ ntftp localhost -w 2 --blksize 256\n" + " > get remote_file\n" + " > get remote_file local_file\n" + " > put path/to/local_file remote_file", " ") .text ("\nCommands:\n" + " get, put.\n\n" + " 'ntftp <command> -h' for more information.\n\n" + "To start a server use the following option:") .option ({ short: "l", long: "listen", metavar: "ROOT", description: "Starts the server. The ROOT is the directory " + "from where the files are served" }) .text ("\nCommon options:"); setMainOptions (main); var setCommandBody = function (body){ body .text ("RFC 3617 uri:") .text ("tftp://<host>[:<port>]/<remote>[;mode=<transfer_mode>]", " ") .text ("\nTransfer mode:") .text ("The only supported mode is 'octet', that is, all the files are " + "assumed to be binary files, therefore the content is not " + "modified. Because the 'mode' parameter is optional it can just be " + "ignored.", " ") }; var command = main .command ("get", { trailing: { min: 1, max: 2 } }) .usages (["ntftp get [options] <rfc3617_uri> [<local>]"]) .description ("GETs a file from the server") .on ("end", function (argv){ var o = parseUri (argv.get[0] + ""); if (o.error) return this.fail (o.error); try{ var files = normalizeGetFiles (o.file, argv.get[1]); }catch (error){ return this.fail (error); } argv.server = { host: o.host, port: o.port }; createClient (argv, true); get (files.remote, files.local, function (error){ if (error) notifyError (error); process.exit (); }); }) .body (); setCommandBody (command); command .text ("\nExample:") .text ("$ ntftp get -w 2 tftp://localhost/file\n\n" + "GETs a file named 'file' from the server in 'octet' mode with " + "a window size of 2.", " ") .text ("\nOptions:") setMainOptions (command); var command = main .command ("put", { trailing: { min: 1, max: 2 } }) .usages (["ntftp put [options] [<local>] <rfc3617_uri>"]) .description ("PUTs a file into the server") .on ("end", function (argv){ var o = parseUri (argv.put[argv.put.length - 1] + ""); if (o.error) return this.fail (o.error); try{ var files = normalizePutFiles ( argv.put.length === 1 ? o.file : argv.put[0], o.file); }catch (error){ return this.fail (error); } argv.server = { host: o.host, port: o.port }; createClient (argv, true); put (files.local, files.remote, function (error){ if (error) notifyError (error); process.exit (); }); }) .body (); setCommandBody (command); command .text ("\nExample:") .text ("$ ntftp put tftp://localhost/file\n\n" + "PUTs a file named 'file' into the server in 'octet' mode.", " ") .text ("\nOptions:") setMainOptions (command); //Start parsing main.argv (); //Free the parsers main = command = null; function createShellParser (){ return argp.createParser () .main () //Don't produce errors when undefined arguments and options are //introduced, they are simply ignored because anyway, if the end event //is executed, it will fail .allowUndefinedArguments () .allowUndefinedOptions () .on ("end", function (){ notifyError (new Error ("Invalid command ('get' or 'put')"), true); }) .on ("error", function (error){ notifyError (error, true); }) .command ("get", { trailing: { min: 1, max: 2 } }) .usages (["get [options] <remote> [<local>]"]) .description ("GETs a file from the server") .on ("option", function (argv, option, value, long, ignore){ //Capture the help option because the prompt needs to be displayed //after the help message if (this.options ({ short: !long, long: long })[option].id === "help"){ this.printHelp (); ignore (); rl.prompt (); } }) .on ("end", function (argv){ try{ var files = normalizeGetFiles (argv.get[0], argv.get[1]); }catch (error){ return notifyError (error, true); } get (files.remote, files.local, function (error){ if (error) return notifyError (error, true); rl.prompt (); }); }) .on ("error", function (error){ notifyError (error, true); }) .body () .text ("Options:") .help () .command ("put", { trailing: { min: 1, max: 2 } }) .usages (["put [options] <local> [<remote>]"]) .description ("PUTs a file into the server") .on ("option", function (argv, option, value, long, ignore){ //Capture the help option because the prompt needs to be displayed //after the help message if (this.options ({ short: !long, long: long })[option].id === "help"){ this.printHelp (); ignore (); rl.prompt (); } }) .on ("end", function (argv){ try{ var files = normalizePutFiles (argv.put[0], argv.put[1]); }catch (error){ return notifyError (error, true); } put (files.local, files.remote, function (error){ if (error) return notifyError (error, true); rl.prompt (); }); }) .on ("error", function (error){ notifyError (error, true); }) .body () .text ("Options:") .help (); }; function createServer (argv){ tftp.createServer ({ root: argv.listen, host: argv.server.host, port: argv.server.port, blockSize: argv.blksize, retries: argv.retries, timeout: argv.timeout, windowSize: argv.windowsize }) .on ("error", notifyError) .on ("request", function (req){ console.log ("CONNECTION [" + req.stats.remoteAddress + ":" + req.stats.remotePort + "] " + req.method + " " + req.file); req.on ("error", function (error){ console.error ("ERROR [" + req.stats.remoteAddress + ":" + req.stats.remotePort + "] " + req.method + " " + req.file + " - " + error.message); }); req.on ("close", function (){ console.log ("CLOSE [" + req.stats.remoteAddress + ":" + req.stats.remotePort + "] " + req.method + " " + req.file); }); }) .on ("listening", function (){ console.log ("Listening on " + this.host + ":" + this.port + " (root: '" + path.resolve (this.root) + "')"); }) .listen (); }; function createClient (argv, onlySigint){ client = tftp.createClient ({ host: argv.server.host, port: argv.server.port, blockSize: argv.blksize, retries: argv.retries, timeout: argv.timeout, windowSize: argv.windowsize }); createPrompt (onlySigint); }; function createPrompt (onlySigint){ if (onlySigint){ rl = readLine.createInterface ({ input: process.stdin, output: process.stdout, }); rl.on ("SIGINT", function (){ //Abort the current transfer if (read){ read.gs.abort (); }else if (write){ write.ps.abort (); }else{ process.exit (); } }); return; } var parser = createShellParser (); var completions = ["get ", "put "]; //Start prompt rl = readLine.createInterface ({ input: process.stdin, output: process.stdout, completer: function (line){ var hits = completions.filter (function (command){ return command.indexOf (line) === 0; }); return [hits.length ? hits : [], line]; } }); rl.on ("line", function (line){ if (!line) return rl.prompt (); parser.argv (line.split (" ").filter (function (word){ return word; })); }); rl.on ("SIGINT", function (){ if (timer){ console.log (); process.exit (); } //Abort the current transfer if (read){ read.gs.abort (); }else if (write){ write.ps.abort (); }else{ again (); } }); rl.prompt (); }; function get (remote, local, cb){ clearTimeout (timer); timer = null; //Check if local is a dir and prevent from starting a request fs.stat (local, function (error, stats){ if (error){ if (error.code !== "ENOENT") return cb (error); }else if (stats.isDirectory ()){ return cb (new Error ("The local file is a directory")); } filename = formatFilename (remote); var started = false; var bar = null; var noExtensionsTimer = null; read = {}; var open = false; var destroy = null; read.gs = client.createGetStream (remote) .on ("error", function (error){ if (bar) bar.cancel (); clearInterval (noExtensionsTimer); if (started) console.log (); if (open){ read.ws.on ("close", function (){ fs.unlink (local, function (){ read = null; cb (error); }); }); read.ws.destroy (); }else{ destroy = error; } }) .on ("abort", function (){ if (bar) bar.cancel (); clearInterval (noExtensionsTimer); if (started) console.log (); if (read.error){ //The error comes from the ws fs.unlink (local, function (){ var error = read.error; read = null; cb (error); }); }else{ read.ws.on ("close", function (){ read = null; fs.unlink (local, function (){ cb (); }); }); read.ws.destroy (); } }) .on ("stats", function (stats){ started = true; if (stats.size !== null){ bar = statusBar.create ({ total: stats.size }) .on ("render", renderStatusBar) this.pipe (bar); }else{ //The server doesn't support extensions var dots = "..."; var i = 1; noExtensionsTimer = setInterval (function (){ i = i%4; process.stdout.clearLine (); process.stdout.cursorTo (0); process.stdout.write (dots.slice (0, i++)); }, 200); } }); read.ws = fs.createWriteStream (local) .on ("error", function (error){ read.error = error; read.gs.abort (errors.EIO); }) .on ("open", function (){ if (destroy){ read.ws.on ("close", function (){ fs.unlink (local, function (){ cb (destroy); }); }); read.ws.destroy (); }else{ open = true; } }) .on ("finish", function (){ read = null; clearInterval (noExtensionsTimer); console.log (); cb (); }); read.gs.pipe (read.ws); }); }; function put (local, remote, cb){ clearTimeout (timer); timer = null; //Check if local is a dir or doesn't exist to prevent from starting a new //request fs.stat (local, function (error, stats){ if (error) return cb (error); if (stats.isDirectory ()){ return cb (new Error ("The local file is a directory")); } filename = formatFilename (local); var bar = statusBar.create ({ total: stats.size }) .on ("render", renderStatusBar) write = {}; write.rs = fs.createReadStream (local) .on ("error", function (error){ write.error = error; write.ps.abort (errors.EIO); }) .on ("close", function (){ write = null; }); write.ps = client.createPutStream (remote, { size: stats.size }) .on ("error", function (error){ if (bar) bar.cancel (); console.log (); if (!write){ //Empty origin file cb (error); }else{ write.rs.on ("close", function (){ write = null; cb (error); }); write.rs.destroy (); } }) .on ("abort", function (){ if (bar) bar.cancel (); console.log (); if (write.error){ //The error comes from the rs var error = write.error; write = null; cb (error); }else{ var rs = write.rs; rs.on ("close", cb); rs.destroy (); } }) .on ("finish", function (){ write = null; console.log (); cb (); }); write.rs.pipe (write.ps); write.rs.pipe (bar); }); };