Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(399)

Side by Side Diff: tools/testing/dart/http_server.dart

Issue 841193003: cleanup to tools/testing/dart (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: one last bit Created 5 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « tools/testing/dart/html_test.dart ('k') | tools/testing/dart/launch_browser.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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 }
OLDNEW
« no previous file with comments | « tools/testing/dart/html_test.dart ('k') | tools/testing/dart/launch_browser.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698