| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | |
| 2 // for details. All rights reserved. Use of this source code is governed by a | |
| 3 // BSD-style license that can be found in the LICENSE file. | |
| 4 | |
| 5 library http_server; | |
| 6 | |
| 7 import 'dart:async'; | |
| 8 import 'dart:io'; | |
| 9 | |
| 10 import 'dart:convert' show | |
| 11 HtmlEscape; | |
| 12 | |
| 13 import 'path.dart'; | |
| 14 import 'test_suite.dart'; // For TestUtils. | |
| 15 // TODO(efortuna): Rewrite to not use the args library and simply take an | |
| 16 // expected number of arguments, so test.dart doesn't rely on the args library? | |
| 17 // See discussion on https://codereview.chromium.org/11931025/. | |
| 18 import 'vendored_pkg/args/args.dart'; | |
| 19 import 'utils.dart'; | |
| 20 | |
| 21 class DispatchingServer { | |
| 22 HttpServer server; | |
| 23 Map<String, Function> _handlers = new Map<String, Function>(); | |
| 24 Function _notFound; | |
| 25 | |
| 26 DispatchingServer(this.server, | |
| 27 void onError(e), | |
| 28 void this._notFound(HttpRequest request)) { | |
| 29 server.listen(_dispatchRequest, onError: onError); | |
| 30 } | |
| 31 | |
| 32 void addHandler(String prefix, void handler(HttpRequest request)) { | |
| 33 _handlers[prefix] = handler; | |
| 34 } | |
| 35 | |
| 36 void _dispatchRequest(HttpRequest request) { | |
| 37 // If the request path matches a prefix in _handlers, send it to that | |
| 38 // handler. Otherwise, run the notFound handler. | |
| 39 for (String prefix in _handlers.keys) { | |
| 40 if (request.uri.path.startsWith(prefix)) { | |
| 41 _handlers[prefix](request); | |
| 42 return; | |
| 43 } | |
| 44 } | |
| 45 _notFound(request); | |
| 46 } | |
| 47 } | |
| 48 | |
| 49 | |
| 50 | |
| 51 /// Interface of the HTTP server: | |
| 52 /// | |
| 53 /// /echo: This will stream the data received in the request stream back | |
| 54 /// to the client. | |
| 55 /// /root_dart/X: This will serve the corresponding file from the dart | |
| 56 /// directory (i.e. '$DartDirectory/X'). | |
| 57 /// /root_build/X: This will serve the corresponding file from the build | |
| 58 /// directory (i.e. '$BuildDirectory/X'). | |
| 59 /// /FOO/packages/BAR: This will serve the corresponding file from the packages | |
| 60 /// directory (i.e. '$BuildDirectory/packages/BAR') or the | |
| 61 /// passed-in package root | |
| 62 /// /ws: This will upgrade the connection to a WebSocket connection and echo | |
| 63 /// all data back to the client. | |
| 64 /// | |
| 65 /// In case a path does not refer to a file but rather to a directory, a | |
| 66 /// directory listing will be displayed. | |
| 67 | |
| 68 const PREFIX_BUILDDIR = 'root_build'; | |
| 69 const PREFIX_DARTDIR = 'root_dart'; | |
| 70 | |
| 71 // TODO(kustermann,ricow): We could change this to the following scheme: | |
| 72 // http://host:port/root_packages/X -> $BuildDir/packages/X | |
| 73 // Issue: 8368 | |
| 74 | |
| 75 main(List<String> arguments) { | |
| 76 // This script is in [dart]/tools/testing/dart. | |
| 77 TestUtils.setDartDirUri(Platform.script.resolve('../../..')); | |
| 78 /** Convenience method for local testing. */ | |
| 79 var parser = new ArgParser(); | |
| 80 parser.addOption('port', abbr: 'p', | |
| 81 help: 'The main server port we wish to respond to requests.', | |
| 82 defaultsTo: '0'); | |
| 83 parser.addOption('crossOriginPort', abbr: 'c', | |
| 84 help: 'A different port that accepts request from the main server port.', | |
| 85 defaultsTo: '0'); | |
| 86 parser.addFlag('help', abbr: 'h', negatable: false, | |
| 87 help: 'Print this usage information.'); | |
| 88 parser.addOption('build-directory', help: 'The build directory to use.'); | |
| 89 parser.addOption('package-root', help: 'The package root to use.'); | |
| 90 parser.addOption('network', help: 'The network interface to use.', | |
| 91 defaultsTo: '0.0.0.0'); | |
| 92 parser.addFlag('csp', help: 'Use Content Security Policy restrictions.', | |
| 93 defaultsTo: false); | |
| 94 parser.addOption('runtime', help: 'The runtime we are using (for csp flags).', | |
| 95 defaultsTo: 'none'); | |
| 96 | |
| 97 var args = parser.parse(arguments); | |
| 98 if (args['help']) { | |
| 99 print(parser.getUsage()); | |
| 100 } else { | |
| 101 var servers = new TestingServers(new Path(args['build-directory']), | |
| 102 args['csp'], | |
| 103 args['runtime'], | |
| 104 null, | |
| 105 args['package-root']); | |
| 106 var port = int.parse(args['port']); | |
| 107 var crossOriginPort = int.parse(args['crossOriginPort']); | |
| 108 servers.startServers(args['network'], | |
| 109 port: port, | |
| 110 crossOriginPort: crossOriginPort).then((_) { | |
| 111 DebugLogger.info('Server listening on port ${servers.port}'); | |
| 112 DebugLogger.info('Server listening on port ${servers.crossOriginPort}'); | |
| 113 }); | |
| 114 } | |
| 115 } | |
| 116 | |
| 117 /** | |
| 118 * Runs a set of servers that are initialized specifically for the needs of our | |
| 119 * test framework, such as dealing with package-root. | |
| 120 */ | |
| 121 class TestingServers { | |
| 122 static final _CACHE_EXPIRATION_IN_SECONDS = 30; | |
| 123 static final _HARMLESS_REQUEST_PATH_ENDINGS = [ | |
| 124 "/apple-touch-icon.png", | |
| 125 "/apple-touch-icon-precomposed.png", | |
| 126 "/favicon.ico", | |
| 127 "/foo", | |
| 128 "/bar", | |
| 129 "/NonExistingFile", | |
| 130 "IntentionallyMissingFile", | |
| 131 ]; | |
| 132 | |
| 133 List _serverList = []; | |
| 134 Path _buildDirectory = null; | |
| 135 Path _dartDirectory = null; | |
| 136 Path _packageRoot; | |
| 137 final bool useContentSecurityPolicy; | |
| 138 final String runtime; | |
| 139 DispatchingServer _server; | |
| 140 | |
| 141 TestingServers(Path buildDirectory, | |
| 142 this.useContentSecurityPolicy, | |
| 143 [String this.runtime = 'none', String dartDirectory, | |
| 144 String packageRoot]) { | |
| 145 _buildDirectory = TestUtils.absolutePath(buildDirectory); | |
| 146 _dartDirectory = dartDirectory == null ? TestUtils.dartDir | |
| 147 : new Path(dartDirectory); | |
| 148 _packageRoot = packageRoot == null ? | |
| 149 _buildDirectory.append('packages') : | |
| 150 new Path(packageRoot); | |
| 151 } | |
| 152 | |
| 153 int get port => _serverList[0].port; | |
| 154 int get crossOriginPort => _serverList[1].port; | |
| 155 DispatchingServer get server => _server; | |
| 156 | |
| 157 /** | |
| 158 * [startServers] will start two Http servers. | |
| 159 * The first server listens on [port] and sets | |
| 160 * "Access-Control-Allow-Origin: *" | |
| 161 * The second server listens on [crossOriginPort] and sets | |
| 162 * "Access-Control-Allow-Origin: client:port1 | |
| 163 * "Access-Control-Allow-Credentials: true" | |
| 164 */ | |
| 165 Future startServers(String host, {int port: 0, int crossOriginPort: 0}) { | |
| 166 return _startHttpServer(host, port: port).then((server) { | |
| 167 _server = server; | |
| 168 return _startHttpServer(host, | |
| 169 port: crossOriginPort, | |
| 170 allowedPort:_serverList[0].port); | |
| 171 }); | |
| 172 } | |
| 173 | |
| 174 String httpServerCommandline() { | |
| 175 var dart = TestUtils.dartTestExecutable.toNativePath(); | |
| 176 var dartDir = TestUtils.dartDir; | |
| 177 var script = dartDir.join(new Path("tools/testing/dart/http_server.dart")); | |
| 178 var buildDirectory = _buildDirectory.toNativePath(); | |
| 179 var csp = useContentSecurityPolicy ? '--csp ' : ''; | |
| 180 return '$dart $script -p $port -c $crossOriginPort $csp' | |
| 181 '--build-directory=$buildDirectory --runtime=$runtime ' | |
| 182 '--package-root=$_packageRoot'; | |
| 183 } | |
| 184 | |
| 185 void stopServers() { | |
| 186 for (var server in _serverList) { | |
| 187 server.close(); | |
| 188 } | |
| 189 } | |
| 190 | |
| 191 void _onError(e) { | |
| 192 DebugLogger.error('HttpServer: an error occured', e); | |
| 193 } | |
| 194 | |
| 195 Future _startHttpServer(String host, {int port: 0, int allowedPort: -1}) { | |
| 196 return HttpServer.bind(host, port).then((HttpServer httpServer) { | |
| 197 var server = new DispatchingServer(httpServer, _onError, _sendNotFound); | |
| 198 server.addHandler('/echo', _handleEchoRequest); | |
| 199 server.addHandler('/ws', _handleWebSocketRequest); | |
| 200 fileHandler(request) { | |
| 201 _handleFileOrDirectoryRequest(request, allowedPort); | |
| 202 } | |
| 203 server.addHandler('/$PREFIX_BUILDDIR', fileHandler); | |
| 204 server.addHandler('/$PREFIX_DARTDIR', fileHandler); | |
| 205 server.addHandler('/packages', fileHandler); | |
| 206 _serverList.add(httpServer); | |
| 207 return server; | |
| 208 }); | |
| 209 } | |
| 210 | |
| 211 void _handleFileOrDirectoryRequest(HttpRequest request, | |
| 212 int allowedPort) { | |
| 213 // Enable browsers to cache file/directory responses. | |
| 214 var response = request.response; | |
| 215 response.headers.set("Cache-Control", | |
| 216 "max-age=$_CACHE_EXPIRATION_IN_SECONDS"); | |
| 217 var path = _getFilePathFromRequestPath(request.uri.path); | |
| 218 if (path != null) { | |
| 219 var file = new File(path.toNativePath()); | |
| 220 file.exists().then((exists) { | |
| 221 if (exists) { | |
| 222 _sendFileContent(request, response, allowedPort, path, file); | |
| 223 } else { | |
| 224 var directory = new Directory(path.toNativePath()); | |
| 225 directory.exists().then((exists) { | |
| 226 if (exists) { | |
| 227 _listDirectory(directory).then((entries) { | |
| 228 _sendDirectoryListing(entries, request, response); | |
| 229 }); | |
| 230 } else { | |
| 231 _sendNotFound(request); | |
| 232 } | |
| 233 }); | |
| 234 } | |
| 235 }); | |
| 236 } else { | |
| 237 if (request.uri.path == '/') { | |
| 238 var entries = [new _Entry('root_dart', 'root_dart/'), | |
| 239 new _Entry('root_build', 'root_build/'), | |
| 240 new _Entry('echo', 'echo')]; | |
| 241 _sendDirectoryListing(entries, request, response); | |
| 242 } else { | |
| 243 _sendNotFound(request); | |
| 244 } | |
| 245 } | |
| 246 } | |
| 247 | |
| 248 void _handleEchoRequest(HttpRequest request) { | |
| 249 request.response.headers.set("Access-Control-Allow-Origin", "*"); | |
| 250 request.pipe(request.response).catchError((e) { | |
| 251 DebugLogger.warning( | |
| 252 'HttpServer: error while closing the response stream', e); | |
| 253 }); | |
| 254 } | |
| 255 | |
| 256 void _handleWebSocketRequest(HttpRequest request) { | |
| 257 WebSocketTransformer.upgrade(request).then((websocket) { | |
| 258 // We ignore failures to write to the socket, this happens if the browser | |
| 259 // closes the connection. | |
| 260 websocket.done.catchError((_) {}); | |
| 261 websocket.listen((data) { | |
| 262 websocket.add(data); | |
| 263 websocket.close(); | |
| 264 }, onError: (e) { | |
| 265 DebugLogger.warning('HttpServer: error while echoing to WebSocket', e); | |
| 266 }); | |
| 267 }).catchError((e) { | |
| 268 DebugLogger.warning( | |
| 269 'HttpServer: error while transforming to WebSocket', e); | |
| 270 }); | |
| 271 } | |
| 272 | |
| 273 Path _getFilePathFromRequestPath(String urlRequestPath) { | |
| 274 // Go to the top of the file to see an explanation of the URL path scheme. | |
| 275 var requestPath = new Path(urlRequestPath.substring(1)).canonicalize(); | |
| 276 var pathSegments = requestPath.segments(); | |
| 277 if (pathSegments.length > 0) { | |
| 278 var basePath; | |
| 279 var relativePath; | |
| 280 if (pathSegments[0] == PREFIX_BUILDDIR) { | |
| 281 basePath = _buildDirectory; | |
| 282 relativePath = new Path( | |
| 283 pathSegments.skip(1).join('/')); | |
| 284 } else if (pathSegments[0] == PREFIX_DARTDIR) { | |
| 285 basePath = _dartDirectory; | |
| 286 relativePath = new Path( | |
| 287 pathSegments.skip(1).join('/')); | |
| 288 } | |
| 289 var packagesIndex = pathSegments.indexOf('packages'); | |
| 290 if (packagesIndex != -1) { | |
| 291 var start = packagesIndex + 1; | |
| 292 basePath = _packageRoot; | |
| 293 relativePath = new Path(pathSegments.skip(start).join('/')); | |
| 294 } | |
| 295 if (basePath != null && relativePath != null) { | |
| 296 return basePath.join(relativePath); | |
| 297 } | |
| 298 } | |
| 299 return null; | |
| 300 } | |
| 301 | |
| 302 Future<List<_Entry>> _listDirectory(Directory directory) { | |
| 303 var completer = new Completer(); | |
| 304 var entries = []; | |
| 305 | |
| 306 directory.list().listen( | |
| 307 (FileSystemEntity fse) { | |
| 308 var filename = new Path(fse.path).filename; | |
| 309 if (fse is File) { | |
| 310 entries.add(new _Entry(filename, filename)); | |
| 311 } else if (fse is Directory) { | |
| 312 entries.add(new _Entry(filename, '$filename/')); | |
| 313 } | |
| 314 }, | |
| 315 onDone: () { | |
| 316 completer.complete(entries); | |
| 317 }); | |
| 318 return completer.future; | |
| 319 } | |
| 320 | |
| 321 void _sendDirectoryListing(List<_Entry> entries, | |
| 322 HttpRequest request, | |
| 323 HttpResponse response) { | |
| 324 response.headers.set('Content-Type', 'text/html'); | |
| 325 var header = '''<!DOCTYPE html> | |
| 326 <html> | |
| 327 <head> | |
| 328 <title>${request.uri.path}</title> | |
| 329 </head> | |
| 330 <body> | |
| 331 <code> | |
| 332 <div>${request.uri.path}</div> | |
| 333 <hr/> | |
| 334 <ul>'''; | |
| 335 var footer = ''' | |
| 336 </ul> | |
| 337 </code> | |
| 338 </body> | |
| 339 </html>'''; | |
| 340 | |
| 341 | |
| 342 entries.sort(); | |
| 343 response.write(header); | |
| 344 for (var entry in entries) { | |
| 345 response.write( | |
| 346 '<li><a href="${new Path(request.uri.path).append(entry.name)}">' | |
| 347 '${entry.displayName}</a></li>'); | |
| 348 } | |
| 349 response.write(footer); | |
| 350 response.close(); | |
| 351 response.done.catchError((e) { | |
| 352 DebugLogger.warning( | |
| 353 'HttpServer: error while closing the response stream', e); | |
| 354 }); | |
| 355 } | |
| 356 | |
| 357 void _sendFileContent(HttpRequest request, | |
| 358 HttpResponse response, | |
| 359 int allowedPort, | |
| 360 Path path, | |
| 361 File file) { | |
| 362 if (allowedPort != -1) { | |
| 363 var headerOrigin = request.headers.value('Origin'); | |
| 364 var allowedOrigin; | |
| 365 if (headerOrigin != null) { | |
| 366 var origin = Uri.parse(headerOrigin); | |
| 367 // Allow loading from http://*:$allowedPort in browsers. | |
| 368 allowedOrigin = | |
| 369 '${origin.scheme}://${origin.host}:${allowedPort}'; | |
| 370 } else { | |
| 371 // IE10 appears to be bugged and is not sending the Origin header | |
| 372 // when making CORS requests to the same domain but different port. | |
| 373 allowedOrigin = '*'; | |
| 374 } | |
| 375 | |
| 376 | |
| 377 response.headers.set("Access-Control-Allow-Origin", allowedOrigin); | |
| 378 response.headers.set('Access-Control-Allow-Credentials', 'true'); | |
| 379 } else { | |
| 380 // No allowedPort specified. Allow from anywhere (but cross-origin | |
| 381 // requests *with credentials* will fail because you can't use "*"). | |
| 382 response.headers.set("Access-Control-Allow-Origin", "*"); | |
| 383 } | |
| 384 if (useContentSecurityPolicy) { | |
| 385 // Chrome respects the standardized Content-Security-Policy header, | |
| 386 // whereas Firefox and IE10 use X-Content-Security-Policy. Safari | |
| 387 // still uses the WebKit- prefixed version. | |
| 388 var content_header_value = "script-src 'self'; object-src 'self'"; | |
| 389 for (var header in ["Content-Security-Policy", | |
| 390 "X-Content-Security-Policy"]) { | |
| 391 response.headers.set(header, content_header_value); | |
| 392 } | |
| 393 if (const ["safari"].contains(runtime)) { | |
| 394 response.headers.set("X-WebKit-CSP", content_header_value); | |
| 395 } | |
| 396 } | |
| 397 if (path.filename.endsWith('.html')) { | |
| 398 response.headers.set('Content-Type', 'text/html'); | |
| 399 } else if (path.filename.endsWith('.js')) { | |
| 400 response.headers.set('Content-Type', 'application/javascript'); | |
| 401 } else if (path.filename.endsWith('.dart')) { | |
| 402 response.headers.set('Content-Type', 'application/dart'); | |
| 403 } else if (path.filename.endsWith('.css')) { | |
| 404 response.headers.set('Content-Type', 'text/css'); | |
| 405 } else if (path.filename.endsWith('.xml')) { | |
| 406 response.headers.set('Content-Type', 'text/xml'); | |
| 407 } | |
| 408 response.headers.removeAll("X-Frame-Options"); | |
| 409 file.openRead().pipe(response).catchError((e) { | |
| 410 DebugLogger.warning( | |
| 411 'HttpServer: error while closing the response stream', e); | |
| 412 }); | |
| 413 } | |
| 414 | |
| 415 void _sendNotFound(HttpRequest request) { | |
| 416 bool isHarmlessPath(String path) { | |
| 417 return _HARMLESS_REQUEST_PATH_ENDINGS.any((pattern) { | |
| 418 return path.contains(pattern); | |
| 419 }); | |
| 420 } | |
| 421 if (!isHarmlessPath(request.uri.path)) { | |
| 422 DebugLogger.warning('HttpServer: could not find file for request path: ' | |
| 423 '"${request.uri.path}"'); | |
| 424 } | |
| 425 var response = request.response; | |
| 426 response.statusCode = HttpStatus.NOT_FOUND; | |
| 427 | |
| 428 // Send a nice HTML page detailing the error message. Most browsers expect | |
| 429 // this, for example, Chrome will simply display a blank page if you don't | |
| 430 // provide any information. A nice side effect of this is to work around | |
| 431 // Firefox bug 1016313 | |
| 432 // (https://bugzilla.mozilla.org/show_bug.cgi?id=1016313). | |
| 433 response.headers.set(HttpHeaders.CONTENT_TYPE, 'text/html'); | |
| 434 String escapedPath = const HtmlEscape().convert(request.uri.path); | |
| 435 response.write(""" | |
| 436 <!DOCTYPE html> | |
| 437 <html lang='en'> | |
| 438 <head> | |
| 439 <title>Not Found</title> | |
| 440 </head> | |
| 441 <body> | |
| 442 <h1>Not Found</h1> | |
| 443 <p style='white-space:pre'>The file '$escapedPath\' could not be found.</p> | |
| 444 </body> | |
| 445 </html> | |
| 446 """); | |
| 447 response.close(); | |
| 448 response.done.catchError((e) { | |
| 449 DebugLogger.warning( | |
| 450 'HttpServer: error while closing the response stream', e); | |
| 451 }); | |
| 452 } | |
| 453 } | |
| 454 | |
| 455 // Helper class for displaying directory listings. | |
| 456 class _Entry implements Comparable { | |
| 457 final String name; | |
| 458 final String displayName; | |
| 459 | |
| 460 _Entry(this.name, this.displayName); | |
| 461 | |
| 462 int compareTo(_Entry other) { | |
| 463 return name.compareTo(other.name); | |
| 464 } | |
| 465 } | |
| OLD | NEW |