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  )