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 f5e47d0..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, 8080, - 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 1bbe7d5..9b67c57 100644 --- a/lib/src/web_server/http_server_request_handler.dart +++ b/lib/src/web_server/http_server_request_handler.dart @@ -1,77 +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"], - ".gif": const ["image", "gif"], - ".webp": const ["image", "webp"], - ".svg": const ["image", "svg+xml"], - ".otf": const ["font", "otf"], - ".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 + // 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])) + // 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])) { - ServerLogger.log('Url has matched to a file. Routing to it...'); + 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; @@ -92,18 +165,15 @@ class _HttpServerRequestHandler { if (this._possibleDirectories.containsKey(possibleDirectoryPath) && this._functionStore.fnStore.containsKey(this._possibleDirectories[possibleDirectoryPath])) { - ServerLogger.log('Url has matched to a directory. Routing to it...'); + 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.'); + if (HttpServerRequestHandler.shouldBeVerbose) ServerLogger.log('No registered url match found.'); - httpRequest.response - ..statusCode = HttpStatus.NOT_FOUND - ..headers.contentType = new ContentType("text", "plain", charset: "utf-8") - ..close(); + this._callListenerForErrorDocument(HttpStatus.NOT_FOUND, httpRequest); } } } @@ -112,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; @@ -142,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; @@ -152,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; @@ -163,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; @@ -175,13 +252,13 @@ 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 @@ -190,21 +267,25 @@ class _HttpServerRequestHandler { ..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') + ..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') + ..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/".'; } @@ -220,34 +301,49 @@ 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 - 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 + + _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; - } 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,11 +352,11 @@ 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.'); + if (HttpServerRequestHandler.shouldBeVerbose) ServerLogger.error('The file at path ($pathToFile) was not found in the filesystem; unable to serve it.'); } } @@ -271,6 +367,7 @@ 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 @@ -283,151 +380,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, + Future serveStaticVirtualDirectory(String pathToDirectory, { + final List supportedFileExtensions: null, + final bool shouldPreCache: false, + final bool includeContainerDirNameInPath: false, final bool shouldFollowLinks: false, - final String prefixWithDirName: '', - final bool isRelativeDirPath: true, + 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'); - // 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.'; + final Completer completer = new Completer(); + + // 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', ' '); + // 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; + } + + 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); } - static void serveVirtualDirectoryWithAuth() {} + // 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]; } + + // Read the file, and send it to the client + await standardFile.openRead().pipe(httpRequest.response); } else { // File not found - ServerLogger.error('File not found at path: ($pathToFile)'); + if (HttpServerRequestHandler.shouldBeVerbose) ServerLogger.error('_HttpServerRequestHandler::_serveStandardFile(String, HttpRequest) - 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(); + 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'); @@ -441,50 +630,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; @@ -495,11 +722,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!