Skip to content

Commit 2d2ef70

Browse files
chriscoolgitster
authored andcommitted
promisor-remote: allow a client to check fields
A previous commit allowed a server to pass additional fields through the "promisor-remote" protocol capability after the "name" and "url" fields, specifically the "partialCloneFilter" and "token" fields. Let's make it possible for a client to check if these fields match what it expects before accepting a promisor remote. We allow this by introducing a new "promisor.checkFields" configuration variable. It should contain a comma or space separated list of fields that will be checked. By limiting the protocol to specific well-defined fields, we ensure both server and client have a shared understanding of field semantics and usage. Signed-off-by: Christian Couder <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 7c03353 commit 2d2ef70

File tree

3 files changed

+143
-6
lines changed

3 files changed

+143
-6
lines changed

Documentation/config/promisor.adoc

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,38 @@ promisor.acceptFromServer::
4646
lazily fetchable from this promisor remote from its responses
4747
to "fetch" and "clone" requests from the client. Name and URL
4848
comparisons are case sensitive. See linkgit:gitprotocol-v2[5].
49+
50+
promisor.checkFields::
51+
A comma or space separated list of additional remote related
52+
fields that a client will check before accepting a promisor
53+
remote. Currently, "partialCloneFilter" and "token" are the only
54+
supported field names.
55+
+
56+
If one of these field names (e.g., "token") is being checked for an
57+
advertised promisor remote (e.g., "foo"), three conditions must be met
58+
for the check of this specific field to pass:
59+
+
60+
1. The corresponding local configuration (e.g., `remote.foo.token`)
61+
must be set.
62+
2. The server must advertise the "token" field for remote "foo".
63+
3. The value of the locally configured `remote.foo.token` must exactly
64+
match the value advertised by the server for the "token" field.
65+
+
66+
If any of these conditions are not met for any field name listed in
67+
`promisor.checkFields`, the advertised remote "foo" will be rejected.
68+
+
69+
For the "partialCloneFilter" field, this allows the client to ensure
70+
that the server's filter matches what it expects locally, preventing
71+
inconsistencies in filtering behavior. For the "token" field, this can
72+
be used to verify that authentication credentials match expected
73+
values.
74+
+
75+
The "name" and "url" fields are always checked according to the
76+
`promisor.acceptFromServer` policy, independently of this setting.
77+
+
78+
The fields should be passed by the server through the
79+
"promisor-remote" capability by using the `promisor.sendFields` config
80+
variable. The fields will be checked only if the
81+
`promisor.acceptFromServer` config variable is not set to "None". If
82+
set to "None", this config variable will have no effect. See
83+
linkgit:gitprotocol-v2[5].

promisor-remote.c

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,20 @@ static struct string_list *fields_sent(void)
381381
return &fields_list;
382382
}
383383

384+
static struct string_list *fields_checked(void)
385+
{
386+
static struct string_list fields_list = STRING_LIST_INIT_NODUP;
387+
static int initialized = 0;
388+
389+
if (!initialized) {
390+
fields_list.cmp = strcasecmp;
391+
fields_from_config(&fields_list, "promisor.checkFields");
392+
initialized = 1;
393+
}
394+
395+
return &fields_list;
396+
}
397+
384398
/*
385399
* Linked list for promisor remotes involved in the "promisor-remote"
386400
* protocol capability.
@@ -537,6 +551,55 @@ enum accept_promisor {
537551
ACCEPT_ALL
538552
};
539553

554+
static int match_field_against_config(const char *field, const char *value,
555+
struct promisor_info *config_info)
556+
{
557+
if (config_info->filter && !strcasecmp(field, promisor_field_filter))
558+
return !strcmp(config_info->filter, value);
559+
else if (config_info->token && !strcasecmp(field, promisor_field_token))
560+
return !strcmp(config_info->token, value);
561+
562+
return 0;
563+
}
564+
565+
static int all_fields_match(struct promisor_info *advertised,
566+
struct promisor_info *config_info,
567+
int in_list)
568+
{
569+
struct string_list* fields = fields_checked();
570+
struct string_list_item *item_checked;
571+
572+
for_each_string_list_item(item_checked, fields) {
573+
int match = 0;
574+
const char *field = item_checked->string;
575+
const char *value = NULL;
576+
577+
if (!strcasecmp(field, promisor_field_filter))
578+
value = advertised->filter;
579+
else if (!strcasecmp(field, promisor_field_token))
580+
value = advertised->token;
581+
582+
if (!value)
583+
return 0;
584+
585+
if (in_list) {
586+
for (struct promisor_info *p = config_info; p; p = p->next) {
587+
if (match_field_against_config(field, value, p)) {
588+
match = 1;
589+
break;
590+
}
591+
}
592+
} else {
593+
match = match_field_against_config(field, value, config_info);
594+
}
595+
596+
if (!match)
597+
return 0;
598+
}
599+
600+
return 1;
601+
}
602+
540603
static int should_accept_remote(enum accept_promisor accept,
541604
struct promisor_info *advertised,
542605
struct promisor_info *config_info)
@@ -546,7 +609,7 @@ static int should_accept_remote(enum accept_promisor accept,
546609
const char *remote_url = advertised->url;
547610

548611
if (accept == ACCEPT_ALL)
549-
return 1;
612+
return all_fields_match(advertised, config_info, 1);
550613

551614
p = remote_nick_find(config_info, remote_name);
552615

@@ -555,7 +618,7 @@ static int should_accept_remote(enum accept_promisor accept,
555618
return 0;
556619

557620
if (accept == ACCEPT_KNOWN_NAME)
558-
return 1;
621+
return all_fields_match(advertised, p, 0);
559622

560623
if (accept != ACCEPT_KNOWN_URL)
561624
BUG("Unhandled 'enum accept_promisor' value '%d'", accept);
@@ -570,7 +633,7 @@ static int should_accept_remote(enum accept_promisor accept,
570633
remote_name);
571634

572635
if (!strcmp(p->url, remote_url))
573-
return 1;
636+
return all_fields_match(advertised, p, 0);
574637

575638
warning(_("known remote named '%s' but with URL '%s' instead of '%s'"),
576639
remote_name, p->url, remote_url);
@@ -602,6 +665,10 @@ static struct promisor_info *parse_one_advertised_remote(struct strbuf *remote_i
602665
info->name = value;
603666
else if (!strcmp(elem, "url"))
604667
info->url = value;
668+
else if (!strcasecmp(elem, promisor_field_filter))
669+
info->filter = value;
670+
else if (!strcasecmp(elem, promisor_field_token))
671+
info->token = value;
605672
else
606673
free(value);
607674
}
@@ -644,9 +711,6 @@ static void filter_promisor_remote(struct repository *repo,
644711
if (accept == ACCEPT_NONE)
645712
return;
646713

647-
if (accept != ACCEPT_ALL)
648-
config_info = promisor_config_info_list(repo, NULL);
649-
650714
/* Parse remote info received */
651715

652716
remotes = strbuf_split_str(info, ';', 0);
@@ -661,6 +725,9 @@ static void filter_promisor_remote(struct repository *repo,
661725
if (!advertised)
662726
continue;
663727

728+
if (!config_info)
729+
config_info = promisor_config_info_list(repo, fields_checked());
730+
664731
if (should_accept_remote(accept, advertised, config_info))
665732
strvec_push(accepted, advertised->name);
666733

t/t5710-promisor-remote-capability.sh

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,41 @@ test_expect_success "clone with promisor.sendFields" '
327327
check_missing_objects server 1 "$oid"
328328
'
329329

330+
test_expect_success "clone with promisor.checkFields" '
331+
git -C server config promisor.advertise true &&
332+
test_when_finished "rm -rf client" &&
333+
334+
git -C server remote add otherLop "https://invalid.invalid" &&
335+
git -C server config remote.otherLop.token "fooBar" &&
336+
git -C server config remote.otherLop.stuff "baz" &&
337+
git -C server config remote.otherLop.partialCloneFilter "blob:limit=10k" &&
338+
test_when_finished "git -C server remote remove otherLop" &&
339+
git -C server config promisor.sendFields "partialCloneFilter, token" &&
340+
test_when_finished "git -C server config unset promisor.sendFields" &&
341+
test_when_finished "rm trace" &&
342+
343+
# Clone from server to create a client
344+
GIT_TRACE_PACKET="$(pwd)/trace" GIT_NO_LAZY_FETCH=0 git clone \
345+
-c remote.lop.promisor=true \
346+
-c remote.lop.fetch="+refs/heads/*:refs/remotes/lop/*" \
347+
-c remote.lop.url="file://$(pwd)/lop" \
348+
-c remote.lop.partialCloneFilter="blob:none" \
349+
-c promisor.acceptfromserver=All \
350+
-c promisor.checkFields=partialcloneFilter \
351+
--no-local --filter="blob:limit=5k" server client &&
352+
353+
# Check that fields are properly transmitted
354+
ENCODED_URL=$(echo "file://$(pwd)/lop" | sed -e "s/ /%20/g") &&
355+
PR1="name=lop,url=$ENCODED_URL,partialCloneFilter=blob:none" &&
356+
PR2="name=otherLop,url=https://invalid.invalid,partialCloneFilter=blob:limit=10k,token=fooBar" &&
357+
test_grep "clone< promisor-remote=$PR1;$PR2" trace &&
358+
test_grep "clone> promisor-remote=lop" trace &&
359+
test_grep ! "clone> promisor-remote=lop;otherLop" trace &&
360+
361+
# Check that the largest object is still missing on the server
362+
check_missing_objects server 1 "$oid"
363+
'
364+
330365
test_expect_success "clone with promisor.advertise set to 'true' but don't delete the client" '
331366
git -C server config promisor.advertise true &&
332367

0 commit comments

Comments
 (0)