1 /*
2  * Hunt - A redis client library for D programming language.
3  *
4  * Copyright (C) 2018-2019 HuntLabs
5  *
6  * Website: https://www.huntlabs.net/
7  *
8  * Licensed under the Apache-2.0 License.
9  *
10  */
11  
12 module hunt.redis.HostAndPort;
13 
14 import hunt.Exceptions;
15 import hunt.logging.ConsoleLogger;
16 
17 import hunt.text.Common;
18 import hunt.text.StringUtils;
19 
20 import std.algorithm;
21 import std.conv;
22 import std.socket;
23 import std.string;
24 
25 /**
26 */
27 class HostAndPort { // : Serializable 
28 
29     __gshared string localhost;
30 
31     private string host;
32     private int port;
33 
34     this(string host, int port) {
35         this.host = host;
36         this.port = port;
37     }
38 
39     string getHost() {
40         return host;
41     }
42 
43     int getPort() {
44         return port;
45     }
46 
47     override bool opEquals(Object obj) {
48         if (obj is null)
49             return false;
50         if (obj is this)
51             return true;
52 
53         HostAndPort hp = cast(HostAndPort) obj;
54         if (hp is null)
55             return false;
56 
57         string thisHost = convertHost(host);
58         string hpHost = convertHost(hp.host);
59         return port == hp.port && thisHost == hpHost;
60     }
61 
62     override size_t toHash() @trusted nothrow {
63         return 31 * convertHost(host).hashOf() + port;
64     }
65 
66     override string toString() {
67         return host ~ ":" ~ port.to!string();
68     }
69 
70     /**
71    * Splits string into host and port parts.
72    * string must be in ( host ~ ":" ~ port ) format.
73    * Port is optional
74    * @param from string to parse
75    * @return array of host and port strings
76      */
77     static string[] extractParts(string from) {
78         int idx = cast(int)from.lastIndexOf(":");
79         string host = idx != -1 ? from.substring(0, idx) : from;
80         string port = idx != -1 ? from.substring(idx + 1) : "";
81         return [host, port];
82     }
83 
84     /**
85    * Creates HostAndPort instance from string.
86    * string must be in ( host ~ ":" ~ port ) format.
87    * Port is mandatory. Can convert host part.
88    * @see #convertHost(string)
89    * @param from string to parse
90    * @return HostAndPort instance
91      */
92     static HostAndPort parseString(string from) {
93         // NOTE: redis answers with
94         // '99aa9999aa9a99aa099aaa990aa99a09aa9a9999 9a09:9a9:a090:9a::99a slave 8c88888888cc08088cc8c8c888c88c8888c88cc8 0 1468251272993 37 connected'
95         // for CLUSTER NODES, ASK and MOVED scenarios. That's why there is no possibility to parse address in 'correct' way.
96         // Redis should switch to 'bracketized' (RFC 3986) IPv6 address.
97         try {
98             string[] parts = extractParts(from);
99             string host = parts[0];
100             int port = to!int(parts[1]);
101             return new HostAndPort(convertHost(host), port);
102         } catch (Exception ex) {
103             throw new IllegalArgumentException(ex);
104         }
105     }
106 
107     static string convertHost(string host) @trusted nothrow {
108         try {
109             /*
110             * Validate the host name as an IPV4/IPV6 address.
111             * If this is an AWS ENDPOINT it will not parse.
112             * In that case accept host as is.
113             *
114             * Costs: If this is an IPV4/6 encoding, e.g. 127.0.0.1 then no DNS lookup
115             * is done.  If it is a name then a DNS lookup is done but it is normally cached.
116             * Secondarily, this class is typically used to create a connection once
117             * at the beginning of processing and then not used again.  So even if the DNS
118             * lookup needs to be done then the cost is miniscule.
119             */
120 
121             // FIXME: Needing refactor or cleanup -@zxp at 7/16/2019, 6:35:54 PM
122             // 
123             Address inetAddress = parseAddress(host);
124             // InetAddress inetAddress = InetAddress.getByName(host);
125             // inetAddress.isLoopbackAddress() ||
126 
127             // isLoopbackAddress() handles both IPV4 and IPV6
128             if (host == "0.0.0.0"
129                     || host.startsWith("169.254"))
130                 return getLocalhost();
131             else
132                 return host;
133         } catch (Exception e) {
134             // Not a valid IP address
135             warning("{}.convertHost '" ~ host ~ "' is not a valid IP address. ",
136                     HostAndPort.stringof, e);
137             return host;
138         }
139     }
140 
141     static void setLocalhost(string localhost) {
142         HostAndPort.localhost = localhost;
143     }
144 
145     /**
146    * This method resolves the localhost in a 'lazy manner'.
147    *
148    * @return localhost
149    */
150     static string getLocalhost() {
151         if (localhost is null) {
152             synchronized {
153                 if (localhost is null) {
154                     return localhost = getLocalHostQuietly();
155                 }
156             }
157         }
158         return localhost;
159     }
160 
161     static string getLocalHostQuietly() {
162         string localAddress;
163         try {
164             // FIXME: Needing refactor or cleanup -@zxp at 7/16/2019, 6:34:32 PM
165             // 
166             localAddress = "localhost"; // InetAddress.getLocalHost().getHostAddress();
167         } catch (Exception ex) {
168             error("%s.getLocalHostQuietly : cant resolve localhost address",
169                     HostAndPort.stringof, ex);
170             localAddress = "localhost";
171         }
172         return localAddress;
173     }
174 }