diff --git a/packages/teleport/src/lib/tdp/client.ts b/packages/teleport/src/lib/tdp/client.ts index b67a791a2..88de278d5 100644 --- a/packages/teleport/src/lib/tdp/client.ts +++ b/packages/teleport/src/lib/tdp/client.ts @@ -32,6 +32,7 @@ import Codec, { SharedDirectoryMoveResponse, SharedDirectoryReadResponse, SharedDirectoryWriteResponse, + SharedDirectoryCreateResponse, FileSystemObject, } from './codec'; import { @@ -81,8 +82,8 @@ export default class Client extends EventEmitterWebAuthnSender { this.emit(TdpClientEvent.WS_OPEN); }; - this.socket.onmessage = (ev: MessageEvent) => { - this.processMessage(ev.data as ArrayBuffer); + this.socket.onmessage = async (ev: MessageEvent) => { + await this.processMessage(ev.data as ArrayBuffer); }; // The socket 'error' event will only ever be emitted by the socket @@ -102,7 +103,9 @@ export default class Client extends EventEmitterWebAuthnSender { }; } - processMessage(buffer: ArrayBuffer) { + // processMessage should be await-ed when called, + // so that its internal await-or-not logic is obeyed. + async processMessage(buffer: ArrayBuffer): Promise { try { const messageType = this.codec.decodeMessageType(buffer); switch (messageType) { @@ -136,6 +139,14 @@ export default class Client extends EventEmitterWebAuthnSender { case MessageType.SHARED_DIRECTORY_INFO_REQUEST: this.handleSharedDirectoryInfoRequest(buffer); break; + case MessageType.SHARED_DIRECTORY_CREATE_REQUEST: + // A typical sequence is that we receive a SharedDirectoryCreateRequest + // immediately followed by a SharedDirectoryWriteRequest. It's important + // that we await here so that this client doesn't field the SharedDirectoryWriteRequest + // until the create has successfully completed, or else we might get an error + // trying to write to a file that hasn't been created yet. + await this.handleSharedDirectoryCreateRequest(buffer); + break; case MessageType.SHARED_DIRECTORY_READ_REQUEST: this.handleSharedDirectoryReadRequest(buffer); break; @@ -272,6 +283,32 @@ export default class Client extends EventEmitterWebAuthnSender { } } + async handleSharedDirectoryCreateRequest(buffer: ArrayBuffer) { + const req = this.codec.decodeSharedDirectoryCreateRequest(buffer); + + try { + await this.sdManager.create(req.path, req.fileType); + const info = await this.sdManager.getInfo(req.path); + this.sendSharedDirectoryCreateResponse({ + completionId: req.completionId, + errCode: SharedDirectoryErrCode.Nil, + fso: this.toFso(info), + }); + } catch (e) { + this.sendSharedDirectoryCreateResponse({ + completionId: req.completionId, + errCode: SharedDirectoryErrCode.Failed, + fso: { + lastModified: BigInt(0), + fileType: FileType.File, + size: BigInt(0), + path: req.path, + }, + }); + this.handleError(e, TdpClientEvent.CLIENT_ERROR, false); + } + } + async handleSharedDirectoryReadRequest(buffer: ArrayBuffer) { const req = this.codec.decodeSharedDirectoryReadRequest(buffer); try { @@ -454,6 +491,10 @@ export default class Client extends EventEmitterWebAuthnSender { this.send(this.codec.encodeSharedDirectoryWriteResponse(response)); } + sendSharedDirectoryCreateResponse(response: SharedDirectoryCreateResponse) { + this.send(this.codec.encodeSharedDirectoryCreateResponse(response)); + } + resize(spec: ClientScreenSpec) { this.send(this.codec.encodeClientScreenSpec(spec)); } diff --git a/packages/teleport/src/lib/tdp/codec.ts b/packages/teleport/src/lib/tdp/codec.ts index 842884ba0..b01ca1671 100644 --- a/packages/teleport/src/lib/tdp/codec.ts +++ b/packages/teleport/src/lib/tdp/codec.ts @@ -41,6 +41,8 @@ export enum MessageType { SHARED_DIRECTORY_ACKNOWLEDGE = 12, SHARED_DIRECTORY_INFO_REQUEST = 13, SHARED_DIRECTORY_INFO_RESPONSE = 14, + SHARED_DIRECTORY_CREATE_REQUEST = 15, + SHARED_DIRECTORY_CREATE_RESPONSE = 16, SHARED_DIRECTORY_READ_REQUEST = 19, SHARED_DIRECTORY_READ_RESPONSE = 20, SHARED_DIRECTORY_WRITE_REQUEST = 21, @@ -124,6 +126,21 @@ export type SharedDirectoryInfoResponse = { fso: FileSystemObject; }; +// | message type (15) | completion_id uint32 | directory_id uint32 | file_type uint32 | path_length uint32 | path []byte | +export type SharedDirectoryCreateRequest = { + completionId: number; + directoryId: number; + fileType: FileType; + path: string; +}; + +// | message type (16) | completion_id uint32 | err_code uint32 | file_system_object fso | +export type SharedDirectoryCreateResponse = { + completionId: number; + errCode: SharedDirectoryErrCode; + fso: FileSystemObject; +}; + // | message type (19) | completion_id uint32 | directory_id uint32 | path_length uint32 | path []byte | offset uint64 | length uint32 | export type SharedDirectoryReadRequest = { completionId: number; @@ -573,6 +590,31 @@ export default class Codec { ]).buffer; } + // | message type (16) | completion_id uint32 | err_code uint32 | file_system_object fso | + encodeSharedDirectoryCreateResponse( + res: SharedDirectoryCreateResponse + ): Message { + const bufLenSansFso = byteLength + 2 * uint32Length; + const bufferSansFso = new ArrayBuffer(bufLenSansFso); + const view = new DataView(bufferSansFso); + let offset = 0; + + view.setUint8(offset, MessageType.SHARED_DIRECTORY_CREATE_RESPONSE); + offset += byteLength; + view.setUint32(offset, res.completionId); + offset += uint32Length; + view.setUint32(offset, res.errCode); + offset += uint32Length; + + const fsoBuffer = this.encodeFileSystemObject(res.fso); + + // https://gist.github.com/72lions/4528834?permalink_comment_id=2395442#gistcomment-2395442 + return new Uint8Array([ + ...new Uint8Array(bufferSansFso), + ...new Uint8Array(fsoBuffer), + ]).buffer; + } + // | message type (20) | completion_id uint32 | err_code uint32 | read_data_length uint32 | read_data []byte | encodeSharedDirectoryReadResponse(res: SharedDirectoryReadResponse): Message { const bufLen = @@ -800,6 +842,30 @@ export default class Codec { }; } + // | message type (15) | completion_id uint32 | directory_id uint32 | file_type uint32 | path_length uint32 | path []byte | + decodeSharedDirectoryCreateRequest( + buffer: ArrayBuffer + ): SharedDirectoryCreateRequest { + const dv = new DataView(buffer); + let offset = 0; + offset += byteLength; // eat message type + const completionId = dv.getUint32(offset); + offset += uint32Length; // eat completion_id + const directoryId = dv.getUint32(offset); + offset += uint32Length; // eat directory_id + const fileType = dv.getUint32(offset); + offset += uint32Length; // eat directory_id + offset += uint32Length; // eat path_length + const path = this.decoder.decode(new Uint8Array(buffer.slice(offset))); + + return { + completionId, + directoryId, + fileType, + path, + }; + } + // | message type (19) | completion_id uint32 | directory_id uint32 | path_length uint32 | path []byte | offset uint64 | length uint32 | decodeSharedDirectoryReadRequest( buffer: ArrayBuffer diff --git a/packages/teleport/src/lib/tdp/playerClient.ts b/packages/teleport/src/lib/tdp/playerClient.ts index 62e5b2820..958e65706 100644 --- a/packages/teleport/src/lib/tdp/playerClient.ts +++ b/packages/teleport/src/lib/tdp/playerClient.ts @@ -50,7 +50,7 @@ export class PlayerClient extends Client { } // Overrides Client implementation. - processMessage(buffer: ArrayBuffer) { + async processMessage(buffer: ArrayBuffer): Promise { const json = JSON.parse(this.textDecoder.decode(buffer)); if (json.message === 'end') { @@ -60,7 +60,7 @@ export class PlayerClient extends Client { } else { const ms = json.ms; this.emit(PlayerClientEvent.UPDATE_CURRENT_TIME, ms); - super.processMessage(base64ToArrayBuffer(json.message)); + await super.processMessage(base64ToArrayBuffer(json.message)); } } diff --git a/packages/teleport/src/lib/tdp/sharedDirectoryManager.ts b/packages/teleport/src/lib/tdp/sharedDirectoryManager.ts index 45620b1bb..2931c89bb 100644 --- a/packages/teleport/src/lib/tdp/sharedDirectoryManager.ts +++ b/packages/teleport/src/lib/tdp/sharedDirectoryManager.ts @@ -11,6 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. +import { FileType } from './codec'; // SharedDirectoryManager manages a FileSystemDirectoryHandle for use // by the TDP client. Most of its methods can potentially throw errors @@ -143,6 +144,32 @@ export class SharedDirectoryManager { return writeData.length; } + /** + * Creates a new file or directory (determined by fileType) at path. + * If the path already exists for the given fileType, this operation is effectively ignored. + * @throws {DomException} If the path already exists but not for the given fileType. + * @throws Anything potentially thrown by getFileHandle/getDirectoryHandle. + * @throws {PathDoesNotExistError} if the path isn't a valid path to a directory. + */ + async create(path: string, fileType: FileType): Promise { + let splitPath = path.split('/'); + const fileOrDirName = splitPath.pop(); + const dirPath = splitPath.join('/'); + + const dirHandle = await this.walkPath(dirPath); + if (dirHandle.kind !== 'directory') { + throw new PathDoesNotExistError( + 'destination was a file, not a directory' + ); + } + + if (fileType === FileType.File) { + await dirHandle.getFileHandle(fileOrDirName, { create: true }); + } else { + await dirHandle.getDirectoryHandle(fileOrDirName, { create: true }); + } + } + /** * walkPath walks a pathstr (assumed to be in the qualified Unix format specified * in the TDP spec), returning the FileSystemDirectoryHandle | FileSystemFileHandle