| // Copyright 2017 The Fuchsia Authors. 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 'package:fuchsia/fuchsia.dart' as fuchsia; |
| import 'package:lib.app.dart/app.dart'; |
| import 'package:fidl/fidl.dart'; |
| import 'package:fidl_fuchsia_modular/fidl.dart'; |
| |
| import 'package:path/path.dart' as path; |
| |
| import 'src/data_handler.dart'; |
| import 'src/ledger_debug_data_handler.dart'; |
| |
| const String _configDir = '/pkg/data'; |
| const String _configFilename = 'dashboard.config'; |
| const String _defaultWebrootPath = 'webroot'; |
| const int _defaultPort = 4001; |
| |
| const String _portPropertyName = 'port'; |
| const String _webrootPropertyName = 'webroot'; |
| |
| int _port = _defaultPort; |
| String _webrootPath = _defaultWebrootPath; |
| Directory _webrootDirectory; |
| |
| final Map<String, DataHandler> _dataHandlerMap = <String, DataHandler>{}; |
| |
| void _log(String msg) { |
| print('[Ledger Dashboad] $msg'); |
| } |
| |
| /// An implementation of the [Lifecycle] interface. |
| class LifecycleImpl implements Lifecycle { |
| final LifecycleBinding _lifecycleBinding = new LifecycleBinding(); |
| |
| LifecycleImpl(); |
| |
| /// Bind an [InterfaceRequest] for a [Lifecycle] interface to this object. |
| void bindLifecycle(InterfaceRequest<Lifecycle> request) { |
| _lifecycleBinding.bind(this, request); |
| } |
| |
| /// Implementation of the Lifecycle.Terminate() method. |
| @override |
| void terminate() { |
| _log('LifecycleImpl.terminate()'); |
| _lifecycleBinding.close(); |
| fuchsia.exit(0); |
| } |
| } |
| |
| void main(List<String> args) { |
| final StartupContext context = new StartupContext.fromStartupInfo(); |
| |
| // Assemble the list of DataHandlers |
| addDataHandler(new LedgerDebugDataHandler()); |
| |
| // Initialize the DataHandlers |
| _dataHandlerMap.forEach((String name, DataHandler handler) { |
| handler.init(context); |
| }); |
| |
| final LifecycleImpl lifeCycleImpl = new LifecycleImpl(); |
| context.outgoingServices.addServiceForName( |
| lifeCycleImpl.bindLifecycle, |
| Lifecycle.$serviceName, |
| ); |
| |
| context.close(); |
| |
| // Read the config file from disk |
| final File configFile = new File(path.join(_configDir, _configFilename)); |
| configFile.readAsString(encoding: ascii).then(parseConfigAndStart); |
| } |
| |
| void addDataHandler(DataHandler handler) { |
| _dataHandlerMap[handler.name] = handler; |
| } |
| |
| void parseConfigAndStart(String configString) { |
| // parse config file as JSON |
| Map<String, dynamic> configMap = json.decode(configString); |
| |
| // port property |
| if (configMap.containsKey(_portPropertyName)) |
| _port = configMap[_portPropertyName]; |
| |
| // webroot property |
| if (configMap.containsKey(_webrootPropertyName)) |
| _webrootPath = configMap[_webrootPropertyName]; |
| _webrootDirectory = new Directory(path.join(_configDir, _webrootPath)); |
| |
| // Start the web server |
| print('[INFO] Starting LEDGER Dashboard web server on port $_port...'); |
| HttpServer.bind(InternetAddress.anyIPv6, _port).then((HttpServer server) { |
| server.listen(handleRequest); |
| // ignore: always_specify_types |
| }).catchError((error) { |
| print('[WARN] LEDGER Dashboard bind failed... $error'); |
| }); |
| } |
| |
| void handleRequest(HttpRequest request) { |
| // Identify websocket requests |
| // Such requests will end with /ws/<service>/ |
| final RegExp websocketRequestPattern = new RegExp('/ws/([^/]+)/'); |
| final Match match = |
| websocketRequestPattern.firstMatch(request.requestedUri.path); |
| if (match != null) { |
| WebSocketTransformer.upgrade(request).then((WebSocket socket) { |
| final String serviceName = match.group(1); |
| final DataHandler handler = _dataHandlerMap[serviceName]; |
| if (handler != null) { |
| handler.handleNewWebSocket(socket); |
| } else { |
| send404(request.response); |
| } |
| }); |
| } else if (request.requestedUri.path.startsWith('/ws')) { |
| send404(request.response); |
| } else { |
| // Identify requests requiring return of context data |
| // Such requests will begin with /data/<service>/... |
| final RegExp dataRequestPattern = new RegExp('/data/([^/]+)(/.*)'); |
| final Match match = |
| dataRequestPattern.firstMatch(request.requestedUri.path); |
| if (match != null) { |
| final String serviceName = |
| match.group(1); // first match group is the service name |
| // print('Returning data for service ${serviceName}'); |
| |
| // we are returning JSON |
| request.response.headers.contentType = |
| new ContentType('application', 'json', charset: 'utf-8'); |
| |
| // If an appropriate handler can be found, ask it to respond |
| final DataHandler handler = _dataHandlerMap[serviceName]; |
| if (handler?.handleRequest(match.group(2), request) ?? false) { |
| return; |
| } |
| |
| // Nothing handled the request, so respond with a 404 |
| send404(request.response); |
| } else { |
| // Find the referenced file |
| // path.join does not work in this case, possibly because the request path |
| // may start with a /, so using a simple string concatenation instead |
| String requestPath = |
| '${_webrootDirectory.path}/${request.requestedUri.path}'; |
| if (requestPath.endsWith('/')) { |
| requestPath = '${requestPath}index.html'; |
| } |
| final File requestFile = new File(requestPath); |
| if (requestFile.existsSync()) { |
| // Make sure the referenced file is within the webroot |
| if (requestFile.uri.path.startsWith(_webrootDirectory.path)) { |
| sendFile(requestFile, request.response); |
| return; |
| } |
| } else { |
| send404(request.response); |
| } |
| } |
| } |
| } |
| |
| Future<Null> sendFile(File requestFile, HttpResponse response) async { |
| // Set the content type correctly based on the file name suffix |
| // The content type is text/plain if the suffix isn't identified |
| if (requestFile.path.endsWith('html')) { |
| response.headers.contentType = |
| new ContentType('text', 'html', charset: 'utf-8'); |
| } else if (requestFile.path.endsWith('json')) { |
| response.headers.contentType = |
| new ContentType('application', 'json', charset: 'utf-8'); |
| } else if (requestFile.path.endsWith('js')) { |
| response.headers.contentType = |
| new ContentType('application', 'javascript', charset: 'utf-8'); |
| } else if (requestFile.path.endsWith('css')) { |
| response.headers.contentType = |
| new ContentType('text', 'css', charset: 'utf-8'); |
| } else if (requestFile.path.endsWith('jpg') || |
| requestFile.path.endsWith('jpeg')) { |
| response.headers.contentType = new ContentType('image', 'jpeg'); |
| } else if (requestFile.path.endsWith('png')) { |
| response.headers.contentType = new ContentType('image', 'png'); |
| } else { |
| response.headers.contentType = |
| new ContentType('text', 'plain', charset: 'utf-8'); |
| } |
| |
| // Send the contents of the file |
| await requestFile.openRead().pipe(response); |
| |
| return response.close(); |
| } |
| |
| void send404(HttpResponse response) { |
| response |
| ..statusCode = 404 |
| ..reasonPhrase = 'File not found.' |
| ..close(); |
| } |