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 |
| 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 |