mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-01-06 05:01:10 -05:00
433 lines
12 KiB
Go
433 lines
12 KiB
Go
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
|
|
}
|