sing-box/daemon/client.go
2026-06-25 17:38:53 +08:00

85 lines
2.5 KiB
Go

package daemon
import (
"context"
"crypto/tls"
"net"
"net/url"
E "github.com/sagernet/sing/common/exceptions"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
)
type RemoteClientOptions struct {
ServerURL string
Secret string
}
func (o RemoteClientOptions) ServerTarget() (string, credentials.TransportCredentials, error) {
if o.ServerURL == "" {
return "", nil, E.New("missing server URL")
}
serverURL, err := url.Parse(o.ServerURL)
if err != nil {
return "", nil, E.Cause(err, "invalid server URL: ", o.ServerURL)
}
var enableTLS bool
switch serverURL.Scheme {
case "http":
case "https":
enableTLS = true
default:
return "", nil, E.New("invalid server URL scheme: ", serverURL.Scheme, ", expected http or https")
}
host := serverURL.Hostname()
if host == "" {
return "", nil, E.New("missing host in server URL: ", o.ServerURL)
}
port := serverURL.Port()
if port == "" {
if enableTLS {
port = "443"
} else {
port = "80"
}
}
transportCredentials := insecure.NewCredentials()
if enableTLS {
transportCredentials = credentials.NewTLS(&tls.Config{ServerName: host})
}
return net.JoinHostPort(host, port), transportCredentials, nil
}
func NewRemoteClient(options RemoteClientOptions) (*grpc.ClientConn, error) {
target, transportCredentials, err := options.ServerTarget()
if err != nil {
return nil, err
}
return grpc.NewClient(target,
grpc.WithTransportCredentials(transportCredentials),
grpc.WithUnaryInterceptor(NewClientAuthUnaryInterceptor(options.Secret)),
grpc.WithStreamInterceptor(NewClientAuthStreamInterceptor(options.Secret)),
)
}
func NewClientAuthUnaryInterceptor(secret string) grpc.UnaryClientInterceptor {
return func(ctx context.Context, method string, request, reply any, clientConn *grpc.ClientConn, invoker grpc.UnaryInvoker, options ...grpc.CallOption) error {
if secret != "" {
ctx = metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer "+secret)
}
return invoker(ctx, method, request, reply, clientConn, options...)
}
}
func NewClientAuthStreamInterceptor(secret string) grpc.StreamClientInterceptor {
return func(ctx context.Context, desc *grpc.StreamDesc, clientConn *grpc.ClientConn, method string, streamer grpc.Streamer, options ...grpc.CallOption) (grpc.ClientStream, error) {
if secret != "" {
ctx = metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer "+secret)
}
return streamer(ctx, desc, clientConn, method, options...)
}
}