Skip to content
This repository was archived by the owner on Feb 8, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
2cbc1b0
super basic picker with logging
Jul 5, 2022
bc51777
Adds SharedDirectoryAnnounce
Jul 6, 2022
bc6586b
Removes recording icon
Jul 6, 2022
fa09ec3
reverting config to its rightful checkin state
Jul 6, 2022
becfad3
updates storybook and storybook test snapshot
Jul 7, 2022
17408f8
Adds types for File System Access API
Jul 7, 2022
edfe30d
prettier
Jul 7, 2022
b9f0690
Adds SharedDirectoryAcknowledge, SharedDirectoryErrCode, updates some…
Jul 7, 2022
b3bf95c
Adds debug to logger
Jul 7, 2022
d69f16d
Adds SharedDirectoryInfoRequest
Jul 7, 2022
3474fed
Updates test with some ts-fu to get the maximum value of a numerical …
Jul 7, 2022
a0c6e9d
Updates babel targets to the latest 2 versions of primary browsers
Jul 13, 2022
1cd8131
Updates Adding Packages section with better instructions
Jul 14, 2022
589bc1e
Merge branch 'master' into isaiah/update-babel-build-targets
Jul 14, 2022
f2970db
super basic picker with logging
Jul 5, 2022
8150acc
Adds SharedDirectoryAnnounce
Jul 6, 2022
d3f9cac
Removes recording icon
Jul 6, 2022
9b5f4c6
updates storybook and storybook test snapshot
Jul 7, 2022
15d8343
Adds types for File System Access API
Jul 7, 2022
ca59266
prettier
Jul 7, 2022
20c14ff
Adds SharedDirectoryAcknowledge, SharedDirectoryErrCode, updates some…
Jul 7, 2022
43f921f
Adds debug to logger
Jul 7, 2022
e779785
Adds SharedDirectoryInfoRequest
Jul 7, 2022
265564f
Updates test with some ts-fu to get the maximum value of a numerical …
Jul 7, 2022
14a6166
swaps out wicg-native-file-system for apparently more up to date wicg…
Jul 12, 2022
317d0d8
adds SharedDirectoryInfoResponse and FileSystemObject to the codec
Jul 12, 2022
6c258ad
Retains existing functionality, substituting in a new sharedDirectory…
Jul 12, 2022
ece73c7
Adds untested walkPath
Jul 18, 2022
a187232
Merge branch 'master' into isaiah/sd-acknowledge
Jul 18, 2022
fe6ea5b
uses consts in decodeSharedDirectoryAcknowledge
Jul 18, 2022
7170e15
Merge branch 'isaiah/sd-acknowledge' into isaiah/sd-info-request
Jul 18, 2022
e54f251
uses consts in decodeSharedDirectoryInfoRequest
Jul 18, 2022
1c2ec0e
uses consts in decodeSharedDirectoryInfoRequest
Jul 18, 2022
d86c2ee
uses consts in decodePngFrame
Jul 18, 2022
85f87e5
uses consts in decodeStringMessage
Jul 18, 2022
f09ec07
uses consts in decodeMfaJson
Jul 18, 2022
a53d2ca
Merge branch 'master' into isaiah/sd-info-request
Jul 18, 2022
868f9d0
Adds __LAST to MessageType enum
Jul 18, 2022
6285d0e
Merge branch 'isaiah/sd-info-request' into isaiah/sd-info-response
Jul 19, 2022
634ad6a
reverting webapps.e change
Jul 19, 2022
e51e4e5
fixes broken merge
Jul 19, 2022
aea3043
creates getInfo for more efficient traversal
Jul 19, 2022
9b1e905
manually tested, cleaning up
Jul 19, 2022
ca89319
Merge branch 'master' into isaiah/sd-info-response
Jul 19, 2022
9a732a5
removing duplicate function definition
Jul 19, 2022
97cc3c0
Update packages/teleport/src/lib/tdp/sharedDirectoryManager.ts
Jul 27, 2022
215f038
Merge branch 'master' into isaiah/sd-info-response
Jul 27, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/teleport/src/DesktopSession/DesktopSession.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ function Session(props: PropsWithChildren<State>) {
.showDirectoryPicker()
.then(sharedDirHandle => {
setIsSharingDirectory(true);
tdpClient.sharedDirectory = sharedDirHandle;
tdpClient.addSharedDirectory(sharedDirHandle);
tdpClient.sendSharedDirectoryAnnounce();
})
.catch(() => {
Expand Down
77 changes: 55 additions & 22 deletions packages/teleport/src/lib/tdp/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,14 @@ import Codec, {
ClientScreenSpec,
PngFrame,
ClipboardData,
FileType,
SharedDirectoryErrCode,
SharedDirectoryInfoResponse,
} from './codec';
import {
PathDoesNotExistError,
SharedDirectoryManager,
} from './sharedDirectoryManager';

export enum TdpClientEvent {
TDP_CLIENT_SCREEN_SPEC = 'tdp client screen spec',
Expand All @@ -45,14 +51,15 @@ export default class Client extends EventEmitterWebAuthnSender {
protected codec: Codec;
protected socket: WebSocket | undefined;
private socketAddr: string;
sharedDirectory: FileSystemDirectoryHandle | undefined;
private sdManager: SharedDirectoryManager;

private logger = Logger.create('TDPClient');

constructor(socketAddr: string) {
super();
this.socketAddr = socketAddr;
this.codec = new Codec();
this.sdManager = new SharedDirectoryManager();
}

// Connect to the websocket and register websocket event handlers.
Expand Down Expand Up @@ -206,23 +213,51 @@ export default class Client extends EventEmitterWebAuthnSender {
return;
}

this.logger.info('Started sharing directory: ' + this.sharedDirectory.name);
this.logger.info('Started sharing directory: ' + this.sdManager.getName());
}

handleSharedDirectoryInfoRequest(buffer: ArrayBuffer) {
async handleSharedDirectoryInfoRequest(buffer: ArrayBuffer) {
const req = this.codec.decodeSharedDirectoryInfoRequest(buffer);
// TODO(isaiah): remove debug once message is handled.
this.logger.debug(
'Received SharedDirectoryInfoRequest: ' + JSON.stringify(req)
);
// TODO(isaiah): here's where we'll respond with SharedDirectoryInfoResponse
const path = req.path;
try {
const info = await this.sdManager.getInfo(path);
this.sendSharedDirectoryInfoResponse({
completionId: req.completionId,
errCode: SharedDirectoryErrCode.Nil,
fso: {
lastModified: BigInt(info.lastModified),
fileType: info.kind === 'file' ? FileType.File : FileType.Directory,
size: BigInt(info.size),
path: path,
},
});
} catch (e) {
if (e.constructor === PathDoesNotExistError) {
this.sendSharedDirectoryInfoResponse({
completionId: req.completionId,
errCode: SharedDirectoryErrCode.DoesNotExist,
fso: {
lastModified: BigInt(0),
fileType: FileType.File,
size: BigInt(0),
path: path,
},
});
} else {
this.handleError(e);
}
}
}

protected send(
data: string | ArrayBufferLike | Blob | ArrayBufferView
): void {
if (this.socket && this.socket.readyState === 1) {
this.socket.send(data);
try {
this.socket.send(data);
} catch (e) {
this.handleError(e);
}
return;
}

Expand Down Expand Up @@ -263,32 +298,30 @@ export default class Client extends EventEmitterWebAuthnSender {
this.send(msg);
}

private sharedDirectoryReady() {
if (!this.sharedDirectory) {
this.handleError(
new Error(
'attempted to use a shared directory before one was initialized'
)
);
return false;
addSharedDirectory(sharedDirectory: FileSystemDirectoryHandle) {
try {
this.sdManager.add(sharedDirectory);
} catch (err) {
this.handleError(err);
}

return true;
}

sendSharedDirectoryAnnounce() {
if (!this.sharedDirectoryReady()) return;
this.socket.send(
this.send(
this.codec.encodeSharedDirectoryAnnounce({
completionId: 0, // This is always the first request.
// Hardcode directoryId for now since we only support sharing 1 directory.
// We're using 2 because the smartcard device is hardcoded to 1 in the backend.
directoryId: 2,
name: this.sharedDirectory.name,
name: this.sdManager.getName(),
})
);
}

sendSharedDirectoryInfoResponse(res: SharedDirectoryInfoResponse) {
this.send(this.codec.encodeSharedDirectoryInfoResponse(res));
}

resize(spec: ClientScreenSpec) {
this.send(this.codec.encodeClientScreenSpec(spec));
}
Expand Down
73 changes: 70 additions & 3 deletions packages/teleport/src/lib/tdp/codec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export enum MessageType {
SHARED_DIRECTORY_ANNOUNCE = 11,
SHARED_DIRECTORY_ACKNOWLEDGE = 12,
SHARED_DIRECTORY_INFO_REQUEST = 13,
SHARED_DIRECTORY_INFO_RESPONSE = 14,
__LAST, // utility value
}

Expand Down Expand Up @@ -95,7 +96,7 @@ export type SharedDirectoryAnnounce = {
name: string;
};

// | message type (12) | errCode error | directory_id uint32 |
// | message type (12) | err_code error | directory_id uint32 |
export type SharedDirectoryAcknowledge = {
errCode: SharedDirectoryErrCode;
directoryId: number;
Expand All @@ -108,6 +109,21 @@ export type SharedDirectoryInfoRequest = {
path: string;
};

// | message type (14) | completion_id uint32 | err_code uint32 | file_system_object fso |
export type SharedDirectoryInfoResponse = {
completionId: number;
errCode: SharedDirectoryErrCode;
fso: FileSystemObject;
};

// | last_modified uint64 | size uint64 | file_type uint32 | path_length uint32 | path byte[] |
export type FileSystemObject = {
lastModified: bigint;
size: bigint;
fileType: FileType;
path: string;
};

export enum SharedDirectoryErrCode {
// nil (no error, operation succeeded)
Nil = 0,
Expand All @@ -119,6 +135,11 @@ export enum SharedDirectoryErrCode {
AlreadyExists = 3,
}

export enum FileType {
File = 0,
Directory = 1,
}

function toSharedDirectoryErrCode(errCode: number): SharedDirectoryErrCode {
if (!(errCode in SharedDirectoryErrCode)) {
throw new Error(`attempted to convert invalid error code ${errCode}`);
Expand Down Expand Up @@ -457,6 +478,51 @@ export default class Codec {
return buffer;
}

// | message type (14) | completion_id uint32 | err_code uint32 | file_system_object fso |
encodeSharedDirectoryInfoResponse(res: SharedDirectoryInfoResponse): 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_INFO_RESPONSE);
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;
}

// | last_modified uint64 | size uint64 | file_type uint32 | path_length uint32 | path byte[] |
encodeFileSystemObject(fso: FileSystemObject): Message {
const dataUtf8array = this.encoder.encode(fso.path);

const bufLen = 2 * uint64Length + 2 * uint32Length + dataUtf8array.length;
const buffer = new ArrayBuffer(bufLen);
const view = new DataView(buffer);
let offset = 0;
view.setBigUint64(offset, fso.lastModified);
offset += uint64Length;
view.setBigUint64(offset, fso.size);
offset += uint64Length;
view.setUint32(offset, fso.fileType);
offset += uint32Length;
view.setUint32(offset, dataUtf8array.length);
offset += uint32Length;
dataUtf8array.forEach(byte => {
view.setUint8(offset++, byte);
});

return buffer;
}

// decodeClipboardData decodes clipboard data
decodeClipboardData(buffer: ArrayBuffer): ClipboardData {
return {
Expand Down Expand Up @@ -533,15 +599,15 @@ export default class Codec {
return pngFrame;
}

// | message type (12) | errCode error | directory_id uint32 |
// | message type (12) | err_code error | directory_id uint32 |
decodeSharedDirectoryAcknowledge(
buffer: ArrayBuffer
): SharedDirectoryAcknowledge {
const dv = new DataView(buffer);
let offset = 0;
offset += byteLength; // eat message type
const errCode = toSharedDirectoryErrCode(dv.getUint32(offset));
offset += uint32Length; // eat errCode
offset += uint32Length; // eat err_code
const directoryId = dv.getUint32(5);

return {
Expand Down Expand Up @@ -579,3 +645,4 @@ export default class Codec {

const byteLength = 1;
const uint32Length = 4;
const uint64Length = uint32Length * 2;
119 changes: 119 additions & 0 deletions packages/teleport/src/lib/tdp/sharedDirectoryManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright 2022 Gravitational, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// 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.

// SharedDirectoryManager manages a FileSystemDirectoryHandle for use
// by the TDP client. Most of its methods can potentially throw errors
// and so should be wrapped in try/catch blocks.
export class SharedDirectoryManager {
private dir: FileSystemDirectoryHandle | undefined;

add(sharedDirectory: FileSystemDirectoryHandle) {
if (this.dir) {
throw new Error(
'SharedDirectoryManager currently only supports sharing a single directory'
);
}
this.dir = sharedDirectory;
}

getName(): string {
this.checkReady();
Comment thread
ibeckermayer marked this conversation as resolved.
return this.dir.name;
}

// Gets the information for the file or directory
// at path where path is the relative path from the
// root directory.
async getInfo(path: string): Promise<{
size: number; // bytes
lastModified: number; // ms since unix epoch
kind: 'file' | 'directory';
}> {
this.checkReady();

const fileOrDir = await this.walkPath(path);

if (fileOrDir.kind === 'directory') {
// Magic numbers are the values for directories where the true
// value is unavailable, according to the TDP spec.
return { size: 4096, lastModified: 0, kind: fileOrDir.kind };
}

let file = await fileOrDir.getFile();
return {
size: file.size,
lastModified: file.lastModified,
kind: fileOrDir.kind,
};
}

// walkPath walks a pathstr (assumed to be in the qualified Unix format specified
// in the TDP spec), returning the FileSystemDirectoryHandle | FileSystemFileHandle
// it finds at its end. If the pathstr isn't a valid path in the shared directory,
// it throws an error.
private async walkPath(
pathstr: string
): Promise<FileSystemDirectoryHandle | FileSystemFileHandle> {
if (pathstr === '') {
return this.dir;
}

let path = pathstr.split('/');

let walkIt = async (
dir: FileSystemDirectoryHandle,
path: string[]
): Promise<FileSystemDirectoryHandle | FileSystemFileHandle> => {
// Pop the next path element off the stack
let nextPathElem = path.shift();

// Iterate through the items in the directory
for await (const entry of dir.values()) {
// If we find the entry we're looking for
if (entry.name === nextPathElem) {
if (path.length === 0) {
// We're at the end of the path, so this
// is the end element we've been walking towards.
return entry;
} else if (entry.kind === 'directory') {
// We're not at the end of the path and
// have encountered a directory, recurse
// further.
return walkIt(entry, path);
} else {
break;
}
}
}

throw new PathDoesNotExistError('path does not exist');
};

return walkIt(this.dir, path);
}

private checkReady() {
if (!this.dir) {
throw new Error(
'attempted to use a shared directory before one was initialized'
);
}
}
}

export class PathDoesNotExistError extends Error {
constructor(message: string) {
super(message);
}
}
Loading