From 1fa5e900f8e236b783c5340782299a4e00d63a89 Mon Sep 17 00:00:00 2001 From: dalbodeule <11470513+dalbodeule@users.noreply.github.com> Date: Mon, 8 Dec 2025 20:30:53 +0900 Subject: [PATCH] [feat](protocol): add Protobuf schemas and code generation for hopgate streams - Defined `hopgate_stream.proto` with message definitions for stream-based DTLS tunneling, including `Request`, `Response`, `StreamOpen`, `StreamData`, `StreamAck`, and `StreamClose`. - Added `Envelope` container for top-level message encapsulation. - Integrated Protobuf code generation into the `Makefile` using `protoc` with `protoc-gen-go`. - Generated Go types under `internal/protocol/pb`. --- Makefile | 14 + internal/protocol/hopgate_stream.pb.go | 799 +++++++++++++++++++++++++ internal/protocol/hopgate_stream.proto | 103 ++++ 3 files changed, 916 insertions(+) create mode 100644 internal/protocol/hopgate_stream.pb.go create mode 100644 internal/protocol/hopgate_stream.proto diff --git a/Makefile b/Makefile index f9afeea..b0d703c 100644 --- a/Makefile +++ b/Makefile @@ -66,3 +66,17 @@ docker-server: @echo "Building server Docker image..." docker build -f Dockerfile.server -t hop-gate-server:$(VERSION) . +# --- Protobuf code generation ------------------------------------------------- +# Requires: +# - protoc (https://grpc.io/docs/protoc-installation/) +# - protoc-gen-go (go install google.golang.org/protobuf/cmd/protoc-gen-go@latest) +# +# Generates Go types under internal/protocol/pb from internal/protocol/hopgate_stream.proto. +proto: + @echo "Generating Go code from Protobuf schemas..." + protoc \ + --go_out=. \ + --go_opt=paths=source_relative \ + internal/protocol/hopgate_stream.proto + @echo "Protobuf generation completed." + diff --git a/internal/protocol/hopgate_stream.pb.go b/internal/protocol/hopgate_stream.pb.go new file mode 100644 index 0000000..2871b3c --- /dev/null +++ b/internal/protocol/hopgate_stream.pb.go @@ -0,0 +1,799 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.10 +// protoc v6.33.1 +// source: internal/protocol/hopgate_stream.proto + +package protocolpb + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// HeaderValues 는 HTTP 헤더의 다중 값 표현을 위한 래퍼입니다. +// HeaderValues wraps multiple header values for a single HTTP header key. +type HeaderValues struct { + state protoimpl.MessageState `protogen:"open.v1"` + Values []string `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HeaderValues) Reset() { + *x = HeaderValues{} + mi := &file_internal_protocol_hopgate_stream_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HeaderValues) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HeaderValues) ProtoMessage() {} + +func (x *HeaderValues) ProtoReflect() protoreflect.Message { + mi := &file_internal_protocol_hopgate_stream_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HeaderValues.ProtoReflect.Descriptor instead. +func (*HeaderValues) Descriptor() ([]byte, []int) { + return file_internal_protocol_hopgate_stream_proto_rawDescGZIP(), []int{0} +} + +func (x *HeaderValues) GetValues() []string { + if x != nil { + return x.Values + } + return nil +} + +// Request 는 DTLS 터널 위에서 교환되는 HTTP 요청을 표현합니다. +// This mirrors internal/protocol.Request. +type Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + ClientId string `protobuf:"bytes,2,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` // optional client identifier + ServiceName string `protobuf:"bytes,3,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` // logical service name on the client side + Method string `protobuf:"bytes,4,opt,name=method,proto3" json:"method,omitempty"` + Url string `protobuf:"bytes,5,opt,name=url,proto3" json:"url,omitempty"` + // HTTP header: map of key -> multiple values. + Header map[string]*HeaderValues `protobuf:"bytes,6,rep,name=header,proto3" json:"header,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Raw HTTP body bytes. + Body []byte `protobuf:"bytes,7,opt,name=body,proto3" json:"body,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Request) Reset() { + *x = Request{} + mi := &file_internal_protocol_hopgate_stream_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Request) ProtoMessage() {} + +func (x *Request) ProtoReflect() protoreflect.Message { + mi := &file_internal_protocol_hopgate_stream_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Request.ProtoReflect.Descriptor instead. +func (*Request) Descriptor() ([]byte, []int) { + return file_internal_protocol_hopgate_stream_proto_rawDescGZIP(), []int{1} +} + +func (x *Request) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +func (x *Request) GetClientId() string { + if x != nil { + return x.ClientId + } + return "" +} + +func (x *Request) GetServiceName() string { + if x != nil { + return x.ServiceName + } + return "" +} + +func (x *Request) GetMethod() string { + if x != nil { + return x.Method + } + return "" +} + +func (x *Request) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +func (x *Request) GetHeader() map[string]*HeaderValues { + if x != nil { + return x.Header + } + return nil +} + +func (x *Request) GetBody() []byte { + if x != nil { + return x.Body + } + return nil +} + +// Response 는 DTLS 터널 위에서 교환되는 HTTP 응답을 표현합니다. +// This mirrors internal/protocol.Response. +type Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + Status int32 `protobuf:"varint,2,opt,name=status,proto3" json:"status,omitempty"` + // HTTP header. + Header map[string]*HeaderValues `protobuf:"bytes,3,rep,name=header,proto3" json:"header,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Raw HTTP body bytes. + Body []byte `protobuf:"bytes,4,opt,name=body,proto3" json:"body,omitempty"` + // Optional error description when tunneling fails. + Error string `protobuf:"bytes,5,opt,name=error,proto3" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Response) Reset() { + *x = Response{} + mi := &file_internal_protocol_hopgate_stream_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Response) ProtoMessage() {} + +func (x *Response) ProtoReflect() protoreflect.Message { + mi := &file_internal_protocol_hopgate_stream_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Response.ProtoReflect.Descriptor instead. +func (*Response) Descriptor() ([]byte, []int) { + return file_internal_protocol_hopgate_stream_proto_rawDescGZIP(), []int{2} +} + +func (x *Response) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +func (x *Response) GetStatus() int32 { + if x != nil { + return x.Status + } + return 0 +} + +func (x *Response) GetHeader() map[string]*HeaderValues { + if x != nil { + return x.Header + } + return nil +} + +func (x *Response) GetBody() []byte { + if x != nil { + return x.Body + } + return nil +} + +func (x *Response) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +// StreamOpen 은 새로운 스트림(HTTP 요청/응답, WebSocket 등)을 여는 메시지입니다. +// This represents opening a new stream (HTTP request/response, WebSocket, etc.). +type StreamOpen struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // StreamID (text form) + // Which logical service / local target to use on the client side. + ServiceName string `protobuf:"bytes,2,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` + TargetAddr string `protobuf:"bytes,3,opt,name=target_addr,json=targetAddr,proto3" json:"target_addr,omitempty"` // e.g. "127.0.0.1:8080" + // Initial HTTP-like headers (including Upgrade, etc.). + Header map[string]*HeaderValues `protobuf:"bytes,4,rep,name=header,proto3" json:"header,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StreamOpen) Reset() { + *x = StreamOpen{} + mi := &file_internal_protocol_hopgate_stream_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StreamOpen) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StreamOpen) ProtoMessage() {} + +func (x *StreamOpen) ProtoReflect() protoreflect.Message { + mi := &file_internal_protocol_hopgate_stream_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StreamOpen.ProtoReflect.Descriptor instead. +func (*StreamOpen) Descriptor() ([]byte, []int) { + return file_internal_protocol_hopgate_stream_proto_rawDescGZIP(), []int{3} +} + +func (x *StreamOpen) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *StreamOpen) GetServiceName() string { + if x != nil { + return x.ServiceName + } + return "" +} + +func (x *StreamOpen) GetTargetAddr() string { + if x != nil { + return x.TargetAddr + } + return "" +} + +func (x *StreamOpen) GetHeader() map[string]*HeaderValues { + if x != nil { + return x.Header + } + return nil +} + +// StreamData 는 이미 열린 스트림에 대한 단방향 데이터 프레임입니다. +// This is a unidirectional data frame on an already-open stream. +type StreamData struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // StreamID + Seq uint64 `protobuf:"varint,2,opt,name=seq,proto3" json:"seq,omitempty"` // per-stream sequence number starting from 0 + Data []byte `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StreamData) Reset() { + *x = StreamData{} + mi := &file_internal_protocol_hopgate_stream_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StreamData) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StreamData) ProtoMessage() {} + +func (x *StreamData) ProtoReflect() protoreflect.Message { + mi := &file_internal_protocol_hopgate_stream_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StreamData.ProtoReflect.Descriptor instead. +func (*StreamData) Descriptor() ([]byte, []int) { + return file_internal_protocol_hopgate_stream_proto_rawDescGZIP(), []int{4} +} + +func (x *StreamData) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *StreamData) GetSeq() uint64 { + if x != nil { + return x.Seq + } + return 0 +} + +func (x *StreamData) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +// StreamAck 는 StreamData 에 대한 ACK/NACK 및 선택적 재전송 힌트를 전달합니다. +// This conveys ACK/NACK and optional retransmission hints for StreamData. +type StreamAck struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // Last contiguously received sequence number (starting from 0). + AckSeq uint64 `protobuf:"varint,2,opt,name=ack_seq,json=ackSeq,proto3" json:"ack_seq,omitempty"` + // Additional missing sequence numbers beyond ack_seq (optional). + LostSeqs []uint64 `protobuf:"varint,3,rep,packed,name=lost_seqs,json=lostSeqs,proto3" json:"lost_seqs,omitempty"` + // Optional receive window size hint. + WindowSize uint32 `protobuf:"varint,4,opt,name=window_size,json=windowSize,proto3" json:"window_size,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StreamAck) Reset() { + *x = StreamAck{} + mi := &file_internal_protocol_hopgate_stream_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StreamAck) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StreamAck) ProtoMessage() {} + +func (x *StreamAck) ProtoReflect() protoreflect.Message { + mi := &file_internal_protocol_hopgate_stream_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StreamAck.ProtoReflect.Descriptor instead. +func (*StreamAck) Descriptor() ([]byte, []int) { + return file_internal_protocol_hopgate_stream_proto_rawDescGZIP(), []int{5} +} + +func (x *StreamAck) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *StreamAck) GetAckSeq() uint64 { + if x != nil { + return x.AckSeq + } + return 0 +} + +func (x *StreamAck) GetLostSeqs() []uint64 { + if x != nil { + return x.LostSeqs + } + return nil +} + +func (x *StreamAck) GetWindowSize() uint32 { + if x != nil { + return x.WindowSize + } + return 0 +} + +// StreamClose 는 스트림 종료(정상/에러)를 알립니다. +// This indicates normal or error termination of a stream. +type StreamClose struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` // empty means normal close + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StreamClose) Reset() { + *x = StreamClose{} + mi := &file_internal_protocol_hopgate_stream_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StreamClose) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StreamClose) ProtoMessage() {} + +func (x *StreamClose) ProtoReflect() protoreflect.Message { + mi := &file_internal_protocol_hopgate_stream_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StreamClose.ProtoReflect.Descriptor instead. +func (*StreamClose) Descriptor() ([]byte, []int) { + return file_internal_protocol_hopgate_stream_proto_rawDescGZIP(), []int{6} +} + +func (x *StreamClose) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *StreamClose) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +// Envelope 는 DTLS 세션 위에서 교환되는 상위 레벨 메시지 컨테이너입니다. +// 하나의 Envelope 에는 HTTP 요청/응답 또는 스트림 관련 메시지 중 하나만 포함됩니다. +// Envelope is the top-level container exchanged over the DTLS session. +// Exactly one payload (http_request/http_response/stream_*) is set per message. +type Envelope struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Payload: + // + // *Envelope_HttpRequest + // *Envelope_HttpResponse + // *Envelope_StreamOpen + // *Envelope_StreamData + // *Envelope_StreamClose + // *Envelope_StreamAck + Payload isEnvelope_Payload `protobuf_oneof:"payload"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Envelope) Reset() { + *x = Envelope{} + mi := &file_internal_protocol_hopgate_stream_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Envelope) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Envelope) ProtoMessage() {} + +func (x *Envelope) ProtoReflect() protoreflect.Message { + mi := &file_internal_protocol_hopgate_stream_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Envelope.ProtoReflect.Descriptor instead. +func (*Envelope) Descriptor() ([]byte, []int) { + return file_internal_protocol_hopgate_stream_proto_rawDescGZIP(), []int{7} +} + +func (x *Envelope) GetPayload() isEnvelope_Payload { + if x != nil { + return x.Payload + } + return nil +} + +func (x *Envelope) GetHttpRequest() *Request { + if x != nil { + if x, ok := x.Payload.(*Envelope_HttpRequest); ok { + return x.HttpRequest + } + } + return nil +} + +func (x *Envelope) GetHttpResponse() *Response { + if x != nil { + if x, ok := x.Payload.(*Envelope_HttpResponse); ok { + return x.HttpResponse + } + } + return nil +} + +func (x *Envelope) GetStreamOpen() *StreamOpen { + if x != nil { + if x, ok := x.Payload.(*Envelope_StreamOpen); ok { + return x.StreamOpen + } + } + return nil +} + +func (x *Envelope) GetStreamData() *StreamData { + if x != nil { + if x, ok := x.Payload.(*Envelope_StreamData); ok { + return x.StreamData + } + } + return nil +} + +func (x *Envelope) GetStreamClose() *StreamClose { + if x != nil { + if x, ok := x.Payload.(*Envelope_StreamClose); ok { + return x.StreamClose + } + } + return nil +} + +func (x *Envelope) GetStreamAck() *StreamAck { + if x != nil { + if x, ok := x.Payload.(*Envelope_StreamAck); ok { + return x.StreamAck + } + } + return nil +} + +type isEnvelope_Payload interface { + isEnvelope_Payload() +} + +type Envelope_HttpRequest struct { + HttpRequest *Request `protobuf:"bytes,1,opt,name=http_request,json=httpRequest,proto3,oneof"` +} + +type Envelope_HttpResponse struct { + HttpResponse *Response `protobuf:"bytes,2,opt,name=http_response,json=httpResponse,proto3,oneof"` +} + +type Envelope_StreamOpen struct { + StreamOpen *StreamOpen `protobuf:"bytes,3,opt,name=stream_open,json=streamOpen,proto3,oneof"` +} + +type Envelope_StreamData struct { + StreamData *StreamData `protobuf:"bytes,4,opt,name=stream_data,json=streamData,proto3,oneof"` +} + +type Envelope_StreamClose struct { + StreamClose *StreamClose `protobuf:"bytes,5,opt,name=stream_close,json=streamClose,proto3,oneof"` +} + +type Envelope_StreamAck struct { + StreamAck *StreamAck `protobuf:"bytes,6,opt,name=stream_ack,json=streamAck,proto3,oneof"` +} + +func (*Envelope_HttpRequest) isEnvelope_Payload() {} + +func (*Envelope_HttpResponse) isEnvelope_Payload() {} + +func (*Envelope_StreamOpen) isEnvelope_Payload() {} + +func (*Envelope_StreamData) isEnvelope_Payload() {} + +func (*Envelope_StreamClose) isEnvelope_Payload() {} + +func (*Envelope_StreamAck) isEnvelope_Payload() {} + +var File_internal_protocol_hopgate_stream_proto protoreflect.FileDescriptor + +const file_internal_protocol_hopgate_stream_proto_rawDesc = "" + + "\n" + + "&internal/protocol/hopgate_stream.proto\x12\x13hopgate.protocol.v1\"&\n" + + "\fHeaderValues\x12\x16\n" + + "\x06values\x18\x01 \x03(\tR\x06values\"\xc6\x02\n" + + "\aRequest\x12\x1d\n" + + "\n" + + "request_id\x18\x01 \x01(\tR\trequestId\x12\x1b\n" + + "\tclient_id\x18\x02 \x01(\tR\bclientId\x12!\n" + + "\fservice_name\x18\x03 \x01(\tR\vserviceName\x12\x16\n" + + "\x06method\x18\x04 \x01(\tR\x06method\x12\x10\n" + + "\x03url\x18\x05 \x01(\tR\x03url\x12@\n" + + "\x06header\x18\x06 \x03(\v2(.hopgate.protocol.v1.Request.HeaderEntryR\x06header\x12\x12\n" + + "\x04body\x18\a \x01(\fR\x04body\x1a\\\n" + + "\vHeaderEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x127\n" + + "\x05value\x18\x02 \x01(\v2!.hopgate.protocol.v1.HeaderValuesR\x05value:\x028\x01\"\x8c\x02\n" + + "\bResponse\x12\x1d\n" + + "\n" + + "request_id\x18\x01 \x01(\tR\trequestId\x12\x16\n" + + "\x06status\x18\x02 \x01(\x05R\x06status\x12A\n" + + "\x06header\x18\x03 \x03(\v2).hopgate.protocol.v1.Response.HeaderEntryR\x06header\x12\x12\n" + + "\x04body\x18\x04 \x01(\fR\x04body\x12\x14\n" + + "\x05error\x18\x05 \x01(\tR\x05error\x1a\\\n" + + "\vHeaderEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x127\n" + + "\x05value\x18\x02 \x01(\v2!.hopgate.protocol.v1.HeaderValuesR\x05value:\x028\x01\"\x83\x02\n" + + "\n" + + "StreamOpen\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12!\n" + + "\fservice_name\x18\x02 \x01(\tR\vserviceName\x12\x1f\n" + + "\vtarget_addr\x18\x03 \x01(\tR\n" + + "targetAddr\x12C\n" + + "\x06header\x18\x04 \x03(\v2+.hopgate.protocol.v1.StreamOpen.HeaderEntryR\x06header\x1a\\\n" + + "\vHeaderEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x127\n" + + "\x05value\x18\x02 \x01(\v2!.hopgate.protocol.v1.HeaderValuesR\x05value:\x028\x01\"B\n" + + "\n" + + "StreamData\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x10\n" + + "\x03seq\x18\x02 \x01(\x04R\x03seq\x12\x12\n" + + "\x04data\x18\x03 \x01(\fR\x04data\"r\n" + + "\tStreamAck\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x17\n" + + "\aack_seq\x18\x02 \x01(\x04R\x06ackSeq\x12\x1b\n" + + "\tlost_seqs\x18\x03 \x03(\x04R\blostSeqs\x12\x1f\n" + + "\vwindow_size\x18\x04 \x01(\rR\n" + + "windowSize\"3\n" + + "\vStreamClose\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x14\n" + + "\x05error\x18\x02 \x01(\tR\x05error\"\xae\x03\n" + + "\bEnvelope\x12A\n" + + "\fhttp_request\x18\x01 \x01(\v2\x1c.hopgate.protocol.v1.RequestH\x00R\vhttpRequest\x12D\n" + + "\rhttp_response\x18\x02 \x01(\v2\x1d.hopgate.protocol.v1.ResponseH\x00R\fhttpResponse\x12B\n" + + "\vstream_open\x18\x03 \x01(\v2\x1f.hopgate.protocol.v1.StreamOpenH\x00R\n" + + "streamOpen\x12B\n" + + "\vstream_data\x18\x04 \x01(\v2\x1f.hopgate.protocol.v1.StreamDataH\x00R\n" + + "streamData\x12E\n" + + "\fstream_close\x18\x05 \x01(\v2 .hopgate.protocol.v1.StreamCloseH\x00R\vstreamClose\x12?\n" + + "\n" + + "stream_ack\x18\x06 \x01(\v2\x1e.hopgate.protocol.v1.StreamAckH\x00R\tstreamAckB\t\n" + + "\apayloadB@Z>github.com/dalbodeule/hop-gate/internal/protocol/pb;protocolpbb\x06proto3" + +var ( + file_internal_protocol_hopgate_stream_proto_rawDescOnce sync.Once + file_internal_protocol_hopgate_stream_proto_rawDescData []byte +) + +func file_internal_protocol_hopgate_stream_proto_rawDescGZIP() []byte { + file_internal_protocol_hopgate_stream_proto_rawDescOnce.Do(func() { + file_internal_protocol_hopgate_stream_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_internal_protocol_hopgate_stream_proto_rawDesc), len(file_internal_protocol_hopgate_stream_proto_rawDesc))) + }) + return file_internal_protocol_hopgate_stream_proto_rawDescData +} + +var file_internal_protocol_hopgate_stream_proto_msgTypes = make([]protoimpl.MessageInfo, 11) +var file_internal_protocol_hopgate_stream_proto_goTypes = []any{ + (*HeaderValues)(nil), // 0: hopgate.protocol.v1.HeaderValues + (*Request)(nil), // 1: hopgate.protocol.v1.Request + (*Response)(nil), // 2: hopgate.protocol.v1.Response + (*StreamOpen)(nil), // 3: hopgate.protocol.v1.StreamOpen + (*StreamData)(nil), // 4: hopgate.protocol.v1.StreamData + (*StreamAck)(nil), // 5: hopgate.protocol.v1.StreamAck + (*StreamClose)(nil), // 6: hopgate.protocol.v1.StreamClose + (*Envelope)(nil), // 7: hopgate.protocol.v1.Envelope + nil, // 8: hopgate.protocol.v1.Request.HeaderEntry + nil, // 9: hopgate.protocol.v1.Response.HeaderEntry + nil, // 10: hopgate.protocol.v1.StreamOpen.HeaderEntry +} +var file_internal_protocol_hopgate_stream_proto_depIdxs = []int32{ + 8, // 0: hopgate.protocol.v1.Request.header:type_name -> hopgate.protocol.v1.Request.HeaderEntry + 9, // 1: hopgate.protocol.v1.Response.header:type_name -> hopgate.protocol.v1.Response.HeaderEntry + 10, // 2: hopgate.protocol.v1.StreamOpen.header:type_name -> hopgate.protocol.v1.StreamOpen.HeaderEntry + 1, // 3: hopgate.protocol.v1.Envelope.http_request:type_name -> hopgate.protocol.v1.Request + 2, // 4: hopgate.protocol.v1.Envelope.http_response:type_name -> hopgate.protocol.v1.Response + 3, // 5: hopgate.protocol.v1.Envelope.stream_open:type_name -> hopgate.protocol.v1.StreamOpen + 4, // 6: hopgate.protocol.v1.Envelope.stream_data:type_name -> hopgate.protocol.v1.StreamData + 6, // 7: hopgate.protocol.v1.Envelope.stream_close:type_name -> hopgate.protocol.v1.StreamClose + 5, // 8: hopgate.protocol.v1.Envelope.stream_ack:type_name -> hopgate.protocol.v1.StreamAck + 0, // 9: hopgate.protocol.v1.Request.HeaderEntry.value:type_name -> hopgate.protocol.v1.HeaderValues + 0, // 10: hopgate.protocol.v1.Response.HeaderEntry.value:type_name -> hopgate.protocol.v1.HeaderValues + 0, // 11: hopgate.protocol.v1.StreamOpen.HeaderEntry.value:type_name -> hopgate.protocol.v1.HeaderValues + 12, // [12:12] is the sub-list for method output_type + 12, // [12:12] is the sub-list for method input_type + 12, // [12:12] is the sub-list for extension type_name + 12, // [12:12] is the sub-list for extension extendee + 0, // [0:12] is the sub-list for field type_name +} + +func init() { file_internal_protocol_hopgate_stream_proto_init() } +func file_internal_protocol_hopgate_stream_proto_init() { + if File_internal_protocol_hopgate_stream_proto != nil { + return + } + file_internal_protocol_hopgate_stream_proto_msgTypes[7].OneofWrappers = []any{ + (*Envelope_HttpRequest)(nil), + (*Envelope_HttpResponse)(nil), + (*Envelope_StreamOpen)(nil), + (*Envelope_StreamData)(nil), + (*Envelope_StreamClose)(nil), + (*Envelope_StreamAck)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_internal_protocol_hopgate_stream_proto_rawDesc), len(file_internal_protocol_hopgate_stream_proto_rawDesc)), + NumEnums: 0, + NumMessages: 11, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_internal_protocol_hopgate_stream_proto_goTypes, + DependencyIndexes: file_internal_protocol_hopgate_stream_proto_depIdxs, + MessageInfos: file_internal_protocol_hopgate_stream_proto_msgTypes, + }.Build() + File_internal_protocol_hopgate_stream_proto = out.File + file_internal_protocol_hopgate_stream_proto_goTypes = nil + file_internal_protocol_hopgate_stream_proto_depIdxs = nil +} diff --git a/internal/protocol/hopgate_stream.proto b/internal/protocol/hopgate_stream.proto new file mode 100644 index 0000000..09eb5c8 --- /dev/null +++ b/internal/protocol/hopgate_stream.proto @@ -0,0 +1,103 @@ +syntax = "proto3"; + +package hopgate.protocol.v1; + +option go_package = "github.com/dalbodeule/hop-gate/internal/protocol/pb;protocolpb"; + +// HeaderValues 는 HTTP 헤더의 다중 값 표현을 위한 래퍼입니다. +// HeaderValues wraps multiple header values for a single HTTP header key. +message HeaderValues { + repeated string values = 1; +} + +// Request 는 DTLS 터널 위에서 교환되는 HTTP 요청을 표현합니다. +// This mirrors internal/protocol.Request. +message Request { + string request_id = 1; + string client_id = 2; // optional client identifier + string service_name = 3; // logical service name on the client side + + string method = 4; + string url = 5; + + // HTTP header: map of key -> multiple values. + map header = 6; + + // Raw HTTP body bytes. + bytes body = 7; +} + +// Response 는 DTLS 터널 위에서 교환되는 HTTP 응답을 표현합니다. +// This mirrors internal/protocol.Response. +message Response { + string request_id = 1; + int32 status = 2; + + // HTTP header. + map header = 3; + + // Raw HTTP body bytes. + bytes body = 4; + + // Optional error description when tunneling fails. + string error = 5; +} + +// StreamOpen 은 새로운 스트림(HTTP 요청/응답, WebSocket 등)을 여는 메시지입니다. +// This represents opening a new stream (HTTP request/response, WebSocket, etc.). +message StreamOpen { + string id = 1; // StreamID (text form) + + // Which logical service / local target to use on the client side. + string service_name = 2; + string target_addr = 3; // e.g. "127.0.0.1:8080" + + // Initial HTTP-like headers (including Upgrade, etc.). + map header = 4; +} + +// StreamData 는 이미 열린 스트림에 대한 단방향 데이터 프레임입니다. +// This is a unidirectional data frame on an already-open stream. +message StreamData { + string id = 1; // StreamID + uint64 seq = 2; // per-stream sequence number starting from 0 + bytes data = 3; +} + +// StreamAck 는 StreamData 에 대한 ACK/NACK 및 선택적 재전송 힌트를 전달합니다. +// This conveys ACK/NACK and optional retransmission hints for StreamData. +message StreamAck { + string id = 1; + + // Last contiguously received sequence number (starting from 0). + uint64 ack_seq = 2; + + // Additional missing sequence numbers beyond ack_seq (optional). + repeated uint64 lost_seqs = 3; + + // Optional receive window size hint. + uint32 window_size = 4; +} + +// StreamClose 는 스트림 종료(정상/에러)를 알립니다. +// This indicates normal or error termination of a stream. +message StreamClose { + string id = 1; + string error = 2; // empty means normal close +} + +// Envelope 는 DTLS 세션 위에서 교환되는 상위 레벨 메시지 컨테이너입니다. +// 하나의 Envelope 에는 HTTP 요청/응답 또는 스트림 관련 메시지 중 하나만 포함됩니다. +// Envelope is the top-level container exchanged over the DTLS session. +// Exactly one payload (http_request/http_response/stream_*) is set per message. +message Envelope { + oneof payload { + Request http_request = 1; + Response http_response = 2; + + StreamOpen stream_open = 3; + StreamData stream_data = 4; + StreamClose stream_close = 5; + StreamAck stream_ack = 6; + } +} \ No newline at end of file