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 }