fc2ブログ
2010.01.16

AtomAPI+Amebaブログ。

年末年始、さんざん悩ませてくれたAtomAPIについてのメモ。

アメブロ。XMLRPCは使えないのでAtomAPIを使う必要がある。
そのまとめ。

1)WSSE認証
HTTP で X-WSSE ヘッダを追加し、ルートAtomエンドポイントに投げる。

アメブロのルートAtomエンドポイントは2010年1月現在は↓で大丈夫。
http://atomblog.ameba.jp/servlet/_atom/blog

追加するヘッダは↓のような感じ。もちろん、↓を投げてもうまくいかない。
X-WSSE: UsernameToken Username="xxxxxxx", PasswordDigest="ZCNaK2jrXr4+zsCaYK/YLUxImZU=", Nonce="Uh95NQlviNpJQR1MmML+zq6pFxE=", Created="2005-01-18T03:20:15Z"

Username ・・・ アメブロでログインするときのID。
PasswordDigest ・・・ nonce + created + password を sha1 でハッシュ値を取得し、Base64エンコードしたもの。(nonce、created、passwordは後述)
アメブロの場合、passwordをmd5でハッシュ値を取得する必要がある。その際、アルファベットはすべて小文字に変換する。

PHPでbase64エンコード、sha1・md5 計算するかは分からないのでイメージだけど↓のような感じ。
↓のコードをそのまま入れても絶対動かない。
toBase64( sha1( nonce . created . toLower( md5( password ) ) ) )

それぞれの説明。
toBase64 ・・・ Base64エンコードする関数。
sha1 ・・・ 引数の文字列から、sha1ハッシュ値を取得する関数。
toLower ・・・ 文字列を、大文字から小文字(A -> a)に変換する関数。
md5 ・・・ 引数の文字列から、md5ハッシュ値を取得する関数。

nonce ・・・ ランダム文字列。自分の場合は、20バイト分のランダムな文字列を使ってた。
 ランダムといいつつ、”abc”とか固定文字でもOKだった。
 1/29訂正・・・Base64変換のタイミングについて誤りがあったので訂正。
 PasswordDigest に渡す際は通常の文字列(例:"abc")を、X-WSSEヘッダのnonce引数にはBase64変換した文字列(例:toBase64("abc"))を指定する。

password ・・・ アメブロにログインするときのパスワード。
 上で書いたとおり、PasswordDigestを生成する際、アメブロに限り、パスワード文字列を md5 ハッシュ値計算してやる必要があるので注意。

created ・・・ 時間。例にあるような 2005-01-18T03:20:15Z という感じのフォーマット(日付と時間の区切りに半角大文字の T を、文字列の最後に Z をつければOK)。
 PasswordDigestを生成する際も、そのままの文字列を渡してやればOK。

このヘッダをつけて、GETで送信すればOK。
成功したら、XMLが返ってくる。
そのXMLに、いろいろなURLがついてるので保存しておくこと。

失敗したら、以下のようなコードが返ってくる。
401 ・・・ X-WSSE ヘッダがおかしい。ほかは大丈夫。
403 ・・・ 変なURLに送ったとか、根本的におかしい。
上以外 ・・・ わからん


2)記事情報の取得
X-WSSE認証がうまくいけば、とりあえず第一関門はクリア。
次は記事情報の取得。
1)でゲットしたXMLからFeedのURLをゲットする。
あとは1)のやつと同じで、エンドポイントを↑でゲットしたURLに変えればいいだけ。


3)新規投稿
新規投稿はそれほど難しくない。

以下のXMLをPOSTメソッドで送信する。
もちろん、X-WSSEヘッダはつけておく。
それ以外につけたヘッダは ContentType を application/x.atom+xml にしたぐらい。
ヘッダの内容は、1)と同じでOK。

<?xml version="1.0" encoding="utf-8"?>
<entry xmlns="http://purl.org/atom/ns#"
xmlns:app="http://www.w3.org/2007/app#"
xmlns:mt="http://www.movabletype.org/atom/ns#">
<title>記事のタイトル</title>
<content type="application/xhtml+xml">
<![CDATA[記事の本文]]>
</content>
<updated>時間</updated>
</entry>

contentは、<![CDATA]]>でくくってやることで、本文にHTMLタグが使えるようになる。
updatedはいるのかいらないのかよく分からない。
試してないけど、多分いらないと思う。

試してできなかったこと
 本文に対する追記(mt:textMore)はダメ。
 公開範囲(app:draft)もダメ。常に全力公開。
タグがxx:~ってなってるやつは多分ダメなんじゃないかな。


4)記事編集
2)でゲットした情報に各記事のEditUrlが書いてあるから、そいつに3)の内容を投げれば、記事編集ができる。
もちろん、本文やタイトルを変えて送る。


5)記事削除
2)のEditUrlに、DELETEメソッドを投げればOK。


まとめ。
とにかく鬼門は1)。
それさえ通れば、後はなんとでもなる。
↓とか結構参考になった。
http://www.so-net.ne.jp/blog/sitetour/atom_api_spec.html


1/29追記
C#のソースコード。

//X-WSSEヘッダを取得
private string getWsseHeader()
{
string wsse = "";
string nonce = CreateNonce();
string created = getDate(DateTime.Now);
System.Security.Cryptography.SHA1Managed shasp =
new System.Security.Cryptography.SHA1Managed();
string nonce64 = nonce;

//CreateNonceで生成した文字列はBase64エンコードされているので、それを元に戻す。
string noncestr = base64toString(nonce);

//wsseヘッダを生成
wsse = string.Format("UsernameToken Username=\"{0}\", PasswordDigest=\"{1}\", Nonce=\"{2}\", Created=\"{3}\"",
id,
getPasswordDigest(noncestr, created),
nonce64,
created);

return wsse;
}


//X-WSSEヘッダ用のPasswordDigestを取得
private string getPasswordDigest(string nonce, string created)
{
string password = pass;
byte[] data = System.Text.Encoding.ASCII.GetBytes(password);
System.Security.Cryptography.MD5CryptoServiceProvider md5 =
new System.Security.Cryptography.MD5CryptoServiceProvider();

byte[] bs = md5.ComputeHash(data);
password = BitConverter.ToString(bs).ToLower().Replace("-", "");

byte[] temp = Encoding.ASCII.GetBytes(nonce + created + password);

System.Security.Cryptography.SHA1Managed sha1 = new System.Security.Cryptography.SHA1Managed();
return Convert.ToBase64String(sha1.ComputeHash(temp));
}


//Base64文字列を1バイト文字列に変換
private string base64toString(string base64)
{
string buf = Encoding.ASCII.GetString(Convert.FromBase64String(base64));
return buf;
}


//Nonceを生成(ランダムで生成した20バイトの文字列)
//戻り値:Base64エンコードしたNonce文字列。
// PasswordDigestにそのまま使えるが、X-WSSEヘッダのnonce値はデコードする必要がある。
private string CreateNonce()
{
byte[] temp = new byte[20];
int seed = Environment.TickCount;
(new Random(seed++)).NextBytes(temp);
temp = Encoding.UTF8.GetBytes(new Random().Next().ToString());
return Convert.ToBase64String(temp);
}


2/11さらに追記
PHPでも書いてみた。
これだと、一応HTTPステータスが200になるけど、エンドポイントのURLをどうやって取得するのかよく分からなかった。
(ユーザ名やパスワードをいじくると401が戻ってきたので、たぶん大丈夫だと思う、というレベル)
WSSEヘッダを作る参考にでもなれば、ということで。

<?php

ini_set( 'display_errors', 1 );

$user="ユーザID";
$pass="パスワード";

$ret = init_atom($user,$pass);

echo $ret;

?>

<?php
//
// wsse認証を突破するまでのサンプル
//
// http://d.hatena.ne.jp/pha/20081203/1228300535 に掲載のソースコードをベースに開発
//
function init_atom($user,$pass){

//PEAR::HTTP_Requestを使う
require_once("HTTP/Request.php");
$posturl = "http://atomblog.ameba.jp/servlet/_atom/blog";
$created = date("Y-m-d\TH:i:s\Z");

//WSSE認証用データの作成
$pass = md5($pass);
$nonce ="abcdefghijk";
$pass_digest = base64_encode(pack('H*', sha1($nonce.$created.$pass)));
$wsse = 'UsernameToken Username="'.$user.'", PasswordDigest="'.$pass_digest.'", Created="'.$created.'", Nonce="'.base64_encode($nonce).'"';

//HTTP_Request()より送信
$req = new HTTP_Request();
$req->addHeader('X-WSSE',$wsse );
$req->addHeader('Content-Type', 'application/x.atom+xml');
$req->setMethod(HTTP_REQUEST_METHOD_GET);
$req->setURL($posturl);

$req->sendRequest();

return $req->getResponseCode();
}
?>

この記事へのコメント
ちょうどアメブロのAtom APIではまっていましたので参考になりました。
ただ、記載された方法で試してみましたが、うまくいきませんでした。(401エラー)
nonceのところがちょっと怪しいのですが、pass digestを作る際は、nonceのランダムテキストをbase64に変換した後、さらにpassと合体させた後、またbase64をやるんでしょうか?
もしよろしけらば、フルのPHPソースを公開していただければありがたいのですが。。
よろしくお願いします。
Posted by askworld at 2010.01.30 04:22 | 編集
>askworldさん
はじめまして。
とりあえず、C#のソースしか手元にないのですが、そちらでよければ追記しておきました。
ご質問については、当方の記載が誤っておりました。
PasswordDigestを作る際は、nonceをbase64エンコードする必要はありませんでした。pass、createdと合体・sha1エンコードさせた後、base64エンコードをします。
X-WSSEヘッダのnonce値は、base64エンコードした値を設定します。

これでうまく行くかと思いますので、よろしければ試してみてくださいな。
Posted by せんぱい at 2010.01.30 12:37 | 編集
senpaiさん
お返事ありがとうございます。
ソースコードの公開ありがとうございます。
私はC#には詳しくなく、VBに翻訳して試してみようと思います。
その後で、PHPに翻訳します。(^_^)
うまくいきましたら、またご連絡させていただきます。

貴重な情報、ありがとうございました。
Posted by askworld at 2010.01.30 22:35 | 編集
その後の結果報告です。

ご呈示されたソースに基づいてVB版とPHP版を作成しましたが、
どちらも401や403エラーが返ってきてうまくいきませんでした。

password digestやヘッダ部でいろいろなパターンを試しましたが全滅です。(;>_<;)

う~ん、もうお手上げ状態です。


Posted by askworld at 2010.02.11 17:36 | 編集
>askworldさん
一応、WSSE認証を抜けるだけのコードをPHPで作ってみました。もしよろしければ、こちらを参考に作ってみてくださいな。
正しく抜けてるのかどうかも若干怪しいところがありますが・・・
Posted by senpai at 2010.02.11 21:26 | 編集
senpaiさん

ご丁寧にPHPのソースを公開してくださいましてありがとうございます。
おかげさまでwsse認証は無事にクリアできました。
response bodyを確認すると、投稿先のブログの投稿用URLらしきものも表示されたので、先ずは最大の難関クリアです。

ただ、その次に、投稿データを付けてPOSTする必要がありますが、これがなかなかうまくいかず、試行錯誤しているところです。
でもおかげさまで、暗闇に光が差してきたので、ホッとした気分です。

ありがとうございまし。
Posted by askworld at 2010.02.12 04:59 | 編集
senpaiさん

無事に投稿もできました!

ソースはこんな感じです。

先ず、GETしたレスポンスを探って、
link rel="service.post" のurlを取得し、投稿先のPOST用urlとする。

POST用データには、タイトルと本文を用意し、ヘッダーでくるんでPOST。
addHeaderには、GETと同じwsseを利用。

アメブロのAPIによる投稿は足かけ1年ほどかかってますので、感激もひとしおです。

ありがとうございました。
Posted by askworld at 2010.02.12 05:32 | 編集
>askworldさん
1年もの間お疲れ様でした。
こちらこそ、助力になれたようでよかったです。
Posted by senpai at 2010.02.12 07:32 | 編集
senpaiさん

今現在javascriptでこの機能を実装しようとしています。

しかし、$pass_digest=base64_encode(pack('H*',sha1($nonce.$created.$pass)))
のpack('H*'sha-)のところで何を行っているか分らずつまってしまいました。

ここでは、なんの作業を行っているのでしょうか教えてください。
Posted by Konboi at 2010.02.16 16:35 | 編集
>konboiさん
pack()は、sha1()関数の戻り値を、base64_encode()関数の引数として適切な値になるよう変換しているだけです。
(文字列を16進バイナリ値に変換しています)
MD5ハッシュ値を取得する関数(sha1())とbase64エンコードを行う関数(base64_convert())の組み合わせによっては、特に必要のなくなる処理です。
Posted by senpai at 2010.02.17 07:28 | 編集
senpaiさん

迅速な返答ありがとうございます。

今現在401が返ってきている状況なので、wsse認証が上手く行われていないと思われます。
ここを参考にもう少し頑張ってみようと思います。
Posted by Konboi at 2010.02.18 02:35 | 編集
このコメントは管理者の承認待ちです
Posted by at 2012.11.13 13:20 | 編集
このコメントは管理者の承認待ちです
Posted by at 2012.12.17 23:29 | 編集
管理者にだけ表示を許可する