blob: bba01061c7e0cb23b43e77f37995dc821d71ddb3 [file] [log] [blame]
// Copyright (c) 2017 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
nassh.sftp = {};
/**
* A SFTP Client that manages the sending and receiving of SFTP packets.
*
* @param {string} [opt_basePath] The base directory for client requests.
*/
nassh.sftp.Client = function(opt_basePath='') {
// The version of the protocol we're using.
this.protoVersion = 3;
// The packet request id counter.
this.requestId_ = 0;
// The nacl plugin for communication.
this.plugin_ = null;
// Whether the SFTP connection has been initialized
this.isInitialised = false;
// Directory to prefix all path requests.
if (opt_basePath) {
// Make sure the path always ends with a slash. This simplifies
// the path logic in the rest of the client.
if (!opt_basePath.endsWith('/'))
opt_basePath += '/';
}
this.basePath_ = opt_basePath;
// The buffered packet data coming from the plugin.
this.buffer_ = '';
// A map of pending packet requests.
// Takes a requestId for a key and a Promise as a value.
this.pendingRequests = {};
// A map of currently opened files.
// Takes a openRequestId for a key and a file handle as a value.
this.openedFiles = {};
};
/**
* Stream wants to write some packet data to the client.
*/
nassh.sftp.Client.prototype.writeUTF8 = function(data) {
// add data to buffer
this.buffer_ += data;
// loop over buffer until all packets have been handled.
try {
var rv = true;
// Exit on false return value, else keep looping through valid packets
while(rv && this.buffer_.length > 4) { // 4 bytes needed to read len field
rv = this.parseBuffer();
}
} catch (e) {
console.warn(e.name + ': ' + e.message);
console.warn(e.stack);
}
};
/**
* Parse the buffer and process the first valid packet in it, else return false.
*/
nassh.sftp.Client.prototype.parseBuffer = function() {
// create packet containing the buffer
var packet = new nassh.sftp.Packet(this.buffer_);
// get packet data length
var dataLength = packet.getUint32();
// check the buffer contains a full and valid packet
if (dataLength > this.buffer_.length - 4 && dataLength > 4) {
return false;
}
// slice buffer packet to contain the expected packet
packet.slice(0, dataLength + 4);
// remove the expected packet from buffer
this.buffer_ = this.buffer_.substr(packet.getLength());
// onPacket handler, will return true if valid, else false
var rv = this.onPacket(packet);
return rv;
};
/**
* onPacket handler. Will read the response packet's type and find the
* requesting packet's callback. If the response packet is valid and has a valid
* request id, it will be returned with the callback. Finding and executing
* a callback will return true, else will return false if an error occurred.
*/
nassh.sftp.Client.prototype.onPacket = function(packet) {
var packetType = packet.getUint8();
if (packetType == nassh.sftp.packets.RequestPackets.VERSION) {
this.pendingRequests['init']();
return true;
}
// Obtain the response packet's constructor and create it.
var ResponseType = nassh.sftp.packets.ResponsePackets[packetType]
|| nassh.sftp.UnknownPacket;
var responsePacket = new ResponseType(packet);
// get request id and execute the callback (if found)
var requestId = responsePacket.requestId;
if(this.pendingRequests.hasOwnProperty(requestId)) {
this.pendingRequests[requestId](responsePacket);
delete this.pendingRequests[requestId];
} else {
throw new TypeError('Received reply to unknown request:', requestId);
}
return true;
};
/**
* Initializes SFTP connection.
*/
nassh.sftp.Client.prototype.initConnection = function(plugin) {
this.plugin_ = plugin;
this.init();
};
/**
* Send a message to the nassh plugin.
*
* @param {string} name The name of the message to send.
* @param {Array} arguments The message arguments.
*/
nassh.sftp.Client.prototype.sendToPlugin_ = function(name, args) {
var str = JSON.stringify({name: name, arguments: args});
this.plugin_.postMessage(str);
};
/**
* Sends a SFTP request and awaits the response.
*
* @param {number} type SFTP packet type of outgoing request
* @param {!nassh.sftp.Packet} data The body of the request (not including
* length, type and requestId)
* @return {!Promise} A Promise that resolves with the response packet
*/
nassh.sftp.Client.prototype.sendRequest_ = function(type, data) {
if (!this.isInitialised) {
throw new Error('Tried sending a SFTP request before the connection had'
+ ' been initialized.');
}
var requestId = this.requestId_++;
var packet = new nassh.sftp.Packet();
packet.setUint32(data.getLength() + 5);
packet.setUint8(type);
packet.setUint32(requestId);
packet.setData(data);
return new Promise(resolve => {
this.pendingRequests[requestId] = resolve;
this.sendToPlugin_('onRead', [0, btoa(packet.toString())]);
});
};
/**
* Checks to see whether the response packet is of the expected type or not.
*/
nassh.sftp.Client.prototype.isExpectedResponse_ = function(responsePacket,
expectedPacket, requestType) {
if (responsePacket instanceof nassh.sftp.packets.StatusPacket) {
throw new nassh.sftp.StatusError(responsePacket, requestType);
}
if (!(responsePacket instanceof expectedPacket)) {
throw new TypeError('Received unexpected response to '
+ requestType + ' packet: ' + responsePacket);
}
return responsePacket;
};
/**
* Checks to see whether the response packet was a successful status packet.
*/
nassh.sftp.Client.prototype.isSuccessResponse_ = function(responsePacket,
requestType) {
if (!(responsePacket instanceof nassh.sftp.packets.StatusPacket)) {
throw new TypeError('Received unexpected response to '
+ requestType + ' packet: ' + responsePacket);
}
if (responsePacket.code != nassh.sftp.packets.StatusCodes.OK) {
throw new nassh.sftp.StatusError(responsePacket, requestType);
}
return responsePacket;
};
/**
* Checks to see whether the response packet was a name packet.
*/
nassh.sftp.Client.prototype.isNameResponse_ = function(responsePacket,
requestType) {
if (responsePacket instanceof nassh.sftp.packets.StatusPacket) {
if (responsePacket.code != nassh.sftp.packets.StatusCodes.EOF)
throw new nassh.sftp.StatusError(responsePacket, requestType);
// EOF
return responsePacket;
}
if (!(responsePacket instanceof nassh.sftp.packets.NamePacket))
throw new TypeError('Received unexpected response to '
+ requestType + ' packet: ' + responsePacket);
return responsePacket;
};
/**
* Sends a SFTP init packet.
*/
nassh.sftp.Client.prototype.init = function() {
var packet = new nassh.sftp.Packet();
packet.setUint32(5); // length, 5 bytes for type and version fields
packet.setUint8(nassh.sftp.packets.RequestPackets.INIT);
packet.setUint32(this.protoVersion);
this.pendingRequests['init'] = () => {
console.log('init: SFTP');
this.isInitialised = true;
};
this.sendToPlugin_('onRead', [0, btoa(packet.toString())]);
};
/**
* Retrieves status information for a remote file.
*
* @param {string} path The path of the remote file
* @return {!Promise<!AttrsPacket>} A Promise that resolves with the remote
* file attributes, or rejects (usually with an nassh.sftp.StatusError)
*/
nassh.sftp.Client.prototype.fileStatus = function(path) {
var packet = new nassh.sftp.Packet();
packet.setString(this.basePath_ + path);
return this.sendRequest_(nassh.sftp.packets.RequestPackets.STAT, packet)
.then(response => this.isExpectedResponse_(response, nassh.sftp.packets.AttrsPacket, 'STAT'))
.then(response => response.attrs);
};
/**
* Retrieves status information for a remote symlink.
*
* @param {string} path The path of the remote symlink
* @return {!Promise<!AttrsPacket>} A Promise that resolves with the remote
* file attributes, or rejects (usually with an nassh.sftp.StatusError)
*/
nassh.sftp.Client.prototype.linkStatus = function(path) {
var packet = new nassh.sftp.Packet();
packet.setString(this.basePath_ + path);
return this.sendRequest_(nassh.sftp.packets.RequestPackets.LSTAT, packet)
.then(response => this.isExpectedResponse_(response, nassh.sftp.packets.AttrsPacket, 'LSTAT'))
.then(response => response.attrs);
};
/**
* Retrieves status information for a remote file handle.
*
* @param {string} handle The open file handle
* @return {!Promise<!AttrsPacket>} A Promise that resolves with the remote
* file attributes, or rejects (usually with an nassh.sftp.StatusError)
*/
nassh.sftp.Client.prototype.fileHandleStatus = function(handle) {
var packet = new nassh.sftp.Packet();
packet.setString(handle);
return this.sendRequest_(nassh.sftp.packets.RequestPackets.FSTAT, packet)
.then(response => this.isExpectedResponse_(response, nassh.sftp.packets.AttrsPacket, 'FSTAT'))
.then(response => response.attrs);
};
/**
* Sets status information for a remote file.
*
* @param {string} path The path of the remote file
* @param {Object} attrs The file attributes to set (see the structure
* nassh.sftp.packets.getFileAttrs sets up)
* @return {!Promise<!AttrsPacket>} A Promise that resolves with the remote
* file attributes, or rejects (usually with an nassh.sftp.StatusError)
*/
nassh.sftp.Client.prototype.setFileStatus = function(path, attrs) {
var packet = new nassh.sftp.Packet();
packet.setString(path);
nassh.sftp.packets.setFileAttrs(packet, attrs);
return this.sendRequest_(nassh.sftp.packets.RequestPackets.SETSTAT, packet)
.then(response => this.isSuccessResponse_(response, 'SETSTAT'));
};
/**
* Sets status information for a remote file handle.
*
* @param {string} handle The open file handle
* @param {Object} attrs The file attributes to set (see the structure
* nassh.sftp.packets.getFileAttrs sets up)
* @return {!Promise<!AttrsPacket>} A Promise that resolves with the remote
* file attributes, or rejects (usually with an nassh.sftp.StatusError)
*/
nassh.sftp.Client.prototype.setFileHandleStatus = function(handle, attrs) {
var packet = new nassh.sftp.Packet();
packet.setString(handle);
nassh.sftp.packets.setFileAttrs(packet, attrs);
return this.sendRequest_(nassh.sftp.packets.RequestPackets.FSETSTAT, packet)
.then(response => this.isSuccessResponse_(response, 'FSETSTAT'));
};
/**
* Opens a remote directory.
*
* @param {string} path The path of the remote directory
* @return {!Promise<!HandlePacket>} A Promise that resolves with the remote
* directory handle, or rejects (usually with an nassh.sftp.StatusError)
*/
nassh.sftp.Client.prototype.openDirectory = function(path) {
var packet = new nassh.sftp.Packet();
packet.setString(this.basePath_ + path);
return this.sendRequest_(nassh.sftp.packets.RequestPackets.OPENDIR, packet)
.then(response => this.isExpectedResponse_(response, nassh.sftp.packets.HandlePacket, 'OPENDIR'))
.then(response => response.handle);
};
/**
* Reads the contents of a remote directory.
*
* @param {string} handle The handle of the remote directory
* @return {!Promise<!NamePacket>} A Promise that resolves with the remote
* directory contents, or rejects (usually with an nassh.sftp.StatusError)
*/
nassh.sftp.Client.prototype.readDirectory = function(handle) {
var packet = new nassh.sftp.Packet();
packet.setString(handle);
return this.sendRequest_(nassh.sftp.packets.RequestPackets.READDIR, packet)
.then(response => this.isNameResponse_(response, 'READDIR'));
};
/**
* Removes a remote directory.
*
* @param {string} path The path of the remote directory
* @return {!Promise<!StatusPacket>} A Promise that resolves or rejects with
* a nassh.sftp.StatusError.
*/
nassh.sftp.Client.prototype.removeDirectory = function(path) {
var packet = new nassh.sftp.Packet();
packet.setString(this.basePath_ + path);
return this.sendRequest_(nassh.sftp.packets.RequestPackets.RMDIR, packet)
.then(response => this.isSuccessResponse_(response, 'RMDIR'));
};
/**
* Opens a remote file.
*
* @param {string} path The path of the remote file
* @param {number} pflags The open flags for the remote file
* @return {!Promise<!HandlePacket>} A Promise that resolves with the remote
* file handle, or rejects (usually with an nassh.sftp.StatusError)
*/
nassh.sftp.Client.prototype.openFile = function(path, pflags) {
var packet = new nassh.sftp.Packet();
packet.setString(this.basePath_ + path);
packet.setUint32(pflags); // open flags
packet.setUint32(0); // default attr values
return this.sendRequest_(nassh.sftp.packets.RequestPackets.OPEN, packet)
.then(response => this.isExpectedResponse_(response, nassh.sftp.packets.HandlePacket, 'OPEN'))
.then(response => response.handle);
};
/**
* Reads a remote file.
*
* @param {string} handle The handle of the remote file
* @param {number} offset The offset to start reading from
* @param {number} len The maximum number of bytes to read
* @return {!Promise<!DataPacket>} A Promise that resolves with the remote
* file data, or rejects (usually with an nassh.sftp.StatusError)
*/
nassh.sftp.Client.prototype.readFile = function(handle, offset, len) {
var packet = new nassh.sftp.Packet();
packet.setString(handle);
packet.setUint64(offset); //offset
packet.setUint32(len); // max bytes per packet
return this.sendRequest_(nassh.sftp.packets.RequestPackets.READ, packet)
.then(response => {
if (response instanceof nassh.sftp.packets.StatusPacket) {
if (response.code != nassh.sftp.packets.StatusCodes.EOF) {
throw new nassh.sftp.StatusError(response, 'READ');
}
return ''; // EOF, return empty data string
}
if (!(response instanceof nassh.sftp.packets.DataPacket)) {
throw new TypeError('Received unexpected response to READ packet: '
+ response);
}
return response.data;
});
};
/**
* Closes a remote file handle.
*
* @param {string} handle The handle of the remote file
* @return {!Promise<!StatusPacket>} A Promise that resolves or rejects with
* a nassh.sftp.StatusError
*/
nassh.sftp.Client.prototype.closeFile = function(handle) {
var packet = new nassh.sftp.Packet();
packet.setString(handle);
return this.sendRequest_(nassh.sftp.packets.RequestPackets.CLOSE, packet)
.then(response => this.isSuccessResponse_(response, 'CLOSE'));
};
/**
* Removes a remote file.
*
* @param {string} handle The handle of the remote file
* @return {!Promise<!StatusPacket>} A Promise that resolves or rejects with
* a nassh.sftp.StatusError
*/
nassh.sftp.Client.prototype.removeFile = function(path) {
var packet = new nassh.sftp.Packet();
packet.setString(this.basePath_ + path);
return this.sendRequest_(nassh.sftp.packets.RequestPackets.REMOVE, packet)
.then(response => this.isSuccessResponse_(response, 'REMOVE'));
};
/**
* Renames the path name of a remote file.
*
* @param {string} sourcePath The source path of the remote file
* @param {string} targetPath The target path of the remote file
* @return {!Promise<!StatusPacket>} A Promise that resolves or rejects with
* a nassh.sftp.StatusError
*/
nassh.sftp.Client.prototype.renameFile = function(sourcePath, targetPath) {
var packet = new nassh.sftp.Packet();
packet.setString(sourcePath);
packet.setString(targetPath);
return this.sendRequest_(nassh.sftp.packets.RequestPackets.RENAME, packet)
.then(response => this.isSuccessResponse_(response, 'RENAME'));
};
/**
* Writes to a remote file.
*
* @param {string} handle The handle of the remote file
* @param {number} offset The offset to start writing from
* @param {string} data The data to write
* @return {!Promise<!StatusPacket>} A Promise that resolves or rejects with
* a nassh.sftp.StatusError
*/
nassh.sftp.Client.prototype.writeFile = function(handle, offset, data) {
var packet = new nassh.sftp.Packet();
packet.setString(handle);
packet.setUint64(offset);
packet.setString(data);
return this.sendRequest_(nassh.sftp.packets.RequestPackets.WRITE, packet)
.then(response => this.isSuccessResponse_(response, 'WRITE'));
};
/**
* Creates a new remote directory.
*
* @param {string} path The path of the remote directory
* @return {!Promise<!StatusPacket>} A Promise that resolves with the remote
* handle, or rejects (usually with an nassh.sftp.StatusError)
*/
nassh.sftp.Client.prototype.makeDirectory = function(path) {
var packet = new nassh.sftp.Packet();
packet.setString(this.basePath_ + path);
packet.setUint32(0); // flags, 0b0000, no modified attributes
return this.sendRequest_(nassh.sftp.packets.RequestPackets.MKDIR, packet)
.then(response => this.isSuccessResponse_(response, 'MKDIR'));
};
/**
* Canonicalize a path.
*
* @param {string} path The path to canonicalize.
* @return {!Promise<!StatusPacket>} A Promise that resolves with the remote
* path, or rejects (usually with an nassh.sftp.StatusError)
*/
nassh.sftp.Client.prototype.realPath = function(path) {
var packet = new nassh.sftp.Packet();
packet.setString(this.basePath_ + path);
return this.sendRequest_(nassh.sftp.packets.RequestPackets.REALPATH, packet)
.then(response => this.isNameResponse_(response, 'REALPATH'));
};
/**
* Read a symlink.
*
* @param {string} path The symlink to read.
* @return {!Promise<!StatusPacket>} A Promise that resolves with the remote
* path, or rejects (usually with an nassh.sftp.StatusError)
*/
nassh.sftp.Client.prototype.readLink = function(path) {
var packet = new nassh.sftp.Packet();
packet.setString(this.basePath_ + path);
return this.sendRequest_(nassh.sftp.packets.RequestPackets.READLINK, packet)
.then(response => this.isNameResponse_(response, 'READLINK'));
};
/**
* Create a symlink.
*
* @param {string} target The target of the symlink.
* @param {string} path The symlink to create.
* @return {!Promise<!StatusPacket>} A Promise that resolves with the remote
* path, or rejects (usually with an nassh.sftp.StatusError)
*/
nassh.sftp.Client.prototype.symLink = function(target, path) {
var packet = new nassh.sftp.Packet();
packet.setString(target);
packet.setString(this.basePath_ + path);
return this.sendRequest_(nassh.sftp.packets.RequestPackets.SYMLINK, packet)
.then(response => this.isSuccessResponse_(response, 'SYMLINK'));
};