Update
This commit is contained in:
2
vendor/tailscale.com/client/web/auth.go
generated
vendored
2
vendor/tailscale.com/client/web/auth.go
generated
vendored
@@ -192,7 +192,7 @@ func (s *Server) controlSupportsCheckMode(ctx context.Context) bool {
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
controlURL, err := url.Parse(prefs.ControlURLOrDefault())
|
||||
controlURL, err := url.Parse(prefs.ControlURLOrDefault(s.polc))
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
8
vendor/tailscale.com/client/web/package.json
generated
vendored
8
vendor/tailscale.com/client/web/package.json
generated
vendored
@@ -3,7 +3,7 @@
|
||||
"version": "0.0.1",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": "18.20.4",
|
||||
"node": "22.14.0",
|
||||
"yarn": "1.22.19"
|
||||
},
|
||||
"type": "module",
|
||||
@@ -20,7 +20,7 @@
|
||||
"zustand": "^4.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.16.1",
|
||||
"@types/node": "^22.14.0",
|
||||
"@types/react": "^18.0.20",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@vitejs/plugin-react-swc": "^3.6.0",
|
||||
@@ -34,10 +34,10 @@
|
||||
"prettier-plugin-organize-imports": "^3.2.2",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.1.7",
|
||||
"vite": "^5.4.21",
|
||||
"vite-plugin-svgr": "^4.2.0",
|
||||
"vite-tsconfig-paths": "^3.5.0",
|
||||
"vitest": "^1.3.1"
|
||||
"vitest": "^1.6.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"@typescript-eslint/eslint-plugin": "^6.2.1",
|
||||
|
||||
175
vendor/tailscale.com/client/web/web.go
generated
vendored
175
vendor/tailscale.com/client/web/web.go
generated
vendored
@@ -5,8 +5,8 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -14,19 +14,20 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/csrf"
|
||||
"tailscale.com/client/local"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/clientupdate"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/envknob/featureknob"
|
||||
"tailscale.com/feature"
|
||||
"tailscale.com/feature/buildfeatures"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
@@ -37,6 +38,7 @@ import (
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/util/httpm"
|
||||
"tailscale.com/util/syspolicy/policyclient"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
@@ -50,6 +52,7 @@ type Server struct {
|
||||
mode ServerMode
|
||||
|
||||
logf logger.Logf
|
||||
polc policyclient.Client // must be non-nil
|
||||
lc *local.Client
|
||||
timeNow func() time.Time
|
||||
|
||||
@@ -60,6 +63,12 @@ type Server struct {
|
||||
cgiMode bool
|
||||
pathPrefix string
|
||||
|
||||
// originOverride is the origin that the web UI is accessible from.
|
||||
// This value is used in the fallback CSRF checks when Sec-Fetch-Site is not
|
||||
// available. In this case the application will compare Host and Origin
|
||||
// header values to determine if the request is from the same origin.
|
||||
originOverride string
|
||||
|
||||
apiHandler http.Handler // serves api endpoints; csrf-protected
|
||||
assetsHandler http.Handler // serves frontend assets
|
||||
assetsCleanup func() // called from Server.Shutdown
|
||||
@@ -134,9 +143,13 @@ type ServerOpts struct {
|
||||
TimeNow func() time.Time
|
||||
|
||||
// Logf optionally provides a logger function.
|
||||
// log.Printf is used as default.
|
||||
// If nil, log.Printf is used as default.
|
||||
Logf logger.Logf
|
||||
|
||||
// PolicyClient, if non-nil, will be used to fetch policy settings.
|
||||
// If nil, the default policy client will be used.
|
||||
PolicyClient policyclient.Client
|
||||
|
||||
// The following two fields are required and used exclusively
|
||||
// in ManageServerMode to facilitate the control server login
|
||||
// check step for authorizing browser sessions.
|
||||
@@ -150,6 +163,9 @@ type ServerOpts struct {
|
||||
// as completed.
|
||||
// This field is required for ManageServerMode mode.
|
||||
WaitAuthURL func(ctx context.Context, id string, src tailcfg.NodeID) (*tailcfg.WebClientAuthResponse, error)
|
||||
|
||||
// OriginOverride specifies the origin that the web UI will be accessible from if hosted behind a reverse proxy or CGI.
|
||||
OriginOverride string
|
||||
}
|
||||
|
||||
// NewServer constructs a new Tailscale web client server.
|
||||
@@ -169,15 +185,17 @@ func NewServer(opts ServerOpts) (s *Server, err error) {
|
||||
opts.LocalClient = &local.Client{}
|
||||
}
|
||||
s = &Server{
|
||||
mode: opts.Mode,
|
||||
logf: opts.Logf,
|
||||
devMode: envknob.Bool("TS_DEBUG_WEB_CLIENT_DEV"),
|
||||
lc: opts.LocalClient,
|
||||
cgiMode: opts.CGIMode,
|
||||
pathPrefix: opts.PathPrefix,
|
||||
timeNow: opts.TimeNow,
|
||||
newAuthURL: opts.NewAuthURL,
|
||||
waitAuthURL: opts.WaitAuthURL,
|
||||
mode: opts.Mode,
|
||||
polc: cmp.Or(opts.PolicyClient, policyclient.Get()),
|
||||
logf: opts.Logf,
|
||||
devMode: envknob.Bool("TS_DEBUG_WEB_CLIENT_DEV"),
|
||||
lc: opts.LocalClient,
|
||||
cgiMode: opts.CGIMode,
|
||||
pathPrefix: opts.PathPrefix,
|
||||
timeNow: opts.TimeNow,
|
||||
newAuthURL: opts.NewAuthURL,
|
||||
waitAuthURL: opts.WaitAuthURL,
|
||||
originOverride: opts.OriginOverride,
|
||||
}
|
||||
if opts.PathPrefix != "" {
|
||||
// Enforce that path prefix always has a single leading '/'
|
||||
@@ -205,7 +223,7 @@ func NewServer(opts ServerOpts) (s *Server, err error) {
|
||||
|
||||
var metric string
|
||||
s.apiHandler, metric = s.modeAPIHandler(s.mode)
|
||||
s.apiHandler = s.withCSRF(s.apiHandler)
|
||||
s.apiHandler = s.csrfProtect(s.apiHandler)
|
||||
|
||||
// Don't block startup on reporting metric.
|
||||
// Report in separate go routine with 5 second timeout.
|
||||
@@ -218,23 +236,64 @@ func NewServer(opts ServerOpts) (s *Server, err error) {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *Server) withCSRF(h http.Handler) http.Handler {
|
||||
csrfProtect := csrf.Protect(s.csrfKey(), csrf.Secure(false))
|
||||
|
||||
// ref https://github.com/tailscale/tailscale/pull/14822
|
||||
// signal to the CSRF middleware that the request is being served over
|
||||
// plaintext HTTP to skip TLS-only header checks.
|
||||
withSetPlaintext := func(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
r = csrf.PlaintextHTTPRequest(r)
|
||||
func (s *Server) csrfProtect(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// CSRF is not required for GET, HEAD, or OPTIONS requests.
|
||||
if slices.Contains([]string{"GET", "HEAD", "OPTIONS"}, r.Method) {
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// NB: the order of the withSetPlaintext and csrfProtect calls is important
|
||||
// to ensure that we signal to the CSRF middleware that the request is being
|
||||
// served over plaintext HTTP and not over TLS as it presumes by default.
|
||||
return withSetPlaintext(csrfProtect(h))
|
||||
// first attempt to use Sec-Fetch-Site header (sent by all modern
|
||||
// browsers to "potentially trustworthy" origins i.e. localhost or those
|
||||
// served over HTTPS)
|
||||
secFetchSite := r.Header.Get("Sec-Fetch-Site")
|
||||
if secFetchSite == "same-origin" {
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
} else if secFetchSite != "" {
|
||||
http.Error(w, fmt.Sprintf("CSRF request denied with Sec-Fetch-Site %q", secFetchSite), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// if Sec-Fetch-Site is not available we presume we are operating over HTTP.
|
||||
// We fall back to comparing the Origin & Host headers.
|
||||
|
||||
// use the Host header to determine the expected origin
|
||||
// (use the override if set to allow for reverse proxying)
|
||||
host := r.Host
|
||||
if host == "" {
|
||||
http.Error(w, "CSRF request denied with no Host header", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if s.originOverride != "" {
|
||||
host = s.originOverride
|
||||
}
|
||||
|
||||
originHeader := r.Header.Get("Origin")
|
||||
if originHeader == "" {
|
||||
http.Error(w, "CSRF request denied with no Origin header", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
parsedOrigin, err := url.Parse(originHeader)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("CSRF request denied with invalid Origin %q", r.Header.Get("Origin")), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
origin := parsedOrigin.Host
|
||||
if origin == "" {
|
||||
http.Error(w, "CSRF request denied with no host in the Origin header", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if origin != host {
|
||||
http.Error(w, fmt.Sprintf("CSRF request denied with mismatched Origin %q and Host %q", origin, host), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
h.ServeHTTP(w, r)
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) modeAPIHandler(mode ServerMode) (http.Handler, string) {
|
||||
@@ -438,6 +497,10 @@ func (s *Server) authorizeRequest(w http.ResponseWriter, r *http.Request) (ok bo
|
||||
// Client using system-specific auth.
|
||||
switch distro.Get() {
|
||||
case distro.Synology:
|
||||
if !buildfeatures.HasSynology {
|
||||
// Synology support not built in.
|
||||
return false
|
||||
}
|
||||
authorized, _ := authorizeSynology(r)
|
||||
return authorized
|
||||
case distro.QNAP:
|
||||
@@ -452,7 +515,6 @@ func (s *Server) authorizeRequest(w http.ResponseWriter, r *http.Request) (ok bo
|
||||
// It should only be called by Server.ServeHTTP, via Server.apiHandler,
|
||||
// which protects the handler using gorilla csrf.
|
||||
func (s *Server) serveLoginAPI(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-CSRF-Token", csrf.Token(r))
|
||||
switch {
|
||||
case r.URL.Path == "/api/data" && r.Method == httpm.GET:
|
||||
s.serveGetNodeData(w, r)
|
||||
@@ -575,7 +637,6 @@ func (s *Server) serveAPI(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("X-CSRF-Token", csrf.Token(r))
|
||||
path := strings.TrimPrefix(r.URL.Path, "/api")
|
||||
switch {
|
||||
case path == "/data" && r.Method == httpm.GET:
|
||||
@@ -902,7 +963,7 @@ func (s *Server) serveGetNodeData(w http.ResponseWriter, r *http.Request) {
|
||||
UnraidToken: os.Getenv("UNRAID_CSRF_TOKEN"),
|
||||
RunningSSHServer: prefs.RunSSH,
|
||||
URLPrefix: strings.TrimSuffix(s.pathPrefix, "/"),
|
||||
ControlAdminURL: prefs.AdminPageURL(),
|
||||
ControlAdminURL: prefs.AdminPageURL(s.polc),
|
||||
LicensesURL: licenses.LicensesURL(),
|
||||
Features: availableFeatures(),
|
||||
|
||||
@@ -922,9 +983,18 @@ func (s *Server) serveGetNodeData(w http.ResponseWriter, r *http.Request) {
|
||||
data.ClientVersion = cv
|
||||
}
|
||||
|
||||
if st.CurrentTailnet != nil {
|
||||
data.TailnetName = st.CurrentTailnet.MagicDNSSuffix
|
||||
data.DomainName = st.CurrentTailnet.Name
|
||||
profile, _, err := s.lc.ProfileStatus(r.Context())
|
||||
if err != nil {
|
||||
s.logf("error fetching profiles: %v", err)
|
||||
// If for some reason we can't fetch profiles,
|
||||
// continue to use st.CurrentTailnet if set.
|
||||
if st.CurrentTailnet != nil {
|
||||
data.TailnetName = st.CurrentTailnet.MagicDNSSuffix
|
||||
data.DomainName = st.CurrentTailnet.Name
|
||||
}
|
||||
} else {
|
||||
data.TailnetName = profile.NetworkProfile.MagicDNSName
|
||||
data.DomainName = profile.NetworkProfile.DisplayNameOrDefault()
|
||||
}
|
||||
if st.Self.Tags != nil {
|
||||
data.Tags = st.Self.Tags.AsSlice()
|
||||
@@ -984,7 +1054,7 @@ func availableFeatures() map[string]bool {
|
||||
"advertise-routes": true, // available on all platforms
|
||||
"use-exit-node": featureknob.CanUseExitNode() == nil,
|
||||
"ssh": featureknob.CanRunTailscaleSSH() == nil,
|
||||
"auto-update": version.IsUnstableBuild() && clientupdate.CanAutoUpdate(),
|
||||
"auto-update": version.IsUnstableBuild() && feature.CanAutoUpdate(),
|
||||
}
|
||||
return features
|
||||
}
|
||||
@@ -1276,37 +1346,6 @@ func (s *Server) proxyRequestToLocalAPI(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
}
|
||||
|
||||
// csrfKey returns a key that can be used for CSRF protection.
|
||||
// If an error occurs during key creation, the error is logged and the active process terminated.
|
||||
// If the server is running in CGI mode, the key is cached to disk and reused between requests.
|
||||
// If an error occurs during key storage, the error is logged and the active process terminated.
|
||||
func (s *Server) csrfKey() []byte {
|
||||
csrfFile := filepath.Join(os.TempDir(), "tailscale-web-csrf.key")
|
||||
|
||||
// if running in CGI mode, try to read from disk, but ignore errors
|
||||
if s.cgiMode {
|
||||
key, _ := os.ReadFile(csrfFile)
|
||||
if len(key) == 32 {
|
||||
return key
|
||||
}
|
||||
}
|
||||
|
||||
// create a new key
|
||||
key := make([]byte, 32)
|
||||
if _, err := rand.Read(key); err != nil {
|
||||
log.Fatalf("error generating CSRF key: %v", err)
|
||||
}
|
||||
|
||||
// if running in CGI mode, try to write the newly created key to disk, and exit if it fails.
|
||||
if s.cgiMode {
|
||||
if err := os.WriteFile(csrfFile, key, 0600); err != nil {
|
||||
log.Fatalf("unable to store CSRF key: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
// enforcePrefix returns a HandlerFunc that enforces a given path prefix is used in requests,
|
||||
// then strips it before invoking h.
|
||||
// Unlike http.StripPrefix, it does not return a 404 if the prefix is not present.
|
||||
|
||||
739
vendor/tailscale.com/client/web/yarn.lock
generated
vendored
739
vendor/tailscale.com/client/web/yarn.lock
generated
vendored
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user