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 }