CLI は比較的簡単に操作できるのでコマンドプロンプトや PowerShell から操作する場合にはお勧めではあるのですが、LotusScript から AWS CLI を使う場合、
- 予めプラットフォーム(OS)に CLI のインストールが必要
- ディレクトリの操作などでプラットフォーム(OS)の違いを気にしなければならないことがある
- コマンドのレスポンスが直接 LotusScript に返らない
- コマンドのレスポンスを取得するためファイルへリダイレクトした場合にファイルから結果を得られるようになるまでにタイムラグがあり、数秒の待ち時間を挿入しなければならない場合がある
といったような場面に遭遇することがあり扱いづらさを感じています。
そこで、Notes/Domino V10 で追加されている NotesHTTPRequest クラスを使って AWS のサービスを操作できないかと試行錯誤していたのですが、ようやく動くものができましたのでここに記録しておきます。
CLI の代わりに LotusScript でリクエストを送信できるようになったことで、プラットフォームの違いを気にしなくて良くなったし、コマンドに対するレスポンスを直接受け取れるようになったことで、これまでのようにリダイレクトで作成されるファイルを待つ時間もなくなりました。
AWS へリクエストを送信するために、必要なクエリーパラメータを URL へ追加したり、リクエストヘッダーにセットするための署名を作ったりしなければなりません。
クエリーパラメータは AWS のサービスごとに用意されている API Reference ページにまとまっているようでした。例えば EC2 なら こちら です。
クエリーパラメータの指定はまだ単純なのですが、面倒だったのは署名作成がらみのタスクです。タスク(署名の作り方)については、次の4つのリンクに説明がありました。
パラメータ名でソートしなきゃならなかったり、文字列を小文字に変換したり、改行を挿入する/しないの違いがあったりといったように、レスポンスがエラーで返ったはいいけどプロセスが多すぎて間違いを発見しづらく、非常に神経使いました。もうへとへとです。
上の説明に従い AWS へ API リクエストを送るまでが下のコードです。下のコードでは AWS の CloudFormation というサービスで ListStacks というアクションを実行しています。このコードを参考にして AWS の(CloudFormation 以外の)サービスを使いたいときに書き換えが発生する部分はそれほど多くない(きちんと確認したわけではありませんが Const で始まる定数で定義している箇所と2つの変数 CanonicalQueryString と CanonicalHeaders あたりと、req.Get メソッド前後あたり?)と思います。
なおハッシュ値を求める箇所は Java のスクリプトライブラリ(このコードも下部に記載)で対応しましたので『「ほぼ」 LotusScriptで操作』ということになりますがご勘弁ください。
LotusScript のエージェント「ListStacks」
UseLSX "*javacon"
Use "SignatureAndHash"
Const regionName = "us-east-1" '米国東部(バージニア北部)
Const serviceName = "cloudformation"
Const serviceEndPoint = "cloudformation.us-east-1.amazonaws.com"
Const Algorithm$ = "AWS4-HMAC-SHA256"
Const accesskeyid$ = "SAMPLEACCESSKEYID"
Const secretaccesskey$ = "SAMPLEACCESSKEY"
Sub Initialize
Dim js As New javasession
Dim jc As JavaClass
Dim jo As JavaObject
Dim ss As New NotesSession
Dim req As NotesHTTPRequest
Dim nav As NotesJSONNavigator
Dim ndt As New NotesDateTime( Now )
Dim HashedCanonicalRequest$, HTTPRequestMethod$, CanonicalURI$
Dim CanonicalQueryString$, SignedHeaders$, RequestPayload$, payload$
Dim CanonicalHeaders$ List, CanonicalHeadersString$
Dim CanonicalRequest$, ISO8601Time$, gmtDate$
Dim StringToSign$, RequestDateTime$, CredentialScope$
Dim signature$, authheaderValue$
Dim headers, ret, gmtTime
Set jc = js.Getclass("signatureandhash")
Set jo = jc.Createobject
gmtTime = Split( ndt.Gmttime, " " )
gmtDate = Join( Split( gmttime(0), "/" ), "" )
ISO8601Time = gmtDate & "T" & Join( Split( gmttime(1), ":" ), "" ) & "Z"
'-------------------
'1. 署名バージョン 4 の正規リクエストを作成する
'-------------------
'リクエストメソッド
HTTPRequestMethod = "GET"
'正規URIパラメータ
CanonicalURI = "/"
'正規クエリ文字列(パラメータ名は文字コードでソート)
CanonicalQueryString = _
"Action=ListStacks" &_
"&StackStatusFilter.member.1=CREATE_IN_PROGRESS" &_
"&StackStatusFilter.member.2=DELETE_COMPLETE" &_
"&Version=2010-05-15"
'正規ヘッダー(ヘッダーは文字コードでソート)
CanonicalHeaders( "Content-Type" ) = "application/x-www-form-urlencoded; charset=utf-8"
CanonicalHeaders( "Host" ) = serviceEndPoint
CanonicalHeaders( "X-Amz-Date" ) = ISO8601Time
'署名付きヘッダー(小文字化、重複するスペースと前後のスペースを除去)
CanonicalHeadersString = ""
ForAll o In CanonicalHeaders
CanonicalHeadersString = CanonicalHeadersString & _
LCase$( FullTrim( ListTag( o ) ) ) & ":" & FullTrim( o ) & |
|
If SignedHeaders <> "" Then
SignedHeaders = SignedHeaders & ";" & LCase( FullTrim( ListTag( o ) ) )
Else
SignedHeaders = LCase( FullTrim( ListTag( o ) ) )
End If
End ForAll
'ペイロードのハッシュ値をとる
payload = ""
RequestPayload = jo.getHashedString( payload )
'正規リクエストの作成
CanonicalRequest = _
HTTPRequestMethod & |
| & CanonicalURI & |
| & CanonicalQueryString & |
| & CanonicalHeadersString & |
| & SignedHeaders & |
| & RequestPayload
'正規リクエストのハッシュ値をとる
HashedCanonicalRequest = jo.getHashedString( CanonicalRequest )
'-------------------
'2. 署名バージョン 4 の署名文字列を作成する
'-------------------
CredentialScope = _
gmtDate & "/" & regionName & "/" & serviceName & "/aws4_request"
StringToSign = _
Algorithm & |
| & ISO8601Time & |
| & CredentialScope & |
| & HashedCanonicalRequest
'-------------------
'3. 署名バージョン 4 の署名を計算する
'-------------------
'署名キーを取得して署名を計算する
signature = jo.getHashedSignature(secretaccesskey, gmtDate, regionName, serviceName, StringToSign)
'-------------------
'4. HTTP リクエストに署名を追加する
'-------------------
Set req = ss.Createhttprequest()
req.Preferjsonnavigator = True
'リクエストヘッダーをセット
ForAll o In CanonicalHeaders
req.Setheaderfield ListTag(o), o
End ForAll
'リクエストヘッダーへ署名をセット
authheaderValue = _
Algorithm &_
" Credential=" & accesskeyid & "/" & CredentialScope &_
", SignedHeaders=" & SignedHeaders &_
", Signature=" & signature
req.Setheaderfield "Authorization", authheaderValue
'リクエストを送信
Set nav = req.Get( "https://" & serviceEndPoint & CanonicalURI & "?" & CanonicalQueryString )
'リクエストの結果
If 0 = InStr(req.Responsecode, "200") Then
Print "失敗しました。 return code = " & req.Responsecode
Exit sub
End If
Print nav.Stringify()
headers = req.Getresponseheaders()
If IsArray( headers ) Then
ForAll header In headers
Print header
End ForAll
End If
Exit Sub
ERRORTRAP:
Print Erl, Err, Error
Exit sub
End SubJava の スクリプトライブラリ「SignatureAndHash」
import java.math.BigInteger;
import java.security.MessageDigest;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class signatureandhash {
static byte[] HmacSHA256(String data, byte[] key) throws Exception {
String algorithm="HmacSHA256";
final SecretKeySpec keySpec = new SecretKeySpec(key, algorithm);
final Mac mac = Mac.getInstance(algorithm);
mac.init(keySpec);
return mac.doFinal(data.getBytes("UTF-8"));
}
static byte[] getSignatureKey(String key, String dateStamp, String regionName, String serviceName) throws Exception {
byte[] kSecret = ("AWS4" + key).getBytes("UTF-8");
byte[] kDate = HmacSHA256(dateStamp, kSecret);
byte[] kRegion = HmacSHA256(regionName, kDate);
byte[] kService = HmacSHA256(serviceName, kRegion);
byte[] kSigning = HmacSHA256("aws4_request", kService);
return kSigning;
}
public static String getHashedSignature(String key, String dateStamp, String regionName, String serviceName, String stringToSign) throws Exception {
String signature = "";
byte[] signatureKey = getSignatureKey(key, dateStamp, regionName, serviceName);
byte[] signatureByte = HmacSHA256(stringToSign, signatureKey);
BigInteger bi = new BigInteger(1, signatureByte);
signature = String.format("%0" + (signatureByte.length << 1) + "x", bi);
return signature;
}
public static String getHashedString( String orgString ) throws Exception {
String hashedStr = "";
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(orgString.getBytes());
byte[] cipher_byte = md.digest();
StringBuilder sb = new StringBuilder(2 * cipher_byte.length);
for(byte b: cipher_byte) {
sb.append(String.format("%02x", b&0xff) );
}
hashedStr = sb.toString();
return hashedStr;
}
}私が試したサービスでは AWS へのリクエストの結果が JSON で返ってきました。エラーも JSON で返りました。ただ、別のサービスではひょっとすると XML を返すかもしれず、その場合は req.Get の行で「型が違う」といったエラーになるように思います。req.Get でエラーになる場合は req.Preferjsonnavigator に False をセットした上で 「set nav = req.Get...」を「ret = req.Get...」に書き換え、 nav.stringfy() のかわりに ret を出力すれば対応できそうです。
0 件のコメント:
コメントを投稿