OLD | NEW |
| (Empty) |
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 library pub.barback.server; | |
6 | |
7 import 'dart:async'; | |
8 import 'dart:io'; | |
9 | |
10 import 'package:barback/barback.dart'; | |
11 import 'package:mime/mime.dart'; | |
12 import 'package:path/path.dart' as path; | |
13 import 'package:shelf/shelf.dart' as shelf; | |
14 import 'package:stack_trace/stack_trace.dart'; | |
15 | |
16 import '../barback.dart'; | |
17 import '../io.dart'; | |
18 import '../log.dart' as log; | |
19 import '../utils.dart'; | |
20 import 'base_server.dart'; | |
21 import 'asset_environment.dart'; | |
22 | |
23 /// Callback for determining if an asset with [id] should be served or not. | |
24 typedef bool AllowAsset(AssetId id); | |
25 | |
26 /// A server that serves assets transformed by barback. | |
27 class BarbackServer extends BaseServer<BarbackServerResult> { | |
28 /// The package whose assets are being served. | |
29 final String package; | |
30 | |
31 /// The directory in the root which will serve as the root of this server as | |
32 /// a native platform path. | |
33 /// | |
34 /// This may be `null` in which case no files in the root package can be | |
35 /// served and only assets in "lib" directories are available. | |
36 final String rootDirectory; | |
37 | |
38 /// Optional callback to determine if an asset should be served. | |
39 /// | |
40 /// This can be set to allow outside code to filter out assets. Pub serve | |
41 /// uses this after plug-ins are loaded to avoid serving ".dart" files in | |
42 /// release mode. | |
43 /// | |
44 /// If this is `null`, all assets may be served. | |
45 AllowAsset allowAsset; | |
46 | |
47 /// Creates a new server and binds it to [port] of [host]. | |
48 /// | |
49 /// This server serves assets from [barback], and uses [rootDirectory] | |
50 /// (which is relative to the root directory of [package]) as the root | |
51 /// directory. If [rootDirectory] is omitted, the bound server can only be | |
52 /// used to serve assets from packages' lib directories (i.e. "packages/..." | |
53 /// URLs). If [package] is omitted, it defaults to the entrypoint package. | |
54 static Future<BarbackServer> bind(AssetEnvironment environment, String host, | |
55 int port, {String package, String rootDirectory}) { | |
56 if (package == null) package = environment.rootPackage.name; | |
57 return bindServer(host, port).then((server) { | |
58 if (rootDirectory == null) { | |
59 log.fine('Serving packages on $host:$port.'); | |
60 } else { | |
61 log.fine('Bound "$rootDirectory" to $host:$port.'); | |
62 } | |
63 return new BarbackServer._(environment, server, package, rootDirectory); | |
64 }); | |
65 } | |
66 | |
67 BarbackServer._(AssetEnvironment environment, HttpServer server, this.package, | |
68 this.rootDirectory) | |
69 : super(environment, server); | |
70 | |
71 /// Converts a [url] served by this server into an [AssetId] that can be | |
72 /// requested from barback. | |
73 AssetId urlToId(Uri url) { | |
74 // See if it's a URL to a public directory in a dependency. | |
75 var id = packagesUrlToId(url); | |
76 if (id != null) return id; | |
77 | |
78 if (rootDirectory == null) { | |
79 throw new FormatException( | |
80 "This server cannot serve out of the root directory. Got $url."); | |
81 } | |
82 | |
83 // Otherwise, it's a path in current package's [rootDirectory]. | |
84 var parts = path.url.split(url.path); | |
85 | |
86 // Strip the leading "/" from the URL. | |
87 if (parts.isNotEmpty && parts.first == "/") parts = parts.skip(1); | |
88 | |
89 var relativePath = path.url.join(rootDirectory, path.url.joinAll(parts)); | |
90 return new AssetId(package, relativePath); | |
91 } | |
92 | |
93 /// Handles an HTTP request. | |
94 handleRequest(shelf.Request request) { | |
95 if (request.method != "GET" && request.method != "HEAD") { | |
96 return methodNotAllowed(request); | |
97 } | |
98 | |
99 var id; | |
100 try { | |
101 id = urlToId(request.url); | |
102 } on FormatException catch (ex) { | |
103 // If we got here, we had a path like "/packages" which is a special | |
104 // directory, but not a valid path since it lacks a following package | |
105 // name. | |
106 return notFound(request, error: ex.message); | |
107 } | |
108 | |
109 // See if the asset should be blocked. | |
110 if (allowAsset != null && !allowAsset(id)) { | |
111 return notFound( | |
112 request, | |
113 error: "Asset $id is not available in this configuration.", | |
114 asset: id); | |
115 } | |
116 | |
117 return environment.barback.getAssetById(id).then((result) { | |
118 return result; | |
119 }).then((asset) => _serveAsset(request, asset)).catchError((error, trace) { | |
120 if (error is! AssetNotFoundException) throw error; | |
121 return environment.barback.getAssetById( | |
122 id.addExtension("/index.html")).then((asset) { | |
123 if (request.url.path.endsWith('/')) return _serveAsset(request, asset); | |
124 | |
125 // We only want to serve index.html if the URL explicitly ends in a | |
126 // slash. For other URLs, we redirect to one with the slash added to | |
127 // implicitly support that too. This follows Apache's behavior. | |
128 logRequest(request, "302 Redirect to ${request.url}/"); | |
129 return new shelf.Response.found('${request.url}/'); | |
130 }).catchError((newError, newTrace) { | |
131 // If we find neither the original file or the index, we should report | |
132 // the error about the original to the user. | |
133 throw newError is AssetNotFoundException ? error : newError; | |
134 }); | |
135 }).catchError((error, trace) { | |
136 if (error is! AssetNotFoundException) { | |
137 trace = new Chain.forTrace(trace); | |
138 logRequest(request, "$error\n$trace"); | |
139 | |
140 addError(error, trace); | |
141 close(); | |
142 return new shelf.Response.internalServerError(); | |
143 } | |
144 | |
145 addResult(new BarbackServerResult._failure(request.url, id, error)); | |
146 return notFound(request, asset: id); | |
147 }).then((response) { | |
148 // Allow requests of any origin to access "pub serve". This is useful for | |
149 // running "pub serve" in parallel with another development server. Since | |
150 // "pub serve" is only used as a development server and doesn't require | |
151 // any sort of credentials anyway, this is secure. | |
152 return response.change(headers: const { | |
153 "Access-Control-Allow-Origin": "*" | |
154 }); | |
155 }); | |
156 } | |
157 | |
158 /// Returns the body of [asset] as a response to [request]. | |
159 Future<shelf.Response> _serveAsset(shelf.Request request, Asset asset) { | |
160 return validateStream(asset.read()).then((stream) { | |
161 addResult(new BarbackServerResult._success(request.url, asset.id)); | |
162 var headers = {}; | |
163 var mimeType = lookupMimeType(asset.id.path); | |
164 if (mimeType != null) headers['Content-Type'] = mimeType; | |
165 return new shelf.Response.ok(stream, headers: headers); | |
166 }).catchError((error, trace) { | |
167 addResult(new BarbackServerResult._failure(request.url, asset.id, error)); | |
168 | |
169 // If we couldn't read the asset, handle the error gracefully. | |
170 if (error is FileSystemException) { | |
171 // Assume this means the asset was a file-backed source asset | |
172 // and we couldn't read it, so treat it like a missing asset. | |
173 return notFound(request, error: error.toString(), asset: asset.id); | |
174 } | |
175 | |
176 trace = new Chain.forTrace(trace); | |
177 logRequest(request, "$error\n$trace"); | |
178 | |
179 // Otherwise, it's some internal error. | |
180 return new shelf.Response.internalServerError(body: error.toString()); | |
181 }); | |
182 } | |
183 } | |
184 | |
185 /// The result of the server handling a URL. | |
186 /// | |
187 /// Only requests for which an asset was requested from barback will emit a | |
188 /// result. Malformed requests will be handled internally. | |
189 class BarbackServerResult { | |
190 /// The requested url. | |
191 final Uri url; | |
192 | |
193 /// The id that [url] identifies. | |
194 final AssetId id; | |
195 | |
196 /// The error thrown by barback. | |
197 /// | |
198 /// If the request was served successfully, this will be null. | |
199 final error; | |
200 | |
201 /// Whether the request was served successfully. | |
202 bool get isSuccess => error == null; | |
203 | |
204 /// Whether the request was served unsuccessfully. | |
205 bool get isFailure => !isSuccess; | |
206 | |
207 BarbackServerResult._success(this.url, this.id) | |
208 : error = null; | |
209 | |
210 BarbackServerResult._failure(this.url, this.id, this.error); | |
211 } | |
OLD | NEW |