├── lib ├── easyweb.mpy ├── easynetwork.mpy ├── easyweb_single.mpy ├── easyweb_thread.mpy ├── easynetwork.py ├── easynetwork-english.py ├── easyweb_single.py ├── easyweb_thread.py └── easyweb.py ├── web ├── EasyWeb_256px.png ├── time.html ├── 404.html └── wifi.html ├── main.py ├── README.ZH-CN.md └── README.md /lib/easyweb.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funnygeeker/micropython-easyweb/HEAD/lib/easyweb.mpy -------------------------------------------------------------------------------- /lib/easynetwork.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funnygeeker/micropython-easyweb/HEAD/lib/easynetwork.mpy -------------------------------------------------------------------------------- /lib/easyweb_single.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funnygeeker/micropython-easyweb/HEAD/lib/easyweb_single.mpy -------------------------------------------------------------------------------- /lib/easyweb_thread.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funnygeeker/micropython-easyweb/HEAD/lib/easyweb_thread.mpy -------------------------------------------------------------------------------- /web/EasyWeb_256px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funnygeeker/micropython-easyweb/HEAD/web/EasyWeb_256px.png -------------------------------------------------------------------------------- /web/time.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Time 6 | 7 | 8 |

Time: {{time}}

9 | 10 | -------------------------------------------------------------------------------- /web/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 404 Not Found 6 | 25 | 26 | 27 |
28 |

404 Not Found

29 |

您访问的网页不存在

30 |
31 | 32 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import time 2 | from lib.easynetwork import Client 3 | from lib.easyweb import EasyWeb, render_template, send_file, make_response 4 | 5 | client = Client() 6 | client.connect("ssid", "password") # 或者 client.connect("ssid", "") 7 | 8 | while not client.isconnected(): 9 | pass 10 | print('IP Address: {}'.format(client.ifconfig()[0])) 11 | 12 | ew = EasyWeb() # 初始化 EasyWeb 13 | 14 | # 添加路由 15 | @ew.route('/') 16 | def home(request): 17 | print("=======[request]=======") 18 | print('URL: ', request.url) 19 | print('Args: ', request.args) 20 | print('Form: ', request.form) 21 | print('Host: ', request.host) 22 | print('Json: ', request.json) 23 | print('Path: ', request.path) 24 | print('Header: ', request.headers) 25 | print('Cookies: ', request.cookies) 26 | print('Full_Path: ', request.full_path) 27 | return render_template("/web/wifi.html") 28 | 29 | # 发送文件 30 | @ew.route('/easyweb.png') 31 | def img(request): 32 | # 访问网页的 /easyweb.png 试试? 33 | return send_file("/web/EasyWeb_256px.png") 34 | 35 | # 下载文件 36 | @ew.route('/download') 37 | def download(request): 38 | # 访问网页的 /easyweb.png 试试? 39 | # as_attachment: 是否作为附件发送文件,作为附件时会被下载 40 | # attachment_filename: 下载文件时向用户显示的文件名。如果未提供,将使用原始文件名 41 | return send_file("/web/EasyWeb_256px.png", as_attachment=True, attachment_filename='easyweb.png') 42 | 43 | # 停止 EasyWeb 44 | @ew.route('/stop') 45 | def stop(request): 46 | ew.stop() 47 | 48 | # 获取字符串 49 | @ew.route('/user/') 50 | def user(request): 51 | # 访问网页的 /user/123456 试试? 52 | return "

Hello {}

".format(request.match) 53 | 54 | # 获取路径 55 | @ew.route('/path/') 56 | def path(request): 57 | # 访问网页的 /path/123/456 试试? 58 | return "

Path {}

".format(request.match) 59 | 60 | # 渲染 HTML 61 | @ew.route('/time') 62 | def the_time(request): 63 | # 访问网页的 /time 然后刷新几遍页面,观察网页的变化 64 | return render_template("/web/time.html", time=time.time()) 65 | 66 | # 获取与设置 Cookies 67 | @ew.route('/cookie') 68 | def cookie(request): 69 | # 访问网页的 /cookie 然后刷新几遍页面,观察网页的变化 70 | response = make_response('

Cookies: {}

'.format(str(request.cookies))) 71 | response.set_cookie('cookie_name', 'cookie_value') 72 | return response 73 | 74 | # 自定义状态码 75 | @ew.route('/404') 76 | def status_code(request): 77 | # 访问网页的 /404,打开开发人员工具观察状态码 78 | return '

404 Not Found

', 404 79 | 80 | # 获取与设置 Cookies,同时自定义状态码 81 | @ew.route('/cookie2') 82 | def cookie2(request): 83 | # 访问网页的 /cookie 然后刷新几遍页面,观察网页的变化 84 | response = make_response() # 也可以在后面为 response.data 赋值,来代替初始化时赋值 85 | response.data = '

Cookies: {}


404 Not Found

'.format(str(request.cookies)) 86 | response.set_cookie('cookie_name', 'cookie_value') 87 | return response, 404 88 | 89 | # 获取 JSON 格式的内容 90 | @ew.route('/json') 91 | def cookie2(request): 92 | # 访问网页的 /json 93 | return {'type': 'json', 'num': 123} 94 | 95 | ew.run() 96 | print('======END======') # 访问 /stop -------------------------------------------------------------------------------- /README.ZH-CN.md: -------------------------------------------------------------------------------- 1 | [English (英语)](./README.md) 2 | 3 | # micropython-easyweb 4 | ![EasyWeb](./web/EasyWeb_256px.png) 5 | - 适用于 `Micropython` 的:简易 Web Server 库,易用,多功能 6 | 7 | ### 特点 8 | - 尽可能模仿了 `Flask` 框架的风格 9 | - 集成了常用的:GET 请求参数解析,表单解析,HTML渲染,文件发送,Cookies设置,Cookies获取,动态路由 等常用功能 10 | 11 | ### 使用说明 12 | - 本项目一共有三个版本的文件,请根据实际需要进行选择: 13 | - `thread`: `/lib/easyweb_thread.py` 使用多线程实现 14 | - `asyncio`: `/lib/easyweb.py` 使用异步实现,具有较好的兼容性和可靠性 15 | - `single`: `/lib/easyweb_single.py` 使用单线程循环实现,具有较好的兼容性 16 | 17 | ### 兼容性 18 | #### 已通过测试设备 19 | - `ESP-01S`: `single` 20 | - `ESP32`: `single`, `thread`, `asyncio` 21 | - `ESP32-C3`: `single`, `thread`, `asyncio` 22 | - `ESP32-S3`: `single`, `thread`, `asyncio` 23 | 24 | 25 | ### 示例代码 26 | - 这里我们使用 [micropython-easynetwork](https://github.com/funnygeeker/micropython-easynetwork) 作为示例,连接局域网(也可以工作在AP模式,使用其他设备连接开发板) 27 | ```python 28 | import time 29 | from lib.easynetwork import Client 30 | from lib.easyweb import EasyWeb, render_template, send_file, make_response 31 | 32 | client = Client() 33 | client.connect("ssid", "password") # 或者 client.connect("ssid", "") 34 | 35 | while not client.isconnected(): 36 | pass 37 | print('IP Address: {}'.format(client.ifconfig()[0])) 38 | 39 | ew = EasyWeb() # 初始化 EasyWeb 40 | 41 | # 添加路由 42 | @ew.route('/') 43 | def home(request): 44 | print("=======[request]=======") 45 | print('URL: ', request.url) 46 | print('Args: ', request.args) 47 | print('Form: ', request.form) 48 | print('Host: ', request.host) 49 | print('Json: ', request.json) 50 | print('Path: ', request.path) 51 | print('Header: ', request.headers) 52 | print('Cookies: ', request.cookies) 53 | print('Full_Path: ', request.full_path) 54 | return render_template("/web/wifi.html") 55 | 56 | # 发送文件 57 | @ew.route('/easyweb.png') 58 | def img(request): 59 | # 访问网页的 /easyweb.png 试试? 60 | return send_file("/web/EasyWeb_256px.png") 61 | 62 | # 下载文件 63 | @ew.route('/download') 64 | def download(request): 65 | # 访问网页的 /easyweb.png 试试? 66 | # as_attachment: 是否作为附件发送文件,作为附件时会被下载 67 | # attachment_filename: 下载文件时向用户显示的文件名。如果未提供,将使用原始文件名 68 | return send_file("/web/EasyWeb_256px.png", as_attachment=True, attachment_filename='easyweb.png') 69 | 70 | # 停止 EasyWeb 71 | @ew.route('/stop') 72 | def stop(request): 73 | ew.stop() 74 | 75 | # 获取字符串 76 | @ew.route('/user/') 77 | def user(request): 78 | # 访问网页的 /user/123456 试试? 79 | return "

Hello {}

".format(request.match) 80 | 81 | # 获取路径 82 | @ew.route('/path/') 83 | def path(request): 84 | # 访问网页的 /path/123/456 试试? 85 | return "

Path {}

".format(request.match) 86 | 87 | # 渲染 HTML 88 | @ew.route('/time') 89 | def the_time(request): 90 | # 访问网页的 /time 然后刷新几遍页面,观察网页的变化 91 | return render_template("/web/time.html", time=time.time()) 92 | 93 | # 获取与设置 Cookies 94 | @ew.route('/cookie') 95 | def cookie(request): 96 | # 访问网页的 /cookie 然后刷新几遍页面,观察网页的变化 97 | response = make_response('

Cookies: {}

'.format(str(request.cookies))) 98 | response.set_cookie('cookie_name', 'cookie_value') 99 | return response 100 | 101 | # 自定义状态码 102 | @ew.route('/404') 103 | def status_code(request): 104 | # 访问网页的 /404,打开开发人员工具观察状态码 105 | return '

404 Not Found

', 404 106 | 107 | # 获取与设置 Cookies,同时自定义状态码 108 | @ew.route('/cookie2') 109 | def cookie2(request): 110 | # 访问网页的 /cookie 然后刷新几遍页面,观察网页的变化 111 | response = make_response() # 也可以在后面为 response.data 赋值,来代替初始化时赋值 112 | response.data = '

Cookies: {}


404 Not Found

'.format(str(request.cookies)) 113 | response.set_cookie('cookie_name', 'cookie_value') 114 | return response, 404 115 | 116 | # 获取 JSON 格式的内容 117 | @ew.route('/json') 118 | def cookie2(request): 119 | # 访问网页的 /json 120 | return {'type': 'json', 'num': 123} 121 | 122 | ew.run() 123 | print('======END======') # 访问 /stop 124 | ``` -------------------------------------------------------------------------------- /web/wifi.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 配置页面 8 | 70 | 71 | 72 | 73 |
74 |
75 |
76 |

基本设置

77 |
78 |
79 |
80 |

网络

81 |

82 |
83 | 84 |

85 |

86 |
87 | 88 |

89 |
90 | 91 |
92 |

应用设置

93 |

时间

94 |

95 |
96 | 97 |

98 |

99 | 100 |
101 | 102 |

103 |

天气

104 |

105 |
106 | 112 |

113 |
114 |
115 |
116 |
117 |

118 | 121 | 124 |

125 |
126 |
127 |
128 | 129 | 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [简体中文 (Chinese)](./README.ZH-CN.md) 2 | # micropython-easyweb 3 | 4 | ![EasyWeb](./web/EasyWeb_256px.png) 5 | - A simple and versatile web server library for `Micropython`, easy to use. 6 | 7 | ### Features 8 | - Aims to mimic the style of the Flask framework as much as possible 9 | - Integrates common functionalities such as GET request parameter parsing, form parsing, HTML rendering, file sending, cookie setting, cookie retrieval, dynamic routing, and more. 10 | 11 | ### Instructions 12 | - There are three versions of the project files, please choose the one that suits your needs: 13 | - `thread`: `/lib/easyweb_thread.py` - implemented with multithreading 14 | - `asyncio`: `/lib/easyweb.py` - implemented with asynchronous support, provides better compatibility and reliability 15 | - `single`: `/lib/easyweb_single.py` - implemented with a single thread loop, provides good compatibility 16 | 17 | ### Compatibility 18 | #### Tested Devices 19 | - `ESP-01S`: `single` 20 | - `ESP32`: `single`, `thread`, `asyncio` 21 | - `ESP32-C3`: `single`, `thread`, `asyncio` 22 | - `ESP32-S3`: `single`, `thread`, `asyncio` 23 | 24 | ### Sample Code 25 | - Here we use [micropython-easynetwork](https://github.com/funnygeeker/micropython-easynetwork) as an example to connect to the local network (it can also work in AP mode, allowing other devices to connect to the development board). 26 | ```python 27 | import time 28 | from lib.easynetwork import Client 29 | from lib.easyweb import EasyWeb, render_template, send_file, make_response 30 | 31 | client = Client() 32 | client.connect("ssid", "password") # or client.connect("ssid", "") 33 | 34 | while not client.isconnected(): 35 | pass 36 | print('IP Address: {}'.format(client.ifconfig()[0])) 37 | 38 | ew = EasyWeb() # Initialize EasyWeb 39 | 40 | # Add routes 41 | @ew.route('/') 42 | def home(request): 43 | print("=======[request]=======") 44 | print('URL: ', request.url) 45 | print('Args: ', request.args) 46 | print('Form: ', request.form) 47 | print('Host: ', request.host) 48 | print('Json: ', request.json) 49 | print('Path: ', request.path) 50 | print('Header: ', request.headers) 51 | print('Cookies: ', request.cookies) 52 | print('Full_Path: ', request.full_path) 53 | return render_template("/web/wifi.html") 54 | 55 | # Send file 56 | @ew.route('/easyweb.png') 57 | def img(request): 58 | # Try accessing /easyweb.png on the website 59 | return send_file("/web/EasyWeb_256px.png") 60 | 61 | # Download file 62 | @ew.route('/download') 63 | def download(request): 64 | # Try accessing /easyweb.png on the website 65 | # as_attachment: Whether to send the file as an attachment, it will be downloaded when sent as an attachment 66 | # attachment_filename: The filename displayed to the user when downloading. If not provided, the original filename will be used 67 | return send_file("/web/EasyWeb_256px.png", as_attachment=True, attachment_filename='easyweb.png') 68 | 69 | # Stop EasyWeb 70 | @ew.route('/stop') 71 | def stop(request): 72 | ew.stop() 73 | 74 | # Get string 75 | @ew.route('/user/') 76 | def user(request): 77 | # Try accessing /user/123456 on the website 78 | return "

Hello {}

".format(request.match) 79 | 80 | # Get path 81 | @ew.route('/path/') 82 | def path(request): 83 | # Try accessing /path/123/456 on the website 84 | return "

Path {}

".format(request.match) 85 | 86 | # Render HTML 87 | @ew.route('/time') 88 | def the_time(request): 89 | # Access /time on the website and refresh the page a few times to observe the changes on the webpage 90 | return render_template("/web/time.html", time=time.time()) 91 | 92 | # Get and set Cookies 93 | @ew.route('/cookie') 94 | def cookie(request): 95 | # Access /cookie on the website and refresh the page a few times to observe the changes on the webpage 96 | response = make_response('

Cookies: {}

'.format(str(request.cookies))) 97 | response.set_cookie('cookie_name', 'cookie_value') 98 | return response 99 | 100 | # Custom status code 101 | @ew.route('/404') 102 | def status_code(request): 103 | # Access /404 on the website, open the developer tools to observe the status code 104 | return '

404 Not Found

', 404 105 | 106 | # Get and set Cookies, while customizing the status code 107 | @ew.route('/cookie2') 108 | def cookie2(request): 109 | # Access /cookie on the website and refresh the page a few times to observe the changes on the webpage 110 | response = make_response() # You can also assign a value to response.data later instead of initializing it during initialization 111 | response.data = '

Cookies: {}


404 Not Found

'.format(str(request.cookies)) 112 | response.set_cookie('cookie_name', 'cookie_value') 113 | return response, 404 114 | 115 | # Get content in JSON format 116 | @ew.route('/json') 117 | def cookie2(request): 118 | # Access /json on the website 119 | return {'type': 'json', 'num': 123} 120 | 121 | ew.run() 122 | print('======END======') # Access /stop 123 | ``` -------------------------------------------------------------------------------- /lib/easynetwork.py: -------------------------------------------------------------------------------- 1 | import network 2 | 3 | 4 | def _active(func): 5 | """ 6 | 检查 WLAN 是否开启,若未开启,则开启后关闭 7 | """ 8 | def change_active(self, *args, **kwargs): 9 | if not self.active(): # wlan 不常用时尽量减小功耗 10 | self.active(True) 11 | result = func(self, *args, **kwargs) 12 | self.active(False) 13 | return result 14 | else: 15 | return func(self, *args, **kwargs) 16 | 17 | return change_active 18 | 19 | 20 | class Client(network.WLAN): 21 | def __init__(self): 22 | super().__init__(network.STA_IF) 23 | self.PM_NONE = 0 24 | self.PM_PERFORMANCE = 1 25 | self.PM_POWERSAVE = 2 26 | 27 | def connect(self, *args, **kwargs): 28 | """ 29 | 连接无线网络 30 | 31 | Args: 32 | ssid: 无线网络名称 33 | key: 无线网络密码 34 | bssid: 无线网络名称相符时,连接指定 MAC地址 的设备,(默认不指定) 35 | reconnects: 重新连接尝试次数(int, 0=无,-1=无限制) 36 | """ 37 | super().active(True) 38 | super().disconnect() 39 | super().connect(*args, **kwargs) 40 | 41 | @_active 42 | def scan(self) -> list: 43 | """ 44 | 扫描无线网络 45 | 46 | Returns: 47 | List[Tuple[bytes, bytes, int, int, int, bool]] 48 | [(ssid, bssid, channel, RSSI, security, hidden), ...] 49 | """ 50 | return super().scan() 51 | 52 | @_active 53 | def config(self, *args, **kwargs): 54 | """ 55 | 获取或设置常规网络接口参数 56 | 57 | Args: 58 | pm: 电源管理设置 59 | Client.PM_NONE - 高性能模式 (0) 60 | Client.PM_PERFORMANCE - 平衡模式 (1)(默认) 61 | Client.PM_POWERSAVE - 节能模式 (2) 62 | mac: 无线网络 MAC 地址 63 | ssid: 无线网络名称,不能设置(只读) 64 | hostname: 主机名,设置时长度不应超过 32 65 | tx_power: 最大发射功率(dBm),一般范围在:2-21 66 | 67 | Example: 68 | # 获取 PM 配置信息 69 | config = client.config('pm') 70 | 71 | # 设置IP配置信息 72 | client.config(pm = 2) 73 | """ 74 | return super().config(*args, **kwargs) 75 | 76 | def active(self, *args, **kwargs): 77 | """ 78 | 设置 或 获取 WLAN 的活动状态 79 | 80 | Args: 81 | True or False (不填写参数则为获取) 82 | 83 | Returns: 84 | None / bool 85 | """ 86 | return super().active(*args, **kwargs) 87 | 88 | def status(self, *args, **kwargs): 89 | """ 90 | 获取网络连接状态 91 | 92 | Returns: 93 | network.STAT_IDLE – 无连接且无活动 (1000) 94 | network.STAT_CONNECTING – 连接进行中 (1001) 95 | network.STAT_BEACON_TIMEOUT – 因广播帧超时而连接失败 (200) 96 | network.STAT_NO_AP_FOUND – 因没有接入点回复而连接失败 (201) 97 | network.STAT_WRONG_PASSWORD – 因密码错误而连接失败 (202) 98 | network.STAT_ASSOC_FAIL – 因关联出现问题而连接失败 (203) 99 | network.STAT_HANDSHAKE_TIMEOUT – 因握手超时而连接失败 (204) 100 | network.STAT_GOT_IP – 连接成功 (1010) 101 | """ 102 | return super().status(*args, **kwargs) 103 | 104 | def isconnected(self) -> bool: 105 | """ 106 | 网络是否已连接 107 | 108 | Returns: 109 | True: 已连接 110 | False: 未连接 111 | """ 112 | return super().isconnected() 113 | 114 | def disconnect(self): 115 | """ 116 | 断开连接的网络 117 | """ 118 | if super().active(): 119 | super().disconnect() 120 | super().active(False) # 关闭 WIFI 接口 121 | 122 | @_active 123 | def ifconfig(self, *args, **kwargs) -> tuple: 124 | """ 125 | 获取或设置IP级网络接口参数。 126 | 127 | Args: 128 | 包含IP地址、子网掩码、网关和 DNS服务器的元组。默认值为 None,即获取网络状态 129 | 130 | Returns: 131 | tuple: 包含IP地址、子网掩码、网关和 DNS服务器的元组。 132 | 133 | Example: 134 | # 获取IP配置信息 135 | ip_config = client.ifconfig() 136 | 137 | # 设置IP配置信息 138 | client.ifconfig(('192.168.3.4', '255.255.255.0', '192.168.3.1', '8.8.8.8')) 139 | """ 140 | return super().ifconfig(*args, **kwargs) 141 | 142 | 143 | class AP(network.WLAN): 144 | def __init__(self): 145 | super().__init__(network.AP_IF) 146 | self.PM_NONE = 0 147 | self.PM_PERFORMANCE = 1 148 | self.PM_POWERSAVE = 2 149 | 150 | @_active 151 | def config(self, *args, **kwargs): 152 | """ 153 | 获取或设置常规网络接口参数 154 | 155 | Args: 156 | pm: 电源管理设置 157 | AP.PM_NONE - 高性能模式 (0) 158 | AP.PM_PERFORMANCE - 平衡模式 (1)(默认) 159 | AP.PM_POWERSAVE - 节能模式 (2) 160 | mac: 无线网络 MAC 地址 161 | key: 连接密码,设置时长度应大于 8,或者为 None, '', 0,修改密码时会自动修改 security(只写,不能读取) 162 | hidden: 是否隐藏 163 | 0 - 可见 164 | 1 - 隐藏 165 | channel: 信道 166 | 一般为 1 - 13 167 | security: 认证模式 168 | 0 - Open 169 | 1 - WEP 170 | 2 - WPA-PSK 171 | 3 - WPA2-PSK 172 | 4 - WPA/WPA2-PSK 173 | ssid: 无线网络名称 174 | hostname: 主机名,设置时长度不应超过 32 175 | tx_power: 最大发射功率(dBm),一般范围在:2-21 176 | 177 | Example: 178 | # 获取 PM 配置信息 179 | config = client.config('pm') 180 | 181 | # 设置IP配置信息 182 | client.config(pm = 2) 183 | """ 184 | if kwargs.get('key') is not None: # 修改密码时自动修改 认证模式 185 | if kwargs['key']: 186 | if super().config('security') == 0 and kwargs.get('security') is None: 187 | if len(kwargs['key']) >= 8: 188 | super().config(key=kwargs['key'], security=4) 189 | else: 190 | print('[ERROR] The password length should not be less than 8.') 191 | else: 192 | super().config(security=0) 193 | return super().config(*args, **kwargs) 194 | 195 | @_active 196 | def ifconfig(self, *args, **kwargs) -> tuple: 197 | """ 198 | 获取或设置IP级网络接口参数。 199 | 200 | Args: 201 | 包含IP地址、子网掩码、网关和 DNS服务器的元组。默认值为 None,即获取网络状态 202 | 203 | Returns: 204 | tuple: 包含IP地址、子网掩码、网关和 DNS服务器的元组。 205 | 206 | Example: 207 | # 获取IP配置信息 208 | ip_config = client.ifconfig() 209 | 210 | # 设置IP配置信息 211 | client.ifconfig(('192.168.4.1', '255.255.255.0', '192.168.4.1', '0.0.0.0')) 212 | """ 213 | return super().ifconfig(*args, **kwargs) 214 | 215 | def isconnected(self) -> bool: 216 | """ 217 | 是否有设备接入 218 | 219 | Returns: 220 | True: 有 221 | False: 无 222 | """ 223 | return super().isconnected() 224 | 225 | def active(self, *args, **kwargs): 226 | """ 227 | 设置 或 获取 WLAN 的活动状态 228 | 229 | Args: 230 | True or False (不填写参数则为获取) 231 | 232 | Returns: 233 | None / bool 234 | """ 235 | return super().active(*args, **kwargs) 236 | -------------------------------------------------------------------------------- /lib/easynetwork-english.py: -------------------------------------------------------------------------------- 1 | import network 2 | 3 | 4 | def _active(func): 5 | """ 6 | Check whether WLAN is turned on, if not, turn it on and turn it off 7 | """ 8 | def change_active(self, *args, **kwargs): 9 | if not self.active(): # wlan 不常用时尽量减小功耗 10 | self.active(True) 11 | result = func(self, *args, **kwargs) 12 | self.active(False) 13 | return result 14 | else: 15 | return func(self, *args, **kwargs) 16 | 17 | return change_active 18 | 19 | 20 | class Client(network.WLAN): 21 | def __init__(self): 22 | super().__init__(network.STA_IF) 23 | self.PM_NONE = 0 24 | self.PM_PERFORMANCE = 1 25 | self.PM_POWERSAVE = 2 26 | 27 | def connect(self, *args, **kwargs): 28 | """ 29 | Connects to a wireless network. 30 | 31 | Args: 32 | ssid: Wireless network name. 33 | key: Wireless network password. 34 | bssid: Connects to a specific device with the specified MAC address if the network name matches (default: None). 35 | reconnects: The number of reconnection attempts (int, 0 for no limit, -1 for unlimited). 36 | """ 37 | super().active(True) 38 | super().disconnect() 39 | super().connect(*args, **kwargs) 40 | 41 | @_active 42 | def scan(self) -> list: 43 | """ 44 | Scans for wireless networks. 45 | 46 | Returns: 47 | List of tuples containing information about each network: 48 | [(ssid, bssid, channel, RSSI, security, hidden), ...] 49 | """ 50 | return super().scan() 51 | 52 | @_active 53 | def config(self, *args, **kwargs): 54 | """ 55 | Gets or sets general network interface parameters. 56 | 57 | Args: 58 | pm: Power management setting. 59 | Client.PM_NONE - High-performance mode (0). 60 | Client.PM_PERFORMANCE - Balanced mode (1) (default). 61 | Client.PM_POWERSAVE - Power-saving mode (2). 62 | mac: Wireless network MAC address. 63 | ssid: Wireless network name (read-only). 64 | hostname: Hostname, length should not exceed 32 characters. 65 | tx_power: Maximum transmit power (dBm), generally ranging from 2 to 21. 66 | 67 | Example: 68 | # Get PM configuration info. 69 | config = client.config('pm') 70 | 71 | # Set IP configuration info. 72 | client.config(pm = 2) 73 | """ 74 | return super().config(*args, **kwargs) 75 | 76 | def active(self, *args, **kwargs): 77 | """ 78 | Sets or gets the active status of WLAN. 79 | 80 | Args: 81 | True or False (default: None for getting the status). 82 | 83 | Returns: 84 | None / bool 85 | """ 86 | return super().active(*args, **kwargs) 87 | 88 | def status(self, *args, **kwargs): 89 | """ 90 | Gets the network connection status. 91 | 92 | Returns: 93 | network.STAT_IDLE - No connection and no activity (1000) 94 | network.STAT_CONNECTING - Connection in progress (1001) 95 | network.STAT_BEACON_TIMEOUT - Connection failed due to beacon timeout (200) 96 | network.STAT_NO_AP_FOUND - Connection failed due to no access point reply (201) 97 | network.STAT_WRONG_PASSWORD - Connection failed due to wrong password (202) 98 | network.STAT_ASSOC_FAIL - Connection failed due to association problem (203) 99 | network.STAT_HANDSHAKE_TIMEOUT - Connection failed due to handshake timeout (204) 100 | network.STAT_GOT_IP - Connection successful (1010) 101 | """ 102 | return super().status(*args, **kwargs) 103 | 104 | def isconnected(self) -> bool: 105 | """ 106 | Checks if the network is connected. 107 | 108 | Returns: 109 | True: Connected. 110 | False: Not connected. 111 | """ 112 | return super().isconnected() 113 | 114 | def disconnect(self): 115 | """ 116 | Disconnects from the network. 117 | """ 118 | if super().active(): 119 | super().disconnect() 120 | super().active(False) # Disable the Wi-Fi interface. 121 | 122 | @_active 123 | def ifconfig(self, *args, **kwargs) -> tuple: 124 | """ 125 | Gets or sets IP-level network interface parameters. 126 | 127 | Args: 128 | A tuple containing the IP address, subnet mask, gateway, and DNS server. The default value is None, which returns the network status. 129 | 130 | Returns: 131 | A tuple containing the IP address, subnet mask, gateway, and DNS server. 132 | 133 | Example: 134 | # Get IP configuration info. 135 | ip_config = client.ifconfig() 136 | 137 | # Set IP configuration info. 138 | client.ifconfig(('192.168.3.4', '255.255.255.0', '192.168.3.1', '8.8.8.8')) 139 | """ 140 | return super().ifconfig(*args, **kwargs) 141 | 142 | 143 | class AP(network.WLAN): 144 | def __init__(self): 145 | super().__init__(network.AP_IF) 146 | self.PM_NONE = 0 147 | self.PM_PERFORMANCE = 1 148 | self.PM_POWERSAVE = 2 149 | 150 | @_active 151 | def config(self, *args, **kwargs): 152 | """ 153 | Gets or sets general network interface parameters. 154 | 155 | Args: 156 | pm: Power management setting. 157 | AP.PM_NONE - High-performance mode (0). 158 | AP.PM_PERFORMANCE - Balanced mode (1) (default). 159 | AP.PM_POWERSAVE - Power-saving mode (2). 160 | mac: Wireless network MAC address. 161 | key: Connection password. Set the length to be greater than 8 or set it as None, '', or 0. When changing the password, the security is automatically changed as well (write-only, cannot read). 162 | hidden: Whether the network is hidden. 163 | 0 - Visible. 164 | 1 - Hidden. 165 | channel: Channel (generally 1-13). 166 | security: Authentication mode. 167 | 0 - Open. 168 | 1 - WEP. 169 | 2 - WPA-PSK. 170 | 3 - WPA2-PSK. 171 | 4 - WPA/WPA2-PSK. 172 | ssid: Wireless network name. 173 | hostname: Hostname, length should not exceed 32 characters. 174 | tx_power: Maximum transmit power (dBm), generally ranging from 2 to 21. 175 | 176 | Example: 177 | # Get PM configuration info. 178 | config = client.config('pm') 179 | 180 | # Set IP configuration info. 181 | client.config(pm = 2) 182 | """ 183 | if kwargs.get('key') is not None: 184 | if kwargs['key']: 185 | if super().config('security') == 0 and kwargs.get('security') is None: 186 | if len(kwargs['key']) >= 8: 187 | super().config(key=kwargs['key'], security=4) 188 | else: 189 | print('[ERROR] The password length should not be less than 8.') 190 | else: 191 | super().config(security=0) 192 | return super().config(*args, **kwargs) 193 | 194 | @_active 195 | def ifconfig(self, *args, **kwargs) -> tuple: 196 | """ 197 | Gets or sets IP-level network interface parameters. 198 | 199 | Args: 200 | A tuple containing the IP address, subnet mask, gateway, and DNS server. The default value is None, which returns the network status. 201 | 202 | Returns: 203 | A tuple containing the IP address, subnet mask, gateway, and DNS server. 204 | 205 | Example: 206 | # Get IP configuration info. 207 | ip_config = client.ifconfig() 208 | 209 | # Set IP configuration info. 210 | client.ifconfig(('192.168.4.1', '255.255.255.0', '192.168.4.1', '0.0.0.0')) 211 | """ 212 | return super().ifconfig(*args, **kwargs) 213 | 214 | def isconnected(self) -> bool: 215 | """ 216 | Checks if any device is connected. 217 | 218 | Returns: 219 | True: Device(s) connected. 220 | False: No device connected. 221 | """ 222 | return super().isconnected() 223 | 224 | def active(self, *args, **kwargs): 225 | """ 226 | Sets or gets the active status of WLAN. 227 | 228 | Args: 229 | True or False (default: None for getting the status). 230 | 231 | Returns: 232 | None / bool 233 | """ 234 | return super().active(*args, **kwargs) 235 | -------------------------------------------------------------------------------- /lib/easyweb_single.py: -------------------------------------------------------------------------------- 1 | # Github: https://github.com/funnygeeker/micropython-easyweb 2 | # Author: funnygeeker 3 | # Licence: MIT 4 | # Date: 2023/11/23 5 | # 6 | # 参考项目: 7 | # https://github.com/maysrp/micropython_webconfig 8 | # 9 | # 参考资料: 10 | # https://blog.csdn.net/wapecheng/article/details/93522153 11 | # https://blog.csdn.net/qq_42482078/article/details/131514743 12 | # https://blog.csdn.net/weixin_41665106/article/details/105599235 13 | import os 14 | import socket 15 | import binascii 16 | import ujson as json 17 | 18 | # 文件类型对照 19 | FILE_TYPE = { 20 | "txt": "text/plain", 21 | "htm": "text/html", 22 | "html": "text/html", 23 | "css": "text/css", 24 | "csv": "text/csv", 25 | "js": "application/javascript", 26 | "xml": "application/xml", 27 | "xhtml": "application/xhtml+xml", 28 | "json": "application/json", 29 | "zip": "application/zip", 30 | "pdf": "application/pdf", 31 | "ts": "application/typescript", 32 | "woff": "font/woff", 33 | "woff2": "font/woff2", 34 | "ttf": "font/ttf", 35 | "otf": "font/otf", 36 | "jpg": "image/jpeg", 37 | "jpeg": "image/jpeg", 38 | "png": "image/png", 39 | "gif": "image/gif", 40 | "svg": "image/svg+xml", 41 | "ico": "image/x-icon" 42 | } # 其他: "application/octet-stream" 43 | 44 | 45 | def exists(path): 46 | """文件是否存在""" 47 | try: 48 | os.stat(path) 49 | return True 50 | except: 51 | print('[ERROR] EasyWeb: File Not Exists - {}'.format(path)) 52 | return False 53 | 54 | 55 | def url_encode(url): 56 | """URL 编码""" 57 | encoded_url = '' 58 | for char in url: 59 | if char.isalpha() or char.isdigit() or char in ('-', '.', '_', '~'): 60 | encoded_url += char 61 | else: 62 | encoded_url += '%' + binascii.hexlify(char.encode('utf-8')).decode('utf-8').upper() 63 | return encoded_url 64 | 65 | 66 | def url_decode(encoded_url): 67 | """ 68 | URL 解码 69 | """ 70 | encoded_url = encoded_url.replace('+', ' ') 71 | # 如果 URL 中不包含'%', 则表示已解码,直接返回原始 URL 72 | if '%' not in encoded_url: 73 | return encoded_url 74 | blocks = encoded_url.split('%') # 以 '%' 分割 URL 75 | decoded_url = blocks[0] # 初始化解码后的 URL 76 | buffer = "" # 初始化缓冲区 77 | 78 | for b in blocks[1:]: 79 | if len(b) == 2: 80 | buffer += b[:2] # 如果是两位的十六进制数,加入缓冲区 81 | else: # 解码相应的十六进制数并将其解码的字符加入解码后的 URL 中 82 | decoded_url += binascii.unhexlify(buffer + b[:2]).decode('utf-8') 83 | buffer = "" # 清空缓冲区 84 | decoded_url += b[2:] # 将剩余部分直接加入解码后的 URL 中 85 | # 处理缓冲区尾部剩余的十六进制数 86 | if buffer: 87 | decoded_url += binascii.unhexlify(buffer).decode('utf-8') 88 | return decoded_url # 返回解码后的 URL 89 | 90 | 91 | class _Response: 92 | """ 93 | 表示 HTTP 响应的类 94 | """ 95 | STATUS_CODE = { 96 | 100: "Continue", 97 | 101: "Switching Protocols", 98 | 102: "Processing", 99 | 200: "OK", 100 | 201: "Created", 101 | 202: "Accepted", 102 | 203: "Non-Authoritative Information", 103 | 204: "No Content", 104 | 205: "Reset Content", 105 | 206: "Partial Content", 106 | 207: "Multi-Status", 107 | 208: "Already Reported", 108 | 226: "IM Used", 109 | 300: "Multiple Choices", 110 | 301: "Moved Permanently", 111 | 302: "Found", 112 | 303: "See Other", 113 | 304: "Not Modified", 114 | 305: "Use Proxy", 115 | 307: "Temporary Redirect", 116 | 308: "Permanent Redirect", 117 | 400: "Bad Request", 118 | 401: "Unauthorized", 119 | 402: "Payment Required", 120 | 403: "Forbidden", 121 | 404: "Not Found", 122 | 405: "Method Not Allowed", 123 | 406: "Not Acceptable", 124 | 407: "Proxy Authentication Required", 125 | 408: "Request Timeout", 126 | 409: "Conflict", 127 | 410: "Gone", 128 | 411: "Length Required", 129 | 412: "Precondition Failed", 130 | 413: "Payload Too Large", 131 | 414: "URI Too Long", 132 | 415: "Unsupported Media Type", 133 | 416: "Range Not Satisfiable", 134 | 417: "Expectation Failed", 135 | 418: "I'm a teapot", 136 | 422: "Unprocessable Entity", 137 | 423: "Locked", 138 | 424: "Failed Dependency", 139 | 426: "Upgrade Required", 140 | 428: "Precondition Required", 141 | 429: "Too Many Requests", 142 | 431: "Request Header Fields Too Large", 143 | 451: "Unavailable For Legal Reasons", 144 | 500: "Internal Server Error", 145 | 501: "Not Implemented", 146 | 502: "Bad Gateway", 147 | 503: "Service Unavailable", 148 | 504: "Gateway Timeout", 149 | 505: "HTTP Version Not Supported", 150 | 506: "Variant Also Negotiates", 151 | 507: "Insufficient Storage", 152 | 508: "Loop Detected", 153 | 510: "Not Extended", 154 | 511: "Network Authentication Required" 155 | } 156 | 157 | def __init__(self): 158 | self.status_code = 200 159 | 'HTTP 状态码' 160 | self.status = None 161 | 'HTTP 状态文本' 162 | self.headers = {} 163 | self.cookies = {} 164 | self.charset = 'utf-8' 165 | self.data = b'' 166 | 167 | def set_data(self, data): 168 | """ 169 | 设置响应体数据 170 | 171 | Args: 172 | data: 数据,可以为 str, bytes, generator[bytes] 173 | """ 174 | if isinstance(data, str): 175 | self.data = data.encode() 176 | 177 | def set_cookie(self, name: str, value: str = '', max_age: int = None): 178 | """ 179 | 构造包含提供的 cookies 和 max_age(可选)的 Set-Cookie 响应头 180 | 181 | 参数: 182 | cookies: 包含 Cookie 键值对的字典。 183 | max_age: Cookie 最大有效期,以秒为单位。默认 None 184 | 185 | 返回: 186 | bytes: Set-Cookie 响应头 187 | """ 188 | if max_age is None: 189 | max_age = "" 190 | else: 191 | max_age = "; Max-Age={}".format(max_age) 192 | self.cookies[name] = "Set-Cookie: {}={}{}".format(url_encode(name), url_encode(value), max_age) 193 | 194 | @staticmethod 195 | def _generator(): 196 | """一个生成器""" 197 | yield 0 198 | 199 | def is_generator(self, obj): 200 | """判断对象是否为生成器""" 201 | return isinstance(obj, type(self._generator())) 202 | 203 | def _get_status(self): 204 | """获取状态码""" 205 | if self.status is not None: 206 | return self.status 207 | else: 208 | return "{} {}".format(self.status_code, self.STATUS_CODE.get(self.status_code, "NULL")) 209 | 210 | def _get_cookies(self): 211 | """获取 Cookies""" 212 | c = "" 213 | for k, v in self.cookies.items(): 214 | c += v 215 | c += "\r\n" 216 | return c 217 | 218 | def get_response(self): 219 | """ 220 | 获取完整的 HTTP 响应生成器 221 | 222 | Returns: 223 | 包含响应内容的生成器 224 | """ 225 | status = self._get_status() 226 | 227 | if isinstance(self.data, str): # 处理数据 228 | self.data = self.data.encode() 229 | self.headers['Content-Type'] = 'text/html' 230 | elif isinstance(self.data, dict): 231 | self.data = json.dumps(self.data).encode() 232 | self.headers['Content-Type'] = 'application/json' 233 | else: 234 | self.data = self.data 235 | 236 | if isinstance(status, bytes): # 响应头 237 | yield b"HTTP/1.1 " + status + b"\r\n" 238 | else: 239 | yield "HTTP/1.1 {}\r\n".format(status).encode() 240 | if isinstance(self.data, bytes): 241 | yield ("\r\n".join([f"{k}: {v}" for k, v in self.headers.items()]) + "\r\n").encode() 242 | yield (self._get_cookies() + "\r\n").encode() 243 | if self.data: 244 | yield self.data 245 | elif self.is_generator(self.data): 246 | i = True 247 | for d in self.data: 248 | if i: # 只执行一次 249 | i = False 250 | if type(d) == dict: 251 | self.headers.update(d) 252 | yield ("\r\n".join([f"{k}: {v}" for k, v in self.headers.items()]) + "\r\n").encode() 253 | yield (self._get_cookies() + "\r\n").encode() 254 | if type(d) != dict: 255 | yield d 256 | else: 257 | yield d 258 | else: 259 | print("[WARN] EasyWeb: Unsupported data type.") 260 | 261 | 262 | class _Request: 263 | """ 264 | 表示 HTTP 请求的类 265 | 266 | Attributes: 267 | path (str): 请求路径 268 | data (bytes): 请求体 269 | method (str): 请求方法,例如 GET / POST 270 | headers (dict): 请求头 271 | protocol (str): HTTP 协议版本 272 | full_path (str): 完整路径 273 | 274 | Properties: 275 | url (str / None): 获取请求中的 URL 276 | json (dict / None): 解析请求中的 JSON 277 | host (str / None): 获取请求中的 Host 278 | args (dict / None): 解析请求中的参数 279 | form (dict / None): 解析请求中的表单 280 | 281 | Note: 282 | 在解析数据时,如果出现异常,则返回 None。 283 | """ 284 | 285 | def __init__(self): 286 | self._url = None 287 | self._args = None 288 | self._form = None 289 | self._json = None 290 | self._cookies = {} 291 | self.path: str = '' 292 | '请求路径' 293 | self.data: bytes = b'' 294 | '请求体' 295 | self.method: str = "" 296 | '请求方法,例如 GET, POST' 297 | self.headers: dict = {} 298 | '请求头 (字典)' 299 | self.protocol: str = "" 300 | 'HTTP 协议版本' 301 | self.full_path: str = "" 302 | '完整路径' 303 | self.match: str = None 304 | '匹配的结果' 305 | 306 | @property 307 | def url(self): 308 | """ 309 | 获取请求中的 URL 310 | 311 | Returns: 312 | str / None: 当成功解析 URL 时返回 URL 字符串,否则返回 None(程序出错) 313 | 314 | Examples: 315 | request.url 316 | """ 317 | if self._url is None: 318 | host = self.host 319 | if host: 320 | self._url = "http://{}{}".format(host, self.full_path) 321 | return self._url 322 | 323 | @property 324 | def json(self): 325 | """ 326 | 解析请求中的 JSON 327 | 328 | Returns: 329 | dict / None: 当成功解析 JSON 时返回字典,否则返回 None(程序出错) 330 | 331 | Examples: 332 | request.json 333 | """ 334 | if self._json is None: 335 | try: 336 | self._json = json.loads(self.data) 337 | except: 338 | pass 339 | return self._json 340 | 341 | @property 342 | def host(self): 343 | """ 344 | 获取请求中的 Host 345 | 346 | Returns: 347 | str / None: 当成功解析 Host 时返回 Host 字符串,否则返回 None 348 | 349 | Examples: 350 | request.host 351 | """ 352 | return self.headers.get('Host') 353 | 354 | @property 355 | def args(self): 356 | """ 357 | 解析请求中的参数 358 | 359 | Returns: 360 | dict / None: 当成功解析参数时返回参数字典,否则返回 None(无法解析时或程序出错) 361 | 362 | Examples: 363 | request.args 364 | """ 365 | if self._args is None: 366 | try: 367 | # 提取问号后面的部分 368 | query_str = self.full_path.split('?', 1)[-1].rstrip("&") 369 | if query_str: 370 | args_list = query_str.split('&') # 分割参数键值对 371 | args = {} 372 | # 解析参数键值对 373 | for arg_pair in args_list: 374 | key, value = arg_pair.split('=') 375 | key = url_decode(key) 376 | value = url_decode(value) 377 | args[key] = value 378 | self._args = args # 缓存结果 379 | except: 380 | pass 381 | return self._args 382 | 383 | @property 384 | def form(self): 385 | """ 386 | 解析请求中的表单数据 387 | 388 | Returns: 389 | dict / None: 当成功解析表单数据时返回表单数据字典,否则返回 None(无法解析时或程序出错) 390 | 391 | Examples: 392 | request.form 393 | """ 394 | if self._form is None: 395 | try: 396 | items = self.data.decode("utf-8").split("&") 397 | form = {} 398 | for _ in items: 399 | k, v = _.split("=", 1) 400 | if not v: 401 | v = None 402 | else: 403 | v = url_decode(v) 404 | form[k] = v 405 | except: 406 | return None 407 | self._form = form # 缓存结果 408 | return self._form 409 | 410 | @property 411 | def cookies(self): 412 | """ 413 | 解析请求中的 Cookie 数据 414 | 415 | Returns: 416 | dict / None: 当成功解析 Cookies 数据时返回 Cookies 数据字典,否则返回 None(无法解析时或程序出错) 417 | 418 | Examples: 419 | request.cookies 420 | """ 421 | if not self._cookies: 422 | try: 423 | cookies = {} 424 | items = self.headers.get('Cookie').split(";") 425 | for item in items: 426 | item = item.strip() 427 | if '=' in item: 428 | k, v = item.split('=', 1) 429 | k = url_decode(k) 430 | v = url_decode(v) 431 | cookies[k] = v 432 | except: 433 | return {} 434 | self._cookies = cookies # 缓存结果 435 | return self._cookies 436 | 437 | 438 | class _HttpError(Exception): 439 | """ 440 | 表示 HTTP 错误的异常类。 441 | """ 442 | pass 443 | 444 | 445 | class EasyWeb: 446 | # HTTP 响应代码 447 | CODE_200 = b"HTTP/1.1 200 OK\r\n" 448 | CODE_404 = b"HTTP/1.1 404 Not Found\r\nContent-Type: text/html\r\n\r\n

Error 404: Page not found.

" 449 | CODE_405 = b"HTTP/1.1 405 Method Not Allowed\r\nContent-Type: text/html\r\n\r\n

Error 405: Method not allowed.

" 450 | 451 | def __init__(self): 452 | self.host = str 453 | '监听的 IP' 454 | self.port = int 455 | '监听的端口' 456 | self.routes = [] 457 | '路由表' 458 | self.server = None 459 | '服务器实例' 460 | 461 | def route(self, path: str, methods: list = None): 462 | """ 463 | 用于添加路由处理的装饰器 464 | 465 | Args: 466 | path: 用于匹配请求路径的字符串 467 | methods: 允许的请求方法列表,默认为 ['POST', 'GET'] 468 | 469 | Example: 470 | @app.route("/") 471 | def index(request): 472 | return "Hello, World!" 473 | 474 | Notes: 475 | 另外支持使用 "/" 和 ”/“ 对末尾的字符串或者路径进行匹配,可以通过 request.match 获取匹配的结果 476 | """ 477 | # 添加路由装饰器 478 | if methods is None: 479 | methods = ['POST', 'GET'] 480 | 481 | def decorator(func): 482 | self.routes.append((path, func, methods)) 483 | return func 484 | 485 | return decorator 486 | 487 | def run(self, host="0.0.0.0", port=80): 488 | """ 489 | 运行 Web 服务器 490 | 创建并启动循环,在其中运行服务器,监听客户端请求。 491 | 492 | Note: 493 | 当调用此方法后,服务器将运行并阻塞事件循环,直到使用 `Ctrl+C` 或者 `stop()` 等方式进行终止。 494 | """ 495 | self.host = host 496 | '监听的 IP' 497 | self.port = port 498 | '监听的端口' 499 | self.server = True 500 | # 创建套接字并监听连接 501 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 502 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 503 | s.bind((self.host, self.port)) 504 | s.listen(5) 505 | # 循环处理连接 506 | while self.server: 507 | conn, addr = s.accept() 508 | self.handle(conn) 509 | 510 | def handle(self, conn): 511 | """ 512 | 处理客户端的请求并生成对应的响应。 513 | 514 | Args: 515 | conn: 用于从客户端读取请求数据和发送响应的对象 516 | """ 517 | request = _Request() 518 | conn.settimeout(5) 519 | try: 520 | raw = conn.readline() # HTTP 请求方法,路径,协议版本 521 | raw = raw.decode('utf-8').split(" ") 522 | if len(raw) != 3: 523 | return 524 | raw[2] = raw[2].rstrip("\r\n") 525 | request.method, request.full_path, request.protocol = raw 526 | request.path = request.full_path.split('?', 1)[0] 527 | # 协议版本检查 528 | if request.protocol not in ("HTTP/1.0", "HTTP/1.1"): 529 | raise _HttpError(request.protocol, 505, "Version Not Supported") 530 | # 解析 HTTP 请求头 531 | while True: 532 | raw = conn.readline() # 请求头参数:\r\n 533 | raw = raw.decode('utf-8').rstrip('\r\n').split(": ", 1) 534 | if len(raw) == 2: 535 | k, v = raw 536 | request.headers[k] = v 537 | elif len(raw) == 1: # 请求头结束:\r\n\r\n 538 | break 539 | else: 540 | pass 541 | # 查找匹配路由 542 | for route_path, route_func, route_methods in self.routes: 543 | # 匹配路由 "/" 和 "/" "/" 544 | # 完全匹配 545 | if route_path.rstrip("/") == request.path.rstrip("/"): 546 | request.match = None 547 | match_success = True 548 | # 匹配字符串 549 | elif route_path[-9:] == "/" and route_path[:-9] == "/".join( 550 | request.path.rstrip("/").split("/")[:-1]): 551 | request.match = request.path[len(route_path[:-9]) + 1:] 552 | match_success = True 553 | # 匹配路径 554 | elif (route_path[-7:] == "/" and 555 | "/".join(request.path.rstrip("/").split("/")[:-1]).startswith(route_path[:-7]) and 556 | request.path[len(route_path[:-7]) + 1:]): 557 | request.match = request.path[len(route_path[:-7]) + 1:] 558 | match_success = True 559 | else: 560 | match_success = False 561 | if match_success: # 完全匹配,字符串匹配,路径匹配 562 | if request.method in route_methods: # 匹配到路由 563 | # 获取请求体 564 | size = int(request.headers.get("Content-Length", 0)) 565 | if size: 566 | request.data = conn.read(size) 567 | else: 568 | request.data = None 569 | # 调用路由处理函数并发送响应 570 | response = route_func(request) # str / bytes / generator / None 571 | try: 572 | response.get_response 573 | except AttributeError: 574 | if isinstance(response, tuple): # return response, status_code, headers 575 | try: 576 | response[0].get_response 577 | status_code = response[1] 578 | if len(response) == 3: 579 | headers = response[2] 580 | else: 581 | headers = None 582 | response = response[0] 583 | response.status_code = status_code 584 | if headers: 585 | response.headers.update(headers) 586 | except AttributeError: 587 | if len(response) == 2: 588 | response = make_response(response[0], response[1]) 589 | elif len(response) == 3: 590 | response = make_response(response[0], response[1], response[2]) 591 | else: # return bytes / str / iterables / tuple (bytes / str / iterables, status_code, headersr) 592 | response = make_response(response) 593 | 594 | for res in response.get_response(): 595 | conn.sendall(res) 596 | else: 597 | # 发送"方法不允许"响应 598 | conn.sendall(self.CODE_405) 599 | break 600 | else: 601 | # 发送"页面不存在"响应 602 | conn.sendall(self.CODE_404) 603 | except OSError: 604 | pass 605 | except Exception as e: 606 | print("[WARN] EasyWEB: {}".format(e)) 607 | finally: 608 | # 关闭连接 609 | conn.close() 610 | 611 | def stop(self): 612 | """ 613 | 停止运行 EasyWeb Server 614 | """ 615 | if self.server: 616 | self.server = None 617 | 618 | 619 | def send_file(file, mimetype: str = None, as_attachment=False, attachment_filename=None): 620 | """ 621 | 发送文件给客户端 622 | 623 | Args: 624 | file: 要发送的文件的路径 625 | mimetype: 文件的 MIME 类型。如果未指定,EasyWeb 将尝试根据文件扩展名进行猜测 626 | as_attachment: 是否作为附件发送文件,作为附件时会被下载 627 | attachment_filename: 下载文件时向用户显示的文件名。如果未提供,将使用原始文件名 628 | 629 | Returns: 630 | 包含 HTTP 200 OK 和 文件的二进制数据 的可迭代对象 631 | """ 632 | head = {'Content-Type': 'application/octet-stream'} 633 | if as_attachment: # 作为附件发送文件 634 | if not attachment_filename: # 下载文件时的文件名 635 | attachment_filename = file.split("/")[-1] 636 | head['Content-Disposition'] = 'attachment; filename="{}"'.format(attachment_filename) 637 | else: 638 | if mimetype is None: # 自动识别文件的 MIME 类型 639 | e = file.split(".") 640 | if len(e) >= 2: 641 | head['Content-Type'] = FILE_TYPE.get(e[-1], "application/octet-stream") 642 | if not exists(file): 643 | yield {'Content-Type': 'text/html'} 644 | yield '

File Not Exists: {}

'.format(file).encode("utf-8") 645 | else: 646 | yield head 647 | with open(file, "rb") as f: 648 | _file = True 649 | while _file: 650 | _file = f.read(1024) 651 | yield _file 652 | 653 | 654 | def render_template(file, **kwargs): 655 | """ 656 | 渲染模板 657 | 658 | Args: 659 | file: 要渲染的 html 模板路径 660 | **kwargs: 传递给函数的其他关键字参数,将在模板中用于渲染 661 | 662 | Returns: 663 | 包含 HTTP 200 OK 和渲染后的 HTML 内容字符串 的可迭代对象 664 | """ 665 | yield {'Content-Type': 'text/html'} 666 | if not exists(file): 667 | yield '

File Not Exists: {}

'.format(file).encode("utf-8") 668 | else: 669 | with open(file, "r") as f: 670 | _file = True 671 | f_readline = f.readline 672 | while _file: 673 | _file = f_readline() + f_readline() + f_readline() + f_readline() + f_readline() # 五行一组进行渲染 674 | for k, v in kwargs.items(): # 渲染参数 675 | _file = _file.replace("{{{{{}}}}}".format(k), str(v)) 676 | yield _file.encode("utf-8") 677 | 678 | 679 | def make_response(content=b'', status_code: int = 200, headers=None) -> _Response: 680 | """ 681 | 创建一个带有 内容、状态码 和 头部 的 响应对象。 682 | 683 | Args: 684 | content: 响应的内容,可以为 Iterable (bytes),str,tuple,dict 685 | status_code (int): 响应的状态码,默认为 200。 686 | headers: 可选的头部,包含在响应中,默认为 None。 687 | 688 | Returns: 689 | _Response: 响应对象 690 | """ 691 | if headers is None: 692 | headers = {} 693 | response = _Response() 694 | if isinstance(content, tuple): 695 | if len(content) >= 2: 696 | content, status_code = content[:2] 697 | if len(content) == 3: 698 | headers = content[2] 699 | response.headers = headers 700 | response.status_code = status_code 701 | response.data = content 702 | return response 703 | -------------------------------------------------------------------------------- /lib/easyweb_thread.py: -------------------------------------------------------------------------------- 1 | # Github: https://github.com/funnygeeker/micropython-easyweb 2 | # Author: funnygeeker 3 | # Licence: MIT 4 | # Date: 2023/11/23 5 | # 6 | # 参考项目: 7 | # https://github.com/maysrp/micropython_webconfig 8 | # 9 | # 参考资料: 10 | # https://blog.csdn.net/wapecheng/article/details/93522153 11 | # https://blog.csdn.net/qq_42482078/article/details/131514743 12 | # https://blog.csdn.net/weixin_41665106/article/details/105599235 13 | import os 14 | import socket 15 | import _thread 16 | import binascii 17 | import ujson as json 18 | 19 | # 文件类型对照 20 | FILE_TYPE = { 21 | "txt": "text/plain", 22 | "htm": "text/html", 23 | "html": "text/html", 24 | "css": "text/css", 25 | "csv": "text/csv", 26 | "js": "application/javascript", 27 | "xml": "application/xml", 28 | "xhtml": "application/xhtml+xml", 29 | "json": "application/json", 30 | "zip": "application/zip", 31 | "pdf": "application/pdf", 32 | "ts": "application/typescript", 33 | "woff": "font/woff", 34 | "woff2": "font/woff2", 35 | "ttf": "font/ttf", 36 | "otf": "font/otf", 37 | "jpg": "image/jpeg", 38 | "jpeg": "image/jpeg", 39 | "png": "image/png", 40 | "gif": "image/gif", 41 | "svg": "image/svg+xml", 42 | "ico": "image/x-icon" 43 | } # 其他: "application/octet-stream" 44 | 45 | 46 | def exists(path): 47 | """文件是否存在""" 48 | try: 49 | os.stat(path) 50 | return True 51 | except: 52 | print('[ERROR] EasyWeb: File Not Exists - {}'.format(path)) 53 | return False 54 | 55 | 56 | def url_encode(url): 57 | """URL 编码""" 58 | encoded_url = '' 59 | for char in url: 60 | if char.isalpha() or char.isdigit() or char in ('-', '.', '_', '~'): 61 | encoded_url += char 62 | else: 63 | encoded_url += '%' + binascii.hexlify(char.encode('utf-8')).decode('utf-8').upper() 64 | return encoded_url 65 | 66 | 67 | def url_decode(encoded_url): 68 | """ 69 | URL 解码 70 | """ 71 | encoded_url = encoded_url.replace('+', ' ') 72 | # 如果 URL 中不包含'%', 则表示已解码,直接返回原始 URL 73 | if '%' not in encoded_url: 74 | return encoded_url 75 | blocks = encoded_url.split('%') # 以 '%' 分割 URL 76 | decoded_url = blocks[0] # 初始化解码后的 URL 77 | buffer = "" # 初始化缓冲区 78 | 79 | for b in blocks[1:]: 80 | if len(b) == 2: 81 | buffer += b[:2] # 如果是两位的十六进制数,加入缓冲区 82 | else: # 解码相应的十六进制数并将其解码的字符加入解码后的 URL 中 83 | decoded_url += binascii.unhexlify(buffer + b[:2]).decode('utf-8') 84 | buffer = "" # 清空缓冲区 85 | decoded_url += b[2:] # 将剩余部分直接加入解码后的 URL 中 86 | # 处理缓冲区尾部剩余的十六进制数 87 | if buffer: 88 | decoded_url += binascii.unhexlify(buffer).decode('utf-8') 89 | return decoded_url # 返回解码后的 URL 90 | 91 | 92 | class _Response: 93 | """ 94 | 表示 HTTP 响应的类 95 | """ 96 | STATUS_CODE = { 97 | 100: "Continue", 98 | 101: "Switching Protocols", 99 | 102: "Processing", 100 | 200: "OK", 101 | 201: "Created", 102 | 202: "Accepted", 103 | 203: "Non-Authoritative Information", 104 | 204: "No Content", 105 | 205: "Reset Content", 106 | 206: "Partial Content", 107 | 207: "Multi-Status", 108 | 208: "Already Reported", 109 | 226: "IM Used", 110 | 300: "Multiple Choices", 111 | 301: "Moved Permanently", 112 | 302: "Found", 113 | 303: "See Other", 114 | 304: "Not Modified", 115 | 305: "Use Proxy", 116 | 307: "Temporary Redirect", 117 | 308: "Permanent Redirect", 118 | 400: "Bad Request", 119 | 401: "Unauthorized", 120 | 402: "Payment Required", 121 | 403: "Forbidden", 122 | 404: "Not Found", 123 | 405: "Method Not Allowed", 124 | 406: "Not Acceptable", 125 | 407: "Proxy Authentication Required", 126 | 408: "Request Timeout", 127 | 409: "Conflict", 128 | 410: "Gone", 129 | 411: "Length Required", 130 | 412: "Precondition Failed", 131 | 413: "Payload Too Large", 132 | 414: "URI Too Long", 133 | 415: "Unsupported Media Type", 134 | 416: "Range Not Satisfiable", 135 | 417: "Expectation Failed", 136 | 418: "I'm a teapot", 137 | 422: "Unprocessable Entity", 138 | 423: "Locked", 139 | 424: "Failed Dependency", 140 | 426: "Upgrade Required", 141 | 428: "Precondition Required", 142 | 429: "Too Many Requests", 143 | 431: "Request Header Fields Too Large", 144 | 451: "Unavailable For Legal Reasons", 145 | 500: "Internal Server Error", 146 | 501: "Not Implemented", 147 | 502: "Bad Gateway", 148 | 503: "Service Unavailable", 149 | 504: "Gateway Timeout", 150 | 505: "HTTP Version Not Supported", 151 | 506: "Variant Also Negotiates", 152 | 507: "Insufficient Storage", 153 | 508: "Loop Detected", 154 | 510: "Not Extended", 155 | 511: "Network Authentication Required" 156 | } 157 | 158 | def __init__(self): 159 | self.status_code = 200 160 | 'HTTP 状态码' 161 | self.status = None 162 | 'HTTP 状态文本' 163 | self.headers = {} 164 | self.cookies = {} 165 | self.charset = 'utf-8' 166 | self.data = b'' 167 | 168 | def set_data(self, data): 169 | """ 170 | 设置响应体数据 171 | 172 | Args: 173 | data: 数据,可以为 str, bytes, generator[bytes] 174 | """ 175 | if isinstance(data, str): 176 | self.data = data.encode() 177 | 178 | def set_cookie(self, name: str, value: str = '', max_age: int = None): 179 | """ 180 | 构造包含提供的 cookies 和 max_age(可选)的 Set-Cookie 响应头 181 | 182 | 参数: 183 | cookies: 包含 Cookie 键值对的字典。 184 | max_age: Cookie 最大有效期,以秒为单位。默认 None 185 | 186 | 返回: 187 | bytes: Set-Cookie 响应头 188 | """ 189 | if max_age is None: 190 | max_age = "" 191 | else: 192 | max_age = "; Max-Age={}".format(max_age) 193 | self.cookies[name] = "Set-Cookie: {}={}{}".format(url_encode(name), url_encode(value), max_age) 194 | 195 | @staticmethod 196 | def _generator(): 197 | """一个生成器""" 198 | yield 0 199 | 200 | def is_generator(self, obj): 201 | """判断对象是否为生成器""" 202 | return isinstance(obj, type(self._generator())) 203 | 204 | def _get_status(self): 205 | """获取状态码""" 206 | if self.status is not None: 207 | return self.status 208 | else: 209 | return "{} {}".format(self.status_code, self.STATUS_CODE.get(self.status_code, "NULL")) 210 | 211 | def _get_cookies(self): 212 | """获取 Cookies""" 213 | c = "" 214 | for k, v in self.cookies.items(): 215 | c += v 216 | c += "\r\n" 217 | return c 218 | 219 | def get_response(self): 220 | """ 221 | 获取完整的 HTTP 响应生成器 222 | 223 | Returns: 224 | 包含响应内容的生成器 225 | """ 226 | status = self._get_status() 227 | 228 | if isinstance(self.data, str): # 处理数据 229 | self.data = self.data.encode() 230 | self.headers['Content-Type'] = 'text/html' 231 | elif isinstance(self.data, dict): 232 | self.data = json.dumps(self.data).encode() 233 | self.headers['Content-Type'] = 'application/json' 234 | else: 235 | self.data = self.data 236 | 237 | if isinstance(status, bytes): # 响应头 238 | yield b"HTTP/1.1 " + status + b"\r\n" 239 | else: 240 | yield "HTTP/1.1 {}\r\n".format(status).encode() 241 | if isinstance(self.data, bytes): 242 | yield ("\r\n".join([f"{k}: {v}" for k, v in self.headers.items()]) + "\r\n").encode() 243 | yield (self._get_cookies() + "\r\n").encode() 244 | if self.data: 245 | yield self.data 246 | elif self.is_generator(self.data): 247 | i = True 248 | for d in self.data: 249 | if i: # 只执行一次 250 | i = False 251 | if type(d) == dict: 252 | self.headers.update(d) 253 | yield ("\r\n".join([f"{k}: {v}" for k, v in self.headers.items()]) + "\r\n").encode() 254 | yield (self._get_cookies() + "\r\n").encode() 255 | if type(d) != dict: 256 | yield d 257 | else: 258 | yield d 259 | else: 260 | print("[WARN] EasyWeb: Unsupported data type.") 261 | 262 | 263 | class _Request: 264 | """ 265 | 表示 HTTP 请求的类 266 | 267 | Attributes: 268 | path (str): 请求路径 269 | data (bytes): 请求体 270 | method (str): 请求方法,例如 GET / POST 271 | headers (dict): 请求头 272 | protocol (str): HTTP 协议版本 273 | full_path (str): 完整路径 274 | 275 | Properties: 276 | url (str / None): 获取请求中的 URL 277 | json (dict / None): 解析请求中的 JSON 278 | host (str / None): 获取请求中的 Host 279 | args (dict / None): 解析请求中的参数 280 | form (dict / None): 解析请求中的表单 281 | 282 | Note: 283 | 在解析数据时,如果出现异常,则返回 None。 284 | """ 285 | 286 | def __init__(self): 287 | self._url = None 288 | self._args = None 289 | self._form = None 290 | self._json = None 291 | self._cookies = {} 292 | self.path: str = '' 293 | '请求路径' 294 | self.data: bytes = b'' 295 | '请求体' 296 | self.method: str = "" 297 | '请求方法,例如 GET, POST' 298 | self.headers: dict = {} 299 | '请求头 (字典)' 300 | self.protocol: str = "" 301 | 'HTTP 协议版本' 302 | self.full_path: str = "" 303 | '完整路径' 304 | self.match: str = None 305 | '匹配的结果' 306 | 307 | @property 308 | def url(self): 309 | """ 310 | 获取请求中的 URL 311 | 312 | Returns: 313 | str / None: 当成功解析 URL 时返回 URL 字符串,否则返回 None(程序出错) 314 | 315 | Examples: 316 | request.url 317 | """ 318 | if self._url is None: 319 | host = self.host 320 | if host: 321 | self._url = "http://{}{}".format(host, self.full_path) 322 | return self._url 323 | 324 | @property 325 | def json(self): 326 | """ 327 | 解析请求中的 JSON 328 | 329 | Returns: 330 | dict / None: 当成功解析 JSON 时返回字典,否则返回 None(程序出错) 331 | 332 | Examples: 333 | request.json 334 | """ 335 | if self._json is None: 336 | try: 337 | self._json = json.loads(self.data) 338 | except: 339 | pass 340 | return self._json 341 | 342 | @property 343 | def host(self): 344 | """ 345 | 获取请求中的 Host 346 | 347 | Returns: 348 | str / None: 当成功解析 Host 时返回 Host 字符串,否则返回 None 349 | 350 | Examples: 351 | request.host 352 | """ 353 | return self.headers.get('Host') 354 | 355 | @property 356 | def args(self): 357 | """ 358 | 解析请求中的参数 359 | 360 | Returns: 361 | dict / None: 当成功解析参数时返回参数字典,否则返回 None(无法解析时或程序出错) 362 | 363 | Examples: 364 | request.args 365 | """ 366 | if self._args is None: 367 | try: 368 | # 提取问号后面的部分 369 | query_str = self.full_path.split('?', 1)[-1].rstrip("&") 370 | if query_str: 371 | args_list = query_str.split('&') # 分割参数键值对 372 | args = {} 373 | # 解析参数键值对 374 | for arg_pair in args_list: 375 | key, value = arg_pair.split('=') 376 | key = url_decode(key) 377 | value = url_decode(value) 378 | args[key] = value 379 | self._args = args # 缓存结果 380 | except: 381 | pass 382 | return self._args 383 | 384 | @property 385 | def form(self): 386 | """ 387 | 解析请求中的表单数据 388 | 389 | Returns: 390 | dict / None: 当成功解析表单数据时返回表单数据字典,否则返回 None(无法解析时或程序出错) 391 | 392 | Examples: 393 | request.form 394 | """ 395 | if self._form is None: 396 | try: 397 | items = self.data.decode("utf-8").split("&") 398 | form = {} 399 | for _ in items: 400 | k, v = _.split("=", 1) 401 | if not v: 402 | v = None 403 | else: 404 | v = url_decode(v) 405 | form[k] = v 406 | except: 407 | return None 408 | self._form = form # 缓存结果 409 | return self._form 410 | 411 | @property 412 | def cookies(self): 413 | """ 414 | 解析请求中的 Cookie 数据 415 | 416 | Returns: 417 | dict / None: 当成功解析 Cookies 数据时返回 Cookies 数据字典,否则返回 None(无法解析时或程序出错) 418 | 419 | Examples: 420 | request.cookies 421 | """ 422 | if not self._cookies: 423 | try: 424 | cookies = {} 425 | items = self.headers.get('Cookie').split(";") 426 | for item in items: 427 | item = item.strip() 428 | if '=' in item: 429 | k, v = item.split('=', 1) 430 | k = url_decode(k) 431 | v = url_decode(v) 432 | cookies[k] = v 433 | except: 434 | return {} 435 | self._cookies = cookies # 缓存结果 436 | return self._cookies 437 | 438 | 439 | class _HttpError(Exception): 440 | """ 441 | 表示 HTTP 错误的异常类。 442 | """ 443 | pass 444 | 445 | 446 | class EasyWeb: 447 | # HTTP 响应代码 448 | CODE_200 = b"HTTP/1.1 200 OK\r\n" 449 | CODE_404 = b"HTTP/1.1 404 Not Found\r\nContent-Type: text/html\r\n\r\n

Error 404: Page not found.

" 450 | CODE_405 = b"HTTP/1.1 405 Method Not Allowed\r\nContent-Type: text/html\r\n\r\n

Error 405: Method not allowed.

" 451 | 452 | def __init__(self): 453 | self.host = str 454 | '监听的 IP' 455 | self.port = int 456 | '监听的端口' 457 | self.routes = [] 458 | '路由表' 459 | self.server = None 460 | '服务器实例' 461 | 462 | def route(self, path: str, methods: list = None): 463 | """ 464 | 用于添加路由处理的装饰器 465 | 466 | Args: 467 | path: 用于匹配请求路径的字符串 468 | methods: 允许的请求方法列表,默认为 ['POST', 'GET'] 469 | 470 | Example: 471 | @app.route("/") 472 | def index(request): 473 | return "Hello, World!" 474 | 475 | Notes: 476 | 另外支持使用 "/" 和 ”/“ 对末尾的字符串或者路径进行匹配,可以通过 request.match 获取匹配的结果 477 | """ 478 | # 添加路由装饰器 479 | if methods is None: 480 | methods = ['POST', 'GET'] 481 | 482 | def decorator(func): 483 | self.routes.append((path, func, methods)) 484 | return func 485 | 486 | return decorator 487 | 488 | def run(self, host="0.0.0.0", port=80): 489 | """ 490 | 运行 Web 服务器 491 | 创建并启动循环,在其中运行服务器,监听客户端请求。 492 | 493 | Note: 494 | 当调用此方法后,服务器将运行并阻塞事件循环,直到使用 `Ctrl+C` 或者 `stop()` 等方式进行终止。 495 | """ 496 | self.host = host 497 | '监听的 IP' 498 | self.port = port 499 | '监听的端口' 500 | self.server = True 501 | # 创建套接字并监听连接 502 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 503 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 504 | s.bind((self.host, self.port)) 505 | s.listen(5) 506 | # 循环处理连接 507 | while self.server: 508 | conn, addr = s.accept() 509 | _thread.start_new_thread(self.handle, (conn,)) 510 | 511 | def handle(self, conn): 512 | """ 513 | 处理客户端的请求并生成对应的响应。 514 | 515 | Args: 516 | conn: 用于从客户端读取请求数据和发送响应的对象 517 | """ 518 | request = _Request() 519 | conn.settimeout(5) 520 | try: 521 | raw = conn.readline() # HTTP 请求方法,路径,协议版本 522 | raw = raw.decode('utf-8').split(" ") 523 | if len(raw) != 3: 524 | return 525 | raw[2] = raw[2].rstrip("\r\n") 526 | request.method, request.full_path, request.protocol = raw 527 | request.path = request.full_path.split('?', 1)[0] 528 | # 协议版本检查 529 | if request.protocol not in ("HTTP/1.0", "HTTP/1.1"): 530 | raise _HttpError(request.protocol, 505, "Version Not Supported") 531 | # 解析 HTTP 请求头 532 | while True: 533 | raw = conn.readline() # 请求头参数:\r\n 534 | raw = raw.decode('utf-8').rstrip('\r\n').split(": ", 1) 535 | if len(raw) == 2: 536 | k, v = raw 537 | request.headers[k] = v 538 | elif len(raw) == 1: # 请求头结束:\r\n\r\n 539 | break 540 | else: 541 | pass 542 | # 查找匹配路由 543 | for route_path, route_func, route_methods in self.routes: 544 | # 匹配路由 "/" 和 "/" "/" 545 | # 完全匹配 546 | if route_path.rstrip("/") == request.path.rstrip("/"): 547 | request.match = None 548 | match_success = True 549 | # 匹配字符串 550 | elif route_path[-9:] == "/" and route_path[:-9] == "/".join( 551 | request.path.rstrip("/").split("/")[:-1]): 552 | request.match = request.path[len(route_path[:-9]) + 1:] 553 | match_success = True 554 | # 匹配路径 555 | elif (route_path[-7:] == "/" and 556 | "/".join(request.path.rstrip("/").split("/")[:-1]).startswith(route_path[:-7]) and 557 | request.path[len(route_path[:-7]) + 1:]): 558 | request.match = request.path[len(route_path[:-7]) + 1:] 559 | match_success = True 560 | else: 561 | match_success = False 562 | if match_success: # 完全匹配,字符串匹配,路径匹配 563 | if request.method in route_methods: # 匹配到路由 564 | # 获取请求体 565 | size = int(request.headers.get("Content-Length", 0)) 566 | if size: 567 | request.data = conn.read(size) 568 | else: 569 | request.data = None 570 | # 调用路由处理函数并发送响应 571 | response = route_func(request) # str / bytes / generator / None 572 | try: 573 | response.get_response 574 | except AttributeError: 575 | if isinstance(response, tuple): # return response, status_code, headers 576 | try: 577 | response[0].get_response 578 | status_code = response[1] 579 | if len(response) == 3: 580 | headers = response[2] 581 | else: 582 | headers = None 583 | response = response[0] 584 | response.status_code = status_code 585 | if headers: 586 | response.headers.update(headers) 587 | except AttributeError: 588 | if len(response) == 2: 589 | response = make_response(response[0], response[1]) 590 | elif len(response) == 3: 591 | response = make_response(response[0], response[1], response[2]) 592 | else: # return bytes / str / iterables / tuple (bytes / str / iterables, status_code, headersr) 593 | response = make_response(response) 594 | 595 | for res in response.get_response(): 596 | conn.sendall(res) 597 | else: 598 | # 发送"方法不允许"响应 599 | conn.sendall(self.CODE_405) 600 | break 601 | else: 602 | # 发送"页面不存在"响应 603 | conn.sendall(self.CODE_404) 604 | except OSError: 605 | pass 606 | except Exception as e: 607 | print("[WARN] EasyWEB: {}".format(e)) 608 | finally: 609 | # 关闭连接 610 | conn.close() 611 | 612 | def stop(self): 613 | """ 614 | 停止运行 EasyWeb Server 615 | """ 616 | if self.server: 617 | self.server = None 618 | _thread.exit() 619 | 620 | 621 | def send_file(file, mimetype: str = None, as_attachment=False, attachment_filename=None): 622 | """ 623 | 发送文件给客户端 624 | 625 | Args: 626 | file: 要发送的文件的路径 627 | mimetype: 文件的 MIME 类型。如果未指定,EasyWeb 将尝试根据文件扩展名进行猜测 628 | as_attachment: 是否作为附件发送文件,作为附件时会被下载 629 | attachment_filename: 下载文件时向用户显示的文件名。如果未提供,将使用原始文件名 630 | 631 | Returns: 632 | 包含 HTTP 200 OK 和 文件的二进制数据 的可迭代对象 633 | """ 634 | head = {'Content-Type': 'application/octet-stream'} 635 | if as_attachment: # 作为附件发送文件 636 | if not attachment_filename: # 下载文件时的文件名 637 | attachment_filename = file.split("/")[-1] 638 | head['Content-Disposition'] = 'attachment; filename="{}"'.format(attachment_filename) 639 | else: 640 | if mimetype is None: # 自动识别文件的 MIME 类型 641 | e = file.split(".") 642 | if len(e) >= 2: 643 | head['Content-Type'] = FILE_TYPE.get(e[-1], "application/octet-stream") 644 | if not exists(file): 645 | yield {'Content-Type': 'text/html'} 646 | yield '

File Not Exists: {}

'.format(file).encode("utf-8") 647 | else: 648 | yield head 649 | with open(file, "rb") as f: 650 | _file = True 651 | while _file: 652 | _file = f.read(1024) 653 | yield _file 654 | 655 | 656 | def render_template(file, **kwargs): 657 | """ 658 | 渲染模板 659 | 660 | Args: 661 | file: 要渲染的 html 模板路径 662 | **kwargs: 传递给函数的其他关键字参数,将在模板中用于渲染 663 | 664 | Returns: 665 | 包含 HTTP 200 OK 和渲染后的 HTML 内容字符串 的可迭代对象 666 | """ 667 | yield {'Content-Type': 'text/html'} 668 | if not exists(file): 669 | yield '

File Not Exists: {}

'.format(file).encode("utf-8") 670 | else: 671 | with open(file, "r") as f: 672 | _file = True 673 | f_readline = f.readline 674 | while _file: 675 | _file = f_readline() + f_readline() + f_readline() + f_readline() + f_readline() # 五行一组进行渲染 676 | for k, v in kwargs.items(): # 渲染参数 677 | _file = _file.replace("{{{{{}}}}}".format(k), str(v)) 678 | yield _file.encode("utf-8") 679 | 680 | 681 | def make_response(content=b'', status_code: int = 200, headers=None) -> _Response: 682 | """ 683 | 创建一个带有 内容、状态码 和 头部 的 响应对象。 684 | 685 | Args: 686 | content: 响应的内容,可以为 Iterable (bytes),str,tuple,dict 687 | status_code (int): 响应的状态码,默认为 200。 688 | headers: 可选的头部,包含在响应中,默认为 None。 689 | 690 | Returns: 691 | _Response: 响应对象 692 | """ 693 | if headers is None: 694 | headers = {} 695 | response = _Response() 696 | if isinstance(content, tuple): 697 | if len(content) >= 2: 698 | content, status_code = content[:2] 699 | if len(content) == 3: 700 | headers = content[2] 701 | response.headers = headers 702 | response.status_code = status_code 703 | response.data = content 704 | return response 705 | -------------------------------------------------------------------------------- /lib/easyweb.py: -------------------------------------------------------------------------------- 1 | # Github: https://github.com/funnygeeker/micropython-easyweb 2 | # Author: funnygeeker 3 | # Licence: MIT 4 | # Date: 2023/11/23 5 | # 6 | # 参考项目: 7 | # https://github.com/maysrp/micropython_webconfig 8 | # 9 | # 参考资料: 10 | # https://flask.palletsprojects.com/en/3.0.x/quickstart/ 11 | # https://blog.csdn.net/wapecheng/article/details/93522153 12 | # https://blog.csdn.net/qq_42482078/article/details/131514743 13 | # https://blog.csdn.net/weixin_41665106/article/details/105599235 14 | import os 15 | import binascii 16 | import ujson as json 17 | import uasyncio as asyncio 18 | 19 | # 文件类型对照 20 | FILE_TYPE = { 21 | "txt": "text/plain", 22 | "htm": "text/html", 23 | "html": "text/html", 24 | "css": "text/css", 25 | "csv": "text/csv", 26 | "js": "application/javascript", 27 | "xml": "application/xml", 28 | "xhtml": "application/xhtml+xml", 29 | "json": "application/json", 30 | "zip": "application/zip", 31 | "pdf": "application/pdf", 32 | "ts": "application/typescript", 33 | "woff": "font/woff", 34 | "woff2": "font/woff2", 35 | "ttf": "font/ttf", 36 | "otf": "font/otf", 37 | "jpg": "image/jpeg", 38 | "jpeg": "image/jpeg", 39 | "png": "image/png", 40 | "gif": "image/gif", 41 | "svg": "image/svg+xml", 42 | "ico": "image/x-icon" 43 | } # 其他: "application/octet-stream" 44 | 45 | 46 | def exists(path): 47 | """文件是否存在""" 48 | try: 49 | os.stat(path) 50 | return True 51 | except: 52 | print('[ERROR] EasyWeb: File Not Exists - {}'.format(path)) 53 | return False 54 | 55 | 56 | def url_encode(url): 57 | """URL 编码""" 58 | encoded_url = '' 59 | for char in url: 60 | if char.isalpha() or char.isdigit() or char in ('-', '.', '_', '~'): 61 | encoded_url += char 62 | else: 63 | encoded_url += '%' + binascii.hexlify(char.encode('utf-8')).decode('utf-8').upper() 64 | return encoded_url 65 | 66 | 67 | def url_decode(encoded_url): 68 | """ 69 | URL 解码 70 | """ 71 | encoded_url = encoded_url.replace('+', ' ') 72 | # 如果 URL 中不包含'%', 则表示已解码,直接返回原始 URL 73 | if '%' not in encoded_url: 74 | return encoded_url 75 | blocks = encoded_url.split('%') # 以 '%' 分割 URL 76 | decoded_url = blocks[0] # 初始化解码后的 URL 77 | buffer = "" # 初始化缓冲区 78 | 79 | for b in blocks[1:]: 80 | if len(b) == 2: 81 | buffer += b[:2] # 如果是两位的十六进制数,加入缓冲区 82 | else: # 解码相应的十六进制数并将其解码的字符加入解码后的 URL 中 83 | decoded_url += binascii.unhexlify(buffer + b[:2]).decode('utf-8') 84 | buffer = "" # 清空缓冲区 85 | decoded_url += b[2:] # 将剩余部分直接加入解码后的 URL 中 86 | # 处理缓冲区尾部剩余的十六进制数 87 | if buffer: 88 | decoded_url += binascii.unhexlify(buffer).decode('utf-8') 89 | return decoded_url # 返回解码后的 URL 90 | 91 | 92 | class _Response: 93 | """ 94 | 表示 HTTP 响应的类 95 | """ 96 | STATUS_CODE = { 97 | 100: "Continue", 98 | 101: "Switching Protocols", 99 | 102: "Processing", 100 | 200: "OK", 101 | 201: "Created", 102 | 202: "Accepted", 103 | 203: "Non-Authoritative Information", 104 | 204: "No Content", 105 | 205: "Reset Content", 106 | 206: "Partial Content", 107 | 207: "Multi-Status", 108 | 208: "Already Reported", 109 | 226: "IM Used", 110 | 300: "Multiple Choices", 111 | 301: "Moved Permanently", 112 | 302: "Found", 113 | 303: "See Other", 114 | 304: "Not Modified", 115 | 305: "Use Proxy", 116 | 307: "Temporary Redirect", 117 | 308: "Permanent Redirect", 118 | 400: "Bad Request", 119 | 401: "Unauthorized", 120 | 402: "Payment Required", 121 | 403: "Forbidden", 122 | 404: "Not Found", 123 | 405: "Method Not Allowed", 124 | 406: "Not Acceptable", 125 | 407: "Proxy Authentication Required", 126 | 408: "Request Timeout", 127 | 409: "Conflict", 128 | 410: "Gone", 129 | 411: "Length Required", 130 | 412: "Precondition Failed", 131 | 413: "Payload Too Large", 132 | 414: "URI Too Long", 133 | 415: "Unsupported Media Type", 134 | 416: "Range Not Satisfiable", 135 | 417: "Expectation Failed", 136 | 418: "I'm a teapot", 137 | 422: "Unprocessable Entity", 138 | 423: "Locked", 139 | 424: "Failed Dependency", 140 | 426: "Upgrade Required", 141 | 428: "Precondition Required", 142 | 429: "Too Many Requests", 143 | 431: "Request Header Fields Too Large", 144 | 451: "Unavailable For Legal Reasons", 145 | 500: "Internal Server Error", 146 | 501: "Not Implemented", 147 | 502: "Bad Gateway", 148 | 503: "Service Unavailable", 149 | 504: "Gateway Timeout", 150 | 505: "HTTP Version Not Supported", 151 | 506: "Variant Also Negotiates", 152 | 507: "Insufficient Storage", 153 | 508: "Loop Detected", 154 | 510: "Not Extended", 155 | 511: "Network Authentication Required" 156 | } 157 | 158 | def __init__(self): 159 | self.status_code = 200 160 | 'HTTP 状态码' 161 | self.status = None 162 | 'HTTP 状态文本' 163 | self.headers = {} 164 | self.cookies = {} 165 | self.charset = 'utf-8' 166 | self.data = b'' 167 | 168 | def set_data(self, data): 169 | """ 170 | 设置响应体数据 171 | 172 | Args: 173 | data: 数据,可以为 str, bytes, generator[bytes] 174 | """ 175 | if isinstance(data, str): 176 | self.data = data.encode() 177 | 178 | def set_cookie(self, name: str, value: str = '', max_age: int = None): 179 | """ 180 | 构造包含提供的 cookies 和 max_age(可选)的 Set-Cookie 响应头 181 | 182 | 参数: 183 | cookies: 包含 Cookie 键值对的字典。 184 | max_age: Cookie 最大有效期,以秒为单位。默认 None 185 | 186 | 返回: 187 | bytes: Set-Cookie 响应头 188 | """ 189 | if max_age is None: 190 | max_age = "" 191 | else: 192 | max_age = "; Max-Age={}".format(max_age) 193 | self.cookies[name] = "Set-Cookie: {}={}{}".format(url_encode(name), url_encode(value), max_age) 194 | 195 | @staticmethod 196 | def _generator(): 197 | """一个生成器""" 198 | yield 0 199 | 200 | def is_generator(self, obj): 201 | """判断对象是否为生成器""" 202 | return isinstance(obj, type(self._generator())) 203 | 204 | def _get_status(self): 205 | """获取状态码""" 206 | if self.status is not None: 207 | return self.status 208 | else: 209 | return "{} {}".format(self.status_code, self.STATUS_CODE.get(self.status_code, "NULL")) 210 | 211 | def _get_cookies(self): 212 | """获取 Cookies""" 213 | c = "" 214 | for k, v in self.cookies.items(): 215 | c += v 216 | c += "\r\n" 217 | return c 218 | 219 | def get_response(self): 220 | """ 221 | 获取完整的 HTTP 响应生成器 222 | 223 | Returns: 224 | 包含响应内容的生成器 225 | """ 226 | status = self._get_status() 227 | 228 | if isinstance(self.data, str): # 处理数据 229 | self.data = self.data.encode() 230 | self.headers['Content-Type'] = 'text/html' 231 | elif isinstance(self.data, dict): 232 | self.data = json.dumps(self.data).encode() 233 | self.headers['Content-Type'] = 'application/json' 234 | else: 235 | self.data = self.data 236 | 237 | if isinstance(status, bytes): # 响应头 238 | yield b"HTTP/1.1 " + status + b"\r\n" 239 | else: 240 | yield "HTTP/1.1 {}\r\n".format(status).encode() 241 | if isinstance(self.data, bytes): 242 | yield ("\r\n".join([f"{k}: {v}" for k, v in self.headers.items()]) + "\r\n").encode() 243 | yield (self._get_cookies() + "\r\n").encode() 244 | if self.data: 245 | yield self.data 246 | elif self.is_generator(self.data): 247 | i = True 248 | for d in self.data: 249 | if i: # 只执行一次 250 | i = False 251 | if type(d) == dict: 252 | self.headers.update(d) 253 | yield ("\r\n".join([f"{k}: {v}" for k, v in self.headers.items()]) + "\r\n").encode() 254 | yield (self._get_cookies() + "\r\n").encode() 255 | if type(d) != dict: 256 | yield d 257 | else: 258 | yield d 259 | else: 260 | print("[WARN] EasyWeb: Unsupported data type.") 261 | 262 | 263 | class _Request: 264 | """ 265 | 表示 HTTP 请求的类 266 | 267 | Attributes: 268 | path (str): 请求路径 269 | data (bytes): 请求体 270 | method (str): 请求方法,例如 GET / POST 271 | headers (dict): 请求头 272 | protocol (str): HTTP 协议版本 273 | full_path (str): 完整路径 274 | 275 | Properties: 276 | url (str / None): 获取请求中的 URL 277 | json (dict / None): 解析请求中的 JSON 278 | host (str / None): 获取请求中的 Host 279 | args (dict / None): 解析请求中的参数 280 | form (dict / None): 解析请求中的表单 281 | 282 | Note: 283 | 在解析数据时,如果出现异常,则返回 None。 284 | """ 285 | 286 | def __init__(self): 287 | self._url = None 288 | self._args = None 289 | self._form = None 290 | self._json = None 291 | self._cookies = {} 292 | self.path: str = '' 293 | '请求路径' 294 | self.data: bytes = b'' 295 | '请求体' 296 | self.method: str = "" 297 | '请求方法,例如 GET, POST' 298 | self.headers: dict = {} 299 | '请求头 (字典)' 300 | self.protocol: str = "" 301 | 'HTTP 协议版本' 302 | self.full_path: str = "" 303 | '完整路径' 304 | self.match: str = None 305 | '匹配的结果' 306 | 307 | @property 308 | def url(self): 309 | """ 310 | 获取请求中的 URL 311 | 312 | Returns: 313 | str / None: 当成功解析 URL 时返回 URL 字符串,否则返回 None(程序出错) 314 | 315 | Examples: 316 | request.url 317 | """ 318 | if self._url is None: 319 | host = self.host 320 | if host: 321 | self._url = "http://{}{}".format(host, self.full_path) 322 | return self._url 323 | 324 | @property 325 | def json(self): 326 | """ 327 | 解析请求中的 JSON 328 | 329 | Returns: 330 | dict / None: 当成功解析 JSON 时返回字典,否则返回 None(程序出错) 331 | 332 | Examples: 333 | request.json 334 | """ 335 | if self._json is None: 336 | try: 337 | self._json = json.loads(self.data) 338 | except: 339 | pass 340 | return self._json 341 | 342 | @property 343 | def host(self): 344 | """ 345 | 获取请求中的 Host 346 | 347 | Returns: 348 | str / None: 当成功解析 Host 时返回 Host 字符串,否则返回 None 349 | 350 | Examples: 351 | request.host 352 | """ 353 | return self.headers.get('Host') 354 | 355 | @property 356 | def args(self): 357 | """ 358 | 解析请求中的参数 359 | 360 | Returns: 361 | dict / None: 当成功解析参数时返回参数字典,否则返回 None(无法解析时或程序出错) 362 | 363 | Examples: 364 | request.args 365 | """ 366 | if self._args is None: 367 | try: 368 | # 提取问号后面的部分 369 | query_str = self.full_path.split('?', 1)[-1].rstrip("&") 370 | if query_str: 371 | args_list = query_str.split('&') # 分割参数键值对 372 | args = {} 373 | # 解析参数键值对 374 | for arg_pair in args_list: 375 | key, value = arg_pair.split('=') 376 | key = url_decode(key) 377 | value = url_decode(value) 378 | args[key] = value 379 | self._args = args # 缓存结果 380 | except: 381 | pass 382 | return self._args 383 | 384 | @property 385 | def form(self): 386 | """ 387 | 解析请求中的表单数据 388 | 389 | Returns: 390 | dict / None: 当成功解析表单数据时返回表单数据字典,否则返回 None(无法解析时或程序出错) 391 | 392 | Examples: 393 | request.form 394 | """ 395 | if self._form is None: 396 | try: 397 | items = self.data.decode("utf-8").split("&") 398 | form = {} 399 | for _ in items: 400 | k, v = _.split("=", 1) 401 | if not v: 402 | v = None 403 | else: 404 | v = url_decode(v) 405 | form[k] = v 406 | except: 407 | return None 408 | self._form = form # 缓存结果 409 | return self._form 410 | 411 | @property 412 | def cookies(self): 413 | """ 414 | 解析请求中的 Cookie 数据 415 | 416 | Returns: 417 | dict / None: 当成功解析 Cookies 数据时返回 Cookies 数据字典,否则返回 None(无法解析时或程序出错) 418 | 419 | Examples: 420 | request.cookies 421 | """ 422 | if not self._cookies: 423 | try: 424 | cookies = {} 425 | items = self.headers.get('Cookie').split(";") 426 | for item in items: 427 | item = item.strip() 428 | if '=' in item: 429 | k, v = item.split('=', 1) 430 | k = url_decode(k) 431 | v = url_decode(v) 432 | cookies[k] = v 433 | except: 434 | return {} 435 | self._cookies = cookies # 缓存结果 436 | return self._cookies 437 | 438 | 439 | class _HttpError(Exception): 440 | """ 441 | 表示 HTTP 错误的异常类。 442 | """ 443 | pass 444 | 445 | 446 | class EasyWeb: 447 | # HTTP 响应代码 448 | CODE_200 = b"HTTP/1.1 200 OK\r\n" 449 | CODE_404 = b"HTTP/1.1 404 Not Found\r\nContent-Type: text/html\r\n\r\n

Error 404: Page not found.

" 450 | CODE_405 = b"HTTP/1.1 405 Method Not Allowed\r\nContent-Type: text/html\r\n\r\n

Error 405: Method not allowed.

" 451 | 452 | def __init__(self): 453 | self.host = str 454 | '监听的 IP' 455 | self.port = int 456 | '监听的端口' 457 | self.routes = [] 458 | '路由表' 459 | self.server = None 460 | '服务器实例' 461 | 462 | def route(self, path: str, methods: list = None): 463 | """ 464 | 用于添加路由处理的装饰器 465 | 466 | Args: 467 | path: 用于匹配请求路径的字符串 468 | methods: 允许的请求方法列表,默认为 ['POST', 'GET'] 469 | 470 | Example: 471 | @app.route("/") 472 | def index(request): 473 | return "Hello, World!" 474 | 475 | Notes: 476 | 另外支持使用 "/" 和 ”/“ 对末尾的字符串或者路径进行匹配,可以通过 request.match 获取匹配的结果 477 | """ 478 | # 添加路由装饰器 479 | if methods is None: 480 | methods = ['POST', 'GET'] 481 | 482 | def decorator(func): 483 | self.routes.append((path, func, methods)) 484 | return func 485 | 486 | return decorator 487 | 488 | async def raw_run(self): 489 | return await asyncio.start_server(self.handle, self.host, self.port) 490 | 491 | def run(self, host="0.0.0.0", port=80): 492 | """ 493 | 运行 Web 服务器 494 | 创建并启动事件循环,在其中运行服务器,监听客户端请求。 495 | 496 | Note: 497 | 当调用此方法后,服务器将运行并阻塞事件循环,直到使用 `Ctrl+C` 或者 `stop()` 等方式进行终止。 498 | """ 499 | self.host = host 500 | '监听的 IP' 501 | self.port = port 502 | '监听的端口' 503 | # asyncio 获取事件循环 504 | self.server = asyncio.get_event_loop() 505 | self.server.create_task(self.raw_run()) 506 | self.server.run_forever() 507 | 508 | async def handle(self, reader, writer): 509 | """ 510 | 处理客户端的请求并生成对应的响应。 511 | 512 | Args: 513 | reader: 用于从客户端读取请求数据的流。 514 | writer: 用于向客户端发送响应数据的流。 515 | """ 516 | request = _Request() 517 | try: 518 | raw = await reader.readline() # HTTP 请求方法,路径,协议版本 519 | raw = raw.decode('utf-8').split(" ") 520 | if len(raw) != 3: 521 | return 522 | raw[2] = raw[2].rstrip("\r\n") 523 | request.method, request.full_path, request.protocol = raw 524 | request.path = request.full_path.split('?', 1)[0] 525 | # 协议版本检查 526 | if request.protocol not in ("HTTP/1.0", "HTTP/1.1"): 527 | raise _HttpError(request.protocol, 505, "Version Not Supported") 528 | # 解析 HTTP 请求头 529 | while True: 530 | raw = await reader.readline() # 请求头参数:\r\n 531 | raw = raw.decode('utf-8').rstrip('\r\n').split(": ", 1) 532 | if len(raw) == 2: 533 | k, v = raw 534 | request.headers[k] = v 535 | elif len(raw) == 1: # 请求头结束:\r\n\r\n 536 | break 537 | else: 538 | pass 539 | # 查找匹配路由 540 | for route_path, route_func, route_methods in self.routes: 541 | # 匹配路由 "/" 和 "/" "/" 542 | # 完全匹配 543 | if route_path.rstrip("/") == request.path.rstrip("/"): 544 | request.match = None 545 | match_success = True 546 | # 匹配字符串 547 | elif route_path[-9:] == "/" and route_path[:-9] == "/".join( 548 | request.path.rstrip("/").split("/")[:-1]): 549 | request.match = request.path[len(route_path[:-9]) + 1:] 550 | match_success = True 551 | # 匹配路径 552 | elif (route_path[-7:] == "/" and 553 | "/".join(request.path.rstrip("/").split("/")[:-1]).startswith(route_path[:-7]) and 554 | request.path[len(route_path[:-7]) + 1:]): 555 | request.match = request.path[len(route_path[:-7]) + 1:] 556 | match_success = True 557 | else: 558 | match_success = False 559 | if match_success: # 完全匹配,字符串匹配,路径匹配 560 | if request.method in route_methods: # 匹配到路由 561 | # 获取请求体 562 | size = int(request.headers.get("Content-Length", 0)) 563 | if size: 564 | try: 565 | request.data = await asyncio.wait_for(reader.read(size), timeout=5) 566 | except asyncio.TimeoutError as e: 567 | raise OSError('[ERROR] Asyncio TimeoutError: {}'.format(e)) 568 | else: 569 | request.data = None 570 | # 调用路由处理函数并发送响应 571 | response = route_func(request) # str / bytes / generator / None 572 | try: 573 | response.get_response 574 | except AttributeError: 575 | if isinstance(response, tuple): # return response, status_code, headers 576 | try: 577 | response[0].get_response 578 | status_code = response[1] 579 | if len(response) == 3: 580 | headers = response[2] 581 | else: 582 | headers = None 583 | response = response[0] 584 | response.status_code = status_code 585 | if headers: 586 | response.headers.update(headers) 587 | except AttributeError: 588 | if len(response) == 2: 589 | response = make_response(response[0], response[1]) 590 | elif len(response) == 3: 591 | response = make_response(response[0], response[1], response[2]) 592 | else: # return bytes / str / iterables / tuple (bytes / str / iterables, status_code, headersr) 593 | response = make_response(response) 594 | 595 | for res in response.get_response(): 596 | await writer.awrite(res) 597 | else: 598 | # 发送"方法不允许"响应 599 | await writer.awrite(self.CODE_405) 600 | break 601 | else: 602 | # 发送"页面不存在"响应 603 | await writer.awrite(self.CODE_404) 604 | except Exception as e: 605 | print("[WARN] EasyWEB: {}".format(e)) 606 | finally: 607 | # 关闭连接 608 | await writer.aclose() 609 | 610 | def stop(self): 611 | """ 612 | 停止运行 EasyWeb Server 613 | """ 614 | if self.server: 615 | self.server.stop() 616 | self.server = None 617 | 618 | 619 | def send_file(file, mimetype: str = None, as_attachment=False, attachment_filename=None): 620 | """ 621 | 发送文件给客户端 622 | 623 | Args: 624 | file: 要发送的文件的路径 625 | mimetype: 文件的 MIME 类型。如果未指定,EasyWeb 将尝试根据文件扩展名进行猜测 626 | as_attachment: 是否作为附件发送文件,作为附件时会被下载 627 | attachment_filename: 下载文件时向用户显示的文件名。如果未提供,将使用原始文件名 628 | 629 | Returns: 630 | 包含 HTTP 200 OK 和 文件的二进制数据 的可迭代对象 631 | """ 632 | head = {'Content-Type': 'application/octet-stream'} 633 | if as_attachment: # 作为附件发送文件 634 | if not attachment_filename: # 下载文件时的文件名 635 | attachment_filename = file.split("/")[-1] 636 | head['Content-Disposition'] = 'attachment; filename="{}"'.format(attachment_filename) 637 | else: 638 | if mimetype is None: # 自动识别文件的 MIME 类型 639 | e = file.split(".") 640 | if len(e) >= 2: 641 | head['Content-Type'] = FILE_TYPE.get(e[-1], "application/octet-stream") 642 | if not exists(file): 643 | yield {'Content-Type': 'text/html'} 644 | yield '

File Not Exists: {}

'.format(file).encode("utf-8") 645 | else: 646 | yield head 647 | with open(file, "rb") as f: 648 | _file = True 649 | while _file: 650 | _file = f.read(1024) 651 | yield _file 652 | 653 | 654 | def render_template(file, **kwargs): 655 | """ 656 | 渲染模板 657 | 658 | Args: 659 | file: 要渲染的 html 模板路径 660 | **kwargs: 传递给函数的其他关键字参数,将在模板中用于渲染 661 | 662 | Returns: 663 | 包含 HTTP 200 OK 和渲染后的 HTML 内容字符串 的可迭代对象 664 | """ 665 | yield {'Content-Type': 'text/html'} 666 | if not exists(file): 667 | yield '

File Not Exists: {}

'.format(file).encode("utf-8") 668 | else: 669 | with open(file, "r") as f: 670 | _file = True 671 | f_readline = f.readline 672 | while _file: 673 | _file = f_readline() + f_readline() + f_readline() + f_readline() + f_readline() # 五行一组进行渲染 674 | for k, v in kwargs.items(): # 渲染参数 675 | _file = _file.replace("{{{{{}}}}}".format(k), str(v)) 676 | yield _file.encode("utf-8") 677 | 678 | 679 | def make_response(content=b'', status_code: int = 200, headers=None) -> _Response: 680 | """ 681 | 创建一个带有 内容、状态码 和 头部 的 响应对象。 682 | 683 | Args: 684 | content: 响应的内容,可以为 Iterable (bytes),str,tuple,dict 685 | status_code (int): 响应的状态码,默认为 200。 686 | headers: 可选的头部,包含在响应中,默认为 None。 687 | 688 | Returns: 689 | _Response: 响应对象 690 | """ 691 | if headers is None: 692 | headers = {} 693 | response = _Response() 694 | if isinstance(content, tuple): 695 | if len(content) >= 2: 696 | content, status_code = content[:2] 697 | if len(content) == 3: 698 | headers = content[2] 699 | response.headers = headers 700 | response.status_code = status_code 701 | response.data = content 702 | return response 703 | --------------------------------------------------------------------------------