Add QR code pairing

This commit is contained in:
Yukino Song
2024-08-31 12:04:39 +08:00
parent d416886999
commit 6e407578c3
10 changed files with 262 additions and 31 deletions

View File

@@ -835,6 +835,8 @@ namespace confighttp {
}
outputTree.put("otp", nvhttp::request_otp(passphrase, deviceName));
outputTree.put("ip", platf::get_local_ip_for_gateway());
outputTree.put("name", config::nvhttp.sunshine_name);
outputTree.put("status", true);
outputTree.put("message", "OTP created, effective within 3 minutes.");
}

View File

@@ -292,7 +292,7 @@ main(int argc, char *argv[]) {
BOOST_LOG(fatal) << "HTTP interface failed to initialize"sv;
#ifdef _WIN32
BOOST_LOG(fatal) << "To relaunch Apollo successfully, use the shortcut in the Start Menu. Do not run Sunshine.exe manually."sv;
BOOST_LOG(fatal) << "To relaunch Apollo successfully, use the shortcut in the Start Menu. Do not run sunshine.exe manually."sv;
std::this_thread::sleep_for(10s);
#endif

View File

@@ -567,6 +567,9 @@ namespace platf {
std::string
get_mac_address(const std::string_view &address);
std::string
get_local_ip_for_gateway();
std::string
from_sockaddr(const sockaddr *const);
std::pair<std::uint16_t, std::string>

View File

@@ -239,6 +239,83 @@ namespace platf {
return "00:00:00:00:00:00"s;
}
std::string
get_local_ip_for_gateway() {
int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
if (fd < 0) {
BOOST_LOG(warning) << "Socket creation failed";
return "";
}
char buffer[8192];
struct nlmsghdr *nlMsg = (struct nlmsghdr *)buffer;
struct rtmsg *rtMsg = (struct rtmsg *)NLMSG_DATA(nlMsg);
struct rtattr *rtAttr;
int len = 0;
memset(nlMsg, 0, sizeof(struct nlmsghdr));
nlMsg->nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg));
nlMsg->nlmsg_type = RTM_GETROUTE;
nlMsg->nlmsg_flags = NLM_F_DUMP | NLM_F_REQUEST;
nlMsg->nlmsg_seq = 1;
nlMsg->nlmsg_pid = getpid();
if (send(fd, nlMsg, nlMsg->nlmsg_len, 0) < 0) {
close(fd);
BOOST_LOG(warning) << "Send message failed";
return "";
}
len = recv(fd, nlMsg, sizeof(buffer), 0);
if (len < 0) {
close(fd);
BOOST_LOG(warning) << "Receive message failed";
return "";
}
std::string local_ip;
for (; NLMSG_OK(nlMsg, len); nlMsg = NLMSG_NEXT(nlMsg, len)) {
rtMsg = (struct rtmsg *)NLMSG_DATA(nlMsg);
if (rtMsg->rtm_family != AF_INET || rtMsg->rtm_table != RT_TABLE_MAIN)
continue;
rtAttr = (struct rtattr *)RTM_RTA(rtMsg);
int rtLen = RTM_PAYLOAD(nlMsg);
in_addr gateway;
in_addr local;
memset(&gateway, 0, sizeof(gateway));
memset(&local, 0, sizeof(local));
for (; RTA_OK(rtAttr, rtLen); rtAttr = RTA_NEXT(rtAttr, rtLen)) {
switch(rtAttr->rta_type) {
case RTA_GATEWAY:
gateway.s_addr = *reinterpret_cast<uint32_t *>(RTA_DATA(rtAttr));
break;
case RTA_PREFSRC:
local.s_addr = *reinterpret_cast<uint32_t *>(RTA_DATA(rtAttr));
break;
default:
break;
}
}
if (gateway.s_addr != 0 && local.s_addr != 0) {
local_ip = inet_ntoa(local);
break;
}
}
close(fd);
if (local_ip.empty()) {
BOOST_LOG(warning) << "No associated IP address found for the default gateway";
}
return local_ip;
}
bp::child
run_command(bool elevated, bool interactive, const std::string &cmd, boost::filesystem::path &working_dir, const bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) {
if (!group) {

View File

@@ -168,6 +168,12 @@ namespace platf {
return "00:00:00:00:00:00"s;
}
// TODO: return actual IP
std::string
get_local_ip_for_gateway() {
return "";
}
bp::child
run_command(bool elevated, bool interactive, const std::string &cmd, boost::filesystem::path &working_dir, const bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) {
if (!group) {

View File

@@ -190,6 +190,62 @@ namespace platf {
return "00:00:00:00:00:00"s;
}
std::string
get_local_ip_for_gateway() {
PIP_ADAPTER_INFO pAdapterInfo;
PIP_ADAPTER_INFO pAdapter = nullptr;
DWORD dwRetVal = 0;
ULONG ulOutBufLen = sizeof(IP_ADAPTER_INFO);
pAdapterInfo = (IP_ADAPTER_INFO *)malloc(sizeof(IP_ADAPTER_INFO));
if (pAdapterInfo == nullptr) {
BOOST_LOG(warning) << "Error allocating memory needed to call GetAdaptersInfo";
return "";
}
// Make an initial call to GetAdaptersInfo to get the necessary size into the ulOutBufLen variable
if (GetAdaptersInfo(pAdapterInfo, &ulOutBufLen) == ERROR_BUFFER_OVERFLOW) {
free(pAdapterInfo);
pAdapterInfo = (IP_ADAPTER_INFO *)malloc(ulOutBufLen);
if (pAdapterInfo == nullptr) {
BOOST_LOG(warning) << "Error allocating memory needed to call GetAdaptersInfo";
return "";
}
}
if ((dwRetVal = GetAdaptersInfo(pAdapterInfo, &ulOutBufLen)) != NO_ERROR) {
if (pAdapterInfo) {
free(pAdapterInfo);
}
BOOST_LOG(warning) << "GetAdaptersInfo failed with error: " + std::to_string(dwRetVal);
return "";
}
pAdapter = pAdapterInfo;
std::string local_ip;
// Iterate through the list of adapters
while (pAdapter) {
IP_ADDR_STRING* pGateway = &pAdapter->GatewayList;
if (pGateway && pGateway->IpAddress.String[0] != '\0') {
// This adapter has a default gateway, use its IP address
local_ip = pAdapter->IpAddressList.IpAddress.String;
break;
}
pAdapter = pAdapter->Next;
}
if (pAdapterInfo) {
free(pAdapterInfo);
}
if (local_ip.empty()) {
BOOST_LOG(warning) << "No associated IP address found for the default gateway";
}
return local_ip;
}
HDESK
syncThreadDesktop() {
auto hDesk = OpenInputDesktop(DF_ALLOWOTHERACCOUNTHOOK, FALSE, GENERIC_ALL);

View File

@@ -4,6 +4,7 @@
<head>
<%- header %>
<script type="text/javascript" src="/assets/js/qrcode.min.js"></script>
<style scoped type="text/css">
.content-container {
padding-top: 2em;
@@ -18,20 +19,29 @@
<body id="app" v-cloak>
<Navbar></Navbar>
<div id="content" class="container content-container">
<ul class="nav nav-pills nav-fill pin-tab-bar">
<ul class="nav nav-pills pin-tab-bar justify-content-center">
<li class="nav-item">
<a class="nav-link" :class="{active: currentTab === 'OTP'}" @click="currentTab = 'OTP'">{{ $t('pin.otp_pairing') }}</a>
<a class="nav-link" :class="{active: currentTab === 'OTP'}" href="#" @click.prevent="switchTab('OTP')">{{ $t('pin.otp_pairing') }}</a>
</li>
<li class="nav-item">
<a class="nav-link" :class="{active: currentTab === 'PIN'}" @click="currentTab = 'PIN'">{{ $t('pin.pin_pairing') }}</a>
<a class="nav-link" :class="{active: currentTab === 'PIN'}" href="#" @click.prevent="switchTab('PIN')">{{ $t('pin.pin_pairing') }}</a>
</li>
</ul>
<form v-if="currentTab === 'OTP'" class="form d-flex flex-column align-items-center" @submit.prevent="requestOTP">
<div class="card flex-column d-flex p-4 mb-4">
<h1 class="my-4 text-center">{{ otp && otp || '????' }}</h1>
<input type="text" pattern="[0-9a-zA-Z]{4,}" :placeholder="`${$t('pin.otp_passphrase')}`" v-model="passphrase" required autofocus class="form-control mt-2" />
<input type="text" :placeholder="`${$t('pin.device_name')}`" v-model="deviceName" class="form-control my-4" />
<button class="btn btn-primary">{{ $t('pin.generate_pin') }}</button>
<div v-show="editingHost || (otp && hostAddr)" id="qrRef"></div>
<p v-if="editingHost || (otp && hostAddr)" class="text-center text-secondary"><a class="text-secondary" :href="deepLink">art://{{ hostAddr }}:{{ hostPort }}</a> <i class="fas fa-fw fa-pen-to-square" @click="editHost"></i></p>
<h1 class="mb-4 text-center">{{ otp && otp || '????' }}</h1>
<div v-if="editingHost" class="d-flex flex-column align-items-stretch">
<input type="text" placeholder="HOST" v-model="hostAddr" autofocus class="form-control mt-2" />
<input type="text" placeholder="PORT" v-model="hostPort" class="form-control my-4" />
<button class="btn btn-primary" :disabled="!this.canSaveHost" @click.prevent="saveHost">{{ $t('_common.save') }}</button>
</div>
<div v-else class="d-flex flex-column align-items-stretch">
<input type="text" pattern="[0-9a-zA-Z]{4,}" :placeholder="`${$t('pin.otp_passphrase')}`" v-model="passphrase" required autofocus class="form-control mt-2" />
<input type="text" :placeholder="`${$t('pin.device_name')}`" v-model="deviceName" class="form-control my-4" />
<button class="btn btn-primary">{{ $t('pin.generate_pin') }}</button>
</div>
</div>
<div v-if="otpMessage" class="alert" :class="['alert-' + otpStatus]">{{ otpMessage }}</div>
<div class="alert alert-info">{{ $t('pin.otp_msg') }}</div>
@@ -56,23 +66,76 @@
import Navbar from './Navbar.vue'
let resetOTPTimeout = null;
const qrContainer = document.createElement('div');
qrContainer.className = "mb-2 p-2 bg-white"
const qrCode = new QRCode(qrContainer);
const updateQR = (url) => {
qrCode.clear()
qrCode.makeCode(url)
const refContainer = document.querySelector('#qrRef');
if (refContainer) refContainer.appendChild(qrContainer);
}
let hostInfoCache = JSON.parse(sessionStorage.getItem('hostInfo'));
let hostManuallySet = false;
if (hostInfoCache) hostManuallySet = true;
const saveHostCache = ({hostAddr, hostPort}, manual) => {
hostInfoCache = {hostAddr, hostPort}
if (manual) {
sessionStorage.setItem('hostInfo', JSON.stringify(hostInfoCache))
hostManuallySet = true;
}
}
const data = () => {
return {
editingHost: false,
currentTab: 'OTP',
otp: '',
passphrase: '',
otpMessage: '',
otpStatus: 'warning',
deviceName: '',
hostAddr: '',
hostPort: '',
hostName: ''
}
}
let app = createApp({
components: {
Navbar
},
inject: ['i18n'],
data() {
return {
currentTab: 'OTP',
otp: '',
passphrase: '',
otpMessage: '',
otpStatus: 'warning',
deviceName: ''
data,
computed: {
deepLink() {
return encodeURI(`art://${this.hostAddr}:${this.hostPort}?pin=${this.otp}&passphrase=${this.passphrase}&name=${this.hostName}`);
},
canSaveHost() {
return !!(this.hostAddr && this.hostPort);
}
},
methods: {
switchTab(currentTab) {
Object.assign(this, data(), {currentTab});
hostInfoCache = null;
clearTimeout(resetOTPTimeout);
},
editHost() {
this.editingHost = !this.editingHost;
Object.assign(this, hostInfoCache);
},
saveHost() {
if (!this.canSaveHost) return;
updateQR(this.deepLink);
this.editingHost = false;
saveHostCache(this, true);
},
registerDevice(e) {
let pin = document.querySelector("#pin-input").value;
let name = document.querySelector("#name-input").value;
@@ -99,6 +162,8 @@
});
},
requestOTP() {
if (this.editingHost) return;
fetch(`/api/otp?passphrase=${this.passphrase}${this.deviceName && `&deviceName=${this.deviceName}` || ''}`, {
credentials: 'include'
})
@@ -111,23 +176,44 @@
}
this.otp = resp.otp
this.hostName = resp.name
this.otpStatus = 'success'
this.otpMessage = this.i18n.t('pin.otp_success')
if (resetOTPTimeout !== null) clearTimeout(resetOTPTimeout)
resetOTPTimeout = setTimeout(() => {
this.otp = this.i18n.t('pin.otp_expired')
this.otpMessage = this.i18n.t('pin.otp_expired_msg')
this.otpStatus = 'warning'
resetOTPTimeout = null
}, 3 * 60 * 1000)
const isLocalHost = ['localhost', '127.0.0.1', '[::1]'].indexOf(location.hostname) < 0
if (['localhost', '127.0.0.1', '[::1]'].indexOf(location.hostname) < 0) {
setTimeout(() => {
if (window.confirm(this.i18n.t('pin.otp_pair_now'))) {
window.open(`art://${location.hostname}:${parseInt(location.port, 10) - 1}?pin=${this.otp}&passphrase=${this.passphrase}`);
}
}, 0)
if (hostManuallySet) {
Object.assign(this, hostInfoCache);
} else {
this.hostAddr = resp.ip
this.hostPort = parseInt(location.port, 10) - 1
if (isLocalHost) {
this.hostAddr = location.hostname
}
saveHostCache(this);
}
if (this.hostAddr) {
updateQR(this.deepLink);
if (resetOTPTimeout !== null) clearTimeout(resetOTPTimeout)
resetOTPTimeout = setTimeout(() => {
Object.assign(this, data(), {
otp: this.i18n.t('pin.otp_expired'),
otpMessage: this.i18n.t('pin.otp_expired_msg')
})
resetOTPTimeout = null
}, 3 * 60 * 1000)
if (isLocalHost) {
setTimeout(() => {
if (window.confirm(this.i18n.t('pin.otp_pair_now'))) {
window.open(this.deepLink);
}
}, 0)
}
}
})
}

File diff suppressed because one or more lines are too long

View File

@@ -380,7 +380,7 @@
"otp_expired": "EXPIRED",
"otp_expired_msg": "OTP expired. Please request a new one.",
"otp_success": "PIN request success, the PIN is available within 3 minutes.",
"otp_msg": "OTP pairing is only available for Artemis clients. Please use legacy pairing method for other clients.",
"otp_msg": "OTP pairing is only available for the latest Artemis clients. Please use legacy pairing method for other clients.",
"otp_pair_now": "PIN generated successfully, do you want to pair now?"
},
"resource_card": {

View File

@@ -381,7 +381,7 @@
"otp_expired": "已过期",
"otp_expired_msg": "口令已过期,请重新请求。",
"otp_success": "一次性 PIN 请求成功3分钟内有效。",
"otp_msg": "一次性口令目前仅支持 Artemis 客户端使用。其他客户端请使用传统配对方式。",
"otp_msg": "一次性口令目前仅支持最新的 Artemis 客户端使用。其他客户端请使用传统配对方式。",
"otp_pair_now": "PIN 请求成功,是否一键配对?"
},
"resource_card": {