Sunday, February 15, 2009

WSSE on Hatena by Erlang

I'm a user of HATENA-bookmark web service. It's a major social-bookmark service in Japan.
The API of the service is in public. So, I've written some code in Erlang.

To initiate operation, WSSE certification is required.
For details of WSSE specification, please refer
 http://www.ibm.com/developerworks/webservices/library/specification/ws-secure/
For details of HATENA-bookmark API, please refer
 http://d.hatena.ne.jp/keyword/%a4%cf%a4%c6%a4%ca%a5%d5%a5%a9%a5%c8%a5%e9%a5%a4%a5%d5AtomAPI?kid=88110#wsse (in Japanese)

Following is the wsse module.


-module(wsse).
-export([new/2]).

-define(SHA1DIGESTLENGTH, 20).

new (User, Password) ->
    {A, B, C} = now(),
    random:seed(A, B, C),
    Nonce = nonce(?SHA1DIGESTLENGTH, ""),

%% create ISO 8601 compling datetime
%% $ ruby -e " require 'open-uri' ; p Time.now.iso8601"
%% => "2008-07-31T16:16:14+09:00"
%%
%%  ref) http://www.trapexit.org/Converting_Between_struct:time_and_ISO8601_Format

    {{Year, Month, Day}, {Hour, Min, Sec}} = erlang:localtime(),
    TZ = os:cmd("date +%:z"),
    Created = io_lib:format("~4.10.0B-~2.10.0B-~2.10.0BT~2.10.0B:~2.10.0B:~2.10.0B~6s",
[Year, Month, Day, Hour, Min, Sec, TZ]),
    crypto:start(),
    Digest = binary_to_list(crypto:sha(Nonce ++ Created ++ Password)),

    "UsernameToken Username=\"" ++ User ++ "\", " ++
    "PasswordDigest=\"" ++
    base64:encode_to_string(Digest) ++ "\", " ++
    "Nonce=\"" ++
    base64:encode_to_string(Nonce) ++ "\", " ++
    "Created=\"" ++ Created ++ "\"".

nonce(0,L) -> L ++ [random:uniform(255)];
nonce(N,L) -> nonce(N -1, L ++ [random:uniform(255)]).


The HATENA-bookmark service API is using Atom format data.
Here is the module for it.
 -module(getatom).
-export([new/3]).

new(User, Password, Uri) ->
RequestHeader = [{ "X-WSSE" , wsse:new(User, Password)}],

inets:start(),
{ok, {{_Version, 200, _ReasonPhrase}, _Headers, Body}} =
http:request(get, {Uri, RequestHeader}, [ ], [ ]),
Body.


Here is a typical useage from erlang interpreter command line.

> Latest = getatom:new("username", "password", "http://b.hatena.ne.jp/atom/feed").
[60,63,120,109,108,32,118,101,114,115,105,111,110,61,34,49,
46,48,34,32,101,110,99,111,100,105,110,103,61|...]
> io:format("~s",[Latest]).

<feed version="0.3"</pp>
xmlns="http://purl.org/atom/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:openSearch="http://a9.com/-/spec/opensearchrss/1.0/"
xml:lang="ja">
<title>kgbu\343\201\256\343\203\226\343\203\203\343\202\257\343\203\236\343\203\274\343\202\257</title>
<link rel="alternate" type="text/html" href="http://b.hatena.ne.jp/kgbu/" />
...to be continued

1 comment:

  1. In original article, endpoint to dump "latest" bookmarks only, to dump all bookmarks try below.
    Results are stored in the file named "hatebudump.dat".

    >Latest = getatom:new("username", "password", "http://b.hatena.ne.jp/dump").
    [60,63,120,109,108,32,118,101,114,115,105,111,110,61,34,49,
    46,48,34,32,101,110,99,111,100,105,110,103,61|...]
    > {ok,FH} = file:open("hatebudump.dat", write).
    {ok,<0.78.0>}
    > io:format(FH, "~s", [Latest]).

    ReplyDelete