Tuesday, July 24, 2007

Erlang and JBOSS, talking AJP13 ! (PART I)

-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.

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),

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

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

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),

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

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) ->
{tcp, Socket, Data} ->
% io:format("~p~n", [Data]),

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

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

{ok, closed} ->

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

{tcp_closed, Socket} ->

after 8000 ->


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(""),
{Remote_host, L3} = ajp_string("ajbchecker"),
{Server_name, L4} = ajp_string("www.server-example.com"),

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...

No comments: