mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 13:05:46 +00:00
dc8287ab3b
Synology and QNAP both run the web client as a CGI script. The old web client didn't care too much about requests paths, since there was only a single GET and POST handler. The new client serves assets on different paths, so now we need to care. First, enforce that the CGI script is always accessed from its full path, including a trailing slash (e.g. /cgi-bin/tailscale/index.cgi/). Then, strip that prefix off before passing the request along to the main serve handler. This allows for properly serving both static files and the API handler in a CGI environment. Also add a CGIPath option to allow other CGI environments to specify a custom path. Finally, update vite and one "api/data" call to no longer assume that we are always serving at the root path of "/". Updates tailscale/corp#13775 Signed-off-by: Will Norris <will@tailscale.com>
79 lines
2.0 KiB
Go
79 lines
2.0 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
// synology.go contains handlers and logic, such as authentication,
|
|
// that is specific to running the web client on Synology.
|
|
|
|
package web
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"os/exec"
|
|
"strings"
|
|
|
|
"tailscale.com/util/groupmember"
|
|
)
|
|
|
|
const synologyPrefix = "/webman/3rdparty/Tailscale/index.cgi/"
|
|
|
|
// authorizeSynology authenticates the logged-in Synology user and verifies
|
|
// that they are authorized to use the web client. It returns true if the
|
|
// request was handled and no further processing is required.
|
|
func authorizeSynology(w http.ResponseWriter, r *http.Request) (handled bool) {
|
|
if synoTokenRedirect(w, r) {
|
|
return true
|
|
}
|
|
|
|
// authenticate the Synology user
|
|
cmd := exec.Command("/usr/syno/synoman/webman/modules/authenticate.cgi")
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("auth: %v: %s", err, out), http.StatusUnauthorized)
|
|
return true
|
|
}
|
|
user := strings.TrimSpace(string(out))
|
|
|
|
// check if the user is in the administrators group
|
|
isAdmin, err := groupmember.IsMemberOfGroup("administrators", user)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusForbidden)
|
|
return true
|
|
}
|
|
if !isAdmin {
|
|
http.Error(w, "not a member of administrators group", http.StatusForbidden)
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func synoTokenRedirect(w http.ResponseWriter, r *http.Request) bool {
|
|
if r.Header.Get("X-Syno-Token") != "" {
|
|
return false
|
|
}
|
|
if r.URL.Query().Get("SynoToken") != "" {
|
|
return false
|
|
}
|
|
if r.Method == "POST" && r.FormValue("SynoToken") != "" {
|
|
return false
|
|
}
|
|
// We need a SynoToken for authenticate.cgi.
|
|
// So we tell the client to get one.
|
|
_, _ = fmt.Fprint(w, synoTokenRedirectHTML)
|
|
return true
|
|
}
|
|
|
|
const synoTokenRedirectHTML = `<html>
|
|
Redirecting with session token...
|
|
<script>
|
|
fetch("/webman/login.cgi")
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
u = new URL(window.location)
|
|
u.searchParams.set("SynoToken", data.SynoToken)
|
|
document.location = u
|
|
})
|
|
</script>
|
|
`
|