blob: 210a653db96410b909bd0449eb8c9a750b586b44 [file] [log] [blame]
// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'package:dwds/dwds.dart' show LogWriter;
import 'package:dwds/src/debugging/remote_debugger.dart';
import 'package:http_multi_server/http_multi_server.dart';
import 'package:pedantic/pedantic.dart';
import 'package:shelf/shelf.dart' as shelf;
import 'package:shelf/shelf.dart' hide Response;
import 'package:shelf/shelf_io.dart';
import 'package:shelf_web_socket/shelf_web_socket.dart';
import 'package:sse/server/sse_handler.dart';
import 'package:vm_service/vm_service.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import '../handlers/asset_handler.dart';
import '../utilities/shared.dart';
import 'chrome_proxy_service.dart';
void Function(WebSocketChannel, String) _createNewConnectionHandler(
ChromeProxyService chromeProxyService,
ServiceExtensionRegistry serviceExtensionRegistry, {
void Function(Map<String, dynamic>) onRequest,
void Function(Map<String, dynamic>) onResponse,
}) {
return (webSocket, protocol) {
var responseController = StreamController<Map<String, Object>>();
webSocket.sink.addStream(responseController.stream.map((response) {
if (onResponse != null) onResponse(response);
return jsonEncode(response);
}));
var inputStream = webSocket.stream.map((value) {
if (value is List<int>) {
value = utf8.decode(value as List<int>);
} else if (value is! String) {
throw StateError(
'Got value with unexpected type ${value.runtimeType} from web '
'socket, expected a List<int> or String.');
}
var request = jsonDecode(value as String) as Map<String, Object>;
if (onRequest != null) onRequest(request);
return request;
});
VmServerConnection(inputStream, responseController.sink,
serviceExtensionRegistry, chromeProxyService);
};
}
Future<void> _handleSseConnections(
SseHandler handler,
ChromeProxyService chromeProxyService,
ServiceExtensionRegistry serviceExtensionRegistry, {
void Function(Map<String, dynamic>) onRequest,
void Function(Map<String, dynamic>) onResponse,
}) async {
while (await handler.connections.hasNext) {
var connection = await handler.connections.next;
var responseController = StreamController<Map<String, Object>>();
var sub = responseController.stream.map((response) {
if (onResponse != null) onResponse(response);
return jsonEncode(response);
}).listen(connection.sink.add);
var inputStream = connection.stream.map((value) {
var request = jsonDecode(value) as Map<String, Object>;
if (onRequest != null) onRequest(request);
return request;
});
var vmServerConnection = VmServerConnection(inputStream,
responseController.sink, serviceExtensionRegistry, chromeProxyService);
unawaited(vmServerConnection.done.whenComplete(sub.cancel));
}
}
/// A Dart Web Debug Service.
///
/// Creates a [ChromeProxyService] from an existing Chrome instance.
class DebugService {
final VmServiceInterface chromeProxyService;
final String hostname;
final ServiceExtensionRegistry serviceExtensionRegistry;
final int port;
final HttpServer _server;
final String _authToken;
final bool _useSse;
DebugService._(
this.chromeProxyService,
this.hostname,
this.port,
this._authToken,
this.serviceExtensionRegistry,
this._server,
this._useSse);
Future<void> close() async {
await _server.close();
}
String get uri => _useSse
? 'sse://$hostname:$port/$_authToken/\$debugHandler'
: 'ws://$hostname:$port/$_authToken';
/// [appInstanceId] is a unique String embedded in the instance of the
/// application available through `window.$dartAppInstanceId`.
static Future<DebugService> start(
String hostname,
RemoteDebugger remoteDebugger,
String tabUrl,
AssetHandler assetHandler,
String appInstanceId,
LogWriter logWriter, {
void Function(Map<String, dynamic>) onRequest,
void Function(Map<String, dynamic>) onResponse,
bool useSse,
}) async {
useSse ??= false;
var chromeProxyService = await ChromeProxyService.create(
remoteDebugger, tabUrl, assetHandler, appInstanceId, logWriter);
var authToken = _makeAuthToken();
var serviceExtensionRegistry = ServiceExtensionRegistry();
Handler handler;
if (useSse) {
var sseHandler = SseHandler(Uri.parse('/$authToken/\$debugHandler'));
handler = sseHandler.handler;
unawaited(_handleSseConnections(
sseHandler, chromeProxyService, serviceExtensionRegistry,
onRequest: onRequest, onResponse: onResponse));
} else {
var innerHandler = webSocketHandler(_createNewConnectionHandler(
chromeProxyService, serviceExtensionRegistry,
onRequest: onRequest, onResponse: onResponse));
handler = (shelf.Request request) {
if (request.url.pathSegments.first != authToken) {
return shelf.Response.forbidden('Incorrect auth token');
}
return innerHandler(request);
};
}
var port = await findUnusedPort();
var server = hostname == 'localhost'
? await HttpMultiServer.loopback(port)
: await HttpServer.bind(hostname, port);
serveRequests(server, handler);
return DebugService._(
chromeProxyService,
hostname,
port,
authToken,
serviceExtensionRegistry,
server,
useSse,
);
}
}
// Creates a random auth token for more secure connections.
String _makeAuthToken() {
final tokenBytes = 8;
final bytes = Uint8List(tokenBytes);
final random = Random.secure();
for (var i = 0; i < tokenBytes; i++) {
bytes[i] = random.nextInt(256);
}
return base64Url.encode(bytes);
}