blob: 6ce667b687dfbb97f858d13d2acb69f91e1801a6 [file] [log] [blame]
// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
part of http_server;
/**
* A [VirtualDirectory] can serve files and directory-listing from a root path,
* to [HttpRequest]s.
*
* The [VirtualDirectory] providing secure handling of request uris and
* file-system links, correct mime-types and custom error pages.
*/
abstract class VirtualDirectory {
final String root;
/**
* Set or get if the [VirtualDirectory] should list the content of
* directories.
*/
bool allowDirectoryListing = false;
/**
* Set or get if the [VirtualDirectory] should follow links, that point
* to other resources within the [root] directory.
*/
bool followLinks = true;
/*
* Create a new [VirtualDirectory] for serving static file content of
* the path [root].
*
* The [root] is not required to exist. If the [root] doesn't exist at time of
* a request, a 404 is generated.
*/
factory VirtualDirectory(String root) => new _VirtualDirectory(root);
/**
* Serve a [Stream] of [HttpRequest]s, in this [VirtualDirectory].
*/
void serve(Stream<HttpRequest> requests);
/**
* Serve a single [HttpRequest], in this [VirtualDirectory].
*/
void serveRequest(HttpRequest request);
/**
* Set the [callback] to override the error page handler. When [callback] is
* invoked, the `statusCode` property of the response is set.
*/
void setErrorPageHandler(void callback(HttpRequest request));
}
class _VirtualDirectory implements VirtualDirectory {
final String root;
bool allowDirectoryListing = false;
bool followLinks = true;
Function _errorCallback;
_VirtualDirectory(this.root);
void serve(Stream<HttpRequest> requests) {
requests.listen(serveRequest);
}
void serveRequest(HttpRequest request) {
var path = new Path(request.uri.path).canonicalize();
if (!path.isAbsolute) {
return _serveErrorPage(HttpStatus.NOT_FOUND, request);
}
_locateResource(new Path('.'), path.segments())
.then((entity) {
if (entity == null) {
_serveErrorPage(HttpStatus.NOT_FOUND, request);
return;
}
if (entity is File) {
_serveFile(entity, request);
} else {
_serveErrorPage(HttpStatus.NOT_FOUND, request);
}
});
}
void setErrorPageHandler(void callback(HttpRequest request)) {
_errorCallback = callback;
}
Future<FileSystemEntity> _locateResource(Path path,
Iterable<String> segments) {
Path fullPath() => new Path(root).join(path);
return FileSystemEntity.type(fullPath().toNativePath(), followLinks: false)
.then((type) {
switch (type) {
case FileSystemEntityType.FILE:
if (segments.isEmpty) return new File.fromPath(fullPath());
break;
case FileSystemEntityType.DIRECTORY:
if (segments.isEmpty) {
if (allowDirectoryListing) {
return new Directory.fromPath(fullPath());
}
} else {
return _locateResource(path.append(segments.first),
segments.skip(1));
}
break;
case FileSystemEntityType.LINK:
if (followLinks) {
return new Link.fromPath(fullPath()).target()
.then((target) {
var targetPath = new Path(target).canonicalize();
if (targetPath.isAbsolute) return null;
targetPath = path.directoryPath.join(targetPath)
.canonicalize();
if (targetPath.segments().isEmpty ||
targetPath.segments().first == '..') return null;
return _locateResource(targetPath.append(segments.first),
segments.skip(1));
});
}
break;
}
// Return `null` on fall-through, to indicate NOT_FOUND.
return null;
});
}
void _serveFile(File file, HttpRequest request) {
var response = request.response;
// TODO(ajohnsen): Set up Zone support for these errors.
file.lastModified().then((lastModified) {
if (request.headers.ifModifiedSince != null &&
!lastModified.isAfter(request.headers.ifModifiedSince)) {
response.statusCode = HttpStatus.NOT_MODIFIED;
response.close();
return;
}
response.headers.set(HttpHeaders.LAST_MODIFIED, lastModified);
response.headers.set(HttpHeaders.ACCEPT_RANGES, "bytes");
if (request.method == 'HEAD') {
response.close();
return;
}
return file.length().then((length) {
String range = request.headers.value("range");
if (range != null) {
// We only support one range, where the standard support several.
Match matches = new RegExp(r"^bytes=(\d*)\-(\d*)$").firstMatch(range);
// If the range header have the right format, handle it.
if (matches != null) {
// Serve sub-range.
int start;
int end;
if (matches[1].isEmpty) {
start = matches[2].isEmpty ?
length :
length - int.parse(matches[2]);
end = length;
} else {
start = int.parse(matches[1]);
end = matches[2].isEmpty ? length : int.parse(matches[2]) + 1;
}
// Override Content-Length with the actual bytes sent.
response.headers.set(HttpHeaders.CONTENT_LENGTH, end - start);
// Set 'Partial Content' status code.
response.statusCode = HttpStatus.PARTIAL_CONTENT;
response.headers.set(HttpHeaders.CONTENT_RANGE,
"bytes $start-${end - 1}/$length");
// Pipe the 'range' of the file.
file.openRead(start, end)
.pipe(new _VirtualDirectoryFileStream(response, file.path))
.catchError((_) {});
return;
}
}
file.openRead()
.pipe(new _VirtualDirectoryFileStream(response, file.path))
.catchError((_) {});
});
}).catchError((_) {
response.close();
});
}
void _serveErrorPage(int error, HttpRequest request) {
var response = request.response;
response.statusCode = error;
if (_errorCallback != null) {
_errorCallback(request);
return;
}
// Default error page.
var path = request.uri.path;
var reason = response.reasonPhrase;
response.write(
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"\n');
response.writeln(
'"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">');
response.writeln('<html xmlns="http://www.w3.org/1999/xhtml">');
response.writeln('<head>');
response.writeln('<title>$reason: $path</title>');
response.writeln('</head>');
response.writeln('<body>');
response.writeln('<h1>Error $error at \'$path\': $reason</h1>');
var server = response.headers.value(HttpHeaders.SERVER);
if (server != null) {
response.writeln(server);
}
response.writeln('</body>');
response.writeln('</html>');
response.close();
}
}
class _VirtualDirectoryFileStream extends StreamConsumer<List<int>> {
final HttpResponse response;
final String path;
var buffer = [];
_VirtualDirectoryFileStream(HttpResponse this.response, String this.path);
Future addStream(Stream<List<int>> stream) {
stream.listen(
(data) {
if (buffer == null) {
response.add(data);
return;
}
if (buffer.length == 0) {
if (data.length >= defaultMagicNumbersMaxLength) {
setMimeType(data);
response.add(data);
buffer = null;
} else {
buffer.addAll(data);
}
} else {
buffer.addAll(data);
if (buffer.length >= defaultMagicNumbersMaxLength) {
setMimeType(buffer);
response.add(buffer);
buffer = null;
}
}
},
onDone: () {
if (buffer != null) {
if (buffer.length == 0) {
setMimeType(null);
} else {
setMimeType(buffer);
response.add(buffer);
}
}
response.close();
},
onError: response.addError);
return response.done;
}
Future close() => new Future.value();
void setMimeType(var bytes) {
var mimeType = lookupMimeType(path, headerBytes: bytes);
if (mimeType != null) {
response.headers.contentType = ContentType.parse(mimeType);
}
}
}