Showing posts with label binary. Show all posts
Showing posts with label binary. Show all posts

Tuesday, September 22, 2009

erlang: Parsing binary data dynamically

Hi,
here's a quick tip for parsing binary data which format is unknown at compile time...

Let's say that you have a binary string and that later you receive its structure. Take for
example the code below:
-module(binm).

-export([test/0, test/2]).

test() ->
        test(<<4,0,0,0,5,0,0,0,7,0,8,0,33,1>>, [ 4, 4, 2, 2]).

test(Bin, List) ->
        {Final, End} = lists:foldl( fun(Len, {Res, Rest}) ->
                case Rest of 
                        <<M:Len/binary, NewRest/binary>> ->
                                {[ M | Res ], NewRest};
                        <<_:1/binary, NewRest/binary>> ->
                                { Res, NewRest}
                end
                end, {[], Bin}, List),
        {lists:reverse(Final), End}.

Precisely, we want to slice the binary part into 4 parts described as '[4, 4, 2, 2]' where each element is the size.
test() ->
        test(<<4,0,0,0,5,0,0,0,7,0,8,0,33,1>>, [ 4, 4, 2, 2]).

Let's compile and run:
2> c(binm).    
{ok,binm}
3> binm:test().
{[<<4,0,0,0>>,<<5,0,0,0>>,<<7,0>>,<<8,0>>],<<33,1>>}
4> 
Isn't this nice ? :p

Wednesday, July 9, 2008

Monitoring your servers with sysstat (sar)

There's sometimes things that are so helpfull that you think that everyone is aware of them, but sometimes this is not the case. Here I'll talk about a little package that is so powerful and efficient that you won't change anymore...
Taken from the ubuntu man page:

DESCRIPTION
The sar command writes to standard output the contents of selected cumula-
tive activity counters in the operating system. The accounting system, based
on the values in the count and interval parameters, writes information the
specified number of times spaced at the specified intervals in seconds. If
the interval parameter is set to zero, the sar command displays the average
statistics for the time since the system was started. The default value for
the count parameter is 1. If its value is set to zero, then reports are gen-
erated continuously. The collected data can also be saved in the file spec-
ified by the -o filename flag, in addition to being displayed onto the
screen. If filename is omitted, sar uses the standard system activity daily
data file, the /var/log/sysstat/sadd file, where the dd parameter indicates
the current day. By default all the data available from the kernel are
saved in the data file. Exceptions are interrupts and disks data, for which
the relevant options must be explicitly passed to sar (or to its backend
sadc ) when the data file is created (see options below).


"sar" comes with the sysstat package. Once it's installed you can monitor your server like never before...

Here's the description of the sysstat package from the author
The sysstat utilities are a collection of performance monitoring tools for Linux. 
These include sar, sadf, mpstat, iostat, pidstat and sa tools. Go to the Features page to display
a list of sysstat's features, or see the Documentation page to learn some more about them.

For example, you can watch realtime the network usage:

# sar -n DEV 1 0
Linux 2.6.22-15-generic (xXxXx) 07/09/2008

11:26:36 AM IFACE rxpck/s txpck/s rxkB/s txkB/s rxcmp/s txcmp/s rxmcst/s
11:26:37 AM lo 0.00 0.00 0.00 0.00 0.00 0.00 0.00
11:26:37 AM eth0 5.05 0.00 0.86 0.00 0.00 0.00 0.00

11:26:37 AM IFACE rxpck/s txpck/s rxkB/s txkB/s rxcmp/s txcmp/s rxmcst/s
11:26:38 AM lo 0.00 0.00 0.00 0.00 0.00 0.00 0.00
11:26:38 AM eth0 4.00 0.00 0.45 0.00 0.00 0.00 0.00
...

Today, I'll introduce the erlang-sar package that's able to retrieve information from the sar command.

The application is composed of a collector "sar_collector", a helper module "sar_values" and the main module "sar".
Here comes a quick sample session:

% Starting the collector
sar_collect:start().

% Retrieving the data
sar:stats(cpu).
[{cpu,idle,<<"98.62">>},
{cpu,steal,<<"0.00">>},
{cpu,iowait,<<"0.00">>},
{cpu,system,<<"0.18">>},
{cpu,nice,<<"0.00">>},
{cpu,user,<<"1.20">>}]

% Retrieving more data
sar:stats([cpu,mem]).
[{swap,swpcad,<<"33236">>},
{swap,usage,<<"64.72">>},
{swap,used,<<"389872">>},
{swap,free,<<"212492">>},
{mem,kbcached,<<"84496">>},
{mem,kbbuffers,<<"63408">>},
{mem,memused,<<"98.78">>},
{mem,kbmemused,<<"508984">>},
{mem,kbmemfree,<<"6308">>},
{cpu,idle,<<"97.83">>},
{cpu,steal,<<"0.00">>},
{cpu,iowait,<<"0.75">>},
{cpu,system,<<"0.20">>},
{cpu,nice,<<"0.00">>},
{cpu,user,<<"1.22">>}]


The module "sar_values" also export an "extractor" function that can be used to build fun()s:

% build a Mem fun()
Mem = sar_values:extractor(mem).

% Calling Mem fun() on sar:stats()
Mem(sar:stats([cpu,mem])).
[{kbcached,<<"84496">>},
{kbbuffers,<<"63480">>},
{memused,<<"98.77">>},
{kbmemused,<<"508976">>},
{kbmemfree,<<"6316">>}]

% Calling it on sar:stats()
Mem(sar:stats()).
[{kbcached,<<"84496">>},
{kbbuffers,<<"63520">>},
{memused,<<"98.80">>},
{kbmemused,<<"509100">>},
{kbmemfree,<<"6192">>}]


With this package you have access to all the data sar can export for you.
Here's the "sar.erl" code:

-module(sar).

-export([systat/0, stats/0, stats/1, option/1]).
-export([extract/1]).
-define(OPTIONS, "-u -r -v -c -q -n DEV").
-define(DATA, "/tmp/last").

systat() ->
Cmd = "sadf " ++ ?DATA ++ " -- " ++ ?OPTIONS,
execute(".", Cmd).

stats() ->
Cmd = "sadf " ++ ?DATA ++ " -- " ++ ?OPTIONS,
{ok, _, Bin} = execute(".", Cmd),
extract(Bin).

stats(List) when is_list(List) ->
Args = lists:foldl(fun(X, Acc) -> case option(X) of
error ->
Acc;
T ->
[ $ , T | Acc ]
end end, [], List),
Cmd = "sadf " ++ ?DATA ++ " -- " ++ lists:reverse(Args),
{ok, _, Bin} = execute(".", lists:flatten(Cmd)),
extract(Bin);

stats(Elem) ->
stats([Elem]).

option(cpu) ->
"-u";
option(disk) ->
"-d";
option(sock) ->
"-n SOCK";
option(eth0) ->
"-n DEV";
option(eth1) ->
"-n DEV";
option(eth2) ->
"-n DEV";
option(proc) ->
"-c";
option(run) ->
"-q";
option(mem) ->
"-r";
option(inode) ->
"-v";
option(switch) ->
"-w";
option(swaping) ->
"-W";
option(_) ->
error.

execute(_Host, Cmd) ->
Port = open_port({spawn, Cmd}, [ exit_status, binary ] ),
wait(Port, []).

wait(Port, Content) ->
receive
{Port, {data, BinData}} ->
%error_logger:info_msg("dump:~n~p~n", [BinData]),
NewContent = [ BinData | Content ],
wait(Port, NewContent);

{Port, {exit_status, Status}} ->
%error_logger:info_msg("exit_code: ~p~n", [Status]),
{ok, Status, Content};

{Port, eof} ->
%error_logger:info_msg("Port closed"),
port_close(Port),
{ok, eof, Content};

{Port, exit} ->
error_logger:info_msg("Received : ~p~n", [Port]),
Content
end.

extract(Bin) ->
sar_values:extract(iolist_to_binary(Bin)).


You can see the "option/1" function that let you convert atoms into command line arguments easily. I use also this function to test if sar is able to handle a specific parameter. For example and with the help of my webservice I can query remote stats easily:
http://monitoring.lan/stats/q/cpu/servername


Here's the "sar_collect" module

-module(sar_collect).

-export([systat/1, sartime/1, start/0, start/1]).
-export([extract/1]).
spawn(?MODULE, systat, []).

start(Seconds) ->
spawn(?MODULE, systat, [Seconds]).

% update the file every second
systat(0) ->
loop(1);

systat(Seconds) ->
loop(Seconds).

%update the file every 59 seconds
systat() ->
loop(59).

loop(Seconds) when Seconds < 60 ->
Cmd = lists:flatten([ "sar -o /tmp/last.tmp ", integer_to_list(Seconds), " 1" ]),
execute(".", Cmd),
file:rename("/tmp/last.tmp", "/tmp/last"),
timer:sleep(60 - Seconds),
receive
stop ->
exit(normal);

{interval, NewSeconds} ->
loop(NewSeconds);

_A ->
loop(Seconds)

after 0 ->
loop(Seconds)

end;

%default update 20 seconds (arbitrary chosen)
loop(_Seconds) ->
loop(20).

execute(Host, Cmd) ->
Port = open_port({spawn, Cmd}, [ {cd, Host}, exit_status, binary ] ),
wait(Port, []).

wait(Port, Content) ->
receive
{Port, {data, _BinData}} ->
wait(Port, Content);

{Port, {exit_status, _Status}} ->
ok;

{Port, eof} ->
port_close(Port),
Content;

{Port, exit} ->
error_logger:info_msg("Received : ~p~n", [Port]),
Content
end.


Finally there is the "sar_values" source code:

-module(sar_values).

-export([extract/1, extractor/1, sort/1]).
-export([parse/1, parse_value/2]).

extract(Bin) ->
extract(Bin, []).

extract(Bin, Stats) ->
case parse(Bin) of
{Class, Type, Rest} ->
%io:format("~p.~p", [Class, Type]),
case parse_value(Rest, <<>>) of
{more, Value, More} ->
NewStats = [ {Class, Type, Value} | Stats ],
extract(More, NewStats);

{eof, Value} ->
NewStats = [ {Class, Type, Value} | Stats ],
NewStats
end;

eof ->
Stats
end.

parse(<<"%user", Rest/binary >>) -> {cpu, user, Rest};
parse(<<"%nice", Rest/binary>>) -> {cpu, nice, Rest};
parse(<<"%system", Rest/binary>>) -> {cpu, system, Rest};
parse(<<"%iowait", Rest/binary>>) -> {cpu, iowait, Rest};
parse(<<"%steal", Rest/binary>>) -> {cpu, steal, Rest};
parse(<<"%idle", Rest/binary>>) -> {cpu, idle, Rest};

parse(<<"kbmemfree", Rest/binary>>) -> {mem, kbmemfree, Rest};
parse(<<"kbmemused", Rest/binary>>) -> {mem, kbmemused, Rest};
parse(<<"%memused", Rest/binary>>) -> {mem, memused, Rest};
parse(<<"kbbuffers", Rest/binary>>) -> {mem, kbbuffers, Rest};
parse(<<"kbcached", Rest/binary>>) -> {mem, kbcached, Rest};

parse(<<"kbswpfree", Rest/binary>>) -> {swap, free, Rest};
parse(<<"kbswpused", Rest/binary>>) -> {swap, used, Rest};
parse(<<"%swpused", Rest/binary>>) -> {swap, usage, Rest};
parse(<<"kbswpcad", Rest/binary>>) -> {swap, swpcad, Rest};

parse(<<"dentunusd", Rest/binary>>) -> {inode, dentryunused, Rest};
parse(<<"file-sz", Rest/binary>>) -> {inode, fileopened, Rest};
parse(<<"inode-sz", Rest/binary>>) -> {inode, inodes, Rest};
parse(<<"super-sz", Rest/binary>>) -> {inode, super, Rest};
parse(<<"%super-sz", Rest/binary>>) -> {inode, superusage, Rest};
parse(<<"dquot-sz", Rest/binary>>) -> {inode, dquotsz, Rest};
parse(<<"%dquot-sz", Rest/binary>>) -> {inode, dquotszusage, Rest};
parse(<<"rtsig-sz", Rest/binary>>) -> {rtsig, count , Rest};
parse(<<"%rtsig-sz", Rest/binary>>) -> {rtsig, usage, Rest};

parse(<<"totsck", Rest/binary>>) -> {sock, total, Rest};
parse(<<"tcpsck", Rest/binary>>) -> {sock, tcp, Rest};
parse(<<"udpsck", Rest/binary>>) -> {sock, udp, Rest};
parse(<<"rawsck", Rest/binary>>) -> {sock, raw, Rest};
parse(<<"ip-frag", Rest/binary>>) -> {sock, ipfrag, Rest};

parse(<<"runq-sz", Rest/binary>>) -> {procs, running, Rest};
parse(<<"plist-sz", Rest/binary>>) -> {procs, total, Rest};

parse(<<"ldavg-15", Rest/binary>>) -> {load, min15, Rest};
parse(<<"ldavg-1", Rest/binary>>) -> {load, min1, Rest};
parse(<<"ldavg-5", Rest/binary>>) -> {load, min5, Rest};

parse(<<"pswpin/s", Rest/binary>>) -> {swaping, pswpin, Rest};
parse(<<"pswpout/s", Rest/binary>>) -> {swaping, pswpout, Rest};

parse(<<"l0", Rest/binary>>) -> parsebis(Rest, l0);
parse(<<"eth0", Rest/binary>>) -> parsebis(Rest, eth0);
parse(<<"eth1", Rest/binary>>) -> parsebis(Rest, eth1);
parse(<<"eth2", Rest/binary>>) -> parsebis(Rest, eth2);

parse(<<>>) -> eof;

parse(Bin) ->
{_, Next} = split_binary(Bin, 1),
parse(Next).

parsebis(<<"rxpck/s", Rest/binary>>, Category) -> {Category, rxpck, Rest};
parsebis(<<"txpck/s", Rest/binary>>, Category) -> {Category, txpck, Rest};
parsebis(<<"rxbyt/s", Rest/binary>>, Category) -> {Category, rxbyt, Rest};
parsebis(<<"txbyt/s", Rest/binary>>, Category) -> {Category, txbyt, Rest};
parsebis(<<"rxcmp/s", Rest/binary>>, Category) -> {Category, rxcmp, Rest};
parsebis(<<"txcmp/s", Rest/binary>>, Category) -> {Category, txcmp, Rest};
parsebis(<<"rxmcst/s", Rest/binary>>, Category) -> {Category, rxmcst, Rest};
parsebis(Bin, Category) ->
{_, Next} = split_binary(Bin, 1),
parsebis(Next, Category).

parse_value(<<$\t, Rest/binary>>, _Value) ->
parse_value(Rest, _Value);
parse_value(<<$ , Rest/binary>>, _Value) ->
parse_value(Rest, _Value);

parse_value(<<$\n, _Rest/binary>>, Value) ->
{more, Value, _Rest};

parse_value(<<>>, Value) ->
{eof, Value};

parse_value(Bin, Value) ->
{H, Next} = split_binary(Bin, 1),
parse_value(Next, iolist_to_binary([Value, H])).

extractor(Motif) ->
fun(L) when is_list(L) ->
[ {Y, Z} || {X, Y, Z} <- L, X == Motif]
end.

sort(List) ->
lists:sort( fun({X, _V}, {Y, _W}) when X < Y ->
true;
(_A, _B) -> false
end, List).


Now that Erlang is R12B, I'm not so sure if "binary parsing code" is really as efficient as it can...

Thursday, June 26, 2008

Parsing Binaries with erlang, lamers inside

Edit: I don't want to offend anyone with the following, this is just an expression of what i encounter every day with people that do technology but don't know anything. I don't say anyone on the mailling list is a lamer, I say that question like the one evocated here exists because there's too much ignorance in the technology world. And lastly, don't forget this is a personal rant...

It's seems that there's a really high expectation on "parsing binaries" with erlang and reach others languages performance. For me this is a complete nonsense.
What's the meaning of "parsing a binary", a binary is not a text, it's a binary, a sequence of bytes...
A sequence must be defined by its length. You must know before reading anything, the size that'll be needed store what's coming.
Every crap software you can find has always prefered to use strcpy instead of memcpy. Whatever the language you use, you MUST know the size of what you're working with, this is not an advice this is mandatory.

From the post above, you can find that the only delimiter seems to be "\r\n". So if someone sends you 4Gb of data not ending with "\r\n" you'll keep reading it... (and of course blow your memory because this was not supposed to be)

While working at low level with C and flex scanners, I've always ask me this question: "What's the max size of the element I can accept ?". This simple question helps me build software that don't break with a simple 'perl print Ax60000' trick...

So is HTTP badly designed, because delimiters are "\r\n" and headers can spread on multiple lines ? The answer is absolutely YES.
Was'it difficult to build something more secure, using prefixed elements with their size ? The answer is absolutely NO ! (take ajp13 for example...)

Now that erlang is becoming more and more popular, lamers are lurking in the erlang direction. This is life, but will the erlang mailling list suffer from this ? The answer is yes :/

Someone with knowledge must not try to resolve someone's else problem, he must help him by asking the good question. (do you know the size a priori ?)

Why parsing binaries in java is faster than erlang ? Who cares, since parsing binaries is of course stupid !
Parsing real world protocol with erlang is lightning fast, both for writing and for executing. So teach lamers how to build real protocols and don't try help them with some trickery.

That's my rant for today :)

Sunday, March 9, 2008

Directories Recursively and Simple Binary Matching

Hi, It has been a long time :)

So today two simple things first, some high order fun to work on tree:

-module(dir).
-export([create/1]).

create(List) ->
H = fun(X) ->
fun(Y) ->
Dir = filename:join([X,Y]),
io:format("mkdir(~s)~n", [Dir]),
Dir
end
end,
build(fun(X) -> X end, H, List).

build(_Fun, _Builder, []) ->
ok;
build(Fun, Builder, [Elem|List]) ->
NewFun = Builder(Fun(Elem)),
build(NewFun, Builder, List).


A sample session:

5> dir:create(["ab","cd", "ef","12", "35", "av"]).
mkdir(ab/cd)
mkdir(ab/cd/ef)
mkdir(ab/cd/ef/12)
mkdir(ab/cd/ef/12/35)
mkdir(ab/cd/ef/12/35/av)
ok

What's interesting is the Builder fun that construct the fun that will be called the next time.
That way we know were we are in the tree... The last Fun has all the knowledge of its ancestors :)

Now a simple binary matcher code, I need it while extracting values from regex results. It takes a list of offsets and returns what's inside:

slice(Slices, Bin) ->
slice(Slices, [], Bin).

slice([], Acc, _Bin) ->
lists:reverse(Acc);
slice([ {Start, Stop} | Rest ], Acc, Bin) ->
Len = Stop - Start,
<<_:Start/binary,Value:Len/binary,_/binary>> = Bin,
slice(Rest, [ Value | Acc ], Bin).


A sample session:

8> matcher:slice([{1,5},{8,10}], <<"this is a not a solution">>).
[<<"his ">>,<<"a ">>]


I hope that someone will find this valuable...

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 ;]

Saturday, August 11, 2007

Managing lists of binaries or strings...

When you want to deal with binaries, the preserve speed and 'normal' memory footprint there's one fun that you must be aware !

This is 'iolist_to_binary' and it's brother 'iolist_size'.

With theses you can do whatever you need to manager large array of strings, or manage small strings or small binaries... In fact you can do everything.

From my blogger module I need a fun to give me back only binary stuff, for example this fun that just returns a valid 'div' element:

test(Title) ->
[ <<"<div class='title'>">>, iolist_to_binary(Title), <<"</div">> ].

With the 'iolist_to_binary' fun I don't have to deal with guards... ie: I don't need to call 'list_to_binary' if the argument is a list...
Here's some tests:

35> blogger_utils:test("blah"). %this is a string
[<<'"<div class='title'>">>,<<"blah">>,<<"</div">>]

36> blogger_utils:test(<<"blah">>). %this is a binary
[<<"<div class='title'>">>,<<"blah">>,<<"</div">>]

37> blogger_utils:test([<<"blah">>, "bli"]). %this is a list of mixed types
[<<"<div class='title'>">>,<<"blahbli">>,<<"</div">>]

Convenient isn't it ?

Tuesday, August 7, 2007

Blogger_srv.erl version 0.1 is here !

Here's the link to find the 'blogger_srv' you're waiting for: blogger_srv.erl.

This is the first version of the code, I think I'll add other features later:
  • auto reauth whenever the authtoken gets invalid
  • add some support for posting binary data, like images..
  • replace those antislashed double quotes by simple quotes ;)
For the time now, the code works, and all you need to do is starting the server:
blogger_srv:init().

Call the 'auth' fun to obtain a valid 'AuthToken':
blogger_srv:auth("youremail@gmail.com", "yourpass").

Finally call the 'new/4' or 'new/5' fun to post a message:
blogger_srv:new(yourBlogId, Title, [Tags], <<Content>>).
blogger_srv:new(yourBlogId, Title, [Tags], <<Content>>, AuthorName, AuthorEmail).

When the post is successfull, 'new/4' or 'new/5' will returns:
{ok, NewPostId}

That's all for now !

Monday, August 6, 2007

More things about the blogger API...

I've reworked many times the atom template I use to post things to blogger, and what I'll describe here is a design I found simple and efficient, so here's the code :

This is the main function:

entry_new(Title, Author, { Content, ContentType }, Tags) when is_list(Tags) ->
NTitle = post_title(Title),
NContent = post_content(Content, ContentType),
NTags = post_tags(Tags),
NAuthor = post_author(Author),
[
entry_header(),
NTitle,
NContent,
NAuthor,
NTags,
entry_footer()
];

The main idea is to use a list of binary, using this simple methods we don't mess with multiple copies or padding or strings management... This is quick and clean.

Here's some little function to create simple tempates:

entry_new(Title, Author, Content, Tags) ->
entry_new(Title, Author, {Content, text}, Tags).

entry_header() ->
<<"<entry xmlns=\"http://www.w3.org/2005/Atom\">">>.

entry_footer() ->
<<"</entry>">>.

post_title(Title) ->
[ <<"<title type=\"text\">">>, list_to_binary(Title), <<"</title>">> ].

post_author({ AuthorName, AuthorEmail }) ->
[
<<"<author><name>">>, list_to_binary(AuthorName), <<"</name><email>">>,
list_to_binary(AuthorEmail), <<"</email></author>">> ];
post_author(AuthorEmail) ->
post_author({ "", AuthorEmail }).



Here's some other function to carefully set the content-type with the post content:

post_content(Content, ContentType) when is_list(Content) ->
post_content(list_to_binary(Content), ContentType);
post_content(Content, html) ->
post_content(Content, "html");
post_content(Content, xhtml) ->
post_content(Content, "xhtml");
post_content(Content, text) ->
post_content(Content, "text");
post_content(Content, ContentType) ->
[ <<"<content type=\"">>, list_to_binary(ContentType), <<"\">">>,
Content,
<<"</content>">> ].


And now the code the post 'tags':

post_tags([]) ->
<<>>;
post_tags(List) ->
lists:map(
fun(X) ->
[ <<"<category scheme='http://www.blogger.com/atom/ns#' term='">>,
list_to_binary(X) ,
<<"'/>">> ]
end,
List).

The magic stands in the 'category' atom tag.

Tuesday, July 31, 2007

Connecting Erlang to Blogger (Part 2) - Adding an entry

For this second part, we start where we left the last time.
We were able to read the response of a succesful login, data was three lines of key value pairs.
The last line holds the final 'AuthToken' we need to send to the blogger atom post service...

So here's the code to extract the line that begins with the 'Auth' keyword and store the value after the '=' and before the end of line:

extract_auth(<<>>) ->
{error, not_found};
extract_auth(<<"Error=", Rest/binary>>) ->
Size = size(Rest) - 1,
<<Msg:Size/binary, _/binary>> = Rest,
{error, binary_to_list(Msg)};

extract_auth(<<"Auth=", Rest/binary>>) ->
{ok, Rest};
extract_auth(<<_:1/binary, Rest/binary>>) ->
extract_auth(Rest).

Note that we are also able to read 'Error' lines, those lines are sent in case of an failed login attempt...
I can describe what 'extract_auth/1' do like this:
  1. if binary in empty returns {error, not_found}
  2. if binary begins with 'Error' catch it and returns its content with the tuple {error, Msg}
  3. if binary begins with the 'Auth' keyword extract everything till the end of the binary
  4. in any other case extract one character and parse the rest of the binary


To conclude, upon successful login this fun will return:

{ok, "AAAAAQAARAFA..."}

This is, of course our Blogger AuthToken...

Now how can we use it ? This is Simple !
Open a new file named blogger.erl and write something like this:

-module(blogger).
-export([ new/3, post/3, test/2, template/3 ]).

post(AuthToken, Title, Content) ->
Data = template(Title, Content),
request(AuthToken, iolist_to_binary(Data)).

template(Title, Content) when is_list(Content) ->
template(Title, Content, {"none", "none"}).

template(Title, Content, Author) when is_list(Content) ->
template(Title, list_to_binary(Content), Author);

template(Title, Content, Author) ->
{AuthorName, AuthorEmail} = Author,
[ <<"<entry xmlns=\"http://www.w3.org/2005/Atom\">\n<title type=\"text\">\n">>,
list_to_binary(Title),
% <<"</title><content type='xhtml'><div xmlns='http://www.w3.org/1999/xhtml'>">>,
<<"</title>\n<content type=\"text\">">>,
Content,
<<"</content>\n<author><name>">>,
list_to_binary(AuthorName),
<<"</name>">>,
list_to_binary(AuthorEmail),
<<"<
</author>\n</entry>\n">> ].

request(AuthToken, Data) when is_binary(AuthToken) ->
request(binary_to_list(AuthToken), Data);

request(AuthToken, Data) ->
io:format("Sending: ~nContent-length: ~p~nBody:~n~s~n", [ size(Data), Data ]),
Authorization = "GoogleLogin auth=" ++ AuthToken,
Url = "http://www.blogger.com/feeds/199963XXXX081936700/posts/default", % Put your BlogID, this one is invalid
case http:request(post,
{ Url,
[ { "Authorization", Authorization } ],
"application/atom+xml; charset=utf-8", Data},
[ {timeout, 3000}, {sync, false} ],
[ {body_format, binary} ]) of

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

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


Once you're done, you'll be pleased to found that this doesn't work :/.
Yep, I wasn't able to post anything !
May be I've missed something in the documentation, but all I get is an nice SAXexception...

Tuesday, July 24, 2007

Erlang and JBOSS, talking AJP13 ! (PART I)


-module(ajp13).
-export([get/3, cping/2, request/1, hexdump/1]).


Every ajp packets starts with 0x1234. In Erlang when you need to express this thing you just need to use the notation 'Base#number'.
So for our example, here's the 'ajp_header' fun :
   
ajp_header() ->
<<16#12, 16#34>>.


We use the binary notation to write 2 bytes expressed on base 16 (hexadecimal). To be crystal clear hex notation can be written with '16#'... :

Eshell V5.5.1 (abort with ^G)
1> 16#deadbeef.
3735928559

I'm sure you get the point !

Let's comes back to our AJP problem... Now that we can write hexadecimal number we can reread the ajp13 protocol description,
and succesfully start to build a simple packet:

get(Host, Port, Url) ->
H = ajp_header(),
Request = request(Url),
Length = size(Request),
Data = <<H/binary, Length:16, Request/binary>>,

case gen_tcp:connect(Host, Port, [binary, {packet, 0}]) of
{ok, Socket} ->
send(Socket, Data),
loop(Socket);

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

Let's look at a simple command in the ajp13 protocol, the 'ping', here's its implementation:
                                   
cping() ->
<<
10:8
>>.

cping(Host, Port) ->
H = ajp_header(),
Request = cping(),
Length = size(Request),
Data = <<H/binary, Length:16, Request/binary>>,

case gen_tcp:connect(Host, Port, [binary, {packet, 0}]) of
{ok, Socket} ->
send(Socket, Data),
loop(Socket);

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

What's important to see is also that ajp13 is derived from the xdr protocol, where every type is always written with its length... In ajp13 this length is always encoded as two bytes (16bits so max size is 16#ffff ;)

The 'Data' variable is what you should look at :

Data = <<H/binary, Length:16, Request/binary>>,

  • H is the ajp header
  • Length is the length of Request written on 2 bytes (2 * 8)
  • Request is the request

Now that we've sent the packet, we need to catch the response, so here's the 'loop' fun:

loop(Socket) ->
receive
{tcp, Socket, Data} ->
% io:format("~p~n", [Data]),

case ajp_response(Data, Socket) of
{ok, continue} ->
loop(Socket);

{ok, body, Bin} ->
io:format("Body: read ~p bytes~n", [size(Bin)]),
loop(Socket);

{ok, closed} ->
gen_tcp:close(Socket)
end;

{tcp_error, Socket, Error} ->
io:format("Error: ~p~n", [Error]),
loop(Socket);

{tcp_closed, Socket} ->
io:format("Closed~n")

after 8000 ->
io:format("Timeout~n"),
gen_tcp:close(Socket)

end.

Whenever our erlang process will receive a message matching the '{tcp, Socket, Data}' tuple we will parse the 'Data' with the 'ajp_response' fun:

ajp_response(<<65,66,0,2,5,1>>, _Socket) ->
{ok, closed};
ajp_response(<<65,66,Rest/binary>>, Socket) ->
ajp_data_length(Rest, Socket);
ajp_response(Bin, Socket) ->
{ok, body, Bin}.

Yeah ! Polymorphism ! Or matching power ?! Whatever, this completely rox the programming planet !
We are simply matching binary data... Binary data that's sent back to us from the jboss server (in our case).

Ajp13 protocol describes the termination of the request by a packet containing 'AB' followed by the response length '2' bytes which are '5' and '1'.

ajp_response(<<65,66,0,2,5,1>>, _Socket) ->
{ok, closed};

Remember Length is encoded on two bytes: '0,2'...

Now comes the AJP13_FORWARD_REQUEST !!!

request(Request) ->
{Protocol, L0} = ajp_string("HTTP/1.1"),
{Request_uri, L1} = ajp_string(Request),
{Remote_addr, L2} = ajp_string("127.0.0.1"),
{Remote_host, L3} = ajp_string("ajbchecker"),
{Server_name, L4} = ajp_string("www.server-example.com"),

<<
2:8, %byte JK_AJP13_FORWARD_REQUEST
2:8, %byte GET
L0:16, Protocol/binary, %string
L1:16, Request_uri/binary, %string
L2:16, Remote_addr/binary, %string
L3:16, Remote_host/binary, %string
L4:16, Server_name/binary, %string
80:16, %integer
0:8, %boolean
1:16, %integer
16#A0, 16#0B, %Header: Host
L4:16, Server_name/binary, %Servername
16#ff %terminator
>>.

ajp_string(String) ->
S = list_to_binary(String),
Bin = <<S/binary, 0>>,
{Bin, size(Bin) - 1}.



The 'ajp_string/1' is used to calculate the final size of the binary data, and is simply used with the ajp13 string encoding format...

BTW, it's really time consuming to explain code ! When I started this article I was thinking that I'll finish it rather quickly, and I realise now that's not the case, there's so many things to say...
This is why I stop here for the First Part, the next part will come tomorrow...

Sunday, June 10, 2007

Shrink or Strip a binary octet-stream easily

I've found a simple way to suppress ending characters of a Binary without parsing it or change it into a list !

I wanted to remove those <<"\r\n">> characters from lines read from a file, and know that my line are made of 29 characters (reading date strings).

So with this function:

shrinkbin(Data, Size) ->
<<Data:Size/binary-unit:8>>.


I can do:

node> test:shrinkbin(<<"Sun Jun 10 15:20:53 CEST 2007\r\n">>, 29).
<<"Sun Jun 10 15:20:53 CEST 2007">>.


That's it !

Sticky