Prefetch from certificate stream (fixes #127)

Works around a bug in Conscrypt. The certificate stream is not read
there if InputStream.available() returns 0, which is the case in acme4j
since the stream is directly read from the CA via HTTP.

The workaround uses a BufferedInputStream and prefetches a few bytes
from the HTTP stream if available() is invoked.
pull/129/head
Richard Körber 2022-05-07 11:15:16 +02:00
parent df221291e8
commit cf0bfc1390
No known key found for this signature in database
GPG Key ID: AAB9FD19C78AA3E0
2 changed files with 91 additions and 23 deletions

View File

@ -13,17 +13,18 @@
*/ */
package org.shredzone.acme4j.connector; package org.shredzone.acme4j.connector;
import java.io.BufferedInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
/** /**
* Normalizes line separators in an InputStream. Converts all line separators to '\n'. * Normalizes line separators in an InputStream. Converts all line separators to '\n'.
* Multiple line separators are compressed to a single line separator. * Multiple line separators are compressed to a single line separator. Leading line
* separators are removed. Trailing line separators are compressed to a single separator.
*/ */
public class TrimmingInputStream extends InputStream { public class TrimmingInputStream extends InputStream {
private final BufferedInputStream in;
private final InputStream in; private boolean startOfFile = true;
private boolean wasLineSeparator = true;
/** /**
* Creates a new {@link TrimmingInputStream}. * Creates a new {@link TrimmingInputStream}.
@ -33,26 +34,50 @@ public class TrimmingInputStream extends InputStream {
* closed. * closed.
*/ */
public TrimmingInputStream(InputStream in) { public TrimmingInputStream(InputStream in) {
this.in = in; this.in = new BufferedInputStream(in, 1024);
} }
@Override @Override
public int read() throws IOException { public int read() throws IOException {
int ch = in.read(); int ch = in.read();
if (wasLineSeparator) { if (!isLineSeparator(ch)) {
startOfFile = false;
return ch;
}
in.mark(1);
ch = in.read();
while (isLineSeparator(ch)) { while (isLineSeparator(ch)) {
in.mark(1);
ch = in.read(); ch = in.read();
} }
}
wasLineSeparator = isLineSeparator(ch);
if (ch == '\r') {
ch = '\n';
}
if (startOfFile) {
startOfFile = false;
return ch; return ch;
} else {
in.reset();
return '\n';
}
}
@Override
public int available() throws IOException {
// Workaround for https://github.com/google/conscrypt/issues/1068. Conscrypt
// requires the stream to have at least one non-blocking byte available for
// reading, otherwise generateCertificates() will not read the stream, but
// immediately returns an empty list. This workaround pre-fills the buffer
// of the BufferedInputStream by reading 1 byte ahead.
if (in.available() == 0) {
in.mark(1);
int read = in.read();
in.reset();
if (read < 0) {
return 0;
}
}
return in.available();
} }
@Override @Override

View File

@ -15,6 +15,7 @@ package org.shredzone.acme4j.connector;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
@ -26,6 +27,15 @@ import org.junit.Test;
* Unit tests for {@link TrimmingInputStream}. * Unit tests for {@link TrimmingInputStream}.
*/ */
public class TrimmingInputStreamTest { public class TrimmingInputStreamTest {
private final static String FULL_TEXT =
"Gallia est omnis divisa in partes tres,\r\n\r\n\r\n"
+ "quarum unam incolunt Belgae, aliam Aquitani,\r\r\r\n\n"
+ "tertiam, qui ipsorum lingua Celtae, nostra Galli appellantur.";
private final static String TRIMMED_TEXT =
"Gallia est omnis divisa in partes tres,\n"
+ "quarum unam incolunt Belgae, aliam Aquitani,\n"
+ "tertiam, qui ipsorum lingua Celtae, nostra Galli appellantur.";
@Test @Test
public void testEmpty() throws IOException { public void testEmpty() throws IOException {
@ -33,15 +43,48 @@ public class TrimmingInputStreamTest {
assertThat(out, is("")); assertThat(out, is(""));
} }
@Test
public void testLineBreakOnly() throws IOException {
String out1 = trimByStream("\n");
assertThat(out1, is(""));
String out2 = trimByStream("\r");
assertThat(out2, is(""));
String out3 = trimByStream("\r\n");
assertThat(out2, is(""));
}
@Test @Test
public void testTrim() throws IOException { public void testTrim() throws IOException {
String out = trimByStream("\n\n" String out = trimByStream(FULL_TEXT);
+ "Gallia est omnis divisa in partes tres,\r\n\r\n\r\n" assertThat(out, is(TRIMMED_TEXT));
+ "quarum unam incolunt Belgae, aliam Aquitani,\r\r\r\n\n" }
+ "tertiam, qui ipsorum lingua Celtae, nostra Galli appellantur.");
assertThat(out, is("Gallia est omnis divisa in partes tres,\n" @Test
+ "quarum unam incolunt Belgae, aliam Aquitani,\n" public void testTrimEndOnly() throws IOException {
+ "tertiam, qui ipsorum lingua Celtae, nostra Galli appellantur.")); String out = trimByStream(FULL_TEXT + "\r\n\r\n");
assertThat(out, is(TRIMMED_TEXT + "\n"));
}
@Test
public void testTrimStartOnly() throws IOException {
String out = trimByStream("\n\n" + FULL_TEXT);
assertThat(out, is(TRIMMED_TEXT));
}
@Test
public void testTrimFull() throws IOException {
String out = trimByStream("\n\n" + FULL_TEXT + "\r\n\r\n");
assertThat(out, is(TRIMMED_TEXT + "\n"));
}
@Test
public void testAvailable() throws IOException {
try (TrimmingInputStream in = new TrimmingInputStream(
new ByteArrayInputStream("Test".getBytes(StandardCharsets.US_ASCII)))) {
assertThat(in.available(), not(0));
}
} }
/** /**