/*
ESXX - The friendly ECMAscript/XML Application Server
Copyright (C) 2007-2015 Martin Blom <martin@blom.org>
This program is free software: you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation, either version 3
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.esxx.js.protocol;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.cert.X509Certificate;
import java.util.*;
import java.util.regex.Pattern;
import javax.mail.internet.ContentType;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import org.apache.http.*;
import org.apache.http.auth.*;
import org.apache.http.client.*;
import org.apache.http.client.methods.*;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.conn.*;
import org.apache.http.conn.params.*;
import org.apache.http.conn.routing.*;
import org.apache.http.conn.scheme.*;
import org.apache.http.conn.ssl.*;
import org.apache.http.entity.*;
import org.apache.http.impl.client.*;
import org.apache.http.impl.conn.tsccm.*;
import org.apache.http.params.*;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.ExecutionContext;
import org.apache.http.protocol.HttpContext;
import org.esxx.ESXX;
import org.esxx.ESXXException;
import org.esxx.Response;
import org.esxx.js.*;
import org.esxx.oauth.OAuthCredentials;
import org.esxx.oauth.OAuthSchemeFactory;
import org.esxx.oauth.WRAPSchemeFactory;
import org.esxx.util.JS;
import org.mozilla.javascript.*;
public class HTTPHandler
extends URLHandler {
public HTTPHandler(JSURI jsuri)
throws URISyntaxException {
super(jsuri);
synchronized (HTTPHandler.class) {
if (preemptiveSchemes == null) {
preemptiveSchemes = new PreemptiveSchemes();
preemptiveInterceptor = new PreemptiveInterceptor();
// Purge preemptive cache peridically
ESXX.getInstance().getExecutor().scheduleWithFixedDelay(new Runnable() {
@Override public void run() {
preemptiveSchemes.purgeEntries();
}
}, 10, 10, java.util.concurrent.TimeUnit.SECONDS);
}
}
}
@Override
public Object load(Context cx, Scriptable thisObj, ContentType recv_ct)
throws Exception {
Result result = sendRequest(cx, thisObj, recv_ct, new HttpGet(jsuri.getURI()));
if (result.status < 200 || result.status >= 300) {
throw new JavaScriptException(makeJSResponse(cx, thisObj, result), null, 0);
}
return result.object;
}
@Override
public Object save(Context cx, Scriptable thisObj,
Object data, ContentType send_ct, ContentType recv_ct)
throws Exception {
HttpPut put = new HttpPut(jsuri.getURI());
attachObject(data, send_ct, put, cx);
Result result = sendRequest(cx, thisObj, recv_ct, put);
if (result.status < 200 || result.status >= 300) {
throw new JavaScriptException(makeJSResponse(cx, thisObj, result), null, 0);
}
return result.object;
}
@Override
public Object append(Context cx, Scriptable thisObj,
Object data, ContentType send_ct, ContentType recv_ct)
throws Exception {
HttpPost post = new HttpPost(jsuri.getURI());
attachObject(data, send_ct, post, cx);
Result result = sendRequest(cx, thisObj, recv_ct, post);
if (result.status < 200 || result.status >= 300) {
throw new JavaScriptException(makeJSResponse(cx, thisObj, result), null, 0);
}
return result.object;
}
@Override
public Object modify(Context cx, Scriptable thisObj,
Object data, ContentType send_ct, ContentType recv_ct)
throws Exception {
HttpPost patch = new HttpPost(jsuri.getURI()) {
@Override public String getMethod() {
return "PATCH";
}
};
attachObject(data, send_ct, patch, cx);
Result result = sendRequest(cx, thisObj, recv_ct, patch);
if (result.status < 200 || result.status >= 300) {
throw new JavaScriptException(makeJSResponse(cx, thisObj, result), null, 0);
}
return result.object;
}
@Override
public Object remove(Context cx, Scriptable thisObj,
ContentType recv_ct)
throws Exception {
Result result = sendRequest(cx, thisObj, recv_ct, new HttpDelete(jsuri.getURI()));
if (result.status < 200 || result.status >= 300) {
throw new JavaScriptException(makeJSResponse(cx, thisObj, result), null, 0);
}
return result.object;
}
@Override
public Object query(Context cx, Scriptable thisObj, Object[] args)
throws Exception {
if (args.length < 1) {
throw Context.reportRuntimeError("Missing arguments to URI.query().");
}
final String method = Context.toString(args[0]);
Scriptable headers = null;
Object send_obj = null;
ContentType send_ct = null;
ContentType recv_ct = null;
if (args.length >= 2) {
if (!(args[1] instanceof Scriptable)) {
throw Context.reportRuntimeError("Second URI.query() argument must be an Object");
}
headers = (Scriptable) args[1];
}
if (args.length >= 3) {
send_obj = args[2];
}
if (args.length >= 4) {
send_ct = new ContentType(Context.toString(args[3]));
}
if (args.length >= 5) {
recv_ct = new ContentType(Context.toString(args[4]));
}
HttpPost req = new HttpPost(jsuri.getURI()) {
@Override public String getMethod() {
return method;
}
};
if (headers != null) {
for (Object p : headers.getIds()) {
if (p instanceof String) {
req.addHeader((String) p,
Context.toString(headers.get((String) p, headers)));
}
}
}
if (send_obj != null && send_obj != Context.getUndefinedValue()) {
attachObject(send_obj, send_ct, req, cx);
}
Result result = sendRequest(cx, thisObj, recv_ct, req);
return makeJSResponse(cx, thisObj, result);
}
private static synchronized HttpParams getHttpParams() {
if (httpParams == null) {
httpParams = new BasicHttpParams();
HttpProtocolParams.setVersion(httpParams, HttpVersion.HTTP_1_1);
// No limits
ConnManagerParams.setMaxTotalConnections(httpParams, Integer.MAX_VALUE);
ConnManagerParams.setMaxConnectionsPerRoute(httpParams, new ConnPerRoute() {
public int getMaxForRoute(HttpRoute route) {
return Integer.MAX_VALUE;
}
});
}
return httpParams;
}
private static synchronized ClientConnectionManager getConnectionManager() {
if (connectionManager == null) {
SchemeRegistry sr = new SchemeRegistry();
sr.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
// sr.register(new Scheme("https", SSLSocketFactory.getSocketFactory(), 443));
try {
SSLContext sslcontext = SSLContext.getInstance(SSLSocketFactory.TLS);
sslcontext.init(null, new TrustManager[] { new X509TrustManager() {
@Override public void checkServerTrusted(X509Certificate[] chain, String auth) {}
@Override public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
@Override public void checkClientTrusted(X509Certificate[] certs, String auth) {}
} }, new java.security.SecureRandom());
SSLSocketFactory ssf = new SSLSocketFactory(sslcontext, null);
ssf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
sr.register(new Scheme("https", ssf, 443));
}
catch (Exception ex) {
ex.printStackTrace();
}
connectionManager = new ThreadSafeClientConnManager(getHttpParams(), sr);
}
return connectionManager;
}
public static synchronized void setConnectionManager(ClientConnectionManager c) {
connectionManager = c;
}
private synchronized HttpClient getHttpClient() {
if (httpClient == null) {
httpClient = new DefaultHttpClient(getConnectionManager(), getHttpParams());
httpClient.addRequestInterceptor(preemptiveInterceptor, 0);
httpClient.setCredentialsProvider(new ESXXCredentialsProvider());
httpClient.setTargetAuthenticationHandler(new ESXXAuthenticationHandler());
httpClient.setCookieStore(new CookieJar(jsuri));
httpClient.getAuthSchemes().register(OAuthSchemeFactory.SCHEME_NAME,
new OAuthSchemeFactory());
httpClient.getAuthSchemes().register(WRAPSchemeFactory.SCHEME_NAME,
new WRAPSchemeFactory());
}
return httpClient;
}
private static class Result {
public int status;
public Header[] headers;
public String contentType;
public Object object;
}
private Result sendRequest(Context cx, Scriptable thisObj,
ContentType ct,
final HttpUriRequest msg)
throws Exception {
// Add HTTP headers
jsuri.enumerateHeaders(cx, new JSURI.PropEnumerator() {
public void handleProperty(Scriptable p, int s) {
msg.addHeader(Context.toString(p.get("name", p)),
Context.toString(p.get("value", p)));
}
}, jsuri.getURI());
HttpResponse response = getHttpClient().execute(msg);
HttpEntity entity = response.getEntity();
try {
Result result = new Result();
result.status = response.getStatusLine().getStatusCode();
result.headers = response.getAllHeaders();
if (entity != null && entity.getContentLength() != 0) {
if (ct == null) {
Header hdr = entity.getContentType();
result.contentType = hdr == null ? "application/octet-stream" : hdr.getValue();
ct = new ContentType(result.contentType);
}
else {
result.contentType = ct.toString();
}
result.object = ESXX.getInstance().parseStream(ct, entity.getContent(), jsuri.getURI(),
null,
null,//js_esxx.jsGet_debug(),
cx, thisObj);
if (result.object instanceof java.io.InputStream) {
// Do not consume content yet
entity = null;
}
}
return result;
}
finally {
if (entity != null) {
entity.consumeContent();
}
}
}
private void attachObject(Object data, ContentType ct,
HttpEntityEnclosingRequest request, Context cx)
throws IOException {
// FIXME: This may store the data three times in memory -- If
// there were a way to turn the Object into an InputStream
// instead, we would not have this problem.
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ct = ESXX.getInstance().serializeObject(data, ct, bos, true);
ByteArrayEntity bae = new ByteArrayEntity(bos.toByteArray());
bae.setContentType(ct.toString());
request.setEntity(bae);
}
private static Scriptable makeJSResponse(Context cx, Scriptable scope, Result result) {
Scriptable hdr = cx.newObject(scope);
for (Header h : result.headers) {
hdr.put(h.getName(), hdr, h.getValue());
}
return JSESXX.newObject(cx, scope, "Response", new Object[] {
result.status, hdr, result.object, result.contentType
});
}
private static class PreemptiveInterceptor
implements HttpRequestInterceptor {
@Override public void process(HttpRequest request, HttpContext context)
throws HttpException, IOException {
try {
AuthState as = (AuthState)
context.getAttribute(ClientContext.TARGET_AUTH_STATE);
CredentialsProvider cp = (CredentialsProvider)
context.getAttribute(ClientContext.CREDS_PROVIDER);
HttpHost host = (HttpHost)
context.getAttribute(ExecutionContext.HTTP_TARGET_HOST);
String full_uri = host.toURI() + request.getRequestLine().getUri();
if (as.getAuthScheme() == null) {
// This request does not have an AuthScheme, so lets see if
// we've already been here, and if so, add the apporopriate
// authentication header.
AuthScheme scheme = null;
Credentials creds = null;
PreemptiveSchemes.PreemptiveScheme ps = preemptiveSchemes.find(full_uri);
if (ps != null) {
// We've been here before; use known state
scheme = ps.getScheme();
creds = cp.getCredentials(ps.getScope());
}
else {
// We've not been here before; check if the user wishes to
// force preemptive authentication
AuthScope scope = new AuthScope(host.getHostName(), host.getPort());
creds = cp.getCredentials(scope);
if (cp instanceof ESXXCredentialsProvider) {
ESXXCredentialsProvider ecp = (ESXXCredentialsProvider) cp;
AuthSchemeRegistry authreg = (AuthSchemeRegistry)
context.getAttribute(ClientContext.AUTHSCHEME_REGISTRY);
Auth auth = ecp.getAuth(scope);
if (auth.isPreemptive()) {
HttpParams params = getHttpParams().copy();
params.setParameter("defaultRealm", auth.getRealm());
scheme = authreg.getAuthScheme(auth.getMechanism(), params);
}
}
}
if (scheme != null && creds != null) {
as.setAuthScheme(scheme);
as.setCredentials(creds);
}
}
else {
// This is a request with authenication available. Make a
// note of this for the next time we access this host.
preemptiveSchemes.remember(host.toURI(), // RFC 2617 says all host URLs
as.getAuthScheme(),
as.getAuthScope());
}
}
catch (Exception ex) {
ex.printStackTrace();
}
}
}
private class ESXXCredentialsProvider
implements CredentialsProvider {
@Override public void clear() {
throw new UnsupportedOperationException("HttpURI.CredentialsProvider.clear()"
+ " not implemented.");
}
@Override public void setCredentials(AuthScope authscope, Credentials credentials) {
throw new UnsupportedOperationException("HttpURI.CredentialsProvider.setCredentials()"
+ " not implemented.");
}
@Override public Credentials getCredentials(AuthScope authscope) {
Auth auth = getAuth(authscope);
if ("oauth".equalsIgnoreCase(auth.getMechanism())) {
OAuthCredentials creds = new OAuthCredentials(auth.getUsername(), auth.getPassword());
creds.getAccessor().accessToken = auth.getUsername2();
creds.getAccessor().tokenSecret = auth.getPassword2();
return creds;
}
else if (auth.getUsername() != null) {
return new UsernamePasswordCredentials(auth.getUsername(), auth.getPassword());
}
else if (auth.getPassword() != null) {
return new UsernamePasswordCredentials("", auth.getPassword());
}
else {
return null;
}
}
public Auth getAuth(AuthScope authscope) {
try {
Scriptable auth = jsuri.getAuth(Context.getCurrentContext(),
new URI(jsuri.getURI().getScheme(),
null,
authscope.getHost(),
authscope.getPort(),
null, null, null),
authscope.getRealm(), authscope.getScheme());
return new Auth(auth);
}
catch (URISyntaxException ex) {
throw new ESXXException("Failed to convert AuthScope to URI: " + ex.getMessage(), ex);
}
}
}
private class ESXXAuthenticationHandler
extends DefaultTargetAuthenticationHandler {
@Override public AuthScheme selectScheme(Map<String, Header> challenges,
HttpResponse response,
HttpContext context)
throws AuthenticationException {
AuthSchemeRegistry registry = (AuthSchemeRegistry)
context.getAttribute(ClientContext.AUTHSCHEME_REGISTRY);
if (registry == null) {
throw new IllegalStateException("No AuthScheme registry");
}
Context cx = Context.getCurrentContext();
HttpHost h = (HttpHost) context.getAttribute(ExecutionContext.HTTP_TARGET_HOST);
URI uri = URI.create(h.toURI());
String[] mechanisms = jsuri.getAuthMechanisms(cx, uri, null, preferredSchemes);
for (String mechanism : mechanisms) {
Header challenge = challenges.get(mechanism.toLowerCase(Locale.ENGLISH));
try {
AuthScheme scheme = registry.getAuthScheme(mechanism, response.getParams());
scheme.processChallenge(challenge);
if (jsuri.getAuth(cx, uri, scheme.getRealm(), scheme.getSchemeName()) != null) {
// We've found a scheme that works -- use it
return scheme;
}
}
catch (Exception ignored) {
// Try next mechanism/scheme
}
}
throw new AuthenticationException("Unable to find a suitable AuthScheme for the" +
" provided challenges");
}
private String[] preferredSchemes = new String[] {
// Note: Lowercase names only, because of addAll() in JSURI.getAuthMechanisms()
"ntlm", "digest", "oauth", "wrap", "basic"
};
}
private static HttpParams httpParams;
private static ClientConnectionManager connectionManager;
private static PreemptiveSchemes preemptiveSchemes;
private static PreemptiveInterceptor preemptiveInterceptor;
private DefaultHttpClient httpClient;
private BasicHttpContext httpContext;
}