😺

net http Hijackerを調べてみた

2022/05/27に公開

某slackでHijackerという発言がでてきたのでしらべてみた。調査した結果、何をするものなのかはわかったので書き残しておきます。

概要

  • Hijackerとは?
  • Hijacker Interface の内容
  • Hijackerの実装

Hijackerとは?

Hijackerとは、結論から言うと通常のnet/httpのhandlerで使用した場合respoonseに実装されているHijackメソッドからコネクションを引き継ぐことができる機能です。後述のサンプルを見てもらうとわかるがconnectionのcloseなども自分でやらねばならず割と面倒ではあります。

The Hijacker interface is implemented by ResponseWriters that allow an HTTP handler to take over the connection.

実際の使用ケース

src/net/rpc/server.go に実際に使用しているケースがありました。

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)
}

その他の使用箇所

ここ以外でgo本体で使用している箇所はほぼテストでした。

 ag Hijacker -l
api/go1.txt
src/net/rpc/server.go
src/net/http/example_test.go
src/net/http/serve_test.go
src/net/http/httputil/reverseproxy.go
src/net/http/server.go
src/net/http/httputil/reverseproxy_test.go
src/net/http/transport_test.go
src/net/http/clientserver_test.go

Hijacker Interface の内容

Hijackerはconnectionを引き継いでその後http server libraryはconnectionに対して何もしないようだ。ちなみにこの以下のコメントちゃんと読んだほうがいいと思います。

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)
}

Hijackerの実装

net/http/server.goに実装されていて以下の処理内容になる

  1. ServeHTTPが終了した後に呼び出されていればエラー判定
  2. ヘッダーに何か書かれていればFlushする
  3. responseのconnectionを受取ロックをかける
  4. c.hijackLockedの実行
// Hijack implements the Hijacker.Hijack method. Our response is both a ResponseWriter
// and a Hijacker.
func (w *response) Hijack() (rwc net.Conn, buf *bufio.ReadWriter, err error) {
	if w.handlerDone.isSet() {
		panic("net/http: Hijack called after ServeHTTP finished")
	}
	if w.wroteHeader {
		w.cw.flush()
	}

	c := w.conn
	c.mu.Lock()
	defer c.mu.Unlock()

	// Release the bufioWriter that writes to the chunk writer, it is not
	// used after a connection has been hijacked.
	rwc, buf, err = c.hijackLocked()
	if err == nil {
		putBufioWriter(w.w)
		w.w = nil
	}
	return rwc, buf, err
}

ServeHTTPが終了した後に呼び出されていればエラー判定

  • w.handlerDone.isSet
  • handleDoneはhandlerが終了しているかを持っている
handlerDone atomicBool // set true when the handler exits

type atomicBool int32

func (b *atomicBool) isSet() bool { return atomic.LoadInt32((*int32)(b)) != 0 }
func (b *atomicBool) setTrue()    { atomic.StoreInt32((*int32)(b), 1) }
func (b *atomicBool) setFalse()   { atomic.StoreInt32((*int32)(b), 0) }
  • こちらはresponseのfinishRequestでsetされているので
  • finishRequestが呼ばれてないかのチェックをしていると考えればいいはず
func (w *response) finishRequest() {
	w.handlerDone.setTrue()

ヘッダーに何か書かれていればFlushする

  • w.wroteHeader
  • headerが書かれていればbufferをflushする
if w.wroteHeader {
	w.cw.flush()
}
wroteHeader      bool               // reply header has been (logically) written

responseのconnectionを受取ロックをかける

c := w.conn
c.mu.Lock()
defer c.mu.Unlock()

c.hijackLockedの実行

  • hijackLockedを実行してerrでなければbufferioWriterを開放する
// Release the bufioWriter that writes to the chunk writer, it is not
// used after a connection has been hijacked.
rwc, buf, err = c.hijackLocked()
if err == nil {
	putBufioWriter(w.w)
	w.w = nil
}

  • named return value
  • 新しいbufを生成してconnectionのstateを更新した上で返す
func (c *conn) hijackLocked() (rwc net.Conn, buf *bufio.ReadWriter, err error) {
	if c.hijackedv {
		return nil, nil, ErrHijacked
	}
	c.r.abortPendingRead()

	c.hijackedv = true
	rwc = c.rwc
	rwc.SetDeadline(time.Time{})

	buf = bufio.NewReadWriter(c.bufr, bufio.NewWriter(rwc))
	if c.r.hasByte {
		if _, err := c.bufr.Peek(c.bufr.Buffered() + 1); err != nil {
			return nil, nil, fmt.Errorf("unexpected Peek failure reading buffered byte: %v", err)
		}
	}
	c.setState(rwc, StateHijacked, runHooks)
	return
}

サンプル

package main

import (
	"fmt"
	"log"
	"net/http"
)

func main() {
	http.HandleFunc("/hijack", func(w http.ResponseWriter, r *http.Request) {
		hj, ok := w.(http.Hijacker)
		if !ok {
			http.Error(w, "webserver doesn't support hijacking", http.StatusInternalServerError)
			return
		}
		conn, bufrw, err := hj.Hijack()
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		// Don't forget to close the connection:
		defer conn.Close()
		bufrw.WriteString("Now we're speaking raw TCP. Say hi: ")
		bufrw.Flush()
		s, err := bufrw.ReadString('\n')
		if err != nil {
			log.Printf("error reading string: %v", err)
			return
		}
		fmt.Fprintf(bufrw, "You said: %q\nBye.\n", s)
		bufrw.Flush()
	})

	log.Println("Starting server on :8080")
	if err := http.ListenAndServe(":8080", nil); err != nil {
		log.Fatalf("ListenAndServe failed: %v", err)
	}
}

Discussion