mirror of
https://github.com/dalbodeule/hop-gate.git
synced 2025-12-08 04:45:43 +09:00
[feat](proxy,build): enhance HTTP tunneling protocol and add Docker publishing
- Refactored HTTP tunneling protocol to use `protocol.Envelope` for better extensibility. - Implemented support for message types including HTTP requests, stream handling, and responses. - Added common security headers like HSTS and X-Forwarded-For processing for improved security and identity handling. - Introduced GitHub Actions workflow to build and publish Docker images to GHCR. - Added new protocol structures for stream-based communication in anticipation of future WebSocket/TCP tunneling. - Updated `go.sum` to remove unused dependencies and reflect new changes.
This commit is contained in:
59
.github/workflows/docker-publish.yml
vendored
Normal file
59
.github/workflows/docker-publish.yml
vendored
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
name: Build and publish HopGate image to GHCR
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
tags:
|
||||||
|
- 'v*.*.*'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/hop-gate
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata (tags, labels)
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=tag
|
||||||
|
type=sha,prefix=sha-,format=short
|
||||||
|
type=raw,value=latest,enable={{is_default_branch}}
|
||||||
|
|
||||||
|
- name: Build and push multi-arch image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile.server
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -78,7 +79,7 @@ func (w *dtlsSessionWrapper) ForwardHTTP(ctx context.Context, logger logging.Log
|
|||||||
// 간단한 RequestID 생성 (실제 서비스에서는 UUID 등을 사용하는 것이 좋음)
|
// 간단한 RequestID 생성 (실제 서비스에서는 UUID 등을 사용하는 것이 좋음)
|
||||||
requestID := time.Now().UTC().Format("20060102T150405.000000000")
|
requestID := time.Now().UTC().Format("20060102T150405.000000000")
|
||||||
|
|
||||||
protoReq := &protocol.Request{
|
httpReq := &protocol.Request{
|
||||||
RequestID: requestID,
|
RequestID: requestID,
|
||||||
ClientID: "", // TODO: 클라이언트 식별자 도입 시 채우기
|
ClientID: "", // TODO: 클라이언트 식별자 도입 시 채우기
|
||||||
ServiceName: serviceName,
|
ServiceName: serviceName,
|
||||||
@@ -100,29 +101,45 @@ func (w *dtlsSessionWrapper) ForwardHTTP(ctx context.Context, logger logging.Log
|
|||||||
"scheme": req.URL.Scheme,
|
"scheme": req.URL.Scheme,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// HTTP 요청을 Envelope 로 감싸서 전송합니다.
|
||||||
|
env := &protocol.Envelope{
|
||||||
|
Type: protocol.MessageTypeHTTP,
|
||||||
|
HTTPRequest: httpReq,
|
||||||
|
}
|
||||||
|
|
||||||
enc := json.NewEncoder(w.sess)
|
enc := json.NewEncoder(w.sess)
|
||||||
if err := enc.Encode(protoReq); err != nil {
|
if err := enc.Encode(env); err != nil {
|
||||||
log.Error("failed to encode protocol request", logging.Fields{
|
log.Error("failed to encode http envelope", logging.Fields{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var protoResp protocol.Response
|
// 클라이언트로부터 HTTP 응답 Envelope 를 수신합니다.
|
||||||
|
var respEnv protocol.Envelope
|
||||||
dec := json.NewDecoder(w.sess)
|
dec := json.NewDecoder(w.sess)
|
||||||
if err := dec.Decode(&protoResp); err != nil {
|
if err := dec.Decode(&respEnv); err != nil {
|
||||||
log.Error("failed to decode protocol response", logging.Fields{
|
log.Error("failed to decode http envelope", logging.Fields{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if respEnv.Type != protocol.MessageTypeHTTP || respEnv.HTTPResponse == nil {
|
||||||
|
log.Error("received non-http envelope from client", logging.Fields{
|
||||||
|
"type": respEnv.Type,
|
||||||
|
})
|
||||||
|
return nil, fmt.Errorf("unexpected envelope type %q or empty http_response", respEnv.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
protoResp := respEnv.HTTPResponse
|
||||||
|
|
||||||
log.Info("received dtls response", logging.Fields{
|
log.Info("received dtls response", logging.Fields{
|
||||||
"status": protoResp.Status,
|
"status": protoResp.Status,
|
||||||
"error": protoResp.Error,
|
"error": protoResp.Error,
|
||||||
})
|
})
|
||||||
|
|
||||||
return &protoResp, nil
|
return protoResp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -142,6 +159,36 @@ func (w *statusRecorder) WriteHeader(code int) {
|
|||||||
w.ResponseWriter.WriteHeader(code)
|
w.ResponseWriter.WriteHeader(code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// hopGateOwnedHeaders 는 HopGate 서버가 스스로 관리하는 응답 헤더 목록입니다. (ko)
|
||||||
|
// hopGateOwnedHeaders lists response headers that are owned by the HopGate server. (en)
|
||||||
|
var hopGateOwnedHeaders = map[string]struct{}{
|
||||||
|
"X-HopGate-Server": {},
|
||||||
|
"Strict-Transport-Security": {},
|
||||||
|
"X-Content-Type-Options": {},
|
||||||
|
"Referrer-Policy": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// setSecurityAndIdentityHeaders 는 HopGate 에서 공통으로 추가하는 보안/식별 헤더를 설정합니다. (ko)
|
||||||
|
// setSecurityAndIdentityHeaders configures common security and identity headers for HopGate. (en)
|
||||||
|
func setSecurityAndIdentityHeaders(w http.ResponseWriter, r *http.Request) {
|
||||||
|
h := w.Header()
|
||||||
|
|
||||||
|
// HopGate 로 구성된 서버임을 나타내는 식별 헤더 (ko)
|
||||||
|
// Header to indicate that this server is powered by HopGate. (en)
|
||||||
|
h.Set("X-HopGate-Server", "hop-gate")
|
||||||
|
|
||||||
|
// 기본 보안 헤더 설정 (ko)
|
||||||
|
// Basic security headers (best-effort). (en)
|
||||||
|
h.Set("X-Content-Type-Options", "nosniff")
|
||||||
|
h.Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
||||||
|
|
||||||
|
// HTTPS 요청에 대해서만 HSTS 헤더를 추가합니다. (ko)
|
||||||
|
// Only send HSTS for HTTPS requests. (en)
|
||||||
|
if r != nil && r.TLS != nil {
|
||||||
|
h.Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains; preload")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// hostDomainHandler 는 HOP_SERVER_DOMAIN 에 지정된 도메인으로만 요청을 허용하는 래퍼입니다.
|
// hostDomainHandler 는 HOP_SERVER_DOMAIN 에 지정된 도메인으로만 요청을 허용하는 래퍼입니다.
|
||||||
// Host 헤더에서 포트를 제거한 뒤 소문자 비교를 수행합니다.
|
// Host 헤더에서 포트를 제거한 뒤 소문자 비교를 수행합니다.
|
||||||
func hostDomainHandler(allowedDomain string, logger logging.Logger, next http.Handler) http.Handler {
|
func hostDomainHandler(allowedDomain string, logger logging.Logger, next http.Handler) http.Handler {
|
||||||
@@ -213,6 +260,9 @@ func newHTTPHandler(logger logging.Logger) http.Handler {
|
|||||||
ResponseWriter: w,
|
ResponseWriter: w,
|
||||||
status: http.StatusOK,
|
status: http.StatusOK,
|
||||||
}
|
}
|
||||||
|
// 보안/식별 헤더를 공통으로 설정합니다. (ko)
|
||||||
|
// Configure common security and identity headers. (en)
|
||||||
|
setSecurityAndIdentityHeaders(sr, r)
|
||||||
|
|
||||||
log := logger.With(logging.Fields{
|
log := logger.With(logging.Fields{
|
||||||
"component": "http_entry",
|
"component": "http_entry",
|
||||||
@@ -286,6 +336,29 @@ func newHTTPHandler(logger logging.Logger) http.Handler {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 원본 클라이언트 IP를 X-Forwarded-For / X-Real-IP 헤더로 전달합니다. (ko)
|
||||||
|
// Forward original client IP via X-Forwarded-For / X-Real-IP headers. (en)
|
||||||
|
if r.RemoteAddr != "" {
|
||||||
|
remoteIP := r.RemoteAddr
|
||||||
|
if ip, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
|
||||||
|
remoteIP = ip
|
||||||
|
}
|
||||||
|
if remoteIP != "" {
|
||||||
|
// X-Forwarded-For 는 기존 값 뒤에 원본 IP를 추가합니다. (ko)
|
||||||
|
// Append original IP to X-Forwarded-For if present. (en)
|
||||||
|
if prior := r.Header.Get("X-Forwarded-For"); prior == "" {
|
||||||
|
r.Header.Set("X-Forwarded-For", remoteIP)
|
||||||
|
} else {
|
||||||
|
r.Header.Set("X-Forwarded-For", prior+", "+remoteIP)
|
||||||
|
}
|
||||||
|
// X-Real-IP 가 비어있는 경우에만 설정합니다. (ko)
|
||||||
|
// Set X-Real-IP only if it is not already set. (en)
|
||||||
|
if r.Header.Get("X-Real-IP") == "" {
|
||||||
|
r.Header.Set("X-Real-IP", remoteIP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// r.Body 는 ForwardHTTP 내에서 읽고 닫지 않으므로 여기서 닫기
|
// r.Body 는 ForwardHTTP 내에서 읽고 닫지 않으므로 여기서 닫기
|
||||||
defer r.Body.Close()
|
defer r.Body.Close()
|
||||||
|
|
||||||
@@ -302,6 +375,11 @@ func newHTTPHandler(logger logging.Logger) http.Handler {
|
|||||||
|
|
||||||
// 응답 헤더/바디 복원
|
// 응답 헤더/바디 복원
|
||||||
for k, vs := range protoResp.Header {
|
for k, vs := range protoResp.Header {
|
||||||
|
// HopGate 가 소유한 보안/식별 헤더는 백엔드 값 대신 서버 값만 사용합니다. (ko)
|
||||||
|
// For security/identity headers owned by HopGate, ignore backend values. (en)
|
||||||
|
if _, ok := hopGateOwnedHeaders[http.CanonicalHeaderKey(k)]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
for _, v := range vs {
|
for _, v := range vs {
|
||||||
sr.Header().Add(k, v)
|
sr.Header().Add(k, v)
|
||||||
}
|
}
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -72,8 +72,6 @@ github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8
|
|||||||
github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
|
github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
|
||||||
github.com/zclconf/go-cty-yaml v1.1.0 h1:nP+jp0qPHv2IhUVqmQSzjvqAWcObN0KBkUl2rWBdig0=
|
github.com/zclconf/go-cty-yaml v1.1.0 h1:nP+jp0qPHv2IhUVqmQSzjvqAWcObN0KBkUl2rWBdig0=
|
||||||
github.com/zclconf/go-cty-yaml v1.1.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs=
|
github.com/zclconf/go-cty-yaml v1.1.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs=
|
||||||
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
|
||||||
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
|
||||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package protocol
|
package protocol
|
||||||
|
|
||||||
// Request 는 서버-클라이언트 간에 전달되는 HTTP 요청을 표현합니다.
|
// Request 는 서버-클라이언트 간에 전달되는 HTTP 요청을 표현합니다.
|
||||||
|
// 기존 HTTP 터널링 경로에서는 이 구조체를 그대로 사용합니다.
|
||||||
type Request struct {
|
type Request struct {
|
||||||
RequestID string
|
RequestID string
|
||||||
ClientID string // 대상 클라이언트 식별자
|
ClientID string // 대상 클라이언트 식별자
|
||||||
@@ -13,6 +14,7 @@ type Request struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Response 는 서버-클라이언트 간에 전달되는 HTTP 응답을 표현합니다.
|
// Response 는 서버-클라이언트 간에 전달되는 HTTP 응답을 표현합니다.
|
||||||
|
// 기존 HTTP 터널링 경로에서는 이 구조체를 그대로 사용합니다.
|
||||||
type Response struct {
|
type Response struct {
|
||||||
RequestID string
|
RequestID string
|
||||||
Status int
|
Status int
|
||||||
@@ -20,3 +22,68 @@ type Response struct {
|
|||||||
Body []byte
|
Body []byte
|
||||||
Error string // 에러 발생 시 설명 메시지
|
Error string // 에러 발생 시 설명 메시지
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- 확장 가능 DTLS 메시지 Envelope 및 스트림 구조체 ---
|
||||||
|
//
|
||||||
|
// WebSocket/TCP 스트림 터널링을 지원하기 위해, 단일 HTTP 요청/응답 외에도
|
||||||
|
// 스트림 기반 메시지를 운반할 수 있는 Envelope 타입을 정의합니다.
|
||||||
|
// 현재 구현에서는 아직 사용하지 않으며, 향후 단계적으로 적용할 예정입니다.
|
||||||
|
|
||||||
|
// MessageType 은 DTLS 위에서 교환되는 상위 레벨 메시지 종류를 나타냅니다.
|
||||||
|
type MessageType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// MessageTypeHTTP 는 기존 단일 HTTP 요청/응답 메시지를 의미합니다.
|
||||||
|
// 이 경우 HTTPRequest / HTTPResponse 필드를 사용합니다.
|
||||||
|
MessageTypeHTTP MessageType = "http"
|
||||||
|
|
||||||
|
// MessageTypeStreamOpen 은 새로운 스트림(TCP/WebSocket 등)의 오픈을 의미합니다.
|
||||||
|
MessageTypeStreamOpen MessageType = "stream_open"
|
||||||
|
|
||||||
|
// MessageTypeStreamData 는 열린 스트림에 대한 양방향 데이터 프레임을 의미합니다.
|
||||||
|
MessageTypeStreamData MessageType = "stream_data"
|
||||||
|
|
||||||
|
// MessageTypeStreamClose 는 스트림 종료(정상/에러)를 의미합니다.
|
||||||
|
MessageTypeStreamClose MessageType = "stream_close"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Envelope 는 DTLS 세션 위에서 교환되는 상위 레벨 메시지 컨테이너입니다.
|
||||||
|
// 하나의 Envelope 에는 HTTP 요청/응답 또는 스트림 관련 메시지 중 하나만 포함됩니다.
|
||||||
|
type Envelope struct {
|
||||||
|
Type MessageType `json:"type"`
|
||||||
|
|
||||||
|
// HTTP 1회성 요청/응답 (기존 터널링 경로)
|
||||||
|
HTTPRequest *Request `json:"http_request,omitempty"`
|
||||||
|
HTTPResponse *Response `json:"http_response,omitempty"`
|
||||||
|
|
||||||
|
// 스트림 기반 메시지 (WebSocket/TCP 터널용)
|
||||||
|
StreamOpen *StreamOpen `json:"stream_open,omitempty"`
|
||||||
|
StreamData *StreamData `json:"stream_data,omitempty"`
|
||||||
|
StreamClose *StreamClose `json:"stream_close,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StreamID 는 스트림(예: 특정 WebSocket 연결 또는 TCP 커넥션)을 구분하기 위한 식별자입니다.
|
||||||
|
type StreamID string
|
||||||
|
|
||||||
|
// StreamOpen 은 새로운 스트림을 여는 요청을 나타냅니다.
|
||||||
|
type StreamOpen struct {
|
||||||
|
ID StreamID `json:"id"`
|
||||||
|
|
||||||
|
// Service / TargetAddr 는 클라이언트 측에서 어느 로컬 서비스로 연결해야 하는지를 나타냅니다.
|
||||||
|
// 최소 구현에서는 LocalTarget 하나만 사용해도 되며, 추후 서비스별로 확장 가능합니다.
|
||||||
|
Service string `json:"service_name,omitempty"`
|
||||||
|
TargetAddr string `json:"target_addr,omitempty"` // 예: "127.0.0.1:8080"
|
||||||
|
Header map[string][]string `json:"header,omitempty"` // 초기 HTTP 헤더(Upgrade 포함) 전달용
|
||||||
|
}
|
||||||
|
|
||||||
|
// StreamData 는 이미 열린 스트림에 대해 한 방향으로 전송되는 데이터 프레임을 표현합니다.
|
||||||
|
type StreamData struct {
|
||||||
|
ID StreamID `json:"id"`
|
||||||
|
Data []byte `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StreamClose 는 스트림 종료를 알리는 메시지입니다.
|
||||||
|
type StreamClose struct {
|
||||||
|
ID StreamID `json:"id"`
|
||||||
|
Error string `json:"error,omitempty"` // 비워두면 정상 종료로 해석
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,10 +51,10 @@ func NewClientProxy(logger logging.Logger, localTarget string) *ClientProxy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartLoop 는 DTLS 세션에서 protocol.Request 를 읽고 로컬 HTTP 요청을 수행한 뒤
|
// StartLoop 는 DTLS 세션에서 protocol.Envelope 를 읽고, HTTP 요청의 경우 로컬 HTTP 요청을 수행한 뒤
|
||||||
// protocol.Response 를 다시 세션으로 쓰는 루프를 실행합니다. (ko)
|
// protocol.Envelope(HTTP 응답 포함)을 다시 세션으로 쓰는 루프를 실행합니다. (ko)
|
||||||
// StartLoop reads protocol.Request messages from the DTLS session, performs local HTTP
|
// StartLoop reads protocol.Envelope messages from the DTLS session; for HTTP messages it
|
||||||
// requests, and writes back protocol.Response objects. (en)
|
// performs local HTTP requests and writes back HTTP responses wrapped in an Envelope. (en)
|
||||||
func (p *ClientProxy) StartLoop(ctx context.Context, sess dtls.Session) error {
|
func (p *ClientProxy) StartLoop(ctx context.Context, sess dtls.Session) error {
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
@@ -74,18 +74,28 @@ func (p *ClientProxy) StartLoop(ctx context.Context, sess dtls.Session) error {
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
var req protocol.Request
|
var env protocol.Envelope
|
||||||
if err := dec.Decode(&req); err != nil {
|
if err := dec.Decode(&env); err != nil {
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
log.Info("dtls session closed by server", nil)
|
log.Info("dtls session closed by server", nil)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
log.Error("failed to decode protocol request", logging.Fields{
|
log.Error("failed to decode protocol envelope", logging.Fields{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 현재는 HTTP 타입만 지원하며, 그 외 타입은 에러로 처리합니다.
|
||||||
|
if env.Type != protocol.MessageTypeHTTP || env.HTTPRequest == nil {
|
||||||
|
log.Error("received unsupported envelope type from server", logging.Fields{
|
||||||
|
"type": env.Type,
|
||||||
|
})
|
||||||
|
return fmt.Errorf("unsupported envelope type %q or missing http_request", env.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := env.HTTPRequest
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
logReq := log.With(logging.Fields{
|
logReq := log.With(logging.Fields{
|
||||||
"request_id": req.RequestID,
|
"request_id": req.RequestID,
|
||||||
@@ -95,7 +105,7 @@ func (p *ClientProxy) StartLoop(ctx context.Context, sess dtls.Session) error {
|
|||||||
"client_id": req.ClientID,
|
"client_id": req.ClientID,
|
||||||
"local_target": p.LocalTarget,
|
"local_target": p.LocalTarget,
|
||||||
})
|
})
|
||||||
logReq.Info("received protocol request from server", nil)
|
logReq.Info("received http envelope from server", nil)
|
||||||
|
|
||||||
resp := protocol.Response{
|
resp := protocol.Response{
|
||||||
RequestID: req.RequestID,
|
RequestID: req.RequestID,
|
||||||
@@ -103,7 +113,7 @@ func (p *ClientProxy) StartLoop(ctx context.Context, sess dtls.Session) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 로컬 HTTP 요청 수행
|
// 로컬 HTTP 요청 수행
|
||||||
if err := p.forwardToLocal(ctx, &req, &resp); err != nil {
|
if err := p.forwardToLocal(ctx, req, &resp); err != nil {
|
||||||
resp.Status = http.StatusBadGateway
|
resp.Status = http.StatusBadGateway
|
||||||
resp.Error = err.Error()
|
resp.Error = err.Error()
|
||||||
logReq.Error("local http request failed", logging.Fields{
|
logReq.Error("local http request failed", logging.Fields{
|
||||||
@@ -111,14 +121,20 @@ func (p *ClientProxy) StartLoop(ctx context.Context, sess dtls.Session) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := enc.Encode(&resp); err != nil {
|
// HTTP 응답을 Envelope 로 감싸서 서버로 전송합니다.
|
||||||
logReq.Error("failed to encode protocol response", logging.Fields{
|
respEnv := protocol.Envelope{
|
||||||
|
Type: protocol.MessageTypeHTTP,
|
||||||
|
HTTPResponse: &resp,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := enc.Encode(&respEnv); err != nil {
|
||||||
|
logReq.Error("failed to encode http response envelope", logging.Fields{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
logReq.Info("protocol response sent to server", logging.Fields{
|
logReq.Info("http response envelope sent to server", logging.Fields{
|
||||||
"status": resp.Status,
|
"status": resp.Status,
|
||||||
"elapsed_ms": time.Since(start).Milliseconds(),
|
"elapsed_ms": time.Since(start).Milliseconds(),
|
||||||
"error": resp.Error,
|
"error": resp.Error,
|
||||||
|
|||||||
Reference in New Issue
Block a user