├── 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 |
--------------------------------------------------------------------------------