UI: Add support for external browser OAuth
(Jim note: Adds abstraction to the OAuth class to allow the ability to perform OAuth via external browser, and adds an AuthListener to act as the local auth server.)
This commit is contained in:
parent
63ad0642ae
commit
0654675f32
@ -141,13 +141,11 @@ endif()
|
||||
if(BROWSER_AVAILABLE_INTERNAL)
|
||||
list(APPEND obs_PLATFORM_SOURCES
|
||||
obf.c
|
||||
auth-oauth.cpp
|
||||
window-dock-browser.cpp
|
||||
window-extra-browsers.cpp
|
||||
)
|
||||
list(APPEND obs_PLATFORM_HEADERS
|
||||
obf.h
|
||||
auth-oauth.hpp
|
||||
window-dock-browser.hpp
|
||||
window-extra-browsers.hpp
|
||||
)
|
||||
@ -226,6 +224,8 @@ set(obs_SOURCES
|
||||
window-remux.cpp
|
||||
window-missing-files.cpp
|
||||
auth-base.cpp
|
||||
auth-oauth.cpp
|
||||
auth-listener.cpp
|
||||
source-tree.cpp
|
||||
scene-tree.cpp
|
||||
properties-view.cpp
|
||||
@ -290,6 +290,8 @@ set(obs_HEADERS
|
||||
window-remux.hpp
|
||||
window-missing-files.hpp
|
||||
auth-base.hpp
|
||||
auth-oauth.hpp
|
||||
auth-listener.hpp
|
||||
source-tree.hpp
|
||||
scene-tree.hpp
|
||||
properties-view.hpp
|
||||
|
@ -39,6 +39,17 @@ Auth::Type Auth::AuthType(const std::string &service)
|
||||
return Type::None;
|
||||
}
|
||||
|
||||
bool Auth::External(const std::string &service)
|
||||
{
|
||||
for (auto &a : authDefs) {
|
||||
if (service.find(a.def.service) != std::string::npos) {
|
||||
return a.def.externalOAuth;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void Auth::Load()
|
||||
{
|
||||
OBSBasic *main = OBSBasic::Get();
|
||||
|
@ -27,11 +27,13 @@ public:
|
||||
enum class Type {
|
||||
None,
|
||||
OAuth_StreamKey,
|
||||
OAuth_LinkedAccount,
|
||||
};
|
||||
|
||||
struct Def {
|
||||
std::string service;
|
||||
Type type;
|
||||
bool externalOAuth;
|
||||
};
|
||||
|
||||
typedef std::function<std::shared_ptr<Auth>()> create_cb;
|
||||
@ -41,6 +43,7 @@ public:
|
||||
|
||||
inline Type type() const { return def.type; }
|
||||
inline const char *service() const { return def.service.c_str(); }
|
||||
inline bool external() const { return def.externalOAuth; }
|
||||
|
||||
virtual void LoadUI() {}
|
||||
|
||||
@ -48,6 +51,7 @@ public:
|
||||
|
||||
static std::shared_ptr<Auth> Create(const std::string &service);
|
||||
static Type AuthType(const std::string &service);
|
||||
static bool External(const std::string &service);
|
||||
static void Load();
|
||||
static void Save();
|
||||
|
||||
|
86
UI/auth-listener.cpp
Normal file
86
UI/auth-listener.cpp
Normal file
@ -0,0 +1,86 @@
|
||||
#include <auth-listener.hpp>
|
||||
|
||||
#include <QRegularExpression>
|
||||
#include <QRegularExpressionMatch>
|
||||
#include <QString>
|
||||
#include <QtNetwork/QTcpSocket>
|
||||
|
||||
#include "obs-app.hpp"
|
||||
#include "qt-wrappers.hpp"
|
||||
|
||||
#define LOGO_URL "https://obsproject.com/assets/images/new_icon_small-r.png"
|
||||
|
||||
static const QString serverResponseHeader =
|
||||
QStringLiteral("HTTP/1.0 200 OK\n"
|
||||
"Connection: close\n"
|
||||
"Content-Type: text/html; charset=UTF-8\n"
|
||||
"Server: OBS Studio\n"
|
||||
"\n"
|
||||
"<html><head><title>OBS Studio"
|
||||
"</title></head>");
|
||||
|
||||
static const QString responseTemplate =
|
||||
"<center>"
|
||||
"<img src=\"" LOGO_URL
|
||||
"\" alt=\"OBS\" class=\"center\" height=\"60\" width=\"60\">"
|
||||
"</center>"
|
||||
"<center><p style=\"font-family:verdana; font-size:13pt\">%1</p></center>";
|
||||
|
||||
AuthListener::AuthListener(QObject *parent) : QObject(parent)
|
||||
{
|
||||
server = new QTcpServer(this);
|
||||
connect(server, &QTcpServer::newConnection, this,
|
||||
&AuthListener::NewConnection);
|
||||
if (!server->listen(QHostAddress::LocalHost, 0)) {
|
||||
blog(LOG_DEBUG, "Server could not start");
|
||||
emit fail();
|
||||
} else {
|
||||
blog(LOG_DEBUG, "Server started at port %d",
|
||||
server->serverPort());
|
||||
}
|
||||
}
|
||||
|
||||
quint16 AuthListener::GetPort()
|
||||
{
|
||||
return server ? server->serverPort() : 0;
|
||||
}
|
||||
|
||||
void AuthListener::NewConnection()
|
||||
{
|
||||
QTcpSocket *socket = server->nextPendingConnection();
|
||||
if (socket) {
|
||||
connect(socket, &QTcpSocket::disconnected, socket,
|
||||
&QTcpSocket::deleteLater);
|
||||
connect(socket, &QTcpSocket::readyRead, socket, [&, socket]() {
|
||||
QByteArray buffer;
|
||||
while (socket->bytesAvailable() > 0) {
|
||||
buffer.append(socket->readAll());
|
||||
}
|
||||
socket->write(QT_TO_UTF8(serverResponseHeader));
|
||||
QString redirect = QString::fromLatin1(buffer);
|
||||
blog(LOG_DEBUG, "redirect: %s", QT_TO_UTF8(redirect));
|
||||
|
||||
QRegularExpression re("(&|\\?)code=(?<code>[^&]+)");
|
||||
QRegularExpressionMatch match = re.match(redirect);
|
||||
if (!match.hasMatch())
|
||||
blog(LOG_DEBUG, "no 'code' in server redirect");
|
||||
|
||||
QString code = match.captured("code");
|
||||
|
||||
if (code.isEmpty()) {
|
||||
auto data = QTStr("YouTube.Auth.NoCode");
|
||||
socket->write(QT_TO_UTF8(data));
|
||||
emit fail();
|
||||
} else {
|
||||
auto data = responseTemplate.arg(
|
||||
QTStr("YouTube.Auth.Ok"));
|
||||
socket->write(QT_TO_UTF8(data));
|
||||
emit ok(code);
|
||||
}
|
||||
socket->flush();
|
||||
socket->close();
|
||||
});
|
||||
} else {
|
||||
emit fail();
|
||||
}
|
||||
}
|
21
UI/auth-listener.hpp
Normal file
21
UI/auth-listener.hpp
Normal file
@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QtNetwork/QTcpServer>
|
||||
|
||||
class AuthListener : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
QTcpServer *server;
|
||||
|
||||
signals:
|
||||
void ok(const QString &code);
|
||||
void fail();
|
||||
|
||||
protected:
|
||||
void NewConnection();
|
||||
|
||||
public:
|
||||
explicit AuthListener(QObject *parent = 0);
|
||||
quint16 GetPort();
|
||||
};
|
@ -16,15 +16,18 @@
|
||||
|
||||
using namespace json11;
|
||||
|
||||
#ifdef BROWSER_AVAILABLE
|
||||
#include <browser-panel.hpp>
|
||||
extern QCef *cef;
|
||||
extern QCefCookieManager *panel_cookies;
|
||||
#endif
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
OAuthLogin::OAuthLogin(QWidget *parent, const std::string &url, bool token)
|
||||
: QDialog(parent), get_token(token)
|
||||
{
|
||||
#ifdef BROWSER_AVAILABLE
|
||||
if (!cef) {
|
||||
return;
|
||||
}
|
||||
@ -61,19 +64,23 @@ OAuthLogin::OAuthLogin(QWidget *parent, const std::string &url, bool token)
|
||||
QVBoxLayout *topLayout = new QVBoxLayout(this);
|
||||
topLayout->addWidget(cefWidget);
|
||||
topLayout->addLayout(bottomLayout);
|
||||
#endif
|
||||
}
|
||||
|
||||
OAuthLogin::~OAuthLogin()
|
||||
{
|
||||
#ifdef BROWSER_AVAILABLE
|
||||
delete cefWidget;
|
||||
#endif
|
||||
}
|
||||
|
||||
int OAuthLogin::exec()
|
||||
{
|
||||
#ifdef BROWSER_AVAILABLE
|
||||
if (cefWidget) {
|
||||
return QDialog::exec();
|
||||
}
|
||||
|
||||
#endif
|
||||
return QDialog::Rejected;
|
||||
}
|
||||
|
||||
@ -174,7 +181,24 @@ bool OAuth::TokenExpired()
|
||||
}
|
||||
|
||||
bool OAuth::GetToken(const char *url, const std::string &client_id,
|
||||
const std::string &secret, const std::string &redirect_uri,
|
||||
int scope_ver, const std::string &auth_code, bool retry)
|
||||
{
|
||||
return GetTokenInternal(url, client_id, secret, redirect_uri, scope_ver,
|
||||
auth_code, retry);
|
||||
}
|
||||
|
||||
bool OAuth::GetToken(const char *url, const std::string &client_id,
|
||||
int scope_ver, const std::string &auth_code, bool retry)
|
||||
{
|
||||
return GetTokenInternal(url, client_id, {}, {}, scope_ver, auth_code,
|
||||
retry);
|
||||
}
|
||||
|
||||
bool OAuth::GetTokenInternal(const char *url, const std::string &client_id,
|
||||
const std::string &secret,
|
||||
const std::string &redirect_uri, int scope_ver,
|
||||
const std::string &auth_code, bool retry)
|
||||
try {
|
||||
std::string output;
|
||||
std::string error;
|
||||
@ -199,6 +223,14 @@ try {
|
||||
std::string post_data;
|
||||
post_data += "action=redirect&client_id=";
|
||||
post_data += client_id;
|
||||
if (!secret.empty()) {
|
||||
post_data += "&client_secret=";
|
||||
post_data += secret;
|
||||
}
|
||||
if (!redirect_uri.empty()) {
|
||||
post_data += "&redirect_uri=";
|
||||
post_data += redirect_uri;
|
||||
}
|
||||
|
||||
if (!auth_code.empty()) {
|
||||
post_data += "&grant_type=authorization_code&code=";
|
||||
|
@ -64,6 +64,16 @@ protected:
|
||||
int scope_ver,
|
||||
const std::string &auth_code = std::string(),
|
||||
bool retry = false);
|
||||
bool GetToken(const char *url, const std::string &client_id,
|
||||
const std::string &secret,
|
||||
const std::string &redirect_uri, int scope_ver,
|
||||
const std::string &auth_code, bool retry);
|
||||
|
||||
private:
|
||||
bool GetTokenInternal(const char *url, const std::string &client_id,
|
||||
const std::string &secret,
|
||||
const std::string &redirect_uri, int scope_ver,
|
||||
const std::string &auth_code, bool retry);
|
||||
};
|
||||
|
||||
class OAuthStreamKey : public OAuth {
|
||||
|
@ -438,7 +438,8 @@ void AutoConfigStreamPage::OnAuthConnected()
|
||||
std::string service = QT_TO_UTF8(ui->service->currentText());
|
||||
Auth::Type type = Auth::AuthType(service);
|
||||
|
||||
if (type == Auth::Type::OAuth_StreamKey) {
|
||||
if (type == Auth::Type::OAuth_StreamKey ||
|
||||
type == Auth::Type::OAuth_LinkedAccount) {
|
||||
OnOAuthStreamKeyConnected();
|
||||
}
|
||||
}
|
||||
|
@ -560,7 +560,8 @@ void OBSBasicSettings::OnAuthConnected()
|
||||
std::string service = QT_TO_UTF8(ui->service->currentText());
|
||||
Auth::Type type = Auth::AuthType(service);
|
||||
|
||||
if (type == Auth::Type::OAuth_StreamKey) {
|
||||
if (type == Auth::Type::OAuth_StreamKey ||
|
||||
type == Auth::Type::OAuth_LinkedAccount) {
|
||||
OnOAuthStreamKeyConnected();
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user