😺

net http Hijackerを調べてみた

2022/05/27に公開約5,500字
  • 某slackでHijackという発言がでてきたのでしらべてみた。
  • 正直何をするものなのかはわかったんだが
  • テスト以外の使用ケースがそこまで思い浮かびませんでした

Hijackとは?

  • https://pkg.go.dev/net/http#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.

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)
}
  • ここ以外で本体で使用している箇所はほぼテストでした
 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

サンプル

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()
	})
}
  • connectionとbufferを得ます
conn, bufrw, err := hj.Hijack()
if err != nil {
	http.Error(w, err.Error(), http.StatusInternalServerError)
	return
}

Hijacker Interface

  • 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の実装

  • responseに実装されている
// 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
}

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

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
}

Discussion

ログインするとコメントできます