概要

appengine/java で提供しているサービス向けのガジェットを用意するにあたり署名付きリクエストを使ってユーザー(viwer)を特定することをしたかったのだが、その署名を検証する方法がなかなか見つからずに苦労した。
ようやく有効な方法を発見できたのでここにまとめてみたい。

署名付きリクエスト

gadgets.io.makeRequest()で認証タイプをSIGNEDに指定するとOpenSocialコンテナ側(GoogleSites)がapp_id,app_url,owner_id,viewer_idを付加してサーバーにプロキシーしてくれる。
しかし、サーバー側ではそれが本当にガジェット(コンテナ)から送信されたものなのかどうか検証できなければならない。
そのしくみとしてOAuth Signatureを使った署名検証の方式が用意されている。(opensocialの仕様)

署名付きリクエストの検証方法には、

  1. コンテナの証明書を使う方法(RSA-SHA1方式)
  2. あらかじめConsumerKey,ConsumerSecretをコンテナに登録しておく方法(HMAC-SHA1方式)

の2通りがあるようだった。
mixiなどは前者の方式が使われているようでこちらの方式のほうが一般的(?)なのかもしれないが、
googleの場合はこの方式で使う証明書の有効期限が2009年で切れており、他に有効なドキュメントも見つからず完全に放置プレイっぽい匂いを感じていたので別の方法を探した。

ちなみにこの方式について書かれている mixiのデベロッパーガイドは 今回の実装にあたりかなり参考になった。

そこでもうひとつのHMAC-SHA1方式で実装する方法を以下にまとめてみる。

コンテナに登録

Googleの場合は以下のページでガジェットのURLを登録し、発行されたConsumerKey,ConsumerSecretを使用して検証を行う。

リクエストを検証するコードサンプル(java)

ここからjava用のコードをとってきて使用する。(net.oauth.*)


OAuthServiceProvider provider = new OAuthServiceProvider(null, null, null);
OAuthConsumer consumer = new OAuthConsumer(null, CONSUMER_KEY, CONSUMER_SECRET,	provider);
OAuthValidator validator = new SimpleOAuthValidator();
OAuthAccessor accessor = new OAuthAccessor(consumer);
accessor.tokenSecret = ""; // nullだとまずいかもしれないので・・

まず、OAuth関連のクラスをnewする。今回は署名検証するだけなので不要なパラメータはnullでOK。


OAuthMessage message = new OAuthMessage(
    request.getMethod(), 
    parseRequestUrl(request).toString(), 
    parseRequestParameters(request));

requestを仕様に従って正規化して、OAuthMessageのコンストラクタに渡す。
ここで使っている、parseRequestUrl, parseRequestParametersは以下のような実装となる。


	private List<OAuth.Parameter> parseRequestParameters(HttpServletRequest request)
	{
		List<OAuth.Parameter> parameters = new ArrayList<OAuth.Parameter>();
	
		for (Object e : request.getParameterMap().entrySet()) {
			@SuppressWarnings("unchecked")
			Map.Entry<String, String> entry = (Map.Entry<String, String>) e;
	
			for (String value : entry.getValue()) {
				parameters.add(new OAuth.Parameter(entry.getKey(), value));
			}
		}
	
		return parameters;
	}

	private StringBuilder parseRequestUrl(HttpServletRequest request)
	{
		StringBuilder url = new StringBuilder();
	
		String scheme = request.getScheme();
		url.append(scheme);
		url.append("://");
		url.append(request.getServerName());
		
		int port = request.getLocalPort();
		if (port == 0) {
			//nothing
		}
		else if ( (scheme.equals("http") &amp;&amp; port != 80)||(scheme.equals("https") &amp;&amp; port != 443) ) {
			url.append(":");
			url.append(port);
		}
		url.append(request.getRequestURI());
		
		return url;
	}

最後にvalidateMessageを実行する。検証エラーとなった場合はExceptionがThrowされる。


validator.validateMessage(message, accessor);

これで署名検証に関する基本的な実装はOKなはずだ。

その他の検証項目

[1]
上記SimpleOAuthValidatorの実装ではinstanceが永続化されている間のみnonceのチェックが働く仕様となっているので
appengineなど分散環境では正しくチェックはできない。
nonceのチェックは別途実装するかValidatorの仕様を修正する必要がありそうだ。

[2]
本検証によりリクエストが正しくコンテナからプロキシーされてきたものだということは保証されるが、
「自分のアプリから来たリクエストか」どうかは検証できていない。
それが必要な場合は送られてきたapp_idを検証する処理が必要となる。


Comments [appengine] GoogleSites用ガジェットの署名付きリクエストを検証する方法 はコメントを受け付けていません


レプリケーションの設定をしているDBがいつの間にかバイナリログによってディスク容量の空きが無くなっていた・・・
想像以上に早くディスク容量を消費する・・・
以下、バイナリログの削除方法と自動削除の設定方法。

http://wiki.bit-hive.com/tomizoo/pg/MySQL%20%A5%D0%A5%A4%A5%CA%A5%EA%A5%ED%A5%B0%A4%CE%BA%EF%BD%FC

Comments [mysql] MySQLのバイナリログ削除 はコメントを受け付けていません



appengineのアプリケーションログをチェックして、ERRORレベルのログがあった場合、管理者にメールで通知するプログラムを作ってみました。
Servletで作ってcronで実行します。


public class LogWatchServlet extends HttpServlet {
    
    private final static Log log = LogFactory.getLog(LogWatchServlet.class);
    
    private final static long duration = 20 * 60 * 1000; // 20min
    
    public void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
        
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        TimeZone.setDefault(TimeZone.getTimeZone("GMT+9:00"));
        
        LogQuery query = LogQuery.Builder
            .withDefaults()
            .minLogLevel(LogLevel.ERROR)
            .majorVersionIds(Arrays.asList(new String[]{SystemProperty.applicationVersion.get().split("\\.")[0]}))
            .includeAppLogs(true)
            .startTimeMillis(System.currentTimeMillis() - duration);
        
        log.debug("log watch version: " + SystemProperty.applicationVersion.get());
        
        // ログ取得
        String lastOffset = null;
        List<RequestLogs> logs = new ArrayList<RequestLogs>();
        do {
            if (lastOffset != null) {
                query.offset(lastOffset);
            }
            int count = 0;
            for (RequestLogs record : LogServiceFactory.getLogService().fetch(query)) {
                logs.add(record);
                lastOffset = record.getOffset();
                count++;
            }
            if (count == 0) {
                break;
            }
        } while(true);
        
        // ログがあったらメール送信
        if (! logs.isEmpty()) {
            List<String> logbuffer = new ArrayList<String>();
            int i=0;
            for (RequestLogs record: logs) {
                logbuffer.add(format.format(new Date(record.getEndTimeUsec()/1000)) + " " + record.getResource());
                for (AppLogLine line: record.getAppLogLines()) {
                    if (line.getLogLevel().compareTo(LogLevel.ERROR) >= 0) {
                        logbuffer.add(" " + line.getLogMessage());
                    }
                }
                if (i > 50) {
                    break;
                }
                i++;
            }
            logbuffer.add("\n\n全" + logs.size() + "件");
            
            // メール送信
            MailService mailService = MailServiceFactory.getMailService();
            String sender = "admin@" + AppConst.appId() + ".appspotmail.com";
            String subject = "[" + AppConst.appId() + "] applicaton log notification";
            MailService.Message message = new MailService.Message(sender, null, subject, StringUtils.join(logbuffer, "\n"));
            mailService.sendToAdmins(message);
        }
        
        return;
    }
    
    @Override
    public void doPost(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
        doGet(request, response);
    }

}


Comments [appengine] アプリケーションログをチェックしてメールで通知する はコメントを受け付けていません


これでいいのかしら。。


    public static Key allocateId(AsyncDatastoreService ds, String kind)
            throws NullPointerException {
        if (ds == null) {
            throw new NullPointerException("The ds parameter must not be null.");
        }
        if (kind == null) {
            throw new NullPointerException(
                "The kind parameter must not be null.");
        }
        String ns = NamespaceManager.get();
        if (ns == null) {
        	ns = "";
        }
        String cacheKey = "!"+ns+":"+kind;
        Iterator<Key> keys = keysCache.get(cacheKey);
        if (keys != null && keys.hasNext()) {
            return keys.next();
        }
        keys =
            FutureUtil
                .getQuietly(allocateIdsAsync(ds, kind, KEY_CACHE_SIZE))
                .iterator();
        keysCache.put(cacheKey, keys);
        return keys.next();
    }

Comments [appengine][slim3] DatastoreUtil#allocateIdがNamespaceに対応するように修正 はコメントを受け付けていません


listプロパティにInMemoryInCriterionを適用するとエラーになるので修正。
これでいいのかしら。。


   public boolean accept(Object model) {
        Object v = convertValueForDatastore(attributeMeta.getValue(model));
        for (Object o : value) {
        	if (v instanceof Collection<?>) {
        		for (Object v2: (Collection<?>)v) {
                    if (compareValue(v2, o) == 0) {
                        return true;
                    }
        		}
        	} else {
        		if (compareValue(v, o) == 0) {
        			return true;
        		}
        	}
        }
        return false;
    }

Comments [appengine][slim3]InMemoryInCriterionがlistプロパティでも動作するように修正 はコメントを受け付けていません