-
Notifications
You must be signed in to change notification settings - Fork 439
/
wsgiserver.py
executable file
·286 lines (251 loc) · 10 KB
/
wsgiserver.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
# SPDX-FileCopyrightText: Copyright (c) 2019 Matt Costi for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
`adafruit_esp32spi_wsgiserver`
================================================================================
A simple WSGI (Web Server Gateway Interface) server that interfaces with the ESP32 over SPI.
Opens a specified port on the ESP32 to listen for incoming HTTP Requests and
Accepts an Application object that must be callable, which gets called
whenever a new HTTP Request has been received.
The Application MUST accept 2 ordered parameters:
1. environ object (incoming request data)
2. start_response function. Must be called before the Application
callable returns, in order to set the response status and headers.
The Application MUST return a single string in a list,
which is the response data
Requires update_poll being called in the applications main event loop.
For more details about Python WSGI see:
https://www.python.org/dev/peps/pep-0333/
* Author(s): Matt Costi
"""
# pylint: disable=no-name-in-module
import io
import gc
from micropython import const
import socketpool
import wifi
class BadRequestError(Exception):
"""Raised when the client sends an unexpected empty line"""
pass
_BUFFER_SIZE = 32
buffer = bytearray(_BUFFER_SIZE)
def readline(socketin):
"""
Implement readline() for native wifi using recv_into
"""
data_string = b""
while True:
try:
num = socketin.recv_into(buffer, 1)
data_string += str(buffer, 'utf8')[:num]
if num == 0:
return data_string
if data_string[-2:] == b"\r\n":
return data_string[:-2]
except OSError as ex:
# if ex.errno == 9: # [Errno 9] EBADF
# return None
if ex.errno == 11: # [Errno 11] EAGAIN
continue
raise
def read(socketin,length = -1):
total = 0
data_string = b""
try:
if length > 0:
while total < length:
reste = length - total
num = socketin.recv_into(buffer, min(_BUFFER_SIZE, reste))
#
if num == 0:
# timeout
# raise OSError(110)
return data_string
#
data_string += buffer[:num]
total = total + num
return data_string
else:
while True:
num = socketin.recv_into(buffer, 1)
data_string += str(buffer, 'utf8')[:num]
if num == 0:
return data_string
except OSError as ex:
if ex.errno == 11: # [Errno 11] EAGAIN
return data_string
raise
def parse_headers(sock):
"""
Parses the header portion of an HTTP request/response from the socket.
Expects first line of HTTP request/response to have been read already
return: header dictionary
rtype: Dict
"""
headers = {}
while True:
line = readline(sock)
if not line or line == b"\r\n":
break
#print("**line: ", line)
title, content = line.split(b': ', 1)
if title and content:
title = str(title.lower(), 'utf-8')
content = str(content, 'utf-8')
headers[title] = content
return headers
pool = socketpool.SocketPool(wifi.radio)
NO_SOCK_AVAIL = const(255)
# pylint: disable=invalid-name
class WSGIServer:
"""
A simple server that implements the WSGI interface
"""
def __init__(self, port=80, debug=False, application=None):
self.application = application
self.port = port
self._server_sock = None
self._client_sock = None
self._debug = debug
self._response_status = None
self._response_headers = []
def start(self):
"""
starts the server and begins listening for incoming connections.
Call update_poll in the main loop for the application callable to be
invoked on receiving an incoming request.
"""
self._server_sock = pool.socket(pool.AF_INET,pool.SOCK_STREAM)
HOST = repr(wifi.radio.ipv4_address_ap)
self._server_sock.bind((repr(wifi.radio.ipv4_address_ap), self.port))
self._server_sock.listen(1)
# if self._debug:
# ip = _the_interface.pretty_ip(_the_interface.ip_address)
# print("Server available at {0}:{1}".format(ip, self.port))
# print(
# "Sever status: ",
# _the_interface.get_server_state(self._server_sock.socknum),
# )
def pretty_ip(self):
return f"http://{wifi.radio.ipv4_address_ap}:{self.port}"
def update_poll(self):
"""
Call this method inside your main event loop to get the server
check for new incoming client requests. When a request comes in,
the application callable will be invoked.
"""
self.client_available()
if self._client_sock:
try:
environ = self._get_environ(self._client_sock)
result = self.application(environ, self._start_response)
self.finish_response(result)
except BadRequestError:
self._start_response("400 Bad Request", [])
self.finish_response([])
def finish_response(self, result):
"""
Called after the application callbile returns result data to respond with.
Creates the HTTP Response payload from the response_headers and results data,
and sends it back to client.
:param string result: the data string to send back in the response to the client.
"""
try:
response = "HTTP/1.1 {0}\r\n".format(self._response_status)
for header in self._response_headers:
response += "{0}: {1}\r\n".format(*header)
response += "\r\n"
self._client_sock.send(response.encode("utf-8"))
for data in result:
if isinstance(data, str):
data = data.encode("utf-8")
elif not isinstance(data, bytes):
data = str(data).encode("utf-8")
bytes_sent = 0
while bytes_sent < len(data):
try:
bytes_sent += self._client_sock.send(data[bytes_sent:])
except OSError as ex:
if ex.errno != 11: # [Errno 11] EAGAIN
raise
gc.collect()
except OSError as ex:
if ex.errno != 104: # [Errno 104] ECONNRESET
raise
finally:
#print("closing")
self._client_sock.close()
self._client_sock = None
def client_available(self):
"""
returns a client socket connection if available.
Otherwise, returns None
:return: the client
:rtype: Socket
"""
sock = None
if not self._server_sock:
print("Server has not been started, cannot check for clients!")
elif not self._client_sock:
self._server_sock.setblocking(False)
try:
self._client_sock, addr = self._server_sock.accept()
except OSError as ex:
if ex.errno != 11: # [Errno 11] EAGAIN
raise
return None
def _start_response(self, status, response_headers):
"""
The application callable will be given this method as the second param
This is to be called before the application callable returns, to signify
the response can be started with the given status and headers.
:param string status: a status string including the code and reason. ex: "200 OK"
:param list response_headers: a list of tuples to represent the headers.
ex ("header-name", "header value")
"""
self._response_status = status
self._response_headers = [("Server", "esp32WSGIServer")] + response_headers
def _get_environ(self, client):
"""
The application callable will be given the resulting environ dictionary.
It contains metadata about the incoming request and the request body ("wsgi.input")
:param Socket client: socket to read the request from
"""
env = {}
line = readline(client).decode("utf-8")
try:
(method, path, ver) = line.rstrip("\r\n").split(None, 2)
except ValueError:
raise BadRequestError("Unknown request from client.")
env["wsgi.version"] = (1, 0)
env["wsgi.url_scheme"] = "http"
env["wsgi.multithread"] = False
env["wsgi.multiprocess"] = False
env["wsgi.run_once"] = False
env["REQUEST_METHOD"] = method
env["SCRIPT_NAME"] = ""
env["SERVER_NAME"] = str(wifi.radio.ipv4_address_ap)
env["SERVER_PROTOCOL"] = ver
env["SERVER_PORT"] = self.port
if path.find("?") >= 0:
env["PATH_INFO"] = path.split("?")[0]
env["QUERY_STRING"] = path.split("?")[1]
else:
env["PATH_INFO"] = path
headers = parse_headers(client)
if "content-type" in headers:
env["CONTENT_TYPE"] = headers.get("content-type")
if "content-length" in headers:
env["CONTENT_LENGTH"] = headers.get("content-length")
body = read(client, int(env["CONTENT_LENGTH"]))
env["wsgi.input"] = io.StringIO(body)
else:
body = read(client)
env["wsgi.input"] = io.StringIO(body)
for name, value in headers.items():
key = "HTTP_" + name.replace("-", "_").upper()
if key in env:
value = "{0},{1}".format(env[key], value)
env[key] = value
return env