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 spinor (@tplantd) <http://d.hatena.ne.jp/spinor/>
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 HttpConnection = OpenTween.HttpConnection;
28 using IHttpConnection = OpenTween.IHttpConnection;
29 using DateTime = System.DateTime;
30 using DateTimeKind = System.DateTimeKind;
31 using Random = System.Random;
32 using HttpWebRequest = System.Net.HttpWebRequest;
33 using HttpStatusCode = System.Net.HttpStatusCode;
34 using Uri = System.Uri;
35 using System.Collections.Generic; // for Dictionary<TKey, TValue>, List<T>, KeyValuePair<TKey, TValue>, SortedDictionary<TKey, TValue>
36 using CallbackDelegate = OpenTween.CallbackDelegate;
37 using StackFrame = System.Diagnostics.StackFrame;
38 using FileInfo = System.IO.FileInfo;
39 using Stream = System.IO.Stream;
40 using HttpWebResponse = System.Net.HttpWebResponse;
41 using WebException = System.Net.WebException;
42 using WebExceptionStatus = System.Net.WebExceptionStatus;
43 using Exception = System.Exception;
44 using NameValueCollection = System.Collections.Specialized.NameValueCollection;
45 using Convert = System.Convert;
46 using InvalidDataException = System.IO.InvalidDataException;
47 using UriBuilder = System.UriBuilder;
48 using Environment = System.Environment;
49 using StringBuilder = System.Text.StringBuilder;
50 using HttpRequestHeader = System.Net.HttpRequestHeader;
51 using HMACSHA1 = System.Security.Cryptography.HMACSHA1;
52 using Encoding = System.Text.Encoding;
58 /// OAuth認証を使用するHTTP通信。HMAC-SHA1固定
61 /// 使用前に認証情報を設定する。認証確認を伴う場合はAuthenticate系のメソッドを、認証不要な場合はInitializeを呼ぶこと。
63 abstract public class HttpConnectionOAuth : HttpConnection, IHttpConnection
66 /// OAuth署名のoauth_timestamp算出用基準日付(1970/1/1 00:00:00)
68 private static readonly DateTime UnixEpoch = new DateTime( 1970, 1, 1, 0, 0, 0, DateTimeKind.Unspecified );
71 /// OAuth署名のoauth_nonce算出用乱数クラス
73 private static readonly Random NonceRandom = new Random();
76 /// OAuthのアクセストークン。永続化可能(ユーザー取り消しの可能性はある)。
78 private string token = "";
81 /// OAuthの署名作成用秘密アクセストークン。永続化可能(ユーザー取り消しの可能性はある)。
83 private string tokenSecret = "";
88 private string consumerKey;
91 /// OAuthの署名作成用秘密コンシューマーデータ
93 protected string consumerSecret;
96 /// 認証成功時の応答でユーザー情報を取得する場合のキー。設定しない場合は、AuthUsernameもブランクのままとなる
98 private string userIdentKey = "";
101 /// 認証成功時の応答でユーザーID情報を取得する場合のキー。設定しない場合は、AuthUserIdもブランクのままとなる
103 private string userIdIdentKey = "";
106 /// 認証完了時の応答からuserIdentKey情報に基づいて取得するユーザー情報
108 private string authorizedUsername = "";
111 /// 認証完了時の応答からuserIdentKey情報に基づいて取得するユーザー情報
113 private long authorizedUserId;
116 /// Stream用のHttpWebRequest
118 private HttpWebRequest streamReq = null;
121 /// OAuth認証で指定のURLとHTTP通信を行い、結果を返す
123 /// <param name="method">HTTP通信メソッド(GET/HEAD/POST/PUT/DELETE)</param>
124 /// <param name="requestUri">通信先URI</param>
125 /// <param name="param">GET時のクエリ、またはPOST時のエンティティボディ</param>
126 /// <param name="content">[OUT]HTTP応答のボディデータ</param>
127 /// <param name="headerInfo">[IN/OUT]HTTP応答のヘッダ情報。必要なヘッダ名を事前に設定しておくこと</param>
128 /// <param name="callback">処理終了直前に呼ばれるコールバック関数のデリゲート 不要な場合はnullを渡すこと</param>
129 /// <returns>HTTP応答のステータスコード</returns>
130 public HttpStatusCode GetContent( string method,
132 Dictionary< string, string > param,
134 Dictionary< string, string > headerInfo,
135 CallbackDelegate callback )
138 if ( string.IsNullOrEmpty( token ) )
139 return HttpStatusCode.Unauthorized;
141 HttpWebRequest webReq = this.CreateRequest( method, requestUri, param, gzip: true );
143 this.AppendOAuthInfo( webReq, param, token, tokenSecret );
146 if ( content == null )
147 code = this.GetResponse( webReq, headerInfo );
149 code = this.GetResponse( webReq, out content, headerInfo );
151 if ( callback != null )
153 StackFrame frame = new StackFrame( 1 );
154 callback( frame.GetMethod().Name, code, headerInfo, content );
162 public HttpStatusCode GetContent( string method,
164 Dictionary< string, string > param,
165 List< KeyValuePair< string, IMediaItem > > binary,
167 Dictionary< string, string > headerInfo,
168 CallbackDelegate callback )
171 if ( string.IsNullOrEmpty( token ) )
172 return HttpStatusCode.Unauthorized;
174 HttpWebRequest webReq = this.CreateRequest( method, requestUri, param, binary );
176 this.AppendOAuthInfo( webReq, null, token, tokenSecret );
179 if ( content == null )
180 code = this.GetResponse( webReq, headerInfo );
182 code = this.GetResponse( webReq, out content, headerInfo );
184 if ( callback != null )
186 StackFrame frame = new StackFrame( 1 );
187 callback( frame.GetMethod().Name, code, headerInfo, content );
193 /// OAuth認証で指定のURLとHTTP通信を行い、ストリームを返す
195 /// <param name="method">HTTP通信メソッド(GET/HEAD/POST/PUT/DELETE)</param>
196 /// <param name="requestUri">通信先URI</param>
197 /// <param name="param">GET時のクエリ、またはPOST時のエンティティボディ</param>
198 /// <param name="content">[OUT]HTTP応答のボディストリーム</param>
199 /// <returns>HTTP応答のステータスコード</returns>
200 public HttpStatusCode GetContent( string method,
202 Dictionary< string, string > param,
207 if ( string.IsNullOrEmpty( token ) )
208 return HttpStatusCode.Unauthorized;
211 this.streamReq = this.CreateRequest( method, requestUri, param );
212 // User-Agent指定がある場合は付加
213 if ( !string.IsNullOrEmpty( userAgent ) )
214 this.streamReq.UserAgent = userAgent;
217 this.AppendOAuthInfo( this.streamReq, param, token, tokenSecret );
221 HttpWebResponse webRes = (HttpWebResponse)this.streamReq.GetResponse();
222 content = webRes.GetResponseStream();
223 return webRes.StatusCode;
225 catch ( WebException ex )
227 if ( ex.Status == WebExceptionStatus.ProtocolError )
229 HttpWebResponse res = (HttpWebResponse)ex.Response;
230 return res.StatusCode;
236 public void RequestAbort()
240 if ( this.streamReq != null )
242 this.streamReq.Abort();
243 this.streamReq = null;
246 catch ( Exception ) {}
251 /// OAuth認証の開始要求(リクエストトークン取得)。PIN入力用の前段
254 /// 呼び出し元では戻されたurlをブラウザで開き、認証完了後PIN入力を受け付けて、リクエストトークンと共にAuthenticatePinFlowを呼び出す
256 /// <param name="requestTokenUrl">リクエストトークンの取得先URL</param>
257 /// <param name="authorizeUrl">ブラウザで開く認証用URLのベース</param>
258 /// <param name="requestToken">[OUT]認証要求で戻されるリクエストトークン。使い捨て</param>
259 /// <param name="authUri">[OUT]requestUriを元に生成された認証用URL。通常はリクエストトークンをクエリとして付加したUri</param>
260 /// <returns>取得結果真偽値</returns>
261 public bool AuthenticatePinFlowRequest( string requestTokenUrl, string authorizeUrl, ref string requestToken, ref Uri authUri )
264 authUri = this.GetAuthenticatePageUri( requestTokenUrl, authorizeUrl, ref requestToken );
265 if ( authUri == null )
271 /// OAuth認証のアクセストークン取得。PIN入力用の後段
274 /// 事前にAuthenticatePinFlowRequestを呼んで、ブラウザで認証後に表示されるPINを入力してもらい、その値とともに呼び出すこと
276 /// <param name="accessTokenUrl">アクセストークンの取得先URL</param>
277 /// <param name="requestToken">AuthenticatePinFlowRequestで取得したリクエストトークン</param>
278 /// <param name="pinCode">Webで認証後に表示されるPINコード</param>
279 /// <returns>取得結果真偽値</returns>
280 public HttpStatusCode AuthenticatePinFlow( string accessTokenUrl, string requestToken, string pinCode )
283 if ( string.IsNullOrEmpty( requestToken ) )
284 throw new InvalidOperationException( "Sequence error.(requestToken is blank)" );
288 NameValueCollection accessTokenData;
289 HttpStatusCode httpCode = this.GetOAuthToken( new Uri( accessTokenUrl ), pinCode, requestToken, null, ref content );
290 if ( httpCode != HttpStatusCode.OK )
292 accessTokenData = base.ParseQueryString( content );
294 if ( accessTokenData != null )
296 this.token = accessTokenData[ "oauth_token" ];
297 this.tokenSecret = accessTokenData[ "oauth_token_secret" ];
300 if ( !string.IsNullOrEmpty(this.userIdentKey) )
301 this.authorizedUsername = accessTokenData[ this.userIdentKey ];
303 this.authorizedUsername = "";
305 if ( !string.IsNullOrEmpty(this.userIdIdentKey) )
309 this.authorizedUserId = Convert.ToInt64( accessTokenData[ this.userIdIdentKey ] );
313 this.authorizedUserId = 0;
318 this.authorizedUserId = 0;
321 if ( string.IsNullOrEmpty(token) )
322 throw new InvalidDataException( "Token is null." );
323 return HttpStatusCode.OK;
327 throw new InvalidDataException( "Return value is null." );
331 public HttpStatusCode Authenticate(Uri accessTokenUrl, string username, string password, ref string content)
333 return this.AuthenticateXAuth(accessTokenUrl, username, password, ref content);
337 /// OAuth認証のアクセストークン取得。xAuth方式
339 /// <param name="accessTokenUrl">アクセストークンの取得先URL</param>
340 /// <param name="username">認証用ユーザー名</param>
341 /// <param name="password">認証用パスワード</param>
342 /// <returns>取得結果真偽値</returns>
343 public HttpStatusCode AuthenticateXAuth( Uri accessTokenUrl, string username, string password, ref string content )
346 if ( string.IsNullOrEmpty( username ) )
347 throw new ArgumentException( "username is null or empty", nameof(username) );
348 if ( string.IsNullOrEmpty( password ) )
349 throw new ArgumentException( "password is null or empty", nameof(password) );
352 Dictionary< string, string > parameter = new Dictionary< string, string >();
353 parameter.Add( "x_auth_mode", "client_auth" );
354 parameter.Add( "x_auth_username", username );
355 parameter.Add( "x_auth_password", password );
358 HttpStatusCode httpCode = this.GetOAuthToken( accessTokenUrl, "", "", parameter, ref content );
359 if ( httpCode != HttpStatusCode.OK )
361 NameValueCollection accessTokenData = base.ParseQueryString( content );
363 if ( accessTokenData != null )
365 this.token = accessTokenData[ "oauth_token" ];
366 this.tokenSecret = accessTokenData[ "oauth_token_secret" ];
369 if ( !string.IsNullOrEmpty(this.userIdentKey) )
370 this.authorizedUsername = accessTokenData[ this.userIdentKey ];
372 this.authorizedUsername = "";
374 if ( !string.IsNullOrEmpty(this.userIdIdentKey) )
378 this.authorizedUserId = Convert.ToInt64( accessTokenData[ this.userIdIdentKey ] );
382 this.authorizedUserId = 0;
387 this.authorizedUserId = 0;
390 if ( string.IsNullOrEmpty(token) )
391 throw new InvalidDataException( "Token is null." );
392 return HttpStatusCode.OK;
396 throw new InvalidDataException( "Return value is null." );
401 /// OAuth認証のリクエストトークン取得。リクエストトークンと組み合わせた認証用のUriも生成する
403 /// <param name="requestTokenUrl">リクエストトークンの取得先URL</param>
404 /// <param name="authorizeUrl">ブラウザで開く認証用URLのベース</param>
405 /// <param name="requestToken">[OUT]取得したリクエストトークン</param>
406 /// <returns>取得結果真偽値</returns>
407 private Uri GetAuthenticatePageUri( string requestTokenUrl, string authorizeUrl, ref string requestToken )
409 const string tokenKey = "oauth_token";
413 NameValueCollection reqTokenData;
414 if ( this.GetOAuthToken( new Uri( requestTokenUrl ), "", "", null, ref content, callbackUrl: "oob" ) != HttpStatusCode.OK )
416 reqTokenData = base.ParseQueryString( content );
418 if ( reqTokenData != null )
420 requestToken = reqTokenData[ tokenKey ];
422 UriBuilder ub = new UriBuilder( authorizeUrl );
423 ub.Query = string.Format( "{0}={1}", tokenKey, requestToken );
433 /// OAuth認証のトークン取得共通処理
435 /// <param name="requestUri">各種トークンの取得先URL</param>
436 /// <param name="pinCode">PINフロー時のアクセストークン取得時に設定。それ以外は空文字列</param>
437 /// <param name="requestToken">PINフロー時のリクエストトークン取得時に設定。それ以外は空文字列</param>
438 /// <param name="parameter">追加パラメータ。xAuthで使用</param>
439 /// <returns>取得結果のデータ。正しく取得出来なかった場合はNothing</returns>
440 private HttpStatusCode GetOAuthToken( Uri requestUri, string pinCode, string requestToken, Dictionary< string , string > parameter, ref string content, string callbackUrl = null )
442 HttpWebRequest webReq = null;
443 // HTTPリクエスト生成。PINコードもパラメータも未指定の場合はGETメソッドで通信。それ以外はPOST
444 if ( string.IsNullOrEmpty( pinCode ) && parameter != null )
445 webReq = this.CreateRequest( "GET", requestUri, null );
447 webReq = this.CreateRequest( "POST", requestUri, parameter ); // ボディに追加パラメータ書き込み
449 // OAuth関連パラメータ準備。追加パラメータがあれば追加
450 Dictionary< string, string > query = new Dictionary< string, string >();
451 if ( parameter != null )
452 foreach ( KeyValuePair< string, string > kvp in parameter )
453 query.Add( kvp.Key, kvp.Value );
455 // PINコードが指定されていればパラメータに追加
456 if ( ! string.IsNullOrEmpty( pinCode ) )
457 query.Add( "oauth_verifier", pinCode );
459 // コールバックURLが指定されていればパラメータに追加
460 if (!string.IsNullOrEmpty(callbackUrl))
461 query.Add("oauth_callback", callbackUrl);
463 // OAuth関連情報をHTTPリクエストに追加
464 this.AppendOAuthInfo( webReq, query, requestToken, "" );
467 Dictionary< string, string > header = new Dictionary< string, string >() { { "Date", "" } };
468 HttpStatusCode responseCode = this.GetResponse( webReq, out content, header );
469 if ( responseCode == HttpStatusCode.OK )
472 if ( !string.IsNullOrEmpty( header[ "Date" ] ) )
473 content += Environment.NewLine + "Check the Date & Time of this computer." + Environment.NewLine
474 + "Server:" + DateTime.Parse( header[ "Date" ] ).ToString() + " PC:" + DateTime.Now.ToString();
479 #region "OAuth認証用ヘッダ作成・付加処理"
481 /// HTTPリクエストにOAuth関連ヘッダを追加
483 /// <param name="webRequest">追加対象のHTTPリクエスト</param>
484 /// <param name="query">OAuth追加情報+クエリ or POSTデータ</param>
485 /// <param name="token">アクセストークン、もしくはリクエストトークン。未取得なら空文字列</param>
486 /// <param name="tokenSecret">アクセストークンシークレット。認証処理では空文字列</param>
487 protected virtual void AppendOAuthInfo( HttpWebRequest webRequest, Dictionary< string, string > query, string token, string tokenSecret )
490 Dictionary< string, string > parameter = this.GetOAuthParameter( token );
491 // OAuth共通情報にquery情報を追加
493 foreach ( KeyValuePair< string, string > item in query )
494 parameter.Add( item.Key, item.Value );
496 parameter.Add( "oauth_signature", this.CreateSignature( tokenSecret, webRequest.Method, webRequest.RequestUri, parameter ) );
498 StringBuilder sb = new StringBuilder( "OAuth " );
499 foreach ( KeyValuePair< string, string > item in parameter )
500 // 各種情報のうち、oauth_で始まる情報のみ、ヘッダに追加する。各情報はカンマ区切り、データはダブルクォーテーションで括る
501 if ( item.Key.StartsWith("oauth_") )
502 sb.AppendFormat( "{0}=\"{1}\",", item.Key, MyCommon.UrlEncode( item.Value ) );
503 webRequest.Headers.Add( HttpRequestHeader.Authorization, sb.ToString() );
507 /// OAuthで使用する共通情報を取得する
509 /// <param name="token">アクセストークン、もしくはリクエストトークン。未取得なら空文字列</param>
510 /// <returns>OAuth情報のディクショナリ</returns>
511 protected Dictionary< string, string > GetOAuthParameter( string token )
513 Dictionary< string, string > parameter = new Dictionary< string, string >();
514 parameter.Add( "oauth_consumer_key", this.consumerKey );
515 parameter.Add( "oauth_signature_method", "HMAC-SHA1" );
516 parameter.Add( "oauth_timestamp", Convert.ToInt64( ( DateTime.UtcNow - HttpConnectionOAuth.UnixEpoch ).TotalSeconds ).ToString() ); // epoch秒
517 parameter.Add( "oauth_nonce", HttpConnectionOAuth.NonceRandom.Next( 123400, 9999999 ).ToString() );
518 parameter.Add( "oauth_version", "1.0" );
519 if ( !string.IsNullOrEmpty( token ) )
520 parameter.Add( "oauth_token", token ); // トークンがあれば追加
527 /// <param name="tokenSecret">アクセストークン秘密鍵</param>
528 /// <param name="method">HTTPメソッド文字列</param>
529 /// <param name="uri">アクセス先Uri</param>
530 /// <param name="parameter">クエリ、もしくはPOSTデータ</param>
531 /// <returns>署名文字列</returns>
532 protected virtual string CreateSignature( string tokenSecret, string method, Uri uri, Dictionary< string, string > parameter )
534 // パラメタをソート済みディクショナリに詰替(OAuthの仕様)
535 SortedDictionary< string, string > sorted = new SortedDictionary< string, string >( parameter );
536 // URLエンコード済みのクエリ形式文字列に変換
537 string paramString = MyCommon.BuildQueryString( sorted );
539 string url = string.Format( "{0}://{1}{2}", uri.Scheme, uri.Host, uri.AbsolutePath );
540 // 署名のベース文字列生成(&区切り)。クエリ形式文字列は再エンコードする
541 string signatureBase = string.Format( "{0}&{1}&{2}", method, MyCommon.UrlEncode( url ), MyCommon.UrlEncode( paramString ) );
542 // 署名鍵の文字列をコンシューマー秘密鍵とアクセストークン秘密鍵から生成(&区切り。アクセストークン秘密鍵なくても&残すこと)
543 string key = MyCommon.UrlEncode( this.consumerSecret ) + "&";
544 if ( !string.IsNullOrEmpty( tokenSecret ) )
545 key += MyCommon.UrlEncode( tokenSecret );
547 using ( HMACSHA1 hmac = new HMACSHA1( Encoding.ASCII.GetBytes( key ) ) )
549 byte[] hash = hmac.ComputeHash( Encoding.ASCII.GetBytes( signatureBase ) );
550 return Convert.ToBase64String( hash );
553 #endregion // OAuth認証用ヘッダ作成・付加処理
556 /// 初期化。各種トークンの設定とユーザー識別情報設定
558 /// <param name="consumerKey">コンシューマー鍵</param>
559 /// <param name="consumerSecret">コンシューマー秘密鍵</param>
560 /// <param name="accessToken">アクセストークン</param>
561 /// <param name="accessTokenSecret">アクセストークン秘密鍵</param>
562 /// <param name="userIdentifier">アクセストークン取得時に得られるユーザー識別情報。不要なら空文字列</param>
563 public void Initialize( string consumerKey, string consumerSecret,
564 string accessToken, string accessTokenSecret,
565 string userIdentifier, string userIdIdentifier )
567 this.consumerKey = consumerKey;
568 this.consumerSecret = consumerSecret;
569 this.token = accessToken;
570 this.tokenSecret = accessTokenSecret;
571 this.userIdentKey = userIdentifier;
572 this.userIdIdentKey = userIdIdentifier;
576 /// 初期化。各種トークンの設定とユーザー識別情報設定
578 /// <param name="consumerKey">コンシューマー鍵</param>
579 /// <param name="consumerSecret">コンシューマー秘密鍵</param>
580 /// <param name="accessToken">アクセストークン</param>
581 /// <param name="accessTokenSecret">アクセストークン秘密鍵</param>
582 /// <param name="username">認証済みユーザー名</param>
583 /// <param name="userIdentifier">アクセストークン取得時に得られるユーザー識別情報。不要なら空文字列</param>
584 public void Initialize( string consumerKey, string consumerSecret,
585 string accessToken, string accessTokenSecret,
586 string username, long userId,
587 string userIdentifier, string userIdIdentifier )
589 this.Initialize( consumerKey, consumerSecret, accessToken, accessTokenSecret, userIdentifier, userIdIdentifier );
590 this.authorizedUsername = username;
591 this.authorizedUserId = userId;
597 public string AccessToken
599 get { return this.token; }
605 public string AccessTokenSecret
607 get { return this.tokenSecret; }
613 public string AuthUsername
615 get { return this.authorizedUsername; }
621 public long AuthUserId
623 get { return this.authorizedUserId; }
624 set { this.authorizedUserId = value; }