7e3183488b
Fix bugs in the file handler: unbuffered send channel deadlock on Close, conn leak when send drops on done, and Addr() returning nil. Add done- priority select in send(), explicit ln.Close() in Close(), Unwrap() on responseWriter for http.ResponseController, and nil guards. Also add package doc, NewHandler doc, and 39 unit tests covering the listener, responseWriter, handler lifecycle, auth, and file serving.
244 lines
5.3 KiB
Go
244 lines
5.3 KiB
Go
// Package file provides a static file-serving handler for GOST.
|
|
// It serves files from a configured directory over HTTP via the GOST proxy chain.
|
|
package file
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/go-gost/core/auth"
|
|
"github.com/go-gost/core/handler"
|
|
"github.com/go-gost/core/logger"
|
|
md "github.com/go-gost/core/metadata"
|
|
"github.com/go-gost/core/recorder"
|
|
xctx "github.com/go-gost/x/ctx"
|
|
xrecorder "github.com/go-gost/x/recorder"
|
|
"github.com/go-gost/x/registry"
|
|
)
|
|
|
|
func init() {
|
|
registry.HandlerRegistry().Register("file", NewHandler)
|
|
}
|
|
|
|
type fileHandler struct {
|
|
handler http.Handler
|
|
server *http.Server
|
|
ln *singleConnListener
|
|
md metadata
|
|
options handler.Options
|
|
recorder recorder.RecorderObject
|
|
}
|
|
|
|
// NewHandler creates a static file-serving handler. It registers as "file" in the
|
|
// handler registry and serves files from the directory specified by the "file.dir"
|
|
// or "dir" metadata key.
|
|
func NewHandler(opts ...handler.Option) handler.Handler {
|
|
options := handler.Options{}
|
|
for _, opt := range opts {
|
|
opt(&options)
|
|
}
|
|
|
|
return &fileHandler{
|
|
options: options,
|
|
}
|
|
}
|
|
|
|
func (h *fileHandler) Init(md md.Metadata) (err error) {
|
|
if err = h.parseMetadata(md); err != nil {
|
|
return
|
|
}
|
|
|
|
h.handler = http.FileServer(http.Dir(h.md.dir))
|
|
h.server = &http.Server{
|
|
Handler: http.HandlerFunc(h.handleFunc),
|
|
}
|
|
|
|
for _, ro := range h.options.Recorders {
|
|
if ro.Record == xrecorder.RecorderServiceHandler {
|
|
h.recorder = ro
|
|
break
|
|
}
|
|
}
|
|
|
|
h.ln = &singleConnListener{
|
|
conn: make(chan net.Conn, 1),
|
|
done: make(chan struct{}),
|
|
addr: &listenerAddr{},
|
|
}
|
|
go h.server.Serve(h.ln)
|
|
|
|
return
|
|
}
|
|
|
|
func (h *fileHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler.HandleOption) error {
|
|
var clientAddr string
|
|
if srcAddr := xctx.SrcAddrFromContext(ctx); srcAddr != nil {
|
|
clientAddr = srcAddr.String()
|
|
}
|
|
|
|
remoteAddr := conn.RemoteAddr()
|
|
localAddr := conn.LocalAddr()
|
|
|
|
h.options.Logger.WithFields(map[string]any{
|
|
"client": clientAddr,
|
|
"remote": remoteAddr.String(),
|
|
"local": localAddr.String(),
|
|
}).Infof("%s - %s", remoteAddr, localAddr)
|
|
|
|
h.ln.send(conn)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (h *fileHandler) Close() error {
|
|
if h.ln != nil {
|
|
h.ln.Close()
|
|
}
|
|
return h.server.Close()
|
|
}
|
|
|
|
func (h *fileHandler) handleFunc(w http.ResponseWriter, r *http.Request) {
|
|
start := time.Now()
|
|
|
|
ro := &xrecorder.HandlerRecorderObject{
|
|
Service: h.options.Service,
|
|
RemoteAddr: r.RemoteAddr,
|
|
Network: "tcp",
|
|
Host: r.Host,
|
|
Proto: "http",
|
|
HTTP: &xrecorder.HTTPRecorderObject{
|
|
Host: r.Host,
|
|
Method: r.Method,
|
|
Proto: r.Proto,
|
|
Scheme: r.URL.Scheme,
|
|
URI: r.RequestURI,
|
|
Request: xrecorder.HTTPRequestRecorderObject{
|
|
ContentLength: r.ContentLength,
|
|
Header: r.Header,
|
|
},
|
|
},
|
|
Time: start,
|
|
}
|
|
|
|
log := h.options.Logger.WithFields(map[string]any{
|
|
"remote": r.RemoteAddr,
|
|
})
|
|
|
|
if log.IsLevelEnabled(logger.TraceLevel) {
|
|
dump, _ := httputil.DumpRequest(r, false)
|
|
log.Trace(string(dump))
|
|
}
|
|
|
|
rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
|
|
|
|
defer func() {
|
|
ro.Duration = time.Since(start)
|
|
ro.HTTP.StatusCode = rw.statusCode
|
|
ro.HTTP.Response = xrecorder.HTTPResponseRecorderObject{
|
|
ContentLength: rw.contentLength,
|
|
Header: rw.Header(),
|
|
}
|
|
if err := ro.Record(context.Background(), h.recorder.Recorder); err != nil {
|
|
log.Errorf("record: %v", err)
|
|
}
|
|
|
|
log.WithFields(map[string]any{
|
|
"duration": time.Since(start),
|
|
}).Infof("%s %s %s %d %d", r.Method, r.RequestURI, r.Proto, rw.statusCode, rw.contentLength)
|
|
}()
|
|
|
|
if auther := h.options.Auther; auther != nil {
|
|
u, p, _ := r.BasicAuth()
|
|
ro.ClientID = u
|
|
if _, ok := auther.Authenticate(r.Context(), u, p, auth.WithService(ro.Service)); !ok {
|
|
w.Header().Set("WWW-Authenticate", "Basic")
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
}
|
|
|
|
if h.handler != nil {
|
|
h.handler.ServeHTTP(rw, r)
|
|
}
|
|
}
|
|
|
|
type listenerAddr struct{}
|
|
|
|
func (a *listenerAddr) Network() string { return "file" }
|
|
func (a *listenerAddr) String() string { return "file" }
|
|
|
|
type singleConnListener struct {
|
|
conn chan net.Conn
|
|
addr net.Addr
|
|
done chan struct{}
|
|
mu sync.Mutex
|
|
}
|
|
|
|
func (l *singleConnListener) Accept() (net.Conn, error) {
|
|
select {
|
|
case conn := <-l.conn:
|
|
return conn, nil
|
|
|
|
case <-l.done:
|
|
return nil, net.ErrClosed
|
|
}
|
|
}
|
|
|
|
func (l *singleConnListener) Close() error {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
|
|
select {
|
|
case <-l.done:
|
|
default:
|
|
close(l.done)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (l *singleConnListener) Addr() net.Addr {
|
|
return l.addr
|
|
}
|
|
|
|
func (l *singleConnListener) send(conn net.Conn) {
|
|
select {
|
|
case <-l.done:
|
|
conn.Close()
|
|
return
|
|
default:
|
|
}
|
|
select {
|
|
case l.conn <- conn:
|
|
case <-l.done:
|
|
conn.Close()
|
|
}
|
|
}
|
|
|
|
type responseWriter struct {
|
|
http.ResponseWriter
|
|
statusCode int
|
|
contentLength int64
|
|
}
|
|
|
|
func (w *responseWriter) Write(p []byte) (int, error) {
|
|
n, err := w.ResponseWriter.Write(p)
|
|
w.contentLength += int64(n)
|
|
return n, err
|
|
}
|
|
|
|
func (w *responseWriter) WriteHeader(statusCode int) {
|
|
w.statusCode = statusCode
|
|
w.ResponseWriter.WriteHeader(statusCode)
|
|
}
|
|
|
|
// Unwrap returns the underlying ResponseWriter, allowing http.ResponseController
|
|
// to discover optional interfaces (Flusher, Hijacker, etc.) on the wrapped writer.
|
|
func (w *responseWriter) Unwrap() http.ResponseWriter {
|
|
return w.ResponseWriter
|
|
}
|