NiceLeeのBlog 用爱发电 bilibili~

Go 在Web框架中嵌入伪装数据流

2023-02-01
nIceLee

阅读:


总的来说,经典的Web框架会将每个HTTP请求回复抽象为两部分: Request, Response
其中Request包括SchemeMethodHTTP VersionUrlHeadersBody等;Response包括StatusHeadersBody等等。
CookieUser-AgentHostQuery/Post参数等则更加细节,是对UrlHeadersBody的进一步处理。

但有一个问题,这些框架的逻辑,每一次请求更加像是单向的数据流。Client每一次将请求发送(至少是除了Body以外的部分)给Server端完毕后,Server端再处理给出回应。

像特殊情况,上传超大文件这种,一般都是要自己实现的,主要是针对RequestBody进行一些解析实现。 不过虽然开始处理时没有收到完整的数据,但是Body的大小其实在Content-length里是已知的。Server也是在保存文件成功或者失败以后再给Client答复。

Request的Body大小一般来说是已知的,写在Content-length里,当然也可以未知,例如Transfer-Encoding: Chunked通过指示每一份内容来做分割和控制,不过极其少见,估计也没多少场景中会用到这个。

Response的Body大小一般来说也是已知的,写在Content-length里,当然也可以未知,通过Transfer-Encoding: Chunked一段一段的写。

(当然http2里面这个被禁止使用了,详见MDN)

是的,理论上你可以通过Chunked+ Chunked来实现一个双向数据流,至少本地可以跑通。但是会有缺陷,就像TCP的粘包一样,中间层可能会缓存Chunked0Chunked1、…ChunkedN甚至等到结尾再传给你。 毕竟逻辑上的普遍情况是Server端收到完整Request后,再返回Response,之后当次HTTP请求算是跑完了一整个流程。

我们考虑以下场景:

  • Client发送 cChunked0,cChunked1
  • Server接收到 cChunked0,cChunked1,处理后返回 sChunked0、sChunked1
  • Client接收到 sChunked0、sChunked1,处理后再发送 cChunked2,cChunked结束边界
  • Server接收到 cChunked2,cChunked结束边界, 处理后返回sChunked结束边界
  • HTTP请求结束

实际上,可能在上述第二步和第三步就会卡住,无法实现。

所以,讲这么多最简单的还是Websocket。 通过Client通过HTTP GET附带Upgrade请求, Server 回复 101消息表示请求升级。接下来则完全不必按照Websocket来了,因为中间层不会浪费资源去解析或截留缓存接下来的内容。

前言

编程编程,不外乎数据的存储、传输、处理利用以及呈现。翻译一下,就是本地缓存/持久化存储(例如数据库)、网络通信、大数据/机器学习/深度学习、人机交互(GUI、CLI)等等。

每当学一门语言或工具框架,我总喜欢从通信这个角度,通过实现自定义的双向http流(Websocket·伪)客户端以及服务端,来熟悉了解一些较为基础底层的东西。

因为不成熟,基本上是从最基本的东西开始实现,然后根据需要慢慢添加扩展;没有站在一个较高的层面上去规划设计,导致耦合较深。通俗点讲,这个东西被设计用来做这个,以后就只能做这个了,再多一点的话拓展实现就会比较麻烦,再多两点就要命了。

而在玩具已经实现以后,就没有动力去花功夫去重新再实现一遍了。

于是我想,有没有可能在现有的框架下直接自定义DIY呢?

想到了,那就去做!

服务端实现

尝试直接操作Request.Body

下面是一个常见的HTTP Handler,我第一想法是操作像大文件上传那样操作Request.Body
但是很可惜,在HTTP请求方法为GET时,读取Request的Body会返回EOF异常。

func HandleStream(w http.ResponseWriter, r *http.Request) {
    w.Header().Add("xx", "this is a echo stream")
    w.Header().Add("Sec-WebSocket-Accept", randomString1)
    w.Header().Add("Connection", "Upgrade")
    w.Header().Add("Upgrade", "websocket")
    w.WriteHeader(101)
    // io.Copy(w, r.Body)
    buffer := make([]byte, 1024)
    for {
        len, err := r.Body.Read(buffer)
        if len > 0 {
            w.Write(buffer[:len])
        }
        if err != nil {
            break
        }
    }
}

劫持ResponseWriter

实现Websocket·伪的最好方式是参考Websocket·真gorilla/websocket

我们可以看一个服务端的简单例子

var upgrader = websocket.Upgrader{} // use default options

func echo(w http.ResponseWriter, r *http.Request) {
    c, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Print("upgrade:", err)
        return
    }
    defer c.Close()
    for {
        mt, message, err := c.ReadMessage()
        if err != nil {
            log.Println("read:", err)
            break
        }
        log.Printf("recv: %s", message)
        err = c.WriteMessage(mt, message)
        if err != nil {
            log.Println("write:", err)
            break
        }
    }
}

去查看upgrader.Upgrade方法

h, ok := w.(http.Hijacker)
if !ok {
    return u.returnError(w, r, http.StatusInternalServerError, "websocket: response does not implement http.Hijacker")
}
var brw *bufio.ReadWriter
netConn, brw, err := h.Hijack()
if err != nil {
    return u.returnError(w, r, http.StatusInternalServerError, err.Error())
}

if brw.Reader.Buffered() > 0 {
    netConn.Close()
    return nil, errors.New("websocket: client sent data before handshake is complete")
}
// 这里netConn就可以放心使用了

我们可以将http.ResponseWriter 强转为 http.Hijacker并从中获取net.Conn,这是一个可以Read、Write、Close的接口,接下来你懂的。。。

在Gin框架中实现

http.Handler很容易转成gin的Handler

// 假设实现了func Handler(w http.ResponseWriter, r *http.Request)
func HandleTunnel(c *gin.Context) {
    Handler(c.Writer, c.Request)
    // c.Abort()
}

客户端实现

客户端的难点在于路由如何拦截处理到HTTPS代理的CONNECT方法

在http server中实现

http.HandleFunc("/404", demo.Handle404)
http.HandleFunc(pattern, handler)

查看源码,发现CONNECT方法的请求url path为空字符串,无论怎么添加pattern也不匹配,会由默认的NotFound Handler进行处理

一个解决方案是自定义一个ServerMux作为root Handler

mux := router.InitRouters()
http.ListenAndServe(":8080", mux)
func InitRouters() http.Handler {
    // 假设实现了func HandleConnect(w http.ResponseWriter, r *http.Request)
    var handler = http.HandlerFunc(HandleConnect)
    mux := NewMux(&handler, http.DefaultServeMux)
    // 其它
    mux.HandleFunc("/404", demo.Handle404)
    return mux
}

type Mux struct {
    ServeMux                 *http.ServeMux
    ConnectMethodHandlerFunc *http.HandlerFunc
}

func NewMux(ConnectMethodHandlerFunc *http.HandlerFunc, ServeMux *http.ServeMux) *Mux {
    return &Mux{
        ServeMux,
        ConnectMethodHandlerFunc,
    }
}

func (mux *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if r.Method == "CONNECT" && mux.ConnectMethodHandlerFunc != nil {
        (*mux.ConnectMethodHandlerFunc)(w, r)
    } else {
        mux.ServeMux.ServeHTTP(w, r)
    }
}

func (mux *Mux) HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) {
    mux.ServeMux.HandleFunc(pattern, handler)
}

func (mux *Mux) Handle(pattern string, handler http.Handler) {
    mux.ServeMux.Handle(pattern, handler)
}

在Gin框架中实现

// 假设实现了func HandleConnect(w http.ResponseWriter, r *http.Request)
// 初始化路由的时候 gin.NoRoute(HandleNoroute, ...)
func HandleNoroute(c *gin.Context) {
    if c.Request.Method == "CONNECT" {
        HandleConnect(c.Writer, c.Request)
        // c.Abort()
    }
}

关于HTTP代理

基础http server在自定义的root handler对Host进行判断

Gin框架在较上层添加中间件对Host进行判断拦截即可

其它

需要注意的是,以上和Websocket一样,仅适用于HTTP 1.1。
最好再加上一个判断,当遇到HTTP2请求时直接抛出异常。


内容
隐藏