();
+ ipsChain.addAll(xForwardedFor.proxies());
+ ipsChain.add(requestIp.getHostAddress());
+ return new ProxyChain(ipsChain);
+ }
+
+ /**
+ *
+ * In case of X-Forwarded-For set, the ip of the remote client relative
+ * to the server is defined as follows:
+ *
+ * by default the remote client ip is the first ip of the X-Forwarded-For.
+ * If there is a sub proxy chain in the X-Forwarded-For that is trusted, the
+ * client ip is the ip that precedes the starting of the trusted subchain.
+ * example:
+ *
+ * a X-Forwarded-For value "1.1.1.1,2.2.2.2,3.3.3.3" with "3.3.3.3" as
+ * trusted proxy chain will have the "3.3.3.3" subchain trusted. This
+ * determines "2.2.2.2" as the server's remote client
+ *
+ * @return the remote client's ip relative to the server
+ */
+ private String remoteClientIp() {
+ String clientIp = xForwardedFor.client();
+ if (trustedChain() != null) {
+ List trustedProxies = trustedChain().getProxyChain();
+ List proxies = xForwardedFor.proxies();
+ proxies.removeAll(trustedProxies);
+ if (proxies.size() > 0) {
+ clientIp = proxies.get(proxies.size() - 1);
+ }
+ }
+ return clientIp;
+ }
+
+ @Override
+ public String toString() {
+ String addr = requestIp.getHostAddress();
+ String s = "client with request ip " + addr
+ + (xForwardedFor.isSet() ? ", remoteIp: " + remoteClientIp() : "")
+ + " is:"
+ + (authorized ? "Authorized" : "NotAuthorized")
+ + ", "
+ + (trusted ? "Trusted" : "NotTrusted")
+ + " (X-Forwarded-For: "
+ + xForwardedFor
+ + "), "
+ + (whitelisted ? "Whitelisted" : "NotWhitelisted");
+ return s;
+ }
+
+ /**
+ * @return the trusted
+ */
+ public boolean isTrusted() {
+ return trusted;
+ }
+
+ /**
+ * @return the whitelisted
+ */
+ public boolean isWhitelisted() {
+ return whitelisted;
+ }
+
+ /**
+ * @return the authorized
+ */
+ public boolean isAuthorized() {
+ return authorized;
+ }
+}
diff --git a/src/main/java/com/asquera/elasticsearch/plugins/http/auth/InetAddressWhitelist.java b/src/main/java/com/asquera/elasticsearch/plugins/http/auth/InetAddressWhitelist.java
new file mode 100644
index 0000000..dd416f2
--- /dev/null
+++ b/src/main/java/com/asquera/elasticsearch/plugins/http/auth/InetAddressWhitelist.java
@@ -0,0 +1,116 @@
+package com.asquera.elasticsearch.plugins.http.auth;
+import org.elasticsearch.common.logging.Loggers;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.Iterator;
+import java.util.Arrays;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/**
+ *
+ * Wraps the configured whitelisted ips.
+ * It uses a set of {@link InetAddress} internally.
+ *
+ *
+ *
+ *
+ * @author Ernesto Miguez (ernesto.miguez@asquera.de)
+ */
+
+public class InetAddressWhitelist {
+ private Set whitelist;
+ /**
+ *
+ *
+ * @param whitelist
+ */
+ public InetAddressWhitelist(Set whitelist) {
+ this.whitelist = whitelist;
+ }
+
+ /**
+ *
+ *
+ * @param sWhitelist
+ *
+ */
+ public InetAddressWhitelist(String[] sWhitelist) {
+ this(toInetAddress(Arrays.asList(sWhitelist)));
+ }
+
+ /**
+ * Checks the request ip for inclusion.
+ * Since that ip comes in a {@link InetAddress} representation, it is checked
+ * against the whitelist.
+ *
+ * @param candidate
+ * @return if the ip is included in the whitelist
+ */
+ public Boolean contains(InetAddress candidate) {
+ return this.whitelist.contains(candidate);
+ }
+
+ /**
+ *
+ * Checks the xForwardedFor defined client ip for inclusion.
+ * Since that ip comes in a String representation, it is checked against
+ * the String representation of the defined whitelist.
+ *
+ * @param candidate
+ * @return if the ip is included in the String representation of the
+ * whitelist ips
+ */
+ public Boolean contains(String candidate) {
+ return getStringWhitelist().contains(candidate);
+ }
+
+ /**
+ * @return set of the string representations of the whitelist
+ */
+ Set getStringWhitelist() {
+ Iterator iterator = this.whitelist.iterator();
+ Set set = new HashSet();
+ while (iterator.hasNext()) {
+ InetAddress next = iterator.next();
+ set.add(next.getHostAddress());
+ }
+ return set;
+ }
+
+ /**
+ * when an configured InetAddress is Unkown or Invalid it is dropped from the
+ * whitelist
+ *
+ * @param ips a list of string ips
+ * @return a list of {@link InetAddress} objects
+ *
+ */
+ static Set toInetAddress(List ips) {
+ List listIps = new ArrayList();
+ Iterator iterator = ips.iterator();
+ while (iterator.hasNext()) {
+ String next = iterator.next();
+ try {
+ listIps.add(InetAddress.getByName(next));
+ } catch (UnknownHostException e) {
+ String template = "an ip set in the whitelist settings raised an " +
+ "UnknownHostException: {}, dropping it";
+ Loggers.getLogger(InetAddressWhitelist.class).info(template, e.getMessage());
+ }
+ }
+ return new HashSet(listIps);
+ }
+
+ /**
+ * delegate method
+ */
+ @Override
+ public String toString() {
+ return whitelist.toString();
+ }
+
+}
diff --git a/src/main/java/com/asquera/elasticsearch/plugins/http/auth/ProxyChain.java b/src/main/java/com/asquera/elasticsearch/plugins/http/auth/ProxyChain.java
new file mode 100644
index 0000000..34ae434
--- /dev/null
+++ b/src/main/java/com/asquera/elasticsearch/plugins/http/auth/ProxyChain.java
@@ -0,0 +1,104 @@
+package com.asquera.elasticsearch.plugins.http.auth;
+
+import java.util.*;
+
+/**
+ *
+ *
+ * This class wraps an ip chain (an ordered list of ips).
+ *
+ * @author Ernesto Miguez (ernesto.miguez@asquera.de)
+ *
+ */
+public class ProxyChain {
+ private List proxyChain;
+
+ public ProxyChain() {
+ this.proxyChain = new ArrayList();
+ }
+
+ public ProxyChain(List proxyChain) {
+ this.proxyChain = proxyChain;
+ }
+
+ public ProxyChain(String proxyChain) {
+ this(new ArrayList(Arrays.asList(proxyChain.split(","))));
+ }
+
+
+ /**
+ * @return the proxy chain
+ */
+ public List getProxyChain() {
+ return proxyChain;
+ }
+
+ /**
+ * A subchain is every segment of the list matching by the tail, included
+ * itself. example:
+ * "1.1.1.1,2.2.2.2" is trusted by trusted list "3.3.3.3,4.4.4.4,2.2.2.2" since the
+ * subchain "2.2.2.2" is included in a subchain of the trusted list.
+ *
+ * @return a new {@link ProxyChain} instance having all the subchains of the
+ * present instance
+ */
+ public ProxyChains subchains() {
+ List reversedIps = new ArrayList(proxyChain);
+ Collections.reverse(reversedIps);
+ ListIterator iterator = reversedIps.listIterator();
+ ProxyChains subchains = new ProxyChains((Set)new HashSet());
+ ProxyChain subChain = new ProxyChain(new ArrayList());
+ while (iterator.hasNext()) {
+ String next = iterator.next();
+ subChain.add(next);
+ List r = new ArrayList(subChain.getProxyChain());
+ Collections.reverse(r);
+ subchains.add( new ProxyChain(r));
+ }
+ return subchains;
+ }
+
+ /**
+ * delegated method
+ * @param o
+ * @see List#add(Object o);
+ */
+ public void add (Object o) {
+ proxyChain.add((String)o);
+ }
+
+ /**
+ * delegated method
+ * @see List#toString();
+ */
+ @Override
+ public String toString() {
+ return proxyChain.toString();
+ }
+
+ /**
+ * delegated method
+ * @see List#equals();
+ */
+ @Override
+ public boolean equals(Object c) {
+ return proxyChain.equals(((ProxyChain)c).getProxyChain());
+ }
+
+ /**
+ * delegated method
+ * @see List#hashCode();
+ */
+ @Override
+ public int hashCode() {
+ return proxyChain.hashCode();
+ }
+
+ /**
+ * delegated method
+ * @see List#size();
+ */
+ public int size() {
+ return proxyChain.size();
+ }
+}
diff --git a/src/main/java/com/asquera/elasticsearch/plugins/http/auth/ProxyChains.java b/src/main/java/com/asquera/elasticsearch/plugins/http/auth/ProxyChains.java
new file mode 100644
index 0000000..5a23363
--- /dev/null
+++ b/src/main/java/com/asquera/elasticsearch/plugins/http/auth/ProxyChains.java
@@ -0,0 +1,144 @@
+package com.asquera.elasticsearch.plugins.http.auth;
+
+import java.util.*;
+
+/**
+ * This class wraps a set of {@link ProxyChain}
+ *
+ * @author Ernesto Miguez (ernesto.miguez@asquera.de)
+ **/
+
+public class ProxyChains {
+
+ private Set proxyChains;
+
+ public ProxyChains(Set proxyChains) {
+ this.proxyChains = proxyChains;
+ }
+
+ public ProxyChains(String[] proxyChains) {
+ this(getProxies(proxyChains));
+ }
+
+ /**
+ *
+ * An ip chain is trusted iff any of it subchains is contained in
+ * any of the instance subchains
+ *
+ * @param candidate the ip list to check
+ * @return true iff the candidate is included
+ */
+ public Boolean trusts(ProxyChain candidate) {
+ return trustedSubchain(candidate) != null;
+ }
+
+ /**
+ *
+ * Find the trusted subchain if any. note: Any chain is included in its subchains
+ *
+ * @param candidate
+ * @return the trusted subchain or nil if none is trusted.
+ * If more than one is trusted, the longuest will be returned
+ */
+ public ProxyChain trustedSubchain(ProxyChain candidate) {
+ Set sub = subchains();
+ sub.retainAll(candidate.subchains().getProxyChains());
+ ProxyChain trusted = null;
+ if (!sub.isEmpty()) {
+ trusted = Collections.max(sub, new InetAddressChainComparator());
+ }
+ return trusted;
+ }
+
+ /**
+ * a comparator that uses ip chain size
+ */
+ class InetAddressChainComparator implements Comparator {
+ @Override
+ public int compare(ProxyChain a, ProxyChain b) {
+ return a.size() < b.size() ? -1 : a.size() == b.size() ? 0 : 1;
+ }
+ }
+
+ /**
+ *
+ * @return the set of subchains of the trusted ip proxy chains
+ * @see {@link ProxyChain#subchains()}
+ */
+
+ public Set subchains() {
+ Iterator iterator = proxyChains.iterator();
+ Set set = new HashSet();
+ while (iterator.hasNext()) {
+ ProxyChain next = iterator.next();
+ set.addAll(next.subchains().getProxyChains());
+ }
+ return set;
+ }
+
+ /**
+ *
+ * delegated method
+ *
+ */
+ public boolean contains(Object c) {
+ return proxyChains.contains(c);
+ }
+
+
+ /**
+ *
+ * delegated method
+ *
+ * @param chain
+ * @return if it is empty
+ */
+ public boolean isEmpty() {
+ return proxyChains.isEmpty();
+ }
+
+ /**
+ *
+ * delegated method
+ *
+ * @param chain
+ * @return true if it could be added
+ */
+ public boolean add(ProxyChain chain) {
+ return proxyChains.add(chain);
+ }
+
+
+ /**
+ *
+ * delegated method
+ */
+ @Override
+ public String toString() {
+ return proxyChains.toString();
+ }
+
+ /**
+ *
+ * @param array of proxies represented as comma separated strings
+ * @return a {@link ProxyChain} object representing the passed proxies
+ *
+ */
+
+ private static Set getProxies(String[] ips) {
+ Set pChainSet = new HashSet();
+ Iterator iterator = (Arrays.asList(ips)).iterator();
+ while (iterator.hasNext()) {
+ String next = iterator.next();
+ pChainSet.add(new ProxyChain(next));
+ }
+ return pChainSet;
+ }
+
+ /**
+ * @return the proxyChains
+ */
+ public Set getProxyChains() {
+ return proxyChains;
+ }
+}
diff --git a/src/main/java/com/asquera/elasticsearch/plugins/http/auth/XForwardedFor.java b/src/main/java/com/asquera/elasticsearch/plugins/http/auth/XForwardedFor.java
new file mode 100644
index 0000000..16a4b4e
--- /dev/null
+++ b/src/main/java/com/asquera/elasticsearch/plugins/http/auth/XForwardedFor.java
@@ -0,0 +1,92 @@
+package com.asquera.elasticsearch.plugins.http.auth;
+
+import java.util.List;
+import java.util.Arrays;
+import java.util.ArrayList;
+
+/**
+ *
+ * Class that handles the values obtained from the X-Forwarded-For (XFF) HTTP Header
+ * field.
+ *
+ * The X-Forwarded-For (XFF) HTTP header field is a de facto standard for
+ * identifying the originating IP address of a client connecting to a web
+ * server through an HTTP proxy or load balancer.
+ *
+ * The usefulness of XFF depends on the proxy server truthfully reporting the
+ * original host's IP address; for this reason, effective use of XFF requires
+ * knowledge of which proxies are trustworthy, for instance by looking them
+ * up in a whitelist of servers whose maintainers can be trusted.
+ *
+ * @see X-Forwarded-For
+ *
+ *
+ *
+ * @author Ernesto Miguez (ernesto.miguez@asquera.de)
+ */
+
+public class XForwardedFor {
+ /**
+ *
+ * The X-Forwarded-For Header value as received in the request
+ * The general format of the field is:
+ *
+ * X-Forwarded-For: client, proxy1, proxy2
+ */
+ private final String xForwardedFor;
+
+ /**
+ *
+ * @param xForwardedFor
+ */
+ public XForwardedFor(String xForwardedFor) {
+ this.xForwardedFor = xForwardedFor != null ? xForwardedFor : "";
+ }
+
+ /**
+ * @return the ip of the client as defined by the X-Forwarded-For Header
+ */
+ public String client() {
+ ArrayList splitted_ips = new ArrayList(
+ Arrays.asList(xForwardedFor.split(",")));
+ return splitted_ips.remove(0);
+ }
+
+ /**
+ *
+ * @return true if the X-Forwarded-For header was set
+ */
+ public boolean isSet() {
+ return ! xForwardedFor.equals("");
+ }
+
+ /**
+ * delegate method
+ */
+ @Override
+ public String toString() {
+ String s = "not used";
+ if (isSet()) {
+ s = xForwardedFor;
+ }
+ return s;
+ }
+
+ /**
+ * @return the ips of the proxies between the client(as defined by the
+ * X-Forwarded-For Header) and the * server
+ */
+ protected List proxies() {
+ ArrayList splitted_ips = new ArrayList(
+ Arrays.asList(xForwardedFor.split(",")));
+ splitted_ips.remove(0);
+ return splitted_ips;
+ }
+
+ /**
+ * @return the xForwardedFor
+ */
+ public String getxForwardedFor() {
+ return xForwardedFor;
+ }
+}
diff --git a/src/test/java/com/asquera/elasticsearch/plugins/http/auth/ClientTest.java b/src/test/java/com/asquera/elasticsearch/plugins/http/auth/ClientTest.java
new file mode 100644
index 0000000..f698884
--- /dev/null
+++ b/src/test/java/com/asquera/elasticsearch/plugins/http/auth/ClientTest.java
@@ -0,0 +1,229 @@
+package com.asquera.elasticsearch.plugins.http.auth;
+
+import static org.junit.Assert.*;
+import static org.hamcrest.CoreMatchers.is;
+
+import org.junit.Test;
+
+import java.net.UnknownHostException;
+import java.net.InetAddress;
+
+
+public class ClientTest{
+
+ private final String whitelistedIp = "8.8.8.8";
+ private final String notWhitelistedIp = "42.42.42.42";
+ private final String untrustedRequestIp = "50.50.50.50";
+ private final String trustedRequestIp = "6.6.6.6";
+ private String[] trustedIps = {"7.7.7.7,"+ trustedRequestIp};
+ private String[] whitelist = { whitelistedIp };
+ private String xForwardedFor = "9.9.9.9,8.8.8.8,7.7.7.7";
+
+ @Test
+ public void authorizedWhitelistedRequestNilXForwardedFor() throws UnknownHostException {
+ Client c = new Client(
+ InetAddress.getByName(whitelistedIp),
+ new InetAddressWhitelist(whitelist),
+ new XForwardedFor(null),
+ new ProxyChains(trustedIps));
+ assertThat(c.ip(), is(whitelistedIp));
+ assertTrue(c.isTrusted());
+ assertTrue(c.isWhitelisted());
+ assertTrue(c.isAuthorized());
+ }
+
+ public void authorizedWhitelistedRequestUnsetProxies() throws UnknownHostException {
+ Client c = new Client(
+ InetAddress.getByName(whitelistedIp),
+ new InetAddressWhitelist(whitelist),
+ new XForwardedFor(""),
+ new ProxyChains(trustedIps));
+ assertThat(c.ip(), is(whitelistedIp));
+ assertTrue(c.isTrusted());
+ assertTrue(c.isWhitelisted());
+ assertTrue(c.isAuthorized());
+ }
+
+ @Test
+ public void unauthorizedWhitelistedRequestUnsetProxies() throws UnknownHostException {
+ Client c = new Client(
+ InetAddress.getByName(notWhitelistedIp),
+ new InetAddressWhitelist(whitelist),
+ new XForwardedFor(""),
+ new ProxyChains(trustedIps));
+ assertThat(c.ip(), is(notWhitelistedIp));
+ assertTrue(c.isTrusted());
+ assertFalse(c.isWhitelisted());
+ assertFalse(c.isAuthorized());
+ }
+
+ @Test
+ public void ipOfUntrustedRequestViaProxiesIsFirstOfXForwardedFor() throws UnknownHostException {
+ Client c = new Client(
+ InetAddress.getByName(untrustedRequestIp),
+ new InetAddressWhitelist(whitelist),
+ new XForwardedFor(xForwardedFor),
+ new ProxyChains(trustedIps));
+ assertThat(c.ip(), is("9.9.9.9"));
+ assertFalse(c.isTrusted());
+ assertFalse(c.isWhitelisted());
+ assertFalse(c.isAuthorized());
+ }
+
+ @Test
+ public void ipOfTrustedRequestViaProxiesIsInXForwardedFor() throws UnknownHostException {
+ Client c = new Client(
+ InetAddress.getByName(trustedRequestIp),
+ new InetAddressWhitelist(whitelist),
+ new XForwardedFor(xForwardedFor),
+ new ProxyChains(trustedIps));
+ assertThat(c.ip(), is("8.8.8.8"));
+ assertTrue(c.isTrusted());
+ assertTrue(c.isWhitelisted());
+ assertTrue(c.isAuthorized());
+ }
+
+ @Test
+ public void ipOfNotWhitelistedIpViaTrustedProxiesIsFirstOfXForwardedFor() throws UnknownHostException {
+ String[] whitelist = {"10.10.10.10"};
+ Client c = new Client(
+ InetAddress.getByName(trustedRequestIp),
+ new InetAddressWhitelist(whitelist),
+ new XForwardedFor(xForwardedFor),
+ new ProxyChains(trustedIps));
+ assertThat(c.ip(), is("8.8.8.8"));
+ assertTrue(c.isTrusted());
+ assertFalse(c.isWhitelisted());
+ assertFalse(c.isAuthorized());
+ }
+
+ @Test
+ public void noXForwardedSetRequestIpWhitelisted() throws UnknownHostException {
+ Client c = new Client(
+ InetAddress.getByName(whitelistedIp),
+ new InetAddressWhitelist(whitelist),
+ new XForwardedFor(""),
+ new ProxyChains(trustedIps));
+ assertThat(c.ip(), is(whitelistedIp));
+ assertTrue(c.isTrusted());
+ assertTrue(c.isWhitelisted());
+ assertTrue(c.isAuthorized());
+ }
+
+ @Test
+ public void noXForwardedSetRequestIpNotWhitelisted() throws UnknownHostException {
+ Client c = new Client(
+ InetAddress.getByName(notWhitelistedIp),
+ new InetAddressWhitelist(whitelist),
+ new XForwardedFor(""),
+ new ProxyChains(trustedIps));
+ assertThat(c.ip(), is(notWhitelistedIp));
+ assertTrue(c.isTrusted());
+ assertFalse(c.isWhitelisted());
+ assertFalse(c.isAuthorized());
+ }
+
+ @Test
+ public void clientIsTrustedBySeveralProxyChains() throws UnknownHostException {
+
+ String[] trustedIps = {"1.1.1.1,2.2.2.2,3.3.3.3","4.4.4.4,2.2.2.2,3.3.3.3"};
+ String xForwardedFor = whitelistedIp + ",2.2.2.2";
+ Client c = new Client(
+ InetAddress.getByName("3.3.3.3"),
+ new InetAddressWhitelist(whitelist),
+ new XForwardedFor(xForwardedFor),
+ new ProxyChains(trustedIps));
+ assertThat(c.ip(), is(whitelistedIp));
+ assertTrue(c.isTrusted());
+ assertTrue(c.isWhitelisted());
+ assertTrue(c.isAuthorized());
+ }
+
+ @Test
+ public void clientIsUntrustedAndInWhitelist() throws UnknownHostException {
+ String xForwardedFor = whitelistedIp + ",2.2.2.2";
+ Client c = new Client(
+ InetAddress.getByName(untrustedRequestIp),
+ new InetAddressWhitelist(whitelist),
+ new XForwardedFor(xForwardedFor),
+ new ProxyChains(trustedIps));
+ assertThat(c.ip(), is(whitelistedIp));
+ assertFalse(c.isTrusted());
+ assertTrue(c.isWhitelisted());
+ assertFalse(c.isAuthorized());
+ }
+
+ @Test
+ public void clientIsTrustedAndInWhitelistViaOneProxy() throws UnknownHostException {
+ String xForwardedFor = whitelistedIp;
+ Client c = new Client(
+ InetAddress.getByName(trustedRequestIp),
+ new InetAddressWhitelist(whitelist),
+ new XForwardedFor(xForwardedFor),
+ new ProxyChains(trustedIps));
+ assertThat(c.ip(), is(whitelistedIp));
+ assertTrue(c.isTrusted());
+ assertTrue(c.isWhitelisted());
+ assertTrue(c.isAuthorized());
+ }
+
+ @Test
+ public void clientIsUntrustedAndInWhitelistViaOneProxy() throws UnknownHostException {
+ String xForwardedFor = whitelistedIp;
+ Client c = new Client(
+ InetAddress.getByName(untrustedRequestIp),
+ new InetAddressWhitelist(whitelist),
+ new XForwardedFor(xForwardedFor),
+ new ProxyChains(trustedIps));
+ assertThat(c.ip(), is(whitelistedIp));
+ assertFalse(c.isTrusted());
+ assertTrue(c.isWhitelisted());
+ assertFalse(c.isAuthorized());
+ }
+
+ @Test
+ public void lastXForwardTrustedProxyIsNotwhitelistedClient() throws UnknownHostException {
+ String[] trustedIps = {"3.3.3.3,2.2.2.2,1.1.1.1," + trustedRequestIp};
+ String xForwardedFor = notWhitelistedIp + "," + whitelistedIp + ",3.3.3.3,2.2.2.2";
+ Client c = new Client(
+ InetAddress.getByName(trustedRequestIp),
+ new InetAddressWhitelist(whitelist),
+ new XForwardedFor(xForwardedFor),
+ new ProxyChains(trustedIps));
+ assertThat(c.ip(), is("2.2.2.2"));
+ assertTrue(c.isTrusted());
+ assertFalse(c.isWhitelisted());
+ assertFalse(c.isAuthorized());
+ }
+
+ @Test
+ public void lastXForwardTrustedProxyIsWhitelistedClient() throws UnknownHostException {
+ String[] trustedIps = {"3.3.3.3,2.2.2.2," + trustedRequestIp};
+ String xForwardedFor = notWhitelistedIp + "," + whitelistedIp + ",2.2.2.2";
+ Client c = new Client(
+ InetAddress.getByName(trustedRequestIp),
+ new InetAddressWhitelist(whitelist),
+ new XForwardedFor(xForwardedFor),
+ new ProxyChains(trustedIps));
+ assertThat(c.ip(), is(whitelistedIp));
+ assertTrue(c.isTrusted());
+ assertTrue(c.isWhitelisted());
+ assertTrue(c.isAuthorized());
+ }
+
+
+ @Test
+ public void longestTrustedChainDefinesClientNontInWhitelist() throws UnknownHostException {
+ String xForwardedFor = notWhitelistedIp + "," + whitelistedIp + ",2.2.2.2";
+ String[] trustedIps = { xForwardedFor + "," + trustedRequestIp};
+ Client c = new Client(
+ InetAddress.getByName(trustedRequestIp),
+ new InetAddressWhitelist(whitelist),
+ new XForwardedFor(xForwardedFor),
+ new ProxyChains(trustedIps));
+ assertThat(c.ip(), is(notWhitelistedIp));
+ assertTrue(c.isTrusted());
+ assertFalse(c.isWhitelisted());
+ assertFalse(c.isAuthorized());
+ }
+}
diff --git a/src/test/java/com/asquera/elasticsearch/plugins/http/auth/InetAddressWhitelistTest.java b/src/test/java/com/asquera/elasticsearch/plugins/http/auth/InetAddressWhitelistTest.java
new file mode 100644
index 0000000..8112a9a
--- /dev/null
+++ b/src/test/java/com/asquera/elasticsearch/plugins/http/auth/InetAddressWhitelistTest.java
@@ -0,0 +1,55 @@
+package com.asquera.elasticsearch.plugins.http.auth;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+public class InetAddressWhitelistTest {
+
+ static final String localhost = "localhost";
+ static final String containedIp = "1.1.1.1";
+ static String notContainedIp = "2.2.2.2";
+ private InetAddressWhitelist whitelist(String ip) {
+ String[] w = { ip };
+ return new InetAddressWhitelist(w);
+ }
+
+ @Test
+ public void testInnetLocalhost() throws UnknownHostException {
+ assertTrue(whitelist(localhost).contains(InetAddress.getByName(localhost)));
+ }
+ @Test
+ public void testInnetNullDefaultsToLocalhost() throws UnknownHostException {
+ assertTrue(whitelist(null).contains(InetAddress.getByName(localhost)));
+ }
+ @Test
+ public void testStringLocalhostNotMatched() throws UnknownHostException {
+ // the ip that "localhost" resolves to its matched ip and not the string
+ // "localhost" itself
+ assertFalse(whitelist(localhost).contains(localhost));
+ }
+
+ @Test
+ public void testIpContained() throws UnknownHostException {
+ assertTrue(whitelist(containedIp).contains(containedIp));
+ }
+
+ @Test
+ public void testEmptyWhitelist() throws UnknownHostException {
+ assertFalse(whitelist("").contains(notContainedIp));
+ }
+
+ @Test
+ public void testNotContained() throws UnknownHostException {
+ assertFalse(whitelist(containedIp).contains(notContainedIp));
+ }
+
+ @Test
+ public void invalidIpIsDropped() throws UnknownHostException {
+ String invalidIp = "555.555.555.555";
+ assertFalse(whitelist(invalidIp).contains(invalidIp));
+ }
+}
diff --git a/src/test/java/com/asquera/elasticsearch/plugins/http/auth/ProxyChainTest.java b/src/test/java/com/asquera/elasticsearch/plugins/http/auth/ProxyChainTest.java
new file mode 100644
index 0000000..8d58107
--- /dev/null
+++ b/src/test/java/com/asquera/elasticsearch/plugins/http/auth/ProxyChainTest.java
@@ -0,0 +1,61 @@
+package com.asquera.elasticsearch.plugins.http.auth;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+
+public class ProxyChainTest {
+
+
+ @Test
+ public void emptyProxyChainGeneratesEmptySubchain() {
+ assertTrue(new ProxyChain().subchains().isEmpty());
+ }
+
+ @Test
+ public void notEmptyIsIncludedInSubchain() {
+ ArrayList c = new ArrayList();
+ c.add( "123.134.123.213" );
+ ProxyChain r = new ProxyChain(c);
+ assertTrue(r.subchains().contains(r));
+ }
+
+ @Test
+ public void lastTwoIncludedInSubchain() {
+ ArrayList c = new ArrayList();
+ c.add( "1.1.1.1" );
+ c.add( "3.3.3.3" );
+ ProxyChain r = new ProxyChain(c);
+ ProxyChain s = new ProxyChain();
+ s.add( "1.1.1.1" );
+ s.add( "3.3.3.3" );
+ assertTrue(r.subchains().contains(s));
+ }
+
+ @Test
+ public void firstTwoNotIncludedInSubchain() {
+ ArrayList c = new ArrayList();
+ c.add( "123.134.123.213" );
+ c.add( "1.1.1.1" );
+ c.add( "3.3.3.3" );
+ ProxyChain r = new ProxyChain(c);
+ ProxyChain s = new ProxyChain();
+ s.add(c.get(0));
+ s.add(c.get(1));
+ assertFalse(r.subchains().contains(s));
+ }
+
+ @Test
+ public void middleNotIncludedInSubchain() {
+ ArrayList c = new ArrayList();
+ c.add( "123.134.123.213" );
+ c.add( "1.1.1.1" );
+ c.add( "3.3.3.3" );
+ ProxyChain r = new ProxyChain(c);
+ ProxyChain s = new ProxyChain();
+ s.add(c.get(1));
+ assertFalse(r.subchains().contains(s));
+ }
+}
diff --git a/src/test/java/com/asquera/elasticsearch/plugins/http/auth/ProxyChainsTest.java b/src/test/java/com/asquera/elasticsearch/plugins/http/auth/ProxyChainsTest.java
new file mode 100644
index 0000000..c580c5f
--- /dev/null
+++ b/src/test/java/com/asquera/elasticsearch/plugins/http/auth/ProxyChainsTest.java
@@ -0,0 +1,60 @@
+package com.asquera.elasticsearch.plugins.http.auth;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+import org.junit.Before;
+
+public class ProxyChainsTest {
+
+ private ProxyChains trustedChains;
+ private ProxyChain trustedCandidateChain;
+ private ProxyChain unTrustedCandidateChain;
+ private final String untrustedChain1 = "50.50.50.50";
+ private final String trustedChain1 = "7.7.7.7";
+ private final String trustedChain2 = "5.5.5.5,6.6.6.6";
+ private final String trustedChain3 = "8.8.8.8,9.9.9.9,10.10.10.10";
+ private final String trustedChain4 = "2.2.2.2" + "," + trustedChain2;
+ private final String[] t = { trustedChain1, trustedChain2, trustedChain3, trustedChain4 };
+
+ @Before public void initialize() {
+ trustedCandidateChain = new ProxyChain(trustedChain1);
+ unTrustedCandidateChain = new ProxyChain(untrustedChain1);
+ trustedChains = new ProxyChains(t);
+ }
+
+ @Test(expected=NullPointerException.class)
+ public void NullPointerExceptionInNullTrustedIps() {
+ String[] t = null;
+ trustedChains = new ProxyChains(t);
+ assertFalse(trustedChains.trusts(trustedCandidateChain));
+ }
+
+ @Test
+ public void unTrustsEmptyCandidate() {
+ assertFalse(trustedChains.trusts(new ProxyChain("")));
+ }
+
+ @Test
+ public void unTrustsAnyCandidateWithEmptyTrustedChain() {
+ String[] t = {""};
+ trustedChains = new ProxyChains(t);
+ assertFalse(trustedChains.trusts(trustedCandidateChain));
+ }
+
+ @Test
+ public void trustsCandidatesInProxyChain() {
+ assertTrue(trustedChains.trusts(trustedCandidateChain));
+ }
+
+ @Test
+ public void unTrustedCandidateNotInProxyChain() {
+ assertFalse(trustedChains.trusts(unTrustedCandidateChain));
+ }
+
+ @Test
+ public void trustCandidateContainedInTwoTrustedChains() {
+ trustedCandidateChain = new ProxyChain(trustedChain2);
+ assertTrue(trustedChains.trusts(trustedCandidateChain));
+ }
+}
diff --git a/src/test/java/com/asquera/elasticsearch/plugins/http/auth/XForwardedForTest.java b/src/test/java/com/asquera/elasticsearch/plugins/http/auth/XForwardedForTest.java
new file mode 100644
index 0000000..cb38637
--- /dev/null
+++ b/src/test/java/com/asquera/elasticsearch/plugins/http/auth/XForwardedForTest.java
@@ -0,0 +1,36 @@
+package com.asquera.elasticsearch.plugins.http.auth;
+
+import static org.junit.Assert.*;
+import static org.hamcrest.CoreMatchers.is;
+import org.junit.Test;
+
+public class XForwardedForTest {
+
+ @Test
+ public void returnsClientWithClientAndProxy() {
+ // It seems the getName lookup for empty string is localhost
+ String xForwardedFor = "123.123.123.123,122.122.12.1" ;
+ assertThat(new XForwardedFor(xForwardedFor).client(),
+ is(xForwardedFor.split(",")[0]));
+ }
+
+ @Test
+ public void returnsClientWithClient() {
+ // It seems the getName lookup for empty string is localhost
+ String xForwardedFor = "123.123.123.123" ;
+ assertThat(new XForwardedFor(xForwardedFor).client(),
+ is(xForwardedFor.split(",")[0]));
+ }
+
+ @Test
+ public void returnsClientWithNil() {
+ assertThat(new XForwardedFor(null).client(), is(""));
+ }
+
+ @Test
+ public void unsetHeaderReturnsEmptyClient() {
+ // It seems the getName lookup for empty string is localhost
+ String xForwardedFor = "" ;
+ assertFalse(new XForwardedFor(xForwardedFor).isSet());
+ }
+}
diff --git a/src/test/java/com/asquera/elasticsearch/plugins/http/auth/integration/DefaultConfigurationIntegrationTest.java b/src/test/java/com/asquera/elasticsearch/plugins/http/auth/integration/DefaultConfigurationIntegrationTest.java
new file mode 100644
index 0000000..d1bcbf8
--- /dev/null
+++ b/src/test/java/com/asquera/elasticsearch/plugins/http/auth/integration/DefaultConfigurationIntegrationTest.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you 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 com.asquera.elasticsearch.plugins.http.auth.integration;
+
+
+import org.apache.http.impl.client.HttpClients;
+import org.elasticsearch.common.settings.ImmutableSettings;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.rest.RestStatus;
+import org.elasticsearch.test.ElasticsearchIntegrationTest;
+import org.elasticsearch.test.ElasticsearchIntegrationTest.ClusterScope;
+import org.elasticsearch.test.ElasticsearchIntegrationTest.Scope;
+import org.elasticsearch.test.rest.client.http.HttpRequestBuilder;
+import org.elasticsearch.test.rest.client.http.HttpResponse;
+import org.junit.Test;
+
+import static org.hamcrest.Matchers.equalTo;
+
+/**
+ * Test a rest action that sets special response headers
+ */
+@ClusterScope(transportClientRatio = 0.0, scope = Scope.SUITE)
+public class DefaultConfigurationIntegrationTest extends ElasticsearchIntegrationTest {
+
+ @Override
+ protected Settings nodeSettings(int nodeOrdinal) {
+ return ImmutableSettings.settingsBuilder()
+ .build();
+ }
+
+ @Test
+ public void testHealthCheck() throws Exception {
+ HttpResponse response = httpClient().path("/").execute();
+ assertThat(response.getStatusCode(), equalTo(RestStatus.OK.getStatus()));
+ }
+
+ @Test
+ public void localhostClientIsAuthenticated() throws Exception {
+ HttpResponse response = httpClient().path("/_status").execute();
+ assertThat(response.getStatusCode(), equalTo(RestStatus.OK.getStatus()));
+ }
+
+ public static HttpRequestBuilder httpClient() {
+ return new HttpRequestBuilder(HttpClients.createDefault()).host("localhost").port(9200);
+ }
+}
diff --git a/src/test/java/com/asquera/elasticsearch/plugins/http/auth/integration/EmptyWhitelistIntegrationTest.java b/src/test/java/com/asquera/elasticsearch/plugins/http/auth/integration/EmptyWhitelistIntegrationTest.java
new file mode 100644
index 0000000..3635e34
--- /dev/null
+++ b/src/test/java/com/asquera/elasticsearch/plugins/http/auth/integration/EmptyWhitelistIntegrationTest.java
@@ -0,0 +1,112 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you 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 com.asquera.elasticsearch.plugins.http.auth.integration;
+
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.elasticsearch.common.settings.ImmutableSettings;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.Base64;
+import org.elasticsearch.rest.RestStatus;
+import org.elasticsearch.test.ElasticsearchIntegrationTest;
+import org.elasticsearch.test.ElasticsearchIntegrationTest.ClusterScope;
+import org.elasticsearch.test.rest.client.http.HttpGetWithEntity;
+import org.elasticsearch.test.rest.client.http.HttpRequestBuilder;
+import org.elasticsearch.test.rest.client.http.HttpResponse;
+import org.junit.Test;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import static org.elasticsearch.test.ElasticsearchIntegrationTest.Scope;
+import static org.hamcrest.Matchers.equalTo;
+
+/**
+ * Test a rest action that sets special response headers
+ */
+@ClusterScope(transportClientRatio = 0.0, scope = Scope.SUITE)
+public class EmptyWhitelistIntegrationTest extends ElasticsearchIntegrationTest {
+
+ @Override
+ protected Settings nodeSettings(int nodeOrdinal) {
+ return ImmutableSettings.settingsBuilder().putArray("http.basic.ipwhitelist", "unkown")
+ .build();
+ }
+
+ @Test
+ public void testHealthCheck() throws Exception {
+ HttpResponse response = httpClient().path("/").execute();
+ assertThat(response.getStatusCode(), equalTo(RestStatus.OK.getStatus()));
+ }
+
+ @Test
+ public void localhostClientIsNotIpAuthenticated() throws Exception {
+ HttpResponse response = httpClient().path("/_status").execute();
+ assertThat(response.getStatusCode(), equalTo(RestStatus.UNAUTHORIZED.getStatus()));
+ }
+
+ @Test
+ public void localhostClientIsBasicAuthenticated() throws Exception {
+ HttpUriRequest request = httpRequest();
+ String credentials = "admin:admin_pw";
+ request.setHeader("Authorization", "Basic " + Base64.encodeBytes(credentials.getBytes()));
+ CloseableHttpResponse response = closeableHttpClient().execute(request);
+ assertThat(response.getStatusLine().getStatusCode(), equalTo(RestStatus.OK.getStatus()));
+ }
+
+
+ @Test
+ public void localhostClientIsBasicAuthenticatedPassingXForward() throws Exception {
+ HttpUriRequest request = httpRequest();
+ String credentials = "admin:admin_pw";
+ request.setHeader("Authorization", "Basic " + Base64.encodeBytes(credentials.getBytes()));
+ request.setHeader("X-Forwarded-For", "1.1.1.1" );
+ CloseableHttpResponse response = closeableHttpClient().execute(request);
+ assertThat(response.getStatusLine().getStatusCode(), equalTo(RestStatus.OK.getStatus()));
+ }
+ @Test
+ public void localhostClientNotBasicAuthenticated() throws Exception {
+ HttpUriRequest request = httpRequest();
+ String credentials = "admin:wrong";
+ request.setHeader("Authorization", "Basic " + Base64.encodeBytes(credentials.getBytes()));
+ CloseableHttpResponse response = closeableHttpClient().execute(request);
+ assertThat(response.getStatusLine().getStatusCode(), equalTo(RestStatus.UNAUTHORIZED.getStatus()));
+ }
+
+ public static HttpRequestBuilder httpClient() {
+ return new HttpRequestBuilder(HttpClients.createDefault()).host("localhost").port(9200);
+ }
+
+ public static HttpUriRequest httpRequest() {
+ HttpUriRequest httpUriRequest = null;
+ try {
+ httpUriRequest = new HttpGetWithEntity(new URI("http", null, "localhost", 9200, "/_status", null, null));
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException(e);
+ }
+ return httpUriRequest;
+ }
+
+ public static CloseableHttpClient closeableHttpClient() {
+ return HttpClients.createDefault();
+ }
+
+}
diff --git a/src/test/java/com/asquera/elasticsearch/plugins/http/auth/integration/IpAuthenticationIntegrationTest.java b/src/test/java/com/asquera/elasticsearch/plugins/http/auth/integration/IpAuthenticationIntegrationTest.java
new file mode 100644
index 0000000..41895bb
--- /dev/null
+++ b/src/test/java/com/asquera/elasticsearch/plugins/http/auth/integration/IpAuthenticationIntegrationTest.java
@@ -0,0 +1,130 @@
+
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you 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 com.asquera.elasticsearch.plugins.http.auth.integration;
+
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.elasticsearch.common.settings.ImmutableSettings;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.Base64;
+import org.elasticsearch.rest.RestStatus;
+import org.elasticsearch.test.ElasticsearchIntegrationTest;
+import org.elasticsearch.test.ElasticsearchIntegrationTest.ClusterScope;
+import org.elasticsearch.test.rest.client.http.HttpGetWithEntity;
+import org.elasticsearch.test.rest.client.http.HttpRequestBuilder;
+import org.elasticsearch.test.rest.client.http.HttpResponse;
+import org.junit.Test;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import static org.elasticsearch.test.ElasticsearchIntegrationTest.Scope;
+import static org.hamcrest.Matchers.equalTo;
+
+/**
+ * Test a rest action that sets special response headers
+ */
+@ClusterScope(transportClientRatio = 0.0, scope = Scope.SUITE)
+public class IpAuthenticationIntegrationTest extends ElasticsearchIntegrationTest {
+
+ protected final String localhost = "127.0.0.1";
+ protected final String whitelistedIp = "2.2.2.2";
+ protected final String notWhitelistedIp = "3.3.3.3";
+ protected final String trustedIp = "4.4.4.4";
+
+ @Override
+ protected Settings nodeSettings(int nodeOrdinal) {
+ return ImmutableSettings.settingsBuilder()
+ .putArray("http.basic.ipwhitelist", whitelistedIp)
+ .putArray("http.basic.trusted_proxy_chains", trustedIp + "," + localhost)
+ .put("http.basic.xforward", "X-Forwarded-For")
+ .build();
+ }
+
+ @Test
+ public void testHealthCheck() throws Exception {
+ HttpResponse response = httpClient().path("/").execute();
+ assertThat(response.getStatusCode(), equalTo(RestStatus.OK.getStatus()));
+ }
+
+ @Test
+ public void localhostClientIsBasicAuthenticated() throws Exception {
+ HttpUriRequest request = httpRequest();
+ String credentials = "admin:admin_pw";
+ request.setHeader("Authorization", "Basic " + Base64.encodeBytes(credentials.getBytes()));
+ CloseableHttpResponse response = closeableHttpClient().execute(request);
+ assertThat(response.getStatusLine().getStatusCode(), equalTo(RestStatus.OK.getStatus()));
+ }
+
+ @Test
+ public void proxyViaLocalhostIpAuthenticatesWhitelistedClients() throws Exception {
+ HttpUriRequest request = httpRequest();
+ request.setHeader("X-Forwarded-For", whitelistedIp );
+ CloseableHttpResponse response = closeableHttpClient().execute(request);
+ assertThat(response.getStatusLine().getStatusCode(), equalTo(RestStatus.OK.getStatus()));
+ request = httpRequest();
+ request.setHeader("X-Forwarded-For", notWhitelistedIp + "," + whitelistedIp);
+ response = closeableHttpClient().execute(request);
+ assertThat(response.getStatusLine().getStatusCode(), equalTo(RestStatus.OK.getStatus()));
+ request = httpRequest();
+ request.setHeader("X-Forwarded-For", notWhitelistedIp + "," + whitelistedIp + "," + trustedIp);
+ response = closeableHttpClient().execute(request);
+ assertThat(response.getStatusLine().getStatusCode(), equalTo(RestStatus.OK.getStatus()));
+ }
+
+ @Test
+ public void proxyViaLocalhostIpUnauthenticatesNonWhitelistedClients() throws Exception {
+ HttpUriRequest request = httpRequest();
+ request.setHeader("X-Forwarded-For", notWhitelistedIp);
+ CloseableHttpResponse response = closeableHttpClient().execute(request);
+ assertThat(response.getStatusLine().getStatusCode(), equalTo(RestStatus.UNAUTHORIZED.getStatus()));
+ request = httpRequest();
+ request.setHeader("X-Forwarded-For", whitelistedIp + "," + notWhitelistedIp + "," + trustedIp);
+ response = closeableHttpClient().execute(request);
+ assertThat(response.getStatusLine().getStatusCode(), equalTo(RestStatus.UNAUTHORIZED.getStatus()));
+ request = httpRequest();
+ request.setHeader("X-Forwarded-For", "");
+ response = closeableHttpClient().execute(request);
+ assertThat(response.getStatusLine().getStatusCode(), equalTo(RestStatus.UNAUTHORIZED.getStatus()));
+ }
+
+ public static HttpRequestBuilder httpClient() {
+ return new HttpRequestBuilder(HttpClients.createDefault())
+ .host("localhost").port(9200);
+ }
+
+ public static HttpUriRequest httpRequest() {
+ HttpUriRequest httpUriRequest = null;
+ try {
+ httpUriRequest = new HttpGetWithEntity(new URI("http",
+ null, "localhost", 9200, "/_status", null, null));
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException(e);
+ }
+ return httpUriRequest;
+ }
+
+ public static CloseableHttpClient closeableHttpClient() {
+ return HttpClients.createDefault();
+ }
+
+}