Skip to content

Commit 92c56da

Browse files
committed
Merge branch 'mc/credential-helper-www-authenticate'
Allow information carried on the WWW-AUthenticate header to be passed to the credential helpers. * mc/credential-helper-www-authenticate: credential: add WWW-Authenticate header to cred requests http: read HTTP WWW-Authenticate response headers t5563: add tests for basic and anoymous HTTP access
2 parents af5388d + 5f2117b commit 92c56da

9 files changed

+538
-1
lines changed

Documentation/git-credential.txt

+18-1
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,13 @@ separated by an `=` (equals) sign, followed by a newline.
113113
The key may contain any bytes except `=`, newline, or NUL. The value may
114114
contain any bytes except newline or NUL.
115115

116-
In both cases, all bytes are treated as-is (i.e., there is no quoting,
116+
Attributes with keys that end with C-style array brackets `[]` can have
117+
multiple values. Each instance of a multi-valued attribute forms an
118+
ordered list of values - the order of the repeated attributes defines
119+
the order of the values. An empty multi-valued attribute (`key[]=\n`)
120+
acts to clear any previous entries and reset the list.
121+
122+
In all cases, all bytes are treated as-is (i.e., there is no quoting,
117123
and one cannot transmit a value with newline or NUL in it). The list of
118124
attributes is terminated by a blank line or end-of-file.
119125

@@ -166,6 +172,17 @@ empty string.
166172
Components which are missing from the URL (e.g., there is no
167173
username in the example above) will be left unset.
168174

175+
`wwwauth[]`::
176+
177+
When an HTTP response is received by Git that includes one or more
178+
'WWW-Authenticate' authentication headers, these will be passed by Git
179+
to credential helpers.
180+
+
181+
Each 'WWW-Authenticate' header value is passed as a multi-valued
182+
attribute 'wwwauth[]', where the order of the attributes is the same as
183+
they appear in the HTTP response. This attribute is 'one-way' from Git
184+
to pass additional information to credential helpers.
185+
169186
Unrecognised attributes are silently discarded.
170187

171188
GIT

credential.c

+3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ void credential_clear(struct credential *c)
2323
free(c->username);
2424
free(c->password);
2525
string_list_clear(&c->helpers, 0);
26+
strvec_clear(&c->wwwauth_headers);
2627

2728
credential_init(c);
2829
}
@@ -280,6 +281,8 @@ void credential_write(const struct credential *c, FILE *fp)
280281
credential_write_item(fp, "password_expiry_utc", s, 0);
281282
free(s);
282283
}
284+
for (size_t i = 0; i < c->wwwauth_headers.nr; i++)
285+
credential_write_item(fp, "wwwauth[]", c->wwwauth_headers.v[i], 0);
283286
}
284287

285288
static int run_credential_helper(struct credential *c,

credential.h

+16
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#define CREDENTIAL_H
33

44
#include "string-list.h"
5+
#include "strvec.h"
56

67
/**
78
* The credentials API provides an abstracted way of gathering username and
@@ -115,6 +116,20 @@ struct credential {
115116
*/
116117
struct string_list helpers;
117118

119+
/**
120+
* A `strvec` of WWW-Authenticate header values. Each string
121+
* is the value of a WWW-Authenticate header in an HTTP response,
122+
* in the order they were received in the response.
123+
*/
124+
struct strvec wwwauth_headers;
125+
126+
/**
127+
* Internal use only. Keeps track of if we previously matched against a
128+
* WWW-Authenticate header line in order to re-fold future continuation
129+
* lines into one value.
130+
*/
131+
unsigned header_is_last_match:1;
132+
118133
unsigned approved:1,
119134
configured:1,
120135
quit:1,
@@ -132,6 +147,7 @@ struct credential {
132147
#define CREDENTIAL_INIT { \
133148
.helpers = STRING_LIST_INIT_DUP, \
134149
.password_expiry_utc = TIME_MAX, \
150+
.wwwauth_headers = STRVEC_INIT, \
135151
}
136152

137153
/* Initialize a credential structure, setting all fields to empty. */

git-compat-util.h

+19
Original file line numberDiff line numberDiff line change
@@ -1288,6 +1288,25 @@ static inline int skip_iprefix(const char *str, const char *prefix,
12881288
return 0;
12891289
}
12901290

1291+
/*
1292+
* Like skip_prefix_mem, but compare case-insensitively. Note that the
1293+
* comparison is done via tolower(), so it is strictly ASCII (no multi-byte
1294+
* characters or locale-specific conversions).
1295+
*/
1296+
static inline int skip_iprefix_mem(const char *buf, size_t len,
1297+
const char *prefix,
1298+
const char **out, size_t *outlen)
1299+
{
1300+
do {
1301+
if (!*prefix) {
1302+
*out = buf;
1303+
*outlen = len;
1304+
return 1;
1305+
}
1306+
} while (len-- > 0 && tolower(*buf++) == tolower(*prefix++));
1307+
return 0;
1308+
}
1309+
12911310
static inline int strtoul_ui(char const *s, int base, unsigned int *result)
12921311
{
12931312
unsigned long ul;

http.c

+111
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,115 @@ size_t fwrite_buffer(char *ptr, size_t eltsize, size_t nmemb, void *buffer_)
182182
return nmemb;
183183
}
184184

185+
/*
186+
* A folded header continuation line starts with any number of spaces or
187+
* horizontal tab characters (SP or HTAB) as per RFC 7230 section 3.2.
188+
* It is not a continuation line if the line starts with any other character.
189+
*/
190+
static inline int is_hdr_continuation(const char *ptr, const size_t size)
191+
{
192+
return size && (*ptr == ' ' || *ptr == '\t');
193+
}
194+
195+
static size_t fwrite_wwwauth(char *ptr, size_t eltsize, size_t nmemb, void *p)
196+
{
197+
size_t size = eltsize * nmemb;
198+
struct strvec *values = &http_auth.wwwauth_headers;
199+
struct strbuf buf = STRBUF_INIT;
200+
const char *val;
201+
size_t val_len;
202+
203+
/*
204+
* Header lines may not come NULL-terminated from libcurl so we must
205+
* limit all scans to the maximum length of the header line, or leverage
206+
* strbufs for all operations.
207+
*
208+
* In addition, it is possible that header values can be split over
209+
* multiple lines as per RFC 7230. 'Line folding' has been deprecated
210+
* but older servers may still emit them. A continuation header field
211+
* value is identified as starting with a space or horizontal tab.
212+
*
213+
* The formal definition of a header field as given in RFC 7230 is:
214+
*
215+
* header-field = field-name ":" OWS field-value OWS
216+
*
217+
* field-name = token
218+
* field-value = *( field-content / obs-fold )
219+
* field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
220+
* field-vchar = VCHAR / obs-text
221+
*
222+
* obs-fold = CRLF 1*( SP / HTAB )
223+
* ; obsolete line folding
224+
* ; see Section 3.2.4
225+
*/
226+
227+
/* Start of a new WWW-Authenticate header */
228+
if (skip_iprefix_mem(ptr, size, "www-authenticate:", &val, &val_len)) {
229+
strbuf_add(&buf, val, val_len);
230+
231+
/*
232+
* Strip the CRLF that should be present at the end of each
233+
* field as well as any trailing or leading whitespace from the
234+
* value.
235+
*/
236+
strbuf_trim(&buf);
237+
238+
strvec_push(values, buf.buf);
239+
http_auth.header_is_last_match = 1;
240+
goto exit;
241+
}
242+
243+
/*
244+
* This line could be a continuation of the previously matched header
245+
* field. If this is the case then we should append this value to the
246+
* end of the previously consumed value.
247+
*/
248+
if (http_auth.header_is_last_match && is_hdr_continuation(ptr, size)) {
249+
/*
250+
* Trim the CRLF and any leading or trailing from this line.
251+
*/
252+
strbuf_add(&buf, ptr, size);
253+
strbuf_trim(&buf);
254+
255+
/*
256+
* At this point we should always have at least one existing
257+
* value, even if it is empty. Do not bother appending the new
258+
* value if this continuation header is itself empty.
259+
*/
260+
if (!values->nr) {
261+
BUG("should have at least one existing header value");
262+
} else if (buf.len) {
263+
char *prev = xstrdup(values->v[values->nr - 1]);
264+
265+
/* Join two non-empty values with a single space. */
266+
const char *const sp = *prev ? " " : "";
267+
268+
strvec_pop(values);
269+
strvec_pushf(values, "%s%s%s", prev, sp, buf.buf);
270+
free(prev);
271+
}
272+
273+
goto exit;
274+
}
275+
276+
/* Not a continuation of a previously matched auth header line. */
277+
http_auth.header_is_last_match = 0;
278+
279+
/*
280+
* If this is a HTTP status line and not a header field, this signals
281+
* a different HTTP response. libcurl writes all the output of all
282+
* response headers of all responses, including redirects.
283+
* We only care about the last HTTP request response's headers so clear
284+
* the existing array.
285+
*/
286+
if (skip_iprefix_mem(ptr, size, "http/", &val, &val_len))
287+
strvec_clear(values);
288+
289+
exit:
290+
strbuf_release(&buf);
291+
return size;
292+
}
293+
185294
size_t fwrite_null(char *ptr, size_t eltsize, size_t nmemb, void *strbuf)
186295
{
187296
return nmemb;
@@ -1896,6 +2005,8 @@ static int http_request(const char *url,
18962005
fwrite_buffer);
18972006
}
18982007

2008+
curl_easy_setopt(slot->curl, CURLOPT_HEADERFUNCTION, fwrite_wwwauth);
2009+
18992010
accept_language = http_get_accept_language_header();
19002011

19012012
if (accept_language)

t/lib-httpd.sh

+1
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ prepare_httpd() {
142142
install_script error-smart-http.sh
143143
install_script error.sh
144144
install_script apply-one-time-perl.sh
145+
install_script nph-custom-auth.sh
145146

146147
ln -s "$LIB_HTTPD_MODULE_PATH" "$HTTPD_ROOT_PATH/modules"
147148

t/lib-httpd/apache.conf

+6
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,11 @@ Alias /auth/dumb/ www/auth/dumb/
141141
SetEnv GIT_HTTP_EXPORT_ALL
142142
SetEnv GIT_PROTOCOL
143143
</LocationMatch>
144+
<LocationMatch /custom_auth/>
145+
SetEnv GIT_EXEC_PATH ${GIT_EXEC_PATH}
146+
SetEnv GIT_HTTP_EXPORT_ALL
147+
CGIPassAuth on
148+
</LocationMatch>
144149
ScriptAlias /smart/incomplete_length/git-upload-pack incomplete-length-upload-pack-v2-http.sh/
145150
ScriptAlias /smart/incomplete_body/git-upload-pack incomplete-body-upload-pack-v2-http.sh/
146151
ScriptAlias /smart/no_report/git-receive-pack error-no-report.sh/
@@ -150,6 +155,7 @@ ScriptAlias /broken_smart/ broken-smart-http.sh/
150155
ScriptAlias /error_smart/ error-smart-http.sh/
151156
ScriptAlias /error/ error.sh/
152157
ScriptAliasMatch /one_time_perl/(.*) apply-one-time-perl.sh/$1
158+
ScriptAliasMatch /custom_auth/(.*) nph-custom-auth.sh/$1
153159
<Directory ${GIT_EXEC_PATH}>
154160
Options FollowSymlinks
155161
</Directory>

t/lib-httpd/nph-custom-auth.sh

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#!/bin/sh
2+
3+
VALID_CREDS_FILE=custom-auth.valid
4+
CHALLENGE_FILE=custom-auth.challenge
5+
6+
#
7+
# If $VALID_CREDS_FILE exists in $HTTPD_ROOT_PATH, consider each line as a valid
8+
# credential for the current request. Each line in the file is considered a
9+
# valid HTTP Authorization header value. For example:
10+
#
11+
# Basic YWxpY2U6c2VjcmV0LXBhc3N3ZA==
12+
#
13+
# If $CHALLENGE_FILE exists in $HTTPD_ROOT_PATH, output the contents as headers
14+
# in a 401 response if no valid authentication credentials were included in the
15+
# request. For example:
16+
#
17+
# WWW-Authenticate: Bearer authorize_uri="id.example.com" p=1 q=0
18+
# WWW-Authenticate: Basic realm="example.com"
19+
#
20+
21+
if test -n "$HTTP_AUTHORIZATION" && \
22+
grep -Fqsx "${HTTP_AUTHORIZATION}" "$VALID_CREDS_FILE"
23+
then
24+
# Note that although git-http-backend returns a status line, it
25+
# does so using a CGI 'Status' header. Because this script is an
26+
# No Parsed Headers (NPH) script, we must return a real HTTP
27+
# status line.
28+
# This is only a test script, so we don't bother to check for
29+
# the actual status from git-http-backend and always return 200.
30+
echo 'HTTP/1.1 200 OK'
31+
exec "$GIT_EXEC_PATH"/git-http-backend
32+
fi
33+
34+
echo 'HTTP/1.1 401 Authorization Required'
35+
if test -f "$CHALLENGE_FILE"
36+
then
37+
cat "$CHALLENGE_FILE"
38+
fi
39+
echo

0 commit comments

Comments
 (0)