// OpenTween - Client of Twitter // Copyright (c) 2007-2011 kiri_feather (@kiri_feather) // (c) 2008-2011 Moz (@syo68k) // (c) 2008-2011 takeshik (@takeshik) // (c) 2010-2011 anis774 (@anis774) // (c) 2010-2011 fantasticswallow (@f_swallow) // (c) 2011 kim_upsilon (@kim_upsilon) // All rights reserved. // // This file is part of OpenTween. // // This program is free software; you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by the Free // Software Foundation; either version 3 of the License, or (at your option) // any later version. // // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License // for more details. // // You should have received a copy of the GNU General Public License along // with this program. If not, see , or write to // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, // Boston, MA 02110-1301, USA. using System.Collections.Specialized; using System.IO; using System.Net; using System.Net.Http; using System.Text; using System.Threading; using System; using System.Collections.Generic; using System.IO.Compression; using System.Drawing; using OpenTween.Connection; using System.Net.Cache; /// ///HttpWebRequest,HttpWebResponseを使用した基本的な通信機能を提供する /// /// ///プロキシ情報などを設定するため、使用前に静的メソッドInitializeConnectionを呼び出すこと。 ///通信方式によって必要になるHTTPヘッダの付加などは、派生クラスで行う。 /// namespace OpenTween { public class HttpConnection { /// /// キャッシュを有効にするか否か /// public bool CacheEnabled { get; set; } = true; /// /// リクエスト間で Cookie を保持するか否か /// public bool UseCookie { get; set; } /// /// クッキー保存用コンテナ /// private CookieContainer cookieContainer = new CookieContainer(); protected const string PostMethod = "POST"; protected const string GetMethod = "GET"; protected const string HeadMethod = "HEAD"; /// ///HttpWebRequestオブジェクトを取得する。パラメータはGET/HEAD/DELETEではクエリに、POST/PUTではエンティティボディに変換される。 /// /// ///追加で必要となるHTTPヘッダや通信オプションは呼び出し元で付加すること ///(Timeout,AutomaticDecompression,AllowAutoRedirect,UserAgent,ContentType,Accept,HttpRequestHeader.Authorization,カスタムヘッダ) ///POST/PUTでクエリが必要な場合は、requestUriに含めること。 /// ///HTTP通信メソッド(GET/HEAD/POST/PUT/DELETE) ///通信先URI ///GET時のクエリ、またはPOST時のエンティティボディ ///Accept-Encodingヘッダにgzipを付加するかどうかを表す真偽値 ///引数で指定された内容を反映したHttpWebRequestオブジェクト protected HttpWebRequest CreateRequest(string method, Uri requestUri, Dictionary param, bool gzip = false) { Networking.CheckInitialized(); //GETメソッドの場合はクエリとurlを結合 UriBuilder ub = new UriBuilder(requestUri.AbsoluteUri); if (param != null && (method == "GET" || method == "DELETE" || method == "HEAD")) { ub.Query = MyCommon.BuildQueryString(param); } HttpWebRequest webReq = (HttpWebRequest)WebRequest.Create(ub.Uri); webReq.ReadWriteTimeout = 90 * 1000; //Streamの読み込みは90秒でタイムアウト(デフォルト5分) //プロキシ設定 if (Networking.ProxyType != ProxyType.IE) webReq.Proxy = Networking.Proxy; if (gzip) { // Accept-Encodingヘッダを付加 webReq.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip; } webReq.Method = method; if (method == "POST" || method == "PUT") { webReq.ContentType = "application/x-www-form-urlencoded"; //POST/PUTメソッドの場合は、ボディデータとしてクエリ構成して書き込み using (StreamWriter writer = new StreamWriter(webReq.GetRequestStream())) { writer.Write(MyCommon.BuildQueryString(param)); } } //cookie設定 if (this.UseCookie) webReq.CookieContainer = this.cookieContainer; //タイムアウト設定 webReq.Timeout = this.InstanceTimeout ?? (int)Networking.DefaultTimeout.TotalMilliseconds; webReq.UserAgent = Networking.GetUserAgentString(); // KeepAlive無効なサーバー(Twitter等)に使用すると、タイムアウト後にWebExceptionが発生する場合あり webReq.KeepAlive = false; if (!this.CacheEnabled) webReq.CachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache); return webReq; } /// ///HttpWebRequestオブジェクトを取得する。multipartでのバイナリアップロード用。 /// /// ///methodにはPOST/PUTのみ指定可能 /// ///HTTP通信メソッド(POST/PUT) ///通信先URI ///form-dataで指定する名前と文字列のディクショナリ ///form-dataで指定する名前とバイナリファイル情報のリスト ///引数で指定された内容を反映したHttpWebRequestオブジェクト protected HttpWebRequest CreateRequest(string method, Uri requestUri, Dictionary param, List> binaryFileInfo) { Networking.CheckInitialized(); //methodはPOST,PUTのみ許可 UriBuilder ub = new UriBuilder(requestUri.AbsoluteUri); if (method == "GET" || method == "DELETE" || method == "HEAD") throw new ArgumentException("Method must be POST or PUT", nameof(method)); if ((param == null || param.Count == 0) && (binaryFileInfo == null || binaryFileInfo.Count == 0)) throw new ArgumentException("Data is empty"); HttpWebRequest webReq = (HttpWebRequest)WebRequest.Create(ub.Uri); //プロキシ設定 if (Networking.ProxyType != ProxyType.IE) webReq.Proxy = Networking.Proxy; webReq.Method = method; if (method == "POST" || method == "PUT") { string boundary = System.Environment.TickCount.ToString(); webReq.ContentType = "multipart/form-data; boundary=" + boundary; using (Stream reqStream = webReq.GetRequestStream()) { //POST送信する文字データを作成 if (param != null) { string postData = ""; foreach (KeyValuePair kvp in param) { postData += "--" + boundary + "\r\n" + "Content-Disposition: form-data; name=\"" + kvp.Key + "\"" + "\r\n\r\n" + kvp.Value + "\r\n"; } byte[] postBytes = Encoding.UTF8.GetBytes(postData); reqStream.Write(postBytes, 0, postBytes.Length); } //POST送信するバイナリデータを作成 if (binaryFileInfo != null) { foreach (KeyValuePair kvp in binaryFileInfo) { string postData = ""; byte[] crlfByte = Encoding.UTF8.GetBytes("\r\n"); //コンテンツタイプの指定 string mime = ""; switch (kvp.Value.Extension.ToLowerInvariant()) { case ".jpg": case ".jpeg": case ".jpe": mime = "image/jpeg"; break; case ".gif": mime = "image/gif"; break; case ".png": mime = "image/png"; break; case ".tiff": case ".tif": mime = "image/tiff"; break; case ".bmp": mime = "image/x-bmp"; break; case ".avi": mime = "video/avi"; break; case ".wmv": mime = "video/x-ms-wmv"; break; case ".flv": mime = "video/x-flv"; break; case ".m4v": mime = "video/x-m4v"; break; case ".mov": mime = "video/quicktime"; break; case ".mp4": mime = "video/3gpp"; break; case ".rm": mime = "application/vnd.rn-realmedia"; break; case ".mpeg": case ".mpg": mime = "video/mpeg"; break; case ".3gp": mime = "movie/3gp"; break; case ".3g2": mime = "video/3gpp2"; break; default: mime = "application/octet-stream\r\nContent-Transfer-Encoding: binary"; break; } postData = "--" + boundary + "\r\n" + "Content-Disposition: form-data; name=\"" + kvp.Key + "\"; filename=\"" + kvp.Value.Name + "\"\r\n" + "Content-Type: " + mime + "\r\n\r\n"; byte[] postBytes = Encoding.UTF8.GetBytes(postData); reqStream.Write(postBytes, 0, postBytes.Length); //ファイルを読み出してHTTPのストリームに書き込み kvp.Value.CopyTo(reqStream); reqStream.Write(crlfByte, 0, crlfByte.Length); } } //終端 byte[] endBytes = Encoding.UTF8.GetBytes("--" + boundary + "--\r\n"); reqStream.Write(endBytes, 0, endBytes.Length); } } //cookie設定 if (this.UseCookie) webReq.CookieContainer = this.cookieContainer; //タイムアウト設定 webReq.Timeout = this.InstanceTimeout ?? (int)Networking.DefaultTimeout.TotalMilliseconds; // KeepAlive無効なサーバー(Twitter等)に使用すると、タイムアウト後にWebExceptionが発生する場合あり webReq.KeepAlive = false; return webReq; } /// ///HTTPの応答を処理し、引数で指定されたストリームに書き込み /// /// ///リダイレクト応答の場合(AllowAutoRedirect=Falseの場合のみ)は、headerInfoインスタンスがあればLocationを追加してリダイレクト先を返却 ///WebExceptionはハンドルしていないので、呼び出し元でキャッチすること ///gzipファイルのダウンロードを想定しているため、他形式の場合は伸張時に問題が発生する可能性があります。 /// ///HTTP通信リクエストオブジェクト ///[OUT]HTTP応答のボディストリームのコピー先 ///[IN/OUT]HTTP応答のヘッダ情報。ヘッダ名をキーにして空データのコレクションを渡すことで、該当ヘッダの値をデータに設定して戻す ///HTTP応答のステータスコード protected HttpStatusCode GetResponse(HttpWebRequest webRequest, Stream contentStream, Dictionary headerInfo) { try { using (HttpWebResponse webRes = (HttpWebResponse)webRequest.GetResponse()) { HttpStatusCode statusCode = webRes.StatusCode; //cookie保持 if (this.UseCookie) this.FixCookies(webRes.Cookies); //リダイレクト応答の場合は、リダイレクト先を設定 GetHeaderInfo(webRes, headerInfo); //応答のストリームをコピーして戻す if (webRes.ContentLength > 0) { //gzipなら応答ストリームの内容は伸張済み。それ以外なら伸張する。 if (webRes.ContentEncoding == "gzip" || webRes.ContentEncoding == "deflate") { using (Stream stream = webRes.GetResponseStream()) { stream?.CopyTo(contentStream); } } else { using (Stream stream = new GZipStream(webRes.GetResponseStream(), CompressionMode.Decompress)) { stream?.CopyTo(contentStream); } } } return statusCode; } } catch (WebException ex) { if (ex.Status == WebExceptionStatus.ProtocolError) { HttpWebResponse res = (HttpWebResponse)ex.Response; GetHeaderInfo(res, headerInfo); return res.StatusCode; } throw; } } /// ///HTTPの応答を処理し、応答ボディデータをテキストとして返却する /// /// ///リダイレクト応答の場合(AllowAutoRedirect=Falseの場合のみ)は、headerInfoインスタンスがあればLocationを追加してリダイレクト先を返却 ///WebExceptionはハンドルしていないので、呼び出し元でキャッチすること ///テキストの文字コードはUTF-8を前提として、エンコードはしていません /// ///HTTP通信リクエストオブジェクト ///[OUT]HTTP応答のボディデータ ///[IN/OUT]HTTP応答のヘッダ情報。ヘッダ名をキーにして空データのコレクションを渡すことで、該当ヘッダの値をデータに設定して戻す ///HTTP応答のステータスコード protected HttpStatusCode GetResponse(HttpWebRequest webRequest, out string contentText, Dictionary headerInfo) { try { using (HttpWebResponse webRes = (HttpWebResponse)webRequest.GetResponse()) { HttpStatusCode statusCode = webRes.StatusCode; //cookie保持 if (this.UseCookie) this.FixCookies(webRes.Cookies); //リダイレクト応答の場合は、リダイレクト先を設定 GetHeaderInfo(webRes, headerInfo); //応答のストリームをテキストに書き出し using (StreamReader sr = new StreamReader(webRes.GetResponseStream())) { contentText = sr.ReadToEnd(); } return statusCode; } } catch (WebException ex) { if (ex.Status == WebExceptionStatus.ProtocolError) { HttpWebResponse res = (HttpWebResponse)ex.Response; GetHeaderInfo(res, headerInfo); using (StreamReader sr = new StreamReader(res.GetResponseStream())) { contentText = sr.ReadToEnd(); } return res.StatusCode; } throw; } } /// ///HTTPの応答を処理します。応答ボディデータが不要な用途向け。 /// /// ///リダイレクト応答の場合(AllowAutoRedirect=Falseの場合のみ)は、headerInfoインスタンスがあればLocationを追加してリダイレクト先を返却 ///WebExceptionはハンドルしていないので、呼び出し元でキャッチすること /// ///HTTP通信リクエストオブジェクト ///[IN/OUT]HTTP応答のヘッダ情報。ヘッダ名をキーにして空データのコレクションを渡すことで、該当ヘッダの値をデータに設定して戻す ///HTTP応答のステータスコード protected HttpStatusCode GetResponse(HttpWebRequest webRequest, Dictionary headerInfo) { try { using (HttpWebResponse webRes = (HttpWebResponse)webRequest.GetResponse()) { HttpStatusCode statusCode = webRes.StatusCode; //cookie保持 if (this.UseCookie) this.FixCookies(webRes.Cookies); //リダイレクト応答の場合は、リダイレクト先を設定 GetHeaderInfo(webRes, headerInfo); return statusCode; } } catch (WebException ex) { if (ex.Status == WebExceptionStatus.ProtocolError) { HttpWebResponse res = (HttpWebResponse)ex.Response; GetHeaderInfo(res, headerInfo); return res.StatusCode; } throw; } } /// ///HTTPの応答を処理し、応答ボディデータをBitmapとして返却します /// /// ///リダイレクト応答の場合(AllowAutoRedirect=Falseの場合のみ)は、headerInfoインスタンスがあればLocationを追加してリダイレクト先を返却 ///WebExceptionはハンドルしていないので、呼び出し元でキャッチすること /// ///HTTP通信リクエストオブジェクト ///[OUT]HTTP応答のボディデータを書き込むBitmap ///[IN/OUT]HTTP応答のヘッダ情報。ヘッダ名をキーにして空データのコレクションを渡すことで、該当ヘッダの値をデータに設定して戻す ///HTTP応答のステータスコード protected HttpStatusCode GetResponse(HttpWebRequest webRequest, out Bitmap contentBitmap, Dictionary headerInfo) { try { using (HttpWebResponse webRes = (HttpWebResponse)webRequest.GetResponse()) { HttpStatusCode statusCode = webRes.StatusCode; //cookie保持 if (this.UseCookie) this.FixCookies(webRes.Cookies); //リダイレクト応答の場合は、リダイレクト先を設定 GetHeaderInfo(webRes, headerInfo); //応答のストリームをBitmapにして戻す //if (webRes.ContentLength > 0) contentBitmap = new Bitmap(webRes.GetResponseStream()); contentBitmap = new Bitmap(webRes.GetResponseStream()); return statusCode; } } catch (WebException ex) { if (ex.Status == WebExceptionStatus.ProtocolError) { HttpWebResponse res = (HttpWebResponse)ex.Response; GetHeaderInfo(res, headerInfo); contentBitmap = null; return res.StatusCode; } throw; } } /// /// ホスト名なしのドメインはドメイン名から先頭のドットを除去しないと再利用されないため修正して追加する /// private void FixCookies(CookieCollection cookieCollection) { foreach (Cookie ck in cookieCollection) { if (ck.Domain.StartsWith(".", StringComparison.Ordinal)) { ck.Domain = ck.Domain.Substring(1); cookieContainer.Add(ck); } } } /// ///headerInfoのキー情報で指定されたHTTPヘッダ情報を取得・格納する。redirect応答時はLocationヘッダの内容を追記する /// ///HTTP応答 ///[IN/OUT]キーにヘッダ名を指定したデータ空のコレクション。取得した値をデータにセットして戻す private void GetHeaderInfo(HttpWebResponse webResponse, Dictionary headerInfo) { if (headerInfo == null) return; if (headerInfo.Count > 0) { var headers = webResponse.Headers; var dictKeys = new string[headerInfo.Count]; headerInfo.Keys.CopyTo(dictKeys, 0); foreach (var key in dictKeys) { var value = headers[key]; headerInfo[key] = value ?? ""; } } HttpStatusCode statusCode = webResponse.StatusCode; if (statusCode == HttpStatusCode.MovedPermanently || statusCode == HttpStatusCode.Found || statusCode == HttpStatusCode.SeeOther || statusCode == HttpStatusCode.TemporaryRedirect) { if (webResponse.Headers["Location"] != null) { headerInfo["Location"] = webResponse.Headers["Location"]; } } } /// ///クエリ形式(key1=value1&key2=value2&...)の文字列をkey-valueコレクションに詰め直し /// ///クエリ文字列 ///key-valueのコレクション protected NameValueCollection ParseQueryString(string queryString) { NameValueCollection query = new NameValueCollection(); string[] parts = queryString.Split('&'); foreach (string part in parts) { int index = part.IndexOf('='); if (index == -1) query.Add(Uri.UnescapeDataString(part), ""); else query.Add(Uri.UnescapeDataString(part.Substring(0, index)), Uri.UnescapeDataString(part.Substring(index + 1))); } return query; } #region "InstanceTimeout" /// ///通信タイムアウト時間(ms) /// private int? _timeout = null; /// ///通信タイムアウト時間(ms)。10~120秒の範囲で指定。範囲外は20秒とする /// protected int? InstanceTimeout { get { return _timeout; } set { const int TimeoutMinValue = 10000; const int TimeoutMaxValue = 120000; if (value < TimeoutMinValue || value > TimeoutMaxValue) throw new ArgumentOutOfRangeException(nameof(value), "Set " + TimeoutMinValue + "-" + TimeoutMaxValue + ": Value=" + value); else _timeout = value; } } #endregion } }