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