diff options
Diffstat (limited to 'lib/inets/src')
93 files changed, 1600 insertions, 8026 deletions
diff --git a/lib/inets/src/Makefile b/lib/inets/src/Makefile index a2a70a7b8f..281dd388cb 100644 --- a/lib/inets/src/Makefile +++ b/lib/inets/src/Makefile @@ -1,7 +1,7 @@ # # %CopyrightBegin% # -# Copyright Ericsson AB 1996-2009. All Rights Reserved. +# Copyright Ericsson AB 1996-2016. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/inets/src/ftp/Makefile b/lib/inets/src/ftp/Makefile deleted file mode 100644 index f8daa2b894..0000000000 --- a/lib/inets/src/ftp/Makefile +++ /dev/null @@ -1,104 +0,0 @@ -# -# %CopyrightBegin% -# -# Copyright Ericsson AB 2005-2012. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# %CopyrightEnd% -# -# - -include $(ERL_TOP)/make/target.mk -EBIN = ../../ebin -include $(ERL_TOP)/make/$(TARGET)/otp.mk - - -# ---------------------------------------------------- -# Application version -# ---------------------------------------------------- -include ../../vsn.mk - -VSN = $(INETS_VSN) - - -# ---------------------------------------------------- -# Release directory specification -# ---------------------------------------------------- -RELSYSDIR = $(RELEASE_PATH)/lib/$(APPLICATION)-$(VSN) - - -# ---------------------------------------------------- -# Target Specs -# ---------------------------------------------------- -MODULES = \ - ftp \ - ftp_progress \ - ftp_response \ - ftp_sup - -HRL_FILES = ftp_internal.hrl - -ERL_FILES = $(MODULES:%=%.erl) - -TARGET_FILES= $(MODULES:%=$(EBIN)/%.$(EMULATOR)) - - -# ---------------------------------------------------- -# FLAGS -# ---------------------------------------------------- - -include ../inets_app/inets.mk - -ifeq ($(FTP_DEBUG),true) - INETS_FLAGS += -Dftp_debug -endif - -ERL_COMPILE_FLAGS += \ - $(INETS_FLAGS) \ - $(INETS_ERL_COMPILE_FLAGS) \ - -I../../include \ - -I../inets_app - - -# ---------------------------------------------------- -# Targets -# ---------------------------------------------------- - -debug opt: $(TARGET_FILES) - -clean: - rm -f $(TARGET_FILES) - rm -f core - -docs: - -# ---------------------------------------------------- -# Release Target -# ---------------------------------------------------- -include $(ERL_TOP)/make/otp_release_targets.mk - -release_spec: opt - $(INSTALL_DIR) "$(RELSYSDIR)/src" - $(INSTALL_DIR) "$(RELSYSDIR)/src/ftp" - $(INSTALL_DATA) $(HRL_FILES) $(ERL_FILES) "$(RELSYSDIR)/src/ftp" - $(INSTALL_DIR) "$(RELSYSDIR)/ebin" - $(INSTALL_DATA) $(TARGET_FILES) "$(RELSYSDIR)/ebin" - -release_docs_spec: - -info: - @echo "APPLICATION = $(APPLICATION)" - @echo "INETS_DEBUG = $(INETS_DEBUG)" - @echo "INETS_FLAGS = $(INETS_FLAGS)" - @echo "ERL_COMPILE_FLAGS = $(ERL_COMPILE_FLAGS)" diff --git a/lib/inets/src/ftp/ftp.erl b/lib/inets/src/ftp/ftp.erl deleted file mode 100644 index db6260c7af..0000000000 --- a/lib/inets/src/ftp/ftp.erl +++ /dev/null @@ -1,2523 +0,0 @@ -%% -%% %CopyrightBegin% -%% -%% Copyright Ericsson AB 1997-2015. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%% -%% %CopyrightEnd% -%% -%% -%% Description: This module implements an ftp client, RFC 959. -%% It also supports ipv6 RFC 2428 and starttls RFC 4217. - --module(ftp). - --behaviour(gen_server). --behaviour(inets_service). - - -%% API - Client interface --export([cd/2, close/1, delete/2, formaterror/1, - lcd/2, lpwd/1, ls/1, ls/2, - mkdir/2, nlist/1, nlist/2, - open/1, open/2, - pwd/1, quote/2, - recv/2, recv/3, recv_bin/2, - recv_chunk_start/2, recv_chunk/1, - rename/3, rmdir/2, - send/2, send/3, send_bin/3, - send_chunk_start/2, send_chunk/2, send_chunk_end/1, - type/2, user/3, user/4, account/2, - append/3, append/2, append_bin/3, - append_chunk/2, append_chunk_end/1, append_chunk_start/2, - info/1, latest_ctrl_response/1]). - -%% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, - handle_info/2, terminate/2, code_change/3]). - -%% supervisor callbacks --export([start_link/1, start_link/2]). - -%% Behavior callbacks --export([start_standalone/1, start_service/1, - stop_service/1, services/0, service_info/1]). - --include("ftp_internal.hrl"). - -%% Constants used in internal state definition --define(CONNECTION_TIMEOUT, 60*1000). --define(DATA_ACCEPT_TIMEOUT, infinity). --define(DEFAULT_MODE, passive). --define(PROGRESS_DEFAULT, ignore). --define(FTP_EXT_DEFAULT, false). - -%% Internal Constants --define(FTP_PORT, 21). --define(FILE_BUFSIZE, 4096). - -%% Internal state --record(state, { - csock = undefined, % socket() - Control connection socket - dsock = undefined, % socket() - Data connection socket - tls_options = undefined, % list() - verbose = false, % boolean() - ldir = undefined, % string() - Current local directory - type = ftp_server_default, % atom() - binary | ascii - chunk = false, % boolean() - Receiving data chunks - mode = ?DEFAULT_MODE, % passive | active - timeout = ?CONNECTION_TIMEOUT, % integer() - %% Data received so far on the data connection - data = <<>>, % binary() - %% Data received so far on the control connection - %% {BinStream, AccLines}. If a binary sequence - %% ends with ?CR then keep it in the binary to - %% be able to detect if the next received byte is ?LF - %% and hence the end of the response is reached! - ctrl_data = {<<>>, [], start}, % {binary(), [bytes()], LineStatus} - %% pid() - Client pid (note not the same as "From") - latest_ctrl_response = "", - owner = undefined, - client = undefined, % "From" to be used in gen_server:reply/2 - %% Function that activated a connection and maybe some - %% data needed further on. - caller = undefined, % term() - ipfamily, % inet | inet6 | inet6fb4 - progress = ignore, % ignore | pid() - dtimeout = ?DATA_ACCEPT_TIMEOUT, % non_neg_integer() | infinity - tls_upgrading_data_connection = false, - ftp_extension = ?FTP_EXT_DEFAULT - }). - - --type shortage_reason() :: 'etnospc' | 'epnospc'. --type restriction_reason() :: 'epath' | 'efnamena' | 'elogin' | 'enotbinary'. --type common_reason() :: 'econn' | 'eclosed' | term(). --type file_write_error_reason() :: term(). % See file:write for more info - --define(DBG(F,A), 'n/a'). -%%-define(DBG(F,A), io:format(F,A)). - -%%%========================================================================= -%%% API - CLIENT FUNCTIONS -%%%========================================================================= - -%%-------------------------------------------------------------------------- -%% open(HostOrOtpList, <Port>, <Flags>) -> {ok, Pid} | {error, ehost} -%% HostOrOtpList = string() | [{option_list, Options}] -%% Port = integer(), -%% Flags = [Flag], -%% Flag = verbose | debug | trace -%% -%% Description: Start an ftp client and connect to a host. -%%-------------------------------------------------------------------------- - --spec open(Host :: string() | inet:ip_address()) -> - {'ok', Pid :: pid()} | {'error', Reason :: 'ehost' | term()}. - -%% <BACKWARD-COMPATIBILLITY> -open({option_list, Options}) when is_list(Options) -> - try - {ok, StartOptions} = start_options(Options), - {ok, OpenOptions} = open_options(Options), - case ftp_sup:start_child([[[{client, self()} | StartOptions], []]]) of - {ok, Pid} -> - call(Pid, {open, ip_comm, OpenOptions}, plain); - Error1 -> - Error1 - end - catch - throw:Error2 -> - Error2 - end; -%% </BACKWARD-COMPATIBILLITY> - -open(Host) -> - open(Host, []). - --spec open(Host :: string() | inet:ip_address(), Opts :: list()) -> - {'ok', Pid :: pid()} | {'error', Reason :: 'ehost' | term()}. - -%% <BACKWARD-COMPATIBILLITY> -open(Host, Port) when is_integer(Port) -> - open(Host, [{port, Port}]); -%% </BACKWARD-COMPATIBILLITY> - -open(Host, Opts) when is_list(Opts) -> - ?fcrt("open", [{host, Host}, {opts, Opts}]), - try - {ok, StartOptions} = start_options(Opts), - ?fcrt("open", [{start_options, StartOptions}]), - {ok, OpenOptions} = open_options([{host, Host}|Opts]), - ?fcrt("open", [{open_options, OpenOptions}]), - case start_link(StartOptions, []) of - {ok, Pid} -> - do_open(Pid, OpenOptions, tls_options(Opts)); - Error1 -> - ?fcrt("open - error", [{error1, Error1}]), - Error1 - end - catch - throw:Error2 -> - ?fcrt("open - error", [{error2, Error2}]), - Error2 - end. - -do_open(Pid, OpenOptions, TLSOpts) -> - case call(Pid, {open, ip_comm, OpenOptions}, plain) of - {ok, Pid} -> - maybe_tls_upgrade(Pid, TLSOpts); - Error -> - Error - end. -%%-------------------------------------------------------------------------- -%% user(Pid, User, Pass, <Acc>) -> ok | {error, euser} | {error, econn} -%% | {error, eacct} -%% Pid = pid(), -%% User = Pass = Acc = string() -%% -%% Description: Login with or without a supplied account name. -%%-------------------------------------------------------------------------- --spec user(Pid :: pid(), - User :: string(), - Pass :: string()) -> - 'ok' | {'error', Reason :: 'euser' | common_reason()}. - -user(Pid, User, Pass) -> - case {is_name_sane(User), is_name_sane(Pass)} of - {true, true} -> - call(Pid, {user, User, Pass}, atom); - _ -> - {error, euser} - end. - --spec user(Pid :: pid(), - User :: string(), - Pass :: string(), - Acc :: string()) -> - 'ok' | {'error', Reason :: 'euser' | common_reason()}. - -user(Pid, User, Pass, Acc) -> - case {is_name_sane(User), is_name_sane(Pass), is_name_sane(Acc)} of - {true, true, true} -> - call(Pid, {user, User, Pass, Acc}, atom); - _ -> - {error, euser} - end. - - -%%-------------------------------------------------------------------------- -%% account(Pid, Acc) -> ok | {error, eacct} -%% Pid = pid() -%% Acc= string() -%% -%% Description: Set a user Account. -%%-------------------------------------------------------------------------- - --spec account(Pid :: pid(), Acc :: string()) -> - 'ok' | {'error', Reason :: 'eacct' | common_reason()}. - -account(Pid, Acc) -> - case is_name_sane(Acc) of - true -> - call(Pid, {account, Acc}, atom); - _ -> - {error, eacct} - end. - - -%%-------------------------------------------------------------------------- -%% pwd(Pid) -> {ok, Dir} | {error, elogin} | {error, econn} -%% Pid = pid() -%% Dir = string() -%% -%% Description: Get the current working directory at remote server. -%%-------------------------------------------------------------------------- - --spec pwd(Pid :: pid()) -> - {'ok', Dir :: string()} | - {'error', Reason :: restriction_reason() | common_reason()}. - -pwd(Pid) -> - call(Pid, pwd, ctrl). - - -%%-------------------------------------------------------------------------- -%% lpwd(Pid) -> {ok, Dir} -%% Pid = pid() -%% Dir = string() -%% -%% Description: Get the current working directory at local server. -%%-------------------------------------------------------------------------- - --spec lpwd(Pid :: pid()) -> - {'ok', Dir :: string()}. - -lpwd(Pid) -> - call(Pid, lpwd, string). - - -%%-------------------------------------------------------------------------- -%% cd(Pid, Dir) -> ok | {error, epath} | {error, elogin} | {error, econn} -%% Pid = pid() -%% Dir = string() -%% -%% Description: Change current working directory at remote server. -%%-------------------------------------------------------------------------- - --spec cd(Pid :: pid(), Dir :: string()) -> - 'ok' | {'error', Reason :: restriction_reason() | common_reason()}. - -cd(Pid, Dir) -> - case is_name_sane(Dir) of - true -> - call(Pid, {cd, Dir}, atom); - _ -> - {error, efnamena} - end. - - -%%-------------------------------------------------------------------------- -%% lcd(Pid, Dir) -> ok | {error, epath} -%% Pid = pid() -%% Dir = string() -%% -%% Description: Change current working directory for the local client. -%%-------------------------------------------------------------------------- - --spec lcd(Pid :: pid(), Dir :: string()) -> - 'ok' | {'error', Reason :: restriction_reason()}. - -lcd(Pid, Dir) -> - call(Pid, {lcd, Dir}, string). - - -%%-------------------------------------------------------------------------- -%% ls(Pid) -> Result -%% ls(Pid, <Dir>) -> Result -%% -%% Pid = pid() -%% Dir = string() -%% Result = {ok, Listing} | {error, Reason} -%% Listing = string() -%% Reason = epath | elogin | econn -%% -%% Description: Returns a list of files in long format. -%%-------------------------------------------------------------------------- - --spec ls(Pid :: pid()) -> - {'ok', Listing :: string()} | - {'error', Reason :: restriction_reason() | common_reason()}. - -ls(Pid) -> - ls(Pid, ""). - --spec ls(Pid :: pid(), Dir :: string()) -> - {'ok', Listing :: string()} | - {'error', Reason :: restriction_reason() | common_reason()}. - -ls(Pid, Dir) -> - case is_name_sane(Dir) of - true -> - call(Pid, {dir, long, Dir}, string); - _ -> - {error, efnamena} - end. - - -%%-------------------------------------------------------------------------- -%% nlist(Pid) -> Result -%% nlist(Pid, Pathname) -> Result -%% -%% Pid = pid() -%% Pathname = string() -%% Result = {ok, Listing} | {error, Reason} -%% Listing = string() -%% Reason = epath | elogin | econn -%% -%% Description: Returns a list of files in short format -%%-------------------------------------------------------------------------- - --spec nlist(Pid :: pid()) -> - {'ok', Listing :: string()} | - {'error', Reason :: restriction_reason() | common_reason()}. - -nlist(Pid) -> - nlist(Pid, ""). - --spec nlist(Pid :: pid(), Pathname :: string()) -> - {'ok', Listing :: string()} | - {'error', Reason :: restriction_reason() | common_reason()}. - -nlist(Pid, Dir) -> - case is_name_sane(Dir) of - true -> - call(Pid, {dir, short, Dir}, string); - _ -> - {error, efnamena} - end. - - -%%-------------------------------------------------------------------------- -%% rename(Pid, Old, New) -> ok | {error, epath} | {error, elogin} -%% | {error, econn} -%% Pid = pid() -%% CurrFile = NewFile = string() -%% -%% Description: Rename a file at remote server. -%%-------------------------------------------------------------------------- - --spec rename(Pid :: pid(), Old :: string(), New :: string()) -> - 'ok' | {'error', Reason :: restriction_reason() | common_reason()}. - -rename(Pid, Old, New) -> - case {is_name_sane(Old), is_name_sane(New)} of - {true, true} -> - call(Pid, {rename, Old, New}, string); - _ -> - {error, efnamena} - end. - - -%%-------------------------------------------------------------------------- -%% delete(Pid, File) -> ok | {error, epath} | {error, elogin} | -%% {error, econn} -%% Pid = pid() -%% File = string() -%% -%% Description: Remove file at remote server. -%%-------------------------------------------------------------------------- - --spec delete(Pid :: pid(), File :: string()) -> - 'ok' | {'error', Reason :: restriction_reason() | common_reason()}. - -delete(Pid, File) -> - case is_name_sane(File) of - true -> - call(Pid, {delete, File}, string); - _ -> - {error, efnamena} - end. - - -%%-------------------------------------------------------------------------- -%% mkdir(Pid, Dir) -> ok | {error, epath} | {error, elogin} | {error, econn} -%% Pid = pid(), -%% Dir = string() -%% -%% Description: Make directory at remote server. -%%-------------------------------------------------------------------------- - --spec mkdir(Pid :: pid(), Dir :: string()) -> - 'ok' | {'error', Reason :: restriction_reason() | common_reason()}. - -mkdir(Pid, Dir) -> - case is_name_sane(Dir) of - true -> - call(Pid, {mkdir, Dir}, atom); - _ -> - {error, efnamena} - end. - - -%%-------------------------------------------------------------------------- -%% rmdir(Pid, Dir) -> ok | {error, epath} | {error, elogin} | {error, econn} -%% Pid = pid(), -%% Dir = string() -%% -%% Description: Remove directory at remote server. -%%-------------------------------------------------------------------------- - --spec rmdir(Pid :: pid(), Dir :: string()) -> - 'ok' | {'error', Reason :: restriction_reason() | common_reason()}. - -rmdir(Pid, Dir) -> - case is_name_sane(Dir) of - true -> - call(Pid, {rmdir, Dir}, atom); - _ -> - {error, efnamena} - end. - - -%%-------------------------------------------------------------------------- -%% type(Pid, Type) -> ok | {error, etype} | {error, elogin} | {error, econn} -%% Pid = pid() -%% Type = ascii | binary -%% -%% Description: Set transfer type. -%%-------------------------------------------------------------------------- - --spec type(Pid :: pid(), Type :: ascii | binary) -> - 'ok' | - {'error', Reason :: 'etype' | restriction_reason() | common_reason()}. - -type(Pid, Type) -> - call(Pid, {type, Type}, atom). - - -%%-------------------------------------------------------------------------- -%% recv(Pid, RemoteFileName [, LocalFileName]) -> ok | {error, epath} | -%% {error, elogin} | {error, econn} -%% Pid = pid() -%% RemoteFileName = LocalFileName = string() -%% -%% Description: Transfer file from remote server. -%%-------------------------------------------------------------------------- - --spec recv(Pid :: pid(), RemoteFileName :: string()) -> - 'ok' | {'error', Reason :: restriction_reason() | - common_reason() | - file_write_error_reason()}. - -recv(Pid, RemotFileName) -> - recv(Pid, RemotFileName, RemotFileName). - --spec recv(Pid :: pid(), - RemoteFileName :: string(), - LocalFileName :: string()) -> - 'ok' | {'error', Reason :: term()}. - -recv(Pid, RemotFileName, LocalFileName) -> - case is_name_sane(RemotFileName) of - true -> - call(Pid, {recv, RemotFileName, LocalFileName}, atom); - _ -> - {error, efnamena} - end. - - -%%-------------------------------------------------------------------------- -%% recv_bin(Pid, RemoteFile) -> {ok, Bin} | {error, epath} | {error, elogin} -%% | {error, econn} -%% Pid = pid() -%% RemoteFile = string() -%% Bin = binary() -%% -%% Description: Transfer file from remote server into binary. -%%-------------------------------------------------------------------------- - --spec recv_bin(Pid :: pid(), - RemoteFile :: string()) -> - {'ok', Bin :: binary()} | - {'error', Reason :: restriction_reason() | common_reason()}. - -recv_bin(Pid, RemoteFile) -> - case is_name_sane(RemoteFile) of - true -> - call(Pid, {recv_bin, RemoteFile}, bin); - _ -> - {error, efnamena} - end. - - -%%-------------------------------------------------------------------------- -%% recv_chunk_start(Pid, RemoteFile) -> ok | {error, elogin} | {error, epath} -%% | {error, econn} -%% Pid = pid() -%% RemoteFile = string() -%% -%% Description: Start receive of chunks of remote file. -%%-------------------------------------------------------------------------- - --spec recv_chunk_start(Pid :: pid(), - RemoteFile :: string()) -> - 'ok' | {'error', Reason :: restriction_reason() | common_reason()}. - -recv_chunk_start(Pid, RemoteFile) -> - case is_name_sane(RemoteFile) of - true -> - call(Pid, {recv_chunk_start, RemoteFile}, atom); - _ -> - {error, efnamena} - end. - - -%%-------------------------------------------------------------------------- -%% recv_chunk(Pid, RemoteFile) -> ok | {ok, Bin} | {error, Reason} -%% Pid = pid() -%% RemoteFile = string() -%% -%% Description: Transfer file from remote server into binary in chunks -%%-------------------------------------------------------------------------- - --spec recv_chunk(Pid :: pid()) -> - 'ok' | - {'ok', Bin :: binary()} | - {'error', Reason :: restriction_reason() | common_reason()}. - -recv_chunk(Pid) -> - call(Pid, recv_chunk, atom). - - -%%-------------------------------------------------------------------------- -%% send(Pid, LocalFileName [, RemotFileName]) -> ok | {error, epath} -%% | {error, elogin} -%% | {error, econn} -%% Pid = pid() -%% LocalFileName = RemotFileName = string() -%% -%% Description: Transfer file to remote server. -%%-------------------------------------------------------------------------- - --spec send(Pid :: pid(), LocalFileName :: string()) -> - 'ok' | - {'error', Reason :: restriction_reason() | - common_reason() | - shortage_reason()}. - -send(Pid, LocalFileName) -> - send(Pid, LocalFileName, LocalFileName). - --spec send(Pid :: pid(), - LocalFileName :: string(), - RemoteFileName :: string()) -> - 'ok' | - {'error', Reason :: restriction_reason() | - common_reason() | - shortage_reason()}. - -send(Pid, LocalFileName, RemotFileName) -> - case is_name_sane(RemotFileName) of - true -> - call(Pid, {send, LocalFileName, RemotFileName}, atom); - _ -> - {error, efnamena} - end. - - -%%-------------------------------------------------------------------------- -%% send_bin(Pid, Bin, RemoteFile) -> ok | {error, epath} | {error, elogin} -%% | {error, enotbinary} | {error, econn} -%% Pid = pid() -%% Bin = binary() -%% RemoteFile = string() -%% -%% Description: Transfer a binary to a remote file. -%%-------------------------------------------------------------------------- - --spec send_bin(Pid :: pid(), Bin :: binary(), RemoteFile :: string()) -> - 'ok' | - {'error', Reason :: restriction_reason() | - common_reason() | - shortage_reason()}. - -send_bin(Pid, Bin, RemoteFile) when is_binary(Bin) -> - case is_name_sane(RemoteFile) of - true -> - call(Pid, {send_bin, Bin, RemoteFile}, atom); - _ -> - {error, efnamena} - end; -send_bin(_Pid, _Bin, _RemoteFile) -> - {error, enotbinary}. - - -%%-------------------------------------------------------------------------- -%% send_chunk_start(Pid, RemoteFile) -> ok | {error, elogin} | {error, epath} -%% | {error, econn} -%% Pid = pid() -%% RemoteFile = string() -%% -%% Description: Start transfer of chunks to remote file. -%%-------------------------------------------------------------------------- - --spec send_chunk_start(Pid :: pid(), RemoteFile :: string()) -> - 'ok' | {'error', Reason :: restriction_reason() | common_reason()}. - -send_chunk_start(Pid, RemoteFile) -> - case is_name_sane(RemoteFile) of - true -> - call(Pid, {send_chunk_start, RemoteFile}, atom); - _ -> - {error, efnamena} - end. - - -%%-------------------------------------------------------------------------- -%% append_chunk_start(Pid, RemoteFile) -> ok | {error, elogin} | -%% {error, epath} | {error, econn} -%% Pid = pid() -%% RemoteFile = string() -%% -%% Description: Start append chunks of data to remote file. -%%-------------------------------------------------------------------------- - --spec append_chunk_start(Pid :: pid(), RemoteFile :: string()) -> - 'ok' | {'error', Reason :: term()}. - -append_chunk_start(Pid, RemoteFile) -> - case is_name_sane(RemoteFile) of - true -> - call(Pid, {append_chunk_start, RemoteFile}, atom); - _ -> - {error, efnamena} - end. - - -%%-------------------------------------------------------------------------- -%% send_chunk(Pid, Bin) -> ok | {error, elogin} | {error, enotbinary} -%% | {error, echunk} | {error, econn} -%% Pid = pid() -%% Bin = binary(). -%% -%% Purpose: Send chunk to remote file. -%%-------------------------------------------------------------------------- - --spec send_chunk(Pid :: pid(), Bin :: binary()) -> - 'ok' | - {'error', Reason :: 'echunk' | - restriction_reason() | - common_reason()}. - -send_chunk(Pid, Bin) when is_binary(Bin) -> - call(Pid, {transfer_chunk, Bin}, atom); -send_chunk(_Pid, _Bin) -> - {error, enotbinary}. - - -%%-------------------------------------------------------------------------- -%% append_chunk(Pid, Bin) -> ok | {error, elogin} | {error, enotbinary} -%% | {error, echunk} | {error, econn} -%% Pid = pid() -%% Bin = binary() -%% -%% Description: Append chunk to remote file. -%%-------------------------------------------------------------------------- - --spec append_chunk(Pid :: pid(), Bin :: binary()) -> - 'ok' | - {'error', Reason :: 'echunk' | - restriction_reason() | - common_reason()}. - -append_chunk(Pid, Bin) when is_binary(Bin) -> - call(Pid, {transfer_chunk, Bin}, atom); -append_chunk(_Pid, _Bin) -> - {error, enotbinary}. - - -%%-------------------------------------------------------------------------- -%% send_chunk_end(Pid) -> ok | {error, elogin} | {error, echunk} -%% | {error, econn} -%% Pid = pid() -%% -%% Description: End sending of chunks to remote file. -%%-------------------------------------------------------------------------- - --spec send_chunk_end(Pid :: pid()) -> - 'ok' | - {'error', Reason :: restriction_reason() | - common_reason() | - shortage_reason()}. - -send_chunk_end(Pid) -> - call(Pid, chunk_end, atom). - - -%%-------------------------------------------------------------------------- -%% append_chunk_end(Pid) -> ok | {error, elogin} | {error, echunk} -%% | {error, econn} -%% Pid = pid() -%% -%% Description: End appending of chunks to remote file. -%%-------------------------------------------------------------------------- - --spec append_chunk_end(Pid :: pid()) -> - 'ok' | - {'error', Reason :: restriction_reason() | - common_reason() | - shortage_reason()}. - -append_chunk_end(Pid) -> - call(Pid, chunk_end, atom). - - -%%-------------------------------------------------------------------------- -%% append(Pid, LocalFileName [, RemotFileName]) -> ok | {error, epath} -%% | {error, elogin} -%% | {error, econn} -%% Pid = pid() -%% LocalFileName = RemotFileName = string() -%% -%% Description: Append the local file to the remote file -%%-------------------------------------------------------------------------- - --spec append(Pid :: pid(), LocalFileName :: string()) -> - 'ok' | - {'error', Reason :: 'epath' | - 'elogin' | - 'etnospc' | - 'epnospc' | - 'efnamena' | common_reason()}. - -append(Pid, LocalFileName) -> - append(Pid, LocalFileName, LocalFileName). - --spec append(Pid :: pid(), - LocalFileName :: string(), - RemoteFileName :: string()) -> - 'ok' | {'error', Reason :: term()}. - -append(Pid, LocalFileName, RemotFileName) -> - case is_name_sane(RemotFileName) of - true -> - call(Pid, {append, LocalFileName, RemotFileName}, atom); - _ -> - {error, efnamena} - end. - - -%%-------------------------------------------------------------------------- -%% append_bin(Pid, Bin, RemoteFile) -> ok | {error, epath} | {error, elogin} -%% | {error, enotbinary} | {error, econn} -%% Pid = pid() -%% Bin = binary() -%% RemoteFile = string() -%% -%% Purpose: Append a binary to a remote file. -%%-------------------------------------------------------------------------- - --spec append_bin(Pid :: pid(), - Bin :: binary(), - RemoteFile :: string()) -> - 'ok' | - {'error', Reason :: restriction_reason() | - common_reason() | - shortage_reason()}. - -append_bin(Pid, Bin, RemoteFile) when is_binary(Bin) -> - case is_name_sane(RemoteFile) of - true -> - call(Pid, {append_bin, Bin, RemoteFile}, atom); - _ -> - {error, efnamena} - end; -append_bin(_Pid, _Bin, _RemoteFile) -> - {error, enotbinary}. - - -%%-------------------------------------------------------------------------- -%% quote(Pid, Cmd) -> list() -%% Pid = pid() -%% Cmd = string() -%% -%% Description: Send arbitrary ftp command. -%%-------------------------------------------------------------------------- - --spec quote(Pid :: pid(), Cmd :: string()) -> list(). - -quote(Pid, Cmd) when is_list(Cmd) -> - call(Pid, {quote, Cmd}, atom). - - -%%-------------------------------------------------------------------------- -%% close(Pid) -> ok -%% Pid = pid() -%% -%% Description: End the ftp session. -%%-------------------------------------------------------------------------- - --spec close(Pid :: pid()) -> 'ok'. - -close(Pid) -> - cast(Pid, close), - ok. - - -%%-------------------------------------------------------------------------- -%% formaterror(Tag) -> string() -%% Tag = atom() | {error, atom()} -%% -%% Description: Return diagnostics. -%%-------------------------------------------------------------------------- - --spec formaterror(Tag :: term()) -> string(). - -formaterror(Tag) -> - ftp_response:error_string(Tag). - - -info(Pid) -> - call(Pid, info, list). - - -%%-------------------------------------------------------------------------- -%% latest_ctrl_response(Pid) -> string() -%% Pid = pid() -%% -%% Description: The latest received response from the server -%%-------------------------------------------------------------------------- - --spec latest_ctrl_response(Pid :: pid()) -> string(). - -latest_ctrl_response(Pid) -> - call(Pid, latest_ctrl_response, string). - -%%%======================================================================== -%%% Behavior callbacks -%%%======================================================================== -start_standalone(Options) -> - try - {ok, StartOptions} = start_options(Options), - {ok, OpenOptions} = open_options(Options), - case start_link(StartOptions, []) of - {ok, Pid} -> - call(Pid, {open, ip_comm, OpenOptions}, plain); - Error1 -> - Error1 - end - catch - throw:Error2 -> - Error2 - end. - -start_service(Options) -> - try - {ok, StartOptions} = start_options(Options), - {ok, OpenOptions} = open_options(Options), - case ftp_sup:start_child([[[{client, self()} | StartOptions], []]]) of - {ok, Pid} -> - call(Pid, {open, ip_comm, OpenOptions}, plain); - Error1 -> - Error1 - end - catch - throw:Error2 -> - Error2 - end. - -stop_service(Pid) -> - close(Pid). - -services() -> - [{ftpc, Pid} || {_, Pid, _, _} <- - supervisor:which_children(ftp_sup)]. -service_info(Pid) -> - {ok, Info} = call(Pid, info, list), - {ok, [proplists:lookup(mode, Info), - proplists:lookup(local_port, Info), - proplists:lookup(peer, Info), - proplists:lookup(peer_port, Info)]}. - - -%% This function extracts the start options from the -%% Valid options: -%% debug, -%% verbose -%% ipfamily -%% priority -%% flags (for backward compatibillity) -start_options(Options) -> - ?fcrt("start_options", [{options, Options}]), - case lists:keysearch(flags, 1, Options) of - {value, {flags, Flags}} -> - Verbose = lists:member(verbose, Flags), - IsTrace = lists:member(trace, Flags), - IsDebug = lists:member(debug, Flags), - DebugLevel = - if - (IsTrace =:= true) -> - trace; - IsDebug =:= true -> - debug; - true -> - disable - end, - {ok, [{verbose, Verbose}, - {debug, DebugLevel}, - {priority, low}]}; - false -> - ValidateVerbose = - fun(true) -> true; - (false) -> true; - (_) -> false - end, - ValidateDebug = - fun(trace) -> true; - (debug) -> true; - (disable) -> true; - (_) -> false - end, - ValidatePriority = - fun(low) -> true; - (normal) -> true; - (high) -> true; - (_) -> false - end, - ValidOptions = - [{verbose, ValidateVerbose, false, false}, - {debug, ValidateDebug, false, disable}, - {priority, ValidatePriority, false, low}], - validate_options(Options, ValidOptions, []) - end. - - -%% This function extracts and validates the open options from the -%% Valid options: -%% mode -%% host -%% port -%% timeout -%% dtimeout -%% progress -%% ftp_extension - -open_options(Options) -> - ?fcrt("open_options", [{options, Options}]), - ValidateMode = - fun(active) -> true; - (passive) -> true; - (_) -> false - end, - ValidateHost = - fun(Host) when is_list(Host) -> - true; - (Host) when is_tuple(Host) andalso - ((size(Host) =:= 4) orelse (size(Host) =:= 8)) -> - true; - (_) -> - false - end, - ValidatePort = - fun(Port) when is_integer(Port) andalso (Port > 0) -> true; - (_) -> false - end, - ValidateIpFamily = - fun(inet) -> true; - (inet6) -> true; - (inet6fb4) -> true; - (_) -> false - end, - ValidateTimeout = - fun(Timeout) when is_integer(Timeout) andalso (Timeout >= 0) -> true; - (_) -> false - end, - ValidateDTimeout = - fun(DTimeout) when is_integer(DTimeout) andalso (DTimeout >= 0) -> true; - (infinity) -> true; - (_) -> false - end, - ValidateProgress = - fun(ignore) -> - true; - ({Mod, Func, _InitProgress}) when is_atom(Mod) andalso - is_atom(Func) -> - true; - (_) -> - false - end, - ValidateFtpExtension = - fun(true) -> true; - (false) -> true; - (_) -> false - end, - ValidOptions = - [{mode, ValidateMode, false, ?DEFAULT_MODE}, - {host, ValidateHost, true, ehost}, - {port, ValidatePort, false, ?FTP_PORT}, - {ipfamily, ValidateIpFamily, false, inet}, - {timeout, ValidateTimeout, false, ?CONNECTION_TIMEOUT}, - {dtimeout, ValidateDTimeout, false, ?DATA_ACCEPT_TIMEOUT}, - {progress, ValidateProgress, false, ?PROGRESS_DEFAULT}, - {ftp_extension, ValidateFtpExtension, false, ?FTP_EXT_DEFAULT}], - validate_options(Options, ValidOptions, []). - -tls_options(Options) -> - %% Options will be validated by ssl application - proplists:get_value(tls, Options, undefined). - -validate_options([], [], Acc) -> - ?fcrt("validate_options -> done", [{acc, Acc}]), - {ok, lists:reverse(Acc)}; -validate_options([], ValidOptions, Acc) -> - ?fcrt("validate_options -> done", - [{valid_options, ValidOptions}, {acc, Acc}]), - %% Check if any mandatory options are missing! - case [{Key, Reason} || {Key, _, true, Reason} <- ValidOptions] of - [] -> - Defaults = - [{Key, Default} || {Key, _, _, Default} <- ValidOptions], - {ok, lists:reverse(Defaults ++ Acc)}; - [{_, Reason}|_Missing] -> - throw({error, Reason}) - end; -validate_options([{Key, Value}|Options], ValidOptions, Acc) -> - ?fcrt("validate_options -> check", - [{key, Key}, {value, Value}, {acc, Acc}]), - case lists:keysearch(Key, 1, ValidOptions) of - {value, {Key, Validate, _, Default}} -> - case (catch Validate(Value)) of - true -> - ?fcrt("validate_options -> check - accept", []), - NewValidOptions = lists:keydelete(Key, 1, ValidOptions), - validate_options(Options, NewValidOptions, - [{Key, Value} | Acc]); - _ -> - ?fcrt("validate_options -> check - reject", - [{default, Default}]), - NewValidOptions = lists:keydelete(Key, 1, ValidOptions), - validate_options(Options, NewValidOptions, - [{Key, Default} | Acc]) - end; - false -> - validate_options(Options, ValidOptions, Acc) - end; -validate_options([_|Options], ValidOptions, Acc) -> - validate_options(Options, ValidOptions, Acc). - - - -%%%======================================================================== -%%% gen_server callback functions -%%%======================================================================== - -%%------------------------------------------------------------------------- -%% init(Args) -> {ok, State} | {ok, State, Timeout} | {stop, Reason} -%% Description: Initiates the erlang process that manages a ftp connection. -%%------------------------------------------------------------------------- -init(Options) -> - process_flag(trap_exit, true), - - %% Keep track of the client - {value, {client, Client}} = lists:keysearch(client, 1, Options), - erlang:monitor(process, Client), - - %% Make sure inet is started - inet_db:start(), - - %% Where are we - {ok, Dir} = file:get_cwd(), - - %% Maybe activate dbg - case key_search(debug, Options, disable) of - trace -> - dbg:tracer(), - dbg:p(all, [call]), - dbg:tpl(ftp, [{'_', [], [{return_trace}]}]), - dbg:tpl(ftp_response, [{'_', [], [{return_trace}]}]), - dbg:tpl(ftp_progress, [{'_', [], [{return_trace}]}]); - debug -> - dbg:tracer(), - dbg:p(all, [call]), - dbg:tp(ftp, [{'_', [], [{return_trace}]}]), - dbg:tp(ftp_response, [{'_', [], [{return_trace}]}]), - dbg:tp(ftp_progress, [{'_', [], [{return_trace}]}]); - _ -> - %% Keep silent - ok - end, - - %% Verbose? - Verbose = key_search(verbose, Options, false), - - %% IpFamily? - IpFamily = key_search(ipfamily, Options, inet), - - State = #state{owner = Client, - verbose = Verbose, - ipfamily = IpFamily, - ldir = Dir}, - - %% Set process prio - Priority = key_search(priority, Options, low), - process_flag(priority, Priority), - - %% And we are done - {ok, State}. - - -%%-------------------------------------------------------------------------- -%% handle_call(Request, From, State) -> {reply, Reply, State} | -%% {reply, Reply, State, Timeout} | -%% {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, Reply, State} | -%% Description: Handle incoming requests. -%%------------------------------------------------------------------------- - -%% Anyone can ask this question -handle_call({_, info}, _, #state{verbose = Verbose, - mode = Mode, - timeout = Timeout, - ipfamily = IpFamily, - csock = Socket, - progress = Progress} = State) -> - {ok, {_, LocalPort}} = sockname(Socket), - {ok, {Address, Port}} = peername(Socket), - Options = [{verbose, Verbose}, - {ipfamily, IpFamily}, - {mode, Mode}, - {peer, Address}, - {peer_port, Port}, - {local_port, LocalPort}, - {timeout, Timeout}, - {progress, Progress}], - {reply, {ok, Options}, State}; - -handle_call({_,latest_ctrl_response}, _, #state{latest_ctrl_response=Resp} = State) -> - {reply, {ok,Resp}, State}; - -%% But everything else must come from the owner -handle_call({Pid, _}, _, #state{owner = Owner} = State) when Owner =/= Pid -> - {reply, {error, not_connection_owner}, State}; - -handle_call({_, {open, ip_comm, Opts}}, From, State) -> - ?fcrd("handle_call(open)", [{opts, Opts}]), - case key_search(host, Opts, undefined) of - undefined -> - {stop, normal, {error, ehost}, State}; - Host -> - Mode = key_search(mode, Opts, ?DEFAULT_MODE), - Port = key_search(port, Opts, ?FTP_PORT), - Timeout = key_search(timeout, Opts, ?CONNECTION_TIMEOUT), - DTimeout = key_search(dtimeout, Opts, ?DATA_ACCEPT_TIMEOUT), - Progress = key_search(progress, Opts, ignore), - IpFamily = key_search(ipfamily, Opts, inet), - FtpExt = key_search(ftp_extension, Opts, ?FTP_EXT_DEFAULT), - - State2 = State#state{client = From, - mode = Mode, - progress = progress(Progress), - ipfamily = IpFamily, - dtimeout = DTimeout, - ftp_extension = FtpExt}, - - ?fcrd("handle_call(open) -> setup ctrl connection with", - [{host, Host}, {port, Port}, {timeout, Timeout}]), - case setup_ctrl_connection(Host, Port, Timeout, State2) of - {ok, State3, WaitTimeout} -> - ?fcrd("handle_call(open) -> ctrl connection setup done", - [{waittimeout, WaitTimeout}]), - {noreply, State3, WaitTimeout}; - {error, Reason} -> - ?fcrd("handle_call(open) -> ctrl connection setup failed", - [{reason, Reason}]), - gen_server:reply(From, {error, ehost}), - {stop, normal, State2#state{client = undefined}} - end - end; - -handle_call({_, {open, ip_comm, Host, Opts}}, From, State) -> - Mode = key_search(mode, Opts, ?DEFAULT_MODE), - Port = key_search(port, Opts, ?FTP_PORT), - Timeout = key_search(timeout, Opts, ?CONNECTION_TIMEOUT), - DTimeout = key_search(dtimeout, Opts, ?DATA_ACCEPT_TIMEOUT), - Progress = key_search(progress, Opts, ignore), - FtpExt = key_search(ftp_extension, Opts, ?FTP_EXT_DEFAULT), - - State2 = State#state{client = From, - mode = Mode, - progress = progress(Progress), - dtimeout = DTimeout, - ftp_extension = FtpExt}, - - case setup_ctrl_connection(Host, Port, Timeout, State2) of - {ok, State3, WaitTimeout} -> - {noreply, State3, WaitTimeout}; - {error, _Reason} -> - gen_server:reply(From, {error, ehost}), - {stop, normal, State2#state{client = undefined}} - end; - -handle_call({_, {open, tls_upgrade, TLSOptions}}, From, State) -> - send_ctrl_message(State, mk_cmd("AUTH TLS", [])), - activate_ctrl_connection(State), - {noreply, State#state{client = From, caller = open, tls_options = TLSOptions}}; - -handle_call({_, {user, User, Password}}, From, - #state{csock = CSock} = State) when (CSock =/= undefined) -> - handle_user(User, Password, "", State#state{client = From}); - -handle_call({_, {user, User, Password, Acc}}, From, - #state{csock = CSock} = State) when (CSock =/= undefined) -> - handle_user(User, Password, Acc, State#state{client = From}); - -handle_call({_, {account, Acc}}, From, State)-> - handle_user_account(Acc, State#state{client = From}); - -handle_call({_, pwd}, From, #state{chunk = false} = State) -> - send_ctrl_message(State, mk_cmd("PWD", [])), - activate_ctrl_connection(State), - {noreply, State#state{client = From, caller = pwd}}; - -handle_call({_, lpwd}, From, #state{ldir = LDir} = State) -> - {reply, {ok, LDir}, State#state{client = From}}; - -handle_call({_, {cd, Dir}}, From, #state{chunk = false} = State) -> - send_ctrl_message(State, mk_cmd("CWD ~s", [Dir])), - activate_ctrl_connection(State), - {noreply, State#state{client = From, caller = cd}}; - -handle_call({_,{lcd, Dir}}, _From, #state{ldir = LDir0} = State) -> - LDir = filename:absname(Dir, LDir0), - case file:read_file_info(LDir) of %% FIX better check that LDir is a dir. - {ok, _ } -> - {reply, ok, State#state{ldir = LDir}}; - _ -> - {reply, {error, epath}, State} - end; - -handle_call({_, {dir, Len, Dir}}, {_Pid, _} = From, - #state{chunk = false} = State) -> - setup_data_connection(State#state{caller = {dir, Dir, Len}, - client = From}); -handle_call({_, {rename, CurrFile, NewFile}}, From, - #state{chunk = false} = State) -> - send_ctrl_message(State, mk_cmd("RNFR ~s", [CurrFile])), - activate_ctrl_connection(State), - {noreply, State#state{caller = {rename, NewFile}, client = From}}; - -handle_call({_, {delete, File}}, {_Pid, _} = From, - #state{chunk = false} = State) -> - send_ctrl_message(State, mk_cmd("DELE ~s", [File])), - activate_ctrl_connection(State), - {noreply, State#state{client = From}}; - -handle_call({_, {mkdir, Dir}}, From, #state{chunk = false} = State) -> - send_ctrl_message(State, mk_cmd("MKD ~s", [Dir])), - activate_ctrl_connection(State), - {noreply, State#state{client = From}}; - -handle_call({_,{rmdir, Dir}}, From, #state{chunk = false} = State) -> - send_ctrl_message(State, mk_cmd("RMD ~s", [Dir])), - activate_ctrl_connection(State), - {noreply, State#state{client = From}}; - -handle_call({_,{type, Type}}, From, #state{chunk = false} - = State) -> - case Type of - ascii -> - send_ctrl_message(State, mk_cmd("TYPE A", [])), - activate_ctrl_connection(State), - {noreply, State#state{caller = type, type = ascii, - client = From}}; - binary -> - send_ctrl_message(State, mk_cmd("TYPE I", [])), - activate_ctrl_connection(State), - {noreply, State#state{caller = type, type = binary, - client = From}}; - _ -> - {reply, {error, etype}, State} - end; - -handle_call({_,{recv, RemoteFile, LocalFile}}, From, - #state{chunk = false, ldir = LocalDir} = State) -> - progress_report({remote_file, RemoteFile}, State), - NewLocalFile = filename:absname(LocalFile, LocalDir), - - case file_open(NewLocalFile, write) of - {ok, Fd} -> - setup_data_connection(State#state{client = From, - caller = - {recv_file, - RemoteFile, Fd}}); - {error, _What} -> - {reply, {error, epath}, State} - end; - -handle_call({_, {recv_bin, RemoteFile}}, From, #state{chunk = false} = - State) -> - setup_data_connection(State#state{caller = {recv_bin, RemoteFile}, - client = From}); - -handle_call({_,{recv_chunk_start, RemoteFile}}, From, #state{chunk = false} - = State) -> - setup_data_connection(State#state{caller = {start_chunk_transfer, - "RETR", RemoteFile}, - client = From}); - -handle_call({_, recv_chunk}, _, #state{chunk = false} = State) -> - {reply, {error, "ftp:recv_chunk_start/2 not called"}, State}; - -handle_call({_, recv_chunk}, From, #state{chunk = true} = State0) -> - State = activate_data_connection(State0), - {noreply, State#state{client = From, caller = recv_chunk}}; - -handle_call({_, {send, LocalFile, RemoteFile}}, From, - #state{chunk = false, ldir = LocalDir} = State) -> - progress_report({local_file, filename:absname(LocalFile, LocalDir)}, - State), - setup_data_connection(State#state{caller = {transfer_file, - {"STOR", - LocalFile, RemoteFile}}, - client = From}); -handle_call({_, {append, LocalFile, RemoteFile}}, From, - #state{chunk = false} = State) -> - setup_data_connection(State#state{caller = {transfer_file, - {"APPE", - LocalFile, RemoteFile}}, - client = From}); -handle_call({_, {send_bin, Bin, RemoteFile}}, From, - #state{chunk = false} = State) -> - setup_data_connection(State#state{caller = {transfer_data, - {"STOR", Bin, RemoteFile}}, - client = From}); -handle_call({_,{append_bin, Bin, RemoteFile}}, From, - #state{chunk = false} = State) -> - setup_data_connection(State#state{caller = {transfer_data, - {"APPE", Bin, RemoteFile}}, - client = From}); -handle_call({_, {send_chunk_start, RemoteFile}}, From, #state{chunk = false} - = State) -> - setup_data_connection(State#state{caller = {start_chunk_transfer, - "STOR", RemoteFile}, - client = From}); -handle_call({_, {append_chunk_start, RemoteFile}}, From, #state{chunk = false} - = State) -> - setup_data_connection(State#state{caller = {start_chunk_transfer, - "APPE", RemoteFile}, - client = From}); -handle_call({_, {transfer_chunk, Bin}}, _, #state{chunk = true} = State) -> - send_data_message(State, Bin), - {reply, ok, State}; - -handle_call({_, chunk_end}, From, #state{chunk = true} = State) -> - close_data_connection(State), - activate_ctrl_connection(State), - {noreply, State#state{client = From, dsock = undefined, - caller = end_chunk_transfer, chunk = false}}; - -handle_call({_, {quote, Cmd}}, From, #state{chunk = false} = State) -> - send_ctrl_message(State, mk_cmd(Cmd, [])), - activate_ctrl_connection(State), - {noreply, State#state{client = From, caller = quote}}; - -handle_call({_, _Req}, _From, #state{csock = CSock} = State) - when (CSock =:= undefined) -> - {reply, {error, not_connected}, State}; - -handle_call(_, _, #state{chunk = true} = State) -> - {reply, {error, echunk}, State}; - -%% Catch all - This can only happen if the application programmer writes -%% really bad code that violates the API. -handle_call(Request, _Timeout, State) -> - {stop, {'API_violation_connection_closed', Request}, - {error, {connection_terminated, 'API_violation'}}, State}. - -%%-------------------------------------------------------------------------- -%% handle_cast(Request, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handles cast messages. -%%------------------------------------------------------------------------- -handle_cast({Pid, close}, #state{owner = Pid} = State) -> - send_ctrl_message(State, mk_cmd("QUIT", [])), - close_ctrl_connection(State), - close_data_connection(State), - {stop, normal, State#state{csock = undefined, dsock = undefined}}; - -handle_cast({Pid, close}, State) -> - Report = io_lib:format("A none owner process ~p tried to close an " - "ftp connection: ~n", [Pid]), - error_logger:info_report(Report), - {noreply, State}; - -%% Catch all - This can oly happen if the application programmer writes -%% really bad code that violates the API. -handle_cast(Msg, State) -> - {stop, {'API_violation_connection_closed', Msg}, State}. - -%%-------------------------------------------------------------------------- -%% handle_info(Msg, State) -> {noreply, State} | {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handles tcp messages from the ftp-server. -%% Note: The order of the function clauses is significant. -%%-------------------------------------------------------------------------- - -handle_info(timeout, #state{caller = open} = State) -> - {stop, timeout, State}; - -handle_info(timeout, State) -> - {noreply, State}; - -%%% Data socket messages %%% -handle_info({Trpt, Socket, Data}, - #state{dsock = {Trpt,Socket}, - caller = {recv_file, Fd}} = State0) when Trpt==tcp;Trpt==ssl -> - ?DBG('L~p --data ~p ----> ~s~p~n',[?LINE,Socket,Data,State0]), - file_write(binary_to_list(Data), Fd), - progress_report({binary, Data}, State0), - State = activate_data_connection(State0), - {noreply, State}; - -handle_info({Trpt, Socket, Data}, #state{dsock = {Trpt,Socket}, client = From, - caller = recv_chunk} - = State) when Trpt==tcp;Trpt==ssl -> - ?DBG('L~p --data ~p ----> ~s~p~n',[?LINE,Socket,Data,State]), - gen_server:reply(From, {ok, Data}), - {noreply, State#state{client = undefined, data = <<>>}}; - -handle_info({Trpt, Socket, Data}, #state{dsock = {Trpt,Socket}} = State0) when Trpt==tcp;Trpt==ssl -> - ?DBG('L~p --data ~p ----> ~s~p~n',[?LINE,Socket,Data,State0]), - State = activate_data_connection(State0), - {noreply, State#state{data = <<(State#state.data)/binary, - Data/binary>>}}; - -handle_info({Cls, Socket}, #state{dsock = {Trpt,Socket}, - caller = {recv_file, Fd}} - = State) when {Cls,Trpt}=={tcp_closed,tcp} ; {Cls,Trpt}=={ssl_closed,ssl} -> - file_close(Fd), - progress_report({transfer_size, 0}, State), - activate_ctrl_connection(State), - {noreply, State#state{dsock = undefined, data = <<>>}}; - -handle_info({Cls, Socket}, #state{dsock = {Trpt,Socket}, client = From, - caller = recv_chunk} - = State) when {Cls,Trpt}=={tcp_closed,tcp} ; {Cls,Trpt}=={ssl_closed,ssl} -> - gen_server:reply(From, ok), - {noreply, State#state{dsock = undefined, client = undefined, - data = <<>>, caller = undefined, - chunk = false}}; - -handle_info({Cls, Socket}, #state{dsock = {Trpt,Socket}, caller = recv_bin, - data = Data} = State) - when {Cls,Trpt}=={tcp_closed,tcp} ; {Cls,Trpt}=={ssl_closed,ssl} -> - activate_ctrl_connection(State), - {noreply, State#state{dsock = undefined, data = <<>>, - caller = {recv_bin, Data}}}; - -handle_info({Cls, Socket}, #state{dsock = {Trpt,Socket}, data = Data, - caller = {handle_dir_result, Dir}} - = State) when {Cls,Trpt}=={tcp_closed,tcp} ; {Cls,Trpt}=={ssl_closed,ssl} -> - activate_ctrl_connection(State), - {noreply, State#state{dsock = undefined, - caller = {handle_dir_result, Dir, Data}, -% data = <<?CR,?LF>>}}; - data = <<>>}}; - -handle_info({Err, Socket, Reason}, #state{dsock = {Trpt,Socket}, - client = From} = State) - when {Err,Trpt}=={tcp_error,tcp} ; {Err,Trpt}=={ssl_error,ssl} -> - gen_server:reply(From, {error, Reason}), - close_data_connection(State), - {noreply, State#state{dsock = undefined, client = undefined, - data = <<>>, caller = undefined, chunk = false}}; - -%%% Ctrl socket messages %%% -handle_info({Transport, Socket, Data}, #state{csock = {Transport, Socket}, - verbose = Verbose, - caller = Caller, - client = From, - ctrl_data = {CtrlData, AccLines, - LineStatus}} - = State) -> - ?DBG('--ctrl ~p ----> ~s~p~n',[Socket,<<CtrlData/binary, Data/binary>>,State]), - case ftp_response:parse_lines(<<CtrlData/binary, Data/binary>>, - AccLines, LineStatus) of - {ok, Lines, NextMsgData} -> - verbose(Lines, Verbose, 'receive'), - CtrlResult = ftp_response:interpret(Lines), - case Caller of - quote -> - gen_server:reply(From, string:tokens(Lines, [?CR, ?LF])), - {noreply, State#state{client = undefined, - caller = undefined, - latest_ctrl_response = Lines, - ctrl_data = {NextMsgData, [], - start}}}; - _ -> - ?DBG(' ...handle_ctrl_result(~p,...) ctrl_data=~p~n',[CtrlResult,{NextMsgData, [], start}]), - handle_ctrl_result(CtrlResult, - State#state{latest_ctrl_response = Lines, - ctrl_data = - {NextMsgData, [], start}}) - end; - {continue, NewCtrlData} -> - ?DBG(' ...Continue... ctrl_data=~p~n',[NewCtrlData]), - activate_ctrl_connection(State), - {noreply, State#state{ctrl_data = NewCtrlData}} - end; - -%% If the server closes the control channel it is -%% the expected behavior that connection process terminates. -handle_info({Cls, Socket}, #state{csock = {Trpt, Socket}}) - when {Cls,Trpt}=={tcp_closed,tcp} ; {Cls,Trpt}=={ssl_closed,ssl} -> - exit(normal); %% User will get error message from terminate/2 - -handle_info({Err, Socket, Reason}, _) when Err==tcp_error ; Err==ssl_error -> - Report = - io_lib:format("~p on socket: ~p for reason: ~p~n", - [Err, Socket, Reason]), - error_logger:error_report(Report), - %% If tcp does not work the only option is to terminate, - %% this is the expected behavior under these circumstances. - exit(normal); %% User will get error message from terminate/2 - -%% Monitor messages - if the process owning the ftp connection goes -%% down there is no point in continuing. -handle_info({'DOWN', _Ref, _Type, _Process, normal}, State) -> - {stop, normal, State#state{client = undefined}}; - -handle_info({'DOWN', _Ref, _Type, _Process, shutdown}, State) -> - {stop, normal, State#state{client = undefined}}; - -handle_info({'DOWN', _Ref, _Type, _Process, timeout}, State) -> - {stop, normal, State#state{client = undefined}}; - -handle_info({'DOWN', _Ref, _Type, Process, Reason}, State) -> - {stop, {stopped, {'EXIT', Process, Reason}}, - State#state{client = undefined}}; - -handle_info({'EXIT', Pid, Reason}, #state{progress = Pid} = State) -> - Report = io_lib:format("Progress reporting stopped for reason ~p~n", - [Reason]), - error_logger:info_report(Report), - {noreply, State#state{progress = ignore}}; - -%% Catch all - throws away unknown messages (This could happen by "accident" -%% so we do not want to crash, but we make a log entry as it is an -%% unwanted behaviour.) -handle_info(Info, State) -> - Report = io_lib:format("ftp : ~p : Unexpected message: ~p~nState: ~p~n", - [self(), Info, State]), - error_logger:info_report(Report), - {noreply, State}. - -%%-------------------------------------------------------------------------- -%% terminate/2 and code_change/3 -%%-------------------------------------------------------------------------- -terminate(normal, State) -> - %% If terminate reason =/= normal the progress reporting process will - %% be killed by the exit signal. - progress_report(stop, State), - do_termiante({error, econn}, State); -terminate(Reason, State) -> - Report = io_lib:format("Ftp connection closed due to: ~p~n", [Reason]), - error_logger:error_report(Report), - do_termiante({error, eclosed}, State). - -do_termiante(ErrorMsg, State) -> - close_data_connection(State), - close_ctrl_connection(State), - case State#state.client of - undefined -> - ok; - From -> - gen_server:reply(From, ErrorMsg) - end, - ok. - -code_change(_Vsn, State1, upgrade_from_pre_5_12) -> - {state, CSock, DSock, Verbose, LDir, Type, Chunk, Mode, Timeout, - Data, CtrlData, Owner, Client, Caller, IPv6Disable, Progress} = State1, - IpFamily = - if - (IPv6Disable =:= true) -> - inet; - true -> - inet6fb4 - end, - State2 = #state{csock = CSock, - dsock = DSock, - verbose = Verbose, - ldir = LDir, - type = Type, - chunk = Chunk, - mode = Mode, - timeout = Timeout, - data = Data, - ctrl_data = CtrlData, - owner = Owner, - client = Client, - caller = Caller, - ipfamily = IpFamily, - progress = Progress}, - {ok, State2}; - -code_change(_Vsn, State1, downgrade_to_pre_5_12) -> - #state{csock = CSock, - dsock = DSock, - verbose = Verbose, - ldir = LDir, - type = Type, - chunk = Chunk, - mode = Mode, - timeout = Timeout, - data = Data, - ctrl_data = CtrlData, - owner = Owner, - client = Client, - caller = Caller, - ipfamily = IpFamily, - progress = Progress} = State1, - IPv6Disable = - if - (IpFamily =:= inet) -> - true; - true -> - false - end, - State2 = - {state, CSock, DSock, Verbose, LDir, Type, Chunk, Mode, Timeout, - Data, CtrlData, Owner, Client, Caller, IPv6Disable, Progress}, - {ok, State2}; - -code_change(_Vsn, State, _Extra) -> - {ok, State}. - - -%%%========================================================================= -%% Start/stop -%%%========================================================================= -%%-------------------------------------------------------------------------- -%% start_link([Opts, GenServerOptions]) -> {ok, Pid} | {error, Reason} -%% -%% Description: Callback function for the ftp supervisor. It is called -%% : when start_service/1 calls ftp_sup:start_child/1 to start an -%% : instance of the ftp process. Also called by start_standalone/1 -%%-------------------------------------------------------------------------- -start_link([Opts, GenServerOptions]) -> - start_link(Opts, GenServerOptions). - -start_link(Opts, GenServerOptions) -> - case lists:keysearch(client, 1, Opts) of - {value, _} -> - %% Via the supervisor - gen_server:start_link(?MODULE, Opts, GenServerOptions); - false -> - Opts2 = [{client, self()} | Opts], - gen_server:start_link(?MODULE, Opts2, GenServerOptions) - end. - - -%%% Stop functionality is handled by close/1 - -%%%======================================================================== -%%% Internal functions -%%%======================================================================== - -%%-------------------------------------------------------------------------- -%%% Help functions to handle_call and/or handle_ctrl_result -%%-------------------------------------------------------------------------- -%% User handling -handle_user(User, Password, Acc, State) -> - send_ctrl_message(State, mk_cmd("USER ~s", [User])), - activate_ctrl_connection(State), - {noreply, State#state{caller = {handle_user, Password, Acc}}}. - -handle_user_passwd(Password, Acc, State) -> - send_ctrl_message(State, mk_cmd("PASS ~s", [Password])), - activate_ctrl_connection(State), - {noreply, State#state{caller = {handle_user_passwd, Acc}}}. - -handle_user_account(Acc, State) -> - send_ctrl_message(State, mk_cmd("ACCT ~s", [Acc])), - activate_ctrl_connection(State), - {noreply, State#state{caller = handle_user_account}}. - - -%%-------------------------------------------------------------------------- -%% handle_ctrl_result -%%-------------------------------------------------------------------------- -handle_ctrl_result({tls_upgrade, _}, #state{csock = {tcp, Socket}, - tls_options = TLSOptions, - timeout = Timeout, - caller = open, client = From} - = State0) -> - ?DBG('<--ctrl ssl:connect(~p, ~p)~n~p~n',[Socket,TLSOptions,State0]), - case ssl:connect(Socket, TLSOptions, Timeout) of - {ok, TLSSocket} -> - State = State0#state{csock = {ssl,TLSSocket}}, - send_ctrl_message(State, mk_cmd("PBSZ 0", [])), - activate_ctrl_connection(State), - {noreply, State#state{tls_upgrading_data_connection = {true, pbsz}} }; - {error, _} = Error -> - gen_server:reply(From, {Error, self()}), - {stop, normal, State0#state{client = undefined, - caller = undefined, - tls_upgrading_data_connection = false}} - end; - -handle_ctrl_result({pos_compl, _}, #state{tls_upgrading_data_connection = {true, pbsz}} = State) -> - send_ctrl_message(State, mk_cmd("PROT P", [])), - activate_ctrl_connection(State), - {noreply, State#state{tls_upgrading_data_connection = {true, prot}}}; - -handle_ctrl_result({pos_compl, _}, #state{tls_upgrading_data_connection = {true, prot}, - client = From} = State) -> - gen_server:reply(From, {ok, self()}), - {noreply, State#state{client = undefined, - caller = undefined, - tls_upgrading_data_connection = false}}; - -handle_ctrl_result({pos_compl, _}, #state{caller = open, client = From} - = State) -> - gen_server:reply(From, {ok, self()}), - {noreply, State#state{client = undefined, - caller = undefined }}; -handle_ctrl_result({_, Lines}, #state{caller = open} = State) -> - ctrl_result_response(econn, State, {error, Lines}); - -%%-------------------------------------------------------------------------- -%% Data connection setup active mode -handle_ctrl_result({pos_compl, _Lines}, - #state{mode = active, - caller = {setup_data_connection, - {LSock, Caller}}} = State) -> - handle_caller(State#state{caller = Caller, dsock = {lsock, LSock}}); - -handle_ctrl_result({Status, Lines}, - #state{mode = active, - caller = {setup_data_connection, {LSock, _}}} - = State) -> - close_connection(LSock), - ctrl_result_response(Status, State, {error, Lines}); - -%% Data connection setup passive mode -handle_ctrl_result({pos_compl, Lines}, - #state{mode = passive, - ipfamily = inet6, - client = From, - caller = {setup_data_connection, Caller}, - csock = CSock, - timeout = Timeout} - = State) -> - [_, PortStr | _] = lists:reverse(string:tokens(Lines, "|")), - {ok, {IP, _}} = peername(CSock), - case connect(IP, list_to_integer(PortStr), Timeout, State) of - {ok, _, Socket} -> - handle_caller(State#state{caller = Caller, dsock = {tcp, Socket}}); - {error, _Reason} = Error -> - gen_server:reply(From, Error), - {noreply, State#state{client = undefined, caller = undefined}} - end; - -handle_ctrl_result({pos_compl, Lines}, - #state{mode = passive, - ipfamily = inet, - client = From, - caller = {setup_data_connection, Caller}, - timeout = Timeout, - ftp_extension = false} = State) -> - - {_, [?LEFT_PAREN | Rest]} = - lists:splitwith(fun(?LEFT_PAREN) -> false; (_) -> true end, Lines), - {NewPortAddr, _} = - lists:splitwith(fun(?RIGHT_PAREN) -> false; (_) -> true end, Rest), - [A1, A2, A3, A4, P1, P2] = - lists:map(fun(X) -> list_to_integer(X) end, - string:tokens(NewPortAddr, [$,])), - IP = {A1, A2, A3, A4}, - Port = (P1 * 256) + P2, - - ?DBG('<--data tcp connect to ~p:~p, Caller=~p~n',[IP,Port,Caller]), - case connect(IP, Port, Timeout, State) of - {ok, _, Socket} -> - handle_caller(State#state{caller = Caller, dsock = {tcp,Socket}}); - {error, _Reason} = Error -> - gen_server:reply(From, Error), - {noreply,State#state{client = undefined, caller = undefined}} - end; - -handle_ctrl_result({pos_compl, Lines}, - #state{mode = passive, - ipfamily = inet, - client = From, - caller = {setup_data_connection, Caller}, - csock = CSock, - timeout = Timeout, - ftp_extension = true} = State) -> - - [_, PortStr | _] = lists:reverse(string:tokens(Lines, "|")), - {ok, {IP, _}} = peername(CSock), - - ?DBG('<--data tcp connect to ~p:~p, Caller=~p~n',[IP,PortStr,Caller]), - case connect(IP, list_to_integer(PortStr), Timeout, State) of - {ok, _, Socket} -> - handle_caller(State#state{caller = Caller, dsock = {tcp, Socket}}); - {error, _Reason} = Error -> - gen_server:reply(From, Error), - {noreply, State#state{client = undefined, caller = undefined}} - end; - - -%% FTP server does not support passive mode: try to fallback on active mode -handle_ctrl_result(_, - #state{mode = passive, - caller = {setup_data_connection, Caller}} = State) -> - setup_data_connection(State#state{mode = active, caller = Caller}); - - -%%-------------------------------------------------------------------------- -%% User handling -handle_ctrl_result({pos_interm, _}, - #state{caller = {handle_user, PassWord, Acc}} = State) -> - handle_user_passwd(PassWord, Acc, State); -handle_ctrl_result({Status, _}, - #state{caller = {handle_user, _, _}} = State) -> - ctrl_result_response(Status, State, {error, euser}); - -%% Accounts -handle_ctrl_result({pos_interm_acct, _}, - #state{caller = {handle_user_passwd, Acc}} = State) - when Acc =/= "" -> - handle_user_account(Acc, State); -handle_ctrl_result({Status, _}, - #state{caller = {handle_user_passwd, _}} = State) -> - ctrl_result_response(Status, State, {error, euser}); - -%%-------------------------------------------------------------------------- -%% Print current working directory -handle_ctrl_result({pos_compl, Lines}, - #state{caller = pwd, client = From} = State) -> - Dir = pwd_result(Lines), - gen_server:reply(From, {ok, Dir}), - {noreply, State#state{client = undefined, caller = undefined}}; - -%%-------------------------------------------------------------------------- -%% Directory listing -handle_ctrl_result({pos_prel, _}, #state{caller = {dir, Dir}} = State0) -> - case accept_data_connection(State0) of - {ok, State1} -> - State = activate_data_connection(State1), - {noreply, State#state{caller = {handle_dir_result, Dir}}}; - {error, _Reason} = ERROR -> - case State0#state.client of - undefined -> - {stop, ERROR, State0}; - From -> - gen_server:reply(From, ERROR), - {stop, normal, State0#state{client = undefined}} - end - end; - -handle_ctrl_result({pos_compl, _}, #state{caller = {handle_dir_result, Dir, - Data}, client = From} - = State) -> - case Dir of - "" -> % Current directory - gen_server:reply(From, {ok, Data}), - {noreply, State#state{client = undefined, - caller = undefined}}; - _ -> - %% <WTF> - %% Dir cannot be assumed to be a dir. It is a string that - %% could be a dir, but could also be a file or even a string - %% containing wildcards (*). - %% - %% %% If there is only one line it might be a directory with one - %% %% file but it might be an error message that the directory - %% %% was not found. So in this case we have to endure a little - %% %% overhead to be able to give a good return value. Alas not - %% %% all ftp implementations behave the same and returning - %% %% an error string is allowed by the FTP RFC. - %% case lists:dropwhile(fun(?CR) -> false;(_) -> true end, - %% binary_to_list(Data)) of - %% L when (L =:= [?CR, ?LF]) orelse (L =:= []) -> - %% send_ctrl_message(State, mk_cmd("PWD", [])), - %% activate_ctrl_connection(State), - %% {noreply, - %% State#state{caller = {handle_dir_data, Dir, Data}}}; - %% _ -> - %% gen_server:reply(From, {ok, Data}), - %% {noreply, State#state{client = undefined, - %% caller = undefined}} - %% end - %% </WTF> - gen_server:reply(From, {ok, Data}), - {noreply, State#state{client = undefined, - caller = undefined}} - end; - -handle_ctrl_result({pos_compl, Lines}, - #state{caller = {handle_dir_data, Dir, DirData}} = - State) -> - OldDir = pwd_result(Lines), - send_ctrl_message(State, mk_cmd("CWD ~s", [Dir])), - activate_ctrl_connection(State), - {noreply, State#state{caller = {handle_dir_data_second_phase, OldDir, - DirData}}}; -handle_ctrl_result({Status, _}, - #state{caller = {handle_dir_data, _, _}} = State) -> - ctrl_result_response(Status, State, {error, epath}); - -handle_ctrl_result(S={_Status, _}, - #state{caller = {handle_dir_result, _, _}} = State) -> - %% OTP-5731, macosx - ctrl_result_response(S, State, {error, epath}); - -handle_ctrl_result({pos_compl, _}, - #state{caller = {handle_dir_data_second_phase, OldDir, - DirData}} = State) -> - send_ctrl_message(State, mk_cmd("CWD ~s", [OldDir])), - activate_ctrl_connection(State), - {noreply, State#state{caller = {handle_dir_data_third_phase, DirData}}}; -handle_ctrl_result({Status, _}, - #state{caller = {handle_dir_data_second_phase, _, _}} - = State) -> - ctrl_result_response(Status, State, {error, epath}); -handle_ctrl_result(_, #state{caller = {handle_dir_data_third_phase, DirData}, - client = From} = State) -> - gen_server:reply(From, {ok, DirData}), - {noreply, State#state{client = undefined, caller = undefined}}; - -handle_ctrl_result({Status, _}, #state{caller = cd} = State) -> - ctrl_result_response(Status, State, {error, epath}); - -handle_ctrl_result(Status={epath, _}, #state{caller = {dir,_}} = State) -> - ctrl_result_response(Status, State, {error, epath}); - -%%-------------------------------------------------------------------------- -%% File renaming -handle_ctrl_result({pos_interm, _}, #state{caller = {rename, NewFile}} - = State) -> - send_ctrl_message(State, mk_cmd("RNTO ~s", [NewFile])), - activate_ctrl_connection(State), - {noreply, State#state{caller = rename_second_phase}}; - -handle_ctrl_result({Status, _}, - #state{caller = {rename, _}} = State) -> - ctrl_result_response(Status, State, {error, epath}); - -handle_ctrl_result({Status, _}, - #state{caller = rename_second_phase} = State) -> - ctrl_result_response(Status, State, {error, epath}); - -%%-------------------------------------------------------------------------- -%% File handling - recv_bin -handle_ctrl_result({pos_prel, _}, #state{caller = recv_bin} = State0) -> - case accept_data_connection(State0) of - {ok, State1} -> - State = activate_data_connection(State1), - {noreply, State}; - {error, _Reason} = ERROR -> - case State0#state.client of - undefined -> - {stop, ERROR, State0}; - From -> - gen_server:reply(From, ERROR), - {stop, normal, State0#state{client = undefined}} - end - end; - -handle_ctrl_result({pos_compl, _}, #state{caller = {recv_bin, Data}, - client = From} = State) -> - gen_server:reply(From, {ok, Data}), - close_data_connection(State), - {noreply, State#state{client = undefined, caller = undefined}}; - -handle_ctrl_result({Status, _}, #state{caller = recv_bin} = State) -> - close_data_connection(State), - ctrl_result_response(Status, State#state{dsock = undefined}, - {error, epath}); - -handle_ctrl_result({Status, _}, #state{caller = {recv_bin, _}} = State) -> - close_data_connection(State), - ctrl_result_response(Status, State#state{dsock = undefined}, - {error, epath}); -%%-------------------------------------------------------------------------- -%% File handling - start_chunk_transfer -handle_ctrl_result({pos_prel, _}, #state{client = From, - caller = start_chunk_transfer} - = State0) -> - case accept_data_connection(State0) of - {ok, State1} -> - State = start_chunk(State1), - {noreply, State}; - {error, _Reason} = ERROR -> - case State0#state.client of - undefined -> - {stop, ERROR, State0}; - From -> - gen_server:reply(From, ERROR), - {stop, normal, State0#state{client = undefined}} - end - end; - -%%-------------------------------------------------------------------------- -%% File handling - recv_file -handle_ctrl_result({pos_prel, _}, #state{caller = {recv_file, _}} = State0) -> - case accept_data_connection(State0) of - {ok, State1} -> - State = activate_data_connection(State1), - {noreply, State}; - {error, _Reason} = ERROR -> - case State0#state.client of - undefined -> - {stop, ERROR, State0}; - From -> - gen_server:reply(From, ERROR), - {stop, normal, State0#state{client = undefined}} - end - end; - -handle_ctrl_result({Status, _}, #state{caller = {recv_file, Fd}} = State) -> - file_close(Fd), - close_data_connection(State), - ctrl_result_response(Status, State#state{dsock = undefined}, - {error, epath}); -%%-------------------------------------------------------------------------- -%% File handling - transfer_* -handle_ctrl_result({pos_prel, _}, #state{caller = {transfer_file, Fd}} - = State0) -> - case accept_data_connection(State0) of - {ok, State1} -> - send_file(State1, Fd); - {error, _Reason} = ERROR -> - case State0#state.client of - undefined -> - {stop, ERROR, State0}; - From -> - gen_server:reply(From, ERROR), - {stop, normal, State0#state{client = undefined}} - end - end; - -handle_ctrl_result({pos_prel, _}, #state{caller = {transfer_data, Bin}} - = State0) -> - case accept_data_connection(State0) of - {ok, State} -> - send_bin(State, Bin); - {error, _Reason} = ERROR -> - case State0#state.client of - undefined -> - {stop, ERROR, State0}; - From -> - gen_server:reply(From, ERROR), - {stop, normal, State0#state{client = undefined}} - end - end; - -%%-------------------------------------------------------------------------- -%% Default -handle_ctrl_result({Status, Lines}, #state{client = From} = State) - when From =/= undefined -> - ctrl_result_response(Status, State, {error, Lines}). - -%%-------------------------------------------------------------------------- -%% Help functions to handle_ctrl_result -%%-------------------------------------------------------------------------- -ctrl_result_response(pos_compl, #state{client = From} = State, _) -> - gen_server:reply(From, ok), - {noreply, State#state{client = undefined, caller = undefined}}; - -ctrl_result_response(enofile, #state{client = From} = State, _) -> - gen_server:reply(From, {error, enofile}), - {noreply, State#state{client = undefined, caller = undefined}}; - -ctrl_result_response(Status, #state{client = From} = State, _) - when (Status =:= etnospc) orelse - (Status =:= epnospc) orelse - (Status =:= efnamena) orelse - (Status =:= econn) -> -%Status == etnospc; Status == epnospc; Status == econn -> - gen_server:reply(From, {error, Status}), -%% {stop, normal, {error, Status}, State#state{client = undefined}}; - {stop, normal, State#state{client = undefined}}; - -ctrl_result_response(_, #state{client = From} = State, ErrorMsg) -> - gen_server:reply(From, ErrorMsg), - {noreply, State#state{client = undefined, caller = undefined}}. - -%%-------------------------------------------------------------------------- -handle_caller(#state{caller = {dir, Dir, Len}} = State) -> - Cmd = case Len of - short -> "NLST"; - long -> "LIST" - end, - case Dir of - "" -> - send_ctrl_message(State, mk_cmd(Cmd, "")); - _ -> - send_ctrl_message(State, mk_cmd(Cmd ++ " ~s", [Dir])) - end, - activate_ctrl_connection(State), - {noreply, State#state{caller = {dir, Dir}}}; - -handle_caller(#state{caller = {recv_bin, RemoteFile}} = State) -> - send_ctrl_message(State, mk_cmd("RETR ~s", [RemoteFile])), - activate_ctrl_connection(State), - {noreply, State#state{caller = recv_bin}}; - -handle_caller(#state{caller = {start_chunk_transfer, Cmd, RemoteFile}} = - State) -> - send_ctrl_message(State, mk_cmd("~s ~s", [Cmd, RemoteFile])), - activate_ctrl_connection(State), - {noreply, State#state{caller = start_chunk_transfer}}; - -handle_caller(#state{caller = {recv_file, RemoteFile, Fd}} = State) -> - send_ctrl_message(State, mk_cmd("RETR ~s", [RemoteFile])), - activate_ctrl_connection(State), - {noreply, State#state{caller = {recv_file, Fd}}}; - -handle_caller(#state{caller = {transfer_file, {Cmd, LocalFile, RemoteFile}}, - ldir = LocalDir, client = From} = State) -> - case file_open(filename:absname(LocalFile, LocalDir), read) of - {ok, Fd} -> - send_ctrl_message(State, mk_cmd("~s ~s", [Cmd, RemoteFile])), - activate_ctrl_connection(State), - {noreply, State#state{caller = {transfer_file, Fd}}}; - {error, _} -> - gen_server:reply(From, {error, epath}), - {noreply, State#state{client = undefined, caller = undefined, - dsock = undefined}} - end; - -handle_caller(#state{caller = {transfer_data, {Cmd, Bin, RemoteFile}}} = - State) -> - send_ctrl_message(State, mk_cmd("~s ~s", [Cmd, RemoteFile])), - activate_ctrl_connection(State), - {noreply, State#state{caller = {transfer_data, Bin}}}. - -%% ----------- FTP SERVER COMMUNICATION ------------------------- - -%% Connect to FTP server at Host (default is TCP port 21) -%% in order to establish a control connection. -setup_ctrl_connection(Host, Port, Timeout, State) -> - MsTime = inets_time_compat:monotonic_time(), - case connect(Host, Port, Timeout, State) of - {ok, IpFam, CSock} -> - NewState = State#state{csock = {tcp, CSock}, ipfamily = IpFam}, - activate_ctrl_connection(NewState), - case Timeout - inets_lib:millisec_passed(MsTime) of - Timeout2 when (Timeout2 >= 0) -> - {ok, NewState#state{caller = open}, Timeout2}; - _ -> - %% Oups: Simulate timeout - {ok, NewState#state{caller = open}, 0} - end; - Error -> - Error - end. - -setup_data_connection(#state{mode = active, - caller = Caller, - csock = CSock, - ftp_extension = FtpExt} = State) -> - case (catch sockname(CSock)) of - {ok, {{_, _, _, _, _, _, _, _} = IP, _}} -> - {ok, LSock} = - gen_tcp:listen(0, [{ip, IP}, {active, false}, - inet6, binary, {packet, 0}]), - {ok, {_, Port}} = sockname({tcp,LSock}), - IpAddress = inet_parse:ntoa(IP), - Cmd = mk_cmd("EPRT |2|~s|~p|", [IpAddress, Port]), - send_ctrl_message(State, Cmd), - activate_ctrl_connection(State), - {noreply, State#state{caller = {setup_data_connection, - {LSock, Caller}}}}; - {ok, {{_,_,_,_} = IP, _}} -> - {ok, LSock} = gen_tcp:listen(0, [{ip, IP}, {active, false}, - binary, {packet, 0}]), - {ok, Port} = inet:port(LSock), - case FtpExt of - false -> - {IP1, IP2, IP3, IP4} = IP, - {Port1, Port2} = {Port div 256, Port rem 256}, - send_ctrl_message(State, - mk_cmd("PORT ~w,~w,~w,~w,~w,~w", - [IP1, IP2, IP3, IP4, Port1, Port2])); - true -> - IpAddress = inet_parse:ntoa(IP), - Cmd = mk_cmd("EPRT |1|~s|~p|", [IpAddress, Port]), - send_ctrl_message(State, Cmd) - end, - activate_ctrl_connection(State), - {noreply, State#state{caller = {setup_data_connection, - {LSock, Caller}}}} - end; - -setup_data_connection(#state{mode = passive, ipfamily = inet6, - caller = Caller} = State) -> - send_ctrl_message(State, mk_cmd("EPSV", [])), - activate_ctrl_connection(State), - {noreply, State#state{caller = {setup_data_connection, Caller}}}; - -setup_data_connection(#state{mode = passive, ipfamily = inet, - caller = Caller, - ftp_extension = false} = State) -> - send_ctrl_message(State, mk_cmd("PASV", [])), - activate_ctrl_connection(State), - {noreply, State#state{caller = {setup_data_connection, Caller}}}; - -setup_data_connection(#state{mode = passive, ipfamily = inet, - caller = Caller, - ftp_extension = true} = State) -> - send_ctrl_message(State, mk_cmd("EPSV", [])), - activate_ctrl_connection(State), - {noreply, State#state{caller = {setup_data_connection, Caller}}}. - -connect(Host, Port, Timeout, #state{ipfamily = inet = IpFam}) -> - connect2(Host, Port, IpFam, Timeout); - -connect(Host, Port, Timeout, #state{ipfamily = inet6 = IpFam}) -> - connect2(Host, Port, IpFam, Timeout); - -connect(Host, Port, Timeout, #state{ipfamily = inet6fb4}) -> - case inet:getaddr(Host, inet6) of - {ok, {0, 0, 0, 0, 0, 16#ffff, _, _} = IPv6} -> - case inet:getaddr(Host, inet) of - {ok, IPv4} -> - IpFam = inet, - connect2(IPv4, Port, IpFam, Timeout); - - _ -> - IpFam = inet6, - connect2(IPv6, Port, IpFam, Timeout) - end; - - {ok, IPv6} -> - IpFam = inet6, - connect2(IPv6, Port, IpFam, Timeout); - - _ -> - case inet:getaddr(Host, inet) of - {ok, IPv4} -> - IpFam = inet, - connect2(IPv4, Port, IpFam, Timeout); - Error -> - Error - end - end. - -connect2(Host, Port, IpFam, Timeout) -> - Opts = [IpFam, binary, {packet, 0}, {active, false}], - case gen_tcp:connect(Host, Port, Opts, Timeout) of - {ok, Sock} -> - {ok, IpFam, Sock}; - Error -> - Error - end. - - -accept_data_connection(#state{mode = active, - dtimeout = DTimeout, - tls_options = TLSOptions, - dsock = {lsock, LSock}} = State0) -> - case gen_tcp:accept(LSock, DTimeout) of - {ok, Socket} when is_list(TLSOptions) -> - gen_tcp:close(LSock), - ?DBG('<--data ssl:connect(~p, ~p)~n~p~n',[Socket,TLSOptions,State0]), - case ssl:connect(Socket, TLSOptions, DTimeout) of - {ok, TLSSocket} -> - {ok, State0#state{dsock={ssl,TLSSocket}}}; - {error, Reason} -> - {error, {ssl_connect_failed, Reason}} - end; - {ok, Socket} -> - gen_tcp:close(LSock), - {ok, State0#state{dsock={tcp,Socket}}}; - {error, Reason} -> - {error, {data_connect_failed, Reason}} - end; - -accept_data_connection(#state{mode = passive, - dtimeout = DTimeout, - dsock = {tcp,Socket}, - tls_options = TLSOptions} = State) when is_list(TLSOptions) -> - ?DBG('<--data ssl:connect(~p, ~p)~n~p~n',[Socket,TLSOptions,State]), - case ssl:connect(Socket, TLSOptions, DTimeout) of - {ok, TLSSocket} -> - {ok, State#state{dsock={ssl,TLSSocket}}}; - {error, Reason} -> - {error, {ssl_connect_failed, Reason}} - end; -accept_data_connection(#state{mode = passive} = State) -> - {ok,State}. - - -send_ctrl_message(_S=#state{csock = Socket, verbose = Verbose}, Message) -> - verbose(lists:flatten(Message),Verbose,send), - ?DBG('<--ctrl ~p ---- ~s~p~n',[Socket,Message,_S]), - send_message(Socket, Message). - -send_data_message(_S=#state{dsock = Socket}, Message) -> - ?DBG('<==data ~p ==== ~s~n~p~n',[Socket,Message,_S]), - case send_message(Socket, Message) of - ok -> - ok; - {error, Reason} -> - Report = io_lib:format("send/2 for socket ~p failed with " - "reason ~p~n", [Socket, Reason]), - error_logger:error_report(Report), - %% If tcp/ssl does not work the only option is to terminate, - %% this is the expected behavior under these circumstances. - exit(normal) %% User will get error message from terminate/2 - end. - -send_message({tcp, Socket}, Message) -> - gen_tcp:send(Socket, Message); -send_message({ssl, Socket}, Message) -> - ssl:send(Socket, Message). - -activate_ctrl_connection(#state{csock = Socket, ctrl_data = {<<>>, _, _}}) -> - activate_connection(Socket); -activate_ctrl_connection(#state{csock = Socket}) -> - %% We have already received at least part of the next control message, - %% that has been saved in ctrl_data, process this first. - self() ! {tcp, unwrap_socket(Socket), <<>>}. - -unwrap_socket({tcp,Socket}) -> Socket; -unwrap_socket({ssl,Socket}) -> Socket; -unwrap_socket(Socket) -> Socket. - - -activate_data_connection(#state{dsock = Socket} = State) -> - activate_connection(Socket), - State. - -activate_connection({tcp, Socket}) -> inet:setopts(Socket, [{active, once}]); -activate_connection({ssl, Socket}) -> ssl:setopts(Socket, [{active, once}]). - -close_ctrl_connection(#state{csock = undefined}) -> ok; -close_ctrl_connection(#state{csock = Socket}) -> close_connection(Socket). - -close_data_connection(#state{dsock = undefined}) -> ok; -close_data_connection(#state{dsock = Socket}) -> close_connection(Socket). - -close_connection({tcp, Socket}) -> gen_tcp:close(Socket); -close_connection({ssl, Socket}) -> ssl:close(Socket). - -%% ------------ FILE HANDELING ---------------------------------------- -send_file(#state{tls_upgrading_data_connection = {true, CTRL, _}} = State, Fd) -> - {noreply, State#state{tls_upgrading_data_connection = {true, CTRL, ?MODULE, send_file, Fd}}}; -send_file(State, Fd) -> - case file_read(Fd) of - {ok, N, Bin} when N > 0-> - send_data_message(State, Bin), - progress_report({binary, Bin}, State), - send_file(State, Fd); - {ok, _, _} -> - file_close(Fd), - close_data_connection(State), - progress_report({transfer_size, 0}, State), - activate_ctrl_connection(State), - {noreply, State#state{caller = transfer_file_second_phase, - dsock = undefined}}; - {error, Reason} -> - gen_server:reply(State#state.client, {error, Reason}), - {stop, normal, State#state{client = undefined}} - end. - -file_open(File, Option) -> - file:open(File, [raw, binary, Option]). - -file_close(Fd) -> - file:close(Fd). - -file_read(Fd) -> - case file:read(Fd, ?FILE_BUFSIZE) of - {ok, Bytes} -> - {ok, size(Bytes), Bytes}; - eof -> - {ok, 0, []}; - Other -> - Other - end. - -file_write(Bytes, Fd) -> - file:write(Fd, Bytes). - -%% -------------- MISC ---------------------------------------------- - -call(GenServer, Msg, Format) -> - call(GenServer, Msg, Format, infinity). -call(GenServer, Msg, Format, Timeout) -> - Req = {self(), Msg}, - case (catch gen_server:call(GenServer, Req, Timeout)) of - {ok, Bin} when is_binary(Bin) andalso (Format =:= string) -> - {ok, binary_to_list(Bin)}; - {'EXIT', _} -> - {error, eclosed}; - Result -> - Result - end. - -cast(GenServer, Msg) -> - gen_server:cast(GenServer, {self(), Msg}). - -send_bin(#state{tls_upgrading_data_connection = {true, CTRL, _}} = State, Bin) -> - State#state{tls_upgrading_data_connection = {true, CTRL, ?MODULE, send_bin, Bin}}; -send_bin(State, Bin) -> - send_data_message(State, Bin), - close_data_connection(State), - activate_ctrl_connection(State), - {noreply, State#state{caller = transfer_data_second_phase, - dsock = undefined}}. - -mk_cmd(Fmt, Args) -> - [io_lib:format(Fmt, Args)| [?CR, ?LF]]. % Deep list ok. - -is_name_sane([]) -> - true; -is_name_sane([?CR| _]) -> - false; -is_name_sane([?LF| _]) -> - false; -is_name_sane([_| Rest]) -> - is_name_sane(Rest). - -pwd_result(Lines) -> - {_, [?DOUBLE_QUOTE | Rest]} = - lists:splitwith(fun(?DOUBLE_QUOTE) -> false; (_) -> true end, Lines), - {Dir, _} = - lists:splitwith(fun(?DOUBLE_QUOTE) -> false; (_) -> true end, Rest), - Dir. - - -key_search(Key, List, Default) -> - case lists:keysearch(Key, 1, List) of - {value, {_,Val}} -> - Val; - false -> - Default - end. - -verbose(Lines, true, Direction) -> - DirStr = - case Direction of - send -> - "Sending: "; - _ -> - "Receiving: " - end, - Str = string:strip(string:strip(Lines, right, ?LF), right, ?CR), - erlang:display(DirStr++Str); -verbose(_, false,_) -> - ok. - -progress(Options) -> - ftp_progress:start_link(Options). - -progress_report(_, #state{progress = ignore}) -> - ok; -progress_report(stop, #state{progress = ProgressPid}) -> - ftp_progress:stop(ProgressPid); -progress_report({binary, Data}, #state{progress = ProgressPid}) -> - ftp_progress:report(ProgressPid, {transfer_size, size(Data)}); -progress_report(Report, #state{progress = ProgressPid}) -> - ftp_progress:report(ProgressPid, Report). - - -peername({tcp, Socket}) -> inet:peername(Socket); -peername({ssl, Socket}) -> ssl:peername(Socket). - -sockname({tcp, Socket}) -> inet:sockname(Socket); -sockname({ssl, Socket}) -> ssl:sockname(Socket). - -maybe_tls_upgrade(Pid, undefined) -> - {ok, Pid}; -maybe_tls_upgrade(Pid, TLSOptions) -> - catch ssl:start(), - call(Pid, {open, tls_upgrade, TLSOptions}, plain). - -start_chunk(#state{tls_upgrading_data_connection = {true, CTRL, _}} = State) -> - State#state{tls_upgrading_data_connection = {true, CTRL, ?MODULE, start_chunk, undefined}}; -start_chunk(#state{client = From} = State) -> - gen_server:reply(From, ok), - State#state{chunk = true, - client = undefined, - caller = undefined}. diff --git a/lib/inets/src/ftp/ftp_progress.erl b/lib/inets/src/ftp/ftp_progress.erl deleted file mode 100644 index 9c42723a07..0000000000 --- a/lib/inets/src/ftp/ftp_progress.erl +++ /dev/null @@ -1,128 +0,0 @@ -%% -%% %CopyrightBegin% -%% -%% Copyright Ericsson AB 2005-2009. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%% -%% %CopyrightEnd% -%% -%% -%% Description: This module impements a temporary process that -%% performes progress reporting during file transfer calling a user -%% defined callback function. Its life span is as long as the ftp connection -%% processes that spawned it lives. The purpose of this process is to -%% shild the ftp connection process from errors and time consuming operations -%% in the user defined callback function. - --module(ftp_progress). - -%% Internal API --export([start_link/1, report/2, stop/1]). - -%% Spawn export --export([init/1]). - --include_lib("kernel/include/file.hrl"). - --record(progress, { - file, % string() - cb_module, % atom() - cb_function, % atom() - init_progress_term, % term() - current_progress_term % term() - }). - -%%%========================================================================= -%%% Internal application API -%%%========================================================================= -%%-------------------------------------------------------------------------- -%% start_link(Options) -> ignore | pid() -%% Options = ignore | {CBModule, CBFunction, InitProgressTerm} -%% -%% Description: Starts the progress report process unless progress reporting -%% should not be performed. -%%-------------------------------------------------------------------------- -start_link(ignore) -> - ignore; -start_link(Options) -> - spawn_link(?MODULE, init, [Options]). - -%%-------------------------------------------------------------------------- -%% report_progress(Pid, Report) -> _ -%% Pid = pid() -%% Report = {local_file, File} | {remote_file, File} | -%% {transfer_size, Size} -%% Size = integer() -%% -%% Description: Reports progress to the reporting process that calls the -%% user defined callback function. -%%-------------------------------------------------------------------------- -report(Pid, Report) -> - Pid ! {progress_report, Report}. - -%%-------------------------------------------------------------------------- -%% stop(Pid) -> _ -%% Pid = pid() -%% -%% Description: -%%-------------------------------------------------------------------------- -stop(Pid) -> - Pid ! stop. - -%%%========================================================================= -%%% Internal functions -%%%========================================================================= -init(Options) -> - loop(progress(Options)). - -loop(Progress) -> - receive - {progress_report, Report} -> - NewProgress = report_progress(Report, Progress), - loop(NewProgress); - stop -> - ok - end. - -progress({CBModule, CBFunction, InitProgressTerm}) when is_atom(CBModule), - is_atom(CBFunction) -> - #progress{cb_module = CBModule, - cb_function = CBFunction, - init_progress_term = InitProgressTerm, - current_progress_term = InitProgressTerm}. - -report_progress({local_file, File}, Progress) -> - {ok, FileInfo} = file:read_file_info(File), - report_progress({file_size, FileInfo#file_info.size}, - Progress#progress{file = File}); - -report_progress({remote_file, File}, Progress) -> - report_progress({file_size, unknown}, Progress#progress{file = File}); - -report_progress(Size, #progress{file = File, - cb_module = CBModule, - cb_function = CBFunction, - current_progress_term = Term, - init_progress_term = InitTerm} = Progress) -> - - NewProgressTerm = CBModule:CBFunction(Term, File, Size), - - case Size of - {transfer_size, 0} -> - %% Transfer is compleat reset initial values - Progress#progress{current_progress_term = InitTerm, - file = undefined}; - _ -> - Progress#progress{current_progress_term = NewProgressTerm} - end. diff --git a/lib/inets/src/ftp/ftp_response.erl b/lib/inets/src/ftp/ftp_response.erl deleted file mode 100644 index 0aac15ef6c..0000000000 --- a/lib/inets/src/ftp/ftp_response.erl +++ /dev/null @@ -1,198 +0,0 @@ -%% -%% %CopyrightBegin% -%% -%% Copyright Ericsson AB 2005-2013. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%% -%% %CopyrightEnd% -%% -%% -%% Description: This module impements handling of ftp server responses. - --module(ftp_response). - -%% Internal API --export([parse_lines/3, interpret/1, error_string/1]). - --include("ftp_internal.hrl"). - -%% First group of reply code digits --define(POS_PREL, 1). --define(POS_COMPL, 2). --define(POS_INTERM, 3). --define(TRANS_NEG_COMPL, 4). --define(PERM_NEG_COMPL, 5). -%% Second group of reply code digits --define(SYNTAX,0). --define(INFORMATION,1). --define(CONNECTION,2). --define(AUTH_ACC,3). --define(UNSPEC,4). --define(FILE_SYSTEM,5). - -%%%========================================================================= -%%% INTERNAL API -%%%========================================================================= - -%%-------------------------------------------------------------------------- -%% parse_lines(Data, AccLines, StatusCode) -> {ok, Lines} | -%% {continue, {Data, -%% AccLines, StatusCode}} -%% -%% Data = binary() - data recived on the control connection from the -%% ftp-server. -%% AccLines = [string()] -%% StatusCode = start | {byte(), byte(), byte()} | finish - -%% Indicates where in the parsing process we are. -%% start - (looking for the status code of the message) -%% {byte(), byte(), byte()} - status code found, now -%% looking for the last line indication. -%% finish - now on the last line. -%% Description: Parses a ftp control response message. -%% "A reply is defined to contain the 3-digit code, followed by Space -%% <SP>, followed by one line of text (where some maximum line length -%% has been specified), and terminated by the Telnet end-of-line -%% code (CRLF), or a so called multilined reply for example: -%% -%% 123-First line -%% Second line -%% 234 A line beginning with numbers -%% 123 The last line -%% -%% The user-process then simply needs to search for the second -%% occurrence of the same reply code, followed by <SP> (Space), at -%% the beginning of a line, and ignore all intermediary lines. If -%% an intermediary line begins with a 3-digit number, the Server -%% will pad the front to avoid confusion. -%%-------------------------------------------------------------------------- - -%% Make sure we received the first 4 bytes so we know how to parse -%% the FTP server response e.i. is the response composed of one -%% or multiple lines. -parse_lines(Bin, Lines, start) when size(Bin) < 4 -> - {continue, {Bin, Lines, start}}; -%% Multiple lines exist -parse_lines(<<C1, C2, C3, $-, Rest/binary>>, Lines, start) -> - parse_lines(Rest, [$-, C3, C2, C1 | Lines], {C1, C2, C3}); -%% Only one line exists -parse_lines(<<C1, C2, C3, ?WHITE_SPACE, Bin/binary>>, Lines, start) -> - parse_lines(Bin, [?WHITE_SPACE, C3, C2, C1 | Lines], finish); - -%% Last line found -parse_lines(<<C1, C2, C3, ?WHITE_SPACE, Rest/binary>>, Lines, {C1, C2, C3}) -> - parse_lines(Rest, [?WHITE_SPACE, C3, C2, C1 | Lines], finish); -%% Potential end found wait for more data -parse_lines(<<C1, C2, C3>> = Bin, Lines, {C1, C2, C3}) -> - {continue, {Bin, Lines, {C1, C2, C3}}}; -%% Intermidate line begining with status code -parse_lines(<<C1, C2, C3, Rest/binary>>, Lines, {C1, C2, C3}) -> - parse_lines(Rest, [C3, C2, C1 | Lines], {C1, C2, C3}); - -%% Potential last line wait for more data -parse_lines(<<C1, C2>> = Data, Lines, {C1, C2, _} = StatusCode) -> - {continue, {Data, Lines, StatusCode}}; -parse_lines(<<C1>> = Data, Lines, {C1, _, _} = StatusCode) -> - {continue, {Data, Lines, StatusCode}}; -parse_lines(<<>> = Data, Lines, {_,_,_} = StatusCode) -> - {continue, {Data, Lines, StatusCode}}; -%% Part of the multiple lines -parse_lines(<<Octet, Rest/binary>>, Lines, {_,_, _} = StatusCode) -> - parse_lines(Rest, [Octet | Lines], StatusCode); - -%% End of FTP server response found -parse_lines(<<?CR, ?LF>>, Lines, finish) -> - {ok, lists:reverse([?LF, ?CR | Lines]), <<>>}; -parse_lines(<<?CR, ?LF, Rest/binary>>, Lines, finish) -> - {ok, lists:reverse([?LF, ?CR | Lines]), Rest}; - -%% Potential end found wait for more data -parse_lines(<<?CR>> = Data, Lines, finish) -> - {continue, {Data, Lines, finish}}; -parse_lines(<<>> = Data, Lines, finish) -> - {continue, {Data, Lines, finish}}; -%% Part of last line -parse_lines(<<Octet, Rest/binary>>, Lines, finish) -> - parse_lines(Rest, [Octet | Lines], finish). - -%%-------------------------------------------------------------------------- -%% interpret(Lines) -> {Status, Text} -%% Lines = [byte(), byte(), byte() | Text] - ftp server response as -%% returned by parse_lines/3 -%% Stauts = atom() (see interpret_status/3) -%% Text = [string()] -%% -%% Description: Create nicer data to match on. -%%-------------------------------------------------------------------------- -interpret([Didgit1, Didgit2, Didgit3 | Data]) -> - Code1 = Didgit1 - $0, - Code2 = Didgit2 - $0, - Code3 = Didgit3 - $0, - {interpret_status(Code1, Code2, Code3), Data}. - -%%-------------------------------------------------------------------------- -%% error_string(Error) -> string() -%% Error = {error, term()} | term() -%% -%% Description: Translates error codes into strings intended for -%% human interpretation. -%%-------------------------------------------------------------------------- -error_string({error, Reason}) -> - error_string(Reason); - -error_string(echunk) -> "Synchronisation error during chunk sending."; -error_string(eclosed) -> "Session has been closed."; -error_string(econn) -> "Connection to remote server prematurely closed."; -error_string(eexists) ->"File or directory already exists."; -error_string(ehost) -> "Host not found, FTP server not found, " - "or connection rejected."; -error_string(elogin) -> "User not logged in."; -error_string(enotbinary) -> "Term is not a binary."; -error_string(epath) -> "No such file or directory, already exists, " - "or permission denied."; -error_string(etype) -> "No such type."; -error_string(euser) -> "User name or password not valid."; -error_string(etnospc) -> "Insufficient storage space in system."; -error_string(enofile) -> "No files found or file unavailable"; -error_string(epnospc) -> "Exceeded storage allocation " - "(for current directory or dataset)."; -error_string(efnamena) -> "File name not allowed."; -error_string(Reason) -> - lists:flatten(io_lib:format("Unknown error: ~w", [Reason])). - -%%%======================================================================== -%%% Internal functions -%%%======================================================================== - -%% Positive Preleminary Reply -interpret_status(?POS_PREL,_,_) -> pos_prel; -%%FIXME ??? 3??? interpret_status(?POS_COMPL, ?AUTH_ACC, 3) -> tls_upgrade; -interpret_status(?POS_COMPL, ?AUTH_ACC, 4) -> tls_upgrade; -%% Positive Completion Reply -interpret_status(?POS_COMPL,_,_) -> pos_compl; -%% Positive Intermediate Reply nedd account -interpret_status(?POS_INTERM,?AUTH_ACC,2) -> pos_interm_acct; -%% Positive Intermediate Reply -interpret_status(?POS_INTERM,_,_) -> pos_interm; -%% No files found or file not available -interpret_status(?TRANS_NEG_COMPL,?FILE_SYSTEM,0) -> enofile; -%% No storage area no action taken -interpret_status(?TRANS_NEG_COMPL,?FILE_SYSTEM,2) -> etnospc; -%% Temporary Error, no action taken -interpret_status(?TRANS_NEG_COMPL,_,_) -> trans_neg_compl; -%% Permanent disk space error, the user shall not try again -interpret_status(?PERM_NEG_COMPL,?FILE_SYSTEM,0) -> epath; -interpret_status(?PERM_NEG_COMPL,?FILE_SYSTEM,2) -> epnospc; -interpret_status(?PERM_NEG_COMPL,?FILE_SYSTEM,3) -> efnamena; -interpret_status(?PERM_NEG_COMPL,_,_) -> perm_neg_compl. - diff --git a/lib/inets/src/ftp/ftp_sup.erl b/lib/inets/src/ftp/ftp_sup.erl deleted file mode 100644 index b5b3f6a105..0000000000 --- a/lib/inets/src/ftp/ftp_sup.erl +++ /dev/null @@ -1,60 +0,0 @@ -%% -%% %CopyrightBegin% -%% -%% Copyright Ericsson AB 2004-2009. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%% -%% %CopyrightEnd% -%% -%% -%%---------------------------------------------------------------------- -%% Purpose: The top supervisor for the ftp hangs under inets_sup. -%%---------------------------------------------------------------------- --module(ftp_sup). - --behaviour(supervisor). - -%% API --export([start_link/0]). --export([start_child/1]). - -%% Supervisor callback --export([init/1]). - -%%%========================================================================= -%%% API -%%%========================================================================= -start_link() -> - supervisor:start_link({local, ?MODULE}, ?MODULE, []). - -start_child(Args) -> - supervisor:start_child(?MODULE, Args). - -%%%========================================================================= -%%% Supervisor callback -%%%========================================================================= -init(_) -> - RestartStrategy = simple_one_for_one, - MaxR = 0, - MaxT = 3600, - - Name = undefined, % As simple_one_for_one is used. - StartFunc = {ftp, start_link, []}, - Restart = temporary, % E.g. should not be restarted - Shutdown = 4000, - Modules = [ftp], - Type = worker, - - ChildSpec = {Name, StartFunc, Restart, Shutdown, Type, Modules}, - {ok, {{RestartStrategy, MaxR, MaxT}, [ChildSpec]}}. diff --git a/lib/inets/src/http_client/Makefile b/lib/inets/src/http_client/Makefile index cb97b525f6..a1c1f36b70 100644 --- a/lib/inets/src/http_client/Makefile +++ b/lib/inets/src/http_client/Makefile @@ -1,7 +1,7 @@ # # %CopyrightBegin% # -# Copyright Ericsson AB 2005-2012. All Rights Reserved. +# Copyright Ericsson AB 2005-2016. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/inets/src/http_client/httpc.erl b/lib/inets/src/http_client/httpc.erl index 4554881d79..24a205ced9 100644 --- a/lib/inets/src/http_client/httpc.erl +++ b/lib/inets/src/http_client/httpc.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2009-2013. All Rights Reserved. +%% Copyright Ericsson AB 2009-2018. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -147,24 +147,39 @@ request(Method, Request, HttpOptions, Options) -> request(Method, Request, HttpOptions, Options, default_profile()). request(Method, + {Url, Headers, ContentType, TupleBody}, + HTTPOptions, Options, Profile) + when ((Method =:= post) orelse (Method =:= patch) orelse (Method =:= put) orelse (Method =:= delete)) + andalso (is_atom(Profile) orelse is_pid(Profile)) andalso + is_list(ContentType) andalso is_tuple(TupleBody)-> + case check_body_gen(TupleBody) of + ok -> + do_request(Method, {Url, Headers, ContentType, TupleBody}, HTTPOptions, Options, Profile); + Error -> + Error + end; +request(Method, + {Url, Headers, ContentType, Body}, + HTTPOptions, Options, Profile) + when ((Method =:= post) orelse (Method =:= patch) orelse (Method =:= put) orelse (Method =:= delete)) + andalso (is_atom(Profile) orelse is_pid(Profile)) andalso + is_list(ContentType) andalso (is_list(Body) orelse is_binary(Body)) -> + do_request(Method, {Url, Headers, ContentType, Body}, HTTPOptions, Options, Profile); + +request(Method, {Url, Headers}, HTTPOptions, Options, Profile) when (Method =:= options) orelse (Method =:= get) orelse + (Method =:= put) orelse (Method =:= head) orelse (Method =:= delete) orelse (Method =:= trace) andalso (is_atom(Profile) orelse is_pid(Profile)) -> - ?hcrt("request", [{method, Method}, - {url, Url}, - {headers, Headers}, - {http_options, HTTPOptions}, - {options, Options}, - {profile, Profile}]), - case uri_parse(Url, Options) of - {error, Reason} -> + case uri_string:parse(uri_string:normalize(Url)) of + {error, Reason, _} -> {error, Reason}; - {ok, ParsedUrl} -> + ParsedUrl -> case header_parse(Headers) of {error, Reason} -> {error, Reason}; @@ -172,31 +187,18 @@ request(Method, handle_request(Method, Url, ParsedUrl, Headers, [], [], HTTPOptions, Options, Profile) end - end; - -request(Method, - {Url, Headers, ContentType, Body}, - HTTPOptions, Options, Profile) - when ((Method =:= post) orelse (Method =:= patch) orelse (Method =:= put) orelse - (Method =:= delete)) andalso (is_atom(Profile) orelse is_pid(Profile)) -> - ?hcrt("request", [{method, Method}, - {url, Url}, - {headers, Headers}, - {content_type, ContentType}, - {body, Body}, - {http_options, HTTPOptions}, - {options, Options}, - {profile, Profile}]), - case uri_parse(Url, Options) of - {error, Reason} -> + end. + +do_request(Method, {Url, Headers, ContentType, Body}, HTTPOptions, Options, Profile) -> + case uri_string:parse(uri_string:normalize(Url)) of + {error, Reason, _} -> {error, Reason}; - {ok, ParsedUrl} -> + ParsedUrl -> handle_request(Method, Url, ParsedUrl, Headers, ContentType, Body, HTTPOptions, Options, Profile) end. - %%-------------------------------------------------------------------------- %% cancel_request(RequestId) -> ok %% cancel_request(RequestId, Profile) -> ok @@ -209,7 +211,6 @@ cancel_request(RequestId) -> cancel_request(RequestId, Profile) when is_atom(Profile) orelse is_pid(Profile) -> - ?hcrt("cancel request", [{request_id, RequestId}, {profile, Profile}]), httpc_manager:cancel_request(RequestId, profile_name(Profile)). @@ -232,7 +233,6 @@ cancel_request(RequestId, Profile) set_options(Options) -> set_options(Options, default_profile()). set_options(Options, Profile) when is_atom(Profile) orelse is_pid(Profile) -> - ?hcrt("set options", [{options, Options}, {profile, Profile}]), case validate_options(Options) of {ok, Opts} -> httpc_manager:set_options(Opts, profile_name(Profile)); @@ -272,7 +272,6 @@ get_options(all = _Options, Profile) -> get_options(Options, Profile) when (is_list(Options) andalso (is_atom(Profile) orelse is_pid(Profile))) -> - ?hcrt("get options", [{options, Options}, {profile, Profile}]), case Options -- get_options() of [] -> try @@ -314,26 +313,28 @@ store_cookies(SetCookieHeaders, Url) -> store_cookies(SetCookieHeaders, Url, Profile) when is_atom(Profile) orelse is_pid(Profile) -> - ?hcrt("store cookies", [{set_cookie_headers, SetCookieHeaders}, - {url, Url}, - {profile, Profile}]), - try - begin + case uri_string:parse(uri_string:normalize(Url)) of + {error, Bad, _} -> + {error, {parse_failed, Bad}}; + URI -> + Scheme = scheme_to_atom(maps:get(scheme, URI, '')), + Host = maps:get(host, URI, ""), + Port = maps:get(port, URI, default_port(Scheme)), + Path = uri_string:recompose(#{path => maps:get(path, URI, "")}), %% Since the Address part is not actually used %% by the manager when storing cookies, we dont %% care about ipv6-host-with-brackets. - {ok, {_, _, Host, Port, Path, _}} = uri_parse(Url), Address = {Host, Port}, ProfileName = profile_name(Profile), Cookies = httpc_cookie:cookies(SetCookieHeaders, Path, Host), httpc_manager:store_cookies(Cookies, Address, ProfileName), ok - end - catch - error:{badmatch, Bad} -> - {error, {parse_failed, Bad}} end. +default_port(http) -> + 80; +default_port(https) -> + 443. %%-------------------------------------------------------------------------- %% cookie_header(Url) -> Header | {error, Reason} @@ -353,9 +354,6 @@ cookie_header(Url, Opts) when is_list(Opts) -> cookie_header(Url, Opts, Profile) when (is_list(Opts) andalso (is_atom(Profile) orelse is_pid(Profile))) -> - ?hcrt("cookie header", [{url, Url}, - {opts, Opts}, - {profile, Profile}]), try begin httpc_manager:which_cookies(Url, Opts, profile_name(Profile)) @@ -398,7 +396,6 @@ which_sessions() -> which_sessions(default_profile()). which_sessions(Profile) -> - ?hcrt("which sessions", [{profile, Profile}]), try begin httpc_manager:which_sessions(profile_name(Profile)) @@ -419,7 +416,6 @@ info() -> info(default_profile()). info(Profile) -> - ?hcrt("info", [{profile, Profile}]), try begin httpc_manager:info(profile_name(Profile)) @@ -440,7 +436,6 @@ reset_cookies() -> reset_cookies(default_profile()). reset_cookies(Profile) -> - ?hcrt("reset cookies", [{profile, Profile}]), try begin httpc_manager:reset_cookies(profile_name(Profile)) @@ -458,7 +453,6 @@ reset_cookies(Profile) -> %% same behavior as active once for sockets. %%------------------------------------------------------------------------- stream_next(Pid) -> - ?hcrt("stream next", [{handler, Pid}]), httpc_handler:stream_next(Pid). @@ -466,7 +460,6 @@ stream_next(Pid) -> %%% Behaviour callbacks %%%======================================================================== start_standalone(PropList) -> - ?hcrt("start standalone", [{proplist, PropList}]), case proplists:get_value(profile, PropList) of undefined -> {error, no_profile}; @@ -477,14 +470,11 @@ start_standalone(PropList) -> end. start_service(Config) -> - ?hcrt("start service", [{config, Config}]), httpc_profile_sup:start_child(Config). stop_service(Profile) when is_atom(Profile) -> - ?hcrt("stop service", [{profile, Profile}]), httpc_profile_sup:stop_child(Profile); stop_service(Pid) when is_pid(Pid) -> - ?hcrt("stop service", [{pid, Pid}]), case service_info(Pid) of {ok, [{profile, Profile}]} -> stop_service(Profile); @@ -510,9 +500,8 @@ service_info(Pid) -> %%%======================================================================== %%% Internal functions %%%======================================================================== - handle_request(Method, Url, - {Scheme, UserInfo, Host, Port, Path, Query}, + URI, Headers0, ContentType, Body0, HTTPOptions0, Options0, Profile) -> @@ -521,9 +510,6 @@ handle_request(Method, Url, try begin - ?hcrt("begin processing", [{started, Started}, - {new_headers, NewHeaders0}]), - {NewHeaders, Body} = case Body0 of {chunkify, ProcessBody, Acc} @@ -540,62 +526,81 @@ handle_request(Method, Url, throw({error, {bad_body, Body0}}) end, - HTTPOptions = http_options(HTTPOptions0), - Options = request_options(Options0), - Sync = proplists:get_value(sync, Options), - Stream = proplists:get_value(stream, Options), - Host2 = header_host(Scheme, Host, Port), - HeadersRecord = header_record(NewHeaders, Host2, HTTPOptions), - Receiver = proplists:get_value(receiver, Options), - SocketOpts = proplists:get_value(socket_opts, Options), - BracketedHost = proplists:get_value(ipv6_host_with_brackets, - Options), - MaybeEscPath = maybe_encode_uri(HTTPOptions, Path), - MaybeEscQuery = maybe_encode_uri(HTTPOptions, Query), - AbsUri = maybe_encode_uri(HTTPOptions, Url), + HTTPOptions = http_options(HTTPOptions0), + Options = request_options(Options0), + Sync = proplists:get_value(sync, Options), + Stream = proplists:get_value(stream, Options), + Receiver = proplists:get_value(receiver, Options), + SocketOpts = proplists:get_value(socket_opts, Options), + UnixSocket = proplists:get_value(unix_socket, Options), + BracketedHost = proplists:get_value(ipv6_host_with_brackets, + Options), + + Scheme = scheme_to_atom(maps:get(scheme, URI, '')), + Userinfo = maps:get(userinfo, URI, ""), + Host = http_util:maybe_add_brackets(maps:get(host, URI, ""), BracketedHost), + Port = maps:get(port, URI, default_port(Scheme)), + Host2 = http_request:normalize_host(Scheme, Host, Port), + Path = uri_string:recompose(#{path => maps:get(path, URI, "")}), + Query = add_question_mark(maps:get(query, URI, "")), + HeadersRecord = header_record(NewHeaders, Host2, HTTPOptions), Request = #request{from = Receiver, - scheme = Scheme, - address = {host_address(Host, BracketedHost), Port}, - path = MaybeEscPath, - pquery = MaybeEscQuery, + scheme = Scheme, + address = {Host, Port}, + path = Path, + pquery = Query, method = Method, headers = HeadersRecord, content = {ContentType, Body}, settings = HTTPOptions, - abs_uri = AbsUri, - userinfo = UserInfo, + abs_uri = Url, + userinfo = Userinfo, stream = Stream, headers_as_is = headers_as_is(Headers0, Options), socket_opts = SocketOpts, started = Started, + unix_socket = UnixSocket, ipv6_host_with_brackets = BracketedHost}, - case httpc_manager:request(Request, profile_name(Profile)) of {ok, RequestId} -> handle_answer(RequestId, Sync, Options); {error, Reason} -> - ?hcrd("request failed", [{reason, Reason}]), {error, Reason} end end catch error:{noproc, _} -> - ?hcrv("noproc", [{profile, Profile}]), {error, {not_started, Profile}}; throw:Error -> - ?hcrv("throw", [{error, Error}]), Error end. + +add_question_mark(<<>>) -> + <<>>; +add_question_mark([]) -> + []; +add_question_mark(Comp) when is_binary(Comp) -> + <<$?, Comp/binary>>; +add_question_mark(Comp) when is_list(Comp) -> + [$?|Comp]. + + +scheme_to_atom("http") -> + http; +scheme_to_atom("https") -> + https; +scheme_to_atom('') -> + ''; +scheme_to_atom(Scheme) -> + throw({error, {bad_scheme, Scheme}}). + + ensure_chunked_encoding(Hdrs) -> Key = "transfer-encoding", lists:keystore(Key, 1, Hdrs, {Key, "chunked"}). -maybe_encode_uri(#http_options{url_encode = true}, URI) -> - http_uri:encode(URI); -maybe_encode_uri(_, URI) -> - URI. mk_chunkify_fun(ProcessBody) -> fun(eof_body) -> @@ -620,15 +625,10 @@ handle_answer(RequestId, false, _) -> handle_answer(RequestId, true, Options) -> receive {http, {RequestId, saved_to_file}} -> - ?hcrt("received saved-to-file", [{request_id, RequestId}]), {ok, saved_to_file}; {http, {RequestId, {_,_,_} = Result}} -> - ?hcrt("received answer", [{request_id, RequestId}, - {result, Result}]), return_answer(Options, Result); {http, {RequestId, {error, Reason}}} -> - ?hcrt("received error", [{request_id, RequestId}, - {reason, Reason}]), {error, Reason} end. @@ -826,7 +826,7 @@ request_options_defaults() -> error end, - VerifyBrackets = VerifyBoolean, + VerifyBrackets = VerifyBoolean, [ {sync, true, VerifySync}, @@ -897,11 +897,36 @@ request_options_sanity_check(Opts) -> end, ok. -validate_options(Options) -> - (catch validate_options(Options, [])). - -validate_options([], ValidateOptions) -> - {ok, lists:reverse(ValidateOptions)}; +validate_ipfamily_unix_socket(Options0) -> + IpFamily = proplists:get_value(ipfamily, Options0, inet), + UnixSocket = proplists:get_value(unix_socket, Options0, undefined), + Options1 = proplists:delete(ipfamily, Options0), + Options2 = proplists:delete(ipfamily, Options1), + validate_ipfamily_unix_socket(IpFamily, UnixSocket, Options2, + [{ipfamily, IpFamily}, {unix_socket, UnixSocket}]). +%% +validate_ipfamily_unix_socket(local, undefined, _Options, _Acc) -> + bad_option(unix_socket, undefined); +validate_ipfamily_unix_socket(IpFamily, UnixSocket, _Options, _Acc) + when IpFamily =/= local, UnixSocket =/= undefined -> + bad_option(ipfamily, IpFamily); +validate_ipfamily_unix_socket(IpFamily, UnixSocket, Options, Acc) -> + validate_ipfamily(IpFamily), + validate_unix_socket(UnixSocket), + {Options, Acc}. + + +validate_options(Options0) -> + try + {Options, Acc} = validate_ipfamily_unix_socket(Options0), + validate_options(Options, Acc) + catch + error:Reason -> + {error, Reason} + end. +%% +validate_options([], ValidOptions) -> + {ok, lists:reverse(ValidOptions)}; validate_options([{proxy, Proxy} = Opt| Tail], Acc) -> validate_proxy(Proxy), @@ -961,6 +986,10 @@ validate_options([{verbose, Value} = Opt| Tail], Acc) -> validate_verbose(Value), validate_options(Tail, [Opt | Acc]); +validate_options([{unix_socket, Value} = Opt| Tail], Acc) -> + validate_unix_socket(Value), + validate_options(Tail, [Opt | Acc]); + validate_options([{_, _} = Opt| _], _Acc) -> {error, {not_an_option, Opt}}. @@ -1029,7 +1058,8 @@ validate_ipv6(BadValue) -> bad_option(ipv6, BadValue). validate_ipfamily(Value) - when (Value =:= inet) orelse (Value =:= inet6) orelse (Value =:= inet6fb4) -> + when (Value =:= inet) orelse (Value =:= inet6) orelse + (Value =:= inet6fb4) orelse (Value =:= local) -> Value; validate_ipfamily(BadValue) -> bad_option(ipfamily, BadValue). @@ -1059,18 +1089,19 @@ validate_verbose(Value) validate_verbose(BadValue) -> bad_option(verbose, BadValue). +validate_unix_socket(Value) + when (Value =:= undefined) -> + Value; +validate_unix_socket(Value) + when is_list(Value) andalso length(Value) > 0 -> + Value; +validate_unix_socket(BadValue) -> + bad_option(unix_socket, BadValue). + bad_option(Option, BadValue) -> throw({error, {bad_option, Option, BadValue}}). -header_host(https, Host, 443 = _Port) -> - Host; -header_host(http, Host, 80 = _Port) -> - Host; -header_host(_Scheme, Host, Port) -> - Host ++ ":" ++ integer_to_list(Port). - - header_record(NewHeaders, Host, #http_options{version = Version}) -> header_record(NewHeaders, #http_request_h{}, Host, Version). @@ -1226,17 +1257,6 @@ validate_headers(RequestHeaders, _, _) -> %% These functions is just simple wrappers to parse specifically HTTP URIs %%-------------------------------------------------------------------------- -scheme_defaults() -> - [{http, 80}, {https, 443}]. - -uri_parse(URI) -> - http_uri:parse(URI, [{scheme_defaults, scheme_defaults()}]). - -uri_parse(URI, Opts) -> - http_uri:parse(URI, [{scheme_defaults, scheme_defaults()} | Opts]). - - -%%-------------------------------------------------------------------------- header_parse([]) -> ok; header_parse([{Field, Value}|T]) when is_list(Field), is_list(Value) -> @@ -1257,18 +1277,10 @@ child_name(Pid, [{Name, Pid} | _]) -> child_name(Pid, [_ | Children]) -> child_name(Pid, Children). -%% d(F) -> -%% d(F, []). - -%% d(F, A) -> -%% d(get(dbg), F, A). -%% d(true, F, A) -> -%% io:format(user, "~w:~w:" ++ F ++ "~n", [self(), ?MODULE | A]); -%% d(_, _, _) -> -%% ok. - -host_address(Host, false) -> - Host; -host_address(Host, true) -> - string:strip(string:strip(Host, right, $]), left, $[). +check_body_gen({Fun, _}) when is_function(Fun) -> + ok; +check_body_gen({chunkify, Fun, _}) when is_function(Fun) -> + ok; +check_body_gen(Gen) -> + {error, {bad_body_generator, Gen}}. diff --git a/lib/inets/src/http_client/httpc_cookie.erl b/lib/inets/src/http_client/httpc_cookie.erl index 0c4f44a575..cbf428ab3e 100644 --- a/lib/inets/src/http_client/httpc_cookie.erl +++ b/lib/inets/src/http_client/httpc_cookie.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2004-2011. All Rights Reserved. +%% Copyright Ericsson AB 2004-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/lib/inets/src/http_client/httpc_handler.erl b/lib/inets/src/http_client/httpc_handler.erl index d1c52dcc78..1bf5d25c98 100644 --- a/lib/inets/src/http_client/httpc_handler.erl +++ b/lib/inets/src/http_client/httpc_handler.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2002-2015. All Rights Reserved. +%% Copyright Ericsson AB 2002-2018. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -32,7 +32,6 @@ %% Internal Application API -export([ start_link/4, - %% connect_and_send/2, send/2, cancel/2, stream_next/1, @@ -45,28 +44,28 @@ -record(timers, { - request_timers = [], % [ref()] - queue_timer % ref() + request_timers = [] :: [reference()], + queue_timer :: reference() | 'undefined' }). -record(state, { - request, % #request{} - session, % #session{} + request :: request() | 'undefined', + session :: session() | 'undefined', status_line, % {Version, StatusCode, ReasonPharse} - headers, % #http_response_h{} - body, % binary() + headers :: http_response_h() | 'undefined', + body :: binary() | 'undefined', mfa, % {Module, Function, Args} - pipeline = queue:new(), % queue:queue() - keep_alive = queue:new(), % queue:queue() - status, % undefined | new | pipeline | keep_alive | close | {ssl_tunnel, Request} + pipeline = queue:new() :: queue:queue(), + keep_alive = queue:new() :: queue:queue(), + status :: undefined | new | pipeline | keep_alive | close | {ssl_tunnel, request()}, canceled = [], % [RequestId] - max_header_size = nolimit, % nolimit | integer() - max_body_size = nolimit, % nolimit | integer() - options, % #options{} - timers = #timers{}, % #timers{} - profile_name, % atom() - id of httpc_manager process. - once = inactive % inactive | once + max_header_size = nolimit :: nolimit | integer(), + max_body_size = nolimit :: nolimit | integer(), + options :: options(), + timers = #timers{} :: #timers{}, + profile_name :: atom(), % id of httpc_manager process. + once = inactive :: 'inactive' | 'once' }). @@ -108,12 +107,12 @@ start_link(Parent, Request, Options, ProfileName) -> %% to be called by the httpc manager process. %%-------------------------------------------------------------------- send(Request, Pid) -> - call(Request, Pid, 5000). + call(Request, Pid). %%-------------------------------------------------------------------- %% Function: cancel(RequestId, Pid) -> ok -%% RequestId = ref() +%% RequestId = reference() %% Pid = pid() - the pid of the http-request handler process. %% %% Description: Cancels a request. Intended to be called by the httpc @@ -163,14 +162,12 @@ info(Pid) -> %%-------------------------------------------------------------------- %% Request should not be streamed stream(BodyPart, #request{stream = none} = Request, _) -> - ?hcrt("stream - none", []), {false, BodyPart, Request}; %% Stream to caller stream(BodyPart, #request{stream = Self} = Request, Code) when ?IS_STREAMED(Code) andalso ((Self =:= self) orelse (Self =:= {self, once})) -> - ?hcrt("stream - self", [{stream, Self}, {code, Code}]), httpc_response:send(Request#request.from, {Request#request.id, stream, BodyPart}), {true, <<>>, Request}; @@ -180,10 +177,8 @@ stream(BodyPart, #request{stream = Self} = Request, Code) %% We keep this for backward compatibillity... stream(BodyPart, #request{stream = Filename} = Request, Code) when ?IS_STREAMED(Code) andalso is_list(Filename) -> - ?hcrt("stream - filename", [{stream, Filename}, {code, Code}]), case file:open(Filename, [write, raw, append, delayed_write]) of {ok, Fd} -> - ?hcrt("stream - file open ok", [{fd, Fd}]), stream(BodyPart, Request#request{stream = Fd}, 200); {error, Reason} -> exit({stream_to_file_failed, Reason}) @@ -192,7 +187,6 @@ stream(BodyPart, #request{stream = Filename} = Request, Code) %% Stream to file stream(BodyPart, #request{stream = Fd} = Request, Code) when ?IS_STREAMED(Code) -> - ?hcrt("stream to file", [{stream, Fd}, {code, Code}]), case file:write(Fd, BodyPart) of ok -> {true, <<>>, Request}; @@ -201,7 +195,6 @@ stream(BodyPart, #request{stream = Fd} = Request, Code) end; stream(BodyPart, Request,_) -> % only 200 and 206 responses can be streamed - ?hcrt("stream - ignore", [{request, Request}]), {false, BodyPart, Request}. @@ -255,22 +248,131 @@ init([Parent, Request, Options, ProfileName]) -> %% {stop, Reason, State} (terminate/2 is called) %% Description: Handling call messages %%-------------------------------------------------------------------- -handle_call(#request{address = Addr} = Request, _, +handle_call(Request, From, State) -> + try do_handle_call(Request, From, State) of + Result -> + Result + catch + Class:Reason:ST -> + {stop, {shutdown, {{Class, Reason}, ST}}, State} + end. + + +%%-------------------------------------------------------------------- +%% Function: handle_cast(Msg, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} (terminate/2 is called) +%% Description: Handling cast messages +%%-------------------------------------------------------------------- +handle_cast(Msg, State) -> + try do_handle_cast(Msg, State) of + Result -> + Result + catch + Class:Reason:ST -> + {stop, {shutdown, {{Class, Reason}, ST}}, State} + end. + +%%-------------------------------------------------------------------- +%% Function: handle_info(Info, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} (terminate/2 is called) +%% Description: Handling all non call/cast messages +%%-------------------------------------------------------------------- +handle_info(Info, State) -> + try do_handle_info(Info, State) of + Result -> + Result + catch + Class:Reason:ST -> + {stop, {shutdown, {{Class, Reason}, ST}}, State} + end. + +%%-------------------------------------------------------------------- +%% Function: terminate(Reason, State) -> _ (ignored by gen_server) +%% Description: Shutdown the httpc_handler +%%-------------------------------------------------------------------- + +terminate(normal, #state{session = undefined}) -> + ok; + +%% Init error sending, no session information has been setup but +%% there is a socket that needs closing. +terminate(normal, + #state{session = #session{id = undefined} = Session}) -> + close_socket(Session); + +%% Socket closed remotely +terminate(normal, + #state{session = #session{socket = {remote_close, Socket}, + socket_type = SocketType, + id = Id}, + profile_name = ProfileName, + request = Request, + timers = Timers, + pipeline = Pipeline, + keep_alive = KeepAlive} = State) -> + %% Clobber session + (catch httpc_manager:delete_session(Id, ProfileName)), + + maybe_retry_queue(Pipeline, State), + maybe_retry_queue(KeepAlive, State), + + %% Cancel timers + cancel_timers(Timers), + + %% Maybe deliver answers to requests + deliver_answer(Request), + + %% And, just in case, close our side (**really** overkill) + http_transport:close(SocketType, Socket); + +terminate(_Reason, #state{session = #session{id = Id, + socket = Socket, + socket_type = SocketType}, + request = undefined, + profile_name = ProfileName, + timers = Timers, + pipeline = Pipeline, + keep_alive = KeepAlive} = State) -> + + %% Clobber session + (catch httpc_manager:delete_session(Id, ProfileName)), + + maybe_retry_queue(Pipeline, State), + maybe_retry_queue(KeepAlive, State), + + cancel_timer(Timers#timers.queue_timer, timeout_queue), + http_transport:close(SocketType, Socket); + +terminate(_Reason, #state{request = undefined}) -> + ok; + +terminate(Reason, #state{request = Request} = State) -> + NewState = maybe_send_answer(Request, + httpc_response:error(Request, Reason), + State), + terminate(Reason, NewState#state{request = undefined}). + +%%-------------------------------------------------------------------- +%% Func: code_change(_OldVsn, State, Extra) -> {ok, NewState} +%% Purpose: Convert process state when code is changed +%%-------------------------------------------------------------------- + +code_change(_, State, _) -> + {ok, State}. + +%%%-------------------------------------------------------------------- +%%% Internal functions +%%%-------------------------------------------------------------------- +do_handle_call(#request{address = Addr} = Request, _, #state{status = Status, session = #session{type = pipeline} = Session, timers = Timers, options = #options{proxy = Proxy} = _Options, profile_name = ProfileName} = State0) when Status =/= undefined -> - - ?hcrv("new request on a pipeline session", - [{request, Request}, - {profile, ProfileName}, - {status, Status}, - {timers, Timers}]), - Address = handle_proxy(Addr, Proxy), - case httpc_request:send(Address, Session, Request) of ok -> @@ -285,9 +387,8 @@ handle_call(#request{address = Addr} = Request, _, case State0#state.request of #request{} = OldRequest -> %% Old request not yet finished - ?hcrd("old request still not finished", []), %% Make sure to use the new value of timers in state - NewTimers = State1#state.timers, + NewTimers = State1#state.timers, NewPipeline = queue:in(Request, State1#state.pipeline), NewSession = Session#session{queue_length = @@ -295,7 +396,6 @@ handle_call(#request{address = Addr} = Request, _, queue:len(NewPipeline) + 1, client_close = ClientClose}, insert_session(NewSession, ProfileName), - ?hcrd("session updated", []), {reply, ok, State1#state{ request = OldRequest, pipeline = NewPipeline, @@ -304,7 +404,6 @@ handle_call(#request{address = Addr} = Request, _, undefined -> %% Note: tcp-message receiving has already been %% activated by handle_pipeline/2. - ?hcrd("no current request", []), cancel_timer(Timers#timers.queue_timer, timeout_queue), NewSession = @@ -312,18 +411,16 @@ handle_call(#request{address = Addr} = Request, _, client_close = ClientClose}, httpc_manager:insert_session(NewSession, ProfileName), NewTimers = Timers#timers{queue_timer = undefined}, - ?hcrd("session created", []), State = init_wait_for_response_state(Request, State1#state{session = NewSession, timers = NewTimers}), {reply, ok, State} end; {error, Reason} -> - ?hcri("failed sending request", [{reason, Reason}]), NewPipeline = queue:in(Request, State0#state.pipeline), - {stop, shutdown, {pipeline_failed, Reason}, State0#state{pipeline = NewPipeline}} + {stop, {shutdown, {pipeline_failed, Reason}}, State0#state{pipeline = NewPipeline}} end; -handle_call(#request{address = Addr} = Request, _, +do_handle_call(#request{address = Addr} = Request, _, #state{status = Status, session = #session{type = keep_alive} = Session, timers = Timers, @@ -331,17 +428,11 @@ handle_call(#request{address = Addr} = Request, _, profile_name = ProfileName} = State0) when Status =/= undefined -> - ?hcrv("new request on a keep-alive session", - [{request, Request}, - {profile, ProfileName}, - {status, Status}]), - ClientClose = httpc_request:is_client_closing(Request#request.headers), case State0#state.request of #request{} -> %% Old request not yet finished %% Make sure to use the new value of timers in state - ?hcrd("old request still not finished", []), NewKeepAlive = queue:in(Request, State0#state.keep_alive), NewSession = Session#session{queue_length = @@ -349,13 +440,11 @@ handle_call(#request{address = Addr} = Request, _, queue:len(NewKeepAlive) + 1, client_close = ClientClose}, insert_session(NewSession, ProfileName), - ?hcrd("session updated", []), {reply, ok, State0#state{keep_alive = NewKeepAlive, session = NewSession}}; undefined -> %% Note: tcp-message receiving has already been %% activated by handle_pipeline/2. - ?hcrd("no current request", []), cancel_timer(Timers#timers.queue_timer, timeout_queue), NewTimers = Timers#timers{queue_timer = undefined}, @@ -363,8 +452,6 @@ handle_call(#request{address = Addr} = Request, _, Address = handle_proxy(Addr, Proxy), case httpc_request:send(Address, Session, Request) of ok -> - ?hcrd("request sent", []), - %% Activate the request time out for the new request State2 = activate_request_timeout(State1#state{request = Request}), @@ -375,22 +462,13 @@ handle_call(#request{address = Addr} = Request, _, State = init_wait_for_response_state(Request, State2#state{session = NewSession}), {reply, ok, State}; {error, Reason} -> - ?hcri("failed sending request", [{reason, Reason}]), - {stop, shutdown, {keepalive_failed, Reason}, State1} + {stop, {shutdown, {keepalive_failed, Reason}}, State1} end end; - -handle_call(info, _, State) -> +do_handle_call(info, _, State) -> Info = handler_info(State), {reply, Info, State}. -%%-------------------------------------------------------------------- -%% Function: handle_cast(Msg, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} (terminate/2 is called) -%% Description: Handling cast messages -%%-------------------------------------------------------------------- - %% When the request in process has been canceled the handler process is %% stopped and the pipelined requests will be reissued or remaining %% requests will be sent on a new connection. This is is @@ -403,145 +481,102 @@ handle_call(info, _, State) -> %% handle_keep_alive_queue/2 on the other hand will just skip the %% request as if it was never issued as in this case the request will %% not have been sent. -handle_cast({cancel, RequestId}, +do_handle_cast({cancel, RequestId}, #state{request = #request{id = RequestId} = Request, - profile_name = ProfileName, canceled = Canceled} = State) -> - ?hcrv("cancel current request", [{request_id, RequestId}, - {profile, ProfileName}, - {canceled, Canceled}]), {stop, normal, State#state{canceled = [RequestId | Canceled], request = Request#request{from = answer_sent}}}; -handle_cast({cancel, RequestId}, - #state{profile_name = ProfileName, - request = #request{id = CurrId}, - canceled = Canceled} = State) -> - ?hcrv("cancel", [{request_id, RequestId}, - {curr_req_id, CurrId}, - {profile, ProfileName}, - {canceled, Canceled}]), +do_handle_cast({cancel, RequestId}, + #state{request = #request{}, + canceled = Canceled} = State) -> {noreply, State#state{canceled = [RequestId | Canceled]}}; -handle_cast({cancel, RequestId}, - #state{profile_name = ProfileName, - request = undefined, - canceled = Canceled} = State) -> - ?hcrv("cancel", [{request_id, RequestId}, - {curr_req_id, undefined}, - {profile, ProfileName}, - {canceled, Canceled}]), +do_handle_cast({cancel, _}, + #state{request = undefined} = State) -> {noreply, State}; - -handle_cast(stream_next, #state{session = Session} = State) -> +do_handle_cast(stream_next, #state{session = Session} = State) -> activate_once(Session), %% Inactivate the #state.once here because we don't want %% next_body_chunk/1 to activate the socket twice. {noreply, State#state{once = inactive}}. - -%%-------------------------------------------------------------------- -%% Function: handle_info(Info, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} (terminate/2 is called) -%% Description: Handling all non call/cast messages -%%-------------------------------------------------------------------- -handle_info({Proto, _Socket, Data}, +do_handle_info({Proto, _Socket, Data}, #state{mfa = {Module, Function, Args}, - request = #request{method = Method, - stream = Stream} = Request, + request = #request{method = Method} = Request, session = Session, status_line = StatusLine} = State) when (Proto =:= tcp) orelse (Proto =:= ssl) orelse (Proto =:= httpc_handler) -> - ?hcri("received data", [{proto, Proto}, - {module, Module}, - {function, Function}, - {method, Method}, - {stream, Stream}, - {session, Session}, - {status_line, StatusLine}]), - - FinalResult = - try Module:Function([Data | Args]) of - {ok, Result} -> - ?hcrd("data processed - ok", []), - handle_http_msg(Result, State); - {_, whole_body, _} when Method =:= head -> - ?hcrd("data processed - whole body", []), - handle_response(State#state{body = <<>>}); - {Module, whole_body, [Body, Length]} -> - ?hcrd("data processed - whole body", [{length, Length}]), - {_, Code, _} = StatusLine, - {Streamed, NewBody, NewRequest} = stream(Body, Request, Code), - %% When we stream we will not keep the already - %% streamed data, that would be a waste of memory. - NewLength = - case Streamed of - false -> - Length; - true -> - Length - size(Body) - end, - - NewState = next_body_chunk(State, Code), - NewMFA = {Module, whole_body, [NewBody, NewLength]}, - {noreply, NewState#state{mfa = NewMFA, - request = NewRequest}}; - {Module, decode_size, - [TotalChunk, HexList, - {MaxBodySize, BodySoFar, AccLength, MaxHeaderSize}]} - when BodySoFar =/= <<>> -> - ?hcrd("data processed - decode_size", []), - %% The response body is chunk-encoded. Steal decoded - %% chunks as much as possible to stream. - {_, Code, _} = StatusLine, - {_, NewBody, NewRequest} = stream(BodySoFar, Request, Code), - NewState = next_body_chunk(State, Code), - NewMFA = {Module, decode_size, - [TotalChunk, HexList, - {MaxBodySize, NewBody, AccLength, MaxHeaderSize}]}, - {noreply, NewState#state{mfa = NewMFA, - request = NewRequest}}; - {Module, decode_data, - [ChunkSize, TotalChunk, + try Module:Function([Data | Args]) of + {ok, Result} -> + handle_http_msg(Result, State); + {_, whole_body, _} when Method =:= head -> + handle_response(State#state{body = <<>>}); + {Module, whole_body, [Body, Length]} -> + {_, Code, _} = StatusLine, + {Streamed, NewBody, NewRequest} = stream(Body, Request, Code), + %% When we stream we will not keep the already + %% streamed data, that would be a waste of memory. + NewLength = + case Streamed of + false -> + Length; + true -> + Length - size(Body) + end, + + NewState = next_body_chunk(State, Code), + NewMFA = {Module, whole_body, [NewBody, NewLength]}, + {noreply, NewState#state{mfa = NewMFA, + request = NewRequest}}; + {Module, decode_size, + [TotalChunk, HexList, AccHeaderSize, {MaxBodySize, BodySoFar, AccLength, MaxHeaderSize}]} - when TotalChunk =/= <<>> orelse BodySoFar =/= <<>> -> - ?hcrd("data processed - decode_data", []), - %% The response body is chunk-encoded. Steal decoded - %% chunks as much as possible to stream. - ChunkSizeToSteal = min(ChunkSize, byte_size(TotalChunk)), - <<StolenChunk:ChunkSizeToSteal/binary, NewTotalChunk/binary>> = TotalChunk, - StolenBody = <<BodySoFar/binary, StolenChunk/binary>>, - NewChunkSize = ChunkSize - ChunkSizeToSteal, - {_, Code, _} = StatusLine, - - {_, NewBody, NewRequest} = stream(StolenBody, Request, Code), - NewState = next_body_chunk(State, Code), - NewMFA = {Module, decode_data, - [NewChunkSize, NewTotalChunk, + when BodySoFar =/= <<>> -> + %% The response body is chunk-encoded. Steal decoded + %% chunks as much as possible to stream. + {_, Code, _} = StatusLine, + {_, NewBody, NewRequest} = stream(BodySoFar, Request, Code), + NewState = next_body_chunk(State, Code), + NewMFA = {Module, decode_size, + [TotalChunk, HexList, AccHeaderSize, {MaxBodySize, NewBody, AccLength, MaxHeaderSize}]}, + {noreply, NewState#state{mfa = NewMFA, + request = NewRequest}}; + {Module, decode_data, + [ChunkSize, TotalChunk, + {MaxBodySize, BodySoFar, AccLength, MaxHeaderSize}]} + when TotalChunk =/= <<>> orelse BodySoFar =/= <<>> -> + %% The response body is chunk-encoded. Steal decoded + %% chunks as much as possible to stream. + ChunkSizeToSteal = min(ChunkSize, byte_size(TotalChunk)), + <<StolenChunk:ChunkSizeToSteal/binary, NewTotalChunk/binary>> = TotalChunk, + StolenBody = <<BodySoFar/binary, StolenChunk/binary>>, + NewChunkSize = ChunkSize - ChunkSizeToSteal, + {_, Code, _} = StatusLine, + + {_, NewBody, NewRequest} = stream(StolenBody, Request, Code), + NewState = next_body_chunk(State, Code), + NewMFA = {Module, decode_data, + [NewChunkSize, NewTotalChunk, + {MaxBodySize, NewBody, AccLength, MaxHeaderSize}]}, {noreply, NewState#state{mfa = NewMFA, request = NewRequest}}; - NewMFA -> - ?hcrd("data processed - new mfa", []), - activate_once(Session), - {noreply, State#state{mfa = NewMFA}} - catch - _:_Reason -> - ?hcrd("data processing exit", [{exit, _Reason}]), - ClientReason = {could_not_parse_as_http, Data}, - ClientErrMsg = httpc_response:error(Request, ClientReason), - NewState = answer_request(Request, ClientErrMsg, State), - {stop, normal, NewState} - end, - ?hcri("data processed", [{final_result, FinalResult}]), - FinalResult; - + NewMFA -> + activate_once(Session), + {noreply, State#state{mfa = NewMFA}} + catch + Class:Reason:ST -> + ClientReason = {could_not_parse_as_http, Data}, + ClientErrMsg = httpc_response:error(Request, ClientReason), + NewState = answer_request(Request, ClientErrMsg, State), + {stop, {shutdown, {{Class, Reason}, ST}}, NewState} + end; -handle_info({Proto, Socket, Data}, +do_handle_info({Proto, Socket, Data}, #state{mfa = MFA, request = Request, session = Session, @@ -566,200 +601,111 @@ handle_info({Proto, Socket, Data}, {noreply, State}; - %% The Server may close the connection to indicate that the %% whole body is now sent instead of sending an length %% indicator. -handle_info({tcp_closed, _}, State = #state{mfa = {_, whole_body, Args}}) -> +do_handle_info({tcp_closed, _}, State = #state{mfa = {_, whole_body, Args}}) -> handle_response(State#state{body = hd(Args)}); -handle_info({ssl_closed, _}, State = #state{mfa = {_, whole_body, Args}}) -> +do_handle_info({ssl_closed, _}, State = #state{mfa = {_, whole_body, Args}}) -> handle_response(State#state{body = hd(Args)}); %%% Server closes idle pipeline -handle_info({tcp_closed, _}, State = #state{request = undefined}) -> +do_handle_info({tcp_closed, _}, State = #state{request = undefined}) -> {stop, normal, State}; -handle_info({ssl_closed, _}, State = #state{request = undefined}) -> +do_handle_info({ssl_closed, _}, State = #state{request = undefined}) -> {stop, normal, State}; %%% Error cases -handle_info({tcp_closed, _}, #state{session = Session0} = State) -> +do_handle_info({tcp_closed, _}, #state{session = Session0} = State) -> Socket = Session0#session.socket, Session = Session0#session{socket = {remote_close, Socket}}, %% {stop, session_remotly_closed, State}; {stop, normal, State#state{session = Session}}; -handle_info({ssl_closed, _}, #state{session = Session0} = State) -> +do_handle_info({ssl_closed, _}, #state{session = Session0} = State) -> Socket = Session0#session.socket, Session = Session0#session{socket = {remote_close, Socket}}, %% {stop, session_remotly_closed, State}; {stop, normal, State#state{session = Session}}; -handle_info({tcp_error, _, _} = Reason, State) -> +do_handle_info({tcp_error, _, _} = Reason, State) -> {stop, Reason, State}; -handle_info({ssl_error, _, _} = Reason, State) -> +do_handle_info({ssl_error, _, _} = Reason, State) -> {stop, Reason, State}; %% Timeouts %% Internally, to a request handling process, a request timeout is %% seen as a canceled request. -handle_info({timeout, RequestId}, +do_handle_info({timeout, RequestId}, #state{request = #request{id = RequestId} = Request, canceled = Canceled, profile_name = ProfileName} = State) -> - ?hcri("timeout of current request", [{id, RequestId}]), httpc_response:send(Request#request.from, httpc_response:error(Request, timeout)), httpc_manager:request_done(RequestId, ProfileName), - ?hcrv("response (timeout) sent - now terminate", []), {stop, normal, State#state{request = Request#request{from = answer_sent}, canceled = [RequestId | Canceled]}}; -handle_info({timeout, RequestId}, +do_handle_info({timeout, RequestId}, #state{canceled = Canceled, profile_name = ProfileName} = State) -> - ?hcri("timeout", [{id, RequestId}]), Filter = fun(#request{id = Id, from = From} = Request) when Id =:= RequestId -> - ?hcrv("found request", [{id, Id}, {from, From}]), %% Notify the owner httpc_response:send(From, httpc_response:error(Request, timeout)), httpc_manager:request_done(RequestId, ProfileName), - ?hcrv("response (timeout) sent", []), [Request#request{from = answer_sent}]; (_) -> true end, case State#state.status of pipeline -> - ?hcrd("pipeline", []), Pipeline = queue:filter(Filter, State#state.pipeline), {noreply, State#state{canceled = [RequestId | Canceled], pipeline = Pipeline}}; keep_alive -> - ?hcrd("keep_alive", []), KeepAlive = queue:filter(Filter, State#state.keep_alive), {noreply, State#state{canceled = [RequestId | Canceled], keep_alive = KeepAlive}} end; -handle_info(timeout_queue, State = #state{request = undefined}) -> +do_handle_info(timeout_queue, State = #state{request = undefined}) -> {stop, normal, State}; %% Timing was such as the queue_timeout was not canceled! -handle_info(timeout_queue, #state{timers = Timers} = State) -> +do_handle_info(timeout_queue, #state{timers = Timers} = State) -> {noreply, State#state{timers = Timers#timers{queue_timer = undefined}}}; %% Setting up the connection to the server somehow failed. -handle_info({init_error, Tag, ClientErrMsg}, +do_handle_info({init_error, Reason, ClientErrMsg}, State = #state{request = Request}) -> - ?hcrv("init error", [{tag, Tag}, {client_error, ClientErrMsg}]), NewState = answer_request(Request, ClientErrMsg, State), - {stop, normal, NewState}; - + {stop, {shutdown, Reason}, NewState}; %%% httpc_manager process dies. -handle_info({'EXIT', _, _}, State = #state{request = undefined}) -> +do_handle_info({'EXIT', _, _}, State = #state{request = undefined}) -> {stop, normal, State}; %%Try to finish the current request anyway, %% there is a fairly high probability that it can be done successfully. %% Then close the connection, hopefully a new manager is started that %% can retry requests in the pipeline. -handle_info({'EXIT', _, _}, State) -> +do_handle_info({'EXIT', _, _}, State) -> {noreply, State#state{status = close}}. - - -%%-------------------------------------------------------------------- -%% Function: terminate(Reason, State) -> _ (ignored by gen_server) -%% Description: Shutdown the httpc_handler -%%-------------------------------------------------------------------- - -%% Init error there is no socket to be closed. -terminate(normal, - #state{request = Request, - session = {send_failed, AReason} = Reason} = State) -> - ?hcrd("terminate", [{send_reason, AReason}, {request, Request}]), - maybe_send_answer(Request, - httpc_response:error(Request, Reason), - State), - ok; - -terminate(normal, - #state{request = Request, - session = {connect_failed, AReason} = Reason} = State) -> - ?hcrd("terminate", [{connect_reason, AReason}, {request, Request}]), - maybe_send_answer(Request, - httpc_response:error(Request, Reason), - State), - ok; - -terminate(normal, #state{session = undefined}) -> - ok; - -%% Init error sending, no session information has been setup but -%% there is a socket that needs closing. -terminate(normal, - #state{session = #session{id = undefined} = Session}) -> - close_socket(Session); - -%% Socket closed remotely -terminate(normal, - #state{session = #session{socket = {remote_close, Socket}, - socket_type = SocketType, - id = Id}, - profile_name = ProfileName, - request = Request, - timers = Timers, - pipeline = Pipeline, - keep_alive = KeepAlive} = State) -> - ?hcrt("terminate(normal) - remote close", - [{id, Id}, {profile, ProfileName}]), - - %% Clobber session - (catch httpc_manager:delete_session(Id, ProfileName)), - maybe_retry_queue(Pipeline, State), - maybe_retry_queue(KeepAlive, State), - - %% Cancel timers - cancel_timers(Timers), - - %% Maybe deliver answers to requests - deliver_answer(Request), - - %% And, just in case, close our side (**really** overkill) - http_transport:close(SocketType, Socket); - -terminate(Reason, #state{session = #session{id = Id, - socket = Socket, - socket_type = SocketType}, - request = undefined, - profile_name = ProfileName, - timers = Timers, - pipeline = Pipeline, - keep_alive = KeepAlive} = State) -> - ?hcrt("terminate", - [{id, Id}, {profile, ProfileName}, {reason, Reason}]), - - %% Clobber session - (catch httpc_manager:delete_session(Id, ProfileName)), - - maybe_retry_queue(Pipeline, State), - maybe_retry_queue(KeepAlive, State), - - cancel_timer(Timers#timers.queue_timer, timeout_queue), - http_transport:close(SocketType, Socket); - -terminate(Reason, #state{request = undefined}) -> - ?hcrt("terminate", [{reason, Reason}]), - ok; +call(Msg, Pid) -> + try gen_server:call(Pid, Msg, infinity) + catch + exit:{noproc, _} -> + {error, closed}; + exit:{normal, _} -> + {error, closed}; + exit:{{shutdown, _},_} -> + {error, closed} + end. -terminate(Reason, #state{request = Request} = State) -> - ?hcrd("terminate", [{reason, Reason}, {request, Request}]), - NewState = maybe_send_answer(Request, - httpc_response:error(Request, Reason), - State), - terminate(Reason, NewState#state{request = undefined}). +cast(Msg, Pid) -> + gen_server:cast(Pid, Msg). maybe_retry_queue(Q, State) -> case queue:is_empty(Q) of @@ -774,86 +720,13 @@ maybe_send_answer(#request{from = answer_sent}, _Reason, State) -> maybe_send_answer(Request, Answer, State) -> answer_request(Request, Answer, State). -deliver_answer(#request{id = Id, from = From} = Request) - when is_pid(From) -> +deliver_answer(#request{from = From} = Request) + when From =/= answer_sent -> Response = httpc_response:error(Request, socket_closed_remotely), - ?hcrd("deliver answer", [{id, Id}, {from, From}, {response, Response}]), httpc_response:send(From, Response); -deliver_answer(Request) -> - ?hcrd("skip deliver answer", [{request, Request}]), +deliver_answer(_Request) -> ok. - -%%-------------------------------------------------------------------- -%% Func: code_change(_OldVsn, State, Extra) -> {ok, NewState} -%% Purpose: Convert process state when code is changed -%%-------------------------------------------------------------------- - -code_change(_, - #state{session = OldSession, - profile_name = ProfileName} = State, - upgrade_from_pre_5_8_1) -> - case OldSession of - {session, - Id, ClientClose, Scheme, Socket, SocketType, QueueLen, Type} -> - NewSession = #session{id = Id, - client_close = ClientClose, - scheme = Scheme, - socket = Socket, - socket_type = SocketType, - queue_length = QueueLen, - type = Type}, - insert_session(NewSession, ProfileName), - {ok, State#state{session = NewSession}}; - _ -> - {ok, State} - end; - -code_change(_, - #state{session = OldSession, - profile_name = ProfileName} = State, - downgrade_to_pre_5_8_1) -> - case OldSession of - #session{id = Id, - client_close = ClientClose, - scheme = Scheme, - socket = Socket, - socket_type = SocketType, - queue_length = QueueLen, - type = Type} -> - NewSession = {session, - Id, ClientClose, Scheme, Socket, SocketType, - QueueLen, Type}, - insert_session(NewSession, ProfileName), - {ok, State#state{session = NewSession}}; - _ -> - {ok, State} - end; - -code_change(_, State, _) -> - {ok, State}. - - -%% new_http_options({http_options, TimeOut, AutoRedirect, SslOpts, -%% Auth, Relaxed}) -> -%% {http_options, "HTTP/1.1", TimeOut, AutoRedirect, SslOpts, -%% Auth, Relaxed}. - -%% old_http_options({http_options, _, TimeOut, AutoRedirect, -%% SslOpts, Auth, Relaxed}) -> -%% {http_options, TimeOut, AutoRedirect, SslOpts, Auth, Relaxed}. - -%% new_queue(Queue, Fun) -> -%% List = queue:to_list(Queue), -%% NewList = -%% lists:map(fun(Request) -> -%% Settings = -%% Fun(Request#request.settings), -%% Request#request{settings = Settings} -%% end, List), -%% queue:from_list(NewList). - - %%%-------------------------------------------------------------------- %%% Internal functions %%%-------------------------------------------------------------------- @@ -862,6 +735,7 @@ connect(SocketType, ToAddress, #options{ipfamily = IpFamily, ip = FromAddress, port = FromPort, + unix_socket = UnixSocket, socket_opts = Opts0}, Timeout) -> Opts1 = case FromPort of @@ -897,6 +771,16 @@ connect(SocketType, ToAddress, OK -> OK end; + local -> + Opts3 = [IpFamily | Opts2], + SocketAddr = {local, UnixSocket}, + case http_transport:connect(SocketType, {SocketAddr, 0}, Opts3, Timeout) of + {error, Reason} -> + {error, {failed_connect, [{to_address, SocketAddr}, + {IpFamily, Opts3, Reason}]}}; + Else -> + Else + end; _ -> Opts3 = [IpFamily | Opts2], case http_transport:connect(SocketType, ToAddress, Opts3, Timeout) of @@ -908,34 +792,42 @@ connect(SocketType, ToAddress, end end. -connect_and_send_first_request(Address, Request, #state{options = Options} = State) -> +handle_unix_socket_options(#request{unix_socket = UnixSocket}, Options) + when UnixSocket =:= undefined -> + Options; + +handle_unix_socket_options(#request{unix_socket = UnixSocket}, + Options = #options{ipfamily = IpFamily}) -> + case IpFamily of + local -> + Options#options{unix_socket = UnixSocket}; + Else -> + error({badarg, [{ipfamily, Else}, {unix_socket, UnixSocket}]}) + end. + +connect_and_send_first_request(Address, Request, #state{options = Options0} = State) -> SocketType = socket_type(Request), ConnTimeout = (Request#request.settings)#http_options.connect_timeout, - ?hcri("connect", - [{address, Address}, {request, Request}, {options, Options}]), + Options = handle_unix_socket_options(Request, Options0), case connect(SocketType, Address, Options, ConnTimeout) of {ok, Socket} -> ClientClose = - httpc_request:is_client_closing( - Request#request.headers), + httpc_request:is_client_closing( + Request#request.headers), SessionType = httpc_manager:session_type(Options), SocketType = socket_type(Request), Session = #session{id = {Request#request.address, self()}, scheme = Request#request.scheme, socket = Socket, - socket_type = SocketType, - client_close = ClientClose, - type = SessionType}, - ?hcri("connected - now send first request", [{socket, Socket}]), - + socket_type = SocketType, + client_close = ClientClose, + type = SessionType}, case httpc_request:send(Address, Session, Request) of ok -> - ?hcri("first request sent", []), TmpState = State#state{request = Request, session = Session, mfa = init_mfa(Request, State), - status_line = - init_status_line(Request), + status_line = init_status_line(Request), headers = undefined, body = undefined, status = new}, @@ -947,8 +839,7 @@ connect_and_send_first_request(Address, Request, #state{options = Options} = Sta self() ! {init_error, error_sending, httpc_response:error(Request, Reason)}, {ok, State#state{request = Request, - session = - #session{socket = Socket}}} + session = Session}} end; {error, Reason} -> self() ! {init_error, error_connecting, @@ -956,9 +847,10 @@ connect_and_send_first_request(Address, Request, #state{options = Options} = Sta {ok, State#state{request = Request}} end. -connect_and_send_upgrade_request(Address, Request, #state{options = Options} = State) -> +connect_and_send_upgrade_request(Address, Request, #state{options = Options0} = State) -> ConnTimeout = (Request#request.settings)#http_options.connect_timeout, SocketType = ip_comm, + Options = handle_unix_socket_options(Request, Options0), case connect(SocketType, Address, Options, ConnTimeout) of {ok, Socket} -> SessionType = httpc_manager:session_type(Options), @@ -990,12 +882,6 @@ handler_info(#state{request = Request, options = _Options, timers = _Timers} = _State) -> - ?hcrt("handler info", [{request, Request}, - {session, Session}, - {pipeline, Pipeline}, - {keep_alive, KeepAlive}, - {status, Status}]), - %% Info about the current request RequestInfo = case Request of @@ -1006,8 +892,6 @@ handler_info(#state{request = Request, [{id, Id}, {started, ReqStarted}] end, - ?hcrt("handler info", [{request_info, RequestInfo}]), - %% Info about the current session/socket SessionType = Session#session.type, QueueLen = case SessionType of @@ -1020,22 +904,12 @@ handler_info(#state{request = Request, Socket = Session#session.socket, SocketType = Session#session.socket_type, - ?hcrt("handler info", [{session_type, SessionType}, - {queue_length, QueueLen}, - {scheme, Scheme}, - {socket, Socket}]), - SocketOpts = http_transport:getopts(SocketType, Socket), SocketStats = http_transport:getstat(SocketType, Socket), Remote = http_transport:peername(SocketType, Socket), Local = http_transport:sockname(SocketType, Socket), - ?hcrt("handler info", [{remote, Remote}, - {local, Local}, - {socket_opts, SocketOpts}, - {socket_stats, SocketStats}]), - SocketInfo = [{remote, Remote}, {local, Local}, {socket_opts, SocketOpts}, @@ -1055,7 +929,6 @@ handler_info(#state{request = Request, handle_http_msg({Version, StatusCode, ReasonPharse, Headers, Body}, State = #state{request = Request}) -> - ?hcrt("handle_http_msg", [{headers, Headers}]), case Headers#http_response_h.'content-type' of "multipart/byteranges" ++ _Param -> exit({not_yet_implemented, multypart_byteranges}); @@ -1069,15 +942,12 @@ handle_http_msg({Version, StatusCode, ReasonPharse, Headers, Body}, end; handle_http_msg({ChunkedHeaders, Body}, #state{status_line = {_, Code, _}, headers = Headers} = State) -> - ?hcrt("handle_http_msg", - [{chunked_headers, ChunkedHeaders}, {headers, Headers}]), NewHeaders = http_chunk:handle_headers(Headers, ChunkedHeaders), {_, NewBody, NewRequest} = stream(Body, State#state.request, Code), handle_response(State#state{headers = NewHeaders, body = NewBody, request = NewRequest}); handle_http_msg(Body, #state{status_line = {_,Code, _}} = State) -> - ?hcrt("handle_http_msg", [{code, Code}]), {_, NewBody, NewRequest} = stream(Body, State#state.request, Code), handle_response(State#state{body = NewBody, request = NewRequest}). @@ -1091,42 +961,39 @@ handle_http_body(_, #state{status = {ssl_tunnel, Request}, NewState = answer_request(Request, ClientErrMsg, State), {stop, normal, NewState}; -handle_http_body(<<>>, #state{status_line = {_,304, _}} = State) -> - ?hcrt("handle_http_body - 304", []), +%% All 1xx (informational), 204 (no content), and 304 (not modified) +%% responses MUST NOT include a message-body, and thus are always +%% terminated by the first empty line after the header fields. +%% This implies that chunked encoding MUST NOT be used for these +%% status codes. +handle_http_body(<<>>, #state{headers = Headers, + status_line = {_,StatusCode, _}} = State) + when Headers#http_response_h.'transfer-encoding' =/= "chunked" andalso + (StatusCode =:= 204 orelse %% No Content + StatusCode =:= 304 orelse %% Not Modified + 100 =< StatusCode andalso StatusCode =< 199) -> %% Informational handle_response(State#state{body = <<>>}); -handle_http_body(<<>>, #state{status_line = {_,204, _}} = State) -> - ?hcrt("handle_http_body - 204", []), - handle_response(State#state{body = <<>>}); -handle_http_body(<<>>, #state{request = #request{method = head}} = State) -> - ?hcrt("handle_http_body - head", []), +handle_http_body(<<>>, #state{headers = Headers, + request = #request{method = head}} = State) + when Headers#http_response_h.'transfer-encoding' =/= "chunked" -> handle_response(State#state{body = <<>>}); handle_http_body(Body, #state{headers = Headers, max_body_size = MaxBodySize, status_line = {_,Code, _}, request = Request} = State) -> - ?hcrt("handle_http_body", - [{max_body_size, MaxBodySize}, {headers, Headers}, {code, Code}]), TransferEnc = Headers#http_response_h.'transfer-encoding', case case_insensitive_header(TransferEnc) of "chunked" -> - ?hcrt("handle_http_body - chunked", []), try http_chunk:decode(Body, State#state.max_body_size, State#state.max_header_size) of {Module, Function, Args} -> - ?hcrt("handle_http_body - new mfa", - [{module, Module}, - {function, Function}, - {args, Args}]), NewState = next_body_chunk(State, Code), {noreply, NewState#state{mfa = {Module, Function, Args}}}; {ok, {ChunkedHeaders, NewBody}} -> - ?hcrt("handle_http_body - new body", - [{chunked_headers, ChunkedHeaders}, - {new_body, NewBody}]), NewHeaders = http_chunk:handle_headers(Headers, ChunkedHeaders), case Body of @@ -1148,7 +1015,6 @@ handle_http_body(Body, #state{headers = Headers, {stop, normal, NewState} end; Enc when Enc =:= "identity"; Enc =:= undefined -> - ?hcrt("handle_http_body - identity", []), Length = list_to_integer(Headers#http_response_h.'content-length'), case ((Length =< MaxBodySize) orelse (MaxBodySize =:= nolimit)) of @@ -1172,7 +1038,6 @@ handle_http_body(Body, #state{headers = Headers, {stop, normal, NewState} end; Encoding when is_list(Encoding) -> - ?hcrt("handle_http_body - other", [{encoding, Encoding}]), NewState = answer_request(Request, httpc_response:error(Request, unknown_encoding), @@ -1184,27 +1049,19 @@ handle_response(#state{status = new} = State) -> ?hcrd("handle response - status = new", []), handle_response(try_to_enable_pipeline_or_keep_alive(State)); -handle_response(#state{request = Request, - status = Status, - session = Session, - status_line = StatusLine, - headers = Headers, - body = Body, - options = Options, - profile_name = ProfileName} = State) - when Status =/= new -> - - ?hcrd("handle response", [{profile, ProfileName}, - {status, Status}, - {request, Request}, - {session, Session}, - {status_line, StatusLine}]), - +handle_response(#state{status = Status0} = State0) when Status0 =/= new -> + State = handle_server_closing(State0), + #state{request = Request, + session = Session, + status_line = StatusLine, + headers = Headers, + body = Body, + options = Options, + profile_name = ProfileName} = State, handle_cookies(Headers, Request, Options, ProfileName), case httpc_response:result({StatusLine, Headers, Body}, Request) of %% 100-continue continue -> - ?hcrd("handle response - continue", []), %% Send request body {_, RequestBody} = Request#request.content, send_raw(Session, RequestBody), @@ -1221,7 +1078,6 @@ handle_response(#state{request = Request, %% Ignore unexpected 100-continue response and receive the %% actual response that the server will send right away. {ignore, Data} -> - ?hcrd("handle response - ignore", [{data, Data}]), Relaxed = (Request#request.settings)#http_options.relaxed, MFA = {httpc_response, parse, [State#state.max_header_size, Relaxed]}, @@ -1235,23 +1091,17 @@ handle_response(#state{request = Request, %% obsolete and the manager will create a new request %% with the same id as the current. {redirect, NewRequest, Data} -> - ?hcrt("handle response - redirect", - [{new_request, NewRequest}, {data, Data}]), ok = httpc_manager:redirect_request(NewRequest, ProfileName), handle_queue(State#state{request = undefined}, Data); {retry, TimeNewRequest, Data} -> - ?hcrt("handle response - retry", - [{time_new_request, TimeNewRequest}, {data, Data}]), ok = httpc_manager:retry_request(TimeNewRequest, ProfileName), handle_queue(State#state{request = undefined}, Data); {ok, Msg, Data} -> - ?hcrd("handle response - ok", []), stream_remaining_body(Body, Request, StatusLine), end_stream(StatusLine, Request), NewState = maybe_send_answer(Request, Msg, State), handle_queue(NewState, Data); {stop, Msg} -> - ?hcrd("handle response - stop", [{msg, Msg}]), end_stream(StatusLine, Request), NewState = maybe_send_answer(Request, Msg, State), {stop, normal, NewState} @@ -1286,28 +1136,19 @@ handle_pipeline(#state{status = pipeline, profile_name = ProfileName, options = #options{pipeline_timeout = TimeOut}} = State, Data) -> - - ?hcrd("handle pipeline", [{profile, ProfileName}, - {session, Session}, - {timeout, TimeOut}]), - case queue:out(State#state.pipeline) of {empty, _} -> - ?hcrd("pipeline queue empty", []), handle_empty_queue(Session, ProfileName, TimeOut, State); {{value, NextRequest}, Pipeline} -> - ?hcrd("pipeline queue non-empty", []), case lists:member(NextRequest#request.id, State#state.canceled) of true -> - ?hcrv("next request had been cancelled", []), %% See comment for handle_cast({cancel, RequestId}) {stop, normal, State#state{request = NextRequest#request{from = answer_sent}, pipeline = Pipeline}}; false -> - ?hcrv("next request", [{request, NextRequest}]), NewSession = Session#session{queue_length = %% Queue + current @@ -1324,25 +1165,16 @@ handle_keep_alive_queue(#state{status = keep_alive, options = #options{keep_alive_timeout = TimeOut, proxy = Proxy}} = State, Data) -> - - ?hcrd("handle keep_alive", [{profile, ProfileName}, - {session, Session}, - {timeout, TimeOut}]), - case queue:out(State#state.keep_alive) of {empty, _} -> - ?hcrd("keep_alive queue empty", []), handle_empty_queue(Session, ProfileName, TimeOut, State); {{value, NextRequest}, KeepAlive} -> - ?hcrd("keep_alive queue non-empty", []), case lists:member(NextRequest#request.id, State#state.canceled) of true -> - ?hcrv("next request has already been canceled", []), handle_keep_alive_queue( State#state{keep_alive = KeepAlive}, Data); false -> - ?hcrv("next request", [{request, NextRequest}]), #request{address = Addr} = NextRequest, Address = handle_proxy(Addr, Proxy), case httpc_request:send(Address, Session, NextRequest) of @@ -1355,7 +1187,6 @@ handle_keep_alive_queue(#state{status = keep_alive, end end end. - handle_empty_queue(Session, ProfileName, TimeOut, State) -> %% The server may choose too terminate an idle pipline| keep_alive session %% in this case we want to receive the close message @@ -1365,17 +1196,20 @@ handle_empty_queue(Session, ProfileName, TimeOut, State) -> %% If a pipline | keep_alive session has been idle for some time is not %% closed by the server, the client may want to close it. NewState = activate_queue_timeout(TimeOut, State), - update_session(ProfileName, Session, #session.queue_length, 0), - %% Note mfa will be initialized when a new request - %% arrives. - {noreply, - NewState#state{request = undefined, - mfa = undefined, - status_line = undefined, - headers = undefined, - body = undefined - } - }. + case update_session(ProfileName, Session, #session.queue_length, 0) of + {stop, Reason} -> + {stop, {shutdown, Reason}, State}; + _ -> + %% Note mfa will be initialized when a new request + %% arrives. + {noreply, + NewState#state{request = undefined, + mfa = undefined, + status_line = undefined, + headers = undefined, + body = undefined + }} + end. receive_response(Request, Session, Data, State) -> NewState = init_wait_for_response_state(Request, State), @@ -1391,7 +1225,6 @@ init_wait_for_response_state(Request, State) -> status_line = undefined, headers = undefined, body = undefined}. - gather_data(<<>>, Session, State) -> activate_once(Session), {noreply, State}; @@ -1415,29 +1248,29 @@ close_socket(#session{socket = Socket, socket_type = SocketType}) -> http_transport:close(SocketType, Socket). activate_request_timeout( - #state{request = #request{timer = undefined} = Request} = State) -> + #state{request = #request{timer = OldRef} = Request} = State) -> Timeout = (Request#request.settings)#http_options.timeout, case Timeout of infinity -> State; _ -> ReqId = Request#request.id, - ?hcrt("activate request timer", - [{request_id, ReqId}, - {time_consumed, t() - Request#request.started}, - {timeout, Timeout}]), Msg = {timeout, ReqId}, + case OldRef of + undefined -> + ok; + _ -> + %% Timer is already running! This is the case for a redirect or retry + %% We need to restart the timer because the handler pid has changed + cancel_timer(OldRef, Msg) + end, Ref = erlang:send_after(Timeout, self(), Msg), Request2 = Request#request{timer = Ref}, ReqTimers = [{Request#request.id, Ref} | (State#state.timers)#timers.request_timers], Timers = #timers{request_timers = ReqTimers}, State#state{request = Request2, timers = Timers} - end; - -%% Timer is already running! This is the case for a redirect or retry -activate_request_timeout(State) -> - State. + end. activate_queue_timeout(infinity, State) -> State; @@ -1468,10 +1301,6 @@ try_to_enable_pipeline_or_keep_alive( status_line = {Version, _, _}, headers = Headers, profile_name = ProfileName} = State) -> - ?hcrd("try to enable pipeline or keep-alive", - [{version, Version}, - {headers, Headers}, - {session, Session}]), case is_keep_alive_enabled_server(Version, Headers) andalso is_keep_alive_connection(Headers, Session) of true -> @@ -1492,11 +1321,18 @@ try_to_enable_pipeline_or_keep_alive( State#state{status = close} end. +handle_server_closing(State = #state{status = close}) -> State; +handle_server_closing(State = #state{headers = undefined}) -> State; +handle_server_closing(State = #state{headers = Headers}) -> + case httpc_response:is_server_closing(Headers) of + true -> State#state{status = close}; + false -> State + end. + answer_request(#request{id = RequestId, from = From} = Request, Msg, #state{session = Session, timers = Timers, profile_name = ProfileName} = State) -> - ?hcrt("answer request", [{request, Request}, {msg, Msg}]), httpc_response:send(From, Msg), RequestTimers = Timers#timers.request_timers, TimerRef = @@ -1643,42 +1479,32 @@ socket_type(#request{scheme = http}) -> ip_comm; socket_type(#request{scheme = https, settings = Settings}) -> Settings#http_options.ssl. -%% socket_type(http) -> -%% ip_comm; -%% socket_type(https) -> -%% {ssl1, []}. %% Dummy value ok for ex setopts that does not use this value start_stream({_Version, _Code, _ReasonPhrase}, _Headers, #request{stream = none} = Request) -> - ?hcrt("start stream - none", []), {ok, Request}; start_stream({_Version, Code, _ReasonPhrase}, Headers, #request{stream = self} = Request) when ?IS_STREAMED(Code) -> - ?hcrt("start stream - self", [{code, Code}]), Msg = httpc_response:stream_start(Headers, Request, ignore), httpc_response:send(Request#request.from, Msg), {ok, Request}; start_stream({_Version, Code, _ReasonPhrase}, Headers, #request{stream = {self, once}} = Request) when ?IS_STREAMED(Code) -> - ?hcrt("start stream - self:once", [{code, Code}]), Msg = httpc_response:stream_start(Headers, Request, self()), httpc_response:send(Request#request.from, Msg), {ok, Request}; start_stream({_Version, Code, _ReasonPhrase}, _Headers, #request{stream = Filename} = Request) when ?IS_STREAMED(Code) andalso is_list(Filename) -> - ?hcrt("start stream", [{code, Code}, {filename, Filename}]), case file:open(Filename, [write, raw, append, delayed_write]) of {ok, Fd} -> - ?hcri("start stream - file open ok", [{fd, Fd}]), {ok, Request#request{stream = Fd}}; {error, Reason} -> exit({stream_to_file_failed, Reason}) end; start_stream(_StatusLine, _Headers, Request) -> - ?hcrt("start stream - no op", []), {ok, Request}. stream_remaining_body(<<>>, _, _) -> @@ -1689,16 +1515,12 @@ stream_remaining_body(Body, Request, {_, Code, _}) -> %% Note the end stream message is handled by httpc_response and will %% be sent by answer_request end_stream(_, #request{stream = none}) -> - ?hcrt("end stream - none", []), ok; end_stream(_, #request{stream = self}) -> - ?hcrt("end stream - self", []), ok; end_stream(_, #request{stream = {self, once}}) -> - ?hcrt("end stream - self:once", []), ok; end_stream({_,200,_}, #request{stream = Fd}) -> - ?hcrt("end stream - 200", [{stream, Fd}]), case file:close(Fd) of ok -> ok; @@ -1706,15 +1528,13 @@ end_stream({_,200,_}, #request{stream = Fd}) -> file:close(Fd) end; end_stream({_,206,_}, #request{stream = Fd}) -> - ?hcrt("end stream - 206", [{stream, Fd}]), case file:close(Fd) of ok -> ok; {error, enospc} -> % Could be due to delayed_write file:close(Fd) end; -end_stream(SL, R) -> - ?hcrt("end stream", [{status_line, SL}, {request, R}]), +end_stream(_, _) -> ok. @@ -1743,11 +1563,8 @@ handle_verbose(trace) -> handle_verbose(_) -> ok. - - send_raw(#session{socket = Socket, socket_type = SocketType}, {ProcessBody, Acc}) when is_function(ProcessBody, 1) -> - ?hcrt("send raw", [{acc, Acc}]), send_raw(SocketType, Socket, ProcessBody, Acc); send_raw(#session{socket = Socket, socket_type = SocketType}, Body) -> http_transport:send(SocketType, Socket, Body). @@ -1758,7 +1575,6 @@ send_raw(SocketType, Socket, ProcessBody, Acc) -> ok; {ok, Data, NewAcc} -> DataBin = iolist_to_binary(Data), - ?hcrd("send", [{data, DataBin}]), case http_transport:send(SocketType, Socket, DataBin) of ok -> send_raw(SocketType, Socket, ProcessBody, NewAcc); @@ -1790,14 +1606,16 @@ tls_tunnel(Address, Request, #state{session = #session{socket = Socket, tls_tunnel_request(#request{headers = Headers, settings = Options, + id = RequestId, + from = From, address = {Host, Port}= Adress, ipv6_host_with_brackets = IPV6}) -> URI = Host ++":" ++ integer_to_list(Port), #request{ - id = make_ref(), - from = self(), + id = RequestId, + from = From, scheme = http, %% Use tcp-first and then upgrade! address = Adress, path = URI, @@ -1887,11 +1705,13 @@ update_session(ProfileName, #session{id = SessionId} = Session, Pos, Value) -> httpc_manager:update_session(ProfileName, SessionId, Pos, Value) end catch - error:undef -> % This could happen during code upgrade + error:undef -> %% This could happen during code upgrade Session2 = erlang:setelement(Pos, Session, Value), insert_session(Session2, ProfileName); - T:E -> - Stacktrace = erlang:get_stacktrace(), + error:badarg -> + {stop, normal}; + T:E:Stacktrace -> + %% Unexpected this must be an error! error_logger:error_msg("Failed updating session: " "~n ProfileName: ~p" "~n SessionId: ~p" @@ -1908,27 +1728,14 @@ update_session(ProfileName, #session{id = SessionId} = Session, Pos, Value) -> Session, (catch httpc_manager:lookup_session(SessionId, ProfileName)), T, E]), - exit({failed_updating_session, - [{profile, ProfileName}, - {session_id, SessionId}, - {pos, Pos}, - {value, Value}, - {etype, T}, - {error, E}, - {stacktrace, Stacktrace}]}) + {stop, {failed_updating_session, + [{profile, ProfileName}, + {session_id, SessionId}, + {pos, Pos}, + {value, Value}, + {etype, T}, + {error, E}, + {stacktrace, Stacktrace}]}} end. -%% --------------------------------------------------------------------- - -call(Msg, Pid) -> - Timeout = infinity, - call(Msg, Pid, Timeout). -call(Msg, Pid, Timeout) -> - gen_server:call(Pid, Msg, Timeout). - -cast(Msg, Pid) -> - gen_server:cast(Pid, Msg). - -t() -> - http_util:timestamp(). diff --git a/lib/inets/src/http_client/httpc_handler_sup.erl b/lib/inets/src/http_client/httpc_handler_sup.erl index 403512fc25..ad70afe2ae 100644 --- a/lib/inets/src/http_client/httpc_handler_sup.erl +++ b/lib/inets/src/http_client/httpc_handler_sup.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2007-2011. All Rights Reserved. +%% Copyright Ericsson AB 2007-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/lib/inets/src/http_client/httpc_internal.hrl b/lib/inets/src/http_client/httpc_internal.hrl index bb8c0e20fa..a14d8aa71b 100644 --- a/lib/inets/src/http_client/httpc_internal.hrl +++ b/lib/inets/src/http_client/httpc_internal.hrl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2005-2014. All Rights Reserved. +%% Copyright Ericsson AB 2005-2018. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -43,32 +43,32 @@ %%% HTTP Client per request settings -record(http_options, { - %% string() - "HTTP/1.1" | "HTTP/1.0" | "HTTP/0.9" - version, + %% "HTTP/1.1" | "HTTP/1.0" | "HTTP/0.9" + version :: 'undefined' | string(), - %% integer() | infinity - ms before a request times out - timeout = ?HTTP_REQUEST_TIMEOUT, + %% ms before a request times out + timeout = ?HTTP_REQUEST_TIMEOUT :: timeout(), - %% bool() - true if auto redirect on 30x response - autoredirect = true, + %% true if auto redirect on 30x response + autoredirect = true :: boolean(), %% ssl socket options - ssl = [], + ssl = [], %% {User, Password} = {string(), string()} proxy_auth, - %% bool() - true if not strictly std compliant - relaxed = false, + %% true if not strictly std compliant + relaxed = false :: boolean(), %% integer() - ms before a connect times out - connect_timeout = ?HTTP_REQUEST_CTIMEOUT, - - %% bool() - Use %-encoding rfc 2396 - url_encode + connect_timeout = ?HTTP_REQUEST_CTIMEOUT :: timeout(), + %% Use %-encoding rfc 2396 + url_encode :: 'undefined' | boolean() } ). +-type http_options() :: #http_options{}. %%% HTTP Client per profile setting. -record(options, @@ -82,18 +82,20 @@ keep_alive_timeout = ?HTTP_KEEP_ALIVE_TIMEOUT, % Used when pipeline_timeout = 0 max_sessions = ?HTTP_MAX_TCP_SESSIONS, cookies = disabled, % enabled | disabled | verify - verbose = false, - ipfamily = inet, % inet | inet6 | inet6fb4 + verbose = false, % boolean(), + ipfamily = inet, % inet | inet6 | inet6fb4 | local ip = default, % specify local interface port = default, % specify local port - socket_opts = [] % other socket options + socket_opts = [], % other socket options + unix_socket = undefined % Local unix socket } ). +-type options() :: #options{}. %%% All data associated to a specific HTTP request -record(request, { - id, % ref() - Request Id + id :: 'undefined' | reference(), % Request Id from, % pid() - Caller redircount = 0,% Number of redirects made for this request scheme, % http | https @@ -103,7 +105,7 @@ method, % atom() - HTTP request Method headers, % #http_request_h{} content, % {ContentType, Body} - Current HTTP request - settings, % #http_options{} - User defined settings + settings :: http_options(), % User defined settings abs_uri, % string() ex: "http://www.erlang.org" userinfo, % string() - optinal "<userinfo>@<host>:<port>" stream, % boolean() - stream async reply? @@ -112,20 +114,20 @@ % for testing purposes. started, % integer() > 0 - When we started processing the % request - timer, % undefined | ref() + timer :: undefined | reference(), socket_opts, % undefined | [socket_option()] + unix_socket, % undefined | string() ipv6_host_with_brackets % boolean() } - ). - + ). +-type request() :: #request{}. -record(session, { %% {{Host, Port}, HandlerPid} id, - %% true | false - client_close, + client_close :: 'undefined' | boolean(), %% http (HTTP/TCP) | https (HTTP/SSL/TCP) scheme, @@ -140,14 +142,13 @@ queue_length = 1, %% pipeline | keep_alive (wait for response before sending new request) - type, + type :: 'undefined' | 'pipeline' | 'keep_alive', - %% true | false %% This will be true, when a response has been received for %% the first request. See type above. - available = false + available = false :: boolean() }). - +-type session() :: #session{}. -record(http_cookie, { @@ -162,7 +163,7 @@ secure = false, version = "0" }). - +-type http_cookie() :: #http_cookie{}. %% -record(parsed_uri, %% { diff --git a/lib/inets/src/http_client/httpc_manager.erl b/lib/inets/src/http_client/httpc_manager.erl index c7974836c2..0333442bf2 100644 --- a/lib/inets/src/http_client/httpc_manager.erl +++ b/lib/inets/src/http_client/httpc_manager.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2002-2014. All Rights Reserved. +%% Copyright Ericsson AB 2002-2018. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -137,7 +137,7 @@ redirect_request(Request, ProfileName) -> %%-------------------------------------------------------------------- %% Function: cancel_request(RequestId, ProfileName) -> ok -%% RequestId - ref() +%% RequestId - reference() %% ProfileName = atom() %% %% Description: Cancels the request with <RequestId>. @@ -148,7 +148,7 @@ cancel_request(RequestId, ProfileName) -> %%-------------------------------------------------------------------- %% Function: request_done(RequestId, ProfileName) -> ok -%% RequestId - ref() +%% RequestId - reference() %% ProfileName = atom() %% %% Description: Inform tha manager that a request has been completed. @@ -553,7 +553,8 @@ handle_cast({set_options, Options}, State = #state{options = OldOptions}) -> ip = get_ip(Options, OldOptions), port = get_port(Options, OldOptions), verbose = get_verbose(Options, OldOptions), - socket_opts = get_socket_opts(Options, OldOptions) + socket_opts = get_socket_opts(Options, OldOptions), + unix_socket = get_unix_socket_opts(Options, OldOptions) }, case {OldOptions#options.verbose, NewOptions#options.verbose} of {Same, Same} -> @@ -749,8 +750,26 @@ handle_request(#request{settings = start_handler(NewRequest#request{headers = NewHeaders}, State), {reply, {ok, NewRequest#request.id}, State}; -handle_request(Request, State = #state{options = Options}) -> +%% Simple socket options handling (ERL-441). +%% +%% TODO: Refactor httpc to enable sending socket options in requests +%% using persistent connections. This workaround opens a new +%% connection for each request with non-empty socket_opts. +handle_request(Request0 = #request{socket_opts = SocketOpts}, + State0 = #state{options = Options0}) + when is_list(SocketOpts) andalso length(SocketOpts) > 0 -> + Request = handle_cookies(generate_request_id(Request0), State0), + Options = convert_options(SocketOpts, Options0), + State = State0#state{options = Options}, + Headers = + (Request#request.headers)#http_request_h{connection + = "close"}, + %% Reset socket_opts to avoid setopts failure. + start_handler(Request#request{headers = Headers, socket_opts = []}, State), + %% Do not change the state + {reply, {ok, Request#request.id}, State0}; +handle_request(Request, State = #state{options = Options}) -> NewRequest = handle_cookies(generate_request_id(Request), State), SessionType = session_type(Options), case select_session(Request#request.method, @@ -774,6 +793,18 @@ handle_request(Request, State = #state{options = Options}) -> {reply, {ok, NewRequest#request.id}, State}. +%% Convert Request options to State options +convert_options([], Options) -> + Options; +convert_options([{ipfamily, Value}|T], Options) -> + convert_options(T, Options#options{ipfamily = Value}); +convert_options([{ip, Value}|T], Options) -> + convert_options(T, Options#options{ip = Value}); +convert_options([{port, Value}|T], Options) -> + convert_options(T, Options#options{port = Value}); +convert_options([Option|T], Options = #options{socket_opts = SocketOpts}) -> + convert_options(T, Options#options{socket_opts = SocketOpts ++ [Option]}). + start_handler(#request{id = Id, from = From} = Request, #state{profile_name = ProfileName, @@ -849,11 +880,11 @@ pipeline_or_keep_alive(#request{id = Id, from = From} = Request, HandlerPid, #state{handler_db = HandlerDb} = State) -> - case (catch httpc_handler:send(Request, HandlerPid)) of + case httpc_handler:send(Request, HandlerPid) of ok -> HandlerInfo = {Id, HandlerPid, From}, ets:insert(HandlerDb, HandlerInfo); - _ -> % timeout pipelining failed + {error, closed} -> % timeout pipelining failed start_handler(Request, State) end. @@ -963,7 +994,10 @@ get_option(ip, #options{ip = IP}) -> get_option(port, #options{port = Port}) -> Port; get_option(socket_opts, #options{socket_opts = SocketOpts}) -> - SocketOpts. + SocketOpts; +get_option(unix_socket, #options{unix_socket = UnixSocket}) -> + UnixSocket. + get_proxy(Opts, #options{proxy = Default}) -> proplists:get_value(proxy, Opts, Default). @@ -1016,6 +1050,8 @@ get_verbose(Opts, #options{verbose = Default}) -> get_socket_opts(Opts, #options{socket_opts = Default}) -> proplists:get_value(socket_opts, Opts, Default). +get_unix_socket_opts(Opts, #options{unix_socket = Default}) -> + proplists:get_value(unix_socket, Opts, Default). handle_verbose(debug) -> dbg:p(self(), [call]), diff --git a/lib/inets/src/http_client/httpc_profile_sup.erl b/lib/inets/src/http_client/httpc_profile_sup.erl index b83aeaa4e0..334f2eb1c7 100644 --- a/lib/inets/src/http_client/httpc_profile_sup.erl +++ b/lib/inets/src/http_client/httpc_profile_sup.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2007-2010. All Rights Reserved. +%% Copyright Ericsson AB 2007-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/lib/inets/src/http_client/httpc_request.erl b/lib/inets/src/http_client/httpc_request.erl index af4c3f75f2..0f20d93bc1 100644 --- a/lib/inets/src/http_client/httpc_request.erl +++ b/lib/inets/src/http_client/httpc_request.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2004-2011. All Rights Reserved. +%% Copyright Ericsson AB 2004-2018. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -88,9 +88,11 @@ send(SendAddr, Socket, SocketType, case Address of SendAddr -> {TmpHdrs2, Path ++ Query}; - _Proxy -> + _Proxy when SocketType == ip_comm -> TmpHdrs3 = handle_proxy(HttpOptions, TmpHdrs2), - {TmpHdrs3, AbsUri} + {TmpHdrs3, AbsUri}; + _ -> + {TmpHdrs2, Path ++ Query} end, FinalHeaders = @@ -186,34 +188,13 @@ is_client_closing(Headers) -> %%%======================================================================== %%% Internal functions %%%======================================================================== -post_data(Method, Headers, {ContentType, Body}, HeadersAsIs) - when (Method =:= post) orelse (Method =:= put) - orelse (Method =:= patch) -> - NewBody = case Headers#http_request_h.expect of - "100-continue" -> - ""; - _ -> - Body - end, - - NewHeaders = case HeadersAsIs of - [] -> - Headers#http_request_h{ - 'content-type' = ContentType, - 'content-length' = case body_length(Body) of - undefined -> - % on upload streaming the caller must give a - % value to the Content-Length header - % (or use chunked Transfer-Encoding) - Headers#http_request_h.'content-length'; - Len when is_list(Len) -> - Len - end - }; - _ -> - HeadersAsIs - end, - +post_data(Method, Headers, {ContentType, Body}, HeadersAsIs) + when (Method =:= post) + orelse (Method =:= put) + orelse (Method =:= patch) + orelse (Method =:= delete) -> + NewBody = update_body(Headers, Body), + NewHeaders = update_headers(Headers, ContentType, Body, HeadersAsIs), {NewHeaders, NewBody}; post_data(_, Headers, _, []) -> @@ -221,14 +202,56 @@ post_data(_, Headers, _, []) -> post_data(_, _, _, HeadersAsIs = [_|_]) -> {HeadersAsIs, ""}. +update_body(Headers, Body) -> + case Headers#http_request_h.expect of + "100-continue" -> + ""; + _ -> + Body + end. + +update_headers(Headers, ContentType, Body, []) -> + case Body of + [] -> + Headers1 = Headers#http_request_h{'content-length' = "0"}, + handle_content_type(Headers1, ContentType); + <<>> -> + Headers1 = Headers#http_request_h{'content-length' = "0"}, + handle_content_type(Headers1, ContentType); + {Fun, _Acc} when is_function(Fun, 1) -> + %% A client MUST NOT generate a 100-continue expectation in a request + %% that does not include a message body. This implies that either the + %% Content-Length or the Transfer-Encoding header MUST be present. + %% DO NOT send content-type when Body is empty. + Headers1 = Headers#http_request_h{'content-type' = ContentType}, + handle_transfer_encoding(Headers1); + _ -> + Headers#http_request_h{ + 'content-length' = body_length(Body), + 'content-type' = ContentType} + end; +update_headers(_, _, _, HeadersAsIs) -> + HeadersAsIs. + +handle_transfer_encoding(Headers = #http_request_h{'transfer-encoding' = undefined}) -> + Headers; +handle_transfer_encoding(Headers) -> + %% RFC7230 3.3.2 + %% A sender MUST NOT send a 'Content-Length' header field in any message + %% that contains a 'Transfer-Encoding' header field. + Headers#http_request_h{'content-length' = undefined}. + body_length(Body) when is_binary(Body) -> integer_to_list(size(Body)); body_length(Body) when is_list(Body) -> - integer_to_list(length(Body)); + integer_to_list(length(Body)). -body_length({DataFun, _Acc}) when is_function(DataFun, 1) -> - undefined. +%% Set 'Content-Type' when it is explicitly set. +handle_content_type(Headers, "") -> + Headers; +handle_content_type(Headers, ContentType) -> + Headers#http_request_h{'content-type' = ContentType}. method(Method) -> http_util:to_upper(atom_to_list(Method)). diff --git a/lib/inets/src/http_client/httpc_response.erl b/lib/inets/src/http_client/httpc_response.erl index 4bf2ba2b9b..78d6b4ed24 100644 --- a/lib/inets/src/http_client/httpc_response.erl +++ b/lib/inets/src/http_client/httpc_response.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2004-2013. All Rights Reserved. +%% Copyright Ericsson AB 2004-2018. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -83,7 +83,6 @@ whole_body(Body, Length) -> %% result(Response, Request) -> %% Response - {StatusLine, Headers, Body} %% Request - #request{} -%% Session - #tcp_session{} %% %% Description: Checks the status code ... %%------------------------------------------------------------------------- @@ -110,27 +109,30 @@ result(Response = {{_, 300, _}, _, _}, redirect(Response, Request); result(Response = {{_, Code, _}, _, _}, + Request = #request{settings = + #http_options{autoredirect = true}, + method = post}) when (Code =:= 301) orelse + (Code =:= 302) orelse + (Code =:= 303) -> + redirect(Response, Request#request{method = get}); +result(Response = {{_, Code, _}, _, _}, + Request = #request{settings = + #http_options{autoredirect = true}, + method = post}) when (Code =:= 307) -> + redirect(Response, Request); +result(Response = {{_, Code, _}, _, _}, Request = #request{settings = #http_options{autoredirect = true}, - method = head}) when (Code =:= 301) orelse + method = Method}) when (Code =:= 301) orelse (Code =:= 302) orelse (Code =:= 303) orelse (Code =:= 307) -> - redirect(Response, Request); -result(Response = {{_, Code, _}, _, _}, - Request = #request{settings = - #http_options{autoredirect = true}, - method = get}) when (Code =:= 301) orelse - (Code =:= 302) orelse - (Code =:= 303) orelse - (Code =:= 307) -> - redirect(Response, Request); -result(Response = {{_, 303, _}, _, _}, - Request = #request{settings = - #http_options{autoredirect = true}, - method = post}) -> - redirect(Response, Request#request{method = get}); - + case lists:member(Method, [get, head, options, trace]) of + true -> + redirect(Response, Request); + false -> + transparent(Response, Request) + end; result(Response = {{_,503,_}, _, _}, Request) -> status_service_unavailable(Response, Request); @@ -187,7 +189,7 @@ parse_status_code(<<?CR, ?LF, Rest/binary>>, StatusCodeStr, MaxHeaderSize, Result, true) -> parse_headers(Rest, [], [], MaxHeaderSize, [" ", list_to_integer(lists:reverse( - string:strip(StatusCodeStr))) + string:trim(StatusCodeStr))) | Result], true); parse_status_code(<<?SP, Rest/binary>>, StatusCodeStr, @@ -266,7 +268,7 @@ parse_headers(<<?LF,?LF,Body/binary>>, Header, Headers, MaxHeaderSize, Result, Relaxed); parse_headers(<<?CR,?LF,?CR,?LF,Body/binary>>, Header, Headers, - MaxHeaderSize, Result, _) -> + MaxHeaderSize, Result, Relaxed) -> HTTPHeaders = [lists:reverse(Header) | Headers], Length = lists:foldl(fun(H, Acc) -> length(H) + Acc end, 0, HTTPHeaders), @@ -274,8 +276,42 @@ parse_headers(<<?CR,?LF,?CR,?LF,Body/binary>>, Header, Headers, true -> ResponseHeaderRcord = http_response:headers(HTTPHeaders, #http_response_h{}), - {ok, list_to_tuple( - lists:reverse([Body, ResponseHeaderRcord | Result]))}; + + %% RFC7230, Section 3.3.3 + %% If a message is received with both a Transfer-Encoding and a + %% Content-Length header field, the Transfer-Encoding overrides the + %% Content-Length. Such a message might indicate an attempt to + %% perform request smuggling (Section 9.5) or response splitting + %% (Section 9.4) and ought to be handled as an error. A sender MUST + %% remove the received Content-Length field prior to forwarding such + %% a message downstream. + case ResponseHeaderRcord#http_response_h.'transfer-encoding' of + undefined -> + {ok, list_to_tuple( + lists:reverse([Body, ResponseHeaderRcord | Result]))}; + Value -> + TransferEncoding = string:lowercase(Value), + ContentLength = ResponseHeaderRcord#http_response_h.'content-length', + if + %% Respond without error but remove Content-Length field in relaxed mode + (Relaxed =:= true) + andalso (TransferEncoding =:= "chunked") + andalso (ContentLength =/= "-1") -> + ResponseHeaderRcordFixed = + ResponseHeaderRcord#http_response_h{'content-length' = "-1"}, + {ok, list_to_tuple( + lists:reverse([Body, ResponseHeaderRcordFixed | Result]))}; + %% Respond with error in default (not relaxed) mode + (Relaxed =:= false) + andalso (TransferEncoding =:= "chunked") + andalso (ContentLength =/= "-1") -> + throw({error, {headers_conflict, {'content-length', + 'transfer-encoding'}}}); + true -> + {ok, list_to_tuple( + lists:reverse([Body, ResponseHeaderRcord | Result]))} + end + end; false -> throw({error, {header_too_long, MaxHeaderSize, MaxHeaderSize-Length}}) @@ -340,57 +376,172 @@ status_server_error_50x(Response, Request) -> {stop, {Request#request.id, Msg}}. -redirect(Response = {StatusLine, Headers, Body}, Request) -> +redirect(Response = {_, Headers, _}, Request) -> {_, Data} = format_response(Response), case Headers#http_response_h.location of - undefined -> - transparent(Response, Request); - RedirUrl -> - UrlParseOpts = [{ipv6_host_with_brackets, - Request#request.ipv6_host_with_brackets}], - case uri_parse(RedirUrl, UrlParseOpts) of - {error, no_scheme} when - (Request#request.settings)#http_options.relaxed -> - NewLocation = fix_relative_uri(Request, RedirUrl), - redirect({StatusLine, Headers#http_response_h{ - location = NewLocation}, - Body}, Request); - {error, Reason} -> - {ok, error(Request, Reason), Data}; - %% Automatic redirection - {ok, {Scheme, _, Host, Port, Path, Query}} -> - NewHeaders = - (Request#request.headers)#http_request_h{host = Host}, - NewRequest = - Request#request{redircount = - Request#request.redircount+1, - scheme = Scheme, - headers = NewHeaders, - address = {Host,Port}, - path = Path, - pquery = Query, - abs_uri = - atom_to_list(Scheme) ++ "://" ++ - Host ++ ":" ++ - integer_to_list(Port) ++ - Path ++ Query}, - {redirect, NewRequest, Data} - end + undefined -> + transparent(Response, Request); + RedirUrl -> + Brackets = Request#request.ipv6_host_with_brackets, + case uri_string:parse(RedirUrl) of + {error, Reason, _} -> + {ok, error(Request, Reason), Data}; + %% Automatic redirection + URI -> + {Host, Port0} = Request#request.address, + Port = maybe_to_integer(Port0), + Path = Request#request.path, + Scheme = atom_to_list(Request#request.scheme), + Query = Request#request.pquery, + URIMap = resolve_uri(Scheme, Host, Port, Path, Query, URI), + TScheme = list_to_atom(maps:get(scheme, URIMap)), + THost = http_util:maybe_add_brackets(maps:get(host, URIMap), Brackets), + TPort = maps:get(port, URIMap), + TPath = maps:get(path, URIMap), + TQuery = maps:get(query, URIMap, ""), + NewURI = uri_string:normalize( + uri_string:recompose(URIMap)), + HostPort = http_request:normalize_host(TScheme, THost, TPort), + NewHeaders = + (Request#request.headers)#http_request_h{host = HostPort}, + NewRequest = + Request#request{redircount = + Request#request.redircount+1, + scheme = TScheme, + headers = NewHeaders, + address = {THost,TPort}, + path = TPath, + pquery = TQuery, + abs_uri = NewURI}, + {redirect, NewRequest, Data} + end + end. + + +%% RFC3986 - 5.2.2. Transform References +resolve_uri(Scheme, Host, Port, Path, Query, URI) -> + resolve_uri(Scheme, Host, Port, Path, Query, URI, #{}). +%% +resolve_uri(Scheme, Host, Port, Path, Query, URI, Map0) -> + case maps:get(scheme, URI, undefined) of + undefined -> + Port0 = get_port(Scheme, URI), + Map = Map0#{scheme => Scheme, + port => Port0}, + resolve_authority(Host, Port, Path, Query, URI, Map); + URIScheme -> + Port0 = get_port(URIScheme, URI), + maybe_add_query( + Map0#{scheme => URIScheme, + host => maps:get(host, URI), + port => Port0, + path => maps:get(path, URI)}, + URI) + end. + + +get_port(Scheme, URI) -> + case maps:get(port, URI, undefined) of + undefined -> + get_default_port(Scheme); + Port -> + Port + end. + + +get_default_port("http") -> + 80; +get_default_port("https") -> + 443. + + +resolve_authority(Host, Port, Path, Query, RelURI, Map) -> + case maps:is_key(host, RelURI) of + true -> + maybe_add_query( + Map#{host => maps:get(host, RelURI), + path => maps:get(path, RelURI)}, + RelURI); + false -> + Map1 = Map#{host => Host, + port => Port}, + resolve_path(Path, Query, RelURI, Map1) + end. + + +maybe_add_query(Map, RelURI) -> + case maps:is_key(query, RelURI) of + true -> + Map#{query => maps:get(query, RelURI)}; + false -> + Map + end. + + +resolve_path(Path, Query, RelURI, Map) -> + case maps:is_key(path, RelURI) of + true -> + Path1 = calculate_path(Path, maps:get(path, RelURI)), + maybe_add_query( + Map#{path => Path1}, + RelURI); + false -> + Map1 = Map#{path => Path}, + resolve_query(Query, RelURI, Map1) + end. + + +calculate_path(BaseP, RelP) -> + case starts_with_slash(RelP) of + true -> + RelP; + false -> + merge_paths(BaseP, RelP) + end. + + +starts_with_slash([$/|_]) -> + true; +starts_with_slash(<<$/,_/binary>>) -> + true; +starts_with_slash(_) -> + false. + + +%% RFC3986 - 5.2.3. Merge Paths +merge_paths("", RelP) -> + [$/|RelP]; +merge_paths(BaseP, RelP) when is_list(BaseP) -> + do_merge_paths(lists:reverse(BaseP), RelP); +merge_paths(BaseP, RelP) when is_binary(BaseP) -> + B = binary_to_list(BaseP), + R = binary_to_list(RelP), + Res = merge_paths(B, R), + list_to_binary(Res). + + +do_merge_paths([$/|_] = L, RelP) -> + lists:reverse(L) ++ RelP; +do_merge_paths([_|T], RelP) -> + do_merge_paths(T, RelP). + + +resolve_query(Query, RelURI, Map) -> + case maps:is_key(query, RelURI) of + true -> + Map#{query => maps:get(query, RelURI)}; + false -> + Map#{query => Query} end. -maybe_to_list(Port) when is_integer(Port) -> - integer_to_list(Port); -maybe_to_list(Port) when is_list(Port) -> + +maybe_to_integer(Port) when is_list(Port) -> + {Port1, _} = string:to_integer(Port), + Port1; +maybe_to_integer(Port) when is_integer(Port) -> Port. -%%% Guessing that we received a relative URI, fix it to become an absoluteURI -fix_relative_uri(Request, RedirUrl) -> - {Server, Port0} = Request#request.address, - Port = maybe_to_list(Port0), - Path = Request#request.path, - atom_to_list(Request#request.scheme) ++ "://" ++ Server ++ ":" ++ Port - ++ Path ++ RedirUrl. - + error(#request{id = Id}, Reason) -> {Id, {error, Reason}}. @@ -431,7 +582,7 @@ format_response({StatusLine, Headers, Body}) -> Length = list_to_integer(Headers#http_response_h.'content-length'), {NewBody, Data} = case Length of - -1 -> % When no lenght indicator is provided + -1 -> % When no length indicator is provided {Body, <<>>}; Length when (Length =< size(Body)) -> <<BodyThisReq:Length/binary, Next/binary>> = Body, @@ -440,18 +591,3 @@ format_response({StatusLine, Headers, Body}) -> {Body, <<>>} end, {{StatusLine, http_response:header_list(Headers), NewBody}, Data}. - -%%-------------------------------------------------------------------------- -%% These functions is just simple wrappers to parse specifically HTTP URIs -%%-------------------------------------------------------------------------- - -scheme_defaults() -> - [{http, 80}, {https, 443}]. - -uri_parse(URI, Opts) -> - http_uri:parse(URI, [{scheme_defaults, scheme_defaults()} | Opts]). - - -%%-------------------------------------------------------------------------- - - diff --git a/lib/inets/src/http_client/httpc_sup.erl b/lib/inets/src/http_client/httpc_sup.erl index 2b2ee0f34a..b2d944c834 100644 --- a/lib/inets/src/http_client/httpc_sup.erl +++ b/lib/inets/src/http_client/httpc_sup.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2004-2009. All Rights Reserved. +%% Copyright Ericsson AB 2004-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/lib/inets/src/http_lib/Makefile b/lib/inets/src/http_lib/Makefile index 4a4eef9f24..8248e37c44 100644 --- a/lib/inets/src/http_lib/Makefile +++ b/lib/inets/src/http_lib/Makefile @@ -1,7 +1,7 @@ # # %CopyrightBegin% # -# Copyright Ericsson AB 2005-2012. All Rights Reserved. +# Copyright Ericsson AB 2005-2016. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/inets/src/http_lib/http_chunk.erl b/lib/inets/src/http_lib/http_chunk.erl index 7325f24809..742d634577 100644 --- a/lib/inets/src/http_lib/http_chunk.erl +++ b/lib/inets/src/http_lib/http_chunk.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2004-2013. All Rights Reserved. +%% Copyright Ericsson AB 2004-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/lib/inets/src/http_lib/http_internal.hrl b/lib/inets/src/http_lib/http_internal.hrl index ae92b5df8f..ca1dad07cd 100644 --- a/lib/inets/src/http_lib/http_internal.hrl +++ b/lib/inets/src/http_lib/http_internal.hrl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2002-2015. All Rights Reserved. +%% Copyright Ericsson AB 2002-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -71,7 +71,7 @@ 'last-modified', other=[] % list() - Key/Value list with other headers }). - +-type http_response_h() :: #http_response_h{}. %%% Request headers -record(http_request_h,{ @@ -118,5 +118,6 @@ 'last-modified', other=[] % list() - Key/Value list with other headers }). +-type http_request_h() :: #http_request_h{}. -endif. % -ifdef(http_internal_hrl). diff --git a/lib/inets/src/http_lib/http_request.erl b/lib/inets/src/http_lib/http_request.erl index c77b616f0d..2b1a0bd40f 100644 --- a/lib/inets/src/http_lib/http_request.erl +++ b/lib/inets/src/http_lib/http_request.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2005-2015. All Rights Reserved. +%% Copyright Ericsson AB 2005-2018. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -22,15 +22,17 @@ -include("http_internal.hrl"). --export([headers/2, http_headers/1, is_absolut_uri/1, key_value/1]). +-export([headers/2, http_headers/1, is_absolut_uri/1, key_value/1, normalize_host/3]). key_value(KeyValueStr) -> case lists:splitwith(fun($:) -> false; (_) -> true end, KeyValueStr) of - {Key, [$: | Value]} -> + {Key, [$: | Value]} when Key =/= [] -> {http_util:to_lower(string:strip(Key)), string:strip(Value)}; {_, []} -> - undefined + undefined; + _ -> + undefined end. %%------------------------------------------------------------------------- %% headers(HeaderList, #http_request_h{}) -> #http_request_h{} @@ -85,6 +87,22 @@ is_absolut_uri("https://" ++ _) -> is_absolut_uri(_) -> false. +%%------------------------------------------------------------------------- +%% normalize_host(Scheme, Host, Port) -> string() +%% Scheme - http | https +%% Host - string() +%% Port - integer() +%% +%% Description: returns a normalized Host header value, with the port +%% number omitted for well-known ports +%%------------------------------------------------------------------------- +normalize_host(https, Host, 443 = _Port) -> + Host; +normalize_host(http, Host, 80 = _Port) -> + Host; +normalize_host(_Scheme, Host, Port) -> + Host ++ ":" ++ integer_to_list(Port). + %%%======================================================================== %%% Internal functions %%%======================================================================== diff --git a/lib/inets/src/http_lib/http_response.erl b/lib/inets/src/http_lib/http_response.erl index 42e5dd263d..cbdea82523 100644 --- a/lib/inets/src/http_lib/http_response.erl +++ b/lib/inets/src/http_lib/http_response.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2005-2009. All Rights Reserved. +%% Copyright Ericsson AB 2005-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/lib/inets/src/http_lib/http_transport.erl b/lib/inets/src/http_lib/http_transport.erl index ab6afe9c6c..2e3e099e5b 100644 --- a/lib/inets/src/http_lib/http_transport.erl +++ b/lib/inets/src/http_lib/http_transport.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2004-2013. All Rights Reserved. +%% Copyright Ericsson AB 2004-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/lib/inets/src/http_lib/http_uri.erl b/lib/inets/src/http_lib/http_uri.erl index 9940136f5a..6805b0293d 100644 --- a/lib/inets/src/http_lib/http_uri.erl +++ b/lib/inets/src/http_lib/http_uri.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2006-2012. All Rights Reserved. +%% Copyright Ericsson AB 2006-2018. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -61,19 +61,35 @@ scheme_defaults/0, encode/1, decode/1]). --export_type([scheme/0, default_scheme_port_number/0]). +-export_type([uri/0, + user_info/0, + scheme/0, default_scheme_port_number/0, + host/0, + path/0, + query/0, + fragment/0]). +-type uri() :: string() | binary(). +-type user_info() :: string() | binary(). +-type scheme() :: atom(). +-type host() :: string() | binary(). +-type path() :: string() | binary(). +-type query() :: string() | binary(). +-type fragment() :: string() | binary(). +-type port_number() :: inet:port_number(). +-type default_scheme_port_number() :: port_number(). +-type hex_uri() :: string() | binary(). %% Hexadecimal encoded URI. +-type maybe_hex_uri() :: string() | binary(). %% A possibly hexadecimal encoded URI. + +-type scheme_defaults() :: [{scheme(), default_scheme_port_number()}]. +-type scheme_validation_fun() :: fun((SchemeStr :: string() | binary()) -> + valid | {error, Reason :: term()}). %%%========================================================================= %%% API %%%========================================================================= --type scheme() :: atom(). --type default_scheme_port_number() :: pos_integer(). - --spec scheme_defaults() -> - [{scheme(), default_scheme_port_number()}]. - +-spec scheme_defaults() -> scheme_defaults(). scheme_defaults() -> [{http, 80}, {https, 443}, @@ -82,9 +98,20 @@ scheme_defaults() -> {sftp, 22}, {tftp, 69}]. +-type parse_result() :: + {scheme(), user_info(), host(), port_number(), path(), query()} | + {scheme(), user_info(), host(), port_number(), path(), query(), + fragment()}. + +-spec parse(uri()) -> {ok, parse_result()} | {error, term()}. parse(AbsURI) -> parse(AbsURI, []). +-spec parse(uri(), [Option]) -> {ok, parse_result()} | {error, term()} when + Option :: {ipv6_host_with_brackets, boolean()} | + {scheme_defaults, scheme_defaults()} | + {fragment, boolean()} | + {scheme_validation_fun, scheme_validation_fun() | none}. parse(AbsURI, Opts) -> case parse_scheme(AbsURI, Opts) of {error, Reason} -> @@ -102,15 +129,22 @@ parse(AbsURI, Opts) -> reserved() -> sets:from_list([$;, $:, $@, $&, $=, $+, $,, $/, $?, - $#, $[, $], $<, $>, $\", ${, $}, $|, + $#, $[, $], $<, $>, $\", ${, $}, $|, %" $\\, $', $^, $%, $ ]). -encode(URI) -> +-spec encode(uri()) -> hex_uri(). +encode(URI) when is_list(URI) -> Reserved = reserved(), - lists:append([uri_encode(Char, Reserved) || Char <- URI]). + lists:append([uri_encode(Char, Reserved) || Char <- URI]); +encode(URI) when is_binary(URI) -> + Reserved = reserved(), + << <<(uri_encode_binary(Char, Reserved))/binary>> || <<Char>> <= URI >>. -decode(String) -> - do_decode(String). +-spec decode(maybe_hex_uri()) -> uri(). +decode(String) when is_list(String) -> + do_decode(String); +decode(String) when is_binary(String) -> + do_decode_binary(String). do_decode([$%,Hex1,Hex2|Rest]) -> [hex2dec(Hex1)*16+hex2dec(Hex2)|do_decode(Rest)]; @@ -119,6 +153,12 @@ do_decode([First|Rest]) -> do_decode([]) -> []. +do_decode_binary(<<$%, Hex:2/binary, Rest/bits>>) -> + <<(binary_to_integer(Hex, 16)), (do_decode_binary(Rest))/binary>>; +do_decode_binary(<<First:1/binary, Rest/bits>>) -> + <<First/binary, (do_decode_binary(Rest))/binary>>; +do_decode_binary(<<>>) -> + <<>>. %%%======================================================================== %%% Internal functions @@ -157,14 +197,35 @@ extract_scheme(Str, Opts) -> {value, {scheme_validation_fun, Fun}} when is_function(Fun) -> case Fun(Str) of valid -> - {ok, list_to_atom(http_util:to_lower(Str))}; + {ok, to_atom(http_util:to_lower(Str))}; {error, Error} -> {error, Error} end; _ -> - {ok, list_to_atom(http_util:to_lower(Str))} + {ok, to_atom(http_util:to_lower(Str))} end. +to_atom(S) when is_list(S) -> + list_to_atom(S); +to_atom(S) when is_binary(S) -> + binary_to_atom(S, unicode). + +parse_uri_rest(Scheme, DefaultPort, <<"//", URIPart/binary>>, Opts) -> + {Authority, PathQueryFragment} = + split_uri(URIPart, "[/?#]", {URIPart, <<"">>}, 1, 0), + {RawPath, QueryFragment} = + split_uri(PathQueryFragment, "[?#]", {PathQueryFragment, <<"">>}, 1, 0), + {Query, Fragment} = + split_uri(QueryFragment, "#", {QueryFragment, <<"">>}, 1, 0), + {UserInfo, HostPort} = split_uri(Authority, "@", {<<"">>, Authority}, 1, 1), + {Host, Port} = parse_host_port(Scheme, DefaultPort, HostPort, Opts), + Path = path(RawPath), + case lists:keyfind(fragment, 1, Opts) of + {fragment, true} -> + {ok, {Scheme, UserInfo, Host, Port, Path, Query, Fragment}}; + _ -> + {ok, {Scheme, UserInfo, Host, Port, Path, Query}} + end; parse_uri_rest(Scheme, DefaultPort, "//" ++ URIPart, Opts) -> {Authority, PathQueryFragment} = split_uri(URIPart, "[/?#]", {URIPart, ""}, 1, 0), @@ -185,6 +246,11 @@ parse_uri_rest(Scheme, DefaultPort, "//" ++ URIPart, Opts) -> %% In this version of the function, we no longer need %% the Scheme argument, but just in case... +parse_host_port(_Scheme, DefaultPort, <<"[", HostPort/binary>>, Opts) -> %ipv6 + {Host, ColonPort} = split_uri(HostPort, "\\]", {HostPort, <<"">>}, 1, 1), + Host2 = maybe_ipv6_host_with_brackets(Host, Opts), + {_, Port} = split_uri(ColonPort, ":", {<<"">>, DefaultPort}, 0, 1), + {Host2, int_port(Port)}; parse_host_port(_Scheme, DefaultPort, "[" ++ HostPort, Opts) -> %ipv6 {Host, ColonPort} = split_uri(HostPort, "\\]", {HostPort, ""}, 1, 1), Host2 = maybe_ipv6_host_with_brackets(Host, Opts), @@ -198,12 +264,19 @@ parse_host_port(_Scheme, DefaultPort, HostPort, _Opts) -> split_uri(UriPart, SplitChar, NoMatchResult, SkipLeft, SkipRight) -> case re:run(UriPart, SplitChar, [{capture, first}]) of {match, [{Match, _}]} -> - {string:substr(UriPart, 1, Match + 1 - SkipLeft), - string:substr(UriPart, Match + 1 + SkipRight, length(UriPart))}; + {string:slice(UriPart, 0, Match + 1 - SkipLeft), + string:slice(UriPart, Match + SkipRight, string:length(UriPart))}; nomatch -> NoMatchResult end. +maybe_ipv6_host_with_brackets(Host, Opts) when is_binary(Host) -> + case lists:keysearch(ipv6_host_with_brackets, 1, Opts) of + {value, {ipv6_host_with_brackets, true}} -> + <<"[", Host/binary, "]">>; + _ -> + Host + end; maybe_ipv6_host_with_brackets(Host, Opts) -> case lists:keysearch(ipv6_host_with_brackets, 1, Opts) of {value, {ipv6_host_with_brackets, true}} -> @@ -212,15 +285,18 @@ maybe_ipv6_host_with_brackets(Host, Opts) -> Host end. - int_port(Port) when is_integer(Port) -> Port; +int_port(Port) when is_binary(Port) -> + binary_to_integer(Port); int_port(Port) when is_list(Port) -> list_to_integer(Port); %% This is the case where no port was found and there was no default port int_port(no_default_port) -> throw({error, no_default_port}). +path(<<"">>) -> + <<"/">>; path("") -> "/"; path(Path) -> @@ -234,6 +310,14 @@ uri_encode(Char, Reserved) -> [Char] end. +uri_encode_binary(Char, Reserved) -> + case sets:is_element(Char, Reserved) of + true -> + << $%, (integer_to_binary(Char, 16))/binary >>; + false -> + <<Char>> + end. + hex2dec(X) when (X>=$0) andalso (X=<$9) -> X-$0; hex2dec(X) when (X>=$A) andalso (X=<$F) -> X-$A+10; hex2dec(X) when (X>=$a) andalso (X=<$f) -> X-$a+10. diff --git a/lib/inets/src/http_lib/http_util.erl b/lib/inets/src/http_lib/http_util.erl index aafa97afee..5577b00cc8 100644 --- a/lib/inets/src/http_lib/http_util.erl +++ b/lib/inets/src/http_lib/http_util.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2005-2011. All Rights Reserved. +%% Copyright Ericsson AB 2005-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -27,7 +27,8 @@ convert_month/1, is_hostname/1, timestamp/0, timeout/2, - html_encode/1 + html_encode/1, + maybe_add_brackets/2 ]). @@ -35,10 +36,10 @@ %%% Internal application API %%%========================================================================= to_upper(Str) -> - string:to_upper(Str). + string:uppercase(Str). to_lower(Str) -> - string:to_lower(Str). + string:lowercase(Str). %% Example: Mon, 09-Dec-2002 13:46:00 GMT convert_netscapecookie_date([_D,_A,_Y, $,, $ , @@ -194,6 +195,24 @@ html_encode(Chars) -> lists:append([char_to_html_entity(Char, Reserved) || Char <- Chars]). +maybe_add_brackets(Addr, false) -> + Addr; +maybe_add_brackets(Addr, true) when is_list(Addr) -> + case is_ipv6_address(Addr) of + true -> + [$[|Addr] ++ "]"; + false -> + Addr + end; +maybe_add_brackets(Addr, true) when is_binary(Addr) -> + case is_ipv6_address(Addr) of + true -> + <<$[,Addr/binary,$]>>; + false -> + Addr + end. + + %%%======================================================================== %%% Internal functions %%%======================================================================== @@ -205,3 +224,14 @@ char_to_html_entity(Char, Reserved) -> false -> [Char] end. + +is_ipv6_address(Addr) when is_binary(Addr) -> + B = binary_to_list(Addr), + is_ipv6_address(B); +is_ipv6_address(Addr) when is_list(Addr) -> + case inet:parse_ipv6strict_address(Addr) of + {ok, _ } -> + true; + {error, _} -> + false + end. diff --git a/lib/inets/src/http_server/httpd.erl b/lib/inets/src/http_server/httpd.erl index e6377b4882..f4b53ce129 100644 --- a/lib/inets/src/http_server/httpd.erl +++ b/lib/inets/src/http_server/httpd.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 1997-2014. All Rights Reserved. +%% Copyright Ericsson AB 1997-2018. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -36,7 +36,13 @@ ]). %% API --export([parse_query/1, reload_config/2, info/1, info/2, info/3]). +-export([ + parse_query/1, + reload_config/2, + info/1, + info/2, + info/3 + ]). %%%======================================================================== %%% API @@ -49,13 +55,24 @@ parse_query(String) -> reload_config(Config = [Value| _], Mode) when is_tuple(Value) -> do_reload_config(Config, Mode); reload_config(ConfigFile, Mode) -> - case httpd_conf:load(ConfigFile) of - {ok, ConfigList} -> - do_reload_config(ConfigList, Mode); - Error -> - Error + try file:consult(ConfigFile) of + {ok, [PropList]} -> + %% Erlang terms format + do_reload_config(PropList, Mode); + {error, _ } -> + %% Apache format + case httpd_conf:load(ConfigFile) of + {ok, ConfigList} -> + do_reload_config(ConfigList, Mode); + Error -> + Error + end + catch + exit:_ -> + throw({error, {could_not_consult_proplist_file, ConfigFile}}) end. + info(Pid) when is_pid(Pid) -> info(Pid, []). @@ -99,7 +116,14 @@ start_service(Conf) -> stop_service({Address, Port}) -> stop_service({Address, Port, ?DEFAULT_PROFILE}); stop_service({Address, Port, Profile}) -> - httpd_sup:stop_child(Address, Port, Profile); + Name = httpd_util:make_name("httpd_instance_sup", Address, Port, Profile), + Pid = whereis(Name), + MonitorRef = erlang:monitor(process, Pid), + Result = httpd_sup:stop_child(Address, Port, Profile), + receive + {'DOWN', MonitorRef, _, _, _} -> + Result + end; stop_service(Pid) when is_pid(Pid) -> case service_info(Pid) of {ok, Info} -> diff --git a/lib/inets/src/http_server/httpd.hrl b/lib/inets/src/http_server/httpd.hrl index 29dc45f93a..751264ae0a 100644 --- a/lib/inets/src/http_server/httpd.hrl +++ b/lib/inets/src/http_server/httpd.hrl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 1997-2010. All Rights Reserved. +%% Copyright Ericsson AB 1997-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/lib/inets/src/http_server/httpd_acceptor.erl b/lib/inets/src/http_server/httpd_acceptor.erl index adccaf3b69..447faec12f 100644 --- a/lib/inets/src/http_server/httpd_acceptor.erl +++ b/lib/inets/src/http_server/httpd_acceptor.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2001-2013. All Rights Reserved. +%% Copyright Ericsson AB 2001-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/lib/inets/src/http_server/httpd_acceptor_sup.erl b/lib/inets/src/http_server/httpd_acceptor_sup.erl index 172498df8b..09d81a66d0 100644 --- a/lib/inets/src/http_server/httpd_acceptor_sup.erl +++ b/lib/inets/src/http_server/httpd_acceptor_sup.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2001-2013. All Rights Reserved. +%% Copyright Ericsson AB 2001-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/lib/inets/src/http_server/httpd_cgi.erl b/lib/inets/src/http_server/httpd_cgi.erl index fb5feb5fbe..583c0be985 100644 --- a/lib/inets/src/http_server/httpd_cgi.erl +++ b/lib/inets/src/http_server/httpd_cgi.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2005-2010. All Rights Reserved. +%% Copyright Ericsson AB 2005-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/lib/inets/src/http_server/httpd_conf.erl b/lib/inets/src/http_server/httpd_conf.erl index 132e1b5b7a..9e54f2b2c5 100644 --- a/lib/inets/src/http_server/httpd_conf.erl +++ b/lib/inets/src/http_server/httpd_conf.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 1997-2015. All Rights Reserved. +%% Copyright Ericsson AB 1997-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -395,7 +395,8 @@ validate_properties(Properties) -> %% That is, if property A depends on property B. %% The only sunch preperty at this time is bind_address that depends %% on ipfamily. -validate_properties2(Properties) -> +validate_properties2(Properties0) -> + Properties = fix_ipfamily(Properties0), case proplists:get_value(bind_address, Properties) of undefined -> case proplists:get_value(sock_type, Properties, ip_comm) of @@ -422,6 +423,15 @@ validate_properties2(Properties) -> end end. +fix_ipfamily(Properties) -> + case proplists:get_value(ipfamily, Properties) of + undefined -> + Properties; + IpFamily -> + NewProps = proplists:delete(ipfamily, Properties), + [{ipfamily, validate_ipfamily(IpFamily)} | NewProps] + end. + add_inet_defaults(Properties) -> case proplists:get_value(ipfamily, Properties) of undefined -> diff --git a/lib/inets/src/http_server/httpd_connection_sup.erl b/lib/inets/src/http_server/httpd_connection_sup.erl index 939aa5366b..f7b3bef245 100644 --- a/lib/inets/src/http_server/httpd_connection_sup.erl +++ b/lib/inets/src/http_server/httpd_connection_sup.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2008-2014. All Rights Reserved. +%% Copyright Ericsson AB 2008-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/lib/inets/src/http_server/httpd_esi.erl b/lib/inets/src/http_server/httpd_esi.erl index a925fac217..3b66b86348 100644 --- a/lib/inets/src/http_server/httpd_esi.erl +++ b/lib/inets/src/http_server/httpd_esi.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2005-2011. All Rights Reserved. +%% Copyright Ericsson AB 2005-2018. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -66,7 +66,7 @@ handle_headers("") -> {ok, [], 200}; handle_headers(Headers) -> NewHeaders = string:tokens(Headers, ?CRLF), - handle_headers(NewHeaders, [], 200). + handle_headers(NewHeaders, [], 200, true). %%%======================================================================== %%% Internal functions @@ -80,21 +80,17 @@ parse_headers([?CR, ?LF, ?CR, ?LF | Rest], Acc) -> parse_headers([Char | Rest], Acc) -> parse_headers(Rest, [Char | Acc]). -handle_headers([], NewHeaders, StatusCode) -> +handle_headers([], NewHeaders, StatusCode, _) -> {ok, NewHeaders, StatusCode}; -handle_headers([Header | Headers], NewHeaders, StatusCode) -> +handle_headers([Header | Headers], NewHeaders, StatusCode, NoESIStatus) -> {FieldName, FieldValue} = httpd_response:split_header(Header, []), case FieldName of - "location" -> - case http_request:is_absolut_uri(FieldValue) of - true -> - handle_headers(Headers, - [{FieldName, FieldValue} | NewHeaders], - 302); - false -> - {proceed, FieldValue} - end; + "location" when NoESIStatus == true -> + handle_headers(Headers, + [{FieldName, FieldValue} | NewHeaders], + 302, NoESIStatus); + "status" -> NewStatusCode = case httpd_util:split(FieldValue," ",2) of @@ -103,8 +99,9 @@ handle_headers([Header | Headers], NewHeaders, StatusCode) -> _ -> 200 end, - handle_headers(Headers, NewHeaders, NewStatusCode); + handle_headers(Headers, NewHeaders, NewStatusCode, false); _ -> handle_headers(Headers, - [{FieldName, FieldValue}| NewHeaders], StatusCode) - end. + [{FieldName, FieldValue}| NewHeaders], StatusCode, + NoESIStatus) + end. diff --git a/lib/inets/src/http_server/httpd_example.erl b/lib/inets/src/http_server/httpd_example.erl index ad29b5b29a..37e4f97bc0 100644 --- a/lib/inets/src/http_server/httpd_example.erl +++ b/lib/inets/src/http_server/httpd_example.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 1997-2014. All Rights Reserved. +%% Copyright Ericsson AB 1997-2018. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -20,9 +20,9 @@ %% -module(httpd_example). -export([print/1]). --export([get/2, post/2, yahoo/2, test1/2, get_bin/2, peer/2]). +-export([get/2, put/2, post/2, yahoo/2, test1/2, get_bin/2, peer/2,new_status_and_location/2]). --export([newformat/3]). +-export([newformat/3, post_chunked/3, post_204/3]). %% These are used by the inets test-suite -export([delay/1, chunk_timeout/3]). @@ -59,6 +59,11 @@ get(_Env,[]) -> get(Env,Input) -> default(Env,Input). +put(Env,{Input,_Body}) -> + default(Env,Input); +put(Env,Input) -> + default(Env,Input). + get_bin(_Env,_Input) -> [list_to_binary(header()), list_to_binary(top("GET Example")), @@ -85,6 +90,9 @@ post(Env,Input) -> yahoo(_Env,_Input) -> "Location: http://www.yahoo.com\r\n\r\n". +new_status_and_location(_Env,_Input) -> + "status:201 Created\r\n Location: http://www.yahoo.com\r\n\r\n". + default(Env,Input) -> [header(), top("Default Example"), @@ -94,7 +102,7 @@ default(Env,Input) -> io_lib:format("~p",[httpd:parse_query(Input)]),"\n", footer()]. -peer(Env, Input) -> +peer(Env, _Input) -> Header = case proplists:get_value(peer_cert, Env) of undefined -> @@ -126,15 +134,37 @@ footer() -> "</BODY> </HTML>\n". - -newformat(SessionID, _Env, _Input)-> +post_chunked(_SessionID, _Env, {first, _Body} = _Bodychunk) -> + {continue, {state, 1}}; +post_chunked(_SessionID, _Env, {continue, _Body, {state, N}} = _Bodychunk) -> + {continue, {state, N+1}}; +post_chunked(SessionID, _Env, {last, _Body, {state, N}} = _Bodychunk) -> + mod_esi:deliver(SessionID, "Content-Type:text/html\r\n\r\n"), + mod_esi:deliver(SessionID, top("Received chunked body")), + mod_esi:deliver(SessionID, "Received" ++ integer_to_list(N) ++ "chunks"), + mod_esi:deliver(SessionID, footer()); +post_chunked(SessionID, _Env, {last, _Body, undefined} = _Bodychunk) -> + mod_esi:deliver(SessionID, "Content-Type:text/html\r\n\r\n"), + mod_esi:deliver(SessionID, top("Received chunked body")), + mod_esi:deliver(SessionID, "Received 1 chunk"), + mod_esi:deliver(SessionID, footer()); +post_chunked(_, _, _Body) -> + exit(body_not_chunked). + +post_204(SessionID, _Env, _Input) -> + mod_esi:deliver(SessionID, + ["Status: 204 No Content" ++ "\r\n\r\n"]), + mod_esi:deliver(SessionID, []). + + +newformat(SessionID,_,_) -> mod_esi:deliver(SessionID, "Content-Type:text/html\r\n\r\n"), mod_esi:deliver(SessionID, top("new esi format test")), mod_esi:deliver(SessionID, "This new format is nice<BR>"), mod_esi:deliver(SessionID, "This new format is nice<BR>"), mod_esi:deliver(SessionID, "This new format is nice<BR>"), mod_esi:deliver(SessionID, footer()). - + %% ------------------------------------------------------ delay(Time) when is_integer(Time) -> @@ -161,7 +191,7 @@ sleep(T) -> receive after T -> ok end. %% ------------------------------------------------------ -chunk_timeout(SessionID, _, StrInt) -> +chunk_timeout(SessionID, _, _StrInt) -> mod_esi:deliver(SessionID, "Tranfer-Encoding:chunked/html\r\n\r\n"), mod_esi:deliver(SessionID, top("Test chunk encoding timeout")), timer:sleep(20000), diff --git a/lib/inets/src/http_server/httpd_file.erl b/lib/inets/src/http_server/httpd_file.erl index 5ada71ad76..bf7554bd08 100644 --- a/lib/inets/src/http_server/httpd_file.erl +++ b/lib/inets/src/http_server/httpd_file.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2006-2011. All Rights Reserved. +%% Copyright Ericsson AB 2006-2018. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -33,6 +33,9 @@ handle_error(enoent, Op, ModData, Path) -> handle_error(enotdir, Op, ModData, Path) -> handle_error(404, Op, ModData, Path, ": A component of the file name is not a directory"); +handle_error(eisdir, Op, ModData, Path) -> + handle_error(403, Op, ModData, Path, + ":Ilegal operation expected a file not a directory"); handle_error(emfile, Op, _ModData, Path) -> handle_error(500, Op, none, Path, ": Too many open files"); handle_error({enfile,_}, Op, _ModData, Path) -> diff --git a/lib/inets/src/http_server/httpd_instance_sup.erl b/lib/inets/src/http_server/httpd_instance_sup.erl index 079cc464ba..b77aa174ca 100644 --- a/lib/inets/src/http_server/httpd_instance_sup.erl +++ b/lib/inets/src/http_server/httpd_instance_sup.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2001-2013. All Rights Reserved. +%% Copyright Ericsson AB 2001-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/lib/inets/src/http_server/httpd_internal.hrl b/lib/inets/src/http_server/httpd_internal.hrl index 79b53668ad..09d720ee85 100644 --- a/lib/inets/src/http_server/httpd_internal.hrl +++ b/lib/inets/src/http_server/httpd_internal.hrl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2009-2010. All Rights Reserved. +%% Copyright Ericsson AB 2009-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/lib/inets/src/http_server/httpd_log.erl b/lib/inets/src/http_server/httpd_log.erl index 0bad759774..56a08cd502 100644 --- a/lib/inets/src/http_server/httpd_log.erl +++ b/lib/inets/src/http_server/httpd_log.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2008-2012. All Rights Reserved. +%% Copyright Ericsson AB 2008-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/lib/inets/src/http_server/httpd_manager.erl b/lib/inets/src/http_server/httpd_manager.erl index 763ddae524..7cb39937e8 100644 --- a/lib/inets/src/http_server/httpd_manager.erl +++ b/lib/inets/src/http_server/httpd_manager.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2000-2014. All Rights Reserved. +%% Copyright Ericsson AB 2000-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/lib/inets/src/http_server/httpd_misc_sup.erl b/lib/inets/src/http_server/httpd_misc_sup.erl index 114a3c746f..cac9315c27 100644 --- a/lib/inets/src/http_server/httpd_misc_sup.erl +++ b/lib/inets/src/http_server/httpd_misc_sup.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2001-2009. All Rights Reserved. +%% Copyright Ericsson AB 2001-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/lib/inets/src/http_server/httpd_request.erl b/lib/inets/src/http_server/httpd_request.erl index 749f58c197..9d7538a13d 100644 --- a/lib/inets/src/http_server/httpd_request.erl +++ b/lib/inets/src/http_server/httpd_request.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2005-2015. All Rights Reserved. +%% Copyright Ericsson AB 2005-2018. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -36,7 +36,7 @@ %% little at a time on a socket. -export([ parse_method/1, parse_uri/1, parse_version/1, parse_headers/1, - whole_body/1 + whole_body/1, body_chunk_first/3, body_chunk/3, add_chunk/1 ]). @@ -76,13 +76,12 @@ body_data(Headers, Body) -> ContentLength = list_to_integer(Headers#http_request_h.'content-length'), case size(Body) - ContentLength of 0 -> - {binary_to_list(Body), <<>>}; + {Body, <<>>}; _ -> <<BodyThisReq:ContentLength/binary, Next/binary>> = Body, - {binary_to_list(BodyThisReq), Next} + {BodyThisReq, Next} end. - %%------------------------------------------------------------------------- %% validate(Method, Uri, Version) -> ok | {error, {bad_request, Reason} | %% {error, {not_supported, {Method, Uri, Version}} @@ -260,17 +259,17 @@ parse_headers(<<?LF, Octet, Rest/binary>>, Header, Headers, Current, Max, %% If ?CR is is missing RFC2616 section-19.3 parse_headers(<<?CR,?LF, Octet, Rest/binary>>, Header, Headers, Current, Max, Options, Result); -parse_headers(<<?CR,?LF, Octet, Rest/binary>>, Header, Headers, _, Max, +parse_headers(<<?CR,?LF, Octet, Rest/binary>>, Header, Headers, Current, Max, Options, Result) -> case http_request:key_value(lists:reverse(Header)) of undefined -> %% Skip headers with missing : parse_headers(Rest, [Octet], Headers, - 0, Max, Options, Result); + Current, Max, Options, Result); NewHeader -> case check_header(NewHeader, Options) of ok -> parse_headers(Rest, [Octet], [NewHeader | Headers], - 0, Max, Options, Result); + Current, Max, Options, Result); {error, Reason} -> HttpVersion = lists:nth(3, lists:reverse(Result)), {error, Reason, HttpVersion} @@ -292,10 +291,46 @@ parse_headers(<<Octet, Rest/binary>>, Header, Headers, Current, parse_headers(Rest, [Octet | Header], Headers, Current + 1, Max, Options, Result). +body_chunk_first(Body, 0 = Length, _) -> + whole_body(Body, Length); +body_chunk_first(Body, Length, MaxChunk) -> + case body_chunk(Body, Length, MaxChunk) of + {ok, {last, NewBody}} -> + {ok, NewBody}; + Other -> + Other + end. +%% Used to chunk non chunk decoded post/put data +add_chunk([<<>>, Body, Length, MaxChunk]) -> + body_chunk(Body, Length, MaxChunk); +add_chunk([More, Body, Length, MaxChunk]) -> + body_chunk(<<Body/binary, More/binary>>, Length, MaxChunk). + +body_chunk(Body, Length, nolimit) -> + whole_body(Body, Length); +body_chunk(<<>> = Body, Length, MaxChunk) -> + {ok, {continue, ?MODULE, add_chunk, [Body, Length, MaxChunk]}}; + +body_chunk(Body, Length, MaxChunk) when Length > MaxChunk -> + case size(Body) >= MaxChunk of + true -> + <<Chunk:MaxChunk/binary, Rest/binary>> = Body, + {ok, {{continue, Chunk}, ?MODULE, add_chunk, [Rest, Length - MaxChunk, MaxChunk]}}; + false -> + {ok, {continue, ?MODULE, add_chunk, [Body, Length, MaxChunk]}} + end; +body_chunk(Body, Length, MaxChunk) -> + case size(Body) of + Length -> + {ok, {last, Body}}; + _ -> + {ok, {continue, ?MODULE, add_chunk, [Body, Length, MaxChunk]}} + end. + whole_body(Body, Length) -> case size(Body) of N when N < Length, Length > 0 -> - {?MODULE, whole_body, [Body, Length]}; + {?MODULE, add_chunk, [Body, Length, nolimit]}; N when N >= Length, Length >= 0 -> %% When a client uses pipelining trailing data %% may be part of the next request! @@ -443,6 +478,3 @@ check_header({"content-length", Value}, Maxsizes) -> end; check_header(_, _) -> ok. - - - diff --git a/lib/inets/src/http_server/httpd_request_handler.erl b/lib/inets/src/http_server/httpd_request_handler.erl index 01686b2596..d918f10424 100644 --- a/lib/inets/src/http_server/httpd_request_handler.erl +++ b/lib/inets/src/http_server/httpd_request_handler.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 1997-2015. All Rights Reserved. +%% Copyright Ericsson AB 1997-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -37,18 +37,20 @@ -include("httpd_internal.hrl"). -define(HANDSHAKE_TIMEOUT, 5000). + -record(state, {mod, %% #mod{} manager, %% pid() status, %% accept | busy | blocked mfa, %% {Module, Function, Args} max_keep_alive_request = infinity, %% integer() | infinity - response_sent = false, %% true | false - timeout, %% infinity | integer() > 0 - timer, %% ref() - Request timer - headers, %% #http_request_h{} + response_sent = false :: boolean(), + timeout, %% infinity | integer() > 0 + timer :: 'undefined' | reference(), % Request timer + headers, %% #http_request_h{} body, %% binary() data, %% The total data received in bits, checked after 10s - byte_limit %% Bit limit per second before kick out + byte_limit, %% Bit limit per second before kick out + chunk }). %%==================================================================== @@ -123,7 +125,8 @@ continue_init(Manager, ConfigDB, SocketType, Socket, TimeOut) -> NrOfRequest = max_keep_alive_request(ConfigDB), MaxContentLen = max_content_length(ConfigDB), Customize = customize(ConfigDB), - + MaxChunk = max_client_body_chunk(ConfigDB), + {_, Status} = httpd_manager:new_connection(Manager), MFA = {httpd_request, parse, [[{max_uri, MaxURISize}, {max_header, MaxHeaderSize}, @@ -138,7 +141,8 @@ continue_init(Manager, ConfigDB, SocketType, Socket, TimeOut) -> status = Status, timeout = TimeOut, max_keep_alive_request = NrOfRequest, - mfa = MFA}, + mfa = MFA, + chunk = chunk_start(MaxChunk)}, http_transport:setopts(SocketType, Socket, [binary, {packet, 0}, {active, once}]), @@ -193,6 +197,7 @@ handle_cast(Msg, #state{mod = ModData} = State) -> %%-------------------------------------------------------------------- handle_info({Proto, Socket, Data}, #state{mfa = {Module, Function, Args}, + chunk = {ChunkState, _}, mod = #mod{socket_type = SockType, socket = Socket} = ModData} = State) when (((Proto =:= tcp) orelse @@ -206,7 +211,8 @@ handle_info({Proto, Socket, Data}, _ -> State#state.data + byte_size(Data) end, - case PROCESSED of + + case PROCESSED of {ok, Result} -> NewState = case NewDataSize of undefined -> @@ -214,7 +220,7 @@ handle_info({Proto, Socket, Data}, _ -> set_new_data_size(cancel_request_timeout(State), NewDataSize) end, - handle_http_msg(Result, NewState); + handle_msg(Result, NewState); {error, {size_error, MaxSize, ErrCode, ErrStr}, Version} -> NewModData = ModData#mod{http_version = Version}, httpd_response:send_status(NewModData, ErrCode, ErrStr), @@ -223,7 +229,10 @@ handle_info({Proto, Socket, Data}, error_log(Reason, NewModData), {stop, normal, State#state{response_sent = true, mod = NewModData}}; - + + {http_chunk = Module, Function, Args} when ChunkState =/= undefined -> + NewState = handle_chunk(Module, Function, Args, State), + {noreply, NewState}; NewMFA -> http_transport:setopts(SockType, Socket, [{active, once}]), case NewDataSize of @@ -348,6 +357,34 @@ await_socket_ownership_transfer(AcceptTimeout) -> exit(accept_socket_timeout) end. + +%%% Internal chunking of client body +handle_msg({{continue, Chunk}, Module, Function, Args}, #state{chunk = {_, CbState}} = State) -> + handle_internal_chunk(State#state{chunk = {continue, CbState}, + body = Chunk}, Module, Function, Args); +handle_msg({continue, Module, Function, Args}, #state{mod = ModData} = State) -> + http_transport:setopts(ModData#mod.socket_type, + ModData#mod.socket, + [{active, once}]), + {noreply, State#state{mfa = {Module, Function, Args}}}; +handle_msg({last, Body}, #state{headers = Headers, chunk = {_, CbState}} = State) -> + NewHeaders = Headers#http_request_h{'content-length' = integer_to_list(size(Body))}, + handle_response(State#state{chunk = {last, CbState}, + headers = NewHeaders, + body = Body}); +%%% Last data chunked by client +handle_msg({ChunkedHeaders, Body}, #state{headers = Headers , chunk = {ChunkState, CbState}} = State) when ChunkState =/= undefined -> + NewHeaders = http_chunk:handle_headers(Headers, ChunkedHeaders), + handle_response(State#state{chunk = {last, CbState}, + headers = NewHeaders, + body = Body}); +handle_msg({ChunkedHeaders, Body}, #state{headers = Headers , chunk = {undefined, _}} = State) -> + NewHeaders = http_chunk:handle_headers(Headers, ChunkedHeaders), + handle_response(State#state{headers = NewHeaders, + body = Body}); +handle_msg(Result, State) -> + handle_http_msg(Result, State). + handle_http_msg({_, _, Version, {_, _}, _}, #state{status = busy, mod = ModData} = State) -> handle_manager_busy(State#state{mod = @@ -404,10 +441,6 @@ handle_http_msg({Method, Uri, Version, {RecordHeaders, Headers}, Body}, error_log(Reason, ModData), {stop, normal, State#state{response_sent = true}} end; -handle_http_msg({ChunkedHeaders, Body}, - State = #state{headers = Headers}) -> - NewHeaders = http_chunk:handle_headers(Headers, ChunkedHeaders), - handle_response(State#state{headers = NewHeaders, body = Body}); handle_http_msg(Body, State) -> handle_response(State#state{body = Body}). @@ -442,22 +475,25 @@ handle_body(#state{mod = #mod{config_db = ConfigDB}} = State) -> end. -handle_body(#state{headers = Headers, body = Body, mod = ModData} = State, +handle_body(#state{headers = Headers, body = Body, + chunk = {ChunkState, CbState}, mod = #mod{config_db = ConfigDB} = ModData} = State, MaxHeaderSize, MaxBodySize) -> + MaxChunk = max_client_body_chunk(ConfigDB), case Headers#http_request_h.'transfer-encoding' of "chunked" -> try http_chunk:decode(Body, MaxBodySize, MaxHeaderSize) of - {Module, Function, Args} -> + {Module, Function, Args} -> http_transport:setopts(ModData#mod.socket_type, ModData#mod.socket, [{active, once}]), {noreply, State#state{mfa = - {Module, Function, Args}}}; - {ok, {ChunkedHeaders, NewBody}} -> - NewHeaders = - http_chunk:handle_headers(Headers, ChunkedHeaders), - handle_response(State#state{headers = NewHeaders, - body = NewBody}) + {Module, Function, Args}, + chunk = chunk_start(MaxChunk)}}; + {ok, {ChunkedHeaders, NewBody}} -> + NewHeaders = http_chunk:handle_headers(Headers, ChunkedHeaders), + handle_response(State#state{headers = NewHeaders, + body = NewBody, + chunk = chunk_finish(ChunkState, CbState, MaxChunk)}) catch throw:Error -> httpd_response:send_status(ModData, 400, @@ -475,21 +511,36 @@ handle_body(#state{headers = Headers, body = Body, mod = ModData} = State, error_log(Reason, ModData), {stop, normal, State#state{response_sent = true}}; _ -> - Length = list_to_integer(Headers#http_request_h.'content-length'), + Length = list_to_integer(Headers#http_request_h.'content-length'), + MaxChunk = max_client_body_chunk(ConfigDB), case ((Length =< MaxBodySize) or (MaxBodySize == nolimit)) of true -> - case httpd_request:whole_body(Body, Length) of - {Module, Function, Args} -> - http_transport:setopts(ModData#mod.socket_type, + case httpd_request:body_chunk_first(Body, Length, MaxChunk) of + %% This is the case that the we need more data to complete + %% the body but chunking to the mod_esi user is not enabled. + {Module, add_chunk = Function, Args} -> + http_transport:setopts(ModData#mod.socket_type, + ModData#mod.socket, + [{active, once}]), + {noreply, State#state{mfa = + {Module, Function, Args}}}; + %% Chunking to mod_esi user is enabled + {ok, {continue, Module, Function, Args}} -> + http_transport:setopts(ModData#mod.socket_type, ModData#mod.socket, [{active, once}]), {noreply, State#state{mfa = {Module, Function, Args}}}; - - {ok, NewBody} -> - handle_response( - State#state{headers = Headers, - body = NewBody}) + {ok, {{continue, Chunk}, Module, Function, Args}} -> + handle_internal_chunk(State#state{chunk = chunk_start(MaxChunk), + body = Chunk}, Module, Function, Args); + %% Whole body delivered, if chunking mechanism is enabled the whole + %% body fits in one chunk. + {ok, NewBody} -> + handle_response(State#state{chunk = chunk_finish(ChunkState, + CbState, MaxChunk), + headers = Headers, + body = NewBody}) end; false -> httpd_response:send_status(ModData, 413, "Body too long"), @@ -549,15 +600,61 @@ expect(Headers, _, ConfigDB) -> end end. +handle_chunk(http_chunk = Module, decode_data = Function, + [ChunkSize, TotalChunk, {MaxBodySize, BodySoFar, _AccLength, MaxHeaderSize}], + #state{chunk = {_, CbState}, + mod = #mod{socket_type = SockType, + socket = Socket} = ModData} = State) -> + {continue, NewCbState} = httpd_response:handle_continuation(ModData#mod{entity_body = + {continue, BodySoFar, CbState}}), + http_transport:setopts(SockType, Socket, [{active, once}]), + State#state{chunk = {continue, NewCbState}, mfa = {Module, Function, [ChunkSize, TotalChunk, {MaxBodySize, <<>>, 0, MaxHeaderSize}]}}; + +handle_chunk(http_chunk = Module, decode_size = Function, + [Data, HexList, _AccSize, {MaxBodySize, BodySoFar, _AccLength, MaxHeaderSize}], + #state{chunk = {_, CbState}, + mod = #mod{socket_type = SockType, + socket = Socket} = ModData} = State) -> + {continue, NewCbState} = httpd_response:handle_continuation(ModData#mod{entity_body = {continue, BodySoFar, CbState}}), + http_transport:setopts(SockType, Socket, [{active, once}]), + State#state{chunk = {continue, NewCbState}, mfa = {Module, Function, [Data, HexList, 0, {MaxBodySize, <<>>, 0, MaxHeaderSize}]}}; +handle_chunk(Module, Function, Args, #state{mod = #mod{socket_type = SockType, + socket = Socket}} = State) -> + http_transport:setopts(SockType, Socket, [{active, once}]), + State#state{mfa = {Module, Function, Args}}. + +handle_internal_chunk(#state{chunk = {ChunkState, CbState}, body = Chunk, + mod = #mod{socket_type = SockType, + socket = Socket} = ModData} = State, Module, Function, Args)-> + Bodychunk = body_chunk(ChunkState, CbState, Chunk), + {continue, NewCbState} = httpd_response:handle_continuation(ModData#mod{entity_body = Bodychunk}), + case Args of + [<<>> | _] -> + http_transport:setopts(SockType, Socket, [{active, once}]), + {noreply, State#state{chunk = {continue, NewCbState}, mfa = {Module, Function, Args}}}; + _ -> + handle_info({dummy, Socket, <<>>}, State#state{chunk = {continue, NewCbState}, + mfa = {Module, Function, Args}}) + end. + +handle_response(#state{body = Body, + headers = Headers, + mod = ModData, + chunk = {last, CbState}, + max_keep_alive_request = Max} = State) when Max > 0 -> + {NewBody, Data} = httpd_request:body_data(Headers, Body), + ok = httpd_response:generate_and_send_response( + ModData#mod{entity_body = {last, NewBody, CbState}}), + handle_next_request(State#state{response_sent = true}, Data); handle_response(#state{body = Body, mod = ModData, headers = Headers, max_keep_alive_request = Max} = State) when Max > 0 -> {NewBody, Data} = httpd_request:body_data(Headers, Body), + %% Backwards compatible, may cause memory explosion ok = httpd_response:generate_and_send_response( - ModData#mod{entity_body = NewBody}), + ModData#mod{entity_body = binary_to_list(NewBody)}), handle_next_request(State#state{response_sent = true}, Data); - handle_response(#state{body = Body, headers = Headers, mod = ModData} = State) -> @@ -577,6 +674,7 @@ handle_next_request(#state{mod = #mod{connection = true} = ModData, MaxURISize = max_uri_size(ModData#mod.config_db), MaxContentLen = max_content_length(ModData#mod.config_db), Customize = customize(ModData#mod.config_db), + MaxChunk = max_client_body_chunk(ModData#mod.config_db), MFA = {httpd_request, parse, [[{max_uri, MaxURISize}, {max_header, MaxHeaderSize}, {max_version, ?HTTP_MAX_VERSION_STRING}, @@ -589,6 +687,7 @@ handle_next_request(#state{mod = #mod{connection = true} = ModData, max_keep_alive_request = decrease(Max), headers = undefined, body = undefined, + chunk = chunk_start(MaxChunk), response_sent = false}, NewState = activate_request_timeout(TmpState), @@ -646,6 +745,9 @@ error_log(ReasonString, #mod{config_db = ConfigDB}) -> max_header_size(ConfigDB) -> httpd_util:lookup(ConfigDB, max_header_size, ?HTTP_MAX_HEADER_SIZE). +max_client_body_chunk(ConfigDB) -> + httpd_util:lookup(ConfigDB, max_client_body_chunk, nolimit). + max_uri_size(ConfigDB) -> httpd_util:lookup(ConfigDB, max_uri_size, ?HTTP_MAX_URI_SIZE). @@ -660,3 +762,17 @@ max_content_length(ConfigDB) -> customize(ConfigDB) -> httpd_util:lookup(ConfigDB, customize, httpd_custom). + +chunk_start(nolimit) -> + {undefined, undefined}; +chunk_start(_) -> + {first, undefined}. +chunk_finish(_, _, nolimit) -> + {undefined, undefined}; +chunk_finish(_, CbState, _) -> + {last, CbState}. + +body_chunk(first, _, Chunk) -> + {first, Chunk}; +body_chunk(ChunkState, CbState, Chunk) -> + {ChunkState, Chunk, CbState}. diff --git a/lib/inets/src/http_server/httpd_response.erl b/lib/inets/src/http_server/httpd_response.erl index c0b5f09faf..bb946664f9 100644 --- a/lib/inets/src/http_server/httpd_response.erl +++ b/lib/inets/src/http_server/httpd_response.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 1997-2015. All Rights Reserved. +%% Copyright Ericsson AB 1997-2018. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -21,7 +21,7 @@ -module(httpd_response). -export([generate_and_send_response/1, send_status/3, send_header/3, send_body/3, send_chunk/3, send_final_chunk/2, send_final_chunk/3, - split_header/2, is_disable_chunked_send/1, cache_headers/2]). + split_header/2, is_disable_chunked_send/1, cache_headers/2, handle_continuation/1]). -export([map_status_code/2]). -include_lib("inets/src/inets_app/inets_internal.hrl"). @@ -31,6 +31,9 @@ -define(VMODULE,"RESPONSE"). +handle_continuation(Mod) -> + generate_and_send_response(Mod). + %% If peername does not exist the client already discarded the %% request so we do not need to send a reply. generate_and_send_response(#mod{init_data = @@ -39,6 +42,8 @@ generate_and_send_response(#mod{init_data = generate_and_send_response(#mod{config_db = ConfigDB} = ModData) -> Modules = httpd_util:lookup(ConfigDB, modules, ?DEFAULT_MODS), case traverse_modules(ModData, Modules) of + {continue, _} = Continue -> + Continue; done -> ok; {proceed, Data} -> @@ -56,8 +61,12 @@ generate_and_send_response(#mod{config_db = ConfigDB} = ModData) -> {StatusCode, Response} -> %% Old way send_response_old(ModData, StatusCode, Response), ok; - undefined -> - send_status(ModData, 500, none), + undefined -> + %% Happens when no mod_* + %% handles the request + send_status(ModData, 501, {ModData#mod.method, + ModData#mod.request_uri, + ModData#mod.http_version}), ok end end @@ -69,26 +78,24 @@ generate_and_send_response(#mod{config_db = ConfigDB} = ModData) -> traverse_modules(ModData,[]) -> {proceed,ModData#mod.data}; traverse_modules(ModData,[Module|Rest]) -> - ?hdrd("traverse modules", [{callback_module, Module}]), try apply(Module, do, [ModData]) of + {continue, _} = Continue -> + Continue; done -> - ?hdrt("traverse modules - done", []), - done; + done; {break, NewData} -> - ?hdrt("traverse modules - break", [{new_data, NewData}]), - {proceed, NewData}; + {proceed, NewData}; {proceed, NewData} -> - ?hdrt("traverse modules - proceed", [{new_data, NewData}]), - traverse_modules(ModData#mod{data = NewData}, Rest) + traverse_modules(ModData#mod{data = NewData}, Rest) catch - T:E -> + T:E:Stacktrace -> String = lists:flatten( io_lib:format("module traverse failed: ~p:do => " "~n Error Type: ~p" "~n Error: ~p" "~n Stack trace: ~p", - [Module, T, E, ?STACK()])), + [Module, T, E, Stacktrace])), httpd_util:error_log(ModData#mod.config_db, String), send_status(ModData, 500, none), done @@ -104,15 +111,10 @@ send_status(#mod{socket_type = SocketType, socket = Socket, config_db = ConfigDB} = ModData, StatusCode, PhraseArgs) -> - ?hdrd("send status", [{status_code, StatusCode}, - {phrase_args, PhraseArgs}]), - ReasonPhrase = httpd_util:reason_phrase(StatusCode), Message = httpd_util:message(StatusCode, PhraseArgs, ConfigDB), Body = get_body(ReasonPhrase, Message), - ?hdrt("send status - header", [{reason_phrase, ReasonPhrase}, - {message, Message}]), send_header(ModData, StatusCode, [{content_type, "text/html"}, {content_length, integer_to_list(length(Body))}]), @@ -390,8 +392,7 @@ send_response_old(#mod{socket_type = Type, send_header(ModData, StatusCode, [{content_length, content_length(NewResponse)}]), httpd_socket:deliver(Type, Sock, NewResponse); - - {error, _Reason} -> + _ -> send_status(ModData, 500, "Internal Server Error") end. diff --git a/lib/inets/src/http_server/httpd_script_env.erl b/lib/inets/src/http_server/httpd_script_env.erl index 1c5d828b46..055edca211 100644 --- a/lib/inets/src/http_server/httpd_script_env.erl +++ b/lib/inets/src/http_server/httpd_script_env.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2005-2012. All Rights Reserved. +%% Copyright Ericsson AB 2005-2018. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -74,9 +74,13 @@ which_peercert(#mod{socket_type = {Type, _}, socket = Socket}) when Type == essl which_peercert(_) -> %% Not an ssl connection undefined. + which_resolve(#mod{init_data = #init_data{resolve = Resolve}}) -> Resolve. +which_name(#mod{config_db = ConfigDB}) -> + httpd_util:lookup(ConfigDB, server_name). + which_method(#mod{method = Method}) -> Method. @@ -85,7 +89,8 @@ which_request_uri(#mod{request_uri = RUri}) -> create_basic_elements(esi, ModData) -> [{server_software, which_server(ModData)}, - {server_name, which_resolve(ModData)}, + {server_name, which_name(ModData)}, + {host_name, which_resolve(ModData)}, {gateway_interface, ?GATEWAY_INTERFACE}, {server_protocol, ?SERVER_PROTOCOL}, {server_port, which_port(ModData)}, @@ -96,7 +101,8 @@ create_basic_elements(esi, ModData) -> create_basic_elements(cgi, ModData) -> [{"SERVER_SOFTWARE", which_server(ModData)}, - {"SERVER_NAME", which_resolve(ModData)}, + {"SERVER_NAME", which_name(ModData)}, + {"HOST_NAME", which_resolve(ModData)}, {"GATEWAY_INTERFACE", ?GATEWAY_INTERFACE}, {"SERVER_PROTOCOL", ?SERVER_PROTOCOL}, {"SERVER_PORT", integer_to_list(which_port(ModData))}, @@ -160,9 +166,9 @@ create_script_elements(cgi, path_info, PathInfo, ModData) -> [{"PATH_INFO", PathInfo}, {"PATH_TRANSLATED", PathTranslated}]; create_script_elements(esi, entity_body, Body, _) -> - [{content_length, httpd_util:flatlength(Body)}]; + [{content_length, integer_to_list(httpd_util:flatlength(Body))}]; create_script_elements(cgi, entity_body, Body, _) -> - [{"CONTENT_LENGTH", httpd_util:flatlength(Body)}]; + [{"CONTENT_LENGTH", integer_to_list(httpd_util:flatlength(Body))}]; create_script_elements(_, _, _, _) -> []. diff --git a/lib/inets/src/http_server/httpd_socket.erl b/lib/inets/src/http_server/httpd_socket.erl index b7b232a686..aa67c13621 100644 --- a/lib/inets/src/http_server/httpd_socket.erl +++ b/lib/inets/src/http_server/httpd_socket.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 1997-2009. All Rights Reserved. +%% Copyright Ericsson AB 1997-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/lib/inets/src/http_server/httpd_sup.erl b/lib/inets/src/http_server/httpd_sup.erl index bf40cedd5c..d1216e01b0 100644 --- a/lib/inets/src/http_server/httpd_sup.erl +++ b/lib/inets/src/http_server/httpd_sup.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2004-2013. All Rights Reserved. +%% Copyright Ericsson AB 2004-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/lib/inets/src/http_server/httpd_util.erl b/lib/inets/src/http_server/httpd_util.erl index 6dd6db6a0c..4a2eff4770 100644 --- a/lib/inets/src/http_server/httpd_util.erl +++ b/lib/inets/src/http_server/httpd_util.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 1997-2014. All Rights Reserved. +%% Copyright Ericsson AB 1997-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -333,7 +333,9 @@ rfc1123_date(LocalTime) -> {{YYYY,MM,DD},{Hour,Min,Sec}} = case calendar:local_time_to_universal_time_dst(LocalTime) of [Gmt] -> Gmt; - [_,Gmt] -> Gmt + [_,Gmt] -> Gmt; + % Should not happen, but handle the empty list to prevent an error. + [] -> LocalTime end, DayNumber = calendar:day_of_the_week({YYYY,MM,DD}), lists:flatten( diff --git a/lib/inets/src/http_server/mod_actions.erl b/lib/inets/src/http_server/mod_actions.erl index 154fde294e..b5449f20ee 100644 --- a/lib/inets/src/http_server/mod_actions.erl +++ b/lib/inets/src/http_server/mod_actions.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 1997-2010. All Rights Reserved. +%% Copyright Ericsson AB 1997-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/lib/inets/src/http_server/mod_alias.erl b/lib/inets/src/http_server/mod_alias.erl index 727f6e0ce3..68a3de0229 100644 --- a/lib/inets/src/http_server/mod_alias.erl +++ b/lib/inets/src/http_server/mod_alias.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 1997-2015. All Rights Reserved. +%% Copyright Ericsson AB 1997-2018. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -163,28 +163,24 @@ longest_match([], _RequestURI, _LongestNo, LongestAlias) -> real_script_name(_ConfigDB, _RequestURI, []) -> not_a_script; - -real_script_name(ConfigDB, RequestURI, [{MP,Replacement} | Rest]) - when element(1, MP) =:= re_pattern -> - case re:run(RequestURI, MP, [{capture, none}]) of - match -> - ActualName = - re:replace(RequestURI, MP, Replacement, [{return,list}]), - httpd_util:split_script_path(default_index(ConfigDB, ActualName)); - nomatch -> - real_script_name(ConfigDB, RequestURI, Rest) - end; - real_script_name(ConfigDB, RequestURI, [{FakeName,RealName} | Rest]) -> case re:run(RequestURI, "^" ++ FakeName, [{capture, none}]) of match -> - ActualName = + ActualName0 = re:replace(RequestURI, "^" ++ FakeName, RealName, [{return,list}]), + ActualName = abs_script_path(ConfigDB, ActualName0), httpd_util:split_script_path(default_index(ConfigDB, ActualName)); nomatch -> real_script_name(ConfigDB, RequestURI, Rest) end. +%% ERL-574: relative path in script_alias property results in malformed url +abs_script_path(ConfigDB, [$.|_] = RelPath) -> + Root = httpd_util:lookup(ConfigDB, server_root), + Root ++ "/" ++ RelPath; +abs_script_path(_, RelPath) -> + RelPath. + %% default_index default_index(ConfigDB, Path) -> diff --git a/lib/inets/src/http_server/mod_auth.erl b/lib/inets/src/http_server/mod_auth.erl index b03629cabe..fba94df176 100644 --- a/lib/inets/src/http_server/mod_auth.erl +++ b/lib/inets/src/http_server/mod_auth.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 1997-2010. All Rights Reserved. +%% Copyright Ericsson AB 1997-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/lib/inets/src/http_server/mod_auth.hrl b/lib/inets/src/http_server/mod_auth.hrl index 88554a64ed..dd7794c78c 100644 --- a/lib/inets/src/http_server/mod_auth.hrl +++ b/lib/inets/src/http_server/mod_auth.hrl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 1997-2010. All Rights Reserved. +%% Copyright Ericsson AB 1997-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/lib/inets/src/http_server/mod_auth_dets.erl b/lib/inets/src/http_server/mod_auth_dets.erl index 95a2cdd669..9f95b2ca2d 100644 --- a/lib/inets/src/http_server/mod_auth_dets.erl +++ b/lib/inets/src/http_server/mod_auth_dets.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 1998-2010. All Rights Reserved. +%% Copyright Ericsson AB 1998-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/lib/inets/src/http_server/mod_auth_mnesia.erl b/lib/inets/src/http_server/mod_auth_mnesia.erl index 994f25a462..62acc554d4 100644 --- a/lib/inets/src/http_server/mod_auth_mnesia.erl +++ b/lib/inets/src/http_server/mod_auth_mnesia.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 1997-2011. All Rights Reserved. +%% Copyright Ericsson AB 1997-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/lib/inets/src/http_server/mod_auth_plain.erl b/lib/inets/src/http_server/mod_auth_plain.erl index 1a3120e03c..f2b0dfd71f 100644 --- a/lib/inets/src/http_server/mod_auth_plain.erl +++ b/lib/inets/src/http_server/mod_auth_plain.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 1998-2010. All Rights Reserved. +%% Copyright Ericsson AB 1998-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/lib/inets/src/http_server/mod_auth_server.erl b/lib/inets/src/http_server/mod_auth_server.erl index 7d1e1a3431..90d9ee34b1 100644 --- a/lib/inets/src/http_server/mod_auth_server.erl +++ b/lib/inets/src/http_server/mod_auth_server.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2001-2010. All Rights Reserved. +%% Copyright Ericsson AB 2001-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -128,7 +128,7 @@ list_group_members(Addr, Port, Dir, Group, Password) -> list_group_members(Addr, Port, ?DEFAULT_PROFILE, Dir, Group, Password). list_group_members(Addr, Port, Profile, Dir, Group, Password) -> Name = make_name(Addr, Port, Profile), - Req = {list_group_members, Addr, Port, Dir, Group, Password}, + Req = {list_group_members, Addr, Port, Profile, Dir, Group, Password}, call(Name, Req). delete_group(Addr, Port, Dir, GroupName, Password) -> diff --git a/lib/inets/src/http_server/mod_browser.erl b/lib/inets/src/http_server/mod_browser.erl index e3c41793ae..1e8f860746 100644 --- a/lib/inets/src/http_server/mod_browser.erl +++ b/lib/inets/src/http_server/mod_browser.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2001-2009. All Rights Reserved. +%% Copyright Ericsson AB 2001-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/lib/inets/src/http_server/mod_cgi.erl b/lib/inets/src/http_server/mod_cgi.erl index ec8b9be32e..1454332895 100644 --- a/lib/inets/src/http_server/mod_cgi.erl +++ b/lib/inets/src/http_server/mod_cgi.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 1997-2010. All Rights Reserved. +%% Copyright Ericsson AB 1997-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/lib/inets/src/http_server/mod_dir.erl b/lib/inets/src/http_server/mod_dir.erl index 2d8f27af3c..ba93d0b271 100644 --- a/lib/inets/src/http_server/mod_dir.erl +++ b/lib/inets/src/http_server/mod_dir.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 1997-2010. All Rights Reserved. +%% Copyright Ericsson AB 1997-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/lib/inets/src/http_server/mod_disk_log.erl b/lib/inets/src/http_server/mod_disk_log.erl index 5e395a2118..190cf91416 100644 --- a/lib/inets/src/http_server/mod_disk_log.erl +++ b/lib/inets/src/http_server/mod_disk_log.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 1997-2010. All Rights Reserved. +%% Copyright Ericsson AB 1997-2018. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -363,17 +363,21 @@ create_disk_log(Filename, MaxBytes, MaxFiles, ConfigList) -> %%---------------------------------------------------------------------- open(Filename, MaxBytes, MaxFiles, internal) -> - Opts = [{format, internal}, {repair, truncate}], - open1(Filename, MaxBytes, MaxFiles, Opts); + Opt0 = {format, internal}, + Opts1 = [Opt0, {repair, true}], + Opts2 = [Opt0, {repair, truncate}], + open1(Filename, MaxBytes, MaxFiles, Opts1, Opts2); open(Filename, MaxBytes, MaxFiles, _) -> Opts = [{format, external}], - open1(Filename, MaxBytes, MaxFiles, Opts). + open1(Filename, MaxBytes, MaxFiles, Opts, Opts). -open1(Filename, MaxBytes, MaxFiles, Opts0) -> - Opts1 = [{name, Filename}, {file, Filename}, {type, wrap}] ++ Opts0, - case open2(Opts1, {MaxBytes, MaxFiles}) of +open1(Filename, MaxBytes, MaxFiles, Opts1, Opts2) -> + Opts0 = [{name, Filename}, {file, Filename}, {type, wrap}], + case open2(Opts0 ++ Opts1, Opts0 ++ Opts2, {MaxBytes, MaxFiles}) of {ok, LogDB} -> {ok, LogDB}; + {repaired, LogDB, {recovered, _}, {badbytes, _}} -> + {ok, LogDB}; {error, Reason} -> {error, ?NICE("Can't create " ++ Filename ++ @@ -382,11 +386,16 @@ open1(Filename, MaxBytes, MaxFiles, Opts0) -> {error, ?NICE("Can't create "++Filename)} end. -open2(Opts, Size) -> - case disk_log:open(Opts) of +open2(Opts1, Opts2, Size) -> + case disk_log:open(Opts1) of {error, {badarg, size}} -> %% File did not exist, add the size option and try again - disk_log:open([{size, Size} | Opts]); + disk_log:open([{size, Size} | Opts1]); + {error, {Reason, _}} when + Reason == not_a_log_file; + Reason == invalid_index_file -> + %% File was corrupt, add the truncate option and try again + disk_log:open([{size, Size} | Opts2]); Else -> Else end. diff --git a/lib/inets/src/http_server/mod_esi.erl b/lib/inets/src/http_server/mod_esi.erl index 2978ac9095..443b7ee564 100644 --- a/lib/inets/src/http_server/mod_esi.erl +++ b/lib/inets/src/http_server/mod_esi.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 1997-2011. All Rights Reserved. +%% Copyright Ericsson AB 1997-2018. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -31,7 +31,6 @@ -include("httpd.hrl"). -include("httpd_internal.hrl"). --include("inets_internal.hrl"). -define(VMODULE,"ESI"). -define(DEFAULT_ERL_TIMEOUT,15000). @@ -69,7 +68,6 @@ deliver(_SessionID, _Data) -> %% Description: See httpd(3) ESWAPI CALLBACK FUNCTIONS %%------------------------------------------------------------------------- do(ModData) -> - ?hdrt("do", []), case proplists:get_value(status, ModData#mod.data) of {_StatusCode, _PhraseArgs, _Reason} -> {proceed, ModData#mod.data}; @@ -190,7 +188,6 @@ store({erl_script_nocache, Value}, _) -> %%% Internal functions %%%======================================================================== generate_response(ModData) -> - ?hdrt("generate response", []), case scheme(ModData#mod.request_uri, ModData#mod.config_db) of {eval, ESIBody, Modules} -> eval(ModData, ESIBody, Modules); @@ -241,8 +238,7 @@ alias_match_str(Alias, eval_script_alias) -> %%------------------------ Erl mechanism -------------------------------- erl(#mod{method = Method} = ModData, ESIBody, Modules) - when (Method =:= "GET") orelse (Method =:= "HEAD") -> - ?hdrt("erl", [{method, Method}]), + when (Method =:= "GET") orelse (Method =:= "HEAD") orelse (Method =:= "DELETE") -> case httpd_util:split(ESIBody,":|%3A|/",2) of {ok, [ModuleName, FuncAndInput]} -> case httpd_util:split(FuncAndInput,"[\?/]",2) of @@ -264,44 +260,46 @@ erl(#mod{method = Method} = ModData, ESIBody, Modules) {proceed, [{status,{400, none, BadRequest}} | ModData#mod.data]} end; -erl(#mod{request_uri = ReqUri, - method = "PUT", - http_version = Version, - data = Data}, _ESIBody, _Modules) -> - ?hdrt("erl", [{method, put}]), - {proceed, [{status,{501,{"PUT", ReqUri, Version}, - ?NICE("Erl mechanism doesn't support method PUT")}}| - Data]}; +erl(#mod{method = "PUT", entity_body = Body} = ModData, + ESIBody, Modules) -> + case httpd_util:split(ESIBody,":|%3A|/",2) of + {ok, [ModuleName, FuncAndInput]} -> + case httpd_util:split(FuncAndInput,"[\?/]",2) of + {ok, [FunctionName, Input]} -> + generate_webpage(ModData, ESIBody, Modules, + list_to_atom(ModuleName), + FunctionName, {Input,Body}, + script_elements(FuncAndInput, Input)); + {ok, [FunctionName]} -> + generate_webpage(ModData, ESIBody, Modules, + list_to_atom(ModuleName), + FunctionName, {undefined,Body}, + script_elements(FuncAndInput, "")); + {ok, BadRequest} -> + {proceed,[{status,{400,none, BadRequest}} | + ModData#mod.data]} + end; + {ok, BadRequest} -> + {proceed, [{status,{400, none, BadRequest}} | ModData#mod.data]} + end; -erl(#mod{request_uri = ReqUri, - method = "DELETE", - http_version = Version, - data = Data}, _ESIBody, _Modules) -> - ?hdrt("erl", [{method, delete}]), - {proceed,[{status,{501,{"DELETE", ReqUri, Version}, - ?NICE("Erl mechanism doesn't support method DELETE")}}| - Data]}; +erl(#mod{method = "POST", entity_body = Body} = ModData, ESIBody, Modules) -> + case httpd_util:split(ESIBody,":|%3A|/",2) of + {ok,[ModuleName, Function]} -> + generate_webpage(ModData, ESIBody, Modules, + list_to_atom(ModuleName), + Function, Body, []); + {ok, BadRequest} -> + {proceed,[{status, {400, none, BadRequest}} | ModData#mod.data]} + end; erl(#mod{request_uri = ReqUri, method = "PATCH", http_version = Version, data = Data}, _ESIBody, _Modules) -> - ?hdrt("erl", [{method, patch}]), {proceed, [{status,{501,{"PATCH", ReqUri, Version}, ?NICE("Erl mechanism doesn't support method PATCH")}}| - Data]}; - -erl(#mod{method = "POST", - entity_body = Body} = ModData, ESIBody, Modules) -> - ?hdrt("erl", [{method, post}]), - case httpd_util:split(ESIBody,":|%3A|/",2) of - {ok,[ModuleName, Function]} -> - generate_webpage(ModData, ESIBody, Modules, - list_to_atom(ModuleName), - Function, Body, [{entity_body, Body}]); - {ok, BadRequest} -> - {proceed,[{status, {400, none, BadRequest}} | ModData#mod.data]} - end. + Data]}. generate_webpage(ModData, ESIBody, [all], Module, FunctionName, Input, ScriptElements) -> @@ -309,7 +307,6 @@ generate_webpage(ModData, ESIBody, [all], Module, FunctionName, FunctionName, Input, ScriptElements); generate_webpage(ModData, ESIBody, Modules, Module, FunctionName, Input, ScriptElements) -> - ?hdrt("generate webpage", []), Function = list_to_atom(FunctionName), case lists:member(Module, Modules) of true -> @@ -331,7 +328,6 @@ generate_webpage(ModData, ESIBody, Modules, Module, FunctionName, %% Old API that waits for the dymnamic webpage to be totally generated %% before anythig is sent back to the client. erl_scheme_webpage_whole(Mod, Func, Env, Input, ModData) -> - ?hdrt("erl_scheme_webpage_whole", [{module, Mod}, {function, Func}]), case (catch Mod:Func(Env, Input)) of {'EXIT',{undef, _}} -> {proceed, [{status, {404, ModData#mod.request_uri, "Not found"}} @@ -343,33 +339,27 @@ erl_scheme_webpage_whole(Mod, Func, Env, Input, ModData) -> {Headers, Body} = httpd_esi:parse_headers(lists:flatten(Response)), Length = httpd_util:flatlength(Body), - case httpd_esi:handle_headers(Headers) of - {proceed, AbsPath} -> - {proceed, [{real_name, httpd_util:split_path(AbsPath)} - | ModData#mod.data]}; - {ok, NewHeaders, StatusCode} -> - send_headers(ModData, StatusCode, - [{"content-length", - integer_to_list(Length)}| NewHeaders]), - case ModData#mod.method of - "HEAD" -> - {proceed, [{response, {already_sent, 200, 0}} | - ModData#mod.data]}; - _ -> - httpd_response:send_body(ModData, - StatusCode, Body), - {proceed, [{response, {already_sent, 200, - Length}} | - ModData#mod.data]} - end - end + {ok, NewHeaders, StatusCode} = httpd_esi:handle_headers(Headers), + send_headers(ModData, StatusCode, + [{"content-length", + integer_to_list(Length)}| NewHeaders]), + case ModData#mod.method of + "HEAD" -> + {proceed, [{response, {already_sent, 200, 0}} | + ModData#mod.data]}; + _ -> + httpd_response:send_body(ModData, + StatusCode, Body), + {proceed, [{response, {already_sent, 200, + Length}} | + ModData#mod.data]} + end end. %% New API that allows the dynamic wepage to be sent back to the client %% in small chunks at the time during generation. erl_scheme_webpage_chunk(Mod, Func, Env, Input, ModData) -> process_flag(trap_exit, true), - ?hdrt("erl_scheme_webpage_chunk", [{module, Mod}, {function, Func}]), Self = self(), %% Spawn worker that generates the webpage. %% It would be nicer to use erlang:function_exported/3 but if the @@ -380,7 +370,9 @@ erl_scheme_webpage_chunk(Mod, Func, Env, Input, ModData) -> {'EXIT', {undef,_}} -> %% Will force fallback on the old API exit(erl_scheme_webpage_chunk_undefined); - _ -> + {continue, _} = Continue -> + exit(Continue); + _ -> ok end end), @@ -394,38 +386,39 @@ deliver_webpage_chunk(#mod{config_db = Db} = ModData, Pid) -> deliver_webpage_chunk(ModData, Pid, Timeout). deliver_webpage_chunk(#mod{config_db = Db} = ModData, Pid, Timeout) -> - ?hdrt("deliver_webpage_chunk", [{timeout, Timeout}]), case receive_headers(Timeout) of {error, Reason} -> %% Happens when webpage generator callback/3 is undefined - ?hdrv("deliver_webpage_chunk - failed receiving headers", - [{reason, Reason}]), {error, Reason}; + {continue, _} = Continue -> + Continue; {Headers, Body} -> - case httpd_esi:handle_headers(Headers) of - {proceed, AbsPath} -> - {proceed, [{real_name, httpd_util:split_path(AbsPath)} - | ModData#mod.data]}; - {ok, NewHeaders, StatusCode} -> - IsDisableChunkedSend = - httpd_response:is_disable_chunked_send(Db), - case (ModData#mod.http_version =/= "HTTP/1.1") or - (IsDisableChunkedSend) of - true -> - send_headers(ModData, StatusCode, - [{"connection", "close"} | - NewHeaders]); - false -> - send_headers(ModData, StatusCode, - [{"transfer-encoding", - "chunked"} | NewHeaders]) - end, - handle_body(Pid, ModData, Body, Timeout, length(Body), - IsDisableChunkedSend) - end; - timeout -> - ?hdrv("deliver_webpage_chunk - timeout", []), - send_headers(ModData, 504, [{"connection", "close"}]), + {ok, NewHeaders, StatusCode} = httpd_esi:handle_headers(Headers), + %% All 1xx (informational), 204 (no content), and 304 (not modified) + %% responses MUST NOT include a message-body, and thus are always + %% terminated by the first empty line after the header fields. + %% This implies that chunked encoding MUST NOT be used for these + %% status codes. + IsDisableChunkedSend = + httpd_response:is_disable_chunked_send(Db) orelse + StatusCode =:= 204 orelse %% No Content + StatusCode =:= 304 orelse %% Not Modified + (100 =< StatusCode andalso StatusCode =< 199), %% Informational + case (ModData#mod.http_version =/= "HTTP/1.1") or + (IsDisableChunkedSend) of + true -> + send_headers(ModData, StatusCode, + [{"connection", "close"} | + NewHeaders]); + false -> + send_headers(ModData, StatusCode, + [{"transfer-encoding", + "chunked"} | NewHeaders]) + end, + handle_body(Pid, ModData, Body, Timeout, length(Body), + IsDisableChunkedSend); + timeout -> + send_headers(ModData, 504, [{"connection", "close"}]), httpd_socket:close(ModData#mod.socket_type, ModData#mod.socket), {proceed,[{response, {already_sent, 200, 0}} | ModData#mod.data]} end. @@ -433,16 +426,14 @@ deliver_webpage_chunk(#mod{config_db = Db} = ModData, Pid, Timeout) -> receive_headers(Timeout) -> receive {esi_data, Chunk} -> - ?hdrt("receive_headers - received esi data (esi)", []), httpd_esi:parse_headers(lists:flatten(Chunk)); {ok, Chunk} -> - ?hdrt("receive_headers - received esi data (ok)", []), httpd_esi:parse_headers(lists:flatten(Chunk)); {'EXIT', Pid, erl_scheme_webpage_chunk_undefined} when is_pid(Pid) -> - ?hdrd("receive_headers - exit:chunk-undef", []), {error, erl_scheme_webpage_chunk_undefined}; - {'EXIT', Pid, Reason} when is_pid(Pid) -> - ?hdrv("receive_headers - exit", [{reason, Reason}]), + {'EXIT', Pid, {continue, _} = Continue} when is_pid(Pid) -> + Continue; + {'EXIT', Pid, Reason} when is_pid(Pid) -> exit({mod_esi_linked_process_died, Pid, Reason}) after Timeout -> timeout @@ -457,7 +448,6 @@ handle_body(_, #mod{method = "HEAD"} = ModData, _, _, Size, _) -> {proceed, [{response, {already_sent, 200, Size}} | ModData#mod.data]}; handle_body(Pid, ModData, Body, Timeout, Size, IsDisableChunkedSend) -> - ?hdrt("handle_body - send chunk", [{timeout, Timeout}, {size, Size}]), httpd_response:send_chunk(ModData, Body, IsDisableChunkedSend), receive {esi_data, Data} when is_binary(Data) -> @@ -537,7 +527,6 @@ eval(#mod{request_uri = ReqUri, method = "PUT", http_version = Version, data = Data}, _ESIBody, _Modules) -> - ?hdrt("eval", [{method, put}]), {proceed,[{status,{501,{"PUT", ReqUri, Version}, ?NICE("Eval mechanism doesn't support method PUT")}}| Data]}; @@ -546,7 +535,6 @@ eval(#mod{request_uri = ReqUri, method = "DELETE", http_version = Version, data = Data}, _ESIBody, _Modules) -> - ?hdrt("eval", [{method, delete}]), {proceed,[{status,{501,{"DELETE", ReqUri, Version}, ?NICE("Eval mechanism doesn't support method DELETE")}}| Data]}; @@ -555,14 +543,12 @@ eval(#mod{request_uri = ReqUri, method = "POST", http_version = Version, data = Data}, _ESIBody, _Modules) -> - ?hdrt("eval", [{method, post}]), {proceed,[{status,{501,{"POST", ReqUri, Version}, ?NICE("Eval mechanism doesn't support method POST")}}| Data]}; eval(#mod{method = Method} = ModData, ESIBody, Modules) when (Method =:= "GET") orelse (Method =:= "HEAD") -> - ?hdrt("eval", [{method, Method}]), case is_authorized(ESIBody, Modules) of true -> case generate_webpage(ESIBody) of @@ -572,15 +558,10 @@ eval(#mod{method = Method} = ModData, ESIBody, Modules) {ok, Response} -> {Headers, _} = httpd_esi:parse_headers(lists:flatten(Response)), - case httpd_esi:handle_headers(Headers) of - {ok, _, StatusCode} -> - {proceed,[{response, {StatusCode, Response}} | - ModData#mod.data]}; - {proceed, AbsPath} -> - {proceed, [{real_name, AbsPath} | - ModData#mod.data]} - end - end; + {ok, _, StatusCode} =httpd_esi:handle_headers(Headers), + {proceed,[{response, {StatusCode, Response}} | + ModData#mod.data]} + end; false -> {proceed,[{status, {403, ModData#mod.request_uri, @@ -589,7 +570,7 @@ eval(#mod{method = Method} = ModData, ESIBody, Modules) end. generate_webpage(ESIBody) -> - (catch lib:eval_str(string:concat(ESIBody,". "))). + (catch erl_eval:eval_str(string:concat(ESIBody,". "))). is_authorized(_ESIBody, [all]) -> true; diff --git a/lib/inets/src/http_server/mod_get.erl b/lib/inets/src/http_server/mod_get.erl index e8b3896f89..58600f5e3e 100644 --- a/lib/inets/src/http_server/mod_get.erl +++ b/lib/inets/src/http_server/mod_get.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 1997-2012. All Rights Reserved. +%% Copyright Ericsson AB 1997-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/lib/inets/src/http_server/mod_head.erl b/lib/inets/src/http_server/mod_head.erl index 1b68c1c66b..25c11360eb 100644 --- a/lib/inets/src/http_server/mod_head.erl +++ b/lib/inets/src/http_server/mod_head.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 1997-2010. All Rights Reserved. +%% Copyright Ericsson AB 1997-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/lib/inets/src/http_server/mod_htaccess.erl b/lib/inets/src/http_server/mod_htaccess.erl index f229c96f2d..7b742bba24 100644 --- a/lib/inets/src/http_server/mod_htaccess.erl +++ b/lib/inets/src/http_server/mod_htaccess.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2001-2010. All Rights Reserved. +%% Copyright Ericsson AB 2001-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -412,8 +412,8 @@ getAuthenticatingDataFromHeader(Info)-> case httpd_util:split(UnCodedString,":",2) of {ok,[User,PassWord]}-> {user,User,PassWord}; - {error,Error}-> - {error,Error} + Other -> + {error, Other} end end; BadCredentials -> diff --git a/lib/inets/src/http_server/mod_log.erl b/lib/inets/src/http_server/mod_log.erl index 4161f7059c..0323918578 100644 --- a/lib/inets/src/http_server/mod_log.erl +++ b/lib/inets/src/http_server/mod_log.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 1997-2011. All Rights Reserved. +%% Copyright Ericsson AB 1997-2018. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -105,8 +105,8 @@ do(Info) -> Code = proplists:get_value(code,Head,unknown), transfer_log(Info, "-", AuthUser, Date, Code, Size), {proceed, Info#mod.data}; - {_StatusCode, Response} -> - transfer_log(Info,"-",AuthUser,Date,200, + {StatusCode, Response} -> + transfer_log(Info, "-", AuthUser, Date, StatusCode, httpd_util:flatlength(Response)), {proceed,Info#mod.data}; undefined -> diff --git a/lib/inets/src/http_server/mod_range.erl b/lib/inets/src/http_server/mod_range.erl index 66d66c2809..1c6c6d927d 100644 --- a/lib/inets/src/http_server/mod_range.erl +++ b/lib/inets/src/http_server/mod_range.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2001-2010. All Rights Reserved. +%% Copyright Ericsson AB 2001-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/lib/inets/src/http_server/mod_responsecontrol.erl b/lib/inets/src/http_server/mod_responsecontrol.erl index 9b410952f0..07129940a5 100644 --- a/lib/inets/src/http_server/mod_responsecontrol.erl +++ b/lib/inets/src/http_server/mod_responsecontrol.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2001-2011. All Rights Reserved. +%% Copyright Ericsson AB 2001-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/lib/inets/src/http_server/mod_security.erl b/lib/inets/src/http_server/mod_security.erl index 1f936d598a..e7fc043217 100644 --- a/lib/inets/src/http_server/mod_security.erl +++ b/lib/inets/src/http_server/mod_security.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 1998-2010. All Rights Reserved. +%% Copyright Ericsson AB 1998-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/lib/inets/src/http_server/mod_security_server.erl b/lib/inets/src/http_server/mod_security_server.erl index f9281b0fdc..4f52357af3 100644 --- a/lib/inets/src/http_server/mod_security_server.erl +++ b/lib/inets/src/http_server/mod_security_server.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2001-2010. All Rights Reserved. +%% Copyright Ericsson AB 2001-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/lib/inets/src/http_server/mod_trace.erl b/lib/inets/src/http_server/mod_trace.erl index e1cb428264..f007426ae3 100644 --- a/lib/inets/src/http_server/mod_trace.erl +++ b/lib/inets/src/http_server/mod_trace.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2001-2010. All Rights Reserved. +%% Copyright Ericsson AB 2001-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/lib/inets/src/inets_app/Makefile b/lib/inets/src/inets_app/Makefile index 7f51676dc5..ec1ae70305 100644 --- a/lib/inets/src/inets_app/Makefile +++ b/lib/inets/src/inets_app/Makefile @@ -1,7 +1,7 @@ # # %CopyrightBegin% # -# Copyright Ericsson AB 2005-2015. All Rights Reserved. +# Copyright Ericsson AB 2005-2018. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -49,8 +49,8 @@ MODULES = \ inets_sup \ inets_trace \ inets_lib \ - inets_time_compat \ - inets_regexp + inets_ftp_wrapper \ + inets_tftp_wrapper INTERNAL_HRL_FILES = inets_internal.hrl EXTERNAL_HRL_FILES = ../../include/httpd.hrl \ diff --git a/lib/inets/src/inets_app/inets.app.src b/lib/inets/src/inets_app/inets.app.src index c09139872f..e5780b2a4c 100644 --- a/lib/inets/src/inets_app/inets.app.src +++ b/lib/inets/src/inets_app/inets.app.src @@ -1,7 +1,7 @@ %% This is an -*- erlang -*- file. %% %CopyrightBegin% %% -%% Copyright Ericsson AB 1997-2015. All Rights Reserved. +%% Copyright Ericsson AB 1997-2018. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -28,14 +28,9 @@ inets_service, inets_trace, inets_lib, - inets_time_compat, - inets_regexp, %% FTP - ftp, - ftp_progress, - ftp_response, - ftp_sup, + inets_ftp_wrapper, %% HTTP client: httpc, @@ -103,17 +98,11 @@ mod_trace, %% TFTP - tftp, - tftp_binary, - tftp_engine, - tftp_file, - tftp_lib, - tftp_logger, - tftp_sup + inets_tftp_wrapper ]}, {registered,[inets_sup, httpc_manager]}, %% If the "new" ssl is used then 'crypto' must be started before inets. {applications,[kernel,stdlib]}, {mod,{inets_app,[]}}, - {runtime_dependencies, ["stdlib-2.0","ssl-5.3.4","runtime_tools-1.8.14", + {runtime_dependencies, ["stdlib-3.5","ssl-5.3.4","runtime_tools-1.8.14", "mnesia-4.12","kernel-3.0","erts-6.0"]}]}. diff --git a/lib/inets/src/inets_app/inets.appup.src b/lib/inets/src/inets_app/inets.appup.src index f568efd488..b197590bfd 100644 --- a/lib/inets/src/inets_app/inets.appup.src +++ b/lib/inets/src/inets_app/inets.appup.src @@ -1,7 +1,7 @@ %% -*- erlang -*- %% %CopyrightBegin% %% -%% Copyright Ericsson AB 1999-2014. All Rights Reserved. +%% Copyright Ericsson AB 1999-2018. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -18,14 +18,12 @@ %% %CopyrightEnd% {"%VSN%", [ - {<<"6.2.4">>, [{load_module, httpd_request_handler, - soft_purge, soft_purge, []}]}, + {<<"7\\..*">>,[{restart_application, inets}]}, {<<"6\\..*">>,[{restart_application, inets}]}, {<<"5\\..*">>,[{restart_application, inets}]} ], [ - {<<"6.2.4">>, [{load_module, httpd_request_handler, - soft_purge, soft_purge, []}]}, + {<<"7\\..*">>,[{restart_application, inets}]}, {<<"6\\..*">>,[{restart_application, inets}]}, {<<"5\\..*">>,[{restart_application, inets}]} ] diff --git a/lib/inets/src/inets_app/inets.erl b/lib/inets/src/inets_app/inets.erl index 2a40f73889..ab2bc5b784 100644 --- a/lib/inets/src/inets_app/inets.erl +++ b/lib/inets/src/inets_app/inets.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2006-2012. All Rights Reserved. +%% Copyright Ericsson AB 2006-2018. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -465,13 +465,19 @@ call_service(Service, Call, Args) -> exit:{noproc, _} -> {error, inets_not_started} end. - + +%% Obsolete! Kept for backward compatiblity! +%% TFTP application has been moved out from inets service_module(tftpd) -> - tftp; + inets_tftp_wrapper; service_module(tftpc) -> - tftp; + inets_tftp_wrapper; +service_module(tftp) -> + inets_tftp_wrapper; +%% Obsolete! Kept for backward compatiblity! +%% FTP application has been moved out from inets service_module(ftpc) -> - ftp; + inets_ftp_wrapper; service_module(Service) -> Service. diff --git a/lib/inets/src/inets_app/inets.mk b/lib/inets/src/inets_app/inets.mk index 2c85173c68..1228514661 100644 --- a/lib/inets/src/inets_app/inets.mk +++ b/lib/inets/src/inets_app/inets.mk @@ -2,7 +2,7 @@ # %CopyrightBegin% # -# Copyright Ericsson AB 2010-2012. All Rights Reserved. +# Copyright Ericsson AB 2010-2016. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/lib/inets/src/inets_app/inets_app.erl b/lib/inets/src/inets_app/inets_app.erl index 3e4718070c..da4e9ead25 100644 --- a/lib/inets/src/inets_app/inets_app.erl +++ b/lib/inets/src/inets_app/inets_app.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2004-2012. All Rights Reserved. +%% Copyright Ericsson AB 2004-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/lib/inets/src/ftp/ftp_internal.hrl b/lib/inets/src/inets_app/inets_ftp_wrapper.erl index a6c9fa098a..e350a490f7 100644 --- a/lib/inets/src/ftp/ftp_internal.hrl +++ b/lib/inets/src/inets_app/inets_ftp_wrapper.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2005-2010. All Rights Reserved. +%% Copyright Ericsson AB 2006-2018. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -17,17 +17,32 @@ %% %% %CopyrightEnd% %% -%% +-module(inets_ftp_wrapper). + + +-export([start_standalone/1, + start_service/1, + stop_service/1, + services/0, + service_info/1]). + + +start_standalone(Options) -> + ftp:start_standalone(Options). + + +start_service(Options) -> + application:ensure_started(ftp), + ftp:start_service(Options). + + +stop_service(Pid) -> + ftp:stop_service(Pid). --ifndef(ftp_internal_hrl). --define(ftp_internal_hrl, true). --include_lib("inets/src/inets_app/inets_internal.hrl"). +services() -> + []. --define(SERVICE, ftpc). --define(fcri(Label, Content), ?report_important(Label, ?SERVICE, Content)). --define(fcrv(Label, Content), ?report_verbose(Label, ?SERVICE, Content)). --define(fcrd(Label, Content), ?report_debug(Label, ?SERVICE, Content)). --define(fcrt(Label, Content), ?report_trace(Label, ?SERVICE, Content)). --endif. % -ifdef(ftp_internal_hrl). +service_info(_) -> + []. diff --git a/lib/inets/src/inets_app/inets_internal.hrl b/lib/inets/src/inets_app/inets_internal.hrl index cc794d27c0..38b7e68638 100644 --- a/lib/inets/src/inets_app/inets_internal.hrl +++ b/lib/inets/src/inets_app/inets_internal.hrl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2005-2011. All Rights Reserved. +%% Copyright Ericsson AB 2005-2018. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -22,8 +22,6 @@ -ifndef(inets_internal_hrl). -define(inets_internal_hrl, true). --define(STACK(), erlang:get_stacktrace()). - %% Various trace macros -define(report(Severity, Label, Service, Content), diff --git a/lib/inets/src/inets_app/inets_lib.erl b/lib/inets/src/inets_app/inets_lib.erl index 6e16f5ef6e..3fae376a9f 100644 --- a/lib/inets/src/inets_app/inets_lib.erl +++ b/lib/inets/src/inets_app/inets_lib.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2015-2015. All Rights Reserved. +%% Copyright Ericsson AB 2015-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -27,7 +27,7 @@ %% Help function, elapsed milliseconds since T0 millisec_passed({_,_,_} = T0 ) -> %% OTP 17 and earlier - timer:now_diff(inets_time_compat:timestamp(), T0) div 1000; + timer:now_diff(erlang:timestamp(), T0) div 1000; millisec_passed(T0) -> %% OTP 18 diff --git a/lib/inets/src/inets_app/inets_regexp.erl b/lib/inets/src/inets_app/inets_regexp.erl deleted file mode 100644 index fc1608bc5a..0000000000 --- a/lib/inets/src/inets_app/inets_regexp.erl +++ /dev/null @@ -1,414 +0,0 @@ -%% -%% %CopyrightBegin% -%% -%% Copyright Ericsson AB 2009. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%% -%% %CopyrightEnd% -%% - --module(inets_regexp). - --export([parse/1, match/2, first_match/2, split/2, sub/3, gsub/3]). - - -%%%========================================================================= -%%% API -%%%========================================================================= - -%% parse(RegExp) -> {ok, RE} | {error, E}. -%% Parse the regexp described in the string RegExp. - -parse(S) -> - case (catch reg(S)) of - {R, []} -> - {ok, R}; - {_R, [C|_]} -> - {error, {illegal, [C]}}; - {error, E} -> - {error, E} - end. - - -%% Find the longest match of RegExp in String. - -match(S, RegExp) when is_list(RegExp) -> - case parse(RegExp) of - {ok,RE} -> match(S, RE); - {error,E} -> {error,E} - end; -match(S, RE) -> - case match(RE, S, 1, 0, -1) of - {Start,Len} when Len >= 0 -> - {match, Start, Len}; - {_Start,_Len} -> - nomatch - end. - -%% Find the first match of RegExp in String. - -first_match(S, RegExp) when is_list(RegExp) -> - case parse(RegExp) of - {ok, RE} -> - first_match(S, RE); - {error, E} -> - {error, E} - end; -first_match(S, RE) -> - case first_match(RE, S, 1) of - {Start,Len} when Len >= 0 -> - {match, Start,Len}; - nomatch -> - nomatch - end. - -first_match(RE, S, St) when S =/= [] -> - case re_apply(S, St, RE) of - {match, P, _Rest} -> - {St, P-St}; - nomatch -> - first_match(RE, tl(S), St+1) - end; -first_match(_RE, [], _St) -> - nomatch. - - -match(RE, S, St, Pos, L) -> - case first_match(RE, S, St) of - {St1, L1} -> - Nst = St1 + 1, - if L1 > L -> - match(RE, lists:nthtail(Nst-St, S), Nst, St1, L1); - true -> - match(RE, lists:nthtail(Nst-St, S), Nst, Pos, L) - end; - nomatch -> - {Pos, L} - end. - - -%% Split a string into substrings where the RegExp describes the -%% field seperator. The RegExp " " is specially treated. - -split(String, " ") -> %This is really special - {ok, RE} = parse("[ \t]+"), - case split_apply(String, RE, true) of - [[]|Ss] -> - {ok,Ss}; - Ss -> - {ok,Ss} - end; -split(String, RegExp) when is_list(RegExp) -> - case parse(RegExp) of - {ok, RE} -> - {ok, split_apply(String, RE, false)}; - {error, E} -> - {error,E} - end; -split(String, RE) -> - {ok, split_apply(String, RE, false)}. - - -%% Substitute the first match of the regular expression RegExp -%% with the string Replace in String. Accept pre-parsed regular -%% expressions. - -sub(String, RegExp, Rep) when is_list(RegExp) -> - case parse(RegExp) of - {ok, RE} -> - sub(String, RE, Rep); - {error, E} -> - {error, E} - end; -sub(String, RE, Rep) -> - Ss = sub_match(String, RE, 1), - {ok, sub_repl(Ss, Rep, String, 1), length(Ss)}. - - -%% Substitute every match of the regular expression RegExp with -%% the string New in String. Accept pre-parsed regular expressions. - -gsub(String, RegExp, Rep) when is_list(RegExp) -> - case parse(RegExp) of - {ok, RE} -> - gsub(String, RE, Rep); - {error, E} -> - {error, E} - end; -gsub(String, RE, Rep) -> - Ss = matches(String, RE, 1), - {ok, sub_repl(Ss, Rep, String, 1), length(Ss)}. - - -%%%======================================================================== -%%% Internal functions -%%%======================================================================== - -%% This is the regular expression grammar used. It is equivalent to the -%% one used in AWK, except that we allow ^ $ to be used anywhere and fail -%% in the matching. -%% -%% reg -> reg1 : '$1'. -%% reg1 -> reg1 "|" reg2 : {'or','$1','$2'}. -%% reg1 -> reg2 : '$1'. -%% reg2 -> reg2 reg3 : {concat,'$1','$2'}. -%% reg2 -> reg3 : '$1'. -%% reg3 -> reg3 "*" : {kclosure,'$1'}. -%% reg3 -> reg3 "+" : {pclosure,'$1'}. -%% reg3 -> reg3 "?" : {optional,'$1'}. -%% reg3 -> reg4 : '$1'. -%% reg4 -> "(" reg ")" : '$2'. -%% reg4 -> "\\" char : '$2'. -%% reg4 -> "^" : bos. -%% reg4 -> "$" : eos. -%% reg4 -> "." : char. -%% reg4 -> "[" class "]" : {char_class,char_class('$2')} -%% reg4 -> "[" "^" class "]" : {comp_class,char_class('$3')} -%% reg4 -> "\"" chars "\"" : char_string('$2') -%% reg4 -> char : '$1'. -%% reg4 -> empty : epsilon. -%% The grammar of the current regular expressions. The actual parser -%% is a recursive descent implementation of the grammar. - -reg(S) -> reg1(S). - -%% reg1 -> reg2 reg1' -%% reg1' -> "|" reg2 -%% reg1' -> empty - -reg1(S0) -> - {L,S1} = reg2(S0), - reg1p(S1, L). - -reg1p([$||S0], L) -> - {R,S1} = reg2(S0), - reg1p(S1, {'or',L,R}); -reg1p(S, L) -> {L,S}. - -%% reg2 -> reg3 reg2' -%% reg2' -> reg3 -%% reg2' -> empty - -reg2(S0) -> - {L,S1} = reg3(S0), - reg2p(S1, L). - -reg2p([C|S0], L) when (C =/= $|) andalso (C =/= $)) -> - {R,S1} = reg3([C|S0]), - reg2p(S1, {concat,L,R}); -reg2p(S, L) -> {L,S}. - -%% reg3 -> reg4 reg3' -%% reg3' -> "*" reg3' -%% reg3' -> "+" reg3' -%% reg3' -> "?" reg3' -%% reg3' -> empty - -reg3(S0) -> - {L,S1} = reg4(S0), - reg3p(S1, L). - -reg3p([$*|S], L) -> reg3p(S, {kclosure,L}); -reg3p([$+|S], L) -> reg3p(S, {pclosure,L}); -reg3p([$?|S], L) -> reg3p(S, {optional,L}); -reg3p(S, L) -> {L,S}. - -reg4([$(|S0]) -> - case reg(S0) of - {R,[$)|S1]} -> {R,S1}; - {_R,_S} -> throw({error,{unterminated,"("}}) - end; -reg4([$\\,O1,O2,O3|S]) - when ((O1 >= $0) andalso - (O1 =< $7) andalso - (O2 >= $0) andalso - (O2 =< $7) andalso - (O3 >= $0) andalso - (O3 =< $7)) -> - {(O1*8 + O2)*8 + O3 - 73*$0,S}; -reg4([$\\,C|S]) -> - {escape_char(C),S}; -reg4([$\\]) -> - throw({error, {unterminated,"\\"}}); -reg4([$^|S]) -> - {bos,S}; -reg4([$$|S]) -> - {eos,S}; -reg4([$.|S]) -> - {{comp_class,"\n"},S}; -reg4("[^" ++ S0) -> - case char_class(S0) of - {Cc,[$]|S1]} -> {{comp_class,Cc},S1}; - {_Cc,_S} -> throw({error,{unterminated,"["}}) - end; -reg4([$[|S0]) -> - case char_class(S0) of - {Cc,[$]|S1]} -> {{char_class,Cc},S1}; - {_Cc,_S1} -> throw({error,{unterminated,"["}}) - end; -reg4([C|S]) - when (C =/= $*) andalso (C =/= $+) andalso (C =/= $?) andalso (C =/= $]) -> - {C, S}; -reg4([C|_S]) -> - throw({error,{illegal,[C]}}); -reg4([]) -> - {epsilon,[]}. - -escape_char($n) -> $\n; %\n = LF -escape_char($r) -> $\r; %\r = CR -escape_char($t) -> $\t; %\t = TAB -escape_char($v) -> $\v; %\v = VT -escape_char($b) -> $\b; %\b = BS -escape_char($f) -> $\f; %\f = FF -escape_char($e) -> $\e; %\e = ESC -escape_char($s) -> $\s; %\s = SPACE -escape_char($d) -> $\d; %\d = DEL -escape_char(C) -> C. - -char_class([$]|S]) -> char_class(S, [$]]); -char_class(S) -> char_class(S, []). - -char($\\, [O1,O2,O3|S]) when - O1 >= $0, O1 =< $7, O2 >= $0, O2 =< $7, O3 >= $0, O3 =< $7 -> - {(O1*8 + O2)*8 + O3 - 73*$0,S}; -char($\\, [C|S]) -> {escape_char(C),S}; -char(C, S) -> {C,S}. - -char_class([C1|S0], Cc) when C1 =/= $] -> - case char(C1, S0) of - {Cf,[$-,C2|S1]} when C2 =/= $] -> - case char(C2, S1) of - {Cl,S2} when Cf < Cl -> char_class(S2, [{Cf,Cl}|Cc]); - {Cl,_S2} -> throw({error,{char_class,[Cf,$-,Cl]}}) - end; - {C,S1} -> char_class(S1, [C|Cc]) - end; -char_class(S, Cc) -> {Cc,S}. - - -%% re_apply(String, StartPos, RegExp) -> re_app_res(). -%% -%% Apply the (parse of the) regular expression RegExp to String. If -%% there is a match return the position of the remaining string and -%% the string if else return 'nomatch'. BestMatch specifies if we want -%% the longest match, or just a match. -%% -%% StartPos should be the real start position as it is used to decide -%% if we ae at the beginning of the string. -%% -%% Pass two functions to re_apply_or so it can decide, on the basis -%% of BestMatch, whether to just any take any match or try both to -%% find the longest. This is slower but saves duplicatng code. - -re_apply(S, St, RE) -> re_apply(RE, [], S, St). - -re_apply(epsilon, More, S, P) -> %This always matches - re_apply_more(More, S, P); -re_apply({'or',RE1,RE2}, More, S, P) -> - re_apply_or(re_apply(RE1, More, S, P), - re_apply(RE2, More, S, P)); -re_apply({concat,RE1,RE2}, More, S0, P) -> - re_apply(RE1, [RE2|More], S0, P); -re_apply({kclosure,CE}, More, S, P) -> - %% Be careful with the recursion, explicitly do one call before - %% looping. - re_apply_or(re_apply_more(More, S, P), - re_apply(CE, [{kclosure,CE}|More], S, P)); -re_apply({pclosure,CE}, More, S, P) -> - re_apply(CE, [{kclosure,CE}|More], S, P); -re_apply({optional,CE}, More, S, P) -> - re_apply_or(re_apply_more(More, S, P), - re_apply(CE, More, S, P)); -re_apply(bos, More, S, 1) -> re_apply_more(More, S, 1); -re_apply(eos, More, [$\n|S], P) -> re_apply_more(More, S, P); -re_apply(eos, More, [], P) -> re_apply_more(More, [], P); -re_apply({char_class,Cc}, More, [C|S], P) -> - case in_char_class(C, Cc) of - true -> re_apply_more(More, S, P+1); - false -> nomatch - end; -re_apply({comp_class,Cc}, More, [C|S], P) -> - case in_char_class(C, Cc) of - true -> nomatch; - false -> re_apply_more(More, S, P+1) - end; -re_apply(C, More, [C|S], P) when is_integer(C) -> - re_apply_more(More, S, P+1); -re_apply(_RE, _More, _S, _P) -> nomatch. - -%% re_apply_more([RegExp], String, Length) -> re_app_res(). - -re_apply_more([RE|More], S, P) -> re_apply(RE, More, S, P); -re_apply_more([], S, P) -> {match,P,S}. - -%% in_char_class(Char, Class) -> bool(). - -in_char_class(C, [{C1,C2}|_Cc]) when C >= C1, C =< C2 -> true; -in_char_class(C, [C|_Cc]) -> true; -in_char_class(C, [_|Cc]) -> in_char_class(C, Cc); -in_char_class(_C, []) -> false. - -%% re_apply_or(Match1, Match2) -> re_app_res(). -%% If we want the best match then choose the longest match, else just -%% choose one by trying sequentially. - -re_apply_or({match,P1,S1}, {match,P2,_S2}) when P1 >= P2 -> {match,P1,S1}; -re_apply_or({match,_P1,_S1}, {match,P2,S2}) -> {match,P2,S2}; -re_apply_or(nomatch, R2) -> R2; -re_apply_or(R1, nomatch) -> R1. - - -matches(S, RE, St) -> - case first_match(RE, S, St) of - {St1,0} -> - [{St1,0}|matches(string:substr(S, St1+2-St), RE, St1+1)]; - {St1,L1} -> - [{St1,L1}|matches(string:substr(S, St1+L1+1-St), RE, St1+L1)]; - nomatch -> - [] - end. - -sub_match(S, RE, St) -> - case first_match(RE, S, St) of - {St1,L1} -> [{St1,L1}]; - nomatch -> [] - end. - -sub_repl([{St,L}|Ss], Rep, S, Pos) -> - Rs = sub_repl(Ss, Rep, S, St+L), - string:substr(S, Pos, St-Pos) ++ - sub_repl(Rep, string:substr(S, St, L), Rs); -sub_repl([], _Rep, S, Pos) -> - string:substr(S, Pos). - -sub_repl([$&|Rep], M, Rest) -> M ++ sub_repl(Rep, M, Rest); -sub_repl("\\&" ++ Rep, M, Rest) -> [$&|sub_repl(Rep, M, Rest)]; -sub_repl([C|Rep], M, Rest) -> [C|sub_repl(Rep, M, Rest)]; -sub_repl([], _M, Rest) -> Rest. - -split_apply(S, RE, Trim) -> split_apply(S, 1, RE, Trim, []). - -split_apply([], _P, _RE, true, []) -> - []; -split_apply([], _P, _RE, _T, Sub) -> - [lists:reverse(Sub)]; -split_apply(S, P, RE, T, Sub) -> - case re_apply(S, P, RE) of - {match,P,_Rest} -> - split_apply(tl(S), P+1, RE, T, [hd(S)|Sub]); - {match,P1,Rest} -> - [lists:reverse(Sub)|split_apply(Rest, P1, RE, T, [])]; - nomatch -> - split_apply(tl(S), P+1, RE, T, [hd(S)|Sub]) - end. diff --git a/lib/inets/src/inets_app/inets_service.erl b/lib/inets/src/inets_app/inets_service.erl index 706915de92..3441d2a2e4 100644 --- a/lib/inets/src/inets_app/inets_service.erl +++ b/lib/inets/src/inets_app/inets_service.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2007-2012. All Rights Reserved. +%% Copyright Ericsson AB 2007-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. diff --git a/lib/inets/src/inets_app/inets_sup.erl b/lib/inets/src/inets_app/inets_sup.erl index a48a8db190..0d3c7475fc 100644 --- a/lib/inets/src/inets_app/inets_sup.erl +++ b/lib/inets/src/inets_app/inets_sup.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 1997-2012. All Rights Reserved. +%% Copyright Ericsson AB 1997-2018. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -61,19 +61,7 @@ children() -> Services = get_services(), HttpdServices = [Service || Service <- Services, is_httpd(Service)], HttpcServices = [Service || Service <- Services, is_httpc(Service)], - TftpdServices = [Service || Service <- Services, is_tftpd(Service)], - [ftp_child_spec(), httpc_child_spec(HttpcServices), - httpd_child_spec(HttpdServices), tftpd_child_spec(TftpdServices)]. - -ftp_child_spec() -> - Name = ftp_sup, - StartFunc = {ftp_sup, start_link, []}, - Restart = permanent, - Shutdown = infinity, - Modules = [ftp_sup], - Type = supervisor, - {Name, StartFunc, Restart, Shutdown, Type, Modules}. - + [httpc_child_spec(HttpcServices), httpd_child_spec(HttpdServices)]. httpc_child_spec(HttpcServices0) -> HttpcServices = default_profile(HttpcServices0, []), @@ -94,15 +82,6 @@ httpd_child_spec(HttpdServices) -> Type = supervisor, {Name, StartFunc, Restart, Shutdown, Type, Modules}. -tftpd_child_spec(TftpServices) -> - Name = tftp_sup, - StartFunc = {tftp_sup, start_link, [TftpServices]}, - Restart = permanent, - Shutdown = infinity, - Modules = [tftp_sup], - Type = supervisor, - {Name, StartFunc, Restart, Shutdown, Type, Modules}. - is_httpd({httpd, _}) -> true; is_httpd({httpd, _, _}) -> @@ -115,11 +94,6 @@ is_httpc({httpc, _}) -> is_httpc(_) -> false. -is_tftpd({tftpd, _}) -> - true; -is_tftpd(_) -> - false. - default_profile([], Acc) -> [{httpc, {default, only_session_cookies}} | Acc]; default_profile([{httpc, {default, _}} | _] = Profiles, Acc) -> diff --git a/lib/inets/src/inets_app/inets_tftp_wrapper.erl b/lib/inets/src/inets_app/inets_tftp_wrapper.erl new file mode 100644 index 0000000000..1e5deb234b --- /dev/null +++ b/lib/inets/src/inets_app/inets_tftp_wrapper.erl @@ -0,0 +1,48 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2006-2018. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%% +%% %CopyrightEnd% +%% +-module(inets_tftp_wrapper). + + +-export([start_standalone/1, + start_service/1, + stop_service/1, + services/0, + service_info/1]). + + +start_standalone(Options) -> + tftp:start_standalone(Options). + + +start_service(Options) -> + application:ensure_started(tftp), + tftp:start_service(Options). + + +stop_service(Pid) -> + tftp:stop_service(Pid). + + +services() -> + []. + + +service_info(_) -> + []. diff --git a/lib/inets/src/inets_app/inets_time_compat.erl b/lib/inets/src/inets_app/inets_time_compat.erl deleted file mode 100644 index 475f0685dc..0000000000 --- a/lib/inets/src/inets_app/inets_time_compat.erl +++ /dev/null @@ -1,72 +0,0 @@ -%% -%% %CopyrightBegin% -%% -%% Copyright Ericsson AB 2015-2015. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%% -%% %CopyrightEnd% -%% - -%% This module is created to be able to execute on ERTS versions both -%% earlier and later than 7.0. - --module(inets_time_compat). - -%% We don't want warnings about the use of erlang:now/0 in -%% this module. --compile(nowarn_deprecated_function). - --export([monotonic_time/0, - timestamp/0, - unique_integer/0, - unique_integer/1]). - -monotonic_time() -> - try - erlang:monotonic_time() - catch - error:undef -> - %% Use Erlang system time as monotonic time - erlang_system_time_fallback() - end. - -timestamp() -> - try - erlang:timestamp() - catch - error:undef -> - erlang:now() - end. - -unique_integer() -> - try - erlang:unique_integer() - catch - error:undef -> - erlang_system_time_fallback() - end. - -unique_integer(Modifiers) -> - try - erlang:unique_integer(Modifiers) - catch - error:badarg -> - erlang:error(badarg, [Modifiers]); - error:undef -> - erlang_system_time_fallback() - end. - -erlang_system_time_fallback() -> - {MS, S, US} = erlang:now(), - (MS*1000000+S)*1000000+US. diff --git a/lib/inets/src/subdirs.mk b/lib/inets/src/subdirs.mk index 9f2a0079f2..e9f4de959c 100644 --- a/lib/inets/src/subdirs.mk +++ b/lib/inets/src/subdirs.mk @@ -1,3 +1,3 @@ #-*-makefile-*- ; force emacs to enter makefile-mode -SUB_DIRECTORIES = inets_app http_lib http_client http_server ftp tftp +SUB_DIRECTORIES = inets_app http_lib http_client http_server diff --git a/lib/inets/src/tftp/Makefile b/lib/inets/src/tftp/Makefile deleted file mode 100644 index d5d94e1b78..0000000000 --- a/lib/inets/src/tftp/Makefile +++ /dev/null @@ -1,109 +0,0 @@ -# -# %CopyrightBegin% -# -# Copyright Ericsson AB 2005-2012. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# %CopyrightEnd% -# -# - -include $(ERL_TOP)/make/target.mk -EBIN = ../../ebin -include $(ERL_TOP)/make/$(TARGET)/otp.mk - -# ---------------------------------------------------- -# Application version -# ---------------------------------------------------- -include ../../vsn.mk - -VSN = $(INETS_VSN) - - -# ---------------------------------------------------- -# Release directory specification -# ---------------------------------------------------- -RELSYSDIR = $(RELEASE_PATH)/lib/$(APPLICATION)-$(VSN) - - -# ---------------------------------------------------- -# Target Specs -# ---------------------------------------------------- -BEHAVIOUR_MODULES= \ - tftp - -MODULES = \ - tftp_binary \ - tftp_engine \ - tftp_file \ - tftp_lib \ - tftp_logger \ - tftp_sup - -HRL_FILES = tftp.hrl - -ERL_FILES= \ - $(MODULES:%=%.erl) \ - $(BEHAVIOUR_MODULES:%=%.erl) - -TARGET_FILES= $(MODULES:%=$(EBIN)/%.$(EMULATOR)) - -BEHAVIOUR_TARGET_FILES= $(BEHAVIOUR_MODULES:%=$(EBIN)/%.$(EMULATOR)) - -# ---------------------------------------------------- -# FLAGS -# ---------------------------------------------------- - -include ../inets_app/inets.mk - -ERL_COMPILE_FLAGS += \ - $(INETS_FLAGS) \ - $(INETS_ERL_COMPILE_FLAGS) \ - -I../../include \ - -I../inets_app - - -# ---------------------------------------------------- -# Targets -# ---------------------------------------------------- - -$(TARGET_FILES): $(BEHAVIOUR_TARGET_FILES) - -debug opt: $(TARGET_FILES) - -clean: - rm -f $(TARGET_FILES) $(BEHAVIOUR_TARGET_FILES) - rm -f core - -docs: - -# ---------------------------------------------------- -# Release Target -# ---------------------------------------------------- -include $(ERL_TOP)/make/otp_release_targets.mk - -release_spec: opt - $(INSTALL_DIR) "$(RELSYSDIR)/src" - $(INSTALL_DIR) "$(RELSYSDIR)/src/tftp" - $(INSTALL_DATA) $(HRL_FILES) $(ERL_FILES) "$(RELSYSDIR)/src/tftp" - $(INSTALL_DIR) "$(RELSYSDIR)/ebin" - $(INSTALL_DATA) $(TARGET_FILES) $(BEHAVIOUR_TARGET_FILES) "$(RELSYSDIR)/ebin" - -release_docs_spec: - -info: - @echo "APPLICATION = $(APPLICATION)" - @echo "INETS_DEBUG = $(INETS_DEBUG)" - @echo "INETS_FLAGS = $(INETS_FLAGS)" - @echo "ERL_COMPILE_FLAGS = $(ERL_COMPILE_FLAGS)" diff --git a/lib/inets/src/tftp/tftp.erl b/lib/inets/src/tftp/tftp.erl deleted file mode 100644 index 45d7852863..0000000000 --- a/lib/inets/src/tftp/tftp.erl +++ /dev/null @@ -1,398 +0,0 @@ -%% -%% %CopyrightBegin% -%% -%% Copyright Ericsson AB 2005-2012. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%% -%% %CopyrightEnd% -%% -%% - -%%%------------------------------------------------------------------- -%%% File : tftp.erl -%%% Author : Hakan Mattsson <[email protected]> -%%% Description : Trivial FTP -%%% Created : 18 May 2004 by Hakan Mattsson <[email protected]> -%%%------------------------------------------------------------------- -%%% -%%% This is a complete implementation of the following IETF standards: -%%% -%%% RFC 1350, The TFTP Protocol (revision 2). -%%% RFC 2347, TFTP Option Extension. -%%% RFC 2348, TFTP Blocksize Option. -%%% RFC 2349, TFTP Timeout Interval and Transfer Size Options. -%%% -%%% The only feature that not is implemented in this release is -%%% the "netascii" transfer mode. -%%% -%%% The start/1 function starts a daemon process which, listens for -%%% UDP packets on a port. When it receives a request for read or -%%% write it spawns a temporary server process which handles the -%%% actual transfer of the file. On the client side the read_file/3 -%%% and write_file/3 functions spawns a temporary client process which -%%% establishes contact with a TFTP daemon and performs the actual -%%% transfer of the file. -%%% -%%% Most of the options are common for both the client and the server -%%% side, but some of them differs a little. Here are the available -%%% options: -%%% -%%% {debug, Level} -%%% -%%% Level = none | error | warning brief | normal | verbose | all -%%% -%%% Controls the level of debug printouts. The default is none. -%%% -%%% {host, Host} -%%% -%%% The name or IP address of the host where the TFTP daemon -%%% resides. This option is only used by the client. See -%%% 'inet' about valid host names. -%%% -%%% {port, Port} -%%% -%%% Port = integer() -%%% -%%% The TFTP port where the daemon listens. It defaults to the -%%% standardized number 69. On the server side it may sometimes -%%% make sense to set it to 0, which means that the daemon just -%%% will pick a free port (which is returned by the start/1 -%%% function). -%%% -%%% If a socket has somehow already has been connected, the -%%% {udp, [{fd, integer()}]} option can be used to pass the -%%% open file descriptor to gen_udp. This can be automated -%%% a bit by using a command line argument stating the -%%% prebound file descriptor number. For example, if the -%%% Port is 69 and the file descriptor 22 has been opened by -%%% setuid_socket_wrap. Then the command line argument -%%% "-tftpd_69 22" will trigger the prebound file -%%% descriptor 22 to be used instead of opening port 69. -%%% The UDP option {udp, [{fd, 22}]} autmatically be added. -%%% See init:get_argument/ about command line arguments and -%%% gen_udp:open/2 about UDP options. -%%% -%%% {port_policy, Policy} -%%% -%%% Policy = random | Port | {range, MinPort, MaxPort} -%%% Port = MinPort = MaxPort = integer() -%%% -%%% Policy for the selection of the temporary port which is used -%%% by the server/client during the file transfer. It defaults to -%%% 'random' which is the standardized policy. With this policy a -%%% randomized free port used. A single port or a range of ports -%%% can be useful if the protocol should pass thru a firewall. -%%% -%%% {prebound_fd, InitArgFlag} -%%% -%%% InitArgFlag = atom() -%%% -%%% If a socket has somehow already has been connected, the -%%% {udp, [{fd, integer()}]} option can be used to pass the -%%% open file descriptor to gen_udp. -%%% -%%% The prebound_fd option makes it possible to pass give the -%%% file descriptor as a command line argument. The typical -%%% usage is when used in conjunction with setuid_socket_wrap -%%% to be able to open privileged sockets. For example if the -%%% file descriptor 22 has been opened by setuid_socket_wrap -%%% and you have choosen my_tftp_fd as init argument, the -%%% command line should like this "erl -my_tftp_fd 22" and -%%% FileDesc should be set to my_tftpd_fd. This would -%%% automatically imply {fd, 22} to be set as UDP option. -%%% -%%% {udp, UdpOptions} -%%% -%%% Options to gen_udp:open/2. -%%% -%%% {use_tsize, Bool} -%%% -%%% Bool = boolean() -%%% -%%% Flag for automated usage of the "tsize" option. With this set -%%% to true, the write_file/3 client will determine the filesize -%%% and send it to the server as the standardized "tsize" option. -%%% A read_file/3 client will just acquire filesize from the -%%% server by sending a zero "tsize". -%%% -%%% {max_tsize, MaxTsize} -%%% -%%% MaxTsize = integer() | infinity -%%% -%%% Threshold for the maximal filesize in bytes. The transfer will -%%% be aborted if the limit is exceeded. It defaults to -%%% 'infinity'. -%%% -%%% {max_conn, MaxConn} -%%% -%%% MaxConn = integer() | infinity -%%% -%%% Threshold for the maximal number of active connections. The -%%% daemon will reject the setup of new connections if the limit -%%% is exceeded. It defaults to 'infinity'. -%%% -%%% {TftpKey, TftpVal} -%%% -%%% TftpKey = string() -%%% TftpVal = string() -%%% -%%% The name and value of a TFTP option. -%%% -%%% {reject, Feature} -%%% -%%% Feature = Mode | TftpKey -%%% Mode = read | write -%%% TftpKey = string() -%%% -%%% Control which features that should be rejected. -%%% This is mostly useful for the server as it may restrict -%%% usage of certain TFTP options or read/write access. -%%% -%%% {callback, {RegExp, Module, State}} -%%% -%%% RegExp = string() -%%% Module = atom() -%%% State = term() -%%% -%%% Registration of a callback module. When a file is to be -%%% transferred, its local filename will be matched to the -%%% regular expressions of the registered callbacks. The first -%%% matching callback will be used the during the transfer.The -%%% callback module must implement the 'tftp' behaviour. -%%% -%%% On the server side the callback interaction starts with a -%%% call to open/5 with the registered initial callback -%%% state. open/5 is expected to open the (virtual) file. Then -%%% either the read/1 or write/2 functions are invoked -%%% repeatedly, once per transfererred block. At each function -%%% call the state returned from the previous call is -%%% obtained. When the last block has been encountered the read/1 -%%% or write/2 functions is expected to close the (virtual) -%%% file.and return its last state. The abort/3 function is only -%%% used in error situations. prepare/5 is not used on the server -%%% side. -%%% -%%% On the client side the callback interaction is the same, but -%%% it starts and ends a bit differently. It starts with a call -%%% to prepare/5 with the same arguments as open/5 -%%% takes. prepare/5 is expected to validate the TFTP options, -%%% suggested by the user and return the subset of them that it -%%% accepts. Then the options is sent to the server which will -%%% perform the same TFTP option negotiation procedure. The -%%% options that are accepted by the server is forwarded to the -%%% open/5 function on the client side. On the client side the -%%% open/5 function must accept all option as is or reject the -%%% transfer. Then the callback interaction follows the same -%%% pattern as described above for the server side. When the last -%%% block is encountered in read/1 or write/2 the returned stated -%%% is forwarded to the user and returned from read_file/3 or -%%% write_file/3. -%%%------------------------------------------------------------------- - --module(tftp). - -%%------------------------------------------------------------------- -%% Interface -%%------------------------------------------------------------------- - -%% Public functions --export([ - read_file/3, - write_file/3, - start/1, - info/1, - change_config/2, - start/0 - ]). - -%% Application local functions --export([ - start_standalone/1, - start_service/1, - stop_service/1, - services/0, - service_info/1 - ]). - - --type peer() :: {PeerType :: inet | inet6, - PeerHost :: inet:ip_address(), - PeerPort :: port()}. - --type access() :: read | write. - --type options() :: [{Key :: string(), Value :: string()}]. - --type error_code() :: undef | enoent | eacces | enospc | - badop | eexist | baduser | badopt | - integer(). - --callback prepare(Peer :: peer(), - Access :: access(), - Filename :: file:name(), - Mode :: string(), - SuggestedOptions :: options(), - InitialState :: [] | [{root_dir, string()}]) -> - {ok, AcceptedOptions :: options(), NewState :: term()} | - {error, {Code :: error_code(), string()}}. - --callback open(Peer :: peer(), - Access :: access(), - Filename :: file:name(), - Mode :: string(), - SuggestedOptions :: options(), - State :: [] | [{root_dir, string()}] | term()) -> - {ok, AcceptedOptions :: options(), NewState :: term()} | - {error, {Code :: error_code(), string()}}. - --callback read(State :: term()) -> {more, binary(), NewState :: term()} | - {last, binary(), integer()} | - {error, {Code :: error_code(), string()}}. - --callback write(binary(), State :: term()) -> - {more, NewState :: term()} | - {last, FileSize :: integer()} | - {error, {Code :: error_code(), string()}}. - --callback abort(Code :: error_code(), string(), State :: term()) -> 'ok'. - --include("tftp.hrl"). - - -%%------------------------------------------------------------------- -%% read_file(RemoteFilename, LocalFilename, Options) -> -%% {ok, LastCallbackState} | {error, Reason} -%% -%% RemoteFilename = string() -%% LocalFilename = binary | string() -%% Options = [option()] -%% LastCallbackState = term() -%% Reason = term() -%% -%% Reads a (virtual) file from a TFTP server -%% -%% If LocalFilename is the atom 'binary', tftp_binary will be used as -%% callback module. It will concatenate all transferred blocks and -%% return them as one single binary in the CallbackState. -%% -%% When LocalFilename is a string, it will be matched to the -%% registered callback modules and hopefully one of them will be -%% selected. By default, tftp_file will be used as callback module. It -%% will write each transferred block to the file named -%% LocalFilename. The number of transferred bytes will be returned as -%% LastCallbackState. -%%------------------------------------------------------------------- - -read_file(RemoteFilename, LocalFilename, Options) -> - tftp_engine:client_start(read, RemoteFilename, LocalFilename, Options). - -%%------------------------------------------------------------------- -%% write(RemoteFilename, LocalFilename, Options) -> -%% {ok, LastCallbackState} | {error, Reason} -%% -%% RemoteFilename = string() -%% LocalFilename = binary() | string() -%% Options = [option()] -%% LastCallbackState = term() -%% Reason = term() -%% -%% Writes a (virtual) file to a TFTP server -%% -%% If LocalFilename is a binary, tftp_binary will be used as callback -%% module. The binary will be transferred block by block and the number -%% of transferred bytes will be returned as LastCallbackState. -%% -%% When LocalFilename is a string, it will be matched to the -%% registered callback modules and hopefully one of them will be -%% selected. By default, tftp_file will be used as callback module. It -%% will read the file named LocalFilename block by block. The number -%% of transferred bytes will be returned as LastCallbackState. -%%------------------------------------------------------------------- - -write_file(RemoteFilename, LocalFilename, Options) -> - tftp_engine:client_start(write, RemoteFilename, LocalFilename, Options). - -%%------------------------------------------------------------------- -%% start(Options) -> {ok, Pid} | {error, Reason} -%% -%% Options = [option()] -%% Pid = pid() -%% Reason = term() -%% -%% Starts a daemon process which listens for udp packets on a -%% port. When it receives a request for read or write it spawns -%% a temporary server process which handles the actual transfer -%% of the (virtual) file. -%%------------------------------------------------------------------- - -start(Options) -> - tftp_engine:daemon_start(Options). - -%%------------------------------------------------------------------- -%% info(Pid) -> {ok, Options} | {error, Reason} -%% -%% Options = [option()] -%% Reason = term() -%% -%% Returns info about a tftp daemon, server or client process -%%------------------------------------------------------------------- - -info(Pid) -> - tftp_engine:info(Pid). - -%%------------------------------------------------------------------- -%% change_config(Pid, Options) -> ok | {error, Reason} -%% -%% Options = [option()] -%% Reason = term() -%% -%% Changes config for a tftp daemon, server or client process -%% Must be used with care. -%%------------------------------------------------------------------- - -change_config(Pid, Options) -> - tftp_engine:change_config(Pid, Options). - -%%------------------------------------------------------------------- -%% start() -> ok | {error, Reason} -%% -%% Reason = term() -%% -%% Start the application -%%------------------------------------------------------------------- - -start() -> - application:start(inets). - -%%------------------------------------------------------------------- -%% Inets service behavior -%%------------------------------------------------------------------- - -start_standalone(Options) -> - start(Options). - -start_service(Options) -> - tftp_sup:start_child(Options). - -stop_service(Pid) -> - tftp_sup:stop_child(Pid). - -services() -> - tftp_sup:which_children(). - -service_info(Pid) -> - info(Pid). - - - diff --git a/lib/inets/src/tftp/tftp.hrl b/lib/inets/src/tftp/tftp.hrl deleted file mode 100644 index 2cea18a7ea..0000000000 --- a/lib/inets/src/tftp/tftp.hrl +++ /dev/null @@ -1,69 +0,0 @@ -%% -%% %CopyrightBegin% -%% -%% Copyright Ericsson AB 2005-2009. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%% -%% %CopyrightEnd% -%% -%% - -%%%------------------------------------------------------------------- -%%% Defines -%%%------------------------------------------------------------------- - --define(TFTP_DEFAULT_PORT, 69).% Default server port - --define(TFTP_OPCODE_RRQ, 1). % Read request --define(TFTP_OPCODE_WRQ, 2). % Write request --define(TFTP_OPCODE_DATA, 3). % Data --define(TFTP_OPCODE_ACK, 4). % Acknowledgement --define(TFTP_OPCODE_ERROR, 5). % Error --define(TFTP_OPCODE_OACK, 6). % Option acknowledgment - --define(TFTP_ERROR_UNDEF, 0). % Not defined, see error message (if any) --define(TFTP_ERROR_ENOENT, 1). % File not found. --define(TFTP_ERROR_EACCES, 2). % Access violation. --define(TFTP_ERROR_ENOSPC, 3). % Disk full or allocation exceeded. --define(TFTP_ERROR_BADOP, 4). % Illegal TFTP operation. --define(TFTP_ERROR_BADBLK, 5). % Unknown transfer ID. --define(TFTP_ERROR_EEXIST, 6). % File already exists. --define(TFTP_ERROR_BADUSER, 7). % No such user. --define(TFTP_ERROR_BADOPT, 8). % Unrequested or illegal option. - --record(tftp_msg_req, {access, filename, mode, options, local_filename}). --record(tftp_msg_data, {block_no, data}). --record(tftp_msg_ack, {block_no}). --record(tftp_msg_error, {code, text, details}). --record(tftp_msg_oack, {options}). - --record(config, {parent_pid = self(), - udp_socket, - udp_options = [binary, {reuseaddr, true}, {active, once}], - udp_host = "localhost", - udp_port = ?TFTP_DEFAULT_PORT, - port_policy = random, - use_tsize = false, - max_tsize = infinity, % Filesize - max_conn = infinity, - rejected = [], - polite_ack = false, - debug_level = none, - timeout, - user_options = [], - callbacks = [], - logger = tftp_logger, - max_retries = 5}). - --record(callback, {regexp, internal, module, state, block_no, count}). diff --git a/lib/inets/src/tftp/tftp_binary.erl b/lib/inets/src/tftp/tftp_binary.erl deleted file mode 100644 index 6981d741c6..0000000000 --- a/lib/inets/src/tftp/tftp_binary.erl +++ /dev/null @@ -1,239 +0,0 @@ -%% -%% %CopyrightBegin% -%% -%% Copyright Ericsson AB 2005-2009. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%% -%% %CopyrightEnd% -%% -%% - -%%%------------------------------------------------------------------- -%%% File : tft_binary.erl -%%% Author : Hakan Mattsson <[email protected]> -%%% Description : -%%% -%%% Created : 24 May 2004 by Hakan Mattsson <[email protected]> -%%%------------------------------------------------------------------- - --module(tftp_binary). - -%%%------------------------------------------------------------------- -%%% Interface -%%%------------------------------------------------------------------- - --behaviour(tftp). - --export([prepare/6, open/6, read/1, write/2, abort/3]). - --record(read_state, {options, blksize, bin, is_native_ascii, is_network_ascii, count}). --record(write_state, {options, blksize, list, is_native_ascii, is_network_ascii}). - -%%------------------------------------------------------------------- -%% Prepare -%%------------------------------------------------------------------- - -prepare(_Peer, Access, Filename, Mode, SuggestedOptions, Initial) when is_list(Initial) -> - %% Client side - IsNativeAscii = is_native_ascii(Initial), - case catch handle_options(Access, Filename, Mode, SuggestedOptions, IsNativeAscii) of - {ok, IsNetworkAscii, AcceptedOptions} when Access =:= read, is_binary(Filename) -> - State = #read_state{options = AcceptedOptions, - blksize = lookup_blksize(AcceptedOptions), - bin = Filename, - is_network_ascii = IsNetworkAscii, - count = size(Filename), - is_native_ascii = IsNativeAscii}, - {ok, AcceptedOptions, State}; - {ok, IsNetworkAscii, AcceptedOptions} when Access =:= write, Filename =:= binary -> - State = #write_state{options = AcceptedOptions, - blksize = lookup_blksize(AcceptedOptions), - list = [], - is_network_ascii = IsNetworkAscii, - is_native_ascii = IsNativeAscii}, - {ok, AcceptedOptions, State}; - {ok, _, _} -> - {error, {undef, "Illegal callback usage. Mode and filename is incompatible."}}; - {error, {Code, Text}} -> - {error, {Code, Text}} - end; -prepare(_Peer, _Access, _Bin, _Mode, _SuggestedOptions, _Initial) -> - {error, {undef, "Illegal callback options."}}. - -%%------------------------------------------------------------------- -%% Open -%%------------------------------------------------------------------- - -open(Peer, Access, Filename, Mode, SuggestedOptions, Initial) when is_list(Initial) -> - %% Server side - case prepare(Peer, Access, Filename, Mode, SuggestedOptions, Initial) of - {ok, AcceptedOptions, State} -> - open(Peer, Access, Filename, Mode, AcceptedOptions, State); - {error, {Code, Text}} -> - {error, {Code, Text}} - end; -open(_Peer, Access, Filename, Mode, NegotiatedOptions, State) when is_record(State, read_state) -> - %% Both sides - case catch handle_options(Access, Filename, Mode, NegotiatedOptions, State#read_state.is_native_ascii) of - {ok, IsNetworkAscii, Options} - when Options =:= NegotiatedOptions, - IsNetworkAscii =:= State#read_state.is_network_ascii -> - {ok, NegotiatedOptions, State}; - {error, {Code, Text}} -> - {error, {Code, Text}} - end; -open(_Peer, Access, Filename, Mode, NegotiatedOptions, State) when is_record(State, write_state) -> - %% Both sides - case catch handle_options(Access, Filename, Mode, NegotiatedOptions, State#write_state.is_native_ascii) of - {ok, IsNetworkAscii, Options} - when Options =:= NegotiatedOptions, - IsNetworkAscii =:= State#write_state.is_network_ascii -> - {ok, NegotiatedOptions, State}; - {error, {Code, Text}} -> - {error, {Code, Text}} - end; -open(Peer, Access, Filename, Mode, NegotiatedOptions, State) -> - %% Handle upgrade from old releases. Please, remove this clause in next release. - State2 = upgrade_state(State), - open(Peer, Access, Filename, Mode, NegotiatedOptions, State2). - -%%------------------------------------------------------------------- -%% Read -%%------------------------------------------------------------------- - -read(#read_state{bin = Bin} = State) when is_binary(Bin) -> - BlkSize = State#read_state.blksize, - if - size(Bin) >= BlkSize -> - <<Block:BlkSize/binary, Bin2/binary>> = Bin, - State2 = State#read_state{bin = Bin2}, - {more, Block, State2}; - size(Bin) < BlkSize -> - {last, Bin, State#read_state.count} - end; -read(State) -> - %% Handle upgrade from old releases. Please, remove this clause in next release. - State2 = upgrade_state(State), - read(State2). - -%%------------------------------------------------------------------- -%% Write -%%------------------------------------------------------------------- - -write(Bin, #write_state{list = List} = State) when is_binary(Bin), is_list(List) -> - Size = size(Bin), - BlkSize = State#write_state.blksize, - if - Size =:= BlkSize -> - {more, State#write_state{list = [Bin | List]}}; - Size < BlkSize -> - Bin2 = list_to_binary(lists:reverse([Bin | List])), - {last, Bin2} - end; -write(Bin, State) -> - %% Handle upgrade from old releases. Please, remove this clause in next release. - State2 = upgrade_state(State), - write(Bin, State2). - -%%------------------------------------------------------------------- -%% Abort -%%------------------------------------------------------------------- - -abort(_Code, _Text, #read_state{bin = Bin} = State) - when is_record(State, read_state), is_binary(Bin) -> - ok; -abort(_Code, _Text, #write_state{list = List} = State) - when is_record(State, write_state), is_list(List) -> - ok; -abort(Code, Text, State) -> - %% Handle upgrade from old releases. Please, remove this clause in next release. - State2 = upgrade_state(State), - abort(Code, Text, State2). - -%%------------------------------------------------------------------- -%% Process options -%%------------------------------------------------------------------- - -handle_options(Access, Bin, Mode, Options, IsNativeAscii) -> - IsNetworkAscii = handle_mode(Mode, IsNativeAscii), - Options2 = do_handle_options(Access, Bin, Options), - {ok, IsNetworkAscii, Options2}. - -handle_mode(Mode, IsNativeAscii) -> - case Mode of - "netascii" when IsNativeAscii =:= true -> true; - "octet" -> false; - _ -> throw({error, {badop, "Illegal mode " ++ Mode}}) - end. - -do_handle_options(Access, Bin, [{Key, Val} | T]) -> - case Key of - "tsize" -> - case Access of - read when Val =:= "0", is_binary(Bin) -> - Tsize = integer_to_list(size(Bin)), - [{Key, Tsize} | do_handle_options(Access, Bin, T)]; - _ -> - handle_integer(Access, Bin, Key, Val, T, 0, infinity) - end; - "blksize" -> - handle_integer(Access, Bin, Key, Val, T, 8, 65464); - "timeout" -> - handle_integer(Access, Bin, Key, Val, T, 1, 255); - _ -> - do_handle_options(Access, Bin, T) - end; -do_handle_options(_Access, _Bin, []) -> - []. - - -handle_integer(Access, Bin, Key, Val, Options, Min, Max) -> - case catch list_to_integer(Val) of - {'EXIT', _} -> - do_handle_options(Access, Bin, Options); - Int when Int >= Min, Int =< Max -> - [{Key, Val} | do_handle_options(Access, Bin, Options)]; - Int when Int >= Min, Max =:= infinity -> - [{Key, Val} | do_handle_options(Access, Bin, Options)]; - _Int -> - throw({error, {badopt, "Illegal " ++ Key ++ " value " ++ Val}}) - end. - -lookup_blksize(Options) -> - case lists:keysearch("blksize", 1, Options) of - {value, {_, Val}} -> - list_to_integer(Val); - false -> - 512 - end. - -is_native_ascii([]) -> - is_native_ascii(); -is_native_ascii([{native_ascii, Bool}]) -> - case Bool of - true -> true; - false -> false - end. - -is_native_ascii() -> - case os:type() of - {win32, _} -> true; - _ -> false - end. - -%% Handle upgrade from old releases. Please, remove this function in next release. -upgrade_state({read_state, Options, Blksize, Bin, IsNetworkAscii, Count}) -> - {read_state, Options, Blksize, Bin, false, IsNetworkAscii, Count}; -upgrade_state({write_state, Options, Blksize, List, IsNetworkAscii}) -> - {write_state, Options, Blksize, List, false, IsNetworkAscii}. diff --git a/lib/inets/src/tftp/tftp_engine.erl b/lib/inets/src/tftp/tftp_engine.erl deleted file mode 100644 index 493a29a68f..0000000000 --- a/lib/inets/src/tftp/tftp_engine.erl +++ /dev/null @@ -1,1421 +0,0 @@ -%% -%% %CopyrightBegin% -%% -%% Copyright Ericsson AB 2005-2009. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%% -%% %CopyrightEnd% -%% -%%------------------------------------------------------------------- -%% Protocol engine for trivial FTP -%%------------------------------------------------------------------- - --module(tftp_engine). - -%%%------------------------------------------------------------------- -%%% Interface -%%%------------------------------------------------------------------- - -%% application internal functions --export([ - daemon_start/1, - daemon_loop/1, - daemon_loop/3, %% Handle upgrade from old releases. Please, remove this function in next release. - client_start/4, - common_loop/6, - info/1, - change_config/2 - ]). - -%% module internal --export([ - daemon_init/1, - server_init/2, - client_init/2, - wait_for_msg/3, - callback/4 - ]). - -%% sys callback functions --export([ - system_continue/3, - system_terminate/4, - system_code_change/4 - ]). - --include("tftp.hrl"). - --type prep_status() :: 'error' | 'last' | 'more' | 'terminate'. - --record(daemon_state, {config, n_servers, server_tab, file_tab}). --record(server_info, {pid, req, peer}). --record(file_info, {peer_req, pid}). --record(sys_misc, {module, function, arguments}). --record(error, {where, code, text, filename}). --record(prepared, {status :: prep_status(), result, block_no, next_data, prev_data}). --record(transfer_res, {status, decoded_msg, prepared}). --define(ERROR(Where, Code, Text, Filename), - #error{where = Where, code = Code, text = Text, filename = Filename}). - -%%%------------------------------------------------------------------- -%%% Info -%%%------------------------------------------------------------------- - -info(daemons) -> - Daemons = supervisor:which_children(tftp_sup), - [{Pid, info(Pid)} || {_, Pid, _, _} <- Daemons]; -info(servers) -> - [{Pid, info(Pid)} || {_, {ok, DeamonInfo}} <- info(daemons), - {server, Pid} <- DeamonInfo]; -info(ToPid) when is_pid(ToPid) -> - call(info, ToPid, timer:seconds(10)). - -change_config(daemons, Options) -> - Daemons = supervisor:which_children(tftp_sup), - [{Pid, change_config(Pid, Options)} || {_, Pid, _, _} <- Daemons]; -change_config(servers, Options) -> - [{Pid, change_config(Pid, Options)} || {_, {ok, DeamonInfo}} <- info(daemons), - {server, Pid} <- DeamonInfo]; -change_config(ToPid, Options) when is_pid(ToPid) -> - BadKeys = [host, port, udp], - BadOptions = [{Key, Val} || {Key, Val} <- Options, - BadKey <- BadKeys, - Key =:= BadKey], - case BadOptions of - [] -> - call({change_config, Options}, ToPid, timer:seconds(10)); - [{Key, Val} | _] -> - {error, {badarg, {Key, Val}}} - end. - -call(Req, ToPid, Timeout) when is_pid(ToPid) -> - Type = process, - Ref = erlang:monitor(Type, ToPid), - ToPid ! {Req, Ref, self()}, - receive - {Reply, Ref, FromPid} when FromPid =:= ToPid -> - erlang:demonitor(Ref, [flush]), - Reply; - {'DOWN', Ref, Type, FromPid, _Reason} when FromPid =:= ToPid -> - {error, timeout} - after Timeout -> - {error, timeout} - end. - -reply(Reply, Ref, ToPid) -> - ToPid ! {Reply, Ref, self()}. - -%%%------------------------------------------------------------------- -%%% Daemon -%%%------------------------------------------------------------------- - -%% Returns {ok, Port} -daemon_start(Options) when is_list(Options) -> - Config = tftp_lib:parse_config(Options), - proc_lib:start_link(?MODULE, daemon_init, [Config], infinity). - -daemon_init(Config) when is_record(Config, config), - is_pid(Config#config.parent_pid) -> - process_flag(trap_exit, true), - {Port, UdpOptions} = prepare_daemon_udp(Config), - case catch gen_udp:open(Port, UdpOptions) of - {ok, Socket} -> - {ok, ActualPort} = inet:port(Socket), - proc_lib:init_ack({ok, self()}), - Config2 = Config#config{udp_socket = Socket, - udp_port = ActualPort}, - print_debug_info(Config2, daemon, open, #tftp_msg_req{filename = ""}), - ServerTab = ets:new(tftp_daemon_servers, [{keypos, 2}]), - FileTab = ets:new(tftp_daemon_files, [{keypos, 2}]), - State = #daemon_state{config = Config2, - n_servers = 0, - server_tab = ServerTab, - file_tab = FileTab}, - daemon_loop(State); - {error, Reason} -> - Text = lists:flatten(io_lib:format("UDP open ~p -> ~p", [UdpOptions, Reason])), - print_debug_info(Config, daemon, open, ?ERROR(open, undef, Text, "")), - exit({gen_udp_open, UdpOptions, Reason}); - Reason -> - Text = lists:flatten(io_lib:format("UDP open ~p -> ~p", [UdpOptions, Reason])), - print_debug_info(Config, daemon, open, ?ERROR(open, undef, Text, "")), - exit({gen_udp_open, UdpOptions, Reason}) - end. - -prepare_daemon_udp(#config{udp_port = Port, udp_options = UdpOptions} = Config) -> - case lists:keymember(fd, 1, UdpOptions) of - true -> - %% Use explicit fd - {Port, UdpOptions}; - false -> - %% Use fd from setuid_socket_wrap, such as -tftpd_69 - InitArg = list_to_atom("tftpd_" ++ integer_to_list(Port)), - case init:get_argument(InitArg) of - {ok, [[FdStr]] = Badarg} when is_list(FdStr) -> - case catch list_to_integer(FdStr) of - Fd when is_integer(Fd) -> - {0, [{fd, Fd} | lists:keydelete(ip, 1, UdpOptions)]}; - {'EXIT', _} -> - Text = lists:flatten(io_lib:format("Illegal prebound fd ~p: ~p", [InitArg, Badarg])), - print_debug_info(Config, daemon, open, ?ERROR(open, undef, Text, "")), - exit({badarg, {prebound_fd, InitArg, Badarg}}) - end; - {ok, Badarg} -> - Text = lists:flatten(io_lib:format("Illegal prebound fd ~p: ~p", [InitArg, Badarg])), - print_debug_info(Config, daemon, open, ?ERROR(open, undef, Text, "")), - exit({badarg, {prebound_fd, InitArg, Badarg}}); - error -> - {Port, UdpOptions} - end - end. - -daemon_loop(DaemonConfig, N, Servers) when is_list(Servers) -> - %% Handle upgrade from old releases. Please, remove this function in next release. - ServerTab = ets:new(tftp_daemon_servers, [{keypos, 2}]), - FileTab = ets:new(tftp_daemon_files, [{keypos, 2}]), - State = #daemon_state{config = DaemonConfig, - n_servers = N, - server_tab = ServerTab, - file_tab = FileTab}, - Req = #tftp_msg_req{filename = dummy}, - [ets:insert(ServerTab, #server_info{pid = Pid, req = Req, peer = dummy}) || Pid <- Servers], - daemon_loop(State). - -daemon_loop(#daemon_state{config = DaemonConfig, - n_servers = N, - server_tab = ServerTab, - file_tab = FileTab} = State) when is_record(DaemonConfig, config) -> - %% info_msg(DaemonConfig, "=====> TFTP: Daemon #~p\n", [N]), %% XXX - receive - {info, Ref, FromPid} when is_pid(FromPid) -> - Fun = fun(#server_info{pid = Pid}, Acc) -> [{server, Pid} | Acc] end, - ServerInfo = ets:foldl(Fun, [], ServerTab), - Info = internal_info(DaemonConfig, daemon) ++ [{n_conn, N}] ++ ServerInfo, - reply({ok, Info}, Ref, FromPid), - ?MODULE:daemon_loop(State); - {{change_config, Options}, Ref, FromPid} when is_pid(FromPid) -> - case catch tftp_lib:parse_config(Options, DaemonConfig) of - {'EXIT', Reason} -> - reply({error, Reason}, Ref, FromPid), - ?MODULE:daemon_loop(State); - DaemonConfig2 when is_record(DaemonConfig2, config) -> - reply(ok, Ref, FromPid), - ?MODULE:daemon_loop(State#daemon_state{config = DaemonConfig2}) - end; - {udp, Socket, RemoteHost, RemotePort, Bin} when is_binary(Bin) -> - inet:setopts(Socket, [{active, once}]), - ServerConfig = DaemonConfig#config{parent_pid = self(), - udp_host = RemoteHost, - udp_port = RemotePort}, - Msg = (catch tftp_lib:decode_msg(Bin)), - print_debug_info(ServerConfig, daemon, recv, Msg), - case Msg of - Req when is_record(Req, tftp_msg_req), - N =< DaemonConfig#config.max_conn -> - Peer = peer_info(ServerConfig), - PeerReq = {Peer, Req}, - PeerInfo = lists:flatten(io_lib:format("~p", [Peer])), - case ets:lookup(FileTab, PeerReq) of - [] -> - Args = [ServerConfig, Req], - Pid = proc_lib:spawn_link(?MODULE, server_init, Args), - ets:insert(ServerTab, #server_info{pid = Pid, req = Req, peer = Peer}), - ets:insert(FileTab, #file_info{peer_req = PeerReq, pid = Pid}), - ?MODULE:daemon_loop(State#daemon_state{n_servers = N + 1}); - [#file_info{pid = Pid}] -> - %% Yet another request of the file from same peer - warning_msg(DaemonConfig, "~p Reuse connection for ~s\n\t~p\n", - [Pid, PeerInfo, Req#tftp_msg_req.filename]), - ?MODULE:daemon_loop(State) - end; - Req when is_record(Req, tftp_msg_req) -> - Reply = #tftp_msg_error{code = enospc, text = "Too many connections"}, - Peer = peer_info(ServerConfig), - PeerInfo = lists:flatten(io_lib:format("~p", [Peer])), - warning_msg(DaemonConfig, - "Daemon has too many connections (~p)." - "\n\tRejecting request from ~s\n", - [N, PeerInfo]), - send_msg(ServerConfig, daemon, Reply), - ?MODULE:daemon_loop(State); - {'EXIT', Reply} when is_record(Reply, tftp_msg_error) -> - send_msg(ServerConfig, daemon, Reply), - ?MODULE:daemon_loop(State); - Req -> - Reply = #tftp_msg_error{code = badop, - text = "Illegal TFTP operation"}, - warning_msg(DaemonConfig, "Daemon received: ~p.\n\tfrom ~p:~p", - [Req, RemoteHost, RemotePort]), - send_msg(ServerConfig, daemon, Reply), - ?MODULE:daemon_loop(State) - end; - {system, From, Msg} -> - Misc = #sys_misc{module = ?MODULE, function = daemon_loop, arguments = [State]}, - sys:handle_system_msg(Msg, From, DaemonConfig#config.parent_pid, ?MODULE, [], Misc); - {'EXIT', Pid, Reason} when DaemonConfig#config.parent_pid =:= Pid -> - close_port(DaemonConfig, daemon, #tftp_msg_req{filename = ""}), - exit(Reason); - {'EXIT', Pid, _Reason} = Info -> - case ets:lookup(ServerTab, Pid) of - [] -> - warning_msg(DaemonConfig, "Daemon received: ~p", [Info]), - ?MODULE:daemon_loop(State); - [#server_info{req = Req, peer = Peer}] -> - PeerReq = {Peer, Req}, - ets:delete(FileTab, PeerReq), - ets:delete(ServerTab, Pid), - ?MODULE:daemon_loop(State#daemon_state{n_servers = N - 1}) - end; - Info -> - warning_msg(DaemonConfig, "Daemon received: ~p", [Info]), - ?MODULE:daemon_loop(State) - end; -daemon_loop(#daemon_state{config = Config} = State) -> - %% Handle upgrade from old releases. Please, remove this clause in next release. - Config2 = upgrade_config(Config), - daemon_loop(State#daemon_state{config = Config2}). - -upgrade_config({config, ParentPid, UdpSocket, UdpOptions, UdpHost, UdpPort, PortPolicy, - UseTsize, MaxTsize, MaxConn, Rejected, PoliteAck, DebugLevel, - Timeout, UserOptions, Callbacks}) -> - Callbacks2 = tftp_lib:add_default_callbacks(Callbacks), - Logger = tftp_logger, - MaxRetries = 5, - {config, ParentPid, UdpSocket, UdpOptions, UdpHost, UdpPort, PortPolicy, - UseTsize, MaxTsize, MaxConn, Rejected, PoliteAck, DebugLevel, - Timeout, UserOptions, Callbacks2, Logger, MaxRetries}. - -%%%------------------------------------------------------------------- -%%% Server -%%%------------------------------------------------------------------- - -server_init(Config, Req) when is_record(Config, config), - is_pid(Config#config.parent_pid), - is_record(Req, tftp_msg_req) -> - process_flag(trap_exit, true), - %% Config = - %% case os:getenv("TFTPDEBUG") of - %% false -> - %% Config0; - %% DebugLevel -> - %% Config0#config{debug_level = list_to_atom(DebugLevel)} - %% end, - SuggestedOptions = Req#tftp_msg_req.options, - UdpOptions = Config#config.udp_options, - UdpOptions2 = lists:keydelete(fd, 1, UdpOptions), - Config1 = Config#config{udp_options = UdpOptions2}, - Config2 = tftp_lib:parse_config(SuggestedOptions, Config1), - SuggestedOptions2 = Config2#config.user_options, - Req2 = Req#tftp_msg_req{options = SuggestedOptions2}, - case open_free_port(Config2, server, Req2) of - {ok, Config3} -> - Filename = Req#tftp_msg_req.filename, - case match_callback(Filename, Config3#config.callbacks) of - {ok, Callback} -> - print_debug_info(Config3, server, match, Callback), - case pre_verify_options(Config3, Req2) of - ok -> - case callback({open, server_open}, Config3, Callback, Req2) of - {Callback2, {ok, AcceptedOptions}} -> - {LocalAccess, _} = local_file_access(Req2), - OptText = "Internal error. Not allowed to add new options.", - case post_verify_options(Config3, Req2, AcceptedOptions, OptText) of - {ok, Config4, Req3} when AcceptedOptions =/= [] -> - Reply = #tftp_msg_oack{options = AcceptedOptions}, - BlockNo = - case LocalAccess of - read -> 0; - write -> 1 - end, - {Config5, Callback3, TransferRes} = - transfer(Config4, Callback2, Req3, Reply, LocalAccess, BlockNo, #prepared{}), - common_loop(Config5, Callback3, Req3, TransferRes, LocalAccess, BlockNo); - {ok, Config4, Req3} when LocalAccess =:= write -> - BlockNo = 0, - common_ack(Config4, Callback2, Req3, LocalAccess, BlockNo, #prepared{}); - {ok, Config4, Req3} when LocalAccess =:= read -> - BlockNo = 0, - common_read(Config4, Callback2, Req3, LocalAccess, BlockNo, BlockNo, #prepared{}); - {error, {Code, Text}} -> - {undefined, Error} = - callback({abort, {Code, Text}}, Config3, Callback2, Req2), - send_msg(Config3, Req, Error), - terminate(Config3, Req2, ?ERROR(post_verify_options, Code, Text, Req2#tftp_msg_req.filename)) - end; - {undefined, #tftp_msg_error{code = Code, text = Text} = Error} -> - send_msg(Config3, Req, Error), - terminate(Config3, Req, ?ERROR(server_open, Code, Text, Req2#tftp_msg_req.filename)) - end; - {error, {Code, Text}} -> - {undefined, Error} = - callback({abort, {Code, Text}}, Config2, Callback, Req2), - send_msg(Config2, Req, Error), - terminate(Config2, Req2, ?ERROR(pre_verify_options, Code, Text, Req2#tftp_msg_req.filename)) - end; - {error, #tftp_msg_error{code = Code, text = Text} = Error} -> - send_msg(Config3, Req, Error), - terminate(Config3, Req, ?ERROR(match_callback, Code, Text, Req2#tftp_msg_req.filename)) - end; - #error{} = Error -> - terminate(Config2, Req, Error) - end; -server_init(Config, Req) when is_record(Req, tftp_msg_req) -> - Config2 = upgrade_config(Config), - server_init(Config2, Req). - -%%%------------------------------------------------------------------- -%%% Client -%%%------------------------------------------------------------------- - -%% LocalFilename = filename() | 'binary' | binary() -%% Returns {ok, LastCallbackState} | {error, Reason} -client_start(Access, RemoteFilename, LocalFilename, Options) -> - Config = tftp_lib:parse_config(Options), - Config2 = Config#config{parent_pid = self(), - udp_socket = undefined}, - Req = #tftp_msg_req{access = Access, - filename = RemoteFilename, - mode = lookup_mode(Config2#config.user_options), - options = Config2#config.user_options, - local_filename = LocalFilename}, - Args = [Config2, Req], - case proc_lib:start_link(?MODULE, client_init, Args, infinity) of - {ok, LastCallbackState} -> - {ok, LastCallbackState}; - {error, Error} -> - {error, Error} - end. - -client_init(Config, Req) when is_record(Config, config), - is_pid(Config#config.parent_pid), - is_record(Req, tftp_msg_req) -> - process_flag(trap_exit, true), - %% Config = - %% case os:getenv("TFTPDEBUG") of - %% false -> - %% Config0; - %% "none" -> - %% Config0; - %% DebugLevel -> - %% info_msg(Config, "TFTPDEBUG: ~s\n", [DebugLevel]), - %% Config0#config{debug_level = list_to_atom(DebugLevel)} - %% end, - case open_free_port(Config, client, Req) of - {ok, Config2} -> - Req2 = - case Config2#config.use_tsize of - true -> - SuggestedOptions = Req#tftp_msg_req.options, - SuggestedOptions2 = tftp_lib:replace_val("tsize", "0", SuggestedOptions), - Req#tftp_msg_req{options = SuggestedOptions2}; - false -> - Req - end, - LocalFilename = Req2#tftp_msg_req.local_filename, - case match_callback(LocalFilename, Config2#config.callbacks) of - {ok, Callback} -> - print_debug_info(Config2, client, match, Callback), - client_prepare(Config2, Callback, Req2); - {error, #tftp_msg_error{code = Code, text = Text}} -> - terminate(Config, Req, ?ERROR(match, Code, Text, Req#tftp_msg_req.filename)) - end; - #error{} = Error -> - terminate(Config, Req, Error) - end. - -client_prepare(Config, Callback, Req) when is_record(Req, tftp_msg_req) -> - case pre_verify_options(Config, Req) of - ok -> - case callback({open, client_prepare}, Config, Callback, Req) of - {Callback2, {ok, AcceptedOptions}} -> - OptText = "Internal error. Not allowed to add new options.", - case post_verify_options(Config, Req, AcceptedOptions, OptText) of - {ok, Config2, Req2} -> - {LocalAccess, _} = local_file_access(Req2), - BlockNo = 0, - {Config3, Callback3, TransferRes} = - transfer(Config2, Callback2, Req2, Req2, LocalAccess, BlockNo, #prepared{}), - client_open(Config3, Callback3, Req2, BlockNo, TransferRes); - {error, {Code, Text}} -> - callback({abort, {Code, Text}}, Config, Callback2, Req), - terminate(Config, Req, ?ERROR(post_verify_options, Code, Text, Req#tftp_msg_req.filename)) - end; - {undefined, #tftp_msg_error{code = Code, text = Text}} -> - terminate(Config, Req, ?ERROR(client_prepare, Code, Text, Req#tftp_msg_req.filename)) - end; - {error, {Code, Text}} -> - callback({abort, {Code, Text}}, Config, Callback, Req), - terminate(Config, Req, ?ERROR(pre_verify_options, Code, Text, Req#tftp_msg_req.filename)) - end. - -client_open(Config, Callback, Req, BlockNo, #transfer_res{status = Status, decoded_msg = DecodedMsg, prepared = Prepared}) -> - {LocalAccess, _} = local_file_access(Req), - case Status of - ok when is_record(Prepared, prepared) -> - case DecodedMsg of - Msg when is_record(Msg, tftp_msg_oack) -> - ServerOptions = Msg#tftp_msg_oack.options, - OptText = "Protocol violation. Server is not allowed new options", - case post_verify_options(Config, Req, ServerOptions, OptText) of - {ok, Config2, Req2} -> - {Config3, Callback2, Req3} = - do_client_open(Config2, Callback, Req2), - case LocalAccess of - read -> - common_read(Config3, Callback2, Req3, LocalAccess, BlockNo, BlockNo, Prepared); - write -> - common_ack(Config3, Callback2, Req3, LocalAccess, BlockNo, Prepared) - end; - {error, {Code, Text}} -> - {undefined, Error} = - callback({abort, {Code, Text}}, Config, Callback, Req), - send_msg(Config, Req, Error), - terminate(Config, Req, ?ERROR(verify_server_options, Code, Text, Req#tftp_msg_req.filename)) - end; - #tftp_msg_ack{block_no = ActualBlockNo} when LocalAccess =:= read -> - Req2 = Req#tftp_msg_req{options = []}, - {Config2, Callback2, Req2} = do_client_open(Config, Callback, Req2), - ExpectedBlockNo = 0, - common_read(Config2, Callback2, Req2, LocalAccess, ExpectedBlockNo, ActualBlockNo, Prepared); - #tftp_msg_data{block_no = ActualBlockNo, data = Data} when LocalAccess =:= write -> - Req2 = Req#tftp_msg_req{options = []}, - {Config2, Callback2, Req2} = do_client_open(Config, Callback, Req2), - ExpectedBlockNo = 1, - common_write(Config2, Callback2, Req2, LocalAccess, ExpectedBlockNo, ActualBlockNo, Data, Prepared); - %% #tftp_msg_error{code = Code, text = Text} when Req#tftp_msg_req.options =/= [] -> - %% %% Retry without options - %% callback({abort, {Code, Text}}, Config, Callback, Req), - %% Req2 = Req#tftp_msg_req{options = []}, - %% client_prepare(Config, Callback, Req2); - #tftp_msg_error{code = Code, text = Text} -> - callback({abort, {Code, Text}}, Config, Callback, Req), - terminate(Config, Req, ?ERROR(client_open, Code, Text, Req#tftp_msg_req.filename)); - {'EXIT', #tftp_msg_error{code = Code, text = Text}} -> - callback({abort, {Code, Text}}, Config, Callback, Req), - terminate(Config, Req, ?ERROR(client_open, Code, Text, Req#tftp_msg_req.filename)); - Msg when is_tuple(Msg) -> - Code = badop, - Text = "Illegal TFTP operation", - {undefined, Error} = - callback({abort, {Code, Text}}, Config, Callback, Req), - send_msg(Config, Req, Error), - Text2 = lists:flatten([Text, ". ", io_lib:format("~p", [element(1, Msg)])]), - terminate(Config, Req, ?ERROR(client_open, Code, Text2, Req#tftp_msg_req.filename)) - end; - error when is_record(Prepared, tftp_msg_error) -> - #tftp_msg_error{code = Code, text = Text} = Prepared, - callback({abort, {Code, Text}}, Config, Callback, Req), - terminate(Config, Req, ?ERROR(client_open, Code, Text, Req#tftp_msg_req.filename)) - end. - -do_client_open(Config, Callback, Req) -> - case callback({open, client_open}, Config, Callback, Req) of - {Callback2, {ok, FinalOptions}} -> - OptText = "Internal error. Not allowed to change options.", - case post_verify_options(Config, Req, FinalOptions, OptText) of - {ok, Config2, Req2} -> - {Config2, Callback2, Req2}; - {error, {Code, Text}} -> - {undefined, Error} = - callback({abort, {Code, Text}}, Config, Callback2, Req), - send_msg(Config, Req, Error), - terminate(Config, Req, ?ERROR(post_verify_options, Code, Text, Req#tftp_msg_req.filename)) - end; - {undefined, #tftp_msg_error{code = Code, text = Text} = Error} -> - send_msg(Config, Req, Error), - terminate(Config, Req, ?ERROR(client_open, Code, Text, Req#tftp_msg_req.filename)) - end. - -%%%------------------------------------------------------------------- -%%% Common loop for both client and server -%%%------------------------------------------------------------------- - -common_loop(Config, Callback, Req, #transfer_res{status = Status, decoded_msg = DecodedMsg, prepared = Prepared}, LocalAccess, ExpectedBlockNo) - when is_record(Config, config)-> - %% Config = - %% case os:getenv("TFTPMAX") of - %% false -> - %% Config0; - %% MaxBlockNoStr when Config0#config.debug_level =/= none -> - %% case list_to_integer(MaxBlockNoStr) of - %% MaxBlockNo when ExpectedBlockNo > MaxBlockNo -> - %% info_msg(Config, "TFTPMAX: ~p\n", [MaxBlockNo]), - %% info_msg(Config, "TFTPDEBUG: none\n", []), - %% Config0#config{debug_level = none}; - %% _ -> - %% Config0 - %% end; - %% _MaxBlockNoStr -> - %% Config0 - %% end, - case Status of - ok when is_record(Prepared, prepared) -> - case DecodedMsg of - #tftp_msg_ack{block_no = ActualBlockNo} when LocalAccess =:= read -> - common_read(Config, Callback, Req, LocalAccess, ExpectedBlockNo, ActualBlockNo, Prepared); - #tftp_msg_data{block_no = ActualBlockNo, data = Data} when LocalAccess =:= write -> - common_write(Config, Callback, Req, LocalAccess, ExpectedBlockNo, ActualBlockNo, Data, Prepared); - #tftp_msg_error{code = Code, text = Text} -> - callback({abort, {Code, Text}}, Config, Callback, Req), - terminate(Config, Req, ?ERROR(common_loop, Code, Text, Req#tftp_msg_req.filename)); - {'EXIT', #tftp_msg_error{code = Code, text = Text} = Error} -> - callback({abort, {Code, Text}}, Config, Callback, Req), - send_msg(Config, Req, Error), - terminate(Config, Req, ?ERROR(common_loop, Code, Text, Req#tftp_msg_req.filename)); - Msg when is_tuple(Msg) -> - Code = badop, - Text = "Illegal TFTP operation", - {undefined, Error} = - callback({abort, {Code, Text}}, Config, Callback, Req), - send_msg(Config, Req, Error), - Text2 = lists:flatten([Text, ". ", io_lib:format("~p", [element(1, Msg)])]), - terminate(Config, Req, ?ERROR(common_loop, Code, Text2, Req#tftp_msg_req.filename)) - end; - error when is_record(Prepared, tftp_msg_error) -> - #tftp_msg_error{code = Code, text = Text} = Prepared, - send_msg(Config, Req, Prepared), - terminate(Config, Req, ?ERROR(transfer, Code, Text, Req#tftp_msg_req.filename)) - end; -common_loop(Config, Callback, Req, TransferRes, LocalAccess, ExpectedBlockNo) -> - %% Handle upgrade from old releases. Please, remove this clause in next release. - Config2 = upgrade_config(Config), - common_loop(Config2, Callback, Req, TransferRes, LocalAccess, ExpectedBlockNo). - --spec common_read(#config{}, #callback{}, _, 'read', _, _, #prepared{}) -> no_return(). - -common_read(Config, _, Req, _, _, _, #prepared{status = terminate, result = Result}) -> - terminate(Config, Req, {ok, Result}); -common_read(Config, Callback, Req, LocalAccess, ExpectedBlockNo, ActualBlockNo, Prepared) - when ActualBlockNo =:= ExpectedBlockNo, is_record(Prepared, prepared) -> - case early_read(Config, Callback, Req, LocalAccess, ActualBlockNo, Prepared) of - {Callback2, #prepared{status = more, next_data = Data} = Prepared2} when is_binary(Data) -> - Prepared3 = Prepared2#prepared{prev_data = Data, next_data = undefined}, - do_common_read(Config, Callback2, Req, LocalAccess, ActualBlockNo, Data, Prepared3); - {undefined, #prepared{status = last, next_data = Data} = Prepared2} when is_binary(Data) -> - Prepared3 = Prepared2#prepared{status = terminate}, - do_common_read(Config, undefined, Req, LocalAccess, ActualBlockNo, Data, Prepared3); - {undefined, #prepared{status = error, result = Error}} -> - #tftp_msg_error{code = Code, text = Text} = Error, - send_msg(Config, Req, Error), - terminate(Config, Req, ?ERROR(read, Code, Text, Req#tftp_msg_req.filename)) - end; -common_read(Config, Callback, Req, LocalAccess, ExpectedBlockNo, ActualBlockNo, Prepared) - when ActualBlockNo =:= (ExpectedBlockNo - 1), is_record(Prepared, prepared) -> - case Prepared of - #prepared{status = more, prev_data = Data} when is_binary(Data) -> - do_common_read(Config, Callback, Req, LocalAccess, ActualBlockNo, Data, Prepared); - #prepared{status = last, prev_data = Data} when is_binary(Data) -> - do_common_read(Config, Callback, Req, LocalAccess, ActualBlockNo, Data, Prepared); - #prepared{status = error, result = Error} -> - #tftp_msg_error{code = Code, text = Text} = Error, - send_msg(Config, Req, Error), - terminate(Config, Req, ?ERROR(read, Code, Text, Req#tftp_msg_req.filename)) - end; -common_read(Config, Callback, Req, LocalAccess, ExpectedBlockNo, ActualBlockNo, Prepared) - when ActualBlockNo =< ExpectedBlockNo, is_record(Prepared, prepared) -> - %% error_logger:error_msg("TFTP READ ~s: Expected block ~p but got block ~p - IGNORED\n", - %% [Req#tftp_msg_req.filename, ExpectedBlockNo, ActualBlockNo]), - case Prepared of - #prepared{status = more, prev_data = Data} when is_binary(Data) -> - Reply = #tftp_msg_data{block_no = ExpectedBlockNo, data = Data}, - {Config2, Callback2, TransferRes} = - wait_for_msg_and_handle_timeout(Config, Callback, Req, Reply, LocalAccess, ExpectedBlockNo, Prepared), - ?MODULE:common_loop(Config2, Callback2, Req, TransferRes, LocalAccess, ExpectedBlockNo); - #prepared{status = last, prev_data = Data} when is_binary(Data) -> - Reply = #tftp_msg_data{block_no = ExpectedBlockNo, data = Data}, - {Config2, Callback2, TransferRes} = - wait_for_msg_and_handle_timeout(Config, Callback, Req, Reply, LocalAccess, ExpectedBlockNo, Prepared), - ?MODULE:common_loop(Config2, Callback2, Req, TransferRes, LocalAccess, ExpectedBlockNo); - #prepared{status = error, result = Error} -> - #tftp_msg_error{code = Code, text = Text} = Error, - send_msg(Config, Req, Error), - terminate(Config, Req, ?ERROR(read, Code, Text, Req#tftp_msg_req.filename)) - end; -common_read(Config, Callback, Req, _LocalAccess, ExpectedBlockNo, ActualBlockNo, Prepared) - when is_record(Prepared, prepared) -> - Code = badblk, - Text = "Unknown transfer ID = " ++ - integer_to_list(ActualBlockNo) ++ " (" ++ integer_to_list(ExpectedBlockNo) ++ ")", - {undefined, Error} = - callback({abort, {Code, Text}}, Config, Callback, Req), - send_msg(Config, Req, Error), - terminate(Config, Req, ?ERROR(read, Code, Text, Req#tftp_msg_req.filename)). - --spec do_common_read(#config{}, #callback{} | undefined, _, 'read', integer(), binary(), #prepared{}) -> no_return(). - -do_common_read(Config, Callback, Req, LocalAccess, BlockNo, Data, Prepared) - when is_binary(Data), is_record(Prepared, prepared) -> - NextBlockNo = (BlockNo + 1) rem 65536, - Reply = #tftp_msg_data{block_no = NextBlockNo, data = Data}, - {Config2, Callback2, TransferRes} = - transfer(Config, Callback, Req, Reply, LocalAccess, NextBlockNo, Prepared), - ?MODULE:common_loop(Config2, Callback2, Req, TransferRes, LocalAccess, NextBlockNo). - --spec common_write(#config{}, #callback{}, _, 'write', integer(), integer(), _, #prepared{}) -> no_return(). - -common_write(Config, _, Req, _, _, _, _, #prepared{status = terminate, result = Result}) -> - terminate(Config, Req, {ok, Result}); -common_write(Config, Callback, Req, LocalAccess, ExpectedBlockNo, ActualBlockNo, Data, Prepared) - when ActualBlockNo =:= ExpectedBlockNo, is_binary(Data), is_record(Prepared, prepared) -> - case callback({write, Data}, Config, Callback, Req) of - {Callback2, #prepared{status = more} = Prepared2} -> - common_ack(Config, Callback2, Req, LocalAccess, ActualBlockNo, Prepared2); - {undefined, #prepared{status = last, result = Result} = Prepared2} -> - Config2 = pre_terminate(Config, Req, {ok, Result}), - Prepared3 = Prepared2#prepared{status = terminate}, - common_ack(Config2, undefined, Req, LocalAccess, ActualBlockNo, Prepared3); - {undefined, #prepared{status = error, result = Error}} -> - #tftp_msg_error{code = Code, text = Text} = Error, - send_msg(Config, Req, Error), - terminate(Config, Req, ?ERROR(write, Code, Text, Req#tftp_msg_req.filename)) - end; -common_write(Config, Callback, Req, LocalAccess, ExpectedBlockNo, ActualBlockNo, Data, Prepared) - when ActualBlockNo =:= (ExpectedBlockNo - 1), is_binary(Data), is_record(Prepared, prepared) -> - common_ack(Config, Callback, Req, LocalAccess, ExpectedBlockNo - 1, Prepared); -common_write(Config, Callback, Req, LocalAccess, ExpectedBlockNo, ActualBlockNo, Data, Prepared) - when ActualBlockNo =< ExpectedBlockNo, is_binary(Data), is_record(Prepared, prepared) -> - %% error_logger:error_msg("TFTP WRITE ~s: Expected block ~p but got block ~p - IGNORED\n", - %% [Req#tftp_msg_req.filename, ExpectedBlockNo, ActualBlockNo]), - Reply = #tftp_msg_ack{block_no = ExpectedBlockNo}, - {Config2, Callback2, TransferRes} = - wait_for_msg_and_handle_timeout(Config, Callback, Req, Reply, LocalAccess, ExpectedBlockNo, Prepared), - ?MODULE:common_loop(Config2, Callback2, Req, TransferRes, LocalAccess, ExpectedBlockNo); -common_write(Config, Callback, Req, _, ExpectedBlockNo, ActualBlockNo, Data, Prepared) - when is_binary(Data), is_record(Prepared, prepared) -> - Code = badblk, - Text = "Unknown transfer ID = " ++ - integer_to_list(ActualBlockNo) ++ " (" ++ integer_to_list(ExpectedBlockNo) ++ ")", - {undefined, Error} = - callback({abort, {Code, Text}}, Config, Callback, Req), - send_msg(Config, Req, Error), - terminate(Config, Req, ?ERROR(write, Code, Text, Req#tftp_msg_req.filename)). - -common_ack(Config, Callback, Req, LocalAccess, BlockNo, Prepared) - when is_record(Prepared, prepared) -> - Reply = #tftp_msg_ack{block_no = BlockNo}, - NextBlockNo = (BlockNo + 1) rem 65536, - {Config2, Callback2, TransferRes} = - transfer(Config, Callback, Req, Reply, LocalAccess, NextBlockNo, Prepared), - ?MODULE:common_loop(Config2, Callback2, Req, TransferRes, LocalAccess, NextBlockNo). - -pre_terminate(Config, Req, Result) -> - if - Req#tftp_msg_req.local_filename =/= undefined, - Config#config.parent_pid =/= undefined -> - proc_lib:init_ack(Result), - unlink(Config#config.parent_pid), - Config#config{parent_pid = undefined, polite_ack = true}; - true -> - Config#config{polite_ack = true} - end. - --spec terminate(#config{}, #tftp_msg_req{}, {'ok', _} | #error{}) -> no_return(). - -terminate(Config, Req, Result) -> - Result2 = - case Result of - {ok, _} -> - Result; - #error{where = Where, code = Code, text = Text} = Error -> - print_debug_info(Config, Req, Where, Error#error{filename = Req#tftp_msg_req.filename}), - {error, {Where, Code, Text}} - end, - if - Config#config.parent_pid =:= undefined -> - close_port(Config, client, Req), - exit(normal); - Req#tftp_msg_req.local_filename =/= undefined -> - %% Client - close_port(Config, client, Req), - proc_lib:init_ack(Result2), - unlink(Config#config.parent_pid), - exit(normal); - true -> - %% Server - close_port(Config, server, Req), - exit(shutdown) - end. - -close_port(Config, Who, Req) when is_record(Req, tftp_msg_req) -> - case Config#config.udp_socket of - undefined -> - ignore; - Socket -> - print_debug_info(Config, Who, close, Req), - gen_udp:close(Socket) - end. - -open_free_port(Config, Who, Req) when is_record(Config, config), is_record(Req, tftp_msg_req) -> - UdpOptions = Config#config.udp_options, - case Config#config.port_policy of - random -> - %% BUGBUG: Should be a random port - case catch gen_udp:open(0, UdpOptions) of - {ok, Socket} -> - Config2 = Config#config{udp_socket = Socket}, - print_debug_info(Config2, Who, open, Req), - {ok, Config2}; - {error, Reason} -> - Text = lists:flatten(io_lib:format("UDP open ~p -> ~p", [[0 | UdpOptions], Reason])), - ?ERROR(open, undef, Text, Req#tftp_msg_req.filename); - {'EXIT', _} = Reason -> - Text = lists:flatten(io_lib:format("UDP open ~p -> ~p", [[0 | UdpOptions], Reason])), - ?ERROR(open, undef, Text, Req#tftp_msg_req.filename) - end; - {range, Port, Max} when Port =< Max -> - case catch gen_udp:open(Port, UdpOptions) of - {ok, Socket} -> - Config2 = Config#config{udp_socket = Socket}, - print_debug_info(Config2, Who, open, Req), - {ok, Config2}; - {error, eaddrinuse} -> - PortPolicy = {range, Port + 1, Max}, - Config2 = Config#config{port_policy = PortPolicy}, - open_free_port(Config2, Who, Req); - {error, Reason} -> - Text = lists:flatten(io_lib:format("UDP open ~p -> ~p", [[Port | UdpOptions], Reason])), - ?ERROR(open, undef, Text, Req#tftp_msg_req.filename); - {'EXIT', _} = Reason-> - Text = lists:flatten(io_lib:format("UDP open ~p -> ~p", [[Port | UdpOptions], Reason])), - ?ERROR(open, undef, Text, Req#tftp_msg_req.filename) - end; - {range, Port, _Max} -> - Reason = "Port range exhausted", - Text = lists:flatten(io_lib:format("UDP open ~p -> ~p", [[Port | UdpOptions], Reason])), - ?ERROR(Who, undef, Text, Req#tftp_msg_req.filename) - end. - -%%------------------------------------------------------------------- -%% Transfer -%%------------------------------------------------------------------- - -%% Returns {Config, Callback, #transfer_res{}} -transfer(Config, Callback, Req, Msg, LocalAccess, NextBlockNo, Prepared) - when is_record(Prepared, prepared) -> - IoList = tftp_lib:encode_msg(Msg), - Retries = Config#config.max_retries + 1, - do_transfer(Config, Callback, Req, Msg, IoList, LocalAccess, NextBlockNo, Prepared, Retries). - -do_transfer(Config, Callback, Req, Msg, IoList, LocalAccess, NextBlockNo, Prepared, Retries) - when is_record(Prepared, prepared), is_integer(Retries), Retries >= 0 -> - case do_send_msg(Config, Req, Msg, IoList) of - ok -> - {Callback2, Prepared2} = - early_read(Config, Callback, Req, LocalAccess, NextBlockNo, Prepared), - do_wait_for_msg_and_handle_timeout(Config, Callback2, Req, Msg, IoList, LocalAccess, NextBlockNo, Prepared2, Retries); - {error, _Reason} when Retries > 0 -> - Retries2 = 0, % Just retry once when send fails - do_transfer(Config, Callback, Req, Msg, IoList, LocalAccess, NextBlockNo, Prepared, Retries2); - {error, Reason} -> - Code = undef, - Text = lists:flatten(io_lib:format("Transfer failed - giving up -> ~p", [Reason])), - Error = #tftp_msg_error{code = Code, text = Text}, - {Config, Callback, #transfer_res{status = error, prepared = Error}} - end. - -wait_for_msg_and_handle_timeout(Config, Callback, Req, Msg, LocalAccess, NextBlockNo, Prepared) -> - IoList = tftp_lib:encode_msg(Msg), - Retries = Config#config.max_retries + 1, - do_wait_for_msg_and_handle_timeout(Config, Callback, Req, Msg, IoList, LocalAccess, NextBlockNo, Prepared, Retries). - -do_wait_for_msg_and_handle_timeout(Config, Callback, Req, Msg, IoList, LocalAccess, NextBlockNo, Prepared, Retries) -> - Code = undef, - Text = "Transfer timed out.", - case wait_for_msg(Config, Callback, Req) of - timeout when Config#config.polite_ack =:= true -> - do_send_msg(Config, Req, Msg, IoList), - case Prepared of - #prepared{status = terminate, result = Result} -> - terminate(Config, Req, {ok, Result}); - #prepared{} -> - terminate(Config, Req, ?ERROR(transfer, Code, Text, Req#tftp_msg_req.filename)) - end; - timeout when Retries > 0 -> - Retries2 = Retries - 1, - do_transfer(Config, Callback, Req, Msg, IoList, LocalAccess, NextBlockNo, Prepared, Retries2); - timeout -> - Error = #tftp_msg_error{code = Code, text = Text}, - {Config, Callback, #transfer_res{status = error, prepared = Error}}; - {Config2, DecodedMsg} -> - {Config2, Callback, #transfer_res{status = ok, decoded_msg = DecodedMsg, prepared = Prepared}} - end. - -send_msg(Config, Req, Msg) -> - case catch tftp_lib:encode_msg(Msg) of - {'EXIT', Reason} -> - Code = undef, - Text = "Internal error. Encode failed", - Msg2 = #tftp_msg_error{code = Code, text = Text, details = Reason}, - send_msg(Config, Req, Msg2); - IoList -> - do_send_msg(Config, Req, Msg, IoList) - end. - -do_send_msg(#config{udp_socket = Socket, udp_host = RemoteHost, udp_port = RemotePort} = Config, Req, Msg, IoList) -> - %% {ok, LocalPort} = inet:port(Socket), - %% if - %% LocalPort =/= ?TFTP_DEFAULT_PORT -> - %% ok; - %% true -> - %% print_debug_info(Config#config{debug_level = all}, Req, send, Msg), - %% error(Config, - %% "Daemon replies from the default port (~p)\n\t to ~p:~p\n\t¨~p\n", - %% [LocalPort, RemoteHost, RemotePort, Msg]) - %% end, - - print_debug_info(Config, Req, send, Msg), - - %% case os:getenv("TFTPDUMP") of - %% false -> - %% ignore; - %% DumpPath -> - %% trace_udp_send(Req, Msg, IoList, DumpPath) - %% end, - Res = gen_udp:send(Socket, RemoteHost, RemotePort, IoList), - case Res of - ok -> - ok; - {error, einval = Reason} -> - error_msg(Config, - "Stacktrace; ~p\n gen_udp:send(~p, ~p, ~p, ~p) -> ~p\n", - [erlang:get_stacktrace(), Socket, RemoteHost, RemotePort, IoList, {error, Reason}]); - {error, Reason} -> - {error, Reason} - end. - -%% trace_udp_send(#tftp_msg_req{filename = [$/ | RelFile]} = Req, Msg, IoList, DumpPath) -> -%% trace_udp_send(Req#tftp_msg_req{filename = RelFile}, Msg, IoList, DumpPath); -%% trace_udp_send(#tftp_msg_req{filename = RelFile}, -%% #tftp_msg_data{block_no = BlockNo, data = Data}, -%% _IoList, -%% DumpPath) -> -%% File = filename:join([DumpPath, RelFile, "block" ++ string:right(integer_to_list(BlockNo), 5, $0) ++ ".dump"]), -%% if -%% (BlockNo rem 1000) =:= 1 -> -%% info_msg(Config, "TFTPDUMP: Data ~s\n", [File]); -%% true -> -%% ignore -%% end, -%% ok = filelib:ensure_dir(File), -%% ok = file:write_file(File, Data); -%% trace_udp_send(#tftp_msg_req{filename = RelFile}, Msg, _IoList, _DumpPath) -> -%% info_msg(Config, "TFTPDUMP: No data ~s -> ~p\n", [RelFile, element(1, Msg)]). - -wait_for_msg(Config, Callback, Req) -> - receive - {udp, Socket, RemoteHost, RemotePort, Bin} - when is_binary(Bin), Callback#callback.block_no =:= undefined -> - %% Client prepare - inet:setopts(Socket, [{active, once}]), - Config2 = Config#config{udp_host = RemoteHost, - udp_port = RemotePort}, - DecodedMsg = (catch tftp_lib:decode_msg(Bin)), - print_debug_info(Config2, Req, recv, DecodedMsg), - {Config2, DecodedMsg}; - {udp, Socket, Host, Port, Bin} when is_binary(Bin), - Config#config.udp_host =:= Host, - Config#config.udp_port =:= Port -> - inet:setopts(Socket, [{active, once}]), - DecodedMsg = (catch tftp_lib:decode_msg(Bin)), - print_debug_info(Config, Req, recv, DecodedMsg), - {Config, DecodedMsg}; - {info, Ref, FromPid} when is_pid(FromPid) -> - Type = - case Req#tftp_msg_req.local_filename =/= undefined of - true -> client; - false -> server - end, - Info = internal_info(Config, Type), - reply({ok, Info}, Ref, FromPid), - wait_for_msg(Config, Callback, Req); - {{change_config, Options}, Ref, FromPid} when is_pid(FromPid) -> - case catch tftp_lib:parse_config(Options, Config) of - {'EXIT', Reason} -> - reply({error, Reason}, Ref, FromPid), - wait_for_msg(Config, Callback, Req); - Config2 when is_record(Config2, config) -> - reply(ok, Ref, FromPid), - wait_for_msg(Config2, Callback, Req) - end; - {system, From, Msg} -> - Misc = #sys_misc{module = ?MODULE, function = wait_for_msg, arguments = [Config, Callback, Req]}, - sys:handle_system_msg(Msg, From, Config#config.parent_pid, ?MODULE, [], Misc); - {'EXIT', Pid, _Reason} when Config#config.parent_pid =:= Pid -> - Code = undef, - Text = "Parent exited.", - terminate(Config, Req, ?ERROR(wait_for_msg, Code, Text, Req#tftp_msg_req.filename)); - Msg when Req#tftp_msg_req.local_filename =/= undefined -> - warning_msg(Config, "Client received : ~p", [Msg]), - wait_for_msg(Config, Callback, Req); - Msg when Req#tftp_msg_req.local_filename =:= undefined -> - warning_msg(Config, "Server received : ~p", [Msg]), - wait_for_msg(Config, Callback, Req) - after Config#config.timeout * 1000 -> - print_debug_info(Config, Req, recv, timeout), - timeout - end. - -early_read(Config, Callback, Req, LocalAccess, _NextBlockNo, - #prepared{status = Status, next_data = NextData, prev_data = PrevData} = Prepared) -> - if - Status =/= terminate, - LocalAccess =:= read, - Callback#callback.block_no =/= undefined, - NextData =:= undefined -> - case callback(read, Config, Callback, Req) of - {undefined, Error} when is_record(Error, tftp_msg_error) -> - {undefined, Error}; - {Callback2, Prepared2} when is_record(Prepared2, prepared)-> - {Callback2, Prepared2#prepared{prev_data = PrevData}} - end; - true -> - {Callback, Prepared} - end. - -%%------------------------------------------------------------------- -%% Callback -%%------------------------------------------------------------------- - -callback(Access, Config, Callback, Req) -> - {Callback2, Result} = - do_callback(Access, Config, Callback, Req), - print_debug_info(Config, Req, call, {Callback2, Result}), - {Callback2, Result}. - -do_callback(read = Fun, Config, Callback, Req) - when is_record(Config, config), - is_record(Callback, callback), - is_record(Req, tftp_msg_req) -> - Args = [Callback#callback.state], - NextBlockNo = Callback#callback.block_no + 1, - case catch safe_apply(Callback#callback.module, Fun, Args) of - {more, Bin, NewState} when is_binary(Bin) -> - Count = Callback#callback.count + size(Bin), - Callback2 = Callback#callback{state = NewState, - block_no = NextBlockNo, - count = Count}, - Prepared = #prepared{status = more, - result = undefined, - block_no = NextBlockNo, - next_data = Bin}, - verify_count(Config, Callback2, Req, Prepared); - {last, Bin, Result} when is_binary(Bin) -> - Prepared = #prepared{status = last, - result = Result, - block_no = NextBlockNo, - next_data = Bin}, - {undefined, Prepared}; - {error, {Code, Text}} -> - Error = #tftp_msg_error{code = Code, text = Text}, - Prepared = #prepared{status = error, - result = Error}, - {undefined, Prepared}; - Illegal -> - Code = undef, - Text = "Internal error. File handler error.", - callback({abort, {Code, Text, Illegal}}, Config, Callback, Req) - end; -do_callback({write = Fun, Bin}, Config, Callback, Req) - when is_record(Config, config), - is_record(Callback, callback), - is_record(Req, tftp_msg_req), - is_binary(Bin) -> - Args = [Bin, Callback#callback.state], - NextBlockNo = Callback#callback.block_no + 1, - case catch safe_apply(Callback#callback.module, Fun, Args) of - {more, NewState} -> - Count = Callback#callback.count + size(Bin), - Callback2 = Callback#callback{state = NewState, - block_no = NextBlockNo, - count = Count}, - Prepared = #prepared{status = more, - block_no = NextBlockNo}, - verify_count(Config, Callback2, Req, Prepared); - {last, Result} -> - Prepared = #prepared{status = last, - result = Result, - block_no = NextBlockNo}, - {undefined, Prepared}; - {error, {Code, Text}} -> - Error = #tftp_msg_error{code = Code, text = Text}, - Prepared = #prepared{status = error, - result = Error}, - {undefined, Prepared}; - Illegal -> - Code = undef, - Text = "Internal error. File handler error.", - callback({abort, {Code, Text, Illegal}}, Config, Callback, Req) - end; -do_callback({open, Type}, Config, Callback, Req) - when is_record(Config, config), - is_record(Callback, callback), - is_record(Req, tftp_msg_req) -> - {Access, Filename} = local_file_access(Req), - {Fun, BlockNo} = - case Type of - client_prepare -> {prepare, undefined}; - client_open -> {open, 0}; - server_open -> {open, 0} - end, - Mod = Callback#callback.module, - Args = [Access, - Filename, - Req#tftp_msg_req.mode, - Req#tftp_msg_req.options, - Callback#callback.state], - PeerInfo = peer_info(Config), - fast_ensure_loaded(Mod), - Args2 = - case erlang:function_exported(Mod, Fun, length(Args)) of - true -> Args; - false -> [PeerInfo | Args] - end, - case catch safe_apply(Mod, Fun, Args2) of - {ok, AcceptedOptions, NewState} -> - Callback2 = Callback#callback{state = NewState, - block_no = BlockNo, - count = 0}, - {Callback2, {ok, AcceptedOptions}}; - {error, {Code, Text}} -> - {undefined, #tftp_msg_error{code = Code, text = Text}}; - Illegal -> - Code = undef, - Text = "Internal error. File handler error.", - callback({abort, {Code, Text, Illegal}}, Config, Callback, Req) - end; -do_callback({abort, {Code, Text}}, Config, Callback, Req) -> - Error = #tftp_msg_error{code = Code, text = Text}, - do_callback({abort, Error}, Config, Callback, Req); -do_callback({abort, {Code, Text, Details}}, Config, Callback, Req) -> - Error = #tftp_msg_error{code = Code, text = Text, details = Details}, - do_callback({abort, Error}, Config, Callback, Req); -do_callback({abort = Fun, #tftp_msg_error{code = Code, text = Text} = Error}, Config, Callback, Req) - when is_record(Config, config), - is_record(Callback, callback), - is_record(Req, tftp_msg_req) -> - Args = [Code, Text, Callback#callback.state], - catch safe_apply(Callback#callback.module, Fun, Args), - {undefined, Error}; -do_callback({abort, Error}, _Config, undefined, _Req) when is_record(Error, tftp_msg_error) -> - {undefined, Error}. - -peer_info(#config{udp_host = Host, udp_port = Port}) -> - if - is_tuple(Host), size(Host) =:= 4 -> - {inet, tftp_lib:host_to_string(Host), Port}; - is_tuple(Host), size(Host) =:= 8 -> - {inet6, tftp_lib:host_to_string(Host), Port}; - true -> - {undefined, Host, Port} - end. - -match_callback(Filename, Callbacks) -> - if - Filename =:= binary -> - lookup_callback_mod(tftp_binary, Callbacks); - is_binary(Filename) -> - lookup_callback_mod(tftp_binary, Callbacks); - true -> - do_match_callback(Filename, Callbacks) - end. - -do_match_callback(Filename, [C | Tail]) when is_record(C, callback) -> - case catch re:run(Filename, C#callback.internal, [{capture, none}]) of - match -> - {ok, C}; - nomatch -> - do_match_callback(Filename, Tail); - Details -> - Code = baduser, - Text = "Internal error. File handler not found", - {error, #tftp_msg_error{code = Code, text = Text, details = Details}} - end; -do_match_callback(Filename, []) -> - Code = baduser, - Text = "Internal error. File handler not found", - {error, #tftp_msg_error{code = Code, text = Text, details = Filename}}. - -lookup_callback_mod(Mod, Callbacks) -> - {value, C} = lists:keysearch(Mod, #callback.module, Callbacks), - {ok, C}. - -verify_count(Config, Callback, Req, Result) -> - case Config#config.max_tsize of - infinity -> - {Callback, Result}; - Max when Callback#callback.count =< Max -> - {Callback, Result}; - _Max -> - Code = enospc, - Text = "Too large file.", - callback({abort, {Code, Text}}, Config, Callback, Req) - end. - -%%------------------------------------------------------------------- -%% Miscellaneous -%%------------------------------------------------------------------- - -internal_info(Config, Type) when is_record(Config, config) -> - {ok, ActualPort} = inet:port(Config#config.udp_socket), - [ - {type, Type}, - {host, tftp_lib:host_to_string(Config#config.udp_host)}, - {port, Config#config.udp_port}, - {local_port, ActualPort}, - {port_policy, Config#config.port_policy}, - {udp, Config#config.udp_options}, - {use_tsize, Config#config.use_tsize}, - {max_tsize, Config#config.max_tsize}, - {max_conn, Config#config.max_conn}, - {rejected, Config#config.rejected}, - {timeout, Config#config.timeout}, - {polite_ack, Config#config.polite_ack}, - {debug, Config#config.debug_level}, - {parent_pid, Config#config.parent_pid} - ] ++ Config#config.user_options ++ Config#config.callbacks. - -local_file_access(#tftp_msg_req{access = Access, - local_filename = Local, - filename = Filename}) -> - case Local =:= undefined of - true -> - %% Server side - {Access, Filename}; - false -> - %% Client side - case Access of - read -> {write, Local}; - write -> {read, Local} - end - end. - -pre_verify_options(Config, Req) -> - Options = Req#tftp_msg_req.options, - case catch verify_reject(Config, Req, Options) of - ok -> - case verify_integer("tsize", 0, Config#config.max_tsize, Options) of - true -> - case verify_integer("blksize", 0, 65464, Options) of - true -> - ok; - false -> - {error, {badopt, "Too large blksize"}} - end; - false -> - {error, {badopt, "Too large tsize"}} - end; - {error, Reason} -> - {error, Reason} - end. - -post_verify_options(Config, Req, NewOptions, Text) -> - OldOptions = Req#tftp_msg_req.options, - BadOptions = - [Key || {Key, _Val} <- NewOptions, - not lists:keymember(Key, 1, OldOptions)], - case BadOptions =:= [] of - true -> - Config2 = Config#config{timeout = lookup_timeout(NewOptions)}, - Req2 = Req#tftp_msg_req{options = NewOptions}, - {ok, Config2, Req2}; - false -> - {error, {badopt, Text}} - end. - -verify_reject(Config, Req, Options) -> - Access = Req#tftp_msg_req.access, - Rejected = Config#config.rejected, - case lists:member(Access, Rejected) of - true -> - {error, {eacces, atom_to_list(Access) ++ " mode not allowed"}}; - false -> - [throw({error, {badopt, Key ++ " not allowed"}}) || - {Key, _} <- Options, lists:member(Key, Rejected)], - ok - end. - -lookup_timeout(Options) -> - case lists:keysearch("timeout", 1, Options) of - {value, {_, Val}} -> - list_to_integer(Val); - false -> - 3 - end. - -lookup_mode(Options) -> - case lists:keysearch("mode", 1, Options) of - {value, {_, Val}} -> - Val; - false -> - "octet" - end. - -verify_integer(Key, Min, Max, Options) -> - case lists:keysearch(Key, 1, Options) of - {value, {_, Val}} when is_list(Val) -> - case catch list_to_integer(Val) of - {'EXIT', _} -> - false; - Int when Int >= Min, is_integer(Min), - Max =:= infinity -> - true; - Int when Int >= Min, is_integer(Min), - Int =< Max, is_integer(Max) -> - true; - _ -> - false - end; - false -> - true - end. - -error_msg(#config{logger = Logger, debug_level = _Level}, F, A) -> - safe_apply(Logger, error_msg, [F, A]). - -warning_msg(#config{logger = Logger, debug_level = Level}, F, A) -> - case Level of - none -> ok; - error -> ok; - _ -> safe_apply(Logger, warning_msg, [F, A]) - end. - -info_msg(#config{logger = Logger}, F, A) -> - safe_apply(Logger, info_msg, [F, A]). - -safe_apply(Mod, Fun, Args) -> - fast_ensure_loaded(Mod), - apply(Mod, Fun, Args). - -fast_ensure_loaded(Mod) -> - case erlang:function_exported(Mod, module_info, 0) of - true -> - ok; - false -> - Res = code:load_file(Mod), - %% io:format("tftp: code:load_file(~p) -> ~p\n", [Mod, Res]), %% XXX - Res - end. - -print_debug_info(#config{debug_level = Level} = Config, Who, Where, Data) -> - if - Level =:= none -> - ok; - is_record(Data, error) -> - do_print_debug_info(Config, Who, Where, Data); - Level =:= warning -> - ok; - Level =:= error -> - ok; - Level =:= all -> - do_print_debug_info(Config, Who, Where, Data); - Where =:= open -> - do_print_debug_info(Config, Who, Where, Data); - Where =:= close -> - do_print_debug_info(Config, Who, Where, Data); - Level =:= brief -> - ok; - Where =/= recv, Where =/= send -> - ok; - is_record(Data, tftp_msg_data), Level =:= normal -> - ok; - is_record(Data, tftp_msg_ack), Level =:= normal -> - ok; - true -> - do_print_debug_info(Config, Who, Where, Data) - end. - -do_print_debug_info(Config, Who, Where, #tftp_msg_data{data = Bin} = Msg) when is_binary(Bin) -> - Msg2 = Msg#tftp_msg_data{data = {bytes, size(Bin)}}, - do_print_debug_info(Config, Who, Where, Msg2); -do_print_debug_info(Config, Who, Where, #tftp_msg_req{local_filename = Filename} = Msg) when is_binary(Filename) -> - Msg2 = Msg#tftp_msg_req{local_filename = binary}, - do_print_debug_info(Config, Who, Where, Msg2); -do_print_debug_info(Config, Who, Where, Data) -> - Local = - case catch inet:port(Config#config.udp_socket) of - {'EXIT', _Reason} -> - 0; - {ok, Port} -> - Port - end, - %% Remote = Config#config.udp_port, - PeerInfo = lists:flatten(io_lib:format("~p", [peer_info(Config)])), - Side = - if - is_record(Who, tftp_msg_req), - Who#tftp_msg_req.local_filename =/= undefined -> - client; - is_record(Who, tftp_msg_req), - Who#tftp_msg_req.local_filename =:= undefined -> - server; - is_atom(Who) -> - Who - end, - case {Where, Data} of - {_, #error{where = Where, code = Code, text = Text, filename = Filename}} -> - do_format(Config, Side, Local, "error ~s ->\n\t~p ~p\n\t~p ~p: ~s\n", - [PeerInfo, self(), Filename, Where, Code, Text]); - {open, #tftp_msg_req{filename = Filename}} -> - do_format(Config, Side, Local, "open ~s ->\n\t~p ~p\n", - [PeerInfo, self(), Filename]); - {close, #tftp_msg_req{filename = Filename}} -> - do_format(Config, Side, Local, "close ~s ->\n\t~p ~p\n", - [PeerInfo, self(), Filename]); - {recv, _} -> - do_format(Config, Side, Local, "recv ~s <-\n\t~p\n", - [PeerInfo, Data]); - {send, _} -> - do_format(Config, Side, Local, "send ~s ->\n\t~p\n", - [PeerInfo, Data]); - {match, _} when is_record(Data, callback) -> - Mod = Data#callback.module, - State = Data#callback.state, - do_format(Config, Side, Local, "match ~s ~p =>\n\t~p\n", - [PeerInfo, Mod, State]); - {call, _} -> - case Data of - {Callback, _Result} when is_record(Callback, callback) -> - Mod = Callback#callback.module, - State = Callback#callback.state, - do_format(Config, Side, Local, "call ~s ~p =>\n\t~p\n", - [PeerInfo, Mod, State]); - {undefined, Result} -> - do_format(Config, Side, Local, "call ~s result =>\n\t~p\n", - [PeerInfo, Result]) - end - end. - -do_format(Config, Side, Local, Format, Args) -> - info_msg(Config, "~p(~p): " ++ Format, [Side, Local | Args]). - -%%------------------------------------------------------------------- -%% System upgrade -%%------------------------------------------------------------------- - -system_continue(_Parent, _Debug, #sys_misc{module = Mod, function = Fun, arguments = Args}) -> - apply(Mod, Fun, Args); -system_continue(Parent, Debug, {Fun, Args}) -> - %% Handle upgrade from old releases. Please, remove this clause in next release. - system_continue(Parent, Debug, #sys_misc{module = ?MODULE, function = Fun, arguments = Args}). - --spec system_terminate(_, _, _, #sys_misc{} | {_, _}) -> no_return(). - -system_terminate(Reason, _Parent, _Debug, #sys_misc{}) -> - exit(Reason); -system_terminate(Reason, Parent, Debug, {Fun, Args}) -> - %% Handle upgrade from old releases. Please, remove this clause in next release. - system_terminate(Reason, Parent, Debug, #sys_misc{module = ?MODULE, function = Fun, arguments = Args}). - -system_code_change({Fun, Args}, _Module, _OldVsn, _Extra) -> - {ok, {Fun, Args}}. diff --git a/lib/inets/src/tftp/tftp_file.erl b/lib/inets/src/tftp/tftp_file.erl deleted file mode 100644 index 39382d6fd0..0000000000 --- a/lib/inets/src/tftp/tftp_file.erl +++ /dev/null @@ -1,390 +0,0 @@ -%% -%% %CopyrightBegin% -%% -%% Copyright Ericsson AB 2005-2009. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%% -%% %CopyrightEnd% -%% -%% - -%%%------------------------------------------------------------------- -%%% File : tft_file.erl -%%% Author : Hakan Mattsson <[email protected]> -%%% Description : -%%% -%%% Created : 24 May 2004 by Hakan Mattsson <[email protected]> -%%%------------------------------------------------------------------- - --module(tftp_file). - -%%%------------------------------------------------------------------- -%%% Interface -%%%------------------------------------------------------------------- - --behaviour(tftp). - --export([prepare/6, open/6, read/1, write/2, abort/3]). - -%%%------------------------------------------------------------------- -%%% Defines -%%%------------------------------------------------------------------- - --include_lib("kernel/include/file.hrl"). - --record(initial, - {filename, - is_native_ascii}). - --record(state, - {access, - filename, - is_native_ascii, - is_network_ascii, - root_dir, - options, - blksize, - fd, - count, - buffer}). - -%%------------------------------------------------------------------- -%% prepare(Peer, Access, Filename, Mode, SuggestedOptions, InitialState) -> -%% {ok, AcceptedOptions, NewState} | {error, Code, Text} -%% -%% Peer = {PeerType, PeerHost, PeerPort} -%% PeerType = inet | inet6 -%% PeerHost = ip_address() -%% PeerPort = integer() -%% Acess = read | write -%% Filename = string() -%% Mode = string() -%% SuggestedOptions = [{Key, Value}] -%% AcceptedOptions = [{Key, Value}] -%% Key = string() -%% Value = string() -%% InitialState = [] | [{root_dir, string()}] -%% NewState = term() -%% Code = undef | enoent | eacces | enospc | -%% badop | eexist | baduser | badopt | -%% integer() -%% Text = string() -%% -%% Prepares open of a file on the client side. -%% -%% Will be followed by a call to open/4 before any read/write access -%% is performed. The AcceptedOptions will be sent to the server which -%% will reply with those options that it accepts. The options that are -%% accepted by the server will be forwarded to open/4 as SuggestedOptions. -%% -%% No new options may be added, but the ones that are present as -%% SuggestedOptions may be omitted or replaced with new values -%% in the AcceptedOptions. -%%------------------------------------------------------------------- - -prepare(_Peer, Access, Filename, Mode, SuggestedOptions, Initial) when is_list(Initial) -> - %% Client side - case catch handle_options(Access, Filename, Mode, SuggestedOptions, Initial) of - {ok, Filename2, IsNativeAscii, IsNetworkAscii, AcceptedOptions} -> - State = #state{access = Access, - filename = Filename2, - is_native_ascii = IsNativeAscii, - is_network_ascii = IsNetworkAscii, - options = AcceptedOptions, - blksize = lookup_blksize(AcceptedOptions), - count = 0, - buffer = []}, - {ok, AcceptedOptions, State}; - {error, {Code, Text}} -> - {error, {Code, Text}} - end. - -%% --------------------------------------------------------- -%% open(Peer, Access, Filename, Mode, SuggestedOptions, State) -> -%% {ok, AcceptedOptions, NewState} | {error, Code, Text} -%% -%% Peer = {PeerType, PeerHost, PeerPort} -%% PeerType = inet | inet6 -%% PeerHost = ip_address() -%% PeerPort = integer() -%% Acess = read | write -%% Filename = string() -%% Mode = string() -%% SuggestedOptions = [{Key, Value}] -%% AcceptedOptions = [{Key, Value}] -%% Key = string() -%% Value = string() -%% State = InitialState | #state{} -%% InitialState = [] | [{root_dir, string()}] -%% NewState = term() -%% Code = undef | enoent | eacces | enospc | -%% badop | eexist | baduser | badopt | -%% integer() -%% Text = string() -%% -%% Opens a file for read or write access. -%% -%% On the client side where the open/4 call has been preceeded by a -%% call to prepare/4, all options must be accepted or rejected. -%% On the server side, where there are no preceeding prepare/4 call, -%% noo new options may be added, but the ones that are present as -%% SuggestedOptions may be omitted or replaced with new values -%% in the AcceptedOptions. -%%------------------------------------------------------------------- - -open(Peer, Access, Filename, Mode, SuggestedOptions, Initial) when is_list(Initial) -> - %% Server side - case prepare(Peer, Access, Filename, Mode, SuggestedOptions, Initial) of - {ok, AcceptedOptions, State} -> - open(Peer, Access, Filename, Mode, AcceptedOptions, State); - {error, {Code, Text}} -> - {error, {Code, Text}} - end; -open(_Peer, Access, Filename, Mode, NegotiatedOptions, State) when is_record(State, state) -> - %% Both sides - case catch handle_options(Access, Filename, Mode, NegotiatedOptions, State) of - {ok, _Filename2, _IsNativeAscii, _IsNetworkAscii, Options} - when Options =:= NegotiatedOptions -> - do_open(State); - {error, {Code, Text}} -> - {error, {Code, Text}} - end; -open(Peer, Access, Filename, Mode, NegotiatedOptions, State) -> - %% Handle upgrade from old releases. Please, remove this clause in next release. - State2 = upgrade_state(State), - open(Peer, Access, Filename, Mode, NegotiatedOptions, State2). - -do_open(State) when is_record(State, state) -> - case file:open(State#state.filename, file_options(State)) of - {ok, Fd} -> - {ok, State#state.options, State#state{fd = Fd}}; - {error, Reason} when is_atom(Reason) -> - {error, file_error(Reason)} - end. - -file_options(State) -> - case State#state.access of - read -> [read, read_ahead, raw, binary]; - write -> [write, delayed_write, raw, binary] - end. - -file_error(Reason) when is_atom(Reason) -> - Details = file:format_error(Reason), - case Reason of - eexist -> {Reason, Details}; - enoent -> {Reason, Details}; - eacces -> {Reason, Details}; - eperm -> {eacces, Details}; - enospc -> {Reason, Details}; - _ -> {undef, Details ++ " (" ++ atom_to_list(Reason) ++ ")"} - end. - -%%------------------------------------------------------------------- -%% read(State) -> -%% {more, Bin, NewState} | {last, Bin, FileSize} | {error, {Code, Text}} -%% -%% State = term() -%% NewState = term() -%% Bin = binary() -%% FileSize = integer() -%% Code = undef | enoent | eacces | enospc | -%% badop | eexist | baduser | badopt | -%% integer() -%% Text = string() -%% -%% Reads a chunk from the file -%% -%% The file is automatically closed when the last chunk is read. -%%------------------------------------------------------------------- - -read(#state{access = read} = State) -> - BlkSize = State#state.blksize, - case file:read(State#state.fd, BlkSize) of - {ok, Bin} when is_binary(Bin), size(Bin) =:= BlkSize -> - Count = State#state.count + size(Bin), - {more, Bin, State#state{count = Count}}; - {ok, Bin} when is_binary(Bin), size(Bin) < BlkSize -> - file:close(State#state.fd), - Count = State#state.count + size(Bin), - {last, Bin, Count}; - eof -> - {last, <<>>, State#state.count}; - {error, Reason} -> - file:close(State#state.fd), - {error, file_error(Reason)} - end; -read(State) -> - %% Handle upgrade from old releases. Please, remove this clause in next release. - State2 = upgrade_state(State), - read(State2). - -%%------------------------------------------------------------------- -%% write(Bin, State) -> -%% {more, NewState} | {last, FileSize} | {error, {Code, Text}} -%% -%% State = term() -%% NewState = term() -%% Bin = binary() -%% FileSize = integer() -%% Code = undef | enoent | eacces | enospc | -%% badop | eexist | baduser | badopt | -%% integer() -%% Text = string() -%% -%% Writes a chunk to the file -%% -%% The file is automatically closed when the last chunk is written -%%------------------------------------------------------------------- - -write(Bin, #state{access = write} = State) when is_binary(Bin) -> - Size = size(Bin), - BlkSize = State#state.blksize, - case file:write(State#state.fd, Bin) of - ok when Size =:= BlkSize-> - Count = State#state.count + Size, - {more, State#state{count = Count}}; - ok when Size < BlkSize-> - file:close(State#state.fd), - Count = State#state.count + Size, - {last, Count}; - {error, Reason} -> - file:close(State#state.fd), - file:delete(State#state.filename), - {error, file_error(Reason)} - end; -write(Bin, State) -> - %% Handle upgrade from old releases. Please, remove this clause in next release. - State2 = upgrade_state(State), - write(Bin, State2). - -%%------------------------------------------------------------------- -%% abort(Code, Text, State) -> ok -%% -%% State = term() -%% Code = undef | enoent | eacces | enospc | -%% badop | eexist | baduser | badopt | -%% badblk | integer() -%% Text = string() -%% -%% Aborts the file transfer -%%------------------------------------------------------------------- - -abort(_Code, _Text, #state{fd = Fd, access = Access} = State) -> - file:close(Fd), - case Access of - write -> - ok = file:delete(State#state.filename); - read -> - ok - end. - -%%------------------------------------------------------------------- -%% Process options -%%------------------------------------------------------------------- - -handle_options(Access, Filename, Mode, Options, Initial) -> - I = #initial{filename = Filename, is_native_ascii = is_native_ascii()}, - {Filename2, IsNativeAscii} = handle_initial(Initial, I), - IsNetworkAscii = handle_mode(Mode, IsNativeAscii), - Options2 = do_handle_options(Access, Filename2, Options), - {ok, Filename2, IsNativeAscii, IsNetworkAscii, Options2}. - -handle_mode(Mode, IsNativeAscii) -> - case Mode of - "netascii" when IsNativeAscii =:= true -> true; - "octet" -> false; - _ -> throw({error, {badop, "Illegal mode " ++ Mode}}) - end. - -handle_initial([{root_dir, Dir} | Initial], I) -> - case catch filename_join(Dir, I#initial.filename) of - {'EXIT', _} -> - throw({error, {badop, "Internal error. root_dir is not a string"}}); - Filename2 -> - handle_initial(Initial, I#initial{filename = Filename2}) - end; -handle_initial([{native_ascii, Bool} | Initial], I) -> - case Bool of - true -> handle_initial(Initial, I#initial{is_native_ascii = true}); - false -> handle_initial(Initial, I#initial{is_native_ascii = false}) - end; -handle_initial([], I) when is_record(I, initial) -> - {I#initial.filename, I#initial.is_native_ascii}; -handle_initial(State, _) when is_record(State, state) -> - {State#state.filename, State#state.is_native_ascii}. - -filename_join(Dir, Filename) -> - case filename:pathtype(Filename) of - absolute -> - [_ | RelFilename] = filename:split(Filename), - filename:join([Dir, RelFilename]); - _ -> - filename:join([Dir, Filename]) - end. - -do_handle_options(Access, Filename, [{Key, Val} | T]) -> - case Key of - "tsize" -> - case Access of - read when Val =:= "0" -> - case file:read_file_info(Filename) of - {ok, FI} -> - Tsize = integer_to_list(FI#file_info.size), - [{Key, Tsize} | do_handle_options(Access, Filename, T)]; - {error, _} -> - do_handle_options(Access, Filename, T) - end; - _ -> - handle_integer(Access, Filename, Key, Val, T, 0, infinity) - end; - "blksize" -> - handle_integer(Access, Filename, Key, Val, T, 8, 65464); - "timeout" -> - handle_integer(Access, Filename, Key, Val, T, 1, 255); - _ -> - do_handle_options(Access, Filename, T) - end; -do_handle_options(_Access, _Filename, []) -> - []. - - -handle_integer(Access, Filename, Key, Val, Options, Min, Max) -> - case catch list_to_integer(Val) of - {'EXIT', _} -> - do_handle_options(Access, Filename, Options); - Int when Int >= Min, Int =< Max -> - [{Key, Val} | do_handle_options(Access, Filename, Options)]; - Int when Int >= Min, Max =:= infinity -> - [{Key, Val} | do_handle_options(Access, Filename, Options)]; - _Int -> - throw({error, {badopt, "Illegal " ++ Key ++ " value " ++ Val}}) - end. - -lookup_blksize(Options) -> - case lists:keysearch("blksize", 1, Options) of - {value, {_, Val}} -> - list_to_integer(Val); - false -> - 512 - end. - -is_native_ascii() -> - case os:type() of - {win32, _} -> true; - _ -> false - end. - -%% Handle upgrade from old releases. Please, remove this function in next release. -upgrade_state({state, Access, Filename, RootDir, Options, BlkSize, Fd, Count, Buffer}) -> - {state, Access, Filename, false, false, RootDir, Options, BlkSize, Fd, Count, Buffer}. diff --git a/lib/inets/src/tftp/tftp_lib.erl b/lib/inets/src/tftp/tftp_lib.erl deleted file mode 100644 index 01dea97d07..0000000000 --- a/lib/inets/src/tftp/tftp_lib.erl +++ /dev/null @@ -1,474 +0,0 @@ -%% -%% %CopyrightBegin% -%% -%% Copyright Ericsson AB 2005-2009. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%% -%% %CopyrightEnd% -%% -%% - -%%%------------------------------------------------------------------- -%%% File : tftp_lib.erl -%%% Author : Hakan Mattsson <[email protected]> -%%% Description : Option parsing, decode, encode etc. -%%% -%%% Created : 18 May 2004 by Hakan Mattsson <[email protected]> -%%%------------------------------------------------------------------- - --module(tftp_lib). - -%%------------------------------------------------------------------- -%% Interface -%%------------------------------------------------------------------- - -%% application internal functions --export([ - parse_config/1, - parse_config/2, - decode_msg/1, - encode_msg/1, - replace_val/3, - to_lower/1, - host_to_string/1, - add_default_callbacks/1 - ]). - -%%------------------------------------------------------------------- -%% Defines -%%------------------------------------------------------------------- - --include("tftp.hrl"). - --define(LOWER(Char), - if - Char >= $A, Char =< $Z -> - Char - ($A - $a); - true -> - Char - end). - -%%------------------------------------------------------------------- -%% Config -%%------------------------------------------------------------------- - -parse_config(Options) -> - parse_config(Options, #config{}). - -parse_config(Options, Config) -> - do_parse_config(Options, Config). - -do_parse_config([{Key, Val} | Tail], Config) when is_record(Config, config) -> - case Key of - debug -> - if - Val =:= 0; Val =:= none -> - do_parse_config(Tail, Config#config{debug_level = none}); - Val =:= 1; Val =:= error -> - do_parse_config(Tail, Config#config{debug_level = error}); - Val =:= 2; Val =:= warning -> - do_parse_config(Tail, Config#config{debug_level = warning}); - Val =:= 3; Val =:= brief -> - do_parse_config(Tail, Config#config{debug_level = brief}); - Val =:= 4; Val =:= normal -> - do_parse_config(Tail, Config#config{debug_level = normal}); - Val =:= 5; Val =:= verbose -> - do_parse_config(Tail, Config#config{debug_level = verbose}); - Val =:= 6; Val =:= all -> - do_parse_config(Tail, Config#config{debug_level = all}); - true -> - exit({badarg, {Key, Val}}) - end; - host -> - if - is_list(Val) -> - do_parse_config(Tail, Config#config{udp_host = Val}); - is_tuple(Val), size(Val) =:= 4 -> - do_parse_config(Tail, Config#config{udp_host = Val}); - is_tuple(Val), size(Val) =:= 8 -> - do_parse_config(Tail, Config#config{udp_host = Val}); - true -> - exit({badarg, {Key, Val}}) - end; - port -> - if - is_integer(Val), Val >= 0 -> - Config2 = Config#config{udp_port = Val, udp_options = Config#config.udp_options}, - do_parse_config(Tail, Config2); - true -> - exit({badarg, {Key, Val}}) - end; - port_policy -> - case Val of - random -> - do_parse_config(Tail, Config#config{port_policy = Val}); - 0 -> - do_parse_config(Tail, Config#config{port_policy = random}); - MinMax when is_integer(MinMax), MinMax > 0 -> - do_parse_config(Tail, Config#config{port_policy = {range, MinMax, MinMax}}); - {range, Min, Max} when Max >= Min, - is_integer(Min), Min > 0, - is_integer(Max), Max > 0 -> - do_parse_config(Tail, Config#config{port_policy = Val}); - true -> - exit({badarg, {Key, Val}}) - end; - udp when is_list(Val) -> - Fun = - fun({K, V}, List) when K /= active -> - replace_val(K, V, List); - (V, List) when V /= list, V /= binary -> - List ++ [V]; - (V, _List) -> - exit({badarg, {udp, [V]}}) - end, - UdpOptions = lists:foldl(Fun, Config#config.udp_options, Val), - do_parse_config(Tail, Config#config{udp_options = UdpOptions}); - use_tsize -> - case Val of - true -> - do_parse_config(Tail, Config#config{use_tsize = Val}); - false -> - do_parse_config(Tail, Config#config{use_tsize = Val}); - _ -> - exit({badarg, {Key, Val}}) - end; - max_tsize -> - if - Val =:= infinity -> - do_parse_config(Tail, Config#config{max_tsize = Val}); - is_integer(Val), Val >= 0 -> - do_parse_config(Tail, Config#config{max_tsize = Val}); - true -> - exit({badarg, {Key, Val}}) - end; - max_conn -> - if - Val =:= infinity -> - do_parse_config(Tail, Config#config{max_conn = Val}); - is_integer(Val), Val > 0 -> - do_parse_config(Tail, Config#config{max_conn = Val}); - true -> - exit({badarg, {Key, Val}}) - end; - _ when is_list(Key), is_list(Val) -> - Key2 = to_lower(Key), - Val2 = to_lower(Val), - TftpOptions = replace_val(Key2, Val2, Config#config.user_options), - do_parse_config(Tail, Config#config{user_options = TftpOptions}); - reject -> - case Val of - read -> - Rejected = [Val | Config#config.rejected], - do_parse_config(Tail, Config#config{rejected = Rejected}); - write -> - Rejected = [Val | Config#config.rejected], - do_parse_config(Tail, Config#config{rejected = Rejected}); - _ when is_list(Val) -> - Rejected = [Val | Config#config.rejected], - do_parse_config(Tail, Config#config{rejected = Rejected}); - _ -> - exit({badarg, {Key, Val}}) - end; - callback -> - case Val of - {RegExp, Mod, State} when is_list(RegExp), is_atom(Mod) -> - case re:compile(RegExp) of - {ok, Internal} -> - Callback = #callback{regexp = RegExp, - internal = Internal, - module = Mod, - state = State}, - Callbacks = Config#config.callbacks ++ [Callback], - do_parse_config(Tail, Config#config{callbacks = Callbacks}); - {error, Reason} -> - exit({badarg, {Key, Val}, Reason}) - end; - _ -> - exit({badarg, {Key, Val}}) - end; - logger -> - if - is_atom(Val) -> - do_parse_config(Tail, Config#config{logger = Val}); - true -> - exit({badarg, {Key, Val}}) - end; - max_retries -> - if - is_integer(Val), Val > 0 -> - do_parse_config(Tail, Config#config{max_retries = Val}); - true -> - exit({badarg, {Key, Val}}) - end; - _ -> - exit({badarg, {Key, Val}}) - end; -do_parse_config([], #config{udp_host = Host, - udp_options = UdpOptions, - user_options = UserOptions, - callbacks = Callbacks} = Config) -> - IsInet6 = lists:member(inet6, UdpOptions), - IsInet = lists:member(inet, UdpOptions), - Host2 = - if - (IsInet and not IsInet6); (not IsInet and not IsInet6) -> - case inet:getaddr(Host, inet) of - {ok, Addr} -> - Addr; - {error, Reason} -> - exit({badarg, {host, Reason}}) - end; - (IsInet6 and not IsInet) -> - case inet:getaddr(Host, inet6) of - {ok, Addr} -> - Addr; - {error, Reason} -> - exit({badarg, {host, Reason}}) - end; - true -> - %% Conflicting options - exit({badarg, {udp, [inet]}}) - end, - UdpOptions2 = lists:reverse(UdpOptions), - TftpOptions = lists:reverse(UserOptions), - Callbacks2 = add_default_callbacks(Callbacks), - Config#config{udp_host = Host2, - udp_options = UdpOptions2, - user_options = TftpOptions, - callbacks = Callbacks2}; -do_parse_config(Options, Config) when is_record(Config, config) -> - exit({badarg, Options}). - -add_default_callbacks(Callbacks) -> - RegExp = "", - {ok, Internal} = re:compile(RegExp), - File = #callback{regexp = RegExp, - internal = Internal, - module = tftp_file, - state = []}, - Bin = #callback{regexp = RegExp, - internal = Internal, - module = tftp_binary, - state = []}, - Callbacks ++ [File, Bin]. - -host_to_string(Host) -> - case Host of - String when is_list(String) -> - String; - {A1, A2, A3, A4} -> % inet - lists:concat([A1, ".", A2, ".", A3, ".",A4]); - {A1, A2, A3, A4, A5, A6, A7, A8} -> % inet6 - lists:concat([ - int16_to_hex(A1), "::", - int16_to_hex(A2), "::", - int16_to_hex(A3), "::", - int16_to_hex(A4), "::", - int16_to_hex(A5), "::", - int16_to_hex(A6), "::", - int16_to_hex(A7), "::", - int16_to_hex(A8) - ]) - end. - -int16_to_hex(0) -> - [$0]; -int16_to_hex(I) -> - N1 = ((I bsr 8) band 16#ff), - N2 = (I band 16#ff), - [code_character(N1 div 16), code_character(N1 rem 16), - code_character(N2 div 16), code_character(N2 rem 16)]. - -code_character(N) when N < 10 -> - $0 + N; -code_character(N) -> - $A + (N - 10). - -%%------------------------------------------------------------------- -%% Decode -%%------------------------------------------------------------------- - -decode_msg(Bin) when is_binary(Bin) -> - case Bin of - <<?TFTP_OPCODE_RRQ:16/integer, Tail/binary>> -> - case decode_strings(Tail, [keep_case, lower_case]) of - [Filename, Mode | Strings] -> - Options = decode_options(Strings), - #tftp_msg_req{access = read, - filename = Filename, - mode = to_lower(Mode), - options = Options}; - [_Filename | _Strings] -> - exit(#tftp_msg_error{code = undef, - text = "Missing mode"}); - _ -> - exit(#tftp_msg_error{code = undef, - text = "Missing filename"}) - end; - <<?TFTP_OPCODE_WRQ:16/integer, Tail/binary>> -> - case decode_strings(Tail, [keep_case, lower_case]) of - [Filename, Mode | Strings] -> - Options = decode_options(Strings), - #tftp_msg_req{access = write, - filename = Filename, - mode = to_lower(Mode), - options = Options}; - [_Filename | _Strings] -> - exit(#tftp_msg_error{code = undef, - text = "Missing mode"}); - _ -> - exit(#tftp_msg_error{code = undef, - text = "Missing filename"}) - end; - <<?TFTP_OPCODE_DATA:16/integer, SeqNo:16/integer, Data/binary>> -> - #tftp_msg_data{block_no = SeqNo, data = Data}; - <<?TFTP_OPCODE_ACK:16/integer, SeqNo:16/integer>> -> - #tftp_msg_ack{block_no = SeqNo}; - <<?TFTP_OPCODE_ERROR:16/integer, ErrorCode:16/integer, Tail/binary>> -> - case decode_strings(Tail, [keep_case]) of - [ErrorText] -> - ErrorCode2 = decode_error_code(ErrorCode), - #tftp_msg_error{code = ErrorCode2, - text = ErrorText}; - _ -> - exit(#tftp_msg_error{code = undef, - text = "Trailing garbage"}) - end; - <<?TFTP_OPCODE_OACK:16/integer, Tail/binary>> -> - Strings = decode_strings(Tail, [lower_case]), - Options = decode_options(Strings), - #tftp_msg_oack{options = Options}; - _ -> - exit(#tftp_msg_error{code = undef, - text = "Invalid syntax"}) - end. - -decode_strings(Bin, Cases) when is_binary(Bin), is_list(Cases) -> - do_decode_strings(Bin, Cases, []). - -do_decode_strings(<<>>, _Cases, Strings) -> - lists:reverse(Strings); -do_decode_strings(Bin, [Case | Cases], Strings) -> - {String, Tail} = decode_string(Bin, Case, []), - if - Cases =:= [] -> - do_decode_strings(Tail, [Case], [String | Strings]); - true -> - do_decode_strings(Tail, Cases, [String | Strings]) - end. - -decode_string(<<Char:8/integer, Tail/binary>>, Case, String) -> - if - Char =:= 0 -> - {lists:reverse(String), Tail}; - Case =:= keep_case -> - decode_string(Tail, Case, [Char | String]); - Case =:= lower_case -> - Char2 = ?LOWER(Char), - decode_string(Tail, Case, [Char2 | String]) - end; -decode_string(<<>>, _Case, _String) -> - exit(#tftp_msg_error{code = undef, text = "Trailing null missing"}). - -decode_options([Key, Value | Strings]) -> - [{to_lower(Key), Value} | decode_options(Strings)]; -decode_options([]) -> - []. - -decode_error_code(Int) -> - case Int of - ?TFTP_ERROR_UNDEF -> undef; - ?TFTP_ERROR_ENOENT -> enoent; - ?TFTP_ERROR_EACCES -> eacces; - ?TFTP_ERROR_ENOSPC -> enospc; - ?TFTP_ERROR_BADOP -> badop; - ?TFTP_ERROR_BADBLK -> badblk; - ?TFTP_ERROR_EEXIST -> eexist; - ?TFTP_ERROR_BADUSER -> baduser; - ?TFTP_ERROR_BADOPT -> badopt; - Int when is_integer(Int), Int >= 0, Int =< 65535 -> Int; - _ -> exit(#tftp_msg_error{code = undef, text = "Error code outside range."}) - end. - -%%------------------------------------------------------------------- -%% Encode -%%------------------------------------------------------------------- - -encode_msg(#tftp_msg_req{access = Access, - filename = Filename, - mode = Mode, - options = Options}) -> - OpCode = case Access of - read -> ?TFTP_OPCODE_RRQ; - write -> ?TFTP_OPCODE_WRQ - end, - [ - <<OpCode:16/integer>>, - Filename, - 0, - Mode, - 0, - [[Key, 0, Val, 0] || {Key, Val} <- Options] - ]; -encode_msg(#tftp_msg_data{block_no = BlockNo, data = Data}) when BlockNo =< 65535 -> - [ - <<?TFTP_OPCODE_DATA:16/integer, BlockNo:16/integer>>, - Data - ]; -encode_msg(#tftp_msg_ack{block_no = BlockNo}) when BlockNo =< 65535 -> - <<?TFTP_OPCODE_ACK:16/integer, BlockNo:16/integer>>; -encode_msg(#tftp_msg_error{code = Code, text = Text}) -> - IntCode = encode_error_code(Code), - [ - <<?TFTP_OPCODE_ERROR:16/integer, IntCode:16/integer>>, - Text, - 0 - ]; -encode_msg(#tftp_msg_oack{options = Options}) -> - [ - <<?TFTP_OPCODE_OACK:16/integer>>, - [[Key, 0, Val, 0] || {Key, Val} <- Options] - ]. - -encode_error_code(Code) -> - case Code of - undef -> ?TFTP_ERROR_UNDEF; - enoent -> ?TFTP_ERROR_ENOENT; - eacces -> ?TFTP_ERROR_EACCES; - enospc -> ?TFTP_ERROR_ENOSPC; - badop -> ?TFTP_ERROR_BADOP; - badblk -> ?TFTP_ERROR_BADBLK; - eexist -> ?TFTP_ERROR_EEXIST; - baduser -> ?TFTP_ERROR_BADUSER; - badopt -> ?TFTP_ERROR_BADOPT; - Int when is_integer(Int), Int >= 0, Int =< 65535 -> Int - end. - -%%------------------------------------------------------------------- -%% Miscellaneous -%%------------------------------------------------------------------- - -replace_val(Key, Val, List) -> - case lists:keysearch(Key, 1, List) of - false -> - List ++ [{Key, Val}]; - {value, {_, OldVal}} when OldVal =:= Val -> - List; - {value, {_, _}} -> - lists:keyreplace(Key, 1, List, {Key, Val}) - end. - -to_lower(Chars) -> - [?LOWER(Char) || Char <- Chars]. diff --git a/lib/inets/src/tftp/tftp_logger.erl b/lib/inets/src/tftp/tftp_logger.erl deleted file mode 100644 index 5e5d1d56c7..0000000000 --- a/lib/inets/src/tftp/tftp_logger.erl +++ /dev/null @@ -1,99 +0,0 @@ -%% -%% %CopyrightBegin% -%% -%% Copyright Ericsson AB 2008-2015. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%% -%% %CopyrightEnd% -%% -%% --module(tftp_logger). - -%%------------------------------------------------------------------- -%% Interface -%%------------------------------------------------------------------- - -%% public functions --export([ - error_msg/2, - warning_msg/2, - info_msg/2 - ]). - --export([behaviour_info/1]). - -behaviour_info(callbacks) -> - [{error_msg, 2}, {warning_msg, 2}, {info_msg, 2}]; -behaviour_info(_) -> - undefined. - -%%------------------------------------------------------------------- -%% error_msg(Format, Data) -> ok | exit(Reason) -%% -%% Format = string() -%% Data = [term()] -%% Reason = term() -%% -%% Log an error message -%%------------------------------------------------------------------- - -error_msg(Format, Data) -> - {Format2, Data2} = add_timestamp(Format, Data), - error_logger:error_msg(Format2, Data2). - -%%------------------------------------------------------------------- -%% warning_msg(Format, Data) -> ok | exit(Reason) -%% -%% Format = string() -%% Data = [term()] -%% Reason = term() -%% -%% Log a warning message -%%------------------------------------------------------------------- - -warning_msg(Format, Data) -> - {Format2, Data2} = add_timestamp(Format, Data), - error_logger:warning_msg(Format2, Data2). - -%%------------------------------------------------------------------- -%% info_msg(Format, Data) -> ok | exit(Reason) -%% -%% Format = string() -%% Data = [term()] -%% Reason = term() -%% -%% Log an info message -%%------------------------------------------------------------------- - -info_msg(Format, Data) -> - {Format2, Data2} = add_timestamp(Format, Data), - io:format(Format2, Data2). - -%%------------------------------------------------------------------- -%% Add timestamp to log message -%%------------------------------------------------------------------- - -add_timestamp(Format, Data) -> - Time = inets_time_compat:timestamp(), - {{_Y, _Mo, _D}, {H, Mi, S}} = calendar:now_to_universal_time(Time), - %% {"~p-~s-~sT~s:~s:~sZ,~6.6.0w tftp: " ++ Format ++ "\n", - %% [Y, t(Mo), t(D), t(H), t(Mi), t(S), MicroSecs | Data]}. - {"~s:~s:~s tftp: " ++ Format, [t(H), t(Mi), t(S) | Data]}. - -%% Convert 9 to "09". -t(Int) -> - case integer_to_list(Int) of - [Single] -> [$0, Single]; - Multi -> Multi - end. diff --git a/lib/inets/src/tftp/tftp_sup.erl b/lib/inets/src/tftp/tftp_sup.erl deleted file mode 100644 index 98b92cc87c..0000000000 --- a/lib/inets/src/tftp/tftp_sup.erl +++ /dev/null @@ -1,111 +0,0 @@ -%% -%% %CopyrightBegin% -%% -%% Copyright Ericsson AB 2005-2015. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%% -%% %CopyrightEnd% -%% -%% -%%---------------------------------------------------------------------- -%% Purpose: The top supervisor for tftp hangs under inets_sup. -%%---------------------------------------------------------------------- - --module(tftp_sup). - --behaviour(supervisor). - -%% API --export([start_link/1, - start_child/1, - stop_child/1, - which_children/0]). - -%% Supervisor callback --export([init/1]). - -%%%========================================================================= -%%% API -%%%========================================================================= - -start_link(TftpServices) -> - supervisor:start_link({local, ?MODULE}, ?MODULE, [TftpServices]). - -start_child(Options) -> - KillAfter = default_kill_after(), - ChildSpec = worker_spec(KillAfter, Options), - supervisor:start_child(?MODULE, ChildSpec). - -stop_child(Pid) when is_pid(Pid) -> - Children = supervisor:which_children(?MODULE), - case [Id || {Id, P, _Type, _Modules} <- Children, P =:= Pid] of - [] -> - {error, not_found}; - [Id] -> - case supervisor:terminate_child(?MODULE, Id) of - ok -> - supervisor:delete_child(?MODULE, Id); - {error, not_found} -> - supervisor:delete_child(?MODULE, Id); - {error, Reason} -> - {error, Reason} - end - end. - -which_children() -> - Children = supervisor:which_children(?MODULE), - [{tftpd, Pid} || {_Id, Pid, _Type, _Modules} <- Children, Pid =/= undefined]. - -%%%========================================================================= -%%% Supervisor callback -%%%========================================================================= - -init([Services]) when is_list(Services) -> - RestartStrategy = one_for_one, - MaxR = 10, - MaxT = 3600, - KillAfter = default_kill_after(), - Children = [worker_spec(KillAfter, Options) || {tftpd, Options} <- Services], - {ok, {{RestartStrategy, MaxR, MaxT}, Children}}. - -%%%========================================================================= -%%% Internal functions -%%%========================================================================= - -worker_spec(KillAfter, Options) -> - Modules = [proc_lib, tftp, tftp_engine], - KA = supervisor_timeout(KillAfter), - Name = unique_name(Options), - {Name, {tftp, start, [Options]}, permanent, KA, worker, Modules}. - -unique_name(Options) -> - case lists:keysearch(port, 1, Options) of - {value, {_, Port}} when is_integer(Port), Port > 0 -> - {tftpd, Port}; - _ -> - {tftpd, inets_time_compat:unique_integer([positive])} - end. - -default_kill_after() -> - timer:seconds(3). - -%% supervisor_spec(Name) -> -%% {Name, {Name, start, []}, permanent, infinity, supervisor, -%% [Name, supervisor]}. - --ifdef(debug_shutdown). -supervisor_timeout(_KillAfter) -> timer:hours(24). --else. -supervisor_timeout(KillAfter) -> KillAfter. --endif. |