Skip to content

Commit

Permalink
[FAB-4107] Expose proto translator via REST
Browse files Browse the repository at this point in the history
As part of FAB-4100, there is a requirement to expose configuration
utilities through a REST interface.

This CR adds a proto-translator REST component.  Further documentation
is pending, but the high level usage is:

POST binary-proto to /protolator/decode/fq.MessageType replies with a
deeply marshaled JSON version of the proto.

POST json-doc to /protolator/encode/fq.MessageType replies with the
proto version of the deeply marshaled JSON doc.

Change-Id: I595b92b1c292d8d4d360b0bd4223b138615143ac
Signed-off-by: Jason Yellick <jyellick@us.ibm.com>
  • Loading branch information
Jason Yellick committed Jun 2, 2017
1 parent a5d6216 commit 5fb91b5
Show file tree
Hide file tree
Showing 5 changed files with 325 additions and 2 deletions.
12 changes: 10 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,11 @@ PROTOS = $(shell git ls-files *.proto | grep -v vendor)
PROJECT_FILES = $(shell git ls-files)
IMAGES = peer orderer ccenv javaenv buildenv testenv zookeeper kafka couchdb tools
RELEASE_PLATFORMS = windows-amd64 darwin-amd64 linux-amd64 linux-ppc64le linux-s390x
RELEASE_PKGS = configtxgen cryptogen
RELEASE_PKGS = configtxgen cryptogen configtxlator

pkgmap.cryptogen := $(PKGNAME)/common/tools/cryptogen
pkgmap.configtxgen := $(PKGNAME)/common/configtx/tool/configtxgen
pkgmap.configtxlator := $(PKGNAME)/common/tools/configtxlator
pkgmap.peer := $(PKGNAME)/peer
pkgmap.orderer := $(PKGNAME)/orderer
pkgmap.block-listener := $(PKGNAME)/examples/events/block-listener
Expand Down Expand Up @@ -129,6 +130,8 @@ orderer-docker: build/image/orderer/$(DUMMY)
configtxgen: GO_TAGS+= nopkcs11
configtxgen: build/bin/configtxgen

configtxlator: build/bin/configtxlator

cryptogen: build/bin/cryptogen

tools-docker: build/image/tools/$(DUMMY)
Expand All @@ -152,7 +155,7 @@ test-cmd:
@echo "go test -ldflags \"$(GO_LDFLAGS)\""

docker: $(patsubst %,build/image/%/$(DUMMY), $(IMAGES))
native: peer orderer configtxgen cryptogen
native: peer orderer configtxgen cryptogen configtxlator

behave-deps: docker peer build/bin/block-listener configtxgen cryptogen
behave: behave-deps
Expand Down Expand Up @@ -312,6 +315,11 @@ release/linux-s390x: DOCKER_ARCH=s390x
release/linux-s390x: GO_TAGS+= nopkcs11
release/linux-s390x: $(patsubst %,release/linux-s390x/bin/%, $(RELEASE_PKGS)) release/linux-s390x/install

release/%/bin/configtxlator: $(PROJECT_FILES)
@echo "Building $@ for $(GOOS)-$(GOARCH)"
mkdir -p $(@D)
$(CGO_FLAGS) GOOS=$(GOOS) GOARCH=$(GOARCH) go build -o $(abspath $@) -tags "$(GO_TAGS)" -ldflags "$(GO_LDFLAGS)" $(pkgmap.$(@F))

release/%/bin/configtxgen: $(PROJECT_FILES)
@echo "Building $@ for $(GOOS)-$(GOARCH)"
mkdir -p $(@D)
Expand Down
41 changes: 41 additions & 0 deletions common/tools/configtxlator/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
Copyright IBM Corp. 2017 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 main

import (
"flag"
"fmt"
"net/http"

"github.com/hyperledger/fabric/common/tools/configtxlator/rest"

"github.com/op/go-logging"
)

var logger = logging.MustGetLogger("configtxlator")

func main() {
var serverPort int

flag.IntVar(&serverPort, "serverPort", 7059, "Specify the port for the REST server to listen on.")
flag.Parse()

logger.Infof("Serving HTTP requests on port: %d", serverPort)
err := http.ListenAndServe(fmt.Sprintf(":%d", serverPort), rest.NewRouter())

logger.Fatal("Error runing http server:", err)
}
109 changes: 109 additions & 0 deletions common/tools/configtxlator/rest/protolator_handlers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
Copyright IBM Corp. 2017 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 rest

import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"reflect"

"github.com/hyperledger/fabric/common/tools/protolator"

// Import these to register the proto types
_ "github.com/hyperledger/fabric/protos/common"
_ "github.com/hyperledger/fabric/protos/msp"
_ "github.com/hyperledger/fabric/protos/orderer"
_ "github.com/hyperledger/fabric/protos/peer"

"github.com/golang/protobuf/proto"
"github.com/gorilla/mux"
)

func getMsgType(r *http.Request) (proto.Message, error) {
vars := mux.Vars(r)
msgName := vars["msgName"] // Will not arrive is unset

msgType := proto.MessageType(msgName)
if msgType == nil {
return nil, fmt.Errorf("message name not found")
}
return reflect.New(msgType.Elem()).Interface().(proto.Message), nil
}

func Decode(w http.ResponseWriter, r *http.Request) {
msg, err := getMsgType(r)
if err != nil {
w.WriteHeader(http.StatusNotFound)
fmt.Fprintln(w, err)
return
}

buf, err := ioutil.ReadAll(r.Body)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintln(w, err)
return
}

err = proto.Unmarshal(buf, msg)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintln(w, err)
return
}

var buffer bytes.Buffer
err = protolator.DeepMarshalJSON(&buffer, msg)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintln(w, err)
return
}

w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
buffer.WriteTo(w)
}

func Encode(w http.ResponseWriter, r *http.Request) {
msg, err := getMsgType(r)
if err != nil {
w.WriteHeader(http.StatusNotFound)
fmt.Fprintln(w, err)
return
}

err = protolator.DeepUnmarshalJSON(r.Body, msg)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintln(w, err)
return
}

data, err := proto.Marshal(msg)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintln(w, err)
return
}

w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/octet-stream")
w.Write(data)
}
128 changes: 128 additions & 0 deletions common/tools/configtxlator/rest/protolator_handlers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
Copyright IBM Corp. 2017 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 rest

import (
"bytes"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"

cb "github.com/hyperledger/fabric/protos/common"
"github.com/hyperledger/fabric/protos/utils"

"github.com/golang/protobuf/proto"
"github.com/stretchr/testify/assert"
)

var (
testProto = &cb.Block{
Header: &cb.BlockHeader{
PreviousHash: []byte("foo"),
},
Data: &cb.BlockData{
Data: [][]byte{
utils.MarshalOrPanic(&cb.Envelope{
Signature: []byte("bar"),
}),
},
},
}

testOutput = `{"data":{"data":[{"signature":"YmFy"}]},"header":{"previous_hash":"Zm9v"}}`
)

func TestProtolatorDecode(t *testing.T) {
data, err := proto.Marshal(testProto)
assert.NoError(t, err)

url := fmt.Sprintf("/protolator/decode/%s", proto.MessageName(testProto))

req, _ := http.NewRequest("POST", url, bytes.NewReader(data))
rec := httptest.NewRecorder()
r := NewRouter()
r.ServeHTTP(rec, req)

assert.Equal(t, http.StatusOK, rec.Code)

// Remove all the whitespace
compactJSON := strings.Replace(strings.Replace(strings.Replace(rec.Body.String(), "\n", "", -1), "\t", "", -1), " ", "", -1)

assert.Equal(t, testOutput, compactJSON)
}

func TestProtolatorEncode(t *testing.T) {

url := fmt.Sprintf("/protolator/encode/%s", proto.MessageName(testProto))

req, _ := http.NewRequest("POST", url, bytes.NewReader([]byte(testOutput)))
rec := httptest.NewRecorder()
r := NewRouter()
r.ServeHTTP(rec, req)

assert.Equal(t, http.StatusOK, rec.Code)

outputMsg := &cb.Block{}

err := proto.Unmarshal(rec.Body.Bytes(), outputMsg)
assert.NoError(t, err)
assert.Equal(t, testProto, outputMsg)
}

func TestProtolatorDecodeNonExistantProto(t *testing.T) {
req, _ := http.NewRequest("POST", "/protolator/decode/NonExistantMsg", bytes.NewReader([]byte{}))
rec := httptest.NewRecorder()
r := NewRouter()
r.ServeHTTP(rec, req)

assert.Equal(t, http.StatusNotFound, rec.Code)
}

func TestProtolatorEncodeNonExistantProto(t *testing.T) {
req, _ := http.NewRequest("POST", "/protolator/encode/NonExistantMsg", bytes.NewReader([]byte{}))
rec := httptest.NewRecorder()
r := NewRouter()
r.ServeHTTP(rec, req)

assert.Equal(t, http.StatusNotFound, rec.Code)
}

func TestProtolatorDecodeBadData(t *testing.T) {
url := fmt.Sprintf("/protolator/decode/%s", proto.MessageName(testProto))

req, _ := http.NewRequest("POST", url, bytes.NewReader([]byte("Garbage")))

rec := httptest.NewRecorder()
r := NewRouter()
r.ServeHTTP(rec, req)

assert.Equal(t, http.StatusBadRequest, rec.Code)
}

func TestProtolatorEncodeBadData(t *testing.T) {
url := fmt.Sprintf("/protolator/encode/%s", proto.MessageName(testProto))

req, _ := http.NewRequest("POST", url, bytes.NewReader([]byte("Garbage")))

rec := httptest.NewRecorder()
r := NewRouter()
r.ServeHTTP(rec, req)

assert.Equal(t, http.StatusBadRequest, rec.Code)
}
37 changes: 37 additions & 0 deletions common/tools/configtxlator/rest/router.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
Copyright IBM Corp. 2017 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 rest

import (
"github.com/gorilla/mux"
"github.com/op/go-logging"
)

var logger = logging.MustGetLogger("configtxlator/rest")

func NewRouter() *mux.Router {
router := mux.NewRouter().StrictSlash(true)
router.
HandleFunc("/protolator/encode/{msgName}", Encode).
Methods("POST")

router.
HandleFunc("/protolator/decode/{msgName}", Decode).
Methods("POST")

return router
}

0 comments on commit 5fb91b5

Please sign in to comment.