When trying out JavaScript code in a browser, Python's HTTP server comes in handy as an easy way. The Progressive Web App (PWA), which has been the focus of attention in the JavaScript community since 2016, requires HTTPS, but since there was only an article about changing the ssl module in Python's HTTPS server and Python 3 series. , I decided to summarize what I investigated.
It assumes Python 3.6 and OpenSSL 1.0.2 and later versions. Various methods and constants have been added to the Python 3 series ssl module, and the sample code may not work on Python 3.6 and earlier versions.
You can find out the version of OpenSSL from the ssl constants.
>>> import ssl
>>> ssl.OPENSSL_VERSION
If you want to start the server that provides the HTML files from the command line, run the following command:
python3 -m http.server 8000
If you want to use CGI, specify the --cgi
option.
python3 -m http.server --cgi 8000
The script you want to run must be in / cgi-bin
or / htbin
.
#!/usr/bin/env python3
print("Content-Type: text/plain; charset=utf-8;\r\n")
print("hello world")
You can generate a self-signed certificate and private key with the following one-liner.
# https://stackoverflow.com/a/41366949/531320
openssl req -x509 -newkey rsa:4096 -sha256 \
-nodes -keyout server.key -out server.crt \
-subj "/CN=example.com" -days 3650
In addition to the http.server
module, you need to load the ssl
module.
Create a SSLContext
object, specify various settings, and then use SSLContext.wrap_socket
to create a SSLSocket
object.
server.py
from http.server import HTTPServer, SimpleHTTPRequestHandler
import ssl
def run(host, port, ctx, handler):
server = HTTPServer((host, port), handler)
server.socket = ctx.wrap_socket(server.socket)
print('Server Starts - %s:%s' % (host, port))
try:
server.serve_forever()
except KeyboardInterrupt:
pass
server.server_close()
print('Server Stops - %s:%s' % (host, port))
if __name__ == '__main__':
host = 'localhost'
port = 8000
ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ctx.load_cert_chain('server.crt', keyfile='server.key')
ctx.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
handler = SimpleHTTPRequestHandler
run(host, port, ctx, handler)
ssl.OP_NO_TLSv1
and ssl.OP_NO_TLSv1_1
mean to disable TLS 1.0 and TLS 1.1, respectively.
For CGI, use CGIHTTPRequestHandler
instead of SimpleHTTPRequestHandler
.
server.py
from http.server import HTTPServer, CGIHTTPRequestHandler
import ssl
def run(host, port, ctx, handler):
server = HTTPServer((host, port), handler)
server.socket = ctx.wrap_socket(server.socket)
print('Server Starts - %s:%s' % (host, port))
try:
server.serve_forever()
except KeyboardInterrupt:
pass
server.server_close()
print('Server Stops - %s:%s' % (host, port))
if __name__ == '__main__':
host = 'localhost'
port = 8000
ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ctx.load_cert_chain('server.crt', keyfile='server.key')
ctx.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
handler = CGIHTTPRequestHandler
handler.cgi_directories = ['/cgi-bin', '/htbin']
# https://stackoverflow.com/a/27303995/531320
handler.have_fork=False
run(host, port, ctx, handler)
For macOS and Unix, the server will not work without adding handler.have_fork = False
.
The advantage of CGI is that it can run programs in languages other than Python. The Node.js code looks like this:
node.cgi
#!/usr/bin/env node
console.log("Content-type: text/plain; charset=utf-8\n");
console.log("Hello World");
You can use the wsgiref
module to set up a WSGI-enabled server.
server.py
from wsgiref.simple_server import make_server
from pathlib import Path
import ssl
def simple_app(env, start_response):
info = env['PATH_INFO'][1:]
if info == '':
info = 'index.html'
root = Path.cwd()
path = root.joinpath(info).resolve()
if root in path.parents and path.is_file():
status = '200 OK'
body = path.read_bytes()
suffix = path.suffix
content_type = {
'.html': 'text/html',
'.txt': 'text/plain',
'.json': 'application/json'
}.get(suffix, 'text/plain')
else :
body = b'404 Not Found'
status = '404 Not Found'
content_type = 'text/plain'
headers = [
('Content-Type', content_type),
('Content-Length', str(len(body)))
]
start_response(status, headers)
return [body]
def run(host, port, ctx, app):
server = make_server(host, port, app)
server.socket = ctx.wrap_socket(server.socket)
print('Server Starts - %s:%s' % (host, port))
try:
server.serve_forever()
except KeyboardInterrupt:
pass
server.server_close()
print('Server Stops - %s:%s' % (host, port))
if __name__ == '__main__':
host = 'localhost'
port = 8000
ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ctx.load_cert_chain('server.crt', keyfile='server.key')
ctx.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
run(host, port, ctx, simple_app)
If you want to know all the defined environment variables (ʻenv), go to [PEP-333](https://www.python.org/dev/peps/pep-0333/#environ-variables) Please refer. You can also check from the page displayed when
wsgiref.simple_server.demo_app` is made an execution application.
SSLContext
ssl.create_default_context
is recommended for general usage.
ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
The constants you can specify are Purpose.SERVER_AUTH
(used to create a client-side socket) and Purpose.CLIENT_AUTH
(used to create a server-side socket).
You can also use the SSLContext
constructor directly.
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
Possible constants are PROTOCOL_TLS
(default), PROTOCOL_TLS_CLIENT
, and PROTOCOL_TLS_SERVER
. Various constants that were available in Python 3.6 and earlier have been deprecated.
You can check or add settings through options. You can see the default options as follows:
>>> import ssl
>>> ssl.create_default_context().options
<Options.OP_ALL|OP_NO_SSLv3|OP_NO_SSLv2|
OP_CIPHER_SERVER_PREFERENCE|
OP_SINGLE_DH_USE|OP_SINGLE_ECDH_USE|
OP_NO_COMPRESSION: 2203714559>
The following code gives the same result.
>>> ssl.SSLContext().options
The default options for Python 3.6 are:
You can access or change context options after wrapping a socket object with wrap_socket
.
server = HTTPServer((host, port), handler)
server.socket = ctx.wrap_socket(server.socket)
print(server.socket.context.options)
The arguments that can be specified for ssl.wrap_socket
and SSLContext.wrap_socket
are different. The arguments in Python 3.6 are:
ssl.wrap_socket(
sock,
keyfile=None,
certfile=None,
server_side=False,
cert_reqs=CERT_NONE,
ssl_version={see docs},
ca_certs=None,
do_handshake_on_connect=True,
suppress_ragged_eofs=True,
ciphers=None
)
SSLContext.wrap_socket(
sock,
server_side=False,
do_handshake_on_connect=True,
suppress_ragged_eofs=True,
server_hostname=None,
session=None
)
The difference between the two is that SSLContext.wrap_socket
has fewer options to specify. The session
added to SSLContext.wrap_socket
in Python 3.6 cannot be specified in ssl.wrap_socket
.
The criteria for using ssl.wrap_socket
and SSLContext.wrap_socket
is whether there are many or few settings.
If you use ssl.wrap.socket
, the settings will be changed through server.socket.context
, so a large number of settings will make the code harder to read.
Another advantage of creating a SSLContext
object directly is that it can be used for a third-party HTTP library.
Cipher suites can be found with get_ciphers
. Use set_ciphers
to change the cipher suite.
>>> import ssl
>>> ctx = ssl.create_default_context().get_ciphers()
>>> ctx.set_ciphers('ECDHE+AESGCM:!ECDSA')
>>> ctx.get_ciphers()
A recommended cipher suite is available on Mozilla's Site (https://wiki.mozilla.org/Security/Server_Side_TLS). Cloudflare has published an nginx config file (https://github.com/cloudflare/sslconfig) for TLS 1.3. OpenSSL 1.1.0 is part of TLS 1.3 and will be fully supported by OpenSSL 1.1.1.
PCI DSS v3.2, which is used for credit card payment services, requires SSL and TSL 1.0 to be disabled until the end of June 2018. Fastly, the CDN service used by PyPI, also disables TLS 1.1.
You can find out which version of TLS supported by Python 3 installed on your system with the following one-liner.
# https://news.ycombinator.com/item?id=13539034
python3 -c "import json, urllib.request; print(json.loads(urllib.request.urlopen('https://www.howsmyssl.com/a/check').read().decode('UTF-8'))['tls_version'])"
PEP 543 proposes the introduction of a new API for TLS. Development motivations include the fact that the ssl module relies on OpenSSL, which means that Python must be recompiled to introduce the new OpenSSL and that it is not possible to switch to different TLS libraries than OpenSSL. I will.
The main SSL / TLS libraries are:
Recommended Posts