From d74c771fdaab75f9e1785074bc402f4c95ac62d1 Mon Sep 17 00:00:00 2001 From: Will Norris Date: Tue, 22 Aug 2023 16:14:00 -0700 Subject: [PATCH] client/web: always use new web client; remove old client This uses the new react-based web client for all builds, not just with the --dev flag. If the web client assets have not been built, the client will serve a message that Tailscale was built without the web client, and link to build instructions. Because we will include the web client in all of our builds, this should only be seen by developers or users building from source. (And eventually this will be replaced by attempting to download needed assets as runtime.) We do now checkin the build/index.html file, which serves the error message when assets are unavailable. This will also eventually be used to trigger in CI when new assets should be built and uploaded to a well-known location. Updates tailscale/corp#13775 Signed-off-by: Will Norris --- .gitignore | 2 +- README.md | 11 + build_dist.sh | 3 + client/web/build/index.html | 28 + client/web/index.html | 21 +- client/web/src/hooks/node-data.ts | 13 +- client/web/src/index.tsx | 4 + client/web/web.css | 1380 ---------------------------- client/web/web.go | 76 +- client/web/web.html | 210 ----- cmd/tailscale/depaware.txt | 2 +- release/dist/synology/files/config | 2 +- 12 files changed, 97 insertions(+), 1655 deletions(-) create mode 100644 client/web/build/index.html delete mode 100644 client/web/web.css delete mode 100644 client/web/web.html diff --git a/.gitignore b/.gitignore index 72fcb3190..bea5627bc 100644 --- a/.gitignore +++ b/.gitignore @@ -38,7 +38,7 @@ cmd/tailscaled/tailscaled # Ignore web client node modules .vite/ client/web/node_modules -client/web/build +client/web/build/assets /gocross /dist diff --git a/README.md b/README.md index 0eae44624..ea96006a7 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,17 @@ If your distro has conventions that preclude the use of `build_dist.sh`, please do the equivalent of what it does in your distro's way, so that bug reports contain useful version information. +## Building the web client + +To include the embedded web client (accessed via the `tailscale web` command), +you'll need to build the client assets using: + +``` +./tool/yarn --cwd client/web build +``` + +Do this before building the `tailscale.com/cmd/tailscale` binary. + ## Bugs Please file any issues about this code or the hosted service on diff --git a/build_dist.sh b/build_dist.sh index 0c757c26d..e77d7315a 100755 --- a/build_dist.sh +++ b/build_dist.sh @@ -5,6 +5,9 @@ # information into the binaries, so that we can track down user # issues. # +# To include the embedded web client, build the web client assets +# before running this script. See README.md for details. +# # If you're packaging Tailscale for a distro, please consider using # this script, or executing equivalent commands in your # distro-specific build system. diff --git a/client/web/build/index.html b/client/web/build/index.html new file mode 100644 index 000000000..80838dc16 --- /dev/null +++ b/client/web/build/index.html @@ -0,0 +1,28 @@ + + + + Tailscale + + + + + + + + + + + + + diff --git a/client/web/index.html b/client/web/index.html index 062dfd185..264608bcd 100644 --- a/client/web/index.html +++ b/client/web/index.html @@ -8,22 +8,19 @@ - + diff --git a/client/web/src/hooks/node-data.ts b/client/web/src/hooks/node-data.ts index 21470c766..99c28421a 100644 --- a/client/web/src/hooks/node-data.ts +++ b/client/web/src/hooks/node-data.ts @@ -36,7 +36,16 @@ export default function useNodeData() { const [isPosting, setIsPosting] = useState(false) const fetchNodeData = useCallback(() => { - apiFetch("api/data") + const urlParams = new URLSearchParams(window.location.search) + const nextParams = new URLSearchParams() + const token = urlParams.get("SynoToken") + if (token) { + nextParams.set("SynoToken", token) + } + const search = nextParams.toString() + const url = `api/data${search ? `?${search}` : ""}` + + apiFetch(url) .then((r) => r.json()) .then((d) => setData(d)) .catch((error) => console.error(error)) @@ -75,7 +84,7 @@ export default function useNodeData() { nextParams.set("SynoToken", token) } const search = nextParams.toString() - const url = `/api/data${search ? `?${search}` : ""}` + const url = `api/data${search ? `?${search}` : ""}` var body, contentType: string diff --git a/client/web/src/index.tsx b/client/web/src/index.tsx index 3c2d0fc43..6840d2488 100644 --- a/client/web/src/index.tsx +++ b/client/web/src/index.tsx @@ -2,6 +2,10 @@ import React from "react" import { createRoot } from "react-dom/client" import App from "src/components/app" +declare var window: any +// This is used to determine if the react client is built. +window.Tailscale = true + const rootEl = document.createElement("div") rootEl.id = "app-root" rootEl.classList.add("relative", "z-0") diff --git a/client/web/web.css b/client/web/web.css deleted file mode 100644 index 5b9d9e0b6..000000000 --- a/client/web/web.css +++ /dev/null @@ -1,1380 +0,0 @@ -*, -::before, -::after { - box-sizing: border-box; - border-width: 0; - border-style: solid; - border-color: #e5e7eb; -} - -html { - font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, - "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, - "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; - line-height: 1.5; - -webkit-text-size-adjust: 100%; -} - -::selection { - background-color: rgba(97, 122, 255, 0.2); -} - -body { - margin: 0; - font-family: inherit; - line-height: inherit; -} - -hr { - height: 0; - color: inherit; - border-top-width: 1px; -} - -code, -kbd, -samp, -pre { - font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo, - monospace; - font-size: 1em; -} - -img, -svg, -video, -canvas, -audio, -iframe, -embed, -object { - display: block; - vertical-align: middle; -} - -img, -video { - max-width: 100%; - height: auto; -} - -button, -input, -optgroup, -select, -textarea { - font-family: inherit; - font-size: 100%; - line-height: 1.15; - margin: 0; -} - -button, -select { - text-transform: none; -} - -button, -[type="button"], -[type="submit"] { - -webkit-appearance: button; -} - -blockquote, -dl, -dd, -h1, -h2, -h3, -h4, -h5, -h6, -hr, -figure, -p, -pre { - margin: 0; -} - -button, -input, -optgroup, -select, -textarea { - padding: 0; - line-height: inherit; - color: inherit; -} - -button { - cursor: pointer; - background-color: transparent; - background-image: none; -} - -button:focus { - outline: 1px dotted; - outline: 5px auto -webkit-focus-ring-color; -} - -fieldset { - margin: 0; - padding: 0; -} - -ol, -ul { - list-style: none; - margin: 0; - padding: 0; -} - -textarea { - resize: vertical; -} - -input::-moz-placeholder, -textarea::-moz-placeholder { - opacity: 1; - color: #9ca3af; -} - -input:-ms-input-placeholder, -textarea:-ms-input-placeholder { - opacity: 1; - color: #9ca3af; -} - -input::placeholder, -textarea::placeholder { - opacity: 1; - color: #9ca3af; -} - -table { - border-collapse: collapse; -} - -h1, -h2, -h3, -h4, -h5, -h6 { - font-size: inherit; - font-weight: inherit; -} - -a { - color: inherit; - text-decoration: inherit; -} - -.container { - width: 100%; -} - -@media (min-width: 640px) { - .container { - max-width: 640px; - } -} - -@media (min-width: 768px) { - .container { - max-width: 768px; - } -} - -@media (min-width: 1024px) { - .container { - max-width: 1024px; - } -} - -@media (min-width: 1280px) { - .container { - max-width: 1280px; - } -} - -@media (min-width: 1536px) { - .container { - max-width: 1536px; - } -} - -.space-x-2 > :not([hidden]) ~ :not([hidden]) { - --tw-space-x-reverse: 0; - margin-right: calc(0.5rem * var(--tw-space-x-reverse)); - margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse))); -} - -.bg-white { - --tw-bg-opacity: 1; - background-color: rgba(255, 255, 255, var(--tw-bg-opacity)); -} - -.bg-gray-0 { - --tw-bg-opacity: 1; - background-color: rgba(250, 249, 248, var(--tw-bg-opacity)); -} - -.bg-gray-50 { - --tw-bg-opacity: 1; - background-color: rgba(249, 247, 246, var(--tw-bg-opacity)); -} - -.bg-orange-0 { - --tw-bg-opacity: 1; - background-color: rgba(255, 250, 238, var(--tw-bg-opacity)); -} - -.border-gray-200 { - --tw-border-opacity: 1; - border-color: rgba(238, 235, 234, var(--tw-border-opacity)); -} - -.border-gray-400 { - --tw-border-opacity: 1; - border-color: rgba(175, 172, 171, var(--tw-border-opacity)); -} - -.rounded-md { - border-radius: 0.375rem; -} - -.rounded-lg { - border-radius: 0.5rem; -} - -.rounded-full { - border-radius: 9999px; -} - -.border-dashed { - border-style: dashed; -} - -.border { - border-width: 1px; -} - -.flex { - display: flex; -} - -.table { - display: table; -} - -.items-center { - align-items: center; -} - -.justify-start { - justify-content: flex-start; -} - -.justify-end { - justify-content: flex-end; -} - -.justify-center { - justify-content: center; -} - -.justify-between { - justify-content: space-between; -} - -.justify-around { - justify-content: space-around; -} - -.justify-evenly { - justify-content: space-evenly; -} - -.flex-shrink-0 { - flex-shrink: 0; -} - -.font-medium { - font-weight: 500; -} - -.font-semibold { - font-weight: 600; -} - -.h-8 { - height: 2rem; -} - -.mx-auto { - margin-left: auto; - margin-right: auto; -} - -.mt-0 { - margin-top: 0px; -} - -.mr-0 { - margin-right: 0px; -} - -.mb-0 { - margin-bottom: 0px; -} - -.ml-0 { - margin-left: 0px; -} - -.mt-1 { - margin-top: 0.25rem; -} - -.mr-1 { - margin-right: 0.25rem; -} - -.mb-1 { - margin-bottom: 0.25rem; -} - -.ml-1 { - margin-left: 0.25rem; -} - -.mt-2 { - margin-top: 0.5rem; -} - -.mr-2 { - margin-right: 0.5rem; -} - -.mb-2 { - margin-bottom: 0.5rem; -} - -.ml-2 { - margin-left: 0.5rem; -} - -.mt-3 { - margin-top: 0.75rem; -} - -.mr-3 { - margin-right: 0.75rem; -} - -.mb-3 { - margin-bottom: 0.75rem; -} - -.ml-3 { - margin-left: 0.75rem; -} - -.mt-4 { - margin-top: 1rem; -} - -.mr-4 { - margin-right: 1rem; -} - -.mb-4 { - margin-bottom: 1rem; -} - -.ml-4 { - margin-left: 1rem; -} - -.mt-5 { - margin-top: 1.25rem; -} - -.mr-5 { - margin-right: 1.25rem; -} - -.mb-5 { - margin-bottom: 1.25rem; -} - -.ml-5 { - margin-left: 1.25rem; -} - -.mt-6 { - margin-top: 1.5rem; -} - -.mr-6 { - margin-right: 1.5rem; -} - -.mb-6 { - margin-bottom: 1.5rem; -} - -.ml-6 { - margin-left: 1.5rem; -} - -.mt-7 { - margin-top: 1.75rem; -} - -.mr-7 { - margin-right: 1.75rem; -} - -.mb-7 { - margin-bottom: 1.75rem; -} - -.ml-7 { - margin-left: 1.75rem; -} - -.mt-8 { - margin-top: 2rem; -} - -.mr-8 { - margin-right: 2rem; -} - -.mb-8 { - margin-bottom: 2rem; -} - -.ml-8 { - margin-left: 2rem; -} - -.mt-9 { - margin-top: 2.25rem; -} - -.mr-9 { - margin-right: 2.25rem; -} - -.mb-9 { - margin-bottom: 2.25rem; -} - -.ml-9 { - margin-left: 2.25rem; -} - -.mt-10 { - margin-top: 2.5rem; -} - -.mr-10 { - margin-right: 2.5rem; -} - -.mb-10 { - margin-bottom: 2.5rem; -} - -.ml-10 { - margin-left: 2.5rem; -} - -.mt-11 { - margin-top: 2.75rem; -} - -.mr-11 { - margin-right: 2.75rem; -} - -.mb-11 { - margin-bottom: 2.75rem; -} - -.ml-11 { - margin-left: 2.75rem; -} - -.mt-12 { - margin-top: 3rem; -} - -.mr-12 { - margin-right: 3rem; -} - -.mb-12 { - margin-bottom: 3rem; -} - -.ml-12 { - margin-left: 3rem; -} - -.mt-14 { - margin-top: 3.5rem; -} - -.mr-14 { - margin-right: 3.5rem; -} - -.mb-14 { - margin-bottom: 3.5rem; -} - -.ml-14 { - margin-left: 3.5rem; -} - -.mt-16 { - margin-top: 4rem; -} - -.mr-16 { - margin-right: 4rem; -} - -.mb-16 { - margin-bottom: 4rem; -} - -.ml-16 { - margin-left: 4rem; -} - -.mt-20 { - margin-top: 5rem; -} - -.mr-20 { - margin-right: 5rem; -} - -.mb-20 { - margin-bottom: 5rem; -} - -.ml-20 { - margin-left: 5rem; -} - -.mt-24 { - margin-top: 6rem; -} - -.mr-24 { - margin-right: 6rem; -} - -.mb-24 { - margin-bottom: 6rem; -} - -.ml-24 { - margin-left: 6rem; -} - -.mt-28 { - margin-top: 7rem; -} - -.mr-28 { - margin-right: 7rem; -} - -.mb-28 { - margin-bottom: 7rem; -} - -.ml-28 { - margin-left: 7rem; -} - -.mt-32 { - margin-top: 8rem; -} - -.mr-32 { - margin-right: 8rem; -} - -.mb-32 { - margin-bottom: 8rem; -} - -.ml-32 { - margin-left: 8rem; -} - -.mt-36 { - margin-top: 9rem; -} - -.mr-36 { - margin-right: 9rem; -} - -.mb-36 { - margin-bottom: 9rem; -} - -.ml-36 { - margin-left: 9rem; -} - -.mt-40 { - margin-top: 10rem; -} - -.mr-40 { - margin-right: 10rem; -} - -.mb-40 { - margin-bottom: 10rem; -} - -.ml-40 { - margin-left: 10rem; -} - -.mt-44 { - margin-top: 11rem; -} - -.mr-44 { - margin-right: 11rem; -} - -.mb-44 { - margin-bottom: 11rem; -} - -.ml-44 { - margin-left: 11rem; -} - -.mt-48 { - margin-top: 12rem; -} - -.mr-48 { - margin-right: 12rem; -} - -.mb-48 { - margin-bottom: 12rem; -} - -.ml-48 { - margin-left: 12rem; -} - -.mt-52 { - margin-top: 13rem; -} - -.mr-52 { - margin-right: 13rem; -} - -.mb-52 { - margin-bottom: 13rem; -} - -.ml-52 { - margin-left: 13rem; -} - -.mt-56 { - margin-top: 14rem; -} - -.mr-56 { - margin-right: 14rem; -} - -.mb-56 { - margin-bottom: 14rem; -} - -.ml-56 { - margin-left: 14rem; -} - -.mt-60 { - margin-top: 15rem; -} - -.mr-60 { - margin-right: 15rem; -} - -.mb-60 { - margin-bottom: 15rem; -} - -.ml-60 { - margin-left: 15rem; -} - -.mt-64 { - margin-top: 16rem; -} - -.mr-64 { - margin-right: 16rem; -} - -.mb-64 { - margin-bottom: 16rem; -} - -.ml-64 { - margin-left: 16rem; -} - -.mt-72 { - margin-top: 18rem; -} - -.mr-72 { - margin-right: 18rem; -} - -.mb-72 { - margin-bottom: 18rem; -} - -.ml-72 { - margin-left: 18rem; -} - -.mt-80 { - margin-top: 20rem; -} - -.mr-80 { - margin-right: 20rem; -} - -.mb-80 { - margin-bottom: 20rem; -} - -.ml-80 { - margin-left: 20rem; -} - -.mt-96 { - margin-top: 24rem; -} - -.mr-96 { - margin-right: 24rem; -} - -.mb-96 { - margin-bottom: 24rem; -} - -.ml-96 { - margin-left: 24rem; -} - -.max-w-lg { - max-width: 32rem; -} - -.max-w-xl { - max-width: 36rem; -} - -.overflow-hidden { - overflow: hidden; -} - -.p-2 { - padding: 0.5rem; -} - -.py-0 { - padding-top: 0px; - padding-bottom: 0px; -} - -.px-0 { - padding-left: 0px; - padding-right: 0px; -} - -.py-1 { - padding-top: 0.25rem; - padding-bottom: 0.25rem; -} - -.px-1 { - padding-left: 0.25rem; - padding-right: 0.25rem; -} - -.py-2 { - padding-top: 0.5rem; - padding-bottom: 0.5rem; -} - -.px-2 { - padding-left: 0.5rem; - padding-right: 0.5rem; -} - -.py-3 { - padding-top: 0.75rem; - padding-bottom: 0.75rem; -} - -.px-3 { - padding-left: 0.75rem; - padding-right: 0.75rem; -} - -.py-4 { - padding-top: 1rem; - padding-bottom: 1rem; -} - -.px-4 { - padding-left: 1rem; - padding-right: 1rem; -} - -.py-5 { - padding-top: 1.25rem; - padding-bottom: 1.25rem; -} - -.px-5 { - padding-left: 1.25rem; - padding-right: 1.25rem; -} - -.py-6 { - padding-top: 1.5rem; - padding-bottom: 1.5rem; -} - -.px-6 { - padding-left: 1.5rem; - padding-right: 1.5rem; -} - -.py-7 { - padding-top: 1.75rem; - padding-bottom: 1.75rem; -} - -.px-7 { - padding-left: 1.75rem; - padding-right: 1.75rem; -} - -.py-8 { - padding-top: 2rem; - padding-bottom: 2rem; -} - -.px-8 { - padding-left: 2rem; - padding-right: 2rem; -} - -.py-9 { - padding-top: 2.25rem; - padding-bottom: 2.25rem; -} - -.px-9 { - padding-left: 2.25rem; - padding-right: 2.25rem; -} - -.py-10 { - padding-top: 2.5rem; - padding-bottom: 2.5rem; -} - -.px-10 { - padding-left: 2.5rem; - padding-right: 2.5rem; -} - -.py-11 { - padding-top: 2.75rem; - padding-bottom: 2.75rem; -} - -.px-11 { - padding-left: 2.75rem; - padding-right: 2.75rem; -} - -.py-12 { - padding-top: 3rem; - padding-bottom: 3rem; -} - -.px-12 { - padding-left: 3rem; - padding-right: 3rem; -} - -.py-14 { - padding-top: 3.5rem; - padding-bottom: 3.5rem; -} - -.px-14 { - padding-left: 3.5rem; - padding-right: 3.5rem; -} - -.py-16 { - padding-top: 4rem; - padding-bottom: 4rem; -} - -.px-16 { - padding-left: 4rem; - padding-right: 4rem; -} - -.py-20 { - padding-top: 5rem; - padding-bottom: 5rem; -} - -.px-20 { - padding-left: 5rem; - padding-right: 5rem; -} - -.py-24 { - padding-top: 6rem; - padding-bottom: 6rem; -} - -.px-24 { - padding-left: 6rem; - padding-right: 6rem; -} - -.py-28 { - padding-top: 7rem; - padding-bottom: 7rem; -} - -.px-28 { - padding-left: 7rem; - padding-right: 7rem; -} - -.py-32 { - padding-top: 8rem; - padding-bottom: 8rem; -} - -.px-32 { - padding-left: 8rem; - padding-right: 8rem; -} - -.py-36 { - padding-top: 9rem; - padding-bottom: 9rem; -} - -.px-36 { - padding-left: 9rem; - padding-right: 9rem; -} - -.pr-3 { - padding-right: 0.75rem; -} - -.pl-3 { - padding-left: 0.75rem; -} - -.pointer-events-none { - pointer-events: none; -} - -.relative { - position: relative; -} - -* { - --tw-shadow: 0 0 #0000; -} - -.shadow-2xl { - --tw-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), - var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); -} - -* { - --tw-ring-inset: var(--tw-empty, /*!*/ /*!*/); - --tw-ring-offset-width: 0px; - --tw-ring-offset-color: #fff; - --tw-ring-color: rgba(75, 112, 204, 0.5); - --tw-ring-offset-shadow: 0 0 #0000; - --tw-ring-shadow: 0 0 #0000; -} - -.text-xs { - font-size: 0.75rem; - line-height: 1rem; -} - -.text-sm { - font-size: 0.875rem; - line-height: 1.25rem; -} - -.text-base { - font-size: 1rem; - line-height: 1.5rem; -} - -.text-lg { - font-size: 1.125rem; - line-height: 1.75rem; -} - -.text-xl { - font-size: 1.25rem; - line-height: 1.75rem; -} - -.text-2xl { - font-size: 1.5rem; - line-height: 2rem; -} - -.text-3xl { - font-size: 1.875rem; - line-height: 2.25rem; -} - -.text-4xl { - font-size: 2.25rem; - line-height: 2.5rem; -} - -.text-left { - text-align: left; -} - -.text-center { - text-align: center; -} - -.text-right { - text-align: right; -} - -.text-justify { - text-align: justify; -} - -.text-gray-500 { - --tw-text-opacity: 1; - color: rgba(112, 110, 109, var(--tw-text-opacity)); -} - -.text-gray-600 { - --tw-text-opacity: 1; - color: rgba(68, 67, 66, var(--tw-text-opacity)); -} - -.text-gray-700 { - --tw-text-opacity: 1; - color: rgba(46, 45, 45, var(--tw-text-opacity)); -} - -.text-gray-800 { - --tw-text-opacity: 1; - color: rgba(35, 34, 34, var(--tw-text-opacity)); -} - -.text-orange-800 { - --tw-text-opacity: 1; - color: rgba(66, 14, 17, var(--tw-text-opacity)); -} - -.leading-3 { - line-height: 0.75rem; -} - -.leading-4 { - line-height: 1rem; -} - -.leading-5 { - line-height: 1.25rem; -} - -.leading-6 { - line-height: 1.5rem; -} - -.leading-7 { - line-height: 1.75rem; -} - -.leading-8 { - line-height: 2rem; -} - -.leading-9 { - line-height: 2.25rem; -} - -.leading-10 { - line-height: 2.5rem; -} - -.leading-none { - line-height: 1; -} - -.leading-tight { - line-height: 1.25; -} - -.leading-snug { - line-height: 1.375; -} - -.leading-normal { - line-height: 1.5; -} - -.leading-relaxed { - line-height: 1.625; -} - -.leading-loose { - line-height: 2; -} - -.truncate { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.w-8 { - width: 2rem; -} - -.w-1\/2 { - width: 50%; -} - -.w-2\/3 { - width: 66.666667%; -} - -.w-full { - width: 100%; -} - -.hover\:text-gray-0:hover { - --tw-text-opacity: 1; - color: rgba(250, 249, 248, var(--tw-text-opacity)); -} - -.hover\:text-gray-50:hover { - --tw-text-opacity: 1; - color: rgba(249, 247, 246, var(--tw-text-opacity)); -} - -.hover\:text-gray-100:hover { - --tw-text-opacity: 1; - color: rgba(247, 245, 244, var(--tw-text-opacity)); -} - -.hover\:text-gray-200:hover { - --tw-text-opacity: 1; - color: rgba(238, 235, 234, var(--tw-text-opacity)); -} - -.hover\:text-gray-300:hover { - --tw-text-opacity: 1; - color: rgba(218, 214, 213, var(--tw-text-opacity)); -} - -.hover\:text-gray-400:hover { - --tw-text-opacity: 1; - color: rgba(175, 172, 171, var(--tw-text-opacity)); -} - -.hover\:text-gray-500:hover { - --tw-text-opacity: 1; - color: rgba(112, 110, 109, var(--tw-text-opacity)); -} - -.hover\:text-gray-600:hover { - --tw-text-opacity: 1; - color: rgba(68, 67, 66, var(--tw-text-opacity)); -} - -.hover\:text-gray-700:hover { - --tw-text-opacity: 1; - color: rgba(46, 45, 45, var(--tw-text-opacity)); -} - -.hover\:text-gray-800:hover { - --tw-text-opacity: 1; - color: rgba(35, 34, 34, var(--tw-text-opacity)); -} - -.hover\:text-gray-900:hover { - --tw-text-opacity: 1; - color: rgba(31, 30, 30, var(--tw-text-opacity)); -} - -/** - * Non-Tailwind styles begin here. - */ - -html { - letter-spacing: -0.015em; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -.link { - --text-opacity: 1; - color: #4b70cc; - color: rgba(75, 112, 204, var(--text-opacity)); -} - -.link:hover, -.link:active { - --text-opacity: 1; - color: #19224a; - color: rgba(25, 34, 74, var(--text-opacity)); -} - -.link-underline { - text-decoration: underline; -} - -.link-underline:hover, -.link-underline:active { - text-decoration: none; -} - -.link-muted { - /* same as text-gray-500 */ - --tw-text-opacity: 1; - color: rgba(112, 110, 109, var(--tw-text-opacity)); -} - -.link-muted:hover, -.link-muted:active { - /* same as text-gray-500 */ - --tw-text-opacity: 1; - color: rgba(68, 67, 66, var(--tw-text-opacity)); -} - -.button { - font-weight: 500; - padding-top: 0.45rem; - padding-bottom: 0.45rem; - padding-left: 1rem; - padding-right: 1rem; - border-radius: 0.375rem; - border-width: 1px; - border-color: transparent; - transition-property: background-color, border-color, color, box-shadow; - transition-duration: 120ms; - box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); - min-width: 80px; -} - -.button:focus { - outline: 0; - box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5); -} - -.button:disabled { - cursor: not-allowed; - -webkit-user-select: none; - -ms-user-select: none; - user-select: none; -} - -.button-blue { - --bg-opacity: 1; - background-color: #4b70cc; - background-color: rgba(75, 112, 204, var(--bg-opacity)); - --border-opacity: 1; - border-color: #4b70cc; - border-color: rgba(75, 112, 204, var(--border-opacity)); - --text-opacity: 1; - color: #fff; - color: rgba(255, 255, 255, var(--text-opacity)); -} - -.button-blue:enabled:hover { - --bg-opacity: 1; - background-color: #3f5db3; - background-color: rgba(63, 93, 179, var(--bg-opacity)); - --border-opacity: 1; - border-color: #3f5db3; - border-color: rgba(63, 93, 179, var(--border-opacity)); -} - -.button-blue:disabled { - --text-opacity: 1; - color: #cedefd; - color: rgba(206, 222, 253, var(--text-opacity)); - --bg-opacity: 1; - background-color: #6c94ec; - background-color: rgba(108, 148, 236, var(--bg-opacity)); - --border-opacity: 1; - border-color: #6c94ec; - border-color: rgba(108, 148, 236, var(--border-opacity)); -} - -.button-red { - background-color: #d04841; - border-color: #d04841; - color: #fff; -} - -.button-red:enabled:hover { - background-color: #b22d30; - border-color: #b22d30; -} diff --git a/client/web/web.go b/client/web/web.go index 40b5c41b3..c1daa9a57 100644 --- a/client/web/web.go +++ b/client/web/web.go @@ -5,14 +5,13 @@ package web import ( - "bytes" "context" "crypto/rand" "embed" "encoding/json" "fmt" - "html/template" "io" + "io/fs" "log" "net/http" "net/http/httputil" @@ -31,6 +30,7 @@ import ( "tailscale.com/net/netutil" "tailscale.com/tailcfg" "tailscale.com/util/httpm" + "tailscale.com/util/must" "tailscale.com/version/distro" ) @@ -38,15 +38,16 @@ import ( // Because we assign this to the blank identifier, it does not actually embed the files. // However, this does cause `go mod vendor` to include the files when vendoring the package. // External packages that use the web client can `go mod vendor`, run `yarn build` to -// build the assets, then those asset bundles will be able to be embedded. +// build the assets, then those asset bundles will be embedded. // //go:embed yarn.lock index.html *.js *.json src/* var _ embed.FS -//go:embed web.html web.css +//go:embed build/* var embeddedFS embed.FS -var tmpls *template.Template +// staticfiles serves static files from the build directory. +var staticfiles http.Handler // Server is the backend server for a Tailscale web client. type Server struct { @@ -103,14 +104,6 @@ func NewServer(ctx context.Context, opts ServerOpts) (s *Server, cleanup func()) if s.devMode { cleanup = s.startDevServer() s.addProxyToDevServer() - - // Create handler for "/api" requests with CSRF protection. - // We don't require secure cookies, since the web client is regularly used - // on network appliances that are served on local non-https URLs. - // The client is secured by limiting the interface it listens on, - // or by authenticating requests before they reach the web client. - csrfProtect := csrf.Protect(s.csrfKey(), csrf.Secure(false)) - s.apiHandler = csrfProtect(http.HandlerFunc(s.serveAPI)) } var wg sync.WaitGroup @@ -121,12 +114,21 @@ func NewServer(ctx context.Context, opts ServerOpts) (s *Server, cleanup func()) go s.watchSelf(ctx) }() + // Create handler for "/api" requests with CSRF protection. + // We don't require secure cookies, since the web client is regularly used + // on network appliances that are served on local non-https URLs. + // The client is secured by limiting the interface it listens on, + // or by authenticating requests before they reach the web client. + csrfProtect := csrf.Protect(s.csrfKey(), csrf.Secure(false)) + s.apiHandler = csrfProtect(http.HandlerFunc(s.serveAPI)) + s.lc.IncrementCounter(context.Background(), "web_client_initialization", 1) return s, cleanup } func init() { - tmpls = template.Must(template.New("").ParseFS(embeddedFS, "*")) + buildFiles := must.Get(fs.Sub(embeddedFS, "build")) + staticfiles = http.FileServer(http.FS(buildFiles)) } // watchSelf watches the IPN notification bus to refresh @@ -222,30 +224,23 @@ func authorize(w http.ResponseWriter, r *http.Request) (handled bool) { } func (s *Server) serve(w http.ResponseWriter, r *http.Request) { - // Authenticate and authorize the request for platforms that support it. - // Return if the request was processed. - if authorize(w, r) { + switch { + case authorize(w, r): + // Authenticate and authorize the request for platforms that support it. + // Return if the request was processed. return - } - - if s.devMode { - if strings.HasPrefix(r.URL.Path, "/api/") { - // Pass through to other handlers via CSRF protection. - s.apiHandler.ServeHTTP(w, r) - return - } - // When in dev mode, proxy to the Vite dev server. - s.devProxy.ServeHTTP(w, r) + case strings.HasPrefix(r.URL.Path, "/api/"): + // Pass API requests through to the API handler. + s.apiHandler.ServeHTTP(w, r) return - } - - switch { - case r.Method == "POST": - s.servePostNodeUpdate(w, r) + case s.devMode: + // When in dev mode, proxy non-api requests to the Vite dev server. + s.devProxy.ServeHTTP(w, r) return default: + // Otherwise, serve static files from the embedded filesystem. s.lc.IncrementCounter(context.Background(), "web_client_page_load", 1) - s.serveGetNodeData(w, r) + staticfiles.ServeHTTP(w, r) return } } @@ -329,20 +324,6 @@ func (s *Server) getNodeData(ctx context.Context) (*nodeData, error) { return data, nil } -func (s *Server) serveGetNodeData(w http.ResponseWriter, r *http.Request) { - data, err := s.getNodeData(r.Context()) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - buf := new(bytes.Buffer) - if err := tmpls.ExecuteTemplate(buf, "web.html", data); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - w.Write(buf.Bytes()) -} - func (s *Server) serveGetNodeDataJSON(w http.ResponseWriter, r *http.Request) { data, err := s.getNodeData(r.Context()) if err != nil { @@ -354,7 +335,6 @@ func (s *Server) serveGetNodeDataJSON(w http.ResponseWriter, r *http.Request) { return } w.Header().Set("Content-Type", "application/json") - return } type nodeUpdate struct { diff --git a/client/web/web.html b/client/web/web.html deleted file mode 100644 index b990bdd77..000000000 --- a/client/web/web.html +++ /dev/null @@ -1,210 +0,0 @@ - - - - - - - - Tailscale - - - - -
-
- - - - - - - - - - - -
- {{ with .Profile }} -
-

{{.LoginName}}

- -
- {{ end }} -
- {{ with .Profile.ProfilePicURL }} -
- {{ else }} -
- {{ end }} -
-
-
- {{ if .IP }} -
-
- - - - - - -
-

{{.DeviceName}}

-
-
-
{{.IP}}
-
-

- Debug info: Tailscale {{ .IPNVersion }}, tun={{.TUNMode}}{{ if .IsSynology }}, DSM{{ .DSMVersion}} - {{if not .TUNMode}} - (outgoing access not configured) - {{end}} - {{end}} -

- {{ end }} - {{ if or (eq .Status "NeedsLogin") (eq .Status "NoState") }} - {{ if .IP }} -
-

Your device's key has expired. Reauthenticate this device by logging in again, or learn more.

-
- - - - {{ else }} -
-

Log in

-

Get started by logging in to your Tailscale network. Or, learn more at tailscale.com.

-
- - - - {{ end }} - {{ else if eq .Status "NeedsMachineAuth" }} -
- This device is authorized, but needs approval from a network admin before it can connect to the network. -
- {{ else }} -
-

You are connected! Access this device over Tailscale using the device name or IP address above.

-
- - {{ end }} -
- - - - - diff --git a/cmd/tailscale/depaware.txt b/cmd/tailscale/depaware.txt index e8bbc5f5c..88ab152bc 100644 --- a/cmd/tailscale/depaware.txt +++ b/cmd/tailscale/depaware.txt @@ -251,7 +251,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep hash/crc32 from compress/gzip+ hash/maphash from go4.org/mem html from tailscale.com/ipn/ipnstate+ - html/template from tailscale.com/client/web+ + html/template from github.com/gorilla/csrf image from github.com/skip2/go-qrcode+ image/color from github.com/skip2/go-qrcode+ image/png from github.com/skip2/go-qrcode diff --git a/release/dist/synology/files/config b/release/dist/synology/files/config index bd8e567ae..4dbc48dfb 100644 --- a/release/dist/synology/files/config +++ b/release/dist/synology/files/config @@ -4,7 +4,7 @@ "type": "url", "title": "Tailscale", "icon": "PACKAGE_ICON_256.PNG", - "url": "webman/3rdparty/Tailscale/", + "url": "webman/3rdparty/Tailscale/index.cgi/", "urlTarget": "_syno_tailscale" } }