github.com/mutagen-io/mutagen@v0.18.0-rc1/pkg/url/url.go (about)

     1  package url
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"math"
     7  	"path/filepath"
     8  
     9  	"github.com/mutagen-io/mutagen/pkg/comparison"
    10  	"github.com/mutagen-io/mutagen/pkg/extension"
    11  	"github.com/mutagen-io/mutagen/pkg/url/forwarding"
    12  )
    13  
    14  // Supported returns whether or not a URL kind is supported.
    15  func (k Kind) Supported() bool {
    16  	switch k {
    17  	case Kind_Synchronization:
    18  		return true
    19  	case Kind_Forwarding:
    20  		return true
    21  	default:
    22  		return false
    23  	}
    24  }
    25  
    26  // MarshalText implements encoding.TextMarshaler.MarshalText.
    27  func (p Protocol) MarshalText() ([]byte, error) {
    28  	var result string
    29  	switch p {
    30  	case Protocol_Local:
    31  		result = "local"
    32  	case Protocol_SSH:
    33  		result = "ssh"
    34  	case Protocol_Docker:
    35  		result = "docker"
    36  	default:
    37  		result = "unknown"
    38  	}
    39  	return []byte(result), nil
    40  }
    41  
    42  // UnmarshalText implements encoding.TextUnmarshaler.UnmarshalText.
    43  func (p *Protocol) UnmarshalText(textBytes []byte) error {
    44  	// Convert the bytes to a string.
    45  	text := string(textBytes)
    46  
    47  	// Convert to a protocol.
    48  	switch text {
    49  	case "local":
    50  		*p = Protocol_Local
    51  	case "ssh":
    52  		*p = Protocol_SSH
    53  	case "docker":
    54  		*p = Protocol_Docker
    55  	default:
    56  		return fmt.Errorf("unknown protocol specification: %s", text)
    57  	}
    58  
    59  	// Success.
    60  	return nil
    61  }
    62  
    63  // EnsureValid ensures that URL's invariants are respected.
    64  func (u *URL) EnsureValid() error {
    65  	// Ensure that the URL is non-nil.
    66  	if u == nil {
    67  		return errors.New("nil URL")
    68  	}
    69  
    70  	// Ensure that the kind is supported.
    71  	if !u.Kind.Supported() {
    72  		return errors.New("unsupported URL kind")
    73  	}
    74  
    75  	// Validate the User, Host, Port, and Environment components based on
    76  	// protocol.
    77  	if u.Protocol == Protocol_Local {
    78  		if u.User != "" {
    79  			return errors.New("local URL with non-empty username")
    80  		} else if u.Host != "" {
    81  			return errors.New("local URL with non-empty hostname")
    82  		} else if u.Port != 0 {
    83  			return errors.New("local URL with non-zero port")
    84  		} else if len(u.Environment) != 0 {
    85  			return errors.New("local URL with environment variables")
    86  		} else if len(u.Parameters) != 0 {
    87  			return errors.New("local URL with parameters")
    88  		}
    89  	} else if u.Protocol == Protocol_SSH {
    90  		if u.Host == "" {
    91  			return errors.New("SSH URL with empty hostname")
    92  		} else if u.Port > math.MaxUint16 {
    93  			return errors.New("SSH URL with invalid port")
    94  		} else if len(u.Environment) != 0 {
    95  			return errors.New("SSH URL with environment variables")
    96  		}
    97  	} else if u.Protocol == Protocol_Docker {
    98  		// In the case of Docker, we intentionally avoid validating environment
    99  		// variables since the values used could change over time. Since we
   100  		// default to empty values for unspecified environment variables, this
   101  		// works out fine, at least so long as Docker continues to treat empty
   102  		// environment variables the same as unspecified ones.
   103  		if u.Host == "" {
   104  			return errors.New("Docker URL with empty container identifier")
   105  		} else if u.Port != 0 {
   106  			return errors.New("Docker URL with non-zero port")
   107  		}
   108  	} else {
   109  		return errors.New("unknown or unsupported protocol")
   110  	}
   111  
   112  	// Validate the path component depending on the URL kind.
   113  	if u.Kind == Kind_Synchronization {
   114  		// Ensure the path is non-empty.
   115  		if u.Path == "" {
   116  			return errors.New("empty path")
   117  		}
   118  
   119  		// If this is a local URL, then ensure that the path is absolute.
   120  		//
   121  		// HACK: The Mutagen Extension for Docker Desktop needs to avoid this
   122  		// check because of its internal faux-local URLs. In particular, Windows
   123  		// absolute paths will appear as non-absolute in the extension's
   124  		// Linux-based backend container. Technically we only need to avoid this
   125  		// check for alpha URLs, but since we control all of the extension's
   126  		// URLs, and since we don't know which endpoint this URL is targeting,
   127  		// we just disable it entirely when running in the extension.
   128  		if !extension.EnvironmentIsExtension() {
   129  			if u.Protocol == Protocol_Local && !filepath.IsAbs(u.Path) {
   130  				return errors.New("local URL with relative path")
   131  			}
   132  		}
   133  
   134  		// If this is a Docker URL, we can actually do a bit of additional
   135  		// validation.
   136  		if u.Protocol == Protocol_Docker {
   137  			if !(u.Path[0] == '/' || u.Path[0] == '~' || isWindowsPath(u.Path)) {
   138  				return errors.New("incorrect first path character")
   139  			}
   140  		}
   141  	} else if u.Kind == Kind_Forwarding {
   142  		// Parse the forwarding endpoint URL to ensure that it's valid.
   143  		protocol, address, err := forwarding.Parse(u.Path)
   144  		if err != nil {
   145  			return fmt.Errorf("invalid forwarding endpoint URL: %w", err)
   146  		}
   147  
   148  		// If this is a local URL and represents a Unix domain socket endpoint,
   149  		// then ensure that the socket path is absolute.
   150  		if u.Protocol == Protocol_Local && protocol == "unix" && !filepath.IsAbs(address) {
   151  			return errors.New("local Unix domain socket URL with relative path")
   152  		}
   153  
   154  		// TODO: It would be nice to perform some sort of validation on Windows
   155  		// named pipe addresses, but there's not much we can do because the
   156  		// allowed formats vary between source and destination endpoints (so
   157  		// we'd have to weave that information through this function). The only
   158  		// difference is that the ServerName component (see the link below) must
   159  		// be "." for source endpoints but can also name a remote server in the
   160  		// case of destination endpoints. But that's not really the biggest
   161  		// issue. The problem is that the name specification is kind of vague.
   162  		// It says that the PipeName component (again, see the link below) "can
   163  		// include any character other than a backslash, including numbers and
   164  		// special characters", but it doesn't mention whitespace characters
   165  		// (for example a newline character), which, as far as I'm aware, are
   166  		// not allowed. It also limits the "entire pipe name string" to 256
   167  		// characters, but it's not clear if this refers to the PipeName
   168  		// component or the entire address. Finding an appropriate matcher for
   169  		// possible server names is also an uphill battle. This might be
   170  		// specified in the UNC specification. In the end though, we're probably
   171  		// just better off letting the OS decide what to accept and simply
   172  		// returning its errors directly. For further reading, see:
   173  		// https://docs.microsoft.com/en-us/windows/win32/ipc/pipe-names
   174  	}
   175  
   176  	// Success.
   177  	return nil
   178  }
   179  
   180  // Equal returns whether or not the URL is equivalent to another. The result of
   181  // this method is only valid if both URLs are valid.
   182  func (u *URL) Equal(other *URL) bool {
   183  	// Ensure that both are non-nil.
   184  	if u == nil || other == nil {
   185  		return false
   186  	}
   187  
   188  	// Perform an equivalence check.
   189  	return u.Kind == other.Kind &&
   190  		u.Protocol == other.Protocol &&
   191  		u.User == other.User &&
   192  		u.Host == other.Host &&
   193  		u.Port == other.Port &&
   194  		u.Path == other.Path &&
   195  		comparison.StringMapsEqual(u.Environment, other.Environment) &&
   196  		comparison.StringMapsEqual(u.Parameters, other.Parameters)
   197  }