blob: dfe1cd89cf21254f2c65c8cd54a069663e8c75e6 [file] [log] [blame]
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.gct.login;
import com.google.api.client.extensions.java6.auth.oauth2.VerificationCodeReceiver;
import com.google.api.client.repackaged.com.google.common.base.Throwables;
import com.google.api.client.repackaged.org.mortbay.jetty.Connector;
import com.google.api.client.repackaged.org.mortbay.jetty.Request;
import com.google.api.client.repackaged.org.mortbay.jetty.Server;
import com.google.api.client.repackaged.org.mortbay.jetty.handler.AbstractHandler;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* A cancellable Server Receiver.
*/
class CancellableServerReceiver implements VerificationCodeReceiver {
private static final String CALLBACK_PATH = "/Callback";
/** Server or {@code null} before {@link #getRedirectUri()}. */
private Server server;
/** Verification code or {@code null} for none. */
String code;
/** Error code or {@code null} for none. */
String error;
/** Lock on the code and error. */
final Lock lock = new ReentrantLock();
/** Condition for receiving an authorization response. */
final Condition gotAuthorizationResponse = lock.newCondition();
/** Port to use or {@code -1} to select an unused port in {@link #getRedirectUri()}. */
private int port;
/** Host name to use. */
private final String host;
/**
* Constructor that starts the server on {@code "localhost"} selects an unused port.
*
* <p>
* Use {@link Builder} if you need to specify any of the optional parameters.
* </p>
*/
public CancellableServerReceiver() {
this("localhost", -1);
}
/**
* Constructor.
*
* @param host Host name to use
* @param port Port to use or {@code -1} to select an unused port
*/
CancellableServerReceiver(String host, int port) {
this.host = host;
this.port = port;
}
@Override
public String getRedirectUri() throws IOException {
if (port == -1) {
port = getUnusedPort();
}
server = new Server(port);
for (Connector c : server.getConnectors()) {
c.setHost(host);
}
server.addHandler(new CallbackHandler());
try {
server.start();
} catch (Exception e) {
Throwables.propagateIfPossible(e);
throw new IOException(e);
}
return "http://" + host + ":" + port + CALLBACK_PATH;
}
@Override
public String waitForCode() throws IOException {
lock.lock();
try {
while (code == null && error == null) {
gotAuthorizationResponse.awaitUninterruptibly();
}
if (error != null) {
throw new IOException("User authorization failed (" + error + ")");
}
return code;
} finally {
lock.unlock();
}
}
@Override
public void stop() throws IOException {
if (server != null) {
try {
server.stop();
} catch (Exception e) {
Throwables.propagateIfPossible(e);
throw new IOException(e);
}
lock.lock();
try {
error = "Request cancelled.";
code = null;
gotAuthorizationResponse.signal();
}
finally {
lock.unlock();
}
server = null;
}
}
/** Returns the host name to use. */
public String getHost() {
return host;
}
/**
* Returns the port to use or {@code -1} to select an unused port in {@link #getRedirectUri()}.
*/
public int getPort() {
return port;
}
private static int getUnusedPort() throws IOException {
Socket s = new Socket();
s.bind(null);
try {
return s.getLocalPort();
} finally {
s.close();
}
}
/**
* Builder.
*
* <p>
* Implementation is not thread-safe.
* </p>
*/
public static final class Builder {
/** Host name to use. */
private String host = "localhost";
/** Port to use or {@code -1} to select an unused port. */
private int port = -1;
/** Builds the {@link LocalServerReceiver}. */
public CancellableServerReceiver build() {
return new CancellableServerReceiver(host, port);
}
/** Returns the host name to use. */
public String getHost() {
return host;
}
/** Sets the host name to use. */
public Builder setHost(String host) {
this.host = host;
return this;
}
/** Returns the port to use or {@code -1} to select an unused port. */
public int getPort() {
return port;
}
/** Sets the port to use or {@code -1} to select an unused port. */
public Builder setPort(int port) {
this.port = port;
return this;
}
}
/**
* Jetty handler that takes the verifier token passed over from the OAuth provider and stashes it
* where {@link #waitForCode} will find it.
*/
class CallbackHandler extends AbstractHandler {
@Override
public void handle(String target, HttpServletRequest request, HttpServletResponse response, int dispatch) throws IOException {
if (!CALLBACK_PATH.equals(target)) {
return;
}
writeLandingHtml(response);
response.flushBuffer();
((Request)request).setHandled(true);
lock.lock();
try {
error = request.getParameter("error");
code = request.getParameter("code");
gotAuthorizationResponse.signal();
}
finally {
lock.unlock();
}
}
private void writeLandingHtml(HttpServletResponse response) throws IOException {
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType("text/html");
PrintWriter doc = response.getWriter();
doc.println("<html>");
doc.println("<head><title>OAuth 2.0 Authentication Token Received</title></head>");
doc.println("<body>");
doc.println("Received verification code. Closing...");
doc.println("<script type='text/javascript'>");
// We open "" in the same window to trigger JS ownership of it, which lets
// us then close it via JS, at least in Chrome.
doc.println("window.setTimeout(function() {");
doc.println(" window.open('https://developers.google.com/', '_self', '');}, 100);");
doc.println("</script>");
doc.println("</body>");
doc.println("</HTML>");
doc.flush();
}
}
}