blob: ba582f462faaf537c10d6b8f5be0bc71bdda6466 [file] [log] [blame]
// Copyright 2017 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {lib} from '../../libdot/index.js';
import {Packet} from './nassh_sftp_packet.js';
/**
* @typedef {{
* flags: number,
* size: (number|undefined),
* uid: (number|undefined),
* gid: (number|undefined),
* permissions: (number|undefined),
* isCharacterDevice: (boolean|undefined),
* isDirectory: (boolean|undefined),
* isBlockDevice: (boolean|undefined),
* isRegularFile: (boolean|undefined),
* isFifo: (boolean|undefined),
* isLink: (boolean|undefined),
* isSocket: (boolean|undefined),
* lastAccessed: (number|undefined),
* lastModified: (number|undefined),
* extendedCount: (number|undefined),
* extensions: (!Array<{type: string, data: string}>|undefined),
* }}
*/
export const FileAttrs = {};
/**
* @typedef {{
* filename: string,
* longFilename: string,
* }}
*/
export const File = {};
/** @typedef {string} */
export const FileHandle = {};
/**
* Possible status code values.
*
* @enum {number}
*/
export const StatusCodes = {
OK: 0,
EOF: 1,
NO_SUCH_FILE: 2,
PERMISSION_DENIED: 3,
FAILURE: 4,
BAD_MESSAGE: 5,
NO_CONNECTION: 6,
CONNECTION_LOST: 7,
OP_UNSUPPORTED: 8,
};
/**
* SFTP Status Packet containing the request id, status codes, status message
* and language.
*
* @param {!Packet} packet
* @constructor
* @extends {Packet}
*/
export function StatusPacket(packet) {
this.requestId = packet.getUint32();
this.code = packet.getUint32();
this.message = packet.getString();
this.lang = packet.getString();
}
/**
* SFTP Data Packet containing the request id and associated data.
*
* @param {!Packet} packet
* @constructor
* @extends {Packet}
*/
export function DataPacket(packet) {
this.requestId = packet.getUint32();
this.length = packet.getUint32();
this.data = packet.getData(this.length);
}
/**
* SFTP Handle Packet containing the request id and a file handle.
*
* @param {!Packet} packet
* @constructor
* @extends {Packet}
*/
export function HandlePacket(packet) {
this.requestId = packet.getUint32();
this.handle = packet.getString();
}
/**
* SFTP Name Packet containing the request id, file count and an array of file
* attributes.
*
* @param {!Packet} packet
* @constructor
* @extends {Packet}
*/
export function NamePacket(packet) {
this.requestId = packet.getUint32();
this.fileCount = packet.getUint32();
/* @type {!Array<!File|!FileAttrs>} */
this.files = [];
for (let i = 0; i < this.fileCount; i++) {
const fileName = packet.getUtf8String();
const longFileName = packet.getUtf8String();
const fileData = getFileAttrs(packet);
fileData.filename = fileName;
fileData.longFilename = longFileName;
this.files.push(fileData);
}
}
/**
* SFTP Attrs Packet containing the request id and a file's attributes.
*
* @param {!Packet} packet
* @constructor
* @extends {Packet}
*/
export function AttrsPacket(packet) {
this.requestId = packet.getUint32();
this.attrs = getFileAttrs(packet);
}
/**
* SFTP Extended Reply Packet.
*
* Since extended replies need specific parsers, we just save a reference to the
* original packet and delay parsing to whatever call made the extended request.
*
* @param {!Packet} packet The source packet to read from.
* @constructor
* @extends {Packet}
*/
export function ExtendedReplyPacket(packet) {
this.requestId = packet.getUint32();
this.rawPacket = packet;
}
/**
* SFTP response to statvfs@openssh.com packets.
*
* @param {!ExtendedReplyPacket} packet The extended reply.
* @constructor
* @extends {Packet}
*/
export function DiskFreePacket(packet) {
const p = packet.rawPacket;
this.requestId = packet.requestId;
this.bsize = p.getUint64();
this.frsize = p.getUint64();
this.blocks = p.getUint64();
this.bfree = p.getUint64();
this.bavail = p.getUint64();
this.files = p.getUint64();
this.ffree = p.getUint64();
this.favail = p.getUint64();
this.fsid_hi = p.getUint32();
this.fsid_lo = p.getUint32();
this.flag = p.getUint64();
this.namemax = p.getUint64();
this.st_rdonly = !!(this.flag & 0x1);
this.st_nosuid = !!(this.flag & 0x2);
}
/**
* SFTP response to limits@openssh.com packets.
*
* @param {!ExtendedReplyPacket} packet The extended reply.
* @constructor
* @extends {Packet}
*/
export function LimitsPacket(packet) {
const p = packet.rawPacket;
this.requestId = packet.requestId;
this.maxPacketLength = p.getUint64();
this.maxReadLength = p.getUint64();
this.maxWriteLength = p.getUint64();
this.maxOpenHandles = p.getUint64();
}
/**
* Make sure the name conforms to the specification.
*
* The SFTP RFC defines the extension-name format:
* https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-8
* https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-4.2
*
* Which says to follow the SSH "Algorithm and Method Naming" RFC format:
* https://tools.ietf.org/html/rfc4251#section-6
*
* Really we just need this func to filter out extensions that we don't care
* about in our implementation. If another one shows up, we can revisit.
*
* @param {string} ext The protocol name to check.
* @return {boolean} True if the name is valid.
*/
export function ValidExtension(ext) {
// The RFC is a little ambiguous, but it uses "name" to refer to the entire
// extension name, not just sub-components (when using the @ form).
if (ext.length > 64) {
return false;
}
// Split apart the extension@domain format.
const ary = ext.split('@');
if (ary.length > 2) {
return false;
}
const [name, domain] = ary;
// Names cannot contain control chars or whitespace (0x00-0x20), "@" (0x40),
// "," (0x2c), or DEL (0x7f). So remove all valid chars and make sure the
// result is an empty string.
if (name.length == 0 ||
name.replace(/[\x21-\x2b\x2d-\x3f\x41-\x7e]/g, '').length != 0) {
return false;
}
// The domain part is supposed to be a bit more strict ("a valid domain"),
// but using the same form as above should be good enough.
if (domain !== undefined) {
if (domain.replace(/[\x21-\x2b\x2d-\x3f\x41-\x7e]/g, '').length != 0) {
return false;
}
}
return true;
}
/**
* SFTP Version Packet containing the version and possible extensions.
*
* @param {!Packet} packet
* @constructor
* @extends {Packet}
*/
export function VersionPacket(packet) {
this.requestId = 'init';
this.version = packet.getUint32();
// Pull out all the extensions that might exist.
this.extensions = {};
while (!packet.eod()) {
const name = packet.getString();
const data = packet.getString();
// The SFTP RFC says we should silently ignore unknown/invalid entries.
if (ValidExtension(name)) {
this.extensions[name] = data;
}
}
}
/**
* Unknown Packet containing the request id (potentially garbage) and associated
* data (also potentially garbage).
*
* @param {!Packet} packet
* @constructor
* @extends {Packet}
*/
export function UnknownPacket(packet) {
this.requestId = packet.getUint32();
this.data = packet.getData();
}
/**
* Possible SFTP File Transfer flags attributes (SSH_FILEXFER_ATTR_XXX).
*
* @enum {number}
*/
export const FileXferAttrs = {
SIZE: 0x00000001,
UIDGID: 0x00000002,
PERMISSIONS: 0x00000004,
ACMODTIME: 0x00000008,
EXTENDED: 0x10000000,
};
/**
* Possible SFTP permission bits.
*
* Note: The RFC says:
* The `permissions' field contains a bit mask of file permissions as
* defined by posix.
* Except that POSIX only defines the bit values for permissions (ones that
* start with S_Ixxx). It does not define the bit values for file types
* (ones that start with S_IFxxx). We use "common" Linux ones for that.
*/
export const PermissionBits = {
SMODE: 0o007777, // Mask for file mode bits.
ISVTX: 0o001000, // Sticky directory.
ISGID: 0o002000, // Setgid.
ISUID: 0o004000, // Setuid.
IFMT: 0o170000, // Mask for IFxxx fields below.
IFCHR: 0o020000, // Character special.
IFDIR: 0o040000, // Directory.
IFBLK: 0o060000, // Block special.
IFREG: 0o100000, // Regular file.
IFIFO: 0o010000, // FIFO special.
IFLNK: 0o120000, // Symbolic link.
IFSOCK: 0o140000, // Socket.
};
/**
* Convert permission bits into a standard UNIX summary.
*
* Typically used in conjunction with AttrsPacket and the permissions field.
*
* The output will look similar to `ls -l`. e.g. "drwxr-xr-x".
*
* @param {number=} bits The permission bits to convert.
* @return {string} The short `ls -l`-like summary.
*/
export function bitsToUnixModeLine(bits = 0) {
let ret = '';
// First handle the file type.
const ifmt = bits & PermissionBits.IFMT;
const fmtMap = {
[PermissionBits.IFCHR]: 'c',
[PermissionBits.IFDIR]: 'd',
[PermissionBits.IFBLK]: 'b',
[PermissionBits.IFREG]: '-',
[PermissionBits.IFIFO]: 'p',
[PermissionBits.IFLNK]: 'l',
[PermissionBits.IFSOCK]: 's',
};
if (fmtMap[ifmt] === undefined) {
ret += '?';
} else {
ret += fmtMap[ifmt];
}
// Then handle user/group/other permissions.
function threebits(bits, sid, x, X) {
if (!sid) {
x = 'x';
X = '-';
}
return ((bits & 0o4) ? 'r' : '-') +
((bits & 0o2) ? 'w' : '-') +
((bits & 0o1) ? x : X);
}
ret += threebits(bits >> 6, (bits & PermissionBits.ISUID), 's', 'S');
ret += threebits(bits >> 3, (bits & PermissionBits.ISGID), 's', 'S');
ret += threebits(bits >> 0, (bits & PermissionBits.ISVTX), 't', 'T');
return ret;
}
/**
* Given a packet (at the correct offset), will read one file's attributes.
*
* @param {!Packet} packet
* @return {!FileAttrs}
*/
export function getFileAttrs(packet) {
const attrs = {};
attrs.flags = packet.getUint32();
if (attrs.flags & FileXferAttrs.SIZE) {
attrs.size = packet.getUint64();
}
if (attrs.flags & FileXferAttrs.UIDGID) {
attrs.uid = packet.getUint32();
attrs.gid = packet.getUint32();
}
if (attrs.flags & FileXferAttrs.PERMISSIONS) {
attrs.permissions = packet.getUint32();
const fmt = attrs.permissions & PermissionBits.IFMT;
attrs.isCharacterDevice = (fmt == PermissionBits.IFCHR);
attrs.isDirectory = (fmt == PermissionBits.IFDIR);
attrs.isBlockDevice = (fmt == PermissionBits.IFBLK);
attrs.isRegularFile = (fmt == PermissionBits.IFREG);
attrs.isFifo = (fmt == PermissionBits.IFIFO);
attrs.isLink = (fmt == PermissionBits.IFLNK);
attrs.isSocket = (fmt == PermissionBits.IFSOCK);
}
if (attrs.flags & FileXferAttrs.ACMODTIME) {
attrs.lastAccessed = packet.getUint32();
attrs.lastModified = packet.getUint32();
}
if (attrs.flags & FileXferAttrs.EXTENDED) {
const extendedCount = packet.getUint32();
attrs.extendedCount = extendedCount;
const extendedData = [];
for (let i = 0; i < extendedCount; i++) {
extendedData.push({
'type': packet.getString(),
'data': packet.getString(),
});
}
attrs.extensions = extendedData;
}
return attrs;
}
/**
* Serialize an attribute object back into a packet.
*
* @param {!Packet} packet
* @param {!FileAttrs} attrs
*/
export function setFileAttrs(packet, attrs) {
// We only add fields we know how to handle.
packet.setUint32(attrs.flags & (
FileXferAttrs.SIZE |
FileXferAttrs.UIDGID |
FileXferAttrs.PERMISSIONS |
FileXferAttrs.ACMODTIME
));
if (attrs.flags & FileXferAttrs.SIZE) {
packet.setUint64(lib.notUndefined(attrs.size));
}
if (attrs.flags & FileXferAttrs.UIDGID) {
packet.setUint32(lib.notUndefined(attrs.uid));
packet.setUint32(lib.notUndefined(attrs.gid));
}
if (attrs.flags & FileXferAttrs.PERMISSIONS) {
packet.setUint32(lib.notUndefined(attrs.permissions));
}
if (attrs.flags & FileXferAttrs.ACMODTIME) {
packet.setUint32(lib.notUndefined(attrs.lastAccessed));
packet.setUint32(lib.notUndefined(attrs.lastModified));
}
}
/**
* Convert UTC epoch timestamps that we get from the server to local time.
*
* Typically used in conjunction with AttrsPacket and the lastAccessed &
* lastModified fields. This is the same thing as "UNIX time".
*
* @param {number=} epoch The epoch time to convert.
* @return {!Date} A standard Date object.
*/
export function epochToLocal(epoch = 0) {
const date = new Date(0);
date.setUTCSeconds(epoch);
return date;
}
/**
* Possible SFTP Request Packet types
*
* @enum {number}
*/
export const RequestPackets = {
INIT: 1,
VERSION: 2,
OPEN: 3,
CLOSE: 4,
READ: 5,
WRITE: 6,
LSTAT: 7,
FSTAT: 8,
SETSTAT: 9,
FSETSTAT: 10,
OPENDIR: 11,
READDIR: 12,
REMOVE: 13,
MKDIR: 14,
RMDIR: 15,
REALPATH: 16,
STAT: 17,
RENAME: 18,
READLINK: 19,
SYMLINK: 20,
EXTENDED: 200,
};
/**
* Possible SFTP Response Packet types
*
* @type {!Object<number, function(new:Packet, !Packet)>}
* @const
*/
export const ResponsePackets = {
2: VersionPacket,
101: StatusPacket,
102: HandlePacket,
103: DataPacket,
104: NamePacket,
105: AttrsPacket,
201: ExtendedReplyPacket,
};
/**
* Possible bit flags with open packets.
*
* @enum {number}
*/
export const OpenFlags = {
READ: 0x00000001,
WRITE: 0x00000002,
APPEND: 0x00000004,
CREAT: 0x00000008,
TRUNC: 0x00000010,
EXCL: 0x00000020,
};