DefaultConnection implements Connection { @Override public int sendSignedRequest(URL url, JSONBuilder claims, Login login) throws AcmeException { return sendSignedRequest(url, claims, login.getSession(), login.getKeyPair(), login.getAccountLocation(), MIME_JSON); } DefaultConnection(HttpConnector httpConnector); @Override void resetNonce(Session session); @Override int sendRequest(URL url, Session session, @Nullable ZonedDateTime ifModifiedSince); @Override int sendCertificateRequest(URL url, Login login); @Override int sendSignedPostAsGetRequest(URL url, Login login); @Override int sendSignedRequest(URL url, JSONBuilder claims, Login login); @Override int sendSignedRequest(URL url, JSONBuilder claims, Session session, KeyPair keypair); @Override JSON readJsonResponse(); @Override List<X509Certificate> readCertificates(); @Override void handleRetryAfter(String message); @Override @Nullable String getNonce(); @Override @Nullable URL getLocation(); @Override Optional<ZonedDateTime> getLastModified(); @Override Optional<ZonedDateTime> getExpiration(); @Override Collection<URL> getLinks(String relation); @Override void close();  }
@Test public void testAccept() throws Exception { when(mockUrlConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK); when(mockUrlConnection.getOutputStream()).thenReturn(new ByteArrayOutputStream()); session.setNonce(TestUtils.DUMMY_NONCE); try (DefaultConnection conn = new DefaultConnection(mockHttpConnection)) { int rc = conn.sendSignedRequest(requestUrl, new JSONBuilder(), login); assertThat(rc, is(HttpURLConnection.HTTP_OK)); } verify(mockUrlConnection).getResponseCode(); }
@Test public void testAcceptThrowsException() throws Exception { String jsonData = "{\"type\":\"urn:ietf:params:acme:error:unauthorized\",\"detail\":\"Invalid response: 404\"}"; when(mockUrlConnection.getHeaderField("Content-Type")).thenReturn("application/problem+json"); when(mockUrlConnection.getContentLength()).thenReturn(jsonData.length()); when(mockUrlConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_FORBIDDEN); when(mockUrlConnection.getOutputStream()).thenReturn(new ByteArrayOutputStream()); when(mockUrlConnection.getErrorStream()).thenReturn(new ByteArrayInputStream(jsonData.getBytes("utf-8"))); when(mockUrlConnection.getURL()).thenReturn(url("https: session.setNonce(TestUtils.DUMMY_NONCE); try (DefaultConnection conn = new DefaultConnection(mockHttpConnection)) { conn.sendSignedRequest(requestUrl, new JSONBuilder(), login); fail("Expected to fail"); } catch (AcmeUnauthorizedException ex) { assertThat(ex.getType(), is(URI.create("urn:ietf:params:acme:error:unauthorized"))); assertThat(ex.getMessage(), is("Invalid response: 404")); } catch (AcmeException ex) { fail("Expected an AcmeUnauthorizedException"); } verify(mockUrlConnection, atLeastOnce()).getHeaderField("Content-Type"); verify(mockUrlConnection, atLeastOnce()).getResponseCode(); verify(mockUrlConnection).getContentLength(); verify(mockUrlConnection).getErrorStream(); verify(mockUrlConnection).getURL(); }
@Test public void testAcceptThrowsUserActionRequiredException() throws Exception { String jsonData = "{\"type\":\"urn:ietf:params:acme:error:userActionRequired\",\"detail\":\"Accept the TOS\"}"; Map<String, List<String>> linkHeader = new HashMap<>(); linkHeader.put("Link", Arrays.asList("<https: when(mockUrlConnection.getHeaderField("Content-Type")).thenReturn("application/problem+json"); when(mockUrlConnection.getContentLength()).thenReturn(jsonData.length()); when(mockUrlConnection.getHeaderFields()).thenReturn(linkHeader); when(mockUrlConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_FORBIDDEN); when(mockUrlConnection.getOutputStream()).thenReturn(new ByteArrayOutputStream()); when(mockUrlConnection.getErrorStream()).thenReturn(new ByteArrayInputStream(jsonData.getBytes("utf-8"))); when(mockUrlConnection.getURL()).thenReturn(url("https: session.setNonce(TestUtils.DUMMY_NONCE); try (DefaultConnection conn = new DefaultConnection(mockHttpConnection)) { conn.sendSignedRequest(requestUrl, new JSONBuilder(), login); fail("Expected to fail"); } catch (AcmeUserActionRequiredException ex) { assertThat(ex.getType(), is(URI.create("urn:ietf:params:acme:error:userActionRequired"))); assertThat(ex.getMessage(), is("Accept the TOS")); assertThat(ex.getTermsOfServiceUri(), is(URI.create("https: } catch (AcmeException ex) { fail("Expected an AcmeUserActionRequiredException"); } verify(mockUrlConnection, atLeastOnce()).getHeaderField("Content-Type"); verify(mockUrlConnection, atLeastOnce()).getHeaderFields(); verify(mockUrlConnection, atLeastOnce()).getResponseCode(); verify(mockUrlConnection).getErrorStream(); verify(mockUrlConnection).getContentLength(); verify(mockUrlConnection, atLeastOnce()).getURL(); }
@Test public void testAcceptThrowsRateLimitedException() throws Exception { String jsonData = "{\"type\":\"urn:ietf:params:acme:error:rateLimited\",\"detail\":\"Too many invocations\"}"; Map<String, List<String>> linkHeader = new HashMap<>(); linkHeader.put("Link", Arrays.asList("<https: Instant retryAfter = Instant.now().plusSeconds(30L).truncatedTo(ChronoUnit.MILLIS); when(mockUrlConnection.getHeaderField("Content-Type")).thenReturn("application/problem+json"); when(mockUrlConnection.getContentLength()).thenReturn(jsonData.length()); when(mockUrlConnection.getHeaderField("Retry-After")).thenReturn(retryAfter.toString()); when(mockUrlConnection.getHeaderFieldDate("Retry-After", 0L)).thenReturn(retryAfter.toEpochMilli()); when(mockUrlConnection.getHeaderFields()).thenReturn(linkHeader); when(mockUrlConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_FORBIDDEN); when(mockUrlConnection.getOutputStream()).thenReturn(new ByteArrayOutputStream()); when(mockUrlConnection.getErrorStream()).thenReturn(new ByteArrayInputStream(jsonData.getBytes("utf-8"))); when(mockUrlConnection.getURL()).thenReturn(url("https: session.setNonce(TestUtils.DUMMY_NONCE); try (DefaultConnection conn = new DefaultConnection(mockHttpConnection)) { conn.sendSignedRequest(requestUrl, new JSONBuilder(), login); fail("Expected to fail"); } catch (AcmeRateLimitedException ex) { assertThat(ex.getType(), is(URI.create("urn:ietf:params:acme:error:rateLimited"))); assertThat(ex.getMessage(), is("Too many invocations")); assertThat(ex.getRetryAfter(), is(retryAfter)); assertThat(ex.getDocuments(), is(notNullValue())); assertThat(ex.getDocuments().size(), is(1)); assertThat(ex.getDocuments().iterator().next(), is(url("https: } catch (AcmeException ex) { fail("Expected an AcmeRateLimitedException"); } verify(mockUrlConnection, atLeastOnce()).getHeaderField("Content-Type"); verify(mockUrlConnection, atLeastOnce()).getHeaderField("Retry-After"); verify(mockUrlConnection).getHeaderFieldDate("Retry-After", 0L); verify(mockUrlConnection, atLeastOnce()).getHeaderFields(); verify(mockUrlConnection, atLeastOnce()).getResponseCode(); verify(mockUrlConnection).getContentLength(); verify(mockUrlConnection).getErrorStream(); verify(mockUrlConnection, atLeastOnce()).getURL(); }
@Test public void testAcceptThrowsOtherException() throws IOException { when(mockUrlConnection.getHeaderField("Content-Type")) .thenReturn("application/problem+json"); when(mockUrlConnection.getResponseCode()) .thenReturn(HttpURLConnection.HTTP_INTERNAL_ERROR); when(mockUrlConnection.getURL()) .thenReturn(url("https: when(mockUrlConnection.getOutputStream()) .thenReturn(new ByteArrayOutputStream()); session.setNonce(TestUtils.DUMMY_NONCE); try (DefaultConnection conn = new DefaultConnection(mockHttpConnection) { @Override public JSON readJsonResponse() { JSONBuilder result = new JSONBuilder(); result.put("type", "urn:zombie:error:apocalypse"); result.put("detail", "Zombie apocalypse in progress"); return result.toJSON(); } }) { conn.sendSignedRequest(requestUrl, new JSONBuilder(), login); fail("Expected to fail"); } catch (AcmeServerException ex) { assertThat(ex.getType(), is(URI.create("urn:zombie:error:apocalypse"))); assertThat(ex.getMessage(), is("Zombie apocalypse in progress")); } catch (AcmeException ex) { fail("Expected an AcmeServerException"); } verify(mockUrlConnection).getHeaderField("Content-Type"); verify(mockUrlConnection, atLeastOnce()).getResponseCode(); verify(mockUrlConnection).getURL(); }
@Test public void testAcceptThrowsNoTypeException() throws IOException { when(mockUrlConnection.getHeaderField("Content-Type")) .thenReturn("application/problem+json"); when(mockUrlConnection.getResponseCode()) .thenReturn(HttpURLConnection.HTTP_INTERNAL_ERROR); when(mockUrlConnection.getURL()) .thenReturn(url("https: when(mockUrlConnection.getOutputStream()) .thenReturn(new ByteArrayOutputStream()); session.setNonce(TestUtils.DUMMY_NONCE); try (DefaultConnection conn = new DefaultConnection(mockHttpConnection) { @Override public JSON readJsonResponse() { return JSON.empty(); } }) { conn.sendSignedRequest(requestUrl, new JSONBuilder(), login); fail("Expected to fail"); } catch (AcmeProtocolException ex) { assertThat(ex.getMessage(), not(emptyOrNullString())); } catch (AcmeException ex) { fail("Did not expect an AcmeException"); } verify(mockUrlConnection).getHeaderField("Content-Type"); verify(mockUrlConnection, atLeastOnce()).getResponseCode(); verify(mockUrlConnection).getURL(); }
@Test public void testAcceptThrowsServerException() throws IOException { when(mockUrlConnection.getHeaderField("Content-Type")) .thenReturn("text/html"); when(mockUrlConnection.getResponseCode()) .thenReturn(HttpURLConnection.HTTP_INTERNAL_ERROR); when(mockUrlConnection.getResponseMessage()) .thenReturn("Infernal Server Error"); when(mockUrlConnection.getOutputStream()) .thenReturn(new ByteArrayOutputStream()); session.setNonce(TestUtils.DUMMY_NONCE); try (DefaultConnection conn = new DefaultConnection(mockHttpConnection)) { conn.sendSignedRequest(requestUrl, new JSONBuilder(), login); fail("Expected to fail"); } catch (AcmeException ex) { assertThat(ex.getMessage(), is("HTTP 500: Infernal Server Error")); } verify(mockUrlConnection).getHeaderField("Content-Type"); verify(mockUrlConnection, atLeastOnce()).getResponseCode(); verify(mockUrlConnection, atLeastOnce()).getResponseMessage(); }
@Test public void testSendSignedRequest() throws Exception { final String nonce1 = URL_ENCODER.encodeToString("foo-nonce-1-foo".getBytes()); final String nonce2 = URL_ENCODER.encodeToString("foo-nonce-2-foo".getBytes()); final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); when(mockUrlConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK); when(mockUrlConnection.getOutputStream()).thenReturn(outputStream); try (DefaultConnection conn = new DefaultConnection(mockHttpConnection) { @Override public void resetNonce(Session session) { assertThat(session, is(sameInstance(DefaultConnectionTest.this.session))); if (session.getNonce() == null) { session.setNonce(nonce1); } else { fail("unknown nonce"); } } @Override public String getNonce() { assertThat(session, is(sameInstance(DefaultConnectionTest.this.session))); if (session.getNonce() == nonce1) { return nonce2; } else { fail("unknown nonce"); return null; } } }) { JSONBuilder cb = new JSONBuilder(); cb.put("foo", 123).put("bar", "a-string"); conn.sendSignedRequest(requestUrl, cb, login); } verify(mockUrlConnection).setRequestMethod("POST"); verify(mockUrlConnection).setRequestProperty("Accept", "application/json"); verify(mockUrlConnection).setRequestProperty("Accept-Charset", "utf-8"); verify(mockUrlConnection).setRequestProperty("Accept-Language", "ja-JP"); verify(mockUrlConnection).setRequestProperty("Content-Type", "application/jose+json"); verify(mockUrlConnection).connect(); verify(mockUrlConnection).setDoOutput(true); verify(mockUrlConnection).setFixedLengthStreamingMode(outputStream.toByteArray().length); verify(mockUrlConnection).getResponseCode(); verify(mockUrlConnection).getOutputStream(); verify(mockUrlConnection, atLeast(0)).getHeaderFields(); verifyNoMoreInteractions(mockUrlConnection); JSON data = JSON.parse(new String(outputStream.toByteArray(), "utf-8")); String encodedHeader = data.get("protected").asString(); String encodedSignature = data.get("signature").asString(); String encodedPayload = data.get("payload").asString(); StringBuilder expectedHeader = new StringBuilder(); expectedHeader.append('{'); expectedHeader.append("\"nonce\":\"").append(nonce1).append("\","); expectedHeader.append("\"url\":\"").append(requestUrl).append("\","); expectedHeader.append("\"alg\":\"RS256\","); expectedHeader.append("\"kid\":\"").append(accountUrl).append('"'); expectedHeader.append('}'); assertThat(new String(URL_DECODER.decode(encodedHeader), UTF_8), sameJSONAs(expectedHeader.toString())); assertThat(new String(URL_DECODER.decode(encodedPayload), UTF_8), sameJSONAs("{\"foo\":123,\"bar\":\"a-string\"}")); assertThat(encodedSignature, not(emptyOrNullString())); JsonWebSignature jws = new JsonWebSignature(); jws.setCompactSerialization(CompactSerializer.serialize(encodedHeader, encodedPayload, encodedSignature)); jws.setKey(login.getKeyPair().getPublic()); assertThat(jws.verifySignature(), is(true)); }
@Test(expected = AcmeException.class) public void testSendSignedRequestNoNonce() throws Exception { when(mockHttpConnection.openConnection(eq(new URL("https: .thenReturn(mockUrlConnection); when(mockUrlConnection.getResponseCode()) .thenReturn(HttpURLConnection.HTTP_NOT_FOUND); try (DefaultConnection conn = new DefaultConnection(mockHttpConnection)) { JSONBuilder cb = new JSONBuilder(); conn.sendSignedRequest(requestUrl, cb, DefaultConnectionTest.this.session, DefaultConnectionTest.this.keyPair); } }