├── .github └── workflows │ └── dart.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── dart_test.yaml ├── example └── example.dart ├── lib ├── io.dart └── src │ ├── _browser_http_client_impl.dart │ ├── _browser_http_client_request_impl.dart │ ├── _browser_http_client_response_impl.dart │ ├── _exports_in_browser.dart │ ├── _exports_in_nodejs.dart │ ├── _exports_in_vm.dart │ ├── _helpers.dart │ ├── _helpers_impl_browser.dart │ ├── _helpers_impl_elsewhere.dart │ ├── _http_headers_impl.dart │ ├── _io_sink_base.dart │ ├── browser_http_client.dart │ ├── browser_http_client_exception.dart │ ├── browser_http_client_request.dart │ ├── browser_http_client_response.dart │ ├── bytes_builder.dart │ ├── http_client.dart │ ├── internet_address.dart │ ├── new_universal_http_client.dart │ └── platform.dart ├── pubspec.yaml └── test ├── http_client_test.dart ├── internet_address_test.dart ├── localhost.crt ├── localhost.key ├── platform_test.dart └── server.dart /.github/workflows/dart.yml: -------------------------------------------------------------------------------- 1 | name: Dart CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | os: [ubuntu-latest] 11 | sdk: [stable, beta, dev] 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: dart-lang/setup-dart@v1 15 | with: 16 | sdk: ${{ matrix.sdk }} 17 | - name: Install dependencies 18 | run: dart pub get 19 | - name: Run tests 20 | run: dart test --platform vm 21 | - name: Verify formatting 22 | run: dart format --output=none --set-exit-if-changed . 23 | - name: Analyze project source 24 | run: dart analyze -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # VSCode 19 | .vscode/ 20 | 21 | # Flutter/Dart/Pub related 22 | **/doc/api/ 23 | **/ios/Flutter/.last_build_id 24 | .dart_tool/ 25 | .flutter-plugins 26 | .flutter-plugins-dependencies 27 | .packages 28 | .pub-cache/ 29 | .pub/ 30 | pubspec.lock 31 | /build/ 32 | 33 | # Web related 34 | lib/generated_plugin_registrant.dart 35 | 36 | # Symbolication related 37 | app.*.symbols 38 | 39 | # Obfuscation related 40 | app.*.map.json 41 | 42 | # Android Studio will place build artifacts here 43 | /android/app/debug 44 | /android/app/profile 45 | /android/app/release 46 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2.2.2 2 | * Improve dependency constraints. 3 | 4 | # 2.2.1 5 | * Fixes pubspec.yaml for Dart 3.x. 6 | 7 | # 2.2.0 8 | * Refactoring, better documentation, and some additional APIs in BrowserHttpClient. 9 | 10 | # 2.1.0 11 | * Removes lots of unnecessary stuff. 12 | * This has some breaking changes, but it's unlikely that anyone is using the removed stuff. 13 | The package continues to follow the Dart SDK "dart:io" API. 14 | 15 | # 2.0.5 16 | * Fixes various small issues. 17 | 18 | # 2.0.4 19 | * Fixes Platform.operatingSystemVersion ([issue #9](https://github.com/dint-dev/universal_io/issues/9)). 20 | 21 | # 2.0.3 22 | * Fixes various issues. 23 | 24 | # 2.0.2 25 | * Fixes issue [#17](https://github.com/dint-dev/universal_io/issues/17). 26 | 27 | # 2.0.1 28 | * Fixes issues in Node.JS. 29 | 30 | # 2.0.0 31 | * Finishes migration to null safety. 32 | 33 | # 2.0.0-nullsafety.2 34 | * Eliminated unnecessary dependencies. 35 | 36 | # 2.0.0-nullsafety.1 37 | * Improves documentation. 38 | * Improves BrowserHttpClientException messages. 39 | * Deprecates libraries _prefer_sdk/io.dart_ and _prefer_universal/io.dart_. Developers should 40 | import just _io.dart_. 41 | 42 | # 2.0.0-nullsafety.0 43 | * The first null-safe version. 44 | * Makes changes in BrowserHttpClient / BrowserHttpClientRequest API: 45 | * The property for enabling credentials mode is now `browserCredentialsMode`. The default is 46 | `false`. 47 | * The property for setting response type is now `browserResponseType` ("arraybuffer", "text", 48 | etc.). By default, if HTTP request header "Accept" contains only text MIMEs ("text/plain", 49 | etc.), this package uses _responseType_ "text". 50 | * HTTP client now has `onBrowserHttpClientRequestClose` for using your own logic for setting 51 | `browserResponseType`. 52 | * Removes IO adapter API. 53 | 54 | # 1.0.2 55 | * Fixes issue [#11](https://github.com/dint-dev/universal_io/issues/11) (InternetAddress 56 | parameter). 57 | * Fixes issue [#12](https://github.com/dint-dev/universal_io/issues/12) (CORS credentials mode). 58 | Eliminates legacy, complicated behavior. Developers should choose either _omit_ or _include_. 59 | Improves error messages and documentation related to it. 60 | * Replaces MD5/SHA1 implementations used by some of the source code copied from _dart:io_. It now 61 | uses _package:crypto_ instead of implementations copied from _dart:io_. 62 | 63 | # 1.0.0 64 | * Implements recent changes in 'dart:io' (Dart SDK 2.8). 65 | * HttpDriver is replaced by 'dart:io' HttpOverrides. 66 | * FileSystemDriver is replaced by 'dart:io' IOOverrides. 67 | * Various other driver APIs are renamed or removed. 68 | * BrowserLikeHttpClientRequest is now BrowserHttpClientRequest. 69 | * BrowserHttpClientRequest implementation is improved. 70 | 71 | # 0.8.6 72 | * Fixed documentation and small fixes related to `nodejs_io`. 73 | 74 | # 0.8.5 75 | * Raised minimum SDK to 2.6 and upgraded dependencies. 76 | * Changed how CORS credentials mode is enabled. It was previously enabled with a header, but now 77 | we introduced subclasses for HttpClient and HttpClientRequest. This is a breaking change, but we 78 | decided not to bump the major version number. 79 | * Improved analysis and test settings. 80 | 81 | # 0.8.4 82 | * Added 'prefer_sdk/io.dart' and 'prefer_universal/io.dart' libraries for dealing with conditional 83 | export issues. 84 | * Library 'package:universal_io/io.dart' now exports SDK version by default. 85 | 86 | # 0.8.3 87 | * Replaced IP address parsing with the new Uri.parseIPv4Address / Uri.parseIPv6Address. 88 | * Fixed missing HTTP status codes. 89 | 90 | # 0.8.2 91 | * Fixed problems introduced by Dart SDK 2.5.0-dev-2.0. 92 | 93 | # 0.8.1 94 | * Fixed pubspec.yaml and documented Dart SDK 2.5 breaking changes. 95 | 96 | # 0.8.0 97 | * Updated classes to Dart 2.5. See [Dart SDK documentation about the changes](https://github.com/dart-lang/sdk/blob/master/CHANGELOG.md). 98 | * Various APIs now return `Uint8List` instead of `List`. Examples: `File`, `Socket`, `HttpClientResponse`. 99 | * Various other breaking changes such as `Cookie` constructor. 100 | 101 | # 0.7.3 102 | * Fixed the following error thrown by the Dart build system in some cases: "Unsupported conditional import of dart:io found in universal_io|lib/io.dart". 103 | 104 | # 0.7.2 105 | * Small fixes. 106 | 107 | # 0.7.1 108 | * Fixed various bugs. 109 | * Improved the test suite. 110 | 111 | # 0.7.0 112 | * Improved driver base classes and the test suite. 113 | 114 | # 0.6.0 115 | * Major refactoring of IODriver API. 116 | 117 | # 0.5.1 118 | * Fixed small bugs. 119 | 120 | # 0.5.0 121 | * Fixed various bugs. 122 | * Re-organized source code. 123 | * Eliminated dependencies by doing IP parsing in this package. 124 | * Improved the test suite for drivers. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Pub Package](https://img.shields.io/pub/v/universal_io.svg)](https://pub.dartlang.org/packages/universal_io) 2 | [![package publisher](https://img.shields.io/pub/publisher/universal_io.svg)](https://pub.dev/packages/universal_io/publisher) 3 | [![Github Actions CI](https://github.com/dint-dev/universal_io/workflows/Dart%20CI/badge.svg)](https://github.com/dint-dev/universal_io/actions) 4 | 5 | # Overview 6 | A cross-platform [dart:io](https://api.dart.dev/stable/2.19.2/dart-io/dart-io-library.html) that 7 | works on all platforms, including browsers. 8 | 9 | You can simply replace _dart:io_ imports with _package:universal_io/io.dart_. 10 | 11 | Licensed under the [Apache License 2.0](LICENSE). 12 | Some of the source code was derived [from Dart SDK](https://github.com/dart-lang/sdk/tree/master/sdk/lib/io), 13 | which was obtained under the BSD-style license of Dart SDK. See LICENSE file for details. 14 | 15 | ## APIs added on top of "dart:io" 16 | * [newUniversalHttpClient](https://pub.dev/documentation/universal_io/latest/universal_io/newUniversalHttpClient.html) 17 | * Returns BrowserHttpClient on browsers and the normal "dart:io" HttpClient on other platforms. 18 | * [BrowserHttpClient](https://pub.dev/documentation/universal_io/latest/universal_io/BrowserHttpClient-class.html) 19 | * A subclass of "dart:io" [HttpClient](https://api.dart.dev/stable/2.19.2/dart-io/HttpClient-class.html) 20 | that works on browsers. 21 | * [BrowserHttpClientRequest](https://pub.dev/documentation/universal_io/latest/universal_io/BrowserHttpClientRequest-class.html) 22 | * A subclass of "dart:io" [HttpClientRequest](https://api.dart.dev/stable/2.19.2/dart-io/HttpClientRequest-class.html) 23 | that works on browsers. 24 | * [BrowserHttpClientResponse](https://pub.dev/documentation/universal_io/latest/universal_io/BrowserHttpClientResponse-class.html) 25 | * A subclass of "dart:io" [HttpClientResponse](https://api.dart.dev/stable/2.19.2/dart-io/HttpClientResponse-class.html) 26 | that works on browsers. 27 | * [BrowserHttpClientException](https://pub.dev/documentation/universal_io/latest/universal_io/BrowserHttpClientException-class.html) 28 | * An exception that helps you understand why a HTTP request on a browser may have failed 29 | (see explanation below). 30 | 31 | ## Other features 32 | The following features may be deprecated in the future versions (3.x) of the package: 33 | * [HttpClient](https://pub.dev/documentation/universal_io/latest/universal_io/HttpClient-class.html) 34 | * `HttpClient()` factory is changed so that it returns BrowserHttpClient on browsers. 35 | * [Platform](https://pub.dev/documentation/universal_io/latest/universal_io/Platform-class.html) 36 | * The package makes methods like `Platform.isAndroid` and `Platform.isMacOS` work in the 37 | browsers too. 38 | * [InternetAddress](https://pub.dev/documentation/universal_io/latest/universal_io/InternetAddress-class.html) 39 | * The package makes it works in the browsers too. 40 | * [BytesBuilder](https://pub.dev/documentation/universal_io/latest/universal_io/BytesBuilder-class.html) 41 | * The package makes it works in the browsers too. 42 | 43 | ## Links 44 | * [API reference](https://pub.dev/documentation/universal_io/latest/) 45 | * [Github project](https://github.com/dint-dev/universal_io) 46 | * We appreciate feedback, issue reports, and pull requests. 47 | 48 | ## Similar packages 49 | * [universal_html](https://pub.dev/packages/universal_html) (cross-platform _dart:html_) 50 | 51 | 52 | # Getting started 53 | ## 1.Add dependency 54 | In pubspec.yaml: 55 | ```yaml 56 | dependencies: 57 | universal_io: ^2.2.2 58 | ``` 59 | 60 | ## 2.Use HTTP client 61 | ```dart 62 | import 'package:universal_io/io.dart'; 63 | 64 | Future main() async { 65 | // HttpClient can be used in browser too! 66 | HttpClient httpClient = newUniversalHttpClient(); // Recommended way of creating HttpClient. 67 | final request = await httpClient.getUrl(Uri.parse("https://dart.dev/")); 68 | final response = await request.close(); 69 | } 70 | ``` 71 | 72 | # HTTP client behavior 73 | HTTP client is implemented with [XMLHttpRequest (XHR)](https://developer.mozilla.org/en/docs/Web/API/XMLHttpRequest) 74 | API on browsers. 75 | 76 | XHR causes the following differences with _dart:io_: 77 | * HTTP connection is created only after `request.close()` has been called. 78 | * Same-origin policy limitations. For making cross-origin requests, see documentation below. 79 | 80 | ## Helpful error messages 81 | When requests fail and assertions are enabled, error messages contains descriptions how to fix 82 | possible issues such as missing cross-origin headers. 83 | 84 | The error messages look like the following: 85 | ```text 86 | XMLHttpRequest error. 87 | ------------------------------------------------------------------------------- 88 | HTTP method: PUT 89 | HTTP URL: http://destination.com/example 90 | Origin: http://source.com 91 | Cross-origin: true 92 | browserCredentialsMode: false 93 | browserResponseType: arraybuffer 94 | 95 | THE REASON FOR THE XHR ERROR IS UNKNOWN. 96 | (For security reasons, browsers do not explain XHR errors.) 97 | 98 | Is the server down? Did the server have an internal error? 99 | 100 | Enabling credentials mode would enable use of some HTTP headers in both the 101 | request and the response. For example, credentials mode is required for 102 | sending/receiving cookies. If you think you need to enable 'credentials mode', 103 | do the following: 104 | 105 | final httpClientRequest = ...; 106 | if (httpClientRequest is BrowserHttpClientRequest) { 107 | httpClientRequest.browserCredentialsMode = true; 108 | } 109 | 110 | Did the server respond to a cross-origin "preflight" (OPTIONS) request? 111 | 112 | Did the server send the following headers? 113 | * Access-Control-Allow-Origin: http://source.com 114 | * You can also use wildcard ("*"). 115 | * Always required for cross-origin requests! 116 | * Access-Control-Allow-Methods: PUT 117 | * You can also use wildcard ("*"). 118 | ``` 119 | 120 | Sometimes when you do cross-origin requests in browsers, you want to use 121 | [CORS "credentials mode"](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS). This can be 122 | achieved with the following pattern: 123 | ```dart 124 | Future main() async { 125 | final client = HttpClient(); 126 | final request = client.getUrl(Url.parse('http://example/url')); 127 | 128 | // Enable credentials mode 129 | if (request is BrowserHttpClientRequest) { 130 | request.browserCredentialsMode = true; 131 | } 132 | 133 | // Close request 134 | final response = await request.close(); 135 | // ... 136 | } 137 | ``` 138 | 139 | ## Streaming text responses 140 | The underlying XMLHttpRequest (XHR) API supports response streaming only when _responseType_ is 141 | "text". 142 | 143 | This package automatically uses _responseType_ "text" based on value of the 144 | HTTP request header "Accept". These media types are defined 145 | [BrowserHttpClient.defaultTextMimes](https://pub.dev/documentation/universal_io/latest/universal_io/BrowserHttpClient/defaultTextMimes.html): 146 | * "text/*" (any text media type) 147 | * "application/grpc-web" 148 | * "application/grpc-web+proto" 149 | 150 | If you want to disable streaming, use the following pattern: 151 | ```dart 152 | Future main() async { 153 | final client = newUniversalHttpClient(); 154 | if (client is BrowserHttpClient) { 155 | client.textMimes = const {}; 156 | } 157 | // ... 158 | } 159 | ``` -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lints/recommended.yaml -------------------------------------------------------------------------------- /dart_test.yaml: -------------------------------------------------------------------------------- 1 | platforms: [ chrome, vm, node ] 2 | tags: 3 | ipv6: 4 | -------------------------------------------------------------------------------- /example/example.dart: -------------------------------------------------------------------------------- 1 | import 'package:universal_io/io.dart'; 2 | 3 | void main() async { 4 | // Use 'dart:io' HttpClient API. 5 | final httpClient = HttpClient(); 6 | final request = await httpClient.getUrl(Uri.parse('https://dart.dev')); 7 | final response = await request.close(); 8 | print(response.toString()); 9 | } 10 | -------------------------------------------------------------------------------- /lib/io.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 terrier989@gmail.com. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /// Cross-platform implementation of "dart:io" that works on browsers too. 16 | /// 17 | /// # Usage 18 | /// Replace imports of "dart:io" with: 19 | /// ``` 20 | /// import 'package:universal_io/io.dart'; 21 | /// ``` 22 | library universal_io; 23 | 24 | export 'src/_exports_in_vm.dart' 25 | if (dart.library.html) 'src/_exports_in_browser.dart' 26 | if (dart.library.js) 'src/_exports_in_nodejs.dart'; 27 | -------------------------------------------------------------------------------- /lib/src/_browser_http_client_impl.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 terrier989@gmail.com. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'dart:async'; 16 | 17 | import '_browser_http_client_request_impl.dart'; 18 | import '_exports_in_browser.dart'; 19 | 20 | /// Browser implementation of _dart:io_ [HttpClient]. 21 | class BrowserHttpClientImpl extends BrowserHttpClient { 22 | @override 23 | Duration idleTimeout = Duration(seconds: 15); 24 | 25 | @override 26 | Duration? connectionTimeout; 27 | 28 | @override 29 | int? maxConnectionsPerHost; 30 | 31 | @override 32 | bool autoUncompress = true; 33 | 34 | @override 35 | String? userAgent; 36 | 37 | @override 38 | Future Function(Uri url, String scheme, String realm)? authenticate; 39 | 40 | @override 41 | Future Function(String host, int port, String scheme, String realm)? 42 | authenticateProxy; 43 | 44 | @override 45 | bool Function(X509Certificate cert, String host, int port)? 46 | badCertificateCallback; 47 | 48 | @override 49 | String Function(Uri url)? findProxy; 50 | 51 | bool _isClosed = false; 52 | 53 | BrowserHttpClientImpl() : super.constructor(); 54 | 55 | @override 56 | set connectionFactory( 57 | Future> Function( 58 | Uri url, String? proxyHost, int? proxyPort)? 59 | f) { 60 | // TODO: implement connectionFactory 61 | } 62 | 63 | @override 64 | set keyLog(Function(String line)? callback) { 65 | // TODO: implement keyLog 66 | } 67 | 68 | @override 69 | void addCredentials( 70 | Uri url, String realm, HttpClientCredentials credentials) { 71 | throw UnimplementedError(); 72 | } 73 | 74 | @override 75 | void addProxyCredentials( 76 | String host, int port, String realm, HttpClientCredentials credentials) {} 77 | 78 | @override 79 | void close({bool force = false}) { 80 | _isClosed = true; 81 | } 82 | 83 | @override 84 | Future delete(String host, int? port, String path) { 85 | return open('DELETE', host, port, path); 86 | } 87 | 88 | @override 89 | Future deleteUrl(Uri url) { 90 | return openUrl('DELETE', url); 91 | } 92 | 93 | @override 94 | Future get(String host, int? port, String path) { 95 | return open('GET', host, port, path); 96 | } 97 | 98 | @override 99 | Future getUrl(Uri url) { 100 | return openUrl('GET', url); 101 | } 102 | 103 | @override 104 | Future head(String host, int? port, String path) { 105 | return open('HEAD', host, port, path); 106 | } 107 | 108 | @override 109 | Future headUrl(Uri url) { 110 | return openUrl('HEAD', url); 111 | } 112 | 113 | @override 114 | Future open( 115 | String method, String host, int? port, String path) { 116 | String? query; 117 | final i = path.indexOf('?'); 118 | if (i >= 0) { 119 | query = path.substring(i + 1); 120 | path = path.substring(0, i); 121 | } 122 | final uri = Uri( 123 | scheme: 'http', 124 | host: host, 125 | port: port, 126 | path: path, 127 | query: query, 128 | fragment: null, 129 | ); 130 | return openUrl(method, uri); 131 | } 132 | 133 | @override 134 | Future openUrl(String method, Uri url) async { 135 | if (_isClosed) { 136 | throw StateError('HTTP client is closed'); 137 | } 138 | var scheme = url.scheme; 139 | var needsNewUrl = false; 140 | if (scheme.isEmpty) { 141 | scheme = 'https'; 142 | needsNewUrl = true; 143 | } else { 144 | switch (scheme) { 145 | case '': 146 | scheme = 'https'; 147 | needsNewUrl = true; 148 | break; 149 | case 'http': 150 | break; 151 | case 'https': 152 | break; 153 | default: 154 | throw ArgumentError.value( 155 | url, 156 | 'url', 157 | 'Unsupported scheme', 158 | ); 159 | } 160 | } 161 | if (needsNewUrl) { 162 | url = Uri( 163 | scheme: scheme, 164 | userInfo: url.userInfo, 165 | host: url.host, 166 | port: url.port, 167 | query: url.query, 168 | fragment: url.fragment, 169 | ); 170 | } 171 | return BrowserHttpClientRequestImpl(this, method, url); 172 | } 173 | 174 | @override 175 | Future patch(String host, int? port, String path) { 176 | return open('PATCH', host, port, path); 177 | } 178 | 179 | @override 180 | Future patchUrl(Uri url) { 181 | return openUrl('PATCH', url); 182 | } 183 | 184 | @override 185 | Future post(String host, int? port, String path) { 186 | return open('POST', host, port, path); 187 | } 188 | 189 | @override 190 | Future postUrl(Uri url) { 191 | return openUrl('POST', url); 192 | } 193 | 194 | @override 195 | Future put(String host, int? port, String path) { 196 | return open('PUT', host, port, path); 197 | } 198 | 199 | @override 200 | Future putUrl(Uri url) { 201 | return openUrl('PUT', url); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /lib/src/_browser_http_client_request_impl.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 terrier989@gmail.com. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'dart:async'; 16 | import 'dart:convert'; 17 | import 'dart:html' as html; 18 | import 'dart:typed_data'; 19 | 20 | import 'package:meta/meta.dart'; 21 | import 'package:typed_data/typed_buffers.dart'; 22 | 23 | import '_browser_http_client_response_impl.dart'; 24 | import '_exports_in_browser.dart'; 25 | import '_http_headers_impl.dart'; 26 | import '_io_sink_base.dart'; 27 | 28 | class BrowserHttpClientRequestImpl extends IOSinkBase 29 | implements BrowserHttpClientRequest { 30 | final BrowserHttpClient client; 31 | 32 | String? _browserResponseType; 33 | 34 | @override 35 | final String method; 36 | 37 | @override 38 | final Uri uri; 39 | 40 | @override 41 | final HttpHeaders headers = HttpHeadersImpl('1.1'); 42 | 43 | final Completer _completer = 44 | Completer(); 45 | 46 | Future? _addStreamFuture; 47 | 48 | @override 49 | final List cookies = []; 50 | 51 | final bool _supportsBody; 52 | 53 | Future? _result; 54 | 55 | final _buffer = Uint8Buffer(); 56 | 57 | @override 58 | bool bufferOutput = false; 59 | 60 | @override 61 | int contentLength = -1; 62 | 63 | @override 64 | bool followRedirects = true; 65 | 66 | @override 67 | int maxRedirects = 5; 68 | 69 | @override 70 | bool persistentConnection = false; 71 | 72 | @internal 73 | BrowserHttpClientRequestImpl(this.client, this.method, this.uri) 74 | : _supportsBody = _httpMethodSupportsBody(method) { 75 | // Add "User-Agent" header 76 | final userAgent = client.userAgent; 77 | if (userAgent != null) { 78 | headers.set(HttpHeaders.userAgentHeader, userAgent); 79 | } 80 | 81 | // Set default values 82 | browserCredentialsMode = client.browserCredentialsMode; 83 | followRedirects = true; 84 | maxRedirects = 5; 85 | bufferOutput = true; 86 | } 87 | 88 | @override 89 | String? get browserResponseType => _browserResponseType; 90 | 91 | @override 92 | set browserResponseType(String? value) { 93 | if (value != null) { 94 | const validValues = { 95 | 'arraybuffer', 96 | 'blob', 97 | 'document', 98 | 'json', 99 | 'text' 100 | }; 101 | if (!validValues.contains(value)) { 102 | throw ArgumentError.value(value); 103 | } 104 | } 105 | _browserResponseType = value; 106 | } 107 | 108 | @override 109 | HttpConnectionInfo? get connectionInfo => null; 110 | 111 | @override 112 | Future get done { 113 | return _completer.future; 114 | } 115 | 116 | @override 117 | void abort([Object? exception, StackTrace? stackTrace]) {} 118 | 119 | @override 120 | void add(List event) { 121 | _checkAddRequirements(); 122 | _buffer.addAll(event); 123 | } 124 | 125 | @override 126 | void addError(Object error, [StackTrace? stackTrace]) { 127 | if (_completer.isCompleted) { 128 | throw StateError('HTTP request is closed already'); 129 | } 130 | _completer.completeError(error, stackTrace); 131 | } 132 | 133 | @override 134 | Future addStream(Stream> stream) async { 135 | _checkAddRequirements(); 136 | final future = stream.listen((item) { 137 | _buffer.addAll(item); 138 | }, onError: (error) { 139 | addError(error); 140 | }, cancelOnError: true).asFuture(null); 141 | _addStreamFuture = future; 142 | await future; 143 | _addStreamFuture = null; 144 | } 145 | 146 | @override 147 | Future close() async { 148 | return _result ??= _close(); 149 | } 150 | 151 | @override 152 | Future flush() async { 153 | // Wait for added stream 154 | if (_addStreamFuture != null) { 155 | await _addStreamFuture; 156 | _addStreamFuture = null; 157 | } 158 | } 159 | 160 | void _checkAddRequirements() { 161 | if (!_supportsBody) { 162 | throw StateError('HTTP method $method does not support body'); 163 | } 164 | if (_completer.isCompleted) { 165 | throw StateError('StreamSink is closed'); 166 | } 167 | if (_addStreamFuture != null) { 168 | throw StateError('StreamSink is bound to a stream'); 169 | } 170 | } 171 | 172 | String _chooseBrowserResponseType() { 173 | final custom = browserResponseType; 174 | if (custom != null) { 175 | return custom; 176 | } 177 | final accept = headers.value('Accept'); 178 | if (accept != null) { 179 | try { 180 | final contentType = ContentType.parse(accept); 181 | final textMimes = BrowserHttpClient.defaultTextMimes; 182 | if ((contentType.primaryType == 'text' && 183 | textMimes.contains('text/*')) || 184 | textMimes.contains(contentType.mimeType)) { 185 | return 'text'; 186 | } 187 | } catch (error) { 188 | // Ignore error 189 | } 190 | } 191 | return 'arraybuffer'; 192 | } 193 | 194 | Future _close() async { 195 | await flush(); 196 | 197 | if (cookies.isNotEmpty) { 198 | _completer.completeError(StateError( 199 | 'Attempted to send cookies, but XMLHttpRequest does not support them.', 200 | )); 201 | return _completer.future; 202 | } 203 | 204 | final browserResponseType = _chooseBrowserResponseType(); 205 | _browserResponseType = browserResponseType; 206 | 207 | final callback = client.onBrowserHttpClientRequestClose; 208 | if (callback != null) { 209 | await callback(this); 210 | } 211 | 212 | try { 213 | final xhr = html.HttpRequest(); 214 | 215 | // Set method and URI 216 | final method = this.method; 217 | final uriString = uri.toString(); 218 | xhr.open(method, uriString); 219 | 220 | // Set response body type 221 | xhr.responseType = browserResponseType; 222 | 223 | // Timeout 224 | final timeout = client.connectionTimeout; 225 | if (timeout != null) { 226 | xhr.timeout = timeout.inMilliseconds; 227 | } 228 | 229 | // Credentials mode? 230 | final browserCredentialsMode = this.browserCredentialsMode; 231 | xhr.withCredentials = browserCredentialsMode; 232 | 233 | // Copy headers to html.HttpRequest 234 | final headers = this.headers; 235 | headers.forEach((name, values) { 236 | for (var value in values) { 237 | xhr.setRequestHeader(name, value); 238 | } 239 | }); 240 | 241 | final headersCompleter = _completer; 242 | final streamController = StreamController(); 243 | streamController.onCancel = () { 244 | if (xhr.readyState != html.HttpRequest.DONE) { 245 | xhr.abort(); 246 | } 247 | }; 248 | BrowserHttpClientResponseImpl? currentHttpClientResponse; 249 | 250 | void completeHeaders() { 251 | if (headersCompleter.isCompleted) { 252 | return; 253 | } 254 | try { 255 | // Create HttpClientResponse 256 | final httpClientResponse = BrowserHttpClientResponseImpl( 257 | this, 258 | xhr.status ?? 200, 259 | xhr.statusText ?? 'OK', 260 | streamController.stream, 261 | ); 262 | currentHttpClientResponse = httpClientResponse; 263 | final headers = httpClientResponse.headers; 264 | xhr.responseHeaders.forEach((name, value) { 265 | headers.add(name, value); 266 | }); 267 | headersCompleter.complete(httpClientResponse); 268 | httpClientResponse.browserResponse = xhr.response; 269 | } catch (error, stackTrace) { 270 | headersCompleter.completeError(error, stackTrace); 271 | } 272 | } 273 | 274 | if (browserResponseType == 'text') { 275 | // 276 | // "text" 277 | // 278 | var seenTextLength = -1; 279 | void addTextChunk() { 280 | if (!streamController.isClosed) { 281 | final response = xhr.response; 282 | if (response is String) { 283 | final textChunk = seenTextLength < 0 284 | ? response 285 | : response.substring(seenTextLength); 286 | seenTextLength = response.length; 287 | streamController.add(Utf8Encoder().convert(textChunk)); 288 | } 289 | } 290 | } 291 | 292 | xhr.onReadyStateChange.listen((event) { 293 | switch (xhr.readyState) { 294 | case html.HttpRequest.HEADERS_RECEIVED: 295 | completeHeaders(); 296 | break; 297 | 298 | case html.HttpRequest.DONE: 299 | currentHttpClientResponse?.browserResponse = xhr.response; 300 | if (!streamController.isClosed) { 301 | addTextChunk(); 302 | streamController.close(); 303 | } 304 | break; 305 | } 306 | }); 307 | streamController.onListen = () { 308 | addTextChunk(); 309 | if (xhr.readyState == html.HttpRequest.DONE) { 310 | streamController.close(); 311 | } 312 | }; 313 | xhr.onProgress.listen((html.ProgressEvent event) { 314 | if (streamController.hasListener) { 315 | addTextChunk(); 316 | } 317 | }); 318 | } else if (browserResponseType == 'arraybuffer') { 319 | // 320 | // "arraybuffer" 321 | // 322 | xhr.onReadyStateChange.listen((event) { 323 | switch (xhr.readyState) { 324 | case html.HttpRequest.HEADERS_RECEIVED: 325 | completeHeaders(); 326 | break; 327 | 328 | case html.HttpRequest.DONE: 329 | final object = xhr.response; 330 | currentHttpClientResponse?.browserResponse = object; 331 | if (!streamController.isClosed) { 332 | if (object is ByteBuffer) { 333 | // "arraybuffer" response type 334 | streamController.add(Uint8List.view(object)); 335 | } 336 | streamController.close(); 337 | } 338 | break; 339 | } 340 | }); 341 | } else { 342 | // 343 | // Something else than "text" or "arraybuffer" 344 | // 345 | xhr.onReadyStateChange.listen((event) { 346 | switch (xhr.readyState) { 347 | case html.HttpRequest.HEADERS_RECEIVED: 348 | completeHeaders(); 349 | break; 350 | 351 | case html.HttpRequest.DONE: 352 | currentHttpClientResponse?.browserResponse = xhr.response; 353 | if (!streamController.isClosed) { 354 | streamController.close(); 355 | } 356 | break; 357 | } 358 | }); 359 | } 360 | 361 | // ignore: unawaited_futures 362 | xhr.onTimeout.first.then((event) { 363 | if (!headersCompleter.isCompleted) { 364 | headersCompleter.completeError( 365 | TimeoutException(null, timeout), 366 | StackTrace.current, 367 | ); 368 | } 369 | if (!streamController.isClosed) { 370 | streamController.addError( 371 | TimeoutException(null, timeout), 372 | StackTrace.current, 373 | ); 374 | streamController.close(); 375 | } 376 | }); 377 | 378 | final origin = html.window.origin; 379 | // ignore: unawaited_futures 380 | xhr.onError.first.then((html.ProgressEvent event) { 381 | // The underlying XMLHttpRequest API doesn't expose any specific 382 | // information about the error itself. 383 | // 384 | // We gather the information that we have and try to produce a 385 | // descriptive exception. 386 | final error = BrowserHttpClientException( 387 | method: method, 388 | url: uriString, 389 | origin: origin, 390 | headers: headers, 391 | browserResponseType: browserResponseType, 392 | browserCredentialsMode: browserCredentialsMode, 393 | ); 394 | 395 | if (!headersCompleter.isCompleted) { 396 | headersCompleter.completeError(error, StackTrace.current); 397 | } 398 | if (!streamController.isClosed) { 399 | streamController.addError(error, StackTrace.current); 400 | streamController.close(); 401 | } 402 | }); 403 | 404 | final buffer = _buffer; 405 | if (buffer.isNotEmpty) { 406 | // Send with body 407 | xhr.send(Uint8List.fromList(buffer)); 408 | } else { 409 | // Send without body 410 | xhr.send(); 411 | } 412 | } catch (e) { 413 | // Something went wrong 414 | _completer.completeError(e); 415 | } 416 | return _completer.future; 417 | } 418 | 419 | static bool _httpMethodSupportsBody(String method) { 420 | switch (method) { 421 | case 'GET': 422 | return false; 423 | case 'HEAD': 424 | return false; 425 | case 'OPTIONS': 426 | return false; 427 | default: 428 | return true; 429 | } 430 | } 431 | 432 | @override 433 | bool browserCredentialsMode = false; 434 | } 435 | -------------------------------------------------------------------------------- /lib/src/_browser_http_client_response_impl.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 terrier989@gmail.com. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'dart:async'; 16 | import 'dart:typed_data'; 17 | 18 | import '_browser_http_client_request_impl.dart'; 19 | import '_exports_in_browser.dart'; 20 | import '_http_headers_impl.dart'; 21 | 22 | /// Used by [_BrowserHttpClient]. 23 | class BrowserHttpClientResponseImpl extends Stream> 24 | implements BrowserHttpClientResponse { 25 | @override 26 | final HttpHeaders headers = HttpHeadersImpl('1.1'); 27 | final BrowserHttpClientRequestImpl request; 28 | 29 | @override 30 | dynamic browserResponse; 31 | 32 | List? _cookies; 33 | 34 | final Stream _body; 35 | 36 | @override 37 | final String reasonPhrase; 38 | 39 | @override 40 | final int statusCode; 41 | 42 | BrowserHttpClientResponseImpl( 43 | this.request, 44 | this.statusCode, 45 | this.reasonPhrase, 46 | this._body, 47 | ); 48 | 49 | @override 50 | X509Certificate? get certificate => null; 51 | 52 | @override 53 | HttpClientResponseCompressionState get compressionState { 54 | return HttpClientResponseCompressionState.decompressed; 55 | } 56 | 57 | @override 58 | HttpConnectionInfo? get connectionInfo => null; 59 | 60 | @override 61 | int get contentLength => -1; 62 | 63 | @override 64 | List get cookies { 65 | var cookies = _cookies; 66 | if (cookies == null) { 67 | cookies = []; 68 | final headerValues = headers[HttpHeaders.setCookieHeader] ?? []; 69 | for (var headerValue in headerValues) { 70 | _cookies!.add(Cookie.fromSetCookieValue(headerValue)); 71 | } 72 | _cookies = cookies; 73 | } 74 | return cookies; 75 | } 76 | 77 | @override 78 | bool get isRedirect { 79 | if (request.method == 'GET' || request.method == 'HEAD') { 80 | return statusCode == HttpStatus.movedPermanently || 81 | statusCode == HttpStatus.permanentRedirect || 82 | statusCode == HttpStatus.found || 83 | statusCode == HttpStatus.seeOther || 84 | statusCode == HttpStatus.temporaryRedirect; 85 | } else if (request.method == 'POST') { 86 | return statusCode == HttpStatus.seeOther; 87 | } 88 | return false; 89 | } 90 | 91 | @override 92 | bool get persistentConnection => false; 93 | 94 | @override 95 | List get redirects => const []; 96 | 97 | @override 98 | Future detachSocket() { 99 | throw UnimplementedError(); 100 | } 101 | 102 | @override 103 | StreamSubscription listen(void Function(Uint8List event)? onData, 104 | {Function? onError, void Function()? onDone, bool? cancelOnError}) { 105 | return _body.listen( 106 | onData, 107 | onError: onError, 108 | onDone: onDone, 109 | cancelOnError: cancelOnError, 110 | ); 111 | } 112 | 113 | @override 114 | Future redirect( 115 | [String? method, Uri? url, bool? followLoops]) { 116 | final newUrl = url ?? Uri.parse(headers.value(HttpHeaders.locationHeader)!); 117 | return request.client 118 | .openUrl(method ?? request.method, newUrl) 119 | .then((newRequest) { 120 | request.headers.forEach((name, value) { 121 | newRequest.headers.add(name, value); 122 | }); 123 | newRequest.followRedirects = true; 124 | return newRequest.close(); 125 | }); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /lib/src/_exports_in_browser.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 terrier989@gmail.com. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | export '_exports_in_vm.dart' 16 | hide HttpClient, BytesBuilder, InternetAddress, Platform; 17 | export 'bytes_builder.dart'; 18 | export 'http_client.dart'; 19 | export 'internet_address.dart'; 20 | export 'platform.dart'; 21 | -------------------------------------------------------------------------------- /lib/src/_exports_in_nodejs.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 terrier989@gmail.com. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | export '_exports_in_vm.dart' hide BytesBuilder, InternetAddress, Platform; 16 | export 'bytes_builder.dart'; 17 | export 'internet_address.dart'; 18 | export 'platform.dart'; 19 | -------------------------------------------------------------------------------- /lib/src/_exports_in_vm.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 terrier989@gmail.com. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | export 'dart:io'; 16 | 17 | export 'browser_http_client.dart'; 18 | export 'browser_http_client_exception.dart'; 19 | export 'browser_http_client_request.dart'; 20 | export 'browser_http_client_response.dart'; 21 | export 'new_universal_http_client.dart'; 22 | -------------------------------------------------------------------------------- /lib/src/_helpers.dart: -------------------------------------------------------------------------------- 1 | export '_helpers_impl_elsewhere.dart' 2 | if (dart.library.html) '_helpers_impl_browser.dart'; 3 | -------------------------------------------------------------------------------- /lib/src/_helpers_impl_browser.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 terrier989@gmail.com. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'dart:html' as html; 16 | 17 | import 'package:universal_io/io.dart'; 18 | 19 | import '_browser_http_client_impl.dart'; 20 | 21 | String? get htmlWindowOrigin => html.window.origin; 22 | 23 | String get locale { 24 | final languages = html.window.navigator.languages; 25 | if (languages != null && languages.isNotEmpty) { 26 | return languages.first; 27 | } 28 | return 'en-US'; 29 | } 30 | 31 | String get operatingSystem { 32 | final s = html.window.navigator.userAgent.toLowerCase(); 33 | if (s.contains('iphone') || 34 | s.contains('ipad') || 35 | s.contains('ipod') || 36 | s.contains('watch os')) { 37 | return 'ios'; 38 | } 39 | if (s.contains('mac os')) { 40 | return 'macos'; 41 | } 42 | if (s.contains('fuchsia')) { 43 | return 'fuchsia'; 44 | } 45 | if (s.contains('android')) { 46 | return 'android'; 47 | } 48 | if (s.contains('linux') || s.contains('cros') || s.contains('chromebook')) { 49 | return 'linux'; 50 | } 51 | if (s.contains('windows')) { 52 | return 'windows'; 53 | } 54 | return ''; 55 | } 56 | 57 | String get operatingSystemVersion { 58 | final userAgent = html.window.navigator.userAgent; 59 | 60 | // Android? 61 | { 62 | final regExp = RegExp('Android ([a-zA-Z0-9.-_]+)'); 63 | final match = regExp.firstMatch(userAgent); 64 | if (match != null) { 65 | final version = match.group(1) ?? ''; 66 | return version; 67 | } 68 | } 69 | 70 | // iPhone OS? 71 | { 72 | final regExp = RegExp('iPhone OS ([a-zA-Z0-9.-_]+) ([a-zA-Z0-9.-_]+)'); 73 | final match = regExp.firstMatch(userAgent); 74 | if (match != null) { 75 | final version = (match.group(2) ?? '').replaceAll('_', '.'); 76 | return version; 77 | } 78 | } 79 | 80 | // Mac OS X? 81 | { 82 | final regExp = RegExp('Mac OS X ([a-zA-Z0-9.-_]+)'); 83 | final match = regExp.firstMatch(userAgent); 84 | if (match != null) { 85 | final version = (match.group(1) ?? '').replaceAll('_', '.'); 86 | return version; 87 | } 88 | } 89 | 90 | // Chrome OS? 91 | { 92 | final regExp = RegExp('CrOS ([a-zA-Z0-9.-_]+) ([a-zA-Z0-9.-_]+)'); 93 | final match = regExp.firstMatch(userAgent); 94 | if (match != null) { 95 | final version = match.group(2) ?? ''; 96 | return version; 97 | } 98 | } 99 | 100 | // Windows NT? 101 | { 102 | final regExp = RegExp('Windows NT ([a-zA-Z0-9.-_]+)'); 103 | final match = regExp.firstMatch(userAgent); 104 | if (match != null) { 105 | final version = (match.group(1) ?? ''); 106 | return version; 107 | } 108 | } 109 | 110 | return ''; 111 | } 112 | 113 | HttpClient newHttpClient() => BrowserHttpClientImpl(); 114 | -------------------------------------------------------------------------------- /lib/src/_helpers_impl_elsewhere.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 terrier989@gmail.com. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'dart:io'; 16 | 17 | String? get htmlWindowOrigin => null; 18 | 19 | String get locale { 20 | return 'en-US'; 21 | } 22 | 23 | String get operatingSystem { 24 | return 'linux'; 25 | } 26 | 27 | String get operatingSystemVersion { 28 | return ''; 29 | } 30 | 31 | HttpClient newHttpClient() => HttpClient(); 32 | -------------------------------------------------------------------------------- /lib/src/_http_headers_impl.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 terrier989@gmail.com. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // ------------------------------------------------------------------ 16 | // THIS FILE WAS DERIVED FROM SOURCE CODE UNDER THE FOLLOWING LICENSE 17 | // ------------------------------------------------------------------ 18 | // 19 | // Copyright 2012, the Dart project authors. All rights reserved. 20 | // Redistribution and use in source and binary forms, with or without 21 | // modification, are permitted provided that the following conditions are 22 | // met: 23 | // * Redistributions of source code must retain the above copyright 24 | // notice, this list of conditions and the following disclaimer. 25 | // * Redistributions in binary form must reproduce the above 26 | // copyright notice, this list of conditions and the following 27 | // disclaimer in the documentation and/or other materials provided 28 | // with the distribution. 29 | // * Neither the name of Google Inc. nor the names of its 30 | // contributors may be used to endorse or promote products derived 31 | // from this software without specific prior written permission. 32 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 33 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 34 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 35 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 36 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 37 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 38 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 39 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 40 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 41 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 42 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 43 | 44 | import 'dart:collection'; 45 | 46 | import '_exports_in_browser.dart'; 47 | 48 | final _digitsValidator = RegExp(r"^\d+$"); 49 | 50 | class HttpHeadersImpl implements HttpHeaders { 51 | final Map> _headers; 52 | // The original header names keyed by the lowercase header names. 53 | Map? _originalHeaderNames; 54 | final String protocolVersion; 55 | 56 | bool _mutable = true; // Are the headers currently mutable? 57 | List? _noFoldingHeaders; 58 | 59 | int _contentLength = -1; 60 | bool _persistentConnection = true; 61 | bool _chunkedTransferEncoding = false; 62 | String? _host; 63 | int? _port; 64 | 65 | final int _defaultPortForScheme; 66 | 67 | HttpHeadersImpl(this.protocolVersion, 68 | {int defaultPortForScheme = HttpClient.defaultHttpPort, 69 | HttpHeadersImpl? initialHeaders}) 70 | : _headers = HashMap>(), 71 | _defaultPortForScheme = defaultPortForScheme { 72 | if (initialHeaders != null) { 73 | initialHeaders._headers.forEach((name, value) => _headers[name] = value); 74 | _contentLength = initialHeaders._contentLength; 75 | _persistentConnection = initialHeaders._persistentConnection; 76 | _chunkedTransferEncoding = initialHeaders._chunkedTransferEncoding; 77 | _host = initialHeaders._host; 78 | _port = initialHeaders._port; 79 | } 80 | if (protocolVersion == "1.0") { 81 | _persistentConnection = false; 82 | _chunkedTransferEncoding = false; 83 | } 84 | } 85 | 86 | @override 87 | bool get chunkedTransferEncoding => _chunkedTransferEncoding; 88 | 89 | @override 90 | set chunkedTransferEncoding(bool chunkedTransferEncoding) { 91 | _checkMutable(); 92 | if (chunkedTransferEncoding && protocolVersion == "1.0") { 93 | throw HttpException( 94 | "Trying to set 'Transfer-Encoding: Chunked' on HTTP 1.0 headers"); 95 | } 96 | if (chunkedTransferEncoding == _chunkedTransferEncoding) return; 97 | if (chunkedTransferEncoding) { 98 | List? values = _headers[HttpHeaders.transferEncodingHeader]; 99 | if (values == null || !values.contains("chunked")) { 100 | // Headers does not specify chunked encoding - add it if set. 101 | _addValue(HttpHeaders.transferEncodingHeader, "chunked"); 102 | } 103 | contentLength = -1; 104 | } else { 105 | // Headers does specify chunked encoding - remove it if not set. 106 | remove(HttpHeaders.transferEncodingHeader, "chunked"); 107 | } 108 | _chunkedTransferEncoding = chunkedTransferEncoding; 109 | } 110 | 111 | @override 112 | int get contentLength => _contentLength; 113 | 114 | @override 115 | set contentLength(int contentLength) { 116 | _checkMutable(); 117 | if (protocolVersion == "1.0" && 118 | persistentConnection && 119 | contentLength == -1) { 120 | throw HttpException( 121 | "Trying to clear ContentLength on HTTP 1.0 headers with " 122 | "'Connection: Keep-Alive' set"); 123 | } 124 | if (_contentLength == contentLength) return; 125 | _contentLength = contentLength; 126 | if (_contentLength >= 0) { 127 | if (chunkedTransferEncoding) chunkedTransferEncoding = false; 128 | _set(HttpHeaders.contentLengthHeader, contentLength.toString()); 129 | } else { 130 | _headers.remove(HttpHeaders.contentLengthHeader); 131 | if (protocolVersion == "1.1") { 132 | chunkedTransferEncoding = true; 133 | } 134 | } 135 | } 136 | 137 | @override 138 | ContentType? get contentType { 139 | var values = _headers[HttpHeaders.contentTypeHeader]; 140 | if (values != null) { 141 | return ContentType.parse(values[0]); 142 | } else { 143 | return null; 144 | } 145 | } 146 | 147 | @override 148 | set contentType(ContentType? contentType) { 149 | _checkMutable(); 150 | if (contentType == null) { 151 | _headers.remove(HttpHeaders.contentTypeHeader); 152 | } else { 153 | _set(HttpHeaders.contentTypeHeader, contentType.toString()); 154 | } 155 | } 156 | 157 | @override 158 | DateTime? get date { 159 | List? values = _headers[HttpHeaders.dateHeader]; 160 | if (values != null) { 161 | assert(values.isNotEmpty); 162 | try { 163 | return HttpDate.parse(values[0]); 164 | } on Exception { 165 | return null; 166 | } 167 | } 168 | return null; 169 | } 170 | 171 | @override 172 | set date(DateTime? date) { 173 | _checkMutable(); 174 | if (date == null) { 175 | _headers.remove(HttpHeaders.dateHeader); 176 | } else { 177 | // Format "DateTime" header with date in Greenwich Mean Time (GMT). 178 | String formatted = HttpDate.format(date.toUtc()); 179 | _set(HttpHeaders.dateHeader, formatted); 180 | } 181 | } 182 | 183 | @override 184 | DateTime? get expires { 185 | List? values = _headers[HttpHeaders.expiresHeader]; 186 | if (values != null) { 187 | assert(values.isNotEmpty); 188 | try { 189 | return HttpDate.parse(values[0]); 190 | } on Exception { 191 | return null; 192 | } 193 | } 194 | return null; 195 | } 196 | 197 | @override 198 | set expires(DateTime? expires) { 199 | _checkMutable(); 200 | if (expires == null) { 201 | _headers.remove(HttpHeaders.expiresHeader); 202 | } else { 203 | // Format "Expires" header with date in Greenwich Mean Time (GMT). 204 | String formatted = HttpDate.format(expires.toUtc()); 205 | _set(HttpHeaders.expiresHeader, formatted); 206 | } 207 | } 208 | 209 | @override 210 | String? get host => _host; 211 | 212 | @override 213 | set host(String? host) { 214 | _checkMutable(); 215 | _host = host; 216 | _updateHostHeader(); 217 | } 218 | 219 | @override 220 | DateTime? get ifModifiedSince { 221 | List? values = _headers[HttpHeaders.ifModifiedSinceHeader]; 222 | if (values != null) { 223 | assert(values.isNotEmpty); 224 | try { 225 | return HttpDate.parse(values[0]); 226 | } on Exception { 227 | return null; 228 | } 229 | } 230 | return null; 231 | } 232 | 233 | @override 234 | set ifModifiedSince(DateTime? ifModifiedSince) { 235 | _checkMutable(); 236 | if (ifModifiedSince == null) { 237 | _headers.remove(HttpHeaders.ifModifiedSinceHeader); 238 | } else { 239 | // Format "ifModifiedSince" header with date in Greenwich Mean Time (GMT). 240 | String formatted = HttpDate.format(ifModifiedSince.toUtc()); 241 | _set(HttpHeaders.ifModifiedSinceHeader, formatted); 242 | } 243 | } 244 | 245 | @override 246 | bool get persistentConnection => _persistentConnection; 247 | 248 | @override 249 | set persistentConnection(bool persistentConnection) { 250 | _checkMutable(); 251 | if (persistentConnection == _persistentConnection) return; 252 | final originalName = _originalHeaderName(HttpHeaders.connectionHeader); 253 | if (persistentConnection) { 254 | if (protocolVersion == "1.1") { 255 | remove(HttpHeaders.connectionHeader, "close"); 256 | } else { 257 | if (_contentLength < 0) { 258 | throw HttpException( 259 | "Trying to set 'Connection: Keep-Alive' on HTTP 1.0 headers with " 260 | "no ContentLength"); 261 | } 262 | add(originalName, "keep-alive", preserveHeaderCase: true); 263 | } 264 | } else { 265 | if (protocolVersion == "1.1") { 266 | add(originalName, "close", preserveHeaderCase: true); 267 | } else { 268 | remove(HttpHeaders.connectionHeader, "keep-alive"); 269 | } 270 | } 271 | _persistentConnection = persistentConnection; 272 | } 273 | 274 | @override 275 | int? get port => _port; 276 | 277 | @override 278 | set port(int? port) { 279 | _checkMutable(); 280 | _port = port; 281 | _updateHostHeader(); 282 | } 283 | 284 | @override 285 | List? operator [](String name) => _headers[_validateField(name)]; 286 | 287 | @override 288 | void add(String name, value, {bool preserveHeaderCase = false}) { 289 | _checkMutable(); 290 | String lowercaseName = _validateField(name); 291 | 292 | if (preserveHeaderCase && name != lowercaseName) { 293 | (_originalHeaderNames ??= {})[lowercaseName] = name; 294 | } else { 295 | _originalHeaderNames?.remove(lowercaseName); 296 | } 297 | _addAll(lowercaseName, value); 298 | } 299 | 300 | void build(BytesBuilder builder, {bool skipZeroContentLength = false}) { 301 | // per https://tools.ietf.org/html/rfc7230#section-3.3.2 302 | // A user agent SHOULD NOT send a 303 | // Content-Length header field when the request message does not 304 | // contain a payload body and the method semantics do not anticipate 305 | // such a body. 306 | String? ignoreHeader = _contentLength == 0 && skipZeroContentLength 307 | ? HttpHeaders.contentLengthHeader 308 | : null; 309 | _headers.forEach((String name, List values) { 310 | if (ignoreHeader == name) { 311 | return; 312 | } 313 | String originalName = _originalHeaderName(name); 314 | bool fold = _foldHeader(name); 315 | var nameData = originalName.codeUnits; 316 | builder.add(nameData); 317 | builder.addByte(_CharCode.colon); 318 | builder.addByte(_CharCode.sp); 319 | for (int i = 0; i < values.length; i++) { 320 | if (i > 0) { 321 | if (fold) { 322 | builder.addByte(_CharCode.comma); 323 | builder.addByte(_CharCode.sp); 324 | } else { 325 | builder.addByte(_CharCode.cr); 326 | builder.addByte(_CharCode.lf); 327 | builder.add(nameData); 328 | builder.addByte(_CharCode.colon); 329 | builder.addByte(_CharCode.sp); 330 | } 331 | } 332 | builder.add(values[i].codeUnits); 333 | } 334 | builder.addByte(_CharCode.cr); 335 | builder.addByte(_CharCode.lf); 336 | }); 337 | } 338 | 339 | @override 340 | void clear() { 341 | _checkMutable(); 342 | _headers.clear(); 343 | _contentLength = -1; 344 | _persistentConnection = true; 345 | _chunkedTransferEncoding = false; 346 | _host = null; 347 | _port = null; 348 | } 349 | 350 | void finalize() { 351 | _mutable = false; 352 | } 353 | 354 | @override 355 | void forEach(void Function(String name, List values) action) { 356 | _headers.forEach((String name, List values) { 357 | String originalName = _originalHeaderName(name); 358 | action(originalName, values); 359 | }); 360 | } 361 | 362 | @override 363 | void noFolding(String name) { 364 | name = _validateField(name); 365 | (_noFoldingHeaders ??= []).add(name); 366 | } 367 | 368 | @override 369 | void remove(String name, Object value) { 370 | _checkMutable(); 371 | name = _validateField(name); 372 | value = _validateValue(value); 373 | List? values = _headers[name]; 374 | if (values != null) { 375 | values.remove(_valueToString(value)); 376 | if (values.isEmpty) { 377 | _headers.remove(name); 378 | _originalHeaderNames?.remove(name); 379 | } 380 | } 381 | if (name == HttpHeaders.transferEncodingHeader && value == "chunked") { 382 | _chunkedTransferEncoding = false; 383 | } 384 | } 385 | 386 | @override 387 | void removeAll(String name) { 388 | _checkMutable(); 389 | name = _validateField(name); 390 | _headers.remove(name); 391 | _originalHeaderNames?.remove(name); 392 | } 393 | 394 | @override 395 | void set(String name, Object value, {bool preserveHeaderCase = false}) { 396 | _checkMutable(); 397 | String lowercaseName = _validateField(name); 398 | _headers.remove(lowercaseName); 399 | _originalHeaderNames?.remove(lowercaseName); 400 | if (lowercaseName == HttpHeaders.contentLengthHeader) { 401 | _contentLength = -1; 402 | } 403 | if (lowercaseName == HttpHeaders.transferEncodingHeader) { 404 | _chunkedTransferEncoding = false; 405 | } 406 | if (preserveHeaderCase && name != lowercaseName) { 407 | (_originalHeaderNames ??= {})[lowercaseName] = name; 408 | } 409 | _addAll(lowercaseName, value); 410 | } 411 | 412 | @override 413 | String toString() { 414 | final sb = StringBuffer(); 415 | _headers.forEach((String name, List values) { 416 | String originalName = _originalHeaderName(name); 417 | sb 418 | ..write(originalName) 419 | ..write(": "); 420 | bool fold = _foldHeader(name); 421 | for (int i = 0; i < values.length; i++) { 422 | if (i > 0) { 423 | if (fold) { 424 | sb.write(", "); 425 | } else { 426 | sb 427 | ..write("\n") 428 | ..write(originalName) 429 | ..write(": "); 430 | } 431 | } 432 | sb.write(values[i]); 433 | } 434 | sb.write("\n"); 435 | }); 436 | return sb.toString(); 437 | } 438 | 439 | @override 440 | String? value(String name) { 441 | name = _validateField(name); 442 | List? values = _headers[name]; 443 | if (values == null) return null; 444 | assert(values.isNotEmpty); 445 | if (values.length > 1) { 446 | throw HttpException("More than one value for header $name"); 447 | } 448 | return values[0]; 449 | } 450 | 451 | // [name] must be a lower-case version of the name. 452 | void _add(String name, value) { 453 | assert(name == _validateField(name)); 454 | // Use the length as index on what method to call. This is notable 455 | // faster than computing hash and looking up in a hash-map. 456 | switch (name.length) { 457 | case 4: 458 | if (HttpHeaders.dateHeader == name) { 459 | _addDate(name, value); 460 | return; 461 | } 462 | if (HttpHeaders.hostHeader == name) { 463 | _addHost(name, value); 464 | return; 465 | } 466 | break; 467 | case 7: 468 | if (HttpHeaders.expiresHeader == name) { 469 | _addExpires(name, value); 470 | return; 471 | } 472 | break; 473 | case 10: 474 | if (HttpHeaders.connectionHeader == name) { 475 | _addConnection(name, value); 476 | return; 477 | } 478 | break; 479 | case 12: 480 | if (HttpHeaders.contentTypeHeader == name) { 481 | _addContentType(name, value); 482 | return; 483 | } 484 | break; 485 | case 14: 486 | if (HttpHeaders.contentLengthHeader == name) { 487 | _addContentLength(name, value); 488 | return; 489 | } 490 | break; 491 | case 17: 492 | if (HttpHeaders.transferEncodingHeader == name) { 493 | _addTransferEncoding(name, value); 494 | return; 495 | } 496 | if (HttpHeaders.ifModifiedSinceHeader == name) { 497 | _addIfModifiedSince(name, value); 498 | return; 499 | } 500 | } 501 | _addValue(name, value); 502 | } 503 | 504 | void _addAll(String name, value) { 505 | if (value is Iterable) { 506 | for (var v in value) { 507 | _add(name, _validateValue(v)); 508 | } 509 | } else { 510 | _add(name, _validateValue(value)); 511 | } 512 | } 513 | 514 | void _addConnection(String name, String value) { 515 | var lowerCaseValue = value.toLowerCase(); 516 | if (lowerCaseValue == 'close') { 517 | _persistentConnection = false; 518 | } else if (lowerCaseValue == 'keep-alive') { 519 | _persistentConnection = true; 520 | } 521 | _addValue(name, value); 522 | } 523 | 524 | void _addContentLength(String name, value) { 525 | if (value is int) { 526 | if (value < 0) { 527 | throw HttpException("Content-Length must contain only digits"); 528 | } 529 | } else if (value is String) { 530 | if (!_digitsValidator.hasMatch(value)) { 531 | throw HttpException("Content-Length must contain only digits"); 532 | } 533 | value = int.parse(value); 534 | } else { 535 | throw HttpException("Unexpected type for header named $name"); 536 | } 537 | contentLength = value; 538 | } 539 | 540 | void _addContentType(String name, value) { 541 | _set(HttpHeaders.contentTypeHeader, value); 542 | } 543 | 544 | void _addDate(String name, value) { 545 | if (value is DateTime) { 546 | date = value; 547 | } else if (value is String) { 548 | _set(HttpHeaders.dateHeader, value); 549 | } else { 550 | throw HttpException("Unexpected type for header named $name"); 551 | } 552 | } 553 | 554 | void _addExpires(String name, value) { 555 | if (value is DateTime) { 556 | expires = value; 557 | } else if (value is String) { 558 | _set(HttpHeaders.expiresHeader, value); 559 | } else { 560 | throw HttpException("Unexpected type for header named $name"); 561 | } 562 | } 563 | 564 | void _addHost(String name, value) { 565 | if (value is String) { 566 | // value.indexOf will only work for ipv4, ipv6 which has multiple : in its 567 | // host part needs lastIndexOf 568 | int pos = value.lastIndexOf(":"); 569 | // According to RFC 3986, section 3.2.2, host part of ipv6 address must be 570 | // enclosed by square brackets. 571 | // https://serverfault.com/questions/205793/how-can-one-distinguish-the-host-and-the-port-in-an-ipv6-url 572 | if (pos == -1 || value.startsWith("[") && value.endsWith("]")) { 573 | _host = value; 574 | _port = HttpClient.defaultHttpPort; 575 | } else { 576 | if (pos > 0) { 577 | _host = value.substring(0, pos); 578 | } else { 579 | _host = null; 580 | } 581 | if (pos + 1 == value.length) { 582 | _port = HttpClient.defaultHttpPort; 583 | } else { 584 | try { 585 | _port = int.parse(value.substring(pos + 1)); 586 | } on FormatException { 587 | _port = null; 588 | } 589 | } 590 | } 591 | _set(HttpHeaders.hostHeader, value); 592 | } else { 593 | throw HttpException("Unexpected type for header named $name"); 594 | } 595 | } 596 | 597 | void _addIfModifiedSince(String name, value) { 598 | if (value is DateTime) { 599 | ifModifiedSince = value; 600 | } else if (value is String) { 601 | _set(HttpHeaders.ifModifiedSinceHeader, value); 602 | } else { 603 | throw HttpException("Unexpected type for header named $name"); 604 | } 605 | } 606 | 607 | void _addTransferEncoding(String name, value) { 608 | if (value == "chunked") { 609 | chunkedTransferEncoding = true; 610 | } else { 611 | _addValue(HttpHeaders.transferEncodingHeader, value); 612 | } 613 | } 614 | 615 | void _addValue(String name, Object value) { 616 | List values = (_headers[name] ??= []); 617 | values.add(_valueToString(value)); 618 | } 619 | 620 | void _checkMutable() { 621 | if (!_mutable) throw HttpException("HTTP headers are not mutable"); 622 | } 623 | 624 | bool _foldHeader(String name) { 625 | if (name == HttpHeaders.setCookieHeader) return false; 626 | var noFoldingHeaders = _noFoldingHeaders; 627 | return noFoldingHeaders == null || !noFoldingHeaders.contains(name); 628 | } 629 | 630 | String _originalHeaderName(String name) { 631 | return _originalHeaderNames?[name] ?? name; 632 | } 633 | 634 | void _set(String name, String value) { 635 | assert(name == _validateField(name)); 636 | _headers[name] = [value]; 637 | } 638 | 639 | void _updateHostHeader() { 640 | var host = _host; 641 | if (host != null) { 642 | bool defaultPort = _port == null || _port == _defaultPortForScheme; 643 | _set("host", defaultPort ? host : "$host:$_port"); 644 | } 645 | } 646 | 647 | String _valueToString(Object value) { 648 | if (value is DateTime) { 649 | return HttpDate.format(value); 650 | } else if (value is String) { 651 | return value; // TODO(39784): no _validateValue? 652 | } else { 653 | return _validateValue(value.toString()) as String; 654 | } 655 | } 656 | 657 | static String _validateField(String field) { 658 | return field.toLowerCase(); 659 | } 660 | 661 | static Object _validateValue(Object value) { 662 | if (value is! String) return value; 663 | return value; 664 | } 665 | } 666 | 667 | // Frequently used character codes. 668 | class _CharCode { 669 | static const int lf = 10; 670 | static const int cr = 13; 671 | static const int sp = 32; 672 | static const int comma = 44; 673 | static const int colon = 58; 674 | } 675 | -------------------------------------------------------------------------------- /lib/src/_io_sink_base.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 terrier989@gmail.com. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'dart:async'; 16 | import 'dart:convert'; 17 | 18 | import 'package:universal_io/io.dart'; 19 | 20 | /// A base class for [IOSink] subclasses. 21 | abstract class IOSinkBase implements IOSink { 22 | @override 23 | Encoding encoding = utf8; 24 | 25 | @override 26 | Future addStream(Stream> stream) { 27 | return stream.listen((data) { 28 | add(data); 29 | }, onError: (error, stackTrace) { 30 | addError(error, stackTrace); 31 | }, cancelOnError: true).asFuture(); 32 | } 33 | 34 | @override 35 | Future flush() { 36 | return Future.value(null); 37 | } 38 | 39 | @override 40 | void write(Object? obj) { 41 | add(const Utf8Encoder().convert(obj.toString())); 42 | } 43 | 44 | @override 45 | void writeAll(Iterable objects, [String separator = '']) { 46 | var isFirst = true; 47 | for (var object in objects) { 48 | if (isFirst) { 49 | isFirst = false; 50 | } else { 51 | write(separator); 52 | } 53 | write(object); 54 | } 55 | } 56 | 57 | @override 58 | void writeCharCode(int charCode) { 59 | write(String.fromCharCode(charCode)); 60 | } 61 | 62 | @override 63 | void writeln([Object? obj = '']) { 64 | if (obj != '') { 65 | write(obj); 66 | } 67 | write('\n'); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lib/src/browser_http_client.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 terrier989@gmail.com. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'dart:async'; 16 | 17 | import 'package:universal_io/io.dart'; 18 | 19 | /// Implemented by [HttpClient] when the application runs in browser. 20 | abstract class BrowserHttpClient implements HttpClient { 21 | /// HTTP request header "Accept" MIMEs that will cause XMLHttpRequest 22 | /// to use request type "text", which also makes it possible to read the 23 | /// HTTP response body progressively in chunks. 24 | static const Set defaultTextMimes = { 25 | 'application/grpc-web-text', 26 | 'application/grpc-web-text+proto', 27 | 'text/*', 28 | }; 29 | 30 | /// Enables you to set [BrowserHttpClientRequest.browserRequestType] before 31 | /// any _XHR_ request is sent to the server. 32 | FutureOr Function(BrowserHttpClientRequest request)? 33 | onBrowserHttpClientRequestClose; 34 | 35 | /// Enables [CORS "credentials mode"](https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials) 36 | /// for all _XHR_ requests. Disabled by default. 37 | /// 38 | /// "Credentials mode" causes cookies and other credentials to be sent and 39 | /// received. It has complicated implications for CORS headers required from 40 | /// the server. 41 | /// 42 | /// # Example 43 | /// ``` 44 | /// Future main() async { 45 | /// final client = HttpClient(); 46 | /// if (client is BrowserHttpClient) { 47 | /// client.browserCredentialsMode = true; 48 | /// } 49 | /// // ... 50 | /// } 51 | /// ``` 52 | bool browserCredentialsMode = false; 53 | 54 | BrowserHttpClient.constructor(); 55 | } 56 | -------------------------------------------------------------------------------- /lib/src/browser_http_client_exception.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 terrier989@gmail.com. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:universal_io/io.dart'; 16 | 17 | import '_helpers.dart' as helpers; 18 | 19 | /// May be thrown by [BrowserHttpClientRequest.close]. 20 | class BrowserHttpClientException implements SocketException { 21 | /// Can be used to disable verbose messages in development mode. 22 | static bool verbose = true; 23 | 24 | static final Set _corsSimpleMethods = const { 25 | 'GET', 26 | 'HEAD', 27 | 'POST', 28 | }; 29 | 30 | /// HTTP method ("GET, "POST, etc.) 31 | final String method; 32 | 33 | /// URL of the HTTP request. 34 | final String url; 35 | 36 | /// Origin of the HTTP request. 37 | final String? origin; 38 | 39 | /// HTTP headers 40 | final HttpHeaders headers; 41 | 42 | /// Browser "credentials mode". 43 | final bool browserCredentialsMode; 44 | 45 | /// Browser response type. 46 | final String browserResponseType; 47 | 48 | @override 49 | final OSError? osError = null; 50 | 51 | @override 52 | final InternetAddress? address = null; 53 | 54 | @override 55 | final int? port = null; 56 | 57 | BrowserHttpClientException({ 58 | required this.method, 59 | required this.url, 60 | required this.origin, 61 | required this.headers, 62 | required this.browserCredentialsMode, 63 | required this.browserResponseType, 64 | }); 65 | 66 | @override 67 | String get message => 'XMLHttpRequest (XHR) error'; 68 | 69 | @override 70 | String toString() { 71 | final sb = StringBuffer(); 72 | sb.write('XMLHttpRequest (XHR) error.'); 73 | if (verbose) { 74 | assert(() { 75 | sb.write('\n'); 76 | for (var i = 0; i < 80; i++) { 77 | sb.write('-'); 78 | } 79 | sb.write('\n'); 80 | 81 | // Write key details 82 | void addEntry(String key, String? value) { 83 | sb.write(key.padRight(30)); 84 | sb.write(value); 85 | sb.write('\n'); 86 | } 87 | 88 | final parsedUrl = Uri.parse(url); 89 | final isCrossOrigin = parsedUrl.origin != helpers.htmlWindowOrigin; 90 | addEntry('Request method: ', method); 91 | addEntry('Request URL: ', url); 92 | addEntry('Origin: ', origin); 93 | addEntry('Cross-origin: ', '$isCrossOrigin'); 94 | addEntry('browserCredentialsMode: ', '$browserCredentialsMode'); 95 | addEntry('browserResponseType: ', browserResponseType); 96 | sb.write( 97 | ''' 98 | 99 | THE REASON FOR THE XHR ERROR IS UNKNOWN. 100 | (For security reasons, browsers do not explain XHR errors.) 101 | 102 | Is the server down? Did the server have an internal error? 103 | 104 | ''', 105 | ); 106 | 107 | // Warn about possible problem with missing CORS headers 108 | if (isCrossOrigin) { 109 | // List of header name that the server may need to whitelist 110 | final sortedHeaderNames = []; 111 | headers.forEach((name, values) { 112 | sortedHeaderNames.add(name); 113 | }); 114 | sortedHeaderNames.sort(); 115 | if (browserCredentialsMode) { 116 | if (method != 'HEAD' && method != 'GET') { 117 | sb.write( 118 | 'Did the server respond to a cross-origin "preflight" (OPTIONS) request?\n' 119 | '\n', 120 | ); 121 | } 122 | sb.write( 123 | 'Did the server respond with the following headers?\n' 124 | ' * Access-Control-Allow-Credentials: true\n' 125 | ' * Alternatively, disable "credentials mode".\n' 126 | ' * Access-Control-Allow-Origin: $origin\n' 127 | ' * In credentials mode, wildcard ("*") would not work!\n' 128 | ' * Access-Control-Allow-Methods: $method\n' 129 | ' * In credentials mode, wildcard ("*") would not work!\n', 130 | ); 131 | if (sortedHeaderNames.isNotEmpty) { 132 | final joinedHeaderNames = sortedHeaderNames.join(', '); 133 | sb.write( 134 | ' * Access-Control-Allow-Headers: $joinedHeaderNames\n' 135 | ' * In credentials mode, wildcard ("*") would not work!\n', 136 | ); 137 | } 138 | } else { 139 | sb.write(""" 140 | Enabling credentials mode would enable use of some HTTP headers in both the 141 | request and the response. For example, credentials mode is required for 142 | sending/receiving cookies. If you think you need to enable 'credentials mode', 143 | do the following: 144 | 145 | final httpClientRequest = ...; 146 | if (httpClientRequest is BrowserHttpClientRequest) { 147 | httpClientRequest.browserCredentialsMode = true; 148 | } 149 | 150 | """); 151 | if (method != 'HEAD' && method != 'GET') { 152 | sb.write( 153 | 'Did the server respond to a cross-origin "preflight" (OPTIONS) request?\n' 154 | '\n', 155 | ); 156 | } 157 | sb.write( 158 | 'Did the server respond with the following headers?\n' 159 | ' * Access-Control-Allow-Origin: $origin\n' 160 | ' * You can also use wildcard ("*").\n' 161 | ' * Always required for cross-origin requests!\n', 162 | ); 163 | if (!_corsSimpleMethods.contains(method)) { 164 | sb.write( 165 | ' * Access-Control-Allow-Methods: $method\n' 166 | ' * You can also use wildcard ("*").\n', 167 | ); 168 | } 169 | 170 | if (sortedHeaderNames.isNotEmpty) { 171 | final joinedHeaderNames = sortedHeaderNames.join(', '); 172 | sb.write( 173 | ' * Access-Control-Allow-Headers: $joinedHeaderNames\n' 174 | ' * You can also use wildcard ("*").\n', 175 | ); 176 | } 177 | } 178 | } 179 | sb.write( 180 | '\n' 181 | 'Want shorter error messages? Set the following static field:\n' 182 | ' BrowserHttpException.verbose = false;\n', 183 | ); 184 | // Write a line 185 | for (var i = 0; i < 80; i++) { 186 | sb.write('-'); 187 | } 188 | sb.write('\n'); 189 | return true; 190 | }()); 191 | } 192 | return sb.toString(); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /lib/src/browser_http_client_request.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 terrier989@gmail.com. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:universal_io/io.dart'; 16 | 17 | /// Implemented by [HttpClientRequest] when the application runs in browser. 18 | abstract class BrowserHttpClientRequest extends HttpClientRequest { 19 | /// Sets _responseType_ in XMLHttpRequest for this _XHR_ request. 20 | /// 21 | /// # Possible values 22 | /// * "arraybuffer" or `null` (default) 23 | /// * "json" 24 | /// * "text" (makes streaming possible) 25 | /// 26 | String? browserResponseType; 27 | 28 | /// Enables ["credentials mode"](https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials) 29 | /// for this _XHR_ request. Disabled by default. 30 | /// 31 | /// "Credentials mode" causes cookies and other credentials to be sent and 32 | /// received. It has complicated implications for CORS headers required from 33 | /// the server. 34 | /// 35 | /// # Example 36 | /// ``` 37 | /// Future main() async { 38 | /// final client = HttpClient(); 39 | /// final request = client.getUrl(Url.parse('http://host/path')); 40 | /// if (request is BrowserHttpClientRequest) { 41 | /// request.browserCredentialsMode = true; 42 | /// } 43 | /// final response = await request.close(); 44 | /// // ... 45 | /// } 46 | /// ``` 47 | bool browserCredentialsMode = false; 48 | } 49 | -------------------------------------------------------------------------------- /lib/src/browser_http_client_response.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 terrier989@gmail.com. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:universal_io/io.dart'; 16 | 17 | /// Implemented by [HttpClientResponse] when the application runs in browser. 18 | abstract class BrowserHttpClientResponse extends HttpClientResponse { 19 | /// Response object of _XHR_ request. 20 | /// 21 | /// You need to finish reading this [HttpClientResponse] to get the final 22 | /// value. 23 | dynamic get browserResponse; 24 | } 25 | -------------------------------------------------------------------------------- /lib/src/bytes_builder.dart: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------ 2 | // THIS FILE WAS DERIVED FROM SOURCE CODE UNDER THE FOLLOWING LICENSE 3 | // ------------------------------------------------------------------ 4 | // 5 | // Copyright 2012, the Dart project authors. All rights reserved. 6 | // Redistribution and use in source and binary forms, with or without 7 | // modification, are permitted provided that the following conditions are 8 | // met: 9 | // * Redistributions of source code must retain the above copyright 10 | // notice, this list of conditions and the following disclaimer. 11 | // * Redistributions in binary form must reproduce the above 12 | // copyright notice, this list of conditions and the following 13 | // disclaimer in the documentation and/or other materials provided 14 | // with the distribution. 15 | // * Neither the name of Google Inc. nor the names of its 16 | // contributors may be used to endorse or promote products derived 17 | // from this software without specific prior written permission. 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | // 30 | // --------------------------------------------------------- 31 | // THIS, DERIVED FILE IS LICENSE UNDER THE FOLLOWING LICENSE 32 | // --------------------------------------------------------- 33 | // Copyright 2020 terrier989@gmail.com. 34 | // 35 | // Licensed under the Apache License, Version 2.0 (the "License"); 36 | // you may not use this file except in compliance with the License. 37 | // You may obtain a copy of the License at 38 | // 39 | // http://www.apache.org/licenses/LICENSE-2.0 40 | // 41 | // Unless required by applicable law or agreed to in writing, software 42 | // distributed under the License is distributed on an "AS IS" BASIS, 43 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 44 | // See the License for the specific language governing permissions and 45 | // limitations under the License. 46 | 47 | import 'dart:typed_data'; 48 | 49 | /// Builds a list of bytes, allowing bytes and lists of bytes to be added at the 50 | /// end. 51 | /// 52 | /// Used to efficiently collect bytes and lists of bytes. 53 | class BytesBuilder { 54 | final bool _copy; 55 | int _length = 0; 56 | final List> _chunks = []; 57 | 58 | /// Construct a new empty [BytesBuilder]. 59 | /// 60 | /// If [copy] is true (the default), the created builder is a *copying* 61 | /// builder. A copying builder maintains its own internal buffer and copies 62 | /// the bytes added to it eagerly. 63 | /// 64 | /// If [copy] set to false, the created builder assumes that lists added 65 | /// to it will not change. 66 | /// Any [Uint8List] added using [add] is kept until 67 | /// [toBytes] or [takeBytes] is called, 68 | /// and only then are their contents copied. 69 | /// A non-[Uint8List] may be copied eagerly. 70 | /// If only a single [Uint8List] is added to the builder, 71 | /// that list is returned by [toBytes] or [takeBytes] directly, without any copying. 72 | /// A list added to a non-copying builder *should not* change its content 73 | /// after being added, and it *must not* change its length after being added. 74 | /// (Normal [Uint8List]s are fixed length lists, but growing lists implementing 75 | /// [Uint8List] exist.) 76 | factory BytesBuilder({bool copy = true}) { 77 | return BytesBuilder._(copy: copy); 78 | } 79 | 80 | BytesBuilder._({bool copy = true}) : _copy = copy; 81 | 82 | /// Whether the buffer is empty. 83 | bool get isEmpty => length == 0; 84 | 85 | /// Whether the buffer is non-empty. 86 | bool get isNotEmpty => length != 0; 87 | 88 | /// The number of bytes in this builder. 89 | int get length => _length; 90 | 91 | /// Appends [bytes] to the current contents of this builder. 92 | /// 93 | /// Each value of [bytes] will be truncated 94 | /// to an 8-bit value in the range 0 .. 255. 95 | void add(List bytes) { 96 | _length += bytes.length; 97 | if (_copy) { 98 | final copy = Uint8List(bytes.length); 99 | copy.setRange(0, bytes.length, bytes); 100 | _chunks.add(copy); 101 | } else { 102 | _chunks.add(bytes); 103 | } 104 | } 105 | 106 | /// Appends [byte] to the current contents of this builder. 107 | /// 108 | /// The [byte] will be truncated to an 8-bit value in the range 0 .. 255. 109 | void addByte(int byte) { 110 | add([byte]); 111 | } 112 | 113 | /// Clears the contents of this builder. 114 | /// 115 | /// The current contents are discarded and this builder becomes empty. 116 | void clear() { 117 | _length = 0; 118 | _chunks.clear(); 119 | } 120 | 121 | /// Returns the bytes currently contained in this builder and clears it. 122 | /// 123 | /// The returned list may be a view of a larger buffer. 124 | Uint8List takeBytes() { 125 | final result = toBytes(); 126 | clear(); 127 | return result; 128 | } 129 | 130 | /// Returns a copy of the current byte contents of this builder. 131 | /// 132 | /// Leaves the contents of this builder intact. 133 | Uint8List toBytes() { 134 | final result = Uint8List(_length); 135 | var offset = 0; 136 | for (final chunk in _chunks) { 137 | result.setRange(offset, offset + chunk.length, chunk); 138 | offset += chunk.length; 139 | } 140 | return result; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /lib/src/http_client.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 terrier989@gmail.com. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // ------------------------------------------------------------------ 16 | // THIS FILE WAS DERIVED FROM SOURCE CODE UNDER THE FOLLOWING LICENSE 17 | // ------------------------------------------------------------------ 18 | // 19 | // Copyright 2012, the Dart project authors. All rights reserved. 20 | // Redistribution and use in source and binary forms, with or without 21 | // modification, are permitted provided that the following conditions are 22 | // met: 23 | // * Redistributions of source code must retain the above copyright 24 | // notice, this list of conditions and the following disclaimer. 25 | // * Redistributions in binary form must reproduce the above 26 | // copyright notice, this list of conditions and the following 27 | // disclaimer in the documentation and/or other materials provided 28 | // with the distribution. 29 | // * Neither the name of Google Inc. nor the names of its 30 | // contributors may be used to endorse or promote products derived 31 | // from this software without specific prior written permission. 32 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 33 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 34 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 35 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 36 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 37 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 38 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 39 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 40 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 41 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 42 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 43 | 44 | import 'dart:async'; 45 | import 'dart:io' as dart_io; 46 | 47 | import 'package:universal_io/io.dart'; 48 | 49 | /// A client that receives content, such as web pages, from 50 | /// a server using the HTTP protocol. 51 | /// 52 | /// HttpClient contains a number of methods to send an [HttpClientRequest] 53 | /// to an Http server and receive an [HttpClientResponse] back. 54 | /// For example, you can use the [get], [getUrl], [post], and [postUrl] methods 55 | /// for GET and POST requests, respectively. 56 | /// 57 | /// ## Making a simple GET request: an example 58 | /// 59 | /// A `getUrl` request is a two-step process, triggered by two [Future]s. 60 | /// When the first future completes with a [HttpClientRequest], the underlying 61 | /// network connection has been established, but no data has been sent. 62 | /// In the callback function for the first future, the HTTP headers and body 63 | /// can be set on the request. Either the first write to the request object 64 | /// or a call to [close] sends the request to the server. 65 | /// 66 | /// When the HTTP response is received from the server, 67 | /// the second future, which is returned by close, 68 | /// completes with an [HttpClientResponse] object. 69 | /// This object provides access to the headers and body of the response. 70 | /// The body is available as a stream implemented by HttpClientResponse. 71 | /// If a body is present, it must be read. Otherwise, it leads to resource 72 | /// leaks. Consider using [HttpClientResponse.drain] if the body is unused. 73 | /// 74 | /// HttpClient client = HttpClient(); 75 | /// client.getUrl(Uri.parse("http://www.example.com/")) 76 | /// .then((HttpClientRequest request) { 77 | /// // Optionally set up headers... 78 | /// // Optionally write to the request object... 79 | /// // Then call close. 80 | /// ... 81 | /// return request.close(); 82 | /// }) 83 | /// .then((HttpClientResponse response) { 84 | /// // Process the response. 85 | /// ... 86 | /// }); 87 | /// 88 | /// The future for [HttpClientRequest] is created by methods such as 89 | /// [getUrl] and [open]. 90 | /// 91 | /// ## HTTPS connections 92 | /// 93 | /// An HttpClient can make HTTPS requests, connecting to a server using 94 | /// the TLS (SSL) secure networking protocol. Calling [getUrl] with an 95 | /// https: scheme will work automatically, if the server's certificate is 96 | /// signed by a root CA (certificate authority) on the default list of 97 | /// well-known trusted CAs, compiled by Mozilla. 98 | /// 99 | /// To add a custom trusted certificate authority, or to send a client 100 | /// certificate to servers that request one, pass a [SecurityContext] object 101 | /// as the optional `context` argument to the `HttpClient` constructor. 102 | /// The desired security options can be set on the [SecurityContext] object. 103 | /// 104 | /// ## Headers 105 | /// 106 | /// All HttpClient requests set the following header by default: 107 | /// 108 | /// Accept-Encoding: gzip 109 | /// 110 | /// This allows the HTTP server to use gzip compression for the body if 111 | /// possible. If this behavior is not desired set the 112 | /// `Accept-Encoding` header to something else. 113 | /// To turn off gzip compression of the response, clear this header: 114 | /// 115 | /// request.headers.removeAll(HttpHeaders.acceptEncodingHeader) 116 | /// 117 | /// ## Closing the HttpClient 118 | /// 119 | /// The HttpClient supports persistent connections and caches network 120 | /// connections to reuse them for multiple requests whenever 121 | /// possible. This means that network connections can be kept open for 122 | /// some time after a request has completed. Use HttpClient.close 123 | /// to force the HttpClient object to shut down and to close the idle 124 | /// network connections. 125 | /// 126 | /// ## Turning proxies on and off 127 | /// 128 | /// By default the HttpClient uses the proxy configuration available 129 | /// from the environment, see [findProxyFromEnvironment]. To turn off 130 | /// the use of proxies set the [findProxy] property to 131 | /// `null`. 132 | /// 133 | /// HttpClient client = HttpClient(); 134 | /// client.findProxy = null; 135 | abstract class HttpClient implements dart_io.HttpClient { 136 | static const int defaultHttpPort = 80; 137 | static const int defaultHttpsPort = 443; 138 | 139 | /// Current state of HTTP request logging from all [HttpClient]s to the 140 | /// developer timeline. 141 | /// 142 | /// Default is `false`. 143 | static bool enableTimelineLogging = false; 144 | 145 | factory HttpClient({SecurityContext? context}) { 146 | var overrides = HttpOverrides.current; 147 | if (overrides == null) { 148 | return newUniversalHttpClient() as HttpClient; 149 | } 150 | return overrides.createHttpClient(context) as HttpClient; 151 | } 152 | 153 | /// Function for resolving the proxy server to be used for a HTTP 154 | /// connection from the proxy configuration specified through 155 | /// environment variables. 156 | /// 157 | /// The following environment variables are taken into account: 158 | /// 159 | /// http_proxy 160 | /// https_proxy 161 | /// no_proxy 162 | /// HTTP_PROXY 163 | /// HTTPS_PROXY 164 | /// NO_PROXY 165 | /// 166 | /// [:http_proxy:] and [:HTTP_PROXY:] specify the proxy server to use for 167 | /// http:// urls. Use the format [:hostname:port:]. If no port is used a 168 | /// default of 1080 will be used. If both are set the lower case one takes 169 | /// precedence. 170 | /// 171 | /// [:https_proxy:] and [:HTTPS_PROXY:] specify the proxy server to use for 172 | /// https:// urls. Use the format [:hostname:port:]. If no port is used a 173 | /// default of 1080 will be used. If both are set the lower case one takes 174 | /// precedence. 175 | /// 176 | /// [:no_proxy:] and [:NO_PROXY:] specify a comma separated list of 177 | /// postfixes of hostnames for which not to use the proxy 178 | /// server. E.g. the value "localhost,127.0.0.1" will make requests 179 | /// to both "localhost" and "127.0.0.1" not use a proxy. If both are set 180 | /// the lower case one takes precedence. 181 | /// 182 | /// To activate this way of resolving proxies assign this function to 183 | /// the [findProxy] property on the [HttpClient]. 184 | /// 185 | /// HttpClient client = HttpClient(); 186 | /// client.findProxy = HttpClient.findProxyFromEnvironment; 187 | /// 188 | /// If you don't want to use the system environment you can use a 189 | /// different one by wrapping the function. 190 | /// 191 | /// HttpClient client = HttpClient(); 192 | /// client.findProxy = (url) { 193 | /// return HttpClient.findProxyFromEnvironment( 194 | /// url, environment: {"http_proxy": ..., "no_proxy": ...}); 195 | /// } 196 | /// 197 | /// If a proxy requires authentication it is possible to configure 198 | /// the username and password as well. Use the format 199 | /// [:username:password@hostname:port:] to include the username and 200 | /// password. Alternatively the API [addProxyCredentials] can be used 201 | /// to set credentials for proxies which require authentication. 202 | static String findProxyFromEnvironment(Uri url, 203 | {Map? environment}) { 204 | return 'DIRECT'; 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /lib/src/internet_address.dart: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------ 2 | // THIS FILE WAS DERIVED FROM SOURCE CODE UNDER THE FOLLOWING LICENSE 3 | // ------------------------------------------------------------------ 4 | // 5 | // Copyright 2012, the Dart project authors. All rights reserved. 6 | // Redistribution and use in source and binary forms, with or without 7 | // modification, are permitted provided that the following conditions are 8 | // met: 9 | // * Redistributions of source code must retain the above copyright 10 | // notice, this list of conditions and the following disclaimer. 11 | // * Redistributions in binary form must reproduce the above 12 | // copyright notice, this list of conditions and the following 13 | // disclaimer in the documentation and/or other materials provided 14 | // with the distribution. 15 | // * Neither the name of Google Inc. nor the names of its 16 | // contributors may be used to endorse or promote products derived 17 | // from this software without specific prior written permission. 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | // 30 | // --------------------------------------------------------- 31 | // THIS, DERIVED FILE IS LICENSE UNDER THE FOLLOWING LICENSE 32 | // --------------------------------------------------------- 33 | // Copyright 2020 terrier989@gmail.com. 34 | // 35 | // Licensed under the Apache License, Version 2.0 (the "License"); 36 | // you may not use this file except in compliance with the License. 37 | // You may obtain a copy of the License at 38 | // 39 | // http://www.apache.org/licenses/LICENSE-2.0 40 | // 41 | // Unless required by applicable law or agreed to in writing, software 42 | // distributed under the License is distributed on an "AS IS" BASIS, 43 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 44 | // See the License for the specific language governing permissions and 45 | // limitations under the License. 46 | 47 | import 'dart:convert'; 48 | import 'dart:io' show InternetAddressType; 49 | import 'dart:io' as dart_io; 50 | import 'dart:typed_data'; 51 | 52 | import 'package:collection/collection.dart'; 53 | 54 | String _stringFromIp(Uint8List bytes) { 55 | switch (bytes.length) { 56 | case 4: 57 | return bytes.map((item) => item.toString()).join('.'); 58 | case 16: 59 | return _stringFromIp6(bytes); 60 | default: 61 | throw ArgumentError.value(bytes); 62 | } 63 | } 64 | 65 | String _stringFromIp6(Uint8List bytes) { 66 | // --------------------------- 67 | // Find longest span of zeroes 68 | // --------------------------- 69 | 70 | // Longest seen span 71 | int? longestStart; 72 | var longestLength = 0; 73 | 74 | // Current span 75 | int? start; 76 | var length = 0; 77 | 78 | // Iterate 79 | for (var i = 0; i < 16; i++) { 80 | if (bytes[i] == 0) { 81 | // Zero byte 82 | if (start == null) { 83 | if (i % 2 == 0) { 84 | // First byte of a span 85 | start = i; 86 | length = 1; 87 | } 88 | } else { 89 | length++; 90 | } 91 | } else if (start != null) { 92 | // End of a span 93 | if (length > longestLength) { 94 | // Longest so far 95 | longestStart = start; 96 | longestLength = length; 97 | } 98 | start = null; 99 | } 100 | } 101 | if (start != null && length > longestLength) { 102 | // End of the longest span 103 | longestStart = start; 104 | longestLength = length; 105 | } 106 | 107 | // Longest length must be a whole group 108 | longestLength -= longestLength % 2; 109 | 110 | // Ignore longest zero span if it's less than 4 bytes. 111 | if (longestLength < 4) { 112 | longestStart = null; 113 | } 114 | 115 | // ---- 116 | // Print 117 | // ----- 118 | final sb = StringBuffer(); 119 | var colon = false; 120 | for (var i = 0; i < 16; i++) { 121 | if (i == longestStart) { 122 | sb.write('::'); 123 | i += longestLength - 1; 124 | colon = false; 125 | continue; 126 | } 127 | final byte = bytes[i]; 128 | if (i % 2 == 0) { 129 | // 130 | // First byte of a group 131 | // 132 | if (colon) { 133 | sb.write(':'); 134 | } else { 135 | colon = true; 136 | } 137 | if (byte != 0) { 138 | sb.write(byte.toRadixString(16)); 139 | } 140 | } else { 141 | // 142 | // Second byte of a group 143 | // 144 | // If this is a single-digit number and the previous byte was non-zero, 145 | // we must add zero 146 | if (byte < 16 && bytes[i - 1] != 0) { 147 | sb.write('0'); 148 | } 149 | sb.write(byte.toRadixString(16)); 150 | } 151 | } 152 | return sb.toString(); 153 | } 154 | 155 | /// Parses IPv4/IPv6 address. 156 | /// 157 | Uint8List? _tryParseRawAddress(String source) { 158 | // Find first '.' or ':' 159 | for (var i = 0; i < source.length; i++) { 160 | final c = source.substring(i, i + 1); 161 | switch (c) { 162 | case ':': 163 | return Uri.parseIPv6Address(source) as Uint8List; 164 | case '.': 165 | return Uri.parseIPv4Address(source) as Uint8List; 166 | } 167 | } 168 | return null; 169 | } 170 | 171 | InternetAddressType _type(String address) { 172 | for (var i = 0; i < address.length; i++) { 173 | final c = address.substring(i, i + 1); 174 | switch (c) { 175 | case ':': 176 | return InternetAddressType.IPv6; 177 | case '.': 178 | return InternetAddressType.IPv4; 179 | } 180 | } 181 | throw ArgumentError.value(address); 182 | } 183 | 184 | /// An internet address or a Unix domain address. 185 | /// 186 | /// This object holds an internet address. If this internet address 187 | /// is the result of a DNS lookup, the address also holds the hostname 188 | /// used to make the lookup. 189 | /// An Internet address combined with a port number represents an 190 | /// endpoint to which a socket can connect or a listening socket can 191 | /// bind. 192 | class InternetAddress implements dart_io.InternetAddress { 193 | /// IP version 4 any address. Use this address when listening on 194 | /// all adapters IP addresses using IP version 4 (IPv4). 195 | static final InternetAddress anyIPv4 = InternetAddress('0.0.0.0'); 196 | 197 | /// IP version 6 any address. Use this address when listening on 198 | /// all adapters IP addresses using IP version 6 (IPv6). 199 | static final InternetAddress anyIPv6 = InternetAddress('::'); 200 | 201 | /// IP version 4 loopback address. Use this address when listening on 202 | /// or connecting to the loopback adapter using IP version 4 (IPv4). 203 | static final InternetAddress loopbackIPv4 = InternetAddress('127.0.0.1'); 204 | 205 | /// IP version 6 loopback address. Use this address when listening on 206 | /// or connecting to the loopback adapter using IP version 6 (IPv6). 207 | static final InternetAddress loopbackIPv6 = InternetAddress('::1'); 208 | 209 | @override 210 | final String address; 211 | 212 | @override 213 | final Uint8List rawAddress; 214 | 215 | @override 216 | final InternetAddressType type; 217 | 218 | /// Creates a new [InternetAddress] from a numeric address or a file path. 219 | /// 220 | /// If [type] is [InternetAddressType.IPv4], [address] must be a numeric IPv4 221 | /// address (dotted-decimal notation). 222 | /// If [type] is [InternetAddressType.IPv6], [address] must be a numeric IPv6 223 | /// address (hexadecimal notation). 224 | /// If [type] is [InternetAddressType.unix], [address] must be a a valid file 225 | /// path. 226 | /// If [type] is omitted, [address] must be either a numeric IPv4 or IPv6 227 | /// address and the type is inferred from the format. 228 | /// 229 | /// To create a Unix domain address, [type] should be 230 | /// [InternetAddressType.unix] and [address] should be a string. 231 | factory InternetAddress(String address, {InternetAddressType? type}) { 232 | if (type == InternetAddressType.unix) { 233 | if (!address.startsWith('/')) { 234 | throw ArgumentError.value(address, 'address'); 235 | } 236 | return InternetAddress._( 237 | address: address, 238 | rawAddress: Uint8List(0), 239 | type: InternetAddressType.unix, 240 | ); 241 | } 242 | final parsed = tryParse(address); 243 | if (parsed == null) { 244 | throw ArgumentError.value(address, 'address'); 245 | } 246 | return parsed; 247 | } 248 | 249 | /// Creates a new [InternetAddress] from the provided raw address bytes. 250 | /// 251 | /// If the [type] is [InternetAddressType.IPv4], the [rawAddress] must have 252 | /// length 4. 253 | /// If the [type] is [InternetAddressType.IPv6], the [rawAddress] must have 254 | /// length 16. 255 | /// If the [type] is [InternetAddressType.IPv4], the [rawAddress] must be a 256 | /// valid UTF-8 encoded file path. 257 | /// 258 | /// If [type] is omitted, the [rawAddress] must have a length of either 4 or 259 | /// 16, in which case the type defaults to [InternetAddressType.IPv4] or 260 | /// [InternetAddressType.IPv6] respectively. 261 | factory InternetAddress.fromRawAddress(Uint8List rawAddress, 262 | {InternetAddressType? type}) { 263 | if (type == InternetAddressType.unix) { 264 | return InternetAddress(utf8.decode(rawAddress), type: type); 265 | } 266 | final address = _stringFromIp(rawAddress); 267 | type = _type(address); 268 | return InternetAddress._( 269 | address: address, 270 | rawAddress: rawAddress, 271 | type: type, 272 | ); 273 | } 274 | 275 | InternetAddress._({ 276 | required this.address, 277 | required this.rawAddress, 278 | required this.type, 279 | }); 280 | 281 | @override 282 | int get hashCode => const ListEquality().hash(rawAddress); 283 | 284 | @override 285 | String get host => address; 286 | 287 | @override 288 | bool get isLinkLocal { 289 | final rawAddress = this.rawAddress; 290 | if (type == InternetAddressType.IPv6) { 291 | // First 10 bits is 0xFE80 292 | return rawAddress[0] == 0xFE && ((0x80 | 0x40) & rawAddress[1]) == 0x80; 293 | } 294 | return false; 295 | } 296 | 297 | @override 298 | bool get isLoopback => this == loopbackIPv4 || this == loopbackIPv6; 299 | 300 | @override 301 | bool get isMulticast => this == anyIPv4 || this == anyIPv6; 302 | 303 | @override 304 | bool operator ==(other) { 305 | if (other is InternetAddress) { 306 | if (type == InternetAddressType.unix) { 307 | return address == other.address; 308 | } 309 | return const ListEquality().equals(rawAddress, other.rawAddress); 310 | } 311 | return false; 312 | } 313 | 314 | @override 315 | Future reverse() { 316 | throw UnimplementedError(); 317 | } 318 | 319 | /// Lookup a host, returning a Future of a list of 320 | /// [InternetAddress]s. If [type] is [InternetAddressType.any], it 321 | /// will lookup both IP version 4 (IPv4) and IP version 6 (IPv6) 322 | /// addresses. If [type] is either [InternetAddressType.IPv4] or 323 | /// [InternetAddressType.IPv6] it will only lookup addresses of the 324 | /// specified type. The order of the list can, and most likely will, 325 | /// change over time. 326 | static Future> lookup(String host, 327 | {InternetAddressType type = InternetAddressType.any}) => 328 | throw UnimplementedError(); 329 | 330 | /// Attempts to parse [address] as a numeric address. 331 | /// 332 | /// Returns `null` If [address] is not a numeric IPv4 (dotted-decimal 333 | /// notation) or IPv6 (hexadecimal representation) address. 334 | static InternetAddress? tryParse(String address) { 335 | final rawAddress = _tryParseRawAddress(address); 336 | if (rawAddress == null) { 337 | return null; 338 | } 339 | final type = _type(address); 340 | return InternetAddress._( 341 | address: address, 342 | rawAddress: rawAddress, 343 | type: type, 344 | ); 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /lib/src/new_universal_http_client.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 terrier989@gmail.com. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:universal_io/io.dart'; 16 | 17 | import '_helpers.dart' as helpers; 18 | 19 | /// Constructs a new [HttpClient] that will be [BrowserHttpClient] in browsers 20 | /// and the normal _dart:io_ HTTP client everywhere else. 21 | HttpClient newUniversalHttpClient() => helpers.newHttpClient(); 22 | -------------------------------------------------------------------------------- /lib/src/platform.dart: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------ 2 | // THIS FILE WAS DERIVED FROM SOURCE CODE UNDER THE FOLLOWING LICENSE 3 | // ------------------------------------------------------------------ 4 | // 5 | // Copyright 2012, the Dart project authors. All rights reserved. 6 | // Redistribution and use in source and binary forms, with or without 7 | // modification, are permitted provided that the following conditions are 8 | // met: 9 | // * Redistributions of source code must retain the above copyright 10 | // notice, this list of conditions and the following disclaimer. 11 | // * Redistributions in binary form must reproduce the above 12 | // copyright notice, this list of conditions and the following 13 | // disclaimer in the documentation and/or other materials provided 14 | // with the distribution. 15 | // * Neither the name of Google Inc. nor the names of its 16 | // contributors may be used to endorse or promote products derived 17 | // from this software without specific prior written permission. 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | // 30 | // --------------------------------------------------------- 31 | // THIS, DERIVED FILE IS LICENSE UNDER THE FOLLOWING LICENSE 32 | // --------------------------------------------------------- 33 | // Copyright 2020 terrier989@gmail.com. 34 | // 35 | // Licensed under the Apache License, Version 2.0 (the "License"); 36 | // you may not use this file except in compliance with the License. 37 | // You may obtain a copy of the License at 38 | // 39 | // http://www.apache.org/licenses/LICENSE-2.0 40 | // 41 | // Unless required by applicable law or agreed to in writing, software 42 | // distributed under the License is distributed on an "AS IS" BASIS, 43 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 44 | // See the License for the specific language governing permissions and 45 | // limitations under the License. 46 | 47 | import '_helpers.dart' as impl; 48 | 49 | /// Information about the environment in which the current program is running. 50 | /// 51 | /// Platform provides information such as the operating system, 52 | /// the hostname of the computer, the value of environment variables, 53 | /// the path to the running program, 54 | /// and so on. 55 | /// 56 | /// ## Get the URI to the current Dart script 57 | /// 58 | /// Use the [script] getter to get the URI to the currently running 59 | /// Dart script. 60 | /// 61 | /// import 'dart:io' show Platform; 62 | /// 63 | /// void main() { 64 | /// // Get the URI of the script being run. 65 | /// var uri = Platform.script; 66 | /// // Convert the URI to a path. 67 | /// var path = uri.toFilePath(); 68 | /// } 69 | /// 70 | /// ## Get the value of an environment variable 71 | /// 72 | /// The [environment] getter returns a the names and values of environment 73 | /// variables in a [Map] that contains key-value pairs of strings. The Map is 74 | /// unmodifiable. This sample shows how to get the value of the `PATH` 75 | /// environment variable. 76 | /// 77 | /// import 'dart:io' show Platform; 78 | /// 79 | /// void main() { 80 | /// Map envVars = Platform.environment; 81 | /// print(envVars['PATH']); 82 | /// } 83 | /// 84 | /// ## Determine the OS 85 | /// 86 | /// You can get the name of the operating system as a string with the 87 | /// [operatingSystem] getter. You can also use one of the static boolean 88 | /// getters: [isMacOS], [isLinux], and [isWindows]. 89 | /// 90 | /// import 'dart:io' show Platform, stdout; 91 | /// 92 | /// void main() { 93 | /// // Get the operating system as a string. 94 | /// String os = Platform.operatingSystem; 95 | /// // Or, use a predicate getter. 96 | /// if (Platform.isMacOS) { 97 | /// print('is a Mac'); 98 | /// } else { 99 | /// print('is not a Mac'); 100 | /// } 101 | /// } 102 | class Platform { 103 | /// Whether the operating system is a version of 104 | /// [Linux](https://en.wikipedia.org/wiki/Linux). 105 | /// 106 | /// This value is `false` if the operating system is a specialized 107 | /// version of Linux that identifies itself by a different name, 108 | /// for example Android (see [isAndroid]). 109 | static final bool isLinux = (operatingSystem == 'linux'); 110 | 111 | /// Whether the operating system is a version of 112 | /// [macOS](https://en.wikipedia.org/wiki/MacOS). 113 | static final bool isMacOS = (operatingSystem == 'macos'); 114 | 115 | /// Whether the operating system is a version of 116 | /// [Microsoft Windows](https://en.wikipedia.org/wiki/Microsoft_Windows). 117 | static final bool isWindows = (operatingSystem == 'windows'); 118 | 119 | /// Whether the operating system is a version of 120 | /// [Android](https://en.wikipedia.org/wiki/Android_%28operating_system%29). 121 | static final bool isAndroid = (operatingSystem == 'android'); 122 | 123 | /// Whether the operating system is a version of 124 | /// [iOS](https://en.wikipedia.org/wiki/IOS). 125 | static final bool isIOS = (operatingSystem == 'ios'); 126 | 127 | /// Whether the operating system is a version of 128 | /// [Fuchsia](https://en.wikipedia.org/wiki/Google_Fuchsia). 129 | static final bool isFuchsia = (operatingSystem == 'fuchsia'); 130 | 131 | /// The environment for this process as a map from string key to string value. 132 | /// 133 | /// The map is unmodifiable, 134 | /// and its content is retrieved from the operating system on its first use. 135 | /// 136 | /// Environment variables on Windows are case-insensitive, 137 | /// so on Windows the map is case-insensitive and will convert 138 | /// all keys to upper case. 139 | /// On other platforms, keys can be distinguished by case. 140 | static Map get environment => {}; 141 | 142 | /// The path of the executable used to run the script in this isolate. 143 | /// 144 | /// The literal path used to identify the script. 145 | /// This path might be relative or just be a name from which the executable 146 | /// was found by searching the system path. 147 | /// 148 | /// Use [resolvedExecutable] to get an absolute path to the executable. 149 | static String get executable => ''; 150 | 151 | /// The flags passed to the executable used to run the script in this isolate. 152 | /// 153 | /// These are the command-line flags to the executable that precedes 154 | /// the script name. 155 | /// Provides a new list every time the value is read. 156 | static List get executableArguments => []; 157 | 158 | /// Get the name of the current locale. 159 | static String get localeName => impl.locale; 160 | 161 | /// The local hostname for the system. 162 | static String get localHostname => 'localhost'; 163 | 164 | /// The number of individual execution units of the machine. 165 | static int get numberOfProcessors => 1; 166 | 167 | /// A string representing the operating system or platform. 168 | static String get operatingSystem => impl.operatingSystem; 169 | 170 | /// A string representing the version of the operating system or platform. 171 | static String get operatingSystemVersion => impl.operatingSystemVersion; 172 | 173 | /// The `--packages` flag passed to the executable used to run the script 174 | /// in this isolate. 175 | /// 176 | /// If present, it specifies a file describing how Dart packages are looked up. 177 | /// 178 | /// Is `null` if there is no `--packages` flag. 179 | static String? get packageConfig => null; 180 | 181 | /// The path separator used by the operating system to separate 182 | /// components in file paths. 183 | static String get pathSeparator => '/'; 184 | 185 | /// The path of the executable used to run the script in this 186 | /// isolate after it has been resolved by the OS. 187 | /// 188 | /// This is the absolute path, with all symlinks resolved, to the 189 | /// executable used to run the script. 190 | static String get resolvedExecutable => ''; 191 | 192 | /// The absolute URI of the script being run in this isolate. 193 | /// 194 | /// If the script argument on the command line is relative, 195 | /// it is resolved to an absolute URI before fetching the script, and 196 | /// that absolute URI is returned. 197 | /// 198 | /// URI resolution only does string manipulation on the script path, and this 199 | /// may be different from the file system's path resolution behavior. For 200 | /// example, a symbolic link immediately followed by '..' will not be 201 | /// looked up. 202 | /// 203 | /// If the executable environment does not support [script], 204 | /// the URI is empty. 205 | static Uri get script => Uri(); 206 | 207 | /// The version of the current Dart runtime. 208 | /// 209 | /// The value is a [semantic versioning](http://semver.org) 210 | /// string representing the version of the current Dart runtime, 211 | /// possibly followed by whitespace and other version and 212 | /// build details. 213 | static String get version => ''; 214 | } 215 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: universal_io 2 | version: 2.2.2 3 | description: Cross-platform 'dart:io' that adds browser support for HttpClient and some other 4 | "dart:io" APIs. 5 | homepage: https://github.com/dart-io-packages/universal_io 6 | false_secrets: 7 | - /test/localhost.key 8 | 9 | environment: 10 | sdk: '>=2.17.0 <4.0.0' 11 | 12 | dependencies: 13 | collection: ^1.17.0 14 | meta: ^1.9.0 15 | typed_data: ^1.3.0 16 | 17 | dev_dependencies: 18 | async: ^2.11.0 19 | lints: ^2.1.0 20 | stream_channel: ^2.1.1 21 | test: ^1.24.0 -------------------------------------------------------------------------------- /test/http_client_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 terrier989@gmail.com. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | library main_test; 16 | 17 | import 'dart:async'; 18 | import 'dart:convert'; 19 | 20 | import 'package:async/async.dart'; 21 | import 'package:test/test.dart'; 22 | import 'package:universal_io/io.dart'; 23 | 24 | var serverPort = -1; 25 | var secureServerPort = -1; 26 | 27 | void main() { 28 | setUpAll(() async { 29 | final channel = spawnHybridUri('server.dart', message: {}); 30 | final streamQueue = StreamQueue(channel.stream); 31 | serverPort = ((await streamQueue.next) as num).toInt(); 32 | secureServerPort = ((await streamQueue.next) as num).toInt(); 33 | 34 | addTearDown(() { 35 | channel.sink.close(); 36 | streamQueue.cancel(); 37 | }); 38 | }); 39 | 40 | group('Chrome', () { 41 | _testHttpClient(isBrowser: true); 42 | }, testOn: 'chrome'); 43 | 44 | group('VM:', () { 45 | _testHttpClient(isBrowser: false); 46 | }, testOn: 'vm'); 47 | } 48 | 49 | class _HttpOverrides extends HttpOverrides { 50 | @override 51 | HttpClient createHttpClient(SecurityContext? context) { 52 | throw StateError('ERROR'); 53 | } 54 | } 55 | 56 | void _testHttpClient({required bool isBrowser}) async { 57 | group('HttpOverrides', () { 58 | setUp(() { 59 | HttpOverrides.global = _HttpOverrides(); 60 | }); 61 | tearDown(() { 62 | HttpOverrides.global = null; 63 | }); 64 | test('findProxyFromEnvironment', () { 65 | final proxy = HttpClient.findProxyFromEnvironment( 66 | Uri.parse('http://example/path'), 67 | ); 68 | expect(proxy, 'DIRECT'); 69 | }); 70 | test('createHttpClient', () { 71 | expect(() => HttpClient(), throwsStateError); 72 | }); 73 | }); 74 | 75 | group('HttpClient:', () { 76 | test('In VM: does NOT implement BrowserHttpClient', () async { 77 | final client = HttpClient(); 78 | expect(client, isNot(isA())); 79 | }, testOn: '!browser'); 80 | 81 | test('In browser: does implement BrowserHttpClient', () async { 82 | final client = HttpClient(); 83 | expect(client, isA()); 84 | if (client is BrowserHttpClient) { 85 | expect(client.onBrowserHttpClientRequestClose, isNull); 86 | } 87 | }, testOn: 'browser'); 88 | 89 | test('In browser: requests implement BrowserHttpClientRequest', () async { 90 | final client = HttpClient(); 91 | if (client is BrowserHttpClient) { 92 | client.onBrowserHttpClientRequestClose = expectAsync1( 93 | (request) { 94 | request.browserResponseType = 'text'; 95 | }, 96 | count: 1, 97 | ); 98 | } 99 | final request = await client.openUrl( 100 | 'GET', 101 | Uri.parse('http://localhost:$serverPort/greeting'), 102 | ); 103 | expect(request, isA()); 104 | if (request is BrowserHttpClientRequest) { 105 | expect(request.browserCredentialsMode, isFalse); 106 | expect(request.browserResponseType, isNull); 107 | await request.close(); 108 | expect(request.browserCredentialsMode, isFalse); 109 | expect(request.browserResponseType, 'text'); 110 | } 111 | }, testOn: 'browser'); 112 | 113 | test('findProxyFromEnvironment', () { 114 | final proxy = HttpClient.findProxyFromEnvironment( 115 | Uri.parse('http://example/path'), 116 | ); 117 | expect(proxy, 'DIRECT'); 118 | }); 119 | 120 | test('Non-existing server leads to SocketException', () async { 121 | final httpClient = HttpClient(); 122 | final httpRequestFuture = 123 | httpClient.getUrl(Uri.parse('http://localhost:23456')); 124 | if (!isBrowser) { 125 | await expectLater( 126 | () => httpRequestFuture, throwsA(TypeMatcher())); 127 | return; 128 | } 129 | final httpRequest = await httpRequestFuture; 130 | final httpResponseFuture = httpRequest.close(); 131 | await expectLater( 132 | () => httpResponseFuture, throwsA(TypeMatcher())); 133 | }); 134 | 135 | test('GET', () async { 136 | await _testClient( 137 | request: _Request( 138 | method: 'GET', 139 | path: '/greeting', 140 | ), 141 | expectedResponse: _ExpectedResponse( 142 | body: 'Hello world! (GET)', 143 | ), 144 | ); 145 | }); 146 | 147 | test('GET (multiple chunks)', () async { 148 | // Send HTTP request 149 | final client = HttpClient(); 150 | final request = await client.openUrl( 151 | 'GET', 152 | Uri.parse('http://localhost:$serverPort/slow'), 153 | ); 154 | if (request is BrowserHttpClientRequest) { 155 | request.browserResponseType = 'text'; 156 | } 157 | final response = await request.close(); 158 | final list = await response.toList(); 159 | 160 | // Check that the data arrived in multiple parts. 161 | expect(list, hasLength(greaterThanOrEqualTo(2))); 162 | 163 | // Check that the content is correct. 164 | final bytes = list.fold([], (dynamic a, b) => a..addAll(b)); 165 | expect(utf8.decode(bytes), 'First part.\nSecond part.\n'); 166 | }); 167 | 168 | test('POST', () async { 169 | await _testClient( 170 | request: _Request( 171 | method: 'POST', 172 | path: '/greeting', 173 | body: 'Hello from client', 174 | ), 175 | expectedResponse: _ExpectedResponse( 176 | body: 'Hello world! (POST)', 177 | ), 178 | ); 179 | }); 180 | 181 | test('Status 404', () async { 182 | await _testClient( 183 | request: _Request( 184 | method: 'GET', 185 | path: '/404', 186 | ), 187 | expectedResponse: _ExpectedResponse( 188 | status: 404, 189 | ), 190 | ); 191 | }); 192 | 193 | test('Receiving cookies fails without credentials mode', () async { 194 | if (isBrowser) { 195 | final response = (await _testClient( 196 | request: _Request( 197 | method: 'GET', 198 | path: '/set_cookie?name=x&value=y', 199 | ), 200 | expectedResponse: _ExpectedResponse( 201 | status: HttpStatus.ok, 202 | ), 203 | ))!; 204 | expect(response.cookies, []); 205 | } else { 206 | final response = (await _testClient( 207 | request: _Request( 208 | method: 'GET', 209 | path: '/set_cookie?name=x&value=y', 210 | ), 211 | expectedResponse: _ExpectedResponse( 212 | status: HttpStatus.ok, 213 | ), 214 | ))!; 215 | expect(response.cookies, hasLength(1)); 216 | expect(response.cookies.single.name, 'x'); 217 | expect(response.cookies.single.value, 'y'); 218 | } 219 | }); 220 | 221 | test('Receiving cookies succeeds with credentials mode', () async { 222 | final httpClient = HttpClient(); 223 | await _testClient( 224 | existingHttpClient: httpClient, 225 | request: _Request( 226 | method: 'GET', 227 | path: '/expect_cookie?name=x&value=y', 228 | credentialsMode: true, 229 | ), 230 | expectedResponse: _ExpectedResponse( 231 | status: HttpStatus.unauthorized, 232 | ), 233 | ); 234 | await _testClient( 235 | existingHttpClient: httpClient, 236 | request: _Request( 237 | method: 'GET', 238 | path: '/set_cookie?name=x&value=y', 239 | credentialsMode: true, 240 | ), 241 | ); 242 | if (isBrowser) { 243 | (await _testClient( 244 | existingHttpClient: httpClient, 245 | request: _Request( 246 | method: 'GET', 247 | path: '/expect_cookie?name=x&value=y', 248 | credentialsMode: true, 249 | ), 250 | ))!; 251 | } else { 252 | await _testClient( 253 | existingHttpClient: httpClient, 254 | request: _Request( 255 | method: 'GET', 256 | path: '/expect_cookie?name=x&value=y', 257 | credentialsMode: true, 258 | ), 259 | expectedResponse: _ExpectedResponse( 260 | status: HttpStatus.unauthorized, 261 | ), 262 | ); 263 | } 264 | }); 265 | 266 | test('Sending cookies fails without credentials mode', () async { 267 | var expectedStatus = HttpStatus.ok; 268 | if (isBrowser) { 269 | expectedStatus = HttpStatus.unauthorized; 270 | } 271 | await _testClient( 272 | request: _Request( 273 | method: 'GET', 274 | path: '/expect_cookie?name=expectedCookie&value=value', 275 | headers: { 276 | 'Cookie': Cookie('expectedCookie', 'value').toString(), 277 | }, 278 | ), 279 | expectedResponse: _ExpectedResponse( 280 | status: expectedStatus, 281 | ), 282 | ); 283 | }); 284 | 285 | test("Sends 'Authorization' header", () async { 286 | await _testClient( 287 | request: _Request( 288 | method: 'POST', 289 | path: '/expect_authorization', 290 | headers: { 291 | HttpHeaders.authorizationHeader: 'expectedAuthorization', 292 | }, 293 | ), 294 | expectedResponse: _ExpectedResponse( 295 | status: HttpStatus.ok, 296 | body: 'expectedAuthorization', 297 | ), 298 | ); 299 | }); 300 | 301 | // ------ 302 | // DELETE 303 | // ------ 304 | test('client.delete(...)', () async { 305 | await _testClientMethodWithoutUri( 306 | method: 'DELETE', 307 | openUrl: (client, host, port, path) => client.delete(host, port, path), 308 | ); 309 | }); 310 | 311 | test('client.deleteUrl(...)', () async { 312 | await _testClient( 313 | request: _Request( 314 | method: 'DELETE', 315 | path: '/greeting', 316 | ), 317 | expectedResponse: _ExpectedResponse( 318 | body: 'Hello world! (DELETE)', 319 | ), 320 | openUrl: (client, uri) => client.deleteUrl(uri), 321 | ); 322 | }); 323 | 324 | // --- 325 | // GET 326 | // --- 327 | 328 | test('client.get(...)', () async { 329 | await _testClientMethodWithoutUri( 330 | method: 'GET', 331 | openUrl: (client, host, port, path) => client.get(host, port, path), 332 | ); 333 | }); 334 | 335 | test('client.getUrl(...)', () async { 336 | await _testClient( 337 | request: _Request( 338 | method: 'GET', 339 | path: '/greeting', 340 | ), 341 | expectedResponse: _ExpectedResponse( 342 | body: 'Hello world! (GET)', 343 | ), 344 | openUrl: (client, uri) => client.getUrl(uri), 345 | ); 346 | }); 347 | 348 | // ---- 349 | // HEAD 350 | // ---- 351 | 352 | test('client.head(...)', () async { 353 | await _testClientMethodWithoutUri( 354 | method: 'HEAD', 355 | openUrl: (client, host, port, path) => client.head(host, port, path), 356 | ); 357 | }); 358 | 359 | test('client.headUrl(...)', () async { 360 | await _testClient( 361 | request: _Request( 362 | method: 'HEAD', 363 | path: '/greeting', 364 | ), 365 | expectedResponse: _ExpectedResponse( 366 | // HEAD response doesn't have body 367 | body: '', 368 | ), 369 | openUrl: (client, uri) => client.headUrl(uri), 370 | ); 371 | }); 372 | 373 | // ----- 374 | // PATCH 375 | // ----- 376 | 377 | test('client.patch(...)', () async { 378 | await _testClientMethodWithoutUri( 379 | method: 'PATCH', 380 | openUrl: (client, host, port, path) => client.patch(host, port, path), 381 | ); 382 | }); 383 | 384 | test('client.patchUrl(...)', () async { 385 | await _testClient( 386 | request: _Request( 387 | method: 'PATCH', 388 | path: '/greeting', 389 | ), 390 | expectedResponse: _ExpectedResponse( 391 | body: 'Hello world! (PATCH)', 392 | ), 393 | openUrl: (client, uri) => client.patchUrl(uri), 394 | ); 395 | }); 396 | 397 | // ---- 398 | // POST 399 | // ---- 400 | 401 | test('client.post(...)', () async { 402 | await _testClientMethodWithoutUri( 403 | method: 'POST', 404 | openUrl: (client, host, port, path) => client.post(host, port, path), 405 | ); 406 | }); 407 | 408 | test('client.postUrl(...)', () async { 409 | await _testClient( 410 | request: _Request( 411 | method: 'POST', 412 | path: '/greeting', 413 | ), 414 | expectedResponse: _ExpectedResponse( 415 | body: 'Hello world! (POST)', 416 | ), 417 | openUrl: (client, uri) => client.postUrl(uri), 418 | ); 419 | }); 420 | 421 | // --- 422 | // PUT 423 | // --- 424 | 425 | test('client.put(...)', () async { 426 | await _testClientMethodWithoutUri( 427 | method: 'PUT', 428 | openUrl: (client, host, port, path) => client.put(host, port, path), 429 | ); 430 | }); 431 | 432 | test('client.putUrl(...)', () async { 433 | await _testClient( 434 | request: _Request( 435 | method: 'PUT', 436 | path: '/greeting', 437 | ), 438 | expectedResponse: _ExpectedResponse( 439 | body: 'Hello world! (PUT)', 440 | ), 441 | openUrl: (client, uri) => client.putUrl(uri), 442 | ); 443 | }); 444 | 445 | test('TLS connection to a self-signed server fails', () async { 446 | final client = HttpClient(); 447 | final uri = Uri.parse('https://localhost:$secureServerPort/greeting'); 448 | if (isBrowser) { 449 | // In browser, request is sent only after it's closed. 450 | final request = await client.getUrl(uri); 451 | expect(() => request.close(), throwsA(TypeMatcher())); 452 | } else { 453 | expect(() => client.getUrl(uri), 454 | throwsA(TypeMatcher())); 455 | } 456 | }); 457 | 458 | if (!isBrowser) { 459 | test( 460 | 'TLS connection to a self-signed server succeeds with' 461 | " the help of 'badCertificateCallback'", () async { 462 | final client = HttpClient(); 463 | client.badCertificateCallback = (certificate, host, port) { 464 | return true; 465 | }; 466 | final uri = Uri.parse('https://localhost:$secureServerPort/greeting'); 467 | final request = await client.getUrl(uri); 468 | final response = await request.close(); 469 | expect(response.statusCode, 200); 470 | }); 471 | } 472 | }); 473 | } 474 | 475 | Future _testClient({ 476 | HttpClient? existingHttpClient, 477 | required _Request request, 478 | _ExpectedResponse expectedResponse = const _ExpectedResponse(), 479 | 480 | /// Function for opening HTTP request 481 | Future Function(HttpClient client, Uri uri)? openUrl, 482 | 483 | /// Are we expecting XMLHttpRequest error? 484 | bool xmlHttpRequestError = false, 485 | }) async { 486 | // Send HTTP request 487 | final httpClient = existingHttpClient ?? HttpClient(); 488 | HttpClientRequest httpClientRequest; 489 | final originalUri = Uri.parse(request.path); 490 | final queryParameters = { 491 | ...originalUri.queryParameters, 492 | }; 493 | if (request.credentialsMode) { 494 | queryParameters['credentials'] = 'true'; 495 | } 496 | final uri = Uri( 497 | scheme: 'http', 498 | host: 'localhost', 499 | port: serverPort, 500 | path: originalUri.path, 501 | queryParameters: queryParameters, 502 | ); 503 | if (openUrl != null) { 504 | // Use a custom method 505 | // (we test their correctness) 506 | httpClientRequest = await openUrl( 507 | httpClient, 508 | uri, 509 | ).timeout(const Duration(seconds: 5)); 510 | } else { 511 | // Use 'openUrl' 512 | httpClientRequest = await httpClient 513 | .openUrl(request.method, uri) 514 | .timeout(const Duration(seconds: 5)); 515 | } 516 | 517 | // Set headers 518 | request.headers.forEach((name, value) { 519 | httpClientRequest.headers.set(name, value); 520 | }); 521 | 522 | if (httpClientRequest is BrowserHttpClientRequest) { 523 | httpClientRequest.browserCredentialsMode = request.credentialsMode; 524 | } 525 | 526 | // If HTTP method supports a request body, 527 | // write it. 528 | final requestBody = request.body; 529 | if (requestBody != null) { 530 | httpClientRequest.write(requestBody); 531 | } 532 | 533 | // Do we expect XMLHttpRequest error? 534 | if (xmlHttpRequestError) { 535 | expect(() => httpClientRequest.close(), 536 | throwsA(TypeMatcher())); 537 | return null; 538 | } 539 | 540 | // Close HTTP request 541 | final response = await httpClientRequest 542 | .close() 543 | .timeout(const Duration(milliseconds: 500)); 544 | final actualResponseBody = await utf8 545 | .decodeStream(response.cast>()) 546 | .timeout(const Duration(seconds: 5)); 547 | 548 | // Check response status code 549 | expect(response, isNotNull); 550 | expect(response.statusCode, expectedResponse.status); 551 | 552 | // Check response headers 553 | expect(response.headers.value('X-Response-Header'), 'value'); 554 | 555 | // Check response body 556 | final expectedBody = expectedResponse.body; 557 | if (expectedBody != null) { 558 | expect(actualResponseBody, expectedBody); 559 | } 560 | 561 | // Check the request that the server received 562 | expect(response.headers.value('X-Request-Method'), request.method); 563 | expect(response.headers.value('X-Request-Path'), uri.path); 564 | expect(response.headers.value('X-Request-Body'), requestBody ?? ''); 565 | 566 | return response; 567 | } 568 | 569 | /// Tests methods like 'client.get(host,port,path)'. 570 | /// 571 | /// These should default to TLS. 572 | Future _testClientMethodWithoutUri({ 573 | required String method, 574 | required OpenUrlFunction openUrl, 575 | }) async { 576 | // Create a HTTP client 577 | final client = HttpClient(); 578 | 579 | // Create a HTTP request 580 | final host = 'localhost'; 581 | final path = '/greeting'; 582 | final request = await openUrl(client, host, serverPort, path); 583 | 584 | // Test that the request seems correct 585 | expect(request.uri.scheme, 'http'); 586 | expect(request.uri.host, 'localhost'); 587 | expect(request.uri.port, serverPort); 588 | expect(request.uri.path, path); 589 | 590 | // Close request 591 | final response = await request.close(); 592 | await utf8.decodeStream(response.cast>()); 593 | expect(response.statusCode, 200); 594 | } 595 | 596 | typedef OpenUrlFunction = Future Function( 597 | HttpClient client, 598 | String host, 599 | int port, 600 | String path, 601 | ); 602 | 603 | class _ExpectedResponse { 604 | final int status; 605 | final String? body; 606 | 607 | const _ExpectedResponse({ 608 | this.status = 200, 609 | this.body, 610 | }); 611 | } 612 | 613 | class _Request { 614 | final String method; 615 | final String path; 616 | final Map headers; 617 | final String? body; 618 | final bool credentialsMode; 619 | 620 | const _Request({ 621 | required this.method, 622 | required this.path, 623 | this.headers = const {}, 624 | this.body, 625 | this.credentialsMode = false, 626 | }); 627 | } 628 | -------------------------------------------------------------------------------- /test/internet_address_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 terrier989@gmail.com. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'dart:typed_data'; 16 | 17 | import 'package:test/test.dart'; 18 | import 'package:universal_io/io.dart'; 19 | 20 | void main() { 21 | group('InternetAddress', () { 22 | test('== / hashCode', () { 23 | final value = InternetAddress('10.0.0.1'); 24 | final clone = InternetAddress('10.0.0.1'); 25 | final other = InternetAddress('10.0.0.2'); 26 | expect(value, clone); 27 | expect(value, isNot(other)); 28 | expect(value.hashCode, clone.hashCode); 29 | expect(value.hashCode, isNot(other.hashCode)); 30 | }); 31 | 32 | test('InternetAddress.loopbackIPv4', () { 33 | expect(InternetAddress.loopbackIPv4.address, '127.0.0.1'); 34 | }); 35 | 36 | test('InternetAddress.loopbackIPv6', () { 37 | expect(InternetAddress.loopbackIPv6.address, '::1'); 38 | }); 39 | 40 | group('InternetAddress(...) when IPv4', () { 41 | test('255.0.254.1', () { 42 | final address = InternetAddress('255.0.254.1'); 43 | expect(address.address, '255.0.254.1'); 44 | expect(address.host, '255.0.254.1'); 45 | expect(address.rawAddress, [255, 0, 254, 1]); 46 | }); 47 | 48 | test('255.0.254.1, type: InternetAddressType.IPv4', () { 49 | final address = InternetAddress( 50 | '255.0.254.1', 51 | type: InternetAddressType.IPv4, 52 | ); 53 | expect(address.address, '255.0.254.1'); 54 | expect(address.host, '255.0.254.1'); 55 | expect(address.rawAddress, [255, 0, 254, 1]); 56 | }); 57 | 58 | test('255.0.254.1, type: InternetAddressType.IPv6', () { 59 | final address = InternetAddress( 60 | '255.0.254.1', 61 | type: InternetAddressType.IPv6, 62 | ); 63 | expect(address.address, '255.0.254.1'); 64 | expect(address.host, '255.0.254.1'); 65 | expect(address.rawAddress, [255, 0, 254, 1]); 66 | }); 67 | }); 68 | 69 | group('InternetAddress(...) when IPv6', () { 70 | test("'0123:4567:89ab:cdef:0123:4567:89ab:cdef'", () { 71 | final actual = InternetAddress( 72 | '0123:4567:89ab:cdef:0123:4567:89ab:cdef', 73 | ).rawAddress; 74 | final expected = Uint8List(16); 75 | expected[0] = 0x01; 76 | expected[1] = 0x23; 77 | expected[2] = 0x45; 78 | expected[3] = 0x67; 79 | expected[4] = 0x89; 80 | expected[5] = 0xAB; 81 | expected[6] = 0xCD; 82 | expected[7] = 0xEF; 83 | expected[8] = 0x01; 84 | expected[9] = 0x23; 85 | expected[10] = 0x45; 86 | expected[11] = 0x67; 87 | expected[12] = 0x89; 88 | expected[13] = 0xAB; 89 | expected[14] = 0xCD; 90 | expected[15] = 0xEF; 91 | expect(actual, orderedEquals(expected)); 92 | }); 93 | 94 | test("'::'", () { 95 | final actual = InternetAddress('::').rawAddress; 96 | final expected = Uint8List(16); 97 | expect(actual, orderedEquals(expected)); 98 | }); 99 | 100 | test("'1::'", () { 101 | final actual = InternetAddress('1::').rawAddress; 102 | final expected = Uint8List(16); 103 | expected[1] = 1; 104 | expect(actual, orderedEquals(expected)); 105 | }); 106 | 107 | test("'::1'", () { 108 | final actual = InternetAddress('::1').rawAddress; 109 | final expected = Uint8List(16); 110 | expected[15] = 1; 111 | expect(actual, orderedEquals(expected)); 112 | }); 113 | 114 | test("'abcd:ef01::'", () { 115 | final actual = InternetAddress('abcd:ef01::').rawAddress; 116 | final expected = Uint8List(16); 117 | expected[0] = 0xAB; 118 | expected[1] = 0xCD; 119 | expected[2] = 0xEF; 120 | expected[3] = 0x01; 121 | expect(actual, orderedEquals(expected)); 122 | }); 123 | 124 | test("'::abcd:ef01'", () { 125 | final actual = InternetAddress('::abcd:ef01').rawAddress; 126 | final expected = Uint8List(16); 127 | expected[12] = 0xAB; 128 | expected[13] = 0xCD; 129 | expected[14] = 0xEF; 130 | expected[15] = 0x01; 131 | expect(actual, orderedEquals(expected)); 132 | }); 133 | }); 134 | 135 | test('address (IPv4)', () { 136 | expect(InternetAddress('0.1.2.9').address, '0.1.2.9'); 137 | }); 138 | 139 | group('address (IPv6)', () { 140 | test("'0123:4567:89ab:cdef:0123:4567:89ab:cdef'", () { 141 | expect( 142 | InternetAddress('0123:4567:89ab:cdef:0123:4567:89ab:cdef').address, 143 | '0123:4567:89ab:cdef:0123:4567:89ab:cdef', 144 | ); 145 | }); 146 | 147 | test("'::'", () { 148 | expect(InternetAddress('::').address, '::'); 149 | }); 150 | 151 | test("'1::'", () { 152 | expect(InternetAddress('1::').address, '1::'); 153 | }); 154 | 155 | test("'::1'", () { 156 | expect(InternetAddress('::1').address, '::1'); 157 | }); 158 | 159 | test("'1::1'", () { 160 | expect(InternetAddress('1::1').address, '1::1'); 161 | }); 162 | 163 | test("'1:0:0:2::3:0:0:4'", () { 164 | expect(InternetAddress('1:0:2::3:0:4').address, '1:0:2::3:0:4'); 165 | }); 166 | 167 | test("'1:2:3:4:5:6:ff00:0'", () { 168 | expect(InternetAddress('1:2:3:4:5:6:ff00:0').address, 169 | '1:2:3:4:5:6:ff00:0'); 170 | }); 171 | 172 | test("'1:2:3:4:5:ff00:0:0'", () { 173 | expect(InternetAddress('1:2:3:4:5:ff00:0:0').address, 174 | '1:2:3:4:5:ff00:0:0'); 175 | }); 176 | }); 177 | 178 | test('host', () { 179 | expect(InternetAddress('0.1.2.9').host, '0.1.2.9'); 180 | expect(InternetAddress('::').host, '::'); 181 | expect( 182 | InternetAddress('/abc', type: InternetAddressType.unix).host, '/abc'); 183 | }); 184 | 185 | test('type', () { 186 | expect(InternetAddress('0.1.2.9').type, InternetAddressType.IPv4); 187 | expect(InternetAddress('::').type, InternetAddressType.IPv6); 188 | expect(InternetAddress('/abc', type: InternetAddressType.unix).type, 189 | InternetAddressType.unix); 190 | }); 191 | 192 | test('isLoopback', () { 193 | // False 194 | expect(InternetAddress('8.8.8.8').isLoopback, isFalse); 195 | expect(InternetAddress('10.0.0.0').isLoopback, isFalse); 196 | expect(InternetAddress('::').isLoopback, isFalse); 197 | expect(InternetAddress('/abc', type: InternetAddressType.unix).isLoopback, 198 | isFalse); 199 | 200 | // True 201 | expect(InternetAddress('127.0.0.1').isLoopback, isTrue); 202 | expect(InternetAddress('::1').isLoopback, isTrue); 203 | }); 204 | 205 | test('rawAddress (IPv4)', () { 206 | expect(InternetAddress('10.0.0.1').rawAddress, [10, 0, 0, 1]); 207 | }); 208 | }); 209 | } 210 | -------------------------------------------------------------------------------- /test/localhost.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC5TCCAc2gAwIBAgIJAOaFttz6Ia95MA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV 3 | BAMMCWxvY2FsaG9zdDAeFw0xOTA1MDExMjU4MjNaFw0xOTA1MzExMjU4MjNaMBQx 4 | EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC 5 | ggEBAObE/j83wEHL7X/4CvFZMgOi032W8AaXiA9v6oy1SIWyFbckw+UNxdSeF1vg 6 | GUQQ2mZdSbnV3pFAyehAxj6QgK4G2PLaVHd4trx2AbhvsFCIayTuloeGZ3bEvMRe 7 | 2snuwlV9Fs5xTPTNHQFRSC45oxFgd1kaUzQYyr4WWbCjOrZyMuAe91tHnz+Ij5sz 8 | BhLm7nwcpFMjyfAnVSSbu74HU1S7sr2JYaT9af7GuMLajwBtFNieI1x4ZEamJkE7 9 | kYyFU+gBudFSSbwm8GXLQRkYV849I77J42OsnaAefxWwEoryiASmRjIC9QpqLPjB 10 | wN59ZPrfemUtSdjpq+zuX3npZp8CAwEAAaM6MDgwFAYDVR0RBA0wC4IJbG9jYWxo 11 | b3N0MAsGA1UdDwQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDATANBgkqhkiG9w0B 12 | AQsFAAOCAQEA40p9AuQchiG9dwhrGwa/LSrpCTKv4jBcZYMjHV+ERfrbfhu67KX8 13 | HoiiyZvHSF8Jm3Q4FppZ2qL6AD+wz/ylVle5Nu+emVHZxhg4hZF6p/ORYoHVsXkM 14 | 3SlH60yIl5bnMknUrY9feHY8UcpqzZlRPFfh82hxgu1IPFTUn2bTs15s/NPuxj4O 15 | 0AEtzQ3BIaYmXae8dvPpDyEmFgV6pDJkUSxwAy0LmNb5Je+qkniUj2qYyZOcXQvX 16 | z6L7WRDOqCsm3krDgxkj1cOGQgv09B/cOS4jVdoYfFsrgiOiFDsPsTWnt9oKEeME 17 | qZ1I+8CAa8rK6BZ3gLXHkUcsgWD4/p2TOA== 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /test/localhost.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDmxP4/N8BBy+1/ 3 | +ArxWTIDotN9lvAGl4gPb+qMtUiFshW3JMPlDcXUnhdb4BlEENpmXUm51d6RQMno 4 | QMY+kICuBtjy2lR3eLa8dgG4b7BQiGsk7paHhmd2xLzEXtrJ7sJVfRbOcUz0zR0B 5 | UUguOaMRYHdZGlM0GMq+Flmwozq2cjLgHvdbR58/iI+bMwYS5u58HKRTI8nwJ1Uk 6 | m7u+B1NUu7K9iWGk/Wn+xrjC2o8AbRTYniNceGRGpiZBO5GMhVPoAbnRUkm8JvBl 7 | y0EZGFfOPSO+yeNjrJ2gHn8VsBKK8ogEpkYyAvUKaiz4wcDefWT633plLUnY6avs 8 | 7l956WafAgMBAAECggEAW11k9+x/vs0ZY8/1rSZOSeg+aXvH3iRCZzI9H6ZqZxxh 9 | wpfYwygXyh0pTGPGPQ+bGGoY1W2aZ9HJ3p0+n+igOcZWQJg2VvLBDo5+EeKOYOQR 10 | 35ZinJeXAQvVXL3dS5Ou9x/GVse+2zEWKb5foIWLTUbvGaT1iivCoU3CBtApX6/I 11 | hD6BfFiA1xtM1h0Fb0oDsL138mt/PGWm1uoD9VxQN4+lSwriN0FaBzJ8xv1BM80Q 12 | M5O5tL7fPPkemwXVtTN8e/K/QBdg6vbkRd32AU4vazoH6iEogVv4wOGwhRINAzqN 13 | Fv0xxsv+Eup+YeIYfAltoj7Zp3N4Mlxh08pkjABPOQKBgQDzZT3C7pphWg+KgZsX 14 | Q2An0GQ9QLO2oRra3/CI0xNeEiI17LH95gA0bf3LC0EOCZxfc3cC1OgtmTROFr7C 15 | DYlAjsqtamo5XHL+Yo1lzf+O6YiZYNbZf1nOmjnyTBo/TVzOtDxXM4pkw+QTBCGY 16 | 7/Fdj9iPqd6n93nzk0jL6tmFIwKBgQDyuF21uTCR3zxh5eLDWMB+cTi/AFQtUTr0 17 | i48uEdpb+B7XaTHKq2io8jWL6wW0iNffF/CupgFC7E8cA860BKQcblimpxwl2/Nf 18 | jOIzRKmBfzxyaP0vTLa2oAx+l53qKDqL7Ijn9Z+JXrhd+EqIbC9UK8UFaDZV0DlT 19 | VnAlsa8mVQKBgD0hVF29J7EDuZuD6bvyBBh70nE/6uMXm3MVg4gZ22dpDoaUqC+o 20 | sKx6Y5+3d+NarpeanG1to9KwA98I/2glli9MrcLYU02M1qTlg7XqAyGVreU32T5P 21 | KfMl8R/V1VHI0GJqCi2smDmuqX3NL9MFkI17L10FwRJWqZ5vg30TwwYZAoGAbUb4 22 | WdKwvVwmUWUwvQZ+U+8hv0ykmWqTAnq60eET0IhbSlyAIGEf08CIvx/nB6r8leKv 23 | Z9IxizHdRB2quH3GbU549z8RezIV1pUVWkO1lSn1ywTdyKffM0XHmk8kt8G46jpq 24 | QRTi6PQOSbB5zgX3IQw7vf13SRdgV9b40t+2nakCgYBxPNaVnPCtZS31J8QW65yG 25 | 5Ja7eF+bW0xW4H4TOux80zLSlCPqsmVaZf9rk5h/z+c8Bz0SCk4cj8XsNgVNrABe 26 | 66auMVAh4ygKIb4SJthmm9NqW2ssNmLwmw8tDGrAgrJC44Tf6jIPKsNvJtN0PJlQ 27 | VxznSBAXNKhGF/2W7e9w+g== 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /test/platform_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 terrier989@gmail.com. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | @TestOn('chrome') 16 | import 'dart:html' hide Platform; 17 | 18 | import 'package:test/test.dart'; 19 | import 'package:universal_io/io.dart'; 20 | 21 | void main() { 22 | if (window.navigator.languages!.isNotEmpty) { 23 | final locale = window.navigator.languages!.first; 24 | test("Platform.localeName == '$locale'", () { 25 | expect(Platform.localeName, locale); 26 | }); 27 | } 28 | 29 | final userAgent = window.navigator.userAgent.toLowerCase(); 30 | if (userAgent.contains('mac os x')) { 31 | test('Platform.isMacOS == true', () { 32 | expect(Platform.isMacOS, true); 33 | }); 34 | test('Platform.isWindows == false', () { 35 | expect(Platform.isWindows, false); 36 | }); 37 | test('Platform.isLinux == false', () { 38 | expect(Platform.isLinux, false); 39 | }); 40 | test('Platform.operatingSystemVersion', () { 41 | expect(Platform.operatingSystemVersion, isNotEmpty); 42 | }); 43 | } else if (userAgent.contains('windows')) { 44 | test('Platform.isMacOS == false', () { 45 | expect(Platform.isMacOS, false); 46 | }); 47 | test('Platform.isWindows == true', () { 48 | expect(Platform.isWindows, true); 49 | }); 50 | test('Platform.isLinux == false', () { 51 | expect(Platform.isLinux, false); 52 | }); 53 | test('Platform.operatingSystemVersion', () { 54 | expect(Platform.operatingSystemVersion, isNotEmpty); 55 | }); 56 | } else if (userAgent.contains('linux')) { 57 | test('Platform.isMacOS == false', () { 58 | expect(Platform.isMacOS, false); 59 | }); 60 | test('Platform.isWindows == false', () { 61 | expect(Platform.isWindows, false); 62 | }); 63 | test('Platform.isLinux == true', () { 64 | expect(Platform.isLinux, true); 65 | }); 66 | test('Platform.operatingSystemVersion', () { 67 | expect(Platform.operatingSystemVersion, isNotEmpty); 68 | }); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /test/server.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2020 terrier989@gmail.com. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'dart:async'; 16 | import 'dart:convert'; 17 | import 'dart:io'; 18 | 19 | import 'package:stream_channel/stream_channel.dart'; 20 | 21 | void hybridMain(StreamChannel streamChannel, Object message) async { 22 | final securityContext = SecurityContext(); 23 | const testSuitePath = 'test'; 24 | securityContext.useCertificateChain( 25 | '$testSuitePath/localhost.crt', 26 | ); 27 | securityContext.usePrivateKey( 28 | '$testSuitePath/localhost.key', 29 | ); 30 | 31 | final server = await HttpServer.bind( 32 | 'localhost', 33 | 0, 34 | ); 35 | print('Server #1 is listening at: http://localhost:${server.port}/'); 36 | streamChannel.sink.add(server.port); 37 | 38 | final secureServer = await HttpServer.bindSecure( 39 | 'localhost', 40 | 0, 41 | securityContext, 42 | ); 43 | print('Server #2 is listening at: https://localhost:${secureServer.port}/'); 44 | streamChannel.sink.add(secureServer.port); 45 | 46 | try { 47 | final f0 = server.listen(_handleHttpRequest).asFuture(); 48 | final f1 = secureServer.listen(_handleHttpRequest).asFuture(); 49 | await Future.wait([f0, f1]); 50 | } finally { 51 | await Future.wait([server.close(), secureServer.close()]); 52 | } 53 | } 54 | 55 | void _handleHttpRequest(HttpRequest request) async { 56 | // Respond based on the path 57 | final requestBody = await utf8.decodeStream(request); 58 | final response = request.response; 59 | try { 60 | // Check that the request is from loopback 61 | if (!request.connectionInfo!.remoteAddress.isLoopback) { 62 | throw StateError('Unauthorized remote address'); 63 | } 64 | final origin = request.headers.value('Origin') ?? '*'; 65 | final userAgent = request.headers.value('User-Agent') ?? ''; 66 | if (origin == '*' && !userAgent.contains('Dart')) { 67 | print('INVALID ORIGIN: $origin'); 68 | } 69 | response.headers.set( 70 | 'Access-Control-Allow-Origin', 71 | '*', 72 | ); 73 | response.headers.set( 74 | 'Access-Control-Allow-Methods', 75 | '*', 76 | ); 77 | response.headers.set( 78 | 'Access-Control-Expose-Headers', 79 | '*', 80 | ); 81 | final isCredentialsMode = 82 | request.uri.queryParameters['credentials'] == 'true'; 83 | if (isCredentialsMode) { 84 | response.headers.set( 85 | 'Access-Control-Allow-Origin', 86 | origin, 87 | ); 88 | response.headers.set( 89 | 'Access-Control-Allow-Credentials', 90 | 'true', 91 | ); 92 | response.headers.set( 93 | 'Access-Control-Allow-Methods', 94 | 'DELETE, GET, HEAD, PATCH, POST, PUT', 95 | ); 96 | response.headers.set( 97 | 'Access-Control-Expose-Headers', 98 | 'X-Request-Method, X-Request-Path, X-Request-Body, X-Response-Header', 99 | ); 100 | } 101 | response.headers.set('X-Request-Method', request.method); 102 | response.headers.set('X-Request-Path', request.uri.path); 103 | response.headers.set('X-Request-Body', requestBody); 104 | response.headers.set('X-Response-Header', 'value'); 105 | response.headers.contentType = ContentType.text; 106 | 107 | switch (request.uri.path) { 108 | case '/greeting': 109 | response.statusCode = HttpStatus.ok; 110 | response.write('Hello world! (${request.method})'); 111 | break; 112 | 113 | case '/slow': 114 | response.bufferOutput = false; 115 | response.statusCode = HttpStatus.ok; 116 | response.headers.set('Cache-Control', 'no-cache'); 117 | response.headers.chunkedTransferEncoding = true; 118 | response.writeln('First part.'); 119 | await response.flush(); 120 | await Future.delayed(const Duration(milliseconds: 500)); 121 | 122 | response.writeln('Second part.'); 123 | await response.flush(); 124 | await Future.delayed(const Duration(milliseconds: 500)); 125 | break; 126 | 127 | case '/set_cookie': 128 | final name = request.uri.queryParameters['name']!; 129 | final value = request.uri.queryParameters['value']!; 130 | response.statusCode = HttpStatus.ok; 131 | response.cookies.add(Cookie(name, value)); 132 | break; 133 | 134 | case '/expect_cookie': 135 | final name = request.uri.queryParameters['name']!; 136 | final value = request.uri.queryParameters['value']!; 137 | // Not tested in browser 138 | final ok = request.cookies.any( 139 | (cookie) => cookie.name == name && cookie.value == value, 140 | ); 141 | if (ok) { 142 | response.statusCode = HttpStatus.ok; 143 | } else { 144 | response.statusCode = HttpStatus.unauthorized; 145 | } 146 | break; 147 | 148 | case '/expect_authorization': 149 | response.headers.set( 150 | 'Access-Control-Allow-Credentials', 151 | 'true', 152 | ); 153 | response.headers.set( 154 | 'Access-Control-Allow-Headers', 155 | 'Authorization', 156 | ); 157 | 158 | // Is this a preflight? 159 | if (request.method == 'OPTIONS') { 160 | response.statusCode = HttpStatus.ok; 161 | return; 162 | } 163 | 164 | final authorization = 165 | request.headers.value(HttpHeaders.authorizationHeader); 166 | 167 | if (authorization == 'expectedAuthorization') { 168 | response.statusCode = HttpStatus.ok; 169 | } else { 170 | response.statusCode = HttpStatus.unauthorized; 171 | } 172 | response.write(authorization); 173 | break; 174 | 175 | case '/404': 176 | response.statusCode = HttpStatus.notFound; 177 | break; 178 | 179 | default: 180 | response.statusCode = HttpStatus.internalServerError; 181 | response.write("Invalid path '${request.uri.path}'"); 182 | break; 183 | } 184 | } finally { 185 | await response.close(); 186 | } 187 | } 188 | --------------------------------------------------------------------------------