From 4a984e20332a765a81cadeaa4875b228ebf290fb Mon Sep 17 00:00:00 2001 From: Shawn Pearce Date: Thu, 30 Apr 2015 12:43:46 -0700 Subject: [PATCH] Support agent= capability in wire protocol Since git-core ff5effd (v1.7.12.1) the native wire protocol transmits the server and client implementation and version strings using capability "agent=git/1.7.12.1" or similar. Support this in JGit and hang the implementation data off UploadPack and ReceivePack. On HTTP transports default to the User-Agent HTTP header until the client overrides this with the optional capability string in the first line. Extract the user agent string into a UserAgent class under transport where it can be specified to a different value if the application's build process has broken the Implementation-Version header in the JGit package. Change-Id: Icfc6524d84a787386d1786310b421b2f92ae9e65 --- .../jgit/http/server/ReceivePackServlet.java | 4 + .../jgit/http/server/UploadPackServlet.java | 4 + .../jgit/transport/BaseConnection.java | 24 +++ .../jgit/transport/BasePackConnection.java | 14 ++ .../transport/BasePackFetchConnection.java | 1 + .../transport/BasePackPushConnection.java | 1 + .../jgit/transport/BaseReceivePack.java | 22 +++ .../eclipse/jgit/transport/Connection.java | 9 ++ .../eclipse/jgit/transport/FetchProcess.java | 1 + .../jgit/transport/GitProtocolConstants.java | 7 + .../transport/InternalHttpServerGlue.java | 80 ++++++++++ .../jgit/transport/OperationResult.java | 13 ++ .../eclipse/jgit/transport/PushProcess.java | 1 + .../eclipse/jgit/transport/RefAdvertiser.java | 18 ++- .../eclipse/jgit/transport/TransportHttp.java | 40 +++-- .../eclipse/jgit/transport/UploadPack.java | 27 +++- .../org/eclipse/jgit/transport/UserAgent.java | 147 ++++++++++++++++++ .../org/eclipse/jgit/util/HttpSupport.java | 6 + 18 files changed, 392 insertions(+), 27 deletions(-) create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/transport/InternalHttpServerGlue.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/transport/UserAgent.java diff --git a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/ReceivePackServlet.java b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/ReceivePackServlet.java index d1c258047..a8e312d3f 100644 --- a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/ReceivePackServlet.java +++ b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/ReceivePackServlet.java @@ -76,6 +76,7 @@ import javax.servlet.http.HttpServletResponse; import org.eclipse.jgit.errors.UnpackException; import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.InternalHttpServerGlue; import org.eclipse.jgit.transport.ReceivePack; import org.eclipse.jgit.transport.RefAdvertiser.PacketLineOutRefAdvertiser; import org.eclipse.jgit.transport.resolver.ReceivePackFactory; @@ -100,6 +101,9 @@ class ReceivePackServlet extends HttpServlet { throws IOException, ServiceNotEnabledException, ServiceNotAuthorizedException { ReceivePack rp = receivePackFactory.create(req, db); + InternalHttpServerGlue.setPeerUserAgent( + rp, + req.getHeader(HDR_USER_AGENT)); req.setAttribute(ATTRIBUTE_HANDLER, rp); } diff --git a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/UploadPackServlet.java b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/UploadPackServlet.java index c5272b55e..7aefcbd80 100644 --- a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/UploadPackServlet.java +++ b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/UploadPackServlet.java @@ -74,6 +74,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.InternalHttpServerGlue; import org.eclipse.jgit.transport.RefAdvertiser.PacketLineOutRefAdvertiser; import org.eclipse.jgit.transport.UploadPack; import org.eclipse.jgit.transport.UploadPackInternalServerErrorException; @@ -100,6 +101,9 @@ class UploadPackServlet extends HttpServlet { throws IOException, ServiceNotEnabledException, ServiceNotAuthorizedException { UploadPack up = uploadPackFactory.create(req, db); + InternalHttpServerGlue.setPeerUserAgent( + up, + req.getHeader(HDR_USER_AGENT)); req.setAttribute(ATTRIBUTE_HANDLER, up); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseConnection.java index 1072d58ad..59ff1bd99 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseConnection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseConnection.java @@ -65,6 +65,8 @@ import org.eclipse.jgit.lib.Ref; public abstract class BaseConnection implements Connection { private Map advertisedRefs = Collections.emptyMap(); + private String peerUserAgent; + private boolean startedOperation; private Writer messageWriter; @@ -85,6 +87,28 @@ public abstract class BaseConnection implements Connection { return messageWriter != null ? messageWriter.toString() : ""; //$NON-NLS-1$ } + /** + * User agent advertised by the remote server. + * + * @return agent (version of Git) running on the remote server. Null if the + * server does not advertise this version. + * @since 4.0 + */ + public String getPeerUserAgent() { + return peerUserAgent; + } + + /** + * Remember the remote peer's agent. + * + * @param agent + * remote peer agent string. + * @since 4.0 + */ + protected void setPeerUserAgent(String agent) { + peerUserAgent = agent; + } + public abstract void close(); /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java index 8f825ea14..7f9cec734 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java @@ -46,6 +46,8 @@ package org.eclipse.jgit.transport; +import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_AGENT; + import java.io.EOFException; import java.io.IOException; import java.io.InputStream; @@ -275,6 +277,18 @@ abstract class BasePackConnection extends BaseConnection { return true; } + protected void addUserAgentCapability(StringBuilder b) { + String a = UserAgent.get(); + if (a != null && UserAgent.hasAgent(remoteCapablities)) { + b.append(' ').append(OPTION_AGENT).append('=').append(a); + } + } + + @Override + public String getPeerUserAgent() { + return UserAgent.getAgent(remoteCapablities, super.getPeerUserAgent()); + } + private PackProtocolException duplicateAdvertisement(final String name) { return new PackProtocolException(uri, MessageFormat.format(JGitText.get().duplicateAdvertisementsOf, name)); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java index 4036c0028..a6fc63359 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java @@ -521,6 +521,7 @@ public abstract class BasePackFetchConnection extends BasePackConnection OPTION_MULTI_ACK_DETAILED)); } + addUserAgentCapability(line); return line.toString(); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java index 863934da1..1e5b8e8ad 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java @@ -268,6 +268,7 @@ public abstract class BasePackPushConnection extends BasePackConnection implemen outputStream); pckIn = new PacketLineIn(in); } + addUserAgentCapability(line); if (line.length() > 0) line.setCharAt(0, '\0'); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java index cf1d92e8e..cf6b2fd3d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java @@ -48,6 +48,7 @@ import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_DELETE_ import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_OFS_DELTA; import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_REPORT_STATUS; import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_SIDE_BAND_64K; +import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_AGENT; import static org.eclipse.jgit.transport.SideBandOutputStream.CH_DATA; import static org.eclipse.jgit.transport.SideBandOutputStream.CH_PROGRESS; import static org.eclipse.jgit.transport.SideBandOutputStream.MAX_BUF; @@ -224,6 +225,7 @@ public abstract class BaseReceivePack { /** Capabilities requested by the client. */ private Set enabledCapabilities; + String userAgent; private Set clientShallowCommits; private List commands; @@ -738,6 +740,25 @@ public abstract class BaseReceivePack { return enabledCapabilities.contains(CAPABILITY_SIDE_BAND_64K); } + /** + * Get the user agent of the client. + *

+ * If the client is new enough to use {@code agent=} capability that value + * will be returned. Older HTTP clients may also supply their version using + * the HTTP {@code User-Agent} header. The capability overrides the HTTP + * header if both are available. + *

+ * When an HTTP request has been received this method returns the HTTP + * {@code User-Agent} header value until capabilities have been parsed. + * + * @return user agent supplied by the client. Available only if the client + * is new enough to advertise its user agent. + * @since 4.0 + */ + public String getPeerUserAgent() { + return UserAgent.getAgent(enabledCapabilities, userAgent); + } + /** @return all of the command received by the current request. */ public List getAllCommands() { return Collections.unmodifiableList(commands); @@ -955,6 +976,7 @@ public abstract class BaseReceivePack { adv.advertiseCapability(CAPABILITY_ATOMIC); if (allowOfsDelta) adv.advertiseCapability(CAPABILITY_OFS_DELTA); + adv.advertiseCapability(OPTION_AGENT, UserAgent.get()); adv.send(getAdvertisedOrDefaultRefs()); for (ObjectId obj : advertisedHaves) adv.advertiseHave(obj); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Connection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Connection.java index e386c26c1..0ff9fcea7 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Connection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Connection.java @@ -127,4 +127,13 @@ public interface Connection { * remote produced no additional messages. */ public String getMessages(); + + /** + * User agent advertised by the remote server. + * + * @return agent (version of Git) running on the remote server. Null if the + * server does not advertise this version. + * @since 4.0 + */ + public String getPeerUserAgent(); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java index d2902a35b..9aae1c37a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java @@ -136,6 +136,7 @@ class FetchProcess { conn = transport.openFetch(); try { result.setAdvertisedRefs(transport.getURI(), conn.getRefsMap()); + result.peerUserAgent = conn.getPeerUserAgent(); final Set matched = new HashSet(); for (final RefSpec spec : toFetch) { if (spec.getSource() == null) 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 27052db67..8d9d2b718 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/GitProtocolConstants.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/GitProtocolConstants.java @@ -186,6 +186,13 @@ public class GitProtocolConstants { */ public static final String CAPABILITY_PUSH_CERT = "push-cert"; //$NON-NLS-1$ + /** + * Implementation name and version of the client or server. + * + * @since 4.0 + */ + public static final String OPTION_AGENT = "agent"; //$NON-NLS-1$ + static enum MultiAck { OFF, CONTINUE, DETAILED; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/InternalHttpServerGlue.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/InternalHttpServerGlue.java new file mode 100644 index 000000000..fe7aaf769 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/InternalHttpServerGlue.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2015, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.transport; + +/** + * Internal API to to assist {@code org.eclipse.jgit.http.server}. + *

+ * Do not call. + * + * @since 4.0 + */ +public class InternalHttpServerGlue { + /** + * Apply a default user agent for a request. + * + * @param up + * current UploadPack instance. + * @param agent + * user agent string from the HTTP headers. + */ + public static void setPeerUserAgent(UploadPack up, String agent) { + up.userAgent = agent; + } + + /** + * Apply a default user agent for a request. + * + * @param rp + * current ReceivePack instance. + * @param agent + * user agent string from the HTTP headers. + */ + public static void setPeerUserAgent(ReceivePack rp, String agent) { + rp.userAgent = agent; + } + + private InternalHttpServerGlue() { + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/OperationResult.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/OperationResult.java index b4a48b016..ad51f3e70 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/OperationResult.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/OperationResult.java @@ -68,6 +68,8 @@ public abstract class OperationResult { StringBuilder messageBuffer; + String peerUserAgent; + /** * Get the URI this result came from. *

@@ -165,4 +167,15 @@ public abstract class OperationResult { messageBuffer.append('\n'); } } + + /** + * Get the user agent advertised by the peer server, if available. + * + * @return advertised user agent, e.g. {@code "JGit/4.0"}. Null if the peer + * did not advertise version information. + * @since 4.0 + */ + public String getPeerUserAgent() { + return peerUserAgent; + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java index 53fba5557..00f84f70e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java @@ -155,6 +155,7 @@ class PushProcess { try { res.setAdvertisedRefs(transport.getURI(), connection .getRefsMap()); + res.peerUserAgent = connection.getPeerUserAgent(); res.setRemoteUpdates(toPush); monitor.endTask(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefAdvertiser.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefAdvertiser.java index 76547a628..f72a4b2b3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefAdvertiser.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefAdvertiser.java @@ -147,6 +147,21 @@ public abstract class RefAdvertiser { capablities.add(name); } + /** + * Add one protocol capability with a value ({@code "name=value"}). + * + * @param name + * name of the capability. + * @param value + * value. If null the capability will not be added. + * @since 4.0 + */ + public void advertiseCapability(String name, String value) { + if (value != null) { + capablities.add(name + '=' + value); + } + } + /** * Add a symbolic ref to capabilities. *

@@ -164,8 +179,7 @@ public abstract class RefAdvertiser { * @since 3.6 */ public void addSymref(String from, String to) { - String symref = String.format("%s=%s:%s", OPTION_SYMREF, from, to); //$NON-NLS-1$ - advertiseCapability(symref); + advertiseCapability(OPTION_SYMREF, from + ':' + to); } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java index 82d1737b9..b23771e95 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java @@ -134,8 +134,6 @@ public class TransportHttp extends HttpTransport implements WalkTransport, private static final String SVC_RECEIVE_PACK = "git-receive-pack"; //$NON-NLS-1$ - private static final String userAgent = computeUserAgent(); - static final TransportProtocol PROTO_HTTP = new TransportProtocol() { private final String[] schemeNames = { "http", "https" }; //$NON-NLS-1$ //$NON-NLS-2$ @@ -204,17 +202,6 @@ public class TransportHttp extends HttpTransport implements WalkTransport, } }; - private static String computeUserAgent() { - String version; - final Package pkg = TransportHttp.class.getPackage(); - if (pkg != null && pkg.getImplementationVersion() != null) { - version = pkg.getImplementationVersion(); - } else { - version = "unknown"; //$NON-NLS-1$ - } - return "JGit/" + version; //$NON-NLS-1$ - } - private static final Config.SectionParser HTTP_KEY = new SectionParser() { public HttpConfig parse(final Config cfg) { return new HttpConfig(cfg); @@ -309,16 +296,17 @@ public class TransportHttp extends HttpTransport implements WalkTransport, final HttpConnection c = connect(service); final InputStream in = openInputStream(c); try { + BaseConnection f; if (isSmartHttp(c, service)) { readSmartHeaders(in, service); - return new SmartHttpFetchConnection(in); - + f = new SmartHttpFetchConnection(in); } else { // Assume this server doesn't support smart HTTP fetch // and fall back on dumb object walking. - // - return newDumbConnection(in); + f = newDumbConnection(in); } + f.setPeerUserAgent(c.getHeaderField(HttpSupport.HDR_SERVER)); + return (FetchConnection) f; } finally { in.close(); } @@ -331,7 +319,7 @@ public class TransportHttp extends HttpTransport implements WalkTransport, } } - private FetchConnection newDumbConnection(InputStream in) + private WalkFetchConnection newDumbConnection(InputStream in) throws IOException, PackProtocolException { HttpObjectDB d = new HttpObjectDB(objectsUrl); BufferedReader br = toBufferedReader(in); @@ -400,9 +388,7 @@ public class TransportHttp extends HttpTransport implements WalkTransport, final InputStream in = openInputStream(c); try { if (isSmartHttp(c, service)) { - readSmartHeaders(in, service); - return new SmartHttpPushConnection(in); - + return smartPush(service, c, in); } else if (!useSmartHttp) { final String msg = JGitText.get().smartHTTPPushDisabled; throw new NotSupportedException(msg); @@ -423,6 +409,14 @@ public class TransportHttp extends HttpTransport implements WalkTransport, } } + private PushConnection smartPush(String service, HttpConnection c, + InputStream in) throws IOException, TransportException { + readSmartHeaders(in, service); + SmartHttpPushConnection p = new SmartHttpPushConnection(in); + p.setPeerUserAgent(c.getHeaderField(HttpSupport.HDR_SERVER)); + return p; + } + @Override public void close() { // No explicit connections are maintained. @@ -551,7 +545,9 @@ public class TransportHttp extends HttpTransport implements WalkTransport, conn.setUseCaches(false); conn.setRequestProperty(HDR_ACCEPT_ENCODING, ENCODING_GZIP); conn.setRequestProperty(HDR_PRAGMA, "no-cache"); //$NON-NLS-1$ - conn.setRequestProperty(HDR_USER_AGENT, userAgent); + if (UserAgent.get() != null) { + conn.setRequestProperty(HDR_USER_AGENT, UserAgent.get()); + } int timeOut = getTimeout(); if (timeOut != -1) { int effTimeOut = timeOut * 1000; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java index 51718c027..3afdb6114 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java @@ -44,6 +44,7 @@ package org.eclipse.jgit.transport; import static org.eclipse.jgit.lib.RefDatabase.ALL; +import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_AGENT; import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_ALLOW_TIP_SHA1_IN_WANT; import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_INCLUDE_TAG; import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_MULTI_ACK; @@ -253,6 +254,7 @@ public class UploadPack { /** Capabilities requested by the client. */ private Set options; + String userAgent; /** Raw ObjectIds the client has asked for, before validating them. */ private final Set wantIds = new HashSet(); @@ -806,6 +808,7 @@ public class UploadPack { || policy == RequestPolicy.REACHABLE_COMMIT_TIP || policy == null) adv.advertiseCapability(OPTION_ALLOW_TIP_SHA1_IN_WANT); + adv.advertiseCapability(OPTION_AGENT, UserAgent.get()); adv.setDerefTags(true); Map refs = getAdvertisedOrDefaultRefs(); findSymrefs(adv, refs); @@ -891,12 +894,30 @@ public class UploadPack { * @since 4.0 */ public int getDepth() { - if (options == null) { - throw new IllegalStateException("do not call getDepth() before recvWants()"); - } + if (options == null) + throw new RequestNotYetReadException(); return depth; } + /** + * Get the user agent of the client. + *

+ * If the client is new enough to use {@code agent=} capability that value + * will be returned. Older HTTP clients may also supply their version using + * the HTTP {@code User-Agent} header. The capability overrides the HTTP + * header if both are available. + *

+ * When an HTTP request has been received this method returns the HTTP + * {@code User-Agent} header value until capabilities have been parsed. + * + * @return user agent supplied by the client. Available only if the client + * is new enough to advertise its user agent. + * @since 4.0 + */ + public String getPeerUserAgent() { + return UserAgent.getAgent(options, userAgent); + } + private boolean negotiate() throws IOException { okToGiveUp = Boolean.FALSE; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UserAgent.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UserAgent.java new file mode 100644 index 000000000..eadb92dd5 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UserAgent.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2015, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.transport; + +import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_AGENT; + +import java.util.Set; + +import org.eclipse.jgit.util.StringUtils; + +/** + * User agent to be reported by this JGit client and server on the network. + *

+ * On HTTP transports this user agent string is always supplied by the JGit + * client in the {@code User-Agent} HTTP header. + *

+ * On native transports this user agent string is always sent when JGit is a + * server. When JGit is a client the user agent string will be supplied to the + * remote server only if the remote server advertises its own agent identity. + * + * @since 4.0 + */ +public class UserAgent { + private static volatile String userAgent = computeUserAgent(); + + private static String computeUserAgent() { + return clean("JGit/" + computeVersion()); //$NON-NLS-1$ + } + + private static String computeVersion() { + Package pkg = UserAgent.class.getPackage(); + if (pkg != null) { + String ver = pkg.getImplementationVersion(); + if (!StringUtils.isEmptyOrNull(ver)) { + return ver; + } + } + return "unknown"; //$NON-NLS-1$ + } + + private static String clean(String s) { + s = s.trim(); + StringBuilder b = new StringBuilder(s.length()); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c <= 32 || c >= 127) { + if (b.length() > 0 && b.charAt(b.length() - 1) == '.') + continue; + c = '.'; + } + b.append(c); + } + return b.length() > 0 ? b.toString() : null; + } + + /** + * Get the user agent string advertised by JGit. + * + * @return a string similar to {@code "JGit/4.0"}; null if the agent has + * been cleared and should not be shared with a peer. + */ + public static String get() { + return userAgent; + } + + /** + * Change the user agent string advertised by JGit. + *

+ * The new string should start with {@code "JGit/"} (for example + * {@code "JGit/4.0"}) to advertise the implementation as JGit based. + *

+ * Spaces and other whitespace should be avoided as these will be + * automatically converted to {@code "."}. + *

+ * User agent strings are restricted to printable ASCII. + * + * @param agent + * new user agent string for this running JGit library. Setting + * to null or empty string will avoid sending any identification + * to the peer. + */ + public static void set(String agent) { + userAgent = StringUtils.isEmptyOrNull(agent) ? null : clean(agent); + } + + static String getAgent(Set options, String transportAgent) { + if (options == null || options.isEmpty()) { + return transportAgent; + } + for (String o : options) { + if (o.startsWith(OPTION_AGENT) + && o.length() > OPTION_AGENT.length() + && o.charAt(OPTION_AGENT.length()) == '=') { + return o.substring(OPTION_AGENT.length() + 1); + } + } + return transportAgent; + } + + static boolean hasAgent(Set options) { + return getAgent(options, null) != null; + } + + private UserAgent() { + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java index 37c9f7b8a..8b4ad0aa2 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java @@ -74,6 +74,12 @@ public class HttpSupport { /** The {@code User-Agent} header. */ public static final String HDR_USER_AGENT = "User-Agent"; //$NON-NLS-1$ + /** + * The {@code Server} header. + * @since 4.0 + */ + public static final String HDR_SERVER = "Server"; //$NON-NLS-1$ + /** The {@code Date} header. */ public static final String HDR_DATE = "Date"; //$NON-NLS-1$