/* * Copyright (C) 2009-2011 the libgit2 contributors * * 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 "git2/net.h" #include "git2/common.h" #include "git2/types.h" #include "git2/errors.h" #include "git2/net.h" #include "git2/revwalk.h" #include "vector.h" #include "transport.h" #include "pkt.h" #include "common.h" #include "netops.h" #include "filebuf.h" #include "repository.h" typedef struct { git_transport parent; int socket; git_vector refs; git_remote_head **heads; git_transport_caps caps; } transport_git; /* * Create a git procol request. * * For example: 0035git-upload-pack /libgit2/libgit2\0host=github.com\0 */ static int gen_proto(char **out, int *outlen, const char *cmd, const char *url) { char *delim, *repo, *ptr; char default_command[] = "git-upload-pack"; char host[] = "host="; int len; delim = strchr(url, '/'); if (delim == NULL) return git__throw(GIT_EOBJCORRUPTED, "Failed to create proto-request: malformed URL"); repo = delim; delim = strchr(url, ':'); if (delim == NULL) delim = strchr(url, '/'); if (cmd == NULL) cmd = default_command; len = 4 + strlen(cmd) + 1 + strlen(repo) + 1 + strlen(host) + (delim - url) + 2; *out = git__malloc(len); if (*out == NULL) return GIT_ENOMEM; *outlen = len - 1; ptr = *out; memset(ptr, 0x0, len); /* We expect the return value to be > len - 1 so don't bother checking it */ snprintf(ptr, len -1, "%04x%s %s%c%s%s", len - 1, cmd, repo, 0, host, url); return GIT_SUCCESS; } static int send_request(int s, const char *cmd, const char *url) { int error, len; char *msg = NULL; error = gen_proto(&msg, &len, cmd, url); if (error < GIT_SUCCESS) goto cleanup; error = gitno_send(s, msg, len, 0); cleanup: free(msg); return error; } /* * Parse the URL and connect to a server, storing the socket in * out. For convenience this also takes care of asking for the remote * refs */ static int do_connect(transport_git *t, const char *url) { int s = -1; char *host, *port; const char prefix[] = "git://"; int error, connected = 0; if (!git__prefixcmp(url, prefix)) url += strlen(prefix); error = gitno_extract_host_and_port(&host, &port, url, GIT_DEFAULT_PORT); if (error < GIT_SUCCESS) return error; s = gitno_connect(host, port); connected = 1; error = send_request(s, NULL, url); t->socket = s; free(host); free(port); if (error < GIT_SUCCESS && s > 0) close(s); if (!connected) error = git__throw(GIT_EOSERR, "Failed to connect to any of the addresses"); return error; } /* * Read from the socket and store the references in the vector */ static int store_refs(transport_git *t) { gitno_buffer buf; int s = t->socket; git_vector *refs = &t->refs; int error = GIT_SUCCESS; char buffer[1024]; const char *line_end, *ptr; git_pkt *pkt; gitno_buffer_setup(&buf, buffer, sizeof(buffer), s); while (1) { error = gitno_recv(&buf); if (error < GIT_SUCCESS) return git__rethrow(GIT_EOSERR, "Failed to receive data"); if (error == GIT_SUCCESS) /* Orderly shutdown, so exit */ return GIT_SUCCESS; ptr = buf.data; while (1) { if (buf.offset == 0) break; error = git_pkt_parse_line(&pkt, ptr, &line_end, buf.offset); /* * If the error is GIT_ESHORTBUFFER, it means the buffer * isn't long enough to satisfy the request. Break out and * wait for more input. * On any other error, fail. */ if (error == GIT_ESHORTBUFFER) { break; } if (error < GIT_SUCCESS) { return error; } /* Get rid of the part we've used already */ gitno_consume(&buf, line_end); error = git_vector_insert(refs, pkt); if (error < GIT_SUCCESS) return error; if (pkt->type == GIT_PKT_FLUSH) return GIT_SUCCESS; } } return error; } static int detect_caps(transport_git *t) { git_vector *refs = &t->refs; git_pkt_ref *pkt; git_transport_caps *caps = &t->caps; const char *ptr; pkt = git_vector_get(refs, 0); /* No refs or capabilites, odd but not a problem */ if (pkt == NULL || pkt->capabilities == NULL) return GIT_SUCCESS; ptr = pkt->capabilities; while (ptr != NULL && *ptr != '\0') { if (*ptr == ' ') ptr++; if(!git__prefixcmp(ptr, GIT_CAP_OFS_DELTA)) { caps->common = caps->ofs_delta = 1; ptr += strlen(GIT_CAP_OFS_DELTA); continue; } /* We don't know this capability, so skip it */ ptr = strchr(ptr, ' '); } return GIT_SUCCESS; } /* * Since this is a network connection, we need to parse and store the * pkt-lines at this stage and keep them there. */ static int git_connect(git_transport *transport, int direction) { transport_git *t = (transport_git *) transport; int error = GIT_SUCCESS; if (direction == GIT_DIR_PUSH) return git__throw(GIT_EINVALIDARGS, "Pushing is not supported with the git protocol"); t->parent.direction = direction; error = git_vector_init(&t->refs, 16, NULL); if (error < GIT_SUCCESS) goto cleanup; /* Connect and ask for the refs */ error = do_connect(t, transport->url); if (error < GIT_SUCCESS) return error; t->parent.connected = 1; error = store_refs(t); if (error < GIT_SUCCESS) return error; error = detect_caps(t); cleanup: if (error < GIT_SUCCESS) { git_vector_free(&t->refs); } return error; } static int git_ls(git_transport *transport, git_headarray *array) { transport_git *t = (transport_git *) transport; git_vector *refs = &t->refs; int len = 0; unsigned int i; array->heads = git__calloc(refs->length, sizeof(git_remote_head *)); if (array->heads == NULL) return GIT_ENOMEM; for (i = 0; i < refs->length; ++i) { git_pkt *p = git_vector_get(refs, i); if (p->type != GIT_PKT_REF) continue; ++len; array->heads[i] = &(((git_pkt_ref *) p)->head); } array->len = len; t->heads = array->heads; return GIT_SUCCESS; } static int git_send_wants(git_transport *transport, git_headarray *array) { transport_git *t = (transport_git *) transport; return git_pkt_send_wants(array, &t->caps, t->socket); } static int git_send_have(git_transport *transport, git_oid *oid) { transport_git *t = (transport_git *) transport; return git_pkt_send_have(oid, t->socket); } static int git_negotiate_fetch(git_transport *transport, git_repository *repo, git_headarray *GIT_UNUSED(list)) { transport_git *t = (transport_git *) transport; git_revwalk *walk; git_reference *ref; git_strarray refs; git_oid oid; int error; unsigned int i; char buff[128]; gitno_buffer buf; GIT_UNUSED_ARG(list); gitno_buffer_setup(&buf, buff, sizeof(buff), t->socket); error = git_reference_listall(&refs, repo, GIT_REF_LISTALL); if (error < GIT_ERROR) return git__rethrow(error, "Failed to list all references"); error = git_revwalk_new(&walk, repo); if (error < GIT_ERROR) { error = git__rethrow(error, "Failed to list all references"); goto cleanup; } git_revwalk_sorting(walk, GIT_SORT_TIME); for (i = 0; i < refs.count; ++i) { /* No tags */ if (!git__prefixcmp(refs.strings[i], GIT_REFS_TAGS_DIR)) continue; error = git_reference_lookup(&ref, repo, refs.strings[i]); if (error < GIT_ERROR) { error = git__rethrow(error, "Failed to lookup %s", refs.strings[i]); goto cleanup; } if (git_reference_type(ref) == GIT_REF_SYMBOLIC) continue; error = git_revwalk_push(walk, git_reference_oid(ref)); if (error < GIT_ERROR) { error = git__rethrow(error, "Failed to push %s", refs.strings[i]); goto cleanup; } } git_strarray_free(&refs); /* * We don't support any kind of ACK extensions, so the negotiation * boils down to sending what we have and listening for an ACK * every once in a while. */ i = 0; while ((error = git_revwalk_next(&oid, walk)) == GIT_SUCCESS) { error = git_pkt_send_have(&oid, t->socket); i++; if (i % 20 == 0) { const char *ptr = buf.data, *line_end; git_pkt *pkt; git_pkt_send_flush(t->socket); while (1) { /* Wait for max. 1 second */ error = gitno_select_in(&buf, 1, 0); if (error < GIT_SUCCESS) { error = git__throw(GIT_EOSERR, "Error in select"); } else if (error == 0) { /* * Some servers don't respond immediately, so if this * happens, we keep sending information until it * answers. */ break; } error = gitno_recv(&buf); if (error < GIT_SUCCESS) { error = git__rethrow(error, "Error receiving data"); goto cleanup; } error = git_pkt_parse_line(&pkt, ptr, &line_end, buf.offset); if (error == GIT_ESHORTBUFFER) continue; if (error < GIT_SUCCESS) { error = git__rethrow(error, "Failed to get answer"); goto cleanup; } gitno_consume(&buf, line_end); if (pkt->type == GIT_PKT_ACK) { error = GIT_SUCCESS; goto done; } else if (pkt->type == GIT_PKT_NAK) { break; } else { error = git__throw(GIT_ERROR, "Got unexpected pkt type"); goto cleanup; } } } } if (error == GIT_EREVWALKOVER) error = GIT_SUCCESS; done: git_pkt_send_flush(t->socket); git_pkt_send_done(t->socket); cleanup: git_revwalk_free(walk); return error; } static int git_send_flush(git_transport *transport) { transport_git *t = (transport_git *) transport; return git_pkt_send_flush(t->socket); } static int git_send_done(git_transport *transport) { transport_git *t = (transport_git *) transport; return git_pkt_send_done(t->socket); } static int store_pack(char **out, gitno_buffer *buf, git_repository *repo) { git_filebuf file; int error; char path[GIT_PATH_MAX], suff[] = "/objects/pack/pack-received\0"; off_t off = 0; strcpy(path, repo->path_repository); off += strlen(repo->path_repository); strcat(path, suff); //memcpy(path + off, suff, GIT_PATH_MAX - off - strlen(suff) - 1); if (memcmp(buf->data, "PACK", strlen("PACK"))) { return git__throw(GIT_ERROR, "The pack doesn't start with the signature"); } error = git_filebuf_open(&file, path, GIT_FILEBUF_TEMPORARY); if (error < GIT_SUCCESS) goto cleanup; while (1) { /* Part of the packfile has been received, don't loose it */ error = git_filebuf_write(&file, buf->data, buf->offset); if (error < GIT_SUCCESS) goto cleanup; gitno_consume_n(buf, buf->offset); error = gitno_recv(buf); if (error < GIT_SUCCESS) goto cleanup; if (error == 0) /* Orderly shutdown */ break; } *out = git__strdup(file.path_lock); if (*out == NULL) { error = GIT_ENOMEM; goto cleanup; } /* A bit dodgy, but we need to keep the pack at the temporary path */ error = git_filebuf_commit_at(&file, file.path_lock); cleanup: if (error < GIT_SUCCESS) git_filebuf_cleanup(&file); return error; } static int git_download_pack(char **out, git_transport *transport, git_repository *repo) { transport_git *t = (transport_git *) transport; int s = t->socket, error = GIT_SUCCESS; gitno_buffer buf; char buffer[1024]; git_pkt *pkt; const char *line_end, *ptr; gitno_buffer_setup(&buf, buffer, sizeof(buffer), s); /* * For now, we ignore everything and wait for the pack */ while (1) { error = gitno_recv(&buf); if (error < GIT_SUCCESS) return git__rethrow(GIT_EOSERR, "Failed to receive data"); if (error == 0) /* Orderly shutdown */ return GIT_SUCCESS; ptr = buf.data; /* Whilst we're searching for the pack */ while (1) { if (buf.offset == 0) break; error = git_pkt_parse_line(&pkt, ptr, &line_end, buf.offset); if (error == GIT_ESHORTBUFFER) break; if (error < GIT_SUCCESS) return error; if (pkt->type == GIT_PKT_PACK) return store_pack(out, &buf, repo); /* For now we don't care about anything */ free(pkt); gitno_consume(&buf, line_end); } } } static int git_close(git_transport *transport) { transport_git *t = (transport_git*) transport; int s = t->socket; int error; /* Can't do anything if there's an error, so don't bother checking */ git_pkt_send_flush(s); error = close(s); if (error < 0) error = git__throw(GIT_EOSERR, "Failed to close socket"); return error; } static void git_free(git_transport *transport) { transport_git *t = (transport_git *) transport; git_vector *refs = &t->refs; unsigned int i; for (i = 0; i < refs->length; ++i) { git_pkt *p = git_vector_get(refs, i); git_pkt_free(p); } git_vector_free(refs); free(t->heads); free(t->parent.url); free(t); } int git_transport_git(git_transport **out) { transport_git *t; t = git__malloc(sizeof(transport_git)); if (t == NULL) return GIT_ENOMEM; memset(t, 0x0, sizeof(transport_git)); t->parent.connect = git_connect; t->parent.ls = git_ls; t->parent.send_wants = git_send_wants; t->parent.send_have = git_send_have; t->parent.negotiate_fetch = git_negotiate_fetch; t->parent.send_flush = git_send_flush; t->parent.send_done = git_send_done; t->parent.download_pack = git_download_pack; t->parent.close = git_close; t->parent.free = git_free; *out = (git_transport *) t; return GIT_SUCCESS; }