Post

《Python基础教程》笔记 第25章 项目6:使用CGI进行远程编辑

本章的项目使用第15章详细讨论过的CGI实现远程编辑——在另一台机器上通过Web来编辑文档。这在协作系统中很有用,例如多人协作编辑一个文档。也可以用来更新网页。

25.1 问题描述

你在一台机器上存储了一个文档,希望能够在另一台机器上通过Web来编辑它。这能够让多个用户协作编辑一个共享文档,而无需使用FTP或类似的文件传输技术,也无需担心同步多个副本的问题。要编辑文件,只要有Web浏览器就行。

注意:这种远程编辑是维基(wiki)系统(参见 https://en.wikipedia.org/wiki/Wiki )的核心机制之一。

具体地说,这个系统应该满足以下需求:

  • 应该能够将文档显示为普通网页。
  • 应该能够在Web表单的文本域内显示文档。
  • 用户能够保存表单中的文本。
  • 程序应该使用密码保护文档。
  • 程序应该易于扩展以支持对多个文档进行编辑。

25.2 有用的工具

编写CGI程序时的主要工具是cgi模块,以及用于调试的cgitb模块。详见第15章。

25.3 准备工作

15.2节详细介绍了让CGI脚本能够通过Web访问所需的步骤,只需按这些步骤做即可。

25.4 初次实现

初次实现基于代码清单15-7所示问候脚本的基本结构。原型只需做些文件处理工作即可。

要让脚本发挥作用,必须保存编辑后的文本。另外,表单应该比问候脚本(代码清单15-7)中的更大些,将文本字框(<input>)改为文本域(<textarea>,可多行输入的文本框)。同时,应该使用POST方法而不是默认的GET方法(提交大量数据时通常使用POST方法)。

程序的逻辑大体如下:

  1. 获取CGI参数text(默认为数据文件的当前内容)。
  2. 将文本保存到数据文件。
  3. 打印表单,将文本显示在文本域中。

要让脚本能够写入数据文件,必须设置权限,让任何人都能写入这个文件。最终的代码如代码清单25-1所示。

代码清单25-1 简单的Web编辑器

通过Web服务器访问时,这个CGI脚本检查输入值text。如果提交了这个值,就将其写入文件simple_edit.dat,默认为文件的当前内容。最后,显示一个网页,其中包含用于编辑和提交文本的字段,如下图所示。

简单的Web编辑器

注:

  • 在Windows中,要将脚本后缀改为.py,同时修改表单的action属性值。
  • 可以使用http.server模块作为Web服务器。假设文件simple_edit.dat与cgi-bin目录在同一目录下:
1
2
3
4
somedir/
    simple_edit.dat
    cgi-bin/
        simple_edit.cgi

则在somedir目录下运行

1
$ python -m http.server --cgi

之后在浏览器中访问 http://localhost:8000/cgi-bin/simple_edit.cgi (在Windows上将后缀改为.py)。

25.5 再次实现

现在已经有了第一个原型,还缺什么呢?应该能够编辑多个文件(而不是将文件名硬编码),还应该使用密码保护。

相比于第一个原型,再次实现的主要区别在于将其拆分为两个CGI脚本,分别对应系统支持的两种操作。再次实现的文件如下:

  • index.html:一个普通网页,包含一个输入文件名的表单,还有一个触发edit.cgi的Open按钮。
  • edit.cgi:在文本域中显示指定文件的脚本,还包含一个用于输入密码的文本框和一个触发save.cgi的Save按钮。
  • save.cgi:将收到的文本保存到指定的文件并显示成功消息的脚本。这个脚本还应负责检查密码。

25.5.1 创建文件名表单

index.html是一个HTML文件,包含用于输入文件名的表单。

首页

注意,<form>标签的action属性是CGI脚本名,文本框名称filename是提供给CGI脚本的参数名。

25.5.2 编写编辑器脚本

脚本edit.cgi显示的页面应该包含一个文本域,其中包含正在编辑的文件的当前内容,以及一个用于输入密码的文本框。脚本需要的唯一输入是文件名,它是从index.html中的表单获取的。然而要注意,有可能在不提交index.html中表单的情况下直接运行edit.cgi脚本(例如在浏览器地址栏中直接输入URL)。在这种情况下,就不能保证cgi.FieldStoragefilename字段被设置了。因此,需要增加检查以确保有文件名参数。如果有,则打开指定目录中的这个文件。将这个目录命名为data。

警告:用户通过提供包含路径元素(例如..)的文件名,可以访问指定目录外的文件,导致安全风险。为了确保访问的文件在指定的目录内,应该执行额外的检查,例如(使用glob模块)列出指定目录中的所有文件,并检查提供的文件名是否在其中(务必使用完整的绝对路径)。27.5.3节介绍了另一种方法。

这个脚本的代码如代码清单25-2所示。

代码清单25-2 编辑器脚本

注意,这里使用abspath()函数来获取data目录的绝对路径(当前目录/data)。另外,将文件名存储在一个隐藏的表单元素中,以便将其传递给下一个脚本(save.cgi),不给用户修改它的机会。(当然,并不能完全保证,因为用户可以编写自己的表单,使用自定义值调用你的CGI脚本,或者使用curl或Postman直接发送HTTP请求。)

密码输入框的类型为password而不是text,这意味着输入的字符将显示为星号。

注意:这个脚本假定指定的文件存在。可以随意扩展,使其能够处理其他情形。

25.5.3 编写保存脚本

保存脚本接收文件名、密码和一些文本,并检查密码是否正确。如果正确,就将文本保存到指定的文件中。

出于好玩,这里使用hashlib模块中的sha1()来处理密码。安全散列算法(Secure Hash Algorithm, SHA)是一种从输入字符串提取看似随机的数据(摘要(digest),无意义的字符串)的方法。这个算法背后的思想是:几乎不可能构造出具有指定摘要的字符串。因此即便你知道密码的摘要,也没有(简单的)方法重建密码,或创建一个具有相同摘要的密码。这意味着你可以安全地将所提供密码的摘要与正确密码的摘要进行比较,而不用比较密码本身。通过这种方法,你无需将密码本身存储在源代码中,这样阅读代码的人根本不知道密码是什么。

警告:前面说过,这种“安全”特性只是出于好玩。除非使用SSL安全连接或其他类似的技术(超出了这个项目的范围),否则通过网络提交的密码依然可能被窃取。另外,这里使用的SHA1算法现在已经不是非常安全了。

下面的示例演示了sha1()的用法:

1
2
3
4
5
>>> from hashlib import sha1
>>> sha1(b'foobar').hexdigest()
'8843d7f92416211de9ebb963ff4ce28125932878'
>>> sha1(b'foobaz').hexdigest()
'21eb6533733a5e4763acacd1d45a60c2e0e404e1'

如你所见,密码发生细微变化时,得到的摘要完全不同。

脚本save.cgi的代码如代码清单25-3所示。

代码清单25-3 保存脚本

25.5.4 运行编辑器

项目的目录结构如下:

1
2
3
4
5
6
7
project/
    index.html
    cgi-bin/
        edit.cgi
        save.cgi
    data/
        foofile.txt

首先在项目根目录下启动Web服务器,之后按下面的步骤来使用这个编辑器:

1.在Web浏览器中打开页面index.html:http://localhost:8000/index.html。结果如下图所示。

CGI编辑器的起始页面

2.输入文件名,点击Open。浏览器将打开编辑页面,如下图所示。

CGI编辑器的编辑页面

3.随意编辑这个文件,输入密码(你设置的密码,或示例中的密码foobar),点击Save。浏览器将显示消息 “The file has been saved” 。

4.如果要验证文件已被修改,重复打开文件的过程(第1~2步)。

25.6 进一步探索

使用这个项目展示的技术,可以开发各种Web系统。对于这个系统,一些可能的扩展如下:

  • 添加版本控制,保存文件的旧副本,从而可以撤销修改。
  • 添加用户名支持,从而可以知道谁修改了什么。
  • 添加文件锁定功能(例如使用fcntl模块),禁止两个用户同时编辑同一个文件。
  • 添加view.cgi脚本,自动给文件添加标记(就像第20章那样)。
  • 更详尽地检查输入并添加对用户更友好的错误消息,让脚本更健壮。
  • 不打印类似于 “The file has been saved” 这样的确认消息,而是添加一些更有用的输出,或将用户重定向到另一个页面/脚本。重定向可以使用标头Location来实现。只需在标头部分(第一个空行前)添加Location: url指定要重定向到的URL。
This post is licensed under CC BY 4.0 by the author.