]>
Commit | Line | Data |
---|---|---|
22a2d3d5 UG |
1 | /* |
2 | * Copyright (C) the libgit2 contributors. All rights reserved. | |
3 | * | |
4 | * This file is part of libgit2, distributed under the GNU GPL v2 with | |
5 | * a Linking Exception. For full terms see the included COPYING file. | |
6 | */ | |
7 | ||
8 | #include "common.h" | |
9 | #include "git2.h" | |
10 | #include "http_parser.h" | |
11 | #include "vector.h" | |
12 | #include "trace.h" | |
22a2d3d5 UG |
13 | #include "httpclient.h" |
14 | #include "http.h" | |
15 | #include "auth.h" | |
16 | #include "auth_negotiate.h" | |
17 | #include "auth_ntlm.h" | |
18 | #include "git2/sys/credential.h" | |
19 | #include "net.h" | |
20 | #include "stream.h" | |
21 | #include "streams/socket.h" | |
22 | #include "streams/tls.h" | |
23 | #include "auth.h" | |
24 | ||
25 | static git_http_auth_scheme auth_schemes[] = { | |
26 | { GIT_HTTP_AUTH_NEGOTIATE, "Negotiate", GIT_CREDENTIAL_DEFAULT, git_http_auth_negotiate }, | |
27 | { GIT_HTTP_AUTH_NTLM, "NTLM", GIT_CREDENTIAL_USERPASS_PLAINTEXT, git_http_auth_ntlm }, | |
28 | { GIT_HTTP_AUTH_BASIC, "Basic", GIT_CREDENTIAL_USERPASS_PLAINTEXT, git_http_auth_basic }, | |
29 | }; | |
30 | ||
31 | /* | |
32 | * Use a 16kb read buffer to match the maximum size of a TLS packet. This | |
33 | * is critical for compatibility with SecureTransport, which will always do | |
34 | * a network read on every call, even if it has data buffered to return to | |
35 | * you. That buffered data may be the _end_ of a keep-alive response, so | |
36 | * if SecureTransport performs another network read, it will wait until the | |
37 | * server ultimately times out before it returns that buffered data to you. | |
38 | * Since SecureTransport only reads a single TLS packet at a time, by | |
39 | * calling it with a read buffer that is the maximum size of a TLS packet, | |
40 | * we ensure that it will never buffer. | |
41 | */ | |
42 | #define GIT_READ_BUFFER_SIZE (16 * 1024) | |
43 | ||
44 | typedef struct { | |
45 | git_net_url url; | |
46 | git_stream *stream; | |
47 | ||
48 | git_vector auth_challenges; | |
49 | git_http_auth_context *auth_context; | |
50 | } git_http_server; | |
51 | ||
52 | typedef enum { | |
53 | PROXY = 1, | |
54 | SERVER | |
55 | } git_http_server_t; | |
56 | ||
57 | typedef enum { | |
58 | NONE = 0, | |
59 | SENDING_REQUEST, | |
60 | SENDING_BODY, | |
61 | SENT_REQUEST, | |
62 | HAS_EARLY_RESPONSE, | |
63 | READING_RESPONSE, | |
64 | READING_BODY, | |
65 | DONE | |
66 | } http_client_state; | |
67 | ||
68 | /* Parser state */ | |
69 | typedef enum { | |
70 | PARSE_HEADER_NONE = 0, | |
71 | PARSE_HEADER_NAME, | |
72 | PARSE_HEADER_VALUE, | |
73 | PARSE_HEADER_COMPLETE | |
74 | } parse_header_state; | |
75 | ||
76 | typedef enum { | |
77 | PARSE_STATUS_OK, | |
78 | PARSE_STATUS_NO_OUTPUT, | |
79 | PARSE_STATUS_ERROR | |
80 | } parse_status; | |
81 | ||
82 | typedef struct { | |
83 | git_http_client *client; | |
84 | git_http_response *response; | |
85 | ||
86 | /* Temporary buffers to avoid extra mallocs */ | |
e579e0f7 MB |
87 | git_str parse_header_name; |
88 | git_str parse_header_value; | |
22a2d3d5 UG |
89 | |
90 | /* Parser state */ | |
91 | int error; | |
92 | parse_status parse_status; | |
93 | ||
94 | /* Headers parsing */ | |
95 | parse_header_state parse_header_state; | |
96 | ||
97 | /* Body parsing */ | |
98 | char *output_buf; /* Caller's output buffer */ | |
99 | size_t output_size; /* Size of caller's output buffer */ | |
100 | size_t output_written; /* Bytes we've written to output buffer */ | |
101 | } http_parser_context; | |
102 | ||
103 | /* HTTP client connection */ | |
104 | struct git_http_client { | |
105 | git_http_client_options opts; | |
106 | ||
107 | /* Are we writing to the proxy or server, and state of the client. */ | |
108 | git_http_server_t current_server; | |
109 | http_client_state state; | |
110 | ||
111 | http_parser parser; | |
112 | ||
113 | git_http_server server; | |
114 | git_http_server proxy; | |
115 | ||
116 | unsigned request_count; | |
117 | unsigned connected : 1, | |
118 | proxy_connected : 1, | |
119 | keepalive : 1, | |
120 | request_chunked : 1; | |
121 | ||
122 | /* Temporary buffers to avoid extra mallocs */ | |
e579e0f7 MB |
123 | git_str request_msg; |
124 | git_str read_buf; | |
22a2d3d5 UG |
125 | |
126 | /* A subset of information from the request */ | |
127 | size_t request_body_len, | |
128 | request_body_remain; | |
129 | ||
130 | /* | |
131 | * When state == HAS_EARLY_RESPONSE, the response of our proxy | |
132 | * that we have buffered and will deliver during read_response. | |
133 | */ | |
134 | git_http_response early_response; | |
135 | }; | |
136 | ||
137 | bool git_http_response_is_redirect(git_http_response *response) | |
138 | { | |
139 | return (response->status == GIT_HTTP_MOVED_PERMANENTLY || | |
140 | response->status == GIT_HTTP_FOUND || | |
141 | response->status == GIT_HTTP_SEE_OTHER || | |
142 | response->status == GIT_HTTP_TEMPORARY_REDIRECT || | |
143 | response->status == GIT_HTTP_PERMANENT_REDIRECT); | |
144 | } | |
145 | ||
146 | void git_http_response_dispose(git_http_response *response) | |
147 | { | |
c25aa7cd PP |
148 | if (!response) |
149 | return; | |
22a2d3d5 UG |
150 | |
151 | git__free(response->content_type); | |
152 | git__free(response->location); | |
153 | ||
154 | memset(response, 0, sizeof(git_http_response)); | |
155 | } | |
156 | ||
157 | static int on_header_complete(http_parser *parser) | |
158 | { | |
159 | http_parser_context *ctx = (http_parser_context *) parser->data; | |
160 | git_http_client *client = ctx->client; | |
161 | git_http_response *response = ctx->response; | |
162 | ||
e579e0f7 MB |
163 | git_str *name = &ctx->parse_header_name; |
164 | git_str *value = &ctx->parse_header_value; | |
22a2d3d5 UG |
165 | |
166 | if (!strcasecmp("Content-Type", name->ptr)) { | |
167 | if (response->content_type) { | |
168 | git_error_set(GIT_ERROR_HTTP, | |
169 | "multiple content-type headers"); | |
170 | return -1; | |
171 | } | |
172 | ||
173 | response->content_type = | |
174 | git__strndup(value->ptr, value->size); | |
175 | GIT_ERROR_CHECK_ALLOC(ctx->response->content_type); | |
176 | } else if (!strcasecmp("Content-Length", name->ptr)) { | |
177 | int64_t len; | |
178 | ||
179 | if (response->content_length) { | |
180 | git_error_set(GIT_ERROR_HTTP, | |
181 | "multiple content-length headers"); | |
182 | return -1; | |
183 | } | |
184 | ||
185 | if (git__strntol64(&len, value->ptr, value->size, | |
186 | NULL, 10) < 0 || len < 0) { | |
187 | git_error_set(GIT_ERROR_HTTP, | |
188 | "invalid content-length"); | |
189 | return -1; | |
190 | } | |
191 | ||
192 | response->content_length = (size_t)len; | |
193 | } else if (!strcasecmp("Transfer-Encoding", name->ptr) && | |
194 | !strcasecmp("chunked", value->ptr)) { | |
195 | ctx->response->chunked = 1; | |
e579e0f7 | 196 | } else if (!strcasecmp("Proxy-Authenticate", git_str_cstr(name))) { |
22a2d3d5 UG |
197 | char *dup = git__strndup(value->ptr, value->size); |
198 | GIT_ERROR_CHECK_ALLOC(dup); | |
199 | ||
200 | if (git_vector_insert(&client->proxy.auth_challenges, dup) < 0) | |
201 | return -1; | |
202 | } else if (!strcasecmp("WWW-Authenticate", name->ptr)) { | |
203 | char *dup = git__strndup(value->ptr, value->size); | |
204 | GIT_ERROR_CHECK_ALLOC(dup); | |
205 | ||
206 | if (git_vector_insert(&client->server.auth_challenges, dup) < 0) | |
207 | return -1; | |
208 | } else if (!strcasecmp("Location", name->ptr)) { | |
209 | if (response->location) { | |
210 | git_error_set(GIT_ERROR_HTTP, | |
211 | "multiple location headers"); | |
212 | return -1; | |
213 | } | |
214 | ||
215 | response->location = git__strndup(value->ptr, value->size); | |
216 | GIT_ERROR_CHECK_ALLOC(response->location); | |
217 | } | |
218 | ||
219 | return 0; | |
220 | } | |
221 | ||
222 | static int on_header_field(http_parser *parser, const char *str, size_t len) | |
223 | { | |
224 | http_parser_context *ctx = (http_parser_context *) parser->data; | |
225 | ||
226 | switch (ctx->parse_header_state) { | |
227 | /* | |
228 | * We last saw a header value, process the name/value pair and | |
229 | * get ready to handle this new name. | |
230 | */ | |
231 | case PARSE_HEADER_VALUE: | |
232 | if (on_header_complete(parser) < 0) | |
233 | return ctx->parse_status = PARSE_STATUS_ERROR; | |
234 | ||
e579e0f7 MB |
235 | git_str_clear(&ctx->parse_header_name); |
236 | git_str_clear(&ctx->parse_header_value); | |
22a2d3d5 UG |
237 | /* Fall through */ |
238 | ||
239 | case PARSE_HEADER_NONE: | |
240 | case PARSE_HEADER_NAME: | |
241 | ctx->parse_header_state = PARSE_HEADER_NAME; | |
242 | ||
e579e0f7 | 243 | if (git_str_put(&ctx->parse_header_name, str, len) < 0) |
22a2d3d5 UG |
244 | return ctx->parse_status = PARSE_STATUS_ERROR; |
245 | ||
246 | break; | |
247 | ||
248 | default: | |
249 | git_error_set(GIT_ERROR_HTTP, | |
250 | "header name seen at unexpected time"); | |
251 | return ctx->parse_status = PARSE_STATUS_ERROR; | |
252 | } | |
253 | ||
254 | return 0; | |
255 | } | |
256 | ||
257 | static int on_header_value(http_parser *parser, const char *str, size_t len) | |
258 | { | |
259 | http_parser_context *ctx = (http_parser_context *) parser->data; | |
260 | ||
261 | switch (ctx->parse_header_state) { | |
262 | case PARSE_HEADER_NAME: | |
263 | case PARSE_HEADER_VALUE: | |
264 | ctx->parse_header_state = PARSE_HEADER_VALUE; | |
265 | ||
e579e0f7 | 266 | if (git_str_put(&ctx->parse_header_value, str, len) < 0) |
22a2d3d5 UG |
267 | return ctx->parse_status = PARSE_STATUS_ERROR; |
268 | ||
269 | break; | |
270 | ||
271 | default: | |
272 | git_error_set(GIT_ERROR_HTTP, | |
273 | "header value seen at unexpected time"); | |
274 | return ctx->parse_status = PARSE_STATUS_ERROR; | |
275 | } | |
276 | ||
277 | return 0; | |
278 | } | |
279 | ||
280 | GIT_INLINE(bool) challenge_matches_scheme( | |
281 | const char *challenge, | |
282 | git_http_auth_scheme *scheme) | |
283 | { | |
284 | const char *scheme_name = scheme->name; | |
285 | size_t scheme_len = strlen(scheme_name); | |
286 | ||
287 | if (!strncasecmp(challenge, scheme_name, scheme_len) && | |
288 | (challenge[scheme_len] == '\0' || challenge[scheme_len] == ' ')) | |
289 | return true; | |
290 | ||
291 | return false; | |
292 | } | |
293 | ||
294 | static git_http_auth_scheme *scheme_for_challenge(const char *challenge) | |
295 | { | |
296 | size_t i; | |
297 | ||
298 | for (i = 0; i < ARRAY_SIZE(auth_schemes); i++) { | |
299 | if (challenge_matches_scheme(challenge, &auth_schemes[i])) | |
300 | return &auth_schemes[i]; | |
301 | } | |
302 | ||
303 | return NULL; | |
304 | } | |
305 | ||
306 | GIT_INLINE(void) collect_authinfo( | |
307 | unsigned int *schemetypes, | |
308 | unsigned int *credtypes, | |
309 | git_vector *challenges) | |
310 | { | |
311 | git_http_auth_scheme *scheme; | |
312 | const char *challenge; | |
313 | size_t i; | |
314 | ||
315 | *schemetypes = 0; | |
316 | *credtypes = 0; | |
317 | ||
318 | git_vector_foreach(challenges, i, challenge) { | |
319 | if ((scheme = scheme_for_challenge(challenge)) != NULL) { | |
320 | *schemetypes |= scheme->type; | |
321 | *credtypes |= scheme->credtypes; | |
322 | } | |
323 | } | |
324 | } | |
325 | ||
326 | static int resend_needed(git_http_client *client, git_http_response *response) | |
327 | { | |
328 | git_http_auth_context *auth_context; | |
329 | ||
330 | if (response->status == GIT_HTTP_STATUS_UNAUTHORIZED && | |
331 | (auth_context = client->server.auth_context) && | |
332 | auth_context->is_complete && | |
333 | !auth_context->is_complete(auth_context)) | |
334 | return 1; | |
335 | ||
336 | if (response->status == GIT_HTTP_STATUS_PROXY_AUTHENTICATION_REQUIRED && | |
337 | (auth_context = client->proxy.auth_context) && | |
338 | auth_context->is_complete && | |
339 | !auth_context->is_complete(auth_context)) | |
340 | return 1; | |
341 | ||
342 | return 0; | |
343 | } | |
344 | ||
345 | static int on_headers_complete(http_parser *parser) | |
346 | { | |
347 | http_parser_context *ctx = (http_parser_context *) parser->data; | |
348 | ||
349 | /* Finalize the last seen header */ | |
350 | switch (ctx->parse_header_state) { | |
351 | case PARSE_HEADER_VALUE: | |
352 | if (on_header_complete(parser) < 0) | |
353 | return ctx->parse_status = PARSE_STATUS_ERROR; | |
354 | ||
355 | /* Fall through */ | |
356 | ||
357 | case PARSE_HEADER_NONE: | |
358 | ctx->parse_header_state = PARSE_HEADER_COMPLETE; | |
359 | break; | |
360 | ||
361 | default: | |
362 | git_error_set(GIT_ERROR_HTTP, | |
363 | "header completion at unexpected time"); | |
364 | return ctx->parse_status = PARSE_STATUS_ERROR; | |
365 | } | |
366 | ||
367 | ctx->response->status = parser->status_code; | |
368 | ctx->client->keepalive = http_should_keep_alive(parser); | |
369 | ||
370 | /* Prepare for authentication */ | |
371 | collect_authinfo(&ctx->response->server_auth_schemetypes, | |
372 | &ctx->response->server_auth_credtypes, | |
373 | &ctx->client->server.auth_challenges); | |
374 | collect_authinfo(&ctx->response->proxy_auth_schemetypes, | |
375 | &ctx->response->proxy_auth_credtypes, | |
376 | &ctx->client->proxy.auth_challenges); | |
377 | ||
378 | ctx->response->resend_credentials = resend_needed(ctx->client, | |
379 | ctx->response); | |
380 | ||
381 | /* Stop parsing. */ | |
382 | http_parser_pause(parser, 1); | |
383 | ||
384 | if (ctx->response->content_type || ctx->response->chunked) | |
385 | ctx->client->state = READING_BODY; | |
386 | else | |
387 | ctx->client->state = DONE; | |
388 | ||
389 | return 0; | |
390 | } | |
391 | ||
392 | static int on_body(http_parser *parser, const char *buf, size_t len) | |
393 | { | |
394 | http_parser_context *ctx = (http_parser_context *) parser->data; | |
395 | size_t max_len; | |
396 | ||
397 | /* Saw data when we expected not to (eg, in consume_response_body) */ | |
ad5611d8 | 398 | if (ctx->output_buf == NULL || ctx->output_size == 0) { |
22a2d3d5 UG |
399 | ctx->parse_status = PARSE_STATUS_NO_OUTPUT; |
400 | return 0; | |
401 | } | |
402 | ||
c25aa7cd | 403 | GIT_ASSERT(ctx->output_size >= ctx->output_written); |
22a2d3d5 UG |
404 | |
405 | max_len = min(ctx->output_size - ctx->output_written, len); | |
406 | max_len = min(max_len, INT_MAX); | |
407 | ||
408 | memcpy(ctx->output_buf + ctx->output_written, buf, max_len); | |
409 | ctx->output_written += max_len; | |
410 | ||
411 | return 0; | |
412 | } | |
413 | ||
414 | static int on_message_complete(http_parser *parser) | |
415 | { | |
416 | http_parser_context *ctx = (http_parser_context *) parser->data; | |
417 | ||
418 | ctx->client->state = DONE; | |
419 | return 0; | |
420 | } | |
421 | ||
422 | GIT_INLINE(int) stream_write( | |
423 | git_http_server *server, | |
424 | const char *data, | |
425 | size_t len) | |
426 | { | |
427 | git_trace(GIT_TRACE_TRACE, | |
428 | "Sending request:\n%.*s", (int)len, data); | |
429 | ||
430 | return git_stream__write_full(server->stream, data, len, 0); | |
431 | } | |
432 | ||
433 | GIT_INLINE(int) client_write_request(git_http_client *client) | |
434 | { | |
435 | git_stream *stream = client->current_server == PROXY ? | |
436 | client->proxy.stream : client->server.stream; | |
437 | ||
438 | git_trace(GIT_TRACE_TRACE, | |
439 | "Sending request:\n%.*s", | |
440 | (int)client->request_msg.size, client->request_msg.ptr); | |
441 | ||
442 | return git_stream__write_full(stream, | |
443 | client->request_msg.ptr, | |
444 | client->request_msg.size, | |
445 | 0); | |
446 | } | |
447 | ||
448 | static const char *name_for_method(git_http_method method) | |
449 | { | |
450 | switch (method) { | |
451 | case GIT_HTTP_METHOD_GET: | |
452 | return "GET"; | |
453 | case GIT_HTTP_METHOD_POST: | |
454 | return "POST"; | |
455 | case GIT_HTTP_METHOD_CONNECT: | |
456 | return "CONNECT"; | |
457 | } | |
458 | ||
459 | return NULL; | |
460 | } | |
461 | ||
462 | /* | |
463 | * Find the scheme that is suitable for the given credentials, based on the | |
464 | * server's auth challenges. | |
465 | */ | |
466 | static bool best_scheme_and_challenge( | |
467 | git_http_auth_scheme **scheme_out, | |
468 | const char **challenge_out, | |
469 | git_vector *challenges, | |
470 | git_credential *credentials) | |
471 | { | |
472 | const char *challenge; | |
473 | size_t i, j; | |
474 | ||
475 | for (i = 0; i < ARRAY_SIZE(auth_schemes); i++) { | |
476 | git_vector_foreach(challenges, j, challenge) { | |
477 | git_http_auth_scheme *scheme = &auth_schemes[i]; | |
478 | ||
479 | if (challenge_matches_scheme(challenge, scheme) && | |
480 | (scheme->credtypes & credentials->credtype)) { | |
481 | *scheme_out = scheme; | |
482 | *challenge_out = challenge; | |
483 | return true; | |
484 | } | |
485 | } | |
486 | } | |
487 | ||
488 | return false; | |
489 | } | |
490 | ||
491 | /* | |
492 | * Find the challenge from the server for our current auth context. | |
493 | */ | |
494 | static const char *challenge_for_context( | |
495 | git_vector *challenges, | |
496 | git_http_auth_context *auth_ctx) | |
497 | { | |
498 | const char *challenge; | |
499 | size_t i, j; | |
500 | ||
501 | for (i = 0; i < ARRAY_SIZE(auth_schemes); i++) { | |
502 | if (auth_schemes[i].type == auth_ctx->type) { | |
503 | git_http_auth_scheme *scheme = &auth_schemes[i]; | |
504 | ||
505 | git_vector_foreach(challenges, j, challenge) { | |
506 | if (challenge_matches_scheme(challenge, scheme)) | |
507 | return challenge; | |
508 | } | |
509 | } | |
510 | } | |
511 | ||
512 | return NULL; | |
513 | } | |
514 | ||
515 | static const char *init_auth_context( | |
516 | git_http_server *server, | |
517 | git_vector *challenges, | |
518 | git_credential *credentials) | |
519 | { | |
520 | git_http_auth_scheme *scheme; | |
521 | const char *challenge; | |
522 | int error; | |
523 | ||
524 | if (!best_scheme_and_challenge(&scheme, &challenge, challenges, credentials)) { | |
525 | git_error_set(GIT_ERROR_HTTP, "could not find appropriate mechanism for credentials"); | |
526 | return NULL; | |
527 | } | |
528 | ||
529 | error = scheme->init_context(&server->auth_context, &server->url); | |
530 | ||
531 | if (error == GIT_PASSTHROUGH) { | |
532 | git_error_set(GIT_ERROR_HTTP, "'%s' authentication is not supported", scheme->name); | |
533 | return NULL; | |
534 | } | |
535 | ||
536 | return challenge; | |
537 | } | |
538 | ||
539 | static void free_auth_context(git_http_server *server) | |
540 | { | |
541 | if (!server->auth_context) | |
542 | return; | |
543 | ||
544 | if (server->auth_context->free) | |
545 | server->auth_context->free(server->auth_context); | |
546 | ||
547 | server->auth_context = NULL; | |
548 | } | |
549 | ||
550 | static int apply_credentials( | |
e579e0f7 | 551 | git_str *buf, |
22a2d3d5 UG |
552 | git_http_server *server, |
553 | const char *header_name, | |
554 | git_credential *credentials) | |
555 | { | |
556 | git_http_auth_context *auth = server->auth_context; | |
557 | git_vector *challenges = &server->auth_challenges; | |
558 | const char *challenge; | |
e579e0f7 | 559 | git_str token = GIT_STR_INIT; |
22a2d3d5 UG |
560 | int error = 0; |
561 | ||
562 | /* We've started a new request without creds; free the context. */ | |
563 | if (auth && !credentials) { | |
564 | free_auth_context(server); | |
565 | return 0; | |
566 | } | |
567 | ||
568 | /* We haven't authenticated, nor were we asked to. Nothing to do. */ | |
569 | if (!auth && !git_vector_length(challenges)) | |
570 | return 0; | |
571 | ||
572 | if (!auth) { | |
573 | challenge = init_auth_context(server, challenges, credentials); | |
574 | auth = server->auth_context; | |
575 | ||
576 | if (!challenge || !auth) { | |
577 | error = -1; | |
578 | goto done; | |
579 | } | |
580 | } else if (auth->set_challenge) { | |
581 | challenge = challenge_for_context(challenges, auth); | |
582 | } | |
583 | ||
584 | if (auth->set_challenge && challenge && | |
585 | (error = auth->set_challenge(auth, challenge)) < 0) | |
586 | goto done; | |
587 | ||
588 | if ((error = auth->next_token(&token, auth, credentials)) < 0) | |
589 | goto done; | |
590 | ||
591 | if (auth->is_complete && auth->is_complete(auth)) { | |
592 | /* | |
593 | * If we're done with an auth mechanism with connection affinity, | |
594 | * we don't need to send any more headers and can dispose the context. | |
595 | */ | |
596 | if (auth->connection_affinity) | |
597 | free_auth_context(server); | |
598 | } else if (!token.size) { | |
599 | git_error_set(GIT_ERROR_HTTP, "failed to respond to authentication challenge"); | |
c25aa7cd | 600 | error = GIT_EAUTH; |
22a2d3d5 UG |
601 | goto done; |
602 | } | |
603 | ||
604 | if (token.size > 0) | |
e579e0f7 | 605 | error = git_str_printf(buf, "%s: %s\r\n", header_name, token.ptr); |
22a2d3d5 UG |
606 | |
607 | done: | |
e579e0f7 | 608 | git_str_dispose(&token); |
22a2d3d5 UG |
609 | return error; |
610 | } | |
611 | ||
612 | GIT_INLINE(int) apply_server_credentials( | |
e579e0f7 | 613 | git_str *buf, |
22a2d3d5 UG |
614 | git_http_client *client, |
615 | git_http_request *request) | |
616 | { | |
617 | return apply_credentials(buf, | |
618 | &client->server, | |
619 | "Authorization", | |
620 | request->credentials); | |
621 | } | |
622 | ||
623 | GIT_INLINE(int) apply_proxy_credentials( | |
e579e0f7 | 624 | git_str *buf, |
22a2d3d5 UG |
625 | git_http_client *client, |
626 | git_http_request *request) | |
627 | { | |
628 | return apply_credentials(buf, | |
629 | &client->proxy, | |
630 | "Proxy-Authorization", | |
631 | request->proxy_credentials); | |
632 | } | |
633 | ||
e579e0f7 | 634 | static int puts_host_and_port(git_str *buf, git_net_url *url, bool force_port) |
c25aa7cd PP |
635 | { |
636 | bool ipv6 = git_net_url_is_ipv6(url); | |
637 | ||
638 | if (ipv6) | |
e579e0f7 | 639 | git_str_putc(buf, '['); |
c25aa7cd | 640 | |
e579e0f7 | 641 | git_str_puts(buf, url->host); |
c25aa7cd PP |
642 | |
643 | if (ipv6) | |
e579e0f7 | 644 | git_str_putc(buf, ']'); |
c25aa7cd PP |
645 | |
646 | if (force_port || !git_net_url_is_default_port(url)) { | |
e579e0f7 MB |
647 | git_str_putc(buf, ':'); |
648 | git_str_puts(buf, url->port); | |
c25aa7cd PP |
649 | } |
650 | ||
e579e0f7 | 651 | return git_str_oom(buf) ? -1 : 0; |
c25aa7cd PP |
652 | } |
653 | ||
22a2d3d5 UG |
654 | static int generate_connect_request( |
655 | git_http_client *client, | |
656 | git_http_request *request) | |
657 | { | |
e579e0f7 | 658 | git_str *buf; |
22a2d3d5 UG |
659 | int error; |
660 | ||
e579e0f7 | 661 | git_str_clear(&client->request_msg); |
22a2d3d5 UG |
662 | buf = &client->request_msg; |
663 | ||
e579e0f7 | 664 | git_str_puts(buf, "CONNECT "); |
c25aa7cd | 665 | puts_host_and_port(buf, &client->server.url, true); |
e579e0f7 | 666 | git_str_puts(buf, " HTTP/1.1\r\n"); |
22a2d3d5 | 667 | |
e579e0f7 | 668 | git_str_puts(buf, "User-Agent: "); |
22a2d3d5 | 669 | git_http__user_agent(buf); |
e579e0f7 | 670 | git_str_puts(buf, "\r\n"); |
22a2d3d5 | 671 | |
e579e0f7 | 672 | git_str_puts(buf, "Host: "); |
c25aa7cd | 673 | puts_host_and_port(buf, &client->server.url, true); |
e579e0f7 | 674 | git_str_puts(buf, "\r\n"); |
22a2d3d5 UG |
675 | |
676 | if ((error = apply_proxy_credentials(buf, client, request) < 0)) | |
677 | return -1; | |
678 | ||
e579e0f7 | 679 | git_str_puts(buf, "\r\n"); |
22a2d3d5 | 680 | |
e579e0f7 | 681 | return git_str_oom(buf) ? -1 : 0; |
22a2d3d5 UG |
682 | } |
683 | ||
c25aa7cd PP |
684 | static bool use_connect_proxy(git_http_client *client) |
685 | { | |
686 | return client->proxy.url.host && !strcmp(client->server.url.scheme, "https"); | |
687 | } | |
688 | ||
22a2d3d5 UG |
689 | static int generate_request( |
690 | git_http_client *client, | |
691 | git_http_request *request) | |
692 | { | |
e579e0f7 | 693 | git_str *buf; |
22a2d3d5 UG |
694 | size_t i; |
695 | int error; | |
696 | ||
c25aa7cd PP |
697 | GIT_ASSERT_ARG(client); |
698 | GIT_ASSERT_ARG(request); | |
22a2d3d5 | 699 | |
e579e0f7 | 700 | git_str_clear(&client->request_msg); |
22a2d3d5 UG |
701 | buf = &client->request_msg; |
702 | ||
703 | /* GET|POST path HTTP/1.1 */ | |
e579e0f7 MB |
704 | git_str_puts(buf, name_for_method(request->method)); |
705 | git_str_putc(buf, ' '); | |
22a2d3d5 UG |
706 | |
707 | if (request->proxy && strcmp(request->url->scheme, "https")) | |
708 | git_net_url_fmt(buf, request->url); | |
709 | else | |
710 | git_net_url_fmt_path(buf, request->url); | |
711 | ||
e579e0f7 | 712 | git_str_puts(buf, " HTTP/1.1\r\n"); |
22a2d3d5 | 713 | |
e579e0f7 | 714 | git_str_puts(buf, "User-Agent: "); |
22a2d3d5 | 715 | git_http__user_agent(buf); |
e579e0f7 | 716 | git_str_puts(buf, "\r\n"); |
22a2d3d5 | 717 | |
e579e0f7 | 718 | git_str_puts(buf, "Host: "); |
c25aa7cd | 719 | puts_host_and_port(buf, request->url, false); |
e579e0f7 | 720 | git_str_puts(buf, "\r\n"); |
22a2d3d5 UG |
721 | |
722 | if (request->accept) | |
e579e0f7 | 723 | git_str_printf(buf, "Accept: %s\r\n", request->accept); |
22a2d3d5 | 724 | else |
e579e0f7 | 725 | git_str_puts(buf, "Accept: */*\r\n"); |
22a2d3d5 UG |
726 | |
727 | if (request->content_type) | |
e579e0f7 | 728 | git_str_printf(buf, "Content-Type: %s\r\n", |
22a2d3d5 UG |
729 | request->content_type); |
730 | ||
731 | if (request->chunked) | |
e579e0f7 | 732 | git_str_puts(buf, "Transfer-Encoding: chunked\r\n"); |
22a2d3d5 UG |
733 | |
734 | if (request->content_length > 0) | |
e579e0f7 | 735 | git_str_printf(buf, "Content-Length: %"PRIuZ "\r\n", |
22a2d3d5 UG |
736 | request->content_length); |
737 | ||
738 | if (request->expect_continue) | |
e579e0f7 | 739 | git_str_printf(buf, "Expect: 100-continue\r\n"); |
22a2d3d5 UG |
740 | |
741 | if ((error = apply_server_credentials(buf, client, request)) < 0 || | |
c25aa7cd PP |
742 | (!use_connect_proxy(client) && |
743 | (error = apply_proxy_credentials(buf, client, request)) < 0)) | |
22a2d3d5 UG |
744 | return error; |
745 | ||
746 | if (request->custom_headers) { | |
747 | for (i = 0; i < request->custom_headers->count; i++) { | |
748 | const char *hdr = request->custom_headers->strings[i]; | |
749 | ||
750 | if (hdr) | |
e579e0f7 | 751 | git_str_printf(buf, "%s\r\n", hdr); |
22a2d3d5 UG |
752 | } |
753 | } | |
754 | ||
e579e0f7 | 755 | git_str_puts(buf, "\r\n"); |
22a2d3d5 | 756 | |
e579e0f7 | 757 | if (git_str_oom(buf)) |
22a2d3d5 UG |
758 | return -1; |
759 | ||
760 | return 0; | |
761 | } | |
762 | ||
763 | static int check_certificate( | |
764 | git_stream *stream, | |
765 | git_net_url *url, | |
766 | int is_valid, | |
767 | git_transport_certificate_check_cb cert_cb, | |
768 | void *cert_cb_payload) | |
769 | { | |
770 | git_cert *cert; | |
771 | git_error_state last_error = {0}; | |
772 | int error; | |
773 | ||
774 | if ((error = git_stream_certificate(&cert, stream)) < 0) | |
775 | return error; | |
776 | ||
777 | git_error_state_capture(&last_error, GIT_ECERTIFICATE); | |
778 | ||
779 | error = cert_cb(cert, is_valid, url->host, cert_cb_payload); | |
780 | ||
781 | if (error == GIT_PASSTHROUGH && !is_valid) | |
782 | return git_error_state_restore(&last_error); | |
783 | else if (error == GIT_PASSTHROUGH) | |
784 | error = 0; | |
785 | else if (error && !git_error_last()) | |
786 | git_error_set(GIT_ERROR_HTTP, | |
787 | "user rejected certificate for %s", url->host); | |
788 | ||
789 | git_error_state_free(&last_error); | |
790 | return error; | |
791 | } | |
792 | ||
793 | static int server_connect_stream( | |
794 | git_http_server *server, | |
795 | git_transport_certificate_check_cb cert_cb, | |
796 | void *cb_payload) | |
797 | { | |
798 | int error; | |
799 | ||
800 | GIT_ERROR_CHECK_VERSION(server->stream, GIT_STREAM_VERSION, "git_stream"); | |
801 | ||
802 | error = git_stream_connect(server->stream); | |
803 | ||
804 | if (error && error != GIT_ECERTIFICATE) | |
805 | return error; | |
806 | ||
807 | if (git_stream_is_encrypted(server->stream) && cert_cb != NULL) | |
808 | error = check_certificate(server->stream, &server->url, !error, | |
809 | cert_cb, cb_payload); | |
810 | ||
811 | return error; | |
812 | } | |
813 | ||
814 | static void reset_auth_connection(git_http_server *server) | |
815 | { | |
816 | /* | |
817 | * If we've authenticated and we're doing "normal" | |
818 | * authentication with a request affinity (Basic, Digest) | |
819 | * then we want to _keep_ our context, since authentication | |
820 | * survives even through non-keep-alive connections. If | |
821 | * we've authenticated and we're doing connection-based | |
822 | * authentication (NTLM, Negotiate) - indicated by the presence | |
823 | * of an `is_complete` callback - then we need to restart | |
824 | * authentication on a new connection. | |
825 | */ | |
826 | ||
827 | if (server->auth_context && | |
828 | server->auth_context->connection_affinity) | |
829 | free_auth_context(server); | |
830 | } | |
831 | ||
832 | /* | |
833 | * Updates the server data structure with the new URL; returns 1 if the server | |
834 | * has changed and we need to reconnect, returns 0 otherwise. | |
835 | */ | |
836 | GIT_INLINE(int) server_setup_from_url( | |
837 | git_http_server *server, | |
838 | git_net_url *url) | |
839 | { | |
840 | if (!server->url.scheme || strcmp(server->url.scheme, url->scheme) || | |
841 | !server->url.host || strcmp(server->url.host, url->host) || | |
842 | !server->url.port || strcmp(server->url.port, url->port)) { | |
843 | git__free(server->url.scheme); | |
844 | git__free(server->url.host); | |
845 | git__free(server->url.port); | |
846 | ||
847 | server->url.scheme = git__strdup(url->scheme); | |
848 | GIT_ERROR_CHECK_ALLOC(server->url.scheme); | |
849 | ||
850 | server->url.host = git__strdup(url->host); | |
851 | GIT_ERROR_CHECK_ALLOC(server->url.host); | |
852 | ||
853 | server->url.port = git__strdup(url->port); | |
854 | GIT_ERROR_CHECK_ALLOC(server->url.port); | |
855 | ||
856 | return 1; | |
857 | } | |
858 | ||
859 | return 0; | |
860 | } | |
861 | ||
862 | static void reset_parser(git_http_client *client) | |
863 | { | |
864 | http_parser_init(&client->parser, HTTP_RESPONSE); | |
865 | } | |
866 | ||
867 | static int setup_hosts( | |
868 | git_http_client *client, | |
869 | git_http_request *request) | |
870 | { | |
871 | int ret, diff = 0; | |
872 | ||
c25aa7cd PP |
873 | GIT_ASSERT_ARG(client); |
874 | GIT_ASSERT_ARG(request); | |
875 | ||
876 | GIT_ASSERT(request->url); | |
22a2d3d5 UG |
877 | |
878 | if ((ret = server_setup_from_url(&client->server, request->url)) < 0) | |
879 | return ret; | |
880 | ||
881 | diff |= ret; | |
882 | ||
883 | if (request->proxy && | |
884 | (ret = server_setup_from_url(&client->proxy, request->proxy)) < 0) | |
885 | return ret; | |
886 | ||
887 | diff |= ret; | |
888 | ||
889 | if (diff) { | |
890 | free_auth_context(&client->server); | |
891 | free_auth_context(&client->proxy); | |
892 | ||
893 | client->connected = 0; | |
894 | } | |
895 | ||
896 | return 0; | |
897 | } | |
898 | ||
899 | GIT_INLINE(int) server_create_stream(git_http_server *server) | |
900 | { | |
901 | git_net_url *url = &server->url; | |
902 | ||
903 | if (strcasecmp(url->scheme, "https") == 0) | |
904 | return git_tls_stream_new(&server->stream, url->host, url->port); | |
905 | else if (strcasecmp(url->scheme, "http") == 0) | |
906 | return git_socket_stream_new(&server->stream, url->host, url->port); | |
907 | ||
908 | git_error_set(GIT_ERROR_HTTP, "unknown http scheme '%s'", url->scheme); | |
909 | return -1; | |
910 | } | |
911 | ||
912 | GIT_INLINE(void) save_early_response( | |
913 | git_http_client *client, | |
914 | git_http_response *response) | |
915 | { | |
916 | /* Buffer the response so we can return it in read_response */ | |
917 | client->state = HAS_EARLY_RESPONSE; | |
918 | ||
919 | memcpy(&client->early_response, response, sizeof(git_http_response)); | |
920 | memset(response, 0, sizeof(git_http_response)); | |
921 | } | |
922 | ||
923 | static int proxy_connect( | |
924 | git_http_client *client, | |
925 | git_http_request *request) | |
926 | { | |
927 | git_http_response response = {0}; | |
928 | int error; | |
929 | ||
930 | if (!client->proxy_connected || !client->keepalive) { | |
c25aa7cd | 931 | git_trace(GIT_TRACE_DEBUG, "Connecting to proxy %s port %s", |
22a2d3d5 UG |
932 | client->proxy.url.host, client->proxy.url.port); |
933 | ||
934 | if ((error = server_create_stream(&client->proxy)) < 0 || | |
935 | (error = server_connect_stream(&client->proxy, | |
936 | client->opts.proxy_certificate_check_cb, | |
937 | client->opts.proxy_certificate_check_payload)) < 0) | |
938 | goto done; | |
939 | ||
940 | client->proxy_connected = 1; | |
941 | } | |
942 | ||
943 | client->current_server = PROXY; | |
944 | client->state = SENDING_REQUEST; | |
945 | ||
946 | if ((error = generate_connect_request(client, request)) < 0 || | |
947 | (error = client_write_request(client)) < 0) | |
948 | goto done; | |
949 | ||
950 | client->state = SENT_REQUEST; | |
951 | ||
952 | if ((error = git_http_client_read_response(&response, client)) < 0 || | |
953 | (error = git_http_client_skip_body(client)) < 0) | |
954 | goto done; | |
955 | ||
c25aa7cd | 956 | GIT_ASSERT(client->state == DONE); |
22a2d3d5 UG |
957 | |
958 | if (response.status == GIT_HTTP_STATUS_PROXY_AUTHENTICATION_REQUIRED) { | |
959 | save_early_response(client, &response); | |
960 | ||
961 | error = GIT_RETRY; | |
962 | goto done; | |
963 | } else if (response.status != GIT_HTTP_STATUS_OK) { | |
964 | git_error_set(GIT_ERROR_HTTP, "proxy returned unexpected status: %d", response.status); | |
965 | error = -1; | |
966 | goto done; | |
967 | } | |
968 | ||
969 | reset_parser(client); | |
970 | client->state = NONE; | |
971 | ||
972 | done: | |
973 | git_http_response_dispose(&response); | |
974 | return error; | |
975 | } | |
976 | ||
977 | static int server_connect(git_http_client *client) | |
978 | { | |
979 | git_net_url *url = &client->server.url; | |
980 | git_transport_certificate_check_cb cert_cb; | |
981 | void *cert_payload; | |
982 | int error; | |
983 | ||
984 | client->current_server = SERVER; | |
985 | ||
986 | if (client->proxy.stream) | |
987 | error = git_tls_stream_wrap(&client->server.stream, client->proxy.stream, url->host); | |
988 | else | |
989 | error = server_create_stream(&client->server); | |
990 | ||
991 | if (error < 0) | |
992 | goto done; | |
993 | ||
994 | cert_cb = client->opts.server_certificate_check_cb; | |
995 | cert_payload = client->opts.server_certificate_check_payload; | |
996 | ||
997 | error = server_connect_stream(&client->server, cert_cb, cert_payload); | |
998 | ||
999 | done: | |
1000 | return error; | |
1001 | } | |
1002 | ||
1003 | GIT_INLINE(void) close_stream(git_http_server *server) | |
1004 | { | |
1005 | if (server->stream) { | |
1006 | git_stream_close(server->stream); | |
1007 | git_stream_free(server->stream); | |
1008 | server->stream = NULL; | |
1009 | } | |
1010 | } | |
1011 | ||
1012 | static int http_client_connect( | |
1013 | git_http_client *client, | |
1014 | git_http_request *request) | |
1015 | { | |
1016 | bool use_proxy = false; | |
1017 | int error; | |
1018 | ||
1019 | if ((error = setup_hosts(client, request)) < 0) | |
1020 | goto on_error; | |
1021 | ||
1022 | /* We're connected to our destination server; no need to reconnect */ | |
1023 | if (client->connected && client->keepalive && | |
1024 | (client->state == NONE || client->state == DONE)) | |
1025 | return 0; | |
1026 | ||
1027 | client->connected = 0; | |
1028 | client->request_count = 0; | |
1029 | ||
1030 | close_stream(&client->server); | |
1031 | reset_auth_connection(&client->server); | |
1032 | ||
1033 | reset_parser(client); | |
1034 | ||
1035 | /* Reconnect to the proxy if necessary. */ | |
c25aa7cd | 1036 | use_proxy = use_connect_proxy(client); |
22a2d3d5 UG |
1037 | |
1038 | if (use_proxy) { | |
1039 | if (!client->proxy_connected || !client->keepalive || | |
1040 | (client->state != NONE && client->state != DONE)) { | |
1041 | close_stream(&client->proxy); | |
1042 | reset_auth_connection(&client->proxy); | |
1043 | ||
1044 | client->proxy_connected = 0; | |
1045 | } | |
1046 | ||
1047 | if ((error = proxy_connect(client, request)) < 0) | |
1048 | goto on_error; | |
1049 | } | |
1050 | ||
c25aa7cd | 1051 | git_trace(GIT_TRACE_DEBUG, "Connecting to remote %s port %s", |
22a2d3d5 UG |
1052 | client->server.url.host, client->server.url.port); |
1053 | ||
1054 | if ((error = server_connect(client)) < 0) | |
1055 | goto on_error; | |
1056 | ||
1057 | client->connected = 1; | |
1058 | return error; | |
1059 | ||
1060 | on_error: | |
1061 | if (error != GIT_RETRY) | |
1062 | close_stream(&client->proxy); | |
1063 | ||
1064 | close_stream(&client->server); | |
1065 | return error; | |
1066 | } | |
1067 | ||
1068 | GIT_INLINE(int) client_read(git_http_client *client) | |
1069 | { | |
1070 | http_parser_context *parser_context = client->parser.data; | |
1071 | git_stream *stream; | |
1072 | char *buf = client->read_buf.ptr + client->read_buf.size; | |
1073 | size_t max_len; | |
1074 | ssize_t read_len; | |
1075 | ||
1076 | stream = client->current_server == PROXY ? | |
1077 | client->proxy.stream : client->server.stream; | |
1078 | ||
1079 | /* | |
e579e0f7 | 1080 | * We use a git_str for convenience, but statically allocate it and |
22a2d3d5 UG |
1081 | * don't resize. Limit our consumption to INT_MAX since calling |
1082 | * functions use an int return type to return number of bytes read. | |
1083 | */ | |
1084 | max_len = client->read_buf.asize - client->read_buf.size; | |
1085 | max_len = min(max_len, INT_MAX); | |
1086 | ||
1087 | if (parser_context->output_size) | |
1088 | max_len = min(max_len, parser_context->output_size); | |
1089 | ||
1090 | if (max_len == 0) { | |
1091 | git_error_set(GIT_ERROR_HTTP, "no room in output buffer"); | |
1092 | return -1; | |
1093 | } | |
1094 | ||
1095 | read_len = git_stream_read(stream, buf, max_len); | |
1096 | ||
1097 | if (read_len >= 0) { | |
1098 | client->read_buf.size += read_len; | |
1099 | ||
1100 | git_trace(GIT_TRACE_TRACE, "Received:\n%.*s", | |
1101 | (int)read_len, buf); | |
1102 | } | |
1103 | ||
1104 | return (int)read_len; | |
1105 | } | |
1106 | ||
1107 | static bool parser_settings_initialized; | |
1108 | static http_parser_settings parser_settings; | |
1109 | ||
1110 | GIT_INLINE(http_parser_settings *) http_client_parser_settings(void) | |
1111 | { | |
1112 | if (!parser_settings_initialized) { | |
1113 | parser_settings.on_header_field = on_header_field; | |
1114 | parser_settings.on_header_value = on_header_value; | |
1115 | parser_settings.on_headers_complete = on_headers_complete; | |
1116 | parser_settings.on_body = on_body; | |
1117 | parser_settings.on_message_complete = on_message_complete; | |
1118 | ||
1119 | parser_settings_initialized = true; | |
1120 | } | |
1121 | ||
1122 | return &parser_settings; | |
1123 | } | |
1124 | ||
1125 | GIT_INLINE(int) client_read_and_parse(git_http_client *client) | |
1126 | { | |
1127 | http_parser *parser = &client->parser; | |
1128 | http_parser_context *ctx = (http_parser_context *) parser->data; | |
1129 | unsigned char http_errno; | |
1130 | int read_len; | |
1131 | size_t parsed_len; | |
1132 | ||
1133 | /* | |
1134 | * If we have data in our read buffer, that means we stopped early | |
1135 | * when parsing headers. Use the data in the read buffer instead of | |
1136 | * reading more from the socket. | |
1137 | */ | |
1138 | if (!client->read_buf.size && (read_len = client_read(client)) < 0) | |
1139 | return read_len; | |
1140 | ||
1141 | parsed_len = http_parser_execute(parser, | |
1142 | http_client_parser_settings(), | |
1143 | client->read_buf.ptr, | |
1144 | client->read_buf.size); | |
1145 | http_errno = client->parser.http_errno; | |
1146 | ||
1147 | if (parsed_len > INT_MAX) { | |
1148 | git_error_set(GIT_ERROR_HTTP, "unexpectedly large parse"); | |
1149 | return -1; | |
1150 | } | |
1151 | ||
1152 | if (ctx->parse_status == PARSE_STATUS_ERROR) { | |
1153 | client->connected = 0; | |
1154 | return ctx->error ? ctx->error : -1; | |
1155 | } | |
1156 | ||
1157 | /* | |
1158 | * If we finished reading the headers or body, we paused parsing. | |
1159 | * Otherwise the parser will start filling the body, or even parse | |
1160 | * a new response if the server pipelined us multiple responses. | |
1161 | * (This can happen in response to an expect/continue request, | |
1162 | * where the server gives you a 100 and 200 simultaneously.) | |
1163 | */ | |
1164 | if (http_errno == HPE_PAUSED) { | |
1165 | /* | |
1166 | * http-parser has a "feature" where it will not deliver the | |
1167 | * final byte when paused in a callback. Consume that byte. | |
1168 | * https://github.com/nodejs/http-parser/issues/97 | |
1169 | */ | |
c25aa7cd | 1170 | GIT_ASSERT(client->read_buf.size > parsed_len); |
22a2d3d5 UG |
1171 | |
1172 | http_parser_pause(parser, 0); | |
1173 | ||
1174 | parsed_len += http_parser_execute(parser, | |
1175 | http_client_parser_settings(), | |
1176 | client->read_buf.ptr + parsed_len, | |
1177 | 1); | |
1178 | } | |
1179 | ||
1180 | /* Most failures will be reported in http_errno */ | |
1181 | else if (parser->http_errno != HPE_OK) { | |
1182 | git_error_set(GIT_ERROR_HTTP, "http parser error: %s", | |
1183 | http_errno_description(http_errno)); | |
1184 | return -1; | |
1185 | } | |
1186 | ||
1187 | /* Otherwise we should have consumed the entire buffer. */ | |
1188 | else if (parsed_len != client->read_buf.size) { | |
1189 | git_error_set(GIT_ERROR_HTTP, | |
1190 | "http parser did not consume entire buffer: %s", | |
1191 | http_errno_description(http_errno)); | |
1192 | return -1; | |
1193 | } | |
1194 | ||
1195 | /* recv returned 0, the server hung up on us */ | |
1196 | else if (!parsed_len) { | |
1197 | git_error_set(GIT_ERROR_HTTP, "unexpected EOF"); | |
1198 | return -1; | |
1199 | } | |
1200 | ||
e579e0f7 | 1201 | git_str_consume_bytes(&client->read_buf, parsed_len); |
22a2d3d5 UG |
1202 | |
1203 | return (int)parsed_len; | |
1204 | } | |
1205 | ||
1206 | /* | |
1207 | * See if we've consumed the entire response body. If the client was | |
1208 | * reading the body but did not consume it entirely, it's possible that | |
1209 | * they knew that the stream had finished (in a git response, seeing a | |
1210 | * final flush) and stopped reading. But if the response was chunked, | |
1211 | * we may have not consumed the final chunk marker. Consume it to | |
1212 | * ensure that we don't have it waiting in our socket. If there's | |
1213 | * more than just a chunk marker, close the connection. | |
1214 | */ | |
1215 | static void complete_response_body(git_http_client *client) | |
1216 | { | |
1217 | http_parser_context parser_context = {0}; | |
1218 | ||
1219 | /* If we're not keeping alive, don't bother. */ | |
1220 | if (!client->keepalive) { | |
1221 | client->connected = 0; | |
1222 | goto done; | |
1223 | } | |
1224 | ||
1225 | parser_context.client = client; | |
1226 | client->parser.data = &parser_context; | |
1227 | ||
1228 | /* If there was an error, just close the connection. */ | |
1229 | if (client_read_and_parse(client) < 0 || | |
1230 | parser_context.error != HPE_OK || | |
1231 | (parser_context.parse_status != PARSE_STATUS_OK && | |
1232 | parser_context.parse_status != PARSE_STATUS_NO_OUTPUT)) { | |
1233 | git_error_clear(); | |
1234 | client->connected = 0; | |
1235 | } | |
1236 | ||
1237 | done: | |
e579e0f7 | 1238 | git_str_clear(&client->read_buf); |
22a2d3d5 UG |
1239 | } |
1240 | ||
1241 | int git_http_client_send_request( | |
1242 | git_http_client *client, | |
1243 | git_http_request *request) | |
1244 | { | |
1245 | git_http_response response = {0}; | |
1246 | int error = -1; | |
1247 | ||
c25aa7cd PP |
1248 | GIT_ASSERT_ARG(client); |
1249 | GIT_ASSERT_ARG(request); | |
22a2d3d5 UG |
1250 | |
1251 | /* If the client did not finish reading, clean up the stream. */ | |
1252 | if (client->state == READING_BODY) | |
1253 | complete_response_body(client); | |
1254 | ||
1255 | /* If we're waiting for proxy auth, don't sending more requests. */ | |
1256 | if (client->state == HAS_EARLY_RESPONSE) | |
1257 | return 0; | |
1258 | ||
1259 | if (git_trace_level() >= GIT_TRACE_DEBUG) { | |
e579e0f7 | 1260 | git_str url = GIT_STR_INIT; |
22a2d3d5 UG |
1261 | git_net_url_fmt(&url, request->url); |
1262 | git_trace(GIT_TRACE_DEBUG, "Sending %s request to %s", | |
1263 | name_for_method(request->method), | |
1264 | url.ptr ? url.ptr : "<invalid>"); | |
e579e0f7 | 1265 | git_str_dispose(&url); |
22a2d3d5 UG |
1266 | } |
1267 | ||
1268 | if ((error = http_client_connect(client, request)) < 0 || | |
1269 | (error = generate_request(client, request)) < 0 || | |
1270 | (error = client_write_request(client)) < 0) | |
1271 | goto done; | |
1272 | ||
1273 | client->state = SENT_REQUEST; | |
1274 | ||
1275 | if (request->expect_continue) { | |
1276 | if ((error = git_http_client_read_response(&response, client)) < 0 || | |
1277 | (error = git_http_client_skip_body(client)) < 0) | |
1278 | goto done; | |
1279 | ||
1280 | error = 0; | |
1281 | ||
1282 | if (response.status != GIT_HTTP_STATUS_CONTINUE) { | |
1283 | save_early_response(client, &response); | |
1284 | goto done; | |
1285 | } | |
1286 | } | |
1287 | ||
1288 | if (request->content_length || request->chunked) { | |
1289 | client->state = SENDING_BODY; | |
1290 | client->request_body_len = request->content_length; | |
1291 | client->request_body_remain = request->content_length; | |
1292 | client->request_chunked = request->chunked; | |
1293 | } | |
1294 | ||
1295 | reset_parser(client); | |
1296 | ||
1297 | done: | |
1298 | if (error == GIT_RETRY) | |
1299 | error = 0; | |
1300 | ||
1301 | git_http_response_dispose(&response); | |
1302 | return error; | |
1303 | } | |
1304 | ||
1305 | bool git_http_client_has_response(git_http_client *client) | |
1306 | { | |
1307 | return (client->state == HAS_EARLY_RESPONSE || | |
1308 | client->state > SENT_REQUEST); | |
1309 | } | |
1310 | ||
1311 | int git_http_client_send_body( | |
1312 | git_http_client *client, | |
1313 | const char *buffer, | |
1314 | size_t buffer_len) | |
1315 | { | |
1316 | git_http_server *server; | |
e579e0f7 | 1317 | git_str hdr = GIT_STR_INIT; |
22a2d3d5 UG |
1318 | int error; |
1319 | ||
c25aa7cd | 1320 | GIT_ASSERT_ARG(client); |
22a2d3d5 UG |
1321 | |
1322 | /* If we're waiting for proxy auth, don't sending more requests. */ | |
1323 | if (client->state == HAS_EARLY_RESPONSE) | |
1324 | return 0; | |
1325 | ||
1326 | if (client->state != SENDING_BODY) { | |
1327 | git_error_set(GIT_ERROR_HTTP, "client is in invalid state"); | |
1328 | return -1; | |
1329 | } | |
1330 | ||
1331 | if (!buffer_len) | |
1332 | return 0; | |
1333 | ||
1334 | server = &client->server; | |
1335 | ||
1336 | if (client->request_body_len) { | |
c25aa7cd | 1337 | GIT_ASSERT(buffer_len <= client->request_body_remain); |
22a2d3d5 UG |
1338 | |
1339 | if ((error = stream_write(server, buffer, buffer_len)) < 0) | |
1340 | goto done; | |
1341 | ||
1342 | client->request_body_remain -= buffer_len; | |
1343 | } else { | |
e579e0f7 | 1344 | if ((error = git_str_printf(&hdr, "%" PRIxZ "\r\n", buffer_len)) < 0 || |
22a2d3d5 UG |
1345 | (error = stream_write(server, hdr.ptr, hdr.size)) < 0 || |
1346 | (error = stream_write(server, buffer, buffer_len)) < 0 || | |
1347 | (error = stream_write(server, "\r\n", 2)) < 0) | |
1348 | goto done; | |
1349 | } | |
1350 | ||
1351 | done: | |
e579e0f7 | 1352 | git_str_dispose(&hdr); |
22a2d3d5 UG |
1353 | return error; |
1354 | } | |
1355 | ||
1356 | static int complete_request(git_http_client *client) | |
1357 | { | |
1358 | int error = 0; | |
1359 | ||
c25aa7cd PP |
1360 | GIT_ASSERT_ARG(client); |
1361 | GIT_ASSERT(client->state == SENDING_BODY); | |
22a2d3d5 UG |
1362 | |
1363 | if (client->request_body_len && client->request_body_remain) { | |
1364 | git_error_set(GIT_ERROR_HTTP, "truncated write"); | |
1365 | error = -1; | |
1366 | } else if (client->request_chunked) { | |
1367 | error = stream_write(&client->server, "0\r\n\r\n", 5); | |
1368 | } | |
1369 | ||
1370 | client->state = SENT_REQUEST; | |
1371 | return error; | |
1372 | } | |
1373 | ||
1374 | int git_http_client_read_response( | |
1375 | git_http_response *response, | |
1376 | git_http_client *client) | |
1377 | { | |
1378 | http_parser_context parser_context = {0}; | |
1379 | int error; | |
1380 | ||
c25aa7cd PP |
1381 | GIT_ASSERT_ARG(response); |
1382 | GIT_ASSERT_ARG(client); | |
22a2d3d5 UG |
1383 | |
1384 | if (client->state == SENDING_BODY) { | |
1385 | if ((error = complete_request(client)) < 0) | |
1386 | goto done; | |
1387 | } | |
1388 | ||
1389 | if (client->state == HAS_EARLY_RESPONSE) { | |
1390 | memcpy(response, &client->early_response, sizeof(git_http_response)); | |
1391 | memset(&client->early_response, 0, sizeof(git_http_response)); | |
1392 | client->state = DONE; | |
1393 | return 0; | |
1394 | } | |
1395 | ||
1396 | if (client->state != SENT_REQUEST) { | |
1397 | git_error_set(GIT_ERROR_HTTP, "client is in invalid state"); | |
1398 | error = -1; | |
1399 | goto done; | |
1400 | } | |
1401 | ||
1402 | git_http_response_dispose(response); | |
1403 | ||
1404 | if (client->current_server == PROXY) { | |
1405 | git_vector_free_deep(&client->proxy.auth_challenges); | |
1406 | } else if(client->current_server == SERVER) { | |
1407 | git_vector_free_deep(&client->server.auth_challenges); | |
1408 | } | |
1409 | ||
1410 | client->state = READING_RESPONSE; | |
1411 | client->keepalive = 0; | |
1412 | client->parser.data = &parser_context; | |
1413 | ||
1414 | parser_context.client = client; | |
1415 | parser_context.response = response; | |
1416 | ||
1417 | while (client->state == READING_RESPONSE) { | |
1418 | if ((error = client_read_and_parse(client)) < 0) | |
1419 | goto done; | |
1420 | } | |
1421 | ||
c25aa7cd | 1422 | GIT_ASSERT(client->state == READING_BODY || client->state == DONE); |
22a2d3d5 UG |
1423 | |
1424 | done: | |
e579e0f7 MB |
1425 | git_str_dispose(&parser_context.parse_header_name); |
1426 | git_str_dispose(&parser_context.parse_header_value); | |
22a2d3d5 UG |
1427 | |
1428 | return error; | |
1429 | } | |
1430 | ||
1431 | int git_http_client_read_body( | |
1432 | git_http_client *client, | |
1433 | char *buffer, | |
1434 | size_t buffer_size) | |
1435 | { | |
1436 | http_parser_context parser_context = {0}; | |
1437 | int error = 0; | |
1438 | ||
1439 | if (client->state == DONE) | |
1440 | return 0; | |
1441 | ||
1442 | if (client->state != READING_BODY) { | |
1443 | git_error_set(GIT_ERROR_HTTP, "client is in invalid state"); | |
1444 | return -1; | |
1445 | } | |
1446 | ||
1447 | /* | |
1448 | * Now we'll read from the socket and http_parser will pipeline the | |
1449 | * data directly to the client. | |
1450 | */ | |
1451 | ||
1452 | parser_context.client = client; | |
1453 | parser_context.output_buf = buffer; | |
1454 | parser_context.output_size = buffer_size; | |
1455 | ||
1456 | client->parser.data = &parser_context; | |
1457 | ||
1458 | /* | |
1459 | * Clients expect to get a non-zero amount of data from us, | |
1460 | * so we either block until we have data to return, until we | |
1461 | * hit EOF or there's an error. Do this in a loop, since we | |
1462 | * may end up reading only some stream metadata (like chunk | |
1463 | * information). | |
1464 | */ | |
1465 | while (!parser_context.output_written) { | |
1466 | error = client_read_and_parse(client); | |
1467 | ||
1468 | if (error <= 0) | |
1469 | goto done; | |
1470 | ||
1471 | if (client->state == DONE) | |
1472 | break; | |
1473 | } | |
1474 | ||
c25aa7cd | 1475 | GIT_ASSERT(parser_context.output_written <= INT_MAX); |
22a2d3d5 UG |
1476 | error = (int)parser_context.output_written; |
1477 | ||
1478 | done: | |
1479 | if (error < 0) | |
1480 | client->connected = 0; | |
1481 | ||
1482 | return error; | |
1483 | } | |
1484 | ||
1485 | int git_http_client_skip_body(git_http_client *client) | |
1486 | { | |
1487 | http_parser_context parser_context = {0}; | |
1488 | int error; | |
1489 | ||
1490 | if (client->state == DONE) | |
1491 | return 0; | |
1492 | ||
1493 | if (client->state != READING_BODY) { | |
1494 | git_error_set(GIT_ERROR_HTTP, "client is in invalid state"); | |
1495 | return -1; | |
1496 | } | |
1497 | ||
1498 | parser_context.client = client; | |
1499 | client->parser.data = &parser_context; | |
1500 | ||
1501 | do { | |
1502 | error = client_read_and_parse(client); | |
1503 | ||
1504 | if (parser_context.error != HPE_OK || | |
1505 | (parser_context.parse_status != PARSE_STATUS_OK && | |
1506 | parser_context.parse_status != PARSE_STATUS_NO_OUTPUT)) { | |
1507 | git_error_set(GIT_ERROR_HTTP, | |
1508 | "unexpected data handled in callback"); | |
1509 | error = -1; | |
1510 | } | |
c25aa7cd | 1511 | } while (error >= 0 && client->state != DONE); |
22a2d3d5 UG |
1512 | |
1513 | if (error < 0) | |
1514 | client->connected = 0; | |
1515 | ||
1516 | return error; | |
1517 | } | |
1518 | ||
1519 | /* | |
1520 | * Create an http_client capable of communicating with the given remote | |
1521 | * host. | |
1522 | */ | |
1523 | int git_http_client_new( | |
1524 | git_http_client **out, | |
1525 | git_http_client_options *opts) | |
1526 | { | |
1527 | git_http_client *client; | |
1528 | ||
c25aa7cd | 1529 | GIT_ASSERT_ARG(out); |
22a2d3d5 UG |
1530 | |
1531 | client = git__calloc(1, sizeof(git_http_client)); | |
1532 | GIT_ERROR_CHECK_ALLOC(client); | |
1533 | ||
e579e0f7 | 1534 | git_str_init(&client->read_buf, GIT_READ_BUFFER_SIZE); |
22a2d3d5 UG |
1535 | GIT_ERROR_CHECK_ALLOC(client->read_buf.ptr); |
1536 | ||
1537 | if (opts) | |
1538 | memcpy(&client->opts, opts, sizeof(git_http_client_options)); | |
1539 | ||
1540 | *out = client; | |
1541 | return 0; | |
1542 | } | |
1543 | ||
1544 | GIT_INLINE(void) http_server_close(git_http_server *server) | |
1545 | { | |
1546 | if (server->stream) { | |
1547 | git_stream_close(server->stream); | |
1548 | git_stream_free(server->stream); | |
1549 | server->stream = NULL; | |
1550 | } | |
1551 | ||
1552 | git_net_url_dispose(&server->url); | |
1553 | ||
1554 | git_vector_free_deep(&server->auth_challenges); | |
1555 | free_auth_context(server); | |
1556 | } | |
1557 | ||
1558 | static void http_client_close(git_http_client *client) | |
1559 | { | |
1560 | http_server_close(&client->server); | |
1561 | http_server_close(&client->proxy); | |
1562 | ||
e579e0f7 | 1563 | git_str_dispose(&client->request_msg); |
22a2d3d5 UG |
1564 | |
1565 | client->state = 0; | |
1566 | client->request_count = 0; | |
1567 | client->connected = 0; | |
1568 | client->keepalive = 0; | |
1569 | } | |
1570 | ||
1571 | void git_http_client_free(git_http_client *client) | |
1572 | { | |
1573 | if (!client) | |
1574 | return; | |
1575 | ||
1576 | http_client_close(client); | |
e579e0f7 | 1577 | git_str_dispose(&client->read_buf); |
22a2d3d5 UG |
1578 | git__free(client); |
1579 | } |