前言

一直想搭建一个self-hosted的评论系统,心里种草很久。多说的倒闭、Disqus在国内无法访问,加上技术储备并不充分,虽然尝试写了一个非常简单评论功能,见文章,和预期相差甚远。

一方面在互联网搜索各式的开源评论系统设计,

一方面也终于抽出时间尝试。这篇文章记录了基于Python的isso的搭建过程和一些“坑”及解决方案。

准备工作

Isso强烈建议评论系统不应直接暴露在公网环境,同时以作为sub URI作为评论访问地址,能够避免一些强隐私保护浏览器屏蔽评论。但会带来跨域资源共享问题(CORS)。

二级域名解析

以本博客kukumalu.cc为例, 申请了comment.kukumalu.cc作为二级域名,在阿里云设置了A纪录解析到服务器。

  • 解析到自己网站,说明
  • 解析到其他网站,??

配置准备

isso.cfg

[general]
# 数据库所在路径
dbpath = /var/lib/isso/comments.db

# 你的主机域名,注意不是评论系统所在域名, Isso会自动处理CORS
host = http://www.kukumalu.cc

# 允许用户移除、编辑评论的最大时间
max-age = 15m

# 获得评论相关通知的方式
# stdout
#     Log to standard output. Default, if none selected.
# smtp
#     Send notifications via SMTP on new comments with activation (if
#     moderated) and deletion links.
notify = stdout

# 日志文件存放路径
log-file = /var/log/isso/isso.log

#################################
#后面配置保持默认,可运行,按需修改#
#################################

[moderation]
# 评论审核相关配置
# Comments in modertion queue are not visible to other users until you activate
# them.
enabled = false

# remove unprocessed comments in moderation queue after given time.
purge-after = 30d


[server]
# 服务端相关配置
# interface to listen on. Isso supports TCP/IP and unix domain sockets: UNIX
# domain socket listen = unix:///tmp/isso.sock TCP/IP listen =
# http:///localhost:1234/
#
# When gevent is available, it is automatically used for http:// Currently,
# gevent can not handle http requests on unix domain socket (see #295 and #299
# for details).  Does not apply for uWSGI.
listen = http://localhost:8080

# reload application, when the source code has changed. Useful for development.
# Only works with the internal webserver.
reload = off

# show 10 most time consuming function in Isso after each request. Do not use
# in production.
profile = off


[smtp]
# 邮件通知相关配置
# Isso can notify you on new comments via SMTP. In the email notification, you
# also can moderate (=activate or delete) comments.

# self-explanatory, optional
username =

# self-explanatory (yes, plain text, create a dedicated account for
# notifications), optional.
password =

# SMTP server
host = localhost

# SMTP port
port = 587

# use a secure connection to the server, possible values: none, starttls or
# ssl. Note, that there is no easy way for Python 2.7 and 3.3 to implement
# certification validation and thus the connection is vulnerable to
# Man-in-the-Middle attacks. You should definitely use a dedicated SMTP account
# for Isso in that case.
security = starttls

# recipient address, e.g. your email address
to =

# sender address, e.g. "Foo Bar" <isso@example.tld>
from =

# specify a timeout in seconds for blocking operations like the
# connection attempt.
timeout = 10


[guard]
# 反垃圾机制
# Enable basic spam protection features, e.g. rate-limit per IP address (/24
# for IPv4, /48 for IPv6).

# enable guard, recommended in production. Not useful for debugging purposes.
enabled = true

# limit to N new comments per minute.
ratelimit = 2

# how many comments directly to the thread (prevent a simple while true; do
# curl ...; done.
direct-reply = 3

# allow commenters to reply to their own comments when they could still edit
# the comment. After the editing timeframe is gone, commenters can reply to
# their own comments anyways. Do not forget to configure the client.
reply-to-self = true

# force commenters to enter a value into the author field. No validation is
# performed on the provided value.  Do not forget to configure the client
# accordingly.
require-author = true

# require the commenter to enter an email address (note: no validation is
# done on the provided address). Do not forget to configure the client.
require-email = true


[markup]
# 评论内容机制
# Customize markup and sanitized HTML. Currently, only Markdown (via Misaka) is
# supported, but new languages are relatively easy to add.

# Misaka-specific Markdown extensions, all flags starting with EXT_ can be used
# there, separated by comma.
options = strikethrough, autolink, fenced_code, no_intra_emphasis

# Additional HTML tags to allow in the generated output, comma-separated. By
# default, only a, blockquote, br, code, del, em, h1, h2, h3, h4, h5, h6, hr,
# ins, li, ol, p, pre, strong, table, tbody, td, th, thead and ul are allowed.
allowed-elements =

# Additional HTML attributes (independent from elements) to allow in the
# generated output, comma-separated. By default, only align and href are
# allowed.
allowed-attributes =


[hash]
# 安全相关
# Customize used hash functions to hide the actual email addresses from
# commenters but still be able to generate an identicon.


# A salt is used to protect against rainbow tables. Isso does not make use of
# pepper (yet). The default value has been in use since the release of Isso and
# generates the same identicons for same addresses across installations.
salt = Eech7co8Ohloopo9Ol6baimi

# Hash algorithm to use -- either from Python's hashlib or PBKDF2 (a
# computational expensive hash function).
#
# The actual identifier for PBKDF2 is pbkdf2:1000:6:sha1, which means 1000
# iterations, 6 bytes to generate and SHA1 as pseudo-random family used for key
# strengthening. Arguments have to be in that order, but can be reduced to
# pbkdf2:4096 for example to override the iterations only.

修改模板

配置完服务端,客户段(JS)需要相应修改,由于我使用的是Pelican静态博客生成工具,在主题文件夹新增了一个isso.html模板。

isso.html

{% if ISSO_RUNNING %}

<section id="isso-thread"></section>
<script data-isso="//comment.kukumalu.cc/"  
    data-isso-css="true"
    data-isso-lang="en"
    data-isso-reply-to-self="true"
    data-isso-require-author="true"
    data-isso-require-email="true"
    data-isso-max-comments-top="10"
    data-isso-max-comments-nested="5"
    data-isso-reveal-on-click="5"
    data-isso-avatar="true"
    data-isso-avatar-bg="#f0f0f0"
    data-isso-avatar-fg="#9abf88 #5698c4 #e279a3 #9163b6 ..."
    data-isso-vote="true"
    data-vote-levels="[-5,5,15]"
    src="//comment.kukumalu.cc/js/embed.min.js"></script>

{% endif %}

Isso 通常会自动检测 REST API, 但如果 JS 文件(Isso的JS实例)并不在默认位置则需要修改 data-isso属性来覆写,值得注意的是src中URI写法并未包含http头。

JS端配置成功后,可以在网页中看到整个评论的form,在http://comment.kukumalu.cc/info也能查到关于Isso的信息。

Nginx 配置

先提供一些CORS Nginx配置的参考内容

CORS在Isso包文档中谈及的不多,但我仅使用文档中的模板配置没用办法成功运行(500错误,CORS头部丢失)。nohup.out的错误信息如下:

127.0.0.1 - - [2017-10-22 15:04:10] "OPTIONS /count HTTP/1.0" 500 161 0.000654
Traceback (most recent call last):
  File "/home/app/negativedearc.github.io/venv/local/lib/python2.7/site-packages/gevent/pywsgi.py", line 935, in handle_one_response
    self.run_application()
  File "/home/app/negativedearc.github.io/venv/local/lib/python2.7/site-packages/gevent/pywsgi.py", line 908, in run_application
    self.result = self.application(self.environ, self.start_response)
  File "/home/app/negativedearc.github.io/venv/local/lib/python2.7/site-packages/werkzeug/contrib/fixers.py", line 152, in __call__
    return self.app(environ, start_response)
  File "/home/app/negativedearc.github.io/venv/local/lib/python2.7/site-packages/isso/wsgi.py", line 119, in __call__
    return self.app(environ, start_response)
  File "/home/app/negativedearc.github.io/venv/local/lib/python2.7/site-packages/isso/wsgi.py", line 147, in __call__
    add_cors_headers("200 Ok", [("Content-Type", "text/plain")])
  File "/home/app/negativedearc.github.io/venv/local/lib/python2.7/site-packages/isso/wsgi.py", line 144, in add_cors_headers
    return start_response(status, headers.to_list(), exc_info)
  File "/home/app/negativedearc.github.io/venv/local/lib/python2.7/site-packages/gevent/pywsgi.py", line 830, in start_response
    raise UnicodeError("The status string must be a native string")
UnicodeError: The status string must be a native string
Sun Oct 22 15:04:10 2017 {'REMOTE_PORT': '52242', 'HTTP_HOST': 'comment.kukumalu.cc', 'REMOTE_ADDR': '114.216.124.225', (hidden keys: 30)} failed with UnicodeError

Isso在其wsgi.py中已经提前处理了CORS需要的头部信息。

class CORSMiddleware(object):
    """Add Cross-origin resource sharing headers to every request."""

    methods = ("HEAD", "GET", "POST", "PUT", "DELETE")

    def __init__(self, app, origin, allowed=None, exposed=None):
        self.app = app
        self.origin = origin
        self.allowed = allowed
        self.exposed = exposed

    def __call__(self, environ, start_response):

        def add_cors_headers(status, headers, exc_info=None):
            headers = Headers(headers)
            headers.add("Access-Control-Allow-Origin", self.origin(environ))
            headers.add("Access-Control-Allow-Credentials", "true")
            headers.add("Access-Control-Allow-Methods", ", ".join(self.methods))
            if self.allowed:
                headers.add("Access-Control-Allow-Headers", ", ".join(self.allowed))
            if self.exposed:
                headers.add("Access-Control-Expose-Headers", ", ".join(self.exposed))
            return start_response(status, headers.to_list(), exc_info)

        if environ.get("REQUEST_METHOD") == "OPTIONS":
            add_cors_headers("200 Ok", [("Content-Type", "text/plain")])
            return []

        return self.app(environ, add_cors_headers)

但似乎OPTIONS没有处理成功?推测原因:

  • Nginx 没有(无法)正确处理OPTIONS请求,参考资料
  • wsgi MiddleWare 没有正确假如头部信息(需要确认,参考资料)。

无论如何,在改动Nginx配置(手动加入OPTIONS判断)后运行成功了。

isso.conf

server {
    listen         80;
    server_name    comment.kukumalu.cc;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-Proto $scheme;


        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' 'http://www.kukumalu.cc';
            add_header 'Access-Control-Allow-Methods' 'HEAD, GET, POST, PUT, DELETE';
            add_header 'Access-Control-Allow-Credentials' 'true';
            add_header 'Access-Control-Allow-Headers' 'Origin, Referer, Content-Type';

            # Tell client that this pre-flight info is valid for 20 days

            add_header 'Access-Control-Max-Age' 1728000;
            add_header 'Content-Type' 'text/plain; charset=utf-8';
            add_header 'Content-Length' 0;
            return 204;
        }
    }

放入vhost中与其它Nginx服务一起运行。

运行

生产环境下,运行isso最简单的方式是使用gevent,两个步骤就可以搞定。

pip install gevent -i https://pypi.douban.com/simple
isso -c isso.cfg run

至此,isso可以完美的运行起来。

其他