├── .analysis_options ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── example ├── oauth_example.dart ├── oauth_example.html ├── oauth_example_compute_engine.dart └── oauth_example_console.dart ├── lib ├── google_oauth2_browser.dart ├── google_oauth2_console.dart └── src │ ├── browser │ ├── google_oauth2.dart │ ├── oauth2.dart │ ├── proxy_callback.dart │ ├── simple_oauth2.dart │ ├── token.dart │ └── utils.dart │ ├── common │ └── url_pattern.dart │ └── console │ └── oauth2_console_client │ ├── exit_codes.dart │ ├── http.dart │ ├── io.dart │ ├── log.dart │ ├── oauth2.dart │ └── utils.dart ├── pubspec.yaml └── tool └── hop_runner.dart /.analysis_options: -------------------------------------------------------------------------------- 1 | analyzer: 2 | strong-mode: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | packages/ 2 | build/ 3 | pubspec.lock 4 | out 5 | credentials.json 6 | packages 7 | lib/docs 8 | *.dart.js 9 | *.dart.js.* 10 | *.dart.*.js 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.4.2 2 | 3 | - Strong mode compliant 4 | 5 | ## 0.4.1 6 | 7 | - Added optional `tokenValidationUri` to `GoogleOAuth2` constructor. 8 | 9 | ## 0.4.0 2014-08-12 (SDK 1.6.0-dev.8.0 r38967) 10 | 11 | - Added completeError() for un-handled exceptions and cases like immediate + force prompt 12 | - Refactored the code/comments for better readability 13 | - Clarify the process of creating a Client ID 14 | - Add instructions for the latest version of the APIs console 15 | 16 | ## 0.3.9 2014-06-29 (SDK 1.5.0-dev.4.23 r37639) 17 | 18 | * Auto load stored token 19 | 20 | ## 0.3.8 21 | 22 | * Updated many dependencies. 23 | 24 | ## 0.3.7 2014-03-28 (SDK 1.3.0-dev.7.2 r34463) 25 | 26 | - bump of `json_web_token` version 27 | 28 | ## 0.3.6 2014-03-27 (SDK 1.3.0-dev.7.2 r34463) 29 | 30 | - Added JWT support for `ComputeOAuth2Console`, now supporting 31 | non-Google Compute Engine clients 32 | 33 | ## 0.3.5 2014-03-22 (SDK 1.3.0-dev.5.2 r34229) 34 | 35 | - Added implementation of `ComputeOAuth2Console` for connecting to Google Compute Engine 36 | - Cleaned up warnings and missing overrides 37 | 38 | ## 0.3.4 2014-02-21 (SDK 1.2.0-dev.5.12 r32844) 39 | 40 | - Added a `authorziationResponseServerPort` configuration option. 41 | - add support for approval_prompt option (`force` or `auto`) 42 | 43 | ## 0.3.3 2013-12-27 (SDK 1.1.0-dev.5.0 r31329) 44 | 45 | - Upgrading package dependencies 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ------------------------ 2 | 3 | Copyright (c) 2013-2014 Gerwin Sturm & Adam Singer 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); you may not 6 | use this file except in compliance with the License. You may obtain a copy of 7 | the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | License for the specific language governing permissions and limitations under 15 | the License 16 | 17 | ------------------------ 18 | Based on http://code.google.com/p/google-api-dart-client 19 | 20 | Copyright 2012 Google Inc. 21 | Licensed under the Apache License, Version 2.0 (the "License"); you may not 22 | use this file except in compliance with the License. You may obtain a copy of 23 | the License at 24 | 25 | http://www.apache.org/licenses/LICENSE-2.0 26 | 27 | Unless required by applicable law or agreed to in writing, software 28 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 29 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 30 | License for the specific language governing permissions and limitations under 31 | the License 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # google_oauth2_client 2 | 3 | [![Build Status](https://drone.io/github.com/dart-gde/dart-google-oauth2-library/status.png)](https://drone.io/github.com/dart-gde/dart-google-oauth2-library/latest) 4 | 5 | ### Description 6 | 7 | Dart library to use for Google OAuth2 authentication / Client-side flow 8 | 9 | 10 | ### Usage/Installation 11 | 12 | 13 | Go to [Google APIs Console](https://code.google.com/apis/console/) and: 14 | - Create a new Project, 15 | - Create a new `Client ID` for web applications in "API Access" - You should select the "APIS and AUTH > Credentials" left menu item in the new APIs console and click on the "Create new Client ID" button, 16 | - Set JavaScript origins to your server or for example `http://127.0.0.1:3030/` for local testing in Dartium 17 | 18 | Add this dependency to your pubspec.yaml 19 | 20 | ``` 21 | dependencies: 22 | google_oauth2_client: '>=0.3.4' 23 | ``` 24 | 25 | ### Web applications 26 | 27 | Import the library in your dart application 28 | 29 | ``` 30 | import "package:google_oauth2_client/google_oauth2_browser.dart"; 31 | ``` 32 | 33 | Initialize the library with your parameters 34 | 35 | ``` 36 | final auth = new GoogleOAuth2( 37 | "YOUR CLIENT ID HERE", 38 | ["scope1", "scope2", ...], 39 | tokenLoaded: oauthReady, 40 | autoLogin: ); 41 | ``` 42 | 43 | The `oauthReady` function (a function you must define) will be called once your app has a valid OAuth token to call the APIs. 44 | If you set `autoLogin` to `true` and the user has authorized the app in the past, this will happen automatically. 45 | Otherwise, you need to call `auth.login()` to trigger a confirmation dialog. 46 | 47 | Once you have an access token you can use the following to send authenticated requests to the API. 48 | 49 | ``` 50 | var request = new HttpRequest(); 51 | request.onLoad.listen(...) 52 | request.open(method, url); 53 | request.setRequestHeader("Authorization", "${auth.token.type} ${auth.token.data}"); 54 | request.send(); 55 | ``` 56 | 57 | Or you can use the `authenticate` method of the OAuth2 class that takes a request, refreshes the access token if necessary and returns a request with the headers set correctly. 58 | 59 | ``` 60 | var request = new HttpRequest(); 61 | request.onLoad.listen(...); 62 | request.open(method, url); 63 | auth.authenticate(request).then((request) => request.send()); 64 | ``` 65 | 66 | If you have an access token already (f.e. by using the Chrome Extension Identity API) you can use the SimpleOAuth2 class instead, that also supports the `authenticate` method. 67 | 68 | ``` 69 | var auth = new SimpleOAuth2(myToken); 70 | var request new HttpRequest(); 71 | request.onLoad.listen(...); 72 | request.open(method, url); 73 | auth.authenticate(request).then((request) => request.send()); 74 | ``` 75 | 76 | 77 | See [example/oauth_example.dart](https://github.com/dart-gde/dart-google-oauth2-library/blob/master/example/oauth_example.dart) for example login and request. 78 | 79 | ### Console applications 80 | 81 | Import the library in your dart application 82 | 83 | ``` 84 | import "package:google_oauth2_client/google_oauth2_console.dart"; 85 | ``` 86 | Setup the `identifier` and `secret` by creating a [Google Installed App](https://developers.google.com/accounts/docs/OAuth2InstalledApp) client id in [APIs Console](https://code.google.com/apis/console) 87 | 88 | ``` 89 | String identifier = "YOUR IDENTIFIER HERE"; 90 | String secret = "YOUR SECRET HERE"; 91 | List scopes = ["scope1", "scope2", ...]; 92 | final auth = new OAuth2Console(identifier: identifier, secret: secret, scopes: scopes); 93 | ``` 94 | 95 | When making calls the `OAuth2Console` provides a `widthClient` method that will provide you with the `http.Client` which to make requests. This may change in the future, for now it handles if the client has not allowed access to this application. credentials are stored locally by default in a file named `credentials.json`. Also by default the application does not check googles certificates, a certificate is provided [ca-certificates.crt](lib/src/console/oauth2_console_client/ca-certificates.crt). Place the certificate in the same folder as the application curl will check cert before executing. 96 | 97 | ``` 98 | Future clientCallback(http.Client client) { 99 | var completer = new Completer(); 100 | final url = "https://www.googleapis.com/plus/v1/people/me"; 101 | client.get(url).then((http.Response response) { 102 | var data = JSON.parse(response.body); 103 | print("Logged in as ${data["displayName"]}"); 104 | }); 105 | return completer.future; 106 | }; 107 | 108 | auth.withClient(clientCallback); 109 | ``` 110 | 111 | Example below, the user needs to open the link provided to allow for offline support of the application. 112 | 113 | ``` 114 | ~/dart/dart-google-oauth2-library/example$ dart oauth_example_console.dart 115 | 116 | Client needs your authorization for scopes [https://www.googleapis.com/auth/plus.me] 117 | In a web browser, go to https://accounts.google.com/o/oauth2/auth?access_type=offline&approval_prompt=force&response_type=code&client_id=299615367852-n0kfup30mfj5emlclfgud9g76itapvk9.apps.googleusercontent.com&redirect_uri=http%3A%2F%2Flocalhost%3A60476&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fplus.me 118 | Then click "Allow access". 119 | 120 | Waiting for your authorization... 121 | Authorization received, processing... 122 | Successfully authorized. 123 | 124 | Logged in as Adam Singer 125 | ``` 126 | 127 | Currently console oauth2 does not work on windows yet. Mac and Linux should work if `curl` is in your path. `curl` is being used for passing the auth token from the browser back to the application. 128 | 129 | ### Disclaimer 130 | 131 | No guarantees about the security or functionality of this libary 132 | 133 | ### Licenses 134 | 135 | ``` 136 | Copyright (c) 2013-2014 Gerwin Sturm & Adam Singer 137 | 138 | Licensed under the Apache License, Version 2.0 (the "License"); you may not 139 | use this file except in compliance with the License. You may obtain a copy of 140 | the License at 141 | 142 | http://www.apache.org/licenses/LICENSE-2.0 143 | 144 | Unless required by applicable law or agreed to in writing, software 145 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 146 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 147 | License for the specific language governing permissions and limitations under 148 | the License 149 | 150 | ------------------------ 151 | Based on http://code.google.com/p/google-api-dart-client 152 | 153 | Copyright 2012 Google Inc. 154 | Licensed under the Apache License, Version 2.0 (the "License"); you may not 155 | use this file except in compliance with the License. You may obtain a copy of 156 | the License at 157 | 158 | http://www.apache.org/licenses/LICENSE-2.0 159 | 160 | Unless required by applicable law or agreed to in writing, software 161 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 162 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 163 | License for the specific language governing permissions and limitations under 164 | the License 165 | ``` 166 | -------------------------------------------------------------------------------- /example/oauth_example.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import "dart:html"; 3 | import "dart:convert"; 4 | 5 | import "package:google_oauth2_client/google_oauth2_browser.dart"; 6 | 7 | final ButtonElement loginButton = querySelector("#login"); 8 | final logoutButton = querySelector("#logout"); 9 | final outputDiv = querySelector("#output"); 10 | final DivElement loginWrapper = querySelector("#login_wrapper"); 11 | final SelectElement approvalPromptInput = querySelector("#approval_prompt"); 12 | final SelectElement immediateInput = querySelector("#immediate"); 13 | final SelectElement onlyLoadTokenInput = querySelector("#onlyLoadToken"); 14 | 15 | void main() { 16 | // use your own Client ID from the API Console here 17 | final auth = new GoogleOAuth2( 18 | "796343192238.apps.googleusercontent.com", 19 | ["https://www.googleapis.com/auth/books"]); 20 | 21 | outputDiv.innerHtml = ""; 22 | 23 | loginButton.onClick.listen((e) { 24 | outputDiv.innerHtml = "Loading..."; 25 | loginButton.disabled = true; 26 | String approvalPrompt = approvalPromptInput.value; 27 | if (approvalPrompt.isEmpty) { 28 | approvalPrompt = null; 29 | } 30 | auth.approval_prompt = approvalPrompt; 31 | bool isImmediate = (immediateInput.value == "1"); 32 | bool onlyLoadToken = (onlyLoadTokenInput.value == "1"); 33 | auth.login(immediate: isImmediate, onlyLoadToken: onlyLoadToken) 34 | .then(_oauthReady) 35 | .whenComplete(() { 36 | loginButton.disabled = false; 37 | }) 38 | .catchError((e) { 39 | outputDiv.innerHtml = e.toString(); 40 | print("$e"); 41 | }); 42 | }); 43 | 44 | logoutButton.onClick.listen((e) { 45 | auth.logout(); 46 | loginWrapper.style.display = "inline-block"; 47 | logoutButton.style.display = "none"; 48 | outputDiv.innerHtml = ""; 49 | }); 50 | } 51 | 52 | 53 | Future _oauthReady(Token token) { 54 | loginWrapper.style.display = "none"; 55 | logoutButton.style.display = "inline-block"; 56 | final url = "https://www.googleapis.com/books/v1/volumes/zyTCAlFPjgYC"; 57 | 58 | var headers = getAuthorizationHeaders(token.type, token.data); 59 | 60 | return HttpRequest.request(url, requestHeaders: headers) 61 | .then((HttpRequest request) { 62 | if (request.status == 200) { 63 | var data = JSON.decode(request.responseText); 64 | outputDiv.innerHtml = """ 65 |

Book title: ${data['volumeInfo']['title']}

66 |

Description:
${data['volumeInfo']['description']}

67 | """; 68 | } else { 69 | outputDiv.innerHtml = "Error ${request.status}: ${request.statusText}"; 70 | } 71 | }); 72 | } 73 | -------------------------------------------------------------------------------- /example/oauth_example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | OAuth Example 7 | 8 | 9 |

OAuth Example

10 | 11 |

After logging in, this app will call Google Books API to get book info

12 |
13 |

approval_prompt 14 | 18 |

19 |

immediate 20 | 24 |

25 |

onlyLoadToken 26 | 30 |

31 | 32 |
33 | 34 | 35 |
36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /example/oauth_example_compute_engine.dart: -------------------------------------------------------------------------------- 1 | import "dart:async"; 2 | import "package:google_oauth2_client/google_oauth2_console.dart"; 3 | import "package:http/http.dart" as http; 4 | 5 | void main(List args) { 6 | print("project = ${args[0]}"); 7 | ComputeOAuth2Console computeEngineClient = new ComputeOAuth2Console(args[0]); 8 | 9 | computeEngineClient.withClient(_clientCallback) 10 | .then((_) { 11 | print('done'); 12 | computeEngineClient.close(); 13 | }); 14 | } 15 | 16 | Future _clientCallback(http.Client client) { 17 | final url = "https://storage.googleapis.com"; 18 | return client.get(url).then((http.Response response) { 19 | var data = response.body; 20 | var c = "data = ${data}"; 21 | print(c); 22 | return c; 23 | }); 24 | } -------------------------------------------------------------------------------- /example/oauth_example_console.dart: -------------------------------------------------------------------------------- 1 | import "dart:async"; 2 | import "dart:convert"; 3 | import "package:google_oauth2_client/google_oauth2_console.dart"; 4 | import "package:http/http.dart" as http; 5 | 6 | const _IDENTIFIER = 7 | "299615367852-n0kfup30mfj5emlclfgud9g76itapvk9.apps.googleusercontent.com"; 8 | const _SECRET = "azeFTOjszzL57dvMd-JS2Zda"; 9 | const _SCOPES = const ["https://www.googleapis.com/auth/plus.login"]; 10 | const _REQUEST_VISIBLE_ACTIONS = const[ 11 | "http://schemas.google.com/AddActivity", 12 | "http://schemas.google.com/CreateActivity"]; 13 | 14 | void main() { 15 | var auth = new OAuth2Console(identifier: _IDENTIFIER, 16 | secret: _SECRET, 17 | scopes: _SCOPES, 18 | request_visible_actions: _REQUEST_VISIBLE_ACTIONS); 19 | 20 | auth.withClient(_clientCallback) 21 | .then((_) { 22 | print('done'); 23 | auth.close(); 24 | }); 25 | } 26 | 27 | Future _clientCallback(http.Client client) { 28 | final url = "https://www.googleapis.com/plus/v1/people/me"; 29 | return client.get(url).then((http.Response response) { 30 | var data = JSON.decode(response.body); 31 | var c = "Logged in as ${data["displayName"]}"; 32 | print(c); 33 | return c; 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /lib/google_oauth2_browser.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Google Inc. 2 | // Originally part of http://code.google.com/p/google-api-dart-client 3 | // 4 | // Adapted as stand-alone OAuth library: 5 | // Copyright 2013 Gerwin Sturm (scarygami.net/+) 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); 8 | // you may not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, 15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | // See the License for the specific language governing permissions and 17 | // limitations under the License. 18 | 19 | library google_oauth2_browser; 20 | 21 | import "dart:convert"; 22 | import "dart:html"; 23 | import "dart:typed_data"; 24 | import "dart:math"; 25 | 26 | import "dart:async"; 27 | import "src/common/url_pattern.dart"; 28 | export "src/common/url_pattern.dart"; 29 | 30 | part "src/browser/oauth2.dart"; 31 | part "src/browser/google_oauth2.dart"; 32 | part "src/browser/simple_oauth2.dart"; 33 | part "src/browser/proxy_callback.dart"; 34 | part "src/browser/token.dart"; 35 | part "src/browser/utils.dart"; 36 | 37 | Map getAuthorizationHeaders(String tokenType, String token) { 38 | return { 'Authorization': "${tokenType} ${token}"}; 39 | } 40 | -------------------------------------------------------------------------------- /lib/google_oauth2_console.dart: -------------------------------------------------------------------------------- 1 | library google_oauth2_console; 2 | 3 | export "src/console/oauth2_console_client/oauth2.dart"; 4 | export "src/common/url_pattern.dart"; 5 | -------------------------------------------------------------------------------- /lib/src/browser/google_oauth2.dart: -------------------------------------------------------------------------------- 1 | part of google_oauth2_browser; 2 | 3 | /// Google OAuth2 authentication context. For more details on how OAuth2 works for client-side, see 4 | /// [Oauth2 client-side authentication](https://developers.google.com/accounts/docs/OAuth2#clientside) 5 | class GoogleOAuth2 extends OAuth2 { 6 | final String _clientId; 7 | final List _scopes; 8 | final List _request_visible_actions; 9 | final String _provider; 10 | final String _tokenValidationUri; 11 | final Function _tokenLoaded; 12 | final Function _tokenNotLoaded; 13 | String _approval_prompt; 14 | 15 | Future<_ProxyChannel> _channel; 16 | 17 | /// Destination for not-yet-validated tokens we're waiting to receive over 18 | /// the proxy channel. 19 | Completer _tokenCompleter; 20 | 21 | /// The last fetched token. 22 | Token __token; // Double-underscore because it has a private setter _token. 23 | 24 | /** 25 | * Constructor. 26 | * 27 | * The following parameters are accepted: 28 | * 29 | * * [provider] the URI to provide Google OAuth2 authentication. 30 | * * [tokenValidationUri] the URI to validate OAuth2 tokens against. 31 | * * [clientId] Client id for the Google API app. For example, for Google 32 | * Books, use "796343192238.apps.googleusercontent.com". 33 | * * [scopes] list of scopes (kinds of information) you are planning to use. 34 | * For example, to get data related to Google Books and user info, use 35 | * `["https://www.googleapis.com/auth/books", "https://www.googleapis.com/auth/userinfo.email"]` 36 | * * [tokenLoaded] a callback to use when a non-null login token is ready. 37 | * The callback should accept one [Token] parameter. 38 | * * [approval_prompt] can be null or 'force' to force user approval or 'auto' (default) 39 | * * [autoLogin] if true, try to login with "immediate" param (no popup will be shown) 40 | * * [onlyLoadToken] instead of showing user prompt, use stored token (if available) 41 | */ 42 | GoogleOAuth2( 43 | String this._clientId, 44 | List this._scopes, 45 | { List request_visible_actions: null, 46 | String provider: "https://accounts.google.com/o/oauth2/", 47 | String tokenValidationUri: "https://www.googleapis.com/oauth2/v1/tokeninfo", 48 | tokenLoaded(Token token), 49 | tokenNotLoaded(), 50 | bool autoLogin: false, 51 | bool autoLoadStoredToken: true, 52 | String approval_prompt: null}) 53 | : _provider = provider, 54 | _tokenValidationUri = tokenValidationUri, 55 | _tokenLoaded = tokenLoaded, 56 | _tokenNotLoaded = tokenNotLoaded, 57 | _request_visible_actions = request_visible_actions, 58 | _approval_prompt = approval_prompt, 59 | super() { 60 | _channel = _createFutureChannel(); 61 | 62 | // Attempt an immediate login, we may already be authorized. 63 | if (autoLogin) { 64 | login(immediate: true, onlyLoadToken: false) 65 | .then((t) => print("Automatic login successful")) 66 | .catchError((e) => print("Automatic login failed: $e")); 67 | } else if (autoLoadStoredToken) { 68 | login(immediate: true, onlyLoadToken: true) 69 | .then((t) => print("Login with stored token successful")) 70 | .catchError((e) => print("Failed to login with existing token: $e")); 71 | } 72 | } 73 | 74 | Map getAuthHeaders() => getAuthorizationHeaders(token.type, token.data); 75 | 76 | /// Sets up the proxy iframe in the provider's origin that will receive 77 | /// postMessages and relay them to us. 78 | /// 79 | /// This completes asynchronously as the proxy iframe is not ready to use 80 | /// until we've received an 'oauth2relayReady' message from it. 81 | Future<_ProxyChannel> _createFutureChannel() { 82 | final channelCompleter = new Completer<_ProxyChannel>(); 83 | _ProxyChannel channel; 84 | channel = new _ProxyChannel(_provider, (subject, args) { 85 | switch (subject) { 86 | 87 | // Channel is ready at this point 88 | case "oauth2relayReady": 89 | channelCompleter.complete(channel); 90 | break; 91 | case "oauth2callback": 92 | try { 93 | Token token = Token._parse(args[0]); 94 | if (!_tokenCompleter.isCompleted) { 95 | _tokenCompleter.complete(token); 96 | } 97 | } catch (exception) { 98 | if (!_tokenCompleter.isCompleted) { 99 | _tokenCompleter.completeError(exception); 100 | } 101 | } 102 | break; 103 | } 104 | }); 105 | return channelCompleter.future; 106 | } 107 | 108 | /// Gets the URI that prompts the user for pemission (if required). 109 | /// 110 | /// @param immediate if true, generate a URI to prompt user for permission 111 | String _getAuthorizeUri(bool immediate) { 112 | Map queryParams = { 113 | "response_type": "token", 114 | "client_id": _clientId, 115 | "origin": window.location.origin, 116 | "redirect_uri": "postmessage", // Response will post to the proxy iframe 117 | "scope": _scopes.join(" "), 118 | "immediate": immediate.toString(), 119 | "approval_prompt": _approval_prompt 120 | }; 121 | if (_request_visible_actions != null && _request_visible_actions.length > 0) { 122 | queryParams["request_visible_actions"] = _request_visible_actions.join(" "); 123 | } 124 | return UrlPattern.generatePattern("${_provider}auth", {}, queryParams); 125 | } 126 | 127 | /// Deletes the stored token 128 | void logout() { 129 | _token = null; 130 | } 131 | 132 | /** 133 | * Attempts to authenticate. 134 | * 135 | * Scenarios: 136 | * 137 | * * If you have an existing valid token, it will be immediately returned. 138 | * * If you have an expired token, it will be silently renewed (override with 139 | * `immediate: true`) 140 | * * If you have no token, a popup prompt will be displayed. 141 | * * If the user declines, closes the popup, or the service returns a token 142 | * that cannot be validated, an exception will be delivered. 143 | * 144 | * If [immediate] is true, authenticates user with the "immediate" parameter. 145 | * No popup will be shown. If [onlyLoadToken] is true, then use stored token 146 | * (if available) instead of showing user prompt. 147 | */ 148 | Future login({bool immediate: false, bool onlyLoadToken: false}) { 149 | if ((_approval_prompt == "force") && immediate) { 150 | return new Future.error("Can't force approval prompt with immediate login"); 151 | } 152 | 153 | if (token != null) { 154 | 155 | // Return the good token right away 156 | if (!token.expired) { 157 | return new Future.value(token); 158 | } 159 | 160 | // Token expired - simply renew it by later making the immedate auth call 161 | if (immediate == null) { 162 | immediate = true; 163 | } 164 | } 165 | 166 | // Login may already be in progress 167 | if (_tokenCompleter != null && !_tokenCompleter.isCompleted) { 168 | 169 | // An in-progress request will satisfy an immediate request 170 | // (even if it's not immediate). 171 | if (immediate) { 172 | return _tokenCompleter.future; 173 | } 174 | 175 | var tokenCompleter = new Completer(); 176 | _tokenCompleter.future 177 | .then((value) => tokenCompleter.complete(value)) 178 | .catchError((e) { 179 | 180 | // Ongoing login failed - try to login again 181 | login(immediate: immediate, onlyLoadToken: onlyLoadToken) 182 | .then((value) => tokenCompleter.complete(value)) 183 | .catchError((e) => tokenCompleter.completeError(e)); 184 | }); 185 | return tokenCompleter.future; 186 | } 187 | 188 | // If there is valid locally stored token 189 | if ((_storedToken != null) && !_storedToken.expired) { 190 | var storedTokenCompleter = new Completer(); 191 | _storedToken.validate(_clientId, service: _tokenValidationUri) 192 | .then((bool isValid) { 193 | if (isValid) { 194 | _token = _storedToken; 195 | storedTokenCompleter.complete(_storedToken); 196 | return; 197 | } 198 | 199 | _token = null; 200 | 201 | // Stored token not valid - try to log in again 202 | login(immediate: immediate, onlyLoadToken: onlyLoadToken) 203 | .then((token) => storedTokenCompleter.complete(token)) 204 | .catchError((e) => storedTokenCompleter.completeError(e)); 205 | }) 206 | .catchError((e) { 207 | _token = null; 208 | 209 | // Don't prompt user, simply complete with an error 210 | if (onlyLoadToken) { 211 | _tokenCompleter.completeError("Locally saved token is not valid"); 212 | return; 213 | } 214 | 215 | // Try to log in again 216 | login(immediate: immediate, onlyLoadToken: onlyLoadToken) 217 | .then((token) => storedTokenCompleter.complete(token)) 218 | .catchError((e) => storedTokenCompleter.completeError(e)); 219 | }); 220 | return storedTokenCompleter.future; 221 | } 222 | 223 | Completer tokenCompleter = new Completer(); 224 | tokenCompleter.future.then((token) { 225 | _token = token; 226 | }).catchError((e) { 227 | _token = null; 228 | }); 229 | 230 | _tokenCompleter = _wrapValidation(tokenCompleter); 231 | 232 | // Synchronous if the channel is already open -> avoids popup blocker 233 | _channel.then((_ProxyChannel value) { 234 | String uri = _getAuthorizeUri(immediate); 235 | 236 | // Request for immediate authentication 237 | if (immediate) { 238 | IFrameElement iframe = _iframe(uri); 239 | _tokenCompleter.future 240 | .whenComplete(() => iframe.remove()) 241 | .catchError((e) => print("Failed to login with immediate: $e")); 242 | return; 243 | } 244 | 245 | // Prompt user with a popup for user authorization 246 | WindowBase popup = _popup(uri); 247 | new _WindowPoller(_tokenCompleter, popup).poll(); 248 | }).catchError((e) { 249 | _tokenCompleter.completeError(e); 250 | }); 251 | 252 | return _tokenCompleter.future; 253 | } 254 | 255 | Future ensureAuthenticated() { 256 | return login().then((_) => null); 257 | } 258 | 259 | /// Returns the OAuth2 token, if one is currently available. 260 | Token get token => __token; 261 | 262 | set _token(Token value) { 263 | final invokeTokenLoadedCallback = (__token == null) && (value != null); 264 | final invokeTokenNotLoadedCallback = (__token == null) && (value == null); 265 | try { 266 | _storedToken = value; 267 | } catch (e) { 268 | print("Failed to cache OAuth2 token: $e"); 269 | } 270 | __token = value; 271 | if (invokeTokenLoadedCallback && (_tokenLoaded != null)) { 272 | var timer = new Timer(const Duration(milliseconds: 0), () { 273 | try { 274 | _tokenLoaded(value); 275 | } catch (e) { 276 | print("Failed to invoke tokenLoaded callback: $e"); 277 | } 278 | }); 279 | } 280 | if (invokeTokenNotLoadedCallback && (_tokenNotLoaded != null)) { 281 | var timer = new Timer(const Duration(milliseconds: 0), () { 282 | try { 283 | _tokenNotLoaded(); 284 | } catch (e) { 285 | print("Failed to invoke tokenNotLoaded callback: $e"); 286 | } 287 | }); 288 | } 289 | } 290 | 291 | Token get _storedToken => window.localStorage.containsKey(_storageKey) ? new Token.fromJson( 292 | window.localStorage[_storageKey]) : null; 293 | 294 | void set _storedToken(Token value) { 295 | if (value == null) { 296 | window.localStorage.remove(_storageKey); 297 | } else { 298 | window.localStorage[_storageKey] = value.toJson(); 299 | } 300 | } 301 | 302 | /// Returns a unique identifier for this context for use in localStorage. 303 | String get _storageKey => JSON.encode({ 304 | "clientId": _clientId, 305 | "scopes": _scopes, 306 | "provider": _provider, 307 | }); 308 | 309 | /// Takes a completer that accepts validated tokens, and returns a completer 310 | /// that accepts unvalidated tokens. 311 | Completer _wrapValidation(Completer validTokenCompleter) { 312 | Completer result = new Completer(); 313 | result.future.then((Token token) { 314 | token.validate(_clientId, service: _tokenValidationUri) 315 | .then((bool isValid) { 316 | if (isValid) { 317 | validTokenCompleter.complete(token); 318 | } else { 319 | validTokenCompleter.completeError("Server returned token is invalid"); 320 | } 321 | }) 322 | .catchError((e) => validTokenCompleter.completeError(e)); 323 | }).catchError((e) => validTokenCompleter.completeError(e)); 324 | 325 | return result; 326 | } 327 | 328 | String get approval_prompt => _approval_prompt; 329 | 330 | set approval_prompt(String approval_prompt) { 331 | this._approval_prompt = approval_prompt; 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /lib/src/browser/oauth2.dart: -------------------------------------------------------------------------------- 1 | part of google_oauth2_browser; 2 | 3 | /// An OAuth2 authentication context. 4 | abstract class OAuth2 { 5 | 6 | T get token; 7 | 8 | OAuth2(); 9 | 10 | /** 11 | * Takes a [request] and returns the request with the authorization headers 12 | * set correctly. 13 | */ 14 | Future authenticate(HttpRequest request) { 15 | return ensureAuthenticated() 16 | .then((_) { 17 | var headers = getAuthHeaders(); 18 | headers.forEach((k, v) => request.setRequestHeader(k, v)); 19 | return request; 20 | }); 21 | } 22 | 23 | /** 24 | * Returns a [Future] that completes when this instance is authenticated. 25 | */ 26 | Future ensureAuthenticated(); 27 | 28 | Map getAuthHeaders(); 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/browser/proxy_callback.dart: -------------------------------------------------------------------------------- 1 | part of google_oauth2_browser; 2 | 3 | typedef void _ProxyCallback(String subject, List args); 4 | 5 | /// Sets up a channel for listening to the token information posted by the 6 | /// authorization url using the postMessage flow. 7 | /// 8 | /// We create a hidden iframe hosting the provider's 'postmessageRelay' page, 9 | /// which receives token information from the authorization popup and posts 10 | /// it to this document. We also add a message listener to this document. 11 | /// It detects such messages and invokes the provided callback. 12 | class _ProxyChannel { 13 | String _nonce; 14 | String _provider; 15 | String _expectedOrigin; 16 | IFrameElement _element; 17 | _ProxyCallback _callback; 18 | StreamSubscription _streamsub; 19 | 20 | _ProxyChannel(String this._provider, _ProxyCallback this._callback) { 21 | _nonce = (0x7FFFFFFF & _random()).toString(); 22 | _expectedOrigin = _origin(_provider); 23 | _element = _iframe(_getProxyUrl()); 24 | _streamsub = window.onMessage.listen(_onMessage); 25 | } 26 | 27 | void close() { 28 | _element.remove(); 29 | _streamsub.cancel(); 30 | } 31 | 32 | void _onMessage(MessageEvent event) { 33 | if (event.origin != _expectedOrigin) { 34 | print("Invalid message origin: ${event.origin} / Expected ${_expectedOrigin}"); 35 | return; 36 | } 37 | var data; 38 | try { 39 | data = JSON.decode(event.data); 40 | } catch (e) { 41 | print("Invalid JSON received via postMessage: ${event.data}"); 42 | return; 43 | } 44 | if (!(data is Map) || (data['t'] != _nonce)) { 45 | return; 46 | } 47 | String subject = data['s']; 48 | if (subject.endsWith(':$_nonce')) { 49 | subject = subject.substring(0, subject.length - _nonce.length - 1); 50 | } 51 | _callback(subject, data['a'] as List); 52 | } 53 | 54 | /// Computes the javascript origin of an absolute URI. 55 | String _origin(String uriString) { 56 | final uri = Uri.parse(uriString); 57 | var portPart; 58 | if (uri.port == 0 || (uri.port == 443 && uri.scheme == "https")) { 59 | portPart = ""; 60 | } else { 61 | portPart = ":${uri.port}"; 62 | } 63 | return "${uri.scheme}://${uri.host}$portPart"; 64 | } 65 | 66 | String _getProxyUrl() { 67 | Map proxyParams = {"parent": window.location.origin}; 68 | String proxyUrl = UrlPattern.generatePattern("${_provider}postmessageRelay", 69 | {}, proxyParams); 70 | return Uri.parse(proxyUrl) 71 | .resolve("#rpctoken=$_nonce&forcesecure=1").toString(); 72 | } 73 | } -------------------------------------------------------------------------------- /lib/src/browser/simple_oauth2.dart: -------------------------------------------------------------------------------- 1 | part of google_oauth2_browser; 2 | 3 | /** 4 | * A simple OAuth2 authentication context which can use if you already have a [token] 5 | * via another mechanism, for example the Chrome Extension Identity API. 6 | */ 7 | class SimpleOAuth2 extends OAuth2 { 8 | final String token; 9 | final String tokenType; 10 | 11 | /// Creates an OAuth2 context for the application using [token] for authentication 12 | SimpleOAuth2(String this.token, {String this.tokenType: "Bearer"}) : super(); 13 | 14 | Future ensureAuthenticated() => new Future.value(); 15 | 16 | Map getAuthHeaders() => 17 | getAuthorizationHeaders(tokenType, token); 18 | } 19 | -------------------------------------------------------------------------------- /lib/src/browser/token.dart: -------------------------------------------------------------------------------- 1 | part of google_oauth2_browser; 2 | 3 | /// An OAuth2 authentication token. 4 | class Token { 5 | /// The token type, usually "Bearer" 6 | final String type; 7 | /// The raw token data used for authentication 8 | final String data; 9 | /// Time at which the token will be expired 10 | final DateTime expiry; 11 | /// The email address of the user, only set if the scopes include 12 | /// https://www.googleapis.com/auth/userinfo.email 13 | String get email => _email; 14 | /// A unique identifier of the user, only set if the scopes include 15 | /// https://www.googleapis.com/auth/userinfo.profile 16 | String get userId => _userId; 17 | String _email; 18 | String _userId; 19 | 20 | Token(String this.type, String this.data, DateTime this.expiry); 21 | 22 | factory Token.fromJson(String json) { 23 | final map = JSON.decode(json); 24 | final token = new Token(map['type'], map['data'], 25 | new DateTime.fromMillisecondsSinceEpoch(map['expiry'])); 26 | token._email = map['email']; 27 | token._userId = map['userId']; 28 | return token; 29 | } 30 | 31 | bool get expired => new DateTime.now().compareTo(expiry) > 0; 32 | 33 | String toString() => "[Token type=$type, data=$data, expired=$expired, " 34 | "expiry=$expiry, email=$email, userId=$userId]"; 35 | 36 | /** 37 | * Queries whether this token is still valid. 38 | */ 39 | Future validate(String clientId, 40 | {String service: "https://www.googleapis.com/oauth2/v1/tokeninfo"}) { 41 | String url = UrlPattern.generatePattern(service, {}, {"access_token": data}); 42 | var completer = new Completer(); 43 | var request = new HttpRequest(); 44 | request.onLoadEnd.listen((Event e) { 45 | if (request.status == 200) { 46 | completer.complete(request.responseText); 47 | } 48 | }); 49 | request.open("GET", url); 50 | request.send(); 51 | 52 | return completer.future.then((json) { 53 | final data = JSON.decode(json); 54 | final valid = clientId == data['audience']; 55 | if (valid) { 56 | _email = data['email']; 57 | _userId = data['user_id']; 58 | } 59 | return valid; 60 | }); 61 | } 62 | 63 | String toJson() { 64 | return JSON.encode({ 65 | "type": type, 66 | "data": data, 67 | "expiry": expiry.millisecondsSinceEpoch, 68 | "email": email, 69 | "userId": userId, 70 | }); 71 | } 72 | 73 | static Token _parse(String data) { 74 | if (data == null) { 75 | throw new Exception("No auth token data"); 76 | } 77 | 78 | Map params = {}; 79 | for (String kv in _tokenizeRelativeUrl(data)) { 80 | if (kv.isEmpty) { 81 | continue; 82 | } 83 | int eqIndex = kv.indexOf('='); 84 | if (eqIndex < 0) { 85 | params[kv] = ""; 86 | } else { 87 | params[kv.substring(0, eqIndex)] = kv.substring(eqIndex + 1); 88 | } 89 | } 90 | 91 | if (params.containsKey('error')) { 92 | throw new AuthException(params['error'], params); 93 | } 94 | for (String param in ['access_token', 'token_type', 'expires_in']) { 95 | if (!params.containsKey(param)) { 96 | throw new Exception("Missing parameter $param"); 97 | } 98 | } 99 | 100 | // Mark tokens as 'expired' 20 seconds early so it's still valid when used. 101 | Duration duration = 102 | new Duration(seconds: int.parse(params['expires_in']) - 20); 103 | return new Token(params['token_type'], params['access_token'], 104 | new DateTime.now().add(duration)); 105 | } 106 | 107 | /// Extracts &-separated tokens from the path, query, and fragment of [uri]. 108 | static List _tokenizeRelativeUrl(String uri) { 109 | final u = Uri.parse(uri); 110 | final result = []; 111 | [u.path, u.query, u.fragment].forEach((x) { 112 | if (x != null) result.addAll(_tokenize(x)); 113 | }); 114 | return result; 115 | } 116 | 117 | static List _tokenize(String data) { 118 | return data.isEmpty ? [] : data.split('&'); 119 | } 120 | } 121 | 122 | class AuthException implements Exception { 123 | final String message; 124 | final Map data; 125 | AuthException(this.message, this.data); 126 | toString() => "AuthException: $message"; 127 | } 128 | -------------------------------------------------------------------------------- /lib/src/browser/utils.dart: -------------------------------------------------------------------------------- 1 | part of google_oauth2_browser; 2 | 3 | /// Polls until either the future is completed or the window is closed. 4 | /// If the window was closed without the future being completed, completes 5 | /// the future with an exception. 6 | class _WindowPoller { 7 | final Completer _completer; 8 | final WindowBase _window; 9 | 10 | _WindowPoller(Completer this._completer, WindowBase this._window) { 11 | assert(_window != null); 12 | } 13 | 14 | 15 | void poll() { 16 | if (_completer.isCompleted) { 17 | return; 18 | } 19 | if (_window.closed) { 20 | _completer.completeError(new Exception("User closed the window")); 21 | } else { 22 | var timer = new Timer(const Duration(milliseconds: 10), poll); 23 | } 24 | } 25 | } 26 | 27 | /// Opens a popup centered on the screen displaying the provided URL. 28 | WindowBase _popup(String url) { 29 | // Popup is desigend for 650x600, but don't make one bigger than the screen! 30 | int width = min(650, window.screen.width - 20); 31 | int height = min(600, window.screen.height - 30); 32 | int left = (window.screen.width - width) ~/ 2; 33 | int top = (window.screen.height - height) ~/ 2; 34 | return window.open(url, "_blank", "toolbar=no,location=no,directories=" 35 | "no,status=no,menubar=no,scrollbars=yes,resizable=yes,copyhistory=no," 36 | "width=$width,height=$height,top=$top,left=$left"); 37 | } 38 | 39 | /// Creates a hidden iframe displaying the provided URL. 40 | IFrameElement _iframe(String url) { 41 | IFrameElement iframe = new Element.tag("iframe"); 42 | iframe.src = url; 43 | iframe.style.position = "absolute"; 44 | iframe.width = iframe.height = "1"; 45 | iframe.style.top = iframe.style.left = "-100px"; 46 | document.body.children.add(iframe); 47 | return iframe; 48 | } 49 | 50 | /// Returns a random unsigned 32-bit integer. 51 | int _random() { 52 | final ary = new Uint32List(1); 53 | window.crypto.getRandomValues(ary); 54 | return ary[0]; 55 | } 56 | -------------------------------------------------------------------------------- /lib/src/common/url_pattern.dart: -------------------------------------------------------------------------------- 1 | library url_pattern; 2 | 3 | /** Produces part of a URL, when the template parameters are provided. */ 4 | typedef String _UrlPatternToken(Map params); 5 | 6 | /** URL template with placeholders that can be filled in to produce a URL. */ 7 | class UrlPattern { 8 | final List<_UrlPatternToken> _tokens; 9 | 10 | /** 11 | * Creates a UrlPattern from the specification [pattern]. 12 | * See . 13 | * We only implement a very simple subset for now. 14 | */ 15 | UrlPattern(String pattern) : _tokens = [] { 16 | var cursor = 0; 17 | while (cursor < pattern.length) { 18 | final open = pattern.indexOf("{", cursor); 19 | if (open < 0) { 20 | final rest = pattern.substring(cursor); 21 | _tokens.add((params) => rest); 22 | cursor = pattern.length; 23 | } else { 24 | if (open > cursor) { 25 | final intermediate = pattern.substring(cursor, open); 26 | _tokens.add((params) => intermediate); 27 | } 28 | final close = pattern.indexOf("}", open); 29 | if (close < 0) throw new ArgumentError("Token meets end of text: $pattern"); 30 | String variable = pattern.substring(open + 1, close); 31 | _tokens.add((params) => (params[variable] == null) 32 | ? 'null' 33 | : Uri.encodeComponent(params[variable].toString())); 34 | cursor = close + 1; 35 | } 36 | } 37 | } 38 | 39 | /** Generates a URL with the specified list of URL and query parameters. */ 40 | String generate(Map urlParams, Map queryParams) { 41 | final buffer = new StringBuffer(); 42 | _tokens.forEach((token) => buffer.write(token(urlParams))); 43 | var first = true; 44 | queryParams.forEach((key, value) { 45 | if (value == null) return; 46 | if (value is List) { 47 | value.forEach((listValue) { 48 | buffer.write(first ? '?' : '&'); 49 | if (first) first = false; 50 | buffer.write(Uri.encodeComponent(key.toString())); 51 | buffer.write('='); 52 | buffer.write(Uri.encodeComponent(listValue.toString())); 53 | }); 54 | } else { 55 | buffer.write(first ? '?' : '&'); 56 | if (first) first = false; 57 | buffer.write(Uri.encodeComponent(key.toString())); 58 | buffer.write('='); 59 | buffer.write(Uri.encodeComponent(value.toString())); 60 | } 61 | }); 62 | return buffer.toString(); 63 | } 64 | 65 | static String generatePattern(String pattern, Map urlParams, 66 | Map queryParams) { 67 | var urlPattern = new UrlPattern(pattern); 68 | return urlPattern.generate(urlParams, queryParams); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/src/console/oauth2_console_client/exit_codes.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// Exit code constants. From [the BSD sysexits manpage][manpage]. Not every 6 | /// constant here is used, even though some of the unused ones may be 7 | /// appropriate for errors encountered by pub. 8 | /// 9 | /// [manpage]: http://www.freebsd.org/cgi/man.cgi?query=sysexits 10 | library exit_codes; 11 | 12 | /// The command was used incorrectly. 13 | const USAGE = 64; 14 | 15 | /// The input data was incorrect. 16 | const DATA = 65; 17 | 18 | /// An input file did not exist or was unreadable. 19 | const NO_INPUT = 66; 20 | 21 | /// The user specified did not exist. 22 | const NO_USER = 67; 23 | 24 | /// The host specified did not exist. 25 | const NO_HOST = 68; 26 | 27 | /// A service is unavailable. 28 | const UNAVAILABLE = 69; 29 | 30 | /// An internal software error has been detected. 31 | const SOFTWARE = 70; 32 | 33 | /// An operating system error has been detected. 34 | const OS = 71; 35 | 36 | /// Some system file did not exist or was unreadable. 37 | const OS_FILE = 72; 38 | 39 | /// A user-specified output file cannot be created. 40 | const CANT_CREATE = 73; 41 | 42 | /// An error occurred while doing I/O on some file. 43 | const IO = 74; 44 | 45 | /// Temporary failure, indicating something that is not really an error. 46 | const TEMP_FAIL = 75; 47 | 48 | /// The remote system returned something invalid during a protocol exchange. 49 | const PROTOCOL = 76; 50 | 51 | /// The user did not have sufficient permissions. 52 | const NO_PERM = 77; 53 | 54 | /// Something was unconfigured or mis-configured. 55 | const CONFIG = 78; 56 | -------------------------------------------------------------------------------- /lib/src/console/oauth2_console_client/http.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// Helpers for dealing with HTTP. 6 | library pub.http; 7 | 8 | import 'dart:async'; 9 | import 'dart:io'; 10 | import 'dart:convert'; 11 | 12 | import 'package:http/http.dart' as http; 13 | 14 | import 'io.dart'; 15 | import 'log.dart' as log; 16 | import 'utils.dart'; 17 | 18 | // TODO(nweiz): make this configurable 19 | /// The amount of time in milliseconds to allow HTTP requests before assuming 20 | /// they've failed. 21 | final HTTP_TIMEOUT = 30 * 1000; 22 | 23 | /// Headers and field names that should be censored in the log output. 24 | final _CENSORED_FIELDS = const ['refresh_token', 'authorization']; 25 | 26 | /// An HTTP client that transforms 40* errors and socket exceptions into more 27 | /// user-friendly error messages. 28 | class PubHttpClient extends http.BaseClient { 29 | http.Client inner; 30 | Uri tokenEndpoint; 31 | 32 | PubHttpClient([http.Client inner]) 33 | : this.inner = inner == null ? new http.Client() : inner; 34 | 35 | Future send(http.BaseRequest request) { 36 | _logRequest(request); 37 | 38 | // TODO(nweiz): remove this when issue 4061 is fixed. 39 | var stackTrace; 40 | try { 41 | throw null; 42 | } catch (_, localStackTrace) { 43 | stackTrace = localStackTrace; 44 | } 45 | 46 | // TODO(nweiz): Ideally the timeout would extend to reading from the 47 | // response input stream, but until issue 3657 is fixed that's not feasible. 48 | return timeout(inner.send(request).then((streamedResponse) { 49 | _logResponse(streamedResponse); 50 | 51 | var status = streamedResponse.statusCode; 52 | // 401 responses should be handled by the OAuth2 client. It's very 53 | // unlikely that they'll be returned by non-OAuth2 requests. We also want 54 | // to pass along 400 responses from the token endpoint. 55 | var tokenRequest = urisEqual( 56 | streamedResponse.request.url, tokenEndpoint); 57 | if (status < 400 || status == 401 || (status == 400 && tokenRequest)) { 58 | return streamedResponse; 59 | } 60 | 61 | return http.Response.fromStream(streamedResponse).then((response) { 62 | throw new PubHttpException(response); 63 | }); 64 | }).catchError((error) { 65 | if (error is SocketException && 66 | error.osError != null) { 67 | if (error.osError.errorCode == 8 || 68 | error.osError.errorCode == -2 || 69 | error.osError.errorCode == -5 || 70 | error.osError.errorCode == 11001 || 71 | error.osError.errorCode == 11004) { 72 | throw 'Could not resolve URL "${request.url.origin}".'; 73 | } else if (error.osError.errorCode == -12276) { 74 | throw 'Unable to validate SSL certificate for ' 75 | '"${request.url.origin}".'; 76 | } 77 | } 78 | throw error; 79 | }), HTTP_TIMEOUT, 'fetching URL "${request.url}"') as Future; 80 | } 81 | 82 | /// Logs the fact that [request] was sent, and information about it. 83 | void _logRequest(http.BaseRequest request) { 84 | var requestLog = new StringBuffer(); 85 | requestLog.writeln("HTTP ${request.method} ${request.url}"); 86 | request.headers.forEach((name, value) => 87 | requestLog.writeln(_logField(name, value))); 88 | 89 | if (request.method == 'POST') { 90 | var contentTypeString = request.headers[HttpHeaders.CONTENT_TYPE]; 91 | if (contentTypeString == null) contentTypeString = ''; 92 | var contentType = ContentType.parse(contentTypeString); 93 | if (request is http.MultipartRequest) { 94 | requestLog.writeln(); 95 | requestLog.writeln("Body fields:"); 96 | request.fields.forEach((name, value) => 97 | requestLog.writeln(_logField(name, value))); 98 | 99 | // TODO(nweiz): make MultipartRequest.files readable, and log them? 100 | } else if (request is http.Request) { 101 | if (contentType.value == 'application/x-www-form-urlencoded') { 102 | requestLog.writeln(); 103 | requestLog.writeln("Body fields:"); 104 | request.bodyFields.forEach((name, value) => 105 | requestLog.writeln(_logField(name, value))); 106 | } else if (contentType.value == 'text/plain' || 107 | contentType.value == 'application/json') { 108 | requestLog.write(request.body); 109 | } 110 | } 111 | } 112 | 113 | log.fine(requestLog.toString().trim()); 114 | } 115 | 116 | /// Logs the fact that [response] was received, and information about it. 117 | void _logResponse(http.StreamedResponse response) { 118 | // TODO(nweiz): Fork the response stream and log the response body. Be 119 | // careful not to log OAuth2 private data, though. 120 | 121 | var responseLog = new StringBuffer(); 122 | var request = response.request; 123 | responseLog.writeln("HTTP response ${response.statusCode} " 124 | "${response.reasonPhrase} for ${request.method} ${request.url}"); 125 | response.headers.forEach((name, value) => 126 | responseLog.writeln(_logField(name, value))); 127 | 128 | log.fine(responseLog.toString().trim()); 129 | } 130 | 131 | /// Returns a log-formatted string for the HTTP field or header with the given 132 | /// [name] and [value]. 133 | String _logField(String name, String value) { 134 | if (_CENSORED_FIELDS.contains(name.toLowerCase())) { 135 | return "$name: "; 136 | } else { 137 | return "$name: $value"; 138 | } 139 | } 140 | } 141 | 142 | /// The HTTP client to use for all HTTP requests. 143 | //final httpClient = new PubHttpClient(); 144 | 145 | /// Handles a successful JSON-formatted response from pub.dartlang.org. 146 | /// 147 | /// These responses are expected to be of the form `{"success": {"message": 148 | /// "some message"}}`. If the format is correct, the message will be printed; 149 | /// otherwise an error will be raised. 150 | void handleJsonSuccess(http.Response response) { 151 | var parsed = parseJsonResponse(response); 152 | if (parsed['success'] is! Map || 153 | !parsed['success'].containsKey('message') || 154 | parsed['success']['message'] is! String) { 155 | invalidServerResponse(response); 156 | } 157 | log.message(parsed['success']['message']); 158 | } 159 | 160 | /// Handles an unsuccessful JSON-formatted response from pub.dartlang.org. 161 | /// 162 | /// These responses are expected to be of the form `{"error": {"message": "some 163 | /// message"}}`. If the format is correct, the message will be raised as an 164 | /// error; otherwise an [invalidServerResponse] error will be raised. 165 | void handleJsonError(http.Response response) { 166 | var errorMap = parseJsonResponse(response); 167 | if (errorMap['error'] is! Map || 168 | !errorMap['error'].containsKey('message') || 169 | errorMap['error']['message'] is! String) { 170 | invalidServerResponse(response); 171 | } 172 | throw errorMap['error']['message']; 173 | } 174 | 175 | /// Parses a response body, assuming it's JSON-formatted. Throws a user-friendly 176 | /// error if the response body is invalid JSON, or if it's not a map. 177 | Map parseJsonResponse(http.Response response) { 178 | var value; 179 | try { 180 | value = JSON.decode(response.body); 181 | } catch (e) { 182 | // TODO(nweiz): narrow this catch clause once issue 6775 is fixed. 183 | invalidServerResponse(response); 184 | } 185 | if (value is! Map) invalidServerResponse(response); 186 | return value; 187 | } 188 | 189 | /// Throws an error describing an invalid response from the server. 190 | void invalidServerResponse(http.Response response) { 191 | throw 'Invalid server response:\n${response.body}'; 192 | } 193 | 194 | /// Exception thrown when an HTTP operation fails. 195 | class PubHttpException implements Exception { 196 | final http.Response response; 197 | 198 | const PubHttpException(this.response); 199 | 200 | String toString() => 'HTTP error ${response.statusCode}: ' 201 | '${response.reasonPhrase} - ${response.body}'; 202 | } 203 | -------------------------------------------------------------------------------- /lib/src/console/oauth2_console_client/io.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// Helper functionality to make working with IO easier. 6 | library io; 7 | 8 | import 'dart:async'; 9 | import 'dart:collection'; 10 | import 'dart:convert'; 11 | import 'dart:io'; 12 | 13 | 14 | import 'package:path/path.dart' as path; 15 | import 'package:http/http.dart' show ByteStream; 16 | import 'log.dart' as log; 17 | import 'utils.dart'; 18 | 19 | export 'package:http/http.dart' show ByteStream; 20 | 21 | /// Returns whether or not [entry] is nested somewhere within [dir]. This just 22 | /// performs a path comparison; it doesn't look at the actual filesystem. 23 | bool isBeneath(String entry, String dir) { 24 | var relative = path.relative(entry, from: dir); 25 | return !path.isAbsolute(relative) && path.split(relative)[0] != '..'; 26 | } 27 | 28 | /// Determines if a file or directory exists at [path]. 29 | bool entryExists(String path) => 30 | dirExists(path) || fileExists(path) || linkExists(path); 31 | 32 | /// Returns whether [link] exists on the file system. This will return `true` 33 | /// for any symlink, regardless of what it points at or whether it's broken. 34 | bool linkExists(String link) => new Link(link).existsSync(); 35 | 36 | /// Returns whether [file] exists on the file system. This will return `true` 37 | /// for a symlink only if that symlink is unbroken and points to a file. 38 | bool fileExists(String file) => new File(file).existsSync(); 39 | 40 | /// Returns the canonical path for [pathString]. This is the normalized, 41 | /// absolute path, with symlinks resolved. As in [transitiveTarget], broken or 42 | /// recursive symlinks will not be fully resolved. 43 | /// 44 | /// This doesn't require [pathString] to point to a path that exists on the 45 | /// filesystem; nonexistent or unreadable path entries are treated as normal 46 | /// directories. 47 | String canonicalize(String pathString) { 48 | var seen = new Set(); 49 | var components = new Queue.from( 50 | path.split(path.normalize(path.absolute(pathString)))); 51 | 52 | // The canonical path, built incrementally as we iterate through [components]. 53 | var newPath = components.removeFirst(); 54 | 55 | // Move through the components of the path, resolving each one's symlinks as 56 | // necessary. A resolved component may also add new components that need to be 57 | // resolved in turn. 58 | while (!components.isEmpty) { 59 | seen.add(path.join(newPath, path.joinAll(components))); 60 | var resolvedPath = resolveLink( 61 | path.join(newPath, components.removeFirst())); 62 | var relative = path.relative(resolvedPath, from: newPath); 63 | 64 | // If the resolved path of the component relative to `newPath` is just ".", 65 | // that means component was a symlink pointing to its parent directory. We 66 | // can safely ignore such components. 67 | if (relative == '.') continue; 68 | 69 | var relativeComponents = new Queue.from(path.split(relative)); 70 | 71 | // If the resolved path is absolute relative to `newPath`, that means it's 72 | // on a different drive. We need to canonicalize the entire target of that 73 | // symlink again. 74 | if (path.isAbsolute(relative)) { 75 | // If we've already tried to canonicalize the new path, we've encountered 76 | // a symlink loop. Avoid going infinite by treating the recursive symlink 77 | // as the canonical path. 78 | if (seen.contains(relative)) { 79 | newPath = relative; 80 | } else { 81 | newPath = relativeComponents.removeFirst(); 82 | relativeComponents.addAll(components); 83 | components = relativeComponents; 84 | } 85 | continue; 86 | } 87 | 88 | // Pop directories off `newPath` if the component links upwards in the 89 | // directory hierarchy. 90 | while (relativeComponents.first == '..') { 91 | newPath = path.dirname(newPath); 92 | relativeComponents.removeFirst(); 93 | } 94 | 95 | // If there's only one component left, [resolveLink] guarantees that it's 96 | // not a link (or is a broken link). We can just add it to `newPath` and 97 | // continue resolving the remaining components. 98 | if (relativeComponents.length == 1) { 99 | newPath = path.join(newPath, relativeComponents.single); 100 | continue; 101 | } 102 | 103 | // If we've already tried to canonicalize the new path, we've encountered a 104 | // symlink loop. Avoid going infinite by treating the recursive symlink as 105 | // the canonical path. 106 | var newSubPath = path.join(newPath, path.joinAll(relativeComponents)); 107 | if (seen.contains(newSubPath)) { 108 | newPath = newSubPath; 109 | continue; 110 | } 111 | 112 | // If there are multiple new components to resolve, add them to the 113 | // beginning of the queue. 114 | relativeComponents.addAll(components); 115 | components = relativeComponents; 116 | } 117 | return newPath; 118 | } 119 | 120 | /// Returns the transitive target of [link] (if A links to B which links to C, 121 | /// this will return C). If [link] is part of a symlink loop (e.g. A links to B 122 | /// which links back to A), this returns the path to the first repeated link (so 123 | /// `transitiveTarget("A")` would return `"A"` and `transitiveTarget("A")` would 124 | /// return `"B"`). 125 | /// 126 | /// This accepts paths to non-links or broken links, and returns them as-is. 127 | String resolveLink(String link) { 128 | var seen = new Set(); 129 | while (linkExists(link) && !seen.contains(link)) { 130 | seen.add(link); 131 | link = path.normalize(path.join( 132 | path.dirname(link), new Link(link).targetSync())); 133 | } 134 | return link; 135 | } 136 | 137 | /// Reads the contents of the text file [file]. 138 | String readTextFile(String file) => 139 | new File(file).readAsStringSync(encoding: UTF8); 140 | 141 | /// Reads the contents of the binary file [file]. 142 | List readBinaryFile(String file) { 143 | log.io("Reading binary file $file."); 144 | var contents = new File(file).readAsBytesSync(); 145 | log.io("Read ${contents.length} bytes from $file."); 146 | return contents; 147 | } 148 | 149 | /// Creates [file] and writes [contents] to it. 150 | /// 151 | /// If [dontLogContents] is true, the contents of the file will never be logged. 152 | String writeTextFile(String file, String contents, {dontLogContents: false}) { 153 | // Sanity check: don't spew a huge file. 154 | log.io("Writing ${contents.length} characters to text file $file."); 155 | if (!dontLogContents && contents.length < 1024 * 1024) { 156 | log.fine("Contents:\n$contents"); 157 | } 158 | 159 | new File(file).writeAsStringSync(contents); 160 | return file; 161 | } 162 | 163 | /// Creates [file] and writes [contents] to it. 164 | String writeBinaryFile(String file, List contents) { 165 | log.io("Writing ${contents.length} bytes to binary file $file."); 166 | new File(file).openSync(mode: FileMode.WRITE) 167 | ..writeFromSync(contents) 168 | ..closeSync(); 169 | log.fine("Wrote text file $file."); 170 | return file; 171 | } 172 | 173 | /// Writes [stream] to a new file at path [file]. Will replace any file already 174 | /// at that path. Completes when the file is done being written. 175 | Future createFileFromStream(Stream> stream, String file) { 176 | log.io("Creating $file from stream."); 177 | 178 | return stream.pipe(new File(file).openWrite()).then((_) { 179 | log.fine("Created $file from stream."); 180 | return file; 181 | }); 182 | } 183 | 184 | /// Creates a directory [dir]. 185 | String createDir(String dir) { 186 | new Directory(dir).createSync(); 187 | return dir; 188 | } 189 | 190 | /// Ensures that [dirPath] and all its parent directories exist. If they don't 191 | /// exist, creates them. 192 | String ensureDir(String dirPath) { 193 | log.fine("Ensuring directory $dirPath exists."); 194 | var dir = new Directory(dirPath); 195 | if (dirPath == '.' || dirExists(dirPath)) return dirPath; 196 | 197 | ensureDir(path.dirname(dirPath)); 198 | 199 | try { 200 | createDir(dirPath); 201 | } on FileSystemException catch (ex) { 202 | // Error 17 means the directory already exists (or 183 on Windows). 203 | if (ex.osError.errorCode == 17 || ex.osError.errorCode == 183) { 204 | log.fine("Got 'already exists' error when creating directory."); 205 | } else { 206 | throw ex; 207 | } 208 | } 209 | 210 | return dirPath; 211 | } 212 | 213 | /// Creates a temp directory whose name will be based on [dir] with a unique 214 | /// suffix appended to it. If [dir] is not provided, a temp directory will be 215 | /// created in a platform-dependent temporary location. Returns the path of the 216 | /// created directory. 217 | String createTempDir([dir = '']) { 218 | var tempDir = new Directory(dir).createTempSync(); 219 | log.io("Created temp directory ${tempDir.path}"); 220 | return tempDir.path; 221 | } 222 | 223 | /// Lists the contents of [dir]. If [recursive] is `true`, lists subdirectory 224 | /// contents (defaults to `false`). If [includeHidden] is `true`, includes files 225 | /// and directories beginning with `.` (defaults to `false`). 226 | /// 227 | /// The returned paths are guaranteed to begin with [dir]. 228 | List listDir(String dir, {bool recursive: false, 229 | bool includeHidden: false}) { 230 | List doList(String dir, Set listedDirectories) { 231 | var contents = []; 232 | 233 | // Avoid recursive symlinks. 234 | var resolvedPath = canonicalize(dir); 235 | if (listedDirectories.contains(resolvedPath)) return []; 236 | 237 | listedDirectories = new Set.from(listedDirectories); 238 | listedDirectories.add(resolvedPath); 239 | 240 | log.io("Listing directory $dir."); 241 | 242 | var children = []; 243 | for (var entity in new Directory(dir).listSync()) { 244 | if (!includeHidden && path.basename(entity.path).startsWith('.')) { 245 | continue; 246 | } 247 | 248 | contents.add(entity.path); 249 | if (entity is Directory) { 250 | // TODO(nweiz): don't manually recurse once issue 4794 is fixed. 251 | // Note that once we remove the manual recursion, we'll need to 252 | // explicitly filter out files in hidden directories. 253 | if (recursive) { 254 | children.addAll(doList(entity.path, listedDirectories)); 255 | } 256 | } 257 | } 258 | 259 | log.fine("Listed directory $dir:\n${contents.join('\n')}"); 260 | contents.addAll(children); 261 | return contents; 262 | } 263 | 264 | return doList(dir, new Set()); 265 | } 266 | 267 | /// Returns whether [dir] exists on the file system. This will return `true` for 268 | /// a symlink only if that symlink is unbroken and points to a directory. 269 | bool dirExists(String dir) => new Directory(dir).existsSync(); 270 | 271 | /// Deletes whatever's at [path], whether it's a file, directory, or symlink. If 272 | /// it's a directory, it will be deleted recursively. 273 | void deleteEntry(String path) { 274 | if (linkExists(path)) { 275 | log.io("Deleting link $path."); 276 | new Link(path).deleteSync(); 277 | } else if (dirExists(path)) { 278 | log.io("Deleting directory $path."); 279 | new Directory(path).deleteSync(recursive: true); 280 | } else if (fileExists(path)) { 281 | log.io("Deleting file $path."); 282 | new File(path).deleteSync(); 283 | } 284 | } 285 | 286 | /// "Cleans" [dir]. If that directory already exists, it will be deleted. Then a 287 | /// new empty directory will be created. 288 | void cleanDir(String dir) { 289 | if (entryExists(dir)) deleteEntry(dir); 290 | createDir(dir); 291 | } 292 | 293 | /// Renames (i.e. moves) the directory [from] to [to]. 294 | void renameDir(String from, String to) { 295 | log.io("Renaming directory $from to $to."); 296 | new Directory(from).renameSync(to); 297 | } 298 | 299 | /// Creates a new symlink at path [symlink] that points to [target]. Returns a 300 | /// [Future] which completes to the path to the symlink file. 301 | /// 302 | /// If [relative] is true, creates a symlink with a relative path from the 303 | /// symlink to the target. Otherwise, uses the [target] path unmodified. 304 | /// 305 | /// Note that on Windows, only directories may be symlinked to. 306 | void createSymlink(String target, String symlink, 307 | {bool relative: false}) { 308 | if (relative) { 309 | // Relative junction points are not supported on Windows. Instead, just 310 | // make sure we have a clean absolute path because it will interpret a 311 | // relative path to be relative to the cwd, not the symlink, and will be 312 | // confused by forward slashes. 313 | if (Platform.operatingSystem == 'windows') { 314 | target = path.normalize(path.absolute(target)); 315 | } else { 316 | target = path.normalize( 317 | path.relative(target, from: path.dirname(symlink))); 318 | } 319 | } 320 | 321 | log.fine("Creating $symlink pointing to $target"); 322 | new Link(symlink).createSync(target); 323 | } 324 | 325 | /// Creates a new symlink that creates an alias at [symlink] that points to the 326 | /// `lib` directory of package [target]. If [target] does not have a `lib` 327 | /// directory, this shows a warning if appropriate and then does nothing. 328 | /// 329 | /// If [relative] is true, creates a symlink with a relative path from the 330 | /// symlink to the target. Otherwise, uses the [target] path unmodified. 331 | void createPackageSymlink(String name, String target, String symlink, 332 | {bool isSelfLink: false, bool relative: false}) { 333 | // See if the package has a "lib" directory. 334 | target = path.join(target, 'lib'); 335 | log.fine("Creating ${isSelfLink ? "self" : ""}link for package '$name'."); 336 | if (dirExists(target)) { 337 | createSymlink(target, symlink, relative: relative); 338 | return; 339 | } 340 | 341 | // It's OK for the self link (i.e. the root package) to not have a lib 342 | // directory since it may just be a leaf application that only has 343 | // code in bin or web. 344 | if (!isSelfLink) { 345 | log.warning('Warning: Package "$name" does not have a "lib" directory so ' 346 | 'you will not be able to import any libraries from it.'); 347 | } 348 | } 349 | 350 | /// A line-by-line stream of standard input. 351 | final Stream stdinLines = streamToLines( 352 | new ByteStream(stdin).toStringStream()); 353 | 354 | /// Displays a message and reads a yes/no confirmation from the user. Returns 355 | /// a [Future] that completes to `true` if the user confirms or `false` if they 356 | /// do not. 357 | /// 358 | /// This will automatically append " (y/n)?" to the message, so [message] 359 | /// should just be a fragment like, "Are you sure you want to proceed". 360 | Future confirm(String message) { 361 | log.fine('Showing confirm message: $message'); 362 | stdout.write("$message (y/n)? "); 363 | return streamFirst(stdinLines) 364 | .then((line) => new RegExp(r"^[yY]").hasMatch(line)); 365 | } 366 | 367 | /// Reads and discards all output from [stream]. Returns a [Future] that 368 | /// completes when the stream is closed. 369 | Future drainStream(Stream stream) { 370 | return stream.fold(null, (x, y) {}); 371 | } 372 | 373 | /// Returns a [EventSink] that pipes all data to [consumer] and a [Future] that 374 | /// will succeed when [EventSink] is closed or fail with any errors that occur 375 | /// while writing. 376 | Pair consumerToSink(StreamConsumer consumer) { 377 | var controller = new StreamController(); 378 | var done = controller.stream.pipe(consumer); 379 | return new Pair(controller.sink, done); 380 | } 381 | 382 | // TODO(nweiz): remove this when issue 7786 is fixed. 383 | /// Pipes all data and errors from [stream] into [sink]. When [stream] is done, 384 | /// the returned [Future] is completed and [sink] is closed if [closeSink] is 385 | /// true. 386 | /// 387 | /// When an error occurs on [stream], that error is passed to [sink]. If 388 | /// [cancelOnError] is true, [Future] will be completed successfully and no 389 | /// more data or errors will be piped from [stream] to [sink]. If 390 | /// [cancelOnError] and [closeSink] are both true, [sink] will then be 391 | /// closed. 392 | Future store(Stream stream, EventSink sink, 393 | {bool cancelOnError: true, closeSink: true}) { 394 | var completer = new Completer(); 395 | stream.listen(sink.add, 396 | onError: (e) { 397 | sink.addError(e); 398 | if (cancelOnError) { 399 | completer.complete(); 400 | if (closeSink) sink.close(); 401 | } 402 | }, 403 | onDone: () { 404 | if (closeSink) sink.close(); 405 | completer.complete(); 406 | }, cancelOnError: cancelOnError); 407 | return completer.future; 408 | } 409 | 410 | /// Wraps [input] to provide a timeout. If [input] completes before 411 | /// [milliseconds] have passed, then the return value completes in the same way. 412 | /// However, if [milliseconds] pass before [input] has completed, it completes 413 | /// with a [TimeoutException] with [description] (which should be a fragment 414 | /// describing the action that timed out). 415 | /// 416 | /// Note that timing out will not cancel the asynchronous operation behind 417 | /// [input]. 418 | Future timeout(Future input, int milliseconds, String description) { 419 | var completer = new Completer(); 420 | var timer = new Timer(new Duration(milliseconds: milliseconds), () { 421 | completer.completeError(new TimeoutException( 422 | 'Timed out while $description.')); 423 | }); 424 | input.then((value) { 425 | if (completer.isCompleted) return; 426 | timer.cancel(); 427 | completer.complete(value); 428 | }).catchError((e) { 429 | if (completer.isCompleted) return; 430 | timer.cancel(); 431 | completer.completeError(e); 432 | }); 433 | return completer.future; 434 | } 435 | 436 | /// Creates a temporary directory and passes its path to [fn]. Once the [Future] 437 | /// returned by [fn] completes, the temporary directory and all its contents 438 | /// will be deleted. [fn] can also return `null`, in which case the temporary 439 | /// directory is deleted immediately afterwards. 440 | /// 441 | /// Returns a future that completes to the value that the future returned from 442 | /// [fn] completes to. 443 | Future withTempDir(Future fn(String path)) { 444 | return new Future.sync(() { 445 | var tempDir = createTempDir(); 446 | return new Future.sync(() => fn(tempDir)) 447 | .whenComplete(() => deleteEntry(tempDir)); 448 | }); 449 | } 450 | 451 | /// Exception thrown when an operation times out. 452 | class TimeoutException implements Exception { 453 | final String message; 454 | 455 | const TimeoutException(this.message); 456 | 457 | String toString() => message; 458 | } 459 | 460 | /// Gets a [Uri] for [uri], which can either already be one, or be a [String]. 461 | Uri _getUri(uri) { 462 | if (uri is Uri) return uri; 463 | return Uri.parse(uri); 464 | } 465 | -------------------------------------------------------------------------------- /lib/src/console/oauth2_console_client/log.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// Message logging. 6 | library log; 7 | 8 | import 'dart:io'; 9 | 10 | import 'utils.dart'; 11 | 12 | typedef LogFn(Entry entry); 13 | final Map _loggers = new Map(); 14 | 15 | /// The list of recorded log messages. Will only be recorded if 16 | /// [recordTranscript()] is called. 17 | List _transcript; 18 | 19 | /// An enum type for defining the different logging levels. By default, [ERROR] 20 | /// and [WARNING] messages are printed to sterr. [MESSAGE] messages are printed 21 | /// to stdout, and others are ignored. 22 | class Level { 23 | /// An error occurred and an operation could not be completed. Usually shown 24 | /// to the user on stderr. 25 | static const ERROR = const Level._("ERR "); 26 | 27 | /// Something unexpected happened, but the program was able to continue, 28 | /// though possibly in a degraded fashion. 29 | static const WARNING = const Level._("WARN"); 30 | 31 | /// A message intended specifically to be shown to the user. 32 | static const MESSAGE = const Level._("MSG "); 33 | 34 | /// Some interaction with the external world occurred, such as a network 35 | /// operation, process spawning, or file IO. 36 | static const IO = const Level._("IO "); 37 | 38 | /// Incremental output during pub's version constraint solver. 39 | static const SOLVER = const Level._("SLVR"); 40 | 41 | /// Fine-grained and verbose additional information. Can be used to provide 42 | /// program state context for other logs (such as what pub was doing when an 43 | /// IO operation occurred) or just more detail for an operation. 44 | static const FINE = const Level._("FINE"); 45 | 46 | const Level._(this.name); 47 | final String name; 48 | 49 | String toString() => name; 50 | int get hashCode => name.hashCode; 51 | } 52 | 53 | /// A single log entry. 54 | class Entry { 55 | final Level level; 56 | final List lines; 57 | 58 | Entry(this.level, this.lines); 59 | } 60 | 61 | /// Logs [message] at [Level.ERROR]. 62 | void error(message, [error]) { 63 | if (error != null) { 64 | message = "$message: $error"; 65 | var trace; 66 | if (error is Error) trace = error.stackTrace; 67 | if (trace != null) { 68 | message = "$message\nStackTrace: $trace"; 69 | } 70 | } 71 | write(Level.ERROR, message); 72 | } 73 | 74 | /// Logs [message] at [Level.WARNING]. 75 | void warning(message) => write(Level.WARNING, message); 76 | 77 | /// Logs [message] at [Level.MESSAGE]. 78 | void message(message) => write(Level.MESSAGE, message); 79 | 80 | /// Logs [message] at [Level.IO]. 81 | void io(message) => write(Level.IO, message); 82 | 83 | /// Logs [message] at [Level.SOLVER]. 84 | void solver(message) => write(Level.SOLVER, message); 85 | 86 | /// Logs [message] at [Level.FINE]. 87 | void fine(message) => write(Level.FINE, message); 88 | 89 | /// Logs [message] at [level]. 90 | void write(Level level, message) { 91 | if (_loggers.isEmpty) showNormal(); 92 | 93 | var lines = splitLines(message.toString()); 94 | var entry = new Entry(level, lines); 95 | 96 | var logFn = _loggers[level]; 97 | if (logFn != null) logFn(entry); 98 | 99 | if (_transcript != null) _transcript.add(entry); 100 | } 101 | 102 | /// Sets the verbosity to "normal", which shows errors, warnings, and messages. 103 | void showNormal() { 104 | _loggers[Level.ERROR] = _logToStderr; 105 | _loggers[Level.WARNING] = _logToStderr; 106 | _loggers[Level.MESSAGE] = _logToStdout; 107 | _loggers[Level.IO] = null; 108 | _loggers[Level.SOLVER] = null; 109 | _loggers[Level.FINE] = null; 110 | } 111 | 112 | /// Log function that prints the message to stdout. 113 | void _logToStdout(Entry entry) { 114 | _logToStream(stdout, entry, showLabel: false); 115 | } 116 | 117 | /// Log function that prints the message to stderr. 118 | void _logToStderr(Entry entry) { 119 | _logToStream(stderr, entry, showLabel: false); 120 | } 121 | 122 | /// Log function that prints the message to stderr with the level name. 123 | void _logToStderrWithLabel(Entry entry) { 124 | _logToStream(stderr, entry, showLabel: true); 125 | } 126 | 127 | void _logToStream(IOSink sink, Entry entry, {bool showLabel}) { 128 | bool firstLine = true; 129 | for (var line in entry.lines) { 130 | if (showLabel) { 131 | if (firstLine) { 132 | sink.write('${entry.level.name}: '); 133 | } else { 134 | sink.write(' | '); 135 | } 136 | } 137 | 138 | sink.writeln(line); 139 | 140 | firstLine = false; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /lib/src/console/oauth2_console_client/oauth2.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | library oauth2; 6 | import 'dart:core'; 7 | 8 | import 'dart:async'; 9 | import 'dart:convert'; 10 | import 'dart:io'; 11 | 12 | import 'package:oauth2/oauth2.dart'; 13 | import 'package:path/path.dart' as path; 14 | import 'package:http/http.dart' as http; 15 | import "package:json_web_token/json_web_token.dart"; 16 | 17 | import 'http.dart'; 18 | import 'io.dart'; 19 | import 'log.dart' as log; 20 | import 'utils.dart'; 21 | 22 | export 'package:oauth2/oauth2.dart'; 23 | 24 | class OAuth2Console { 25 | 26 | /// The pub client's OAuth2 identifier. 27 | String _identifier = ""; 28 | 29 | /// The pub client's OAuth2 secret. This isn't actually meant to be kept a 30 | /// secret. 31 | String _secret = ""; 32 | 33 | /// The URL to which the user will be directed to authorize the pub client to 34 | /// get an OAuth2 access token. 35 | /// 36 | /// `access_type=offline` and `approval_prompt=force` ensures that we always get 37 | /// a refresh token from the server. See the [Google OAuth2 documentation][]. 38 | /// 39 | /// [Google OAuth2 documentation]: https://developers.google.com/accounts/docs/OAuth2WebServer#offline 40 | Uri _authorizationEndpoint = Uri.parse( 41 | 'https://accounts.google.com/o/oauth2/auth?access_type=offline' 42 | '&approval_prompt=force'); 43 | 44 | /// The URL from which the pub client will request an access token once it's 45 | /// been authorized by the user. 46 | Uri _tokenEndpoint = Uri.parse( 47 | 'https://accounts.google.com/o/oauth2/token'); 48 | Uri get tokenEndpoint => _tokenEndpoint; 49 | 50 | /// The OAuth2 scopes that the pub client needs. Currently the client only needs 51 | /// the user's email so that the server can verify their identity. 52 | List _scopes = ['https://www.googleapis.com/auth/userinfo.email']; 53 | List _request_visible_actions; 54 | 55 | /// An in-memory cache of the user's OAuth2 credentials. This should always be 56 | /// the same as the credentials file stored in the system cache. 57 | Credentials _credentials; 58 | Credentials get credentials => _credentials; 59 | 60 | /// Url to redirect when authorization has been called 61 | final String authorizedRedirect; 62 | 63 | final String credentialsFilePath; 64 | 65 | /// The port that should be used for the one-shot authorization response 66 | /// server 67 | final int authorizationResponseServerPort; 68 | 69 | PubHttpClient _httpClient; 70 | 71 | OAuth2Console({String identifier: null, String secret: null, 72 | Uri authorizationEndpoint: null, Uri tokenEndpoint: null, List scopes: null, 73 | List request_visible_actions: null, 74 | this.authorizedRedirect: 'https://github.com/dart-gde/dart-google-oauth2-library', 75 | this.credentialsFilePath: 'credentials.json', 76 | this.authorizationResponseServerPort: 0}) { 77 | 78 | if (identifier != null) this._identifier = identifier; 79 | if (secret != null) this._secret = secret; 80 | if (authorizationEndpoint != null) this._authorizationEndpoint = authorizationEndpoint; 81 | if (tokenEndpoint != null) this._tokenEndpoint = tokenEndpoint; 82 | if (scopes != null) this._scopes = scopes; 83 | if (request_visible_actions != null) this._request_visible_actions = request_visible_actions; 84 | 85 | _httpClient = new PubHttpClient(); 86 | _httpClient.tokenEndpoint = tokenEndpoint; 87 | } 88 | 89 | /// Delete the cached credentials, if they exist. 90 | void clearCredentials() { 91 | _credentials = null; 92 | if (entryExists(credentialsFilePath)) deleteEntry(credentialsFilePath); 93 | } 94 | 95 | /// Close the httpClient when were done. 96 | void close() { 97 | _httpClient.inner.close(); 98 | } 99 | 100 | /// Asynchronously passes an OAuth2 [Client] to [fn], and closes the client when 101 | /// the [Future] returned by [fn] completes. 102 | /// 103 | /// This takes care of loading and saving the client's credentials, as well as 104 | /// prompting the user for their authorization. It will also re-authorize and 105 | /// re-run [fn] if a recoverable authorization error is detected. 106 | Future withClient(Future fn(Client client)) { 107 | 108 | return _getClient().then((client) { 109 | _credentials = client.credentials; 110 | return fn(client).whenComplete(() { 111 | client.close(); 112 | // Be sure to save the credentials even when an error happens. 113 | _saveCredentials(client.credentials); 114 | }); 115 | }).catchError((error) { 116 | if (error is ExpirationException) { 117 | log.error("Authorization to upload packages has expired and " 118 | "can't be automatically refreshed."); 119 | return withClient(fn); 120 | } else if (error is AuthorizationException) { 121 | var message = "OAuth2 authorization failed"; 122 | if (error.description != null) { 123 | message = "$message (${error.description})"; 124 | } 125 | log.error("$message."); 126 | clearCredentials(); 127 | return withClient(fn); 128 | } else { 129 | throw error; 130 | } 131 | }); 132 | } 133 | 134 | Future _getClient() { 135 | return new Future.sync(() { 136 | var credentials = _loadCredentials(); 137 | if (credentials == null) return _authorize(); 138 | 139 | var client = new Client(_identifier, _secret, credentials, 140 | httpClient: _httpClient); 141 | _saveCredentials(client.credentials); 142 | return client; 143 | }); 144 | } 145 | 146 | /// Loads the user's OAuth2 credentials from the in-memory cache or the 147 | /// filesystem if possible. If the credentials can't be loaded for any reason, 148 | /// the returned [Future] will complete to null. 149 | Credentials _loadCredentials() { 150 | log.fine('Loading OAuth2 credentials.'); 151 | 152 | try { 153 | if (_credentials != null) return _credentials; 154 | 155 | if (!fileExists(credentialsFilePath)) return null; 156 | 157 | var credentials = new Credentials.fromJson(readTextFile(credentialsFilePath)); 158 | if (credentials.isExpired && !credentials.canRefresh) { 159 | log.error("Authorization has expired and " 160 | "can't be automatically refreshed."); 161 | return null; // null means re-authorize. 162 | } 163 | 164 | return credentials; 165 | } catch (e) { 166 | log.error('Warning: could not load the saved OAuth2 credentials: $e\n' 167 | 'Obtaining new credentials...', e); 168 | return null; // null means re-authorize. 169 | } 170 | } 171 | 172 | /// Save the user's OAuth2 credentials to the in-memory cache and the 173 | /// filesystem. 174 | void _saveCredentials(Credentials credentials) { 175 | log.fine('Saving OAuth2 credentials.'); 176 | _credentials = credentials; 177 | ensureDir(path.dirname(credentialsFilePath)); 178 | writeTextFile(credentialsFilePath, credentials.toJson(), dontLogContents: true); 179 | } 180 | 181 | /// Gets the user to authorize pub as a client of pub.dartlang.org via oauth2. 182 | /// Returns a Future that will complete to a fully-authorized [Client]. 183 | Future _authorize() { 184 | var grant = new AuthorizationCodeGrant( 185 | _identifier, 186 | _secret, 187 | _authorizationEndpoint, 188 | tokenEndpoint, 189 | httpClient: _httpClient); 190 | 191 | // Spin up a one-shot HTTP server to receive the authorization code from the 192 | // Google OAuth2 server via redirect. This server will close itself as soon as 193 | // the code is received. 194 | return HttpServer.bind('127.0.0.1', authorizationResponseServerPort).then((server) { 195 | var authUrl = grant.getAuthorizationUrl( 196 | Uri.parse('http://localhost:${server.port}'), scopes: _scopes); 197 | 198 | log.message( 199 | 'Need your authorization to access scopes ${_scopes} on your behalf.\n' 200 | 'In a web browser, go to $authUrl\n' 201 | 'Then click "Allow access".\n\n' 202 | 'Waiting for your authorization...'); 203 | return server.first.then((request) { 204 | var response = request.response; 205 | if (request.uri.path == "/") { 206 | log.message('Authorization received, processing...'); 207 | var queryString = request.uri.query; 208 | if (queryString == null) queryString = ''; 209 | response.statusCode = 302; 210 | response.headers.set('location', authorizedRedirect); 211 | response.close(); 212 | return grant.handleAuthorizationResponse(queryToMap(queryString)) 213 | .then((client) { 214 | server.close(); 215 | return client; 216 | }); 217 | } else { 218 | response.statusCode = 404; 219 | response.close(); 220 | } 221 | }); 222 | }) 223 | .then((client) { 224 | log.message('Successfully authorized.\n'); 225 | return client; 226 | }); 227 | } 228 | } 229 | 230 | /** 231 | * A simple OAuth2 authentication client when [secret] and [accessToken] 232 | * are already available. [credentials] are kept in memory. 233 | */ 234 | class SimpleOAuth2Console implements OAuth2Console { 235 | /// The URL from which the pub client will request an access token once it's 236 | /// been authorized by the user. 237 | Uri _tokenEndpoint = Uri.parse('https://accounts.google.com/o/oauth2/token'); 238 | Uri get tokenEndpoint => _tokenEndpoint; 239 | 240 | Credentials _credentials; 241 | 242 | Credentials get credentials => _credentials; 243 | 244 | void set credentials(value) { 245 | _credentials = value; 246 | } 247 | 248 | String _identifier; 249 | 250 | String _secret; 251 | 252 | String _accessToken; 253 | 254 | final int authorizationResponseServerPort = null; 255 | 256 | Client _simpleHttpClient; 257 | 258 | SimpleOAuth2Console(this._identifier, this._secret, this._accessToken) { 259 | this.credentials = new Credentials(_accessToken); 260 | } 261 | 262 | Future withClient(Future fn(Client client)) { 263 | log.fine("withClient(Future ${fn}(Client client))"); 264 | _simpleHttpClient = new Client(_identifier, _secret, _credentials); 265 | return fn(_simpleHttpClient); 266 | } 267 | 268 | void close() { 269 | _simpleHttpClient.close(); 270 | } 271 | 272 | /* 273 | * Methods and variables not supported by this client. 274 | */ 275 | 276 | void clearCredentials() { 277 | throw new UnsupportedError("clearCredentials"); 278 | } 279 | 280 | void set authorizedRedirect(String _authorizedRedirect) { 281 | throw new UnsupportedError("authorizedRedirect"); 282 | } 283 | 284 | String get authorizedRedirect => null; 285 | 286 | String get credentialsFilePath => null; 287 | 288 | void set credentialsFilePath(String _credentialsFilePath) { 289 | throw new UnsupportedError("credentialsFilePath"); 290 | } 291 | 292 | Future _getClient() { 293 | throw new UnsupportedError("_getClient"); 294 | } 295 | 296 | Credentials _loadCredentials() { 297 | throw new UnsupportedError("_loadCredentials"); 298 | } 299 | void _saveCredentials(Credentials credentials) { 300 | throw new UnsupportedError("_saveCredentials"); 301 | } 302 | Future _authorize() { 303 | throw new UnsupportedError("_authorize"); 304 | } 305 | 306 | List _scopes; 307 | Uri _authorizationEndpoint; 308 | List _request_visible_actions; 309 | PubHttpClient _httpClient; 310 | } 311 | 312 | /** 313 | * A compute engine oauth2 console client. 314 | */ 315 | class ComputeOAuth2Console implements OAuth2Console { 316 | Uri _tokenEndpoint = 317 | Uri.parse('http://metadata/computeMetadata/v1beta1/instance/service-accounts/default/token'); 318 | Uri get tokenEndpoint => _tokenEndpoint; 319 | 320 | Credentials _credentials; 321 | 322 | Credentials get credentials => _credentials; 323 | 324 | void set credentials(value) { 325 | _credentials = value; 326 | } 327 | 328 | String _identifier = ""; 329 | 330 | String _secret = ""; 331 | 332 | String _accessToken = ""; 333 | 334 | final int authorizationResponseServerPort = null; 335 | 336 | Client _simpleHttpClient; 337 | 338 | final String projectId; 339 | final String privateKey; 340 | final String iss; 341 | final String scopes; 342 | 343 | ComputeOAuth2Console(this.projectId, {this.privateKey: null, this.iss: null, this.scopes: null}); 344 | 345 | Future withClient(Future fn(Client client)) { 346 | log.fine("withClient(Future ${fn}(Client client))"); 347 | 348 | if (this.privateKey == null && this.iss == null && this.scopes == null) { 349 | _simpleHttpClient = new ComputeEngineClient(projectId); 350 | } else { 351 | _simpleHttpClient = new OtherPlatformClient(projectId, privateKey, iss, scopes); 352 | } 353 | 354 | return fn(_simpleHttpClient); 355 | } 356 | 357 | void close() { 358 | _simpleHttpClient.close(); 359 | } 360 | 361 | /* 362 | * Methods and variables not supported by this client. 363 | */ 364 | 365 | void clearCredentials() { 366 | throw new UnsupportedError("clearCredentials"); 367 | } 368 | 369 | void set authorizedRedirect(String _authorizedRedirect) { 370 | throw new UnsupportedError("authorizedRedirect"); 371 | } 372 | 373 | String get authorizedRedirect => null; 374 | 375 | String get credentialsFilePath => null; 376 | 377 | void set credentialsFilePath(String _credentialsFilePath) { 378 | throw new UnsupportedError("credentialsFilePath"); 379 | } 380 | 381 | Future _getClient() { 382 | throw new UnsupportedError("_getClient"); 383 | } 384 | 385 | Credentials _loadCredentials() { 386 | throw new UnsupportedError("_loadCredentials"); 387 | } 388 | void _saveCredentials(Credentials credentials) { 389 | throw new UnsupportedError("_saveCredentials"); 390 | } 391 | Future _authorize() { 392 | throw new UnsupportedError("_authorize"); 393 | } 394 | 395 | List _scopes; 396 | Uri _authorizationEndpoint; 397 | List _request_visible_actions; 398 | PubHttpClient _httpClient; 399 | } 400 | 401 | class OtherPlatformClient extends http.BaseClient implements Client { 402 | final String identifier = ""; 403 | final String secret = ""; 404 | final String projectId; 405 | Credentials get credentials => _credentials; 406 | Credentials _credentials; 407 | http.Client _httpClient; 408 | 409 | final String privateKey; 410 | final String iss; 411 | final String scopes; 412 | 413 | OtherPlatformClient(this.projectId, this.privateKey, this.iss, this.scopes, 414 | {http.Client httpClient}) 415 | : _httpClient = httpClient == null ? new http.Client() : httpClient { 416 | JWTStore.getCurrent().registerKey(iss, privateKey); 417 | } 418 | 419 | Future send(http.BaseRequest request) { 420 | return async.then((_) { 421 | // TODO(adam): should we configure iat isExpired check? 422 | return JWTStore.getCurrent().generateJWT(iss, scopes); 423 | }).then((JWT jwt) { 424 | request.headers['authorization'] = "Bearer ${jwt.accessToken}"; 425 | return _httpClient.send(request); 426 | }).then((response) => response); 427 | // TODO(adam): better error handling similar to client.dart 428 | } 429 | 430 | Future refreshCredentials([List newScopes]) { 431 | return new Future.value(); 432 | } 433 | 434 | void close() { 435 | if (_httpClient != null) _httpClient.close(); 436 | _httpClient = null; 437 | } 438 | } 439 | 440 | class ComputeEngineClient extends http.BaseClient implements Client { 441 | final String identifier = ""; 442 | final String secret = ""; 443 | final String projectId; 444 | Credentials get credentials => _credentials; 445 | Credentials _credentials; 446 | http.Client _httpClient; 447 | 448 | Uri _tokenEndpoint = 449 | Uri.parse('http://metadata/computeMetadata/v1beta1/instance/service-accounts/default/token'); 450 | 451 | ComputeEngineClient(this.projectId, 452 | {http.Client httpClient}) 453 | : _httpClient = httpClient == null ? new http.Client() : httpClient; 454 | 455 | Future send(http.BaseRequest request) { 456 | return async.then((_) { 457 | // TODO: check if credentials need to be refreshed. 458 | return refreshCredentials(); 459 | }).then((_) { 460 | request.headers['Authorization'] = "OAuth ${credentials.accessToken}"; 461 | request.headers['x-goog-api-version'] = "2"; 462 | request.headers['x-goog-project-id'] = "${projectId}"; 463 | return _httpClient.send(request); 464 | }).then((response) { 465 | return response; 466 | }); 467 | } 468 | 469 | // TODO: use a token store instead of fetching each time. Checkout JWTStore. 470 | Future refreshCredentials([List newScopes]) { 471 | return async.then((_) { 472 | return _httpClient.get(_tokenEndpoint, 473 | headers: {'X-Google-Metadata-Request': 'True'}); 474 | }).then((http.Response response) { 475 | if (response.statusCode == 200) { 476 | // Successful request 477 | var tokenData = JSON.decode(response.body); 478 | int expiresIn = tokenData["expires_in"]; 479 | var expiresTime = new DateTime.now(); 480 | expiresTime = expiresTime.add(new Duration(seconds: expiresIn)); 481 | _credentials = new Credentials(tokenData["access_token"], null, null, 482 | null, expiresTime); 483 | return this; 484 | } else { 485 | // Unsuccessful request 486 | throw new StateError("status code = ${response.statusCode}"); 487 | } 488 | }); 489 | } 490 | 491 | void close() { 492 | if (_httpClient != null) _httpClient.close(); 493 | _httpClient = null; 494 | } 495 | } 496 | 497 | /// Returns a [Future] that asynchronously completes to `null`. 498 | Future get async => new Future.delayed(const Duration(milliseconds: 0), 499 | () => null); -------------------------------------------------------------------------------- /lib/src/console/oauth2_console_client/utils.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// Generic utility functions. Stuff that should possibly be in core. 6 | library utils; 7 | 8 | import 'dart:async'; 9 | import 'package:crypto/crypto.dart'; 10 | 11 | /// A pair of values. 12 | class Pair { 13 | E first; 14 | F last; 15 | 16 | Pair(this.first, this.last); 17 | 18 | String toString() => '($first, $last)'; 19 | 20 | bool operator==(other) { 21 | if (other is! Pair) return false; 22 | return other.first == first && other.last == last; 23 | } 24 | 25 | int get hashCode => first.hashCode ^ last.hashCode; 26 | } 27 | 28 | /// A completer that waits until all added [Future]s complete. 29 | // TODO(rnystrom): Copied from web_components. Remove from here when it gets 30 | // added to dart:core. (See #6626.) 31 | class FutureGroup { 32 | int _pending = 0; 33 | Completer> _completer = new Completer>(); 34 | final List> futures = >[]; 35 | bool completed = false; 36 | 37 | final List _values = []; 38 | 39 | /// Wait for [task] to complete. 40 | Future add(Future task) { 41 | if (completed) { 42 | throw new StateError("The FutureGroup has already completed."); 43 | } 44 | 45 | _pending++; 46 | futures.add(task.then((value) { 47 | if (completed) return; 48 | 49 | _pending--; 50 | _values.add(value); 51 | 52 | if (_pending <= 0) { 53 | completed = true; 54 | _completer.complete(_values); 55 | } 56 | }).catchError((e) { 57 | if (completed) return; 58 | 59 | completed = true; 60 | _completer.completeError(e); 61 | }) as Future); 62 | 63 | return task; 64 | } 65 | 66 | Future get future => _completer.future; 67 | } 68 | 69 | // TODO(rnystrom): Move into String? 70 | /// Pads [source] to [length] by adding spaces at the end. 71 | String padRight(String source, int length) { 72 | final result = new StringBuffer(); 73 | result.write(source); 74 | 75 | while (result.length < length) { 76 | result.write(' '); 77 | } 78 | 79 | return result.toString(); 80 | } 81 | 82 | /// Flattens nested lists inside an iterable into a single list containing only 83 | /// non-list elements. 84 | List flatten(Iterable nested) { 85 | var result = []; 86 | helper(list) { 87 | for (var element in list) { 88 | if (element is List) { 89 | helper(element); 90 | } else { 91 | result.add(element); 92 | } 93 | } 94 | } 95 | helper(nested); 96 | return result; 97 | } 98 | 99 | /// Asserts that [iter] contains only one element, and returns it. 100 | only(Iterable iter) { 101 | var iterator = iter.iterator; 102 | var currentIsValid = iterator.moveNext(); 103 | assert(currentIsValid); 104 | var obj = iterator.current; 105 | assert(!iterator.moveNext()); 106 | return obj; 107 | } 108 | 109 | /// Returns a set containing all elements in [minuend] that are not in 110 | /// [subtrahend]. 111 | Set setMinus(Iterable minuend, Iterable subtrahend) { 112 | var minuendSet = new Set.from(minuend); 113 | minuendSet.removeAll(subtrahend); 114 | return minuendSet; 115 | } 116 | 117 | /// Replace each instance of [matcher] in [source] with the return value of 118 | /// [fn]. 119 | String replace(String source, Pattern matcher, String fn(Match)) { 120 | var buffer = new StringBuffer(); 121 | var start = 0; 122 | for (var match in matcher.allMatches(source)) { 123 | buffer.write(source.substring(start, match.start)); 124 | start = match.end; 125 | buffer.write(fn(match)); 126 | } 127 | buffer.write(source.substring(start)); 128 | return buffer.toString(); 129 | } 130 | 131 | /// Returns whether or not [str] ends with [matcher]. 132 | bool endsWithPattern(String str, Pattern matcher) { 133 | for (var match in matcher.allMatches(str)) { 134 | if (match.end == str.length) return true; 135 | } 136 | return false; 137 | } 138 | 139 | /// Returns the hex-encoded sha1 hash of [source]. 140 | String sha1(String source) { 141 | var sha = new SHA1(); 142 | sha.add(source.codeUnits); 143 | return CryptoUtils.bytesToHex(sha.close()); 144 | } 145 | 146 | /// Returns a [Future] that completes in [milliseconds]. 147 | Future sleep(int milliseconds) { 148 | var completer = new Completer(); 149 | new Timer(new Duration(milliseconds: milliseconds), completer.complete); 150 | return completer.future; 151 | } 152 | 153 | /// Configures [future] so that its result (success or exception) is passed on 154 | /// to [completer]. 155 | void chainToCompleter(Future future, Completer completer) { 156 | future.then((value) => completer.complete(value), 157 | onError: (e) => completer.completeError(e)); 158 | } 159 | 160 | // TODO(nweiz): remove this when issue 7964 is fixed. 161 | /// Returns a [Future] that will complete to the first element of [stream]. 162 | /// Unlike [Stream.first], this is safe to use with single-subscription streams. 163 | Future streamFirst(Stream stream) { 164 | var completer = new Completer(); 165 | var subscription; 166 | subscription = stream.listen((value) { 167 | subscription.cancel(); 168 | completer.complete(value); 169 | }, onError: (e) { 170 | completer.completeError(e); 171 | }, onDone: () { 172 | completer.completeError(new StateError("No elements")); 173 | }, cancelOnError: true); 174 | return completer.future; 175 | } 176 | 177 | /// Returns a wrapped version of [stream] along with a [StreamSubscription] that 178 | /// can be used to control the wrapped stream. 179 | Pair streamWithSubscription(Stream stream) { 180 | var controller = new StreamController(); 181 | var controllerStream = stream.isBroadcast ? 182 | controller.stream.asBroadcastStream() : 183 | controller.stream; 184 | var subscription = stream.listen(controller.add, 185 | onError: controller.addError, 186 | onDone: controller.close); 187 | return new Pair(controllerStream, subscription); 188 | } 189 | 190 | // TODO(nweiz): remove this when issue 7787 is fixed. 191 | /// Creates two single-subscription [Stream]s that each emit all values and 192 | /// errors from [stream]. This is useful if [stream] is single-subscription but 193 | /// multiple subscribers are necessary. 194 | Pair tee(Stream stream) { 195 | var controller1 = new StreamController(); 196 | var controller2 = new StreamController(); 197 | stream.listen((value) { 198 | controller1.add(value); 199 | controller2.add(value); 200 | }, onError: (error) { 201 | controller1.addError(error); 202 | controller2.addError(error); 203 | }, onDone: () { 204 | controller1.close(); 205 | controller2.close(); 206 | }); 207 | return new Pair(controller1.stream, controller2.stream); 208 | } 209 | 210 | /// A regular expression matching a trailing CR character. 211 | final _trailingCR = new RegExp(r"\r$"); 212 | 213 | // TODO(nweiz): Use `text.split(new RegExp("\r\n?|\n\r?"))` when issue 9360 is 214 | // fixed. 215 | /// Splits [text] on its line breaks in a Windows-line-break-friendly way. 216 | List splitLines(String text) => 217 | text.split("\n").map((line) => line.replaceFirst(_trailingCR, "")).toList(); 218 | 219 | /// Converts a stream of arbitrarily chunked strings into a line-by-line stream. 220 | /// The lines don't include line termination characters. A single trailing 221 | /// newline is ignored. 222 | Stream streamToLines(Stream stream) { 223 | var buffer = new StringBuffer(); 224 | return stream.transform(new StreamTransformer.fromHandlers( 225 | handleData: (chunk, sink) { 226 | var lines = splitLines(chunk); 227 | var leftover = lines.removeLast(); 228 | for (var line in lines) { 229 | if (!buffer.isEmpty) { 230 | buffer.write(line); 231 | line = buffer.toString(); 232 | buffer = new StringBuffer(); 233 | } 234 | 235 | sink.add(line); 236 | } 237 | buffer.write(leftover); 238 | }, 239 | handleDone: (sink) { 240 | if (!buffer.isEmpty) sink.add(buffer.toString()); 241 | sink.close(); 242 | })); 243 | } 244 | 245 | /// Like [Iterable.where], but allows [test] to return [Future]s and uses the 246 | /// results of those [Future]s as the test. 247 | Future futureWhere(Iterable iter, test(value)) { 248 | return Future.wait(iter.map((e) { 249 | var result = test(e); 250 | if (result is! Future) result = new Future.value(result); 251 | return result.then((result) => new Pair(e, result)); 252 | })) 253 | .then((pairs) => pairs.where((pair) => pair.last)) 254 | .then((pairs) => pairs.map((pair) => pair.first)); 255 | } 256 | 257 | // TODO(nweiz): unify the following functions with the utility functions in 258 | // pkg/http. 259 | 260 | /// Like [String.split], but only splits on the first occurrence of the pattern. 261 | /// This will always return an array of two elements or fewer. 262 | List split1(String toSplit, String pattern) { 263 | if (toSplit.isEmpty) return []; 264 | 265 | var index = toSplit.indexOf(pattern); 266 | if (index == -1) return [toSplit]; 267 | return [toSplit.substring(0, index), 268 | toSplit.substring(index + pattern.length)]; 269 | } 270 | 271 | /// Adds additional query parameters to [url], overwriting the original 272 | /// parameters if a name conflict occurs. 273 | Uri addQueryParameters(Uri url, Map parameters) { 274 | var queryMap = queryToMap(url.query); 275 | mapAddAll(queryMap, parameters); 276 | return url.resolve("?${mapToQuery(queryMap)}"); 277 | } 278 | 279 | /// Convert a URL query string (or `application/x-www-form-urlencoded` body) 280 | /// into a [Map] from parameter names to values. 281 | Map queryToMap(String queryList) { 282 | var map = {}; 283 | for (var pair in queryList.split("&")) { 284 | var split = split1(pair, "="); 285 | if (split.isEmpty) continue; 286 | var key = urlDecode(split[0]); 287 | var value = split.length > 1 ? urlDecode(split[1]) : ""; 288 | map[key] = value; 289 | } 290 | return map; 291 | } 292 | 293 | /// Convert a [Map] from parameter names to values to a URL query string. 294 | String mapToQuery(Map map) { 295 | var pairs = >[]; 296 | map.forEach((key, value) { 297 | key = Uri.encodeComponent(key); 298 | value = (value == null || value.isEmpty) ? null : Uri.encodeComponent(value); 299 | pairs.add([key, value]); 300 | }); 301 | return pairs.map((pair) { 302 | if (pair[1] == null) return pair[0]; 303 | return "${pair[0]}=${pair[1]}"; 304 | }).join("&"); 305 | } 306 | 307 | // TODO(nweiz): remove this when issue 9068 has been fixed. 308 | /// Whether [uri1] and [uri2] are equal. This consider HTTP URIs to default to 309 | /// port 80, and HTTPs URIs to default to port 443. 310 | bool urisEqual(Uri uri1, Uri uri2) => 311 | canonicalizeUri(uri1) == canonicalizeUri(uri2); 312 | 313 | /// Return [uri] with redundant port information removed. 314 | Uri canonicalizeUri(Uri uri) { 315 | if (uri == null) return null; 316 | 317 | var sansPort = new Uri( 318 | scheme: uri.scheme, userInfo: uri.userInfo, host: uri.host, 319 | path: uri.path, query: uri.query, fragment: uri.fragment); 320 | if (uri.scheme == 'http' && uri.port == 80) return sansPort; 321 | if (uri.scheme == 'https' && uri.port == 443) return sansPort; 322 | return uri; 323 | } 324 | 325 | /// Add all key/value pairs from [source] to [destination], overwriting any 326 | /// pre-existing values. 327 | void mapAddAll(Map destination, Map source) => 328 | source.forEach((key, value) => destination[key] = value); 329 | 330 | /// Decodes a URL-encoded string. Unlike [decodeUriComponent], this includes 331 | /// replacing `+` with ` `. 332 | String urlDecode(String encoded) => 333 | Uri.decodeComponent(encoded.replaceAll("+", " ")); 334 | 335 | /// Takes a simple data structure (composed of [Map]s, [Iterable]s, scalar 336 | /// objects, and [Future]s) and recursively resolves all the [Future]s contained 337 | /// within. Completes with the fully resolved structure. 338 | Future awaitObject(object) { 339 | // Unroll nested futures. 340 | if (object is Future) return object.then(awaitObject); 341 | if (object is Iterable) { 342 | return Future.wait(object.map(awaitObject).toList()); 343 | } 344 | if (object is! Map) return new Future.value(object); 345 | 346 | var pairs = >[]; 347 | object.forEach((key, value) { 348 | pairs.add(awaitObject(value) 349 | .then((resolved) => new Pair(key, resolved))); 350 | }); 351 | return Future.wait(pairs).then((resolvedPairs) { 352 | var map = {}; 353 | for (var pair in resolvedPairs) { 354 | map[pair.first] = pair.last; 355 | } 356 | return map; 357 | }); 358 | } 359 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: google_oauth2_client 2 | version: 0.4.2 3 | authors: 4 | - Gerwin Sturm 5 | - Adam Singer 6 | - Kevin Moore 7 | description: Library to use for Google OAuth2 authentication / Client-side flow 8 | homepage: https://github.com/dart-gde/dart-google-oauth2-library 9 | environment: 10 | sdk: '>=1.3.0 <2.0.0' 11 | dependencies: 12 | crypto: '>=0.9.0 <0.10.0' 13 | http: '>=0.9.0 <0.12.0' 14 | json_web_token: '>=0.1.1 <0.2.0' 15 | oauth2: '>=0.9.0 <0.10.0' 16 | path: '>=1.0.0 <2.0.0' 17 | dev_dependencies: 18 | browser: '>=0.9.0 <0.11.0' 19 | hop: '>=0.30.3 <0.32.0' 20 | -------------------------------------------------------------------------------- /tool/hop_runner.dart: -------------------------------------------------------------------------------- 1 | library hop_runner; 2 | 3 | import 'package:hop/hop.dart'; 4 | import 'package:hop/hop_tasks.dart'; 5 | 6 | void main(List args) { 7 | 8 | final libList = ['lib/google_oauth2_browser.dart', 'lib/google_oauth2_console.dart']; 9 | 10 | // TODO: move to docgen, it breaks hop 11 | // addTask('docs', createDartDocTask(libList, linkApi: true)); 12 | 13 | addTask('analyze_libs', createAnalyzerTask(libList)); 14 | 15 | runHop(args); 16 | } 17 | --------------------------------------------------------------------------------