/* Package proto provides byte-level interaction with HTTP request payload. Example of HTTP payload for future references, new line symbols escaped: POST /upload HTTP/1.1\r\n User-Agent: Gor\r\n Content-Length: 11\r\n \r\n Hello world GET /index.html HTTP/1.1\r\n User-Agent: Gor\r\n \r\n \r\n */ package proto import ( "bytes" "github.com/buger/gor/byteutils" ) // In HTTP newline defined by 2 bytes (for both windows and *nix support) var CLRF = []byte("\r\n") // New line acts as separator: end of Headers or Body (in some cases) var EmptyLine = []byte("\r\n\r\n") // Separator for Header line. Header looks like: `HeaderName: value` var HeaderDelim = []byte(": ") // MIMEHeadersEndPos finds end of the Headers section, which should end with empty line. func MIMEHeadersEndPos(payload []byte) int { return bytes.Index(payload, EmptyLine) } // MIMEHeadersStartPos finds start of Headers section // It just finds position of second line (first contains location and method). func MIMEHeadersStartPos(payload []byte) int { return bytes.Index(payload, CLRF) + 2 // Find first line end } // header return value and positions of header/value start/end. // If not found, value will be blank, and headerStart will be -1 // Do not support multi-line headers. func header(payload []byte, name []byte) (value []byte, headerStart, valueStart, headerEnd int) { headerStart = bytes.Index(payload, name) if headerStart == -1 { return } valueStart = headerStart + len(name) + 1 // Skip ":" after header name if payload[valueStart] == ' ' { // Ignore empty space after ':' valueStart++ } headerEnd = valueStart + bytes.IndexByte(payload[valueStart:], '\n') if payload[headerEnd-1] == '\r' { headerEnd -= 1 } value = payload[valueStart:headerEnd] return } // Header returns header value, if header not found, value will be blank func Header(payload, name []byte) []byte { val, _, _, _ := header(payload, name) return val } // SetHeader sets header value. If header not found it creates new one. // Returns modified request payload func SetHeader(payload, name, value []byte) []byte { _, hs, vs, he := header(payload, name) if hs != -1 { // If header found we just repace its value return byteutils.Replace(payload, vs, he, value) } return AddHeader(payload, name, value) } // AddHeader takes http payload and appends new header to the start of headers section // Returns modified request payload func AddHeader(payload, name, value []byte) []byte { header := make([]byte, len(name)+2+len(value)+2) copy(header[0:], name) copy(header[len(name):], HeaderDelim) copy(header[len(name)+2:], value) copy(header[len(header)-2:], CLRF) mimeStart := MIMEHeadersStartPos(payload) return byteutils.Insert(payload, mimeStart, header) } // Body returns request/response body func Body(payload []byte) []byte { // 4 -> len(EMPTY_LINE) return payload[MIMEHeadersEndPos(payload)+4:] } // Path takes payload and retuns request path: Split(firstLine, ' ')[1] func Path(payload []byte) []byte { start := bytes.IndexByte(payload, ' ') + 1 end := bytes.IndexByte(payload[start:], ' ') return payload[start : start+end] } // SetPath takes payload, sets new path and returns modified payload func SetPath(payload, path []byte) []byte { start := bytes.IndexByte(payload, ' ') + 1 end := bytes.IndexByte(payload[start:], ' ') return byteutils.Replace(payload, start, start+end, path) } // PathParam returns URL query attribute by given name, if no found: valueStart will be -1 func PathParam(payload, name []byte) (value []byte, valueStart, valueEnd int) { path := Path(payload) if paramStart := bytes.Index(path, append(name, '=')); paramStart != -1 { valueStart := paramStart + len(name) + 1 paramEnd := bytes.IndexByte(path[valueStart:], '&') // Param can end with '&' (another param), or end of line if paramEnd == -1 { // It is final param paramEnd = len(path) } else { paramEnd += valueStart } return path[valueStart:paramEnd], valueStart, paramEnd } return []byte(""), -1, -1 } // SetPathParam takes payload and updates path Query attribute // If query param not found, it will append new // Returns modified payload func SetPathParam(payload, name, value []byte) []byte { path := Path(payload) _, vs, ve := PathParam(payload, name) if vs != -1 { // If param found, replace its value and set new Path newPath := make([]byte, len(path)) copy(newPath, path) newPath = byteutils.Replace(newPath, vs, ve, value) return SetPath(payload, newPath) } // if param not found append to end of url // Adding 2 because of '?' or '&' at start, and '=' in middle newParam := make([]byte, len(name)+len(value)+2) if bytes.IndexByte(path, '?') == -1 { newParam[0] = '?' } else { newParam[0] = '&' } // Copy "param=value" into buffer, after it looks like "?param=value" copy(newParam[1:], name) newParam[1+len(name)] = '=' copy(newParam[2+len(name):], value) // Append param to the end of path newPath := make([]byte, len(path)+len(newParam)) copy(newPath, path) copy(newPath[len(path):], newParam) return SetPath(payload, newPath) } // SetHost updates Host header for HTTP/1.1 or updates host in path for HTTP/1.0 or Proxy requests // Returns modified payload func SetHost(payload, url, host []byte) []byte { // If this is HTTP 1.0 traffic or proxy traffic it may include host right into path variable, so instead of setting Host header we rewrite Path // Fix for https://github.com/buger/gor/issues/156 if path := Path(payload); bytes.HasPrefix(path, []byte("http")) { hostStart := bytes.IndexByte(path, ':') // : position "https?:" hostStart += 3 // Skip 1 ':' and 2 '\' hostEnd := hostStart + bytes.IndexByte(path[hostStart:], '/') newPath := make([]byte, len(path)) copy(newPath, path) newPath = byteutils.Replace(newPath, 0, hostEnd, url) return SetPath(payload, newPath) } return SetHeader(payload, []byte("Host"), host) } // Method returns HTTP method func Method(payload []byte) []byte { end := bytes.IndexByte(payload, ' ') return payload[:end] } // Status returns response status. // It happend to be in same position as request payload path func Status(payload []byte) []byte { return Path(payload) } var httpMethods []string = []string{ "GET ", "OPTI", "HEAD", "POST", "PUT ", "DELE", "TRAC", "CONN", } func IsHTTPPayload(payload []byte) bool { if len(payload) < 4 { return false } method := string(payload[0:4]) for _, m := range httpMethods { if method == m { return true } } return false }