Fix X-Forwarded-For and cleanup old code (#369)
diff --git a/pkgs/appengine/CHANGELOG.md b/pkgs/appengine/CHANGELOG.md
index baf76cf..1772f43 100644
--- a/pkgs/appengine/CHANGELOG.md
+++ b/pkgs/appengine/CHANGELOG.md
@@ -1,11 +1,13 @@
-## 0.13.13-wip
+## 0.13.13
* Rename error classes to use the `Exception` suffix
(`AppEngineException`, `NetworkException`, `ProtocolException`,
`ServiceException`, `ApplicationException`) to follow Dart conventions for
types implementing `Exception`. The old `*Error` names remain available as
deprecated type aliases, so this change is non-breaking.
-- Automated formatting and fixes.
+* Automated formatting and fixes.
+* Cleanup legacy code.
+* Fix `X-Forwarded-For` handling.
## 0.13.12
diff --git a/pkgs/appengine/lib/src/server/context_registry.dart b/pkgs/appengine/lib/src/server/context_registry.dart
index b9dfeba..e97ccb0 100644
--- a/pkgs/appengine/lib/src/server/context_registry.dart
+++ b/pkgs/appengine/lib/src/server/context_registry.dart
@@ -25,6 +25,9 @@
Logging newBackgroundLogger();
}
+/// Sanity check for traceId (must be 32 char hex)
+final _traceIdFormat = RegExp(r'^[0-9a-f]{32}$');
+
class ContextRegistry {
final LoggerFactory _loggingFactory;
final db.DatastoreDB _db;
@@ -42,13 +45,16 @@
ClientContext add(HttpRequest request) {
String? traceId;
- // See https://cloud.google.com/trace/docs/support
+ // See https://docs.cloud.google.com/trace/docs/trace-context#legacy-http-header
final traceHeader = _headerOrEmptyString(
request.headers,
'X-Cloud-Trace-Context',
);
if (traceHeader != '') {
- traceId = traceHeader.split('/')[0];
+ final traceIdFromHeader = traceHeader.split('/').first;
+ if (_traceIdFormat.hasMatch(traceIdFromHeader)) {
+ traceId = traceIdFromHeader;
+ }
}
final services = _getServices(request, traceId);
@@ -84,12 +90,19 @@
String ip;
if (forwardedFor != null && forwardedFor.isNotEmpty) {
- // It seems that, in general, if `x-forwarded-for` has multiple values
- // it is sent as a single header value separated by commas.
- // To ensure only one value for IP is provided, we join all of the
- // `x-forwarded-for` headers into a single string, split on comma,
- // then use the first value.
- ip = forwardedFor.join(',').split(',').first.trim();
+ // Google Cloud Load Balancers append the connecting client IP and the
+ // load balancer IP to any existing `X-Forwarded-For` values.
+ // Client-supplied (possibly spoofed) IPs appear first, so we use
+ // the second-to-last IP as the client IP that arrived at GCLB.
+ // See: https://cloud.google.com/load-balancing/docs/https#x-forwarded-for_header
+ final parts = forwardedFor
+ .expand((header) => header.split(','))
+ .map((ip) => ip.trim())
+ .where((ip) => ip.isNotEmpty)
+ .toList();
+ ip = parts.length >= 2
+ ? parts[parts.length - 2]
+ : request.connectionInfo!.remoteAddress.host;
} else {
ip = request.connectionInfo!.remoteAddress.host;
}
diff --git a/pkgs/appengine/lib/src/server/server.dart b/pkgs/appengine/lib/src/server/server.dart
index 5525ba1..5d3a699 100644
--- a/pkgs/appengine/lib/src/server/server.dart
+++ b/pkgs/appengine/lib/src/server/server.dart
@@ -3,7 +3,6 @@
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
-import 'dart:convert' show utf8;
import 'dart:io';
import '../client_context.dart';
@@ -22,9 +21,6 @@
final bool _shared;
final Completer _shutdownCompleter = Completer();
- int _pendingRequests = 0;
-
- HttpServer? _httpServer;
AppEngineHttpServer(this._contextRegistry,
{String hostname = '0.0.0.0', int port = 8080, bool shared = false})
@@ -38,34 +34,13 @@
Function(HttpRequest request, ClientContext context) applicationHandler, {
void Function(InternetAddress address, int port)? onAcceptingConnections,
}) {
- final serviceHandlers = {
- '/_ah/start': _start,
- '/_ah/health': _health,
- '/_ah/stop': _stop
- };
-
HttpServer.bind(_hostname, _port, shared: _shared)
.then((HttpServer server) {
- _httpServer = server;
if (onAcceptingConnections != null) {
onAcceptingConnections(server.address, server.port);
}
server.listen((HttpRequest request) {
- // Default handling is sending the request to the application.
- dynamic Function(HttpRequest, ClientContext)? handler =
- applicationHandler;
-
- // Check if the request path is one of the service handlers.
- final String path = request.uri.path;
- for (final pattern in serviceHandlers.keys) {
- if (path.startsWith(pattern)) {
- handler = serviceHandlers[pattern];
- break;
- }
- }
-
- _pendingRequests++;
final context = _contextRegistry.add(request);
request.response.done.whenComplete(() {
_contextRegistry.remove(request);
@@ -75,56 +50,10 @@
if (!_contextRegistry.isDevelopmentEnvironment) {
_info('Error while handling response: $error');
}
- _pendingRequests--;
- _checkShutdown();
});
- handler!(request, context);
+ applicationHandler(request, context);
});
});
}
-
- void _start(HttpRequest request, _) {
- request.drain().then((_) {
- _sendResponse(request.response, HttpStatus.ok, 'ok');
- });
- }
-
- void _health(HttpRequest request, _) {
- request.drain().then((_) {
- _sendResponse(request.response, HttpStatus.ok, 'ok');
- });
- }
-
- void _stop(HttpRequest request, _) {
- request.drain().then((_) {
- if (_httpServer != null) {
- _httpServer!.close().then((_) {
- _httpServer = null;
- _sendResponse(request.response, HttpStatus.ok, 'ok');
- });
- } else {
- _sendResponse(request.response, HttpStatus.conflict, 'fail');
- }
- });
- }
-
- void _checkShutdown() {
- if (_pendingRequests == 0 && _httpServer == null) {
- _shutdownCompleter.complete();
- }
- }
-
- void _sendResponse(HttpResponse response, int statusCode, String message) {
- final data = utf8.encode(message);
- response.headers.contentType =
- ContentType('text', 'plain', charset: 'utf-8');
- response.headers.set('Cache-Control', 'no-cache');
- response.headers.set('Server', _hostname);
- response
- ..contentLength = data.length
- ..statusCode = statusCode
- ..add(data)
- ..close();
- }
}
diff --git a/pkgs/appengine/pubspec.yaml b/pkgs/appengine/pubspec.yaml
index 7ad3525..b9ddf7e 100644
--- a/pkgs/appengine/pubspec.yaml
+++ b/pkgs/appengine/pubspec.yaml
@@ -1,5 +1,5 @@
name: appengine
-version: 0.13.13-wip
+version: 0.13.13
description: >-
Support for using Dart as a custom runtime on Google App Engine Flexible
Environment.