Copilot: Add support for proxy settings

For easier testing a docker file is added. You can start
"buildandrun.sh" in copilot/tests/proxy to get a simple
proxy server up and running. The argument "PWDMODE" in
buildandrun.sh can be set to "with" and "without" to get
a proxy server that needs a password or not. The username
and password are user/1234.

Fixes: QTCREATORBUG-29485
Change-Id: I3859c9ad04ebd4f9349e25665ba710e23fb64dea
Reviewed-by: <github-actions-qt-creator@cristianadam.eu>
Reviewed-by: hjk <hjk@qt.io>
This commit is contained in:
Marcus Tillmanns
2023-08-14 09:48:14 +02:00
parent 91596316f2
commit f2d62c6d91
12 changed files with 390 additions and 49 deletions

View File

@@ -44,18 +44,21 @@ AuthWidget::AuthWidget(QWidget *parent)
}.attachTo(this);
// clang-format on
connect(m_button, &QPushButton::clicked, this, [this]() {
if (m_status == Status::SignedIn)
signOut();
else if (m_status == Status::SignedOut)
signIn();
});
auto update = [this] {
updateClient(FilePath::fromUserInput(settings().nodeJsPath.volatileValue()),
FilePath::fromUserInput(settings().distPath.volatileValue()));
};
connect(m_button, &QPushButton::clicked, this, [this, update]() {
if (m_status == Status::SignedIn)
signOut();
else if (m_status == Status::SignedOut)
signIn();
else
update();
});
connect(&settings(), &CopilotSettings::applied, this, update);
connect(settings().nodeJsPath.pathChooser(), &PathChooser::textChanged, this, update);
connect(settings().distPath.pathChooser(), &PathChooser::textChanged, this, update);
@@ -68,35 +71,39 @@ AuthWidget::~AuthWidget()
LanguageClientManager::shutdownClient(m_client);
}
void AuthWidget::setState(const QString &buttonText, bool working)
void AuthWidget::setState(const QString &buttonText, const QString &errorText, bool working)
{
m_button->setText(buttonText);
m_button->setVisible(true);
m_progressIndicator->setVisible(working);
m_statusLabel->setText(errorText);
m_statusLabel->setVisible(!m_statusLabel->text().isEmpty());
m_button->setEnabled(!working);
}
void AuthWidget::checkStatus()
{
if (!isEnabled())
return;
QTC_ASSERT(m_client && m_client->reachable(), return);
setState("Checking status ...", true);
setState("Checking status ...", {}, true);
m_client->requestCheckStatus(false, [this](const CheckStatusRequest::Response &response) {
if (response.error()) {
setState("failed: " + response.error()->message(), false);
setState("Failed to authenticate", response.error()->message(), false);
return;
}
const CheckStatusResponse result = *response.result();
if (result.user().isEmpty()) {
setState("Sign in", false);
setState("Sign in", {}, false);
m_status = Status::SignedOut;
return;
}
setState("Sign out " + result.user(), false);
setState("Sign out " + result.user(), {}, false);
m_status = Status::SignedIn;
});
}
@@ -105,12 +112,12 @@ void AuthWidget::updateClient(const FilePath &nodeJs, const FilePath &agent)
{
LanguageClientManager::shutdownClient(m_client);
m_client = nullptr;
setState(Tr::tr("Sign In"), false);
setState(Tr::tr("Sign In"), {}, false);
m_button->setEnabled(false);
if (!nodeJs.isExecutableFile() || !agent.exists())
return;
setState(Tr::tr("Sign In"), true);
setState(Tr::tr("Sign In"), {}, true);
m_client = new CopilotClient(nodeJs, agent);
connect(m_client, &Client::initialized, this, &AuthWidget::checkStatus);
@@ -127,7 +134,7 @@ void AuthWidget::signIn()
qCritical() << "Not implemented";
QTC_ASSERT(m_client && m_client->reachable(), return);
setState("Signing in ...", true);
setState("Signing in ...", {}, true);
m_client->requestSignInInitiate([this](const SignInInitiateRequest::Response &response) {
QTC_ASSERT(!response.error(), return);
@@ -144,19 +151,17 @@ void AuthWidget::signIn()
m_client
->requestSignInConfirm(response.result()->userCode(),
[this](const SignInConfirmRequest::Response &response) {
m_statusLabel->setText("");
if (response.error()) {
QMessageBox::critical(this,
Tr::tr("Login Failed"),
Tr::tr(
"The login request failed: ")
+ response.error()->message());
setState("Sign in", false);
setState("Sign in", response.error()->message(), false);
return;
}
setState("Sign Out " + response.result()->user(), false);
setState("Sign Out " + response.result()->user(), {}, false);
});
});
}
@@ -165,7 +170,7 @@ void AuthWidget::signOut()
{
QTC_ASSERT(m_client && m_client->reachable(), return);
setState("Signing out ...", true);
setState("Signing out ...", {}, true);
m_client->requestSignOut([this](const SignOutRequest::Response &response) {
QTC_ASSERT(!response.error(), return);

View File

@@ -30,7 +30,7 @@ public:
void updateClient(const Utils::FilePath &nodeJs, const Utils::FilePath &agent);
private:
void setState(const QString &buttonText, bool working);
void setState(const QString &buttonText, const QString &errorText, bool working);
void checkStatus();

View File

@@ -2,26 +2,32 @@
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
#include "copilotclient.h"
#include "copilotconstants.h"
#include "copilotsettings.h"
#include "copilotsuggestion.h"
#include "copilottr.h"
#include <app/app_version.h>
#include <languageclient/languageclientinterface.h>
#include <languageclient/languageclientmanager.h>
#include <languageclient/languageclientsettings.h>
#include <languageserverprotocol/lsptypes.h>
#include <coreplugin/actionmanager/actionmanager.h>
#include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/icore.h>
#include <projectexplorer/projectmanager.h>
#include <utils/filepath.h>
#include <texteditor/textdocumentlayout.h>
#include <texteditor/texteditor.h>
#include <languageserverprotocol/lsptypes.h>
#include <utils/checkablemessagebox.h>
#include <utils/filepath.h>
#include <utils/passworddialog.h>
#include <QInputDialog>
#include <QLoggingCategory>
#include <QTimer>
#include <QToolButton>
@@ -31,6 +37,8 @@ using namespace Utils;
using namespace ProjectExplorer;
using namespace Core;
Q_LOGGING_CATEGORY(copilotClientLog, "qtc.copilot.client", QtWarningMsg)
namespace Copilot::Internal {
static LanguageClient::BaseClientInterface *clientInterface(const FilePath &nodePath,
@@ -52,6 +60,23 @@ CopilotClient::CopilotClient(const FilePath &nodePath, const FilePath &distPath)
langFilter.filePattern = {"*"};
setSupportedLanguage(langFilter);
registerCustomMethod("LogMessage", [this](const LanguageServerProtocol::JsonRpcMessage &message) {
QString msg = message.toJsonObject().value("params").toObject().value("message").toString();
qCDebug(copilotClientLog) << message.toJsonObject()
.value("params")
.toObject()
.value("message")
.toString();
if (msg.contains("Socket Connect returned status code,407")) {
qCWarning(copilotClientLog) << "Proxy authentication required";
QMetaObject::invokeMethod(this,
&CopilotClient::proxyAuthenticationFailed,
Qt::QueuedConnection);
}
});
start();
auto openDoc = [this](IDocument *document) {
@@ -68,6 +93,8 @@ CopilotClient::CopilotClient(const FilePath &nodePath, const FilePath &distPath)
closeDocument(textDocument);
});
connect(this, &LanguageClient::Client::initialized, this, &CopilotClient::requestSetEditorInfo);
for (IDocument *doc : DocumentModel::openedDocuments())
openDoc(doc);
}
@@ -218,6 +245,32 @@ void CopilotClient::cancelRunningRequest(TextEditor::TextEditorWidget *editor)
m_runningRequests.erase(it);
}
static QString currentProxyPassword;
void CopilotClient::requestSetEditorInfo()
{
if (settings().saveProxyPassword())
currentProxyPassword = settings().proxyPassword();
const EditorInfo editorInfo{Core::Constants::IDE_VERSION_DISPLAY, "Qt Creator"};
const EditorPluginInfo editorPluginInfo{Core::Constants::IDE_VERSION_DISPLAY,
"Qt Creator Copilot plugin"};
SetEditorInfoParams params(editorInfo, editorPluginInfo);
if (settings().useProxy()) {
params.setNetworkProxy(
Copilot::NetworkProxy{settings().proxyHost(),
static_cast<int>(settings().proxyPort()),
settings().proxyUser(),
currentProxyPassword,
settings().proxyRejectUnauthorized()});
}
SetEditorInfoRequest request(params);
sendMessage(request);
}
void CopilotClient::requestCheckStatus(
bool localChecksOnly, std::function<void(const CheckStatusRequest::Response &response)> callback)
{
@@ -269,4 +322,36 @@ bool CopilotClient::isEnabled(Project *project)
return settings.isEnabled();
}
void CopilotClient::proxyAuthenticationFailed()
{
static bool doNotAskAgain = false;
if (m_isAskingForPassword || !settings().enableCopilot())
return;
m_isAskingForPassword = true;
auto answer = Utils::PasswordDialog::getUserAndPassword(
Tr::tr("Copilot"),
Tr::tr("Proxy username and password required:"),
Tr::tr("Do not ask again. This will disable Copilot for now."),
settings().proxyUser(),
&doNotAskAgain,
Core::ICore::dialogParent());
if (answer) {
settings().proxyUser.setValue(answer->first);
currentProxyPassword = answer->second;
} else {
settings().enableCopilot.setValue(false);
}
if (settings().saveProxyPassword())
settings().proxyPassword.setValue(currentProxyPassword);
settings().apply();
m_isAskingForPassword = false;
}
} // namespace Copilot::Internal

View File

@@ -6,6 +6,7 @@
#include "copilothoverhandler.h"
#include "requests/checkstatus.h"
#include "requests/getcompletions.h"
#include "requests/seteditorinfo.h"
#include "requests/signinconfirm.h"
#include "requests/signininitiate.h"
#include "requests/signout.h"
@@ -50,7 +51,11 @@ public:
bool isEnabled(ProjectExplorer::Project *project);
void proxyAuthenticationFailed();
private:
void requestSetEditorInfo();
QMap<TextEditor::TextEditorWidget *, GetCompletionRequest> m_runningRequests;
struct ScheduleData
{
@@ -59,6 +64,7 @@ private:
};
QMap<TextEditor::TextEditorWidget *, ScheduleData> m_scheduledRequests;
CopilotHoverHandler m_hoverHandler;
bool m_isAskingForPassword{false};
};
} // namespace Copilot::Internal

View File

@@ -44,7 +44,6 @@ CopilotSettings::CopilotSettings()
const FilePath nodeFromPath = FilePath("node").searchInPath();
const FilePaths searchDirs
= {FilePath::fromUserInput("~/.vim/pack/github/start/copilot.vim/dist/agent.js"),
FilePath::fromUserInput("~/.vim/pack/github/start/copilot.vim/copilot/dist/agent.js"),
FilePath::fromUserInput(
@@ -80,13 +79,75 @@ CopilotSettings::CopilotSettings()
autoComplete.setDisplayName(Tr::tr("Auto Complete"));
autoComplete.setSettingsKey("Copilot.Autocomplete");
autoComplete.setLabelText(Tr::tr("Request completions automatically"));
autoComplete.setLabelText(Tr::tr("Auto request"));
autoComplete.setDefaultValue(true);
autoComplete.setEnabler(&enableCopilot);
autoComplete.setToolTip(Tr::tr("Automatically request suggestions for the current text cursor "
"position after changes to the document."));
autoComplete.setLabelPlacement(BoolAspect::LabelPlacement::InExtraLabel);
useProxy.setDisplayName(Tr::tr("Use Proxy"));
useProxy.setSettingsKey("Copilot.UseProxy");
useProxy.setLabelText(Tr::tr("Use Proxy"));
useProxy.setDefaultValue(false);
useProxy.setEnabler(&enableCopilot);
useProxy.setToolTip(Tr::tr("Use a proxy to connect to the Copilot servers."));
useProxy.setLabelPlacement(BoolAspect::LabelPlacement::InExtraLabel);
proxyHost.setDisplayName(Tr::tr("Proxy Host"));
proxyHost.setDisplayStyle(StringAspect::LineEditDisplay);
proxyHost.setSettingsKey("Copilot.ProxyHost");
proxyHost.setLabelText(Tr::tr("Proxy Host"));
proxyHost.setDefaultValue("");
proxyHost.setEnabler(&useProxy);
proxyHost.setToolTip(Tr::tr("The host name of the proxy server."));
proxyHost.setHistoryCompleter("Copilot.ProxyHost.History");
proxyPort.setDisplayName(Tr::tr("Proxy Port"));
proxyPort.setSettingsKey("Copilot.ProxyPort");
proxyPort.setLabelText(Tr::tr("Proxy Port"));
proxyPort.setDefaultValue(3128);
proxyPort.setEnabler(&useProxy);
proxyPort.setToolTip(Tr::tr("The port of the proxy server."));
proxyPort.setRange(1, 65535);
proxyUser.setDisplayName(Tr::tr("Proxy User"));
proxyUser.setDisplayStyle(StringAspect::LineEditDisplay);
proxyUser.setSettingsKey("Copilot.ProxyUser");
proxyUser.setLabelText(Tr::tr("Proxy User"));
proxyUser.setDefaultValue("");
proxyUser.setEnabler(&useProxy);
proxyUser.setToolTip(Tr::tr("The user name for the proxy server."));
proxyUser.setHistoryCompleter("Copilot.ProxyUser.History");
saveProxyPassword.setDisplayName(Tr::tr("Save Proxy Password"));
saveProxyPassword.setSettingsKey("Copilot.SaveProxyPassword");
saveProxyPassword.setLabelText(Tr::tr("Save Proxy Password"));
saveProxyPassword.setDefaultValue(false);
saveProxyPassword.setEnabler(&useProxy);
saveProxyPassword.setToolTip(
Tr::tr("Save the password for the proxy server (Password is stored insecurely!)."));
saveProxyPassword.setLabelPlacement(BoolAspect::LabelPlacement::InExtraLabel);
proxyPassword.setDisplayName(Tr::tr("Proxy Password"));
proxyPassword.setDisplayStyle(StringAspect::PasswordLineEditDisplay);
proxyPassword.setSettingsKey("Copilot.ProxyPassword");
proxyPassword.setLabelText(Tr::tr("Proxy Password"));
proxyPassword.setDefaultValue("");
proxyPassword.setEnabler(&saveProxyPassword);
proxyPassword.setToolTip(Tr::tr("The password for the proxy server."));
proxyRejectUnauthorized.setDisplayName(Tr::tr("Reject Unauthorized"));
proxyRejectUnauthorized.setSettingsKey("Copilot.ProxyRejectUnauthorized");
proxyRejectUnauthorized.setLabelText(Tr::tr("Reject Unauthorized"));
proxyRejectUnauthorized.setDefaultValue(true);
proxyRejectUnauthorized.setEnabler(&useProxy);
proxyRejectUnauthorized.setToolTip(Tr::tr("Reject unauthorized certificates from the proxy "
"server. This is a security risk."));
proxyRejectUnauthorized.setLabelPlacement(BoolAspect::LabelPlacement::InExtraLabel);
initEnableAspect(enableCopilot);
enableCopilot.setLabelPlacement(BoolAspect::LabelPlacement::InExtraLabel);
readSettings();
}
@@ -140,25 +201,24 @@ public:
auto warningLabel = new QLabel;
warningLabel->setWordWrap(true);
warningLabel->setTextInteractionFlags(Qt::LinksAccessibleByMouse
| Qt::LinksAccessibleByKeyboard
| Qt::TextSelectableByMouse);
warningLabel->setText(Tr::tr(
"Enabling %1 is subject to your agreement and abidance with your applicable "
warningLabel->setTextInteractionFlags(
Qt::LinksAccessibleByMouse | Qt::LinksAccessibleByKeyboard | Qt::TextSelectableByMouse);
warningLabel->setText(
Tr::tr("Enabling %1 is subject to your agreement and abidance with your applicable "
"%1 terms. It is your responsibility to know and accept the requirements and "
"parameters of using tools like %1. This may include, but is not limited to, "
"ensuring you have the rights to allow %1 access to your code, as well as "
"understanding any implications of your use of %1 and suggestions produced "
"(like copyright, accuracy, etc.)." ).arg("Copilot"));
"(like copyright, accuracy, etc.).")
.arg("Copilot"));
auto authWidget = new AuthWidget();
auto helpLabel = new QLabel();
helpLabel->setTextFormat(Qt::MarkdownText);
helpLabel->setWordWrap(true);
helpLabel->setTextInteractionFlags(Qt::LinksAccessibleByMouse
| Qt::LinksAccessibleByKeyboard
| Qt::TextSelectableByMouse);
helpLabel->setTextInteractionFlags(
Qt::LinksAccessibleByMouse | Qt::LinksAccessibleByKeyboard | Qt::TextSelectableByMouse);
helpLabel->setOpenExternalLinks(true);
connect(helpLabel, &QLabel::linkHovered, [](const QString &link) {
QToolTip::showText(QCursor::pos(), link);
@@ -176,14 +236,28 @@ public:
.arg("[agent.js](https://github.com/github/copilot.vim/tree/release/dist)"));
Column {
QString("<b>" + Tr::tr("Note:") + "</b>"), br,
Group {
title(Tr::tr("Note")),
Column {
warningLabel, br,
settings().enableCopilot, br,
helpLabel, br,
}
},
Form {
authWidget, br,
settings().enableCopilot, br,
settings().nodeJsPath, br,
settings().distPath, br,
settings().autoComplete, br,
helpLabel, br,
hr, br,
settings().useProxy, br,
settings().proxyHost, br,
settings().proxyPort, br,
settings().proxyRejectUnauthorized, br,
settings().proxyUser, br,
settings().saveProxyPassword, br,
settings().proxyPassword, br,
},
st
}.attachTo(this);
// clang-format on
@@ -211,4 +285,4 @@ public:
const CopilotSettingsPage settingsPage;
} // Copilot
} // namespace Copilot

View File

@@ -18,6 +18,15 @@ public:
Utils::FilePathAspect distPath{this};
Utils::BoolAspect autoComplete{this};
Utils::BoolAspect enableCopilot{this};
Utils::BoolAspect useProxy{this};
Utils::StringAspect proxyHost{this};
Utils::IntegerAspect proxyPort{this};
Utils::StringAspect proxyUser{this};
Utils::BoolAspect saveProxyPassword{this};
Utils::StringAspect proxyPassword{this};
Utils::BoolAspect proxyRejectUnauthorized{this};
};
CopilotSettings &settings();

View File

@@ -0,0 +1,126 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
#pragma once
#include "checkstatus.h"
#include <languageserverprotocol/jsonrpcmessages.h>
#include <languageserverprotocol/lsptypes.h>
namespace Copilot {
class EditorPluginInfo : public LanguageServerProtocol::JsonObject
{
static constexpr char version[] = "version";
static constexpr char name[] = "name";
public:
using JsonObject::JsonObject;
EditorPluginInfo(const QString &version, const QString &name)
{
setEditorVersion(version);
setEditorName(name);
}
void setEditorVersion(const QString &v) { insert(version, v); }
void setEditorName(const QString &n) { insert(name, n); }
};
class EditorInfo : public LanguageServerProtocol::JsonObject
{
static constexpr char version[] = "version";
static constexpr char name[] = "name";
public:
using JsonObject::JsonObject;
EditorInfo(const QString &version, const QString &name)
{
setEditorVersion(version);
setEditorName(name);
}
void setEditorVersion(const QString &v) { insert(version, v); }
void setEditorName(const QString &n) { insert(name, n); }
};
class NetworkProxy : public LanguageServerProtocol::JsonObject
{
static constexpr char host[] = "host";
static constexpr char port[] = "port";
static constexpr char user[] = "username";
static constexpr char password[] = "password";
static constexpr char rejectUnauthorized[] = "rejectUnauthorized";
public:
using JsonObject::JsonObject;
NetworkProxy(const QString &host,
int port,
const QString &user,
const QString &password,
bool rejectUnauthorized)
{
setHost(host);
setPort(port);
setUser(user);
setPassword(password);
setRejectUnauthorized(rejectUnauthorized);
}
void insertIfNotEmpty(const std::string_view key, const QString &value)
{
if (!value.isEmpty())
insert(key, value);
}
void setHost(const QString &h) { insert(host, h); }
void setPort(int p) { insert(port, p); }
void setUser(const QString &u) { insertIfNotEmpty(user, u); }
void setPassword(const QString &p) { insertIfNotEmpty(password, p); }
void setRejectUnauthorized(bool r) { insert(rejectUnauthorized, r); }
};
class SetEditorInfoParams : public LanguageServerProtocol::JsonObject
{
static constexpr char editorInfo[] = "editorInfo";
static constexpr char editorPluginInfo[] = "editorPluginInfo";
static constexpr char networkProxy[] = "networkProxy";
public:
using JsonObject::JsonObject;
SetEditorInfoParams(const EditorInfo &editorInfo, const EditorPluginInfo &editorPluginInfo)
{
setEditorInfo(editorInfo);
setEditorPluginInfo(editorPluginInfo);
}
SetEditorInfoParams(const EditorInfo &editorInfo,
const EditorPluginInfo &editorPluginInfo,
const NetworkProxy &networkProxy)
{
setEditorInfo(editorInfo);
setEditorPluginInfo(editorPluginInfo);
setNetworkProxy(networkProxy);
}
void setEditorInfo(const EditorInfo &info) { insert(editorInfo, info); }
void setEditorPluginInfo(const EditorPluginInfo &info) { insert(editorPluginInfo, info); }
void setNetworkProxy(const NetworkProxy &proxy) { insert(networkProxy, proxy); }
};
class SetEditorInfoRequest
: public LanguageServerProtocol::Request<CheckStatusResponse, std::nullptr_t, SetEditorInfoParams>
{
public:
explicit SetEditorInfoRequest(const SetEditorInfoParams &params)
: Request(methodName, params)
{}
using Request::Request;
constexpr static const char methodName[] = "setEditorInfo";
};
} // namespace Copilot

View File

@@ -0,0 +1,20 @@
ARG PWDMODE=with
FROM ubuntu:20.04 AS base
ARG DEBIAN_FRONTEND=noninteractive
ENV TZ=Etc/UTC
RUN apt-get update && apt-get install -y squid apache2-utils && rm -rf /var/lib/apt/lists/*
COPY run.sh /
RUN chmod +x /run.sh
FROM base as image-with-pwd
RUN echo 1234 | htpasswd -i -c /etc/squid/pswds user
COPY userauth.conf /etc/squid/conf.d/
FROM base as image-without-pwd
COPY noauth.conf /etc/squid/conf.d/
FROM image-${PWDMODE}-pwd AS final
CMD [ "/run.sh" ]

View File

@@ -0,0 +1,4 @@
#!/bin/bash
docker build --build-arg PWDMODE=with -t copilot-proxy-test . && \
docker run --rm -it -p 3128:3128 copilot-proxy-test

View File

@@ -0,0 +1 @@
http_access allow all

View File

@@ -0,0 +1,7 @@
#!/bin/sh
touch /var/log/squid/access.log
chmod 640 /var/log/squid/access.log
chown proxy:proxy /var/log/squid/access.log
tail -f /var/log/squid/access.log &
exec squid --foreground

View File

@@ -0,0 +1,4 @@
auth_param basic program /usr/lib/squid3/basic_ncsa_auth /etc/squid/pswds
auth_param basic realm proxy
acl authenticated proxy_auth REQUIRED
http_access allow authenticated