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 @@ "tailscale.com/net/netutil" "tailscale.com/tailcfg" "tailscale.com/util/httpm" + "tailscale.com/util/must" "tailscale.com/version/distro" ) @@ -38,15 +38,16 @@ // 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. + case strings.HasPrefix(r.URL.Path, "/api/"): + // Pass API requests through to the API handler. + s.apiHandler.ServeHTTP(w, r) + return + case s.devMode: + // When in dev mode, proxy non-api requests to the Vite dev server. s.devProxy.ServeHTTP(w, r) return - } - - switch { - case r.Method == "POST": - s.servePostNodeUpdate(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" } }