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 }} release-channel: ${{ matrix.sdk }}
- name: Report version - name: Report version
run: dart --version run: dart --version
- name: Install grpcwebproxy - name: Install envoy
if: ${{ matrix.platform == 'chrome' }} if: ${{ matrix.platform == 'chrome' }}
run: | run: |
./tool/install-grpcwebproxy.sh sudo apt update
echo "/tmp/grpcwebproxy" >> $GITHUB_PATH 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: env:
MATRIX_OS: ${{ matrix.os }} MATRIX_OS: ${{ matrix.os }}
shell: bash shell: bash

View File

@ -7,6 +7,8 @@
newer of protobuf compiler plugin. newer of protobuf compiler plugin.
* `Client.$createCall` is deprecated because it does not invoke client * `Client.$createCall` is deprecated because it does not invoke client
interceptors. 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. * 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`. * Add support for Unix domain sockets in `Socket.serve` and `ClientChannel`.

View File

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

View File

@ -169,9 +169,6 @@ class XhrClientConnection extends ClientConnection {
for (final header in metadata.keys) { for (final header in metadata.keys) {
request.setRequestHeader(header, metadata[header]); 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 // Overriding the mimetype allows us to stream and parse the data
request.overrideMimeType('text/plain; charset=x-user-defined'); request.overrideMimeType('text/plain; charset=x-user-defined');
request.responseType = 'text'; request.responseType = 'text';

View File

@ -4,6 +4,7 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:grpc/grpc.dart'; import 'package:grpc/grpc.dart';
import 'package:path/path.dart' as p;
import 'package:stream_channel/stream_channel.dart'; import 'package:stream_channel/stream_channel.dart';
import 'src/generated/echo.pbgrpc.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 { hybridMain(StreamChannel channel) async {
// Spawn a gRPC server. // Spawn a gRPC server.
final server = Server([EchoService()]); final server = Server([EchoService()]);
await server.serve(port: 0); await server.serve(port: 0);
_info('grpc server listening on ${server.port}'); _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 // Spawn a proxy that would translate gRPC-web protocol into gRPC protocol
// for us. We use grpcwebproxy by Improbable Engineering. See CONTRIBUTING.md // for us. We use envoy proxy. See CONTRIBUTING.md for setup.
// for setup.
Process proxy; Process proxy;
try { try {
proxy = proxy = await Process.start('envoy', ['-c', config, '-l', 'debug']);
await Process.start('grpcwebproxy${Platform.isWindows ? '.exe' : ''}', [
'--backend_addr',
'localhost:${server.port}',
'--run_tls_server=false',
'--server_http_debug_port',
'0',
'--allow_all_origins',
]);
} catch (e) { } catch (e) {
print(''' 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. if you are running tests locally.
'''); ''');
channel.sink.add(0); 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. // 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 proxy.stderr
.transform(utf8.decoder) .transform(utf8.decoder)
.transform(const LineSplitter()) .transform(const LineSplitter())
.listen((line) { .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 proxy.stdout
.transform(utf8.decoder) .transform(utf8.decoder)
.transform(const LineSplitter()) .transform(const LineSplitter())
.listen((line) { .listen((line) {
_info('grpcwebproxy|stdout] $line'); _info('envoy|stdout] $line');
final m = portRe.firstMatch(line); });
if (m != null) {
final port = int.parse(m[1]); proxy.exitCode.then((value) {
channel.sink.add(port); _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) { void _info(String line) {

View File

@ -1,64 +1,100 @@
@TestOn('browser') @TestOn('browser')
import 'dart:async';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:stream_channel/stream_channel.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'package:grpc/grpc_web.dart'; import 'package:grpc/grpc_web.dart';
import 'src/generated/echo.pbgrpc.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() { void main() {
// Test verifies that gRPC-web echo example works by talking to a gRPC // 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 // server (written in Dart) via gRPC-web protocol through a third party
// gRPC-web proxy. // gRPC-web proxy.
test('gRPC-web echo test', () async { test('gRPC-web echo test', () async {
final serverUri = await startServer(); final server = await GrpcWebServer.start();
final channel = GrpcWebClientChannel.xhr(serverUri); try {
final service = EchoServiceClient(channel); 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. // First test a simple echo request.
final response = await service.echo(EchoRequest()..message = testMessage); 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) {
expect(response.message, equals(testMessage)); expect(response.message, equals(testMessage));
final timing = sw.elapsedMilliseconds;
sw.reset(); // Now test that streaming requests also works by asking echo server
return timing; // to send us a number of messages every 100 ms. Check that we receive
}).toList(); // them fast enough (if streaming is broken we will receive all of them
final maxDelay = timings.reduce(math.max); // in one go).
expect(maxDelay, lessThan(500)); 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}