urlmatch: allow globbing for the URL host part

The URL matching function computes for two URLs whether they match not. The match is performed by splitting up the URL into different parts and then doing an exact comparison with the to-be-matched URL. The main user of `urlmatch` is the configuration subsystem. It allows to set certain configurations based on the URL which is being connected to via keys like `http.<url>.*`. A common use case for this is to set proxies for only some remotes which match the given URL. Unfortunately, having exact matches for all parts of the URL can become quite tedious in some setups. Imagine for example a corporate network where there are dozens or even hundreds of subdomains, which would have to be configured individually. Allow users to write an asterisk '*' in place of any 'host' or 'subdomain' label as part of the host name. For example, "http.https://*.example.com.proxy" sets "http.proxy" for all direct subdomains of "https://example.com", e.g. "https://foo.example.com", but not "https://foo.bar.example.com". Signed-off-by: Patrick Steinhardt <patrick.steinhardt@elego.de> Helped-by: Junio C Hamano <gitster@pobox.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Patrick Steinhardt committed Jan 31, 2017 at 10:01 UTC a272b9e70a48a355b6dd7ff0179c11f8da7ef0f3
3 files changed +121 -5
Documentation/config.txt
+4 -1
@@ -1914,7 +1914,10 @@ http.<url>.*::
1914 must match exactly between the config key and the URL.
1915
1916 . Host/domain name (e.g., `example.com` in `https://example.com/`).
1917 - This field must match exactly between the config key and the URL.
1917 + This field must match between the config key and the URL. It is
1918 + possible to specify a `*` as part of the host name to match all subdomains
1919 + at this level. `https://*.example.com/` for example would match
1920 + `https://foo.example.com/`, but not `https://foo.bar.example.com/`.
1921
1922 . Port number (e.g., `8080` in `http://example.com:8080/`).
1923 This field must match exactly between the config key and the URL.
t/t1300-repo-config.sh
+72
@@ -1187,6 +1187,18 @@ test_expect_success 'urlmatch favors more specific URLs' '
1187 cookieFile = /tmp/user.txt
1188 [http "https://averylonguser@example.com/"]
1189 cookieFile = /tmp/averylonguser.txt
1190 + [http "https://preceding.example.com"]
1191 + cookieFile = /tmp/preceding.txt
1192 + [http "https://*.example.com"]
1193 + cookieFile = /tmp/wildcard.txt
1194 + [http "https://*.example.com/wildcardwithsubdomain"]
1195 + cookieFile = /tmp/wildcardwithsubdomain.txt
1196 + [http "https://trailing.example.com"]
1197 + cookieFile = /tmp/trailing.txt
1198 + [http "https://user@*.example.com/"]
1199 + cookieFile = /tmp/wildcardwithuser.txt
1200 + [http "https://sub.example.com/"]
1201 + cookieFile = /tmp/sub.txt
1202 EOF
1203
1204 echo http.cookiefile /tmp/root.txt >expect &&
@@ -1207,6 +1219,66 @@ test_expect_success 'urlmatch favors more specific URLs' '
1219
1220 echo http.cookiefile /tmp/subdirectory.txt >expect &&
1221 git config --get-urlmatch HTTP https://averylonguser@example.com/subdirectory >actual &&
1222 + test_cmp expect actual &&
1223 +
1224 + echo http.cookiefile /tmp/preceding.txt >expect &&
1225 + git config --get-urlmatch HTTP https://preceding.example.com >actual &&
1226 + test_cmp expect actual &&
1227 +
1228 + echo http.cookiefile /tmp/wildcard.txt >expect &&
1229 + git config --get-urlmatch HTTP https://wildcard.example.com >actual &&
1230 + test_cmp expect actual &&
1231 +
1232 + echo http.cookiefile /tmp/sub.txt >expect &&
1233 + git config --get-urlmatch HTTP https://sub.example.com/wildcardwithsubdomain >actual &&
1234 + test_cmp expect actual &&
1235 +
1236 + echo http.cookiefile /tmp/trailing.txt >expect &&
1237 + git config --get-urlmatch HTTP https://trailing.example.com >actual &&
1238 + test_cmp expect actual &&
1239 +
1240 + echo http.cookiefile /tmp/sub.txt >expect &&
1241 + git config --get-urlmatch HTTP https://user@sub.example.com >actual &&
1242 + test_cmp expect actual
1243 +'
1244 +
1245 +test_expect_success 'urlmatch with wildcard' '
1246 + cat >.git/config <<-\EOF &&
1247 + [http]
1248 + sslVerify
1249 + [http "https://*.example.com"]
1250 + sslVerify = false
1251 + cookieFile = /tmp/cookie.txt
1252 + EOF
1253 +
1254 + test_expect_code 1 git config --bool --get-urlmatch doesnt.exist https://good.example.com >actual &&
1255 + test_must_be_empty actual &&
1256 +
1257 + echo true >expect &&
1258 + git config --bool --get-urlmatch http.SSLverify https://example.com >actual &&
1259 + test_cmp expect actual &&
1260 +
1261 + echo true >expect &&
1262 + git config --bool --get-urlmatch http.SSLverify https://good-example.com >actual &&
1263 + test_cmp expect actual &&
1264 +
1265 + echo true >expect &&
1266 + git config --bool --get-urlmatch http.sslverify https://deep.nested.example.com >actual &&
1267 + test_cmp expect actual &&
1268 +
1269 + echo false >expect &&
1270 + git config --bool --get-urlmatch http.sslverify https://good.example.com >actual &&
1271 + test_cmp expect actual &&
1272 +
1273 + {
1274 + echo http.cookiefile /tmp/cookie.txt &&
1275 + echo http.sslverify false
1276 + } >expect &&
1277 + git config --get-urlmatch HTTP https://good.example.com >actual &&
1278 + test_cmp expect actual &&
1279 +
1280 + echo http.sslverify >expect &&
1281 + git config --get-urlmatch HTTP https://more.example.com.au >actual &&
1282 test_cmp expect actual
1283 '
1284
urlmatch.c
+45 -4
@@ -63,6 +63,49 @@ static int append_normalized_escapes(struct strbuf *buf,
63 return 1;
64 }
65
66 +static const char *end_of_token(const char *s, int c, size_t n)
67 +{
68 + const char *next = memchr(s, c, n);
69 + if (!next)
70 + next = s + n;
71 + return next;
72 +}
73 +
74 +static int match_host(const struct url_info *url_info,
75 + const struct url_info *pattern_info)
76 +{
77 + const char *url = url_info->url + url_info->host_off;
78 + const char *pat = pattern_info->url + pattern_info->host_off;
79 + int url_len = url_info->host_len;
80 + int pat_len = pattern_info->host_len;
81 +
82 + while (url_len && pat_len) {
83 + const char *url_next = end_of_token(url, '.', url_len);
84 + const char *pat_next = end_of_token(pat, '.', pat_len);
85 +
86 + if (pat_next == pat + 1 && pat[0] == '*')
87 + /* wildcard matches anything */
88 + ;
89 + else if ((pat_next - pat) == (url_next - url) &&
90 + !memcmp(url, pat, url_next - url))
91 + /* the components are the same */
92 + ;
93 + else
94 + return 0; /* found an unmatch */
95 +
96 + if (url_next < url + url_len)
97 + url_next++;
98 + url_len -= url_next - url;
99 + url = url_next;
100 + if (pat_next < pat + pat_len)
101 + pat_next++;
102 + pat_len -= pat_next - pat;
103 + pat = pat_next;
104 + }
105 +
106 + return (!url_len && !pat_len);
107 +}
108 +
109 static char *url_normalize_1(const char *url, struct url_info *out_info, char allow_globs)
110 {
111 /*
@@ -467,9 +510,7 @@ static int match_urls(const struct url_info *url,
510 }
511
512 /* check the host */
470 - if (url_prefix->host_len != url->host_len ||
471 - strncmp(url->url + url->host_off,
472 - url_prefix->url + url_prefix->host_off, url->host_len))
513 + if (!match_host(url, url_prefix))
514 return 0; /* host names do not match */
515
516 /* check the port */
@@ -528,7 +569,7 @@ int urlmatch_config_entry(const char *var, const char *value, void *cb)
569 struct url_info norm_info;
570
571 config_url = xmemdupz(key, dot - key);
531 - norm_url = url_normalize(config_url, &norm_info);
572 + norm_url = url_normalize_1(config_url, &norm_info, 1);
573 free(config_url);
574 if (!norm_url)
575 return 0;