Files
knowledge-governance/internal/objectgateway/http.go
姚章浩 5eef13b615 feat(object-gateway): 统一对象写网关与不可变治理日志
方案A:所有对象写入经 loopback 网关,持久计划/grant/intent/逐版本 receipt 与
attempt→grant delivery 落在治理 PG;scope 由 provider+endpoint+bucket 决定,
同一物理桶只允许一个写域。版本 append 不等于 CAS,未锚定计划在首笔业务写前拒绝。

本提交只含网关与其适配层接线、协议文档;生产独占、凭据切换与桶设置未改动。
2026-11-01 11:35:05 +08:00

317 lines
8.5 KiB
Go

package objectgateway
import (
"bytes"
"context"
"crypto/subtle"
"encoding/json"
"errors"
"io"
"net/http"
"net/url"
"strings"
"time"
)
type ExecuteRequest struct {
Authorization *Grant `json:"authorization,omitempty"`
Request Request `json:"request"`
Body []byte `json:"body,omitempty"`
}
type AdvanceRequest struct {
Expected int64 `json:"expected"`
Grant Grant `json:"grant"`
}
type Capabilities struct {
AutomaticGrants bool `json:"automatic_grants"`
Protocol string `json:"protocol"`
Scope string `json:"scope"`
Ready bool `json:"ready"`
}
type HTTPServer struct {
Gateway *Gateway
DataToken, ControlToken string
}
func (s HTTPServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
token := s.DataToken
if r.URL.Path == "/v1/advance" || r.URL.Path == "/v1/plans" {
token = s.ControlToken
}
if len(token) < 32 || s.DataToken == s.ControlToken || subtle.ConstantTimeCompare([]byte(r.Header.Get("Authorization")), []byte("Bearer "+token)) != 1 {
http.Error(w, "gateway_unauthorized", 401)
return
}
if r.Header.Get("X-KG-Gateway-Protocol") != Protocol || r.Header.Get("X-KG-Gateway-Scope") != s.Gateway.Scope {
http.Error(w, "gateway_protocol_or_target_mismatch", 409)
return
}
fail := func(e error) {
code := 503
if errors.Is(e, ErrEpoch) || errors.Is(e, ErrConflict) || errors.Is(e, ErrHeld) {
code = 409
}
http.Error(w, e.Error(), code)
}
decode := func(v any) bool {
r.Body = http.MaxBytesReader(w, r.Body, s.Gateway.MaxBytes*2+16384)
d := json.NewDecoder(r.Body)
d.DisallowUnknownFields()
if d.Decode(v) != nil || d.Decode(&struct{}{}) != io.EOF {
http.Error(w, "gateway_bad_request", 400)
return false
}
return true
}
if r.Method == "GET" && r.URL.Path == "/v1/capabilities" {
if e := s.Gateway.Ready(r.Context()); e != nil {
fail(e)
return
}
if authority, ok := s.Gateway.Authority.(interface{ Ready(context.Context) error }); ok {
if e := authority.Ready(r.Context()); e != nil {
fail(e)
return
}
}
_ = json.NewEncoder(w).Encode(Capabilities{Protocol: Protocol, Scope: s.Gateway.Scope, Ready: true, AutomaticGrants: s.Gateway.Authority != nil})
return
}
if r.Method != "POST" {
http.Error(w, "gateway_route_not_found", 404)
return
}
switch r.URL.Path {
case "/v1/acquire":
var in Claim
if !decode(&in) {
return
}
out, e := s.Gateway.Acquire(r.Context(), in)
if e != nil {
fail(e)
return
}
_ = json.NewEncoder(w).Encode(out)
case "/v1/plans":
if s.Gateway.Authority != nil {
fail(ErrConflict)
return
}
var in Plan
if !decode(&in) {
return
}
if e := s.Gateway.InstallPlan(r.Context(), in); e != nil {
fail(e)
return
}
_ = json.NewEncoder(w).Encode(in)
case "/v1/advance":
if s.Gateway.Authority != nil {
fail(ErrConflict)
return
}
var in AdvanceRequest
if !decode(&in) {
return
}
if e := s.Gateway.Advance(r.Context(), in.Expected, in.Grant); e != nil {
fail(e)
return
}
_ = json.NewEncoder(w).Encode(in)
case "/v1/execute", "/v1/reconcile":
var in ExecuteRequest
if !decode(&in) {
return
}
var out Receipt
var e error
if r.URL.Path == "/v1/execute" {
if in.Authorization != nil {
out, e = s.Gateway.ExecuteAuthorized(r.Context(), *in.Authorization, in.Request, in.Body)
} else {
out, e = s.Gateway.Execute(r.Context(), in.Request, in.Body)
}
} else {
out, e = s.Gateway.Reconcile(r.Context(), in.Request)
}
if e != nil {
fail(e)
return
}
_ = json.NewEncoder(w).Encode(out)
case "/v1/resolve":
var in Request
if !decode(&in) {
return
}
out, e := s.Gateway.Resolve(r.Context(), in)
if e != nil {
fail(e)
return
}
_ = json.NewEncoder(w).Encode(out)
case "/v1/observe":
var in struct {
Key string `json:"key"`
Version string `json:"version"`
}
if !decode(&in) {
return
}
if in.Key == "" {
http.Error(w, "gateway_bad_key", 400)
return
}
if e := s.Gateway.Ready(r.Context()); e != nil {
fail(e)
return
}
v, exists, e := s.Gateway.Backend.Observe(r.Context(), in.Key, in.Version)
if e != nil {
http.Error(w, "gateway_backend_read_failed", 503)
return
}
_ = json.NewEncoder(w).Encode(struct {
Version Version `json:"version"`
Exists bool `json:"exists"`
}{v, exists})
default:
http.Error(w, "gateway_route_not_found", 404)
}
}
type Client struct {
Endpoint, Token, Scope string
HTTP *http.Client
}
func NewClient(endpoint, token, scope string) (*Client, error) {
u, e := url.Parse(endpoint)
if e != nil || u.Host == "" || u.User != nil || u.RawQuery != "" || u.Fragment != "" || u.Path != "" || len(token) < 32 || scope == "" {
return nil, errors.New("gateway_configuration_invalid")
}
if u.Scheme != "https" && (u.Scheme != "http" || (u.Hostname() != "127.0.0.1" && u.Hostname() != "::1")) {
return nil, errors.New("gateway_transport_requires_tls")
}
return &Client{endpoint, token, scope, &http.Client{Timeout: 2 * time.Minute, CheckRedirect: func(*http.Request, []*http.Request) error { return errors.New("gateway_redirect_refused") }}}, nil
}
func (c *Client) call(ctx context.Context, path string, in, out any) error {
method := "GET"
var body io.Reader
if in != nil {
raw, e := json.Marshal(in)
if e != nil {
return ErrConflict
}
method = "POST"
body = struct{ io.Reader }{bytes.NewReader(raw)}
}
req, e := http.NewRequestWithContext(ctx, method, c.Endpoint+path, body)
if e != nil {
return errors.New("gateway_request_invalid")
}
req.Header.Set("Authorization", "Bearer "+c.Token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-KG-Gateway-Protocol", Protocol)
req.Header.Set("X-KG-Gateway-Scope", c.Scope)
resp, e := c.HTTP.Do(req)
if e != nil {
return errors.New("gateway_unreachable_or_outcome_unknown")
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
raw, _ := io.ReadAll(io.LimitReader(resp.Body, 128))
reason := strings.TrimSpace(string(raw))
switch reason {
case ErrAuthority.Error():
return ErrAuthority
case ErrAuthorityPrivileges.Error():
return ErrAuthorityPrivileges
case ErrEpoch.Error():
return ErrEpoch
case ErrConflict.Error():
return ErrConflict
case ErrHeld.Error():
return ErrHeld
case ErrPersistence.Error():
return ErrPersistence
case ErrIsolation.Error():
return ErrIsolation
}
return errors.New("gateway_request_refused")
}
if json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(out) != nil {
return errors.New("gateway_response_invalid")
}
return nil
}
func (c *Client) Ready(ctx context.Context) error { return c.checkReady(ctx, false) }
func (c *Client) ReadyForAcquisition(ctx context.Context) error { return c.checkReady(ctx, true) }
func (c *Client) checkReady(ctx context.Context, automatic bool) error {
var caps Capabilities
if e := c.call(ctx, "/v1/capabilities", nil, &caps); e != nil {
return e
}
if automatic && !caps.AutomaticGrants {
return ErrAuthority
}
if caps.Protocol != Protocol || caps.Scope != c.Scope || !caps.Ready {
return errors.New("gateway_protocol_or_target_mismatch")
}
return nil
}
func (c *Client) InstallPlan(ctx context.Context, p Plan) error {
var out Plan
if e := c.call(ctx, "/v1/plans", p, &out); e != nil {
return e
}
want, _ := json.Marshal(p)
got, _ := json.Marshal(out)
if string(want) != string(got) {
return ErrConflict
}
return nil
}
func (c *Client) Advance(ctx context.Context, expected int64, g Grant) error {
var out AdvanceRequest
in := AdvanceRequest{expected, g}
if e := c.call(ctx, "/v1/advance", in, &out); e != nil {
return e
}
if in != out {
return ErrConflict
}
return nil
}
func (c *Client) Execute(ctx context.Context, r Request, body []byte) (Receipt, error) {
var out Receipt
e := c.call(ctx, "/v1/execute", ExecuteRequest{Request: r, Body: body}, &out)
if e == nil && (out.Request != r || !out.valid()) {
e = ErrConflict
}
return out, e
}
func (c *Client) Reconcile(ctx context.Context, r Request) (Receipt, error) {
var out Receipt
e := c.call(ctx, "/v1/reconcile", ExecuteRequest{Request: r}, &out)
if e == nil && (out.Request != r || !out.valid()) {
e = ErrConflict
}
return out, e
}
func (c *Client) Observe(ctx context.Context, key, id string) (Version, bool, error) {
var out struct {
Version Version `json:"version"`
Exists bool `json:"exists"`
}
e := c.call(ctx, "/v1/observe", struct {
Key string `json:"key"`
Version string `json:"version"`
}{key, id}, &out)
return out.Version, out.Exists, e
}