This commit doesn't change any of the logic, but just organizes the code a little to prepare for future changes. Updates tailscale/corp#13775 Signed-off-by: Will Norris <will@tailscale.com>main
parent
ff7f4b4224
commit
05486f0f8e
@ -0,0 +1,111 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// qnap.go contains handlers and logic, such as authentication,
|
||||
// that is specific to running the web client on QNAP.
|
||||
|
||||
package web |
||||
|
||||
import ( |
||||
"crypto/tls" |
||||
"encoding/xml" |
||||
"fmt" |
||||
"io" |
||||
"log" |
||||
"net/http" |
||||
"net/url" |
||||
) |
||||
|
||||
type qnapAuthResponse struct { |
||||
AuthPassed int `xml:"authPassed"` |
||||
IsAdmin int `xml:"isAdmin"` |
||||
AuthSID string `xml:"authSid"` |
||||
ErrorValue int `xml:"errorValue"` |
||||
} |
||||
|
||||
func qnapAuthn(r *http.Request) (string, *qnapAuthResponse, error) { |
||||
user, err := r.Cookie("NAS_USER") |
||||
if err != nil { |
||||
return "", nil, err |
||||
} |
||||
token, err := r.Cookie("qtoken") |
||||
if err == nil { |
||||
return qnapAuthnQtoken(r, user.Value, token.Value) |
||||
} |
||||
sid, err := r.Cookie("NAS_SID") |
||||
if err == nil { |
||||
return qnapAuthnSid(r, user.Value, sid.Value) |
||||
} |
||||
return "", nil, fmt.Errorf("not authenticated by any mechanism") |
||||
} |
||||
|
||||
// qnapAuthnURL returns the auth URL to use by inferring where the UI is
|
||||
// running based on the request URL. This is necessary because QNAP has so
|
||||
// many options, see https://github.com/tailscale/tailscale/issues/7108
|
||||
// and https://github.com/tailscale/tailscale/issues/6903
|
||||
func qnapAuthnURL(requestUrl string, query url.Values) string { |
||||
in, err := url.Parse(requestUrl) |
||||
scheme := "" |
||||
host := "" |
||||
if err != nil || in.Scheme == "" { |
||||
log.Printf("Cannot parse QNAP login URL %v", err) |
||||
|
||||
// try localhost and hope for the best
|
||||
scheme = "http" |
||||
host = "localhost" |
||||
} else { |
||||
scheme = in.Scheme |
||||
host = in.Host |
||||
} |
||||
|
||||
u := url.URL{ |
||||
Scheme: scheme, |
||||
Host: host, |
||||
Path: "/cgi-bin/authLogin.cgi", |
||||
RawQuery: query.Encode(), |
||||
} |
||||
|
||||
return u.String() |
||||
} |
||||
|
||||
func qnapAuthnQtoken(r *http.Request, user, token string) (string, *qnapAuthResponse, error) { |
||||
query := url.Values{ |
||||
"qtoken": []string{token}, |
||||
"user": []string{user}, |
||||
} |
||||
return qnapAuthnFinish(user, qnapAuthnURL(r.URL.String(), query)) |
||||
} |
||||
|
||||
func qnapAuthnSid(r *http.Request, user, sid string) (string, *qnapAuthResponse, error) { |
||||
query := url.Values{ |
||||
"sid": []string{sid}, |
||||
} |
||||
return qnapAuthnFinish(user, qnapAuthnURL(r.URL.String(), query)) |
||||
} |
||||
|
||||
func qnapAuthnFinish(user, url string) (string, *qnapAuthResponse, error) { |
||||
// QNAP Force HTTPS mode uses a self-signed certificate. Even importing
|
||||
// the QNAP root CA isn't enough, the cert doesn't have a usable CN nor
|
||||
// SAN. See https://github.com/tailscale/tailscale/issues/6903
|
||||
tr := &http.Transport{ |
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, |
||||
} |
||||
client := &http.Client{Transport: tr} |
||||
resp, err := client.Get(url) |
||||
if err != nil { |
||||
return "", nil, err |
||||
} |
||||
defer resp.Body.Close() |
||||
out, err := io.ReadAll(resp.Body) |
||||
if err != nil { |
||||
return "", nil, err |
||||
} |
||||
authResp := &qnapAuthResponse{} |
||||
if err := xml.Unmarshal(out, authResp); err != nil { |
||||
return "", nil, err |
||||
} |
||||
if authResp.AuthPassed == 0 { |
||||
return "", nil, fmt.Errorf("not authenticated") |
||||
} |
||||
return user, authResp, nil |
||||
} |
||||
@ -0,0 +1,71 @@ |
||||
// 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" |
||||
) |
||||
|
||||
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><body> |
||||
Redirecting with session token... |
||||
<script> |
||||
var serverURL = window.location.protocol + "//" + window.location.host; |
||||
var req = new XMLHttpRequest(); |
||||
req.overrideMimeType("application/json"); |
||||
req.open("GET", serverURL + "/webman/login.cgi", true); |
||||
req.onload = function() { |
||||
var jsonResponse = JSON.parse(req.responseText); |
||||
var token = jsonResponse["SynoToken"]; |
||||
document.location.href = serverURL + "/webman/3rdparty/Tailscale/?SynoToken=" + token; |
||||
}; |
||||
req.send(null); |
||||
</script> |
||||
</body></html> |
||||
` |
||||
|
||||
func synoAuthn() (string, error) { |
||||
cmd := exec.Command("/usr/syno/synoman/webman/modules/authenticate.cgi") |
||||
out, err := cmd.CombinedOutput() |
||||
if err != nil { |
||||
return "", fmt.Errorf("auth: %v: %s", err, out) |
||||
} |
||||
return strings.TrimSpace(string(out)), nil |
||||
} |
||||
|
||||
// authorizeSynology checks whether the provided user has access to the web UI
|
||||
// by consulting the membership of the "administrators" group.
|
||||
func authorizeSynology(name string) error { |
||||
yes, err := groupmember.IsMemberOfGroup("administrators", name) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if !yes { |
||||
return fmt.Errorf("not a member of administrators group") |
||||
} |
||||
return nil |
||||
} |
||||
Loading…
Reference in new issue