Skip to content

Instantly share code, notes, and snippets.

@yukihane
Forked from edvakf/LoginUnifier.java
Created August 18, 2011 09:14
Show Gist options
  • Save yukihane/1153709 to your computer and use it in GitHub Desktop.
Save yukihane/1153709 to your computer and use it in GitHub Desktop.
package extensions;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import dareka.common.Logger;
import dareka.extensions.Extension2;
import dareka.extensions.ExtensionManager;
import dareka.extensions.RequestFilter;
import dareka.processor.HttpHeader;
import dareka.processor.HttpRequestHeader;
import dareka.processor.HttpResponseHeader;
import dareka.processor.URLResource;
/**
* NicoCache_nl経由でのアクセス時, ニコニコ動画サービスセッションを共有します.
* デフォルトでは全アクセスのセッションを1つにまとめて共有しますが,
* セッション共有単位をbrowserのuser-agentで分類することもできます.
* 設定方法は次の通りです.
* <p>
* [設定]<br/>
* config.propertiesファイルに<br/>
* loginUnifier.session.[n]=[user-agent],[user-agent]<br/>
* の書式で設定します。<br/>
* [n]は0から始まる連番を指定します。<br/>
* [user-agent]にはブラウザのユーザエージェント文字列の一部を指定します。<br/>
* カンマは区切り文字として使用しているのでuser-agent文字列の一部としては用いることができません。<br/>
* 大文字小文字は区別されるので正確に記入してください。<br/>
* </p>
* <p>
* [設定例]<br/>
* loginUnifier.session.0=Chrome,MSIE<br/>
* loginUnifier.session.1=Firefox<br/>
* </p>
* <p>
* 以上のようにconfig.propertiesへ設定を行うと、<br/>
* Google ChromeとIEでログイン状態を共有し、<br/>
* それとは別にFirefoxでのログイン状態を保持します。<br/>
* そして、それ以外のブラウザは上記2セッションとは別のセッション管理が行われます。<br/>
* (つまり、上記のように2種類の設定を行う必要があるのは3アカウントを同時に使用したい場合になります)<br/>
* </p>
*/
public class LoginUnifier
implements Extension2, RequestFilter {
// Processor Interface
private static final Pattern PROCESSOR_SUPPORTED_PATTERN = Pattern.compile(
"^http://(?!ext\\.)(?:[^/]+\\.)?nicovideo\\.jp/\\S*$");
private final Session defaultSession = new Session();
private final List<String[]> sessiontypes = new ArrayList<String[]>();
private final List<Session> sessions = new ArrayList<Session>();
/** デバッグ用に情報を記録する. 本質的な処理ではない. */
private final Log log = new Log();
public LoginUnifier() {
for(int i=0;;i++){
final String primes = System.getProperty("loginUnifier.session." + i);
if (primes == null) {
break;
} else {
final String[] splitted = primes.split(",");
final String[] trimmed = new String[splitted.length];
for (int j = 0; j < splitted.length; j++) {
trimmed[j] = splitted[j].trim();
}
sessiontypes.add(trimmed);
sessions.add(new Session());
// ログへ情報出力
Logger.info("Browser(s) using LoginUnifier-session " + i + ":");
for (String s : trimmed) {
Logger.info(s);
}
}
}
}
/**
* {@inheritDoc}
* Extensionのバージョン
*/
public String getVersionString() {
return "LoginUnifier(mod;multi-session) ver.3.1";
}
/**
* ExtensionをNicoCache_nl本体に登録する
*/
public void registerExtensions(ExtensionManager mgr) {
// 必要な行のみコメントを外す
//mgr.registerProcessor(this); // Processorとして登録
//mgr.registerRewriter(this); // Rewriterとして登録
mgr.registerRequestFilter(this); // RequestFilterとして登録
}
/**
* {@inheritDoc}
* RequestFilter interface
* stores and rewrites Cookie header
*/
public int onRequest(HttpRequestHeader requestHeader)
throws IOException {
if (requestHeader == null) { // failed to receive header
return RequestFilter.DROP;
}
final Matcher m = PROCESSOR_SUPPORTED_PATTERN.matcher(requestHeader.getURI());
if (!m.matches()) {
return RequestFilter.OK; // just pass
}
applySession(requestHeader);
return RequestFilter.OK;
}
/**
* リクエストヘッダのユーザエージェントと事前に設定したブラウザのタイプを突き合わせ、ブラウザのタイプごとのセッションを適用します。
* @param requestHeader リクエストヘッダ.
* @throws IOException
*/
private void applySession(HttpRequestHeader requestHeader) throws IOException {
final String userAgent = requestHeader.getMessageHeader("User-Agent");
boolean applied = false;
if (userAgent != null && !userAgent.isEmpty()) {
for (int i = 0; i < sessiontypes.size(); i++) {
final String[] type = sessiontypes.get(i);
for (String pattern : type) {
if (userAgent.contains(pattern)) {
log.logUsingSession(userAgent, i);
final Session session = sessions.get(i);
session.apply(requestHeader);
applied = true;
break;
}
}
if (applied) {
break;
}
}
}
if (!applied) {
log.logUsingSession(userAgent);
defaultSession.apply(requestHeader);
applied = true;
}
}
private static class Log {
/** 接続したことがあるブラウザ(のユーザエージェント文字列). */
private final Set<String> connectedBrowsers = new HashSet<String>();
private void logUsingSession(final String userAgent, int session) {
Logger.debug("browser type: " + session);
if (!connectedBrowsers.contains(userAgent)) {
connectedBrowsers.add(userAgent);
Logger.info("[LoginUnifier]browser connected: " + userAgent);
if (session >= 0) {
Logger.info("[LoginUnifier]provided: session-" + session);
} else {
Logger.info("[LoginUnifier]provided: default session");
}
}
}
private void logUsingSession(final String userAgent) {
logUsingSession(userAgent, -1);
}
}
private static class Session {
// number of expired user_session to store
private static final int expiredSessionsMax = 100;
private static final String SESSION_COOKIE = "user_session";
void apply(HttpRequestHeader requestHeader) throws IOException {
final Map<String, String> cookie = mapCookie(requestHeader.getMessageHeader("cookie"));
final String uaSession = cookie.get(SESSION_COOKIE);
final String newSession = getValidSession(requestHeader, uaSession);
setValidSession(newSession);
if (newSession == null) {
cookie.remove(SESSION_COOKIE);
} else {
cookie.put(SESSION_COOKIE, newSession);
}
if (cookie.isEmpty()) {
requestHeader.removeMessageHeader("cookie");
} else {
requestHeader.setMessageHeader("cookie", demapCookie(cookie));
}
}
private static final Pattern COOKIE_PATTERN = Pattern.compile("([^=; ]*)=([^; ]*)");
/**
* Cookie header filed "hoge=fuga; foo=bar;" to TreeMap
*/
public static Map<String, String> mapCookie(String cookie) {
final Map<String, String> map = new TreeMap<String, String>();
if (cookie == null) {
return map;
}
final Matcher m = COOKIE_PATTERN.matcher(cookie);
while (m.find()) {
map.put(m.group(1), m.group(2));
}
return map;
}
/**
* TreeMap back to Cookie field String
*/
public static String demapCookie(Map<String, String> map) {
final StringBuilder cookie = new StringBuilder();
boolean first = true;
for (String key : map.keySet()) {
if (first) {
first = false;
} else {
cookie.append("; ");
}
cookie.append(key).append("=").append(map.get(key));
}
return cookie.toString();
}
/**
* ニコニコ動画サービスへアクセスし有効なセッションかどうかを試します.
* @param header 試験に用いるヘッダ.
* @return 有効なセッションであればtrue.
*/
private static boolean challengeAuth(HttpRequestHeader header) throws IOException {
URLResource urlResource = new URLResource("http://www.nicovideo.jp/");
HttpRequestHeader tmpRequestHeader = new HttpRequestHeader(header.toString());
tmpRequestHeader.setMethod(HttpHeader.HEAD);
tmpRequestHeader.setURI("http://www.nicovideo.jp/");
tmpRequestHeader.setContentLength(0);
tmpRequestHeader.setMessageHeader("host", "www.nicovideo.jp");
HttpResponseHeader tmpResponseHeader = urlResource.getResponseHeader(null, tmpRequestHeader);
final String authflag = tmpResponseHeader.getMessageHeader("x-niconico-authflag");
if (authflag == null || // something went wrong (should not happen)
authflag.equals("0")) { // expired
return false;
} else { // 1 = normal user, 3 = premium user
return true;
}
}
private String getValidSession(final HttpRequestHeader requestHeader, final String uaSession)
throws IOException {
final String url = requestHeader.getURI();
//Logger.debug("LoginUnifier: " + url);
String newSession = guessValidSession(uaSession);
if (newSession == null && uaSession != null) {
// とりあえず UA のセッションでリクエストし、
// 切れてたら保存してたセッションを使って再度リクエスト
Logger.debug("LoginUnifier: HEAD request to check validity of " + uaSession);
boolean authflag = challengeAuth(requestHeader);
Logger.debug("LoginUnifier: authflag = " + authflag);
if (!authflag) { // expired
enqueHistory(uaSession);
newSession = getValidSession();
} else { // 1 = normal user, 3 = premium user
newSession = uaSession;
}
}
final StringBuilder log = new StringBuilder();
log.append("LoginUnifier: " + url + "\n");
log.append("\tUA session = " + uaSession + "\n");
log.append("\tstored session = " + getValidSession() + "\n");
log.append("\tNew session = " + newSession);
Logger.debug(log.toString());
return newSession;
}
/**
* LoginUnifierが知る限り有効なセッション.
* (あずかり知らぬところで変更されている場合もあるので必ず正しいとは限らない).
*/
private String validSession = null;
/**
* 今までLoginUnifierが扱ってきたセッションの履歴一覧.
* この中に入っているもののうちの1つがvalidSession
* (validSessionがnullの場合はこの限りではないが).
* expiredSessionsMax を超えると古いものから削除される.
*/
private final List<String> sessionHistory = new LinkedList<String>();
/**
* 有効なセッションを、管理している情報を基に推測します.
* @param s クライアントが通知してきたセッション.
* @return 有効であると思われるセッション. 判断できなければnull.
*/
synchronized String guessValidSession(String s) {
// 確からしいSessionが存在しており、なおかつ
// 引数で渡されたセッションも無効/既知であるなら、
// LoginUnifierが管理している情報は正しいであろう.
if (validSession != null) {
if (isInvalid(s)) {
return validSession;
} else {
// validSessionと引数で渡されたセッション、どちらが有効かわからない
return null;
}
}
// validSession が null なので渡されたセッションが有効だと言うしかないじゃない
// (本当はsがinvalidであれば有効なセッションは無いと判断できるが、
// 後続処理的には違いが無いのでsを返すことにする)
return s;
}
/**
* LoginUnifierに有効なセッションを登録します.
* @param s 登録するセッション. nullは有効なセッションは無くなったことを意味します.
*/
synchronized void setValidSession(String s) {
validSession = s;
enqueHistory(s);
}
/**
* LoginUnifierが管理する範囲内での有効なセッションを取得します.
*/
synchronized String getValidSession() {
return validSession;
}
private boolean isInvalid(String s) {
return (s == null || containsHistory(s));
}
synchronized void enqueHistory(String s) {
if (s == null) {
return;
}
// 大抵は最後に入っているのでその場合スキップ
if (!sessionHistory.isEmpty() && sessionHistory.get(sessionHistory.size() - 1).equals(s)) {
return;
}
sessionHistory.add(s);
while (sessionHistory.size() > expiredSessionsMax) {
sessionHistory.remove(0);
}
}
private boolean removeHistory(String s) {
return sessionHistory.remove(s);
}
private boolean containsHistory(String s) {
return sessionHistory.contains(s);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment