Accessing API using Python requests

Forum for questions and support relating to the 1.30.x releases only.
Locked
rshuhart
Posts: 2
Joined: Sun Jun 17, 2018 3:50 pm

Accessing API using Python requests

Post by rshuhart »

Hi everyone,

I'm trying to use python to access the API {"version":"1.30.4","apiversion":"1.0"}. I'm able to successfully use curl from the examples on https://zoneminder.readthedocs.io/en/stable/api.html. However, I can't seem to get past logging in using Python 3.6 with request module.

This works:

Code: Select all

# Python 3.6
import requests    # version 2.18.4
s = requests.Session()

header = {"username":"admin",
          "password":"XXXX",
          "action":"login",
          "view":"console"}

s.get("http://localhost/zm/index.php", headers=header)
s.cookies output is:
<RequestsCookieJar[Cookie(version=0, name='ZMSESSID', value='dv9l1p4fdh4ndhdpjeuarxxxxx', port=None, port_specified=False, domain='localhost.local', domain_specified=False, domain_initial_dot=False, path='/', path_specified=True, secure=False, expires=None, discard=True, comment=None, comment_url=None, rest={'HttpOnly': None}, rfc2109=False), Cookie(version=0, name='zmCSS', value='flat', port=None, port_specified=False, domain='localhost.local', domain_specified=False, domain_initial_dot=False, path='/zm', path_specified=False, secure=False, expires=1840288943, discard=False, comment=None, comment_url=None, rest={}, rfc2109=False), Cookie(version=0, name='zmSkin', value='classic', port=None, port_specified=False, domain='localhost.local', domain_specified=False, domain_initial_dot=False, path='/zm', path_specified=False, secure=False, expires=1840288943, discard=False, comment=None, comment_url=None, rest={}, rfc2109=False)]>
The object s should be maintaining the cookie, but, unfortunately, I'm not successful from here.

Trying to get the monitor list returns unauthorized response.

Code: Select all

s.get("http://localhost/zm/api/monitors.json")
<Response [401]>
There must be some difference between how curl provides the cookie and how python requests provides it. I'm hoping someone else has figured this out already. I've use APIs with api keys, but have never had to maintain cookies, so this is new for me and just not sure what to do to get this to work.

Thanks,

Ryan
User avatar
snake
Posts: 337
Joined: Sat May 21, 2016 2:20 am

Re: Accessing API using Python requests

Post by snake »

rshuhart wrote: Sun Jun 17, 2018 4:25 pm There must be some difference between how curl provides the cookie and how python requests provides it. I'm hoping someone else has figured this out already. I've use APIs with api keys, but have never had to maintain cookies, so this is new for me and just not sure what to do to get this to work.
I have dealt with this although I used arduino devices not python. The problem solving was similar. First off, the cookies management may not be required in ZM > 1.30.4 as newer releases may have longer lasting API keys that you can create from web portal. There was a thread about it a month or two back. Still under development, however.

So if you want to troubleshoot this with python, I would recommend you become familiar with tcpdump or wireshark. Here's a command I use in tcpdump to get the full output of the packets.

Code: Select all

tcpdump -ni eth0 -A -s 1500 &> outputlog 
Basically, you will want to get your request to look like curl's. Compare the packet capture from both and edit as needed. Here's some more notes from my arduino sketch that you might find helpful...

Code: Select all

/*
 *
 * Basic API Request. See notes in main loop below for more details.
 * This passes the host (zm server ip), the cookie, and the API request to Zoneminder
 *
 */

void API_Request(String api_req){
     client.print("GET ");
     client.print(api_req);
     client.println(" HTTP/1.1");
     client.print("Host: ");
     client.println(host);
     client.println("Accept: */*");
     client.print("Cookie: zmCSS=classic; zmSkin=classic; ");
     client.println(cookie);
     // ask for a range back. We don't care whats in the response.
     // and we need to quickly close this connection so in case of
     // a connection fail, we can power off/ try again without error.
     // small micros don't have sram to parse the response fast enough
     client.println("Range: bytes=0-10");
     client.println();
}

Code: Select all

//proper usage of post request for zm. (See ZM Documentation on API and curl)
//POST /zm/index.php HTTP/1.1
//User-Agent: curl/7.38.0
//Host: <zmserverip>
//Accept: */*
//Content-Length: 59
//Content-Type: application/x-www-form-urlencoded
//
//username=user&password=pass&action=login&view=console
//
//NOTE: this must be exactly the same, except for user agent (that is optional). Nothing else can be omitted.
//When in doubt, test with curl, and make sure you get the page to load correctly. Not just
//a 200 response, it also needs to 'login' and load the homepage.
//Also helpful to review both tcpdumps (tcpdump -ni eth0 -A -s 1500 > log)     of a working and non working req
//contrast and compare carefully
     lcd.clear();
      delay(10);
      lcd.print("Cnnected");

      Serial.println("connected");

      client.println("POST /zm/index.php HTTP/1.1"); //required
      client.print  ("Host: "); //required
      client.println(host);   //required
      client.println("Accept: */*"); //i think required
      client.println("Content-Length: 59"); //required
      //client.println("Range: bytes=0-100"); //doesn't work here
      client.println("Content-Type: application/x-www-form-urlencoded");  //required
      client.println();  //required
      client.print("username=");
      client.print(username);
      client.print("&password=");
      client.print(password);
      client.println("&action=login&view=console");

      client.println();  //required

      waitingforcookie = 1;
    }

Code: Select all

  //the Set-Cookie we need is the second ZMSESSID in the return packet from ZM
    //first one, is not relevant.

    //example of average return packet from ZM:
    /*
     *
     *
HTTP/1.1 200 OK
Date: Fri, 29 Dec 2017 02:59:02 GMT
Server: Apache (GNU/Linux)
X-Powered-By: PHP
Set-Cookie: ZMSESSID=7a82go0o4qr8b6v03r690fs085; path=/; HttpOnly
Expires: Mon, 26 Jul 1997 05:00:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Set-Cookie: zmSkin=classic; expires=Sun, 07-Nov-2027 02:59:02 GMT; Max-Age=311040000
Set-Cookie: zmCSS=classic; expires=Sun, 07-Nov-2027 02:59:02 GMT; Max-Age=311040000
Set-Cookie: ZMSESSID=lklkslkslklkslklksslk; path=/; HttpOnly
Last-Modified: Fri, 29 Dec 2017 02:59:02 GMT
Cache-Control: post-check=0, pre-check=0
Vary: Accept-Encoding
     *
     *
     */

Code: Select all

 /*
     * Packet to Send to Zoneminder API:
     * e.g.
     *
     * GET /zm/api/states.json HTTP/1.1
     * User-Agent: curl/7.35.0
     * Host: 127.0.0.1
     * Accept: * / *
     * Cookie: zmCSS=classic; zmSkin=classic; ZMSESSID=<something>
     *
     * note that accept needs the space between asterisks and slash removed.
     *
     */

jantman
Posts: 15
Joined: Tue May 08, 2018 11:21 am
Location: Atlanta, GA, USA
Contact:

Re: Accessing API using Python requests

Post by jantman »

Ryan,

The ZM API docs say that you should use a curl like like:

Code: Select all

curl -d "username=XXXX&password=YYYY&action=login&view=console" -c cookies.txt  http://yourzmip/zm/index.php
to access the API.

Data and headers are NOT the same thing. Your requests call is sending four HTTP Headers called "username", "password", "action" and "view".

Please try the following:

Code: Select all

import requests
s = requests.Session()

data = {
    "username":"admin",
    "password":"XXXX",
    "action":"login",
    "view":"console"
}

s.post("http://localhost/zm/index.php", data=data)
This makes two main changes:
  1. It appears, from the examples, that this request should be a POST not a GET.
  2. The username/password/action/view is supposed to be sent as form-encoded data (curl -d / requests "data", not HTTP headers (curl -H / requests "headers")
-Jason

PS - Disclaimer - I've only been using ZM for a week or two, and I don't have auth enabled. The above may need some minor changes, but the major issue is the difference between HTTP headers and form/post data.
Principal Software Engineer - Some Company
User avatar
asker
Posts: 1553
Joined: Sun Mar 01, 2015 12:12 pm

Re: Accessing API using Python requests

Post by asker »

Yup, that is exactly right.
jantman wrote: Tue Jun 19, 2018 10:24 am Data and headers are NOT the same thing. Your requests call is sending four HTTP Headers called "username", "password", "action" and "view".
I no longer work on zmNinja, zmeventnotification, pyzm or mlapi. I may respond on occasion based on my available time/interest.

Please read before posting:
How to set up logging properly
How to troubleshoot and report - ES
How to troubleshoot and report - zmNinja
ES docs
zmNinja docs
rshuhart
Posts: 2
Joined: Sun Jun 17, 2018 3:50 pm

Re: Accessing API using Python requests

Post by rshuhart »

Thanks everyone for the pointers! I'm hoping to find some time this weekend to take another crack at this. I'll come back to share what worked or for more help.

Ryan
Locked