HTTPChannel basic authentication

I need to issue HTTPS GETs from within my Panda3d application to a site that uses basic authentication. This is my first time using HTTPSClient and HTTPSChannel. I haven’t found how to get them to successfully handle basic authentication. When I run the following code, I’m expecting the returned data to be the requested document. Instead, I receive a login page.

The login page response indicates that the server did not recognize an acceptable Authorization header such as “Authorization: Basic userid:password”. It has returned a login page for me to interactively login.

The site returns the expected document when I access it using the python requests HTTPS library and the Postman application, but not when using HTTPClient+HTTPChannel. What am I missing in setting up basic authentication?

server = 'ncs01.case.edu'
url = 'https://' + server + '/webacs/api/v4/data/AccessPointDetails.json?.full=true&.maxResults=1000&.nocount=true'
client = HTTPClient()
client.add_direct_host(server)
client.set_verify_ssl(HTTPClient.VS_no_verify)
user, password = mylib.credentials(server, None)  # get my default credentials for server
user_password = HTTPClient.base64_encode(f"{user}:{password}")
client.set_username(server, '', user_password)
chan: HTTPChannel = client.make_channel(False)
chan.beginConnectTo(url)   # non-blocking direct connection
while chan.run():
    time.sleep(0.1)
print(f"is_connection_ready={chan.is_connection_ready()}")
chan.beginGetDocument(url)  # sends non-blocking GET?
while chan.run():  # turns False with operation is complete
    time.sleep(0.1)
if not chan.is_valid():
    raise ValueError(f"invalid url")
status = chan.get_status_code()
if status == 200:  # success
    stream: 'ISocketStream' = chan.open_read_body()
    data = bytes()
    reader = StreamReader(stream, True)
    while True:
        s = reader.extract_bytes(100000)
        print(f"extracted {len(s)} bytes")
        if len(s) == 0:  # stream EOF?
            if stream.is_closed():  # document end?
                print(f"stream is_closed")  # Yes
                break  # end of data
            else:  # No. More to come
                time.sleep(0.1)
        else:  # has data
            data += s  # aggregate segment into data
    # Received whole document
    print(f"Received {len(data)} bytes")
    ss = str(data, 'UTF-8')
    print(ss)
else:
    print(f"get_status_code={status}")

Just specify “user:password”, Panda will do the base64 step, I think it’s currently being doubly encoded.

I also suggest leaving the server argument blank to be sure that Panda will pick the chosen username for this request.

Note that 100 ms is a long time to sleep while handling the request. If you want to do it synchronously, you can just use getDocument instead of beginGetDocument.

Thanks. 100 msec is just for this test. In the Panda application, I’ll test the .run() once per frame in a task.

I changed the client.set_username line to

client.set_username('', '', f"{user}:{password}")

However, the server still returns the login page.

Could it be that the server is not returning a 401 error with the WWW-Authenticate header? I think HTTPChannel expects that to be sent in order to retry with authentication.

You can always set the header yourself using channel.send_extra_header.

I inserted the send_extra_header as below. Same results…

user, password = mylib.credentials(server, None)  # get my default credentials for server
client.set_username('', '', f"{user}:{password}")
chan: HTTPChannel = client.make_channel(False)
chan.send_extra_header('Authorization', f"Basic {user}:{password}")
chan.beginConnectTo(url)   # non-blocking direct connection

In another trial, instead of putting the send_extra_header just prior to the beginConnect(url), I tried putting the send_extra_header just prior to the beginGetDocument. This causes chan.is_valid() to return false.

server = 'ncs01.case.edu'
url = 'https://' + server + '/webacs/api/v4/data/AccessPointDetails.json?.full=true&.maxResults=1000&.nocount=true'
client = HTTPClient()
client.add_direct_host(server)
client.set_verify_ssl(HTTPClient.VS_no_verify)
user, password = mylib.credentials(server, None)  # get my default credentials for server
client.set_username('', '', f"{user}:{password}")
chan: HTTPChannel = client.make_channel(False)
chan.beginConnectTo(url)   # non-blocking direct connection
while chan.run():
    time.sleep(0.1)
print(f"is_connection_ready={chan.is_connection_ready()}")
chan.send_extra_header('Authorization', f"Basic {user}:{password}")
chan.beginGetDocument(url)  # sends non-blocking GET?
while chan.run():  # turns False with operation is complete
    time.sleep(0.1)
if not chan.is_valid():
    raise ValueError(f"invalid url")
...

If you pass it as custom header then you should base64 it, since Panda isn’t handling that for you then. It should be done before begin_get_document.

I’m just shooting in the dark for what send_extra_header needs given its (key, value) signature. I tried the three versions below, with no success.

...
chan: HTTPChannel = client.make_channel(False)
chan.beginConnectTo(url)   # non-blocking direct connection
while chan.run():
    time.sleep(0.1)
print(f"is_connection_ready={chan.is_connection_ready()}")
# chan.send_extra_header('Authorization', HTTPClient.base64_encode(f"Basic {user}:{password}"))  # does nothing
# chan.send_extra_header('Authorization', HTTPClient.base64_encode(f"{user}:{password}"))  # does nothing
chan.send_extra_header(HTTPClient.base64_encode('Authorization'),
                       HTTPClient.base64_encode(f"Basic {user}:{password}"))  # --> invalid URL
chan.beginGetDocument(url)  # sends non-blocking GET?
while chan.run():  # turns False with operation is complete
    time.sleep(0.1)
if not chan.is_valid():
    raise ValueError(f"invalid url")
...

This is what you would need:

chan.send_extra_header('Authorization', "Basic " + HTTPClient.base64_encode(f"{user}:{password}")) 

It works! thanks!

1 Like