From 93097f001854af47db782462e78d67e7171043a4 Mon Sep 17 00:00:00 2001 From: Josh Brown Date: Tue, 11 Oct 2022 14:56:09 -0700 Subject: [PATCH] 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 --- .../jgit/http/server/GitSmartHttpTools.java | 3 +- .../transport/parser/FirstCommandTest.java | 38 +++++++++++ .../transport/parser/FirstCommand.java | 38 +++++++---- .../jgit/transport/GitProtocolConstants.java | 7 ++ .../eclipse/jgit/transport/ReceivePack.java | 68 ++++++++++++++++--- .../org/eclipse/jgit/transport/UserAgent.java | 17 +++++ 6 files changed, 144 insertions(+), 27 deletions(-) create mode 100644 org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/transport/parser/FirstCommandTest.java diff --git a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/GitSmartHttpTools.java b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/GitSmartHttpTools.java index f1155dcf5..078b22a70 100644 --- a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/GitSmartHttpTools.java +++ b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/GitSmartHttpTools.java @@ -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. diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/transport/parser/FirstCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/transport/parser/FirstCommandTest.java new file mode 100644 index 000000000..29819a4c3 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/transport/parser/FirstCommandTest.java @@ -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 options = fc.getCapabilities(); + + assertEquals("the-clients-SID", options.get("session-id")); + assertEquals(command, fc.getLine()); + assertTrue(options.containsKey("unknownCap")); + assertEquals(6, options.size()); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/parser/FirstCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/parser/FirstCommand.java index 3f9008005..c75cf5d61 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/parser/FirstCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/parser/FirstCommand.java @@ -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 capabilities; + private final Map 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. emptyMap()); } - Set 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 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. unmodifiableMap(options)); } - private FirstCommand(String line, Set capabilities) { + private FirstCommand(String line, Map 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 getCapabilities() { + public Map getCapabilities() { return capabilities; } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/GitProtocolConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/GitProtocolConstants.java index be14e92d0..7e5179d71 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/GitProtocolConstants.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/GitProtocolConstants.java @@ -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. * diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java index b70eedca6..fe01ecc1f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java @@ -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 getCapabilities() { - return command.getCapabilities(); + Set reconstructedCapabilites = new HashSet<>(); + for (Map.Entry 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 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 getCapabilities() { private Set advertisedHaves; /** Capabilities requested by the client. */ - private Set enabledCapabilities; + private Map 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. * diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UserAgent.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UserAgent.java index 604eb3a66..df98d0cfd 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UserAgent.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UserAgent.java @@ -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 = shape are now parsed + * alongside other capabilities in the ReceivePack flow. + */ + @Deprecated static String getAgent(Set options, String transportAgent) { if (options == null || options.isEmpty()) { return transportAgent; @@ -105,6 +114,14 @@ static String getAgent(Set options, String transportAgent) { return transportAgent; } + /** + * + * @param options + * @return True if the transport agent is set. False otherwise. + * @deprecated Capabilities with = shape are now parsed + * alongside other capabilities in the ReceivePack flow. + */ + @Deprecated static boolean hasAgent(Set options) { return getAgent(options, null) != null; }