better error messages
This commit is contained in:
956
index.html
956
index.html
@@ -1,261 +1,364 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Notaquest</title>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/tweetnacl@1.0.3/nacl-fast.min.js"></script>
|
|
||||||
<link
|
|
||||||
rel="stylesheet"
|
|
||||||
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<meta
|
<head>
|
||||||
name="description"
|
<meta charset="UTF-8" />
|
||||||
content="Enable ADB on your Meta Quest without creating a developer organization or verifying with a credit card. One-click tool using Web Bluetooth."
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
/>
|
<title>Notaquest</title>
|
||||||
<meta
|
<script src="https://cdn.jsdelivr.net/npm/tweetnacl@1.0.3/nacl-fast.min.js"></script>
|
||||||
name="keywords"
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css" />
|
||||||
content="meta quest, quest 3, adb, developer mode, sideload, sidequest, no organization, web bluetooth"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Open Graph (social sharing) -->
|
<meta name="description"
|
||||||
<meta
|
content="Enable ADB on your Meta Quest without creating a developer organization or verifying with a credit card. One-click tool using Web Bluetooth." />
|
||||||
property="og:title"
|
<meta name="keywords"
|
||||||
content="Notaquest - Enable ADB on Meta Quest without a developer org"
|
content="meta quest, quest 3, adb, developer mode, sideload, sidequest, no organization, web bluetooth" />
|
||||||
/>
|
|
||||||
<meta
|
|
||||||
property="og:description"
|
|
||||||
content="Skip the organization, skip the credit card. Enable ADB on your Quest from a web page."
|
|
||||||
/>
|
|
||||||
<meta property="og:url" content="https://notaquest.owendeed.com" />
|
|
||||||
<meta property="og:type" content="website" />
|
|
||||||
|
|
||||||
<!-- Twitter/X -->
|
<!-- Open Graph (social sharing) -->
|
||||||
<meta name="twitter:card" content="summary" />
|
<meta property="og:title" content="Notaquest - Enable ADB on Meta Quest without a developer org" />
|
||||||
<meta
|
<meta property="og:description"
|
||||||
name="twitter:title"
|
content="Skip the organization, skip the credit card. Enable ADB on your Quest from a web page." />
|
||||||
content="Notaquest - Enable ADB on Meta Quest without a developer org"
|
<meta property="og:url" content="https://notaquest.owendeed.com" />
|
||||||
/>
|
<meta property="og:type" content="website" />
|
||||||
<meta
|
|
||||||
name="twitter:description"
|
<!-- Twitter/X -->
|
||||||
content="Skip the organization, skip the credit card. Enable ADB on your Quest from a web page."
|
<meta name="twitter:card" content="summary" />
|
||||||
/>
|
<meta name="twitter:title" content="Notaquest - Enable ADB on Meta Quest without a developer org" />
|
||||||
</head>
|
<meta name="twitter:description"
|
||||||
<body>
|
content="Skip the organization, skip the credit card. Enable ADB on your Quest from a web page." />
|
||||||
<main class="container">
|
</head>
|
||||||
<h1>Notaquest</h1>
|
|
||||||
|
<body>
|
||||||
|
<main class="container">
|
||||||
|
<h1>Notaquest</h1>
|
||||||
|
<p>
|
||||||
|
enabling adb on a meta quest (which is just a weird phone btw) is
|
||||||
|
PAINFUL! you have to get the slow meta horizon app, create an
|
||||||
|
organization, verify it by adding an authenticator or USING YOUR CREDIT
|
||||||
|
CARD and finally, being able to enable a thing androids could do in like
|
||||||
|
8 taps for years. it doesn't have to be a quest to enable adb. meta
|
||||||
|
still made it a quest for me to make this work for you, and it's gonna
|
||||||
|
suck, but it'll suck less than the normal way. i have uploaded 2 images
|
||||||
|
of forks for meta verification, i will update this message if it works
|
||||||
|
and i can do oauth. update: it did not. you can use the tampermonkey
|
||||||
|
script to automate it or use devtools and grab a cookie. i'm very sorry.
|
||||||
|
this seems to work better on mobile than on pc. tested on android with
|
||||||
|
google chrome. you may need to quickly accept a pairing confirmation on
|
||||||
|
the device and headset. the tampermonkey script cannot be ran on Android
|
||||||
|
Chrome since it doesn't support extensions. you can use firefox for it
|
||||||
|
and swap to Chrome later, use a browser that supports bluetooth and
|
||||||
|
extensions, or do it on a pc first and swap later. firefox is not
|
||||||
|
supported for bluetooth :(
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
i apologize for you having to give me the token, this isn't something
|
||||||
|
you should need to do to own your device, but sadly, it is required. you
|
||||||
|
can check the html (human friendly code), and you can log network
|
||||||
|
requests from the site. the only place it goes it to is my site with a
|
||||||
|
proxy to meta. yeah, i know what you're thinking if you get what that
|
||||||
|
means. the token goes to my site. i need to for this to work, i'm sorry,
|
||||||
|
there's a thing below explaining the code. i know i can't prove it, but
|
||||||
|
it's safe. if you're really paranoid, reset your password or log out of
|
||||||
|
the page you got the token from to invalidate the token. if i stole your
|
||||||
|
token i wouldn't be telling you to do this. you can even check if it
|
||||||
|
logs into your account before you reset it.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
currently only tested on meta quest 3 but should work on other devices.
|
||||||
|
make a github issue (link at the bottom) if it doesn't work for any
|
||||||
|
reason.
|
||||||
|
</p>
|
||||||
|
<details>
|
||||||
|
<summary>why does this use a proxy?</summary>
|
||||||
<p>
|
<p>
|
||||||
enabling adb on a meta quest (which is just a weird phone btw) is
|
meta's api blocks requests from websites (CORS). the proxy forwards
|
||||||
PAINFUL! you have to get the slow meta horizon app, create an
|
your request to graph.oculus.com and nothing else. nothing is stored
|
||||||
organization, verify it by adding an authenticator or USING YOUR CREDIT
|
or logged. you can
|
||||||
CARD and finally, being able to enable a thing androids could do in like
|
<a href="https://api.notaquest.owendeed.com" target="_blank">view the live worker source code</a>
|
||||||
8 taps for years. it doesn't have to be a quest to enable adb. meta
|
or check the network tab yourself. the code is a literal quine
|
||||||
still made it a quest for me to make this work for you, and it's gonna
|
program, if i change it, it will change.
|
||||||
suck, but it'll suck less than the normal way. i have uploaded 2 images
|
|
||||||
of forks for meta verification, i will update this message if it works
|
|
||||||
and i can do oauth. update: it did not. you can use the tampermonkey
|
|
||||||
script to automate it or use devtools and grab a cookie. i'm very sorry.
|
|
||||||
this seems to work better on mobile than on pc. tested on android with
|
|
||||||
google chrome. you may need to quickly accept a pairing confirmation on
|
|
||||||
the device and headset. the tampermonkey script cannot be ran on Android
|
|
||||||
Chrome since it doesn't support extensions. you can use firefox for it
|
|
||||||
and swap to Chrome later, use a browser that supports bluetooth and
|
|
||||||
extensions, or do it on a pc first and swap later. firefox is not
|
|
||||||
supported for bluetooth :(
|
|
||||||
</p>
|
</p>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary>what is the point?</summary>
|
||||||
<p>
|
<p>
|
||||||
i apologize for you having to give me the token, this isn't something
|
normally, enabling ADB on your meta headset, is a PAIN to do, like the
|
||||||
you should need to do to own your device, but sadly, it is required. you
|
amount of work rooting a normal phone takes if you're lucky. which is
|
||||||
can check the html (human friendly code), and you can log network
|
still bad. you need to create a meta organization, and then verify
|
||||||
requests from the site. the only place it goes it to is my site with a
|
your account. verifying means a few options: enabling an authenticator
|
||||||
proxy to meta. yeah, i know what you're thinking if you get what that
|
app for signing in, or using a credit card and putting a temporary
|
||||||
means. the token goes to my site. i need to for this to work, i'm sorry,
|
hold on it. that's for enabling the ability to control a device you
|
||||||
there's a thing below explaining the code. i know i can't prove it, but
|
bought. and not even full access to it. this site lets you enable it
|
||||||
it's safe. if you're really paranoid, reset your password or log out of
|
without any of that. the only annoying part is the meta token, which
|
||||||
the page you got the token from to invalidate the token. if i stole your
|
sadly i don't think i can bypass. better than nothing i guess?
|
||||||
token i wouldn't be telling you to do this. you can even check if it
|
|
||||||
logs into your account before you reset it.
|
|
||||||
</p>
|
</p>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary>but i can't access devtools on my phone!</summary>
|
||||||
<p>
|
<p>
|
||||||
currently only tested on meta quest 3 but should work on other devices.
|
do the tampermonkey script on firefox/a browser on mobile with
|
||||||
make a gitea issue (link at the bottom) if it doesn't work for any
|
extension support and swap to one with bluetooth after! or do the
|
||||||
reason.
|
following if you can do devtools on a pc but don't wanna do
|
||||||
|
tampermonkey: at the bottom click the generate token qr code button.
|
||||||
|
this will give you a qr code. scan it on your phone and go to the
|
||||||
|
link. it will be prefilled with your token so you don't need to type
|
||||||
|
it :)
|
||||||
|
</p>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary>what is tampermonkey and how do i get it?</summary>
|
||||||
|
<p>
|
||||||
|
tampermonkey is an extension which lets you install scripts to modify
|
||||||
|
what websites do, such as automatically grabbing cookies when going to
|
||||||
|
a site! which is exactly what this site uses it for. if you can, this
|
||||||
|
is the best way to get a token.
|
||||||
</p>
|
</p>
|
||||||
<details>
|
|
||||||
<summary>why does this use a proxy?</summary>
|
|
||||||
<p>
|
|
||||||
meta's api blocks requests from websites (CORS). the proxy forwards
|
|
||||||
your request to graph.oculus.com and nothing else. nothing is stored
|
|
||||||
or logged. you can
|
|
||||||
<a href="https://api.notaquest.owendeed.com" target="_blank"
|
|
||||||
>view the live worker source code</a
|
|
||||||
>
|
|
||||||
or check the network tab yourself. the code is a literal quine
|
|
||||||
program, if i change it, it will change.
|
|
||||||
</p>
|
|
||||||
</details>
|
|
||||||
<details>
|
|
||||||
<summary>what is the point?</summary>
|
|
||||||
<p>
|
|
||||||
normally, enabling ADB on your meta headset, is a PAIN to do, like the
|
|
||||||
amount of work rooting a normal phone takes if you're lucky. which is
|
|
||||||
still bad. you need to create a meta organization, and then verify
|
|
||||||
your account. verifying means a few options: enabling an authenticator
|
|
||||||
app for signing in, or using a credit card and putting a temporary
|
|
||||||
hold on it. that's for enabling the ability to control a device you
|
|
||||||
bought. and not even full access to it. this site lets you enable it
|
|
||||||
without any of that. the only annoying part is the meta token, which
|
|
||||||
sadly i don't think i can bypass. better than nothing i guess?
|
|
||||||
</p>
|
|
||||||
</details>
|
|
||||||
<details>
|
|
||||||
<summary>but i can't access devtools on my phone!</summary>
|
|
||||||
<p>
|
|
||||||
do the tampermonkey script on firefox/a browser on mobile with
|
|
||||||
extension support and swap to one with bluetooth after! or do the
|
|
||||||
following if you can do devtools on a pc but don't wanna do
|
|
||||||
tampermonkey: at the bottom click the generate token qr code button.
|
|
||||||
this will give you a qr code. scan it on your phone and go to the
|
|
||||||
link. it will be prefilled with your token so you don't need to type
|
|
||||||
it :)
|
|
||||||
</p>
|
|
||||||
</details>
|
|
||||||
<details>
|
|
||||||
<summary>what is tampermonkey and how do i get it?</summary>
|
|
||||||
<p>
|
|
||||||
tampermonkey is an extension which lets you install scripts to modify
|
|
||||||
what websites do, such as automatically grabbing cookies when going to
|
|
||||||
a site! which is exactly what this site uses it for. if you can, this
|
|
||||||
is the best way to get a token.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>
|
<summary>
|
||||||
IMPORTANT: additional setup for (most) chromium based browsers
|
IMPORTANT: additional setup for (most) chromium based browsers
|
||||||
(required or silently doesn't work)
|
(required or silently doesn't work)
|
||||||
</summary>
|
</summary>
|
||||||
|
<p>
|
||||||
|
the following instructions are required for chromium-based browsers
|
||||||
|
or it will do nothing and not work. if you don't have the final
|
||||||
|
switch then try it without doing it. if you just see the Meta
|
||||||
|
account page when you click the get token button, this is your
|
||||||
|
problem.
|
||||||
|
</p>
|
||||||
|
<li>
|
||||||
|
click the extensions icon (normally in the top right), you should
|
||||||
|
see the tampermonkey extension
|
||||||
|
</li>
|
||||||
|
<li>click the 3 vertical dots and then click manage extension</li>
|
||||||
|
<li>turn on the "Allow User Scripts"</li>
|
||||||
|
<li>it should now work</li>
|
||||||
|
</details>
|
||||||
|
<br />
|
||||||
|
<a href="https://www.tampermonkey.net/">tampermonkey website</a><br />
|
||||||
|
<a href="https://chromewebstore.google.com/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo">chrome (+chromium
|
||||||
|
based browsers) extension</a>
|
||||||
|
<br />
|
||||||
|
<a href="https://addons.mozilla.org/firefox/addon/tampermonkey/">firefox (+firefox based browsers) extension</a>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary>automated method (tampermonkey required)</summary>
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
<p>
|
<p>
|
||||||
the following instructions are required for chromium-based browsers
|
first off, please make sure tampermonkey is installed. you can
|
||||||
or it will do nothing and not work. if you don't have the final
|
check the dropdown above this one for instructions.
|
||||||
switch then try it without doing it. if you just see the Meta
|
|
||||||
account page when you click the get token button, this is your
|
|
||||||
problem.
|
|
||||||
</p>
|
</p>
|
||||||
<li>
|
</li>
|
||||||
click the extensions icon (normally in the top right), you should
|
<li>
|
||||||
see the tampermonkey extension
|
<button onclick="window.open('/grab.user.js')">
|
||||||
</li>
|
install tampermonkey script for automating the token process
|
||||||
<li>click the 3 vertical dots and then click manage extension</li>
|
(tampermonkey must be installed)
|
||||||
<li>turn on the "Allow User Scripts"</li>
|
</button>
|
||||||
<li>it should now work</li>
|
</li>
|
||||||
</details>
|
<li>
|
||||||
<br />
|
<p>
|
||||||
<a href="https://www.tampermonkey.net/">tampermonkey website</a><br />
|
after clicking install, click the get token automatically button,
|
||||||
<a
|
which will open a new notaquest tab with your token prefilled.
|
||||||
href="https://chromewebstore.google.com/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo"
|
</p>
|
||||||
>chrome (+chromium based browsers) extension</a
|
</li>
|
||||||
>
|
<li>
|
||||||
<br />
|
<button onclick="
|
||||||
<a href="https://addons.mozilla.org/firefox/addon/tampermonkey/"
|
|
||||||
>firefox (+firefox based browsers) extension</a
|
|
||||||
>
|
|
||||||
</details>
|
|
||||||
<details>
|
|
||||||
<summary>automated method (tampermonkey required)</summary>
|
|
||||||
<ol>
|
|
||||||
<li>
|
|
||||||
<p>
|
|
||||||
first off, please make sure tampermonkey is installed. you can
|
|
||||||
check the dropdown above this one for instructions.
|
|
||||||
</p>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<button onclick="window.open('/grab.user.js')">
|
|
||||||
install tampermonkey script for automating the token process
|
|
||||||
(tampermonkey must be installed)
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<p>
|
|
||||||
after clicking install, click the get token automatically button,
|
|
||||||
which will open a new notaquest tab with your token prefilled.
|
|
||||||
</p>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<button
|
|
||||||
onclick="
|
|
||||||
window.location.href = 'https://secure.oculus.com/#notaquest'
|
window.location.href = 'https://secure.oculus.com/#notaquest'
|
||||||
"
|
">
|
||||||
>
|
get token automatically (tampermonkey script must be installed)
|
||||||
get token automatically (tampermonkey script must be installed)
|
</button>
|
||||||
</button>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<p>feel free to uninstall the tampermonkey script afterwards.</p>
|
||||||
<p>feel free to uninstall the tampermonkey script afterwards.</p>
|
</li>
|
||||||
</li>
|
</ol>
|
||||||
</ol>
|
</details>
|
||||||
</details>
|
<details>
|
||||||
<details>
|
<summary>manual method (no tampermonkey)</summary>
|
||||||
<summary>manual method (no tampermonkey)</summary>
|
<ol>
|
||||||
<ol>
|
<li>
|
||||||
<li>
|
Log into
|
||||||
Log into
|
<a href="https://developers.meta.com/horizon/manage/" target="_blank">developers.meta.com/horizon/manage/</a>
|
||||||
<a
|
</li>
|
||||||
href="https://developers.meta.com/horizon/manage/"
|
<li>Press F12 to open developer tools</li>
|
||||||
target="_blank"
|
<li>Application -> Cookies -> click the site</li>
|
||||||
>developers.meta.com/horizon/manage/</a
|
<li>Find <code>oc_ac_at</code>, double-click the value, copy it</li>
|
||||||
>
|
<li>Paste it below</li>
|
||||||
</li>
|
</ol>
|
||||||
<li>Press F12 to open developer tools</li>
|
</details>
|
||||||
<li>Application -> Cookies -> click the site</li>
|
<input id="token" placeholder="paste your Meta token here" style="width: 100%; max-width: 500px" />
|
||||||
<li>Find <code>oc_ac_at</code>, double-click the value, copy it</li>
|
<br /><br />
|
||||||
<li>Paste it below</li>
|
<button onclick="showQR()">
|
||||||
</ol>
|
generate prefilled token qr code for mobile
|
||||||
</details>
|
</button>
|
||||||
<input
|
<br />
|
||||||
id="token"
|
<br />
|
||||||
placeholder="paste your Meta token here"
|
<img id="qr" style="width: 300px; image-rendering: pixelated" />
|
||||||
style="width: 100%; max-width: 500px"
|
<br />
|
||||||
/>
|
<button id="go" onclick="enableADB()">enable adb</button>
|
||||||
<br /><br />
|
<p id="nobt" style="display: none"></p>
|
||||||
<button onclick="showQR()">
|
<p id="status"></p>
|
||||||
generate prefilled token qr code for mobile
|
<a href="https://github.com/owenthepuppy/notaquest/issues">report an issue here</a><br />
|
||||||
</button>
|
<a href="https://gitea.owendeed.com/Toasterkitten/notaquest">view the source here</a><br />
|
||||||
<br />
|
<a href="https://owenfeldman.com">a project by owenfeldman.com</a>
|
||||||
<br />
|
</main>
|
||||||
<img id="qr" style="width: 300px; image-rendering: pixelated" />
|
<script src="https://cdn.jsdelivr.net/npm/qrcode-generator@1.4.4/qrcode.min.js"></script>
|
||||||
<br />
|
<script>
|
||||||
<button id="go" onclick="enableADB()">enable adb</button>
|
if (!navigator.bluetooth) {
|
||||||
<p id="nobt" style="display: none"></p>
|
document.getElementById("go").style.display = "none";
|
||||||
<p id="status"></p>
|
document.getElementById("nobt").style.display = "block";
|
||||||
<a href="https://github.com/owenthepuppy/notaquest/issues"
|
document.getElementById("nobt").textContent =
|
||||||
>report an issue here</a
|
"your browser doesn't support web bluetooth. grab your token, generate a qr code below, and scan it on your phone in chrome or another web bluetooth supported device. respect to the firefox people seeing this.";
|
||||||
><br />
|
}
|
||||||
<a href="https://gitea.owendeed.com/Toasterkitten/notaquest"
|
var params = new URLSearchParams(location.search);
|
||||||
>view the source here</a
|
if (params.get("token"))
|
||||||
><br />
|
document.getElementById("token").value = params.get("token");
|
||||||
<a href="https://owenfeldman.com">a project by owenfeldman.com</a>
|
var SERVICE = "0000feb8-0000-1000-8000-00805f9b34fb";
|
||||||
</main>
|
var WRITE_UUID = "7a442881-509c-47fa-ac02-b06a37d9eb76";
|
||||||
<script src="https://cdn.jsdelivr.net/npm/qrcode-generator@1.4.4/qrcode.min.js"></script>
|
|
||||||
<script>
|
|
||||||
if (!navigator.bluetooth) {
|
|
||||||
document.getElementById("go").style.display = "none";
|
|
||||||
document.getElementById("nobt").style.display = "block";
|
|
||||||
document.getElementById("nobt").textContent =
|
|
||||||
"your browser doesn't support web bluetooth. grab your token, generate a qr code below, and scan it on your phone in chrome or another web bluetooth supported device. respect to the firefox people seeing this.";
|
|
||||||
}
|
|
||||||
var params = new URLSearchParams(location.search);
|
|
||||||
if (params.get("token"))
|
|
||||||
document.getElementById("token").value = params.get("token");
|
|
||||||
var SERVICE = "0000feb8-0000-1000-8000-00805f9b34fb";
|
|
||||||
var WRITE_UUID = "7a442881-509c-47fa-ac02-b06a37d9eb76";
|
|
||||||
|
|
||||||
function status(msg) {
|
function status(msg) {
|
||||||
document.getElementById("status").textContent = msg;
|
document.getElementById("status").textContent = msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
function showQR() {
|
function showQR() {
|
||||||
|
var token = document.getElementById("token").value.trim();
|
||||||
|
if (!token) {
|
||||||
|
status("paste your token first");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!token.startsWith("OC") && !token.startsWith("FRL")) {
|
||||||
|
status(
|
||||||
|
"that doesn't look like a meta token. it should start with OC or FRL.",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var qr = qrcode(0, "L");
|
||||||
|
qr.addData("https://notaquest.owendeed.com/?token=" + token);
|
||||||
|
qr.make();
|
||||||
|
document.getElementById("qr").src = qr.createDataURL();
|
||||||
|
}
|
||||||
|
|
||||||
|
// protobuf helpers
|
||||||
|
function varint(n) {
|
||||||
|
var r = [];
|
||||||
|
do {
|
||||||
|
var b = n & 0x7f;
|
||||||
|
n >>>= 7;
|
||||||
|
if (n > 0) b |= 0x80;
|
||||||
|
r.push(b);
|
||||||
|
} while (n > 0);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
function varintField(f, v) {
|
||||||
|
return [(f << 3) | 0].concat(varint(v));
|
||||||
|
}
|
||||||
|
function bytesField(f, d) {
|
||||||
|
return [(f << 3) | 2].concat(varint(d.length)).concat(Array.from(d));
|
||||||
|
}
|
||||||
|
|
||||||
|
function readVarint(buf, off) {
|
||||||
|
var r = 0,
|
||||||
|
s = 0;
|
||||||
|
while (off < buf.length) {
|
||||||
|
var b = buf[off++];
|
||||||
|
r |= (b & 0x7f) << s;
|
||||||
|
if (!(b & 0x80)) break;
|
||||||
|
s += 7;
|
||||||
|
}
|
||||||
|
return [r, off];
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseProto(buf) {
|
||||||
|
var fields = {},
|
||||||
|
off = 0;
|
||||||
|
while (off < buf.length) {
|
||||||
|
var tag;
|
||||||
|
[tag, off] = readVarint(buf, off);
|
||||||
|
var fn = tag >> 3,
|
||||||
|
wt = tag & 7;
|
||||||
|
if (wt === 0) {
|
||||||
|
var val;
|
||||||
|
[val, off] = readVarint(buf, off);
|
||||||
|
fields[fn] = val;
|
||||||
|
} else if (wt === 2) {
|
||||||
|
var ln;
|
||||||
|
[ln, off] = readVarint(buf, off);
|
||||||
|
fields[fn] = buf.slice(off, off + ln);
|
||||||
|
off += ln;
|
||||||
|
} else break;
|
||||||
|
}
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
// chunk data with 2-byte framing header
|
||||||
|
function makeChunks(data) {
|
||||||
|
var chunks = [];
|
||||||
|
for (var i = 0; i < data.length; i += 18) {
|
||||||
|
var slice = data.slice(i, i + 18);
|
||||||
|
var seq = Math.floor(i / 18);
|
||||||
|
var last = i + 18 >= data.length;
|
||||||
|
chunks.push(
|
||||||
|
new Uint8Array(
|
||||||
|
[(last ? 0x80 : 0x00) | (seq >> 8), seq & 0xff].concat(
|
||||||
|
Array.from(slice),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return chunks;
|
||||||
|
}
|
||||||
|
|
||||||
|
// send chunked data, then poll-read the response
|
||||||
|
async function sendAndReceive(writeChar, data) {
|
||||||
|
for (var c of makeChunks(Array.from(data))) {
|
||||||
|
await writeChar.writeValueWithoutResponse(c);
|
||||||
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
|
}
|
||||||
|
await new Promise((r) => setTimeout(r, 3000));
|
||||||
|
var response = [];
|
||||||
|
for (var i = 0; i < 200; i++) {
|
||||||
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
|
var val = new Uint8Array((await writeChar.readValue()).buffer);
|
||||||
|
if (val.length <= 1 || val[0] === 0xff) break;
|
||||||
|
response.push(...val.slice(2));
|
||||||
|
if (val[0] & 0x80) break;
|
||||||
|
}
|
||||||
|
return new Uint8Array(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// HMAC-SHA256 using Web Crypto
|
||||||
|
async function hmacSHA256(key, message) {
|
||||||
|
var k = await crypto.subtle.importKey(
|
||||||
|
"raw",
|
||||||
|
key,
|
||||||
|
{ name: "HMAC", hash: "SHA-256" },
|
||||||
|
false,
|
||||||
|
["sign"],
|
||||||
|
);
|
||||||
|
return new Uint8Array(await crypto.subtle.sign("HMAC", k, message));
|
||||||
|
}
|
||||||
|
|
||||||
|
// encrypt a plaintext request with the shared key (nonce + ciphertext)
|
||||||
|
function encrypt(plaintext, sharedKey) {
|
||||||
|
var nonce = nacl.randomBytes(24);
|
||||||
|
var ciphertext = nacl.box.after(plaintext, nonce, sharedKey);
|
||||||
|
var out = new Uint8Array(24 + ciphertext.length);
|
||||||
|
out.set(nonce);
|
||||||
|
out.set(ciphertext, 24);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// decrypt a response (first 24 bytes = nonce, rest = ciphertext)
|
||||||
|
function decrypt(data, sharedKey) {
|
||||||
|
return nacl.box.open.after(
|
||||||
|
data.slice(24),
|
||||||
|
data.slice(0, 24),
|
||||||
|
sharedKey,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function enableADB() {
|
||||||
|
try {
|
||||||
var token = document.getElementById("token").value.trim();
|
var token = document.getElementById("token").value.trim();
|
||||||
if (!token) {
|
if (!token) {
|
||||||
status("paste your token first");
|
status("paste your token first");
|
||||||
@@ -267,265 +370,142 @@
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var qr = qrcode(0, "L");
|
|
||||||
qr.addData("https://notaquest.owendeed.com/?token=" + token);
|
|
||||||
qr.make();
|
|
||||||
document.getElementById("qr").src = qr.createDataURL();
|
|
||||||
}
|
|
||||||
|
|
||||||
// protobuf helpers
|
// 1. get device_secret from meta api
|
||||||
function varint(n) {
|
status("fetching device secret...");
|
||||||
var r = [];
|
var resp = await fetch("https://api.notaquest.owendeed.com", {
|
||||||
do {
|
method: "POST",
|
||||||
var b = n & 0x7f;
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||||
n >>>= 7;
|
body:
|
||||||
if (n > 0) b |= 0x80;
|
"access_token=" +
|
||||||
r.push(b);
|
encodeURIComponent(token) +
|
||||||
} while (n > 0);
|
"&doc_id=25735612872735053&variables=%7B%7D",
|
||||||
return r;
|
});
|
||||||
}
|
var json = await resp.json();
|
||||||
function varintField(f, v) {
|
var secret = json?.data?.viewer?.user?.device_secret;
|
||||||
return [(f << 3) | 0].concat(varint(v));
|
if (!secret) {
|
||||||
}
|
|
||||||
function bytesField(f, d) {
|
|
||||||
return [(f << 3) | 2].concat(varint(d.length)).concat(Array.from(d));
|
|
||||||
}
|
|
||||||
|
|
||||||
function readVarint(buf, off) {
|
|
||||||
var r = 0,
|
|
||||||
s = 0;
|
|
||||||
while (off < buf.length) {
|
|
||||||
var b = buf[off++];
|
|
||||||
r |= (b & 0x7f) << s;
|
|
||||||
if (!(b & 0x80)) break;
|
|
||||||
s += 7;
|
|
||||||
}
|
|
||||||
return [r, off];
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseProto(buf) {
|
|
||||||
var fields = {},
|
|
||||||
off = 0;
|
|
||||||
while (off < buf.length) {
|
|
||||||
var tag;
|
|
||||||
[tag, off] = readVarint(buf, off);
|
|
||||||
var fn = tag >> 3,
|
|
||||||
wt = tag & 7;
|
|
||||||
if (wt === 0) {
|
|
||||||
var val;
|
|
||||||
[val, off] = readVarint(buf, off);
|
|
||||||
fields[fn] = val;
|
|
||||||
} else if (wt === 2) {
|
|
||||||
var ln;
|
|
||||||
[ln, off] = readVarint(buf, off);
|
|
||||||
fields[fn] = buf.slice(off, off + ln);
|
|
||||||
off += ln;
|
|
||||||
} else break;
|
|
||||||
}
|
|
||||||
return fields;
|
|
||||||
}
|
|
||||||
|
|
||||||
// chunk data with 2-byte framing header
|
|
||||||
function makeChunks(data) {
|
|
||||||
var chunks = [];
|
|
||||||
for (var i = 0; i < data.length; i += 18) {
|
|
||||||
var slice = data.slice(i, i + 18);
|
|
||||||
var seq = Math.floor(i / 18);
|
|
||||||
var last = i + 18 >= data.length;
|
|
||||||
chunks.push(
|
|
||||||
new Uint8Array(
|
|
||||||
[(last ? 0x80 : 0x00) | (seq >> 8), seq & 0xff].concat(
|
|
||||||
Array.from(slice),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return chunks;
|
|
||||||
}
|
|
||||||
|
|
||||||
// send chunked data, then poll-read the response
|
|
||||||
async function sendAndReceive(writeChar, data) {
|
|
||||||
for (var c of makeChunks(Array.from(data))) {
|
|
||||||
await writeChar.writeValueWithoutResponse(c);
|
|
||||||
await new Promise((r) => setTimeout(r, 100));
|
|
||||||
}
|
|
||||||
await new Promise((r) => setTimeout(r, 3000));
|
|
||||||
var response = [];
|
|
||||||
for (var i = 0; i < 200; i++) {
|
|
||||||
await new Promise((r) => setTimeout(r, 100));
|
|
||||||
var val = new Uint8Array((await writeChar.readValue()).buffer);
|
|
||||||
if (val.length <= 1 || val[0] === 0xff) break;
|
|
||||||
response.push(...val.slice(2));
|
|
||||||
if (val[0] & 0x80) break;
|
|
||||||
}
|
|
||||||
return new Uint8Array(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
// HMAC-SHA256 using Web Crypto
|
|
||||||
async function hmacSHA256(key, message) {
|
|
||||||
var k = await crypto.subtle.importKey(
|
|
||||||
"raw",
|
|
||||||
key,
|
|
||||||
{ name: "HMAC", hash: "SHA-256" },
|
|
||||||
false,
|
|
||||||
["sign"],
|
|
||||||
);
|
|
||||||
return new Uint8Array(await crypto.subtle.sign("HMAC", k, message));
|
|
||||||
}
|
|
||||||
|
|
||||||
// encrypt a plaintext request with the shared key (nonce + ciphertext)
|
|
||||||
function encrypt(plaintext, sharedKey) {
|
|
||||||
var nonce = nacl.randomBytes(24);
|
|
||||||
var ciphertext = nacl.box.after(plaintext, nonce, sharedKey);
|
|
||||||
var out = new Uint8Array(24 + ciphertext.length);
|
|
||||||
out.set(nonce);
|
|
||||||
out.set(ciphertext, 24);
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
// decrypt a response (first 24 bytes = nonce, rest = ciphertext)
|
|
||||||
function decrypt(data, sharedKey) {
|
|
||||||
return nacl.box.open.after(
|
|
||||||
data.slice(24),
|
|
||||||
data.slice(0, 24),
|
|
||||||
sharedKey,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function enableADB() {
|
|
||||||
try {
|
|
||||||
var token = document.getElementById("token").value.trim();
|
|
||||||
if (!token) {
|
|
||||||
status("paste your token first");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!token.startsWith("OC") && !token.startsWith("FRL")) {
|
|
||||||
status(
|
|
||||||
"that doesn't look like a meta token. it should start with OC or FRL.",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. get device_secret from meta api
|
|
||||||
status("fetching device secret...");
|
|
||||||
var resp = await fetch("https://api.notaquest.owendeed.com", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
||||||
body:
|
|
||||||
"access_token=" +
|
|
||||||
encodeURIComponent(token) +
|
|
||||||
"&doc_id=25735612872735053&variables=%7B%7D",
|
|
||||||
});
|
|
||||||
var json = await resp.json();
|
|
||||||
var secret = json?.data?.viewer?.user?.device_secret;
|
|
||||||
if (!secret) {
|
|
||||||
status(
|
|
||||||
"couldn't get device secret. is your token valid? sign out of the meta page, sign back in, and grab a new token. reloading might also work.",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var deviceSecret = new Uint8Array(
|
|
||||||
secret.match(/.{2}/g).map((b) => parseInt(b, 16)),
|
|
||||||
);
|
|
||||||
|
|
||||||
// 2. connect bluetooth
|
|
||||||
status("select your quest...");
|
|
||||||
var device = await navigator.bluetooth.requestDevice({
|
|
||||||
acceptAllDevices: true,
|
|
||||||
optionalServices: [SERVICE],
|
|
||||||
});
|
|
||||||
var server = await device.gatt.connect();
|
|
||||||
var service = await server.getPrimaryService(SERVICE);
|
|
||||||
var writeChar = await service.getCharacteristic(WRITE_UUID);
|
|
||||||
|
|
||||||
// 3. wait for user to accept pairing on quest
|
|
||||||
status(
|
status(
|
||||||
"accept the bluetooth pairing on your quest and mobile device if you see it. (just wait 15 seconds if you don't see it)",
|
"couldn't get device secret. is your token valid? sign out of the meta page, sign back in, and grab a new token. reloading might also work.",
|
||||||
);
|
);
|
||||||
await new Promise((r) => setTimeout(r, 15000));
|
return;
|
||||||
|
}
|
||||||
|
var deviceSecret = new Uint8Array(
|
||||||
|
secret.match(/.{2}/g).map((b) => parseInt(b, 16)),
|
||||||
|
);
|
||||||
|
|
||||||
// 4. send HELLO
|
// 2. connect bluetooth
|
||||||
status("connecting to quest...");
|
status("select your quest...");
|
||||||
var keypair = nacl.box.keyPair();
|
var device = await navigator.bluetooth.requestDevice({
|
||||||
var helloBody = bytesField(1, keypair.publicKey).concat(
|
acceptAllDevices: true,
|
||||||
bytesField(2, nacl.randomBytes(16)),
|
optionalServices: [SERVICE],
|
||||||
);
|
});
|
||||||
var hello = new Uint8Array(
|
var server = await device.gatt.connect();
|
||||||
varintField(1, 1).concat(
|
var service = await server.getPrimaryService(SERVICE);
|
||||||
varintField(2, 1),
|
var writeChar = await service.getCharacteristic(WRITE_UUID);
|
||||||
varintField(3, 1),
|
|
||||||
bytesField(4, helloBody),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
var helloResp = await sendAndReceive(writeChar, hello);
|
|
||||||
if (!helloResp.length) {
|
|
||||||
status(
|
|
||||||
"quest didn't respond. it's battery may have died, you didn't accept a confirmation, or it went out of bluetooth range. check each and try again.",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. parse server key and compute shared secret
|
// 3. wait for user to accept pairing on quest
|
||||||
var signedData = parseProto(parseProto(parseProto(helloResp)[3])[1]);
|
status(
|
||||||
var sharedKey = nacl.box.before(signedData[1], keypair.secretKey);
|
"accept the bluetooth pairing on your quest and mobile device if you see it. (just wait 15 seconds if you don't see it)",
|
||||||
|
);
|
||||||
|
await new Promise((r) => setTimeout(r, 15000));
|
||||||
|
|
||||||
// 6. AUTHENTICATE
|
// 4. send HELLO
|
||||||
status("authenticating...");
|
status("connecting to quest...");
|
||||||
var hmac = await hmacSHA256(deviceSecret, signedData[2]);
|
var keypair = nacl.box.keyPair();
|
||||||
var authReq = new Uint8Array(
|
var helloBody = bytesField(1, keypair.publicKey).concat(
|
||||||
varintField(1, 1).concat(
|
bytesField(2, nacl.randomBytes(16)),
|
||||||
varintField(2, 2),
|
);
|
||||||
varintField(3, 2),
|
var hello = new Uint8Array(
|
||||||
bytesField(4, bytesField(1, hmac)),
|
varintField(1, 1).concat(
|
||||||
),
|
varintField(2, 1),
|
||||||
|
varintField(3, 1),
|
||||||
|
bytesField(4, helloBody),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
var helloResp = await sendAndReceive(writeChar, hello);
|
||||||
|
if (!helloResp.length) {
|
||||||
|
status(
|
||||||
|
"quest didn't respond. it's battery may have died, you didn't accept a confirmation, or it went out of bluetooth range. check each and try again.",
|
||||||
);
|
);
|
||||||
var authRespRaw = await sendAndReceive(
|
return;
|
||||||
writeChar,
|
}
|
||||||
encrypt(authReq, sharedKey),
|
|
||||||
);
|
|
||||||
if (!authRespRaw.length) {
|
|
||||||
status(
|
|
||||||
"no auth response, did your headset go out of bluetooth range or die? check each and try again.",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var authResp = parseProto(decrypt(authRespRaw, sharedKey));
|
|
||||||
if (authResp[2] !== 0) {
|
|
||||||
status("authentication failed, try again? this shouldn't happen.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7. DEV_MODE_SET
|
// 5. parse server key and compute shared secret
|
||||||
status("enabling adb...");
|
var signedData = parseProto(parseProto(parseProto(helloResp)[3])[1]);
|
||||||
var devReq = new Uint8Array(
|
var sharedKey = nacl.box.before(signedData[1], keypair.secretKey);
|
||||||
varintField(1, 1).concat(
|
|
||||||
varintField(2, 6001),
|
// 6. AUTHENTICATE
|
||||||
varintField(3, 3),
|
status("authenticating...");
|
||||||
bytesField(4, varintField(1, 1)),
|
var hmac = await hmacSHA256(deviceSecret, signedData[2]);
|
||||||
),
|
var authReq = new Uint8Array(
|
||||||
|
varintField(1, 1).concat(
|
||||||
|
varintField(2, 2),
|
||||||
|
varintField(3, 2),
|
||||||
|
bytesField(4, bytesField(1, hmac)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
var authRespRaw = await sendAndReceive(
|
||||||
|
writeChar,
|
||||||
|
encrypt(authReq, sharedKey),
|
||||||
|
);
|
||||||
|
if (!authRespRaw.length) {
|
||||||
|
status(
|
||||||
|
"no auth response, did your headset go out of bluetooth range or die? check each and try again.",
|
||||||
);
|
);
|
||||||
var devRespRaw = await sendAndReceive(
|
return;
|
||||||
writeChar,
|
}
|
||||||
encrypt(devReq, sharedKey),
|
var authResp = parseProto(decrypt(authRespRaw, sharedKey));
|
||||||
);
|
if (authResp[2] !== 0) {
|
||||||
if (devRespRaw.length) {
|
status("authentication failed, try again? this shouldn't happen.");
|
||||||
var devResp = parseProto(decrypt(devRespRaw, sharedKey));
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. DEV_MODE_SET
|
||||||
|
status("enabling adb...");
|
||||||
|
var devReq = new Uint8Array(
|
||||||
|
varintField(1, 1).concat(
|
||||||
|
varintField(2, 6001),
|
||||||
|
varintField(3, 3),
|
||||||
|
bytesField(4, varintField(1, 1)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
var devRespRaw = await sendAndReceive(
|
||||||
|
writeChar,
|
||||||
|
encrypt(devReq, sharedKey),
|
||||||
|
);
|
||||||
|
if (devRespRaw.length) {
|
||||||
|
var decrypted = decrypt(devRespRaw, sharedKey);
|
||||||
|
if (decrypted) {
|
||||||
|
var devResp = parseProto(decrypted);
|
||||||
if (devResp[2] === 0) {
|
if (devResp[2] === 0) {
|
||||||
status(
|
status("done! adb should be enabled. restarting your headset may be required for it to work.");
|
||||||
"done! adb should be enabled. restarting your headset may be required for it to work.",
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (devResp[3]) {
|
||||||
|
var errMsg = new TextDecoder().decode(devResp[3]);
|
||||||
|
if (errMsg.toLowerCase().includes("internet")) {
|
||||||
|
status("your quest isn't connected to wifi. connect it to wifi and try again.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
status("quest rejected the command in a way i don't understand. do basic debugging, reboot, new token, all the normal stuff. if not working still, send a github issue with this data (it shouldn't have private info): " + errMsg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
status("quest responded but said something i don't understand. this is not an issue i have encountered yet. do basic debugging, reboot, new token, all the normal stuff. if not working still, send a github issue with this data (it shouldn't have private info): response fields: " + JSON.stringify(Object.fromEntries(Object.entries(devResp).map(([k, v]) => [k, v instanceof Uint8Array ? Array.from(v).map(b => b.toString(16)).join('') : v]))));
|
||||||
|
} else {
|
||||||
|
status("quest responded but decryption failed. something went very wrong, try a new token, if that doesn't work, make a github issue. raw length: " + devRespRaw.length);
|
||||||
}
|
}
|
||||||
status("sent! check if adb is enabled.");
|
} else {
|
||||||
} catch (e) {
|
status("no response from quest for dev mode command. BLE connection may have dropped, or the Quest refused something silently.");
|
||||||
status(
|
|
||||||
"error: " +
|
|
||||||
e.message +
|
|
||||||
", this error isn't something i have handling for, report an issue on the link at the bottom of the page with this message.",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
status(
|
||||||
|
"error: " +
|
||||||
|
e.message +
|
||||||
|
", this error isn't something i have handling for, report an issue on the link at the bottom of the page with this message.",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
</script>
|
}
|
||||||
</body>
|
</script>
|
||||||
</html>
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user