Add sing-box API service
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
boxService "github.com/sagernet/sing-box/adapter/service"
|
||||
"github.com/sagernet/sing-box/common/listener"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/daemon"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
aTLS "github.com/sagernet/sing/common/tls"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/net/http2/h2c"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func RegisterService(registry *boxService.Registry) {
|
||||
boxService.Register[option.APIServiceOptions](registry, C.TypeAPI, NewService)
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
boxService.Adapter
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
logger log.ContextLogger
|
||||
options option.APIServiceOptions
|
||||
listener *listener.Listener
|
||||
tlsConfig tls.ServerConfig
|
||||
startedService *daemon.StartedService
|
||||
grpcServer *grpc.Server
|
||||
httpServer *http.Server
|
||||
}
|
||||
|
||||
func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.APIServiceOptions) (adapter.Service, error) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
s := &Service{
|
||||
Adapter: boxService.NewAdapter(C.TypeAPI, tag),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
logger: logger,
|
||||
options: options,
|
||||
listener: listener.New(listener.Options{
|
||||
Context: ctx,
|
||||
Logger: logger,
|
||||
Network: []string{N.NetworkTCP},
|
||||
Listen: options.ListenOptions,
|
||||
}),
|
||||
}
|
||||
if options.TLS != nil {
|
||||
tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS))
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, err
|
||||
}
|
||||
s.tlsConfig = tlsConfig
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *Service) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateStarted {
|
||||
return nil
|
||||
}
|
||||
s.startedService = daemon.NewAttachedService(s.ctx)
|
||||
s.grpcServer = daemon.NewServer(s.startedService, s.options.Secret)
|
||||
s.httpServer = &http.Server{
|
||||
Handler: h2c.NewHandler(newHTTPHandler(s.logger, s.grpcServer, s.options), new(http2.Server)),
|
||||
BaseContext: func(net.Listener) context.Context {
|
||||
return s.ctx
|
||||
},
|
||||
}
|
||||
if s.tlsConfig != nil {
|
||||
err := s.tlsConfig.Start()
|
||||
if err != nil {
|
||||
return E.Cause(err, "create TLS config")
|
||||
}
|
||||
if !common.Contains(s.tlsConfig.NextProtos(), http2.NextProtoTLS) {
|
||||
s.tlsConfig.SetNextProtos(append([]string{http2.NextProtoTLS}, s.tlsConfig.NextProtos()...))
|
||||
}
|
||||
if !common.Contains(s.tlsConfig.NextProtos(), "http/1.1") {
|
||||
s.tlsConfig.SetNextProtos(append(s.tlsConfig.NextProtos(), "http/1.1"))
|
||||
}
|
||||
}
|
||||
tcpListener, err := s.listener.ListenTCP()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if s.tlsConfig != nil {
|
||||
tcpListener = aTLS.NewListener(tcpListener, s.tlsConfig)
|
||||
}
|
||||
go func() {
|
||||
serveErr := s.httpServer.Serve(tcpListener)
|
||||
if serveErr != nil && s.ctx.Err() == nil {
|
||||
s.logger.Error("serve error: ", serveErr)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) Close() error {
|
||||
s.cancel()
|
||||
if s.httpServer != nil {
|
||||
s.httpServer.Close()
|
||||
}
|
||||
if s.grpcServer != nil {
|
||||
s.grpcServer.Stop()
|
||||
}
|
||||
if s.startedService != nil {
|
||||
s.startedService.Close()
|
||||
}
|
||||
return common.Close(
|
||||
common.PtrOrNil(s.listener),
|
||||
s.tlsConfig,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/cors"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const (
|
||||
contentTypeGRPC = "application/grpc"
|
||||
contentTypeGRPCWeb = "application/grpc-web"
|
||||
contentTypeGRPCWebText = "application/grpc-web-text"
|
||||
)
|
||||
|
||||
// newHTTPHandler additionally accepts gRPC-Web requests
|
||||
// (https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md) and gRPC-Web
|
||||
// streams over WebSocket, wire compatible with the improbable-eng/grpc-web
|
||||
// client transports.
|
||||
func newHTTPHandler(logger log.ContextLogger, grpcServer *grpc.Server, options option.APIServiceOptions) http.Handler {
|
||||
allowedOrigins := options.AccessControlAllowOrigin
|
||||
if len(allowedOrigins) == 0 {
|
||||
allowedOrigins = []string{"*"}
|
||||
}
|
||||
corsHandler := cors.New(cors.Options{
|
||||
AllowedOrigins: allowedOrigins,
|
||||
AllowedMethods: []string{http.MethodPost, http.MethodOptions},
|
||||
AllowedHeaders: []string{"Content-Type", "Authorization", "X-Grpc-Web", "X-User-Agent", "Grpc-Timeout"},
|
||||
ExposedHeaders: []string{"Grpc-Status", "Grpc-Message", "Grpc-Status-Details-Bin"},
|
||||
AllowPrivateNetwork: options.AccessControlAllowPrivateNetwork,
|
||||
MaxAge: 300,
|
||||
})
|
||||
return corsHandler.Handler(&webBridge{
|
||||
logger: logger,
|
||||
grpcServer: grpcServer,
|
||||
})
|
||||
}
|
||||
|
||||
type webBridge struct {
|
||||
logger log.ContextLogger
|
||||
grpcServer *grpc.Server
|
||||
}
|
||||
|
||||
func (b *webBridge) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||
contentType := request.Header.Get("Content-Type")
|
||||
switch {
|
||||
case isWebSocketGRPCRequest(request):
|
||||
b.serveWebSocket(writer, request)
|
||||
case request.Method == http.MethodPost && strings.HasPrefix(contentType, contentTypeGRPCWeb):
|
||||
b.serveWeb(writer, request)
|
||||
case request.ProtoMajor == 2 && strings.HasPrefix(contentType, contentTypeGRPC):
|
||||
b.grpcServer.ServeHTTP(writer, request)
|
||||
default:
|
||||
http.NotFound(writer, request)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *webBridge) serveWeb(writer http.ResponseWriter, request *http.Request) {
|
||||
isTextFormat := strings.HasPrefix(request.Header.Get("Content-Type"), contentTypeGRPCWebText)
|
||||
webContentType := contentTypeGRPCWeb
|
||||
grpcRequest := request.Clone(request.Context())
|
||||
if isTextFormat {
|
||||
webContentType = contentTypeGRPCWebText
|
||||
grpcRequest.Body = &bodyReadCloser{
|
||||
Reader: base64.NewDecoder(base64.StdEncoding, request.Body),
|
||||
Closer: request.Body,
|
||||
}
|
||||
}
|
||||
// The gRPC server handler transport only accepts requests it sees as
|
||||
// native gRPC over HTTP/2.
|
||||
grpcRequest.ProtoMajor = 2
|
||||
grpcRequest.ProtoMinor = 0
|
||||
grpcRequest.Header.Set("Content-Type", strings.Replace(request.Header.Get("Content-Type"), webContentType, contentTypeGRPC, 1))
|
||||
grpcRequest.Header.Del("Content-Length")
|
||||
response := newWebResponseWriter(writer, isTextFormat)
|
||||
b.grpcServer.ServeHTTP(response, grpcRequest)
|
||||
response.finish()
|
||||
}
|
||||
|
||||
type bodyReadCloser struct {
|
||||
io.Reader
|
||||
io.Closer
|
||||
}
|
||||
|
||||
// webResponseWriter translates a native gRPC response into a gRPC-Web
|
||||
// response: headers set after the first write, including the gRPC status the
|
||||
// handler transport sets via http2.TrailerPrefix keys, become a trailer
|
||||
// frame at the end of the body instead of HTTP trailers.
|
||||
type webResponseWriter struct {
|
||||
writer http.ResponseWriter
|
||||
rawWriter http.ResponseWriter
|
||||
header http.Header
|
||||
contentType string
|
||||
wroteHeaders bool
|
||||
wroteBody bool
|
||||
}
|
||||
|
||||
func newWebResponseWriter(writer http.ResponseWriter, isTextFormat bool) *webResponseWriter {
|
||||
response := &webResponseWriter{
|
||||
writer: writer,
|
||||
rawWriter: writer,
|
||||
header: make(http.Header),
|
||||
contentType: contentTypeGRPCWeb,
|
||||
}
|
||||
if isTextFormat {
|
||||
response.writer = newBase64ResponseWriter(writer)
|
||||
response.contentType = contentTypeGRPCWebText
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
func (w *webResponseWriter) Header() http.Header {
|
||||
return w.header
|
||||
}
|
||||
|
||||
func (w *webResponseWriter) Write(content []byte) (int, error) {
|
||||
if !w.wroteHeaders {
|
||||
w.prepareHeaders()
|
||||
w.wroteHeaders = true
|
||||
}
|
||||
w.wroteBody = true
|
||||
return w.writer.Write(content)
|
||||
}
|
||||
|
||||
func (w *webResponseWriter) WriteHeader(statusCode int) {
|
||||
if !w.wroteHeaders {
|
||||
w.prepareHeaders()
|
||||
w.wroteHeaders = true
|
||||
}
|
||||
w.writer.WriteHeader(statusCode)
|
||||
}
|
||||
|
||||
func (w *webResponseWriter) Flush() {
|
||||
// Flushing before anything was written would commit a 200 response
|
||||
// even for requests that end up as trailers-only responses.
|
||||
if w.wroteHeaders || w.wroteBody {
|
||||
flushWriter(w.writer)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *webResponseWriter) prepareHeaders() {
|
||||
rawHeader := w.rawWriter.Header()
|
||||
for key, values := range w.header {
|
||||
canonicalKey := http.CanonicalHeaderKey(strings.TrimPrefix(key, http2.TrailerPrefix))
|
||||
if canonicalKey == "Trailer" {
|
||||
continue
|
||||
}
|
||||
if canonicalKey == "Content-Type" {
|
||||
newValues := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
newValues = append(newValues, strings.Replace(value, contentTypeGRPC, w.contentType, 1))
|
||||
}
|
||||
values = newValues
|
||||
}
|
||||
rawHeader[canonicalKey] = values
|
||||
}
|
||||
}
|
||||
|
||||
func (w *webResponseWriter) finish() {
|
||||
if w.wroteHeaders || w.wroteBody {
|
||||
w.writeTrailerFrame()
|
||||
} else {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
flushWriter(w.writer)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *webResponseWriter) writeTrailerFrame() {
|
||||
flushedKeys := make(map[string]bool)
|
||||
for key := range w.rawWriter.Header() {
|
||||
flushedKeys[strings.ToLower(key)] = true
|
||||
}
|
||||
trailerHeader := make(http.Header)
|
||||
for key, values := range w.header {
|
||||
lowerKey := strings.ToLower(strings.TrimPrefix(key, http2.TrailerPrefix))
|
||||
if lowerKey == "trailer" || flushedKeys[lowerKey] {
|
||||
continue
|
||||
}
|
||||
trailerHeader[lowerKey] = values
|
||||
}
|
||||
var trailerBuffer bytes.Buffer
|
||||
trailerHeader.Write(&trailerBuffer)
|
||||
w.writer.Write(webMetadataFrameHeader(trailerBuffer.Len()))
|
||||
w.writer.Write(trailerBuffer.Bytes())
|
||||
flushWriter(w.writer)
|
||||
}
|
||||
|
||||
func webMetadataFrameHeader(payloadLength int) []byte {
|
||||
return binary.BigEndian.AppendUint32([]byte{1 << 7}, uint32(payloadLength))
|
||||
}
|
||||
|
||||
func flushWriter(writer http.ResponseWriter) {
|
||||
flusher, isFlusher := writer.(http.Flusher)
|
||||
if isFlusher {
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
type base64ResponseWriter struct {
|
||||
wrapped http.ResponseWriter
|
||||
encoder io.WriteCloser
|
||||
}
|
||||
|
||||
func newBase64ResponseWriter(wrapped http.ResponseWriter) http.ResponseWriter {
|
||||
writer := &base64ResponseWriter{wrapped: wrapped}
|
||||
writer.encoder = base64.NewEncoder(base64.StdEncoding, wrapped)
|
||||
return writer
|
||||
}
|
||||
|
||||
func (w *base64ResponseWriter) Header() http.Header {
|
||||
return w.wrapped.Header()
|
||||
}
|
||||
|
||||
func (w *base64ResponseWriter) Write(content []byte) (int, error) {
|
||||
return w.encoder.Write(content)
|
||||
}
|
||||
|
||||
func (w *base64ResponseWriter) WriteHeader(statusCode int) {
|
||||
w.wrapped.WriteHeader(statusCode)
|
||||
}
|
||||
|
||||
func (w *base64ResponseWriter) Flush() {
|
||||
w.encoder.Close()
|
||||
w.encoder = base64.NewEncoder(base64.StdEncoding, w.wrapped)
|
||||
flushWriter(w.wrapped)
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"github.com/coder/websocket"
|
||||
"golang.org/x/net/http/httpguts"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
const (
|
||||
webSocketSubprotocol = "grpc-websockets"
|
||||
webSocketReadLimit = 1 << 22
|
||||
webSocketPingInterval = 30 * time.Second
|
||||
)
|
||||
|
||||
func isWebSocketGRPCRequest(request *http.Request) bool {
|
||||
return httpguts.HeaderValuesContainsToken(request.Header.Values("Upgrade"), "websocket") &&
|
||||
httpguts.HeaderValuesContainsToken(request.Header.Values("Sec-Websocket-Protocol"), webSocketSubprotocol)
|
||||
}
|
||||
|
||||
// serveWebSocket carries a single gRPC stream over a WebSocket connection:
|
||||
// the first client message contains the request metadata, each subsequent
|
||||
// binary message is prefixed with 0 for body data or is a single 1 byte for
|
||||
// the half-close signal, and the server sends gRPC-Web frames back.
|
||||
func (b *webBridge) serveWebSocket(writer http.ResponseWriter, request *http.Request) {
|
||||
conn, err := websocket.Accept(writer, request, &websocket.AcceptOptions{
|
||||
Subprotocols: []string{webSocketSubprotocol},
|
||||
InsecureSkipVerify: true,
|
||||
})
|
||||
if err != nil {
|
||||
b.logger.Error("upgrade websocket request: ", err)
|
||||
return
|
||||
}
|
||||
conn.SetReadLimit(webSocketReadLimit)
|
||||
ctx, cancel := context.WithCancel(request.Context())
|
||||
defer cancel()
|
||||
messageType, firstMessage, err := conn.Read(ctx)
|
||||
if err != nil {
|
||||
conn.CloseNow()
|
||||
return
|
||||
}
|
||||
if messageType != websocket.MessageBinary {
|
||||
conn.CloseNow()
|
||||
return
|
||||
}
|
||||
header, err := parseWebSocketHeader(firstMessage)
|
||||
if err != nil {
|
||||
b.logger.Error("parse websocket request metadata: ", err)
|
||||
conn.CloseNow()
|
||||
return
|
||||
}
|
||||
contentType := header.Get("Content-Type")
|
||||
if contentType == "" {
|
||||
header.Set("Content-Type", contentTypeGRPC)
|
||||
} else {
|
||||
header.Set("Content-Type", strings.Replace(contentType, contentTypeGRPCWeb, contentTypeGRPC, 1))
|
||||
}
|
||||
header.Del("Content-Length")
|
||||
response := newWebSocketResponseWriter(ctx, conn)
|
||||
grpcRequest := request.WithContext(ctx)
|
||||
grpcRequest.Method = http.MethodPost
|
||||
grpcRequest.ProtoMajor = 2
|
||||
grpcRequest.ProtoMinor = 0
|
||||
grpcRequest.Header = header
|
||||
grpcRequest.Body = &webSocketBodyReader{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
conn: conn,
|
||||
response: response,
|
||||
}
|
||||
go keepWebSocketAlive(ctx, conn)
|
||||
b.grpcServer.ServeHTTP(response, grpcRequest)
|
||||
response.writeTrailerFrame()
|
||||
conn.Close(websocket.StatusNormalClosure, "")
|
||||
}
|
||||
|
||||
func parseWebSocketHeader(content []byte) (http.Header, error) {
|
||||
reader := textproto.NewReader(bufio.NewReader(io.MultiReader(bytes.NewReader(content), strings.NewReader("\r\n"))))
|
||||
mimeHeader, err := reader.ReadMIMEHeader()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return http.Header(mimeHeader), nil
|
||||
}
|
||||
|
||||
func keepWebSocketAlive(ctx context.Context, conn *websocket.Conn) {
|
||||
ticker := time.NewTicker(webSocketPingInterval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
err := conn.Ping(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type webSocketResponseWriter struct {
|
||||
ctx context.Context
|
||||
conn *websocket.Conn
|
||||
header http.Header
|
||||
flushedHeader http.Header
|
||||
wroteHeaders bool
|
||||
wroteTrailers bool
|
||||
}
|
||||
|
||||
func newWebSocketResponseWriter(ctx context.Context, conn *websocket.Conn) *webSocketResponseWriter {
|
||||
return &webSocketResponseWriter{
|
||||
ctx: ctx,
|
||||
conn: conn,
|
||||
header: make(http.Header),
|
||||
flushedHeader: make(http.Header),
|
||||
}
|
||||
}
|
||||
|
||||
func (w *webSocketResponseWriter) Header() http.Header {
|
||||
return w.header
|
||||
}
|
||||
|
||||
func (w *webSocketResponseWriter) Write(content []byte) (int, error) {
|
||||
if !w.wroteHeaders {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
err := w.conn.Write(w.ctx, websocket.MessageBinary, content)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(content), nil
|
||||
}
|
||||
|
||||
func (w *webSocketResponseWriter) WriteHeader(statusCode int) {
|
||||
if w.wroteHeaders {
|
||||
return
|
||||
}
|
||||
w.wroteHeaders = true
|
||||
headerFrame := make(http.Header)
|
||||
for key, values := range w.header {
|
||||
canonicalKey := http.CanonicalHeaderKey(key)
|
||||
if canonicalKey == "Trailer" {
|
||||
continue
|
||||
}
|
||||
w.flushedHeader[canonicalKey] = values
|
||||
headerFrame[canonicalKey] = values
|
||||
}
|
||||
w.writeHeaderFrame(headerFrame)
|
||||
}
|
||||
|
||||
func (w *webSocketResponseWriter) Flush() {
|
||||
}
|
||||
|
||||
func (w *webSocketResponseWriter) writeHeaderFrame(header http.Header) {
|
||||
var headerBuffer bytes.Buffer
|
||||
header.Write(&headerBuffer)
|
||||
frame := make([]byte, 0, 5+headerBuffer.Len())
|
||||
frame = append(frame, webMetadataFrameHeader(headerBuffer.Len())...)
|
||||
frame = append(frame, headerBuffer.Bytes()...)
|
||||
w.conn.Write(w.ctx, websocket.MessageBinary, frame)
|
||||
}
|
||||
|
||||
func (w *webSocketResponseWriter) writeTrailerFrame() {
|
||||
if w.wroteTrailers {
|
||||
return
|
||||
}
|
||||
w.wroteTrailers = true
|
||||
trailerHeader := make(http.Header)
|
||||
for key, values := range w.header {
|
||||
lowerKey := strings.ToLower(strings.TrimPrefix(key, http2.TrailerPrefix))
|
||||
if lowerKey == "trailer" {
|
||||
continue
|
||||
}
|
||||
_, flushed := w.flushedHeader[http.CanonicalHeaderKey(lowerKey)]
|
||||
if flushed {
|
||||
continue
|
||||
}
|
||||
trailerHeader[lowerKey] = values
|
||||
}
|
||||
w.writeHeaderFrame(trailerHeader)
|
||||
}
|
||||
|
||||
type webSocketBodyReader struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
conn *websocket.Conn
|
||||
response *webSocketResponseWriter
|
||||
remaining []byte
|
||||
}
|
||||
|
||||
func (r *webSocketBodyReader) Read(buffer []byte) (int, error) {
|
||||
if len(r.remaining) > 0 {
|
||||
n := copy(buffer, r.remaining)
|
||||
r.remaining = r.remaining[n:]
|
||||
return n, nil
|
||||
}
|
||||
for {
|
||||
messageType, payload, err := r.conn.Read(r.ctx)
|
||||
if err != nil {
|
||||
r.cancel()
|
||||
return 0, io.EOF
|
||||
}
|
||||
if messageType != websocket.MessageBinary {
|
||||
return 0, E.New("unexpected non-binary websocket message")
|
||||
}
|
||||
if len(payload) == 0 {
|
||||
continue
|
||||
}
|
||||
if payload[0] == 1 {
|
||||
go r.waitForClose()
|
||||
return 0, io.EOF
|
||||
}
|
||||
content := payload[1:]
|
||||
if len(content) == 0 {
|
||||
continue
|
||||
}
|
||||
n := copy(buffer, content)
|
||||
r.remaining = content[n:]
|
||||
return n, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *webSocketBodyReader) waitForClose() {
|
||||
for {
|
||||
_, _, err := r.conn.Read(r.ctx)
|
||||
if err != nil {
|
||||
r.cancel()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close is called by the gRPC handler transport after the stream status has
|
||||
// been written; the trailer frame must be sent before the connection closes.
|
||||
func (r *webSocketBodyReader) Close() error {
|
||||
r.response.writeTrailerFrame()
|
||||
return r.conn.Close(websocket.StatusNormalClosure, "")
|
||||
}
|
||||
Reference in New Issue
Block a user