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