【esp32】micropython实现自动登录校园网

本文首发于:本人博客【esp32】micropython实现自动登录校园网

上回书说到,笔者通过python在电脑上成功实现了自动登录校园网的脚本。在这之后,笔者开始思考如何在esp32上实现这一功能,并且实现一个可以无需依赖手机热点的物联网小项目。本文将首先介绍esp32联网所涉及的模块,并且对各个模块的用法进行阐释,再回顾一下登录校园网脚本的步骤,最后总结一下整个过程中的与出现的问题与解决办法并将各个部分的代码整合起来给出完整的示例代码。

一、涉及模块

esp连接校园网需要用到network模块进行wifi的连接,并通过urequests模块进行http数据收发,utime模块进行超时检测。

1. network模块

esp32c3模块提供了wifi模块并且可以通过调用network模块进行wifi的连接。esp32提供的网络接口有两种,分别是`network.STA_IF`与`network.AP_IF`,其中前者是station接口的简称,用于连接其他wifi,后者是access point的简称,用于开启热点并与其他wifi client建立连接。

关于wlan类有以下几个比较重要的函数:

class network.WLAN(interface_id)   # 用于获取创建wlan接口对象

WLAN.active([is_active])  # 获取或设置wlan接口激活状态

WLAN.connect(ssid=None, key=None, *, bssid=None)  # 连接到指定无线网络

WLAN.disconnect()  # 断开当前无线网络连接

WLAN.scan()  # 在STA接口上扫描可用的无线网络

WLAN.status([param])  # 返回无线连接的状态

WLAN.isconnected()  # 返回是否与其他无线网络建立连接状态

WLAN.ifconfig([(ip, subnet, gateway, dns)])  # 获取IP级网络接口参数(一个包含IP 地址、子网掩码、网关和 DNS 服务器的元组)

WLAN.config('param')  # 获取或设置通用网络接口参数

更多有关wlan类的函数和解释可以查看micropython文档中的wlan类介绍

此处我们用到的是station接口,先获取接口示例`wlan = network.WLAN(network.STA_IF)` ,随后激活wifi并进行连接,并且利用utime记录时间防止连接超时。最后利用`ifconfig()`函数获取本地ip。

在连接好wifi后还需要进行是否能够接入互联网的检测,在python中可以利用subprocess模块去ping一个公网地址,不过在micropython中既没有subprocess也没有ping的功能,笔者想到通过向某个网站发送一个http请求并且分析返回内容是否正确的方法来确认是否成功连接互联网。

def ping():
    rq = requests.get("http://www.example.com")  # 向example.com发送get请求
    if rq.status_code != 200 or "Example Domain" not in rq.text:  # 分析返回内容是否正确
        print("Unconnected to the Internet!")
        rq.close()  # 注意此处有必要关闭response对象,同时开启过多的response对象会导致报错OSError 32
        return False
    else:
        print("Connected to the Internet!")
        rq.close()
        return True

2. urequests模块

urequests网络请求模块与python中requests模块的使用方法比较像,可以实现后者大部分的功能,不过仍然有一定的不同。下面给出urequests中比较重要的函数。

urequests.request(method, url, data=None, json=None, headers={})
urequests.head(url, \*\*kw)
urequests.get(url, \*\*kw)
urequests.post(url, \*\*kw)
urequests.patch(url, \*\*kw)
urequests.delete(url, \*\*kw)

先回顾一下requests的使用方法:

def get(url, params=None, **kwargs)

`requests.get()`支持把字典作为参数传给`params`,requests会自动将字典转化成字符串的形式。这个操作也可以通过`urllib.parse.urlencode()`来完成,不过micropython中没有内置的urllib,第三方库的urllib也是出现了各种问题,导致必须手动实现一个`urlencode()`函数或者把所有的字典预处理成字符串。

def post(url, data=None, json=None, **kwargs)

与`get()`类似,`requests.post()`支持把字典传给`data`参数,不过micropython不允许这样做,所以必须先把字典处理成字符串再传入。其实这里讲字符串是不准确的,因为把str类型传给`params`和`data`时会报错`Error: Object with buffer protocol required`,正确的操作是传入一个bytes类型的字符串

micropython除了不支持对字典进行url编码、对参数类型要求格外严苛之外,还有几处很奇妙的不同。
其一是micropython的requests模块的所有函数都会自动向headers中添加一项"Content-Length",注意是自动!这个特性令我痛苦的debug了好久,明明headers和data都是完全正确的,post数据包加了content-length就是Bad Request 400,get数据包啥都不加都返回错误码Internal Server Error 500(~~真不知道学校的垃圾服务器为什么会对有问题的数据包返回一个500~~),也不能靠抓包来细致的查看是发送的哪个环节出现了问题。最后还好在网上找到了urequests的源代码,直接搬下来一行行纠错,发现原来会自动添加正文长度。

其二是micropython不支持gbk,所以所有的encode、decode函数都失效,所以`'字符串'.encode('gbk')`实际上返回的还是一个utf-8编码的bytes。本来这点没什么,但是有意思的在下面。学校的辣鸡网络通是用gbk编码的,然后urequests的`response.text()`是这样实现的:

def text(self):
    return str(self.content, self.encoding)

然后这~~两坨答辩~~就碰撞出了美妙而几近天衣无缝的bug:一个返回gbk,一个没法解码gbk,直接报错`'utf-8' codec can't decode byte 0xb5 in position 4: invalid start byte` ,并且还定位不到错误位置,几乎无解。最后还是通过更改urequests源代码,把encoding去掉的方式解决的。

更多有关requests库和urequests的内容可以查看requests官方文档micropython文档中对urequests的描述。笔者更改过后的urequests模块源代码放在这里

二、校园网登录

利用python实现的校园网自动登录脚本详见上一篇文章,这里只做简单的回顾。

校园网自动登录有两种方式,webdriver模拟浏览器法和requests模拟请求法,其中前者并不受micropython的支持,因此想要实现模拟登陆只能通过urequests模块。校园网络通登录并连接互联网需要进行两次请求,先进行一次post以提交用户信息并让服务端记录本地ip,再发送一次get请求开通网络。

post与get的请求头都通过浏览器抓包获取并且制作成字典,数据正文则可以在浏览器抓包后点击view source按钮,然后复制正文直接放进字符串。

三、实现代码

这样就可以就可以让esp32成功登陆校园网啦!

 下面放出实现代码

import network
import requests
import utime
import ujson
import re

class Wifi(object):
    
    CONNECT_TIMEOUT = 5000 # ms
    
    def connect(self, ssid, password):
        #获取站点WiFi接口的实例并将其存储在变量上
        self.wifi = network.WLAN(network.STA_IF)

        if self.wifi.isconnected() == True:
            self.wifi.disconnect()
            
        #使用存储在前文所述的变量中的凭证来激活网络接口并执行实际连接。
        self.wifi.active(True)
        self.wifi.connect(ssid, password)

        connect_time = utime.ticks_ms()
        while self.wifi.isconnected() == False:
            if utime.ticks_ms() - connect_time >= self.CONNECT_TIMEOUT:
                print("Connect time out!")
                return False
            else:
                pass
        
        self.ipaddr = self.wifi.ifconfig()[0]
        print("Connection successful, IP Address: {}".format(self.ipaddr))
        return True
    
    def disconnect(self):
        self.wifi.disconnect()
    
    def ping():
        rq = requests.get("http://www.example.com")
        if rq.status_code != 200 or "Example Domain" not in rq.text:
            print("Unconnected to the Internet!")
            rq.close()
            return False
        else:
            print("Connected to the Internet!")
            rq.close()
            return True
       
class CampusWifi(Wifi):
    def __init__(self, ssid, password, userid, userpassword):
        self.ssid = ssid
        self.password = password
        self.uid = userid
        self.upassword = userpassword
    
    def connect(self):
        ret = super().connect(self.ssid, self.password)
        if not ret:
            print("Fail to connect to campus wifi!")
            return False
        
        Wifi.ping()
        
        print("Logging into campus wifi·····")
        ret = self._login()
        if ret:
            print("Login Success!")
        else:
            return False

        print("Submmitting·····")
        ret = self._submit()
        if ret:
            print("Submit Success!")
        else:
            return False

        print("Testing for the Internet·····")
        Wifi.ping()
        
        print("Logging out·····")
        ret = self._logout()
        if ret:
            print("Logout Success!")
        else:
            return False
        
        print("Testing for the Internet·····")
        Wifi.ping()
        
        
        return True
    
    def _login(self):
        login_url = "http://202.38.64.59/cgi-bin/ip"
        login_data = "cmd=login&url=URL&ip={ip}&name={uid}&password={upassword}&go=%B5%C7%C2%BC%D5%CA%BB%A7".format(ip = self.ipaddr, uid = self.uid, upassword = self.upassword)
        login_headers = {
            "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
            "Accept-Encoding": "gzip, deflate",
            "Accept-Language": "zh-CN,zh;q=0.9",
            "Cache-Control": "max-age=0",
            "Connection": "keep-alive",
            #"Content-Length": str(len_test), #str(len(login_data)),
            "Content-Type": "application/x-www-form-urlencoded",
            "Cookie": "name=; password=",
            "Host": "202.38.64.59",
            "Origin": "http://202.38.64.59",
            "Referer": "http://202.38.64.59/cgi-bin/ip",
            "Upgrade-Insecure-Requests": "1",
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36"
        }
        
        ret = 1
        rq = requests.request(method = "POST", url = login_url, headers=login_headers, data = login_data)
        if rq.status_code != 200:
            print("In _login(): \n    login failed with status code {sc}, \n    headers:\n{hd} \n    text:\n{tx}".\
                  format(sc = rq.status_code, hd = rq.headers, tx = rq.text))
            ret = 0
      
        if "Set-Cookie" in rq.headers:
            self.cookie_rn = rq.headers["Set-Cookie"]
        else :
            print("Login Failed!\nNo cookie in headers! \n    headers:\n{hd}\n    text:\n{tx}".format(hd = rq.headers, tx = rq.text))
            ret = 0
        rq.close()
        return ret

    def _submit(self):
        try:
            self.cookie_rn = self.cookie_rn
        except :
            raise ValueError("Please Login() before Submit()!")
        
        submit_url = r"http://202.38.64.59/cgi-bin/ip?cmd=set&url=URL&type=8&exp=0&go=+%BF%AA%CD%A8%CD%F8%C2%E7+"
        submit_headers = {
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
            'Accept-Encoding': 'gzip, deflate',
            'Accept-Language': 'zh-CN,zh;q=0.9',
            'Connection': 'keep-alive',
            'Cookie': 'name=; password=; {rn}'.format(rn = self.cookie_rn),
            'Host': '202.38.64.59',
            'Referer': 'http://202.38.64.59/cgi-bin/ip',
            'Upgrade-Insecure-Requests': '1',
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36'
        }
        submit_data = "cmd=set&url=URL&type=8&exp=0&go=+%BF%AA%CD%A8%CD%F8%C2%E7+"
        
        ret = 1
        rq = requests.get(submit_url, headers = submit_headers, data = submit_data)
        if rq.status_code != 200:
            print("In _submit(): \n    submit failed with status code {sc}, \n    headers:\n{hd} \n    text:\n{tx}".\
                  format(sc = rq.status_code, hd = rq.headers, tx = rq.text))
            ret = 0
        rq.close()
        return ret

    def _logout(self):
        try:
            self.cookie_rn = self.cookie_rn
        except :
            raise ValueError("Please Login() before Logout()!")
        
        logout_url = "http://202.38.64.59/cgi-bin/ip?cmd=logout"
        logout_headers = {
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
            'Accept-Encoding': 'gzip, deflate',
            'Accept-Language': 'zh-CN,zh;q=0.9',
            'Connection': 'keep-alive',
            'Cookie': 'name=; password=; {rn}'.format(rn = self.cookie_rn),
            'Host': '202.38.64.59',
            'Referer': 'http://202.38.64.59/cgi-bin/ip?cmd=set&url=URL&type=8&exp=0&go=+%BF%AA%CD%A8%CD%F8%C2%E7+',
            'Upgrade-Insecure-Requests': '1',
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36',
        }
        logout_data = "cmd=logout"
        
        ret = 1
        rq = requests.get(logout_url, headers = logout_headers, data = logout_data)
        if rq.status_code != 200:
            print("In _logout(): \n    logout failed with status code {sc}, \n    headers:\n{hd} \n    text:\n{tx}".  format(sc = rq.status_code, hd = rq.headers, tx = rq.text))
            ret = 0
        rq.close()
        return ret

if __name__ == "__main__":
    
    ssid = "ustcnet"
    password =  ""
    user_name = "username"
    user_password = "userpassword"

    campuswifi = CampusWifi(ssid, password, user_name, user_password)
    campuswifi.connect()

四、总结

经过本次esp32连接校园网项目的实践,笔者对于urequests库及http协议相关知识有了更深的理解,同时也认识到了micropython在某些方面的缺陷,为以后相关的开发找到了解决方案。下面列举并总结了本项目实现过程中遇到的一些问题及解决办法:

- Wifi Internal Error是wifi模块内部错误,reset无用,需要把板子断点重连一下
- OSError 32:套接字连接过多导致,注意将response对象关闭(`response.close()`)即可
- micropython 中不支持gbk编码,encode、decode仅支持str与bytes之间的utf-8转换
- HTTP 500 Internal Server Error 也可能是由于请求格式不对导致的
- urequests有自动添加`content-length`、自动解码`response.text`的bug,需要更改源码解决

以上就是esp32自动登录校园网的项目实现过程及源码,希望能给读者带来帮助~


 

  • 6
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值