Skip to content

Commit

Permalink
Changes to HTTPServer and HTTPMetricHandler to resolve getting Conten…
Browse files Browse the repository at this point in the history
…t-Length=0 when Transfer-Encoding=chunked. Refactored test classes for easier/cleaner testing.

Signed-off-by: Doug Hoard <[email protected]>
  • Loading branch information
dhoard authored and fstab committed Dec 22, 2021
1 parent fd931c1 commit ec6def4
Show file tree
Hide file tree
Showing 4 changed files with 577 additions and 236 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,9 @@ public void handle(HttpExchange t) throws IOException {
}
} else {
long contentLength = response.size();
t.getResponseHeaders().set("Content-Length", String.valueOf(contentLength));
if (contentLength > 0) {
t.getResponseHeaders().set("Content-Length", String.valueOf(contentLength));
}
if (t.getRequestMethod().equals("HEAD")) {
contentLength = -1;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
package io.prometheus.client.exporter;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.xml.bind.DatatypeConverter;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.security.GeneralSecurityException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;

/**
* Class to perform HTTP testing
*/
public class HttpRequest {

enum METHOD { GET, HEAD }

private final Configuration configuration;

/**
* Constructor
*
* @param configuration configuration
*/
private HttpRequest(Configuration configuration) {
this.configuration = configuration;
}

/**
* Method to execute an HTTP request
*
* @return HttpResponse
* @throws IOException
*/
public HttpResponse execute() throws IOException {
if (configuration.url.toLowerCase().startsWith("https://") && (configuration.trustManagers != null)) {
try {
SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, configuration.trustManagers, new java.security.SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
} catch (GeneralSecurityException e) {
throw new IOException(e);
}

if (configuration.hostnameVerifier != null) {
HttpsURLConnection.setDefaultHostnameVerifier(configuration.hostnameVerifier);
}
}

URLConnection urlConnection = new URL(configuration.url).openConnection();
((HttpURLConnection) urlConnection).setRequestMethod(configuration.method.toString());

Set<Map.Entry<String, List<String>>> entries = configuration.headers.entrySet();
for (Map.Entry<String, List<String>> entry : entries) {
for (String value : entry.getValue()) {
urlConnection.addRequestProperty(entry.getKey(), value);
}
}

urlConnection.setUseCaches(false);
urlConnection.setDoInput(true);
urlConnection.setDoOutput(true);
urlConnection.connect();

Scanner scanner = new Scanner(urlConnection.getInputStream(), "UTF-8").useDelimiter("\\A");

return new HttpResponse(
((HttpURLConnection) urlConnection).getResponseCode(),
urlConnection.getHeaderFields(),
urlConnection.getContentLength(), scanner.hasNext() ? scanner.next() : "");
}

/**
* Class to build an HttpRequest
*/
static class Builder {

private final Configuration configuration;

/**
* Constructor
*/
public Builder() {
configuration = new Configuration();
}

/**
* Method to set the HTTP request method
*
* @param method
* @return Builder
*/
public Builder withMethod(METHOD method) {
configuration.method = method;
return this;
}

/**
* Method to set the HTTP request URL
*
* @param url
* @return Builder
*/
public Builder withURL(String url) {
configuration.url = url;
return this;
}

/**
* Method to add an HTTP request header
*
* @param name
* @param value
* @return Builder
*/
public Builder withHeader(String name, String value) {
configuration.addHeader(name, value);
return this;
}

/**
* Method to set the HTTP request "Authorization" header
*
* @param username
* @param password
* @return Builder
*/
public Builder withAuthorization(String username, String password) {
configuration.setHeader("Authorization", encodeCredentials(username, password));
return this;
}

/**
* Method to set the HTTP request trust managers when using an SSL URL
*
* @param trustManagers
* @return Builder
*/
public Builder withTrustManagers(TrustManager[] trustManagers) {
configuration.trustManagers = trustManagers;
return this;
}

/**
* Method to set the HTTP request hostname verifier when using an SSL URL
*
* @param hostnameVerifier
* @return Builder
*/
public Builder withHostnameVerifier(HostnameVerifier hostnameVerifier) {
configuration.hostnameVerifier = hostnameVerifier;
return this;
}

/**
* Method to build the HttpRequest
*
* @return HttpRequest
*/
public HttpRequest build() {
return new HttpRequest(configuration);
}
}

/**
* Class used for Builder configuration
*/
private static class Configuration {

public METHOD method;
public String url;
public Map<String, List<String>> headers;
public TrustManager[] trustManagers;
public HostnameVerifier hostnameVerifier;

/**
* Constructor
*/
Configuration() {
method = METHOD.GET;
headers = new HashMap<String, List<String>>();
}

/**
* Method to add (append) an HTTP request header
*
* @param name
* @param value
* @return Configuration
*/
void addHeader(String name, String value) {
name = name.toLowerCase();
List<String> values = headers.get(name);
if (values == null) {
values = new LinkedList<String>();
headers.put(name, values);
}

values.add(value);
}

/**
* Method to set (overwrite) an HTTP request header, removing all previous header values
*
* @param name
* @param value
* @return Configuration
*/
void setHeader(String name, String value) {
List<String> values = new LinkedList<String>();
values.add(value);
headers.put(name, values);
}
}

/**
* Method to encode "Authorization" credentials
*
* @param username
* @param password
* @return String
*/
private final static String encodeCredentials(String username, String password) {
// Per RFC4648 table 2. We support Java 6, and java.util.Base64 was only added in Java 8,
try {
byte[] credentialsBytes = (username + ":" + password).getBytes("UTF-8");
return "Basic " + DatatypeConverter.printBase64Binary(credentialsBytes);
} catch (UnsupportedEncodingException e) {
throw new IllegalArgumentException(e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package io.prometheus.client.exporter;

import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
* Class to perform HTTP testing
*/
class HttpResponse {

private int responseCode;
private Map<String, List<String>> headers;
private long contentLength = -1;
private String body;

/**
* Constructor
*
* @param responseCode
* @param headers
* @param contentLength
* @param body
*/
HttpResponse(int responseCode, Map<String, List<String>> headers, long contentLength, String body) throws IOException {
this.responseCode = responseCode;
this.body = body;
this.contentLength = contentLength;
this.headers = new HashMap<String, List<String>>();

Set<Map.Entry<String, List<String>>> headerSet = headers.entrySet();
for (String header : headers.keySet()) {
if (header != null) {
List<String> values = headers.get(header);
this.headers.put(header.toLowerCase(), values);
}
}

if (getHeader("content-length") != null && getHeader("transfer-encoding") != null) {
throw new IOException("Invalid HTTP response, should only contain Connect-Length or Transfer-Encoding");
}
}

/**
* Method to get the HTTP response code
*
* @return int
*/
public int getResponseCode() {
return this.responseCode;
}

/**
* Method to get a list of HTTP response headers values
*
* @param name
* @return List<String>
*/
public List<String> getHeaderList(String name) {
return headers.get(name.toLowerCase());
}

/**
* Method to get the first HTTP response header value
*
* @param name
* @return String
*/
public String getHeader(String name) {
String value = null;

List<String> valueList = getHeaderList(name);
if (valueList != null && (valueList.size() >= 0)) {
value = valueList.get(0);
}

return value;
}

/**
* Method to get the first HTTP response header value as a Long.
* Returns null of the header doesn't exist
*
* @param name
* @return Long
*/
public Long getHeaderAsLong(String name) {
String value = getHeader(name);
if (value != null) {
try {
return Long.valueOf(value);
} catch (Exception e) {
}
}

return null;
}

/**
* Method to get the HTTP response body
*
* @return String
*/
public String getBody() {
return body;
}
}
Loading

0 comments on commit ec6def4

Please sign in to comment.