diff options
Diffstat (limited to 'deps/bt/src/obex.erl')
-rw-r--r-- | deps/bt/src/obex.erl | 1696 |
1 files changed, 1696 insertions, 0 deletions
diff --git a/deps/bt/src/obex.erl b/deps/bt/src/obex.erl new file mode 100644 index 0000000..c523413 --- /dev/null +++ b/deps/bt/src/obex.erl @@ -0,0 +1,1696 @@ +%%%---- BEGIN COPYRIGHT ------------------------------------------------------- +%%% +%%% Copyright (C) 2006 - 2014, Rogvall Invest AB, <tony@rogvall.se> +%%% +%%% This software is licensed as described in the file COPYRIGHT, which +%%% you should have received as part of this distribution. The terms +%%% are also available at http://www.rogvall.se/docs/copyright.txt. +%%% +%%% You may opt to use, copy, modify, merge, publish, distribute and/or sell +%%% copies of the Software, and permit persons to whom the Software is +%%% furnished to do so, under the terms of the COPYRIGHT file. +%%% +%%% This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY +%%% KIND, either express or implied. +%%% +%%%---- END COPYRIGHT --------------------------------------------------------- +%%%------------------------------------------------------------------- +%%% File : obex.erl +%%% Author : Tony Rogvall <tony@PBook.local> +%%% Description : OBEX implemenation +%%% +%%% Created : 19 May 2006 by Tony Rogvall <tony@PBook.local> +%%%------------------------------------------------------------------- +-module(obex). + +-behaviour(gen_server). + +%% API +-export([open/2, open/3, close/1]). +-export([connect/1, connect/2]). +-export([disconnect/1, disconnect/2]). +-export([put_chunk_start/2, put_chunk/2, put_chunk_end/2]). +-export([put/3]). +-export([get_chunk_start/2, get_chunk/1]). +-export([get/2]). +-export([abort/2]). +-export([setpath/2, setpath/3]). +-export([command/2]). +%% SERVER API +-export([server/2, server_link/2]). +-export([server_init/6]). + + +%% Xtra - API +-export([connect_dm_client/1]). +-export([connect_syncml_client/1]). +-export([connect_folder_browsing/1]). +-export([set_cwd/2, make_dir/2]). +-export([list_dir/1]). +-export([capability/1, object_profile/2]). +-export([client/4, client_link/4]). +-export([client/3, client_link/3]). +-export([time_iso/1, utc_time_iso/1]). + +%% Debug +-export([encode_headers/1, decode_headers/1, decode_packet/2]). +-export([size_header/2, size_headers/1]). +-export([encode_header/2]). +-export([make_packets/4, make_get_packets/2]). + +-import(lists, [reverse/1, member/2, foldl/3]). + +-include("../include/obex.hrl"). +-include("../include/uuid.hrl"). +-include("../include/sdp.hrl"). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-ifdef(debug). +-define(dbg(Fmt,As), io:format("~s:~w:" Fmt "\n", [?FILE,?LINE | As])). +-else. +-define(dbg(Fmt,As), ok). +-endif. + + +-record(obex_packet, + { + final, %% 0 or 1 + opcode, %% 7 bit opcode + args, %% extra args (list) + headers, %% headers + callback %% packet reply callback + }). + +-record(s, + { + role, %% client | server + transport, %% transport module + ref, %% transport reference + sref, %% pid - server handle + smod, %% server module + sstate, %% server state + channel, %% channel/port number + id, %% connection ID when connected + %% Host side info + host_mtu = ?OBEX_DEFAULT_MTU, %% our recieve buffer size + host_flags = 0, %% out Flags + host_version = ?OBEX_VERSION, %% our OBEX version + %% Peer side info + peer_mtu = ?OBEX_DEFAULT_MTU, %% peer recive buffer size + peer_flags = 0, %% peer flags + peer_version = 0, %% peer OBEX version + %% Receive buffer + recv_remain = -3, %% data remain to be received + recv_buffer = <<>>, %% input buffer + recv_queue = [], %% input packet queue + %% Send buffer + send_queue = [], %% output packet queue + %% + opts=[] %% obex options + }). + + +-define(size_byte_sequence(Value), + if is_binary((Value)) -> size((Value)); + is_list((Value)) -> length((Value)) + end). + +-define(size_unicode(Value), + if (Value) == "" -> 0; + true -> 2*(length((Value))+1) + end). + +%% Note: The length field includes it self and the tag! +-define(enc_byte_sequence(ID, Value), + if is_binary((Value)) -> + [<<(ID):8, (size((Value))+3):16>>, Value]; + is_list((Value)) -> + [<<(ID):8, (length((Value))+3):16>>, Value] + end). + +%% For null terminate byte-ascii sequences +-define(enc_null_sequence(ID, Value), + if is_binary((Value)) -> + [<<(ID):8, (size((Value))+4):16>>, Value, 0]; + is_list((Value)) -> + [<<(ID):8, (length((Value))+4):16>>, Value, 0] + end). + +%%==================================================================== +%% API +%%==================================================================== +open(Address, Channel) -> + open(Address, Channel, [{transport,rfcomm}]). + +open(Address, Channel, Opts) -> + client_link(Address, Channel, Opts). + +close(Obex) -> + gen_server:call(Obex, close). + +%% +%% OBEX CONNECT +%% +connect(Obex) -> + connect(Obex, []). + +connect(Obex, Headers) -> + gen_server:call(Obex, {connect,Headers}, 10000). + +%% +%% OBEX DISCONNECT +%% +disconnect(Obex) -> + disconnect(Obex, []). + +disconnect(Obex, Headers) -> + gen_server:call(Obex, {disconnect,Headers}, 10000). + +%% +%% OBEX PUT +%% +put_chunk_start(Obex, Headers) -> + gen_server:call(Obex, {put_chunk_start, Headers}, 10000). +put_chunk(Obex, Data) -> + gen_server:call(Obex, {put_chunk, Data}, 10000). +put_chunk_end(Obex, Data) -> + gen_server:call(Obex, {put_chunk_end, Data}, 10000). + +put(Obex, Headers, Body) -> + case put_chunk_start(Obex, Headers) of + continue -> + put_chunk_end(Obex,Body); + Error -> Error + end. + +%% +%% OBEX GET +%% +get_chunk_start(Obex, Headers) -> + gen_server:call(Obex, {get_chunk_start, Headers}, 10000). +get_chunk(Obex) -> + gen_server:call(Obex, get_chunk, 10000). + +get(Obex, Headers) -> + case get_chunk_start(Obex, Headers) of + {continue,Hs} -> + get_continue(Obex,[Hs]); + {ok,Hs} -> get_reply([Hs]); + Error -> Error + end. + +get_continue(Obex, Acc) -> + case get_chunk(Obex) of + {continue,Hs} -> get_continue(Obex,[Hs|Acc]); + {ok,Hs} -> get_reply([Hs|Acc]); + Error -> Error + end. + +%% Generate a sorted and useful combined reply +get_reply([]) -> + {ok,[],<<>>}; +get_reply([Hs|HsList]) -> + get_reply(reverse(Hs),HsList,[],[]). + +get_reply([],[],HAcc,BAcc) -> + {ok,reverse(HAcc),list_to_binary(BAcc)}; +get_reply([{bodyEnd,B}|Hs],HsList,HAcc,BAcc) -> + get_reply(Hs, HsList, HAcc, [B | BAcc]); +get_reply([{body,B}|Hs],HsList,HAcc,BAcc) -> + get_reply(Hs, HsList, HAcc, [B | BAcc]); +get_reply([{H,V}|Hs],HsList,HAcc,BAcc) -> + get_reply(Hs, HsList, [{H,V}|HAcc], BAcc); +get_reply([], [Hs|HsList], HAcc, BAcc) -> + get_reply(reverse(Hs), HsList, HAcc, BAcc). + +%% +%% OBEX ABORT +%% +abort(Obex, Headers) -> + gen_server:call(Obex, {abort,Headers}, 10000). + +%% +%% OBEX SETPATH +%% +setpath(Obex,Hs) -> + setpath(Obex,[],Hs). + +setpath(Obex,Opts,Hs) -> + gen_server:call(Obex, {setpath,Opts,Hs}, 10000). + +%% +%% COMMAND +%% +command(Obex, Hs) -> + gen_server:call(Obex, {command, Hs}, 10000). + +%% +%% UTIL COMMANDS +%% + +%% Special - simpified connectes +connect_dm_client(Obex) -> + connect(Obex, [{target,?TARGET_SYNCML_DM}]). + +connect_syncml_client(Obex) -> + connect(Obex, [{target,?TARGET_SYNCML_SYNC}]). + +connect_folder_browsing(Obex) -> + connect(Obex, [{target,?TARGET_FOLDER_BROWSING}]). + +%% requires - connect_folder_browsing +set_cwd(Obex, "..") -> + setpath(Obex,[backup,dont_create],[]); +set_cwd(Obex, Dir) -> + setpath(Obex,[dont_create],[{name,Dir}]). + +%% both connect_folder_browsing and file transfer +make_dir(Obex, Dir) -> + setpath(Obex,[],[{name,Dir}]). + +%% requires - connect_folder_browsing +list_dir(Obex) -> + case get(Obex, [{type,"x-obex/folder-listing"}]) of + {ok,_Hs,Bin} -> + makeup:string(Bin); + Error -> + Error + end. + +%% requires - connect [] +capability(Obex) -> + get(Obex, [{type,"x-obex/capability"}]). + +%% requires - connect [] +object_profile(Obex, MimeType) -> + get(Obex, [{type,"x-obex/object-profile"}, + {name,MimeType}]). + +%% +%% START Functions +%% +client(Address, Channel, Opts) -> + gen_server:start(?MODULE, [Address,Channel,Opts], []). + +client(Name, Address, Channel, Opts) -> + gen_server:start({local,Name},?MODULE,[Address,Channel,Opts],[]). + +client_link(Address, Channel, Opts) -> + gen_server:start_link(?MODULE, [Address,Channel,Opts], []). + +client_link(Name, Address, Channel, Opts) -> + gen_server:start_link({local,Name},?MODULE,[Address,Channel,Opts],[]). + +%%==================================================================== +%% gen_server callbacks +%%==================================================================== + +%%-------------------------------------------------------------------- +%% Function: init(Args) -> {ok, State} | +%% {ok, State, Timeout} | +%% ignore | +%% {stop, Reason} +%% Description: Initiates the server +%%-------------------------------------------------------------------- +init([Address,Channel,Opts]) -> + case transport(Opts, Address) of + undefined -> {stop, {error, unknown_transport}}; + Transport -> + Mod = transport_module(Transport), + S0 = #s { role = client, + transport = Mod, + host_version = ?OBEX_VERSION, + host_mtu = getopt(mtu,Opts,?OBEX_DEFAULT_MTU), + host_flags = getopt(mtu,Opts,0), + opts = Opts + }, + TransOpts = getopt(transport_options, Opts, [binary]), + case catch Mod:open(Address,Channel,TransOpts) of + {ok,Ref} -> + {ok, S0#s{ ref=Ref }}; + {'EXIT',Reason} -> + {stop, {error, Reason}}; + Error -> + {stop, Error} + end + end. + +%%-------------------------------------------------------------------- +%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | +%% {reply, Reply, State, Timeout} | +%% {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, Reply, State} | +%% {stop, Reason, State} +%% Description: Handling call messages +%%-------------------------------------------------------------------- + +handle_call({connect,Hs},From,S) -> + Args = [S#s.host_version, S#s.host_flags, S#s.host_mtu], + P = #obex_packet { final = 1, + opcode = connect, + args = Args, + headers = Hs, + callback = fun(S0,#obex_packet { opcode={ok,What}, + headers=Hs1, + args=[Version,Flags1,Mtu] + }) -> + %% Keep track on connectionID + %% this simplifies alot... + ID=getopt(connectionID,Hs1), + gen_server:reply(From, {ok,What}), + {noreply,S0#s { id = ID, + peer_mtu = Mtu, + peer_flags=Flags1, + peer_version = Version }}; + (S0,#obex_packet { opcode=Error }) -> + gen_server:reply(From, Error), + {noreply, S0} + end + }, + send_packet(P, S); + +handle_call({disconnect,Hs}, From, S) -> + P = #obex_packet { final = 1, + opcode = disconnect, + headers = prepare_headers(Hs,S), + callback = fun(S0,#obex_packet { opcode=Reply }) -> + gen_server:reply(From, Reply), + {noreply,S0#s { id=undefined }} + end + }, + send_packet(P, S); + +handle_call({abort,Hs}, From, S) -> + P = #obex_packet { final = 1, + opcode = abort, + headers = prepare_headers(Hs,S), %% needed? + callback = fun(S0,#obex_packet { opcode=Reply }) -> + gen_server:reply(From, Reply), + {noreply,S0} + end + }, + send_packet(P, S); + +handle_call({get_chunk_start,Hs}, From, S) -> + Hs1 = prepare_headers(Hs, S), + handle_get_chunk_start(Hs1, From, S); + +handle_call(get_chunk, From, S) -> + handle_get_chunk(From, S); + +handle_call({put_chunk_start,Hs}, From, S) -> + Hs1 = prepare_headers(Hs, S), + handle_put_chunk_start(Hs1, From, S); + +handle_call({put_chunk,Data}, From, S) -> + handle_put_chunk(Data, From, S); + +handle_call({put_chunk_end,Data}, From, S) -> + handle_put_chunk_end(Data, From, S); + +handle_call({setpath,Opts,Hs}, From, S) -> + %% MUST FIT in one packet + Constants = 0, %% reserved MUST be 0 (version=1.2) + Flags = case member(backup, Opts) of + true -> 16#01; + false -> 16#00 + end bor + case member(dont_create, Opts) of + true -> 16#02; + false -> 16#00 + end, + P = #obex_packet { final = 1, + opcode = setpath, + args = [Flags, Constants], + headers = prepare_headers(Hs, S), + callback = fun(S0,#obex_packet { opcode=Reply }) -> + gen_server:reply(From, Reply), + {noreply,S0} + end + }, + send_packet(P, S); + +handle_call({command,Hs}, From, S) -> + P = #obex_packet { final = 1, + opcode = command, + headers = prepare_headers(Hs, S), + callback = fun (S0,_) -> + gen_server:reply(From, ok), + {noreply,S0} + end + }, + send_packet(P, S); + +handle_call(close, From, S) -> + if S#s.ref == undefined -> + {stop, normal, ok, S}; + true -> + Callback = fun(S0,_) -> + gen_server:reply(From, ok), + ?dbg("Transport close: ~w\n", [S#s.ref]), + (S#s.transport):close(S#s.ref), + {stop, normal, S0#s { ref=undefined }} + end, + P = #obex_packet { final = 1, + callback = Callback, + opcode = undefined, %% sync packet + headers = [] }, + send_packet(P, S) + end; + +handle_call(_Request, _From, S) -> + {reply, {error, bad_call}, S}. + +%%-------------------------------------------------------------------- +%% Function: handle_cast(Msg, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% Description: Handling cast messages +%%-------------------------------------------------------------------- +handle_cast(_Msg, S) -> + {noreply, S}. + +%%-------------------------------------------------------------------- +%% Function: handle_info(Info, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% Description: Handling all non call/cast messages +%%-------------------------------------------------------------------- + +handle_info({rfcomm,Ref,{data,Data}}, S) when S#s.ref == Ref -> + ?dbg("RFCOMM: Packet received: ~p\n", [Data]), + recv(Data, S); +handle_info({tcp,Ref,Data}, S) when S#s.ref == Ref -> + ?dbg("TCP: Packet received: ~p\n", [Data]), + recv(Data, S); +handle_info({rfcomm,Ref,closed}, S) when S#s.ref == Ref -> + ?dbg("RFCOMM: closed\n", []), + recv_closed(S); +handle_info({tcp_closed,Ref}, S) when S#s.ref == Ref -> + ?dbg("TCP: closed\n", []), + recv_closed(S); +handle_info(_Info, S) -> + ?dbg("Unhandled INFO ~p\n", [_Info]), + {noreply, S}. + +%%-------------------------------------------------------------------- +%% Function: terminate(Reason, State) -> void() +%% Description: This function is called by a gen_server when it is about to +%% terminate. It should be the opposite of Module:init/1 and do any necessary +%% cleaning up. When it returns, the gen_server terminates with Reason. +%% The return value is ignored. +%%-------------------------------------------------------------------- +terminate(_Reason, S) -> + if S#s.ref == undefined -> + ok; + true -> + ?dbg("Transport close: ~w\n", [S#s.ref]), + (S#s.transport):close(S#s.ref) + end. + + +%%-------------------------------------------------------------------- +%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} +%% Description: Convert process state when code is changed +%%-------------------------------------------------------------------- +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%%% Internal functions +%%-------------------------------------------------------------------- + +%% Get transport from options +transport(Opts) -> + transport(Opts, undefined). + +transport(Opts, Address) -> + case getopt(transport, Opts) of + undefined -> + if Address == undefined -> rfcomm; + true -> + case inet_parse:address(Address) of + {ok, _} -> tcp; + _ -> + case bt_util:getaddr(Address) of + {ok,_} -> rfcomm; + _ -> + case inet_parse:domain(Address) of + true -> tcp; + _ -> undefined + end + end + end + end; + Transport when is_atom(Transport) -> Transport; + _ -> undefined + end. + +transport_module(rfcomm) -> obex_rfcomm; +transport_module(tcp) -> obex_tcp; +transport_module(T) -> list_to_atom("obex_" ++ atom_to_list(T)). + +server_link(Channel, Opts) -> + case transport(Opts) of + undefined -> + {error, unknown_transport}; + Transport -> + Mod = transport_module(Transport), + Mod:server_link(Channel, Opts) + end. + +server(Channel, Opts) -> + case transport(Opts) of + undefined -> + {error, unknown_transport}; + Transport -> + Mod = transport_module(Transport), + Mod:server(Channel, Opts) + end. + + +%% A new connection from Transport backend +server_init(SrvHandle, Ref, _Address, Channel, Transport, Opts) -> + ?dbg("obex [~w] Accept from ~w on channel=~w\n", + [Transport,_Address,Channel]), + Wait = #obex_packet { final = 1, + opcode = wait, + callback = fun handle_connect/2 }, + S = #s { role = server, + sref = SrvHandle, + transport = Transport, + ref = Ref, + channel = Channel, + host_mtu = getopt(mtu,Opts,?OBEX_DEFAULT_MTU), + host_flags = getopt(flags,Opts,0), + host_version = ?OBEX_VERSION, + opts = Opts, + send_queue = [Wait] + }, + gen_server:enter_loop(?MODULE, [], S). + +%% handle connect request +handle_connect(S0,#obex_packet { opcode=connect, + headers=Hs, + args=[Version,Flags,Mtu] }) -> + ?dbg("handle_connect: version=~w, flags=~w, mtu=~w, hs=~w", + [Version,Flags,Mtu,Hs]), + S = S0#s { peer_version=Version, peer_mtu=Mtu,peer_flags=Flags }, + case lists:keysearch(target, 1, Hs) of + false -> + handle_connect_target(S, undefined, obex_generic); + {value,{_,Target}} -> + case (S#s.transport):lookup(S#s.sref, Target) of + {error, not_found} -> + Packet = #obex_packet { final = 1, + opcode = {error,not_found}, + headers = [], + callback = fun handle_connect/2 }, + send_packet(Packet, S); + {ok,Module} -> + handle_connect_target(S, Target, Module) + end + end; +handle_connect(S,P=#obex_packet { opcode=_Opcode }) -> + ?dbg("handle_connect: opcode ~w", [_Opcode]), + TS = obex_generic:init(S#s.sref, S#s.opts, S#s.peer_mtu), + handle_request(S#s { smod=obex_generic, sstate=TS},P). + +%% +%% Connected to target handled by Mod +%% +handle_connect_target(S, Target, Mod) -> + TS = Mod:init(S#s.sref, S#s.opts, S#s.peer_mtu), + case target_connect(Target, S#s { smod=Mod, sstate=TS}) of + {reply,{ok,Hs}, S1} -> + Packet = #obex_packet { final = 1, + opcode = connect_response, + args=[S#s.host_version, + S#s.host_flags, + S#s.host_mtu], + headers = Hs, + callback = fun handle_request/2 }, + send_packet(Packet, S1); + {reply,Error,S1} -> + Packet = #obex_packet { final = 1, + opcode = Error, + headers = [], + callback = fun handle_request/2 }, + send_packet(Packet, S1) + end. + +%% +%% Collect ALL get headers before doing the target callback +%% +next_get(HsAcc) -> + fun(S, #obex_packet { final=0, opcode=get, headers=Hs }) -> + ?dbg("GET continue request ~p\n", [Hs]), + Packet = #obex_packet { final=0, opcode=continue, headers=[], + callback=next_get(HsAcc++Hs) }, + send_packet(Packet, S); + (S, P=#obex_packet { final=1, opcode=get, headers=Hs }) -> + handle_request(S,P#obex_packet { headers=HsAcc++Hs }); + (S, _P=#obex_packet { final=1, opcode=abort }) -> + Packet = #obex_packet { final=0, opcode={ok,success}, headers=[], + callback=fun handle_request/2 }, + send_packet(Packet, S); + (S, _P) -> + ?dbg("Got ~p while collecting GET headers",[_P#obex_packet.opcode]), + Packet = #obex_packet { final=0, opcode={error,conflict}, headers=[], + callback=fun handle_request/2 }, + send_packet(Packet, S) + end. +%% +%% Collect ALL put headers before doing the target callback +%% +next_put(HsAcc) -> + fun(S, #obex_packet { final=0, opcode=put, headers=Hs }) -> + ?dbg("PUT continue request ~p\n", [Hs]), + continue_put(S,Hs,HsAcc); + (S, #obex_packet { final=1, opcode=put, headers=Hs }) -> + final_put(S,Hs,HsAcc); + (S, _P=#obex_packet { final=1, opcode=abort }) -> + Packet = #obex_packet { final=0, opcode={ok,success}, headers=[], + callback=fun handle_request/2 }, + send_packet(Packet, S); + (S, _P) -> + ?dbg("Got ~p while collecting PUT headers",[_P#obex_packet.opcode]), + Packet = #obex_packet { final=0, opcode={error,conflict}, + headers=[], + callback=fun handle_request/2 }, + send_packet(Packet, S) + end. + + +continue_put(S,Hs,HsAcc) -> + case split_headers(Hs) of + {true,Hs1,Data} -> %% bodyEnd in non final? trailer? + ?dbg("got bodyEnd in continue put packet!", []), + {reply,Reply,S1} = target_put(HsAcc++Hs1, Data, S), + Packet = #obex_packet { final=1, opcode=Reply, + headers=[], + callback=fun handle_request/2 + }, + send_packet(Packet, S1); + {false,Hs1,<<>>} -> %% no data no end + Packet = #obex_packet { final=1, opcode=continue, + headers=[], + callback=next_put(HsAcc++Hs1) + }, + send_packet(Packet,S); + {false,Hs1,Data} -> %% data found not end + {reply,Reply,S1} = target_put(HsAcc++Hs1, Data, S), + Packet = #obex_packet { final=1, opcode=Reply, + headers=[], + callback=fun handle_request/2 }, + send_packet(Packet,S1) + end. + +final_put(S,Hs,HsAcc) -> + case split_headers(Hs) of + {true,Hs1,Data} -> + case target_put(HsAcc++Hs1, Data, S) of + {reply,continue,S1} -> + {reply,Reply,S2} = target_put([],<<>>,S1), + Packet = #obex_packet { final=1, opcode=Reply, + headers=[], + callback=fun handle_request/2 + }, + send_packet(Packet,S2); + {reply,Reply,S1} -> + Packet = #obex_packet { final=1, opcode=Reply, + headers=[], + callback=fun handle_request/2 + }, + send_packet(Packet,S1) + end; + {false,_Hs1,_Data} -> + ?dbg("missing bodyEnd in final put packet!", []), + Packet = #obex_packet { final=1, opcode={error,bad_request}, + headers=[], + callback=fun handle_request/2}, + send_packet(Packet, S) + end. + +%% +%% Send GET reply packets +%% +send_reply_packets([], _End, S) -> + ?dbg("send_reply_packets: NO packets to send???",[]), + {noreply, S}; +send_reply_packets([P], End, S) -> + P1 = if End == true -> + P#obex_packet { final=1,opcode={ok,success}, + callback=fun handle_request/2}; + true -> + P#obex_packet { final=1, opcode=continue, + callback=fun handle_request/2} + end, + send_packet(P1, S); +send_reply_packets([P|Ps], End, S) -> + Callback = fun(S0,_Pi=#obex_packet { opcode=get }) -> %% ignoring final!!! + send_reply_packets(Ps, End, S0); + (S0,Pi) -> + handle_request(S0, Pi) + end, + P1 = P#obex_packet { final=1, opcode=continue, callback=Callback }, + send_packet(P1, S). + +%% +%% Handle GET +%% +handle_request(S,#obex_packet { final=1, opcode=get, headers=Hs }) -> + ?dbg("GET final=1 request ~p\n", [Hs]), + case target_get(Hs, S) of + {reply,{continue,ReplyHs,Data},S1} -> + Ps = make_packets(ReplyHs, Data, false, S1#s.peer_mtu), + send_reply_packets(Ps, false, S1); + {reply,{ok,ReplyHs,Data},S1} -> + Ps = make_packets(ReplyHs, Data, true, S1#s.peer_mtu), + send_reply_packets(Ps, true, S1); + {reply,Error,S1} -> + Packet = #obex_packet { final=1, + opcode=Error, + headers=[], + callback=fun handle_request/2}, + send_packet(Packet, S1) + end; +handle_request(S,#obex_packet { final=0, opcode=get, headers=Hs }) -> + ?dbg("GET continue request ~p\n", [Hs]), + Packet = #obex_packet { final=0, opcode=continue, headers=[], + callback=next_get(Hs) }, + send_packet(Packet, S); +%% +%% Handle PUT (collect headers until we find body or bodyEnd) +%% +handle_request(S,#obex_packet { final=0, opcode=put, headers=Hs}) -> + ?dbg("PUT continue request ~p\n", [Hs]), + continue_put(S,Hs,[]); +handle_request(S,#obex_packet { final=1, opcode=put, headers=Hs}) -> + ?dbg("PUT final request ~p\n", [Hs]), + final_put(S,Hs,[]); + +%% +%% Handle SETPATH +%% +handle_request(S,#obex_packet { final=1, opcode=setpath, args=Args, headers=Hs }) -> + ?dbg("SETPATH hs=~p\n", [Hs]), + {reply,Reply,S1} = target_setpath(Hs,Args,S), + Packet = #obex_packet { final=1, opcode=Reply, headers=[], + callback=fun handle_request/2 }, + send_packet(Packet, S1); +%% +%% ABORT +%% +handle_request(S,#obex_packet { final=1, opcode=abort, headers=Hs }) -> + ?dbg("ABORT hs=~p\n", [Hs]), + {reply,Reply,S1} = target_abort(Hs, S), + Packet = #obex_packet { final=1, opcode=Reply, headers=[], + callback=fun handle_request/2 }, + send_packet(Packet, S1); +%% +%% DISCONNECT +%% +handle_request(S,#obex_packet { final=1, opcode=disconnect, headers=Hs }) -> + ?dbg("DISCONNECT hs=~p\n", [Hs]), + {reply,Reply,S1} = target_disconnect(Hs, S), + Packet = #obex_packet { final=1, opcode=Reply, headers=[], + callback=fun handle_request/2 }, + send_packet(Packet, S1); +%% +%% COMMAND +%% +handle_request(S,#obex_packet { final=1, opcode=command, headers=Hs }) -> + ?dbg("COMMAND hs=~p\n", [Hs]), + {reply,Reply,S1} = target_command(Hs, S), + Packet = #obex_packet { final=1, opcode=Reply, headers=[], + callback=fun handle_request/2 }, + send_packet(Packet, S1); +%% +%% OTHER packets (final=0 strange opcodes etc) +%% +handle_request(S,_P) -> + ?dbg("OTHER packet=~p", [_P]), + Packet = #obex_packet { final=1, opcode={error,bad_request}, headers=[], + callback=fun handle_request/2 }, + send_packet(Packet, S). + + +%% Wrap calls to target +target_connect(Target, S) -> + {reply,Reply,TS} = (S#s.smod):handle_connect(Target,S#s.sstate), + {reply,Reply,S#s { sstate = TS}}. + +target_disconnect(Hs, S) -> + {reply,Reply,TS} = (S#s.smod):handle_disconnect(Hs,S#s.sstate), + {reply,Reply,S#s { sstate = TS}}. + +target_get(Hs, S) -> + {reply,Reply,TS} = (S#s.smod):handle_get(Hs,S#s.sstate), + {reply,Reply,S#s { sstate = TS}}. + +target_put(Hs, Data, S) -> + {reply,Reply,TS} = (S#s.smod):handle_put(Hs,Data,S#s.sstate), + {reply,Reply,S#s { sstate = TS}}. + +target_abort(Hs, S) -> + {reply,Reply,TS} = (S#s.smod):handle_abort(Hs,S#s.sstate), + {reply,Reply,S#s { sstate = TS}}. + +target_setpath(Hs,Args,S) -> + {reply,Reply,TS} = (S#s.smod):handle_setpath(Hs,Args,S#s.sstate), + {reply,Reply,S#s { sstate = TS}}. + +target_command(Hs, S) -> + {reply,Reply,TS} = (S#s.smod):handle_command(Hs,S#s.sstate), + {reply,Reply,S#s { sstate = TS}}. + + +%% update header with connection id +prepare_headers(Hs, S) -> + if S#s.id == undefined -> + Hs; + true -> + [{connectionID,S#s.id}|Hs] + end. + +handle_get_chunk_start(Hs, From, S) -> + Packets = make_get_packets(Hs, S#s.peer_mtu), + send_get_packets(Packets, From, S). + +handle_get_chunk(From, S) -> + send_get_packet_1(#obex_packet { opcode=get, headers=[] }, From, S). + +handle_put_chunk_start(Hs, From, S) -> + Packets = make_packets(Hs, <<>>, false, S#s.peer_mtu), + send_put_packets(Packets, 0, From, S). + +handle_put_chunk(Data, From, S) -> + Packets = make_packets([], Data, false, S#s.peer_mtu), + send_put_packets(Packets, 0, From, S). + +handle_put_chunk_end(Data, From, S) -> + Packets = make_packets([], Data, true, S#s.peer_mtu), + send_put_packets(Packets, 1, From, S). + + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% send_get_packets: +%% Send the GET header packets util all packets have been sent +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +send_get_packets([], From, S) -> + %% send a empty get packet + send_get_packet_1(#obex_packet { opcode=get, headers=[] }, From, S); +send_get_packets([P], From, S) -> + send_get_packet_1(P, From, S); +send_get_packets([P|Ps], From, S) -> + Callback = fun(S0,_Pi=#obex_packet { final=0, opcode=_Reply}) -> + ?dbg("send_get_packets: input=~p\n", [_Pi]), + send_get_packets(Ps, From, S0); + (S0, _Pi=#obex_packet { final=1, opcode=Reply}) -> + ?dbg("send_get_packets: input=~p\n", [_Pi]), + gen_server:reply(From, Reply), + {noreply,S0} + end, + Pout = P#obex_packet { final=0, callback=Callback}, + send_packet(Pout, S). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% Send the GET final=1 packet +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +send_get_packet_1(P, From, S) -> + Callback = + fun (S0,_P=#obex_packet { final=1,opcode=continue,headers=Hs}) -> + ?dbg("send_get_packet_1: input=~p\n", [_P]), + gen_server:reply(From, {continue,Hs}), + {noreply,S0}; + (S0, _P=#obex_packet { final=1,opcode=Reply,headers=Hs}) -> + ?dbg("send_get_packet_1: input=~p\n", [_P]), + case Reply of + {ok,success} -> + gen_server:reply(From, {ok,Hs}); + Error -> + gen_server:reply(From, Error) + end, + {noreply,S0}; + (S0,_P=#obex_packet { final=0,opcode=_Reply,headers=Hs}) -> + ?dbg("send_get_packet_1: input=~p\n", [_P]), + %% request more request headers !!! + gen_server:reply(From, {more, Hs}), + {noreply,S0} + end, + Packet = P#obex_packet { final= 1, callback = Callback }, + send_packet(Packet, S). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% send_put_packets: +%% Send the PUT packets util all packets have been sent +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +send_put_packets([], _Finally, _From, S) -> + {noreply, S}; +send_put_packets([P], Finally, From, S) -> + Callback = fun(S0,_Pi=#obex_packet { final=0, opcode=Reply}) -> + ?dbg("handle_put: input=~p\n", [_Pi]), + gen_server:reply(From, Reply), + {noreply,S0}; + (S0, _Pi=#obex_packet { final=1, opcode=Reply}) -> + ?dbg("handle_put: input=~p\n", [_Pi]), + gen_server:reply(From, Reply), + {noreply,S0} + end, + Pout = P#obex_packet { opcode=put, final=Finally, callback=Callback}, + send_packet(Pout, S); +send_put_packets([P|Ps], Finally, From, S) -> + Callback = fun(S0,_Pi=#obex_packet { final=0, opcode=_Reply}) -> + ?dbg("handle_put: input=~p\n", [_Pi]), + send_put_packets(Ps, Finally, From, S0); + (S0, _Pi=#obex_packet { final=1, opcode=Reply}) -> + ?dbg("handle_put: input=~p\n", [_Pi]), + gen_server:reply(From, Reply), + {noreply,S0} + end, + Pout = P#obex_packet { opcode=put, final=0, callback=Callback}, + send_packet(Pout, S). + +recv_closed(S) when S#s.role == server -> + ?dbg("server connection closed (normal)\n", []), + {stop, normal, S}; +recv_closed(S) when S#s.role == client -> + ?dbg("client connection closed\n", []), + {noreply, S#s { ref=undefined, id=undefined }}. + +recv(Data, S) -> + Buf = recv_concat(Data, S#s.recv_buffer), + BufLen = size(Buf), + Remain = S#s.recv_remain, + if Remain < 0 -> + if BufLen + Remain < 0 -> + {noreply, S#s { recv_remain=BufLen+Remain, + recv_buffer = Buf }}; + true -> + <<_OP:8,PacketLen:16,_/binary>> = Buf, + if BufLen < PacketLen -> + {noreply, S#s { recv_remain=PacketLen-BufLen, + recv_buffer=Buf }}; + BufLen >= PacketLen -> + <<Packet:PacketLen/binary,Buf1/binary>> = Buf, + recv_packet(Packet, Buf1, S#s { recv_remain=0, + recv_buffer=(<<>>) + }) + end + end; + Remain > 0 -> + if BufLen < Remain -> + {noreply, S#s { recv_remain=Remain - BufLen, + recv_buffer=Buf }}; + true -> + <<_OP:8,PacketLen:16,_/binary>> = Buf, + <<Packet:PacketLen/binary, Buf1/binary>> = Buf, + recv_packet(Packet, Buf1, S#s { recv_remain=0, + recv_buffer=(<<>>) }) + end + end. + +recv_packet(Data, Buf, S) -> + Cmd = case S#s.send_queue of + [P|_] -> P#obex_packet.opcode; + [] -> undefined + end, + Reply = decode_packet(Data, Cmd), + ?dbg("recv_packet: ~p\n", [Reply]), + S1 = push_packets(Buf, S), %% push rest of packet data + case S1#s.send_queue of + [#obex_packet { callback=Callback }|_] -> + %% Note that the callback may send packets, but they + %% are queued after current packet. + case Callback(S1, Reply) of + {noreply, S2} -> + Q = tl(S2#s.send_queue), + send_packet(S2#s { send_queue = Q }); + {stop,Reason,Reply,S2} -> + {stop,Reason,Reply,S2}; + {stop,Reason,S2} -> + {stop,Reason,S2} + end; + [] -> + ?dbg("Reply not expected\n",[]), + {noreply, S1} + end. + +decode_opcode(Code) -> + case Code band 16#7f of + ?OBEX_CMD_CONNECT -> connect; + ?OBEX_CMD_DISCONNECT -> disconnect; + ?OBEX_CMD_PUT -> put; + ?OBEX_CMD_GET -> get; + ?OBEX_CMD_COMMAND -> command; + ?OBEX_CMD_SETPATH -> setpath; + ?OBEX_CMD_ABORT -> abort; + + %% 1x + ?OBEX_RSP_CONTINUE -> continue; + ?OBEX_RSP_SWITCH_PRO -> switch_protocols; + + %% 2x + ?OBEX_RSP_SUCCESS -> {ok,success}; + ?OBEX_RSP_CREATED -> {ok,created}; + ?OBEX_RSP_ACCEPTED -> {ok,accepted}; + ?OBEX_RSP_NO_CONTENT -> {ok,no_content}; + ?OBEX_RSP_RESET_CONTENT -> {ok, reset_content}; + ?OBEX_RSP_PARTIAL_CONTENT -> {ok, partial_content}; + + %% 3x + ?OBEX_RSP_MULTIPLE_CHOICES -> {error,multiple_choices}; + ?OBEX_RSP_MOVED_PERMANENTLY -> {error,moved_permanently}; + ?OBEX_RSP_MOVED_TEMPORARILY -> {error,moved_temporarily}; + ?OBEX_RSP_SEE_OTHER -> {error,see_other}; + ?OBEX_RSP_NOT_MODIFIED -> {error,not_modified}; + ?OBEX_RSP_USE_PROXY -> {error,use_proxy}; + + %% 4x + ?OBEX_RSP_BAD_REQUEST -> {error,bad_request}; + ?OBEX_RSP_UNAUTHORIZED -> {error,unauthorized}; + ?OBEX_RSP_PAYMENT_REQUIRED -> {error,payment_required}; + ?OBEX_RSP_FORBIDDEN -> {error, forbidden}; + ?OBEX_RSP_NOT_FOUND -> {error, not_found}; + ?OBEX_RSP_METHOD_NOT_ALLOWED -> {error,not_allowed}; + ?OBEX_RSP_CONFLICT -> {error, conflict}; + ?OBEX_RSP_GONE -> {error, gone}; + ?OBEX_RSP_LENGTH_REQUIRED -> {error, length_required}; + ?OBEX_RSP_PRECONDITION_FAILED -> {error, precondition_failed}; + ?OBEX_RSP_ENTITY_TOO_LARGE -> {error, entity_too_large}; + ?OBEX_RSP_URL_TOO_LARGE -> {error, url_too_large}; + ?OBEX_RSP_UNSUPPORTED_MEDIA -> {error, unsupported_media}; + + %% + ?OBEX_RSP_INTERNAL_SERVER_ERROR -> {error, internal_server_error}; + ?OBEX_RSP_NOT_IMPLEMENTED -> {error, not_implemented}; + ?OBEX_RSP_BAD_GATEWAY -> {error, bad_gateway}; + ?OBEX_RSP_SERVICE_UNAVAILABLE -> {error, service_unavailable}; + ?OBEX_RSP_GATEWAY_TIMEOUT -> {error, gateway_timeout}; + ?OBEX_RSP_HTTP_VERSION_NOT_SUPPORTED -> {error, http_version_not_supported}; + + %% + ?OBEX_RSP_DATABASE_FULL -> {error, database_full}; + ?OBEX_RSP_DATABASE_LOCKED -> {error, database_locked}; + %% + Code1 -> + case (Code1 bsr 4) of + 2 -> {ok,Code1}; + _ -> {error,Code1} + end + end. + + +encode_opcode(Code) -> + case Code of + %% 0x + connect -> ?OBEX_CMD_CONNECT; + disconnect -> ?OBEX_CMD_DISCONNECT; + put -> ?OBEX_CMD_PUT; + get -> ?OBEX_CMD_GET; + command -> ?OBEX_CMD_COMMAND; + setpath -> ?OBEX_CMD_SETPATH; + abort -> ?OBEX_CMD_ABORT; + %% 1x + continue -> ?OBEX_RSP_CONTINUE; + switch_protocols -> ?OBEX_RSP_SWITCH_PRO; + %% 2x + {ok,success} -> ?OBEX_RSP_SUCCESS; + {ok,created} -> ?OBEX_RSP_CREATED; + {ok,accetped} -> ?OBEX_RSP_ACCEPTED; + {ok,no_content} -> ?OBEX_RSP_NO_CONTENT; + %% 3x + {error,multiple_choices} -> ?OBEX_RSP_MULTIPLE_CHOICES; + {error,moved_permanently} -> ?OBEX_RSP_MOVED_PERMANENTLY; + {error,moved_temporarily} -> ?OBEX_RSP_MOVED_TEMPORARILY; + {error,see_other} -> ?OBEX_RSP_SEE_OTHER; + {error,not_modified} -> ?OBEX_RSP_NOT_MODIFIED; + {error,use_proxy} -> ?OBEX_RSP_USE_PROXY; + + %% 4x + {error,bad_request} -> ?OBEX_RSP_BAD_REQUEST; + {error,unauthorized} -> ?OBEX_RSP_UNAUTHORIZED; + {error,payment_required} -> ?OBEX_RSP_PAYMENT_REQUIRED; + {error,forbidden} -> ?OBEX_RSP_FORBIDDEN; + {error,not_found} -> ?OBEX_RSP_NOT_FOUND; + {error,not_allowed} -> ?OBEX_RSP_METHOD_NOT_ALLOWED; + {error,conflict} -> ?OBEX_RSP_CONFLICT; + {error, gone} -> ?OBEX_RSP_GONE; + {error, length_required} -> ?OBEX_RSP_LENGTH_REQUIRED; + {error, precondition_failed} -> ?OBEX_RSP_PRECONDITION_FAILED; + {error, entity_too_large} -> ?OBEX_RSP_ENTITY_TOO_LARGE; + {error, url_too_large} -> ?OBEX_RSP_URL_TOO_LARGE; + {error, unsupported_media} -> ?OBEX_RSP_UNSUPPORTED_MEDIA; + %% 5x + {error,internal_server_error} -> ?OBEX_RSP_INTERNAL_SERVER_ERROR; + {error,not_implemented} -> ?OBEX_RSP_NOT_IMPLEMENTED; + %% 6x + {error,database_full} -> ?OBEX_RSP_DATABASE_FULL; + {error,database_locked} -> ?OBEX_RSP_DATABASE_LOCKED; + %% + {ok,Code} when Code >= 0, Code =< 15 -> Code; + {error,Code} -> Code + end. + +%% push back extra packet data (ABORT?) +%% (should never happend but why should we not try to support pipelining ...) +push_packets(Buf, S) when size(Buf) < 3 -> + S#s { recv_remain = -3 + size(Buf), recv_buffer = Buf }; +push_packets(Buf = <<_Op:8, PacketLen:16,_>>, S) -> + DataLen = size(Buf), + if DataLen >= PacketLen -> + <<Packet:PacketLen/binary, Buf1/binary>> = Buf, + Qs = S#s.recv_queue ++ [Packet], + push_packets(Buf1, S#s { recv_queue = Qs }); + true -> + S#s { recv_remain = PacketLen-DataLen, + recv_buffer = Buf } + end. + +%% +%% Generate a sequence of packets with +%% Headers body and bodyEnd +%% End == false - Do not generated bodyEnd headers +%% Hs == [] - Only generate body or bodyEnd headers +%% Data == <<>> - No data possible a empty dataEnd is generated +%% +make_packets(Hs, Data, End, Mtu) -> + make_headers(Hs,Mtu-3,Mtu-3,End,Data,[],[]). + +make_headers([{Key,Value}|Hs],Remain,Mtu,End,Data,HsAcc,PsAcc) -> + Sz = size_header(Key,Value), + if Sz =< Remain -> + make_headers(Hs,Remain-Sz,Mtu,End,Data, + [{Key,Value}|HsAcc],PsAcc); + Sz > Mtu -> + io:format("WARNING: SIZE(~p,~p)=~w,MTU=~w\n",[Key,Value,Sz,Mtu]), + if HsAcc == [] -> + P = #obex_packet { headers=[{Key,Value}]}, + make_headers(Hs,Mtu,Mtu,End,Data,[],[P|PsAcc]); + true -> + P1 = #obex_packet { headers=reverse(HsAcc) }, + P2 = #obex_packet { headers=[{Key,Value}]}, + make_headers(Hs,Mtu,Mtu,End,Data,[],[P2,P1|PsAcc]) + end; + true -> + P = #obex_packet { headers=reverse(HsAcc) }, + make_headers(Hs,Mtu-Sz,Mtu,End,Data,[{Key,Value}],[P|PsAcc]) + end; +make_headers([],Remain,Mtu,End,Data,HsAcc,PsAcc) -> + make_data(Data,Remain,Mtu,End,HsAcc,PsAcc). + +make_data(Data,Remain,Mtu,End,HsAcc,PsAcc) -> + Sz = size(Data)+3, %% DataSize + Tag & length + if + Sz =< Remain -> + if End == true -> + HsAcc1 = [{bodyEnd,Data}|HsAcc], + P = #obex_packet { headers=reverse(HsAcc1) }, + reverse([P|PsAcc]); + Sz == 3, HsAcc == [] -> %% No data and No End + reverse(PsAcc); + Sz == 3 -> + P = #obex_packet { headers=reverse(HsAcc) }, + reverse([P|PsAcc]); + true -> %% Data but not Last Chunk + HsAcc1 = [{body,Data}|HsAcc], + P = #obex_packet { headers=reverse(HsAcc1) }, + reverse([P|PsAcc]) + end; + Remain < 4 -> + P = #obex_packet { headers=reverse(HsAcc) }, + make_data(Data,Mtu,Mtu,End,[],[P|PsAcc]); + Sz > Remain -> + Len = Remain - 3, + <<Data1:Len/binary, Data2/binary>> = Data, + HsAcc1 = [{body,Data1} | HsAcc], + P = #obex_packet { headers=reverse(HsAcc1) }, + make_data(Data2,Mtu,Mtu,End,[],[P|PsAcc]) + end. + +%% +%% Create a sequence of GET packets covering +%% the sending of the initial headers +%% +make_get_packets(Hs, Mtu) -> + make_get_headers(Hs,Mtu-3,Mtu-3,[],[]). + +make_get_headers([{Key,Value}|Hs],Remain,Mtu,HsAcc,PsAcc) -> + Sz = size_header(Key,Value), + if Sz =< Remain -> + make_get_headers(Hs,Remain-Sz,Mtu, + [{Key,Value}|HsAcc],PsAcc); + Sz > Mtu -> + io:format("WARNING: SIZE(~p,~p)=~w,MTU=~w\n", [Key,Value,Sz,Mtu]), + if HsAcc == [] -> + P = #obex_packet { opcode=get, headers=[{Key,Value}]}, + make_get_headers(Hs,Mtu,Mtu,[],[P|PsAcc]); + true -> + P1 = #obex_packet { opcode=get, headers=reverse(HsAcc) }, + P2 = #obex_packet { opcode=get, headers=[{Key,Value}]}, + make_get_headers(Hs,Mtu,Mtu,[],[P2,P1|PsAcc]) + end; + true -> + P = #obex_packet { opcode=get, headers=reverse(HsAcc) }, + make_get_headers(Hs,Mtu-Sz,Mtu,[{Key,Value}],[P|PsAcc]) + end; +make_get_headers([],_Remain,_Mtu,[],PsAcc) -> + reverse(PsAcc); +make_get_headers([],_Remain,_Mtu,HsAcc,PsAcc) -> + P = #obex_packet { opcode=get,headers=reverse(HsAcc)}, + reverse([P|PsAcc]). + +%% +%% split headers info {Regular, Body} +%% +split_headers(Hs) -> + split_headers(Hs, [], [], false). + +split_headers([{body,Data}|Hs], Regular, Body, End) -> + split_headers(Hs, Regular, [Data|Body], End); +split_headers([{bodyEnd,Data}|Hs], Regular, Body, _End) -> + split_headers(Hs, Regular, [Data|Body], true); +split_headers([H | Hs], Regular, Body, End) -> + split_headers(Hs, [H|Regular], Body, End); +split_headers([], Regular, Body, End) -> + {End, reverse(Regular), list_to_binary(reverse(Body))}. + +%% +%% Decode packet +%% + +%% 0x - commands +decode_packet(Data = <<Final:1,?OBEX_CMD_CONNECT:7,_:16, + Version:8,Flags:8,MaxPacketLength:16,_/binary>>, _Cmd) -> + #obex_packet { final = Final, + opcode = connect, + args = [Version,Flags,MaxPacketLength], + headers = decode_headers(Data, 7) }; +decode_packet(Data = <<Final:1,?OBEX_CMD_SETPATH:7,_:16, + Flags:8,Constants:8,_/binary>>, _Cmd) -> + #obex_packet { final = Final, + opcode = setpath, + args = [Flags,Constants], + headers = decode_headers(Data, 5) }; +%% 2x - ok (connect reply) +decode_packet(Data = <<Final:1,Code:7,_:16, + Version,Flags,MaxPacketLength:16,_/binary>>, connect) -> + #obex_packet { final=Final, + opcode = decode_opcode(Code), + args = [Version,Flags,MaxPacketLength], + headers = decode_headers(Data, 7)}; +decode_packet(Data = <<Final:1,Code:7,_:16,_/binary>>,_Cmd) -> + #obex_packet { final = Final, + opcode = decode_opcode(Code), + headers = decode_headers(Data, 3) }. + + +recv_concat(Data, []) -> Data; +recv_concat(Data, Buffer) -> <<Buffer/binary, Data/binary>>. + +%% send or buffer a packet +send_packet(_P, S) when S#s.ref == undefined -> + ?dbg("send_packet: transport closed\n", []), + {reply, {error,closed}, S}; +send_packet(P, S) when is_record(P, obex_packet) -> + if S#s.send_queue == [] -> + send_packet(S#s { send_queue = [P]}); + true -> + ?dbg("send_packet: queued ~p\n", [P]), + Q = S#s.send_queue ++ [P], + {noreply, S#s { send_queue = Q }} + end. + +%% send the first packet on the queue +send_packet(S) -> + case S#s.send_queue of + [] -> + {noreply, S}; + [P=#obex_packet { opcode = undefined, callback=Callback } | Q] -> + %% sync / close / packet + Callback(S#s { send_queue=Q} , P); + + [P=#obex_packet { final=Final, opcode=command, callback=Callback, + headers=Hs }| Q] -> + ?dbg("send_packet: ~p\n", [P]), + Data = encode_packet(Final, ?OBEX_CMD_COMMAND,<<>>, Hs, + S#s.peer_mtu), + (S#s.transport):send(S#s.ref, Data), + Callback(S#s { send_queue=Q }, P); + + [_P=#obex_packet { final=Final, opcode=connect, + args=[Version,Flags,MaxPacketLength], + headers=Hs }|_Q] -> + ?dbg("send_packet: ~p\n", [_P]), + Data = encode_packet(Final, ?OBEX_CMD_CONNECT, + <<Version:8,Flags:8,MaxPacketLength:16>>, + Hs, 0), + (S#s.transport):send(S#s.ref, Data), + {noreply,S}; + + %% Synthetic opcode!!! + [_P=#obex_packet { final=Final, opcode=connect_response, + args=[Version,Flags,MaxPacketLength], + headers=Hs }|_Q] -> + ?dbg("send_packet: ~p\n", [_P]), + Data = encode_packet(Final, ?OBEX_RSP_SUCCESS, + <<Version:8,Flags:8,MaxPacketLength:16>>, + Hs, 0), + (S#s.transport):send(S#s.ref, Data), + {noreply,S}; + + [_P=#obex_packet { final=Final, opcode=setpath, + args=[Flags,Constants], + headers=Hs } | _Q] -> + ?dbg("send_packet: ~p\n", [_P]), + Data = encode_packet(Final, ?OBEX_CMD_SETPATH, + <<Flags:8, Constants:8>>, + Hs, S#s.peer_mtu), + (S#s.transport):send(S#s.ref, Data), + {noreply,S}; + + + + [_P=#obex_packet { final=Final, opcode=Op, + args=undefined, + headers=Hs } | _Q] -> + ?dbg("send_packet: ~p\n", [_P]), + OpCode = encode_opcode(Op), + Data = encode_packet(Final, OpCode, <<>>, + Hs, S#s.peer_mtu), + (S#s.transport):send(S#s.ref, Data), + {noreply,S} + end. + + + +getopt(Key, Opts) -> + getopt(Key, Opts, undefined). + +getopt(Key, Opts, Default) -> + case lists:keysearch(Key, 1, Opts) of + {value, {_,Value}} -> + Value; + false -> + Default + end. + +decode_headers(Bin) -> + decode_headers(Bin, 0). + +decode_headers(Bin, Offset) -> + ?dbg("decode_headers: offset=~p\n", [Offset]), + decode_headers(Bin, Offset, []). + +decode_headers(Bin, Offset, Hs) when Offset >= size(Bin) -> + reverse(Hs); +decode_headers(Bin, Offset, Hs) -> + ?dbg("decode_headers: offset=~p\n", [Offset]), + case Bin of + <<_:Offset/binary,?OBEX_V0:2, ID:6,HeaderLen:16,_/binary>> -> + Offset1 = Offset+3, + ValueLen = HeaderLen - 3, %% not counting tag and length + <<_:Offset1/binary, Value:ValueLen/binary, _/binary>> = Bin, + Offset2 = Offset1+ValueLen, + Val = decode_unicode(Value, 0, []), + case ((?OBEX_V0 bsl 6) bor ID) of + ?OBEX_HDR_NAME -> + ?dbg("header: name=~p\n", [Val]), + decode_headers(Bin,Offset2, + [{name,Val}|Hs]); + ?OBEX_HDR_DESCRIPTION -> + ?dbg("header: desciption=~p\n", [Val]), + decode_headers(Bin,Offset2, + [{description,Val}|Hs]); + ID1 -> + ?dbg("header: ~w=~p\n", [ID1,Val]), + decode_headers(Bin,Offset2, + [{ID1,Val}|Hs]) + end; + + <<_:Offset/binary,?OBEX_Vn:2,ID:6,HeaderLen:16,_/binary>> -> + Offset1 = Offset+3, + ValueLen = HeaderLen - 3, + <<_:Offset1/binary,Value:ValueLen/binary,_/binary>> = Bin, + Offset2 = Offset1+ValueLen, + case ((?OBEX_Vn bsl 6) bor ID) of + ?OBEX_HDR_TYPE -> + Val = decode_ascii(Value, 0, []), + ?dbg("header: type=~p\n", [Val]), + decode_headers(Bin,Offset2,[{type,Val}|Hs]); + ?OBEX_HDR_TIME -> + Val = binary_to_list(Value), + ?dbg("header: time=~p\n", [Val]), + decode_headers(Bin,Offset2,[{time,Val}|Hs]); + ?OBEX_HDR_TARGET -> + Val = binary_to_list(Value), + ?dbg("header: target=~p\n", [Val]), + decode_headers(Bin,Offset2,[{target,Val}|Hs]); + ?OBEX_HDR_HTTP -> + Val = binary_to_list(Value), + ?dbg("header: http=~p\n", [Val]), + decode_headers(Bin,Offset2,[{http,Val}|Hs]); + ?OBEX_HDR_BODY -> + ?dbg("header: body=~p\n", [Value]), + decode_headers(Bin,Offset2,[{body,Value}|Hs]); + ?OBEX_HDR_BODY_END -> + ?dbg("header: bodyEnd=~p\n", [Value]), + decode_headers(Bin,Offset2,[{bodyEnd,Value}|Hs]); + ?OBEX_HDR_WHO -> + Val = binary_to_list(Value), + ?dbg("header: who=~p\n", [Val]), + decode_headers(Bin,Offset2,[{who,Val}|Hs]); + + ?OBEX_HDR_APPARAM -> + ?dbg("header: appparam=~p\n", [Value]), + decode_headers(Bin,Offset2, + [{appParameters,Value}|Hs]); + + ?OBEX_HDR_AUTHCHAL -> + ?dbg("header: authChal=~p\n", [Value]), + decode_headers(Bin,Offset2, + [{authorizationChallange,Value}|Hs]); + + ?OBEX_HDR_AUTHRESP -> + ?dbg("header: authResp=~p\n", [Value]), + decode_headers(Bin,Offset2, + [{authorizationResponse,Value}|Hs]); + + ?OBEX_HDR_OBJCLASS -> + Val = binary_to_list(Value), + ?dbg("header: objClass=~p\n", [Val]), + decode_headers(Bin,Offset2,[{objectClass,Val}|Hs]); + + ID1 -> + ?dbg("header: ~p=~p\n", [ID1,Value]), + decode_headers(Bin,Offset2, [{ID1,Value} | Hs]) + end; + + <<_:Offset/binary,?OBEX_I4:2,ID:6,Value:32,_/binary>> -> + Offset1 = Offset+1+4, + case ((?OBEX_I4 bsl 6) bor ID) of + ?OBEX_HDR_COUNT -> + ?dbg("header: count=~p\n", [Value]), + decode_headers(Bin,Offset1, + [{count,Value}|Hs]); + ?OBEX_HDR_CONNECTION_ID -> + ?dbg("header: connectionID=~p\n", [Value]), + decode_headers(Bin,Offset1, + [{connectionID,Value}|Hs]); + ?OBEX_HDR_LENGTH -> + ?dbg("header: length=~p\n", [Value]), + decode_headers(Bin,Offset1, + [{length,Value}|Hs]); + ?OBEX_HDR_TIME2 -> + ?dbg("header: time2=~p\n", [Value]), + decode_headers(Bin,Offset1, + [{time2,Value}|Hs]); + ID1 -> + ?dbg("header: ~p=~p\n", [ID1,Value]), + decode_headers(Bin,Offset1, + [{ID1,Value} | Hs]) + end; + + <<_:Offset/binary,?OBEX_I1:2,ID:6,Value:8,_/binary>> -> + Offset1 = Offset+1+1, + case ((?OBEX_I1 bsl 6) bor ID) of + ID1 -> + ?dbg("header: ~p=~p\n", [ID1,Value]), + decode_headers(Bin,Offset1, + [{ID1,Value} | Hs]) + end + end. + + +decode_unicode(Bin, Offset, Acc) when Offset >= size(Bin) -> + ?dbg("Warning: unicode string not null terminated\n", []), + reverse(Acc); +decode_unicode(Bin, Offset, Acc) -> + case Bin of + <<_:Offset/binary, 0, 0>> -> reverse(Acc); + <<_:Offset/binary, Char:16, _/binary>> -> + decode_unicode(Bin, Offset+2, [Char|Acc]) + end. + +decode_ascii(Bin, Offset, Acc) when Offset >= size(Bin) -> + reverse(Acc); +decode_ascii(Bin, Offset, Acc) -> + case Bin of + <<_:Offset/binary, 0>> -> + reverse(Acc); + <<_:Offset/binary, Char:8, _/binary>> -> + decode_ascii(Bin, Offset+1, [Char|Acc]) + end. + +encode_packet(Final, Opcode, BinArgs, Headers, Mtu) -> + BinHs = encode_headers(Headers), + PacketLen = 3+size(BinArgs)+size(BinHs), + if Mtu == 0 -> + ok; + PacketLen > Mtu -> + ?dbg("Waring: packet bigger than peer mtu (~w)\n", [Mtu]); + true -> ok + end, + [<<Final:1, Opcode:7, PacketLen:16>>, BinArgs, BinHs]. + + +encode_headers(Hs) -> + list_to_binary(enc_headers(Hs)). + +enc_headers([{Key,Value}|Hs]) -> + [encode_header(Key,Value) | enc_headers(Hs)]; +enc_headers([]) -> + []. + +encode_header(Key,Value) -> + case Key of + %% null terminated unicode unicode + name -> + encode_unicode(?OBEX_HDR_NAME, Value); + description -> + encode_unicode(?OBEX_HDR_DESCRIPTION,Value); + ID when is_integer(ID), (ID band ?OBEX_HDR_MASK) == ?OBEX_V0 -> + encode_unicode(ID,Value); + %% byte sequence + type -> + ?enc_null_sequence(?OBEX_HDR_TYPE,Value); + time -> + ?enc_byte_sequence(?OBEX_HDR_TIME,Value); + target -> + ?enc_byte_sequence(?OBEX_HDR_TARGET,Value); + http -> + ?enc_byte_sequence(?OBEX_HDR_HTTP,Value); + body -> + ?enc_byte_sequence(?OBEX_HDR_BODY,Value); + bodyEnd -> + ?enc_byte_sequence(?OBEX_HDR_BODY_END,Value); + who -> + ?enc_byte_sequence(?OBEX_HDR_WHO,Value); + appParameters -> + ?enc_byte_sequence(?OBEX_HDR_APPARAM,Value); + authorizationChallange -> + ?enc_byte_sequence(?OBEX_HDR_AUTHCHAL,Value); + authorizationResponse -> + ?enc_byte_sequence(?OBEX_HDR_AUTHRESP,Value); + objectClass -> + ?enc_byte_sequence(?OBEX_HDR_OBJCLASS,Value); + ID when is_integer(ID), (ID band ?OBEX_HDR_MASK) == ?OBEX_Vn -> + ?enc_byte_sequence(ID, Value); + %% 4 byte values + count -> <<?OBEX_HDR_COUNT,Value:32>>; + length -> <<?OBEX_HDR_LENGTH,Value:32>>; + time2 -> <<?OBEX_HDR_TIME2, Value:32>>; + connectionID -> (<<?OBEX_HDR_CONNECTION_ID,Value:32>>); + ID when is_integer(ID), (ID band ?OBEX_HDR_MASK) == ?OBEX_I4 -> + (<<ID, Value:32>>); + ID when is_integer(ID), (ID band ?OBEX_HDR_MASK) == ?OBEX_I1 -> + (<<ID, Value:8>>) + end. + +%% time header should be in UTC time +utc_time_iso(Time) when is_list(Time) -> + Time; +utc_time_iso(Time={Ms,S,Us}) when is_integer(Ms),is_integer(S),is_integer(Us) -> + utc_time_iso(calendar:now_to_universal_time(Time)); +utc_time_iso({{YYYY,Mon,Day},{H,M,S}}) -> + lists:flatten(io_lib:format("~4..0w~2..0w~2..0wT~2..0w~2..0w~2..0wZ", + [YYYY,Mon,Day,H,M,S])). + +%% but here is a local version +time_iso(Time) when is_list(Time) -> + Time; +time_iso(Time={Ms,S,Us}) + when is_integer(Ms),is_integer(S),is_integer(Us) -> + time_iso(calendar:now_to_local_time(Time)); +time_iso({{YYYY,Mon,Day},{H,M,S}}) -> + lists:flatten(io_lib:format("~4..0w~2..0w~2..0wT~2..0w~2..0w~2..0w", + [YYYY,Mon,Day,H,M,S])). +%% +%% Calculate size for a header list +%% +size_headers(Hs) when is_list(Hs) -> + size_headers(Hs, 0). + +size_headers([{Key,Value}|Hs], Sz) -> + size_headers(Hs, size_header(Key,Value)+Sz); +size_headers([], Sz) -> + Sz. + +%% +%% Calculate the size of header +%% Including <tag> <len:16> <data> +%% +size_header(Key,Value) -> + case Key of + %% null terminates + name -> 3+?size_unicode(Value); + description -> 3+?size_unicode(Value); + ID when is_integer(ID), (ID band ?OBEX_HDR_MASK) == ?OBEX_V0 -> + 3+?size_unicode(Value); + %% byte sequence + type -> 4+?size_byte_sequence(Value); %% (+1 for null termination) + time -> 3+?size_byte_sequence(Value); + target -> 3+?size_byte_sequence(Value); + http -> 3+?size_byte_sequence(Value); + body -> 3+?size_byte_sequence(Value); + bodyEnd -> 3+?size_byte_sequence(Value); + who -> 3+?size_byte_sequence(Value); + appParameters -> 3+?size_byte_sequence(Value); + authorizationChallange -> 3+?size_byte_sequence(Value); + authorizationResponse -> 3+?size_byte_sequence(Value); + objectClass -> 3+?size_byte_sequence(Value); + ID when is_integer(ID), (ID band ?OBEX_HDR_MASK) == ?OBEX_Vn -> + 3+?size_byte_sequence(Value); + %% 4 byte values + count -> 5; + length -> 5; + time2 -> 5; + connectionID -> 5; + ID when is_integer(ID), (ID band ?OBEX_HDR_MASK) == ?OBEX_I4 -> 5; + ID when is_integer(ID), (ID band ?OBEX_HDR_MASK) == ?OBEX_I1 -> 2 + end. + +%% Note: length field include it self and tag + unicode null termination 0,0 +encode_unicode(ID, "") -> %% speical (test) + [ID, <<3:16>>]; +encode_unicode(ID, Value) -> + Len = (length(Value)+1)*2 + 3, + [ID, <<Len:16>> | encode_chars(Value)]. + +encode_chars([H|T]) -> + [<<H:16>> | encode_chars(T)]; +encode_chars([]) -> + [<<0:16>>]. |