Организация безопасного HTTPS сервера средствами Python
Основной способ: HTTPS сервер на базе http.server и SSLContext
Наиболее эффективное решение для быстрого запуска HTTPS сервера на Python использует встроенные модули http.server и ssl. Этот подход не требует установки дополнительных библиотек и подходит для тестирования, локальной разработки или простых внутренних сервисов.
Прежде чем запустить сервер, нужно создать самоподписанный SSL сертификат. Сделать это можно командой OpenSSL:
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes -subj '/CN=localhost'Python file server (файловый сервер на python)
Полученные файлы cert.pem и key.pem будут использованы для шифрования.
Пример кода HTTPS сервера, обслуживающего текущую директорию:
import http.server
import ssl
server_address = ('', 443)
httpd = http.server.HTTPServer(server_address, http.server.SimpleHTTPRequestHandler)
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain(certfile='cert.pem', keyfile='key.pem')
httpd.socket = context.wrap_socket(httpd.socket, server_side=True)
print('HTTPS server running on https://localhost:443')
httpd.serve_forever()Python https server (запуск https сервера на python)
Пояснение шагов: server_address задаёт IP (пустая строка означает все интерфейсы) и порт 443 (стандартный для HTTPS). Создаётся экземпляр HTTPServer с обработчиком SimpleHTTPRequestHandler, который раздаёт файлы из рабочей директории. Затем создаётся SSLContext с протоколом TLS_SERVER, загружаются сертификат и ключ. Метод wrap_socket заменяет обычный сокет на TLS сокет.
Типичные ошибки и их решение:
- Ошибка 'Permission denied' при запуске на порту 443. Решение: использовать порт выше 1024, например 8443, или запустить скрипт с правами root.
- SSL: CERTIFICATE_VERIFY_FAILED. Возникает при подключении браузером, так как сертификат самоподписанный. Решение: добавить сертификат в доверенные корневые центры или нажать 'Принять риск'.
- Модуль ssl не найден. Решение: обновить Python до версии 3.4 или выше, либо установить openssl.
Как запустить HTTPS сервер для Flask приложения?
Flask предоставляет встроенную поддержку HTTPS через параметр ssl_context метода run(). Это удобно для разработки или небольших проектов.
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return 'Hello, HTTPS!'
if __name__ == '__main__':
app.run(ssl_context=('cert.pem', 'key.pem'), host='0.0.0.0', port=8443)В этом примере передаётся кортеж из путей к сертификату и ключу. Приложение будет слушать на всех интерфейсах на порту 8443. Если сертификаты ещё не созданы, их можно сгенерировать командами OpenSSL, как показано выше.
Проблема: при использовании ssl_context в Flask может возникнуть ошибка 'ssl_context' is an invalid keyword argument для некоторых версий Werkzeug. Решение: установить последнюю версию Flask или Werkzeug (pip install --upgrade werkzeug).
Как создать асинхронный HTTPS сервер на aiohttp?
Фреймворк aiohttp отлично подходит для высоконагруженных асинхронных серверов. Настройка SSL аналогична предыдущим примерам.
import asyncio
from aiohttp import web
import ssl
async def handle(request):
return web.Response(text='Async HTTPS server!')
app = web.Application()
app.router.add_get('/', handle)
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ssl_context.load_cert_chain('cert.pem', 'key.pem')
if __name__ == '__main__':
web.run_app(app, host='0.0.0.0', port=8443, ssl_context=ssl_context)Здесь используется ssl.create_default_context с целью CLIENT_AUTH (сервер аутентифицируется перед клиентом). Затем загружаются сертификаты. Метод run_app автоматически оборачивает сокет в TLS.
Распространённая ошибка: RuntimeError: no running event loop. Решение: убедиться, что вызов run_app происходит только один раз и в главном потоке.
Как использовать Twisted для HTTPS сервера?
Twisted - асинхронный движок с готовыми компонентами для SSL. Пример минимального HTTPS сервера, возвращающего фиксированный ответ:
from twisted.internet import reactor
from twisted.web.server import Site
from twisted.web.resource import Resource
import twisted.internet.ssl as tssl
class HelloResource(Resource):
isLeaf = True
def render_GET(self, request):
return b'Hello from Twisted HTTPS!'
factory = Site(HelloResource())
ssl_context = tssl.DefaultOpenSSLContextFactory(
'key.pem',
'cert.pem'
)
reactor.listenSSL(8443, factory, ssl_context)
reactor.run()Класс DefaultOpenSSLContextFactory принимает пути к приватному ключу и сертификату. Метод listenSSL запускает слушатель с SSL.
Проблема: если используется нестандартное имя хоста, сертификат может быть отклонён. Решение: при генерации сертификата указать подходящее значение -subj '/CN=ваш_домен'.
Как поднять HTTPS сервер без внешних библиотек (чистый socket)?
Низкоуровневый подход - использовать модуль socket и ssl. Это даёт полный контроль, но требует ручной обработки запросов.
import socket
import ssl
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain('cert.pem', 'key.pem')
bindsocket = socket.socket()
bindsocket.bind(('0.0.0.0', 8443))
bindsocket.listen(5)
print('HTTPS server waiting for connections...')
while True:
newsocket, fromaddr = bindsocket.accept()
connstream = context.wrap_socket(newsocket, server_side=True)
try:
data = connstream.recv(1024)
response = b'HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nHello raw socket!'
connstream.sendall(response)
finally:
connstream.shutdown(socket.SHUT_RDWR)
connstream.close()Здесь мы вручную принимаем соединение, оборачиваем его в SSL и отправляем простой HTTP ответ. Такой код полезен для изучения протокола или реализации кастомных серверов.
Ошибка: ssl.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] при использовании load_cert_chain. Решение: проверить, что файлы сертификата и ключа существуют и не повреждены. Также ключ не должен быть защищён паролем (или укажите пароль через аргумент password).
Расширенные примеры и нестандартные сценарии
Ниже приведены более сложные и узкоспециализированные примеры настройки HTTPS в Python.
Программная генерация самоподписанного сертификата на Python
Вместо вызова OpenSSL из командной строки можно сгенерировать сертификат средствами библиотеки cryptography. Установите её: pip install cryptography.
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.backends import default_backend
import datetime
# Создание приватного ключа
key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=default_backend()
)
# Создание самоподписанного сертификата
subject = issuer = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, u'RU'),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, u'Test'),
x509.NameAttribute(NameOID.COMMON_NAME, u'localhost'),
])
cert = x509.CertificateBuilder().subject_name(
subject
).issuer_name(
issuer
).public_key(
key.public_key()
).serial_number(
x509.random_serial_number()
).not_valid_before(
datetime.datetime.utcnow()
).not_valid_after(
datetime.datetime.utcnow() + datetime.timedelta(days=365)
).sign(key, hashes.SHA256(), default_backend())
# Сохранение в файлы
with open('key_crypto.pem', 'wb') as f:
f.write(key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption()
))
with open('cert_crypto.pem', 'wb') as f:
f.write(cert.public_bytes(serialization.Encoding.PEM))
print('Сертификат и ключ сгенерированы' )Результат: два файла (key_crypto.pem, cert_crypto.pem) готовы к использованию.
Настройка HTTPS с поддержкой HTTP/2
Для HTTP/2 требуется библиотека hyper-h2 или использование aiohttp с включённой поддержкой HTTP/2 через async_timeout. Но самый простой способ - использовать nghttp2 и Python обёртку python-nghttp2. Пример с aiohttp (начиная с версии 3.0) поддерживает HTTP/2 если установлена библиотека aiohttp[h2].
Установка: pip install aiohttp h2. Код остаётся тем же, что и в разделе про aiohttp, но теперь сервер автоматически использует HTTP/2 для совместимых клиентов.
Использование Let's Encrypt для получения валидного сертификата
Для продакшена необходимо получить сертификат от удостоверяющего центра. Проще всего воспользоваться клиентом certbot от Let's Encrypt.
sudo certbot certonly --standalone -d example.com --preferred-challenges httpПосле выполнения сертификаты будут сохранены в /etc/letsencrypt/live/example.com/fullchain.pem и privkey.pem. Затем используйте их в любом из рассмотренных серверов, указав полные пути. Для автоматического обновления добавьте задачу в cron:
0 0 * * * /usr/bin/certbot renew --quietРезультат: сертификат обновляется каждые 60 дней без вмешательства.
Клиент для проверки HTTPS сервера (Python)
Чтобы проверить работу сервера, можно написать простой HTTPS клиент, который игнорирует ошибки сертификата (для самоподписанных).
import urllib.request
import ssl
ssl_ctx = ssl.create_default_context()
ssl_ctx.check_hostname = False
ssl_ctx.verify_mode = ssl.CERT_NONE
req = urllib.request.Request('https://localhost:8443')
response = urllib.request.urlopen(req, context=ssl_ctx)
print(response.read().decode())Если сервер работает, клиент выведет содержимое ответа (например, 'Hello, HTTPS!').
Ограничение доступа по IP и базовая авторизация
Комбинируя HTTPS с обработчиком, можно добавить проверку IP и авторизацию. Пример для встроенного http.server::
import http.server
import ssl
class RestrictedHandler(http.server.BaseHTTPRequestHandler):
allowed_ips = ['127.0.0.1', '192.168.1.100']
def do_GET(self):
if self.client_address[0] not in self.allowed_ips:
self.send_error(403, 'Forbidden')
return
# Базовая проверка Authorization
auth = self.headers.get('Authorization')
if not auth or auth != 'Bearer secret123':
self.send_response(401)
self.send_header('WWW-Authenticate', 'Bearer realm="example"')
self.end_headers()
return
self.send_response(200)
self.send_header('Content-type', 'text/plain')
self.end_headers()
self.wfile.write(b'Access granted!')
server_address = ('', 8443)
httpd = http.server.HTTPServer(server_address, RestrictedHandler)
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain('cert.pem', 'key.pem')
httpd.socket = context.wrap_socket(httpd.socket, server_side=True)
httpd.serve_forever()Такой сервер разрешает доступ только с определённых IP и требует токен в заголовке Authorization.
Результат работы (проверка через curl):
$ curl -k -H 'Authorization: Bearer secret123' https://localhost:8443/ Access granted! $ curl -k https://localhost:8443/ Unauthorized (401)