Files
m 20d361a886
build / backend (push) Has been cancelled
build / node-agent (push) Has been cancelled
build / worker (push) Has been cancelled
рабочий вариант, но скороть 10 МБит
2026-05-22 21:46:49 +03:00

232 lines
6.2 KiB
Go

package webingress
import (
"context"
"encoding/json"
"io"
"net/http"
"strings"
"time"
)
type RuntimeConfig struct {
ServiceType string
Scope string
ServiceClasses []string
TLSMode string
HTTPSPort int
}
type Runtime struct {
Config RuntimeConfig
Binder FabricBinder
Now func() time.Time
}
type FabricBinder interface {
Forward(ctx context.Context, request FabricRequest) (FabricResponse, error)
}
type FabricRequest struct {
SchemaVersion string
Method string
Path string
Query string
Host string
ServiceType string
Scope string
ServiceClass string
Headers http.Header
Body []byte
ObservedAt time.Time
}
type FabricResponse struct {
StatusCode int
Headers http.Header
Body []byte
}
type Response struct {
SchemaVersion string `json:"schema_version"`
Status string `json:"status"`
Reason string `json:"reason,omitempty"`
ServiceType string `json:"service_type,omitempty"`
Scope string `json:"scope,omitempty"`
ServiceClass string `json:"service_class,omitempty"`
Allowed []string `json:"allowed_service_classes,omitempty"`
ObservedAt string `json:"observed_at"`
}
func (r Runtime) HTTPSHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if req.URL.Path == "/healthz" || req.URL.Path == "/readyz" {
writeJSON(w, http.StatusOK, r.response("ready", "https_runtime_ready", ""))
return
}
serviceClass := strings.TrimSpace(req.Header.Get("X-RAP-Service-Class"))
if serviceClass == "" {
serviceClass = serviceClassFromPath(req.URL.Path)
}
if serviceClass == "" {
writeJSON(w, http.StatusBadRequest, r.response("blocked", "service_class_required", ""))
return
}
if !contains(r.Config.ServiceClasses, serviceClass) {
writeJSON(w, http.StatusForbidden, r.response("blocked", "service_class_not_allowed", serviceClass))
return
}
if r.Binder == nil {
writeJSON(w, http.StatusNotImplemented, r.response("blocked", "fabric_service_channel_binding_not_implemented", serviceClass))
return
}
scope := scopeForServiceClass(serviceClass, req.URL.Path, r.Config.Scope)
body, err := io.ReadAll(http.MaxBytesReader(w, req.Body, 1<<20))
if err != nil {
writeJSON(w, http.StatusRequestEntityTooLarge, r.response("blocked", "request_body_too_large", serviceClass))
return
}
now := time.Now().UTC()
if r.Now != nil {
now = r.Now().UTC()
}
fabricResponse, err := r.Binder.Forward(req.Context(), FabricRequest{
SchemaVersion: "rap.web_ingress.fabric_request.v1",
Method: req.Method,
Path: req.URL.Path,
Query: req.URL.RawQuery,
Host: req.Host,
ServiceType: strings.TrimSpace(r.Config.ServiceType),
Scope: scope,
ServiceClass: serviceClass,
Headers: cloneSafeHeaders(req.Header),
Body: body,
ObservedAt: now,
})
if err != nil {
writeJSON(w, http.StatusBadGateway, r.response("blocked", "fabric_service_channel_forward_failed", serviceClass))
return
}
writeFabricResponse(w, fabricResponse)
})
}
func (r Runtime) response(status, reason, serviceClass string) Response {
now := time.Now().UTC()
if r.Now != nil {
now = r.Now().UTC()
}
return Response{
SchemaVersion: "rap.web_ingress.runtime_response.v1",
Status: status,
Reason: reason,
ServiceType: strings.TrimSpace(r.Config.ServiceType),
Scope: strings.TrimSpace(r.Config.Scope),
ServiceClass: serviceClass,
Allowed: append([]string{}, r.Config.ServiceClasses...),
ObservedAt: now.Format(time.RFC3339Nano),
}
}
func scopeForServiceClass(serviceClass string, path string, fallback string) string {
path = strings.Trim(strings.ToLower(path), "/")
switch strings.TrimSpace(serviceClass) {
case "admin-ingress":
if strings.HasPrefix(path, "clusters/") {
return "cluster"
}
return firstNonEmpty(strings.TrimSpace(fallback), "platform")
case "public-ingress":
if strings.HasPrefix(path, "users/") {
return "user"
}
return firstNonEmpty(strings.TrimSpace(fallback), "organization")
default:
return strings.TrimSpace(fallback)
}
}
func firstNonEmpty(value string, fallback string) string {
if strings.TrimSpace(value) != "" {
return strings.TrimSpace(value)
}
return strings.TrimSpace(fallback)
}
func serviceClassFromPath(path string) string {
path = strings.Trim(strings.ToLower(path), "/")
switch {
case strings.HasPrefix(path, "admin/"), strings.HasPrefix(path, "platform/"), strings.HasPrefix(path, "clusters/"):
return "admin-ingress"
case strings.HasPrefix(path, "public/"), strings.HasPrefix(path, "organizations/"), strings.HasPrefix(path, "users/"):
return "public-ingress"
default:
return ""
}
}
func writeJSON(w http.ResponseWriter, status int, payload Response) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(payload)
}
func writeFabricResponse(w http.ResponseWriter, payload FabricResponse) {
for key, values := range payload.Headers {
if !safeResponseHeader(key) {
continue
}
for _, value := range values {
w.Header().Add(key, value)
}
}
w.Header().Set("Cache-Control", "no-store")
status := payload.StatusCode
if status < 100 || status > 599 {
status = http.StatusOK
}
w.WriteHeader(status)
_, _ = w.Write(payload.Body)
}
func cloneSafeHeaders(headers http.Header) http.Header {
out := http.Header{}
for key, values := range headers {
if !safeRequestHeader(key) {
continue
}
for _, value := range values {
out.Add(key, value)
}
}
return out
}
func safeRequestHeader(key string) bool {
switch strings.ToLower(strings.TrimSpace(key)) {
case "authorization", "cookie", "set-cookie", "x-rap-service-channel-token":
return false
default:
return true
}
}
func safeResponseHeader(key string) bool {
switch strings.ToLower(strings.TrimSpace(key)) {
case "set-cookie", "transfer-encoding", "connection":
return false
default:
return true
}
}
func contains(values []string, needle string) bool {
for _, value := range values {
if value == needle {
return true
}
}
return false
}