package com.getsentry.raven.connection; import com.getsentry.raven.environment.RavenEnvironment; import com.getsentry.raven.event.Event; import com.getsentry.raven.marshaller.Marshaller; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLSession; import java.io.*; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.Proxy; import java.net.URI; import java.net.URL; import java.nio.charset.Charset; import java.util.concurrent.TimeUnit; /** * Basic connection to a Sentry server, using HTTP and HTTPS. * <p> * It is possible to enable the "naive mode" to allow a connection over SSL using a certificate with a wildcard. */ public class HttpConnection extends AbstractConnection { private static final Charset UTF_8 = Charset.forName("UTF-8"); private static final Logger logger = LoggerFactory.getLogger(HttpConnection.class); /** * HTTP Header for the user agent. */ private static final String USER_AGENT = "User-Agent"; /** * HTTP Header for the authentication to Sentry. */ private static final String SENTRY_AUTH = "X-Sentry-Auth"; /** * Default timeout of an HTTP connection to Sentry. */ private static final int DEFAULT_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(1); /** * HostnameVerifier allowing wildcard certificates to work without adding them to the truststore. */ private static final HostnameVerifier NAIVE_VERIFIER = new HostnameVerifier() { @Override public boolean verify(String hostname, SSLSession sslSession) { return true; } }; /** * URL of the Sentry endpoint. */ private final URL sentryUrl; /** * Optional instance of an HTTP proxy server to use. */ private final Proxy proxy; /** * Optional instance of an EventSampler to use. */ private EventSampler eventSampler; /** * Marshaller used to transform and send the {@link Event} over a stream. */ private Marshaller marshaller; /** * Timeout of an HTTP connection to Sentry. */ private int timeout = DEFAULT_TIMEOUT; /** * Setting allowing to bypass the security system which requires wildcard certificates * to be added to the truststore. */ private boolean bypassSecurity = false; /** * Creates an HTTP connection to a Sentry server. * * @param sentryUrl URL to the Sentry API. * @param publicKey public key of the current project. * @param secretKey private key of the current project. * @param proxy address of HTTP proxy or null if using direct connections. * @param eventSampler EventSampler instance to use, or null to not sample events. */ public HttpConnection(URL sentryUrl, String publicKey, String secretKey, Proxy proxy, EventSampler eventSampler) { super(publicKey, secretKey); this.sentryUrl = sentryUrl; this.proxy = proxy; this.eventSampler = eventSampler; } /** * Automatically determines the URL to the HTTP API of Sentry. * * @param sentryUri URI of the Sentry instance. * @param projectId unique identifier of the current project. * @return an URL to the HTTP API of Sentry. */ public static URL getSentryApiUrl(URI sentryUri, String projectId) { try { String url = sentryUri.toString() + "api/" + projectId + "/store/"; return new URL(url); } catch (MalformedURLException e) { throw new IllegalArgumentException("Couldn't build a valid URL from the Sentry API.", e); } } /** * Opens a connection to the Sentry API allowing to send new events. * * @return an HTTP connection to Sentry. */ protected HttpURLConnection getConnection() { try { HttpURLConnection connection; if (proxy != null) { connection = (HttpURLConnection) sentryUrl.openConnection(proxy); } else { connection = (HttpURLConnection) sentryUrl.openConnection(); } if (bypassSecurity && connection instanceof HttpsURLConnection) { ((HttpsURLConnection) connection).setHostnameVerifier(NAIVE_VERIFIER); } connection.setRequestMethod("POST"); connection.setDoOutput(true); connection.setConnectTimeout(timeout); connection.setRequestProperty(USER_AGENT, RavenEnvironment.getRavenName()); connection.setRequestProperty(SENTRY_AUTH, getAuthHeader()); return connection; } catch (IOException e) { throw new IllegalStateException("Couldn't set up a connection to the Sentry server.", e); } } @Override protected void doSend(Event event) throws ConnectionException { if (eventSampler != null && !eventSampler.shouldSendEvent(event)) { return; } HttpURLConnection connection = getConnection(); try { connection.connect(); OutputStream outputStream = connection.getOutputStream(); marshaller.marshall(event, outputStream); outputStream.close(); connection.getInputStream().close(); } catch (IOException e) { String errorMessage = null; final InputStream errorStream = connection.getErrorStream(); if (errorStream != null) { errorMessage = getErrorMessageFromStream(errorStream); } if (null == errorMessage || errorMessage.isEmpty()) { errorMessage = "An exception occurred while submitting the event to the Sentry server."; } Long retryAfterMs = null; String retryAfterHeader = connection.getHeaderField("Retry-After"); if (retryAfterHeader != null) { // CHECKSTYLE.OFF: EmptyCatchBlock try { // CHECKSTYLE.OFF: MagicNumber retryAfterMs = Long.parseLong(retryAfterHeader) * 1000L; // seconds -> milliseconds // CHECKSTYLE.ON: MagicNumber } catch (NumberFormatException nfe) { // noop, use default retry } // CHECKSTYLE.ON: EmptyCatchBlock } throw new ConnectionException(errorMessage, e, retryAfterMs); } finally { connection.disconnect(); } } private String getErrorMessageFromStream(InputStream errorStream) { BufferedReader reader = new BufferedReader(new InputStreamReader(errorStream, UTF_8)); StringBuilder sb = new StringBuilder(); try { String line; // ensure we do not add "\n" to the last line boolean first = true; while ((line = reader.readLine()) != null) { if (!first) { sb.append("\n"); } sb.append(line); first = false; } } catch (Exception e2) { logger.error("Exception while reading the error message from the connection.", e2); } return sb.toString(); } public void setTimeout(int timeout) { this.timeout = timeout; } public void setMarshaller(Marshaller marshaller) { this.marshaller = marshaller; } public void setBypassSecurity(boolean bypassSecurity) { this.bypassSecurity = bypassSecurity; } @Override public void close() throws IOException { } }