OSDN Git Service

a4ddda316927bce681da4fe3bac665a6e965fd45
[opentween/open-tween.git] / Tween / Connection / HttpConnectionOAuth.vb
1 Imports System.Net
2 Imports System.Collections.Generic
3 Imports System.Collections.Specialized
4 Imports System.IO
5 Imports System.Text
6 Imports System.Security
7 Imports System.Diagnostics
8
9 '''<summary>
10 '''OAuth認証を使用するHTTP通信。HMAC-SHA1固定
11 '''</summary>
12 '''<remarks>
13 '''使用前に認証情報を設定する。認証確認を伴う場合はAuthenticate系のメソッドを、認証不要な場合はInitializeを呼ぶこと。
14 '''</remarks>
15 Public Class HttpConnectionOAuth
16     Inherits HttpConnection
17     Implements IHttpConnection
18
19     '''<summary>
20     '''OAuth署名のoauth_timestamp算出用基準日付(1970/1/1 00:00:00)
21     '''</summary>
22     Private Shared ReadOnly UnixEpoch As New DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Unspecified)
23
24     '''<summary>
25     '''OAuth署名のoauth_nonce算出用乱数クラス
26     '''</summary>
27     Private Shared ReadOnly NonceRandom As New Random
28
29     '''<summary>
30     '''OAuthのアクセストークン。永続化可能(ユーザー取り消しの可能性はある)。
31     '''</summary>
32     Private token As String = ""
33
34     '''<summary>
35     '''OAuthの署名作成用秘密アクセストークン。永続化可能(ユーザー取り消しの可能性はある)。
36     '''</summary>
37     Private tokenSecret As String = ""
38
39     '''<summary>
40     '''OAuthのコンシューマー鍵
41     '''</summary>
42     Private consumerKey As String
43
44     '''<summary>
45     '''OAuthの署名作成用秘密コンシューマーデータ
46     '''</summary>
47     Protected consumerSecret As String
48
49     '''<summary>
50     '''認証成功時の応答でユーザー情報を取得する場合のキー。設定しない場合は、AuthUsernameもブランクのままとなる
51     '''</summary>
52     Private userIdentKey As String = ""
53
54     '''<summary>
55     '''認証完了時の応答からuserIdentKey情報に基づいて取得するユーザー情報
56     '''</summary>
57     Private authorizedUsername As String = ""
58
59     '''<summary>
60     '''認証完了時の応答からuserIdentKey情報に基づいて取得するユーザー情報
61     '''</summary>
62     Private streamReq As HttpWebRequest = Nothing
63
64     '''<summary>
65     '''OAuth認証で指定のURLとHTTP通信を行い、結果を返す
66     '''</summary>
67     '''<param name="method">HTTP通信メソッド(GET/HEAD/POST/PUT/DELETE)</param>
68     '''<param name="requestUri">通信先URI</param>
69     '''<param name="param">GET時のクエリ、またはPOST時のエンティティボディ</param>
70     '''<param name="content">[OUT]HTTP応答のボディデータ</param>
71     '''<param name="headerInfo">[IN/OUT]HTTP応答のヘッダ情報。必要なヘッダ名を事前に設定しておくこと</param>
72     '''<param name="callback">処理終了直前に呼ばれるコールバック関数のデリゲート 不要な場合はNothingを渡すこと</param>
73     '''<returns>HTTP応答のステータスコード</returns>
74     Public Function GetContent(ByVal method As String, _
75             ByVal requestUri As Uri, _
76             ByVal param As Dictionary(Of String, String), _
77             ByRef content As String, _
78             ByVal headerInfo As Dictionary(Of String, String), _
79             ByVal callback As IHttpConnection.CallbackDelegate) As HttpStatusCode Implements IHttpConnection.GetContent
80         '認証済かチェック
81         If String.IsNullOrEmpty(token) Then Return HttpStatusCode.Unauthorized
82
83         Dim webReq As HttpWebRequest = CreateRequest(method, _
84                                                     requestUri, _
85                                                     param, _
86                                                     False)
87         'OAuth認証ヘッダを付加
88         AppendOAuthInfo(webReq, param, token, tokenSecret)
89
90         Dim code As HttpStatusCode
91         If content Is Nothing Then
92             code = GetResponse(webReq, headerInfo, False)
93         Else
94             code = GetResponse(webReq, content, headerInfo, False)
95         End If
96         If callback IsNot Nothing Then
97             Dim frame As New StackFrame(1)
98             callback(frame.GetMethod.Name, code, content)
99         End If
100         Return code
101     End Function
102
103     '''<summary>
104     '''バイナリアップロード
105     '''</summary>
106     Public Function GetContent(ByVal method As String, _
107         ByVal requestUri As Uri, _
108         ByVal param As Dictionary(Of String, String), _
109         ByVal binary As List(Of KeyValuePair(Of String, FileInfo)), _
110         ByRef content As String, _
111         ByVal headerInfo As Dictionary(Of String, String), _
112         ByVal callback As IHttpConnection.CallbackDelegate) As HttpStatusCode Implements IHttpConnection.GetContent
113         '認証済かチェック
114         If String.IsNullOrEmpty(token) Then Return HttpStatusCode.Unauthorized
115
116         Dim webReq As HttpWebRequest = CreateRequest(method, _
117                                                     requestUri, _
118                                                     param, _
119                                                     binary, _
120                                                     False)
121         'OAuth認証ヘッダを付加
122         AppendOAuthInfo(webReq, Nothing, token, tokenSecret)
123
124         Dim code As HttpStatusCode
125         If content Is Nothing Then
126             code = GetResponse(webReq, headerInfo, False)
127         Else
128             code = GetResponse(webReq, content, headerInfo, False)
129         End If
130         If callback IsNot Nothing Then
131             Dim frame As New StackFrame(1)
132             callback(frame.GetMethod.Name, code, content)
133         End If
134         Return code
135     End Function
136
137     '''<summary>
138     '''OAuth認証で指定のURLとHTTP通信を行い、ストリームを返す
139     '''</summary>
140     '''<param name="method">HTTP通信メソッド(GET/HEAD/POST/PUT/DELETE)</param>
141     '''<param name="requestUri">通信先URI</param>
142     '''<param name="param">GET時のクエリ、またはPOST時のエンティティボディ</param>
143     '''<param name="content">[OUT]HTTP応答のボディストリーム</param>
144     '''<returns>HTTP応答のステータスコード</returns>
145     Public Function GetContent(ByVal method As String, _
146             ByVal requestUri As Uri, _
147             ByVal param As Dictionary(Of String, String), _
148             ByRef content As Stream) As HttpStatusCode Implements IHttpConnection.GetContent
149         '認証済かチェック
150         If String.IsNullOrEmpty(token) Then Return HttpStatusCode.Unauthorized
151
152         streamReq = CreateRequest(method, requestUri, param, False)
153         'OAuth認証ヘッダを付加
154         AppendOAuthInfo(streamReq, param, token, tokenSecret)
155
156         Try
157             Dim webRes As HttpWebResponse = CType(streamReq.GetResponse(), HttpWebResponse)
158             content = webRes.GetResponseStream()
159             Return webRes.StatusCode
160         Catch ex As WebException
161             If ex.Status = WebExceptionStatus.ProtocolError Then
162                 Dim res As HttpWebResponse = DirectCast(ex.Response, HttpWebResponse)
163                 Return res.StatusCode
164             End If
165             Throw ex
166         End Try
167
168     End Function
169
170     Public Sub RequestAbort() Implements IHttpConnection.RequestAbort
171         Try
172             If streamReq IsNot Nothing Then
173                 streamReq.Abort()
174             End If
175         Catch ex As Exception
176         End Try
177     End Sub
178
179
180 #Region "認証処理"
181     '''<summary>
182     '''OAuth認証の開始要求(リクエストトークン取得)。PIN入力用の前段
183     '''</summary>
184     '''<remarks>
185     '''呼び出し元では戻されたurlをブラウザで開き、認証完了後PIN入力を受け付けて、リクエストトークンと共にAuthenticatePinFlowを呼び出す
186     '''</remarks>
187     '''<param name="requestTokenUrl">リクエストトークンの取得先URL</param>
188     '''<param name="requestUri">ブラウザで開く認証用URLのベース</param>
189     '''<param name="requestToken">[OUT]認証要求で戻されるリクエストトークン。使い捨て</param>
190     '''<param name="authUri">[OUT]requestUriを元に生成された認証用URL。通常はリクエストトークンをクエリとして付加したUri</param>
191     '''<returns>取得結果真偽値</returns>
192     Public Function AuthenticatePinFlowRequest(ByVal requestTokenUrl As String, _
193                                         ByVal authorizeUrl As String, _
194                                         ByRef requestToken As String, _
195                                         ByRef authUri As Uri) As Boolean
196         'PIN-based flow
197         authUri = GetAuthenticatePageUri(requestTokenUrl, authorizeUrl, requestToken)
198         If authUri Is Nothing Then Return False
199         Return True
200     End Function
201
202     '''<summary>
203     '''OAuth認証のアクセストークン取得。PIN入力用の後段
204     '''</summary>
205     '''<remarks>
206     '''事前にAuthenticatePinFlowRequestを呼んで、ブラウザで認証後に表示されるPINを入力してもらい、その値とともに呼び出すこと
207     '''</remarks>
208     '''<param name="accessTokenUrl">アクセストークンの取得先URL</param>
209     '''<param name="requestUri">AuthenticatePinFlowRequestで取得したリクエストトークン</param>
210     '''<param name="pinCode">Webで認証後に表示されるPINコード</param>
211     '''<returns>取得結果真偽値</returns>
212     Public Function AuthenticatePinFlow(ByVal accessTokenUrl As String, _
213                                         ByVal requestToken As String, _
214                                         ByVal pinCode As String) As HttpStatusCode
215         'PIN-based flow
216         If String.IsNullOrEmpty(requestToken) Then Throw New Exception("Sequence error.(requestToken is blank)")
217
218         'アクセストークン取得
219         Dim content As String = ""
220         Dim accessTokenData As NameValueCollection
221         Dim httpCode As HttpStatusCode = GetOAuthToken(New Uri(accessTokenUrl), pinCode, requestToken, Nothing, content)
222         If httpCode <> HttpStatusCode.OK Then Return httpCode
223         accessTokenData = ParseQueryString(content)
224
225         If accessTokenData IsNot Nothing Then
226             token = accessTokenData.Item("oauth_token")
227             tokenSecret = accessTokenData.Item("oauth_token_secret")
228             'サービスごとの独自拡張対応
229             If Me.userIdentKey <> "" Then
230                 authorizedUsername = accessTokenData.Item(Me.userIdentKey)
231             Else
232                 authorizedUsername = ""
233             End If
234             If token = "" Then Throw New InvalidDataException("Token is null.")
235             Return HttpStatusCode.OK
236         Else
237             Throw New InvalidDataException("Return value is null.")
238         End If
239     End Function
240
241     '''<summary>
242     '''OAuth認証のアクセストークン取得。xAuth方式
243     '''</summary>
244     '''<param name="accessTokenUrl">アクセストークンの取得先URL</param>
245     '''<param name="username">認証用ユーザー名</param>
246     '''<param name="password">認証用パスワード</param>
247     '''<returns>取得結果真偽値</returns>
248     Public Function AuthenticateXAuth(ByVal accessTokenUrl As Uri, ByVal username As String, ByVal password As String, ByRef content As String) As HttpStatusCode Implements IHttpConnection.Authenticate
249         'ユーザー・パスワードチェック
250         If String.IsNullOrEmpty(username) OrElse String.IsNullOrEmpty(password) Then
251             Throw New Exception("Sequence error.(username or password is blank)")
252         End If
253         'xAuthの拡張パラメータ設定
254         Dim parameter As New Dictionary(Of String, String)
255         parameter.Add("x_auth_mode", "client_auth")
256         parameter.Add("x_auth_username", username)
257         parameter.Add("x_auth_password", password)
258
259         'アクセストークン取得
260         Dim httpCode As HttpStatusCode = GetOAuthToken(accessTokenUrl, "", "", parameter, content)
261         If httpCode <> HttpStatusCode.OK Then Return httpCode
262         Dim accessTokenData As NameValueCollection = ParseQueryString(content)
263
264         If accessTokenData IsNot Nothing Then
265             token = accessTokenData.Item("oauth_token")
266             tokenSecret = accessTokenData.Item("oauth_token_secret")
267             'サービスごとの独自拡張対応
268             If Me.userIdentKey <> "" Then
269                 authorizedUsername = accessTokenData.Item(Me.userIdentKey)
270             Else
271                 authorizedUsername = ""
272             End If
273             If token = "" Then Throw New InvalidDataException("Token is null.")
274             Return HttpStatusCode.OK
275         Else
276             Throw New InvalidDataException("Return value is null.")
277         End If
278     End Function
279
280     '''<summary>
281     '''OAuth認証のリクエストトークン取得。リクエストトークンと組み合わせた認証用のUriも生成する
282     '''</summary>
283     '''<param name="accessTokenUrl">リクエストトークンの取得先URL</param>
284     '''<param name="authorizeUrl">ブラウザで開く認証用URLのベース</param>
285     '''<param name="requestToken">[OUT]取得したリクエストトークン</param>
286     '''<returns>取得結果真偽値</returns>
287     Private Function GetAuthenticatePageUri(ByVal requestTokenUrl As String, _
288                                         ByVal authorizeUrl As String, _
289                                         ByRef requestToken As String) As Uri
290         Const tokenKey As String = "oauth_token"
291
292         'リクエストトークン取得
293         Dim content As String = ""
294         Dim reqTokenData As NameValueCollection
295         If GetOAuthToken(New Uri(requestTokenUrl), "", "", Nothing, content) <> HttpStatusCode.OK Then Return Nothing
296         reqTokenData = ParseQueryString(content)
297
298         If reqTokenData IsNot Nothing Then
299             requestToken = reqTokenData.Item(tokenKey)
300             'Uri生成
301             Dim ub As New UriBuilder(authorizeUrl)
302             ub.Query = String.Format("{0}={1}", tokenKey, requestToken)
303             Return ub.Uri
304         Else
305             Return Nothing
306         End If
307     End Function
308
309     '''<summary>
310     '''OAuth認証のトークン取得共通処理
311     '''</summary>
312     '''<param name="requestUri">各種トークンの取得先URL</param>
313     '''<param name="pinCode">PINフロー時のアクセストークン取得時に設定。それ以外は空文字列</param>
314     '''<param name="requestToken">PINフロー時のリクエストトークン取得時に設定。それ以外は空文字列</param>
315     '''<param name="parameter">追加パラメータ。xAuthで使用</param>
316     '''<returns>取得結果のデータ。正しく取得出来なかった場合はNothing</returns>
317     Private Function GetOAuthToken(ByVal requestUri As Uri, ByVal pinCode As String, ByVal requestToken As String, ByVal parameter As Dictionary(Of String, String), ByRef content As String) As HttpStatusCode
318         Dim webReq As HttpWebRequest = Nothing
319         'HTTPリクエスト生成。PINコードもパラメータも未指定の場合はGETメソッドで通信。それ以外はPOST
320         If String.IsNullOrEmpty(pinCode) AndAlso parameter Is Nothing Then
321             webReq = CreateRequest("GET", requestUri, Nothing, False)
322         Else
323             webReq = CreateRequest("POST", requestUri, parameter, False) 'ボディに追加パラメータ書き込み
324         End If
325         'OAuth関連パラメータ準備。追加パラメータがあれば追加
326         Dim query As New Dictionary(Of String, String)
327         If parameter IsNot Nothing Then
328             For Each kvp As KeyValuePair(Of String, String) In parameter
329                 query.Add(kvp.Key, kvp.Value)
330             Next
331         End If
332         'PINコードが指定されていればパラメータに追加
333         If Not String.IsNullOrEmpty(pinCode) Then query.Add("oauth_verifier", pinCode)
334         'OAuth関連情報をHTTPリクエストに追加
335         AppendOAuthInfo(webReq, query, requestToken, "")
336         'HTTP応答取得
337         Dim header As New Dictionary(Of String, String) From {{"Date", ""}}
338         Dim responceCode As HttpStatusCode = GetResponse(webReq, content, header, False)
339         If responceCode = HttpStatusCode.OK Then Return responceCode
340         If Not String.IsNullOrEmpty(header("Date")) Then content += Environment.NewLine + "Check the Date & Time of this computer." + Environment.NewLine + "Server:" + CDate(header("Date")).ToString + "  PC:" + Now.ToString
341         Return responceCode
342     End Function
343 #End Region
344
345 #Region "OAuth認証用ヘッダ作成・付加処理"
346     '''<summary>
347     '''HTTPリクエストにOAuth関連ヘッダを追加
348     '''</summary>
349     '''<param name="webRequest">追加対象のHTTPリクエスト</param>
350     '''<param name="query">OAuth追加情報+クエリ or POSTデータ</param>
351     '''<param name="token">アクセストークン、もしくはリクエストトークン。未取得なら空文字列</param>
352     '''<param name="tokenSecret">アクセストークンシークレット。認証処理では空文字列</param>
353     Protected Overridable Sub AppendOAuthInfo(ByVal webRequest As HttpWebRequest, _
354                                         ByVal query As Dictionary(Of String, String), _
355                                         ByVal token As String, _
356                                         ByVal tokenSecret As String)
357         'OAuth共通情報取得
358         Dim parameter As Dictionary(Of String, String) = GetOAuthParameter(token)
359         'OAuth共通情報にquery情報を追加
360         If query IsNot Nothing Then
361             For Each item As KeyValuePair(Of String, String) In query
362                 parameter.Add(item.Key, item.Value)
363             Next
364         End If
365         '署名の作成・追加
366         parameter.Add("oauth_signature", CreateSignature(tokenSecret, webRequest.Method, webRequest.RequestUri, parameter))
367         'HTTPリクエストのヘッダに追加
368         Dim sb As New StringBuilder("OAuth ")
369         For Each item As KeyValuePair(Of String, String) In parameter
370             '各種情報のうち、oauth_で始まる情報のみ、ヘッダに追加する。各情報はカンマ区切り、データはダブルクォーテーションで括る
371             If item.Key.StartsWith("oauth_") Then
372                 sb.AppendFormat("{0}=""{1}"",", item.Key, UrlEncode(item.Value))
373             End If
374         Next
375         webRequest.Headers.Add(HttpRequestHeader.Authorization, sb.ToString)
376     End Sub
377
378     '''<summary>
379     '''OAuthで使用する共通情報を取得する
380     '''</summary>
381     '''<param name="token">アクセストークン、もしくはリクエストトークン。未取得なら空文字列</param>
382     '''<returns>OAuth情報のディクショナリ</returns>
383     Protected Function GetOAuthParameter(ByVal token As String) As Dictionary(Of String, String)
384         Dim parameter As New Dictionary(Of String, String)
385         parameter.Add("oauth_consumer_key", consumerKey)
386         parameter.Add("oauth_signature_method", "HMAC-SHA1")
387         parameter.Add("oauth_timestamp", Convert.ToInt64((DateTime.UtcNow - UnixEpoch).TotalSeconds).ToString())   'epoch秒
388         parameter.Add("oauth_nonce", NonceRandom.Next(123400, 9999999).ToString())
389         parameter.Add("oauth_version", "1.0")
390         If Not String.IsNullOrEmpty(token) Then parameter.Add("oauth_token", token) 'トークンがあれば追加
391         Return parameter
392     End Function
393
394     '''<summary>
395     '''OAuth認証ヘッダの署名作成
396     '''</summary>
397     '''<param name="tokenSecret">アクセストークン秘密鍵</param>
398     '''<param name="method">HTTPメソッド文字列</param>
399     '''<param name="uri">アクセス先Uri</param>
400     '''<param name="parameter">クエリ、もしくはPOSTデータ</param>
401     '''<returns>署名文字列</returns>
402     Protected Overridable Function CreateSignature(ByVal tokenSecret As String, _
403                                             ByVal method As String, _
404                                             ByVal uri As Uri, _
405                                             ByVal parameter As Dictionary(Of String, String) _
406                                         ) As String
407         'パラメタをソート済みディクショナリに詰替(OAuthの仕様)
408         Dim sorted As New SortedDictionary(Of String, String)(parameter)
409         'URLエンコード済みのクエリ形式文字列に変換
410         Dim paramString As String = CreateQueryString(sorted)
411         'アクセス先URLの整形
412         Dim url As String = String.Format("{0}://{1}{2}", uri.Scheme, uri.Host, uri.AbsolutePath)
413         '署名のベース文字列生成(&区切り)。クエリ形式文字列は再エンコードする
414         Dim signatureBase As String = String.Format("{0}&{1}&{2}", method, UrlEncode(url), UrlEncode(paramString))
415         '署名鍵の文字列をコンシューマー秘密鍵とアクセストークン秘密鍵から生成(&区切り。アクセストークン秘密鍵なくても&残すこと)
416         Dim key As String = UrlEncode(consumerSecret) + "&"
417         If Not String.IsNullOrEmpty(tokenSecret) Then key += UrlEncode(tokenSecret)
418         '鍵生成&署名生成
419         Dim hmac As New Cryptography.HMACSHA1(Encoding.ASCII.GetBytes(key))
420         Dim hash As Byte() = hmac.ComputeHash(Encoding.ASCII.GetBytes(signatureBase))
421         Return Convert.ToBase64String(hash)
422     End Function
423
424 #End Region
425
426     '''<summary>
427     '''初期化。各種トークンの設定とユーザー識別情報設定
428     '''</summary>
429     '''<param name="consumerKey">コンシューマー鍵</param>
430     '''<param name="consumerSecret">コンシューマー秘密鍵</param>
431     '''<param name="accessToken">アクセストークン</param>
432     '''<param name="accessTokenSecret">アクセストークン秘密鍵</param>
433     '''<param name="userIdentifier">アクセストークン取得時に得られるユーザー識別情報。不要なら空文字列</param>
434     Public Sub Initialize(ByVal consumerKey As String, _
435                                     ByVal consumerSecret As String, _
436                                     ByVal accessToken As String, _
437                                     ByVal accessTokenSecret As String, _
438                                     ByVal userIdentifier As String)
439         Me.consumerKey = consumerKey
440         Me.consumerSecret = consumerSecret
441         Me.token = accessToken
442         Me.tokenSecret = accessTokenSecret
443         Me.userIdentKey = userIdentifier
444     End Sub
445
446     '''<summary>
447     '''初期化。各種トークンの設定とユーザー識別情報設定
448     '''</summary>
449     '''<param name="consumerKey">コンシューマー鍵</param>
450     '''<param name="consumerSecret">コンシューマー秘密鍵</param>
451     '''<param name="accessToken">アクセストークン</param>
452     '''<param name="accessTokenSecret">アクセストークン秘密鍵</param>
453     '''<param name="username">認証済みユーザー名</param>
454     '''<param name="userIdentifier">アクセストークン取得時に得られるユーザー識別情報。不要なら空文字列</param>
455     Public Sub Initialize(ByVal consumerKey As String, _
456                                 ByVal consumerSecret As String, _
457                                 ByVal accessToken As String, _
458                                 ByVal accessTokenSecret As String, _
459                                 ByVal username As String, _
460                                 ByVal userIdentifier As String)
461         Initialize(consumerKey, consumerSecret, accessToken, accessTokenSecret, userIdentifier)
462         authorizedUsername = username
463     End Sub
464
465     '''<summary>
466     '''アクセストークン
467     '''</summary>
468     Public ReadOnly Property AccessToken() As String
469         Get
470             Return token
471         End Get
472     End Property
473
474     '''<summary>
475     '''アクセストークン秘密鍵
476     '''</summary>
477     Public ReadOnly Property AccessTokenSecret() As String
478         Get
479             Return tokenSecret
480         End Get
481     End Property
482
483     '''<summary>
484     '''認証済みユーザー名
485     '''</summary>
486     Public ReadOnly Property AuthUsername() As String Implements IHttpConnection.AuthUsername
487         Get
488             Return authorizedUsername
489         End Get
490     End Property
491
492 End Class