diff --git a/src/mod_proxy65/Makefile.in b/src/mod_proxy65/Makefile.in index bb70ac5cb..0cd3e0639 100644 --- a/src/mod_proxy65/Makefile.in +++ b/src/mod_proxy65/Makefile.in @@ -1,12 +1,12 @@ # $Id$ -CC = @CC@ +CC = @CC@ CFLAGS = @CFLAGS@ @ERLANG_CFLAGS@ CPPFLAGS = @CPPFLAGS@ LDFLAGS = @LDFLAGS@ LIBS = @LIBS@ @ERLANG_LIBS@ -SUBDIRS = +SUBDIRS = OUTDIR = .. EFLAGS = -I .. -pz .. @@ -20,6 +20,7 @@ OBJS = \ $(OUTDIR)/mod_proxy65_service.beam \ $(OUTDIR)/mod_proxy65_sm.beam \ $(OUTDIR)/mod_proxy65_stream.beam \ + $(OUTDIR)/mod_proxy65_http.beam \ $(OUTDIR)/mod_proxy65_lib.beam all: $(OBJS) @@ -35,4 +36,3 @@ distclean: clean TAGS: etags *.erl - diff --git a/src/mod_proxy65/TODO b/src/mod_proxy65/TODO new file mode 100644 index 000000000..7414ff8ca --- /dev/null +++ b/src/mod_proxy65/TODO @@ -0,0 +1,17 @@ + + +HTTP transfer +------------- + +* stream POST to GET in real time, whithout storing the file +* handle acl +* get base URL from ejabberd_http config +* add parameters: + - store directory + - URL of upload +* virtual host handling +* check security stuffs: + - limit file size at upload time + - acl +* transfer socks5 -> HTTP +* transfer HTTP -> socks5 \ No newline at end of file diff --git a/src/mod_proxy65/mod_proxy65.erl b/src/mod_proxy65/mod_proxy65.erl index 65719241e..dfa5b89cb 100644 --- a/src/mod_proxy65/mod_proxy65.erl +++ b/src/mod_proxy65/mod_proxy65.erl @@ -50,8 +50,14 @@ init([Host, Opts]) -> [gen_mod:get_module_proc(Host, ejabberd_mod_proxy65_sup), mod_proxy65_stream]}, transient, infinity, supervisor, [ejabberd_tmp_sup]}, + HttpStreamSupervisor = + {ejabberd_mod_proxy65_http_sup, + {ejabberd_tmp_sup, start_link, + [gen_mod:get_module_proc(Host, ejabberd_mod_proxy65_http_sup), + mod_proxy65_http]}, + transient, infinity, supervisor, [ejabberd_tmp_sup]}, StreamManager = {mod_proxy65_sm, {mod_proxy65_sm, start_link, [Host, Opts]}, transient, 5000, worker, [mod_proxy65_sm]}, {ok, {{one_for_one, 10, 1}, - [StreamManager, StreamSupervisor, Service]}}. + [StreamManager, StreamSupervisor, HttpStreamSupervisor, Service]}}. diff --git a/src/mod_proxy65/mod_proxy65.hrl b/src/mod_proxy65/mod_proxy65.hrl index eeab6804d..a26c04c4a 100644 --- a/src/mod_proxy65/mod_proxy65.hrl +++ b/src/mod_proxy65/mod_proxy65.hrl @@ -27,20 +27,37 @@ %% RFC 1928 replies -define(SUCCESS, 0). --define(ERR_GENERAL_FAILURE, 1). --define(ERR_NOT_ALLOWED, 2). --define(ERR_NETWORK_UNREACHABLE, 3). --define(ERR_HOST_UNREACHABLE, 4). --define(ERR_CONNECTION_REFUSED, 5). --define(ERR_TTL_EXPIRED, 6). --define(ERR_COMMAND_NOT_SUPPORTED, 7). --define(ERR_ADDRESS_TYPE_NOT_SUPPORTED, 8). +-define(SOCKS5_ERR_GENERAL_FAILURE, 1). +-define(SOCKS5_ERR_NOT_ALLOWED, 2). +-define(SOCKS5_ERR_NETWORK_UNREACHABLE, 3). +-define(SOCKS5_ERR_HOST_UNREACHABLE, 4). +-define(SOCKS5_ERR_CONNECTION_REFUSED, 5). +-define(SOCKS5_ERR_TTL_EXPIRED, 6). +-define(SOCKS5_ERR_COMMAND_NOT_SUPPORTED, 7). +-define(SOCKS5_ERR_ADDRESS_TYPE_NOT_SUPPORTED, 8). %% RFC 1928 defined timeout. -define(SOCKS5_REPLY_TIMEOUT, 10000). +-record(bytestream, { + sha1, %% SHA1 key + target, %% Target Pid + initiator, %% Initiator Pid + active = false, %% Activity flag + jid_i, %% Initiator's JID + jid_t, %% Target's JID (for http file transfert) + file, %% store status of file (for http file transfert) + myhost %% proxy's jid + }). + -record(s5_request, { rsv = 0, cmd, sha1 }). + +% For http transfer +-define(NS_HTTP_BYTESTREAMS, "http://oneteam.im/bs-proxy"). +-define(DEFAULT_HTTP_BASE_PATH, "/proxy"). +-define(DEFAULT_HTTP_UPLOAD_PATH, "/upload"). +-define(DEFAULT_STORE_PATH, "/tmp"). diff --git a/src/mod_proxy65/mod_proxy65_http.erl b/src/mod_proxy65/mod_proxy65_http.erl new file mode 100644 index 000000000..f3d6abbea --- /dev/null +++ b/src/mod_proxy65/mod_proxy65_http.erl @@ -0,0 +1,156 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_proxy65_http +%%% Author : Jérôme Sautret +%%% Purpose : HTTP bytestreams proxy for oneteam file transfert +%%% Created : 28 may 2007 +%%% Id : $Id: mod_last.erl 370 2005-06-20 03:18:13Z alexey $ +%%%---------------------------------------------------------------------- + +-module(mod_proxy65_http). +-author('jerome.sautret@process-one.net'). + +-behaviour(gen_mod). + +%% gen_mod callbacks. +-export([ + start/2, + stop/1 + ]). +%% mod_proxy65 api +-export([ + activate/2 + ]). +%% ejabberd_http callback. +-export([ + process/2 + ]). + +-include("mod_proxy65.hrl"). +-include("../ejabberd.hrl"). +-include("../jlib.hrl"). +-include("../web/ejabberd_http.hrl"). + +%%%---------------------------------------------------------------------- +%%% gen_mod Callbacks +%%%---------------------------------------------------------------------- + +start(_Host, _Opts) -> + ok. + +stop(_Host) -> + ok. + +activate({_IPid, _IJid}, {_TPid, _TJid}) -> + ok. + +%%%---------------------------------------------------------------------- +%%% ejabberd_http Callbacks +%%%---------------------------------------------------------------------- + +% Receive File +% XXX TODO: limit file size +process(["upload"], #request{method='POST', + content_type = "multipart/form-data;"++_Boundary = ContentType, + data=Data} = _Request) -> + io:format("POST~n", []), + DataParts = ejabberd_http:parse_data(Data, ContentType), + {SHA1, {Filename, FileContentType, FileContent}} = parse_upload_data(DataParts), + case mnesia:dirty_read(bytestream, SHA1) of + [#bytestream{jid_t = TargetJID, file = BaseURL, myhost = MyHost}] when is_list(BaseURL) -> + Path = store_file(Filename, FileContent), + F = fun() -> + mnesia:write(#bytestream{sha1 = SHA1, + file = {path, Path}, + target = FileContentType}) + end, + mnesia:transaction(F), + URL = BaseURL ++ "/" ++ filename:join(Path), + send_activated(TargetJID, MyHost, SHA1, URL), + Result = "ok", + ejabberd_web:make_xhtml([{xmlcdata, Result}]); + _Other -> + ?ERROR_MSG("Upload ~p not activated~n", [SHA1]), + ejabberd_web:error(bad_request) + end; + +process([_UID, _Filename] = Path, #request{method='GET'} = _Request) -> + io:format("GET~n~p~n", [Path]), + case mnesia:dirty_index_read(bytestream, {path, Path}, #bytestream.file) of + [#bytestream{sha1=SHA1, target=ContentType}|_Tail] -> + mnesia:dirty_delete({bytestream, SHA1}), + serve_file(Path, ContentType); + _ -> + ?ERROR_MSG("Bad request, GET ~p~n", [Path]), + ejabberd_web:error(bad_request) + end. + + +% store the data transfered in a file and return the URL of the file +store_file(Filename, FileContent) -> + HASH = sha:sha(FileContent), + % TODO store dir from a parameter + Path = ?DEFAULT_STORE_PATH ++ "/" ++ HASH ++ "/", + io:format("Path ~p~n", [Path]), + ok = filelib:ensure_dir(Path), + FilePath = Path ++ "/" ++ Filename, + %io:format("FilePath ~p~n~p", [FilePath, FileContent]), + ok = file:write_file(FilePath, list_to_binary(FileContent)), + [HASH, Filename]. + +% send the connexion to the target. +send_activated(TargetJID, MyJID, SHA1, URL) -> + IQ = #iq{type=set, + sub_el=[{xmlelement, "activated", + [{"xmlns", ?NS_HTTP_BYTESTREAMS}, {"sidhash", SHA1}, {"url", URL}], []}]}, + ejabberd_router:route(jlib:string_to_jid(MyJID), TargetJID, jlib:iq_to_xml(IQ)). + + +parse_upload_data(Data) -> + parse_upload_data(Data, {undefined, undefined, undefined}). +parse_upload_data([], Result) -> + Result; +parse_upload_data([#http_data{content_disposition="form-data", + content_type=ContentType, args=Args, data=Data} | Tail], + Result) -> + Result2 = case lists:keysearch("name", 1, Args) of + {value, {"name", "SIDHASH"}} -> + {remove_cr(Data), element(2, Result)}; + {value, {"name", "FILE"}} -> + case lists:keysearch("filename", 1, Args) of + {value, {"filename", Filename}} -> + {element(1, Result), {Filename, ContentType, remove_cr(Data)}} + end; + _ -> + Result + end, + parse_upload_data(Tail, Result2). + + +% remove last trailling carriage return +remove_cr(String) -> + lists:reverse(remove_leading_cr(lists:reverse(String))). +% remove fisrt leading inversed carriage return +remove_leading_cr([$\n, $\r|String]) -> + String; +remove_leading_cr(String) -> + String. + + +serve_file(Path, ContentType) -> + FileName = filename:join([?DEFAULT_STORE_PATH | Path]), + case file:read_file(FileName) of + {ok, FileContents} -> + ?DEBUG("Delivering content.", []), + {200, + [{"Server", "ejabberd"}, + {"Content-type", ContentType}, + {"Content-disposition", "attachment; filename="++filename:basename(FileName)}], + FileContents}; + {error, Error} -> + ?DEBUG("Delivering error: ~p", [Error]), + case Error of + eacces -> {403, [], "Forbidden"}; + enoent -> {404, [], "Not found"}; + _Else -> {500, [], atom_to_list(Error)} + end + end. diff --git a/src/mod_proxy65/mod_proxy65_lib.erl b/src/mod_proxy65/mod_proxy65_lib.erl index 09ee6b981..02d4ecbe8 100644 --- a/src/mod_proxy65/mod_proxy65_lib.erl +++ b/src/mod_proxy65/mod_proxy65_lib.erl @@ -53,7 +53,7 @@ make_init_reply(Method) -> [?VERSION_5, Method]. make_auth_reply(true) -> [1, ?SUCCESS]; -make_auth_reply(false) -> [1, ?ERR_NOT_ALLOWED]. +make_auth_reply(false) -> [1, ?SOCKS5_ERR_NOT_ALLOWED]. %% WARNING: According to SOCKS5 RFC, this reply is _incorrect_, but %% Psi writes junk to the beginning of the file on correct reply. @@ -63,7 +63,7 @@ make_reply() -> [?VERSION_5, ?SUCCESS, 0, 0, 0, 0]. make_error_reply(Request) -> - make_error_reply(Request, ?ERR_NOT_ALLOWED). + make_error_reply(Request, ?SOCKS5_ERR_NOT_ALLOWED). make_error_reply(#s5_request{rsv = RSV, sha1 = SHA1}, Reason) -> [?VERSION_5, Reason, RSV, ?ATYP_DOMAINNAME, length(SHA1), SHA1, 0,0]. diff --git a/src/mod_proxy65/mod_proxy65_service.erl b/src/mod_proxy65/mod_proxy65_service.erl index f9f3bf12d..c590f22f7 100644 --- a/src/mod_proxy65/mod_proxy65_service.erl +++ b/src/mod_proxy65/mod_proxy65_service.erl @@ -23,6 +23,7 @@ %% API. -export([start_link/2]). +-include("mod_proxy65.hrl"). -include("../ejabberd.hrl"). -include("../jlib.hrl"). @@ -34,6 +35,8 @@ name, stream_addr, port, + http_port, + http_base_path, acl }). @@ -84,12 +87,14 @@ handle_info(_Info, State) -> %%%------------------------ %% disco#info request -process_iq(_, #iq{type = get, xmlns = ?NS_DISCO_INFO, lang = Lang} = IQ, #state{name=Name}) -> +process_iq(_, #iq{type = get, xmlns = ?NS_DISCO_INFO} = IQ, #state{name=Name, http_port=HTTP_Port}) -> + io:format("~p~n", [IQ]), IQ#iq{type = result, sub_el = - [{xmlelement, "query", [{"xmlns", ?NS_DISCO_INFO}], iq_disco_info(Lang, Name)}]}; + [{xmlelement, "query", [{"xmlns", ?NS_DISCO_INFO}], iq_disco_info(Name, HTTP_Port)}]}; %% disco#items request process_iq(_, #iq{type = get, xmlns = ?NS_DISCO_ITEMS} = IQ, _) -> + io:format("~p~n", [IQ]), IQ#iq{type = result, sub_el = [{xmlelement, "query", [{"xmlns", ?NS_DISCO_ITEMS}], []}]}; @@ -101,6 +106,7 @@ process_iq(_, #iq{type = get, xmlns = ?NS_VCARD, lang = Lang} = IQ, _) -> %% bytestreams info request process_iq(JID, #iq{type = get, sub_el = SubEl, xmlns = ?NS_BYTESTREAMS} = IQ, #state{acl = ACL, stream_addr = StreamAddr, serverhost = ServerHost}) -> + io:format("~p~n", [IQ]), case acl:match_rule(ServerHost, ACL, JID) of allow -> StreamHostEl = [{xmlelement, "streamhost", StreamAddr, []}], @@ -110,9 +116,39 @@ process_iq(JID, #iq{type = get, sub_el = SubEl, xmlns = ?NS_BYTESTREAMS} = IQ, IQ#iq{type = error, sub_el = [SubEl, ?ERR_FORBIDDEN]} end; + +%% bytestream target fake connection (for later http connection) +process_iq(TargetJID, #iq{type = set, sub_el = SubEl, xmlns = ?NS_HTTP_BYTESTREAMS} = IQ, + #state{acl = _ACL, myhost = MyHost, + http_port=HTTP_Port, http_base_path=HTTP_Base_Path}) -> + % XXX TODO: acl + SID = xml:get_tag_attr_s("sid", SubEl), + case catch jlib:string_to_jid(xml:get_tag_attr_s("jid", SubEl)) of + InitiatorJID when is_record(InitiatorJID, jid), SID /= "", + length(SID) =< 128, TargetJID /= InitiatorJID -> + Target = jlib:jid_to_string(jlib:jid_tolower(TargetJID)), + Initiator = jlib:jid_to_string(jlib:jid_tolower(InitiatorJID)), + SHA1 = sha:sha(SID ++ Initiator ++ Target), + URL = "http://" ++ MyHost ++ ":"++HTTP_Port++ HTTP_Base_Path, + case catch mod_proxy65_sm:register_stream(SHA1, TargetJID, URL, MyHost, self()) of + {atomic, ok} -> + IQ#iq{type = result, sub_el = + [{xmlelement, "connected", + [{"xmlns", ?NS_HTTP_BYTESTREAMS}, {"jid", MyHost}], []}]}; + _Reason -> + ?ERROR_MSG("process IQ set ~p:~n~p~n", [?NS_HTTP_BYTESTREAMS, _Reason]), + IQ#iq{type = error, sub_el = [SubEl, ?ERR_INTERNAL_SERVER_ERROR]} + end; + _Reason -> + ?ERROR_MSG("process IQ set ~p:~n~p~n", [?NS_HTTP_BYTESTREAMS, _Reason]), + IQ#iq{type = error, sub_el = [SubEl, ?ERR_BAD_REQUEST]} + end; + + %% bytestream activation request process_iq(InitiatorJID, #iq{type = set, sub_el = SubEl, xmlns = ?NS_BYTESTREAMS} = IQ, - #state{acl = ACL, serverhost = ServerHost}) -> + #state{acl = ACL, serverhost = ServerHost, myhost = MyHost, + http_port=HTTP_Port, http_base_path=HTTP_Base_Path}) -> case acl:match_rule(ServerHost, ACL, InitiatorJID) of allow -> ActivateEl = xml:get_path_s(SubEl, [{elem, "activate"}]), @@ -123,9 +159,21 @@ process_iq(InitiatorJID, #iq{type = set, sub_el = SubEl, xmlns = ?NS_BYTESTREAMS Target = jlib:jid_to_string(jlib:jid_tolower(TargetJID)), Initiator = jlib:jid_to_string(jlib:jid_tolower(InitiatorJID)), SHA1 = sha:sha(SID ++ Initiator ++ Target), - case mod_proxy65_sm:activate_stream(SHA1, InitiatorJID, TargetJID, ServerHost) of + {Module, Activated} = + case xml:get_path_s(SubEl, [{elem, "x"}]) of + {xmlelement, "x", [{"xmlns", ?NS_HTTP_BYTESTREAMS}], _} -> + {mod_proxy65_http, + [{xmlelement, "activated", + [{"xmlns", ?NS_HTTP_BYTESTREAMS}, + {"url", "http://" ++ MyHost ++ ":"++HTTP_Port++ + HTTP_Base_Path ++ ?DEFAULT_HTTP_UPLOAD_PATH}], []}]}; + _ -> + {mod_proxy65_sm, []} + end, + case mod_proxy65_sm:activate_stream(SHA1, InitiatorJID, TargetJID, + ServerHost, Module) of ok -> - IQ#iq{type = result, sub_el = []}; + IQ#iq{type = result, sub_el = Activated}; false -> IQ#iq{type = error, sub_el = [SubEl, ?ERR_ITEM_NOT_FOUND]}; limit -> @@ -135,7 +183,8 @@ process_iq(InitiatorJID, #iq{type = set, sub_el = SubEl, xmlns = ?NS_BYTESTREAMS _ -> IQ#iq{type = error, sub_el = [SubEl, ?ERR_INTERNAL_SERVER_ERROR]} end; - _ -> + _Reason -> + ?ERROR_MSG("process IQ set ~p:~n~p~n", [?NS_BYTESTREAMS, _Reason]), IQ#iq{type = error, sub_el = [SubEl, ?ERR_BAD_REQUEST]} end; deny -> @@ -155,15 +204,21 @@ process_iq(_, _, _) -> %%%------------------------- -define(FEATURE(Feat), {xmlelement,"feature",[{"var", Feat}],[]}). -iq_disco_info(Lang, Name) -> +iq_disco_info(Name, HTTP_Port) -> + HTTP_Bytestreams = case HTTP_Port of + "0" -> + []; + _ -> + [?FEATURE(?NS_HTTP_BYTESTREAMS)] + end, [{xmlelement, "identity", [{"category", "proxy"}, {"type", "bytestreams"}, - {"name", translate:translate(Lang, Name)}], []}, + {"name", Name}], []}, ?FEATURE(?NS_DISCO_INFO), ?FEATURE(?NS_DISCO_ITEMS), ?FEATURE(?NS_VCARD), - ?FEATURE(?NS_BYTESTREAMS)]. + ?FEATURE(?NS_BYTESTREAMS)] ++ HTTP_Bytestreams. iq_vcard(Lang) -> [{xmlelement, "FN", [], @@ -177,6 +232,8 @@ iq_vcard(Lang) -> parse_options(ServerHost, Opts) -> MyHost = gen_mod:get_opt(host, Opts, "proxy." ++ ServerHost), Port = gen_mod:get_opt(port, Opts, 7777), + HTTP_Port = integer_to_list(gen_mod:get_opt(http_port, Opts, 0)), + HTTP_Base_Path = gen_mod:get_opt(http_base_path, Opts, ?DEFAULT_HTTP_BASE_PATH), ACL = gen_mod:get_opt(access, Opts, all), Name = gen_mod:get_opt(name, Opts, "SOCKS5 Bytestreams"), IP = case gen_mod:get_opt(ip, Opts, none) of @@ -189,7 +246,9 @@ parse_options(ServerHost, Opts) -> serverhost = ServerHost, name = Name, port = Port, - stream_addr = StreamAddr, + http_port = HTTP_Port, + http_base_path = HTTP_Base_Path, + stream_addr = StreamAddr, acl = ACL}}. %% Return the IP of the proxy host, or if not found, the ip of the xmpp domain @@ -201,4 +260,4 @@ get_proxy_or_domainip(ServerHost, MyHost) -> {ok, Addr} -> Addr; {error, _} -> {127,0,0,1} end - end. \ No newline at end of file + end. diff --git a/src/mod_proxy65/mod_proxy65_sm.erl b/src/mod_proxy65/mod_proxy65_sm.erl index 7ddfd2c89..32e89fac4 100644 --- a/src/mod_proxy65/mod_proxy65_sm.erl +++ b/src/mod_proxy65/mod_proxy65_sm.erl @@ -24,18 +24,14 @@ -export([ start_link/2, register_stream/1, + register_stream/5, unregister_stream/1, - activate_stream/4 + activate_stream/5 ]). +-include("mod_proxy65.hrl"). + -record(state, {max_connections}). --record(bytestream, { - sha1, %% SHA1 key - target, %% Target Pid - initiator, %% Initiator Pid - active = false, %% Activity flag - jid_i %% Initiator's JID - }). -define(PROCNAME, ejabberd_mod_proxy65_sm). @@ -55,6 +51,7 @@ start_link(Host, Opts) -> init([Opts]) -> mnesia:create_table(bytestream, [{ram_copies, [node()]}, {attributes, record_info(fields, bytestream)}]), + mnesia:add_table_index(bytestream, file), mnesia:add_table_copy(bytestream, node(), ram_copies), MaxConnections = gen_mod:get_opt(max_connections, Opts, infinity), {ok, #state{max_connections=MaxConnections}}. @@ -67,7 +64,7 @@ handle_call({activate, SHA1, IJid}, _From, State) -> F = fun() -> case mnesia:read(bytestream, SHA1, write) of [#bytestream{target = TPid, initiator = IPid} = ByteStream] - when is_pid(TPid), is_pid(IPid) -> + when is_pid(TPid), is_pid(IPid) -> ActiveFlag = ByteStream#bytestream.active, if ActiveFlag == false -> @@ -109,13 +106,20 @@ handle_call(_Request, _From, State) -> %%% transaction abort %%% SHA1 = string() %%%--------------------------------------------------- -register_stream(SHA1) when is_list(SHA1) -> +register_stream(SHA1) -> + register_stream(SHA1, undefined, undefined, undefined, undefined). +register_stream(SHA1, JID, URL, MyHost, InitiatorPID) when is_list(SHA1) -> + % PIDs are not used for http, we set it for compatibilty with plain socks5 StreamPid = self(), F = fun() -> case mnesia:read(bytestream, SHA1, write) of [] -> mnesia:write(#bytestream{sha1 = SHA1, - target = StreamPid}); + target = StreamPid, + initiator = InitiatorPID, + jid_t = JID, + file = URL, + myhost = MyHost}); [#bytestream{target = Pid, initiator = undefined} = ByteStream] when is_pid(Pid), Pid /= StreamPid -> @@ -145,14 +149,14 @@ unregister_stream(SHA1) when is_list(SHA1) -> %%% IJid = TJid = jid() %%% Host = string() %%%-------------------------------------------------------- -activate_stream(SHA1, IJid, TJid, Host) when is_list(SHA1) -> +activate_stream(SHA1, IJid, TJid, Host, Module) when is_list(SHA1) -> Proc = gen_mod:get_module_proc(Host, ?PROCNAME), case catch gen_server:call(Proc, {activate, SHA1, IJid}) of {atomic, {ok, IPid, TPid}} -> - mod_proxy65_stream:activate({IPid, IJid}, {TPid, TJid}); + Module:activate({IPid, IJid}, {TPid, TJid}); {atomic, {limit, IPid, TPid}} -> - mod_proxy65_stream:stop(IPid), - mod_proxy65_stream:stop(TPid), + Module:stop(IPid), + Module:stop(TPid), limit; {atomic, conflict} -> conflict; diff --git a/src/mod_proxy65/mod_proxy65_stream.erl b/src/mod_proxy65/mod_proxy65_stream.erl index 1a1503384..88ad1eb29 100644 --- a/src/mod_proxy65/mod_proxy65_stream.erl +++ b/src/mod_proxy65/mod_proxy65_stream.erl @@ -163,7 +163,7 @@ wait_for_request(Packet, #state{socket=Socket} = StateData) -> {stop, normal, StateData} end; #s5_request{cmd=udp} -> - Err = mod_proxy65_lib:make_error_reply(Request, ?ERR_COMMAND_NOT_SUPPORTED), + Err = mod_proxy65_lib:make_error_reply(Request, ?SOCKS5_ERR_COMMAND_NOT_SUPPORTED), gen_tcp:send(Socket, Err), {stop, normal, StateData}; _ -> diff --git a/src/web/ejabberd_http.erl b/src/web/ejabberd_http.erl index baa7f861c..fbc4a8118 100644 --- a/src/web/ejabberd_http.erl +++ b/src/web/ejabberd_http.erl @@ -16,7 +16,10 @@ become_controller/1, socket_type/0, receive_headers/1, - url_encode/1]). + url_encode/1, + test/0, get_line/1, data/0, + parse_data/2 + ]). -include("ejabberd.hrl"). -include("jlib.hrl"). @@ -30,6 +33,7 @@ request_auth, request_keepalive, request_content_length, + request_content_type, request_lang = "en", %% XXX bard: request handlers are configured in %% ejabberd.cfg under the HTTP service. For example, @@ -194,6 +198,8 @@ process_header(State, Data) -> _ -> State end; + {ok, {http_header, _, 'Content-Type', _, ContentType}} -> + State#state{request_content_type = ContentType}; {ok, {http_header, _, 'Accept-Language', _, Langs}} -> State#state{request_lang = parse_lang(Langs)}; {ok, {http_header, _, _, _, _}} -> @@ -302,6 +308,7 @@ process_request(#state{request_method = 'POST', request_auth = Auth, request_content_length = Len, request_lang = Lang, + request_content_type = ContentType, sockmod = SockMod, socket = Socket, request_handlers = RequestHandlers} = State) @@ -317,7 +324,7 @@ process_request(#state{request_method = 'POST', case (catch url_decode_q_split(Path)) of {'EXIT', _} -> process_request(false); - {NPath, Query} -> + {NPath, _Query} -> LPath = string:tokens(NPath, "/"), LQuery = case (catch parse_urlencoded(Data)) of {'EXIT', _Reason} -> @@ -329,6 +336,7 @@ process_request(#state{request_method = 'POST', path = LPath, q = LQuery, auth = Auth, + content_type = ContentType, data = Data, lang = Lang}, case process(RequestHandlers, Request) of @@ -355,7 +363,7 @@ process_request(State) -> recv_data(State, Len) -> recv_data(State, Len, []). -recv_data(State, 0, Acc) -> +recv_data(_State, 0, Acc) -> binary_to_list(list_to_binary(Acc)); recv_data(State, Len, Acc) -> case State#state.trail of @@ -615,7 +623,7 @@ code_to_phrase(504) -> "Gateway Timeout"; code_to_phrase(505) -> "HTTP Version Not Supported". -parse_auth(Orig = "Basic " ++ Auth64) -> +parse_auth(_Orig = "Basic " ++ Auth64) -> case decode_base64(Auth64) of {error, _Err} -> undefined; @@ -972,3 +980,102 @@ get_line("\r\n" ++ Tail, Cur) -> end; get_line([H|T], Cur) -> get_line(T, [H|Cur]). + +% return {Value, [{Arg, ArgValue}]} +% example, for +% String = "form-data; name=\"FILE\"; filename=\"TEST\"" +% return +% {"form-data", [{"name", "FILE"}, {"filename", "TEST"}]} +% XXX TODO: don't work if an arg value contains a semicolon ; +parse_header(String) -> + [Value | Args] = string:tokens(String, ";"), + catch {string:strip(Value), + lists:map(fun(Arg) -> + [ArgName, ArgValue] = string:tokens(string:strip(Arg), "="), + {string:strip(ArgName), string:strip(string:strip(ArgValue), both, $")} + end, Args)}. + +% return [#http_data] +parse_data(Data, ContentType) -> + case parse_header(ContentType) of + {"multipart/form-data", Args} -> + case lists:keysearch("boundary", 1, Args) of + {value, {"boundary", Boundary}} -> + parse_multipart_data(Data, "--"++Boundary); + false -> + {error, "no boundary for multipart/form-data Content-Type"} + end; + {'EXIT', _} -> + {error, "malformed Content-Type"}; + _ -> + [#http_data{content_type=ContentType, data=Data}] + end. + +parse_multipart_data(Data, Boundary)-> + case catch parse_multipart_data([], Data, Boundary) of + {'EXIT', _Reason} -> + {error, "malformed multipart/form-data body"}; + List -> + List + end. + +parse_multipart_data(Acc, Tail, Boundary) -> + BoundaryEnd = Boundary++"--\r\n", + case get_line(Tail) of + {incomplete, BoundaryEnd} -> + lists:reverse(Acc); + {line, Boundary, Tail2} -> + parse_multipart_headers([#http_data{data=[]} | Acc], Tail2, Boundary); + {line, Line, Tail2} -> + [#http_data{data=Data} = Cur | T] = Acc, + parse_multipart_data([Cur#http_data{data=Data++Line++"\r\n"} | T], Tail2, Boundary); + {lastline, Line, Tail2} -> + [#http_data{data=Data} = Cur | T] = Acc, + parse_multipart_data([Cur#http_data{data=Data++Line++"\r\n\r\n"} | T], Tail2, Boundary) + end. +parse_multipart_headers([#http_data{args=Args} = Cur | T], Tail, Boundary) -> + case get_line(Tail) of + {LineType, Line, Tail2} when Line /= Boundary -> + NewCur = case Line of + "Content-Type:"++Value -> + {Header, NewArgs} = parse_header(Value), + Cur#http_data{content_type=Header, + args=Args++NewArgs}; + "Content-Disposition:"++Value -> + {Header, NewArgs} = parse_header(Value), + Cur#http_data{content_disposition=Header, + args=Args++NewArgs}; + _ -> + Cur + end, + case LineType of + line -> + parse_multipart_headers([NewCur | T], Tail2, Boundary); + lastline -> + parse_multipart_data([NewCur | T], Tail2, Boundary) + end + end. + + +data() -> + S="-----------------------------7148830871206398517200280906 +Content-Type: text/plain; charset=ISO-8859-2 +Content-Disposition: form-data; name=\"SIDHASH\" + +SIDHASH +-----------------------------7148830871206398517200280906 +Content-Disposition: form-data; name=\"FILE\"; filename=\"TEST\" +Content-Type: application/octet-stream + +TEST + +-----------------------------7148830871206398517200280906-- +", +lists:flatten(lists:map( fun($\n) -> + "\r\n"; + (C) -> + C + end, S)). + +test() -> +parse_data(data(), "multipart/form-data; boundary=---------------------------7148830871206398517200280906"). diff --git a/src/web/ejabberd_http.hrl b/src/web/ejabberd_http.hrl index 7f94c5e24..fa5f6135f 100644 --- a/src/web/ejabberd_http.hrl +++ b/src/web/ejabberd_http.hrl @@ -12,6 +12,13 @@ us, auth, lang = "", + content_type, data = "", ip }). + +-record(http_data, {content_type, + content_disposition, + args=[], + data + }). diff --git a/src/web/ejabberd_web.erl b/src/web/ejabberd_web.erl index a1d6b6592..d0e037d66 100644 --- a/src/web/ejabberd_web.erl +++ b/src/web/ejabberd_web.erl @@ -1,7 +1,7 @@ %%%---------------------------------------------------------------------- %%% File : ejabberd_web.erl %%% Author : Alexey Shchepin -%%% Purpose : +%%% Purpose : %%% Created : 28 Feb 2004 by Alexey Shchepin %%% Id : $Id$ %%%---------------------------------------------------------------------- @@ -53,7 +53,11 @@ make_xhtml(Els) -> {"name", Name}, {"value", Value}])). +error(bad_request) -> + {400, [], make_xhtml([?XC("h1", "400 Bad Request")])}; +error(not_allowed) -> + {401, [], make_xhtml([?XC("h1", "401 Unauthorized")])}; error(not_found) -> {404, [], make_xhtml([?XC("h1", "404 Not Found")])}; -error(not_allowed) -> - {401, [], make_xhtml([?XC("h1", "401 Unauthorized")])}. +error(internal) -> + {500, [], make_xhtml([?XC("h1", "500 Internal Error")])}.