1%% -*- coding: latin-1 -*-
2%% Copyright (C) 2003 Joakim Greben� <jocke@gleipnir.com>.
3%% All rights reserved.
4%%
5%% Copyright (C) 2006 Gaspar Chilingarov <nm@web.am>
6%%                      Gurgen Tumanyan <barbarian@armkb.com>
7%% All rights reserved.
8%%
9%%
10%% Redistribution and use in source and binary forms, with or without
11%% modification, are permitted provided that the following conditions
12%% are met:
13%%
14%% 1. Redistributions of source code must retain the above copyright
15%%    notice, this list of conditions and the following disclaimer.
16%% 2. Redistributions in binary form must reproduce the above
17%%    copyright notice, this list of conditions and the following
18%%    disclaimer in the documentation and/or other materials provided
19%%    with the distribution.
20%%
21%% THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
22%% OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
23%% WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
24%% ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
25%% DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26%% DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
27%% GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
28%% INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
29%% WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
30%% NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
31%% SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32
33%% NOTE: This module was originally called yaws_jsonrpc.
34%% It was hacked to transparently supports haXe remoting as well,
35%% hence its name was changed to the more generic 'yaws_rpc'.
36%%
37%% modified by Yariv Sadan (yarivvv@gmail.com)
38
39-module(yaws_rpc).
40-author("Gaspar Chilingarov <nm@web.am>, Gurgen Tumanyan <barbarian@armkb.com>").
41-modified_by("Yariv Sadan <yarivvv@gmail.com>").
42-modified_by("Steve Vinoski <vinoski@ieee.org>").
43
44-export([handler/2]).
45-export([handler_session/2, handler_session/3]).
46
47%%-define(debug, 1).
48-include("yaws_debug.hrl").
49-include("../include/yaws_api.hrl").
50
51%%% ######################################################################
52%%% public interface
53%%%
54
55%%%
56%%% use rpc handler which can automagically start sessions if we need
57%%%
58handler_session(Args, Handler) ->
59    handler_session(Args, Handler, 'SID').
60
61%%%
62%%% allow overriding session Cookie name
63%%%
64handler_session(Args, Handler, SID_NAME) when is_atom(SID_NAME) ->
65    handler_session(Args, Handler, atom_to_list(SID_NAME));
66
67handler_session(Args, Handler, SID_NAME) ->
68    handler(Args, Handler, {session, SID_NAME}).    % go to generic handler
69
70%%%
71%%% xmlrpc:handler compatible call
72%%% no session support will be available
73handler(Args, Handler) ->
74    handler(Args, Handler, simple).
75
76
77%%% ######################################################################
78%%% private functions
79%%%
80
81%%% we should be called from yaws page or module
82handler(Args, Handler, Type) when is_record(Args, arg) ->
83    case parse_request(Args) of
84        ok ->
85            handle_payload(Args, Handler, Type);
86        {status, StatusCode} ->        % cannot parse request
87            send(Args, StatusCode)
88    end.
89
90-define(ERROR_LOG(Reason),
91        error_logger:error_report({?MODULE, ?LINE, Reason})).
92
93-define(LOG(Reason), ?ERROR_LOG(Reason)).
94
95%%%
96%%% check that request come in reasonable protocol version and reasonable method
97%%%
98parse_request(Args) ->
99    Req = Args#arg.req,
100    case {Req#http_request.method, Req#http_request.version} of
101        {'POST', {1,0}} ->
102            ?Debug("HTTP Version 1.0~n", []),
103            ok;
104        {'POST', {1,1}} ->
105            ?Debug("HTTP Version 1.1~n", []),
106            ok;
107        {'POST', _HTTPVersion} ->
108            {status, 505};
109        {_Method, {1,1}} ->
110            {status, 501};
111        _ ->
112            {status, 400}
113    end.
114
115handle_payload(Args, Handler, Type) ->
116    RpcType = recognize_rpc_type(Args),
117    %% haXe parameters are URL encoded
118    PL = unicode:characters_to_list(Args#arg.clidata),
119    {Payload,DecodedStr} =
120        case RpcType of
121            T when T==haxe; T==json ->
122                ?Debug("rpc ~p call ~p~n", [T, PL]),
123                {PL, yaws_api:url_decode(PL)};
124            soap_dime ->
125                [{_,_,_,Req}|As] = yaws_dime:decode(Args#arg.clidata),
126                {Args#arg.clidata, {binary_to_list(Req), As}};
127            _ ->
128                ?Debug("rpc plaintext call ~p~n", [PL]),
129                {PL, PL}
130        end,
131    case decode_handler_payload(RpcType, DecodedStr) of
132        Batch when RpcType == json, is_list(Batch) ->
133            BatchRes =
134                lists:foldl(
135                  fun(Req, Acc) ->
136                          Result = check_decoded_payload(Args, Handler,
137                                                         Req, Payload,
138                                                         Type, json),
139                          case Result of
140                              empty ->
141                                  Acc;
142                              {result, _Code, Send} ->
143                                  [Send|Acc];
144                              {send, S} ->
145                                  %% TODO: it would be better if
146                                  %% Result was never of the
147                                  %% {send, ...} variety because
148                                  %% it requires us to take the
149                                  %% content out via searching.
150                                  case lists:keysearch(content,1,S) of
151                                      {value, {content, _, Send}} ->
152                                          [Send|Acc];
153                                      _ ->
154                                          Acc
155                                  end
156                          end
157                  end, [], Batch),
158            case BatchRes of
159                [] ->
160                    %% all notifications, no replies
161                    send(Args, 200, json);
162                _ ->
163                    send(Args, 200,
164                         "["++yaws:join_sep(lists:reverse(BatchRes),",")++"]",
165                         [], json)
166            end;
167        NonBatch ->
168            Result = check_decoded_payload(Args, Handler, NonBatch,
169                                           Payload, Type, RpcType),
170            case Result of
171                {send, Send} ->
172                    Send;
173                empty ->
174                    send(Args, 200, RpcType);
175                {result, Code, Send} ->
176                    send(Args, Code, Send, [], RpcType)
177            end
178    end.
179
180check_decoded_payload(Args, Handler, DecodedResult, Payload, Type, RpcType) ->
181    case DecodedResult of
182        {ok, DecodedPayload, ID} ->
183            ?Debug("client2erl decoded call ~p~n", [DecodedPayload]),
184            eval_payload(Args, Handler, DecodedPayload, Type, ID, RpcType);
185        {error, Reason} ->
186            ?ERROR_LOG({html, client2erl, Payload, Reason}),
187            case RpcType of
188                json ->
189                    case Reason of
190                        {ErrCode, _ErrString} ->
191                            {result, 200, json_error(ErrCode)};
192                        ErrCode ->
193                            {result, 200, json_error(ErrCode)}
194                    end;
195                _ ->
196                    {send, send(Args, 400, RpcType)}
197            end
198    end.
199
200%%% Identify the RPC type. We first try to recognize haXe by the
201%%% "X-Haxe-Remoting" HTTP header, then the "SOAPAction" header,
202%%% and if those are absent we assume the request is JSON.
203recognize_rpc_type(Args) ->
204    case (Args#arg.headers)#headers.content_type of
205        "application/dime" -> soap_dime;
206        _ ->
207            OtherHeaders = ((Args#arg.headers)#headers.other),
208            recognize_rpc_hdr([{X,Y,yaws:to_lower(Z),Q,W} ||
209                                  {X,Y,Z,Q,W} <- OtherHeaders])
210    end.
211
212recognize_rpc_hdr([{_,_,"x-haxe-remoting",_,_}|_]) -> haxe;
213recognize_rpc_hdr([{_,_,"soapaction",_,_}|_])      -> soap;
214recognize_rpc_hdr([_|T])                           -> recognize_rpc_hdr(T);
215recognize_rpc_hdr([])                              -> json.
216
217%%%
218%%% call handler/3 and provide session support
219eval_payload(Args, {M, F}, Payload, {session, CookieName}, ID, RpcType) ->
220    {SessionValue, Cookie} =
221        case yaws_api:find_cookie_val(CookieName,
222                                      (Args#arg.headers)#headers.cookie) of
223            [] ->      %% have no session started, just call handler
224                {undefined, undefined};
225            Cookie2 -> %% get old session data
226                case yaws_api:cookieval_to_opaque(Cookie2) of
227                    {ok, OP} ->
228                        {OP, Cookie2};
229                    {error, _ErrMsg} -> % cannot get corresponding session
230                        {undefined, undefined}
231                end
232        end,
233    CbackFun = callback_fun(M, F, Args, Payload, SessionValue, RpcType),
234    case catch CbackFun() of
235        {'EXIT', {function_clause, _}} when RpcType == json ->
236            case ID of
237                undefined ->
238                    %% empty HTTP reply for notification
239                    empty;
240                _ ->
241                    {result, 200, json_error(-32601, ID)}
242            end;
243        {'EXIT', Reason} ->
244            ?ERROR_LOG({M, F, {'EXIT', Reason}}),
245            {send, send(Args, 500, RpcType)};
246        {error, Reason} ->
247            ?ERROR_LOG({M, F, Reason}),
248            {send, send(Args, 500, RpcType)};
249        {error, Reason, Rc} ->
250            ?ERROR_LOG({M, F, Reason}),
251            {send, send(Args, Rc, Reason, [], RpcType)};
252        {false, ResponsePayload} ->
253            %% do not have updates in session data
254            {send, encode_send(Args, 200, ResponsePayload, [], ID, RpcType)};
255        {false, ResponsePayload, RespCode} ->
256            %% do not have updates in session data
257            {send, encode_send(Args,RespCode,ResponsePayload,[],ID,RpcType)};
258        false ->   % soap or json-rpc notify
259            empty;
260        {true, _NewTimeout, NewSessionValue, ResponsePayload} ->
261            %% be compatible with xmlrpc module
262            CO = handle_cookie(Cookie, CookieName, SessionValue,
263                               NewSessionValue, M, F),
264            {send, encode_send(Args, 200, ResponsePayload, CO, ID, RpcType)};
265        {true, _NewTimeout, NewSessionValue, ResponsePayload, RespCode} ->
266            %% be compatible with xmlrpc module
267            CO = handle_cookie(Cookie, CookieName, SessionValue,
268                               NewSessionValue, M, F),
269            {send, encode_send(Args, RespCode,
270                               ResponsePayload, CO, ID, RpcType)}
271    end;
272
273%%%
274%%% call handler/2 without session support
275%%%
276eval_payload(Args, {M, F}, Payload, simple, ID, RpcType) ->
277    case catch M:F(Args#arg.state, Payload) of
278        {'EXIT', Reason} ->
279            ?ERROR_LOG({M, F, {'EXIT', Reason}}),
280            {send, send(Args, 500)};
281        {error, Reason} ->
282            ?ERROR_LOG({M, F, Reason}),
283            {send, send(Args, 500)};
284        {false, ResponsePayload} ->
285            {send, encode_send(Args, 200, ResponsePayload, [], ID, RpcType)};
286        false -> % Soap notify
287            {send, send(Args, 200, RpcType)};
288        {true, _NewTimeout, _NewState, ResponsePayload} ->
289            {send, encode_send(Args, 200, ResponsePayload, [], ID, RpcType)}
290    end.
291
292handle_cookie(Cookie, CookieName, SessionValue, NewSessionValue, M, F) ->
293    case NewSessionValue of
294        undefined when Cookie == undefined -> []; % nothing to do
295        undefined -> % rpc handler requested session delete
296            yaws_api:delete_cookie_session(Cookie), [];
297        %% XXX: may be return set-cookie with empty val?
298        _ ->
299            %% any other value will stored in session
300            case SessionValue of
301                undefined ->
302                    %% got session data and should start new session now
303                    Cookie1 = yaws_api:new_cookie_session(NewSessionValue),
304                    case get_expire(M, F) of
305                        false ->
306                            yaws_api:setcookie(CookieName, Cookie1, "/");
307                        %% return set_cookie header
308                        Expire ->
309                            yaws_api:setcookie(CookieName, Cookie1, "/",Expire)
310                            %% return set_cookie header
311                    end;
312                _ ->
313                    yaws_api:replace_cookie_session(Cookie, NewSessionValue),
314                    [] % nothing to add to yaws data
315            end
316    end.
317
318%%% Make it possible for callback module to set Cookie Expire string!
319get_expire(M, F) ->
320    case catch M:F(cookie_expire) of
321        Expire when is_list(Expire) -> Expire;
322        _                        -> false
323    end.
324
325callback_fun(M, F, Args, Payload, SessionValue, RpcType)
326  when RpcType =:= soap; RpcType =:= soap_dime ->
327    fun() -> yaws_soap_srv:handler(Args, {M,F}, Payload, SessionValue) end;
328callback_fun(M, F, Args, Payload, SessionValue, _RpcType) ->
329    fun() -> M:F(Args#arg.state, Payload, SessionValue) end.
330
331%%% XXX compatibility with XMLRPC handlers
332%%% XXX - potential bug here?
333encode_send(Args, StatusCode, [Payload], AddOn, ID, RpcType) ->
334    encode_send(Args, StatusCode, Payload, AddOn, ID, RpcType);
335
336encode_send(Args, StatusCode, Payload, AddOn, ID, RpcType) ->
337    ?Debug("rpc response ~p ~n", [Payload]),
338    case encode_handler_payload(Payload, ID, RpcType) of
339        {ok, EncodedPayload, NewRpcType} ->
340            ?Debug("rpc encoded response ~p ~n", [EncodedPayload]),
341            send(Args, StatusCode, EncodedPayload, AddOn, NewRpcType);
342        {ok, EncodedPayload} ->
343            ?Debug("rpc encoded response ~p ~n", [EncodedPayload]),
344            send(Args, StatusCode, EncodedPayload, AddOn, RpcType)
345    end.
346
347send(Args, StatusCode) ->
348    send(Args, StatusCode, json).
349
350send(Args, StatusCode, RpcType) ->
351    send(Args, StatusCode, "", [], RpcType).
352
353send(Args, StatusCode, Payload, AddOn, RpcType) when not is_list(AddOn) ->
354    send(Args, StatusCode, Payload, [AddOn], RpcType);
355send(Args, StatusCode, Payload, AddOnData, RpcType) ->
356    [{status, StatusCode},
357     content_hdr(RpcType, Args, Payload),
358     {header, {content_length, lists:flatlength(Payload)}}] ++ AddOnData.
359
360content_hdr(json, _Args, Payload) -> {content, "application/json", Payload};
361content_hdr(soap, Args, Payload) ->
362    CallerContentType = (Args#arg.headers)#headers.content_type,
363    %% drop caller charset info if present, may not
364    %% be appropriate for the response
365    ContentType = hd(string:tokens(CallerContentType, ";")),
366    {content, ContentType, Payload};
367content_hdr(_, _Args, Payload) -> {content, "text/xml", Payload}.
368
369encode_handler_payload({Xml,[]}, _ID, soap_dime) ->
370    {ok, Xml, soap};
371encode_handler_payload({Xml,As}, _ID, soap_dime) ->
372    EncodedPayload = yaws_dime:encode(Xml, As),
373    {ok, EncodedPayload};
374encode_handler_payload(Xml, _ID, soap_dime) ->
375    {ok, Xml, soap};
376encode_handler_payload({Xml,[]}, _ID, soap) ->
377    {ok, Xml};
378encode_handler_payload({Xml,As}, _ID, soap) ->
379    EncodedPayload = yaws_dime:encode(Xml, As),
380    {ok, EncodedPayload, soap_dime};
381encode_handler_payload(Xml, _ID, soap) ->
382    {ok, Xml};
383encode_handler_payload({error, [ErlStruct]}, ID, RpcType) ->
384    encode_handler_payload({error, ErlStruct}, ID, RpcType);
385encode_handler_payload({error, ErlStruct}, ID, RpcType) ->
386    StructStr =
387        case RpcType of
388            json -> json2:encode({struct, [{id, ID}, {error, ErlStruct},
389                                           {"jsonrpc", "2.0"}]});
390            haxe -> [$h, $x, $r | haxe:encode({exception, ErlStruct})]
391        end,
392    {ok, StructStr};
393encode_handler_payload({response, [ErlStruct]}, ID, RpcType) ->
394    encode_handler_payload({response, ErlStruct}, ID, RpcType);
395encode_handler_payload({response, ErlStruct}, ID, RpcType) ->
396    StructStr =
397        case RpcType of
398            json -> json2:encode({struct, [{result, ErlStruct}, {id, ID},
399                                           {"jsonrpc", "2.0"}]});
400            haxe -> [$h, $x, $r | haxe:encode(ErlStruct)]
401        end,
402    {ok, StructStr}.
403
404decode_handler_payload(json, JSonStr) ->
405    try
406        {ok, Obj} = json2:decode_string(JSonStr),
407        decode_handler_payload_json(Obj)
408    catch
409        error:Err ->
410            ?ERROR_LOG({json_decode, JSonStr, Err}),
411            {error, {-32700, Err}}
412    end;
413decode_handler_payload(haxe, [$_, $_, $x, $= | HaxeStr]) ->
414    try
415        {done, {ok, {array, [MethodName | _]}}, Cont} = haxe:decode(HaxeStr),
416        {done, {ok, Args}, _Cont2} = haxe:decode_next(Cont),
417
418        %% ID is undefined because haXe remoting doesn't automagically handle
419        %% sessions.
420        {ok, {call, list_to_atom(MethodName), Args}, undefined}
421    catch
422        error:Err -> {error, Err}
423    end;
424decode_handler_payload(haxe, _HaxeStr) ->
425    {error, missing_haxe_prefix};
426
427decode_handler_payload(soap_dime, Payload) ->
428    {ok, Payload, undefined};
429decode_handler_payload(soap, Payload) ->
430    {ok, Payload, undefined}.
431
432decode_handler_payload_json({struct, _}=Obj) ->
433    case jsonrpc:s(Obj, method) of
434        undefined ->
435            {error, -32600};
436        Method0 when is_list(Method0) ->
437            Method = case jsonrpc:s(Obj, jsonrpc) of
438                         "2.0" ->
439                             try
440                                 list_to_existing_atom(Method0)
441                             catch
442                                 error:badarg ->
443                                     Method0
444                             end;
445                         undefined ->
446                             list_to_atom(Method0)
447                     end,
448            Args = jsonrpc:s(Obj, params),
449            ArgsOk = case Args of
450                         {struct, _} -> true;
451                         {array, _} -> true;
452                         undefined -> true;
453                         _ -> false
454                     end,
455            case ArgsOk of
456                true ->
457                    ID = jsonrpc:s(Obj, id),
458                    CallOrNotify = case ID of
459                                       undefined ->
460                                           notification;
461                                       _ ->
462                                           call
463                                   end,
464                    {ok, {CallOrNotify, Method, Args}, ID};
465                false ->
466                    {error, -32602}
467            end;
468        _ ->
469            {error, -32600}
470    end;
471decode_handler_payload_json({array, []}) ->
472    {error, -32600};
473decode_handler_payload_json({array, Batch}) ->
474    [decode_handler_payload_json(Obj) || Obj <- Batch];
475decode_handler_payload_json(_) ->
476    {error, -32600}.
477
478json_error(ErrCode) ->
479    json_error(ErrCode, null).
480json_error(ErrCode, Id) ->
481    Err = {struct, [{"jsonrpc", "2.0"},
482                    {"id", Id},
483                    {"error", {struct,
484                               [{"code", ErrCode},
485                                {"message", json_error_message(ErrCode)}]}}]},
486    json2:encode(Err).
487
488json_error_message(-32700) -> "parse error";
489json_error_message(-32600) -> "invalid request";
490json_error_message(-32601) -> "method not found";
491json_error_message(-32602) -> "invalid params";
492json_error_message(-32603) -> "internal error";
493json_error_message(Code) when Code >= -32099, Code =< -32000 -> "server error";
494json_error_message(_) -> "json error".
495