Cleanup (ITN proxy API)

This commit is contained in:
Roman Shevchenko
2018-03-20 13:46:11 +01:00
parent 9c47bbbd1e
commit 82de2c3645
4 changed files with 130 additions and 205 deletions
@@ -1,51 +0,0 @@
/*
* Copyright 2000-2016 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.diagnostic;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.util.io.HttpRequests;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
class DevelopersLoader {
private static final String DEVELOPERS_LIST_URL = "https://ea-engine.labs.intellij.net/data?category=developers";
private DevelopersLoader() { }
@NotNull
static Collection<Developer> fetchDevelopers(@NotNull ProgressIndicator indicator) throws IOException {
return HttpRequests.request(DEVELOPERS_LIST_URL).connect((HttpRequests.RequestProcessor<Collection<Developer>>)request -> {
List<Developer> developers = new ArrayList<>();
developers.add(Developer.NULL);
String line;
while ((line = request.getReader().readLine()) != null) {
int i = line.indexOf('\t');
if (i == -1) throw new IOException("Protocol error");
int id = Integer.parseInt(line.substring(0, i));
String name = line.substring(i + 1);
developers.add(new Developer(id, name));
indicator.checkCanceled();
}
return developers;
});
}
}
@@ -1,9 +1,8 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.errorreport.itn;
package com.intellij.diagnostic;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import com.intellij.diagnostic.DiagnosticBundle;
import com.intellij.errorreport.bean.ErrorBean;
import com.intellij.errorreport.error.InternalEAPException;
import com.intellij.errorreport.error.NoSuchEAPUserException;
@@ -26,16 +25,21 @@ import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.security.CompositeX509TrustManager;
import com.intellij.util.Consumer;
import com.intellij.util.io.HttpRequests;
import com.intellij.util.net.NetUtils;
import com.intellij.util.net.ssl.CertificateUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.net.ssl.*;
import java.io.*;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.MessageDigest;
@@ -44,38 +48,62 @@ import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Calendar;
import java.util.Map;
import java.util.*;
import java.util.function.IntConsumer;
/**
* @author stathik
* @since Aug 4, 2003
*/
public class ITNProxy {
private static final String NEW_THREAD_VIEW_URL = "https://ea.jetbrains.com/browser/ea_reports/";
class ITNProxy {
private static final String DEFAULT_USER = "idea_anonymous";
private static final String DEFAULT_PASS = "guest";
private static final String DEVELOPERS_LIST_URL = "https://ea-engine.labs.intellij.net/data?category=developers";
private static final String NEW_THREAD_POST_URL = "https://ea-report.jetbrains.com/trackerRpc/idea/createScr";
private static final String NEW_THREAD_VIEW_URL = "https://ea.jetbrains.com/browser/ea_reports/";
private static final String ENCODING = "UTF8";
static @NotNull Collection<Developer> fetchDevelopers(@NotNull ProgressIndicator indicator) throws IOException {
return HttpRequests.request(DEVELOPERS_LIST_URL).connect((HttpRequests.RequestProcessor<Collection<Developer>>)request -> {
List<Developer> developers = new ArrayList<>();
developers.add(Developer.NULL);
public static void sendError(@Nullable Project project,
final String login,
final String password,
@NotNull final ErrorBean error,
@NotNull final IntConsumer callback,
@NotNull final Consumer<Exception> errorCallback) {
if (StringUtil.isEmpty(login)) {
return;
String line;
while ((line = request.getReader().readLine()) != null) {
int i = line.indexOf('\t');
if (i == -1) throw new IOException("Protocol error");
int id = Integer.parseInt(line.substring(0, i));
String name = line.substring(i + 1);
developers.add(new Developer(id, name));
indicator.checkCanceled();
}
return developers;
});
}
static void sendError(@Nullable Project project,
@Nullable String login,
@Nullable String password,
@NotNull ErrorBean error,
@NotNull IntConsumer onSuccess,
@NotNull Consumer<Exception> onError) {
if (StringUtil.isEmptyOrSpaces(login)) {
login = DEFAULT_USER;
password = DEFAULT_PASS;
}
else if (password == null) {
password = "";
}
String _login = login, _password = password;
Task.Backgroundable task = new Task.Backgroundable(project, DiagnosticBundle.message("title.submitting.error.report")) {
@Override
public void run(@NotNull ProgressIndicator indicator) {
try {
callback.accept(postNewThread(login, password, error));
onSuccess.accept(postNewThread(_login, _password, error));
}
catch (Exception ex) {
errorCallback.consume(ex);
onError.consume(ex);
}
}
};
@@ -88,14 +116,13 @@ public class ITNProxy {
}
}
@NotNull
public static String getBrowseUrl(int threadId) {
static @NotNull String getBrowseUrl(int threadId) {
return NEW_THREAD_VIEW_URL + threadId;
}
private static SSLContext ourSslContext;
private static int postNewThread(String login, String password, @NotNull ErrorBean error) throws Exception {
private static int postNewThread(String login, String password, ErrorBean error) throws Exception {
if (ourSslContext == null) {
ourSslContext = initContext();
}
@@ -107,14 +134,7 @@ public class ITNProxy {
throw new InternalEAPException(DiagnosticBundle.message("error.http.result.code", responseCode));
}
String response;
InputStream is = connection.getInputStream();
try {
response = FileUtil.loadTextAndClose(is);
}
finally {
is.close();
}
String response = FileUtil.loadTextAndClose(connection.getInputStream());
if ("unauthorized".equals(response)) {
throw new NoSuchEAPUserException(login);
@@ -134,8 +154,7 @@ public class ITNProxy {
}
}
@NotNull
private static Multimap<String, String> createParameters(String login, String password, @NotNull ErrorBean error) {
private static Multimap<String, String> createParameters(String login, String password, ErrorBean error) {
Multimap<String, String> params = ArrayListMultimap.create(40, 1);
params.put("protocol.version", "1");
@@ -194,29 +213,27 @@ public class ITNProxy {
return params;
}
@Nullable
private static String format(@Nullable Calendar calendar) {
private static @Nullable String format(@Nullable Calendar calendar) {
return calendar == null ? null : Long.toString(calendar.getTime().getTime());
}
private static byte[] join(@NotNull Multimap<String, String> params) throws UnsupportedEncodingException {
private static byte[] join(Multimap<String, String> params) throws UnsupportedEncodingException {
StringBuilder builder = new StringBuilder();
for (Map.Entry<String, String> param : params.entries()) {
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));
if (builder.length() > 0) {
builder.append('&');
}
builder.append(param.getKey()).append('=').append(URLEncoder.encode(param.getValue(), StandardCharsets.UTF_8.name()));
}
}
return builder.toString().getBytes(ENCODING);
return builder.toString().getBytes(StandardCharsets.UTF_8);
}
@NotNull
private static HttpURLConnection post(@NotNull URL url, @NotNull byte[] bytes) throws IOException {
private static HttpURLConnection post(URL url, byte[] bytes) throws IOException {
HttpsURLConnection connection = (HttpsURLConnection)url.openConnection();
connection.setSSLSocketFactory(ourSslContext.getSocketFactory());
@@ -227,23 +244,19 @@ public class ITNProxy {
connection.setRequestMethod("POST");
connection.setDoInput(true);
connection.setDoOutput(true);
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=" + ENCODING);
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=" + StandardCharsets.UTF_8.name());
connection.setRequestProperty("Content-Length", Integer.toString(bytes.length));
OutputStream out = connection.getOutputStream();
try {
try (OutputStream out = connection.getOutputStream()) {
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)));
Certificate ca = cf.generateCertificate(new ByteArrayInputStream(JB_CA_CERT.getBytes(StandardCharsets.US_ASCII)));
KeyStore ks = KeyStore.getInstance(CertificateUtil.JKS);
ks.load(null, null);
ks.setCertificateEntry("JetBrains CA", ca);
@@ -328,4 +341,4 @@ public class ITNProxy {
private static final String JB_CA_CN = "JetBrains Enterprise CA";
private static final String JB_CA_FP = "604d3c703a13a3be2d452f14442be11b37e186f";
}
}
@@ -1,18 +1,4 @@
/*
* Copyright 2000-2016 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.
*/
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.diagnostic
import com.intellij.CommonBundle
@@ -21,7 +7,6 @@ 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.errorreport.itn.ITNProxy
import com.intellij.ide.DataManager
import com.intellij.ide.plugins.PluginManager
import com.intellij.idea.IdeaLogger
@@ -39,6 +24,7 @@ import com.intellij.openapi.ui.Messages
import com.intellij.util.Consumer
import com.intellij.xml.util.XmlStringUtil
import java.awt.Component
import java.lang.Exception
import javax.swing.Icon
private var previousExceptionThreadId = 0
@@ -50,7 +36,7 @@ open class ITNReporter : ErrorReportSubmitter() {
additionalInfo: String?,
parentComponent: Component,
consumer: Consumer<SubmittedReportInfo>): Boolean {
return submit(events.get(0), parentComponent, consumer, ErrorBean(events.get(0).throwable, IdeaLogger.ourLastActionId), additionalInfo)
return submit(events[0], parentComponent, consumer, ErrorBean(events[0].throwable, IdeaLogger.ourLastActionId), additionalInfo)
}
/**
@@ -73,10 +59,6 @@ fun setPluginInfo(event: IdeaLoggingEvent, errorBean: ErrorBean) {
}
}
private fun updatePreviousThreadId(threadId: Int?) {
previousExceptionThreadId = threadId!!
}
private fun showMessageDialog(parentComponent: Component, project: Project?, message: String, title: String, icon: Icon) {
if (parentComponent.isShowing) {
Messages.showMessageDialog(parentComponent, message, title, icon)
@@ -86,10 +68,14 @@ private fun showMessageDialog(parentComponent: Component, project: Project?, mes
}
}
private fun submit(event: IdeaLoggingEvent, parentComponent: Component, callback: Consumer<SubmittedReportInfo>, errorBean: ErrorBean, description: String?): Boolean {
private fun submit(event: IdeaLoggingEvent,
parentComponent: Component,
callback: Consumer<SubmittedReportInfo>,
errorBean: ErrorBean,
description: String?): Boolean {
var credentials = ErrorReportConfigurable.getCredentials()
// ask password only if user name was specified
if (credentials.hasOnlyUserName()) {
// ask password only if user name was specified
if (!showJetBrainsAccountDialog(parentComponent).showAndGet()) {
return false
}
@@ -112,53 +98,56 @@ private fun submit(event: IdeaLoggingEvent, parentComponent: Component, callback
errorBean.attachments = data.includedAttachments
}
var login = credentials?.userName
var password = credentials?.getPasswordAsString()
if (login.isNullOrBlank() && password.isNullOrBlank()) {
login = "idea_anonymous"
password = "guest"
}
val project = CommonDataKeys.PROJECT.getData(DataManager.getInstance().getDataContext(parentComponent))
ITNProxy.sendError(project, login, password, errorBean, { threadId ->
updatePreviousThreadId(threadId)
val linkText = threadId.toString()
val reportInfo = SubmittedReportInfo(ITNProxy.getBrowseUrl(threadId), linkText, SubmittedReportInfo.SubmissionStatus.NEW_ISSUE)
callback.consume(reportInfo)
ApplicationManager.getApplication().invokeLater {
val text = StringBuilder()
IdeErrorsDialog.appendSubmissionInformation(reportInfo, text)
text.append('.').append("<br/>").append(DiagnosticBundle.message("error.report.gratitude"))
val content = XmlStringUtil.wrapInHtml(text)
ReportMessages.GROUP.createNotification(ReportMessages.ERROR_REPORT, content, NotificationType.INFORMATION,
NotificationListener.URL_OPENING_LISTENER).setImportant(false).notify(project)
}
}) { e ->
Logger.getInstance(ITNReporter::class.java).info("reporting failed: $e")
ApplicationManager.getApplication().invokeLater {
val msg = when (e) {
is NoSuchEAPUserException -> DiagnosticBundle.message("error.report.authentication.failed")
is InternalEAPException -> DiagnosticBundle.message("error.report.posting.failed", e.message)
else -> DiagnosticBundle.message("error.report.sending.failure")
}
if (e is UpdateAvailableException) {
val message = DiagnosticBundle.message("error.report.new.eap.build.message", e.message)
showMessageDialog(parentComponent, project, message, CommonBundle.getWarningTitle(), Messages.getWarningIcon())
callback.consume(SubmittedReportInfo(SubmittedReportInfo.SubmissionStatus.FAILED))
return@invokeLater
}
if (!MessageDialogBuilder.yesNo(ReportMessages.ERROR_REPORT, msg).project(project).isYes) {
callback.consume(SubmittedReportInfo(SubmittedReportInfo.SubmissionStatus.FAILED))
}
else {
if (e is NoSuchEAPUserException) {
showJetBrainsAccountDialog(parentComponent, project).show()
}
ApplicationManager.getApplication().invokeLater { submit(event, parentComponent, callback, errorBean, description) }
}
}
}
ITNProxy.sendError(project, credentials?.userName, credentials?.getPasswordAsString(), errorBean,
{ threadId -> onSuccess(threadId, callback, project) },
{ e -> onError(e, parentComponent, project, callback, event, errorBean, description) })
return true
}
private fun onSuccess(threadId: Int, callback: Consumer<SubmittedReportInfo>, project: Project?) {
previousExceptionThreadId = threadId
val linkText = threadId.toString()
val reportInfo = SubmittedReportInfo(ITNProxy.getBrowseUrl(threadId), linkText, SubmittedReportInfo.SubmissionStatus.NEW_ISSUE)
callback.consume(reportInfo)
ApplicationManager.getApplication().invokeLater {
val text = StringBuilder()
IdeErrorsDialog.appendSubmissionInformation(reportInfo, text)
text.append('.').append("<br/>").append(DiagnosticBundle.message("error.report.gratitude"))
val content = XmlStringUtil.wrapInHtml(text)
ReportMessages.GROUP.createNotification(ReportMessages.ERROR_REPORT, content, NotificationType.INFORMATION,
NotificationListener.URL_OPENING_LISTENER).setImportant(false).notify(project)
}
}
private fun onError(e: Exception,
parentComponent: Component,
project: Project?,
callback: Consumer<SubmittedReportInfo>,
event: IdeaLoggingEvent,
errorBean: ErrorBean,
description: String?) {
Logger.getInstance(ITNReporter::class.java).info("reporting failed: $e")
ApplicationManager.getApplication().invokeLater {
val msg = when (e) {
is NoSuchEAPUserException -> DiagnosticBundle.message("error.report.authentication.failed")
is InternalEAPException -> DiagnosticBundle.message("error.report.posting.failed", e.message)
else -> DiagnosticBundle.message("error.report.sending.failure")
}
if (e is UpdateAvailableException) {
val message = DiagnosticBundle.message("error.report.new.eap.build.message", e.message)
showMessageDialog(parentComponent, project, message, CommonBundle.getWarningTitle(), Messages.getWarningIcon())
callback.consume(SubmittedReportInfo(SubmittedReportInfo.SubmissionStatus.FAILED))
}
else if (!MessageDialogBuilder.yesNo(ReportMessages.ERROR_REPORT, msg).project(project).isYes) {
callback.consume(SubmittedReportInfo(SubmittedReportInfo.SubmissionStatus.FAILED))
}
else {
if (e is NoSuchEAPUserException) {
showJetBrainsAccountDialog(parentComponent, project).show()
}
ApplicationManager.getApplication().invokeLater { submit(event, parentComponent, callback, errorBean, description) }
}
}
}
@@ -1,18 +1,4 @@
/*
* Copyright 2000-2017 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.
*/
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.diagnostic;
import com.intellij.CommonBundle;
@@ -41,7 +27,6 @@ import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.extensions.PluginDescriptor;
import com.intellij.openapi.extensions.PluginId;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.project.Project;
@@ -142,29 +127,20 @@ public class IdeErrorsDialog extends DialogWrapper implements MessagePoolListene
}
private void loadDevelopersAsynchronously() {
Task.Backgroundable task = new Task.Backgroundable(null, "Loading Developers List", true) {
private Collection<Developer> myDevelopers = Collections.emptyList();
new Task.Backgroundable(null, "Loading Developers List", true) {
@Override
public void run(@NotNull ProgressIndicator indicator) {
try {
myDevelopers = DevelopersLoader.fetchDevelopers(indicator);
Collection<Developer> developers = ITNProxy.fetchDevelopers(indicator);
myDetailsTabForm.setDevelopers(developers);
//noinspection AssignmentToStaticFieldFromInstanceMethod
ourDevelopersList = developers;
}
catch (IOException e) {
//Notifications.Bus.register("Error reporter", NotificationDisplayType.BALLOON);
//Notifications.Bus.notify(new Notification("Error reporter", "Communication error",
// "Unable to load developers list from server.", NotificationType.WARNING));
LOG.warn(e);
}
}
@Override
public void onSuccess() {
myDetailsTabForm.setDevelopers(myDevelopers);
//noinspection AssignmentToStaticFieldFromInstanceMethod
ourDevelopersList = myDevelopers;
}
};
ProgressManager.getInstance().run(task);
}.queue();
}
private boolean moveSelectionToMessage(LogMessage defaultMessage) {
@@ -794,10 +770,8 @@ public class IdeErrorsDialog extends DialogWrapper implements MessagePoolListene
String msg = "Detected plugin " + id + " by class " + className;
IdeaPluginDescriptor descriptor = PluginManager.getPlugin(id);
if (descriptor != null) {
msg += "; ideaLoader=" + descriptor.getUseIdeaClassLoader();
ClassLoader loader = descriptor.getPluginClassLoader();
msg += "; loader=" + loader;
msg += "; loader=" + loader + '/' + loader.getClass();
if (loader instanceof PluginClassLoader) {
msg += "; loaded class: " + ((PluginClassLoader)loader).hasLoadedClass(className);
}