ReceivePack: Receive and parse client session-id.

Before this change JGit did not support the session-id capability
implemented by native Git. This change implements advertising the
capability from the server and parsing the session-id received from
the client during a ReceivePack operation.

Enable the transfer.advertisesid config setting to advertise the
capability from the server. The client may send a session-id capability
in response. If received, the value from this is parsed and available
via the getClientSID method on the ReceivePack object. All capabilities
in the form `capability=value` are now split into key value pairs at the
first `=` character. This change replaces specific handling for the 
agent capability.

This change does not add advertisement or parsing to UploadPack. This
change also does not add the ability to send a session ID from the JGit
client.

https://git-scm.com/docs/protocol-v2/2.33.0#_session_idsession_id

Change-Id: I56fb115e843b11b27e128c4ac427b05d5ec129d0
Signed-off-by: Josh Brown <sjoshbrown@google.com>
This commit is contained in:
Josh Brown 2022-10-11 14:56:09 -07:00 committed by Ivan Frade
parent ad9c217f49
commit 93097f0018
6 changed files with 144 additions and 27 deletions

View File

@ -226,7 +226,8 @@ private static boolean isReceivePackSideBand(HttpServletRequest req) {
// So, cheat and read the first line.
String line = new PacketLineIn(req.getInputStream()).readString();
FirstCommand parsed = FirstCommand.fromLine(line);
return parsed.getCapabilities().contains(CAPABILITY_SIDE_BAND_64K);
return parsed.getCapabilities()
.containsKey(CAPABILITY_SIDE_BAND_64K);
} catch (IOException e) {
// Probably the connection is closed and a subsequent write will fail, but
// try it just in case.

View File

@ -0,0 +1,38 @@
/*
* Copyright (C) 2022, Google LLC. and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.internal.transport.parser;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.util.Map;
import org.junit.Test;
public class FirstCommandTest {
@Test
public void testClientSID() {
String oldStr = "0000000000000000000000000000000000000000";
String newStr = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
String refName = "refs/heads/master";
String command = oldStr + " " + newStr + " " + refName;
String fl = command + "\0"
+ "some capabilities session-id=the-clients-SID and more unknownCap=some-value";
FirstCommand fc = FirstCommand.fromLine(fl);
Map<String, String> options = fc.getCapabilities();
assertEquals("the-clients-SID", options.get("session-id"));
assertEquals(command, fc.getLine());
assertTrue(options.containsKey("unknownCap"));
assertEquals(6, options.size());
}
}

View File

@ -9,12 +9,10 @@
*/
package org.eclipse.jgit.internal.transport.parser;
import static java.util.Arrays.asList;
import static java.util.Collections.emptySet;
import static java.util.Collections.unmodifiableSet;
import static java.util.stream.Collectors.toSet;
import java.util.Set;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jgit.annotations.NonNull;
@ -34,7 +32,7 @@
*/
public final class FirstCommand {
private final String line;
private final Set<String> capabilities;
private final Map<String, String> capabilities;
/**
* Parse the first line of a receive-pack request.
@ -47,16 +45,26 @@ public final class FirstCommand {
public static FirstCommand fromLine(String line) {
int nul = line.indexOf('\0');
if (nul < 0) {
return new FirstCommand(line, emptySet());
return new FirstCommand(line,
Collections.<String, String> emptyMap());
}
Set<String> opts =
asList(line.substring(nul + 1).split(" ")) //$NON-NLS-1$
.stream()
.collect(toSet());
return new FirstCommand(line.substring(0, nul), unmodifiableSet(opts));
String[] splitCapablities = line.substring(nul + 1).split(" "); //$NON-NLS-1$
Map<String, String> options = new HashMap<>();
for (String c : splitCapablities) {
int i = c.indexOf("="); //$NON-NLS-1$
if (i != -1) {
options.put(c.substring(0, i), c.substring(i + 1));
} else {
options.put(c, null);
}
}
return new FirstCommand(line.substring(0, nul),
Collections.<String, String> unmodifiableMap(options));
}
private FirstCommand(String line, Set<String> capabilities) {
private FirstCommand(String line, Map<String, String> capabilities) {
this.line = line;
this.capabilities = capabilities;
}
@ -67,9 +75,9 @@ public String getLine() {
return line;
}
/** @return capabilities parsed from the line, as an immutable set. */
/** @return capabilities parsed from the line, as an immutable map. */
@NonNull
public Set<String> getCapabilities() {
public Map<String, String> getCapabilities() {
return capabilities;
}
}

View File

@ -247,6 +247,13 @@ public final class GitProtocolConstants {
*/
public static final String OPTION_SERVER_OPTION = "server-option"; //$NON-NLS-1$
/**
* Option for passing client session ID to the server.
*
* @since 6.4
*/
public static final String OPTION_SESSION_ID = "session-id"; //$NON-NLS-1$
/**
* The server supports listing refs using protocol v2.
*

View File

@ -22,6 +22,7 @@
import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_AGENT;
import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_ERR;
import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_SHALLOW;
import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_SESSION_ID;
import static org.eclipse.jgit.transport.SideBandOutputStream.CH_DATA;
import static org.eclipse.jgit.transport.SideBandOutputStream.CH_ERROR;
import static org.eclipse.jgit.transport.SideBandOutputStream.CH_PROGRESS;
@ -35,6 +36,7 @@
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@ -113,7 +115,15 @@ public String getLine() {
/** @return capabilities parsed from the line. */
public Set<String> getCapabilities() {
return command.getCapabilities();
Set<String> reconstructedCapabilites = new HashSet<>();
for (Map.Entry<String, String> e : command.getCapabilities()
.entrySet()) {
String cap = e.getValue() == null ? e.getKey()
: e.getKey() + "=" + e.getValue(); //$NON-NLS-1$
reconstructedCapabilites.add(cap);
}
return reconstructedCapabilites;
}
}
@ -166,6 +176,9 @@ public Set<String> getCapabilities() {
private boolean allowQuiet = true;
/** Should the server advertise and accept the session-id capability. */
private boolean allowReceiveClientSID;
/** Identity to record action as within the reflog. */
private PersonIdent refLogIdent;
@ -215,7 +228,10 @@ public Set<String> getCapabilities() {
private Set<ObjectId> advertisedHaves;
/** Capabilities requested by the client. */
private Set<String> enabledCapabilities;
private Map<String, String> enabledCapabilities;
/** Session ID sent from the client. Null if none was received. */
private String clientSID;
String userAgent;
@ -304,6 +320,7 @@ public ReceivePack(Repository into) {
allowNonFastForwards = rc.allowNonFastForwards;
allowOfsDelta = rc.allowOfsDelta;
allowPushOptions = rc.allowPushOptions;
allowReceiveClientSID = rc.allowReceiveClientSID;
maxCommandBytes = rc.maxCommandBytes;
maxDiscardBytes = rc.maxDiscardBytes;
advertiseRefsHook = AdvertiseRefsHook.DEFAULT;
@ -327,6 +344,8 @@ private static class ReceiveConfig {
final boolean allowPushOptions;
final boolean allowReceiveClientSID;
final long maxCommandBytes;
final long maxDiscardBytes;
@ -342,6 +361,10 @@ private static class ReceiveConfig {
true);
allowPushOptions = config.getBoolean("receive", "pushoptions", //$NON-NLS-1$ //$NON-NLS-2$
false);
// TODO: This should not be enabled until the corresponding change to
// upload pack has been implemented.
allowReceiveClientSID = config.getBoolean("transfer", //$NON-NLS-1$
"advertisesid", false); //$NON-NLS-1$
maxCommandBytes = config.getLong("receive", //$NON-NLS-1$
"maxCommandBytes", //$NON-NLS-1$
3 << 20);
@ -886,7 +909,7 @@ public void setMaxPackSizeLimit(long limit) {
*/
public boolean isSideBand() throws RequestNotYetReadException {
checkRequestWasRead();
return enabledCapabilities.contains(CAPABILITY_SIDE_BAND_64K);
return enabledCapabilities.containsKey(CAPABILITY_SIDE_BAND_64K);
}
/**
@ -987,7 +1010,11 @@ private PushCertificateParser getPushCertificateParser() {
* @since 4.0
*/
public String getPeerUserAgent() {
return UserAgent.getAgent(enabledCapabilities, userAgent);
if (enabledCapabilities == null || enabledCapabilities.isEmpty()) {
return userAgent;
}
return enabledCapabilities.getOrDefault(OPTION_AGENT, userAgent);
}
/**
@ -1182,7 +1209,7 @@ protected void init(final InputStream input, final OutputStream output,
pckOut = new PacketLineOut(rawOut);
pckOut.setFlushOnEnd(false);
enabledCapabilities = new HashSet<>();
enabledCapabilities = new HashMap<>();
commands = new ArrayList<>();
}
@ -1267,25 +1294,33 @@ public void sendAdvertisedRefs(RefAdvertiser adv)
adv.advertiseCapability(CAPABILITY_SIDE_BAND_64K);
adv.advertiseCapability(CAPABILITY_DELETE_REFS);
adv.advertiseCapability(CAPABILITY_REPORT_STATUS);
if (allowQuiet)
if (allowReceiveClientSID) {
adv.advertiseCapability(OPTION_SESSION_ID);
}
if (allowQuiet) {
adv.advertiseCapability(CAPABILITY_QUIET);
}
String nonce = getPushCertificateParser().getAdvertiseNonce();
if (nonce != null) {
adv.advertiseCapability(nonce);
}
if (db.getRefDatabase().performsAtomicTransactions())
if (db.getRefDatabase().performsAtomicTransactions()) {
adv.advertiseCapability(CAPABILITY_ATOMIC);
if (allowOfsDelta)
}
if (allowOfsDelta) {
adv.advertiseCapability(CAPABILITY_OFS_DELTA);
}
if (allowPushOptions) {
adv.advertiseCapability(CAPABILITY_PUSH_OPTIONS);
}
adv.advertiseCapability(OPTION_AGENT, UserAgent.get());
adv.send(getAdvertisedOrDefaultRefs().values());
for (ObjectId obj : advertisedHaves)
for (ObjectId obj : advertisedHaves) {
adv.advertiseHave(obj);
if (adv.isEmpty())
}
if (adv.isEmpty()) {
adv.advertiseId(ObjectId.zeroId(), "capabilities^{}"); //$NON-NLS-1$
}
adv.end();
}
@ -1437,6 +1472,9 @@ private void enableCapabilities() {
usePushOptions = isCapabilityEnabled(CAPABILITY_PUSH_OPTIONS);
sideBand = isCapabilityEnabled(CAPABILITY_SIDE_BAND_64K);
quiet = allowQuiet && isCapabilityEnabled(CAPABILITY_QUIET);
clientSID = enabledCapabilities.get(OPTION_SESSION_ID);
if (sideBand) {
OutputStream out = rawOut;
@ -1457,7 +1495,7 @@ private void enableCapabilities() {
* @return true if the peer requested the capability to be enabled.
*/
private boolean isCapabilityEnabled(String name) {
return enabledCapabilities.contains(name);
return enabledCapabilities.containsKey(name);
}
private void checkRequestWasRead() {
@ -2117,6 +2155,14 @@ public void setEchoCommandFailures(boolean echo) {
// No-op.
}
/**
* @return The client session-id.
* @since 6.4
*/
public String getClientSID() {
return clientSID;
}
/**
* Execute the receive task on the socket.
*

View File

@ -91,6 +91,15 @@ public static void set(String agent) {
userAgent = StringUtils.isEmptyOrNull(agent) ? null : clean(agent);
}
/**
*
* @param options
* @param transportAgent
* @return The transport agent.
* @deprecated Capabilities with <key>=<value> shape are now parsed
* alongside other capabilities in the ReceivePack flow.
*/
@Deprecated
static String getAgent(Set<String> options, String transportAgent) {
if (options == null || options.isEmpty()) {
return transportAgent;
@ -105,6 +114,14 @@ static String getAgent(Set<String> options, String transportAgent) {
return transportAgent;
}
/**
*
* @param options
* @return True if the transport agent is set. False otherwise.
* @deprecated Capabilities with <key>=<value> shape are now parsed
* alongside other capabilities in the ReceivePack flow.
*/
@Deprecated
static boolean hasAgent(Set<String> options) {
return getAgent(options, null) != null;
}