blob: d27ac3de94bb46162c49b1a8b34b2123181544c5 [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:io';
import 'package:cli_util/cli_logging.dart';
import 'package:nnbd_migration/src/front_end/migration_state.dart';
import 'package:nnbd_migration/src/preview/preview_site.dart';
/// Instances of the class [AbstractGetHandler] handle GET requests.
abstract class AbstractGetHandler {
/// Handle a GET request received by the HTTP server.
Future<void> handleGetRequest(HttpRequest request);
}
/// Instances of the class [AbstractPostHandler] handle POST requests.
abstract class AbstractPostHandler {
/// Handle a POST request received by the HTTP server.
Future<void> handlePostRequest(HttpRequest request);
}
/// Instances of the class [HttpPreviewServer] implement a simple HTTP server
/// that serves up dartfix preview pages.
class HttpPreviewServer {
/// The state of the migration being previewed.
final MigrationState migrationState;
/// The [PreviewSite] that can handle GET and POST requests.
PreviewSite? _previewSite;
/// Future that is completed with the HTTP server once it is running.
Future<HttpServer>? _serverFuture;
// A function which allows the migration to be rerun, taking changed paths.
final Future<MigrationState> Function() rerunFunction;
/// Callback function that should be invoked after successfully applying
/// migration.
final void Function() applyHook;
/// The internet address the server should bind to. Should be suitable for
/// passing to HttpServer.bind, i.e. either a [String] or an
/// [InternetAddress].
final Object? bindAddress;
/// Integer for a port to run the preview server on. If null or zero, allow
/// [HttpServer.bind] to pick one.
final int? preferredPort;
final Logger _logger;
/// Initialize a newly created HTTP server.
HttpPreviewServer(this.migrationState, this.rerunFunction, this.applyHook,
this.bindAddress, this.preferredPort, this._logger)
: assert(bindAddress is String || bindAddress is InternetAddress);
Future<String> get authToken async {
await _serverFuture;
return previewSite.serviceAuthToken;
}
/// Return the port this server is bound to.
Future<String?> get boundHostname async {
return (await _serverFuture)?.address.host;
}
/// Return the port this server is bound to.
Future<int?> get boundPort async {
return (await _serverFuture)?.port;
}
PreviewSite get previewSite => _previewSite ??=
PreviewSite(migrationState, rerunFunction, applyHook, _logger);
void close() {
_serverFuture?.then((HttpServer server) {
server.close();
});
}
/// Begin serving HTTP requests over the given port.
Future<int?> serveHttp() async {
if (_serverFuture != null) {
return boundPort;
}
try {
_serverFuture = HttpServer.bind(bindAddress, preferredPort ?? 0);
var server = await _serverFuture!;
_handleServer(server);
return server.port;
} catch (ignore) {
// If we can't bind to the specified port, don't remember the broken
// server.
_serverFuture = null;
// TODO(jcollins-g): Display a better error message?
rethrow;
}
}
/// Handle a GET request received by the HTTP server.
Future<void> _handleGetRequest(HttpRequest request) async {
await previewSite.handleGetRequest(request);
}
/// Handle a POST request received by the HTTP server.
Future<void> _handlePostRequest(HttpRequest request) async {
await previewSite.handlePostRequest(request);
}
/// Attach a listener to a newly created HTTP server.
void _handleServer(HttpServer httpServer) {
httpServer.listen((HttpRequest request) async {
var updateValues = request.headers[HttpHeaders.upgradeHeader];
if (request.method == 'GET') {
await _handleGetRequest(request);
} else if (request.method == 'POST') {
await _handlePostRequest(request);
} else if (updateValues != null && updateValues.contains('websocket')) {
// We do not support serving analysis server communications over
// WebSocket connections.
var response = request.response;
response.statusCode = HttpStatus.notFound;
response.headers.contentType = ContentType.text;
response.write(
'WebSocket connections not supported (${request.uri.path}).');
response.close();
} else {
_returnUnknownRequest(request);
}
});
}
/// Return an error in response to an unrecognized request received by the HTTP
/// server.
void _returnUnknownRequest(HttpRequest request) {
var response = request.response;
response.statusCode = HttpStatus.notFound;
response.headers.contentType = ContentType.text;
response.write('Not found');
response.close();
}
}