Skip to content

Commit d469ab9

Browse files
committed
untested first draft of simple WSGI server
1 parent f6d9415 commit d469ab9

File tree

1 file changed

+210
-0
lines changed

1 file changed

+210
-0
lines changed
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
# The MIT License (MIT)
2+
#
3+
# Copyright (c) 2019 Matt Costi for Adafruit Industries
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy
6+
# of this software and associated documentation files (the "Software"), to deal
7+
# in the Software without restriction, including without limitation the rights
8+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
# copies of the Software, and to permit persons to whom the Software is
10+
# furnished to do so, subject to the following conditions:
11+
#
12+
# The above copyright notice and this permission notice shall be included in
13+
# all copies or substantial portions of the Software.
14+
#
15+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
# THE SOFTWARE.
22+
23+
"""
24+
`adafruit_esp32spi_wsgiserver`
25+
================================================================================
26+
27+
A simple WSGI (Web Server Gateway Interface) server that interfaces with the ESP32 over SPI.
28+
Opens a specified port on the ESP32 to listen for incoming HTTP Requests and
29+
Accepts an Application object that must be callable, which gets called
30+
whenever a new HTTP Request has been received.
31+
32+
The Application MUST accept 2 ordered parameters:
33+
1. environ object (incoming request data)
34+
2. start_response function. Must be called before the Application
35+
callable returns, in order to set the response status and headers.
36+
37+
The Application MUST return a single string in a list,
38+
which is the response data
39+
40+
Requires update_poll being called in the applications main event loop.
41+
42+
For more details about Python WSGI see:
43+
https://www.python.org/dev/peps/pep-0333/
44+
45+
* Author(s): Matt Costi
46+
"""
47+
# pylint: disable=no-name-in-module
48+
49+
import io
50+
from micropython import const
51+
import adafruit_esp32spi.adafruit_esp32spi_socket as socket
52+
from adafruit_esp32spi.adafruit_esp32spi_requests import parse_headers
53+
54+
_the_interface = None # pylint: disable=invalid-name
55+
def set_interface(iface):
56+
"""Helper to set the global internet interface"""
57+
global _the_interface # pylint: disable=global-statement, invalid-name
58+
_the_interface = iface
59+
socket.set_interface(iface)
60+
61+
NO_SOCK_AVAIL = const(255)
62+
63+
# pylint: disable=invalid-name
64+
class WSGIServer:
65+
"""
66+
A simple server that implements the WSGI interface
67+
"""
68+
69+
def __init__(self, port=80, debug=False, application=None):
70+
self.application = application
71+
self.port = port
72+
self._server_sock = socket.socket(socknum=NO_SOCK_AVAIL)
73+
self._client_sock = socket.socket(socknum=NO_SOCK_AVAIL)
74+
self._debug = debug
75+
76+
self._response_status = None
77+
self._response_headers = []
78+
79+
def start(self):
80+
"""
81+
starts the server and begins listening for incoming connections.
82+
Call update_poll in the main loop for the application callable to be
83+
invoked on receiving an incoming request.
84+
"""
85+
self._server_sock = socket.socket()
86+
_the_interface.start_server(self.port, self._server_sock.socknum)
87+
if self._debug:
88+
ip = _the_interface.pretty_ip(_the_interface.ip_address)
89+
print("Server available at {0}:{1}".format(ip, self.port))
90+
print("Sever status: ", _the_interface.get_server_state(self._server_sock.socknum))
91+
92+
def update_poll(self):
93+
"""
94+
Call this method inside your main event loop to get the server
95+
check for new incoming client requests. When a request comes in,
96+
the application callable will be invoked.
97+
"""
98+
self.client_available()
99+
if (self._client_sock and self._client_sock.available()):
100+
environ = self._get_environ(self._client_sock)
101+
result = self.application(environ, self._start_response)
102+
self.finish_response(result)
103+
104+
def finish_response(self, result):
105+
"""
106+
Called after the application callbile returns result data to respond with.
107+
Creates the HTTP Response payload from the response_headers and results data,
108+
and sends it back to client.
109+
110+
:param string result: the data string to send back in the response to the client.
111+
"""
112+
try:
113+
response = "HTTP/1.1 {0}\r\n".format(self._response_status)
114+
for header in self._response_headers:
115+
response += "{0}: {1}\r\n".format(*header)
116+
response += "\r\n"
117+
for data in result:
118+
response += data
119+
self._client_sock.write(response.encode("utf-8"))
120+
finally:
121+
self._client_sock.close()
122+
123+
def client_available(self):
124+
"""
125+
returns a client socket connection if available.
126+
Otherwise, returns None
127+
:return: the client
128+
:rtype: Socket
129+
"""
130+
sock = None
131+
if self._server_sock.socknum != NO_SOCK_AVAIL:
132+
if self._client_sock.socknum != NO_SOCK_AVAIL:
133+
# check previous received client socket
134+
if self._debug > 2:
135+
print("checking if last client sock still valid")
136+
if self._client_sock.connected() and self._client_sock.available():
137+
sock = self._client_sock
138+
if not sock:
139+
# check for new client sock
140+
if self._debug > 2:
141+
print("checking for new client sock")
142+
client_sock_num = _the_interface.socket_available(self._server_sock.socknum)
143+
sock = socket.socket(socknum=client_sock_num)
144+
else:
145+
print("Server has not been started, cannot check for clients!")
146+
147+
if sock and sock.socknum != NO_SOCK_AVAIL:
148+
if self._debug > 2:
149+
print("client sock num is: ", sock.socknum)
150+
self._client_sock = sock
151+
return self._client_sock
152+
153+
return None
154+
155+
def _start_response(self, status, response_headers):
156+
"""
157+
The application callable will be given this method as the second param
158+
This is to be called before the application callable returns, to signify
159+
the response can be started with the given status and headers.
160+
161+
:param string status: a status string including the code and reason. ex: "200 OK"
162+
:param list response_headers: a list of tuples to represent the headers.
163+
ex ("header-name", "header value")
164+
"""
165+
self._response_status = status
166+
self._response_headers = [("Server", "esp32WSGIServer")] + response_headers
167+
168+
def _get_environ(self, client):
169+
"""
170+
The application callable will be given the resulting environ dictionary.
171+
It contains metadata about the incoming request and the request body ("wsgi.input")
172+
173+
:param Socket client: socket to read the request from
174+
"""
175+
env = {}
176+
line = str(client.readline(), "utf-8")
177+
(method, path, ver) = line.rstrip("\r\n").split(None, 2)
178+
179+
env["wsgi.version"] = (1, 0)
180+
env["wsgi.url_scheme"] = "http"
181+
env["wsgi.multithread"] = False
182+
env["wsgi.multiprocess"] = False
183+
env["wsgi.run_once"] = False
184+
185+
env["REQUEST_METHOD"] = method
186+
env["SCRIPT_NAME"] = ""
187+
env["PATH_INFO"] = path
188+
env["SERVER_NAME"] = _the_interface.pretty_ip(_the_interface.ip_address)
189+
env["SERVER_PROTOCOL"] = ver
190+
env["SERVER_PORT"] = self.port
191+
if line.find("?"):
192+
env["QUERY_STRING"] = line.split("?")[1]
193+
194+
headers = parse_headers(client)
195+
if "content-type" in headers:
196+
env["CONTENT_TYPE"] = headers.get("content-type")
197+
if "content-length" in headers:
198+
env["CONTENT_LENGTH"] = headers.get("content-length")
199+
body = client.read(env["CONTENT_LENGTH"])
200+
env["wsgi.input"] = io.StringIO(body)
201+
else:
202+
body = client.read()
203+
env["wsgi.input"] = io.StringIO(body)
204+
for name, value in headers:
205+
key = "HTTP_" + name.replace('-', '_').upper()
206+
if key in env:
207+
value = "{0},{1}".format(env[key], value)
208+
env[key] = value
209+
210+
return env

0 commit comments

Comments
 (0)