How to programmatically control TP-Link WiFi router using Python requests

Once, I faced the task of implementing software control of one of the common home Wi-Fi routers TP-Link TL-WR841N, which, unfortunately, does not have a command line control interface (telnet, SSH). I wanted my Telegram bot, implemented in Python based on SBC in the local home network, based on my commands to perform the following router management functions:

  • Reboot Router
  • Opening / Closing NAT Port Forwarding to Internal Web Services
  • Opening / closing remote access to the router from the WAN (Internet)
  • Identification of devices registered in the local WiFi network of the router

Of course, the user can do all this manually himself using the WEB interface, but, firstly, I wanted to automate these functions, and secondly, sometimes I needed to do this remotely, and I did not want to constantly keep open access to the router management via Unencrypted HTTP over the Internet for security reasons. Management using the "closed" Telegram bot seemed to me more reliable.

To access the management of the router, I used the only available interface - the WEB user interface, which I implemented interaction with HTTP requests Python requests. In order to determine which HTTP GET requests should be routed to the router, I used the well-known Wireshark traffic sniffer. Simply put, with Python requests, I reproduced the requests I saw in Wireshark in the required sequence.

Import, authorization and initial settings


So, first of all, we import the requests library into Python, on the basis of which we will implement HTTP requests.

import requests

As the initial parameters, we indicate the internal IP address of the router as an HTTP request string. In my case, it is 192.168.0.1.

router_ip='http://192.168.0.1'

For authorization, we need a token, the calculation of which is based on the username and password of the user. The token calculation function is defined in the JS script on the router 192.168.0.1/login/encrypt.js . It looks like this.

encrypt.js
function hex_md5(s)
{ 
	return binl2hex(core_md5(str2binl(s), s.length * 8));
}
function core_md5(x, len)
{
  /* append padding */
  x[len >> 5] |= 0x80 << ((len) % 32);
  x[(((len + 64) >>> 9) << 4) + 14] = len;
  var a =  1732584193;
  var b = -271733879;
  var c = -1732584194;
  var d =  271733878;
  for(var i = 0; i < x.length; i += 16)
  {
    var olda = a;
    var oldb = b;
    var oldc = c;
    var oldd = d;
    a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936);
    d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586);
    c = md5_ff(c, d, a, b, x[i+ 2], 17,  606105819);
    b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330);
    a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897);
    d = md5_ff(d, a, b, c, x[i+ 5], 12,  1200080426);
    c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341);
    b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983);
    a = md5_ff(a, b, c, d, x[i+ 8], 7 ,  1770035416);
    d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417);
    c = md5_ff(c, d, a, b, x[i+10], 17, -42063);
    b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162);
    a = md5_ff(a, b, c, d, x[i+12], 7 ,  1804603682);
    d = md5_ff(d, a, b, c, x[i+13], 12, -40341101);
    c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290);
    b = md5_ff(b, c, d, a, x[i+15], 22,  1236535329);
    a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510);
    d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632);
    c = md5_gg(c, d, a, b, x[i+11], 14,  643717713);
    b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302);
    a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691);
    d = md5_gg(d, a, b, c, x[i+10], 9 ,  38016083);
    c = md5_gg(c, d, a, b, x[i+15], 14, -660478335);
    b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848);
    a = md5_gg(a, b, c, d, x[i+ 9], 5 ,  568446438);
    d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690);
    c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961);
    b = md5_gg(b, c, d, a, x[i+ 8], 20,  1163531501);
    a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467);
    d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784);
    c = md5_gg(c, d, a, b, x[i+ 7], 14,  1735328473);
    b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734);
    a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558);
    d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463);
    c = md5_hh(c, d, a, b, x[i+11], 16,  1839030562);
    b = md5_hh(b, c, d, a, x[i+14], 23, -35309556);
    a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060);
    d = md5_hh(d, a, b, c, x[i+ 4], 11,  1272893353);
    c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632);
    b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640);
    a = md5_hh(a, b, c, d, x[i+13], 4 ,  681279174);
    d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222);
    c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979);
    b = md5_hh(b, c, d, a, x[i+ 6], 23,  76029189);
    a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487);
    d = md5_hh(d, a, b, c, x[i+12], 11, -421815835);
    c = md5_hh(c, d, a, b, x[i+15], 16,  530742520);
    b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651);
    a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844);
    d = md5_ii(d, a, b, c, x[i+ 7], 10,  1126891415);
    c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905);
    b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055);
    a = md5_ii(a, b, c, d, x[i+12], 6 ,  1700485571);
    d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606);
    c = md5_ii(c, d, a, b, x[i+10], 15, -1051523);
    b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799);
    a = md5_ii(a, b, c, d, x[i+ 8], 6 ,  1873313359);
    d = md5_ii(d, a, b, c, x[i+15], 10, -30611744);
    c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380);
    b = md5_ii(b, c, d, a, x[i+13], 21,  1309151649);
    a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070);
    d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379);
    c = md5_ii(c, d, a, b, x[i+ 2], 15,  718787259);
    b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551);
    a = safe_add(a, olda);
    b = safe_add(b, oldb);
    c = safe_add(c, oldc);
    d = safe_add(d, oldd);
  }
  return Array(a, b, c, d);
}
function md5_cmn(q, a, b, x, s, t)
{
  return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b);
}
function md5_ff(a, b, c, d, x, s, t)
{
  return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);
}
function md5_gg(a, b, c, d, x, s, t)
{
  return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);
}
function md5_hh(a, b, c, d, x, s, t)
{
  return md5_cmn(b ^ c ^ d, a, b, x, s, t);
}
function md5_ii(a, b, c, d, x, s, t)
{
  return md5_cmn(c ^ (b | (~d)), a, b, x, s, t);
}
/*
 * Add integers, wrapping at 2^32. This uses 16-bit operations internally
 * to work around bugs in some JS interpreters.
 */
function safe_add(x, y)
{
  var lsw = (x & 0xFFFF) + (y & 0xFFFF);
  var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
  return (msw << 16) | (lsw & 0xFFFF);
}
function bit_rol(num, cnt)
{
  return (num << cnt) | (num >>> (32 - cnt));
}
function str2binl(str)
{
  var bin = Array();
  var mask = (1 << 8) - 1;
  for(var i = 0; i < str.length * 8; i += 8)
    bin[i>>5] |= (str.charCodeAt(i / 8) & mask) << (i%32);
  return bin;
}
function binl2hex(binarray)
{
  var hex_tab = "0123456789abcdef";
  var str = "";
  for(var i = 0; i < binarray.length * 4; i++)
  {
    str += hex_tab.charAt((binarray[i>>2] >> ((i%4)*8+4)) & 0xF) +
           hex_tab.charAt((binarray[i>>2] >> ((i%4)*8  )) & 0xF);
  }
  return str;
}
function Base64Encoding(input) 
{
	var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
	var output = "";
	var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
	var i = 0;
	//input = utf8_encode(input);
	while (i < input.length) 
	{
		chr1 = input.charCodeAt(i++);
		chr2 = input.charCodeAt(i++);
		chr3 = input.charCodeAt(i++);
		enc1 = chr1 >> 2;
		enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
		enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
		enc4 = chr3 & 63;
		if (isNaN(chr2)) {
			enc3 = enc4 = 64;
		} else if (isNaN(chr3)) {
			enc4 = 64;
		}
		output = output +
		keyStr.charAt(enc1) + keyStr.charAt(enc2) +
		keyStr.charAt(enc3) + keyStr.charAt(enc4);
	}
	return output;
}
function utf8_encode (string) 
{
	string = string.replace(/\r\n/g,"\n");
	var utftext = "";
	for (var n = 0; n < string.length; n++) {
		var c = string.charCodeAt(n);
		if (c < 128) {
			utftext += String.fromCharCode(c);
		}
		else if((c > 127) && (c < 2048)) {
			utftext += String.fromCharCode((c >> 6) | 192);
			utftext += String.fromCharCode((c & 63) | 128);
		}
		else {
			utftext += String.fromCharCode((c >> 12) | 224);
			utftext += String.fromCharCode(((c >> 6) & 63) | 128);
			utftext += String.fromCharCode((c & 63) | 128);
		}
	}
	return utftext;
}


But, I confess, I did not port this function from JS to Python. I simplified the approach and looked at the parameter value that my browser sends to Wireshark.



A highlighted request is an authorization request, which we will reproduce a little later. So I copied the cookie pair parameter in the authorization request

auth_token='Authorization=Basic%20YWRtaW46YjAxYzZmYzYyMDgwMzA5Y2ZiMzc2ZTE4NzI3YzMwNzk%3D'

Perhaps this is not the most elegant, but simple approach. In addition, we will not store the login and password from the router in plain text, which, in my opinion, is not so bad.

Let's move on to the authorization function.

def login():
    r = requests.get(router_ip+'/userRpm/LoginRpm.htm?Save=Save',headers={'Referer':router_ip+'/','Cookie': auth_token})
    if r.status_code==200:
        x=1
        while x<3:
            try:
                session_id=r.text[r.text.index(router_ip)+len(router_ip)+1:r.text.index('userRpm')-1]
                return session_id
                break
            except ValueError:
                return 'Login error'
            x+=1
    else:
        return 'IP unreachable'

I used the while loop, since the router did not always authorize my bot the first time. Perhaps you can do without it.

In case of successful authorization, the function returns session_id. This is the session identifier that the router generates during authorization. After authorization, session_id must be present in all subsequent HTTP requests to the router.

Next, we implement the logout exit function.

def logout(session_id):
    r = requests.get(router_ip+'/'+session_id+'/userRpm/LogoutRpm.htm',headers={'Referer':router_ip+'/'+session_id+'/userRpm/MenuRpm.htm','Cookie': auth_token})  
    if r.status_code==200:
        return 'Loging out: '+str(r.status_code)
    else:
        return 'Unable to logout''

I do a logout after each operation, since the router allows only one user to connect at a time. Therefore, if your bot logs in and does not exit the router, then you will not be able to log into it until the router closes the open session by timeout after a few minutes. Thus, I decided to strictly adhere to the sequence "login -> operation -> logout".

By the way, it is worth considering that if someone is already logged in to the router, then the bot will obviously not be able to log in until the user logs in or the active session closes by timeout. In a word, “the one who first got up is slippers.” It is worth noting that the Python bot performs router control operations in a fraction of a second. Thus, your router will not be occupied by the bot for a long time.

Reboot, NAP Port Forwarding


Let's move on to the operations that we can perform after successful authorization.

#Перезагрузка
r = requests.get(router_ip+'/'+session+'/userRpm/SysRebootRpm.htm?Reboot=%D0%9F%D0%B5%D1%80%D0%B5%D0%B7%D0%B0%D0%B3%D1%80%D1%83%D0%B7%D0%B8%D1%82%D1%8C',headers={'Referer':router_ip+'/'+session+'/userRpm/SysRebootRpm.htm','Cookie': auth_token})
#Открыть Port Forwarding
r = requests.get(router_ip+'/'+session+'/userRpm/VirtualServerRpm.htm?doAll=EnAll&Page=1',headers={'Referer':router_ip+'/'+session+'/userRpm/VirtualServerRpm.htm','Cookie': auth_token})
#Закрыть Port Forwarding
r = requests.get(router_ip+'/'+session+'/userRpm/VirtualServerRpm.htm?doAll=DisAll&Page=1',headers={'Referer':router_ip+'/'+session+'/userRpm/VirtualServerRpm.htm','Cookie': auth_token})

Here I would like to emphasize that the indicated requests activate / deactivate all Port Forwarding rules created earlier in the corresponding section of the router management.



Simply put, queries are similar to clicking the Enable All and Disable All buttons. By analogy, you can implement the creation / activation of individual rules.

Remote access to the router from the WAN (Internet)



#Задание IP адреса удаленного управления
r = requests.get(router_ip+'/'+session+'/userRpm/ManageControlRpm.htm?port=5110&ip='+remote_ip+'&Save=%D0%A1%D0%BE%D1%85%D1%80%D0%B0%D0%BD%D0%B8%D1%82%D1%8C',headers={'Referer':router_ip+'/'+session+'/userRpm/SysRebootRpm.htm','Cookie': auth_token})

Here, the remote_ip parameter sets the IP address of the remote control, i.e. the IP address from which it is allowed to remotely access the router via the Internet.



remote_ip=’255.255.255.255’ #открыто для всех.
remote_ip=’0.0.0.0’ #закрыто для всех.

If desired, you can specify a specific IP address from which you want to remotely connect to the router.

Defining a list of connected devices


#Определение подключенных устройств
r = requests.get(router_ip+'/'+session+'/userRpm/WlanStationRpm.htm',headers={'Referer':router_ip+'/'+session+'/userRpm/MenuRpm.htm','Cookie': auth_token})
presence='Дома находятся:'
if 'DC-31-54-97-51-06' in r.text:
            presence=presence+'\n'+'DC-31-54-97-51-06'

Why do we need the functionality of detecting devices registered in the home WiFi network. Suppose I have a smartphone with WiFi and MAC address DC-31-54-97-51-06. When I get home, my smartphone registers with my home WiFi network. In such a simple way, I can track my presence (or rather, the presence of my smartphone) in the home network (i.e. at home). This functionality allows you to automate a number of functions related to the determination of my presence. For example, when I get home, the motion detector of the surveillance camera turns off, etc. Previously, I used the detection of the presence of devices on the network using Ping'a IP addresses, but in the end I was disappointed in this method, since smartphones, as it turned out, are reluctant and do not always respond to Ping. Additionally, I use the ARP packet sniffer implemented in Python,

So, r.text in the code above returns, among other things, a list of MAC addresses of devices registered in the WiFi network of the router. What you will do with this list depends only on your imagination.

var hostList = new Array(
"94-36-44-8F-2F-ED", 5, 23487, 10618, 2, 
"60-46-37-C0-43-FC", 5, 27088, 10126, 2, 
"EF-71-44-63-51-E1", 5, 600, 364, 2, 
"77-25-9D-99-ED-33", 5, 1547, 1722, 2, 
0,0 );

So, I can only summarize. All code is as follows.

router.py
import requests
router_ip='http://192.168.0.1'
auth_token='Authorization=Basic%20YWRtaW46YjAxYzZmYzYyMDgwMzA5Y2ZiMzc2ZTE4NzI3YzMwNzk%3D'
def logout(session_id):
    r = requests.get(router_ip+'/'+session_id+'/userRpm/LogoutRpm.htm',headers={'Referer':router_ip+'/'+session_id+'/userRpm/MenuRpm.htm','Cookie': auth_token})  
    if r.status_code==200:
        return 'Loging out: '+str(r.status_code)
    else:
        return 'Unnable to logout'
def login():
    r = requests.get(router_ip+'/userRpm/LoginRpm.htm?Save=Save',headers={'Referer':router_ip+'/','Cookie': auth_token})
    if r.status_code==200:
        x=1
        while x<3:
            try:
                session_id=r.text[r.text.index(router_ip)+len(router_ip)+1:r.text.index('userRpm')-1]
                return session_id
                break
            except ValueError:
                return 'Login error'
            x+=1
    else:
        return 'IP unreachable'
def routercontrol(operation,remote_ip='255.255.255.255'):
        #Авторизация
    if login()=='IP unreachable' or login()=='Login error':
        return login()
        exit(0)
    else:
        session=login()
        print ('Login OK: '+session)
    if operation=='Enable ports': 
        #Открыть Port Forwarding
        r = requests.get(router_ip+'/'+session+'/userRpm/VirtualServerRpm.htm?doAll=EnAll&Page=1',headers={'Referer':router_ip+'/'+session+'/userRpm/VirtualServerRpm.htm','Cookie': auth_token})
        status=str(r.status_code)
        print (logout(session))
        return 'Enable all ports: '+status+' http://31.207.73.10:8082'
    elif operation=='Disable ports':
        #Закрыть Port Forwarding
        r = requests.get(router_ip+'/'+session+'/userRpm/VirtualServerRpm.htm?doAll=DisAll&Page=1',headers={'Referer':router_ip+'/'+session+'/userRpm/VirtualServerRpm.htm','Cookie': auth_token})
        status=str(r.status_code)
        print (logout(session))
        return 'Disable all ports: '+status
    elif operation=='Reboot':
        #Перезагрузка
        r = requests.get(router_ip+'/'+session+'/userRpm/SysRebootRpm.htm?Reboot=%D0%9F%D0%B5%D1%80%D0%B5%D0%B7%D0%B0%D0%B3%D1%80%D1%83%D0%B7%D0%B8%D1%82%D1%8C',headers={'Referer':router_ip+'/'+session+'/userRpm/SysRebootRpm.htm','Cookie': auth_token})
        status=str(r.status_code)
        print (logout(session))
        return 'Reboot: '+status
    elif operation=='Remote IP':
        #Задание IP адреса удаленного управления
        r = requests.get(router_ip+'/'+session+'/userRpm/ManageControlRpm.htm?port=5110&ip='+remote_ip+'&Save=%D0%A1%D0%BE%D1%85%D1%80%D0%B0%D0%BD%D0%B8%D1%82%D1%8C',headers={'Referer':router_ip+'/'+session+'/userRpm/SysRebootRpm.htm','Cookie': auth_token})
        status=str(r.status_code)
        print (logout(session))
        return 'Remote IP '+remote_ip+': '+status
    elif operation=='Check presence':
        #Определение подключенных устройств
        r = requests.get(router_ip+'/'+session+'/userRpm/WlanStationRpm.htm',headers={'Referer':router_ip+'/'+session+'/userRpm/MenuRpm.htm','Cookie': auth_token})
        status=str(r.status_code)
        print (logout(session))
        presence='Дома находятся:'
        if 'DC-31-54-97-51-06' in r.text:
            presence=presence+'\n'+'DC-31-54-97-51-06'
        return presence 
    else:
        return 'Wrong command'


Obviously, in a similar way it is possible to automate other functions of managing devices that are deprived of the command line by accessing the WEB interface. For example, in a similar way using HTTP Post I implemented a reboot of a DLink IP camera. Thanks for attention!

Also popular now: