1 /++
2     Parsing of all kind of socket addresses
3 
4     ## Currently supported
5 
6     $(LIST
7         * Unix Domain Socket path (e.g. `/var/run/myapp.sock`)
8         * IPv4 address (e.g. `192.168.0.1`)
9         * IPv4 address with port (e.g. `192.168.0.1:1234`)
10         * IPv6 address (e.g. `[2001:0db8::1]`)
11         * IPv6 address with port (e.g. `[2001:0db8::1]:1234`)
12     )
13 
14     ## Rules
15 
16     $(LIST
17         * Unix Domain socket paths MUST be absolute (i.e. start with `/`) to prevent ambiguity.
18         * IPv4 resp. IPv6 addresses are separated from their (optional) accompanying port number by `:`.
19         * IPv6 addresses MUST be encapsulated in square brackets (`[…]`). (This prevents ambiguity with regard to the address/port separator `:`.)
20     )
21 
22     ## Idea
23 
24     Socket addresses are intended to provide a concise way to describe the “address” of network socket.
25 
26     The original use case was to provide a simple way to describe listening sockets in a uniform way.
27     For a command line interface there should only be a single `--socket=<address>` parameter,
28     regardless of whether the specified address is IPv4 or IPv6 – or even a Unix Domain Socket path.
29 
30     ### Relation to URIs
31 
32     While similarly looking, socket addresses neither are URIs nor to be considered a part/subset of them.
33     
34     For example, in comparison to URIs there’s no protocol scheme or path.
35     Also there are no domain names in socket addresses (thus no DNS resolution necessary or provided).
36  +/
37 module socketplate.address;
38 
39 import std.ascii : isDigit;
40 import std.conv : to;
41 import std.string : indexOf;
42 
43 @safe pure nothrow:
44 
45 struct SocketAddress
46 {
47     ///
48     Type type = Type.invalid;
49 
50     ///
51     string address = null;
52 
53     ///
54     int port = int.min;
55 
56     ///
57     enum Type
58     {
59         invalid = -1,
60 
61         /// Unix Domain Socket
62         unixDomain = 0,
63 
64         /// Internet Protocal Version 4 (IPv4) address
65         ipv4,
66 
67         /// Internet Protocal Version 6 (IPv6) address
68         ipv6,
69     }
70 }
71 
72 /++
73     Socket address triage and parsing function
74 
75     Determines the type (IPv4, IPv6, Unix Domain Socket etc.) of a socket
76     and parses it according to the rules mentioned in this module’s description.
77 
78     $(WARNING
79         Does not actually validate addresses (or paths).
80     )
81 
82     Returns:
83         true = on success, or
84         false = on error (invalid input)
85  +/
86 bool parseSocketAddress(string input, out SocketAddress result)
87 {
88     // Unix Domain Socket
89     if (input[0] == '/')
90         return parseUnixDomain(input, result);
91 
92     // IPv6
93     if (input[0] == '[')
94         return parseIPv6(input, result);
95 
96     // IPv4
97 
98     // basic garbage detection
99     foreach (ref c; input)
100         if ((!c.isDigit) && (c != '.') && (c != ':'))
101             return false;
102 
103     return parseIPv4(input, result);
104 }
105 
106 ///
107 unittest
108 {
109     SocketAddress sockAddr;
110 
111     assert(parseSocketAddress("127.0.0.1:8080", sockAddr));
112     assert(sockAddr == SocketAddress(SocketAddress.Type.ipv4, "127.0.0.1", 8080));
113 
114     assert(parseSocketAddress("127.0.0.1", sockAddr));
115     assert(sockAddr == SocketAddress(SocketAddress.Type.ipv4, "127.0.0.1", int.min));
116 
117     assert(parseSocketAddress("[::]:993", sockAddr));
118     assert(sockAddr == SocketAddress(SocketAddress.Type.ipv6, "::", 993));
119 
120     assert(parseSocketAddress("[::1]", sockAddr));
121     assert(sockAddr == SocketAddress(SocketAddress.Type.ipv6, "::1", int.min));
122 
123     assert(parseSocketAddress("/var/run/myapp.sock", sockAddr));
124     assert(sockAddr == SocketAddress(SocketAddress.Type.unixDomain, "/var/run/myapp.sock", int.min));
125 
126     assert(!parseSocketAddress("myapp.sock", sockAddr));
127     assert(!parseSocketAddress("::1", sockAddr));
128     assert(!parseSocketAddress("http://127.0.0.1", sockAddr));
129 }
130 
131 ///
132 SocketAddress makeSocketAddress(string address, ushort port)
133 {
134     assert(address.length >= 4, "Invalid IP address");
135 
136     // IPv6?
137     if (address[0] == '[')
138         return SocketAddress(
139             SocketAddress.Type.ipv6,
140             address[1 .. ($ - 1)],
141             port
142         );
143 
144     // IPv4?
145     else if (address[0].isDigit)
146         return SocketAddress(
147             SocketAddress.Type.ipv4,
148             address,
149             port
150         );
151 
152     assert(false, "Invalid IP address");
153 }
154 
155 ///
156 SocketAddress makeSocketAddress(string unixDomainSocketPath)
157 {
158     return SocketAddress(SocketAddress.Type.unixDomain, unixDomainSocketPath);
159 }
160 
161 private:
162 
163 bool parseUnixDomain(string input, out SocketAddress result) @nogc
164 {
165     result = SocketAddress(SocketAddress.Type.unixDomain, input);
166     return true;
167 }
168 
169 bool parseIPv6(string input, out SocketAddress result)
170 {
171     immutable ptrdiff_t idxEndOfAddress = input.indexOf(']');
172     if (idxEndOfAddress < 0)
173         return false;
174 
175     int port = int.min;
176 
177     if ((idxEndOfAddress + 1) < input.length)
178     {
179         string portStr = input[(idxEndOfAddress + 1) .. $];
180         if (portStr[0] != ':')
181             return false;
182 
183         portStr = portStr[1 .. $];
184         immutable portValid = parsePort(portStr, port);
185         if (!portValid)
186             return false;
187     }
188 
189     immutable string address = input[1 .. idxEndOfAddress];
190 
191     result = SocketAddress(SocketAddress.Type.ipv6, address, port);
192     return true;
193 }
194 
195 bool parseIPv4(string input, out SocketAddress result)
196 {
197     ptrdiff_t idxPortSep = input.indexOf(':');
198 
199     if (idxPortSep == 0)
200         return false;
201 
202     int port = int.min;
203 
204     if (idxPortSep > 0)
205     {
206         immutable portValid = parsePort(input[(idxPortSep + 1) .. $], port);
207         if (!portValid)
208             return false;
209     }
210 
211     immutable string address = (idxPortSep > 0) ? input[0 .. idxPortSep] : input;
212 
213     result = SocketAddress(SocketAddress.Type.ipv4, address, port);
214     return true;
215 }
216 
217 bool parsePort(scope string input, out int result)
218 {
219     try
220         result = input.to!ushort;
221     catch (Exception)
222         return false;
223 
224     return true;
225 }