mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-06-28 19:51:33 +00:00
239 lines
6.8 KiB
Go
239 lines
6.8 KiB
Go
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, dashboard *dashboard) http.Handler {
|
|
allowedOrigins := options.AccessControlAllowOrigin
|
|
if len(allowedOrigins) == 0 {
|
|
allowedOrigins = []string{"*"}
|
|
}
|
|
corsHandler := cors.New(cors.Options{
|
|
AllowedOrigins: allowedOrigins,
|
|
AllowedMethods: []string{http.MethodGet, 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,
|
|
dashboard: dashboard,
|
|
})
|
|
}
|
|
|
|
type webBridge struct {
|
|
logger log.ContextLogger
|
|
grpcServer *grpc.Server
|
|
dashboard *dashboard
|
|
}
|
|
|
|
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)
|
|
case b.dashboard != nil:
|
|
b.dashboard.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)
|
|
}
|