paint-brush
How I Successfully "Reverse-Engineered" ChatGPT to Create an Unofficial API Wrapperby@akdev
14,705 reads
14,705 reads

How I Successfully "Reverse-Engineered" ChatGPT to Create an Unofficial API Wrapper

by AK DEVDecember 19th, 2022
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

How I successfully reverse-engineered ChatGPT to create an unofficial api wrapper and my efforts became useless after 3 hours.
featured image - How I Successfully "Reverse-Engineered" ChatGPT to Create an Unofficial API Wrapper
AK DEV HackerNoon profile picture

And my efforts became useless after 3 hours.


Hi everyone, I want to tell you a story that happened just a few days ago.


As you know, ChatGPT stirred up the whole world community, and not just from the IT sector. Experts and scientists from around the world are testing how well this neural network answers the given questions, Stackoverflow urgently imposes the restrictions on publishing the answers using ChatGPT, in specialized forums, there is a new discussion about how soon the work of programmers will be automated.


I first of all was interested in whether it will be possible to fully automate the process of authorization, sending requests, and receiving responses in ChatGPT, so that I could use the resulting module on the server for some experiments and just to spoil the fun.


I should note that I started studying ChatGPT quite late, about 7 December. When I looked at GitHub, I saw that there were already a number of projects there, but they were all made with browsers (like Selenium, Playwright and other testing tools). That didn't seem sporty enough to me, I wanted to get a solution purely on simple http requests, so I got to work.


Let's go!


Toolkit

  • Chrome Dev Tools
  • Wireshark
  • Python (httpx, bs4)


Chapter 1: Authorization

Usually, when it comes to automating access to some services, going through authorization is a lot of headache. In contrast, during the ChatGPT testing I saw only a simple captcha, and it appears very rarely. Sometimes I also saw a message about temporary account blocking and the need to change the password, but it turned out to be related to the password itself: for the test account I used an extremely simple password, which had long been spotted in various data leaks. After changing it to a more complex one, the problem went away.


Authorization in ChatGPT consists of several steps:


  1. Getting CSRF key with simple GET request:

req = session.get("https://chat.openai.com/api/auth/csrf")

csrf = req.json().get("csrfToken")


  1. Getting the authorization link with a simple POST request:

params = {

    "callbackUrl": "/",

    "csrfToken": csrf,

    "json": "true"

}

req = session.post(

    "https://chat.openai.com/api/auth/signin/auth0?prompt=login", data=params

)

login_link = req.json().get("url")


  1. Opening the login page - there may be a captcha in .svg:

req = session.get(login_link)

response = BeautifulSoup(req.text, features="html.parser")

captcha = resp.find("img", {"alt": "captcha"})

if captcha:

    src = captcha.get("src").split(",")[1]

    with open("captcha.svg", "wb") as fh:

        fh.write(base64.urlsafe_b64decode(src))


  1. Get unique state parameter from page code:

response.find("input", {"name": "state", "type": "hidden"}).get("value")


  1. Sending an email along with an optional captcha code to the next page:

params = {

    "state": "<state>",

    "username": "<email>",

    "captcha": "<captcha_code or None>",

    "js-available": "true",

    "webauthn-available": "true",

    "is-brave": "false",

    "webauthn-platform-available": "false",

    "action": "default"

}

url = f"https://auth0.openai.com/u/login/identifier?state={state}"

req = session.post(url, data=params)


  1. Getting the state parameter again (step 4)


  2. Sending an email and password:

params = {

    "state": "<state>",

    "username": "<email>",

    "password": "<password>",

    "action": "default"

}

url = f"https://auth0.openai.com/u/login/password?state={state}"

req = session.post(url, data=params)


And that's it! Of course, there were more nuances, for example with headers (you need to repeat Content-type and other headers exactly). And of course, you need to initialize httpx.Client with http2 protocol support, carefully storing all session cookies. But all in all, there were no problems.


My favorite Chrome Dev Tools is the best tool for viewing and then replaying requests, I thought. But that turned out not to be quite the case.


Chapter 2: Sending a Message

Activate the Fetch/XHR tab in the Chrome Network pane again and start sending the test message.


Requests to ChatGPT


We see that two requests are sent when sending a message: to moderations and to endpoints.


With moderations everything is clear and transparent: we send typed text, specify ChatGPT model, in response we getmoderation_id (whatever that means).


moderations payload


moderations response


But what about the conversation?


In payload we see that the text is not sent, only the IDs, and 2 different!


conversation payload


And instead of the Preview/Response tabs, we are greeted by an EventStream, which I have not encountered before. Moreover, the EventStream tab is empty!


conversation EventStream


I started a long search of where these message id's are coming from. I checked the code of pages, headers, everything I could. And... nothing!


Hypothesis 1: they are generated on the client side

Go to the Sources tab and put a breakpoint on any XHR event:


trying to catch the source of those IDs


We send the test message again and start the hours-long dive into the maze of minified (and obfuscated?) code, placing more and more breakpoints.


it was long and long night


In the process of this dive, I was unable to find where these id's are generated. I'm not very good at JS debugging, so I moved on to the next hypothesis.


Hypothesis 2: not all network activity is logged in the Network tab

Pretty obvious, isn't it? I started with Chrome Network Log, which I hadn't encountered before. It is located at: chrome://net-export/


Chrome capturing network logs settings


Click Start Logging to Disk, go back to the ChatGPT tab, send another test message. Go back to Network Log, stop logging, and go to Netlog Viewer.

What do we see there?


The dump


We see the sending of some IDs again!


At this point, I was ready to give up and leave everything. But I decided to try Wireshark. The last time I ran it was so long ago that SSL certificates were not yet ubiquitous and the traffic was plain to see.

That is why I had to go through another quest - to set the environment variable for decoding TLS packets in Wireshark.


I will not describe everything in detail, but in the end this approach did not work either. Apparently, I don't know how to cook Wireshark.


Hypothesis 3: Leeeeeeeeeeeeeeeroy!

Let's try to simply send requests!


First, we send a random UUID4 instead of all identifiers. Of course, that didn't work.


Then I just tried sending the same conversation_id, but with a different message text as I saw in the browser logs. And it worked! After trying other combinations, I found out which parameters were required, which could be random.


The final code for sending the message:


session.headers.update({

    "accept": "text/event-stream",

    "X-OpenAI-Assistant-App-Id": "",

    "Authorization": f"Bearer {token}"

})

params = {

    "action": "next",

    "messages": [

        {

            "id": str(uuid.uuid4()),  # <-- random!

            "role": "user",

            "content": {

                "content_type": "text",

                "parts": ["<message>"]

            }

        }

    ],

    "parent_message_id": "",  # <-- empty!

    "model": "<chatgpt_model>"

}

with session.stream(

    "POST", "https://chat.openai.com/backend-api/conversation",

    json=params, headers={"content-type": "application/json"}

) as response:

    for chunk in response.iter_bytes():

        print(chunk)


Chapter 3: Sad Ending

I was insanely happy that I was able to complete this quest. I tidied up the code a bit, uploaded this proof-of-concept to GitHub, and went to bed, planning to finish it the next day. The plans were quite simple:

  1. Automatic captcha recognition.

  2. Maintaining dialogs

  3. Formalizing code as a package


and much more.


The next morning I open my computer, run the code, and... nothing works! It's only been a few hours, and in that time ChatGPT has changed the captcha to Google Recaptcha, and, most critically, enabled Cloudflare protection. It requires browser emulation, JS execution, and many other things to work around, which absolutely ruined the original idea of doing with simple http requests.



Anyway, I had an amazing time and had a great experience!