OSDN Git Service

HttpTwitter.GetConfigurationメソッドをTwitterApiクラスに置き換え
[opentween/open-tween.git] / OpenTween / Connection / HttpConnection.cs
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.
9 // 
10 // This file is part of OpenTween.
11 // 
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)
15 // any later version.
16 // 
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
20 // for more details. 
21 // 
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.
26
27 using System.Collections.Specialized;
28 using System.IO;
29 using System.Net;
30 using System.Net.Http;
31 using System.Text;
32 using System.Threading;
33 using System;
34 using System.Collections.Generic;
35 using System.IO.Compression;
36 using System.Drawing;
37 using OpenTween.Connection;
38 using System.Net.Cache;
39
40 ///<summary>
41 ///HttpWebRequest,HttpWebResponseを使用した基本的な通信機能を提供する
42 ///</summary>
43 ///<remarks>
44 ///プロキシ情報などを設定するため、使用前に静的メソッドInitializeConnectionを呼び出すこと。
45 ///通信方式によって必要になるHTTPヘッダの付加などは、派生クラスで行う。
46 ///</remarks>
47 namespace OpenTween
48 {
49     public class HttpConnection
50     {
51         /// <summary>
52         /// キャッシュを有効にするか否か
53         /// </summary>
54         public bool CacheEnabled { get; set; } = true;
55
56         /// <summary>
57         /// リクエスト間で Cookie を保持するか否か
58         /// </summary>
59         public bool UseCookie { get; set; }
60
61         /// <summary>
62         /// クッキー保存用コンテナ
63         /// </summary>
64         private CookieContainer cookieContainer = new CookieContainer();
65
66         protected const string PostMethod = "POST";
67         protected const string GetMethod = "GET";
68         protected const string HeadMethod = "HEAD";
69
70         ///<summary>
71         ///HttpWebRequestオブジェクトを取得する。パラメータはGET/HEAD/DELETEではクエリに、POST/PUTではエンティティボディに変換される。
72         ///</summary>
73         ///<remarks>
74         ///追加で必要となるHTTPヘッダや通信オプションは呼び出し元で付加すること
75         ///(Timeout,AutomaticDecompression,AllowAutoRedirect,UserAgent,ContentType,Accept,HttpRequestHeader.Authorization,カスタムヘッダ)
76         ///POST/PUTでクエリが必要な場合は、requestUriに含めること。
77         ///</remarks>
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,
84                                                Uri requestUri,
85                                                Dictionary<string, string> param,
86                                                bool gzip = false)
87         {
88             Networking.CheckInitialized();
89
90             //GETメソッドの場合はクエリとurlを結合
91             UriBuilder ub = new UriBuilder(requestUri.AbsoluteUri);
92             if (param != null && (method == "GET" || method == "DELETE" || method == "HEAD"))
93             {
94                 ub.Query = MyCommon.BuildQueryString(param);
95             }
96
97             HttpWebRequest webReq = (HttpWebRequest)WebRequest.Create(ub.Uri);
98
99             webReq.ReadWriteTimeout = 90 * 1000; //Streamの読み込みは90秒でタイムアウト(デフォルト5分)
100
101             //プロキシ設定
102             if (Networking.ProxyType != ProxyType.IE) webReq.Proxy = Networking.Proxy;
103
104             if (gzip)
105             {
106                 // Accept-Encodingヘッダを付加
107                 webReq.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip;
108             }
109
110             webReq.Method = method;
111             if (method == "POST" || method == "PUT")
112             {
113                 webReq.ContentType = "application/x-www-form-urlencoded";
114                 //POST/PUTメソッドの場合は、ボディデータとしてクエリ構成して書き込み
115                 using (StreamWriter writer = new StreamWriter(webReq.GetRequestStream()))
116                 {
117                     writer.Write(MyCommon.BuildQueryString(param));
118                 }
119             }
120             //cookie設定
121             if (this.UseCookie) webReq.CookieContainer = this.cookieContainer;
122             //タイムアウト設定
123             webReq.Timeout = this.InstanceTimeout ?? (int)Networking.DefaultTimeout.TotalMilliseconds;
124
125             webReq.UserAgent = Networking.GetUserAgentString();
126
127             // KeepAlive無効なサーバー(Twitter等)に使用すると、タイムアウト後にWebExceptionが発生する場合あり
128             webReq.KeepAlive = false;
129
130             if (!this.CacheEnabled)
131                 webReq.CachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache);
132
133             return webReq;
134         }
135
136         ///<summary>
137         ///HttpWebRequestオブジェクトを取得する。multipartでのバイナリアップロード用。
138         ///</summary>
139         ///<remarks>
140         ///methodにはPOST/PUTのみ指定可能
141         ///</remarks>
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,
148                                                Uri requestUri,
149                                                Dictionary<string, string> param,
150                                                List<KeyValuePair<String, IMediaItem>> binaryFileInfo)
151         {
152             Networking.CheckInitialized();
153
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");
160
161             HttpWebRequest webReq = (HttpWebRequest)WebRequest.Create(ub.Uri);
162
163             //プロキシ設定
164             if (Networking.ProxyType != ProxyType.IE) webReq.Proxy = Networking.Proxy;
165
166             webReq.Method = method;
167             if (method == "POST" || method == "PUT")
168             {
169                 string boundary = System.Environment.TickCount.ToString();
170                 webReq.ContentType = "multipart/form-data; boundary=" + boundary;
171                 using (Stream reqStream = webReq.GetRequestStream())
172                 {
173                     //POST送信する文字データを作成
174                     if (param != null)
175                     {
176                         string postData = "";
177                         foreach (KeyValuePair<string, string> kvp in param)
178                         {
179                             postData += "--" + boundary + "\r\n" +
180                                     "Content-Disposition: form-data; name=\"" + kvp.Key + "\"" +
181                                     "\r\n\r\n" + kvp.Value + "\r\n";
182                         }
183                         byte[] postBytes = Encoding.UTF8.GetBytes(postData);
184                         reqStream.Write(postBytes, 0, postBytes.Length);
185                     }
186                     //POST送信するバイナリデータを作成
187                     if (binaryFileInfo != null)
188                     {
189                         foreach (KeyValuePair<string, IMediaItem> kvp in binaryFileInfo)
190                         {
191                             string postData = "";
192                             byte[] crlfByte = Encoding.UTF8.GetBytes("\r\n");
193                             //コンテンツタイプの指定
194                             string mime = "";
195                             switch (kvp.Value.Extension.ToLowerInvariant())
196                             {
197                                 case ".jpg":
198                                 case ".jpeg":
199                                 case ".jpe":
200                                     mime = "image/jpeg";
201                                     break;
202                                 case ".gif":
203                                     mime = "image/gif";
204                                     break;
205                                 case ".png":
206                                     mime = "image/png";
207                                     break;
208                                 case ".tiff":
209                                 case ".tif":
210                                     mime = "image/tiff";
211                                     break;
212                                 case ".bmp":
213                                     mime = "image/x-bmp";
214                                     break;
215                                 case ".avi":
216                                     mime = "video/avi";
217                                     break;
218                                 case ".wmv":
219                                     mime = "video/x-ms-wmv";
220                                     break;
221                                 case ".flv":
222                                     mime = "video/x-flv";
223                                     break;
224                                 case ".m4v":
225                                     mime = "video/x-m4v";
226                                     break;
227                                 case ".mov":
228                                     mime = "video/quicktime";
229                                     break;
230                                 case ".mp4":
231                                     mime = "video/3gpp";
232                                     break;
233                                 case ".rm":
234                                     mime = "application/vnd.rn-realmedia";
235                                     break;
236                                 case ".mpeg":
237                                 case ".mpg":
238                                     mime = "video/mpeg";
239                                     break;
240                                 case ".3gp":
241                                     mime = "movie/3gp";
242                                     break;
243                                 case ".3g2":
244                                     mime = "video/3gpp2";
245                                     break;
246                                 default:
247                                     mime = "application/octet-stream\r\nContent-Transfer-Encoding: binary";
248                                     break;
249                             }
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);
259                         }
260                     }
261                     //終端
262                     byte[] endBytes = Encoding.UTF8.GetBytes("--" + boundary + "--\r\n");
263                     reqStream.Write(endBytes, 0, endBytes.Length);
264                 }
265             }
266             //cookie設定
267             if (this.UseCookie) webReq.CookieContainer = this.cookieContainer;
268             //タイムアウト設定
269             webReq.Timeout = this.InstanceTimeout ?? (int)Networking.DefaultTimeout.TotalMilliseconds;
270
271             // KeepAlive無効なサーバー(Twitter等)に使用すると、タイムアウト後にWebExceptionが発生する場合あり
272             webReq.KeepAlive = false;
273
274             return webReq;
275         }
276
277         ///<summary>
278         ///HTTPの応答を処理し、引数で指定されたストリームに書き込み
279         ///</summary>
280         ///<remarks>
281         ///リダイレクト応答の場合(AllowAutoRedirect=Falseの場合のみ)は、headerInfoインスタンスがあればLocationを追加してリダイレクト先を返却
282         ///WebExceptionはハンドルしていないので、呼び出し元でキャッチすること
283         ///gzipファイルのダウンロードを想定しているため、他形式の場合は伸張時に問題が発生する可能性があります。
284         ///</remarks>
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)
292         {
293             try
294             {
295                 using (HttpWebResponse webRes = (HttpWebResponse)webRequest.GetResponse())
296                 {
297                     HttpStatusCode statusCode = webRes.StatusCode;
298                     //cookie保持
299                     if (this.UseCookie) this.FixCookies(webRes.Cookies);
300                     //リダイレクト応答の場合は、リダイレクト先を設定
301                     GetHeaderInfo(webRes, headerInfo);
302                     //応答のストリームをコピーして戻す
303                     if (webRes.ContentLength > 0)
304                     {
305                         //gzipなら応答ストリームの内容は伸張済み。それ以外なら伸張する。
306                         if (webRes.ContentEncoding == "gzip" || webRes.ContentEncoding == "deflate")
307                         {
308                             using (Stream stream = webRes.GetResponseStream())
309                             {
310                                 stream?.CopyTo(contentStream);
311                             }
312                         }
313                         else
314                         {
315                             using (Stream stream = new GZipStream(webRes.GetResponseStream(), CompressionMode.Decompress))
316                             {
317                                 stream?.CopyTo(contentStream);
318                             }
319                         }
320                     }
321                     return statusCode;
322                 }
323             }
324             catch (WebException ex)
325             {
326                 if (ex.Status == WebExceptionStatus.ProtocolError)
327                 {
328                     HttpWebResponse res = (HttpWebResponse)ex.Response;
329                     GetHeaderInfo(res, headerInfo);
330                     return res.StatusCode;
331                 }
332                 throw;
333             }
334         }
335
336         ///<summary>
337         ///HTTPの応答を処理し、応答ボディデータをテキストとして返却する
338         ///</summary>
339         ///<remarks>
340         ///リダイレクト応答の場合(AllowAutoRedirect=Falseの場合のみ)は、headerInfoインスタンスがあればLocationを追加してリダイレクト先を返却
341         ///WebExceptionはハンドルしていないので、呼び出し元でキャッチすること
342         ///テキストの文字コードはUTF-8を前提として、エンコードはしていません
343         ///</remarks>
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)
351         {
352             try
353             {
354                 using (HttpWebResponse webRes = (HttpWebResponse)webRequest.GetResponse())
355                 {
356                     HttpStatusCode statusCode = webRes.StatusCode;
357                     //cookie保持
358                     if (this.UseCookie) this.FixCookies(webRes.Cookies);
359                     //リダイレクト応答の場合は、リダイレクト先を設定
360                     GetHeaderInfo(webRes, headerInfo);
361                     //応答のストリームをテキストに書き出し
362                     using (StreamReader sr = new StreamReader(webRes.GetResponseStream()))
363                     {
364                         contentText = sr.ReadToEnd();
365                     }
366                     return statusCode;
367                 }
368             }
369             catch (WebException ex)
370             {
371                 if (ex.Status == WebExceptionStatus.ProtocolError)
372                 {
373                     HttpWebResponse res = (HttpWebResponse)ex.Response;
374                     GetHeaderInfo(res, headerInfo);
375                     using (StreamReader sr = new StreamReader(res.GetResponseStream()))
376                     {
377                         contentText = sr.ReadToEnd();
378                     }
379                     return res.StatusCode;
380                 }
381                 throw;
382             }
383         }
384
385         ///<summary>
386         ///HTTPの応答を処理します。応答ボディデータが不要な用途向け。
387         ///</summary>
388         ///<remarks>
389         ///リダイレクト応答の場合(AllowAutoRedirect=Falseの場合のみ)は、headerInfoインスタンスがあればLocationを追加してリダイレクト先を返却
390         ///WebExceptionはハンドルしていないので、呼び出し元でキャッチすること
391         ///</remarks>
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)
397         {
398             try
399             {
400                 using (HttpWebResponse webRes = (HttpWebResponse)webRequest.GetResponse())
401                 {
402                     HttpStatusCode statusCode = webRes.StatusCode;
403                     //cookie保持
404                     if (this.UseCookie) this.FixCookies(webRes.Cookies);
405                     //リダイレクト応答の場合は、リダイレクト先を設定
406                     GetHeaderInfo(webRes, headerInfo);
407                     return statusCode;
408                 }
409             }
410             catch (WebException ex)
411             {
412                 if (ex.Status == WebExceptionStatus.ProtocolError)
413                 {
414                     HttpWebResponse res = (HttpWebResponse)ex.Response;
415                     GetHeaderInfo(res, headerInfo);
416                     return res.StatusCode;
417                 }
418                 throw;
419             }
420         }
421
422         ///<summary>
423         ///HTTPの応答を処理し、応答ボディデータをBitmapとして返却します
424         ///</summary>
425         ///<remarks>
426         ///リダイレクト応答の場合(AllowAutoRedirect=Falseの場合のみ)は、headerInfoインスタンスがあればLocationを追加してリダイレクト先を返却
427         ///WebExceptionはハンドルしていないので、呼び出し元でキャッチすること
428         ///</remarks>
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)
436         {
437             try
438             {
439                 using (HttpWebResponse webRes = (HttpWebResponse)webRequest.GetResponse())
440                 {
441                     HttpStatusCode statusCode = webRes.StatusCode;
442                     //cookie保持
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());
449                     return statusCode;
450                 }
451             }
452             catch (WebException ex)
453             {
454                 if (ex.Status == WebExceptionStatus.ProtocolError)
455                 {
456                     HttpWebResponse res = (HttpWebResponse)ex.Response;
457                     GetHeaderInfo(res, headerInfo);
458                     contentBitmap = null;
459                     return res.StatusCode;
460                 }
461                 throw;
462             }
463         }
464
465         /// <summary>
466         /// ホスト名なしのドメインはドメイン名から先頭のドットを除去しないと再利用されないため修正して追加する
467         /// </summary>
468         private void FixCookies(CookieCollection cookieCollection)
469         {
470             foreach (Cookie ck in cookieCollection)
471             {
472                 if (ck.Domain.StartsWith(".", StringComparison.Ordinal))
473                 {
474                     ck.Domain = ck.Domain.Substring(1);
475                     cookieContainer.Add(ck);
476                 }
477             }
478         }
479
480         ///<summary>
481         ///headerInfoのキー情報で指定されたHTTPヘッダ情報を取得・格納する。redirect応答時はLocationヘッダの内容を追記する
482         ///</summary>
483         ///<param name="webResponse">HTTP応答</param>
484         ///<param name="headerInfo">[IN/OUT]キーにヘッダ名を指定したデータ空のコレクション。取得した値をデータにセットして戻す</param>
485         private void GetHeaderInfo(HttpWebResponse webResponse,
486                                    Dictionary<string, string> headerInfo)
487         {
488             if (headerInfo == null) return;
489
490             if (headerInfo.Count > 0)
491             {
492                 var headers = webResponse.Headers;
493                 var dictKeys = new string[headerInfo.Count];
494                 headerInfo.Keys.CopyTo(dictKeys, 0);
495
496                 foreach (var key in dictKeys)
497                 {
498                     var value = headers[key];
499                     headerInfo[key] = value ?? "";
500                 }
501             }
502
503             HttpStatusCode statusCode = webResponse.StatusCode;
504             if (statusCode == HttpStatusCode.MovedPermanently ||
505                 statusCode == HttpStatusCode.Found ||
506                 statusCode == HttpStatusCode.SeeOther ||
507                 statusCode == HttpStatusCode.TemporaryRedirect)
508             {
509                 if (webResponse.Headers["Location"] != null)
510                 {
511                     headerInfo["Location"] = webResponse.Headers["Location"];
512                 }
513             }
514         }
515
516         ///<summary>
517         ///クエリ形式(key1=value1&amp;key2=value2&amp;...)の文字列をkey-valueコレクションに詰め直し
518         ///</summary>
519         ///<param name="queryString">クエリ文字列</param>
520         ///<returns>key-valueのコレクション</returns>
521         protected NameValueCollection ParseQueryString(string queryString)
522         {
523             NameValueCollection query = new NameValueCollection();
524             string[] parts = queryString.Split('&');
525             foreach (string part in parts)
526             {
527                 int index = part.IndexOf('=');
528                 if (index == -1)
529                     query.Add(Uri.UnescapeDataString(part), "");
530                 else
531                     query.Add(Uri.UnescapeDataString(part.Substring(0, index)), Uri.UnescapeDataString(part.Substring(index + 1)));
532             }
533             return query;
534         }
535
536         #region "InstanceTimeout"
537         ///<summary>
538         ///通信タイムアウト時間(ms)
539         ///</summary>
540         private int? _timeout = null;
541
542         ///<summary>
543         ///通信タイムアウト時間(ms)。10~120秒の範囲で指定。範囲外は20秒とする
544         ///</summary>
545         protected int? InstanceTimeout
546         {
547             get { return _timeout; }
548             set
549             {
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);
554                 else
555                     _timeout = value;
556             }
557         }
558         #endregion
559     }
560 }