From f2a4bfd390225240304290d398ea3848805dd5be Mon Sep 17 00:00:00 2001 From: Steven Rhodes Date: Wed, 12 Jul 2023 08:50:18 -0700 Subject: [PATCH] Add a sansshell service for proxying HTTP requests. (#266) * Add a sansshell service for proxying HTTP requests. This adds a structured way to make an HTTP request to a port on localhost. The structure allows fine-grained access control on the http request. The httpoverrpc proxy subcommand launches a web server that translates http calls into grpc calls, creating a http->grpc->http flow. The webserver only supports one host at a time. The httpoverrpc get subcommand allows passing in exact arguments to call, giving a way to query the same port across many machines. A few design notes: - I've limited the rpc interface to localhost because sansshell is normally used for host-local actions and I don't have a clear use case for making this call cross-host. If we want to make this more general, I could rename things or we could add more RPCs. - I'm only supporting http calls. Supporting https might require some thought about how to handle certificates and server identity. - We don't automatically follow redirects because we expect users of the RPCs to do their own redirect-following logic. - Sufficiently-large http body sizes that are too big to fit into a single grpc message will fail. We could get around size limitations by streaming the response, but that adds complexity to the API. To test this out, try running the following with each command in a separate terminal. ``` python -m http.Server go run ./cmd/sansshell-server go run ./cmd/sanssh -targets localhost httpoverrpc proxy -addr localhost:8001 8000 curl localhost:8001 ``` * Fix lint errors * Address some review comments --- cmd/proxy-server/default-policy.rego | 5 + cmd/proxy-server/main.go | 1 + cmd/sanssh/main.go | 1 + cmd/sansshell-server/default-policy.rego | 4 + cmd/sansshell-server/main.go | 1 + services/httpoverrpc/client/client.go | 271 +++++++++++ services/httpoverrpc/client/client_test.go | 161 +++++++ services/httpoverrpc/httpoverrpc.go | 22 + services/httpoverrpc/httpoverrpc.pb.go | 432 ++++++++++++++++++ services/httpoverrpc/httpoverrpc.proto | 54 +++ services/httpoverrpc/httpoverrpc_grpc.pb.go | 124 +++++ .../httpoverrpc/httpoverrpc_grpcproxy.pb.go | 100 ++++ services/httpoverrpc/server/server.go | 87 ++++ services/httpoverrpc/server/server_test.go | 108 +++++ 14 files changed, 1371 insertions(+) create mode 100644 services/httpoverrpc/client/client.go create mode 100644 services/httpoverrpc/client/client_test.go create mode 100644 services/httpoverrpc/httpoverrpc.go create mode 100644 services/httpoverrpc/httpoverrpc.pb.go create mode 100644 services/httpoverrpc/httpoverrpc.proto create mode 100644 services/httpoverrpc/httpoverrpc_grpc.pb.go create mode 100644 services/httpoverrpc/httpoverrpc_grpcproxy.pb.go create mode 100644 services/httpoverrpc/server/server.go create mode 100644 services/httpoverrpc/server/server_test.go diff --git a/cmd/proxy-server/default-policy.rego b/cmd/proxy-server/default-policy.rego index 4427ac0a..0ceab6e7 100644 --- a/cmd/proxy-server/default-policy.rego +++ b/cmd/proxy-server/default-policy.rego @@ -20,6 +20,11 @@ allow { ## Access control for targets +# Allow proxying HTTP requests +allow { + input.method = "/HTTPOverRPC.HTTPOverRPC/Localhost" +} + # Allow anyone to call healthcheck on any host allow { input.method = "/HealthCheck.HealthCheck/Ok" diff --git a/cmd/proxy-server/main.go b/cmd/proxy-server/main.go index a7302819..a87bba63 100644 --- a/cmd/proxy-server/main.go +++ b/cmd/proxy-server/main.go @@ -47,6 +47,7 @@ import ( _ "github.com/Snowflake-Labs/sansshell/services/ansible" _ "github.com/Snowflake-Labs/sansshell/services/exec" _ "github.com/Snowflake-Labs/sansshell/services/healthcheck" + _ "github.com/Snowflake-Labs/sansshell/services/httpoverrpc" _ "github.com/Snowflake-Labs/sansshell/services/localfile" _ "github.com/Snowflake-Labs/sansshell/services/packages" _ "github.com/Snowflake-Labs/sansshell/services/process" diff --git a/cmd/sanssh/main.go b/cmd/sanssh/main.go index 71a01927..4eda6eea 100644 --- a/cmd/sanssh/main.go +++ b/cmd/sanssh/main.go @@ -45,6 +45,7 @@ import ( _ "github.com/Snowflake-Labs/sansshell/services/exec/client" _ "github.com/Snowflake-Labs/sansshell/services/fdb/client" _ "github.com/Snowflake-Labs/sansshell/services/healthcheck/client" + _ "github.com/Snowflake-Labs/sansshell/services/httpoverrpc/client" _ "github.com/Snowflake-Labs/sansshell/services/localfile/client" _ "github.com/Snowflake-Labs/sansshell/services/packages/client" _ "github.com/Snowflake-Labs/sansshell/services/power/client" diff --git a/cmd/sansshell-server/default-policy.rego b/cmd/sansshell-server/default-policy.rego index 7676f197..27ba1989 100644 --- a/cmd/sansshell-server/default-policy.rego +++ b/cmd/sansshell-server/default-policy.rego @@ -15,6 +15,10 @@ allow { input.method = "/Dns.Lookup/Lookup" } +allow { + input.method = "/HTTPOverRPC.HTTPOverRPC/Localhost" +} + allow { input.type = "LocalFile.ReadActionRequest" input.message.file.filename = "/etc/hosts" diff --git a/cmd/sansshell-server/main.go b/cmd/sansshell-server/main.go index e15b464a..61e206e9 100644 --- a/cmd/sansshell-server/main.go +++ b/cmd/sansshell-server/main.go @@ -58,6 +58,7 @@ import ( ansible "github.com/Snowflake-Labs/sansshell/services/ansible/server" _ "github.com/Snowflake-Labs/sansshell/services/dns/server" _ "github.com/Snowflake-Labs/sansshell/services/exec/server" + _ "github.com/Snowflake-Labs/sansshell/services/httpoverrpc/server" fdbserver "github.com/Snowflake-Labs/sansshell/services/fdb/server" _ "github.com/Snowflake-Labs/sansshell/services/healthcheck/server" diff --git a/services/httpoverrpc/client/client.go b/services/httpoverrpc/client/client.go new file mode 100644 index 00000000..8fecf992 --- /dev/null +++ b/services/httpoverrpc/client/client.go @@ -0,0 +1,271 @@ +/* Copyright (c) 2023 Snowflake Inc. All rights reserved. + + Licensed under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + +// Package client provides the client interface for 'httpoverrpc' +package client + +import ( + "context" + "flag" + "fmt" + "io" + "net" + "net/http" + "os" + "strconv" + "strings" + "time" + + "github.com/google/subcommands" + + "github.com/Snowflake-Labs/sansshell/client" + pb "github.com/Snowflake-Labs/sansshell/services/httpoverrpc" + "github.com/Snowflake-Labs/sansshell/services/util" +) + +const subPackage = "httpoverrpc" + +func init() { + subcommands.Register(&httpCmd{}, subPackage) +} + +func (*httpCmd) GetSubpackage(f *flag.FlagSet) *subcommands.Commander { + c := client.SetupSubpackage(subPackage, f) + c.Register(&proxyCmd{}, "") + c.Register(&getCmd{}, "") + return c +} + +type httpCmd struct{} + +func (*httpCmd) Name() string { return subPackage } +func (p *httpCmd) Synopsis() string { + return client.GenerateSynopsis(p.GetSubpackage(flag.NewFlagSet("", flag.ContinueOnError)), 2) +} +func (p *httpCmd) Usage() string { + return client.GenerateUsage(subPackage, p.Synopsis()) +} +func (*httpCmd) SetFlags(f *flag.FlagSet) {} + +func (p *httpCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus { + c := p.GetSubpackage(f) + return c.Execute(ctx, args...) +} + +type proxyCmd struct { + listenAddr string +} + +func (*proxyCmd) Name() string { return "proxy" } +func (*proxyCmd) Synopsis() string { + return "Starts a web server that proxies to a port on a remote host" +} +func (*proxyCmd) Usage() string { + return `proxy [-addr ip:port] remoteport: + Launch a HTTP proxy server that translates HTTP calls into SansShell calls. Any HTTP request to the proxy server will be sent to the sansshell node and translated into a call to the node's localhost on the specified remote port. If -addr is unspecified, it listens on localhost on a random port. Only a single target at a time is supported. +` +} + +func (p *proxyCmd) SetFlags(f *flag.FlagSet) { + f.StringVar(&p.listenAddr, "addr", "localhost:0", "Address to listen on, defaults to a random localhost port") +} + +// This context detachment is temporary until we use go1.21 and context.WithoutCancel is available. +type noCancel struct { + ctx context.Context +} + +func (c noCancel) Deadline() (time.Time, bool) { return time.Time{}, false } +func (c noCancel) Done() <-chan struct{} { return nil } +func (c noCancel) Err() error { return nil } +func (c noCancel) Value(key interface{}) interface{} { return c.ctx.Value(key) } + +// WithoutCancel returns a context that is never canceled. +func WithoutCancel(ctx context.Context) context.Context { + return noCancel{ctx: ctx} +} + +func sendError(resp http.ResponseWriter, code int, err error) { + resp.WriteHeader(code) + if _, err := resp.Write([]byte(err.Error())); err != nil { + fmt.Fprintln(os.Stderr, err) + } +} + +func (p *proxyCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus { + // Ignore the parent context timeout because we don't want to time out here. + ctx = WithoutCancel(ctx) + state := args[0].(*util.ExecuteState) + if f.NArg() != 1 { + fmt.Fprintln(os.Stderr, "Please specify a port to proxy.") + return subcommands.ExitUsageError + } + if len(state.Out) != 1 { + fmt.Fprintln(os.Stderr, "Proxying can only be done with exactly one target.") + return subcommands.ExitUsageError + } + port, err := strconv.Atoi(f.Arg(0)) + if err != nil { + fmt.Fprintln(os.Stderr, "Port could not be interpreted as a number.") + return subcommands.ExitUsageError + } + + proxy := pb.NewHTTPOverRPCClientProxy(state.Conn) + + m := http.NewServeMux() + m.HandleFunc("/", func(httpResp http.ResponseWriter, httpReq *http.Request) { + var reqHeaders []*pb.Header + for k, v := range httpReq.Header { + reqHeaders = append(reqHeaders, &pb.Header{Key: k, Values: v}) + } + body, err := io.ReadAll(httpReq.Body) + if err != nil { + sendError(httpResp, http.StatusBadRequest, err) + return + } + req := &pb.LocalhostHTTPRequest{ + Request: &pb.HTTPRequest{ + RequestUri: httpReq.RequestURI, + Method: httpReq.Method, + Headers: reqHeaders, + Body: body, + }, + Port: int32(port), + } + resp, err := proxy.Localhost(ctx, req) + if err != nil { + sendError(httpResp, http.StatusInternalServerError, err) + return + } + for _, h := range resp.Headers { + for _, v := range h.Values { + httpResp.Header().Add(h.Key, v) + } + } + httpResp.WriteHeader(int(resp.StatusCode)) + if _, err := httpResp.Write(resp.Body); err != nil { + fmt.Fprintln(os.Stdout, err) + } + }) + l, err := net.Listen("tcp4", p.listenAddr) + if err != nil { + fmt.Fprintf(state.Err[0], "Unable to listen on %v.\n", p.listenAddr) + return subcommands.ExitFailure + } + fmt.Fprintf(state.Out[0], "Listening on http://%v, ctrl-c to exit...", l.Addr()) + if err := http.Serve(l, m); err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + return subcommands.ExitUsageError + } + return subcommands.ExitSuccess +} + +type repeatedString []string + +func (i *repeatedString) String() string { + if i == nil { + return "[]" + } + return fmt.Sprint([]string(*i)) +} + +func (i *repeatedString) Set(value string) error { + *i = append(*i, value) + return nil +} + +type getCmd struct { + method string + headers repeatedString + body string + showResponseHeaders bool +} + +func (*getCmd) Name() string { return "get" } +func (*getCmd) Synopsis() string { return "Makes a HTTP call to a port on a remote host" } +func (*getCmd) Usage() string { + return `get [-method METHOD] [-header Header...] [-body body] remoteport request_uri: + Make a HTTP request to a specified port on the remote host, defaulting to GET. +` +} + +func (g *getCmd) SetFlags(f *flag.FlagSet) { + f.StringVar(&g.method, "method", "GET", "Method to use in the HTTP request") + f.Var(&g.headers, "header", "Header to send in the request, may be specified multiple times.") + f.StringVar(&g.body, "body", "", "Body to send in request") + f.BoolVar(&g.showResponseHeaders, "show-response-headers", false, "If true, print response code and headers") +} + +func (g *getCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus { + state := args[0].(*util.ExecuteState) + if f.NArg() != 2 { + fmt.Fprintln(os.Stderr, "Please specify exactly two arguments: a port and a query.") + return subcommands.ExitUsageError + } + port, err := strconv.Atoi(f.Arg(0)) + if err != nil { + fmt.Fprintln(os.Stderr, "Port could not be interpreted as a number.") + return subcommands.ExitUsageError + } + + var reqHeaders []*pb.Header + for _, v := range g.headers { + split := strings.SplitN(v, ":", 2) + if len(split) != 2 { + fmt.Fprintf(os.Stderr, "Unable to parse %q as header, expected \"Key: value\" format", v) + return subcommands.ExitUsageError + } + reqHeaders = append(reqHeaders, &pb.Header{Key: split[0], Values: []string{strings.TrimSpace(split[1])}}) + } + + proxy := pb.NewHTTPOverRPCClientProxy(state.Conn) + + req := &pb.LocalhostHTTPRequest{ + Request: &pb.HTTPRequest{ + RequestUri: f.Arg(1), + Method: g.method, + Headers: reqHeaders, + Body: []byte(g.body), + }, + Port: int32(port), + } + + resp, err := proxy.LocalhostOneMany(ctx, req) + if err != nil { + // Emit this to every error file as it's not specific to a given target. + for _, e := range state.Err { + fmt.Fprintf(e, "All targets - could not execute: %v\n", err) + } + return subcommands.ExitFailure + } + for r := range resp { + if r.Error != nil { + fmt.Fprintf(state.Err[r.Index], "%v\n", r.Error) + continue + } + if g.showResponseHeaders { + fmt.Fprintf(state.Out[r.Index], "%v %v\n", r.Resp.StatusCode, http.StatusText(int(r.Resp.StatusCode))) + for _, h := range r.Resp.Headers { + for _, v := range h.Values { + fmt.Fprintf(state.Out[r.Index], "%v: %v\n", h.Key, v) + + } + } + } + fmt.Fprintln(state.Out[r.Index], string(r.Resp.Body)) + } + return subcommands.ExitSuccess +} diff --git a/services/httpoverrpc/client/client_test.go b/services/httpoverrpc/client/client_test.go new file mode 100644 index 00000000..85e5086b --- /dev/null +++ b/services/httpoverrpc/client/client_test.go @@ -0,0 +1,161 @@ +package client + +import ( + "context" + "flag" + "io" + "log" + "net" + "net/http" + "os" + "strings" + "testing" + + "github.com/Snowflake-Labs/sansshell/proxy/proxy" + "github.com/Snowflake-Labs/sansshell/services" + _ "github.com/Snowflake-Labs/sansshell/services/httpoverrpc/server" + "github.com/Snowflake-Labs/sansshell/services/util" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/test/bufconn" +) + +var ( + bufSize = 1024 * 1024 + lis *bufconn.Listener +) + +func bufDialer(context.Context, string) (net.Conn, error) { + return lis.Dial() +} + +func TestMain(m *testing.M) { + lis = bufconn.Listen(bufSize) + s := grpc.NewServer() + for _, svc := range services.ListServices() { + svc.Register(s) + } + go func() { + if err := s.Serve(lis); err != nil { + log.Fatalf("Server exited with error: %v", err) + } + }() + defer s.GracefulStop() + + os.Exit(m.Run()) +} + +func TestProxy(t *testing.T) { + ctx := context.Background() + + // Set up web server + m := http.NewServeMux() + m.HandleFunc("/", func(httpResp http.ResponseWriter, httpReq *http.Request) { + _, _ = httpResp.Write([]byte("hello world")) + }) + l, err := net.Listen("tcp4", "localhost:0") + if err != nil { + t.Fatal(err) + } + go func() { _ = http.Serve(l, m) }() + + // Dial out to sansshell server set up in TestMain + conn, err := proxy.DialContext(ctx, "", []string{"bufnet"}, grpc.WithContextDialer(bufDialer), grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { conn.Close() }) + + // Start proxying command + f := flag.NewFlagSet("proxy", flag.PanicOnError) + p := &proxyCmd{} + p.SetFlags(f) + _, port, err := net.SplitHostPort(l.Addr().String()) + if err != nil { + t.Fatal(err) + } + if err := f.Parse([]string{port}); err != nil { + t.Fatal(err) + } + reader, writer := io.Pipe() + go p.Execute(ctx, f, &util.ExecuteState{ + Conn: conn, + Out: []io.Writer{writer}, + Err: []io.Writer{os.Stderr}, + }) + + // Find the port to use + buf := make([]byte, 1024) + if _, err := reader.Read(buf); err != nil { + t.Fatal(err) + } + msg := strings.Fields(string(buf)) + // Parse out "Listening on http://%v, " + addr := msg[2][:len(msg[2])-1] + + // Make a call + resp, err := http.Get(addr) + if err != nil { + t.Fatal(err) + } + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + want := "hello world" + if string(body) != want { + t.Errorf("got %q, want %q", body, want) + } +} + +func TestGet(t *testing.T) { + ctx := context.Background() + + // Set up web server + m := http.NewServeMux() + m.HandleFunc("/", func(httpResp http.ResponseWriter, httpReq *http.Request) { + _, _ = httpResp.Write([]byte("hello world")) + }) + l, err := net.Listen("tcp4", "localhost:0") + if err != nil { + t.Fatal(err) + } + go func() { _ = http.Serve(l, m) }() + + // Dial out to sansshell server set up in TestMain + conn, err := proxy.DialContext(ctx, "", []string{"bufnet"}, grpc.WithContextDialer(bufDialer), grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { conn.Close() }) + + // Start get command + f := flag.NewFlagSet("proxy", flag.PanicOnError) + g := &getCmd{} + g.SetFlags(f) + _, port, err := net.SplitHostPort(l.Addr().String()) + if err != nil { + t.Fatal(err) + } + if err := f.Parse([]string{port, "/"}); err != nil { + t.Fatal(err) + } + reader, writer := io.Pipe() + go g.Execute(ctx, f, &util.ExecuteState{ + Conn: conn, + Out: []io.Writer{writer}, + Err: []io.Writer{os.Stderr}, + }) + + // See if we got the data + buf := make([]byte, 1024) + n, err := reader.Read(buf) + if err != nil { + t.Fatal(err) + } + got := string(buf[:n]) + want := "hello world\n" + if got != want { + t.Errorf("got %q, want %q", got, want) + } +} diff --git a/services/httpoverrpc/httpoverrpc.go b/services/httpoverrpc/httpoverrpc.go new file mode 100644 index 00000000..d5058079 --- /dev/null +++ b/services/httpoverrpc/httpoverrpc.go @@ -0,0 +1,22 @@ +/* Copyright (c) 2023 Snowflake Inc. All rights reserved. + + Licensed under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + +// Package httpoverrpc defines the RPC interface for the sansshell HTTP actions. +package httpoverrpc + +// To regenerate the proto headers if the proto changes, just run go generate +// and this encodes the necessary magic: +//go:generate protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=require_unimplemented_servers=false:. --go-grpc_opt=paths=source_relative --go-grpcproxy_out=. --go-grpcproxy_opt=paths=source_relative httpoverrpc.proto diff --git a/services/httpoverrpc/httpoverrpc.pb.go b/services/httpoverrpc/httpoverrpc.pb.go new file mode 100644 index 00000000..70db5cb7 --- /dev/null +++ b/services/httpoverrpc/httpoverrpc.pb.go @@ -0,0 +1,432 @@ +// Copyright (c) 2022 Snowflake Inc. All rights reserved. +// +//Licensed under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//with the License. You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, +//software distributed under the License is distributed on an +//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +//KIND, either express or implied. See the License for the +//specific language governing permissions and limitations +//under the License. + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.31.0 +// protoc v3.21.12 +// source: httpoverrpc.proto + +package httpoverrpc + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +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) +) + +type LocalhostHTTPRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Request *HTTPRequest `protobuf:"bytes,1,opt,name=request,proto3" json:"request,omitempty"` + // The port to use for the request on the local host. + Port int32 `protobuf:"varint,2,opt,name=port,proto3" json:"port,omitempty"` +} + +func (x *LocalhostHTTPRequest) Reset() { + *x = LocalhostHTTPRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_httpoverrpc_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *LocalhostHTTPRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LocalhostHTTPRequest) ProtoMessage() {} + +func (x *LocalhostHTTPRequest) ProtoReflect() protoreflect.Message { + mi := &file_httpoverrpc_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LocalhostHTTPRequest.ProtoReflect.Descriptor instead. +func (*LocalhostHTTPRequest) Descriptor() ([]byte, []int) { + return file_httpoverrpc_proto_rawDescGZIP(), []int{0} +} + +func (x *LocalhostHTTPRequest) GetRequest() *HTTPRequest { + if x != nil { + return x.Request + } + return nil +} + +func (x *LocalhostHTTPRequest) GetPort() int32 { + if x != nil { + return x.Port + } + return 0 +} + +type Header struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Values []string `protobuf:"bytes,2,rep,name=values,proto3" json:"values,omitempty"` +} + +func (x *Header) Reset() { + *x = Header{} + if protoimpl.UnsafeEnabled { + mi := &file_httpoverrpc_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Header) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Header) ProtoMessage() {} + +func (x *Header) ProtoReflect() protoreflect.Message { + mi := &file_httpoverrpc_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Header.ProtoReflect.Descriptor instead. +func (*Header) Descriptor() ([]byte, []int) { + return file_httpoverrpc_proto_rawDescGZIP(), []int{1} +} + +func (x *Header) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *Header) GetValues() []string { + if x != nil { + return x.Values + } + return nil +} + +// HTTPRequest describes the HTTP request +type HTTPRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Method string `protobuf:"bytes,1,opt,name=method,proto3" json:"method,omitempty"` + RequestUri string `protobuf:"bytes,2,opt,name=request_uri,json=requestUri,proto3" json:"request_uri,omitempty"` + Headers []*Header `protobuf:"bytes,3,rep,name=headers,proto3" json:"headers,omitempty"` + Body []byte `protobuf:"bytes,4,opt,name=body,proto3" json:"body,omitempty"` +} + +func (x *HTTPRequest) Reset() { + *x = HTTPRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_httpoverrpc_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HTTPRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HTTPRequest) ProtoMessage() {} + +func (x *HTTPRequest) ProtoReflect() protoreflect.Message { + mi := &file_httpoverrpc_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HTTPRequest.ProtoReflect.Descriptor instead. +func (*HTTPRequest) Descriptor() ([]byte, []int) { + return file_httpoverrpc_proto_rawDescGZIP(), []int{2} +} + +func (x *HTTPRequest) GetMethod() string { + if x != nil { + return x.Method + } + return "" +} + +func (x *HTTPRequest) GetRequestUri() string { + if x != nil { + return x.RequestUri + } + return "" +} + +func (x *HTTPRequest) GetHeaders() []*Header { + if x != nil { + return x.Headers + } + return nil +} + +func (x *HTTPRequest) GetBody() []byte { + if x != nil { + return x.Body + } + return nil +} + +// HTTPReply describes the HTTP reply +type HTTPReply struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + StatusCode int32 `protobuf:"varint,1,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"` + Headers []*Header `protobuf:"bytes,2,rep,name=headers,proto3" json:"headers,omitempty"` + Body []byte `protobuf:"bytes,3,opt,name=body,proto3" json:"body,omitempty"` +} + +func (x *HTTPReply) Reset() { + *x = HTTPReply{} + if protoimpl.UnsafeEnabled { + mi := &file_httpoverrpc_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HTTPReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HTTPReply) ProtoMessage() {} + +func (x *HTTPReply) ProtoReflect() protoreflect.Message { + mi := &file_httpoverrpc_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HTTPReply.ProtoReflect.Descriptor instead. +func (*HTTPReply) Descriptor() ([]byte, []int) { + return file_httpoverrpc_proto_rawDescGZIP(), []int{3} +} + +func (x *HTTPReply) GetStatusCode() int32 { + if x != nil { + return x.StatusCode + } + return 0 +} + +func (x *HTTPReply) GetHeaders() []*Header { + if x != nil { + return x.Headers + } + return nil +} + +func (x *HTTPReply) GetBody() []byte { + if x != nil { + return x.Body + } + return nil +} + +var File_httpoverrpc_proto protoreflect.FileDescriptor + +var file_httpoverrpc_proto_rawDesc = []byte{ + 0x0a, 0x11, 0x68, 0x74, 0x74, 0x70, 0x6f, 0x76, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x12, 0x0b, 0x48, 0x54, 0x54, 0x50, 0x4f, 0x76, 0x65, 0x72, 0x52, 0x50, 0x43, + 0x22, 0x5e, 0x0a, 0x14, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x68, 0x6f, 0x73, 0x74, 0x48, 0x54, 0x54, + 0x50, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x32, 0x0a, 0x07, 0x72, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x48, 0x54, 0x54, 0x50, + 0x4f, 0x76, 0x65, 0x72, 0x52, 0x50, 0x43, 0x2e, 0x48, 0x54, 0x54, 0x50, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x52, 0x07, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, + 0x70, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, + 0x22, 0x32, 0x0a, 0x06, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x16, 0x0a, 0x06, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x73, 0x22, 0x89, 0x01, 0x0a, 0x0b, 0x48, 0x54, 0x54, 0x50, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x1f, 0x0a, 0x0b, + 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x75, 0x72, 0x69, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x55, 0x72, 0x69, 0x12, 0x2d, 0x0a, + 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, + 0x2e, 0x48, 0x54, 0x54, 0x50, 0x4f, 0x76, 0x65, 0x72, 0x52, 0x50, 0x43, 0x2e, 0x48, 0x65, 0x61, + 0x64, 0x65, 0x72, 0x52, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x12, 0x12, 0x0a, 0x04, + 0x62, 0x6f, 0x64, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x62, 0x6f, 0x64, 0x79, + 0x22, 0x6f, 0x0a, 0x09, 0x48, 0x54, 0x54, 0x50, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x1f, 0x0a, + 0x0b, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x0a, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x2d, + 0x0a, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x13, 0x2e, 0x48, 0x54, 0x54, 0x50, 0x4f, 0x76, 0x65, 0x72, 0x52, 0x50, 0x43, 0x2e, 0x48, 0x65, + 0x61, 0x64, 0x65, 0x72, 0x52, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x12, 0x12, 0x0a, + 0x04, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x62, 0x6f, 0x64, + 0x79, 0x32, 0x57, 0x0a, 0x0b, 0x48, 0x54, 0x54, 0x50, 0x4f, 0x76, 0x65, 0x72, 0x52, 0x50, 0x43, + 0x12, 0x48, 0x0a, 0x09, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x68, 0x6f, 0x73, 0x74, 0x12, 0x21, 0x2e, + 0x48, 0x54, 0x54, 0x50, 0x4f, 0x76, 0x65, 0x72, 0x52, 0x50, 0x43, 0x2e, 0x4c, 0x6f, 0x63, 0x61, + 0x6c, 0x68, 0x6f, 0x73, 0x74, 0x48, 0x54, 0x54, 0x50, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x16, 0x2e, 0x48, 0x54, 0x54, 0x50, 0x4f, 0x76, 0x65, 0x72, 0x52, 0x50, 0x43, 0x2e, 0x48, + 0x54, 0x54, 0x50, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x53, 0x6e, 0x6f, 0x77, 0x66, 0x6c, 0x61, + 0x6b, 0x65, 0x2d, 0x4c, 0x61, 0x62, 0x73, 0x2f, 0x73, 0x61, 0x6e, 0x73, 0x73, 0x68, 0x65, 0x6c, + 0x6c, 0x2f, 0x68, 0x74, 0x74, 0x70, 0x6f, 0x76, 0x65, 0x72, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_httpoverrpc_proto_rawDescOnce sync.Once + file_httpoverrpc_proto_rawDescData = file_httpoverrpc_proto_rawDesc +) + +func file_httpoverrpc_proto_rawDescGZIP() []byte { + file_httpoverrpc_proto_rawDescOnce.Do(func() { + file_httpoverrpc_proto_rawDescData = protoimpl.X.CompressGZIP(file_httpoverrpc_proto_rawDescData) + }) + return file_httpoverrpc_proto_rawDescData +} + +var file_httpoverrpc_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_httpoverrpc_proto_goTypes = []interface{}{ + (*LocalhostHTTPRequest)(nil), // 0: HTTPOverRPC.LocalhostHTTPRequest + (*Header)(nil), // 1: HTTPOverRPC.Header + (*HTTPRequest)(nil), // 2: HTTPOverRPC.HTTPRequest + (*HTTPReply)(nil), // 3: HTTPOverRPC.HTTPReply +} +var file_httpoverrpc_proto_depIdxs = []int32{ + 2, // 0: HTTPOverRPC.LocalhostHTTPRequest.request:type_name -> HTTPOverRPC.HTTPRequest + 1, // 1: HTTPOverRPC.HTTPRequest.headers:type_name -> HTTPOverRPC.Header + 1, // 2: HTTPOverRPC.HTTPReply.headers:type_name -> HTTPOverRPC.Header + 0, // 3: HTTPOverRPC.HTTPOverRPC.Localhost:input_type -> HTTPOverRPC.LocalhostHTTPRequest + 3, // 4: HTTPOverRPC.HTTPOverRPC.Localhost:output_type -> HTTPOverRPC.HTTPReply + 4, // [4:5] is the sub-list for method output_type + 3, // [3:4] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_httpoverrpc_proto_init() } +func file_httpoverrpc_proto_init() { + if File_httpoverrpc_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_httpoverrpc_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*LocalhostHTTPRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_httpoverrpc_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Header); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_httpoverrpc_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*HTTPRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_httpoverrpc_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*HTTPReply); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_httpoverrpc_proto_rawDesc, + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_httpoverrpc_proto_goTypes, + DependencyIndexes: file_httpoverrpc_proto_depIdxs, + MessageInfos: file_httpoverrpc_proto_msgTypes, + }.Build() + File_httpoverrpc_proto = out.File + file_httpoverrpc_proto_rawDesc = nil + file_httpoverrpc_proto_goTypes = nil + file_httpoverrpc_proto_depIdxs = nil +} diff --git a/services/httpoverrpc/httpoverrpc.proto b/services/httpoverrpc/httpoverrpc.proto new file mode 100644 index 00000000..9ea91a97 --- /dev/null +++ b/services/httpoverrpc/httpoverrpc.proto @@ -0,0 +1,54 @@ +/* Copyright (c) 2022 Snowflake Inc. All rights reserved. + + Licensed under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + +syntax = "proto3"; + +option go_package = "github.com/Snowflake-Labs/sansshell/httpoverrpc"; + +package HTTPOverRPC; + +// The HTTPOverRPC service definition +service HTTPOverRPC { + // Make an HTTP call to localhost + rpc Localhost(LocalhostHTTPRequest) returns (HTTPReply) {} +} + +message LocalhostHTTPRequest { + HTTPRequest request = 1; + // The port to use for the request on the local host. + int32 port = 2; +} + +message Header { + string key = 1; + repeated string values = 2; +} + +// HTTPRequest describes the HTTP request +message HTTPRequest { + string method = 1; + string request_uri = 2; + repeated Header headers = 3; + bytes body = 4; +} + + +// HTTPReply describes the HTTP reply +message HTTPReply { + int32 status_code = 1; + repeated Header headers = 2; + bytes body = 3; +} diff --git a/services/httpoverrpc/httpoverrpc_grpc.pb.go b/services/httpoverrpc/httpoverrpc_grpc.pb.go new file mode 100644 index 00000000..17e6ce0e --- /dev/null +++ b/services/httpoverrpc/httpoverrpc_grpc.pb.go @@ -0,0 +1,124 @@ +// Copyright (c) 2022 Snowflake Inc. All rights reserved. +// +//Licensed under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//with the License. You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, +//software distributed under the License is distributed on an +//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +//KIND, either express or implied. See the License for the +//specific language governing permissions and limitations +//under the License. + +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.3.0 +// - protoc v3.21.12 +// source: httpoverrpc.proto + +package httpoverrpc + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +const ( + HTTPOverRPC_Localhost_FullMethodName = "/HTTPOverRPC.HTTPOverRPC/Localhost" +) + +// HTTPOverRPCClient is the client API for HTTPOverRPC service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type HTTPOverRPCClient interface { + // Make an HTTP call to localhost + Localhost(ctx context.Context, in *LocalhostHTTPRequest, opts ...grpc.CallOption) (*HTTPReply, error) +} + +type hTTPOverRPCClient struct { + cc grpc.ClientConnInterface +} + +func NewHTTPOverRPCClient(cc grpc.ClientConnInterface) HTTPOverRPCClient { + return &hTTPOverRPCClient{cc} +} + +func (c *hTTPOverRPCClient) Localhost(ctx context.Context, in *LocalhostHTTPRequest, opts ...grpc.CallOption) (*HTTPReply, error) { + out := new(HTTPReply) + err := c.cc.Invoke(ctx, HTTPOverRPC_Localhost_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// HTTPOverRPCServer is the server API for HTTPOverRPC service. +// All implementations should embed UnimplementedHTTPOverRPCServer +// for forward compatibility +type HTTPOverRPCServer interface { + // Make an HTTP call to localhost + Localhost(context.Context, *LocalhostHTTPRequest) (*HTTPReply, error) +} + +// UnimplementedHTTPOverRPCServer should be embedded to have forward compatible implementations. +type UnimplementedHTTPOverRPCServer struct { +} + +func (UnimplementedHTTPOverRPCServer) Localhost(context.Context, *LocalhostHTTPRequest) (*HTTPReply, error) { + return nil, status.Errorf(codes.Unimplemented, "method Localhost not implemented") +} + +// UnsafeHTTPOverRPCServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to HTTPOverRPCServer will +// result in compilation errors. +type UnsafeHTTPOverRPCServer interface { + mustEmbedUnimplementedHTTPOverRPCServer() +} + +func RegisterHTTPOverRPCServer(s grpc.ServiceRegistrar, srv HTTPOverRPCServer) { + s.RegisterService(&HTTPOverRPC_ServiceDesc, srv) +} + +func _HTTPOverRPC_Localhost_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(LocalhostHTTPRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(HTTPOverRPCServer).Localhost(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: HTTPOverRPC_Localhost_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(HTTPOverRPCServer).Localhost(ctx, req.(*LocalhostHTTPRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// HTTPOverRPC_ServiceDesc is the grpc.ServiceDesc for HTTPOverRPC service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var HTTPOverRPC_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "HTTPOverRPC.HTTPOverRPC", + HandlerType: (*HTTPOverRPCServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Localhost", + Handler: _HTTPOverRPC_Localhost_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "httpoverrpc.proto", +} diff --git a/services/httpoverrpc/httpoverrpc_grpcproxy.pb.go b/services/httpoverrpc/httpoverrpc_grpcproxy.pb.go new file mode 100644 index 00000000..44987dd1 --- /dev/null +++ b/services/httpoverrpc/httpoverrpc_grpcproxy.pb.go @@ -0,0 +1,100 @@ +// Auto generated code by protoc-gen-go-grpcproxy +// DO NOT EDIT + +// Adds OneMany versions of RPC methods for use by proxy clients + +package httpoverrpc + +import ( + context "context" + proxy "github.com/Snowflake-Labs/sansshell/proxy/proxy" + grpc "google.golang.org/grpc" +) + +import ( + "fmt" +) + +// HTTPOverRPCClientProxy is the superset of HTTPOverRPCClient which additionally includes the OneMany proxy methods +type HTTPOverRPCClientProxy interface { + HTTPOverRPCClient + LocalhostOneMany(ctx context.Context, in *LocalhostHTTPRequest, opts ...grpc.CallOption) (<-chan *LocalhostManyResponse, error) +} + +// Embed the original client inside of this so we get the other generated methods automatically. +type hTTPOverRPCClientProxy struct { + *hTTPOverRPCClient +} + +// NewHTTPOverRPCClientProxy creates a HTTPOverRPCClientProxy for use in proxied connections. +// NOTE: This takes a proxy.Conn instead of a generic ClientConnInterface as the methods here are only valid in proxy.Conn contexts. +func NewHTTPOverRPCClientProxy(cc *proxy.Conn) HTTPOverRPCClientProxy { + return &hTTPOverRPCClientProxy{NewHTTPOverRPCClient(cc).(*hTTPOverRPCClient)} +} + +// LocalhostManyResponse encapsulates a proxy data packet. +// It includes the target, index, response and possible error returned. +type LocalhostManyResponse struct { + Target string + // As targets can be duplicated this is the index into the slice passed to proxy.Conn. + Index int + Resp *HTTPReply + Error error +} + +// LocalhostOneMany provides the same API as Localhost but sends the same request to N destinations at once. +// N can be a single destination. +// +// NOTE: The returned channel must be read until it closes in order to avoid leaking goroutines. +func (c *hTTPOverRPCClientProxy) LocalhostOneMany(ctx context.Context, in *LocalhostHTTPRequest, opts ...grpc.CallOption) (<-chan *LocalhostManyResponse, error) { + conn := c.cc.(*proxy.Conn) + ret := make(chan *LocalhostManyResponse) + // If this is a single case we can just use Invoke and marshal it onto the channel once and be done. + if len(conn.Targets) == 1 { + go func() { + out := &LocalhostManyResponse{ + Target: conn.Targets[0], + Index: 0, + Resp: &HTTPReply{}, + } + err := conn.Invoke(ctx, "/HTTPOverRPC.HTTPOverRPC/Localhost", in, out.Resp, opts...) + if err != nil { + out.Error = err + } + // Send and close. + ret <- out + close(ret) + }() + return ret, nil + } + manyRet, err := conn.InvokeOneMany(ctx, "/HTTPOverRPC.HTTPOverRPC/Localhost", in, opts...) + if err != nil { + return nil, err + } + // A goroutine to retrive untyped responses and convert them to typed ones. + go func() { + for { + typedResp := &LocalhostManyResponse{ + Resp: &HTTPReply{}, + } + + resp, ok := <-manyRet + if !ok { + // All done so we can shut down. + close(ret) + return + } + typedResp.Target = resp.Target + typedResp.Index = resp.Index + typedResp.Error = resp.Error + if resp.Error == nil { + if err := resp.Resp.UnmarshalTo(typedResp.Resp); err != nil { + typedResp.Error = fmt.Errorf("can't decode any response - %v. Original Error - %v", err, resp.Error) + } + } + ret <- typedResp + } + }() + + return ret, nil +} diff --git a/services/httpoverrpc/server/server.go b/services/httpoverrpc/server/server.go new file mode 100644 index 00000000..fb5995f5 --- /dev/null +++ b/services/httpoverrpc/server/server.go @@ -0,0 +1,87 @@ +/* Copyright (c) 2023 Snowflake Inc. All rights reserved. + + Licensed under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + +// Package server implements the sansshell 'httpoverrpc' service. +package server + +import ( + "bytes" + "context" + "fmt" + "io" + "net/http" + + "github.com/Snowflake-Labs/sansshell/services" + pb "github.com/Snowflake-Labs/sansshell/services/httpoverrpc" + sansshellserver "github.com/Snowflake-Labs/sansshell/services/sansshell/server" + "github.com/Snowflake-Labs/sansshell/telemetry/metrics" + "google.golang.org/grpc" +) + +// Metrics +var ( + localhostFailureCounter = metrics.MetricDefinition{Name: "actions_httpoverrpc_localhost_failure", + Description: "number of failures when performing HTTPOverRPC/Localhost"} +) + +// Server is used to implement the gRPC Server +type server struct{} + +func (s *server) Localhost(ctx context.Context, req *pb.LocalhostHTTPRequest) (*pb.HTTPReply, error) { + recorder := metrics.RecorderFromContextOrNoop(ctx) + + url := fmt.Sprintf("http://localhost:%v%v", req.Port, req.Request.RequestUri) + httpReq, err := http.NewRequestWithContext(ctx, req.Request.Method, url, bytes.NewReader(req.Request.Body)) + if err != nil { + recorder.CounterOrLog(ctx, localhostFailureCounter, 1) + return nil, err + } + // Set a default user agent that can be overridden in the request. + httpReq.Header["User-Agent"] = []string{"sansshell/" + sansshellserver.Version} + for _, header := range req.Request.Headers { + httpReq.Header[header.Key] = header.Values + } + client := &http.Client{ + CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }, + } + httpResp, err := client.Do(httpReq) + if err != nil { + return nil, err + } + defer httpResp.Body.Close() + body, err := io.ReadAll(httpResp.Body) + if err != nil { + return nil, err + } + var respHeaders []*pb.Header + for k, v := range httpResp.Header { + respHeaders = append(respHeaders, &pb.Header{Key: k, Values: v}) + } + return &pb.HTTPReply{ + StatusCode: int32(httpResp.StatusCode), + Headers: respHeaders, + Body: body, + }, nil +} + +// Register is called to expose this handler to the gRPC server +func (s *server) Register(gs *grpc.Server) { + pb.RegisterHTTPOverRPCServer(gs, s) +} + +func init() { + services.RegisterSansShellService(&server{}) +} diff --git a/services/httpoverrpc/server/server_test.go b/services/httpoverrpc/server/server_test.go new file mode 100644 index 00000000..cd2b85a5 --- /dev/null +++ b/services/httpoverrpc/server/server_test.go @@ -0,0 +1,108 @@ +package server + +import ( + "context" + "log" + "net" + "net/http" + "os" + "sort" + "strconv" + "testing" + + "github.com/Snowflake-Labs/sansshell/services/httpoverrpc" + "github.com/Snowflake-Labs/sansshell/testing/testutil" + "github.com/google/go-cmp/cmp" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/test/bufconn" + "google.golang.org/protobuf/testing/protocmp" +) + +var ( + bufSize = 1024 * 1024 + lis *bufconn.Listener +) + +func bufDialer(context.Context, string) (net.Conn, error) { + return lis.Dial() +} + +func TestMain(m *testing.M) { + lis = bufconn.Listen(bufSize) + s := grpc.NewServer() + lfs := &server{} + lfs.Register(s) + go func() { + if err := s.Serve(lis); err != nil { + log.Fatalf("Server exited with error: %v", err) + } + }() + defer s.GracefulStop() + + os.Exit(m.Run()) +} + +func TestServer(t *testing.T) { + var err error + ctx := context.Background() + conn, err := grpc.DialContext(ctx, "bufnet", grpc.WithContextDialer(bufDialer), grpc.WithTransportCredentials(insecure.NewCredentials())) + testutil.FatalOnErr("Failed to dial bufnet", err, t) + t.Cleanup(func() { conn.Close() }) + + client := httpoverrpc.NewHTTPOverRPCClient(conn) + + // Set up web server + m := http.NewServeMux() + m.HandleFunc("/", func(httpResp http.ResponseWriter, httpReq *http.Request) { + _, _ = httpResp.Write([]byte("hello world")) + }) + l, err := net.Listen("tcp4", "localhost:0") + if err != nil { + t.Fatal(err) + } + go func() { _ = http.Serve(l, m) }() + + _, p, err := net.SplitHostPort(l.Addr().String()) + if err != nil { + t.Fatal(err) + } + httpPort, err := strconv.Atoi(p) + if err != nil { + t.Fatal(err) + } + + got, err := client.Localhost(ctx, &httpoverrpc.LocalhostHTTPRequest{ + Request: &httpoverrpc.HTTPRequest{ + Method: "GET", + RequestUri: "/", + }, + Port: int32(httpPort), + }) + if err != nil { + t.Fatal(err) + } + + sort.Slice(got.Headers, func(i, j int) bool { + return got.Headers[i].Key < got.Headers[j].Key + }) + for _, h := range got.Headers { + if h.Key == "Date" { + // Clear out the date header because it varies based on time. + h.Values = nil + } + } + + want := &httpoverrpc.HTTPReply{ + StatusCode: 200, + Headers: []*httpoverrpc.Header{ + {Key: "Content-Length", Values: []string{"11"}}, + {Key: "Content-Type", Values: []string{"text/plain; charset=utf-8"}}, + {Key: "Date"}, + }, + Body: []byte("hello world"), + } + if !cmp.Equal(got, want, protocmp.Transform()) { + t.Fatalf("want %v, got %v", want, got) + } +}