1package url 2 3import ( 4 "strings" 5 6 "github.com/pkg/errors" 7 8 "github.com/mutagen-io/mutagen/pkg/url/forwarding" 9) 10 11const ( 12 // dockerURLPrefix is the lowercase version of the Docker URL prefix. 13 dockerURLPrefix = "docker://" 14 15 // DockerHostEnvironmentVariable is the name of the DOCKER_HOST environment 16 // variable. 17 DockerHostEnvironmentVariable = "DOCKER_HOST" 18 // DockerTLSVerifyEnvironmentVariable is the name of the DOCKER_TLS_VERIFY 19 // environment variable. 20 DockerTLSVerifyEnvironmentVariable = "DOCKER_TLS_VERIFY" 21 // DockerCertPathEnvironmentVariable is the name of the DOCKER_CERT_PATH 22 // environment variable. 23 DockerCertPathEnvironmentVariable = "DOCKER_CERT_PATH" 24 // DockerAPIVersionEnvironmentVariable is the name of the DOCKER_API_VERSION 25 // environment variable. 26 DockerAPIVersionEnvironmentVariable = "DOCKER_API_VERSION" 27) 28 29// DockerEnvironmentVariables is a list of Docker environment variables that 30// should be locked in to the URL at parse time. 31var DockerEnvironmentVariables = []string{ 32 DockerHostEnvironmentVariable, 33 DockerTLSVerifyEnvironmentVariable, 34 DockerCertPathEnvironmentVariable, 35 DockerAPIVersionEnvironmentVariable, 36} 37 38// isDockerURL checks whether or not a URL is a Docker URL. It requires the 39// presence of a Docker protocol prefix. 40func isDockerURL(raw string) bool { 41 return strings.HasPrefix(strings.ToLower(raw), dockerURLPrefix) 42} 43 44// parseDocker parses a Docker URL. 45func parseDocker(raw string, kind Kind, first bool) (*URL, error) { 46 // Strip off the prefix. 47 raw = raw[len(dockerURLPrefix):] 48 49 // Determine the character that splits the container name from the path or 50 // forwarding endpoint component. 51 var splitCharacter rune 52 if kind == Kind_Synchronization { 53 splitCharacter = '/' 54 } else if kind == Kind_Forwarding { 55 splitCharacter = ':' 56 } else { 57 panic("unhandled URL kind") 58 } 59 60 // Parse off the username. If we hit a '/', then we've reached the end of a 61 // container specification and there was no username. Similarly, if we hit 62 // the end of the string without seeing an '@', then there's also no 63 // username specified. Ideally we'd want also to break on any character that 64 // isn't allowed in a username, but that isn't well-defined, even for POSIX 65 // (it's effectively determined by a configurable regular expression - 66 // NAME_REGEX). 67 var username string 68 for i, r := range raw { 69 if r == splitCharacter { 70 break 71 } else if r == '@' { 72 username = raw[:i] 73 raw = raw[i+1:] 74 break 75 } 76 } 77 78 // Split what remains into the container and the path (or forwarding 79 // endpoint, depending on the URL kind). Ideally we'd want to be a bit more 80 // stringent here about what characters we accept in container names, 81 // potentially breaking early with an error if we see a "disallowed" 82 // character, but we're better off just allowing Docker to reject container 83 // names that it doesn't like. 84 var container, path string 85 for i, r := range raw { 86 if r == splitCharacter { 87 container = raw[:i] 88 path = raw[i:] 89 break 90 } 91 } 92 if container == "" { 93 return nil, errors.New("empty container name") 94 } else if path == "" { 95 if kind == Kind_Synchronization { 96 return nil, errors.New("missing path") 97 } else if kind == Kind_Forwarding { 98 return nil, errors.New("missing forwarding endpoint") 99 } else { 100 panic("unhandled URL kind") 101 } 102 } 103 104 // Perform path processing based on URL kind. 105 if kind == Kind_Synchronization { 106 // If the path starts with "/~", then we assume that it's supposed to be 107 // a home-directory-relative path and remove the slash. At this point we 108 // already know that the path starts with "/" since we retained that as 109 // part of the path in the split operation above. 110 if len(path) > 1 && path[1] == '~' { 111 path = path[1:] 112 } 113 114 // If the path is of the form "/" + Windows path, then assume it's 115 // supposed to be a Windows path. This is a heuristic, but a reasonable 116 // one. We do this on all systems (not just on Windows as with SSH URLs) 117 // because users can connect to Windows containers from non-Windows 118 // systems. At this point we already know that the path starts with "/" 119 // since we retained that as part of the path in the split operation 120 // above. 121 if isWindowsPath(path[1:]) { 122 path = path[1:] 123 } 124 } else if kind == Kind_Forwarding { 125 // For forwarding paths, we need to trim the split character at the 126 // beginning. 127 path = path[1:] 128 129 // Parse the forwarding endpoint URL to ensure that it's valid. 130 if _, _, err := forwarding.Parse(path); err != nil { 131 return nil, errors.Wrap(err, "invalid forwarding endpoint URL") 132 } 133 } else { 134 panic("unhandled URL kind") 135 } 136 137 // Loop over and record the values for the Docker environment variables that 138 // we need to preserve. For the variables in question, Docker treats an 139 // empty value the same as an unspecified value, so we always store 140 // something for each variable, even if it's just an empty string to 141 // indicate that its value was empty or unspecified. 142 // 143 // TODO: I'm a little concerned that Docker may eventually add environment 144 // variables where an empty value is not the same as an unspecified value, 145 // but we'll cross that bridge when we come to it. 146 environment := make(map[string]string, len(DockerEnvironmentVariables)) 147 for _, variable := range DockerEnvironmentVariables { 148 value, _ := getEnvironmentVariable(variable, kind, first) 149 environment[variable] = value 150 } 151 152 // Success. 153 return &URL{ 154 Kind: kind, 155 Protocol: Protocol_Docker, 156 User: username, 157 Host: container, 158 Path: path, 159 Environment: environment, 160 }, nil 161} 162