zoukankan      html  css  js  c++  java
  • how to read openstack code : wsgi

    要读懂本篇,你至少得写过一个python的web程序,并且把它部署到web服务器上过。

    什么是wsgi

    假设你写了一个python的web程序,并部署到了nginx上,那么一个http request的处理流程一般是下面这样:

    client/浏览器(发送请求)  - - - - > web服务器(转发该请求) - - - - > 你的程序(1. 处理请求。2.生成结果。3.返回结果)
                                                                                                          |
                                                                                                          |
                                                                                                          V
                                   client/浏览器(收到结果) <- - - -web服务器(接受你的程序返回的结果,并返回给浏览器)
    

    问题是,开发web程序的人和开发web服务器的人并没有沟通,为什么web application 能够部署在web 服务器上呢? 这是因为他们都遵循了相同的规则 -- WSGI。 WSGI 全名叫 web server gateway interface, 是python 中web程序和web服务器沟通的标准。 简单的说,只要你写的web application 遵守这个规则,另一个人开发的web 服务器也遵循这个规则,那么你写的程序就能运行在他开发的web服务器上。

    wsgi application

    WSGI对application和server做出了不同的规定,大部分人不需要写server,因此我们只了解一下application的规定即可。首先看一个标准的WSGI application。该application实现的逻辑就是,返回client所使用的http method(GET/POST等):

    def application(env, callback):
    	"""
    	我们知道,web server 接到浏览器请求后会转发请求给 application。 请求的内容,以及一些环境变量如client IP, http method等会放在一个字典对象中传递给application。 就是这里的env参数。
    	同时,server还会传递一个callback对象给application. 这样application可以通过该对象返回给server一些信息。所以,wsgi的application必须要有这两个参数
    	"""
    
    	# EVN中包含了client 的各种环境变量,从中可以获取http method
    	response_body = 'The request method was %s' % env['REQUEST_METHOD']
    
    	# HTTP response的结果通常会有一个状态值和message。
    	# 比如, 状态有500/404/403/200等, message 有OK/Not Found/Internal error/Forbidden 等。
    	# 这里我们返回200 OK. 代表成功处理请求
    	status = '200 OK'
    
    	# HTTP response 的header 会包含一些必要的信息以便浏览器方便处理response。 这些必要的header信息需要按照以下格式放入list中
    	response_headers = [('Content-Type', 'text/plain'), ('Content-Length', str(len(response_body)))]
    
    	# 利用callback 告诉server这次访问的状态信息以及 response header。
    	callback(status, response_headers)
    
    	# response 的body必须是一个iterable的对象。 我们这里把response body 放入一个list。(string虽然也是可迭代对象,但string的迭代次数显然远远大于这个list。会影响效率)
    	return [response_body]
    

    总结上面的代码,可知道WSGI 对application 只做了如下几点规定:

    1. application 必须是可调用的object, 如函数,实现了__call__的对象
    
    2. application 必须接收一个字典类型的参数用于保存环境变量,一个callback function
    
    3. application 内要使用callback 返回 http status 和 response header
    
    4. 返回值必须是iterable的
    

    我们用一个简单的wsgi server来调用一下上面的 application, 看一下效果。

    from wsgiref.simple_server import make_server
    httpd = make_server('localhost', 8051, application)
    httpd.serve_forever()
    

    上面代码启动了一个wsgi server监听在localhost:8051, 并且把我们的application 部署到了该server上。 我们访问localhost 8051,看一下效果

        [root@netflow-AIO ~]# curl -i http://127.0.0.1:8051
        HTTP/1.0 200 OK
        Date: Sat, 26 Nov 2016 07:34:38 GMT
        Server: WSGIServer/0.1 Python/2.7.5
        Content-Type: text/plain
        Content-Length: 26
    
        The request method was GET
    

    可以看到, application 成功返回了代码中指定的response message,并且http code / http msg / response header 也都和代码中指定的一样。

    wsgi server

    大多数情况我们不需要编写server,因此不必了解太深。 但至少我们应该知道,在server中一定有这样的代码

    callable(env, callback)
    

    这里是server 调用application的地方。callable 是application对象的名字, env 是包含环境变量的字典,callback 是server传递给application的callback函数。想在applicaiton 被调用之前加一些逻辑,你可以在这行代码之前做改动。想修改application的返回结果,你可以在这行代码之后加逻辑

    wsgi 中间件

    WSGI 标准中除了application / server 还有一个很重要的概念--middleware。 其实 middleware 很好理解。 我们知道 server 会调用 application 处理请求, application 会把请求结果返回给server. Middleware,顾名思义就是在server 和 application 中间的一个对象。 对于server 来说 middleware 是一个application, 对于 application 来说, middleware 是一个server。也就是说,middleware同时实现了WSGI中对 application 和 server所做的规定。

    因为WSGI的这一特性,很多按照wsgi构建的系统都会有如下这样的结构

    app1 app2 app3 ... appN server
    

    当一个http request发给server 时, 该请求会依次传递给 appN ... app3 app2 app1, 然后response 又会从app1 开始往回传递直到 server 最后返回给客户端。也就是说,WSGI的系统是可以无限堆叠中间件的,你可以把自己的业务逻辑包装成一个个的中间件,需要的时候部署上去,不需要就拿下来。 openstack中所有的服务都是依照wsgi写的,因此也遵循这种结构。 比如neutron 服务的结构:

    request_id catch_errors authtoken keystonecontext extensions neutronapiapp_v2_0
    

    其中 catch_errors , authtoken, keystonecontext , extensions 都是WSGI的中间件。 了解了这些之后,我们可以做很多事情, 比如拿掉 authoken 和 keystonecontext, 这样调用neutron 的API就可以绕过权限校验了,因为这两个中间件是做权限校验用的。

    App 中间件 server示例

    接下来我们用一个例子展示一下 wsgi application middleware server 到底是什么样的。

    首先我们设计一个app,假设带着一个参数访问该app,它将告诉你该参数是奇数还是偶数。 api的url 假设是 check_number/, 代码如下:

    def check_number(env, start_response):
    
        number = int(env.get('PATH_INFO').split('/')[-1])
        response_body = 'even' if number%2 == 0 else 'odd'
    
        status = '200 OK'
        response_headers = [('Content-Type', 'text/plain'), ('Content-Length', str(len(response_body)))]
        start_response(status, response_headers)
        return [response_body]
    

    代码非常简单,因为我们假设url为check_number/, 所以int(env.get('PATH_INFO').split('/')[-1])可以很轻松得到参数。这里我们不考虑异常。

    接下来,我们设计一个middleware, 用于校验, 该middleware会检查http 访问的header, 如果带有特定的token,则认为是可信任用户,可以放行,继续访问check_number, 否则返回403 Forbidden。 代码如下:

    class AuthToken(object):
        def __init__(self, app):
            self.app = app
    
        def __call__(self, env, start_response):
            if env.get('HTTP_TOKEN') == '222':
                return self.app(env, start_response)
            else:
                response_body = 'Auth failed'
                status = '403 forbidden'
                response_headers = [('Content-Type', 'text/plain'), ('Content-Length', str(len(response_body)))]
                start_response(status, response_headers)
                return [response_body]
    

    这里稍微解释一下。 所谓中间件,肯定要能被server 调用, 所以必须提供__call__(env, start_response)接口,并且返回可迭代的return 如[response_body]。 但同时它又需要调用别的app。
    看我们AuthToken的代码,可以看到,它的逻辑就是,如果token合法(等于222) ,则继续调用application, 否则返回错误。

    有了验证中间件AuthToken, 有了业务逻辑check_number, 我们还可以再加一个中间件 CheckError。 该中间件捕捉所有的error / exception, 返回一个用户友好的消息,如 service maintain 等, 而不是直接把异常抛给用户。 代码如下:

    class CheckError(object):
        def __init__(self, app):
            self.app = app
    
        def __call__(self, env, start_response):
            try:
                return self.app(env, start_response)
            except Exception as e:
                response_body = 'Server is maintaining '
                status = '503  service maintain'
                response_headers = [('Content-Type', 'text/plain'), ('Content-Length', str(len(response_body)))]
                start_response(status, response_headers)
                return [response_body]
    

    逻辑很简单,如果有错误,则返回 service maintain, 否则返回正常的业务处理结果。

    接下来可以部署这些程序到服务器

    from wsgiref.simple_server import make_server
    httpd = make_server('10.79.99.86', 8051, CheckError(AuthToken(check_number)))
    httpd.serve_forever()
    

    尝试访问:

    [root@netflow-AIO bin]# curl http://10.79.99.86:8051/check_number/100 -H "token:222"
    even
    
    [root@netflow-AIO bin]# curl http://10.79.99.86:8051/check_number/100 -H "token:wrong"
    Auth failed
    
    [root@netflow-AIO bin]# curl http://10.79.99.86:8051/check_number/aaa -H "token:222"
    Server is maintaining 
    

    后面可以看到,网上对这种wsgi模块堆叠的部署方式叫pipeline。 但其实它和linux的pipeline不太一样, 其实是一种堆栈式的调用,调用流程图如下:

    request ----> server 生成 env 和 start_response
                ----> Check_Error.__call__
                    ---->AuthToken.__call__
                        ---->Check_Number(env,start_response)
                        <----Check_Number 返回
                    <----AuthToken 返回
                <----Check_Error 返回
    response <----
    

    WSGI in openstack

    前面提到,写一个wsgi application 有若干规则要遵守,比如提供两个参数 env 和 callback , 再比如return 一个可迭代对象。 这些规则其实跟业务逻辑无关。作为一个程序员,你可能更关心业务逻辑而不是wsgi的语法。你可能更希望写一个下面这样的程序

    def application(req):
        # some logic
        return response
    

    req 是客户的http 请求, response 是程序进行处理后的返回。这样对程序员来说就简单多了,只需要关心业务逻辑而不用关心wsgi稍显繁琐的语法。openstack中就实现了这样的简化,在openstack中,实现一个wsgi 程序可以这样写

    import webob
    import webob.dec
    from webob.response import Response
    
    @webob.dec.wsgify
    def myapp(req):
        return Response(body=req.url)
    
    from wsgiref.simple_server import make_server
    httpd = make_server('localhost', 8051, myapp)
    httpd.serve_forever()
    

    webob是用于web编程的一个model,通过它的wsgify这个装饰器,你可以很方便的把一个 接受 request 参数,返回response参数的函数转成wsgi application。现在你可以不必关心wsgi的细节,只要实现一个接受request 返回 response的函数就可以了。 默认情况下, req 是webob.request.Request类型的对象。

  • 相关阅读:
    绕过卡巴斯基等杀软抓取 lsass 内存踩坑
    Redis 未授权访问 getshll
    linux 中用 sed 指令 删除/添加 指定行首内容
    使用 git 的时候,出现很多别人的commit
    关于近源渗透测试的免杀
    GIT 使用
    web安全入门(更新中)
    一篇入门代码审计
    动态加载dll的实现+远线程注入
    spring系列cve poc编写
  • 原文地址:https://www.cnblogs.com/kramer/p/6114418.html
Copyright © 2011-2022 走看看