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.)
master
Yuriy Chumak 2021-06-27 05:27:12 -07:00 committed by jp9000
parent 63ad0642ae
commit 0654675f32
9 changed files with 173 additions and 5 deletions

View File

@ -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

View File

@ -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();

View File

@ -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
View 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
View 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();
};

View File

@ -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=";

View File

@ -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 {

View File

@ -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();
}
}

View File

@ -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();
}