diff --git a/rebar.config b/rebar.config index 5d12704..b4f15f6 100644 --- a/rebar.config +++ b/rebar.config @@ -9,7 +9,8 @@ rand_compat, {inet_cidr, "1.0.1", {pkg, erl_cidr}}, {inet_ext, "0.4.0"}, - {intercept, "1.0.0"} + {intercept, "1.0.0"}, + {lhttpc, "1.6.2"} ]}. diff --git a/rebar.lock b/rebar.lock index 2bd2813..4a91765 100644 --- a/rebar.lock +++ b/rebar.lock @@ -2,11 +2,13 @@ [{<<"inet_cidr">>,{pkg,<<"erl_cidr">>,<<"1.0.1">>},0}, {<<"inet_ext">>,{pkg,<<"inet_ext">>,<<"0.4.0">>},0}, {<<"intercept">>,{pkg,<<"intercept">>,<<"1.0.0">>},0}, + {<<"lhttpc">>,{pkg,<<"lhttpc">>,<<"1.6.2">>},0}, {<<"rand_compat">>,{pkg,<<"rand_compat">>,<<"0.0.3">>},0}]}. [ {pkg_hash,[ {<<"inet_cidr">>, <<"9EA93F2B885820C1C3ADEC24E7AB5B04AAD829FBF7B3F8F41F1ACD4550D8BF97">>}, {<<"inet_ext">>, <<"EF51FE5EA13DB6B40CBA48E66D9117BBD31E5A4347FA432B83D0C0547C7AB522">>}, {<<"intercept">>, <<"1F6C725E6FC070720643BD4D97EE53B1209365C80E520E1F5A1ACB36712A7EB5">>}, + {<<"lhttpc">>, <<"044F16F0018C7AA7E945E9E9406C7F6035E0B8BC08BF77B00C78CE260E1071E3">>}, {<<"rand_compat">>, <<"011646BC1F0B0C432FE101B816F25B9BBB74A085713CEE1DAFD2D62E9415EAD3">>}]} ]. diff --git a/src/nat.app.src b/src/nat.app.src index c7975d5..95e8654 100644 --- a/src/nat.app.src +++ b/src/nat.app.src @@ -3,7 +3,7 @@ {vsn, "0.3.1"}, {modules, []}, {registered, []}, - {applications, [kernel,stdlib,inet_cidr,inet_ext,inets,xmerl,rand_compat]}, + {applications, [kernel,stdlib,inet_cidr,inet_ext,inets,xmerl,rand_compat,lhttpc]}, {maintainers, ["Benoit Chesneau"]}, {licenses, ["MIT"]}, {links, [{"Github", "https://github.com/benoitc/erlang-nat"}]}, diff --git a/src/nat.erl b/src/nat.erl index d97ad52..2db64f2 100644 --- a/src/nat.erl +++ b/src/nat.erl @@ -85,7 +85,7 @@ get_internal_address({Mod, Ctx}) -> ExternalPortRequest :: non_neg_integer(), Since :: non_neg_integer(), ExternalPort :: non_neg_integer(), - MappingLifetime :: non_neg_integer(), + MappingLifetime :: non_neg_integer() | infinity, Reason :: any() | timeout. %% @doc add a port mapping with default lifetime add_port_mapping(NatCtx, Protocol, InternalPort, ExternalPort) -> @@ -102,7 +102,7 @@ add_port_mapping(NatCtx, Protocol, InternalPort, ExternalPort) -> Lifetime :: non_neg_integer(), Since :: non_neg_integer(), ExternalPort :: non_neg_integer(), - MappingLifetime :: non_neg_integer(), + MappingLifetime :: non_neg_integer() | infinity, Reason :: any() | timeout(). %% @doc add a port mapping add_port_mapping({Mod, Ctx}, Protocol, InternalPort, ExternalPort, Lifetime) -> diff --git a/src/nat_lib.erl b/src/nat_lib.erl index d4283ce..573b486 100644 --- a/src/nat_lib.erl +++ b/src/nat_lib.erl @@ -24,22 +24,20 @@ soap_request(Url, Function, Msg0, Options) -> Action = "\"urn:schemas-upnp-org:service:WANIPConnection:1#" ++ Function ++ "\"", - Headers = [{"Content-Length", integer_to_list(length(Msg))}, + Headers = [{"Content-Type", "text/xml; charset=\"utf-8\""}, + {"Content-Length", integer_to_list(length(Msg))}, {"User-Agent", "Darwin/10.0.0, UPnP/1.0, MiniUPnPc/1.3"}, {"SOAPAction", Action}, {"Connection", "close"}, {"Cache-Control", "no-cache"}, {"Pragma", "no-cache"}], - - Req = {Url, Headers, "text/xml; charset=\"utf-8\"", Msg}, - - case httpc:request(post, Req, [], Options) of - {ok, {{_, 200, _}, _, Body}} -> - {ok, Body}; - OK = {ok, {{_, Status, _}, _, _}} -> + case lhttpc:request(Url, post, Headers, Msg, 5000, Options) of + {ok, {{200, _}, _, Body}} -> + {ok, binary_to_list(Body)}; + OK = {ok, {{Status, _}, _, Body}} -> error_logger:info_msg("UPNP SOAP error: ~p~n", [OK]), - {error, integer_to_list(Status)}; + {error, {http_error, integer_to_list(Status), binary_to_list(Body)}}; Error -> Error end. diff --git a/src/natupnp_v1.erl b/src/natupnp_v1.erl index f44a57c..0c27fc0 100644 --- a/src/natupnp_v1.erl +++ b/src/natupnp_v1.erl @@ -145,8 +145,10 @@ random_port_mapping(Ctx, Protocol, InternalPort, Lifetime, _LastError, Tries) -> Tries -1) end. -add_port_mapping1(#nat_upnp{ip=Ip, service_url=Url}, Protocol, InternalPort, - ExternalPort, Lifetime) -> + +add_port_mapping1(#nat_upnp{ip=Ip, service_url=Url}=NatCtx, + Protocol, InternalPort, ExternalPort, + Lifetime) when is_integer(Lifetime), Lifetime >= 0 -> Description = Ip ++ "_" ++ Protocol ++ "_" ++ integer_to_list(InternalPort), Msg = "" @@ -164,14 +166,44 @@ add_port_mapping1(#nat_upnp{ip=Ip, service_url=Url}, Protocol, InternalPort, "", {ok, IAddr} = inet:parse_address(Ip), Start = nat_lib:timestamp(), - case nat_lib:soap_request(Url, "AddPortMapping", Msg, [{socket_opts, [{ip, IAddr}]}]) of + case nat_lib:soap_request(Url, "AddPortMapping", Msg, [{connect_options, [{ip, IAddr}]}]) of {ok, _} -> Now = nat_lib:timestamp(), - MappingLifetime = Lifetime - (Now - Start), + MappingLifetime = if + Lifetime > 0 -> + Lifetime - (Now - Start); + true -> + infinity + end, {ok, Now, InternalPort, ExternalPort, MappingLifetime}; - Error -> Error + Error when Lifetime > 0 -> + %% Try to repair error code 725 - OnlyPermanentLeasesSupported + case only_permanent_lease_supported(Error) of + true -> + error_logger:info_msg("UPNP: only permanent lease supported~n", []), + add_port_mapping1(NatCtx, Protocol, InternalPort, ExternalPort, 0); + false -> + Error + end; + Error -> + Error end. +only_permanent_lease_supported({error, {http_error, "500", Body}}) -> + {Xml, _} = xmerl_scan:string(Body, [{space, normalize}]), + [Error | _] = xmerl_xpath:string("//s:Envelope/s:Body/s:Fault/detail/" + "UPnPError", Xml), + ErrorCode = extract_txt( + xmerl_xpath:string("errorCode/text()", Error) + ), + + case ErrorCode of + "725" -> true; + _ -> false + end; +only_permanent_lease_supported(_) -> + false. + %% @doc Delete a port mapping from the router -spec delete_port_mapping(Context :: nat:nat_upnp(), Protocol :: nat:nat_protocol(), InternalPort :: integer(), @@ -187,7 +219,7 @@ delete_port_mapping(#nat_upnp{ip=Ip, service_url=Url}, Protocol0, _InternalPort, "" ++ Protocol ++ "" "", {ok, IAddr} = inet:parse_address(Ip), - case nat_lib:soap_request(Url, "DeletePortMapping", Msg, [{socket_opts, [{ip, IAddr}]}]) of + case nat_lib:soap_request(Url, "DeletePortMapping", Msg, [{connect_options, [{ip, IAddr}]}]) of {ok, _} -> ok; Error -> Error end. @@ -207,7 +239,7 @@ get_port_mapping(#nat_upnp{ip=Ip, service_url=Url}, Protocol0, ExternalPort) -> "" ++ Protocol ++ "" "", {ok, IAddr} = inet:parse_address(Ip), - case nat_lib:soap_request(Url, "GetSpecificPortMappingEntry", Msg, [{socket_opts, [{ip, IAddr}]}]) of + case nat_lib:soap_request(Url, "GetSpecificPortMappingEntry", Msg, [{connect_options, [{ip, IAddr}]}]) of {ok, Body} -> {Xml, _} = xmerl_scan:string(Body, [{space, normalize}]), [Infos | _] = xmerl_xpath:string("//s:Envelope/s:Body/" @@ -281,9 +313,9 @@ get_location(Raw) -> end. get_service_url(RootUrl) -> - case httpc:request(RootUrl) of - {ok, {{_, 200, _}, _, Body}} -> - {Xml, _} = xmerl_scan:string(Body, [{space, normalize}]), + case lhttpc:request(RootUrl, get, [], 5000) of + {ok, {{200, _}, _, Body}} -> + {Xml, _} = xmerl_scan:string(binary_to_list(Body), [{space, normalize}]), [Device | _] = xmerl_xpath:string("//device", Xml), case device_type(Device) of "urn:schemas-upnp-org:device:InternetGatewayDevice:1" -> @@ -291,7 +323,7 @@ get_service_url(RootUrl) -> _ -> {error, no_gateway_device} end; - {ok, {{_, StatusCode, _}, _, _}} -> + {ok, {{StatusCode, _}, _, _}} -> {error, integer_to_list(StatusCode)}; Error -> Error diff --git a/src/natupnp_v2.erl b/src/natupnp_v2.erl index dbafcfc..8bc4621 100644 --- a/src/natupnp_v2.erl +++ b/src/natupnp_v2.erl @@ -151,8 +151,9 @@ random_port_mapping(Ctx, Protocol, InternalPort, Lifetime, _LastError, Tries) -> Tries -1) end. -add_port_mapping1(#nat_upnp{ip=Ip, service_url=Url}, Protocol, InternalPort, - ExternalPort, Lifetime) -> +add_port_mapping1(#nat_upnp{ip=Ip, service_url=Url} = NatCtx, + Protocol, InternalPort, ExternalPort, + Lifetime) when is_integer(Lifetime), Lifetime >= 0 -> Description = Ip ++ "_" ++ Protocol ++ "_" ++ integer_to_list(InternalPort), Msg = "" @@ -170,7 +171,7 @@ add_port_mapping1(#nat_upnp{ip=Ip, service_url=Url}, Protocol, InternalPort, "", {ok, IAddr} = inet:parse_address(Ip), Start = nat_lib:timestamp(), - case nat_lib:soap_request(Url, "AddAnyPortMapping", Msg, [{socket_opts, [{ip, IAddr}]}]) of + case nat_lib:soap_request(Url, "AddAnyPortMapping", Msg, [{connect_options, [{ip, IAddr}]}]) of {ok, Body} -> {Xml, _} = xmerl_scan:string(Body, [{space, normalize}]), @@ -185,9 +186,34 @@ add_port_mapping1(#nat_upnp{ip=Ip, service_url=Url}, Protocol, InternalPort, Now = nat_lib:timestamp(), MappingLifetime = Lifetime - (Now - Start), {ok, Now, InternalPort, list_to_integer(ReservedPort), MappingLifetime}; - Error -> Error + Error when Lifetime > 0 -> + %% Try to repair error code 725 - OnlyPermanentLeasesSupported + case only_permanent_lease_supported(Error) of + true -> + error_logger:info_msg("UPNP: only permanent lease supported~n", []), + add_port_mapping1(NatCtx, Protocol, InternalPort, ExternalPort, 0); + false -> + Error + end; + Error -> + Error end. +only_permanent_lease_supported({error, {http_error, "500", Body}}) -> + {Xml, _} = xmerl_scan:string(Body, [{space, normalize}]), + [Error | _] = xmerl_xpath:string("//s:Envelope/s:Body/s:Fault/detail/" + "UPnPError", Xml), + ErrorCode = extract_txt( + xmerl_xpath:string("errorCode/text()", Error) + ), + + case ErrorCode of + "725" -> true; + _ -> false + end; +only_permanent_lease_supported(_) -> + false. + %% @doc Delete a port mapping from the router -spec delete_port_mapping(Context :: nat:nat_upnp(), Protocol :: nat:nat_protocol(), InternalPort :: integer(), @@ -203,7 +229,7 @@ delete_port_mapping(#nat_upnp{ip=Ip, service_url=Url}, Protocol0, _InternalPort, "" ++ Protocol ++ "" "", {ok, IAddr} = inet:parse_address(Ip), - case nat_lib:soap_request(Url, "DeletePortMapping", Msg, [{socket_opts, [{ip, IAddr}]}]) of + case nat_lib:soap_request(Url, "DeletePortMapping", Msg, [{connect_options, [{ip, IAddr}]}]) of {ok, _} -> ok; Error -> Error end. @@ -224,7 +250,7 @@ get_port_mapping(#nat_upnp{ip=Ip, service_url=Url}, Protocol0, ExternalPort) -> "" ++ Protocol ++ "" "", {ok, IAddr} = inet:parse_address(Ip), - case nat_lib:soap_request(Url, "GetSpecificPortMappingEntry", Msg, [{socket_opts, [{ip, IAddr}]}]) of + case nat_lib:soap_request(Url, "GetSpecificPortMappingEntry", Msg, [{connect_options, [{ip, IAddr}]}]) of {ok, Body} -> {Xml, _} = xmerl_scan:string(Body, [{space, normalize}]), [Infos | _] = xmerl_xpath:string("//s:Envelope/s:Body/" @@ -299,9 +325,9 @@ get_location(Raw) -> end. get_service_url(RootUrl) -> - case httpc:request(RootUrl) of - {ok, {{_, 200, _}, _, Body}} -> - {Xml, _} = xmerl_scan:string(Body, [{space, normalize}]), + case lhttpc:request(RootUrl, get, [], 5000) of + {ok, {{200, _}, _, Body}} -> + {Xml, _} = xmerl_scan:string(binary_to_list(Body), [{space, normalize}]), [Device | _] = xmerl_xpath:string("//device", Xml), case device_type(Device) of "urn:schemas-upnp-org:device:InternetGatewayDevice:2" -> @@ -309,7 +335,7 @@ get_service_url(RootUrl) -> _ -> {error, no_gateway_device} end; - {ok, {{_, StatusCode, _}, _, _}} -> + {ok, {{StatusCode, _}, _, _}} -> {error, integer_to_list(StatusCode)}; Error -> Error