View Javadoc
1   package emissary.client;
2   
3   import emissary.client.EmissaryResponse.EmissaryResponseHandler;
4   import emissary.config.ConfigUtil;
5   import emissary.config.Configurator;
6   
7   import com.google.common.annotations.VisibleForTesting;
8   import jakarta.annotation.Nullable;
9   import jakarta.ws.rs.core.MediaType;
10  import org.apache.hc.client5.http.auth.AuthCache;
11  import org.apache.hc.client5.http.auth.AuthScope;
12  import org.apache.hc.client5.http.auth.Credentials;
13  import org.apache.hc.client5.http.auth.StandardAuthScheme;
14  import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
15  import org.apache.hc.client5.http.classic.methods.HttpPost;
16  import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
17  import org.apache.hc.client5.http.config.ConnectionConfig;
18  import org.apache.hc.client5.http.config.RequestConfig;
19  import org.apache.hc.client5.http.cookie.Cookie;
20  import org.apache.hc.client5.http.entity.EntityBuilder;
21  import org.apache.hc.client5.http.impl.auth.BasicAuthCache;
22  import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
23  import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
24  import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
25  import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
26  import org.apache.hc.client5.http.protocol.HttpClientContext;
27  import org.apache.hc.core5.http.HttpStatus;
28  import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
29  import org.apache.hc.core5.util.Timeout;
30  import org.eclipse.jetty.util.security.Password;
31  import org.glassfish.jersey.server.filter.CsrfProtectionFilter;
32  import org.slf4j.Logger;
33  import org.slf4j.LoggerFactory;
34  
35  import java.io.IOException;
36  import java.net.URISyntaxException;
37  import java.util.Collections;
38  import java.util.Properties;
39  import java.util.concurrent.TimeUnit;
40  
41  /**
42   * Base class of all the actions that use HttpClient.
43   */
44  @SuppressWarnings("NonFinalStaticField")
45  public class EmissaryClient {
46  
47      public static final String DEFAULT_CONTEXT = "emissary";
48      public static final String JETTY_USER_FILE_PROPERTY_NAME = "emissary.jetty.users.file";
49      public static final int DEFAULT_CONNECTION_TIMEOUT = (int) TimeUnit.MINUTES.toMillis(100L); // 2 X 50 min
50      public static final int DEFAULT_CONNECTION_MANAGER_TIMEOUT = (int) TimeUnit.MINUTES.toMillis(2L);
51      public static final int DEFAULT_SOCKET_TIMEOUT = (int) TimeUnit.MINUTES.toMillis(1L);
52      public static final int DEFAULT_RETRIES = 3;
53      public static final String DEFAULT_USERNAME = "emissary";
54      public static final String DEFAULT_PASSWORD = "password";
55      public static final String CSRF_HEADER_PARAM = CsrfProtectionFilter.HEADER_NAME;
56  
57      private static final Logger LOGGER = LoggerFactory.getLogger(EmissaryClient.class);
58  
59      private static final PoolingHttpClientConnectionManager CONNECTION_MANAGER = HTTPConnectionFactory.getFactory().getDefaultConnectionManager();
60      // some default objects to use
61      private static final BasicCredentialsProvider CRED_PROV = new BasicCredentialsProvider();
62      protected static final int ANY_PORT = -1;
63      @Nullable
64      protected static final String ANY_HOST = null;
65  
66      @Nullable
67      private static CloseableHttpClient staticClient = null;
68      @Nullable
69      private static RequestConfig staticRequestConfig = null;
70      @Nullable
71      private static ConnectionConfig staticConnectionConfig = null;
72  
73      // static config variables
74      public static String context = DEFAULT_CONTEXT;
75      protected static int retries = DEFAULT_RETRIES;
76      protected static String username = DEFAULT_USERNAME;
77      // How long to wait while establishing a connection (ms)
78      protected static int connectionTimeout = DEFAULT_CONNECTION_TIMEOUT;
79      // How long to wait for a connection from the pool (ms)
80      protected static int connectionManagerTimeout = DEFAULT_CONNECTION_MANAGER_TIMEOUT;
81      // How long to wait on a data read in a connection (ms)
82      protected static int socketTimeout = DEFAULT_SOCKET_TIMEOUT;
83      // class is thread-safe
84      protected static final AuthCache AUTH_CACHE = new BasicAuthCache();
85  
86      private final CloseableHttpClient client;
87      private final RequestConfig requestConfig;
88      private ConnectionConfig connectionConfig;
89  
90      static {
91          configure();
92      }
93  
94      @VisibleForTesting
95      protected static void configure() {
96          LOGGER.debug("Configuring EmissaryClient");
97  
98          // parse configs
99          try {
100             final Configurator c = ConfigUtil.getConfigInfo(EmissaryClient.class);
101             retries = c.findIntEntry("retries", DEFAULT_RETRIES);
102             username = c.findStringEntry("username", DEFAULT_USERNAME);
103             connectionTimeout = c.findIntEntry("connectionTimeout", DEFAULT_CONNECTION_TIMEOUT);
104             connectionManagerTimeout = c.findIntEntry("connectionManagerTimeout", DEFAULT_CONNECTION_MANAGER_TIMEOUT);
105             socketTimeout = c.findIntEntry("socketTimeout", DEFAULT_SOCKET_TIMEOUT);
106             context = c.findStringEntry("context", DEFAULT_CONTEXT);
107         } catch (IOException iox) {
108             LOGGER.warn("Cannot read EmissaryClient properties, configuring defaults: {}", iox.getMessage());
109             retries = DEFAULT_RETRIES;
110             username = DEFAULT_USERNAME;
111             connectionTimeout = DEFAULT_CONNECTION_TIMEOUT;
112             connectionManagerTimeout = DEFAULT_CONNECTION_MANAGER_TIMEOUT;
113             socketTimeout = DEFAULT_SOCKET_TIMEOUT;
114             context = DEFAULT_CONTEXT;
115         }
116 
117         // Read the jetty user realm formatted property file for the password
118         // Value for password remains unchanged if there is a problem
119         try {
120             String userPropertiesFile = System.getProperty(JETTY_USER_FILE_PROPERTY_NAME);
121             if (null == userPropertiesFile) {
122                 LOGGER.debug("System property '{}' not set, using default jetty-users.properties", JETTY_USER_FILE_PROPERTY_NAME);
123                 userPropertiesFile = "jetty-users.properties";
124             }
125             LOGGER.debug("Reading password from {}", userPropertiesFile);
126             final Properties props = ConfigUtil.getPropertyInfo(userPropertiesFile);
127             String pass = DEFAULT_PASSWORD;
128             final String value = props.getProperty(username, pass);
129             if (value != null && value.indexOf(',') != -1) {
130                 pass = value.substring(0, value.indexOf(',')).trim();
131             } else if (pass.equals(value)) {
132                 LOGGER.error("Error reading password from {}", userPropertiesFile);
133             }
134             // Supply default credentials for anyone we want to connect to
135             final String decodedPassword = pass.startsWith("OBF:") ? Password.deobfuscate(pass) : pass;
136             final Credentials cred = new UsernamePasswordCredentials(username, decodedPassword.toCharArray());
137             CRED_PROV.setCredentials(new AuthScope(ANY_HOST, ANY_PORT), cred);
138         } catch (IOException iox) {
139             LOGGER.error("Cannot read {} in EmissaryClient, defaulting credentials", System.getProperty(JETTY_USER_FILE_PROPERTY_NAME));
140             final Credentials cred = new UsernamePasswordCredentials(username, DEFAULT_PASSWORD.toCharArray());
141             final AuthScope authScope = new AuthScope(ANY_HOST, ANY_PORT);
142             CRED_PROV.setCredentials(authScope, cred);
143         }
144 
145         staticRequestConfig =
146                 RequestConfig.custom()
147                         .setConnectionRequestTimeout(Timeout.ofMilliseconds(connectionManagerTimeout))
148                         .setTargetPreferredAuthSchemes(Collections.singleton(StandardAuthScheme.DIGEST))
149                         .setProxyPreferredAuthSchemes(Collections.singleton(StandardAuthScheme.DIGEST))
150                         .build();
151 
152         staticConnectionConfig = ConnectionConfig.custom()
153                 .setConnectTimeout(Timeout.ofMilliseconds(connectionTimeout))
154                 .setSocketTimeout(Timeout.ofMilliseconds(socketTimeout)).build();
155 
156         CONNECTION_MANAGER.setDefaultConnectionConfig(staticConnectionConfig);
157 
158         staticClient =
159                 HttpClientBuilder.create().setConnectionManager(CONNECTION_MANAGER).setDefaultCredentialsProvider(CRED_PROV)
160                         .setDefaultRequestConfig(staticRequestConfig).build();
161 
162     }
163 
164     public EmissaryClient() {
165         this(staticClient, staticRequestConfig, staticConnectionConfig);
166     }
167 
168     public EmissaryClient(RequestConfig requestConfig, ConnectionConfig connectionConfig) {
169         this(staticClient, requestConfig, connectionConfig);
170     }
171 
172     public EmissaryClient(CloseableHttpClient client) {
173         this(client, staticRequestConfig, staticConnectionConfig);
174     }
175 
176     public EmissaryClient(CloseableHttpClient client, RequestConfig requestConfig, ConnectionConfig connectionConfig) {
177         this.client = client;
178         this.requestConfig = requestConfig;
179         this.connectionConfig = connectionConfig;
180     }
181 
182     public EmissaryResponse send(final HttpUriRequestBase method) {
183         return send(method, null);
184     }
185 
186     /**
187      * Sends a request to the web server. The request can be any HttpMethod. Adds the specified cookie to the Http State
188      *
189      * @param method the method to be sent
190      * @param cookie a cookie to set on the request
191      */
192     public EmissaryResponse send(final HttpUriRequestBase method, @Nullable final Cookie cookie) {
193         try {
194             LOGGER.debug("Sending {} to {}", method.getMethod(), method.getUri());
195         } catch (URISyntaxException e) {
196             LOGGER.debug("Sending {} and failed to retrieve URI", method.getMethod());
197         }
198 
199         HttpClientContext localContext = HttpClientContext.create();
200         localContext.setAttribute(HttpClientContext.AUTH_CACHE, EmissaryClient.AUTH_CACHE);
201 
202         if (cookie != null) {
203             localContext.getCookieStore().addCookie(cookie);
204         }
205 
206         try {
207             // This is thread safe. The client is instantiated in a static block above
208             // with a connection pool. Calling new EmissaryClient().send allows you
209             // to use a different context and request config per request
210             method.setConfig(requestConfig);
211             CloseableHttpClient thisClient = getHttpClient();
212             return thisClient.execute(method, localContext, new EmissaryResponseHandler());
213         } catch (IOException e) {
214             LOGGER.debug("Problem processing request:", e);
215             BasicClassicHttpResponse response = new BasicClassicHttpResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR, e.getMessage());
216             response.setEntity(EntityBuilder.create().setText(e.getClass() + ": " + e.getMessage()).setContentEncoding(MediaType.TEXT_PLAIN).build());
217             return new EmissaryResponse(response);
218         }
219     }
220 
221     protected CloseableHttpClient getHttpClient() {
222         return client;
223     }
224 
225     protected RequestConfig getRequestConfig() {
226         return requestConfig;
227     }
228 
229     protected ConnectionConfig getConnectionConfig() {
230         return connectionConfig;
231     }
232 
233 
234     public void setConnectionTimeout(int timeout) {
235         if (timeout > 0) {
236             connectionConfig = ConnectionConfig.copy(connectionConfig).setConnectTimeout(Timeout.ofMilliseconds(timeout)).build();
237         } else {
238             LOGGER.warn("Tried to set timeout to {}", timeout);
239         }
240     }
241 
242     protected String getCsrfToken() {
243         return DEFAULT_CONTEXT;
244     }
245 
246     public HttpPost createHttpPost(String uri, String context, String endpoint) {
247         return createHttpPost(uri + context + endpoint, getCsrfToken());
248     }
249 
250     public HttpPost createHttpPost(String uri) {
251         return createHttpPost(uri, getCsrfToken());
252     }
253 
254     public HttpPost createHttpPost(String uri, String csrfToken) {
255         HttpPost method = new HttpPost(uri);
256         setCsrfHeader(method, csrfToken);
257         return method;
258     }
259 
260     public HttpUriRequestBase setCsrfHeader(HttpUriRequestBase request, String csrfToken) {
261         return setCsrfHeader(request, CSRF_HEADER_PARAM, csrfToken);
262     }
263 
264     public HttpUriRequestBase setCsrfHeader(HttpUriRequestBase request, String csrfParam, String csrfToken) {
265         request.addHeader(csrfParam, csrfToken);
266         return request;
267     }
268 
269 }