| #define USE_THE_REPOSITORY_VARIABLE | |
| #include "git-compat-util.h" | |
| #include "gettext.h" | |
| #include "hex.h" | |
| #include "odb.h" | |
| #include "promisor-remote.h" | |
| #include "config.h" | |
| #include "trace2.h" | |
| #include "transport.h" | |
| #include "strvec.h" | |
| #include "packfile.h" | |
| #include "environment.h" | |
| #include "url.h" | |
| #include "urlmatch.h" | |
| #include "version.h" | |
| #include "wildmatch.h" | |
| struct promisor_remote_config { | |
| struct promisor_remote *promisors; | |
| struct promisor_remote **promisors_tail; | |
| }; | |
| static int fetch_objects(struct repository *repo, | |
| const char *remote_name, | |
| const struct object_id *oids, | |
| int oid_nr) | |
| { | |
| struct child_process child = CHILD_PROCESS_INIT; | |
| int i; | |
| FILE *child_in; | |
| int quiet; | |
| if (git_env_bool(NO_LAZY_FETCH_ENVIRONMENT, 0)) { | |
| static int warning_shown; | |
| if (!warning_shown) { | |
| warning_shown = 1; | |
| warning(_("lazy fetching disabled; some objects may not be available")); | |
| } | |
| return -1; | |
| } | |
| child.git_cmd = 1; | |
| child.in = -1; | |
| if (repo != the_repository) | |
| prepare_other_repo_env(&child.env, repo->gitdir); | |
| strvec_pushl(&child.args, "-c", "fetch.negotiationAlgorithm=noop", | |
| "fetch", remote_name, "--no-tags", | |
| "--no-write-fetch-head", "--recurse-submodules=no", | |
| "--filter=blob:none", "--stdin", NULL); | |
| if (!repo_config_get_bool(repo, "promisor.quiet", &quiet) && quiet) | |
| strvec_push(&child.args, "--quiet"); | |
| if (start_command(&child)) | |
| die(_("promisor-remote: unable to fork off fetch subprocess")); | |
| child_in = xfdopen(child.in, "w"); | |
| trace2_data_intmax("promisor", repo, "fetch_count", oid_nr); | |
| for (i = 0; i < oid_nr; i++) { | |
| if (fputs(oid_to_hex(&oids[i]), child_in) < 0) | |
| die_errno(_("promisor-remote: could not write to fetch subprocess")); | |
| if (fputc('\n', child_in) < 0) | |
| die_errno(_("promisor-remote: could not write to fetch subprocess")); | |
| } | |
| if (fclose(child_in) < 0) | |
| die_errno(_("promisor-remote: could not close stdin to fetch subprocess")); | |
| return finish_command(&child) ? -1 : 0; | |
| } | |
| static struct promisor_remote *promisor_remote_new(struct promisor_remote_config *config, | |
| const char *remote_name) | |
| { | |
| struct promisor_remote *r; | |
| if (*remote_name == '/') { | |
| warning(_("promisor remote name cannot begin with '/': %s"), | |
| remote_name); | |
| return NULL; | |
| } | |
| FLEX_ALLOC_STR(r, name, remote_name); | |
| *config->promisors_tail = r; | |
| config->promisors_tail = &r->next; | |
| return r; | |
| } | |
| static struct promisor_remote *promisor_remote_lookup(struct promisor_remote_config *config, | |
| const char *remote_name, | |
| struct promisor_remote **previous) | |
| { | |
| struct promisor_remote *r, *p; | |
| for (p = NULL, r = config->promisors; r; p = r, r = r->next) | |
| if (!strcmp(r->name, remote_name)) { | |
| if (previous) | |
| *previous = p; | |
| return r; | |
| } | |
| return NULL; | |
| } | |
| static void promisor_remote_move_to_tail(struct promisor_remote_config *config, | |
| struct promisor_remote *r, | |
| struct promisor_remote *previous) | |
| { | |
| if (!r->next) | |
| return; | |
| if (previous) | |
| previous->next = r->next; | |
| else | |
| config->promisors = r->next ? r->next : r; | |
| r->next = NULL; | |
| *config->promisors_tail = r; | |
| config->promisors_tail = &r->next; | |
| } | |
| static int promisor_remote_config(const char *var, const char *value, | |
| const struct config_context *ctx UNUSED, | |
| void *data) | |
| { | |
| struct promisor_remote_config *config = data; | |
| const char *name; | |
| size_t namelen; | |
| const char *subkey; | |
| if (parse_config_key(var, "remote", &name, &namelen, &subkey) < 0) | |
| return 0; | |
| if (!strcmp(subkey, "promisor")) { | |
| char *remote_name; | |
| if (!git_config_bool(var, value)) | |
| return 0; | |
| remote_name = xmemdupz(name, namelen); | |
| if (!promisor_remote_lookup(config, remote_name, NULL)) | |
| promisor_remote_new(config, remote_name); | |
| free(remote_name); | |
| return 0; | |
| } | |
| if (!strcmp(subkey, "partialclonefilter")) { | |
| struct promisor_remote *r; | |
| char *remote_name = xmemdupz(name, namelen); | |
| r = promisor_remote_lookup(config, remote_name, NULL); | |
| if (!r) | |
| r = promisor_remote_new(config, remote_name); | |
| free(remote_name); | |
| if (!r) | |
| return 0; | |
| FREE_AND_NULL(r->partial_clone_filter); | |
| return git_config_string(&r->partial_clone_filter, var, value); | |
| } | |
| return 0; | |
| } | |
| static void promisor_remote_init(struct repository *r) | |
| { | |
| struct promisor_remote_config *config; | |
| if (r->promisor_remote_config) | |
| return; | |
| config = r->promisor_remote_config = | |
| xcalloc(1, sizeof(*r->promisor_remote_config)); | |
| config->promisors_tail = &config->promisors; | |
| repo_config(r, promisor_remote_config, config); | |
| if (r->repository_format_partial_clone) { | |
| struct promisor_remote *o, *previous; | |
| o = promisor_remote_lookup(config, | |
| r->repository_format_partial_clone, | |
| &previous); | |
| if (o) | |
| promisor_remote_move_to_tail(config, o, previous); | |
| else | |
| promisor_remote_new(config, r->repository_format_partial_clone); | |
| } | |
| } | |
| void promisor_remote_clear(struct promisor_remote_config *config) | |
| { | |
| while (config->promisors) { | |
| struct promisor_remote *r = config->promisors; | |
| free(r->partial_clone_filter); | |
| free(r->advertised_filter); | |
| config->promisors = config->promisors->next; | |
| free(r); | |
| } | |
| config->promisors_tail = &config->promisors; | |
| } | |
| void repo_promisor_remote_reinit(struct repository *r) | |
| { | |
| promisor_remote_clear(r->promisor_remote_config); | |
| FREE_AND_NULL(r->promisor_remote_config); | |
| promisor_remote_init(r); | |
| } | |
| struct promisor_remote *repo_promisor_remote_find(struct repository *r, | |
| const char *remote_name) | |
| { | |
| promisor_remote_init(r); | |
| if (!remote_name) | |
| return r->promisor_remote_config->promisors; | |
| return promisor_remote_lookup(r->promisor_remote_config, remote_name, NULL); | |
| } | |
| int repo_has_promisor_remote(struct repository *r) | |
| { | |
| return !!repo_promisor_remote_find(r, NULL); | |
| } | |
| int repo_has_accepted_promisor_remote(struct repository *r) | |
| { | |
| struct promisor_remote *p; | |
| promisor_remote_init(r); | |
| for (p = r->promisor_remote_config->promisors; p; p = p->next) | |
| if (p->accepted) | |
| return 1; | |
| return 0; | |
| } | |
| static int remove_fetched_oids(struct repository *repo, | |
| struct object_id **oids, | |
| int oid_nr, int to_free) | |
| { | |
| int i, remaining_nr = 0; | |
| int *remaining = xcalloc(oid_nr, sizeof(*remaining)); | |
| struct object_id *old_oids = *oids; | |
| struct object_id *new_oids; | |
| for (i = 0; i < oid_nr; i++) | |
| if (odb_read_object_info_extended(repo->objects, &old_oids[i], NULL, | |
| OBJECT_INFO_SKIP_FETCH_OBJECT)) { | |
| remaining[i] = 1; | |
| remaining_nr++; | |
| } | |
| if (remaining_nr) { | |
| int j = 0; | |
| CALLOC_ARRAY(new_oids, remaining_nr); | |
| for (i = 0; i < oid_nr; i++) | |
| if (remaining[i]) | |
| oidcpy(&new_oids[j++], &old_oids[i]); | |
| *oids = new_oids; | |
| if (to_free) | |
| free(old_oids); | |
| } | |
| free(remaining); | |
| return remaining_nr; | |
| } | |
| static int try_promisor_remotes(struct repository *repo, | |
| struct object_id **remaining_oids, | |
| int *remaining_nr, int *to_free, | |
| bool accepted_only) | |
| { | |
| struct promisor_remote *r = repo->promisor_remote_config->promisors; | |
| for (; r; r = r->next) { | |
| if (accepted_only != r->accepted) | |
| continue; | |
| if (fetch_objects(repo, r->name, *remaining_oids, *remaining_nr) < 0) { | |
| if (*remaining_nr == 1) | |
| continue; | |
| *remaining_nr = remove_fetched_oids(repo, remaining_oids, | |
| *remaining_nr, *to_free); | |
| if (*remaining_nr) { | |
| *to_free = 1; | |
| continue; | |
| } | |
| } | |
| return 1; /* all fetched */ | |
| } | |
| return 0; | |
| } | |
| void promisor_remote_get_direct(struct repository *repo, | |
| const struct object_id *oids, | |
| int oid_nr) | |
| { | |
| struct object_id *remaining_oids = (struct object_id *)oids; | |
| int remaining_nr = oid_nr; | |
| int to_free = 0; | |
| int i; | |
| if (oid_nr == 0) | |
| return; | |
| promisor_remote_init(repo); | |
| /* Try accepted remotes first (those the server told us to use) */ | |
| if (try_promisor_remotes(repo, &remaining_oids, &remaining_nr, | |
| &to_free, true)) | |
| goto all_fetched; | |
| if (try_promisor_remotes(repo, &remaining_oids, &remaining_nr, | |
| &to_free, false)) | |
| goto all_fetched; | |
| for (i = 0; i < remaining_nr; i++) { | |
| if (is_promisor_object(repo, &remaining_oids[i])) | |
| die(_("could not fetch %s from promisor remote"), | |
| oid_to_hex(&remaining_oids[i])); | |
| } | |
| all_fetched: | |
| if (to_free) | |
| free(remaining_oids); | |
| } | |
| static int allow_unsanitized(char ch) | |
| { | |
| if (ch == ',' || ch == ';' || ch == '%') | |
| return 0; | |
| return ch > 32 && ch < 127; | |
| } | |
| /* | |
| * All the fields used in "promisor-remote" protocol capability, | |
| * including the mandatory "name" and "url" ones. | |
| */ | |
| static const char promisor_field_name[] = "name"; | |
| static const char promisor_field_url[] = "url"; | |
| static const char promisor_field_filter[] = "partialCloneFilter"; | |
| static const char promisor_field_token[] = "token"; | |
| /* | |
| * List of optional field names that can be used in the | |
| * "promisor-remote" protocol capability (others must be | |
| * ignored). Each field should correspond to a configurable property | |
| * of a remote that can be relevant for the client. | |
| */ | |
| static const char *known_fields[] = { | |
| promisor_field_filter, /* Filter used for partial clone */ | |
| promisor_field_token, /* Authentication token for the remote */ | |
| NULL | |
| }; | |
| /* | |
| * Check if 'field' is in the list of the known field names for the | |
| * "promisor-remote" protocol capability. | |
| */ | |
| static int is_known_field(const char *field) | |
| { | |
| const char **p; | |
| for (p = known_fields; *p; p++) | |
| if (!strcasecmp(*p, field)) | |
| return 1; | |
| return 0; | |
| } | |
| static int is_valid_field(struct string_list_item *item, void *cb_data) | |
| { | |
| const char *field = item->string; | |
| const char *config_key = (const char *)cb_data; | |
| if (!is_known_field(field)) { | |
| warning(_("unsupported field '%s' in '%s' config"), field, config_key); | |
| return 0; | |
| } | |
| return 1; | |
| } | |
| static char *fields_from_config(struct string_list *fields_list, const char *config_key) | |
| { | |
| char *fields = NULL; | |
| if (!repo_config_get_string(the_repository, config_key, &fields) && *fields) { | |
| string_list_split_in_place_f(fields_list, fields, ",", -1, | |
| STRING_LIST_SPLIT_TRIM | | |
| STRING_LIST_SPLIT_NONEMPTY); | |
| filter_string_list(fields_list, 0, is_valid_field, (void *)config_key); | |
| } | |
| return fields; | |
| } | |
| static struct string_list *initialize_fields_list(struct string_list *fields_list, int *initialized, | |
| const char *config_key) | |
| { | |
| if (!*initialized) { | |
| fields_list->cmp = strcasecmp; | |
| fields_from_config(fields_list, config_key); | |
| *initialized = 1; | |
| } | |
| return fields_list; | |
| } | |
| static struct string_list *fields_sent(void) | |
| { | |
| static struct string_list fields_list = STRING_LIST_INIT_NODUP; | |
| static int initialized; | |
| return initialize_fields_list(&fields_list, &initialized, "promisor.sendFields"); | |
| } | |
| static struct string_list *fields_checked(void) | |
| { | |
| static struct string_list fields_list = STRING_LIST_INIT_NODUP; | |
| static int initialized; | |
| return initialize_fields_list(&fields_list, &initialized, "promisor.checkFields"); | |
| } | |
| static struct string_list *fields_stored(void) | |
| { | |
| static struct string_list fields_list = STRING_LIST_INIT_NODUP; | |
| static int initialized; | |
| return initialize_fields_list(&fields_list, &initialized, "promisor.storeFields"); | |
| } | |
| /* | |
| * Struct for promisor remotes involved in the "promisor-remote" | |
| * protocol capability. | |
| * | |
| * Except for "name" and "local_name", each <member> in this struct | |
| * and its <value> should correspond (either on the client side or on | |
| * the server side) to a "remote.<name>.<member>" config variable set | |
| * to <value> where "<name>" is a promisor remote name. | |
| */ | |
| struct promisor_info { | |
| const char *name; /* name the server advertised */ | |
| const char *local_name; /* name used locally (may be auto-generated) */ | |
| const char *url; | |
| const char *filter; | |
| const char *token; | |
| }; | |
| static void promisor_info_free(struct promisor_info *p) | |
| { | |
| free((char *)p->name); | |
| free((char *)p->local_name); | |
| free((char *)p->url); | |
| free((char *)p->filter); | |
| free((char *)p->token); | |
| free(p); | |
| } | |
| static void promisor_info_list_clear(struct string_list *list) | |
| { | |
| for (size_t i = 0; i < list->nr; i++) | |
| promisor_info_free(list->items[i].util); | |
| string_list_clear(list, 0); | |
| } | |
| static const char *promisor_info_local_name(struct promisor_info *p) | |
| { | |
| return p->local_name ? p->local_name : p->name; | |
| } | |
| static void set_one_field(struct promisor_info *p, | |
| const char *field, const char *value) | |
| { | |
| if (!strcasecmp(field, promisor_field_filter)) | |
| p->filter = xstrdup(value); | |
| else if (!strcasecmp(field, promisor_field_token)) | |
| p->token = xstrdup(value); | |
| else | |
| BUG("invalid field '%s'", field); | |
| } | |
| static void set_fields(struct promisor_info *p, | |
| struct string_list *field_names) | |
| { | |
| struct string_list_item *item; | |
| for_each_string_list_item(item, field_names) { | |
| char *key = xstrfmt("remote.%s.%s", p->name, item->string); | |
| const char *val; | |
| if (!repo_config_get_string_tmp(the_repository, key, &val) && *val) | |
| set_one_field(p, item->string, val); | |
| free(key); | |
| } | |
| } | |
| /* | |
| * Populate 'list' with promisor remote information from the config. | |
| * The 'util' pointer of each list item will hold a 'struct | |
| * promisor_info'. Except "name" and "url", only members of that | |
| * struct specified by the 'field_names' list are set (using values | |
| * from the configuration). | |
| */ | |
| static void promisor_config_info_list(struct repository *repo, | |
| struct string_list *list, | |
| struct string_list *field_names) | |
| { | |
| struct promisor_remote *r; | |
| promisor_remote_init(repo); | |
| for (r = repo->promisor_remote_config->promisors; r; r = r->next) { | |
| const char *url; | |
| char *url_key = xstrfmt("remote.%s.url", r->name); | |
| /* Only add remotes with a non empty URL */ | |
| if (!repo_config_get_string_tmp(the_repository, url_key, &url) && *url) { | |
| struct promisor_info *new_info = xcalloc(1, sizeof(*new_info)); | |
| struct string_list_item *item; | |
| new_info->name = xstrdup(r->name); | |
| new_info->url = xstrdup(url); | |
| if (field_names) | |
| set_fields(new_info, field_names); | |
| item = string_list_append(list, new_info->name); | |
| item->util = new_info; | |
| } | |
| free(url_key); | |
| } | |
| } | |
| char *promisor_remote_info(struct repository *repo) | |
| { | |
| struct strbuf sb = STRBUF_INIT; | |
| int advertise_promisors = 0; | |
| struct string_list config_info = STRING_LIST_INIT_NODUP; | |
| struct string_list_item *item; | |
| repo_config_get_bool(the_repository, "promisor.advertise", &advertise_promisors); | |
| if (!advertise_promisors) | |
| return NULL; | |
| promisor_config_info_list(repo, &config_info, fields_sent()); | |
| if (!config_info.nr) | |
| return NULL; | |
| for_each_string_list_item(item, &config_info) { | |
| struct promisor_info *p = item->util; | |
| if (item != config_info.items) | |
| strbuf_addch(&sb, ';'); | |
| strbuf_addf(&sb, "%s=", promisor_field_name); | |
| strbuf_addstr_urlencode(&sb, p->name, allow_unsanitized); | |
| strbuf_addf(&sb, ",%s=", promisor_field_url); | |
| strbuf_addstr_urlencode(&sb, p->url, allow_unsanitized); | |
| if (p->filter) { | |
| strbuf_addf(&sb, ",%s=", promisor_field_filter); | |
| strbuf_addstr_urlencode(&sb, p->filter, allow_unsanitized); | |
| } | |
| if (p->token) { | |
| strbuf_addf(&sb, ",%s=", promisor_field_token); | |
| strbuf_addstr_urlencode(&sb, p->token, allow_unsanitized); | |
| } | |
| } | |
| promisor_info_list_clear(&config_info); | |
| return strbuf_detach(&sb, NULL); | |
| } | |
| enum accept_promisor { | |
| ACCEPT_NONE = 0, | |
| ACCEPT_KNOWN_URL, | |
| ACCEPT_KNOWN_NAME, | |
| ACCEPT_ALL | |
| }; | |
| /* | |
| * Check if a specific field and its advertised value match the local | |
| * configuration of a given promisor remote. | |
| * | |
| * Returns 1 if they match, 0 otherwise. | |
| */ | |
| static int match_field_against_config(const char *field, const char *value, | |
| struct promisor_info *config_info) | |
| { | |
| if (config_info->filter && !strcasecmp(field, promisor_field_filter)) | |
| return !strcmp(config_info->filter, value); | |
| else if (config_info->token && !strcasecmp(field, promisor_field_token)) | |
| return !strcmp(config_info->token, value); | |
| return 0; | |
| } | |
| /* | |
| * Check that the advertised fields match the local configuration. | |
| * | |
| * When 'config_entry' is NULL (ACCEPT_ALL mode), every checked field | |
| * must match at least one remote in 'config_info'. | |
| * | |
| * When 'config_entry' points to a specific remote's config, the | |
| * checked fields are compared against that single remote only. | |
| */ | |
| static int all_fields_match(struct promisor_info *advertised, | |
| struct string_list *config_info, | |
| struct promisor_info *config_entry) | |
| { | |
| struct string_list *fields = fields_checked(); | |
| struct string_list_item *item_checked; | |
| for_each_string_list_item(item_checked, fields) { | |
| int match = 0; | |
| const char *field = item_checked->string; | |
| const char *value = NULL; | |
| if (!strcasecmp(field, promisor_field_filter)) | |
| value = advertised->filter; | |
| else if (!strcasecmp(field, promisor_field_token)) | |
| value = advertised->token; | |
| if (!value) | |
| return 0; | |
| if (config_entry) { | |
| match = match_field_against_config(field, value, | |
| config_entry); | |
| } else { | |
| struct string_list_item *item; | |
| for_each_string_list_item(item, config_info) { | |
| struct promisor_info *p = item->util; | |
| if (match_field_against_config(field, value, p)) { | |
| match = 1; | |
| break; | |
| } | |
| } | |
| } | |
| if (!match) | |
| return 0; | |
| } | |
| return 1; | |
| } | |
| static bool has_control_char(const char *s) | |
| { | |
| for (const char *c = s; *c; c++) | |
| if (iscntrl(*c)) | |
| return true; | |
| return false; | |
| } | |
| struct allowed_url { | |
| char *remote_name; | |
| char *url_pattern; | |
| struct url_info pattern_info; | |
| }; | |
| static void allowed_url_free(void *util, const char *str UNUSED) | |
| { | |
| struct allowed_url *allowed = util; | |
| if (!allowed) | |
| return; | |
| /* Depending on prefix, free either remote_name or url_pattern */ | |
| free(allowed->remote_name ? allowed->remote_name : allowed->url_pattern); | |
| free(allowed->pattern_info.url); | |
| free(allowed); | |
| } | |
| static struct allowed_url *valid_accept_url(const char *url) | |
| { | |
| char *dup, *p; | |
| struct allowed_url *allowed; | |
| if (!url) | |
| return NULL; | |
| dup = xstrdup(url); | |
| p = strchr(dup, '='); | |
| if (p) { | |
| *p = '\0'; | |
| if (!valid_remote_name(dup)) { | |
| warning(_("invalid remote name '%s' before '=' sign " | |
| "in '%s' from promisor.acceptFromServerUrl config"), | |
| dup, url); | |
| free(dup); | |
| return NULL; | |
| } | |
| p++; | |
| } else { | |
| p = dup; | |
| } | |
| if (has_control_char(p)) { | |
| warning(_("invalid url pattern '%s' " | |
| "in '%s' from promisor.acceptFromServerUrl config"), p, url); | |
| free(dup); | |
| return NULL; | |
| } | |
| allowed = xmalloc(sizeof(*allowed)); | |
| allowed->remote_name = (p == dup) ? NULL : dup; | |
| allowed->url_pattern = p; | |
| allowed->pattern_info.url = url_normalize_pattern(p, &allowed->pattern_info); | |
| if (!allowed->pattern_info.url) { | |
| warning(_("invalid url pattern '%s' " | |
| "in '%s' from promisor.acceptFromServerUrl config"), p, url); | |
| free(dup); | |
| free(allowed); | |
| return NULL; | |
| } | |
| return allowed; | |
| } | |
| static void load_accept_from_server_url(struct repository *repo, | |
| struct string_list *accept_urls) | |
| { | |
| const struct string_list *config_urls; | |
| if (!repo_config_get_string_multi(repo, "promisor.acceptfromserverurl", &config_urls)) { | |
| struct string_list_item *item; | |
| for_each_string_list_item(item, config_urls) { | |
| struct allowed_url *allowed = valid_accept_url(item->string); | |
| if (allowed) { | |
| struct string_list_item *new; | |
| new = string_list_append(accept_urls, item->string); | |
| new->util = allowed; | |
| } | |
| } | |
| } | |
| } | |
| static bool match_pattern_url(const char *pat, size_t pat_len, | |
| const char *url, size_t url_len) | |
| { | |
| char *p_str = xstrndup(pat, pat_len); | |
| char *u_str = xstrndup(url, url_len); | |
| bool res = !wildmatch(p_str, u_str, 0); | |
| free(p_str); | |
| free(u_str); | |
| return res; | |
| } | |
| static bool match_one_url(const struct url_info *pi, const struct url_info *ui) | |
| { | |
| const char *pat = pi->url; | |
| const char *url = ui->url; | |
| /* | |
| * Schemes must match exactly. They are case-folded by | |
| * url_normalize(), so strncmp() suffices. | |
| */ | |
| if (pi->scheme_len != ui->scheme_len || strncmp(pat, url, pi->scheme_len)) | |
| return false; | |
| /* | |
| * Ports must match exactly. url_normalize() strips default | |
| * ports (like 443 for https), so length and content | |
| * comparisons are sufficient. | |
| */ | |
| if (pi->port_len != ui->port_len || | |
| strncmp(pat + pi->port_off, url + ui->port_off, pi->port_len)) | |
| return false; | |
| /* | |
| * Match host and path separately to prevent a '*' in the host | |
| * portion of the pattern from matching across the '/' | |
| * boundary into the path. | |
| */ | |
| return match_pattern_url(pat + pi->host_off, pi->host_len, | |
| url + ui->host_off, ui->host_len) && | |
| match_pattern_url(pat + pi->path_off, pi->path_len, | |
| url + ui->path_off, ui->path_len); | |
| } | |
| static struct allowed_url *url_matches_accept_list( | |
| struct string_list *accept_urls, const char *url) | |
| { | |
| struct string_list_item *item; | |
| struct url_info url_info; | |
| url_info.url = url_normalize(url, &url_info); | |
| if (!url_info.url) | |
| return NULL; | |
| for_each_string_list_item(item, accept_urls) { | |
| struct allowed_url *allowed = item->util; | |
| if (match_one_url(&allowed->pattern_info, &url_info)) { | |
| free(url_info.url); | |
| return allowed; | |
| } | |
| } | |
| free(url_info.url); | |
| return NULL; | |
| } | |
| /* | |
| * Sanitize the buffer to make it a valid remote name coming from the | |
| * server by: | |
| * | |
| * - replacing any non alphanumeric character with a '-' | |
| * - stripping any leading '-', | |
| * - condensing multiple '-' into one, | |
| * - prepending "promisor-auto-", | |
| * - validating the result. | |
| */ | |
| static int sanitize_remote_name(struct strbuf *buf, const char *url) | |
| { | |
| char prev = '-'; | |
| for (size_t i = 0; i < buf->len; ) { | |
| if (!isalnum(buf->buf[i])) | |
| buf->buf[i] = '-'; | |
| if (prev == '-' && buf->buf[i] == '-') { | |
| strbuf_remove(buf, i, 1); | |
| } else { | |
| prev = buf->buf[i]; | |
| i++; | |
| } | |
| } | |
| strbuf_strip_suffix(buf, "-"); | |
| if (!buf->len) { | |
| warning(_("couldn't generate a valid remote name from " | |
| "advertised url '%s', ignoring this remote"), url); | |
| return -1; | |
| } | |
| strbuf_insertstr(buf, 0, "promisor-auto-"); | |
| if (!valid_remote_name(buf->buf)) { | |
| warning(_("generated remote name '%s' from advertised url '%s' " | |
| "is invalid, ignoring this remote"), buf->buf, url); | |
| return -1; | |
| } | |
| return 0; | |
| } | |
| static char *promisor_remote_name_from_url(const char *url) | |
| { | |
| struct url_info url_info = { 0 }; | |
| char *normalized = url_normalize(url, &url_info); | |
| struct strbuf buf = STRBUF_INIT; | |
| if (!normalized) { | |
| warning(_("couldn't normalize advertised url '%s', " | |
| "ignoring this remote"), url); | |
| return NULL; | |
| } | |
| if (url_info.host_len) { | |
| strbuf_add(&buf, normalized + url_info.host_off, url_info.host_len); | |
| strbuf_addch(&buf, '-'); | |
| } | |
| if (url_info.port_len) { | |
| strbuf_add(&buf, normalized + url_info.port_off, url_info.port_len); | |
| strbuf_addch(&buf, '-'); | |
| } | |
| if (url_info.path_len) { | |
| strbuf_add(&buf, normalized + url_info.path_off, url_info.path_len); | |
| strbuf_trim_trailing_dir_sep(&buf); | |
| strbuf_strip_suffix(&buf, ".git"); | |
| } | |
| free(normalized); | |
| if (sanitize_remote_name(&buf, url)) { | |
| strbuf_release(&buf); | |
| return NULL; | |
| } | |
| return strbuf_detach(&buf, NULL); | |
| } | |
| static void configure_auto_promisor_remote(struct repository *repo, | |
| const char *name, | |
| const char *url, | |
| const char *advertised_as, | |
| bool reuse) | |
| { | |
| char *key; | |
| if (!reuse) { | |
| fprintf(stderr, _("Auto-creating promisor remote '%s' for URL '%s'\n"), | |
| name, url); | |
| key = xstrfmt("remote.%s.url", name); | |
| repo_config_set_gently(repo, key, url); | |
| free(key); | |
| } | |
| /* NB: when reusing, this promotes an existing non-promisor remote */ | |
| key = xstrfmt("remote.%s.promisor", name); | |
| repo_config_set_gently(repo, key, "true"); | |
| free(key); | |
| if (advertised_as) { | |
| key = xstrfmt("remote.%s.advertisedAs", name); | |
| repo_config_set_gently(repo, key, advertised_as); | |
| free(key); | |
| } | |
| } | |
| #define MAX_REMOTES_WITH_SIMILAR_NAMES 20 | |
| /* Return the allocated local name, or NULL on failure */ | |
| static char *handle_matching_allowed_url(struct repository *repo, | |
| char *allowed_name, | |
| const char *remote_url, | |
| const char *remote_name) | |
| { | |
| char *name; | |
| char *basename = allowed_name ? | |
| xstrdup(allowed_name) : | |
| promisor_remote_name_from_url(remote_url); | |
| int i = 0; | |
| bool reuse = false; | |
| if (!basename) | |
| return NULL; | |
| name = xstrdup(basename); | |
| while (i < MAX_REMOTES_WITH_SIMILAR_NAMES) { | |
| char *url_key = xstrfmt("remote.%s.url", name); | |
| const char *existing_url; | |
| int exists = !repo_config_get_string_tmp(repo, url_key, &existing_url); | |
| free(url_key); | |
| if (!exists) | |
| break; /* Free to use */ | |
| if (!strcmp(existing_url, remote_url)) { | |
| reuse = true; | |
| break; /* Same URL, so safe to reuse */ | |
| } | |
| i++; | |
| free(name); | |
| name = xstrfmt("%s-%d", basename, i); | |
| } | |
| if (i < MAX_REMOTES_WITH_SIMILAR_NAMES) { | |
| configure_auto_promisor_remote(repo, name, | |
| remote_url, remote_name, | |
| reuse); | |
| } else { | |
| warning(_("too many remotes accepted with name like '%s-X', " | |
| "ignoring this remote"), basename); | |
| FREE_AND_NULL(name); | |
| } | |
| free(basename); | |
| return name; | |
| } | |
| static int should_accept_new_remote_url(struct repository *repo, | |
| struct string_list *accept_urls, | |
| struct promisor_info *advertised) | |
| { | |
| struct allowed_url *allowed = url_matches_accept_list(accept_urls, | |
| advertised->url); | |
| if (allowed) { | |
| char *name = handle_matching_allowed_url(repo, | |
| allowed->remote_name, | |
| advertised->url, | |
| advertised->name); | |
| if (name) { | |
| free((char *)advertised->local_name); | |
| advertised->local_name = name; | |
| return 1; | |
| } | |
| } | |
| return 0; | |
| } | |
| static int should_accept_remote(struct repository *repo, | |
| enum accept_promisor accept, | |
| struct promisor_info *advertised, | |
| struct string_list *accept_urls, | |
| struct string_list *config_info, | |
| bool *reload_config) | |
| { | |
| struct promisor_info *p; | |
| struct string_list_item *item; | |
| const char *remote_name = advertised->name; | |
| const char *remote_url = advertised->url; | |
| if (!remote_url || !*remote_url) | |
| BUG("no or empty URL advertised for remote '%s'; " | |
| "this remote should have been rejected earlier", | |
| remote_name); | |
| /* Get config info for that promisor remote */ | |
| item = string_list_lookup(config_info, remote_name); | |
| if (!item) { | |
| /* We don't know about that remote */ | |
| int res = should_accept_new_remote_url(repo, accept_urls, advertised); | |
| if (res) { | |
| *reload_config = true; | |
| return res; | |
| } | |
| if (accept == ACCEPT_ALL) | |
| return all_fields_match(advertised, config_info, NULL); | |
| return 0; | |
| } | |
| p = item->util; | |
| /* Known remote in the allowlist? */ | |
| if (!strcmp(p->url, remote_url) && url_matches_accept_list(accept_urls, remote_url)) | |
| return all_fields_match(advertised, config_info, p); | |
| if (accept == ACCEPT_ALL) | |
| return all_fields_match(advertised, config_info, NULL); | |
| if (accept == ACCEPT_KNOWN_NAME) | |
| return all_fields_match(advertised, config_info, p); | |
| if (strcmp(p->url, remote_url)) { | |
| warning(_("known remote named '%s' but with URL '%s' instead of '%s', " | |
| "ignoring this remote"), | |
| remote_name, p->url, remote_url); | |
| return 0; | |
| } | |
| if (accept == ACCEPT_KNOWN_URL) | |
| return all_fields_match(advertised, config_info, p); | |
| if (accept != ACCEPT_NONE) | |
| BUG("Unhandled 'enum accept_promisor' value '%d'", accept); | |
| return 0; | |
| } | |
| static int skip_field_name_prefix(const char *elem, const char *field_name, const char **value) | |
| { | |
| const char *p; | |
| if (!skip_prefix(elem, field_name, &p) || *p != '=') | |
| return 0; | |
| *value = p + 1; | |
| return 1; | |
| } | |
| static struct promisor_info *parse_one_advertised_remote(const char *remote_info) | |
| { | |
| struct promisor_info *info = xcalloc(1, sizeof(*info)); | |
| struct string_list elem_list = STRING_LIST_INIT_DUP; | |
| struct string_list_item *item; | |
| string_list_split(&elem_list, remote_info, ",", -1); | |
| for_each_string_list_item(item, &elem_list) { | |
| const char *elem = item->string; | |
| const char *p = strchr(elem, '='); | |
| if (!p) { | |
| warning(_("invalid element '%s' from remote info"), elem); | |
| continue; | |
| } | |
| if (skip_field_name_prefix(elem, promisor_field_name, &p)) | |
| info->name = url_percent_decode(p); | |
| else if (skip_field_name_prefix(elem, promisor_field_url, &p)) | |
| info->url = url_percent_decode(p); | |
| else if (skip_field_name_prefix(elem, promisor_field_filter, &p)) | |
| info->filter = url_percent_decode(p); | |
| else if (skip_field_name_prefix(elem, promisor_field_token, &p)) | |
| info->token = url_percent_decode(p); | |
| } | |
| string_list_clear(&elem_list, 0); | |
| if (!info->name || !*info->name || !info->url || !*info->url) { | |
| warning(_("server advertised a promisor remote without a name or URL: '%s', " | |
| "ignoring this remote"), remote_info); | |
| promisor_info_free(info); | |
| return NULL; | |
| } | |
| return info; | |
| } | |
| static bool store_one_field(struct repository *repo, const char *remote_name, | |
| const char *field_name, const char *field_key, | |
| const char *advertised, const char *current) | |
| { | |
| if (advertised && (!current || strcmp(current, advertised))) { | |
| char *key = xstrfmt("remote.%s.%s", remote_name, field_key); | |
| fprintf(stderr, _("Storing new %s from server for remote '%s'.\n" | |
| " '%s' -> '%s'\n"), | |
| field_name, remote_name, | |
| current ? current : "", | |
| advertised); | |
| repo_config_set_gently(repo, key, advertised); | |
| free(key); | |
| return true; | |
| } | |
| return false; | |
| } | |
| /* Check that a filter is valid by parsing it */ | |
| static bool valid_filter(const char *filter, const char *remote_name) | |
| { | |
| struct list_objects_filter_options filter_opts = LIST_OBJECTS_FILTER_INIT; | |
| struct strbuf err = STRBUF_INIT; | |
| int res = gently_parse_list_objects_filter(&filter_opts, filter, &err); | |
| if (res) | |
| warning(_("invalid filter '%s' for remote '%s' " | |
| "will not be stored: %s"), | |
| filter, remote_name, err.buf); | |
| list_objects_filter_release(&filter_opts); | |
| strbuf_release(&err); | |
| return !res; | |
| } | |
| static bool valid_token(const char *token, const char *remote_name) | |
| { | |
| if (has_control_char(token)) { | |
| warning(_("invalid token '%s' for remote '%s' " | |
| "will not be stored"), | |
| token, remote_name); | |
| return false; | |
| } | |
| return true; | |
| } | |
| struct store_info { | |
| struct repository *repo; | |
| struct string_list config_info; | |
| bool store_filter; | |
| bool store_token; | |
| }; | |
| static struct store_info *store_info_new(struct repository *repo) | |
| { | |
| struct string_list *fields_to_store = fields_stored(); | |
| struct store_info *s = xmalloc(sizeof(*s)); | |
| s->repo = repo; | |
| string_list_init_nodup(&s->config_info); | |
| promisor_config_info_list(repo, &s->config_info, fields_to_store); | |
| string_list_sort(&s->config_info); | |
| s->store_filter = !!string_list_lookup(fields_to_store, promisor_field_filter); | |
| s->store_token = !!string_list_lookup(fields_to_store, promisor_field_token); | |
| return s; | |
| } | |
| static void store_info_free(struct store_info *s) | |
| { | |
| if (s) { | |
| promisor_info_list_clear(&s->config_info); | |
| free(s); | |
| } | |
| } | |
| static bool promisor_store_advertised_fields(struct promisor_info *advertised, | |
| struct store_info *store_info) | |
| { | |
| struct promisor_info *p; | |
| struct string_list_item *item; | |
| const char *remote_name = promisor_info_local_name(advertised); | |
| bool reload_config = false; | |
| if (!(store_info->store_filter || store_info->store_token)) | |
| return false; | |
| /* | |
| * Get existing config info for the advertised promisor | |
| * remote. This ensures the remote is already configured on | |
| * the client side. | |
| */ | |
| item = string_list_lookup(&store_info->config_info, remote_name); | |
| if (!item) | |
| return false; | |
| p = item->util; | |
| if (store_info->store_filter && advertised->filter && | |
| valid_filter(advertised->filter, remote_name)) | |
| reload_config |= store_one_field(store_info->repo, remote_name, | |
| "filter", promisor_field_filter, | |
| advertised->filter, p->filter); | |
| if (store_info->store_token && advertised->token && | |
| valid_token(advertised->token, remote_name)) | |
| reload_config |= store_one_field(store_info->repo, remote_name, | |
| "token", promisor_field_token, | |
| advertised->token, p->token); | |
| return reload_config; | |
| } | |
| static enum accept_promisor accept_from_server(struct repository *repo) | |
| { | |
| const char *accept_str; | |
| enum accept_promisor accept = ACCEPT_NONE; | |
| if (!repo_config_get_string_tmp(repo, "promisor.acceptfromserver", &accept_str)) { | |
| if (!*accept_str || !strcasecmp("None", accept_str)) | |
| accept = ACCEPT_NONE; | |
| else if (!strcasecmp("KnownUrl", accept_str)) | |
| accept = ACCEPT_KNOWN_URL; | |
| else if (!strcasecmp("KnownName", accept_str)) | |
| accept = ACCEPT_KNOWN_NAME; | |
| else if (!strcasecmp("All", accept_str)) | |
| accept = ACCEPT_ALL; | |
| else | |
| warning(_("unknown '%s' value for '%s' config option"), | |
| accept_str, "promisor.acceptfromserver"); | |
| } | |
| return accept; | |
| } | |
| static void filter_promisor_remote(struct repository *repo, | |
| struct string_list *accepted_remotes, | |
| const char *info) | |
| { | |
| struct string_list config_info = STRING_LIST_INIT_NODUP; | |
| struct string_list remote_info = STRING_LIST_INIT_DUP; | |
| struct store_info *store_info = NULL; | |
| struct string_list_item *item; | |
| bool reload_config = false; | |
| enum accept_promisor accept = accept_from_server(repo); | |
| struct string_list accept_urls = STRING_LIST_INIT_DUP; | |
| /* Load and validate the acceptFromServerUrl config */ | |
| load_accept_from_server_url(repo, &accept_urls); | |
| if (accept == ACCEPT_NONE && !accept_urls.nr) | |
| return; | |
| /* Parse remote info received */ | |
| string_list_split(&remote_info, info, ";", -1); | |
| for_each_string_list_item(item, &remote_info) { | |
| struct promisor_info *advertised; | |
| advertised = parse_one_advertised_remote(item->string); | |
| if (!advertised) | |
| continue; | |
| if (!config_info.nr) { | |
| promisor_config_info_list(repo, &config_info, fields_checked()); | |
| string_list_sort(&config_info); | |
| } | |
| if (should_accept_remote(repo, accept, advertised, &accept_urls, | |
| &config_info, &reload_config)) { | |
| if (!store_info) | |
| store_info = store_info_new(repo); | |
| if (promisor_store_advertised_fields(advertised, store_info)) | |
| reload_config = true; | |
| string_list_append(accepted_remotes, advertised->name)->util = advertised; | |
| } else { | |
| promisor_info_free(advertised); | |
| } | |
| } | |
| string_list_clear_func(&accept_urls, allowed_url_free); | |
| promisor_info_list_clear(&config_info); | |
| string_list_clear(&remote_info, 0); | |
| store_info_free(store_info); | |
| if (reload_config) | |
| repo_promisor_remote_reinit(repo); | |
| /* Apply accepted remotes to the stable repo state */ | |
| for_each_string_list_item(item, accepted_remotes) { | |
| struct promisor_info *info = item->util; | |
| const char *remote_name = promisor_info_local_name(info); | |
| struct promisor_remote *r = repo_promisor_remote_find(repo, remote_name); | |
| if (r) { | |
| r->accepted = 1; | |
| if (info->filter) { | |
| free(r->advertised_filter); | |
| r->advertised_filter = xstrdup(info->filter); | |
| } | |
| } | |
| } | |
| } | |
| void promisor_remote_reply(const char *info, char **accepted_out) | |
| { | |
| struct string_list accepted_remotes = STRING_LIST_INIT_NODUP; | |
| filter_promisor_remote(the_repository, &accepted_remotes, info); | |
| if (accepted_out) { | |
| if (accepted_remotes.nr) { | |
| struct strbuf reply = STRBUF_INIT; | |
| struct string_list_item *item; | |
| for_each_string_list_item(item, &accepted_remotes) { | |
| if (reply.len) | |
| strbuf_addch(&reply, ';'); | |
| strbuf_addstr_urlencode(&reply, item->string, allow_unsanitized); | |
| } | |
| *accepted_out = strbuf_detach(&reply, NULL); | |
| } else { | |
| *accepted_out = NULL; | |
| } | |
| } | |
| promisor_info_list_clear(&accepted_remotes); | |
| } | |
| void mark_promisor_remotes_as_accepted(struct repository *r, const char *remotes) | |
| { | |
| struct string_list accepted_remotes = STRING_LIST_INIT_DUP; | |
| struct string_list_item *item; | |
| string_list_split(&accepted_remotes, remotes, ";", -1); | |
| for_each_string_list_item(item, &accepted_remotes) { | |
| char *decoded_remote = url_percent_decode(item->string); | |
| struct promisor_remote *p = repo_promisor_remote_find(r, decoded_remote); | |
| if (p) | |
| p->accepted = 1; | |
| else | |
| warning(_("accepted promisor remote '%s' not found"), | |
| decoded_remote); | |
| free(decoded_remote); | |
| } | |
| string_list_clear(&accepted_remotes, 0); | |
| } | |
| char *promisor_remote_construct_filter(struct repository *repo) | |
| { | |
| struct promisor_remote *r; | |
| struct list_objects_filter_options filter_options = LIST_OBJECTS_FILTER_INIT; | |
| struct strbuf err = STRBUF_INIT; | |
| char *result = NULL; | |
| promisor_remote_init(repo); | |
| for (r = repo->promisor_remote_config->promisors; r; r = r->next) { | |
| if (r->accepted && r->advertised_filter) | |
| if (gently_parse_list_objects_filter(&filter_options, | |
| r->advertised_filter, | |
| &err)) { | |
| warning(_("promisor remote '%s' advertised invalid filter '%s': %s"), | |
| r->name, r->advertised_filter, err.buf); | |
| strbuf_reset(&err); | |
| continue; | |
| } | |
| } | |
| if (filter_options.choice) | |
| result = xstrdup(expand_list_objects_filter_spec(&filter_options)); | |
| list_objects_filter_release(&filter_options); | |
| strbuf_release(&err); | |
| return result; | |
| } |