LCOV - code coverage report
Current view: top level - lib - webserver.dart (source / functions) Hit Total Coverage
Test: coverage.lcov Lines: 127 127 100.0 %
Date: 2021-03-29 22:32:58 Functions: 0 0 -

          Line data    Source code
       1             : import 'dart:async';
       2             : import 'dart:convert';
       3             : import 'dart:io';
       4             : import 'dart:typed_data';
       5             : 
       6             : import 'package:enum_to_string/enum_to_string.dart';
       7             : import 'package:http_server/http_server.dart';
       8             : import 'package:mime_type/mime_type.dart';
       9             : import 'package:webserver/src/route_matcher.dart';
      10             : 
      11           3 : enum RouteMethod { get, post, put, delete, all }
      12           2 : enum RequestMethod { get, post, put, delete }
      13             : 
      14             : /// Server application class
      15             : ///
      16             : /// This is the core of the server application. Generally you would create one
      17             : /// for each app.
      18             : class Webserver {
      19             :   /// List of routes
      20             :   ///
      21             :   /// Generally you don't want to manipulate this array directly, instead add
      22             :   /// routes by calling the [get,post,put,delete] methods.
      23             :   final routes = <HttpRoute>[];
      24             : 
      25             :   final staticFiles = <String, HttpRoute>{};
      26             : 
      27             :   /// HttpServer instance from the dart:io library
      28             :   ///
      29             :   /// If there is anything the app can't do, you can do it through here.
      30             :   HttpServer? server;
      31             : 
      32             :   /// Log requests immediately as they come in
      33             :   ///
      34             :   bool logRequests;
      35             : 
      36             :   /// Optional handler for when a route is not found
      37             :   ///
      38             :   FutureOr Function(HttpRequest req, HttpResponse res)? onNotFound;
      39             : 
      40             :   /// Optional handler for when the server throws an unhandled error
      41             :   ///
      42             :   FutureOr Function(HttpRequest req, HttpResponse res)? onInternalError;
      43             : 
      44           1 :   Webserver({this.onNotFound, this.onInternalError, this.logRequests = true});
      45             : 
      46             :   /// Create a get route
      47             :   ///
      48           1 :   HttpRoute get(String path,
      49             :       FutureOr Function(HttpRequest req, HttpResponse res) callback,
      50             :       {List<FutureOr Function(HttpRequest req, HttpResponse res)> middleware =
      51             :           const []}) {
      52             :     final route =
      53           1 :         HttpRoute(path, callback, RouteMethod.get, middleware: middleware);
      54           2 :     routes.add(route);
      55             :     return route;
      56             :   }
      57             : 
      58             :   /// Create a post route
      59             :   ///
      60           1 :   HttpRoute post(String path,
      61             :       FutureOr Function(HttpRequest req, HttpResponse res) callback,
      62             :       {List<FutureOr Function(HttpRequest req, HttpResponse res)> middleware =
      63             :           const []}) {
      64           1 :     final route = HttpRoute(path, callback, RouteMethod.post);
      65           2 :     routes.add(route);
      66             :     return route;
      67             :   }
      68             : 
      69             :   /// Create a put route
      70           1 :   HttpRoute put(String path,
      71             :       FutureOr Function(HttpRequest req, HttpResponse res) callback,
      72             :       {List<FutureOr Function(HttpRequest req, HttpResponse res)> middleware =
      73             :           const []}) {
      74           1 :     final route = HttpRoute(path, callback, RouteMethod.put);
      75           2 :     routes.add(route);
      76             :     return route;
      77             :   }
      78             : 
      79             :   /// Create a delete route
      80             :   ///
      81           1 :   HttpRoute delete(String path,
      82             :       FutureOr Function(HttpRequest req, HttpResponse res) callback,
      83             :       {List<FutureOr Function(HttpRequest req, HttpResponse res)> middleware =
      84             :           const []}) {
      85           1 :     final route = HttpRoute(path, callback, RouteMethod.delete);
      86           2 :     routes.add(route);
      87             :     return route;
      88             :   }
      89             : 
      90             :   /// Create a route that listens on all methods
      91             :   ///
      92           1 :   HttpRoute all(String path,
      93             :       FutureOr Function(HttpRequest req, HttpResponse res) callback,
      94             :       {List<FutureOr Function(HttpRequest req, HttpResponse res)> middleware =
      95             :           const []}) {
      96           1 :     final route = HttpRoute(path, callback, RouteMethod.all);
      97           2 :     routes.add(route);
      98             :     return route;
      99             :   }
     100             : 
     101             :   /// Serve some static files on a route
     102             :   ///
     103           1 :   void serveStatic(String path, Directory directory) {
     104           4 :     staticFiles[path] = HttpRoute(path, (req, res) async {
     105           5 :       final filePath = directory.path + req.uri.path.replaceFirst(path, "");
     106           1 :       final file = File(filePath);
     107           2 :       final exists = await file.exists();
     108             :       if (!exists) {
     109           2 :         throw WebserverException(404, {"message": "file not found"});
     110             :       }
     111           1 :       res.setContentTypeFromFile(file);
     112           3 :       await res.addStream(file.openRead());
     113           2 :       await res.close();
     114             :     }, RouteMethod.get);
     115             :   }
     116             : 
     117             :   /// Call this function to fire off the server
     118             :   ///
     119           1 :   Future<HttpServer> listen(
     120             :       [int port = 3000, dynamic bindIp = "0.0.0.0"]) async {
     121           2 :     final _server = await HttpServer.bind(bindIp, port);
     122             : 
     123           2 :     _server.listen((HttpRequest request) {
     124           2 :       unawaited(_incomingRequest(request));
     125             :     });
     126             : 
     127           1 :     return server = _server;
     128             :   }
     129             : 
     130             :   /// Handles and routes an incoming request
     131             :   ///
     132           1 :   Future _incomingRequest(HttpRequest request) async {
     133             :     bool isDone = false;
     134           1 :     if (logRequests) {
     135           5 :       print("${request.method} - ${request.uri.toString()}");
     136             :     }
     137             : 
     138           5 :     unawaited(request.response.done.then((value) {
     139             :       isDone = true;
     140             :     }));
     141             : 
     142           1 :     final effectiveRoutes = RouteMatcher.match(
     143           2 :         request.uri.toString(),
     144           1 :         routes,
     145           1 :         EnumToString.fromString<RouteMethod>(
     146           1 :                 RouteMethod.values, request.method) ??
     147             :             RouteMethod.get);
     148             : 
     149           2 :     final staticRoutes = staticFiles.values
     150           6 :         .where((element) => request.uri.path.startsWith(element.route))
     151           1 :         .toList();
     152             : 
     153             :     try {
     154           1 :       if (effectiveRoutes.isEmpty) {
     155           1 :         if (staticRoutes.isNotEmpty) {
     156           4 :           await staticRoutes.first.callback(request, request.response);
     157           1 :         } else if (onNotFound != null) {
     158           3 :           final result = await onNotFound!(request, request.response);
     159             :           if (result != null && !isDone) {
     160           2 :             await _handleResponse(result, request);
     161             :           }
     162           3 :           await request.response.close();
     163             :         } else {
     164           2 :           request.response.statusCode = 404;
     165           2 :           request.response.write("404 not found");
     166           3 :           await request.response.close();
     167             :         }
     168             :       } else {
     169           2 :         for (var route in effectiveRoutes) {
     170             :           /// Loop through any middleware
     171           2 :           for (var middleware in route.middleware) {
     172             :             if (isDone) {
     173             :               break;
     174             :             }
     175           2 :             await _handleResponse(
     176           2 :                 await middleware(request, request.response), request);
     177             :           }
     178             :           if (isDone) {
     179             :             break;
     180             :           }
     181           2 :           await _handleResponse(
     182           3 :               await route.callback(request, request.response), request);
     183             :         }
     184             :         if (!isDone) {
     185           4 :           if (request.response.contentLength == -1) {
     186           1 :             print(
     187           5 :                 "Warning: Returning a response with no content. ${effectiveRoutes.map((e) => e.route).join(", ")}");
     188             :           }
     189           3 :           await request.response.close();
     190             :         }
     191             :       }
     192           1 :     } on WebserverException catch (e) {
     193           3 :       request.response.statusCode = e.statusCode;
     194           3 :       await _handleResponse(e.response, request);
     195             :     } catch (e, s) {
     196           1 :       print(e);
     197           1 :       print(s);
     198           1 :       if (onInternalError != null) {
     199           3 :         final result = await onInternalError!(request, request.response);
     200             :         if (result != null && !isDone) {
     201           2 :           await _handleResponse(result, request);
     202             :         }
     203           3 :         await request.response.close();
     204             :       } else {
     205           2 :         request.response.statusCode = 500;
     206           2 :         request.response.write(e);
     207           3 :         await request.response.close();
     208             :       }
     209             :     }
     210             :   }
     211             : 
     212             :   /// Handle an automated response
     213             :   ///
     214           1 :   Future<void> _handleResponse(dynamic result, HttpRequest request) async {
     215             :     if (result != null) {
     216           2 :       if (result is Uint8List || result is List<int>) {
     217           3 :         if (request.response.headers.contentType == null ||
     218           5 :             request.response.headers.contentType!.value == "text/plain") {
     219           4 :           request.response.headers.contentType = ContentType.binary;
     220             :         }
     221           2 :         request.response.add(result);
     222           2 :       } else if (result is Map<String, dynamic> || result is List<dynamic>) {
     223           4 :         request.response.headers.contentType = ContentType.json;
     224           3 :         request.response.write(jsonEncode(result));
     225           1 :       } else if (result is String) {
     226             :         //Default content type is text, no need to set it
     227           2 :         request.response.write(result);
     228           1 :       } else if (result is File) {
     229           2 :         request.response.setContentTypeFromFile(result);
     230           4 :         await request.response.addStream(result.openRead());
     231           1 :       } else if (result is Stream<List<int>>) {
     232           3 :         if (request.response.headers.contentType == null ||
     233           5 :             request.response.headers.contentType!.value == "text/plain") {
     234           4 :           request.response.headers.contentType = ContentType.binary;
     235             :         }
     236           3 :         await request.response.addStream(result);
     237             :       }
     238           3 :       await request.response.close();
     239             :     }
     240             :   }
     241             : 
     242             :   /// Close the server
     243             :   ///
     244           1 :   Future close({bool force = true}) async {
     245           1 :     if (server != null) {
     246           3 :       await server!.close(force: force);
     247             :     }
     248             :   }
     249             : }
     250             : 
     251             : extension RequestHelpers on HttpRequest {
     252             :   /// Parse the body automatically and return the result
     253             :   ///
     254           1 :   Future<Object?> get body async =>
     255           3 :       (await HttpBodyHandler.processRequest(this)).body;
     256             : 
     257             :   /// Get the content type
     258             :   ///
     259           3 :   ContentType? get contentType => headers.contentType;
     260             : }
     261             : 
     262             : extension ResponseHelpers on HttpResponse {
     263             :   /// Set the appropriate headers to download the file
     264             :   ///
     265           1 :   void setDownload({required String filename}) {
     266           3 :     headers.add("Content-Disposition", "attachment; filename=$filename");
     267             :   }
     268             : 
     269             :   /// Set the content type from the extension ie. 'pdf'
     270             :   ///
     271           1 :   void setContentTypeFromExtension(String extension) {
     272           1 :     final mime = mimeFromExtension(extension);
     273             :     if (mime != null) {
     274           1 :       final split = mime.split("/");
     275           5 :       headers.contentType = ContentType(split[0], split[1]);
     276             :     }
     277             :   }
     278             : 
     279             :   /// Set the content type given a file
     280             :   ///
     281           1 :   void setContentTypeFromFile(File file) {
     282           2 :     if (headers.contentType == null ||
     283           4 :         headers.contentType!.mimeType == "text/plain") {
     284           3 :       headers.contentType = file.contentType;
     285             :     } else {
     286           4 :       headers.contentType == ContentType.binary;
     287             :     }
     288             :   }
     289             : 
     290             :   /// Helper method for those used to res.json()
     291             :   ///
     292           1 :   Future json(Object? json) async {
     293           3 :     headers.contentType = ContentType.json;
     294           2 :     write(jsonEncode(json));
     295           2 :     await close();
     296             :   }
     297             : 
     298             :   /// Helper method to just send data;
     299           1 :   Future send(Object? data) async {
     300           1 :     write(data);
     301           2 :     await close();
     302             :   }
     303             : }
     304             : 
     305             : extension FileHelpers on File {
     306             :   /// Get the mimeType as a string
     307             :   ///
     308           3 :   String? get mimeType => mime(path);
     309             : 
     310             :   /// Get the contentType header from the current
     311             :   ///
     312           1 :   ContentType? get contentType {
     313           1 :     final mimeType = this.mimeType;
     314             :     if (mimeType != null) {
     315           1 :       final split = mimeType.split("/");
     316           3 :       return ContentType(split[0], split[1]);
     317             :     }
     318             :   }
     319             : }
     320             : 
     321             : /// Used to prevent lint warnings about unawaited futures;
     322           1 : void unawaited(Future future) {}
     323             : 
     324             : /// Throw these exceptions to bubble up an error from sub functions and have them
     325             : /// handled automatically for the client
     326             : class WebserverException implements Exception {
     327             :   /// The response to send to the client
     328             :   ///
     329             :   final Object? response;
     330             : 
     331             :   /// The statusCode to send to the client
     332             :   ///
     333             :   final int statusCode;
     334             : 
     335           1 :   WebserverException(this.statusCode, this.response);
     336             : }

Generated by: LCOV version 1.14