package icapclient import ( "bytes" "context" "errors" "io" "net/http" "net/url" "os" "slices" "strconv" "strings" ) // Request represents the icap client request data. type Request struct { Method string URL *url.URL Header http.Header HTTPRequest *http.Request HTTPResponse *http.Response ChunkLength int PreviewBytes int ctx context.Context previewSet bool bodyFittedInPreview bool remainingPreviewBytes []byte } // NewRequest returns a new Request given a context, method, url, http request and http response // todo: method iota func NewRequest(ctx context.Context, method, urlStr string, httpReq *http.Request, httpResp *http.Response) (Request, error) { u, err := url.Parse(urlStr) if err != nil { return Request{}, err } req := Request{ Method: strings.ToUpper(method), URL: u, Header: make(map[string][]string), HTTPRequest: httpReq, HTTPResponse: httpResp, ctx: ctx, } if err := req.validate(); err != nil { return Request{}, err } return req, nil } // SetPreview sets the preview bytes in the icap header // todo: defer close error func (r *Request) SetPreview(maxBytes int) (err error) { var bodyBytes []byte var previewBytes int // receiving the body bites to determine the preview bytes depending on the request ICAP method if r.Method == MethodREQMOD { if r.HTTPRequest == nil { return nil } if r.HTTPRequest.Body != nil { b, err := io.ReadAll(r.HTTPRequest.Body) if err != nil { return err } bodyBytes = b defer func() { err = errors.Join(err, r.HTTPRequest.Body.Close()) }() } } if r.Method == MethodRESPMOD { if r.HTTPResponse == nil { return nil } if r.HTTPResponse.Body != nil { b, err := io.ReadAll(r.HTTPResponse.Body) if err != nil { return err } bodyBytes = b defer func() { err = errors.Join(err, r.HTTPResponse.Body.Close()) }() } } previewBytes = len(bodyBytes) // if the preview byte is 0 or less, there is no question of the body-fitting insides if previewBytes > 0 { r.bodyFittedInPreview = true } // if the preview bytes are greater than what was mentioned by the ICAP Server (did not fit in the body) if previewBytes > maxBytes { previewBytes = maxBytes r.bodyFittedInPreview = false // storing the rest of the body byte which was not sent as preview for further operations r.remainingPreviewBytes = bodyBytes[maxBytes:] } // set the body to the http message depending on the request method if r.Method == MethodREQMOD { r.HTTPRequest.Body = io.NopCloser(bytes.NewReader(bodyBytes)) } if r.Method == MethodRESPMOD { r.HTTPResponse.Body = io.NopCloser(bytes.NewReader(bodyBytes)) } // assign preview byte information to the header r.Header.Set("Preview", strconv.Itoa(previewBytes)) r.PreviewBytes = previewBytes r.previewSet = true return err } // to the ICAP server to ensure all required headers are set. func (r *Request) setDefaultRequestHeaders() { if _, exists := r.Header["Allow"]; !exists { r.Header.Add("Allow", "204") // assigning 204 by default if Allow not provided } if _, exists := r.Header["Host"]; !exists { hostName, _ := os.Hostname() r.Header.Add("Host", hostName) } } // extendHeader extends the current ICAP Request header with a new header. func (r *Request) extendHeader(hdr http.Header) error { for header, values := range hdr { if header == previewHeader && r.previewSet { continue } if header == encapsulatedHeader { continue } for _, value := range values { if header == previewHeader { pb, err := strconv.Atoi(value) if err != nil { return err } err = r.SetPreview(pb) if err != nil { return err } continue } r.Header.Add(header, value) } } return nil } // validate checks if the ICAP request is valid or not. func (r *Request) validate() error { var err error // check if the ICAP request has a context if r.ctx == nil { err = errors.Join(err, ErrNoContext) } // check if the ICAP request method is allowed if methodAllowed := slices.Contains([]string{ MethodOPTIONS, MethodRESPMOD, MethodREQMOD, }, r.Method); !methodAllowed { err = errors.Join(err, ErrMethodNotAllowed) } // check if the ICAP url is valid and contains all required fields { if r.URL.Scheme != schemeICAP { err = errors.Join(err, ErrInvalidScheme) } if r.URL.Host == "" { err = errors.Join(err, ErrInvalidHost) } } // check if the ICAP request method is aligned with the http messages { if r.Method == MethodREQMOD && r.HTTPRequest == nil { err = errors.Join(err, ErrREQMODWithoutReq) } if r.Method == MethodREQMOD && r.HTTPResponse != nil { err = errors.Join(err, ErrREQMODWithResp) } if r.Method == MethodRESPMOD && r.HTTPResponse == nil { err = errors.Join(err, ErrRESPMODWithoutResp) } } return err }