Initial QSfera import
This commit is contained in:
+432
@@ -0,0 +1,432 @@
|
||||
package icapclient
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// the icap request methods.
|
||||
const (
|
||||
MethodOPTIONS = "OPTIONS"
|
||||
MethodRESPMOD = "RESPMOD"
|
||||
MethodREQMOD = "REQMOD"
|
||||
)
|
||||
|
||||
// shared errors.
|
||||
var (
|
||||
// ErrNoContext is used when no context is provided.
|
||||
ErrNoContext = errors.New("no context provided")
|
||||
|
||||
// ErrInvalidScheme is used when the url scheme is not icap://.
|
||||
ErrInvalidScheme = errors.New("the url scheme must be icap://")
|
||||
|
||||
// ErrMethodNotAllowed is used when the method is not allowed.
|
||||
ErrMethodNotAllowed = errors.New("the requested method is not registered")
|
||||
|
||||
// ErrInvalidHost is used when the host is invalid.
|
||||
ErrInvalidHost = errors.New("the requested host is invalid")
|
||||
|
||||
// ErrInvalidMsg is used when the tcp message is invalid.
|
||||
ErrInvalidMsg = errors.New("invalid message")
|
||||
|
||||
// ErrInvalidConnection is used when the connection is invalid.
|
||||
ErrInvalidConnection = errors.New("invalid connection")
|
||||
|
||||
// ErrREQMODWithoutReq is used when the request is nil for REQMOD method.
|
||||
ErrREQMODWithoutReq = errors.New("http request cannot be nil for method REQMOD")
|
||||
|
||||
// ErrREQMODWithResp is used when the response is not nil for REQMOD method.
|
||||
ErrREQMODWithResp = errors.New("http response must be nil for method REQMOD")
|
||||
|
||||
// ErrRESPMODWithoutResp is used when the response is nil for RESPMOD method.
|
||||
ErrRESPMODWithoutResp = errors.New("http response cannot be nil for method RESPMOD")
|
||||
)
|
||||
|
||||
// general constants required for the package.
|
||||
const (
|
||||
schemeICAP = "icap"
|
||||
icapVersion = "ICAP/1.0"
|
||||
httpVersion = "HTTP/1.1"
|
||||
schemeHTTPReq = "http_request"
|
||||
schemeHTTPResp = "http_response"
|
||||
crlf = "\r\n"
|
||||
doubleCRLF = crlf + crlf
|
||||
lf = "\n"
|
||||
bodyEndIndicator = crlf + "0" + crlf
|
||||
fullBodyEndIndicatorPreviewMode = "; ieof" + doubleCRLF
|
||||
icap100ContinueMsg = "ICAP/1.0 100 Continue" + doubleCRLF
|
||||
icap204NoModsMsg = "ICAP/1.0 204 Unmodified"
|
||||
)
|
||||
|
||||
// Common ICAP headers.
|
||||
const (
|
||||
previewHeader = "Preview"
|
||||
encapsulatedHeader = "Encapsulated"
|
||||
)
|
||||
|
||||
// Conn represents the connection to the icap server.
|
||||
type Conn interface {
|
||||
io.Closer
|
||||
Connect(ctx context.Context, address string) error
|
||||
Send(in []byte) ([]byte, error)
|
||||
}
|
||||
|
||||
// Response represents the icap server response data.
|
||||
type Response struct {
|
||||
StatusCode int
|
||||
Status string
|
||||
PreviewBytes int
|
||||
Header http.Header
|
||||
ContentRequest *http.Request
|
||||
ContentResponse *http.Response
|
||||
}
|
||||
|
||||
// getStatusWithCode prepares the status code and status text from two given strings.
|
||||
func getStatusWithCode(str1, str2 string) (int, string, error) {
|
||||
statusCode, err := strconv.Atoi(str1)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
status := strings.TrimSpace(str2)
|
||||
|
||||
return statusCode, status, nil
|
||||
}
|
||||
|
||||
// getHeaderValue parses the header and its value from a tcp message string.
|
||||
func getHeaderValue(str string) (string, string) {
|
||||
headerValues := strings.SplitN(str, ":", 2)
|
||||
header := headerValues[0]
|
||||
|
||||
if len(headerValues) >= 2 {
|
||||
return header, strings.TrimSpace(headerValues[1])
|
||||
}
|
||||
|
||||
return header, ""
|
||||
}
|
||||
|
||||
// isRequestLine determines if the tcp message string is a request line, i.e., the first line of the message or not.
|
||||
func isRequestLine(str string) bool {
|
||||
return strings.Contains(str, icapVersion) || strings.Contains(str, httpVersion)
|
||||
}
|
||||
|
||||
// setEncapsulatedHeaderValue generates the Encapsulated values and assigns to the ICAP request string.
|
||||
func setEncapsulatedHeaderValue(icapReqStr, httpReqStr, httpRespStr string) string {
|
||||
encVal := " "
|
||||
|
||||
if strings.HasPrefix(icapReqStr, MethodOPTIONS) {
|
||||
switch {
|
||||
// the most common case for OPTIONS method, no Encapsulated body
|
||||
case httpReqStr == "" && httpRespStr == "":
|
||||
encVal += "null-body=0"
|
||||
// if there is an Encapsulated body
|
||||
default:
|
||||
encVal += "opt-body=0"
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(icapReqStr, MethodREQMOD) || strings.HasPrefix(icapReqStr, MethodRESPMOD) {
|
||||
// looking for the match of the string \r\n\r\n,
|
||||
// as that is the expression that separates each block, i.e., headers and bodies
|
||||
re := regexp.MustCompile(doubleCRLF)
|
||||
|
||||
// getting the offsets of the matches, tells us the starting/ending point of headers and bodies
|
||||
reqIndices := re.FindAllStringIndex(httpReqStr, -1)
|
||||
|
||||
// is needed to calculate the response headers by adding the last offset of the request block
|
||||
reqEndsAt := 0
|
||||
|
||||
if reqIndices != nil {
|
||||
encVal += "req-hdr=0"
|
||||
reqEndsAt = reqIndices[0][1]
|
||||
|
||||
switch {
|
||||
// indicating there is a body present for the request block, as length would have been 1 for a single match of \r\n\r\n
|
||||
case len(reqIndices) > 1:
|
||||
encVal += fmt.Sprintf(", req-body=%d", reqIndices[0][1]) // assigning the starting point of the body
|
||||
reqEndsAt = reqIndices[1][1]
|
||||
case httpRespStr == "":
|
||||
encVal += fmt.Sprintf(", null-body=%d", reqIndices[0][1])
|
||||
}
|
||||
|
||||
if httpRespStr != "" {
|
||||
encVal += ", "
|
||||
}
|
||||
}
|
||||
|
||||
respIndices := re.FindAllStringIndex(httpRespStr, -1)
|
||||
|
||||
if respIndices != nil {
|
||||
encVal += fmt.Sprintf("res-hdr=%d", reqEndsAt)
|
||||
|
||||
switch {
|
||||
case len(respIndices) > 1:
|
||||
encVal += fmt.Sprintf(", res-body=%d", reqEndsAt+respIndices[0][1])
|
||||
default:
|
||||
encVal += fmt.Sprintf(", null-body=%d", reqEndsAt+respIndices[0][1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// formatting the ICAP request Encapsulated header with the value
|
||||
return fmt.Sprintf(icapReqStr, encVal)
|
||||
}
|
||||
|
||||
// replaceRequestURIWithActualURL replaces just the escaped portion of the url with the entire URL in the dumped request message.
|
||||
func replaceRequestURIWithActualURL(str, uri, url string) string {
|
||||
if uri == "" {
|
||||
uri = "/"
|
||||
}
|
||||
|
||||
return strings.Replace(str, uri, url, 1)
|
||||
}
|
||||
|
||||
// addFullBodyInPreviewIndicator adds 0; ieof\r\n\r\n which indicates the entire body fitted in the preview.
|
||||
func addFullBodyInPreviewIndicator(str string) string {
|
||||
return strings.TrimSuffix(str, doubleCRLF) + fullBodyEndIndicatorPreviewMode
|
||||
}
|
||||
|
||||
// splitBodyAndHeader separates header and body from a http message.
|
||||
func splitBodyAndHeader(str string) (string, string, bool) {
|
||||
ss := strings.SplitN(str, doubleCRLF, 2)
|
||||
|
||||
if len(ss) < 2 || ss[1] == "" {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
headerStr := ss[0]
|
||||
bodyStr := ss[1]
|
||||
|
||||
return headerStr, bodyStr, true
|
||||
}
|
||||
|
||||
// bodyIsChunked determines if the http body is already chunked from the origin server or not.
|
||||
func bodyIsChunked(str string) bool {
|
||||
_, bodyStr, ok := splitBodyAndHeader(str)
|
||||
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return regexp.MustCompile(`\r\n0(\r\n)+$`).MatchString(bodyStr)
|
||||
}
|
||||
|
||||
// parsePreviewBodyBytes parses the preview portion of the body and only keeps that in the message.
|
||||
func parsePreviewBodyBytes(str string, pb int) string {
|
||||
headerStr, bodyStr, ok := splitBodyAndHeader(str)
|
||||
if !ok {
|
||||
return str
|
||||
}
|
||||
|
||||
return headerStr + doubleCRLF + bodyStr[:pb]
|
||||
}
|
||||
|
||||
// 0.
|
||||
func addHexBodyByteNotations(str string) string {
|
||||
return fmt.Sprintf("%x%s%s%s", len([]byte(str)), crlf, str, bodyEndIndicator)
|
||||
}
|
||||
|
||||
// addHeaderAndBody merges the header and body of the http message.
|
||||
func addHeaderAndBody(headerStr, bodyStr string) string {
|
||||
return headerStr + doubleCRLF + bodyStr
|
||||
}
|
||||
|
||||
// toICAPRequest returns the given request in its ICAP/1.x wire.
|
||||
func toICAPRequest(req Request) ([]byte, error) {
|
||||
// Making the ICAP message block
|
||||
reqStr := fmt.Sprintf("%s %s %s%s", req.Method, req.URL.String(), icapVersion, crlf)
|
||||
|
||||
for headerName, values := range req.Header {
|
||||
for _, value := range values {
|
||||
reqStr += fmt.Sprintf("%s: %s%s", headerName, value, crlf)
|
||||
}
|
||||
}
|
||||
|
||||
// will populate the Encapsulated header value after making the http Request & Response messages
|
||||
reqStr += "Encapsulated: %s" + crlf
|
||||
reqStr += crlf
|
||||
|
||||
// build the HTTP Request message block
|
||||
httpReqStr := ""
|
||||
if req.HTTPRequest != nil {
|
||||
b, err := httputil.DumpRequestOut(req.HTTPRequest, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpReqStr += string(b)
|
||||
httpReqStr = replaceRequestURIWithActualURL(httpReqStr, req.HTTPRequest.URL.EscapedPath(), req.HTTPRequest.URL.String())
|
||||
|
||||
if req.Method == MethodREQMOD {
|
||||
if req.previewSet {
|
||||
httpReqStr = parsePreviewBodyBytes(httpReqStr, req.PreviewBytes)
|
||||
}
|
||||
|
||||
if !bodyIsChunked(httpReqStr) {
|
||||
headerStr, bodyStr, ok := splitBodyAndHeader(httpReqStr)
|
||||
if ok {
|
||||
bodyStr = addHexBodyByteNotations(bodyStr)
|
||||
httpReqStr = addHeaderAndBody(headerStr, bodyStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if the HTTP Request message block doesn't end with a \r\n\r\n,
|
||||
// then going to add one by force for better calculation of byte offsets
|
||||
if httpReqStr != "" {
|
||||
for !strings.HasSuffix(httpReqStr, doubleCRLF) {
|
||||
httpReqStr += crlf
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// build the HTTP Response message block
|
||||
httpRespStr := ""
|
||||
if req.HTTPResponse != nil {
|
||||
b, err := httputil.DumpResponse(req.HTTPResponse, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpRespStr += string(b)
|
||||
|
||||
if req.previewSet {
|
||||
httpRespStr = parsePreviewBodyBytes(httpRespStr, req.PreviewBytes)
|
||||
}
|
||||
|
||||
if !bodyIsChunked(httpRespStr) {
|
||||
headerStr, bodyStr, ok := splitBodyAndHeader(httpRespStr)
|
||||
if ok {
|
||||
bodyStr = addHexBodyByteNotations(bodyStr)
|
||||
httpRespStr = addHeaderAndBody(headerStr, bodyStr)
|
||||
}
|
||||
}
|
||||
|
||||
if httpRespStr != "" && !strings.HasSuffix(httpRespStr, doubleCRLF) { // if the HTTP Response message block doesn't end with a \r\n\r\n, then going to add one by force for better calculation of byte offsets
|
||||
httpRespStr += crlf
|
||||
}
|
||||
}
|
||||
|
||||
if encVal := req.Header.Get(encapsulatedHeader); encVal != "" {
|
||||
reqStr = fmt.Sprintf(reqStr, encVal)
|
||||
} else {
|
||||
// populating the Encapsulated header of the ICAP message portion
|
||||
reqStr = setEncapsulatedHeaderValue(reqStr, httpReqStr, httpRespStr)
|
||||
}
|
||||
|
||||
// determining if the http message needs the full body fitted in the preview portion indicator or not
|
||||
if httpRespStr != "" && req.previewSet && req.bodyFittedInPreview {
|
||||
httpRespStr = addFullBodyInPreviewIndicator(httpRespStr)
|
||||
}
|
||||
|
||||
if req.Method == MethodREQMOD && req.previewSet && req.bodyFittedInPreview {
|
||||
httpReqStr = addFullBodyInPreviewIndicator(httpReqStr)
|
||||
}
|
||||
|
||||
data := []byte(reqStr + httpReqStr + httpRespStr)
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// toClientResponse reads an ICAP message and returns a Response.
|
||||
func toClientResponse(b *bufio.Reader) (Response, error) {
|
||||
resp := Response{
|
||||
Header: make(map[string][]string),
|
||||
}
|
||||
|
||||
scheme := ""
|
||||
httpMsg := ""
|
||||
for currentMsg, err := b.ReadString('\n'); err == nil || currentMsg != ""; currentMsg, err = b.ReadString('\n') { // keep reading the buffer message which is the http response message
|
||||
// if the current message line if the first line of the message portion(request line)
|
||||
if isRequestLine(currentMsg) {
|
||||
ss := strings.Split(currentMsg, " ")
|
||||
|
||||
// must contain 3 words, for example, "ICAP/1.0 200 OK" or "GET /something HTTP/1.1"
|
||||
if len(ss) < 3 {
|
||||
return Response{}, fmt.Errorf("%w: %s", ErrInvalidMsg, currentMsg)
|
||||
}
|
||||
|
||||
// preparing the scheme below
|
||||
if ss[0] == icapVersion {
|
||||
scheme = schemeICAP
|
||||
|
||||
resp.StatusCode, resp.Status, err = getStatusWithCode(ss[1], strings.Join(ss[2:], " "))
|
||||
if err != nil {
|
||||
return Response{}, err
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if ss[0] == httpVersion {
|
||||
scheme = schemeHTTPResp
|
||||
httpMsg = ""
|
||||
}
|
||||
|
||||
// http request message scheme version should always be at the end,
|
||||
// for example, GET /something HTTP/1.1
|
||||
if strings.TrimSpace(ss[2]) == httpVersion {
|
||||
scheme = schemeHTTPReq
|
||||
httpMsg = ""
|
||||
}
|
||||
}
|
||||
|
||||
// preparing the header for ICAP & contents for the HTTP messages below
|
||||
if scheme == schemeICAP {
|
||||
// ignore the CRLF and the LF, shouldn't be counted
|
||||
if currentMsg == lf || currentMsg == crlf {
|
||||
continue
|
||||
}
|
||||
|
||||
header, val := getHeaderValue(currentMsg)
|
||||
if header == previewHeader {
|
||||
pb, _ := strconv.Atoi(val)
|
||||
resp.PreviewBytes = pb
|
||||
}
|
||||
|
||||
resp.Header.Add(header, val)
|
||||
}
|
||||
|
||||
if scheme == schemeHTTPReq {
|
||||
httpMsg += strings.TrimSpace(currentMsg) + crlf
|
||||
bufferEmpty := b.Buffered() == 0
|
||||
|
||||
// a crlf indicates the end of the HTTP message,
|
||||
// and the buffer check is just in case the buffer ended with one last message instead of a crlf
|
||||
if currentMsg == crlf || bufferEmpty {
|
||||
request, err := http.ReadRequest(bufio.NewReader(strings.NewReader(httpMsg)))
|
||||
if err != nil {
|
||||
return Response{}, err
|
||||
}
|
||||
resp.ContentRequest = request
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if scheme == schemeHTTPResp {
|
||||
httpMsg += strings.TrimSpace(currentMsg) + crlf
|
||||
bufferEmpty := b.Buffered() == 0
|
||||
|
||||
if currentMsg == crlf || bufferEmpty {
|
||||
response, err := http.ReadResponse(bufio.NewReader(strings.NewReader(httpMsg)), resp.ContentRequest)
|
||||
if err != nil {
|
||||
return Response{}, err
|
||||
}
|
||||
resp.ContentResponse = response
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
Reference in New Issue
Block a user