├── .gitattributes ├── lib ├── okhttp3 │ ├── foundation │ │ ├── basic_types.dart │ │ └── character.dart │ ├── proxy.dart │ ├── interceptor.dart │ ├── internal │ │ ├── version.dart │ │ ├── encoding_util.dart │ │ ├── http │ │ │ ├── http_method.dart │ │ │ ├── call_server_interceptor.dart │ │ │ └── bridge_interceptor.dart │ │ ├── util.dart │ │ ├── cache │ │ │ ├── disk_cache.dart │ │ │ ├── cache_interceptor.dart │ │ │ └── cache_strategy.dart │ │ └── http_extension.dart │ ├── cookie_jar.dart │ ├── chain.dart │ ├── tools │ │ ├── user_agent_interceptor.dart │ │ ├── optimized_response_interceptor.dart │ │ ├── curl_interceptor.dart │ │ ├── progress_interceptor.dart │ │ ├── optimized_request_interceptor.dart │ │ ├── http_logging_interceptor.dart │ │ └── persistent_cookie_jar.dart │ ├── call.dart │ ├── response_body.dart │ ├── media_type.dart │ ├── form_body.dart │ ├── request_body.dart │ ├── request.dart │ ├── headers.dart │ ├── http_url.dart │ ├── response.dart │ ├── okhttp_client.dart │ ├── multipart_body.dart │ ├── cache_control.dart │ └── cache.dart └── okhttp_kit.dart ├── .gitignore ├── CHANGELOG.md ├── pubspec.yaml ├── README.md ├── test └── okhttp_kit_test.dart ├── .drone.yml ├── analysis_options.yaml └── LICENSE /.gitattributes: -------------------------------------------------------------------------------- 1 | * linguist-language=Dart -------------------------------------------------------------------------------- /lib/okhttp3/foundation/basic_types.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | typedef FutureOr AsyncValueGetter(); 4 | -------------------------------------------------------------------------------- /lib/okhttp3/proxy.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | typedef String Proxy(Uri url); 4 | 5 | abstract class ProxySelector { 6 | FutureOr select(); 7 | } 8 | -------------------------------------------------------------------------------- /lib/okhttp3/interceptor.dart: -------------------------------------------------------------------------------- 1 | import 'package:okhttp_kit/okhttp3/chain.dart'; 2 | import 'package:okhttp_kit/okhttp3/response.dart'; 3 | 4 | abstract class Interceptor { 5 | Future intercept(Chain chain); 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Files and directories created by pub 2 | .dart_tool/ 3 | .packages 4 | # Remove the following pattern if you wish to check in your lock file 5 | pubspec.lock 6 | 7 | # Conventional directory for build outputs 8 | build/ 9 | 10 | # Directory created by dartdoc 11 | doc/api/ 12 | 13 | # 14 | *.iml 15 | .idea/ 16 | -------------------------------------------------------------------------------- /lib/okhttp3/internal/version.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | class Version { 4 | Version._(); 5 | 6 | static String userAgent() { 7 | String version = Platform.version; 8 | // Only include major and minor version numbers. 9 | int index = version.indexOf('.', version.indexOf('.') + 1); 10 | version = version.substring(0, index); 11 | return 'okhttp/3.10.0 Dart/$version (dart:io)'; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.2 2 | 3 | - 修正 MultipartBody 4 | 5 | ## 1.0.1 6 | 7 | - 优化 8 | 9 | ## 1.0.0 10 | 11 | - 更名 okhttp_kit 12 | 13 | ## 0.2.1 14 | 15 | - 优化 16 | 17 | ## 0.2.0 18 | 19 | - 简化依赖 20 | 21 | ## 0.1.4 22 | 23 | - 优化 24 | 25 | ## 0.1.3 26 | 27 | - 优化 28 | 29 | ## 0.1.2 30 | 31 | - curl add headerFilter 32 | 33 | ## 0.1.1 34 | 35 | - bugfix 36 | - curl 37 | 38 | ## 0.1.0 39 | 40 | - dart - okhttp 3.10.0 41 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: okhttp_kit 2 | description: A powerful http package for Dart/Flutter. 3 | version: 1.0.2 4 | author: v7lin 5 | homepage: https://github.com/v7lin/fake_okhttp 6 | 7 | environment: 8 | sdk: '>=2.3.0 <3.0.0' 9 | 10 | dependencies: 11 | charcode: ^1.1.2 12 | convert: ^2.1.1 13 | crypto: ^2.0.6 14 | fixnum: ^0.10.9 15 | path: ^1.6.2 16 | 17 | dev_dependencies: 18 | test: ^1.0.0 19 | 20 | pedantic: 21 | -------------------------------------------------------------------------------- /lib/okhttp3/internal/encoding_util.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:okhttp_kit/okhttp3/media_type.dart'; 4 | 5 | class EncodingUtil { 6 | EncodingUtil._(); 7 | 8 | static Encoding encoding( 9 | MediaType contentType, [ 10 | Encoding defaultValue = utf8, 11 | ]) { 12 | Encoding encoding; 13 | if (contentType != null && contentType.charset() != null) { 14 | encoding = Encoding.getByName(contentType.charset()); 15 | } 16 | if (encoding == null) { 17 | encoding = defaultValue; 18 | } 19 | return encoding; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/okhttp3/cookie_jar.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:okhttp_kit/okhttp3/http_url.dart'; 5 | 6 | abstract class CookieJar { 7 | static const CookieJar noCookies = _NoCookieJar(); 8 | 9 | Future saveFromResponse(HttpUrl url, List cookies); 10 | 11 | Future> loadForRequest(HttpUrl url); 12 | } 13 | 14 | class _NoCookieJar implements CookieJar { 15 | const _NoCookieJar(); 16 | 17 | @override 18 | Future saveFromResponse(HttpUrl url, List cookies) async {} 19 | 20 | @override 21 | Future> loadForRequest(HttpUrl url) async { 22 | return List.unmodifiable([]); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/okhttp3/foundation/character.dart: -------------------------------------------------------------------------------- 1 | class Character { 2 | Character._(); 3 | 4 | static bool isIsoControl(int rune) { 5 | return (rune >= 0x00 && rune <= 0x1F) || (rune >= 0x7F && rune <= 0x9F); 6 | } 7 | 8 | static bool isWhitespace(int rune) { 9 | return (rune >= 0x0009 && rune <= 0x000D) || 10 | rune == 0x0020 || 11 | rune == 0x0085 || 12 | rune == 0x00A0 || 13 | rune == 0x1680 || 14 | rune == 0x180E || 15 | (rune >= 0x2000 && rune <= 0x200A) || 16 | rune == 0x2028 || 17 | rune == 0x2029 || 18 | rune == 0x202F || 19 | rune == 0x205F || 20 | rune == 0x3000 || 21 | rune == 0xFEFF; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/okhttp3/internal/http/http_method.dart: -------------------------------------------------------------------------------- 1 | class HttpMethod { 2 | HttpMethod._(); 3 | 4 | static const String head = 'HEAD'; 5 | static const String get = 'GET'; 6 | static const String post = 'POST'; 7 | static const String put = 'PUT'; 8 | static const String patch = 'PATCH'; 9 | static const String delete = 'DELETE'; 10 | 11 | static bool invalidatesCache(String method) { 12 | return method == post || 13 | method == patch || 14 | method == put || 15 | method == delete; 16 | } 17 | 18 | static bool requiresRequestBody(String method) { 19 | return method == post || method == put || method == patch; 20 | } 21 | 22 | static bool permitsRequestBody(String method) { 23 | return !(method == get || method == head); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # okhttp_kit 2 | 3 | [![Build Status](https://cloud.drone.io/api/badges/v7lin/okhttp_kit/status.svg)](https://cloud.drone.io/v7lin/okhttp_kit) 4 | [![GitHub Tag](https://img.shields.io/github/tag/v7lin/okhttp_kit.svg)](https://github.com/v7lin/okhttp_kit/releases) 5 | [![Pub Package](https://img.shields.io/pub/v/okhttp_kit.svg)](https://pub.dartlang.org/packages/okhttp_kit) 6 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/v7lin/okhttp_kit/blob/master/LICENSE) 7 | 8 | dart版okhttp3 9 | 10 | ## Usage 11 | 12 | 还是原来的配方,还是熟悉的味道 13 | 14 | ```dart 15 | import 'package:okhttp_kit/okhttp_kit.dart'; 16 | ``` 17 | 18 | ## Features and bugs 19 | 20 | Please file feature requests and bugs at the [issue tracker][tracker]. 21 | 22 | [tracker]: https://github.com/v7lin/okhttp_kit/issues 23 | -------------------------------------------------------------------------------- /lib/okhttp3/chain.dart: -------------------------------------------------------------------------------- 1 | import 'package:okhttp_kit/okhttp3/interceptor.dart'; 2 | import 'package:okhttp_kit/okhttp3/request.dart'; 3 | import 'package:okhttp_kit/okhttp3/response.dart'; 4 | 5 | abstract class Chain { 6 | Request request(); 7 | 8 | Future proceed(Request request); 9 | } 10 | 11 | class RealInterceptorChain implements Chain { 12 | RealInterceptorChain( 13 | List interceptors, 14 | int index, 15 | Request request, 16 | ) : _interceptors = interceptors, 17 | _index = index, 18 | _request = request; 19 | 20 | final List _interceptors; 21 | final int _index; 22 | final Request _request; 23 | 24 | @override 25 | Request request() { 26 | return _request; 27 | } 28 | 29 | @override 30 | Future proceed(Request request) { 31 | if (_index >= _interceptors.length) { 32 | throw AssertionError(); 33 | } 34 | RealInterceptorChain next = 35 | RealInterceptorChain(_interceptors, _index + 1, request); 36 | Interceptor interceptor = _interceptors[_index]; 37 | return interceptor.intercept(next); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/okhttp3/tools/user_agent_interceptor.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:okhttp_kit/okhttp3/chain.dart'; 5 | import 'package:okhttp_kit/okhttp3/foundation/basic_types.dart'; 6 | import 'package:okhttp_kit/okhttp3/interceptor.dart'; 7 | import 'package:okhttp_kit/okhttp3/request.dart'; 8 | import 'package:okhttp_kit/okhttp3/response.dart'; 9 | 10 | /// 应用层拦截器 11 | class UserAgentInterceptor implements Interceptor { 12 | UserAgentInterceptor( 13 | AsyncValueGetter userAgent, 14 | ) : assert(userAgent != null), 15 | _userAgent = userAgent; 16 | 17 | final AsyncValueGetter _userAgent; 18 | 19 | @override 20 | Future intercept(Chain chain) async { 21 | Request originalRequest = chain.request(); 22 | String userAgent = originalRequest.header(HttpHeaders.userAgentHeader); 23 | if (userAgent != null) { 24 | return await chain.proceed(originalRequest); 25 | } 26 | Response response = await chain.proceed(originalRequest 27 | .newBuilder() 28 | .header(HttpHeaders.userAgentHeader, await _userAgent()) 29 | .build()); 30 | return response.newBuilder().request(originalRequest).build(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/okhttp_kit.dart: -------------------------------------------------------------------------------- 1 | library okhttp_kit; 2 | 3 | export 'dart:io' 4 | show 5 | HttpClient, 6 | HttpStatus, 7 | HttpHeaders, 8 | HeaderValue, 9 | ContentType, 10 | Cookie, 11 | HttpDate, 12 | HttpException, 13 | RedirectException; 14 | 15 | export 'okhttp3/cache.dart'; 16 | export 'okhttp3/cache_control.dart'; 17 | export 'okhttp3/chain.dart'; 18 | export 'okhttp3/cookie_jar.dart'; 19 | export 'okhttp3/form_body.dart'; 20 | export 'okhttp3/headers.dart'; 21 | export 'okhttp3/http_url.dart'; 22 | export 'okhttp3/interceptor.dart'; 23 | export 'okhttp3/internal/cache/cache_strategy.dart'; 24 | export 'okhttp3/internal/cache/disk_cache.dart'; 25 | export 'okhttp3/internal/encoding_util.dart'; 26 | export 'okhttp3/internal/http/http_method.dart'; 27 | export 'okhttp3/internal/util.dart'; 28 | export 'okhttp3/internal/version.dart'; 29 | export 'okhttp3/media_type.dart'; 30 | export 'okhttp3/multipart_body.dart'; 31 | export 'okhttp3/okhttp_client.dart'; 32 | export 'okhttp3/proxy.dart'; 33 | export 'okhttp3/request.dart'; 34 | export 'okhttp3/request_body.dart'; 35 | export 'okhttp3/response.dart'; 36 | export 'okhttp3/response_body.dart'; 37 | export 'okhttp3/tools/curl_interceptor.dart'; 38 | export 'okhttp3/tools/http_logging_interceptor.dart'; 39 | export 'okhttp3/tools/optimized_request_interceptor.dart'; 40 | export 'okhttp3/tools/optimized_response_interceptor.dart'; 41 | export 'okhttp3/tools/persistent_cookie_jar.dart'; 42 | export 'okhttp3/tools/progress_interceptor.dart'; 43 | export 'okhttp3/tools/user_agent_interceptor.dart'; 44 | -------------------------------------------------------------------------------- /lib/okhttp3/call.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:okhttp_kit/okhttp3/chain.dart'; 4 | import 'package:okhttp_kit/okhttp3/interceptor.dart'; 5 | import 'package:okhttp_kit/okhttp3/internal/cache/cache_interceptor.dart'; 6 | import 'package:okhttp_kit/okhttp3/internal/http/bridge_interceptor.dart'; 7 | import 'package:okhttp_kit/okhttp3/internal/http/call_server_interceptor.dart'; 8 | import 'package:okhttp_kit/okhttp3/okhttp_client.dart'; 9 | import 'package:okhttp_kit/okhttp3/request.dart'; 10 | import 'package:okhttp_kit/okhttp3/response.dart'; 11 | 12 | abstract class Call { 13 | Future enqueue(); 14 | 15 | void cancel(Object error, [StackTrace stackTrace]); 16 | } 17 | 18 | class RealCall implements Call { 19 | RealCall.newRealCall(OkHttpClient client, Request originalRequest) 20 | : _client = client, 21 | _originalRequest = originalRequest; 22 | 23 | final OkHttpClient _client; 24 | final Request _originalRequest; 25 | final Completer _completer = Completer(); 26 | 27 | @override 28 | Future enqueue() { 29 | List interceptors = []; 30 | interceptors.addAll(_client.interceptors()); 31 | interceptors.add(BridgeInterceptor(_client.cookieJar())); 32 | interceptors.add(CacheInterceptor(_client.cache())); 33 | interceptors.addAll(_client.networkInterceptors()); 34 | interceptors.add(CallServerInterceptor(_client)); 35 | Chain chain = RealInterceptorChain(interceptors, 0, _originalRequest); 36 | _completer.complete(chain.proceed(_originalRequest)); 37 | return _completer.future; 38 | } 39 | 40 | @override 41 | void cancel(Object error, [StackTrace stackTrace]) { 42 | _completer.completeError(error, stackTrace); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/okhttp3/response_body.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | 4 | import 'package:okhttp_kit/okhttp3/internal/encoding_util.dart'; 5 | import 'package:okhttp_kit/okhttp3/internal/util.dart'; 6 | import 'package:okhttp_kit/okhttp3/media_type.dart'; 7 | 8 | abstract class ResponseBody { 9 | MediaType contentType(); 10 | 11 | int contentLength(); 12 | 13 | Stream> source(); 14 | 15 | Future> bytes() { 16 | return Util.readAsBytes(source()); 17 | } 18 | 19 | Future string() async { 20 | Encoding encoding = EncodingUtil.encoding(contentType()); 21 | return encoding.decode(await bytes()); 22 | // return encoding.decodeStream(source()); 23 | } 24 | 25 | static ResponseBody bytesBody(MediaType contentType, List bytes) { 26 | return streamBody(contentType, bytes.length, 27 | Stream>.fromIterable(>[bytes])); 28 | } 29 | 30 | static ResponseBody streamBody( 31 | MediaType contentType, int contentLength, Stream> source) { 32 | return _StreamResponseBody(contentType, contentLength, source); 33 | } 34 | } 35 | 36 | class _StreamResponseBody extends ResponseBody { 37 | _StreamResponseBody( 38 | MediaType contentType, 39 | int contentLength, 40 | Stream> source, 41 | ) : _contentType = contentType, 42 | _contentLength = contentLength, 43 | _source = source; 44 | 45 | final MediaType _contentType; 46 | final int _contentLength; 47 | final Stream> _source; 48 | 49 | @override 50 | MediaType contentType() { 51 | return _contentType; 52 | } 53 | 54 | @override 55 | int contentLength() { 56 | return _contentLength; 57 | } 58 | 59 | @override 60 | Stream> source() { 61 | return _source; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/okhttp3/media_type.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | class MediaType { 4 | MediaType( 5 | String primaryType, 6 | String subType, [ 7 | String charset, 8 | Map parameters, 9 | ]) : _contentType = ContentType( 10 | primaryType, 11 | subType, 12 | charset: charset, 13 | parameters: parameters, 14 | ); 15 | 16 | MediaType._( 17 | ContentType contentType, 18 | ) : _contentType = contentType; 19 | 20 | static final MediaType text = MediaType._(ContentType.text); 21 | static final MediaType html = MediaType._(ContentType.html); 22 | static final MediaType json = MediaType._(ContentType.json); 23 | static final MediaType binary = MediaType._(ContentType.binary); 24 | 25 | final ContentType _contentType; 26 | 27 | String type() { 28 | return _contentType.primaryType; 29 | } 30 | 31 | String subtype() { 32 | return _contentType.subType; 33 | } 34 | 35 | String mimeType() { 36 | return _contentType.mimeType; 37 | } 38 | 39 | String charset() { 40 | return _contentType.charset; 41 | } 42 | 43 | @override 44 | String toString() { 45 | if (_contentType.charset != null) { 46 | return '${_contentType.mimeType}; charset=${_contentType.charset}'; 47 | } 48 | return _contentType.mimeType; 49 | } 50 | 51 | @override 52 | int get hashCode { 53 | return toString().hashCode; 54 | } 55 | 56 | @override 57 | bool operator ==(dynamic other) { 58 | if (identical(this, other)) { 59 | return true; 60 | } 61 | return other is MediaType && 62 | runtimeType == other.runtimeType && 63 | toString() == other.toString(); 64 | } 65 | 66 | static MediaType parse(String value) { 67 | ContentType contentType = ContentType.parse(value); 68 | return MediaType._(contentType); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/okhttp3/form_body.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | 4 | import 'package:okhttp_kit/okhttp3/media_type.dart'; 5 | import 'package:okhttp_kit/okhttp3/request_body.dart'; 6 | 7 | class FormBody extends RequestBody { 8 | FormBody._( 9 | Encoding encoding, 10 | List namesAndValues, 11 | ) : _encoding = encoding, 12 | _contentType = 13 | MediaType('application', 'x-www-form-urlencoded', encoding.name), 14 | _namesAndValues = namesAndValues, 15 | _bytes = encoding.encode(_pairsToQuery(namesAndValues)); 16 | 17 | final Encoding _encoding; 18 | final MediaType _contentType; 19 | final List _namesAndValues; 20 | final List _bytes; 21 | 22 | @override 23 | MediaType contentType() { 24 | return _contentType; 25 | } 26 | 27 | @override 28 | int contentLength() { 29 | return _bytes.length; 30 | } 31 | 32 | @override 33 | Stream> source() { 34 | return Stream>.fromIterable(>[_bytes]); 35 | } 36 | 37 | static String _pairsToQuery(List namesAndValues) { 38 | return List.generate(namesAndValues.length ~/ 2, (int index) { 39 | return '${namesAndValues[index * 2]}=${namesAndValues[index * 2 + 1]}'; 40 | }).join('&'); 41 | } 42 | } 43 | 44 | class FormBodyBuilder { 45 | FormBodyBuilder([ 46 | Encoding encoding = utf8, 47 | ]) : assert(encoding != null), 48 | _encoding = encoding; 49 | 50 | final Encoding _encoding; 51 | final List _namesAndValues = []; 52 | 53 | FormBodyBuilder add(String name, String value) { 54 | assert(name != null && name.isNotEmpty); 55 | assert(value != null); 56 | _namesAndValues.add(Uri.encodeQueryComponent(name, encoding: _encoding)); 57 | _namesAndValues.add(Uri.encodeQueryComponent(value, encoding: _encoding)); 58 | return this; 59 | } 60 | 61 | FormBody build() { 62 | return FormBody._(_encoding, List.unmodifiable(_namesAndValues)); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/okhttp3/request_body.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:io'; 4 | 5 | import 'package:okhttp_kit/okhttp3/internal/encoding_util.dart'; 6 | import 'package:okhttp_kit/okhttp3/media_type.dart'; 7 | 8 | /// buffer 9 | abstract class RequestBody { 10 | MediaType contentType(); 11 | 12 | int contentLength(); 13 | 14 | Stream> source(); 15 | 16 | static RequestBody bytesBody(MediaType contentType, List bytes) { 17 | assert(bytes != null); 18 | return _SimpleRequestBody(contentType, bytes.length, bytes); 19 | } 20 | 21 | static RequestBody textBody(MediaType contentType, String text) { 22 | assert(text != null); 23 | Encoding encoding = EncodingUtil.encoding(contentType); 24 | return bytesBody(contentType, encoding.encode(text)); 25 | } 26 | 27 | static RequestBody fileBody(MediaType contentType, File file) { 28 | return _FileRequestBody(contentType, file); 29 | } 30 | } 31 | 32 | class _SimpleRequestBody extends RequestBody { 33 | _SimpleRequestBody( 34 | MediaType contentType, 35 | int contentLength, 36 | List bytes, 37 | ) : _contentType = contentType, 38 | _contentLength = contentLength, 39 | _bytes = bytes; 40 | 41 | final MediaType _contentType; 42 | final int _contentLength; 43 | final List _bytes; 44 | 45 | @override 46 | MediaType contentType() { 47 | return _contentType; 48 | } 49 | 50 | @override 51 | int contentLength() { 52 | return _contentLength; 53 | } 54 | 55 | @override 56 | Stream> source() { 57 | return Stream>.fromIterable(>[_bytes]); 58 | } 59 | } 60 | 61 | class _FileRequestBody extends RequestBody { 62 | _FileRequestBody( 63 | MediaType contentType, 64 | File file, 65 | ) : _contentType = contentType, 66 | _file = file; 67 | 68 | final MediaType _contentType; 69 | final File _file; 70 | 71 | @override 72 | MediaType contentType() { 73 | return _contentType; 74 | } 75 | 76 | @override 77 | int contentLength() { 78 | return _file.lengthSync(); 79 | } 80 | 81 | @override 82 | Stream> source() { 83 | return _file.openRead(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /lib/okhttp3/tools/optimized_response_interceptor.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:okhttp_kit/okhttp3/cache_control.dart'; 5 | import 'package:okhttp_kit/okhttp3/chain.dart'; 6 | import 'package:okhttp_kit/okhttp3/interceptor.dart'; 7 | import 'package:okhttp_kit/okhttp3/internal/http/http_method.dart'; 8 | import 'package:okhttp_kit/okhttp3/request.dart'; 9 | import 'package:okhttp_kit/okhttp3/response.dart'; 10 | 11 | /// 网络层拦截器 12 | /// 优化缓存 13 | /// 14 | /// https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Caching_FAQ 15 | class OptimizedResponseInterceptor implements Interceptor { 16 | OptimizedResponseInterceptor([ 17 | int maxAgeSeconds = 3, 18 | ]) : _maxAgeSeconds = maxAgeSeconds; 19 | 20 | final int _maxAgeSeconds; 21 | 22 | @override 23 | Future intercept(Chain chain) async { 24 | Request originalRequest = chain.request(); 25 | Response originalResponse = await chain.proceed(originalRequest); 26 | if (!HttpMethod.invalidatesCache(originalRequest.method())) { 27 | if (originalResponse.isSuccessful()) { 28 | if (originalResponse.header(HttpHeaders.lastModifiedHeader) == null && 29 | originalResponse.header(HttpHeaders.etagHeader) == null && 30 | originalResponse.header(HttpHeaders.expiresHeader) == null && 31 | originalResponse.header(HttpHeaders.ageHeader) == null) { 32 | // 智能添加缓存信息 33 | bool shouldOptimizedCache = false; 34 | if (originalResponse.header(HttpHeaders.cacheControlHeader) == null && 35 | originalResponse.header(HttpHeaders.pragmaHeader) == null) { 36 | shouldOptimizedCache = true; 37 | } else { 38 | CacheControl cacheControl = originalResponse.cacheControl(); 39 | shouldOptimizedCache = 40 | cacheControl.noCache() || cacheControl.noStore(); 41 | } 42 | if (shouldOptimizedCache) { 43 | return originalResponse 44 | .newBuilder() 45 | .removeHeader(HttpHeaders.pragmaHeader) 46 | .header( 47 | HttpHeaders.cacheControlHeader, 48 | CacheControlBuilder() 49 | .maxAge(Duration(seconds: _maxAgeSeconds)) 50 | .build() 51 | .toString()) 52 | .build(); 53 | } 54 | } 55 | } 56 | } 57 | return originalResponse; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/okhttp3/internal/util.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:math'; 4 | 5 | import 'package:okhttp_kit/okhttp3/http_url.dart'; 6 | import 'package:okhttp_kit/okhttp3/request_body.dart'; 7 | import 'package:okhttp_kit/okhttp3/response_body.dart'; 8 | 9 | class Util { 10 | Util._(); 11 | 12 | static const int _boundaryLength = 70; 13 | static const List _boundaryCharacters = [ 14 | 43, 15 | 95, 16 | 45, 17 | 46, 18 | 48, 19 | 49, 20 | 50, 21 | 51, 22 | 52, 23 | 53, 24 | 54, 25 | 55, 26 | 56, 27 | 57, 28 | 65, 29 | 66, 30 | 67, 31 | 68, 32 | 69, 33 | 70, 34 | 71, 35 | 72, 36 | 73, 37 | 74, 38 | 75, 39 | 76, 40 | 77, 41 | 78, 42 | 79, 43 | 80, 44 | 81, 45 | 82, 46 | 83, 47 | 84, 48 | 85, 49 | 86, 50 | 87, 51 | 88, 52 | 89, 53 | 90, 54 | 97, 55 | 98, 56 | 99, 57 | 100, 58 | 101, 59 | 102, 60 | 103, 61 | 104, 62 | 105, 63 | 106, 64 | 107, 65 | 108, 66 | 109, 67 | 110, 68 | 111, 69 | 112, 70 | 113, 71 | 114, 72 | 115, 73 | 116, 74 | 117, 75 | 118, 76 | 119, 77 | 120, 78 | 121, 79 | 122, 80 | ]; 81 | 82 | static final ResponseBody emptyResponse = 83 | ResponseBody.bytesBody(null, const []); 84 | 85 | static final RequestBody emptyRequest = 86 | RequestBody.bytesBody(null, const []); 87 | 88 | static String hostHeader(HttpUrl url, bool includeDefaultPort) { 89 | String host = url.host().contains(':') ? '[${url.host()}]' : url.host(); 90 | return includeDefaultPort || url.port() != HttpUrl.defaultPort(url.scheme()) 91 | ? '$host:${url.port()}' 92 | : host; 93 | } 94 | 95 | static bool verifyAsIpAddress(String host) { 96 | return RegExp('([0-9a-fA-F]*:[0-9a-fA-F:.]*)|([\\d.]+)') 97 | .stringMatch(host) == 98 | host; 99 | } 100 | 101 | static Future> readAsBytes(Stream> source) { 102 | Completer> completer = Completer>(); 103 | ByteConversionSink sink = 104 | ByteConversionSink.withCallback((List accumulated) { 105 | completer.complete(accumulated); 106 | }); 107 | source.listen( 108 | sink.add, 109 | onError: completer.completeError, 110 | onDone: sink.close, 111 | cancelOnError: true, 112 | ); 113 | return completer.future; 114 | } 115 | 116 | static String boundaryString() { 117 | String prefix = "dart-http-boundary-"; 118 | Random random = Random(); 119 | List list = List.generate( 120 | _boundaryLength - prefix.length, 121 | (int index) => 122 | _boundaryCharacters[random.nextInt(_boundaryCharacters.length)], 123 | growable: false, 124 | ); 125 | return "$prefix${String.fromCharCodes(list)}"; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /test/okhttp_kit_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:okhttp_kit/okhttp_kit.dart'; 4 | import 'package:path/path.dart' as path; 5 | import 'package:test/test.dart'; 6 | 7 | void main() { 8 | Directory directory = 9 | Directory(path.join(Directory.current.path, 'build', 'cache')); 10 | if (!directory.existsSync()) { 11 | directory.createSync(recursive: true); 12 | } 13 | 14 | OkHttpClient client = OkHttpClientBuilder() 15 | .cookieJar(PersistentCookieJar.memory()) 16 | .cache(Cache(DiskCache.create(() => directory))) 17 | .proxy((Uri url) { 18 | print('Proxy Url: $url'); 19 | return HttpClient.findProxyFromEnvironment(url); 20 | }) 21 | .addInterceptor(UserAgentInterceptor(() => 'xxx')) 22 | .addInterceptor(OptimizedRequestInterceptor(() => true)) 23 | // .addNetworkInterceptor(OptimizedResponseInterceptor()) 24 | .addNetworkInterceptor(CurlInterceptor(true, (String name) { 25 | return name != HttpHeaders.connectionHeader && 26 | name != HttpHeaders.acceptEncodingHeader; 27 | })) 28 | .addNetworkInterceptor(HttpLoggingInterceptor(LoggingLevel.headers)) 29 | .addNetworkInterceptor(ProgressRequestInterceptor((HttpUrl url, 30 | String method, int progressBytes, int totalBytes, bool isDone) { 31 | print( 32 | 'progress request - $method $url $progressBytes/$totalBytes done:$isDone'); 33 | })) 34 | .addNetworkInterceptor(ProgressResponseInterceptor((HttpUrl url, 35 | String method, int progressBytes, int totalBytes, bool isDone) { 36 | print( 37 | 'progress response - $method $url $progressBytes/$totalBytes done:$isDone'); 38 | })) 39 | .build(); 40 | 41 | test('pub.dev', () async { 42 | Request request = 43 | RequestBuilder().get().url(HttpUrl.parse('https://pub.dev/')).build(); 44 | Response response = await client.newCall(request).enqueue(); 45 | print( 46 | '${response.code()} - ${response.message()} - ${response.cacheControl()}'); 47 | }); 48 | 49 | test('baidu.com', () async { 50 | Request request = RequestBuilder() 51 | .get() 52 | .url(HttpUrl.parse('https://www.baidu.com/')) 53 | .build(); 54 | Response response = await client.newCall(request).enqueue(); 55 | print( 56 | '${response.code()} - ${response.message()} - ${response.cacheControl()}'); 57 | }); 58 | 59 | test('taobao.com', () async { 60 | Request request = RequestBuilder() 61 | .get() 62 | .url(HttpUrl.parse('https://www.taobao.com/')) 63 | .build(); 64 | Response response = await client.newCall(request).enqueue(); 65 | print( 66 | '${response.code()} - ${response.message()} - ${response.cacheControl()} - ${(await response.body().bytes()).length}'); 67 | }); 68 | 69 | test('fanyi.baidu.com', () async { 70 | HttpUrl url = HttpUrl.parse('http://fanyi.baidu.com/v2transapi'); 71 | RequestBody body = FormBodyBuilder() 72 | .add('from', 'zh') 73 | .add('to', 'en') 74 | .add('query', '奇数') 75 | .add('simple_means_flag', '3') 76 | .add('sign', '763725.1000572') 77 | .add('token', 'ace5889a5474fc144c730ce9e95878a8') 78 | .build(); 79 | Request request = RequestBuilder() 80 | .url(url) 81 | .header(HttpHeaders.refererHeader, 'http://fanyi.baidu.com/') 82 | .post(body) 83 | .build(); 84 | Response response = await client.newCall(request).enqueue(); 85 | print( 86 | '${response.code()} - ${response.message()} - ${response.cacheControl()} - ${(await response.body().bytes()).length}'); 87 | }); 88 | } 89 | -------------------------------------------------------------------------------- /lib/okhttp3/tools/curl_interceptor.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:io'; 4 | 5 | import 'package:okhttp_kit/okhttp3/chain.dart'; 6 | import 'package:okhttp_kit/okhttp3/foundation/character.dart'; 7 | import 'package:okhttp_kit/okhttp3/headers.dart'; 8 | import 'package:okhttp_kit/okhttp3/interceptor.dart'; 9 | import 'package:okhttp_kit/okhttp3/internal/encoding_util.dart'; 10 | import 'package:okhttp_kit/okhttp3/internal/util.dart'; 11 | import 'package:okhttp_kit/okhttp3/media_type.dart'; 12 | import 'package:okhttp_kit/okhttp3/request.dart'; 13 | import 'package:okhttp_kit/okhttp3/request_body.dart'; 14 | import 'package:okhttp_kit/okhttp3/response.dart'; 15 | 16 | const List _headerFilters = [ 17 | HttpHeaders.connectionHeader, 18 | HttpHeaders.acceptEncodingHeader, 19 | HttpHeaders.cacheControlHeader, 20 | HttpHeaders.ifNoneMatchHeader, 21 | HttpHeaders.ifModifiedSinceHeader, 22 | ]; 23 | 24 | bool _defaultHeaderFilter(String name) { 25 | return !_headerFilters.contains(name); 26 | } 27 | 28 | /// 网络层拦截器 29 | class CurlInterceptor implements Interceptor { 30 | CurlInterceptor([ 31 | this.enabled = true, 32 | this.headerFilter = _defaultHeaderFilter, 33 | ]); 34 | 35 | bool enabled; 36 | bool Function(String) headerFilter; 37 | 38 | @override 39 | Future intercept(Chain chain) async { 40 | Request request = chain.request(); 41 | if (!enabled) { 42 | return await chain.proceed(request); 43 | } 44 | bool supportCurl = false; 45 | List parts = []; 46 | parts.add('curl'); 47 | parts.add('-X ${request.method()}'); 48 | parts.add('${request.url().toString()}'); 49 | Headers headers = request.headers(); 50 | headers.names().forEach((String name) { 51 | if (headerFilter == null || headerFilter(name)) { 52 | parts.add('-H \'$name:${headers.value(name)}\''); 53 | } 54 | }); 55 | RequestBody requestBody = request.body(); 56 | if (requestBody != null) { 57 | if (!_bodyHasUnknownEncoding(request.headers())) { 58 | MediaType contentType = requestBody.contentType(); 59 | if (_isPlainContentType(contentType)) { 60 | List bytes = await Util.readAsBytes(requestBody.source()); 61 | Encoding encoding = EncodingUtil.encoding(contentType); 62 | String body = encoding.decode(bytes); 63 | if (_isPlainText(body)) { 64 | supportCurl = true; 65 | parts.add('-d \'$body\''); 66 | } 67 | request = request 68 | .newBuilder() 69 | .method( 70 | request.method(), RequestBody.bytesBody(contentType, bytes)) 71 | .build(); 72 | } 73 | } 74 | } else { 75 | supportCurl = true; 76 | } 77 | if (supportCurl) { 78 | print('curl: ${parts.join(' ')}'); 79 | } 80 | return await chain.proceed(request); 81 | } 82 | 83 | bool _bodyHasUnknownEncoding(Headers headers) { 84 | String contentEncoding = headers.value(HttpHeaders.contentEncodingHeader); 85 | return contentEncoding != null && 86 | contentEncoding.toLowerCase() != 'identity' && 87 | contentEncoding.toLowerCase() != 'gzip'; 88 | } 89 | 90 | static bool _isPlainContentType(MediaType contentType) { 91 | if (contentType != null && 92 | ('text' == contentType.type().toLowerCase() || 93 | 'json' == contentType.subtype().toLowerCase() || 94 | 'application/x-www-form-urlencoded' == 95 | contentType.mimeType().toLowerCase())) { 96 | return true; 97 | } 98 | return false; 99 | } 100 | 101 | static bool _isPlainText(String body) { 102 | return body.runes.take(16).every((int rune) { 103 | return !Character.isIsoControl(rune) || Character.isWhitespace(rune); 104 | }); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /.drone.yml: -------------------------------------------------------------------------------- 1 | kind: pipeline 2 | name: default 3 | 4 | steps: 5 | - name: prepare 6 | image: google/dart:2.3.0 7 | volumes: 8 | - name: pub-cache 9 | path: /root/.pub-cache 10 | commands: 11 | - pub get 12 | 13 | #- name: build_runner 14 | # image: google/dart:2.3.0 15 | # volumes: 16 | # - name: pub-cache 17 | # path: /root/.pub-cache 18 | # commands: 19 | # - pub run build_runner build 20 | 21 | - name: format 22 | image: google/dart:2.3.0 23 | volumes: 24 | - name: pub-cache 25 | path: /root/.pub-cache 26 | commands: 27 | - dartfmt --dry-run --set-exit-if-changed . 28 | 29 | - name: analyze 30 | image: google/dart:2.3.0 31 | volumes: 32 | - name: pub-cache 33 | path: /root/.pub-cache 34 | commands: 35 | - dartanalyzer --fatal-infos --fatal-warnings . 36 | 37 | - name: test 38 | image: google/dart:2.3.0 39 | volumes: 40 | - name: pub-cache 41 | path: /root/.pub-cache 42 | commands: 43 | - pub run test --run-skipped 44 | 45 | - name: publish-check 46 | image: google/dart:2.3.0 47 | volumes: 48 | - name: pub-cache 49 | path: /root/.pub-cache 50 | commands: 51 | - pub publish --dry-run 52 | 53 | volumes: 54 | - name: pub-cache 55 | temp: {} 56 | 57 | --- 58 | kind: pipeline 59 | name: publish 60 | 61 | steps: 62 | - name: restore-cache 63 | image: alpine:3.9.3 64 | volumes: 65 | - name: pub-cache 66 | path: /root/.pub-cache 67 | environment: 68 | PUB_CACHE: /root/.pub-cache 69 | commands: 70 | - wget -P $PUB_CACHE https://raw.githubusercontent.com/v7lin/pub_credentials/master/credentials.json.enc 71 | 72 | - name: restore-cache-openssl 73 | image: v7lin/openssl:1.1.1b 74 | volumes: 75 | - name: pub-cache 76 | path: /root/.pub-cache 77 | environment: 78 | PUB_CACHE: /root/.pub-cache 79 | ENC_METHOD: 80 | from_secret: ENC_METHOD 81 | ENC_PASSWORD: 82 | from_secret: ENC_PASSWORD 83 | commands: 84 | - openssl enc -d -$ENC_METHOD -k $ENC_PASSWORD -in $PUB_CACHE/credentials.json.enc -out $PUB_CACHE/credentials.json 85 | - rm $PUB_CACHE/credentials.json.enc 86 | 87 | - name: publish 88 | image: google/dart:2.3.0 89 | volumes: 90 | - name: pub-cache 91 | path: /root/.pub-cache 92 | commands: 93 | - echo "y" | pub publish 94 | 95 | - name: save-cache-openssl 96 | image: v7lin/openssl:1.1.1b 97 | volumes: 98 | - name: pub-cache 99 | path: /root/.pub-cache 100 | environment: 101 | PUB_CACHE: /root/.pub-cache 102 | ENC_METHOD: 103 | from_secret: ENC_METHOD 104 | ENC_PASSWORD: 105 | from_secret: ENC_PASSWORD 106 | commands: 107 | - openssl enc -e -$ENC_METHOD -k $ENC_PASSWORD -in $PUB_CACHE/credentials.json -out $PUB_CACHE/credentials.json.enc 108 | - rm $PUB_CACHE/credentials.json 109 | 110 | - name: save-cache 111 | image: docker:git 112 | volumes: 113 | - name: pub-cache 114 | path: /root/.pub-cache 115 | environment: 116 | PUB_CACHE: /root/.pub-cache 117 | GIT_USER_EMAIL: 118 | from_secret: GIT_USER_EMAIL 119 | GIT_USER_NAME: 120 | from_secret: GIT_USER_NAME 121 | GIT_USER_PASSWORD: 122 | from_secret: GIT_USER_PASSWORD # 密码含'@',用'%40'替换 -> URLEncoder.encode("@","utf-8"); 123 | commands: 124 | - git config --global user.email $GIT_USER_EMAIL 125 | - git config --global user.name $GIT_USER_NAME 126 | - git config --global credential.helper store 127 | - git clone -b master https://$GIT_USER_NAME:$GIT_USER_PASSWORD@github.com/v7lin/pub_credentials.git $PUB_CACHE/pub_credentials 128 | - rm $PUB_CACHE/pub_credentials/credentials.json.enc 129 | - mv $PUB_CACHE/credentials.json.enc $PUB_CACHE/pub_credentials/credentials.json.enc 130 | - cd $PUB_CACHE/pub_credentials 131 | - git commit -am "update credentials by ci/cd tools" 132 | - git push 133 | 134 | volumes: 135 | - name: pub-cache 136 | temp: {} 137 | 138 | trigger: 139 | status: 140 | - success 141 | event: 142 | - tag 143 | 144 | depends_on: 145 | - default 146 | -------------------------------------------------------------------------------- /lib/okhttp3/request.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:okhttp_kit/okhttp3/cache_control.dart'; 4 | import 'package:okhttp_kit/okhttp3/headers.dart'; 5 | import 'package:okhttp_kit/okhttp3/http_url.dart'; 6 | import 'package:okhttp_kit/okhttp3/internal/http/http_method.dart'; 7 | import 'package:okhttp_kit/okhttp3/request_body.dart'; 8 | 9 | class Request { 10 | Request._( 11 | RequestBuilder builder, 12 | ) : _url = builder._url, 13 | _method = builder._method, 14 | _headers = builder._headers.build(), 15 | _body = builder._body; 16 | 17 | final HttpUrl _url; 18 | final String _method; 19 | final Headers _headers; 20 | final RequestBody _body; 21 | 22 | CacheControl _cacheControl; 23 | 24 | HttpUrl url() { 25 | return _url; 26 | } 27 | 28 | String method() { 29 | return _method; 30 | } 31 | 32 | String header(String name) { 33 | return _headers.value(name); 34 | } 35 | 36 | Headers headers() { 37 | return _headers; 38 | } 39 | 40 | CacheControl cacheControl() { 41 | if (_cacheControl == null) { 42 | _cacheControl = CacheControl.parse(_headers); 43 | } 44 | return _cacheControl; 45 | } 46 | 47 | RequestBody body() { 48 | return _body; 49 | } 50 | 51 | RequestBuilder newBuilder() { 52 | return RequestBuilder._(this); 53 | } 54 | } 55 | 56 | class RequestBuilder { 57 | RequestBuilder() 58 | : _method = HttpMethod.get, 59 | _headers = HeadersBuilder(); 60 | 61 | RequestBuilder._( 62 | Request request, 63 | ) : _url = request._url, 64 | _method = request._method, 65 | _headers = request._headers.newBuilder(), 66 | _body = request._body; 67 | 68 | HttpUrl _url; 69 | String _method; 70 | HeadersBuilder _headers; 71 | RequestBody _body; 72 | 73 | RequestBuilder url(HttpUrl value) { 74 | _url = value; 75 | return this; 76 | } 77 | 78 | RequestBuilder header(String name, String value) { 79 | _headers.set(name, value); 80 | return this; 81 | } 82 | 83 | RequestBuilder addHeader(String name, String value) { 84 | _headers.add(name, value); 85 | return this; 86 | } 87 | 88 | RequestBuilder removeHeader(String name) { 89 | _headers.removeAll(name); 90 | return this; 91 | } 92 | 93 | RequestBuilder headers(Headers headers) { 94 | _headers = headers.newBuilder(); 95 | return this; 96 | } 97 | 98 | RequestBuilder cacheControl(CacheControl cacheControl) { 99 | String value = cacheControl?.toString() ?? ''; 100 | if (value.isEmpty) { 101 | return removeHeader(HttpHeaders.cacheControlHeader); 102 | } 103 | return header(HttpHeaders.cacheControlHeader, value); 104 | } 105 | 106 | RequestBuilder get() { 107 | return method(HttpMethod.get, null); 108 | } 109 | 110 | RequestBuilder head() { 111 | return method(HttpMethod.head, null); 112 | } 113 | 114 | RequestBuilder post(RequestBody body) { 115 | return method(HttpMethod.post, body); 116 | } 117 | 118 | RequestBuilder delete(RequestBody body) { 119 | return method(HttpMethod.delete, body); 120 | } 121 | 122 | RequestBuilder put(RequestBody body) { 123 | return method(HttpMethod.put, body); 124 | } 125 | 126 | RequestBuilder patch(RequestBody body) { 127 | return method(HttpMethod.patch, body); 128 | } 129 | 130 | RequestBuilder method(String method, RequestBody body) { 131 | assert(method != null && method.isNotEmpty); 132 | if (body != null && !HttpMethod.permitsRequestBody(method)) { 133 | throw AssertionError('method $method must not have a request body.'); 134 | } 135 | if (body == null && HttpMethod.requiresRequestBody(method)) { 136 | throw AssertionError('method $method must have a request body.'); 137 | } 138 | _method = method; 139 | _body = body; 140 | return this; 141 | } 142 | 143 | Request build() { 144 | assert(_url != null); 145 | return Request._(this); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /lib/okhttp3/internal/http/call_server_interceptor.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:okhttp_kit/okhttp3/chain.dart'; 5 | import 'package:okhttp_kit/okhttp3/interceptor.dart'; 6 | import 'package:okhttp_kit/okhttp3/internal/http/http_method.dart'; 7 | import 'package:okhttp_kit/okhttp3/internal/http_extension.dart'; 8 | import 'package:okhttp_kit/okhttp3/media_type.dart'; 9 | import 'package:okhttp_kit/okhttp3/okhttp_client.dart'; 10 | import 'package:okhttp_kit/okhttp3/request.dart'; 11 | import 'package:okhttp_kit/okhttp3/response.dart'; 12 | import 'package:okhttp_kit/okhttp3/response_body.dart'; 13 | 14 | class CallServerInterceptor implements Interceptor { 15 | CallServerInterceptor( 16 | OkHttpClient client, 17 | ) : _client = client; 18 | 19 | final OkHttpClient _client; 20 | 21 | @override 22 | Future intercept(Chain chain) async { 23 | HttpClient httpClient = _client.securityContext() != null 24 | ? HttpClient(context: _client.securityContext()) 25 | : HttpClient(); 26 | httpClient.autoUncompress = true; 27 | httpClient.idleTimeout = _client.idleTimeout(); 28 | httpClient.connectionTimeout = _client.connectionTimeout(); 29 | 30 | if (_client.proxy() != null) { 31 | httpClient.findProxy = _client.proxy(); 32 | } else if (_client.proxySelector() != null) { 33 | httpClient.findProxy = await _client.proxySelector().select(); 34 | } 35 | 36 | Request request = chain.request(); 37 | 38 | int sentRequestMillis = DateTime.now().millisecondsSinceEpoch; 39 | 40 | HttpClientRequest ioRequest = 41 | await httpClient.openUrl(request.method(), request.url().uri()); 42 | ioRequest 43 | ..followRedirects = _client.followRedirects() 44 | ..maxRedirects = _client.maxRedirects() 45 | ..contentLength = request.body()?.contentLength() ?? -1 46 | ..persistentConnection = 47 | request.header(HttpHeaders.connectionHeader)?.toLowerCase() == 48 | 'keep-alive'; 49 | for (int i = 0, size = request.headers().size(); i < size; i++) { 50 | ioRequest.headers 51 | .add(request.headers().nameAt(i), request.headers().valueAt(i)); 52 | } 53 | if (HttpMethod.permitsRequestBody(request.method()) && 54 | request.body() != null) { 55 | await ioRequest.addStream(request.body().source()); 56 | } 57 | 58 | HttpClientResponse ioResponse = await ioRequest.close(); 59 | 60 | ResponseBuilder responseBuilder = ResponseBuilder(); 61 | responseBuilder.code(ioResponse.statusCode); 62 | responseBuilder.message(ioResponse.reasonPhrase); 63 | 64 | if (ioResponse.headers != null) { 65 | ioResponse.headers.forEach((String name, List values) { 66 | values.forEach((String value) { 67 | responseBuilder.addHeader(name, value); 68 | }); 69 | }); 70 | } 71 | 72 | Response response = responseBuilder 73 | .request(request) 74 | .sentRequestAtMillis(sentRequestMillis) 75 | .receivedResponseAtMillis(DateTime.now().millisecondsSinceEpoch) 76 | .build(); 77 | 78 | // 绑定 HttpClient 生命周期 79 | Stream> source = 80 | StreamTransformer, List>.fromHandlers( 81 | handleDone: (EventSink> sink) { 82 | sink.close(); 83 | httpClient.close(force: false); // keep alive 84 | }).bind(ioResponse); 85 | 86 | String contentType = response.header(HttpHeaders.contentTypeHeader); 87 | response = response 88 | .newBuilder() 89 | .body(ResponseBody.streamBody( 90 | contentType != null ? MediaType.parse(contentType) : null, 91 | HttpHeadersExtension.contentLength(response), 92 | source)) 93 | .build(); 94 | 95 | if ((response.code() == HttpStatus.noContent || 96 | response.code() == HttpStatus.resetContent) && 97 | response.body() != null && 98 | response.body().contentLength() > 0) { 99 | throw HttpException( 100 | 'HTTP ${response.code()} had non-zero Content-Length: ${response.body().contentLength()}', 101 | uri: request.url().uri(), 102 | ); 103 | } 104 | return response; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /lib/okhttp3/internal/http/bridge_interceptor.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:okhttp_kit/okhttp3/chain.dart'; 5 | import 'package:okhttp_kit/okhttp3/cookie_jar.dart'; 6 | import 'package:okhttp_kit/okhttp3/headers.dart'; 7 | import 'package:okhttp_kit/okhttp3/interceptor.dart'; 8 | import 'package:okhttp_kit/okhttp3/internal/http_extension.dart'; 9 | import 'package:okhttp_kit/okhttp3/internal/util.dart'; 10 | import 'package:okhttp_kit/okhttp3/internal/version.dart'; 11 | import 'package:okhttp_kit/okhttp3/media_type.dart'; 12 | import 'package:okhttp_kit/okhttp3/request.dart'; 13 | import 'package:okhttp_kit/okhttp3/request_body.dart'; 14 | import 'package:okhttp_kit/okhttp3/response.dart'; 15 | 16 | class BridgeInterceptor implements Interceptor { 17 | BridgeInterceptor( 18 | CookieJar cookieJar, 19 | ) : _cookieJar = cookieJar; 20 | 21 | final CookieJar _cookieJar; 22 | 23 | @override 24 | Future intercept(Chain chain) async { 25 | Request userRequest = chain.request(); 26 | RequestBuilder requestBuilder = userRequest.newBuilder(); 27 | 28 | RequestBody body = userRequest.body(); 29 | if (body != null) { 30 | MediaType contentType = body.contentType(); 31 | if (contentType != null) { 32 | requestBuilder.header( 33 | HttpHeaders.contentTypeHeader, contentType.toString()); 34 | } 35 | int contentLength = body.contentLength(); 36 | if (contentLength != -1) { 37 | requestBuilder.header( 38 | HttpHeaders.contentLengthHeader, contentLength.toString()); 39 | requestBuilder.removeHeader(HttpHeaders.transferEncodingHeader); 40 | } else { 41 | requestBuilder.header(HttpHeaders.transferEncodingHeader, 'chunked'); 42 | requestBuilder.removeHeader(HttpHeaders.contentLengthHeader); 43 | } 44 | } 45 | 46 | if (userRequest.header(HttpHeaders.hostHeader) == null) { 47 | requestBuilder.header( 48 | HttpHeaders.hostHeader, Util.hostHeader(userRequest.url(), false)); 49 | } 50 | 51 | if (userRequest.header(HttpHeaders.connectionHeader) == null) { 52 | requestBuilder.header(HttpHeaders.connectionHeader, 'keep-alive'); 53 | } 54 | 55 | // If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing 56 | // the transfer stream. 57 | bool transparentGzip = false; 58 | if (userRequest.header(HttpHeaders.acceptEncodingHeader) == null && 59 | userRequest.header(HttpHeaders.rangeHeader) == null) { 60 | transparentGzip = true; 61 | requestBuilder.header(HttpHeaders.acceptEncodingHeader, 'gzip'); 62 | } 63 | 64 | List cookies = await _cookieJar.loadForRequest(userRequest.url()); 65 | if (cookies.isNotEmpty) { 66 | requestBuilder.header(HttpHeaders.cookieHeader, _cookieHeader(cookies)); 67 | } 68 | 69 | if (userRequest.header(HttpHeaders.userAgentHeader) == null) { 70 | requestBuilder.header(HttpHeaders.userAgentHeader, Version.userAgent()); 71 | } 72 | 73 | Response networkResponse = await chain.proceed(requestBuilder.build()); 74 | 75 | await HttpHeadersExtension.receiveHeaders( 76 | _cookieJar, userRequest.url(), networkResponse.headers()); 77 | 78 | ResponseBuilder responseBuilder = 79 | networkResponse.newBuilder().request(userRequest); 80 | if (transparentGzip && 81 | (networkResponse.header(HttpHeaders.contentEncodingHeader) != null && 82 | networkResponse 83 | .header(HttpHeaders.contentEncodingHeader) 84 | .toLowerCase() == 85 | 'gzip') && 86 | HttpHeadersExtension.hasBody(networkResponse)) { 87 | // Gzip 88 | Headers strippedHeaders = networkResponse 89 | .headers() 90 | .newBuilder() 91 | .removeAll(HttpHeaders.contentEncodingHeader) 92 | .removeAll(HttpHeaders.contentLengthHeader) 93 | .build(); 94 | responseBuilder.headers(strippedHeaders); 95 | } 96 | return responseBuilder.build(); 97 | } 98 | 99 | String _cookieHeader(List cookies) { 100 | return cookies.map((Cookie cookie) { 101 | return '${cookie.name}=${cookie.value}'; 102 | }).join('; '); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /lib/okhttp3/headers.dart: -------------------------------------------------------------------------------- 1 | class Headers { 2 | Headers._(HeadersBuilder builder) 3 | : _namesAndValues = List.unmodifiable(builder._namesAndValues); 4 | 5 | final List _namesAndValues; 6 | 7 | int size() { 8 | return _namesAndValues.length ~/ 2; 9 | } 10 | 11 | String nameAt(int index) { 12 | return _namesAndValues[index * 2]; 13 | } 14 | 15 | String valueAt(int index) { 16 | return _namesAndValues[index * 2 + 1]; 17 | } 18 | 19 | Set names() { 20 | List names = []; 21 | for (int i = 0; i < _namesAndValues.length; i += 2) { 22 | names.add(_namesAndValues[i]); 23 | } 24 | return Set.of(names); 25 | } 26 | 27 | String value(String name) { 28 | assert(name != null); 29 | for (int i = _namesAndValues.length - 2; i >= 0; i -= 2) { 30 | if (name.toLowerCase() == _namesAndValues[i]) { 31 | return _namesAndValues[i + 1]; 32 | } 33 | } 34 | return null; 35 | } 36 | 37 | List values(String name) { 38 | assert(name != null); 39 | List values = []; 40 | for (int i = 0; i < _namesAndValues.length; i += 2) { 41 | if (name.toLowerCase() == _namesAndValues[i]) { 42 | values.add(_namesAndValues[i + 1]); 43 | } 44 | } 45 | return List.unmodifiable(values); 46 | } 47 | 48 | Map> toMultimap() { 49 | Map> multimap = >{}; 50 | for (int i = 0; i < _namesAndValues.length; i += 2) { 51 | String name = _namesAndValues[i]; 52 | List values = multimap[name]; 53 | if (values == null) { 54 | values = []; 55 | multimap.putIfAbsent(name, () => values); 56 | } 57 | values.add(_namesAndValues[i + 1]); 58 | } 59 | return Map>.unmodifiable(multimap); 60 | } 61 | 62 | HeadersBuilder newBuilder() { 63 | return HeadersBuilder._(this); 64 | } 65 | 66 | static Headers of(Map> multimap) { 67 | HeadersBuilder builder = HeadersBuilder(); 68 | if (multimap != null && multimap.isNotEmpty) { 69 | multimap.forEach((String name, List values) { 70 | if (values != null && values.isNotEmpty) { 71 | values.forEach((String value) { 72 | builder.add(name, value); 73 | }); 74 | } 75 | }); 76 | } 77 | return builder.build(); 78 | } 79 | } 80 | 81 | class HeadersBuilder { 82 | HeadersBuilder(); 83 | 84 | HeadersBuilder._( 85 | Headers headers, 86 | ) { 87 | _namesAndValues.addAll(headers._namesAndValues); 88 | } 89 | 90 | final List _namesAndValues = []; 91 | 92 | HeadersBuilder add(String name, String value) { 93 | _checkNameAndValue(name, value); 94 | addLenient(name, value); 95 | return this; 96 | } 97 | 98 | HeadersBuilder addLenient(String name, String value) { 99 | _namesAndValues.add(name.toLowerCase()); 100 | _namesAndValues.add(value.trim()); 101 | return this; 102 | } 103 | 104 | HeadersBuilder addLenientLine(String line) { 105 | int index = line.indexOf(':', 1); 106 | if (index != -1) { 107 | return addLenient(line.substring(0, index), line.substring(index + 1)); 108 | } else if (line.startsWith(':')) { 109 | // Work around empty header names and header names that start with a 110 | // colon (created by old broken SPDY versions of the response cache). 111 | return addLenient('', line.substring(1)); // Empty header name. 112 | } else { 113 | return addLenient('', line); // No header name. 114 | } 115 | } 116 | 117 | HeadersBuilder removeAll(String name) { 118 | for (int i = 0; i < _namesAndValues.length; i += 2) { 119 | if (name.toLowerCase() == _namesAndValues[i]) { 120 | _namesAndValues.removeAt(i); // name 121 | _namesAndValues.removeAt(i); // value 122 | i -= 2; 123 | } 124 | } 125 | return this; 126 | } 127 | 128 | HeadersBuilder set(String name, String value) { 129 | _checkNameAndValue(name, value); 130 | removeAll(name); 131 | addLenient(name, value); 132 | return this; 133 | } 134 | 135 | void _checkNameAndValue(String name, String value) { 136 | assert(name != null && name.isNotEmpty); 137 | assert(value != null); 138 | } 139 | 140 | Headers build() { 141 | return Headers._(this); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /lib/okhttp3/tools/progress_interceptor.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:okhttp_kit/okhttp3/chain.dart'; 4 | import 'package:okhttp_kit/okhttp3/http_url.dart'; 5 | import 'package:okhttp_kit/okhttp3/interceptor.dart'; 6 | import 'package:okhttp_kit/okhttp3/media_type.dart'; 7 | import 'package:okhttp_kit/okhttp3/request.dart'; 8 | import 'package:okhttp_kit/okhttp3/request_body.dart'; 9 | import 'package:okhttp_kit/okhttp3/response.dart'; 10 | import 'package:okhttp_kit/okhttp3/response_body.dart'; 11 | 12 | typedef void ProgressListener( 13 | HttpUrl url, String method, int progressBytes, int totalBytes, bool isDone); 14 | 15 | /// 网络层拦截器 16 | class ProgressRequestInterceptor implements Interceptor { 17 | ProgressRequestInterceptor( 18 | ProgressListener listener, 19 | ) : _listener = listener; 20 | 21 | final ProgressListener _listener; 22 | 23 | @override 24 | Future intercept(Chain chain) { 25 | Request originalRequest = chain.request(); 26 | if (originalRequest.body() == null) { 27 | return chain.proceed(originalRequest); 28 | } 29 | RequestBody originalRequestBody = originalRequest.body(); 30 | int totalBytes = originalRequestBody.contentLength(); 31 | int progressBytes = 0; 32 | Stream> source = 33 | StreamTransformer, List>.fromHandlers( 34 | handleData: (List data, EventSink> sink) { 35 | sink.add(data); 36 | progressBytes += data.length; 37 | if (_listener != null) { 38 | _listener(originalRequest.url(), originalRequest.method(), 39 | progressBytes, totalBytes, false); 40 | } 41 | }, handleDone: (EventSink> sink) { 42 | sink.close(); 43 | if (_listener != null) { 44 | _listener(originalRequest.url(), originalRequest.method(), 45 | progressBytes, totalBytes, true); 46 | } 47 | }).bind(originalRequestBody.source()); 48 | Request progressRequest = originalRequest 49 | .newBuilder() 50 | .method( 51 | originalRequest.method(), 52 | _StreamRequestBody(originalRequestBody.contentType(), 53 | originalRequestBody.contentLength(), source)) 54 | .build(); 55 | return chain.proceed(progressRequest); 56 | } 57 | } 58 | 59 | class _StreamRequestBody extends RequestBody { 60 | _StreamRequestBody( 61 | MediaType contentType, 62 | int contentLength, 63 | Stream> source, 64 | ) : _contentType = contentType, 65 | _contentLength = contentLength, 66 | _source = source; 67 | 68 | final MediaType _contentType; 69 | final int _contentLength; 70 | final Stream> _source; 71 | 72 | @override 73 | MediaType contentType() { 74 | return _contentType; 75 | } 76 | 77 | @override 78 | int contentLength() { 79 | return _contentLength; 80 | } 81 | 82 | @override 83 | Stream> source() { 84 | return _source; 85 | } 86 | } 87 | 88 | /// 网络层拦截器 89 | class ProgressResponseInterceptor implements Interceptor { 90 | ProgressResponseInterceptor( 91 | ProgressListener listener, 92 | ) : _listener = listener; 93 | 94 | final ProgressListener _listener; 95 | 96 | @override 97 | Future intercept(Chain chain) async { 98 | Request originalRequest = chain.request(); 99 | Response originalResponse = await chain.proceed(originalRequest); 100 | if (originalResponse.body() == null) { 101 | return originalResponse; 102 | } 103 | ResponseBody originalResponseBody = originalResponse.body(); 104 | int totalBytes = originalResponseBody.contentLength(); 105 | int progressBytes = 0; 106 | Stream> source = 107 | StreamTransformer, List>.fromHandlers( 108 | handleData: (List data, EventSink> sink) { 109 | sink.add(data); 110 | progressBytes += data.length; 111 | if (_listener != null) { 112 | _listener(originalRequest.url(), originalRequest.method(), 113 | progressBytes, totalBytes, false); 114 | } 115 | }, handleDone: (EventSink> sink) { 116 | sink.close(); 117 | if (_listener != null) { 118 | _listener(originalRequest.url(), originalRequest.method(), 119 | progressBytes, totalBytes, true); 120 | } 121 | }).bind(originalResponseBody.source()); 122 | return originalResponse 123 | .newBuilder() 124 | .body(ResponseBody.streamBody(originalResponseBody.contentType(), 125 | originalResponseBody.contentLength(), source)) 126 | .build(); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /lib/okhttp3/http_url.dart: -------------------------------------------------------------------------------- 1 | class HttpUrl { 2 | HttpUrl._( 3 | HttpUrlBuilder builder, 4 | ) : _uri = builder._uri; 5 | 6 | static const String schemeHttp = 'http'; 7 | static const String schemeHttps = 'https'; 8 | 9 | final Uri _uri; 10 | 11 | String scheme() { 12 | return _uri.scheme; 13 | } 14 | 15 | bool isHttps() { 16 | return _uri.isScheme(schemeHttps); 17 | } 18 | 19 | String host() { 20 | return _uri.host; 21 | } 22 | 23 | int port() { 24 | return _uri.port; 25 | } 26 | 27 | String authority() { 28 | return _uri.authority; 29 | } 30 | 31 | String userInfo() { 32 | return _uri.userInfo; 33 | } 34 | 35 | String path() { 36 | return _uri.path; 37 | } 38 | 39 | String query() { 40 | return _uri.query; 41 | } 42 | 43 | String fragment() { 44 | return _uri.fragment; 45 | } 46 | 47 | List pathSegments() { 48 | return _uri.pathSegments; 49 | } 50 | 51 | String queryParameter(String name) { 52 | List values = _uri.queryParametersAll[name]; 53 | return values != null ? values.first : null; 54 | } 55 | 56 | Set queryParameterNames() { 57 | return Set.of(_uri.queryParametersAll.keys); 58 | } 59 | 60 | List queryParameterValues(String name) { 61 | return _uri.queryParametersAll[name]; 62 | } 63 | 64 | Map> queryParametersAll() { 65 | return _uri.queryParametersAll; 66 | } 67 | 68 | HttpUrlBuilder newBuilder() { 69 | return HttpUrlBuilder._(this); 70 | } 71 | 72 | Uri uri() { 73 | return _uri; 74 | } 75 | 76 | @override 77 | bool operator ==(dynamic other) { 78 | if (identical(this, other)) { 79 | return true; 80 | } 81 | return other is HttpUrl && 82 | runtimeType == other.runtimeType && 83 | _uri == other._uri; 84 | } 85 | 86 | @override 87 | int get hashCode { 88 | return _uri.hashCode; 89 | } 90 | 91 | @override 92 | String toString() { 93 | return _uri.toString(); 94 | } 95 | 96 | static HttpUrl parse(String url) { 97 | return HttpUrlBuilder(Uri.parse(url)).build(); 98 | } 99 | 100 | static HttpUrl from(Uri uri) { 101 | return HttpUrlBuilder(uri).build(); 102 | } 103 | 104 | static int defaultPort(String scheme) { 105 | if (scheme == schemeHttp) { 106 | return 80; 107 | } else if (scheme == schemeHttps) { 108 | return 443; 109 | } else { 110 | return -1; 111 | } 112 | } 113 | } 114 | 115 | class HttpUrlBuilder { 116 | HttpUrlBuilder([ 117 | Uri uri, 118 | ]) : _uri = uri ?? Uri.parse(""); 119 | 120 | HttpUrlBuilder._( 121 | HttpUrl url, 122 | ) : _uri = url._uri; 123 | 124 | Uri _uri; 125 | 126 | HttpUrlBuilder scheme(String scheme) { 127 | _uri = _uri.replace(scheme: scheme); 128 | return this; 129 | } 130 | 131 | HttpUrlBuilder userInfo(String userInfo) { 132 | _uri = _uri.replace(userInfo: userInfo); 133 | return this; 134 | } 135 | 136 | HttpUrlBuilder host(String host) { 137 | _uri = _uri.replace(host: host); 138 | return this; 139 | } 140 | 141 | HttpUrlBuilder port(int port) { 142 | _uri = _uri.replace(port: port); 143 | return this; 144 | } 145 | 146 | HttpUrlBuilder addPathSegment(String pathSegment) { 147 | List pathSegments = _uri.pathSegments.toList(); 148 | pathSegments.add(pathSegment); 149 | _uri = _uri.replace(pathSegments: pathSegments); 150 | return this; 151 | } 152 | 153 | HttpUrlBuilder addQueryParameter(String name, String value) { 154 | Map> queryParametersAll = 155 | Map>.of(_uri.queryParametersAll); 156 | List values = queryParametersAll[name]; 157 | if (values == null) { 158 | values = []; 159 | queryParametersAll.putIfAbsent(name, () => values); 160 | } 161 | values.add(value); 162 | _uri = _uri.replace(queryParameters: queryParametersAll); 163 | return this; 164 | } 165 | 166 | HttpUrlBuilder removeAllQueryParameters(String name) { 167 | Map> queryParametersAll = 168 | Map>.of(_uri.queryParametersAll); 169 | queryParametersAll.remove(name); 170 | _uri = _uri.replace(queryParameters: queryParametersAll); 171 | return this; 172 | } 173 | 174 | HttpUrlBuilder setQueryParameter(String name, String value) { 175 | removeAllQueryParameters(name); 176 | addQueryParameter(name, value); 177 | return this; 178 | } 179 | 180 | HttpUrlBuilder fragment(String fragment) { 181 | _uri = _uri.replace(fragment: fragment); 182 | return this; 183 | } 184 | 185 | HttpUrl build() { 186 | return HttpUrl._(this); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /lib/okhttp3/internal/cache/disk_cache.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:io'; 4 | 5 | import 'package:okhttp_kit/okhttp3/cache.dart'; 6 | import 'package:okhttp_kit/okhttp3/foundation/basic_types.dart'; 7 | import 'package:path/path.dart' as path; 8 | 9 | class DiskCache implements RawCache { 10 | DiskCache._( 11 | AsyncValueGetter directory, 12 | int valueCount, 13 | ) : _directory = directory, 14 | _valueCount = valueCount; 15 | 16 | final AsyncValueGetter _directory; 17 | final int _valueCount; 18 | 19 | @override 20 | Future edit( 21 | String key, [ 22 | int expectedSequenceNumber, 23 | ]) async { 24 | _Entry entry = _Entry(await _directory(), _valueCount, key); 25 | return entry.editor(); 26 | } 27 | 28 | @override 29 | Future get(String key) async { 30 | _Entry entry = _Entry(await _directory(), _valueCount, key); 31 | return entry.snapshot(); 32 | } 33 | 34 | @override 35 | Future remove(String key) async { 36 | _Entry entry = _Entry(await _directory(), _valueCount, key); 37 | return await entry.remove(); 38 | } 39 | 40 | static DiskCache create(AsyncValueGetter directory) { 41 | assert(directory != null); 42 | return DiskCache._(directory, Cache.entryCount); 43 | } 44 | } 45 | 46 | class _Entry { 47 | _Entry( 48 | Directory directory, 49 | int valueCount, 50 | String key, 51 | ) : _key = key, 52 | _cacheFiles = List.generate(valueCount, (int index) { 53 | return File(path.join(directory.path, '$key.$index')); 54 | }); 55 | 56 | final String _key; 57 | final List _cacheFiles; 58 | 59 | String key() { 60 | return _key; 61 | } 62 | 63 | List cacheFiles() { 64 | return _cacheFiles; 65 | } 66 | 67 | Editor editor() { 68 | return _EditorImpl(this); 69 | } 70 | 71 | Snapshot snapshot() { 72 | List>> sources = _cacheFiles.map((File cacheFile) { 73 | return cacheFile.openRead(); 74 | }).toList(); 75 | List lengths = _cacheFiles.map((File cacheFile) { 76 | return cacheFile.lengthSync(); 77 | }).toList(); 78 | return Snapshot(_key, RawCache.anySequenceNumber, sources, lengths); 79 | } 80 | 81 | Future remove() async { 82 | for (File cacheFile in _cacheFiles) { 83 | if (cacheFile.existsSync()) { 84 | cacheFile.deleteSync(); 85 | } 86 | } 87 | return true; 88 | } 89 | } 90 | 91 | class _EditorImpl implements Editor { 92 | _EditorImpl( 93 | _Entry entry, 94 | ) : _cleanFiles = entry.cacheFiles(), 95 | _dirtyFiles = entry.cacheFiles().map((File cacheFile) { 96 | return File(path.join(path.dirname(cacheFile.path), 97 | '${path.basename(cacheFile.path)}.${DateTime.now().millisecondsSinceEpoch}')); 98 | }).toList(); 99 | 100 | final List _cleanFiles; 101 | final List _dirtyFiles; 102 | 103 | bool _done = false; 104 | 105 | @override 106 | StreamSink> newSink(int index, Encoding encoding) { 107 | if (_done) { 108 | throw AssertionError(); 109 | } 110 | File dirtyFile = _dirtyFiles[index]; 111 | if (dirtyFile.existsSync()) { 112 | dirtyFile.deleteSync(); 113 | } 114 | dirtyFile.createSync(recursive: true); 115 | return dirtyFile.openWrite(mode: FileMode.write, encoding: encoding); 116 | } 117 | 118 | @override 119 | Stream> newSource(int index, Encoding encoding) { 120 | if (!_done) { 121 | throw AssertionError(); 122 | } 123 | File cleanFile = _cleanFiles[index]; 124 | if (!cleanFile.existsSync()) { 125 | throw AssertionError('cleanFile is not exists.'); 126 | } 127 | return cleanFile.openRead(); 128 | } 129 | 130 | @override 131 | void commit() { 132 | if (_done) { 133 | throw AssertionError(); 134 | } 135 | _complete(true); 136 | _done = true; 137 | } 138 | 139 | @override 140 | void abort() { 141 | if (_done) { 142 | throw AssertionError(); 143 | } 144 | _complete(false); 145 | _done = true; 146 | } 147 | 148 | void _detach() { 149 | for (File dirtyFile in _dirtyFiles) { 150 | if (dirtyFile.existsSync()) { 151 | dirtyFile.deleteSync(); 152 | } 153 | } 154 | } 155 | 156 | void _complete(bool success) { 157 | if (success) { 158 | for (int i = 0; i < _dirtyFiles.length; i++) { 159 | File dirtyFile = _dirtyFiles[i]; 160 | if (dirtyFile.existsSync()) { 161 | File cleanFile = _cleanFiles[i]; 162 | // print('${dirtyFile.path} - ${cleanFile.path}'); 163 | if (cleanFile.existsSync()) { 164 | cleanFile.deleteSync(); 165 | } 166 | dirtyFile.renameSync(cleanFile.path); 167 | } 168 | } 169 | } else { 170 | _detach(); 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /lib/okhttp3/response.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:okhttp_kit/okhttp3/cache_control.dart'; 4 | import 'package:okhttp_kit/okhttp3/headers.dart'; 5 | import 'package:okhttp_kit/okhttp3/request.dart'; 6 | import 'package:okhttp_kit/okhttp3/response_body.dart'; 7 | 8 | class Response { 9 | Response._( 10 | ResponseBuilder builder, 11 | ) : _request = builder._request, 12 | _code = builder._code, 13 | _message = builder._message, 14 | _headers = builder._headers.build(), 15 | _body = builder._body, 16 | _networkResponse = builder._networkResponse, 17 | _cacheResponse = builder._cacheResponse, 18 | _priorResponse = builder._priorResponse, 19 | _sentRequestAtMillis = builder._sentRequestAtMillis, 20 | _receivedResponseAtMillis = builder._receivedResponseAtMillis; 21 | 22 | final Request _request; 23 | final int _code; 24 | final String _message; 25 | final Headers _headers; 26 | final ResponseBody _body; 27 | final Response _networkResponse; 28 | final Response _cacheResponse; 29 | final Response _priorResponse; 30 | final int _sentRequestAtMillis; 31 | final int _receivedResponseAtMillis; 32 | 33 | CacheControl _cacheControl; 34 | 35 | Request request() { 36 | return _request; 37 | } 38 | 39 | int code() { 40 | return _code; 41 | } 42 | 43 | bool isSuccessful() { 44 | return _code >= HttpStatus.ok && _code < HttpStatus.multipleChoices; 45 | } 46 | 47 | String message() { 48 | return _message; 49 | } 50 | 51 | String header(String name) { 52 | return _headers.value(name); 53 | } 54 | 55 | Headers headers() { 56 | return _headers; 57 | } 58 | 59 | ResponseBody body() { 60 | return _body; 61 | } 62 | 63 | Response networkResponse() { 64 | return _networkResponse; 65 | } 66 | 67 | Response cacheResponse() { 68 | return _cacheResponse; 69 | } 70 | 71 | Response priorResponse() { 72 | return _priorResponse; 73 | } 74 | 75 | CacheControl cacheControl() { 76 | if (_cacheControl == null) { 77 | _cacheControl = CacheControl.parse(_headers); 78 | } 79 | return _cacheControl; 80 | } 81 | 82 | int sentRequestAtMillis() { 83 | return _sentRequestAtMillis; 84 | } 85 | 86 | int receivedResponseAtMillis() { 87 | return _receivedResponseAtMillis; 88 | } 89 | 90 | ResponseBuilder newBuilder() { 91 | return ResponseBuilder._(this); 92 | } 93 | } 94 | 95 | class ResponseBuilder { 96 | ResponseBuilder() : _headers = HeadersBuilder(); 97 | 98 | ResponseBuilder._( 99 | Response response, 100 | ) : _request = response._request, 101 | _code = response._code, 102 | _message = response._message, 103 | _headers = response._headers.newBuilder(), 104 | _body = response._body, 105 | _networkResponse = response._networkResponse, 106 | _cacheResponse = response._cacheResponse, 107 | _priorResponse = response._priorResponse, 108 | _sentRequestAtMillis = response._sentRequestAtMillis, 109 | _receivedResponseAtMillis = response._receivedResponseAtMillis; 110 | 111 | Request _request; 112 | int _code = -1; 113 | String _message; 114 | HeadersBuilder _headers; 115 | ResponseBody _body; 116 | Response _networkResponse; 117 | Response _cacheResponse; 118 | Response _priorResponse; 119 | int _sentRequestAtMillis = 0; 120 | int _receivedResponseAtMillis = 0; 121 | 122 | ResponseBuilder request(Request value) { 123 | _request = value; 124 | return this; 125 | } 126 | 127 | ResponseBuilder code(int value) { 128 | _code = value; 129 | return this; 130 | } 131 | 132 | ResponseBuilder message(String value) { 133 | _message = value; 134 | return this; 135 | } 136 | 137 | ResponseBuilder header(String name, String value) { 138 | _headers.set(name, value); 139 | return this; 140 | } 141 | 142 | ResponseBuilder addHeader(String name, String value) { 143 | _headers.add(name, value); 144 | return this; 145 | } 146 | 147 | ResponseBuilder removeHeader(String name) { 148 | _headers.removeAll(name); 149 | return this; 150 | } 151 | 152 | ResponseBuilder headers(Headers value) { 153 | _headers = value.newBuilder(); 154 | return this; 155 | } 156 | 157 | ResponseBuilder body(ResponseBody value) { 158 | _body = value; 159 | return this; 160 | } 161 | 162 | ResponseBuilder networkResponse(Response value) { 163 | _networkResponse = value; 164 | return this; 165 | } 166 | 167 | ResponseBuilder cacheResponse(Response value) { 168 | _cacheResponse = value; 169 | return this; 170 | } 171 | 172 | ResponseBuilder priorResponse(Response value) { 173 | _priorResponse = value; 174 | return this; 175 | } 176 | 177 | ResponseBuilder sentRequestAtMillis(int value) { 178 | _sentRequestAtMillis = value; 179 | return this; 180 | } 181 | 182 | ResponseBuilder receivedResponseAtMillis(int value) { 183 | _receivedResponseAtMillis = value; 184 | return this; 185 | } 186 | 187 | Response build() { 188 | assert(_request != null); 189 | if (_code < 0) { 190 | throw AssertionError('code < 0: $_code'); 191 | } 192 | assert(_message != null); 193 | return Response._(this); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /lib/okhttp3/tools/optimized_request_interceptor.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:okhttp_kit/okhttp3/cache_control.dart'; 5 | import 'package:okhttp_kit/okhttp3/chain.dart'; 6 | import 'package:okhttp_kit/okhttp3/foundation/basic_types.dart'; 7 | import 'package:okhttp_kit/okhttp3/interceptor.dart'; 8 | import 'package:okhttp_kit/okhttp3/internal/http/http_method.dart'; 9 | import 'package:okhttp_kit/okhttp3/request.dart'; 10 | import 'package:okhttp_kit/okhttp3/response.dart'; 11 | 12 | /// 应用层拦截器 13 | /// 14 | /// 有缓存情况下,如果无网络/请求失败,就使用缓存 15 | class OptimizedRequestInterceptor implements Interceptor { 16 | OptimizedRequestInterceptor( 17 | AsyncValueGetter connectivity, 18 | ) : assert(connectivity != null), 19 | _connectivity = connectivity; 20 | 21 | final AsyncValueGetter _connectivity; 22 | 23 | @override 24 | Future intercept(Chain chain) async { 25 | Request originalRequest = chain.request(); 26 | if (!HttpMethod.invalidatesCache(originalRequest.method())) { 27 | // 强刷 28 | if (originalRequest.cacheControl().toString() == 29 | CacheControl.forceNetwork.toString()) { 30 | Request originalFixedRequest = originalRequest 31 | .newBuilder() 32 | .removeHeader(HttpHeaders.cacheControlHeader) 33 | .removeHeader(HttpHeaders.pragmaHeader) 34 | .cacheControl(CacheControl.forceNetwork) 35 | .build(); 36 | return await chain.proceed(originalFixedRequest); 37 | } 38 | if (originalRequest.cacheControl().toString() == 39 | CacheControl.forceCache.toString()) { 40 | Request originalFixedRequest = originalRequest 41 | .newBuilder() 42 | .removeHeader(HttpHeaders.cacheControlHeader) 43 | .removeHeader(HttpHeaders.pragmaHeader) 44 | .removeHeader(HttpHeaders.ifNoneMatchHeader) 45 | .removeHeader(HttpHeaders.ifModifiedSinceHeader) 46 | .cacheControl(CacheControl.forceCache) 47 | .build(); 48 | return await chain.proceed(originalFixedRequest); 49 | } 50 | Response response; 51 | // 非强刷 52 | try { 53 | response = await chain.proceed(originalRequest); 54 | // 用户手动调时间,让当前时间小于缓存创建时间,这时候缓存不会过期 55 | if (response.receivedResponseAtMillis() > 56 | DateTime.now().millisecondsSinceEpoch) { 57 | originalRequest = originalRequest 58 | .newBuilder() 59 | .removeHeader(HttpHeaders.cacheControlHeader) 60 | .removeHeader(HttpHeaders.pragmaHeader) 61 | .cacheControl(CacheControl.forceNetwork) 62 | .build(); 63 | response = await chain.proceed(originalRequest); 64 | } 65 | } on SocketException catch (e) { 66 | if (await shouldUseCacheIfWeakConnect(originalRequest, e)) { 67 | Request forceCacheRequest = originalRequest 68 | .newBuilder() 69 | .removeHeader(HttpHeaders.cacheControlHeader) 70 | .removeHeader(HttpHeaders.pragmaHeader) 71 | .removeHeader(HttpHeaders.ifNoneMatchHeader) 72 | .removeHeader(HttpHeaders.ifModifiedSinceHeader) 73 | .cacheControl(CacheControl.forceCache) 74 | .build(); 75 | return await chain.proceed(forceCacheRequest); 76 | } else { 77 | rethrow; 78 | } 79 | } on IOException catch (e) { 80 | // 判断是否需要强制调用缓存 81 | if (await shouldUseCacheIfThrowError(originalRequest, e)) { 82 | Request forceCacheRequest = originalRequest 83 | .newBuilder() 84 | .removeHeader(HttpHeaders.cacheControlHeader) 85 | .removeHeader(HttpHeaders.pragmaHeader) 86 | .removeHeader(HttpHeaders.ifNoneMatchHeader) 87 | .removeHeader(HttpHeaders.ifModifiedSinceHeader) 88 | .cacheControl(CacheControl.forceCache) 89 | .build(); 90 | return await chain.proceed(forceCacheRequest); 91 | } else { 92 | rethrow; 93 | } 94 | } 95 | if (response.code() == HttpStatus.internalServerError) { 96 | // 判断是否需要强制调用缓存 97 | if (await shouldUseCacheIfServerError(originalRequest)) { 98 | Request forceCacheRequest = originalRequest 99 | .newBuilder() 100 | .removeHeader(HttpHeaders.cacheControlHeader) 101 | .removeHeader(HttpHeaders.pragmaHeader) 102 | .removeHeader(HttpHeaders.ifNoneMatchHeader) 103 | .removeHeader(HttpHeaders.ifModifiedSinceHeader) 104 | .cacheControl(CacheControl.forceCache) 105 | .build(); 106 | response = await chain.proceed(forceCacheRequest); 107 | return response; 108 | } 109 | } 110 | return response; 111 | } 112 | return await chain.proceed(originalRequest); 113 | } 114 | 115 | Future shouldUseCacheIfWeakConnect( 116 | Request originalRequest, Exception e) async { 117 | return true; 118 | } 119 | 120 | Future shouldUseCacheIfServerError(Request originalRequest) async { 121 | return true; 122 | } 123 | 124 | Future shouldUseCacheIfThrowError( 125 | Request originalRequest, Exception e) async { 126 | return !await _connectivity(); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # Defines a default set of lint rules enforced for 2 | # projects at Google. For details and rationale, 3 | # see https://github.com/dart-lang/pedantic#enabled-lints. 4 | include: package:pedantic/analysis_options.yaml 5 | 6 | analyzer: 7 | strong-mode: 8 | implicit-casts: false 9 | # implicit-dynamic: false 10 | 11 | errors: 12 | # treat missing required parameters as a warning (not a hint) 13 | missing_required_param: warning 14 | # treat missing returns as a warning (not a hint) 15 | missing_return: warning 16 | # allow having TODOs in the code 17 | todo: ignore 18 | # Ignore errors like 19 | # 'super_goes_last' is a deprecated lint rule and should not be used • included_file_warning 20 | included_file_warning: ignore 21 | 22 | # 过滤 json_serializable 23 | exclude: 24 | - "**/*.g.dart" 25 | 26 | linter: 27 | rules: 28 | # these rules are documented on and in the same order as 29 | # the Dart Lint rules page to make maintenance easier 30 | # http://dart-lang.github.io/linter/lints/ 31 | 32 | # === error rules === 33 | - avoid_empty_else 34 | - avoid_slow_async_io 35 | - cancel_subscriptions 36 | # - close_sinks # https://github.com/flutter/flutter/issues/5789 37 | # - comment_references # blocked on https://github.com/dart-lang/dartdoc/issues/1153 38 | - control_flow_in_finally 39 | - empty_statements 40 | - hash_and_equals 41 | # - invariant_booleans # https://github.com/flutter/flutter/issues/5790 42 | - iterable_contains_unrelated_type 43 | - list_remove_unrelated_type 44 | # - literal_only_boolean_expressions # https://github.com/flutter/flutter/issues/5791 45 | - no_adjacent_strings_in_list 46 | - no_duplicate_case_values 47 | - test_types_in_equals 48 | - throw_in_finally 49 | - unrelated_type_equality_checks 50 | - valid_regexps 51 | 52 | # === style rules === 53 | - always_declare_return_types 54 | # - always_put_control_body_on_new_line 55 | - always_require_non_null_named_parameters 56 | - always_specify_types 57 | - annotate_overrides 58 | # - avoid_annotating_with_dynamic # not yet tested 59 | # - avoid_as # 2019-01-30 removed for no-implicit-casts 60 | # - avoid_catches_without_on_clauses # not yet tested 61 | # - avoid_catching_errors # not yet tested 62 | # - avoid_classes_with_only_static_members # not yet tested 63 | # - avoid_function_literals_in_foreach_calls # not yet tested 64 | - avoid_init_to_null 65 | - avoid_null_checks_in_equality_operators 66 | # - avoid_positional_boolean_parameters # not yet tested 67 | - avoid_return_types_on_setters 68 | # - avoid_returning_null # not yet tested 69 | # - avoid_returning_this # not yet tested 70 | # - avoid_setters_without_getters # not yet tested 71 | # - avoid_types_on_closure_parameters # not yet tested 72 | - await_only_futures 73 | - camel_case_types 74 | # - cascade_invocations # not yet tested 75 | # - constant_identifier_names # https://github.com/dart-lang/linter/issues/204 76 | - directives_ordering 77 | - empty_catches 78 | - empty_constructor_bodies 79 | - implementation_imports 80 | # - join_return_with_assignment # not yet tested 81 | - library_names 82 | - library_prefixes 83 | - non_constant_identifier_names 84 | # - omit_local_variable_types # opposite of always_specify_types 85 | # - one_member_abstracts # too many false positives 86 | # - only_throw_errors # https://github.com/flutter/flutter/issues/5792 87 | - overridden_fields 88 | - package_api_docs 89 | - package_prefixed_library_names 90 | # - parameter_assignments # we do this commonly 91 | - prefer_adjacent_string_concatenation 92 | - prefer_collection_literals 93 | # - prefer_conditional_assignment # not yet tested 94 | - prefer_const_constructors 95 | # - prefer_constructors_over_static_methods # not yet tested 96 | - prefer_contains 97 | - prefer_equal_for_default_values 98 | # - prefer_expression_function_bodies # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#consider-using--for-short-functions-and-methods 99 | # - prefer_final_fields # https://github.com/dart-lang/linter/issues/506 100 | # - prefer_final_locals 101 | # - prefer_foreach # not yet tested 102 | # - prefer_function_declarations_over_variables # not yet tested 103 | - prefer_initializing_formals 104 | # - prefer_interpolation_to_compose_strings # not yet tested 105 | - prefer_is_empty 106 | - prefer_is_not_empty 107 | - prefer_void_to_null 108 | # - recursive_getters # https://github.com/dart-lang/linter/issues/452 109 | - slash_for_doc_comments 110 | - sort_constructors_first 111 | - sort_unnamed_constructors_first 112 | # - super_goes_last 113 | # - type_annotate_public_apis # subset of always_specify_types 114 | - type_init_formals 115 | # - unawaited_futures # https://github.com/flutter/flutter/issues/5793 116 | - unnecessary_brace_in_string_interps 117 | - unnecessary_const 118 | - unnecessary_getters_setters 119 | # - unnecessary_lambdas # https://github.com/dart-lang/linter/issues/498 120 | - unnecessary_new 121 | - unnecessary_null_aware_assignments 122 | - unnecessary_null_in_if_null_operators 123 | # - unnecessary_overrides # https://github.com/dart-lang/linter/issues/626 and https://github.com/dart-lang/linter/issues/627 124 | - unnecessary_statements 125 | - unnecessary_this 126 | - use_rethrow_when_possible 127 | # - use_setters_to_change_properties # not yet tested 128 | # - use_string_buffers # https://github.com/dart-lang/linter/pull/664 129 | # - use_to_and_as_if_applicable # has false positives, so we prefer to catch this by code-review 130 | 131 | # === pub rules === 132 | - package_names 133 | 134 | # === doc rules === 135 | # - public_member_api_docs 136 | -------------------------------------------------------------------------------- /lib/okhttp3/okhttp_client.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:okhttp_kit/okhttp3/cache.dart'; 4 | import 'package:okhttp_kit/okhttp3/call.dart'; 5 | import 'package:okhttp_kit/okhttp3/cookie_jar.dart'; 6 | import 'package:okhttp_kit/okhttp3/interceptor.dart'; 7 | import 'package:okhttp_kit/okhttp3/proxy.dart'; 8 | import 'package:okhttp_kit/okhttp3/request.dart'; 9 | 10 | class OkHttpClient { 11 | OkHttpClient._( 12 | List interceptors, 13 | List networkInterceptors, 14 | SecurityContext securityContext, 15 | Proxy proxy, 16 | ProxySelector proxySelector, 17 | CookieJar cookieJar, 18 | Cache cache, 19 | bool followRedirects, 20 | int maxRedirects, 21 | Duration idleTimeout, 22 | Duration connectionTimeout, 23 | ) : _interceptors = interceptors, 24 | _networkInterceptors = networkInterceptors, 25 | _securityContext = securityContext, 26 | _proxy = proxy, 27 | _proxySelector = proxySelector, 28 | _cookieJar = cookieJar, 29 | _cache = cache, 30 | _followRedirects = followRedirects, 31 | _maxRedirects = maxRedirects, 32 | _idleTimeout = idleTimeout, 33 | _connectionTimeout = connectionTimeout; 34 | 35 | final List _interceptors; 36 | final List _networkInterceptors; 37 | 38 | final SecurityContext _securityContext; 39 | final Proxy _proxy; 40 | final ProxySelector _proxySelector; 41 | 42 | final CookieJar _cookieJar; 43 | final Cache _cache; 44 | 45 | final bool _followRedirects; 46 | final int _maxRedirects; 47 | 48 | final Duration _idleTimeout; 49 | final Duration _connectionTimeout; 50 | 51 | List interceptors() { 52 | return _interceptors; 53 | } 54 | 55 | List networkInterceptors() { 56 | return _networkInterceptors; 57 | } 58 | 59 | SecurityContext securityContext() { 60 | return _securityContext; 61 | } 62 | 63 | Proxy proxy() { 64 | return _proxy; 65 | } 66 | 67 | ProxySelector proxySelector() { 68 | return _proxySelector; 69 | } 70 | 71 | CookieJar cookieJar() { 72 | return _cookieJar; 73 | } 74 | 75 | Cache cache() { 76 | return _cache; 77 | } 78 | 79 | bool followRedirects() { 80 | return _followRedirects; 81 | } 82 | 83 | int maxRedirects() { 84 | return _maxRedirects; 85 | } 86 | 87 | Duration idleTimeout() { 88 | return _idleTimeout; 89 | } 90 | 91 | Duration connectionTimeout() { 92 | return _connectionTimeout; 93 | } 94 | 95 | Call newCall(Request request) { 96 | return RealCall.newRealCall(this, request); 97 | } 98 | 99 | OkHttpClientBuilder newBuilder() { 100 | return OkHttpClientBuilder._(this); 101 | } 102 | } 103 | 104 | class OkHttpClientBuilder { 105 | OkHttpClientBuilder(); 106 | 107 | OkHttpClientBuilder._(OkHttpClient client) 108 | : _securityContext = client.securityContext(), 109 | _proxy = client.proxy(), 110 | _proxySelector = client.proxySelector(), 111 | _cookieJar = client.cookieJar(), 112 | _cache = client.cache(), 113 | _followRedirects = client.followRedirects(), 114 | _maxRedirects = client.maxRedirects(), 115 | _idleTimeout = client.idleTimeout(), 116 | _connectionTimeout = client.connectionTimeout() { 117 | _interceptors.addAll(client.interceptors()); 118 | _networkInterceptors.addAll(client.networkInterceptors()); 119 | } 120 | 121 | final List _interceptors = []; 122 | final List _networkInterceptors = []; 123 | 124 | SecurityContext _securityContext; 125 | Proxy _proxy; 126 | ProxySelector _proxySelector; 127 | 128 | CookieJar _cookieJar = CookieJar.noCookies; 129 | Cache _cache; 130 | 131 | bool _followRedirects = true; 132 | int _maxRedirects = 5; 133 | 134 | Duration _idleTimeout = Duration(seconds: 15); 135 | Duration _connectionTimeout = Duration(seconds: 10); 136 | 137 | OkHttpClientBuilder addInterceptor(Interceptor interceptor) { 138 | assert(interceptor != null); 139 | _interceptors.add(interceptor); 140 | return this; 141 | } 142 | 143 | OkHttpClientBuilder addNetworkInterceptor(Interceptor networkInterceptor) { 144 | assert(networkInterceptor != null); 145 | _networkInterceptors.add(networkInterceptor); 146 | return this; 147 | } 148 | 149 | OkHttpClientBuilder securityContext(SecurityContext securityContext) { 150 | _securityContext = securityContext; 151 | return this; 152 | } 153 | 154 | OkHttpClientBuilder proxy(Proxy proxy) { 155 | _proxy = proxy; 156 | return this; 157 | } 158 | 159 | OkHttpClientBuilder proxySelector(ProxySelector proxySelector) { 160 | _proxySelector = proxySelector; 161 | return this; 162 | } 163 | 164 | OkHttpClientBuilder cookieJar(CookieJar cookieJar) { 165 | _cookieJar = cookieJar; 166 | return this; 167 | } 168 | 169 | OkHttpClientBuilder cache(Cache cache) { 170 | _cache = cache; 171 | return this; 172 | } 173 | 174 | OkHttpClientBuilder followRedirects(bool value) { 175 | assert(value != null); 176 | _followRedirects = value; 177 | return this; 178 | } 179 | 180 | OkHttpClientBuilder maxRedirects(int value) { 181 | assert(value != null); 182 | _maxRedirects = value; 183 | return this; 184 | } 185 | 186 | OkHttpClientBuilder idleTimeout(Duration value) { 187 | assert(value != null); 188 | _idleTimeout = value; 189 | return this; 190 | } 191 | 192 | OkHttpClientBuilder connectionTimeout(Duration value) { 193 | assert(value != null); 194 | _connectionTimeout = value; 195 | return this; 196 | } 197 | 198 | OkHttpClient build() { 199 | return OkHttpClient._( 200 | List.unmodifiable(_interceptors), 201 | List.unmodifiable(_networkInterceptors), 202 | _securityContext, 203 | _proxy, 204 | _proxySelector, 205 | _cookieJar, 206 | _cache, 207 | _followRedirects, 208 | _maxRedirects, 209 | _idleTimeout, 210 | _connectionTimeout, 211 | ); 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /lib/okhttp3/internal/http_extension.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:charcode/ascii.dart' as ascii; 4 | import 'package:okhttp_kit/okhttp3/cookie_jar.dart'; 5 | import 'package:okhttp_kit/okhttp3/headers.dart'; 6 | import 'package:okhttp_kit/okhttp3/http_url.dart'; 7 | import 'package:okhttp_kit/okhttp3/internal/http/http_method.dart'; 8 | import 'package:okhttp_kit/okhttp3/internal/util.dart'; 9 | import 'package:okhttp_kit/okhttp3/request.dart'; 10 | import 'package:okhttp_kit/okhttp3/response.dart'; 11 | 12 | class HttpHeadersExtension { 13 | HttpHeadersExtension._(); 14 | 15 | static const String contentDispositionHeader = 'content-disposition'; 16 | 17 | static int skipUntil(String input, int pos, String characters) { 18 | for (; pos < input.length; pos++) { 19 | if (characters.contains(input[pos])) { 20 | break; 21 | } 22 | } 23 | return pos; 24 | } 25 | 26 | static int skipWhitespace(String input, int pos) { 27 | for (; pos < input.length; pos++) { 28 | String c = input[pos]; 29 | if (c != ' ' && c != '\t') { 30 | break; 31 | } 32 | } 33 | return pos; 34 | } 35 | 36 | static Future receiveHeaders( 37 | CookieJar cookieJar, HttpUrl url, Headers headers) async { 38 | if (cookieJar == CookieJar.noCookies) { 39 | return; 40 | } 41 | List cookies = CookieExtension.parseAllCookies(url, headers); 42 | if (cookies.isEmpty) { 43 | return; 44 | } 45 | await cookieJar.saveFromResponse(url, cookies); 46 | } 47 | 48 | static bool hasBody(Response response) { 49 | // HEAD requests never yield a body regardless of the response headers. 50 | if (response.request().method() == HttpMethod.head) { 51 | return false; 52 | } 53 | 54 | int responseCode = response.code(); 55 | if ((responseCode < HttpStatus.continue_ || 56 | responseCode >= HttpStatus.ok) && 57 | responseCode != HttpStatus.noContent && 58 | responseCode != HttpStatus.notModified) { 59 | return true; 60 | } 61 | 62 | // If the Content-Length or Transfer-Encoding headers disagree with the response code, the 63 | // response is malformed. For best compatibility, we honor the headers. 64 | if (contentLength(response) != -1 || 65 | (response.header(HttpHeaders.transferEncodingHeader) != null && 66 | response.header(HttpHeaders.transferEncodingHeader).toLowerCase() == 67 | 'chunked')) { 68 | return true; 69 | } 70 | 71 | return false; 72 | } 73 | 74 | static int contentLength(Response response) { 75 | String contentLength = response.header(HttpHeaders.contentLengthHeader); 76 | return contentLength != null ? (int.tryParse(contentLength) ?? -1) : -1; 77 | } 78 | 79 | static bool varyMatches( 80 | Response cachedResponse, Headers cachedVaryHeaders, Request newRequest) { 81 | Set fields = varyFields(cachedResponse.headers()); 82 | for (String field in fields) { 83 | List cachedVaryHeaderValues = cachedVaryHeaders.values(field); 84 | List newRequestHeaderValues = newRequest.headers().values(field); 85 | if (cachedVaryHeaderValues.length != newRequestHeaderValues.length) { 86 | return false; 87 | } 88 | for (String cachedVaryHeaderValue in cachedVaryHeaderValues) { 89 | if (!newRequestHeaderValues.contains(cachedVaryHeaderValue)) { 90 | return false; 91 | } 92 | } 93 | } 94 | return true; 95 | } 96 | 97 | static Headers varyHeaders(Response response) { 98 | // Use the request headers sent over the network, since that's what the 99 | // response varies on. Otherwise OkHttp-supplied headers like 100 | // "Accept-Encoding: gzip" may be lost. 101 | Headers requestHeaders = response.networkResponse().request().headers(); 102 | Headers responseHeaders = response.headers(); 103 | return _varyHeaders(requestHeaders, responseHeaders); 104 | } 105 | 106 | static Headers _varyHeaders(Headers requestHeaders, Headers responseHeaders) { 107 | Set fields = varyFields(responseHeaders); 108 | if (fields.isEmpty) { 109 | return HeadersBuilder().build(); 110 | } 111 | 112 | HeadersBuilder result = HeadersBuilder(); 113 | for (int i = 0, size = requestHeaders.size(); i < size; i++) { 114 | String name = requestHeaders.nameAt(i); 115 | if (fields.contains(name)) { 116 | result.add(name, requestHeaders.valueAt(i)); 117 | } 118 | } 119 | return result.build(); 120 | } 121 | 122 | static bool hasVaryAll(Headers responseHeaders) { 123 | return varyFields(responseHeaders).contains("*"); 124 | } 125 | 126 | static Set varyFields(Headers responseHeaders) { 127 | // ignore: prefer_collection_literals 128 | Set result = Set(); 129 | for (int i = 0, size = responseHeaders.size(); i < size; i++) { 130 | if (HttpHeaders.varyHeader != responseHeaders.nameAt(i)) { 131 | continue; 132 | } 133 | String value = responseHeaders.valueAt(i); 134 | for (String varyField in value.split(",")) { 135 | result.add(varyField.trim()); 136 | } 137 | } 138 | return result; 139 | } 140 | } 141 | 142 | class CookieExtension { 143 | CookieExtension._(); 144 | 145 | static List parseAllCookies(HttpUrl url, Headers headers) { 146 | List cookies = []; 147 | List cookieStrings = headers.values(HttpHeaders.setCookieHeader); 148 | cookieStrings.forEach((String value) { 149 | Cookie cookie = Cookie.fromSetCookieValue(value); 150 | if (cookie.domain == null || domainMatch(url.host(), cookie.domain)) { 151 | cookies.add(cookie); 152 | } 153 | }); 154 | return List.unmodifiable(cookies); 155 | } 156 | 157 | static bool domainMatch(String urlHost, String domain) { 158 | if (urlHost == domain) { 159 | return true; 160 | } 161 | if (urlHost.endsWith(domain) && 162 | urlHost.codeUnitAt(urlHost.length - domain.length - 1) == ascii.$dot && 163 | !Util.verifyAsIpAddress(urlHost)) { 164 | return true; // As in 'example.com' matching 'www.example.com'. 165 | } 166 | return false; 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /lib/okhttp3/tools/http_logging_interceptor.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:io'; 4 | 5 | import 'package:okhttp_kit/okhttp3/chain.dart'; 6 | import 'package:okhttp_kit/okhttp3/foundation/character.dart'; 7 | import 'package:okhttp_kit/okhttp3/headers.dart'; 8 | import 'package:okhttp_kit/okhttp3/interceptor.dart'; 9 | import 'package:okhttp_kit/okhttp3/internal/encoding_util.dart'; 10 | import 'package:okhttp_kit/okhttp3/internal/http_extension.dart'; 11 | import 'package:okhttp_kit/okhttp3/internal/util.dart'; 12 | import 'package:okhttp_kit/okhttp3/media_type.dart'; 13 | import 'package:okhttp_kit/okhttp3/request.dart'; 14 | import 'package:okhttp_kit/okhttp3/request_body.dart'; 15 | import 'package:okhttp_kit/okhttp3/response.dart'; 16 | import 'package:okhttp_kit/okhttp3/response_body.dart'; 17 | 18 | enum LoggingLevel { 19 | none, 20 | basic, 21 | headers, 22 | body, 23 | } 24 | 25 | /// 网络层拦截器 26 | class HttpLoggingInterceptor implements Interceptor { 27 | HttpLoggingInterceptor([ 28 | LoggingLevel level = LoggingLevel.basic, 29 | ]) : _level = level; 30 | 31 | final LoggingLevel _level; 32 | 33 | @override 34 | Future intercept(Chain chain) async { 35 | Request request = chain.request(); 36 | if (_level == LoggingLevel.none) { 37 | return await chain.proceed(request); 38 | } 39 | 40 | bool logBody = _level == LoggingLevel.body; 41 | bool logHeaders = logBody || _level == LoggingLevel.headers; 42 | 43 | RequestBody requestBody = request.body(); 44 | bool hasRequestBody = requestBody != null; 45 | if (!logHeaders && hasRequestBody) { 46 | print( 47 | '--> ${request.method()} ${request.url().toString()} ${requestBody.contentLength()}-byte body'); 48 | } else { 49 | print('--> ${request.method()} ${request.url().toString()}'); 50 | } 51 | 52 | if (logHeaders) { 53 | if (hasRequestBody) { 54 | if (requestBody.contentType() != null) { 55 | print( 56 | '${HttpHeaders.contentTypeHeader}: ${requestBody.contentType().toString()}'); 57 | } 58 | if (requestBody.contentLength() != -1) { 59 | print( 60 | '${HttpHeaders.contentLengthHeader}: ${requestBody.contentLength()}'); 61 | } 62 | } 63 | 64 | Headers headers = request.headers(); 65 | headers.names().forEach((String name) { 66 | if (!hasRequestBody || 67 | (HttpHeaders.contentTypeHeader != name && 68 | HttpHeaders.contentLengthHeader != name)) { 69 | print('$name: ${headers.value(name)}'); 70 | } 71 | }); 72 | 73 | if (!logBody || !hasRequestBody) { 74 | print('--> END ${request.method()}'); 75 | } else if (_bodyHasUnknownEncoding(request.headers())) { 76 | print('--> END ${request.method()} (encoded body omitted)'); 77 | } else { 78 | MediaType contentType = requestBody.contentType(); 79 | 80 | if (_isPlainContentType(contentType)) { 81 | List bytes = await Util.readAsBytes(requestBody.source()); 82 | 83 | Encoding encoding = EncodingUtil.encoding(contentType); 84 | String body = encoding.decode(bytes); 85 | 86 | if (_isPlainText(body)) { 87 | print(body); 88 | print('--> END ${request.method()} (${bytes.length}-byte body)'); 89 | } else { 90 | print( 91 | '--> END ${request.method()} (binary ${bytes.length}-byte body omitted)'); 92 | } 93 | 94 | request = request 95 | .newBuilder() 96 | .method( 97 | request.method(), RequestBody.bytesBody(contentType, bytes)) 98 | .build(); 99 | } else { 100 | print( 101 | '--> END ${request.method()} (binary ${requestBody.contentLength()}-byte body omitted)'); 102 | } 103 | } 104 | } 105 | 106 | Stopwatch watch = Stopwatch()..start(); 107 | Response response; 108 | try { 109 | response = await chain.proceed(request); 110 | } catch (e) { 111 | print('<-- HTTP FAILED: ${e.toString()}'); 112 | rethrow; 113 | } 114 | int tookMs = watch.elapsedMilliseconds; 115 | ResponseBody responseBody = response.body(); 116 | String bodySize = responseBody.contentLength() != -1 117 | ? '${responseBody.contentLength()}-byte body' 118 | : 'unknown-length body'; 119 | print( 120 | '<-- ${response.code()} ${response.message() ?? ''} ${response.request().url().toString()} (${tookMs}ms${!logHeaders ? ', $bodySize body' : ''})'); 121 | 122 | if (logHeaders) { 123 | if (responseBody.contentType() != null) { 124 | print( 125 | '${HttpHeaders.contentTypeHeader}: ${responseBody.contentType().toString()}'); 126 | } 127 | if (responseBody.contentLength() != -1) { 128 | print( 129 | '${HttpHeaders.contentLengthHeader}: ${responseBody.contentLength()}'); 130 | } 131 | 132 | Headers headers = response.headers(); 133 | headers.names().forEach((String name) { 134 | if (HttpHeaders.contentTypeHeader != name && 135 | HttpHeaders.contentLengthHeader != name) { 136 | print('$name: ${headers.value(name)}'); 137 | } 138 | }); 139 | 140 | if (!logBody || !HttpHeadersExtension.hasBody(response)) { 141 | print('<-- END HTTP'); 142 | } else if (_bodyHasUnknownEncoding(response.headers())) { 143 | print("<-- END HTTP (encoded body omitted)"); 144 | } else { 145 | MediaType contentType = responseBody.contentType(); 146 | 147 | if (_isPlainContentType(contentType)) { 148 | List bytes = await responseBody.bytes(); 149 | 150 | Encoding encoding = EncodingUtil.encoding(contentType); 151 | String body = encoding.decode(bytes); 152 | if (_isPlainText(body)) { 153 | print(body); 154 | print('<-- END HTTP (${bytes.length}-byte body)'); 155 | } else { 156 | print('<-- END HTTP (binary ${bytes.length}-byte body omitted)'); 157 | } 158 | 159 | response = response 160 | .newBuilder() 161 | .body(ResponseBody.bytesBody(contentType, bytes)) 162 | .build(); 163 | } else { 164 | print( 165 | '<-- END HTTP (binary ${responseBody.contentLength()}-byte body omitted)'); 166 | } 167 | } 168 | } 169 | return response; 170 | } 171 | 172 | bool _bodyHasUnknownEncoding(Headers headers) { 173 | String contentEncoding = headers.value(HttpHeaders.contentEncodingHeader); 174 | return contentEncoding != null && 175 | contentEncoding.toLowerCase() != 'identity' && 176 | contentEncoding.toLowerCase() != 'gzip'; 177 | } 178 | 179 | static bool _isPlainContentType(MediaType contentType) { 180 | if (contentType != null && 181 | ('text' == contentType.type().toLowerCase() || 182 | 'json' == contentType.subtype().toLowerCase() || 183 | 'application/x-www-form-urlencoded' == 184 | contentType.mimeType().toLowerCase())) { 185 | return true; 186 | } 187 | return false; 188 | } 189 | 190 | static bool _isPlainText(String body) { 191 | return body.runes.take(16).every((int rune) { 192 | return !Character.isIsoControl(rune) || Character.isWhitespace(rune); 193 | }); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /lib/okhttp3/internal/cache/cache_interceptor.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:okhttp_kit/okhttp3/cache.dart'; 5 | import 'package:okhttp_kit/okhttp3/chain.dart'; 6 | import 'package:okhttp_kit/okhttp3/headers.dart'; 7 | import 'package:okhttp_kit/okhttp3/interceptor.dart'; 8 | import 'package:okhttp_kit/okhttp3/internal/cache/cache_strategy.dart'; 9 | import 'package:okhttp_kit/okhttp3/internal/http/http_method.dart'; 10 | import 'package:okhttp_kit/okhttp3/internal/http_extension.dart'; 11 | import 'package:okhttp_kit/okhttp3/internal/util.dart'; 12 | import 'package:okhttp_kit/okhttp3/request.dart'; 13 | import 'package:okhttp_kit/okhttp3/response.dart'; 14 | import 'package:okhttp_kit/okhttp3/response_body.dart'; 15 | 16 | class CacheInterceptor implements Interceptor { 17 | CacheInterceptor( 18 | Cache cache, 19 | ) : _cache = cache; 20 | 21 | final Cache _cache; 22 | 23 | @override 24 | Future intercept(Chain chain) async { 25 | Response cacheCandidate = 26 | _cache != null ? await _cache.get(chain.request()) : null; 27 | 28 | int now = DateTime.now().millisecondsSinceEpoch; 29 | 30 | CacheStrategy strategy = 31 | CacheStrategyFactory(now, chain.request(), cacheCandidate).get(); 32 | Request networkRequest = strategy.networkRequest; 33 | Response cacheResponse = strategy.cacheResponse; 34 | 35 | if (_cache != null) { 36 | await _cache.trackResponse(strategy); 37 | } 38 | 39 | // If we're forbidden from using the network and the cache is insufficient, fail. 40 | if (networkRequest == null && cacheResponse == null) { 41 | return ResponseBuilder() 42 | .request(chain.request()) 43 | .code(HttpStatus.gatewayTimeout) 44 | .message("Unsatisfiable Request (only-if-cached)") 45 | .body(Util.emptyResponse) 46 | .sentRequestAtMillis(-1) 47 | .receivedResponseAtMillis(DateTime.now().millisecondsSinceEpoch) 48 | .build(); 49 | } 50 | 51 | // If we don't need the network, we're done. 52 | if (networkRequest == null) { 53 | return cacheResponse 54 | .newBuilder() 55 | .cacheResponse(_stripBody(cacheResponse)) 56 | .build(); 57 | } 58 | 59 | Response networkResponse = await chain.proceed(networkRequest); 60 | 61 | // If we have a cache response too, then we're doing a conditional get. 62 | if (cacheResponse != null) { 63 | if (networkResponse.code() == HttpStatus.notModified) { 64 | Response response = cacheResponse 65 | .newBuilder() 66 | .headers( 67 | _combine(cacheResponse.headers(), networkResponse.headers())) 68 | .sentRequestAtMillis(networkResponse.sentRequestAtMillis()) 69 | .receivedResponseAtMillis( 70 | networkResponse.receivedResponseAtMillis()) 71 | .cacheResponse(_stripBody(cacheResponse)) 72 | .networkResponse(_stripBody(networkResponse)) 73 | .build(); 74 | 75 | // Update the cache after combining headers but before stripping the 76 | // Content-Encoding header (as performed by initContentStream()). 77 | await _cache.trackConditionalCacheHit(); 78 | await _cache.update(cacheResponse, response); 79 | return response; 80 | } 81 | } 82 | 83 | Response response = networkResponse 84 | .newBuilder() 85 | .cacheResponse(_stripBody(cacheResponse)) 86 | .networkResponse(_stripBody(networkResponse)) 87 | .build(); 88 | 89 | if (_cache != null) { 90 | if (HttpHeadersExtension.hasBody(response) && 91 | CacheStrategy.isCacheable(response, networkRequest)) { 92 | // Offer this request to the cache. 93 | CacheRequest cacheRequest = await _cache.put(response); 94 | return _cacheWritingResponse(cacheRequest, response); 95 | } 96 | 97 | if (HttpMethod.invalidatesCache(networkRequest.method())) { 98 | try { 99 | await _cache.remove(networkRequest); 100 | } catch (e) { 101 | // The cache cannot be written. 102 | } 103 | } 104 | } 105 | 106 | return response; 107 | } 108 | 109 | Future _cacheWritingResponse( 110 | CacheRequest cacheRequest, Response response) async { 111 | if (cacheRequest == null) { 112 | return response; 113 | } 114 | EventSink> cacheSink = 115 | cacheRequest.body(); // 用作 EventSink,不然 StreamTransformer close 会报错 116 | if (cacheSink == null) { 117 | return response; 118 | } 119 | 120 | Stream> cacheWritingSource = 121 | StreamTransformer, List>.fromHandlers( 122 | handleData: (List data, EventSink> sink) { 123 | sink.add(data); 124 | cacheSink.add(data); 125 | }, 126 | handleError: 127 | (Object error, StackTrace stackTrace, EventSink> sink) { 128 | sink.addError(error, stackTrace); 129 | cacheSink.addError(error, stackTrace); 130 | }, 131 | handleDone: (EventSink> sink) { 132 | sink.close(); 133 | cacheSink.close(); 134 | }, 135 | ).bind(response.body().source()); 136 | 137 | return response 138 | .newBuilder() 139 | .body(ResponseBody.streamBody(response.body().contentType(), 140 | response.body().contentLength(), cacheWritingSource)) 141 | .build(); 142 | } 143 | 144 | static Response _stripBody(Response response) { 145 | return response != null && response.body() != null 146 | ? response.newBuilder().body(null).build() 147 | : response; 148 | } 149 | 150 | /// Combines cached headers with a network headers as defined by RFC 7234, 4.3.4. 151 | static Headers _combine(Headers cachedHeaders, Headers networkHeaders) { 152 | HeadersBuilder result = HeadersBuilder(); 153 | 154 | for (int i = 0, size = cachedHeaders.size(); i < size; i++) { 155 | String name = cachedHeaders.nameAt(i); 156 | String value = cachedHeaders.valueAt(i); 157 | if (name == HttpHeaders.warningHeader && value.startsWith('1')) { 158 | continue; // Drop 100-level freshness warnings. 159 | } 160 | if (_isContentSpecificHeader(name) || 161 | !_isEndToEnd(name) || 162 | networkHeaders.value(name) == null) { 163 | result.addLenient(name, value); 164 | } 165 | } 166 | 167 | for (int i = 0, size = networkHeaders.size(); i < size; i++) { 168 | String name = networkHeaders.nameAt(i); 169 | if (!_isContentSpecificHeader(name) && _isEndToEnd(name)) { 170 | result.addLenient(name, networkHeaders.valueAt(i)); 171 | } 172 | } 173 | 174 | return result.build(); 175 | } 176 | 177 | static bool _isContentSpecificHeader(String name) { 178 | return name == HttpHeaders.contentLengthHeader || 179 | name == HttpHeaders.contentEncodingHeader || 180 | name == HttpHeaders.contentTypeHeader; 181 | } 182 | 183 | static bool _isEndToEnd(String name) { 184 | return name != HttpHeaders.connectionHeader && 185 | name != 'keep-alive' && 186 | name != HttpHeaders.proxyAuthenticateHeader && 187 | name != HttpHeaders.proxyAuthorizationHeader && 188 | name != HttpHeaders.teHeader && 189 | name != HttpHeaders.trailerHeader && 190 | name != HttpHeaders.transferEncodingHeader && 191 | name != HttpHeaders.upgradeHeader; 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /lib/okhttp3/tools/persistent_cookie_jar.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:okhttp_kit/okhttp3/cookie_jar.dart'; 5 | import 'package:okhttp_kit/okhttp3/http_url.dart'; 6 | 7 | class PersistentCookieJar implements CookieJar { 8 | PersistentCookieJar._( 9 | _PersistentCookieStore cookieStore, 10 | ) : _cookieStore = cookieStore; 11 | 12 | final _PersistentCookieStore _cookieStore; 13 | 14 | @override 15 | Future saveFromResponse(HttpUrl url, List cookies) async { 16 | if (_cookieStore != null) { 17 | if (cookies != null && cookies.isNotEmpty) { 18 | List persistentCookies = cookies.map((Cookie cookie) { 19 | return PersistentCookie._(cookie); 20 | }).toList(); 21 | await _cookieStore.put( 22 | url, List.unmodifiable(persistentCookies)); 23 | } 24 | } 25 | } 26 | 27 | @override 28 | Future> loadForRequest(HttpUrl url) async { 29 | List cookies = []; 30 | if (_cookieStore != null) { 31 | List persistentCookies = await _cookieStore.get(url); 32 | if (persistentCookies != null && persistentCookies.isNotEmpty) { 33 | persistentCookies.forEach((PersistentCookie persistentCookie) { 34 | if (!persistentCookie.isExpired()) { 35 | cookies.add(persistentCookie._cookie); 36 | } 37 | }); 38 | } 39 | } 40 | return List.unmodifiable(cookies); 41 | } 42 | 43 | static PersistentCookieJar memory() { 44 | return persistent(CookiePersistor._memory); 45 | } 46 | 47 | static PersistentCookieJar persistent(CookiePersistor persistor) { 48 | return PersistentCookieJar._(_PersistentCookieStore._(persistor)); 49 | } 50 | } 51 | 52 | class PersistentCookie { 53 | PersistentCookie._( 54 | Cookie cookie, 55 | ) : assert(cookie != null), 56 | _cookie = cookie, 57 | _createTimestamp = (DateTime.now().millisecondsSinceEpoch ~/ 58 | Duration.millisecondsPerSecond) 59 | .toInt(); 60 | 61 | PersistentCookie.fromValue(String value) { 62 | List params = value.split('; $_COOKIE_CTS='); 63 | _cookie = Cookie.fromSetCookieValue(params[0]); 64 | _createTimestamp = int.tryParse(params[1]); 65 | } 66 | 67 | static const String _COOKIE_CTS = '_CTS'; 68 | 69 | Cookie _cookie; 70 | int _createTimestamp; 71 | 72 | String name() { 73 | return _cookie.name; 74 | } 75 | 76 | String domain() { 77 | return _cookie.domain; 78 | } 79 | 80 | String path() { 81 | return _cookie.path; 82 | } 83 | 84 | bool persistent() { 85 | return _cookie.expires != null || _cookie.maxAge != null; 86 | } 87 | 88 | int expiresAt() { 89 | // http 1.1 90 | if (_cookie.maxAge != null) { 91 | return _createTimestamp + _cookie.maxAge; 92 | } 93 | // http 1.0 94 | if (_cookie.expires != null) { 95 | return (_cookie.expires.millisecondsSinceEpoch ~/ 96 | Duration.millisecondsPerSecond) 97 | .toInt(); 98 | } 99 | return 0; 100 | } 101 | 102 | bool isExpired() { 103 | return (DateTime.now().millisecondsSinceEpoch ~/ 104 | Duration.millisecondsPerSecond) 105 | .toInt() > 106 | expiresAt(); 107 | } 108 | 109 | @override 110 | bool operator ==(dynamic other) { 111 | if (identical(this, other)) { 112 | return true; 113 | } 114 | return other is PersistentCookie && 115 | runtimeType == other.runtimeType && 116 | toString() == other.toString(); 117 | } 118 | 119 | @override 120 | int get hashCode { 121 | return toString().hashCode; 122 | } 123 | 124 | @override 125 | String toString() { 126 | return _cookie.toString() + "; $_COOKIE_CTS=$_createTimestamp"; 127 | } 128 | } 129 | 130 | class _PersistentCookieStore { 131 | _PersistentCookieStore._( 132 | CookiePersistor persistor, 133 | ) : assert(persistor != null), 134 | _persistor = persistor; 135 | 136 | final CookiePersistor _persistor; 137 | 138 | Future put( 139 | HttpUrl url, List persistentCookies) async { 140 | if (persistentCookies != null && persistentCookies.isNotEmpty) { 141 | HttpUrl index = _getEffectiveUrl(url); 142 | 143 | List persistPersistentCookies = 144 | await _persistor.load(index); 145 | List effectivePersistentCookies = 146 | persistPersistentCookies != null 147 | ? persistPersistentCookies.toList() 148 | : []; 149 | 150 | List shouldRemovePersistentCookies = 151 | []; 152 | persistentCookies.forEach((PersistentCookie persistentCookie) { 153 | effectivePersistentCookies 154 | .forEach((PersistentCookie effectivePersistentCookie) { 155 | if (effectivePersistentCookie._cookie == persistentCookie._cookie) { 156 | shouldRemovePersistentCookies.add(effectivePersistentCookie); 157 | } else if (effectivePersistentCookie.name() == 158 | persistentCookie.name() && 159 | effectivePersistentCookie.domain() == persistentCookie.domain() && 160 | effectivePersistentCookie.path() == persistentCookie.path()) { 161 | shouldRemovePersistentCookies.add(effectivePersistentCookie); 162 | } 163 | }); 164 | }); 165 | shouldRemovePersistentCookies 166 | .forEach((PersistentCookie shouldRemovePersistentCookie) { 167 | effectivePersistentCookies.remove(shouldRemovePersistentCookie); 168 | }); 169 | effectivePersistentCookies.addAll(persistentCookies); 170 | 171 | await _persistor.update(index, effectivePersistentCookies); 172 | } 173 | } 174 | 175 | Future> get(HttpUrl url) async { 176 | HttpUrl index = _getEffectiveUrl(url); 177 | 178 | List persistPersistentCookies = 179 | await _persistor.load(index); 180 | List effectivePersistentCookies = 181 | persistPersistentCookies != null 182 | ? persistPersistentCookies.toList() 183 | : []; 184 | 185 | return List.unmodifiable(effectivePersistentCookies); 186 | } 187 | 188 | HttpUrl _getEffectiveUrl(HttpUrl url) { 189 | return HttpUrlBuilder().scheme('http').host(url.host()).build(); 190 | } 191 | } 192 | 193 | abstract class CookiePersistor { 194 | static final CookiePersistor _memory = _MemoryCookiePersistor(); 195 | 196 | Future> load(HttpUrl index); 197 | 198 | Future update(HttpUrl index, List persistentCookies); 199 | 200 | Future clear(); 201 | } 202 | 203 | class _MemoryCookiePersistor implements CookiePersistor { 204 | final Map> _urlIndex = 205 | >{}; 206 | 207 | @override 208 | Future> load(HttpUrl index) async { 209 | List persistentCookies = _urlIndex[index]; 210 | return persistentCookies != null 211 | ? List.unmodifiable(persistentCookies) 212 | : null; 213 | } 214 | 215 | @override 216 | Future update( 217 | HttpUrl index, List persistentCookies) async { 218 | if (persistentCookies != null) { 219 | _urlIndex.update(index, (_) => persistentCookies, 220 | ifAbsent: () => persistentCookies); 221 | } else { 222 | _urlIndex.remove(index); 223 | } 224 | return true; 225 | } 226 | 227 | @override 228 | Future clear() async { 229 | _urlIndex.clear(); 230 | return true; 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /lib/okhttp3/multipart_body.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:io'; 4 | 5 | import 'package:okhttp_kit/okhttp3/headers.dart'; 6 | import 'package:okhttp_kit/okhttp3/internal/http_extension.dart'; 7 | import 'package:okhttp_kit/okhttp3/internal/util.dart'; 8 | import 'package:okhttp_kit/okhttp3/media_type.dart'; 9 | import 'package:okhttp_kit/okhttp3/request_body.dart'; 10 | 11 | class MultipartBody extends RequestBody { 12 | MultipartBody._( 13 | String boundary, 14 | MediaType type, 15 | List parts, 16 | ) : _boundary = boundary, 17 | _originalType = type, 18 | _contentType = 19 | MediaType.parse('${type.toString()}; boundary=$boundary'), 20 | _parts = parts; 21 | 22 | static final MediaType mixed = MediaType.parse('multipart/mixed'); 23 | static final MediaType alternative = MediaType.parse('multipart/alternative'); 24 | static final MediaType digest = MediaType.parse('multipart/digest'); 25 | static final MediaType parallel = MediaType.parse('multipart/parallel'); 26 | static final MediaType form = MediaType.parse('multipart/form-data'); 27 | 28 | static const String _colonSpace = ': '; 29 | static const String _crlf = '\r\n'; 30 | static const String _dashDash = '--'; 31 | 32 | final String _boundary; 33 | final MediaType _originalType; 34 | final MediaType _contentType; 35 | final List _parts; 36 | int _contentLength = -1; 37 | 38 | MediaType type() { 39 | return _originalType; 40 | } 41 | 42 | String boundary() { 43 | return _boundary; 44 | } 45 | 46 | int size() { 47 | return _parts.length; 48 | } 49 | 50 | List parts() { 51 | return _parts; 52 | } 53 | 54 | @override 55 | MediaType contentType() { 56 | return _contentType; 57 | } 58 | 59 | @override 60 | int contentLength() { 61 | if (_contentLength != -1) { 62 | return _contentLength; 63 | } 64 | 65 | List readAscii(String source) { 66 | return utf8.encode(source); 67 | } 68 | 69 | List readUtf8(String source) { 70 | return utf8.encode(source); 71 | } 72 | 73 | int length = 0; 74 | 75 | for (int p = 0, partCount = _parts.length; p < partCount; p++) { 76 | Part part = _parts[p]; 77 | Headers headers = part.headers(); 78 | RequestBody body = part.body(); 79 | 80 | length += readAscii(_dashDash).length; 81 | length += readAscii(_boundary).length; 82 | length += readAscii(_crlf).length; 83 | 84 | if (headers != null) { 85 | for (int h = 0, headerCount = headers.size(); h < headerCount; h++) { 86 | length += readUtf8(headers.nameAt(h)).length; 87 | length += readAscii(_colonSpace).length; 88 | length += readUtf8(headers.valueAt(h)).length; 89 | length += readAscii(_crlf).length; 90 | } 91 | } 92 | 93 | MediaType contentType = body.contentType(); 94 | if (contentType != null) { 95 | length += readUtf8(HttpHeaders.contentTypeHeader).length; 96 | length += readAscii(_colonSpace).length; 97 | length += readUtf8(contentType.toString()).length; 98 | length += readAscii(_crlf).length; 99 | } 100 | 101 | int contentLength = body.contentLength(); 102 | if (contentLength != -1) { 103 | length += readUtf8(HttpHeaders.contentLengthHeader).length; 104 | length += readAscii(_colonSpace).length; 105 | length += readUtf8('$contentLength').length; 106 | length += readAscii(_crlf).length; 107 | } else { 108 | return -1; 109 | } 110 | 111 | length += readAscii(_crlf).length; 112 | 113 | length += contentLength; 114 | 115 | length += readAscii(_crlf).length; 116 | } 117 | 118 | length += readAscii(_dashDash).length; 119 | length += readAscii(_boundary).length; 120 | length += readAscii(_dashDash).length; 121 | length += readAscii(_crlf).length; 122 | 123 | _contentLength = length; 124 | return _contentLength; 125 | } 126 | 127 | @override 128 | Stream> source() { 129 | StreamController> controller = 130 | StreamController>(sync: true); 131 | 132 | void writeAscii(String string) { 133 | controller.add(utf8.encode(string)); 134 | } 135 | 136 | void writeUtf8(String source) { 137 | controller.add(utf8.encode(source)); 138 | } 139 | 140 | Future> writeStreamToSink( 141 | Stream> stream, EventSink> sink) { 142 | Completer> completer = Completer>(); 143 | stream.listen(sink.add, 144 | onError: sink.addError, onDone: () => completer.complete()); 145 | return completer.future; 146 | } 147 | 148 | Future.forEach(_parts, (Part part) { 149 | Headers headers = part.headers(); 150 | RequestBody body = part.body(); 151 | 152 | writeAscii(_dashDash); 153 | writeAscii(_boundary); 154 | writeAscii(_crlf); 155 | 156 | if (headers != null) { 157 | for (int h = 0, headerCount = headers.size(); h < headerCount; h++) { 158 | writeUtf8(headers.nameAt(h)); 159 | writeAscii(_colonSpace); 160 | writeUtf8(headers.valueAt(h)); 161 | writeAscii(_crlf); 162 | } 163 | } 164 | 165 | MediaType contentType = body.contentType(); 166 | if (contentType != null) { 167 | writeUtf8(HttpHeaders.contentTypeHeader); 168 | writeAscii(_colonSpace); 169 | writeUtf8(contentType.toString()); 170 | writeAscii(_crlf); 171 | } 172 | 173 | int contentLength = body.contentLength(); 174 | if (contentLength != -1) { 175 | writeUtf8(HttpHeaders.contentLengthHeader); 176 | writeAscii(_colonSpace); 177 | writeUtf8('$contentLength'); 178 | writeAscii(_crlf); 179 | } 180 | 181 | writeAscii(_crlf); 182 | return writeStreamToSink(body.source(), controller).then((_) { 183 | writeAscii(_crlf); 184 | }); 185 | }).then((_) { 186 | writeAscii(_dashDash); 187 | writeAscii(_boundary); 188 | writeAscii(_dashDash); 189 | writeAscii(_crlf); 190 | controller.close(); 191 | }); 192 | 193 | return controller.stream; 194 | } 195 | } 196 | 197 | class Part { 198 | Part._( 199 | Headers headers, 200 | RequestBody body, 201 | ) : _headers = headers, 202 | _body = body; 203 | 204 | final Headers _headers; 205 | final RequestBody _body; 206 | 207 | Headers headers() { 208 | return _headers; 209 | } 210 | 211 | RequestBody body() { 212 | return _body; 213 | } 214 | 215 | static Part create(Headers headers, RequestBody body) { 216 | if (body == null) { 217 | throw ArgumentError.notNull('body'); 218 | } 219 | if (headers != null && 220 | headers.value(HttpHeaders.contentTypeHeader) != null) { 221 | throw ArgumentError( 222 | 'Unexpected header: ${HttpHeaders.contentTypeHeader}'); 223 | } 224 | if (headers != null && 225 | headers.value(HttpHeaders.contentLengthHeader) != null) { 226 | throw ArgumentError( 227 | 'Unexpected header: ${HttpHeaders.contentLengthHeader}'); 228 | } 229 | return Part._(headers, body); 230 | } 231 | 232 | static Part createFormData(String name, String filename, RequestBody body) { 233 | if (name == null) { 234 | throw ArgumentError.notNull('name'); 235 | } 236 | 237 | String disposition = 'form-data; name="${_browserEncode(name)}"'; 238 | if (filename != null) { 239 | disposition = '$disposition; filename="${_browserEncode(filename)}"'; 240 | } 241 | Headers headers = HeadersBuilder() 242 | .add(HttpHeadersExtension.contentDispositionHeader, 243 | disposition.toString()) 244 | .build(); 245 | return create(headers, body); 246 | } 247 | 248 | static String _browserEncode(String value) { 249 | // http://tools.ietf.org/html/rfc2388 mandates some complex encodings for 250 | // field names and file names, but in practice user agents seem not to 251 | // follow this at all. Instead, they URL-encode `\r`, `\n`, and `\r\n` as 252 | // `\r\n`; URL-encode `"`; and do nothing else (even for `%` or non-ASCII 253 | // characters). We follow their behavior. 254 | return value 255 | .replaceAll(RegExp(r'\r\n|\r|\n'), '%0D%0A') 256 | .replaceAll('"', '%22'); 257 | } 258 | } 259 | 260 | class MultipartBodyBuilder { 261 | MultipartBodyBuilder( 262 | String boundary, 263 | ) : _boundary = boundary ?? Util.boundaryString(); 264 | 265 | final String _boundary; 266 | MediaType _type = MultipartBody.mixed; 267 | final List _parts = []; 268 | 269 | MultipartBodyBuilder setType(MediaType type) { 270 | if (type == null) { 271 | throw ArgumentError.notNull('type'); 272 | } 273 | if (type.type() != 'multipart') { 274 | throw ArgumentError('${type.type()} != multipart'); 275 | } 276 | _type = type; 277 | return this; 278 | } 279 | 280 | MultipartBodyBuilder addPart(Headers headers, RequestBody body) { 281 | return _addPart(Part.create(headers, body)); 282 | } 283 | 284 | MultipartBodyBuilder addFormDataPart( 285 | String name, String filename, RequestBody body) { 286 | return _addPart(Part.createFormData(name, filename, body)); 287 | } 288 | 289 | MultipartBodyBuilder _addPart(Part part) { 290 | if (part == null) { 291 | throw ArgumentError.notNull('part'); 292 | } 293 | _parts.add(part); 294 | return this; 295 | } 296 | 297 | MultipartBody build() { 298 | if (_parts.isEmpty) { 299 | throw ArgumentError('Multipart body must have at least one part.'); 300 | } 301 | return MultipartBody._(_boundary, _type, List.unmodifiable(_parts)); 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /lib/okhttp3/internal/cache/cache_strategy.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:math' as math; 3 | 4 | import 'package:okhttp_kit/okhttp3/cache_control.dart'; 5 | import 'package:okhttp_kit/okhttp3/headers.dart'; 6 | import 'package:okhttp_kit/okhttp3/request.dart'; 7 | import 'package:okhttp_kit/okhttp3/response.dart'; 8 | 9 | class CacheStrategy { 10 | CacheStrategy._( 11 | this.networkRequest, 12 | this.cacheResponse, 13 | ); 14 | 15 | final Request networkRequest; 16 | final Response cacheResponse; 17 | 18 | static bool isCacheable(Response response, Request request) { 19 | // Always go to network for uncacheable response codes (RFC 7231 section 6.1), 20 | // This implementation doesn't support caching partial content. 21 | switch (response.code()) { 22 | case HttpStatus.ok: 23 | case HttpStatus.nonAuthoritativeInformation: 24 | case HttpStatus.noContent: 25 | case HttpStatus.multipleChoices: 26 | case HttpStatus.movedPermanently: 27 | case HttpStatus.notFound: 28 | case HttpStatus.methodNotAllowed: 29 | case HttpStatus.gone: 30 | case HttpStatus.requestUriTooLong: 31 | case HttpStatus.notImplemented: 32 | case HttpStatus.permanentRedirect: 33 | // These codes can be cached unless headers forbid it. 34 | break; 35 | case HttpStatus.movedTemporarily: 36 | case HttpStatus.temporaryRedirect: 37 | // These codes can only be cached with the right response headers. 38 | // http://tools.ietf.org/html/rfc7234#section-3 39 | // s-maxage is not checked because OkHttp is a private cache that should ignore s-maxage. 40 | if (response.header(HttpHeaders.expiresHeader) != null || 41 | response.cacheControl().maxAgeSeconds() != -1 || 42 | response.cacheControl().isPublic() || 43 | response.cacheControl().isPrivate()) { 44 | break; 45 | } 46 | return false; 47 | default: 48 | // All other codes cannot be cached. 49 | return false; 50 | } 51 | // A 'no-store' directive on request or response prevents the response from being cached. 52 | return !response.cacheControl().noStore() && 53 | !request.cacheControl().noStore(); 54 | } 55 | } 56 | 57 | class CacheStrategyFactory { 58 | CacheStrategyFactory( 59 | this.nowMillis, 60 | this.request, 61 | this.cacheResponse, 62 | ) { 63 | if (cacheResponse != null) { 64 | _sentRequestMillis = cacheResponse.sentRequestAtMillis(); 65 | _receivedResponseMillis = cacheResponse.receivedResponseAtMillis(); 66 | Headers headers = cacheResponse.headers(); 67 | for (int i = 0, size = headers.size(); i < size; i++) { 68 | String name = headers.nameAt(i); 69 | String value = headers.valueAt(i); 70 | if (name == HttpHeaders.dateHeader) { 71 | _servedDate = HttpDate.parse(value); 72 | _servedDateString = value; 73 | } else if (name == HttpHeaders.expiresHeader) { 74 | _expires = HttpDate.parse(value); 75 | } else if (name == HttpHeaders.lastModifiedHeader) { 76 | _lastModified = HttpDate.parse(value); 77 | _lastModifiedString = value; 78 | } else if (name == HttpHeaders.etagHeader) { 79 | _etag = value; 80 | } else if (name == HttpHeaders.ageHeader) { 81 | _ageSeconds = value != null ? (int.tryParse(value) ?? -1) : -1; 82 | } 83 | } 84 | } 85 | } 86 | 87 | final int nowMillis; 88 | final Request request; 89 | final Response cacheResponse; 90 | 91 | DateTime _servedDate; 92 | String _servedDateString; 93 | 94 | DateTime _lastModified; 95 | String _lastModifiedString; 96 | 97 | DateTime _expires; 98 | 99 | int _sentRequestMillis; 100 | 101 | int _receivedResponseMillis; 102 | 103 | String _etag; 104 | 105 | int _ageSeconds = -1; 106 | 107 | CacheStrategy get() { 108 | CacheStrategy candidate = _getCandidate(); 109 | if (candidate.networkRequest != null && 110 | request.cacheControl().onlyIfCached()) { 111 | return CacheStrategy._(null, null); 112 | } 113 | return candidate; 114 | } 115 | 116 | CacheStrategy _getCandidate() { 117 | // No cached response. 118 | if (cacheResponse == null) { 119 | return CacheStrategy._(request, null); 120 | } 121 | 122 | // If this response shouldn't have been stored, it should never be used 123 | // as a response source. This check should be redundant as long as the 124 | // persistence store is well-behaved and the rules are constant. 125 | if (!CacheStrategy.isCacheable(cacheResponse, request)) { 126 | return CacheStrategy._(request, null); 127 | } 128 | 129 | CacheControl requestCaching = request.cacheControl(); 130 | if (requestCaching.noCache() || _hasConditions(request)) { 131 | return CacheStrategy._(request, null); 132 | } 133 | 134 | CacheControl responseCaching = cacheResponse.cacheControl(); 135 | if (responseCaching.immutable()) { 136 | return CacheStrategy._(null, cacheResponse); 137 | } 138 | 139 | int ageMillis = _cacheResponseAge(); 140 | int freshMillis = _computeFreshnessLifetime(); 141 | 142 | if (requestCaching.maxAgeSeconds() != -1) { 143 | freshMillis = math.min(freshMillis, 144 | Duration(seconds: requestCaching.maxAgeSeconds()).inMilliseconds); 145 | } 146 | 147 | int minFreshMillis = 0; 148 | if (requestCaching.minFreshSeconds() != -1) { 149 | minFreshMillis = 150 | Duration(seconds: requestCaching.minFreshSeconds()).inMilliseconds; 151 | } 152 | 153 | int maxStaleMillis = 0; 154 | if (!responseCaching.mustRevalidate() && 155 | requestCaching.maxStaleSeconds() != -1) { 156 | maxStaleMillis = 157 | Duration(seconds: requestCaching.maxStaleSeconds()).inMilliseconds; 158 | } 159 | 160 | if (!responseCaching.noCache() && 161 | ageMillis + minFreshMillis < freshMillis + maxStaleMillis) { 162 | ResponseBuilder builder = cacheResponse.newBuilder(); 163 | if (ageMillis + minFreshMillis >= freshMillis) { 164 | builder.addHeader(HttpHeaders.warningHeader, 165 | "110 HttpURLConnection \"Response is stale\""); 166 | } 167 | int oneDayMillis = Duration(days: 1).inMilliseconds; 168 | if (ageMillis > oneDayMillis && _isFreshnessLifetimeHeuristic()) { 169 | builder.addHeader(HttpHeaders.warningHeader, 170 | "113 HttpURLConnection \"Heuristic expiration\""); 171 | } 172 | return CacheStrategy._(null, builder.build()); 173 | } 174 | 175 | // Find a condition to add to the request. If the condition is satisfied, the response body 176 | // will not be transmitted. 177 | String conditionName; 178 | String conditionValue; 179 | if (_etag != null) { 180 | conditionName = HttpHeaders.ifNoneMatchHeader; 181 | conditionValue = _etag; 182 | } else if (_lastModified != null) { 183 | conditionName = HttpHeaders.ifModifiedSinceHeader; 184 | conditionValue = _lastModifiedString; 185 | } else if (_servedDate != null) { 186 | conditionName = HttpHeaders.ifModifiedSinceHeader; 187 | conditionValue = _servedDateString; 188 | } else { 189 | return CacheStrategy._( 190 | request, null); // No condition! Make a regular request. 191 | } 192 | 193 | HeadersBuilder conditionalRequestHeaders = request.headers().newBuilder(); 194 | conditionalRequestHeaders.addLenient(conditionName, conditionValue); 195 | 196 | Request conditionalRequest = 197 | request.newBuilder().headers(conditionalRequestHeaders.build()).build(); 198 | return CacheStrategy._(conditionalRequest, cacheResponse); 199 | } 200 | 201 | int _computeFreshnessLifetime() { 202 | CacheControl responseCaching = cacheResponse.cacheControl(); 203 | if (responseCaching.maxAgeSeconds() != -1) { 204 | return Duration(seconds: responseCaching.maxAgeSeconds()).inMilliseconds; 205 | } else if (_expires != null) { 206 | int servedMillis = _servedDate != null 207 | ? _servedDate.millisecondsSinceEpoch 208 | : _receivedResponseMillis; 209 | int delta = _expires.millisecondsSinceEpoch - servedMillis; 210 | return delta > 0 ? delta : 0; 211 | } else if (_lastModified != null && 212 | cacheResponse.request().url().query() == null) { 213 | // As recommended by the HTTP RFC and implemented in Firefox, the 214 | // max age of a document should be defaulted to 10% of the 215 | // document's age at the time it was served. Default expiration 216 | // dates aren't used for URIs containing a query. 217 | int servedMillis = _servedDate != null 218 | ? _servedDate.millisecondsSinceEpoch 219 | : _sentRequestMillis; 220 | int delta = servedMillis - _lastModified.millisecondsSinceEpoch; 221 | return delta > 0 ? (delta ~/ 10) : 0; 222 | } 223 | return 0; 224 | } 225 | 226 | int _cacheResponseAge() { 227 | int apparentReceivedAge = _servedDate != null 228 | ? math.max( 229 | 0, _receivedResponseMillis - _servedDate.millisecondsSinceEpoch) 230 | : 0; 231 | int receivedAge = _ageSeconds != -1 232 | ? math.max( 233 | apparentReceivedAge, Duration(seconds: _ageSeconds).inMilliseconds) 234 | : apparentReceivedAge; 235 | int responseDuration = _receivedResponseMillis - _sentRequestMillis; 236 | int residentDuration = nowMillis - _receivedResponseMillis; 237 | return receivedAge + responseDuration + residentDuration; 238 | } 239 | 240 | bool _isFreshnessLifetimeHeuristic() { 241 | return cacheResponse.cacheControl().maxAgeSeconds() == -1 && 242 | _expires == null; 243 | } 244 | 245 | static bool _hasConditions(Request request) { 246 | return request.header(HttpHeaders.ifModifiedSinceHeader) != null || 247 | request.header(HttpHeaders.ifNoneMatchHeader) != null; 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /lib/okhttp3/cache_control.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:okhttp_kit/okhttp3/headers.dart'; 4 | import 'package:okhttp_kit/okhttp3/internal/http_extension.dart'; 5 | import 'package:fixnum/fixnum.dart'; 6 | 7 | class CacheControl { 8 | CacheControl._( 9 | bool noCache, 10 | bool noStore, 11 | int maxAgeSeconds, 12 | int sMaxAgeSeconds, 13 | bool isPrivate, 14 | bool isPublic, 15 | bool mustRevalidate, 16 | int maxStaleSeconds, 17 | int minFreshSeconds, 18 | bool onlyIfCached, 19 | bool noTransform, 20 | bool immutable, 21 | ) : _noCache = noCache, 22 | _noStore = noStore, 23 | _maxAgeSeconds = maxAgeSeconds, 24 | _sMaxAgeSeconds = sMaxAgeSeconds, 25 | _isPrivate = isPrivate, 26 | _isPublic = isPublic, 27 | _mustRevalidate = mustRevalidate, 28 | _maxStaleSeconds = maxStaleSeconds, 29 | _minFreshSeconds = minFreshSeconds, 30 | _onlyIfCached = onlyIfCached, 31 | _noTransform = noTransform, 32 | _immutable = immutable; 33 | 34 | CacheControl._fromBuilder( 35 | CacheControlBuilder builder, 36 | ) : _noCache = builder._noCache, 37 | _noStore = builder._noStore, 38 | _maxAgeSeconds = builder._maxAgeSeconds, 39 | _sMaxAgeSeconds = -1, 40 | _isPrivate = false, 41 | _isPublic = false, 42 | _mustRevalidate = false, 43 | _maxStaleSeconds = builder._maxStaleSeconds, 44 | _minFreshSeconds = builder._minFreshSeconds, 45 | _onlyIfCached = builder._onlyIfCached, 46 | _noTransform = builder._noTransform, 47 | _immutable = builder._immutable; 48 | 49 | static final CacheControl forceNetwork = 50 | CacheControlBuilder().noCache().build(); 51 | 52 | static final CacheControl forceCache = CacheControlBuilder() 53 | .onlyIfCached() 54 | .maxStale(Duration(seconds: Int32.MAX_VALUE.toInt())) 55 | .build(); 56 | 57 | static const String _params_no_cache = 'no-cache'; 58 | static const String _params_no_store = 'no-store'; 59 | static const String _params_max_age = 'max-age'; 60 | static const String _params_s_maxage = 's-maxage'; 61 | static const String _params_private = 'private'; 62 | static const String _params_public = 'public'; 63 | static const String _params_must_revalidate = 'must-revalidate'; 64 | static const String _params_max_stale = 'max-stale'; 65 | static const String _params_min_fresh = 'min-fresh'; 66 | static const String _params_only_if_cached = 'only-if-cached'; 67 | static const String _params_no_transform = 'no-transform'; 68 | static const String _params_immutable = 'immutable'; 69 | 70 | final bool _noCache; 71 | final bool _noStore; 72 | final int _maxAgeSeconds; 73 | final int _sMaxAgeSeconds; 74 | final bool _isPrivate; 75 | final bool _isPublic; 76 | final bool _mustRevalidate; 77 | final int _maxStaleSeconds; 78 | final int _minFreshSeconds; 79 | final bool _onlyIfCached; 80 | final bool _noTransform; 81 | final bool _immutable; 82 | 83 | bool noCache() { 84 | return _noCache; 85 | } 86 | 87 | bool noStore() { 88 | return _noStore; 89 | } 90 | 91 | int maxAgeSeconds() { 92 | return _maxAgeSeconds; 93 | } 94 | 95 | int sMaxAgeSeconds() { 96 | return _sMaxAgeSeconds; 97 | } 98 | 99 | bool isPrivate() { 100 | return _isPrivate; 101 | } 102 | 103 | bool isPublic() { 104 | return _isPublic; 105 | } 106 | 107 | bool mustRevalidate() { 108 | return _mustRevalidate; 109 | } 110 | 111 | int maxStaleSeconds() { 112 | return _maxStaleSeconds; 113 | } 114 | 115 | int minFreshSeconds() { 116 | return _minFreshSeconds; 117 | } 118 | 119 | bool onlyIfCached() { 120 | return _onlyIfCached; 121 | } 122 | 123 | bool noTransform() { 124 | return _noTransform; 125 | } 126 | 127 | bool immutable() { 128 | return _immutable; 129 | } 130 | 131 | @override 132 | String toString() { 133 | return _headerValue(); 134 | } 135 | 136 | String _headerValue() { 137 | StringBuffer result = StringBuffer(); 138 | if (_noCache) { 139 | result.write('$_params_no_cache, '); 140 | } 141 | if (_noStore) { 142 | result.write('$_params_no_store, '); 143 | } 144 | if (_maxAgeSeconds >= 0) { 145 | result.write('$_params_max_age=$_maxAgeSeconds, '); 146 | } 147 | if (_sMaxAgeSeconds >= 0) { 148 | result.write('$_params_s_maxage=$_sMaxAgeSeconds, '); 149 | } 150 | if (_isPrivate) { 151 | result.write('$_params_private, '); 152 | } 153 | if (_isPublic) { 154 | result.write('$_params_public, '); 155 | } 156 | if (_mustRevalidate) { 157 | result.write('$_params_must_revalidate, '); 158 | } 159 | if (_maxStaleSeconds >= 0) { 160 | result.write('$_params_max_stale=$_maxStaleSeconds, '); 161 | } 162 | if (_minFreshSeconds >= 0) { 163 | result.write('$_params_min_fresh=$_minFreshSeconds, '); 164 | } 165 | if (_onlyIfCached) { 166 | result.write('$_params_only_if_cached, '); 167 | } 168 | if (_noTransform) { 169 | result.write('$_params_no_transform, '); 170 | } 171 | if (_immutable) { 172 | result.write('$_params_immutable}, '); 173 | } 174 | return result.isNotEmpty 175 | ? result.toString().substring(0, result.length - 2) 176 | : ''; 177 | } 178 | 179 | static CacheControl parse(Headers headers) { 180 | bool noCache = false; 181 | bool noStore = false; 182 | int maxAgeSeconds = -1; 183 | int sMaxAgeSeconds = -1; 184 | bool isPrivate = false; 185 | bool isPublic = false; 186 | bool mustRevalidate = false; 187 | int maxStaleSeconds = -1; 188 | int minFreshSeconds = -1; 189 | bool onlyIfCached = false; 190 | bool noTransform = false; 191 | bool immutable = false; 192 | 193 | for (int i = 0, size = headers.size(); i < size; i++) { 194 | String name = headers.nameAt(i); 195 | String value = headers.valueAt(i); 196 | if (name == HttpHeaders.cacheControlHeader || 197 | name == HttpHeaders.pragmaHeader) { 198 | int pos = 0; 199 | while (pos < value.length) { 200 | int tokenStart = pos; 201 | pos = HttpHeadersExtension.skipUntil(value, pos, '=,;'); 202 | String directive = value.substring(tokenStart, pos).trim(); 203 | 204 | String parameter; 205 | if (pos == value.length || value[pos] == ',' || value[pos] == ';') { 206 | pos++; // consume ',' or ';' (if necessary) 207 | parameter = null; 208 | } else { 209 | pos++; // consume '=' 210 | pos = HttpHeadersExtension.skipWhitespace(value, pos); 211 | 212 | // quoted string 213 | if (pos < value.length && value[pos] == '\"') { 214 | pos++; // consume '"' open quote 215 | int parameterStart = pos; 216 | pos = HttpHeadersExtension.skipUntil(value, pos, '\"'); 217 | parameter = value.substring(parameterStart, pos); 218 | pos++; // consume '"' close quote (if necessary) 219 | 220 | // unquoted string 221 | } else { 222 | int parameterStart = pos; 223 | pos = HttpHeadersExtension.skipUntil(value, pos, ',;'); 224 | parameter = value.substring(parameterStart, pos).trim(); 225 | } 226 | } 227 | 228 | if (_params_no_cache == directive.toLowerCase()) { 229 | noCache = true; 230 | } else if (_params_no_store == directive.toLowerCase()) { 231 | noStore = true; 232 | } else if (_params_max_age == directive.toLowerCase()) { 233 | maxAgeSeconds = parameter != null ? int.tryParse(parameter) : -1; 234 | } else if (_params_s_maxage == directive.toLowerCase()) { 235 | sMaxAgeSeconds = parameter != null ? int.tryParse(parameter) : -1; 236 | } else if (_params_private == directive.toLowerCase()) { 237 | isPrivate = true; 238 | } else if (_params_public == directive.toLowerCase()) { 239 | isPublic = true; 240 | } else if (_params_must_revalidate == directive.toLowerCase()) { 241 | mustRevalidate = true; 242 | } else if (_params_max_stale == directive.toLowerCase()) { 243 | maxStaleSeconds = parameter != null 244 | ? int.tryParse(parameter) 245 | : Int32.MAX_VALUE.toInt(); 246 | } else if (_params_min_fresh == directive.toLowerCase()) { 247 | minFreshSeconds = parameter != null ? int.tryParse(parameter) : -1; 248 | } else if (_params_only_if_cached == directive.toLowerCase()) { 249 | onlyIfCached = true; 250 | } else if (_params_no_transform == directive.toLowerCase()) { 251 | noTransform = true; 252 | } else if (_params_immutable == directive.toLowerCase()) { 253 | immutable = true; 254 | } 255 | } 256 | } 257 | } 258 | return CacheControl._( 259 | noCache, 260 | noStore, 261 | maxAgeSeconds, 262 | sMaxAgeSeconds, 263 | isPrivate, 264 | isPublic, 265 | mustRevalidate, 266 | maxStaleSeconds, 267 | minFreshSeconds, 268 | onlyIfCached, 269 | noTransform, 270 | immutable, 271 | ); 272 | } 273 | } 274 | 275 | class CacheControlBuilder { 276 | CacheControlBuilder(); 277 | 278 | CacheControlBuilder._( 279 | CacheControl cacheControl, 280 | ) : _noCache = cacheControl._noCache, 281 | _noStore = cacheControl._noStore, 282 | _maxAgeSeconds = cacheControl._maxAgeSeconds, 283 | _maxStaleSeconds = cacheControl._maxStaleSeconds, 284 | _minFreshSeconds = cacheControl._minFreshSeconds, 285 | _onlyIfCached = cacheControl._onlyIfCached, 286 | _noTransform = cacheControl._noTransform, 287 | _immutable = cacheControl._immutable; 288 | 289 | bool _noCache = false; 290 | bool _noStore = false; 291 | int _maxAgeSeconds = -1; 292 | int _maxStaleSeconds = -1; 293 | int _minFreshSeconds = -1; 294 | bool _onlyIfCached = false; 295 | bool _noTransform = false; 296 | bool _immutable = false; 297 | 298 | CacheControlBuilder noCache() { 299 | _noCache = true; 300 | return this; 301 | } 302 | 303 | CacheControlBuilder noStore() { 304 | _noStore = true; 305 | return this; 306 | } 307 | 308 | CacheControlBuilder maxAge(Duration maxAge) { 309 | assert(maxAge != null); 310 | _maxAgeSeconds = maxAge.inSeconds; 311 | return this; 312 | } 313 | 314 | CacheControlBuilder maxStale(Duration maxStale) { 315 | assert(maxStale != null); 316 | _maxStaleSeconds = maxStale.inSeconds; 317 | return this; 318 | } 319 | 320 | CacheControlBuilder minFresh(Duration minFresh) { 321 | assert(minFresh != null); 322 | _minFreshSeconds = minFresh.inSeconds; 323 | return this; 324 | } 325 | 326 | CacheControlBuilder onlyIfCached() { 327 | _onlyIfCached = true; 328 | return this; 329 | } 330 | 331 | CacheControlBuilder noTransform() { 332 | _noTransform = true; 333 | return this; 334 | } 335 | 336 | CacheControlBuilder immutable() { 337 | _immutable = true; 338 | return this; 339 | } 340 | 341 | CacheControl build() { 342 | return CacheControl._fromBuilder(this); 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /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 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /lib/okhttp3/cache.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:io'; 4 | 5 | import 'package:convert/convert.dart'; 6 | import 'package:crypto/crypto.dart'; 7 | import 'package:okhttp_kit/okhttp3/headers.dart'; 8 | import 'package:okhttp_kit/okhttp3/http_url.dart'; 9 | import 'package:okhttp_kit/okhttp3/internal/cache/cache_strategy.dart'; 10 | import 'package:okhttp_kit/okhttp3/internal/http/http_method.dart'; 11 | import 'package:okhttp_kit/okhttp3/internal/http_extension.dart'; 12 | import 'package:okhttp_kit/okhttp3/internal/util.dart'; 13 | import 'package:okhttp_kit/okhttp3/media_type.dart'; 14 | import 'package:okhttp_kit/okhttp3/request.dart'; 15 | import 'package:okhttp_kit/okhttp3/response.dart'; 16 | import 'package:okhttp_kit/okhttp3/response_body.dart'; 17 | 18 | class Cache { 19 | Cache( 20 | RawCache cache, [ 21 | KeyExtractor keyExtractor, 22 | ]) : assert(cache != null), 23 | _cache = cache, 24 | _keyExtractor = keyExtractor ?? _defaultKeyExtractor; 25 | 26 | static const int version = 201105; 27 | static const int entryMetaData = 0; 28 | static const int entryBody = 1; 29 | static const int entryCount = 2; 30 | 31 | static final RegExp _legalKeyPattern = RegExp('[a-z0-9_-]{1,120}'); 32 | 33 | final RawCache _cache; 34 | final KeyExtractor _keyExtractor; 35 | int _networkCount = 0; 36 | int _hitCount = 0; 37 | int _requestCount = 0; 38 | 39 | int networkCount() { 40 | return _networkCount; 41 | } 42 | 43 | int hitCount() { 44 | return _hitCount; 45 | } 46 | 47 | int requestCount() { 48 | return _requestCount; 49 | } 50 | 51 | String _key(HttpUrl url) { 52 | String key = _keyExtractor(url); 53 | if (key == null || key.isEmpty) { 54 | throw AssertionError('key is null or empty'); 55 | } 56 | if (_legalKeyPattern.stringMatch(key) != key) { 57 | throw AssertionError('keys must match regex [a-z0-9_-]{1,120}: \"$key\"'); 58 | } 59 | return key; 60 | } 61 | 62 | Future trackConditionalCacheHit() async { 63 | _networkCount++; 64 | } 65 | 66 | Future trackResponse(CacheStrategy cacheStrategy) async { 67 | _requestCount++; 68 | 69 | if (cacheStrategy.networkRequest != null) { 70 | // If this is a conditional request, we'll increment hitCount if/when it hits. 71 | _networkCount++; 72 | } else if (cacheStrategy.cacheResponse != null) { 73 | // This response uses the cache and not the network. That's a cache hit. 74 | _hitCount++; 75 | } 76 | } 77 | 78 | Future get(Request request) async { 79 | String key = _key(request.url()); 80 | Snapshot snapshot; 81 | Entry entry; 82 | try { 83 | snapshot = await _cache.get(key); 84 | if (snapshot == null) { 85 | return null; 86 | } 87 | } catch (e) { 88 | // Give up because the cache cannot be read. 89 | return null; 90 | } 91 | 92 | try { 93 | entry = await Entry.sourceEntry(snapshot.getSource(entryMetaData)); 94 | } catch (e) { 95 | return null; 96 | } 97 | 98 | Response response = entry.response(snapshot); 99 | 100 | if (!entry.matches(request, response)) { 101 | return null; 102 | } 103 | 104 | return response; 105 | } 106 | 107 | Future put(Response response) async { 108 | String requestMethod = response.request().method(); 109 | if (HttpMethod.invalidatesCache(requestMethod)) { 110 | try { 111 | await remove(response.request()); 112 | } catch (e) { 113 | // The cache cannot be written. 114 | } 115 | return null; 116 | } 117 | if (requestMethod != HttpMethod.get) { 118 | // Don't cache non-GET responses. We're technically allowed to cache 119 | // HEAD requests and some POST requests, but the complexity of doing 120 | // so is high and the benefit is low. 121 | return null; 122 | } 123 | 124 | if (HttpHeadersExtension.hasVaryAll(response.headers())) { 125 | return null; 126 | } 127 | 128 | Entry entry = Entry.responseEntry(response); 129 | Editor editor; 130 | try { 131 | editor = await _cache.edit(_key(response.request().url())); 132 | if (editor == null) { 133 | return null; 134 | } 135 | List metaData = entry.metaData(); 136 | return CacheRequest(editor, metaData); 137 | } catch (e) { 138 | await _abortQuietly(editor); 139 | return null; 140 | } 141 | } 142 | 143 | Future update(Response cached, Response network) async { 144 | Entry entry = Entry.responseEntry(network); 145 | _CacheResponseBody body = cached.body() as _CacheResponseBody; 146 | Snapshot snapshot = body.snapshot(); 147 | Editor editor; 148 | try { 149 | editor = await _cache.edit(snapshot.key(), snapshot.sequenceNumber()); 150 | if (editor != null) { 151 | List metaData = entry.metaData(); 152 | EventSink> sink = editor.newSink(Cache.entryMetaData, utf8); 153 | sink.add(metaData); 154 | editor.commit(); 155 | } 156 | } catch (e) { 157 | await _abortQuietly(editor); 158 | } 159 | } 160 | 161 | Future remove(Request request) { 162 | return _cache.remove(_key(request.url())); 163 | } 164 | 165 | Future _abortQuietly(Editor editor) async { 166 | // Give up because the cache cannot be written. 167 | try { 168 | if (editor != null) { 169 | editor.abort(); 170 | } 171 | } catch (e) { 172 | // do nothing 173 | } 174 | } 175 | } 176 | 177 | class CacheRequest { 178 | CacheRequest(this.editor, this.metaData); 179 | 180 | final Editor editor; 181 | final List metaData; 182 | 183 | EventSink> body() { 184 | EventSink> bodySink; 185 | StreamController> streamController = 186 | StreamController>(); 187 | streamController.stream.listen( 188 | (List event) { 189 | // add 190 | if (bodySink == null) { 191 | bodySink = editor.newSink(Cache.entryBody, utf8); 192 | } 193 | bodySink.add(event); 194 | }, 195 | onError: (Object error, StackTrace stackTrace) { 196 | // detch 197 | bodySink?.addError(error, stackTrace); 198 | editor.abort(); 199 | }, 200 | onDone: () { 201 | // close 202 | EventSink> metaDataSink = 203 | editor.newSink(Cache.entryMetaData, utf8); 204 | metaDataSink.add(metaData); 205 | editor.commit(); 206 | }, 207 | cancelOnError: true, 208 | ); 209 | return streamController; 210 | } 211 | } 212 | 213 | abstract class Editor { 214 | StreamSink> newSink(int index, Encoding encoding); 215 | 216 | Stream> newSource(int index, Encoding encoding); 217 | 218 | void commit(); 219 | 220 | void abort(); 221 | } 222 | 223 | abstract class RawCache { 224 | static const int anySequenceNumber = -1; 225 | 226 | Future get(String key); 227 | 228 | Future edit(String key, [int expectedSequenceNumber]); 229 | 230 | Future remove(String key); 231 | } 232 | 233 | typedef String KeyExtractor(HttpUrl url); 234 | 235 | String _defaultKeyExtractor(HttpUrl url) => 236 | hex.encode(md5.convert(utf8.encode(url.toString())).bytes); 237 | 238 | class Snapshot { 239 | Snapshot( 240 | String key, 241 | int sequenceNumber, 242 | List>> sources, 243 | List lengths, 244 | ) : _key = key, 245 | _sequenceNumber = sequenceNumber, 246 | _sources = sources, 247 | _lengths = lengths; 248 | 249 | final String _key; 250 | final int _sequenceNumber; 251 | final List>> _sources; 252 | final List _lengths; 253 | 254 | String key() { 255 | return _key; 256 | } 257 | 258 | int sequenceNumber() { 259 | return _sequenceNumber; 260 | } 261 | 262 | Stream> getSource(int index) { 263 | return _sources[index]; 264 | } 265 | 266 | int getLength(int index) { 267 | return _lengths[index]; 268 | } 269 | } 270 | 271 | class Entry { 272 | Entry( 273 | String url, 274 | String requestMethod, 275 | Headers varyHeaders, 276 | int code, 277 | String message, 278 | Headers responseHeaders, 279 | int sentRequestMillis, 280 | int receivedResponseMillis, 281 | ) : _url = url, 282 | _requestMethod = requestMethod, 283 | _varyHeaders = varyHeaders, 284 | _code = code, 285 | _message = message, 286 | _responseHeaders = responseHeaders, 287 | _sentRequestMillis = sentRequestMillis, 288 | _receivedResponseMillis = receivedResponseMillis; 289 | 290 | static const String _sentMillis = 'OkHttp-Sent-Millis'; 291 | static const String _receivedMillis = 'OkHttp-Received-Millis'; 292 | 293 | final String _url; 294 | final String _requestMethod; 295 | final Headers _varyHeaders; 296 | final int _code; 297 | final String _message; 298 | final Headers _responseHeaders; 299 | final int _sentRequestMillis; 300 | final int _receivedResponseMillis; 301 | 302 | List metaData() { 303 | StringBuffer builder = StringBuffer(); 304 | builder.writeln(_url); 305 | builder.writeln(_requestMethod); 306 | builder.writeln(_varyHeaders.size().toString()); 307 | for (int i = 0, size = _varyHeaders.size(); i < size; i++) { 308 | builder.writeln('${_varyHeaders.nameAt(i)}: ${_varyHeaders.valueAt(i)}'); 309 | } 310 | builder.writeln('$_code $_message'); 311 | Headers responseHeaders = _responseHeaders 312 | .newBuilder() 313 | .set(_sentMillis, _sentRequestMillis.toString()) 314 | .set(_receivedMillis, _receivedResponseMillis.toString()) 315 | .build(); 316 | builder.writeln(responseHeaders.size().toString()); 317 | for (int i = 0, size = responseHeaders.size(); i < size; i++) { 318 | builder.writeln( 319 | '${responseHeaders.nameAt(i)}: ${responseHeaders.valueAt(i)}'); 320 | } 321 | return utf8.encode(builder.toString()); 322 | } 323 | 324 | Response response(Snapshot snapshot) { 325 | String contentTypeString = 326 | _responseHeaders.value(HttpHeaders.contentTypeHeader); 327 | MediaType contentType = 328 | contentTypeString != null ? MediaType.parse(contentTypeString) : null; 329 | String contentLengthString = 330 | _responseHeaders.value(HttpHeaders.contentLengthHeader); 331 | int contentLength = contentLengthString != null 332 | ? (int.tryParse(contentLengthString) ?? -1) 333 | : -1; 334 | Request cacheRequest = RequestBuilder() 335 | .url(HttpUrl.parse(_url)) 336 | .method(_requestMethod, null) 337 | .headers(_varyHeaders) 338 | .build(); 339 | return ResponseBuilder() 340 | .request(cacheRequest) 341 | .code(_code) 342 | .message(_message) 343 | .headers(_responseHeaders) 344 | .body(_CacheResponseBody(contentType, contentLength, snapshot)) 345 | .sentRequestAtMillis(_sentRequestMillis) 346 | .receivedResponseAtMillis(_receivedResponseMillis) 347 | .build(); 348 | } 349 | 350 | bool matches(Request request, Response response) { 351 | return _url == request.url().toString() && 352 | _requestMethod == request.method() && 353 | HttpHeadersExtension.varyMatches(response, _varyHeaders, request); 354 | } 355 | 356 | static Future sourceEntry(Stream> source) async { 357 | List lines = await Util.readAsBytes(source).then((List bytes) { 358 | return utf8.decode(bytes); 359 | }).then(const LineSplitter().convert); 360 | 361 | int cursor = 0; 362 | String url = lines[cursor++]; 363 | String requestMethod = lines[cursor++]; 364 | HeadersBuilder varyHeadersBuilder = HeadersBuilder(); 365 | int varyRequestHeaderLineCount = int.tryParse(lines[cursor++]); 366 | for (int i = 0; i < varyRequestHeaderLineCount; i++) { 367 | varyHeadersBuilder.addLenientLine(lines[cursor++]); 368 | } 369 | Headers varyHeaders = varyHeadersBuilder.build(); 370 | 371 | String statusLine = lines[cursor++]; 372 | if (statusLine == null || statusLine.length < 3) { 373 | throw Exception('Unexpected status line: $statusLine'); 374 | } 375 | int code = int.tryParse(statusLine.substring(0, 3)); 376 | String message = statusLine.substring(3).replaceFirst(' ', ''); 377 | 378 | HeadersBuilder responseHeadersBuilder = HeadersBuilder(); 379 | int responseHeaderLineCount = int.tryParse(lines[cursor++]); 380 | for (int i = 0; i < responseHeaderLineCount; i++) { 381 | responseHeadersBuilder.addLenientLine(lines[cursor++]); 382 | } 383 | Headers responseHeaders = responseHeadersBuilder.build(); 384 | 385 | String sendRequestMillisString = responseHeaders.value(_sentMillis); 386 | String receivedResponseMillisString = 387 | responseHeaders.value(_receivedMillis); 388 | 389 | responseHeaders = responseHeaders 390 | .newBuilder() 391 | .removeAll(_sentMillis) 392 | .removeAll(_receivedMillis) 393 | .build(); 394 | 395 | int sentRequestMillis = int.tryParse(sendRequestMillisString); 396 | int receivedResponseMillis = int.tryParse(receivedResponseMillisString); 397 | 398 | return Entry(url, requestMethod, varyHeaders, code, message, 399 | responseHeaders, sentRequestMillis, receivedResponseMillis); 400 | } 401 | 402 | static Entry responseEntry(Response response) { 403 | String url = response.request().url().toString(); 404 | Headers varyHeaders = HttpHeadersExtension.varyHeaders(response); 405 | String requestMethod = response.request().method(); 406 | int code = response.code(); 407 | String message = response.message(); 408 | Headers responseHeaders = response.headers(); 409 | int sentRequestMillis = response.sentRequestAtMillis(); 410 | int receivedResponseMillis = response.receivedResponseAtMillis(); 411 | return Entry(url, requestMethod, varyHeaders, code, message, 412 | responseHeaders, sentRequestMillis, receivedResponseMillis); 413 | } 414 | } 415 | 416 | class _CacheResponseBody extends ResponseBody { 417 | _CacheResponseBody( 418 | MediaType contentType, 419 | int contentLength, 420 | Snapshot snapshot, 421 | ) : _contentType = contentType, 422 | _contentLength = contentLength, 423 | _snapshot = snapshot; 424 | 425 | final MediaType _contentType; 426 | final int _contentLength; 427 | final Snapshot _snapshot; 428 | 429 | @override 430 | MediaType contentType() { 431 | return _contentType; 432 | } 433 | 434 | @override 435 | int contentLength() { 436 | return _contentLength; 437 | } 438 | 439 | Snapshot snapshot() { 440 | return _snapshot; 441 | } 442 | 443 | @override 444 | Stream> source() { 445 | return _snapshot.getSource(Cache.entryBody); 446 | } 447 | } 448 | --------------------------------------------------------------------------------