├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── lib ├── fs.filesystem.js ├── fs.js └── ftpd.js ├── package.json └── test ├── data └── hello.txt └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | test/data/* 2 | !test/data/hello.txt 3 | node_modules 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.4 4 | - 0.6 5 | 6 | branches: 7 | only: 8 | - master 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2012 Nicolas Chambrier 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://secure.travis-ci.org/naholyr/node-ftp-server.png)](http://travis-ci.org/naholyr/node-ftp-server) 2 | 3 | # FTP Server -- Simple featureless FTP server 4 | 5 | This is a very simple FTP server. At first it's aimed to simply provide a full-Node implementation of FTP server to be embedded for Unit Testing purpose. 6 | 7 | It's currently highly experimental and could crash anytime. It could become a real FTP server if you want to contribute a bit ;) Don't be afraid: FTP protocol is quite simple. 8 | 9 | ## Install 10 | 11 | ```bash 12 | # Using NPM 13 | npm install ftp-server 14 | ``` 15 | 16 | Or from source: 17 | 18 | ```bash 19 | # Install from sources... 20 | git clone git://github.com/naholyr/node-ftp-server.git ftp-server 21 | cd ftp-server 22 | npm link 23 | 24 | # ...Then in your project 25 | npm link ftp-server 26 | ``` 27 | 28 | You can run unit tests: 29 | 30 | ```bash 31 | # From your project where ftp-server has been installed as a module 32 | npm test ftp-server 33 | 34 | # Or directly from ftp-server 35 | npm test 36 | ``` 37 | 38 | ## Usage 39 | 40 | Example: Simply serve a given directory: 41 | 42 | ```javascript 43 | var ftpd = require('ftp-server') 44 | // Path to your FTP root 45 | ftpd.fsOptions.root = '/path/to/ftp-root' 46 | // Start listening on port 21 (you need to be root for ports < 1024) 47 | ftpd.listen(21) 48 | ``` 49 | 50 | ## Extend server 51 | 52 | Just look at the code. I'll fully document the ways to extend the server with additional features when it's at least more stable. 53 | 54 | ## Paternity 55 | 56 | Note that the original implementation I based my work on was [@billywhizz 's from GitHub](https://github.com/billywhizz/nodeftpd). 57 | 58 | ## Roadmap 59 | 60 | * Add support for rename commands 61 | * Better implementation of `LIST` and `NLST` to be cross-platform 62 | * Add support for `REST` command (restart an interrupted download) 63 | * Maybe wrap all this stuff in a class or at least a function with options (like what FS we'll use) 64 | * Add better documentation on how to extend server (add "features") or new FS wrappers 65 | * Implement MemoryFS 66 | * Support authentication from config or even from database 67 | * Implement all the RFCs from FTP protocol 68 | 69 | ## License: MIT 70 | 71 | ``` 72 | Copyright (C) 2012 Nicolas Chambrier 73 | 74 | Permission is hereby granted, free of charge, to any person obtaining a copy of 75 | this software and associated documentation files (the "Software"), to deal in 76 | the Software without restriction, including without limitation the rights to 77 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 78 | of the Software, and to permit persons to whom the Software is furnished to do 79 | so, subject to the following conditions: 80 | 81 | The above copyright notice and this permission notice shall be included in all 82 | copies or substantial portions of the Software. 83 | 84 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 85 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 86 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 87 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 88 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 89 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 90 | SOFTWARE. 91 | ``` 92 | -------------------------------------------------------------------------------- /lib/fs.filesystem.js: -------------------------------------------------------------------------------- 1 | var Base = require('./fs').Base 2 | , util = require('util') 3 | , path = require('path') 4 | , fs = require('fs') 5 | , spawn = require('child_process').spawn 6 | 7 | module.exports = Filesystem 8 | 9 | util.inherits(Filesystem, Base) 10 | 11 | function Filesystem (options) { 12 | Base.call(this, options) 13 | 14 | this.cwd = '' 15 | } 16 | 17 | Filesystem.prototype.pwd = function () { 18 | return '/' + this.cwd 19 | } 20 | 21 | Filesystem.prototype.chdir = function (dir, cb) { 22 | var new_cwd 23 | if (dir.match(/^\//)) { // Absolute 24 | new_cwd = dir.substring(1) 25 | } else { // Relative 26 | if (path.relative(this.cwd, dir).match(/^\.\./)) { 27 | // Tried to go farther than root 28 | return this.respond(cb, {"code": 431, "message": 'Root it root'}) 29 | } 30 | new_cwd = path.join(this.cwd, dir) 31 | } 32 | var self = this 33 | fs.stat(path.join(this.options.root, new_cwd), function (err, stats) { 34 | if (err || !stats.isDirectory()) { 35 | self.respond(cb, {"code": 431, "message": 'No such directory'}) 36 | } else { 37 | self.cwd = new_cwd 38 | self.respond(cb, null, ['/' + new_cwd]) 39 | } 40 | }) 41 | } 42 | 43 | Filesystem.prototype.list = function (dir, cb) { 44 | var self = this 45 | , target = path.join(this.options.root, this.cwd) 46 | path.exists(target, function (exists) { 47 | if (!exists) { 48 | self.respond(cb, {"code": 431, "message": 'No such directory'}) 49 | } else { 50 | var ls = spawn('ls', ['-l', target]) 51 | , result = '' 52 | ls.stdout.setEncoding(self.encoding) 53 | ls.stdout.on('data', function (chunk) { 54 | result += chunk.toString() 55 | }) 56 | ls.on('exit', function (code) { 57 | var lines = result.split('\n') 58 | result = lines.slice(1, lines.length).join('\r\n') 59 | var err 60 | if (code != 0) { 61 | err = {} 62 | } 63 | self.respond(cb, null, [result]) 64 | }) 65 | } 66 | }) 67 | } 68 | 69 | Filesystem.prototype.readFile = function (file, cb) { 70 | var self = this 71 | , target = path.join(this.options.root, this.cwd, file) 72 | fs.stat(target, function (err, stats) { 73 | if (err || !stats.isFile()) { 74 | self.respond(cb, {"code": 431, "message": 'No such file'}) 75 | } else { 76 | self.respond(cb, null, [fs.createReadStream(target)]) 77 | } 78 | }) 79 | } 80 | 81 | Filesystem.prototype.writeFile = function (file, cb) { 82 | this.respond(cb, null, [fs.createWriteStream(path.join(this.options.root, this.cwd, file))]) 83 | } 84 | 85 | Filesystem.prototype.unlink = function (file, cb) { 86 | var self = this 87 | fs.unlink(path.join(this.options.root, this.cwd, file), function (err) { 88 | self.respond(cb, err) 89 | }) 90 | } 91 | -------------------------------------------------------------------------------- /lib/fs.js: -------------------------------------------------------------------------------- 1 | exports.Base = Base 2 | exports.Filesystem = require('./fs.filesystem.js') 3 | // TODO exports.Memory = require('./fs.memory.js') 4 | 5 | function Base (options) { 6 | this.options = options 7 | } 8 | 9 | Base.prototype.respond = function (cb, err, args) { 10 | if (this.onError) { 11 | if (err) { 12 | this.onError(err) 13 | } else { 14 | cb.apply(this, args || []) 15 | } 16 | } else { 17 | args.unshift(err) 18 | cb.apply(this, args || []) 19 | } 20 | } 21 | 22 | Base.prototype.chdir = function (dir, cb) { 23 | this.respond(cb, new Error('Not implemented yet')) 24 | } 25 | 26 | Base.prototype.pwd = function () { 27 | throw new Error('Not implemented yet') 28 | } 29 | 30 | -------------------------------------------------------------------------------- /lib/ftpd.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | , net = require('net') 3 | , server = module.exports = net.createServer() 4 | , commands 5 | , messages 6 | 7 | /** 8 | * FS emulator 9 | */ 10 | var fsWrapper = require('./fs') 11 | server.fsOptions = {} 12 | 13 | /** 14 | * Patch server.close 15 | */ 16 | server.closing = false 17 | var original_server_close = server.close 18 | server.close = function () { 19 | this.closing = true 20 | original_server_close.call(this) 21 | } 22 | 23 | /** 24 | * Some information when listening (should be removed) 25 | */ 26 | server.on('listening', function () { 27 | console.log('Server listening on ' + server.address().address + ':' + server.address().port) 28 | }) 29 | 30 | /** 31 | * When server receives a new client socket 32 | */ 33 | server.on('connection', function (socket) { 34 | /** 35 | * Configure client connection info 36 | */ 37 | socket.setTimeout(0) 38 | socket.setNoDelay() 39 | socket.dataEncoding = "binary" 40 | socket.asciiEncoding = "utf8" 41 | socket.passive = false 42 | socket.dataInfo = null 43 | socket.username = null 44 | 45 | /** 46 | * Initialize filesystem 47 | */ 48 | socket.fs = new fsWrapper.Filesystem(server.fsOptions) 49 | // Catch-all 50 | socket.fs.onError = function (err) { 51 | if (!err.code) err.code = 550 52 | socket.reply(err.code, err.message) 53 | } 54 | 55 | /** 56 | * Socket response shortcut 57 | */ 58 | socket.server = server 59 | socket.reply = function (status, message, callback) { 60 | if (!message) message = messages[status.toString()] || 'No information' 61 | if (this.writable) { 62 | this.write(status.toString() + ' ' + message.toString() + '\r\n', callback) 63 | } 64 | } 65 | 66 | /** 67 | * Data transfer 68 | */ 69 | socket.dataTransfer = function (handle) { 70 | function finish (dataSocket) { 71 | return function (err) { 72 | if (err) { 73 | dataSocket.emit('error', err) 74 | } else { 75 | dataSocket.end() 76 | } 77 | } 78 | } 79 | function execute () { 80 | socket.reply(150) 81 | handle.call(socket, this, finish(this)) 82 | } 83 | // Will be unqueued in PASV command 84 | if (socket.passive) { 85 | socket.dataTransfer.queue.push(execute) 86 | } 87 | // Or we initialize directly the connection to the client 88 | else { 89 | dataSocket = net.createConnection(socket.dataInfo.port, socket.dataInfo.host) 90 | dataSocket.on('connect', execute) 91 | } 92 | } 93 | socket.dataTransfer.queue = [] 94 | 95 | /** 96 | * When socket has established connection, reply with a hello message 97 | */ 98 | socket.on('connect', function () { 99 | socket.reply(220) 100 | }) 101 | 102 | /** 103 | * Received a command from socket 104 | */ 105 | socket.on('data', function (chunk) { 106 | /** 107 | * If server is closing, refuse all commands 108 | */ 109 | if (server.closing) { 110 | socket.reply(421) 111 | } 112 | /** 113 | * Parse received command and reply accordingly 114 | */ 115 | var parts = trim(chunk.toString()).split(" ") 116 | , command = trim(parts[0]).toUpperCase() 117 | , args = parts.slice(1, parts.length) 118 | , callable = commands[command] 119 | if (!callable) { 120 | socket.reply(502) 121 | } else { 122 | callable.apply(socket, args) 123 | } 124 | }) 125 | }) 126 | 127 | /** 128 | * Standard messages for status (RFC 959) 129 | */ 130 | messages = exports.messages = { 131 | "200": "Command okay.", 132 | "500": "Syntax error, command unrecognized.", // This may include errors such as command line too long. 133 | "501": "Syntax error in parameters or arguments.", 134 | "202": "Command not implemented, superfluous at this site.", 135 | "502": "Command not implemented.", 136 | "503": "Bad sequence of commands.", 137 | "504": "Command not implemented for that parameter.", 138 | "110": "Restart marker reply.", // In this case, the text is exact and not left to the particular implementation; it must read: MARK yyyy = mmmm Where yyyy is User-process data stream marker, and mmmm server's equivalent marker (note the spaces between markers and "="). 139 | "211": "System status, or system help reply.", 140 | "212": "Directory status.", 141 | "213": "File status.", 142 | "214": "Help message.", // On how to use the server or the meaning of a particular non-standard command. This reply is useful only to the human user. 143 | "215": "NodeFTP server emulator.", // NAME system type. Where NAME is an official system name from the list in the Assigned Numbers document. 144 | "120": "Service ready in %s minutes.", 145 | "220": "Service ready for new user.", 146 | "221": "Service closing control connection.", // Logged out if appropriate. 147 | "421": "Service not available, closing control connection.", // This may be a reply to any command if the service knows it must shut down. 148 | "125": "Data connection already open; transfer starting.", 149 | "225": "Data connection open; no transfer in progress.", 150 | "425": "Can't open data connection.", 151 | "226": "Closing data connection.", // Requested file action successful (for example, file transfer or file abort). 152 | "426": "Connection closed; transfer aborted.", 153 | "227": "Entering Passive Mode.", // (h1,h2,h3,h4,p1,p2). 154 | "230": "User logged in, proceed.", 155 | "530": "Not logged in.", 156 | "331": "User name okay, need password.", 157 | "332": "Need account for login.", 158 | "532": "Need account for storing files.", 159 | "150": "File status okay; about to open data connection.", 160 | "250": "Requested file action okay, completed.", 161 | "257": "\"%s\" created.", 162 | "350": "Requested file action pending further information.", 163 | "450": "Requested file action not taken.", // File unavailable (e.g., file busy). 164 | "550": "Requested action not taken.", // File unavailable (e.g., file not found, no access). 165 | "451": "Requested action aborted. Local error in processing.", 166 | "551": "Requested action aborted. Page type unknown.", 167 | "452": "Requested action not taken.", // Insufficient storage space in system. 168 | "552": "Requested file action aborted.", // Exceeded storage allocation (for current directory or dataset). 169 | "553": "Requested action not taken.", // File name not allowed. 170 | } 171 | 172 | /** 173 | * Commands implemented by the FTP server 174 | */ 175 | commands = exports.commands = { 176 | /** 177 | * Unsupported commands 178 | * They're specifically listed here as a roadmap, but any unexisting command will reply with 202 Not supported 179 | */ 180 | "ABOR": function () { this.reply(202) }, // Abort an active file transfer. 181 | "ACCT": function () { this.reply(202) }, // Account information 182 | "ADAT": function () { this.reply(202) }, // Authentication/Security Data (RFC 2228) 183 | "ALLO": function () { this.reply(202) }, // Allocate sufficient disk space to receive a file. 184 | "APPE": function () { this.reply(202) }, // Append. 185 | "AUTH": function () { this.reply(202) }, // Authentication/Security Mechanism (RFC 2228) 186 | "CCC": function () { this.reply(202) }, // Clear Command Channel (RFC 2228) 187 | "CONF": function () { this.reply(202) }, // Confidentiality Protection Command (RFC 697) 188 | "ENC": function () { this.reply(202) }, // Privacy Protected Channel (RFC 2228) 189 | "EPRT": function () { this.reply(202) }, // Specifies an extended address and port to which the server should connect. (RFC 2428) 190 | "EPSV": function () { this.reply(202) }, // Enter extended passive mode. (RFC 2428) 191 | "HELP": function () { this.reply(202) }, // Returns usage documentation on a command if specified, else a general help document is returned. 192 | "LANG": function () { this.reply(202) }, // Language Negotiation (RFC 2640) 193 | "LPRT": function () { this.reply(202) }, // Specifies a long address and port to which the server should connect. (RFC 1639) 194 | "LPSV": function () { this.reply(202) }, // Enter long passive mode. (RFC 1639) 195 | "MDTM": function () { this.reply(202) }, // Return the last-modified time of a specified file. (RFC 3659) 196 | "MIC": function () { this.reply(202) }, // Integrity Protected Command (RFC 2228) 197 | "MKD": function () { this.reply(202) }, // Make directory. 198 | "MLSD": function () { this.reply(202) }, // Lists the contents of a directory if a directory is named. (RFC 3659) 199 | "MLST": function () { this.reply(202) }, // Provides data about exactly the object named on its command line, and no others. (RFC 3659) 200 | "MODE": function () { this.reply(202) }, // Sets the transfer mode (Stream, Block, or Compressed). 201 | "NOOP": function () { this.reply(202) }, // No operation (dummy packet; used mostly on keepalives). 202 | "OPTS": function () { this.reply(202) }, // Select options for a feature. (RFC 2389) 203 | "REIN": function () { this.reply(202) }, // Re initializes the connection. 204 | "STOU": function () { this.reply(202) }, // Store file uniquely. 205 | "STRU": function () { this.reply(202) }, // Set file transfer structure. 206 | "PBSZ": function () { this.reply(202) }, // Protection Buffer Size (RFC 2228) 207 | "SITE": function () { this.reply(202) }, // Sends site specific commands to remote server. 208 | "SMNT": function () { this.reply(202) }, // Mount file structure. 209 | "RMD": function () { this.reply(202) }, // Remove a directory. 210 | "STAT": function () { this.reply(202) }, // 211 | /** 212 | * General info 213 | */ 214 | "FEAT": function () { 215 | this.write('211-Extensions supported\r\n') 216 | // No feature 217 | this.reply(211, 'End') 218 | }, 219 | "SYST": function () { 220 | this.reply(215, 'Node FTP featureless server') 221 | }, 222 | /** 223 | * Path commands 224 | */ 225 | "CDUP": function () { // Change to parent directory 226 | commands.CWD.call(this, '..') 227 | }, 228 | "CWD": function (dir) { // Change working directory 229 | var socket = this 230 | socket.fs.chdir(dir, function (cwd) { 231 | socket.reply(250, 'Directory changed to "' + cwd + '"') 232 | }) 233 | }, 234 | "PWD": function () { // Get working directory 235 | this.reply(257, '"' + this.fs.pwd() + '"') 236 | }, 237 | "XPWD": function() { // Alias to PWD 238 | commands.PWD.call(this) 239 | }, 240 | /** 241 | * Change data encoding 242 | */ 243 | "TYPE": function (dataEncoding) { 244 | if (dataEncoding == "A" || dataEncoding == "I") { 245 | this.dataEncoding = (dataEncoding == "A") ? this.asciiEncoding : "binary" 246 | this.reply(200) 247 | } else { 248 | this.reply(501) 249 | } 250 | }, 251 | /** 252 | * Authentication 253 | */ 254 | "USER": function (username) { 255 | this.username = username 256 | this.reply(331) 257 | }, 258 | "PASS": function (password) { 259 | // Automatically accept password 260 | this.reply(230) 261 | }, 262 | /** 263 | * Passive mode 264 | */ 265 | "PASV": function () { // Enter passive mode 266 | var socket = this 267 | , dataServer = net.createServer() 268 | socket.passive = true 269 | dataServer.on('connection', function (dataSocket) { 270 | dataSocket.setEncoding(socket.dataEncoding) 271 | dataSocket.on('connect', function () { 272 | // Unqueue method that has been queued previously 273 | if (socket.dataTransfer.queue.length) { 274 | socket.dataTransfer.queue.shift().call(dataSocket) 275 | } else { 276 | dataSocket.emit('error', {"code": 421}) 277 | socket.end() 278 | } 279 | }).on('close', function () { 280 | socket.reply(this.error ? 426 : 226) 281 | dataServer.close() 282 | }).on('error', function (err) { 283 | this.error = err 284 | socket.reply(err.code || 500, err.message) 285 | }) 286 | }).on('listening', function () { 287 | var port = this.address().port 288 | , host = server.address().address 289 | socket.dataInfo = { "host": host, "port": port } 290 | socket.reply(227, 'PASV OK (' + host.split('.').join(',') + ',' + parseInt(port/256,10) + ',' + (port%256) + ')') 291 | }).listen() 292 | }, 293 | /** 294 | * TODO Active mode 295 | */ 296 | "PORT": function (info) { 297 | this.reply(202) 298 | // Specifies an address and port to which the server should connect. 299 | /*socket.passive = false; 300 | var addr = command[1].split(","); 301 | socket.pasvhost = addr[0]+"."+addr[1]+"."+addr[2]+"."+addr[3]; 302 | socket.pasvport = (parseInt(addr[4]) * 256) + parseInt(addr[5]); 303 | socket.send("200 PORT command successful.\r\n");*/ 304 | }, 305 | /** 306 | * Filesystem 307 | */ 308 | "LIST": function (target) { 309 | var socket = this 310 | socket.dataTransfer(function (dataSocket, finish) { 311 | socket.fs.list(target || socket.fs.pwd(), function (result) { 312 | dataSocket.write(result + '\r\n', finish) 313 | }) 314 | }) 315 | }, 316 | "NLST": function (target) { 317 | // TODO: just the list of file names 318 | this.reply(202) 319 | }, 320 | "RETR": function (file) { 321 | var socket = this 322 | socket.dataTransfer(function (dataSocket, finish) { 323 | socket.fs.readFile(file, function (stream) { 324 | stream.on('data', function (chunk) { 325 | dataSocket.write(chunk, socket.dataEncoding) 326 | }) 327 | stream.on('end', function () { 328 | dataSocket.end() 329 | }) 330 | }) 331 | }) 332 | }, 333 | "STOR": function (file) { 334 | var socket = this 335 | socket.dataTransfer(function (dataSocket, finish) { 336 | socket.fs.writeFile(file, function (stream) { 337 | dataSocket.on('data', function (chunk) { 338 | stream.write(chunk, socket.dataEncoding) 339 | }) 340 | dataSocket.on('end', function () { 341 | stream.end() 342 | }) 343 | }) 344 | }) 345 | }, 346 | "DELE": function (file) { 347 | var socket = this 348 | socket.fs.unlink(file, function () { 349 | socket.reply(250) 350 | }) 351 | }, 352 | "RNFR": function (name) { 353 | this.reply(202) 354 | // Rename from. 355 | /*socket.filefrom = socket.fs.cwd() + command[1].trim(); 356 | socket.send("350 File exists, ready for destination name.\r\n");*/ 357 | }, 358 | "RNTO": function (name) { 359 | this.reply(202) 360 | // Rename to. 361 | /*var fileto = socket.fs.cwd() + command[1].trim(); 362 | rn = sys.exec("mv " + socket.filefrom + " " + fileto); 363 | rn.addCallback(function (stdout, stderr) { 364 | socket.send("250 file renamed successfully\r\n"); 365 | }); 366 | rn.addErrback(function () { 367 | socket.send("250 file renamed successfully\r\n"); 368 | });*/ 369 | }, 370 | /** 371 | * Allow restart interrupted transfer 372 | */ 373 | "REST": function (start) { 374 | this.reply(202) 375 | // Restart transfer from the specified point. 376 | /*socket.totsize = parseInt(command[1].trim()); 377 | socket.send("350 Rest supported. Restarting at " + socket.totsize + "\r\n");*/ 378 | }, 379 | /** 380 | * Disconnection 381 | */ 382 | "QUIT": function () { 383 | this.reply(221) 384 | this.end() 385 | } 386 | } 387 | 388 | function trim (string) { 389 | return string.replace(/^\s+|\s+$/g,"") 390 | } 391 | 392 | if (!module.parent) { 393 | server.fsOptions.root = path.resolve(__dirname, '..', 'test', 'data') 394 | server.listen(21) 395 | } 396 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Nicolas Chambrier (http://naholyr.fr)", 3 | "name": "ftp-server", 4 | "description": "Featureless FTP server", 5 | "version": "0.1.0", 6 | "homepage": "https://github.com/naholyr/node-ftp-server", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/naholyr/node-ftp-server.git" 10 | }, 11 | "main": "lib/ftpd.js", 12 | "engines": { 13 | "node": ">= 0.4" 14 | }, 15 | "dependencies": {}, 16 | "devDependencies": {}, 17 | "scripts": { 18 | "test": "node test" 19 | }, 20 | "license": { 21 | "type": "MIT", 22 | "url": "https://raw.github.com/naholyr/node-ftp-server/master/LICENSE" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/data/hello.txt: -------------------------------------------------------------------------------- 1 | Hello, world ! 2 | Line 2 3 | Line 3 4 | With some «UTF8» chæract€rs 5 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var ftpd = require('..') 2 | ftpd.fsOptions.root = require('path').resolve(__dirname, 'data') 3 | ftpd.on('listening', function () { 4 | var server = this, client = new FTPClient(server.address()) 5 | client.on('connect', function () { 6 | console.log('connected') 7 | client.once('2xx', function () { 8 | client.send('USER', 'anonymous', function (code, message) { 9 | console.log('TODO ?') 10 | client.end() 11 | }) 12 | }) 13 | }) 14 | client.on('close', function () { 15 | console.log('disconnected') 16 | server.close() 17 | server.on('close', function () { 18 | console.log('server closed') 19 | }) 20 | }) 21 | }) 22 | ftpd.listen() 23 | 24 | 25 | require('util').inherits(FTPClient, require('events').EventEmitter) 26 | 27 | function FTPClient (address) { 28 | var self = this, client = this.client = require('net').createConnection(address.port, address.address) 29 | client.on('connect', function () { self.emit('connect', client) }) 30 | client.on('close', function () { self.emit('close', client) }) 31 | client.on('data', function (chunk) { 32 | var lines = chunk.toString().split('\n'), parts, status, text 33 | for (var i=0; i