package io import ( "bufio" "net" "strings" "time" "git.smesh.lol/torly/pkg/httpsrv" "git.smesh.lol/torly/pkg/wire" "git.smesh.lol/torly/pkg/ws" ) const ( httpReadTimeout = 30 * time.Second wsIdleTimeout = 150 * time.Second ) // Reader is one reader worker domain. It owns the read side and all parsing // state for one connection at a time; the paired Writer owns the write side. func Reader(data ReaderData, ch ReaderChans) { for { select { case <-ch.Stop.Done(): return default: } var a wire.Assign select { case a = <-ch.Assign.Recv(): case <-ch.Stop.Done(): return } if a.Socket == nil { continue } serve(data, ch, a) } } func serve(data ReaderData, ch ReaderChans, a wire.Assign) { conn := a.Socket.Conn if !sendCtl(ch, wire.WriterCtl{Kind: wire.CtlBind, Conn: a.ID, Socket: a.Socket}) { conn.Close() return } if !waitAck(ch, wire.CtlBind) { return } br := bufio.NewReaderSize(conn, 16<<10) maxBody := data.Cfg.BlossomMaxBlob + 4096 if maxBody < 1<<20 { maxBody = 1 << 20 } _ = conn.SetReadDeadline(time.Now().Add(httpReadTimeout)) req, err := httpsrv.ReadRequest(br, conn.RemoteAddr().String(), 32<<10, maxBody, func() error { sendCtl(ch, wire.WriterCtl{Kind: wire.CtlWrite, Payload: []byte("HTTP/1.1 100 Continue\r\n\r\n")}) return nil }) if err != nil || req == nil { sendIn(ch, wire.Inbound{Conn: a.ID, Kind: wire.InClosed, Reason: "http read"}) sendCtl(ch, wire.WriterCtl{Kind: wire.CtlClose, Conn: a.ID}) return } if isWebSocket(req.Header) { resp, herr := ws.HandshakeResponse(req.Header) if herr != nil { body := herr.Error() + "\n" bad := wire.HTTPResponse{ Status: 400, Header: [][2]string{{"Content-Type", "text/plain"}, {"Content-Length", itoa(len(body))}}, Body: []byte(body), } sendCtl(ch, wire.WriterCtl{Kind: wire.CtlRespond, Payload: SerializeHTTP(bad), Close: true}) return } if !sendCtl(ch, wire.WriterCtl{Kind: wire.CtlUpgrade, Conn: a.ID, Payload: resp}) { return } if !waitAck(ch, wire.CtlUpgrade) { return } if !sendIn(ch, wire.Inbound{Conn: a.ID, Kind: wire.InOpened}) { return } readWebSocket(data, ch, a.ID, br, conn) return } if resp, handled := localResponse(data.Cfg, req); handled { sendCtl(ch, wire.WriterCtl{Kind: wire.CtlRespond, Payload: SerializeHTTP(resp), Close: true}) return } // Health and Blossom requests are decided by the network domain. Once the // request is handed over, the reader has no further read work: the writer // sends the response and closes the socket. sendIn(ch, wire.Inbound{ Conn: a.ID, Kind: wire.InHTTP, HTTP: wire.HTTPRequest{ Method: req.Method, Path: req.Path, Query: req.RawQuery, Header: req.Header, Body: req.Body, }, }) } func readWebSocket(data ReaderData, ch ReaderChans, id wire.ConnID, br *bufio.Reader, conn net.Conn) { max := int64(data.Cfg.MaxEventBytes*2 + 8192) if max < 256<<10 { max = 256 << 10 } reader := ws.NewReader(br, max) for { select { case <-ch.Stop.Done(): sendCtl(ch, wire.WriterCtl{Kind: wire.CtlClose, Conn: id}) return default: } _ = conn.SetReadDeadline(time.Now().Add(wsIdleTimeout)) op, payload, err := reader.Read() if err != nil { sendIn(ch, wire.Inbound{Conn: id, Kind: wire.InClosed, Reason: err.Error()}) sendCtl(ch, wire.WriterCtl{Kind: wire.CtlClose, Conn: id}) return } switch op { case ws.OpText: if !sendIn(ch, wire.Inbound{Conn: id, Kind: wire.InText, Text: payload}) { return } case ws.OpPing: sendCtl(ch, wire.WriterCtl{Kind: wire.CtlPong, Conn: id, Payload: payload}) case ws.OpPong: // nothing case ws.OpClose: sendIn(ch, wire.Inbound{Conn: id, Kind: wire.InClosed, Reason: "client close"}) sendCtl(ch, wire.WriterCtl{Kind: wire.CtlClose, Conn: id}) return case ws.OpBinary: sendCtl(ch, wire.WriterCtl{Kind: wire.CtlClose, Conn: id, Payload: closeReason(1003)}) sendIn(ch, wire.Inbound{Conn: id, Kind: wire.InClosed, Reason: "binary frame"}) return } } } func isWebSocket(h map[string]string) bool { for _, part := range strings.Split(h["upgrade"], ",") { if strings.EqualFold(strings.TrimSpace(part), "websocket") { return true } } return false } func closeReason(code uint16) []byte { return ws.AppendClose(nil, code, "") } func sendIn(ch ReaderChans, m wire.Inbound) bool { select { case ch.In.Tx <- m: return true case <-ch.Stop.Done(): return false } } func waitAck(ch ReaderChans, kind uint8) bool { select { case got := <-ch.Ack.Recv(): return got == kind case <-ch.Stop.Done(): return false } } func sendCtl(ch ReaderChans, m wire.WriterCtl) bool { select { case ch.Ctl.Tx <- m: return true case <-ch.Stop.Done(): return false } } func itoa(n int) string { if n == 0 { return "0" } var b [20]byte i := len(b) for n > 0 { i-- b[i] = byte('0' + n%10) n /= 10 } return string(b[i:]) }