io.macgyver.plugin.elb.a10.A10ClientImpl.java Source code

Java tutorial

Introduction

Here is the source code for io.macgyver.plugin.elb.a10.A10ClientImpl.java

Source

/**
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.macgyver.plugin.elb.a10;

import io.macgyver.core.okhttp.LoggingInterceptor;
import io.macgyver.plugin.elb.ElbException;

import java.io.IOException;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.GeneralSecurityException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.tomcat.util.net.URL;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;
import org.jdom2.output.Format;
import org.jdom2.output.XMLOutputter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.MoreObjects;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.squareup.okhttp.ConnectionSpec;
import com.squareup.okhttp.ConnectionSpec.Builder;
import com.squareup.okhttp.FormEncodingBuilder;
import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.Response;
import com.squareup.okhttp.TlsVersion;

public class A10ClientImpl implements A10Client {

    public static final String A10_AUTH_TOKEN_KEY = "token";
    Logger logger = LoggerFactory.getLogger(A10ClientImpl.class);
    private String username;
    private String password;
    private String url;
    LoadingCache<String, String> tokenCache;

    public static final int DEFAULT_TOKEN_CACHE_DURATION = 5;
    private static final TimeUnit DEFAULT_TOKEN_CACHE_DURATION_TIME_UNIT = TimeUnit.MINUTES;

    public static final String UTF8 = "UTF-8";

    public static final String INVALID_SESSION_ID_CODE = "1009";
    ObjectMapper mapper = new ObjectMapper();
    public boolean validateCertificates = true;

    boolean immutable = false;

    public A10ClientImpl(String url, String username, String password) {
        this.url = normalizeUrl(url);
        this.username = username;
        this.password = password;

        setTokenCacheDuration(DEFAULT_TOKEN_CACHE_DURATION, DEFAULT_TOKEN_CACHE_DURATION_TIME_UNIT);
        logger.info("url: {}", this.url);

    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public void setTokenCacheDuration(int duration, TimeUnit timeUnit) {
        Preconditions.checkArgument(duration >= 0, "duration must be >=0");
        Preconditions.checkNotNull(timeUnit, "TimeUnit must be set");

        this.tokenCache = CacheBuilder.newBuilder().expireAfterWrite(duration, timeUnit)
                .build(new TokenCacheLoader());

    }

    public void setCertificateVerificationEnabled(boolean b) {
        validateCertificates = b;
        if (validateCertificates && (!b)) {
            logger.warn("certificate validation disabled");
        }
    }

    void throwExceptionIfNecessary(Element element) {
        if (element.getName().equals("response")) {
            String status = element.getAttributeValue("status");

            if (status.equalsIgnoreCase("ok")) {
                // ok
            } else if (status.equalsIgnoreCase("fail")) {
                String code = "";
                String msg = "";

                Element error = element.getChild("error");
                if (error != null) {
                    code = error.getAttributeValue("code");
                    msg = error.getAttributeValue("msg");
                }
                if (code != null && INVALID_SESSION_ID_CODE.equals(code)) {
                    tokenCache.invalidateAll();
                }
                throw new A10RemoteException(code, msg);
            } else {
                logger.warn("unexpected status: {}", status);
            }

        } else {
            logger.warn("unexpected response element: {}", element.getName());
        }

    }

    void throwExceptionIfNecessary(ObjectNode response) {

        if (response.has("response") && response.get("response").has("err")) {

            String code = response.path("response").path("err").path("code").asText();
            String msg = response.path("response").path("err").path("msg").asText();

            logger.warn("error response: {}", response);
            if (code != null && INVALID_SESSION_ID_CODE.equals(code)) {
                tokenCache.invalidateAll();
            }
            A10RemoteException x = new A10RemoteException(code, msg);
            throw x;

        }

    }

    /**
     * This probably does not have a lot of practical value outside of testing.
     * By forcibly setting the the authentication token, we can prevent an
     * implicit call to authenticate(). This helps simplify mocked server
     * exchanges.
     *
     * @param token
     */
    public void setAuthToken(String token) {
        tokenCache.put(A10_AUTH_TOKEN_KEY, token);
    }

    /**
     * Performs an authentication, caches the resulting authentication token,
     * and returns it.
     *
     * @return
     */
    public String authenticate() {

        try {
            FormEncodingBuilder b = new FormEncodingBuilder();
            b = b.add("username", username).add("password", password).add("format", "json").add("method",
                    "authenticate");

            Request r = new Request.Builder().url(getUrl()).addHeader("Accept", "application/json").post(b.build())
                    .build();
            Response resp = getClient().newCall(r).execute();

            ObjectNode obj = parseJsonResponse(resp, "authenticate");

            String sid = obj.path("session_id").asText();
            if (Strings.isNullOrEmpty(sid)) {
                throw new ElbException("authentication failed");
            }
            tokenCache.put(A10_AUTH_TOKEN_KEY, sid);
            return sid;

        } catch (IOException e) {
            throw new ElbException(e);
        }

    }

    class TokenCacheLoader extends CacheLoader<String, String> {

        @Override
        public String load(String arg0) throws Exception {
            return authenticate();
        }

    }

    protected String getAuthToken() {
        try {
            return tokenCache.get(A10_AUTH_TOKEN_KEY);
        } catch (ExecutionException e) {
            throw new ElbException(e.getCause());
        }

    }

    protected static Map<String, String> toMap(String... args) {
        Map<String, String> m = Maps.newHashMap();
        if (args == null || args.length == 0) {
            return m;
        }
        if (args.length % 2 != 0) {
            throw new IllegalArgumentException("arguments must be in multiples of 2 (key/value)");
        }
        for (int i = 0; i < args.length; i += 2) {
            Preconditions.checkNotNull(args[i]);
            Preconditions.checkNotNull(args[i + 1]);
            m.put(args[i], args[i + 1]);
        }
        return m;
    }

    @Override
    @Deprecated
    public ObjectNode invoke(String method, String... args) {
        return invokeJson(method, args);
    }

    @Override
    @Deprecated
    public ObjectNode invokeJson(String method, String... args) {
        return invokeJson(method, null, toMap(args));
    }

    @Override
    @Deprecated
    public ObjectNode invokeJson(String method, JsonNode body, String... args) {
        return invokeJson(method, body, toMap(args));
    }

    @Override
    @Deprecated
    public Response invokeJsonWithRawResponse(String method, JsonNode body, String... args) {
        return newRequest(method).body(body).params(args).execute();
    }

    @Override
    @Deprecated
    public Element invokeXml(String method, Element body, String... args) {
        return invokeXml(method, body, toMap(args));
    }

    @Override
    @Deprecated
    public Element invokeXml(String method, String... args) {
        return newRequest(method).params(args).executeXml();

    }

    @Override
    @Deprecated
    public Response invokeXmlWithRawResponse(String method, Element body, String... args) {

        return newRequest(method).body(body).params(args).execute();

    }

    @Override
    @Deprecated
    public ObjectNode invoke(String method, Map<String, String> params) {
        return newRequest(method).params(params).executeJson();

    }

    @Override
    @Deprecated
    public ObjectNode invokeJson(String method, Map<String, String> params) {

        return newRequest(method).params(params).executeJson();

    }

    @Override
    @Deprecated
    public ObjectNode invokeJson(String method, JsonNode body, Map<String, String> params) {

        return newRequest(method).body(body).params(params).executeJson();

    }

    @Override
    @Deprecated
    public Response invokeJsonWithRawResponse(String method, JsonNode body, Map<String, String> params) {
        return newRequest(method).body(body).params(params).execute();
    }

    @Override
    @Deprecated
    public Element invokeXml(String method, Map<String, String> params) {

        return newRequest(method).params(params).executeXml();

    }

    @Override
    @Deprecated
    public Element invokeXml(String method, Element body, Map<String, String> params) {
        return newRequest(method).body(body).params(params).executeXml();
    }

    @Override
    @Deprecated
    public Response invokeXmlWithRawResponse(String method, Element body, Map<String, String> params) {
        return newRequest(method).body(body).params(params).execute();
    }

    protected Element parseXmlResponse(Response response, String method) {
        try {
            Document d = new SAXBuilder().build(response.body().charStream());

            return d.getRootElement();
        } catch (IOException | JDOMException e) {
            throw new ElbException(e);
        }
    }

    protected ObjectNode parseJsonResponse(Response response, String method) {
        try {
            Preconditions.checkNotNull(response);

            String contentType = response.header("Content-type");

            // aXAPI is very sketchy with regard to content type of response.
            // Sometimes we get XML/HTML back even though
            // we ask for JSON. This hack helps figure out what is going on.

            String val = response.body().string().trim();
            if (!val.startsWith("{") && !val.startsWith("[")) {
                throw new ElbException("response contained non-JSON data: " + val);
            }

            ObjectNode json = (ObjectNode) mapper.readTree(val);

            if (logger.isDebugEnabled()) {

                String body = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(json);
                logger.debug("response: \n{}", body);
            }
            throwExceptionIfNecessary(json);
            return json;
        } catch (IOException e) {
            throw new ElbException(e);
        }
    }

    AtomicReference<OkHttpClient> clientReference = new AtomicReference<OkHttpClient>();

    protected OkHttpClient getClient() {

        // not guaranteed to be singleton, but close enough
        if (clientReference.get() == null) {
            OkHttpClient c = new OkHttpClient();

            c.setConnectTimeout(20, TimeUnit.SECONDS);

            c.setHostnameVerifier(withoutHostnameVerification());
            c.setSslSocketFactory(withoutCertificateValidation().getSocketFactory());

            c.setConnectionSpecs(getA10CompatibleConnectionSpecs());
            c.interceptors().add(LoggingInterceptor.create(A10ClientImpl.class));
            clientReference.set(c);
        }
        return clientReference.get();
    }

    public static HostnameVerifier withoutHostnameVerification() {
        HostnameVerifier verifier = new HostnameVerifier() {

            @Override
            public boolean verify(String hostname, SSLSession session) {
                // TODO Auto-generated method stub
                return true;
            }
        };
        return verifier;
    }

    static AtomicReference<SSLContext> trustAllContext = new AtomicReference<>();

    public static synchronized SSLContext withoutCertificateValidation() {
        try {
            SSLContext sslContext = trustAllContext.get();
            if (sslContext != null) {
                return sslContext;
            }
            TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {

                @Override
                public X509Certificate[] getAcceptedIssuers() {

                    return null;
                }

                @Override
                public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {

                }

                @Override
                public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {

                }
            } };

            // A10 management port seems to implement a fairly broken HTTPS
            // stack which
            // does not support re-negotiation
            sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, trustAllCerts, new java.security.SecureRandom());

            trustAllContext.set(sslContext);
            return sslContext;
        } catch (GeneralSecurityException e) {
            throw new RuntimeException(e);
        }

    }

    /**
     * The A10 control port has a brain-dead HTTPS stack that is unable to
     * negotiate TLS versions.
     *
     * @return
     */
    List<ConnectionSpec> getA10CompatibleConnectionSpecs() {
        List<ConnectionSpec> list = Lists.newArrayList();

        list.add(new Builder(ConnectionSpec.MODERN_TLS).tlsVersions(TlsVersion.TLS_1_0).build()); // This
        // is
        // essential
        list.add(ConnectionSpec.MODERN_TLS);
        list.add(ConnectionSpec.CLEARTEXT);
        return ImmutableList.copyOf(list);
    }

    @Override
    public boolean isActive() {
        try {
            Element e = invokeXml("ha.group.fetchStatistics");

            Element statusListElement = e.getChild("ha_group_status_list");
            if (statusListElement == null || statusListElement.getChildren().isEmpty()) {

                return true;
            }
            String x = statusListElement.getChildren().get(0).getChild("local_status").getText();
            if (Strings.nullToEmpty(x).trim().equals("1")) {
                return true;
            }
            return false;

        } catch (ElbException e) {
            throw e;
        } catch (RuntimeException e) {
            throw new ElbException(e);
        }
    }

    @Override
    public String toString() {
        return MoreObjects.toStringHelper(this).add("url", url).toString();
    }

    protected String formatUrl(Map<String, String> x, String format) {
        try {
            StringBuffer sb = new StringBuffer();
            sb.append(String.format("%s?session_id=%s&format=%s", getUrl(), URLEncoder.encode(getAuthToken(), UTF8),
                    URLEncoder.encode(format, UTF8)));

            for (String key : x.keySet()) {
                if (!key.equals("format")) {
                    String val = x.get(key);
                    sb.append(String.format("&%s=%s", URLEncoder.encode(key, UTF8), URLEncoder.encode(val, UTF8)));

                }
            }

            return sb.toString();
        } catch (UnsupportedEncodingException e) {
            throw new ElbException(e);
        }
    }

    public RequestBuilder newRequest(String method) {
        RequestBuilder b = new RequestBuilder(this, method);

        return b;
    }

    protected String normalizeUrl(String url) {

        Preconditions.checkNotNull(url);
        Preconditions.checkArgument(url.startsWith("http://") || url.startsWith("https://"), "url must be http(s)");
        try {

            URL u = new URL(url);

            String normalized = u.getProtocol() + "://" + u.getHost() + ((u.getPort() > 0) ? ":" + u.getPort() : "")
                    + "/services/rest/v2/";

            return normalized;
        } catch (IOException e) {
            throw new IllegalArgumentException("invalid url: " + url);
        }
    }

    public ObjectNode executeJson(RequestBuilder b) {
        ObjectNode n = parseJsonResponse(execute(b), b.getMethod());
        throwExceptionIfNecessary(n);
        return n;

    }

    public Element executeXml(RequestBuilder b) {
        Element element = parseXmlResponse(execute(b), b.getMethod());
        throwExceptionIfNecessary(element);
        return element;

    }

    public Response execute(RequestBuilder b) {
        try {

            String method = b.getMethod();
            Preconditions.checkArgument(!Strings.isNullOrEmpty(method), "method argument must be passed");

            String url = null;

            Response resp;
            String format = b.getParams().getOrDefault("format", "xml");
            b = b.param("format", format);
            if (!b.hasBody()) {

                url = formatUrl(b.getParams(), format);
                FormEncodingBuilder fb = new FormEncodingBuilder().add("session_id", getAuthToken());
                for (Map.Entry<String, String> entry : b.getParams().entrySet()) {
                    if (!entry.getValue().equals("format")) {
                        fb = fb.add(entry.getKey(), entry.getValue());
                    }
                }
                resp = getClient().newCall(new Request.Builder().url(getUrl()).post(fb.build()).build()).execute();
            } else if (b.getXmlBody().isPresent()) {
                b = b.param("format", "xml");
                url = formatUrl(b.getParams(), "xml");
                String bodyAsString = new XMLOutputter(Format.getRawFormat()).outputString(b.getXmlBody().get());
                final MediaType XML = MediaType.parse("text/xml");

                resp = getClient().newCall(new Request.Builder().url(url)
                        .post(RequestBody.create(XML, bodyAsString)).header("Content-Type", "text/xml").build())
                        .execute();
            } else if (b.getJsonBody().isPresent()) {
                b = b.param("format", "json");
                url = formatUrl(b.getParams(), "json");
                String bodyAsString = mapper.writeValueAsString(b.getJsonBody().get());

                final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
                resp = getClient().newCall(new Request.Builder().url(url).post(

                        RequestBody.create(JSON, bodyAsString)).header("Content-Type", "application/json").build())
                        .execute();
            } else {
                throw new UnsupportedOperationException("body type not supported");
            }
            // the A10 API rather stupidly uses 200 responses even when there is
            // an error
            if (!resp.isSuccessful()) {
                logger.warn("response code={}", resp.code());
            }

            return resp;

        } catch (IOException e) {
            throw new ElbException(e);
        }
    }

}