mirror of https://github.com/tailscale/tailscale/
client/web: move synology and qnap logic into separate files
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>pull/9077/head
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