Selaa lähdekoodia

UploadPack: Fix races in smart HTTP negotiation

Clients cache the set of advertised references at the start of a
negotiation, and keep replaying the same "want SHA1" list to the
server on each negotiation step.  If another client pushes into
a branch and moves it by fast-forward, any request to obtain that
branch's prior SHA-1 is still valid, the commit is reachable from
the new position of the reference.  Unfortunately the fast-forward
causes smart HTTP negotations to fail, as the server no longer is
advertising that prior SHA-1.

Instead of causing clients to fail out with a "want invalid" error
and forcing the end-user retry, possibly getting into a never ending
try-fail-retry race while other clients are pushing into the same
busy repository, allow the slightly stale want request so long as
it is still reachable.

C Git implemented this same change recently to fix races on the
smart HTTP protocol when the C Git git-http-backend is used.

The new RequestPolicy feature also allows server authors to make
an even more lenient configuration that exports any SHA-1 to the
client. This might be useful in certain settings where a server
has authenticated the client as the "repository owner" and wants
to allow them to grab any content from the server as a complete
unbroken history chain.

The new setAdvertisedRefs() method allows server authors to manually
fix the references that are advertised, possibly bypassing the
getAllRefs() call on the Repository object.

Change-Id: I7cdb563bf9c55c83653f217f6e53c3add55a0541
Signed-off-by: Shawn O. Pearce <spearce@spearce.org>
tags/v1.2.0.201112221803-r
Shawn O. Pearce 12 vuotta sitten
vanhempi
commit
01888db892
1 muutettua tiedostoa jossa 109 lisäystä ja 9 poistoa
  1. 109
    9
      org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java

+ 109
- 9
org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java Näytä tiedosto

import java.io.OutputStream; import java.io.OutputStream;
import java.text.MessageFormat; import java.text.MessageFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;


static final String OPTION_SHALLOW = BasePackFetchConnection.OPTION_SHALLOW; static final String OPTION_SHALLOW = BasePackFetchConnection.OPTION_SHALLOW;


/** Policy the server uses to validate client requests */
public static enum RequestPolicy {
/** Client may only ask for objects the server advertised a reference for. */
ADVERTISED,
/** Client may ask for any commit reachable from a reference. */
REACHABLE_COMMIT,
/** Client may ask for any SHA-1 in the repository. */
ANY;
}

/** Database we read the objects from. */ /** Database we read the objects from. */
private final Repository db; private final Repository db;




private final RevFlagSet SAVE; private final RevFlagSet SAVE;


private RequestPolicy requestPolicy = RequestPolicy.ADVERTISED;

private MultiAck multiAck = MultiAck.OFF; private MultiAck multiAck = MultiAck.OFF;


private boolean noDone; private boolean noDone;


/** @return all refs which were advertised to the client. */ /** @return all refs which were advertised to the client. */
public final Map<String, Ref> getAdvertisedRefs() { public final Map<String, Ref> getAdvertisedRefs() {
if (refs == null) {
refs = refFilter.filter(db.getAllRefs());
}
if (refs == null)
setAdvertisedRefs(db.getAllRefs());
return refs; return refs;
} }


/**
* @param allRefs
* explicit set of references to claim as advertised by this
* UploadPack instance. This overrides any references that
* may exist in the source repository. The map is passed
* to the configured {@link #getRefFilter()}.
*/
public void setAdvertisedRefs(Map<String, Ref> allRefs) {
refs = refFilter.filter(allRefs);
}

/** @return timeout (in seconds) before aborting an IO operation. */ /** @return timeout (in seconds) before aborting an IO operation. */
public int getTimeout() { public int getTimeout() {
return timeout; return timeout;
*/ */
public void setBiDirectionalPipe(final boolean twoWay) { public void setBiDirectionalPipe(final boolean twoWay) {
biDirectionalPipe = twoWay; biDirectionalPipe = twoWay;
if (!biDirectionalPipe && requestPolicy == RequestPolicy.ADVERTISED)
requestPolicy = RequestPolicy.REACHABLE_COMMIT;
}

/** @return policy used by the service to validate client requests. */
public RequestPolicy getRequestPolicy() {
return requestPolicy;
}

/**
* @param policy
* the policy used to enforce validation of a client's want list.
* By default the policy is {@link RequestPolicy#ADVERTISED},
* which is the Git default requiring clients to only ask for an
* object that a reference directly points to. This may be relaxed
* to {@link RequestPolicy#REACHABLE_COMMIT} when callers
* have {@link #setBiDirectionalPipe(boolean)} set to false.
*/
public void setRequestPolicy(RequestPolicy policy) {
requestPolicy = policy != null ? policy : RequestPolicy.ADVERTISED;
} }


/** @return the filter used while advertising the refs to the client */ /** @return the filter used while advertising the refs to the client */
private void service() throws IOException { private void service() throws IOException {
if (biDirectionalPipe) if (biDirectionalPipe)
sendAdvertisedRefs(new PacketLineOutRefAdvertiser(pckOut)); sendAdvertisedRefs(new PacketLineOutRefAdvertiser(pckOut));
else if (requestPolicy == RequestPolicy.ANY)
advertised = Collections.emptySet();
else { else {
advertised = new HashSet<ObjectId>(); advertised = new HashSet<ObjectId>();
for (Ref ref : getAdvertisedRefs().values()) { for (Ref ref : getAdvertisedRefs().values()) {
needMissing = true; needMissing = true;
} }


Set<RevObject> notAdvertisedWants = null;
int haveCnt = 0; int haveCnt = 0;
AsyncRevObjectQueue q = walk.parseAny(toParse, needMissing); AsyncRevObjectQueue q = walk.parseAny(toParse, needMissing);
try { try {
// list wasn't parsed earlier, and was done in this batch. // list wasn't parsed earlier, and was done in this batch.
// //
if (wantIds.remove(obj)) { if (wantIds.remove(obj)) {
if (!advertised.contains(obj)) {
String msg = MessageFormat.format(
JGitText.get().wantNotValid, obj.name());
throw new PackProtocolException(msg);
if (!advertised.contains(obj) && requestPolicy != RequestPolicy.ANY) {
if (notAdvertisedWants == null)
notAdvertisedWants = new HashSet<RevObject>();
notAdvertisedWants.add(obj);
} }


if (!obj.has(WANT)) { if (!obj.has(WANT)) {
} finally { } finally {
q.release(); q.release();
} }

// If the client asked for non advertised object, check our policy.
if (notAdvertisedWants != null && !notAdvertisedWants.isEmpty()) {
switch (requestPolicy) {
case ADVERTISED:
default:
throw new PackProtocolException(MessageFormat.format(
JGitText.get().wantNotValid,
notAdvertisedWants.iterator().next().name()));

case REACHABLE_COMMIT:
checkNotAdvertisedWants(notAdvertisedWants);
break;

case ANY:
// Allow whatever was asked for.
break;
}
}

int missCnt = peerHas.size() - haveCnt; int missCnt = peerHas.size() - haveCnt;


// If we don't have one of the objects but we're also willing to // If we don't have one of the objects but we're also willing to
return last; return last;
} }


private void checkNotAdvertisedWants(Set<RevObject> notAdvertisedWants)
throws MissingObjectException, IncorrectObjectTypeException, IOException {
// Walk the requested commits back to the advertised commits.
// If any commit exists, a branch was deleted or rewound and
// the repository owner no longer exports that requested item.
// If the requested commit is merged into an advertised branch
// it will be marked UNINTERESTING and no commits return.

for (RevObject o : notAdvertisedWants) {
if (!(o instanceof RevCommit)) {
throw new PackProtocolException(MessageFormat.format(
JGitText.get().wantNotValid,
notAdvertisedWants.iterator().next().name()));
}
walk.markStart((RevCommit) o);
}

for (ObjectId id : advertised) {
try {
walk.markUninteresting(walk.parseCommit(id));
} catch (IncorrectObjectTypeException notCommit) {
continue;
}
}

RevCommit bad = walk.next();
if (bad != null) {
throw new PackProtocolException(MessageFormat.format(
JGitText.get().wantNotValid,
bad.name()));
}
walk.reset();
}

private void addCommonBase(final RevObject o) { private void addCommonBase(final RevObject o) {
if (!o.has(COMMON)) { if (!o.has(COMMON)) {
o.add(COMMON); o.add(COMMON);
pw.setThin(options.contains(OPTION_THIN_PACK)); pw.setThin(options.contains(OPTION_THIN_PACK));
pw.setReuseValidatingObjects(false); pw.setReuseValidatingObjects(false);


if (commonBase.isEmpty()) {
if (commonBase.isEmpty() && refs != null) {
Set<ObjectId> tagTargets = new HashSet<ObjectId>(); Set<ObjectId> tagTargets = new HashSet<ObjectId>();
for (Ref ref : refs.values()) { for (Ref ref : refs.values()) {
if (ref.getPeeledObjectId() != null) if (ref.getPeeledObjectId() != null)
rw = ow; rw = ow;
} }


if (options.contains(OPTION_INCLUDE_TAG)) {
if (options.contains(OPTION_INCLUDE_TAG) && refs != null) {
for (Ref ref : refs.values()) { for (Ref ref : refs.values()) {
ObjectId objectId = ref.getObjectId(); ObjectId objectId = ref.getObjectId();



Loading…
Peruuta
Tallenna