Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a | 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. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 library leap_server; | 5 library leap_server; |
| 6 | 6 |
| 7 import 'dart:io'; | 7 import 'dart:io'; |
| 8 | 8 |
| 9 import 'dart:convert' show JSON, HtmlEscape; | |
| 10 | |
| 11 class ProjectCommand { | |
|
kasperl
2014/03/24 10:32:51
Maybe add a short comment for these classes to giv
ahe
2014/03/24 15:20:23
Done.
| |
| 12 final String name; | |
| 13 final Map<String, String> rules; | |
| 14 final Function handle; | |
| 15 | |
| 16 const ProjectCommand(this.name, this.rules, this.handle); | |
| 17 } | |
| 18 | |
| 9 class Conversation { | 19 class Conversation { |
| 10 HttpRequest request; | 20 HttpRequest request; |
| 11 HttpResponse response; | 21 HttpResponse response; |
| 12 | 22 |
| 23 static const String PROJECT_PATH = '/project'; | |
| 24 | |
| 25 static const String PACKAGES_PATH = '/packages'; | |
| 26 | |
| 13 static const String CONTENT_TYPE = HttpHeaders.CONTENT_TYPE; | 27 static const String CONTENT_TYPE = HttpHeaders.CONTENT_TYPE; |
| 14 | 28 |
| 15 static const String LEAP_LANDING_PAGE = | 29 static Uri documentRoot = Uri.base; |
| 16 'sdk/lib/_internal/compiler/samples/leap/index.html'; | |
| 17 | 30 |
| 18 static String landingPage = LEAP_LANDING_PAGE; | 31 static Uri projectRoot = Uri.base.resolve('site/try/src/'); |
| 32 | |
| 33 static Uri packageRoot = Uri.base.resolve('sdk/lib/_internal/'); | |
| 34 | |
| 35 static const List<ProjectCommand> COMMANDS = const <ProjectCommand>[ | |
| 36 const ProjectCommand('list', const {'list': null}, handleProjectList), | |
| 37 ]; | |
|
kasperl
2014/03/24 10:32:51
Less indentation?
ahe
2014/03/24 15:20:23
Done.
| |
| 19 | 38 |
| 20 Conversation(this.request, this.response); | 39 Conversation(this.request, this.response); |
| 21 | 40 |
| 22 onClosed(_) { | 41 onClosed(_) { |
| 23 if (response.statusCode == HttpStatus.OK) return; | 42 if (response.statusCode == HttpStatus.OK) return; |
| 24 print('Request for ${request.uri} ${response.statusCode}'); | 43 print('Request for ${request.uri} ${response.statusCode}'); |
| 25 } | 44 } |
| 26 | 45 |
| 27 notFound(path) { | 46 notFound(path) { |
| 28 response.statusCode = HttpStatus.NOT_FOUND; | 47 response.statusCode = HttpStatus.NOT_FOUND; |
| 29 response.write(htmlInfo('Not Found', | 48 response.write(htmlInfo('Not Found', |
| 30 'The file "$path" could not be found.')); | 49 'The file "$path" could not be found.')); |
| 31 response.close(); | 50 response.close(); |
| 32 } | 51 } |
| 33 | 52 |
| 34 redirect(String location) { | 53 redirect(String location) { |
| 35 response.statusCode = HttpStatus.FOUND; | 54 response.statusCode = HttpStatus.FOUND; |
| 36 response.headers.add(HttpHeaders.LOCATION, location); | 55 response.headers.add(HttpHeaders.LOCATION, location); |
| 37 response.close(); | 56 response.close(); |
| 38 } | 57 } |
| 39 | 58 |
| 59 badRequest(String problem) { | |
| 60 response.statusCode = HttpStatus.BAD_REQUEST; | |
| 61 response.write(htmlInfo("Bad request", | |
| 62 "Bad request '${request.uri}': $problem")); | |
| 63 response.close(); | |
| 64 } | |
| 65 | |
| 66 bool validate(Map<String, String> parameters, Map<String, String> rules) { | |
| 67 String problem; | |
| 68 rules.forEach((String name, String desc) { | |
| 69 if (problem != null) return; | |
|
kasperl
2014/03/24 10:32:51
This seems hacky. How about something like this:
ahe
2014/03/24 15:20:23
Done.
| |
| 70 if (!parameters.containsKey(name)) { | |
| 71 return problem = "Missing parameter: '$name'."; | |
| 72 } | |
| 73 }); | |
| 74 if (problem == null) { | |
| 75 Set extra = new Set.from(parameters.keys)..removeAll(rules.keys); | |
| 76 if (!extra.isEmpty) { | |
| 77 String extraString = (extra.toList()..sort()).join("', '"); | |
| 78 problem = "Extra parameters: '$extraString'."; | |
| 79 } | |
| 80 } | |
| 81 if (problem != null) { | |
| 82 badRequest(problem); | |
| 83 return false; | |
| 84 } | |
| 85 return true; | |
| 86 } | |
| 87 | |
| 88 static handleProjectList(Conversation self) { | |
| 89 String nativeDir = projectRoot.toFilePath(); | |
| 90 Directory dir = new Directory(nativeDir); | |
| 91 var future = dir.list(recursive: true, followLinks: false).toList(); | |
| 92 future.then((List<FileSystemEntity> entries) { | |
| 93 List<String> files = <String>[]; | |
| 94 for (FileSystemEntity entry in entries) { | |
|
kasperl
2014/03/24 10:32:51
List<String> files = entries
.map((e) => e.pat
ahe
2014/03/24 15:20:23
Done.
| |
| 95 String nativePath = entry.path; | |
| 96 if (nativePath.endsWith("~")) continue; | |
| 97 if (!nativePath.startsWith(nativeDir)) continue; | |
| 98 nativePath = nativePath.substring(nativeDir.length); | |
| 99 files.add(new Uri.file(nativePath).path); | |
| 100 } | |
| 101 self.response | |
| 102 ..write(JSON.encode(files)) | |
| 103 ..close(); | |
| 104 }); | |
| 105 } | |
| 106 | |
| 107 handleProjectRequest() { | |
| 108 Map<String, String> parameters = request.uri.queryParameters; | |
| 109 for (ProjectCommand command in COMMANDS) { | |
| 110 if (parameters.containsKey(command.name)) { | |
| 111 if (validate(parameters, command.rules)) { | |
| 112 (command.handle)(this); | |
| 113 } | |
| 114 return; | |
| 115 } | |
| 116 } | |
| 117 String commands = COMMANDS.map((c) => c.name).join("', '"); | |
| 118 badRequest("Valid commands are: '$commands'"); | |
| 119 } | |
| 120 | |
| 40 handle() { | 121 handle() { |
| 41 response.done | 122 response.done |
| 42 .then(onClosed) | 123 .then(onClosed) |
| 43 .catchError(onError); | 124 .catchError(onError); |
| 44 | 125 |
| 45 String path = request.uri.path; | 126 Uri uri = request.uri; |
| 46 if (path == '/') return redirect('/$landingPage'); | 127 if (uri.path == PROJECT_PATH) { |
| 47 if (path == '/favicon.ico') { | 128 return handleProjectRequest(); |
| 48 path = '/sdk/lib/_internal/dartdoc/static/favicon.ico'; | |
| 49 } | 129 } |
| 50 if (path.contains('..') || path.contains('%')) return notFound(path); | 130 if (uri.path.endsWith('/')) { |
| 51 var f = new File("./$path"); | 131 uri = uri.resolve('index.html'); |
| 132 } | |
| 133 if (uri.path == '/css/fonts/fontawesome-webfont.woff') { | |
| 134 uri = uri.resolve('/fontawesome-webfont.woff'); | |
| 135 } | |
| 136 if (uri.path.contains('..') || uri.path.contains('%')) { | |
| 137 return notFound(uri.path); | |
| 138 } | |
| 139 String path = uri.path; | |
| 140 Uri root = documentRoot; | |
| 141 String dartType = 'application/dart'; | |
| 142 if (path.startsWith('/project/packages/')) { | |
| 143 root = packageRoot; | |
| 144 path = path.substring('/project/packages'.length); | |
| 145 } else if (path.startsWith('${PROJECT_PATH}/')) { | |
| 146 root = projectRoot; | |
| 147 path = path.substring(PROJECT_PATH.length); | |
| 148 dartType = 'text/plain'; | |
| 149 } else if (path.startsWith('${PACKAGES_PATH}/')) { | |
| 150 root = packageRoot; | |
| 151 path = path.substring(PACKAGES_PATH.length); | |
| 152 } | |
| 153 var f = new File(root.resolve('.$path').toFilePath()); | |
| 52 f.exists().then((bool exists) { | 154 f.exists().then((bool exists) { |
| 53 if (!exists) return notFound(path); | 155 if (!exists) return notFound(path); |
| 54 if (path.endsWith('.html')) { | 156 if (path.endsWith('.html')) { |
| 55 response.headers.set(CONTENT_TYPE, 'text/html'); | 157 response.headers.set(CONTENT_TYPE, 'text/html'); |
| 56 } else if (path.endsWith('.dart')) { | 158 } else if (path.endsWith('.dart')) { |
| 57 response.headers.set(CONTENT_TYPE, 'application/dart'); | 159 response.headers.set(CONTENT_TYPE, dartType); |
| 58 } else if (path.endsWith('.js')) { | 160 } else if (path.endsWith('.js')) { |
| 59 response.headers.set(CONTENT_TYPE, 'application/javascript'); | 161 response.headers.set(CONTENT_TYPE, 'application/javascript'); |
| 60 } else if (path.endsWith('.ico')) { | 162 } else if (path.endsWith('.ico')) { |
| 61 response.headers.set(CONTENT_TYPE, 'image/x-icon'); | 163 response.headers.set(CONTENT_TYPE, 'image/x-icon'); |
| 62 } else if (path.endsWith('.appcache')) { | 164 } else if (path.endsWith('.appcache')) { |
| 63 response.headers.set(CONTENT_TYPE, 'text/cache-manifest'); | 165 response.headers.set(CONTENT_TYPE, 'text/cache-manifest'); |
| 64 } | 166 } |
| 65 f.openRead().pipe(response).catchError(onError); | 167 f.openRead().pipe(response).catchError(onError); |
| 66 }); | 168 }); |
| 67 } | 169 } |
| 68 | 170 |
| 69 static onRequest(HttpRequest request) { | 171 static onRequest(HttpRequest request) { |
| 70 new Conversation(request, request.response).handle(); | 172 new Conversation(request, request.response).handle(); |
| 71 } | 173 } |
| 72 | 174 |
| 73 static onError(error) { | 175 static onError(error) { |
| 74 if (error is HttpException) { | 176 if (error is HttpException) { |
| 75 print('Error: ${error.message}'); | 177 print('Error: ${error.message}'); |
| 76 } else { | 178 } else { |
| 77 print('Error: ${error}'); | 179 print('Error: ${error}'); |
| 78 } | 180 } |
| 79 } | 181 } |
| 80 | 182 |
| 81 String htmlInfo(String title, String text) { | 183 String htmlInfo(String title, String text) { |
| 184 // No script injection, please. | |
| 185 title = const HtmlEscape().convert(title); | |
| 186 text = const HtmlEscape().convert(text); | |
| 82 return """ | 187 return """ |
| 83 <!DOCTYPE html> | 188 <!DOCTYPE html> |
| 84 <html lang='en'> | 189 <html lang='en'> |
| 85 <head> | 190 <head> |
| 86 <title>$title</title> | 191 <title>$title</title> |
| 87 </head> | 192 </head> |
| 88 <body> | 193 <body> |
| 89 <h1>$title</h1> | 194 <h1>$title</h1> |
| 90 <p>$text</p> | 195 <p>$text</p> |
| 91 </body> | 196 </body> |
| 92 </html> | 197 </html> |
| 93 """; | 198 """; |
| 94 } | 199 } |
| 95 } | 200 } |
| 96 | 201 |
| 97 main(List<String> arguments) { | 202 main(List<String> arguments) { |
| 98 if (arguments.length > 0) { | 203 if (arguments.length > 0) { |
| 99 Conversation.landingPage = arguments[0]; | 204 Conversation.documentRoot = Uri.base.resolve(arguments[0]); |
| 100 } | 205 } |
| 101 var host = '127.0.0.1'; | 206 var host = '127.0.0.1'; |
| 102 if (arguments.length > 1) { | 207 if (arguments.length > 1) { |
| 103 host = arguments[1]; | 208 host = arguments[1]; |
| 104 } | 209 } |
| 105 int port = 0; | 210 int port = 0; |
| 106 if (arguments.length > 2) { | 211 if (arguments.length > 2) { |
| 107 port = int.parse(arguments[2]); | 212 port = int.parse(arguments[2]); |
| 108 } | 213 } |
| 214 if (arguments.length > 3) { | |
| 215 Conversation.projectRoot = Uri.base.resolve(arguments[3]); | |
| 216 } | |
| 217 if (arguments.length > 4) { | |
| 218 Conversation.packageRoot = Uri.base.resolve(arguments[4]); | |
| 219 } | |
| 109 HttpServer.bind(host, port).then((HttpServer server) { | 220 HttpServer.bind(host, port).then((HttpServer server) { |
| 110 print('HTTP server started on http://$host:${server.port}/'); | 221 print('HTTP server started on http://$host:${server.port}/'); |
| 111 server.listen(Conversation.onRequest, onError: Conversation.onError); | 222 server.listen(Conversation.onRequest, onError: Conversation.onError); |
| 112 }).catchError((e) { | 223 }).catchError((e) { |
| 113 print("HttpServer.bind error: $e"); | 224 print("HttpServer.bind error: $e"); |
| 114 exit(1); | 225 exit(1); |
| 115 }); | 226 }); |
| 116 } | 227 } |
| OLD | NEW |