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:
parent
ad9c217f49
commit
93097f0018
|
@ -226,7 +226,8 @@ private static boolean isReceivePackSideBand(HttpServletRequest req) {
|
||||||
// So, cheat and read the first line.
|
// So, cheat and read the first line.
|
||||||
String line = new PacketLineIn(req.getInputStream()).readString();
|
String line = new PacketLineIn(req.getInputStream()).readString();
|
||||||
FirstCommand parsed = FirstCommand.fromLine(line);
|
FirstCommand parsed = FirstCommand.fromLine(line);
|
||||||
return parsed.getCapabilities().contains(CAPABILITY_SIDE_BAND_64K);
|
return parsed.getCapabilities()
|
||||||
|
.containsKey(CAPABILITY_SIDE_BAND_64K);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
// Probably the connection is closed and a subsequent write will fail, but
|
// Probably the connection is closed and a subsequent write will fail, but
|
||||||
// try it just in case.
|
// try it just in case.
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,12 +9,10 @@
|
||||||
*/
|
*/
|
||||||
package org.eclipse.jgit.internal.transport.parser;
|
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;
|
import org.eclipse.jgit.annotations.NonNull;
|
||||||
|
|
||||||
|
@ -34,7 +32,7 @@
|
||||||
*/
|
*/
|
||||||
public final class FirstCommand {
|
public final class FirstCommand {
|
||||||
private final String line;
|
private final String line;
|
||||||
private final Set<String> capabilities;
|
private final Map<String, String> capabilities;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse the first line of a receive-pack request.
|
* Parse the first line of a receive-pack request.
|
||||||
|
@ -47,16 +45,26 @@ public final class FirstCommand {
|
||||||
public static FirstCommand fromLine(String line) {
|
public static FirstCommand fromLine(String line) {
|
||||||
int nul = line.indexOf('\0');
|
int nul = line.indexOf('\0');
|
||||||
if (nul < 0) {
|
if (nul < 0) {
|
||||||
return new FirstCommand(line, emptySet());
|
return new FirstCommand(line,
|
||||||
|
Collections.<String, String> emptyMap());
|
||||||
}
|
}
|
||||||
Set<String> opts =
|
String[] splitCapablities = line.substring(nul + 1).split(" "); //$NON-NLS-1$
|
||||||
asList(line.substring(nul + 1).split(" ")) //$NON-NLS-1$
|
Map<String, String> options = new HashMap<>();
|
||||||
.stream()
|
|
||||||
.collect(toSet());
|
for (String c : splitCapablities) {
|
||||||
return new FirstCommand(line.substring(0, nul), unmodifiableSet(opts));
|
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.line = line;
|
||||||
this.capabilities = capabilities;
|
this.capabilities = capabilities;
|
||||||
}
|
}
|
||||||
|
@ -67,9 +75,9 @@ public String getLine() {
|
||||||
return line;
|
return line;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return capabilities parsed from the line, as an immutable set. */
|
/** @return capabilities parsed from the line, as an immutable map. */
|
||||||
@NonNull
|
@NonNull
|
||||||
public Set<String> getCapabilities() {
|
public Map<String, String> getCapabilities() {
|
||||||
return capabilities;
|
return capabilities;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -247,6 +247,13 @@ public final class GitProtocolConstants {
|
||||||
*/
|
*/
|
||||||
public static final String OPTION_SERVER_OPTION = "server-option"; //$NON-NLS-1$
|
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.
|
* The server supports listing refs using protocol v2.
|
||||||
*
|
*
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_AGENT;
|
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_ERR;
|
||||||
import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_SHALLOW;
|
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_DATA;
|
||||||
import static org.eclipse.jgit.transport.SideBandOutputStream.CH_ERROR;
|
import static org.eclipse.jgit.transport.SideBandOutputStream.CH_ERROR;
|
||||||
import static org.eclipse.jgit.transport.SideBandOutputStream.CH_PROGRESS;
|
import static org.eclipse.jgit.transport.SideBandOutputStream.CH_PROGRESS;
|
||||||
|
@ -35,6 +36,7 @@
|
||||||
import java.text.MessageFormat;
|
import java.text.MessageFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
@ -113,7 +115,15 @@ public String getLine() {
|
||||||
|
|
||||||
/** @return capabilities parsed from the line. */
|
/** @return capabilities parsed from the line. */
|
||||||
public Set<String> getCapabilities() {
|
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;
|
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. */
|
/** Identity to record action as within the reflog. */
|
||||||
private PersonIdent refLogIdent;
|
private PersonIdent refLogIdent;
|
||||||
|
|
||||||
|
@ -215,7 +228,10 @@ public Set<String> getCapabilities() {
|
||||||
private Set<ObjectId> advertisedHaves;
|
private Set<ObjectId> advertisedHaves;
|
||||||
|
|
||||||
/** Capabilities requested by the client. */
|
/** 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;
|
String userAgent;
|
||||||
|
|
||||||
|
@ -304,6 +320,7 @@ public ReceivePack(Repository into) {
|
||||||
allowNonFastForwards = rc.allowNonFastForwards;
|
allowNonFastForwards = rc.allowNonFastForwards;
|
||||||
allowOfsDelta = rc.allowOfsDelta;
|
allowOfsDelta = rc.allowOfsDelta;
|
||||||
allowPushOptions = rc.allowPushOptions;
|
allowPushOptions = rc.allowPushOptions;
|
||||||
|
allowReceiveClientSID = rc.allowReceiveClientSID;
|
||||||
maxCommandBytes = rc.maxCommandBytes;
|
maxCommandBytes = rc.maxCommandBytes;
|
||||||
maxDiscardBytes = rc.maxDiscardBytes;
|
maxDiscardBytes = rc.maxDiscardBytes;
|
||||||
advertiseRefsHook = AdvertiseRefsHook.DEFAULT;
|
advertiseRefsHook = AdvertiseRefsHook.DEFAULT;
|
||||||
|
@ -327,6 +344,8 @@ private static class ReceiveConfig {
|
||||||
|
|
||||||
final boolean allowPushOptions;
|
final boolean allowPushOptions;
|
||||||
|
|
||||||
|
final boolean allowReceiveClientSID;
|
||||||
|
|
||||||
final long maxCommandBytes;
|
final long maxCommandBytes;
|
||||||
|
|
||||||
final long maxDiscardBytes;
|
final long maxDiscardBytes;
|
||||||
|
@ -342,6 +361,10 @@ private static class ReceiveConfig {
|
||||||
true);
|
true);
|
||||||
allowPushOptions = config.getBoolean("receive", "pushoptions", //$NON-NLS-1$ //$NON-NLS-2$
|
allowPushOptions = config.getBoolean("receive", "pushoptions", //$NON-NLS-1$ //$NON-NLS-2$
|
||||||
false);
|
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 = config.getLong("receive", //$NON-NLS-1$
|
||||||
"maxCommandBytes", //$NON-NLS-1$
|
"maxCommandBytes", //$NON-NLS-1$
|
||||||
3 << 20);
|
3 << 20);
|
||||||
|
@ -886,7 +909,7 @@ public void setMaxPackSizeLimit(long limit) {
|
||||||
*/
|
*/
|
||||||
public boolean isSideBand() throws RequestNotYetReadException {
|
public boolean isSideBand() throws RequestNotYetReadException {
|
||||||
checkRequestWasRead();
|
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
|
* @since 4.0
|
||||||
*/
|
*/
|
||||||
public String getPeerUserAgent() {
|
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 = new PacketLineOut(rawOut);
|
||||||
pckOut.setFlushOnEnd(false);
|
pckOut.setFlushOnEnd(false);
|
||||||
|
|
||||||
enabledCapabilities = new HashSet<>();
|
enabledCapabilities = new HashMap<>();
|
||||||
commands = new ArrayList<>();
|
commands = new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1267,25 +1294,33 @@ public void sendAdvertisedRefs(RefAdvertiser adv)
|
||||||
adv.advertiseCapability(CAPABILITY_SIDE_BAND_64K);
|
adv.advertiseCapability(CAPABILITY_SIDE_BAND_64K);
|
||||||
adv.advertiseCapability(CAPABILITY_DELETE_REFS);
|
adv.advertiseCapability(CAPABILITY_DELETE_REFS);
|
||||||
adv.advertiseCapability(CAPABILITY_REPORT_STATUS);
|
adv.advertiseCapability(CAPABILITY_REPORT_STATUS);
|
||||||
if (allowQuiet)
|
if (allowReceiveClientSID) {
|
||||||
|
adv.advertiseCapability(OPTION_SESSION_ID);
|
||||||
|
}
|
||||||
|
if (allowQuiet) {
|
||||||
adv.advertiseCapability(CAPABILITY_QUIET);
|
adv.advertiseCapability(CAPABILITY_QUIET);
|
||||||
|
}
|
||||||
String nonce = getPushCertificateParser().getAdvertiseNonce();
|
String nonce = getPushCertificateParser().getAdvertiseNonce();
|
||||||
if (nonce != null) {
|
if (nonce != null) {
|
||||||
adv.advertiseCapability(nonce);
|
adv.advertiseCapability(nonce);
|
||||||
}
|
}
|
||||||
if (db.getRefDatabase().performsAtomicTransactions())
|
if (db.getRefDatabase().performsAtomicTransactions()) {
|
||||||
adv.advertiseCapability(CAPABILITY_ATOMIC);
|
adv.advertiseCapability(CAPABILITY_ATOMIC);
|
||||||
if (allowOfsDelta)
|
}
|
||||||
|
if (allowOfsDelta) {
|
||||||
adv.advertiseCapability(CAPABILITY_OFS_DELTA);
|
adv.advertiseCapability(CAPABILITY_OFS_DELTA);
|
||||||
|
}
|
||||||
if (allowPushOptions) {
|
if (allowPushOptions) {
|
||||||
adv.advertiseCapability(CAPABILITY_PUSH_OPTIONS);
|
adv.advertiseCapability(CAPABILITY_PUSH_OPTIONS);
|
||||||
}
|
}
|
||||||
adv.advertiseCapability(OPTION_AGENT, UserAgent.get());
|
adv.advertiseCapability(OPTION_AGENT, UserAgent.get());
|
||||||
adv.send(getAdvertisedOrDefaultRefs().values());
|
adv.send(getAdvertisedOrDefaultRefs().values());
|
||||||
for (ObjectId obj : advertisedHaves)
|
for (ObjectId obj : advertisedHaves) {
|
||||||
adv.advertiseHave(obj);
|
adv.advertiseHave(obj);
|
||||||
if (adv.isEmpty())
|
}
|
||||||
|
if (adv.isEmpty()) {
|
||||||
adv.advertiseId(ObjectId.zeroId(), "capabilities^{}"); //$NON-NLS-1$
|
adv.advertiseId(ObjectId.zeroId(), "capabilities^{}"); //$NON-NLS-1$
|
||||||
|
}
|
||||||
adv.end();
|
adv.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1437,6 +1472,9 @@ private void enableCapabilities() {
|
||||||
usePushOptions = isCapabilityEnabled(CAPABILITY_PUSH_OPTIONS);
|
usePushOptions = isCapabilityEnabled(CAPABILITY_PUSH_OPTIONS);
|
||||||
sideBand = isCapabilityEnabled(CAPABILITY_SIDE_BAND_64K);
|
sideBand = isCapabilityEnabled(CAPABILITY_SIDE_BAND_64K);
|
||||||
quiet = allowQuiet && isCapabilityEnabled(CAPABILITY_QUIET);
|
quiet = allowQuiet && isCapabilityEnabled(CAPABILITY_QUIET);
|
||||||
|
|
||||||
|
clientSID = enabledCapabilities.get(OPTION_SESSION_ID);
|
||||||
|
|
||||||
if (sideBand) {
|
if (sideBand) {
|
||||||
OutputStream out = rawOut;
|
OutputStream out = rawOut;
|
||||||
|
|
||||||
|
@ -1457,7 +1495,7 @@ private void enableCapabilities() {
|
||||||
* @return true if the peer requested the capability to be enabled.
|
* @return true if the peer requested the capability to be enabled.
|
||||||
*/
|
*/
|
||||||
private boolean isCapabilityEnabled(String name) {
|
private boolean isCapabilityEnabled(String name) {
|
||||||
return enabledCapabilities.contains(name);
|
return enabledCapabilities.containsKey(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkRequestWasRead() {
|
private void checkRequestWasRead() {
|
||||||
|
@ -2117,6 +2155,14 @@ public void setEchoCommandFailures(boolean echo) {
|
||||||
// No-op.
|
// No-op.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The client session-id.
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
public String getClientSID() {
|
||||||
|
return clientSID;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute the receive task on the socket.
|
* Execute the receive task on the socket.
|
||||||
*
|
*
|
||||||
|
|
|
@ -91,6 +91,15 @@ public static void set(String agent) {
|
||||||
userAgent = StringUtils.isEmptyOrNull(agent) ? null : clean(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) {
|
static String getAgent(Set<String> options, String transportAgent) {
|
||||||
if (options == null || options.isEmpty()) {
|
if (options == null || options.isEmpty()) {
|
||||||
return transportAgent;
|
return transportAgent;
|
||||||
|
@ -105,6 +114,14 @@ static String getAgent(Set<String> options, String transportAgent) {
|
||||||
return 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) {
|
static boolean hasAgent(Set<String> options) {
|
||||||
return getAgent(options, null) != null;
|
return getAgent(options, null) != null;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue