blob: 66731b0ce13c110b6cbf0e56065a61889b88a13a [file] [log] [blame]
/*
* Copyright (C) the libgit2 contributors. All rights reserved.
*
* This file is part of libgit2, distributed under the GNU GPL v2 with
* a Linking Exception. For full terms see the included COPYING file.
*/
#include "common.h"
#ifndef GIT_WINHTTP
#include "git2.h"
#include "http_parser.h"
#include "buffer.h"
#include "net.h"
#include "netops.h"
#include "global.h"
#include "remote.h"
#include "git2/sys/credential.h"
#include "smart.h"
#include "auth.h"
#include "http.h"
#include "auth_negotiate.h"
#include "auth_ntlm.h"
#include "trace.h"
#include "streams/tls.h"
#include "streams/socket.h"
#include "httpclient.h"
bool git_http__expect_continue = false;
typedef enum {
HTTP_STATE_NONE = 0,
HTTP_STATE_SENDING_REQUEST,
HTTP_STATE_RECEIVING_RESPONSE,
HTTP_STATE_DONE
} http_state;
typedef struct {
git_http_method method;
const char *url;
const char *request_type;
const char *response_type;
unsigned chunked : 1;
} http_service;
typedef struct {
git_smart_subtransport_stream parent;
const http_service *service;
http_state state;
unsigned replay_count;
} http_stream;
typedef struct {
git_net_url url;
git_credential *cred;
unsigned auth_schemetypes;
unsigned url_cred_presented : 1;
} http_server;
typedef struct {
git_smart_subtransport parent;
transport_smart *owner;
http_server server;
http_server proxy;
git_http_client *http_client;
} http_subtransport;
static const http_service upload_pack_ls_service = {
GIT_HTTP_METHOD_GET, "/info/refs?service=git-upload-pack",
NULL,
"application/x-git-upload-pack-advertisement",
0
};
static const http_service upload_pack_service = {
GIT_HTTP_METHOD_POST, "/git-upload-pack",
"application/x-git-upload-pack-request",
"application/x-git-upload-pack-result",
0
};
static const http_service receive_pack_ls_service = {
GIT_HTTP_METHOD_GET, "/info/refs?service=git-receive-pack",
NULL,
"application/x-git-receive-pack-advertisement",
0
};
static const http_service receive_pack_service = {
GIT_HTTP_METHOD_POST, "/git-receive-pack",
"application/x-git-receive-pack-request",
"application/x-git-receive-pack-result",
1
};
#define SERVER_TYPE_REMOTE "remote"
#define SERVER_TYPE_PROXY "proxy"
#define OWNING_SUBTRANSPORT(s) ((http_subtransport *)(s)->parent.subtransport)
static int apply_url_credentials(
git_credential **cred,
unsigned int allowed_types,
const char *username,
const char *password)
{
if (allowed_types & GIT_CREDENTIAL_USERPASS_PLAINTEXT)
return git_credential_userpass_plaintext_new(cred, username, password);
if ((allowed_types & GIT_CREDENTIAL_DEFAULT) && *username == '\0' && *password == '\0')
return git_credential_default_new(cred);
return GIT_PASSTHROUGH;
}
GIT_INLINE(void) free_cred(git_credential **cred)
{
if (*cred) {
git_credential_free(*cred);
(*cred) = NULL;
}
}
static int handle_auth(
http_server *server,
const char *server_type,
const char *url,
unsigned int allowed_schemetypes,
unsigned int allowed_credtypes,
git_credential_acquire_cb callback,
void *callback_payload)
{
int error = 1;
if (server->cred)
free_cred(&server->cred);
/* Start with URL-specified credentials, if there were any. */
if ((allowed_credtypes & GIT_CREDENTIAL_USERPASS_PLAINTEXT) &&
!server->url_cred_presented &&
server->url.username &&
server->url.password) {
error = apply_url_credentials(&server->cred, allowed_credtypes, server->url.username, server->url.password);
server->url_cred_presented = 1;
/* treat GIT_PASSTHROUGH as if callback isn't set */
if (error == GIT_PASSTHROUGH)
error = 1;
}
if (error > 0 && callback) {
error = callback(&server->cred, url, server->url.username, allowed_credtypes, callback_payload);
/* treat GIT_PASSTHROUGH as if callback isn't set */
if (error == GIT_PASSTHROUGH)
error = 1;
}
if (error > 0) {
git_error_set(GIT_ERROR_HTTP, "%s authentication required but no callback set", server_type);
error = -1;
}
if (!error)
server->auth_schemetypes = allowed_schemetypes;
return error;
}
GIT_INLINE(int) handle_remote_auth(
http_stream *stream,
git_http_response *response)
{
http_subtransport *transport = OWNING_SUBTRANSPORT(stream);
if (response->server_auth_credtypes == 0) {
git_error_set(GIT_ERROR_HTTP, "server requires authentication that we do not support");
return -1;
}
/* Otherwise, prompt for credentials. */
return handle_auth(
&transport->server,
SERVER_TYPE_REMOTE,
transport->owner->url,
response->server_auth_schemetypes,
response->server_auth_credtypes,
transport->owner->cred_acquire_cb,
transport->owner->cred_acquire_payload);
}
GIT_INLINE(int) handle_proxy_auth(
http_stream *stream,
git_http_response *response)
{
http_subtransport *transport = OWNING_SUBTRANSPORT(stream);
if (response->proxy_auth_credtypes == 0) {
git_error_set(GIT_ERROR_HTTP, "proxy requires authentication that we do not support");
return -1;
}
/* Otherwise, prompt for credentials. */
return handle_auth(
&transport->proxy,
SERVER_TYPE_PROXY,
transport->owner->proxy.url,
response->server_auth_schemetypes,
response->proxy_auth_credtypes,
transport->owner->proxy.credentials,
transport->owner->proxy.payload);
}
static int handle_response(
bool *complete,
http_stream *stream,
git_http_response *response,
bool allow_replay)
{
http_subtransport *transport = OWNING_SUBTRANSPORT(stream);
int error;
*complete = false;
if (allow_replay && git_http_response_is_redirect(response)) {
if (!response->location) {
git_error_set(GIT_ERROR_HTTP, "redirect without location");
return -1;
}
if (git_net_url_apply_redirect(&transport->server.url, response->location, stream->service->url) < 0) {
return -1;
}
return 0;
} else if (git_http_response_is_redirect(response)) {
git_error_set(GIT_ERROR_HTTP, "unexpected redirect");
return -1;
}
/* If we're in the middle of challenge/response auth, continue. */
if (allow_replay && response->resend_credentials) {
return 0;
} else if (allow_replay && response->status == GIT_HTTP_STATUS_UNAUTHORIZED) {
if ((error = handle_remote_auth(stream, response)) < 0)
return error;
return git_http_client_skip_body(transport->http_client);
} else if (allow_replay && response->status == GIT_HTTP_STATUS_PROXY_AUTHENTICATION_REQUIRED) {
if ((error = handle_proxy_auth(stream, response)) < 0)
return error;
return git_http_client_skip_body(transport->http_client);
} else if (response->status == GIT_HTTP_STATUS_UNAUTHORIZED ||
response->status == GIT_HTTP_STATUS_PROXY_AUTHENTICATION_REQUIRED) {
git_error_set(GIT_ERROR_HTTP, "unexpected authentication failure");
return -1;
}
if (response->status != GIT_HTTP_STATUS_OK) {
git_error_set(GIT_ERROR_HTTP, "unexpected http status code: %d", response->status);
return -1;
}
/* The response must contain a Content-Type header. */
if (!response->content_type) {
git_error_set(GIT_ERROR_HTTP, "no content-type header in response");
return -1;
}
/* The Content-Type header must match our expectation. */
if (strcmp(response->content_type, stream->service->response_type) != 0) {
git_error_set(GIT_ERROR_HTTP, "invalid content-type: '%s'", response->content_type);
return -1;
}
*complete = true;
stream->state = HTTP_STATE_RECEIVING_RESPONSE;
return 0;
}
static int lookup_proxy(
bool *out_use,
http_subtransport *transport)
{
const char *proxy;
git_remote *remote;
bool use_ssl;
char *config = NULL;
int error = 0;
*out_use = false;
git_net_url_dispose(&transport->proxy.url);
switch (transport->owner->proxy.type) {
case GIT_PROXY_SPECIFIED:
proxy = transport->owner->proxy.url;
break;
case GIT_PROXY_AUTO:
remote = transport->owner->owner;
use_ssl = !strcmp(transport->server.url.scheme, "https");
error = git_remote__get_http_proxy(remote, use_ssl, &config);
if (error || !config)
goto done;
proxy = config;
break;
default:
return 0;
}
if (!proxy ||
(error = git_net_url_parse(&transport->proxy.url, proxy)) < 0)
goto done;
*out_use = true;
done:
git__free(config);
return error;
}
static int generate_request(
git_net_url *url,
git_http_request *request,
http_stream *stream,
size_t len)
{
http_subtransport *transport = OWNING_SUBTRANSPORT(stream);
bool use_proxy = false;
int error;
if ((error = git_net_url_joinpath(url,
&transport->server.url, stream->service->url)) < 0 ||
(error = lookup_proxy(&use_proxy, transport)) < 0)
return error;
request->method = stream->service->method;
request->url = url;
request->credentials = transport->server.cred;
request->proxy = use_proxy ? &transport->proxy.url : NULL;
request->proxy_credentials = transport->proxy.cred;
request->custom_headers = &transport->owner->custom_headers;
if (stream->service->method == GIT_HTTP_METHOD_POST) {
request->chunked = stream->service->chunked;
request->content_length = stream->service->chunked ? 0 : len;
request->content_type = stream->service->request_type;
request->accept = stream->service->response_type;
request->expect_continue = git_http__expect_continue;
}
return 0;
}
/*
* Read from an HTTP transport - for the first invocation of this function
* (ie, when stream->state == HTTP_STATE_NONE), we'll send a GET request
* to the remote host. We will stream that data back on all subsequent
* calls.
*/
static int http_stream_read(
git_smart_subtransport_stream *s,
char *buffer,
size_t buffer_size,
size_t *out_len)
{
http_stream *stream = (http_stream *)s;
http_subtransport *transport = OWNING_SUBTRANSPORT(stream);
git_net_url url = GIT_NET_URL_INIT;
git_net_url proxy_url = GIT_NET_URL_INIT;
git_http_request request = {0};
git_http_response response = {0};
bool complete;
int error;
*out_len = 0;
if (stream->state == HTTP_STATE_NONE) {
stream->state = HTTP_STATE_SENDING_REQUEST;
stream->replay_count = 0;
}
/*
* Formulate the URL, send the request and read the response
* headers. Some of the request body may also be read.
*/
while (stream->state == HTTP_STATE_SENDING_REQUEST &&
stream->replay_count < GIT_HTTP_REPLAY_MAX) {
git_net_url_dispose(&url);
git_net_url_dispose(&proxy_url);
git_http_response_dispose(&response);
if ((error = generate_request(&url, &request, stream, 0)) < 0 ||
(error = git_http_client_send_request(
transport->http_client, &request)) < 0 ||
(error = git_http_client_read_response(
&response, transport->http_client)) < 0 ||
(error = handle_response(&complete, stream, &response, true)) < 0)
goto done;
if (complete)
break;
stream->replay_count++;
}
if (stream->state == HTTP_STATE_SENDING_REQUEST) {
git_error_set(GIT_ERROR_HTTP, "too many redirects or authentication replays");
error = -1;
goto done;
}
assert (stream->state == HTTP_STATE_RECEIVING_RESPONSE);
error = git_http_client_read_body(transport->http_client, buffer, buffer_size);
if (error > 0) {
*out_len = error;
error = 0;
}
done:
git_net_url_dispose(&url);
git_net_url_dispose(&proxy_url);
git_http_response_dispose(&response);
return error;
}
static bool needs_probe(http_stream *stream)
{
http_subtransport *transport = OWNING_SUBTRANSPORT(stream);
return (transport->server.auth_schemetypes == GIT_HTTP_AUTH_NTLM ||
transport->server.auth_schemetypes == GIT_HTTP_AUTH_NEGOTIATE);
}
static int send_probe(http_stream *stream)
{
http_subtransport *transport = OWNING_SUBTRANSPORT(stream);
git_http_client *client = transport->http_client;
const char *probe = "0000";
size_t len = 4;
git_net_url url = GIT_NET_URL_INIT;
git_http_request request = {0};
git_http_response response = {0};
bool complete = false;
size_t step, steps = 1;
int error;
/* NTLM requires a full challenge/response */
if (transport->server.auth_schemetypes == GIT_HTTP_AUTH_NTLM)
steps = GIT_AUTH_STEPS_NTLM;
/*
* Send at most two requests: one without any authentication to see
* if we get prompted to authenticate. If we do, send a second one
* with the first authentication message. The final authentication
* message with the response will occur with the *actual* POST data.
*/
for (step = 0; step < steps && !complete; step++) {
git_net_url_dispose(&url);
git_http_response_dispose(&response);
if ((error = generate_request(&url, &request, stream, len)) < 0 ||
(error = git_http_client_send_request(client, &request)) < 0 ||
(error = git_http_client_send_body(client, probe, len)) < 0 ||
(error = git_http_client_read_response(&response, client)) < 0 ||
(error = git_http_client_skip_body(client)) < 0 ||
(error = handle_response(&complete, stream, &response, true)) < 0)
goto done;
}
done:
git_http_response_dispose(&response);
git_net_url_dispose(&url);
return error;
}
/*
* Write to an HTTP transport - for the first invocation of this function
* (ie, when stream->state == HTTP_STATE_NONE), we'll send a POST request
* to the remote host. If we're sending chunked data, then subsequent calls
* will write the additional data given in the buffer. If we're not chunking,
* then the caller should have given us all the data in the original call.
* The caller should call http_stream_read_response to get the result.
*/
static int http_stream_write(
git_smart_subtransport_stream *s,
const char *buffer,
size_t len)
{
http_stream *stream = GIT_CONTAINER_OF(s, http_stream, parent);
http_subtransport *transport = OWNING_SUBTRANSPORT(stream);
git_net_url url = GIT_NET_URL_INIT;
git_http_request request = {0};
git_http_response response = {0};
int error;
while (stream->state == HTTP_STATE_NONE &&
stream->replay_count < GIT_HTTP_REPLAY_MAX) {
git_net_url_dispose(&url);
git_http_response_dispose(&response);
/*
* If we're authenticating with a connection-based mechanism
* (NTLM, Kerberos), send a "probe" packet. Servers SHOULD
* authenticate an entire keep-alive connection, so ideally
* we should not need to authenticate but some servers do
* not support this. By sending a probe packet, we'll be
* able to follow up with a second POST using the actual
* data (and, in the degenerate case, the authentication
* header as well).
*/
if (needs_probe(stream) && (error = send_probe(stream)) < 0)
goto done;
/* Send the regular POST request. */
if ((error = generate_request(&url, &request, stream, len)) < 0 ||
(error = git_http_client_send_request(
transport->http_client, &request)) < 0)
goto done;
if (request.expect_continue &&
git_http_client_has_response(transport->http_client)) {
bool complete;
/*
* If we got a response to an expect/continue, then
* it's something other than a 100 and we should
* deal with the response somehow.
*/
if ((error = git_http_client_read_response(&response, transport->http_client)) < 0 ||
(error = handle_response(&complete, stream, &response, true)) < 0)
goto done;
} else {
stream->state = HTTP_STATE_SENDING_REQUEST;
}
stream->replay_count++;
}
if (stream->state == HTTP_STATE_NONE) {
git_error_set(GIT_ERROR_HTTP,
"too many redirects or authentication replays");
error = -1;
goto done;
}
assert(stream->state == HTTP_STATE_SENDING_REQUEST);
error = git_http_client_send_body(transport->http_client, buffer, len);
done:
git_http_response_dispose(&response);
git_net_url_dispose(&url);
return error;
}
/*
* Read from an HTTP transport after it has been written to. This is the
* response from a POST request made by http_stream_write.
*/
static int http_stream_read_response(
git_smart_subtransport_stream *s,
char *buffer,
size_t buffer_size,
size_t *out_len)
{
http_stream *stream = (http_stream *)s;
http_subtransport *transport = OWNING_SUBTRANSPORT(stream);
git_http_client *client = transport->http_client;
git_http_response response = {0};
bool complete;
int error;
*out_len = 0;
if (stream->state == HTTP_STATE_SENDING_REQUEST) {
if ((error = git_http_client_read_response(&response, client)) < 0 ||
(error = handle_response(&complete, stream, &response, false)) < 0)
goto done;
assert(complete);
stream->state = HTTP_STATE_RECEIVING_RESPONSE;
}
error = git_http_client_read_body(client, buffer, buffer_size);
if (error > 0) {
*out_len = error;
error = 0;
}
done:
git_http_response_dispose(&response);
return error;
}
static void http_stream_free(git_smart_subtransport_stream *stream)
{
http_stream *s = GIT_CONTAINER_OF(stream, http_stream, parent);
git__free(s);
}
static const http_service *select_service(git_smart_service_t action)
{
switch (action) {
case GIT_SERVICE_UPLOADPACK_LS:
return &upload_pack_ls_service;
case GIT_SERVICE_UPLOADPACK:
return &upload_pack_service;
case GIT_SERVICE_RECEIVEPACK_LS:
return &receive_pack_ls_service;
case GIT_SERVICE_RECEIVEPACK:
return &receive_pack_service;
}
return NULL;
}
static int http_action(
git_smart_subtransport_stream **out,
git_smart_subtransport *t,
const char *url,
git_smart_service_t action)
{
http_subtransport *transport = GIT_CONTAINER_OF(t, http_subtransport, parent);
http_stream *stream;
const http_service *service;
int error;
assert(out && t);
*out = NULL;
/*
* If we've seen a redirect then preserve the location that we've
* been given. This is important to continue authorization against
* the redirect target, not the user-given source; the endpoint may
* have redirected us from HTTP->HTTPS and is using an auth mechanism
* that would be insecure in plaintext (eg, HTTP Basic).
*/
if (!git_net_url_valid(&transport->server.url) &&
(error = git_net_url_parse(&transport->server.url, url)) < 0)
return error;
if ((service = select_service(action)) == NULL) {
git_error_set(GIT_ERROR_HTTP, "invalid action");
return -1;
}
stream = git__calloc(sizeof(http_stream), 1);
GIT_ERROR_CHECK_ALLOC(stream);
if (!transport->http_client) {
git_http_client_options opts = {0};
opts.server_certificate_check_cb = transport->owner->certificate_check_cb;
opts.server_certificate_check_payload = transport->owner->message_cb_payload;
opts.proxy_certificate_check_cb = transport->owner->proxy.certificate_check;
opts.proxy_certificate_check_payload = transport->owner->proxy.payload;
if (git_http_client_new(&transport->http_client, &opts) < 0)
return -1;
}
stream->service = service;
stream->parent.subtransport = &transport->parent;
if (service->method == GIT_HTTP_METHOD_GET) {
stream->parent.read = http_stream_read;
} else {
stream->parent.write = http_stream_write;
stream->parent.read = http_stream_read_response;
}
stream->parent.free = http_stream_free;
*out = (git_smart_subtransport_stream *)stream;
return 0;
}
static int http_close(git_smart_subtransport *t)
{
http_subtransport *transport = GIT_CONTAINER_OF(t, http_subtransport, parent);
free_cred(&transport->server.cred);
free_cred(&transport->proxy.cred);
transport->server.url_cred_presented = false;
transport->proxy.url_cred_presented = false;
git_net_url_dispose(&transport->server.url);
git_net_url_dispose(&transport->proxy.url);
return 0;
}
static void http_free(git_smart_subtransport *t)
{
http_subtransport *transport = GIT_CONTAINER_OF(t, http_subtransport, parent);
git_http_client_free(transport->http_client);
http_close(t);
git__free(transport);
}
int git_smart_subtransport_http(git_smart_subtransport **out, git_transport *owner, void *param)
{
http_subtransport *transport;
GIT_UNUSED(param);
assert(out);
transport = git__calloc(sizeof(http_subtransport), 1);
GIT_ERROR_CHECK_ALLOC(transport);
transport->owner = (transport_smart *)owner;
transport->parent.action = http_action;
transport->parent.close = http_close;
transport->parent.free = http_free;
*out = (git_smart_subtransport *) transport;
return 0;
}
#endif /* !GIT_WINHTTP */