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