From e621022284fcb15a9ceb1dddf5ff9f02fa7b0bba Mon Sep 17 00:00:00 2001 From: Tyler Thompson Date: Sat, 25 Apr 2015 01:01:55 -0500 Subject: [PATCH 1/3] added new supported file formats like .ico and .eot, also fixed relative path issue with windows. is working. but requested url is still not being found when server starts up on windows machine. --- example/virtual_directory.dart | 2 +- .../http_server_request_handler.dart | 124 ++++++++++-------- 2 files changed, 68 insertions(+), 58 deletions(-) diff --git a/example/virtual_directory.dart b/example/virtual_directory.dart index f5e47d0..907d963 100644 --- a/example/virtual_directory.dart +++ b/example/virtual_directory.dart @@ -3,7 +3,7 @@ import "package:web_server/web_server.dart"; void main() { // Initialize the WebServer - final WebServer localWebServer = new WebServer(InternetAddress.LOOPBACK_IP_V4, 8080, + final WebServer localWebServer = new WebServer(InternetAddress.LOOPBACK_IP_V4, 8989, hasHttpServer: true, hasWebSocketServer: true); // Log out some of the connection information diff --git a/lib/src/web_server/http_server_request_handler.dart b/lib/src/web_server/http_server_request_handler.dart index 1bbe7d5..665c3ec 100644 --- a/lib/src/web_server/http_server_request_handler.dart +++ b/lib/src/web_server/http_server_request_handler.dart @@ -16,10 +16,13 @@ class _HttpServerRequestHandler { ".txt": const ["text", "plain"], ".png": const ["image", "png"], ".jpg": const ["image", "jpg"], + ".jpeg": const ["image", "jpeg"], ".gif": const ["image", "gif"], ".webp": const ["image", "webp"], + ".ico": const ["image", "ico"], ".svg": const ["image", "svg+xml"], ".otf": const ["font", "otf"], + ".eot": const ["font", "eot"], ".woff": const ["font", "woff"], ".woff2": const ["font", "woff2"], ".ttf": const ["font", "ttf"], @@ -38,7 +41,8 @@ class _HttpServerRequestHandler { final String path = httpRequest.uri.path; // Is there basic auth needed for this path. - if (this._doesThisPathRequireAuth(path)) { // BasicAuth IS required + if (this._doesThisPathRequireAuth(path)) { + // BasicAuth IS required final PathDataWithAuth pathDataWithAuthForPath = this._getAcceptedCredentialsForPath(path); final AuthCheckResults authCheckResults = this._checkAuthFromRequest(httpRequest, pathDataWithAuthForPath); @@ -50,11 +54,11 @@ class _HttpServerRequestHandler { } return; - } else { // BasicAuth is NOT required + } else { + // BasicAuth is NOT required // Check if the URL matches a registered file and that a URL ID is in the FunctionStore if (this._possibleFiles.containsKey(path) && - this._functionStore.fnStore.containsKey(this._possibleFiles[path])) - { + this._functionStore.fnStore.containsKey(this._possibleFiles[path])) { ServerLogger.log('Url has matched to a file. Routing to it...'); final int urlId = this._possibleFiles[path]; @@ -90,20 +94,20 @@ class _HttpServerRequestHandler { // Check if the URL matches a registered directory and that a URL ID is in the FunctionStore if (this._possibleDirectories.containsKey(possibleDirectoryPath) && - this._functionStore.fnStore.containsKey(this._possibleDirectories[possibleDirectoryPath])) - { + this._functionStore.fnStore.containsKey(this._possibleDirectories[possibleDirectoryPath])) { ServerLogger.log('Url has matched to a directory. Routing to it...'); final int urlId = this._possibleDirectories[possibleDirectoryPath]; this._functionStore.runEvent(urlId, httpRequest); - } else { // Respond with 404 error because nothing was matched. + } else { + // Respond with 404 error because nothing was matched. ServerLogger.log('No registered url match found.'); httpRequest.response - ..statusCode = HttpStatus.NOT_FOUND - ..headers.contentType = new ContentType("text", "plain", charset: "utf-8") - ..close(); + ..statusCode = HttpStatus.NOT_FOUND + ..headers.contentType = new ContentType("text", "plain", charset: "utf-8") + ..close(); } } } @@ -184,24 +188,24 @@ class _HttpServerRequestHandler { /// Send an HTTP 401 Auth required response static void sendRequiredBasicAuthResponse(final HttpRequest httpRequest, final String errMessage) { httpRequest.response - ..statusCode = HttpStatus.UNAUTHORIZED - ..headers.add(HttpHeaders.WWW_AUTHENTICATE, 'Basic realm="Enter credentials"') - ..write(errMessage) - ..close(); + ..statusCode = HttpStatus.UNAUTHORIZED + ..headers.add(HttpHeaders.WWW_AUTHENTICATE, 'Basic realm="Enter credentials"') + ..write(errMessage) + ..close(); } static void sendPageNotFoundResponse(final HttpRequest httpRequest, final String errMessage) { httpRequest.response - ..statusCode = HttpStatus.NOT_FOUND - ..write('404 - Page not found') - ..close(); + ..statusCode = HttpStatus.NOT_FOUND + ..write('404 - Page not found') + ..close(); } static void sendInternalServerErrorResponse(final HttpRequest httpRequest, final String errMessage) { httpRequest.response - ..statusCode = HttpStatus.INTERNAL_SERVER_ERROR - ..write('500 - Internal Server Error') - ..close(); + ..statusCode = HttpStatus.INTERNAL_SERVER_ERROR + ..write('500 - Internal Server Error') + ..close(); } Stream registerDirectory(final UrlData urlData) { @@ -223,8 +227,8 @@ class _HttpServerRequestHandler { * [isRelativeFilePath] (opt) - Is the [pathToFile] value a relative path? Default is true. */ Future serveStaticFile(final UrlData urlData, String pathToFile, { - final bool enableCaching: true, - final bool isRelativeFilePath: true + final bool enableCaching: true, + final bool isRelativeFilePath: true }) async { if (isRelativeFilePath) { pathToFile = '${path.dirname(Platform.script.path)}/$pathToFile'.replaceAll('%20', ' '); @@ -241,13 +245,16 @@ class _HttpServerRequestHandler { this._functionStore[urlData.id].listen((final HttpRequest httpRequest) async { String _localFileContents; - if (enableCaching == true) { // Use a cached file, or initialize the cached file, if enabled - if (_fileContents == null) { // If a version has not been cached before + if (enableCaching == true) { + // Use a cached file, or initialize the cached file, if enabled + if (_fileContents == null) { + // If a version has not been cached before _fileContents = await file.readAsString(); } _localFileContents = _fileContents; - } else if (enableCaching == false) { // Read freshly, if caching is not enabled + } else if (enableCaching == false) { + // Read freshly, if caching is not enabled _localFileContents = await file.readAsString(); } @@ -256,8 +263,8 @@ class _HttpServerRequestHandler { } httpRequest.response - ..write(_localFileContents) - ..close(); + ..write(_localFileContents) + ..close(); }); } else { ServerLogger.error('The file at path ($pathToFile) was not found in the filesystem; unable to serve it.'); @@ -272,8 +279,8 @@ class _HttpServerRequestHandler { * [enableCaching] - Should the file be cached in-memory; updates the cache when a newer copy is found. */ static Future serveFileWithAuth(final String pathToFile, { - final Map varModifiers: const {}, - final bool enableCaching: false + final Map varModifiers: const {}, + final bool enableCaching: false }) async { final File file = new File(pathToFile); @@ -293,11 +300,11 @@ class _HttpServerRequestHandler { * [shouldFollowLinks] - Should SymLinks be treated as they are in this directory and, therefore, served? */ Future serveVirtualDirectory(String pathToDirectory, final List supportedFileExtensions, { - final bool includeDirNameInPath: false, - final bool shouldFollowLinks: false, - final String prefixWithDirName: '', - final bool isRelativeDirPath: true, - final bool parseForFilesRecursively: true + final bool includeDirNameInPath: false, + final bool shouldFollowLinks: false, + final String prefixWithDirName: '', + final bool isRelativeDirPath: true, + final bool parseForFilesRecursively: true }) async { ServerLogger.log('_HttpServerRequestHandler.serveVirtualDirectory(String, List, {bool}) -> Future'); @@ -308,6 +315,9 @@ class _HttpServerRequestHandler { if (isRelativeDirPath) { pathToDirectory = '${path.dirname(Platform.script.path)}/$pathToDirectory'.replaceAll('%20', ' '); + if (Platform.operatingSystem == "windows") { + pathToDirectory = pathToDirectory.replaceFirst(new RegExp("/"), ''); + } } // Get the directory for virtualizing @@ -329,7 +339,7 @@ class _HttpServerRequestHandler { (includeDirNameInPath) ? pathToDirectory.replaceFirst(matchThisDirNameAtEnd, '') : pathToDirectory, prefixWithDirName + ((includeDirNameInPath) ? '/$thisDirName' : '') + entity.path.replaceFirst(matchPathToDirectoryAtStart, ''), ((includeDirNameInPath) ? '/$thisDirName' : '') + entity.path.replaceFirst(matchPathToDirectoryAtStart, '') - ); + ); if (shouldBeVerbose) { ServerLogger.log('Adding virtual file: ' + _virtualFileData.directoryPath + _virtualFileData.virtualFilePath + ' at Url: ' + _virtualFileData.virtualFilePath); @@ -346,7 +356,8 @@ class _HttpServerRequestHandler { } } - static void serveVirtualDirectoryWithAuth() {} + static void serveVirtualDirectoryWithAuth() { + } /** * Serve the file with zero processing done to it. @@ -364,16 +375,15 @@ class _HttpServerRequestHandler { // If the file needs to be read as bytes if (fileExtension == '.png' || - fileExtension == '.jpg' || - fileExtension == '.gif' || - fileExtension == '.webp' || - fileExtension == '.otf' || - fileExtension == '.woff' || - fileExtension == '.woff2' || - fileExtension == '.ttf' || - fileExtension == '.rar' || - fileExtension == '.zip') - { + fileExtension == '.jpg' || + fileExtension == '.gif' || + fileExtension == '.webp' || + fileExtension == '.otf' || + fileExtension == '.woff' || + fileExtension == '.woff2' || + fileExtension == '.ttf' || + fileExtension == '.rar' || + fileExtension == '.zip') { contentsOfFile = await standardFile.readAsBytes(); // Determine the content type to send @@ -388,11 +398,10 @@ class _HttpServerRequestHandler { // Do the bytes need to be converted back to characters? // (not sure if this is necessary, but readAsString() would otherwise fail for these types - probably charset?) if (fileExtension == '.otf' || - fileExtension == '.woff' || - fileExtension == '.woff2' || - fileExtension == '.ttf' || - fileExtension == '.zip') - { + fileExtension == '.woff' || + fileExtension == '.woff2' || + fileExtension == '.ttf' || + fileExtension == '.zip') { httpRequest.response.write(new String.fromCharCodes(contentsOfFile)); } else { httpRequest.response.write(contentsOfFile); @@ -411,16 +420,17 @@ class _HttpServerRequestHandler { httpRequest.response.write(contentsOfFile); } - } else { // File not found + } else { + // File not found ServerLogger.error('File not found at path: ($pathToFile)'); httpRequest.response - ..statusCode = HttpStatus.NOT_FOUND - ..headers.contentType = new ContentType("text", "plain", charset: "utf-8") - ..write(r'404 - Page not found') - ..close(); + ..statusCode = HttpStatus.NOT_FOUND + ..headers.contentType = new ContentType("text", "plain", charset: "utf-8") + ..write(r'404 - Page not found') + ..close(); } - } catch(err) { + } catch (err) { ServerLogger.error(err); } finally { httpRequest.response.close(); From 55cc78b9d71d1a3e30e2b260b2f86eae72f01c84 Mon Sep 17 00:00:00 2001 From: tthompson Date: Sat, 12 Mar 2016 19:33:45 -0600 Subject: [PATCH 2/3] Adding cache file to serveStaticFile, changed http://socialflare.us to http://www.socialflare.us in readme, added support for windows when serving files in serveStaticFile function, updated example --- LICENSE | 31 + bin/web_server.dart | 116 ++++ changelog.md | 213 +++++- example/virtual_directory.dart | 15 +- example/web_demo/static_page.html | 2 + .../{web_server.dart => web_server_misc.dart} | 36 +- lib/src/web_server/api_response.dart | 63 +- .../http_server_request_handler.dart | 628 ++++++++++++------ .../web_socket_request_payload.dart | 2 +- .../web_socket_server_request_handler.dart | 4 +- .../web_socket_object_store.dart | 2 +- .../ws_connection.dart | 2 +- lib/web_server.dart | 64 +- lib/web_socket_connection_manager.dart | 6 +- pubspec.yaml | 11 +- readme.md | 236 ++++++- 16 files changed, 1158 insertions(+), 273 deletions(-) create mode 100644 LICENSE create mode 100644 bin/web_server.dart create mode 100644 example/web_demo/static_page.html rename example/{web_server.dart => web_server_misc.dart} (57%) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..668b37a --- /dev/null +++ b/LICENSE @@ -0,0 +1,31 @@ +Copyright (c) 2014-2016, Brandon White +All rights reserved. + +Contributors: +Brandon White - jennexproject+webserver@gmail.com +Tyler Thompson - tyler@tylerthompson.me + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may + be used to endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/bin/web_server.dart b/bin/web_server.dart new file mode 100644 index 0000000..6b1fca3 --- /dev/null +++ b/bin/web_server.dart @@ -0,0 +1,116 @@ +import "dart:io"; +import "dart:async"; +import "package:web_server/web_server.dart" as webServer; + +/** + * Accepted command line arguments: + * > --port= + */ +Future main(final List args) async { + const Map SHORTHAND_TO_FULL_CMD_LINE_ARG_KEYS = const { + "h": "help" + }; + final Map cmdLineArgsMap = _parseCmdLineArgs(args, SHORTHAND_TO_FULL_CMD_LINE_ARG_KEYS); + InternetAddress hostAddr = InternetAddress.ANY_IP_V4; + int portNumber = 8080; // Default value. + + if (cmdLineArgsMap.containsKey('help')) { + _outputHelpDetails(); + exit(0); + } + + // Interpret the command line arguments if needed. + if (cmdLineArgsMap.containsKey('host') && cmdLineArgsMap['host'] is String) { + final RegExp _ipv4AddrRegExp = new RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$'); + + if (_ipv4AddrRegExp.hasMatch(cmdLineArgsMap['host'])) { + hostAddr = new InternetAddress(cmdLineArgsMap['host']); + } else { + stderr.writeln('The specified (--host=${cmdLineArgsMap['host']}) argument is invalid; only IPV4 addresses are accepted right now (e.g. --host=127.0.0.1).'); + exit(1); + } + } + + if (cmdLineArgsMap.containsKey('port')) { + if (cmdLineArgsMap['port'] is int) { + portNumber = cmdLineArgsMap['port']; + } else { + stderr.writeln('The specified (--port=${cmdLineArgsMap['port']}) argument is invalid; must be an integer value (e.g. --port=8080).'); + exit(1); + } + } + + final webServer.WebServer localWebServer = new webServer.WebServer(hostAddr, portNumber, hasHttpServer: true); + + stdout.writeln('WebServer started and listening for HTTP requests at the address: ${localWebServer.isSecure ? 'https' : 'http'}://${localWebServer.address.host}:$portNumber'); + + await localWebServer.httpServerHandler.serveStaticVirtualDirectory(Directory.current.path, shouldPreCache: false); + + // Handle errors + localWebServer.httpServerHandler + ..onErrorDocument(HttpStatus.NOT_FOUND, (final HttpRequest httpRequest) { + // Use the helper method from this WebServer package + webServer.HttpServerRequestHandler.sendPageNotFoundResponse(httpRequest, + '

${HttpStatus.NOT_FOUND} - Page not found

'); + }) + ..onErrorDocument(HttpStatus.INTERNAL_SERVER_ERROR, (final HttpRequest httpRequest) { + // Use the helper method from this WebServer package + webServer.HttpServerRequestHandler.sendInternalServerErrorResponse(httpRequest, + '

${HttpStatus.INTERNAL_SERVER_ERROR} - Internal Server Error

'); + }); +} + +Map _parseCmdLineArgs(final List cmdLineArgsList, [final Map argKeyMappingIndex = null]) { + final Map cmdLineArgsMap = {}; + final RegExp leadingDashesRegExp = new RegExp(r'^\-{1,2}'); + final RegExp keyValArgRegExp = new RegExp(r'^\-{1,2}[A-z]+\=\S+'); + final RegExp intValRegExp = new RegExp(r'^\-?\d+$'); + + cmdLineArgsList.forEach((final String cmdLineArg) { + if (cmdLineArg.startsWith(new RegExp('^\-{1,2}'))) { + if (cmdLineArg.startsWith(keyValArgRegExp)) { + final List _keyValPieces = cmdLineArg.split('='); + String _argKey = _keyValPieces[0].replaceFirst(leadingDashesRegExp, ''); + dynamic _argVal = _keyValPieces[1]; + + if (intValRegExp.hasMatch(_argVal)) { + _argVal = int.parse(_argVal); + } + + // Map the keyname, if needed. + if (argKeyMappingIndex != null && argKeyMappingIndex.containsKey(_argKey)) { + _argKey = argKeyMappingIndex[_argKey]; + } + + cmdLineArgsMap[_argKey] = _argVal; + } else { + String _argKey = cmdLineArg.replaceFirst(leadingDashesRegExp, ''); + + // Map the keyname, if needed. + if (argKeyMappingIndex != null && argKeyMappingIndex.containsKey(_argKey)) { + _argKey = argKeyMappingIndex[_argKey]; + } + + cmdLineArgsMap[_argKey] = true; + } + } + }); + + return cmdLineArgsMap; +} + +void _outputHelpDetails() { + final String outputHelpDetails = ''' +WebServer is a Dart package for serving files from a directory. + +Usage: web_server [arguments] + +Global options: +-h, --help Prints this usage information. + --host=
Bind the web server to the specified host address; the default is 0.0.0.0 (any available addresses). + --port= Uses the provided port number to bind the web server to; the default is 8080. + +See https://github.com/bwhite000/web-server for package details.'''; + + stdout.writeln(outputHelpDetails); +} \ No newline at end of file diff --git a/changelog.md b/changelog.md index a245b40..a03b4b6 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,213 @@ -WebServer -========= +WebServer Changelog +=================== + +v2.0.0+3 (3.5.2016) +------------------- + +### Tool Changes + +* `web_server` - pub global + * The developer can now pass a `--port=` argument to the executable in the command line to + specify a specific port, not just the default 8080, for the web server to bind itself to; e.g. + `web_server --port=80` + * Added an interpretation of the `--help` or `-h` argument to output details into the terminal about the + executable and its functionality. + * Minor understandability improvement to the output terminal log when the server is started. + * Added another optional developer command line argument for `--host=
` to allow binding to a + specific address, such as only `127.0.0.1`; the default value is `0.0.0.0`. + +### Documentation/Example Changes +* ReadMe + * Changed the filename to uppercase to be more inline with README file naming formats. + * Updated the examples to reflect the new parameters in the web_server tool. + * Added the very important details about reminding developers to run `pub global activate` on the package + every once and a while to get the latest updates; also included details about easily checking for breaking + changes before updating; hopefully the details will make it not-so-scary to update. +* ChangeLog + * Changed the filename to uppercase to be more inline with CHANGELOG file naming formats. +* Code Example Files + * `virtual_directory.dart` - (Issue #5) Updated the InternetAddress.LOOPBACK_IP_V4 to be ANY_IP_V4 in the + example to help out developers new to the package with avoiding the frustration of not catching that they + were binding only to a local host address when they try out their code on a remote machine and try to access + their web server. + +v2.0.0+2 (1.13.2016) +-------------------- + +### Tool Changes + +* `web_server` - pub global + * `shouldPreCache` has been changed to false since the Dart language seems to not be closing out filesystem + connections, even when explicitly coded to, and will error when setting up to serve directories with more + files than the computer's maximum file connection limit; will now cache as the files are requested instead + of in advance. + +v2.0.0+1 (12.22.2015) +--------------------- + +### Library Changes + +* `HttpServerRequestHandler` + * Added better comments to some methods for the DartDocs generator to make better use of. + * Removed the need for the '.' in the supportedFileExtensions extension list in `serveStaticVirtualDirectory` + that was unintentionally introduced in the previous release; functionality still behaved like before, + but the dot was needed; not anymore now. + +### Documentation/Example Changes + +* ReadMe + * Updated some code examples and section information to be easier to understand. + * Added a code example of preprocessing using the `html` Dart package to show that a developer can modify + web pages as richly and complexly as they could on the client side, but before even serving the page; this + way, the developer can have code templates in their web page's HTML, use it on the server like client side + DOM to fill it with data and clone it, if wanted, then return the built page to the client. + +* Code Example Files + * Cleaned-up and fixed the code to be efficiently and aesthetically better; e.g. added the `final` keyword + in a few missing spots. + +* Changelog + * Made the markdown data easier to read by dividing common change areas into sections; inspired by the + Changelog pattern in the main Dart SDK GitHub. + +v2.0.0 (12.20.2015) +------------------- + +### Library Changes + +* `WebServer` + * Added a new constructor for binding a secure server and switched the syntax to using the new Dart 1.13 + BoringSSL format instead of the old NSS method; use the `new WebServer.secure()` constructor to support this. + * Removed the 'GET' and 'POST' allowedMethods default value so that any type of request header will be + allowed by default; the developer will no longer have to specify every type of allowed request method. + * Bumped the default wait time for a response to generate from the server code (response deadline) from + 20 seconds to 30 seconds to allow for more complex response generation to not catch the developer off + guard as quickly in case the developer isn't coding with this deadline in mind. + +* `HttpServerRequestHandler` + * Added content types for .mp3, .ogg, .oga, .ogv. + * Changed the \_fileExtensions content types over to using the ContentType Object instead of a List + and having to build the ContentType Object for every request; also, removed the declaration of it being a + 'const' so that a developer using the server would be able to add their own new content types + programatically. + * Some code clean-up and optimizations for the occasional variable reuse optimization or the similar. + * Created a new format for listening for webpage path requests to make it easier to understand and more + intuitive by making the event format similar to binding DOM events on a webpage, like, for example, the + format of `querySelector('...').onClick.listen((_) {})`. + * Added a method and structures for opening the ability for developers using this package to add their own + custom error code handlers using `.onErrorDocument()`. + * Deprecated `serveVirtualDirectory()` in favor of the clearer and having more features, like being dynamic, + `serveStaticVirtualDirectory()` and `serveDynamicVirtualDirectory()` (arriving eventually; a.k.a. coming + soon). + * Renamed the `UrlData` Class to `UrlPath` to make it easier to understand what the Object is representing. + * `serveStaticVirtualDirectory` + * Made the requirement of providing a whitelist of supported file extensions + an optional parameter to allow for serving an entire directory, or a directory with flexible file types, + simple and not requiring a server restart; also, this makes it possible for the pub global `web_server` + command to operate with any directory's files. + * Removed a loop that was checking file extensions on every file entity for a match in the + `serveStaticVirtualDirectory` and is now doing a `.contains()` on the List to be much faster and loop + less. + * Removed the redundant `isRelativePath` parameter. + * Now, there is a parameter to enable pre-caching for files in this static Directory to make reads pull + from memory instead of the FileSystem, which will be much faster and economical. + * Improved the helper methods for sending 404 and 500 errors easily with `sendPageNotFoundResponse()` and + `sendInternalServerErrorResponse()`; will now use the supplied custom error response HTML from the developer, + if provided. + * `serveStaticFile:` Automatically detects relative file paths and more efficiently resolves the relative path + to access the static file; removed the redundant `isRelativePath` parameter. + +* Improved some of the verbose error logging by including the name of the Class and method in the error text to + make debugging easier (especially if manually working with a forked version of this repo and learning how it + works). + +### Tool Changes + +* `web_server` + * Added an `bin/` directory and an executable rule to the Pubspec to enable using this package with `pub global + activate` and running a server right from a directory using the command line without even having to write any + code! It's as simple as the `web_server` command from the terminal, and it will serve that directory as a + `serveStaticVirtualDirectory()` command. + +### Documentation/Example Changes + +* ReadMe + * Updates for the new methods and features; clarified and demonstrated features that might not have been as + well-known or exemplified before; added a testimonial for my work using this package in many side projects + and work-requested projects at Ebates. + * Added a section asking other developers to let me know if they are making something exciting using my Dart + package. + * Clarification to some of the code examples and section titles; added details about SocialFlare to the + "Who is using this package?" section. + +* Code Example Files + * Updated the code examples directory files to reflect the new API changes and additions from this release. + +* LICENSE + * Added a LICENSE file to allow other developers to use this code and for compatibility with the Dart Pub + requirements. + +v1.1.4 (5.14.2015) +------------------ +* Found that HttpRequest paths were not matching serveVirtualDirectory() generated paths on + Windows machines because the Url would be something like: '/main.dart' and Windows would + provide and store a path segment with the opposite separator at '\main.dart' resulting in + the String comparison to fail; this has been resolved. + +v1.1.3 (5.9.2015) +----------------- +* Added a handleRequestsStartingWith() method for intercepting requests starting with a specified + string; this is useful for handling everything in API patterns such as starting with '/api/'; + added an example to the examples folder in "example/web_server.dart". + +v1.1.2 (5.5.2015) +----------------- +* Removed a single inefficient .runtimeType use. +* Changed a mimetype definition and added more. +* Images and some binary sources were loading incorrectly, so switching to directly piping the + file contents into the HttpResponse, instead of reading, then adding. +* Fixed issue with the file extension not matching in serveVirtualDirectory if the extension + was not all lowercase. +* Solved issue with file and directory path building that would assemble incorrectly on Windows + machines. +* Changed the serveVirtualDirectory() parameter for "includeDirNameInPath" to + "includeContainerDirNameInPath" for parameter meaning clarity. +* Fixed a broken try/catch when loading a virtual file for a request. +* Made _VirtualDirectoryFileData easier to use by adding getters with clearer meaning such as + .absoluteFileSystemPath and .httpRequestPath. +* Greatly improved the efficiency of serveStandardFile for certain binary file formats and nicely + improved speed and memory for all binary file formats. +* Removed UTF-8 from being the default charset for non-matched mimetypes in binary files. +* Removed diffent handling in serveStandardFile based on the mimetype and now all use the same + piping to the HttpResponse. +* Included another log into shouldBeVerbose guide. +* Nicely clarified some confusing parts of the code. + +v1.1.1 (4.26.2015) +----------------- +* Removing the default UTF-8 charset requirement in the response header to allow for different + file encodings; will re-add the charset soon when encoding detection (appears to be difficult + at the moment) is implemented. +* Handling for non-UTF8 file encoding by piping the file bytes directly into the response without + passing through a string decoder first; last release, I understood this differently and the + behavior was not what I wanted it to behave as; stinkin' byte encoding detection. +* Will re-add the byte encoding to the content-type header soon; I really don't like leaving this + out in requests, but don't want to prevent clients from serving non-UTF8 files until this can + be determined. + +v1.1.0 (4.25.2015) +------------------ +* Renamed the WebServer.webServer library to just WebServer. +* Renamed the WebServer.webSocketConnectionManager library to just WebSocketConnectionManager. +* Added tons more docs, comments, and inline code examples; published Docs online using Jennex + as the host and placed the link in the Pubspec. +* Implemented the URI Object to make relative file path resolution more accurate. +* Possibly solved issue that appears on Windows when resolving relative paths in + serveVirtualDirectory() and serverStaticFile(). +* Added better honoring of the shouldBeVerbose parameter and changed to static property + [breaking API change]. +* UTF8 encoding was required for files to be read before, but now it will work with any + encoding and convert it to UTF8 during file read. v1.0.9 (4.16.2015) -------------------- @@ -13,6 +221,7 @@ v1.0.9 (4.16.2015) potential confusion for future developers using this Dart package; also added this to `serveStaticFile()`. * Improved some of the comments and code in the example files. +* Added an option to switch off recursive indexing in `serveVirtualDirectory()`. v1.0.8 (4.15.2015) ------------------ diff --git a/example/virtual_directory.dart b/example/virtual_directory.dart index 907d963..c50bf78 100644 --- a/example/virtual_directory.dart +++ b/example/virtual_directory.dart @@ -1,14 +1,15 @@ import "dart:io"; -import "package:web_server/web_server.dart"; +import "dart:async"; +import "package:web_server/web_server.dart" as webServer; -void main() { +Future main() async { // Initialize the WebServer - final WebServer localWebServer = new WebServer(InternetAddress.LOOPBACK_IP_V4, 8989, - hasHttpServer: true, hasWebSocketServer: true); + final webServer.WebServer localWebServer = new webServer.WebServer(InternetAddress.ANY_IP_V4, 8080, + hasHttpServer: true); - // Log out some of the connection information - print('Local web server started at: (http://${localWebServer.address.address}:${localWebServer.port})'); // http://127.0.0.1:8080 + // Log out some of the connection information. + stdout.writeln('Local web server started at: (http://${localWebServer.address.address}:${localWebServer.port})'); // http://127.0.0.1:8080 // Automatically parse for indexing and serve all recursive items in this directory matching the accepted file extensions. - localWebServer.httpServerHandler.serveVirtualDirectory('test_dir', const ['html', 'css', 'dart', 'js']); + await localWebServer.httpServerHandler.serveStaticVirtualDirectory('test_dir', shouldPreCache: true); } \ No newline at end of file diff --git a/example/web_demo/static_page.html b/example/web_demo/static_page.html new file mode 100644 index 0000000..d25075f --- /dev/null +++ b/example/web_demo/static_page.html @@ -0,0 +1,2 @@ +

Static Page

+

This is a test page.

\ No newline at end of file diff --git a/example/web_server.dart b/example/web_server_misc.dart similarity index 57% rename from example/web_server.dart rename to example/web_server_misc.dart index eb364c9..cce2284 100644 --- a/example/web_server.dart +++ b/example/web_server_misc.dart @@ -2,35 +2,40 @@ import "dart:io"; import "package:web_server/web_server.dart"; void main() { - // Initialize the WebServer - final WebServer localWebServer = new WebServer(InternetAddress.LOOPBACK_IP_V4, 8080, + // Initialize and bind the HTTP and WebSocket WebServer + final WebServer localWebServer = new WebServer(InternetAddress.ANY_IP_V4, 8080, hasHttpServer: true, hasWebSocketServer: true); // Log out some of the connection information - print('Local web server started at: (http://${localWebServer.address.address}:${localWebServer.port})'); // http://127.0.0.1:8080 + stdout.writeln('Local web server started at: (http://${localWebServer.address.address}:${localWebServer.port})'); // http://127.0.0.1:8080 + + HttpServerRequestHandler.shouldBeVerbose = true; // Attach HttpServer pages and event handlers localWebServer.httpServerHandler - // Gain handling of navigations to "/index.html" - ..registerFile(new UrlData('/index.html')).listen((final HttpRequest httpRequest) { /*...*/ }) + // Gain handling of navigation to "/index.html" + ..forRequestPath(new UrlPath('/index.html')).onRequest.listen((final HttpRequest httpRequest) { /*...*/ }) // Gain handling to ANY immediate sub-item in the directory; // serveVirtualDirectory() is preferred over this unless you need fine grain controls - ..registerDirectory(new UrlData('/img/profile_pics/80/')).listen((final HttpRequest httpRequest) { /*...*/ }) + ..registerDirectory(new UrlPath('/img/profile_pics/80/')).listen((final HttpRequest httpRequest) { /*...*/ }) // Automatically parse for indexing and serve all recursive items in this directory matching the accepted file extensions. - ..serveVirtualDirectory('web', const ['html', 'css', 'dart', 'js']) + ..serveStaticVirtualDirectory('example/web_demo', supportedFileExtensions: const ['html', 'css', 'dart', 'js']) // Automatically handle serving this file at navigation to '/static_page', with optional in-memory caching - ..serveStaticFile(new UrlData('/static_page'), 'web/static_page.html', enableCaching: false) + ..serveStaticFile(new UrlPath('/static_page'), 'example/web_demo/static_page.html', enableCaching: true) + + // Gain handling of all API requests, for example; catches all paths starting with the String in UrlData + ..handleRequestsStartingWith(new UrlPath('/api/')).listen((final HttpRequest httpRequest) {/*...*/}) // Handle requiring Basic Authentication on the specified Url, allowing only the users in the authentication list. // The required credentials are "user:password" (from the BasicAuth base64 encoded -> 'dXNlcjpwYXNzd29yZA==') - ..registerPathWithBasicAuth(new UrlData('/api/auth/required/dateTime'), const [ + ..registerPathWithBasicAuth(new UrlPath('/api/auth/required/dateTime'), const [ const AuthUserData('username', 'dXNlcjpwYXNzd29yZA==') // user:password --> Base64 ]).listen((final HttpRequest httpRequest) { // Create a new ApiResponse object for returning the API data; - // Value --> {"sucess": true, "dateTime": "XXXX-XX-XX XX:XX:XX.XXX"} + // Value --> {"success": true, "dateTime": "XXXX-XX-XX XX:XX:XX.XXX"} final ApiResponse apiResponse = new ApiResponse() ..addData('dateTime', '${new DateTime.now()}'); // Add the DateTime @@ -39,7 +44,16 @@ void main() { ..headers.contentType = ContentType.JSON // Set the 'content-type' header as JSON ..write(apiResponse.toJsonEncoded()) // Export as a JSON encoded string ..close(); - }); + }) + + // Add a custom function for handling the request in case of the error code supplied as the parameter. + ..onErrorDocument(HttpStatus.NOT_FOUND, (final HttpRequest httpRequest) { + httpRequest.response + ..statusCode = HttpStatus.NOT_FOUND + ..headers.contentType = new ContentType('text', 'html', charset: 'utf-8') + ..write('

404 Error accessing: ${httpRequest.requestedUri.path}

') + ..close(); + }); // Attach WebSocket command listeners and base events localWebServer.webSocketServerHandler diff --git a/lib/src/web_server/api_response.dart b/lib/src/web_server/api_response.dart index 606851b..c125198 100644 --- a/lib/src/web_server/api_response.dart +++ b/lib/src/web_server/api_response.dart @@ -1,17 +1,67 @@ -part of WebServer.webServer; +part of WebServer; /** - * Create this class and pass the toJsonEncoded() as an API respones for a successful API response. + * An output generator for successful API responses. + * + * // Create the Object for the response + * final ApiResponse apiResponse = new ApiResponse() + * ..addData('foodType', 'ice cream') // Add data + * ..addData('flavor', 'vanilla') + * ..addData('alternateFlavors', 5); // Add numeric values, too! + * + * // Send the data back through to the request + * httpRequest.response + * // Set to "application/json; charset=utf-8" + * ..headers.contentType = ContentType.JSON + * + * // Stringify the JSON output, then send to client + * ..write(apiResponse.toJsonEncoded()) + * + * ..close(); */ class ApiResponse { final Map _dataToAdd = {}; + /// Default constructor. ApiResponse(); + /** + * Add data to the response message. + * + * apiResponse.addData('animal', 'cat'); // Add Strings, + * + * apiResponse.addData('numberOfCats', 3); // numbers, + * + * // Lists, too, + * apiResponse.addData('furTypes', [ + * 'long', 'medium', 'short' + * ]); + * + * // and Maps! + * apiResponse.addData('catData', { + * "name": "Mr. Fluffle", + * "age": 3 + * }); + */ void addData(final String keyName, final dynamic value) { this._dataToAdd[keyName] = value; } + /** + * Output as Json. + * + * // Returns from the [addData] example above: + * { + * "success": true, // <-- For a 'false' value, use [ApiErrorResponse] Object + * "animal": "cat", + * "numberOfCats": 3, + * "furTypes": ["long", "medium", "short"], + * "catData": { + * "name": "Mr. Fluffle", + * "age": 3 + * } + * } + */ Map toJson() { final Map response = { "success": true @@ -24,16 +74,23 @@ class ApiResponse { return response; } + /** + * Calls [toJson], then processes it through [JSON.encode()] before returning. + */ String toJsonEncoded() { return JSON.encode(this.toJson()); } } /** - * Create this class and pass the toJsonEncoded() as an API response for something that went wrong. + * An output generator for API responses where something went wrong, such as forgetting + * a parameter or something erring server-side while generating the values. */ class ApiErrorResponse { + /// An optional message about the error. String errorMessage; + + /// An optional error code. String errorCode; ApiErrorResponse([final String this.errorMessage, final String this.errorCode]); diff --git a/lib/src/web_server/http_server_request_handler.dart b/lib/src/web_server/http_server_request_handler.dart index 665c3ec..310ee48 100644 --- a/lib/src/web_server/http_server_request_handler.dart +++ b/lib/src/web_server/http_server_request_handler.dart @@ -1,81 +1,150 @@ -part of WebServer.webServer; +part of WebServer; -class _HttpServerRequestHandler { +typedef ErrorPageListenerFn(HttpRequest httpRequest); + +/** + * This is part of the WebServer object used for setting up HttpRequest + * handlers. + */ +class HttpServerRequestHandler { final FunctionStore _functionStore = new FunctionStore(); final Map _possibleFiles = {}; final Map _possibleDirectories = {}; final List<_VirtualDirectoryFileData> _virtualDirectoryFiles = <_VirtualDirectoryFileData>[]; - final List _pathDataForAuthList = []; + final List<_PathDataWithAuth> _pathDataForAuthList = <_PathDataWithAuth>[]; + final List _urlPathStartString = []; + + /// The message text that will be returned in the response when a BasicAuth request fails. final String strForUnauthorizedError = '401 - Unauthorized'; - static const Map> _fileExtensions = const >{ - ".html": const ["text", "html"], - ".css": const ["text", "css"], - ".js": const ["text", "javascript"], - ".dart": const ["application", "dart"], - ".txt": const ["text", "plain"], - ".png": const ["image", "png"], - ".jpg": const ["image", "jpg"], - ".jpeg": const ["image", "jpeg"], - ".gif": const ["image", "gif"], - ".webp": const ["image", "webp"], - ".ico": const ["image", "ico"], - ".svg": const ["image", "svg+xml"], - ".otf": const ["font", "otf"], - ".eot": const ["font", "eot"], - ".woff": const ["font", "woff"], - ".woff2": const ["font", "woff2"], - ".ttf": const ["font", "ttf"], - ".rar": const ["application", "x-rar-compressed"], - ".zip": const ["application", "zip"] + static Map _fileExtensions = { + ".html": new ContentType("text", "html"), + ".css": new ContentType("text", "css"), + ".js": new ContentType("text", "javascript"), + ".dart": new ContentType("application", "dart"), + ".txt": new ContentType("text", "plain"), + ".png": new ContentType("image", "png"), + ".jpg": new ContentType("image", "jpeg"), + ".jpeg": new ContentType("image", "jpeg"), + ".gif": new ContentType("image", "gif"), + ".ico": new ContentType("image", "x-icon"), + ".webp": new ContentType("image", "webp"), + ".mp3": new ContentType("audio", "mpeg3"), + ".oga": new ContentType("audio", "ogg"), + ".ogv": new ContentType("video", "ogg"), + ".ogg": new ContentType("application", "ogg"), + ".svg": new ContentType("image", "svg+xml"), + ".otf": new ContentType("font", "otf"), + ".woff": new ContentType("font", "woff"), + ".woff2": new ContentType("font", "woff2"), + ".ttf": new ContentType("font", "ttf"), + ".rar": new ContentType("application", "x-rar-compressed"), + ".zip": new ContentType("application", "zip") }; - bool shouldBeVerbose = false; + static bool shouldBeVerbose = false; + // The int is the HttpStatus + final Map _errorCodeListenerFns = {}; + + HttpServerRequestHandler(); + + // Getter + void onErrorDocument(final int httpStatus, ErrorPageListenerFn errorPageListenerFn) { + this._errorCodeListenerFns[httpStatus] = errorPageListenerFn; + } - _HttpServerRequestHandler(); + void _callListenerForErrorDocument(final int httpStatus, final HttpRequest httpRequest) { + if (this._errorCodeListenerFns.containsKey(httpStatus)) { + // Set the default status code, but the developer is welcome to override it in their error handler function + httpRequest.response.statusCode = httpStatus; + + this._errorCodeListenerFns[httpStatus](httpRequest); + } else { // Default handler + httpRequest.response + ..statusCode = httpStatus + ..headers.contentType = new ContentType('text', 'plain', charset: 'utf-8') + ..write('$httpStatus Error') + ..close(); + } + } // Util - void _onHttpRequest(final HttpRequest httpRequest) { - ServerLogger.log('_HttpServerRequestHandler.onRequest()'); - ServerLogger.log('Requested Url: ${httpRequest.uri.path}'); + Future _onHttpRequest(final HttpRequest httpRequest) async { + if (HttpServerRequestHandler.shouldBeVerbose) { + ServerLogger.log('_HttpServerRequestHandler.onRequest()'); + ServerLogger.log('Requested Url: ${httpRequest.uri.path}'); + } - final String path = httpRequest.uri.path; + final String requestPath = httpRequest.uri.path; // Is there basic auth needed for this path. - if (this._doesThisPathRequireAuth(path)) { - // BasicAuth IS required - final PathDataWithAuth pathDataWithAuthForPath = this._getAcceptedCredentialsForPath(path); - final AuthCheckResults authCheckResults = this._checkAuthFromRequest(httpRequest, pathDataWithAuthForPath); + if (this._doesThisPathRequireAuth(requestPath)) { // BasicAuth IS required + final _PathDataWithAuth pathDataWithAuthForPath = this._getAcceptedCredentialsForPath(requestPath); + final _AuthCheckResults authCheckResults = this._checkAuthFromRequest(httpRequest, pathDataWithAuthForPath); if (authCheckResults.didPass) { - final int urlId = this._possibleFiles[path]; + final int urlId = this._possibleFiles[requestPath]; + this._functionStore.runEvent(urlId, httpRequest); } else { - _HttpServerRequestHandler.sendRequiredBasicAuthResponse(httpRequest, this.strForUnauthorizedError); + HttpServerRequestHandler.sendRequiredBasicAuthResponse(httpRequest, this.strForUnauthorizedError); } return; - } else { - // BasicAuth is NOT required + } else { // BasicAuth is NOT required + // Is this a 'startsWith' registered path? + for (UrlPath _urlData in this._urlPathStartString) { + if (requestPath.startsWith(_urlData.path)) { + this._functionStore.runEvent(_urlData.id, httpRequest); + + return; + } + } + // Check if the URL matches a registered file and that a URL ID is in the FunctionStore - if (this._possibleFiles.containsKey(path) && - this._functionStore.fnStore.containsKey(this._possibleFiles[path])) { - ServerLogger.log('Url has matched to a file. Routing to it...'); + // NOTE: This format is being deprecated in favor of using the RequestPath Object. + if (this._possibleFiles.containsKey(requestPath) && + this._functionStore.fnStore.containsKey(this._possibleFiles[requestPath])) + { + if (HttpServerRequestHandler.shouldBeVerbose) ServerLogger.log('Url has matched to a file. Routing to it...'); - final int urlId = this._possibleFiles[path]; + final int urlId = this._possibleFiles[requestPath]; this._functionStore.runEvent(urlId, httpRequest); + } else if (RequestPath._possibleUrlDataFormats.containsKey(requestPath) && + RequestPath._functionStore.fnStore.containsKey(RequestPath._possibleUrlDataFormats[requestPath])) + { + if (HttpServerRequestHandler.shouldBeVerbose) ServerLogger.log('Url has matched to a file in RequestPath Object. Routing to it...'); + + final int urlId = RequestPath._possibleUrlDataFormats[requestPath]; + + RequestPath._functionStore.runEvent(urlId, httpRequest); } else { bool wasVirtualFileMatched = false; + // Look for the request path in the registered virtual file list for (_VirtualDirectoryFileData virtualFilePathData in this._virtualDirectoryFiles) { // If the requested path matches a virtual path - if (httpRequest.uri.path == virtualFilePathData.virtualFilePathWithPrefix) { + if (requestPath == virtualFilePathData.httpRequestPath) { wasVirtualFileMatched = true; - try { + + final String fileContents = await Cache.matchFile(new Uri.file(virtualFilePathData.absoluteFileSystemPath)); + + // If the fileContents are not empty, then the file must be present in the Cache; + // otherwise, read the file and serve it as a standard served file. + if (fileContents != null) { + final String extension = path.extension(virtualFilePathData.absoluteFileSystemPath); + + // Check if the file extension matches a registered one, then add the Http response header for it, if it matches. + if (HttpServerRequestHandler._fileExtensions.containsKey(extension)) { + httpRequest.response.headers.contentType = HttpServerRequestHandler._fileExtensions[extension]; + } + + httpRequest.response + ..write(fileContents) + ..close(); + } else { // Serve the matched virtual file - _HttpServerRequestHandler._serveStandardFile('${virtualFilePathData.directoryPath}${virtualFilePathData.virtualFilePath}', httpRequest); - } catch (err) { - ServerLogger.error(err); + this._serveStandardFile('${virtualFilePathData.containerDirectoryPath}${virtualFilePathData.filePathFromContainerDirectory}', httpRequest).catchError(ServerLogger.error); } break; @@ -94,20 +163,17 @@ class _HttpServerRequestHandler { // Check if the URL matches a registered directory and that a URL ID is in the FunctionStore if (this._possibleDirectories.containsKey(possibleDirectoryPath) && - this._functionStore.fnStore.containsKey(this._possibleDirectories[possibleDirectoryPath])) { - ServerLogger.log('Url has matched to a directory. Routing to it...'); + this._functionStore.fnStore.containsKey(this._possibleDirectories[possibleDirectoryPath])) + { + if (HttpServerRequestHandler.shouldBeVerbose) ServerLogger.log('Url has matched to a directory. Routing to it...'); final int urlId = this._possibleDirectories[possibleDirectoryPath]; this._functionStore.runEvent(urlId, httpRequest); - } else { - // Respond with 404 error because nothing was matched. - ServerLogger.log('No registered url match found.'); - - httpRequest.response - ..statusCode = HttpStatus.NOT_FOUND - ..headers.contentType = new ContentType("text", "plain", charset: "utf-8") - ..close(); + } else { // Respond with 404 error because nothing was matched. + if (HttpServerRequestHandler.shouldBeVerbose) ServerLogger.log('No registered url match found.'); + + this._callListenerForErrorDocument(HttpStatus.NOT_FOUND, httpRequest); } } } @@ -116,27 +182,34 @@ class _HttpServerRequestHandler { /** * Register a file and return a Stream for adding a listeners to when that filepath is requested. + * + * DEPRECATED: Please begin using forUrlData(UrlData).onRequest.listen() instead. */ - Stream registerFile(final UrlData urlData) { + @deprecated + Stream registerFile(final UrlPath urlData) { this._possibleFiles[urlData.path] = urlData.id; return this._functionStore[urlData.id]; } + RequestPath forRequestPath(final UrlPath urlPath) { + return new RequestPath(urlPath); + } + /** * Require basic authentication by the client to view this Url path. * * [pathToRegister] - The path that will navigated to in order to call this; e.g. "/support/client/contact-us" * [authUserList] - A list of */ - Stream registerPathWithBasicAuth(final UrlData pathToRegister, final List authUserList) { - ServerLogger.log('HttpServerRequestHandler.registerPathWithAuth() -> Stream'); + Stream registerPathWithBasicAuth(final UrlPath pathToRegister, final List authUserList) { + if (HttpServerRequestHandler.shouldBeVerbose) ServerLogger.log('HttpServerRequestHandler.registerPathWithAuth() -> Stream'); if (authUserList.length == 0) { throw 'There are no users in the list of authorized users.'; } - final PathDataWithAuth pathDataWithAuth = new PathDataWithAuth(pathToRegister.path, authUserList); + final _PathDataWithAuth pathDataWithAuth = new _PathDataWithAuth(pathToRegister.path, authUserList); this._pathDataForAuthList.add(pathDataWithAuth); this._possibleFiles[pathToRegister.path] = pathToRegister.id; @@ -146,7 +219,7 @@ class _HttpServerRequestHandler { /// Does this request path need to be handled by the authentication engine? bool _doesThisPathRequireAuth(final String pathName) { - for (PathDataWithAuth pathDataWithAuth in this._pathDataForAuthList) { + for (_PathDataWithAuth pathDataWithAuth in this._pathDataForAuthList) { // Do the paths match? if (pathDataWithAuth.urlPath == pathName) { return true; @@ -156,8 +229,8 @@ class _HttpServerRequestHandler { return false; } - PathDataWithAuth _getAcceptedCredentialsForPath(final String pathName) { - for (PathDataWithAuth pathDataWithAuth in this._pathDataForAuthList) { + _PathDataWithAuth _getAcceptedCredentialsForPath(final String pathName) { + for (_PathDataWithAuth pathDataWithAuth in this._pathDataForAuthList) { // Do the paths match? if (pathDataWithAuth.urlPath == pathName) { return pathDataWithAuth; @@ -167,10 +240,10 @@ class _HttpServerRequestHandler { return null; } - AuthCheckResults _checkAuthFromRequest(final HttpRequest httpRequest, final PathDataWithAuth acceptedCredentialsPathData) { + _AuthCheckResults _checkAuthFromRequest(final HttpRequest httpRequest, final _PathDataWithAuth acceptedCredentialsPathData) { // If no auth header supplied if (httpRequest.headers.value(HttpHeaders.AUTHORIZATION) == null) { - return const AuthCheckResults(false); + return const _AuthCheckResults(false); } const int MAX_ALLOWED_CHARACTER_RANGE = 256; @@ -179,36 +252,40 @@ class _HttpServerRequestHandler { final String clientProvidedAuthInfo = authHeaderStr.substring(0, trimRange).replaceFirst(new RegExp('^Basic '), ''); // Remove the prefixed "Basic " from auth header if (acceptedCredentialsPathData.doCredentialsMatch(clientProvidedAuthInfo)) { - return new AuthCheckResults(true, acceptedCredentialsPathData.getUsernameForCredentials(clientProvidedAuthInfo)); + return new _AuthCheckResults(true, acceptedCredentialsPathData.getUsernameForCredentials(clientProvidedAuthInfo)); } - return const AuthCheckResults(false); + return const _AuthCheckResults(false); } - /// Send an HTTP 401 Auth required response + /// Helper for sending an HTTP 401 Auth required response static void sendRequiredBasicAuthResponse(final HttpRequest httpRequest, final String errMessage) { httpRequest.response - ..statusCode = HttpStatus.UNAUTHORIZED - ..headers.add(HttpHeaders.WWW_AUTHENTICATE, 'Basic realm="Enter credentials"') - ..write(errMessage) - ..close(); + ..statusCode = HttpStatus.UNAUTHORIZED + ..headers.add(HttpHeaders.WWW_AUTHENTICATE, 'Basic realm="Enter credentials"') + ..write(errMessage) + ..close(); } - static void sendPageNotFoundResponse(final HttpRequest httpRequest, final String errMessage) { + /// Helper for sending a HTTP 404 response with an optional custom HTML error message. + static void sendPageNotFoundResponse(final HttpRequest httpRequest, [final String responseVal = '404 - Page not found']) { httpRequest.response - ..statusCode = HttpStatus.NOT_FOUND - ..write('404 - Page not found') - ..close(); + ..statusCode = HttpStatus.NOT_FOUND + ..headers.contentType = new ContentType('text', 'html', charset: 'utf-8') + ..write(responseVal) + ..close(); } - static void sendInternalServerErrorResponse(final HttpRequest httpRequest, final String errMessage) { + /// Helper for sending an HTTP 500 response with an optional custom HTML error message. + static void sendInternalServerErrorResponse(final HttpRequest httpRequest, [final String responseVal = '500 - Internal Server Error']) { httpRequest.response - ..statusCode = HttpStatus.INTERNAL_SERVER_ERROR - ..write('500 - Internal Server Error') - ..close(); + ..statusCode = HttpStatus.INTERNAL_SERVER_ERROR + ..headers.contentType = new ContentType('text', 'html', charset: 'utf-8') + ..write(responseVal) + ..close(); } - Stream registerDirectory(final UrlData urlData) { + Stream registerDirectory(final UrlPath urlData) { if (urlData.path.endsWith('/') == false) { throw 'Urls registered as directories must end with a trailing forward slash ("/"); e.g. "/profile_pics/80/".'; } @@ -224,32 +301,45 @@ class _HttpServerRequestHandler { * [urlData] - The path to navigate to in your browser to load this file. * [pathToFile] - The path on your computer to read the file contents from. * [enableCaching] (opt) - Should this file be cached in memory after it is first read? Default is true. - * [isRelativeFilePath] (opt) - Is the [pathToFile] value a relative path? Default is true. */ - Future serveStaticFile(final UrlData urlData, String pathToFile, { - final bool enableCaching: true, - final bool isRelativeFilePath: true + Future serveStaticFile(final UrlPath urlData, String pathToFile, { + final bool enableCaching: true }) async { - if (isRelativeFilePath) { - pathToFile = '${path.dirname(Platform.script.path)}/$pathToFile'.replaceAll('%20', ' '); + // Is the provided path a relative path that needs to be made absolute? + if (path.isRelative(pathToFile)) { + pathToFile = path.join(Directory.current.path, pathToFile); + + if (HttpServerRequestHandler.shouldBeVerbose) ServerLogger.log('Resolved the Uri to be: ($pathToFile)'); } + // Checking the file system for the file final File file = new File(pathToFile); if (await file.exists()) { - String _fileContents; /// The contents of the file, if caching is enabled + // The file exists, lets configure the http request and serve it + + String _fileContents; + + /// The contents of the file, if caching is enabled final ContentType _contentType = getContentTypeForFilepathExtension(pathToFile); this._possibleFiles[urlData.path] = urlData.id; this._functionStore[urlData.id].listen((final HttpRequest httpRequest) async { + String _localFileContents; if (enableCaching == true) { // Use a cached file, or initialize the cached file, if enabled + + _fileContents = await Cache.matchFile(new Uri.file(pathToFile)); + if (_fileContents == null) { // If a version has not been cached before _fileContents = await file.readAsString(); + if (HttpServerRequestHandler.shouldBeVerbose) ServerLogger.log('adding $pathToFile to cache'); + // Store the file in the cache for serving next load + Cache.addFile(new Uri.file(pathToFile, windows: Platform.isWindows)); } _localFileContents = _fileContents; @@ -267,7 +357,7 @@ class _HttpServerRequestHandler { ..close(); }); } else { - ServerLogger.error('The file at path ($pathToFile) was not found in the filesystem; unable to serve it.'); + if (HttpServerRequestHandler.shouldBeVerbose) ServerLogger.error('The file at path ($pathToFile) was not found in the filesystem; unable to serve it.'); } } @@ -278,9 +368,10 @@ class _HttpServerRequestHandler { * [varModifiers] - A key/value map of modifiers to automatically replace in the file * [enableCaching] - Should the file be cached in-memory; updates the cache when a newer copy is found. */ + /* static Future serveFileWithAuth(final String pathToFile, { - final Map varModifiers: const {}, - final bool enableCaching: false + final Map varModifiers: const {}, + final bool enableCaching: false }) async { final File file = new File(pathToFile); @@ -290,154 +381,243 @@ class _HttpServerRequestHandler { ServerLogger.error('The file at path ($pathToFile) was not found in the filesystem; unable to serve it.'); } } + */ + + // Deprecating in favor of serverStaticVirtualDirectory and serveDynamicVirtualDirectory + @deprecated + Future serveVirtualDirectory(String pathToDirectory, final List supportedFileExtensions, { + final bool includeContainerDirNameInPath: false, + final bool shouldFollowLinks: false, + final String prefixWithPseudoDirName: '', + final bool isRelativeDirPath: true, + final bool parseForFilesRecursively: true + }) { + return this.serveStaticVirtualDirectory(pathToDirectory, + supportedFileExtensions: supportedFileExtensions, + includeContainerDirNameInPath: includeContainerDirNameInPath, + shouldFollowLinks: shouldFollowLinks, + prefixWithPseudoDirName: prefixWithPseudoDirName, + parseForFilesRecursively: parseForFilesRecursively); + } /** - * Serve this entire directory automatically, but only for the allowed file extensions. + * Serve this entire directory automatically, but only for the allowed file extensions. Parses the + * files in the Directory when the server is started, and will reflect changes to those files, but + * will not serve files newly added to the directory after the static scraping has happened. * * [pathToDirectory] - The path to this directory to server files recursively from. * [supportedFileExtensions] - A list of file extensions (without the "." before the extension name) that are allowed to be served from this directory. - * [includeDirNameInPath] - Should the folder being served also have it's name in the browser navigation path; such as serving a 'js/' folder while retaining 'js/' in the browser Url; default is false. + * [includeContainerDirNameInPath] - Should the folder being served also have it's name in the browser navigation path; such as serving a 'js/' folder while retaining 'js/' in the browser Url; default is false. * [shouldFollowLinks] - Should SymLinks be treated as they are in this directory and, therefore, served? + * [prefixWithPseudoDirName] + * [parseForFilesRecursively] + * + * new WebServer().serveVirtualDirectory('web/js', + * supportedFileExtensions: ['html', 'dart', 'js', 'css'], + * shouldPreCache: true, + * parseForFilesRecursively: false); */ - Future serveVirtualDirectory(String pathToDirectory, final List supportedFileExtensions, { - final bool includeDirNameInPath: false, - final bool shouldFollowLinks: false, - final String prefixWithDirName: '', - final bool isRelativeDirPath: true, - final bool parseForFilesRecursively: true + Future serveStaticVirtualDirectory(String pathToDirectory, { + final List supportedFileExtensions: null, + final bool shouldPreCache: false, + final bool includeContainerDirNameInPath: false, + final bool shouldFollowLinks: false, + final String prefixWithPseudoDirName: '', + final bool parseForFilesRecursively: true }) async { - ServerLogger.log('_HttpServerRequestHandler.serveVirtualDirectory(String, List, {bool}) -> Future'); + if (HttpServerRequestHandler.shouldBeVerbose) ServerLogger.log('_HttpServerRequestHandler.serveVirtualDirectory(String, List, {bool}) -> Future'); + + final Completer completer = new Completer(); - // Make sure that supported file extensions were supplied. - if (supportedFileExtensions == null || supportedFileExtensions.length == 0) { - throw 'There were no supported file extensions set. Nothing would have been included from this directory.'; + // Make sure that more than zero supported file extensions were supplied, if a List was supplied. + if (supportedFileExtensions != null && supportedFileExtensions.length == 0) { + throw 'There were no supported file extensions set in the List. Nothing would have been included from this directory.'; } - if (isRelativeDirPath) { - pathToDirectory = '${path.dirname(Platform.script.path)}/$pathToDirectory'.replaceAll('%20', ' '); - if (Platform.operatingSystem == "windows") { - pathToDirectory = pathToDirectory.replaceFirst(new RegExp("/"), ''); - } + // Is the provided directory path for virtualizing a relative path that needs to be made absolute? + if (path.isRelative(pathToDirectory)) { + pathToDirectory = path.join(Directory.current.path, pathToDirectory); + + if (HttpServerRequestHandler.shouldBeVerbose) ServerLogger.log('Resolved the Uri to be: ($pathToDirectory)'); } - // Get the directory for virtualizing + // Get the directory for virtualizing. final Directory dir = new Directory(pathToDirectory); - final String thisDirName = path.basename(pathToDirectory); - final RegExp matchThisDirNameAtEnd = new RegExp('/' + thisDirName + r'$'); - final RegExp matchPathToDirectoryAtStart = new RegExp(r'^' + pathToDirectory); // If the directory exists if (await dir.exists()) { + // The directory entity looper will not hold this method from returning when using `await`, + // so this List must be used to add all of the Futures to and wait for them to complete. + final List _queueOfCacheEventsToWaitFor = []; + // Loop through all of the entities in this directory and determine which ones to make serve later. dir.list(recursive: parseForFilesRecursively, followLinks: shouldFollowLinks).listen((final FileSystemEntity entity) async { final FileStat fileStat = await entity.stat(); - for (String supportedFileExtension in supportedFileExtensions) { - // If this is a file AND ends with a supported file extension - if (fileStat.type == FileSystemEntityType.FILE && entity.path.endsWith('.$supportedFileExtension')) { - final _VirtualDirectoryFileData _virtualFileData = new _VirtualDirectoryFileData( - (includeDirNameInPath) ? pathToDirectory.replaceFirst(matchThisDirNameAtEnd, '') : pathToDirectory, - prefixWithDirName + ((includeDirNameInPath) ? '/$thisDirName' : '') + entity.path.replaceFirst(matchPathToDirectoryAtStart, ''), - ((includeDirNameInPath) ? '/$thisDirName' : '') + entity.path.replaceFirst(matchPathToDirectoryAtStart, '') - ); - - if (shouldBeVerbose) { - ServerLogger.log('Adding virtual file: ' + _virtualFileData.directoryPath + _virtualFileData.virtualFilePath + ' at Url: ' + _virtualFileData.virtualFilePath); - } + // Don't process if this is not a file. + if (fileStat.type != FileSystemEntityType.FILE) { + return; + } - this._virtualDirectoryFiles.add(_virtualFileData); + // Does this Filesystem entity need to be filtered by its file extension? + if (supportedFileExtensions != null) { + // Change the returned '.html' to 'html', for example, to match the supportedFileExtensions list. + final String _extWithoutDot = path.extension(entity.path).replaceFirst(new RegExp(r'^\.'), ''); - break; + if (supportedFileExtensions.contains(_extWithoutDot)) { + _addFileToVirtualDirectoryListing(entity, pathToDirectory, includeContainerDirNameInPath, prefixWithPseudoDirName); + + if (shouldPreCache) { + _queueOfCacheEventsToWaitFor.add(Cache.addFile(entity.uri, shouldPreCache: true)); + } + } + } else { + _addFileToVirtualDirectoryListing(entity, pathToDirectory, includeContainerDirNameInPath, prefixWithPseudoDirName); + + if (shouldPreCache) { + _queueOfCacheEventsToWaitFor.add(Cache.addFile(entity.uri, shouldPreCache: true)); } } + }, onDone: () { + // If there are files to wait for to add to Cache, wait for all of these to return. + if (_queueOfCacheEventsToWaitFor.isNotEmpty) { + Future.wait(_queueOfCacheEventsToWaitFor).then((_) { + completer.complete(); + }); + } else { + completer.complete(); + } }); } else { ServerLogger.error('The directory path supplied was not found in the filesystem at: (${dir.path})'); + + completer.complete(); } + + return completer.future; } - static void serveVirtualDirectoryWithAuth() { + void _addFileToVirtualDirectoryListing(final FileSystemEntity entity, + final String pathToDirectory, + final bool includeContainerDirNameInPath, + final String prefixWithPseudoDirName) + { + final String _containerDirectoryPath = pathToDirectory; + final String _filePathFromContainerDirectory = entity.path.replaceFirst(_containerDirectoryPath, ''); + String _optPrefix = (includeContainerDirNameInPath) ? path.basename(_containerDirectoryPath) : ''; + + if (prefixWithPseudoDirName != null && + prefixWithPseudoDirName.isNotEmpty) + { + if (_optPrefix.isNotEmpty) { + _optPrefix = prefixWithPseudoDirName + '/' + _optPrefix; // 'psuedoPrefix' + '/' + 'web'; + } else { + _optPrefix = prefixWithPseudoDirName; // 'pseudoPrefix'; + } + } + + final _VirtualDirectoryFileData _virtualFileData = new _VirtualDirectoryFileData( + _containerDirectoryPath, + _filePathFromContainerDirectory, + _optPrefix + ); + + if (HttpServerRequestHandler.shouldBeVerbose) { + ServerLogger.log('Adding virtual file: ' + _virtualFileData.absoluteFileSystemPath + ' at Url: ' + _virtualFileData.httpRequestPath); + } + + this._virtualDirectoryFiles.add(_virtualFileData); } + // Coming soon! (commented at 12.20.2015 during v2.0.0 development) + //Future serveDynamicVirtualDirectory() async {} + + /** + * All HTTP requests starting the the specified [UrlPath] path String parameter will be + * forwarded to the attached event listener. + * + * This is a useful method for catching all API prefixed path requests and handling them + * in your own style: + * + * .handleRequestsStartingWith(new UrlData('/api/')).listen(apiRouter); + */ + Stream handleRequestsStartingWith(final UrlPath urlPathStartData) { + this._urlPathStartString.add(urlPathStartData); + + return this._functionStore[urlPathStartData.id]; + } + + // Arriving eventually! + // void serveVirtualDirectoryWithAuth() {} + /** * Serve the file with zero processing done to it. */ - static Future _serveStandardFile(final String pathToFile, final HttpRequest httpRequest) async { + Future _serveStandardFile(final String pathToFile, final HttpRequest httpRequest) async { try { - ServerLogger.log('_HttpServerRequestHandler::_serveStandardFile(String, HttpRequest) -> Future'); + if (HttpServerRequestHandler.shouldBeVerbose) ServerLogger.log('_HttpServerRequestHandler::_serveStandardFile(String, HttpRequest) -> Future'); final File standardFile = new File(pathToFile); // Does the file exist? if (await standardFile.exists()) { final String fileExtension = path.extension(standardFile.path); - dynamic contentsOfFile; - - // If the file needs to be read as bytes - if (fileExtension == '.png' || - fileExtension == '.jpg' || - fileExtension == '.gif' || - fileExtension == '.webp' || - fileExtension == '.otf' || - fileExtension == '.woff' || - fileExtension == '.woff2' || - fileExtension == '.ttf' || - fileExtension == '.rar' || - fileExtension == '.zip') { - contentsOfFile = await standardFile.readAsBytes(); - - // Determine the content type to send - if (_HttpServerRequestHandler._fileExtensions.containsKey(fileExtension)) { - final List _mimeTypePieces = _HttpServerRequestHandler._fileExtensions[path.extension(standardFile.path)]; - - httpRequest.response.headers.contentType = new ContentType(_mimeTypePieces[0], _mimeTypePieces[1]); - } else { - httpRequest.response.headers.contentType = new ContentType("text", "plain", charset: "utf-8"); - } - - // Do the bytes need to be converted back to characters? - // (not sure if this is necessary, but readAsString() would otherwise fail for these types - probably charset?) - if (fileExtension == '.otf' || - fileExtension == '.woff' || - fileExtension == '.woff2' || - fileExtension == '.ttf' || - fileExtension == '.zip') { - httpRequest.response.write(new String.fromCharCodes(contentsOfFile)); - } else { - httpRequest.response.write(contentsOfFile); - } - } else { - contentsOfFile = await standardFile.readAsString(); - // Determine the content type to send - if (_HttpServerRequestHandler._fileExtensions.containsKey(fileExtension)) { - final List _mimeTypePieces = _HttpServerRequestHandler._fileExtensions[path.extension(standardFile.path)]; - - httpRequest.response.headers.contentType = new ContentType(_mimeTypePieces[0], _mimeTypePieces[1], charset: "utf-8"); - } else { - httpRequest.response.headers.contentType = new ContentType("text", "plain", charset: "utf-8"); - } - - httpRequest.response.write(contentsOfFile); + // Determine the content-type to send, if possible + if (HttpServerRequestHandler._fileExtensions.containsKey(fileExtension)) { + httpRequest.response.headers.contentType = HttpServerRequestHandler._fileExtensions[fileExtension]; } - } else { - // File not found - ServerLogger.error('File not found at path: ($pathToFile)'); - httpRequest.response - ..statusCode = HttpStatus.NOT_FOUND - ..headers.contentType = new ContentType("text", "plain", charset: "utf-8") - ..write(r'404 - Page not found') - ..close(); + // Read the file, and send it to the client + await standardFile.openRead().pipe(httpRequest.response); + } else { // File not found + if (HttpServerRequestHandler.shouldBeVerbose) ServerLogger.error('_HttpServerRequestHandler::_serveStandardFile(String, HttpRequest) - File not found at path: ($pathToFile)'); + + this._callListenerForErrorDocument(HttpStatus.NOT_FOUND, httpRequest); } - } catch (err) { + } catch(err, stackTrace) { ServerLogger.error(err); + ServerLogger.error(stackTrace); } finally { httpRequest.response.close(); } } + + /** + * Add a new content type to the server that didn't come prepackaged with the server. + */ + static void addContentType(final String fileExtension, final ContentType contentType) { + HttpServerRequestHandler._fileExtensions[fileExtension] = contentType; + } } +class RequestPath { + static final FunctionStore _functionStore = new FunctionStore(); + static final Map _possibleUrlDataFormats = {}; + UrlPath urlData; + + RequestPath(final UrlPath this.urlData); + + Stream get onRequest { + RequestPath._possibleUrlDataFormats[urlData.path] = urlData.id; + + return RequestPath._functionStore[urlData.id]; + } +} + +/** + * Replace String variable in an AngularJS style of {{...}} and using a Map to + * determine the values to replace with. By default, it will switch all variables + * without a conversion value to an empty String value (e.g. ""), or in Layman's + * terms, nothing. + * + * // Returns with the variables replaced: + * // --> "My name is Bobert Robertson." + * applyVarModifiers('My name is {{firstName}} {{lastName}}.', { + * "firstName": "Bobert", + * "lastName": "Robertson" + * }); + */ String applyVarModifiers(String fileContents, final Map varModifiers, {final bool clearUnusedVars: true}) { varModifiers.forEach((final String key, final dynamic value) { fileContents = fileContents.replaceAll('{{$key}}', '$value'); @@ -451,50 +631,88 @@ String applyVarModifiers(String fileContents, final Map varModi return fileContents; } -/// Get the ContentType back based on the type of file path; -/// e.g. hello_world.html -> ContentType("text", "html") +/** + * Get the ContentType back based on the type of the file path. + * + * // --> ContentType("application", "dart"); + * final ContentType contentType = + * getContentTypeForFilepathExtension('/dart/modules/unittest.dart'); + */ ContentType getContentTypeForFilepathExtension(final String filePath) { final String extension = new RegExp(r'\.\S+$').firstMatch(filePath).group(0); - if (_HttpServerRequestHandler._fileExtensions.containsKey(extension)) { - final List _fileExtensionData = _HttpServerRequestHandler._fileExtensions[extension]; - - return new ContentType(_fileExtensionData[0], _fileExtensionData[1]); + if (HttpServerRequestHandler._fileExtensions.containsKey(extension)) { + return HttpServerRequestHandler._fileExtensions[extension]; } return null; } class _VirtualDirectoryFileData { - final String directoryPath; - final String virtualFilePathWithPrefix; - final String virtualFilePath; + final String containerDirectoryPath; // e.g. "/Users/Test/home/server_project/web" + final String filePathFromContainerDirectory; // e.g. "dart/index_page/main.dart" + String _slashSafeFilePathFromContainerDirectoryForHttpRequests; // e.g. the [filePathFromContainerDirectory] with '\' converted to '/' for Url path matching (Windows quirk) + final String _optPrefix; // Optional prefix before the file path in the public Url path + + _VirtualDirectoryFileData(final String this.containerDirectoryPath, final String this.filePathFromContainerDirectory, [final String this._optPrefix = '']) { + if (path.separator == '\\' && this.filePathFromContainerDirectory.startsWith(path.separator)) { + this._slashSafeFilePathFromContainerDirectoryForHttpRequests = this.filePathFromContainerDirectory.replaceAll(path.separator, '/'); + } else { + this._slashSafeFilePathFromContainerDirectoryForHttpRequests = this.filePathFromContainerDirectory; + } + } - _VirtualDirectoryFileData(final String this.directoryPath, final String this.virtualFilePathWithPrefix, final String this.virtualFilePath); + String get absoluteFileSystemPath { + return this.containerDirectoryPath + this.filePathFromContainerDirectory; + } + + String get httpRequestPath { + if (this._optPrefix != null && + this._optPrefix.isNotEmpty) + { + // The this.filePathFromContainerDirectory has a leading "/", add another one if there is an optional prefix + return "/${this._optPrefix}${this._slashSafeFilePathFromContainerDirectoryForHttpRequests}"; + } + + return this._slashSafeFilePathFromContainerDirectoryForHttpRequests; + } } /** - * Factory for creating UrlData holder with a dynamically generated ID. + * Factory for creating UrlData holder with a dynamically generated reference ID. + * + * This is most often used for telling the server what the navigation Url will + * be for a method to register at. */ -class UrlData { - static int _pageIndex = 0; +class UrlPath { + static int _pageCounterIndex = 0; final int id; final String path; - factory UrlData(final String url) { - return new UrlData._internal(UrlData._pageIndex++, url); + factory UrlPath(final String urlPath) { + return new UrlPath._internal(UrlPath._pageCounterIndex++, urlPath); } - const UrlData._internal(final int this.id, final String this.path); + const UrlPath._internal(final int this.id, final String this.path); } -class AuthCheckResults { +class _AuthCheckResults { final bool didPass; final String username; - const AuthCheckResults(final bool this.didPass, [final String this.username = null]); + const _AuthCheckResults(final bool this.didPass, [final String this.username = null]); } +/** + * A user:password base64 encoded auth data. + * + * The username parameter is solely for an alias to the specific + * [AuthUserData]. It does not need to be the same as the [encodedAuth] + * parameter's username, but most often will be. + * + * The [encodedAuth] parameter will be the "user:password" String after having + * been base64 encoded. These will be used for checking credentials on the server. + */ class AuthUserData { final String username; final String encodedAuth; @@ -505,11 +723,11 @@ class AuthUserData { /** * Path data for storing with the required auth data. */ -class PathDataWithAuth { +class _PathDataWithAuth { final String urlPath; final List _authUsersList; - PathDataWithAuth(final String this.urlPath, final List authUsersList) : this._authUsersList = authUsersList; + _PathDataWithAuth(final String this.urlPath, final List authUsersList) : this._authUsersList = authUsersList; bool doCredentialsMatch(final String encodedAuth) { for (AuthUserData authUserData in this._authUsersList) { diff --git a/lib/src/web_server/web_socket_request_payload.dart b/lib/src/web_server/web_socket_request_payload.dart index 248f5f5..08423cd 100644 --- a/lib/src/web_server/web_socket_request_payload.dart +++ b/lib/src/web_server/web_socket_request_payload.dart @@ -1,4 +1,4 @@ -part of WebServer.webServer; +part of WebServer; class WebSocketRequestPayload { final int cmd; diff --git a/lib/src/web_server/web_socket_server_request_handler.dart b/lib/src/web_server/web_socket_server_request_handler.dart index b6c542f..a0443d9 100644 --- a/lib/src/web_server/web_socket_server_request_handler.dart +++ b/lib/src/web_server/web_socket_server_request_handler.dart @@ -1,4 +1,4 @@ -part of WebServer.webServer; +part of WebServer; typedef String FunctionBinaryParam(Uint32List encodeMessage, HttpRequest httpRequest, WebSocket ws); @@ -36,7 +36,7 @@ class _WebSocketServerRequestHandler { this._onOpenStreamController.add(new WebSocketConnectionData(httpRequest, webSocket)); webSocket.map((final dynamic message) { - if (message.runtimeType != String) { + if ((message is String) == false) { return JSON.decode(this.customDecodeMessage(message, httpRequest, webSocket)); } diff --git a/lib/src/web_socket_connection_manager/web_socket_object_store.dart b/lib/src/web_socket_connection_manager/web_socket_object_store.dart index 5741087..9d606d9 100644 --- a/lib/src/web_socket_connection_manager/web_socket_object_store.dart +++ b/lib/src/web_socket_connection_manager/web_socket_object_store.dart @@ -1,4 +1,4 @@ -part of WebServer.webSocketConnectionManager; +part of WebSocketConnectionManager; class WebSocketObjectStore { final Map _mainObjectStore = {}; diff --git a/lib/src/web_socket_connection_manager/ws_connection.dart b/lib/src/web_socket_connection_manager/ws_connection.dart index 75f1d0c..6911eb6 100644 --- a/lib/src/web_socket_connection_manager/ws_connection.dart +++ b/lib/src/web_socket_connection_manager/ws_connection.dart @@ -1,4 +1,4 @@ -part of WebServer.webSocketConnectionManager; +part of WebSocketConnectionManager; class WebSocketConnection { final WebSocket webSocket; diff --git a/lib/web_server.dart b/lib/web_server.dart index 847181f..eb38295 100644 --- a/lib/web_server.dart +++ b/lib/web_server.dart @@ -1,9 +1,14 @@ -library WebServer.webServer; +/** + * A powerful WebServer package to make getting reliable, strong servers + * running quickly with many features. + */ +library WebServer; import "dart:io"; import "dart:async"; -import "dart:convert" show JSON; +import "dart:convert" show JSON, UTF8, LineSplitter; import "dart:typed_data"; +import "package:cache/cache.dart"; import "package:event_listener/event_listener.dart"; import "package:path/path.dart" as path; import "package:server_logger/server_logger.dart" as ServerLogger; @@ -13,13 +18,16 @@ part "src/web_server/http_server_request_handler.dart"; part "src/web_server/web_socket_request_payload.dart"; part "src/web_server/web_socket_server_request_handler.dart"; +/** + * The base class for all of the WebServer functionality. + */ class WebServer { final InternetAddress address; final int port; final bool hasHttpServer; final bool hasWebSocketServer; final bool isSecure; - _HttpServerRequestHandler httpServerHandler; + HttpServerRequestHandler httpServerHandler; _WebSocketServerRequestHandler webSocketServerHandler; final List allowedMethods; final Duration responseDeadline; @@ -28,47 +36,59 @@ class WebServer { final bool this.hasHttpServer: false, final bool this.hasWebSocketServer: false, final bool enableCompression: true, - final bool this.isSecure: false, - final String certificateName, - final List this.allowedMethods: const ['GET', 'POST'], - final Duration this.responseDeadline: const Duration(seconds: 20) - }) { + final List this.allowedMethods, + final Duration this.responseDeadline: const Duration(seconds: 30) + }) : this.isSecure = false { if (this.hasHttpServer == false && this.hasWebSocketServer == false) { return; } if (this.hasHttpServer) { - this.httpServerHandler = new _HttpServerRequestHandler(); + this.httpServerHandler = new HttpServerRequestHandler(); } if (this.hasWebSocketServer) { this.webSocketServerHandler = new _WebSocketServerRequestHandler(); } - if (this.isSecure) { - throw "Secure server binding is not supported at this time."; + HttpServer.bind(address, port).then((final HttpServer httpServer) { + httpServer.autoCompress = enableCompression; // Enable GZIP? - SecureSocket.initialize(useBuiltinRoots: true); + httpServer.listen(this._onRequest); + }); + } - HttpServer.bindSecure(address, port, certificateName: certificateName).then((final HttpServer httpServer) { - httpServer.autoCompress = enableCompression; // Enable GZIP + WebServer.secure(final InternetAddress this.address, final int this.port, final SecurityContext securityContext, { + final bool this.hasHttpServer: false, + final bool this.hasWebSocketServer: false, + final bool enableCompression: true, + final List this.allowedMethods, + final Duration this.responseDeadline: const Duration(seconds: 30) + }) : this.isSecure = true { + if (this.hasHttpServer == false && this.hasWebSocketServer == false) { + return; + } - httpServer.listen(this._onRequest); - }); - } else { - HttpServer.bind(address, port).then((final HttpServer httpServer) { - httpServer.autoCompress = enableCompression; // Enable GZIP + if (this.hasHttpServer) { + this.httpServerHandler = new HttpServerRequestHandler(); + } - httpServer.listen(this._onRequest); - }); + if (this.hasWebSocketServer) { + this.webSocketServerHandler = new _WebSocketServerRequestHandler(); } + + HttpServer.bindSecure(address, port, securityContext).then((final HttpServer httpServer) { + httpServer.autoCompress = enableCompression; // Enable GZIP? + + httpServer.listen(this._onRequest); + }); } void _onRequest(final HttpRequest httpRequest) { if (httpRequest.method == null || httpRequest.method.isEmpty || httpRequest.method.length > 16 || - this.allowedMethods.contains(httpRequest.method) == false) + (this.allowedMethods != null && this.allowedMethods.contains(httpRequest.method) == false)) { httpRequest.response ..statusCode = HttpStatus.FORBIDDEN diff --git a/lib/web_socket_connection_manager.dart b/lib/web_socket_connection_manager.dart index f4ff166..9f23247 100644 --- a/lib/web_socket_connection_manager.dart +++ b/lib/web_socket_connection_manager.dart @@ -1,4 +1,8 @@ -library WebServer.webSocketConnectionManager; +/** + * An additional library for managing WebSocket connections that works + * very well with the WebServer library. + */ +library WebSocketConnectionManager; import "dart:io"; import "dart:convert" show JSON; diff --git a/pubspec.yaml b/pubspec.yaml index 60f5a7a..2ce8e74 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,15 +1,20 @@ name: web_server -version: 1.0.9 +version: 2.0.0+3 author: Brandon White description: An efficient server library for quickly creating a WebServer and handling HTTP requests, WebSocket connections, and API requests. -homepage: https://google.com/+BrandonWhite +homepage: https://github.com/bwhite000/web-server environment: sdk: '>=1.9.0 <2.0.0' +documentation: http://jennex.it/github/bwhite000/web-server/docs/ dependencies: + cache: + git: https://github.com/bwhite000/cache.git event_listener: git: https://github.com/bwhite000/event-listener.git version: '>=0.0.0 <0.1.0' path: '>=1.0.0 <2.0.0' server_logger: git: https://github.com/bwhite000/server-logger.git - version: '>=0.1.0 <0.2.0' + version: '>=0.1.0 <2.0.0' +executables: + web_server: \ No newline at end of file diff --git a/readme.md b/readme.md index 11c7d71..424ea22 100644 --- a/readme.md +++ b/readme.md @@ -2,24 +2,232 @@ WebServer ========= An efficient server library for quickly creating a WebServer and handling HTTP requests, WebSocket -connections, and API requests in the Dart language. +connections, and routing API requests using the Dart language. Includes extra nice features, such as setting a parameter to require Basic Authentication for a Url, -with all of the difficult auth checking and responding taken care of by the server. +with all of the difficult auth checking and responding taken care of by the server, plus much more! -Example -------- +#### Who is using this package? -Please check out the ["example/"](example/) folder in this package for better details. +__[SocialFlare](https://www.socialflare.us/)__ +* Used for preprocessing and serving webpages and resources, serving API responses, and multiple other features. +* __Major backer:__ Guarantees long-term support for this project's concept. Thank you, SocialFlare! + +__[Ebates, Inc.](http://www.ebates.com/)__ +* For a handful of internal tools for organizing data and serving stat pages. +* Used to serve a realtime Purchases Stat information webpage to merchant representatives at the __Ebates MAX Conference__ + built around this package. + +Use Example (for no coding needed) +---------------------------------- + +You can use this WebServer to serve files without even having to code a single line. Using Dart's +`pub global activate` feature, you can add the WebServer package as an executable to call from the +command line in any directory and serve files from it. + +~~~bash +# Activate the WebServer globally. +pub global activate web_server +~~~ + +~~~bash +# Navigate to the Directory that you want to serve files from. +cd /path/to/directory + +# Activate the WebServer on that Directory. Defaults to port 8080. +# May require 'sudo' on Mac/Linux systems to bind to port 80. +# For: http://127.0.0.1:9090/path/to/file +web_server --port=9090 +~~~ + +For details on all of the possible arguments and uses: +~~~bash +# Use the 'help' argument +web_server --help +~~~ + +__Please don't forget to run the__ `pub global activate web_server` __command every once and a while__ +to get the latest version of the WebServer package; Pub/Dart does not automatically update the package to +avoid the risk of breaking changes. + +Feel free to view the [CHANGELOG](CHANGELOG.md) before updating for documentation about whenever there is +a __breaking change__. Skim quickly by looking for the bold text "__Breaking Change:__" before a +"Tools > web_server" category change. It is safe to assume there will NOT be a breaking change unless the +version number increases by 2.x; the 2.0+x format changes, for example, are non-breaking when the number +after "+" is the only difference. + +Features & Use Example (for coders) +----------------------------------- + +Please check out the ["example/"](example/) folder in this package for full details. + +### For preprocessing HTML like PHP + +Use Angular-like variables, which will be converted using a helper method from this package (see Dart +code below). + +__web/index.html__ +~~~html + +

Welcome, {{username}}!

+ +

The date today is: {{todayDate}}.

+ +~~~ + +Then, process variables like PHP on the Dart server side: + +__server.dart__ ~~~dart -// Initialize the WebServer -final WebServer localWebServer = new WebServer(InternetAddress.LOOPBACK_IP_V4, 8080, - hasHttpServer: true, hasWebSocketServer: true); +import "dart:io"; +import "package:html/parser.dart" as domParser; // https://pub.dartlang.org/packages/html +import "package:html/dom.dart" as dom; +import "package:web_server/web_server.dart" as webServer; + +void main() { + // Initialize the WebServer + final webServer.WebServer localWebServer = new webServer.WebServer(InternetAddress.ANY_IP_V4, 8080, + hasHttpServer: true); + + localWebServer.httpServerHandler + .forRequestPath(new webServer.UrlPath('/index.html')).onRequested.listen((final HttpRequest httpRequest) async { + String indexFileContents = await new File('path/to/index.html').readAsString(); -// Attach HttpServer pages and event handlers -localWebServer.httpServerHandler - // Automatically recursively parse and serve all items in this - // directory matching the accepted file types. - .serveVirtualDirectory('web', const ['html', 'css', 'dart', 'js']); -~~~ \ No newline at end of file + // Apply the Dart variables to the HTML file's variables like + // AngularJS/AngularDart + indexFileContents = webServer.applyVarModifiers(indexFileContents, { + "username": "mrDude", + "todayDate": '${new DateTime.now()}' + }); + + // ===== AND/OR ===== + // Interact with the HTML like client side Dart. + final dom.Document document = domParser.parse(indexFileContents); + + // The HTML library has its own Element Objects; separate from the 'dart:html' ones. + final dom.Element pElm = document.querySelector('p'); + + pElm.remove(); // Remove the

Element from the document's DOM. + + // Add data to and close out the Http request's response. + httpRequest.response + ..headers.contentType = new ContentType('text', 'html', charset: 'utf-8') + + ..write(indexFileContents) + // OR + ..write(document.outerHtml) + + ..close(); + }); +} +~~~ + +### For Hosting APIs + +Filter every request starting with a certain Url pattern into a request handler. + +~~~dart +import "dart:io"; +import "package:web_server/web_server.dart" as webServer; + +void main() { + // Initialize the WebServer + final webServer.WebServer localWebServer = new webServer.WebServer(InternetAddress.ANY_IP_V4, 8080, + hasHttpServer: true); + + localWebServer.httpServerHandler + // NOTE: ApiHandler would be a Class or namespace created by you in your code, for example. + ..handleRequestsStartingWith(new webServer.UrlPath('/api/categories')).listen(ApiHandler.forCategories) + ..handleRequestsStartingWith(new webServer.UrlPath('/api/products')).listen(ApiHandler.forProducts) + ..handleRequestsStartingWith(new webServer.UrlPath('/api/users')).listen((final HttpRequest httpRequest) { + // Create the Object for the response + final webServer.ApiResponse apiResponse = new webServer.ApiResponse() + ..addData("username", "mrDude") // Add data + ..addData("email", "radical_surfer@example.com") + ..addData("userId", 1425302); + + // Send the data back through to the request + httpRequest.response + // Set to "application/json; charset=utf-8" + ..headers.contentType = ContentType.JSON + + // Stringify the JSON output, then send to client + ..write(apiResponse.toJsonEncoded()) + + ..close(); + }); +} +~~~ + +### Static File Directory/Basic WebServer + +~~~dart +import "dart:io"; +import "dart:async"; +import "package:web_server/web_server.dart" as webServer; + +Future main() async { + // Initialize the WebServer + final webServer.WebServer localWebServer = new webServer.WebServer(InternetAddress.ANY_IP_V4, 8080, + hasHttpServer: true); + + // Attach HttpServer pages and event handlers + await localWebServer.httpServerHandler + // Automatically recursively parse and serve all items in this + // directory matching the accepted file types (optional parameter). + .serveStaticVirtualDirectory('web', + supportedFileExtensions: const ['html', 'css', 'dart', 'js'], // Optional restriction + shouldPreCache: true); +} +~~~ + +### WebSocket Server + +Let the WebServer automatically handle upgrading and connecting to WebSockets from the client +side. The WebServer will forward data related to important events and automatically call your +event listeners if you send data through a WebSocket from the client with the "cmd" parameter +in the payload's Map Object. + +~~~dart +import "dart:io"; +import "package:web_server/web_server.dart" as webServer; + +void main() { + // Initialize the WebServer with the hasWebSocketServer parameter + final webServer.WebServer localWebServer = new webServer.WebServer(InternetAddress.ANY_IP_V4, 8080, + hasHttpServer: true, hasWebSocketServer: true); + + // HTTP Server handlers code here... + + // Attach WebSocket command listeners and base events + localWebServer.webSocketServerHandler + // For automatically routing handling of data sent through a WebSocket with this pattern of "cmd": + // {"cmd": 0, "data": { "pokemonCount": 151 }} + ..on[0].listen((final webServer.WebSocketRequestPayload requestPayload) { /*...*/ }) + ..onConnectionOpen.listen((final webServer.WebSocketConnectionData connectionData) { /*...*/ }) + ..onConnectionError.listen((final WebSocket webSocket) { /*...*/ }) + ..onConnectionClose.listen((final WebSocket webSocket) { /*...*/ }); +} +~~~ + +### Add a custom ContentType + +Allows the server to automatically pick up on this file extension as the supplied ContentType parameter +when it is handling serving files. + +~~~dart +HttpServerRequestHandler.addContentType('.html', new ContentType('text', 'html', charset: 'utf-8')); +~~~ + +Features and bugs +----------------- + +Please file feature requests and bugs using the GitHub issue tracker for this repository. + +Using this package? Let me know! +-------------------------------- + +I am excited to see if other developers are able to make something neat with this package. +If you have a project using it, please send me a quick email at the email address listed on +[my GitHub's main page](https://github.com/bwhite000). Thanks a bunch! From 25fdefc3d959b5fb31dce2345e4719b0eb4dde84 Mon Sep 17 00:00:00 2001 From: tthompson Date: Sat, 12 Mar 2016 19:42:50 -0600 Subject: [PATCH 3/3] Moved var description because it looked like it was describing the function --- lib/src/web_server/http_server_request_handler.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/src/web_server/http_server_request_handler.dart b/lib/src/web_server/http_server_request_handler.dart index 310ee48..9b67c57 100644 --- a/lib/src/web_server/http_server_request_handler.dart +++ b/lib/src/web_server/http_server_request_handler.dart @@ -318,9 +318,8 @@ class HttpServerRequestHandler { if (await file.exists()) { // The file exists, lets configure the http request and serve it - String _fileContents; + String _fileContents; // The contents of the file, if caching is enabled - /// The contents of the file, if caching is enabled final ContentType _contentType = getContentTypeForFilepathExtension(pathToFile); this._possibleFiles[urlData.path] = urlData.id;