OLD | NEW |
---|---|
1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
2 // for details. All rights reserved. Use of this source code is governed by a | 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. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 library http_server; | 5 library http_server; |
6 | 6 |
7 import 'dart:async'; | |
7 import 'dart:io'; | 8 import 'dart:io'; |
8 import 'dart:isolate'; | 9 import 'dart:isolate'; |
9 import 'dart:uri'; | 10 import 'dart:uri'; |
10 import 'test_suite.dart'; // For TestUtils. | 11 import 'test_suite.dart'; // For TestUtils. |
11 // TODO(efortuna): Rewrite to not use the args library and simply take an | 12 // TODO(efortuna): Rewrite to not use the args library and simply take an |
12 // expected number of arguments, so test.dart doesn't rely on the args library? | 13 // expected number of arguments, so test.dart doesn't rely on the args library? |
13 // See discussion on https://codereview.chromium.org/11931025/. | 14 // See discussion on https://codereview.chromium.org/11931025/. |
14 import 'vendored_pkg/args/args.dart'; | 15 import 'vendored_pkg/args/args.dart'; |
16 import 'utils.dart'; | |
17 | |
18 | |
19 // Interface of the HTTP server: | |
20 // | |
21 // /echo: This will stream the data received in the request stream back | |
Emily Fortuna
2013/02/16 02:22:01
nit: doc comments should be /// or /*
kustermann
2013/02/18 09:27:02
Done.
| |
22 // to the client. | |
23 // /root_dart/X: This will serve the corresponding file from the dart | |
24 // directory (i.e. '$DartDirectory/X'). | |
25 // /root_build/X: This will serve the corresponding file from the build | |
26 // directory (i.e. '$BuildDirectory/X'). | |
27 // /FOO/packages/BAR: This will serve the corresponding file from the packages | |
28 // directory (i.e. '$BuildDirectory/packages/BAR') | |
29 // | |
30 // In case a path does not refer to a file but rather to a directory, a | |
31 // directory listing will be displayed. | |
32 | |
33 const PREFIX_BUILDDIR = 'root_build'; | |
34 const PREFIX_DARTDIR = 'root_dart'; | |
35 | |
36 // TODO(kustermann,ricow): We could change this to the following scheme: | |
37 // http://host:port/root_packages/X -> $BuildDir/packages/X | |
38 // Issue: 8368 | |
39 | |
15 | 40 |
16 main() { | 41 main() { |
17 /** Convenience method for local testing. */ | 42 /** Convenience method for local testing. */ |
18 var parser = new ArgParser(); | 43 var parser = new ArgParser(); |
19 parser.addOption('port', abbr: 'p', | 44 parser.addOption('port', abbr: 'p', |
20 help: 'The main server port we wish to respond to requests.', | 45 help: 'The main server port we wish to respond to requests.', |
21 defaultsTo: '0'); | 46 defaultsTo: '0'); |
22 parser.addOption('crossOriginPort', abbr: 'c', | 47 parser.addOption('crossOriginPort', abbr: 'c', |
23 help: 'A different port that accepts request from the main server port.', | 48 help: 'A different port that accepts request from the main server port.', |
24 defaultsTo: '0'); | 49 defaultsTo: '0'); |
(...skipping 11 matching lines...) Expand all Loading... | |
36 print(parser.getUsage()); | 61 print(parser.getUsage()); |
37 } else { | 62 } else { |
38 // Pretend we're running test.dart so that TestUtils doesn't get confused | 63 // Pretend we're running test.dart so that TestUtils doesn't get confused |
39 // about the "current directory." This is only used if we're trying to run | 64 // about the "current directory." This is only used if we're trying to run |
40 // this file independently for local testing. | 65 // this file independently for local testing. |
41 TestUtils.testScriptPath = new Path(new Options().script) | 66 TestUtils.testScriptPath = new Path(new Options().script) |
42 .directoryPath | 67 .directoryPath |
43 .join(new Path('../../test.dart')) | 68 .join(new Path('../../test.dart')) |
44 .canonicalize() | 69 .canonicalize() |
45 .toNativePath(); | 70 .toNativePath(); |
71 // Note: args['package-root'] is always the build directory. We have the | |
72 // implicit assumption that it contains the 'packages' subdirectory. | |
73 // TODO: We should probably rename 'package-root' to 'build-directory'. | |
46 TestingServerRunner._packageRootDir = new Path(args['package-root']); | 74 TestingServerRunner._packageRootDir = new Path(args['package-root']); |
75 TestingServerRunner._buildDirectory = new Path(args['package-root']); | |
47 var network = args['network']; | 76 var network = args['network']; |
48 TestingServerRunner.startHttpServer(network, | 77 TestingServerRunner.startHttpServer(network, |
49 port: int.parse(args['port'])); | 78 port: int.parse(args['port'])); |
50 print('Server listening on port ' | 79 print('Server listening on port ' |
51 '${TestingServerRunner.serverList[0].port}.'); | 80 '${TestingServerRunner.serverList[0].port}.'); |
52 TestingServerRunner.startHttpServer(network, | 81 TestingServerRunner.startHttpServer(network, |
53 allowedPort: TestingServerRunner.serverList[0].port, port: | 82 allowedPort: TestingServerRunner.serverList[0].port, port: |
54 int.parse(args['crossOriginPort'])); | 83 int.parse(args['crossOriginPort'])); |
55 print( | 84 print( |
56 'Server listening on port ${TestingServerRunner.serverList[1].port}.'); | 85 'Server listening on port ${TestingServerRunner.serverList[1].port}.'); |
57 } | 86 } |
58 } | 87 } |
59 /** | 88 /** |
60 * Runs a set of servers that are initialized specifically for the needs of our | 89 * Runs a set of servers that are initialized specifically for the needs of our |
61 * test framework, such as dealing with package-root. | 90 * test framework, such as dealing with package-root. |
62 */ | 91 */ |
63 class TestingServerRunner { | 92 class TestingServerRunner { |
64 static List serverList = []; | 93 static List serverList = []; |
65 static Path _packageRootDir = null; | 94 static Path _packageRootDir = null; |
95 static Path _buildDirectory = null; | |
66 | 96 |
67 // Added as a getter so that the function will be called again each time the | 97 // Added as a getter so that the function will be called again each time the |
68 // default request handler closure is executed. | 98 // default request handler closure is executed. |
69 static Path get packageRootDir => _packageRootDir; | 99 static Path get packageRootDir => _packageRootDir; |
100 static Path get buildDirectory => _buildDirectory; | |
70 | 101 |
71 static setPackageRootDir(Map configuration) { | 102 static setPackageRootDir(Map configuration) { |
72 _packageRootDir = TestUtils.currentWorkingDirectory.join( | 103 _packageRootDir = TestUtils.absolutePath( |
104 new Path(TestUtils.buildDir(configuration))); | |
105 } | |
106 | |
107 static setBuildDir(Map configuration) { | |
108 _buildDirectory = TestUtils.absolutePath( | |
73 new Path(TestUtils.buildDir(configuration))); | 109 new Path(TestUtils.buildDir(configuration))); |
74 } | 110 } |
75 | 111 |
76 static startHttpServer(String host, {int allowedPort:-1, int port: 0}) { | 112 static startHttpServer(String host, {int allowedPort:-1, int port: 0}) { |
77 var basePath = TestUtils.dartDir(); | |
78 var httpServer = new HttpServer(); | 113 var httpServer = new HttpServer(); |
79 var packagesDirName = 'packages'; | |
80 httpServer.onError = (e) { | 114 httpServer.onError = (e) { |
81 // TODO(ricow): Once we have a debug log we should write this out there. | 115 DebugLogger.error('HttpServer: an error occured: $e'); |
82 print('Test http server error: $e'); | |
83 }; | 116 }; |
84 httpServer.defaultRequestHandler = (request, resp) { | 117 httpServer.defaultRequestHandler = (request, response) { |
85 var requestPath = new Path(request.path.substring(1)).canonicalize(); | 118 handleFileOrDirectoryRequest(request, response, allowedPort); |
86 var path = basePath.join(requestPath); | |
87 var file = new File(path.toNativePath()); | |
88 | |
89 if (requestPath.segments().contains(packagesDirName)) { | |
90 // Essentially implement the packages path rewriting, so we don't have | |
91 // to pass environment variables to the browsers. | |
92 var requestPathStr = requestPath.toNativePath().substring( | |
93 requestPath.toNativePath().indexOf(packagesDirName)); | |
94 path = packageRootDir.append(requestPathStr); | |
95 file = new File(path.toNativePath()); | |
96 } | |
97 file.exists().then((exists) { | |
98 if (exists) { | |
99 if (allowedPort != -1) { | |
100 if (request.headers.value('Origin') != null) { | |
101 var origin = new Uri(request.headers.value('Origin')); | |
102 // Allow loading from http://*:$allowedPort in browsers. | |
103 var allowedOrigin = | |
104 '${origin.scheme}://${origin.domain}:${allowedPort}'; | |
105 resp.headers.set("Access-Control-Allow-Origin", allowedOrigin); | |
106 resp.headers.set('Access-Control-Allow-Credentials', 'true'); | |
107 } | |
108 } else { | |
109 // No allowedPort specified. Allow from anywhere (but cross-origin | |
110 // requests *with credentials* will fail because you can't use "*"). | |
111 resp.headers.set("Access-Control-Allow-Origin", "*"); | |
112 } | |
113 if (path.toNativePath().endsWith('.html')) { | |
114 resp.headers.set('Content-Type', 'text/html'); | |
115 } else if (path.toNativePath().endsWith('.js')) { | |
116 resp.headers.set('Content-Type', 'application/javascript'); | |
117 } else if (path.toNativePath().endsWith('.dart')) { | |
118 resp.headers.set('Content-Type', 'application/dart'); | |
119 } | |
120 file.openInputStream().pipe(resp.outputStream); | |
121 } else { | |
122 var directory = new Directory.fromPath(path); | |
123 directory.exists().then((exists) { | |
124 if (!exists) { | |
125 sendNotFound(resp); | |
126 } else { | |
127 sendDirectoryListing(directory, request, resp); | |
128 } | |
129 }); | |
130 } | |
131 }); | |
132 }; | 119 }; |
133 | 120 httpServer.addRequestHandler( |
134 // Echos back the contents of the request as the response data. | 121 (req) => req.path == "/echo", handleEchoRequest); |
135 httpServer.addRequestHandler((req) => req.path == "/echo", (request, resp) { | |
136 resp.headers.set("Access-Control-Allow-Origin", "*"); | |
137 | |
138 request.inputStream.pipe(resp.outputStream); | |
139 }); | |
140 | 122 |
141 httpServer.listen(host, port); | 123 httpServer.listen(host, port); |
142 serverList.add(httpServer); | 124 serverList.add(httpServer); |
143 } | 125 } |
144 | 126 |
145 static void sendNotFound(HttpResponse response) { | 127 |
146 response.statusCode = HttpStatus.NOT_FOUND; | 128 static void handleFileOrDirectoryRequest(HttpRequest request, |
147 try { | 129 HttpResponse response, |
148 response.outputStream.close(); | 130 int allowedPort) { |
149 } catch (e) { | 131 var path = getFilePathFromRequestPath(request.path); |
150 if (e is StreamException) { | 132 if (path != null) { |
151 print('Test http_server error closing the response stream: $e'); | 133 var file = new File.fromPath(path); |
134 file.exists().then((exists) { | |
135 if (exists) { | |
136 sendFileContent(request, response, allowedPort, path, file); | |
137 } else { | |
138 var directory = new Directory.fromPath(path); | |
139 directory.exists().then((exists) { | |
140 if (exists) { | |
141 listDirectory(directory).then((entries) { | |
142 sendDirectoryListing(entries, request, response); | |
143 }); | |
144 } else { | |
145 sendNotFound(request, response); | |
146 } | |
147 }); | |
148 } | |
149 }); | |
150 } else { | |
151 if (request.path == '/') { | |
152 var entries = [new _Entry('root_dart', 'root_dart/'), | |
153 new _Entry('root_build', 'root_build/'), | |
154 new _Entry('echo', 'echo')]; | |
155 sendDirectoryListing(entries, request, response); | |
152 } else { | 156 } else { |
153 throw e; | 157 sendNotFound(request, response); |
154 } | 158 } |
155 } | 159 } |
156 } | 160 } |
157 | 161 |
162 static void handleEchoRequest(HttpRequest request, HttpResponse response) { | |
163 response.headers.set("Access-Control-Allow-Origin", "*"); | |
164 request.inputStream.pipe(response.outputStream); | |
165 } | |
166 | |
167 static Path getFilePathFromRequestPath(String urlRequestPath) { | |
168 // Go to the top of the file to see an explanation of the URL path scheme. | |
169 var requestPath = new Path(urlRequestPath.substring(1)).canonicalize(); | |
170 var pathSegments = requestPath.segments(); | |
171 if (pathSegments.length > 0) { | |
172 var basePath; | |
173 var relativePath; | |
174 if (pathSegments[0] == PREFIX_BUILDDIR) { | |
175 basePath = _buildDirectory; | |
176 relativePath = new Path( | |
177 pathSegments.getRange(1, pathSegments.length - 1).join('/')); | |
178 } else if (pathSegments[0] == PREFIX_DARTDIR) { | |
179 basePath = TestUtils.dartDir(); | |
180 relativePath = new Path( | |
181 pathSegments.getRange(1, pathSegments.length - 1).join('/')); | |
182 } | |
183 var packagesDirName = 'packages'; | |
184 var packagesIndex = pathSegments.indexOf(packagesDirName); | |
185 if (packagesIndex != -1) { | |
186 var start = packagesIndex + 1; | |
187 var length = pathSegments.length - start; | |
188 basePath = _packageRootDir.append(packagesDirName); | |
189 relativePath = new Path( | |
190 pathSegments.getRange(start, length).join('/')); | |
191 } | |
192 if (basePath != null && relativePath != null) { | |
193 return basePath.join(relativePath); | |
194 } | |
195 } | |
196 return null; | |
197 } | |
198 | |
199 static Future<List<_Entry>> listDirectory(Directory directory) { | |
200 var completer = new Completer(); | |
201 var entries = []; | |
202 | |
203 directory.list() | |
204 ..onFile = (filepath) { | |
205 var filename = new Path(filepath).filename; | |
206 entries.add(new _Entry(filename, filename)); | |
207 } | |
208 ..onDir = (dirpath) { | |
209 var filename = new Path(dirpath).filename; | |
210 entries.add(new _Entry(filename, '$filename/')); | |
211 } | |
212 ..onDone = (_) { | |
213 completer.complete(entries); | |
214 }; | |
215 return completer.future; | |
216 } | |
217 | |
158 /** | 218 /** |
159 * Sends a simple listing of all the files and sub-directories within | 219 * Sends a simple listing of all the files and sub-directories within |
160 * directory. | 220 * directory. |
161 * | 221 * |
162 * This is intended to make it easier to browse tests when manually running | 222 * This is intended to make it easier to browse tests when manually running |
163 * tests against this test server. | 223 * tests against this test server. |
164 */ | 224 */ |
165 static void sendDirectoryListing(Directory directory, HttpRequest request, | 225 static void sendDirectoryListing(entries, |
166 HttpResponse response) { | 226 HttpRequest request, |
227 HttpResponse response) { | |
167 response.headers.set('Content-Type', 'text/html'); | 228 response.headers.set('Content-Type', 'text/html'); |
168 var header = '''<!DOCTYPE html> | 229 var header = '''<!DOCTYPE html> |
169 <html> | 230 <html> |
170 <head> | 231 <head> |
171 <title>${request.path}</title> | 232 <title>${request.path}</title> |
172 </head> | 233 </head> |
173 <body> | 234 <body> |
174 <code> | 235 <code> |
175 <div>${request.path}</div> | 236 <div>${request.path}</div> |
176 <hr/> | 237 <hr/> |
177 <ul>'''; | 238 <ul>'''; |
178 var footer = ''' | 239 var footer = ''' |
179 </ul> | 240 </ul> |
180 </code> | 241 </code> |
181 </body> | 242 </body> |
182 </html>'''; | 243 </html>'''; |
183 | 244 |
184 var entries = []; | |
185 | 245 |
186 directory.list() | 246 entries.sort(); |
187 ..onFile = (filepath) { | 247 response.outputStream.writeString(header); |
188 var filename = new Path(filepath).filename; | 248 for (var entry in entries) { |
189 entries.add(new _Entry(filename, filename)); | 249 response.outputStream.writeString( |
250 '<li><a href="${new Path(request.path).append(entry.name)}">' | |
251 '${entry.displayName}</a></li>'); | |
252 } | |
253 response.outputStream.writeString(footer); | |
254 response.outputStream.close(); | |
255 } | |
256 | |
257 static void sendFileContent(HttpRequest request, | |
258 HttpResponse response, | |
259 int allowedPort, | |
260 Path path, | |
261 File file) { | |
262 if (allowedPort != -1) { | |
263 var origin = new Uri(request.headers.value('Origin')); | |
264 // Allow loading from http://*:$allowedPort in browsers. | |
265 var allowedOrigin = | |
266 '${origin.scheme}://${origin.domain}:${allowedPort}'; | |
267 response.headers.set("Access-Control-Allow-Origin", allowedOrigin); | |
268 response.headers.set('Access-Control-Allow-Credentials', 'true'); | |
269 } else { | |
270 // No allowedPort specified. Allow from anywhere (but cross-origin | |
271 // requests *with credentials* will fail because you can't use "*"). | |
272 response.headers.set("Access-Control-Allow-Origin", "*"); | |
273 } | |
274 if (path.filename.endsWith('.html')) { | |
275 response.headers.set('Content-Type', 'text/html'); | |
276 } else if (path.filename.endsWith('.js')) { | |
277 response.headers.set('Content-Type', 'application/javascript'); | |
278 } else if (path.filename.endsWith('.dart')) { | |
279 response.headers.set('Content-Type', 'application/dart'); | |
280 } | |
281 file.openInputStream().pipe(response.outputStream); | |
282 } | |
283 | |
284 static void sendNotFound(HttpRequest request, HttpResponse response) { | |
285 // NOTE: Since some tests deliberately try to access non-existent files. | |
286 // We might want to remove this warning (otherwise it will show | |
287 // up in the debug.log every time). | |
288 DebugLogger.warning('HttpServer: could not find file for request path: ' | |
289 '"${request.path}"'); | |
290 response.statusCode = HttpStatus.NOT_FOUND; | |
291 try { | |
292 response.outputStream.close(); | |
293 } catch (e) { | |
294 if (e is StreamException) { | |
295 DebugLogger.warning('HttpServer: error while closing the response ' | |
296 'stream: $e'); | |
297 } else { | |
298 throw e; | |
190 } | 299 } |
191 ..onDir = (dirpath) { | 300 } |
192 var filename = new Path(dirpath).filename; | |
193 entries.add(new _Entry(filename, '$filename/')); | |
194 } | |
195 ..onDone = (_) { | |
196 var requestPath = new Path.raw(request.path); | |
197 entries.sort(); | |
198 | |
199 response.outputStream.writeString(header); | |
200 for (var entry in entries) { | |
201 response.outputStream.writeString( | |
202 '<li><a href="${requestPath.append(entry.name)}">' | |
203 '${entry.displayName}</a></li>'); | |
204 } | |
205 response.outputStream.writeString(footer); | |
206 response.outputStream.close(); | |
207 }; | |
208 } | 301 } |
209 | 302 |
210 static terminateHttpServers() { | 303 static terminateHttpServers() { |
211 for (var server in serverList) server.close(); | 304 for (var server in serverList) server.close(); |
212 } | 305 } |
213 } | 306 } |
214 | 307 |
215 // Helper class for displaying directory listings. | 308 // Helper class for displaying directory listings. |
216 class _Entry { | 309 class _Entry { |
217 final String name; | 310 final String name; |
218 final String displayName; | 311 final String displayName; |
219 | 312 |
220 _Entry(this.name, this.displayName); | 313 _Entry(this.name, this.displayName); |
221 | 314 |
222 int compareTo(_Entry other) { | 315 int compareTo(_Entry other) { |
223 return name.compareTo(other.name); | 316 return name.compareTo(other.name); |
224 } | 317 } |
225 } | 318 } |
OLD | NEW |