OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2017, 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 part of sync.http; |
| 6 |
| 7 /// A simple synchronous HTTP client. |
| 8 /// |
| 9 /// This is a two-step process. When a [SyncHttpClientRequest] is returned the |
| 10 /// underlying network connection has been established, but no data has yet been |
| 11 /// sent. The HTTP headers and body can be set on the request, and close is |
| 12 /// called to send it to the server and get the [SyncHttpClientResponse]. |
| 13 abstract class SyncHttpClient { |
| 14 /// Send a GET request to the provided URL. |
| 15 static SyncHttpClientRequest getUrl(Uri uri) => |
| 16 new SyncHttpClientRequest._('GET', uri, false); |
| 17 |
| 18 /// Send a POST request to the provided URL. |
| 19 static SyncHttpClientRequest postUrl(uri) => |
| 20 new SyncHttpClientRequest._('POST', uri, true); |
| 21 |
| 22 /// Send a DELETE request to the provided URL. |
| 23 static SyncHttpClientRequest deleteUrl(uri) => |
| 24 new SyncHttpClientRequest._('DELETE', uri, false); |
| 25 |
| 26 /// Send a PUT request to the provided URL. |
| 27 static SyncHttpClientRequest putUrl(uri) => |
| 28 new SyncHttpClientRequest._('PUT', uri, true); |
| 29 } |
| 30 |
| 31 /// HTTP request for a synchronous client connection. |
| 32 class SyncHttpClientRequest { |
| 33 static const String _protocolVersion = '1.1'; |
| 34 |
| 35 /// The length of the request body. Is set to null when no body exists. |
| 36 int get contentLength => hasBody ? _body.length : null; |
| 37 |
| 38 HttpHeaders _headers; |
| 39 |
| 40 /// The headers associated with the HTTP request. |
| 41 HttpHeaders get headers { |
| 42 if (_headers == null) { |
| 43 _headers = new _SyncHttpClientRequestHeaders(this); |
| 44 } |
| 45 return _headers; |
| 46 } |
| 47 |
| 48 /// The type of HTTP request being made. |
| 49 final String method; |
| 50 |
| 51 /// The Uri the HTTP request will be sent to. |
| 52 final Uri uri; |
| 53 |
| 54 /// The default encoding for the HTTP request (UTF8). |
| 55 final Encoding encoding = UTF8; |
| 56 |
| 57 /// The body of the HTTP request. This can be empty if there is no body |
| 58 /// associated with the request. |
| 59 final BytesBuilder _body; |
| 60 |
| 61 /// The synchronous socket used to initiate the HTTP request. |
| 62 final RawSynchronousSocket _socket; |
| 63 |
| 64 SyncHttpClientRequest._(this.method, Uri uri, bool body) |
| 65 : this.uri = uri, |
| 66 this._body = body ? new BytesBuilder() : null, |
| 67 this._socket = RawSynchronousSocket.connectSync(uri.host, uri.port); |
| 68 |
| 69 /// Write content into the body of the HTTP request. |
| 70 void write(Object obj) { |
| 71 if (hasBody) { |
| 72 _body.add(encoding.encoder.convert(obj.toString())); |
| 73 } else { |
| 74 throw new StateError('write not allowed for method $method'); |
| 75 } |
| 76 } |
| 77 |
| 78 /// Specifies whether or not the HTTP request has a body. |
| 79 bool get hasBody => _body != null; |
| 80 |
| 81 /// Send the HTTP request and get the response. |
| 82 SyncHttpClientResponse close() { |
| 83 StringBuffer buffer = new StringBuffer(); |
| 84 buffer.write('$method ${uri.path} HTTP/$_protocolVersion\r\n'); |
| 85 headers.forEach((name, values) { |
| 86 values.forEach((value) { |
| 87 buffer.write('$name: $value\r\n'); |
| 88 }); |
| 89 }); |
| 90 buffer.write('\r\n'); |
| 91 if (hasBody) { |
| 92 buffer.write(new String.fromCharCodes(_body.takeBytes())); |
| 93 } |
| 94 _socket.writeFromSync(buffer.toString().codeUnits); |
| 95 return new SyncHttpClientResponse(_socket); |
| 96 } |
| 97 } |
| 98 |
| 99 class _SyncHttpClientRequestHeaders implements HttpHeaders { |
| 100 Map<String, List> _headers = <String, List<String>>{}; |
| 101 |
| 102 final SyncHttpClientRequest _request; |
| 103 ContentType contentType; |
| 104 |
| 105 _SyncHttpClientRequestHeaders(this._request); |
| 106 |
| 107 @override |
| 108 List<String> operator [](String name) { |
| 109 switch (name) { |
| 110 case HttpHeaders.ACCEPT_CHARSET: |
| 111 return ['utf-8']; |
| 112 case HttpHeaders.ACCEPT_ENCODING: |
| 113 return ['identity']; |
| 114 case HttpHeaders.CONNECTION: |
| 115 return ['close']; |
| 116 case HttpHeaders.CONTENT_LENGTH: |
| 117 if (!_request.hasBody) { |
| 118 return null; |
| 119 } |
| 120 return [contentLength.toString()]; |
| 121 case HttpHeaders.CONTENT_TYPE: |
| 122 if (contentType == null) { |
| 123 return null; |
| 124 } |
| 125 return [contentType.toString()]; |
| 126 case HttpHeaders.HOST: |
| 127 return ['$host:$port']; |
| 128 default: |
| 129 var values = _headers[name]; |
| 130 if (values == null || values.isEmpty) { |
| 131 return null; |
| 132 } |
| 133 return values.map((e) => e.toString()).toList(growable: false); |
| 134 } |
| 135 } |
| 136 |
| 137 /// Add [value] to the list of values associated with header [name]. |
| 138 @override |
| 139 void add(String name, Object value) { |
| 140 switch (name) { |
| 141 case HttpHeaders.ACCEPT_CHARSET: |
| 142 case HttpHeaders.ACCEPT_ENCODING: |
| 143 case HttpHeaders.CONNECTION: |
| 144 case HttpHeaders.CONTENT_LENGTH: |
| 145 case HttpHeaders.DATE: |
| 146 case HttpHeaders.EXPIRES: |
| 147 case HttpHeaders.IF_MODIFIED_SINCE: |
| 148 case HttpHeaders.HOST: |
| 149 throw new UnsupportedError('Unsupported or immutable property: $name'); |
| 150 case HttpHeaders.CONTENT_TYPE: |
| 151 contentType = value; |
| 152 break; |
| 153 default: |
| 154 if (_headers[name] == null) { |
| 155 _headers[name] = []; |
| 156 } |
| 157 _headers[name].add(value); |
| 158 } |
| 159 } |
| 160 |
| 161 /// Remove [value] from the list associated with header [name]. |
| 162 @override |
| 163 void remove(String name, Object value) { |
| 164 switch (name) { |
| 165 case HttpHeaders.ACCEPT_CHARSET: |
| 166 case HttpHeaders.ACCEPT_ENCODING: |
| 167 case HttpHeaders.CONNECTION: |
| 168 case HttpHeaders.CONTENT_LENGTH: |
| 169 case HttpHeaders.DATE: |
| 170 case HttpHeaders.EXPIRES: |
| 171 case HttpHeaders.IF_MODIFIED_SINCE: |
| 172 case HttpHeaders.HOST: |
| 173 throw new UnsupportedError('Unsupported or immutable property: $name'); |
| 174 case HttpHeaders.CONTENT_TYPE: |
| 175 if (contentType == value) { |
| 176 contentType = null; |
| 177 } |
| 178 break; |
| 179 default: |
| 180 if (_headers[name] != null) { |
| 181 _headers[name].remove(value); |
| 182 if (_headers[name].isEmpty) { |
| 183 _headers.remove(name); |
| 184 } |
| 185 } |
| 186 } |
| 187 } |
| 188 |
| 189 /// Remove all headers associated with key [name]. |
| 190 @override |
| 191 void removeAll(String name) { |
| 192 switch (name) { |
| 193 case HttpHeaders.ACCEPT_CHARSET: |
| 194 case HttpHeaders.ACCEPT_ENCODING: |
| 195 case HttpHeaders.CONNECTION: |
| 196 case HttpHeaders.CONTENT_LENGTH: |
| 197 case HttpHeaders.DATE: |
| 198 case HttpHeaders.EXPIRES: |
| 199 case HttpHeaders.IF_MODIFIED_SINCE: |
| 200 case HttpHeaders.HOST: |
| 201 throw new UnsupportedError('Unsupported or immutable property: $name'); |
| 202 case HttpHeaders.CONTENT_TYPE: |
| 203 contentType = null; |
| 204 break; |
| 205 default: |
| 206 _headers.remove(name); |
| 207 } |
| 208 } |
| 209 |
| 210 /// Replace values associated with key [name] with [value]. |
| 211 @override |
| 212 void set(String name, Object value) { |
| 213 removeAll(name); |
| 214 add(name, value); |
| 215 } |
| 216 |
| 217 /// Returns the values associated with key [name], if it exists, otherwise |
| 218 /// returns null. |
| 219 @override |
| 220 String value(String name) { |
| 221 var val = this[name]; |
| 222 if (val == null || val.isEmpty) { |
| 223 return null; |
| 224 } else if (val.length == 1) { |
| 225 return val[0]; |
| 226 } else { |
| 227 throw new HttpException('header $name has more than one value'); |
| 228 } |
| 229 } |
| 230 |
| 231 /// Iterates over all header key-value pairs and applies [f]. |
| 232 @override |
| 233 void forEach(void f(String name, List<String> values)) { |
| 234 var forEachFunc = (String name) { |
| 235 var values = this[name]; |
| 236 if (values != null && values.isNotEmpty) { |
| 237 f(name, values); |
| 238 } |
| 239 }; |
| 240 |
| 241 [ |
| 242 HttpHeaders.ACCEPT_CHARSET, |
| 243 HttpHeaders.ACCEPT_ENCODING, |
| 244 HttpHeaders.CONNECTION, |
| 245 HttpHeaders.CONTENT_LENGTH, |
| 246 HttpHeaders.CONTENT_TYPE, |
| 247 HttpHeaders.HOST |
| 248 ].forEach(forEachFunc); |
| 249 _headers.keys.forEach(forEachFunc); |
| 250 } |
| 251 |
| 252 @override |
| 253 bool get chunkedTransferEncoding => null; |
| 254 |
| 255 @override |
| 256 void set chunkedTransferEncoding(bool _chunkedTransferEncoding) { |
| 257 throw new UnsupportedError('chunked transfer is unsupported'); |
| 258 } |
| 259 |
| 260 @override |
| 261 int get contentLength => _request.contentLength; |
| 262 |
| 263 @override |
| 264 void set contentLength(int _contentLength) { |
| 265 throw new UnsupportedError('content length is automatically set'); |
| 266 } |
| 267 |
| 268 @override |
| 269 void set date(DateTime _date) { |
| 270 throw new UnsupportedError('date is unsupported'); |
| 271 } |
| 272 |
| 273 @override |
| 274 DateTime get date => null; |
| 275 |
| 276 @override |
| 277 void set expires(DateTime _expires) { |
| 278 throw new UnsupportedError('expires is unsupported'); |
| 279 } |
| 280 |
| 281 @override |
| 282 DateTime get expires => null; |
| 283 |
| 284 @override |
| 285 void set host(String _host) { |
| 286 throw new UnsupportedError('host is automatically set'); |
| 287 } |
| 288 |
| 289 @override |
| 290 String get host => _request.uri.host; |
| 291 |
| 292 @override |
| 293 DateTime get ifModifiedSince => null; |
| 294 |
| 295 @override |
| 296 void set ifModifiedSince(DateTime _ifModifiedSince) { |
| 297 throw new UnsupportedError('if modified since is unsupported'); |
| 298 } |
| 299 |
| 300 @override |
| 301 void noFolding(String name) { |
| 302 throw new UnsupportedError('no folding is unsupported'); |
| 303 } |
| 304 |
| 305 @override |
| 306 bool get persistentConnection => false; |
| 307 |
| 308 @override |
| 309 void set persistentConnection(bool _persistentConnection) { |
| 310 throw new UnsupportedError('persistence connections are unsupported'); |
| 311 } |
| 312 |
| 313 @override |
| 314 void set port(int _port) { |
| 315 throw new UnsupportedError('port is automatically set'); |
| 316 } |
| 317 |
| 318 @override |
| 319 int get port => _request.uri.port; |
| 320 |
| 321 /// Clear all header key-value pairs. |
| 322 @override |
| 323 void clear() { |
| 324 contentType = null; |
| 325 _headers.clear(); |
| 326 } |
| 327 } |
| 328 |
| 329 /// HTTP response for a client connection. |
| 330 class SyncHttpClientResponse { |
| 331 /// The length of the body associated with the HTTP response. |
| 332 int get contentLength => headers.contentLength; |
| 333 |
| 334 /// The headers associated with the HTTP response. |
| 335 final HttpHeaders headers; |
| 336 |
| 337 /// A short textual description of the status code associated with the HTTP |
| 338 /// response. |
| 339 final String reasonPhrase; |
| 340 |
| 341 /// The resulting HTTP status code associated with the HTTP response. |
| 342 final int statusCode; |
| 343 |
| 344 /// The body of the HTTP response. |
| 345 final String body; |
| 346 |
| 347 /// Creates an instance of [SyncHttpClientResponse] that contains the response |
| 348 /// sent by the HTTP server over [socket]. |
| 349 factory SyncHttpClientResponse(RawSynchronousSocket socket) { |
| 350 int statusCode; |
| 351 String reasonPhrase; |
| 352 StringBuffer body = new StringBuffer(); |
| 353 Map<String, List<String>> headers = {}; |
| 354 |
| 355 bool inHeader = false; |
| 356 bool inBody = false; |
| 357 int contentLength = 0; |
| 358 int contentRead = 0; |
| 359 |
| 360 void processLine(String line, int bytesRead, _LineDecoder decoder) { |
| 361 if (inBody) { |
| 362 body.write(line); |
| 363 contentRead += bytesRead; |
| 364 } else if (inHeader) { |
| 365 if (line.trim().isEmpty) { |
| 366 inBody = true; |
| 367 if (contentLength > 0) { |
| 368 decoder.expectedByteCount = contentLength; |
| 369 } |
| 370 return; |
| 371 } |
| 372 int separator = line.indexOf(':'); |
| 373 String name = line.substring(0, separator).toLowerCase().trim(); |
| 374 String value = line.substring(separator + 1).trim(); |
| 375 if (name == HttpHeaders.TRANSFER_ENCODING && |
| 376 value.toLowerCase() != 'identity') { |
| 377 throw new UnsupportedError( |
| 378 'only identity transfer encoding is accepted'); |
| 379 } |
| 380 if (name == HttpHeaders.CONTENT_LENGTH) { |
| 381 contentLength = int.parse(value); |
| 382 } |
| 383 if (!headers.containsKey(name)) { |
| 384 headers[name] = []; |
| 385 } |
| 386 headers[name].add(value); |
| 387 } else if (line.startsWith('HTTP/1.1') || line.startsWith('HTTP/1.0')) { |
| 388 statusCode = int |
| 389 .parse(line.substring('HTTP/1.x '.length, 'HTTP/1.x xxx'.length)); |
| 390 reasonPhrase = line.substring('HTTP/1.x xxx '.length); |
| 391 inHeader = true; |
| 392 } else { |
| 393 throw new UnsupportedError('unsupported http response format'); |
| 394 } |
| 395 } |
| 396 |
| 397 var lineDecoder = new _LineDecoder.withCallback(processLine); |
| 398 |
| 399 try { |
| 400 while (!inHeader || |
| 401 !inBody || |
| 402 ((contentRead + lineDecoder.bufferedBytes) < contentLength)) { |
| 403 var bytes = socket.readSync(1024); |
| 404 |
| 405 if (bytes == null || bytes.length == 0) { |
| 406 break; |
| 407 } |
| 408 lineDecoder.add(bytes); |
| 409 } |
| 410 } finally { |
| 411 try { |
| 412 lineDecoder.close(); |
| 413 } finally { |
| 414 socket.closeSync(); |
| 415 } |
| 416 } |
| 417 |
| 418 return new SyncHttpClientResponse._( |
| 419 reasonPhrase: reasonPhrase, |
| 420 statusCode: statusCode, |
| 421 body: body.toString(), |
| 422 headers: headers); |
| 423 } |
| 424 |
| 425 SyncHttpClientResponse._( |
| 426 {this.reasonPhrase, this.statusCode, this.body, headers}) |
| 427 : this.headers = new _SyncHttpClientResponseHeaders(headers); |
| 428 } |
| 429 |
| 430 class _SyncHttpClientResponseHeaders implements HttpHeaders { |
| 431 final Map<String, List<String>> _headers; |
| 432 |
| 433 _SyncHttpClientResponseHeaders(this._headers); |
| 434 |
| 435 @override |
| 436 List<String> operator [](String name) => _headers[name]; |
| 437 |
| 438 @override |
| 439 void add(String name, Object value) { |
| 440 throw new UnsupportedError('Response headers are immutable'); |
| 441 } |
| 442 |
| 443 @override |
| 444 bool get chunkedTransferEncoding => null; |
| 445 |
| 446 @override |
| 447 void set chunkedTransferEncoding(bool _chunkedTransferEncoding) { |
| 448 throw new UnsupportedError('Response headers are immutable'); |
| 449 } |
| 450 |
| 451 @override |
| 452 int get contentLength { |
| 453 String val = value(HttpHeaders.CONTENT_LENGTH); |
| 454 if (val != null) { |
| 455 return int.parse(val, onError: (_) => null); |
| 456 } |
| 457 return null; |
| 458 } |
| 459 |
| 460 @override |
| 461 void set contentLength(int _contentLength) { |
| 462 throw new UnsupportedError('Response headers are immutable'); |
| 463 } |
| 464 |
| 465 @override |
| 466 ContentType get contentType { |
| 467 var val = value(HttpHeaders.CONTENT_TYPE); |
| 468 if (val != null) { |
| 469 return ContentType.parse(val); |
| 470 } |
| 471 return null; |
| 472 } |
| 473 |
| 474 @override |
| 475 void set contentType(ContentType _contentType) { |
| 476 throw new UnsupportedError('Response headers are immutable'); |
| 477 } |
| 478 |
| 479 @override |
| 480 void set date(DateTime _date) { |
| 481 throw new UnsupportedError('Response headers are immutable'); |
| 482 } |
| 483 |
| 484 @override |
| 485 DateTime get date { |
| 486 var val = value(HttpHeaders.DATE); |
| 487 if (val != null) { |
| 488 return DateTime.parse(val); |
| 489 } |
| 490 return null; |
| 491 } |
| 492 |
| 493 @override |
| 494 void set expires(DateTime _expires) { |
| 495 throw new UnsupportedError('Response headers are immutable'); |
| 496 } |
| 497 |
| 498 @override |
| 499 DateTime get expires { |
| 500 var val = value(HttpHeaders.EXPIRES); |
| 501 if (val != null) { |
| 502 return DateTime.parse(val); |
| 503 } |
| 504 return null; |
| 505 } |
| 506 |
| 507 @override |
| 508 void forEach(void f(String name, List<String> values)) => _headers.forEach(f); |
| 509 |
| 510 @override |
| 511 void set host(String _host) { |
| 512 throw new UnsupportedError('Response headers are immutable'); |
| 513 } |
| 514 |
| 515 @override |
| 516 String get host { |
| 517 var val = value(HttpHeaders.HOST); |
| 518 if (val != null) { |
| 519 return Uri.parse(val).host; |
| 520 } |
| 521 return null; |
| 522 } |
| 523 |
| 524 @override |
| 525 DateTime get ifModifiedSince { |
| 526 var val = value(HttpHeaders.IF_MODIFIED_SINCE); |
| 527 if (val != null) { |
| 528 return DateTime.parse(val); |
| 529 } |
| 530 return null; |
| 531 } |
| 532 |
| 533 @override |
| 534 void set ifModifiedSince(DateTime _ifModifiedSince) { |
| 535 throw new UnsupportedError('Response headers are immutable'); |
| 536 } |
| 537 |
| 538 @override |
| 539 void noFolding(String name) { |
| 540 throw new UnsupportedError('Response headers are immutable'); |
| 541 } |
| 542 |
| 543 @override |
| 544 bool get persistentConnection => false; |
| 545 |
| 546 @override |
| 547 void set persistentConnection(bool _persistentConnection) { |
| 548 throw new UnsupportedError('Response headers are immutable'); |
| 549 } |
| 550 |
| 551 @override |
| 552 void set port(int _port) { |
| 553 throw new UnsupportedError('Response headers are immutable'); |
| 554 } |
| 555 |
| 556 @override |
| 557 int get port { |
| 558 var val = value(HttpHeaders.HOST); |
| 559 if (val != null) { |
| 560 return Uri.parse(val).port; |
| 561 } |
| 562 return null; |
| 563 } |
| 564 |
| 565 @override |
| 566 void remove(String name, Object value) { |
| 567 throw new UnsupportedError('Response headers are immutable'); |
| 568 } |
| 569 |
| 570 @override |
| 571 void removeAll(String name) { |
| 572 throw new UnsupportedError('Response headers are immutable'); |
| 573 } |
| 574 |
| 575 @override |
| 576 void set(String name, Object value) { |
| 577 throw new UnsupportedError('Response headers are immutable'); |
| 578 } |
| 579 |
| 580 @override |
| 581 String value(String name) { |
| 582 var val = this[name]; |
| 583 if (val == null || val.isEmpty) { |
| 584 return null; |
| 585 } else if (val.length == 1) { |
| 586 return val[0]; |
| 587 } else { |
| 588 throw new HttpException('header $name has more than one value'); |
| 589 } |
| 590 } |
| 591 |
| 592 @override |
| 593 void clear() { |
| 594 throw new UnsupportedError('Response headers are immutable'); |
| 595 } |
| 596 } |
OLD | NEW |