diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateParserTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateParserTest.java index 8fdf386ec..26b4d88f5 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateParserTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateParserTest.java @@ -45,6 +45,7 @@ package org.eclipse.jgit.transport; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -52,6 +53,8 @@ import static org.junit.Assert.fail; import java.io.ByteArrayInputStream; import java.io.EOFException; import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; import org.eclipse.jgit.errors.PackProtocolException; import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription; @@ -60,6 +63,7 @@ import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.PushCertificate.NonceStatus; import org.junit.Before; import org.junit.Test; @@ -274,6 +278,62 @@ public class PushCertificateParserTest { assertEquals("line 2\nline 3\n", concatPacketLines(input, 1, 4)); } + @Test + public void testParseReader() throws Exception { + Reader reader = new InputStreamReader( + new ByteArrayInputStream( + Constants.encode(concatPacketLines(INPUT, 0, 18)))); + PushCertificate streamCert = PushCertificateParser.fromReader(reader); + + PacketLineIn pckIn = newPacketLineIn(INPUT); + PushCertificateParser pckParser = + new PushCertificateParser(db, newEnabledConfig()); + pckParser.receiveHeader(pckIn, false); + pckParser.addCommand(pckIn.readString()); + assertEquals(PushCertificateParser.BEGIN_SIGNATURE, pckIn.readString()); + pckParser.receiveSignature(pckIn); + PushCertificate pckCert = pckParser.build(); + + // Nonce status is unsolicited since this was not parsed in the context of + // the wire protocol; as a result, certs are not actually equal. + assertEquals(NonceStatus.UNSOLICITED, streamCert.getNonceStatus()); + + assertEquals(pckCert.getVersion(), streamCert.getVersion()); + assertEquals(pckCert.getPusherIdent().getName(), + streamCert.getPusherIdent().getName()); + assertEquals(pckCert.getPusherIdent().getEmailAddress(), + streamCert.getPusherIdent().getEmailAddress()); + assertEquals(pckCert.getPusherIdent().getWhen().getTime(), + streamCert.getPusherIdent().getWhen().getTime()); + assertEquals(pckCert.getPusherIdent().getTimeZoneOffset(), + streamCert.getPusherIdent().getTimeZoneOffset()); + assertEquals(pckCert.getPushee(), streamCert.getPushee()); + assertEquals(pckCert.getNonce(), streamCert.getNonce()); + assertEquals(pckCert.getSignature(), streamCert.getSignature()); + assertEquals(pckCert.toText(), streamCert.toText()); + + assertEquals(pckCert.getCommands().size(), streamCert.getCommands().size()); + ReceiveCommand pckCmd = pckCert.getCommands().get(0); + ReceiveCommand streamCmd = streamCert.getCommands().get(0); + assertEquals(pckCmd.getRefName(), streamCmd.getRefName()); + assertEquals(pckCmd.getOldId(), streamCmd.getOldId()); + assertEquals(pckCmd.getNewId().name(), streamCmd.getNewId().name()); + } + + @Test + public void testParseMultipleFromStream() throws Exception { + String input = concatPacketLines(INPUT, 0, 17); + assertFalse(input.contains(PushCertificateParser.END_CERT)); + input += input; + Reader reader = new InputStreamReader( + new ByteArrayInputStream(Constants.encode(input))); + + assertNotNull(PushCertificateParser.fromReader(reader)); + assertNotNull(PushCertificateParser.fromReader(reader)); + assertEquals(-1, reader.read()); + assertNull(PushCertificateParser.fromReader(reader)); + } + private static String concatPacketLines(String input, int begin, int end) throws IOException { StringBuilder result = new StringBuilder(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateParser.java index b34fb9fc6..93a1e54b0 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateParser.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateParser.java @@ -40,6 +40,7 @@ * 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.BaseReceivePack.parseCommand; @@ -47,6 +48,7 @@ import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_PUSH_CE import java.io.EOFException; import java.io.IOException; +import java.io.Reader; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collections; @@ -57,6 +59,7 @@ import org.eclipse.jgit.errors.PackProtocolException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.transport.PushCertificate.NonceStatus; +import org.eclipse.jgit.util.IO; /** * Parser for signed push certificates. @@ -77,9 +80,97 @@ public class PushCertificateParser { static final String NONCE = "nonce"; //$NON-NLS-1$ + static final String END_CERT = "push-cert-end"; //$NON-NLS-1$ + private static final String VERSION_0_1 = "0.1"; //$NON-NLS-1$ - private static final String END_CERT = "push-cert-end"; //$NON-NLS-1$ + private static interface StringReader { + /** + * @return the next string from the input, up to an optional newline, with + * newline stripped if present + * + * @throws EOFException + * if EOF was reached. + * @throws IOException + * if an error occurred during reading. + */ + String read() throws EOFException, IOException; + } + + private static class PacketLineReader implements StringReader { + private final PacketLineIn pckIn; + + private PacketLineReader(PacketLineIn pckIn) { + this.pckIn = pckIn; + } + + @Override + public String read() throws IOException { + return pckIn.readString(); + } + } + + private static class StreamReader implements StringReader { + private final Reader reader; + + private StreamReader(Reader reader) { + this.reader = reader; + } + + @Override + public String read() throws IOException { + // Presize for a command containing 2 SHA-1s and some refname. + String line = IO.readLine(reader, 41 * 2 + 64); + if (line.isEmpty()) { + throw new EOFException(); + } else if (line.charAt(line.length() - 1) == '\n') { + line = line.substring(0, line.length() - 1); + } + return line; + } + } + + /** + * Parse a push certificate from a reader. + *

+ * Differences from the {@link PacketLineIn} receiver methods: + *

+ * + * @param r + * input reader; consumed only up until the end of the next + * signature in the input. + * @return the parsed certificate, or null if the reader was at EOF. + * @throws PackProtocolException + * if the certificate is malformed. + * @throws IOException + * if there was an error reading from the input. + * @since 4.1 + */ + public static PushCertificate fromReader(Reader r) + throws PackProtocolException, IOException { + PushCertificateParser parser = new PushCertificateParser(); + StreamReader reader = new StreamReader(r); + parser.receiveHeader(reader, true); + String line; + try { + while (!(line = reader.read()).isEmpty()) { + if (line.equals(BEGIN_SIGNATURE)) { + parser.receiveSignature(reader); + break; + } + parser.addCommand(line); + } + } catch (EOFException e) { + // EOF reached, but might have been at a valid state. Let build call below + // sort it out. + } + return parser.build(); + } private boolean received; private String version; @@ -109,8 +200,9 @@ public class PushCertificateParser { */ private final int nonceSlopLimit; + private final boolean enabled; private final NonceGenerator nonceGenerator; - private final List commands; + private final List commands = new ArrayList<>(); PushCertificateParser(Repository into, SignedPushConfig cfg) { if (cfg != null) { @@ -121,7 +213,14 @@ public class PushCertificateParser { nonceGenerator = null; } db = into; - commands = new ArrayList<>(); + enabled = nonceGenerator != null; + } + + private PushCertificateParser() { + db = null; + nonceSlopLimit = 0; + nonceGenerator = null; + enabled = true; } /** @@ -131,7 +230,7 @@ public class PushCertificateParser { * @since 4.1 */ public PushCertificate build() throws IOException { - if (!received || nonceGenerator == null) { + if (!received || !enabled) { return null; } try { @@ -143,11 +242,12 @@ public class PushCertificateParser { } /** - * @return if the server is configured to use signed pushes. + * @return if the repository is configured to use signed pushes in this + * context. * @since 4.0 */ public boolean enabled() { - return nonceGenerator != null; + return enabled; } /** @@ -171,9 +271,12 @@ public class PushCertificateParser { return sentNonce; } - private static String parseHeader(PacketLineIn pckIn, String header) + private static String parseHeader(StringReader reader, String header) throws IOException { - String s = pckIn.readString(); + String s = reader.read(); + if (s.isEmpty()) { + throw new EOFException(); + } if (s.length() <= header.length() || !s.startsWith(header) || s.charAt(header.length()) != ' ') { @@ -205,24 +308,37 @@ public class PushCertificateParser { */ public void receiveHeader(PacketLineIn pckIn, boolean stateless) throws IOException { - received = true; + receiveHeader(new PacketLineReader(pckIn), stateless); + } + + private void receiveHeader(StringReader reader, boolean stateless) + throws IOException { try { - version = parseHeader(pckIn, VERSION); + try { + version = parseHeader(reader, VERSION); + } catch (EOFException e) { + return; + } + received = true; if (!version.equals(VERSION_0_1)) { throw new PackProtocolException(MessageFormat.format( JGitText.get().pushCertificateInvalidFieldValue, VERSION, version)); } - String rawPusher = parseHeader(pckIn, PUSHER); + String rawPusher = parseHeader(reader, PUSHER); pusher = PushCertificateIdent.parse(rawPusher); if (pusher == null) { throw new PackProtocolException(MessageFormat.format( JGitText.get().pushCertificateInvalidFieldValue, PUSHER, rawPusher)); } - pushee = parseHeader(pckIn, PUSHEE); - receivedNonce = parseHeader(pckIn, NONCE); + pushee = parseHeader(reader, PUSHEE); + receivedNonce = parseHeader(reader, NONCE); + nonceStatus = nonceGenerator != null + ? nonceGenerator.verify( + receivedNonce, sentNonce(), db, stateless, nonceSlopLimit) + : NonceStatus.UNSOLICITED; // An empty line. - if (!pckIn.readString().isEmpty()) { + if (!reader.read().isEmpty()) { throw new PackProtocolException( JGitText.get().pushCertificateInvalidHeader); } @@ -230,10 +346,6 @@ public class PushCertificateParser { throw new PackProtocolException( JGitText.get().pushCertificateInvalidHeader, eof); } - nonceStatus = nonceGenerator != null - ? nonceGenerator.verify( - receivedNonce, sentNonce(), db, stateless, nonceSlopLimit) - : NonceStatus.UNSOLICITED; } /** @@ -251,18 +363,23 @@ public class PushCertificateParser { * @since 4.0 */ public void receiveSignature(PacketLineIn pckIn) throws IOException { + StringReader reader = new PacketLineReader(pckIn); + receiveSignature(reader); + if (!reader.read().equals(END_CERT)) { + throw new PackProtocolException( + JGitText.get().pushCertificateInvalidSignature); + } + } + + private void receiveSignature(StringReader reader) throws IOException { received = true; try { StringBuilder sig = new StringBuilder(BEGIN_SIGNATURE).append('\n'); String line; - while (!(line = pckIn.readString()).equals(END_SIGNATURE)) { + while (!(line = reader.read()).equals(END_SIGNATURE)) { sig.append(line).append('\n'); } signature = sig.append(END_SIGNATURE).append('\n').toString(); - if (!pckIn.readString().equals(END_CERT)) { - throw new PackProtocolException( - JGitText.get().pushCertificateInvalidSignature); - } } catch (EOFException eof) { throw new PackProtocolException( JGitText.get().pushCertificateInvalidSignature, eof);