├── .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 |
58 |

WiFi login credentials

59 | 60 | 61 |
62 | 63 | 69 | 70 |
71 | 72 | 73 |
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 | --------------------------------------------------------------------------------