package main import ( "bufio" "bytes" "context" "encoding/binary" "fmt" "io" "net" "sync" "time" "dragontcp/internal/wire" ) const ( bhttpModeProbe byte = 0 bhttpModeUpload byte = 1 bhttpModeDownload byte = 2 bhttpModeBatchDownload byte = 3 bhttpModeACK byte = 4 bhttpProbeVersion byte = 1 bhttpRequestHeaderSize = 29 ) var bhttpProbeMagic = [4]byte{'B', 'H', 'P', '1'} var bhttpOpenMagic = [4]byte{'D', 'O', 'P', '1'} var bpCloseMagic = [4]byte{'D', 'C', 'L', '1'} // bhttpSession intentionally models only the transport/session behavior that // is observable in bhttp_remote_test.py. The supplied client test contains no // destination-selection handshake, so uploads are acknowledged and counted but // are not forwarded to an invented target. type bhttpSession struct { mu sync.Mutex lastSeen time.Time uploaded uint64 acked uint64 stream *streamSession } func (s *bhttpSession) touch() { s.mu.Lock() s.lastSeen = time.Now() s.mu.Unlock() } type bhttpSessionManager struct { mu sync.RWMutex sessions map[string]*bhttpSession timeout time.Duration max int } func newBHTTPSessionManager(timeout time.Duration, max int) *bhttpSessionManager { if timeout <= 0 { timeout = 2 * time.Minute } if max < 1 { max = 1 } m := &bhttpSessionManager{ sessions: make(map[string]*bhttpSession), timeout: timeout, max: max, } go m.cleanupLoop() return m } func (m *bhttpSessionManager) get(sid wire.SessionID) *bhttpSession { m.mu.RLock() s := m.sessions[sidKey(sid)] m.mu.RUnlock() if s != nil { s.touch() } return s } func (m *bhttpSessionManager) register(sid wire.SessionID) bool { key := sidKey(sid) m.mu.Lock() if old := m.sessions[key]; old != nil { m.mu.Unlock() old.touch() return true } if len(m.sessions) >= m.max { m.mu.Unlock() return false } m.sessions[key] = &bhttpSession{lastSeen: time.Now()} m.mu.Unlock() return true } func (m *bhttpSessionManager) remove(sid wire.SessionID) bool { key := sidKey(sid) m.mu.Lock() session := m.sessions[key] delete(m.sessions, key) m.mu.Unlock() if session == nil { return false } session.mu.Lock() stream := session.stream session.stream = nil session.mu.Unlock() if stream != nil { stream.close() } return true } func (m *bhttpSessionManager) cleanupLoop() { ticker := time.NewTicker(30 * time.Second) defer ticker.Stop() for now := range ticker.C { cutoff := now.Add(-m.timeout) var closing []*streamSession m.mu.Lock() for key, session := range m.sessions { session.mu.Lock() stale := session.lastSeen.Before(cutoff) stream := session.stream session.mu.Unlock() if stale { delete(m.sessions, key) if stream != nil { closing = append(closing, stream) } } } m.mu.Unlock() for _, stream := range closing { stream.close() } } } type bhttpRequest struct { mode byte session wire.SessionID seq uint64 value uint32 payload []byte headerMask byte clear bool } type binaryHeader struct { mode byte session wire.SessionID seq uint64 length uint32 } func peekBinaryHeader(r *bufio.Reader, headerMask byte) (binaryHeader, error) { var out binaryHeader header, err := r.Peek(bhttpRequestHeaderSize) if err != nil { return out, err } out.mode = header[0] ^ headerMask copy(out.session[:], header[1:17]) out.seq = binary.BigEndian.Uint64(header[17:25]) out.length = binary.BigEndian.Uint32(header[25:29]) return out, nil } func readBHTTPRequest(r *bufio.Reader, headerMask byte, clear bool) (bhttpRequest, error) { var req bhttpRequest var header [bhttpRequestHeaderSize]byte if _, err := io.ReadFull(r, header[:]); err != nil { return req, err } req.mode = header[0] ^ headerMask req.headerMask = headerMask req.clear = clear if req.mode > bhttpModeACK { return req, fmt.Errorf("unknown BP mode") } copy(req.session[:], header[1:17]) req.seq = binary.BigEndian.Uint64(header[17:25]) req.value = binary.BigEndian.Uint32(header[25:29]) // BHTTP mode 2 overloads the normal body-length field as a download-size // hint and sends no payload bytes after the 29-byte header. if req.mode == bhttpModeDownload { return req, nil } if req.value > wire.MaxPayload { return req, fmt.Errorf("BP payload too large") } if req.value > 0 { req.payload = make([]byte, int(req.value)) if _, err := io.ReadFull(r, req.payload); err != nil { return req, err } if !clear { wire.MaskInPlace(req.payload, req.session, req.mode, req.seq, false) } } return req, nil } func parseBHTTPProbe(payload []byte) (byte, int, error) { if len(payload) < 10 || !bytes.Equal(payload[:4], bhttpProbeMagic[:]) || payload[4] != bhttpProbeVersion { return 0, 0, fmt.Errorf("bad BP probe") } submode := payload[5] if submode > bhttpModeACK { return 0, 0, fmt.Errorf("unknown BP probe submode") } param := int(binary.BigEndian.Uint32(payload[6:10])) want := 10 if submode == bhttpModeUpload && param >= 10 { want = param } if len(payload) != want { return 0, 0, fmt.Errorf("bad BP probe length") } for i := 10; i < len(payload); i++ { if payload[i] != byte(i*31) { return 0, 0, fmt.Errorf("bad BP probe pattern") } } return submode, param, nil } func makeBHTTPProbe(submode byte, param int) []byte { total := 10 if submode == bhttpModeDownload && param > total { total = param } out := make([]byte, total) copy(out[:4], bhttpProbeMagic[:]) out[4] = bhttpProbeVersion out[5] = submode binary.BigEndian.PutUint32(out[6:10], uint32(param)) for i := 10; i < len(out); i++ { out[i] = byte(i * 31) } return out } func writeBHTTPError(conn net.Conn, message string) error { return wire.WriteResponse(conn, wire.StatusError, []byte(message)) } func writeBHTTPMasked(conn net.Conn, status byte, body []byte, req bhttpRequest) error { return wire.WriteMaskedResponseProfileEncoding(conn, status, body, req.session, req.mode, req.seq, req.headerMask, req.clear) } func writeBHTTPData(conn net.Conn, req bhttpRequest, data []byte) error { // Build and mask the complete response once. The generic two-step path // first built a BP body and then copied it into another framed packet, // temporarily allocating roughly twice the download size. if req.clear { var header [wire.ResponseHeaderSize]byte header[0] = wire.StatusData ^ req.headerMask binary.BigEndian.PutUint32(header[1:5], uint32(4+len(data))) var length [4]byte binary.BigEndian.PutUint32(length[:], uint32(len(data))) buffers := net.Buffers{header[:], length[:], data} _, err := buffers.WriteTo(conn) return err } packet := make([]byte, wire.ResponseHeaderSize+4+len(data)) packet[0] = wire.StatusData ^ req.headerMask binary.BigEndian.PutUint32(packet[1:5], uint32(4+len(data))) binary.BigEndian.PutUint32(packet[5:9], uint32(len(data))) copy(packet[9:], data) wire.MaskInPlace(packet[5:], req.session, req.mode, req.seq, true) for len(packet) > 0 { n, err := conn.Write(packet) if err != nil { return err } if n <= 0 { return io.ErrShortWrite } packet = packet[n:] } return nil } type bhttpServerContext struct { sessions *bhttpSessionManager token string allowPrivate bool cache *dnsCache tcpBuffer int maxChunk int maxBuffer int pollWait time.Duration debug *serverDebug } func processBHTTPRequest(conn net.Conn, req bhttpRequest, ctx *bhttpServerContext) error { sessions := ctx.sessions maxChunk := ctx.maxChunk switch req.mode { case bhttpModeProbe: submode, param, err := parseBHTTPProbe(req.payload) if err != nil { return writeBHTTPError(conn, err.Error()) } if submode == bhttpModeUpload && len(req.payload) > maxChunk { return writeBHTTPError(conn, "probe too large") } if submode == bhttpModeDownload && (param < 0 || param > maxChunk) { return writeBHTTPError(conn, "probe too large") } count := 1 if submode == bhttpModeACK { count = param if count < 1 { count = 1 } if count > 256 { count = 256 } } body := makeBHTTPProbe(submode, param) for i := 0; i < count; i++ { // The reference client decrypts every batch echo with the original // request sequence, rather than incrementing it per response. if err := writeBHTTPMasked(conn, wire.StatusOK, body, req); err != nil { return err } } return nil case bhttpModeUpload: if req.seq == 0 && len(req.payload) == 0 { if !sessions.register(req.session) { return writeBHTTPError(conn, "session limit reached") } return wire.WriteResponse(conn, wire.StatusOK, nil) } session := sessions.get(req.session) if session == nil { return writeBHTTPError(conn, "unknown session") } if len(req.payload) > maxChunk { return writeBHTTPError(conn, "upload too large") } if req.seq == 1 && len(req.payload) >= len(bhttpOpenMagic) && bytes.Equal(req.payload[:len(bhttpOpenMagic)], bhttpOpenMagic[:]) { supplied, host, port, err := parseOpen(req.payload[len(bhttpOpenMagic):]) if err != nil { return writeBHTTPError(conn, err.Error()) } if !tokenEqual(supplied, ctx.token) { return writeBHTTPError(conn, "authentication failed") } session.mu.Lock() alreadyOpen := session.stream != nil session.mu.Unlock() if alreadyOpen { return wire.WriteResponse(conn, wire.StatusOK, nil) } dialCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) target, err := dialTarget(dialCtx, host, port, ctx.allowPrivate, ctx.cache, ctx.tcpBuffer) cancel() if err != nil { return writeBHTTPError(conn, err.Error()) } stream := newStreamSession(req.session, target, fmt.Sprintf("%s:%d", host, port), maxChunk, ctx.maxBuffer, ctx.debug) session.mu.Lock() if session.stream == nil { session.stream = stream session.lastSeen = time.Now() stream = nil } session.mu.Unlock() if stream != nil { stream.close() } if ctx.debug != nil && ctx.debug.enabled { ctx.debug.logf("BP OPEN sid=%x target=%s:%d", req.session[:4], host, port) } return wire.WriteResponse(conn, wire.StatusOK, nil) } session.mu.Lock() stream := session.stream session.mu.Unlock() if stream != nil { if req.seq < 2 { return writeBHTTPError(conn, "bad upload sequence") } if err := stream.upload(req.seq-2, req.payload); err != nil { return writeBHTTPError(conn, err.Error()) } } session.mu.Lock() session.uploaded += uint64(len(req.payload)) session.lastSeen = time.Now() session.mu.Unlock() return wire.WriteResponse(conn, wire.StatusOK, nil) case bhttpModeDownload: session := sessions.get(req.session) if session == nil { return writeBHTTPError(conn, "unknown session") } session.mu.Lock() stream := session.stream session.mu.Unlock() if stream == nil { // The reference transport has no observable downstream producer. return wire.WriteResponse(conn, wire.StatusOK, nil) } limit := int(req.value) if limit < 1 { limit = 1 } if limit > maxChunk { limit = maxChunk } data, status, err := stream.readAt(req.seq, limit, ctx.pollWait) if err != nil { return writeBHTTPError(conn, err.Error()) } switch status { case wire.StatusData: return writeBHTTPData(conn, req, data) case wire.StatusEOF: return wire.WriteResponse(conn, wire.StatusEOF, nil) default: return wire.WriteResponse(conn, wire.StatusOK, nil) } case bhttpModeBatchDownload: session := sessions.get(req.session) if session == nil { return writeBHTTPError(conn, "unknown session") } if len(req.payload) != 6 { return writeBHTTPError(conn, "bad batch download request") } count := int(binary.BigEndian.Uint16(req.payload[4:6])) limit := int(binary.BigEndian.Uint32(req.payload[:4])) if limit < 1 { limit = 1 } if limit > maxChunk { limit = maxChunk } if count < 1 { count = 1 } if count > 256 { count = 256 } session.mu.Lock() stream := session.stream session.mu.Unlock() offset := req.seq for i := 0; i < count; i++ { if stream == nil { if err := wire.WriteResponse(conn, wire.StatusOK, nil); err != nil { return err } continue } wait := time.Duration(0) if i == 0 { wait = ctx.pollWait } data, status, err := stream.readAt(offset, limit, wait) if err != nil { return writeBHTTPError(conn, err.Error()) } switch status { case wire.StatusData: if err := writeBHTTPData(conn, req, data); err != nil { return err } offset += uint64(len(data)) case wire.StatusEOF: if err := wire.WriteResponse(conn, wire.StatusEOF, nil); err != nil { return err } default: if err := wire.WriteResponse(conn, wire.StatusOK, nil); err != nil { return err } } } return nil case bhttpModeACK: session := sessions.get(req.session) if session == nil { return writeBHTTPError(conn, "unknown session") } // Dragon's BP extension sends an explicit close marker. Reference BP // clients continue to use an empty ACK, while Dragon clients release the // target socket and buffered download data immediately instead of waiting // for the idle-session reaper. if bytes.Equal(req.payload, bpCloseMagic[:]) { sessions.remove(req.session) return wire.WriteResponse(conn, wire.StatusOK, nil) } session.mu.Lock() if req.seq > session.acked { session.acked = req.seq } session.lastSeen = time.Now() stream := session.stream session.mu.Unlock() if stream != nil { stream.ack(req.seq) } return wire.WriteResponse(conn, wire.StatusOK, nil) } return writeBHTTPError(conn, "unknown mode") } type binaryFlavor byte const ( binaryFlavorUnknown binaryFlavor = iota binaryFlavorDragon binaryFlavorBHTTP ) func isBHTTPProbe(payload []byte) bool { return len(payload) >= 4 && bytes.Equal(payload[:4], bhttpProbeMagic[:]) } // handleBinary auto-detects the two protocols without changing the native B // header space. BHTTP is clear-header only; Dragon profiles and cover-prefaced // connections continue through the existing handler unchanged. func handleBinary( conn net.Conn, headerMask byte, clearPayload bool, token string, allowPrivate bool, cache *dnsCache, tcpBuffer int, manager *streamManager, bhttp *bhttpSessionManager, chunkMax int, bufferBytes int, pollWait time.Duration, debug *serverDebug, ) { reader := bufio.NewReader(conn) bhttpContext := &bhttpServerContext{ sessions: bhttp, token: token, allowPrivate: allowPrivate, cache: cache, tcpBuffer: tcpBuffer, maxChunk: chunkMax, maxBuffer: bufferBytes, pollWait: pollWait, debug: debug, } flavor := binaryFlavorUnknown deadline := newIdleDeadline(conn, 30*time.Second) for { if deadline.refresh() != nil { return } header, err := peekBinaryHeader(reader, headerMask) if err != nil { return } if flavor == binaryFlavorUnknown { switch header.mode { case bhttpModeProbe: // Probe framing is shared, so consume it once and use its magic // to select BHP1 or DTP2 without losing any bytes. req, err := wire.ReadRequestProfileEncoding(reader, headerMask, clearPayload) if err != nil { return } if isBHTTPProbe(req.Payload) { flavor = binaryFlavorBHTTP breq := bhttpRequest{mode: req.Mode, session: req.Session, seq: req.Seq, value: uint32(len(req.Payload)), payload: req.Payload, headerMask: headerMask, clear: clearPayload} if processBHTTPRequest(conn, breq, bhttpContext) != nil { return } continue } flavor = binaryFlavorDragon if processWireRequest(conn, req, token, allowPrivate, cache, tcpBuffer, manager, chunkMax, bufferBytes, pollWait, debug) != nil { return } continue case bhttpModeUpload: if bhttp.get(header.session) != nil || (header.seq == 0 && header.length == 0) { flavor = binaryFlavorBHTTP } else { flavor = binaryFlavorDragon } case bhttpModeDownload: if bhttp.get(header.session) != nil { flavor = binaryFlavorBHTTP } else if manager.get(header.session) != nil { flavor = binaryFlavorDragon } else { // The BHTTP unknown-session test sends only a header whose // length field is a hint. Consume no nonexistent body. if _, err := readBHTTPRequest(reader, headerMask, clearPayload); err == nil { _ = writeBHTTPError(conn, "unknown session") } return } case bhttpModeBatchDownload: if bhttp.get(header.session) != nil || header.length == 6 { flavor = binaryFlavorBHTTP } else { flavor = binaryFlavorDragon } case bhttpModeACK: if bhttp.get(header.session) != nil { flavor = binaryFlavorBHTTP } else { flavor = binaryFlavorDragon } default: return } } if flavor == binaryFlavorBHTTP { req, err := readBHTTPRequest(reader, headerMask, clearPayload) if err != nil || processBHTTPRequest(conn, req, bhttpContext) != nil { return } continue } req, err := wire.ReadRequestProfileEncoding(reader, headerMask, clearPayload) if err != nil { return } if processWireRequest(conn, req, token, allowPrivate, cache, tcpBuffer, manager, chunkMax, bufferBytes, pollWait, debug) != nil { return } } }