Skip to content

Commit 1dc65d6

Browse files
committed
Add ValidationClient
1 parent 453b1c3 commit 1dc65d6

File tree

4 files changed

+429
-0
lines changed

4 files changed

+429
-0
lines changed
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package com.twilio.http;
2+
3+
4+
import com.google.common.collect.Lists;
5+
import com.twilio.Twilio;
6+
import com.twilio.exception.ApiException;
7+
import org.apache.http.Header;
8+
import org.apache.http.HttpHeaders;
9+
import org.apache.http.HttpResponse;
10+
import org.apache.http.HttpVersion;
11+
import org.apache.http.client.config.RequestConfig;
12+
import org.apache.http.client.methods.RequestBuilder;
13+
import org.apache.http.impl.client.HttpClientBuilder;
14+
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
15+
import org.apache.http.message.BasicHeader;
16+
17+
import java.io.IOException;
18+
import java.nio.charset.StandardCharsets;
19+
import java.util.Collection;
20+
import java.util.List;
21+
import java.util.Map;
22+
23+
public class ValidationClient extends HttpClient {
24+
25+
private static final int CONNECTION_TIMEOUT = 10000;
26+
private static final int SOCKET_TIMEOUT = 30500;
27+
28+
private final org.apache.http.client.HttpClient client;
29+
30+
public ValidationClient(String credentialSid, String publicKey) {
31+
RequestConfig config = RequestConfig.custom()
32+
.setConnectTimeout(CONNECTION_TIMEOUT)
33+
.setSocketTimeout(SOCKET_TIMEOUT)
34+
.build();
35+
36+
Collection<Header> headers = Lists.<Header>newArrayList(
37+
new BasicHeader("X-Twilio-Client", "java-" + Twilio.VERSION),
38+
new BasicHeader(HttpHeaders.USER_AGENT, "twilio-java/" + Twilio.VERSION + " (" + Twilio.JAVA_VERSION + ")"),
39+
new BasicHeader(HttpHeaders.ACCEPT, "application/json"),
40+
new BasicHeader(HttpHeaders.ACCEPT_ENCODING, "utf-8")
41+
);
42+
43+
client = HttpClientBuilder.create()
44+
.setConnectionManager(new PoolingHttpClientConnectionManager())
45+
.setDefaultRequestConfig(config)
46+
.setDefaultHeaders(headers)
47+
.setMaxConnPerRoute(10)
48+
.addInterceptorLast(new ValidationInterceptor(credentialSid, publicKey))
49+
.build();
50+
}
51+
52+
@Override
53+
public Response makeRequest(Request request) {
54+
RequestBuilder builder = RequestBuilder.create(request.getMethod().toString())
55+
.setUri(request.constructURL().toString())
56+
.setVersion(HttpVersion.HTTP_1_1)
57+
.setCharset(StandardCharsets.UTF_8);
58+
59+
if (request.requiresAuthentication()) {
60+
builder.addHeader(HttpHeaders.AUTHORIZATION, request.getAuthString());
61+
}
62+
63+
HttpMethod method = request.getMethod();
64+
if (method == HttpMethod.POST) {
65+
builder.addHeader(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded");
66+
67+
for (Map.Entry<String, List<String>> entry : request.getPostParams().entrySet()) {
68+
for (String value : entry.getValue()) {
69+
builder.addParameter(entry.getKey(), value);
70+
}
71+
}
72+
}
73+
74+
try {
75+
HttpResponse response = client.execute(builder.build());
76+
return new Response(
77+
response.getEntity() == null ? null : response.getEntity().getContent(),
78+
response.getStatusLine().getStatusCode()
79+
);
80+
} catch (IOException e) {
81+
throw new ApiException(e.getMessage());
82+
}
83+
}
84+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.twilio.http;
2+
3+
import com.google.common.collect.Lists;
4+
import com.twilio.jwt.Jwt;
5+
import com.twilio.jwt.validation.ValidationToken;
6+
import org.apache.http.HttpException;
7+
import org.apache.http.HttpRequest;
8+
import org.apache.http.HttpRequestInterceptor;
9+
import org.apache.http.protocol.HttpContext;
10+
11+
import java.io.IOException;
12+
import java.util.List;
13+
14+
public class ValidationInterceptor implements HttpRequestInterceptor {
15+
16+
private static final List<String> HEADERS = Lists.newArrayList("authorization", "host");
17+
18+
private final String credentialSid;
19+
private final String privateKey;
20+
21+
public ValidationInterceptor(String credentialSid, String privateKey) {
22+
this.credentialSid = credentialSid;
23+
this.privateKey = privateKey;
24+
}
25+
26+
@Override
27+
public void process(HttpRequest request, HttpContext context) throws HttpException, IOException {
28+
Jwt jwt = ValidationToken.fromHttpRequest(credentialSid, privateKey, request, HEADERS);
29+
request.addHeader("Twilio-Client-Validation", jwt.toJwt());
30+
}
31+
}
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
package com.twilio.jwt.validation;
2+
3+
import com.google.common.base.Charsets;
4+
import com.google.common.base.Function;
5+
import com.google.common.base.Joiner;
6+
import com.google.common.hash.HashFunction;
7+
import com.google.common.hash.Hashing;
8+
import com.google.common.io.CharStreams;
9+
import com.twilio.http.HttpMethod;
10+
import com.twilio.jwt.Jwt;
11+
import io.jsonwebtoken.SignatureAlgorithm;
12+
import org.apache.http.Header;
13+
import org.apache.http.HttpEntity;
14+
import org.apache.http.HttpEntityEnclosingRequest;
15+
import org.apache.http.HttpRequest;
16+
import org.apache.http.message.BasicHeader;
17+
18+
import java.io.IOException;
19+
import java.io.InputStreamReader;
20+
import java.util.Arrays;
21+
import java.util.Collections;
22+
import java.util.Comparator;
23+
import java.util.Date;
24+
import java.util.HashMap;
25+
import java.util.List;
26+
import java.util.Map;
27+
28+
29+
public class ValidationToken extends Jwt {
30+
31+
private static final HashFunction HASH_FUNCTION = Hashing.sha256();
32+
private static final String NEW_LINE = "\n";
33+
34+
private final String method;
35+
private final String uri;
36+
private final String queryString;
37+
private final Header[] headers;
38+
private final List<String> signedHeaders;
39+
private final String requestBody;
40+
41+
private ValidationToken(Builder b) {
42+
super(
43+
SignatureAlgorithm.HS256,
44+
b.privateKey,
45+
b.credentialSid,
46+
new Date(new Date().getTime() + b.ttl * 1000)
47+
);
48+
this.method = b.method;
49+
this.uri = b.uri;
50+
this.queryString = b.queryString;
51+
this.headers = b.headers;
52+
this.signedHeaders = b.signedHeaders;
53+
this.requestBody = b.requestBody;
54+
}
55+
56+
@Override
57+
public Map<String, Object> getHeaders() {
58+
return Collections.emptyMap();
59+
}
60+
61+
@Override
62+
public Map<String, Object> getClaims() {
63+
Map<String, Object> payload = new HashMap<>();
64+
65+
// Sort the signed headers
66+
Collections.sort(signedHeaders);
67+
String includedHeaders = Joiner.on(";").join(signedHeaders);
68+
payload.put("hrh", includedHeaders);
69+
70+
// Add the method and uri
71+
StringBuilder signature = new StringBuilder();
72+
signature.append(method).append(NEW_LINE);
73+
signature.append(uri).append(NEW_LINE);
74+
75+
// Get the query args, sort and rejoin
76+
String[] queryArgs = queryString.split("&");
77+
Arrays.sort(queryArgs);
78+
String sortedQueryString = Joiner.on("&").join(queryArgs);
79+
signature.append(sortedQueryString).append(NEW_LINE);
80+
81+
// Normalize all the headers
82+
Header[] lowercaseHeaders = LOWERCASE_KEYS.apply(headers);
83+
Arrays.sort(lowercaseHeaders, new Comparator<Header>() {
84+
@Override
85+
public int compare(Header o1, Header o2) {
86+
return o1.getName().compareTo(o2.getName());
87+
}
88+
});
89+
90+
// Add the headers that we care about
91+
for (Header header: lowercaseHeaders) {
92+
if (signedHeaders.contains(header.getName().toLowerCase())) {
93+
signature.append(header.getName().toLowerCase().trim())
94+
.append(":")
95+
.append(header.getValue().trim())
96+
.append(NEW_LINE);
97+
}
98+
}
99+
signature.append(NEW_LINE);
100+
101+
// Mark the headers that we care about
102+
signature.append(includedHeaders).append(NEW_LINE);
103+
104+
// Hash and hex the request payload
105+
String hashedPayload = HASH_FUNCTION.hashString(requestBody, Charsets.UTF_8).toString();
106+
signature.append(hashedPayload).append(NEW_LINE);
107+
108+
// Hash and hex the canonical request
109+
String hashedSignature = HASH_FUNCTION.hashString(signature.toString(), Charsets.UTF_8).toString();
110+
payload.put("rqh", hashedSignature);
111+
112+
return payload;
113+
}
114+
115+
public static ValidationToken fromHttpRequest(
116+
String credentialSid,
117+
String privateKey,
118+
HttpRequest request,
119+
List<String> signedHeaders
120+
) throws IOException {
121+
Builder builder = new Builder(credentialSid, privateKey);
122+
123+
String method = request.getRequestLine().getMethod();
124+
builder.method(method);
125+
126+
String uri = request.getRequestLine().getUri();
127+
if (uri.contains("?")) {
128+
String[] uriParts = uri.split("\\?");
129+
builder.uri(uriParts[0]);
130+
builder.queryString(uriParts[1]);
131+
} else {
132+
builder.uri(uri);
133+
}
134+
135+
builder.headers(request.getAllHeaders());
136+
builder.signedHeaders(signedHeaders);
137+
138+
if (HttpMethod.POST.toString().equals(method.toUpperCase())) {
139+
HttpEntity entity = ((HttpEntityEnclosingRequest)request).getEntity();
140+
builder.requestBody(CharStreams.toString(new InputStreamReader(entity.getContent(), Charsets.UTF_8)));
141+
}
142+
143+
return builder.build();
144+
}
145+
146+
private static Function<Header[], Header[]> LOWERCASE_KEYS = new Function<Header[], Header[]>() {
147+
@Override
148+
public Header[] apply(Header[] headers) {
149+
Header[] lowercaseHeaders = new Header[headers.length];
150+
for (int i = 0; i < headers.length; i++) {
151+
lowercaseHeaders[i] = new BasicHeader(headers[i].getName().toLowerCase(), headers[i].getValue());
152+
}
153+
154+
return lowercaseHeaders;
155+
}
156+
};
157+
158+
public static class Builder {
159+
160+
private String credentialSid;
161+
private String privateKey;
162+
private String method;
163+
private String uri;
164+
private String queryString = "";
165+
private Header[] headers;
166+
private List<String> signedHeaders = Collections.emptyList();
167+
private String requestBody = "";
168+
private int ttl = 3600;
169+
170+
public Builder(String credentialSid, String privateKey) {
171+
this.credentialSid = credentialSid;
172+
this.privateKey = privateKey;
173+
}
174+
175+
public Builder method(String method) {
176+
this.method = method;
177+
return this;
178+
}
179+
180+
public Builder uri(String uri) {
181+
this.uri = uri;
182+
return this;
183+
}
184+
185+
public Builder queryString(String queryString) {
186+
this.queryString = queryString;
187+
return this;
188+
}
189+
190+
public Builder headers(Header[] headers) {
191+
this.headers = headers;
192+
return this;
193+
}
194+
195+
public Builder signedHeaders(List<String> signedHeaders) {
196+
this.signedHeaders = signedHeaders;
197+
return this;
198+
}
199+
200+
public Builder requestBody(String requestBody) {
201+
this.requestBody = requestBody;
202+
return this;
203+
}
204+
205+
public Builder ttl(int ttl) {
206+
this.ttl = ttl;
207+
return this;
208+
}
209+
210+
public ValidationToken build() {
211+
return new ValidationToken(this);
212+
}
213+
}
214+
215+
}

0 commit comments

Comments
 (0)