Skip to content

Instantly share code, notes, and snippets.

@shin1ogawa
Created March 28, 2011 08:08
Show Gist options
  • Save shin1ogawa/890144 to your computer and use it in GitHub Desktop.
Save shin1ogawa/890144 to your computer and use it in GitHub Desktop.
package appengine.util;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
/**
* @author shin1ogawa
*/
public interface Auth {
/**
* @param connection
* @param body
* @throws Exception
*/
void setup(HttpURLConnection connection, byte[] body) throws Exception;
/**
* Default implementation using ClientLogin.
* @author shin1ogawa
* @see <a href="http://code.google.com/apis/accounts/docs/AuthForInstalledApps.html">ClientLogin for Installed Applications</a>
*/
public static class ClientLogin implements Auth {
final String cookieName;
final String cookieValue;
/**
* the constructor.
* @param cookieValue
* @param useSsl
* @category constructor
*/
public ClientLogin(String cookieValue, boolean useSsl) {
this.cookieValue = cookieValue;
this.cookieName = useSsl ? "SACSID" : "ACSID";
}
@Override
public void setup(HttpURLConnection connection, byte[] body) {
}
}
/**
* Default implementaion usin OAuth.
* @author shin1ogawa
* @see <a href="http://code.google.com/apis/accounts/docs/OAuthForInstalledApps.html">OAuth for Installed Applications</a>
*/
public static class OAuth implements Auth {
final String consumerKey;
final String consumerSecret;
final String accessToken;
final String tokenSecret;
/**
* the constructor.
* @param consumerKey
* @param consumerSecret
* @param accessToken
* @param tokenSecret
* @category constructor
*/
public OAuth(String consumerKey, String consumerSecret, String accessToken,
String tokenSecret) {
super();
this.consumerKey = consumerKey;
this.consumerSecret = consumerSecret;
this.accessToken = accessToken;
this.tokenSecret = tokenSecret;
}
@Override
public void setup(HttpURLConnection connection, byte[] body) throws InvalidKeyException,
UnsupportedEncodingException, NoSuchAlgorithmException {
String headerValue =
OAuthUtil.getAuthorizationHeaderValue(connection.getRequestMethod(), connection
.getURL().toString(), null, consumerKey, consumerSecret, accessToken,
tokenSecret);
connection.setRequestProperty("Authorization", headerValue);
}
}
}
package appengine.util;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Utilities for Google Client Login api.
* @author shin1ogawa
*/
public class ClientLoginUtil {
static final Logger logger = Logger.getLogger(ClientLoginUtil.class.getName());
static final String CLIENTLOGIN_URL = "https://www.google.com/accounts/ClientLogin";
/**
* @param email
* @param password
* @param applicationName
* {yourname}-{applicationName}-{versionNumber}
* @param applicationUrl url of your application
* e.g. {@literal http://shin1ogawa.appspot.com/}
* @param continueUrl url of needs authenticate
* e.g. {@literal http://shin1ogawa.appspot.com/pathToNeedsAuthenticate}
* @return cookie value {@literal ACSID=....} or {@literal SACSID=....}
* @throws IOException
*/
public static String getCookie(String email, String password, String applicationName,
String applicationUrl, String continueUrl) throws IOException {
String authToken = getAuthTokenUsingGoogleClientLogin(email, password, applicationName);
return getCookieUsingAppengineLogin(applicationUrl, continueUrl, authToken);
}
static String getAuthTokenUsingGoogleClientLogin(String email, String password,
String applicationName) throws IOException {
URL url = new URL(CLIENTLOGIN_URL);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.setDoOutput(true);
connection.setRequestProperty("Content-type", "application/x-www-form-urlencoded");
PrintWriter w = new PrintWriter(new OutputStreamWriter(connection.getOutputStream()));
w.print("Email=" + URLEncoder.encode(email, "utf-8"));
w.print("&Passwd=" + URLEncoder.encode(password, "utf-8"));
w.print("&service=" + URLEncoder.encode("ah", "utf-8"));
w.print("&source=" + URLEncoder.encode(applicationName, "utf-8"));
w.print("&accountType=" + URLEncoder.encode("HOSTED_OR_GOOGLE", "utf-8"));
w.flush();
w.close();
connection.connect();
BufferedReader reader =
new BufferedReader(new InputStreamReader(connection.getInputStream()));
String line = null;
while ((line = reader.readLine()) != null) {
if (line.startsWith("Auth") == false) {
continue;
}
return line.substring(line.indexOf('=') + 1);
}
throw new AssertionError();
}
static String getCookieUsingAppengineLogin(String applicationUrl, String continueUrl,
String authToken) throws IOException {
StringBuilder _url =
new StringBuilder(applicationUrl).append(applicationUrl.endsWith("/") ? "" : "/")
.append("_ah/login?").append("continue=")
.append(URLEncoder.encode(continueUrl, "utf-8")).append("&").append("auth=")
.append(URLEncoder.encode(authToken, "utf-8"));
if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE, "GET " + _url);
}
URL url = new URL(_url.toString());
boolean followRedirects = HttpURLConnection.getFollowRedirects();
HttpURLConnection.setFollowRedirects(false);
try {
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestProperty("X-appcfg-api-version", "dummy");
connection.connect();
if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE, "responseCode=" + connection.getResponseCode());
}
Map<String, List<String>> headers = connection.getHeaderFields();
Iterator<Map.Entry<String, List<String>>> i = headers.entrySet().iterator();
while (i.hasNext()) {
Map.Entry<String, List<String>> next = i.next();
String key = next.getKey();
if (key == null || key.equals("Set-Cookie") == false) {
continue;
}
List<String> values = next.getValue();
if (values.isEmpty() == false) {
return values.get(0);
} else {
return null;
}
}
return null;
} finally {
HttpURLConnection.setFollowRedirects(followRedirects);
}
}
}
package appengine.servlet;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.util.Date;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.google.apphosting.api.ApiProxy;
/**
* リクエストされたbyte配列を{@link ApiProxy#makeSyncCall(String, String, byte[])}へ引き渡し、
* その結果をレスポンスするだけのServlet.
* <p>
* 特殊なサーブレットなので、web.xmlにてセキュリティを設定しておくのがおすすめ。
* </p>
* <div>
* <ul>
* <li>HttpHeaderに以下を設定する。
* <ul>
* <li>serviceName</li>
* <li>methodName</li>
* <li>applicationId</li>
* </ul>
* </li>
* <li>payloadにProtocolBufferで出力されたbyte配列を設定して{code POST}する。</li>
* <li>{@literal application/octet-stream}で
* {@link ApiProxy#makeSyncCall(String, String, byte[])}の結果を返すので、クライアント側でよしなに。</li>
* </ul>
* </div>
*
* @author shin1ogawa
*/
public class MakeSyncCallServlet extends HttpServlet {
private static final long serialVersionUID = -7389134447989025199L;
private static final String APPLICATION_ID = "applicationId";
private static final String METHOD_NAME = "methodName";
private static final String SERVICE_NAME = "serviceName";
private static Logger logger = Logger.getLogger(MakeSyncCallServlet.class.getName());
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setContentType("text/html");
resp.setCharacterEncoding("utf-8");
resp.getWriter().println("<html><head><title>makeSyncCallServlet</title></head><body>");
resp.getWriter().println("<p>makeSyncCallServlet.</p>");
resp.getWriter().println("<p>" + new Date(System.currentTimeMillis()) + "</p>");
resp.getWriter().println("</body></html>");
resp.getWriter().flush();
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String serviceName = req.getHeader(SERVICE_NAME);
String methodName = req.getHeader(METHOD_NAME);
String applicationId = req.getHeader(APPLICATION_ID);
logger.fine(applicationId + ":" + serviceName + "#" + methodName);
if (validateParameters(resp, serviceName, methodName, applicationId) == false) {
return;
}
byte[] requestBytes = toByteArray(req.getInputStream());
logger.fine(applicationId + ":" + serviceName + "#" + methodName + ": requestBytes.length="
+ (requestBytes != null ? requestBytes.length : "null"));
byte[] responseBytes = null;
try {
@SuppressWarnings("unchecked")
byte[] bytes =
ApiProxy.getDelegate().makeSyncCall(ApiProxy.getCurrentEnvironment(),
serviceName, methodName, requestBytes);
responseBytes = bytes;
} catch (Throwable th) {
logger.log(Level.WARNING, applicationId + ":" + serviceName + "#" + methodName, th);
resp.setStatus(HttpURLConnection.HTTP_INTERNAL_ERROR);
resp.setContentType("text/plain");
th.printStackTrace(resp.getWriter());
resp.getWriter().flush();
return;
}
if (responseBytes == null) {
logger.fine(serviceName + "#" + methodName + ": responseBytes == null.");
responseBytes = new byte[0];
} else {
logger.fine(serviceName + "#" + methodName + ": responseBytes.length="
+ responseBytes.length);
}
resp.setContentType("application/octet-stream");
resp.getOutputStream().write(responseBytes);
resp.getOutputStream().flush();
}
private boolean validateParameters(HttpServletResponse resp, String serviceName,
String methodName, String applicationId) throws IOException {
if (serviceName == null || serviceName.length() == 0) {
onErrorInParameters(resp, "serviceName was not specified.");
return false;
}
if (methodName == null || methodName.length() == 0) {
onErrorInParameters(resp, "methodName was not specified.");
return false;
}
if (applicationId == null || applicationId.length() == 0) {
onErrorInParameters(resp, "applicationId was not specified.");
return false;
}
// 念のためサーバ環境のApplicationIdと同じかどうか確認する。
String serverApplicationId = ApiProxy.getCurrentEnvironment().getAppId();
if (serverApplicationId.equals(applicationId) == false) {
onErrorInParameters(resp, "applicationId was not equals to " + serverApplicationId
+ ".");
return false;
}
return true;
}
private void onErrorInParameters(HttpServletResponse resp, String message) throws IOException {
logger.warning(message);
resp.setStatus(HttpURLConnection.HTTP_INTERNAL_ERROR);
resp.setContentType("text/plain");
resp.getWriter().println(message);
resp.getWriter().flush();
}
static byte[] toByteArray(InputStream is) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
long count = 0;
int n = 0;
while (-1 != (n = is.read(buffer))) {
bos.write(buffer, 0, n);
count += n;
}
return bos.toByteArray();
}
}
package appengine.util;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.concurrent.Future;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.http.HttpServletResponse;
import com.google.apphosting.api.ApiProxy;
import com.google.apphosting.api.ApiProxy.ApiConfig;
import com.google.apphosting.api.ApiProxy.ApiProxyException;
import com.google.apphosting.api.ApiProxy.Environment;
import com.google.apphosting.api.ApiProxy.LogRecord;
/**
* MakeSyncCallを行うサーブレットへ処理を委譲する{@link ApiProxy.Delegate}の実装。
* @author shin1ogawa
*/
public class MakeSyncCallServletDelegate implements ApiProxy.Delegate<Environment> {
static final Logger logger = Logger.getLogger(MakeSyncCallServletDelegate.class.getName());
@SuppressWarnings("unchecked")
final ApiProxy.Delegate<Environment> parent = ApiProxy.getDelegate();
String applicationId;
URL servletUrl;
Auth auth;
/**
* the constructor.
* @param applicationId your application id
* @param servletUrl url to makeSyncCallServlet
* @param auth {@link Auth}. it doesnt authenticate if {@code null}.
* @category constructor
*/
public MakeSyncCallServletDelegate(String applicationId, URL servletUrl, Auth auth) {
this.auth = auth;
}
@Override
public void log(Environment environment, LogRecord logRecord) {
getOriginal().log(environment, logRecord);
}
@Override
public byte[] makeSyncCall(Environment environment, String serviceName, String methodName,
byte[] request) throws ApiProxyException {
try {
return protocolBufferOnHttp(environment, serviceName, methodName, request);
} catch (Exception e) {
throw new MakeSyncCallServletFailureException(e);
}
}
@Override
public Future<byte[]> makeAsyncCall(Environment arg0, String arg1, String arg2, byte[] arg3,
ApiConfig arg4) {
throw new UnsupportedOperationException();
}
private byte[] protocolBufferOnHttp(Environment environment, String serviceName,
String methodName, final byte[] request) throws Exception {
HttpURLConnection connection = (HttpURLConnection) servletUrl.openConnection();
connection.setRequestMethod("POST");
connection.setDoOutput(true);
connection.setRequestProperty("Content-Type", "application/octet-stream");
connection.setRequestProperty("Content-Length", String.valueOf(request.length));
connection.setRequestProperty("serviceName", serviceName);
connection.setRequestProperty("methodName", methodName);
connection.setRequestProperty("applicationId", environment.getAppId());
if (auth != null) {
auth.setup(connection, request);
}
OutputStream os = connection.getOutputStream();
os.write(request);
os.flush();
try {
connection.connect();
int statusCode = connection.getResponseCode();
if (statusCode != HttpServletResponse.SC_OK) {
if (connection.getResponseMessage() != null) {
logger.log(Level.WARNING, statusCode + ": " + connection.getResponseMessage());
throw new MakeSyncCallServletFailureException(statusCode + ": "
+ connection.getResponseMessage());
}
}
InputStream is = connection.getInputStream();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int n = 0;
while (-1 != (n = is.read(buffer))) {
bos.write(buffer, 0, n);
}
return bos.toByteArray();
} catch (IOException e) {
throw new MakeSyncCallServletFailureException(e);
}
}
/**
* @return 元々ApiProxyに設定されていた{@link ApiProxy.Delegate}のインスタンス
*/
public ApiProxy.Delegate<Environment> getOriginal() {
return parent;
}
/**
* {@link MakeSyncCallServletDelegate#makeSyncCall(Environment, String, String, byte[])}
* を実行中に{@link Exception}が発生した場合にそれをラップして投げ直す{@link RuntimeException}.
* @author shin1ogawa
*/
public static class MakeSyncCallServletFailureException extends RuntimeException {
private static final long serialVersionUID = -6963701246227196182L;
/**
* the constructor.
* @param message
* @category constructor
*/
public MakeSyncCallServletFailureException(String message) {
super(message);
}
/**
* the constructor.
* @param cause
* @category constructor
*/
public MakeSyncCallServletFailureException(Throwable cause) {
super(cause);
}
}
}
package appengine.util;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
/**
* utilities for oauth of google app engine.
* @author shin1ogawa
*/
public class OAuthUtil {
static final String ENC = "utf-8";
static final String HMAC_SHA1_ALGORITHM = "HmacSHA1";
/**
* @param method
* @param requestUrlBase
* @param parameters
* @param consumerKey
* @param consumerSecret
* @param accessToken
* @param accessTokenSecret
* @return value for Authorization Header
* @throws UnsupportedEncodingException
* @throws InvalidKeyException
* @throws NoSuchAlgorithmException
* @author shin1ogawa
*/
public static String getAuthorizationHeaderValue(String method, String requestUrlBase,
Map<String, String> parameters, String consumerKey, String consumerSecret,
String accessToken, String accessTokenSecret) throws UnsupportedEncodingException,
InvalidKeyException, NoSuchAlgorithmException {
Map<String, String> _params = new LinkedHashMap<String, String>();
if (parameters != null && parameters.isEmpty() == false) {
_params.putAll(parameters);
}
_params.put("oauth_consumer_key", consumerKey);
_params.put("oauth_nonce", generateNonce());
_params.put("oauth_signature_method", "HMAC-SHA1");
_params.put("oauth_timestamp", generateTimestamp());
_params.put("oauth_token", accessToken);
_params.put("oauth_version", "1.0");
String baseString = generateSignatureBaseString(method, requestUrlBase, _params);
String key =
URLEncoder.encode(consumerKey, "utf-8") + "&"
+ URLEncoder.encode(accessTokenSecret, "utf-8");
String signature = URLEncoder.encode(generateHmacSha1Signature(key, baseString), ENC);
StringBuilder b = new StringBuilder("OAuth ");
if (_params.containsKey("oauth_body_hash")) {
b.append("oauth_body_hash").append("=\"").append(_params.get("oauth_body_hash"))
.append("\",");
}
b.append("oauth_consumer_key").append("=\"")
.append(URLEncoder.encode(consumerKey, "utf-8")).append("\"");
b.append(",oauth_token").append("=\"").append(URLEncoder.encode(accessToken, "utf-8"))
.append("\"");
b.append(",oauth_version").append("=\"").append("1.0").append("\"");
b.append(",oauth_timestamp").append("=\"").append(_params.get("oauth_timestamp"))
.append("\"");
b.append(",oauth_nonce").append("=\"").append(_params.get("oauth_nonce")).append("\"");
b.append(",oauth_signature_method").append("=\"").append("HMAC-SHA1").append("\"");
b.append(",oauth_signature").append("=\"").append(signature).append("\"");
return b.toString();
}
static String generateSignatureBaseString(String method, String requestUrlBase,
Map<String, String> params) throws UnsupportedEncodingException {
return escapeAndJoinStrings(Arrays.asList(method, requestUrlBase,
escapeAndJoinParameters(params)));
}
static String escapeAndJoinParameters(Map<String, String> params)
throws UnsupportedEncodingException {
StringBuilder b = new StringBuilder();
boolean first = true;
for (String key : sortedKeyList(params)) {
if (first == false) {
b.append('&');
} else {
first = false;
}
b.append(key).append('=')
.append(URLEncoder.encode(params.get(key), ENC).replace("+", "%20"));
}
return b.toString();
}
static List<String> sortedKeyList(Map<String, String> map) {
List<String> list = new ArrayList<String>(map.keySet());
Collections.sort(list);
return list;
}
static String escapeAndJoinStrings(Iterable<String> params) throws UnsupportedEncodingException {
StringBuilder b = new StringBuilder();
boolean first = true;
for (String s : params) {
if (first == false) {
b.append('&');
} else {
first = false;
}
b.append(URLEncoder.encode(s, ENC));
}
return b.toString();
}
static String generateHmacSha1Signature(String key, String data)
throws NoSuchAlgorithmException, InvalidKeyException, IllegalStateException,
UnsupportedEncodingException {
SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(ENC), HMAC_SHA1_ALGORITHM);
Mac mac = Mac.getInstance(HMAC_SHA1_ALGORITHM);
mac.init(signingKey);
return byteArrayToBase64String(mac.doFinal(data.getBytes(ENC)));
}
static String generateTimestamp() {
return Long.toString(System.currentTimeMillis() / 1000L);
}
static String generateNonce() {
return Long.toString(new Random().nextLong());
}
static String byteArrayToBase64String(byte[] bytes) {
int bytesLength = bytes.length;
int fullGroups = bytesLength / 3;
int partialGroup = bytesLength - 3 * fullGroups;
int resultLength = 4 * ((bytesLength + 2) / 3);
StringBuilder b = new StringBuilder(resultLength);
int cursor = 0;
for (int i = 0; i < fullGroups; i++) {
int byte0 = bytes[cursor++] & 0xff;
int byte1 = bytes[cursor++] & 0xff;
int byte2 = bytes[cursor++] & 0xff;
b.append(intToBase64[byte0 >> 2]);
b.append(intToBase64[(byte0 << 4) & 0x3f | (byte1 >> 4)]);
b.append(intToBase64[(byte1 << 2) & 0x3f | (byte2 >> 6)]);
b.append(intToBase64[byte2 & 0x3f]);
}
if (partialGroup != 0) {
int byte0 = bytes[cursor++] & 0xff;
b.append(intToBase64[byte0 >> 2]);
if (partialGroup == 1) {
b.append(intToBase64[(byte0 << 4) & 0x3f]);
b.append("==");
} else {
int byte1 = bytes[cursor++] & 0xff;
b.append(intToBase64[(byte0 << 4) & 0x3f | (byte1 >> 4)]);
b.append(intToBase64[(byte1 << 2) & 0x3f]);
b.append('=');
}
}
return b.toString();
}
static final char intToBase64[] = {
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z',
'a',
'b',
'c',
'd',
'e',
'f',
'g',
'h',
'i',
'j',
'k',
'l',
'm',
'n',
'o',
'p',
'q',
'r',
's',
't',
'u',
'v',
'w',
'x',
'y',
'z',
'0',
'1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9',
'+',
'/'
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment