mirror of https://github.com/grpc/grpc-web.git
Update Debian (and other deps) and remove Java In-process Proxy (#1335)
This commit is contained in:
parent
c5c1e29756
commit
f8fbbe32f9
|
@ -311,6 +311,8 @@ Multiple proxies support the gRPC-web protocol.
|
|||
|
||||
3. Apache [APISIX](https://apisix.apache.org/) has also added grpc-web support, and more details can be found [here](https://apisix.apache.org/blog/2022/01/25/apisix-grpc-web-integration/).
|
||||
|
||||
4. [Nginx](https://www.nginx.com/) has a grpc-web module ([doc](https://nginx.org/en/docs/http/ngx_http_grpc_module.html), [announcement](https://www.nginx.com/blog/nginx-1-13-10-grpc/))), and seems to work with simple configs, according to user [feedback](https://github.com/grpc/grpc-web/discussions/1322).
|
||||
|
||||
### Web Frameworks with gRPC-Web support
|
||||
- [Armeria (JVM)](https://armeria.dev/docs/server-grpc/#grpc-web)
|
||||
- [Tonic (Rust)](https://docs.rs/tonic-web/latest/tonic_web/)
|
||||
|
|
|
@ -93,11 +93,6 @@ services:
|
|||
image: grpcweb/interop-client
|
||||
ports:
|
||||
- "8081:8081"
|
||||
java-interop-server:
|
||||
build:
|
||||
context: ./
|
||||
dockerfile: ./net/grpc/gateway/docker/java_interop_server/Dockerfile
|
||||
image: grpcweb/java-interop-server
|
||||
protoc-plugin:
|
||||
build:
|
||||
context: ./
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
# Copyright 2018 Google LLC
|
||||
#
|
||||
# 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
|
||||
#
|
||||
# https://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.
|
||||
|
||||
FROM maven:3.6.1-jdk-8
|
||||
|
||||
RUN apt-get -qq update && apt-get -qq install -y \
|
||||
unzip
|
||||
|
||||
WORKDIR /tmp
|
||||
|
||||
RUN curl -sSL https://github.com/protocolbuffers/protobuf/releases/download/v3.14.0/\
|
||||
protoc-3.14.0-linux-x86_64.zip -o protoc.zip && \
|
||||
unzip -qq protoc.zip && \
|
||||
cp ./bin/protoc /usr/local/bin/protoc
|
||||
|
||||
RUN curl -sSL https://github.com/grpc/grpc-web/releases/download/1.4.2/\
|
||||
protoc-gen-grpc-web-1.4.2-linux-x86_64 -o /usr/local/bin/protoc-gen-grpc-web && \
|
||||
chmod +x /usr/local/bin/protoc-gen-grpc-web
|
||||
|
||||
WORKDIR /var/www/html/dist
|
||||
|
||||
WORKDIR /github/grpc-web
|
||||
|
||||
RUN git clone https://github.com/grpc/grpc-web . && \
|
||||
cd src/connector && \
|
||||
mvn install --no-transfer-progress && \
|
||||
cd ../../net/grpc/gateway/examples/grpc-web-java/interop-test-service && \
|
||||
mvn package --no-transfer-progress
|
||||
|
||||
ENTRYPOINT ["java", "-cp", "net/grpc/gateway/examples/grpc-web-java/interop-test-service/target/interop-test-0.1-jar-with-dependencies.jar", "grpcweb.examples.StartServiceAndGrpcwebProxy"]
|
|
@ -12,7 +12,7 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
FROM node:12.22.6-stretch
|
||||
FROM node:20.0.0-bullseye
|
||||
|
||||
WORKDIR /github/grpc-node
|
||||
|
||||
|
|
|
@ -18,8 +18,8 @@
|
|||
######################################
|
||||
# Stage 1: Fetch binaries
|
||||
######################################
|
||||
# node:... Docker image is based on buildpack-deps:stretch
|
||||
FROM buildpack-deps:stretch AS prepare
|
||||
# node:... Docker image is based on buildpack-deps:bullseye (11)
|
||||
FROM buildpack-deps:bullseye AS prepare
|
||||
|
||||
ARG BUILDIFIER_VERSION=1.0.0
|
||||
ARG PROTOBUF_VERSION=3.19.4
|
||||
|
@ -49,7 +49,7 @@ RUN ./scripts/init_submodules.sh
|
|||
######################################
|
||||
# Stage 2: Copy source files and build
|
||||
######################################
|
||||
FROM node:12.22.6-stretch AS copy-and-build
|
||||
FROM node:20.0.0-bullseye AS copy-and-build
|
||||
|
||||
ARG MAKEFLAGS=-j8
|
||||
ARG BAZEL_VERSION=4.1.0
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
"grpc-web": "~1.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"webpack": "~4.43.0",
|
||||
"webpack-cli": "~3.3.11"
|
||||
"webpack": "~5.82.1",
|
||||
"webpack-cli": "~5.1.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
"jquery": "~3.5.1",
|
||||
"mock-xmlhttprequest": "~2.0.0",
|
||||
"typescript": "latest",
|
||||
"webpack": "~4.43.0",
|
||||
"webpack-cli": "~3.3.11"
|
||||
"webpack": "~5.82.1",
|
||||
"webpack-cli": "~5.1.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -172,8 +172,8 @@ You will need a `package.json` file
|
|||
"grpc-web": "~1.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"webpack": "~4.43.0",
|
||||
"webpack-cli": "~3.3.11"
|
||||
"webpack": "~5.82.1",
|
||||
"webpack-cli": "~5.1.1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
This dir contains code to exercise the [java grpc-web in-process
|
||||
proxy](../../../../../../src/connector) with a simple
|
||||
[service](src/main/proto/greeter.proto).
|
||||
|
||||
To run this example, run this command
|
||||
```
|
||||
mvn test
|
||||
```
|
||||
|
||||
This example does the following:
|
||||
1. Implement a [service](src/main/proto/greeter.proto)
|
||||
2. Start the service on a **grpc-port**. Service is now ready to accept grpc requests on **grpc-port**
|
||||
3. Start the grpc-web in-process proxy on another port **grpc-web-port**
|
||||
4. Service is now ready to accept grpc-web requests on the **grpc-web-port**
|
|
@ -1,120 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>grpcweb.examples</groupId>
|
||||
<artifactId>grpcweb-java-examples-greeter</artifactId>
|
||||
<version>0.1</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.compiler.source>1.7</maven.compiler.source>
|
||||
<maven.compiler.target>1.7</maven.compiler.target>
|
||||
<grpc.version>1.30.0</grpc.version>
|
||||
<protobuf.version>3.12.0</protobuf.version>
|
||||
<grpc-port>7080</grpc-port>
|
||||
<grpc-web-port>8080</grpc-web-port>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.grpcweb</groupId>
|
||||
<artifactId>grpcweb-java</artifactId>
|
||||
<version>0.1-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>fluent-hc</artifactId>
|
||||
<version>4.5.12</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.tomcat</groupId>
|
||||
<artifactId>annotations-api</artifactId>
|
||||
<version>6.0.53</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-netty-shaded</artifactId>
|
||||
<version>${grpc.version}</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.13.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<extensions>
|
||||
<extension>
|
||||
<groupId>kr.motd.maven</groupId>
|
||||
<artifactId>os-maven-plugin</artifactId>
|
||||
<version>1.6.2</version>
|
||||
</extension>
|
||||
</extensions>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.xolstice.maven.plugins</groupId>
|
||||
<artifactId>protobuf-maven-plugin</artifactId>
|
||||
<version>0.6.1</version>
|
||||
<configuration>
|
||||
<protocArtifact>com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}</protocArtifact>
|
||||
<pluginId>grpc-java</pluginId>
|
||||
<pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
<goal>compile-custom</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>properties-maven-plugin</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>generate-resources</phase>
|
||||
<goals>
|
||||
<goal>write-project-properties</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<outputFile>${project.build.outputDirectory}/my.properties</outputFile>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-enforcer-plugin</artifactId>
|
||||
<version>1.4.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>enforce</id>
|
||||
<goals>
|
||||
<goal>enforce</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<rules>
|
||||
<requireUpperBoundDeps />
|
||||
</rules>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
|
@ -1,31 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 The gRPC Authors
|
||||
*
|
||||
* 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 grpcweb.examples.greeter;
|
||||
|
||||
import grpcweb.examples.greeter.GreeterOuterClass.HelloReply;
|
||||
import grpcweb.examples.greeter.GreeterOuterClass.HelloRequest;
|
||||
import io.grpc.stub.StreamObserver;
|
||||
|
||||
public class GreeterService extends GreeterGrpc.GreeterImplBase {
|
||||
@Override
|
||||
public void sayHello(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
|
||||
System.out.println("Greeter Service responding in sayhello() method");
|
||||
HelloReply reply = HelloReply.newBuilder().setMessage("Hello " + req.getName()).build();
|
||||
responseObserver.onNext(reply);
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
package grpcweb.examples.greeter;
|
||||
|
||||
import io.grpc.BindableService;
|
||||
import io.grpc.Server;
|
||||
import io.grpc.ServerBuilder;
|
||||
import io.grpcweb.GrpcPortNumRelay;
|
||||
import io.grpcweb.JettyWebserverForGrpcwebTraffic;
|
||||
|
||||
/**
|
||||
* This class starts the service on a port (property: grpc-port)
|
||||
* and starts the grpc-web proxy on a different port (property: grpc-web-port).
|
||||
*/
|
||||
class StartServiceAndGrpcwebProxy {
|
||||
private void startGrpcService(int port) throws Exception {
|
||||
Server grpcServer = ServerBuilder.forPort(port)
|
||||
.addService((BindableService) new GreeterService())
|
||||
.build();
|
||||
grpcServer.start();
|
||||
System.out.println("**** started gRPC Service on port# " + port);
|
||||
}
|
||||
|
||||
void start() throws Exception {
|
||||
int grpcPort = Util.getGrpcServicePortNum();
|
||||
int grpcWebPort = Util.getGrpcwebServicePortNum();
|
||||
|
||||
// Start the Grpc service on grpc-port
|
||||
startGrpcService(grpcPort);
|
||||
|
||||
// Start the grpc-web proxy on grpc-web-port.
|
||||
(new JettyWebserverForGrpcwebTraffic(grpcWebPort)).start();
|
||||
|
||||
// grpc-web proxy needs to know the grpc-port# so it could connect to the grpc service.
|
||||
GrpcPortNumRelay.setGrpcPortNum(grpcPort);
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
package grpcweb.examples.greeter;
|
||||
|
||||
import grpcweb.examples.greeter.GreeterOuterClass.HelloRequest;
|
||||
import java.io.IOException;
|
||||
import java.util.Properties;
|
||||
|
||||
class Util {
|
||||
static byte[] packageReqObjIntoGrpcwebProtocol(HelloRequest req) {
|
||||
byte[] reqObjBytes = req.toByteArray();
|
||||
int len = reqObjBytes.length;
|
||||
byte[] packagedBytes = new byte[5 + len];
|
||||
packagedBytes[0] = (byte) 0x00;
|
||||
packagedBytes[1] = (byte) ((len >> 24) & 0xff);
|
||||
packagedBytes[2] = (byte) ((len >> 16) & 0xff);
|
||||
packagedBytes[3] = (byte) ((len >> 8) & 0xff);
|
||||
packagedBytes[4] = (byte) ((len >> 0) & 0xff);
|
||||
System.arraycopy(reqObjBytes, 0, packagedBytes, 5, len);
|
||||
return packagedBytes;
|
||||
}
|
||||
|
||||
private static int getIntPropertyValue(String s) throws IOException {
|
||||
java.io.InputStream inputStream =
|
||||
Thread.currentThread().getContextClassLoader().getResourceAsStream("my.properties");
|
||||
java.util.Properties properties = new Properties();
|
||||
properties.load(inputStream);
|
||||
return Integer.parseInt(properties.getProperty(s));
|
||||
}
|
||||
|
||||
static int getGrpcServicePortNum() throws IOException {
|
||||
return getIntPropertyValue("grpc-port");
|
||||
}
|
||||
|
||||
static int getGrpcwebServicePortNum() throws IOException {
|
||||
return getIntPropertyValue("grpc-web-port");
|
||||
}
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
// Copyright 2020 The gRPC Authors
|
||||
//
|
||||
// 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.
|
||||
|
||||
// =======================================
|
||||
//
|
||||
// DO NOT EDIT
|
||||
// this is copy of
|
||||
// https://github.com/grpc/grpc-web/blob/master/net/grpc/gateway/
|
||||
// examples/helloworld/helloworld.proto
|
||||
//
|
||||
// TODO: can the original be directly used without making copy here
|
||||
// =======================================
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
option java_package = "grpcweb.examples.greeter";
|
||||
|
||||
package grpcweb.examples.greeter;
|
||||
|
||||
// The greeting service definition.
|
||||
service Greeter {
|
||||
// Sends a greeting
|
||||
rpc SayHello (HelloRequest) returns (HelloReply) {}
|
||||
}
|
||||
|
||||
// The request message containing the user's name.
|
||||
message HelloRequest {
|
||||
string name = 1;
|
||||
}
|
||||
|
||||
// The response message containing the greetings
|
||||
message HelloReply {
|
||||
string message = 1;
|
||||
}
|
|
@ -1,108 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 The gRPC Authors
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// ================================================
|
||||
//
|
||||
// This is for testing only. Just to demonstrate a simple
|
||||
// Java client to send grpc-web request and receive response
|
||||
// and validate the response.
|
||||
//
|
||||
// ================================================
|
||||
|
||||
package grpcweb.examples.greeter;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
import grpcweb.examples.greeter.GreeterOuterClass.HelloReply;
|
||||
import grpcweb.examples.greeter.GreeterOuterClass.HelloRequest;
|
||||
import java.io.IOException;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.logging.Logger;
|
||||
import org.apache.http.HttpVersion;
|
||||
import org.apache.http.client.fluent.Request;
|
||||
import org.apache.http.entity.ContentType;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/**
|
||||
* A simple client that requests a greeting from the {@link GreeterService}.
|
||||
*/
|
||||
@RunWith(JUnit4.class)
|
||||
public class GreeterClientTest {
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(MethodHandles.lookup().lookupClass().getName());
|
||||
|
||||
private void validateResponse(byte[] response) throws InvalidProtocolBufferException {
|
||||
LOG.info("Response is: " + new String(response));
|
||||
|
||||
// validate the 1st byte
|
||||
assertEquals((byte)0x00, response[0]);
|
||||
|
||||
int len = ByteBuffer.wrap(response, 1, 4).getInt();
|
||||
assertEquals(11, len);
|
||||
|
||||
// copy len bytes into a byte array, which can be xlated into HelloResponse
|
||||
byte[] protoBytes = new byte[len];
|
||||
System.arraycopy(response, 5, protoBytes, 0, len);
|
||||
HelloReply reply = HelloReply.parseFrom(protoBytes);
|
||||
assertEquals("Hello foo", reply.getMessage());
|
||||
|
||||
// if there is more data in the response, it should be a trailer.
|
||||
int offset = len + 5;
|
||||
int trailerBlockLen = response.length - offset;
|
||||
if (trailerBlockLen == 0) {
|
||||
// don't have any more bytes. we are done
|
||||
return;
|
||||
}
|
||||
assertEquals((byte)0x80, response[offset]);
|
||||
offset++;
|
||||
int trailerLen = ByteBuffer.wrap(response, offset, 4).getInt();
|
||||
assertTrue(trailerLen > 0);
|
||||
|
||||
byte[] trailer = new byte[trailerLen];
|
||||
System.arraycopy(response, offset+4, trailer, 0, trailerLen);
|
||||
String trailerStr = new String(trailer);
|
||||
// LOG.info("received trailer: " + trailerStr);
|
||||
assertTrue(trailerStr.startsWith("grpc-status:0"));
|
||||
}
|
||||
|
||||
private byte[] sendGrpcWebReqAndReceiveResponse() throws IOException {
|
||||
// create a HelloRequest obj
|
||||
HelloRequest reqObj = HelloRequest.newBuilder().setName("foo").build();
|
||||
byte[] packagedBytes = Util.packageReqObjIntoGrpcwebProtocol(reqObj);
|
||||
|
||||
// send request to the grpc-web server
|
||||
ContentType contentType = ContentType.create("application/grpc-web");
|
||||
int grpcWebPort = Util.getGrpcwebServicePortNum();
|
||||
return Request.Post("http://localhost:" + grpcWebPort +
|
||||
"/grpcweb.examples.greeter.Greeter/SayHello")
|
||||
.useExpectContinue()
|
||||
.version(HttpVersion.HTTP_1_1)
|
||||
.bodyByteArray(packagedBytes, contentType)
|
||||
.execute().returnContent().asBytes();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGreeterClient() throws Exception {
|
||||
new StartServiceAndGrpcwebProxy().start();
|
||||
byte[] response = sendGrpcWebReqAndReceiveResponse();
|
||||
validateResponse(response);
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
This dir contains code to run
|
||||
[interop-tests](../../../../../../test/interop/README.md)
|
||||
on the [java grpc-web in-process proxy](../../../../../../src/connector)
|
||||
|
||||
See [details](../../../../../../src/connector/README.md)
|
||||
on how to run the interop-tests using code in this dir.
|
|
@ -1,136 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>grpcweb.examples</groupId>
|
||||
<artifactId>interop-test</artifactId>
|
||||
<version>0.1</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.compiler.source>1.7</maven.compiler.source>
|
||||
<maven.compiler.target>1.7</maven.compiler.target>
|
||||
<grpc.version>1.33.0</grpc.version>
|
||||
<protobuf.version>3.12.0</protobuf.version>
|
||||
<grpc-port>7080</grpc-port>
|
||||
<grpc-web-port>8080</grpc-web-port>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-core</artifactId>
|
||||
<version>${grpc.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.grpcweb</groupId>
|
||||
<artifactId>grpcweb-java</artifactId>
|
||||
<version>0.1-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.tomcat</groupId>
|
||||
<artifactId>annotations-api</artifactId>
|
||||
<version>6.0.53</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-netty-shaded</artifactId>
|
||||
<version>${grpc.version}</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<extensions>
|
||||
<extension>
|
||||
<groupId>kr.motd.maven</groupId>
|
||||
<artifactId>os-maven-plugin</artifactId>
|
||||
<version>1.6.2</version>
|
||||
</extension>
|
||||
</extensions>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.xolstice.maven.plugins</groupId>
|
||||
<artifactId>protobuf-maven-plugin</artifactId>
|
||||
<version>0.6.1</version>
|
||||
<configuration>
|
||||
<protocArtifact>com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}</protocArtifact>
|
||||
<pluginId>grpc-java</pluginId>
|
||||
<pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
<goal>compile-custom</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>properties-maven-plugin</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>generate-resources</phase>
|
||||
<goals>
|
||||
<goal>write-project-properties</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<outputFile>${project.build.outputDirectory}/my.properties</outputFile>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<version>3.3.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<mainClass>grpcweb.examples.StartServiceAndGrpcwebProxy</mainClass>
|
||||
</manifest>
|
||||
</archive>
|
||||
<descriptorRefs>
|
||||
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||
</descriptorRefs>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-enforcer-plugin</artifactId>
|
||||
<version>1.4.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>enforce</id>
|
||||
<goals>
|
||||
<goal>enforce</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<rules>
|
||||
<requireUpperBoundDeps />
|
||||
</rules>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
|
@ -1,546 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 The gRPC Authors
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// ******************* DO NOT EDIT
|
||||
// This is copy of the following:
|
||||
// github.com/grpc/grpc-java/blob/master/interop-testing/src/main/java/io/grpc/testing/integration/TestServiceImpl.java
|
||||
|
||||
package grpcweb.examples;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.Queues;
|
||||
import com.google.protobuf.ByteString;
|
||||
import io.grpc.ForwardingServerCall.SimpleForwardingServerCall;
|
||||
import io.grpc.Metadata;
|
||||
import io.grpc.ServerCall;
|
||||
import io.grpc.ServerCallHandler;
|
||||
import io.grpc.ServerInterceptor;
|
||||
import io.grpc.Status;
|
||||
import io.grpc.internal.LogExceptionRunnable;
|
||||
import io.grpc.protobuf.lite.ProtoLiteUtils;
|
||||
import io.grpc.stub.ServerCallStreamObserver;
|
||||
import io.grpc.stub.StreamObserver;
|
||||
import grpc.testing.EmptyProtos;
|
||||
import grpc.testing.Messages;
|
||||
import grpc.testing.Messages.Payload;
|
||||
import grpc.testing.Messages.ResponseParameters;
|
||||
import grpc.testing.Messages.SimpleRequest;
|
||||
import grpc.testing.Messages.SimpleResponse;
|
||||
import grpc.testing.Messages.StreamingInputCallRequest;
|
||||
import grpc.testing.Messages.StreamingInputCallResponse;
|
||||
import grpc.testing.Messages.StreamingOutputCallRequest;
|
||||
import grpc.testing.Messages.StreamingOutputCallResponse;
|
||||
import grpc.testing.TestServiceGrpc;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import javax.annotation.concurrent.GuardedBy;
|
||||
|
||||
/**
|
||||
* Implementation of the business logic for the TestService. Uses an executor to schedule chunks
|
||||
* sent in response streams.
|
||||
*/
|
||||
public class InteropTestService extends TestServiceGrpc.TestServiceImplBase {
|
||||
private final Random random = new Random();
|
||||
|
||||
private final ScheduledExecutorService executor;
|
||||
private final ByteString compressableBuffer;
|
||||
|
||||
private static final Metadata.Key<Messages.SimpleContext> METADATA_KEY =
|
||||
Metadata.Key.of(
|
||||
"grpc.testing.SimpleContext" + Metadata.BINARY_HEADER_SUFFIX,
|
||||
ProtoLiteUtils.metadataMarshaller(Messages.SimpleContext.getDefaultInstance()));
|
||||
private static final Metadata.Key<String> ECHO_INITIAL_METADATA_KEY
|
||||
= Metadata.Key.of("x-grpc-test-echo-initial", Metadata.ASCII_STRING_MARSHALLER);
|
||||
private static final Metadata.Key<byte[]> ECHO_TRAILING_METADATA_KEY
|
||||
= Metadata.Key.of("x-grpc-test-echo-trailing-bin", Metadata.BINARY_BYTE_MARSHALLER);
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a controller using the given executor for scheduling response stream chunks.
|
||||
*/
|
||||
public InteropTestService(ScheduledExecutorService executor) {
|
||||
this.executor = executor;
|
||||
this.compressableBuffer = ByteString.copyFrom(new byte[1024]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void emptyCall(EmptyProtos.Empty request,
|
||||
StreamObserver<EmptyProtos.Empty> responseObserver) {
|
||||
responseObserver.onNext(EmptyProtos.Empty.getDefaultInstance());
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
|
||||
/**
|
||||
* Immediately responds with a payload of the type and size specified in the request.
|
||||
*/
|
||||
@Override
|
||||
public void unaryCall(SimpleRequest req, StreamObserver<SimpleResponse> responseObserver) {
|
||||
ServerCallStreamObserver<SimpleResponse> obs =
|
||||
(ServerCallStreamObserver<SimpleResponse>) responseObserver;
|
||||
SimpleResponse.Builder responseBuilder = SimpleResponse.newBuilder();
|
||||
try {
|
||||
if (req.hasResponseCompressed() && req.getResponseCompressed().getValue()) {
|
||||
obs.setCompression("gzip");
|
||||
} else {
|
||||
obs.setCompression("identity");
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
System.out.println("sending error");
|
||||
obs.onError(Status.UNIMPLEMENTED
|
||||
.withDescription("compression not supported.")
|
||||
.withCause(e)
|
||||
.asRuntimeException());
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.getResponseSize() != 0) {
|
||||
// For consistency with the c++ TestServiceImpl, use a random offset for unary calls.
|
||||
// TODO(wonderfly): whether or not this is a good approach needs further discussion.
|
||||
int offset = random.nextInt(compressableBuffer.size());
|
||||
ByteString payload = generatePayload(compressableBuffer, offset, req.getResponseSize());
|
||||
responseBuilder.setPayload(
|
||||
Payload.newBuilder()
|
||||
.setBody(payload));
|
||||
}
|
||||
|
||||
if (req.hasResponseStatus()) {
|
||||
obs.onError(Status.fromCodeValue(req.getResponseStatus().getCode())
|
||||
.withDescription(req.getResponseStatus().getMessage())
|
||||
.asRuntimeException());
|
||||
return;
|
||||
}
|
||||
|
||||
responseObserver.onNext(responseBuilder.build());
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a request that specifies chunk size and interval between responses, creates and schedules
|
||||
* the response stream.
|
||||
*/
|
||||
@Override
|
||||
public void streamingOutputCall(StreamingOutputCallRequest request,
|
||||
StreamObserver<StreamingOutputCallResponse> responseObserver) {
|
||||
// Create and start the response dispatcher.
|
||||
new ResponseDispatcher(responseObserver).enqueue(toChunkQueue(request)).completeInput();
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until we have received all of the request messages and then returns the aggregate payload
|
||||
* size for all of the received requests.
|
||||
*/
|
||||
@Override
|
||||
public StreamObserver<Messages.StreamingInputCallRequest> streamingInputCall(
|
||||
final StreamObserver<Messages.StreamingInputCallResponse> responseObserver) {
|
||||
return new StreamObserver<StreamingInputCallRequest>() {
|
||||
private int totalPayloadSize;
|
||||
|
||||
@Override
|
||||
public void onNext(StreamingInputCallRequest message) {
|
||||
totalPayloadSize += message.getPayload().getBody().size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleted() {
|
||||
responseObserver.onNext(StreamingInputCallResponse.newBuilder()
|
||||
.setAggregatedPayloadSize(totalPayloadSize).build());
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable cause) {
|
||||
responseObserver.onError(cause);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* True bi-directional streaming. Processes requests as they come in. Begins streaming results
|
||||
* immediately.
|
||||
*/
|
||||
@Override
|
||||
public StreamObserver<Messages.StreamingOutputCallRequest> fullDuplexCall(
|
||||
final StreamObserver<Messages.StreamingOutputCallResponse> responseObserver) {
|
||||
final ResponseDispatcher dispatcher = new ResponseDispatcher(responseObserver);
|
||||
return new StreamObserver<StreamingOutputCallRequest>() {
|
||||
@Override
|
||||
public void onNext(StreamingOutputCallRequest request) {
|
||||
if (request.hasResponseStatus()) {
|
||||
dispatcher.cancel();
|
||||
dispatcher.onError(Status.fromCodeValue(request.getResponseStatus().getCode())
|
||||
.withDescription(request.getResponseStatus().getMessage())
|
||||
.asRuntimeException());
|
||||
return;
|
||||
}
|
||||
dispatcher.enqueue(toChunkQueue(request));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleted() {
|
||||
if (!dispatcher.isCancelled()) {
|
||||
// Tell the dispatcher that all input has been received.
|
||||
dispatcher.completeInput();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable cause) {
|
||||
dispatcher.onError(cause);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to {@link #fullDuplexCall}, except that it waits for all streaming requests to be
|
||||
* received before starting the streaming responses.
|
||||
*/
|
||||
@Override
|
||||
public StreamObserver<Messages.StreamingOutputCallRequest> halfDuplexCall(
|
||||
final StreamObserver<Messages.StreamingOutputCallResponse> responseObserver) {
|
||||
final ResponseDispatcher dispatcher = new ResponseDispatcher(responseObserver);
|
||||
final Queue<Chunk> chunks = new ArrayDeque<>();
|
||||
return new StreamObserver<StreamingOutputCallRequest>() {
|
||||
@Override
|
||||
public void onNext(StreamingOutputCallRequest request) {
|
||||
chunks.addAll(toChunkQueue(request));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleted() {
|
||||
// Dispatch all of the chunks in one shot.
|
||||
dispatcher.enqueue(chunks).completeInput();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable cause) {
|
||||
dispatcher.onError(cause);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules the dispatch of a queue of chunks. Whenever chunks are added or input is completed,
|
||||
* the next response chunk is scheduled for delivery to the client. When no more chunks are
|
||||
* available, the stream is half-closed.
|
||||
*/
|
||||
private class ResponseDispatcher {
|
||||
private final Chunk completionChunk = new Chunk(0, 0, 0);
|
||||
private final Queue<Chunk> chunks;
|
||||
private final StreamObserver<StreamingOutputCallResponse> responseStream;
|
||||
private boolean scheduled;
|
||||
@GuardedBy("this") private boolean cancelled;
|
||||
private Throwable failure;
|
||||
private Runnable dispatchTask = new Runnable() {
|
||||
@Override
|
||||
@SuppressWarnings("CatchAndPrintStackTrace")
|
||||
public void run() {
|
||||
try {
|
||||
|
||||
// Dispatch the current chunk to the client.
|
||||
try {
|
||||
dispatchChunk();
|
||||
} catch (RuntimeException e) {
|
||||
// Indicate that nothing is scheduled and re-throw.
|
||||
synchronized (ResponseDispatcher.this) {
|
||||
scheduled = false;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Schedule the next chunk if there is one.
|
||||
synchronized (ResponseDispatcher.this) {
|
||||
// Indicate that nothing is scheduled.
|
||||
scheduled = false;
|
||||
scheduleNextChunk();
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
t.printStackTrace();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The {@link StreamObserver} will be used to send the queue of response chunks. Since calls to
|
||||
* {@link StreamObserver} must be synchronized across threads, no further calls should be made
|
||||
* directly on {@code responseStream} after it is provided to the {@link ResponseDispatcher}.
|
||||
*/
|
||||
public ResponseDispatcher(StreamObserver<StreamingOutputCallResponse> responseStream) {
|
||||
this.chunks = Queues.newLinkedBlockingQueue();
|
||||
this.responseStream = responseStream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given chunks to the response stream and schedules the next chunk to be delivered if
|
||||
* needed.
|
||||
*/
|
||||
public synchronized ResponseDispatcher enqueue(Queue<Chunk> moreChunks) {
|
||||
assertNotFailed();
|
||||
chunks.addAll(moreChunks);
|
||||
scheduleNextChunk();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that the input is completed and the currently enqueued response chunks are all that
|
||||
* remain to be scheduled for dispatch to the client.
|
||||
*/
|
||||
public ResponseDispatcher completeInput() {
|
||||
assertNotFailed();
|
||||
chunks.add(completionChunk);
|
||||
scheduleNextChunk();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows the service to cancel the remaining responses.
|
||||
*/
|
||||
public synchronized void cancel() {
|
||||
Preconditions.checkState(!cancelled, "Dispatcher already cancelled");
|
||||
chunks.clear();
|
||||
cancelled = true;
|
||||
}
|
||||
|
||||
public synchronized boolean isCancelled() {
|
||||
return cancelled;
|
||||
}
|
||||
|
||||
private synchronized void onError(Throwable cause) {
|
||||
responseStream.onError(cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches the current response chunk to the client. This is only called by the executor. At
|
||||
* any time, a given dispatch task should only be registered with the executor once.
|
||||
*/
|
||||
private synchronized void dispatchChunk() {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Pop off the next chunk and send it to the client.
|
||||
Chunk chunk = chunks.remove();
|
||||
if (chunk == completionChunk) {
|
||||
responseStream.onCompleted();
|
||||
} else {
|
||||
responseStream.onNext(chunk.toResponse());
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
failure = e;
|
||||
if (Status.fromThrowable(e).getCode() == Status.CANCELLED.getCode()) {
|
||||
// Stream was cancelled by client, responseStream.onError() might be called already or
|
||||
// will be called soon by inbounding StreamObserver.
|
||||
chunks.clear();
|
||||
} else {
|
||||
responseStream.onError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules the next response chunk to be dispatched. If all input has been received and there
|
||||
* are no more chunks in the queue, the stream is closed.
|
||||
*/
|
||||
private void scheduleNextChunk() {
|
||||
synchronized (this) {
|
||||
if (scheduled) {
|
||||
// Dispatch task is already scheduled.
|
||||
return;
|
||||
}
|
||||
|
||||
// Schedule the next response chunk if there is one.
|
||||
Chunk nextChunk = chunks.peek();
|
||||
if (nextChunk != null) {
|
||||
scheduled = true;
|
||||
// TODO(ejona): cancel future if RPC is cancelled
|
||||
Future<?> unused = executor.schedule(new LogExceptionRunnable(dispatchTask),
|
||||
nextChunk.delayMicroseconds, TimeUnit.MICROSECONDS);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void assertNotFailed() {
|
||||
if (failure != null) {
|
||||
throw new IllegalStateException("Stream already failed", failure);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Breaks down the request and creates a queue of response chunks for the given request.
|
||||
*/
|
||||
public Queue<Chunk> toChunkQueue(StreamingOutputCallRequest request) {
|
||||
Queue<Chunk> chunkQueue = new ArrayDeque<>();
|
||||
int offset = 0;
|
||||
for (ResponseParameters params : request.getResponseParametersList()) {
|
||||
chunkQueue.add(new Chunk(params.getIntervalUs(), offset, params.getSize()));
|
||||
|
||||
// Increment the offset past this chunk. Buffer need to be circular.
|
||||
offset = (offset + params.getSize()) % compressableBuffer.size();
|
||||
}
|
||||
return chunkQueue;
|
||||
}
|
||||
|
||||
/**
|
||||
* A single chunk of a response stream. Contains delivery information for the dispatcher and can
|
||||
* be converted to a streaming response proto. A chunk just references it's payload in the
|
||||
* {@link #compressableBuffer} array. The payload isn't actually created until {@link
|
||||
* #toResponse()} is called.
|
||||
*/
|
||||
private class Chunk {
|
||||
private final int delayMicroseconds;
|
||||
private final int offset;
|
||||
private final int length;
|
||||
|
||||
public Chunk(int delayMicroseconds, int offset, int length) {
|
||||
this.delayMicroseconds = delayMicroseconds;
|
||||
this.offset = offset;
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert this chunk into a streaming response proto.
|
||||
*/
|
||||
private StreamingOutputCallResponse toResponse() {
|
||||
StreamingOutputCallResponse.Builder responseBuilder =
|
||||
StreamingOutputCallResponse.newBuilder();
|
||||
ByteString payload = generatePayload(compressableBuffer, offset, length);
|
||||
responseBuilder.setPayload(
|
||||
Payload.newBuilder()
|
||||
.setBody(payload));
|
||||
return responseBuilder.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a payload of desired type and size. Reads compressableBuffer or
|
||||
* uncompressableBuffer as a circular buffer.
|
||||
*/
|
||||
private ByteString generatePayload(ByteString dataBuffer, int offset, int size) {
|
||||
ByteString payload = ByteString.EMPTY;
|
||||
// This offset would never pass the array boundary.
|
||||
int begin = offset;
|
||||
int end = 0;
|
||||
int bytesLeft = size;
|
||||
while (bytesLeft > 0) {
|
||||
end = Math.min(begin + bytesLeft, dataBuffer.size());
|
||||
// ByteString.substring returns the substring from begin, inclusive, to end, exclusive.
|
||||
payload = payload.concat(dataBuffer.substring(begin, end));
|
||||
bytesLeft -= (end - begin);
|
||||
begin = end % dataBuffer.size();
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
/** Returns interceptors necessary for full service implementation. */
|
||||
public static List<ServerInterceptor> interceptors() {
|
||||
return Arrays.asList(
|
||||
echoRequestHeadersInterceptor(METADATA_KEY),
|
||||
echoRequestMetadataInHeaders(ECHO_INITIAL_METADATA_KEY),
|
||||
echoRequestMetadataInTrailers(ECHO_TRAILING_METADATA_KEY));
|
||||
}
|
||||
|
||||
/**
|
||||
* Echo the request headers from a client into response headers and trailers. Useful for
|
||||
* testing end-to-end metadata propagation.
|
||||
*/
|
||||
private static ServerInterceptor echoRequestHeadersInterceptor(final Metadata.Key<?>... keys) {
|
||||
final Set<Metadata.Key<?>> keySet = new HashSet<>(Arrays.asList(keys));
|
||||
return new ServerInterceptor() {
|
||||
@Override
|
||||
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
|
||||
ServerCall<ReqT, RespT> call,
|
||||
final Metadata requestHeaders,
|
||||
ServerCallHandler<ReqT, RespT> next) {
|
||||
return next.startCall(new SimpleForwardingServerCall<ReqT, RespT>(call) {
|
||||
@Override
|
||||
public void sendHeaders(Metadata responseHeaders) {
|
||||
responseHeaders.merge(requestHeaders, keySet);
|
||||
super.sendHeaders(responseHeaders);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close(Status status, Metadata trailers) {
|
||||
trailers.merge(requestHeaders, keySet);
|
||||
super.close(status, trailers);
|
||||
}
|
||||
}, requestHeaders);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Echoes request headers with the specified key(s) from a client into response headers only.
|
||||
*/
|
||||
private static ServerInterceptor echoRequestMetadataInHeaders(final Metadata.Key<?>... keys) {
|
||||
final Set<Metadata.Key<?>> keySet = new HashSet<>(Arrays.asList(keys));
|
||||
return new ServerInterceptor() {
|
||||
@Override
|
||||
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
|
||||
ServerCall<ReqT, RespT> call,
|
||||
final Metadata requestHeaders,
|
||||
ServerCallHandler<ReqT, RespT> next) {
|
||||
return next.startCall(new SimpleForwardingServerCall<ReqT, RespT>(call) {
|
||||
@Override
|
||||
public void sendHeaders(Metadata responseHeaders) {
|
||||
responseHeaders.merge(requestHeaders, keySet);
|
||||
super.sendHeaders(responseHeaders);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close(Status status, Metadata trailers) {
|
||||
super.close(status, trailers);
|
||||
}
|
||||
}, requestHeaders);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Echoes request headers with the specified key(s) from a client into response trailers only.
|
||||
*/
|
||||
private static ServerInterceptor echoRequestMetadataInTrailers(final Metadata.Key<?>... keys) {
|
||||
final Set<Metadata.Key<?>> keySet = new HashSet<>(Arrays.asList(keys));
|
||||
return new ServerInterceptor() {
|
||||
@Override
|
||||
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
|
||||
ServerCall<ReqT, RespT> call,
|
||||
final Metadata requestHeaders,
|
||||
ServerCallHandler<ReqT, RespT> next) {
|
||||
return next.startCall(new SimpleForwardingServerCall<ReqT, RespT>(call) {
|
||||
@Override
|
||||
public void sendHeaders(Metadata responseHeaders) {
|
||||
super.sendHeaders(responseHeaders);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close(Status status, Metadata trailers) {
|
||||
trailers.merge(requestHeaders, keySet);
|
||||
super.close(status, trailers);
|
||||
}
|
||||
}, requestHeaders);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
package grpcweb.examples;
|
||||
|
||||
import io.grpc.BindableService;
|
||||
import io.grpc.Server;
|
||||
import io.grpc.ServerBuilder;
|
||||
import io.grpc.ServerInterceptors;
|
||||
import io.grpcweb.GrpcPortNumRelay;
|
||||
import io.grpcweb.JettyWebserverForGrpcwebTraffic;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/**
|
||||
* This class starts the service on a port (property: grpc-port)
|
||||
* and starts the grpc-web proxy on a different port (property: grpc-web-port).
|
||||
*/
|
||||
class StartServiceAndGrpcwebProxy {
|
||||
private void startGrpcService(int port) throws Exception {
|
||||
Server grpcServer = ServerBuilder.forPort(port)
|
||||
.addService(
|
||||
ServerInterceptors.intercept(
|
||||
(BindableService) new InteropTestService(Executors.newSingleThreadScheduledExecutor()),
|
||||
InteropTestService.interceptors()))
|
||||
.build();
|
||||
grpcServer.start();
|
||||
System.out.println("**** started gRPC Service on port# " + port);
|
||||
}
|
||||
|
||||
void start() throws Exception {
|
||||
int grpcPort = Util.getGrpcServicePortNum();
|
||||
int grpcWebPort = Util.getGrpcwebServicePortNum();
|
||||
|
||||
// Start the Grpc service on grpc-port
|
||||
startGrpcService(grpcPort);
|
||||
|
||||
// Start the grpc-web proxy on grpc-web-port.
|
||||
(new JettyWebserverForGrpcwebTraffic(grpcWebPort)).start();
|
||||
|
||||
// grpc-web proxy needs to know the grpc-port# so it could connect to the grpc service.
|
||||
GrpcPortNumRelay.setGrpcPortNum(grpcPort);
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
new StartServiceAndGrpcwebProxy().start();
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
package grpcweb.examples;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Properties;
|
||||
|
||||
class Util {
|
||||
private static int getIntPropertyValue(String s) throws IOException {
|
||||
java.io.InputStream inputStream =
|
||||
Thread.currentThread().getContextClassLoader().getResourceAsStream("my.properties");
|
||||
java.util.Properties properties = new Properties();
|
||||
properties.load(inputStream);
|
||||
return Integer.parseInt(properties.getProperty(s));
|
||||
}
|
||||
|
||||
static int getGrpcServicePortNum() throws IOException {
|
||||
return getIntPropertyValue("grpc-port");
|
||||
}
|
||||
|
||||
static int getGrpcwebServicePortNum() throws IOException {
|
||||
return getIntPropertyValue("grpc-web-port");
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
// Copyright 2015 The gRPC Authors
|
||||
//
|
||||
// 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.
|
||||
|
||||
// ******************* DO NOT EDIT
|
||||
// This is copy of the following:
|
||||
// github.com/grpc/grpc-java/blob/master/interop-testing/src/main/proto/grpc/testing/empty.proto
|
||||
|
||||
syntax = "proto2";
|
||||
|
||||
package grpc.testing;
|
||||
|
||||
option java_package = "grpc.testing";
|
||||
option java_outer_classname = "EmptyProtos";
|
||||
|
||||
// An empty message that you can re-use to avoid defining duplicated empty
|
||||
// messages in your project. A typical example is to use it as argument or the
|
||||
// return value of a service API. For instance:
|
||||
//
|
||||
// service Foo {
|
||||
// rpc Bar (grpc.testing.Empty) returns (grpc.testing.Empty) { };
|
||||
// };
|
||||
//
|
||||
message Empty {}
|
|
@ -1,199 +0,0 @@
|
|||
// Copyright 2015 The gRPC Authors
|
||||
//
|
||||
// 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.
|
||||
|
||||
// Message definitions to be used by integration test service definitions.
|
||||
|
||||
// ******************* DO NOT EDIT
|
||||
// This is copy of the following:
|
||||
// github.com/grpc/grpc-java/blob/master/interop-testing/src/main/proto/grpc/testing/messages.proto
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package grpc.testing;
|
||||
|
||||
option java_package = "grpc.testing";
|
||||
|
||||
// TODO(jihuncho): Use well-known types once all languages are synced.
|
||||
message BoolValue {
|
||||
// The bool value.
|
||||
bool value = 1;
|
||||
}
|
||||
|
||||
// A block of data, to simply increase gRPC message size.
|
||||
message Payload {
|
||||
reserved 1;
|
||||
|
||||
// Primary contents of payload.
|
||||
bytes body = 2;
|
||||
}
|
||||
|
||||
// A protobuf representation for grpc status. This is used by test
|
||||
// clients to specify a status that the server should attempt to return.
|
||||
message EchoStatus {
|
||||
int32 code = 1;
|
||||
string message = 2;
|
||||
}
|
||||
|
||||
// The type of route that a client took to reach a server w.r.t. gRPCLB.
|
||||
// The server must fill in "fallback" if it detects that the RPC reached
|
||||
// the server via the "gRPCLB fallback" path, and "backend" if it detects
|
||||
// that the RPC reached the server via "gRPCLB backend" path (i.e. if it got
|
||||
// the address of this server from the gRPCLB server BalanceLoad RPC). Exactly
|
||||
// how this detection is done is context and server dependent.
|
||||
enum GrpclbRouteType {
|
||||
// Server didn't detect the route that a client took to reach it.
|
||||
GRPCLB_ROUTE_TYPE_UNKNOWN = 0;
|
||||
// Indicates that a client reached a server via gRPCLB fallback.
|
||||
GRPCLB_ROUTE_TYPE_FALLBACK = 1;
|
||||
// Indicates that a client reached a server as a gRPCLB-given backend.
|
||||
GRPCLB_ROUTE_TYPE_BACKEND = 2;
|
||||
}
|
||||
|
||||
// Unary request.
|
||||
message SimpleRequest {
|
||||
reserved 1;
|
||||
|
||||
// Desired payload size in the response from the server.
|
||||
int32 response_size = 2;
|
||||
|
||||
// Optional input payload sent along with the request.
|
||||
Payload payload = 3;
|
||||
|
||||
// Whether SimpleResponse should include username.
|
||||
bool fill_username = 4;
|
||||
|
||||
// Whether SimpleResponse should include OAuth scope.
|
||||
bool fill_oauth_scope = 5;
|
||||
|
||||
// Whether to request the server to compress the response. This field is
|
||||
// "nullable" in order to interoperate seamlessly with clients not able to
|
||||
// implement the full compression tests by introspecting the call to verify
|
||||
// the response's compression status.
|
||||
BoolValue response_compressed = 6;
|
||||
|
||||
// Whether server should return a given status
|
||||
EchoStatus response_status = 7;
|
||||
|
||||
// Whether the server should expect this request to be compressed.
|
||||
BoolValue expect_compressed = 8;
|
||||
|
||||
// Whether SimpleResponse should include server_id.
|
||||
bool fill_server_id = 9;
|
||||
}
|
||||
|
||||
// Unary response, as configured by the request.
|
||||
message SimpleResponse {
|
||||
// Payload to increase message size.
|
||||
Payload payload = 1;
|
||||
// The user the request came from, for verifying authentication was
|
||||
// successful when the client expected it.
|
||||
string username = 2;
|
||||
// OAuth scope.
|
||||
string oauth_scope = 3;
|
||||
// Server ID. This must be unique among different server instances,
|
||||
// but the same across all RPC's made to a particular server instance.
|
||||
string server_id = 4;
|
||||
// gRPCLB Path.
|
||||
GrpclbRouteType grpclb_route_type = 5;
|
||||
// Server hostname.
|
||||
string hostname = 6;
|
||||
}
|
||||
|
||||
message SimpleContext {
|
||||
string value = 1;
|
||||
}
|
||||
|
||||
// Client-streaming request.
|
||||
message StreamingInputCallRequest {
|
||||
// Optional input payload sent along with the request.
|
||||
Payload payload = 1;
|
||||
|
||||
// Whether the server should expect this request to be compressed. This field
|
||||
// is "nullable" in order to interoperate seamlessly with servers not able to
|
||||
// implement the full compression tests by introspecting the call to verify
|
||||
// the request's compression status.
|
||||
BoolValue expect_compressed = 2;
|
||||
|
||||
// Not expecting any payload from the response.
|
||||
}
|
||||
|
||||
// Client-streaming response.
|
||||
message StreamingInputCallResponse {
|
||||
// Aggregated size of payloads received from the client.
|
||||
int32 aggregated_payload_size = 1;
|
||||
}
|
||||
|
||||
// Configuration for a particular response.
|
||||
message ResponseParameters {
|
||||
// Desired payload sizes in responses from the server.
|
||||
int32 size = 1;
|
||||
|
||||
// Desired interval between consecutive responses in the response stream in
|
||||
// microseconds.
|
||||
int32 interval_us = 2;
|
||||
|
||||
// Whether to request the server to compress the response. This field is
|
||||
// "nullable" in order to interoperate seamlessly with clients not able to
|
||||
// implement the full compression tests by introspecting the call to verify
|
||||
// the response's compression status.
|
||||
BoolValue compressed = 3;
|
||||
}
|
||||
|
||||
// Server-streaming request.
|
||||
message StreamingOutputCallRequest {
|
||||
reserved 1;
|
||||
|
||||
// Configuration for each expected response message.
|
||||
repeated ResponseParameters response_parameters = 2;
|
||||
|
||||
// Optional input payload sent along with the request.
|
||||
Payload payload = 3;
|
||||
|
||||
// Whether server should return a given status
|
||||
EchoStatus response_status = 7;
|
||||
}
|
||||
|
||||
// Server-streaming response, as configured by the request and parameters.
|
||||
message StreamingOutputCallResponse {
|
||||
// Payload to increase response size.
|
||||
Payload payload = 1;
|
||||
}
|
||||
|
||||
// For reconnect interop test only.
|
||||
// Client tells server what reconnection parameters it used.
|
||||
message ReconnectParams {
|
||||
int32 max_reconnect_backoff_ms = 1;
|
||||
}
|
||||
|
||||
// For reconnect interop test only.
|
||||
// Server tells client whether its reconnects are following the spec and the
|
||||
// reconnect backoffs it saw.
|
||||
message ReconnectInfo {
|
||||
bool passed = 1;
|
||||
repeated int32 backoff_ms = 2;
|
||||
}
|
||||
|
||||
message LoadBalancerStatsRequest {
|
||||
// Request stats for the next num_rpcs sent by client.
|
||||
int32 num_rpcs = 1;
|
||||
// If num_rpcs have not completed within timeout_sec, return partial results.
|
||||
int32 timeout_sec = 2;
|
||||
}
|
||||
|
||||
message LoadBalancerStatsResponse {
|
||||
// The number of completed RPCs for each peer.
|
||||
map<string, int32> rpcs_by_peer = 1;
|
||||
// The number of RPCs that failed to record a remote peer.
|
||||
int32 num_failures = 2;
|
||||
}
|
|
@ -1,94 +0,0 @@
|
|||
// Copyright 2015 The gRPC Authors
|
||||
//
|
||||
// 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.
|
||||
// An integration test service that covers all the method signature permutations
|
||||
// of unary/streaming requests/responses.
|
||||
//
|
||||
//
|
||||
// ******************* DO NOT EDIT
|
||||
//
|
||||
// This is copy of the following:
|
||||
// github.com/grpc/grpc-java/blob/master/interop-testing/src/main/proto/grpc/testing/test.proto
|
||||
//
|
||||
// *********************
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
import "grpc/testing/empty.proto";
|
||||
import "grpc/testing/messages.proto";
|
||||
|
||||
package grpc.testing;
|
||||
|
||||
option java_package = "grpc.testing";
|
||||
|
||||
// A simple service to test the various types of RPCs and experiment with
|
||||
// performance with various types of payload.
|
||||
service TestService {
|
||||
// One empty request followed by one empty response.
|
||||
rpc EmptyCall(grpc.testing.Empty) returns (grpc.testing.Empty);
|
||||
|
||||
// One request followed by one response.
|
||||
rpc UnaryCall(SimpleRequest) returns (SimpleResponse);
|
||||
|
||||
// One request followed by one response. Response has cache control
|
||||
// headers set such that a caching HTTP proxy (such as GFE) can
|
||||
// satisfy subsequent requests.
|
||||
rpc CacheableUnaryCall(SimpleRequest) returns (SimpleResponse);
|
||||
|
||||
// One request followed by a sequence of responses (streamed download).
|
||||
// The server returns the payload with client desired type and sizes.
|
||||
rpc StreamingOutputCall(StreamingOutputCallRequest)
|
||||
returns (stream StreamingOutputCallResponse);
|
||||
|
||||
// A sequence of requests followed by one response (streamed upload).
|
||||
// The server returns the aggregated size of client payload as the result.
|
||||
rpc StreamingInputCall(stream StreamingInputCallRequest)
|
||||
returns (StreamingInputCallResponse);
|
||||
|
||||
// A sequence of requests with each request served by the server immediately.
|
||||
// As one request could lead to multiple responses, this interface
|
||||
// demonstrates the idea of full duplexing.
|
||||
rpc FullDuplexCall(stream StreamingOutputCallRequest)
|
||||
returns (stream StreamingOutputCallResponse);
|
||||
|
||||
// A sequence of requests followed by a sequence of responses.
|
||||
// The server buffers all the client requests and then serves them in order. A
|
||||
// stream of responses are returned to the client when the server starts with
|
||||
// first request.
|
||||
rpc HalfDuplexCall(stream StreamingOutputCallRequest)
|
||||
returns (stream StreamingOutputCallResponse);
|
||||
|
||||
// The test server will not implement this method. It will be used
|
||||
// to test the behavior when clients call unimplemented methods.
|
||||
rpc UnimplementedCall(grpc.testing.Empty) returns (grpc.testing.Empty);
|
||||
}
|
||||
|
||||
// A simple service NOT implemented at servers so clients can test for
|
||||
// that case.
|
||||
service UnimplementedService {
|
||||
// A call that no server should implement
|
||||
rpc UnimplementedCall(grpc.testing.Empty) returns (grpc.testing.Empty);
|
||||
}
|
||||
|
||||
// A service used to control reconnect server.
|
||||
service ReconnectService {
|
||||
rpc Start(grpc.testing.Empty) returns (grpc.testing.Empty);
|
||||
rpc Stop(grpc.testing.Empty) returns (grpc.testing.ReconnectInfo);
|
||||
}
|
||||
|
||||
// A service used to obtain stats for verifying LB behavior.
|
||||
service LoadBalancerStatsService {
|
||||
// Gets the backend distribution for RPCs sent by a test client.
|
||||
rpc GetClientStats(LoadBalancerStatsRequest)
|
||||
returns (LoadBalancerStatsResponse) {}
|
||||
}
|
|
@ -210,8 +210,8 @@ the `client.js` files.
|
|||
"google-protobuf": "~3.14.0",
|
||||
"grpc-web": "~1.4.2",
|
||||
"lodash": "~4.17.0",
|
||||
"webpack": "~4.43.0",
|
||||
"webpack-cli": "~3.3.11"
|
||||
"webpack": "~5.82.1",
|
||||
"webpack-cli": "~5.1.1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
"google-protobuf": "~3.14.0",
|
||||
"grpc-web": "~1.4.2",
|
||||
"lodash": "~4.17.0",
|
||||
"webpack": "~4.43.0",
|
||||
"webpack-cli": "~3.3.11"
|
||||
"webpack": "~5.82.1",
|
||||
"webpack-cli": "~5.1.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
FROM selenium/standalone-chrome:93.0.4577.63
|
||||
|
||||
# Matching the node version used in the node:12.22.6-stretch image.
|
||||
ARG NODE_VERSION=12.22.6
|
||||
# Matching the node version used in the node:20.0.0-bullseye image.
|
||||
ARG NODE_VERSION=20.0.0
|
||||
|
||||
USER root
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ cd "$REPO_DIR"
|
|||
mkdir -p "$GEN_DIR"
|
||||
|
||||
echo "Generating dependency file..."
|
||||
$(npm bin)/closure-make-deps \
|
||||
npx closure-make-deps \
|
||||
--closure-path="node_modules/google-closure-library/closure/goog" \
|
||||
--file="node_modules/google-closure-library/closure/goog/deps.js" \
|
||||
--root="$JAVASCRIPT_DIR" \
|
||||
|
|
|
@ -27,7 +27,6 @@
|
|||
set -e
|
||||
|
||||
cd "$(dirname $(dirname "$0"))"
|
||||
NPM_BIN_PATH=$(npm bin)
|
||||
PROTRACTOR_BIN_PATH="./node_modules/protractor/bin"
|
||||
|
||||
function cleanup () {
|
||||
|
@ -37,7 +36,7 @@ function cleanup () {
|
|||
|
||||
# Start the local webserver.
|
||||
echo "Starting local HTP Server..."
|
||||
$NPM_BIN_PATH/gulp serve &
|
||||
npx gulp serve &
|
||||
serverPid=$!
|
||||
echo "Local HTTP Server started with PID $serverPid."
|
||||
|
||||
|
|
|
@ -25,7 +25,6 @@ REPO_DIR=$(realpath "${SCRIPT_DIR}/..")
|
|||
# Set up
|
||||
cd "${REPO_DIR}"
|
||||
|
||||
|
||||
# These programs need to be already installed
|
||||
progs=(docker docker-compose npm)
|
||||
for p in "${progs[@]}"
|
||||
|
@ -38,18 +37,17 @@ function cleanup () {
|
|||
echo "Killing lingering Docker servers..."
|
||||
docker rm -f "$pid1"
|
||||
docker rm -f "$pid2"
|
||||
docker rm -f "$pid3"
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
# Build all relevant docker images. They should all build successfully.
|
||||
docker-compose build prereqs node-interop-server java-interop-server
|
||||
docker-compose build prereqs node-interop-server
|
||||
|
||||
##########################################################
|
||||
# Run interop tests (against Envoy)
|
||||
##########################################################
|
||||
echo -e "\n[Running] Interop test #1 - against Envoy"
|
||||
echo -e "\n[Running] Interop test (Envoy)"
|
||||
pid1=$(docker run -d \
|
||||
-v "$(pwd)"/test/interop/envoy.yaml:/etc/envoy/envoy.yaml:ro \
|
||||
--network=host envoyproxy/envoy:v1.22.0)
|
||||
|
@ -58,19 +56,4 @@ pid2=$(docker run -d --network=host grpcweb/node-interop-server)
|
|||
run_tests
|
||||
|
||||
docker rm -f "$pid1"
|
||||
docker rm -f "$pid2"
|
||||
|
||||
|
||||
##########################################################
|
||||
# Run interop tests (against grpc-web Java connector code)
|
||||
##########################################################
|
||||
echo -e "\n[Running] Interop test #2 - against Java interop server"
|
||||
pid3=$(docker run -d --network=host grpcweb/java-interop-server)
|
||||
|
||||
run_tests
|
||||
|
||||
docker rm -f "$pid3"
|
||||
|
||||
# Clean up
|
||||
git clean -f -d -x
|
||||
echo 'Completed'
|
||||
docker rm -f "$pid2"
|
|
@ -1,67 +0,0 @@
|
|||
(On Hold) Java gRPC-web in-process proxy
|
||||
====================================================
|
||||
|
||||
### _(Development on the Java in-process proxy is On Hold. No active development or bug fixes is being done at this moment.)_
|
||||
|
||||
### Background & Motivation
|
||||
This project enables gRPC-web support in a Java Service that currently
|
||||
serves only gRPC clients but not equipped to handle gRPC-web clients.
|
||||
This project provides a java jar file that can be added to the Java Service
|
||||
when it is deployed. There are minimal changes to be made to the Java Service
|
||||
before linking the jar file provided by this project and re-deploying the
|
||||
Java Service.
|
||||
|
||||
### How to use it in your Java Service
|
||||
Here are the steps needed to use this project to add gRPC-web client serving
|
||||
capability to your existing Java Service (that only serves gRPC clients but not
|
||||
gRPC-web clients)
|
||||
|
||||
Examine the code in the following
|
||||
[dir](../../net/grpc/gateway/examples/grpc-web-java/greeter-service)
|
||||
|
||||
- A Java Service specified by
|
||||
[this proto](../../net/grpc/gateway/examples/grpc-web-java/greeter-service/src/main/proto/greeter.proto) is implemented
|
||||
[here](../../net/grpc/gateway/examples/grpc-web-java/greeter-service/src/main/java/grpcweb/examples/greeter/GreeterService.java)
|
||||
|
||||
- [This](../../net/grpc/gateway/examples/grpc-web-java/greeter-service/src/main/java/grpcweb/examples/greeter/StartServiceAndGrpcwebProxy.java)
|
||||
code starts the above gRPC-Service and a gRPC-web in-process proxy
|
||||
provided by this project.
|
||||
|
||||
- Since you will already have implemented code to start your gRPC-Service,
|
||||
add code to start gRPC-web in-process proxy, as demonstrated by
|
||||
the above.
|
||||
|
||||
- The only critical piece of info needed by the gRPC-web in-process proxy is the port
|
||||
to listen on, for gRPC-web requests.
|
||||
[This file](../../net/grpc/gateway/examples/grpc-web-java/greeter-service/src/main/java/grpcweb/examples/greeter/Util.java)
|
||||
externalizes grpc-web port# thru a static member.
|
||||
|
||||
Rebuild your Service with the Java jar file provided by this project and re-deploy.
|
||||
|
||||
Voila! Your service can now service gRPC and gRPC-web clients!
|
||||
|
||||
### How to run the [Interop tests](https://github.com/grpc/grpc-web/blob/master/test/interop/README.md) with this code
|
||||
|
||||
- Install "grpc-web java" jar locally
|
||||
```shell script
|
||||
$ cd src/connector
|
||||
$ mvn install
|
||||
```
|
||||
- Bring up a Test Service with this code as "grpc-web in-process proxy"
|
||||
```shell script
|
||||
$ cd net/grpc/gateway/examples/grpc-web-java/interop-test-service
|
||||
$ mvn package
|
||||
$ java -jar target/interop-test-0.1-jar-with-dependencies.jar
|
||||
```
|
||||
- Run the interop tests from browser
|
||||
```shell script
|
||||
$ cd test/interop
|
||||
$ docker-compose up interop-client
|
||||
```
|
||||
Open browser at http://localhost:8081/index.html and
|
||||
check the console for messages like the following:
|
||||
```shell script
|
||||
EmptyUnary: passed
|
||||
LargeUnary: passed
|
||||
etc..
|
||||
```
|
|
@ -1,79 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>io.grpcweb</groupId>
|
||||
<artifactId>grpcweb-java</artifactId>
|
||||
<version>0.1-SNAPSHOT</version>
|
||||
<name>Java gRPC-web in-process proxy</name>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.compiler.source>1.7</maven.compiler.source>
|
||||
<maven.compiler.target>1.7</maven.compiler.target>
|
||||
<grpc.version>1.33.0</grpc.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-bom</artifactId>
|
||||
<version>${grpc.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-server</artifactId>
|
||||
<version>10.0.14</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-servlet</artifactId>
|
||||
<version>9.4.40.v20210413</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
<version>2.8.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>3.12.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-protobuf</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-stub</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.inject</groupId>
|
||||
<artifactId>guice</artifactId>
|
||||
<version>4.2.3</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.13.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.truth</groupId>
|
||||
<artifactId>truth</artifactId>
|
||||
<version>1.1.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
|
@ -1,74 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 Google LLC
|
||||
*
|
||||
* 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 io.grpcweb;
|
||||
|
||||
import java.io.IOException;
|
||||
import javax.servlet.Filter;
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.FilterConfig;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import com.google.common.net.HttpHeaders;
|
||||
|
||||
public class CorsFilter implements Filter {
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
this.handle((HttpServletRequest)request, (HttpServletResponse)response);
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private void handle(HttpServletRequest req, HttpServletResponse resp)
|
||||
throws IOException, ServletException {
|
||||
resp.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS,
|
||||
StringUtils.joinWith(",",
|
||||
"user-agent",
|
||||
"cache-control",
|
||||
"content-type",
|
||||
"content-transfer-encoding",
|
||||
"grpc-timeout",
|
||||
"keep-alive",
|
||||
"x-accept-content-transfer-encoding",
|
||||
"x-accept-response-streaming",
|
||||
"x-grpc-test-echo-initial",
|
||||
"x-grpc-test-echo-trailing-bin",
|
||||
"x-grpc-web",
|
||||
"x-user-agent"));
|
||||
resp.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN,
|
||||
req.getHeader("Origin"));
|
||||
resp.setHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS,
|
||||
"content-type,x-grpc-web");
|
||||
resp.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS,
|
||||
"OPTIONS,GET,POST,HEAD");
|
||||
resp.setHeader(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS,
|
||||
StringUtils.joinWith(",",
|
||||
"x-grpc-test-echo-initial",
|
||||
"x-grpc-test-echo-trailing-bin",
|
||||
"grpc-status",
|
||||
"grpc-message"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(FilterConfig arg0) throws ServletException {}
|
||||
|
||||
@Override
|
||||
public void destroy() {}
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 Google LLC
|
||||
*
|
||||
* 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 io.grpcweb;
|
||||
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.util.Enumeration;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
class DebugInfo {
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(MethodHandles.lookup().lookupClass().getName());
|
||||
|
||||
static void printRequest(HttpServletRequest req) {
|
||||
if (!LOG.isLoggable(Level.FINE)) return;
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
Enumeration<String> headerNames = req.getHeaderNames();
|
||||
while (headerNames.hasMoreElements()) {
|
||||
String headerName = headerNames.nextElement();
|
||||
Enumeration<String> headers = req.getHeaders(headerName);
|
||||
while (headers.hasMoreElements()) {
|
||||
String headerValue = headers.nextElement();
|
||||
sb.append("\n\t" + headerName + " = " + headerValue);
|
||||
}
|
||||
}
|
||||
sb.append("\n\t ContextPath: ").append(req.getContextPath());
|
||||
sb.append("\n\t LocalAddr: ").append(req.getLocalAddr());
|
||||
sb.append("\n\t LocalName: ").append(req.getLocalName());
|
||||
sb.append("\n\t LocalPort: ").append(req.getLocalPort());
|
||||
sb.append("\n\t PathInfo: ").append(req.getPathInfo());
|
||||
sb.append("\n\t PathTranslated: ").append(req.getPathTranslated());
|
||||
sb.append("\n\t Protocol: ").append(req.getProtocol());
|
||||
sb.append("\n\t RemoteAddr: ").append(req.getRemoteAddr());
|
||||
sb.append("\n\t RemoteHost: ").append(req.getRemoteHost());
|
||||
sb.append("\n\t RemotePort: ").append(req.getRemotePort());
|
||||
sb.append("\n\t RequestURI: ").append(req.getRequestURI());
|
||||
sb.append("\n\t RequestURL: ").append(req.getRequestURL());
|
||||
sb.append("\n\t Scheme: ").append(req.getScheme());
|
||||
sb.append("\n\t ServerName: ").append(req.getServerName());
|
||||
sb.append("\n\t ServerPort: ").append(req.getServerPort());
|
||||
sb.append("\n\t ServletPath: ").append(req.getServletPath());
|
||||
sb.append("\n\t Method: ").append(req.getMethod());
|
||||
LOG.fine(sb.toString());
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 Google LLC
|
||||
*
|
||||
* 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 io.grpcweb;
|
||||
|
||||
public class GrpcPortNumRelay {
|
||||
public static void setGrpcPortNum(int i) {
|
||||
// TODO This class & method names are wrong - involves more than just setting the portnum
|
||||
GrpcWebGuiceModule.init(i);
|
||||
}
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 Google LLC
|
||||
*
|
||||
* 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 io.grpcweb;
|
||||
|
||||
import com.google.inject.Singleton;
|
||||
import io.grpc.Channel;
|
||||
import io.grpc.ClientInterceptors;
|
||||
import io.grpc.ManagedChannel;
|
||||
import io.grpc.ManagedChannelBuilder;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* TODO: Manage the connection pool to talk to the grpc-service
|
||||
*/
|
||||
@Singleton
|
||||
class GrpcServiceConnectionManager {
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(MethodHandles.lookup().lookupClass().getName());
|
||||
private final ManagedChannel mChannel;
|
||||
|
||||
GrpcServiceConnectionManager(int grpcPortNum) {
|
||||
// TODO: Manage a connection pool.
|
||||
mChannel = ManagedChannelBuilder.forAddress("localhost", grpcPortNum)
|
||||
.usePlaintext()
|
||||
.build();
|
||||
LOG.info("**** connection channel initiated");
|
||||
}
|
||||
|
||||
Channel getChannelWithClientInterceptor(GrpcWebClientInterceptor interceptor) {
|
||||
return ClientInterceptors.intercept(mChannel, interceptor);
|
||||
}
|
||||
}
|
|
@ -1,82 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 Google LLC
|
||||
*
|
||||
* 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 io.grpcweb;
|
||||
|
||||
import io.grpc.CallOptions;
|
||||
import io.grpc.Channel;
|
||||
import io.grpc.ClientCall;
|
||||
import io.grpc.ClientCall.Listener;
|
||||
import io.grpc.ClientInterceptor;
|
||||
import io.grpc.ForwardingClientCall.SimpleForwardingClientCall;
|
||||
import io.grpc.ForwardingClientCallListener.SimpleForwardingClientCallListener;
|
||||
import io.grpc.Metadata;
|
||||
import io.grpc.MethodDescriptor;
|
||||
import io.grpc.Status;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
class GrpcWebClientInterceptor implements ClientInterceptor {
|
||||
|
||||
private final CountDownLatch mLatch;
|
||||
private final HttpServletResponse mResp;
|
||||
private final SendResponse mSendResponse;
|
||||
|
||||
GrpcWebClientInterceptor(HttpServletResponse resp, CountDownLatch latch, SendResponse send) {
|
||||
mLatch = latch;
|
||||
mResp = resp;
|
||||
mSendResponse = send;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(MethodDescriptor<ReqT, RespT> method,
|
||||
CallOptions callOptions, Channel channel) {
|
||||
return new SimpleForwardingClientCall<ReqT, RespT>(channel.newCall(method, callOptions)) {
|
||||
@Override
|
||||
public void start(Listener<RespT> responseListener, Metadata headers) {
|
||||
super.start(new MetadataResponseListener<RespT>(responseListener), headers);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class MetadataResponseListener<T> extends SimpleForwardingClientCallListener<T> {
|
||||
private boolean headersSent = false;
|
||||
|
||||
MetadataResponseListener(Listener<T> responseListener) {
|
||||
super(responseListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHeaders(Metadata h) {
|
||||
mSendResponse.writeHeaders(h);
|
||||
headersSent = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClose(Status s, Metadata t) {
|
||||
if (!headersSent) {
|
||||
// seems, sometimes onHeaders() is not called before this method is called!
|
||||
// so far, they are the error cases. let onError() method in ClientListener
|
||||
// handle this call. Could ignore this.
|
||||
// TODO is this correct? what if onError() never gets called?
|
||||
} else {
|
||||
mSendResponse.writeTrailer(s, t);
|
||||
mLatch.countDown();
|
||||
}
|
||||
super.onClose(s, t);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 Google LLC
|
||||
*
|
||||
* 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 io.grpcweb;
|
||||
|
||||
import com.google.inject.AbstractModule;
|
||||
import com.google.inject.Guice;
|
||||
import com.google.inject.Injector;
|
||||
|
||||
class GrpcWebGuiceModule extends AbstractModule {
|
||||
private static Injector sInjector;
|
||||
private static int sGrpcPortNum;
|
||||
|
||||
// This method should be called only once.
|
||||
static void init(int i) {
|
||||
sGrpcPortNum = i;
|
||||
sInjector = Guice.createInjector(new GrpcWebGuiceModule());
|
||||
}
|
||||
|
||||
static Injector getInjector() {
|
||||
return sInjector;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
bind(GrpcServiceConnectionManager.class)
|
||||
.toInstance(new GrpcServiceConnectionManager(sGrpcPortNum));
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 Google LLC
|
||||
*
|
||||
* 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 io.grpcweb;
|
||||
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* The main class that handles all the grpc-web traffic.
|
||||
*/
|
||||
public class GrpcWebTrafficServlet extends HttpServlet {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest request, HttpServletResponse response) {
|
||||
RequestHandler reqHandler = GrpcWebGuiceModule.getInjector().getInstance(RequestHandler.class);
|
||||
reqHandler.handle(request, response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doPost(HttpServletRequest request, HttpServletResponse response) {
|
||||
doGet(request, response);
|
||||
}
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 Google LLC
|
||||
*
|
||||
* 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 io.grpcweb;
|
||||
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.util.EnumSet;
|
||||
import java.util.logging.Logger;
|
||||
import javax.servlet.DispatcherType;
|
||||
import org.eclipse.jetty.servlet.ServletHandler;
|
||||
|
||||
public class JettyWebserverForGrpcwebTraffic {
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(MethodHandles.lookup().lookupClass().getName());
|
||||
|
||||
private final int mGrpcwebPort;
|
||||
|
||||
public JettyWebserverForGrpcwebTraffic(int grpcwebPort) {
|
||||
mGrpcwebPort = grpcwebPort;
|
||||
}
|
||||
|
||||
public org.eclipse.jetty.server.Server start() {
|
||||
// Start a jetty server to listen on the grpc-web port#
|
||||
org.eclipse.jetty.server.Server jServer = new org.eclipse.jetty.server.Server(mGrpcwebPort);
|
||||
ServletHandler handler = new ServletHandler();
|
||||
jServer.setHandler(handler);
|
||||
handler.addFilterWithMapping(CorsFilter.class, "/*",
|
||||
EnumSet.of(DispatcherType.REQUEST));
|
||||
handler.addServletWithMapping(GrpcWebTrafficServlet.class, "/*");
|
||||
try {
|
||||
jServer.start();
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Jetty Server couldn't be started. " + e.getLocalizedMessage());
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
LOG.info("**** started gRPC-web Service on port# " + mGrpcwebPort);
|
||||
jServer.setStopAtShutdown(true);
|
||||
return jServer;
|
||||
}
|
||||
}
|
|
@ -1,126 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 Google LLC
|
||||
*
|
||||
* 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 io.grpcweb;
|
||||
|
||||
import io.grpcweb.MessageHandler.ContentType;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.logging.Logger;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
|
||||
/**
|
||||
* Reads frames from the input bytes and returns a single message.
|
||||
*/
|
||||
class MessageDeframer {
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(MethodHandles.lookup().lookupClass().getName());
|
||||
|
||||
static final byte DATA_BYTE = (byte) 0x00;
|
||||
|
||||
// TODO: fix this code to be able to handle upto 4GB input size.
|
||||
private int mLength = 0;
|
||||
private int mReadSoFar = 0;
|
||||
|
||||
private ArrayList<byte[]> mFrames = new ArrayList<>();
|
||||
private byte[] mMsg = null;
|
||||
private int mNumFrames;
|
||||
|
||||
byte[] getMessageBytes() { return mMsg;}
|
||||
int getLength() { return mLength;}
|
||||
int getNumberOfFrames() {return mNumFrames;}
|
||||
|
||||
/** Reads the bytes from the given InputStream and populates bytes in {@link #mMsg}
|
||||
*/
|
||||
boolean processInput(InputStream in, MessageHandler.ContentType contentType) {
|
||||
byte[] inBytes;
|
||||
try {
|
||||
InputStream inStream = (contentType == ContentType.GRPC_WEB_TEXT)
|
||||
? Base64.getDecoder().wrap(in)
|
||||
: in;
|
||||
inBytes = IOUtils.toByteArray(inStream);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
LOG.warning("invalid input");
|
||||
return false;
|
||||
}
|
||||
if (inBytes.length < 5) {
|
||||
LOG.fine("invalid input. Expected minimum of 5 bytes");
|
||||
return false;
|
||||
}
|
||||
|
||||
while (getNextFrameBytes(inBytes)) {}
|
||||
mNumFrames = mFrames.size();
|
||||
|
||||
// common case is only one frame.
|
||||
if (mNumFrames == 1) {
|
||||
mMsg = mFrames.get(0);
|
||||
} else {
|
||||
// concatenate all frames into one byte array
|
||||
// TODO: this is inefficient.
|
||||
mMsg = new byte[mLength];
|
||||
int offset = 0;
|
||||
for (byte[] f : mFrames) {
|
||||
System.arraycopy(f, 0, mMsg, offset, f.length);
|
||||
offset += f.length;
|
||||
}
|
||||
mFrames = null;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** returns true if the next frame is a DATA frame */
|
||||
private boolean getNextFrameBytes(byte[] inBytes) {
|
||||
// Firstbyte should be 0x00 (for this to be a DATA frame)
|
||||
int firstByteValue = inBytes[mReadSoFar] | DATA_BYTE;
|
||||
if (firstByteValue != 0) {
|
||||
LOG.fine("done with DATA bytes");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Next 4 bytes = length of the bytes array starting after the 4 bytes.
|
||||
int offset = mReadSoFar + 1;
|
||||
int len = ByteBuffer.wrap(inBytes, offset, 4).getInt();
|
||||
|
||||
// Empty message is special case.
|
||||
// TODO: Can this is special handling be removed?
|
||||
if (len == 0) {
|
||||
mFrames.add(new byte[0]);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Make sure we have enough bytes in the inputstream
|
||||
int expectedNumBytes = len + 5 + mReadSoFar;
|
||||
if (inBytes.length < expectedNumBytes) {
|
||||
LOG.warning(String.format("input doesn't have enough bytes. expected: %d, found %d",
|
||||
expectedNumBytes, inBytes.length));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read "len" bytes into message
|
||||
mLength += len;
|
||||
offset += 4;
|
||||
byte[] inputBytes = Arrays.copyOfRange(inBytes, offset, len + offset);
|
||||
mFrames.add(inputBytes);
|
||||
mReadSoFar += (len + 5);
|
||||
// we have more frames to process, if there are bytes unprocessed
|
||||
return inBytes.length > mReadSoFar;
|
||||
}
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 Google LLC
|
||||
*
|
||||
* 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 io.grpcweb;
|
||||
|
||||
/**
|
||||
* Creates frames from the input bytes.
|
||||
*/
|
||||
class MessageFramer {
|
||||
enum Type {
|
||||
DATA ((byte) 0x00),
|
||||
TRAILER ((byte) 0x80);
|
||||
|
||||
public final byte value;
|
||||
Type(byte b) {
|
||||
value = b;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: handle more than single frame; i.e., input byte array size > (2GB - 1)
|
||||
byte[] getPrefix(byte[] in, Type type) {
|
||||
int len = in.length;
|
||||
return new byte[] {
|
||||
type.value,
|
||||
(byte) ((len >> 24) & 0xff),
|
||||
(byte) ((len >> 16) & 0xff),
|
||||
(byte) ((len >> 8) & 0xff),
|
||||
(byte) ((len >> 0) & 0xff),
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,86 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 Google LLC
|
||||
*
|
||||
* 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 io.grpcweb;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
class MessageHandler {
|
||||
@VisibleForTesting
|
||||
enum ContentType {
|
||||
GRPC_WEB_BINARY,
|
||||
GRPC_WEB_TEXT;
|
||||
}
|
||||
|
||||
private static Map<String, ContentType> GRPC_GCP_CONTENT_TYPES =
|
||||
new HashMap<String, ContentType>() {{
|
||||
put("application/grpc-web", ContentType.GRPC_WEB_BINARY);
|
||||
put("application/grpc-web+proto", ContentType.GRPC_WEB_BINARY);
|
||||
put("application/grpc-web-text", ContentType.GRPC_WEB_TEXT);
|
||||
put("application/grpc-web-text+proto", ContentType.GRPC_WEB_TEXT);
|
||||
}};
|
||||
|
||||
/**
|
||||
* Validate the content-type
|
||||
*/
|
||||
ContentType validateContentType(HttpServletRequest req) throws IllegalArgumentException {
|
||||
String contentType = req.getContentType();
|
||||
if (contentType == null || !GRPC_GCP_CONTENT_TYPES.containsKey(contentType)) {
|
||||
throw new IllegalArgumentException("This content type is not used for grpc-web: "
|
||||
+ contentType);
|
||||
}
|
||||
return getContentType(contentType);
|
||||
}
|
||||
|
||||
static ContentType getContentType(String type) {
|
||||
return GRPC_GCP_CONTENT_TYPES.get(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the input arg protobuf class for the given rpc-method.
|
||||
* Convert the given bytes to the input protobuf. return that.
|
||||
*/
|
||||
Object getInputProtobufObj(Method rpcMethod, byte[] in) {
|
||||
Class[] inputArgs = rpcMethod.getParameterTypes();
|
||||
Class inputArgClass = inputArgs[0];
|
||||
|
||||
// use the inputArg classtype to create a protobuf object
|
||||
Method parseFromObj;
|
||||
try {
|
||||
parseFromObj = inputArgClass.getMethod("parseFrom", byte[].class);
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new IllegalArgumentException("Couldn't find method in 'parseFrom' in "
|
||||
+ inputArgClass.getName());
|
||||
}
|
||||
|
||||
Object inputObj;
|
||||
try {
|
||||
inputObj = parseFromObj.invoke(null, in);
|
||||
} catch (InvocationTargetException | IllegalAccessException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
|
||||
if (inputObj == null || !inputArgClass.isInstance(inputObj)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Input obj is **not** instance of the correct input class type");
|
||||
}
|
||||
return inputObj;
|
||||
}
|
||||
}
|
|
@ -1,87 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 Google LLC
|
||||
*
|
||||
* 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 io.grpcweb;
|
||||
|
||||
import io.grpc.Metadata;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
class MetadataUtil {
|
||||
private static final String BINARY_HEADER_SUFFIX = "-bin";
|
||||
private static final String GRPC_HEADER_PREFIX = "x-grpc-";
|
||||
private static final List<String> EXCLUDED = Arrays.asList("x-grpc-web", "content-type",
|
||||
"grpc-accept-encoding", "grpc-encoding");
|
||||
|
||||
static Metadata getHtpHeaders(HttpServletRequest req) {
|
||||
Metadata httpHeaders = new Metadata();
|
||||
Enumeration<String> headerNames = req.getHeaderNames();
|
||||
if (headerNames == null) {
|
||||
return httpHeaders;
|
||||
}
|
||||
// copy all headers "x-grpc-*" into Metadata
|
||||
// TODO: do we need to copy all "x-*" headers instead?
|
||||
while (headerNames.hasMoreElements()) {
|
||||
String headerName = headerNames.nextElement();
|
||||
if (EXCLUDED.contains(headerName.toLowerCase())) {
|
||||
continue;
|
||||
}
|
||||
if (headerName.toLowerCase().startsWith(GRPC_HEADER_PREFIX)) {
|
||||
// Get all the values of this header.
|
||||
Enumeration<String> values = req.getHeaders(headerName);
|
||||
if (values != null) {
|
||||
// Java enumerations have klunky API. lets convert to a list.
|
||||
// this will be a short list usually.
|
||||
List<String> list = Collections.list(values);
|
||||
for (String s : list) {
|
||||
if (headerName.toLowerCase().endsWith(BINARY_HEADER_SUFFIX)) {
|
||||
// Binary header
|
||||
httpHeaders.put(
|
||||
Metadata.Key.of(headerName, Metadata.BINARY_BYTE_MARSHALLER), s.getBytes());
|
||||
} else {
|
||||
// String header
|
||||
httpHeaders.put(
|
||||
Metadata.Key.of(headerName, Metadata.ASCII_STRING_MARSHALLER), s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return httpHeaders;
|
||||
}
|
||||
|
||||
static Map<String, String> getHttpHeadersFromMetadata(Metadata trailer) {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
for (String key : trailer.keys()) {
|
||||
if (EXCLUDED.contains(key.toLowerCase())) {
|
||||
continue;
|
||||
}
|
||||
if (key.endsWith(Metadata.BINARY_HEADER_SUFFIX)) {
|
||||
// TODO allow any object type here
|
||||
byte[] value = trailer.get(Metadata.Key.of(key, Metadata.BINARY_BYTE_MARSHALLER));
|
||||
map.put(key, new String(value));
|
||||
} else {
|
||||
String value = trailer.get(Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER));
|
||||
map.put(key, value);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
|
@ -1,177 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 Google LLC
|
||||
*
|
||||
* 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 io.grpcweb;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import io.grpc.Channel;
|
||||
import io.grpc.Metadata;
|
||||
import io.grpc.Status;
|
||||
import io.grpc.stub.MetadataUtils;
|
||||
import io.grpc.stub.StreamObserver;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Logger;
|
||||
import javax.servlet.ServletInputStream;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
||||
class RequestHandler {
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(MethodHandles.lookup().lookupClass().getName());
|
||||
|
||||
private final MessageHandler mMessageHandler;
|
||||
private final GrpcServiceConnectionManager mGrpcServiceConnectionManager;
|
||||
|
||||
@Inject
|
||||
RequestHandler(GrpcServiceConnectionManager g, MessageHandler m) {
|
||||
mMessageHandler = m;
|
||||
mGrpcServiceConnectionManager = g;
|
||||
}
|
||||
|
||||
public void handle(final HttpServletRequest req, final HttpServletResponse resp) {
|
||||
DebugInfo.printRequest(req);
|
||||
MessageHandler.ContentType contentType = mMessageHandler.validateContentType(req);
|
||||
SendResponse sendResponse = new SendResponse(req, resp);
|
||||
|
||||
try {
|
||||
// From the request, get the rpc-method name and class name and then get their corresponding
|
||||
// concrete objects.
|
||||
Pair<String, String> classAndMethodNames = getClassAndMethod(req);
|
||||
String className = classAndMethodNames.getLeft();
|
||||
String methodName = classAndMethodNames.getRight();
|
||||
Class cls = getClassObject(className);
|
||||
if (cls == null) {
|
||||
LOG.info("incorrect classname in the request: " + className);
|
||||
// incorrect classname specified in the request.
|
||||
sendResponse.returnUnimplementedStatusCode();
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a ClientInterceptor object
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
GrpcWebClientInterceptor interceptor =
|
||||
new GrpcWebClientInterceptor(resp, latch, sendResponse);
|
||||
Channel channel = mGrpcServiceConnectionManager.getChannelWithClientInterceptor(interceptor);
|
||||
|
||||
// get the stub for the rpc call and the method to be called within the stub
|
||||
io.grpc.stub.AbstractStub asyncStub = getRpcStub(channel, cls, "newStub");
|
||||
Metadata headers = MetadataUtil.getHtpHeaders(req);
|
||||
if (!headers.keys().isEmpty()) {
|
||||
asyncStub = MetadataUtils.attachHeaders(asyncStub, headers);
|
||||
}
|
||||
Method asyncStubCall = getRpcMethod(asyncStub, methodName);
|
||||
// Get the input object bytes
|
||||
ServletInputStream in = req.getInputStream();
|
||||
MessageDeframer deframer = new MessageDeframer();
|
||||
Object inObj = null;
|
||||
if (deframer.processInput(in, contentType)) {
|
||||
inObj = mMessageHandler.getInputProtobufObj(asyncStubCall, deframer.getMessageBytes());
|
||||
}
|
||||
|
||||
// Invoke the rpc call
|
||||
asyncStubCall.invoke(asyncStub, inObj,
|
||||
new GrpcCallResponseReceiver(sendResponse, latch));
|
||||
if (!latch.await(500, TimeUnit.MILLISECONDS)) {
|
||||
LOG.warning("grpc call took too long!");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.info("Exception occurred: " + e.getMessage());
|
||||
resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
private Pair<String, String> getClassAndMethod(HttpServletRequest req)
|
||||
throws IllegalArgumentException {
|
||||
String pathInfo = req.getPathInfo();
|
||||
// pathInfo starts with "/". ignore that first char.
|
||||
String[] rpcClassAndMethodTokens = pathInfo.substring(1).split("/");
|
||||
if (rpcClassAndMethodTokens.length != 2) {
|
||||
throw new IllegalArgumentException("incorrect pathinfo: " + pathInfo);
|
||||
}
|
||||
|
||||
String rpcClassName = rpcClassAndMethodTokens[0];
|
||||
String rpcMethodNameRecvd = rpcClassAndMethodTokens[1];
|
||||
String rpcMethodName = rpcMethodNameRecvd.substring(0, 1).toLowerCase()
|
||||
+ rpcMethodNameRecvd.substring(1);
|
||||
return new ImmutablePair<>(rpcClassName, rpcMethodName);
|
||||
}
|
||||
|
||||
private Class<?> getClassObject(String className) {
|
||||
Class rpcClass = null;
|
||||
try {
|
||||
rpcClass = Class.forName(className + "Grpc");
|
||||
} catch (ClassNotFoundException e) {
|
||||
LOG.info("no such class " + className);
|
||||
}
|
||||
return rpcClass;
|
||||
}
|
||||
|
||||
private io.grpc.stub.AbstractStub getRpcStub(Channel ch, Class cls, String stubName) {
|
||||
try {
|
||||
Method m = cls.getDeclaredMethod(stubName, io.grpc.Channel.class);
|
||||
return (io.grpc.stub.AbstractStub) m.invoke(null, ch);
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Error when fetching " + stubName + " for: " + cls.getName());
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the matching method in the stub class.
|
||||
*/
|
||||
private Method getRpcMethod(Object stub, String rpcMethodName) {
|
||||
for (Method m : stub.getClass().getMethods()) {
|
||||
if (m.getName().equals(rpcMethodName)) {
|
||||
return m;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Couldn't find rpcmethod: " + rpcMethodName);
|
||||
}
|
||||
|
||||
private static class GrpcCallResponseReceiver<Object> implements StreamObserver {
|
||||
private final SendResponse sendResponse;
|
||||
private final CountDownLatch latch;
|
||||
|
||||
GrpcCallResponseReceiver(SendResponse s, CountDownLatch c) {
|
||||
sendResponse = s;
|
||||
latch = c;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(java.lang.Object resp) {
|
||||
// TODO verify that the resp object is of Class instance returnedCls.
|
||||
byte[] outB = ((com.google.protobuf.GeneratedMessageV3)resp).toByteArray();
|
||||
sendResponse.writeResponse(outB);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable t) {
|
||||
Status s = Status.fromThrowable(t);
|
||||
sendResponse.writeError(s);
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleted() {
|
||||
sendResponse.writeStatusTrailer(Status.OK);
|
||||
latch.countDown();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,122 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 Google LLC
|
||||
*
|
||||
* 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 io.grpcweb;
|
||||
|
||||
import io.grpc.Metadata;
|
||||
import io.grpc.Status;
|
||||
import io.grpcweb.MessageHandler.ContentType;
|
||||
import java.io.IOException;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Logger;
|
||||
import javax.servlet.ServletOutputStream;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.Base64;
|
||||
|
||||
class SendResponse {
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(MethodHandles.lookup().lookupClass().getName());
|
||||
|
||||
private final String mContentType;
|
||||
private final HttpServletResponse mResp;
|
||||
private boolean isFinalResponseSent = false;
|
||||
|
||||
SendResponse(HttpServletRequest req, HttpServletResponse resp) {
|
||||
mContentType = req.getContentType();
|
||||
mResp = resp;
|
||||
}
|
||||
|
||||
synchronized void writeHeaders(Metadata headers) {
|
||||
if (isFinalResponseSent) return;
|
||||
mResp.setContentType(mContentType);
|
||||
mResp.setHeader("transfer-encoding", "chunked");
|
||||
mResp.setHeader("trailer", "grpc-status,grpc-message");
|
||||
if (headers == null) return;
|
||||
Map<String, String> ht = MetadataUtil.getHttpHeadersFromMetadata(headers);
|
||||
StringBuffer sb = new StringBuffer();
|
||||
for (String key : ht.keySet()) {
|
||||
mResp.setHeader(key, ht.get(key));
|
||||
}
|
||||
}
|
||||
|
||||
synchronized void returnUnimplementedStatusCode() {
|
||||
if (isFinalResponseSent) return;
|
||||
writeHeaders(null);
|
||||
writeStatusTrailer(Status.UNIMPLEMENTED);
|
||||
isFinalResponseSent = true;
|
||||
}
|
||||
|
||||
synchronized void writeError(Status s) {
|
||||
if (isFinalResponseSent) return;
|
||||
writeHeaders(null);
|
||||
writeStatusTrailer(s);
|
||||
isFinalResponseSent = true;
|
||||
}
|
||||
|
||||
synchronized void writeStatusTrailer(Status status) {
|
||||
writeTrailer(status, null);
|
||||
}
|
||||
|
||||
synchronized void writeTrailer(Status status, Metadata trailer) {
|
||||
if (isFinalResponseSent) return;
|
||||
StringBuffer sb = new StringBuffer();
|
||||
if (trailer != null) {
|
||||
Map<String, String> ht = MetadataUtil.getHttpHeadersFromMetadata(trailer);
|
||||
for (String key : ht.keySet()) {
|
||||
sb.append(String.format("%s:%s\r\n", key, ht.get(key)));
|
||||
}
|
||||
}
|
||||
sb.append(String.format("grpc-status:%d\r\n", status.getCode().value()));
|
||||
if (status.getDescription() != null && !status.getDescription().isEmpty()) {
|
||||
sb.append(String.format("grpc-message:%s\r\n", status.getDescription()));
|
||||
}
|
||||
LOG.fine("writing trailer: " + sb.toString());
|
||||
writeResponse(sb.toString().getBytes(), MessageFramer.Type.TRAILER);
|
||||
writeOk();
|
||||
}
|
||||
|
||||
synchronized void writeResponse(byte[] out) {
|
||||
writeResponse(out, MessageFramer.Type.DATA);
|
||||
}
|
||||
|
||||
private void writeResponse(byte[] out, MessageFramer.Type type) {
|
||||
if (isFinalResponseSent) return;
|
||||
try {
|
||||
// PUNT multiple frames not handled
|
||||
byte[] prefix = new MessageFramer().getPrefix(out, type);
|
||||
ServletOutputStream oStream = mResp.getOutputStream();
|
||||
// binary encode if it is "text" content type
|
||||
if (MessageHandler.getContentType(mContentType) == ContentType.GRPC_WEB_TEXT) {
|
||||
byte[] concated = new byte[out.length + 5];
|
||||
System.arraycopy(prefix, 0, concated, 0, 5);
|
||||
System.arraycopy(out, 0, concated, 5, out.length);
|
||||
oStream.write(Base64.getEncoder().encode(concated));
|
||||
} else {
|
||||
oStream.write(prefix);
|
||||
oStream.write(out);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// TODO what to do here?
|
||||
LOG.warning("can't write?");
|
||||
}
|
||||
}
|
||||
|
||||
private void writeOk() {
|
||||
mResp.setStatus(HttpServletResponse.SC_OK);
|
||||
isFinalResponseSent = true;
|
||||
}
|
||||
}
|
|
@ -1,134 +0,0 @@
|
|||
package io.grpcweb;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import io.grpcweb.MessageHandler.ContentType;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
@RunWith(JUnit4.class)
|
||||
public class MessageDeframerTest {
|
||||
|
||||
private MessageDeframer testInstance;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
testInstance = new MessageDeframer();
|
||||
assertNotNull(testInstance);
|
||||
}
|
||||
|
||||
private void singleFrameTest(boolean encode) throws IOException {
|
||||
String source = "This is the source of my input stream";
|
||||
byte[] str = stringToFrame(source, encode);
|
||||
InputStream in = new ByteArrayInputStream(str);
|
||||
assertTrue(testInstance.processInput(in, getContentType(encode)));
|
||||
byte[] result = testInstance.getMessageBytes();
|
||||
assertTrue(source.equals(new String(result)));
|
||||
assertEquals(source.length(), testInstance.getLength());
|
||||
assertEquals(1, testInstance.getNumberOfFrames());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProcessInput_Singleframe() throws IOException {
|
||||
singleFrameTest(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProcessInput_Singleframe_Base64Encoded() throws IOException {
|
||||
singleFrameTest(true);
|
||||
}
|
||||
|
||||
private void manyFramesTest(boolean encode) throws IOException {
|
||||
ArrayList<String> inputSrcs = new ArrayList<>();
|
||||
// Create 10 frames
|
||||
int numFrames = 10;
|
||||
for (int i = 0; i < numFrames; i++) {
|
||||
inputSrcs.add("this is string# " + i);
|
||||
}
|
||||
ByteArrayOutputStream combined = new ByteArrayOutputStream();
|
||||
String concatenatedInputSrc = "";
|
||||
// create 10 frames first and then Base64 Encode the whole string.
|
||||
for (String s : inputSrcs) {
|
||||
combined.write(stringToFrame(s, false));
|
||||
concatenatedInputSrc += s;
|
||||
}
|
||||
byte[] combinedBytesOrig = combined.toByteArray();
|
||||
byte[] combinedBytes = encode ? Base64.getEncoder().encode(combinedBytesOrig)
|
||||
: combinedBytesOrig;
|
||||
if (!encode) {
|
||||
assertEquals(concatenatedInputSrc.length() + inputSrcs.size() * 5,
|
||||
combinedBytes.length);
|
||||
}
|
||||
InputStream in = new ByteArrayInputStream(combinedBytes);
|
||||
assertTrue(testInstance.processInput(in, getContentType(encode)));
|
||||
byte[] result = testInstance.getMessageBytes();
|
||||
assertTrue(concatenatedInputSrc.equals(new String(result)));
|
||||
assertEquals(concatenatedInputSrc.length(), testInstance.getLength());
|
||||
assertEquals(10, testInstance.getNumberOfFrames());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProcessInput_Manyframes() throws IOException {
|
||||
manyFramesTest(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProcessInput_Manyframes_Base64Encoded() throws IOException {
|
||||
manyFramesTest(true);
|
||||
}
|
||||
|
||||
public void emptyFrameTest(boolean encode) throws IOException {
|
||||
// Empty frame is a valid frame.
|
||||
byte[] str = stringToFrame("", false);
|
||||
InputStream in = new ByteArrayInputStream(str);
|
||||
assertTrue(testInstance.processInput(in, getContentType(encode)));
|
||||
assertEquals(0, testInstance.getMessageBytes().length);
|
||||
assertEquals(1, testInstance.getNumberOfFrames());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProcessInput_EmptyDataframeInInput() throws IOException {
|
||||
emptyFrameTest(false);
|
||||
}
|
||||
|
||||
public void noDataTest(boolean encode) throws IOException {
|
||||
// Input has no data frames at all
|
||||
InputStream in = new ByteArrayInputStream(new byte[0]);
|
||||
assertFalse(testInstance.processInput(in, getContentType(encode)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProcessInput_NoDataframesInInput() throws IOException {
|
||||
noDataTest(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProcessInput_NoDataframesInInput_Base64Encoded() throws IOException {
|
||||
noDataTest(true);
|
||||
}
|
||||
|
||||
private byte[] stringToFrame(String source, boolean encode) throws IOException {
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
output.write(MessageDeframer.DATA_BYTE);
|
||||
output.write(ByteBuffer.allocate(4).putInt(source.length()).array());
|
||||
output.write(source.getBytes());
|
||||
byte[] outB = output.toByteArray();
|
||||
return (encode) ? Base64.getEncoder().encode(outB) : outB;
|
||||
}
|
||||
|
||||
private MessageHandler.ContentType getContentType(boolean encode) {
|
||||
return encode ? ContentType.GRPC_WEB_TEXT : ContentType.GRPC_WEB_BINARY;
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
package io.grpcweb;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
@RunWith(JUnit4.class)
|
||||
public class MessageFramerTest {
|
||||
|
||||
private MessageFramer testInstance;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
testInstance = new MessageFramer();
|
||||
assertNotNull(testInstance);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProcessInput_Singleframe() throws IOException {
|
||||
String source = "This is the source of my input stream";
|
||||
byte[] bytes = source.getBytes();
|
||||
byte[] prefix = testInstance.getPrefix(bytes, MessageFramer.Type.DATA);
|
||||
assertEquals(5, prefix.length);
|
||||
int len = ByteBuffer.wrap(prefix, 1, 4).getInt();
|
||||
assertEquals(source.length(), len);
|
||||
}
|
||||
// PUNT add more tests: Empty frame, zero frames
|
||||
}
|
|
@ -13,8 +13,8 @@
|
|||
"devDependencies": {
|
||||
"minimist": "~1.2.5",
|
||||
"mocha": "~7.1.1",
|
||||
"webpack": "~4.43.0",
|
||||
"webpack-cli": "~3.3.11",
|
||||
"webpack": "~5.82.1",
|
||||
"webpack-cli": "~5.1.1",
|
||||
"xhr2": "~0.2.0"
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue