рабочий вариант, но скороть 10 МБит
This commit is contained in:
@@ -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}}
|
||||
}
|
||||
Reference in New Issue
Block a user