package transport import "bytes" // parseHTTPHeaders parses request-line + headers from a header block (no body). func parseHTTPHeaders(data []byte) *httpReq { lineEnd := bytes.IndexByte(data, '\n') if lineEnd < 0 { return nil } line := data[:lineEnd] if len(line) > 0 && line[len(line)-1] == '\r' { line = line[:len(line)-1] } sp1 := bytes.IndexByte(line, ' ') if sp1 < 0 { return nil } method := string(makeCopy(line[:sp1])) rest := line[sp1+1:] sp2 := bytes.IndexByte(rest, ' ') var path string if sp2 >= 0 { path = string(makeCopy(rest[:sp2])) } else { path = string(makeCopy(rest)) } if qi := bytes.IndexByte([]byte(path), '?'); qi >= 0 { path = path[:qi] } headers := map[string]string{} pos := lineEnd + 1 for pos < len(data) { nlPos := bytes.IndexByte(data[pos:], '\n') var hline []byte if nlPos < 0 { hline = data[pos:] pos = len(data) } else { nlPos += pos hline = data[pos:nlPos] pos = nlPos + 1 } if len(hline) > 0 && hline[len(hline)-1] == '\r' { hline = hline[:len(hline)-1] } if len(hline) == 0 { continue } colon := bytes.IndexByte(hline, ':') if colon >= 0 { key := string(toLower(makeCopy(hline[:colon]))) val := string(makeCopy(trimSpace(hline[colon+1:]))) headers[key] = val } } return &httpReq{method: method, path: path, headers: headers} } func parseContentLength(s string) int { if s == "" { return 0 } n := 0 for i := 0; i < len(s); i++ { if s[i] < '0' || s[i] > '9' { return 0 } n = n*10 + int(s[i]-'0') } return n } func buildHTTPResponse(status int, headers map[string]string, body []byte) []byte { statusText := "OK" switch status { case 200: statusText = "OK" case 204: statusText = "No Content" case 206: statusText = "Partial Content" case 301: statusText = "Moved Permanently" case 302: statusText = "Found" case 304: statusText = "Not Modified" case 400: statusText = "Bad Request" case 401: statusText = "Unauthorized" case 403: statusText = "Forbidden" case 404: statusText = "Not Found" case 405: statusText = "Method Not Allowed" case 415: statusText = "Unsupported Media Type" case 429: statusText = "Too Many Requests" case 500: statusText = "Internal Server Error" case 502: statusText = "Bad Gateway" case 503: statusText = "Service Unavailable" case 504: statusText = "Gateway Timeout" } var buf []byte buf = append(buf, "HTTP/1.1 "...) buf = appendInt(buf, status) buf = append(buf, ' ') buf = append(buf, statusText...) buf = append(buf, "\r\n"...) hasCL := false for k, v := range headers { if k == "Content-Length" { hasCL = true } buf = append(buf, k...) buf = append(buf, ": "...) buf = append(buf, v...) buf = append(buf, "\r\n"...) } if !hasCL { buf = append(buf, "Content-Length: "...) buf = appendInt(buf, len(body)) buf = append(buf, "\r\n"...) } buf = append(buf, "Connection: keep-alive\r\n"...) buf = append(buf, "\r\n"...) buf = append(buf, body...) return buf } func writeHTTPResponse(fd int, status int, headers map[string]string, body []byte) bool { return writeAll(fd, buildHTTPResponse(status, headers, body)) == nil } // DecodeHeaders deserializes null-separated key\0val\0 pairs to a map. // Used by static worker responses. func DecodeHeaders(b []byte) map[string]string { h := map[string]string{} for len(b) > 0 { ki := 0 for ki < len(b) && b[ki] != 0 { ki++ } if ki >= len(b) { break } k := string(b[:ki]) b = b[ki+1:] vi := 0 for vi < len(b) && b[vi] != 0 { vi++ } v := string(b[:vi]) if vi < len(b) { b = b[vi+1:] } else { b = b[vi:] } h[k] = v } return h } func appendInt(buf []byte, n int) []byte { if n == 0 { return append(buf, '0') } if n < 0 { buf = append(buf, '-') n = -n } var digits [20]byte i := len(digits) for n > 0 { i-- digits[i] = byte('0' + n%10) n /= 10 } return append(buf, digits[i:]...) } // AppendInt is the exported version used by server for NIP-11 JSON construction. func AppendInt(buf []byte, n int) []byte { return appendInt(buf, n) } func AppendStr(buf []byte, s string) []byte { return append(buf, s...) } func AppendJSONString(buf []byte, s string) []byte { buf = append(buf, '"') for i := 0; i < len(s); i++ { c := s[i] switch c { case '"': buf = append(buf, '\\', '"') case '\\': buf = append(buf, '\\', '\\') case '\n': buf = append(buf, '\\', 'n') case '\r': buf = append(buf, '\\', 'r') case '\t': buf = append(buf, '\\', 't') default: if c < 0x20 { buf = append(buf, '\\', 'u', '0', '0', hexNibble(c>>4), hexNibble(c&0x0f)) } else { buf = append(buf, c) } } } buf = append(buf, '"') return buf } func hexNibble(b byte) byte { if b < 10 { return '0' + b } return 'a' + (b - 10) } // FirstXFF returns the leftmost IP from an X-Forwarded-For header. func FirstXFF(xff string) string { for i := 0; i < len(xff); i++ { if xff[i] == ',' { return string(makeCopy(trimSpace([]byte(xff[:i])))) } } return string(makeCopy(trimSpace([]byte(xff)))) } // HasPrefix is exported for use by server. func HasPrefix(s, prefix string) bool { return len(s) >= len(prefix) && s[:len(prefix)] == prefix } func hasPrefix(s, prefix string) bool { return len(s) >= len(prefix) && s[:len(prefix)] == prefix } var botAgents = []string{ "SemrushBot", "AhrefsBot", "GPTBot", "ClaudeBot", "MJ12bot", "DotBot", "PetalBot", "Bytespider", "Sogou", "YandexBot", "BLEXBot", "DataForSeoBot", } func isBot(ua string) bool { if ua == "" { return false } for _, bot := range botAgents { if bytes.Contains([]byte(ua), []byte(bot)) { return true } } return false } func makeCopy(b []byte) []byte { c := []byte{:len(b)} copy(c, b) return c } func trimSpace(b []byte) []byte { for len(b) > 0 && (b[0] == ' ' || b[0] == '\t') { b = b[1:] } for len(b) > 0 && (b[len(b)-1] == ' ' || b[len(b)-1] == '\t') { b = b[:len(b)-1] } return b } func toLower(b []byte) []byte { for i := range b { if b[i] >= 'A' && b[i] <= 'Z' { b[i] = b[i] + 32 } } return b } // ParseAddr splits "host:port" into ([4]byte IP, int port). func ParseAddr(addr string) ([4]byte, int) { ab := []byte(addr) var ip [4]byte colon := -1 for i := len(ab) - 1; i >= 0; i-- { if ab[i] == ':' { colon = i break } } if colon < 0 { return ip, 0 } port := 0 for i := colon + 1; i < len(ab); i++ { port = port*10 + int(ab[i]-'0') } host := ab[:colon] if len(host) > 0 { octet := 0 idx := 0 for i := 0; i < len(host); i++ { if host[i] == '.' { if idx < 4 { ip[idx] = byte(octet) } idx++ octet = 0 } else { octet = octet*10 + int(host[i]-'0') } } if idx < 4 { ip[idx] = byte(octet) } } return ip, port }