אחד הכאבים המפתיעים והלא צפויים שנתקלתי בהם לאחרונה עם ג'אווה היה כשניסיתי לתקשר עם שרת HTTPS שנחתם בחתימה של StartSSL.
מסתבר שג'אווה מגיעה כמעט בלי חתימות של ספקי חתימות (verisign שם, אבל הרבה מאוד אחרים לא).
חפירות באינטרנט הובילו לכל מני פתרונות שלא עבדו, אולי כי החתימה שלי היא Wildcard certificate (*.site.com).
פתרון אפשרי הוא ליבא את חתימת השורש של StartSSL לתוך הJVM, אבל זו פעולה ידנית שכל משתמש צריך לעשות ובכל מקרה היא לא עבדה לי, אולי בגלל סוג החתימה.
כל זה מעצבן למדי, הדפדפן סומך על האתר אבל ג'אווה לא מסכימה להתחבר:
נסיון להשתמש בURL הרגיל של ג'אווה כדי להתחבר בHTTPS בדרך כלל מוביל לשגיאה הנפלאה הבאה (אלא אם מדובר בחתימה שחתם השורש שלה ידוע לJVM):
Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:294)
at sun.security.validator.PKIXValidator.engineValidate(PKIXValidator.java:200)
at sun.security.validator.Validator.validate(Validator.java:218)
at com.sun.net.ssl.internal.ssl.X509TrustManagerImpl.validate(X509TrustManagerImpl.java:126)
at com.sun.net.ssl.internal.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:209)
at com.sun.net.ssl.internal.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:249)
at com.sun.net.ssl.internal.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1053)
... 16 more
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at sun.security.provider.certpath.SunCertPathBuilder.engineBuild(SunCertPathBuilder.java:174)
at java.security.cert.CertPathBuilder.build(CertPathBuilder.java:238)
at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:289)
פתרון:
מעבר לHTTPClient של אפאצ'י ושימוש בקוד הבא (דוגמא לGET ולPOST)
התיעוד של הספריה לא משהו, יש כמה גרסאות לא תואמות שלה והתיעוד מתייחס לגרסא ישנה.
הקוד הבא עובד עם גרסא 4.0.3 של HTTPClient ו4.1.0 של HTTPCore (זו תלות נדרשת לHTTPClient, אפשר להוריד מאותו אתר).
קחו בחשבון שהפתרון הזה מאפשר תקיפת MAN IN THE MIDDLE כדי לזייף את החתימה, אבל מבחינה פרקטית עדיף משהו שעובד אבל קצת פגיע מאשר משהו שלא עובד בכלל. (וכמובן ששימוש בHTTPS הוא עדיף על שימוש בHTTP נקי בכל מקרה).
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.util.EntityUtils;
public class HTTPClientSSLExample
{
public static void main(String[] args) throws MalformedURLException, IOException
{
byte[] bytes = getURLBytes_httpclient("https://www.startssl.com/", 30000, 30000);
System.out.println(new String(bytes));
}
public static byte[] getURLBytes_httpclient(String url, int connectionTimeout, int readTimeout) throws IOException
{
long now = System.currentTimeMillis();
DefaultHttpClient httpclient = getHttpClient(url);
httpclient.getParams().setIntParameter(CoreConnectionPNames.SO_TIMEOUT, readTimeout);
httpclient.getParams().setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, connectionTimeout);
HttpGet get = new HttpGet(url);
HttpResponse response = httpclient.execute(get);
int res = response.getStatusLine().getStatusCode();
if (res == 200)
{
HttpEntity entity = response.getEntity();
int len = (int) entity.getContentLength();
InputStream in = entity.getContent();
ByteArrayOutputStream bout = new ByteArrayOutputStream(len > 0 ? len : 1000);
pump(in, bout);
return bout.toByteArray();
}
else
{
String bs = "";
try
{
HttpEntity entity = response.getEntity();
bs = entity == null ? null : EntityUtils.toString(entity);
}
catch (IOException e)
{
bs += " || Exception while trying to read data from stream : " + e.getMessage();
}
throw new IOException("Server returned HTTP " + res + " after " + (System.currentTimeMillis() - now) + " ms, URL : " + url + " data: " + bs);
}
}
public static ByteArrayInputStream openInputStream_httpclient(String url, int connectionTimeout, int readTimeout, byte[] postdata) throws IOException
{
if (postdata != null)
{
DefaultHttpClient httpClient = getHttpClient(url);
HttpPost post = new HttpPost(url);
InputStreamEntity reqEntity = new InputStreamEntity(new ByteArrayInputStream(postdata), postdata.length);
reqEntity.setContentType("binary/octet-stream");
post.setEntity(reqEntity);
HttpResponse response = httpClient.execute(post);
int res = response.getStatusLine().getStatusCode();
if (res == 200)
{
HttpEntity entity = response.getEntity();
int len = (int) entity.getContentLength();
InputStream in = entity.getContent();
ByteArrayOutputStream bout = new ByteArrayOutputStream(len > 0 ? len : 1000);
pump(in, bout);
return new ByteArrayInputStream(bout.toByteArray());
}
else
{
throw new IOException("Http response code " + res);
}
}
else
{
byte[] bytes = getURLBytes_httpclient(url, connectionTimeout, readTimeout);
return new ByteArrayInputStream(bytes);
}
}
private static DefaultHttpClient getHttpClient(String url1) throws IOException
{
DefaultHttpClient httpclient = new DefaultHttpClient();
try
{
SSLContext ctx = SSLContext.getInstance("TLS");
X509TrustManager tm = new X509TrustManager()
{
public void checkClientTrusted(X509Certificate[] xcs, String string) throws CertificateException
{
}
public void checkServerTrusted(X509Certificate[] xcs, String string) throws CertificateException
{
}
public X509Certificate[] getAcceptedIssuers()
{
return null;
}
};
ctx.init(null, new TrustManager[]
{
tm
}, null);
SSLSocketFactory ssf = new SSLSocketFactory(ctx);
ssf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
ClientConnectionManager ccm = httpclient.getConnectionManager();
SchemeRegistry sr = ccm.getSchemeRegistry();
sr.register(new Scheme("https", ssf, 443));
httpclient = new DefaultHttpClient(ccm, httpclient.getParams());
}
catch (Exception ex)
{
ex.printStackTrace();
return null;
}
// This block handles urls with user:password@server block
URL u = new URL(url1);
String userInfo = u.getUserInfo();
if (userInfo != null)
{
String user;
String password;
int i = userInfo != null ? userInfo.indexOf(':') : -1;
if (i == -1)
{
user = userInfo.substring(0);
password = "";
}
else
{
user = userInfo.substring(0, i);
password = userInfo.substring(i + 1);
}
httpclient.getCredentialsProvider().setCredentials(new AuthScope(u.getHost(), u.getPort()), new UsernamePasswordCredentials(user, password));
}
return httpclient;
}
/**
* Writes the bytes read from the given input stream into the given output
* stream until the end of the input stream is reached. Returns the amount
* of bytes actually read/written.
*/
public static int pump(InputStream in, OutputStream out) throws IOException
{
byte[] buf = new byte[4096];
int count;
int amountRead = 0;
while ((count = in.read(buf)) != -1)
{
out.write(buf, 0, count);
amountRead += count;
}
return amountRead;
}
}