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