--- /dev/null
+/*\r
+ * manage HTTP access\r
+ *\r
+ * Copyright(c) 2008 olyutorskii\r
+ * $Id: ServerAccess.java 993 2010-03-14 11:55:46Z olyutorskii $\r
+ */\r
+\r
+package jp.sourceforge.jindolf;\r
+\r
+import java.awt.image.BufferedImage;\r
+import java.io.IOException;\r
+import java.io.InputStream;\r
+import java.io.OutputStream;\r
+import java.io.UnsupportedEncodingException;\r
+import java.lang.ref.SoftReference;\r
+import java.net.HttpURLConnection;\r
+import java.net.MalformedURLException;\r
+import java.net.Proxy;\r
+import java.net.URL;\r
+import java.net.URLEncoder;\r
+import java.nio.charset.Charset;\r
+import java.util.Collections;\r
+import java.util.HashMap;\r
+import java.util.Map;\r
+import javax.imageio.ImageIO;\r
+import jp.sourceforge.jindolf.parser.ContentBuilder;\r
+import jp.sourceforge.jindolf.parser.ContentBuilderSJ;\r
+import jp.sourceforge.jindolf.parser.ContentBuilderUCS2;\r
+import jp.sourceforge.jindolf.parser.DecodeException;\r
+import jp.sourceforge.jindolf.parser.DecodedContent;\r
+import jp.sourceforge.jindolf.parser.SjisDecoder;\r
+import jp.sourceforge.jindolf.parser.StreamDecoder;\r
+\r
+/**\r
+ * 国ごとの人狼BBSサーバとの通信を一手に引き受ける。\r
+ */\r
+public class ServerAccess{\r
+\r
+ private static final String USER_AGENT = HttpUtils.getUserAgentName();\r
+ private static final String JINRO_CGI = "./index.rb";\r
+ private static final\r
+ Map<String, SoftReference<BufferedImage>> IMAGE_CACHE;\r
+\r
+ static{\r
+ Map<String, SoftReference<BufferedImage>> cache =\r
+ new HashMap<String, SoftReference<BufferedImage>>();\r
+ IMAGE_CACHE = Collections.synchronizedMap(cache);\r
+ }\r
+\r
+ /**\r
+ * 画像キャッシュを検索する。\r
+ * @param key キー\r
+ * @return キャッシュされた画像。キャッシュされていなければnull。\r
+ */\r
+ private static BufferedImage getImageCache(String key){\r
+ if(key == null) return null;\r
+\r
+ BufferedImage image;\r
+\r
+ synchronized(IMAGE_CACHE){\r
+ SoftReference<BufferedImage> ref = IMAGE_CACHE.get(key);\r
+ if(ref == null) return null;\r
+\r
+ Object referent = ref.get();\r
+ if(referent == null){\r
+ IMAGE_CACHE.remove(key);\r
+ return null;\r
+ }\r
+\r
+ image = (BufferedImage) referent;\r
+ }\r
+\r
+ return image;\r
+ }\r
+\r
+ /**\r
+ * 画像キャッシュに登録する。\r
+ * @param key キー\r
+ * @param image キャッシュしたい画像。\r
+ */\r
+ private static void putImageCache(String key, BufferedImage image){\r
+ if(key == null || image == null) return;\r
+\r
+ synchronized(IMAGE_CACHE){\r
+ if(getImageCache(key) != null) return;\r
+ SoftReference<BufferedImage> ref =\r
+ new SoftReference<BufferedImage>(image);\r
+ IMAGE_CACHE.put(key, ref);\r
+ }\r
+\r
+ return;\r
+ }\r
+\r
+ /**\r
+ * 与えられた文字列に対し「application/x-www-form-urlencoded」符号化を行う。\r
+ * この符号化はHTTPのPOSTメソッドで必要になる。\r
+ * この処理は、一般的なPC用Webブラウザにおける、\r
+ * Shift_JISで書かれたHTML文書のFORMタグに伴う\r
+ * submit処理を模倣する。\r
+ * @param formData 元の文字列\r
+ * @return 符号化された文字列\r
+ */\r
+ public static String formEncode(String formData){\r
+ if(formData == null){\r
+ return null;\r
+ }\r
+ String result;\r
+ try{\r
+ result = URLEncoder.encode(formData, "US-ASCII");\r
+ }catch(UnsupportedEncodingException e){\r
+ assert false;\r
+ result = null;\r
+ }\r
+ return result;\r
+ }\r
+\r
+ /**\r
+ * 配列版formEncode。\r
+ * @param formData 元の文字列\r
+ * @return 符号化された文字列\r
+ */\r
+ public static String formEncode(char[] formData){\r
+ return formEncode(new String(formData));\r
+ }\r
+\r
+ private final URL baseURL;\r
+ private final Charset charset;\r
+ private Proxy proxy = Proxy.NO_PROXY;\r
+ private long lastServerMs;\r
+ private long lastLocalMs;\r
+ private long lastSystemMs;\r
+ private AccountCookie cookieAuth = null;\r
+ private String encodedUserID = null;\r
+\r
+ /**\r
+ * 人狼BBSサーバとの接続管理を生成する。\r
+ * この時点ではまだ通信は行われない。\r
+ * @param baseURL 国別のベースURL\r
+ * @param charset 国のCharset\r
+ */\r
+ public ServerAccess(URL baseURL, Charset charset){\r
+ this.baseURL = baseURL;\r
+ this.charset = charset;\r
+ return;\r
+ }\r
+\r
+ /**\r
+ * HTTP-Proxyを返す。\r
+ * @return HTTP-Proxy\r
+ */\r
+ public Proxy getProxy(){\r
+ return this.proxy;\r
+ }\r
+\r
+ /**\r
+ * HTTP-Proxyを設定する。\r
+ * @param proxy HTTP-Proxy。nullならProxyなしと解釈される。\r
+ */\r
+ public void setProxy(Proxy proxy){\r
+ if(proxy == null) this.proxy = Proxy.NO_PROXY;\r
+ else this.proxy = proxy;\r
+ return;\r
+ }\r
+\r
+ /**\r
+ * 国のベースURLを返す。\r
+ * @return ベースURL\r
+ */\r
+ public URL getBaseURL(){\r
+ return this.baseURL;\r
+ }\r
+\r
+ /**\r
+ * 与えられたクエリーとCGIのURLから新たにURLを合成する。\r
+ * @param query クエリー\r
+ * @return 新たなURL\r
+ */\r
+ protected URL getQueryURL(String query){\r
+ if(query.length() >= 1 && query.charAt(0) != '?'){\r
+ return null;\r
+ }\r
+\r
+ URL result;\r
+ try{\r
+ result = new URL(getBaseURL(), JINRO_CGI + query);\r
+ }catch(MalformedURLException e){\r
+ assert false;\r
+ return null;\r
+ }\r
+ return result;\r
+ }\r
+\r
+ /**\r
+ * 「Shift_JIS」でエンコーディングされた入力ストリームから文字列を生成する。\r
+ * @param istream 入力ストリーム\r
+ * @return 文字列\r
+ * @throws java.io.IOException 入出力エラー(おそらくネットワーク関連)\r
+ */\r
+ public DecodedContent downloadHTMLStream(InputStream istream)\r
+ throws IOException{\r
+ StreamDecoder decoder;\r
+ ContentBuilder builder;\r
+ if(this.charset.name().equalsIgnoreCase("Shift_JIS")){\r
+ decoder = new SjisDecoder();\r
+ builder = new ContentBuilderSJ(200 * 1024);\r
+ }else if(this.charset.name().equalsIgnoreCase("UTF-8")){\r
+ decoder = new StreamDecoder(this.charset.newDecoder());\r
+ builder = new ContentBuilderUCS2(200 * 1024);\r
+ }else{\r
+ assert false;\r
+ return null;\r
+ }\r
+ decoder.setDecodeHandler(builder);\r
+\r
+ // TODO デコーダをインスタンス変数にできないか。\r
+ // TODO DecodedContentのキャッシュ管理。\r
+\r
+ try{\r
+ decoder.decode(istream);\r
+ }catch(DecodeException e){\r
+ return null;\r
+ }\r
+\r
+ return builder.getContent();\r
+ }\r
+\r
+ /**\r
+ * 与えられたクエリーを用いてHTMLデータを取得する。\r
+ * @param query HTTP-GET クエリー\r
+ * @return HTMLデータ\r
+ * @throws java.io.IOException ネットワークエラー\r
+ */\r
+ protected HtmlSequence downloadHTML(String query)\r
+ throws IOException{\r
+ URL url = getQueryURL(query);\r
+ HtmlSequence result = downloadHTML(url);\r
+ return result;\r
+ }\r
+\r
+ /**\r
+ * 与えられたURLを用いてHTMLデータを取得する。\r
+ * @param url URL\r
+ * @return HTMLデータ\r
+ * @throws java.io.IOException ネットワークエラー\r
+ */\r
+ protected HtmlSequence downloadHTML(URL url)\r
+ throws IOException{\r
+ HttpURLConnection connection =\r
+ (HttpURLConnection) url.openConnection(this.proxy);\r
+ connection.setRequestProperty("Accept", "*/*");\r
+ connection.setRequestProperty("User-Agent", USER_AGENT);\r
+ connection.setUseCaches(false);\r
+ connection.setInstanceFollowRedirects(false);\r
+ connection.setDoInput(true);\r
+ connection.setRequestMethod("GET");\r
+\r
+ AccountCookie cookie = this.cookieAuth;\r
+ if(cookie != null){\r
+ if(shouldAccept(url, cookie)){\r
+ connection.setRequestProperty(\r
+ "Cookie",\r
+ "login=" + cookie.getLoginData());\r
+ }else{\r
+ clearAuthentication();\r
+ }\r
+ }\r
+\r
+ connection.connect();\r
+\r
+ long datems = updateLastAccess(connection);\r
+\r
+ int responseCode = connection.getResponseCode();\r
+ if(responseCode != HttpURLConnection.HTTP_OK){ // 200\r
+ String logMessage = "発言のダウンロードに失敗しました。";\r
+ logMessage += HttpUtils.formatHttpStat(connection, 0, 0);\r
+ Jindolf.logger().warn(logMessage);\r
+ return null;\r
+ }\r
+\r
+ String cs = HttpUtils.getHTMLCharset(connection);\r
+ if(!cs.equalsIgnoreCase(this.charset.name())){\r
+ return null;\r
+ }\r
+\r
+ InputStream stream = TallyInputStream.getInputStream(connection);\r
+ DecodedContent html = downloadHTMLStream(stream);\r
+\r
+ stream.close();\r
+ connection.disconnect();\r
+\r
+ HtmlSequence hseq = new HtmlSequence(url, datems, html);\r
+\r
+ return hseq;\r
+ }\r
+\r
+ /**\r
+ * 絶対または相対URLの指すパーマネントなイメージ画像をダウンロードする。\r
+ * @param url 画像URL文字列\r
+ * @return 画像イメージ\r
+ * @throws java.io.IOException ネットワークエラー\r
+ */\r
+ public BufferedImage downloadImage(String url) throws IOException{\r
+ URL absolute;\r
+ try{\r
+ URL base = getBaseURL();\r
+ absolute = new URL(base, url);\r
+ }catch(MalformedURLException e){\r
+ assert false;\r
+ return null;\r
+ }\r
+\r
+ BufferedImage image;\r
+ image = getImageCache(absolute.toString());\r
+ if(image != null) return image;\r
+\r
+ HttpURLConnection connection =\r
+ (HttpURLConnection) absolute.openConnection(this.proxy);\r
+ connection.setRequestProperty("Accept", "*/*");\r
+ connection.setRequestProperty("User-Agent", USER_AGENT);\r
+ connection.setUseCaches(true);\r
+ connection.setInstanceFollowRedirects(true);\r
+ connection.setDoInput(true);\r
+ connection.setRequestMethod("GET");\r
+\r
+ connection.connect();\r
+\r
+ int responseCode = connection.getResponseCode();\r
+ if(responseCode != HttpURLConnection.HTTP_OK){\r
+ String logMessage = "イメージのダウンロードに失敗しました。";\r
+ logMessage += HttpUtils.formatHttpStat(connection, 0, 0);\r
+ Jindolf.logger().warn(logMessage);\r
+ return null;\r
+ }\r
+\r
+ InputStream stream = TallyInputStream.getInputStream(connection);\r
+ image = ImageIO.read(stream);\r
+ stream.close();\r
+\r
+ connection.disconnect();\r
+\r
+ putImageCache(absolute.toString(), image);\r
+\r
+ return image;\r
+ }\r
+\r
+ /**\r
+ * 指定された認証情報をPOSTする。\r
+ * @param authData 認証情報\r
+ * @return 認証情報が受け入れられたらtrue\r
+ * @throws java.io.IOException ネットワークエラー\r
+ */\r
+ protected boolean postAuthData(String authData) throws IOException{\r
+ URL url = getQueryURL("");\r
+ HttpURLConnection connection =\r
+ (HttpURLConnection) url.openConnection(this.proxy);\r
+ connection.setRequestProperty("Accept", "*/*");\r
+ connection.setRequestProperty("User-Agent", USER_AGENT);\r
+ connection.setUseCaches(false);\r
+ connection.setInstanceFollowRedirects(false);\r
+ connection.setDoInput(true);\r
+ connection.setDoOutput(true);\r
+ connection.setRequestMethod("POST");\r
+\r
+ byte[] authBytes = authData.getBytes();\r
+\r
+ OutputStream os = TallyOutputStream.getOutputStream(connection);\r
+ os.write(authBytes);\r
+ os.flush();\r
+ os.close();\r
+\r
+ updateLastAccess(connection);\r
+\r
+ int responseCode = connection.getResponseCode();\r
+ if(responseCode != HttpURLConnection.HTTP_MOVED_TEMP){ // 302\r
+ String logMessage = "認証情報の送信に失敗しました。";\r
+ Jindolf.logger().warn(logMessage);\r
+ connection.disconnect();\r
+ return false;\r
+ }\r
+\r
+ connection.disconnect();\r
+\r
+ AccountCookie loginCookie = AccountCookie.createCookie(connection);\r
+ if(loginCookie == null){\r
+ return false;\r
+ }\r
+\r
+ setAuthentication(loginCookie);\r
+\r
+ Jindolf.logger().info("正しく認証が行われました。");\r
+\r
+ return true;\r
+ }\r
+\r
+ /**\r
+ * トップページのHTMLデータを取得する。\r
+ * @return HTMLデータ\r
+ * @throws java.io.IOException ネットワークエラー\r
+ */\r
+ public HtmlSequence getHTMLTopPage() throws IOException{\r
+ return downloadHTML("");\r
+ }\r
+\r
+ /**\r
+ * 国に含まれる村一覧HTMLデータを取得する。\r
+ * @return HTMLデータ\r
+ * @throws java.io.IOException ネットワークエラー\r
+ */\r
+ public HtmlSequence getHTMLLandList() throws IOException{\r
+ return downloadHTML("?cmd=log");\r
+ }\r
+\r
+ /**\r
+ * 指定された村のPeriod一覧のHTMLデータを取得する。\r
+ * 現在ゲーム進行中の村にも可能。\r
+ * ※ 古国では使えないよ!\r
+ * @param village 村\r
+ * @return HTMLデータ\r
+ * @throws java.io.IOException ネットワークエラー\r
+ */\r
+ public HtmlSequence getHTMLBoneHead(Village village) throws IOException{\r
+ String villageID = village.getVillageID();\r
+ return downloadHTML("?vid=" + villageID + "&meslog=");\r
+ }\r
+\r
+ /**\r
+ * 指定された村の最新PeriodのHTMLデータをロードする。\r
+ * 既にGAMEOVERの村ではPeriod一覧のHTMLデータとなる。\r
+ * @param village 村\r
+ * @return HTMLデータ\r
+ * @throws java.io.IOException ネットワークエラー\r
+ */\r
+ public HtmlSequence getHTMLVillage(Village village) throws IOException{\r
+ URL url = getVillageURL(village);\r
+ return downloadHTML(url);\r
+ }\r
+\r
+ /**\r
+ * 指定された村の最新PeriodのHTMLデータのURLを取得する。\r
+ * @param village 村\r
+ * @return URL\r
+ */\r
+ public URL getVillageURL(Village village){\r
+ String villageID = village.getVillageID();\r
+ URL url = getQueryURL("?vid=" + villageID);\r
+ return url;\r
+ }\r
+\r
+ /**\r
+ * 指定されたPeriodのHTMLデータをロードする。\r
+ * @param period Period\r
+ * @return HTMLデータ\r
+ * @throws java.io.IOException ネットワークエラー\r
+ */\r
+ public HtmlSequence getHTMLPeriod(Period period) throws IOException{\r
+ URL url = getPeriodURL(period);\r
+ return downloadHTML(url);\r
+ }\r
+\r
+ /**\r
+ * 指定されたPeriodのHTMLデータのURLを取得する。\r
+ * @param period 日\r
+ * @return URL\r
+ */\r
+ public URL getPeriodURL(Period period){\r
+ String query = period.getCGIQuery();\r
+ URL url = getQueryURL(query);\r
+ return url;\r
+ }\r
+\r
+ /**\r
+ * 最終アクセス時刻を更新する。\r
+ * @param connection HTTP接続\r
+ * @return リソース送信時刻\r
+ */\r
+ public long updateLastAccess(HttpURLConnection connection){\r
+ this.lastServerMs = connection.getDate();\r
+ this.lastLocalMs = System.currentTimeMillis();\r
+ this.lastSystemMs = System.nanoTime() / (1000 * 1000);\r
+ return this.lastServerMs;\r
+ }\r
+\r
+ /**\r
+ * 指定したURLに対しCookieを送っても良いか否か判定する。\r
+ * 判別材料は Cookie の寿命とパス指定のみ。\r
+ * @param url URL\r
+ * @param cookie Cookie\r
+ * @return 送ってもよければtrue\r
+ */\r
+ private static boolean shouldAccept(URL url, AccountCookie cookie){\r
+ if(cookie.hasExpired()){\r
+ return false;\r
+ }\r
+\r
+ String urlPath = url.getPath();\r
+ String cookiePath = cookie.getPathURI().getPath();\r
+\r
+ if( ! urlPath.startsWith(cookiePath) ){\r
+ return false;\r
+ }\r
+\r
+ return true;\r
+ }\r
+\r
+ /**\r
+ * 現在ログイン中か否か判別する。\r
+ * @return ログイン中ならtrue\r
+ */\r
+ // TODO interval call\r
+ public boolean hasLoggedIn(){\r
+ AccountCookie cookie = this.cookieAuth;\r
+ if(cookie == null){\r
+ return false;\r
+ }\r
+ if(cookie.hasExpired()){\r
+ clearAuthentication();\r
+ return false;\r
+ }\r
+ return true;\r
+ }\r
+\r
+ /**\r
+ * 与えられたユーザIDとパスワードでログイン処理を行う。\r
+ * @param userID ユーザID\r
+ * @param password パスワード\r
+ * @return ログインに成功すればtrue\r
+ * @throws java.io.IOException ネットワークエラー\r
+ */\r
+ public final boolean login(String userID, char[] password)\r
+ throws IOException{\r
+ if(hasLoggedIn()){\r
+ return true;\r
+ }\r
+\r
+ String id = formEncode(userID);\r
+ if(id == null || id.length() <= 0){\r
+ return false;\r
+ }\r
+\r
+ String pw = formEncode(password);\r
+ if(pw == null || pw.length() <= 0){\r
+ return false;\r
+ }\r
+\r
+ this.encodedUserID = id;\r
+\r
+ String redirect = formEncode("&#bottom"); // TODO ほんとに必要?\r
+\r
+ StringBuilder postData = new StringBuilder();\r
+ postData.append("cmd=login");\r
+ postData.append('&').append("cgi_param=").append(redirect);\r
+ postData.append('&').append("user_id=").append(id);\r
+ postData.append('&').append("password=").append(pw);\r
+\r
+ boolean result;\r
+ try{\r
+ result = postAuthData(postData.toString());\r
+ }catch(IOException e){\r
+ clearAuthentication();\r
+ throw e;\r
+ }\r
+\r
+ return result;\r
+ }\r
+\r
+ /**\r
+ * ログアウト処理を行う。\r
+ * @throws java.io.IOException ネットワーク入出力エラー\r
+ */\r
+ // TODO シャットダウンフックでログアウトさせようかな…\r
+ public void logout() throws IOException{\r
+ if(!hasLoggedIn()){\r
+ return;\r
+ }\r
+ if(this.encodedUserID == null){\r
+ clearAuthentication();\r
+ return;\r
+ }\r
+\r
+ String redirect = formEncode("&#bottom"); // TODO 必要?\r
+\r
+ StringBuilder postData = new StringBuilder();\r
+ postData.append("cmd=logout");\r
+ postData.append('&').append("cgi_param=").append(redirect);\r
+ postData.append('&').append("user_id=").append(this.encodedUserID);\r
+\r
+ try{\r
+ postAuthData(postData.toString());\r
+ }finally{\r
+ clearAuthentication();\r
+ }\r
+\r
+ return;\r
+ }\r
+\r
+ /**\r
+ * 認証情報クリア。\r
+ */\r
+ // TODO タイマーでExpire date の時刻にクリアしたい。\r
+ protected void clearAuthentication(){\r
+ this.cookieAuth = null;\r
+ this.encodedUserID = null;\r
+ return;\r
+ }\r
+\r
+ /**\r
+ * 認証情報のセット。\r
+ * @param cookie 認証Cookie\r
+ */\r
+ private void setAuthentication(AccountCookie cookie){\r
+ this.cookieAuth = cookie;\r
+ return;\r
+ }\r
+\r
+ // TODO JRE1.6対応するときに HttpCookie, CookieManager 利用へ移行したい。\r
+\r
+}\r