migrate from github
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.log
|
||||
*.local
|
||||
.DS_Store
|
||||
.git
|
||||
.vscode
|
||||
.idea
|
||||
pkg/gen/
|
||||
@@ -0,0 +1,8 @@
|
||||
FROM golang:1.24-alpine AS dev
|
||||
WORKDIR /app
|
||||
|
||||
ENV GOPROXY=https://goproxy.cn,direct
|
||||
|
||||
EXPOSE 1234
|
||||
|
||||
CMD ["sh", "-c", "go mod download && go run main.go"]
|
||||
@@ -0,0 +1,31 @@
|
||||
FROM golang:1.24-alpine AS builder
|
||||
WORKDIR /app
|
||||
|
||||
ENV GOPROXY=https://goproxy.cn,direct
|
||||
|
||||
RUN apk add --no-cache protobuf
|
||||
RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.5 && \
|
||||
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.5.1
|
||||
|
||||
COPY collab-go/go.mod collab-go/go.sum ./
|
||||
COPY collab-go ./collab-go
|
||||
COPY proto ./proto
|
||||
|
||||
WORKDIR /app/collab-go
|
||||
RUN go mod download
|
||||
RUN mkdir -p /app/collab-go/pkg/gen && \
|
||||
protoc -I /app/proto \
|
||||
--go_out=/app/collab-go/pkg/gen --go_opt=paths=source_relative \
|
||||
--go-grpc_out=/app/collab-go/pkg/gen --go-grpc_opt=paths=source_relative \
|
||||
/app/proto/yoresee_doc/v1/yoresee_doc.proto
|
||||
RUN go mod tidy
|
||||
RUN CGO_ENABLED=0 go build -p 1 -o collab-go main.go
|
||||
|
||||
FROM alpine:latest AS runner
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/collab-go .
|
||||
|
||||
EXPOSE 1234
|
||||
|
||||
CMD ["./collab-go"]
|
||||
@@ -0,0 +1,34 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
type Authenticator struct {
|
||||
secret string
|
||||
}
|
||||
|
||||
func NewAuthenticator(secret string) *Authenticator {
|
||||
return &Authenticator{
|
||||
secret: secret,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Authenticator) ValidateToken(tokenString string) error {
|
||||
if tokenString == "" {
|
||||
return http.ErrNoCookie
|
||||
}
|
||||
|
||||
if a.secret != "" {
|
||||
_, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(a.secret), nil
|
||||
}, jwt.WithValidMethods([]string{"HS256", "HS384", "HS512"}))
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: jwt redis support
|
||||
_, _, err := new(jwt.Parser).ParseUnverified(tokenString, jwt.MapClaims{})
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package config
|
||||
|
||||
import "github.com/caarlos0/env/v11"
|
||||
|
||||
type Config struct {
|
||||
Addr string `env:"ADDR" envDefault:":1234"`
|
||||
JWTSecret string `env:"JWT_SECRET"`
|
||||
CollabCoreURL string `env:"COLLAB_CORE_URL" envDefault:"ws://collab-core:1234"`
|
||||
BackendGRPCAddr string `env:"BACKEND_GRPC_ADDR" envDefault:"backend:9090"`
|
||||
InternalRPCKey string `env:"INTERNAL_RPC_KEY"`
|
||||
}
|
||||
|
||||
func Load() (*Config, error) {
|
||||
cfg := &Config{}
|
||||
if err := env.Parse(cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
module github.com/XingfenD/yoresee_doc/collab-go
|
||||
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.1
|
||||
|
||||
require (
|
||||
github.com/caarlos0/env/v11 v11.4.0
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
google.golang.org/grpc v1.79.2
|
||||
google.golang.org/protobuf v1.36.10
|
||||
)
|
||||
|
||||
require (
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||
)
|
||||
@@ -0,0 +1,44 @@
|
||||
github.com/caarlos0/env/v11 v11.4.0 h1:Kcb6t5kIIr4XkoQC9AF2j+8E1Jsrl3Wz/hhm1LtoGAc=
|
||||
github.com/caarlos0/env/v11 v11.4.0/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
|
||||
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"google.golang.org/grpc/metadata"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/collab-go/auth"
|
||||
yoreseedocpb "github.com/XingfenD/yoresee_doc/collab-go/pkg/gen/yoresee_doc/v1"
|
||||
"github.com/XingfenD/yoresee_doc/collab-go/proxy"
|
||||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
}
|
||||
|
||||
type WSHandler struct {
|
||||
authenticator *auth.Authenticator
|
||||
proxy *proxy.Proxy
|
||||
documentService yoreseedocpb.DocumentServiceClient
|
||||
internalRPCKey string
|
||||
}
|
||||
|
||||
func NewWSHandler(authenticator *auth.Authenticator, proxy *proxy.Proxy, documentService yoreseedocpb.DocumentServiceClient, internalRPCKey string) *WSHandler {
|
||||
return &WSHandler{
|
||||
authenticator: authenticator,
|
||||
proxy: proxy,
|
||||
documentService: documentService,
|
||||
internalRPCKey: internalRPCKey,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *WSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
docID := strings.TrimPrefix(r.URL.Path, "/ws/doc/")
|
||||
if docID == "" || strings.Contains(docID, "/") {
|
||||
http.Error(w, "invalid doc id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := authenticateRequest(r, h.authenticator); err != nil {
|
||||
log.Printf("collab-gateway unauthorized path=%s remote=%s err=%v", r.URL.Path, r.RemoteAddr, err)
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if code, err := checkDocumentExists(r.Context(), h.documentService, h.internalRPCKey, docID); err != nil {
|
||||
log.Printf("collab-gateway doc check failed docID=%s err=%v", docID, err)
|
||||
http.Error(w, err.Error(), code)
|
||||
return
|
||||
}
|
||||
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Printf("collab-gateway upgrade failed path=%s remote=%s err=%v", r.URL.Path, r.RemoteAddr, err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
coreConn, err := h.proxy.DialCore(docID)
|
||||
if err != nil {
|
||||
log.Printf("collab-gateway dial core failed docID=%s err=%v", docID, err)
|
||||
return
|
||||
}
|
||||
defer coreConn.Close()
|
||||
|
||||
errCh := make(chan error, 2)
|
||||
go h.proxy.ProxyWS(conn, coreConn, errCh)
|
||||
go h.proxy.ProxyWS(coreConn, conn, errCh)
|
||||
|
||||
<-errCh
|
||||
}
|
||||
|
||||
func authenticateRequest(r *http.Request, authenticator *auth.Authenticator) error {
|
||||
token := r.URL.Query().Get("token")
|
||||
return authenticator.ValidateToken(token)
|
||||
}
|
||||
|
||||
func checkDocumentExists(ctx context.Context, documentService yoreseedocpb.DocumentServiceClient, internalRPCKey, docID string) (int, error) {
|
||||
if documentService == nil {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
md := metadata.New(map[string]string{})
|
||||
if internalRPCKey != "" {
|
||||
md.Set("x-internal-key", internalRPCKey)
|
||||
}
|
||||
ctx = metadata.NewOutgoingContext(ctx, md)
|
||||
|
||||
resp, err := documentService.GetDocumentSettings(ctx, &yoreseedocpb.GetDocumentSettingsRequest{
|
||||
DocumentExternalId: docID,
|
||||
})
|
||||
if err != nil {
|
||||
return http.StatusServiceUnavailable, fmt.Errorf("service unavailable")
|
||||
}
|
||||
if resp.GetBase().GetCode() != 0 {
|
||||
log.Printf("collab-gateway doc not found docID=%s code=%d msg=%s", docID, resp.GetBase().GetCode(), resp.GetBase().GetMessage())
|
||||
return http.StatusNotFound, fmt.Errorf("document not found")
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
yoreseedocpb "github.com/XingfenD/yoresee_doc/collab-go/pkg/gen/yoresee_doc/v1"
|
||||
)
|
||||
|
||||
type HealthChecker struct {
|
||||
systemService yoreseedocpb.SystemServiceClient
|
||||
draining atomic.Bool
|
||||
}
|
||||
|
||||
type probeResponse struct {
|
||||
Status string `json:"status"`
|
||||
Detail string `json:"detail,omitempty"`
|
||||
Backend string `json:"backend,omitempty"`
|
||||
}
|
||||
|
||||
func NewHealthChecker(systemService yoreseedocpb.SystemServiceClient) *HealthChecker {
|
||||
return &HealthChecker{
|
||||
systemService: systemService,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HealthChecker) SetDraining(v bool) {
|
||||
h.draining.Store(v)
|
||||
}
|
||||
|
||||
func (h *HealthChecker) RegisterProbeRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/health", h.Health)
|
||||
mux.HandleFunc("/readyz", h.Readiness)
|
||||
mux.HandleFunc("/livez", h.Liveness)
|
||||
}
|
||||
|
||||
func (h *HealthChecker) Liveness(w http.ResponseWriter, _ *http.Request) {
|
||||
writeProbeResponse(w, http.StatusOK, "ok", "", "")
|
||||
}
|
||||
|
||||
func (h *HealthChecker) Readiness(w http.ResponseWriter, _ *http.Request) {
|
||||
if err := h.readinessErr(); err != nil {
|
||||
writeProbeResponse(w, http.StatusServiceUnavailable, "not_ready", err.Error(), probeBackendStatusFromErr(err))
|
||||
return
|
||||
}
|
||||
writeProbeResponse(w, http.StatusOK, "ok", "", "ok")
|
||||
}
|
||||
|
||||
func (h *HealthChecker) Health(w http.ResponseWriter, _ *http.Request) {
|
||||
detail := ""
|
||||
backend := "ok"
|
||||
if err := h.readinessErr(); err != nil {
|
||||
detail = err.Error()
|
||||
backend = probeBackendStatusFromErr(err)
|
||||
}
|
||||
writeProbeResponse(w, http.StatusOK, "ok", detail, backend)
|
||||
}
|
||||
|
||||
func (h *HealthChecker) readinessErr() error {
|
||||
if h.draining.Load() {
|
||||
return fmt.Errorf("server is draining")
|
||||
}
|
||||
return h.checkBackend()
|
||||
}
|
||||
|
||||
func writeProbeResponse(w http.ResponseWriter, statusCode int, status, detail, backend string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(statusCode)
|
||||
_ = json.NewEncoder(w).Encode(probeResponse{
|
||||
Status: status,
|
||||
Detail: detail,
|
||||
Backend: backend,
|
||||
})
|
||||
}
|
||||
|
||||
func probeBackendStatusFromErr(err error) string {
|
||||
if err == nil {
|
||||
return "ok"
|
||||
}
|
||||
if err.Error() == "server is draining" {
|
||||
return "unknown"
|
||||
}
|
||||
return "unhealthy"
|
||||
}
|
||||
|
||||
func (h *HealthChecker) checkBackend() error {
|
||||
if h.systemService == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if _, err := h.systemService.Health(ctx, &yoreseedocpb.HealthRequest{}); err != nil {
|
||||
log.Printf("backend health check failed: %v", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/collab-go/auth"
|
||||
"github.com/XingfenD/yoresee_doc/collab-go/config"
|
||||
"github.com/XingfenD/yoresee_doc/collab-go/handler"
|
||||
"github.com/XingfenD/yoresee_doc/collab-go/health"
|
||||
yoreseedocpb "github.com/XingfenD/yoresee_doc/collab-go/pkg/gen/yoresee_doc/v1"
|
||||
"github.com/XingfenD/yoresee_doc/collab-go/proxy"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load config: %v", err)
|
||||
}
|
||||
|
||||
var systemService yoreseedocpb.SystemServiceClient
|
||||
var documentService yoreseedocpb.DocumentServiceClient
|
||||
|
||||
grpcClient, err := grpc.NewClient(cfg.BackendGRPCAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
log.Printf("Warning: failed to initialize gRPC client: %v. Health check will show backend as unknown.", err)
|
||||
} else {
|
||||
systemService = yoreseedocpb.NewSystemServiceClient(grpcClient)
|
||||
documentService = yoreseedocpb.NewDocumentServiceClient(grpcClient)
|
||||
defer grpcClient.Close()
|
||||
}
|
||||
|
||||
authenticator := auth.NewAuthenticator(cfg.JWTSecret)
|
||||
proxyHandler := proxy.NewProxy(cfg.CollabCoreURL)
|
||||
healthChecker := health.NewHealthChecker(systemService)
|
||||
wsHandler := handler.NewWSHandler(authenticator, proxyHandler, documentService, cfg.InternalRPCKey)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
healthChecker.RegisterProbeRoutes(mux)
|
||||
mux.Handle("/ws/doc/", wsHandler)
|
||||
|
||||
server := &http.Server{
|
||||
Addr: cfg.Addr,
|
||||
Handler: mux,
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
log.Printf("collab-gateway listening on %s", cfg.Addr)
|
||||
go func() {
|
||||
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Fatalf("collab-gateway exited unexpectedly: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
signalCtx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
<-signalCtx.Done()
|
||||
|
||||
log.Printf("shutdown signal received, start draining")
|
||||
healthChecker.SetDraining(true)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
if err := server.Shutdown(ctx); err != nil {
|
||||
log.Printf("collab-gateway graceful shutdown failed: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/url"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type Proxy struct {
|
||||
coreURL string
|
||||
}
|
||||
|
||||
func NewProxy(coreURL string) *Proxy {
|
||||
return &Proxy{
|
||||
coreURL: coreURL,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Proxy) DialCore(docID string) (*websocket.Conn, error) {
|
||||
u, err := url.Parse(p.coreURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u.Path = "/doc-" + docID
|
||||
u.RawQuery = ""
|
||||
|
||||
coreConn, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return coreConn, nil
|
||||
}
|
||||
|
||||
func (p *Proxy) ProxyWS(src, dst *websocket.Conn, errCh chan<- error) {
|
||||
for {
|
||||
msgType, r, err := src.NextReader()
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
w, err := dst.NextWriter(msgType)
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
if _, err := io.Copy(w, r); err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user