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;
38 using System.Net.Cache;
41 ///HttpWebRequest,HttpWebResponseを使用した基本的な通信機能を提供する
44 ///プロキシ情報などを設定するため、使用前に静的メソッドInitializeConnectionを呼び出すこと。
45 ///通信方式によって必要になるHTTPヘッダの付加などは、派生クラスで行う。
49 public class HttpConnection
54 public bool CacheEnabled { get; set; } = true;
57 /// リクエスト間で Cookie を保持するか否か
59 public bool UseCookie { get; set; }
64 private CookieContainer cookieContainer = new CookieContainer();
66 protected const string PostMethod = "POST";
67 protected const string GetMethod = "GET";
68 protected const string HeadMethod = "HEAD";
71 ///HttpWebRequestオブジェクトを取得する。パラメータはGET/HEAD/DELETEではクエリに、POST/PUTではエンティティボディに変換される。
74 ///追加で必要となるHTTPヘッダや通信オプションは呼び出し元で付加すること
75 ///(Timeout,AutomaticDecompression,AllowAutoRedirect,UserAgent,ContentType,Accept,HttpRequestHeader.Authorization,カスタムヘッダ)
76 ///POST/PUTでクエリが必要な場合は、requestUriに含めること。
78 ///<param name="method">HTTP通信メソッド(GET/HEAD/POST/PUT/DELETE)</param>
79 ///<param name="requestUri">通信先URI</param>
80 ///<param name="param">GET時のクエリ、またはPOST時のエンティティボディ</param>
81 ///<param name="gzip">Accept-Encodingヘッダにgzipを付加するかどうかを表す真偽値</param>
82 ///<returns>引数で指定された内容を反映したHttpWebRequestオブジェクト</returns>
83 protected HttpWebRequest CreateRequest(string method,
85 Dictionary<string, string> param,
88 Networking.CheckInitialized();
90 //GETメソッドの場合はクエリとurlを結合
91 UriBuilder ub = new UriBuilder(requestUri.AbsoluteUri);
92 if (param != null && (method == "GET" || method == "DELETE" || method == "HEAD"))
94 ub.Query = MyCommon.BuildQueryString(param);
97 HttpWebRequest webReq = (HttpWebRequest)WebRequest.Create(ub.Uri);
99 webReq.ReadWriteTimeout = 90 * 1000; //Streamの読み込みは90秒でタイムアウト(デフォルト5分)
102 if (Networking.ProxyType != ProxyType.IE) webReq.Proxy = Networking.Proxy;
106 // Accept-Encodingヘッダを付加
107 webReq.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip;
110 webReq.Method = method;
111 if (method == "POST" || method == "PUT")
113 webReq.ContentType = "application/x-www-form-urlencoded";
114 //POST/PUTメソッドの場合は、ボディデータとしてクエリ構成して書き込み
115 using (StreamWriter writer = new StreamWriter(webReq.GetRequestStream()))
117 writer.Write(MyCommon.BuildQueryString(param));
121 if (this.UseCookie) webReq.CookieContainer = this.cookieContainer;
123 webReq.Timeout = this.InstanceTimeout ?? (int)Networking.DefaultTimeout.TotalMilliseconds;
125 webReq.UserAgent = Networking.GetUserAgentString();
127 // KeepAlive無効なサーバー(Twitter等)に使用すると、タイムアウト後にWebExceptionが発生する場合あり
128 webReq.KeepAlive = false;
130 if (!this.CacheEnabled)
131 webReq.CachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache);
137 ///HttpWebRequestオブジェクトを取得する。multipartでのバイナリアップロード用。
140 ///methodにはPOST/PUTのみ指定可能
142 ///<param name="method">HTTP通信メソッド(POST/PUT)</param>
143 ///<param name="requestUri">通信先URI</param>
144 ///<param name="param">form-dataで指定する名前と文字列のディクショナリ</param>
145 ///<param name="binaryFileInfo">form-dataで指定する名前とバイナリファイル情報のリスト</param>
146 ///<returns>引数で指定された内容を反映したHttpWebRequestオブジェクト</returns>
147 protected HttpWebRequest CreateRequest(string method,
149 Dictionary<string, string> param,
150 List<KeyValuePair<String, IMediaItem>> binaryFileInfo)
152 Networking.CheckInitialized();
154 //methodはPOST,PUTのみ許可
155 UriBuilder ub = new UriBuilder(requestUri.AbsoluteUri);
156 if (method == "GET" || method == "DELETE" || method == "HEAD")
157 throw new ArgumentException("Method must be POST or PUT", nameof(method));
158 if ((param == null || param.Count == 0) && (binaryFileInfo == null || binaryFileInfo.Count == 0))
159 throw new ArgumentException("Data is empty");
161 HttpWebRequest webReq = (HttpWebRequest)WebRequest.Create(ub.Uri);
164 if (Networking.ProxyType != ProxyType.IE) webReq.Proxy = Networking.Proxy;
166 webReq.Method = method;
167 if (method == "POST" || method == "PUT")
169 string boundary = System.Environment.TickCount.ToString();
170 webReq.ContentType = "multipart/form-data; boundary=" + boundary;
171 using (Stream reqStream = webReq.GetRequestStream())
176 string postData = "";
177 foreach (KeyValuePair<string, string> kvp in param)
179 postData += "--" + boundary + "\r\n" +
180 "Content-Disposition: form-data; name=\"" + kvp.Key + "\"" +
181 "\r\n\r\n" + kvp.Value + "\r\n";
183 byte[] postBytes = Encoding.UTF8.GetBytes(postData);
184 reqStream.Write(postBytes, 0, postBytes.Length);
187 if (binaryFileInfo != null)
189 foreach (KeyValuePair<string, IMediaItem> kvp in binaryFileInfo)
191 string postData = "";
192 byte[] crlfByte = Encoding.UTF8.GetBytes("\r\n");
195 switch (kvp.Value.Extension.ToLowerInvariant())
213 mime = "image/x-bmp";
219 mime = "video/x-ms-wmv";
222 mime = "video/x-flv";
225 mime = "video/x-m4v";
228 mime = "video/quicktime";
234 mime = "application/vnd.rn-realmedia";
244 mime = "video/3gpp2";
247 mime = "application/octet-stream\r\nContent-Transfer-Encoding: binary";
250 postData = "--" + boundary + "\r\n" +
251 "Content-Disposition: form-data; name=\"" + kvp.Key + "\"; filename=\"" +
252 kvp.Value.Name + "\"\r\n" +
253 "Content-Type: " + mime + "\r\n\r\n";
254 byte[] postBytes = Encoding.UTF8.GetBytes(postData);
255 reqStream.Write(postBytes, 0, postBytes.Length);
256 //ファイルを読み出してHTTPのストリームに書き込み
257 kvp.Value.CopyTo(reqStream);
258 reqStream.Write(crlfByte, 0, crlfByte.Length);
262 byte[] endBytes = Encoding.UTF8.GetBytes("--" + boundary + "--\r\n");
263 reqStream.Write(endBytes, 0, endBytes.Length);
267 if (this.UseCookie) webReq.CookieContainer = this.cookieContainer;
269 webReq.Timeout = this.InstanceTimeout ?? (int)Networking.DefaultTimeout.TotalMilliseconds;
271 // KeepAlive無効なサーバー(Twitter等)に使用すると、タイムアウト後にWebExceptionが発生する場合あり
272 webReq.KeepAlive = false;
278 ///HTTPの応答を処理し、引数で指定されたストリームに書き込み
281 ///リダイレクト応答の場合(AllowAutoRedirect=Falseの場合のみ)は、headerInfoインスタンスがあればLocationを追加してリダイレクト先を返却
282 ///WebExceptionはハンドルしていないので、呼び出し元でキャッチすること
283 ///gzipファイルのダウンロードを想定しているため、他形式の場合は伸張時に問題が発生する可能性があります。
285 ///<param name="webRequest">HTTP通信リクエストオブジェクト</param>
286 ///<param name="contentStream">[OUT]HTTP応答のボディストリームのコピー先</param>
287 ///<param name="headerInfo">[IN/OUT]HTTP応答のヘッダ情報。ヘッダ名をキーにして空データのコレクションを渡すことで、該当ヘッダの値をデータに設定して戻す</param>
288 ///<returns>HTTP応答のステータスコード</returns>
289 protected HttpStatusCode GetResponse(HttpWebRequest webRequest,
290 Stream contentStream,
291 Dictionary<string, string> headerInfo)
295 using (HttpWebResponse webRes = (HttpWebResponse)webRequest.GetResponse())
297 HttpStatusCode statusCode = webRes.StatusCode;
299 if (this.UseCookie) this.FixCookies(webRes.Cookies);
300 //リダイレクト応答の場合は、リダイレクト先を設定
301 GetHeaderInfo(webRes, headerInfo);
303 if (webRes.ContentLength > 0)
305 //gzipなら応答ストリームの内容は伸張済み。それ以外なら伸張する。
306 if (webRes.ContentEncoding == "gzip" || webRes.ContentEncoding == "deflate")
308 using (Stream stream = webRes.GetResponseStream())
310 stream?.CopyTo(contentStream);
315 using (Stream stream = new GZipStream(webRes.GetResponseStream(), CompressionMode.Decompress))
317 stream?.CopyTo(contentStream);
324 catch (WebException ex)
326 if (ex.Status == WebExceptionStatus.ProtocolError)
328 HttpWebResponse res = (HttpWebResponse)ex.Response;
329 GetHeaderInfo(res, headerInfo);
330 return res.StatusCode;
337 ///HTTPの応答を処理し、応答ボディデータをテキストとして返却する
340 ///リダイレクト応答の場合(AllowAutoRedirect=Falseの場合のみ)は、headerInfoインスタンスがあればLocationを追加してリダイレクト先を返却
341 ///WebExceptionはハンドルしていないので、呼び出し元でキャッチすること
342 ///テキストの文字コードはUTF-8を前提として、エンコードはしていません
344 ///<param name="webRequest">HTTP通信リクエストオブジェクト</param>
345 ///<param name="contentText">[OUT]HTTP応答のボディデータ</param>
346 ///<param name="headerInfo">[IN/OUT]HTTP応答のヘッダ情報。ヘッダ名をキーにして空データのコレクションを渡すことで、該当ヘッダの値をデータに設定して戻す</param>
347 ///<returns>HTTP応答のステータスコード</returns>
348 protected HttpStatusCode GetResponse(HttpWebRequest webRequest,
349 out string contentText,
350 Dictionary<string, string> headerInfo)
354 using (HttpWebResponse webRes = (HttpWebResponse)webRequest.GetResponse())
356 HttpStatusCode statusCode = webRes.StatusCode;
358 if (this.UseCookie) this.FixCookies(webRes.Cookies);
359 //リダイレクト応答の場合は、リダイレクト先を設定
360 GetHeaderInfo(webRes, headerInfo);
362 using (StreamReader sr = new StreamReader(webRes.GetResponseStream()))
364 contentText = sr.ReadToEnd();
369 catch (WebException ex)
371 if (ex.Status == WebExceptionStatus.ProtocolError)
373 HttpWebResponse res = (HttpWebResponse)ex.Response;
374 GetHeaderInfo(res, headerInfo);
375 using (StreamReader sr = new StreamReader(res.GetResponseStream()))
377 contentText = sr.ReadToEnd();
379 return res.StatusCode;
386 ///HTTPの応答を処理します。応答ボディデータが不要な用途向け。
389 ///リダイレクト応答の場合(AllowAutoRedirect=Falseの場合のみ)は、headerInfoインスタンスがあればLocationを追加してリダイレクト先を返却
390 ///WebExceptionはハンドルしていないので、呼び出し元でキャッチすること
392 ///<param name="webRequest">HTTP通信リクエストオブジェクト</param>
393 ///<param name="headerInfo">[IN/OUT]HTTP応答のヘッダ情報。ヘッダ名をキーにして空データのコレクションを渡すことで、該当ヘッダの値をデータに設定して戻す</param>
394 ///<returns>HTTP応答のステータスコード</returns>
395 protected HttpStatusCode GetResponse(HttpWebRequest webRequest,
396 Dictionary<string, string> headerInfo)
400 using (HttpWebResponse webRes = (HttpWebResponse)webRequest.GetResponse())
402 HttpStatusCode statusCode = webRes.StatusCode;
404 if (this.UseCookie) this.FixCookies(webRes.Cookies);
405 //リダイレクト応答の場合は、リダイレクト先を設定
406 GetHeaderInfo(webRes, headerInfo);
410 catch (WebException ex)
412 if (ex.Status == WebExceptionStatus.ProtocolError)
414 HttpWebResponse res = (HttpWebResponse)ex.Response;
415 GetHeaderInfo(res, headerInfo);
416 return res.StatusCode;
423 ///HTTPの応答を処理し、応答ボディデータをBitmapとして返却します
426 ///リダイレクト応答の場合(AllowAutoRedirect=Falseの場合のみ)は、headerInfoインスタンスがあればLocationを追加してリダイレクト先を返却
427 ///WebExceptionはハンドルしていないので、呼び出し元でキャッチすること
429 ///<param name="webRequest">HTTP通信リクエストオブジェクト</param>
430 ///<param name="contentBitmap">[OUT]HTTP応答のボディデータを書き込むBitmap</param>
431 ///<param name="headerInfo">[IN/OUT]HTTP応答のヘッダ情報。ヘッダ名をキーにして空データのコレクションを渡すことで、該当ヘッダの値をデータに設定して戻す</param>
432 ///<returns>HTTP応答のステータスコード</returns>
433 protected HttpStatusCode GetResponse(HttpWebRequest webRequest,
434 out Bitmap contentBitmap,
435 Dictionary<string, string> headerInfo)
439 using (HttpWebResponse webRes = (HttpWebResponse)webRequest.GetResponse())
441 HttpStatusCode statusCode = webRes.StatusCode;
443 if (this.UseCookie) this.FixCookies(webRes.Cookies);
444 //リダイレクト応答の場合は、リダイレクト先を設定
445 GetHeaderInfo(webRes, headerInfo);
446 //応答のストリームをBitmapにして戻す
447 //if (webRes.ContentLength > 0) contentBitmap = new Bitmap(webRes.GetResponseStream());
448 contentBitmap = new Bitmap(webRes.GetResponseStream());
452 catch (WebException ex)
454 if (ex.Status == WebExceptionStatus.ProtocolError)
456 HttpWebResponse res = (HttpWebResponse)ex.Response;
457 GetHeaderInfo(res, headerInfo);
458 contentBitmap = null;
459 return res.StatusCode;
466 /// ホスト名なしのドメインはドメイン名から先頭のドットを除去しないと再利用されないため修正して追加する
468 private void FixCookies(CookieCollection cookieCollection)
470 foreach (Cookie ck in cookieCollection)
472 if (ck.Domain.StartsWith(".", StringComparison.Ordinal))
474 ck.Domain = ck.Domain.Substring(1);
475 cookieContainer.Add(ck);
481 ///headerInfoのキー情報で指定されたHTTPヘッダ情報を取得・格納する。redirect応答時はLocationヘッダの内容を追記する
483 ///<param name="webResponse">HTTP応答</param>
484 ///<param name="headerInfo">[IN/OUT]キーにヘッダ名を指定したデータ空のコレクション。取得した値をデータにセットして戻す</param>
485 private void GetHeaderInfo(HttpWebResponse webResponse,
486 Dictionary<string, string> headerInfo)
488 if (headerInfo == null) return;
490 if (headerInfo.Count > 0)
492 var headers = webResponse.Headers;
493 var dictKeys = new string[headerInfo.Count];
494 headerInfo.Keys.CopyTo(dictKeys, 0);
496 foreach (var key in dictKeys)
498 var value = headers[key];
499 headerInfo[key] = value ?? "";
503 HttpStatusCode statusCode = webResponse.StatusCode;
504 if (statusCode == HttpStatusCode.MovedPermanently ||
505 statusCode == HttpStatusCode.Found ||
506 statusCode == HttpStatusCode.SeeOther ||
507 statusCode == HttpStatusCode.TemporaryRedirect)
509 if (webResponse.Headers["Location"] != null)
511 headerInfo["Location"] = webResponse.Headers["Location"];
517 ///クエリ形式(key1=value1&key2=value2&...)の文字列をkey-valueコレクションに詰め直し
519 ///<param name="queryString">クエリ文字列</param>
520 ///<returns>key-valueのコレクション</returns>
521 protected NameValueCollection ParseQueryString(string queryString)
523 NameValueCollection query = new NameValueCollection();
524 string[] parts = queryString.Split('&');
525 foreach (string part in parts)
527 int index = part.IndexOf('=');
529 query.Add(Uri.UnescapeDataString(part), "");
531 query.Add(Uri.UnescapeDataString(part.Substring(0, index)), Uri.UnescapeDataString(part.Substring(index + 1)));
536 #region "InstanceTimeout"
540 private int? _timeout = null;
543 ///通信タイムアウト時間(ms)。10~120秒の範囲で指定。範囲外は20秒とする
545 protected int? InstanceTimeout
547 get { return _timeout; }
550 const int TimeoutMinValue = 10000;
551 const int TimeoutMaxValue = 120000;
552 if (value < TimeoutMinValue || value > TimeoutMaxValue)
553 throw new ArgumentOutOfRangeException(nameof(value), "Set " + TimeoutMinValue + "-" + TimeoutMaxValue + ": Value=" + value);