Рубрики
Без рубрики

Плевочный веб-портал для ESP8266 с микроситоном – часть 3

В части 2 этой серии я закончил реализовать «пленочный портал» DNS-сервера, так что после CONNE … Помечено Python, Microphthon, ESP8266, розетки.

В части 2 этой серии я закончил реализацию DNS-сервера «Pictive Portal», чтобы после подключения к моим точке доступа WEMOS D1 Mini MCU все вопросы DNS-запросов указывают на IP-адрес MCU вместо фактического IP-адреса для запрошенного домена Отказ Я реализую этот пленнический портал как способ иметь возможность настроить MCU, чтобы иметь возможность войти в мой домашний Wi-Fi без жесткого образования SSID и пароль, так что мне нужно сделать дальше, устанавливается HTTP-сервер, который представит Форма, где я могу заполнить эти детали.

До сих пор CaptivePortal Класс создает DNS-сервер и регистрирует его сокет с рецептом, который слушает данные о потоке сокета. Это было довольно просто, поскольку UDP (который использует DNS) Bondless, и мне не нужно было отслеживать, было ли соединение полностью закрыто или нет. Я сделаю что-то подобное для HTTP-сервера, но нужно будет отслеживать, какие соединения входящие и исходящие, а которые закрыты. Это делает вещи немного сложнее, но я начну с моим классом Base Server и продолжаете шаг за шагом оттуда.

Создайте новый файл под названием captive_http.py и добавьте код ниже:

# captive_http.py
import usocket as socket

from server import Server

class HTTPServer(Server):
    def __init__(self, poller, local_ip):
        super().__init__(poller, 80, socket.SOCK_STREAM, "HTTP Server")
        if type(local_ip) is bytes:
            self.local_ip = local_ip
        else:
            self.local_ip = local_ip.encode()
        self.request = dict()
        self.conns = dict()

        # queue up to 5 connection requests before refusing
        self.sock.listen(5)
        self.sock.setblocking(False)

    def handle(self, sock, event, others):
        if sock is self.sock:
            # client connecting on port 80, so spawn off a new
            # socket to handle this connection
            print("- Accepting new HTTP connection")
            self.accept(sock)
         elif event & select.POLLIN:
            # socket has data to read in
            print("- Reading incoming HTTP data")
            self.read(sock)
       elif event & select.POLLOUT:
            # existing connection has space to send more data
            print("- Sending outgoing HTTP data")
            self.write_to(sock)

Вот основа для того, как будет выглядеть HTTP-сервер. У этого есть ручка () Метод, как и DNS-сервер, но в этом случае нам нужно рассмотреть три разных случая:

  1. Клиент хочет инициировать новый запрос с сервера HTTP. В этом случае событие придет по оригинальному розетку «Сервер», на порту 80. Я справимся с этим в Принять () Способ, нерешив новый «клиентский» сокет для обработки этого конкретного запроса/ответа. ПРИМЕЧАНИЕ: «Client Socket» в этом случае относится к розетке, принадлежащей серверу, но что нередит для обработки определенного запроса клиента. Напротив, «Server Socket» в этом обсуждении отвечает только за то, что для принятия новых подключений и создания «Client Socket» для них для них, но сама не отвечает на любой запрос.

  2. Существующее соединение (на клиентском розетке) отправила нам больше данных. Это означает Голлин Событие на розетке клиента и называет Читать () Способ для чтения данных из сокета, пока не дойдем до конца полного сообщения.

  3. Существующее соединение (на клиентском сокете) готова к нам, чтобы отправить больше данных клиенту. Это означает с помощью Восстановка Событие на клиентском розетке, и я справимся с этим в write_to () Способ, отправив следующий набор данных для этого соединения.

Одна точка, чтобы иметь в виду, что здесь, независимо от фактического размера входящих или исходящих сообщений, мы будем отправлять только сегменты 536 байтов за раз; Это по умолчанию Максимальный размер сегмента для TCP/IP.

  • Для чтения данных в сокете это относительно легко: просто прочитайте все данные, которые отправляются, пока не дойдем до конца. Для этого сервера нам нужно только обрабатывать запросы HTTP Get, поэтому мы можем предположить, что если мы найдем пустую строку в данных ( \ R \ n \ r \ n ), это конец сообщения. Если нам нужно также обрабатывать почтовые запросы, нам нужно будет прочитать в Длина содержимого или Переводное кодирование: Concaked Заголовки, чтобы определить, как узнать, когда запрос был закончен.
  • Для написания данных в сокете нам нужно разбить ответ на куски 536 байтов каждый. После отправки каждая кусок мы продвинуем указатель к тому, где будет следующий кусок данных, то вернуть и дождаться, чтобы опропиться сообщить нам, что сокет доступен для отправки большего количества данных. Как только нет больше данных, чтобы написать, мы закрываем сокет.

Принимая новое сокетное соединение

Начнем с самых простых случаев: принимая новое соединение. Здесь мы уже знаем, какое сокет ожидает, что включается запрос на соединение, поскольку это тот же сокет, который мы создали при инициализации («Сервер сокет»). Socket.Accept () Метод будет выпустить новый сокет («Client Socket») для использования для этого соединения и вернуть новую клиентскую сокет, а также адрес назначения, к которому он сочетается.

После того, как у меня есть новый клиентский сокет, я установил его в неблокирующую и многоразовую (например, серверную розетку), а затем добавить его в список открытых потоков, на котором Slier будет прослушивать события.

На данный момент сокет сервера восходит к прослушиванию новых входящих соединений, а вновь созданная розетка клиента выполнит работу чтения в запросе и запись ответа.

# captive_http.py
...
import uerrno
import uselect as select
...
class HTTPServer(Server):
    ...
    def accept(self, server_sock):
        """accept a new client request socket and register it for polling"""

        try:
            client_sock, addr = server_sock.accept()
        except OSError as e:
            if e.args[0] == uerrno.EAGAIN:
                return

        client_sock.setblocking(False)
        client_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.poller.register(client_sock, select.POLLIN)
    ...

Чтение от клиентской розетки

После того, как мы вырываемся с розетки клиента для запроса входящего соединения, и настроить его для опроса, мы должны ожидать последующего Голлин Событие (или несколько), где клиент отправляет нам данные, которые нам нужно прочитать. Как мы строим этот проект, мы можем предположить, что любой Голлин Событие, которое не принадлежит к розетке DNS, будет сознателем клиента для HTTP-сервера, поэтому мы можем просто пройти их все нашему новым Читать () метод ниже.

Вызов входящего сокета Читать () Метод дает нам некоторое количество данных, но мы не знаем аванс, будь то полное сообщение или частичное сообщение. Если нет данных, это хороший знак, который мы уже попали в конец сообщения, и мы должны просто закрыть розетку сейчас.

В противном случае, давайте проверим, мы уже наматываемся через входящее сообщение для этого клиентского сокета. Если мы есть, добавьте новые данные в существующее частичное сообщение, в противном случае создайте пустую ByteString и добавьте данные.

Здесь мы немного обманываем, так как нам нужно только обрабатывать запросы Get, и мы можем предположить, что любая пустая строка означает конец сообщения. Если последние четыре байта входящих данных не являются \ r \ n \ r \ n Мы ожидаем, что придет больше, так что вернитесь рано и ждать следующего Голлин мероприятие.

Если мы нашли пустую строку в конце, получите полный запрос и распечатайте его, чтобы мы могли посмотреть. Клиент будет ожидать сообщение обратно, поэтому пока давайте просто вверять до конца 404. Не найден Сообщение, чтобы получить их от наших спинок, пока мы не сможем кодировать способ отправки настоящего HTML.

# captive_http.py
...
import uio
...
class HTTPServer(Server):
    ...
    def read(self, s):
        """read in client request from socket"""

        data = s.read()
        if not data:
            # no data in the TCP stream, so close the socket
            self.close(s)
            return

        # add new data to the full request
        sid = id(s)
        self.request[sid] = self.request.get(sid, b"") + data

        # check if additional data expected
        if data[-4:] != b"\r\n\r\n":
            # HTTP request is not finished if no blank line at the end
            # wait for next read event on this socket instead
            return

        # get the completed request
        req = self.request.pop(sid)

        print("Client raw request:\n", req)

        # send a 404 response for now
        headers = b"HTTP/1.1 404 Not Found\r\n"
        body = uio.BytesIO(b"")
        self.prepare_write(s, body, headers)
    ...

Отправка на розетку клиента

Теперь все, что осталось, чтобы завершить транзакцию HTTP – это написать ответ на клиента. Помните, что мы ограничиваемся написанием 536 байтов за раз, но наш ответ может быть длиннее этого. В предыдущем разделе, после прочтения в запросе мы позвонили Prepare_write () С заголовками и телом мы хотели отправить обратно клиенту. Нам понадобится способ отслеживать, сколько данных мы выписали на каждый ответ, и где мы должны начать писать в следующий раз, когда тот же сокет готов принять больше нашего ответа.

Чтобы помочь следить за всем этим, я сделал NamedTuple Для соединения писателя, который содержит:

  • Тело полного ответа нам нужно отправить
  • Буфер, который будет содержать только следующие 536 (максимальные) байты, которые мы планируем отправить
  • MemoryView буфера, который поможет с записью и выходить из буфера без необходимости копирования
  • Диапазон для запуска и окончания байтов буфера, который мы хотим отправить дальше.

Обратите внимание, что я не поддерживаю копию заголовков, поскольку они всегда будут (в нашем случае) составляют менее 536 байтов и всегда будут выходить с первого сегмента сообщений.

Со всем тем, что на месте, я добавляю дополнительную новую линию для заголовков, чтобы получить пустую строку (требуется между заголовками и корпусом), а затем писать заголовки в буфер (дополненный, чтобы сделать размер буфера 536 байтов). Затем я использую MemoryView для чтения из организма в буфер, до максимальной суммы 536 байтов в буфере. Readinto () Вызов рассказывает мне, сколько байтов на самом деле читали из тела в буфер, и я использую это число для установки конечной точки write_range Отказ Я экономлю Writeconn информация в Self.conns Словарь с ключом идентификатора сокета, поэтому я могу посмотреть на это позже, когда мне нужно будет написать остаток сообщения.

Наконец, я позволил опрометчику узнать, что я готов пишеть в розетку, поэтому он может дать мне знать, когда сокет доступен для исходящих данных. Это значит, что я начну получать Восстановка События для этого сокета, которые мне нужно будет обрабатывать, чтобы на самом деле начать писать эти данные.

# captive_http.py
...
from collections import namedtuple
WriteConn = namedtuple("WriteConn", ["body", "buff", "buffmv", "write_range"])
...
class HTTPServer(Server):
    ...
    def prepare_write(self, s, body, headers):
        # add newline to headers to signify transition to body
        headers += "\r\n"
        # TCP/IP MSS is 536 bytes, so create buffer of this size and
        # initially populate with header data
        buff = bytearray(headers + "\x00" * (536 - len(headers)))
        # use memoryview to read directly into the buffer without copying
        buffmv = memoryview(buff)
        # start reading body data into the memoryview starting after
        # the headers, and writing at most the remaining space of the buffer
        # return the number of bytes written into the memoryview from the body
        bw = body.readinto(buffmv[len(headers) :], 536 - len(headers))
        # save place for next write event
        c = WriteConn(body, buff, buffmv, [0, len(headers) + bw])
        self.conns[id(s)] = c
        # let the poller know we want to know when it's OK to write
        self.poller.modify(s, select.POLLOUT)
    ...

Теперь, когда опросы установлен на проход Восстановка События к этому клиентскому розетку нам нужно обрабатывать эти события, которые будут вызывать звонок на наше новое write_to () метод. Во-первых, нам нужно вернуть писатель связи, который будет иметь все, что нам нужно, чтобы определить, какие байты должны быть выписаны. Помните, что write_range Значение этого кортежа – это список начальных и концевых позиций буфера, который мы должны написать:

  • Если буфер заполнен, это обычно будет [0, 536] И мы бы написали все содержимое буфера.
  • Если буфер только частично заполнен, второе значение в списке было бы чем-то менее 536.
  • Если мы прерваны во время записи в прошлый раз, первое значение может быть чем-то выше 0, и мы начнем писать с этой позиции вместо 0.

После записи в розетку мы проверяем, сколько было написано. Если бы мы не писали байты, или у нас было меньше 536 байтов, чтобы написать, мы закончили отправку этого ответа, и мы можем закрыть розетку.

В противном случае мы написали все байты в буфере, поэтому нам нужно сохранить разрыв сокета, продвиньте буфер к следующим байтам тела ответа, чтобы, когда следующий Восстановка событие приходит, мы будем готовы продолжить писать; Эта часть происходит в buff_advance () метод.

# captive_http.py
...
class HTTPServer(Server):
    ...
    def write_to(self, sock):
        """write the next message to an open socket"""

        # get the data that needs to be written to this socket
        c = self.conns[id(sock)]
        if c:
            # write next 536 bytes (max) into the socket
            bytes_written = sock.write(
                c.buffmv[c.write_range[0] : c.write_range[1]]
            )
            if not bytes_written or c.write_range[1] < 536:
                # either we wrote no bytes, or we wrote < TCP MSS of bytes
                # so we're done with this connection
                self.close(sock)
            else:
                # more to write, so read the next portion of the data into
                # the memoryview for the next send event
                self.buff_advance(c, bytes_written)

    def buff_advance(self, c, bytes_written):
        """advance the writer buffer for this connection to next outgoing bytes"""

        if bytes_written == c.write_range[1] - c.write_range[0]:
            # wrote all the bytes we had buffered into the memoryview
            # set next write start on the memoryview to the beginning
            c.write_range[0] = 0
            # set next write end on the memoryview to length of bytes
            # read in from remainder of the body, up to TCP MSS
            c.write_range[1] = c.body.readinto(c.buff, 536)
        else:
            # didn't read in all the bytes that were in the memoryview
            # so just set next write start to where we ended the write
            c.write_range[0] += bytes_written
    ...

Закрытие розетки клиента

Вышло несколько случаев выше, где нам нужно было закрыть розетку клиента, либо потому, что мы были сделаны, написав ему, либо делали чтение от него. Обратите внимание, что мы никогда не закрываем сокет сервера (пока не остановим нашу программу), поскольку мы хотим, чтобы мы могли продолжить прослушивание запросов входящие соединения.

Чтобы чисто закрыть клиентскую розетку, нам также нужно отменить его от опросы и удалить любые оставшиеся запросы или данные подключения, которые мы сохранили для него. Поскольку у нас очень ограниченная оперативная память на ESP8266, мы делаем это вручную, а затем позвонили в сборщик мусора, чтобы убедиться, что мы сохраняем каждый единственный байт RAM как можно раньше, чтобы избежать ошибок памяти.

# captive_http.py
...
import gc
...
class HTTPServer(Server):
    ...
    def close(self, s):
        """close the socket, unregister from poller, and delete connection"""

        s.close()
        self.poller.unregister(s)
        sid = id(s)
        if sid in self.request:
            del self.request[sid]
        if sid in self.conns:
            del self.conns[sid]
        gc.collect()
    ...

Теперь, когда у нас есть скелет HTTP-сервера, давайте создать экземпляр один в CaptivePortal Класс и добавьте его в петлю избирательного участка. Мы можем проверить, что мы фактически читаем данные правильно, прежде чем перейдем к правильно правильно обработке запросов.

Ранее мы написали наши handle_dns () Метод, чтобы он вернул false, если он не обрабатывает событие потока и True, если это сделало. Мы воспользуемся этим здесь, чтобы убедиться, что HTTP-сервер не пытается обрабатывать события, которые не принадлежат ему или одному из его дочерних клиентских розетков.

# captive_portal.py
...
from captive_dns import DNSServer
from captive_http import HTTPServer

class CaptivePortal:
    ...
    def __init__(self, essid=None):
        ...
        self.dns_server = None
        self.http_server = None
        ...

    def captive_portal(self):
        print("Starting captive portal")
        self.start_access_point()

        if self.http_server is None:
            self.http_server = HTTPServer(self.poller, self.local_ip)
            print("Configured HTTP server")
        if self.dns_server is None:
            self.dns_server = DNSServer(self.poller, self.local_ip)
            print("Configured DNS server")

        try:
            while True:
                gc.collect()
                # check for socket events and handle them
                for response in self.poller.ipoll(1000):
                    sock, event, *others = response
                    is_handled = self.handle_dns(sock, event, others)
                    if not is_handled:
                        self.handle_http(sock, event, others)
        except KeyboardInterrupt:
            print("Captive portal stopped")
        self.cleanup()

    def handle_http(self, sock, event, others):
        self.http_server.handle(sock, event, others)

    ...

Время, чтобы проверить это. Скопируйте код на MCU и запустите его. Подключитесь к точке доступа MCU с вашим телефоном, а затем откройте браузер и попробуйте перейти к не HTTPS Страница ( http://neverssl.com , http://example.com. , Например).

Entering REPL. Use Control-X to exit.
>
MicroPython v1.12 on 2019-12-20; ESP module with ESP8266
Type "help()" for more information.
>>>
>>> import main
Trying to load WiFi credentials from ./wifi.creds
./wifi.creds does not exist
Starting captive portal
Waiting for access point to turn on
#37 ets_task(4020f510, 29, 3fff8f88, 10)
AP mode configured: ('192.168.4.1', '255.255.255.0', '192.168.4.1', '192.168.4.1')
HTTP Server listening on ('0.0.0.0', 80)
Configured HTTP server
DNS Server listening on ('0.0.0.0', 53)
Configured DNS server
Sending connectivitycheck.gstatic.com. -> 192.168.4.1
- Accepting new HTTP connection
- Reading incoming HTTP data
Client raw request:
 b'GET /generate_204 HTTP/1.1\r\nUser-Agent: Dalvik/2.1.0 (Linux; U; Android 6.0.1;
 Nexus 7 Build/MOB30X)\r\nHost: connectivitycheck.gstatic.com\r\nConnection:
 Keep-Alive\r\nAccept-Encoding: gzip\r\n\r\n'
- Sending outgoing HTTP data
Captive portal stopped
Cleaning up
DNS Server stopped

Это выглядит хорошо. Мы видим, что DNS-сервер перенаправляет все домены на IP-адрес MCU, а затем HTTP-сервер принимает соединение и чтение в данных запроса. Этот конкретный запрос – это мое устройство Android, проверяющее, есть ли он активным Интернетом или нет. Устройства iOS делают что-то подобное, но с другой конечной точкой. Если вы хотели обмануть устройство, думая, что он на самом деле имел подключение к Интернету, все, что вам нужно сделать, это отвечать на этот запрос с помощью 204 № Содержание отклик. Если вам интересно, как это работает, Эта статья имеет несколько дополнительных деталей.

До сих пор мы отвечаем на все запросы с 404 не найден заголовок и пустое тело. Очевидно, нам придется прислать реальные ответы, чтобы сделать эту работу. Есть две вещи, которые нам нужно начать с этим:

  1. Какой-то фактический контент в виде файлов HTML.
  2. Способ изучить запрос, получить запрошенный путь и сопоставить, что в определенный HTML-файл.

Плевовающая портала посадка HTML страница

Поскольку целью этого проекта – позволить мне ввести свои полномочия WiFi WiFi, поэтому MCU может подключиться к нему, мне понадобится простая страница HTML с формой, где я могу ввести эти детали. Я создал файл под названием index.html с базовой формой и немного встроенным стилем. Поскольку это чрезвычайно простая страница на очень базовом сервере, я в порядке с использованием Inline CSS вместо отдельного файла, чтобы сделать для меня сервирующие вещи.



  
    
    
    WiFi Login
    
  
  
    

WiFi login credentials



Отображение файлов на HTTP Paths

Теперь, когда у меня есть HTML-файл, мне нужно сообщить моему серверу, какие HTTP Paths должен служить этому файлу. Давайте добавим маршруты Словарь к Httpserver класс. Если бы я сделал более общего назначения HTTP-сервера, я бы, вероятно, хотел создать способ добавления маршрутов, и пусть родитель создает маршруты вместо того, чтобы сервер сделать это напрямую. В этом случае, однако, у меня будет очень ограниченное количество маршрутов, которые никогда не изменится, поэтому я просто собираюсь создать их через буквальный словарь в конструкторе HTTPSERVER.

# captive_http.py
...
class HTTPServer(Server):
    def __init__(self, poller, local_ip):
        ...
        self.request = dict()
        self.conns = dict()
        self.routes = {b"/": b"./index.html"}
        ...

HTTP маршрутизация на основе запрошенного пути

Теперь, когда у нас есть созданный файл и знаете, какой маршрут должен служить этому файлу, нам просто нужно изучить каждый входящий запрос, чтобы увидеть, какой путь запрашивается, и отвечать соответствующим образом.

Во-первых, давайте анализировать HTTP-запрос и вытащить детали, которые нас могут быть заинтересованы в:

# captive_http.py
...
ReqInfo = namedtuple("ReqInfo", ["type", "path", "params", "host"])
...

class HTTPServer(Server):
    ...
    def parse_request(self, req):
        """parse a raw HTTP request to get items of interest"""

        req_lines = req.split(b"\r\n")
        req_type, full_path, http_ver = req_lines[0].split(b" ")
        path = full_path.split(b"?")
        base_path = path[0]
        query = path[1] if len(path) > 1 else None
        query_params = (
            {
                key: val
                for key, val in [param.split() for param in query.split(b"&")]
            }
            if query
            else {}
        )
        host = [line.split(b": ")[1] for line in req_lines if b"Host:" in line][0]

        return ReqInfo(req_type, base_path, query_params, host)

    def read(self, s):
        ...
        # get the completed request
        req = self.parse_request(self.request.pop(sid))

        # send a 404 response for now
        headers = b"HTTP/1.1 404 Not Found\r\n"
        body = uio.BytesIO(b"")
        self.prepare_write(s, body, headers)
    ...

Затем я создаю функцию помощника, чтобы принять анаразложенную запрос и либо вернуть либо содержимое HTML-файла, если маршрутный матч или еще пустая байтовая строка. Добавьте следующее в Captive_html.py :

# captive_http.py
...
class HTTPServer(Server):
    ...
    def get_response(self, req):
        """generate a response body and headers, given a route"""

        headers = b"HTTP/1.1 200 OK\r\n"
        route = self.routes.get(req.path, None)

        if type(route) is bytes:
            # expect a filename, so return contents of file
            return open(route, "rb"), headers

        headers = b"HTTP/1.1 404 Not Found\r\n"
        return uio.BytesIO(b""), headers
    ...

Если маршрут найден, я получу Байты Строна с пути к файлу HTML, и я открою этот файл (в двоичном режиме) и верните объект потока файла.

Если маршрут не найден, я получу Нет Из словаря, а вместо этого я верну пустой бэйт потоковой объект. В обоих случаях я возвращаю соответствующие заголовки вместе с телом.

Давайте изменим Читать () Функция, чтобы попытаться получить тело в зависимости от маршрута, а также генерировать правильный заголовок в зависимости от того, был ли маршрут сопоставлен или нет:

# captive_http.py
...
class HTTPServer(Server):
    ...
    def read(self, s):
        ...
        # get the completed request
        req = self.parse_request(self.request.pop(sid))

        body, headers = self.get_response(req)

        self.prepare_write(s, body, headers)
    ...

Время, чтобы проверить это. Скопируйте все новые/измененные файлы на MCU и выпустите его.

Примечание: Не забудьте скопировать свой новый index.html Файл на MCU, а также .py файлы. Поместите файлы HTML в той же папке ( /pyboard ) как другие.

После того, как Pointive Portal работает, подключитесь к точке доступа к WiFi MCU и перейдите к http://192.168.4.1/

На самом деле, если вы перейдите на любой сайт только HTTP-только, на корневом пути, вы получите ту же страницу, поскольку наш DNS-сервер сообщает клиенту, что все домены указывают на IP-адрес MCU. Вы можете проверить это, навигацию на любой сайт, который не использует HTTPS (попробуйте http://neverssl.com для тестирования). Это определенно прогресс, но все же не совсем то, что мы хотим. Если (например) я попробовал навигацию на http://neverssl.com/online. Я бы получил 404. Не найден Вернуться с сервера вместо этого перенаправляя меня на страницу входа в систему, которую я хочу. Кроме того, он вроде ошибок мне, что домен, который я попробовал навигацию, чтобы все еще отображается в адресной строке браузера вместо IP-адреса MCU.

Оба этими вопроса могут быть в основном решены таким же образом: если запрашиваемый хост или путь от клиента не являются теми, которые я хочу служить, я могу отправить ответ перенаправления обратно на клиента, чтобы указать им, где я хочу, чтобы они пошли Отказ Этот ответ просто будет пустым телом с 307 временных перенаправления заголовок

Давайте изменим Httpserver.read () Метод для проверки действительного запроса. Если запрашиваемый хост соответствует IP-адресу MCU, и маршрут известен, затем обслуживайте страницу для этого маршрута. В противном случае верните ответ перенаправления, указав на корневой путь IP-адреса MCU.

# captive_http.py
...
class HTTPServer(Server):
    ...
    def read(self, s):
        ...
        # get the completed request
        req = self.parse_request(self.request.pop(sid))

        if not self.is_valid_req(req):
            headers = (
                b"HTTP/1.1 307 Temporary Redirect\r\n"
                b"Location: http://{:s}/\r\n".format(self.local_ip)
            )
            body = uio.BytesIO(b"")
            self.prepare_write(s, body, headers)
            return

        # by this point, we know the request has the correct
        # host and a valid route
        body, headers = self.get_response(req)

        self.prepare_write(s, body, headers)
    ...

Тогда мы можем написать IS_Valid_req () Метод такой:

# captive_http.py
...
class HTTPServer(Server):
    ...
    def is_valid_req(self, req):
        if req.host != self.local_ip:
            # force a redirect to the MCU's IP address
            return False
        # redirect if we don't have a route for the requested path
        return req.path in self.routes
    ...

Идите вперед и проверьте это. Любой не-HTTPS домен, который вы пытаетесь, с любым путем, должен перенаправить вас обратно в http://192.168.4.1 , который соответствует маршруту на странице индекса с помощью формы входа.

Мы добились прогресса, но все еще не имеют функционирующего продукта. Mini D1 имеет рабочий DNS-сервер для указания всех запросов домена к локальному IP-адресу и HTTP-сервере, который будет перенаправлять все неизвестные хосты и пути к корневому пути локального IP-адреса, который теперь будет служить форме, спрашиваемой WiFi SSID и пароль, в котором мы хотим, чтобы Mini D1 подключиться в будущем.

Все, что осталось, это фактически разбирать представление формы, у D1 Mini пытаются подключиться к новому Wi-Fi, а затем позволить пользователю знать, если это было успешно. Это растягивается в более длинную перезагружение, чем я ожидал, но я не хотел слишком много скачать на детали, или сделать любой пост слишком долго, чтобы с комфортом.

Оригинал: “https://dev.to/ansonvandoren/captive-web-portal-for-esp8266-with-micropython-part-3-elg”