云深不归处 發表於 2020-4-4 11:35:00

Go Hijack黑科技

<p>最近在看Go标准库里面的<code>rpc</code>源码,发现了下面一段代码:</p>
<pre><code>// ServeHTTP implements an http.Handler that answers RPC requests.
func (server *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {
        if req.Method != "CONNECT" {
                w.Header().Set("Content-Type", "text/plain; charset=utf-8")
                w.WriteHeader(http.StatusMethodNotAllowed)
                io.WriteString(w, "405 must CONNECT\n")
                return
        }
        conn, _, err := w.(http.Hijacker).Hijack()        //注意看这里
        if err != nil {
                log.Print("rpc hijacking ", req.RemoteAddr, ": ", err.Error())
                return
        }
        io.WriteString(conn, "HTTP/1.0 "+connected+"\n\n")
        server.ServeConn(conn)
}
</code></pre>
<p>这是一段接管 HTTP 连接的代码,所谓的接管 HTTP 连接是指这里接管了 HTTP 的 TCP 连接,也就是说 Golang 的内置 HTTP 库和 HTTPServer 库将不会管理这个 TCP 连接的生命周期,这个生命周期已经划给 Hijacker 了。</p>
<h2 id="hijack-介绍"><code>Hijack</code> 介绍</h2>
<p>首先看下官方的<code>http.Hijacker</code>接口介绍:</p>
<pre><code>type Hijacker interface {
        // Hijack lets the caller take over the connection.
        // After a call to Hijack the HTTP server library
        // will not do anything else with the connection.
        //
        // It becomes the caller's responsibility to manage
        // and close the connection.
        //
        // The returned net.Conn may have read or write deadlines
        // already set, depending on the configuration of the
        // Server. It is the caller's responsibility to set
        // or clear those deadlines as needed.
        //
        // The returned bufio.Reader may contain unprocessed buffered
        // data from the client.
        //
        // After a call to Hijack, the original Request.Body must not
        // be used. The original Request's Context remains valid and
        // is not canceled until the Request's ServeHTTP method
        // returns.
        Hijack() (net.Conn, *bufio.ReadWriter, error)
}
</code></pre>
<p><code>Hijack()</code>可以将HTTP对应的TCP连接取出,连接在<code>Hijack()</code>之后,HTTP的相关操作就会受到影响,调用方需要负责去关闭连接。</p>
<h2 id="实现原理">实现原理</h2>
<p>当我们在调用<code>Hijack</code>时,<code>Http Server</code>会将当前连接设置为<code>StateHijacked</code>,并从管理的connection列表中删除,交给使用者自己来管理:</p>
<pre><code>// 当前连接设置为StateHijacked
func (c *conn) setState(nc net.Conn, state ConnState) {
        srv := c.server
        switch state {
        case StateNew:
                srv.trackConn(c, true)
        case StateHijacked, StateClosed:
                srv.trackConn(c, false) //这里调用trackConn方法
        }
        if state &gt; 0xff || state &lt; 0 {
                panic("internal error")
        }
        packedState := uint64(time.Now().Unix()&lt;&lt;8) | uint64(state)
        atomic.StoreUint64(&amp;c.curState.atomic, packedState)
        if hook := srv.ConnState; hook != nil {
                hook(nc, state)
        }
}

//从管理的连接中删除
func (s *Server) trackConn(c *conn, add bool) {
        s.mu.Lock()
        defer s.mu.Unlock()
        if s.activeConn == nil {
                s.activeConn = make(map[*conn]struct{})
        }
        if add {
                s.activeConn = struct{}{}
        } else {
                delete(s.activeConn, c)
        }
}
</code></pre>
<h2 id="和正常的http请求区别">和正常的HTTP请求区别</h2>
<p>为了和正常的HTTP请求对比,写了下面代码:</p>
<pre><code>//hijack请求
http.HandleFunc("/hijack", func(w http.ResponseWriter, r *http.Request) {
                hj, _ := w.(http.Hijacker)
                conn, buf, _ := hj.Hijack()
                defer conn.Close()

                buf.WriteString("hello hijack\n")
                buf.Flush()
        })

//正常的http请求
http.HandleFunc("/http", func(writer http.ResponseWriter, request *http.Request) {
        fmt.Fprintln(writer, "hello http\n")
})
</code></pre>
<p>我们分别来curl请求下这两个接口:</p>
<pre><code>$ curl "http://127.0.0.1:9001/hijack" -i
hello hijack

$ curl "http://127.0.0.1:9001/http" -i
HTTP/1.1 200 OK
Date: Mon, 17 Feb 2020 03:41:52 GMT
Content-Length: 12
Content-Type: text/plain; charset=utf-8

hello http
</code></pre>
<p>看出来了吗?乍一看,首先我们能看到使用<code>Hijack</code>的请求返回没有响应头信息。这里我们要明白的是,<code>Hijack</code>之后虽然能正常输出数据,但完全没有遵守http协议。</p>
<p>为什么呢?翻下源码:</p>
<pre><code>// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
        //省略...
        serverHandler{c.server}.ServeHTTP(w, w.req)
        w.cancelCtx()
        if c.hijacked() { //被hijack后直接返回
                        return
        }
        w.finishRequest()
        //省略...       
}
</code></pre>
<p>这是<code>net/http</code>包中的方法,也是http路由的核心方法。调用<code>ServeHTTP</code>方法,如果被<code>Hijack</code>了就直接return了,而一般的http请求会经过后边的<code>finishRequest</code>方法,加入headers等并关闭连接。</p>
<h2 id="使用场景">使用场景</h2>
<p>当不想使用内置服务器的HTTP协议实现时,请使用<code>Hijack</code>。</p>
<p>一般在在创建连接阶段使用HTTP连接,后续自己完全处理connection。符合这样的使用场景的并不多,基于HTTP协议的rpc算一个,从HTTP升级到WebSocket也算一个。</p>
<h4 id="rpc使用">RPC使用</h4>
<p>go中自带的rpc可以直接复用http server处理请求的那一套流程去创建连接,连接创建完毕后再使用Hijack方法拿到连接。</p>
<pre><code>// ServeHTTP implements an http.Handler that answers RPC requests.
func (server *server) servehttp(w http.responsewriter, req *http.request) {
    if req.method != "connect" {
      w.header().set("content-type", "text/plain; charset=utf-8")
      w.writeheader(http.statusmethodnotallowed)
      io.writestring(w, "405 must connect\n")
      return
    }
    conn, _, err := w.(http.hijacker).hijack()
    if err != nil {
      log.print("rpc hijacking ", req.remoteaddr, ": ", err.error())
      return
    }
    io.writestring(conn, "http/1.0 "+connected+"\n\n")
    server.serveconn(conn)
}
</code></pre>
<p>客户端通过向服务端发送method为connect的请求创建连接,创建成功后即可开始rpc调用。</p>
<h4 id="websocket使用">WebSocket使用</h4>
<pre><code>// ServeHTTP implements the http.Handler interface for a WebSocket
func (s Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    s.serveWebSocket(w, req)
}

func (s Server) serveWebSocket(w http.ResponseWriter, req *http.Request) {
    rwc, buf, err := w.(http.Hijacker).Hijack()
    if err != nil {
      panic("Hijack failed: " + err.Error())
    }
    // The server should abort the WebSocket connection if it finds
    // the client did not send a handshake that matches with protocol
    // specification.
    defer rwc.Close()
    conn, err := newServerConn(rwc, buf, req, &amp;s.Config, s.Handshake)
    if err != nil {
      return
    }
    if conn == nil {
      panic("unexpected nil conn")
    }
    s.Handler(conn)
}
</code></pre>
<p>websocket在创建连接的阶段与http使用相同的协议,而在后边的数据传输的过程中使用了他自己的协议,符合了Hijack的用途。通过serveWebSocket方法将HTTP协议升级到Websocket协议。</p>
<h2 id="小结">小结</h2>
<p>使用<code>Hijack</code>时的一些注意点:</p>
<ul>
<li><code>Hijack</code>之后的conn需要手动关闭;</li>
<li><code>Hijack</code>之后不能再对<code>w http.responsewriter</code>里面的<code>w</code>写入数据;</li>
<li><code>http.response</code>实现了<code>http.Hijacker</code>接口;</li>
</ul><br><br>
来源:https://www.cnblogs.com/maratrix/p/12631005.html
頁: [1]
查看完整版本: Go Hijack黑科技