├── .gitignore ├── Dockerfile ├── .dockerignore ├── demo ├── node_client.js ├── helloworld.proto └── helloserver.js ├── kubernetes-manifest.yaml ├── package.json ├── README.markdown ├── grpc-bus.proto └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8-onbuild 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .git 3 | .gitignore 4 | -------------------------------------------------------------------------------- /demo/node_client.js: -------------------------------------------------------------------------------- 1 | var grpc = require('grpc'); 2 | var helloworld = grpc.load('helloworld.proto').helloworld; 3 | 4 | var client = new helloworld.Greeter('localhost:50051', grpc.credentials.createInsecure()); 5 | 6 | var concat = client.concat(function (err, res) { 7 | console.log(res) 8 | }); 9 | concat.write({content: 'concat data 1'}); 10 | concat.write({content: 'concat data 2'}); 11 | concat.end(); 12 | -------------------------------------------------------------------------------- /kubernetes-manifest.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: grpc-bus-websocket-proxy 5 | spec: 6 | replicas: 1 7 | template: 8 | metadata: 9 | labels: 10 | run: grpc-bus-websocket-proxy 11 | spec: 12 | containers: 13 | - name: grpc-bus-websocket-proxy 14 | image: gabrielgrant/grpc-bus-websocket-proxy:0.1.1-debug3 15 | ports: 16 | - name: http 17 | containerPort: 8081 18 | protocol: TCP 19 | --- 20 | apiVersion: v1 21 | kind: Service 22 | metadata: 23 | name: grpc-bus-websocket-proxy-svc 24 | spec: 25 | ports: 26 | - name: grpc-port 27 | port: 8081 28 | nodePort: 32082 29 | selector: 30 | run: grpc-bus-websocket-proxy 31 | type: NodePort 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Gabriel Grant ", 3 | "name": "grpc-bus-websocket-server", 4 | "description": "A grpc-bus server using websockets for client communication", 5 | "version": "0.4.10", 6 | "license": "AGPL-3.0", 7 | "main": "index.js", 8 | "keywords": [ 9 | "Proxy", 10 | "RPC", 11 | "GRPC", 12 | "WebSocket", 13 | "WebSockets", 14 | "real-time" 15 | ], 16 | "repository": { 17 | "type": "git", 18 | "url": "git://github.com/gabrielgrant/grpc-bus-websocket-server.git" 19 | }, 20 | "bin": "server.js", 21 | "scripts": {}, 22 | "dependencies": { 23 | "grpc": "^1.24.0", 24 | "grpc-bus": "https://github.com/gabrielgrant/grpc-bus/releases/download/v1.0.1-dev5/grpc-bus-1.0.1-dev5.tgz", 25 | "protobufjs": "^5.0.2", 26 | "rsvp": "^3.3.3", 27 | "ws": "^7.0.0" 28 | }, 29 | "devDependencies": {} 30 | } 31 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | GRPC-Bus WebSocket Proxy Server 2 | 3 | This Node.js server acts as a proxy, connecting GRPC clients running in a browser context to standard GRPC service(s) via a WebSocket. 4 | 5 | You'll want to use the [GRPC Bus WebSocket Proxy Client](http://github.com/gabrielgrant/grpc-bus-websocket-proxy-client) to connect from the browser. 6 | 7 | Usage 8 | 9 | ``` 10 | node server.js 11 | ``` 12 | 13 | How it Works 14 | 15 | The browser client loads the Protobuf definition (either as a `.proto` 16 | file or compiled as a `.proto.json`), and passes it to the server via 17 | the initial message after creating the WebSocket connection. 18 | 19 | Deployment 20 | 21 | There is a [Docker image built](https://hub.docker.com/r/gabrielgrant/grpc-bus-websocket-proxy/) that can be run with: 22 | 23 | ``` 24 | docker run gabrielgrant/grpc-bus-websocket-proxy 25 | ``` 26 | 27 | It can also be deployed on a Kubernetes cluster using the included manifest: 28 | 29 | ``` 30 | kubectl create -f kubernetes-manifest.yaml 31 | ``` 32 | 33 | If redeploying, delete the earlier deployment first: 34 | 35 | ``` 36 | kubectl delete -f kubernetes-manifest.yaml && kubectl create -f kubernetes-manifest.yaml 37 | ``` 38 | 39 | TODO 40 | 41 | - Upgrade to Protobuf JS v6 42 | - Serve static content 43 | - Allow server to load .proto file directly 44 | - Push .proto file from server to client 45 | - Validate service map against proto file 46 | - Beter Error Handling 47 | - Support bundled/synchronous loading of JSON-formatted protoDefs 48 | - Specify allowed connections as CLI arg: --allow [service_name:]server:port 49 | - Specify port as CLI arg: --port 8080 50 | 51 | Publishing a new version: 52 | 53 | ``` 54 | git checkout master 55 | git pull 56 | npm version patch 57 | git push 58 | VERSION=$(cat package.json| jq --raw-output .version) 59 | docker build -t gabrielgrant/grpc-bus-websocket-proxy:latest -t gabrielgrant/grpc-bus-websocket-proxy:$VERSION . 60 | docker push gabrielgrant/grpc-bus-websocket-proxy:$VERSION 61 | docker push gabrielgrant/grpc-bus-websocket-proxy:latest 62 | ``` 63 | 64 | -------------------------------------------------------------------------------- /demo/helloworld.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2015, Google Inc. 2 | // All rights reserved. 3 | // 4 | // Redistribution and use in source and binary forms, with or without 5 | // modification, are permitted provided that the following conditions are 6 | // met: 7 | // 8 | // * Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer. 10 | // * Redistributions in binary form must reproduce the above 11 | // copyright notice, this list of conditions and the following disclaimer 12 | // in the documentation and/or other materials provided with the 13 | // distribution. 14 | // * Neither the name of Google Inc. nor the names of its 15 | // contributors may be used to endorse or promote products derived from 16 | // this software without specific prior written permission. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | syntax = "proto3"; 31 | 32 | option java_multiple_files = true; 33 | option java_package = "io.grpc.examples.helloworld"; 34 | option java_outer_classname = "HelloWorldProto"; 35 | 36 | package helloworld; 37 | 38 | // The greeting service definition. 39 | service Greeter { 40 | // Sends a greeting 41 | rpc SayHello (HelloRequest) returns (HelloReply) {} 42 | rpc StreamOutHello (HelloStreamRequest) returns (stream HelloReply) {} 43 | rpc StreamInHello(stream HelloStreamInRequest) returns (HelloStreamInResponse) {} 44 | rpc Echo (stream EchoMessage) returns (stream EchoMessage) {} 45 | rpc Concat (stream EchoMessage) returns (EchoMessage) {} 46 | } 47 | 48 | // The request message containing the user's name. 49 | message HelloRequest { 50 | string name = 1; 51 | } 52 | 53 | // The response message containing the greetings 54 | message HelloReply { 55 | string message = 1; 56 | } 57 | 58 | message HelloStreamRequest { 59 | string name = 1; 60 | int32 iterations = 2; 61 | } 62 | 63 | message HelloStreamInRequest { 64 | string name = 1; 65 | int32 iterations = 2; 66 | } 67 | 68 | message HelloStreamInResponse { 69 | string name = 1; 70 | } 71 | 72 | message EchoMessage { 73 | string content = 1; 74 | } 75 | -------------------------------------------------------------------------------- /grpc-bus.proto: -------------------------------------------------------------------------------- 1 | syntax="proto3"; 2 | package grpcbus; 3 | 4 | // Wrapper for a message from client 5 | message GBClientMessage { 6 | // Service management 7 | GBCreateService service_create = 1; 8 | GBReleaseService service_release = 2; 9 | 10 | // Call management 11 | GBCreateCall call_create = 3; 12 | // Terminates a call 13 | GBCallEnd call_end = 4; 14 | GBSendCall call_send = 5; 15 | } 16 | 17 | message GBServerMessage { 18 | // service management responses 19 | GBCreateServiceResult service_create = 1; 20 | GBReleaseServiceResult service_release = 2; 21 | 22 | // Call management 23 | GBCreateCallResult call_create = 3; 24 | GBCallEvent call_event = 4; 25 | GBCallEnded call_ended = 5; 26 | } 27 | 28 | // Information about a service 29 | message GBServiceInfo { 30 | // Endpoint 31 | string endpoint = 1; 32 | // Fully qualified service identifier 33 | string service_id = 2; 34 | // TODO: figure out how to serialize credentials 35 | } 36 | 37 | // Initialize a new Service. 38 | message GBCreateService { 39 | // ID of the service, client-generated, unique. 40 | int32 service_id = 1; 41 | GBServiceInfo service_info = 2; 42 | } 43 | 44 | // Release an existing / pending Service. 45 | message GBReleaseService { 46 | int32 service_id = 1; 47 | } 48 | 49 | message GBMetadataValues { 50 | repeated string values = 1; 51 | } 52 | 53 | message GBCallInfo { 54 | string method_id = 1; 55 | bytes bin_argument = 2; 56 | // Meta is a map from string to []string 57 | // e.g: https://godoc.org/google.golang.org/grpc/metadata#MD 58 | map metadata = 3; 59 | } 60 | 61 | // Create a call 62 | message GBCreateCall { 63 | int32 service_id = 1; 64 | int32 call_id = 2; 65 | // Info 66 | GBCallInfo info = 3; 67 | } 68 | 69 | // When the call is ended 70 | message GBCallEnded { 71 | int32 call_id = 1; 72 | int32 service_id = 2; 73 | } 74 | 75 | // End the call 76 | message GBEndCall { 77 | int32 call_id = 1; 78 | int32 service_id = 2; 79 | } 80 | 81 | // Send a message on a streaming call 82 | message GBSendCall { 83 | int32 call_id = 1; 84 | int32 service_id = 2; 85 | bytes bin_data = 3; 86 | // Do we want to just send end() over a streaming call? 87 | bool is_end = 4; 88 | } 89 | 90 | // Result of attempting to create a service 91 | message GBCreateServiceResult { 92 | // ID of service, client-generated, unique 93 | int32 service_id = 1; 94 | // Result 95 | ECreateServiceResult result = 2; 96 | // Error details 97 | string error_details = 3; 98 | 99 | enum ECreateServiceResult { 100 | // Success 101 | SUCCESS = 0; 102 | // Invalid service ID, retry with a new one. 103 | INVALID_ID = 1; 104 | // GRPC internal error constructing the service. 105 | GRPC_ERROR = 2; 106 | } 107 | } 108 | 109 | // When the server releases a service 110 | message GBReleaseServiceResult { 111 | int32 service_id = 1; 112 | } 113 | 114 | // Result of creating a call. 115 | // This is sent immediately after starting call. 116 | message GBCreateCallResult { 117 | int32 call_id = 1; 118 | int32 service_id = 4; 119 | 120 | // Result 121 | ECreateCallResult result = 2; 122 | string error_details = 3; 123 | 124 | enum ECreateCallResult { 125 | // Success 126 | SUCCESS = 0; 127 | // Invalid call ID, retry with a new one. 128 | INVALID_ID = 1; 129 | // GRPC internal error initializing the call 130 | GRPC_ERROR = 2; 131 | } 132 | } 133 | 134 | // Received message during streaming call. 135 | message GBCallEvent { 136 | // Call ID 137 | int32 call_id = 1; 138 | // Service ID 139 | int32 service_id = 4; 140 | // Event ID 141 | string event = 2; 142 | // JSON data. 143 | string json_data = 3; 144 | // Binary data 145 | bytes bin_data = 5; 146 | } 147 | 148 | // Terminate a call 149 | message GBCallEnd { 150 | int32 call_id = 1; 151 | int32 service_id = 2; 152 | } 153 | -------------------------------------------------------------------------------- /demo/helloserver.js: -------------------------------------------------------------------------------- 1 | // 2 | // Hello World GRPC Server 3 | // 4 | 5 | /* 6 | * 7 | * Copyright 2015, Google Inc. 8 | * All rights reserved. 9 | * 10 | * Redistribution and use in source and binary forms, with or without 11 | * modification, are permitted provided that the following conditions are 12 | * met: 13 | * 14 | * * Redistributions of source code must retain the above copyright 15 | * notice, this list of conditions and the following disclaimer. 16 | * * Redistributions in binary form must reproduce the above 17 | * copyright notice, this list of conditions and the following disclaimer 18 | * in the documentation and/or other materials provided with the 19 | * distribution. 20 | * * Neither the name of Google Inc. nor the names of its 21 | * contributors may be used to endorse or promote products derived from 22 | * this software without specific prior written permission. 23 | * 24 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 25 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 26 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 27 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 28 | * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 29 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 30 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 31 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 32 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 33 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 34 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 35 | * 36 | */ 37 | 38 | 39 | var PROTO_PATH = __dirname + '/helloworld.proto'; 40 | 41 | console.log('\n\nPROTO_PATH: ', PROTO_PATH); 42 | var grpc = require('grpc'); 43 | var hello_proto = grpc.load(PROTO_PATH).helloworld; 44 | 45 | /** 46 | * Implements the SayHello RPC method. 47 | */ 48 | function sayHello(call, callback) { 49 | callback(null, {message: 'Hello ' + call.request.name}); 50 | } 51 | 52 | /** 53 | * Implements the StreamOutHello RPC method. 54 | */ 55 | function streamOutHello(call) { 56 | for (var i=0; i {}; 15 | console.dir = () => {}; 16 | } else { 17 | console.log('running in verbose mode'); 18 | } 19 | 20 | 21 | if (process.env.OVERRIDE_ENDPOINT) { 22 | var overrideEndpoint = process.env.OVERRIDE_ENDPOINT; 23 | overrideEndpoint = overrideEndpoint.trim(); 24 | alwaysLog('all service endpoints will be overriden with: ', overrideEndpoint); 25 | } else { 26 | if (process.env.ALLOWED_ENDPOINTS) { 27 | var allowedEndpoints = process.env.ALLOWED_ENDPOINTS.split(','); 28 | allowedEndpoints = allowedEndpoints.map(s => s.trim()); 29 | alwaysLog('allowed service endpoints: ', allowedEndpoints); 30 | } else { 31 | alwaysLog('no ALLOWED_ENDPOINTS defined, so connections to all hosts will be allowed') 32 | } 33 | 34 | if (process.env.DEFAULT_ENDPOINT) { 35 | var defaultEndpoint = process.env.DEFAULT_ENDPOINT; 36 | defaultEndpoint = defaultEndpoint.trim(); 37 | alwaysLog('default service endpoint: ', defaultEndpoint); 38 | } else { 39 | alwaysLog('no DEFAULT_ENDPOINT is defined, so all client connection requests must specify their endpoint explicitly') 40 | } 41 | } 42 | 43 | gbBuilder = protobuf.loadProtoFile('grpc-bus.proto'); 44 | gbTree = gbBuilder.build().grpcbus; 45 | 46 | wss.on('connection', function connection(ws, request) { 47 | alwaysLog(`New connection established from ${request.connection.remoteAddress}`); 48 | 49 | ws.once('message', function incoming(data, flags) { 50 | var message = JSON.parse(data); 51 | console.log('connected with'); 52 | console.dir(message, { depth: null }); 53 | var protoFileExt = message.filename.substr(message.filename.lastIndexOf('.') + 1); 54 | if (protoFileExt === "json") { 55 | protoDefs = protobuf.loadJson(message.contents, null, message.filename); 56 | } else { 57 | protoDefs = protobuf.loadProto(message.contents, null, message.filename); 58 | } 59 | console.log('protoDefs'); 60 | console.log(protoDefs); 61 | var gbServer = new grpcBus.Server(protoDefs, function(message) { 62 | console.log('sending (pre-stringify): %s') 63 | console.dir(message, { depth: null }); 64 | console.log('sending (post-stringify): %s') 65 | console.dir(JSON.stringify(message)); 66 | //ws.send(JSON.stringify(message)); 67 | var pbMessage = new gbTree.GBServerMessage(message); 68 | console.log('sending (pbMessage message):', pbMessage); 69 | console.log('sending (raw message):', pbMessage.toBuffer()); 70 | console.log('re-decoded message:', gbTree.GBServerMessage.decode(pbMessage.toBuffer())); 71 | if (ws.readyState === ws.OPEN) { 72 | ws.send(pbMessage.toBuffer()); 73 | } else { 74 | console.error('WebSocket closed before message could be sent:', pbMessage); 75 | } 76 | }, require('grpc')); 77 | 78 | ws.on('message', function incoming(data, flags) { 79 | console.log('received (raw):'); 80 | console.log(data); 81 | console.log('with flags:') 82 | console.dir(flags); 83 | //var message = JSON.parse(data); 84 | var message = gbTree.GBClientMessage.decode(data); 85 | console.log('received (parsed):'); 86 | // We specify a constant depth here because the incoming 87 | // message may contain the Metadata object, which has 88 | // circular references and crashes console.dir if its 89 | // allowed to recurse to print. Depth of 3 was chosen 90 | // because it supplied enough detail when printing 91 | console.dir(message, { depth: 3 }); 92 | if (message.service_create) { 93 | let serviceId = message.service_create.service_info.service_id; 94 | let endpoint = message.service_create.service_info.endpoint; 95 | console.log(`client requested creation of a new service (${serviceId}) on ${endpoint}`) 96 | if (overrideEndpoint) { 97 | console.log(`overriding endpoing with ${overrideEndpoint}`) 98 | if (endpoint !== overrideEndpoint) { 99 | alwaysLog(`overriding endpoint ${endpoint} with ${overrideEndpoint}`); 100 | } 101 | message.service_create.service_info.endpoint = overrideEndpoint; 102 | } else { 103 | if (typeof defaultEndpoint !== 'undefined' && !endpoint) { 104 | console.log(`no endpoint specified, using default: ${defaultEndpoint}`) 105 | message.service_create.service_info.endpoint = defaultEndpoint; 106 | } 107 | if (typeof allowedEndpoints === 'undefined') { 108 | console.log('no allowedEndpoints defined, so connection will be allowed'); 109 | } else { 110 | console.log(`checking against allowedEndpoints whitelist (${allowedEndpoints})`); 111 | if (allowedEndpoints.includes(endpoint)) { 112 | console.log(`Requested endpoint in allowedEndpoints, so connection will be allowed`); 113 | } else { 114 | let msg = `Error: Attempted to connect to ${endpoint}, but that is not an allowed server (${allowedEndpoints})`; 115 | console.log(msg); 116 | ws.send(msg); 117 | } 118 | } 119 | } 120 | } 121 | gbServer.handleMessage(message); 122 | }); 123 | 124 | ws.on('error', error => console.error(`WebSocket Error: ${error.message}`)); 125 | }); 126 | }); 127 | 128 | wss.on('error', error => console.error(`WebSocket Server Error: ${error.message}`)); 129 | --------------------------------------------------------------------------------