Update Debian (and other deps) and remove Java In-process Proxy (#1335)

This commit is contained in:
Eryu Xia 2023-05-14 02:57:11 -07:00 committed by GitHub
parent c5c1e29756
commit f8fbbe32f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 25 additions and 2931 deletions

View File

@ -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/)

View File

@ -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: ./

View File

@ -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"]

View File

@ -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

View File

@ -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

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}
```

View File

@ -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**

View File

@ -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>

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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");
}
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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.

View File

@ -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>

View File

@ -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);
}
};
}
}

View File

@ -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();
}
}

View File

@ -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");
}
}

View File

@ -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 {}

View File

@ -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;
}

View File

@ -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) {}
}

View File

@ -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"
}
}
```

View File

@ -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"
}
}

View File

@ -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

View File

@ -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" \

View File

@ -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."

View File

@ -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"

View File

@ -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..
```

View File

@ -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>

View File

@ -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() {}
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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));
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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),
};
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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
}

View File

@ -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"
}
}