// 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"
)

// authorizeQNAP authenticates the logged-in QNAP 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 authorizeQNAP(w http.ResponseWriter, r *http.Request) (handled bool) {
	_, resp, err := qnapAuthn(r)
	if err != nil {
		http.Error(w, err.Error(), http.StatusUnauthorized)
		return true
	}
	if resp.IsAdmin == 0 {
		http.Error(w, "user is not an admin", http.StatusForbidden)
		return true
	}

	return false
}

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
}
