]> source.dussan.org Git - jgit.git/commitdiff
ReceivePack: Receive and parse client session-id. 23/196323/14
authorJosh Brown <sjoshbrown@google.com>
Tue, 11 Oct 2022 21:56:09 +0000 (14:56 -0700)
committerIvan Frade <ifrade@google.com>
Thu, 27 Oct 2022 20:17:50 +0000 (16:17 -0400)
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>
org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/GitSmartHttpTools.java
org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/transport/parser/FirstCommandTest.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/parser/FirstCommand.java
org.eclipse.jgit/src/org/eclipse/jgit/transport/GitProtocolConstants.java
org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java
org.eclipse.jgit/src/org/eclipse/jgit/transport/UserAgent.java

index f1155dcf57a9e1e8412b5551aaf7b9b8896e6c1c..078b22a700994f8344397421b48d82e1b633052b 100644 (file)
@@ -226,7 +226,8 @@ public class GitSmartHttpTools {
                        // 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 (file)
index 0000000..29819a4
--- /dev/null
@@ -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());
+       }
+}
index 3f900800583cdd5b8ea5a152e614ee7647c18557..c75cf5d6180443d5f6c36b16ff77db9db7a63255 100644 (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 @@ import org.eclipse.jgit.annotations.NonNull;
  */
 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 final class FirstCommand {
                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;
        }
 }
index be14e92d07e7aa8909dc53da2ef097c53e3e2b53..7e5179d71d57123ce15f460892b577d9be392469 100644 (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.
         *
index b70eedca631ee00b1ef70291f0a3e9e33e95f931..fe01ecc1f34fb96ca824f6dfb0e0f3273dd00efc 100644 (file)
@@ -22,6 +22,7 @@ import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_SIDE_BA
 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.io.UncheckedIOException;
 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 class ReceivePack {
 
                /** @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 class ReceivePack {
 
        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 class ReceivePack {
        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 class ReceivePack {
                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 @@ public class ReceivePack {
 
                final boolean allowPushOptions;
 
+               final boolean allowReceiveClientSID;
+
                final long maxCommandBytes;
 
                final long maxDiscardBytes;
@@ -342,6 +361,10 @@ public class ReceivePack {
                                        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 class ReceivePack {
         */
        public boolean isSideBand() throws RequestNotYetReadException {
                checkRequestWasRead();
-               return enabledCapabilities.contains(CAPABILITY_SIDE_BAND_64K);
+               return enabledCapabilities.containsKey(CAPABILITY_SIDE_BAND_64K);
        }
 
        /**
@@ -987,7 +1010,11 @@ public class ReceivePack {
         * @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 @@ public class ReceivePack {
                pckOut = new PacketLineOut(rawOut);
                pckOut.setFlushOnEnd(false);
 
-               enabledCapabilities = new HashSet<>();
+               enabledCapabilities = new HashMap<>();
                commands = new ArrayList<>();
        }
 
@@ -1267,25 +1294,33 @@ public class ReceivePack {
                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 @@ public class ReceivePack {
                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 @@ public class ReceivePack {
         * @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 class ReceivePack {
                // No-op.
        }
 
+       /**
+        * @return The client session-id.
+        * @since 6.4
+        */
+       public String getClientSID() {
+               return clientSID;
+       }
+
        /**
         * Execute the receive task on the socket.
         *
index 604eb3a66cb3e337d866416f8e084f0b28b9db46..df98d0cfd50a4cf93a89a5ca1bfda61aa6dc6904 100644 (file)
@@ -91,6 +91,15 @@ public class UserAgent {
                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 @@ public class UserAgent {
                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;
        }