feat: 实现管理鉴权、API 令牌与操作日志

This commit is contained in:
Nixevol
2026-09-30 06:58:54 +08:00
parent 308b0b9edd
commit 0e068c6ade
13 changed files with 1655 additions and 1 deletions
+91
View File
@@ -0,0 +1,91 @@
package httpx
import (
"net"
"net/http"
"strings"
)
// ClientIP 按 DEVELOPMENT 4.5:仅当对端在 trusted 网段内时采信
// X-Forwarded-For(从右往左第一个不在 trusted 内的地址)。
func ClientIP(r *http.Request, trusted []*net.IPNet) string {
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
host = r.RemoteAddr
}
ip := net.ParseIP(host)
if ip == nil {
return host
}
if !ipInNets(ip, trusted) {
return ip.String()
}
xff := r.Header.Get("X-Forwarded-For")
if xff == "" {
return ip.String()
}
parts := strings.Split(xff, ",")
for i := len(parts) - 1; i >= 0; i-- {
cand := strings.TrimSpace(parts[i])
parsed := net.ParseIP(cand)
if parsed == nil {
continue
}
if !ipInNets(parsed, trusted) {
return parsed.String()
}
}
return ip.String()
}
// IsHTTPS 判定请求是否视为 HTTPS(直连 TLS 或受信任代理的 X-Forwarded-Proto)。
func IsHTTPS(r *http.Request, trusted []*net.IPNet) bool {
if r.TLS != nil {
return true
}
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
host = r.RemoteAddr
}
ip := net.ParseIP(host)
if ip == nil || !ipInNets(ip, trusted) {
return false
}
proto := strings.ToLower(strings.TrimSpace(r.Header.Get("X-Forwarded-Proto")))
return proto == "https"
}
// ParseCIDRs 解析 CIDR 列表;非法项跳过。
func ParseCIDRs(cidrs []string) []*net.IPNet {
var out []*net.IPNet
for _, c := range cidrs {
c = strings.TrimSpace(c)
if c == "" {
continue
}
_, n, err := net.ParseCIDR(c)
if err != nil {
// 允许单 IP 写成无掩码
if ip := net.ParseIP(c); ip != nil {
if ip.To4() != nil {
_, n, err = net.ParseCIDR(ip.String() + "/32")
} else {
_, n, err = net.ParseCIDR(ip.String() + "/128")
}
}
}
if err == nil && n != nil {
out = append(out, n)
}
}
return out
}
func ipInNets(ip net.IP, nets []*net.IPNet) bool {
for _, n := range nets {
if n.Contains(ip) {
return true
}
}
return false
}
+34
View File
@@ -0,0 +1,34 @@
package httpx_test
import (
"net/http"
"net/http/httptest"
"testing"
"git.asio.asia/nixevol/NixMsg/internal/httpx"
)
func TestClientIPWithoutProxy(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/", nil)
r.RemoteAddr = "203.0.113.9:1234"
r.Header.Set("X-Forwarded-For", "198.51.100.1")
ip := httpx.ClientIP(r, nil)
if ip != "203.0.113.9" {
t.Fatalf("got %q", ip)
}
}
func TestParseCIDRsAndTrustedXFF(t *testing.T) {
trusted := httpx.ParseCIDRs([]string{"127.0.0.1/32"})
r := httptest.NewRequest(http.MethodGet, "/", nil)
r.RemoteAddr = "127.0.0.1:9999"
r.Header.Set("X-Forwarded-For", "198.51.100.7, 127.0.0.1")
ip := httpx.ClientIP(r, trusted)
if ip != "198.51.100.7" {
t.Fatalf("got %q", ip)
}
r.Header.Set("X-Forwarded-Proto", "https")
if !httpx.IsHTTPS(r, trusted) {
t.Fatal("expected https via proxy")
}
}
+61
View File
@@ -0,0 +1,61 @@
// Package httpx 提供管理接口共用的 HTTP 辅助(JSON 信封、客户端 IP、HTTPS 判定)。
package httpx
import (
"encoding/json"
"errors"
"io"
"net/http"
)
// ErrorBody 是失败响应里的 error 对象。
type ErrorBody struct {
Code string `json:"code"`
Message string `json:"message"`
}
// Envelope 是管理接口通用响应信封。
type Envelope struct {
OK bool `json:"ok"`
Data any `json:"data,omitempty"`
Error *ErrorBody `json:"error,omitempty"`
}
// WriteJSON 写入 JSON 响应。
func WriteJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
enc := json.NewEncoder(w)
enc.SetEscapeHTML(false)
_ = enc.Encode(v)
}
// WriteOK 写入成功信封。
func WriteOK(w http.ResponseWriter, data any) {
WriteJSON(w, http.StatusOK, Envelope{OK: true, Data: data})
}
// WriteError 写入失败信封。
func WriteError(w http.ResponseWriter, status int, code, message string) {
WriteJSON(w, status, Envelope{
OK: false,
Error: &ErrorBody{
Code: code,
Message: message,
},
})
}
// DecodeJSON 解码请求 JSON 体;空体对 dst 保持零值。
func DecodeJSON(r *http.Request, dst any) error {
defer func() { _ = r.Body.Close() }()
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
if err := dec.Decode(dst); err != nil {
if errors.Is(err, io.EOF) {
return nil
}
return err
}
return nil
}