blob: a0446ad222a02f5e3ac7489f510a44acbff88931 [file] [log] [blame]
// Copyright (c) 2013, 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.
part of vmservice_io;
final bool silentObservatory = const bool.fromEnvironment('SILENT_OBSERVATORY');
void serverPrint(String s) {
if (silentObservatory) {
// We've been requested to be silent.
return;
}
print(s);
}
class WebSocketClient extends Client {
static const int PARSE_ERROR_CODE = 4000;
static const int BINARY_MESSAGE_ERROR_CODE = 4001;
static const int NOT_MAP_ERROR_CODE = 4002;
static const int ID_ERROR_CODE = 4003;
final WebSocket socket;
WebSocketClient(this.socket, VMService service) : super(service) {
socket.listen((message) => onWebSocketMessage(message));
socket.done.then((_) => close());
}
disconnect() {
if (socket != null) {
socket.close();
}
}
void onWebSocketMessage(message) {
if (message is String) {
var map;
try {
map = JSON.decode(message);
} catch (e) {
socket.close(PARSE_ERROR_CODE, 'Message parse error: $e');
return;
}
if (map is! Map) {
socket.close(NOT_MAP_ERROR_CODE, 'Message must be a JSON map.');
return;
}
var serial = map['id'];
if (serial != null && serial is! num && serial is! String) {
socket.close(ID_ERROR_CODE, '"id" must be a number, string, or null.');
}
onMessage(serial, new Message.fromJsonRpc(this, map));
} else {
socket.close(BINARY_MESSAGE_ERROR_CODE, 'Message must be a string.');
}
}
void post(dynamic result) {
if (result == null) {
// Do nothing.
return;
}
try {
if (result is String || result is Uint8List) {
socket.add(result); // String or binary message.
} else {
// String message as external Uint8List.
assert(result is List);
Uint8List cstring = result[0];
socket.addUtf8Text(cstring);
}
} catch (e, st) {
serverPrint("Ignoring error posting over WebSocket.");
serverPrint(e);
serverPrint(st);
}
}
dynamic toJson() {
Map map = super.toJson();
map['type'] = 'WebSocketClient';
map['socket'] = '$socket';
return map;
}
}
class HttpRequestClient extends Client {
static ContentType jsonContentType =
new ContentType("application", "json", charset: "utf-8");
final HttpRequest request;
HttpRequestClient(this.request, VMService service)
: super(service, sendEvents: false);
disconnect() {
request.response.close();
close();
}
void post(dynamic result) {
if (result == null) {
close();
return;
}
HttpResponse response = request.response;
// We closed the connection for bad origins earlier.
response.headers.add('Access-Control-Allow-Origin', '*');
response.headers.contentType = jsonContentType;
if (result is String) {
response.write(result);
} else {
assert(result is List);
Uint8List cstring = result[0]; // Already in UTF-8.
response.add(cstring);
}
response.close();
close();
}
dynamic toJson() {
Map map = super.toJson();
map['type'] = 'HttpRequestClient';
map['request'] = '$request';
return map;
}
}
class Server {
static const WEBSOCKET_PATH = '/ws';
static const ROOT_REDIRECT_PATH = '/index.html';
final VMService _service;
final String _ip;
final int _port;
final bool _originCheckDisabled;
HttpServer _server;
bool get running => _server != null;
/// Returns the server address including the auth token.
Uri get serverAddress {
if (!running) {
return null;
}
var ip = _server.address.address;
var port = _server.port;
var path = useAuthToken ? "$serviceAuthToken/" : "/";
return new Uri(scheme: 'http', host: ip, port: port, path: path);
}
Server(this._service, this._ip, this._port, this._originCheckDisabled);
bool _isAllowedOrigin(String origin) {
Uri uri;
try {
uri = Uri.parse(origin);
} catch (_) {
return false;
}
// Explicitly add localhost and 127.0.0.1 on any port (necessary for
// adb port forwarding).
if ((uri.host == 'localhost') ||
(uri.host == '::1') ||
(uri.host == '127.0.0.1')) {
return true;
}
if ((uri.port == _server.port) &&
((uri.host == _server.address.address) ||
(uri.host == _server.address.host))) {
return true;
}
return false;
}
bool _originCheck(HttpRequest request) {
if (_originCheckDisabled || Platform.isFuchsia) {
// Always allow.
return true;
}
// First check the web-socket specific origin.
List<String> origins = request.headers["Sec-WebSocket-Origin"];
if (origins == null) {
// Fall back to the general Origin field.
origins = request.headers["Origin"];
}
if (origins == null) {
// No origin sent. This is a non-browser client or a same-origin request.
return true;
}
for (String origin in origins) {
if (_isAllowedOrigin(origin)) {
return true;
}
}
return false;
}
/// Checks the [requestUri] for the service auth token and returns the path.
/// If the service auth token check fails, returns null.
String _checkAuthTokenAndGetPath(Uri requestUri) {
if (!useAuthToken) {
return requestUri.path == '/' ? ROOT_REDIRECT_PATH : requestUri.path;
}
final List<String> requestPathSegments = requestUri.pathSegments;
if (requestPathSegments.length < 2) {
// Malformed.
return null;
}
// Check that we were given the auth token.
final String authToken = requestPathSegments[0];
if (authToken != serviceAuthToken) {
// Malformed.
return null;
}
// Construct the actual request path by chopping off the auth token.
return (requestPathSegments[1] == '')
? ROOT_REDIRECT_PATH
: '/${requestPathSegments.sublist(1).join('/')}';
}
Future _requestHandler(HttpRequest request) async {
if (!_originCheck(request)) {
// This is a cross origin attempt to connect
request.response.close();
return;
}
if (request.method == 'PUT') {
// PUT requests are forwarded to DevFS for processing.
List fsNameList;
List fsPathList;
List fsPathBase64List;
List fsUriBase64List;
Object fsName;
Object fsPath;
Object fsUri;
try {
// Extract the fs name and fs path from the request headers.
fsNameList = request.headers['dev_fs_name'];
fsName = fsNameList[0];
// Prefer Uri encoding first.
fsUriBase64List = request.headers['dev_fs_uri_b64'];
if ((fsUriBase64List != null) && (fsUriBase64List.length > 0)) {
String decodedFsUri = UTF8.decode(BASE64.decode(fsUriBase64List[0]));
fsUri = Uri.parse(decodedFsUri);
}
// Fallback to path encoding.
if (fsUri == null) {
fsPathList = request.headers['dev_fs_path'];
fsPathBase64List = request.headers['dev_fs_path_b64'];
// If the 'dev_fs_path_b64' header field was sent, use that instead.
if ((fsPathBase64List != null) && (fsPathBase64List.length > 0)) {
fsPath = UTF8.decode(BASE64.decode(fsPathBase64List[0]));
} else {
fsPath = fsPathList[0];
}
}
} catch (e) {/* ignore */}
String result;
try {
result = await _service.devfs.handlePutStream(
fsName, fsPath, fsUri, request.transform(GZIP.decoder));
} catch (e) {/* ignore */}
if (result != null) {
request.response.headers.contentType =
HttpRequestClient.jsonContentType;
request.response.write(result);
}
request.response.close();
return;
}
if (request.method != 'GET') {
// Not a GET request. Do nothing.
request.response.close();
return;
}
final String path = _checkAuthTokenAndGetPath(request.uri);
if (path == null) {
// Malformed.
request.response.close();
return;
}
if (path == WEBSOCKET_PATH) {
WebSocketTransformer.upgrade(request).then((WebSocket webSocket) {
new WebSocketClient(webSocket, _service);
});
return;
}
if (assets == null) {
request.response.headers.contentType = ContentType.TEXT;
request.response.write("This VM was built without the Observatory UI.");
request.response.close();
return;
}
Asset asset = assets[path];
if (asset != null) {
// Serving up a static asset (e.g. .css, .html, .png).
request.response.headers.contentType = ContentType.parse(asset.mimeType);
request.response.add(asset.data);
request.response.close();
return;
}
// HTTP based service request.
try {
var client = new HttpRequestClient(request, _service);
var message = new Message.fromUri(client, request.uri);
client.onMessage(null, message);
} catch (e) {
serverPrint('Unexpected error processing HTTP request uri: '
'${request.uri}\n$e\n');
rethrow;
}
}
Future startup() async {
if (_server != null) {
// Already running.
return this;
}
// Startup HTTP server.
var pollError;
var pollStack;
Future<bool> poll() async {
try {
var address;
if (Platform.isFuchsia) {
address = InternetAddress.ANY_IP_V4;
} else {
var addresses = await InternetAddress.lookup(_ip);
// Prefer IPv4 addresses.
for (var i = 0; i < addresses.length; i++) {
address = addresses[i];
if (address.type == InternetAddressType.IP_V4) break;
}
}
_server = await HttpServer.bind(address, _port);
return true;
} catch (e, st) {
pollError = e;
pollStack = st;
return false;
}
}
// poll for the network for ~10 seconds.
int attempts = 0;
final int maxAttempts = 10;
while (!await poll()) {
attempts++;
serverPrint("Observatory server failed to start after $attempts tries");
if (attempts > maxAttempts) {
serverPrint('Could not start Observatory HTTP server:\n'
'$pollError\n$pollStack\n');
_notifyServerState("");
onServerAddressChange(null);
return this;
}
await new Future<Null>.delayed(const Duration(seconds: 1));
}
_server.listen(_requestHandler, cancelOnError: true);
serverPrint('Observatory listening on $serverAddress');
if (Platform.isFuchsia) {
// Create a file with the port number.
String tmp = Directory.systemTemp.path;
String path = "$tmp/dart.services/${_server.port}";
serverPrint("Creating $path");
new File(path)..createSync(recursive: true);
}
// Server is up and running.
_notifyServerState(serverAddress.toString());
onServerAddressChange('$serverAddress');
return this;
}
Future cleanup(bool force) {
if (_server == null) {
return new Future.value(null);
}
if (Platform.isFuchsia) {
// Remove the file with the port number.
String tmp = Directory.systemTemp.path;
String path = "$tmp/dart.services/${_server.port}";
serverPrint("Deleting $path");
new File(path)..deleteSync();
}
return _server.close(force: force);
}
Future shutdown(bool forced) {
if (_server == null) {
// Not started.
return new Future.value(this);
}
// Shutdown HTTP server and subscription.
Uri oldServerAddress = serverAddress;
return cleanup(forced).then((_) {
serverPrint('Observatory no longer listening on $oldServerAddress');
_server = null;
_notifyServerState("");
onServerAddressChange(null);
return this;
}).catchError((e, st) {
_server = null;
serverPrint('Could not shutdown Observatory HTTP server:\n$e\n$st\n');
_notifyServerState("");
onServerAddressChange(null);
return this;
});
}
}
void _notifyServerState(String uri) native "VMServiceIO_NotifyServerState";