+
-
`;
@@ -514,194 +517,197 @@ exports[`connected settings true 1`] = `
`;
exports[`connection error 1`] = `
-.c6 {
- display: inline-block;
- transition: color 0.3s;
- padding-right: 16px;
+.c7 {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 2px;
+ box-sizing: border-box;
+ box-shadow: 0 1px 4px rgba(0,0,0,0.24);
+ margin: 0 0 24px 0;
+ min-height: 40px;
+ padding: 8px 16px;
+ overflow: auto;
+ word-break: break-word;
+ line-height: 1.5;
+ background: #f50057;
color: #FFFFFF;
}
-.c10 {
- display: inline-block;
- transition: color 0.3s;
+.c7 a {
color: #FFFFFF;
}
.c9 {
+ 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: 40px;
font-size: 12px;
- height: 24px;
- width: 24px;
- margin-left: 24px;
- color: rgba(255,255,255,0.56);
+ padding: 0px 40px;
+ width: 30%;
}
-.c9 .c5 {
- color: inherit;
+.c9:active {
+ opacity: 0.56;
}
-.c9:disabled {
- color: rgba(255,255,255,0.3);
+.c9:hover,
+.c9:focus {
+ background: #2C3A73;
}
.c9: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);
-}
-
-.c2 {
+.c5 {
overflow: hidden;
text-overflow: ellipsis;
+ font-weight: 300;
+ font-size: 22px;
+ line-height: 32px;
+ text-transform: uppercase;
margin: 0px;
- padding-left: 16px;
- padding-right: 16px;
+ color: rgba(255,255,255,0.87);
+}
+
+.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: 484px;
}
.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: 32px;
+ flex: 1;
+ display: flex;
+ flex-direction: column;
}
-.c7 {
- font-weight: 600;
- font-size: 22px;
- align-self: 'center';
+.c8 {
+ box-sizing: border-box;
}
-.c11 {
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 2px;
- box-sizing: border-box;
- box-shadow: 0 1px 4px rgba(0,0,0,0.24);
- margin: 0 0 24px 0;
- min-height: 40px;
- padding: 8px 16px;
- overflow: auto;
- word-break: break-word;
- line-height: 1.5;
- margin-left: 72px;
- margin-right: 72px;
- margin-top: 8px;
- margin-bottom: 8px;
- background: #f50057;
- color: #FFFFFF;
- align-self: center;
- min-width: 450px;
-}
-
-.c11 a {
- color: #FFFFFF;
-}
-
-
+
+
-
`;
@@ -887,302 +893,401 @@ exports[`disconnected 1`] = `
`;
-exports[`fetch error 1`] = `
-.c6 {
- display: inline-block;
- transition: color 0.3s;
- padding-right: 16px;
+exports[`dismissible error 1`] = `
+.c7 {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 2px;
+ box-sizing: border-box;
+ box-shadow: 0 1px 4px rgba(0,0,0,0.24);
+ margin: 0 0 24px 0;
+ min-height: 40px;
+ padding: 8px 16px;
+ overflow: auto;
+ word-break: break-word;
+ line-height: 1.5;
+ margin-top: 8px;
+ margin-bottom: 8px;
+ background: #ff9100;
color: #FFFFFF;
}
-.c10 {
- display: inline-block;
- transition: color 0.3s;
+.c7 a {
color: #FFFFFF;
}
.c9 {
+ 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: 40px;
font-size: 12px;
- height: 24px;
- width: 24px;
- margin-left: 24px;
- color: rgba(255,255,255,0.56);
+ padding: 0px 40px;
+ width: 30%;
}
-.c9 .c5 {
- color: inherit;
+.c9:active {
+ opacity: 0.56;
}
-.c9:disabled {
- color: rgba(255,255,255,0.3);
+.c9:hover,
+.c9:focus {
+ background: #2C3A73;
}
.c9: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);
-}
-
-.c2 {
+.c5 {
overflow: hidden;
text-overflow: ellipsis;
+ font-weight: 300;
+ font-size: 22px;
+ line-height: 32px;
+ text-transform: uppercase;
margin: 0px;
- padding-left: 16px;
- padding-right: 16px;
+ color: rgba(255,255,255,0.87);
+}
+
+.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: 484px;
}
.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';
-}
-
-.c7 {
- font-weight: 600;
- font-size: 22px;
- align-self: 'center';
-}
-
-.c11 {
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 2px;
+.c6 {
box-sizing: border-box;
- box-shadow: 0 1px 4px rgba(0,0,0,0.24);
- margin: 0 0 24px 0;
- min-height: 40px;
- padding: 8px 16px;
- overflow: auto;
- word-break: break-word;
- line-height: 1.5;
- margin-left: 72px;
- margin-right: 72px;
- margin-top: 8px;
- margin-bottom: 8px;
- background: #f50057;
- color: #FFFFFF;
- align-self: center;
- min-width: 450px;
+ margin-bottom: 32px;
+ flex: 1;
+ display: flex;
+ flex-direction: column;
}
-.c11 a {
- color: #FFFFFF;
+.c8 {
+ box-sizing: border-box;
}
-
+
+
-
`;
-exports[`unintended disconnect 1`] = `
-.c6 {
- display: inline-block;
- transition: color 0.3s;
- padding-right: 16px;
+exports[`fetch error 1`] = `
+.c7 {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 2px;
+ box-sizing: border-box;
+ box-shadow: 0 1px 4px rgba(0,0,0,0.24);
+ margin: 0 0 24px 0;
+ min-height: 40px;
+ padding: 8px 16px;
+ overflow: auto;
+ word-break: break-word;
+ line-height: 1.5;
+ background: #f50057;
color: #FFFFFF;
}
-.c10 {
- display: inline-block;
- transition: color 0.3s;
+.c7 a {
color: #FFFFFF;
}
.c9 {
+ 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: 40px;
font-size: 12px;
- height: 24px;
- width: 24px;
- margin-left: 24px;
- color: rgba(255,255,255,0.56);
+ padding: 0px 40px;
+ width: 30%;
}
-.c9 .c5 {
- color: inherit;
+.c9:active {
+ opacity: 0.56;
}
-.c9:disabled {
- color: rgba(255,255,255,0.3);
+.c9:hover,
+.c9:focus {
+ background: #2C3A73;
}
.c9: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);
-}
-
-.c2 {
+.c5 {
overflow: hidden;
text-overflow: ellipsis;
+ font-weight: 300;
+ font-size: 22px;
+ line-height: 32px;
+ text-transform: uppercase;
margin: 0px;
- padding-left: 16px;
- padding-right: 16px;
+ color: rgba(255,255,255,0.87);
+}
+
+.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: 484px;
}
.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: 32px;
+ flex: 1;
+ display: flex;
+ flex-direction: column;
}
-.c7 {
- font-weight: 600;
- font-size: 22px;
- align-self: 'center';
+.c8 {
+ box-sizing: border-box;
}
-.c11 {
+
+
+
+
+
+
+
+ some fetch error
+
+ Refresh the page to try again.
+
+
+
+
+
+
+
+`;
+
+exports[`unintended disconnect 1`] = `
+.c7 {
display: flex;
align-items: center;
justify-content: center;
@@ -1195,249 +1300,418 @@ exports[`unintended disconnect 1`] = `
overflow: auto;
word-break: break-word;
line-height: 1.5;
- margin-left: 72px;
- margin-right: 72px;
- margin-top: 8px;
- margin-bottom: 8px;
background: #f50057;
color: #FFFFFF;
- align-self: center;
- min-width: 450px;
}
-.c11 a {
+.c7 a {
color: #FFFFFF;
}
-
+.c9 {
+ 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;
+ 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: #222C59;
+ color: rgba(255,255,255,0.87);
+ min-height: 40px;
+ font-size: 12px;
+ padding: 0px 40px;
+ width: 30%;
+}
+
+.c9:active {
+ opacity: 0.56;
+}
+
+.c9:hover,
+.c9:focus {
+ background: #2C3A73;
+}
+
+.c9:disabled {
+ background: rgba(255,255,255,0.12);
+ color: rgba(255,255,255,0.3);
+}
+
+.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);
+}
+
+.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 {
+ position: fixed;
+ z-index: 1200;
+ right: 0;
+ bottom: 0;
+ top: 0;
+ left: 0;
+}
+
+.c2 {
+ height: 100%;
+ outline: none;
+ color: black;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 1;
+ will-change: opacity;
+ transition: opacity 225ms cubic-bezier(0.4,0,0.2,1) 0ms;
+}
+
+.c3 {
+ 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: 484px;
+}
+
+.c4 {
+ box-sizing: border-box;
+ margin-bottom: 16px;
+ min-height: 32px;
+ display: flex;
+ align-items: center;
+}
+
+.c6 {
+ box-sizing: border-box;
+ margin-bottom: 32px;
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+}
+
+.c8 {
+ box-sizing: border-box;
+}
+
+
+
-
`;
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 399ba2ffb..99585ea5d 100644
--- a/packages/teleport/src/DesktopSession/useDesktopSession.tsx
+++ b/packages/teleport/src/DesktopSession/useDesktopSession.tsx
@@ -28,10 +28,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');
@@ -43,8 +43,11 @@ export default function useDesktopSession() {
// disconnected tracks whether the user intentionally disconnected the client
const [disconnected, setDisconnected] = useState(false);
- const [canShareDirectory, setCanShareDirectory] = useState(false);
- const [isSharingDirectory, setIsSharingDirectory] = useState(false);
+ const [directorySharingState, setDirectorySharingState] = useState({
+ canShare: false,
+ isSharing: false,
+ browserError: false,
+ });
const { username, desktopName, clusterId } = useParams
();
@@ -124,7 +127,10 @@ export default function useDesktopSession() {
.then(desktop => setHostname(desktop.name)),
userService.fetchUserContext().then(user => {
setHasClipboardSharingEnabled(user.acl.clipboardSharingEnabled);
- setCanShareDirectory(user.acl.directorySharingEnabled);
+ setDirectorySharingState(prevState => ({
+ ...prevState,
+ canShare: user.acl.directorySharingEnabled,
+ }));
}),
])
);
@@ -137,7 +143,7 @@ export default function useDesktopSession() {
setTdpConnection,
setWsConnection,
setClipboardState,
- setIsSharingDirectory,
+ setDirectorySharingState,
enableClipboardSharing:
clipboardState.enabled &&
clipboardState.permission.state === 'granted' &&
@@ -151,15 +157,16 @@ export default function useDesktopSession() {
username,
clipboardState,
setClipboardState,
- canShareDirectory,
- isSharingDirectory,
- setIsSharingDirectory,
+ directorySharingState,
+ setDirectorySharingState,
+ isUsingChrome,
fetchAttempt,
tdpConnection,
wsConnection,
disconnected,
setDisconnected,
webauthn,
+ setTdpConnection,
...clientCanvasProps,
};
}
diff --git a/packages/teleport/src/DesktopSession/useTdpClientCanvas.tsx b/packages/teleport/src/DesktopSession/useTdpClientCanvas.tsx
index 975110a9e..8065c4bc7 100644
--- a/packages/teleport/src/DesktopSession/useTdpClientCanvas.tsx
+++ b/packages/teleport/src/DesktopSession/useTdpClientCanvas.tsx
@@ -31,7 +31,7 @@ export default function useTdpClientCanvas(props: Props) {
setTdpConnection,
setWsConnection,
setClipboardState,
- setIsSharingDirectory,
+ setDirectorySharingState,
enableClipboardSharing,
} = props;
const [tdpClient, setTdpClient] = useState(null);
@@ -77,14 +77,21 @@ export default function useTdpClientCanvas(props: Props) {
}
};
- // Default TdpClientEvent.TDP_ERROR handler
- const onTdpError = (err: Error) => {
- setIsSharingDirectory(false);
+ // Default TdpClientEvent.TDP_ERROR and TdpClientEvent.CLIENT_ERROR handler
+ const onTdpError = (error: { err: Error; isFatal: boolean }) => {
+ const { err, isFatal } = error;
+ setDirectorySharingState(prevState => ({
+ ...prevState,
+ isSharing: false,
+ }));
setClipboardState(prevState => ({
...prevState,
enabled: false,
}));
- setTdpConnection({ status: 'failed', statusText: err.message });
+ setTdpConnection({
+ status: isFatal ? 'failed' : '',
+ statusText: err.message,
+ });
};
const onWsClose = () => {
@@ -215,6 +222,12 @@ type Props = {
errorText: string;
}>
>;
- setIsSharingDirectory: Dispatch>;
+ setDirectorySharingState: Dispatch<
+ SetStateAction<{
+ canShare: boolean;
+ isSharing: boolean;
+ browserError: boolean;
+ }>
+ >;
enableClipboardSharing: boolean;
};
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 1dd579649..75baa74c7 100644
--- a/packages/teleport/src/components/TdpClientCanvas/TdpClientCanvas.tsx
+++ b/packages/teleport/src/components/TdpClientCanvas/TdpClientCanvas.tsx
@@ -128,9 +128,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]);
@@ -291,7 +293,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 077ee6234..bdad9ee56 100644
--- a/packages/teleport/src/lib/tdp/client.ts
+++ b/packages/teleport/src/lib/tdp/client.ts
@@ -23,14 +23,31 @@ import Codec, {
ClientScreenSpec,
PngFrame,
ClipboardData,
+ FileType,
SharedDirectoryErrCode,
+ SharedDirectoryInfoResponse,
+ SharedDirectoryListResponse,
+ SharedDirectoryMoveResponse,
+ SharedDirectoryReadResponse,
+ SharedDirectoryWriteResponse,
+ SharedDirectoryCreateResponse,
+ SharedDirectoryDeleteResponse,
+ FileSystemObject,
} from './codec';
+import {
+ PathDoesNotExistError,
+ SharedDirectoryManager,
+ FileOrDirInfo,
+} from './sharedDirectoryManager';
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',
}
@@ -43,7 +60,7 @@ 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');
@@ -51,6 +68,7 @@ export default class Client extends EventEmitterWebAuthnSender {
super();
this.socketAddr = socketAddr;
this.codec = new Codec();
+ this.sdManager = new SharedDirectoryManager();
}
// Connect to the websocket and register websocket event handlers.
@@ -63,8 +81,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
@@ -84,7 +102,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) {
@@ -104,7 +124,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);
@@ -115,11 +138,34 @@ 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_DELETE_REQUEST:
+ this.handleSharedDirectoryDeleteRequest(buffer);
+ break;
+ case MessageType.SHARED_DIRECTORY_READ_REQUEST:
+ this.handleSharedDirectoryReadRequest(buffer);
+ break;
+ case MessageType.SHARED_DIRECTORY_WRITE_REQUEST:
+ this.handleSharedDirectoryWriteRequest(buffer);
+ break;
+ case MessageType.SHARED_DIRECTORY_MOVE_REQUEST:
+ this.handleSharedDirectoryMoveRequest(buffer);
+ break;
+ case MessageType.SHARED_DIRECTORY_LIST_REQUEST:
+ this.handleSharedDirectoryListRequest(buffer);
+ break;
default:
this.logger.warn(`received unsupported message type ${messageType}`);
}
} catch (err) {
- this.handleError(err);
+ this.handleError(err, TdpClientEvent.CLIENT_ERROR);
}
}
@@ -162,9 +208,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);
@@ -178,11 +221,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);
}
}
@@ -192,7 +236,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;
}
@@ -203,28 +248,187 @@ export default class Client extends EventEmitterWebAuthnSender {
if (!this.wasSuccessful(ack.errCode)) {
return;
}
-
- this.logger.info('Started sharing directory: ' + this.sharedDirectory.name);
+ try {
+ this.logger.info(
+ 'Started sharing directory: ' + this.sdManager.getName()
+ );
+ } catch (e) {
+ this.handleError(e, TdpClientEvent.CLIENT_ERROR);
+ }
}
- 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)
+ const path = req.path;
+ try {
+ const info = await this.sdManager.getInfo(path);
+ this.sendSharedDirectoryInfoResponse({
+ completionId: req.completionId,
+ errCode: SharedDirectoryErrCode.Nil,
+ fso: this.toFso(info),
+ });
+ } 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, TdpClientEvent.CLIENT_ERROR);
+ }
+ }
+ }
+
+ 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 handleSharedDirectoryDeleteRequest(buffer: ArrayBuffer) {
+ const req = this.codec.decodeSharedDirectoryDeleteRequest(buffer);
+
+ try {
+ await this.sdManager.delete(req.path);
+ this.sendSharedDirectoryDeleteResponse({
+ completionId: req.completionId,
+ errCode: SharedDirectoryErrCode.Nil,
+ });
+ } catch (e) {
+ this.sendSharedDirectoryDeleteResponse({
+ completionId: req.completionId,
+ errCode: SharedDirectoryErrCode.Failed,
+ });
+ this.handleError(e, TdpClientEvent.CLIENT_ERROR, false);
+ }
+ }
+
+ async handleSharedDirectoryReadRequest(buffer: ArrayBuffer) {
+ const req = this.codec.decodeSharedDirectoryReadRequest(buffer);
+ try {
+ const readData = await this.sdManager.readFile(
+ req.path,
+ req.offset,
+ req.length
+ );
+ this.sendSharedDirectoryReadResponse({
+ completionId: req.completionId,
+ errCode: SharedDirectoryErrCode.Nil,
+ readDataLength: readData.length,
+ readData,
+ });
+ } catch (e) {
+ this.handleError(e, TdpClientEvent.CLIENT_ERROR);
+ }
+ }
+
+ async handleSharedDirectoryWriteRequest(buffer: ArrayBuffer) {
+ const req = this.codec.decodeSharedDirectoryWriteRequest(buffer);
+ try {
+ const bytesWritten = await this.sdManager.writeFile(
+ req.path,
+ req.offset,
+ req.writeData
+ );
+
+ this.sendSharedDirectoryWriteResponse({
+ completionId: req.completionId,
+ errCode: SharedDirectoryErrCode.Nil,
+ bytesWritten,
+ });
+ } catch (e) {
+ this.handleError(e, TdpClientEvent.CLIENT_ERROR);
+ }
+ }
+
+ handleSharedDirectoryMoveRequest(buffer: ArrayBuffer) {
+ const req = this.codec.decodeSharedDirectoryMoveRequest(buffer);
+ // 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
);
- // TODO(isaiah): here's where we'll respond with SharedDirectoryInfoResponse
+ }
+
+ async handleSharedDirectoryListRequest(buffer: ArrayBuffer) {
+ try {
+ const req = this.codec.decodeSharedDirectoryListRequest(buffer);
+ const path = req.path;
+
+ const infoList: FileOrDirInfo[] = await this.sdManager.listContents(path);
+ const fsoList: FileSystemObject[] = infoList.map(info =>
+ this.toFso(info)
+ );
+
+ this.sendSharedDirectoryListResponse({
+ completionId: req.completionId,
+ errCode: SharedDirectoryErrCode.Nil,
+ fsoList,
+ });
+ } catch (e) {
+ this.handleError(e, TdpClientEvent.CLIENT_ERROR);
+ }
+ }
+
+ private toFso(info: FileOrDirInfo): FileSystemObject {
+ return {
+ lastModified: BigInt(info.lastModified),
+ fileType: info.kind === 'file' ? FileType.File : FileType.Directory,
+ size: BigInt(info.size),
+ path: info.path,
+ };
}
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, TdpClientEvent.CLIENT_ERROR);
+ }
return;
}
- this.handleError(new Error('websocket unavailable'));
+ this.handleError(
+ new Error('websocket unavailable'),
+ TdpClientEvent.CLIENT_ERROR
+ );
}
sendUsername(username: string) {
@@ -261,42 +465,73 @@ 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'
- )
+ addSharedDirectory(sharedDirectory: FileSystemDirectoryHandle) {
+ try {
+ this.sdManager.add(sharedDirectory);
+ } catch (err) {
+ this.handleError(err, TdpClientEvent.CLIENT_ERROR);
+ }
+ }
+
+ sendSharedDirectoryAnnounce() {
+ 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,
+ })
);
- return false;
+ } catch (e) {
+ this.handleError(e, TdpClientEvent.CLIENT_ERROR);
}
+ }
- return true;
+ sendSharedDirectoryInfoResponse(res: SharedDirectoryInfoResponse) {
+ this.send(this.codec.encodeSharedDirectoryInfoResponse(res));
}
- sendSharedDirectoryAnnounce() {
- if (!this.sharedDirectoryReady()) return;
- this.socket.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,
- })
- );
+ sendSharedDirectoryListResponse(res: SharedDirectoryListResponse) {
+ this.send(this.codec.encodeSharedDirectoryListResponse(res));
+ }
+
+ sendSharedDirectoryMoveResponse(res: SharedDirectoryMoveResponse) {
+ this.send(this.codec.encodeSharedDirectoryMoveResponse(res));
+ }
+
+ sendSharedDirectoryReadResponse(response: SharedDirectoryReadResponse) {
+ this.send(this.codec.encodeSharedDirectoryReadResponse(response));
+ }
+
+ sendSharedDirectoryWriteResponse(response: SharedDirectoryWriteResponse) {
+ this.send(this.codec.encodeSharedDirectoryWriteResponse(response));
+ }
+
+ sendSharedDirectoryCreateResponse(response: SharedDirectoryCreateResponse) {
+ this.send(this.codec.encodeSharedDirectoryCreateResponse(response));
+ }
+
+ sendSharedDirectoryDeleteResponse(response: SharedDirectoryDeleteResponse) {
+ this.send(this.codec.encodeSharedDirectoryDeleteResponse(response));
}
resize(spec: ClientScreenSpec) {
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 ccc1e848d..971073a8e 100644
--- a/packages/teleport/src/lib/tdp/codec.ts
+++ b/packages/teleport/src/lib/tdp/codec.ts
@@ -40,6 +40,19 @@ export enum MessageType {
SHARED_DIRECTORY_ANNOUNCE = 11,
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_DELETE_REQUEST = 17,
+ SHARED_DIRECTORY_DELETE_RESPONSE = 18,
+ SHARED_DIRECTORY_READ_REQUEST = 19,
+ SHARED_DIRECTORY_READ_RESPONSE = 20,
+ 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
}
@@ -95,7 +108,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;
@@ -108,6 +121,114 @@ 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;
+};
+
+// | 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 (17) | completion_id uint32 | directory_id uint32 | path_length uint32 | path []byte |
+export type SharedDirectoryDeleteRequest = {
+ completionId: number;
+ directoryId: number;
+ path: string;
+};
+
+// | message type (18) | completion_id uint32 | err_code uint32 |
+export type SharedDirectoryDeleteResponse = {
+ completionId: number;
+ errCode: SharedDirectoryErrCode;
+};
+
+// | message type (19) | completion_id uint32 | directory_id uint32 | path_length uint32 | path []byte | offset uint64 | length uint32 |
+export type SharedDirectoryReadRequest = {
+ completionId: number;
+ directoryId: number;
+ path: string;
+ pathLength: number;
+ offset: bigint;
+ length: number;
+};
+
+// | message type (20) | completion_id uint32 | err_code uint32 | read_data_length uint32 | read_data []byte |
+export type SharedDirectoryReadResponse = {
+ completionId: number;
+ errCode: SharedDirectoryErrCode;
+ readDataLength: number;
+ readData: Uint8Array;
+};
+
+// | message type (21) | completion_id uint32 | directory_id uint32 | path_length uint32 | path []byte | offset uint64 | write_data_length uint32 | write_data []byte |
+export type SharedDirectoryWriteRequest = {
+ completionId: number;
+ directoryId: number;
+ pathLength: number;
+ path: string;
+ offset: bigint;
+ writeData: Uint8Array;
+};
+
+// | message type (22) | completion_id uint32 | err_code uint32 | bytes_written uint32 |
+export type SharedDirectoryWriteResponse = {
+ completionId: number;
+ errCode: number;
+ bytesWritten: number;
+};
+
+// | message type (23) | completion_id uint32 | directory_id uint32 | original_path_length uint32 | original_path []byte | new_path_length uint32 | new_path []byte |
+export type SharedDirectoryMoveRequest = {
+ completionId: number;
+ directoryId: number;
+ originalPathLength: number;
+ originalPath: string;
+ newPathLength: number;
+ 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;
+ directoryId: number;
+ path: string;
+};
+
+// | message type (26) | completion_id uint32 | err_code uint32 | fso_list_length uint32 | fso_list fso[] |
+export type SharedDirectoryListResponse = {
+ completionId: number;
+ errCode: SharedDirectoryErrCode;
+ fsoList: 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,
@@ -119,6 +240,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}`);
@@ -457,6 +583,185 @@ 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;
+ }
+
+ // | 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 (18) | completion_id uint32 | err_code uint32 |
+ encodeSharedDirectoryDeleteResponse(
+ res: SharedDirectoryDeleteResponse
+ ): 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_DELETE_RESPONSE);
+ offset += byteLength;
+ view.setUint32(offset, res.completionId);
+ offset += uint32Length;
+ view.setUint32(offset, res.errCode);
+ offset += uint32Length;
+
+ return buffer;
+ }
+
+ // | message type (20) | completion_id uint32 | err_code uint32 | read_data_length uint32 | read_data []byte |
+ encodeSharedDirectoryReadResponse(res: SharedDirectoryReadResponse): Message {
+ const bufLen =
+ byteLength + 3 * uint32Length + byteLength * res.readDataLength;
+ const buffer = new ArrayBuffer(bufLen);
+ const view = new DataView(buffer);
+ let offset = 0;
+
+ view.setUint8(offset, MessageType.SHARED_DIRECTORY_READ_RESPONSE);
+ offset += byteLength;
+ view.setUint32(offset, res.completionId);
+ offset += uint32Length;
+ view.setUint32(offset, res.errCode);
+ offset += uint32Length;
+ view.setUint32(offset, res.readDataLength);
+ offset += uint32Length;
+ res.readData.forEach(byte => {
+ view.setUint8(offset++, byte);
+ });
+
+ return buffer;
+ }
+
+ // | message type (22) | completion_id uint32 | err_code uint32 | bytes_written uint32 |
+ encodeSharedDirectoryWriteResponse(
+ res: SharedDirectoryWriteResponse
+ ): Message {
+ const bufLen = byteLength + 3 * uint32Length;
+ const buffer = new ArrayBuffer(bufLen);
+ const view = new DataView(buffer);
+ let offset = 0;
+
+ view.setUint8(offset, MessageType.SHARED_DIRECTORY_WRITE_RESPONSE);
+ offset += byteLength;
+ view.setUint32(offset, res.completionId);
+ offset += uint32Length;
+ view.setUint32(offset, res.errCode);
+ offset += uint32Length;
+ view.setUint32(offset, res.bytesWritten);
+ offset += uint32Length;
+
+ 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;
+ const bufferSansFsoList = new ArrayBuffer(bufLenSansFsoList);
+ const view = new DataView(bufferSansFsoList);
+ let offset = 0;
+
+ view.setUint8(offset++, MessageType.SHARED_DIRECTORY_LIST_RESPONSE);
+ view.setUint32(offset, res.completionId);
+ offset += uint32Length;
+ view.setUint32(offset, res.errCode);
+ offset += uint32Length;
+ view.setUint32(offset, res.fsoList.length);
+ offset += uint32Length;
+
+ let withFsoList = new Uint8Array(bufferSansFsoList);
+ res.fsoList.forEach(fso => {
+ const fsoBuffer = this.encodeFileSystemObject(fso);
+
+ // https://gist.github.com/72lions/4528834?permalink_comment_id=2395442#gistcomment-2395442
+ withFsoList = new Uint8Array([
+ ...withFsoList,
+ ...new Uint8Array(fsoBuffer),
+ ]);
+ });
+
+ return withFsoList.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 {
@@ -484,7 +789,7 @@ export default class Codec {
// decodeMfaChallenge decodes a raw tdp MFA challenge message and returns it as a string (of a json).
// | message type (10) | mfa_type byte | message_length uint32 | json []byte
decodeMfaJson(buffer: ArrayBuffer): MfaJson {
- let dv = new DataView(buffer);
+ const dv = new DataView(buffer);
let offset = 0;
offset += byteLength; // eat message type
const mfaType = String.fromCharCode(dv.getUint8(offset));
@@ -533,7 +838,7 @@ 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 {
@@ -541,7 +846,7 @@ export default class Codec {
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 {
@@ -571,6 +876,155 @@ 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 (17) | completion_id uint32 | directory_id uint32 | path_length uint32 | path []byte |
+ decodeSharedDirectoryDeleteRequest(
+ buffer: ArrayBuffer
+ ): SharedDirectoryDeleteRequest {
+ 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
+ offset += uint32Length; // eat path_length
+ const path = this.decoder.decode(new Uint8Array(buffer.slice(offset)));
+
+ return {
+ completionId,
+ directoryId,
+ path,
+ };
+ }
+
+ // | message type (19) | completion_id uint32 | directory_id uint32 | path_length uint32 | path []byte | offset uint64 | length uint32 |
+ decodeSharedDirectoryReadRequest(
+ buffer: ArrayBuffer
+ ): SharedDirectoryReadRequest {
+ const dv = new DataView(buffer);
+ let bufOffset = 0;
+ bufOffset += byteLength; // eat message type
+ const completionId = dv.getUint32(bufOffset);
+ bufOffset += uint32Length; // eat completion_id
+ const directoryId = dv.getUint32(bufOffset);
+ bufOffset += uint32Length; // eat directory_id
+ const pathLength = dv.getUint32(bufOffset);
+ bufOffset += uint32Length; // eat path_length
+ const path = this.decoder.decode(
+ new Uint8Array(buffer.slice(bufOffset, bufOffset + pathLength))
+ );
+ bufOffset += pathLength; // eat path
+ const offset = dv.getBigUint64(bufOffset);
+ bufOffset += uint64Length; // eat offset
+ const length = dv.getUint32(bufOffset);
+
+ return {
+ completionId,
+ directoryId,
+ pathLength,
+ path,
+ offset,
+ length,
+ };
+ }
+
+ // | message type (21) | completion_id uint32 | directory_id uint32 | path_length uint32 | path []byte | offset uint64 | write_data_length uint32 | write_data []byte |
+ decodeSharedDirectoryWriteRequest(
+ buffer: ArrayBuffer
+ ): SharedDirectoryWriteRequest {
+ const dv = new DataView(buffer);
+ let bufOffset = byteLength; // eat message type
+ const completionId = dv.getUint32(bufOffset);
+ bufOffset += uint32Length; // eat completion_id
+ const directoryId = dv.getUint32(bufOffset);
+ bufOffset += uint32Length; // eat directory_id
+ const offset = dv.getBigUint64(bufOffset);
+ bufOffset += uint64Length; // eat offset
+ const pathLength = dv.getUint32(bufOffset);
+ bufOffset += uint32Length; // eat path_length
+ const path = this.decoder.decode(
+ new Uint8Array(buffer.slice(bufOffset, bufOffset + pathLength))
+ );
+ bufOffset += pathLength; // eat path
+ const writeDataLength = dv.getUint32(bufOffset);
+ bufOffset += uint32Length; // eat write_data_length
+ const writeData = new Uint8Array(
+ buffer.slice(bufOffset, bufOffset + writeDataLength)
+ );
+
+ return {
+ completionId,
+ directoryId,
+ pathLength,
+ path,
+ offset,
+ writeData,
+ };
+ }
+
+ // | message type (23) | completion_id uint32 | directory_id uint32 | original_path_length uint32 | original_path []byte | new_path_length uint32 | new_path []byte |
+ decodeSharedDirectoryMoveRequest(
+ buffer: ArrayBuffer
+ ): SharedDirectoryMoveRequest {
+ const dv = new DataView(buffer);
+ let bufOffset = byteLength; // eat message type
+ const completionId = dv.getUint32(bufOffset);
+ bufOffset += uint32Length; // eat completion_id
+ const directoryId = dv.getUint32(bufOffset);
+ bufOffset += uint32Length; // eat directory_id
+ const originalPathLength = dv.getUint32(bufOffset);
+ bufOffset += uint32Length; // eat original_path_length
+ const originalPath = this.decoder.decode(
+ new Uint8Array(buffer.slice(bufOffset, bufOffset + originalPathLength))
+ );
+ bufOffset += originalPathLength; // eat original_path
+ const newPathLength = dv.getUint32(bufOffset);
+ bufOffset += uint32Length; // eat new_path_length
+ const newPath = this.decoder.decode(
+ new Uint8Array(buffer.slice(bufOffset, bufOffset + newPathLength))
+ );
+
+ return {
+ completionId,
+ directoryId,
+ originalPathLength,
+ originalPath,
+ newPathLength,
+ newPath,
+ };
+ }
+
+ // | message type (25) | completion_id uint32 | directory_id uint32 | path_length uint32 | path []byte |
+ decodeSharedDirectoryListRequest(
+ buffer: ArrayBuffer
+ ): SharedDirectoryListRequest {
+ return this.decodeSharedDirectoryInfoRequest(buffer);
+ }
+
// asBase64Url creates a data:image uri from the png data part of a PNG_FRAME tdp message.
private asBase64Url(buffer: ArrayBuffer, offset: number): string {
return `data:image/png;base64,${arrayBufferToBase64(buffer.slice(offset))}`;
@@ -579,3 +1033,4 @@ export default class Codec {
const byteLength = 1;
const uint32Length = 4;
+const uint64Length = uint32Length * 2;
diff --git a/packages/teleport/src/lib/tdp/playerClient.ts b/packages/teleport/src/lib/tdp/playerClient.ts
index 9180723a6..9d1be6294 100644
--- a/packages/teleport/src/lib/tdp/playerClient.ts
+++ b/packages/teleport/src/lib/tdp/playerClient.ts
@@ -49,7 +49,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') {
@@ -59,7 +59,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
new file mode 100644
index 000000000..60b90e8a0
--- /dev/null
+++ b/packages/teleport/src/lib/tdp/sharedDirectoryManager.ts
@@ -0,0 +1,264 @@
+// 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.
+import { FileType } from './codec';
+
+// 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;
+
+ /**
+ * @throws Will throw an error if a directory is already being shared.
+ */
+ add(sharedDirectory: FileSystemDirectoryHandle) {
+ if (this.dir) {
+ throw new Error(
+ 'SharedDirectoryManager currently only supports sharing a single directory'
+ );
+ }
+ this.dir = sharedDirectory;
+ }
+
+ /**
+ * @throws Will throw an error if a directory has not already been initialized via add().
+ */
+ getName(): string {
+ this.checkReady();
+ return this.dir.name;
+ }
+
+ /**
+ * Gets the information for the file or directory at path where path is the relative path from the root directory.
+ * @throws Will throw an error if a directory has not already been initialized via add().
+ * @throws {PathDoesNotExistError} if the pathstr isn't a valid path in the shared directory
+ */
+ async getInfo(path: string): Promise {
+ 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, path };
+ }
+
+ let file = await fileOrDir.getFile();
+ return {
+ size: file.size,
+ lastModified: file.lastModified,
+ kind: fileOrDir.kind,
+ path,
+ };
+ }
+
+ /**
+ * Gets the FileOrDirInfo for all the children of the directory at path.
+ * @throws Will throw an error if a directory has not already been initialized via add().
+ * @throws {PathDoesNotExistError} if the pathstr isn't a valid path in the shared directory
+ */
+ async listContents(path: string): Promise {
+ this.checkReady();
+
+ // Get the directory whose contents we want to list.
+ const dir = await this.walkPath(path);
+ if (dir.kind !== 'directory') {
+ throw new Error('cannot list the contents of a file');
+ }
+
+ let infos: FileOrDirInfo[] = [];
+ for await (const entry of dir.values()) {
+ // Create the full relative path to the entry
+ let entryPath = path;
+ if (entryPath !== '') {
+ entryPath = [entryPath, entry.name].join('/');
+ } else {
+ entryPath = entry.name;
+ }
+ infos.push(await this.getInfo(entryPath));
+ }
+
+ return infos;
+ }
+
+ /**
+ * Reads length bytes starting at offset from a file at path.
+ * @throws Will throw an error if a directory has not already been initialized via add().
+ * @throws {PathDoesNotExistError} if the pathstr isn't a valid path in the shared directory
+ */
+ async readFile(
+ path: string,
+ offset: bigint,
+ length: number
+ ): Promise {
+ this.checkReady();
+
+ const fileHandle = await this.walkPath(path);
+ if (fileHandle.kind !== 'file') {
+ throw new Error('cannot read the bytes of a directory');
+ }
+
+ const file = await fileHandle.getFile();
+
+ return new Uint8Array(
+ await file.slice(Number(offset), Number(offset) + length).arrayBuffer()
+ );
+ }
+
+ /**
+ * Writes the bytes in writeData to the file at path starting at offset.
+ * @throws Will throw an error if a directory has not already been initialized via add().
+ * @throws {PathDoesNotExistError} if the pathstr isn't a valid path in the shared directory
+ */
+ async writeFile(
+ path: string,
+ offset: bigint,
+ writeData: Uint8Array
+ ): Promise {
+ this.checkReady();
+
+ const fileHandle = await this.walkPath(path);
+ if (fileHandle.kind !== 'file') {
+ throw new Error('cannot read the bytes of a directory');
+ }
+
+ const file = await fileHandle.createWritable();
+ if (offset > 0) {
+ file.seek(Number(offset));
+ }
+ file.write(writeData);
+ file.close(); // Needed to actually write data to disk.
+
+ 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 });
+ }
+ }
+
+ /**
+ * Deletes a file or directory at path.
+ * If the path doesn't exist, this operation is effectively ignored.
+ * @throws Anything potentially thrown by getFileHandle/getDirectoryHandle.
+ * @throws {PathDoesNotExistError} if the path isn't a valid path to a directory.
+ */
+ async delete(path: string): 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'
+ );
+ }
+
+ await dirHandle.removeEntry(fileOrDirName, { recursive: true });
+ }
+
+ /**
+ * 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.
+ * @throws {PathDoesNotExistError} if the pathstr isn't a valid path in the shared directory
+ */
+ private async walkPath(
+ pathstr: string
+ ): Promise {
+ if (pathstr === '') {
+ return this.dir;
+ }
+
+ let path = pathstr.split('/');
+
+ let walkIt = async (
+ dir: FileSystemDirectoryHandle,
+ path: string[]
+ ): Promise => {
+ // 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);
+ }
+
+ /**
+ * @throws Will throw an error if a directory has not already been initialized via add().
+ */
+ 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);
+ }
+}
+
+export type FileOrDirInfo = {
+ size: number; // bytes
+ lastModified: number; // ms since unix epoch
+ kind: 'file' | 'directory';
+ path: string;
+};
diff --git a/tsconfig.json b/tsconfig.json
index 648f92e8e..6203030d0 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -31,6 +31,9 @@
"skipLibCheck": true,
// TODO: https://github.com/gravitational/webapps/issues/957
"strictNullChecks": false,
+ "types": [
+ "@types/wicg-file-system-access"
+ ],
},
"exclude": [
"node_modules",
diff --git a/yarn.lock b/yarn.lock
index 4e573343f..9304f55d3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3070,10 +3070,10 @@
tapable "^2.2.0"
webpack "^5"
-"@types/wicg-native-file-system@^2020.6.0":
- version "2020.6.0"
- resolved "https://registry.yarnpkg.com/@types/wicg-native-file-system/-/wicg-native-file-system-2020.6.0.tgz#63cbb7bac47bdb9eae4b0d66e63134b33e47e05d"
- integrity sha512-M7n6jvHfUzUXDtf6UGpL6rVIddV7UzEYrvwZPORApeHvDGQnZJ79fXorLlDj8xJKyUemnEBohRd8yx09k9NBUw==
+"@types/wicg-file-system-access@^2020.9.5":
+ version "2020.9.5"
+ resolved "https://registry.yarnpkg.com/@types/wicg-file-system-access/-/wicg-file-system-access-2020.9.5.tgz#4a0c8f3d1ed101525f329e86c978f7735404474f"
+ integrity sha512-UYK244awtmcUYQfs7FR8710MJcefL2WvkyHMjA8yJzxd1mo0Gfn88sRZ1Bls7hiUhA2w7ne1gpJ9T5g3G0wOyA==
"@types/ws@^8.5.1":
version "8.5.3"
@@ -4394,7 +4394,7 @@ braces@^2.3.1, braces@^2.3.2:
split-string "^3.0.2"
to-regex "^3.0.1"
-braces@^3.0.1, braces@~3.0.2:
+braces@^3.0.1, braces@^3.0.2, braces@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
@@ -6269,7 +6269,7 @@ enhanced-resolve@^4.5.0:
memory-fs "^0.5.0"
tapable "^1.0.0"
-enhanced-resolve@^5.10.0:
+enhanced-resolve@^5.0.0, enhanced-resolve@^5.10.0:
version "5.10.0"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.10.0.tgz#0dc579c3bb2a1032e357ac45b8f3a6f3ad4fb1e6"
integrity sha512-T0yTFjdpldGY8PmuXXR0PyQ1ufZpEGiHVrp7zHKB7jdR4qlmZHhONVM5AQOAWXuF/w3dnHbEQVrNptJgt7F+cQ==
@@ -10027,6 +10027,14 @@ micromatch@^3.1.10, micromatch@^3.1.4:
snapdragon "^0.8.1"
to-regex "^3.0.2"
+micromatch@^4.0.0:
+ version "4.0.5"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
+ integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
+ dependencies:
+ braces "^3.0.2"
+ picomatch "^2.3.1"
+
micromatch@^4.0.2, micromatch@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9"
@@ -11084,6 +11092,11 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.0:
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==
+picomatch@^2.3.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
+ integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+
pify@^2.0.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
@@ -13686,6 +13699,16 @@ ts-essentials@^2.0.3:
resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-2.0.12.tgz#c9303f3d74f75fa7528c3d49b80e089ab09d8745"
integrity sha512-3IVX4nI6B5cc31/GFFE+i8ey/N2eA0CZDbo6n0yrz0zDX8ZJ8djmU1p+XRz7G3is0F3bB3pu2pAroFdAWQKU3w==
+ts-loader@^9.3.1:
+ version "9.3.1"
+ resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.3.1.tgz#fe25cca56e3e71c1087fe48dc67f4df8c59b22d4"
+ integrity sha512-OkyShkcZTsTwyS3Kt7a4rsT/t2qvEVQuKCTg4LJmpj9fhFR7ukGdZwV6Qq3tRUkqcXtfGpPR7+hFKHCG/0d3Lw==
+ dependencies:
+ chalk "^4.1.0"
+ enhanced-resolve "^5.0.0"
+ micromatch "^4.0.0"
+ semver "^7.3.4"
+
ts-pnp@^1.1.6:
version "1.2.0"
resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92"