рабочий вариант, но скороть 10 МБит
This commit is contained in:
@@ -22,7 +22,7 @@ import (
|
||||
|
||||
const (
|
||||
ModeStrict = "strict"
|
||||
ModeLegacy = "legacy"
|
||||
ModeCompat = "compat"
|
||||
|
||||
ActivationSchemaVersion = "rap.installation.activation.v1"
|
||||
|
||||
@@ -60,7 +60,7 @@ type Verifier struct {
|
||||
func NewVerifier(cfg config.InstallationConfig) (*Verifier, error) {
|
||||
mode := strings.ToLower(strings.TrimSpace(cfg.AuthorityMode))
|
||||
if mode == "" {
|
||||
mode = ModeLegacy
|
||||
mode = ModeCompat
|
||||
}
|
||||
verifier := &Verifier{
|
||||
mode: mode,
|
||||
@@ -69,7 +69,7 @@ func NewVerifier(cfg config.InstallationConfig) (*Verifier, error) {
|
||||
}
|
||||
|
||||
switch mode {
|
||||
case ModeLegacy:
|
||||
case ModeCompat:
|
||||
return verifier, nil
|
||||
case ModeStrict:
|
||||
publicKey, err := decodeEd25519PublicKey(cfg.ProductRootPublicKeyBase64)
|
||||
@@ -87,7 +87,7 @@ func NewVerifier(cfg config.InstallationConfig) (*Verifier, error) {
|
||||
|
||||
func (v *Verifier) Mode() string {
|
||||
if v == nil || v.mode == "" {
|
||||
return ModeLegacy
|
||||
return ModeCompat
|
||||
}
|
||||
return v.mode
|
||||
}
|
||||
@@ -162,7 +162,7 @@ func EffectivePlatformRole(ctx context.Context, db postgresplatform.DBTX, verifi
|
||||
return PlatformRoleUser, nil
|
||||
}
|
||||
if verifier == nil || !verifier.Strict() {
|
||||
return legacyPlatformRole(ctx, db, userID)
|
||||
return storedPlatformRole(ctx, db, userID)
|
||||
}
|
||||
|
||||
var email string
|
||||
@@ -220,7 +220,7 @@ END, prg.granted_at DESC
|
||||
} else if ok {
|
||||
return role, nil
|
||||
}
|
||||
return legacyPlatformRole(ctx, db, userID)
|
||||
return storedPlatformRole(ctx, db, userID)
|
||||
}
|
||||
return bestRole, nil
|
||||
}
|
||||
@@ -257,7 +257,7 @@ WHERE u.id = $1::uuid
|
||||
}
|
||||
}
|
||||
|
||||
func legacyPlatformRole(ctx context.Context, db postgresplatform.DBTX, userID string) (string, error) {
|
||||
func storedPlatformRole(ctx context.Context, db postgresplatform.DBTX, userID string) (string, error) {
|
||||
var role string
|
||||
if err := db.QueryRow(ctx, `SELECT platform_role FROM users WHERE id = $1::uuid`, userID).Scan(&role); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
|
||||
@@ -10,17 +10,18 @@ import (
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
App AppConfig
|
||||
HTTP HTTPConfig
|
||||
Postgres PostgresConfig
|
||||
Redis RedisConfig
|
||||
Auth AuthConfig
|
||||
Installation InstallationConfig
|
||||
DataPlane DataPlaneConfig
|
||||
Secret SecretConfig
|
||||
Session SessionConfig
|
||||
Worker WorkerConfig
|
||||
WebSocket WebSocketConfig
|
||||
App AppConfig
|
||||
HTTP HTTPConfig
|
||||
FabricControl FabricControlConfig
|
||||
Postgres PostgresConfig
|
||||
Redis RedisConfig
|
||||
Auth AuthConfig
|
||||
Installation InstallationConfig
|
||||
DataPlane DataPlaneConfig
|
||||
Secret SecretConfig
|
||||
Session SessionConfig
|
||||
Worker WorkerConfig
|
||||
WebSocket WebSocketConfig
|
||||
}
|
||||
|
||||
type AppConfig struct {
|
||||
@@ -37,6 +38,11 @@ type HTTPConfig struct {
|
||||
ShutdownTimeout time.Duration
|
||||
}
|
||||
|
||||
type FabricControlConfig struct {
|
||||
Enabled bool
|
||||
ListenAddr string
|
||||
}
|
||||
|
||||
type PostgresConfig struct {
|
||||
DSN string
|
||||
MaxConns int32
|
||||
@@ -118,6 +124,10 @@ func Load() (Config, error) {
|
||||
IdleTimeout: getDuration("HTTP_IDLE_TIMEOUT", 60*time.Second),
|
||||
ShutdownTimeout: getDuration("HTTP_SHUTDOWN_TIMEOUT", 10*time.Second),
|
||||
},
|
||||
FabricControl: FabricControlConfig{
|
||||
Enabled: getBool("FABRIC_CONTROL_QUIC_ENABLED", false),
|
||||
ListenAddr: getEnv("FABRIC_CONTROL_QUIC_LISTEN_ADDR", ":19191"),
|
||||
},
|
||||
Postgres: PostgresConfig{
|
||||
DSN: getEnv("POSTGRES_DSN", ""),
|
||||
MaxConns: int32(getInt("POSTGRES_MAX_CONNS", 20)),
|
||||
@@ -235,13 +245,13 @@ func Load() (Config, error) {
|
||||
func normalizeInstallationAuthorityMode(mode string, rootPublicKey string) string {
|
||||
mode = strings.ToLower(strings.TrimSpace(mode))
|
||||
switch mode {
|
||||
case "strict", "legacy":
|
||||
case "strict", "compat":
|
||||
return mode
|
||||
case "":
|
||||
if strings.TrimSpace(rootPublicKey) != "" {
|
||||
return "strict"
|
||||
}
|
||||
return "legacy"
|
||||
return "compat"
|
||||
default:
|
||||
return mode
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package fabriccontrol
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
const (
|
||||
frameMagic uint32 = 0x52415046
|
||||
frameVersion uint8 = 1
|
||||
frameHeaderSize = 32
|
||||
maxPayload = 1024 * 1024
|
||||
|
||||
frameData uint8 = 5
|
||||
trafficClassReliable uint8 = 4
|
||||
controlForwardQUICStream uint64 = 3
|
||||
)
|
||||
|
||||
type frame struct {
|
||||
Type uint8
|
||||
TrafficClass uint8
|
||||
StreamID uint64
|
||||
Sequence uint64
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
var errInvalidFrame = errors.New("invalid fabric frame")
|
||||
|
||||
func readFrame(r io.Reader) (frame, error) {
|
||||
header := make([]byte, frameHeaderSize)
|
||||
if _, err := io.ReadFull(r, header); err != nil {
|
||||
return frame{}, err
|
||||
}
|
||||
if binary.BigEndian.Uint32(header[0:4]) != frameMagic || header[4] != frameVersion {
|
||||
return frame{}, errInvalidFrame
|
||||
}
|
||||
payloadLen := int(binary.BigEndian.Uint32(header[28:32]))
|
||||
if payloadLen > maxPayload {
|
||||
return frame{}, fmt.Errorf("%w: payload too large", errInvalidFrame)
|
||||
}
|
||||
out := frame{
|
||||
Type: header[5],
|
||||
TrafficClass: header[8],
|
||||
StreamID: binary.BigEndian.Uint64(header[12:20]),
|
||||
Sequence: binary.BigEndian.Uint64(header[20:28]),
|
||||
}
|
||||
if payloadLen > 0 {
|
||||
out.Payload = make([]byte, payloadLen)
|
||||
if _, err := io.ReadFull(r, out.Payload); err != nil {
|
||||
return frame{}, err
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func writeFrame(w io.Writer, f frame) error {
|
||||
if len(f.Payload) > maxPayload {
|
||||
return fmt.Errorf("%w: payload too large", errInvalidFrame)
|
||||
}
|
||||
header := make([]byte, frameHeaderSize)
|
||||
binary.BigEndian.PutUint32(header[0:4], frameMagic)
|
||||
header[4] = frameVersion
|
||||
header[5] = f.Type
|
||||
header[8] = f.TrafficClass
|
||||
binary.BigEndian.PutUint64(header[12:20], f.StreamID)
|
||||
binary.BigEndian.PutUint64(header[20:28], f.Sequence)
|
||||
binary.BigEndian.PutUint32(header[28:32], uint32(len(f.Payload)))
|
||||
if _, err := w.Write(header); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(f.Payload) == 0 {
|
||||
return nil
|
||||
}
|
||||
_, err := w.Write(f.Payload)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
package fabriccontrol
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
)
|
||||
|
||||
const nextProto = "rap-fabric-data-session-v1"
|
||||
|
||||
type Config struct {
|
||||
Enabled bool
|
||||
ListenAddr string
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
cfg Config
|
||||
router http.Handler
|
||||
ln *quic.Listener
|
||||
}
|
||||
|
||||
type rawControlRequest struct {
|
||||
Method string `json:"method"`
|
||||
Path string `json:"path"`
|
||||
Body json.RawMessage `json:"body,omitempty"`
|
||||
}
|
||||
|
||||
type rawControlResponse struct {
|
||||
StatusCode int `json:"status_code"`
|
||||
Body json.RawMessage `json:"body,omitempty"`
|
||||
}
|
||||
|
||||
type controlEnvelope struct {
|
||||
Payload json.RawMessage `json:"payload,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func New(cfg Config, router http.Handler) *Server {
|
||||
return &Server{cfg: cfg, router: router}
|
||||
}
|
||||
|
||||
func (s *Server) ListenAndServe(ctx context.Context) error {
|
||||
if s == nil || !s.cfg.Enabled {
|
||||
return nil
|
||||
}
|
||||
listenAddr := strings.TrimSpace(s.cfg.ListenAddr)
|
||||
if listenAddr == "" {
|
||||
listenAddr = ":19191"
|
||||
}
|
||||
ln, err := quic.ListenAddr(listenAddr, selfSignedTLSConfig(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.ln = ln
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
_ = ln.Close()
|
||||
}()
|
||||
for {
|
||||
conn, err := ln.Accept(ctx)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
go s.handleConn(ctx, conn)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Close() error {
|
||||
if s == nil || s.ln == nil {
|
||||
return nil
|
||||
}
|
||||
return s.ln.Close()
|
||||
}
|
||||
|
||||
func (s *Server) handleConn(ctx context.Context, conn *quic.Conn) {
|
||||
for {
|
||||
stream, err := conn.AcceptStream(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go s.handleStream(ctx, stream)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleStream(ctx context.Context, stream *quic.Stream) {
|
||||
defer stream.Close()
|
||||
for {
|
||||
reqFrame, err := readFrame(stream)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if reqFrame.Type != frameData || reqFrame.StreamID != controlForwardQUICStream {
|
||||
continue
|
||||
}
|
||||
payload, err := s.handlePayload(ctx, reqFrame.Payload)
|
||||
envelope := controlEnvelope{Payload: payload}
|
||||
if err != nil {
|
||||
envelope = controlEnvelope{Error: err.Error()}
|
||||
}
|
||||
raw, _ := json.Marshal(envelope)
|
||||
_ = writeFrame(stream, frame{
|
||||
Type: frameData,
|
||||
TrafficClass: trafficClassReliable,
|
||||
StreamID: controlForwardQUICStream,
|
||||
Sequence: reqFrame.Sequence,
|
||||
Payload: raw,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handlePayload(ctx context.Context, payload []byte) (json.RawMessage, error) {
|
||||
var req rawControlRequest
|
||||
if err := json.Unmarshal(payload, &req); err != nil {
|
||||
return nil, fmt.Errorf("invalid fabric control request")
|
||||
}
|
||||
method := strings.ToUpper(strings.TrimSpace(req.Method))
|
||||
if method == "" {
|
||||
method = http.MethodGet
|
||||
}
|
||||
path := normalizeControlPath(req.Path)
|
||||
if path == "" {
|
||||
return nil, fmt.Errorf("fabric control path is not allowed")
|
||||
}
|
||||
httpReq := httptest.NewRequest(method, path, bytes.NewReader(req.Body)).WithContext(ctx)
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("X-RAP-Fabric-Control", "quic")
|
||||
rec := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(rec, httpReq)
|
||||
body := append(json.RawMessage(nil), rec.Body.Bytes()...)
|
||||
raw, err := json.Marshal(rawControlResponse{StatusCode: rec.Code, Body: body})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
func normalizeControlPath(path string) string {
|
||||
path = strings.TrimSpace(path)
|
||||
if path == "" || strings.Contains(path, "://") || strings.Contains(path, "..") {
|
||||
return ""
|
||||
}
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
if strings.HasPrefix(path, "/api/v1/") {
|
||||
return path
|
||||
}
|
||||
switch {
|
||||
case strings.HasPrefix(path, "/clusters/"),
|
||||
strings.HasPrefix(path, "/organizations/"),
|
||||
strings.HasPrefix(path, "/node-agents/"),
|
||||
strings.HasPrefix(path, "/auth/"):
|
||||
return "/api/v1" + path
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func selfSignedTLSConfig() *tls.Config {
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
tmpl := x509.Certificate{
|
||||
SerialNumber: big.NewInt(time.Now().UnixNano()),
|
||||
Subject: pkix.Name{CommonName: "rap-fabric-control"},
|
||||
NotBefore: time.Now().Add(-time.Hour),
|
||||
NotAfter: time.Now().Add(365 * 24 * time.Hour),
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
}
|
||||
der, err := x509.CreateCertificate(rand.Reader, &tmpl, &tmpl, &key.PublicKey, key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
cert := tls.Certificate{Certificate: [][]byte{der}, PrivateKey: key}
|
||||
return &tls.Config{Certificates: []tls.Certificate{cert}, NextProtos: []string{nextProto}}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/example/remote-access-platform/backend/internal/modules/worker"
|
||||
"github.com/example/remote-access-platform/backend/internal/platform/authority"
|
||||
"github.com/example/remote-access-platform/backend/internal/platform/config"
|
||||
"github.com/example/remote-access-platform/backend/internal/platform/fabriccontrol"
|
||||
"github.com/example/remote-access-platform/backend/internal/platform/httpserver"
|
||||
"github.com/example/remote-access-platform/backend/internal/platform/logging"
|
||||
"github.com/example/remote-access-platform/backend/internal/platform/module"
|
||||
@@ -33,12 +34,13 @@ import (
|
||||
)
|
||||
|
||||
type App struct {
|
||||
cfg config.Config
|
||||
logger *slog.Logger
|
||||
httpServer *http.Server
|
||||
workers []backgroundRunner
|
||||
db closeFunc
|
||||
redis closeFunc
|
||||
cfg config.Config
|
||||
logger *slog.Logger
|
||||
httpServer *http.Server
|
||||
fabricControl *fabriccontrol.Server
|
||||
workers []backgroundRunner
|
||||
db closeFunc
|
||||
redis closeFunc
|
||||
}
|
||||
|
||||
type closeFunc func() error
|
||||
@@ -138,7 +140,11 @@ func NewApp(ctx context.Context) (*App, error) {
|
||||
cfg: cfg,
|
||||
logger: logger,
|
||||
httpServer: httpserver.New(cfg.HTTP, router),
|
||||
workers: []backgroundRunner{workerEvents.Run, leaseMonitor.Run},
|
||||
fabricControl: fabriccontrol.New(fabriccontrol.Config{
|
||||
Enabled: cfg.FabricControl.Enabled,
|
||||
ListenAddr: cfg.FabricControl.ListenAddr,
|
||||
}, router),
|
||||
workers: []backgroundRunner{workerEvents.Run, leaseMonitor.Run},
|
||||
db: func() error {
|
||||
db.Close()
|
||||
return nil
|
||||
@@ -167,6 +173,14 @@ func (a *App) Run(ctx context.Context) error {
|
||||
}
|
||||
}()
|
||||
}
|
||||
if a.fabricControl != nil && a.cfg.FabricControl.Enabled {
|
||||
go func() {
|
||||
a.logger.Info("fabric control quic starting", "addr", a.cfg.FabricControl.ListenAddr, "service", a.cfg.App.Name)
|
||||
if err := a.fabricControl.ListenAndServe(ctx); err != nil {
|
||||
errCh <- err
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -184,6 +198,9 @@ func (a *App) Run(ctx context.Context) error {
|
||||
if err := a.httpServer.Shutdown(shutdownCtx); err != nil {
|
||||
return fmt.Errorf("shutdown http server: %w", err)
|
||||
}
|
||||
if a.fabricControl != nil {
|
||||
_ = a.fabricControl.Close()
|
||||
}
|
||||
|
||||
if err := a.redis(); err != nil {
|
||||
return fmt.Errorf("close redis: %w", err)
|
||||
|
||||
Reference in New Issue
Block a user