github.com/openshift-online/ocm-sdk-go@v0.1.473/internal/server_address.go (about) 1 /* 2 Copyright (c) 2021 Red Hat, Inc. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 // This file contains the implementation of the server address parser. 18 19 package internal 20 21 import ( 22 "context" 23 "fmt" 24 neturl "net/url" 25 "strings" 26 ) 27 28 // ServerAddress contains a parsed URL and additional information extracted from int, like the 29 // network (tcp or unix) and the socket name (for Unix sockets). 30 type ServerAddress struct { 31 // Text is the original text that was passed to the ParseServerAddress function to create 32 // this server address. 33 Text string 34 35 // Network is the network that should be used to connect to the server. Possible values are 36 // `tcp` and `unix`. 37 Network string 38 39 // Protocol is the application protocol used to connect to the server. Possible values are 40 // `http`, `https` and `h2c`. 41 Protocol string 42 43 // Host is the name of the host used to connect to the server. This will be populated only 44 // even when using Unix sockets, because clients will need it in order to populate the 45 // `Host` header. 46 Host string 47 48 // Port is the port number used to connect to the server. This will only be populated when 49 // using TCP. When using Unix sockets it will be zero. 50 Port string 51 52 // Socket is tha nem of the path of the Unix socket used to connect to the server. 53 Socket string 54 55 // URL is the regular URL calculated from this server address. The scheme will be `http` if 56 // the protocol is `http` or `h2c` and will be `https` if the protocol is https. 57 URL *neturl.URL 58 } 59 60 // ParseServerAddress parses the given text as a server address. Server addresses should be URLs 61 // with this format: 62 // 63 // network+protocol://host:port/path?network=...&protocol=...&socket=... 64 // 65 // The `network` and `protocol` parts of the scheme are optional. 66 // 67 // Valid values for the `network` part of the scheme are `unix` and `tcp`. If not specified the 68 // default value is `tcp`. 69 // 70 // Valid values for the `protocol` part of the scheme are `http`, `https` and `h2c`. If not 71 // specified the default value is `http`. 72 // 73 // The `host` is mandatory even when using Unix sockets, because it is necessary to populate the 74 // `Host` header. 75 // 76 // The `port` part is optional. If not specified it will be 80 for HTTP and H2C and 443 for HTTPS. 77 // 78 // When using Unix sockets the `path` part will be used as the name of the Unix socket. 79 // 80 // The network protocol and Unix socket can alternatively be specified using the `network`, 81 // `protocol` and `socket` query parameters. This is useful specially for specifying the Unix 82 // sockets when the path of the URL has some other meaning. For example, in order to specify 83 // the OpenID token URL it is usually necessary to include a path, so to use a Unix socket it 84 // is necessary to put it in the `socket` parameter instead: 85 // 86 // unix://my.sso.com/my/token/path?socket=/sockets/my.socket 87 // 88 // When the Unix socket is specified in the `socket` query parameter as in the above example 89 // the URL path will be ignored. 90 // 91 // Some examples of valid server addresses: 92 // 93 // - http://my.server.com - HTTP on top of TCP. 94 // - https://my.server.com - HTTPS on top of TCP. 95 // - unix://my.server.com/sockets/my.socket - HTTP on top Unix socket. 96 // - unix+https://my.server.com/sockets/my.socket - HTTPS on top of Unix socket. 97 // - h2c+unix://my.server.com?socket=/sockets/my.socket - H2C on top of Unix. 98 func ParseServerAddress(ctx context.Context, text string) (result *ServerAddress, err error) { 99 // Parse the URL: 100 parsed, err := neturl.Parse(text) 101 if err != nil { 102 return 103 } 104 query := parsed.Query() 105 106 // Extract the network and protocol from the scheme: 107 networkFromScheme, protocolFromScheme, err := parseScheme(ctx, parsed.Scheme) 108 if err != nil { 109 return 110 } 111 112 // Check if the network is also specified with a query parameter. If it is it should not be 113 // conflicting with the value specified in the scheme. 114 var network string 115 networkValues, ok := query["network"] 116 if ok { 117 if len(networkValues) != 1 { 118 err = fmt.Errorf( 119 "expected exactly one value for the 'network' query parameter "+ 120 "but found %d", 121 len(networkValues), 122 ) 123 return 124 } 125 networkFromQuery := strings.TrimSpace(strings.ToLower(networkValues[0])) 126 err = checkNetwork(networkFromQuery) 127 if err != nil { 128 return 129 } 130 if networkFromScheme != "" && networkFromScheme != networkFromQuery { 131 err = fmt.Errorf( 132 "network '%s' from query parameter isn't compatible with "+ 133 "network '%s' from scheme", 134 networkFromQuery, networkFromScheme, 135 ) 136 return 137 } 138 network = networkFromQuery 139 } else { 140 network = networkFromScheme 141 } 142 143 // Check if the protocol is also specified with a query parameter. If it is it should not be 144 // conflicting with the value specified in the scheme. 145 var protocol string 146 protocolValues, ok := query["protocol"] 147 if ok { 148 if len(protocolValues) != 1 { 149 err = fmt.Errorf( 150 "expected exactly one value for the 'protocol' query parameter "+ 151 "but found %d", 152 len(protocolValues), 153 ) 154 return 155 } 156 protocolFromQuery := strings.TrimSpace(strings.ToLower(protocolValues[0])) 157 err = checkProtocol(protocolFromQuery) 158 if err != nil { 159 return 160 } 161 if protocolFromScheme != "" && protocolFromScheme != protocolFromQuery { 162 err = fmt.Errorf( 163 "protocol '%s' from query parameter isn't compatible with "+ 164 "protocol '%s' from scheme", 165 protocolFromQuery, protocolFromScheme, 166 ) 167 return 168 } 169 protocol = protocolFromQuery 170 } else { 171 protocol = protocolFromScheme 172 } 173 174 // Set default values for the network and protocol if needed: 175 if network == "" { 176 network = TCPNetwork 177 } 178 if protocol == "" { 179 protocol = HTTPProtocol 180 } 181 182 // Get the host name. Note that the host name is mandatory even when using Unix sockets, 183 // because it is used to populate the `Host` header. 184 host := parsed.Hostname() 185 if host == "" { 186 err = fmt.Errorf("host name is mandatory, but it is empty") 187 return 188 } 189 190 // Get the port number: 191 port := parsed.Port() 192 if port == "" { 193 switch protocol { 194 case HTTPProtocol, H2CProtocol: 195 port = "80" 196 case HTTPSProtocol: 197 port = "443" 198 } 199 } 200 201 // Get the socket from the `socket` query parameter or from the path: 202 var socket string 203 if network == UnixNetwork { 204 socketValues, ok := query["socket"] 205 if ok { 206 if len(socketValues) != 1 { 207 err = fmt.Errorf( 208 "expected exactly one value for the 'socket' query "+ 209 "parameter but found %d", 210 len(socketValues), 211 ) 212 return 213 } 214 socket = socketValues[0] 215 } else { 216 socket = parsed.Path 217 } 218 if socket == "" { 219 err = fmt.Errorf( 220 "expected socket name in the 'socket' query parameter or in " + 221 "the path but both are empty", 222 ) 223 return 224 } 225 } 226 227 // Calculate the URL: 228 url := &neturl.URL{ 229 Host: host, 230 } 231 switch protocol { 232 case HTTPProtocol, H2CProtocol: 233 url.Scheme = "http" 234 if port != "80" { 235 url.Host = fmt.Sprintf("%s:%s", url.Host, port) 236 } 237 case HTTPSProtocol: 238 url.Scheme = "https" 239 if port != "443" { 240 url.Host = fmt.Sprintf("%s:%s", url.Host, port) 241 } 242 } 243 244 // Create and populate the result: 245 result = &ServerAddress{ 246 Text: text, 247 Network: network, 248 Protocol: protocol, 249 Host: host, 250 Port: port, 251 Socket: socket, 252 URL: url, 253 } 254 255 return 256 } 257 258 func parseScheme(ctx context.Context, scheme string) (network, protocol string, 259 err error) { 260 components := strings.Split(strings.ToLower(scheme), "+") 261 if len(components) > 2 { 262 err = fmt.Errorf( 263 "scheme '%s' should have at most two components separated by '+', "+ 264 "but it has %d", 265 scheme, len(components), 266 ) 267 return 268 } 269 for _, component := range components { 270 switch strings.TrimSpace(component) { 271 case TCPNetwork, UnixNetwork: 272 network = component 273 case HTTPProtocol, HTTPSProtocol, H2CProtocol: 274 protocol = component 275 default: 276 err = fmt.Errorf( 277 "component '%s' of scheme '%s' doesn't correspond to any "+ 278 "supported network or protocol, supported networks "+ 279 "are 'tcp' and 'unix', supported protocols are 'http', "+ 280 "'https' and 'h2c'", 281 component, scheme, 282 ) 283 return 284 } 285 } 286 return 287 } 288 289 func checkNetwork(value string) error { 290 switch value { 291 case UnixNetwork, TCPNetwork: 292 return nil 293 default: 294 return fmt.Errorf( 295 "network '%s' isn't valid, valid values are 'unix' and 'tcp'", 296 value, 297 ) 298 } 299 } 300 301 func checkProtocol(value string) error { 302 switch value { 303 case HTTPProtocol, HTTPSProtocol, H2CProtocol: 304 return nil 305 default: 306 return fmt.Errorf( 307 "protocol '%s' isn't valid, valid values are 'http', 'https' "+ 308 "and 'h2c'", 309 value, 310 ) 311 } 312 } 313 314 // Network names: 315 const ( 316 UnixNetwork = "unix" 317 TCPNetwork = "tcp" 318 ) 319 320 // Protocol names: 321 const ( 322 HTTPProtocol = "http" 323 HTTPSProtocol = "https" 324 H2CProtocol = "h2c" 325 )