001/*
002 * acme4j - Java ACME client
003 *
004 * Copyright (C) 2017 Richard "Shred" Körber
005 *   http://acme4j.shredzone.org
006 *
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
013 */
014package org.shredzone.acme4j.it;
015
016import java.io.IOException;
017import java.util.Collections;
018import java.util.Objects;
019
020import org.apache.http.HttpStatus;
021import org.apache.http.client.ClientProtocolException;
022import org.apache.http.client.HttpClient;
023import org.apache.http.client.methods.HttpPost;
024import org.apache.http.entity.ContentType;
025import org.apache.http.entity.StringEntity;
026import org.apache.http.impl.client.HttpClients;
027import org.apache.http.util.EntityUtils;
028import org.shredzone.acme4j.toolbox.JSONBuilder;
029
030/**
031 * The BammBamm client connects to the pebble-challtestsrv.
032 */
033public class BammBammClient {
034    private static final HttpClient CLIENT = HttpClients.createDefault();
035
036    private final String baseUrl;
037
038    /**
039     * Creates a new BammBamm client.
040     *
041     * @param baseUrl
042     *            Base URL of the pebble-challtestsrv server to connect to.
043     */
044    public BammBammClient(String baseUrl) {
045        this.baseUrl = Objects.requireNonNull(baseUrl) + '/';
046    }
047
048    /**
049     * Adds a HTTP token.
050     *
051     * @param token
052     *            Token to add
053     * @param challenge
054     *            Challenge to respond with
055     */
056    public void httpAddToken(String token, String challenge) throws IOException {
057        var jb = new JSONBuilder();
058        jb.put("token", token);
059        jb.put("content", challenge);
060        sendRequest("add-http01", jb.toString());
061    }
062
063    /**
064     * Removes a HTTP token.
065     *
066     * @param token
067     *            Token to remove
068     */
069    public void httpRemoveToken(String token) throws IOException {
070        var jb = new JSONBuilder();
071        jb.put("token", token);
072        sendRequest("del-http01", jb.toString());
073    }
074
075    /**
076     * Adds an A Record to the DNS. Only one A Record is supported per domain. If another
077     * A Record is set, it will replace the existing one.
078     *
079     * @param domain
080     *            Domain of the A Record
081     * @param ip
082     *            IP address or domain name. If a domain name is used, it will be resolved
083     *            and the IP will be used.
084     */
085    public void dnsAddARecord(String domain, String ip) throws IOException {
086        var jb = new JSONBuilder();
087        jb.put("host", domain);
088        jb.array("addresses", Collections.singletonList(ip));
089        sendRequest("add-a", jb.toString());
090    }
091
092    /**
093     * Removes an A Record from the DNS.
094     *
095     * @param domain
096     *            Domain to remove the A Record from
097     */
098    public void dnsRemoveARecord(String domain) throws IOException {
099        var jb = new JSONBuilder();
100        jb.put("host", domain);
101        sendRequest("clear-a", jb.toString());
102    }
103
104    /**
105     * Adds a TXT Record to the DNS. Only one TXT Record is supported per domain. If
106     * another TXT Record is set, it will replace the existing one.
107     *
108     * @param domain
109     *            Domain name to add the TXT Record to
110     * @param txt
111     *            TXT record to add
112     */
113    public void dnsAddTxtRecord(String domain, String txt) throws IOException {
114        var jb = new JSONBuilder();
115        jb.put("host", domain);
116        jb.put("value", txt);
117        sendRequest("set-txt", jb.toString());
118    }
119
120    /**
121     * Removes a TXT Record from the DNS.
122     *
123     * @param domain
124     *            Domain to remove the TXT Record from
125     */
126    public void dnsRemoveTxtRecord(String domain) throws IOException {
127        var jb = new JSONBuilder();
128        jb.put("host", domain + '.');
129        sendRequest("clear-txt", jb.toString());
130    }
131
132    /**
133     * Adds a CNAME Record to the DNS. Only one CNAME Record is supported per domain. If
134     * another CNAME Record is set, it will replace the existing one.
135     *
136     * @param domain
137     *         Domain to add the CNAME Record to
138     * @param cname
139     *         CNAME Record to add
140     * @since 2.9
141     */
142    public void dnsAddCnameRecord(String domain, String cname) throws IOException {
143        var jb = new JSONBuilder();
144        jb.put("host", domain);
145        jb.put("target", cname);
146        sendRequest("set-cname", jb.toString());
147    }
148
149    /**
150     * Removes a CNAME Record from the DNS.
151     *
152     * @param domain
153     *         Domain to remove the CNAME Record from
154     * @since 2.9
155     */
156    public void dnsRemoveCnameRecord(String domain) throws IOException {
157        var jb = new JSONBuilder();
158        jb.put("host", domain);
159        sendRequest("clear-cname", jb.toString());
160    }
161
162    /**
163     * Simulates a SERVFAIL for the given domain.
164     *
165     * @param domain
166     *         Domain that will give a SERVFAIL response
167     * @since 2.9
168     */
169    public void dnsAddServFailRecord(String domain) throws IOException {
170        var jb = new JSONBuilder();
171        jb.put("host", domain);
172        sendRequest("set-servfail", jb.toString());
173    }
174
175    /**
176     * Removes a SERVFAIL Record from the DNS.
177     *
178     * @param domain
179     *         Domain to remove the SEVFAIL Record from
180     * @since 2.9
181     */
182    public void dnsRemoveServFailRecord(String domain) throws IOException {
183        var jb = new JSONBuilder();
184        jb.put("host", domain);
185        sendRequest("clear-servfail", jb.toString());
186    }
187
188    /**
189     * Adds a certificate for TLS-ALPN tests.
190     *
191     * @param domain
192     *            Certificate domain to be added
193     * @param keyauth
194     *            Key authorization to be used for validation
195     */
196    public void tlsAlpnAddCertificate(String domain, String keyauth) throws IOException {
197        var jb = new JSONBuilder();
198        jb.put("host", domain);
199        jb.put("content", keyauth);
200        sendRequest("add-tlsalpn01", jb.toString());
201    }
202
203    /**
204     * Removes a certificate.
205     *
206     * @param domain
207     *            Certificate domain to be removed
208     */
209    public void tlsAlpnRemoveCertificate(String domain) throws IOException {
210        var jb = new JSONBuilder();
211        jb.put("host", domain);
212        sendRequest("del-tlsalpn01", jb.toString());
213    }
214
215    /**
216     * Sends a request to the pebble-challtestsrv.
217     *
218     * @param call
219     *            Endpoint to be called
220     * @param body
221     *            JSON body
222     */
223    private void sendRequest(String call, String body) throws IOException {
224        try {
225            var httppost = new HttpPost(baseUrl + call);
226            httppost.setEntity(new StringEntity(body, ContentType.APPLICATION_JSON));
227
228            var response = CLIENT.execute(httppost);
229
230            EntityUtils.consume(response.getEntity());
231
232            if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
233                throw new IOException(response.getStatusLine().getReasonPhrase());
234            }
235        } catch (ClientProtocolException ex) {
236            throw new IOException(ex);
237        }
238    }
239
240}