Skip to content

Commit 581dcd2

Browse files
AMQP 1.0 client: make it easier to pass in a virtual host
while AMQP 1.0 does not have the concept of virtual hosts, RabbitMQ still does, and so do shovels and other components. Currently the way to pass a virtual host is via a query parameter named 'hostname', which is very counterintuitive to most users. With this PR, there are now two better options: 1. The URI path, with the leading slash stripped off 2. The 'vhost' query parameter which is formatted correctly Both still feed the 'hostname' connection parameter but are much easier to guess and remember for, say, AMQP 1.0 Shovel users.
1 parent a1ec795 commit 581dcd2

File tree

2 files changed

+147
-10
lines changed

2 files changed

+147
-10
lines changed

deps/amqp10_client/src/amqp10_client.erl

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@
3939
link_handle/1,
4040
get_msg/1,
4141
get_msg/2,
42-
parse_uri/1
42+
parse_uri/1,
43+
%% for tests
44+
binary_without_leading_slash/1
4345
]).
4446

4547
-type snd_settle_mode() :: amqp10_client_session:snd_settle_mode().
@@ -412,14 +414,24 @@ parse_uri(Uri) ->
412414
end.
413415

414416
parse_result(Map) ->
415-
_ = case maps:get(path, Map, "/") of
416-
"/" -> ok;
417-
"" -> ok;
418-
_ -> throw(path_segment_not_supported)
419-
end,
420417
Scheme = maps:get(scheme, Map, "amqp"),
421418
UserInfo = maps:get(userinfo, Map, undefined),
422419
Host = maps:get(host, Map),
420+
421+
%% AMQP 1.0 may not have the concept of virtual hosts but
422+
%% Shovels and Erlang/BEAM-based apps connecting to RabbitMQ
423+
%% need to be able to pass it, so treat any "non-default" path as a virtual host name
424+
PathSegment = case maps:get(path, Map, "/") of
425+
"/" -> undefined;
426+
"" -> undefined;
427+
Value0 -> binary_without_leading_slash(Value0)
428+
end,
429+
%% Note: this is not the same thing as a hostname at the TCP/IP level, that is, not 'address'.
430+
DefaultHostname = case PathSegment of
431+
undefined -> to_binary(Host);
432+
Value1 -> list_to_binary(io_lib:format("vhost:~ts", [Value1]))
433+
end,
434+
423435
DefaultPort = case Scheme of
424436
"amqp" -> 5672;
425437
"amqps" -> 5671
@@ -444,13 +456,15 @@ parse_result(Map) ->
444456
Acc#{max_frame_size => list_to_integer(V)};
445457
("hostname", V, Acc) ->
446458
Acc#{hostname => list_to_binary(V)};
459+
("vhost", V, Acc) ->
460+
Acc#{hostname => list_to_binary(io_lib:format("vhost:~ts", [V]))};
447461
("container_id", V, Acc) ->
448462
Acc#{container_id => list_to_binary(V)};
449463
("transfer_limit_margin", V, Acc) ->
450464
Acc#{transfer_limit_margin => list_to_integer(V)};
451465
(_, _, Acc) -> Acc
452466
end, #{address => Host,
453-
hostname => to_binary(Host),
467+
hostname => DefaultHostname,
454468
port => Port,
455469
sasl => Sasl}, Query),
456470
case Scheme of
@@ -460,6 +474,15 @@ parse_result(Map) ->
460474
Ret0#{tls_opts => {secure_port, TlsOpts}}
461475
end.
462476

477+
-spec binary_without_leading_slash(binary() | string()) -> binary().
478+
binary_without_leading_slash(Bin) when is_binary(Bin) ->
479+
case Bin of
480+
<<"/", Rest/binary>> -> Rest;
481+
Other -> Other
482+
end;
483+
binary_without_leading_slash(Bin) when is_list(Bin) ->
484+
?FUNCTION_NAME(list_to_binary(Bin)).
485+
463486
parse_usertoken(U) ->
464487
[User, Pass] = string:tokens(U, ":"),
465488
{plain,
@@ -558,6 +581,12 @@ parse_uri_test_() ->
558581
hostname => <<"my_proxy">>,
559582
sasl => {plain, <<"fred">>, <<"passw">>}}},
560583
parse_uri("amqp://fred:passw@my_proxy:9876")),
584+
%% treat URI path as a virtual host name
585+
?_assertEqual({ok, #{port => 5672,
586+
address => "my_host",
587+
sasl => anon,
588+
hostname => <<"vhost:my_path_segment:9876">>}},
589+
parse_uri("amqp://my_host/my_path_segment:9876")),
561590
?_assertEqual(
562591
{ok, #{address => "my_proxy", port => 9876,
563592
hostname => <<"my_proxy">>,
@@ -597,9 +626,7 @@ parse_uri_test_() ->
597626
"cacertfile=/etc/cacertfile.pem&certfile=/etc/certfile.pem&" ++
598627
"keyfile=/etc/keyfile.key&fail_if_no_peer_cert=banana")),
599628
?_assertEqual({error, plain_sasl_missing_userinfo},
600-
parse_uri("amqp://my_host:9876?sasl=plain")),
601-
?_assertEqual({error, path_segment_not_supported},
602-
parse_uri("amqp://my_host/my_path_segment:9876"))
629+
parse_uri("amqp://my_host:9876?sasl=plain"))
603630
].
604631

605632
-endif.
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
%% This Source Code Form is subject to the terms of the Mozilla Public
2+
%% License, v. 2.0. If a copy of the MPL was not distributed with this
3+
%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
%%
5+
%% Copyright (c) 2007-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved.
6+
%%
7+
8+
-module(unit_SUITE).
9+
10+
-include_lib("common_test/include/ct.hrl").
11+
-include_lib("eunit/include/eunit.hrl").
12+
13+
-compile([export_all, nowarn_export_all]).
14+
15+
suite() ->
16+
[{timetrap, {minutes, 1}}].
17+
18+
all() ->
19+
[
20+
{group, uri_parsing}
21+
].
22+
23+
groups() ->
24+
[
25+
{uri_parsing, [parallel], [
26+
without_leading_slash,
27+
parse_uri_case1,
28+
parse_uri_case2,
29+
parse_uri_case3,
30+
parse_uri_case4,
31+
parse_uri_case5,
32+
parse_uri_case6,
33+
parse_uri_case7
34+
]}
35+
].
36+
37+
%%
38+
%% Test cases
39+
%%
40+
41+
without_leading_slash(_) ->
42+
?assertEqual(<<>>, amqp10_client:binary_without_leading_slash(<<>>)),
43+
?assertEqual(<<>>, amqp10_client:binary_without_leading_slash(<<"/">>)),
44+
?assertEqual(<<"abc">>, amqp10_client:binary_without_leading_slash(<<"/abc">>)),
45+
46+
?assertEqual(<<>>, amqp10_client:binary_without_leading_slash("")),
47+
?assertEqual(<<>>, amqp10_client:binary_without_leading_slash("/")),
48+
?assertEqual(<<"abc">>, amqp10_client:binary_without_leading_slash("/abc")).
49+
50+
parse_uri_case1(_) ->
51+
URI = "amqp://target.hostname:5672",
52+
{ok, Result} = amqp10_client:parse_uri(URI),
53+
54+
?assertEqual(maps:get(address, Result), "target.hostname"),
55+
?assertEqual(maps:get(port, Result), 5672),
56+
?assertEqual(maps:get(sasl, Result), anon),
57+
?assertEqual(maps:get(tls_opts, Result, undefined), undefined).
58+
59+
parse_uri_case2(_) ->
60+
URI = "amqps://target.hostname:5671",
61+
{ok, Result} = amqp10_client:parse_uri(URI),
62+
63+
?assertEqual(maps:get(address, Result), "target.hostname"),
64+
?assertEqual(maps:get(port, Result), 5671),
65+
?assertMatch({secure_port, _}, maps:get(tls_opts, Result)).
66+
67+
parse_uri_case3(_) ->
68+
URI = "amqp://target.hostname",
69+
{ok, Result} = amqp10_client:parse_uri(URI),
70+
71+
?assertEqual(maps:get(address, Result), "target.hostname"),
72+
?assertEqual(maps:get(port, Result), 5672).
73+
74+
parse_uri_case4(_) ->
75+
URI = "amqp://username:[email protected]",
76+
{ok, Result} = amqp10_client:parse_uri(URI),
77+
78+
?assertEqual(maps:get(address, Result), "target.hostname"),
79+
?assertEqual(maps:get(port, Result), 5672),
80+
?assertEqual(maps:get(sasl, Result), {plain, <<"username">>, <<"secre7">>}).
81+
82+
parse_uri_case5(_) ->
83+
URI = "amqp://username:[email protected]?container_id=container9&hostname=vhost:abc",
84+
{ok, Result} = amqp10_client:parse_uri(URI),
85+
ct:pal("~tp", [Result]),
86+
?assertEqual(maps:get(address, Result), "target.hostname"),
87+
?assertEqual(maps:get(port, Result), 5672),
88+
?assertEqual(maps:get(sasl, Result), {plain, <<"username">>, <<"secre7">>}),
89+
?assertEqual(maps:get(container_id, Result), <<"container9">>),
90+
?assertEqual(maps:get(hostname, Result), <<"vhost:abc">>).
91+
92+
parse_uri_case6(_) ->
93+
URI = "amqp://username:[email protected]?container_id=container7&vhost=abc",
94+
{ok, Result} = amqp10_client:parse_uri(URI),
95+
ct:pal("~tp", [Result]),
96+
?assertEqual(maps:get(address, Result), "target.hostname"),
97+
?assertEqual(maps:get(port, Result), 5672),
98+
?assertEqual(maps:get(sasl, Result), {plain, <<"username">>, <<"secre7">>}),
99+
?assertEqual(maps:get(container_id, Result), <<"container7">>),
100+
?assertEqual(maps:get(hostname, Result), <<"vhost:abc">>).
101+
102+
parse_uri_case7(_) ->
103+
URI = "amqp://username:[email protected]/abc?container_id=container5",
104+
{ok, Result} = amqp10_client:parse_uri(URI),
105+
ct:pal("~tp", [Result]),
106+
?assertEqual(maps:get(address, Result), "target.hostname"),
107+
?assertEqual(maps:get(port, Result), 5672),
108+
?assertEqual(maps:get(sasl, Result), {plain, <<"username">>, <<"secre7">>}),
109+
?assertEqual(maps:get(container_id, Result), <<"container5">>),
110+
?assertEqual(maps:get(hostname, Result), <<"vhost:abc">>).

0 commit comments

Comments
 (0)