UNPKG

node-sftp-server

Version:

Node.js SFTP Server bindings to implement your own SFTP Server

480 lines (412 loc) 14.6 kB
"use strict"; var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty, slice = [].slice; var ssh2 = require('ssh2'); var ssh2_stream = require('ssh2-streams'); var SFTP = ssh2_stream.SFTPStream; var tmp = require('tmp'); tmp.setGracefulCleanup(); var Readable = require('stream').Readable; var Writable = require('stream').Writable; var Transform = require('stream').Transform; var EventEmitter = require("events").EventEmitter; var fs = require('fs'); var constants = require('constants'); var Responder = (function(superClass) { extend(Responder, superClass); Responder.Statuses = { "denied": "PERMISSION_DENIED", "nofile": "NO_SUCH_FILE", "end": "EOF", "ok": "OK", "fail": "FAILURE", "bad_message": "BAD_MESSAGE", "unsupported": "OP_UNSUPPORTED" }; function Responder(sftpStream1, req1) { var fn, methodname, ref, symbol; this.req = req1; this.sftpStream = sftpStream1; ref = this.constructor.Statuses; fn = (function(_this) { return function(symbol) { return _this[methodname] = function() { _this.done = true; return _this.sftpStream.status(_this.req, ssh2.SFTP_STATUS_CODE[symbol]); }; }; })(this); for (methodname in ref) { symbol = ref[methodname]; fn(symbol); } } return Responder; })(EventEmitter); var DirectoryEmitter = (function(superClass) { extend(DirectoryEmitter, superClass); function DirectoryEmitter(sftpStream1, req1) { this.sftpStream = sftpStream1; this.req = req1 != null ? req1 : null; this.stopped = false; this.done = false; DirectoryEmitter.__super__.constructor.call(this, sftpStream1, this.req); } DirectoryEmitter.prototype.request_directory = function(req) { this.req = req; if (!this.done) { return this.emit("dir"); } else { return this.end(); } }; DirectoryEmitter.prototype.file = function(name, attrs) { if (typeof attrs === 'undefined') { attrs = {}; } this.stopped = this.sftpStream.name(this.req, { filename: name.toString(), longname: name.toString(), attrs: attrs }); if (!this.stopped && !this.done) { return this.emit("dir"); } }; return DirectoryEmitter; })(Responder); var ContextWrapper = (function() { function ContextWrapper(ctx1, server) { this.ctx = ctx1; this.server = server; this.method = this.ctx.method; this.username = this.ctx.username; this.password = this.ctx.password; } ContextWrapper.prototype.reject = function(methodsLeft, isPartial) { return this.ctx.reject(methodsLeft, isPartial); }; ContextWrapper.prototype.accept = function(callback) { if (callback == null) { callback = function() {}; } this.ctx.accept(); return this.server._session_start_callback = callback; }; return ContextWrapper; })(); var debug = function(msg) {}; var SFTPServer = (function(superClass) { extend(SFTPServer, superClass); function SFTPServer(options) { // Expose options for the other classes to read. if (!options) options = { privateKeyFile: 'ssh_host_rsa_key' }; if (typeof options === 'string') options = { privateKeyFile: options }; // Original constructor had just a privateKey string, so this preserves backwards compatibility. if (options.debug) { debug = function(msg) { console.log(msg); }; } SFTPServer.options = options; this.server = new ssh2.Server({ hostKeys: [fs.readFileSync(options.privateKeyFile)] }, (function(_this) { return function(client, info) { client.on('error', function(err) { debug("SFTP Server: error"); return _this.emit("error", err); }); client.on('authentication', function(ctx) { debug("SFTP Server: on('authentication')"); _this.auth_wrapper = new ContextWrapper(ctx, _this); return _this.emit("connect", _this.auth_wrapper, info); }); client.on('end', function() { debug("SFTP Server: on('end')"); return _this.emit("end"); }); return client.on('ready', function(channel) { client._sshstream.debug = debug; return client.on('session', function(accept, reject) { var session; session = accept(); return session.on('sftp', function(accept, reject) { var sftpStream; sftpStream = accept(); session = new SFTPSession(sftpStream); return _this._session_start_callback(session); }); }); }); }; })(this)); } SFTPServer.prototype.listen = function(port) { return this.server.listen(port); }; return SFTPServer; })(EventEmitter); module.exports = SFTPServer var Statter = (function() { function Statter(sftpStream1, reqid1) { this.sftpStream = sftpStream1; this.reqid = reqid1; } Statter.prototype.is_file = function() { return this.type = constants.S_IFREG; }; Statter.prototype.is_directory = function() { return this.type = constants.S_IFDIR; }; Statter.prototype.file = function() { return this.sftpStream.attrs(this.reqid, this._get_statblock()); }; Statter.prototype.nofile = function() { return this.sftpStream.status(this.reqid, ssh2.SFTP_STATUS_CODE.NO_SUCH_FILE); }; Statter.prototype._get_mode = function() { return this.type | this.permissions; }; Statter.prototype._get_statblock = function() { return { mode: this._get_mode(), uid: this.uid, gid: this.gid, size: this.size, atime: this.atime, mtime: this.mtime }; }; return Statter; })(); var SFTPFileStream = (function(superClass) { extend(SFTPFileStream, superClass); function SFTPFileStream() { return SFTPFileStream.__super__.constructor.apply(this, arguments); } SFTPFileStream.prototype._read = function(size) {}; return SFTPFileStream; })(Readable); var SFTPSession = (function(superClass) { extend(SFTPSession, superClass); SFTPSession.Events = [ "REALPATH", "STAT", "LSTAT", "FSTAT", "OPENDIR", "CLOSE", "REMOVE", "READDIR", "OPEN", "READ", "WRITE", "RENAME", "MKDIR", "RMDIR" ]; function SFTPSession(sftpStream1) { var event, fn, i, len, ref; this.sftpStream = sftpStream1; this.max_filehandle = 0; this.handles = {}; ref = this.constructor.Events; fn = (function(_this) { return function(event) { return _this.sftpStream.on(event, function() { var args; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; debug('DEBUG: SFTP Session Event: ' + event); return _this[event].apply(_this, args); }); }; })(this); for (i = 0, len = ref.length; i < len; i++) { event = ref[i]; fn(event); } } SFTPSession.prototype.fetchhandle = function() { var prevhandle; prevhandle = this.max_filehandle; this.max_filehandle++; return new Buffer(prevhandle.toString()); }; SFTPSession.prototype.REALPATH = function(reqid, path) { var callback; if (EventEmitter.listenerCount(this, "realpath")) { callback = (function(_this) { return function(name) { return _this.sftpStream.name(reqid, { filename: name, longname: "-rwxrwxrwx 1 foo foo 3 Dec 8 2009 " + name, attrs: {} }); }; })(this); return this.emit("realpath", path, callback); } else { return this.sftpStream.name(reqid, { filename: path, longname: path, attrs: {} }); } }; SFTPSession.prototype.do_stat = function(reqid, path, kind) { if (EventEmitter.listenerCount(this, "stat")) { return this.emit("stat", path, kind, new Statter(this.sftpStream, reqid)); } else { console.log("WARNING: No stat function for " + kind + ", all files exist!"); return this.sftpStream.attrs(reqid, { filename: path, longname: path, attrs: {} }); } }; SFTPSession.prototype.STAT = function(reqid, path) { return this.do_stat(reqid, path, 'STAT'); }; SFTPSession.prototype.LSTAT = function(reqid, path) { return this.do_stat(reqid, path, 'LSTAT'); }; SFTPSession.prototype.FSTAT = function(reqid, handle) { return this.do_stat(reqid, this.handles[handle].path, 'FSTAT'); }; SFTPSession.prototype.OPENDIR = function(reqid, path) { var diremit; diremit = new DirectoryEmitter(this.sftpStream, reqid); diremit.on("newListener", (function(_this) { return function(event, listener) { var handle; if (event !== "dir") { return; } handle = _this.fetchhandle(); _this.handles[handle] = { mode: "OPENDIR", path: path, loc: 0, responder: diremit }; return _this.sftpStream.handle(reqid, handle); }; })(this)); return this.emit("readdir", path, diremit); }; SFTPSession.prototype.READDIR = function(reqid, handle) { var ref; if (((ref = this.handles[handle]) != null ? ref.mode : void 0) !== "OPENDIR") { return this.sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.NO_SUCH_FILE); } return this.handles[handle].responder.request_directory(reqid); }; SFTPSession.prototype.OPEN = function(reqid, pathname, flags, attrs) { var handle, rs, started, stringflags, ts; stringflags = SFTP.flagsToString(flags); switch (stringflags) { case "r": // Create a temporary file to hold stream contents. var options = {}; if (SFTPServer.options.temporaryFileDirectory) options.dir = SFTPServer.options.temporaryFileDirectory; return tmp.file(options, function (err, tmpPath, fd) { if (err) throw err; handle = this.fetchhandle(); this.handles[handle] = { mode: "READ", path: pathname, finished: false, tmpPath: tmpPath, tmpFile: fd }; var writestream = fs.createWriteStream(tmpPath); writestream.on("finish", function () { this.handles[handle].finished = true; }.bind(this)); this.emit("readfile", pathname, writestream); return this.sftpStream.handle(reqid, handle); }.bind(this)); case "w": rs = new Readable(); started = false; rs._read = (function(_this) { return function(bytes) { if (started) { return; } handle = _this.fetchhandle(); _this.handles[handle] = { mode: "WRITE", path: pathname, stream: rs }; _this.sftpStream.handle(reqid, handle); return started = true; }; })(this); return this.emit("writefile", pathname, rs); default: return this.emit("error", new Error("Unknown open flags: " + stringflags)); } }; SFTPSession.prototype.READ = function(reqid, handle, offset, length) { var localHandle = this.handles[handle]; // Once our readstream is at eof, we're done reading into the // buffer, and we know we can check against it for EOF state. if (localHandle.finished) { return fs.stat(localHandle.tmpPath, function(err, stats) { if (err) throw err; if (offset >= stats.size) { return this.sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.EOF); } else { var buffer = Buffer.alloc(length); return fs.read(localHandle.tmpFile, buffer, 0, length, offset, function (err, bytesRead, buffer) { return this.sftpStream.data(reqid, buffer.slice(0, bytesRead)); }.bind(this)); } }.bind(this)); } // If we're not at EOF from the buffer yet, we either need to put more data // down the wire, or need to wait for more data to become available. return fs.stat(localHandle.tmpPath, function(err, stats) { if (stats.size >= offset + length) { var buffer = Buffer.alloc(length); return fs.read(localHandle.tmpFile, buffer, 0, length, offset, function (err, bytesRead, buffer) { return this.sftpStream.data(reqid, buffer.slice(0, bytesRead)); }.bind(this)); } else { // Wait for more data to become available. setTimeout(function() { this.READ(reqid, handle, offset, length); }.bind(this), 50); } }.bind(this)); }; SFTPSession.prototype.WRITE = function(reqid, handle, offset, data) { this.handles[handle].stream.push(data); return this.sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.OK); }; SFTPSession.prototype.CLOSE = function(reqid, handle) { //return this.sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.OK); if (this.handles[handle]) { switch (this.handles[handle].mode) { case "OPENDIR": this.handles[handle].responder.emit("end"); delete this.handles[handle]; return this.sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.OK); case "READ": delete this.handles[handle]; return this.sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.OK); case "WRITE": this.handles[handle].stream.push(null); //delete this.handles[handle]; //can't delete it while it's still going, right? return this.sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.OK); default: return this.sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.FAILURE); } } }; SFTPSession.prototype.REMOVE = function(reqid, path) { return this.emit("delete", path, new Responder(this.sftpStream, reqid)); }; SFTPSession.prototype.RENAME = function(reqid, oldPath, newPath) { return this.emit("rename", oldPath, newPath, new Responder(this.sftpStream, reqid)); }; SFTPSession.prototype.MKDIR = function(reqid, path) { return this.emit("mkdir", path, new Responder(this.sftpStream, reqid)); }; SFTPSession.prototype.RMDIR = function(reqid, path) { return this.emit("rmdir", path, new Responder(this.sftpStream, reqid)); }; return SFTPSession; })(EventEmitter);