今天看到光头哥更新的最新一篇博文:Running Your Flask Application Over HTTPS,虽然他讲的是如何改进配置使用更安全的 Https 服务,然而我更感兴趣的是其中的一段:

以前印象中看过 Flask 是原生支持 ssl 的,但是当时没怎么在意,今天突然来了兴趣,在想 Flask 是如何实现的,所以就扒了一下源码,发现关键不在于 Flask,还是它的底层包:Werkzeug,所以就继续看看 Werkzeug 的源码,看下具体是如何实现的,为了简化一下源码解析,直接从 Werkzeug 如何提供 Https 服务建起。

使用 Werkzeug 创建 Https 服务器

这里其实有个前提没有介绍,那就是读者可能需要有一些关于 Https 的基础知识,如果没有的话,可以想看一下 wiki 中关于 https 的内容了解一下背景,然后再继续。

生成 https 证书

在创建一个 https 服务器之前,我们需要先生成一份证书,对于生产环节的证书都是需要验证过的,但是对于我们实验的话,那就随便了,这里我有一个简单的代码可以生成证书,可以直接拿来使用:

执行之后,我们就可以在指定的目录下看到证书和公钥了,默认的路径是:/tmp/flask/key/

看到这些我们就可以确认我们的证书和公钥是生成好了,可以进行下一步了。

创建 Werkzeug https 服务器

关于 Werkzeug ,在我的博客之前写过一篇介绍的文章,感兴趣的同学可以先看一下:werkzeug初级指导;这里就不多做累赘了,直接给出一份我写的简单服务器代码:

出于我的习惯,我平时都是编写一份启动脚本的,这里也一并给出来: run.sh

执行 ./run.sh 之后我们应该可以看到一些 Log,然后尝试访问一下,我这里的配置都在 run.sh 里面了,所以访问的时候就直接访问:https://localhost:9527,但是,可以在 chrome 中发现有警报:

当然,这个警告是正常的,因为之前说了,这个证书是未授权的,所以会警报,这里也不延伸如何解决这种警告(根本性解决),我们可以选择高级选项选择信任并且打开,你讲可以成功访问这个服务器:

这个例子只是想演示一下如何使用 Werkzeug 创建一个 https 服务器,并且是能够访问成功的,但是咱们的正事是探究 Werkzeug 是如何实现的。接下来,将以这个例子进行深入,看看 Werkzeug 的实现原理。

Werkzeug 实现

截止至写这篇文章的时候,Werkzeug 最新的稳定版本是 0.12.1,所以源码就以 0.12.1 这个版本为例进行阅读。基于刚才的 Demo 代码,我们忽略前面创建 WSGI 引用的过程,直接看关键的一行,其实也就这行是真正和 https 相关的,所以我们就以这行为出发点,进行深入:

在 werkzeug 的源代码:werkzeug/serving.py 中我们可以看到 run_simple 的源代码:

这里忽略了很多代码,同时因为工具限制,代码行数后续有点混乱,但是,无妨,不影响阅读,最后有两行是标红的,其实这两行也就是 run_simple 的核心代码了,为了简单,我们就以 use_reloader = False 看,那么,其实 run_simple 的代码就可以简约成这样:

寥寥几行代码,所以我们一下子就看出来了,核心还得看 make_server 这个函数的实现,所以毫不犹豫得就跟进去了,位置在 werkzeug/serving.py line: 534 处,代码也是异常简单,为了方便阅读,我也做了一个简化,简化之后就成了:

很好,这里又多了一个新东西,那就是 BaseWSGIServer,这个到底是啥呢,让我们跟进去看看,位置还是在 werkzeug/serving.py 中,line 446,简化之后可以看成:

一目了然,我们可以发现其实这就是一个 Python 自带的 HTTPServer 的子类,然后再细看一下,其实这就是 Python 原生实现 https 服务器的简单实现了,到此可以终止了。

从这一条线我们可以看到,其实 Flask 或者说 Werkzeug 实现 https 还是在原生 HTTPServer 的基础上简单做了一些封装实现的,并没有做关于 ssl 的特殊处理,所以用的时候我们需要记住这个实现原理就可以定位很多问题了。

被忽略掉的东西

在第二部分的实现中,我们简化了不少代码,现在,我们理清了代码的思路之后,就回过头来看看被我们忽略掉的有价值的东西。

  1. DEBUG 模式是如何实现的

    在使用 Flask 的时候,有个很有趣的功能就是当我们在调用 run 的时候加上个参数 run(debug=True) 就是 DEBUG 模式了,那么它是如何实现的呢?

    首先,我想了想,DEBUG 模式和非 DEBUG 模式的区别是啥,好像很少,但是细细数一下好像又很多,例如:

    • DEBUG 模式的时候异常是跳到异常的 html 页面,而非 DEBUG 模式直接 500 了
    • DEBUG 模式的时候 log 信息比非 DEBUG 模式多了很多,应该是打印级别降低了
    • DEBUG 模式的时候还支持在前端页面上调试代码

      还有很多的差别,那么这些差别都是如何实现的捏?看一下代码,可以在 werkzeug/serving.py line 632 中看到 WSGI app 对象被包装了一层,包装类是:DebuggedApplication,源码位置在 werkzeug/debug/__init__.py line 228 处,其中微妙之处在于 line 437,这里其实就是处理各种 DEBUG 特性的地方:

  2. 静态文件处理

    在我们的 DEMO 代码里面,有一段关于静态文件的代码,可以简单明了得看到静态文件是通过中间件的方式实现的,而内部的具体实现则是通过匹配 url 的前缀是否是静态文件的前缀实现的,简化只则为:

  3. 重用描述符?

    werkzeug/serving.pyinner 函数中,有一段被我忽略了,原来是这样的:

    之后跟进 make_server 之后可以发现 fd 是这样被使用的:

    卧槽,居然还可以这样,跟了一下 HTTPServer 的源代码,可以发现 HTTPServer -> TCPServer -> BaseServer,然后 srv.serve_forever() 中的 serve_forever 是继承自 BaseServer 的,在 BaseServer 中是这么用的

    查看一下 select.select 的文档可以发现:

    select.select(rlist, wlist, xlist[, timeout]) This is a straightforward interface to the Unix select() system call. The first three arguments are sequences of ‘waitable objects’: either integers representing file descriptors or objects with a parameterless method named fileno() returning such an integer:

    所以继续往下看可以发现 TCPServer 的代码中有这么一段:

    所以,可以看到,刚才那段替换居然是可以的!!!

  4. 更多的点

    这篇文章已经写很多了,留个坑吧,下次接着写,可以发现这里还是有很多知识是平时不常用的,但是很有意思。

Tips

本文中涉及的 DEMO 代码可以在 Github 中查看:Werkzeug https Demo

Reference

  1. HTTPS Wiki
  2. Running Your Flask Application Over HTTPS
  3. How to serve HTTPS directly from Flask
  4. werkzeug 初探
  5. Creating an HTTPS server in Python
  6. Python select funciton docs