├── .gitignore
├── CONTRIBUTORS
├── main.py
├── esp8266-20191220-v1.12.bin
├── boot.py
├── connected.html
├── server.py
├── LICENSE
├── README.md
├── credentials.py
├── index.html
├── captive_dns.py
├── captive_portal.py
└── captive_http.py
/.gitignore:
--------------------------------------------------------------------------------
1 | tags
2 | .idea/
3 |
--------------------------------------------------------------------------------
/CONTRIBUTORS:
--------------------------------------------------------------------------------
1 | Anson VanDoren (@anson-vandoren)
2 | @grey27
3 | @jasonbrackman
4 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | from captive_portal import CaptivePortal
2 |
3 | portal = CaptivePortal()
4 |
5 | portal.start()
6 |
--------------------------------------------------------------------------------
/esp8266-20191220-v1.12.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anson-vandoren/esp8266-captive-portal/HEAD/esp8266-20191220-v1.12.bin
--------------------------------------------------------------------------------
/boot.py:
--------------------------------------------------------------------------------
1 | # This file is executed on every boot (including wake-boot from deepsleep)
2 | #import esp
3 | #esp.osdebug(None)
4 | import uos, machine
5 | #uos.dupterm(None, 1) # disable REPL on UART(0)
6 | import gc
7 | #import webrepl
8 | #webrepl.start()
9 | gc.collect()
10 |
--------------------------------------------------------------------------------
/connected.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Connected!
6 |
7 |
8 | Connected!
9 |
10 | Device is connected to WiFi access point '%s' with IP
11 | address %s
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/server.py:
--------------------------------------------------------------------------------
1 | import usocket as socket
2 | import uselect as select
3 |
4 |
5 | class Server:
6 | def __init__(self, poller, port, sock_type, name):
7 | self.name = name
8 | # create socket with correct type: stream (TCP) or datagram (UDP)
9 | self.sock = socket.socket(socket.AF_INET, sock_type)
10 |
11 | # register to get event updates for this socket
12 | self.poller = poller
13 | self.poller.register(self.sock, select.POLLIN)
14 |
15 | addr = socket.getaddrinfo("0.0.0.0", port)[0][-1]
16 | # allow new requests while still sending last response
17 | self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
18 | self.sock.bind(addr)
19 |
20 | print(self.name, "listening on", addr)
21 |
22 | def stop(self, poller):
23 | poller.unregister(self.sock)
24 | self.sock.close()
25 | print(self.name, "stopped")
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Anson VanDoren
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 all
13 | 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 THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Blog post series
2 |
3 | The code in this repo is the results of a blog series I wrote about building a captive web portal for a Wemos D1 Mini
4 | using MicroPython. You can find the articles here:
5 |
6 | - [Part 1](https://ansonvandoren.com/posts/esp8266-captive-web-portal-part-1/)
7 | - [Part 2](https://ansonvandoren.com/posts/esp8266-captive-web-portal-part-2/)
8 | - [Part 3](https://ansonvandoren.com/posts/esp8266-captive-web-portal-part-3/)
9 | - [Part 4](https://ansonvandoren.com/posts/esp8266-captive-web-portal-part-4/)
10 |
11 | # Starting the captive portal
12 |
13 | Copy the .py and .html files to your ESP8266 board. If you already have a `main.py` file, then just copy the contents of
14 | the `main.py` file from this repo. There's only a couple of lines there.
15 |
16 | Instantiating a `CaptivePortal` and calling its `start()` method will turn on your MCU's WiFi access point, and you can
17 | then connect to it and input your home WiFi credentials. Once you do, the MCU will turn off its AP, and connect to your
18 | home WiFi instead.
19 |
20 | # Purpose
21 |
22 | This is not really a standalone project, but rather a bit of useful functionality that I drop into other projects
23 | I make so that if I send one to someone else, I don't need to hardcode their home WiFi credentials to get the thing
24 | to work. Instead, they can easily enter their own WiFi SSID and password to allow the device to connect and
25 | start doing whatever it's supposed to be doing.
26 |
--------------------------------------------------------------------------------
/credentials.py:
--------------------------------------------------------------------------------
1 | import uos
2 |
3 |
4 | class Creds:
5 |
6 | CRED_FILE = "./wifi.creds"
7 |
8 | def __init__(self, ssid=None, password=None):
9 | self.ssid = ssid
10 | self.password = password
11 |
12 | def write(self):
13 | """Write credentials to CRED_FILE if valid input found."""
14 | if self.is_valid():
15 | with open(self.CRED_FILE, "wb") as f:
16 | f.write(b",".join([self.ssid, self.password]))
17 | print("Wrote credentials to {:s}".format(self.CRED_FILE))
18 |
19 | def load(self):
20 |
21 | try:
22 | with open(self.CRED_FILE, "rb") as f:
23 | contents = f.read().split(b",")
24 | print("Loaded WiFi credentials from {:s}".format(self.CRED_FILE))
25 | if len(contents) == 2:
26 | self.ssid, self.password = contents
27 |
28 | if not self.is_valid():
29 | self.remove()
30 | except OSError:
31 | pass
32 |
33 | return self
34 |
35 | def remove(self):
36 | """
37 | 1. Delete credentials file from disk.
38 | 2. Set ssid and password to None
39 | """
40 | # print("Attempting to remove {}".format(self.CRED_FILE))
41 | try:
42 | uos.remove(self.CRED_FILE)
43 | except OSError:
44 | pass
45 |
46 | self.ssid = self.password = None
47 |
48 | def is_valid(self):
49 | # Ensure the credentials are entered as bytes
50 | if not isinstance(self.ssid, bytes):
51 | return False
52 | if not isinstance(self.password, bytes):
53 | return False
54 |
55 | # Ensure credentials are not None or empty
56 | return all((self.ssid, self.password))
57 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | WiFi Login
6 |
55 |
56 |
57 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/captive_dns.py:
--------------------------------------------------------------------------------
1 | import usocket as socket
2 | import gc
3 |
4 | from server import Server
5 |
6 |
7 | class DNSQuery:
8 | def __init__(self, data):
9 | self.data = data
10 | self.domain = ""
11 | # header is bytes 0-11, so question starts on byte 12
12 | head = 12
13 | # length of this label defined in first byte
14 | length = data[head]
15 | while length != 0:
16 | label = head + 1
17 | # add the label to the requested domain and insert a dot after
18 | self.domain += data[label : label + length].decode("utf-8") + "."
19 | # check if there is another label after this one
20 | head += length + 1
21 | length = data[head]
22 |
23 | def answer(self, ip_addr):
24 | # ** create the answer header **
25 | # copy the ID from incoming request
26 | packet = self.data[:2]
27 | # set response flags (assume RD=1 from request)
28 | packet += b"\x81\x80"
29 | # copy over QDCOUNT and set ANCOUNT equal
30 | packet += self.data[4:6] + self.data[4:6]
31 | # set NSCOUNT and ARCOUNT to 0
32 | packet += b"\x00\x00\x00\x00"
33 |
34 | # ** create the answer body **
35 | # respond with original domain name question
36 | packet += self.data[12:]
37 | # pointer back to domain name (at byte 12)
38 | packet += b"\xC0\x0C"
39 | # set TYPE and CLASS (A record and IN class)
40 | packet += b"\x00\x01\x00\x01"
41 | # set TTL to 60sec
42 | packet += b"\x00\x00\x00\x3C"
43 | # set response length to 4 bytes (to hold one IPv4 address)
44 | packet += b"\x00\x04"
45 | # now actually send the IP address as 4 bytes (without the "."s)
46 | packet += bytes(map(int, ip_addr.split(".")))
47 |
48 | gc.collect()
49 |
50 | return packet
51 |
52 |
53 | class DNSServer(Server):
54 | def __init__(self, poller, ip_addr):
55 | super().__init__(poller, 53, socket.SOCK_DGRAM, "DNS Server")
56 | self.ip_addr = ip_addr
57 |
58 | def handle(self, sock, event, others):
59 | # server doesn't spawn other sockets, so only respond to its own socket
60 | if sock is not self.sock:
61 | return
62 |
63 | # check the DNS question, and respond with an answer
64 | try:
65 | data, sender = sock.recvfrom(1024)
66 | request = DNSQuery(data)
67 |
68 | print("Sending {:s} -> {:s}".format(request.domain, self.ip_addr))
69 | sock.sendto(request.answer(self.ip_addr), sender)
70 |
71 | # help MicroPython with memory management
72 | del request
73 | gc.collect()
74 | except Exception as e:
75 | print("DNS server exception:", e)
76 |
--------------------------------------------------------------------------------
/captive_portal.py:
--------------------------------------------------------------------------------
1 | import gc
2 | import network
3 | import ubinascii as binascii
4 | import uselect as select
5 | import utime as time
6 |
7 | from captive_dns import DNSServer
8 | from captive_http import HTTPServer
9 | from credentials import Creds
10 |
11 |
12 | class CaptivePortal:
13 | AP_IP = "192.168.4.1"
14 | AP_OFF_DELAY = const(10 * 1000)
15 | MAX_CONN_ATTEMPTS = 10
16 |
17 | def __init__(self, essid=None):
18 | self.local_ip = self.AP_IP
19 | self.sta_if = network.WLAN(network.STA_IF)
20 | self.ap_if = network.WLAN(network.AP_IF)
21 |
22 | if essid is None:
23 | essid = b"ESP8266-%s" % binascii.hexlify(self.ap_if.config("mac")[-3:])
24 | self.essid = essid
25 |
26 | self.creds = Creds()
27 |
28 | self.dns_server = None
29 | self.http_server = None
30 | self.poller = select.poll()
31 |
32 | self.conn_time_start = None
33 |
34 | def start_access_point(self):
35 | # sometimes need to turn off AP before it will come up properly
36 | self.ap_if.active(False)
37 | while not self.ap_if.active():
38 | print("Waiting for access point to turn on")
39 | self.ap_if.active(True)
40 | time.sleep(1)
41 | # IP address, netmask, gateway, DNS
42 | self.ap_if.ifconfig(
43 | (self.local_ip, "255.255.255.0", self.local_ip, self.local_ip)
44 | )
45 | self.ap_if.config(essid=self.essid, authmode=network.AUTH_OPEN)
46 | print("AP mode configured:", self.ap_if.ifconfig())
47 |
48 | def connect_to_wifi(self):
49 | print(
50 | "Trying to connect to SSID '{:s}' with password {:s}".format(
51 | self.creds.ssid, self.creds.password
52 | )
53 | )
54 |
55 | # initiate the connection
56 | self.sta_if.active(True)
57 | self.sta_if.connect(self.creds.ssid, self.creds.password)
58 |
59 | attempts = 1
60 | while attempts <= self.MAX_CONN_ATTEMPTS:
61 | if not self.sta_if.isconnected():
62 | print("Connection attempt {:d}/{:d} ...".format(attempts, self.MAX_CONN_ATTEMPTS))
63 | time.sleep(2)
64 | attempts += 1
65 | else:
66 | print("Connected to {:s}".format(self.creds.ssid))
67 | self.local_ip = self.sta_if.ifconfig()[0]
68 | return True
69 |
70 | print(
71 | "Failed to connect to {:s} with {:s}. WLAN status={:d}".format(
72 | self.creds.ssid, self.creds.password, self.sta_if.status()
73 | )
74 | )
75 | # forget the credentials since they didn't work, and turn off station mode
76 | self.creds.remove()
77 | self.sta_if.active(False)
78 | return False
79 |
80 | def check_valid_wifi(self):
81 | if not self.sta_if.isconnected():
82 | if self.creds.load().is_valid():
83 | # have credentials to connect, but not yet connected
84 | # return value based on whether the connection was successful
85 | return self.connect_to_wifi()
86 | # not connected, and no credentials to connect yet
87 | return False
88 |
89 | if not self.ap_if.active():
90 | # access point is already off; do nothing
91 | return False
92 |
93 | # already connected to WiFi, so turn off Access Point after a delay
94 | if self.conn_time_start is None:
95 | self.conn_time_start = time.ticks_ms()
96 | remaining = self.AP_OFF_DELAY
97 | else:
98 | remaining = self.AP_OFF_DELAY - time.ticks_diff(
99 | time.ticks_ms(), self.conn_time_start
100 | )
101 | if remaining <= 0:
102 | self.ap_if.active(False)
103 | print("Turned off access point")
104 | return False
105 |
106 | def captive_portal(self):
107 | print("Starting captive portal")
108 | self.start_access_point()
109 |
110 | if self.http_server is None:
111 | self.http_server = HTTPServer(self.poller, self.local_ip)
112 | print("Configured HTTP server")
113 | if self.dns_server is None:
114 | self.dns_server = DNSServer(self.poller, self.local_ip)
115 | print("Configured DNS server")
116 |
117 | try:
118 | while True:
119 | gc.collect()
120 | # check for socket events and handle them
121 | for response in self.poller.ipoll(1000):
122 | sock, event, *others = response
123 | is_handled = self.handle_dns(sock, event, others)
124 | if not is_handled:
125 | self.handle_http(sock, event, others)
126 |
127 | if self.check_valid_wifi():
128 | print("Connected to WiFi!")
129 | self.http_server.set_ip(self.local_ip, self.creds.ssid)
130 | self.dns_server.stop(self.poller)
131 | break
132 |
133 | except KeyboardInterrupt:
134 | print("Captive portal stopped")
135 | self.cleanup()
136 |
137 | def handle_dns(self, sock, event, others):
138 | if sock is self.dns_server.sock:
139 | # ignore UDP socket hangups
140 | if event == select.POLLHUP:
141 | return True
142 | self.dns_server.handle(sock, event, others)
143 | return True
144 | return False
145 |
146 | def handle_http(self, sock, event, others):
147 | self.http_server.handle(sock, event, others)
148 |
149 | def cleanup(self):
150 | print("Cleaning up")
151 | if self.dns_server:
152 | self.dns_server.stop(self.poller)
153 | gc.collect()
154 |
155 | def try_connect_from_file(self):
156 | if self.creds.load().is_valid():
157 | if self.connect_to_wifi():
158 | return True
159 |
160 | # WiFi Connection failed - remove credentials from disk
161 | self.creds.remove()
162 | return False
163 |
164 | def start(self):
165 | # turn off station interface to force a reconnect
166 | self.sta_if.active(False)
167 | if not self.try_connect_from_file():
168 | self.captive_portal()
169 |
170 |
--------------------------------------------------------------------------------
/captive_http.py:
--------------------------------------------------------------------------------
1 | import uerrno
2 | import uio
3 | import uselect as select
4 | import usocket as socket
5 |
6 | from collections import namedtuple
7 | from credentials import Creds
8 |
9 | WriteConn = namedtuple("WriteConn", ["body", "buff", "buffmv", "write_range"])
10 | ReqInfo = namedtuple("ReqInfo", ["type", "path", "params", "host"])
11 |
12 | from server import Server
13 |
14 | import gc
15 |
16 |
17 | def unquote(string):
18 | """stripped down implementation of urllib.parse unquote_to_bytes"""
19 |
20 | if not string:
21 | return b''
22 |
23 | if isinstance(string, str):
24 | string = string.encode('utf-8')
25 | string = string.replace(b'+', b' ')
26 |
27 | # split into substrings on each escape character
28 | bits = string.split(b'%')
29 | if len(bits) == 1:
30 | return string # there was no escape character
31 |
32 | res = [bits[0]] # everything before the first escape character
33 |
34 | # for each escape character, get the next two digits and convert to
35 | for item in bits[1:]:
36 | code = item[:2]
37 | char = bytes([int(code, 16)]) # convert to utf-8-encoded byte
38 | res.append(char) # append the converted character
39 | res.append(item[2:]) # append anything else that occurred before the next escape character
40 |
41 | return b''.join(res)
42 |
43 |
44 | class HTTPServer(Server):
45 | def __init__(self, poller, local_ip):
46 | super().__init__(poller, 80, socket.SOCK_STREAM, "HTTP Server")
47 | if type(local_ip) is bytes:
48 | self.local_ip = local_ip
49 | else:
50 | self.local_ip = local_ip.encode()
51 | self.request = dict()
52 | self.conns = dict()
53 | self.routes = {b"/": b"./index.html", b"/login": self.login}
54 |
55 | self.ssid = None
56 |
57 | # queue up to 5 connection requests before refusing
58 | self.sock.listen(5)
59 | self.sock.setblocking(False)
60 |
61 | def set_ip(self, new_ip, new_ssid):
62 | """update settings after connected to local WiFi"""
63 |
64 | self.local_ip = new_ip.encode()
65 | self.ssid = new_ssid
66 | self.routes = {b"/": self.connected}
67 |
68 | @micropython.native
69 | def handle(self, sock, event, others):
70 | if sock is self.sock:
71 | # client connecting on port 80, so spawn off a new
72 | # socket to handle this connection
73 | print("- Accepting new HTTP connection")
74 | self.accept(sock)
75 | elif event & select.POLLIN:
76 | # socket has data to read in
77 | print("- Reading incoming HTTP data")
78 | self.read(sock)
79 | elif event & select.POLLOUT:
80 | # existing connection has space to send more data
81 | print("- Sending outgoing HTTP data")
82 | self.write_to(sock)
83 |
84 | def accept(self, server_sock):
85 | """accept a new client request socket and register it for polling"""
86 |
87 | try:
88 | client_sock, addr = server_sock.accept()
89 | except OSError as e:
90 | if e.args[0] == uerrno.EAGAIN:
91 | return
92 |
93 | client_sock.setblocking(False)
94 | client_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
95 | self.poller.register(client_sock, select.POLLIN)
96 |
97 | def parse_request(self, req):
98 | """parse a raw HTTP request to get items of interest"""
99 |
100 | req_lines = req.split(b"\r\n")
101 | req_type, full_path, http_ver = req_lines[0].split(b" ")
102 | path = full_path.split(b"?")
103 | base_path = path[0]
104 | query = path[1] if len(path) > 1 else None
105 | query_params = (
106 | {
107 | key: val
108 | for key, val in [param.split(b"=") for param in query.split(b"&")]
109 | }
110 | if query
111 | else {}
112 | )
113 | host = [line.split(b": ")[1] for line in req_lines if b"Host:" in line][0]
114 |
115 | return ReqInfo(req_type, base_path, query_params, host)
116 |
117 | def login(self, params):
118 | ssid = unquote(params.get(b"ssid", None))
119 | password = unquote(params.get(b"password", None))
120 |
121 | # Write out credentials
122 | Creds(ssid=ssid, password=password).write()
123 |
124 | headers = (
125 | b"HTTP/1.1 307 Temporary Redirect\r\n"
126 | b"Location: http://{:s}\r\n".format(self.local_ip)
127 | )
128 |
129 | return b"", headers
130 |
131 | def connected(self, params):
132 | headers = b"HTTP/1.1 200 OK\r\n"
133 | body = open("./connected.html", "rb").read() % (self.ssid, self.local_ip)
134 | return body, headers
135 |
136 | def get_response(self, req):
137 | """generate a response body and headers, given a route"""
138 |
139 | headers = b"HTTP/1.1 200 OK\r\n"
140 | route = self.routes.get(req.path, None)
141 |
142 | if type(route) is bytes:
143 | # expect a filename, so return contents of file
144 | return open(route, "rb"), headers
145 |
146 | if callable(route):
147 | # call a function, which may or may not return a response
148 | response = route(req.params)
149 | body = response[0] or b""
150 | headers = response[1] or headers
151 | return uio.BytesIO(body), headers
152 |
153 | headers = b"HTTP/1.1 404 Not Found\r\n"
154 | return uio.BytesIO(b""), headers
155 |
156 | def is_valid_req(self, req):
157 | if req.host != self.local_ip:
158 | # force a redirect to the MCU's IP address
159 | return False
160 | # redirect if we don't have a route for the requested path
161 | return req.path in self.routes
162 |
163 | def read(self, s):
164 | """read in client request from socket"""
165 |
166 | data = s.read()
167 | if not data:
168 | # no data in the TCP stream, so close the socket
169 | self.close(s)
170 | return
171 |
172 | # add new data to the full request
173 | sid = id(s)
174 | self.request[sid] = self.request.get(sid, b"") + data
175 |
176 | # check if additional data expected
177 | if data[-4:] != b"\r\n\r\n":
178 | # HTTP request is not finished if no blank line at the end
179 | # wait for next read event on this socket instead
180 | return
181 |
182 | # get the completed request
183 | req = self.parse_request(self.request.pop(sid))
184 |
185 | if not self.is_valid_req(req):
186 | headers = (
187 | b"HTTP/1.1 307 Temporary Redirect\r\n"
188 | b"Location: http://{:s}/\r\n".format(self.local_ip)
189 | )
190 | body = uio.BytesIO(b"")
191 | self.prepare_write(s, body, headers)
192 | return
193 |
194 | # by this point, we know the request has the correct
195 | # host and a valid route
196 | body, headers = self.get_response(req)
197 | self.prepare_write(s, body, headers)
198 |
199 | def prepare_write(self, s, body, headers):
200 | # add newline to headers to signify transition to body
201 | headers += "\r\n"
202 | # TCP/IP MSS is 536 bytes, so create buffer of this size and
203 | # initially populate with header data
204 | buff = bytearray(headers + "\x00" * (536 - len(headers)))
205 | # use memoryview to read directly into the buffer without copying
206 | buffmv = memoryview(buff)
207 | # start reading body data into the memoryview starting after
208 | # the headers, and writing at most the remaining space of the buffer
209 | # return the number of bytes written into the memoryview from the body
210 | bw = body.readinto(buffmv[len(headers) :], 536 - len(headers))
211 | # save place for next write event
212 | c = WriteConn(body, buff, buffmv, [0, len(headers) + bw])
213 | self.conns[id(s)] = c
214 | # let the poller know we want to know when it's OK to write
215 | self.poller.modify(s, select.POLLOUT)
216 |
217 | def write_to(self, sock):
218 | """write the next message to an open socket"""
219 |
220 | # get the data that needs to be written to this socket
221 | c = self.conns[id(sock)]
222 | if c:
223 | # write next 536 bytes (max) into the socket
224 | try:
225 | bytes_written = sock.write(c.buffmv[c.write_range[0] : c.write_range[1]])
226 | except OSError:
227 | print('cannot write to a closed socket')
228 | return
229 | if not bytes_written or c.write_range[1] < 536:
230 | # either we wrote no bytes, or we wrote < TCP MSS of bytes
231 | # so we're done with this connection
232 | self.close(sock)
233 | else:
234 | # more to write, so read the next portion of the data into
235 | # the memoryview for the next send event
236 | self.buff_advance(c, bytes_written)
237 |
238 | def buff_advance(self, c, bytes_written):
239 | """advance the writer buffer for this connection to next outgoing bytes"""
240 |
241 | if bytes_written == c.write_range[1] - c.write_range[0]:
242 | # wrote all the bytes we had buffered into the memoryview
243 | # set next write start on the memoryview to the beginning
244 | c.write_range[0] = 0
245 | # set next write end on the memoryview to length of bytes
246 | # read in from remainder of the body, up to TCP MSS
247 | c.write_range[1] = c.body.readinto(c.buff, 536)
248 | else:
249 | # didn't read in all the bytes that were in the memoryview
250 | # so just set next write start to where we ended the write
251 | c.write_range[0] += bytes_written
252 |
253 | def close(self, s):
254 | """close the socket, unregister from poller, and delete connection"""
255 |
256 | s.close()
257 | self.poller.unregister(s)
258 | sid = id(s)
259 | if sid in self.request:
260 | del self.request[sid]
261 | if sid in self.conns:
262 | del self.conns[sid]
263 | gc.collect()
264 |
--------------------------------------------------------------------------------