From c3c429494f0e8e18d5217d8ee590cd32fad3f84d Mon Sep 17 00:00:00 2001 From: stasatdaglabs <39559713+stasatdaglabs@users.noreply.github.com> Date: Thu, 4 Jul 2019 11:16:05 +0300 Subject: [PATCH] [NOD-228] Added JSONRPCifyer to the project and created a Dockerfile for it. (#337) --- cmd/jsonrpcifyer/config.go | 32 +++++++ cmd/jsonrpcifyer/docker/Dockerfile | 33 ++++++++ cmd/jsonrpcifyer/docker/README | 5 ++ cmd/jsonrpcifyer/jsonrpcifyer.go | 48 +++++++++++ cmd/jsonrpcifyer/server.go | 129 +++++++++++++++++++++++++++++ 5 files changed, 247 insertions(+) create mode 100644 cmd/jsonrpcifyer/config.go create mode 100644 cmd/jsonrpcifyer/docker/Dockerfile create mode 100644 cmd/jsonrpcifyer/docker/README create mode 100644 cmd/jsonrpcifyer/jsonrpcifyer.go create mode 100644 cmd/jsonrpcifyer/server.go diff --git a/cmd/jsonrpcifyer/config.go b/cmd/jsonrpcifyer/config.go new file mode 100644 index 000000000..ffdd3da61 --- /dev/null +++ b/cmd/jsonrpcifyer/config.go @@ -0,0 +1,32 @@ +package main + +import ( + "errors" + + "github.com/jessevdk/go-flags" +) + +type config struct { + Host string `long:"host" default:"localhost:18334" description:"IP:Port of the JSON-RPC endpoint"` + ListenPort int `long:"port" default:"8080" description:"Port to listen on"` + RPCCert string `long:"rpccert" description:"Path to certificate accepted by JSON-RPC endpoint"` + RPCUser string `long:"rpcuser" required:"true" description:"Username to connect to JSON-RPC endpoint"` + RPCPass string `long:"rpcpass" required:"true" description:"Password to connect to JSON-RPC endpoint"` + DisableTLS bool `long:"notls" description:"Disable TLS"` +} + +func parseConfig() (*config, error) { + cfg := &config{} + parser := flags.NewParser(cfg, flags.PrintErrors|flags.HelpFlag) + _, err := parser.Parse() + + if err != nil { + return nil, err + } + + if cfg.RPCCert == "" && !cfg.DisableTLS { + return nil, errors.New("either --notls or --rpccert must be set") + } + + return cfg, nil +} diff --git a/cmd/jsonrpcifyer/docker/Dockerfile b/cmd/jsonrpcifyer/docker/Dockerfile new file mode 100644 index 000000000..938727aff --- /dev/null +++ b/cmd/jsonrpcifyer/docker/Dockerfile @@ -0,0 +1,33 @@ +# -- multistage docker build: stage #1: build stage +FROM golang:1.12-alpine AS build + +RUN mkdir -p /go/src/github.com/daglabs/btcd + +WORKDIR /go/src/github.com/daglabs/btcd + +RUN apk add --no-cache curl git + +# GO111MODULE=on forces Go to use the go-module system +# TODO: remove this once Go 1.13 is released +ENV GO111MODULE=on + +COPY go.mod . +COPY go.sum . + +RUN go mod download + +COPY . . + +RUN cd cmd/jsonrpcifyer && CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o jsonrpcifyer . + +# --- multistage docker build: stage #2: runtime image +FROM alpine +WORKDIR /app + +RUN apk add --no-cache tini + +COPY --from=build /go/src/github.com/daglabs/btcd/cmd/jsonrpcifyer/jsonrpcifyer /app/ + +ENTRYPOINT ["/sbin/tini", "--"] + +CMD ["/app/jsonrpcifyer"] diff --git a/cmd/jsonrpcifyer/docker/README b/cmd/jsonrpcifyer/docker/README new file mode 100644 index 000000000..0a21ee23e --- /dev/null +++ b/cmd/jsonrpcifyer/docker/README @@ -0,0 +1,5 @@ +1. To build docker image invoke following command from btcd root directory: + docker build -t jsonrpcifyer -f ./cmd/jsonrpcifyer/docker/Dockerfile . + +2. To run: + docker run -v ~/.btcd:/root/.btcd -t jsonrpcifyer diff --git a/cmd/jsonrpcifyer/jsonrpcifyer.go b/cmd/jsonrpcifyer/jsonrpcifyer.go new file mode 100644 index 000000000..475301412 --- /dev/null +++ b/cmd/jsonrpcifyer/jsonrpcifyer.go @@ -0,0 +1,48 @@ +package main + +import ( + "github.com/daglabs/btcd/signal" + "log" + "os" + "runtime/debug" +) + +func main() { + defer handlePanic() + + cfg, err := parseConfig() + if err != nil { + log.Printf("error parsing command-line arguments: %s", err) + os.Exit(1) + } + + server, err := newServer(cfg) + if err != nil { + log.Panicf("couldn't create server: %s", err) + } + + defer func() { + err := server.stop() + if err != nil { + log.Panicf("couldn't stop server: %s", err) + } + }() + + go func() { + err = server.start() + if err != nil { + log.Panicf("server error: %s", err) + } + }() + + interrupt := signal.InterruptListener() + <-interrupt +} + +func handlePanic() { + err := recover() + if err != nil { + log.Printf("Fatal error: %s", err) + log.Printf("Stack trace: %s", debug.Stack()) + } +} diff --git a/cmd/jsonrpcifyer/server.go b/cmd/jsonrpcifyer/server.go new file mode 100644 index 000000000..4a5af8841 --- /dev/null +++ b/cmd/jsonrpcifyer/server.go @@ -0,0 +1,129 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "log" + "net/http" + "strings" + + "github.com/daglabs/btcd/rpcclient" +) + +type server struct { + cfg *config + rpcConnConfig *rpcclient.ConnConfig + + httpServer *http.Server + rpcClient *rpcclient.Client +} + +func newServer(cfg *config) (*server, error) { + server := server{ + cfg: cfg, + } + + server.rpcConnConfig = &rpcclient.ConnConfig{ + Host: cfg.Host, + Endpoint: "ws", + User: cfg.RPCUser, + Pass: cfg.RPCPass, + DisableTLS: cfg.DisableTLS, + } + if !cfg.DisableTLS { + certificate, err := ioutil.ReadFile(cfg.RPCCert) + if err != nil { + return nil, err + } + server.rpcConnConfig.Certificates = certificate + } + + return &server, nil +} + +func (s *server) start() error { + log.Printf("Connecting RPC client to %s", s.cfg.Host) + + rpcClient, err := rpcclient.New(s.rpcConnConfig, nil) + if err != nil { + return errors.New(fmt.Sprintf("failed to create RPC client: %s", err)) + } + s.rpcClient = rpcClient + + log.Printf("Starting server on port %d", s.cfg.ListenPort) + + handler := http.NewServeMux() + handler.HandleFunc("/", s.handleRequest) + + s.httpServer = &http.Server{ + Addr: fmt.Sprintf(":%d", s.cfg.ListenPort), + Handler: handler, + } + + return s.httpServer.ListenAndServe() +} + +func (s *server) handleRequest(responseWriter http.ResponseWriter, request *http.Request) { + s.allowCrossOrigin(responseWriter) + if request.Method == "OPTIONS" { + // OPTIONS must stop here or else CORS protection will throw a tantrum. + return + } + + if request.Method != "POST" { + responseWriter.WriteHeader(404) + return + } + + forwardedResponse, err := s.forwardRequest(request) + if err != nil { + responseWriter.WriteHeader(500) + log.Printf("failed to forward request: %s", err) + return + } + + _, err = responseWriter.Write([]byte(forwardedResponse)) + if err != nil { + responseWriter.WriteHeader(500) + log.Printf("failed to write response: %s", err) + return + } +} + +func (s *server) allowCrossOrigin(responseWriter http.ResponseWriter) { + responseWriter.Header().Set("Access-Control-Allow-Origin", "*") + responseWriter.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization") + responseWriter.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") +} + +func (s *server) forwardRequest(request *http.Request) ([]byte, error) { + jsonRPCMethod := strings.TrimPrefix(request.URL.Path, "/") + + requestBody, err := ioutil.ReadAll(request.Body) + if err != nil { + return nil, errors.New(fmt.Sprintf("failed to read request body: %s", err)) + } + + var jsonRPCParams []json.RawMessage + err = json.Unmarshal(requestBody, &jsonRPCParams) + if err != nil { + return nil, errors.New(fmt.Sprintf("failed to parse params: %s", err)) + } + + response, err := s.rpcClient.RawRequest(jsonRPCMethod, jsonRPCParams) + if err != nil { + return nil, errors.New(fmt.Sprintf("request to rpc server failed: %s", err)) + } + + return response, nil +} + +func (s *server) stop() error { + log.Printf("Disconnecting RPC client") + s.rpcClient.Disconnect() + + log.Printf("Stopping server") + return s.httpServer.Close() +}