Skip to content

Commit 4c55f0e

Browse files
committed
Native STOMP: remove amqp_client dependency, rewrite frame parser, binary headers
Remove the AMQP 0-9-1 client dependency from the STOMP plugin, making it a truly native protocol implementation (following the MQTT native pattern). Key changes: - Remove amqp_client from DEPS; replace #amqp_adapter_info{} with #conn_info{} record storing connection endpoints directly via rabbit_net:socket_ends/2 - Replace #'basic.deliver'{} construction with direct parameter passing - Add per-version global counters (STOMP 1.0/1.1/1.2) and per-queue-type initialization, publisher/consumer lifecycle counters - Add force_event_refresh handler, rabbit_trace integration (tap_in/tap_out), consumer timeout handling, vhost existence and connection limit checks - Add structured logger metadata (connection, vhost, user) for traceable logs - Rewrite frame parser: bulk binary scanning for commands and headers (zero-copy fast path), byte-by-byte fallback only for escape sequences, bounded accumulation in all phases, unknown commands produce frames (not parse errors) - Convert all header representation from charlists to binaries throughout: frame parser, serializer, header accessors, rabbit_stomp_util, rabbit_stomp_processor, and all test files - Restructure python_SUITE: auto-generate per-function CT test cases from Python test files, organized into nested groups by class - Fix rabbitmq_cli plugin tests to reflect STOMP's independence from amqp_client - Update rabbit_web_stomp_handler for all API changes
1 parent ec46123 commit 4c55f0e

41 files changed

Lines changed: 3512 additions & 1946 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

deps/rabbit/src/rabbit_confirms.erl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030
-opaque state() :: #?MODULE{}.
3131

3232
-export_type([
33-
state/0
33+
state/0,
34+
mx/0
3435
]).
3536

3637
-spec init() -> state().

deps/rabbitmq_cli/test/plugins/disable_plugins_command_test.exs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -258,23 +258,26 @@ defmodule DisablePluginsCommandTest do
258258
test "disabling a dependency disables all plugins that depend on it", context do
259259
assert {:stream, test_stream} = @command.run(["amqp_client"], context[:opts])
260260
result = Enum.to_list(test_stream)
261-
expected_list = [:rabbitmq_exchange_federation, :rabbitmq_federation, :rabbitmq_federation_common, :rabbitmq_queue_federation, :rabbitmq_stomp]
261+
expected_disabled = [:rabbitmq_exchange_federation, :rabbitmq_federation,
262+
:rabbitmq_federation_common, :rabbitmq_queue_federation]
262263
expected = [
263-
[],
264+
[:rabbitmq_stomp],
264265
%{
265266
mode: :online,
266267
started: [],
267-
stopped: expected_list,
268-
disabled: expected_list,
269-
set: []
268+
stopped: expected_disabled,
269+
disabled: expected_disabled,
270+
set: [:rabbitmq_stomp]
270271
}
271272
]
272273
assert normalize_stream_result(expected) == normalize_stream_result(result)
273274

274-
assert {:ok, [[]]} == :file.consult(context[:opts][:enabled_plugins_file])
275+
assert {:ok, [[:rabbitmq_stomp]]} == :file.consult(context[:opts][:enabled_plugins_file])
275276

277+
# Before native STOMP, this would be empty because STOMP depended on
278+
# amqp_client. Native STOMP does not depend on amqp_client.
276279
result = :rabbit_misc.rpc_call(context[:opts][:node], :rabbit_plugins, :active, [])
277-
assert Enum.empty?(result)
280+
assert Enum.sort(result) == [:rabbitmq_stomp]
278281
end
279282

280283
test "formats enabled plugins mismatch errors", context do

deps/rabbitmq_cli/test/plugins/enable_plugins_command_test.exs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,8 @@ defmodule EnablePluginsCommandTest do
205205
Enum.to_list(test_stream0)
206206

207207
check_plugins_enabled([:rabbitmq_stomp], context)
208-
assert_equal_sets([:amqp_client, :rabbitmq_stomp], currently_active_plugins(context))
208+
# Native STOMP does not depend on amqp_client
209+
assert_equal_sets([:rabbitmq_stomp], currently_active_plugins(context))
209210

210211
{:stream, test_stream1} = @command.run(["rabbitmq_federation"], context[:opts])
211212

deps/rabbitmq_cli/test/plugins/set_plugins_command_test.exs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,8 @@ defmodule SetPluginsCommandTest do
127127

128128
assert {:ok, [[:rabbitmq_stomp]]} = :file.consult(context[:opts][:enabled_plugins_file])
129129

130-
assert [:amqp_client, :rabbitmq_stomp] =
130+
# Native STOMP does not depend on amqp_client
131+
assert [:rabbitmq_stomp] =
131132
Enum.sort(:rabbit_misc.rpc_call(context[:opts][:node], :rabbit_plugins, :active, []))
132133

133134
assert {:stream, test_stream1} = @command.run(["rabbitmq_federation"], context[:opts])

deps/rabbitmq_mqtt/test/protocol_interop_SUITE.erl

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -482,12 +482,12 @@ amqp_mqtt(Qos, Config) ->
482482

483483
mqtt_stomp_mqtt(Config) ->
484484
{ok, StompC0} = stomp_connect(Config),
485-
ok = stomp_send(StompC0, "SUBSCRIBE", [{"destination", "/topic/t.1"},
486-
{"receipt", "my-receipt"},
487-
{"id", "subscription-888"},
488-
{"durable", "true"}]),
489-
{#stomp_frame{command = "RECEIPT",
490-
headers = [{"receipt-id","my-receipt"}]}, StompC1} = stomp_recv(StompC0),
485+
ok = stomp_send(StompC0, 'SUBSCRIBE', [{<<"destination">>, <<"/topic/t.1">>},
486+
{<<"receipt">>, <<"my-receipt">>},
487+
{<<"id">>, <<"subscription-888">>},
488+
{<<"durable">>, <<"true">>}]),
489+
{#stomp_frame{command = 'RECEIPT',
490+
headers = [{<<"receipt-id">>,<<"my-receipt">>}]}, StompC1} = stomp_recv(StompC0),
491491

492492
%% MQTT 5.0 to STOMP 1.2
493493
C = connect(<<"my-mqtt-client">>, Config),
@@ -513,40 +513,40 @@ mqtt_stomp_mqtt(Config) ->
513513
'User-Property' => UserProperty},
514514
RequestPayload, [{qos, 1}]),
515515

516-
{#stomp_frame{command = "MESSAGE",
516+
{#stomp_frame{command = 'MESSAGE',
517517
headers = Headers0,
518-
body_iolist = Body} = Msg1, StompC2} = stomp_recv(StompC1),
518+
body_iolist_rev = BodyRev} = Msg1, StompC2} = stomp_recv(StompC1),
519+
Body = lists:reverse(BodyRev),
519520
?assertEqual(RequestPayload, iolist_to_binary(Body)),
520-
Headers1 = maps:from_list(Headers0),
521-
Headers = maps:map(fun(_K, V) -> unicode:characters_to_binary(V) end, Headers1),
521+
Headers = maps:from_list(Headers0),
522522
ct:pal("Received STOMP 1.2 message:~n~p~n"
523523
"with headers map:~n~p", [Msg1, Headers]),
524524
?assertMatch(
525-
#{"content-type" := ContentType,
526-
"correlation-id" := Correlation,
527-
"destination" := <<"/topic/t.1">>,
525+
#{<<"content-type">> := ContentType,
526+
<<"correlation-id">> := Correlation,
527+
<<"destination">> := <<"/topic/t.1">>,
528528
%% With Native STOMP, this should be translated to
529529
%% reply-to: /topic/response.topic
530-
"x-reply-to-topic" := <<"response.topic">>,
531-
"subscription" := <<"subscription-888">>,
532-
"persistent" := <<"true">>,
530+
<<"x-reply-to-topic">> := <<"response.topic">>,
531+
<<"subscription">> := <<"subscription-888">>,
532+
<<"persistent">> := <<"true">>,
533533
%% The STOMP spec mandates headers to be encoded as UTF-8, but unfortunately the RabbitMQ
534534
%% STOMP implementation (as of 3.13) does not adhere and therefore does not provide UTF-8 support.
535-
% "rabbit🐇" := <<"carrot🥕"/utf8>>,
536-
% "x-rabbit🐇" := <<"carrot🥕"/utf8>>,
537-
"key" := <<"val1">>,
538-
"x-key" := <<"val1">>
535+
% <<"rabbit🐇"/utf8>> := <<"carrot🥕"/utf8>>,
536+
% <<"x-rabbit🐇"/utf8>> := <<"carrot🥕"/utf8>>,
537+
<<"key">> := <<"val1">>,
538+
<<"x-key">> := <<"val1">>
539539
},
540540
Headers),
541541

542542
%% STOMP 1.2 to MQTT 5.0
543-
ok = stomp_send(StompC2, "SEND",
544-
[{"destination", "/topic/response.topic"},
545-
{"persistent", "true"},
546-
{"content-type", "application/json"},
547-
{"correlation-id", binary_to_list(Correlation)},
548-
{"x-key", "val4"}],
549-
["{\"my\" : \"response\"}"]),
543+
ok = stomp_send(StompC2, 'SEND',
544+
[{<<"destination">>, <<"/topic/response.topic">>},
545+
{<<"persistent">>, <<"true">>},
546+
{<<"content-type">>, <<"application/json">>},
547+
{<<"correlation-id">>, Correlation},
548+
{<<"x-key">>, <<"val4">>}],
549+
[<<"{\"my\" : \"response\"}">>]),
550550
ok = stomp_disconnect(StompC2),
551551

552552
receive {publish, MqttMsg} ->
@@ -713,12 +713,12 @@ stomp_connect(Config) ->
713713
Port = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_stomp),
714714
{ok, Sock} = gen_tcp:connect(localhost, Port, [{active, false}, binary]),
715715
Client0 = {Sock, []},
716-
stomp_send(Client0, "CONNECT", [{"accept-version", "1.2"}]),
717-
{#stomp_frame{command = "CONNECTED"}, Client1} = stomp_recv(Client0),
716+
stomp_send(Client0, 'CONNECT', [{<<"accept-version">>, <<"1.2">>}]),
717+
{#stomp_frame{command = 'CONNECTED'}, Client1} = stomp_recv(Client0),
718718
{ok, Client1}.
719719

720720
stomp_disconnect(Client = {Sock, _}) ->
721-
stomp_send(Client, "DISCONNECT"),
721+
stomp_send(Client, 'DISCONNECT'),
722722
gen_tcp:close(Sock).
723723

724724
stomp_send(Client, Command) ->
@@ -729,9 +729,9 @@ stomp_send(Client, Command, Headers) ->
729729

730730
stomp_send({Sock, _}, Command, Headers, Body) ->
731731
Frame = rabbit_stomp_frame:serialize(
732-
#stomp_frame{command = list_to_binary(Command),
733-
headers = Headers,
734-
body_iolist = Body}),
732+
#stomp_frame{command = Command,
733+
headers = Headers,
734+
body_iolist_rev = Body}),
735735
gen_tcp:send(Sock, Frame).
736736

737737
stomp_recv({_Sock, []} = Client) ->

deps/rabbitmq_stomp/Makefile

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ define PROJECT_ENV
99
{passcode, <<"guest">>}]},
1010
{default_vhost, <<"/">>},
1111
{default_topic_exchange, <<"amq.topic">>},
12-
{default_nack_requeue, true},
12+
{default_nack_requeue, true},
1313
{ssl_cert_login, false},
1414
{implicit_connect, false},
1515
{tcp_listeners, [61613]},
@@ -30,7 +30,7 @@ define PROJECT_APP_EXTRA_KEYS
3030
{broker_version_requirements, []}
3131
endef
3232

33-
DEPS = ranch rabbit_common rabbit amqp_client
33+
DEPS = ranch rabbit_common rabbit
3434
TEST_DEPS = rabbitmq_ct_helpers rabbitmq_ct_client_helpers rabbitmq_management proper
3535

3636
PLT_APPS += rabbitmq_cli elixir ssl
@@ -40,3 +40,9 @@ DEP_PLUGINS = rabbit_common/mk/rabbitmq-plugin.mk
4040

4141
include ../../rabbitmq-components.mk
4242
include ../../erlang.mk
43+
44+
# Regenerate per-function CT test cases from Python test files.
45+
# The generated .hrl is committed; this rule updates it when Python sources change.
46+
# Runs only when python3 is available.
47+
test/python_SUITE_generated.hrl:: $(wildcard test/python_SUITE_data/src/*.py) test/generate_python_tests.py
48+
$(if $(shell which python3 2>/dev/null),python3 test/generate_python_tests.py test/python_SUITE_data/src $@,@echo "python3 not found, using committed $@")

deps/rabbitmq_stomp/include/rabbit_stomp.hrl

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,27 @@
99
default_passcode,
1010
force_default_creds = false,
1111
implicit_connect,
12-
ssl_cert_login}).
12+
ssl_cert_login,
13+
max_header_length,
14+
max_headers,
15+
max_body_length}).
16+
1317

1418
-define(SUPPORTED_VERSIONS, ["1.0", "1.1", "1.2"]).
1519

20+
-define(STOMP_PROTO_V1_0, 'STOMP 1.0').
21+
-define(STOMP_PROTO_V1_1, 'STOMP 1.1').
22+
-define(STOMP_PROTO_V1_2, 'STOMP 1.2').
23+
24+
25+
1626
-define(INFO_ITEMS,
1727
[conn_name,
28+
name,
29+
user,
1830
connection,
1931
connection_state,
2032
session_id,
21-
channel,
2233
version,
2334
implicit_connect,
2435
auth_login,
@@ -29,6 +40,7 @@
2940
peer_host,
3041
peer_port,
3142
protocol,
43+
connected_at,
3244
channels,
3345
channel_max,
3446
frame_max,
@@ -43,3 +55,18 @@
4355

4456
-define(DEFAULT_MAX_FRAME_SIZE, 4 * 1024 * 1024).
4557
-define(DEFAULT_MAX_FRAME_SIZE_UNAUTHENTICATED, 65536).
58+
59+
-define(SIMPLE_METRICS,
60+
[pid,
61+
recv_oct,
62+
send_oct,
63+
reductions]).
64+
-define(OTHER_METRICS,
65+
[recv_cnt,
66+
send_cnt,
67+
send_pend,
68+
garbage_collection,
69+
state,
70+
timeout]).
71+
72+
-type send_fun() :: fun ((iodata()) -> ok).

deps/rabbitmq_stomp/include/rabbit_stomp_frame.hrl

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,9 @@
55
%% Copyright (c) 2007-2026 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved.
66
%%
77

8-
-record(stomp_frame, {command, headers, body_iolist}).
8+
-record(stomp_frame, {command, headers, body_iolist_rev}).
9+
10+
-record(stomp_parser_config, {max_header_length = 1024*100,
11+
max_headers = 100,
12+
max_body_length = 1024*1024*100}).
13+
-define(DEFAULT_STOMP_PARSER_CONFIG, #stomp_parser_config{}).

deps/rabbitmq_stomp/include/rabbit_stomp_headers.hrl

Lines changed: 60 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -5,57 +5,59 @@
55
%% Copyright (c) 2007-2026 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved.
66
%%
77

8-
-define(HEADER_ACCEPT_VERSION, "accept-version").
9-
-define(HEADER_ACK, "ack").
10-
-define(HEADER_AMQP_MESSAGE_ID, "amqp-message-id").
11-
-define(HEADER_APP_ID, "app-id").
12-
-define(HEADER_AUTO_DELETE, "auto-delete").
13-
-define(HEADER_CONTENT_ENCODING, "content-encoding").
14-
-define(HEADER_CONTENT_LENGTH, "content-length").
15-
-define(HEADER_CONTENT_TYPE, "content-type").
16-
-define(HEADER_CORRELATION_ID, "correlation-id").
17-
-define(HEADER_DESTINATION, "destination").
18-
-define(HEADER_DURABLE, "durable").
19-
-define(HEADER_EXPIRATION, "expiration").
20-
-define(HEADER_EXCLUSIVE, "exclusive").
21-
-define(HEADER_HEART_BEAT, "heart-beat").
22-
-define(HEADER_HOST, "host").
23-
-define(HEADER_ID, "id").
24-
-define(HEADER_LOGIN, "login").
25-
-define(HEADER_MESSAGE_ID, "message-id").
26-
-define(HEADER_PASSCODE, "passcode").
27-
-define(HEADER_PERSISTENT, "persistent").
28-
-define(HEADER_PREFETCH_COUNT, "prefetch-count").
29-
-define(HEADER_X_STREAM_OFFSET, "x-stream-offset").
30-
-define(HEADER_X_STREAM_FILTER, "x-stream-filter").
31-
-define(HEADER_X_STREAM_MATCH_UNFILTERED, "x-stream-match-unfiltered").
32-
-define(HEADER_PRIORITY, "priority").
33-
-define(HEADER_X_PRIORITY, "x-priority").
34-
-define(HEADER_RECEIPT, "receipt").
35-
-define(HEADER_REDELIVERED, "redelivered").
36-
-define(HEADER_REPLY_TO, "reply-to").
37-
-define(HEADER_SERVER, "server").
38-
-define(HEADER_SESSION, "session").
39-
-define(HEADER_SUBSCRIPTION, "subscription").
40-
-define(HEADER_TIMESTAMP, "timestamp").
41-
-define(HEADER_TRANSACTION, "transaction").
42-
-define(HEADER_TYPE, "type").
43-
-define(HEADER_USER_ID, "user-id").
44-
-define(HEADER_VERSION, "version").
45-
-define(HEADER_X_DEAD_LETTER_EXCHANGE, "x-dead-letter-exchange").
46-
-define(HEADER_X_DEAD_LETTER_ROUTING_KEY, "x-dead-letter-routing-key").
47-
-define(HEADER_X_EXPIRES, "x-expires").
48-
-define(HEADER_X_MAX_LENGTH, "x-max-length").
49-
-define(HEADER_X_MAX_AGE, "x-max-age").
50-
-define(HEADER_X_MAX_LENGTH_BYTES, "x-max-length-bytes").
51-
-define(HEADER_X_STREAM_MAX_SEGMENT_SIZE_BYTES, "x-stream-max-segment-size-bytes").
52-
-define(HEADER_X_MAX_PRIORITY, "x-max-priority").
53-
-define(HEADER_X_MESSAGE_TTL, "x-message-ttl").
54-
-define(HEADER_X_QUEUE_NAME, "x-queue-name").
55-
-define(HEADER_X_QUEUE_TYPE, "x-queue-type").
56-
-define(HEADER_X_STREAM_FILTER_SIZE_BYTES, "x-stream-filter-size-bytes").
8+
-include("rabbit_stomp_routing_prefixes.hrl").
579

58-
-define(MESSAGE_ID_SEPARATOR, "@@").
10+
-define(HEADER_ACCEPT_VERSION, <<"accept-version">>).
11+
-define(HEADER_ACK, <<"ack">>).
12+
-define(HEADER_AMQP_MESSAGE_ID, <<"amqp-message-id">>).
13+
-define(HEADER_APP_ID, <<"app-id">>).
14+
-define(HEADER_AUTO_DELETE, <<"auto-delete">>).
15+
-define(HEADER_CONTENT_ENCODING, <<"content-encoding">>).
16+
-define(HEADER_CONTENT_LENGTH, <<"content-length">>).
17+
-define(HEADER_CONTENT_TYPE, <<"content-type">>).
18+
-define(HEADER_CORRELATION_ID, <<"correlation-id">>).
19+
-define(HEADER_DESTINATION, <<"destination">>).
20+
-define(HEADER_DURABLE, <<"durable">>).
21+
-define(HEADER_EXPIRATION, <<"expiration">>).
22+
-define(HEADER_EXCLUSIVE, <<"exclusive">>).
23+
-define(HEADER_HEART_BEAT, <<"heart-beat">>).
24+
-define(HEADER_HOST, <<"host">>).
25+
-define(HEADER_ID, <<"id">>).
26+
-define(HEADER_LOGIN, <<"login">>).
27+
-define(HEADER_MESSAGE_ID, <<"message-id">>).
28+
-define(HEADER_PASSCODE, <<"passcode">>).
29+
-define(HEADER_PERSISTENT, <<"persistent">>).
30+
-define(HEADER_PREFETCH_COUNT, <<"prefetch-count">>).
31+
-define(HEADER_X_STREAM_OFFSET, <<"x-stream-offset">>).
32+
-define(HEADER_X_STREAM_FILTER, <<"x-stream-filter">>).
33+
-define(HEADER_X_STREAM_MATCH_UNFILTERED, <<"x-stream-match-unfiltered">>).
34+
-define(HEADER_PRIORITY, <<"priority">>).
35+
-define(HEADER_X_PRIORITY, <<"x-priority">>).
36+
-define(HEADER_RECEIPT, <<"receipt">>).
37+
-define(HEADER_REDELIVERED, <<"redelivered">>).
38+
-define(HEADER_REPLY_TO, <<"reply-to">>).
39+
-define(HEADER_SERVER, <<"server">>).
40+
-define(HEADER_SESSION, <<"session">>).
41+
-define(HEADER_SUBSCRIPTION, <<"subscription">>).
42+
-define(HEADER_TIMESTAMP, <<"timestamp">>).
43+
-define(HEADER_TRANSACTION, <<"transaction">>).
44+
-define(HEADER_TYPE, <<"type">>).
45+
-define(HEADER_USER_ID, <<"user-id">>).
46+
-define(HEADER_VERSION, <<"version">>).
47+
-define(HEADER_X_DEAD_LETTER_EXCHANGE, <<"x-dead-letter-exchange">>).
48+
-define(HEADER_X_DEAD_LETTER_ROUTING_KEY, <<"x-dead-letter-routing-key">>).
49+
-define(HEADER_X_EXPIRES, <<"x-expires">>).
50+
-define(HEADER_X_MAX_LENGTH, <<"x-max-length">>).
51+
-define(HEADER_X_MAX_AGE, <<"x-max-age">>).
52+
-define(HEADER_X_MAX_LENGTH_BYTES, <<"x-max-length-bytes">>).
53+
-define(HEADER_X_STREAM_MAX_SEGMENT_SIZE_BYTES, <<"x-stream-max-segment-size-bytes">>).
54+
-define(HEADER_X_MAX_PRIORITY, <<"x-max-priority">>).
55+
-define(HEADER_X_MESSAGE_TTL, <<"x-message-ttl">>).
56+
-define(HEADER_X_QUEUE_NAME, <<"x-queue-name">>).
57+
-define(HEADER_X_QUEUE_TYPE, <<"x-queue-type">>).
58+
-define(HEADER_X_STREAM_FILTER_SIZE_BYTES, <<"x-stream-filter-size-bytes">>).
59+
60+
-define(MESSAGE_ID_SEPARATOR, <<"@@">>).
5961

6062
-define(HEADERS_NOT_ON_SEND, [?HEADER_MESSAGE_ID]).
6163

@@ -81,3 +83,11 @@
8183
?HEADER_EXCLUSIVE,
8284
?HEADER_PERSISTENT
8385
]).
86+
87+
88+
%%-------------------------------------------------
89+
90+
-define(DEST_PREFIXES, [?EXCHANGE_PREFIX, ?TOPIC_PREFIX, ?QUEUE_PREFIX,
91+
?AMQQUEUE_PREFIX, ?REPLY_QUEUE_PREFIX]).
92+
93+
-define(ALL_DEST_PREFIXES, [?TEMP_QUEUE_PREFIX | ?DEST_PREFIXES]).

0 commit comments

Comments
 (0)