diff --git a/mod_http_upload/COPYING b/mod_http_upload/COPYING new file mode 100644 index 0000000..cc498bd --- /dev/null +++ b/mod_http_upload/COPYING @@ -0,0 +1,342 @@ +As a special exception, the authors give permission to link this program +with the OpenSSL library and distribute the resulting binary. + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/mod_http_upload/README.txt b/mod_http_upload/README.txt new file mode 100644 index 0000000..9a842a2 --- /dev/null +++ b/mod_http_upload/README.txt @@ -0,0 +1,121 @@ + + mod_http_upload - HTTP File Upload + + Author: Holger Weiss + Requirements: ejabberd 13.06 or newer + + + DESCRIPTION + ----------- + +This module allows for requesting permissions to upload a file via HTTP. +If the request is accepted, the client receives a URL to use for uploading +the file and another URL from which that file can later be downloaded. + +PLEASE NOTE: This module implements an experimental protocol which has NOT +been approved by the XMPP Standards Foundation and may change at any time: + + http://xmpp.org/extensions/inbox/http-upload.html + +There are already suggestions for improvements, e.g. from ProcessOne: + + https://github.com/processone/ejabberd-saas-docs/blob/master/xmpp-specs/http-filetransfer/http-filetransfer.md + + + CONFIGURATION + ------------- + +In order to use this module, add configuration snippets such as the +following to your ejabberd.yml file: + + listen: + # [...] + - + module: ejabberd_http + port: 5443 + tls: true + certfile: "/etc/ejabberd/example.com.pem" + request_handlers: + "": mod_http_upload + + modules: + # [...] + mod_http_upload: + docroot: "/home/xmpp/upload" + +The configurable mod_http_upload options are: + +- host (default: "upload.@HOST@") + + This option defines the JID for the HTTP upload service. The keyword + @HOST@ is replaced with the virtual host name. + +- name (default: "HTTP File Upload") + + This option defines the Service Discovery name for the HTTP upload + service. + +- access (default: 'local') + + This option defines the access rule to limit who is permitted to use the + HTTP upload service. The default value is 'local'. If no access rule of + that name exists, no user will be allowed to use the service. + +- max_size (default: 104857600) + + This option limits the acceptable file size. Either a number of bytes + (larger than zero) or 'infinity' must be specified. + +- docroot (default: 'undefined') + + Uploaded files are stored below the directory specified (as an absolute + path) with this option. It is mandatory to specify either this option or + the 'service_url' option. + +- put_url (default: "https://@HOST@:5443") + + This option specifies the initial part of the PUT URLs used for file + uploads. Note that @HOST@ can NOT be specified for this option in the + configuration file, but the virtual host name is used as part of the URL + by default. + +- get_url (default: $put_url) + + This option specifies the initial part of the GET URLs used for + downloading the files. By default, it is set to the same value as the + 'put_url', but you can set it to a different value in order to have the + files served by a proper HTTP server such as Nginx or Apache. + +- service_url (default: 'undefined') + + If a 'service_url' is specified, HTTP upload slot requests are forwarded + to this external service instead of being handled by mod_http_upload + itself. An HTTP GET query such as the following is issued whenever an + HTTP upload slot request is accepted as per the 'access' rule: + + http://localhost:5444/?jid=juliet%40example.com&size=10240&name=example.jpg + + In order to accept the request, the service must return an HTTP status + code of 200 or 201 and two lines of text/plain output. The first line is + forwarded to the XMPP client as the HTTP upload PUT URL, the second line + as the GET URL. + + In order to reject the request, the service should return one of the + following HTTP status codes: + + - 402 + In this case, a 'resource-constraint' error stanza is sent to the + client. Use this to indicate a temporary error after the client + exceeded a quota, for example. + + - 403 + In this case, a 'not-allowed' error stanza is sent to the client. Use + this to indicate a permanent error to a client that is not permitted to + upload files, for example. + + - 413 + In this case, a 'not-acceptable' error stanza is sent to the client. + Use this if the file size was too large, for example. + + In any other case, a 'service-unavailable' error stanza is sent to the + client. diff --git a/mod_http_upload/conf/mod_http_upload.yml b/mod_http_upload/conf/mod_http_upload.yml new file mode 100644 index 0000000..5ca82b4 --- /dev/null +++ b/mod_http_upload/conf/mod_http_upload.yml @@ -0,0 +1,12 @@ +listen: + - + module: ejabberd_http + port: 5443 + tls: true + certfile: "/etc/ejabberd/example.com.pem" + request_handlers: + "": mod_http_upload + +modules: + mod_http_upload: + docroot: "/home/xmpp/upload" diff --git a/mod_http_upload/mod_http_upload.spec b/mod_http_upload/mod_http_upload.spec new file mode 100644 index 0000000..7163084 --- /dev/null +++ b/mod_http_upload/mod_http_upload.spec @@ -0,0 +1,5 @@ +author: "Holger Weiss " +category: "http" +summary: "HTTP File Upload" +home: "https://github.com/processone/ejabberd-contrib/tree/master/" +url: "git@github.com:processone/ejabberd-contrib.git" diff --git a/mod_http_upload/src/mod_http_upload.erl b/mod_http_upload/src/mod_http_upload.erl new file mode 100644 index 0000000..b9bcf72 --- /dev/null +++ b/mod_http_upload/src/mod_http_upload.erl @@ -0,0 +1,670 @@ +%%%------------------------------------------------------------------- +%%% File : mod_http_upload.erl +%%% Author : Holger Weiss +%%% Purpose : HTTP File Upload +%%% Created : 20 Aug 2015 by Holger Weiss +%%%------------------------------------------------------------------- + +-module(mod_http_upload). +-author('holger@zedat.fu-berlin.de'). + +-define(GEN_SERVER, gen_server). +-define(NS_UPLOAD, <<"eu:siacs:conversations:http:upload">>). +-define(SERVICE_REQUEST_TIMEOUT, 5000). % 5 seconds. +-define(SLOT_TIMEOUT, 600000). % 10 minutes. +-define(PROCNAME, ?MODULE). +-define(URL_ENC(URL), binary_to_list(ejabberd_http:url_encode(URL))). +-define(ADDR_TO_STR(IP), ejabberd_config:may_hide_data(jlib:ip_to_list(IP))). +-define(DEFAULT_CONTENT_TYPE, <<"application/octet-stream">>). +-define(CONTENT_TYPES, + [{<<".avi">>, <<"video/avi">>}, + {<<".bmp">>, <<"image/bmp">>}, + {<<".bz2">>, <<"application/x-bzip2">>}, + {<<".gif">>, <<"image/gif">>}, + {<<".gz">>, <<"application/x-gzip">>}, + {<<".html">>, <<"text/html">>}, + {<<".jpeg">>, <<"image/jpeg">>}, + {<<".jpg">>, <<"image/jpeg">>}, + {<<".mp3">>, <<"audio/mpeg">>}, + {<<".mp4">>, <<"video/mp4">>}, + {<<".mpeg">>, <<"video/mpeg">>}, + {<<".mpg">>, <<"video/mpeg">>}, + {<<".ogg">>, <<"application/ogg">>}, + {<<".pdf">>, <<"application/pdf">>}, + {<<".png">>, <<"image/png">>}, + {<<".rtf">>, <<"application/rtf">>}, + {<<".svg">>, <<"image/svg+xml">>}, + {<<".tiff">>, <<"image/tiff">>}, + {<<".txt">>, <<"text/plain">>}, + {<<".wav">>, <<"audio/wav">>}, + {<<".webp">>, <<"image/webp">>}, + {<<".xz">>, <<"application/x-xz">>}, + {<<".zip">>, <<"application/zip">>}]). + +-behaviour(?GEN_SERVER). +-behaviour(gen_mod). + +%% gen_mod/supervisor callbacks. +-export([start_link/3, + start/2, + stop/1, + mod_opt_type/1]). + +%% gen_server callbacks. +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). + +%% ejabberd_http callback. +-export([process/2]). + +%% ejabberd_hooks callback. +-export([remove_user/2]). + +-include("ejabberd.hrl"). +-include("ejabberd_http.hrl"). +-include("jlib.hrl"). +-include("logger.hrl"). + +-record(state, + {server_host :: binary(), + host :: binary(), + name :: binary(), + access :: atom(), + max_size :: pos_integer() | infinity, + docroot :: binary(), + put_url :: binary(), + get_url :: binary(), + service_url :: binary() | undefined, + slots = dict:new() :: term()}). % dict:dict() requires Erlang 17. + +-type state() :: #state{}. +-type slot() :: [binary()]. + +%%-------------------------------------------------------------------- +%% gen_mod/supervisor callbacks. +%%-------------------------------------------------------------------- + +-spec start_link(binary(), binary(), gen_mod:opts()) + -> {ok, pid()} | ignore | {error, _}. + +start_link(ServerHost, ProcHost, Opts) -> + Proc = gen_mod:get_module_proc(ProcHost, ?PROCNAME), + ?GEN_SERVER:start_link({local, Proc}, ?MODULE, {ServerHost, Opts}, []). + +-spec start(binary(), gen_mod:opts()) -> {ok, _} | {ok, _, _} | {error, _}. + +start(ServerHost, Opts) -> + ejabberd_hooks:add(remove_user, ServerHost, ?MODULE, + remove_user, 50), + ejabberd_hooks:add(anonymous_purge_hook, ServerHost, ?MODULE, + remove_user, 50), + PutURL = gen_mod:get_opt(put_url, Opts, + fun(<<"http://", _/binary>> = URL) -> URL; + (<<"https://", _/binary>> = URL) -> URL; + (_) -> <<"https://", ServerHost/binary>> + end, <<"https://", ServerHost/binary>>), + [_, ProcHost | _] = binary:split(PutURL, + [<<"http://">>, <<"https://">>, + <<":">>, <<"/">>], [global]), + Proc = gen_mod:get_module_proc(ProcHost, ?PROCNAME), + Spec = {Proc, + {?MODULE, start_link, [ServerHost, ProcHost, Opts]}, + permanent, + 3000, + worker, + [?MODULE]}, + supervisor:start_child(ejabberd_sup, Spec). + +-spec stop(binary()) -> ok. + +stop(ServerHost) -> + ejabberd_hooks:delete(remove_user, ServerHost, ?MODULE, + remove_user, 50), + ejabberd_hooks:delete(anonymous_purge_hook, ServerHost, ?MODULE, + remove_user, 50), + Proc = gen_mod:get_module_proc(ServerHost, ?PROCNAME), + ok = supervisor:terminate_child(ejabberd_sup, Proc), + ok = supervisor:delete_child(ejabberd_sup, Proc). + +-spec mod_opt_type(atom()) -> fun((term()) -> term()). + +mod_opt_type(host) -> + fun iolist_to_binary/1; +mod_opt_type(name) -> + fun iolist_to_binary/1; +mod_opt_type(access) -> + fun (A) when is_atom(A) -> A end; +mod_opt_type(max_size) -> + fun (I) when is_integer(I), I > 0 -> I; + (infinity) -> infinity + end; +mod_opt_type(docroot) -> + fun iolist_to_binary/1; +mod_opt_type(put_url) -> + fun(<<"http://", _/binary>> = URL) -> URL; + (<<"https://", _/binary>> = URL) -> URL + end; +mod_opt_type(get_url) -> + fun(<<"http://", _/binary>> = URL) -> URL; + (<<"https://", _/binary>> = URL) -> URL + end; +mod_opt_type(service_url) -> + fun(<<"http://", _/binary>> = URL) -> URL; + (<<"https://", _/binary>> = URL) -> URL + end; +mod_opt_type(_) -> + [host, name, access, max_size, docroot, put_url, get_url, service_url]. + +%%-------------------------------------------------------------------- +%% gen_server callbacks. +%%-------------------------------------------------------------------- + +-spec init({binary(), gen_mod:opts()}) -> {ok, state()}. + +init({ServerHost, Opts}) -> + process_flag(trap_exit, true), + Host = gen_mod:get_opt_host(ServerHost, Opts, <<"upload.@HOST@">>), + Name = gen_mod:get_opt(name, Opts, fun iolist_to_binary/1, + <<"HTTP File Upload">>), + Access = gen_mod:get_opt(access, Opts, fun(A) when is_atom(A) -> A end, + local), + MaxSize = gen_mod:get_opt(max_size, Opts, + fun (I) when is_integer(I), I > 0 -> I; + (infinity) -> infinity + end, 104857600), + DocRoot = gen_mod:get_opt(docroot, Opts, fun iolist_to_binary/1, + undefined), + PutURL = gen_mod:get_opt(put_url, Opts, + fun(<<"http://", _/binary>> = URL) -> URL; + (<<"https://", _/binary>> = URL) -> URL + end, <<"https://", ServerHost/binary, ":5443">>), + GetURL = gen_mod:get_opt(get_url, Opts, + fun(<<"http://", _/binary>> = URL) -> URL; + (<<"https://", _/binary>> = URL) -> URL + end, PutURL), + ServiceURL = gen_mod:get_opt(service_url, Opts, + fun(<<"http://", _/binary>> = URL) -> URL; + (<<"https://", _/binary>> = URL) -> URL + end, undefined), + case {DocRoot, ServiceURL} of + {undefined, undefined} -> + ?ERROR_MSG("A mod_http_upload 'docroot' MUST be specified", []), + erlang:error(configuration_error); + _ -> + ok + end, + case ServiceURL of + undefined -> + ok; + <<"http://", _/binary>> -> + application:start(inets); + <<"https://", _/binary>> -> + application:start(inets), + application:start(crypto), + application:start(public_key), + application:start(ssl) + end, + ejabberd_router:register_route(Host), + {ok, #state{server_host = ServerHost, host = Host, name = Name, + access = Access, max_size = MaxSize, docroot = DocRoot, + put_url = str:strip(PutURL, right, $/), + get_url = str:strip(GetURL, right, $/), + service_url = ServiceURL}}. + +-spec handle_call(_, {pid(), _}, state()) -> {noreply, state()}. + +handle_call({use_slot, Slot}, _From, #state{docroot = DocRoot} = State) -> + case get_slot(Slot, State) of + {ok, {Size, Timer}} -> + timer:cancel(Timer), + NewState = del_slot(Slot, State), + Path = str:join([DocRoot | Slot], <<$/>>), + {reply, {ok, Size, Path}, NewState}; + error -> + {reply, {error, <<"Invalid slot">>}, State} + end; +handle_call(get_docroot, _From, #state{docroot = DocRoot} = State) -> + {reply, {ok, DocRoot}, State}; +handle_call(Request, From, State) -> + ?ERROR_MSG("Got unexpected request from ~p: ~p", [From, Request]), + {noreply, State}. + +-spec handle_cast(_, state()) -> {noreply, state()}. + +handle_cast(Request, State) -> + ?ERROR_MSG("Got unexpected request: ~p", [Request]), + {noreply, State}. + +-spec handle_info(timeout | _, state()) -> {noreply, state()}. + +handle_info({route, From, To, #xmlel{name = <<"iq">>} = Stanza}, State) -> + Request = jlib:iq_query_info(Stanza), + {Reply, NewState} = case process_iq(From, Request, State) of + R when is_record(R, iq) -> + {R, State}; + {R, S} -> + {R, S} + end, + ejabberd_router:route(To, From, jlib:iq_to_xml(Reply)), + {noreply, NewState}; +handle_info({slot_timed_out, Slot}, State) -> + NewState = del_slot(Slot, State), + {noreply, NewState}; +handle_info(Info, State) -> + ?ERROR_MSG("Got unexpected info: ~p", [Info]), + {noreply, State}. + +-spec terminate(normal | shutdown | {shutdown, _} | _, _) -> ok. + +terminate(Reason, #state{server_host = ServerHost, host = Host}) -> + ?DEBUG("Stopping HTTP upload process for ~s: ~p", [ServerHost, Reason]), + ejabberd_router:unregister_route(Host), + ok. + +-spec code_change({down, _} | _, state(), _) -> {ok, state()}. + +code_change(_OldVsn, #state{server_host = ServerHost} = State, _Extra) -> + ?DEBUG("Updating HTTP upload process for ~s", [ServerHost]), + {ok, State}. + +%%-------------------------------------------------------------------- +%% ejabberd_http callback. +%%-------------------------------------------------------------------- + +-spec process([binary()], #request{}) + -> {pos_integer(), [{binary(), binary()}], binary()}. + +process(LocalPath, #request{method = 'PUT', host = Host, ip = IP, + data = Data}) -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + case catch gen_server:call(Proc, {use_slot, LocalPath}) of + {ok, Size, Path} when byte_size(Data) == Size -> + ?DEBUG("Storing file from ~s for ~s: ~s", + [?ADDR_TO_STR(IP), Host, Path]), + case store_file(Path, Data) of + ok -> + http_response(201); + {error, Error} -> + ?ERROR_MSG("Cannot store file ~s from ~s for ~s: ~s", + [Path, ?ADDR_TO_STR(IP), Host, Error]), + http_response(500) + end; + {ok, Size, Path} -> + ?INFO_MSG("Rejecting file ~s from ~s for ~s: Size is ~B, not ~B", + [Path, ?ADDR_TO_STR(IP), Host, byte_size(Data), Size]), + http_response(413); + {error, Error} -> + ?INFO_MSG("Rejecting file from ~s for ~s: ~p", + [?ADDR_TO_STR(IP), Host, Error]), + http_response(403); + Error -> + ?ERROR_MSG("Cannot handle PUT request from ~s for ~s: ~p", + [?ADDR_TO_STR(IP), Host, Error]), + http_response(500) + end; +process(LocalPath, #request{method = 'GET', host = Host, ip = IP}) -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + case catch gen_server:call(Proc, get_docroot) of + {ok, DocRoot} -> + Path = str:join([DocRoot | LocalPath], <<$/>>), + case file:read_file(Path) of + {ok, Data} -> + ?INFO_MSG("Serving ~s to ~s", [Path, ?ADDR_TO_STR(IP)]), + FileName = lists:last(LocalPath), + ContentType = guess_content_type(FileName), + Headers1 = case ContentType of + <<"image/", _SubType/binary>> -> []; + <<"text/", _SubType/binary>> -> []; + _ -> + [{<<"Content-Disposition">>, + <<"attachment; filename=", + $", FileName/binary, $">>}] + end, + Headers2 = [{<<"Content-Type">>, ContentType} | Headers1], + http_response(200, Headers2, Data); + {error, eacces} -> + ?INFO_MSG("Cannot serve ~s to ~s: Permission denied", + [Path, ?ADDR_TO_STR(IP)]), + http_response(403); + {error, enoent} -> + ?INFO_MSG("Cannot serve ~s to ~s: No such file or directory", + [Path, ?ADDR_TO_STR(IP)]), + http_response(404); + {error, eisdir} -> + ?INFO_MSG("Cannot serve ~s to ~s: Is a directory", + [Path, ?ADDR_TO_STR(IP)]), + http_response(404); + {error, Error} -> + ?INFO_MSG("Cannot serve ~s to ~s: ~p", + [Path, ?ADDR_TO_STR(IP), Error]), + http_response(500) + end; + Error -> + ?ERROR_MSG("Cannot handle GET request from ~s for ~s: ~p", + [?ADDR_TO_STR(IP), Host, Error]), + http_response(500) + end; +process(_LocalPath, #request{method = Method, host = Host, ip = IP}) -> + ?DEBUG("Rejecting ~s request from ~s for ~s", + [Method, ?ADDR_TO_STR(IP), Host]), + http_response(405, [{<<"Allow">>, <<"GET, PUT">>}]). + +%%-------------------------------------------------------------------- +%% Internal functions. +%%-------------------------------------------------------------------- + +%% XMPP request handling. + +-spec process_iq(jid(), iq_request(), state()) + -> {iq_reply(), state()} | iq_reply(). + +process_iq(_From, + #iq{type = get, xmlns = ?NS_DISCO_INFO, lang = Lang} = IQ, + #state{server_host = ServerHost, name = Name}) -> + AddInfo = ejabberd_hooks:run_fold(disco_info, ServerHost, [], + [ServerHost, ?MODULE, <<"">>, <<"">>]), + IQ#iq{type = result, + sub_el = [#xmlel{name = <<"query">>, + attrs = [{<<"xmlns">>, ?NS_DISCO_INFO}], + children = iq_disco_info(Lang, Name) ++ AddInfo}]}; +process_iq(#jid{luser = LUser, lserver = LServer} = From, + #iq{type = get, xmlns = ?NS_UPLOAD, lang = Lang, + sub_el = SubEl} = IQ, + #state{server_host = ServerHost, access = Access} = State) -> + User = <>, + case acl:match_rule(ServerHost, Access, From) of + allow -> + case parse_request(SubEl, Lang) of + {ok, File, Size, ContentType} -> + case create_slot(State, User, File, Size, ContentType, Lang) of + {ok, Slot} -> + {ok, Timer} = timer:send_after(?SLOT_TIMEOUT, + {slot_timed_out, Slot}), + NewState = add_slot(Slot, Size, Timer, State), + {IQ#iq{type = result, sub_el = [slot_el(Slot, State)]}, + NewState}; + {ok, PutURL, GetURL} -> + IQ#iq{type = result, sub_el = [slot_el(PutURL, GetURL)]}; + {error, Error} -> + IQ#iq{type = error, sub_el = [SubEl, Error]} + end; + {error, Error} -> + ?DEBUG("Cannot parse request from ~s", [User]), + IQ#iq{type = error, sub_el = [SubEl, Error]} + end; + deny -> + ?DEBUG("Denying HTTP upload slot request from ~s", [User]), + IQ#iq{type = error, sub_el = [SubEl, ?ERR_FORBIDDEN]} + end; +process_iq(_From, #iq{sub_el = SubEl} = IQ, _State) -> + IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}. + +-spec parse_request(xmlel(), binary()) + -> {ok, binary(), pos_integer(), binary()} | {error, xmlel()}. + +parse_request(#xmlel{name = <<"request">>, attrs = Attrs} = Request, Lang) -> + case xml:get_attr(<<"xmlns">>, Attrs) of + {value, ?NS_UPLOAD} -> + case {xml:get_subtag_cdata(Request, <<"filename">>), + xml:get_subtag_cdata(Request, <<"size">>), + xml:get_subtag_cdata(Request, <<"content-type">>)} of + {File, SizeStr, ContentType} when byte_size(File) > 0 -> + case catch binary_to_integer(SizeStr) of + Size when is_integer(Size), Size > 0 -> + {ok, File, Size, yield_content_type(ContentType)}; + _ -> + Text = <<"Please specify file size.">>, + {error, ?ERRT_BAD_REQUEST(Lang, Text)} + end; + _ -> + Text = <<"Please specify file name.">>, + {error, ?ERRT_BAD_REQUEST(Lang, Text)} + end; + _ -> + {error, ?ERR_BAD_REQUEST} + end; +parse_request(_El, _Lang) -> {error, ?ERR_BAD_REQUEST}. + +-spec create_slot(state(), binary(), binary(), pos_integer(), binary(), + binary()) + -> {ok, slot()} | {ok, binary(), binary()} | {error, xmlel()}. + +create_slot(#state{service_url = undefined, max_size = MaxSize}, + User, File, Size, _ContentType, Lang) when MaxSize /= infinity, + Size > MaxSize -> + Text = <<"File larger than ", (integer_to_binary(MaxSize))/binary, " B.">>, + ?INFO_MSG("Rejecting file ~s from ~s (too large: ~B bytes)", + [File, User, Size]), + {error, ?ERRT_NOT_ACCEPTABLE(Lang, Text)}; +create_slot(#state{service_url = undefined}, User, File, _Size, _ContentType, + _Lang) -> + UserHash = p1_sha:sha(User), + RandStr = make_rand_string(40), + SaneFile = re:replace(File, <<"[^a-zA-Z0-9_.-]">>, <<$_>>, + [global, {return, binary}]), + ?INFO_MSG("Got HTTP upload slot for ~s (file: ~s)", [User, File]), + {ok, [UserHash, RandStr, SaneFile]}; +create_slot(#state{service_url = ServiceURL}, User, File, Size, ContentType, + _Lang) -> + Options = [{body_format, binary}, {full_result, false}], + HttpOptions = [{timeout, ?SERVICE_REQUEST_TIMEOUT}], + SizeStr = integer_to_binary(Size), + GetRequest = binary_to_list(ServiceURL) ++ + "?jid=" ++ ?URL_ENC(User) ++ + "&name=" ++ ?URL_ENC(File) ++ + "&size=" ++ ?URL_ENC(SizeStr) ++ + "&content_type=" ++ ?URL_ENC(ContentType), + case httpc:request(get, {GetRequest, []}, HttpOptions, Options) of + {ok, {Code, Body}} when Code >= 200, Code =< 299 -> + case binary:split(Body, <<$\n>>, [global, trim]) of + [<<"http", _/binary>> = PutURL, <<"http", _/binary>> = GetURL] -> + ?INFO_MSG("Got HTTP upload slot for ~s (file: ~s)", + [User, File]), + {ok, PutURL, GetURL}; + Lines -> + ?ERROR_MSG("Cannot parse data received for ~s from <~s>: ~p", + [User, ServiceURL, Lines]), + {error, ?ERR_SERVICE_UNAVAILABLE} + end; + {error, {402, _Body}} -> + ?INFO_MSG("Got status code 402 for ~s from <~s>", [User, ServiceURL]), + {error, ?ERR_RESOURCE_CONSTRAINT}; + {error, {403, _Body}} -> + ?INFO_MSG("Got status code 403 for ~s from <~s>", [User, ServiceURL]), + {error, ?ERR_NOT_ALLOWED}; + {error, {413, _Body}} -> + ?INFO_MSG("Got status code 413 for ~s from <~s>", [User, ServiceURL]), + {error, ?ERR_NOT_ACCEPTABLE}; + {error, {Code, _Body}} -> + ?ERROR_MSG("Got unexpected status code ~s from <~s>: ~B", + [User, ServiceURL, Code]), + {error, ?ERR_SERVICE_UNAVAILABLE}; + {error, Reason} -> + ?ERROR_MSG("Error requesting upload slot for ~s from <~s>: ~p", + [User, ServiceURL, Reason]), + {error, ?ERR_SERVICE_UNAVAILABLE} + end. + +-spec add_slot(slot(), pos_integer(), timer:tref(), state()) -> state(). + +add_slot(Slot, Size, Timer, #state{slots = Slots} = State) -> + NewSlots = dict:store(Slot, {Size, Timer}, Slots), + State#state{slots = NewSlots}. + +-spec get_slot(slot(), state()) -> {ok, {pos_integer(), timer:tref()}} | error. + +get_slot(Slot, #state{slots = Slots}) -> + dict:find(Slot, Slots). + +-spec del_slot(slot(), state()) -> state(). + +del_slot(Slot, #state{slots = Slots} = State) -> + NewSlots = dict:erase(Slot, Slots), + State#state{slots = NewSlots}. + +-spec slot_el(slot() | binary(), state() | binary()) -> xmlel(). + +slot_el(Slot, #state{put_url = PutPrefix, get_url = GetPrefix}) -> + PutURL = str:join([PutPrefix | Slot], <<$/>>), + GetURL = str:join([GetPrefix | Slot], <<$/>>), + slot_el(PutURL, GetURL); +slot_el(PutURL, GetURL) -> + #xmlel{name = <<"slot">>, + attrs = [{<<"xmlns">>, ?NS_UPLOAD}], + children = [#xmlel{name = <<"put">>, + children = [{xmlcdata, PutURL}]}, + #xmlel{name = <<"get">>, + children = [{xmlcdata, GetURL}]}]}. + +-spec make_rand_string(non_neg_integer()) -> binary(). + +make_rand_string(Length) -> + list_to_binary(make_rand_string([], Length)). + +-spec make_rand_string(string(), non_neg_integer()) -> string(). + +make_rand_string(S, 0) -> S; +make_rand_string(S, N) -> make_rand_string([make_rand_char() | S], N - 1). + +-spec make_rand_char() -> char(). + +make_rand_char() -> + map_int_to_char(crypto:rand_uniform(0, 62)). + +-spec map_int_to_char(0..61) -> char(). + +map_int_to_char(N) when N =< 9 -> N + 48; % Digit. +map_int_to_char(N) when N =< 35 -> N + 55; % Upper-case character. +map_int_to_char(N) when N =< 61 -> N + 61. % Lower-case character. + +-spec yield_content_type(binary()) -> binary(). + +yield_content_type(<<"">>) -> <<"application/octet-stream">>; +yield_content_type(Type) -> Type. + +-spec iq_disco_info(binary(), binary()) -> [xmlel()]. + +iq_disco_info(Lang, Name) -> + [#xmlel{name = <<"identity">>, + attrs = [{<<"category">>, <<"store">>}, + {<<"type">>, <<"file">>}, + {<<"name">>, translate:translate(Lang, Name)}]}, + #xmlel{name = <<"feature">>, + attrs = [{<<"var">>, ?NS_UPLOAD}]}]. + +%% HTTP request handling. + +-spec store_file(file:filename_all(), binary()) -> ok | {error, term()}. + +store_file(Path, Data) -> + try + ok = filelib:ensure_dir(Path), + {ok, Io} = file:open(Path, [write, exclusive, raw]), + Ok = file:write(Io, Data), + ok = file:close(Io), % Close file even if file:write/2 failed. + ok = Ok % But raise an exception in that case. + catch + _:{badmatch, {error, Error}} -> + {error, Error}; + _:Error -> + {error, Error} + end. + +-spec guess_content_type(binary()) -> binary(). + +guess_content_type(FileName) -> + mod_http_fileserver:content_type(FileName, + ?DEFAULT_CONTENT_TYPE, + ?CONTENT_TYPES). + +-spec http_response(100..599) + -> {pos_integer(), [{binary(), binary()}], binary()}. + +http_response(Code) -> + http_response(Code, []). + +-spec http_response(100..599, [{binary(), binary()}]) + -> {pos_integer(), [{binary(), binary()}], binary()}. + +http_response(Code, ExtraHeaders) -> + http_response(Code, ExtraHeaders, <<(code_to_message(Code))/binary, $\n>>). + +-spec http_response(100..599, [{binary(), binary()}], binary()) + -> {pos_integer(), [{binary(), binary()}], binary()}. + +http_response(Code, ExtraHeaders, Body) -> + ServerHeader = {<<"Server">>, <<"ejabberd ", (?VERSION)/binary>>}, + Headers = case proplists:is_defined(<<"Content-Type">>, ExtraHeaders) of + true -> + [ServerHeader | ExtraHeaders]; + false -> + [ServerHeader, {<<"Content-Type">>, <<"text/plain">>} | + ExtraHeaders] + end, + {Code, Headers, Body}. + +-spec code_to_message(100..599) -> binary(). + +code_to_message(201) -> <<"Upload successful.">>; +code_to_message(403) -> <<"Forbidden.">>; +code_to_message(404) -> <<"Not found.">>; +code_to_message(405) -> <<"Method not allowed.">>; +code_to_message(413) -> <<"File size doesn't match requested size.">>; +code_to_message(500) -> <<"Internal server error.">>. + +%%-------------------------------------------------------------------- +%% Remove user. +%%-------------------------------------------------------------------- + +-spec remove_user(binary(), binary()) -> ok. + +remove_user(User, Server) -> + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Server), + case gen_mod:get_module_opt(LServer, ?MODULE, docroot, + fun iolist_to_binary/1) of + undefined -> + ok; + DocRoot -> + UserHash = p1_sha:sha(LUser), + UserDir = str:join([DocRoot, UserHash], <<$/>>), + case del_tree(UserDir) of + ok -> + ?INFO_MSG("Removed HTTP upload directory of ~s@~s", + [User, Server]); + {error, enoent} -> + ?DEBUG("Found no HTTP upload directory of ~s@~s", + [User, Server]); + {error, Error} -> + ?ERROR_MSG("Cannot remove HTTP upload directory of ~s@~s: ~p", + [User, Server, Error]) + end, + ok + end. + +-spec del_tree(file:filename_all()) -> ok | {error, term()}. + +del_tree(Dir) when is_binary(Dir) -> + del_tree(binary_to_list(Dir)); +del_tree(Dir) -> + try + {ok, Entries} = file:list_dir(Dir), + lists:foreach(fun(Path) -> + case filelib:is_dir(Path) of + true -> + ok = del_tree(Path); + false -> + ok = file:delete(Path) + end + end, [Dir ++ "/" ++ Entry || Entry <- Entries]), + ok = file:del_dir(Dir) + catch + _:{badmatch, {error, Error}} -> + {error, Error}; + _:Error -> + {error, Error} + end.