Fix for duplicate headers (#397)

This commit is contained in:
Vyacheslav Egorov 2020-11-12 09:51:57 +01:00 committed by GitHub
parent b8197a5897
commit e634f9749f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 168 additions and 118 deletions

View File

@ -65,11 +65,16 @@ jobs:
release-channel: ${{ matrix.sdk }}
- name: Report version
run: dart --version
- name: Install grpcwebproxy
- name: Install envoy
if: ${{ matrix.platform == 'chrome' }}
run: |
./tool/install-grpcwebproxy.sh
echo "/tmp/grpcwebproxy" >> $GITHUB_PATH
sudo apt update
sudo apt install apt-transport-https ca-certificates curl gnupg-agent software-properties-common
curl -sL 'https://getenvoy.io/gpg' | sudo apt-key add -
apt-key fingerprint 6FF974DB | grep "5270 CEAC 57F6 3EBD 9EA9 005D 0253 D0B2 6FF9 74DB"
sudo add-apt-repository "deb [arch=amd64] https://dl.bintray.com/tetrate/getenvoy-deb $(lsb_release -cs) stable"
sudo apt update
sudo apt install -y getenvoy-envoy
env:
MATRIX_OS: ${{ matrix.os }}
shell: bash

View File

@ -7,6 +7,8 @@
newer of protobuf compiler plugin.
* `Client.$createCall` is deprecated because it does not invoke client
interceptors.
* Fix an issue [#380](https://github.com/grpc/grpc-dart/issues/380) causing
incorrect duplicated headers in gRPC-Web requests.
* Change minimum required Dart SDK to 2.8 to enable access to Unix domain sockets.
* Add support for Unix domain sockets in `Socket.serve` and `ClientChannel`.

View File

@ -37,9 +37,9 @@ pub get
pub run test
```
gRPC-web tests require [`grpcwebproxy`](
https://github.com/improbable-eng/grpc-web/tree/master/go/grpcwebproxy) by
Improbable Engineering to be available in the PATH. Pre-built binaries are [available](https://github.com/improbable-eng/grpc-web/releases).
gRPC-web tests require [`envoy`](
https://www.envoyproxy.io/docs/envoy/latest/start/start.html) binary to be
available in the PATH.
## Guidelines for Pull Requests

View File

@ -169,9 +169,6 @@ class XhrClientConnection extends ClientConnection {
for (final header in metadata.keys) {
request.setRequestHeader(header, metadata[header]);
}
request.setRequestHeader('Content-Type', 'application/grpc-web+proto');
request.setRequestHeader('X-User-Agent', 'grpc-web-dart/0.1');
request.setRequestHeader('X-Grpc-Web', '1');
// Overriding the mimetype allows us to stream and parse the data
request.overrideMimeType('text/plain; charset=x-user-defined');
request.responseType = 'text';

View File

@ -4,6 +4,7 @@ import 'dart:convert';
import 'dart:io';
import 'package:grpc/grpc.dart';
import 'package:path/path.dart' as p;
import 'package:stream_channel/stream_channel.dart';
import 'src/generated/echo.pbgrpc.dart';
@ -30,31 +31,71 @@ class EchoService extends EchoServiceBase {
}
}
final envoyPort = 9999;
final envoyConfig = '''
static_resources:
listeners:
- name: listener_0
address:
socket_address: { address: 0.0.0.0, port_value: 0 }
filter_chains:
- filters:
- name: envoy.http_connection_manager
config:
codec_type: auto
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match: { prefix: "/" }
route: { cluster: echo_service }
cors:
allow_origin_string_match:
- prefix: "*"
allow_methods: GET, PUT, DELETE, POST, OPTIONS
allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout
max_age: "1728000"
expose_headers: custom-header-1,grpc-status,grpc-message
http_filters:
- name: envoy.filters.http.grpc_web
- name: envoy.filters.http.cors
- name: envoy.filters.http.router
clusters:
- name: echo_service
connect_timeout: 0.25s
type: static
http2_protocol_options: {}
lb_policy: round_robin
hosts:
- socket_address: { address: 127.0.0.1, port_value: %TARGET_PORT% }
''';
hybridMain(StreamChannel channel) async {
// Spawn a gRPC server.
final server = Server([EchoService()]);
await server.serve(port: 0);
_info('grpc server listening on ${server.port}');
// Create Envoy configuration.
final tempDir = await Directory.systemTemp.createTemp();
final config = p.join(tempDir.path, 'config.yaml');
await File(config).writeAsString(
envoyConfig.replaceAll('%TARGET_PORT%', server.port.toString()));
// Spawn a proxy that would translate gRPC-web protocol into gRPC protocol
// for us. We use grpcwebproxy by Improbable Engineering. See CONTRIBUTING.md
// for setup.
// for us. We use envoy proxy. See CONTRIBUTING.md for setup.
Process proxy;
try {
proxy =
await Process.start('grpcwebproxy${Platform.isWindows ? '.exe' : ''}', [
'--backend_addr',
'localhost:${server.port}',
'--run_tls_server=false',
'--server_http_debug_port',
'0',
'--allow_all_origins',
]);
proxy = await Process.start('envoy', ['-c', config, '-l', 'debug']);
} catch (e) {
print('''
Failed to start grpcwebproxy: $e.
Failed to start envoy: $e.
Make sure that grpcwebproxy is available in the PATH see CONTRIBUTING.md
Make sure that envoy is available in the PATH see CONTRIBUTING.md
if you are running tests locally.
''');
channel.sink.add(0);
@ -62,28 +103,41 @@ if you are running tests locally.
}
// Parse output of the proxy process looking for a port it selected.
final portRe = RegExp(r'listening for http on: .*:(\d+)');
final portRe = RegExp(
r'Set listener listener_0 socket factory local address to 0.0.0.0:(\d+)');
proxy.stderr
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen((line) {
_info('grpcwebproxy|stderr] $line');
_info('envoy|stderr] $line');
final m = portRe.firstMatch(line);
if (m != null) {
channel.sink.add(int.parse(m[1]));
}
});
proxy.stdout
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen((line) {
_info('grpcwebproxy|stdout] $line');
final m = portRe.firstMatch(line);
if (m != null) {
final port = int.parse(m[1]);
channel.sink.add(port);
_info('envoy|stdout] $line');
});
proxy.exitCode.then((value) {
_info('proxy quit with ${value}');
if (value != 0) {
channel.sink.addError('proxy exited with ${value}');
}
});
proxy.exitCode.then((value) => _info('proxy quit with ${value}'));
// Wait for the harness to tell us to shutdown.
await channel.stream.first;
proxy.kill();
if (tempDir.existsSync()) {
tempDir.deleteSync(recursive: true);
}
channel.sink.add('EXITED');
}
void _info(String line) {

View File

@ -1,64 +1,100 @@
@TestOn('browser')
import 'dart:async';
import 'dart:math' as math;
import 'package:stream_channel/stream_channel.dart';
import 'package:test/test.dart';
import 'package:grpc/grpc_web.dart';
import 'src/generated/echo.pbgrpc.dart';
/// Starts gRPC server and a gRPC-web proxy (see grpc_web_server.dart for
/// implementation.
///
/// Returns uri which can be used to talk to using gRPC-web channel.
///
/// Note: server will be shut down when the test which spawned it finishes
/// running.
Future<Uri> startServer() async {
// Spawn the server code on the server side, it will send us back port
// number we should be talking to.
final serverChannel = spawnHybridUri('grpc_web_server.dart');
final port = await serverChannel.stream.first;
// Note: we would like to test https as well, but we can't easily do it
// because browsers like chrome don't trust self-signed certificates by
// default.
return Uri.parse('http://localhost:$port');
}
void main() {
// Test verifies that gRPC-web echo example works by talking to a gRPC
// server (written in Dart) via gRPC-web protocol through a third party
// gRPC-web proxy.
test('gRPC-web echo test', () async {
final serverUri = await startServer();
final channel = GrpcWebClientChannel.xhr(serverUri);
final service = EchoServiceClient(channel);
final server = await GrpcWebServer.start();
try {
final channel = GrpcWebClientChannel.xhr(server.uri);
final service = EchoServiceClient(channel);
const testMessage = 'hello from gRPC-web';
const testMessage = 'hello from gRPC-web';
// First test a simple echo request.
final response = await service.echo(EchoRequest()..message = testMessage);
expect(response.message, equals(testMessage));
// Now test that streaming requests also works by asking echo server
// to send us a number of messages every 100 ms. Check that we receive
// them fast enough (if streaming is broken we will receive all of them
// in one go).
final sw = Stopwatch()..start();
final timings = await service
.serverStreamingEcho(ServerStreamingEchoRequest()
..message = testMessage
..messageCount = 20
..messageInterval = 100)
.map((response) {
// First test a simple echo request.
final response = await service.echo(EchoRequest()..message = testMessage);
expect(response.message, equals(testMessage));
final timing = sw.elapsedMilliseconds;
sw.reset();
return timing;
}).toList();
final maxDelay = timings.reduce(math.max);
expect(maxDelay, lessThan(500));
// Now test that streaming requests also works by asking echo server
// to send us a number of messages every 100 ms. Check that we receive
// them fast enough (if streaming is broken we will receive all of them
// in one go).
final sw = Stopwatch()..start();
final timings = await service
.serverStreamingEcho(ServerStreamingEchoRequest()
..message = testMessage
..messageCount = 20
..messageInterval = 100)
.map((response) {
expect(response.message, equals(testMessage));
final timing = sw.elapsedMilliseconds;
sw.reset();
return timing;
}).toList();
final maxDelay = timings.reduce(math.max);
expect(maxDelay, lessThan(500));
} finally {
await server.shutdown();
}
});
}
class GrpcWebServer {
final StreamChannel channel;
final Future<void> whenExited;
final Uri uri;
GrpcWebServer(this.channel, this.whenExited, this.uri);
Future<void> shutdown() async {
channel.sink.add('shutdown');
await whenExited;
}
/// Starts gRPC server and a gRPC-web proxy (see grpc_web_server.dart for
/// implementation.
///
/// Returns uri which can be used to talk to using gRPC-web channel.
///
/// Note: you need to explicitly call shutdown on the returned object
/// otherwise envoy proxy process leaks.
static Future<GrpcWebServer> start() async {
// Spawn the server code on the server side, it will send us back port
// number we should be talking to.
final serverChannel = spawnHybridUri('grpc_web_server.dart');
final portCompleter = Completer<int>();
final exitCompleter = Completer<void>();
serverChannel.stream.listen((event) {
if (!portCompleter.isCompleted) {
portCompleter.complete(event);
} else if (event == 'EXITED') {
exitCompleter.complete();
}
}, onError: (e) {
if (!portCompleter.isCompleted) {
portCompleter.completeError(e);
} else if (!exitCompleter.isCompleted) {
exitCompleter.completeError(e);
}
});
final port = await portCompleter.future;
// Note: we would like to test https as well, but we can't easily do it
// because browsers like chrome don't trust self-signed certificates by
// default.
return GrpcWebServer(serverChannel, exitCompleter.future,
Uri.parse('http://localhost:$port'));
}
}

View File

@ -1,44 +0,0 @@
#!/bin/sh
set -ex
VERSION=v0.13.0
SUFFIX=
WGET=wget
case $TRAVIS_OS_NAME in
linux)
VARIANT=linux-x86_64
;;
osx)
VARIANT=osx-x86_64
;;
windows)
VARIANT=win64.exe
SUFFIX=.exe
;;
esac
case $MATRIX_OS in
ubuntu-latest)
VARIANT=linux-x86_64
;;
macos-latest)
VARIANT=osx-x86_64
;;
windows-latest)
VARIANT=win64.exe
SUFFIX=.exe
WGET=C:/msys64/usr/bin/wget.exe
;;
esac
BINARY=grpcwebproxy-${VERSION}-${VARIANT}
${WGET} https://github.com/improbable-eng/grpc-web/releases/download/${VERSION}/${BINARY}.zip -O /tmp/grpcwebproxy.zip
rm -rf /tmp/grpcwebproxy
mkdir /tmp/grpcwebproxy
cd /tmp/grpcwebproxy
unzip /tmp/grpcwebproxy.zip
mv dist/${BINARY} ./grpcwebproxy${SUFFIX}