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 : }
|