github.com/nikandfor/tlog@v0.21.5-0.20231108111739-3ef89426a96d/web/server.go (about)

     1  package web
     2  
     3  import (
     4  	"bufio"
     5  	"context"
     6  	"embed"
     7  	"fmt"
     8  	"io"
     9  	"net"
    10  	"net/http"
    11  	"strings"
    12  	"sync"
    13  
    14  	"github.com/nikandfor/errors"
    15  	"github.com/nikandfor/hacked/hnet"
    16  	"github.com/nikandfor/tlog"
    17  	"github.com/nikandfor/tlog/convert"
    18  	"github.com/nikandfor/tlog/tlz"
    19  )
    20  
    21  type (
    22  	Agent interface {
    23  		Query(ctx context.Context, w io.Writer, q string) error
    24  	}
    25  
    26  	Server struct {
    27  		Agent Agent
    28  		FS    http.FileSystem
    29  	}
    30  
    31  	response struct {
    32  		req *http.Request
    33  		w   io.Writer
    34  		h   http.Header
    35  
    36  		once    sync.Once
    37  		nl      bool
    38  		written bool
    39  	}
    40  
    41  	Proto func(context.Context, net.Conn) error
    42  )
    43  
    44  var (
    45  	//go:embed index.html
    46  	//go:embed manifest.json
    47  	//go:embed static
    48  	static embed.FS
    49  )
    50  
    51  func New(a Agent) (*Server, error) {
    52  	return &Server{
    53  		Agent: a,
    54  		FS:    http.FS(static),
    55  	}, nil
    56  }
    57  
    58  func (s *Server) Serve(ctx context.Context, l net.Listener, proto Proto) (err error) {
    59  	var wg sync.WaitGroup
    60  	defer wg.Wait()
    61  
    62  	ctx, cancel := context.WithCancel(ctx)
    63  	defer cancel()
    64  
    65  	for {
    66  		c, err := hnet.Accept(ctx, l)
    67  		if err != nil {
    68  			return err
    69  		}
    70  
    71  		wg.Add(1)
    72  
    73  		go func() {
    74  			defer wg.Done()
    75  
    76  			_ = proto(ctx, c)
    77  		}()
    78  	}
    79  }
    80  
    81  func (s *Server) HandleConn(ctx context.Context, c net.Conn) (err error) {
    82  	defer func() {
    83  		e := c.Close()
    84  		if err == nil {
    85  			err = errors.Wrap(e, "close conn")
    86  		}
    87  	}()
    88  
    89  	c = hnet.NewStoppableConn(ctx, c)
    90  
    91  	br := bufio.NewReader(c)
    92  
    93  	req, err := http.ReadRequest(br)
    94  	if errors.Is(err, io.EOF) {
    95  		return nil
    96  	}
    97  	if err != nil {
    98  		return errors.Wrap(err, "read request")
    99  	}
   100  
   101  	ctx, cancel := context.WithCancel(ctx)
   102  
   103  	go func() {
   104  		defer cancel()
   105  
   106  		_, _ = io.Copy(io.Discard, c)
   107  	}()
   108  
   109  	resp := &response{
   110  		req: req,
   111  		w:   c,
   112  	}
   113  
   114  	defer func() {
   115  		if resp.written {
   116  			return
   117  		}
   118  
   119  		if err == nil {
   120  			resp.WriteHeader(http.StatusNotFound)
   121  			return
   122  		}
   123  
   124  		resp.WriteHeader(http.StatusInternalServerError)
   125  		_, _ = fmt.Fprintf(resp, "%v\n", err)
   126  	}()
   127  
   128  	err = s.HandleRequest(ctx, resp, req)
   129  	if err != nil {
   130  		return err
   131  	}
   132  
   133  	return nil // TODO: handle Content-Length
   134  }
   135  
   136  func (s *Server) HandleRequest(ctx context.Context, rw http.ResponseWriter, req *http.Request) (err error) {
   137  	tr := tlog.SpanFromContext(ctx)
   138  	p := req.URL.Path
   139  
   140  	tr.Printw("request", "method", req.Method, "url", req.URL)
   141  
   142  	switch {
   143  	case strings.HasPrefix(p, "/v0/events"):
   144  		var qdata []byte
   145  		qdata, err = io.ReadAll(req.Body)
   146  		if err != nil {
   147  			return errors.Wrap(err, "read query")
   148  		}
   149  
   150  		var w io.Writer = rw
   151  
   152  		switch ext := pathExt(p); ext {
   153  		case ".tl", ".tlog":
   154  		case ".tlz":
   155  			w = tlz.NewEncoder(w, tlz.MiB)
   156  		case ".json":
   157  			w = convert.NewJSON(w)
   158  		case ".logfmt":
   159  			w = convert.NewLogfmt(w)
   160  		case ".html":
   161  			ww := convert.NewWeb(w)
   162  			defer closeWrap(ww, "close Web", &err)
   163  
   164  			w = ww
   165  		default:
   166  			return errors.New("unsupported ext: %v", ext)
   167  		}
   168  
   169  		err = s.Agent.Query(ctx, w, string(qdata))
   170  		if errors.Is(err, context.Canceled) {
   171  			err = nil
   172  		}
   173  
   174  		return errors.Wrap(err, "process query")
   175  	}
   176  
   177  	http.FileServer(s.FS).ServeHTTP(rw, req)
   178  
   179  	return nil
   180  }
   181  
   182  func (r *response) WriteHeader(code int) {
   183  	r.once.Do(func() {
   184  		fmt.Fprintf(r.w, "HTTP/%d.%d %03d %s\r\n", 1, 0, code, http.StatusText(code))
   185  
   186  		for k, v := range r.h {
   187  			fmt.Fprintf(r.w, "%s:", k)
   188  
   189  			for _, v := range v {
   190  				fmt.Fprintf(r.w, " %s", v)
   191  			}
   192  
   193  			fmt.Fprintf(r.w, "\r\n")
   194  		}
   195  
   196  		fmt.Fprintf(r.w, "\r\n")
   197  	})
   198  }
   199  
   200  func (r *response) Header() http.Header {
   201  	if r.h == nil {
   202  		r.h = make(http.Header)
   203  	}
   204  
   205  	return r.h
   206  }
   207  
   208  func (r *response) Write(p []byte) (n int, err error) {
   209  	r.WriteHeader(http.StatusOK)
   210  
   211  	n, err = r.w.Write(p)
   212  
   213  	if n < len(p) {
   214  		r.nl = p[n] == '\n'
   215  	}
   216  
   217  	return
   218  }
   219  
   220  func closeWrap(c io.Closer, msg string, errp *error) {
   221  	e := c.Close()
   222  	if *errp == nil {
   223  		*errp = errors.Wrap(e, msg)
   224  	}
   225  }
   226  
   227  func pathExt(name string) string {
   228  	last := len(name)
   229  
   230  	for i := len(name) - 1; i >= 0; i-- {
   231  		if name[i] == '/' {
   232  			return ""
   233  		}
   234  
   235  		if name[i] != '.' {
   236  			continue
   237  		}
   238  
   239  		switch name[i:last] {
   240  		case ".tl", ".tlog", ".tlz", ".json", ".logfmt", ".html":
   241  			return name[i:]
   242  		default:
   243  			return ""
   244  		}
   245  	}
   246  
   247  	return ""
   248  }