mirror of
https://github.com/wavetermdev/waveterm
synced 2026-05-24 09:18:27 +00:00
140 lines
3.4 KiB
Go
140 lines
3.4 KiB
Go
// Copyright 2026, Command Line Inc.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package web
|
|
|
|
import (
|
|
"bufio"
|
|
"crypto/subtle"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"net/url"
|
|
"strings"
|
|
)
|
|
|
|
const RemotePasswordHeader = "X-Remote-Password"
|
|
const AuthKeyHeader = "X-AuthKey"
|
|
|
|
// RemoteEntry is a password-guarded reverse-proxy that forwards HTTP and
|
|
// WebSocket traffic to wavesrv's internal listeners after stripping the
|
|
// remote password and injecting the internal authkey.
|
|
type RemoteEntry struct {
|
|
password string
|
|
webAddr string // host:port of the internal web listener
|
|
wsAddr string // host:port of the internal ws listener
|
|
authKey string
|
|
httpProxy *httputil.ReverseProxy
|
|
}
|
|
|
|
func NewRemoteEntry(password, webAddr, wsAddr, authKey string) *RemoteEntry {
|
|
target, _ := url.Parse("http://" + webAddr)
|
|
proxy := httputil.NewSingleHostReverseProxy(target)
|
|
origDirector := proxy.Director
|
|
proxy.Director = func(r *http.Request) {
|
|
origDirector(r)
|
|
r.Header.Del(RemotePasswordHeader)
|
|
r.Header.Set(AuthKeyHeader, authKey)
|
|
}
|
|
return &RemoteEntry{
|
|
password: password,
|
|
webAddr: webAddr,
|
|
wsAddr: wsAddr,
|
|
authKey: authKey,
|
|
httpProxy: proxy,
|
|
}
|
|
}
|
|
|
|
// Serve runs the entry on the given listener. Blocks until listener closes.
|
|
func (e *RemoteEntry) Serve(ln net.Listener) error {
|
|
srv := &http.Server{Handler: http.HandlerFunc(e.handle)}
|
|
return srv.Serve(ln)
|
|
}
|
|
|
|
func (e *RemoteEntry) handle(w http.ResponseWriter, r *http.Request) {
|
|
if !e.authOK(r) {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
w.Write([]byte("unauthorized"))
|
|
return
|
|
}
|
|
if isWebSocketUpgrade(r) {
|
|
e.proxyWebSocket(w, r)
|
|
return
|
|
}
|
|
e.httpProxy.ServeHTTP(w, r)
|
|
}
|
|
|
|
func (e *RemoteEntry) authOK(r *http.Request) bool {
|
|
got := r.Header.Get(RemotePasswordHeader)
|
|
if got == "" {
|
|
return false
|
|
}
|
|
return subtle.ConstantTimeCompare([]byte(got), []byte(e.password)) == 1
|
|
}
|
|
|
|
func isWebSocketUpgrade(r *http.Request) bool {
|
|
return strings.EqualFold(r.Header.Get("Upgrade"), "websocket")
|
|
}
|
|
|
|
func (e *RemoteEntry) proxyWebSocket(w http.ResponseWriter, r *http.Request) {
|
|
backend, err := net.Dial("tcp", e.wsAddr)
|
|
if err != nil {
|
|
http.Error(w, "backend dial failed", http.StatusBadGateway)
|
|
return
|
|
}
|
|
|
|
upstreamReq := r.Clone(r.Context())
|
|
upstreamReq.Header.Del(RemotePasswordHeader)
|
|
upstreamReq.Header.Set(AuthKeyHeader, e.authKey)
|
|
upstreamReq.URL.Scheme = "http"
|
|
upstreamReq.URL.Host = e.wsAddr
|
|
upstreamReq.RequestURI = ""
|
|
|
|
if err := upstreamReq.Write(backend); err != nil {
|
|
backend.Close()
|
|
http.Error(w, "handshake write failed", http.StatusBadGateway)
|
|
return
|
|
}
|
|
|
|
br := bufio.NewReader(backend)
|
|
resp, err := http.ReadResponse(br, upstreamReq)
|
|
if err != nil {
|
|
backend.Close()
|
|
http.Error(w, "handshake read failed", http.StatusBadGateway)
|
|
return
|
|
}
|
|
|
|
hj, ok := w.(http.Hijacker)
|
|
if !ok {
|
|
backend.Close()
|
|
http.Error(w, "hijack unsupported", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
client, clientBuf, err := hj.Hijack()
|
|
if err != nil {
|
|
backend.Close()
|
|
http.Error(w, "hijack failed", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer client.Close()
|
|
defer backend.Close()
|
|
|
|
if err := resp.Write(client); err != nil {
|
|
return
|
|
}
|
|
|
|
clientToBackend := io.MultiReader(clientBuf, client)
|
|
backendToClient := io.MultiReader(br, backend)
|
|
|
|
errc := make(chan error, 2)
|
|
go func() {
|
|
_, err := io.Copy(backend, clientToBackend)
|
|
errc <- err
|
|
}()
|
|
go func() {
|
|
_, err := io.Copy(client, backendToClient)
|
|
errc <- err
|
|
}()
|
|
<-errc
|
|
}
|