| 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); |
| (...skipping 48 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 290 <td style="text-align: right">$size</td> | 294 <td style="text-align: right">$size</td> |
| 291 </tr>'''; | 295 </tr>'''; |
| 292 response.write(entry); | 296 response.write(entry); |
| 293 } | 297 } |
| 294 | 298 |
| 295 if (path != '/') { | 299 if (path != '/') { |
| 296 add('../', null, null); | 300 add('../', null, null); |
| 297 } | 301 } |
| 298 | 302 |
| 299 dir.list(followLinks: true).listen((entity) { | 303 dir.list(followLinks: true).listen((entity) { |
| 300 // TODO(ajohnsen): Consider async dir listing. | |
| 301 if (entity is File) { | 304 if (entity is File) { |
| 302 var stat = entity.statSync(); | 305 var stat = entity.statSync(); |
| 303 add(basename(entity.path), | 306 add(basename(entity.path), |
| 304 stat.modified.toString(), | 307 stat.modified.toString(), |
| 305 stat.size); | 308 stat.size); |
| 306 } else if (entity is Directory) { | 309 } else if (entity is Directory) { |
| 307 add(basename(entity.path) + '/', | 310 add(basename(entity.path) + '/', |
| 308 entity.statSync().modified.toString(), | 311 entity.statSync().modified.toString(), |
| 309 null); | 312 null); |
| 310 } | 313 } |
| 311 }, onError: (e) { | 314 }, onError: (e) { |
| 315 // TODO(kevmoo): log error |
| 312 }, onDone: () { | 316 }, onDone: () { |
| 313 response.write(footer); | 317 response.write(footer); |
| 314 response.close(); | 318 response.close(); |
| 315 }); | 319 }); |
| 316 }, onError: (e) => response.close()); | 320 }, onError: (e) { |
| 321 // TODO(kevmoo): log error |
| 322 response.close(); |
| 323 }); |
| 317 } | 324 } |
| 318 | 325 |
| 319 void _serveErrorPage(int error, HttpRequest request) { | 326 void _serveErrorPage(int error, HttpRequest request) { |
| 320 var response = request.response; | 327 var response = request.response; |
| 321 response.statusCode = error; | 328 response.statusCode = error; |
| 322 if (_errorCallback != null) { | 329 if (_errorCallback != null) { |
| 323 _errorCallback(request); | 330 _errorCallback(request); |
| 324 return; | 331 return; |
| 325 } | 332 } |
| 326 // Default error page. | 333 // Default error page. |
| (...skipping 15 matching lines...) Expand all Loading... |
| 342 </body> | 349 </body> |
| 343 </html>'''; | 350 </html>'''; |
| 344 response.write(page); | 351 response.write(page); |
| 345 response.close(); | 352 response.close(); |
| 346 } | 353 } |
| 347 } | 354 } |
| 348 | 355 |
| 349 class _VirtualDirectoryFileStream extends StreamConsumer<List<int>> { | 356 class _VirtualDirectoryFileStream extends StreamConsumer<List<int>> { |
| 350 final HttpResponse response; | 357 final HttpResponse response; |
| 351 final String path; | 358 final String path; |
| 352 var buffer = []; | 359 List<int> buffer = []; |
| 353 | 360 |
| 354 _VirtualDirectoryFileStream(HttpResponse this.response, String this.path); | 361 _VirtualDirectoryFileStream(HttpResponse this.response, String this.path); |
| 355 | 362 |
| 356 Future addStream(Stream<List<int>> stream) { | 363 Future addStream(Stream<List<int>> stream) { |
| 357 stream.listen( | 364 stream.listen( |
| 358 (data) { | 365 (data) { |
| 359 if (buffer == null) { | 366 if (buffer == null) { |
| 360 response.add(data); | 367 response.add(data); |
| 361 return; | 368 return; |
| 362 } | 369 } |
| (...skipping 24 matching lines...) Expand all Loading... |
| 387 } | 394 } |
| 388 } | 395 } |
| 389 response.close(); | 396 response.close(); |
| 390 }, | 397 }, |
| 391 onError: response.addError); | 398 onError: response.addError); |
| 392 return response.done; | 399 return response.done; |
| 393 } | 400 } |
| 394 | 401 |
| 395 Future close() => new Future.value(); | 402 Future close() => new Future.value(); |
| 396 | 403 |
| 397 void setMimeType(var bytes) { | 404 void setMimeType(List<int> bytes) { |
| 398 var mimeType = lookupMimeType(path, headerBytes: bytes); | 405 var mimeType = lookupMimeType(path, headerBytes: bytes); |
| 399 if (mimeType != null) { | 406 if (mimeType != null) { |
| 400 response.headers.contentType = ContentType.parse(mimeType); | 407 response.headers.contentType = ContentType.parse(mimeType); |
| 401 } | 408 } |
| 402 } | 409 } |
| 403 } | 410 } |
| OLD | NEW |