From 54608a711ec6b27ccae9b6ce8756590c04c820dd Mon Sep 17 00:00:00 2001 From: gmpassos Date: Tue, 5 Dec 2023 15:09:38 -0300 Subject: [PATCH 01/11] Add context to `Response.notFound` and `Response.notModified` for including `file` and `file_not_found` to be identified by other Shelf `Middleware`. --- pkgs/shelf_static/lib/src/static_handler.dart | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/pkgs/shelf_static/lib/src/static_handler.dart b/pkgs/shelf_static/lib/src/static_handler.dart index 92b5009..8d72bb0 100644 --- a/pkgs/shelf_static/lib/src/static_handler.dart +++ b/pkgs/shelf_static/lib/src/static_handler.dart @@ -82,7 +82,10 @@ Handler createStaticHandler(String fileSystemPath, } if (fileFound == null) { - return Response.notFound('Not Found'); + return Response.notFound( + 'Not Found', + context: _buildResponseContext(fileNotFound: fileFound), + ); } final file = fileFound; @@ -120,6 +123,18 @@ Handler createStaticHandler(String fileSystemPath, }; } +Map? _buildResponseContext({File? file, File? fileNotFound}) { + if (file == null && fileNotFound == null) return null; + + // Ensure other Shelf `Middleware` can identify + // the processed file in the `Response` by + // including `file` and `file_not_found` in the context: + return { + if (file != null) 'shelf_static:file': file, + if (fileNotFound != null) 'shelf_static:file_not_found': fileNotFound, + }; +} + Response _redirectToAddTrailingSlash(Uri uri) { final location = Uri( scheme: uri.scheme, @@ -184,7 +199,9 @@ Future _handleFile(Request request, File file, if (ifModifiedSince != null) { final fileChangeAtSecResolution = toSecondResolution(stat.modified); if (!fileChangeAtSecResolution.isAfter(ifModifiedSince)) { - return Response.notModified(); + return Response.notModified( + context: _buildResponseContext(file: file), + ); } } @@ -199,6 +216,7 @@ Future _handleFile(Request request, File file, Response.ok( request.method == 'HEAD' ? null : file.openRead(), headers: headers..[HttpHeaders.contentLengthHeader] = '${stat.size}', + context: _buildResponseContext(file: file), ); } From 7923a7f009b13245f2573b34fcf3003f8591d026 Mon Sep 17 00:00:00 2001 From: gmpassos Date: Tue, 5 Dec 2023 15:09:38 -0300 Subject: [PATCH 02/11] Add context to `Response` for including `file` and `file_not_found` to be identified by other Shelf `Middleware`. --- pkgs/shelf_static/lib/src/static_handler.dart | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/pkgs/shelf_static/lib/src/static_handler.dart b/pkgs/shelf_static/lib/src/static_handler.dart index 92b5009..8d72bb0 100644 --- a/pkgs/shelf_static/lib/src/static_handler.dart +++ b/pkgs/shelf_static/lib/src/static_handler.dart @@ -82,7 +82,10 @@ Handler createStaticHandler(String fileSystemPath, } if (fileFound == null) { - return Response.notFound('Not Found'); + return Response.notFound( + 'Not Found', + context: _buildResponseContext(fileNotFound: fileFound), + ); } final file = fileFound; @@ -120,6 +123,18 @@ Handler createStaticHandler(String fileSystemPath, }; } +Map? _buildResponseContext({File? file, File? fileNotFound}) { + if (file == null && fileNotFound == null) return null; + + // Ensure other Shelf `Middleware` can identify + // the processed file in the `Response` by + // including `file` and `file_not_found` in the context: + return { + if (file != null) 'shelf_static:file': file, + if (fileNotFound != null) 'shelf_static:file_not_found': fileNotFound, + }; +} + Response _redirectToAddTrailingSlash(Uri uri) { final location = Uri( scheme: uri.scheme, @@ -184,7 +199,9 @@ Future _handleFile(Request request, File file, if (ifModifiedSince != null) { final fileChangeAtSecResolution = toSecondResolution(stat.modified); if (!fileChangeAtSecResolution.isAfter(ifModifiedSince)) { - return Response.notModified(); + return Response.notModified( + context: _buildResponseContext(file: file), + ); } } @@ -199,6 +216,7 @@ Future _handleFile(Request request, File file, Response.ok( request.method == 'HEAD' ? null : file.openRead(), headers: headers..[HttpHeaders.contentLengthHeader] = '${stat.size}', + context: _buildResponseContext(file: file), ); } From c44971737f640448bfe9143634d60941135c919f Mon Sep 17 00:00:00 2001 From: gmpassos Date: Tue, 5 Dec 2023 16:10:17 -0300 Subject: [PATCH 03/11] Remove unnecessary context build when `fileFound == null`. --- pkgs/shelf_static/lib/src/static_handler.dart | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pkgs/shelf_static/lib/src/static_handler.dart b/pkgs/shelf_static/lib/src/static_handler.dart index 8d72bb0..a7078db 100644 --- a/pkgs/shelf_static/lib/src/static_handler.dart +++ b/pkgs/shelf_static/lib/src/static_handler.dart @@ -82,11 +82,9 @@ Handler createStaticHandler(String fileSystemPath, } if (fileFound == null) { - return Response.notFound( - 'Not Found', - context: _buildResponseContext(fileNotFound: fileFound), - ); + return Response.notFound('Not Found'); } + final file = fileFound; if (!serveFilesOutsidePath) { From 688969f03514e364e2a4eba20a86600a5a646f20 Mon Sep 17 00:00:00 2001 From: gmpassos Date: Tue, 5 Dec 2023 16:23:34 -0300 Subject: [PATCH 04/11] Build `fileNotFound` and ensure to not expose a file path outside of the original fileSystemPath. Add `Response.context` documentation. --- pkgs/shelf_static/lib/src/static_handler.dart | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/pkgs/shelf_static/lib/src/static_handler.dart b/pkgs/shelf_static/lib/src/static_handler.dart index a7078db..58c5bf1 100644 --- a/pkgs/shelf_static/lib/src/static_handler.dart +++ b/pkgs/shelf_static/lib/src/static_handler.dart @@ -39,6 +39,11 @@ final _defaultMimeTypeResolver = MimeTypeResolver(); /// /// Specify a custom [contentTypeResolver] to customize automatic content type /// detection. +/// +/// The [Response.context] will be populated with "shelf_static:file" or +/// "shelf_static:file_not_found" with the resolved [File] for the [Response], +/// while respecting [serveFilesOutsidePath] to prevent exposing a [File] path +/// outside of the [fileSystemPath]. Handler createStaticHandler(String fileSystemPath, {bool serveFilesOutsidePath = false, String? defaultDocument, @@ -82,7 +87,18 @@ Handler createStaticHandler(String fileSystemPath, } if (fileFound == null) { - return Response.notFound('Not Found'); + File? fileNotFound = File(fsPath); + + // Do not expose a file path outside of the original fileSystemPath: + if (!serveFilesOutsidePath && + !p.isWithin(fileSystemPath, fileNotFound.path)) { + fileNotFound = null; + } + + return Response.notFound( + 'Not Found', + context: _buildResponseContext(fileNotFound: fileNotFound), + ); } final file = fileFound; From 9d22a8136705bc68d2b3e80baf4f4bc99e0df394 Mon Sep 17 00:00:00 2001 From: gmpassos Date: Tue, 5 Dec 2023 22:44:31 -0300 Subject: [PATCH 05/11] `createStaticHandler`: update `Response.context` documentation. --- pkgs/shelf_static/lib/src/static_handler.dart | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pkgs/shelf_static/lib/src/static_handler.dart b/pkgs/shelf_static/lib/src/static_handler.dart index 58c5bf1..83fd3c8 100644 --- a/pkgs/shelf_static/lib/src/static_handler.dart +++ b/pkgs/shelf_static/lib/src/static_handler.dart @@ -41,9 +41,10 @@ final _defaultMimeTypeResolver = MimeTypeResolver(); /// detection. /// /// The [Response.context] will be populated with "shelf_static:file" or -/// "shelf_static:file_not_found" with the resolved [File] for the [Response], -/// while respecting [serveFilesOutsidePath] to prevent exposing a [File] path -/// outside of the [fileSystemPath]. +/// "shelf_static:file_not_found" with the resolved [File] for the [Response]. +/// If the file is considered not found because it is outside of the +/// [fileSystemPath] and [serveFilesOutsidePath] is false, then neither key +/// will be included in the context. Handler createStaticHandler(String fileSystemPath, {bool serveFilesOutsidePath = false, String? defaultDocument, @@ -140,7 +141,7 @@ Handler createStaticHandler(String fileSystemPath, Map? _buildResponseContext({File? file, File? fileNotFound}) { if (file == null && fileNotFound == null) return null; - // Ensure other Shelf `Middleware` can identify + // Ensure other shelf `Middleware` can identify // the processed file in the `Response` by // including `file` and `file_not_found` in the context: return { From b6a704de235d1ddbd1551011a64651ea11bf1c98 Mon Sep 17 00:00:00 2001 From: gmpassos Date: Fri, 14 Jun 2024 04:56:54 -0300 Subject: [PATCH 06/11] * `Response`: * Populate `context` with the file system paths used for resolution. * Possible entries: `shelf_static:file`, `shelf_static:file_not_found`, and `shelf_static:directory`. --- pkgs/shelf_static/CHANGELOG.md | 4 ++ .../lib/src/directory_listing.dart | 7 ++- pkgs/shelf_static/lib/src/static_handler.dart | 44 ++++++++++--------- pkgs/shelf_static/lib/src/util.dart | 18 ++++++++ 4 files changed, 51 insertions(+), 22 deletions(-) diff --git a/pkgs/shelf_static/CHANGELOG.md b/pkgs/shelf_static/CHANGELOG.md index 7713e43..f759134 100644 --- a/pkgs/shelf_static/CHANGELOG.md +++ b/pkgs/shelf_static/CHANGELOG.md @@ -1,5 +1,9 @@ ## 1.1.3-wip +* `Response`: + * Populate `context` with the file system paths used for resolution. + * Possible entries: `shelf_static:file`, `shelf_static:file_not_found`, and `shelf_static:directory`. + * Require Dart `^3.3.0`. ## 1.1.2 diff --git a/pkgs/shelf_static/lib/src/directory_listing.dart b/pkgs/shelf_static/lib/src/directory_listing.dart index a9d7bfc..47fca03 100644 --- a/pkgs/shelf_static/lib/src/directory_listing.dart +++ b/pkgs/shelf_static/lib/src/directory_listing.dart @@ -9,6 +9,8 @@ import 'dart:io'; import 'package:path/path.dart' as path; import 'package:shelf/shelf.dart'; +import 'util.dart' show buildResponseContext; + String _getHeader(String sanitizedHeading) => ''' @@ -68,8 +70,10 @@ Response listDirectory(String fileSystemPath, String dirPath) { add(_getHeader(sanitizer.convert(heading))); + var dir = Directory(dirPath); + // Return a sorted listing of the directory contents asynchronously. - Directory(dirPath).list().toList().then((entities) { + dir.list().toList().then((entities) { entities.sort((e1, e2) { if (e1 is Directory && e2 is! Directory) { return -1; @@ -95,5 +99,6 @@ Response listDirectory(String fileSystemPath, String dirPath) { controller.stream, encoding: encoding, headers: {HttpHeaders.contentTypeHeader: 'text/html'}, + context: buildResponseContext(directory: dir), ); } diff --git a/pkgs/shelf_static/lib/src/static_handler.dart b/pkgs/shelf_static/lib/src/static_handler.dart index 83fd3c8..b8abc21 100644 --- a/pkgs/shelf_static/lib/src/static_handler.dart +++ b/pkgs/shelf_static/lib/src/static_handler.dart @@ -82,7 +82,9 @@ Handler createStaticHandler(String fileSystemPath, fileFound = _tryDefaultFile(fsPath, defaultDocument); if (fileFound == null && listDirectories) { final uri = request.requestedUri; - if (!uri.path.endsWith('/')) return _redirectToAddTrailingSlash(uri); + if (!uri.path.endsWith('/')) { + return _redirectToAddTrailingSlash(uri, fsPath); + } return listDirectory(fileSystemPath, fsPath); } } @@ -92,13 +94,14 @@ Handler createStaticHandler(String fileSystemPath, // Do not expose a file path outside of the original fileSystemPath: if (!serveFilesOutsidePath && - !p.isWithin(fileSystemPath, fileNotFound.path)) { + !p.isWithin(fileSystemPath, fileNotFound.path) && + !p.equals(fileSystemPath, fileNotFound.path)) { fileNotFound = null; } return Response.notFound( 'Not Found', - context: _buildResponseContext(fileNotFound: fileNotFound), + context: buildResponseContext(fileNotFound: fileNotFound), ); } @@ -118,7 +121,7 @@ Handler createStaticHandler(String fileSystemPath, final uri = request.requestedUri; if (entityType == FileSystemEntityType.directory && !uri.path.endsWith('/')) { - return _redirectToAddTrailingSlash(uri); + return _redirectToAddTrailingSlash(uri, fsPath); } return _handleFile(request, file, () async { @@ -138,19 +141,7 @@ Handler createStaticHandler(String fileSystemPath, }; } -Map? _buildResponseContext({File? file, File? fileNotFound}) { - if (file == null && fileNotFound == null) return null; - - // Ensure other shelf `Middleware` can identify - // the processed file in the `Response` by - // including `file` and `file_not_found` in the context: - return { - if (file != null) 'shelf_static:file': file, - if (fileNotFound != null) 'shelf_static:file_not_found': fileNotFound, - }; -} - -Response _redirectToAddTrailingSlash(Uri uri) { +Response _redirectToAddTrailingSlash(Uri uri, String fsPath) { final location = Uri( scheme: uri.scheme, userInfo: uri.userInfo, @@ -159,7 +150,8 @@ Response _redirectToAddTrailingSlash(Uri uri) { path: '${uri.path}/', query: uri.query); - return Response.movedPermanently(location.toString()); + return Response.movedPermanently(location.toString(), + context: buildResponseContext(directory: Directory(fsPath))); } File? _tryDefaultFile(String dirPath, String? defaultFile) { @@ -192,11 +184,19 @@ Handler createFileHandler(String path, {String? url, String? contentType}) { throw ArgumentError.value(url, 'url', 'must be relative.'); } + final parent = file.parent; + final mimeType = contentType ?? _defaultMimeTypeResolver.lookup(path); url ??= p.toUri(p.basename(path)).toString(); return (request) { - if (request.url.path != url) return Response.notFound('Not Found'); + if (request.url.path != url) { + var fileNotFound = File(p.join(parent.path, request.url.path)); + return Response.notFound( + 'Not Found', + context: buildResponseContext(fileNotFound: fileNotFound), + ); + } return _handleFile(request, file, () => mimeType); }; } @@ -215,7 +215,7 @@ Future _handleFile(Request request, File file, final fileChangeAtSecResolution = toSecondResolution(stat.modified); if (!fileChangeAtSecResolution.isAfter(ifModifiedSince)) { return Response.notModified( - context: _buildResponseContext(file: file), + context: buildResponseContext(file: file), ); } } @@ -231,7 +231,7 @@ Future _handleFile(Request request, File file, Response.ok( request.method == 'HEAD' ? null : file.openRead(), headers: headers..[HttpHeaders.contentLengthHeader] = '${stat.size}', - context: _buildResponseContext(file: file), + context: buildResponseContext(file: file), ); } @@ -281,6 +281,7 @@ Response? _fileRangeResponse( return Response( HttpStatus.requestedRangeNotSatisfiable, headers: headers, + context: buildResponseContext(file: file), ); } return Response( @@ -289,5 +290,6 @@ Response? _fileRangeResponse( headers: headers ..[HttpHeaders.contentLengthHeader] = (end - start + 1).toString() ..[HttpHeaders.contentRangeHeader] = 'bytes $start-$end/$actualLength', + context: buildResponseContext(file: file), ); } diff --git a/pkgs/shelf_static/lib/src/util.dart b/pkgs/shelf_static/lib/src/util.dart index 2d42379..de9219e 100644 --- a/pkgs/shelf_static/lib/src/util.dart +++ b/pkgs/shelf_static/lib/src/util.dart @@ -2,7 +2,25 @@ // 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. +import 'dart:io'; + DateTime toSecondResolution(DateTime dt) { if (dt.millisecond == 0) return dt; return dt.subtract(Duration(milliseconds: dt.millisecond)); } + +Map? buildResponseContext( + {File? file, File? fileNotFound, Directory? directory}) { + // Ensure other shelf `Middleware` can identify + // the processed file/directory in the `Response` by including + // `file`, `file_not_found` and `directory` in the context: + if (file != null) { + return {'shelf_static:file': file}; + } else if (fileNotFound != null) { + return {'shelf_static:file_not_found': fileNotFound}; + } else if (directory != null) { + return {'shelf_static:directory': directory}; + } else { + return null; + } +} From ad79fd4876e5521ea7ed900a664232ad7f072ed0 Mon Sep 17 00:00:00 2001 From: gmpassos Date: Fri, 14 Jun 2024 04:57:57 -0300 Subject: [PATCH 07/11] test `Response.context`. --- .../test/alternative_root_test.dart | 6 +++ pkgs/shelf_static/test/basic_file_test.dart | 31 +++++++++++++ .../test/create_file_handler_test.dart | 46 +++++++++++++++++++ .../test/default_document_test.dart | 41 +++++++++++++++++ .../test/directory_listing_test.dart | 11 +++++ .../shelf_static/test/symbolic_link_test.dart | 27 +++++++++++ pkgs/shelf_static/test/test_util.dart | 10 ++++ 7 files changed, 172 insertions(+) diff --git a/pkgs/shelf_static/test/alternative_root_test.dart b/pkgs/shelf_static/test/alternative_root_test.dart index 3a18e96..0d57d38 100644 --- a/pkgs/shelf_static/test/alternative_root_test.dart +++ b/pkgs/shelf_static/test/alternative_root_test.dart @@ -4,6 +4,7 @@ import 'dart:io'; +import 'package:path/path.dart' as p; import 'package:shelf_static/shelf_static.dart'; import 'package:test/test.dart'; import 'package:test_descriptor/test_descriptor.dart' as d; @@ -27,6 +28,11 @@ void main() { expect(response.statusCode, HttpStatus.ok); expect(response.contentLength, 8); expect(response.readAsString(), completion('root txt')); + + expect( + response.context.toFilePath(), + equals({'shelf_static:file': p.join(d.sandbox, 'root.txt')}), + ); }); test('access root file with space', () async { diff --git a/pkgs/shelf_static/test/basic_file_test.dart b/pkgs/shelf_static/test/basic_file_test.dart index a1425da..9aa0005 100644 --- a/pkgs/shelf_static/test/basic_file_test.dart +++ b/pkgs/shelf_static/test/basic_file_test.dart @@ -46,6 +46,11 @@ void main() { expect(response.statusCode, HttpStatus.ok); expect(response.contentLength, 8); expect(response.readAsString(), completion('root txt')); + + expect( + response.context.toFilePath(), + equals({'shelf_static:file': p.join(d.sandbox, 'root.txt')}), + ); }); test('HEAD', () async { @@ -55,6 +60,11 @@ void main() { expect(response.statusCode, HttpStatus.ok); expect(response.contentLength, 8); expect(await response.readAsString(), isEmpty); + + expect( + response.context.toFilePath(), + equals({'shelf_static:file': p.join(d.sandbox, 'root.txt')}), + ); }); test('access root file with space', () async { @@ -64,6 +74,11 @@ void main() { expect(response.statusCode, HttpStatus.ok); expect(response.contentLength, 18); expect(response.readAsString(), completion('with space content')); + + expect( + response.context.toFilePath(), + equals({'shelf_static:file': p.join(d.sandbox, 'files/with space.txt')}), + ); }); test('access root file with unencoded space', () async { @@ -89,6 +104,12 @@ void main() { final response = await makeRequest(handler, '/not_here.txt'); expect(response.statusCode, HttpStatus.notFound); + + expect( + response.context.toFilePath(), + equals( + {'shelf_static:file_not_found': p.join(d.sandbox, 'not_here.txt')}), + ); }); test('last modified', () async { @@ -99,6 +120,11 @@ void main() { final response = await makeRequest(handler, '/root.txt'); expect(response.lastModified, atSameTimeToSecond(modified)); + + expect( + response.context.toFilePath(), + equals({'shelf_static:file': p.join(d.sandbox, 'root.txt')}), + ); }); group('if modified since', () { @@ -116,6 +142,11 @@ void main() { await makeRequest(handler, '/root.txt', headers: headers); expect(response.statusCode, HttpStatus.notModified); expect(response.contentLength, 0); + + expect( + response.context.toFilePath(), + equals({'shelf_static:file': p.join(d.sandbox, 'root.txt')}), + ); }); test('before last modified', () async { diff --git a/pkgs/shelf_static/test/create_file_handler_test.dart b/pkgs/shelf_static/test/create_file_handler_test.dart index 91c4c86..0e7ddec 100644 --- a/pkgs/shelf_static/test/create_file_handler_test.dart +++ b/pkgs/shelf_static/test/create_file_handler_test.dart @@ -23,12 +23,23 @@ void main() { expect(response.statusCode, equals(HttpStatus.ok)); expect(response.contentLength, equals(8)); expect(response.readAsString(), completion(equals('contents'))); + + expect( + response.context.toFilePath(), + equals({'shelf_static:file': p.join(d.sandbox, 'file.txt')}), + ); }); test('serves a 404 for a non-matching URL', () async { final handler = createFileHandler(p.join(d.sandbox, 'file.txt')); final response = await makeRequest(handler, '/foo/file.txt'); expect(response.statusCode, equals(HttpStatus.notFound)); + + expect( + response.context.toFilePath(), + equals( + {'shelf_static:file_not_found': p.join(d.sandbox, 'foo/file.txt')}), + ); }); test('serves the file contents under a custom URL', () async { @@ -38,6 +49,11 @@ void main() { expect(response.statusCode, equals(HttpStatus.ok)); expect(response.contentLength, equals(8)); expect(response.readAsString(), completion(equals('contents'))); + + expect( + response.context.toFilePath(), + equals({'shelf_static:file': p.join(d.sandbox, 'file.txt')}), + ); }); test("serves a 404 if the custom URL isn't matched", () async { @@ -45,6 +61,11 @@ void main() { createFileHandler(p.join(d.sandbox, 'file.txt'), url: 'foo/bar'); final response = await makeRequest(handler, '/file.txt'); expect(response.statusCode, equals(HttpStatus.notFound)); + + expect( + response.context.toFilePath(), + equals({'shelf_static:file_not_found': p.join(d.sandbox, 'file.txt')}), + ); }); group('the content type header', () { @@ -53,6 +74,11 @@ void main() { final response = await makeRequest(handler, '/file.txt'); expect(response.statusCode, equals(HttpStatus.ok)); expect(response.mimeType, equals('text/plain')); + + expect( + response.context.toFilePath(), + equals({'shelf_static:file': p.join(d.sandbox, 'file.txt')}), + ); }); test("is omitted if it can't be inferred", () async { @@ -89,6 +115,11 @@ void main() { containsPair(HttpHeaders.contentRangeHeader, 'bytes 0-4/8'), ); expect(response.headers, containsPair('content-length', '5')); + + expect( + response.context.toFilePath(), + equals({'shelf_static:file': p.join(d.sandbox, 'file.txt')}), + ); }); test('at the end of has overflow from 0 to 9', () async { @@ -111,6 +142,11 @@ void main() { containsPair(HttpHeaders.contentRangeHeader, 'bytes 0-7/8'), ); expect(response.headers, containsPair('content-length', '8')); + + expect( + response.context.toFilePath(), + equals({'shelf_static:file': p.join(d.sandbox, 'file.txt')}), + ); }); test('at the start of has overflow from 8 to 9', () async { @@ -129,6 +165,11 @@ void main() { response.statusCode, HttpStatus.requestedRangeNotSatisfiable, ); + + expect( + response.context.toFilePath(), + equals({'shelf_static:file': p.join(d.sandbox, 'file.txt')}), + ); }); test('ignores invalid request with start > end', () async { @@ -141,6 +182,11 @@ void main() { expect(response.statusCode, equals(HttpStatus.ok)); expect(response.contentLength, equals(8)); expect(response.readAsString(), completion(equals('contents'))); + + expect( + response.context.toFilePath(), + equals({'shelf_static:file': p.join(d.sandbox, 'file.txt')}), + ); }); test('ignores request with start > end', () async { diff --git a/pkgs/shelf_static/test/default_document_test.dart b/pkgs/shelf_static/test/default_document_test.dart index c6b5fb1..97ae8a4 100644 --- a/pkgs/shelf_static/test/default_document_test.dart +++ b/pkgs/shelf_static/test/default_document_test.dart @@ -4,6 +4,7 @@ import 'dart:io'; +import 'package:path/path.dart' as p; import 'package:shelf_static/shelf_static.dart'; import 'package:test/test.dart'; import 'package:test_descriptor/test_descriptor.dart' as d; @@ -45,6 +46,11 @@ void main() { expect(response.statusCode, HttpStatus.ok); expect(response.contentLength, 13); expect(response.readAsString(), completion('')); + + expect( + response.context.toFilePath(), + equals({'shelf_static:file': p.join(d.sandbox, 'index.html')}), + ); }); test('access "/"', () async { @@ -52,6 +58,11 @@ void main() { final response = await makeRequest(handler, '/'); expect(response.statusCode, HttpStatus.notFound); + + expect( + response.context.toFilePath(), + equals({'shelf_static:file_not_found': d.sandbox}), + ); }); test('access "/files"', () async { @@ -59,6 +70,11 @@ void main() { final response = await makeRequest(handler, '/files'); expect(response.statusCode, HttpStatus.notFound); + + expect( + response.context.toFilePath(), + equals({'shelf_static:file_not_found': p.join(d.sandbox, 'files')}), + ); }); test('access "/files/" dir', () async { @@ -66,6 +82,11 @@ void main() { final response = await makeRequest(handler, '/files/'); expect(response.statusCode, HttpStatus.notFound); + + expect( + response.context.toFilePath(), + equals({'shelf_static:file_not_found': p.join(d.sandbox, 'files')}), + ); }); }); @@ -79,6 +100,11 @@ void main() { expect(response.contentLength, 13); expect(response.readAsString(), completion('')); expect(response.mimeType, 'text/html'); + + expect( + response.context.toFilePath(), + equals({'shelf_static:file': p.join(d.sandbox, 'index.html')}), + ); }); test('access "/"', () async { @@ -90,6 +116,11 @@ void main() { expect(response.contentLength, 13); expect(response.readAsString(), completion('')); expect(response.mimeType, 'text/html'); + + expect( + response.context.toFilePath(), + equals({'shelf_static:file': p.join(d.sandbox, 'index.html')}), + ); }); test('access "/files"', () async { @@ -100,6 +131,11 @@ void main() { expect(response.statusCode, HttpStatus.movedPermanently); expect(response.headers, containsPair(HttpHeaders.locationHeader, 'http://localhost/files/')); + + expect( + response.context.toDirectoryPath(), + equals({'shelf_static:directory': p.join(d.sandbox, 'files')}), + ); }); test('access "/files/" dir', () async { @@ -112,6 +148,11 @@ void main() { expect(response.readAsString(), completion('files')); expect(response.mimeType, 'text/html'); + + expect( + response.context.toFilePath(), + equals({'shelf_static:file': p.join(d.sandbox, 'files/index.html')}), + ); }); }); } diff --git a/pkgs/shelf_static/test/directory_listing_test.dart b/pkgs/shelf_static/test/directory_listing_test.dart index f587af1..643f0ef 100644 --- a/pkgs/shelf_static/test/directory_listing_test.dart +++ b/pkgs/shelf_static/test/directory_listing_test.dart @@ -4,6 +4,7 @@ import 'dart:io'; +import 'package:path/path.dart' as p; import 'package:shelf_static/shelf_static.dart'; import 'package:test/test.dart'; import 'package:test_descriptor/test_descriptor.dart' as d; @@ -27,6 +28,11 @@ void main() { final response = await makeRequest(handler, '/'); expect(response.statusCode, HttpStatus.ok); expect(response.readAsString(), completes); + + expect( + response.context.toDirectoryPath(), + equals({'shelf_static:directory': d.sandbox}), + ); }); test('access "/files"', () async { @@ -36,6 +42,11 @@ void main() { expect(response.statusCode, HttpStatus.movedPermanently); expect(response.headers, containsPair(HttpHeaders.locationHeader, 'http://localhost/files/')); + + expect( + response.context.toDirectoryPath(), + equals({'shelf_static:directory': p.join(d.sandbox, 'files')}), + ); }); test('access "/files/"', () async { diff --git a/pkgs/shelf_static/test/symbolic_link_test.dart b/pkgs/shelf_static/test/symbolic_link_test.dart index 80ba32c..8871883 100644 --- a/pkgs/shelf_static/test/symbolic_link_test.dart +++ b/pkgs/shelf_static/test/symbolic_link_test.dart @@ -44,6 +44,12 @@ void main() { expect(response.statusCode, HttpStatus.ok); expect(response.contentLength, 13); expect(response.readAsString(), completion('')); + + expect( + response.context.toFilePath(), + equals( + {'shelf_static:file': p.join(d.sandbox, 'originals/index.html')}), + ); }); group('links under root dir', () { @@ -56,6 +62,11 @@ void main() { expect(response.statusCode, HttpStatus.ok); expect(response.contentLength, 13); expect(response.readAsString(), completion('')); + + expect( + response.context.toFilePath(), + equals({'shelf_static:file': p.join(d.sandbox, 'link_index.html')}), + ); }, onPlatform: _skipSymlinksOnWindows, ); @@ -67,6 +78,12 @@ void main() { expect(response.statusCode, HttpStatus.ok); expect(response.contentLength, 13); expect(response.readAsString(), completion('')); + + expect( + response.context.toFilePath(), + equals( + {'shelf_static:file': p.join(d.sandbox, 'link_dir/index.html')}), + ); }); }); @@ -76,6 +93,11 @@ void main() { final response = await makeRequest(handler, '/link_index.html'); expect(response.statusCode, HttpStatus.notFound); + + expect( + response.context.toFilePath(), + equals({}), // outside of the root path: empty context. + ); }); test('access file in sym linked dir', () async { @@ -83,6 +105,11 @@ void main() { final response = await makeRequest(handler, '/link_dir/index.html'); expect(response.statusCode, HttpStatus.notFound); + + expect( + response.context.toFilePath(), + equals({}), // outside of the root path: empty context. + ); }); }); }); diff --git a/pkgs/shelf_static/test/test_util.dart b/pkgs/shelf_static/test/test_util.dart index 81d5ead..879f631 100644 --- a/pkgs/shelf_static/test/test_util.dart +++ b/pkgs/shelf_static/test/test_util.dart @@ -2,6 +2,8 @@ // 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. +import 'dart:io'; + import 'package:path/path.dart' as p; import 'package:shelf/shelf.dart'; import 'package:shelf_static/src/util.dart'; @@ -69,3 +71,11 @@ class _SecondResolutionDateTimeMatcher extends Matcher { bool _datesEqualToSecond(DateTime d1, DateTime d2) => toSecondResolution(d1).isAtSameMomentAs(toSecondResolution(d2)); + +extension ResponseContextExtension on Map { + Map toFilePath() => + map((k, v) => MapEntry(k, v is File ? v.path : '$v')); + + Map toDirectoryPath() => + map((k, v) => MapEntry(k, v is Directory ? v.path : '$v')); +} From fbd52798e90fb17a570ce5d8925740fc9e39c77d Mon Sep 17 00:00:00 2001 From: gmpassos Date: Fri, 14 Jun 2024 05:11:57 -0300 Subject: [PATCH 08/11] Improve documentation --- pkgs/shelf_static/lib/src/static_handler.dart | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/pkgs/shelf_static/lib/src/static_handler.dart b/pkgs/shelf_static/lib/src/static_handler.dart index b8abc21..64474a9 100644 --- a/pkgs/shelf_static/lib/src/static_handler.dart +++ b/pkgs/shelf_static/lib/src/static_handler.dart @@ -42,9 +42,10 @@ final _defaultMimeTypeResolver = MimeTypeResolver(); /// /// The [Response.context] will be populated with "shelf_static:file" or /// "shelf_static:file_not_found" with the resolved [File] for the [Response]. -/// If the file is considered not found because it is outside of the -/// [fileSystemPath] and [serveFilesOutsidePath] is false, then neither key -/// will be included in the context. +/// If the path resolves to a [Directory], it will populate +/// "shelf_static:directory". If the path is considered not found because it is +/// outside of the [fileSystemPath] and [serveFilesOutsidePath] is false, +/// then none of the keys will be included in the context. Handler createStaticHandler(String fileSystemPath, {bool serveFilesOutsidePath = false, String? defaultDocument, @@ -176,6 +177,12 @@ File? _tryDefaultFile(String dirPath, String? defaultFile) { /// This uses the given [contentType] for the Content-Type header. It defaults /// to looking up a content type based on [path]'s file extension, and failing /// that doesn't sent a [contentType] header at all. +/// +/// The [Response.context] will be populated with "shelf_static:file" or +/// "shelf_static:file_not_found" with the resolved [File] for the [Response]. +/// If the path is considered not found because it is +/// outside of the [fileSystemPath] and [serveFilesOutsidePath] is false, +/// then neither key will be included in the context. Handler createFileHandler(String path, {String? url, String? contentType}) { final file = File(path); if (!file.existsSync()) { From cb88bc2f9666432a3eb6e14546b96a2957154eca Mon Sep 17 00:00:00 2001 From: gmpassos Date: Fri, 14 Jun 2024 15:35:08 -0300 Subject: [PATCH 09/11] Fix tests for windows --- pkgs/shelf_static/test/basic_file_test.dart | 3 ++- pkgs/shelf_static/test/create_file_handler_test.dart | 5 +++-- pkgs/shelf_static/test/default_document_test.dart | 2 +- pkgs/shelf_static/test/symbolic_link_test.dart | 10 ++++++---- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/pkgs/shelf_static/test/basic_file_test.dart b/pkgs/shelf_static/test/basic_file_test.dart index 9aa0005..740c5f9 100644 --- a/pkgs/shelf_static/test/basic_file_test.dart +++ b/pkgs/shelf_static/test/basic_file_test.dart @@ -77,7 +77,8 @@ void main() { expect( response.context.toFilePath(), - equals({'shelf_static:file': p.join(d.sandbox, 'files/with space.txt')}), + equals( + {'shelf_static:file': p.join(d.sandbox, 'files', 'with space.txt')}), ); }); diff --git a/pkgs/shelf_static/test/create_file_handler_test.dart b/pkgs/shelf_static/test/create_file_handler_test.dart index 0e7ddec..faefd3d 100644 --- a/pkgs/shelf_static/test/create_file_handler_test.dart +++ b/pkgs/shelf_static/test/create_file_handler_test.dart @@ -37,8 +37,9 @@ void main() { expect( response.context.toFilePath(), - equals( - {'shelf_static:file_not_found': p.join(d.sandbox, 'foo/file.txt')}), + equals({ + 'shelf_static:file_not_found': p.join(d.sandbox, 'foo', 'file.txt') + }), ); }); diff --git a/pkgs/shelf_static/test/default_document_test.dart b/pkgs/shelf_static/test/default_document_test.dart index 97ae8a4..08ea17d 100644 --- a/pkgs/shelf_static/test/default_document_test.dart +++ b/pkgs/shelf_static/test/default_document_test.dart @@ -151,7 +151,7 @@ void main() { expect( response.context.toFilePath(), - equals({'shelf_static:file': p.join(d.sandbox, 'files/index.html')}), + equals({'shelf_static:file': p.join(d.sandbox, 'files', 'index.html')}), ); }); }); diff --git a/pkgs/shelf_static/test/symbolic_link_test.dart b/pkgs/shelf_static/test/symbolic_link_test.dart index 8871883..dc3b392 100644 --- a/pkgs/shelf_static/test/symbolic_link_test.dart +++ b/pkgs/shelf_static/test/symbolic_link_test.dart @@ -47,8 +47,9 @@ void main() { expect( response.context.toFilePath(), - equals( - {'shelf_static:file': p.join(d.sandbox, 'originals/index.html')}), + equals({ + 'shelf_static:file': p.join(d.sandbox, 'originals', 'index.html') + }), ); }); @@ -81,8 +82,9 @@ void main() { expect( response.context.toFilePath(), - equals( - {'shelf_static:file': p.join(d.sandbox, 'link_dir/index.html')}), + equals({ + 'shelf_static:file': p.join(d.sandbox, 'link_dir', 'index.html') + }), ); }); }); From e40c761eb91ec0f23c3f5b0f71a2501febaeb636 Mon Sep 17 00:00:00 2001 From: gmpassos Date: Fri, 14 Jun 2024 15:45:56 -0300 Subject: [PATCH 10/11] Small fix for `createFileHandler` and window paths --- pkgs/shelf_static/lib/src/static_handler.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkgs/shelf_static/lib/src/static_handler.dart b/pkgs/shelf_static/lib/src/static_handler.dart index 64474a9..a8c16e1 100644 --- a/pkgs/shelf_static/lib/src/static_handler.dart +++ b/pkgs/shelf_static/lib/src/static_handler.dart @@ -198,7 +198,8 @@ Handler createFileHandler(String path, {String? url, String? contentType}) { return (request) { if (request.url.path != url) { - var fileNotFound = File(p.join(parent.path, request.url.path)); + var fileNotFound = + File(p.joinAll([parent.path, ...p.split(request.url.path)])); return Response.notFound( 'Not Found', context: buildResponseContext(fileNotFound: fileNotFound), From bcc44c5f4caa04858701e7cc44796a2c5ff0e876 Mon Sep 17 00:00:00 2001 From: gmpassos Date: Fri, 14 Jun 2024 15:48:12 -0300 Subject: [PATCH 11/11] optimize using request.url.pathSegments to build File --- pkgs/shelf_static/lib/src/static_handler.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/shelf_static/lib/src/static_handler.dart b/pkgs/shelf_static/lib/src/static_handler.dart index a8c16e1..3b4e2c6 100644 --- a/pkgs/shelf_static/lib/src/static_handler.dart +++ b/pkgs/shelf_static/lib/src/static_handler.dart @@ -199,7 +199,7 @@ Handler createFileHandler(String path, {String? url, String? contentType}) { return (request) { if (request.url.path != url) { var fileNotFound = - File(p.joinAll([parent.path, ...p.split(request.url.path)])); + File(p.joinAll([parent.path, ...request.url.pathSegments])); return Response.notFound( 'Not Found', context: buildResponseContext(fileNotFound: fileNotFound),