added http auth config to the client-side control channel

This commit is contained in:
2025-02-01 00:06:05 +09:00
parent 16327fc576
commit 0fb57cb77b
8 changed files with 206 additions and 127 deletions

98
hodu.go
View File

@ -1,10 +1,12 @@
package hodu
import "crypto/rsa"
import "fmt"
import "net"
import "net/http"
import "net/netip"
import "os"
import "path/filepath"
import "runtime"
import "strings"
import "sync"
@ -61,6 +63,17 @@ type HttpAccessRule struct {
Action HttpAccessAction
}
type HttpAuthCredMap map[string]string
type HttpAuthConfig struct {
Enabled bool
Realm string
Creds HttpAuthCredMap
TokenTtl time.Duration
TokenRsaKey *rsa.PrivateKey
AccessRules []HttpAccessRule
}
type JsonErrmsg struct {
Text string `json:"error-text"`
}
@ -230,13 +243,15 @@ func DurationToSecString(d time.Duration) string {
return fmt.Sprintf("%.09f", d.Seconds())
}
// ------------------------------------
func WriteJsonRespHeader(w http.ResponseWriter, status_code int) int {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status_code)
return status_code
}
func write_js_resp_header(w http.ResponseWriter, status_code int) int {
func WriteJsRespHeader(w http.ResponseWriter, status_code int) int {
w.Header().Set("Content-Type", "application/javascript")
w.WriteHeader(status_code)
return status_code
@ -259,6 +274,8 @@ func WriteEmptyRespHeader(w http.ResponseWriter, status_code int) int {
return status_code
}
// ------------------------------------
func server_route_to_proxy_info(r *ServerRoute) *ServerRouteProxyInfo {
return &ServerRouteProxyInfo{
SvcOption: r.SvcOption,
@ -279,6 +296,8 @@ func proxy_info_to_server_route(pi *ServerRouteProxyInfo) *ServerRoute {
}
}
// ------------------------------------
func (stats *json_out_go_stats) from_runtime_stats() {
var mstat runtime.MemStats
@ -312,3 +331,80 @@ func (stats *json_out_go_stats) from_runtime_stats() {
stats.GCSysBytes = mstat.GCSys
stats.OtherSysBytes = mstat.OtherSys
}
// ------------------------------------
func (auth *HttpAuthConfig) Authenticate(req *http.Request) (int, string) {
var rule HttpAccessRule
var raddrport netip.AddrPort
var raddr netip.Addr
var err error
raddrport, err = netip.ParseAddrPort(req.RemoteAddr)
if err == nil { raddr = raddrport.Addr() }
for _, rule = range auth.AccessRules {
// i don't take into account X-Forwarded-For and similar headers
if req.URL.Path == rule.Prefix || strings.HasPrefix(req.URL.Path, filepath.Clean(rule.Prefix + "/")) {
var org_net_ok bool
if len(rule.OrgNets) > 0 && raddr.IsValid() {
var netpfx netip.Prefix
org_net_ok = false
for _, netpfx = range rule.OrgNets {
if err == nil && netpfx.Contains(raddr) {
org_net_ok = true
break
}
}
} else {
org_net_ok = true
}
if org_net_ok {
if rule.Action == HTTP_ACCESS_ACCEPT {
return http.StatusOK, ""
} else if rule.Action == HTTP_ACCESS_REJECT {
return http.StatusForbidden, ""
}
}
}
}
if auth != nil && auth.Enabled {
var auth_hdr string
var auth_parts []string
var username string
var password string
var credpass string
var ok bool
var err error
auth_hdr = req.Header.Get("Authorization")
if auth_hdr == "" { return http.StatusUnauthorized, auth.Realm }
auth_parts = strings.Fields(auth_hdr)
if len(auth_parts) == 2 && strings.EqualFold(auth_parts[0], "Bearer") && auth.TokenRsaKey != nil {
var jwt *JWT[ServerTokenClaim]
var claim ServerTokenClaim
jwt = NewJWT(auth.TokenRsaKey, &claim)
err = jwt.VerifyRS512(strings.TrimSpace(auth_parts[1]))
if err == nil {
// verification ok. let's check the actual payload
var now time.Time
now = time.Now()
if now.After(time.Unix(claim.IssuedAt, 0)) && now.Before(time.Unix(claim.ExpiresAt, 0)) { return http.StatusOK, "" } // not expired
}
}
// fall back to basic authentication
username, password, ok = req.BasicAuth()
if !ok { return http.StatusUnauthorized, auth.Realm }
credpass, ok = auth.Creds[username]
if !ok || credpass != password { return http.StatusUnauthorized, auth.Realm }
}
return http.StatusOK, ""
}