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 }