Skip to content

Commit

Permalink
Fix issue 16 by working around Java's lack of SNI support
Browse files Browse the repository at this point in the history
GetSentry.com uses Server Name Indication and Java versions before 7 do
not support this. Through the naiveSsl configuration option users are
able to work around this problem without having to add the certificate
to their keystore. This should only be used for testing purposes,
but... yeah.
  • Loading branch information
roam committed Jun 9, 2012
1 parent b7f1a12 commit 31a0b38
Show file tree
Hide file tree
Showing 7 changed files with 172 additions and 82 deletions.
10 changes: 10 additions & 0 deletions README.rst
Expand Up @@ -77,6 +77,16 @@ If you want to make sure log events always reach sentry, you can turn blocking o


WARNING: By setting blocking true, you will effectively lock up the thread doing the logging! Use with care. WARNING: By setting blocking true, you will effectively lock up the thread doing the logging! Use with care.


Naive SSL
^^^^^^^^^
If you're using Java 6 or earlier, you might encounter errors when connecting to a Sentry instance using a TLS (https)
connection since Server Name Indication (SNI) support has only been available in Java since Java 7. You can either add
the corresponding certificate to your keystore (recommended!) or enable ``naiveSsl`` on the Sentry appender. This will
make the Raven client use a custom hostname verifier that *will* allow the JVM to connect with the host - in fact it
will let the Raven client connect to any host even if the certificate is invalid. Use at your own risk::

log4j.appender.sentry.naiveSsl=true



SENTRY_DSN Environment Variable SENTRY_DSN Environment Variable
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
201 changes: 128 additions & 73 deletions src/main/java/net/kencochrane/sentry/RavenClient.java
Expand Up @@ -3,10 +3,14 @@
import org.json.simple.JSONArray; import org.json.simple.JSONArray;
import org.json.simple.JSONObject; import org.json.simple.JSONObject;


import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSession;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;


import static org.apache.commons.codec.binary.Base64.encodeBase64String; import static org.apache.commons.codec.binary.Base64.encodeBase64String;
Expand All @@ -23,23 +27,24 @@ public class RavenClient {
private RavenConfig config; private RavenConfig config;
private String sentryDSN; private String sentryDSN;
private String lastID; private String lastID;
private MessageSender messageSender;


public RavenClient() { public RavenClient() {
this.sentryDSN = System.getenv("SENTRY_DSN"); this.sentryDSN = System.getenv("SENTRY_DSN");
if (this.sentryDSN == null || this.sentryDSN.length() == 0) { if (this.sentryDSN == null || this.sentryDSN.length() == 0) {
throw new RuntimeException("You must provide a DSN to RavenClient"); throw new RuntimeException("You must provide a DSN to RavenClient");
} }
this.config = new RavenConfig(this.sentryDSN); setConfig(new RavenConfig(this.sentryDSN));
} }


public RavenClient(String sentryDSN) { public RavenClient(String sentryDSN) {
this.sentryDSN = sentryDSN; this.sentryDSN = sentryDSN;
this.config = new RavenConfig(sentryDSN); setConfig(new RavenConfig(sentryDSN));
} }


public RavenClient(String sentryDSN, String proxy) { public RavenClient(String sentryDSN, String proxy, boolean naiveSsl) {
this.sentryDSN = sentryDSN; this.sentryDSN = sentryDSN;
this.config = new RavenConfig(sentryDSN, proxy); setConfig(new RavenConfig(sentryDSN, proxy, naiveSsl));
} }


public RavenConfig getConfig() { public RavenConfig getConfig() {
Expand All @@ -48,6 +53,16 @@ public RavenConfig getConfig() {


public void setConfig(RavenConfig config) { public void setConfig(RavenConfig config) {
this.config = config; this.config = config;
try {
URL endpoint = new URL(config.getSentryURL());
if (config.isNaiveSsl() && "https".equals(endpoint.getProtocol())) {
messageSender = new NaiveHttpsMessageSender(config, endpoint);
} else {
messageSender = new MessageSender(config, endpoint);
}
} catch (MalformedURLException e) {
throw new RuntimeException("Sentry URL is malformed", e);
}
} }


public String getSentryDSN() { public String getSentryDSN() {
Expand Down Expand Up @@ -192,67 +207,15 @@ private String buildMessage(String message, String timestamp, String loggerClass
return buildMessageBody(jsonMessage); return buildMessageBody(jsonMessage);
} }



/**
* build up the sentry auth header in the following format
* <p/>
* The header is composed of a SHA1-signed HMAC, the timestamp from when the message was generated, and an
* arbitrary client version string. The client version should be something distinct to your client,
* and is simply for reporting purposes.
* <p/>
* X-Sentry-Auth: Sentry sentry_version=2.0,
* sentry_signature=<hmac signature>,
* sentry_timestamp=<signature timestamp>[,
* sentry_key=<public api key>,[
* sentry_client=<client version, arbitrary>]]
*
* @param hmacSignature SHA1-signed HMAC
* @param timestamp is the timestamp of which this message was generated
* @param publicKey is either the public_key or the shared global key between client and server.
* @return String version of the sentry auth header
*/
private String buildAuthHeader(String hmacSignature, long timestamp, String publicKey) {
StringBuilder header = new StringBuilder();
header.append("Sentry sentry_version=2.0,sentry_signature=");
header.append(hmacSignature);
header.append(",sentry_timestamp=");
header.append(timestamp);
header.append(",sentry_key=");
header.append(publicKey);
header.append(",sentry_client=");
header.append(RAVEN_JAVA_VERSION);

return header.toString();
}

/** /**
* Send the message to the sentry server. * Send the message to the sentry server.
* *
* @param messageBody the encoded json message we are sending to the sentry server * @param messageBody the encoded json message we are sending to the sentry server
* @param timestamp the timestamp of the message * @param timestamp the timestamp of the message
*/ */
private void sendMessage(String messageBody, long timestamp) { private void sendMessage(String messageBody, long timestamp) {
HttpURLConnection connection = null;
try { try {

messageSender.send(messageBody, timestamp);
// get the hmac Signature for the header
String hmacSignature = RavenUtils.getSignature(messageBody, timestamp, config.getSecretKey());

// get the auth header
String authHeader = buildAuthHeader(hmacSignature, timestamp, getConfig().getPublicKey());

URL endpoint = new URL(getConfig().getSentryURL());
connection = (HttpURLConnection) endpoint.openConnection(getConfig().getProxy());
connection.setRequestMethod("POST");
connection.setDoOutput(true);
connection.setReadTimeout(10000);
connection.setRequestProperty("X-Sentry-Auth", authHeader);
OutputStream output = connection.getOutputStream();
output.write(messageBody.getBytes());
output.close();
connection.connect();
InputStream input = connection.getInputStream();
input.close();
} catch (IOException e) { } catch (IOException e) {
// Eat the errors, we don't want to cause problems if there are major issues. // Eat the errors, we don't want to cause problems if there are major issues.
e.printStackTrace(); e.printStackTrace();
Expand All @@ -261,16 +224,16 @@ private void sendMessage(String messageBody, long timestamp) {


/** /**
* Send the log message to the sentry server. * Send the log message to the sentry server.
* * <p/>
* This method is deprecated. You should use captureMessage or captureException instead. * This method is deprecated. You should use captureMessage or captureException instead.
* *
* @deprecated
* @param theLogMessage The log message * @param theLogMessage The log message
* @param timestamp unix timestamp * @param timestamp unix timestamp
* @param loggerClass The class associated with the log message * @param loggerClass The class associated with the log message
* @param logLevel int value for Log level for message (DEBUG, ERROR, INFO, etc.) * @param logLevel int value for Log level for message (DEBUG, ERROR, INFO, etc.)
* @param culprit Who we think caused the problem. * @param culprit Who we think caused the problem.
* @param exception exception that occurred * @param exception exception that occurred
* @deprecated
*/ */
public void logMessage(String theLogMessage, long timestamp, String loggerClass, int logLevel, String culprit, Throwable exception) { public void logMessage(String theLogMessage, long timestamp, String loggerClass, int logLevel, String culprit, Throwable exception) {
String message = buildMessage(theLogMessage, RavenUtils.getTimestampString(timestamp), loggerClass, logLevel, culprit, exception); String message = buildMessage(theLogMessage, RavenUtils.getTimestampString(timestamp), loggerClass, logLevel, culprit, exception);
Expand All @@ -281,11 +244,11 @@ public void logMessage(String theLogMessage, long timestamp, String loggerClass,
/** /**
* Send the log message to the sentry server. * Send the log message to the sentry server.
* *
* @param message The log message * @param message The log message
* @param timestamp unix timestamp * @param timestamp unix timestamp
* @param loggerClass The class associated with the log message * @param loggerClass The class associated with the log message
* @param logLevel int value for Log level for message (DEBUG, ERROR, INFO, etc.) * @param logLevel int value for Log level for message (DEBUG, ERROR, INFO, etc.)
* @param culprit Who we think caused the problem. * @param culprit Who we think caused the problem.
* @return lastID The ID for the last message. * @return lastID The ID for the last message.
*/ */
public String captureMessage(String message, long timestamp, String loggerClass, int logLevel, String culprit) { public String captureMessage(String message, long timestamp, String loggerClass, int logLevel, String culprit) {
Expand All @@ -297,7 +260,7 @@ public String captureMessage(String message, long timestamp, String loggerClass,
/** /**
* Send the log message to the sentry server. * Send the log message to the sentry server.
* *
* @param message The log message * @param message The log message
* @return lastID The ID for the last message. * @return lastID The ID for the last message.
*/ */
public String captureMessage(String message) { public String captureMessage(String message) {
Expand All @@ -307,12 +270,12 @@ public String captureMessage(String message) {
/** /**
* Send the exception to the sentry server. * Send the exception to the sentry server.
* *
* @param message The log message * @param message The log message
* @param timestamp unix timestamp * @param timestamp unix timestamp
* @param loggerClass The class associated with the log message * @param loggerClass The class associated with the log message
* @param logLevel int value for Log level for message (DEBUG, ERROR, INFO, etc.) * @param logLevel int value for Log level for message (DEBUG, ERROR, INFO, etc.)
* @param culprit Who we think caused the problem. * @param culprit Who we think caused the problem.
* @param exception exception that occurred * @param exception exception that occurred
* @return lastID The ID for the last message. * @return lastID The ID for the last message.
*/ */
public String captureException(String message, long timestamp, String loggerClass, int logLevel, String culprit, Throwable exception) { public String captureException(String message, long timestamp, String loggerClass, int logLevel, String culprit, Throwable exception) {
Expand All @@ -324,10 +287,102 @@ public String captureException(String message, long timestamp, String loggerClas
/** /**
* Send an exception to the sentry server. * Send an exception to the sentry server.
* *
* @param exception exception that occurred * @param exception exception that occurred
* @return lastID The ID for the last message. * @return lastID The ID for the last message.
*/ */
public String captureException(Throwable exception) { public String captureException(Throwable exception) {
return captureException(exception.getMessage(), RavenUtils.getTimestampLong(), "root", 50, null, exception); return captureException(exception.getMessage(), RavenUtils.getTimestampLong(), "root", 50, null, exception);
} }

public static class MessageSender {

public final RavenConfig config;
public final URL endpoint;

public MessageSender(RavenConfig config, URL endpoint) {
this.config = config;
this.endpoint = endpoint;
}

public void send(String messageBody, long timestamp) throws IOException {
// get the hmac Signature for the header
String hmacSignature = RavenUtils.getSignature(messageBody, timestamp, config.getSecretKey());

// get the auth header
String authHeader = buildAuthHeader(hmacSignature, timestamp, config.getPublicKey());

HttpURLConnection connection = getConnection();
connection.setRequestMethod("POST");
connection.setDoOutput(true);
connection.setReadTimeout(10000);
connection.setRequestProperty("X-Sentry-Auth", authHeader);
OutputStream output = connection.getOutputStream();
output.write(messageBody.getBytes());
output.close();
connection.connect();
InputStream input = connection.getInputStream();
input.close();
}

/**
* Build up the sentry auth header in the following format.
* <p/>
* The header is composed of a SHA1-signed HMAC, the timestamp from when the message was generated, and an
* arbitrary client version string. The client version should be something distinct to your client,
* and is simply for reporting purposes.
* <p/>
* X-Sentry-Auth: Sentry sentry_version=2.0,
* sentry_signature=<hmac signature>,
* sentry_timestamp=<signature timestamp>[,
* sentry_key=<public api key>,[
* sentry_client=<client version, arbitrary>]]
*
* @param hmacSignature SHA1-signed HMAC
* @param timestamp is the timestamp of which this message was generated
* @param publicKey is either the public_key or the shared global key between client and server.
* @return String version of the sentry auth header
*/
protected String buildAuthHeader(String hmacSignature, long timestamp, String publicKey) {
StringBuilder header = new StringBuilder();
header.append("Sentry sentry_version=2.0,sentry_signature=");
header.append(hmacSignature);
header.append(",sentry_timestamp=");
header.append(timestamp);
header.append(",sentry_key=");
header.append(publicKey);
header.append(",sentry_client=");
header.append(RAVEN_JAVA_VERSION);

return header.toString();
}

protected HttpURLConnection getConnection() throws IOException {
return (HttpURLConnection) endpoint.openConnection(config.getProxy());
}
}

public static class NaiveHttpsMessageSender extends MessageSender {

public final HostnameVerifier hostnameVerifier;

public NaiveHttpsMessageSender(RavenConfig config, URL endpoint) {
super(config, endpoint);
this.hostnameVerifier = new AcceptAllHostnameVerifier();
}

@Override
protected HttpURLConnection getConnection() throws IOException {
HttpsURLConnection connection = (HttpsURLConnection) endpoint.openConnection(config.getProxy());
connection.setHostnameVerifier(hostnameVerifier);
return connection;
}
}

public static class AcceptAllHostnameVerifier implements HostnameVerifier {
@Override
public boolean verify(String hostname, SSLSession sslSession) {
return true;
}
}

} }
17 changes: 14 additions & 3 deletions src/main/java/net/kencochrane/sentry/RavenConfig.java
Expand Up @@ -13,24 +13,26 @@ public class RavenConfig {
private int port; private int port;
private String proxyType, proxyHost; private String proxyType, proxyHost;
private int proxyPort; private int proxyPort;
private boolean naiveSsl;


/** /**
* Takes in a sentryDSN and builds up the configuration * Takes in a sentryDSN and builds up the configuration
* *
* @param sentryDSN '{PROTOCOL}://{PUBLIC_KEY}:{SECRET_KEY}@{HOST}/{PATH}/{PROJECT_ID}' * @param sentryDSN '{PROTOCOL}://{PUBLIC_KEY}:{SECRET_KEY}@{HOST}/{PATH}/{PROJECT_ID}'
*/ */
public RavenConfig(String sentryDSN) { public RavenConfig(String sentryDSN) {
this(sentryDSN, null); this(sentryDSN, null, false);
} }


/** /**
* Takes in a sentryDSN and builds up the configuration * Takes in a sentryDSN and builds up the configuration
* *
* @param sentryDSN '{PROTOCOL}://{PUBLIC_KEY}:{SECRET_KEY}@{HOST}/{PATH}/{PROJECT_ID}' * @param sentryDSN '{PROTOCOL}://{PUBLIC_KEY}:{SECRET_KEY}@{HOST}/{PATH}/{PROJECT_ID}'
* @param proxy proxy to use for the HTTP connections; blank or null when no proxy is to be used * @param proxy proxy to use for the HTTP connections; blank or null when no proxy is to be used
* @param naiveSsl use a hostname verifier for SSL connections that allows all connections
*/ */
public RavenConfig(String sentryDSN, String proxy) { public RavenConfig(String sentryDSN, String proxy, boolean naiveSsl) {

this.naiveSsl = naiveSsl;
try { try {
URL url = new URL(sentryDSN); URL url = new URL(sentryDSN);
this.host = url.getHost(); this.host = url.getHost();
Expand Down Expand Up @@ -181,6 +183,14 @@ public void setPort(int port) {
this.port = port; this.port = port;
} }


public boolean isNaiveSsl() {
return naiveSsl;
}

public void setNaiveSsl(boolean naiveSsl) {
this.naiveSsl = naiveSsl;
}

@Override @Override
public String toString() { public String toString() {
return "RavenConfig{" + return "RavenConfig{" +
Expand All @@ -190,6 +200,7 @@ public String toString() {
", secretKey='" + secretKey + '\'' + ", secretKey='" + secretKey + '\'' +
", path='" + path + '\'' + ", path='" + path + '\'' +
", projectId='" + projectId + '\'' + ", projectId='" + projectId + '\'' +
", naiveSsl='" + naiveSsl + '\'' +
", SentryUrl='" + getSentryURL() + '\'' + ", SentryUrl='" + getSentryURL() + '\'' +
'}'; '}';
} }
Expand Down

0 comments on commit 31a0b38

Please sign in to comment.