package websocket

import (
	"encoding/json"
	"net/http"
	"net/url"
	"strings"

	"github.com/rancher/norman/httperror"
)

const (
	originHeader     = "Origin"
	connectionHeader = "Connection"
	userAgentHeader  = "User-Agent"
)

func NewWebsocketHandler(handler http.Handler) http.Handler {
	return &websocketHandler{
		handler,
	}
}

type websocketHandler struct {
	next http.Handler
}

func (h websocketHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
	if isWebsocket(req) && isBrowserUserAgent(req.Header) {
		if !checkSameOrigin(req) {
			response(rw, httperror.PermissionDenied, "origin not allowed")
			return
		}
	}
	h.next.ServeHTTP(rw, req)
}

// Inspired by https://github.com/gorilla/websocket/blob/80c2d40e9b91f2ef7a9c1a403aeec64d1b89a9a6/server.go#L87
// checkSameOrigin returns true if the origin is not set or is equal to the request host.
func checkSameOrigin(r *http.Request) bool {
	origins := r.Header[originHeader]

	if len(origins) == 0 {
		return true
	}

	for _, origin := range r.Header[originHeader] {
		u, err := url.Parse(origin)
		if err != nil {
			continue
		}

		if u.Port() == "" {
			if u.Host == r.Host {
				return true
			}
		} else if u.Host == r.Host && u.Port() == portOnly(r.Host) {
			return true
		}
	}
	return false
}

// isWebsocket returns true if the request is a websocket
func isWebsocket(r *http.Request) bool {
	if !headerListContainsValue(r.Header, connectionHeader, "upgrade") {
		return false
	}
	return true
}

// headerListContainsValue returns true if the token header with the given name contains token.
func headerListContainsValue(header http.Header, name string, value string) bool {
	for _, v := range header[name] {
		for _, s := range strings.Split(v, ",") {
			if strings.EqualFold(value, strings.TrimSpace(s)) {
				return true
			}
		}
	}
	return false
}

// isBrowserUserAgent returns true if User-Agent is assumed to be a browser (UA header contains "mozilla").
func isBrowserUserAgent(header http.Header) bool {
	for _, v := range header[userAgentHeader] {
		if strings.Contains(strings.ToLower(v), "mozilla") {
			return true
		}
	}
	return false
}

func response(rw http.ResponseWriter, code httperror.ErrorCode, message string) {
	rw.WriteHeader(code.Status)
	rw.Header().Set("content-type", "application/json")
	json.NewEncoder(rw).Encode(httperror.NewAPIError(code, message))
}

// portOnly returns the port part of localhost:port, without the leading colon
func portOnly(hostport string) string {
	colon := strings.IndexByte(hostport, ':')
	if colon == -1 {
		return ""
	}
	if i := strings.Index(hostport, "]:"); i != -1 {
		return hostport[i+len("]:"):]
	}
	if strings.Contains(hostport, "]") {
		return ""
	}
	return hostport[colon+len(":"):]
}
