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.
|
||||
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.
|
||||
|
|
|
@ -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;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue