Showing posts with label http. Show all posts
Showing posts with label http. Show all posts

Thursday, July 10, 2008

My mochiweb webservice...

Now that mochiweb is popular, and that I use it for some of my work, I can publish here some webservice thingy that I use.

I wanted to have this simple thing: modulename,funname, and parameters inside the URL:

http://www.server.tld/modulename/fun/arg1/arg2
wmodulename:Fun(arg1, arg2)

(Note the 'w' prefix, I explain it later)

Here's the code:

-module(webservice).
-export([start/0, start/2, loop/2, stop/0, test/0]).
-export([dolog/2]).
-define(PORT, 8080).

start() ->
start("/home/rolphin/Devel/Web", ?PORT).

start(Wwwroot, Port) ->
Loop = fun (Req) ->
?MODULE:loop(Req, Wwwroot)
end,
mochiweb_http:start([{loop, Loop}, {name, ?MODULE}, {port, Port}]).

stop() ->
mochiweb_http:stop(?MODULE).

loop(Req, DocRoot) ->
log(Req),
case string:tokens(Req:get(path), "/") of
[ "dump" ] ->
Req:ok({"text/plain",
io_lib:format("~p~n", [Req:dump()])});

[ "favicon.ico" ] ->
Req:respond({404, [], ""});

[ "codepath" ] ->
Req:ok({"text/plain",
io_lib:format("codepath: ~p~n", [code:get_path()])});

[ "codepath", "json" ] ->
Req:ok({"text/plain",
mochijson:encode({array, code:get_path()})});

[ Path, Fun | Elems ] ->
% Every module name should begin with 'w'
dispatch(Req, DocRoot, list_to_atom("w" ++ Path), Fun, Elems);

[] ->
launch(Req, DocRoot, wdefault, do, []);

_ ->
Req:respond({502, [], []})

end.

dispatch(Req, DocRoot, Module, Fun, Elems) ->
M = Req:get(method),
case M of
'GET' ->
launch(Req, DocRoot, Module, Fun, Elems);
'POST' ->
launch(Req, DocRoot, Module, Fun, Elems);
'PUT' ->
launch(Req, DocRoot, Module, Fun, Elems);
'DELETE' ->
launch(Req, DocRoot, Module, Fun, Elems);
'HEAD' ->
launch(Req, DocRoot, Module, Fun, Elems);
_Any ->
launch(Req, DocRoot, wdefault, get, [])
end.

launch(Req, DocRoot, wcontent, Fun, Args) ->
case catch wcontent:default(Req, DocRoot, [ Fun | Args] ) of
{'EXIT', {Type, _Error}} ->
Req:ok({"text/plain",
io_lib:format("GET Error: '~p' for '~p' ~p ~p~n~p~n", [Type, wcontent, Fun, Args, _Error])});
_ ->
ok
end;

launch(Req, DocRoot, Module, Fun, Args) ->
F = list_to_atom(Fun),
case catch Module:F(Req, DocRoot, Args) of
{'EXIT', {Type, _Error}} ->
Req:ok({"text/plain",
io_lib:format("~p Error: '~p' for ~p ~p ~p~n~p~n", [Req:get(method), Type, Module, Fun, Args, _Error])});
_ ->
ok
end.

log(Req) ->
Ip = Req:get(peer),
spawn(?MODULE, dolog, [Req, Ip]).

dolog(Req, Ip) ->
stat_logger:log("~p ~p", [Ip, Req:get(path)]).



First you can see that I use "mochiweb:start" and "mochiweb:stop" in the "start" and "stop" funs.
Then parameters are extracted from the URI:

case string:tokens(Req:get(path), "/") of

I split the URI on the "/" character, and compare the resulting list with various possibilities.

Then the main part of the code:

launch(Req, DocRoot, Module, Fun, Args) ->
F = list_to_atom(Fun),
case catch Module:F(Req, DocRoot, Args) of
{'EXIT', {Type, _Error}} ->
Req:ok({"text/plain",
io_lib:format("~p Error: '~p' for ~p ~p ~p~n~p~n", [Req:get(method), Type, Module, Fun, Args, _Error])});
_ ->
ok
end.

Using "catch" prevent your app from crashing without any information. If a module doesn't exists, you'll be able to see why (the main problem is sometimes the module is not in the code:path...)

Every webservice module name should start with "w", it's some sort of namespace :), now you know the reason for the 'w' prefix:

[ Path, Fun | Elems ] ->
% Every module name should begin with 'w'
dispatch(Req, DocRoot, list_to_atom("w" ++ Path), Fun, Elems);


Now, how can I use it ? It's simple, you just have to build a module with such a template:

-module(wsample).
-export([do/3]).

do(Req, _DocRoot, Args) ->
Req:ok({"text/plain", [ <<"Hello ">>, hd(Args) , <<", you are authorized :)">>] }).



Then you build it, and store it somewhere in your "code:path". And call

webservice:start().
in your erl shell. Then locate your webrowser to the URI
www.server.tld:8080/sample/do/PUT-ANYTHING-YOU-WANT-HERE


As a sample module, here is a module that is used everyday the "mobile tags" generator:

-module(wbarcode).
-export([do/3]).

-define(PNG, "image/png").

do(Req, _DocRoot, _Args) ->
barcode(Req).

% the process 'barcode' must be there
barcode(Req) ->
Options = Req:parse_qs(),
%io:format("~p~n", [Options]),

Text = proplists:get_value("text", Options, "http://www.shootit.fr"),
Res = proplists:get_value("res", Options, "72"),
Z = proplists:get_value("z", Options, "1"),

case barcode:generate('barcode', Text) of
timeout ->
Req:ok({"text/plain", [], <<"Timeout">>});

{data, {Width, Height}, Data} ->
Zoom = list_to_integer(Z),
Xres = integer_to_list(list_to_integer(Res) * Zoom),
Yres = integer_to_list(list_to_integer(Res) * Zoom),
Params = [ {"PS", [integer_to_list(Width + 1), integer_to_list(Height + 1)]}, {"HW", [Xres, Yres]} ],
case gs:draw(gs, Data, Params) of
{png, Image} ->
Req:ok({?PNG, Image});

_E ->
Req:ok({"text/plain", [], [ <<"Error: ">>,
io_lib:format("~s", [_E])]})
end
end.

Wednesday, August 15, 2007

Erlang Picasa API, it works, and here's some reason why...

The Post request to the picasa web service:

request_picasa(AuthToken, User, ImgName, Data) ->
Body = iolist_to_binary(Data),
Authorization = <<"GoogleLogin auth=", AuthToken/binary>>,
Url = "http://picasaweb.google.com/data/feed/api/user/" ++ User ++ "/album/blog",
case http:request(post,
{ Url,
[ { "Authorization", binary_to_list(Authorization) },
{ "Slug", ImgName }
],
"image/jpeg", % Content-type
Body % Body
},
[ {timeout, 30000}, {sync, false} ], % HTTPOptions
[ {body_format, binary} ]) of % Options

{ok, Result} ->
case Result of
{{_, 201, _}, Headers, Response} ->
{ok, Headers, Response};

{_,_,Response} ->
Response
end;

{error, Reason} ->
io:format("Error: ~p~n", [Reason])
end.

And what's you must note is the following:
  • a new url to Post your image.
  • a new header named 'Slug' holding the value of the filename you want for your image
  • a timeout of 30 seconds, letting enough time for the upload process to complete.
Other headers are mandatory, and hopefully already set by 'http:request':
  • content-length holding the size of the binary data of your image
  • content-type holding the image type, here 'image/jpeg'
I choose to use the 'prim_file:load_file/1' fun to retrieve a binary stream of octet composing the image I want to upload:

{ok, File} = prim_file:read_file(Image),
{value, {_, Auth}} = lists:keysearch(picasa, 1, State#state.auth),
case request_picasa(Auth, User, ImgName, File ) of
The variable File holds the binary data, the Auth variable holds the 'correct' Picasa AuthToken, so the 'request_picasa/4' fun can be called correctly...

I'll upgrade later the project http://code.google.com/p/googerl, because I want the 'request_picasa' fun to return {ok, ImageUrl}, and to do that I need to parse the atom response sent by Google... And I'm asking myself wether I doit using xmerl or leex ;]

Monday, August 6, 2007

The final request the post your Atom message.

The final request to post the message in the atom format:

request(AuthToken, BlogId, Data) ->
Body = iolist_to_binary(Data),
io:format("Sending: ~nContent-length: ~p~nBody:~n~s~n", [ size(Body), Body ]),
Authorization = "GoogleLogin auth=" ++ AuthToken,
Url = "http://www.blogger.com/feeds/" ++ BlogId ++ "/posts/default",
case http:request(post, % Method ;)
{
Url, % URL
[ { "Authorization", Authorization } ], % Headers
"application/atom+xml; charset=utf-8", % Content-type
Body % Body
},
[ {timeout, 3000}, {sync, false} ], % HTTPOptions
[ {body_format, binary} ]) of % Options

{ok, Result} ->
case Result of
{{_, 201, _}, Headers, _Response} ->
PostId = get_postid(Headers),
{ok, PostId};

{_,_,Response} ->
Response
end;

{error, Reason} ->
io:format("Error: ~p~n", [Reason])
end.

The thing that's cool overthere is the 'get_postid/1' to retrieve the postId generated by the blogger api... This fun will be called only when the HTTP 'status' code will be "201", which means "created".

I parse response headers and search for 'location' string, and once found I extract the last part of the url:

get_postid([]) ->
"none";
get_postid(Headers) ->
case lists:keysearch("location", 1, Headers) of
{value, {_, Value}} ->
lists:last( string:tokens(Value, "/") );

_ ->
"none"
end.

I've look at the Zend Framework, and found that it parse the entire response body to extract the same value... I think that my method is simpler and works better. That's also why erlang is for smarter people than php (gratuitous troll :p)

Monday, July 30, 2007

Connecting Erlang to Blogger (Part 1) - Auth with ClientLogin

With the Gdata API from google you can connect your application to some nice services... Calendar, Blogger etc.
Since this is completly REST based you can of course use your 'http:request' to connect and exploit those services. Let's begin with the ClientLogin process.

For this article we will focus on the Blogger API, the main purpose is of course create an Erlang client for Blogger :)

Connecting to google is as simple as sending something like this:

accountType=HOSTED_OR_GOOGLE&Email=YOURGOOGLEACOUNT&Passwd=YOURPASSWORD&source=SelfCo-TestApp-1&service=blogger


Now we can do it in Erlang too !. First we need to build the query string, second we need to send it to the ClientLogin service using 'http:request'.


auth(Username, Password, Application) ->
Sep = <<"&">>,
Post = [
<<"accountType=HOSTED_OR_GOOGLE&">>,
<<"Email=">>, list_to_binary(Username), Sep,
<<"Passwd=">>, list_to_binary(Password), Sep,
<<"source=">>, list_to_binary(Application), Sep,
<<"service=blogger">> ],
request(erlang:iolist_to_binary(Post)).



The fun 'erlang:iolist_to_binary/1' transforms the list of binaries to a simple binary, this is not really necessary but this will ease yourself later for debugging...

Now we can send this query string to the google ClientLogin process:

request(Data) ->
case http:request(post,
{"https://www.google.com/accounts/ClientLogin", [],
"application/x-www-form-urlencoded", Data},
[ {timeout, 3000} ], [{stream, "/tmp/google.test"}, {body_format, binary}]) of

{ok, saved_to_file} ->
io:format("Saved to file~n");

{ok, Result} ->
io:format("Received: ~p~n", [Result]);

{error, Reason} ->
io:format("Error: ~p~n", [Reason])
end.


  • This is a POST query
  • The service is https://www.google.com/accounts/ClientLogin
  • The content-type is application/x-www-form-urlencoded
  • We sets the timeout to 3 seconds
  • We store the result (if successful to '/tmp/google.test')


Let's try this code:

65> google:auth("test@gmail.com", "secretcode").
Received: {{"HTTP/1.1",403,"Forbidden"},
[{"cache-control","no-cache"},
{"date","Sun, 29 Jul 2007 20:44:20 GMT"},
{"pragma","no-cache"},
{"server","GFE/1.3"},
{"content-length","24"},
{"content-type","text/plain"}],
<<"Error=BadAuthentication\n">>}
ok

The connection fails, so let's try with a valid user account:

70> google:auth("validaccount@gmail.com", "validpassword").
Saved to file
ok

Success !

The content of '/tmp/google.test':

SID=DQAAAG8AAACuATb7YJxMdqQhp0LIf546SWLfDNfTlANffRc0B6OGbTat4Ebdj89s6hVEzfNZRL...
LSID=DQAAAHEAAAAG1iqBgOrgzrY5cdgpBv9y42HxkvjNuUaYKImw6yH7xh0GtL5EG19C9GkGdPEb1...
Auth=DQAAAHAAAAAG1iqBgOrgzrY5cdgpBv9y42HxkvjNuUaYKImw6yH7xh0GtL5EG19C9GkGdPEb1...


The final token we need is the 'Auth=' one, this string will be passed with every new query as an 'Authorization' header:

Authorization: GoogleLogin auth=DQAAAHAAAA...


Next Time in Part 2, I'll show you how we'll use this AuthToken and how we will be able to post a message to our blog !

Sticky