mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-11-04 10:11:18 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			128 lines
		
	
	
		
			3.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			128 lines
		
	
	
		
			3.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// 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"
 | 
						|
	"errors"
 | 
						|
	"fmt"
 | 
						|
	"io"
 | 
						|
	"log"
 | 
						|
	"net/http"
 | 
						|
	"net/url"
 | 
						|
)
 | 
						|
 | 
						|
// authorizeQNAP authenticates the logged-in QNAP user and verifies that they
 | 
						|
// are authorized to use the web client.
 | 
						|
// If the user is not authorized to use the client, an error is returned.
 | 
						|
func authorizeQNAP(r *http.Request) (authorized bool, err error) {
 | 
						|
	_, resp, err := qnapAuthn(r)
 | 
						|
	if err != nil {
 | 
						|
		return false, err
 | 
						|
	}
 | 
						|
	if resp.IsAdmin == 0 {
 | 
						|
		return false, errors.New("user is not an admin")
 | 
						|
	}
 | 
						|
 | 
						|
	return true, nil
 | 
						|
}
 | 
						|
 | 
						|
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
 | 
						|
}
 |