/* * Copyright (C) 2008, 2010 Google Inc. * Copyright (C) 2008, Marek Zawirski * Copyright (C) 2008, Robin Rosenberg * Copyright (C) 2008, 2020 Shawn O. Pearce 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.transport; import static org.eclipse.jgit.transport.GitProtocolConstants.COMMAND_LS_REFS; import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_AGENT; import static org.eclipse.jgit.transport.GitProtocolConstants.REF_ATTR_PEELED; import static org.eclipse.jgit.transport.GitProtocolConstants.REF_ATTR_SYMREF_TARGET; import static org.eclipse.jgit.transport.GitProtocolConstants.VERSION_1; import static org.eclipse.jgit.transport.GitProtocolConstants.VERSION_2; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.text.MessageFormat; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; import java.util.Set; import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.errors.InvalidObjectIdException; import org.eclipse.jgit.errors.NoRemoteRepositoryException; import org.eclipse.jgit.errors.PackProtocolException; import org.eclipse.jgit.errors.RemoteRepositoryException; import org.eclipse.jgit.errors.TransportException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectIdRef; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.SymbolicRef; import org.eclipse.jgit.util.StringUtils; import org.eclipse.jgit.util.io.InterruptTimer; import org.eclipse.jgit.util.io.TimeoutInputStream; import org.eclipse.jgit.util.io.TimeoutOutputStream; /** * Base helper class for pack-based operations implementations. Provides partial * implementation of pack-protocol - refs advertising and capabilities support, * and some other helper methods. * * @see BasePackFetchConnection * @see BasePackPushConnection */ abstract class BasePackConnection extends BaseConnection { /** The capability prefix for a symlink */ protected static final String CAPABILITY_SYMREF_PREFIX = "symref="; //$NON-NLS-1$ /** The repository this transport fetches into, or pushes out of. */ protected final Repository local; /** Remote repository location. */ protected final URIish uri; /** A transport connected to {@link BasePackConnection#uri}. */ protected final Transport transport; /** Low-level input stream, if a timeout was configured. */ protected TimeoutInputStream timeoutIn; /** Low-level output stream, if a timeout was configured. */ protected TimeoutOutputStream timeoutOut; /** * Timer to manage {@link #timeoutIn} and * {@link BasePackConnection#timeoutOut}. */ private InterruptTimer myTimer; /** Input stream reading from the remote. */ protected InputStream in; /** Output stream sending to the remote. */ protected OutputStream out; /** Packet line decoder around {@link BasePackConnection#in}. */ protected PacketLineIn pckIn; /** Packet line encoder around {@link BasePackConnection#out}. */ protected PacketLineOut pckOut; /** * Send {@link PacketLineOut#end()} before closing * {@link BasePackConnection#out}? */ protected boolean outNeedsEnd; /** True if this is a stateless RPC connection. */ protected boolean statelessRPC; /** Capability tokens advertised by the remote side. */ private final Map remoteCapabilities = new HashMap<>(); /** Extra objects the remote has, but which aren't offered as refs. */ protected final Set additionalHaves = new HashSet<>(); private TransferConfig.ProtocolVersion protocol = TransferConfig.ProtocolVersion.V0; BasePackConnection(PackTransport packTransport) { transport = (Transport) packTransport; local = transport.local; uri = transport.uri; } TransferConfig.ProtocolVersion getProtocolVersion() { return protocol; } void setProtocolVersion(@NonNull TransferConfig.ProtocolVersion protocol) { this.protocol = protocol; } /** * Configure this connection with the directional pipes. * * @param myIn * input stream to receive data from the peer. Caller must ensure * the input is buffered, otherwise read performance may suffer. * @param myOut * output stream to transmit data to the peer. Caller must ensure * the output is buffered, otherwise write performance may * suffer. */ protected final void init(InputStream myIn, OutputStream myOut) { final int timeout = transport.getTimeout(); if (timeout > 0) { final Thread caller = Thread.currentThread(); if (myTimer == null) { myTimer = new InterruptTimer(caller.getName() + "-Timer"); //$NON-NLS-1$ } timeoutIn = new TimeoutInputStream(myIn, myTimer); timeoutOut = new TimeoutOutputStream(myOut, myTimer); timeoutIn.setTimeout(timeout * 1000); timeoutOut.setTimeout(timeout * 1000); myIn = timeoutIn; myOut = timeoutOut; } in = myIn; out = myOut; pckIn = new PacketLineIn(in); pckOut = new PacketLineOut(out); outNeedsEnd = true; } /** * Reads the advertised references through the initialized stream. *

* Subclass implementations may call this method only after setting up the * input and output streams with {@link #init(InputStream, OutputStream)}. *

* If any errors occur, this connection is automatically closed by invoking * {@link #close()} and the exception is wrapped (if necessary) and thrown * as a {@link org.eclipse.jgit.errors.TransportException}. * * @return {@code true} if the refs were read; {@code false} otherwise * indicating that {@link #lsRefs} must be called * * @throws org.eclipse.jgit.errors.TransportException * the reference list could not be scanned. */ protected boolean readAdvertisedRefs() throws TransportException { try { return readAdvertisedRefsImpl(); } catch (TransportException err) { close(); throw err; } catch (IOException | RuntimeException err) { close(); throw new TransportException(err.getMessage(), err); } } private String readLine() throws IOException { String line = pckIn.readString(); if (PacketLineIn.isEnd(line)) { return null; } if (line.startsWith("ERR ")) { //$NON-NLS-1$ // This is a customized remote service error. // Users should be informed about it. throw new RemoteRepositoryException(uri, line.substring(4)); } return line; } private boolean readAdvertisedRefsImpl() throws IOException { final Map avail = new LinkedHashMap<>(); final Map symRefs = new LinkedHashMap<>(); for (boolean first = true;; first = false) { String line; if (first) { boolean isV1 = false; try { line = readLine(); } catch (EOFException e) { throw noRepository(e); } if (line != null && VERSION_1.equals(line)) { // Same as V0, except for this extra line. We shouldn't get // it since we never request V1. setProtocolVersion(TransferConfig.ProtocolVersion.V0); isV1 = true; line = readLine(); } if (line == null) { break; } final int nul = line.indexOf('\0'); if (nul >= 0) { // Protocol V0: The first line (if any) may contain // "hidden" capability values after a NUL byte. for (String capability : line.substring(nul + 1) .split(" ")) { //$NON-NLS-1$ if (capability.startsWith(CAPABILITY_SYMREF_PREFIX)) { String[] parts = capability .substring( CAPABILITY_SYMREF_PREFIX.length()) .split(":", 2); //$NON-NLS-1$ if (parts.length == 2) { symRefs.put(parts[0], parts[1]); } } else { addCapability(capability); } } line = line.substring(0, nul); setProtocolVersion(TransferConfig.ProtocolVersion.V0); } else if (!isV1 && VERSION_2.equals(line)) { // Protocol V2: remaining lines are capabilities as // key=value pairs setProtocolVersion(TransferConfig.ProtocolVersion.V2); readCapabilitiesV2(); // Break out here so that stateless RPC transports get a // chance to set up the output stream. return false; } else { setProtocolVersion(TransferConfig.ProtocolVersion.V0); } } else { line = readLine(); if (line == null) { break; } } // Expecting to get a line in the form "sha1 refname" if (line.length() < 41 || line.charAt(40) != ' ') { throw invalidRefAdvertisementLine(line); } String name = line.substring(41, line.length()); if (first && name.equals("capabilities^{}")) { //$NON-NLS-1$ // special line from git-receive-pack (protocol V0) to show // capabilities when there are no refs to advertise continue; } final ObjectId id = toId(line, line.substring(0, 40)); if (name.equals(".have")) { //$NON-NLS-1$ additionalHaves.add(id); } else { processLineV1(name, id, avail); } } updateWithSymRefs(avail, symRefs); available(avail); return true; } /** * Issue a protocol V2 ls-refs command and read its response. * * @param refSpecs * to produce ref prefixes from if the server supports git * protocol V2 * @param additionalPatterns * to use for ref prefixes if the server supports git protocol V2 * @throws TransportException * if the command could not be run or its output not be read */ protected void lsRefs(Collection refSpecs, String... additionalPatterns) throws TransportException { try { lsRefsImpl(refSpecs, additionalPatterns); } catch (TransportException err) { close(); throw err; } catch (IOException | RuntimeException err) { close(); throw new TransportException(err.getMessage(), err); } } private void lsRefsImpl(Collection refSpecs, String... additionalPatterns) throws IOException { pckOut.writeString("command=" + COMMAND_LS_REFS); //$NON-NLS-1$ // Add the user-agent String agent = UserAgent.get(); if (agent != null && isCapableOf(OPTION_AGENT)) { pckOut.writeString(OPTION_AGENT + '=' + agent); } pckOut.writeDelim(); pckOut.writeString("peel"); //$NON-NLS-1$ pckOut.writeString("symrefs"); //$NON-NLS-1$ for (String refPrefix : getRefPrefixes(refSpecs, additionalPatterns)) { pckOut.writeString("ref-prefix " + refPrefix); //$NON-NLS-1$ } pckOut.end(); final Map avail = new LinkedHashMap<>(); final Map symRefs = new LinkedHashMap<>(); for (;;) { String line = readLine(); if (line == null) { break; } // Expecting to get a line in the form "sha1 refname" if (line.length() < 41 || line.charAt(40) != ' ') { throw invalidRefAdvertisementLine(line); } String name = line.substring(41, line.length()); final ObjectId id = toId(line, line.substring(0, 40)); if (name.equals(".have")) { //$NON-NLS-1$ additionalHaves.add(id); } else { processLineV2(line, id, name, avail, symRefs); } } updateWithSymRefs(avail, symRefs); available(avail); } private Collection getRefPrefixes(Collection refSpecs, String... additionalPatterns) { if (refSpecs.isEmpty() && (additionalPatterns == null || additionalPatterns.length == 0)) { return Collections.emptyList(); } Set patterns = new HashSet<>(); if (additionalPatterns != null) { Arrays.stream(additionalPatterns).filter(Objects::nonNull) .forEach(patterns::add); } for (RefSpec spec : refSpecs) { // TODO: for now we only do protocol V2 for fetch. For push // RefSpecs, the logic would need to be different. (At the // minimum, take spec.getDestination().) String src = spec.getSource(); if (ObjectId.isId(src)) { continue; } if (spec.isWildcard()) { patterns.add(src.substring(0, src.indexOf('*'))); } else { patterns.add(src); patterns.add(Constants.R_REFS + src); patterns.add(Constants.R_HEADS + src); patterns.add(Constants.R_TAGS + src); } } return patterns; } private void readCapabilitiesV2() throws IOException { // In git protocol V2, capabilities are different. If it's a key-value // pair, the key may be a command name, and the value a space-separated // list of capabilities for that command. We still store it in the same // map as for protocol v0/v1. Protocol v2 code has to account for this. for (;;) { String line = readLine(); if (line == null) { break; } addCapability(line); } } private void addCapability(String capability) { String parts[] = capability.split("=", 2); //$NON-NLS-1$ if (parts.length == 2) { remoteCapabilities.put(parts[0], parts[1]); } remoteCapabilities.put(capability, null); } private ObjectId toId(String line, String value) throws PackProtocolException { try { return ObjectId.fromString(value); } catch (InvalidObjectIdException e) { PackProtocolException ppe = invalidRefAdvertisementLine(line); ppe.initCause(e); throw ppe; } } private void processLineV1(String name, ObjectId id, Map avail) throws IOException { if (name.endsWith("^{}")) { //$NON-NLS-1$ name = name.substring(0, name.length() - 3); final Ref prior = avail.get(name); if (prior == null) { throw new PackProtocolException(uri, MessageFormat.format( JGitText.get().advertisementCameBefore, name, name)); } if (prior.getPeeledObjectId() != null) { throw duplicateAdvertisement(name + "^{}"); //$NON-NLS-1$ } avail.put(name, new ObjectIdRef.PeeledTag(Ref.Storage.NETWORK, name, prior.getObjectId(), id)); } else { final Ref prior = avail.put(name, new ObjectIdRef.PeeledNonTag( Ref.Storage.NETWORK, name, id)); if (prior != null) { throw duplicateAdvertisement(name); } } } private void processLineV2(String line, ObjectId id, String rest, Map avail, Map symRefs) throws IOException { String[] parts = rest.split(" "); //$NON-NLS-1$ String name = parts[0]; // Two attributes possible, symref-target or peeled String symRefTarget = null; String peeled = null; for (int i = 1; i < parts.length; i++) { if (parts[i].startsWith(REF_ATTR_SYMREF_TARGET)) { if (symRefTarget != null) { throw new PackProtocolException(uri, MessageFormat.format( JGitText.get().duplicateRefAttribute, line)); } symRefTarget = parts[i] .substring(REF_ATTR_SYMREF_TARGET.length()); } else if (parts[i].startsWith(REF_ATTR_PEELED)) { if (peeled != null) { throw new PackProtocolException(uri, MessageFormat.format( JGitText.get().duplicateRefAttribute, line)); } peeled = parts[i].substring(REF_ATTR_PEELED.length()); } if (peeled != null && symRefTarget != null) { break; } } Ref idRef; if (peeled != null) { idRef = new ObjectIdRef.PeeledTag(Ref.Storage.NETWORK, name, id, toId(line, peeled)); } else { idRef = new ObjectIdRef.PeeledNonTag(Ref.Storage.NETWORK, name, id); } Ref prior = avail.put(name, idRef); if (prior != null) { throw duplicateAdvertisement(name); } if (!StringUtils.isEmptyOrNull(symRefTarget)) { symRefs.put(name, symRefTarget); } } /** * Updates the given refMap with {@link SymbolicRef}s defined by the given * symRefs. *

* For each entry, symRef, in symRefs, whose value is a key in refMap, adds * a new entry to refMap with that same key and value of a new * {@link SymbolicRef} with source=symRef.key and * target=refMap.get(symRef.value), then removes that entry from symRefs. *

* If refMap already contains an entry for symRef.key, it is replaced. *

*

* For example, given: *

* *
	 * refMap.put("refs/heads/main", ref);
	 * symRefs.put("HEAD", "refs/heads/main");
	 * 
* * then: * *
	 * updateWithSymRefs(refMap, symRefs);
	 * 
* * has the effect of: * *
	 * refMap.put("HEAD",
	 * 		new SymbolicRef("HEAD", refMap.get(symRefs.remove("HEAD"))))
	 * 
*

* Any entry in symRefs whose value is not a key in refMap is ignored. Any * circular symRefs are ignored. *

*

* Upon completion, symRefs will contain only any unresolvable entries. *

* * @param refMap * a non-null, modifiable, Map to update, and the provider of * symref targets. * @param symRefs * a non-null, modifiable, Map of symrefs. * @throws NullPointerException * if refMap or symRefs is null */ static void updateWithSymRefs(Map refMap, Map symRefs) { boolean haveNewRefMapEntries = !refMap.isEmpty(); while (!symRefs.isEmpty() && haveNewRefMapEntries) { haveNewRefMapEntries = false; final Iterator> iterator = symRefs.entrySet().iterator(); while (iterator.hasNext()) { final Map.Entry symRef = iterator.next(); if (!symRefs.containsKey(symRef.getValue())) { // defer forward reference final Ref r = refMap.get(symRef.getValue()); if (r != null) { refMap.put(symRef.getKey(), new SymbolicRef(symRef.getKey(), r)); haveNewRefMapEntries = true; iterator.remove(); } } } } // If HEAD is still in the symRefs map here, the real ref was not // reported, but we know it must point to the object reported for HEAD. // So fill it in in the refMap. String headRefName = symRefs.get(Constants.HEAD); if (headRefName != null && !refMap.containsKey(headRefName)) { Ref headRef = refMap.get(Constants.HEAD); if (headRef != null) { ObjectId headObj = headRef.getObjectId(); headRef = new ObjectIdRef.PeeledNonTag(Ref.Storage.NETWORK, headRefName, headObj); refMap.put(headRefName, headRef); headRef = new SymbolicRef(Constants.HEAD, headRef); refMap.put(Constants.HEAD, headRef); symRefs.remove(Constants.HEAD); } } } /** * Create an exception to indicate problems finding a remote repository. The * caller is expected to throw the returned exception. * * Subclasses may override this method to provide better diagnostics. * * @param cause * root cause exception * @return a TransportException saying a repository cannot be found and * possibly why. */ protected TransportException noRepository(Throwable cause) { return new NoRemoteRepositoryException(uri, JGitText.get().notFound, cause); } /** * Whether this option is supported * * @param option * option string * @return whether this option is supported */ protected boolean isCapableOf(String option) { return remoteCapabilities.containsKey(option); } /** * Request capability * * @param b * buffer * @param option * option we want * @return {@code true} if the requested option is supported */ protected boolean wantCapability(StringBuilder b, String option) { if (!isCapableOf(option)) return false; b.append(' '); b.append(option); return true; } /** * Return a capability value. * * @param option * to get * @return the value stored, if any. */ protected String getCapability(String option) { return remoteCapabilities.get(option); } /** * Add user agent capability * * @param b * a {@link java.lang.StringBuilder} object. */ protected void addUserAgentCapability(StringBuilder b) { String a = UserAgent.get(); if (a != null && remoteCapabilities.get(OPTION_AGENT) != null) { b.append(' ').append(OPTION_AGENT).append('=').append(a); } } @Override public String getPeerUserAgent() { String agent = remoteCapabilities.get(OPTION_AGENT); return agent != null ? agent : super.getPeerUserAgent(); } private PackProtocolException duplicateAdvertisement(String name) { return new PackProtocolException(uri, MessageFormat.format(JGitText.get().duplicateAdvertisementsOf, name)); } private PackProtocolException invalidRefAdvertisementLine(String line) { return new PackProtocolException(uri, MessageFormat.format(JGitText.get().invalidRefAdvertisementLine, line)); } @Override public void close() { if (out != null) { try { if (outNeedsEnd) { outNeedsEnd = false; pckOut.end(); } out.close(); } catch (IOException err) { // Ignore any close errors. } finally { out = null; pckOut = null; } } if (in != null) { try { in.close(); } catch (IOException err) { // Ignore any close errors. } finally { in = null; pckIn = null; } } if (myTimer != null) { try { myTimer.terminate(); } finally { myTimer = null; timeoutIn = null; timeoutOut = null; } } } /** * Tell the peer we are disconnecting, if it cares to know. */ protected void endOut() { if (outNeedsEnd && out != null) { try { outNeedsEnd = false; pckOut.end(); } catch (IOException e) { try { out.close(); } catch (IOException err) { // Ignore any close errors. } finally { out = null; pckOut = null; } } } } }