Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 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 | 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 part of http_server; | 5 part of http_server; |
| 6 | 6 |
| 7 | 7 |
| 8 // Used for signal a directory redirecting, where a tailing slash is missing. | 8 // Used for signal a directory redirecting, where a tailing slash is missing. |
| 9 class _DirectoryRedirect { | 9 class _DirectoryRedirect { |
| 10 const _DirectoryRedirect(); | 10 const _DirectoryRedirect(); |
| 11 } | 11 } |
| 12 | 12 |
| 13 typedef dynamic _DirCallback(Directory dir, HttpRequest request); | |
| 14 typedef dynamic _ErrorCallback(HttpRequest request); | |
| 15 | |
| 13 /** | 16 /** |
| 14 * A [VirtualDirectory] can serve files and directory-listing from a root path, | 17 * A [VirtualDirectory] can serve files and directory-listing from a root path, |
| 15 * to [HttpRequest]s. | 18 * to [HttpRequest]s. |
| 16 * | 19 * |
| 17 * The [VirtualDirectory] providing secure handling of request uris and | 20 * The [VirtualDirectory] providing secure handling of request uris and |
| 18 * file-system links, correct mime-types and custom error pages. | 21 * file-system links, correct mime-types and custom error pages. |
| 19 */ | 22 */ |
| 20 class VirtualDirectory { | 23 class VirtualDirectory { |
| 21 final String root; | 24 final String root; |
| 22 | 25 |
| (...skipping 10 matching lines...) Expand all Loading... | |
| 33 bool followLinks = true; | 36 bool followLinks = true; |
| 34 | 37 |
| 35 /** | 38 /** |
| 36 * Set or get if the [VirtualDirectory] should jail the root. When the root is | 39 * Set or get if the [VirtualDirectory] should jail the root. When the root is |
| 37 * not jailed, links can be followed to outside the [root] directory. | 40 * not jailed, links can be followed to outside the [root] directory. |
| 38 */ | 41 */ |
| 39 bool jailRoot = true; | 42 bool jailRoot = true; |
| 40 | 43 |
| 41 final RegExp _invalidPathRegExp = new RegExp("[\\\/\x00]"); | 44 final RegExp _invalidPathRegExp = new RegExp("[\\\/\x00]"); |
| 42 | 45 |
| 43 Function _errorCallback; | 46 _ErrorCallback _errorCallback; |
| 44 Function _dirCallback; | 47 _DirCallback _dirCallback; |
| 45 | 48 |
| 46 /* | 49 /* |
| 47 * Create a new [VirtualDirectory] for serving static file content of | 50 * Create a new [VirtualDirectory] for serving static file content of |
| 48 * the path [root]. | 51 * the path [root]. |
| 49 * | 52 * |
| 50 * The [root] is not required to exist. If the [root] doesn't exist at time of | 53 * The [root] is not required to exist. If the [root] doesn't exist at time of |
| 51 * a request, a 404 is generated. | 54 * a request, a 404 is generated. |
| 52 */ | 55 */ |
| 53 VirtualDirectory(this.root); | 56 VirtualDirectory(this.root); |
| 54 | 57 |
| 55 /** | 58 /** |
| 56 * Serve a [Stream] of [HttpRequest]s, in this [VirtualDirectory]. | 59 * Serve a [Stream] of [HttpRequest]s, in this [VirtualDirectory]. |
| 57 */ | 60 */ |
| 58 void serve(Stream<HttpRequest> requests) { | 61 StreamSubscription<HttpRequest> serve(Stream<HttpRequest> requests) => |
| 59 requests.listen(serveRequest); | 62 requests.listen(serveRequest); |
| 60 } | |
| 61 | 63 |
| 62 /** | 64 /** |
| 63 * Serve a single [HttpRequest], in this [VirtualDirectory]. | 65 * Serve a single [HttpRequest], in this [VirtualDirectory]. |
| 64 */ | 66 */ |
| 65 void serveRequest(HttpRequest request) { | 67 Future serveRequest(HttpRequest request) { |
| 66 _locateResource('.', request.uri.pathSegments.iterator..moveNext()) | 68 return _locateResource('.', request.uri.pathSegments.iterator..moveNext()) |
| 67 .then((entity) { | 69 .then((entity) { |
| 68 if (entity == null) { | |
| 69 _serveErrorPage(HttpStatus.NOT_FOUND, request); | |
| 70 return; | |
| 71 } | |
| 72 if (entity is File) { | 70 if (entity is File) { |
| 73 serveFile(entity, request); | 71 serveFile(entity, request); |
| 74 } else if (entity is Directory) { | 72 } else if (entity is Directory) { |
| 75 if (allowDirectoryListing) { | 73 if (allowDirectoryListing) { |
| 76 _serveDirectory(entity, request); | 74 _serveDirectory(entity, request); |
| 77 } else { | 75 } else { |
| 78 _serveErrorPage(HttpStatus.NOT_FOUND, request); | 76 _serveErrorPage(HttpStatus.NOT_FOUND, request); |
| 79 } | 77 } |
| 80 } else if (entity is _DirectoryRedirect) { | 78 } else if (entity is _DirectoryRedirect) { |
| 81 // TODO(ajohnsen): Use HttpRequest.requestedUri once 1.2 is out. | 79 // TODO(ajohnsen): Use HttpRequest.requestedUri once 1.2 is out. |
| 82 request.response.redirect(Uri.parse('${request.uri}/'), | 80 request.response.redirect(Uri.parse('${request.uri}/'), |
| 83 status: HttpStatus.MOVED_PERMANENTLY); | 81 status: HttpStatus.MOVED_PERMANENTLY); |
| 84 } else { | 82 } else { |
| 83 assert(entity == null); | |
| 85 _serveErrorPage(HttpStatus.NOT_FOUND, request); | 84 _serveErrorPage(HttpStatus.NOT_FOUND, request); |
| 86 } | 85 } |
| 86 return request.response.done; | |
| 87 }); | 87 }); |
| 88 } | 88 } |
| 89 | 89 |
| 90 /** | 90 /** |
| 91 * Set the [callback] to override the default directory listing. The | 91 * Set the [callback] to override the default directory listing. The |
| 92 * [callback] will be called with the [Directory] to be listed and the | 92 * [callback] will be called with the [Directory] to be listed and the |
| 93 * [HttpRequest]. | 93 * [HttpRequest]. |
| 94 */ | 94 */ |
| 95 void set directoryHandler(void callback(Directory dir, HttpRequest request)) { | 95 void set directoryHandler(void callback(Directory dir, HttpRequest request)) { |
| 96 _dirCallback = callback; | 96 _dirCallback = callback; |
| (...skipping 75 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 172 * is closed with error-code [HttpStatus.NOT_FOUND]. | 172 * is closed with error-code [HttpStatus.NOT_FOUND]. |
| 173 */ | 173 */ |
| 174 void serveFile(File file, HttpRequest request) { | 174 void serveFile(File file, HttpRequest request) { |
| 175 var response = request.response; | 175 var response = request.response; |
| 176 // TODO(ajohnsen): Set up Zone support for these errors. | 176 // TODO(ajohnsen): Set up Zone support for these errors. |
| 177 file.lastModified().then((lastModified) { | 177 file.lastModified().then((lastModified) { |
| 178 if (request.headers.ifModifiedSince != null && | 178 if (request.headers.ifModifiedSince != null && |
| 179 !lastModified.isAfter(request.headers.ifModifiedSince)) { | 179 !lastModified.isAfter(request.headers.ifModifiedSince)) { |
| 180 response.statusCode = HttpStatus.NOT_MODIFIED; | 180 response.statusCode = HttpStatus.NOT_MODIFIED; |
| 181 response.close(); | 181 response.close(); |
| 182 return; | 182 return null; |
| 183 } | 183 } |
| 184 | 184 |
| 185 response.headers.set(HttpHeaders.LAST_MODIFIED, lastModified); | 185 response.headers.set(HttpHeaders.LAST_MODIFIED, lastModified); |
| 186 response.headers.set(HttpHeaders.ACCEPT_RANGES, "bytes"); | 186 response.headers.set(HttpHeaders.ACCEPT_RANGES, "bytes"); |
| 187 | 187 |
| 188 if (request.method == 'HEAD') { | 188 if (request.method == 'HEAD') { |
| 189 response.close(); | 189 response.close(); |
| 190 return; | 190 return null; |
| 191 } | 191 } |
| 192 | 192 |
| 193 return file.length().then((length) { | 193 return file.length().then((length) { |
| 194 String range = request.headers.value("range"); | 194 String range = request.headers.value("range"); |
| 195 if (range != null) { | 195 if (range != null) { |
| 196 // We only support one range, where the standard support several. | 196 // We only support one range, where the standard support several. |
| 197 Match matches = new RegExp(r"^bytes=(\d*)\-(\d*)$").firstMatch(range); | 197 Match matches = new RegExp(r"^bytes=(\d*)\-(\d*)$").firstMatch(range); |
| 198 // If the range header have the right format, handle it. | 198 // If the range header have the right format, handle it. |
| 199 if (matches != null) { | 199 if (matches != null) { |
| 200 // Serve sub-range. | 200 // Serve sub-range. |
| (...skipping 13 matching lines...) Expand all Loading... | |
| 214 response.headers.set(HttpHeaders.CONTENT_LENGTH, end - start); | 214 response.headers.set(HttpHeaders.CONTENT_LENGTH, end - start); |
| 215 | 215 |
| 216 // Set 'Partial Content' status code. | 216 // Set 'Partial Content' status code. |
| 217 response.statusCode = HttpStatus.PARTIAL_CONTENT; | 217 response.statusCode = HttpStatus.PARTIAL_CONTENT; |
| 218 response.headers.set(HttpHeaders.CONTENT_RANGE, | 218 response.headers.set(HttpHeaders.CONTENT_RANGE, |
| 219 "bytes $start-${end - 1}/$length"); | 219 "bytes $start-${end - 1}/$length"); |
| 220 | 220 |
| 221 // Pipe the 'range' of the file. | 221 // Pipe the 'range' of the file. |
| 222 file.openRead(start, end) | 222 file.openRead(start, end) |
| 223 .pipe(new _VirtualDirectoryFileStream(response, file.path)) | 223 .pipe(new _VirtualDirectoryFileStream(response, file.path)) |
| 224 .catchError((_) {}); | 224 .catchError((_) { |
| 225 // TODO(kevmoo): log errors | |
| 226 }); | |
| 225 return; | 227 return; |
| 226 } | 228 } |
| 227 } | 229 } |
| 228 | 230 |
| 229 file.openRead() | 231 file.openRead() |
| 230 .pipe(new _VirtualDirectoryFileStream(response, file.path)) | 232 .pipe(new _VirtualDirectoryFileStream(response, file.path)) |
| 231 .catchError((_) {}); | 233 .catchError((_) { |
| 234 // TODO(kevmoo): log errors | |
| 235 }); | |
| 232 }); | 236 }); |
| 233 }).catchError((_) { | 237 }).catchError((_) { |
| 234 response.statusCode = HttpStatus.NOT_FOUND; | 238 response.statusCode = HttpStatus.NOT_FOUND; |
| 235 response.close(); | 239 response.close(); |
| 236 }); | 240 }); |
| 237 } | 241 } |
| 238 | 242 |
| 239 void _serveDirectory(Directory dir, HttpRequest request) { | 243 void _serveDirectory(Directory dir, HttpRequest request) { |
| 240 if (_dirCallback != null) { | 244 if (_dirCallback != null) { |
| 241 _dirCallback(dir, request); | 245 _dirCallback(dir, request); |
| 242 return; | 246 return; |
| 243 } | 247 } |
| 244 var response = request.response; | 248 var response = request.response; |
| 245 dir.stat().then((stats) { | 249 dir.stat().then((stats) { |
| 246 if (request.headers.ifModifiedSince != null && | 250 if (request.headers.ifModifiedSince != null && |
| 247 !stats.modified.isAfter(request.headers.ifModifiedSince)) { | 251 !stats.modified.isAfter(request.headers.ifModifiedSince)) { |
| 248 response.statusCode = HttpStatus.NOT_MODIFIED; | 252 response.statusCode = HttpStatus.NOT_MODIFIED; |
| 249 response.close(); | 253 return null; |
| 250 return; | |
| 251 } | 254 } |
| 252 | 255 |
| 253 response.headers.set(HttpHeaders.LAST_MODIFIED, stats.modified); | 256 response.headers.set(HttpHeaders.LAST_MODIFIED, stats.modified); |
| 254 var path = request.uri.path; | 257 var path = request.uri.path; |
| 255 var header = | 258 var header = |
| 256 '''<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" | 259 '''<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" |
| 257 http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> | 260 http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> |
| 258 <html xmlns="http://www.w3.org/1999/xhtml"> | 261 <html xmlns="http://www.w3.org/1999/xhtml"> |
| 259 <head> | 262 <head> |
| 260 <title>Index of $path</title> | 263 <title>Index of $path</title> |
| (...skipping 28 matching lines...) Expand all Loading... | |
| 289 <td>$modified</td> | 292 <td>$modified</td> |
| 290 <td style="text-align: right">$size</td> | 293 <td style="text-align: right">$size</td> |
| 291 </tr>'''; | 294 </tr>'''; |
| 292 response.write(entry); | 295 response.write(entry); |
| 293 } | 296 } |
| 294 | 297 |
| 295 if (path != '/') { | 298 if (path != '/') { |
| 296 add('../', null, null); | 299 add('../', null, null); |
| 297 } | 300 } |
| 298 | 301 |
| 299 dir.list(followLinks: true).listen((entity) { | 302 return dir.list(followLinks: true) |
| 300 // TODO(ajohnsen): Consider async dir listing. | 303 .handleError((error) { |
|
Anders Johnsen
2014/01/10 18:19:45
Why is this better than the old version. Its longe
kevmoo
2014/01/10 18:51:24
Went back to old impl...it was just personal prefe
Anders Johnsen
2014/01/10 18:53:35
Yep :)
| |
| 301 if (entity is File) { | 304 // TODO(kevmoo): log errors; |
| 302 var stat = entity.statSync(); | 305 }) |
| 303 add(basename(entity.path), | 306 .forEach((entity) { |
| 304 stat.modified.toString(), | 307 // TODO(ajohnsen): Consider async dir listing. |
| 305 stat.size); | 308 if (entity is File) { |
| 306 } else if (entity is Directory) { | 309 var stat = entity.statSync(); |
| 307 add(basename(entity.path) + '/', | 310 add(basename(entity.path), |
| 308 entity.statSync().modified.toString(), | 311 stat.modified.toString(), |
| 309 null); | 312 stat.size); |
| 310 } | 313 } else if (entity is Directory) { |
| 311 }, onError: (e) { | 314 add(basename(entity.path) + '/', |
| 312 }, onDone: () { | 315 entity.statSync().modified.toString(), |
| 313 response.write(footer); | 316 null); |
| 314 response.close(); | 317 } |
| 315 }); | 318 }) |
| 316 }, onError: (e) => response.close()); | 319 .then((_) { |
| 320 response.write(footer); | |
| 321 }); | |
| 322 }) | |
| 323 .catchError((error) { | |
| 324 // TODO(kevmoo): log errors | |
| 325 }) | |
| 326 .whenComplete(() { | |
| 327 response.close(); | |
| 328 }); | |
| 317 } | 329 } |
| 318 | 330 |
| 319 void _serveErrorPage(int error, HttpRequest request) { | 331 void _serveErrorPage(int error, HttpRequest request) { |
| 320 var response = request.response; | 332 var response = request.response; |
| 321 response.statusCode = error; | 333 response.statusCode = error; |
| 322 if (_errorCallback != null) { | 334 if (_errorCallback != null) { |
| 323 _errorCallback(request); | 335 _errorCallback(request); |
| 324 return; | 336 return; |
| 325 } | 337 } |
| 326 // Default error page. | 338 // Default error page. |
| (...skipping 15 matching lines...) Expand all Loading... | |
| 342 </body> | 354 </body> |
| 343 </html>'''; | 355 </html>'''; |
| 344 response.write(page); | 356 response.write(page); |
| 345 response.close(); | 357 response.close(); |
| 346 } | 358 } |
| 347 } | 359 } |
| 348 | 360 |
| 349 class _VirtualDirectoryFileStream extends StreamConsumer<List<int>> { | 361 class _VirtualDirectoryFileStream extends StreamConsumer<List<int>> { |
| 350 final HttpResponse response; | 362 final HttpResponse response; |
| 351 final String path; | 363 final String path; |
| 352 var buffer = []; | 364 List<int> buffer = []; |
| 353 | 365 |
| 354 _VirtualDirectoryFileStream(HttpResponse this.response, String this.path); | 366 _VirtualDirectoryFileStream(HttpResponse this.response, String this.path); |
| 355 | 367 |
| 356 Future addStream(Stream<List<int>> stream) { | 368 Future addStream(Stream<List<int>> stream) { |
| 357 stream.listen( | 369 stream.listen( |
| 358 (data) { | 370 (data) { |
| 359 if (buffer == null) { | 371 if (buffer == null) { |
| 360 response.add(data); | 372 response.add(data); |
| 361 return; | 373 return; |
| 362 } | 374 } |
| (...skipping 24 matching lines...) Expand all Loading... | |
| 387 } | 399 } |
| 388 } | 400 } |
| 389 response.close(); | 401 response.close(); |
| 390 }, | 402 }, |
| 391 onError: response.addError); | 403 onError: response.addError); |
| 392 return response.done; | 404 return response.done; |
| 393 } | 405 } |
| 394 | 406 |
| 395 Future close() => new Future.value(); | 407 Future close() => new Future.value(); |
| 396 | 408 |
| 397 void setMimeType(var bytes) { | 409 void setMimeType(List<int> bytes) { |
| 398 var mimeType = lookupMimeType(path, headerBytes: bytes); | 410 var mimeType = lookupMimeType(path, headerBytes: bytes); |
| 399 if (mimeType != null) { | 411 if (mimeType != null) { |
| 400 response.headers.contentType = ContentType.parse(mimeType); | 412 response.headers.contentType = ContentType.parse(mimeType); |
| 401 } | 413 } |
| 402 } | 414 } |
| 403 } | 415 } |
| OLD | NEW |