View Javadoc
1   /*
2    * Copyright (C) 2015, Google Inc.
3    * and other copyright owners as documented in the project's IP log.
4    *
5    * This program and the accompanying materials are made available
6    * under the terms of the Eclipse Distribution License v1.0 which
7    * accompanies this distribution, is reproduced below, and is
8    * available at http://www.eclipse.org/org/documents/edl-v10.php
9    *
10   * All rights reserved.
11   *
12   * Redistribution and use in source and binary forms, with or
13   * without modification, are permitted provided that the following
14   * conditions are met:
15   *
16   * - Redistributions of source code must retain the above copyright
17   *   notice, this list of conditions and the following disclaimer.
18   *
19   * - Redistributions in binary form must reproduce the above
20   *   copyright notice, this list of conditions and the following
21   *   disclaimer in the documentation and/or other materials provided
22   *   with the distribution.
23   *
24   * - Neither the name of the Eclipse Foundation, Inc. nor the
25   *   names of its contributors may be used to endorse or promote
26   *   products derived from this software without specific prior
27   *   written permission.
28   *
29   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42   */
43  
44  package org.eclipse.jgit.transport;
45  
46  import static java.nio.charset.StandardCharsets.UTF_8;
47  import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
48  import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT;
49  import static org.eclipse.jgit.lib.FileMode.TYPE_FILE;
50  
51  import java.io.BufferedReader;
52  import java.io.IOException;
53  import java.io.InputStream;
54  import java.io.InputStreamReader;
55  import java.io.Reader;
56  import java.text.MessageFormat;
57  import java.util.ArrayList;
58  import java.util.Collection;
59  import java.util.Collections;
60  import java.util.Comparator;
61  import java.util.HashMap;
62  import java.util.Iterator;
63  import java.util.List;
64  import java.util.Map;
65  import java.util.NoSuchElementException;
66  
67  import org.eclipse.jgit.dircache.DirCache;
68  import org.eclipse.jgit.dircache.DirCacheBuilder;
69  import org.eclipse.jgit.dircache.DirCacheEditor;
70  import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
71  import org.eclipse.jgit.dircache.DirCacheEntry;
72  import org.eclipse.jgit.internal.JGitText;
73  import org.eclipse.jgit.lib.BatchRefUpdate;
74  import org.eclipse.jgit.lib.CommitBuilder;
75  import org.eclipse.jgit.lib.Constants;
76  import org.eclipse.jgit.lib.FileMode;
77  import org.eclipse.jgit.lib.ObjectId;
78  import org.eclipse.jgit.lib.ObjectInserter;
79  import org.eclipse.jgit.lib.ObjectLoader;
80  import org.eclipse.jgit.lib.ObjectReader;
81  import org.eclipse.jgit.lib.PersonIdent;
82  import org.eclipse.jgit.lib.Ref;
83  import org.eclipse.jgit.lib.RefUpdate;
84  import org.eclipse.jgit.lib.Repository;
85  import org.eclipse.jgit.revwalk.RevCommit;
86  import org.eclipse.jgit.revwalk.RevWalk;
87  import org.eclipse.jgit.treewalk.TreeWalk;
88  import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
89  import org.eclipse.jgit.treewalk.filter.PathFilter;
90  import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
91  import org.eclipse.jgit.treewalk.filter.TreeFilter;
92  
93  /**
94   * Storage for recorded push certificates.
95   * <p>
96   * Push certificates are stored in a special ref {@code refs/meta/push-certs}.
97   * The filenames in the tree are ref names followed by the special suffix
98   * <code>@{cert}</code>, and the contents are the latest push cert affecting
99   * that ref. The special suffix allows storing certificates for both refs/foo
100  * and refs/foo/bar in case those both existed at some point.
101  *
102  * @since 4.1
103  */
104 public class PushCertificateStore implements AutoCloseable {
105 	/** Ref name storing push certificates. */
106 	static final String REF_NAME =
107 			Constants.R_REFS + "meta/push-certs"; //$NON-NLS-1$
108 
109 	private static class PendingCert {
110 		private PushCertificate cert;
111 		private PersonIdent ident;
112 		private Collection<ReceiveCommand> matching;
113 
114 		private PendingCert(PushCertificate cert, PersonIdent ident,
115 				Collection<ReceiveCommand> matching) {
116 			this.cert = cert;
117 			this.ident = ident;
118 			this.matching = matching;
119 		}
120 	}
121 
122 	private final Repository db;
123 	private final List<PendingCert> pending;
124 	private ObjectReader reader;
125 	private RevCommit commit;
126 
127 	/**
128 	 * Create a new store backed by the given repository.
129 	 *
130 	 * @param db
131 	 *            the repository.
132 	 */
133 	public PushCertificateStore(Repository db) {
134 		this.db = db;
135 		pending = new ArrayList<>();
136 	}
137 
138 	/**
139 	 * Close resources opened by this store.
140 	 * <p>
141 	 * If {@link #get(String)} was called, closes the cached object reader created
142 	 * by that method. Does not close the underlying repository.
143 	 */
144 	public void close() {
145 		if (reader != null) {
146 			reader.close();
147 			reader = null;
148 			commit = null;
149 		}
150 	}
151 
152 	/**
153 	 * Get latest push certificate associated with a ref.
154 	 * <p>
155 	 * Lazily opens {@code refs/meta/push-certs} and reads from the repository as
156 	 * necessary. The state is cached between calls to {@code get}; to reread the,
157 	 * call {@link #close()} first.
158 	 *
159 	 * @param refName
160 	 *            the ref name to get the certificate for.
161 	 * @return last certificate affecting the ref, or null if no cert was recorded
162 	 *         for the last update to this ref.
163 	 * @throws IOException
164 	 *             if a problem occurred reading the repository.
165 	 */
166 	public PushCertificate get(String refName) throws IOException {
167 		if (reader == null) {
168 			load();
169 		}
170 		try (TreeWalk tw = newTreeWalk(refName)) {
171 			return read(tw);
172 		}
173 	}
174 
175 	/**
176 	 * Iterate over all push certificates affecting a ref.
177 	 * <p>
178 	 * Only includes push certificates actually stored in the tree; see class
179 	 * Javadoc for conditions where this might not include all push certs ever
180 	 * seen for this ref.
181 	 * <p>
182 	 * The returned iterable may be iterated multiple times, and push certs will
183 	 * be re-read from the current state of the store on each call to {@link
184 	 * Iterable#iterator()}. However, method calls on the returned iterator may
185 	 * fail if {@code save} or {@code close} is called on the enclosing store
186 	 * during iteration.
187 	 *
188 	 * @param refName
189 	 *            the ref name to get certificates for.
190 	 * @return iterable over certificates; must be fully iterated in order to
191 	 *         close resources.
192 	 */
193 	public Iterable<PushCertificate> getAll(final String refName) {
194 		return new Iterable<PushCertificate>() {
195 			@Override
196 			public Iterator<PushCertificate> iterator() {
197 				return new Iterator<PushCertificate>() {
198 					private final String path = pathName(refName);
199 					private PushCertificate next;
200 
201 					private RevWalk rw;
202 					{
203 						try {
204 							if (reader == null) {
205 								load();
206 							}
207 							if (commit != null) {
208 								rw = new RevWalk(reader);
209 								rw.setTreeFilter(AndTreeFilter.create(
210 										PathFilterGroup.create(
211 											Collections.singleton(PathFilter.create(path))),
212 										TreeFilter.ANY_DIFF));
213 								rw.setRewriteParents(false);
214 								rw.markStart(rw.parseCommit(commit));
215 							} else {
216 								rw = null;
217 							}
218 						} catch (IOException e) {
219 							throw new RuntimeException(e);
220 						}
221 					}
222 
223 					@Override
224 					public boolean hasNext() {
225 						try {
226 							if (next == null) {
227 								if (rw == null) {
228 									return false;
229 								}
230 								try {
231 									RevCommit c = rw.next();
232 									if (c != null) {
233 										try (TreeWalk tw = TreeWalk.forPath(
234 												rw.getObjectReader(), path, c.getTree())) {
235 											next = read(tw);
236 										}
237 									} else {
238 										next = null;
239 									}
240 								} catch (IOException e) {
241 									throw new RuntimeException(e);
242 								}
243 							}
244 							return next != null;
245 						} finally {
246 							if (next == null && rw != null) {
247 								rw.close();
248 								rw = null;
249 							}
250 						}
251 					}
252 
253 					@Override
254 					public PushCertificate next() {
255 						hasNext();
256 						PushCertificate n = next;
257 						if (n == null) {
258 							throw new NoSuchElementException();
259 						}
260 						next = null;
261 						return n;
262 					}
263 
264 					@Override
265 					public void remove() {
266 						throw new UnsupportedOperationException();
267 					}
268 				};
269 			}
270 		};
271 	}
272 
273 	private void load() throws IOException {
274 		close();
275 		reader = db.newObjectReader();
276 		Ref ref = db.getRefDatabase().exactRef(REF_NAME);
277 		if (ref == null) {
278 			// No ref, same as empty.
279 			return;
280 		}
281 		try (RevWalk rw = new RevWalk(reader)) {
282 			commit = rw.parseCommit(ref.getObjectId());
283 		}
284 	}
285 
286 	private static PushCertificate read(TreeWalk tw) throws IOException {
287 		if (tw == null || (tw.getRawMode(0) & TYPE_FILE) != TYPE_FILE) {
288 			return null;
289 		}
290 		ObjectLoader loader =
291 				tw.getObjectReader().open(tw.getObjectId(0), OBJ_BLOB);
292 		try (InputStream in = loader.openStream();
293 				Reader r = new BufferedReader(new InputStreamReader(in, UTF_8))) {
294 			return PushCertificateParser.fromReader(r);
295 		}
296 	}
297 
298 	/**
299 	 * Put a certificate to be saved to the store.
300 	 * <p>
301 	 * Writes the contents of this certificate for each ref mentioned. It is up to
302 	 * the caller to ensure this certificate accurately represents the state of
303 	 * the ref.
304 	 * <p>
305 	 * Pending certificates added to this method are not returned by {@link
306 	 * #get(String)} and {@link #getAll(String)} until after calling {@link
307 	 * #save()}.
308 	 *
309 	 * @param cert
310 	 *            certificate to store.
311 	 * @param ident
312 	 *            identity for the commit that stores this certificate. Pending
313 	 *            certificates are sorted by identity timestamp during {@link
314 	 *            #save()}.
315 	 */
316 	public void put(PushCertificate cert, PersonIdent ident) {
317 		put(cert, ident, null);
318 	}
319 
320 	/**
321 	 * Put a certificate to be saved to the store, matching a set of commands.
322 	 * <p>
323 	 * Like {@link #put(PushCertificate, PersonIdent)}, except a value is only
324 	 * stored for a push certificate if there is a corresponding command in the
325 	 * list that exactly matches the old/new values mentioned in the push
326 	 * certificate.
327 	 * <p>
328 	 * Pending certificates added to this method are not returned by {@link
329 	 * #get(String)} and {@link #getAll(String)} until after calling {@link
330 	 * #save()}.
331 	 *
332 	 * @param cert
333 	 *            certificate to store.
334 	 * @param ident
335 	 *            identity for the commit that stores this certificate. Pending
336 	 *            certificates are sorted by identity timestamp during {@link
337 	 *            #save()}.
338 	 * @param matching
339 	 *            only store certs for the refs listed in this list whose values
340 	 *            match the commands in the cert.
341 	 */
342 	public void put(PushCertificate cert, PersonIdent ident,
343 			Collection<ReceiveCommand> matching) {
344 		pending.add(new PendingCert(cert, ident, matching));
345 	}
346 
347 	/**
348 	 * Save pending certificates to the store.
349 	 * <p>
350 	 * One commit is created per certificate added with {@link
351 	 * #put(PushCertificate, PersonIdent)}, in order of identity timestamps, and
352 	 * a single ref update is performed.
353 	 * <p>
354 	 * The pending list is cleared if and only the ref update fails, which allows
355 	 * for easy retries in case of lock failure.
356 	 *
357 	 * @return the result of attempting to update the ref.
358 	 * @throws IOException
359 	 *             if there was an error reading from or writing to the
360 	 *             repository.
361 	 */
362 	public RefUpdate.Result save() throws IOException {
363 		ObjectId newId = write();
364 		if (newId == null) {
365 			return RefUpdate.Result.NO_CHANGE;
366 		}
367 		try (ObjectInserter inserter = db.newObjectInserter()) {
368 			RefUpdate.Result result = updateRef(newId);
369 			switch (result) {
370 				case FAST_FORWARD:
371 				case NEW:
372 				case NO_CHANGE:
373 					pending.clear();
374 					break;
375 				default:
376 					break;
377 			}
378 			return result;
379 		} finally {
380 			close();
381 		}
382 	}
383 
384 	/**
385 	 * Save pending certificates to the store in an existing batch ref update.
386 	 * <p>
387 	 * One commit is created per certificate added with {@link
388 	 * #put(PushCertificate, PersonIdent)}, in order of identity timestamps, all
389 	 * commits are flushed, and a single command is added to the batch.
390 	 * <p>
391 	 * The cached ref value and pending list are <em>not</em> cleared. If the ref
392 	 * update succeeds, the caller is responsible for calling {@link #close()}
393 	 * and/or {@link #clear()}.
394 	 *
395 	 * @param batch
396 	 *            update to save to.
397 	 * @return whether a command was added to the batch.
398 	 * @throws IOException
399 	 *             if there was an error reading from or writing to the
400 	 *             repository.
401 	 */
402 	public boolean save(BatchRefUpdate batch) throws IOException {
403 		ObjectId newId = write();
404 		if (newId == null || newId.equals(commit)) {
405 			return false;
406 		}
407 		batch.addCommand(new ReceiveCommand(
408 				commit != null ? commit : ObjectId.zeroId(), newId, REF_NAME));
409 		return true;
410 	}
411 
412 	/**
413 	 * Clear pending certificates added with {@link #put(PushCertificate,
414 	 * PersonIdent)}.
415 	 */
416 	public void clear() {
417 		pending.clear();
418 	}
419 
420 	private ObjectId write() throws IOException {
421 		if (pending.isEmpty()) {
422 			return null;
423 		}
424 		if (reader == null) {
425 			load();
426 		}
427 		sortPending(pending);
428 
429 		ObjectId curr = commit;
430 		DirCache dc = newDirCache();
431 		try (ObjectInserter inserter = db.newObjectInserter()) {
432 			for (PendingCert pc : pending) {
433 				curr = saveCert(inserter, dc, pc, curr);
434 			}
435 			inserter.flush();
436 			return curr;
437 		}
438 	}
439 
440 	private static void sortPending(List<PendingCert> pending) {
441 		Collections.sort(pending, new Comparator<PendingCert>() {
442 			@Override
443 			public int compare(PendingCert a, PendingCert b) {
444 				return Long.signum(
445 						a.ident.getWhen().getTime() - b.ident.getWhen().getTime());
446 			}
447 		});
448 	}
449 
450 	private DirCache newDirCache() throws IOException {
451 		DirCache dc = DirCache.newInCore();
452 		if (commit != null) {
453 			DirCacheBuilder b = dc.builder();
454 			b.addTree(new byte[0], DirCacheEntry.STAGE_0, reader, commit.getTree());
455 			b.finish();
456 		}
457 		return dc;
458 	}
459 
460 	private ObjectId saveCert(ObjectInserter inserter, DirCache dc,
461 			PendingCert pc, ObjectId curr) throws IOException {
462 		Map<String, ReceiveCommand> byRef;
463 		if (pc.matching != null) {
464 			byRef = new HashMap<>();
465 			for (ReceiveCommand cmd : pc.matching) {
466 				if (byRef.put(cmd.getRefName(), cmd) != null) {
467 					throw new IllegalStateException();
468 				}
469 			}
470 		} else {
471 			byRef = null;
472 		}
473 
474 		DirCacheEditor editor = dc.editor();
475 		String certText = pc.cert.toText() + pc.cert.getSignature();
476 		final ObjectId certId = inserter.insert(OBJ_BLOB, certText.getBytes(UTF_8));
477 		boolean any = false;
478 		for (ReceiveCommand cmd : pc.cert.getCommands()) {
479 			if (byRef != null && !commandsEqual(cmd, byRef.get(cmd.getRefName()))) {
480 				continue;
481 			}
482 			any = true;
483 			editor.add(new PathEdit(pathName(cmd.getRefName())) {
484 				@Override
485 				public void apply(DirCacheEntry ent) {
486 					ent.setFileMode(FileMode.REGULAR_FILE);
487 					ent.setObjectId(certId);
488 				}
489 			});
490 		}
491 		if (!any) {
492 			return curr;
493 		}
494 		editor.finish();
495 		CommitBuilder cb = new CommitBuilder();
496 		cb.setAuthor(pc.ident);
497 		cb.setCommitter(pc.ident);
498 		cb.setTreeId(dc.writeTree(inserter));
499 		if (curr != null) {
500 			cb.setParentId(curr);
501 		} else {
502 			cb.setParentIds(Collections.<ObjectId> emptyList());
503 		}
504 		cb.setMessage(buildMessage(pc.cert));
505 		return inserter.insert(OBJ_COMMIT, cb.build());
506 	}
507 
508 	private static boolean commandsEqual(ReceiveCommand c1, ReceiveCommand c2) {
509 		if (c1 == null || c2 == null) {
510 			return c1 == c2;
511 		}
512 		return c1.getRefName().equals(c2.getRefName())
513 				&& c1.getOldId().equals(c2.getOldId())
514 				&& c1.getNewId().equals(c2.getNewId());
515 	}
516 
517 	private RefUpdate.Result updateRef(ObjectId newId) throws IOException {
518 		RefUpdate ru = db.updateRef(REF_NAME);
519 		ru.setExpectedOldObjectId(commit != null ? commit : ObjectId.zeroId());
520 		ru.setNewObjectId(newId);
521 		ru.setRefLogIdent(pending.get(pending.size() - 1).ident);
522 		ru.setRefLogMessage(JGitText.get().storePushCertReflog, false);
523 		try (RevWalk rw = new RevWalk(reader)) {
524 			return ru.update(rw);
525 		}
526 	}
527 
528 	private TreeWalk newTreeWalk(String refName) throws IOException {
529 		if (commit == null) {
530 			return null;
531 		}
532 		return TreeWalk.forPath(reader, pathName(refName), commit.getTree());
533 	}
534 
535 	private static String pathName(String refName) {
536 		return refName + "@{cert}"; //$NON-NLS-1$
537 	}
538 
539 	private static String buildMessage(PushCertificate cert) {
540 		StringBuilder sb = new StringBuilder();
541 		if (cert.getCommands().size() == 1) {
542 			sb.append(MessageFormat.format(
543 					JGitText.get().storePushCertOneRef,
544 					cert.getCommands().get(0).getRefName()));
545 		} else {
546 			sb.append(MessageFormat.format(
547 					JGitText.get().storePushCertMultipleRefs,
548 					Integer.valueOf(cert.getCommands().size())));
549 		}
550 		return sb.append('\n').toString();
551 	}
552 }