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;
37 using OpenTween.Connection;
40 ///HttpWebRequest,HttpWebResponseを使用した基本的な通信機能を提供する
43 ///プロキシ情報などを設定するため、使用前に静的メソッドInitializeConnectionを呼び出すこと。
44 ///通信方式によって必要になるHTTPヘッダの付加などは、派生クラスで行う。
48 public class HttpConnection
51 /// リクエスト間で Cookie を保持するか否か
53 public bool UseCookie { get; set; }
58 private CookieContainer cookieContainer = new CookieContainer();
60 protected const string PostMethod = "POST";
61 protected const string GetMethod = "GET";
62 protected const string HeadMethod = "HEAD";
65 ///HttpWebRequestオブジェクトを取得する。パラメータはGET/HEAD/DELETEではクエリに、POST/PUTではエンティティボディに変換される。
68 ///追加で必要となるHTTPヘッダや通信オプションは呼び出し元で付加すること
69 ///(Timeout,AutomaticDecompression,AllowAutoRedirect,UserAgent,ContentType,Accept,HttpRequestHeader.Authorization,カスタムヘッダ)
70 ///POST/PUTでクエリが必要な場合は、requestUriに含めること。
72 ///<param name="method">HTTP通信メソッド(GET/HEAD/POST/PUT/DELETE)</param>
73 ///<param name="requestUri">通信先URI</param>
74 ///<param name="param">GET時のクエリ、またはPOST時のエンティティボディ</param>
75 ///<param name="gzip">Accept-Encodingヘッダにgzipを付加するかどうかを表す真偽値</param>
76 ///<returns>引数で指定された内容を反映したHttpWebRequestオブジェクト</returns>
77 protected HttpWebRequest CreateRequest(string method,
79 Dictionary<string, string> param,
82 Networking.CheckInitialized();
84 //GETメソッドの場合はクエリとurlを結合
85 UriBuilder ub = new UriBuilder(requestUri.AbsoluteUri);
86 if (param != null && (method == "GET" || method == "DELETE" || method == "HEAD"))
88 ub.Query = MyCommon.BuildQueryString(param);
91 HttpWebRequest webReq = (HttpWebRequest)WebRequest.Create(ub.Uri);
93 webReq.ReadWriteTimeout = 90 * 1000; //Streamの読み込みは90秒でタイムアウト(デフォルト5分)
96 if (Networking.ProxyType != ProxyType.IE) webReq.Proxy = Networking.Proxy;
100 // Accept-Encodingヘッダを付加
101 webReq.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip;
104 webReq.Method = method;
105 if (method == "POST" || method == "PUT")
107 webReq.ContentType = "application/x-www-form-urlencoded";
108 //POST/PUTメソッドの場合は、ボディデータとしてクエリ構成して書き込み
109 using (StreamWriter writer = new StreamWriter(webReq.GetRequestStream()))
111 writer.Write(MyCommon.BuildQueryString(param));
115 if (this.UseCookie) webReq.CookieContainer = this.cookieContainer;
117 webReq.Timeout = this.InstanceTimeout ?? (int)Networking.DefaultTimeout.TotalMilliseconds;
119 webReq.UserAgent = Networking.GetUserAgentString();
121 // KeepAlive無効なサーバー(Twitter等)に使用すると、タイムアウト後にWebExceptionが発生する場合あり
122 webReq.KeepAlive = false;
128 ///HttpWebRequestオブジェクトを取得する。multipartでのバイナリアップロード用。
131 ///methodにはPOST/PUTのみ指定可能
133 ///<param name="method">HTTP通信メソッド(POST/PUT)</param>
134 ///<param name="requestUri">通信先URI</param>
135 ///<param name="param">form-dataで指定する名前と文字列のディクショナリ</param>
136 ///<param name="binaryFileInfo">form-dataで指定する名前とバイナリファイル情報のリスト</param>
137 ///<returns>引数で指定された内容を反映したHttpWebRequestオブジェクト</returns>
138 protected HttpWebRequest CreateRequest(string method,
140 Dictionary<string, string> param,
141 List<KeyValuePair<String, FileInfo>> binaryFileInfo)
143 Networking.CheckInitialized();
145 //methodはPOST,PUTのみ許可
146 UriBuilder ub = new UriBuilder(requestUri.AbsoluteUri);
147 if (method == "GET" || method == "DELETE" || method == "HEAD")
148 throw new ArgumentException("Method must be POST or PUT");
149 if ((param == null || param.Count == 0) && (binaryFileInfo == null || binaryFileInfo.Count == 0))
150 throw new ArgumentException("Data is empty");
152 HttpWebRequest webReq = (HttpWebRequest)WebRequest.Create(ub.Uri);
155 if (Networking.ProxyType != ProxyType.IE) webReq.Proxy = Networking.Proxy;
157 webReq.Method = method;
158 if (method == "POST" || method == "PUT")
160 string boundary = System.Environment.TickCount.ToString();
161 webReq.ContentType = "multipart/form-data; boundary=" + boundary;
162 using (Stream reqStream = webReq.GetRequestStream())
167 string postData = "";
168 foreach (KeyValuePair<string, string> kvp in param)
170 postData += "--" + boundary + "\r\n" +
171 "Content-Disposition: form-data; name=\"" + kvp.Key + "\"" +
172 "\r\n\r\n" + kvp.Value + "\r\n";
174 byte[] postBytes = Encoding.UTF8.GetBytes(postData);
175 reqStream.Write(postBytes, 0, postBytes.Length);
178 if (binaryFileInfo != null)
180 foreach (KeyValuePair<string, FileInfo> kvp in binaryFileInfo)
182 string postData = "";
183 byte[] crlfByte = Encoding.UTF8.GetBytes("\r\n");
186 switch (kvp.Value.Extension.ToLower())
204 mime = "image/x-bmp";
210 mime = "video/x-ms-wmv";
213 mime = "video/x-flv";
216 mime = "video/x-m4v";
219 mime = "video/quicktime";
225 mime = "application/vnd.rn-realmedia";
235 mime = "video/3gpp2";
238 mime = "application/octet-stream\r\nContent-Transfer-Encoding: binary";
241 postData = "--" + boundary + "\r\n" +
242 "Content-Disposition: form-data; name=\"" + kvp.Key + "\"; filename=\"" +
243 kvp.Value.Name + "\"\r\n" +
244 "Content-Type: " + mime + "\r\n\r\n";
245 byte[] postBytes = Encoding.UTF8.GetBytes(postData);
246 reqStream.Write(postBytes, 0, postBytes.Length);
247 //ファイルを読み出してHTTPのストリームに書き込み
248 using (FileStream fs = new FileStream(kvp.Value.FullName, FileMode.Open, FileAccess.Read))
251 byte[] readBytes = new byte[0x1000];
254 readSize = fs.Read(readBytes, 0, readBytes.Length);
255 if (readSize == 0) break;
256 reqStream.Write(readBytes, 0, readSize);
259 reqStream.Write(crlfByte, 0, crlfByte.Length);
263 byte[] endBytes = Encoding.UTF8.GetBytes("--" + boundary + "--\r\n");
264 reqStream.Write(endBytes, 0, endBytes.Length);
268 if (this.UseCookie) webReq.CookieContainer = this.cookieContainer;
270 webReq.Timeout = this.InstanceTimeout ?? (int)Networking.DefaultTimeout.TotalMilliseconds;
272 // KeepAlive無効なサーバー(Twitter等)に使用すると、タイムアウト後にWebExceptionが発生する場合あり
273 webReq.KeepAlive = false;
279 ///HTTPの応答を処理し、引数で指定されたストリームに書き込み
282 ///リダイレクト応答の場合(AllowAutoRedirect=Falseの場合のみ)は、headerInfoインスタンスがあればLocationを追加してリダイレクト先を返却
283 ///WebExceptionはハンドルしていないので、呼び出し元でキャッチすること
284 ///gzipファイルのダウンロードを想定しているため、他形式の場合は伸張時に問題が発生する可能性があります。
286 ///<param name="webRequest">HTTP通信リクエストオブジェクト</param>
287 ///<param name="contentStream">[OUT]HTTP応答のボディストリームのコピー先</param>
288 ///<param name="headerInfo">[IN/OUT]HTTP応答のヘッダ情報。ヘッダ名をキーにして空データのコレクションを渡すことで、該当ヘッダの値をデータに設定して戻す</param>
289 ///<returns>HTTP応答のステータスコード</returns>
290 protected HttpStatusCode GetResponse(HttpWebRequest webRequest,
291 Stream contentStream,
292 Dictionary<string, string> headerInfo)
296 using (HttpWebResponse webRes = (HttpWebResponse)webRequest.GetResponse())
298 HttpStatusCode statusCode = webRes.StatusCode;
300 if (this.UseCookie) this.FixCookies(webRes.Cookies);
301 //リダイレクト応答の場合は、リダイレクト先を設定
302 GetHeaderInfo(webRes, headerInfo);
304 if (webRes.ContentLength > 0)
306 //gzipなら応答ストリームの内容は伸張済み。それ以外なら伸張する。
307 if (webRes.ContentEncoding == "gzip" || webRes.ContentEncoding == "deflate")
309 using (Stream stream = webRes.GetResponseStream())
311 if (stream != null) stream.CopyTo(contentStream);
316 using (Stream stream = new GZipStream(webRes.GetResponseStream(), CompressionMode.Decompress))
318 if (stream != null) stream.CopyTo(contentStream);
325 catch (WebException ex)
327 if (ex.Status == WebExceptionStatus.ProtocolError)
329 HttpWebResponse res = (HttpWebResponse)ex.Response;
330 GetHeaderInfo(res, headerInfo);
331 return res.StatusCode;
338 ///HTTPの応答を処理し、応答ボディデータをテキストとして返却する
341 ///リダイレクト応答の場合(AllowAutoRedirect=Falseの場合のみ)は、headerInfoインスタンスがあればLocationを追加してリダイレクト先を返却
342 ///WebExceptionはハンドルしていないので、呼び出し元でキャッチすること
343 ///テキストの文字コードはUTF-8を前提として、エンコードはしていません
345 ///<param name="webRequest">HTTP通信リクエストオブジェクト</param>
346 ///<param name="contentText">[OUT]HTTP応答のボディデータ</param>
347 ///<param name="headerInfo">[IN/OUT]HTTP応答のヘッダ情報。ヘッダ名をキーにして空データのコレクションを渡すことで、該当ヘッダの値をデータに設定して戻す</param>
348 ///<returns>HTTP応答のステータスコード</returns>
349 protected HttpStatusCode GetResponse(HttpWebRequest webRequest,
350 out string contentText,
351 Dictionary<string, string> headerInfo)
355 using (HttpWebResponse webRes = (HttpWebResponse)webRequest.GetResponse())
357 HttpStatusCode statusCode = webRes.StatusCode;
359 if (this.UseCookie) this.FixCookies(webRes.Cookies);
360 //リダイレクト応答の場合は、リダイレクト先を設定
361 GetHeaderInfo(webRes, headerInfo);
363 using (StreamReader sr = new StreamReader(webRes.GetResponseStream()))
365 contentText = sr.ReadToEnd();
370 catch (WebException ex)
372 if (ex.Status == WebExceptionStatus.ProtocolError)
374 HttpWebResponse res = (HttpWebResponse)ex.Response;
375 GetHeaderInfo(res, headerInfo);
376 using (StreamReader sr = new StreamReader(res.GetResponseStream()))
378 contentText = sr.ReadToEnd();
380 return res.StatusCode;
387 ///HTTPの応答を処理します。応答ボディデータが不要な用途向け。
390 ///リダイレクト応答の場合(AllowAutoRedirect=Falseの場合のみ)は、headerInfoインスタンスがあればLocationを追加してリダイレクト先を返却
391 ///WebExceptionはハンドルしていないので、呼び出し元でキャッチすること
393 ///<param name="webRequest">HTTP通信リクエストオブジェクト</param>
394 ///<param name="headerInfo">[IN/OUT]HTTP応答のヘッダ情報。ヘッダ名をキーにして空データのコレクションを渡すことで、該当ヘッダの値をデータに設定して戻す</param>
395 ///<returns>HTTP応答のステータスコード</returns>
396 protected HttpStatusCode GetResponse(HttpWebRequest webRequest,
397 Dictionary<string, string> headerInfo)
401 using (HttpWebResponse webRes = (HttpWebResponse)webRequest.GetResponse())
403 HttpStatusCode statusCode = webRes.StatusCode;
405 if (this.UseCookie) this.FixCookies(webRes.Cookies);
406 //リダイレクト応答の場合は、リダイレクト先を設定
407 GetHeaderInfo(webRes, headerInfo);
411 catch (WebException ex)
413 if (ex.Status == WebExceptionStatus.ProtocolError)
415 HttpWebResponse res = (HttpWebResponse)ex.Response;
416 GetHeaderInfo(res, headerInfo);
417 return res.StatusCode;
424 ///HTTPの応答を処理し、応答ボディデータをBitmapとして返却します
427 ///リダイレクト応答の場合(AllowAutoRedirect=Falseの場合のみ)は、headerInfoインスタンスがあればLocationを追加してリダイレクト先を返却
428 ///WebExceptionはハンドルしていないので、呼び出し元でキャッチすること
430 ///<param name="webRequest">HTTP通信リクエストオブジェクト</param>
431 ///<param name="contentBitmap">[OUT]HTTP応答のボディデータを書き込むBitmap</param>
432 ///<param name="headerInfo">[IN/OUT]HTTP応答のヘッダ情報。ヘッダ名をキーにして空データのコレクションを渡すことで、該当ヘッダの値をデータに設定して戻す</param>
433 ///<returns>HTTP応答のステータスコード</returns>
434 protected HttpStatusCode GetResponse(HttpWebRequest webRequest,
435 out Bitmap contentBitmap,
436 Dictionary<string, string> headerInfo)
440 using (HttpWebResponse webRes = (HttpWebResponse)webRequest.GetResponse())
442 HttpStatusCode statusCode = webRes.StatusCode;
444 if (this.UseCookie) this.FixCookies(webRes.Cookies);
445 //リダイレクト応答の場合は、リダイレクト先を設定
446 GetHeaderInfo(webRes, headerInfo);
447 //応答のストリームをBitmapにして戻す
448 //if (webRes.ContentLength > 0) contentBitmap = new Bitmap(webRes.GetResponseStream());
449 contentBitmap = new Bitmap(webRes.GetResponseStream());
453 catch (WebException ex)
455 if (ex.Status == WebExceptionStatus.ProtocolError)
457 HttpWebResponse res = (HttpWebResponse)ex.Response;
458 GetHeaderInfo(res, headerInfo);
459 contentBitmap = null;
460 return res.StatusCode;
467 /// ホスト名なしのドメインはドメイン名から先頭のドットを除去しないと再利用されないため修正して追加する
469 private void FixCookies(CookieCollection cookieCollection)
471 foreach (Cookie ck in cookieCollection)
473 if (ck.Domain.StartsWith("."))
475 ck.Domain = ck.Domain.Substring(1);
476 cookieContainer.Add(ck);
482 ///headerInfoのキー情報で指定されたHTTPヘッダ情報を取得・格納する。redirect応答時はLocationヘッダの内容を追記する
484 ///<param name="webResponse">HTTP応答</param>
485 ///<param name="headerInfo">[IN/OUT]キーにヘッダ名を指定したデータ空のコレクション。取得した値をデータにセットして戻す</param>
486 private void GetHeaderInfo(HttpWebResponse webResponse,
487 Dictionary<string, string> headerInfo)
489 if (headerInfo == null) return;
491 if (headerInfo.Count > 0)
493 var headers = webResponse.Headers;
494 var dictKeys = new string[headerInfo.Count];
495 headerInfo.Keys.CopyTo(dictKeys, 0);
497 foreach (var key in dictKeys)
499 var value = headers[key];
500 headerInfo[key] = value ?? "";
504 HttpStatusCode statusCode = webResponse.StatusCode;
505 if (statusCode == HttpStatusCode.MovedPermanently ||
506 statusCode == HttpStatusCode.Found ||
507 statusCode == HttpStatusCode.SeeOther ||
508 statusCode == HttpStatusCode.TemporaryRedirect)
510 if (webResponse.Headers["Location"] != null)
512 headerInfo["Location"] = webResponse.Headers["Location"];
518 ///クエリ形式(key1=value1&key2=value2&...)の文字列をkey-valueコレクションに詰め直し
520 ///<param name="queryString">クエリ文字列</param>
521 ///<returns>key-valueのコレクション</returns>
522 protected NameValueCollection ParseQueryString(string queryString)
524 NameValueCollection query = new NameValueCollection();
525 string[] parts = queryString.Split('&');
526 foreach (string part in parts)
528 int index = part.IndexOf('=');
530 query.Add(Uri.UnescapeDataString(part), "");
532 query.Add(Uri.UnescapeDataString(part.Substring(0, index)), Uri.UnescapeDataString(part.Substring(index + 1)));
538 ///2バイト文字も考慮したUrlエンコード
540 ///<param name="stringToEncode">エンコードする文字列</param>
541 ///<returns>エンコード結果文字列</returns>
542 protected string UrlEncode(string stringToEncode)
544 const string UnreservedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~";
545 StringBuilder sb = new StringBuilder();
546 byte[] bytes = Encoding.UTF8.GetBytes(stringToEncode);
548 foreach (byte b in bytes)
550 if (UnreservedChars.IndexOf((char)b) != -1)
553 sb.AppendFormat("%{0:X2}", b);
555 return sb.ToString();
558 #region "InstanceTimeout"
562 private int? _timeout = null;
565 ///通信タイムアウト時間(ms)。10~120秒の範囲で指定。範囲外は20秒とする
567 protected int? InstanceTimeout
569 get { return _timeout; }
572 const int TimeoutMinValue = 10000;
573 const int TimeoutMaxValue = 120000;
574 if (value < TimeoutMinValue || value > TimeoutMaxValue)
575 throw new ArgumentOutOfRangeException("Set " + TimeoutMinValue + "-" + TimeoutMaxValue + ": Value=" + value);