Having a proxy server between the Eth2 API client and the grpc-gateway server allows to translate incoming and outgoing data between them. This document presents a working example of how this can be achieved.
The main idea here is that we make a request to the proxy server with data formatted in accordance with the Eth2 APIs spec and massage this data so that it fits into grpc-gateway.
One example is a bytes
proto definition. grpc-gateway expects a base64 string, but the Eth2 API spec defines hex values for such endpoints. The proxy server can be leveraged to transform the data back and forth between hex and base64.
To see the example in action, run the below code and issue an HTTP POST request to localhost:8080/test
posting a JSON of the form {"h": "[hex with '0x' prefix]", "inner": {"h": "[hex with '0x' prefix]"}}
. As you will see, even though the posted values are not base64-encoded, the response contains the original strings. This is because the proxy server base-64 encoded and decoded them for you.
Please see code comments for an in-depth explanation. The code assumes you have matching proto definitions in a package called v1
.
type HexMessageJson struct {
// Tag 'bytes' is responsible for translation between base64 and hex.
H string `json:"h" bytes:"true"`
Inner *InnerJson `json:"inner"`
}
type InnerJson struct {
H string `json:"h" bytes:"true"`
}
func run() error {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
s := Server{}
// Register the gRPC server on port 8082.
go func() {
lis, _ := net.Listen("tcp", fmt.Sprintf("localhost:8082"))
grpcServer := grpc.NewServer()
v1.RegisterPocServiceServer(grpcServer, s)
grpcServer.Serve(lis)
}()
// Register the grcp-gateway server on port 8081.
// Its endpoint is '/test'.
go func() {
m := runtime.NewServeMux()
opts := []grpc.DialOption{grpc.WithInsecure()}
v1.RegisterPocServiceHandlerFromEndpoint(ctx, m, "localhost:8082", opts)
http.ListenAndServe(":8081", m)
}()
// Create the proxy server. Its endpoint is '/test'.
r := mux.NewRouter()
registerEndpoint(r, "/test")
// Register the proxy server on port 8080.
return http.ListenAndServe("localhost:8080", r)
}
func registerEndpoint(r *mux.Router, endpoint string) {
r.HandleFunc(endpoint, func(writer http.ResponseWriter, request *http.Request) {
// Structs for body deserialization.
e := ErrorJson{}
m := MessageJson{}
// Deserialize the body into the 'm' struct, and post it to grpc-gateway.
json.NewDecoder(request.Body).Decode(&m)
// Encode all fields tagged 'bytes' into a base64 string.
processBytes(&m, func(v reflect.Value) {
v.SetString(base64.StdEncoding.EncodeToString([]byte(v.String())))
})
// Serialize the struct, which now includes a base64-encoded value, into JSON.
j, _ := json.Marshal(m)
// Post the JSON to grpc-gateway's server and grab the response.
request.Body = ioutil.NopCloser(bytes.NewReader(j))
request.URL.Scheme = "http"
request.URL.Host = "127.0.0.1:8081"
request.URL.Path = endpoint
request.RequestURI = ""
request.Header.Set("Content-Length", strconv.Itoa(len(j)))
request.ContentLength = int64(len(j))
grpcResp, _ := http.DefaultClient.Do(request)
if grpcResp == nil {
panic("nil response from grpc-gateway")
}
// Deserialize the output of grpc-gateway's server into the 'e' struct.
body, _ := ioutil.ReadAll(grpcResp.Body)
json.Unmarshal(body, &e)
// The output might have contained a 'Not Found' error,
// in which case the 'Message' field will be populated.
if e.Message != "" {
// Serialize the error message into JSON.
j, _ = json.Marshal(e)
} else {
// Deserialize the output of grpc-gateway's server into the 'm' struct.
json.Unmarshal(body, &m)
// Decode all fields tagged 'bytes' from a base64 string.
processBytes(&m, func(v reflect.Value) {
b, _ := base64.StdEncoding.DecodeString(v.Interface().(string))
v.SetString(string(b))
})
// Serialize the return value into JSON.
j, _ = json.Marshal(m)
}
// Write the response and PROFIT!
writer.WriteHeader(grpcResp.StatusCode)
for h, vs := range grpcResp.Header {
for _, v := range vs {
writer.Header().Add(h, v)
}
}
io.Copy(writer, ioutil.NopCloser(bytes.NewReader(j)))
grpcResp.Body.Close()
})
}
// processBytes calls 'processor' on any field that has the 'bytes' tag set.
// It is a recursive function.
func processBytes(s interface{}, processor func(value reflect.Value)) {
t := reflect.TypeOf(s).Elem()
v := reflect.ValueOf(s).Elem()
for i := 0; i < t.NumField(); i++ {
switch v.Field(i).Kind() {
case reflect.Ptr: // struct pointer
processBytes(v.Field(i).Interface(), processor)
default:
f := t.Field(i)
_, isBytes := f.Tag.Lookup("bytes")
if isBytes {
processor(v.Field(i))
}
}
}
}
// gRPC service implementation.
type PocServer struct {
}
// gRPC endpoint implementation.
func (PocServer) Echo(_ context.Context, m *v1.HexMessage) (*v1.HexMessage, error) {
return &v1.HexMessage{
H: m.H,
Inner: m.Inner,
}, nil
}
registerEndpoint
, so that MessageJson
is not hard-coded (note: simply passing interface{}
results in reflection erros)