1 // Copyright 2011 Google Inc. All Rights Reserved.
3 package com
.google
.appengine
.tools
.admin
;
5 import com
.google
.appengine
.tools
.admin
.AppAdminFactory
.ConnectOptions
;
6 import com
.google
.appengine
.tools
.util
.ClientCookie
;
7 import com
.google
.appengine
.tools
.util
.ClientCookieManager
;
8 import com
.google
.common
.annotations
.VisibleForTesting
;
10 import java
.io
.BufferedReader
;
11 import java
.io
.ByteArrayOutputStream
;
12 import java
.io
.IOException
;
13 import java
.io
.ObjectOutputStream
;
14 import java
.net
.HttpURLConnection
;
16 import java
.util
.HashMap
;
17 import java
.util
.Iterator
;
18 import java
.util
.List
;
20 import java
.util
.prefs
.Preferences
;
24 public class ClientLoginServerConnection
extends AbstractServerConnection
{
27 * Exception type for email/password mismatch failures.
29 public class ClientAuthFailException
extends ClientLoginException
{
30 public ClientAuthFailException(String s
) {
34 public ClientAuthFailException(String s
, Throwable t
) {
39 * Exception type for login failures.
41 public class ClientLoginException
extends IOException
{
42 public ClientLoginException(String s
) {
46 public ClientLoginException(String s
, Throwable t
) {
51 protected ClientCookieManager cookies
;
53 public ClientLoginServerConnection(ConnectOptions options
) {
55 cookies
= options
.getCookies();
56 if (cookies
== null) {
57 cookies
= new ClientCookieManager();
62 * Authenticates the user.
64 * The authentication process works as follows: 1) We get a username and
65 * password from the user 2) We use ClientLogin to obtain an AUTH token for
66 * the user 3) We pass the auth token to /_ah/login on the server to obtain an
67 * authentication cookie. If login was successful, it tries to redirect us to
68 * the URL we provided.
70 * If we attempt to access the upload API without first obtaining an
71 * authentication cookie, it returns a 401 response and directs us to
72 * authenticate ourselves with ClientLogin.
74 * @param url which require authentication
77 * href="http://code.google.com/apis/accounts/AuthForInstalledApps.html"></a>
79 private void authenticate(URL url
, String account_type
) throws ClientLoginException
,
81 for (int unused
= 1;; ++unused
) {
83 String authToken
= getAuthToken(url
.getHost(), account_type
);
84 getAuthCookie(authToken
);
86 } catch (ClientLoginException e
) {
96 void checkAuthCookie(URL url
) throws ClientLoginException
{
97 long minExpireTime
= System
.currentTimeMillis() + 60 * 1000;
98 Iterator
<ClientCookie
> li
= cookies
.getCookies();
99 while (li
.hasNext()) {
100 final ClientCookie cookie
= li
.next();
101 if (cookie
.getExpirationTime() > minExpireTime
&& cookie
.match(url
)) {
106 throw new ClientLoginException("This system's clock appears to be set incorrectly. "+
107 "Check the system time and set it to the correct time " +
108 "before trying again.");
112 protected void doHandleSendErrors(int status
, URL url
, HttpURLConnection conn
,
113 BufferedReader connReader
) throws IOException
{
115 authenticate(url
, null);
116 } else if (status
== 403) {
117 System
.out
.println(constructHttpErrorMessage(conn
, connReader
));
118 authenticate(url
, null);
119 } else if (status
>= 500 && status
<= 600) {
120 } else if (status
== 302) {
121 Map
<String
, List
<String
>> headers
= conn
.getHeaderFields();
122 String location
= headers
.get("Location").get(0);
123 if (location
.startsWith("https://www.google.com/accounts/ServiceLogin")) {
124 authenticate(url
, null);
125 } else if (location
.matches("https://www.google.com/a/[a-z0-9.-]+/ServiceLogin.*")) {
126 authenticate(url
, "HOSTED");
132 protected void doPostConnect(String method
, HttpURLConnection conn
, DataPoster data
)
134 cookies
.readCookies(conn
);
135 if (options
.getUsePersistedCredentials()) {
141 protected void doPreConnect(String method
, HttpURLConnection conn
, DataPoster data
) {
142 cookies
.writeCookies(conn
);
145 private void getAuthCookie(String token
) throws IOException
{
146 Map
<String
, String
> params
= new HashMap
<String
, String
>();
147 params
.put("continue", "http://localhost/");
148 params
.put("auth", token
);
149 String query
= buildQuery(params
);
150 URL url
= buildURL("/_ah/login?" + query
);
151 HttpURLConnection conn
= (HttpURLConnection
) url
.openConnection();
152 if (options
.getHost() != null) {
153 conn
.setRequestProperty("Host", options
.getHost());
156 IOException ioe
= connect(POST
, conn
, null);
157 if (conn
.getResponseCode() != 302
158 || !"http://localhost/".equals(conn
.getHeaderField("Location"))) {
159 throw new RuntimeException("Bad authentication response: " + conn
.getResponseCode() + " "
160 + conn
.getResponseMessage());
161 } else if (ioe
!= null) {
167 private String
getAuthToken(String host
, String accountType
) throws IOException
,
168 ClientLoginException
{
169 if (accountType
== null) {
170 if (host
.endsWith(".google.com")) {
171 accountType
= "HOSTED_OR_GOOGLE";
172 } else if (options
.getHost() != null && options
.getHost().endsWith(".google.com")) {
173 accountType
= "HOSTED_OR_GOOGLE";
175 accountType
= "GOOGLE";
178 URL url
= new URL("https://www.google.com/accounts/ClientLogin");
179 HttpURLConnection conn
= (HttpURLConnection
) url
.openConnection();
181 String password
= options
.getPasswordPrompt().getPassword();
182 Map
<String
, String
> params
= new HashMap
<String
, String
>();
183 params
.put("Email", options
.getUserId());
184 params
.put("Passwd", password
);
185 params
.put("service", "ah");
186 params
.put("source", "Google-appcfg-java-unknown");
187 params
.put("accountType", accountType
);
188 IOException ioe
= connect(POST
, conn
, new StringPoster(buildQuery(params
)));
190 BufferedReader reader
= getReader(conn
);
191 HashMap
<String
, String
> response
= new HashMap
<String
, String
>();
193 while ((line
= reader
.readLine()) != null) {
194 String
[] pair
= line
.split("=", 2);
195 if (pair
.length
== 2) {
196 response
.put(pair
[0], pair
[1]);
200 if (conn
.getResponseCode() == 200) {
201 return response
.get("Auth");
202 } else if (conn
.getResponseCode() == 403) {
203 String reason
= response
.get("Error");
204 if ("BadAuthentication".equals(reason
)) {
205 String info
= response
.get("Info");
206 if ("InvalidSecondFactor".equals(info
)) {
207 throw new ClientLoginException(""
208 + "Use an application-specific password instead of your regular account password. "
209 + "See http://www.google.com/support/accounts/answer/185833",
213 throw new ClientLoginException("Email \"" + options
.getUserId()
214 + "\" and password do not match.", ioe
);
216 } else if ("CaptchaRequired".equals(reason
)) {
217 throw new ClientLoginException("Please go to "
218 + "https://www.google.com/accounts/DisplayUnlockCaptcha "
219 + "and verify you are a human. Then try again.");
220 } else if ("NotVerified".equals(reason
)) {
221 throw new ClientLoginException("Your account has not yet been "
222 + "verfied. Please check your email to do that, then try again.");
223 } else if ("TermsNotAgreed".equals(reason
)) {
224 throw new ClientLoginException("You have not yet agreed to the "
225 + "Terms of Service on your account. Please do that, then try again.");
226 } else if ("AccountDeleted".equals(reason
)) {
227 throw new ClientLoginException("Your user account has been deleted."
228 + " If this is an error, contact account support at "
229 + "http://www.google.com/support/accounts/");
230 } else if ("AccountDisabled".equals(reason
)) {
231 throw new ClientLoginException("Your user account has been disabled."
232 + " If this is an error, contact account support at "
233 + "http://www.google.com/support/accounts/");
234 } else if ("ServiceUnavailable".equals(reason
)) {
235 throw new ClientLoginException("The service is currently "
236 + "unavailable; try again later.");
238 throw new ClientLoginException(response
.get("Error"), ioe
);
239 } else if (conn
.getResponseCode() == 401) {
240 throw new ClientAuthFailException("Email \"" + options
.getUserId()
241 + "\" and password do not match.", ioe
);
243 throw new RuntimeException("Bad authentication response: " + conn
.getResponseCode() + " "
244 + conn
.getResponseMessage(), ioe
);
248 public void saveCookies() throws IOException
{
249 if (options
.getUserId() == null) {
252 ByteArrayOutputStream out
= new ByteArrayOutputStream();
253 new ObjectOutputStream(out
).writeObject(cookies
);
254 byte[] bytes
= out
.toByteArray();
255 Preferences prefs
= Preferences
.userNodeForPackage(ServerConnection
.class);
256 prefs
.put("email", options
.getUserId());
257 prefs
.putByteArray("cookies", bytes
);