blob: 4339b36cbf226f54669d86d5a5dccf2e97df8548 [file] [log] [blame]
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.errorreport.itn;
import com.intellij.diagnostic.DiagnosticBundle;
import com.intellij.errorreport.bean.ErrorBean;
import com.intellij.errorreport.error.InternalEAPException;
import com.intellij.errorreport.error.NoSuchEAPUserException;
import com.intellij.errorreport.error.UpdateAvailableException;
import com.intellij.idea.IdeaLogger;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ApplicationNamesInfo;
import com.intellij.openapi.application.ex.ApplicationInfoEx;
import com.intellij.openapi.diagnostic.Attachment;
import com.intellij.openapi.progress.EmptyProgressIndicator;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.updateSettings.impl.UpdateSettings;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.Consumer;
import com.intellij.util.SystemProperties;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.net.ssl.CertificateUtil;
import org.jetbrains.annotations.NotNull;
import javax.net.ssl.*;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Calendar;
import java.util.Map;
import java.util.Set;
/**
* @author stathik
* @since Aug 4, 2003
*/
public class ITNProxy {
private static final String NEW_THREAD_VIEW_URL = "https://ea.jetbrains.com/browser/ea_reports/";
private static final String NEW_THREAD_POST_URL = "https://ea-report.jetbrains.com/trackerRpc/idea/createScr";
private static final String ENCODING = "UTF8";
public static void sendError(Project project,
final String login,
final String password,
final ErrorBean error,
final Consumer<Integer> callback,
final Consumer<Exception> errback) {
if (StringUtil.isEmpty(login)) {
return;
}
Task.Backgroundable task = new Task.Backgroundable(project, DiagnosticBundle.message("title.submitting.error.report")) {
@Override
public void run(@NotNull ProgressIndicator indicator) {
try {
int threadId = postNewThread(login, password, error);
callback.consume(threadId);
}
catch (Exception ex) {
errback.consume(ex);
}
}
};
if (project == null) {
task.run(new EmptyProgressIndicator());
}
else {
ProgressManager.getInstance().run(task);
}
}
public static String getBrowseUrl(int threadId) {
return NEW_THREAD_VIEW_URL + threadId;
}
private static SSLContext ourSslContext;
private static int postNewThread(String login, String password, ErrorBean error) throws Exception {
if (ourSslContext == null) {
ourSslContext = initContext();
}
Map<String, String> params = createParameters(login, password, error);
HttpURLConnection connection = post(new URL(NEW_THREAD_POST_URL), join(params));
int responseCode = connection.getResponseCode();
if (responseCode != HttpURLConnection.HTTP_OK) {
throw new InternalEAPException(DiagnosticBundle.message("error.http.result.code", responseCode));
}
String response;
InputStream is = connection.getInputStream();
try {
byte[] bytes = FileUtil.loadBytes(is);
response = new String(bytes, ENCODING);
}
finally {
is.close();
}
if ("unauthorized".equals(response)) {
throw new NoSuchEAPUserException(login);
}
if (response.startsWith("update ")) {
throw new UpdateAvailableException(response.substring(7));
}
if (response.startsWith("message ")) {
throw new InternalEAPException(response.substring(8));
}
try {
return Integer.valueOf(response.trim()).intValue();
}
catch (NumberFormatException ex) {
throw new InternalEAPException(DiagnosticBundle.message("error.itn.returns.wrong.data"));
}
}
private static Map<String, String> createParameters(String login, String password, ErrorBean error) {
Map<String, String> params = ContainerUtil.newLinkedHashMap(40);
params.put("protocol.version", "1");
params.put("user.login", login);
params.put("user.password", password);
params.put("os.name", SystemProperties.getOsName());
params.put("java.version", SystemProperties.getJavaVersion());
params.put("java.vm.vendor", SystemProperties.getJavaVmVendor());
ApplicationInfoEx appInfo = ApplicationInfoEx.getInstanceEx();
ApplicationNamesInfo namesInfo = ApplicationNamesInfo.getInstance();
Application application = ApplicationManager.getApplication();
params.put("app.name", namesInfo.getProductName());
params.put("app.name.full", namesInfo.getFullProductName());
params.put("app.name.version", appInfo.getVersionName());
params.put("app.eap", Boolean.toString(appInfo.isEAP()));
params.put("app.internal", Boolean.toString(application.isInternal()));
params.put("app.build", appInfo.getBuild().asString());
params.put("app.version.major", appInfo.getMajorVersion());
params.put("app.version.minor", appInfo.getMinorVersion());
params.put("app.build.date", format(appInfo.getBuildDate()));
params.put("app.build.date.release", format(appInfo.getMajorReleaseBuildDate()));
params.put("app.compilation.timestamp", IdeaLogger.getOurCompilationTimestamp());
UpdateSettings updateSettings = UpdateSettings.getInstance();
params.put("update.channel.status", updateSettings.getSelectedChannelStatus().getCode());
params.put("update.ignored.builds", StringUtil.join(updateSettings.getIgnoredBuildNumbers(), ","));
params.put("plugin.name", error.getPluginName());
params.put("plugin.version", error.getPluginVersion());
params.put("last.action", error.getLastAction());
params.put("previous.exception", error.getPreviousException() == null ? null : Integer.toString(error.getPreviousException()));
params.put("error.message", error.getMessage());
params.put("error.stacktrace", error.getStackTrace());
params.put("error.description", error.getDescription());
params.put("assignee.id", error.getAssigneeId() == null ? null : Integer.toString(error.getAssigneeId()));
for (Attachment attachment : error.getAttachments()) {
params.put("attachment.name", attachment.getName());
params.put("attachment.value", attachment.getEncodedBytes());
}
return params;
}
private static String format(Calendar calendar) {
return calendar == null ? null : Long.toString(calendar.getTime().getTime());
}
private static byte[] join(Map<String, String> params) throws UnsupportedEncodingException {
StringBuilder builder = new StringBuilder();
for (Map.Entry<String, String> param : params.entrySet()) {
if (StringUtil.isEmpty(param.getKey())) {
throw new IllegalArgumentException(param.toString());
}
if (builder.length() > 0) {
builder.append('&');
}
if (StringUtil.isNotEmpty(param.getValue())) {
builder.append(param.getKey()).append('=').append(URLEncoder.encode(param.getValue(), ENCODING));
}
}
return builder.toString().getBytes(ENCODING);
}
private static HttpURLConnection post(URL url, byte[] bytes) throws IOException {
HttpsURLConnection connection = (HttpsURLConnection)url.openConnection();
connection.setSSLSocketFactory(ourSslContext.getSocketFactory());
if (!SystemInfo.isJavaVersionAtLeast("1.7") || !SystemProperties.getBooleanProperty("jsse.enableSNIExtension", true)) {
connection.setHostnameVerifier(new EaHostnameVerifier(url.getHost(), "ftp.intellij.net"));
}
connection.setRequestMethod("POST");
connection.setDoInput(true);
connection.setDoOutput(true);
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=" + ENCODING);
connection.setRequestProperty("Content-Length", Integer.toString(bytes.length));
OutputStream out = connection.getOutputStream();
try {
out.write(bytes);
}
finally {
out.close();
}
return connection;
}
private synchronized static SSLContext initContext() throws GeneralSecurityException, IOException {
CertificateFactory cf = CertificateFactory.getInstance(CertificateUtil.X509);
Certificate ca = cf.generateCertificate(new ByteArrayInputStream(JB_CA_CERT.getBytes(ENCODING)));
KeyStore ks = KeyStore.getInstance(CertificateUtil.JKS);
ks.load(null, null);
ks.setCertificateEntry("JetBrains CA", ca);
TrustManagerFactory tmf = TrustManagerFactory.getInstance(CertificateUtil.X509);
tmf.init(ks);
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, tmf.getTrustManagers(), null);
return ctx;
}
private static class EaHostnameVerifier implements HostnameVerifier {
private final Set<String> myAllowedHosts;
public EaHostnameVerifier(@NotNull String... allowedHosts) {
myAllowedHosts = ContainerUtil.newHashSet(allowedHosts);
}
@Override
public boolean verify(String hostname, SSLSession session) {
try {
Certificate[] certificates = session.getPeerCertificates();
if (certificates.length > 0) {
Certificate certificate = certificates[0];
if (certificate instanceof X509Certificate) {
String cn = CertificateUtil.getCommonName((X509Certificate)certificate);
return myAllowedHosts.contains(cn);
}
}
}
catch (SSLPeerUnverifiedException ignored) { }
return false;
}
}
@SuppressWarnings("SpellCheckingInspection") private static final String JB_CA_CERT =
"-----BEGIN CERTIFICATE-----\n" +
"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\n" +
"-----END CERTIFICATE-----\n";
}