1 // OpenTween - Client of Twitter
2 // Copyright (c) 2007-2011 kiri_feather (@kiri_feather) <kiri.feather@gmail.com>
3 // (c) 2008-2011 Moz (@syo68k)
4 // (c) 2008-2011 takeshik (@takeshik) <http://www.takeshik.org/>
5 // (c) 2010-2011 anis774 (@anis774) <http://d.hatena.ne.jp/anis774/>
6 // (c) 2010-2011 fantasticswallow (@f_swallow) <http://twitter.com/f_swallow>
7 // (c) 2011 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
8 // All rights reserved.
10 // This file is part of OpenTween.
12 // This program is free software; you can redistribute it and/or modify it
13 // under the terms of the GNU General Public License as published by the Free
14 // Software Foundation; either version 3 of the License, or (at your option)
17 // This program is distributed in the hope that it will be useful, but
18 // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
19 // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
22 // You should have received a copy of the GNU General Public License along
23 // with this program. If not, see <http://www.gnu.org/licenses/>, or write to
24 // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
25 // Boston, MA 02110-1301, USA.
27 using System.Collections.Specialized;
30 using System.Net.Http;
32 using System.Threading;
34 using System.Collections.Generic;
35 using System.IO.Compression;
39 ///HttpWebRequest,HttpWebResponseを使用した基本的な通信機能を提供する
42 ///プロキシ情報などを設定するため、使用前に静的メソッドInitializeConnectionを呼び出すこと。
43 ///通信方式によって必要になるHTTPヘッダの付加などは、派生クラスで行う。
47 public class HttpConnection
52 private static IWebProxy proxy = null;
57 private static ProxyType proxyKind = ProxyType.IE;
62 private static bool isInitialize = false;
72 /// リクエスト間で Cookie を保持するか否か
74 public bool UseCookie { get; set; }
79 private CookieContainer cookieContainer = new CookieContainer();
81 protected const string PostMethod = "POST";
82 protected const string GetMethod = "GET";
83 protected const string HeadMethod = "HEAD";
86 ///HttpWebRequestオブジェクトを取得する。パラメータはGET/HEAD/DELETEではクエリに、POST/PUTではエンティティボディに変換される。
89 ///追加で必要となるHTTPヘッダや通信オプションは呼び出し元で付加すること
90 ///(Timeout,AutomaticDecompression,AllowAutoRedirect,UserAgent,ContentType,Accept,HttpRequestHeader.Authorization,カスタムヘッダ)
91 ///POST/PUTでクエリが必要な場合は、requestUriに含めること。
93 ///<param name="method">HTTP通信メソッド(GET/HEAD/POST/PUT/DELETE)</param>
94 ///<param name="requestUri">通信先URI</param>
95 ///<param name="param">GET時のクエリ、またはPOST時のエンティティボディ</param>
96 ///<returns>引数で指定された内容を反映したHttpWebRequestオブジェクト</returns>
97 protected HttpWebRequest CreateRequest(string method,
99 Dictionary<string, string> param)
101 if (!isInitialize) throw new Exception("Sequence error.(not initialized)");
103 //GETメソッドの場合はクエリとurlを結合
104 UriBuilder ub = new UriBuilder(requestUri.AbsoluteUri);
105 if (param != null && (method == "GET" || method == "DELETE" || method == "HEAD"))
107 ub.Query = MyCommon.BuildQueryString(param);
110 HttpWebRequest webReq = (HttpWebRequest)WebRequest.Create(ub.Uri);
112 webReq.ReadWriteTimeout = 90 * 1000; //Streamの読み込みは90秒でタイムアウト(デフォルト5分)
115 if (proxyKind != ProxyType.IE) webReq.Proxy = proxy;
117 webReq.Method = method;
118 if (method == "POST" || method == "PUT")
120 webReq.ContentType = "application/x-www-form-urlencoded";
121 //POST/PUTメソッドの場合は、ボディデータとしてクエリ構成して書き込み
122 using (StreamWriter writer = new StreamWriter(webReq.GetRequestStream()))
124 writer.Write(MyCommon.BuildQueryString(param));
128 if (this.UseCookie) webReq.CookieContainer = this.cookieContainer;
130 webReq.Timeout = this.InstanceTimeout ?? HttpConnection.DefaultTimeout;
132 webReq.UserAgent = MyCommon.GetUserAgentString();
138 ///HttpWebRequestオブジェクトを取得する。multipartでのバイナリアップロード用。
141 ///methodにはPOST/PUTのみ指定可能
143 ///<param name="method">HTTP通信メソッド(POST/PUT)</param>
144 ///<param name="requestUri">通信先URI</param>
145 ///<param name="param">form-dataで指定する名前と文字列のディクショナリ</param>
146 ///<param name="binaryFileInfo">form-dataで指定する名前とバイナリファイル情報のリスト</param>
147 ///<returns>引数で指定された内容を反映したHttpWebRequestオブジェクト</returns>
148 protected HttpWebRequest CreateRequest(string method,
150 Dictionary<string, string> param,
151 List<KeyValuePair<String, FileInfo>> binaryFileInfo)
153 if (!isInitialize) throw new Exception("Sequence error.(not initialized)");
155 //methodはPOST,PUTのみ許可
156 UriBuilder ub = new UriBuilder(requestUri.AbsoluteUri);
157 if (method == "GET" || method == "DELETE" || method == "HEAD")
158 throw new ArgumentException("Method must be POST or PUT");
159 if ((param == null || param.Count == 0) && (binaryFileInfo == null || binaryFileInfo.Count == 0))
160 throw new ArgumentException("Data is empty");
162 HttpWebRequest webReq = (HttpWebRequest)WebRequest.Create(ub.Uri);
165 if (proxyKind != ProxyType.IE) webReq.Proxy = proxy;
167 webReq.Method = method;
168 if (method == "POST" || method == "PUT")
170 string boundary = System.Environment.TickCount.ToString();
171 webReq.ContentType = "multipart/form-data; boundary=" + boundary;
172 using (Stream reqStream = webReq.GetRequestStream())
177 string postData = "";
178 foreach (KeyValuePair<string, string> kvp in param)
180 postData += "--" + boundary + "\r\n" +
181 "Content-Disposition: form-data; name=\"" + kvp.Key + "\"" +
182 "\r\n\r\n" + kvp.Value + "\r\n";
184 byte[] postBytes = Encoding.UTF8.GetBytes(postData);
185 reqStream.Write(postBytes, 0, postBytes.Length);
188 if (binaryFileInfo != null)
190 foreach (KeyValuePair<string, FileInfo> kvp in binaryFileInfo)
192 string postData = "";
193 byte[] crlfByte = Encoding.UTF8.GetBytes("\r\n");
196 switch (kvp.Value.Extension.ToLower())
214 mime = "image/x-bmp";
220 mime = "video/x-ms-wmv";
223 mime = "video/x-flv";
226 mime = "video/x-m4v";
229 mime = "video/quicktime";
235 mime = "application/vnd.rn-realmedia";
245 mime = "video/3gpp2";
248 mime = "application/octet-stream\r\nContent-Transfer-Encoding: binary";
251 postData = "--" + boundary + "\r\n" +
252 "Content-Disposition: form-data; name=\"" + kvp.Key + "\"; filename=\"" +
253 kvp.Value.Name + "\"\r\n" +
254 "Content-Type: " + mime + "\r\n\r\n";
255 byte[] postBytes = Encoding.UTF8.GetBytes(postData);
256 reqStream.Write(postBytes, 0, postBytes.Length);
257 //ファイルを読み出してHTTPのストリームに書き込み
258 using (FileStream fs = new FileStream(kvp.Value.FullName, FileMode.Open, FileAccess.Read))
261 byte[] readBytes = new byte[0x1000];
264 readSize = fs.Read(readBytes, 0, readBytes.Length);
265 if (readSize == 0) break;
266 reqStream.Write(readBytes, 0, readSize);
269 reqStream.Write(crlfByte, 0, crlfByte.Length);
273 byte[] endBytes = Encoding.UTF8.GetBytes("--" + boundary + "--\r\n");
274 reqStream.Write(endBytes, 0, endBytes.Length);
278 if (this.UseCookie) webReq.CookieContainer = this.cookieContainer;
280 webReq.Timeout = this.InstanceTimeout ?? HttpConnection.DefaultTimeout;
286 ///HTTPの応答を処理し、引数で指定されたストリームに書き込み
289 ///リダイレクト応答の場合(AllowAutoRedirect=Falseの場合のみ)は、headerInfoインスタンスがあればLocationを追加してリダイレクト先を返却
290 ///WebExceptionはハンドルしていないので、呼び出し元でキャッチすること
291 ///gzipファイルのダウンロードを想定しているため、他形式の場合は伸張時に問題が発生する可能性があります。
293 ///<param name="webRequest">HTTP通信リクエストオブジェクト</param>
294 ///<param name="contentStream">[OUT]HTTP応答のボディストリームのコピー先</param>
295 ///<param name="headerInfo">[IN/OUT]HTTP応答のヘッダ情報。ヘッダ名をキーにして空データのコレクションを渡すことで、該当ヘッダの値をデータに設定して戻す</param>
296 ///<returns>HTTP応答のステータスコード</returns>
297 protected HttpStatusCode GetResponse(HttpWebRequest webRequest,
298 Stream contentStream,
299 Dictionary<string, string> headerInfo)
303 using (HttpWebResponse webRes = (HttpWebResponse)webRequest.GetResponse())
305 HttpStatusCode statusCode = webRes.StatusCode;
307 if (this.UseCookie) this.FixCookies(webRes.Cookies);
308 //リダイレクト応答の場合は、リダイレクト先を設定
309 GetHeaderInfo(webRes, headerInfo);
311 if (webRes.ContentLength > 0)
313 //gzipなら応答ストリームの内容は伸張済み。それ以外なら伸張する。
314 if (webRes.ContentEncoding == "gzip" || webRes.ContentEncoding == "deflate")
316 using (Stream stream = webRes.GetResponseStream())
318 if (stream != null) stream.CopyTo(contentStream);
323 using (Stream stream = new GZipStream(webRes.GetResponseStream(), CompressionMode.Decompress))
325 if (stream != null) stream.CopyTo(contentStream);
332 catch (WebException ex)
334 if (ex.Status == WebExceptionStatus.ProtocolError)
336 HttpWebResponse res = (HttpWebResponse)ex.Response;
337 GetHeaderInfo(res, headerInfo);
338 return res.StatusCode;
345 ///HTTPの応答を処理し、応答ボディデータをテキストとして返却する
348 ///リダイレクト応答の場合(AllowAutoRedirect=Falseの場合のみ)は、headerInfoインスタンスがあればLocationを追加してリダイレクト先を返却
349 ///WebExceptionはハンドルしていないので、呼び出し元でキャッチすること
350 ///テキストの文字コードはUTF-8を前提として、エンコードはしていません
352 ///<param name="webRequest">HTTP通信リクエストオブジェクト</param>
353 ///<param name="contentText">[OUT]HTTP応答のボディデータ</param>
354 ///<param name="headerInfo">[IN/OUT]HTTP応答のヘッダ情報。ヘッダ名をキーにして空データのコレクションを渡すことで、該当ヘッダの値をデータに設定して戻す</param>
355 ///<returns>HTTP応答のステータスコード</returns>
356 protected HttpStatusCode GetResponse(HttpWebRequest webRequest,
357 out string contentText,
358 Dictionary<string, string> headerInfo)
362 using (HttpWebResponse webRes = (HttpWebResponse)webRequest.GetResponse())
364 HttpStatusCode statusCode = webRes.StatusCode;
366 if (this.UseCookie) this.FixCookies(webRes.Cookies);
367 //リダイレクト応答の場合は、リダイレクト先を設定
368 GetHeaderInfo(webRes, headerInfo);
370 using (StreamReader sr = new StreamReader(webRes.GetResponseStream()))
372 contentText = sr.ReadToEnd();
377 catch (WebException ex)
379 if (ex.Status == WebExceptionStatus.ProtocolError)
381 HttpWebResponse res = (HttpWebResponse)ex.Response;
382 GetHeaderInfo(res, headerInfo);
383 using (StreamReader sr = new StreamReader(res.GetResponseStream()))
385 contentText = sr.ReadToEnd();
387 return res.StatusCode;
394 ///HTTPの応答を処理します。応答ボディデータが不要な用途向け。
397 ///リダイレクト応答の場合(AllowAutoRedirect=Falseの場合のみ)は、headerInfoインスタンスがあればLocationを追加してリダイレクト先を返却
398 ///WebExceptionはハンドルしていないので、呼び出し元でキャッチすること
400 ///<param name="webRequest">HTTP通信リクエストオブジェクト</param>
401 ///<param name="headerInfo">[IN/OUT]HTTP応答のヘッダ情報。ヘッダ名をキーにして空データのコレクションを渡すことで、該当ヘッダの値をデータに設定して戻す</param>
402 ///<returns>HTTP応答のステータスコード</returns>
403 protected HttpStatusCode GetResponse(HttpWebRequest webRequest,
404 Dictionary<string, string> headerInfo)
408 using (HttpWebResponse webRes = (HttpWebResponse)webRequest.GetResponse())
410 HttpStatusCode statusCode = webRes.StatusCode;
412 if (this.UseCookie) this.FixCookies(webRes.Cookies);
413 //リダイレクト応答の場合は、リダイレクト先を設定
414 GetHeaderInfo(webRes, headerInfo);
418 catch (WebException ex)
420 if (ex.Status == WebExceptionStatus.ProtocolError)
422 HttpWebResponse res = (HttpWebResponse)ex.Response;
423 GetHeaderInfo(res, headerInfo);
424 return res.StatusCode;
431 ///HTTPの応答を処理し、応答ボディデータをBitmapとして返却します
434 ///リダイレクト応答の場合(AllowAutoRedirect=Falseの場合のみ)は、headerInfoインスタンスがあればLocationを追加してリダイレクト先を返却
435 ///WebExceptionはハンドルしていないので、呼び出し元でキャッチすること
437 ///<param name="webRequest">HTTP通信リクエストオブジェクト</param>
438 ///<param name="contentBitmap">[OUT]HTTP応答のボディデータを書き込むBitmap</param>
439 ///<param name="headerInfo">[IN/OUT]HTTP応答のヘッダ情報。ヘッダ名をキーにして空データのコレクションを渡すことで、該当ヘッダの値をデータに設定して戻す</param>
440 ///<returns>HTTP応答のステータスコード</returns>
441 protected HttpStatusCode GetResponse(HttpWebRequest webRequest,
442 out Bitmap contentBitmap,
443 Dictionary<string, string> headerInfo)
447 using (HttpWebResponse webRes = (HttpWebResponse)webRequest.GetResponse())
449 HttpStatusCode statusCode = webRes.StatusCode;
451 if (this.UseCookie) this.FixCookies(webRes.Cookies);
452 //リダイレクト応答の場合は、リダイレクト先を設定
453 GetHeaderInfo(webRes, headerInfo);
454 //応答のストリームをBitmapにして戻す
455 //if (webRes.ContentLength > 0) contentBitmap = new Bitmap(webRes.GetResponseStream());
456 contentBitmap = new Bitmap(webRes.GetResponseStream());
460 catch (WebException ex)
462 if (ex.Status == WebExceptionStatus.ProtocolError)
464 HttpWebResponse res = (HttpWebResponse)ex.Response;
465 GetHeaderInfo(res, headerInfo);
466 contentBitmap = null;
467 return res.StatusCode;
474 /// ホスト名なしのドメインはドメイン名から先頭のドットを除去しないと再利用されないため修正して追加する
476 private void FixCookies(CookieCollection cookieCollection)
478 foreach (Cookie ck in cookieCollection)
480 if (ck.Domain.StartsWith("."))
482 ck.Domain = ck.Domain.Substring(1);
483 cookieContainer.Add(ck);
489 ///headerInfoのキー情報で指定されたHTTPヘッダ情報を取得・格納する。redirect応答時はLocationヘッダの内容を追記する
491 ///<param name="webResponse">HTTP応答</param>
492 ///<param name="headerInfo">[IN/OUT]キーにヘッダ名を指定したデータ空のコレクション。取得した値をデータにセットして戻す</param>
493 private void GetHeaderInfo(HttpWebResponse webResponse,
494 Dictionary<string, string> headerInfo)
496 if (headerInfo == null) return;
498 if (headerInfo.Count > 0)
500 var headers = webResponse.Headers;
501 var dictKeys = new string[headerInfo.Count];
502 headerInfo.Keys.CopyTo(dictKeys, 0);
504 foreach (var key in dictKeys)
506 var value = headers[key];
507 headerInfo[key] = value ?? "";
511 HttpStatusCode statusCode = webResponse.StatusCode;
512 if (statusCode == HttpStatusCode.MovedPermanently ||
513 statusCode == HttpStatusCode.Found ||
514 statusCode == HttpStatusCode.SeeOther ||
515 statusCode == HttpStatusCode.TemporaryRedirect)
517 if (webResponse.Headers["Location"] != null)
519 headerInfo["Location"] = webResponse.Headers["Location"];
525 ///クエリ形式(key1=value1&key2=value2&...)の文字列をkey-valueコレクションに詰め直し
527 ///<param name="queryString">クエリ文字列</param>
528 ///<returns>key-valueのコレクション</returns>
529 protected NameValueCollection ParseQueryString(string queryString)
531 NameValueCollection query = new NameValueCollection();
532 string[] parts = queryString.Split('&');
533 foreach (string part in parts)
535 int index = part.IndexOf('=');
537 query.Add(Uri.UnescapeDataString(part), "");
539 query.Add(Uri.UnescapeDataString(part.Substring(0, index)), Uri.UnescapeDataString(part.Substring(index + 1)));
545 ///2バイト文字も考慮したUrlエンコード
547 ///<param name="stringToEncode">エンコードする文字列</param>
548 ///<returns>エンコード結果文字列</returns>
549 protected string UrlEncode(string stringToEncode)
551 const string UnreservedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~";
552 StringBuilder sb = new StringBuilder();
553 byte[] bytes = Encoding.UTF8.GetBytes(stringToEncode);
555 foreach (byte b in bytes)
557 if (UnreservedChars.IndexOf((char)b) != -1)
560 sb.AppendFormat("%{0:X2}", b);
562 return sb.ToString();
565 #region "InstanceTimeout"
569 private int? _timeout = null;
572 ///通信タイムアウト時間(ms)。10~120秒の範囲で指定。範囲外は20秒とする
574 protected int? InstanceTimeout
576 get { return _timeout; }
579 const int TimeoutMinValue = 10000;
580 const int TimeoutMaxValue = 120000;
581 if (value < TimeoutMinValue || value > TimeoutMaxValue)
582 throw new ArgumentOutOfRangeException("Set " + TimeoutMinValue + "-" + TimeoutMaxValue + ": Value=" + value);
589 #region "DefaultTimeout"
593 private static int timeout = 20000;
596 ///通信タイムアウト時間(ms)。10~120秒の範囲で指定。範囲外は20秒とする
598 protected static int DefaultTimeout
600 get { return timeout; }
603 const int TimeoutMinValue = 10000;
604 const int TimeoutMaxValue = 120000;
605 const int TimeoutDefaultValue = 20000;
606 if (value < TimeoutMinValue || value > TimeoutMaxValue)
608 timeout = TimeoutDefaultValue;
616 /// OpenTween 内で共通して使用する HttpClient インスタンス
618 public static HttpClient GlobalHttpClient
620 get { return globalHttpClient; }
624 /// Webプロキシの設定が変更された場合に発生します
626 public static event EventHandler WebProxyChanged;
628 private static HttpClient globalHttpClient = CreateHttpClient(new HttpClientHandler());
631 ///通信クラスの初期化処理。タイムアウト値とプロキシを設定する
636 ///<param name="timeout">タイムアウト値(秒)</param>
637 ///<param name="proxyType">なし・指定・IEデフォルト</param>
638 ///<param name="proxyAddress">プロキシのホスト名orIPアドレス</param>
639 ///<param name="proxyPort">プロキシのポート番号</param>
640 ///<param name="proxyUser">プロキシ認証が必要な場合のユーザ名。不要なら空文字</param>
641 ///<param name="proxyPassword">プロキシ認証が必要な場合のパスワード。不要なら空文字</param>
642 public static void InitializeConnection(int timeout,
643 ProxyType proxyType, string proxyAddress, int proxyPort,
644 string proxyUser, string proxyPassword)
646 HttpConnection.isInitialize = true;
647 HttpConnection.DefaultTimeout = timeout * 1000; // s -> ms
649 ServicePointManager.Expect100Continue = false;
651 SetWebProxy(proxyType, proxyAddress, proxyPort, proxyUser, proxyPassword);
654 public static void SetWebProxy(ProxyType proxyType, string proxyAddress, int proxyPort,
655 string proxyUser, string proxyPassword)
663 case ProxyType.Specified:
664 proxy = new WebProxy(proxyAddress, proxyPort);
665 if (!string.IsNullOrEmpty(proxyUser) || !string.IsNullOrEmpty(proxyPassword))
666 proxy.Credentials = new NetworkCredential(proxyUser, proxyPassword);
670 proxy = WebRequest.GetSystemWebProxy();
674 HttpConnection.proxyKind = proxyType;
675 HttpConnection.proxy = proxy;
677 Win32Api.SetProxy(proxyType, proxyAddress, proxyPort, proxyUser, proxyPassword);
679 OnWebProxyChanged(EventArgs.Empty);
683 /// プロキシ等の設定を施した HttpClient インスタンスを生成します
686 /// 通常は HttpConnection.GlobalHttpClient を使用すべきです。
687 /// このメソッドを使用する場合は、WebProxyChanged イベントが発生する度に HttpClient を生成し直すように実装してください。
689 public static HttpClient CreateHttpClient(HttpClientHandler handler)
691 if (HttpConnection.proxy != null)
693 handler.UseProxy = true;
694 handler.Proxy = HttpConnection.proxy;
698 handler.UseProxy = false;
701 var client = new HttpClient(handler);
702 client.Timeout = TimeSpan.FromMilliseconds(HttpConnection.DefaultTimeout);
703 client.DefaultRequestHeaders.Add("User-Agent", MyCommon.GetUserAgentString());
708 private static void OnWebProxyChanged(EventArgs e)
710 var newClient = HttpConnection.CreateHttpClient(new HttpClientHandler());
711 var oldClient = Interlocked.Exchange(ref globalHttpClient, newClient);
714 if (WebProxyChanged != null)
715 WebProxyChanged(null, e);