python

celery最佳实践

作为一个Celery使用重度用户,看到Celery Best Practices这篇文章,不由得菊花一紧。干脆翻译出来,同时也会加入我们项目中celery的实战经验。

通常在使用Django的时候,你可能需要执行一些长时间的后台任务,没准你可能需要使用一些能排序的任务队列,那么Celery将会是一个非常好的选择。

当把Celery作为一个任务队列用于很多项目中后,作者积累了一些最佳实践方式,譬如如何用合适的方式使用Celery,以及一些Celery提供的但是还未充分使用的特性。

1,不要使用数据库作为你的AMQP Broker

数据库并不是天生设计成能用于AMQP broker的,在生产环境下,它很有可能在某时候当机(PS,当掉这点我觉得任何系统都不能保证不当吧!!!)。

作者猜想为啥很多人使用数据库作为broker主要是因为他们已经有一个数据库用来给web app提供数据存储了,于是干脆直接拿来使用,设置成Celery的broker是很容易的,并且不需要再安装其他组件(譬如RabbitMQ)。

假设有如下场景:你有4个后端workers去获取并处理放入到数据库里面的任务,这意味着你有4个进程为了获取最新任务,需要频繁地去轮询数据库,没准每个worker同时还有多个自己的并发线程在干这事情。

某一天,你发现因为太多的任务产生,4个worker不够用了,处理任务的速度已经大大落后于生产任务的速度,于是你不停去增加worker的数量。突然,你的数据库因为大量进程轮询任务而变得响应缓慢,磁盘IO一直处于高峰值状态,你的web应用也开始受到影响。这一切,都因为workers在不停地对数据库进行DDOS。

而当你使用一个合适的AMQP(譬如RabbitMQ)的时候,这一切都不会发生,以RabbitMQ为例,首先,它将任务队列放到内存里面,你不需要去访问硬盘。其次,consumers(也就是上面的worker)并不需要频繁地去轮询因为RabbitMQ能将新的任务推送给consumers。当然,如果RabbitMQ真出现问题了,至少也不会影响到你的web应用。

这也就是作者说的不用数据库作为broker的原因,而且很多地方都提供了编译好的RabbitMQ镜像,你都能直接使用,譬如这些

对于这点,我是深表赞同的。我们系统大量使用Celery处理异步任务,大概平均一天几百万的异步任务,以前我们使用的mysql,然后总会出现任务处理延时太严重的问题,即使增加了worker也不管用。于是我们使用了redis,性能提升了很多。至于为啥使用mysql很慢,我们没去深究,没准也还真出现了DDOS的问题。

2,使用更多的queue(不要只用默认的)

Celery非常容易设置,通常它会使用默认的queue用来存放任务(除非你显示指定其他queue)。通常写法如下:

@app.task()
def my_taskA(a, b, c):
    print("doing something here...")

@app.task()
def my_taskB(x, y):
    print("doing something here...")

这两个任务都会在同一个queue里面执行,这样写其实很有吸引力的,因为你只需要使用一个decorator就能实现一个异步任务。作者关心的是taskA和taskB没准是完全两个不同的东西,或者一个可能比另一个更加重要,那么为什么要把它们放到一个篮子里面呢?(鸡蛋都不能放到一个篮子里面,是吧!)没准taskB其实不怎么重要,但是量太多,以至于重要的taskA反而不能快速地被worker进行处理。增加workers也解决不了这个问题,因为taskA和taskB仍然在一个queue里面执行。

3,使用具有优先级的workers

为了解决2里面出现的问题,我们需要让taskA在一个队列Q1,而taskB在另一个队列Q2执行。同时指定x workers去处理队列Q1的任务,然后使用其它的workers去处理队列Q2的任务。使用这种方式,taskB能够获得足够的workers去处理,同时一些优先级workers也能很好地处理taskA而不需要进行长时间的等待。

首先手动定义queue

CELERY_QUEUES = (
    Queue('default', Exchange('default'), routing_key='default'),
    Queue('for_task_A', Exchange('for_task_A'), routing_key='for_task_A'),
    Queue('for_task_B', Exchange('for_task_B'), routing_key='for_task_B'),
)

然后定义routes用来决定不同的任务去哪一个queue

CELERY_ROUTES = {
    'my_taskA': {'queue': 'for_task_A', 'routing_key': 'for_task_A'},
    'my_taskB': {'queue': 'for_task_B', 'routing_key': 'for_task_B'},
}

最后再为每个task启动不同的workers

celery worker -E -l INFO -n workerA -Q for_task_A
celery worker -E -l INFO -n workerB -Q for_task_B

在我们项目中,会涉及到大量文件转换问题,有大量小于1mb的文件转换,同时也有少量将近20mb的文件转换,小文件转换的优先级是最高的,同时不用占用很多时间,但大文件的转换很耗时。如果将转换任务放到一个队列里面,那么很有可能因为出现转换大文件,导致耗时太严重造成小文件转换延时的问题。

所以我们按照文件大小设置了3个优先队列,并且每个队列设置了不同的workers,很好地解决了我们文件转换的问题。

4,使用Celery的错误处理机制

大多数任务并没有使用错误处理,如果任务失败,那就失败了。在一些情况下这很不错,但是作者见到的多数失败任务都是去调用第三方API然后出现了网络错误,或者资源不可用这些错误,而对于这些错误,最简单的方式就是重试一下,也许就是第三方API临时服务或者网络出现问题,没准马上就好了,那么为什么不试着重试一下呢?

@app.task(bind=True, default_retry_delay=300, max_retries=5)
def my_task_A():
    try:
        print("doing stuff here...")
    except SomeNetworkException as e:
        print("maybe do some clenup here....")
        self.retry(e)

作者喜欢给每一个任务定义一个等待多久重试的时间,以及最大的重试次数。当然还有更详细的参数设置,自己看文档去。

对于错误处理,我们因为使用场景特殊,例如一个文件转换失败,那么无论多少次重试都会失败,所以没有加入重试机制。

5,使用Flower

Flower是一个非常强大的工具,用来监控celery的tasks和works。

这玩意我们也没怎么使用,因为多数时候我们都是直接连接redis去查看celery相关情况了。貌似挺傻逼的对不,尤其是celery在redis里面存放的数据并不能方便的取出来。

6,没事别太关注任务退出状态

一个任务状态就是该任务结束的时候成功还是失败信息,没准在一些统计场合,这很有用。但我们需要知道,任务退出的状态并不是该任务执行的结果,该任务执行的一些结果因为会对程序有影响,通常会被写入数据库(例如更新一个用户的朋友列表)。

作者见过的多数项目都将任务结束的状态存放到sqlite或者自己的数据库,但是存这些真有必要吗,没准可能影响到你的web服务的,所以作者通常设置CELERY_IGNORE_RESULT = True去丢弃。

对于我们来说,因为是异步任务,知道任务执行完成之后的状态真没啥用,所以果断丢弃。

7,不要给任务传递 Database/ORM 对象

这个其实就是不要传递Database对象(例如一个用户的实例)给任务,因为没准序列化之后的数据已经是过期的数据了。所以最好还是直接传递一个user id,然后在任务执行的时候实时的从数据库获取。

对于这个,我们也是如此,给任务只传递相关id数据,譬如文件转换的时候,我们只会传递文件的id,而其它文件信息的获取我们都是直接通过该id从数据库里面取得。

最后

后面就是我们自己的感触了,上面作者提到的Celery的使用,真的可以算是很好地实践方式,至少现在我们的Celery没出过太大的问题,当然小坑还是有的。至于RabbitMQ,这玩意我们是真没用过,效果怎么样不知道,至少比mysql好用吧。

最后,附上作者的一个Celery Talk https://denibertovic.com/talks/celery-best-practices/

学习Tornado:基本

前言

在python里面,有许多web framework。对于我来说,因为很长一段时间都在使用tornado,所以有了一些心得体会。

在这里,要说明一下,tornado采用的是2.4版本。

架构

tornado是一个典型的prefork + io event loop的web server架构,Alt text

从图上可以看出,tornado的架构是很简单清晰的。

  • ioloop是tornado的核心,它就是一个io event loop,底层封装了select,epoll和kqueue,并根据不同的平台选择不同的实现。
  • iostream封装了non-blocking socket,用它来进行实际socket的数据读写。
  • TCPServer则是通过封装ioloop实现了一个简易的server,同时我们也在这里进行prefork的处理
  • HTTPServer则是继承TCPServer实现了一个能够处理http协议的server。
  • Application则是实际处理http请求的模块,HTTPServer收到http请求并解析之后会通过Application进行处理。
  • RequestHandler和WebSocketHandler则是注册给Application用来处理对应url的。
  • WSGIApplication则是tornado用于支持WSGI标准的接口,通过WSGIContainer包装共HTTPServer使用。

例子

通过上面的分析,直到tornado的架构是很简单明了的,所以自然我们也能够通过简短的一些代码就能搭建起自己的http server。以一个hello world开始:

import tornado.web 
import tornado.httpserver 
import tornado.ioloop 

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write('Hello World')

application = tornado.web.Application([
    (r"/", MainHandler),
])
http_server = tornado.httpserver.HTTPServer(application)
http_server.listen(8080)
tornado.ioloop.IOLoop.instance().start()

流程很简单,如下:

  • 定义了一个MainHandler,该handler用来处理对应url
  • 生成一个Application实例,并设置url dispatch规则,(r”/“, MainHandler)就是一个规则,第一个pattern用来表明需要处理的url,内部会使用正则匹配,第二个就是对应url处理的handler
  • 生成一个HTTPServer实例,使用Application进行构造,这样HTTPServer处理的http请求就会转给application处理。
  • HTTPServer监听一个端口8080,该listen socket会加入ioloop中,用于监听连接的建立。
  • ioloop启动,程序进入io event loop模式。

当ioloop start之后,服务器就启动了,后续就是一个http server最基本的流程处理了。

ReuqestHandler

pattern and handler

从上面例子可以看出,搭建一个http server很简单,所以我们重点只需要考虑的是如何处理不同的url http请求,这也就是RequestHandler需要做的事情。

我们在创建Application的时候,会指定不同的url pattern需要处理的handler。如下:

import tornado.web 
import tornado.httpserver 
import tornado.ioloop 

class Index1Handler(tornado.web.RequestHandler):
    def get(self):
        self.write('Index1')

class Index2Handler(tornado.web.RequestHandler):
    def get(self, data):
        self.write('Index2')
        self.write(data)

application = tornado.web.Application([
    (r"/index1", Index1Handler),
    (r"/index2/(\w+)", Index2Handler),
])

http_server = tornado.httpserver.HTTPServer(application)
http_server.listen(8080)
tornado.ioloop.IOLoop.instance().start()

在上面的例子中,我们有两个handler,分别处理url path为index1和index2的情况,对于index2来说,我们看到,它后面还需要匹配一个单词。我们通过curl访问如下:

$ curl http://127.0.0.1:8080/index1
index1

$ curl http://127.0.0.1:8080/index2/abc
index2abc

http method

RequestHandler支持任何http mthod,包括get,post,head和delete,也就是说,tornado天生支持restful编程模型。

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        pass

    def post(self):
        pass

    def head(self):
        pass

    def delete(self):
        pass

从上面可以看到,我们只需要在handler里面实现自己的get,post,head和delete函数就可以了,这点再次说明tornado的简洁与强大。

后续next

这里,只是简单了介绍了一下tornado,后续将会从template,asynchronous,security等分别介绍一下。希望通过这个能让自己对tornado的理解更加深刻,同时也为后续使用其他python web framework做参考。

学习Tornado:异步

why asynchronous

tornado是一个异步web framework,说是异步,是因为tornado server与client的网络交互是异步的,底层基于io event loop。但是如果client请求server处理的handler里面有一个阻塞的耗时操作,那么整体的server性能就会下降。

def MainHandler(tornado.web.RequestHandler):
    def get(self):
        client = tornado.httpclient.HttpClient()
        response = client.fetch("http://www.google.com/")
        self.write('Hello World')

在上面的例子中,tornado server的整体性能依赖于访问google的时间,如果访问google的时间比较长,就会导致整体server的阻塞。所以,为了提升整体的server性能,我们需要一套机制,使得handler处理都能够通过异步的方式实现。

幸运的是,tornado提供了一套异步机制,方便我们实现自己的异步操作。当handler处理需要进行其余的网络操作的时候,tornado提供了一个async http client用来支持异步。

def MainHandler(tornado.web.RequestHandler):
    @tornado.web.asynchronous
    def get(self):
        client = tornado.httpclient.AsyncHTTPClient()
        def callback(response):
            self.write("Hello World")
            self.finish()

        client.fetch("http://www.google.com/", callback)

上面的例子,主要有几个变化:

  • 使用asynchronous decorator,它主要设置_auto_finish为false,这样handler的get函数返回的时候tornado就不会关闭与client的连接。
  • 使用AsyncHttpClient,fetch的时候提供callback函数,这样当fetch http请求完成的时候才会去调用callback,而不会阻塞。
  • callback调用完成之后通过finish结束与client的连接。

asynchronous flaw

异步操作是一个很强大的操作,但是它也有一些缺陷。最主要的问题就是在于callback导致了代码逻辑的拆分。对于程序员来说,同步顺序的想法是一个很自然的习惯,但是异步打破了这种顺序性,导致代码编写的困难。这点,对于写nodejs的童鞋来说,可能深有体会,如果所有的操作都是异步,那么最终我们的代码可能写成这样:

def MainHandler(tornado.web.RequestHandler):
    @tornado.web.asynchronous
    def get(self):
        client = tornado.httpclient.AsyncHTTPClient()
        def callback1(response):

            def callback2(response):
                self.write("Hello World")
                self.finish()
            client.fetch("http://www.google.com", callback2)

        client.fetch("http://www.google.com/", callback1)

也就是说,我们可能会写出callback嵌套callback的情况,这个极大的会影响代码的阅读与流程的实现。

synchronous

我个人认为,异步拆散了代码流程这个问题不大,毕竟如果一个逻辑需要过多的嵌套callback来实现的话,那么我们就需要考虑这个逻辑是否合理了,所以异步一般也不会有过多的嵌套层次。

虽然我认为异步的callback问题不大,但是如果仍然能够有一套机制,使得异步能够顺序化,那么对于代码逻辑的编写来说,会方便很多。tornado有一些机制来实现。

yield

在python里面如果一个函数内部实现了yield,那么这个函数就不是函数了,而是一个生成器,它的整个运行机制也跟普通函数不一样,举一个例子:

def test_yield():
    print 'yield 1'
    a = yield 'yielded'
    print 'over', a

t = test_yield()
print 'main', type(t)
ret = t.send(None)
print ret
try:
    t.send('hello yield')
except StopIteration:
    print 'yield over'

输出结果如下:

main <type 'generator'>
yield 1
yielded
over hello yield
yield over

从上面可以看到,test_yield是一个生成器,当它第一次调用的时候,只是生成了一个Generator,不会执行。当第一次调用send的时候,生成器被resume,开始执行,然后碰到yield,就挂起,等待下一次被send唤醒。当生成器执行完毕,会抛出StopIteration异常,供外部send的地方知晓。

因为yield很方便的提供了一套函数挂起,运行的机制,所以我们能够通过yield来将原本是异步的流程变成同步的。

gen

tornado有一个gen模块,提供了Task和Callback/Wait机制用来支持同步模型,以task为例:

def MainHandler(tornado.web.RequestHandler):
    @tornado.web.asynchronous
    @tornado.gen.engine
    def get(self):
        client = tornado.httpclient.AsyncHTTPClient()
        response = yield tornado.gen.Task(client.fetch, "http://www.google.com/")
        self.write("Hello World")
        self.finish()

可以看到,tornado的gen模块就是通过yield来进行同步化的。主要有如下需要注意的地方:

  • 使用gen.engine的decorator,该函数主要就是用来管理generator的流程控制。
  • 使用了gen.Task,在gen.Task内部,会生成一个callback函数,传给async fetch,并执行fetch,因为fetch是一个异步操作,所以会很快返回。
  • 在gen.Task返回之后使用yield,挂起
  • 当fetch的callback执行之后,唤醒挂起的流程继续执行。

可以看到,使用gen和yield之后,原先的异步逻辑变成了同步流程,在代码的阅读性上面就有不错的提升,不过对于不熟悉yield的童鞋来说,开始反而会很迷惑,不过只要理解了yield,那就很容易了。

greenlet

虽然yield很强大,但是它只能挂起当前函数,而无法挂起整个堆栈,这个怎么说呢,譬如我想实现下面的功能:

def a():
    yield 1

def b():
    a()

t = b()
t.send(None)

这个通过yield是无法实现的,也就是说,a里面使用yield,它是一个生成器,但是a的挂起无法将b也同时挂起。也就是说,我们需要一套机制,使得堆栈在任何地方都能够被挂起和恢复,能方便的进行栈切换,而这套机制就是coroutine。

最开始使用coroutine是在lua里面,它原生提供了coroutine的支持。然后在使用luajit的时候,发现内部是基于fiber(win)和context(unix),也就是说,不光lua,其实c/c++我们也能实现coroutine。现在研究了go,也是内置coroutine,并且这里极力推荐一篇slide

python没有原生提供coroutine,不知道以后会不会有。但有一个greenlet,能帮我们实现coroutine机制。而且还有人专门写好了tornado与greenlet结合的模块,叫做greenlet_tornado,使用也很简单

class MainHandler(tornado.web.RequestHandler):
    @greenlet_asynchronous
    def get(self):
        response = greenlet_fetch('http://www.google.com')
        self.write("Hello World")
        self.finish()

可以看到,使用greenlet,能更方便的实现代码逻辑,这点比使用gen更方便,因为这些连写代码的童鞋都不用去纠结yield问题了。

总结

这里只是简单的介绍了tornado的一些异步处理流程,以及将异步同步化的一些方法。另外,这里举得例子都是网络http请求方面的,但是server处理请求的时候,可能还需要进行数据库,本地文件的操作,而这些也是同步阻塞耗时操作,同样可以通过异步来解决的,这里就不详细说明了。

学习Tornado:安全

在web编程中,安全性是我们都必须面临的一个问题,包括cookie伪造,xsrf攻击等。tornado作为一个web framework,在安全性方面也提供了很多功能,这里简单介绍一下。

cookie

在web编程中,浏览器经常使用cookie来保存相关用户信息,用于与server交互,但是cookie有很多安全问题,譬如cookie伪造。cookie有很多方式被修改,javascript,flash,以及browser plugin等,所以首先需要保证cookie不被修改。

tornado提供了secure cookie机制来保证cookie不被修改。tornado使用一个密钥用来给cookie进行签名,用来保证cookie只能被服务器修改。因为密钥只有tornado server知道,所以其它应用程序是没办法修改cookie的值。

tornado使用set_secure_cookie和get_secure_cookie来设置和读取browser的cookie。使用secure cookie,只需要在tornado启动的时候设置cookie_secret就行了,如下:

import tornado.web 
import tornado.httpserver 
import tornado.ioloop 

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        count = self.get_secure_cookie('count')
        count = (int(count) + 1) if count else 1

        self.set_secure_cookie('count', str(count))

        self.write(str(count))

settings = {
    'cookie_secret' : 'S6Bp2cVjSAGFXDZqyOh+hfn/fpBnaEzFh22IVmCsVJQ='
}

application = tornado.web.Application([
    (r"/", MainHandler),
], **settings)

http_server = tornado.httpserver.HTTPServer(application)
http_server.listen(8080)
tornado.ioloop.IOLoop.instance().start()

cookie_secret的生成如下:

import base64
import uuid
base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes)

httponly

为了防止cross-site scripting attack,tornado可以再设置cookie的时候增加httponly字段,这样该cookie就不能够被javascript读取。同时,为了更高的安全性,可以给cookie设置secure属性,这个跟先前讨论的set_secure_cookie不一样,上面那个是对cookie进行加密签名,保证不被修改,而这个则是让browser通过ssl传输cookie。启用这些功能很简单,只需要在设置cookie的时候处理。

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.set_cookie('count1', '1', httponly = True, secure = True)
        self.set_secure_cookie('count2', '2', httponly = True, secure = True)

XSRF

上面介绍的方法虽然能够保证cookie的安全,但是还是防止不了XSRF攻击。为了防止XSRF攻击,首先设计web服务的时候就需要考虑http method的side effects。按照restful的编程模型,get只能用来获取数据,post才会去修改数据,这样就能在很大的程度上面防止XSRF,因为对于通常情况来说,XSRF攻击就是通过设置img的src为一个恶意的get请求,只要我们的get不会进行数据修改,自然就能防止。

但是一些恶意的操作仍然能够发送post请求来进行攻击,譬如通过HTML forms或者XMLHTTPRequest API,所以为了防止post这种的攻击,我们需要一些额外的机制。

tornado提供了一个XSRF保护机制,原理很简单,就是在post提交数据的时候额外加入一个_xsrf字段,这个字段的值是从secure cookie里面获取的,因为其它的应用程序获取不到这个cookie的值,所以我们能够保证post的安全性。开启xsrf protection也很简单。

settings = {
    'cookie_secret' : 'S6Bp2cVjSAGFXDZqyOh+hfn/fpBnaEzFh22IVmCsVJQ=',
    'xsrf_cookies' : True
}

application = tornado.web.Application([
    (r"/", MainHandler),
    (r"/purchase", PurchaseHandler)
], **settings)

在post提交的form里面,我们只需要设置如下:

<form action="/purchase" method="POST">

    <input type="submit" value="同意" name="agree"/>
</form>

User Authentication

tornado还提供了用户认证功能,当用户登录之后,会将用户的相关信息保存到一个地方,通常是在cookie里面。当用户的cookie过期,再次访问web服务的时候就会被重定向到登陆页面。

要实现上述功能,只需要重载get_current_user函数,配置login_url和使用authenticated decorator就行了。如下:

class BaseHandler(tornado.web.RequestHandler):
    def get_current_user(self):
        return self.get_secure_cookie('username')

class LoginHandler(BaseHandler):
    def get(self):
        str = '''<body><form action="/login" method="POST">
                UserName: <input type="text" name="username" />
                          <input type="submit" value="Login" />
                </form></body>'''
        self.write(str)

    def post(self):
        self.set_secure_cookie('username', self.get_argument('username'))
        self.redirect('/')

class MainHandler(BaseHandler):
    @tornado.web.authenticated
    def get(self):
        user = self.current_user
        self.write('hello ' + user)

class LogoutHandler(BaseHandler):
    def get(self):
        self.clear_cookie('username')
        self.redirect('/')

settings = {
    'cookie_secret' : 'S6Bp2cVjSAGFXDZqyOh+hfn/fpBnaEzFh22IVmCsVJQ=',
    'login_url' : '/login'
}

application = tornado.web.Application([
    (r'/', MainHandler),
    (r'/login', LoginHandler),
    (r'/logout', LogoutHandler)
], **settings)

http_server = tornado.httpserver.HTTPServer(application)
http_server.listen(8080)
tornado.ioloop.IOLoop.instance().start()

总结

web安全一直是一个非常严重的问题,我们在写代码的时候一定要注意,这里有一篇如何写安全web的Guide。tornado虽然提供了一些安全机制,但是仍然不能完全保证绝对安全性,所以很多时候就需要我们决定我所写的服务到底应该有什么样的安全级别,从在这个级别下面如何保证安全性就可以了。

mtunnel - a simple http tunnel

介绍

mtunnel是一个简易的http隧道工具,使用python实现,基于tornado。

为了解决远端用户服务器的一些问题,我们需要远程连接到用户的服务器上面,但因为企业安全性问题,外部根本不可能访问相关服务器,只能通过用户的本地机器去访问,但是用户本地机器也在内网环境下面,外部不能直接连接。

因为用户的机器在内网环境下面,也不可能通过通常的代理去连接。所以比较好的方式就是通过一台双方都能访问的公网服务器,来交换双方的数据。

mtunnel通过http tunnel的方式来进行数据的交互。它将双方交互的数据封装到http的body里面,通过http协议进行传输。这样有几个好处:

  • 不用关心交互的具体协议,mtunnel只是将双方交互的所有信息完全封装到http body里面进行传递。
  • http一般不会被企业的网络安全规则给屏蔽,内网穿透性更好。

假设,企业服务器启动了sshd,而我们这边使用putty。流程如下:

image

  • putty直接将数据发送给forward proxy,由forward proxy将数据通过http body发送给server。
  • server收到数据之后将其放置在一个buffer中。如果reverse proxy这时候已经连接到server,则直接将数据发送给reverse proxy。
  • reverse proxy定时去server获取数据,如果有则将其发送给sshd。
  • sshd返回的数据同样流程返回给putty。

使用

mtunnel分为3个部分,与putty交互的forward proxy,与sshd交互的reverse proxy以及负责双方数据中转的server。

start server

    python server.py -p 8888

假设sever的ip地址为10.20.187.118,监听port为8888。

start reverse proxy

因为我们需要通过用户本地机器去访问服务器,所以首先必须用户同意让我们访问,也就是他需要在本地启动reverse proxy。

    python rproxy.py -host 10.20.189.241 -p 22 --server 10.20.187.118:8888

host和port连接的是sshd的ip和port,而server则是服务器的地址。
reverse proxy如果成功连接上了server,则会获取一个channel id。后续所有的通信都必须通过该channel id进行。

start forward proxy

    python fproxy.py --server 10.20.187.118:8888 --channel 112122 -p 8889

我们在本地启动forward proxy,监听本地8889端口,server为服务器的地址,而channel则是用户启动reverse proxy之后获取的channel id,由用户负责告诉我们。

use

当做了上述操作之后,我们只需要启动putty,连接forward proxy。然后putty就能与远端的sshd交互了。

远程协助?

为什么不使用QQ远程协助这种类似功能?主要有以下几点原因:

  • QQ远程协助能看到用户本地机器的很多信息,企业用户对于安全性问题比较敏感。
  • 网络问题,现在很多企业的网络环境非常不好,我们就碰到过太多次远程的时候网络坑爹造成完全无法工作的情况。

后续方向

现在的版本只是一个最基本的版本,很多问题没有考虑,后续主要考虑以下几个方面:

  • 安全性,尤其要保证channel id的安全性。后续考虑使用签名机制等。
  • 网络容错,还没怎么很好的处理网络出问题的情况,譬如网络断开等。
  • P2P,数据都走server会有延时,稳定等问题,如果能够P2P互通,就很强大了。

代码在这里,该版本只是笔者使用python实现的一个非常基础的版本,很多地方存在不足,后续会慢慢完善。

版权声明:自由转载-非商用-非衍生-保持署名 Creative Commons BY-NC-ND 3.0

swig的学习以及国密的python封装

起因

最近在研究国密算法,而我们主要是使用python来进行开发,所以就需要构建一个国密的python模块。

国密算法网上已经有很好的实现,笔者使用的是一个参考Xyssl实现的那个版本。因为这些版本都是c的,所以很容易将其扩展到python里面,但是为了跟python自身的crypto的行为一致,需要将国密生成相应的class。譬如,python的hashlib的md5,使用方式如下:

md = hashlib.md5("123")
md.update("456")
print md.hexdigest()
mdCopy = md.copy()
print mdCopy.digest()

所以,对于类似的国密sm3算法,我们扩展到python里面,也应该提供如上的使用方式。

注册一个class到python,也不是一件很困难的事情,但是笔者觉得可以采用更简单的方式,自然就考虑使用swig。

swig

swig(需翻墙)可以算是将c、c++代码生成到其他语言的一个代码生成器,

网上关于swig的学习介绍有很多,这里笔者只是记录一下写python的国密扩展时候使用到的swig知识点。

以sm3来说,为了以class的方式在python里面使用,我们首先需要在swig定义其class的template,如下:

class SM3 
{
public:
    SM3(const unsigned char* input, int inputLen);
    ~SM3();

    void update(const unsigned char* input, int inputLen);
    void digest(unsigned char output[32]);
    void hexdigest(unsigned char output[64]);
    SM3 copy();
};

在python里面使用的时候,我们可以使用如下方式创建一个sm3对象:

md = SM3("1234567890")

可以看到,在python里面,我们创建sm3对象只传入了一个参数,那么怎么对应c++里面的这两个(unsigned char*, int)呢?这里,我们使用swig里面的typemaps

%typemap(in) (const unsigned char* input, int inputLen) 
{
    $1 = (unsigned char*)PyString_AsString($input);
    $2 = PyString_Size($input);
}

这里,我们定义一个in typemap,swig碰到这种typemap会将python里的函数参数转换成对应的c++参数。

对于sm3的digest以及hexdigest,python里面如下:

print md.digest()
print md.hexdigest()

可以看到,在c++里面,output是函数的参数,而在python里面则是作为返回值。这里,我们可以使用argout typemap来处理。

%typemap(in, numinputs = 0) unsigned char [ANY] (unsigned char temp[$1_dim0]) 
{
    $1 = temp;
}

%typemap(argout) unsigned char [ANY] 
{
    Py_XDECREF($result);
    $result = PyString_FromStringAndSize((const char*)$1, $1_dim0);
}

这里,我们需要注意几个地方:

  • 因为digest以及hexdigest的array长度是32和64,所以使用ANY来匹配,使用$1_dim0来获取实际的array长度。
  • 因为digest以及hexdigest在python里面是没有参数的,所以我们需要swig忽略输入参数,使用numinputs = 0。
  • 因为没有参数传入,所以我们需要手动构造一个array,使用unsigned char temp[$1_dim0],然后将这个array的地址赋给实际的参数output。
  • 使用argout,将output的结果放到python函数的返回值。

上述的实现,是参考multi_argument_typemaps里面的很多例子,尤其是关于in buffer和out buffer的。

pygmcrypto

对于国密算法,笔者这里只使用了sm3和sm4,对于sm2来说,笔者认为太过复杂,还没有较好的开源实现,同时笔者也没有精力自己去写一个,如果有谁有好的实现,欢迎联系我,笔者乐意将其build进python模块里去。

代码pygmcrypto在这里。直接使用python setup.py install进行安装,使用如下:

from gmcrypto import sm4, sm3
md = sm4.new("1" * 16)
eData = md.encrypt("1" * 16)
dData = md.decrypt(dData)

md = sm3.new("123")
md.update("456")
md.hexdigest()

如果出现编译不成功的情况,很有可能是生成的swig template问题,大家只需要在swig目录下面make重新生成就可以了。swig的版本需要2.0以上。

版权声明:自由转载-非商用-非衍生-保持署名 Creative Commons BY-NC-ND 3.0