mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 04:55:31 +00:00
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 <will@tailscale.com>
This commit is contained in:
parent
be5bd1e619
commit
d74c771fda
2
.gitignore
vendored
2
.gitignore
vendored
@ -38,7 +38,7 @@ cmd/tailscaled/tailscaled
|
|||||||
# Ignore web client node modules
|
# Ignore web client node modules
|
||||||
.vite/
|
.vite/
|
||||||
client/web/node_modules
|
client/web/node_modules
|
||||||
client/web/build
|
client/web/build/assets
|
||||||
|
|
||||||
/gocross
|
/gocross
|
||||||
/dist
|
/dist
|
||||||
|
11
README.md
11
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
|
`build_dist.sh`, please do the equivalent of what it does in your
|
||||||
distro's way, so that bug reports contain useful version information.
|
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
|
## Bugs
|
||||||
|
|
||||||
Please file any issues about this code or the hosted service on
|
Please file any issues about this code or the hosted service on
|
||||||
|
@ -5,6 +5,9 @@
|
|||||||
# information into the binaries, so that we can track down user
|
# information into the binaries, so that we can track down user
|
||||||
# issues.
|
# 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
|
# If you're packaging Tailscale for a distro, please consider using
|
||||||
# this script, or executing equivalent commands in your
|
# this script, or executing equivalent commands in your
|
||||||
# distro-specific build system.
|
# distro-specific build system.
|
||||||
|
28
client/web/build/index.html
Normal file
28
client/web/build/index.html
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html class="bg-gray-50">
|
||||||
|
<head>
|
||||||
|
<title>Tailscale</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="shortcut icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAAHdElNRQflAx4QGA4EvmzDAAAA30lEQVRIx2NgGAWMCKa8JKM4A8Ovt88ekyLCDGOoyDBJMjExMbFy8zF8/EKsCAMDE8yAPyIwFps48SJIBpAL4AZwvoSx/r0lXgQpDN58EWL5x/7/H+vL20+JFxluQKVe5b3Ke5V+0kQQCamfoYKBg4GDwUKI8d0BYkWQkrLKewYBKPPDHUFiRaiZkBgmwhj/F5IgggyUJ6i8V3mv0kCayDAAeEsklXqGAgYGhgV3CnGrwVciYSYk0kokhgS44/JxqqFpiYSZbEgskd4dEBRk1GD4wdB5twKXmlHAwMDAAACdEZau06NQUwAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMC0wNy0xNVQxNTo1Mzo0MCswMDowMCVXsDIAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjAtMDctMTVUMTU6NTM6NDArMDA6MDBUCgiOAAAAAElFTkSuQmCC" />
|
||||||
|
|
||||||
|
<script type="module" crossorigin src="./assets/index-f8beba53.js"></script>
|
||||||
|
<link rel="stylesheet" href="./assets/index-8612dca6.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
<p class="mb-2">You need to enable Javascript to access the Tailscale web client.</p>
|
||||||
|
<p>If you need any help, feel free to <a href="mailto:support+webclient@tailscale.com" class="link">contact us</a>.</p>
|
||||||
|
</noscript>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.addEventListener("load", () => {
|
||||||
|
if (!window.Tailscale) {
|
||||||
|
const rootEl = document.createElement("p")
|
||||||
|
rootEl.innerHTML = 'Tailscale was built without the web client. See <a href="https://github.com/tailscale/tailscale#building-the-web-client">Building the web client</a> for more information.'
|
||||||
|
document.body.append(rootEl)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -8,22 +8,19 @@
|
|||||||
<link rel="stylesheet" type="text/css" href="/src/index.css" />
|
<link rel="stylesheet" type="text/css" href="/src/index.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="min-h-screen py-10 flex justify-center items-center" style="display: none">
|
|
||||||
<div class="max-w-md">
|
|
||||||
<h3 class="font-semibold text-lg mb-4">Your web browser is unsupported.</h3>
|
|
||||||
<p class="mb-2">
|
|
||||||
Update to a modern browser to access the Tailscale web client. You can use
|
|
||||||
<a class="link" href="https://www.mozilla.org/en-US/firefox/new/" target="_blank">Firefox</a>,
|
|
||||||
<a class="link" href="https://www.microsoft.com/en-us/edge" target="_blank">Edge</a>,
|
|
||||||
<a class="link" href="https://www.apple.com/safari/" target="_blank">Safari</a>,
|
|
||||||
or <a class="link" href="https://www.google.com/chrome/" target="_blank">Chrome</a>.</p>
|
|
||||||
<p>If you need any help, feel free to <a href="mailto:support+webclient@tailscale.com" class="link">contact us</a></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<noscript>
|
<noscript>
|
||||||
<p class="mb-2">You need to enable Javascript to access the Tailscale web client.</p>
|
<p class="mb-2">You need to enable Javascript to access the Tailscale web client.</p>
|
||||||
<p>If you need any help, feel free to <a href="mailto:support+webclient@tailscale.com" class="link">contact us</a>.</p>
|
<p>If you need any help, feel free to <a href="mailto:support+webclient@tailscale.com" class="link">contact us</a>.</p>
|
||||||
</noscript>
|
</noscript>
|
||||||
<script type="module" src="/src/index.tsx"></script>
|
<script type="module" src="/src/index.tsx"></script>
|
||||||
|
<script>
|
||||||
|
window.addEventListener("load", () => {
|
||||||
|
if (!window.Tailscale) {
|
||||||
|
const rootEl = document.createElement("p")
|
||||||
|
rootEl.innerHTML = 'Tailscale was built without the web client. See <a href="https://github.com/tailscale/tailscale#building-the-web-client">Building the web client</a> for more information.'
|
||||||
|
document.body.append(rootEl)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -36,7 +36,16 @@ export default function useNodeData() {
|
|||||||
const [isPosting, setIsPosting] = useState<boolean>(false)
|
const [isPosting, setIsPosting] = useState<boolean>(false)
|
||||||
|
|
||||||
const fetchNodeData = useCallback(() => {
|
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((r) => r.json())
|
||||||
.then((d) => setData(d))
|
.then((d) => setData(d))
|
||||||
.catch((error) => console.error(error))
|
.catch((error) => console.error(error))
|
||||||
@ -75,7 +84,7 @@ export default function useNodeData() {
|
|||||||
nextParams.set("SynoToken", token)
|
nextParams.set("SynoToken", token)
|
||||||
}
|
}
|
||||||
const search = nextParams.toString()
|
const search = nextParams.toString()
|
||||||
const url = `/api/data${search ? `?${search}` : ""}`
|
const url = `api/data${search ? `?${search}` : ""}`
|
||||||
|
|
||||||
var body, contentType: string
|
var body, contentType: string
|
||||||
|
|
||||||
|
@ -2,6 +2,10 @@ import React from "react"
|
|||||||
import { createRoot } from "react-dom/client"
|
import { createRoot } from "react-dom/client"
|
||||||
import App from "src/components/app"
|
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")
|
const rootEl = document.createElement("div")
|
||||||
rootEl.id = "app-root"
|
rootEl.id = "app-root"
|
||||||
rootEl.classList.add("relative", "z-0")
|
rootEl.classList.add("relative", "z-0")
|
||||||
|
1380
client/web/web.css
1380
client/web/web.css
File diff suppressed because it is too large
Load Diff
@ -5,14 +5,13 @@
|
|||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"embed"
|
"embed"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
|
||||||
"io"
|
"io"
|
||||||
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
@ -31,6 +30,7 @@
|
|||||||
"tailscale.com/net/netutil"
|
"tailscale.com/net/netutil"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/util/httpm"
|
"tailscale.com/util/httpm"
|
||||||
|
"tailscale.com/util/must"
|
||||||
"tailscale.com/version/distro"
|
"tailscale.com/version/distro"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -38,15 +38,16 @@
|
|||||||
// Because we assign this to the blank identifier, it does not actually embed the files.
|
// 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.
|
// 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
|
// 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/*
|
//go:embed yarn.lock index.html *.js *.json src/*
|
||||||
var _ embed.FS
|
var _ embed.FS
|
||||||
|
|
||||||
//go:embed web.html web.css
|
//go:embed build/*
|
||||||
var embeddedFS embed.FS
|
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.
|
// Server is the backend server for a Tailscale web client.
|
||||||
type Server struct {
|
type Server struct {
|
||||||
@ -103,14 +104,6 @@ func NewServer(ctx context.Context, opts ServerOpts) (s *Server, cleanup func())
|
|||||||
if s.devMode {
|
if s.devMode {
|
||||||
cleanup = s.startDevServer()
|
cleanup = s.startDevServer()
|
||||||
s.addProxyToDevServer()
|
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
|
var wg sync.WaitGroup
|
||||||
@ -121,12 +114,21 @@ func NewServer(ctx context.Context, opts ServerOpts) (s *Server, cleanup func())
|
|||||||
go s.watchSelf(ctx)
|
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)
|
s.lc.IncrementCounter(context.Background(), "web_client_initialization", 1)
|
||||||
return s, cleanup
|
return s, cleanup
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
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
|
// 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) {
|
func (s *Server) serve(w http.ResponseWriter, r *http.Request) {
|
||||||
// Authenticate and authorize the request for platforms that support it.
|
switch {
|
||||||
// Return if the request was processed.
|
case authorize(w, r):
|
||||||
if authorize(w, r) {
|
// Authenticate and authorize the request for platforms that support it.
|
||||||
|
// Return if the request was processed.
|
||||||
return
|
return
|
||||||
}
|
case strings.HasPrefix(r.URL.Path, "/api/"):
|
||||||
|
// Pass API requests through to the API handler.
|
||||||
if s.devMode {
|
s.apiHandler.ServeHTTP(w, r)
|
||||||
if strings.HasPrefix(r.URL.Path, "/api/") {
|
return
|
||||||
// Pass through to other handlers via CSRF protection.
|
case s.devMode:
|
||||||
s.apiHandler.ServeHTTP(w, r)
|
// When in dev mode, proxy non-api requests to the Vite dev server.
|
||||||
return
|
|
||||||
}
|
|
||||||
// When in dev mode, proxy to the Vite dev server.
|
|
||||||
s.devProxy.ServeHTTP(w, r)
|
s.devProxy.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case r.Method == "POST":
|
|
||||||
s.servePostNodeUpdate(w, r)
|
|
||||||
return
|
|
||||||
default:
|
default:
|
||||||
|
// Otherwise, serve static files from the embedded filesystem.
|
||||||
s.lc.IncrementCounter(context.Background(), "web_client_page_load", 1)
|
s.lc.IncrementCounter(context.Background(), "web_client_page_load", 1)
|
||||||
s.serveGetNodeData(w, r)
|
staticfiles.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -329,20 +324,6 @@ func (s *Server) getNodeData(ctx context.Context) (*nodeData, error) {
|
|||||||
return data, nil
|
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) {
|
func (s *Server) serveGetNodeDataJSON(w http.ResponseWriter, r *http.Request) {
|
||||||
data, err := s.getNodeData(r.Context())
|
data, err := s.getNodeData(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -354,7 +335,6 @@ func (s *Server) serveGetNodeDataJSON(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type nodeUpdate struct {
|
type nodeUpdate struct {
|
||||||
|
@ -1,210 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html class="bg-gray-50">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<link rel="shortcut icon"
|
|
||||||
href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAAHdElNRQflAx4QGA4EvmzDAAAA30lEQVRIx2NgGAWMCKa8JKM4A8Ovt88ekyLCDGOoyDBJMjExMbFy8zF8/EKsCAMDE8yAPyIwFps48SJIBpAL4AZwvoSx/r0lXgQpDN58EWL5x/7/H+vL20+JFxluQKVe5b3Ke5V+0kQQCamfoYKBg4GDwUKI8d0BYkWQkrLKewYBKPPDHUFiRaiZkBgmwhj/F5IgggyUJ6i8V3mv0kCayDAAeEsklXqGAgYGhgV3CnGrwVciYSYk0kokhgS44/JxqqFpiYSZbEgskd4dEBRk1GD4wdB5twKXmlHAwMDAAACdEZau06NQUwAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMC0wNy0xNVQxNTo1Mzo0MCswMDowMCVXsDIAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjAtMDctMTVUMTU6NTM6NDArMDA6MDBUCgiOAAAAAElFTkSuQmCC" />
|
|
||||||
<title>Tailscale</title>
|
|
||||||
<style>{{template "web.css"}}</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body class="py-14">
|
|
||||||
<main class="container max-w-lg mx-auto mb-8 py-6 px-8 bg-white rounded-md shadow-2xl" style="width: 95%">
|
|
||||||
<header class="flex justify-between items-center min-width-0 py-2 mb-8">
|
|
||||||
<svg width="26" height="26" viewBox="0 0 23 23" title="Tailscale" fill="none" xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="flex-shrink-0 mr-4">
|
|
||||||
<circle opacity="0.2" cx="3.4" cy="3.25" r="2.7" fill="currentColor"></circle>
|
|
||||||
<circle cx="3.4" cy="11.3" r="2.7" fill="currentColor"></circle>
|
|
||||||
<circle opacity="0.2" cx="3.4" cy="19.5" r="2.7" fill="currentColor"></circle>
|
|
||||||
<circle cx="11.5" cy="11.3" r="2.7" fill="currentColor"></circle>
|
|
||||||
<circle cx="11.5" cy="19.5" r="2.7" fill="currentColor"></circle>
|
|
||||||
<circle opacity="0.2" cx="11.5" cy="3.25" r="2.7" fill="currentColor"></circle>
|
|
||||||
<circle opacity="0.2" cx="19.5" cy="3.25" r="2.7" fill="currentColor"></circle>
|
|
||||||
<circle cx="19.5" cy="11.3" r="2.7" fill="currentColor"></circle>
|
|
||||||
<circle opacity="0.2" cx="19.5" cy="19.5" r="2.7" fill="currentColor"></circle>
|
|
||||||
</svg>
|
|
||||||
<div class="flex items-center justify-end space-x-2 w-2/3">
|
|
||||||
{{ with .Profile }}
|
|
||||||
<div class="text-right w-full leading-4">
|
|
||||||
<h4 class="truncate leading-normal">{{.LoginName}}</h4>
|
|
||||||
<div class="text-xs text-gray-500 text-right">
|
|
||||||
<a href="#" class="hover:text-gray-700 js-loginButton">Switch account</a> | <a href="#"
|
|
||||||
class="hover:text-gray-700 js-loginButton">Reauthenticate</a> | <a href="#"
|
|
||||||
class="hover:text-gray-700 js-logoutButton">Logout</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
<div class="relative flex-shrink-0 w-8 h-8 rounded-full overflow-hidden">
|
|
||||||
{{ with .Profile.ProfilePicURL }}
|
|
||||||
<div class="w-8 h-8 flex pointer-events-none rounded-full bg-gray-200"
|
|
||||||
style="background-image: url('{{.}}'); background-size: cover;"></div>
|
|
||||||
{{ else }}
|
|
||||||
<div class="w-8 h-8 flex pointer-events-none rounded-full border border-gray-400 border-dashed"></div>
|
|
||||||
{{ end }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
{{ if .IP }}
|
|
||||||
<div
|
|
||||||
class="border border-gray-200 bg-gray-0 rounded-md p-2 pl-3 pr-3 width-full flex items-center justify-between">
|
|
||||||
<div class="flex items-center min-width-0">
|
|
||||||
<svg class="flex-shrink-0 text-gray-600 mr-3 ml-1" xmlns="http://www.w3.org/2000/svg" width="20" height="20"
|
|
||||||
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
|
||||||
stroke-linejoin="round">
|
|
||||||
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect>
|
|
||||||
<rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect>
|
|
||||||
<line x1="6" y1="6" x2="6.01" y2="6"></line>
|
|
||||||
<line x1="6" y1="18" x2="6.01" y2="18"></line>
|
|
||||||
</svg>
|
|
||||||
<div>
|
|
||||||
<h4 class="font-semibold truncate mr-2">{{.DeviceName}}</h4>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h5>{{.IP}}</h5>
|
|
||||||
</div>
|
|
||||||
<p class="mt-1 ml-1 mb-6 text-xs text-gray-600">
|
|
||||||
Debug info: Tailscale {{ .IPNVersion }}, tun={{.TUNMode}}{{ if .IsSynology }}, DSM{{ .DSMVersion}}
|
|
||||||
{{if not .TUNMode}}
|
|
||||||
(<a href="https://tailscale.com/kb/1152/synology-outbound/" class="link-underline text-gray-600" target="_blank"
|
|
||||||
aria-label="Configure outbound synology traffic"
|
|
||||||
rel="noopener noreferrer">outgoing access not configured</a>)
|
|
||||||
{{end}}
|
|
||||||
{{end}}
|
|
||||||
</p>
|
|
||||||
{{ end }}
|
|
||||||
{{ if or (eq .Status "NeedsLogin") (eq .Status "NoState") }}
|
|
||||||
{{ if .IP }}
|
|
||||||
<div class="mb-6">
|
|
||||||
<p class="text-gray-700">Your device's key has expired. Reauthenticate this device by logging in again, or <a
|
|
||||||
href="https://tailscale.com/kb/1028/key-expiry" class="link" target="_blank">learn more</a>.</p>
|
|
||||||
</div>
|
|
||||||
<a href="#" class="mb-4 js-loginButton" target="_blank">
|
|
||||||
<button class="button button-blue w-full">Reauthenticate</button>
|
|
||||||
</a>
|
|
||||||
{{ else }}
|
|
||||||
<div class="mb-6">
|
|
||||||
<h3 class="text-3xl font-semibold mb-3">Log in</h3>
|
|
||||||
<p class="text-gray-700">Get started by logging in to your Tailscale network. Or, learn more at <a
|
|
||||||
href="https://tailscale.com/" class="link" target="_blank">tailscale.com</a>.</p>
|
|
||||||
</div>
|
|
||||||
<a href="#" class="mb-4 js-loginButton" target="_blank">
|
|
||||||
<button class="button button-blue w-full">Log In</button>
|
|
||||||
</a>
|
|
||||||
{{ end }}
|
|
||||||
{{ else if eq .Status "NeedsMachineAuth" }}
|
|
||||||
<div class="mb-4">
|
|
||||||
This device is authorized, but needs approval from a network admin before it can connect to the network.
|
|
||||||
</div>
|
|
||||||
{{ else }}
|
|
||||||
<div class="mb-4">
|
|
||||||
<p>You are connected! Access this device over Tailscale using the device name or IP address above.</p>
|
|
||||||
</div>
|
|
||||||
<div class="mb-4">
|
|
||||||
<a href="#" class="mb-4 js-advertiseExitNode">
|
|
||||||
{{if .AdvertiseExitNode}}
|
|
||||||
<button class="button button-red button-medium" id="enabled">Stop advertising Exit Node</button>
|
|
||||||
{{else}}
|
|
||||||
<button class="button button-blue button-medium" id="enabled">Advertise as Exit Node</button>
|
|
||||||
{{end}}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
</main>
|
|
||||||
<footer class="container max-w-lg mx-auto text-center">
|
|
||||||
<a class="text-xs text-gray-500 hover:text-gray-600" href="{{ .LicensesURL }}">Open Source Licenses</a>
|
|
||||||
</footer>
|
|
||||||
<script>(function () {
|
|
||||||
const advertiseExitNode = {{ .AdvertiseExitNode }};
|
|
||||||
const isUnraid = {{ .IsUnraid }};
|
|
||||||
const unraidCsrfToken = "{{ .UnraidToken }}";
|
|
||||||
let fetchingUrl = false;
|
|
||||||
var data = {
|
|
||||||
AdvertiseRoutes: "{{ .AdvertiseRoutes }}",
|
|
||||||
AdvertiseExitNode: advertiseExitNode,
|
|
||||||
Reauthenticate: false,
|
|
||||||
ForceLogout: false
|
|
||||||
};
|
|
||||||
|
|
||||||
function postData(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (fetchingUrl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchingUrl = true;
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
const token = urlParams.get("SynoToken");
|
|
||||||
const nextParams = new URLSearchParams({ up: true });
|
|
||||||
if (token) {
|
|
||||||
nextParams.set("SynoToken", token)
|
|
||||||
}
|
|
||||||
const nextUrl = new URL(window.location);
|
|
||||||
nextUrl.search = nextParams.toString()
|
|
||||||
|
|
||||||
let body = JSON.stringify(data);
|
|
||||||
let contentType = "application/json";
|
|
||||||
|
|
||||||
if (isUnraid) {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
params.append("csrf_token", unraidCsrfToken);
|
|
||||||
params.append("ts_data", JSON.stringify(data));
|
|
||||||
|
|
||||||
body = params.toString();
|
|
||||||
contentType = "application/x-www-form-urlencoded;charset=UTF-8";
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = nextUrl.toString();
|
|
||||||
fetch(url, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Accept": "application/json",
|
|
||||||
"Content-Type": contentType,
|
|
||||||
},
|
|
||||||
body: body
|
|
||||||
}).then(res => res.json()).then(res => {
|
|
||||||
fetchingUrl = false;
|
|
||||||
const err = res["error"];
|
|
||||||
if (err) {
|
|
||||||
throw new Error(err);
|
|
||||||
}
|
|
||||||
const url = res["url"];
|
|
||||||
if (url) {
|
|
||||||
if(isUnraid) {
|
|
||||||
window.open(url, "_blank");
|
|
||||||
} else {
|
|
||||||
document.location.href = url;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
}).catch(err => {
|
|
||||||
alert("Failed operation: " + err.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
document.querySelectorAll(".js-loginButton").forEach(function (el){
|
|
||||||
el.addEventListener("click", function(e) {
|
|
||||||
data.Reauthenticate = true;
|
|
||||||
postData(e);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
document.querySelectorAll(".js-logoutButton").forEach(function(el) {
|
|
||||||
el.addEventListener("click", function (e) {
|
|
||||||
data.ForceLogout = true;
|
|
||||||
postData(e);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
document.querySelectorAll(".js-advertiseExitNode").forEach(function (el) {
|
|
||||||
el.addEventListener("click", function(e) {
|
|
||||||
data.AdvertiseExitNode = !advertiseExitNode;
|
|
||||||
postData(e);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
|
|
||||||
})();</script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
@ -251,7 +251,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
|||||||
hash/crc32 from compress/gzip+
|
hash/crc32 from compress/gzip+
|
||||||
hash/maphash from go4.org/mem
|
hash/maphash from go4.org/mem
|
||||||
html from tailscale.com/ipn/ipnstate+
|
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 from github.com/skip2/go-qrcode+
|
||||||
image/color from github.com/skip2/go-qrcode+
|
image/color from github.com/skip2/go-qrcode+
|
||||||
image/png from github.com/skip2/go-qrcode
|
image/png from github.com/skip2/go-qrcode
|
||||||
|
2
release/dist/synology/files/config
vendored
2
release/dist/synology/files/config
vendored
@ -4,7 +4,7 @@
|
|||||||
"type": "url",
|
"type": "url",
|
||||||
"title": "Tailscale",
|
"title": "Tailscale",
|
||||||
"icon": "PACKAGE_ICON_256.PNG",
|
"icon": "PACKAGE_ICON_256.PNG",
|
||||||
"url": "webman/3rdparty/Tailscale/",
|
"url": "webman/3rdparty/Tailscale/index.cgi/",
|
||||||
"urlTarget": "_syno_tailscale"
|
"urlTarget": "_syno_tailscale"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user