`;
exports[`webauthn prompt 1`] = `
-.c11 {
+.c9 {
+ line-height: 1.5;
+ margin: 0;
+ display: inline-flex;
+ justify-content: center;
+ align-items: center;
box-sizing: border-box;
- margin: 72px;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-family: inherit;
+ font-weight: 600;
+ outline: none;
+ position: relative;
text-align: center;
+ text-decoration: none;
+ text-transform: uppercase;
+ transition: all 0.3s;
+ -webkit-font-smoothing: antialiased;
+ background: #512FC9;
+ color: rgba(255,255,255,0.87);
+ min-height: 32px;
+ font-size: 12px;
+ padding: 0px 24px;
+ margin-right: 16px;
+ width: 130px;
}
-.c6 {
- display: inline-block;
- transition: color 0.3s;
- padding-right: 16px;
- color: #FFFFFF;
+.c9:active {
+ opacity: 0.56;
}
-.c10 {
- display: inline-block;
- transition: color 0.3s;
- color: #FFFFFF;
+.c9:hover,
+.c9:focus {
+ background: #651FFF;
}
-.c9 {
+.c9:active {
+ background: #354AA4;
+}
+
+.c9:disabled {
+ background: rgba(255,255,255,0.12);
+ color: rgba(255,255,255,0.3);
+}
+
+.c10 {
+ line-height: 1.5;
+ margin: 0;
+ display: inline-flex;
+ justify-content: center;
align-items: center;
+ box-sizing: border-box;
border: none;
+ border-radius: 4px;
cursor: pointer;
- display: flex;
+ font-family: inherit;
+ font-weight: 600;
outline: none;
- border-radius: 50%;
- overflow: visible;
- justify-content: center;
+ position: relative;
text-align: center;
- flex: 0 0 auto;
- background: transparent;
- color: inherit;
+ text-decoration: none;
+ text-transform: uppercase;
transition: all 0.3s;
-webkit-font-smoothing: antialiased;
+ background: #222C59;
+ color: rgba(255,255,255,0.87);
+ min-height: 32px;
font-size: 12px;
- height: 24px;
- width: 24px;
- margin-left: 24px;
- color: rgba(255,255,255,0.56);
+ padding: 0px 24px;
}
-.c9 .c5 {
- color: inherit;
+.c10:active {
+ opacity: 0.56;
}
-.c9:disabled {
- color: rgba(255,255,255,0.3);
+.c10:hover,
+.c10:focus {
+ background: #2C3A73;
}
-.c9:disabled {
+.c10:disabled {
+ background: rgba(255,255,255,0.12);
color: rgba(255,255,255,0.3);
}
-.c9:hover,
-.c9:focus {
- background: rgba(255,255,255,0.1);
+.c5 {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ font-weight: 300;
+ font-size: 22px;
+ line-height: 32px;
+ text-transform: uppercase;
+ margin: 0px;
+ color: rgba(255,255,255,0.87);
+ text-align: center;
}
-.c2 {
+.c7 {
overflow: hidden;
text-overflow: ellipsis;
margin: 0px;
- padding-left: 16px;
- padding-right: 16px;
+ text-align: center;
+}
+
+.c1 {
+ z-index: -1;
+ position: fixed;
+ right: 0;
+ bottom: 0;
+ top: 0;
+ left: 0;
+ background-color: rgba(0,0,0,0.5);
+ opacity: 1;
+ touch-action: none;
}
.c0 {
- box-sizing: border-box;
- display: flex;
- flex-direction: column;
+ position: fixed;
+ z-index: 1200;
+ right: 0;
+ bottom: 0;
+ top: 0;
+ left: 0;
}
-.c1 {
- box-sizing: border-box;
- height: 40px;
- background-color: #000;
- flex: 0 0 auto;
+.c2 {
+ height: 100%;
+ outline: none;
+ color: black;
display: flex;
align-items: center;
- flex-direction: row;
+ justify-content: center;
+ opacity: 1;
+ will-change: opacity;
+ transition: opacity 225ms cubic-bezier(0.4,0,0.2,1) 0ms;
}
.c3 {
- box-sizing: border-box;
- padding-left: 16px;
- padding-right: 16px;
+ padding: 32px;
+ padding-top: 24px;
+ background: #1C254D;
+ color: rgba(255,255,255,0.87);
+ border-radius: 8px;
+ box-shadow: 0 8px 32px rgba(0,0,0,0.24);
display: flex;
+ flex-direction: column;
+ position: relative;
+ overflow-y: auto;
+ max-height: calc(100% - 96px);
+ width: 400px;
}
.c4 {
box-sizing: border-box;
+ margin-bottom: 16px;
+ min-height: 32px;
display: flex;
align-items: center;
}
-.c8 {
- font-weight: 600;
- font-size: 18px;
- align-self: 'center';
+.c6 {
+ box-sizing: border-box;
+ margin-bottom: 40px;
+ flex: 1;
+ display: flex;
+ flex-direction: column;
}
-.c7 {
- font-weight: 600;
- font-size: 22px;
- align-self: 'center';
+.c8 {
+ box-sizing: border-box;
+ text-align: center;
}
-
+
+
-
`;
diff --git a/packages/teleport/src/DesktopSession/useDesktopSession.tsx b/packages/teleport/src/DesktopSession/useDesktopSession.tsx
index c7eca99cc..3119decce 100644
--- a/packages/teleport/src/DesktopSession/useDesktopSession.tsx
+++ b/packages/teleport/src/DesktopSession/useDesktopSession.tsx
@@ -30,10 +30,10 @@ export default function useDesktopSession() {
const { attempt: fetchAttempt, run } = useAttempt('processing');
// tdpConnection tracks the state of the tdpClient's TDP connection
- // tdpConnection.status ===
// - 'processing' at first
// - 'success' once the first TdpClientEvent.IMAGE_FRAGMENT is seen
- // - 'failed' if a TdpClientEvent.TDP_ERROR is encountered
+ // - 'failed' if a fatal error is encountered
+ // - '' if a non-fatal error is encountered
const { attempt: tdpConnection, setAttempt: setTdpConnection } =
useAttempt('processing');
@@ -162,6 +162,7 @@ export default function useDesktopSession() {
disconnected,
setDisconnected,
webauthn,
+ setTdpConnection,
...clientCanvasProps,
};
}
diff --git a/packages/teleport/src/DesktopSession/useTdpClientCanvas.tsx b/packages/teleport/src/DesktopSession/useTdpClientCanvas.tsx
index 92fa4a431..d6f8194ec 100644
--- a/packages/teleport/src/DesktopSession/useTdpClientCanvas.tsx
+++ b/packages/teleport/src/DesktopSession/useTdpClientCanvas.tsx
@@ -79,14 +79,18 @@ export default function useTdpClientCanvas(props: Props) {
}
};
- // Default TdpClientEvent.TDP_ERROR handler
- const onTdpError = (err: Error) => {
+ // Default TdpClientEvent.TDP_ERROR and TdpClientEvent.CLIENT_ERROR handler
+ const onTdpError = (error: { err: Error; isFatal: boolean }) => {
+ const { err, isFatal } = error;
setIsSharingDirectory(false);
setClipboardState(prevState => ({
...prevState,
enabled: false,
}));
- setTdpConnection({ status: 'failed', statusText: err.message });
+ setTdpConnection({
+ status: isFatal ? 'failed' : '',
+ statusText: err.message,
+ });
};
const onWsClose = () => {
diff --git a/packages/teleport/src/Player/DesktopPlayer.tsx b/packages/teleport/src/Player/DesktopPlayer.tsx
index 34c8a8a5c..cc39320d2 100644
--- a/packages/teleport/src/Player/DesktopPlayer.tsx
+++ b/packages/teleport/src/Player/DesktopPlayer.tsx
@@ -171,7 +171,8 @@ const useDesktopPlayer = ({
});
};
- const tdpCliOnTdpError = (err: Error) => {
+ const tdpCliOnTdpError = (error: { err: Error; isFatal: boolean }) => {
+ const { err } = error;
setAttempt({
status: 'failed',
statusText: err.message,
diff --git a/packages/teleport/src/components/TdpClientCanvas/TdpClientCanvas.tsx b/packages/teleport/src/components/TdpClientCanvas/TdpClientCanvas.tsx
index 460cd9ad1..ffae41ec4 100644
--- a/packages/teleport/src/components/TdpClientCanvas/TdpClientCanvas.tsx
+++ b/packages/teleport/src/components/TdpClientCanvas/TdpClientCanvas.tsx
@@ -129,9 +129,11 @@ export default function TdpClientCanvas(props: Props) {
useEffect(() => {
if (tdpCli && tdpCliOnTdpError) {
tdpCli.on(TdpClientEvent.TDP_ERROR, tdpCliOnTdpError);
+ tdpCli.on(TdpClientEvent.CLIENT_ERROR, tdpCliOnTdpError);
return () => {
tdpCli.removeListener(TdpClientEvent.TDP_ERROR, tdpCliOnTdpError);
+ tdpCli.removeListener(TdpClientEvent.CLIENT_ERROR, tdpCliOnTdpError);
};
}
}, [tdpCli, tdpCliOnTdpError]);
@@ -292,7 +294,7 @@ export type Props = {
pngFrame: PngFrame
) => void;
tdpCliOnClipboardData?: (clipboardData: ClipboardData) => void;
- tdpCliOnTdpError?: (err: Error) => void;
+ tdpCliOnTdpError?: (error: { err: Error; isFatal: boolean }) => void;
tdpCliOnWsClose?: () => void;
tdpCliOnWsOpen?: () => void;
tdpCliOnClientScreenSpec?: (
diff --git a/packages/teleport/src/lib/tdp/client.ts b/packages/teleport/src/lib/tdp/client.ts
index f8cf89c1e..b67a791a2 100644
--- a/packages/teleport/src/lib/tdp/client.ts
+++ b/packages/teleport/src/lib/tdp/client.ts
@@ -29,6 +29,7 @@ import Codec, {
SharedDirectoryErrCode,
SharedDirectoryInfoResponse,
SharedDirectoryListResponse,
+ SharedDirectoryMoveResponse,
SharedDirectoryReadResponse,
SharedDirectoryWriteResponse,
FileSystemObject,
@@ -43,7 +44,10 @@ export enum TdpClientEvent {
TDP_CLIENT_SCREEN_SPEC = 'tdp client screen spec',
TDP_PNG_FRAME = 'tdp png frame',
TDP_CLIPBOARD_DATA = 'tdp clipboard data',
+ // TDP_ERROR corresponds with https://github.com/gravitational/teleport/blob/86e824fc7879538e4de400eb1518e4f88930c109/rfd/0037-desktop-access-protocol.md?plain=1#L200-L206
TDP_ERROR = 'tdp error',
+ // CLIENT_ERROR represents an error event in the client that isn't a TDP_ERROR
+ CLIENT_ERROR = 'client error',
WS_OPEN = 'ws open',
WS_CLOSE = 'ws close',
}
@@ -118,7 +122,10 @@ export default class Client extends EventEmitterWebAuthnSender {
this.handleClipboardData(buffer);
break;
case MessageType.ERROR:
- this.handleError(new Error(this.codec.decodeErrorMessage(buffer)));
+ this.handleError(
+ new Error(this.codec.decodeErrorMessage(buffer)),
+ TdpClientEvent.TDP_ERROR
+ );
break;
case MessageType.MFA_JSON:
this.handleMfaChallenge(buffer);
@@ -145,7 +152,7 @@ export default class Client extends EventEmitterWebAuthnSender {
this.logger.warn(`received unsupported message type ${messageType}`);
}
} catch (err) {
- this.handleError(err);
+ this.handleError(err, TdpClientEvent.CLIENT_ERROR);
}
}
@@ -188,9 +195,6 @@ export default class Client extends EventEmitterWebAuthnSender {
);
}
- // TODO(isaiah): neither of the TdpClientEvent.TDP_ERROR are accurate, they should
- // instead be associated with a new event TdpClientEvent.CLIENT_ERROR.
- // https://github.com/gravitational/webapps/issues/615
handleMfaChallenge(buffer: ArrayBuffer) {
try {
const mfaJson = this.codec.decodeMfaJson(buffer);
@@ -204,11 +208,12 @@ export default class Client extends EventEmitterWebAuthnSender {
however the U2F API for hardware keys is not supported for desktop sessions. \
Please notify your system administrator to update cluster settings \
to use WebAuthn as the second factor protocol.'
- )
+ ),
+ TdpClientEvent.CLIENT_ERROR
);
}
} catch (err) {
- this.handleError(err);
+ this.handleError(err, TdpClientEvent.CLIENT_ERROR);
}
}
@@ -218,7 +223,8 @@ export default class Client extends EventEmitterWebAuthnSender {
}
this.handleError(
- new Error(`Encountered shared directory error: ${errCode}`)
+ new Error(`Encountered shared directory error: ${errCode}`),
+ TdpClientEvent.CLIENT_ERROR
);
return false;
}
@@ -234,7 +240,7 @@ export default class Client extends EventEmitterWebAuthnSender {
'Started sharing directory: ' + this.sdManager.getName()
);
} catch (e) {
- this.handleError(e);
+ this.handleError(e, TdpClientEvent.CLIENT_ERROR);
}
}
@@ -261,7 +267,7 @@ export default class Client extends EventEmitterWebAuthnSender {
},
});
} else {
- this.handleError(e);
+ this.handleError(e, TdpClientEvent.CLIENT_ERROR);
}
}
}
@@ -281,7 +287,7 @@ export default class Client extends EventEmitterWebAuthnSender {
readData,
});
} catch (e) {
- this.handleError(e);
+ this.handleError(e, TdpClientEvent.CLIENT_ERROR);
}
}
@@ -300,16 +306,25 @@ export default class Client extends EventEmitterWebAuthnSender {
bytesWritten,
});
} catch (e) {
- this.handleError(e);
+ this.handleError(e, TdpClientEvent.CLIENT_ERROR);
}
}
handleSharedDirectoryMoveRequest(buffer: ArrayBuffer) {
const req = this.codec.decodeSharedDirectoryMoveRequest(buffer);
- // TODO(isaiah): delete debug logs
- this.logger.debug('Received SharedDirectoryMoveRequest:');
- this.logger.debug(req);
- // TODO(isaiah): here's where we'll respond with a SharedDirectoryMoveResponse
+ // Always send back Failed for now, see https://github.com/gravitational/webapps/issues/1064
+ this.sendSharedDirectoryMoveResponse({
+ completionId: req.completionId,
+ errCode: SharedDirectoryErrCode.Failed,
+ });
+ this.handleError(
+ new Error(
+ 'Moving files and directories within a shared \
+ directory is not supported.'
+ ),
+ TdpClientEvent.CLIENT_ERROR,
+ false
+ );
}
async handleSharedDirectoryListRequest(buffer: ArrayBuffer) {
@@ -328,7 +343,7 @@ export default class Client extends EventEmitterWebAuthnSender {
fsoList,
});
} catch (e) {
- this.handleError(e);
+ this.handleError(e, TdpClientEvent.CLIENT_ERROR);
}
}
@@ -348,12 +363,15 @@ export default class Client extends EventEmitterWebAuthnSender {
try {
this.socket.send(data);
} catch (e) {
- this.handleError(e);
+ this.handleError(e, TdpClientEvent.CLIENT_ERROR);
}
return;
}
- this.handleError(new Error('websocket unavailable'));
+ this.handleError(
+ new Error('websocket unavailable'),
+ TdpClientEvent.CLIENT_ERROR
+ );
}
sendUsername(username: string) {
@@ -394,7 +412,7 @@ export default class Client extends EventEmitterWebAuthnSender {
try {
this.sdManager.add(sharedDirectory);
} catch (err) {
- this.handleError(err);
+ this.handleError(err, TdpClientEvent.CLIENT_ERROR);
}
}
@@ -402,18 +420,18 @@ export default class Client extends EventEmitterWebAuthnSender {
let name: string;
try {
name = this.sdManager.getName();
+ 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,
+ })
+ );
} catch (e) {
- this.handleError(e);
+ this.handleError(e, TdpClientEvent.CLIENT_ERROR);
}
- 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,
- })
- );
}
sendSharedDirectoryInfoResponse(res: SharedDirectoryInfoResponse) {
@@ -424,6 +442,10 @@ export default class Client extends EventEmitterWebAuthnSender {
this.send(this.codec.encodeSharedDirectoryListResponse(res));
}
+ sendSharedDirectoryMoveResponse(res: SharedDirectoryMoveResponse) {
+ this.send(this.codec.encodeSharedDirectoryMoveResponse(res));
+ }
+
sendSharedDirectoryReadResponse(response: SharedDirectoryReadResponse) {
this.send(this.codec.encodeSharedDirectoryReadResponse(response));
}
@@ -436,12 +458,15 @@ export default class Client extends EventEmitterWebAuthnSender {
this.send(this.codec.encodeClientScreenSpec(spec));
}
- // Emits an TdpClientEvent.ERROR event. Sets this.errored to true to alert the socket.onclose handler that
- // it needn't emit a generic unknown error event.
- private handleError(err: Error) {
+ // Emits an errType event, closing the socket if the error was fatal.
+ private handleError(
+ err: Error,
+ errType: TdpClientEvent.TDP_ERROR | TdpClientEvent.CLIENT_ERROR,
+ isFatal = true
+ ) {
this.logger.error(err);
- this.emit(TdpClientEvent.TDP_ERROR, err);
- this.socket?.close();
+ this.emit(errType, { err, isFatal });
+ if (isFatal) this.socket?.close();
}
// Ensures full cleanup of this object.
diff --git a/packages/teleport/src/lib/tdp/codec.ts b/packages/teleport/src/lib/tdp/codec.ts
index 293608dd5..842884ba0 100644
--- a/packages/teleport/src/lib/tdp/codec.ts
+++ b/packages/teleport/src/lib/tdp/codec.ts
@@ -46,6 +46,7 @@ export enum MessageType {
SHARED_DIRECTORY_WRITE_REQUEST = 21,
SHARED_DIRECTORY_WRITE_RESPONSE = 22,
SHARED_DIRECTORY_MOVE_REQUEST = 23,
+ SHARED_DIRECTORY_MOVE_RESPONSE = 24,
SHARED_DIRECTORY_LIST_REQUEST = 25,
SHARED_DIRECTORY_LIST_RESPONSE = 26,
__LAST, // utility value
@@ -168,6 +169,12 @@ export type SharedDirectoryMoveRequest = {
newPath: string;
};
+// | message type (24) | completion_id uint32 | err_code uint32 |
+export type SharedDirectoryMoveResponse = {
+ completionId: number;
+ errCode: SharedDirectoryErrCode;
+};
+
// | message type (25) | completion_id uint32 | directory_id uint32 | path_length uint32 | path []byte |
export type SharedDirectoryListRequest = {
completionId: number;
@@ -589,6 +596,7 @@ export default class Codec {
return buffer;
}
+ // | message type (22) | completion_id uint32 | err_code uint32 | bytes_written uint32 |
encodeSharedDirectoryWriteResponse(
res: SharedDirectoryWriteResponse
): Message {
@@ -609,6 +617,23 @@ export default class Codec {
return buffer;
}
+ // | message type (24) | completion_id uint32 | err_code uint32 |
+ encodeSharedDirectoryMoveResponse(res: SharedDirectoryMoveResponse): Message {
+ const bufLen = byteLength + 2 * uint32Length;
+ const buffer = new ArrayBuffer(bufLen);
+ const view = new DataView(buffer);
+ let offset = 0;
+
+ view.setUint8(offset, MessageType.SHARED_DIRECTORY_MOVE_RESPONSE);
+ offset += byteLength;
+ view.setUint32(offset, res.completionId);
+ offset += uint32Length;
+ view.setUint32(offset, res.errCode);
+ offset += uint32Length;
+
+ return buffer;
+ }
+
// | message type (26) | completion_id uint32 | err_code uint32 | fso_list_length uint32 | fso_list fso[] |
encodeSharedDirectoryListResponse(res: SharedDirectoryListResponse): Message {
const bufLenSansFsoList = byteLength + 3 * uint32Length;