├── priv ├── www │ ├── dir1 │ │ └── file1.txt │ └── index.html ├── key.pem └── cert.pem ├── rebar3 ├── .gitignore ├── src ├── nkpacket.app.src ├── nkpacket_sup.erl ├── nkpacket_config.erl ├── nkpacket_app.erl ├── http_client │ ├── nkpacket_httpc_pool.erl │ ├── nkpacket_httpc.erl │ └── nkpacket_httpc_protocol.erl ├── nkpacket_connection_lib.erl ├── nkpacket_tls.erl ├── nkpacket_syntax.erl ├── nkpacket_resolve.erl ├── nkpacket_connection_ws.erl ├── nkpacket_util.erl ├── nkpacket_transport_sctp.erl └── nkpacket_stun.erl ├── config └── shell.config ├── rebar.config ├── Makefile ├── test ├── basic_test.erl ├── test_util.erl ├── sctp_test.erl ├── ipv6_test.erl ├── test_protocol.erl ├── dns_test.erl ├── http_test.erl ├── udp_test.erl └── tcp_test.erl ├── include └── nkpacket.hrl ├── README.md └── LICENSE /priv/www/dir1/file1.txt: -------------------------------------------------------------------------------- 1 | file1.txt -------------------------------------------------------------------------------- /rebar3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetComposer/nkpacket/HEAD/rebar3 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .eunit 2 | *.beam 3 | *.plt 4 | erl_crash.dump 5 | ebin 6 | rel 7 | log 8 | .rebar 9 | _build 10 | /nkpacket.version 11 | /.rebar3 12 | -------------------------------------------------------------------------------- /priv/www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | A Small Hello 6 | 7 | 8 | 9 |

Hi

10 |

This is very minimal "hello world" HTML document.

11 | 12 | -------------------------------------------------------------------------------- /src/nkpacket.app.src: -------------------------------------------------------------------------------- 1 | {application, nkpacket, [ 2 | {description, "NkPACKET"}, 3 | {vsn, "1.0.1"}, 4 | {modules, []}, 5 | {registered, []}, 6 | {mod, {nkpacket_app, []}}, 7 | {applications, [ 8 | crypto, 9 | ssl, 10 | nklib, 11 | ssh, 12 | ranch, 13 | cowlib, 14 | cowboy, 15 | gun 16 | ]}, 17 | {env, [ 18 | ]} 19 | ]}. 20 | -------------------------------------------------------------------------------- /config/shell.config: -------------------------------------------------------------------------------- 1 | [ 2 | {nkpacket, [ 3 | ]}, 4 | 5 | {sasl, [ 6 | {sasl_error_logger, false} 7 | ]}, 8 | 9 | {lager, [ 10 | {handlers, [ 11 | {lager_console_backend, debug}, 12 | {lager_file_backend, [{file, "log/error.log"}, {level, error}]}, 13 | {lager_file_backend, [{file, "log/console.log"}, {level, info}]} 14 | ]}, 15 | {error_logger_redirect, false}, 16 | {crash_log, "log/crash.log"}, 17 | {colored, true}, 18 | {colors, [ 19 | {debug, "\e[0;38m" }, 20 | {info, "\e[0;32m" }, 21 | {notice, "\e[1;36m" }, 22 | {warning, "\e[1;33m" }, 23 | {error, "\e[1;31m" } 24 | ]} 25 | ]} 26 | 27 | ]. 28 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | 2 | {cover_enabled, true}. 3 | 4 | {erl_opts, [ 5 | debug_info, warnings_as_errors, {parse_transform, lager_transform} 6 | ]}. 7 | 8 | % {edoc_opts, [{dir, "../../doc"}]}. 9 | 10 | {project_plugins, [pc]}. 11 | 12 | 13 | {deps, [ 14 | {nklib, {git, "https://github.com/netcomposer/nklib.git", {branch, "master"}}}, 15 | {cowboy, {git, "https://github.com/extend/cowboy", {tag, "2.6.0"}}}, 16 | certifi, 17 | ssl_verify_fun, 18 | % Used for tests 19 | {gun, {git, "https://github.com/ninenines/gun.git", {branch, "master"}}}, 20 | {meck, "0.8.12"} 21 | ]}. 22 | 23 | 24 | {xref_checks, [ 25 | undefined_function_calls, undefined_functions, 26 | locals_not_used, deprecated_function_calls, deprecated_functions 27 | ]}. 28 | 29 | {profiles, [{test, [{deps, [meck]}]}]}. 30 | -------------------------------------------------------------------------------- /priv/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICWwIBAAKBgQCc2F6uu3udwiIZpnx/mzTgGiiURCqkm6yiLAJqNx2a270iMPaU 3 | ahw5OFZwUxzGVFyDuE9tsJJ/VxfICoU4e3L1UE44Aa3nSHE5ZYzLySk5HmUv/GO9 4 | H6G678qQDZ0i8+aStIMLbQTnc2cKUphnsTpwijH2lxCYS6bJeHLMTFHhvwIDAQAB 5 | AoGAF4QRXh1aAWMz/aiKjg9VNCx33RMDWb7FeWMj0Y0F8Uv05YV10va92I5r11d+ 6 | vvWzEUS3E/kfXdxPAn0hUECiEY2gGJW4W5+2nf6Oa5sEYqyAgSa0WNY/skcVWYzR 7 | Bi3LwgZCcrUZS8ROfToFX2V3FhHqpQeW31b68/LysNrddQECQQDQk9KHS/rhtWk3 8 | 0oV25wrzDaXSrZcsu581iusygIgjLVDe4F1ppf7ZoLuD+aS1Ia3PHu5NiM1jcsgU 9 | oeXMuA7BAkEAwIF9jl3tpmbVuTTTSNPTlbaKBvgt59aecVxxRBd47LTih+6gRNEN 10 | PfS/PW92yHJKwU77g9cgeFKRXPkvGDaQfwJAfp6OerYEWosPkeTKQvFlc0GAvhHF 11 | qVFJCG8J8wGWI4y6AGNCMgWkXac2zpp5g8ArTIZhck4vKUUf826JG6tMwQJAXUdh 12 | m3aQDS2PKisaphNeVxEYWMAxHkG0jKGKkL/+7FPJ4KwUJMRXckoB0LcOC5q19m1b 13 | GktHhIYdwbtMwqLN6QJAdW94BFoB2Eg3WDEr6UqIVYQxrQRK1Z8ai6u2uT+SMQNY 14 | EaqhDuxkdNSMN6hL+kIuwPTBq44hlhNoNigee5VvcQ== 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /priv/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICsDCCAhmgAwIBAgIJANhA5kOkppbIMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV 3 | BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX 4 | aWRnaXRzIFB0eSBMdGQwHhcNMTMwMTI4MTY1MTI4WhcNMjMwMTI2MTY1MTI4WjBF 5 | MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 6 | ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB 7 | gQCc2F6uu3udwiIZpnx/mzTgGiiURCqkm6yiLAJqNx2a270iMPaUahw5OFZwUxzG 8 | VFyDuE9tsJJ/VxfICoU4e3L1UE44Aa3nSHE5ZYzLySk5HmUv/GO9H6G678qQDZ0i 9 | 8+aStIMLbQTnc2cKUphnsTpwijH2lxCYS6bJeHLMTFHhvwIDAQABo4GnMIGkMB0G 10 | A1UdDgQWBBR6F3p1QJNXh4jAWKeU4OfHm3qtHDB1BgNVHSMEbjBsgBR6F3p1QJNX 11 | h4jAWKeU4OfHm3qtHKFJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUt 12 | U3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJANhA5kOk 13 | ppbIMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAArVDaATjKRn4RpQV 14 | ON6h4LJMLcepcCu267lUDqfYiSdjXYeVtZuzbVxxguoYWUv8pwQp5CvQCzuoh8kN 15 | vyVGPsL+p0h4XVd5fLem8TnMPYbhkOsC743o2bUHSn2jU2I284YzT7E8nXfua9yp 16 | d2iqhBl2/Hlnf2hP5eciKzwIYBE= 17 | -----END CERTIFICATE----- 18 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | APP = nkpacket 2 | REBAR = rebar3 3 | 4 | .PHONY: rel stagedevrel package version all tree shell 5 | 6 | all: version compile 7 | 8 | 9 | version: 10 | @echo "$(shell git symbolic-ref HEAD 2> /dev/null | cut -b 12-)-$(shell git log --pretty=format:'%h, %ad' -1)" > $(APP).version 11 | 12 | 13 | version_header: version 14 | @echo "-define(VERSION, <<\"$(shell cat $(APP).version)\">>)." > include/$(APP)_version.hrl 15 | 16 | 17 | clean: 18 | $(REBAR) clean 19 | 20 | 21 | rel: 22 | $(REBAR) release 23 | 24 | 25 | compile: 26 | $(REBAR) compile 27 | 28 | 29 | tests: 30 | $(REBAR) eunit 31 | 32 | 33 | dialyzer: 34 | $(REBAR) dialyzer 35 | 36 | 37 | xref: 38 | $(REBAR) xref 39 | 40 | 41 | upgrade: 42 | $(REBAR) upgrade 43 | make tree 44 | 45 | 46 | update: 47 | $(REBAR) update 48 | 49 | 50 | tree: 51 | $(REBAR) tree | grep -v '=' | sed 's/ (.*//' > tree 52 | 53 | 54 | tree-diff: tree 55 | git diff test -- tree 56 | 57 | 58 | docs: 59 | $(REBAR) edoc 60 | 61 | 62 | shell: 63 | $(REBAR) shell --config config/shell.config --name $(APP)@127.0.0.1 --setcookie nk --apps $(APP) 64 | 65 | -------------------------------------------------------------------------------- /test/basic_test.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2015 Carlos Gonzalez Florido. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | -module(basic_test). 22 | -author('Carlos Gonzalez '). 23 | 24 | -compile([export_all, nowarn_export_all]). 25 | -include_lib("eunit/include/eunit.hrl"). 26 | -include("nkpacket.hrl"). 27 | 28 | basic_test_() -> 29 | {setup, spawn, 30 | fun() -> 31 | application:stop(nkpacket), 32 | ok = nkpacket_app:start(), 33 | ?debugMsg("Starting BASIC test") 34 | end, 35 | fun(_) -> 36 | ok 37 | end, 38 | fun(_) -> 39 | [ 40 | fun() -> config() end 41 | ] 42 | end 43 | }. 44 | 45 | 46 | config() -> 47 | 1024 = nkpacket_config:max_connections(), 48 | 30000 = nkpacket_config:udp_timeout(), 49 | nkpacket_app:put(max_connections, 100), 50 | nkpacket_config:set_config(), 51 | 100 = nkpacket_config:max_connections(), 52 | 53 | nklib_config:del(nkpacket, {protocol, scheme}), 54 | nklib_config:del_domain(nkpacket, srv1, {protocol, scheme}), 55 | 56 | undefined = nkpacket:get_protocol(scheme), 57 | undefined = nkpacket:get_protocol(srv1, scheme), 58 | ok = nkpacket:register_protocol(scheme, ?MODULE), 59 | ?MODULE = nkpacket:get_protocol(scheme), 60 | ?MODULE = nkpacket:get_protocol(srv1, scheme), 61 | ok = nkpacket:register_protocol(srv1, scheme, test_protocol), 62 | ?MODULE = nkpacket:get_protocol(scheme), 63 | test_protocol = nkpacket:get_protocol(srv1, scheme), 64 | ok. 65 | 66 | -------------------------------------------------------------------------------- /src/nkpacket_sup.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2019 Carlos Gonzalez Florido. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %% @private NkPACKET main supervisor 22 | -module(nkpacket_sup). 23 | -author('Carlos Gonzalez '). 24 | -behaviour(supervisor). 25 | 26 | -export([add_listener/2, del_listener/1, get_listeners/0]). 27 | -export([init/1, start_link/0, start_listen_sup/0]). 28 | 29 | -include("nkpacket.hrl"). 30 | 31 | 32 | %% @private Adds a supervised listener 33 | -spec add_listener(nkpacket:id(), supervisor:child_spec()) -> 34 | {ok, pid()} | {error, term()}. 35 | 36 | add_listener(Id, Spec) -> 37 | case supervisor:start_child(nkpacket_listen_sup, Spec) of 38 | {ok, Pid} -> 39 | {ok, Id, Pid}; 40 | {error, already_present} -> 41 | ok = supervisor:delete_child(nkpacket_listen_sup, Id), 42 | add_listener(Id, Spec); 43 | {error, {Error, _}} -> 44 | {error, Error}; 45 | {error, Error} -> 46 | {error, Error} 47 | end. 48 | 49 | 50 | %% @private Removes a supervised listener 51 | -spec del_listener(term()) -> 52 | ok | {error, term()}. 53 | 54 | del_listener(Id) -> 55 | case catch supervisor:terminate_child(nkpacket_listen_sup, Id) of 56 | ok -> 57 | supervisor:delete_child(nkpacket_listen_sup, Id); 58 | {error, Reason} -> 59 | {error, Reason} 60 | end. 61 | 62 | 63 | %% @private Gets supervised listeners 64 | -spec get_listeners() -> 65 | [{term(), pid()}]. 66 | 67 | get_listeners() -> 68 | [{Id, Pid} || {Id, Pid, _, _} <- supervisor:which_children(nkpacket_listen_sup)]. 69 | 70 | 71 | %% @private 72 | start_link() -> 73 | ChildsSpec = [ 74 | {nkpacket_dns, 75 | {nkpacket_dns, start_link, []}, 76 | permanent, 77 | 5000, 78 | worker, 79 | [nkpacket_dns]}, 80 | {nkpacket_listen_sup, 81 | {?MODULE, start_listen_sup, []}, 82 | permanent, 83 | infinity, 84 | supervisor, 85 | [?MODULE]} 86 | ], 87 | supervisor:start_link({local, ?MODULE}, ?MODULE, {{one_for_one, 10, 60}, ChildsSpec}). 88 | 89 | %% @private 90 | start_listen_sup() -> 91 | supervisor:start_link({local, nkpacket_listen_sup}, 92 | ?MODULE, {{one_for_one, 10, 60}, []}). 93 | 94 | 95 | %% @private 96 | init(ChildSpecs) -> 97 | {ok, ChildSpecs}. 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /test/test_util.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2015 Carlos Gonzalez Florido. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | -module(test_util). 22 | -author('Carlos Gonzalez '). 23 | 24 | -compile([export_all]). 25 | -compile(nowarn_export_all). 26 | -include_lib("eunit/include/eunit.hrl"). 27 | -include("nkpacket.hrl"). 28 | 29 | 30 | reset_1() -> 31 | ok = nkpacket:register_protocol(test, test_protocol), 32 | ok = nkpacket:stop_all(), 33 | ok = nkpacket_connection:stop_all(), 34 | timer:sleep(100), 35 | Pid = self(), 36 | Ref = make_ref(), 37 | M = #{user_state=>{Pid, Ref}}, 38 | {Ref, M}. 39 | 40 | 41 | reset_2() -> 42 | ok = nkpacket:register_protocol(test, test_protocol), 43 | ok = nkpacket:stop_all(), 44 | ok = nkpacket_connection:stop_all(), 45 | timer:sleep(100), 46 | Pid = self(), 47 | Ref1 = make_ref(), 48 | M1 = #{user_state=>{Pid, Ref1}}, 49 | Ref2 = make_ref(), 50 | M2 = #{user_state=>{Pid, Ref2}}, 51 | {Ref1, M1, Ref2, M2}. 52 | 53 | 54 | reset_3() -> 55 | ok = nkpacket:register_protocol(test, test_protocol), 56 | ok = nkpacket:stop_all(), 57 | ok = nkpacket_connection:stop_all(), 58 | timer:sleep(100), 59 | Pid = self(), 60 | Ref1 = make_ref(), 61 | M1 = #{user_state=>{Pid, Ref1}}, 62 | Ref2 = make_ref(), 63 | M2 = #{user_state=>{Pid, Ref2}}, 64 | Ref3 = make_ref(), 65 | M3 = #{user_state=>{Pid, Ref3}}, 66 | {Ref1, M1, Ref2, M2, Ref3, M3}. 67 | 68 | 69 | reset_4() -> 70 | ok = nkpacket:register_protocol(test, test_protocol), 71 | ok = nkpacket:stop_all(), 72 | ok = nkpacket_connection:stop_all(), 73 | timer:sleep(100), 74 | Pid = self(), 75 | Ref1 = make_ref(), 76 | M1 = #{user_state=>{Pid, Ref1}}, 77 | Ref2 = make_ref(), 78 | M2 = #{user_state=>{Pid, Ref2}}, 79 | Ref3 = make_ref(), 80 | M3 = #{user_state=>{Pid, Ref3}}, 81 | Ref4 = make_ref(), 82 | M4 = #{user_state=>{Pid, Ref4}}, 83 | {Ref1, M1, Ref2, M2, Ref3, M3, Ref4, M4}. 84 | 85 | 86 | ensure([]) -> 87 | ok; 88 | 89 | ensure([Ref|Rest]) -> 90 | timer:sleep(50), 91 | receive {Ref, V} -> error({unexpected, V}) after 0 -> ok end, 92 | ensure(Rest). 93 | 94 | 95 | 96 | get_port(udp) -> 97 | {ok, Socket} = gen_udp:open(0, [{reuseaddr, true}]), 98 | {ok, Port1} = inet:port(Socket), 99 | gen_udp:close(Socket), 100 | Port1; 101 | 102 | get_port(tcp) -> 103 | {ok, Socket} = gen_tcp:listen(0, [{reuseaddr, true}]), 104 | {ok, Port1} = inet:port(Socket), 105 | gen_tcp:close(Socket), 106 | Port1. 107 | 108 | 109 | listeners(Dom) -> 110 | lists:sort([element(2, nkpacket:get_nkport(P)) || P <- nkpacket:get_class_ids(Dom)]). 111 | 112 | 113 | conns(Dom) -> 114 | lists:sort([element(2, nkpacket:get_nkport(element(2,P))) || P <- nkpacket_connection:get_all_class(Dom)]). 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /include/nkpacket.hrl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2019 Carlos Gonzalez Florido. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | -ifndef(NKPACKET_HRL_). 22 | -define(NKPACKET_HRL_, 1). 23 | 24 | %% =================================================================== 25 | %% Defines 26 | %% =================================================================== 27 | 28 | -define(CONN_LISTEN_OPTS, 29 | [group, user, idle_timeout, host, path, ws_proto, refresh_fun, debug, external_url]). 30 | 31 | -define(CONN_CLIENT_OPTS, [monitor|?CONN_LISTEN_OPTS]). 32 | 33 | 34 | -define( 35 | DO_LOG(Level, Domain, Text, Opts), 36 | lager:Level([{domain, Domain}], "~p "++Text, [Domain|Opts])). 37 | 38 | -define(debug(Domain, Text, List), 39 | ?DO_LOG(debug, Domain, Text, List)). 40 | 41 | -define(info(Domain, Text, List), 42 | ?DO_LOG(info, Domain, Text, List)). 43 | 44 | -define(notice(Domain, Text, List), 45 | ?DO_LOG(notice, Domain, Text, List)). 46 | 47 | -define(warning(Domain, Text, List), 48 | ?DO_LOG(warning, Domain, Text, List)). 49 | 50 | -define(error(Domain, Text, List), 51 | ?DO_LOG(error, Domain, Text, List)). 52 | 53 | 54 | 55 | %%-define(PACKET_TYPES, 56 | %% packet_idle_timeout => integer() 57 | %% packet_connect_timeout => integer(), 58 | %% packet_sctp_out_streams => integer(), 59 | %% packet_sctp_in_streams => integer, 60 | %% packet_no_dns_cache => integer(), 61 | %%). 62 | 63 | 64 | %% =================================================================== 65 | %% Records 66 | %% =================================================================== 67 | 68 | %% Meta can contain most values from listener_opts and connect_opts 69 | 70 | -record(nkport, { 71 | id :: nkpacket:id() | undefined, 72 | class :: nkpacket:class() | undefined, 73 | protocol :: nkpacket:protocol() | undefined, 74 | transp :: nkpacket:transport() | undefined, 75 | local_ip :: inet:ip_address() | undefined, 76 | local_port :: inet:port_number() | undefined, 77 | remote_ip :: inet:ip_address() | undefined, 78 | remote_port :: inet:port_number() | undefined, 79 | listen_ip :: inet:ip_address() | undefined, 80 | listen_port :: inet:port_number() | undefined, 81 | pid :: pid() | undefined, 82 | socket :: nkpacket_transport:socket() | undefined, 83 | opts = #{} :: nkpacket:listen_opts() | nkpacket:send_opts(), 84 | user_state = undefined :: nkpacket:user_state() 85 | }). 86 | 87 | 88 | -record(nkconn, { 89 | protocol :: nkpacket:protocol(), 90 | transp :: nkpacket:transport(), 91 | ip = {0,0,0,0} :: inet:ip_address(), 92 | port = 0 :: inet:port_number(), 93 | opts = #{} :: nkpacket:listen_opts() | nkpacket:send_opts() 94 | }). 95 | 96 | 97 | -record(cowboy_filter, { 98 | pid :: pid(), 99 | module :: module(), 100 | transp :: http | https | ws | wss, 101 | host = any :: any | binary(), 102 | paths = [] :: [binary()], 103 | ws_proto = any :: binary() | any, 104 | meta :: #{get_headers => [binary()], compress => boolean(), idle_timeout=>pos_integer()}, 105 | mon :: reference() | undefined 106 | }). 107 | 108 | 109 | -endif. 110 | 111 | -------------------------------------------------------------------------------- /src/nkpacket_config.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2019 Carlos Gonzalez Florido. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %% @private NkPACKET global config 22 | 23 | -module(nkpacket_config). 24 | -author('Carlos Gonzalez '). 25 | -export([set_config/0]). 26 | -export([max_connections/0, dns_cache_ttl/0, udp_timeout/0, tcp_timeout/0, sctp_timeout/0, 27 | ws_timeout/0, http_timeout/0, connect_timeout/0, sctp_in_streams/0, 28 | sctp_out_streams/0, main_ip/0, main_ip6/0, ext_ip/0, ext_ip6/0, local_ips/0]). 29 | 30 | 31 | -record(nkpacket_config, { 32 | max_connections :: integer(), 33 | dns_cache_ttl :: integer(), 34 | udp_timeout :: integer(), 35 | tcp_timeout :: integer(), 36 | sctp_timeout :: integer(), 37 | ws_timeout :: integer(), 38 | http_timeout :: integer(), 39 | connect_timeout :: integer(), 40 | sctp_in_streams :: integer(), 41 | sctp_out_streams :: integer(), 42 | main_ip :: inet:ip4_address(), 43 | main_ip6 :: inet:ip6_address(), 44 | ext_ip :: inet:ip4_address(), 45 | ext_ip6 :: inet:ip6_address(), 46 | local_ips :: [inet:ip4_address()|inet:ip6_address()] 47 | }). 48 | 49 | 50 | set_config() -> 51 | Config = #nkpacket_config{ 52 | max_connections = nkpacket_app:get(max_connections), 53 | dns_cache_ttl = nkpacket_app:get(dns_cache_ttl), 54 | udp_timeout = nkpacket_app:get(udp_timeout), 55 | tcp_timeout = nkpacket_app:get(tcp_timeout), 56 | sctp_timeout = nkpacket_app:get(sctp_timeout), 57 | ws_timeout = nkpacket_app:get(ws_timeout), 58 | http_timeout = nkpacket_app:get(http_timeout), 59 | connect_timeout = nkpacket_app:get(connect_timeout), 60 | sctp_in_streams = nkpacket_app:get(sctp_in_streams), 61 | sctp_out_streams = nkpacket_app:get(sctp_out_streams), 62 | main_ip = nkpacket_app:get(main_ip), 63 | main_ip6 = nkpacket_app:get(main_ip6), 64 | ext_ip = nkpacket_app:get(ext_ip), 65 | ext_ip6 = nkpacket_app:get(ext_ip6), 66 | local_ips = nkpacket_app:get(local_ips) 67 | }, 68 | nklib_util:do_config_put(?MODULE, Config). 69 | 70 | 71 | 72 | max_connections() -> do_get_config(#nkpacket_config.max_connections). 73 | dns_cache_ttl() -> do_get_config(#nkpacket_config.dns_cache_ttl). 74 | udp_timeout() -> do_get_config(#nkpacket_config.udp_timeout). 75 | tcp_timeout() -> do_get_config(#nkpacket_config.tcp_timeout). 76 | sctp_timeout() -> do_get_config(#nkpacket_config.sctp_timeout). 77 | ws_timeout() -> do_get_config(#nkpacket_config.ws_timeout). 78 | http_timeout() -> do_get_config(#nkpacket_config.http_timeout). 79 | connect_timeout() -> do_get_config(#nkpacket_config.connect_timeout). 80 | sctp_in_streams() -> do_get_config(#nkpacket_config.sctp_in_streams). 81 | sctp_out_streams() -> do_get_config(#nkpacket_config.sctp_out_streams). 82 | main_ip() -> do_get_config(#nkpacket_config.main_ip). 83 | main_ip6() -> do_get_config(#nkpacket_config.main_ip6). 84 | ext_ip() -> do_get_config(#nkpacket_config.ext_ip). 85 | ext_ip6() -> do_get_config(#nkpacket_config.ext_ip6). 86 | local_ips() -> do_get_config(#nkpacket_config.local_ips). 87 | 88 | 89 | do_get_config(Key) -> 90 | element(Key, nklib_util:do_config_get(?MODULE)). 91 | 92 | 93 | -------------------------------------------------------------------------------- /src/nkpacket_app.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2019 Carlos Gonzalez Florido. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %% @doc NkPACKET OTP Application Module 22 | -module(nkpacket_app). 23 | -author('Carlos Gonzalez '). 24 | -behaviour(application). 25 | 26 | -export([start/0, start/2, stop/1]). 27 | -export([get/1, get/2, get_srv/2, put/2]). 28 | 29 | -include("nkpacket.hrl"). 30 | 31 | -define(APP, nkpacket). 32 | -compile({no_auto_import, [get/1, put/2]}). 33 | 34 | %% =================================================================== 35 | %% Private 36 | %% =================================================================== 37 | 38 | %% @doc Starts NkPACKET stand alone. 39 | -spec start() -> 40 | ok | {error, Reason::term()}. 41 | 42 | start() -> 43 | case nklib_util:ensure_all_started(?APP, permanent) of 44 | {ok, _Started} -> 45 | ok; 46 | Error -> 47 | Error 48 | end. 49 | 50 | 51 | %% @private OTP standard start callback 52 | start(_Type, _Args) -> 53 | put(default_certs, nkpacket_tls:defaults_certs()), 54 | Syntax = nkpacket_syntax:app_syntax(), 55 | case nklib_config:load_env(nkpacket, Syntax) of 56 | {ok, _} -> 57 | get_auto_ips(), 58 | nkpacket_config:set_config(), 59 | {ok, Pid} = nkpacket_sup:start_link(), 60 | {ok, Vsn} = application:get_key(nkpacket, vsn), 61 | lager:info("NkPACKET v~s has started.", [Vsn]), 62 | MainIp = nklib_util:to_host(nkpacket_app:get(main_ip)), 63 | MainIp6 = nklib_util:to_host(nkpacket_app:get(main_ip6)), 64 | ExtIp = nklib_util:to_host(nkpacket_app:get(ext_ip)), 65 | lager:info("Main IP is ~s (~s). External IP is ~s", 66 | [MainIp, MainIp6, ExtIp]), 67 | code:ensure_loaded(nkpacket_httpc_protocol), 68 | {ok, Pid}; 69 | {error, Error} -> 70 | lager:error("Config error: ~p", [Error]), 71 | error(config_error) 72 | end. 73 | 74 | 75 | %% @private OTP standard stop callback 76 | stop(_) -> 77 | ok. 78 | 79 | 80 | 81 | %% Config Management 82 | get(Key) -> 83 | nklib_config:get(?APP, Key). 84 | 85 | get(Key, Default) -> 86 | nklib_config:get(?APP, Key, Default). 87 | 88 | get_srv(Class, Key) -> 89 | nklib_config:get_domain(?APP, Class, Key). 90 | 91 | put(Key, Val) -> 92 | nklib_config:put(?APP, Key, Val). 93 | 94 | 95 | %% @private 96 | get_auto_ips() -> 97 | case nkpacket_app:get(main_ip) of 98 | auto -> 99 | nkpacket_app:put(main_ip, nkpacket_util:find_main_ip()); 100 | _ -> 101 | ok 102 | end, 103 | case nkpacket_app:get(main_ip6) of 104 | auto -> 105 | nkpacket_app:put(main_ip6, nkpacket_util:find_main_ip(auto, ipv6)); 106 | _ -> 107 | ok 108 | end, 109 | case nkpacket_app:get(ext_ip) of 110 | auto -> 111 | ExtIp = nkpacket_stun:ext_ip(), 112 | nkpacket_app:put(ext_ip, ExtIp); 113 | _ -> 114 | ok 115 | end, 116 | case nkpacket_app:get(ext_ip6) of 117 | auto -> 118 | nkpacket_app:put(ext_ip6, {0,0,0,0,0,0,0,1}); 119 | _ -> 120 | ok 121 | end, 122 | put(local_ips, nkpacket_util:get_local_ips()). 123 | -------------------------------------------------------------------------------- /test/sctp_test.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2015 Carlos Gonzalez Florido. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | -module(sctp_test). 22 | -author('Carlos Gonzalez '). 23 | 24 | -compile([export_all]). 25 | -compile(nowarn_export_all). 26 | -include_lib("eunit/include/eunit.hrl"). 27 | -include("nkpacket.hrl"). 28 | 29 | sctp_test_() -> 30 | case gen_sctp:open() of 31 | {ok, S} -> 32 | gen_sctp:close(S), 33 | {setup, spawn, 34 | fun() -> 35 | ok = nkpacket_app:start(), 36 | ?debugMsg("Starting SCTP test") 37 | end, 38 | fun(_) -> 39 | ok 40 | end, 41 | fun(_) -> 42 | [ 43 | fun() -> basic() end 44 | ] 45 | end 46 | }; 47 | {error, eprotonosupport} -> 48 | ?debugMsg("Skipping SCTP test (no Erlang support)"), 49 | []; 50 | {error, esocktnosupport} -> 51 | ?debugMsg("Skipping SCTP test (no OS support)"), 52 | [] 53 | end. 54 | 55 | 56 | basic() -> 57 | {Ref1, M1, Ref2, M2} = test_util:reset_2(), 58 | {ok, _, LSctp1} = nkpacket:start_listener({test_protocol, sctp, {0,0,0,0}, 0}, 59 | M1#{class=>dom1, idle_timeout=>5000}), 60 | Sctp1 = whereis(LSctp1), 61 | {ok, _, LSctp2} = nkpacket:start_listener({test_protocol, sctp, {127,0,0,1}, 0}, 62 | M2#{class=>dom2}), 63 | Sctp2 = whereis(LSctp2), 64 | timer:sleep(100), 65 | receive {Ref1, listen_init} -> ok after 1000 -> error(?LINE) end, 66 | receive {Ref2, listen_init} -> ok after 1000 -> error(?LINE) end, 67 | [Listen1] = nkpacket:get_class_ids(dom1), 68 | {ok, #nkport{ 69 | class = dom1, 70 | transp=sctp, 71 | local_ip={0,0,0,0}, local_port=Port1, 72 | listen_ip={0,0,0,0}, listen_port=Port1, 73 | remote_ip=undefined, remote_port=undefined, 74 | pid=Sctp1, socket={_Port1, 0} 75 | }} = nkpacket:get_nkport(Listen1), 76 | [Listen2] = nkpacket:get_class_ids(dom2), 77 | {ok, #nkport{ 78 | class = dom2, 79 | transp=sctp, 80 | local_ip={127,0,0,1}, local_port=Port2, 81 | listen_ip={127,0,0,1}, listen_port=Port2, 82 | remote_ip=undefined, remote_port=undefined, 83 | pid=Sctp2, socket = {_Port2, 0} 84 | }} = nkpacket:get_nkport(Listen2), 85 | {ok, {_, _, _, Port1}} = nkpacket:get_local(Sctp1), 86 | {ok, {_, _, _, _}} = nkpacket:get_local(Sctp2), 87 | 88 | Uri = "", 89 | {ok, _Conn1} = nkpacket:send(Uri, msg1, 90 | M2#{class=>dom2, connect_timeout=>5000, 91 | idle_timeout=>1000}), 92 | receive {Ref1, conn_init} -> ok after 1000 -> error(?LINE) end, 93 | receive {Ref1, {parse, msg1}} -> ok after 1000 -> error(?LINE) end, 94 | receive {Ref2, conn_init} -> ok after 1000 -> error(?LINE) end, 95 | receive {Ref2, {encode, msg1}} -> ok after 1000 -> error(?LINE) end, 96 | 97 | [{_, _, PidA}, {_, _, PidB}] = nkpacket_connection:get_all(), 98 | {ok, ConnA} = nkpacket:get_nkport(PidA), 99 | {ok, ConnB} = nkpacket:get_nkport(PidB), 100 | 101 | [ #nkport{ 102 | class = dom1, 103 | transp=sctp, 104 | local_ip={0,0,0,0}, local_port=Port1, 105 | remote_ip={127,0,0,1}, remote_port=Port2, 106 | listen_ip={0,0,0,0}, listen_port=Port1 107 | }, 108 | #nkport{ 109 | class = dom2, 110 | transp=sctp, 111 | local_ip={127,0,0,1}, local_port=Port2, 112 | remote_ip={127,0,0,1}, remote_port=Port1, 113 | listen_ip={127,0,0,1}, listen_port=Port2 114 | } 115 | ] = 116 | lists:sort([ConnA, ConnB]), 117 | 118 | % Reverse 119 | {ok, _Conn1R} = nkpacket:send({test_protocol, sctp, {127,0,0,1}, Port2}, 120 | msg2, M1#{class=>dom1}), 121 | receive {Ref2, {parse, msg2}} -> ok after 1000 -> error(?LINE) end, 122 | receive {Ref1, {encode, msg2}} -> ok after 1000 -> error(?LINE) end, 123 | 124 | % %% Connection 2 will stop after 1 msec, and will tear down conn1 125 | receive {Ref2, conn_stop} -> ok after 2000 -> error(?LINE) end, 126 | receive {Ref1, conn_stop} -> ok after 2000 -> error(?LINE) end, 127 | timer:sleep(50), 128 | [Listen2] = nkpacket:get_class_ids(dom2), 129 | [Listen1] = nkpacket:get_class_ids(dom1), 130 | test_util:ensure([Ref1, Ref2]), 131 | ok. 132 | 133 | -------------------------------------------------------------------------------- /src/http_client/nkpacket_httpc_pool.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2019 Carlos Gonzalez Florido. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %% @doc HTTP Client Pool server 22 | %% It resolves periodically the destinations and assign weights 23 | %% When a pid is request, one destination is selected randomly based on weight 24 | %% We see if we are already at full pool capacity for that destination, 25 | %% in that case one of the connections is selected randomly. If not, 26 | %% a new connection is started 27 | %% If we cannot connect to a destination, is marked as failed and retried later 28 | 29 | 30 | -module(nkpacket_httpc_pool). 31 | -author('Carlos Gonzalez '). 32 | 33 | -export([sample/0, request/5]). 34 | -export([start_link/2]). 35 | 36 | 37 | 38 | sample() -> 39 | Config = #{ 40 | targets => [ 41 | %% #{ 42 | %% url => "https://microsoft.com", 43 | %% opts => #{debug => true}, 44 | %% weight => 20 45 | %% }, 46 | #{ 47 | url => "http://127.0.0.1:9000", 48 | weight => 10, 49 | opts => #{idle_timeout=>5000, debug=>false}, 50 | pool => 3, 51 | refresh => true 52 | } 53 | ], 54 | debug => true, 55 | resolve_interval_secs => 0 56 | }, 57 | start_link(test, Config). 58 | 59 | 60 | %% =================================================================== 61 | %% Types 62 | %% =================================================================== 63 | 64 | -type id() :: nkpacket_pool:id(). 65 | 66 | 67 | -type config() :: 68 | #{ 69 | targets => [ 70 | #{ 71 | url => string()|binary(), % Can resolve to multiple IPs 72 | opts => nkpacket:connect_opts(), % Can include debug 73 | weight => integer(), % Shared weight for all IPs 74 | pool => integer(), % Connections to start 75 | refresh => boolean(), % Send a periodic GET / (idle_timeout) 76 | headers => [{binary(), binary()}] % To include in each request 77 | } 78 | ], 79 | debug => boolean(), 80 | resolve_interval_secs => integer() % Secs, 0 to avoid 81 | }. 82 | 83 | -type request_opts() :: 84 | #{ 85 | headers => [{binary(), binary()}], 86 | timeout => integer() 87 | }. 88 | 89 | 90 | 91 | %% =================================================================== 92 | %% Public 93 | %% =================================================================== 94 | 95 | 96 | %% @doc 97 | -spec start_link(id(), config()) -> 98 | {ok, pid()} | {error, term()}. 99 | 100 | start_link(Id, Config) -> 101 | Config2 = make_pool_config(Config), 102 | nkpacket_pool:start_link(Id, Config2). 103 | 104 | 105 | %% @doc 106 | -spec request(pid(), nkpacket_httpc:method(), nkpacket:path(), nkpacket:body(), request_opts()) -> 107 | {ok, nkpacket_httpc:status(), [nkpacket_httpc:header()], nkpacket_httpc:body()} 108 | | {error, term()}. 109 | 110 | request(Pid, Method, Path, Body, Opts) -> 111 | case nkpacket_pool:get_conn_pid(Pid) of 112 | {ok, ConnPid, _Meta} -> 113 | Hds = maps:get(headers, Opts, []), 114 | nkpacket_httpc:do_request(ConnPid, Method, Path, Hds, Body, Opts); 115 | {error, Error} -> 116 | {error, Error} 117 | end. 118 | 119 | 120 | %% @private 121 | make_pool_config(Config) -> 122 | Targets1 = maps:get(targets, Config, []), 123 | Targets2 = lists:map( 124 | fun(Spec1) -> 125 | Opts1 = maps:get(opts, Spec1, #{}), 126 | UserState1 = case maps:get(refresh, Spec1, false) of 127 | true -> 128 | #{refresh_request=>{get, <<"/">>, [], <<>>}}; 129 | false -> 130 | #{} 131 | end, 132 | UserState2 = case maps:get(headers, Spec1, []) of 133 | [] -> 134 | UserState1; 135 | Headers -> 136 | UserState1#{headers=>Headers} 137 | end, 138 | Opts2 = Opts1#{user_state => UserState2}, 139 | Spec2 = maps:without([refresh, headers], Spec1), 140 | Spec2#{opts=>Opts2} 141 | end, 142 | Targets1), 143 | Config#{targets=>Targets2}. -------------------------------------------------------------------------------- /src/nkpacket_connection_lib.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2019 Carlos Gonzalez Florido. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %% @private Generic transport connection process library functions 22 | -module(nkpacket_connection_lib). 23 | -author('Carlos Gonzalez '). 24 | 25 | -export([is_max/0, raw_send/2, raw_send_sync/2, raw_stop/1]). 26 | 27 | -include_lib("nklib/include/nklib.hrl"). 28 | -include_lib("kernel/include/inet_sctp.hrl"). 29 | -include("nkpacket.hrl"). 30 | 31 | 32 | -define(SYNC_TIMEOUT, 30000). 33 | -define(UDP_MAX_SIZE, 1300). 34 | 35 | 36 | 37 | %% =================================================================== 38 | %% Private 39 | %% =================================================================== 40 | 41 | %% @doc Checks if we already have the maximum number of connections 42 | -spec is_max() -> 43 | boolean(). 44 | 45 | is_max() -> 46 | Max = nkpacket_config:max_connections(), 47 | case nklib_counters:value(nkpacket_connections) of 48 | Current when Current > Max -> 49 | true; 50 | _ -> 51 | false 52 | end. 53 | 54 | 55 | %% @doc Sends data directly to a transport 56 | -spec raw_send(nkpacket:nkport(), nkpacket:outcoming()) -> 57 | ok | {error, term()}. 58 | 59 | raw_send(#nkport{transp=udp, opts=Opts} = NkPort, Data) -> 60 | MaxSize = maps:get(udp_max_size, Opts, ?UDP_MAX_SIZE), 61 | case byte_size(Data) > MaxSize of 62 | true -> 63 | {error, udp_too_large}; 64 | false -> 65 | #nkport{socket=Socket, remote_ip=Ip, remote_port=Port} = NkPort, 66 | case gen_udp:send(Socket, Ip, Port, Data) of 67 | {error, emsgsize} -> 68 | {error, udp_too_large}; 69 | Other -> 70 | Other 71 | end 72 | end; 73 | 74 | raw_send(#nkport{transp=tcp, socket=Socket}, Data) -> 75 | gen_tcp:send(Socket, Data); 76 | 77 | raw_send(#nkport{transp=tls, socket=Socket}, Data) -> 78 | % lager:warning("Send: ~p", [list_to_binary([Data])]), 79 | ssl:send(Socket, Data); 80 | 81 | raw_send(#nkport{transp=sctp, socket={Socket, AssocId}}, Data) -> 82 | gen_sctp:send(Socket, AssocId, 0, Data); 83 | 84 | raw_send(#nkport{transp=ws, socket=Socket}, Data) when is_port(Socket) -> 85 | Bin = nkpacket_connection_ws:encode(get_ws_frame(Data)), 86 | gen_tcp:send(Socket, Bin); 87 | 88 | raw_send(#nkport{transp=wss, socket={sslsocket, _, _}=Socket}, Data) -> 89 | Bin = nkpacket_connection_ws:encode(get_ws_frame(Data)), 90 | ssl:send(Socket, Bin); 91 | 92 | raw_send(#nkport{transp=Transp, socket=Pid}, Data) when is_pid(Pid) -> 93 | Msg = if 94 | Transp==ws; Transp==wss -> get_ws_frame(Data); 95 | true -> Data 96 | end, 97 | case is_process_alive(Pid) of 98 | true -> 99 | Pid ! {nkpacket_send, Msg}, 100 | ok; 101 | false -> 102 | {error, no_process} 103 | end; 104 | 105 | %% HTTP client pseudo-transport 106 | raw_send(#nkport{transp=http, socket=Socket}, Data) when is_port(Socket) -> 107 | gen_tcp:send(Socket, Data); 108 | 109 | raw_send(#nkport{transp=https, socket={sslsocket, _, _}=Socket}, Data) -> 110 | ssl:send(Socket, Data); 111 | 112 | raw_send(_, _) -> 113 | {error, invalid_transport}. 114 | 115 | 116 | %% @doc Sends data directly to a transport, ensures sync sending 117 | -spec raw_send_sync(nkpacket:nkport(), nkpacket:outcoming()) -> 118 | ok | {error, term()}. 119 | 120 | raw_send_sync(#nkport{transp=Transp, socket=Pid}, Data) when is_pid(Pid) -> 121 | Msg = if 122 | Transp==ws; Transp==wss -> get_ws_frame(Data); 123 | true -> Data 124 | end, 125 | case is_process_alive(Pid) of 126 | true -> 127 | Ref = make_ref(), 128 | Self = self(), 129 | Pid ! {nkpacket_send, Ref, Self, Msg}, 130 | receive 131 | {nkpacket_reply, Ref} -> ok 132 | after 133 | ?SYNC_TIMEOUT -> {error, timeout} 134 | end; 135 | false -> 136 | {error, no_process} 137 | end; 138 | 139 | raw_send_sync(NkPort, Data) -> 140 | raw_send(NkPort, Data). 141 | 142 | 143 | %% @private 144 | get_ws_frame(Data) when is_binary(Data) -> {binary, Data}; 145 | get_ws_frame(Data) when is_list(Data) -> {binary, list_to_binary(Data)}; 146 | get_ws_frame(Other) -> Other. 147 | 148 | 149 | %% @doc Stops a transport 150 | -spec raw_stop(nkpacket:nkport()) -> 151 | ok | {error, term()}. 152 | 153 | raw_stop(#nkport{transp=udp}) -> 154 | ok; 155 | 156 | raw_stop(#nkport{transp=tcp, socket=Socket}) -> 157 | gen_tcp:close(Socket); 158 | 159 | raw_stop(#nkport{transp=tls, socket=Socket}) -> 160 | ssl:close(Socket); 161 | 162 | raw_stop(#nkport{transp=sctp, socket={Socket, AssocId}}) -> 163 | gen_sctp:eof(Socket, #sctp_assoc_change{assoc_id=AssocId}); 164 | 165 | raw_stop(#nkport{transp=ws, socket=Socket}) when is_port(Socket) -> 166 | gen_tcp:close(Socket); 167 | 168 | raw_stop(#nkport{transp=wss, socket={sslsocket, _, _}=Socket}) -> 169 | ssl:close(Socket); 170 | 171 | raw_stop(#nkport{transp=http, socket=Socket}) when is_port(Socket) -> 172 | gen_tcp:close(Socket); 173 | 174 | raw_stop(#nkport{transp=https, socket={sslsocket, _, _}=Socket}) -> 175 | ssl:close(Socket); 176 | 177 | raw_stop(#nkport{socket=Pid}) when is_pid(Pid) -> 178 | Pid ! nkpacket_stop, 179 | ok; 180 | 181 | raw_stop(_) -> 182 | {error, invalid_transport}. 183 | 184 | -------------------------------------------------------------------------------- /src/http_client/nkpacket_httpc.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2019 Carlos Gonzalez Florido. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %% @doc Default implementation for HTTP1 clients 22 | %% Expects parameter notify_pid in user's state 23 | %% Will send messages {nkpacket_httpc_protocol, Ref, Term}, 24 | %% Term :: {head, Status, Headers} | {body, Body} | {chunk, Chunk} 25 | 26 | -module(nkpacket_httpc). 27 | -author('Carlos Gonzalez '). 28 | 29 | -export([request/3, request/4, request/5, request/6, do_connect/2, do_request/6]). 30 | -export_type([method/0, path/0, header/0, body/0]). 31 | 32 | -include("nkpacket.hrl"). 33 | -include_lib("nklib/include/nklib.hrl"). 34 | 35 | 36 | %% =================================================================== 37 | %% Types 38 | %% =================================================================== 39 | 40 | -type method() :: get | post | put | delete | head | patch | binary(). 41 | -type path() :: binary(). 42 | -type header() :: {binary(), binary()}. 43 | -type body() :: iolist(). 44 | -type status() :: 100..599. 45 | 46 | -type opts() :: 47 | nkpacket:connect_opts() | 48 | #{ 49 | headers => [header()] % Headers to include in all requests 50 | }. 51 | 52 | 53 | 54 | %% =================================================================== 55 | %% Public 56 | %% =================================================================== 57 | 58 | %% @doc 59 | -spec request(nkpacket:connect_spec(), method(), path()) -> 60 | {ok, status(), [header()], binary()} | {error, term()}. 61 | 62 | request(Url, Method, Path) -> 63 | request(Url, Method, Path, []). 64 | 65 | 66 | %% @doc 67 | -spec request(nkpacket:connect_spec(), method(), path(), [header()]) -> 68 | {ok, status(), [header()], binary()} | {error, term()}. 69 | 70 | request(Url, Method, Path, Hds) -> 71 | request(Url, Method, Path, Hds, <<>>). 72 | 73 | 74 | %% @doc 75 | -spec request(nkpacket:connect_spec(), method(), path(), [header()], body()) -> 76 | {ok, status(), [header()], binary()} | {error, term()}. 77 | 78 | request(Url, Method, Path, Hds, Body) -> 79 | request(Url, Method, Path, Hds, Body, #{}). 80 | 81 | 82 | %% @doc 83 | -spec request(nkpacket:connect_spec(), method(), path(), [header()], body(), opts()) -> 84 | {ok, status(), [header()], binary()} | {error, term()}. 85 | 86 | request(Url, Method, Path, Hds, Body, Opts) -> 87 | case do_connect(Url, Opts) of 88 | {ok, ConnPid} -> 89 | do_request(ConnPid, Method, Path, Hds, Body, Opts); 90 | {error, Error} -> 91 | {error, Error} 92 | end. 93 | 94 | 95 | %% @doc 96 | -spec do_connect(nkpacket:connect_spec(), opts()) -> 97 | {ok, pid()} | {error, term()}. 98 | 99 | do_connect(Url, Opts) -> 100 | ConnOpts = #{ 101 | monitor => self(), 102 | user_state => maps:with([headers], Opts), 103 | connect_timeout => maps:get(connect_timeout, Opts, 1000), 104 | idle_timeout => maps:get(idle_timeout, Opts, 60000), 105 | debug => maps:get(debug, Opts, false) 106 | }, 107 | case nkpacket:connect(Url, ConnOpts) of 108 | {ok, ConnPid} -> 109 | {ok, ConnPid}; 110 | {error, Error} -> 111 | {error, Error} 112 | end. 113 | 114 | 115 | %% @doc 116 | -spec do_request(pid(), method(), path(), [header()], body(), opts()) -> 117 | {ok, status(), [header()], binary()} | {error, term()}. 118 | 119 | do_request(ConnPid, Method, Path, Hds, Body, Opts) when is_atom(Method) -> 120 | Ref = make_ref(), 121 | Req = #{ 122 | ref => Ref, 123 | pid => self(), 124 | method => Method, 125 | path => Path, 126 | headers => Hds, 127 | body => Body 128 | }, 129 | Timeout = maps:get(timeout, Opts, 5000), 130 | case nkpacket:send(ConnPid, {nkpacket_http, Req}) of 131 | {ok, _ConnPid2} -> 132 | receive 133 | {nkpacket_httpc_protocol, Ref, {head, Status, Headers}} -> 134 | do_request_body(Ref, Opts, Timeout, Status, Headers, []); 135 | {nkpacket_httpc_protocol, Ref, {error, Error}} -> 136 | {error, Error} 137 | after 138 | Timeout -> 139 | {error, timeout} 140 | end; 141 | {error, Error} -> 142 | {error, Error} 143 | end; 144 | 145 | do_request(ConnPid, Method, Path, Hds, Body, Opts) -> 146 | case catch binary_to_existing_atom(nklib_util:to_lower(Method), latin1) of 147 | Method2 when is_atom(Method2) -> 148 | do_request(ConnPid, Method2, Path, Hds, Body, Opts); 149 | _ -> 150 | {error, method_unknown} 151 | end. 152 | 153 | 154 | %% @private 155 | do_request_body(Ref, Opts, Timeout, Status, Headers, Chunks) -> 156 | receive 157 | {nkpacket_httpc_protocol, Ref, {chunk, Data}} -> 158 | do_request_body(Ref, Opts, Timeout, Status, Headers, [Data|Chunks]); 159 | {nkpacket_httpc_protocol, Ref, {body, Body}} -> 160 | case Chunks of 161 | [] -> 162 | {ok, Status, Headers, Body}; 163 | _ when Body == <<>> -> 164 | {ok, Status, Headers, list_to_binary(lists:reverse(Chunks))}; 165 | _ -> 166 | {error, invalid_chunked} 167 | end; 168 | {nkpacket_httpc_protocol, Ref, {error, Error}} -> 169 | {error, Error} 170 | after Timeout -> 171 | {error, timeout2} 172 | end. 173 | -------------------------------------------------------------------------------- /test/ipv6_test.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2015 Carlos Gonzalez Florido. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | -module(ipv6_test). 22 | -author('Carlos Gonzalez '). 23 | 24 | -compile([export_all]). 25 | -compile(nowarn_export_all). 26 | -include_lib("eunit/include/eunit.hrl"). 27 | -include("nkpacket.hrl"). 28 | 29 | ipv6_test_() -> 30 | {setup, spawn, 31 | fun() -> 32 | ok = nkpacket_app:start(), 33 | ?debugMsg("Starting IPv6 test") 34 | end, 35 | fun(_) -> 36 | ok 37 | end, 38 | fun(_) -> 39 | [ 40 | fun() -> basic() end, 41 | fun() -> is_local() end 42 | ] 43 | end 44 | }. 45 | 46 | 47 | basic() -> 48 | LPort1 = test_util:get_port(tcp), 49 | {Ref1, M1, Ref2, M2} = test_util:reset_2(), 50 | All6 = {0,0,0,0,0,0,0,0}, 51 | Local6 = {0,0,0,0,0,0,0,1}, 52 | Url = "", 53 | {ok, _, Tcp1} = nkpacket:start_listener(Url, M1#{class=>dom1}), 54 | {ok, _, Tcp2} = nkpacket:start_listener(#nkconn{protocol=test_protocol, transp=tcp, ip=All6, port=0, opts=M2#{class=>dom2}}), 55 | {ok, {_, tcp, _, LPort1}} = nkpacket:get_local(Tcp1), 56 | {ok, {_, tcp, _, LPort2}} = nkpacket:get_local(Tcp2), 57 | case LPort2 of 58 | 1235 -> ok; 59 | _ -> lager:warning("Could not open 1235") 60 | end, 61 | 62 | timer:sleep(100), 63 | receive {Ref1, listen_init} -> ok after 1000 -> error(?LINE) end, 64 | receive {Ref2, listen_init} -> ok after 1000 -> error(?LINE) end, 65 | 66 | [Listen1] = nkpacket:get_class_ids(dom1), 67 | {ok, #nkport{ 68 | class = dom1, 69 | transp = tcp, 70 | local_ip = Local6, local_port = LPort1, 71 | listen_ip = Local6, listen_port = LPort1, 72 | protocol = test_protocol 73 | }} = nkpacket:get_nkport(Listen1), 74 | 75 | [Listen2] = nkpacket:get_class_ids(dom2), 76 | {ok, #nkport{ 77 | class = dom2, 78 | transp = tcp, 79 | local_ip = All6, local_port = LPort2, 80 | remote_ip = undefined, remote_port = undefined, 81 | listen_ip = All6, listen_port = LPort2, 82 | protocol = test_protocol 83 | }} = nkpacket:get_nkport(Listen2), 84 | 85 | {ok, _} = nkpacket:send(Url, msg1, M2#{class=>dom2, base_nkport=>true}), 86 | receive {Ref1, conn_init} -> ok after 1000 -> error(?LINE) end, 87 | receive {Ref1, {parse, msg1}} -> ok after 1000 -> error(?LINE) end, 88 | receive {Ref2, conn_init} -> ok after 1000 -> error(?LINE) end, 89 | receive {Ref2, {encode, msg1}} -> ok after 1000 -> error(?LINE) end, 90 | 91 | [{_, Conn1}] = nkpacket_connection:get_all_class(dom1), 92 | {ok, #nkport{ 93 | class = dom1, 94 | transp=tcp, 95 | local_ip=Local6, local_port=_ConnPort1, 96 | remote_ip=Local6, remote_port=ConnPort2, 97 | listen_ip=Local6, listen_port=LPort1 98 | }} = nkpacket:get_nkport(Conn1), 99 | 100 | [{_, Conn2}] = nkpacket_connection:get_all_class(dom2), 101 | {ok, #nkport{ 102 | class = dom2, 103 | transp=tcp, 104 | local_ip=Local6, local_port=ConnPort2, 105 | remote_ip=Local6, remote_port=LPort1, 106 | listen_ip=All6, listen_port=LPort2 107 | }} = nkpacket:get_nkport(Conn2), 108 | 109 | ok = nkpacket:stop_listeners(Tcp1), 110 | ok = nkpacket:stop_listeners(Tcp2), 111 | 112 | receive {Ref2, conn_stop} -> ok after 2000 -> error(?LINE) end, 113 | receive {Ref1, conn_stop} -> ok after 2000 -> error(?LINE) end, 114 | receive {Ref2, listen_stop} -> ok after 2000 -> error(?LINE) end, 115 | receive {Ref1, listen_stop} -> ok after 2000 -> error(?LINE) end, 116 | test_util:ensure([Ref1, Ref2]). 117 | 118 | 119 | is_local() -> 120 | LPort0 = test_util:get_port(tcp), 121 | LPort1 = test_util:get_port(tcp), 122 | LPort2 = test_util:get_port(tcp), 123 | _ = test_util:reset_2(), 124 | 125 | {ok, _, Tcp0} = nkpacket:start_listener( 126 | "", #{}), 127 | {ok, _, Tcp1} = nkpacket:start_listener( 128 | "", #{class=>dom1}), 129 | {ok, _, Tcp2} = nkpacket:start_listener( 130 | "", #{class=>dom2}), 131 | 132 | [Uri0] = nklib_parse:uris( 133 | ""), 134 | true = nkpacket:is_local(Uri0), 135 | false = nkpacket:is_local(Uri0, #{class=>dom1}), 136 | false = nkpacket:is_local(Uri0, #{class=>dom2}), 137 | 138 | [Uri1] = nklib_parse:uris( 139 | ""), 140 | false = nkpacket:is_local(Uri1), 141 | true = nkpacket:is_local(Uri1, #{class=>dom1}), 142 | false = nkpacket:is_local(Uri1, #{class=>dom2}), 143 | 144 | [Uri2] = nklib_parse:uris( 145 | ""), 146 | false = nkpacket:is_local(Uri2), 147 | false = nkpacket:is_local(Uri2, #{class=>dom1}), 148 | true = nkpacket:is_local(Uri2, #{class=>dom2}), 149 | 150 | [Uri3] = nklib_parse:uris( 151 | ""), 152 | false = nkpacket:is_local(Uri3), 153 | false = nkpacket:is_local(Uri3, #{class=>dom1}), 154 | false = nkpacket:is_local(Uri3, #{class=>dom2}), 155 | 156 | case 157 | [Ip || Ip <- nkpacket_config:local_ips(), size(Ip)==8] 158 | -- [{0,0,0,0,0,0,0,1}] 159 | of 160 | [] -> 161 | ok; 162 | [Local6|_] -> 163 | Url4 = list_to_binary([ 164 | ""]), 166 | [Uri4] = nklib_parse:uris(Url4), 167 | false = nkpacket:is_local(Uri4, #{class=>dom1}), 168 | true = nkpacket:is_local(Uri4, #{class=>dom2}) 169 | end, 170 | 171 | ok = nkpacket:stop_listeners(Tcp0), 172 | ok = nkpacket:stop_listeners(Tcp1), 173 | ok = nkpacket:stop_listeners(Tcp2). 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | -------------------------------------------------------------------------------- /test/test_protocol.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2015 Carlos Gonzalez Florido. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %% @doc TEST Protocol behaviour 22 | 23 | -module(test_protocol). 24 | -author('Carlos Gonzalez '). 25 | % -behaviour(nkpacket_protocol). 26 | 27 | -export([transports/1, default_port/1]). 28 | -export([conn_init/1, conn_parse/3, conn_encode/3, conn_stop/3]). 29 | -export([conn_handle_call/4, conn_handle_cast/3, conn_handle_info/3]). 30 | -export([listen_init/1, listen_parse/5, listen_stop/3]). 31 | -export([listen_handle_call/4, listen_handle_cast/3, listen_handle_info/3]). 32 | -export([http_init/4]). 33 | 34 | -include("nkpacket.hrl"). 35 | 36 | %% =================================================================== 37 | %% Types 38 | %% =================================================================== 39 | 40 | 41 | -spec transports(nklib:scheme()) -> 42 | [nkpacket:transport()]. 43 | 44 | transports(_) -> [http, https, udp, tcp, tls, sctp, ws, wss]. 45 | 46 | -spec default_port(nkpacket:transport()) -> 47 | inet:port_number(). 48 | 49 | default_port(udp) -> 1234; 50 | default_port(tcp) -> 1235; 51 | default_port(tls) -> 1236; 52 | default_port(sctp) -> 1237; 53 | default_port(ws) -> 1238; 54 | default_port(wss) -> 1239; 55 | default_port(http) -> 1240; 56 | default_port(https) -> 1241; 57 | default_port(_) -> invalid. 58 | 59 | 60 | 61 | %% =================================================================== 62 | %% Listen callbacks 63 | %% =================================================================== 64 | 65 | 66 | -record(listen_state, { 67 | pid, 68 | ref 69 | }). 70 | 71 | 72 | -spec listen_init(nkpacket:nkport()) -> 73 | #listen_state{}. 74 | 75 | listen_init(NkPort) -> 76 | lager:info("Protocol LISTEN init: ~p (~p)", [NkPort, self()]), 77 | State = case nkpacket:get_user_state(NkPort) of 78 | {ok, {Pid, Ref}} -> 79 | #listen_state{pid=Pid, ref=Ref}; 80 | _ -> 81 | #listen_state{} 82 | end, 83 | maybe_reply(listen_init, State), 84 | {ok, State}. 85 | 86 | 87 | listen_handle_call(Msg, _From, _NkPort, State) -> 88 | lager:warning("Unexpected call: ~p", [Msg]), 89 | {ok, State}. 90 | 91 | 92 | listen_handle_cast(Msg, _NkPort, State) -> 93 | lager:warning("Unexpected cast: ~p", [Msg]), 94 | {ok, State}. 95 | 96 | 97 | listen_handle_info({'EXIT', _, forced_stop}, _NkPort, State) -> 98 | {stop, forced_stop, State}; 99 | 100 | listen_handle_info(Msg, _NkPort, State) -> 101 | lager:warning("Unexpected listen info: ~p", [Msg]), 102 | {ok, State}. 103 | 104 | listen_parse(Ip, Port, Data, _NkPort, State) -> 105 | lager:info("LISTEN Parsing fromm ~p:~p: ~p", [Ip, Port, Data]), 106 | maybe_reply({listen_parse, Data}, State), 107 | {ok, State}. 108 | 109 | listen_stop(Reason, _NkPort, State) -> 110 | lager:info("LISTEN stop: ~p, ~p", [Reason, State]), 111 | maybe_reply(listen_stop, State), 112 | ok. 113 | 114 | 115 | %% =================================================================== 116 | %% Conn callbacks 117 | %% =================================================================== 118 | 119 | 120 | -record(conn_state, { 121 | pid, 122 | ref 123 | }). 124 | 125 | -spec conn_init(nkpacket:nkport()) -> 126 | {ok, #conn_state{}}. 127 | 128 | conn_init(NkPort) -> 129 | lager:info("Protocol CONN init: ~p (~p)", [NkPort, self()]), 130 | State = case nkpacket:get_user_state(NkPort) of 131 | {ok, {Pid, Ref}} -> #conn_state{pid=Pid, ref=Ref}; 132 | _ -> #conn_state{} 133 | end, 134 | maybe_reply(conn_init, State), 135 | {ok, State}. 136 | 137 | 138 | conn_parse({text, Data}, _NkPort, State) -> 139 | lager:debug("Parsing WS TEXT: ~p", [Data]), 140 | maybe_reply({parse, {text, Data}}, State), 141 | {ok, State}; 142 | 143 | conn_parse({binary, <<>>}, _NkPort, State) -> 144 | lager:error("EMPTY"), 145 | {ok, State}; 146 | 147 | conn_parse({binary, Data}, _NkPort, State) -> 148 | Msg = erlang:binary_to_term(Data), 149 | lager:debug("Parsing WS BIN: ~p", [Msg]), 150 | maybe_reply({parse, {binary, Msg}}, State), 151 | {ok, State}; 152 | 153 | conn_parse(close, _NkPort, State) -> 154 | {ok, State}; 155 | 156 | conn_parse(pong, _NkPort, State) -> 157 | {ok, State}; 158 | 159 | conn_parse({pong, Payload}, _NkPort, State) -> 160 | lager:debug("Parsing WS PONG: ~p", [Payload]), 161 | maybe_reply({pong, Payload}, State), 162 | {ok, State}; 163 | 164 | conn_parse(Data, #nkport{class=Class}, State) -> 165 | Msg = erlang:binary_to_term(Data), 166 | lager:debug("Parsing: ~p (~p)", [Msg, Class]), 167 | maybe_reply({parse, Msg}, State), 168 | {ok, State}. 169 | 170 | conn_encode({nkraw, Msg}, NkPort, State) -> 171 | lager:debug("UnParsing RAW: ~p, ~p", [Msg, NkPort]), 172 | maybe_reply({encode, Msg}, State), 173 | {ok, Msg, State}; 174 | 175 | conn_encode(Msg, NkPort, State) -> 176 | lager:debug("UnParsing: ~p, ~p", [Msg, NkPort]), 177 | maybe_reply({encode, Msg}, State), 178 | {ok, erlang:term_to_binary(Msg), State}. 179 | 180 | conn_handle_call(Msg, _From, _NkPort, State) -> 181 | lager:warning("Unexpected call: ~p", [Msg]), 182 | {ok, State}. 183 | 184 | 185 | conn_handle_cast(Msg, _NkPort, State) -> 186 | lager:warning("Unexpected cast: ~p", [Msg]), 187 | {ok, State}. 188 | 189 | 190 | conn_handle_info(Msg, _NkPort, State) -> 191 | lager:warning("Unexpected conn info: ~p", [Msg]), 192 | {ok, State}. 193 | 194 | conn_stop(Reason, _NkPort, State) -> 195 | lager:info("CONN stop: ~p", [Reason]), 196 | maybe_reply(conn_stop, State), 197 | ok. 198 | 199 | 200 | 201 | % encode(Msg, NkPort) -> 202 | % lager:info("Quick UnParsing: ~p, ~p", [Msg, NkPort]), 203 | % {ok, erlang:term_to_binary(Msg)}. 204 | 205 | 206 | 207 | %% =================================================================== 208 | %% HTTP 209 | %% =================================================================== 210 | 211 | http_init(Paths, Req, Env, NkPort) -> 212 | case nkpacket:get_class(NkPort) of 213 | {ok, Class} when Class==dom1; Class==dom2; Class==dom3; Class==dom4 -> 214 | {ok, {Pid, Ref}} = nkpacket:get_user_state(NkPort), 215 | Pid ! {Ref, http_init, self()}, 216 | Req2 = cowboy_req:reply(200, #{<<"content-type">> => <<"text/plain">>}, <<"Hello World!">>, Req), 217 | Pid ! {Ref, http_terminate, self()}, 218 | {ok, Req2, Env}; 219 | {ok, dom5} when Paths==[] -> 220 | {redirect, "/index.html"}; 221 | {ok, dom5} -> 222 | {cowboy_static, {priv_dir, nkpacket, "/www"}} 223 | end. 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | %% =================================================================== 232 | %% Util 233 | %% =================================================================== 234 | 235 | 236 | maybe_reply(Msg, #listen_state{pid=Pid, ref=Ref}) when is_pid(Pid) -> Pid ! {Ref, Msg}; 237 | maybe_reply(Msg, #conn_state{pid=Pid, ref=Ref}) when is_pid(Pid) -> Pid ! {Ref, Msg}; 238 | maybe_reply(_, _) -> ok. 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | -------------------------------------------------------------------------------- /src/nkpacket_tls.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2019 Carlos Gonzalez Florido. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %% @doc NkPACKET TLS processing from Hackney 22 | 23 | -module(nkpacket_tls). 24 | -author('Carlos Gonzalez '). 25 | -export([make_outbound_opts/1, make_inbound_opts/1, defaults_certs/0]). 26 | -export([partial_chain/1]). 27 | 28 | -include("nkpacket.hrl"). 29 | -include_lib("public_key/include/OTP-PUB-KEY.hrl"). 30 | 31 | %% =================================================================== 32 | %% Public 33 | %% =================================================================== 34 | 35 | 36 | %% @doc Adds SSL options 37 | -spec make_outbound_opts(nkpacket:tls_types()) -> 38 | list(). 39 | 40 | make_outbound_opts(#{tls_verify:=host, host:=Host}) -> 41 | Defaults = default_outbound(), 42 | Opts = Defaults#{ 43 | verify => verify_peer, 44 | depth => 99, 45 | cacerts => certifi:cacerts(), 46 | partial_chain => fun ?MODULE:partial_chain/1, 47 | verify_fun => { 48 | fun ssl_verify_hostname:verify_fun/3, 49 | [{check_hostname, binary_to_list(Host)}] 50 | } 51 | }, 52 | maps:to_list(Opts); 53 | 54 | make_outbound_opts(#{tls_verify:=host}=Opts) -> 55 | lager:warning("NkPACKET: TLS host is not available"), 56 | make_outbound_opts(maps:remove(tls_verify, Opts)); 57 | 58 | make_outbound_opts(Opts) -> 59 | make_opts(Opts, default_outbound()). 60 | 61 | 62 | %% @doc Adds SSL options 63 | -spec make_inbound_opts(nkpacket:tls_types()) -> 64 | list(). 65 | 66 | make_inbound_opts(Opts) -> 67 | make_opts(Opts, default_inbound()). 68 | 69 | 70 | %% @private 71 | make_opts(Opts, Defaults) -> 72 | Opts2 = maps:fold( 73 | fun(Key, Val, Acc) -> 74 | case Key of 75 | tls_verify -> Acc#{verify => Val}; 76 | tls_certfile -> Acc#{certfile => Val}; 77 | tls_keyfile -> Acc#{keyfile => Val}; 78 | tls_cacertfile -> Acc#{cacertfile => Val}; 79 | tls_password -> Acc#{password => Val}; 80 | tls_depth -> Acc#{depth => Val}; 81 | tls_versions -> Acc#{versions => Val}; 82 | _ -> Acc 83 | end 84 | end, 85 | Defaults, 86 | Opts), 87 | Opts3 = case Opts2 of 88 | #{verify:=true} -> 89 | Opts2#{verify=>verify_peer, fail_if_no_peer_cert=>true}; 90 | _ -> 91 | maps:remove(verify, Opts2) 92 | end, 93 | maps:to_list(Opts3). 94 | 95 | 96 | default_outbound() -> 97 | #{ 98 | secure_renegotiate => true, 99 | reuse_sessions => true, 100 | %honor_cipher_order => true, % server only? 101 | ciphers => ciphers(), 102 | versions => ['tlsv1.2', 'tlsv1.1', tlsv1] % removed sslv3 103 | }. 104 | 105 | default_inbound() -> 106 | Certs = nkpacket_app:get(default_certs), 107 | Certs#{ 108 | versions => ['tlsv1.2', 'tlsv1.1', tlsv1] 109 | }. 110 | 111 | 112 | %% from Hackney and https://wiki.mozilla.org/Security/Server_Side_TLS 113 | ciphers() -> 114 | [ 115 | "ECDHE-ECDSA-AES256-GCM-SHA384", 116 | "ECDHE-RSA-AES256-GCM-SHA384", 117 | "ECDHE-ECDSA-AES256-SHA384", 118 | "ECDHE-RSA-AES256-SHA384", 119 | "ECDHE-ECDSA-DES-CBC3-SHA", 120 | "ECDH-ECDSA-AES256-GCM-SHA384", 121 | "ECDH-RSA-AES256-GCM-SHA384", 122 | "ECDH-ECDSA-AES256-SHA384", 123 | "ECDH-RSA-AES256-SHA384", 124 | "DHE-DSS-AES256-GCM-SHA384", 125 | "DHE-DSS-AES256-SHA256", 126 | "AES256-GCM-SHA384", 127 | "AES256-SHA256", 128 | "ECDHE-ECDSA-AES128-GCM-SHA256", 129 | "ECDHE-RSA-AES128-GCM-SHA256", 130 | "ECDHE-ECDSA-AES128-SHA256", 131 | "ECDHE-RSA-AES128-SHA256", 132 | "ECDH-ECDSA-AES128-GCM-SHA256", 133 | "ECDH-RSA-AES128-GCM-SHA256", 134 | "ECDH-ECDSA-AES128-SHA256", 135 | "ECDH-RSA-AES128-SHA256", 136 | "DHE-DSS-AES128-GCM-SHA256", 137 | "DHE-DSS-AES128-SHA256", 138 | "AES128-GCM-SHA256", 139 | "AES128-SHA256", 140 | "ECDHE-ECDSA-AES256-SHA", 141 | "ECDHE-RSA-AES256-SHA", 142 | "DHE-DSS-AES256-SHA", 143 | "ECDH-ECDSA-AES256-SHA", 144 | "ECDH-RSA-AES256-SHA", 145 | "AES256-SHA", 146 | "ECDHE-ECDSA-AES128-SHA", 147 | "ECDHE-RSA-AES128-SHA", 148 | "DHE-DSS-AES128-SHA", 149 | "ECDH-ECDSA-AES128-SHA", 150 | "ECDH-RSA-AES128-SHA", 151 | "AES128-SHA" 152 | ]. 153 | 154 | defaults_certs() -> 155 | case code:priv_dir(nkpacket) of 156 | PrivDir when is_list(PrivDir) -> 157 | #{ 158 | certfile => filename:join(PrivDir, "cert.pem"), 159 | keyfile => filename:join(PrivDir, "key.pem") 160 | }; 161 | _ -> 162 | #{} 163 | end. 164 | 165 | 166 | %% @private 167 | partial_chain(Certs) -> 168 | find_partial_chain(lists:reverse(Certs)). 169 | 170 | 171 | %% @private 172 | find_partial_chain([]) -> 173 | unknown_ca; 174 | 175 | find_partial_chain([Cert|Rest]) -> 176 | case check_cert(certifi:cacerts(), Cert) of 177 | true -> 178 | {trusted_ca, Cert}; 179 | false -> 180 | find_partial_chain(Rest) 181 | end. 182 | 183 | 184 | %% @private 185 | check_cert([], _Cert) -> 186 | false; 187 | 188 | check_cert([CACert|Rest], Cert) -> 189 | case extract_public_key_info(CACert) == extract_public_key_info(Cert) of 190 | true -> 191 | true; 192 | false -> 193 | check_cert(Rest, Cert) 194 | end. 195 | 196 | extract_public_key_info(Cert) -> 197 | Cert2 = public_key:pkix_decode_cert(Cert, otp), 198 | ((Cert2#'OTPCertificate'.tbsCertificate)#'OTPTBSCertificate'.subjectPublicKeyInfo). 199 | 200 | 201 | 202 | %% Code from hackney, previous is much cleaner but not tested 203 | 204 | %%%% code from rebar3 undert BSD license 205 | %%partial_chain(Certs) -> 206 | %% Certs1 = lists:reverse([{Cert, public_key:pkix_decode_cert(Cert, otp)} || 207 | %% Cert <- Certs]), 208 | %% CACerts = certifi:cacerts(), 209 | %% CACerts1 = [public_key:pkix_decode_cert(Cert, otp) || Cert <- CACerts], 210 | %% 211 | %% case find(fun({_, Cert}) -> 212 | %% check_cert(CACerts1, Cert) 213 | %% end, Certs1) of 214 | %% {ok, Trusted} -> 215 | %% {trusted_ca, element(1, Trusted)}; 216 | %% _ -> 217 | %% unknown_ca 218 | %% end. 219 | %% 220 | %%extract_public_key_info(Cert) -> 221 | %% ((Cert#'OTPCertificate'.tbsCertificate)#'OTPTBSCertificate'.subjectPublicKeyInfo). 222 | %% 223 | %%check_cert(CACerts, Cert) -> 224 | %% lists:any(fun(CACert) -> 225 | %% extract_public_key_info(CACert) == extract_public_key_info(Cert) 226 | %% end, CACerts). 227 | %% 228 | %%-spec find(fun(), list()) -> {ok, term()} | error. 229 | %%find(Fun, [Head|Tail]) when is_function(Fun) -> 230 | %% case Fun(Head) of 231 | %% true -> 232 | %% {ok, Head}; 233 | %% false -> 234 | %% find(Fun, Tail) 235 | %% end; 236 | %%find(_Fun, []) -> 237 | %% error. 238 | 239 | 240 | -------------------------------------------------------------------------------- /src/nkpacket_syntax.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2019 Carlos Gonzalez Florido. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %% @doc NkPACKET Syntax 22 | -module(nkpacket_syntax). 23 | -author('Carlos Gonzalez '). 24 | 25 | -export([app_syntax/0]). 26 | -export([syntax/0, safe_syntax/0, tls_syntax/0, tls_syntax/1, extract_tls/1, 27 | packet_syntax/0, resolve_syntax/1]). 28 | -export([spec_http_proto/3, spec_headers/1]). 29 | 30 | -include("nkpacket.hrl"). 31 | 32 | %% =================================================================== 33 | %% Public 34 | %% =================================================================== 35 | 36 | 37 | app_syntax() -> 38 | #{ 39 | max_connections => {integer, 1, 1000000}, 40 | dns_cache_ttl => {integer, 0, none}, 41 | udp_timeout => nat_integer, % Overrided by idle_timeout 42 | tcp_timeout => nat_integer, % " 43 | sctp_timeout => nat_integer, % " 44 | ws_timeout => nat_integer, % " 45 | http_timeout => nat_integer, % " 46 | connect_timeout => nat_integer, 47 | sctp_out_streams => nat_integer, 48 | sctp_in_streams => nat_integer, 49 | main_ip => [ip4, {atom, [auto]}], 50 | main_ip6 => [ip6, {atom, [auto]}], 51 | ext_ip => [ip4, {atom, [auto]}], 52 | ext_ip6 => [ip6, {atom, [auto]}], 53 | '__defaults' => #{ 54 | max_connections => 1024, 55 | dns_cache_ttl => 30000, % msecs 56 | udp_timeout => 30000, % 57 | tcp_timeout => 180000, % 58 | sctp_timeout => 180000, % 59 | ws_timeout => 180000, % 60 | http_timeout => 180000, % 61 | connect_timeout => 30000, % 62 | sctp_out_streams => 10, 63 | sctp_in_streams => 10, 64 | main_ip => auto, 65 | main_ip6 => auto, 66 | ext_ip => auto, 67 | ext_ip6 => auto 68 | } 69 | }. 70 | 71 | 72 | syntax() -> 73 | Base = #{ 74 | id => any, 75 | class => any, 76 | monitor => proc, 77 | protocol => module, 78 | idle_timeout => pos_integer, 79 | connect_timeout => nat_integer, 80 | sctp_out_streams => nat_integer, 81 | sctp_in_streams => nat_integer, 82 | no_dns_cache => boolean, 83 | refresh_fun => {function, 1}, 84 | udp_starts_tcp => boolean, 85 | udp_to_tcp => boolean, 86 | udp_max_size => nat_integer, % Only used for sending packets 87 | udp_no_connections => boolean, 88 | udp_stun_reply => boolean, 89 | udp_stun_t1 => nat_integer, 90 | tcp_packet => [{atom, [raw]}, {integer, [1, 2, 4]}], 91 | send_timeout => integer, 92 | send_timeout_close => boolean, 93 | tcp_max_connections => nat_integer, 94 | tcp_listeners => nat_integer, 95 | user => binary, 96 | password => binary, 97 | host => host, 98 | path => binary, % Changed from 'path' to allow ending '/' 99 | get_headers => [boolean, {list, binary}], 100 | external_url => binary, 101 | http_inactivity_timeout => pos_integer, % msecs 102 | http_max_empty_lines => pos_integer, 103 | http_max_header_name_length => pos_integer, 104 | http_max_header_value_length => pos_integer, 105 | http_max_headers => pos_integer, 106 | http_max_keepalive => pos_integer, 107 | http_max_method_length => pos_integer, 108 | http_max_request_line_length => pos_integer, 109 | http_request_timeout => pos_integer, 110 | ws_proto => lower, 111 | headers => fun ?MODULE:spec_headers/1, 112 | http_proto => fun ?MODULE:spec_http_proto/3, 113 | force_new => boolean, 114 | resolve_type => {atom, [listen, connect, send]}, 115 | base_nkport => [boolean, {record, nkport}], 116 | user_state => any, 117 | debug => boolean 118 | }, 119 | add_tls_syntax(Base). 120 | 121 | 122 | safe_syntax() -> 123 | Opts = [ 124 | id, 125 | idle_timeout, 126 | connect_timeout, 127 | no_dns_cache, 128 | udp_max_size, 129 | tcp_packet, 130 | send_timeout, 131 | send_timeout_close, 132 | tcp_listeners, 133 | host, 134 | path, 135 | user, 136 | password, 137 | get_headers, 138 | external_url, 139 | ws_proto, 140 | headers, % Not sure 141 | debug, 142 | http_inactivity_timeout, 143 | http_max_empty_lines, 144 | http_max_header_name_length, 145 | http_max_header_value_length, 146 | http_max_headers, 147 | http_max_keepalive, 148 | http_max_method_length, 149 | http_max_request_line_length, 150 | http_request_timeout 151 | ] ++ maps:keys(tls_syntax()), 152 | Syntax = syntax(), 153 | maps:with(Opts, Syntax). 154 | 155 | 156 | tls_syntax() -> 157 | tls_syntax(#{}). 158 | 159 | 160 | %% Config for letsencrypt: 161 | %% tls_keyfile => "/etc/letsencrypt/archive/.../privkey.pem", 162 | %% tls_cacertfile => "/etc/letsencrypt/archive/.../chain.pem", 163 | %% tls_certfile => "/etc/letsencrypt/archive/.../cert.pem" 164 | 165 | tls_syntax(Base) -> 166 | Base#{ 167 | tls_verify => {atom, [host, true, false]}, 168 | tls_certfile => string, 169 | tls_keyfile => string, 170 | tls_cacertfile => string, 171 | tls_password => string, 172 | tls_depth => {integer, 0, 16}, 173 | tls_versions => {list, atom} 174 | }. 175 | 176 | 177 | add_tls_syntax(Syntax) -> 178 | tls_syntax(Syntax). 179 | 180 | 181 | extract_tls(Map) when is_map(Map) -> 182 | Keys = lists:filter( 183 | fun(Key) -> 184 | case nklib_util:to_list(Key) of 185 | "tls_" ++ _ -> true; 186 | _ -> false 187 | end 188 | end, 189 | maps:keys(Map)), 190 | maps:with(Keys, Map). 191 | 192 | 193 | packet_syntax() -> 194 | #{ 195 | packet_idle_timeout => pos_integer, 196 | packet_connect_timeout => nat_integer, 197 | packet_sctp_out_streams => nat_integer, 198 | packet_sctp_in_streams => nat_integer, 199 | packet_no_dns_cache => boolean 200 | }. 201 | 202 | 203 | resolve_syntax(Protocol) -> 204 | {mfa, nkpacket_resolve, check_syntax, [Protocol]}. 205 | 206 | 207 | 208 | %% @private 209 | spec_http_proto(_, {static, #{path:=_}}, _) -> ok; 210 | spec_http_proto(_, {dispatch, #{routes:=_}}, _) -> ok; 211 | spec_http_proto(_, {custom, #{env:=_, middlewares:=_}}, _) -> ok; 212 | spec_http_proto(_, _, _) -> error. 213 | 214 | 215 | %% @private 216 | spec_headers(List) when is_list(List) -> 217 | spec_headers(List, []); 218 | spec_headers(_) -> 219 | error. 220 | 221 | 222 | %% @private 223 | spec_headers([], Acc) -> 224 | {ok, Acc}; 225 | spec_headers([{K, V}|Rest], Acc) -> 226 | spec_headers(Rest, [{to_bin(K), to_bin(V)}|Acc]); 227 | spec_headers(_, _Acc) -> 228 | error. 229 | 230 | 231 | 232 | %% @private 233 | to_bin(K) -> nklib_util:to_binary(K). -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NkPACKET: Generic Erlang transport layer 2 | 3 | NkPACKET is a generic transport layer for Erlang. 4 | 5 | It can be used to develop high perfomance, low latency network servers, clients and proxies. 6 | 7 | ### Features: 8 | * Support for UDP, TCP/TLS, SCTP and WS/WSS. 9 | * STUN server. 10 | * Connection-oriented (even for UDP). 11 | * DNS engine with full support for NAPTR and SRV location, including priority and weights. 12 | * URL-mapping of servers and connections. 13 | * Wrap over [Cowboy](https://github.com/ninenines/cowboy) to write domain-specific, high perfomance http servers. 14 | 15 | In order to write a server or client using NkPACKET, you must define a _protocol_, an Erlang module implementing some of the callback functions defined in [nkpacket_protocol.erl](src/nkpacket_protocol.erl). All callbacks are optional. In your protocol callback module, you can specify available transports, default ports and callback functions for your protocol. 16 | 17 | Listeners and connections can belong to a _group_, and can then be managed together. When sending packets, if a previous connection exists belonging to the same group, it will be used instead of starting a new one. 18 | 19 | 20 | ## Registering the protocol 21 | 22 | If you want to use a _scheme_ associated with your protocol (like in `my_scheme://0.0.0.0:5315;transport=wss`) you must _register_ your protocol with NkPACKET calling `nkpacket:register_protocol/2,3`. Different goups can have different protocol implementations for the same scheme: 23 | 24 | ```erlang 25 | nkpacket:register_protocol(my_scheme, my_protocol) 26 | ``` 27 | In this example, the module `my_protocol.erl` must exist. 28 | 29 | 30 | ## Writing a server 31 | 32 | After defining your callback protocol module, you can start your server calling [nkpacket:start_listener/2](src/nkpacket.erl). You must provide a `nkpacket:user_connection()` network specification, for example: 33 | 34 | ```erlang 35 | nkpacket:start_listener("my_scheme://0.0.0.0:5315;transport=wss", 36 | #{tcp_listeners=>100, idle_timeout=>5000}) 37 | ``` 38 | or 39 | ```erlang 40 | nkpacket:start_listener({my_protocol, wss, {0,0,0,0}, 5315}, 41 | #{group=>my_group, tcp_listeners=>100, idle_timeout=>5000}) 42 | ``` 43 | or even 44 | ```erlang 45 | nkpacket:start_listener(my_domain, "my_scheme://0.0.0.0:5315;transport=wss;tcp_listeners=100;idle_timeout=5000") 46 | ``` 47 | 48 | There are many available options, like setting connection timeouts, start STUN servers fot UDP, TLS parameters, maximum number of connections, etc. (See `nkpacket:listener_opts()`). The following options are allowed in urls: idle_timeout, connect_timeout, sctp_out_streams, sctp_in_streams, no_dns_cache, tcp_listeners, host, path, ws_proto, tls_certfile, tls_keyfile, tls_cacertfile, tls_password, tls_verify, tls_depth. 49 | 50 | NkPACKET will then start the indicated transport. When a new connection arrives, a new _connection process_ will be started, and the `conn_init/0` callback function in your protocol callback function will be called. 51 | Incoming data will be _parsed_ and sent to your protocol module. 52 | 53 | You can use the option `user` to pass specific metadata to the callback `init` function. If you use the url format, you can use header values, and they will generate an erlang `list()`. The following are equivalent: 54 | 55 | ```erlang 56 | nkpacket:start_listener(my_domain, "my_scheme://0.0.0.0:5315;transport=wss", 57 | #{user=>[{<<"key1">>, <<"value1">>}, <<"key2">>]}) 58 | ``` 59 | and 60 | ```erlang 61 | nkpacket:start_listener(my_domain, "my_scheme://0.0.0.0:5315;transport=wss?key1=value1&key2", 62 | #{}) 63 | ``` 64 | 65 | You can send packets over the started connection calling `nkpacket:send/2,3`. Packets will be _encoded_ calling the corresponding function in the callback module. 66 | 67 | After a configurable timeout, if no packets are sent or received, the connection is dropped. 68 | Incoming UDP packets will also (by default) generate a new _connection_, associated to that remote _ip_ and _port_. New packets to/from the same ip and port will be sent/received through the same _connection process_. You can disable this behaviour. 69 | 70 | 71 | ## Writing a client 72 | 73 | After defining the callback protocol module (if it is not already defined) you can send any packet to a remote server calling [nkpacket:send/2,3](src/nkpacket.erl), for example: 74 | 75 | ```erlang 76 | nkpacket:send("my_scheme://my_host:5315;transport=wss", my_msg) 77 | ``` 78 | (to use urls like in this example you need to register your protocol previously). 79 | 80 | 81 | After resolving `my_host` using the local DNS engine (using NAPTR and SRV if available), your message will be _encoded_ (using the corresponding callback function on your protocol callback module), and, if a connection is already started for the same _group_, _transport_, _ip_ and _port_, the packet will be sent through it. If none is available, or no group was specified, a new one will be automatically started. 82 | 83 | NkPACKET offers a sofisticated mechanism to specify destinations, with multiple fallback routes, forcing new or old connections, trying tcp after udp failure, etc. (See `nkpacket:send_opts()`). You can also force a new connection to start without sending any packet yet, calling `nkpacket:connect/2`. 84 | 85 | 86 | ## Writing a web server 87 | 88 | NkPACKET includes two _pseudo_transports_: `http` and `https`. 89 | 90 | NkPACKET registers on start the included protocol [nkpacket_protocol_http](src/nkpacket_protocol_http.erl), associating it with the schema `http` (only for _pseudo-transport http_) and the schema `https` (for _pseudo-transport https_). You can then start a server listening on this protocol and transport. 91 | 92 | NkPACKET allows several different domains to share the same web server. You must use the options `host` and/or `path` to filter and select the right domain to send the request to (see `nkpacket:listener_opts()`). You must also use `cowboy_dispatch` to process the request as an standard _Cowboy_ request. 93 | 94 | For more specific behaviours, use `cowboy_opts` instead of `cowbow_dispatch`, including any supported Cowboy middlewares and environment. 95 | 96 | You can of course register your own protocol using tranports `http` and `https` (using schemas `http` and `https` or not), implementing the callback function `http_init/3` (see [nkpacket_protocol_http](src/nkpacket_protocol_http.erl) for an example). 97 | 98 | 99 | ## Application configurarion 100 | 101 | There are several aspects of NkPACKET that can be configured globally, using standard Erlang application environment: 102 | 103 | Option|Type|Default|Comment 104 | ---|---|---|--- 105 | max_connections|`integer()`|1024|Maximum globally number of connections. 106 | dns_cache_ttl|`integer()`|30000|Time to cache DNS queries. 0 to disable cache (msecs). 107 | udp_timeout|`integer()`|30000|(msecs) 108 | tcp_timeout|`integer()`|180000|(msecs) 109 | sctp_timeout|`integer()`|180000|(msecs) 110 | ws_timeout|`integer()`|180000|(msecs) 111 | http_timeout|`integer()`|180000|(msecs) 112 | connect_timeout|`integer()`|30000|(msecs) 113 | sctp_out_streams|`integer()`|10|Default SCTP out streams 114 | sctp_in_streams|`integer()`|10|Default SCTP in streams 115 | tcp_listeners|`integer()`|10|Default number of TCP listeners 116 | main_ip|`inet:ip4_address()`|auto|Main IPv4 of the host 117 | main_ip6|`inet:ip6_address()`|auto|Main IPv6 of the host 118 | ext_ip|`inet:ip4_address()`|auto|Public Ipv4 of the host 119 | tls_certfile|`string()`|-|Custom certificate file 120 | tls_keyfile|`string()`|-|Custom key file 121 | tls_cacertfile|`string()`|-|Custom CA certificate file 122 | tls_password|`string()`|-|Password fort the certificate 123 | tls_verify|`boolean()`|false|If we must check certificate 124 | tls_depth|`integer()`|0|TLS check depth 125 | 126 | main_ip, main_ip6, if auto, are guessed from the main network cards. 127 | ext_ip, if auto, is obtained using STUN. 128 | None of them are used by nkpacket itself, but are available for client projects. 129 | 130 | 131 | NkPACKET uses [lager](https://github.com/basho/lager) for log management. 132 | 133 | NkPACKET needs Erlang >= 17 and it is tested on Linux and OSX. 134 | 135 | 136 | 137 | 138 | # Contributing 139 | 140 | Please contribute with code, bug fixes, documentation fixes, testing or any other form. Use GitHub Issues and Pull Requests, forking this repository. Please make sure all tests pass and Dialyzer is happy after your patch. 141 | 142 | -------------------------------------------------------------------------------- /test/dns_test.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2015 Carlos Gonzalez Florido. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | -module(dns_test). 22 | -author('Carlos Gonzalez '). 23 | 24 | -compile([export_all]). 25 | -compile(nowarn_export_all). 26 | -include_lib("eunit/include/eunit.hrl"). 27 | -include("nkpacket.hrl"). 28 | 29 | dns_test_() -> 30 | {setup, spawn, 31 | fun() -> 32 | ok = nkpacket_app:start(), 33 | nkpacket:register_protocol(sip, ?MODULE), 34 | nkpacket:register_protocol(sips, ?MODULE), 35 | ?debugMsg("Starting DNS test") 36 | end, 37 | fun(_) -> 38 | ok 39 | end, 40 | fun(_) -> 41 | [ 42 | fun() -> uris() end, 43 | {timeout, 60, fun() -> resolv1() end}, 44 | {timeout, 60, fun() -> resolv2() end} 45 | ] 46 | end 47 | }. 48 | 49 | 50 | start() -> 51 | nkpacket_app:start(), 52 | nkpacket:register_protocol(sip, ?MODULE), 53 | nkpacket:register_protocol(sips, ?MODULE). 54 | 55 | 56 | uris() -> 57 | Test = [ 58 | {"", {ok, [{udp, {1,2,3,4}, 5060}]}}, 59 | {"", {ok, [{tcp, {1,2,3,4}, 5060}]}}, 60 | {"", {ok, [{tls, {1,2,3,4}, 5061}]}}, 61 | {"", {ok, [{sctp, {1,2,3,4}, 5060}]}}, 62 | {"", {ok, [{ws, {1,2,3,4}, 80}]}}, 63 | {"", {ok, [{wss, {1,2,3,4}, 443}]}}, 64 | {"", {error, {invalid_transport, <<"other">>}}}, 65 | {"", {error, {invalid_transport, udp}}}, 66 | {"", {error, {invalid_transport, tcp}}}, 67 | {"", {ok, [{tls, {1,2,3,4}, 5061}]}}, 68 | {"", {error, {invalid_transport, sctp}}}, 69 | {"", {error, {invalid_transport, ws}}}, 70 | {"", {ok, [{wss, {1,2,3,4}, 443}]}}, 71 | {"", {error, {invalid_transport, <<"other">>}}}, 72 | 73 | {"", {ok, [{tcp, {1,2,3,4}, 4321}]}}, 74 | {"", {ok, [{tls, {127,0,0,1}, 4321}]}}, 75 | 76 | {"", {ok, [{udp, {1,2,3,4}, 5060}]}}, 77 | {"", {ok, [{udp, {1,2,3,4}, 4321}]}}, 78 | {"", {ok, [{tls, {1,2,3,4}, 5061}]}}, 79 | {"", {ok, [{tls, {1,2,3,4}, 4321}]}}, 80 | 81 | {"", {ok, [{udp, {127,0,0,1}, 1234}]}}, 82 | {"", {ok, [{tls, {127,0,0,1}, 1234}]}}, 83 | 84 | {"", {ok, [{udp, {0,0,0,0}, 5060}]}}, 85 | {"", {ok, [{tls, {0,0,0,0}, 5061}]}} 86 | ], 87 | lists:foreach( 88 | fun({Uri, Result}) -> 89 | Result = nkpacket_dns:resolve(Uri, #{protocol=>?MODULE}) end, 90 | Test). 91 | 92 | 93 | resolv1() -> 94 | Naptr = [ 95 | {1, 1, "s", "sips+d2t", [], "_sips._tcp.test1.local"}, 96 | {1, 2, "s", "sip+d2t", [], "_sip._tcp.test2.local"}, 97 | {2, 1, "s", "sip+d2t", [], "_sip._tcp.test3.local"}, 98 | {2, 2, "s", "sip+d2u", [], "_sip._udp.test4.local"} 99 | ], 100 | save_cache({naptr, "test.local"}, Naptr), 101 | 102 | Srvs1 = [{1, 1, {"test100.local", 100}}], 103 | save_cache({srvs, "_sips._tcp.test1.local"}, Srvs1), 104 | 105 | Srvs2 = [{1, 1, {"test200.local", 200}}, 106 | {2, 1, {"test201.local", 201}}, {2, 5, {"test202.local", 202}}, 107 | {3, 1, {"test300.local", 300}}], 108 | save_cache({srvs, "_sip._tcp.test2.local"}, Srvs2), 109 | 110 | Srvs3 = [{1, 1, {"test400.local", 400}}], 111 | save_cache({srvs, "_sip._tcp.test3.local"}, Srvs3), 112 | Srvs4 = [{1, 1, {"test500.local", 500}}], 113 | save_cache({srvs, "_sip._udp.test4.local"}, Srvs4), 114 | 115 | save_cache({ips, "test100.local"}, [{1,1,100,1}, {1,1,100,2}]), 116 | save_cache({ips, "test200.local"}, [{1,1,200,1}]), 117 | save_cache({ips, "test201.local"}, [{1,1,201,1}]), 118 | save_cache({ips, "test202.local"}, [{1,1,202,1}]), 119 | save_cache({ips, "test300.local"}, [{1,1,300,1}]), 120 | save_cache({ips, "test400.local"}, []), 121 | save_cache({ips, "test500.local"}, [{1,1,500,1}]), 122 | 123 | %% Travis test machine returns two hosts... 124 | Sip = #{protocol=>?MODULE}, 125 | {ok, [{udp, {127,0,0,1}, 5060}|_]} = nkpacket_dns:resolve("sip:localhost", Sip), 126 | {ok, [{tls, {127,0,0,1}, 5061}|_]} = nkpacket_dns:resolve("sips:localhost", Sip), 127 | 128 | {ok, [A, B, C, D, E, F, G]} = nkpacket_dns:resolve("sip:test.local", Sip), 129 | 130 | true = (A=={tls, {1,1,100,1}, 100} orelse A=={tls, {1,1,100,2}, 100}), 131 | true = (B=={tls, {1,1,100,1}, 100} orelse B=={tls, {1,1,100,2}, 100}), 132 | true = A/=B, 133 | 134 | C = {tcp, {1,1,200,1}, 200}, 135 | true = (D=={tcp, {1,1,201,1}, 201} orelse D=={tcp, {1,1,202,1}, 202}), 136 | true = (E=={tcp, {1,1,201,1}, 201} orelse E=={tcp, {1,1,202,1}, 202}), 137 | true = D/=E, 138 | 139 | F = {tcp, {1,1,300,1}, 300}, 140 | G = {udp, {1,1,500,1}, 500}, 141 | 142 | {ok, [H, I]} = nkpacket_dns:resolve("sips:test.local", Sip), 143 | true = (H=={tls, {1,1,100,1}, 100} orelse H=={tls, {1,1,100,2}, 100}), 144 | true = (I=={tls, {1,1,100,1}, 100} orelse I=={tls, {1,1,100,2}, 100}), 145 | true = H/=I, 146 | ok. 147 | 148 | 149 | resolv2() -> 150 | save_cache( 151 | {naptr,"sip2sip.info"}, 152 | [ 153 | {5,100,"s","sips+d2t",[],"_sips._tcp.sip2sip.info"}, 154 | {10,100,"s","sip+d2t",[],"_sip._tcp.sip2sip.info"}, 155 | {30,100,"s","sip+d2u",[],"_sip._udp.sip2sip.info"} 156 | ]), 157 | save_cache( 158 | {srvs,"_sips._tcp.sip2sip.info"}, 159 | [{100,100,{"proxy.sipthor.net",443}}]), 160 | save_cache( 161 | {srvs,"_sip._tcp.sip2sip.info"}, 162 | [{100,100,{"proxy.sipthor.net",5060}}]), 163 | save_cache( 164 | {srvs,"_sip._udp.sip2sip.info"}, 165 | [{100,100,{"proxy.sipthor.net",5060}}]), 166 | save_cache( 167 | {ips,"proxy.sipthor.net"}, 168 | [{81,23,228,129},{85,17,186,7},{81,23,228,150}]), 169 | 170 | {ok, [ 171 | {tls, Ip1, 443}, 172 | {tls, Ip2, 443}, 173 | {tls, Ip3, 443}, 174 | {tcp, Ip4, 5060}, 175 | {tcp, Ip5, 5060}, 176 | {tcp, Ip6, 5060}, 177 | {udp, Ip7, 5060}, 178 | {udp, Ip8, 5060}, 179 | {udp, Ip9, 5060} 180 | ]} = 181 | nkpacket_dns:resolve("sip:sip2sip.info", #{protocol=>?MODULE}), 182 | 183 | Ips = lists:sort([{81,23,228,129},{85,17,186,7},{81,23,228,150}]), 184 | Ips = lists:sort([Ip1, Ip2, Ip3]), 185 | Ips = lists:sort([Ip4, Ip5, Ip6]), 186 | Ips = lists:sort([Ip7, Ip8, Ip9]), 187 | 188 | {ok, [{udp, _, 0}|_]} = 189 | nkpacket_dns:resolve("sip:sip2sip.info", 190 | #{protocol=>?MODULE, resolve_type=>listen}), 191 | ok. 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | %% Protocol callbacks 200 | 201 | transports(sip) -> [udp, tcp, tls, sctp, ws, wss]; 202 | transports(sips) -> [tls, wss]. 203 | 204 | default_port(udp) -> 5060; 205 | default_port(tcp) -> 5060; 206 | default_port(tls) -> 5061; 207 | default_port(sctp) -> 5060; 208 | default_port(ws) -> 80; 209 | default_port(wss) -> 443; 210 | default_port(_) -> invalid. 211 | 212 | naptr(sip, "sips+d2t") -> {ok, tls}; 213 | naptr(sip, "sip+d2u") -> {ok, udp}; 214 | naptr(sip, "sip+d2t") -> {ok, tcp}; 215 | naptr(sip, "sip+d2s") -> {ok, sctp}; 216 | naptr(sip, "sips+d2w") -> {ok, wss}; 217 | naptr(sip, "sip+d2w") -> {ok, ws}; 218 | naptr(sips, "sips+d2t") -> {ok, tls}; 219 | naptr(sips, "sips+d2w") -> {ok, wss}; 220 | naptr(_, _) -> invalid. 221 | 222 | 223 | 224 | 225 | %% Util 226 | 227 | % resolve(Uri) -> 228 | % case nkpacket:resolve(Uri) of 229 | % {ok, List, _} -> List; 230 | % {error, Error} -> {error, Error} 231 | % end. 232 | 233 | 234 | save_cache(Key, Value) -> 235 | Now = nklib_util:timestamp(), 236 | true = ets:insert(nkpacket_dns, {Key, Value, Now+10}). 237 | 238 | 239 | 240 | -------------------------------------------------------------------------------- /test/http_test.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2015 Carlos Gonzalez Florido. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | -module(http_test). 22 | -author('Carlos Gonzalez '). 23 | 24 | -compile([export_all]). 25 | -compile(nowarn_export_all). 26 | -include_lib("eunit/include/eunit.hrl"). 27 | -include_lib("nklib/include/nklib.hrl"). 28 | -include_lib("kernel/include/file.hrl"). 29 | -include("nkpacket.hrl"). 30 | 31 | http_test_() -> 32 | {setup, spawn, 33 | fun() -> 34 | ok = nkpacket_app:start(), 35 | ?debugMsg("Starting HTTP test") 36 | end, 37 | fun(_) -> 38 | ok 39 | end, 40 | fun(_) -> 41 | [ 42 | fun() -> basic() end, 43 | fun() -> https() end, 44 | fun() -> static() end 45 | ] 46 | end 47 | }. 48 | 49 | 50 | basic() -> 51 | Port = test_util:get_port(tcp), 52 | {Ref1, M1, Ref2, M2, Ref3, M3} = test_util:reset_3(), 53 | 54 | Url1 = "", 55 | {ok, _, Http1} = nkpacket:start_listener(Url1, M1#{protocol=>test_protocol, class=>dom1}), 56 | 57 | Url2 = "", 58 | {ok, _, Http2} = nkpacket:start_listener(Url2, M2#{protocol=>test_protocol, class=>dom2}), 59 | 60 | Url3 = "", 61 | {ok, _, Http3} = nkpacket:start_listener(Url3, M3#{ 62 | protocol => test_protocol, 63 | class => dom3, 64 | host => "localhost", 65 | path => "/test3/a" 66 | }), 67 | timer:sleep(100), 68 | 69 | [Listen1] = nkpacket:get_class_ids(dom1), 70 | {ok, #nkport{ 71 | class = dom1, 72 | transp = http, 73 | local_ip = {0,0,0,0}, local_port= Port, 74 | listen_ip= {0,0,0,0}, listen_port= Port, 75 | protocol = test_protocol, pid=Http1, socket= CowPid, 76 | opts = #{path := <<"/test1">>} 77 | }} = nkpacket:get_nkport(Listen1), 78 | [Listen2] = nkpacket:get_class_ids(dom2), 79 | {ok, #nkport{ 80 | class = dom2, 81 | transp = http, 82 | local_ip = {0,0,0,0}, local_port= Port, 83 | listen_ip= {0,0,0,0}, listen_port= Port, 84 | pid = Http2, socket= CowPid, 85 | opts = #{path := <<"/test2">>} 86 | }} = nkpacket:get_nkport(Listen2), 87 | [Listen3] = nkpacket:get_class_ids(dom3), 88 | {ok, #nkport{ 89 | class = dom3, 90 | transp = http, 91 | local_ip = {0,0,0,0}, local_port= Port, 92 | listen_ip= {0,0,0,0}, listen_port= Port, 93 | pid = Http3, socket= CowPid, 94 | opts = #{ 95 | host := <<"localhost">>, 96 | path := <<"/test3/a">> 97 | } 98 | }} = nkpacket:get_nkport(Listen3), 99 | 100 | Gun = open(Port, tcp), 101 | {ok, 404, H1} = get(Gun, "/", []), 102 | <<"NkPACKET">> = nklib_util:get_value(<<"server">>, H1), 103 | 104 | {ok, 200, _, <<"Hello World!">>} = get(Gun, "/test1", []), 105 | receive {Ref1, http_init, P1} -> P1 after 1000 -> error(?LINE) end, 106 | 107 | {ok, 200, _, <<"Hello World!">>} = get(Gun, "/test2", []), 108 | receive {Ref2, http_init, P3} -> P3 after 1000 -> error(?LINE) end, 109 | 110 | {ok, 404, _} = get(Gun, "/test3", []), 111 | {ok, 404, _} = get(Gun, "/test3/a/1", []), 112 | {ok, 404, _} = get(Gun, "/test3", [{<<"host">>, <<"localhost">>}]), 113 | {ok, 200, _, _} = get(Gun, "/test3/a/1", [{<<"host">>, <<"localhost">>}]), 114 | receive {Ref3, http_init, P5} -> P5 after 1000 -> error(?LINE) end, 115 | 116 | 117 | % If we close the transport, NkPacket blocks access, but only for the next 118 | % connection 119 | ok = nkpacket:stop_listeners(Http3), 120 | timer:sleep(100), 121 | Gun2 = open(Port, tcp), 122 | {ok, 404, H4} = get(Gun2, "/test3/a/1", [{<<"host">>, <<"localhost">>}]), 123 | <<"NkPACKET">> = nklib_util:get_value(<<"server">>, H4), 124 | 125 | ok = nkpacket:stop_listeners(Http1), 126 | ok = nkpacket:stop_listeners(Http2), 127 | ok. 128 | 129 | 130 | https() -> 131 | Port = test_util:get_port(tcp), 132 | {Ref1, M1} = test_util:reset_1(), 133 | Url1 = "", 134 | {ok, _, Http1} = nkpacket:start_listener(Url1, M1#{class=>dom4, protocol=>test_protocol}), 135 | 136 | Gun = open(Port, ssl), 137 | {ok, 200, _, <<"Hello World!">>} = get(Gun, "/test1", []), 138 | receive {Ref1, http_init, P1} -> P1 after 1000 -> error(?LINE) end, 139 | {ok, 404, H1} = get(Gun, "/test2", []), 140 | <<"NkPACKET">> = nklib_util:get_value(<<"server">>, H1), 141 | ok = nkpacket:stop_listeners(Http1). 142 | 143 | 144 | static() -> 145 | % nkpacket:stop_all(static), 146 | % timer:sleep(100), 147 | Port = 8123, %test_util:get_port(tcp), 148 | 149 | Url1 = "http://all:"++integer_to_list(Port)++"/", 150 | {ok, _, S1} = nkpacket:start_listener(Url1, #{class=>dom5, protocol=>test_protocol}), 151 | 152 | Url2 = "http://all:"++integer_to_list(Port)++"/1/2/", 153 | {ok, _, S2} = nkpacket:start_listener(Url2, #{class=>dom5, protocol=>test_protocol}), 154 | 155 | 156 | 157 | Gun = open(Port, tcp), 158 | 159 | Path = filename:join(code:priv_dir(nkpacket), "www"), 160 | {ok, 301, Hds1} = get(Gun, "/", []), 161 | <<"http://127.0.0.1:8123/index.html">> = nklib_util:get_value(<<"location">>, Hds1), 162 | {ok, 200, H1, <<">} = get(Gun, "/index.html", []), 163 | % Cowboy now only returns connection when necessary 164 | [ 165 | % {<<"connection">>, <<"keep-alive">>}, 166 | {<<"accept-ranges">>,<<"bytes">>}, 167 | {<<"content-length">>, <<"211">>}, 168 | {<<"content-type">>,<<"text/html">>}, 169 | {<<"date">>, _}, 170 | {<<"etag">>, Etag}, 171 | {<<"last-modified">>, Date}, 172 | {<<"server">>, <<"NkPACKET">>} 173 | ] = lists:sort(H1), 174 | File1 = filename:join(Path, "index.html"), 175 | {ok, #file_info{mtime=Mtime, size=Size}} = file:read_file_info(File1, [{time, universal}]), 176 | Etag = <<$", (integer_to_binary(erlang:phash2({Size, Mtime}, 16#ffffffff)))/binary, $">>, 177 | Date = cowboy_clock:rfc1123(Mtime), 178 | 179 | %{ok, 400, _} = get(Gun, "../..", []), 180 | 181 | {ok, 403, _} = get(Gun, "/1/2/", []), 182 | {ok, 200, _, <<">} = get(Gun, "/1/2/index.html", []), 183 | {ok, 404, _} = get(Gun, "/1/2/index.htm", []), 184 | {ok, 200, H3, <<"file1.txt">>} = get(Gun, "/dir1/file1.txt", []), 185 | {ok, 200, H3, <<"file1.txt">>} = get(Gun, "/dir1/././file1.txt", []), 186 | lager:warning("Next warning about unathorized access is expected"), 187 | {ok, 400, _} = get(Gun, "/dir1/../../file1.txt", []), 188 | {ok, 200, H3, <<"file1.txt">>} = get(Gun, "/dir1/../dir1/file1.txt", []), 189 | [ 190 | %{<<"connection">>, <<"keep-alive">>}, 191 | {<<"accept-ranges">>,<<"bytes">>}, 192 | {<<"content-length">>, <<"9">>}, 193 | {<<"content-type">>, <<"application/octet-stream">>}, 194 | {<<"date">>, _}, 195 | {<<"etag">>, _}, 196 | {<<"last-modified">>, _}, 197 | {<<"server">>,<<"NkPACKET">>} 198 | ] = lists:sort(H3), 199 | {ok, 200, H3, <<"file1.txt">>} = get(Gun, "/1/2/dir1/file1.txt", []), 200 | % 201 | 202 | ok = nkpacket:stop_listeners(S1), 203 | 204 | timer:sleep(100), 205 | 206 | Gun2 = open(Port, tcp), 207 | {ok, 404, _} = get(Gun2, "/dir1/file1.txt", []), 208 | {ok, 200, _, <<"file1.txt">>} = get(Gun2, "/1/2/dir1/file1.txt", []), 209 | 210 | Url3 = "http://all:"++integer_to_list(Port)++"/1/2/", 211 | {ok, _, S3} = nkpacket:start_listener(Url3, 212 | #{class=>dom5, protocol=>test_protocol, path=>"/a/", host=>"localhost"}), 213 | {ok, 404, _} = get(Gun2, "/a/index.html", []), 214 | {ok, 200, _, _} = get(Gun2, "/a/index.html", [{<<"host">>, <<"localhost">>}]), 215 | {ok, 404, _} = get(Gun2, "/c/index.html", [{<<"host">>, <<"localhost">>}]), 216 | 217 | % Gun3 = open(Port, tcp), 218 | % {ok, 200, _, _} = get(Gun3, "/a/index.html", []), 219 | % {ok, 404, _} = get(Gun3, "/c/index.html", []), 220 | 221 | ok = nkpacket:stop_listeners(S2), 222 | ok = nkpacket:stop_listeners(S3), 223 | ok. 224 | 225 | 226 | 227 | %%%%%%%% Util 228 | 229 | open(Port, Transp) -> 230 | {ok, Gun} = gun:open("127.0.0.1", Port, #{transport=>Transp, retry=>0}), 231 | Gun. 232 | 233 | 234 | get(Pid, Path, Hds) -> 235 | % Add [{<<"connection">>, <<"close">>}] to check Keep Alive 236 | Ref = gun:get(Pid, Path, Hds), 237 | receive 238 | {gun_response, _, Ref, fin, Code, RHds} -> 239 | {ok, Code, RHds}; 240 | {gun_response, _, Ref, nofin, Code, RHds} -> 241 | receive 242 | {gun_data, _, Ref, fin, Data} -> 243 | {ok, Code, RHds, Data} 244 | after 1000 -> 245 | timeout 246 | end; 247 | {gun_error, _, Ref, Error} -> 248 | {error, Error} 249 | after 1000 -> 250 | {error, timeout} 251 | end. 252 | 253 | 254 | 255 | -------------------------------------------------------------------------------- /test/udp_test.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2015 Carlos Gonzalez Florido. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | -module(udp_test). 22 | -author('Carlos Gonzalez '). 23 | 24 | -compile([export_all]). 25 | -compile(nowarn_export_all). 26 | -include_lib("eunit/include/eunit.hrl"). 27 | -include("nkpacket.hrl"). 28 | 29 | udp_test_() -> 30 | {setup, spawn, 31 | fun() -> 32 | ok = nkpacket_app:start(), 33 | ?debugMsg("Starting UDP test") 34 | end, 35 | fun(_) -> 36 | ok 37 | end, 38 | fun(_) -> 39 | [ 40 | fun() -> basic() end, 41 | fun() -> listen() end, 42 | fun() -> stun() end 43 | ] 44 | end 45 | }. 46 | 47 | 48 | basic() -> 49 | _ = test_util:reset_2(), 50 | Conn1 = #nkconn{protocol=test_protocol, transp=udp, ip={0,0,0,0}, port=0}, 51 | % First '0' port try to open default transport port (1234) 52 | {ok, _, UdpP1} = nkpacket:start_listener(Conn1, #{}), % No class 53 | {ok, {_, udp, {0,0,0,0}, Port1}} = nkpacket:get_local(UdpP1), 54 | case Port1 of 55 | 1234 -> ok; 56 | _ -> lager:warning("Could not open port 1234") 57 | end, 58 | [{Listen1, _, _}] = nkpacket:get_all(), 59 | [Listen1] = nkpacket:get_class_ids(none), 60 | [] = nkpacket:get_class_ids(dom1), 61 | {ok, #nkport{ 62 | transp = udp, 63 | local_ip = {0,0,0,0}, local_port = Port1, 64 | remote_ip = undefined, remote_port = undefined, 65 | listen_ip = {0,0,0,0}, listen_port = Port1, 66 | protocol = test_protocol, pid = UdpP1 67 | }} = nkpacket:get_nkport(Listen1), 68 | 69 | % Since '1234' is not available, a random one is used 70 | % (Oops, in linux it allows to open it again, the old do not receive any more packets!) 71 | Port2 = test_util:get_port(udp), 72 | Conn = #nkconn{protocol=test_protocol, transp=udp, ip={0,0,0,0}, port=Port2}, 73 | {ok, _, LisA} = nkpacket:start_listener(Conn, 74 | #{class=>dom2, udp_starts_tcp=>true, 75 | tcp_listeners=>1}), 76 | timer:sleep(100), 77 | [ 78 | #nkport{transp=tcp, local_port=Port2, pid=ConnA}, 79 | #nkport{transp=udp, local_port=Port2, pid=LisA} 80 | ] = test_util:listeners(dom2), 81 | 82 | lager:warning("Some processes will be killed now..."), 83 | % Should also work with kill 84 | % exit(ConnA, kill), 85 | exit(ConnA, forced_stop), 86 | timer:sleep(2000), 87 | [ 88 | #nkport{transp=tcp, local_port=Port3, pid=LisB}, 89 | #nkport{transp=udp, local_port=Port3, pid=ConnB} 90 | ] = test_util:listeners(dom2), 91 | 92 | % In Linux, using {reuseaddr, true} results in the same ports being assigned! 93 | % true = Port3/=Port2, 94 | true = LisB/=ConnA, 95 | true = ConnB/=LisA, 96 | 97 | % exit(ConnB, kill), 98 | exit(ConnB, forced_stop), 99 | timer:sleep(2000), % We need this for Linux, it tries to use the same port, sometimes 100 | % it has to retry 101 | 102 | [ 103 | #nkport{transp=tcp, local_port=Port4, pid=LisC}, 104 | #nkport{transp=udp, local_port=Port4, pid=ConnC} 105 | ] = test_util:listeners(dom2), 106 | 107 | % true = Port4/=Port3, 108 | true = LisC/=LisB, 109 | true = ConnC/=ConnB, 110 | ok = nkpacket:stop_all(none), 111 | ok = nkpacket:stop_all(dom2), 112 | timer:sleep(500), 113 | [] = nkpacket:get_class_ids(none), 114 | [] = nkpacket:get_class_ids(dom2), 115 | [] = nkpacket:get_all(), 116 | ok. 117 | 118 | 119 | listen() -> 120 | Port1 = test_util:get_port(udp), 121 | {Ref1, M1, Ref2, M2} = test_util:reset_2(), 122 | {ok, _, Udp1} = nkpacket:start_listener( 123 | "", 124 | M1#{class=><<"dom1">>}), 125 | receive {Ref1, listen_init} -> ok after 1000 -> error(?LINE) end, 126 | 127 | {ok, Socket} = gen_udp:open(0, [binary, {active, false}]), 128 | {ok, {{0,0,0,0}, LocalPort}} = inet:sockname(Socket), 129 | ok = gen_udp:send(Socket, {127,0,0,1}, Port1, erlang:term_to_binary(<<"test1">>)), 130 | receive {Ref1, conn_init} -> ok after 1000 -> error(?LINE) end, 131 | receive {Ref1, {parse, <<"test1">>}} -> ok after 1000 -> error(?LINE) end, 132 | 133 | [#nkport{local_ip={0,0,0,0}, local_port=Port1, remote_ip=undefined, 134 | remote_port=undefined, pid=Udp1, socket=UdpS1} = Listen] = 135 | test_util:listeners(<<"dom1">>), 136 | 137 | [#nkport{local_ip={0,0,0,0}, local_port=Port1, remote_ip={127,0,0,1}, 138 | remote_port=LocalPort, socket=UdpS1, pid=_Conn1Pid} = Conn1] = 139 | test_util:conns(<<"dom1">>), 140 | 141 | % Send a message back, directly through the connection 142 | ok = nkpacket_connection:send(Conn1, <<"test2">>), 143 | receive {Ref1, {encode, <<"test2">>}} -> ok after 1000 -> error(?LINE) end, 144 | {ok, {{127,0,0,1}, Port1, Raw1}} = gen_udp:recv(Socket, 0, 5000), 145 | <<"test2">> = binary_to_term(Raw1), 146 | 147 | % Send a message directly to the raw connection 148 | ok = nkpacket_connection_lib:raw_send(Conn1, <<"test3">>), 149 | {ok, {{127,0,0,1}, Port1, <<"test3">>}} = gen_udp:recv(Socket, 0, 5000), 150 | 151 | % Send a message directly from the listening process 152 | ok = nkpacket_transport_udp:send(Listen, {127,0,0,1}, LocalPort, <<"test4">>), 153 | % We use the parse in test_protocol:listen_parse 154 | {ok, {{127,0,0,1}, Port1, <<"test4">>}} = gen_udp:recv(Socket, 0, 5000), 155 | 156 | [Conn1Pid] = 157 | nkpacket_transport:get_connected(#nkconn{protocol=test_protocol, transp=udp, ip={127,0,0,1}, port=LocalPort, 158 | opts=#{class=><<"dom1">>}}), 159 | [{_, Conn1Pid}] = nkpacket_connection:get_all_class(<<"dom1">>), 160 | ok = nkpacket_connection:stop(Conn1Pid, normal), 161 | receive {Ref1, conn_stop} -> ok after 1000 -> error(?LINE) end, 162 | timer:sleep(50), 163 | [] = nkpacket_transport:get_connected(#nkconn{protocol=test_protocol, transp=udp, ip={127,0,0,1}, port=LocalPort, 164 | opts=#{class=><<"dom1">>}}), 165 | [] = nkpacket_connection:get_all_class(<<"dom1">>), 166 | 167 | ok = nkpacket:stop_listeners(Udp1), 168 | receive {Ref1, listen_stop} -> ok after 1000 -> error(?LINE) end, 169 | timer:sleep(50), 170 | [] = nkpacket:get_class_ids(<<"dom1">>), 171 | [] = nkpacket:get_all(), 172 | 173 | % Now testing UDP without creating connections 174 | {ok, _, Udp2} = nkpacket:start_listener(";class=dom1", 175 | M2#{udp_no_connections=>true}), 176 | receive {Ref2, listen_init} -> ok after 1000 -> error(?LINE) end, 177 | {ok, {_, _, _, 1234}} = nkpacket:get_local(Udp2), 178 | ok = gen_udp:send(Socket, {127,0,0,1}, 1234, <<"test5">>), 179 | receive {Ref2, {listen_parse, <<"test5">>}} -> ok after 1000 -> error(?LINE) end, 180 | 181 | [] = nkpacket_transport:get_connected(#nkconn{protocol=test_protocol, transp=udp, ip={127,0,0,1}, port=LocalPort, 182 | opts=#{class=><<"dom1">>}}), 183 | 184 | [] = nkpacket_connection:get_all_class(<<"dom1">>), 185 | ok = nkpacket:stop_listeners(Udp2), 186 | receive {Ref2, listen_stop} -> ok after 1000 -> error(?LINE) end, 187 | timer:sleep(50), 188 | [] = nkpacket:get_class_ids(<<"dom1">>), 189 | test_util:ensure([Ref1, Ref2]), 190 | ok. 191 | 192 | stun() -> 193 | Port1 = test_util:get_port(udp), 194 | {Ref1, M1} = test_util:reset_1(), 195 | ok = nkpacket:register_protocol(test, test_protocol), 196 | {ok, _, Udp1} = nkpacket:start_listener( 197 | "", 198 | M1#{udp_stun_reply=>true, udp_no_connections=>true}), 199 | receive {Ref1, listen_init} -> ok after 1000 -> error(?LINE) end, 200 | {ok, Socket} = gen_udp:open(0, [binary, {active, false}]), 201 | {ok, {{0,0,0,0}, LocalPort}} = inet:sockname(Socket), 202 | {Id, Request} = nkpacket_stun:binding_request(), 203 | 204 | % We send a STUN request to our server, it replies 205 | ok = gen_udp:send(Socket, {127,0,0,1}, Port1, Request), 206 | 207 | {ok, {_, _, Raw}} = gen_udp:recv(Socket, 0, 5000), 208 | {response, binding, Id, Data} = nkpacket_stun:decode(Raw), 209 | {{127,0,0,1}, LocalPort} = nklib_util:get_value(xor_mapped_address, Data), 210 | 211 | % We start a second listener that does not reply to STUNS 212 | {ok, _, Udp2} = nkpacket:start_listener("", 213 | M1#{udp_no_connections=>true}), 214 | receive {Ref1, listen_init} -> ok after 1000 -> error(?LINE) end, 215 | ok = gen_udp:send(Socket, {127,0,0,1}, 20001, Request), 216 | receive {Ref1, {listen_parse, <<0, 1, _/binary>>}} -> ok after 1000 -> error(?LINE) end, 217 | 218 | % But we can use it to send STUNS to our first server 219 | {ok, {127,0,0,1}, 20001} = 220 | nkpacket_transport_udp:send_stun_sync(Udp2, {127,0,0,1}, Port1, 5000), 221 | ok = nkpacket:stop_listeners(Udp1), 222 | ok = nkpacket:stop_listeners(Udp2), 223 | receive {Ref1, listen_stop} -> ok after 1000 -> error(?LINE) end, 224 | receive {Ref1, listen_stop} -> ok after 1000 -> error(?LINE) end, 225 | test_util:ensure([Ref1]). 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | -------------------------------------------------------------------------------- /src/nkpacket_resolve.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2019 Carlos Gonzalez Florido. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %% @doc Resolver 22 | %% 23 | %% - An 'id' will always be added to returned conn if not present 24 | %% 25 | %% - If it is a #nkconn{}, {connect, #nkconn{}} or {current, #nkconn{}}: 26 | %% - call options are merged with nkconn's options, 27 | %% and also with protocol's options if resolve_opts/0 is exported. 28 | %% - options a are parsed and added to nkconn's options. 29 | %% 30 | %% - If it is an #uri{}: 31 | %% - a protocol is found: 32 | %% - if 'protocol' present in options, that's is 33 | %% - If not, but http or https, is nkpacket_httpc_protocol 34 | %% - If 'class' is present in options, nkpacket:get_protocol(Class, Scheme) is called 35 | %% - If 'schemes' is present, protocol is extracted from it if present 36 | %% - If nothing works, nkpacket:get_protocol(Scheme) is called 37 | %% - protocols' resolve_opts/0 is called to get additional options 38 | %% - uri's options are processed: 39 | %% - 'host' is added if not standard 40 | %% - 'user' and 'pass' are added 41 | %% - 'headers' is added 42 | %% - uri's options and parameter options are parsed and merged 43 | %% - nkpacket_dns:resolve(Uri, Opts) is called with the protocol to get #nkconn{}'s 44 | %% 45 | %% - Pid's and #nkport's are only allowed if resolve_type = send 46 | %% 47 | %% - User uris are parsed and, if an #uri{} is found, it tries again 48 | 49 | 50 | -module(nkpacket_resolve). 51 | -author('Carlos Gonzalez '). 52 | 53 | -export([resolve/1, resolve/2, check_syntax/2]). 54 | 55 | -include_lib("nklib/include/nklib.hrl"). 56 | -include("nkpacket.hrl"). 57 | 58 | 59 | %% @private 60 | -spec resolve(nkpacket:send_spec()|[nkpacket:send_spec()]) -> 61 | {ok, [nkpacket:send_spec()]} | {error, term()}. 62 | 63 | 64 | resolve(Any) -> 65 | resolve(Any, #{}). 66 | 67 | 68 | %% @private 69 | -spec resolve([nkpacket:send_spec()], nkpacket:resolve_opts()) -> 70 | {ok, [nkpacket:send_spec()]} | {error, term()}. 71 | 72 | resolve([], _Opts) -> 73 | {ok, []}; 74 | 75 | resolve(List, Opts) when is_list(List), not is_integer(hd(List)) -> 76 | resolve(List, Opts, []); 77 | 78 | resolve(Other, Opts) -> 79 | resolve([Other], Opts, []). 80 | 81 | 82 | %% @private 83 | resolve([], _Opts, Acc) -> 84 | {ok, Acc}; 85 | 86 | resolve([#nkconn{}=Conn|Rest], Opts, Acc) -> 87 | case do_resolve_nkconn(Conn, Opts) of 88 | {ok, Conn2} -> 89 | resolve(Rest, Opts, [Conn2|Acc]); 90 | {error, Error} -> 91 | {error, Error} 92 | end; 93 | 94 | resolve([{connect, #nkconn{}=Conn}|Rest], #{resolve_type:=send}=Opts, Acc) -> 95 | case do_resolve_nkconn(Conn, Opts) of 96 | {ok, Conn2} -> 97 | resolve(Rest, Opts, [{connect, Conn2}|Acc]); 98 | {error, Error} -> 99 | {error, Error} 100 | end; 101 | 102 | resolve([{current, #nkconn{}=Conn}|Rest], #{resolve_type:=send}=Opts, Acc) -> 103 | case do_resolve_nkconn(Conn, Opts) of 104 | {ok, Conn2} -> 105 | resolve(Rest, Opts, [{current ,Conn2}|Acc]); 106 | {error, Error} -> 107 | {error, Error} 108 | end; 109 | 110 | resolve([#uri{}=Uri|Rest], Opts, Acc) -> 111 | case do_resolve_uri(Uri, Opts) of 112 | {ok, Conns} -> 113 | resolve(Rest, Opts, Acc++Conns); 114 | {error, Error} -> 115 | {error, Error} 116 | end; 117 | 118 | resolve([Pid|Rest], #{resolve_type:=send}=Opts, Acc) when is_pid(Pid) -> 119 | resolve(Rest, Opts, Acc++[Pid]); 120 | 121 | resolve([#nkport{}=Port|Rest], #{resolve_type:=send}=Opts, Acc) -> 122 | resolve(Rest, Opts, Acc++[Port]); 123 | 124 | resolve([Uri|Rest], Opts, Acc) -> 125 | case nklib_parse:uris(Uri) of 126 | error -> 127 | {error, {invalid_uri, Uri}}; 128 | Parsed -> 129 | resolve(Parsed++Rest, Opts, Acc) 130 | end. 131 | 132 | 133 | %% @private 134 | do_resolve_nkconn(#nkconn{protocol=Protocol, opts=Opts0} = Conn, Opts) -> 135 | Opts2 = case erlang:function_exported(Protocol, resolve_opts, 0) of 136 | true -> 137 | ProtocolOpts = Protocol:resolve_opts(), 138 | maps:merge(ProtocolOpts, Opts); 139 | false -> 140 | Opts 141 | end, 142 | Opts3 = maps:merge(Opts0, Opts2), 143 | case nkpacket_util:parse_opts(Opts3) of 144 | {ok, Opts4} -> 145 | Conn2 = Conn#nkconn{opts=Opts4}, 146 | Conn3 = gen_external_url(Conn2), 147 | {ok, Conn3}; 148 | {error, Error} -> 149 | {error, Error} 150 | end. 151 | 152 | 153 | %% @private 154 | %% Generates key "external_url" if not yet present 155 | gen_external_url(#nkconn{transp=Transp}=Conn) when Transp==http; Transp==https -> 156 | #nkconn{ip=Ip, port=Port, opts=Opts} = Conn, 157 | case maps:get(external_url, Opts, <<>>) of 158 | <<>> -> 159 | ExtUrl1 = list_to_binary([ 160 | nklib_util:to_binary(Transp), "://", 161 | case nklib_util:to_host(Ip) of 162 | <<"0.0.0.0">> -> 163 | <<"127.0.0.1">>; 164 | IpHost -> 165 | IpHost 166 | end, 167 | case Port of 168 | 80 when Transp == http -> 169 | <<>>; 170 | 443 when Transp == https -> 171 | <<>>; 172 | MyPort -> 173 | [":", integer_to_binary(MyPort)] 174 | end, 175 | maps:get(path, Opts, "/") 176 | ]), 177 | ExtUrl2 = nklib_url:norm(ExtUrl1), 178 | Conn#nkconn{opts=Opts#{external_url=>ExtUrl2}}; 179 | _ -> 180 | Conn 181 | end; 182 | 183 | gen_external_url(Conn) -> 184 | Conn. 185 | 186 | 187 | %% @private 188 | do_resolve_uri(Uri, Opts) -> 189 | #uri{ 190 | scheme = Scheme, 191 | user = User, 192 | pass = Pass, 193 | domain = Host, 194 | path = Path, 195 | ext_opts = UriOpts, 196 | ext_headers = Headers 197 | } = Uri, 198 | Protocol = case Opts of 199 | #{protocol:=UserProtocol} -> 200 | UserProtocol; 201 | _ when Scheme==http; Scheme==https -> 202 | nkpacket_httpc_protocol; 203 | #{class:=Class} -> 204 | nkpacket:get_protocol(Class, Scheme); 205 | #{schemes:=Schemes} -> 206 | case maps:find(Scheme, Schemes) of 207 | {ok, SchemeProto} -> 208 | SchemeProto; 209 | error -> 210 | nkpacket:get_protocol(Scheme) 211 | end; 212 | _ -> 213 | nkpacket:get_protocol(Scheme) 214 | end, 215 | Opts2 = case erlang:function_exported(Protocol, resolve_opts, 0) of 216 | true -> 217 | ProtocolOpts = Protocol:resolve_opts(), 218 | maps:merge(ProtocolOpts, Opts); 219 | false -> 220 | Opts 221 | end, 222 | UriOpts1 = [{nklib_parse:unquote(K), nklib_parse:unquote(V)} || {K, V} <- UriOpts], 223 | % Let's see if we want to listen or connect to a specific host 224 | UriOpts2 = case Host of 225 | <<"0.0.0.0">> -> 226 | UriOpts1; 227 | <<"0:0:0:0:0:0:0:0">> -> 228 | UriOpts1; 229 | <<"::0">> -> 230 | UriOpts1; 231 | <<"all">> -> 232 | UriOpts1; 233 | <<"node">> -> 234 | UriOpts1; 235 | _ -> 236 | [{host, Host}|UriOpts1] % Host to listen on for WS/HTTP 237 | end, 238 | UriOpts3 = case User of 239 | <<>> -> 240 | UriOpts2; 241 | _ -> 242 | case Pass of 243 | <<>> -> 244 | [{user, User}|UriOpts2]; 245 | _ -> 246 | [{user, User}, {password, Pass}|UriOpts2] 247 | end 248 | end, 249 | UriOpts4 = case Path of 250 | <<>> -> 251 | UriOpts3; 252 | _ -> 253 | [{path, Path}|UriOpts3] % Path to listen on for WS/HTTP 254 | end, 255 | UriOpts5 = case Headers of 256 | [] -> 257 | UriOpts4; 258 | _ -> 259 | [{user, Headers}|UriOpts4] % TODO Is this right? 260 | end, 261 | try 262 | % Opts is used here only for parse_syntax 263 | UriOpts6 = case nkpacket_util:parse_uri_opts(UriOpts5, Opts) of 264 | {ok, ParsedUriOpts} -> 265 | ParsedUriOpts; 266 | {error, Error1} -> 267 | throw(Error1) 268 | end, 269 | Opts3 = case nkpacket_util:parse_opts(Opts2) of 270 | {ok, CoreOpts} -> 271 | maps:merge(UriOpts6, CoreOpts); 272 | {error, Error2} -> 273 | throw(Error2) 274 | end, 275 | % Now we have all the options, from the uri and the supplied options 276 | Opts4 = maps:without([resolve_type, protocol], Opts3), 277 | case nkpacket_dns:resolve(Uri, Opts3#{protocol=>Protocol}) of 278 | {ok, Addrs} -> 279 | Conns = lists:map( 280 | fun({Transp, Addr, Port}) -> 281 | Conn = #nkconn{protocol=Protocol, transp=Transp, ip=Addr, port=Port, opts=Opts4}, 282 | gen_external_url(Conn) 283 | end, 284 | Addrs), 285 | {ok, Conns}; 286 | {error, Error} -> 287 | {error, Error} 288 | end 289 | catch 290 | throw:Throw -> {error, Throw} 291 | end. 292 | 293 | 294 | %% @doc 295 | check_syntax(Protocol, Url) -> 296 | case resolve(Url, #{protocol=>Protocol}) of 297 | {ok, _Conns} -> 298 | ok; 299 | {error, _Error} -> 300 | error 301 | end. 302 | 303 | 304 | 305 | -------------------------------------------------------------------------------- /src/nkpacket_connection_ws.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% Heavily based on gun_http.erl and gun_ws.erl 3 | %% Copyright (c) 2016, Loïc Hoguin 4 | %% 5 | %% Permission to use, copy, modify, and/or distribute this software for any 6 | %% purpose with or without fee is hereby granted, provided that the above 7 | %% copyright notice and this permission notice appear in all copies. 8 | %% 9 | %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | %% ------------------------------------------------------------------- 17 | 18 | %% @doc WS Connection Library Functions 19 | -module(nkpacket_connection_ws). 20 | -author('Carlos Gonzalez '). 21 | 22 | -export([start_handshake/1, init/1, handle/2, encode/1]). 23 | 24 | -include_lib("nklib/include/nklib.hrl"). 25 | -include("nkpacket.hrl"). 26 | 27 | 28 | -define(DEBUG(Txt, Args), 29 | case erlang:get(nkpacket_debug) of 30 | true -> ?LLOG(debug, Txt, Args); 31 | _ -> ok 32 | end). 33 | 34 | -define(LLOG(Type, Txt, Args), 35 | lager:Type("NkPACKET Conn "++Txt, Args)). 36 | 37 | 38 | -record(payload, { 39 | type = undefined :: cow_ws:frame_type(), 40 | rsv = undefined :: cow_ws:rsv(), 41 | len = undefined :: non_neg_integer(), 42 | mask_key = undefined :: cow_ws:mask_key(), 43 | close_code = undefined :: undefined | cow_ws:close_code(), 44 | unmasked = <<>> :: binary(), 45 | unmasked_len = 0 :: non_neg_integer() 46 | }). 47 | 48 | -record(ws_state, { 49 | buffer = <<>> :: binary(), 50 | in = head :: head | #payload{} | close, 51 | frag_state = undefined :: cow_ws:frag_state(), 52 | frag_buffer = <<>> :: binary(), 53 | utf8_state = 0 :: cow_ws:utf8_state(), 54 | extensions = #{} :: cow_ws:extensions() 55 | }). 56 | 57 | 58 | 59 | %% =================================================================== 60 | %% Handshake 61 | %% =================================================================== 62 | 63 | -spec start_handshake(#nkport{}) -> 64 | {ok, binary()|undefined, binary()} | {error, term()}. 65 | 66 | start_handshake(NkPort) -> 67 | #nkport{ 68 | transp = Transp, 69 | socket = Socket 70 | } = NkPort, 71 | {ok, Req, Key} = get_handshake_req(NkPort), 72 | TranspMod = case Transp of 73 | ws -> ranch_tcp; 74 | wss -> ranch_ssl 75 | end, 76 | ?DEBUG("sending ws request: ~s", [print_headers(list_to_binary(Req))]), 77 | case TranspMod:send(Socket, Req) of 78 | ok -> 79 | case recv(TranspMod, Socket, <<>>) of 80 | {ok, Data} -> 81 | ?DEBUG("received ws reply: ~s", [print_headers(Data)]), 82 | case get_handshake_resp(Data, Key) of 83 | {ok, WsProto, Rest} -> 84 | {ok, WsProto, Rest}; 85 | close -> 86 | {error, closed} 87 | end; 88 | {error, Error} -> 89 | {error, Error} 90 | end; 91 | {error, Error} -> 92 | {error, Error} 93 | end. 94 | 95 | 96 | %% @private 97 | get_handshake_req(#nkport{remote_ip=Ip, remote_port=Port, opts=Meta}) -> 98 | Host = case maps:get(host, Meta, undefined) of 99 | undefined -> nklib_util:to_host(Ip); 100 | Host0 -> Host0 101 | end, 102 | Path = maps:get(path, Meta), 103 | Key = cow_ws:key(), 104 | Headers2 = [ 105 | {<<"Host">>, [Host, $:, integer_to_binary(Port)]}, 106 | {<<"Connection">>, <<"Upgrade">>}, 107 | {<<"Upgrade">>, <<"websocket">>}, 108 | {<<"Sec-WebSocket-Version">>, <<"13">>}, 109 | {<<"Sec-WebSocket-Key">>, Key} 110 | ], 111 | Headers3 = case maps:get(ws_proto, Meta, undefined) of 112 | undefined -> 113 | Headers2; 114 | WsProto -> 115 | [ 116 | {<<"sec-websocket-protocol">>, nklib_util:to_binary(WsProto)} 117 | | Headers2 118 | ] 119 | end, 120 | Headers4 = Headers3 ++ maps:get(headers, Meta, []), 121 | Req = cow_http:request(<<"GET">>, Path, 'HTTP/1.1', Headers4), 122 | {ok, Req, Key}. 123 | 124 | 125 | %% @private 126 | get_handshake_resp(Data, Key) -> 127 | {_Version, _Status, _, Rest} = cow_http:parse_status_line(Data), 128 | {Headers, Rest2} = cow_http:parse_headers(Rest), 129 | WsProto = case lists:keyfind(<<"sec-websocket-protocol">>, 1, Headers) of 130 | false -> undefined; 131 | {_, WsProto0} -> WsProto0 132 | end, 133 | case lists:keyfind(<<"sec-websocket-accept">>, 1, Headers) of 134 | false -> 135 | close; 136 | {_, Accept} -> 137 | case cow_ws:encode_key(Key) of 138 | Accept -> 139 | {ok, WsProto, Rest2}; 140 | _ -> 141 | close 142 | end 143 | end. 144 | 145 | 146 | %% =================================================================== 147 | %% Incoming 148 | %% =================================================================== 149 | 150 | %% @private 151 | -spec init(cow_ws:extensions()) -> 152 | #ws_state{}. 153 | 154 | init(Exts) -> 155 | #ws_state{extensions=Exts}. 156 | 157 | 158 | %% @private 159 | -spec handle(binary(), #ws_state{}) -> 160 | {ok, #ws_state{}} | 161 | {data, cow_ws:frame(), Rest::binary(), #ws_state{}} | 162 | {reply, cow_ws:frame(), Rest::binary(), #ws_state{}} | 163 | close. 164 | 165 | %% Do not handle anything if we received a close frame. 166 | handle(_, State=#ws_state{in=close}) -> 167 | {ok, State}; 168 | 169 | %% Shortcut for common case when Data is empty after processing a frame. 170 | handle(<<>>, State=#ws_state{in=head}) -> 171 | {ok, State}; 172 | 173 | handle(Data, #ws_state{in=head}=State) -> 174 | #ws_state{buffer=Buffer, frag_state=FragState, extensions=Exts} = State, 175 | Data2 = << Buffer/binary, Data/binary >>, 176 | case cow_ws:parse_header(Data2, Exts, FragState) of 177 | {Type, FragState2, Rsv, Len, MaskKey, Rest} -> 178 | In = #payload{type=Type, rsv=Rsv, len=Len, mask_key=MaskKey}, 179 | State1 = State#ws_state{buffer= <<>>, in=In, frag_state=FragState2}, 180 | handle(Rest, State1); 181 | more -> 182 | {ok, State#ws_state{buffer=Data2}}; 183 | error -> 184 | close({error, badframe}, State) 185 | end; 186 | 187 | handle(Data, State) -> 188 | #ws_state{ 189 | in = In, 190 | frag_state = FragState, 191 | utf8_state = Utf8State, 192 | extensions = Exts 193 | } = State, 194 | #payload{ 195 | type = Type, 196 | rsv = Rsv, 197 | len = Len, 198 | mask_key = MaskKey, 199 | close_code = CloseCode, 200 | unmasked = Unmasked, 201 | unmasked_len = UnmaskedLen 202 | } = In, 203 | case 204 | cow_ws:parse_payload(Data, MaskKey, Utf8State, UnmaskedLen, Type, 205 | Len, FragState, Exts, Rsv) 206 | of 207 | {ok, CloseCode2, Payload, Utf8State2, Rest} -> 208 | State1 = State#ws_state{in=head, utf8_state=Utf8State2}, 209 | Data1 = << Unmasked/binary, Payload/binary >>, 210 | dispatch(Rest, State1, Type, Data1, CloseCode2); 211 | {ok, Payload, Utf8State2, Rest} -> 212 | State1 = State#ws_state{in=head, utf8_state=Utf8State2}, 213 | Data1 = << Unmasked/binary, Payload/binary >>, 214 | dispatch(Rest, State1, Type, Data1, CloseCode); 215 | {more, CloseCode2, Payload, Utf8State2} -> 216 | In1 = In#payload{ 217 | close_code = CloseCode2, 218 | unmasked = << Unmasked/binary, Payload/binary >>, 219 | len = Len - byte_size(Data), 220 | unmasked_len =2 + byte_size(Data) 221 | }, 222 | {ok, State#ws_state{in=In1, utf8_state=Utf8State2}}; 223 | {more, Payload, Utf8State2} -> 224 | In1 = In#payload{ 225 | unmasked= << Unmasked/binary, Payload/binary >>, 226 | len = Len - byte_size(Data), 227 | unmasked_len = UnmaskedLen + byte_size(Data) 228 | }, 229 | {ok, State#ws_state{in=In1, utf8_state=Utf8State2}}; 230 | {error, _Reason} = Error -> 231 | close(Error, State) 232 | end. 233 | 234 | 235 | %% @private 236 | dispatch(Rest, State, Type0, Payload0, CloseCode0) -> 237 | #ws_state{frag_state=FragState, frag_buffer=SoFar} = State, 238 | case cow_ws:make_frame(Type0, Payload0, CloseCode0, FragState) of 239 | {fragment, nofin, _, Payload} -> 240 | Frag1 = << SoFar/binary, Payload/binary >>, 241 | handle(Rest, State#ws_state{frag_buffer=Frag1}); 242 | {fragment, fin, Type, Payload} -> 243 | Data1 = << SoFar/binary, Payload/binary >>, 244 | State1 = State#ws_state{frag_state=undefined, frag_buffer= <<>>}, 245 | {data, {Type, Data1}, Rest, State1}; 246 | ping -> 247 | {reply, pong, Rest, State}; 248 | {ping, Payload} -> 249 | {reply, {pong, Payload}, Rest, State}; 250 | pong -> 251 | {data, pong, Rest, State}; 252 | {pong, Payload} -> 253 | {data, {pong, Payload}, Rest, State}; 254 | close -> 255 | close; 256 | {close, _, _} -> 257 | close; 258 | Frame -> 259 | {data, Frame, Rest, State} 260 | end. 261 | 262 | 263 | %% @private 264 | -spec close({error, badframe|badencoding}, #ws_state{}) -> 265 | {reply, cow_ws:frame(), binary(), #ws_state{}}. 266 | 267 | close(Reason, State) -> 268 | case Reason of 269 | % stop -> 270 | % {reply, {close, 1000, <<>>}, <<>>, State}; 271 | % timeout -> 272 | % {reply, {close, 1000, <<>>}, <<>>, State}; 273 | {error, badframe} -> 274 | {reply, {close, 1002, <<>>}, <<>>, State}; 275 | {error, badencoding} -> 276 | {reply, {close, 1007, <<>>}, <<>>, State} 277 | end. 278 | 279 | 280 | %% =================================================================== 281 | %% Send 282 | %% =================================================================== 283 | 284 | %% @private 285 | -spec encode(cow_ws:frame()) -> 286 | binary(). 287 | 288 | encode(Frame) -> 289 | cow_ws:masked_frame(Frame, #{}). 290 | 291 | 292 | 293 | 294 | %% =================================================================== 295 | %% Util 296 | %% =================================================================== 297 | 298 | 299 | %% @private 300 | recv(Mod, Socket, Buff) -> 301 | case Mod:recv(Socket, 0, 5000) of 302 | {ok, Data} -> 303 | Data1 = <>, 304 | case binary:match(Data, <<"\r\n\r\n">>) of 305 | nomatch -> 306 | recv(Mod, Socket, Data1); 307 | _ -> 308 | {ok, Data1} 309 | end; 310 | {error, Error} -> 311 | {error, Error} 312 | end. 313 | 314 | 315 | %% private 316 | print_headers(Binary) -> 317 | Lines = [ 318 | [<<" ">>, Line, <<"\n">>] 319 | || Line <- binary:split(Binary, <<"\r\n">>, [global]) 320 | ], 321 | list_to_binary(io_lib:format("\r\n\r\n~s\r\n", [list_to_binary(Lines)])). 322 | 323 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /src/http_client/nkpacket_httpc_protocol.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2019 Carlos Gonzalez Florido. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %% @doc Default implementation for HTTP1 clients 22 | %% Will send messages {nkpacket_httpc_protocol, Ref, Term}, 23 | %% Term :: {head, Status, Headers} | {body, Body} | {chunk, Chunk} | {error, term()} 24 | 25 | 26 | -module(nkpacket_httpc_protocol). 27 | -author('Carlos Gonzalez '). 28 | % -behaviour(nkpacket_protocol). 29 | 30 | -export([transports/1, default_port/1]). 31 | -export([conn_init/1, conn_parse/3, conn_encode/3, conn_timeout/2, conn_stop/3]). 32 | 33 | -include("nkpacket.hrl"). 34 | -include_lib("nklib/include/nklib.hrl"). 35 | 36 | 37 | -define(DEBUG(Txt, Args, NkPort), 38 | case erlang:get(nkpacket_debug) of 39 | true -> ?LLOG(debug, Txt, Args, NkPort); 40 | _ -> ok 41 | end). 42 | 43 | 44 | -define(LLOG(Type, Txt, Args, NkPort), 45 | lager:Type("NkPACKET Conn HTTP ~p (~p) "++Txt, 46 | [NkPort#nkport.protocol, NkPort#nkport.transp|Args])). 47 | 48 | 49 | %% =================================================================== 50 | %% Types 51 | %% =================================================================== 52 | 53 | -type request() :: 54 | #{ 55 | method => atom() | binary(), 56 | path => binary, 57 | headers => [{string()|binary(), string()|binary()}], 58 | body => iolist(), 59 | ref => reference(), 60 | pid => pid() 61 | }. 62 | 63 | 64 | %% =================================================================== 65 | %% Protocol callbacks 66 | %% =================================================================== 67 | 68 | 69 | %% @private 70 | -spec transports(nklib:scheme()) -> 71 | [nkpacket:transport()]. 72 | 73 | transports(_) -> [https, http]. 74 | 75 | 76 | default_port(http) -> 80; 77 | default_port(https) -> 443. 78 | 79 | -record(state, { 80 | host :: binary(), 81 | refresh_req :: request() | undefined, 82 | headers :: [{binary(), binary()}], 83 | buff = <<>> :: binary(), 84 | streams = [] :: [{reference(), pid()}], 85 | next = head :: head | {body, non_neg_integer()} | chunked | stream 86 | }). 87 | 88 | 89 | %% @private 90 | -spec conn_init(nkpacket:nkport()) -> 91 | {ok, #state{}}. 92 | 93 | conn_init(NkPort) -> 94 | #nkport{remote_ip=Ip, remote_port=Port, opts=Opts} = NkPort, 95 | {ok, UserState1} = nkpacket:get_user_state(NkPort), 96 | ?DEBUG("protocol init", [], NkPort), 97 | UserState2 = case UserState1 of 98 | undefined -> 99 | #{}; 100 | _ -> 101 | UserState1 102 | end, 103 | RefreshReq = case maps:find(refresh_request, UserState2) of 104 | {ok, Req1} -> 105 | Req1#{ref=>refresh}; 106 | error -> 107 | undefined 108 | end, 109 | Host = case maps:find(host, Opts) of 110 | {ok, Host0} -> 111 | <>; 112 | error -> 113 | <<(nklib_util:to_host(Ip))/binary, $:, (nklib_util:to_binary(Port))/binary>> 114 | end, 115 | Hds = [{to_bin(K), to_bin(V)} || {K, V} <- maps:get(headers, UserState2, [])], 116 | State = #state{ 117 | host = Host, 118 | refresh_req = RefreshReq, 119 | headers = Hds 120 | }, 121 | {ok, State}. 122 | 123 | 124 | %% @private 125 | -spec conn_parse(term()|close, nkpacket:nkport(), #state{}) -> 126 | {ok, #state{}} | {stop, normal, #state{}}. 127 | 128 | conn_parse(close, _NkPort, State) -> 129 | {ok, State}; 130 | 131 | conn_parse(Data, NkPort, State) -> 132 | handle(Data, NkPort, State). 133 | 134 | 135 | %% @private 136 | -spec conn_encode(term(), nkpacket:nkport(), #state{}) -> 137 | {ok, nkpacket:raw_msg(), #state{}} | {error, term(), #state{}} | 138 | {stop, Reason::term()}. 139 | 140 | conn_encode({nkpacket_http, Request}, _NkPort, State) -> 141 | request(Request, State); 142 | 143 | conn_encode({nkpacket_data, Ref, Data}, _NkPort, State) -> 144 | data(Ref, Data, State). 145 | 146 | %% @doc This function is called when the idle_timer timeout fires 147 | %% If not implemented, will stop the connection 148 | %% If ok is returned, timer is restarted 149 | -spec conn_timeout(nkpacket:nkport(), #state{}) -> 150 | {ok, #state{}} | {stop, term(), #state{}}. 151 | 152 | conn_timeout(_NkPort, #state{refresh_req=undefined}=State) -> 153 | {stop, normal, State}; 154 | 155 | conn_timeout(_NkPort, #state{refresh_req=Refresh}=State) -> 156 | ?DEBUG("sending refresh", [], _NkPort), 157 | nkpacket_connection:send_async(self(), {nkpacket_http, Refresh}), 158 | {ok, State}. 159 | 160 | 161 | %% @doc 162 | conn_stop(_Reason, _NkPort, #state{streams=[]}) -> 163 | ok; 164 | 165 | conn_stop(_Reason, _NkPort, #state{next=head, streams=Streams}) -> 166 | lists:foreach( 167 | fun({Ref, Pid}) -> notify(Ref, Pid, {error, process_failed}) end, 168 | Streams); 169 | 170 | conn_stop(Reason, NkPort, #state{streams=[{Ref, Pid}|Rest]}=State) -> 171 | notify(Ref, Pid, {body, <<>>}), 172 | conn_stop(Reason, NkPort, State#state{streams=Rest}). 173 | 174 | 175 | %% =================================================================== 176 | %% HTTP handle 177 | %% =================================================================== 178 | 179 | %% @private 180 | -spec request(request(), #state{}) -> 181 | {ok, iolist(), #state{}}. 182 | 183 | request(#{ref:=Ref}=Req, State) -> 184 | #state{host=Host, headers=BaseHeaders, streams=Streams} = State, 185 | Method2 = nklib_util:to_upper(maps:get(method, Req, <<"GET">>)), 186 | Path2 = to_bin(maps:get(path, Req, <<"/">>)), 187 | Hds1 = maps:get(headers, Req, []) ++ BaseHeaders, 188 | Hds2 = [{to_bin(H), to_bin(V)} || {H, V} <- Hds1], 189 | Hds3 = case 190 | lists:keymember(<<"host">>, 1, Hds2) orelse 191 | lists:keymember(<<"Host">>, 1, Hds2) 192 | of 193 | true -> 194 | Hds2; 195 | false -> 196 | [{<<"Host">>, Host}|Hds2] 197 | end, 198 | Body = to_bin(maps:get(body, Req, <<>>)), 199 | BodySize = nklib_util:to_binary(byte_size(Body)), 200 | Hds4 = [{<<"Content-Length">>, BodySize}|Hds3], 201 | Pid = maps:get(pid, Req, none), 202 | State2 = State#state{streams = Streams ++ [{Ref, Pid}]}, 203 | RawMsg = cow_http:request(Method2, Path2, 'HTTP/1.1', Hds4), 204 | {ok, [RawMsg, Body], State2}. 205 | 206 | 207 | %% @private 208 | -spec data(term(), iolist(), #state{}) -> 209 | {ok, iolist(), #state{}} | {error, invalid_ref, #state{}}. 210 | 211 | data(_Ref, _Data, #state{streams=[]}=State) -> 212 | {error, invalid_ref, State}; 213 | 214 | data(Ref, Data, #state{streams=Streams}=State) -> 215 | case lists:last(Streams) of 216 | {Ref, _} -> 217 | {ok, Data, State}; 218 | _ -> 219 | {error, invalid_ref, State} 220 | end. 221 | 222 | 223 | -spec handle(binary(), #nkport{}, #state{}) -> 224 | {ok, #state{}} | {stop, term(), #state{}}. 225 | 226 | handle(<<>>, _NkPort, State) -> 227 | {ok, State}; 228 | 229 | handle(_, _NkPort, #state{streams=[]}=State) -> 230 | {stop, normal, State}; 231 | 232 | handle(Data, NkPort, #state{next=head, buff=Buff}=State) -> 233 | Data1 = << Buff/binary, Data/binary >>, 234 | case binary:match(Data1, <<"\r\n\r\n">>) of 235 | nomatch -> 236 | {ok, State#state{buff=Data1}}; 237 | {_, _} -> 238 | handle_head(Data1, NkPort, State#state{buff = <<>>}) 239 | end; 240 | 241 | handle(Data, NkPort, #state{next={body, Length}}=State) -> 242 | ?DEBUG("parsing body: ~s", [Data], NkPort), 243 | #state{buff=Buff, streams=[{Ref, Pid}|_]} = State, 244 | Data1 = << Buff/binary, Data/binary>>, 245 | case byte_size(Data1) of 246 | Length -> 247 | notify(Ref, Pid, {body, Data1}), 248 | {ok, do_next(State)}; 249 | Size when Size < Length -> 250 | {ok, State#state{buff=Data1}}; 251 | _ -> 252 | {Data2, Rest} = erlang:split_binary(Data1, Length), 253 | notify(Ref, Pid, {body, Data2}), 254 | handle(Rest, NkPort, do_next(State)) 255 | end; 256 | 257 | handle(Data, NkPort, #state{next=chunked}=State) -> 258 | ?DEBUG("parsing chunked: ~s", [Data], NkPort), 259 | #state{buff=Buff, streams=[{Ref, Pid}|_]} = State, 260 | Data1 = << Buff/binary, Data/binary>>, 261 | case parse_chunked(Data1) of 262 | {data, <<>>, Rest} -> 263 | notify(Ref, Pid, {body, <<>>}), 264 | handle(Rest, NkPort, do_next(State)); 265 | {data, Chunk, Rest} -> 266 | notify(Ref, Pid, {chunk, Chunk}), 267 | handle(Rest, NkPort, State#state{buff = <<>>}); 268 | more -> 269 | {ok, State#state{buff=Data1}} 270 | end; 271 | 272 | handle(Data, _NkPort, #state{next=stream, streams=[{Ref, Pid}|_]}=State) -> 273 | ?DEBUG("parsing stream: ~s", [Data], _NkPort), 274 | notify(Ref, Pid, {chunk, Data}), 275 | {ok, State}. 276 | 277 | 278 | %% @private 279 | -spec handle_head(binary(), #nkport{}, #state{}) -> 280 | {ok, #state{}}. 281 | 282 | handle_head(Data, NkPort, #state{streams=[{Ref, Pid}|_]}=State) -> 283 | {_Version, Status, _Msg, Rest} = cow_http:parse_status_line(Data), 284 | ?DEBUG("received head: ~s ~p ~s", [_Version, Status, _Msg], NkPort), 285 | {Hds, Rest2} = cow_http:parse_headers(Rest), 286 | ?DEBUG("received headers: ~p", [Hds], NkPort), 287 | notify(Ref, Pid, {head, Status, Hds}), 288 | Remaining = case lists:keyfind(<<"content-length">>, 1, Hds) of 289 | {_, <<"0">>} -> 290 | 0; 291 | {_, Length} -> 292 | cow_http_hd:parse_content_length(Length); 293 | false when Status==204; Status==304 -> 294 | 0; 295 | false -> 296 | case lists:keyfind(<<"transfer-encoding">>, 1, Hds) of 297 | false -> 298 | stream; 299 | {_, TE} -> 300 | case cow_http_hd:parse_transfer_encoding(TE) of 301 | [<<"chunked">>] -> chunked; 302 | [<<"identity">>] -> 0 303 | end 304 | end 305 | end, 306 | ?DEBUG("remaining: ~p", [Remaining], NkPort), 307 | State1 = case Remaining of 308 | 0 -> 309 | notify(Ref, Pid, {body, <<>>}), 310 | do_next(State); 311 | chunked -> 312 | State#state{next=chunked}; 313 | stream -> 314 | State#state{next=stream}; 315 | _ -> 316 | State#state{next={body, Remaining}} 317 | end, 318 | handle(Rest2, NkPort, State1). 319 | 320 | 321 | 322 | %% @private 323 | -spec parse_chunked(binary()) -> 324 | {ok, binary(), binary()} | more. 325 | 326 | parse_chunked(S) -> 327 | case find_chunked_length(S, []) of 328 | {ok, Length, Data} -> 329 | FullLength = Length + 2, 330 | case byte_size(Data) of 331 | FullLength -> 332 | <> = Data, 333 | {data, Data1, <<>>}; 334 | Size when Size < FullLength -> 335 | more; 336 | _ -> 337 | {Data1, Rest} = erlang:split_binary(Data, FullLength), 338 | <> = Data1, 339 | {data, Data2, Rest} 340 | end; 341 | more -> 342 | more 343 | end. 344 | 345 | 346 | %% @private 347 | -spec find_chunked_length(binary(), string()) -> 348 | {ok, integer(), binary()} | more. 349 | 350 | find_chunked_length(<>, Acc) -> 351 | {V, _} = lists:foldl( 352 | fun(Ch, {Sum, Mult}) -> 353 | if 354 | Ch >= $0, Ch =< $9 -> {Sum + (Ch-$0)*Mult, 16*Mult}; 355 | Ch >= $a, Ch =< $f -> {Sum + (Ch-$a+10)*Mult, 16*Mult}; 356 | Ch >= $A, Ch =< $F -> {Sum + (Ch-$A+10)*Mult, 16*Mult} 357 | end 358 | end, 359 | {0, 1}, 360 | [C|Acc]), 361 | {ok, V, Rest}; 362 | 363 | find_chunked_length(<>, Acc) -> 364 | find_chunked_length(Rest, [C|Acc]); 365 | 366 | find_chunked_length(<<>>, _Acc) -> 367 | more. 368 | 369 | 370 | %% @private 371 | notify(Ref, Pid, Term) when is_pid(Pid) -> 372 | Pid ! {nkpacket_httpc_protocol, Ref, Term}; 373 | 374 | notify(_Ref, _Pid, _Term) -> 375 | ok. 376 | 377 | 378 | %% @private 379 | do_next(#state{streams=[_|Rest]}=State) -> 380 | State#state{next=head, buff= <<>>, streams=Rest}. 381 | 382 | 383 | %% @private 384 | to_bin(Term) when is_binary(Term) -> Term; 385 | to_bin(Term) -> nklib_util:to_binary(Term). 386 | 387 | 388 | 389 | 390 | 391 | 392 | -------------------------------------------------------------------------------- /test/tcp_test.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2015 Carlos Gonzalez Florido. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | -module(tcp_test). 22 | -author('Carlos Gonzalez '). 23 | 24 | -compile([export_all]). 25 | -compile(nowarn_export_all). 26 | -include_lib("eunit/include/eunit.hrl"). 27 | -include("nkpacket.hrl"). 28 | 29 | tcp_test_() -> 30 | {setup, spawn, 31 | fun() -> 32 | ok = nkpacket_app:start(), 33 | ?debugMsg("Starting TCP test") 34 | end, 35 | fun(_) -> 36 | ok 37 | end, 38 | fun(_) -> 39 | [ 40 | fun() -> basic() end, 41 | fun() -> tls() end, 42 | fun() -> send() end 43 | ] 44 | end 45 | }. 46 | 47 | 48 | basic() -> 49 | {Ref1, M1, Ref2, M2} = test_util:reset_2(), 50 | {ok, _, Tcp1} = nkpacket:start_listener(#nkconn{protocol=test_protocol, transp=tcp, ip={0,0,0,0}, port=0, 51 | opts=M1#{class=>dom1, idle_timeout=>1000}}), 52 | {ok, _, Tcp2} = nkpacket:start_listener(#nkconn{protocol=test_protocol, transp=tcp, ip={0,0,0,0}, port=0, 53 | opts=M2#{class=>dom2}}), 54 | timer:sleep(100), 55 | receive {Ref1, listen_init} -> ok after 1000 -> error(?LINE) end, 56 | receive {Ref2, listen_init} -> ok after 1000 -> error(?LINE) end, 57 | 58 | [Listen1] = nkpacket:get_class_ids(dom1), 59 | {ok, #nkport{transp=tcp, 60 | class = dom1, 61 | local_ip={0,0,0,0}, local_port=ListenPort1, 62 | listen_ip={0,0,0,0}, listen_port=ListenPort1, 63 | remote_ip=undefined, remote_port=undefined, pid=Tcp1 64 | }} = nkpacket:get_nkport(Listen1), 65 | 66 | [Listen2] = nkpacket:get_class_ids(dom2), 67 | {ok, #nkport{transp=tcp, 68 | class = dom2, 69 | local_port=ListenPort2, pid=Tcp2, 70 | listen_ip={0,0,0,0}, listen_port=ListenPort2, 71 | remote_ip=undefined, remote_port=undefined 72 | }} = nkpacket:get_nkport(Listen2), 73 | 74 | {ok, {_, _, _, ListenPort1}} = nkpacket:get_local(Tcp1), 75 | {ok, {_, _, _, ListenPort2}} = nkpacket:get_local(Tcp2), 76 | case ListenPort1 of 77 | 1235 -> ok; 78 | _ -> lager:warning("Could not open port 1235") 79 | end, 80 | 81 | Uri = "", 82 | {ok, _} = nkpacket:send(Uri, msg1, M2#{idle_timeout=>5000, class=>dom2, debug=>true, base_nkport=>true}), 83 | receive {Ref1, conn_init} -> ok after 1000 -> error(?LINE) end, 84 | receive {Ref1, {parse, msg1}} -> ok after 1000 -> error(?LINE) end, 85 | receive {Ref2, conn_init} -> ok after 1000 -> error(?LINE) end, 86 | receive {Ref2, {encode, msg1}} -> ok after 1000 -> error(?LINE) end, 87 | 88 | [{_, Conn2}] = nkpacket_connection:get_all_class(dom2), 89 | {ok, #nkport{ 90 | class = dom2, 91 | transp=tcp, pid=Conn2, 92 | local_ip={127,0,0,1}, local_port=LPort2, 93 | remote_ip={127,0,0,1}, remote_port=ListenPort1, 94 | listen_ip={0,0,0,0}, listen_port=ListenPort2 95 | }} = nkpacket:get_nkport(Conn2), 96 | 97 | [{_, Conn1}] = nkpacket_connection:get_all_class(dom1), 98 | {ok, #nkport{ 99 | class = dom1, 100 | transp=tcp, pid=Conn1, 101 | local_ip={127,0,0,1}, local_port=_LPort1, 102 | remote_ip={127,0,0,1}, remote_port=LPort2, 103 | listen_ip={0,0,0,0}, listen_port=ListenPort1 104 | }} = nkpacket:get_nkport(Conn1), 105 | 106 | Time1 = nkpacket_connection:get_timeout(Conn1), 107 | true = Time1 > 0 andalso Time1 =< 1000, 108 | Time2 = nkpacket_connection:get_timeout(Conn2), 109 | true = Time2 > 4000 andalso Time2 =< 5000, 110 | 111 | %% Connection 2 will stop after 1 sec, and will tear down conn1 112 | receive {Ref2, conn_stop} -> ok after 2000 -> error(?LINE) end, 113 | receive {Ref1, conn_stop} -> ok after 2000 -> error(?LINE) end, 114 | timer:sleep(50), 115 | [Listen2] = nkpacket:get_class_ids(dom2), 116 | [Listen1] = nkpacket:get_class_ids(dom1), 117 | test_util:ensure([Ref1, Ref2]), 118 | ok. 119 | 120 | 121 | 122 | tls() -> 123 | {Ref1, M1, Ref2, M2} = test_util:reset_2(), 124 | ok = nkpacket:register_protocol(test, test_protocol), 125 | {ok, _, Tls1} = nkpacket:start_listener(#nkconn{protocol=test_protocol, transp=tls, ip={0,0,0,0}, port=0, 126 | opts=M1#{class=>dom1, tcp_listeners=>1}}), 127 | {ok, {_, _, _, ListenPort1}} = nkpacket:get_local(Tls1), 128 | case ListenPort1 of 129 | 1236 -> ok; 130 | _ -> lager:warning("Could not open port 1236") 131 | end, 132 | receive {Ref1, listen_init} -> ok after 1000 -> error(?LINE) end, 133 | timer:sleep(1000), 134 | 135 | % Sending a request without a matching started listener 136 | 137 | Uri = "", 138 | {ok, _} = nkpacket:send(Uri, msg1, M2#{idle_timeout=>1000, class=>dom2, base_nkport=>false}), 139 | receive {Ref1, conn_init} -> ok after 1000 -> error(?LINE) end, 140 | receive {Ref1, {parse, msg1}} -> ok after 1000 -> error(?LINE) end, 141 | receive {Ref2, conn_init} -> ok after 1000 -> error(?LINE) end, 142 | receive {Ref2, {encode, msg1}} -> ok after 1000 -> error(?LINE) end, 143 | 144 | [Listen1] = nkpacket:get_class_ids(dom1), 145 | {ok, #nkport{ 146 | class = dom1, 147 | transp = tls, 148 | local_ip = {0,0,0,0}, local_port = ListenPort, 149 | remote_ip = undefined, remote_port = undefined, 150 | listen_ip={0,0,0,0}, listen_port = ListenPort, 151 | protocol = test_protocol, pid = Tls1, 152 | socket = {sslsocket, _, _} 153 | }} = nkpacket:get_nkport(Listen1), 154 | 155 | [{_, Conn1}] = nkpacket_connection:get_all_class(dom1), 156 | {ok, #nkport{ 157 | class = dom1, 158 | transp = tls, 159 | local_ip = {127,0,0,1}, local_port = _Dom1Port, 160 | remote_ip = {127,0,0,1}, remote_port = Dom2Port, 161 | listen_ip = {0,0,0,0}, listen_port = ListenPort, 162 | protocol = test_protocol, pid = _Dom1Pid, 163 | socket = {sslsocket, _, _} 164 | }} = nkpacket:get_nkport(Conn1), 165 | 166 | [{_, Conn2}] = nkpacket_connection:get_all_class(dom2), 167 | {ok, #nkport{ 168 | class = dom2, 169 | transp = tls, 170 | local_ip = {127,0,0,1}, local_port = Dom2Port, 171 | remote_ip = {127,0,0,1}, remote_port = ListenPort, 172 | listen_ip = undefined, listen_port = undefined, 173 | protocol = test_protocol, pid = _Dom2Pid, 174 | socket = {sslsocket, _, _} 175 | }} = nkpacket:get_nkport(Conn2), 176 | 177 | % If we send another message, the same connection is reused 178 | {ok, _} = nkpacket:send(Uri, msg2, #{class=>dom2}), 179 | receive {Ref1, {parse, msg2}} -> ok after 1000 -> error(?LINE) end, 180 | receive {Ref2, {encode, msg2}} -> ok after 1000 -> error(?LINE) end, 181 | [{_, Conn1}] = nkpacket_connection:get_all_class(dom1), 182 | 183 | % Wait for the timeout 184 | timer:sleep(1500), 185 | [{IdTls1, _, Tls1}] = nkpacket:get_all(), 186 | [Tls1] = nkpacket:get_id_pids(IdTls1), 187 | ok = nkpacket:stop_listeners(Tls1), 188 | receive {Ref1, conn_stop} -> ok after 1000 -> error(?LINE) end, 189 | receive {Ref2, conn_stop} -> ok after 1000 -> error(?LINE) end, 190 | receive {Ref1, listen_stop} -> ok after 1000 -> error(?LINE) end, 191 | test_util:ensure([Ref1, Ref2]). 192 | 193 | 194 | send() -> 195 | {Ref1, M1, Ref2, M2} = test_util:reset_2(), 196 | ok = nkpacket:register_protocol(test, test_protocol), 197 | {ok, _, Udp1} = nkpacket:start_listener(#nkconn{protocol=test_protocol, transp=udp, ip={0,0,0,0}, port=0, 198 | opts=M1#{class=>dom1, udp_starts_tcp=>true}}), 199 | % Since '1234' is not available, a random one is used 200 | % (Oops, in linux it allows to open it again, the old do not receive more packets!) 201 | Port2 = test_util:get_port(udp), 202 | {ok, _, Udp2} = nkpacket:start_listener(#nkconn{protocol=test_protocol, transp=udp, ip={0,0,0,0}, port=Port2, 203 | opts=M2#{class=>dom2, idle_timeout=>1000, udp_starts_tcp=>true, tcp_packet=>4}}), 204 | timer:sleep(100), 205 | receive {Ref1, listen_init} -> ok after 1000 -> error(?LINE) end, 206 | receive {Ref1, listen_init} -> ok after 1000 -> error(?LINE) end, 207 | receive {Ref2, listen_init} -> ok after 1000 -> error(?LINE) end, 208 | receive {Ref2, listen_init} -> ok after 1000 -> error(?LINE) end, 209 | %% {ok, {_, _, _, Listen1}} = nkpacket:get_local(Udp1), 210 | {ok, {_, udp, _, Listen2}} = nkpacket:get_local(Udp2), 211 | 212 | 213 | % Invalid sends 214 | lager:warning("Next warning about a invalid send specification is expected"), 215 | {error, {invalid_uri, wrong}} = nkpacket:send(wrong, msg1), 216 | Base0 = #nkconn{protocol=test_protocol, transp=tcp, ip={0,0,0,0}, port=Listen2}, 217 | Base1 = Base0#nkconn{ip={127,0,0,1}}, 218 | {error, no_transports} = nkpacket:send({current, Base0}, msg1), 219 | {error, no_listening_transport} = nkpacket:send(Base1#nkconn{transp=sctp}, msg1, #{base_nkport=>true}), 220 | Msg = crypto:strong_rand_bytes(5000), 221 | % No class 222 | 223 | %% {ok, _, _Udp3} = nkpacket:start_listener(#nkconn{protocol=test_protocol, transp=udp, opts=#{class=>dom3}}), 224 | {error, no_listening_transport} = nkpacket:send(Base1#nkconn{transp=udp, opts=M1}, {msg1, Msg}), 225 | 226 | {error, udp_too_large} = nkpacket:send({connect, Base1#nkconn{transp=udp, opts=M1#{class=>dom1, udp_max_size=>1500}}}, {msg1, Msg}, #{base_nkport=>true}), 227 | receive {Ref1, conn_init} -> ok after 1000 -> error(?LINE) end, 228 | receive {Ref1, {encode, {msg1, Msg}}} -> ok after 1000 -> error(?LINE) end, 229 | 230 | receive {Ref1, conn_stop} -> ok after 1000 -> error(?LINE) end, 231 | 232 | 233 | % This is going to use tcp 234 | {ok, Conn1Pid} = nkpacket:send(Base1#nkconn{transp=udp, opts=M1#{class=>dom1, udp_to_tcp=>true, tcp_packet=>4, 235 | udp_max_size=>1500, base_nkport=>true}}, 236 | {msg1, Msg}), 237 | receive {Ref1, conn_init} -> ok after 1000 -> error(?LINE) end, 238 | receive {Ref1, conn_init} -> ok after 1000 -> error(?LINE) end, 239 | receive {Ref1, {encode, {msg1, Msg}}} -> ok after 1000 -> error(?LINE) end, % Udp 240 | receive {Ref1, {encode, {msg1, Msg}}} -> ok after 1000 -> error(?LINE) end, % Tcp 241 | receive {Ref2, conn_init} -> ok after 1000 -> error(?LINE) end, 242 | receive {Ref2, {parse, {msg1, Msg}}} -> ok after 1000 -> error(?LINE) end, 243 | {ok, #nkport{transp=tcp}} = nkpacket:get_nkport(Conn1Pid), 244 | 245 | % Conn1A = Conn1#nkport{meta=#{}}, 246 | {ok, Conn1Pid} = nkpacket:send(Base1#nkconn{opts=M1#{class=>dom1}}, msg2), 247 | receive {Ref1, {encode, msg2}} -> ok after 1000 -> error(?LINE) end, 248 | receive {Ref2, {parse, msg2}} -> ok after 1000 -> error(?LINE) end, 249 | 250 | %% {ok, Conn1Pid} = nkpacket:send(Conn1Pid, msg3, M1#{class=>dom1}), 251 | {ok, Conn1Pid} = nkpacket:send(Conn1Pid, msg3), 252 | receive {Ref1, {encode, msg3}} -> ok after 1000 -> error(?LINE) end, 253 | receive {Ref2, {parse, msg3}} -> ok after 1000 -> error(?LINE) end, 254 | 255 | {ok, Conn1Pid} = nkpacket:send({current, Base1#nkconn{opts=M1#{class=>dom1}}}, msg4), 256 | receive {Ref1, {encode, msg4}} -> ok after 1000 -> error(?LINE) end, 257 | receive {Ref2, {parse, msg4}} -> ok after 1000 -> error(?LINE) end, 258 | 259 | % Force a new connection 260 | {ok, Conn2Pid} = nkpacket:send({connect, Base1#nkconn{opts=M1#{tcp_packet=>4, class=>dom1}}}, msg5), 261 | receive {Ref1, conn_init} -> ok after 1000 -> error(?LINE) end, 262 | receive {Ref1, {encode, msg5}} -> ok after 1000 -> error(?LINE) end, 263 | receive {Ref2, conn_init} -> ok after 1000 -> error(?LINE) end, 264 | receive {Ref2, {parse, msg5}} -> ok after 1000 -> error(?LINE) end, 265 | true = Conn1Pid /= Conn2Pid, 266 | 267 | ok = nkpacket:stop_listeners(Udp1), 268 | ok = nkpacket:stop_listeners(Udp2), 269 | receive {Ref1, conn_stop} -> ok after 1000 -> error(?LINE) end, % First UDP 270 | receive {Ref1, conn_stop} -> ok after 1000 -> error(?LINE) end, % Second TCP 271 | receive {Ref2, conn_stop} -> ok after 1000 -> error(?LINE) end, % Second TCP-R 272 | receive {Ref1, conn_stop} -> ok after 1000 -> error(?LINE) end, % Third TCP 273 | receive {Ref2, conn_stop} -> ok after 1000 -> error(?LINE) end, % Third TCP-R 274 | 275 | receive {Ref1, listen_stop} -> ok after 1000 -> error(?LINE) end, 276 | receive {Ref1, listen_stop} -> ok after 1000 -> error(?LINE) end, 277 | receive {Ref2, listen_stop} -> ok after 1000 -> error(?LINE) end, 278 | receive {Ref2, listen_stop} -> ok after 1000 -> error(?LINE) end, 279 | 280 | test_util:ensure([Ref1, Ref2]). 281 | 282 | 283 | 284 | 285 | 286 | -------------------------------------------------------------------------------- /src/nkpacket_util.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2019 Carlos Gonzalez Florido. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %% @doc Common library utility functions 22 | -module(nkpacket_util). 23 | -author('Carlos Gonzalez '). 24 | 25 | 26 | -export([get_plugin_net_syntax/1, get_plugin_net_opts/1]). 27 | -export([register_listener/1]). 28 | -export([listen_print_all/0, conn_print_all/0]). 29 | -export([get_local_ips/0, find_main_ip/0, find_main_ip/2]). 30 | -export([get_local_uri/2, get_remote_uri/2, get_uri/4]). 31 | -export([init_protocol/3, call_protocol/4]). 32 | -export([norm_path/1, join_path/2, conn_string/3]). 33 | -export([parse_opts/1, parse_uri_opts/2]). 34 | 35 | -include("nkpacket.hrl"). 36 | -include_lib("nklib/include/nklib.hrl"). 37 | 38 | 39 | %% =================================================================== 40 | %% Public 41 | %% ================================================================= 42 | 43 | %% @doc 44 | get_plugin_net_syntax(Syntax) -> 45 | S0 = maps:merge( 46 | nkpacket_syntax:tls_syntax(), 47 | nkpacket_syntax:packet_syntax() 48 | ), 49 | maps:merge(Syntax, S0). 50 | 51 | 52 | get_plugin_net_opts(Config) -> 53 | Data = lists:filtermap( 54 | fun({Key, Val}) -> 55 | case nklib_util:to_binary(Key) of 56 | <<"packet_", Rest/binary>> -> 57 | {true, {nklib_util:to_existing_atom(Rest), Val}}; 58 | <<"tls_", _/binary>> -> 59 | {true, {Key, Val}}; 60 | _ -> 61 | false 62 | end 63 | end, 64 | maps:to_list(Config)), 65 | maps:from_list(Data). 66 | 67 | 68 | %% @private 69 | register_listener(#nkport{id=Id, class=Class}) -> 70 | nklib_proc:put(nkpacket_listeners, {Id, Class}), 71 | nklib_proc:put({nkpacket_id, Id}, Class), 72 | ok. 73 | 74 | 75 | listen_print_all() -> 76 | print_all(nkpacket:get_all()). 77 | 78 | 79 | conn_print_all() -> 80 | print_all(nkpacket_connection:get_all()). 81 | 82 | 83 | print_all([]) -> 84 | ok; 85 | print_all([{_Id, _Class, Pid}|Rest]) -> 86 | {ok, #nkport{socket=Socket}=NkPort} = nkpacket:get_nkport(Pid), 87 | NkPort1 = case is_tuple(Socket) of true -> 88 | NkPort#nkport{socket=element(1,Socket)}; 89 | false -> NkPort 90 | end, 91 | {_, _, List} = lager:pr(NkPort1, ?MODULE), 92 | io:format("~p\n", [List]), 93 | print_all(Rest). 94 | 95 | 96 | 97 | 98 | %%tls_keys() -> 99 | %% maps:keys(nkpacket_syntax:tls_syntax()). 100 | 101 | 102 | 103 | %% @private It adds an 'id' field if not present 104 | -spec parse_opts(map()|list()) -> 105 | {ok, map()} | {error, term()}. 106 | 107 | parse_opts(Opts) -> 108 | Syntax = case Opts of 109 | #{parse_syntax:=UserSyntax} -> 110 | maps:merge(UserSyntax, nkpacket_syntax:syntax()); 111 | _ -> 112 | nkpacket_syntax:syntax() 113 | end, 114 | case nklib_syntax:parse(Opts, Syntax) of 115 | {ok, Map, _} -> 116 | case maps:is_key(id, Map) of 117 | true -> 118 | {ok, Map}; 119 | false -> 120 | {ok, Map#{id=>nklib_util:uid()}} 121 | end; 122 | {error, Error} -> 123 | {error, Error} 124 | end. 125 | 126 | 127 | %% @private 128 | -spec parse_uri_opts(map()|list(), #{parse_syntax=>map()}) -> 129 | {ok, map()} | {error, term()}. 130 | 131 | parse_uri_opts(UriOpts, Opts) -> 132 | Base = maps:get(parse_syntax, Opts, #{}), 133 | Syntax = maps:merge(Base, nkpacket_syntax:safe_syntax()), 134 | case nklib_syntax:parse(UriOpts, Syntax) of 135 | {ok, Map, _} -> 136 | {ok, Map}; 137 | {error, Error} -> 138 | {error, Error} 139 | end. 140 | 141 | 142 | %% @doc Get all local network ips. 143 | -spec get_local_ips() -> 144 | [inet:ip_address()]. 145 | 146 | get_local_ips() -> 147 | {ok, All} = inet:getifaddrs(), 148 | lists:flatten([proplists:get_all_values(addr, Data) || {_, Data} <- All]). 149 | 150 | 151 | %% @doc Equivalent to `find_main_ip(auto, ipv4)'. 152 | -spec find_main_ip() -> 153 | inet:ip_address(). 154 | 155 | find_main_ip() -> 156 | find_main_ip(auto, ipv4). 157 | 158 | 159 | %% @doc Finds the best local IP. 160 | %% If a network interface is supplied (as "en0") it returns its ip. 161 | %% If `auto' is used, probes `ethX' and `enX' interfaces. If none is available returns 162 | %% localhost 163 | -spec find_main_ip(auto|string(), ipv4|ipv6) -> 164 | inet:ip_address(). 165 | 166 | find_main_ip(NetInterface, Type) -> 167 | {ok, All} = inet:getifaddrs(), 168 | case NetInterface of 169 | auto -> 170 | IFaces = lists:filter( 171 | fun(Name) -> 172 | case Name of 173 | "eth" ++ _ -> true; 174 | "en" ++ _ -> true; 175 | _ -> false 176 | end 177 | end, 178 | proplists:get_keys(All)), 179 | find_main_ip(lists:sort(IFaces), All, Type); 180 | _ -> 181 | find_main_ip([NetInterface], All, Type) 182 | end. 183 | 184 | 185 | %% @private 186 | find_main_ip([], _, ipv4) -> 187 | {127,0,0,1}; 188 | 189 | find_main_ip([], _, ipv6) -> 190 | {0,0,0,0,0,0,0,1}; 191 | 192 | find_main_ip([IFace|R], All, Type) -> 193 | Data = nklib_util:get_value(IFace, All, []), 194 | Flags = nklib_util:get_value(flags, Data, []), 195 | case lists:member(up, Flags) andalso lists:member(running, Flags) of 196 | true -> 197 | Addrs = lists:zip( 198 | proplists:get_all_values(addr, Data), 199 | proplists:get_all_values(netmask, Data)), 200 | case find_real_ip(Addrs, Type) of 201 | error -> find_main_ip(R, All, Type); 202 | Ip -> Ip 203 | end; 204 | false -> 205 | find_main_ip(R, All, Type) 206 | end. 207 | 208 | %% @private 209 | find_real_ip([], _Type) -> 210 | error; 211 | 212 | % Skip link-local addresses 213 | find_real_ip([{{65152,_,_,_,_,_,_,_}, _Netmask}|R], Type) -> 214 | find_real_ip(R, Type); 215 | 216 | find_real_ip([{{A,B,C,D}, Netmask}|_], ipv4) 217 | when Netmask /= {255,255,255,255} -> 218 | {A,B,C,D}; 219 | 220 | find_real_ip([{{A,B,C,D,E,F,G,H}, Netmask}|_], ipv6) 221 | when Netmask /= {65535,65535,65535,65535,65535,65535,65535,65535} -> 222 | {A,B,C,D,E,F,G,H}; 223 | 224 | find_real_ip([_|R], Type) -> 225 | find_real_ip(R, Type). 226 | 227 | 228 | %% @private 229 | -spec init_protocol(nkpacket:protocol(), atom(), term()) -> 230 | {ok, undefined} | term(). 231 | 232 | init_protocol(Protocol, Fun, Arg) -> 233 | case 234 | Protocol/=undefined andalso 235 | erlang:function_exported(Protocol, Fun, 1) 236 | of 237 | false -> 238 | {ok, undefined}; 239 | true -> 240 | TryFun = fun() -> Protocol:Fun(Arg) end, 241 | case nklib_util:do_try(TryFun) of 242 | {exception, {Class, {Reason, Stacktrace}}} -> 243 | lager:error("Exception ~p (~p) calling ~p:~p(~p). Stack: ~p", 244 | [Class, Reason, Protocol, Fun, Arg, Stacktrace]), 245 | erlang:Class([{reason, Reason}, {stacktrace, Stacktrace}]); 246 | Other -> 247 | Other 248 | end 249 | end. 250 | 251 | 252 | %% @private 253 | -spec call_protocol(atom(), list(), tuple(), integer()) -> 254 | {atom(), tuple()} | {atom(), term(), tuple()} | undefined. 255 | 256 | call_protocol(Fun, Args, State, Pos) -> 257 | Protocol = element(Pos, State), 258 | ProtoState = element(Pos+1, State), 259 | case 260 | Protocol/=undefined andalso 261 | erlang:function_exported(Protocol, Fun, length(Args)+1) 262 | of 263 | 264 | false when Fun==conn_handle_call; Fun==conn_handle_cast; 265 | Fun==conn_handle_info; Fun==listen_handle_call; 266 | Fun==listen_handle_cast; Fun==listen_handle_info -> 267 | lager:error("Module ~p received unexpected ~p: ~p", [?MODULE, Fun, Args]), 268 | undefined; 269 | false -> 270 | undefined; 271 | true -> 272 | TryFun = fun() -> 273 | case apply(Protocol, Fun, Args++[ProtoState]) of 274 | ok -> 275 | {ok, State}; 276 | {Class, ProtoState1} when is_atom(Class) -> 277 | {Class, setelement(Pos+1, State, ProtoState1)}; 278 | {Class, Value, ProtoState1} when is_atom(Class) -> 279 | {Class, Value, setelement(Pos+1, State, ProtoState1)} 280 | end 281 | end, 282 | case nklib_util:do_try(TryFun) of 283 | {exception, {EClass, {Reason, Stacktrace}}} -> 284 | lager:error("Exception ~p (~p) calling ~p:~p(~p). Stack: ~p", 285 | [EClass, Reason, Protocol, Fun, Args, Stacktrace]), 286 | erlang:EClass([{reason, Reason}, {stacktrace, Stacktrace}]); 287 | Other -> 288 | Other 289 | end 290 | end. 291 | 292 | 293 | %% @doc Gets a binary represtation of an uri based on local address 294 | -spec get_local_uri(term(), nkpacket:nkport()) -> 295 | binary(). 296 | 297 | get_local_uri(Scheme, #nkport{transp=Transp, local_ip=Ip, local_port=Port}) -> 298 | get_uri(Scheme, Transp, Ip, Port). 299 | 300 | 301 | %% @doc Gets a binary represtation of an uri based on remote address 302 | -spec get_remote_uri(term(), nkpacket:nkport()) -> 303 | binary(). 304 | 305 | get_remote_uri(Scheme, #nkport{transp=Transp, remote_ip=Ip, remote_port=Port}) -> 306 | get_uri(Scheme, Transp, Ip, Port). 307 | 308 | 309 | %% @private 310 | get_uri(Scheme, Transp, Ip, Port) -> 311 | list_to_binary([ 312 | "<", nklib_util:to_binary(Scheme), "://", nklib_util:to_host(Ip), ":", 313 | nklib_util:to_binary(Port), ";transport=", nklib_util:to_binary(Transp), ">" 314 | ]). 315 | 316 | 317 | %%%% @doc Removes the user part from a nkport() 318 | %%-spec remove_user(nkpacket:nkport()) -> 319 | %% nkpacket:nkport(). 320 | %% 321 | %%remove_user(#nkport{meta=#{user:=_}=Meta}=NkPort) -> 322 | %% NkPort#nkport{meta=maps:remove(user, Meta)}; 323 | %% 324 | %%remove_user(NkPort) -> 325 | %% NkPort. 326 | 327 | 328 | %% @private 329 | norm_path(any) -> 330 | []; 331 | 332 | norm_path(<<>>) -> 333 | []; 334 | 335 | norm_path(<<"/">>) -> 336 | []; 337 | 338 | norm_path(Path) when is_binary(Path) -> 339 | case binary:split(nklib_util:to_binary(Path), <<"/">>, [global]) of 340 | [<<>> | Rest] -> Rest; 341 | Other -> Other 342 | end; 343 | 344 | norm_path(Other) -> 345 | norm_path(nklib_util:to_binary(Other)). 346 | 347 | 348 | %% @doc 349 | join_path(Base, Path) -> 350 | Base2 = nklib_util:to_binary(Base), 351 | Base3 = case byte_size(Base2) of 352 | BaseSize when BaseSize > 0 -> 353 | case binary:at(Base2, BaseSize-1) of 354 | $/ when BaseSize > 1 -> 355 | binary:part(Base2, 0, BaseSize-1); 356 | _ when Base2 == <<"/">> -> 357 | <<>>; 358 | _ -> 359 | Base2 360 | end; 361 | 0 -> 362 | <<>> 363 | end, 364 | Path2 = case nklib_util:to_binary(Path) of 365 | <<$/, Rest/binary>> -> 366 | Rest; 367 | BinPath -> 368 | BinPath 369 | end, 370 | <>. 371 | 372 | 373 | %% @doc 374 | conn_string(Transp, Ip, Port) -> 375 | << 376 | (nklib_util:to_binary(Transp))/binary, ":", 377 | (nklib_util:to_host(Ip))/binary, ":", 378 | (nklib_util:to_binary(Port))/binary 379 | >>. 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | %% =================================================================== 389 | %% Tests 390 | %% ================================================================= 391 | 392 | 393 | %-define(TEST, true). 394 | -ifdef(TEST). 395 | -include_lib("eunit/include/eunit.hrl"). 396 | 397 | path_test() -> 398 | <<"/">> = join_path("", ""), 399 | <<"/">> = join_path("/", ""), 400 | <<"/">> = join_path("", "/"), 401 | <<"/">> = join_path("/", "/"), 402 | <<"a/">> = join_path("a", ""), 403 | <<"a/">> = join_path("a/", ""), 404 | <<"a/">> = join_path("a", "/"), 405 | <<"a/">> = join_path("a/", "/"), 406 | <<"a/b">> = join_path("a", "b"), 407 | <<"a/b">> = join_path("a", "/b"), 408 | <<"/b">> = join_path("", "b"), 409 | <<"/b">> = join_path("", "/b"), 410 | ok. 411 | 412 | 413 | -endif. 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | -------------------------------------------------------------------------------- /src/nkpacket_transport_sctp.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2019 Carlos Gonzalez Florido. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %% @private SCTP Transport. 22 | -module(nkpacket_transport_sctp). 23 | -author('Carlos Gonzalez '). 24 | -behaviour(gen_server). 25 | 26 | -export([get_listener/1, connect/1]). 27 | -export([start_link/1, init/1, terminate/2, code_change/3, handle_call/3, 28 | handle_cast/2, handle_info/2]). 29 | 30 | -include("nkpacket.hrl"). 31 | -include_lib("kernel/include/inet_sctp.hrl"). 32 | 33 | %% To get debug info, start with debug=>true 34 | 35 | -define(DEBUG(Txt, Args), 36 | case get(nkpacket_debug) of 37 | true -> ?LLOG(debug, Txt, Args); 38 | _ -> ok 39 | end). 40 | 41 | -define(LLOG(Type, Txt, Args), lager:Type("NkPACKET SCTP "++Txt, Args)). 42 | 43 | 44 | %% =================================================================== 45 | %% Private 46 | %% =================================================================== 47 | 48 | %% @private Starts a new listening server 49 | -spec get_listener(nkpacket:nkport()) -> 50 | supervisor:child_spec(). 51 | 52 | get_listener(#nkport{id=Id, listen_ip=Ip, listen_port=Port, transp=sctp}=NkPort) -> 53 | Str = nkpacket_util:conn_string(sctp, Ip, Port), 54 | #{ 55 | id => {Id, Str}, 56 | start => {?MODULE, start_link, [NkPort]}, 57 | restart => transient, 58 | shutdown => 5000, 59 | type => worker, 60 | modules => [?MODULE] 61 | }. 62 | 63 | 64 | 65 | %% @private Starts a new connection to a remote server 66 | -spec connect(nkpacket:nkport()) -> 67 | {ok, #nkport{}} | {error, term()}. 68 | 69 | connect(#nkport{transp=sctp, pid=Pid}=NkPort) -> 70 | case catch gen_server:call(Pid, {nkpacket_connect, NkPort}, 180000) of 71 | {ok, NkPort2} -> 72 | {ok, NkPort2}; 73 | {error, Error} -> 74 | {error, Error}; 75 | {'EXIT', Error} -> 76 | {error, Error} 77 | end. 78 | 79 | 80 | 81 | %% =================================================================== 82 | %% gen_server 83 | %% =================================================================== 84 | 85 | 86 | %% @private 87 | start_link(NkPort) -> 88 | gen_server:start_link(?MODULE, [NkPort], []). 89 | 90 | 91 | -record(state, { 92 | nkport :: nkpacket:nkport(), 93 | socket :: port(), 94 | pending_froms :: [{{inet:ip_address(), inet:port_number()}, {pid(), term()}, map()}], 95 | pending_conns :: [pid()], 96 | protocol :: nkpacket:protocol(), 97 | proto_state :: term(), 98 | monitor_ref :: reference() 99 | }). 100 | 101 | 102 | %% @private 103 | -spec init(term()) -> 104 | {ok, #state{}} | {stop, term()}. 105 | 106 | init([NkPort]) -> 107 | #nkport{ 108 | class = Class, 109 | transp = sctp, 110 | listen_ip = Ip, 111 | listen_port = Port, 112 | protocol = Protocol, 113 | opts = Meta 114 | } = NkPort, 115 | process_flag(priority, high), 116 | process_flag(trap_exit, true), %% Allow calls to terminate/2 117 | Debug = maps:get(debug, Meta, false), 118 | put(nkpacket_debug, Debug), 119 | ListenOpts = listen_opts(NkPort), 120 | case nkpacket_transport:open_port(NkPort, ListenOpts) of 121 | {ok, Socket} -> 122 | {ok, Port1} = inet:port(Socket), 123 | NkPort1 = NkPort#nkport{ 124 | local_ip = Ip, 125 | local_port = Port1, 126 | listen_port = Port1, 127 | pid = self(), 128 | socket = {Socket, 0} 129 | }, 130 | ok = gen_sctp:listen(Socket, true), 131 | nkpacket_util:register_listener(NkPort), 132 | ConnMeta = maps:with(?CONN_LISTEN_OPTS, Meta), 133 | ConnPort = NkPort1#nkport{opts=ConnMeta}, 134 | ListenType = case size(Ip) of 135 | 4 -> nkpacket_listen4; 136 | 8 -> nkpacket_listen6 137 | end, 138 | nklib_proc:put({ListenType, Class, Protocol, sctp}, ConnPort), 139 | {ok, ProtoState} = nkpacket_util:init_protocol(Protocol, listen_init, NkPort1), 140 | MonRef = case Meta of 141 | #{monitor:=UserPid} -> erlang:monitor(process, UserPid); 142 | _ -> undefined 143 | end, 144 | State = #state{ 145 | nkport = ConnPort, 146 | socket = Socket, 147 | pending_froms = [], 148 | pending_conns = [], 149 | protocol = Protocol, 150 | proto_state = ProtoState, 151 | monitor_ref = MonRef 152 | }, 153 | {ok, State}; 154 | {error, Error} -> 155 | ?LLOG(error, "could not start SCTP transport on ~p:~p (~p)", 156 | [Ip, Port, Error]), 157 | {stop, Error} 158 | end. 159 | 160 | 161 | %% @private 162 | -spec handle_call(term(), {pid(), term()}, #state{}) -> 163 | {reply, term(), #state{}} | {noreply, #state{}} | 164 | {stop, term(), #state{}} | {stop, term(), term(), #state{}}. 165 | 166 | handle_call({nkpacket_connect, ConnPort}, From, State) -> 167 | #nkport{ 168 | remote_ip = Ip, 169 | remote_port = Port, 170 | opts = Meta 171 | } = ConnPort, 172 | #state{ 173 | socket = Socket, 174 | pending_froms = Froms, 175 | pending_conns = Conns 176 | } = State, 177 | Timeout = case maps:get(connect_timeout, Meta, undefined) of 178 | undefined -> 179 | nkpacket_config:connect_timeout(); 180 | Timeout0 -> 181 | Timeout0 182 | end, 183 | Self = self(), 184 | Fun = fun() -> 185 | case catch gen_sctp:connect_init(Socket, Ip, Port, [], Timeout) of 186 | ok -> 187 | % Socket process will receive the SCTP up message 188 | ok; 189 | {error, Error} -> 190 | gen_server:reply(From, {error, Error}), 191 | gen_server:cast(Self, {nkpacket_connection_error, From}); 192 | Error -> 193 | gen_server:reply(From, {error, Error}), 194 | gen_server:cast(Self, {nkpacket_connection_error, From}) 195 | end 196 | end, 197 | ConnPid = spawn_link(Fun), 198 | State2 = State#state{ 199 | pending_froms = [{{Ip, Port}, From, Meta}|Froms], 200 | pending_conns = [ConnPid|Conns] 201 | }, 202 | {noreply, State2}; 203 | 204 | handle_call({nkpacket_apply_nkport, Fun}, _From, #state{nkport=NkPort}=State) -> 205 | {reply, Fun(NkPort), State}; 206 | 207 | handle_call(nkpacket_stop, _From, State) -> 208 | {stop, normal, ok, State}; 209 | 210 | handle_call(Msg, From, #state{nkport=NkPort}=State) -> 211 | case call_protocol(listen_handle_call, [Msg, From, NkPort], State) of 212 | undefined -> 213 | {noreply, State}; 214 | {ok, State1} -> 215 | {noreply, State1}; 216 | {stop, Reason, State1} -> 217 | {stop, Reason, State1} 218 | end. 219 | 220 | 221 | %% @private 222 | -spec handle_cast(term(), #state{}) -> 223 | {noreply, #state{}} | {stop, term(), #state{}}. 224 | 225 | handle_cast({nkpacket_connection_error, From}, #state{pending_froms=Froms}=State) -> 226 | Froms1 = lists:keydelete(From, 2, Froms), 227 | {noreply, State#state{pending_froms=Froms1}}; 228 | 229 | handle_cast(nkpacket_stop, State) -> 230 | {stop, normal, State}; 231 | 232 | handle_cast(Msg, #state{nkport=NkPort}=State) -> 233 | case call_protocol(listen_handle_cast, [Msg, NkPort], State) of 234 | undefined -> {noreply, State}; 235 | {ok, State1} -> {noreply, State1}; 236 | {stop, Reason, State1} -> {stop, Reason, State1} 237 | end. 238 | 239 | 240 | %% @private 241 | -spec handle_info(term(), #state{}) -> 242 | {noreply, #state{}} | {stop, term(), #state{}}. 243 | 244 | handle_info({sctp, Socket, Ip, Port, {Anc, SAC}}, State) -> 245 | #state{socket=Socket, nkport=NkPort} = State, 246 | #nkport{class=Class, protocol=Proto} = NkPort, 247 | State1 = case SAC of 248 | #sctp_assoc_change{state=comm_up, assoc_id=AssocId} -> 249 | ?DEBUG("COMM_UP: ~p", [AssocId]), 250 | #state{pending_froms=Froms} = State, 251 | case lists:keytake({Ip, Port}, 1, Froms) of 252 | {value, {_, From, Meta}, Froms1} -> 253 | Reply = case do_connect(Ip, Port, AssocId, Meta, State) of 254 | {ok, Pid} -> 255 | {ok, NkPort#nkport{pid=Pid}}; 256 | {error, Error} -> 257 | {error, Error} 258 | end, 259 | gen_server:reply(From, Reply), 260 | State#state{pending_froms=Froms1}; 261 | false -> 262 | State 263 | end; 264 | #sctp_assoc_change{state=shutdown_comp, assoc_id=AssocId} -> 265 | ?DEBUG("COMM_DOWN: ~p", [AssocId]), 266 | Conn = #nkconn{protocol=Proto, transp=sctp, ip=Ip, port=Port, opts=#{class=>Class}}, 267 | case nkpacket_transport:get_connected(Conn) of 268 | [Pid|_] -> nkpacket_connection:stop(Pid, normal); 269 | _ -> ok 270 | end, 271 | State; 272 | #sctp_paddr_change{} -> 273 | % We don't support address change yet 274 | State; 275 | #sctp_shutdown_event{assoc_id=_AssocId} -> 276 | % Should be already processed 277 | State; 278 | Data when is_binary(Data) -> 279 | [#sctp_sndrcvinfo{assoc_id=AssocId}] = Anc, 280 | case do_connect(Ip, Port, AssocId, State) of 281 | {ok, Pid} when is_pid(Pid) -> 282 | nkpacket_connection:incoming(Pid, Data); 283 | {error, Error} -> 284 | ?LLOG(info, "error ~p on SCTP connection up", [Error]) 285 | end, 286 | State; 287 | Other -> 288 | ?LLOG(info, "SCTP unknown data from ~p, ~p: ~p", [Ip, Port, Other]), 289 | State 290 | end, 291 | ok = inet:setopts(Socket, [{active, once}]), 292 | {noreply, State1}; 293 | 294 | handle_info({'DOWN', MRef, process, _Pid, _Reason}, #state{monitor_ref=MRef}=State) -> 295 | {stop, normal, State}; 296 | 297 | handle_info({'EXIT', Pid, _Status}=Msg, #state{pending_conns=Conns}=State) -> 298 | case lists:member(Pid, Conns) of 299 | true -> 300 | {noreply, State#state{pending_conns=Conns--[Pid]}}; 301 | false -> 302 | #state{nkport=NkPort} = State, 303 | case call_protocol(listen_handle_info, [Msg, NkPort], State) of 304 | undefined -> {noreply, State}; 305 | {ok, State1} -> {noreply, State1}; 306 | {stop, Reason, State1} -> {stop, Reason, State1} 307 | end 308 | end; 309 | 310 | handle_info(Msg, #state{nkport=NkPort}=State) -> 311 | case call_protocol(listen_handle_info, [Msg, NkPort], State) of 312 | undefined -> {noreply, State}; 313 | {ok, State1} -> {noreply, State1}; 314 | {stop, Reason, State1} -> {stop, Reason, State1} 315 | end. 316 | 317 | 318 | %% @private 319 | -spec code_change(term(), #state{}, term()) -> 320 | {ok, #state{}}. 321 | 322 | code_change(_OldVsn, State, _Extra) -> 323 | {ok, State}. 324 | 325 | 326 | %% @private 327 | -spec terminate(term(), #state{}) -> 328 | ok. 329 | 330 | terminate(Reason, #state{nkport=NkPort, socket=Socket}=State) -> 331 | ?DEBUG("server process stopped", []), 332 | catch call_protocol(listen_stop, [Reason, NkPort], State), 333 | gen_sctp:close(Socket). 334 | 335 | 336 | 337 | %% =================================================================== 338 | %% Internal 339 | %% =================================================================== 340 | 341 | 342 | -spec listen_opts(#nkport{}) -> 343 | list(). 344 | 345 | listen_opts(#nkport{listen_ip=Ip, opts=Meta}) -> 346 | Timeout = case maps:get(idle_timeout, Meta, undefined) of 347 | undefined -> nkpacket_config:sctp_timeout(); 348 | Timeout0 -> Timeout0 349 | end, 350 | OutStreams = case maps:get(sctp_out_streams, Meta, undefined) of 351 | undefined -> nkpacket_config:sctp_out_streams(); 352 | OS -> OS 353 | end, 354 | InStreams = case maps:get(sctp_in_streams, Meta, undefined) of 355 | undefined -> nkpacket_config:sctp_in_streams(); 356 | IS -> IS 357 | end, 358 | [ 359 | binary, {reuseaddr, true}, {ip, Ip}, {active, once}, 360 | {sctp_initmsg, #sctp_initmsg{num_ostreams=OutStreams, max_instreams=InStreams}}, 361 | {sctp_autoclose, Timeout}, 362 | {sctp_default_send_param, #sctp_sndrcvinfo{stream=0, flags=[unordered]}} 363 | ]. 364 | 365 | 366 | %% @private 367 | do_connect(Ip, Port, AssocId, State) -> 368 | do_connect(Ip, Port, AssocId, undefined, State). 369 | 370 | 371 | %% @private 372 | do_connect(Ip, Port, AssocId, Meta, State) -> 373 | #state{nkport=NkPort, socket=Socket} = State, 374 | #nkport{class=Class, protocol=Proto, opts=ListenMeta} = NkPort, 375 | Conn = #nkconn{protocol=Proto, transp=sctp, ip=Ip, port=Port, opts=#{class=>Class}}, 376 | case nkpacket_transport:get_connected(Conn) of 377 | [Pid|_] -> 378 | {ok, Pid}; 379 | [] -> 380 | Meta2 = case Meta of 381 | undefined -> 382 | ListenMeta; 383 | _ -> 384 | maps:merge(ListenMeta, Meta) 385 | end, 386 | NkPort2 = NkPort#nkport{ 387 | remote_ip = Ip, 388 | remote_port = Port, 389 | socket = {Socket, AssocId}, 390 | opts = Meta2 391 | }, 392 | % Connection will monitor us using nkport's pid 393 | nkpacket_connection:start(NkPort2) 394 | end. 395 | 396 | 397 | %% @private 398 | call_protocol(Fun, Args, State) -> 399 | nkpacket_util:call_protocol(Fun, Args, State, #state.protocol). 400 | -------------------------------------------------------------------------------- /src/nkpacket_stun.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2019 Carlos Gonzalez Florido. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %% @doc RFC5389 STUN utility functions. 22 | %% This module implements several functions to help sending and receiving 23 | %% STUN requests and responses. Only Binding method is supported. 24 | 25 | -module(nkpacket_stun). 26 | -author('Carlos Gonzalez '). 27 | 28 | -export([ext_ip/0, ext_ip/1, get_stun_servers/0, get_stun_servers/1]). 29 | -export([decode/1, binding_request/0, binding_response/3]). 30 | 31 | -export_type([class/0, method/0, attribute/0]). 32 | 33 | -include_lib("nklib/include/nklib.hrl"). 34 | 35 | %% =================================================================== 36 | %% Types 37 | %% =================================================================== 38 | 39 | -type class() :: request | indication | response | error. 40 | 41 | -type method() :: binding | unknown. 42 | 43 | -type attribute() :: 44 | {mapped_address, {inet:ip_address(), inet:port_number()}} | {username, binary()} | 45 | {message_integrity, binary()} | {error_code, binary()} | 46 | {unknown_attributes, binary()} | {realm, binary()} | {nonce, binary()} | 47 | {xor_mapped_address, {inet:ip4_address(), inet:port_number()}} | {software, binary()} | 48 | {alternate_server, binary()} | {fingerprint, binary()} | 49 | {{unknown, integer()}, binary()} | {{error, integer()}, binary()}. 50 | 51 | 52 | 53 | %% =================================================================== 54 | %% Public 55 | %% =================================================================== 56 | 57 | 58 | %% @doc Uses a known list of Stun servers to find external ip 59 | -spec ext_ip() -> 60 | inet:ip_address(). 61 | 62 | ext_ip() -> 63 | ext_ip(nklib_util:randomize(stun_servers())). 64 | 65 | 66 | %% @doc Finds external IP with supplied host or list of hosts 67 | -spec ext_ip([binary()|nklib:user_uri()]) -> 68 | inet:ip_address(). 69 | 70 | ext_ip([]) -> 71 | {127,0,0,1}; 72 | 73 | ext_ip([Uri|Rest]) -> 74 | case get_stun_servers([Uri]) of 75 | {{127,0,0,1}, _List} -> ext_ip(Rest); 76 | {ExtIp, _List} -> ExtIp 77 | end. 78 | 79 | 80 | 81 | %% @doc Get external IP and a list of working stun servers 82 | -spec get_stun_servers() -> 83 | {ExtIp::inet:ip_address(), [{inet:ip_address(), inet:ip_port()}]} | error. 84 | 85 | get_stun_servers() -> 86 | get_stun_servers(stun_servers()). 87 | 88 | 89 | %% @doc Get external IP and a list of working stun servers 90 | -spec get_stun_servers([binary()|nklib:user_uri()]) -> 91 | {ExtIp::inet:ip_address(), [{inet:ip_address(), inet:ip_port()}]} | error. 92 | 93 | get_stun_servers(List) -> 94 | {ok, Socket} = gen_udp:open(0, [binary, {active, false}]), 95 | {ok, {_LocalIp, LocalPort}} = inet:sockname(Socket), 96 | Res = get_stun_servers(List, Socket, LocalPort, []), 97 | gen_udp:close(Socket), 98 | Res. 99 | 100 | 101 | 102 | 103 | %% @doc Decodes a STUN packet 104 | -spec decode(Packet::binary()) -> 105 | {Class::class(), Method::method(), Id::binary(), 106 | Attributes::[attribute()]} | error. 107 | 108 | decode(<<0:2, M1:5, C1:1, M2:3, C2:1, M3:4, Length:16, 16#2112A442:32, 109 | Id:96, Msg/binary>>) 110 | when byte_size(Msg)==Length -> 111 | Class = case <> of 112 | <<2#00:2>> -> request; 113 | <<2#01:2>> -> indication; 114 | <<2#10:2>> -> response; 115 | <<2#11:2>> -> error 116 | end, 117 | Method = case <<0:4, M1:5, M2:3, M3:4>> of 118 | <<16#0001:16>> -> binding; 119 | _ -> unknown 120 | end, 121 | {Class, Method, <>, attributes(Msg)}; 122 | 123 | decode(_) -> 124 | error. 125 | 126 | 127 | %% @doc Generates a Binding request. 128 | -spec binding_request() -> 129 | {Id::binary(), Msg::binary()}. 130 | 131 | binding_request() -> 132 | Id = crypto:strong_rand_bytes(12), 133 | {Id, <<16#0001:16, 0:16, 16#2112A442:32, Id/binary>>}. 134 | 135 | 136 | %% @doc Generates a Binding response. 137 | -spec binding_response(Id::binary(), Ip::inet:ip4_address(), Port::inet:port_number()) -> 138 | Packet::binary(). 139 | 140 | binding_response(Id, {I1, I2, I3, I4}, P) -> 141 | case binary:encode_unsigned(P) of 142 | <> -> ok; 143 | <> -> P1 = 0 144 | end, 145 | <> = crypto:exor(<>, <<33, 18>>), 146 | <> = crypto:exor(<>, <<33, 18, 164, 66>>), 147 | % Binding, 12 bytes, Cookie, Id, xor_mapping_address, 8 bytes, ipv4 (1), port, ip 148 | <<16#0101:16, 12:16, 16#2112A442:32, Id/binary, 149 | 16#0020:16, 8:16, 1:16, XP1, XP2, XI1, XI2, XI3, XI4>>. 150 | 151 | 152 | %% =================================================================== 153 | %% Internal 154 | %% =================================================================== 155 | 156 | %% @private Process a list of STUN attributes 157 | -spec attributes(binary()) -> 158 | [attribute()]. 159 | 160 | attributes(Msg) -> 161 | attributes(Msg, []). 162 | 163 | attributes(<>, Acc) -> 164 | Total = Length + case Length rem 4 of 0 -> 0; 1 ->3; 2->2; 3->1 end, 165 | case byte_size(Rest) >= Total of 166 | true -> 167 | {Data, _} = split_binary(Rest, Length), 168 | {_, Rest1} = split_binary(Rest, Total), 169 | {Class, Value} = case Type of 170 | % Must-process attributes 171 | 16#0001 -> 172 | case Data of 173 | <<1:16, Port:16, A:8, B:8, C:8, D:8>> -> 174 | {mapped_address, {{A,B,C,D}, Port}}; 175 | <<2:16, Port:16, A:16, B:16, C:16, D:16, 176 | E:16, F:16, G:16, H:16>> -> 177 | {mapped_address, {{A,B,C,D,E,F,G,H}, Port}}; 178 | _ -> 179 | {{error, Type}, Data} 180 | end; 181 | 16#0006 -> {username, Data}; 182 | 16#0008 -> {message_integrity, Data}; 183 | 16#0009 -> {error_code, Data}; 184 | 16#000A -> {unknown_attributes, Data}; 185 | 16#0014 -> {realm, Data}; 186 | 16#0015 -> {nonce, Data}; 187 | 16#0020 -> 188 | case Data of 189 | <<1:16, PA:8, PB:8, IA:8, IB:8, IC:8, ID:8>> -> 190 | P1 = crypto:exor(<>, <<33,18>>), 191 | P2 = binary:decode_unsigned(P1), 192 | <> 193 | = crypto:exor(<>, <<33,18,164,66>>), 194 | {xor_mapped_address, {{A,B,C,D}, P2}}; 195 | % <<2:16, Port:16, Ip:128>> -> 196 | % % IPv6 NOT DONE YET 197 | % {xor_mapped_address, {Ip, Port}}; 198 | _ -> 199 | {{error, Type}, Data} 200 | end; 201 | % Optional attributes 202 | 16#8022 -> {software, Data}; 203 | 16#8023 -> {alternate_server, Data}; 204 | 16#8028 -> {fingerprint, Data}; 205 | _ -> {{unknown, Type}, Data} 206 | end, 207 | attributes(Rest1, [{Class, Value}|Acc]); 208 | false -> 209 | lists:reverse(Acc) 210 | end; 211 | 212 | attributes(_, Acc) -> 213 | lists:reverse(Acc). 214 | 215 | 216 | %% @private 217 | get_stun_servers([], _Socket, _Local, List) -> 218 | ExtIps = [ExtIp || {ExtIp, _Type, _StunIp, _StunPort, _Time} <- List], 219 | case lists:usort(ExtIps) of 220 | [ExtIp] -> 221 | ok; 222 | [ExtIp|_] = All -> 223 | lager:error("STUN multiple external IPs!!: ~p", [All]); 224 | [] -> 225 | ExtIp = {127,0,0,1}, 226 | lager:notice("STUN could not find external IP!!") 227 | end, 228 | case lists:keymember(port_changed, 2, List) of 229 | true -> 230 | lager:warning("Current NAT is changing ports!"); 231 | false -> 232 | ok 233 | end, 234 | Stuns = [{StunIp, StunPort} 235 | || {_ExtIp, _Type, StunIp, StunPort, _Time} <- lists:keysort(5, List)], 236 | {ExtIp, Stuns}; 237 | 238 | get_stun_servers([Uri|Rest], Socket, Local, Acc) -> 239 | {Host, Port} = case nklib_parse:uris(Uri) of 240 | error -> 241 | {nklib_util:to_list(Uri), 0}; 242 | [#uri{domain=Host0, port=Port0}|_] -> 243 | {nklib_util:to_list(Host0), Port0} 244 | end, 245 | Ips = case inet:getaddrs(Host, inet) of 246 | {ok, Ips0} -> 247 | Ips0; 248 | {error, _} -> 249 | case inet:getaddrs(Host, inet6) of 250 | {ok, Ips1} -> Ips1; 251 | {error, _} -> [] 252 | end 253 | end, 254 | case Ips of 255 | [] -> 256 | lager:notice("Skipping STUN ~s", [Host]), 257 | get_stun_servers(Rest, Socket, Local, Acc); 258 | _ -> 259 | lager:info("Checking STUN ~s", [Host]), 260 | Acc2 = check_stun_server(Ips, Port, Socket, Local, Acc), 261 | get_stun_servers(Rest, Socket, Local, Acc2) 262 | end. 263 | 264 | 265 | %% @private 266 | check_stun_server([], _Port, _Socket, _Local, Acc) -> 267 | Acc; 268 | 269 | check_stun_server([Ip|Rest], Port, Socket, LocalPort, Acc) -> 270 | {Id, Request} = binding_request(), 271 | Start = nklib_util:l_timestamp(), 272 | Port2 = case Port of 273 | 0 -> 3478; 274 | _ -> Port 275 | end, 276 | Acc2 = case send_and_recv(Socket, Ip, Port2, Request) of 277 | {ok, {_, _, Raw}} -> 278 | case decode(Raw) of 279 | {response, binding, Id, Data} -> 280 | Time = (nklib_util:l_timestamp() - Start) div 1000, 281 | % lager:info("STUN server ~p: ~p msecs", [Ip, Time]), 282 | case proplists:get_value(mapped_address, Data) of 283 | {RemoteIp, LocalPort} -> 284 | [{RemoteIp, ok, Ip, Port2, Time}|Acc]; 285 | {RemoteIp, _} -> 286 | [{RemoteIp, port_changed, Ip, Port2, Time}|Acc]; 287 | _ -> 288 | Acc 289 | end; 290 | _ -> 291 | Acc 292 | end; 293 | _ -> 294 | Acc 295 | end, 296 | check_stun_server(Rest, Port, Socket, LocalPort, Acc2). 297 | 298 | %% @private 299 | send_and_recv(Socket, Ip, Port, Request) -> 300 | try 301 | ok = gen_udp:send(Socket, Ip, Port, Request), 302 | gen_udp:recv(Socket, 0, 5000) 303 | catch 304 | E:R -> 305 | lager:warning("gen-udp:send errored with ~p, reason ~p", [E, R]), 306 | {E, R} 307 | end. 308 | 309 | %% @private 310 | stun_servers() -> 311 | [ 312 | %"stun:stun.ekiga.net", 313 | "stun:stun.ideasip.com", 314 | % "stun:stun.iptel.org", % Not working 315 | "stun:stun.schlund.de", 316 | "stun:stun.voiparound.com", % same ips as voipbuster.com and voipstunt.com 317 | "stun:stun.freeswitch.org", 318 | "stun:stun.voip.eutelia.it" 319 | ]. 320 | 321 | 322 | 323 | 324 | 325 | %%==================================================================== 326 | %% Eunit tests 327 | %%==================================================================== 328 | 329 | 330 | -ifdef(TEST1). 331 | -include_lib("eunit/include/eunit.hrl"). 332 | 333 | stun_test() -> 334 | M = << 335 | 128,40,0,4,97,98,99,100, 336 | 16#0001:16, 8:16, 1:16, 1234:16, 127, 0, 0, 1, 337 | 16#0006:16, 5:16, 1,2,3,4,5,6,7,8 338 | >>, 339 | ?assertMatch( 340 | [ 341 | {fingerprint, <<"abcd">>}, 342 | {mapped_address, {{127,0,0,1}, 1234}}, 343 | {username, <<1,2,3,4,5>>} 344 | ], 345 | attributes(M)), 346 | {Id, Request} = binding_request(), 347 | ?assertMatch({request, binding, Id, []}, decode(Request)), 348 | Response = binding_response(Id, {1,2,3,4}, 5), 349 | ?assertMatch({response, binding, Id, [{xor_mapped_address,{{1,2,3,4},5}}]}, 350 | decode(Response)). 351 | 352 | client_test() -> 353 | ServerIp = {216,93,246,18}, % stun.counterpath.net 354 | {ok, Socket} = gen_udp:open(0, [binary, {active, false}]), 355 | {ok, {_LocalIp, LocalPort}} = inet:sockname(Socket), 356 | {Id, Request} = binding_request(), 357 | ?debugFmt("Sending STUN binding request to ~s", [inet_parse:ntoa(ServerIp)]), 358 | ok = gen_udp:send(Socket, ServerIp, 3478, Request), 359 | case gen_udp:recv(Socket, 0, 5000) of 360 | {ok, {_, _, Raw}} -> 361 | case decode(Raw) of 362 | {response, binding, Id, Data} -> 363 | case proplists:get_value(mapped_address, Data) of 364 | {RemoteIp, RemotePort} -> 365 | ?debugFmt("STUN OK (Local: ~p, Remote: ~p, ~p)", 366 | [LocalPort, RemoteIp, RemotePort]); 367 | _ -> 368 | ?debugMsg("STUN: Incorrect response\n") 369 | end; 370 | _ -> 371 | ?debugMsg("STUN: Incorrect response\n") 372 | end; 373 | _ -> 374 | ?debugMsg("STUN: Timeout\n") 375 | end, 376 | gen_udp:close(Socket). 377 | 378 | gen_udp_returns_error_test() -> 379 | meck:new(gen_udp, [unstick, passthrough]), 380 | meck:expect(gen_udp, send, fun(_,_,_,_) -> meck:exception(error, eperm) end), 381 | 382 | Ip = {1,2,3,4}, 383 | {ok, Socket} = gen_udp:open(0, [binary, {active, false}]), 384 | {ok, {_LocalIp, Port}} = inet:sockname(Socket), 385 | {_Id, Request} = binding_request(), 386 | 387 | ?assertMatch({error,eperm}, send_and_recv(Socket, Ip, Port, Request)), 388 | meck:unload(gen_udp). 389 | 390 | -endif. 391 | 392 | 393 | --------------------------------------------------------------------------------