fix(antivirus): update icap-client library which fixes tcp socket reuse

This commit is contained in:
fschade
2025-09-30 15:42:51 +02:00
parent 25246782b2
commit 1d038e87c7
24 changed files with 318 additions and 353 deletions

4
go.mod
View File

@@ -18,7 +18,6 @@ require (
github.com/davidbyttow/govips/v2 v2.16.0
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8
github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e
github.com/egirna/icap-client v0.1.1
github.com/gabriel-vasile/mimetype v1.4.10
github.com/ggwhite/go-masker v1.1.0
github.com/go-chi/chi/v5 v5.2.3
@@ -64,6 +63,7 @@ require (
github.com/onsi/ginkgo/v2 v2.25.3
github.com/onsi/gomega v1.38.2
github.com/open-policy-agent/opa v1.9.0
github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20250724122329-41ba6b191e76
github.com/opencloud-eu/reva/v2 v2.38.1-0.20250924125540-eaa2437c36b2
github.com/opensearch-project/opensearch-go/v4 v4.5.0
@@ -390,8 +390,6 @@ require (
replace github.com/studio-b12/gowebdav => github.com/kobergj/gowebdav v0.0.0-20250102091030-aa65266db202
replace github.com/egirna/icap-client => github.com/fschade/icap-client v0.0.0-20240802074440-aade4a234387
replace github.com/unrolled/secure => github.com/DeepDiver1975/secure v0.0.0-20240611112133-abc838fb797c
replace go-micro.dev/v4 => github.com/butonic/go-micro/v4 v4.11.1-0.20241115112658-b5d4de5ed9b3

4
go.sum
View File

@@ -345,8 +345,6 @@ github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI=
github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
github.com/fschade/icap-client v0.0.0-20240802074440-aade4a234387 h1:Y3wZgTr29sLxWSMz4KF91o0x87EaJF6FIPNJFepRIiw=
github.com/fschade/icap-client v0.0.0-20240802074440-aade4a234387/go.mod h1:HpntrRsQA6RKNXy2Nbr4kVj+NO3OYWpAQUVxeya+3sU=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
@@ -932,6 +930,8 @@ github.com/open-policy-agent/opa v1.9.0 h1:QWFNwbcc29IRy0xwD3hRrMc/RtSersLY1Z6Ta
github.com/open-policy-agent/opa v1.9.0/go.mod h1:72+lKmTda0O48m1VKAxxYl7MjP/EWFZu9fxHQK2xihs=
github.com/opencloud-eu/go-micro-plugins/v4/store/nats-js-kv v0.0.0-20250512152754-23325793059a h1:Sakl76blJAaM6NxylVkgSzktjo2dS504iDotEFJsh3M=
github.com/opencloud-eu/go-micro-plugins/v4/store/nats-js-kv v0.0.0-20250512152754-23325793059a/go.mod h1:pjcozWijkNPbEtX5SIQaxEW/h8VAVZYTLx+70bmB3LY=
github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89 h1:W1ms+lP5lUUIzjRGDg93WrQfZJZCaV1ZP3KeyXi8bzY=
github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89/go.mod h1:vigJkNss1N2QEceCuNw/ullDehncuJNFB6mEnzfq9UI=
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20250724122329-41ba6b191e76 h1:vD/EdfDUrv4omSFjrinT8Mvf+8D7f9g4vgQ2oiDrVUI=
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20250724122329-41ba6b191e76/go.mod h1:pzatilMEHZFT3qV7C/X3MqOa3NlRQuYhlRhZTL+hN6Q=
github.com/opencloud-eu/reva/v2 v2.38.1-0.20250924125540-eaa2437c36b2 h1:e3B6KbWMjloKpqoTwTwvBLoCETRyyCDkQsqwRQMUdxc=

View File

@@ -11,7 +11,7 @@ import (
"github.com/opencloud-eu/reva/v2/pkg/mime"
ic "github.com/egirna/icap-client"
ic "github.com/opencloud-eu/icap-client"
)
// Scanner is the interface that wraps the basic Do method

View File

@@ -9,7 +9,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
ic "github.com/egirna/icap-client"
ic "github.com/opencloud-eu/icap-client"
"github.com/opencloud-eu/opencloud/services/antivirus/pkg/scanners"
"github.com/opencloud-eu/opencloud/services/antivirus/pkg/scanners/mocks"
)

View File

@@ -5,8 +5,8 @@
package mocks
import (
"github.com/egirna/icap-client"
mock "github.com/stretchr/testify/mock"
"github.com/opencloud-eu/icap-client"
"github.com/stretchr/testify/mock"
)
// NewScanner creates a new instance of Scanner. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.

View File

@@ -1,19 +0,0 @@
dist: xenial
language: go
env:
- GO111MODULE=ON
go:
- 1.21.x
git:
depth: 1
notifications:
email: true
script:
- go test -v -count=1 .

View File

@@ -1,9 +0,0 @@
# Contributing
1. Fork the repo
1. Make the change
1. Update the README.md with details of the changes (if needed)
1. Open your pull request against the dev branch

View File

@@ -1,84 +0,0 @@
# icap-client
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
[![Project status](https://img.shields.io/badge/version-0.1.0-green.svg)](https://github.com/egirna/icap-client/releases)
[![GoDoc](https://godoc.org/github.com/egirna/icap-client?status.svg)](https://godoc.org/github.com/egirna/icap-client)
Talk to the ICAP servers using probably the first ICAP client package in GO!
### Installing
```console
go get -u github.com/egirna/icap-client
```
### Usage
**Import The Package**
```go
import ic "github.com/egirna/icap-client"
```
**Making a simple RESPMOD call**
```go
req, err := ic.NewRequest(context.Background(), MethodRESPMOD, "icap://<host>:<port>/<path>", httpReq, httpResp)
if err != nil {
log.Fatal(err)
}
client, err := ic.NewClient(
ic.WithICAPConnectionTimeout(5 * time.Second),
)
if err != nil {
log.Fatal(err)
}
resp, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
```
**Note**: `httpReq` & `httpResp` here are `*http.Response` & `*http.Request` respectively
**Setting preview obtained from OPTIONS call**
```go
req, err := ic.NewRequest(context.Background(), ic.MethodOPTIONS, "icap://<host>:<port>/<path>", nil, nil)
if err != nil {
log.Fatal(err)
}
client, err := ic.NewClient(
ic.WithICAPConnectionTimeout(5 * time.Second),
)
if err != nil {
log.Fatal(err)
}
req.SetPreview(optReq.PreviewBytes)
// do something with req(ICAP *Request)
```
By default, the icap-client will dump the debugging logs to the standard output(stdout),
but you can always add your custom writer
```go
f, _ := os.OpenFile("logs.txt", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
ic.SetDebugOutput(f)
```
For more details, see the [docs](https://godoc.org/github.com/egirna/icap-client) and [examples](examples/).
### Contributing
This package is still WIP, so totally open to suggestions. See the contribution guide [here](CONTRIBUTING.md).
### License
**icap-client** is licensed under the [Apache License](LICENSE).

View File

@@ -1,140 +0,0 @@
package icapclient
import (
"bytes"
"context"
"io"
"net"
"sync"
"syscall"
"time"
)
// ICAPConnConfig is the configuration for the icap connection
type ICAPConnConfig struct {
// Timeout is the maximum amount of time a connection will be kept open
Timeout time.Duration
}
// ICAPConn is the one responsible for driving the transport layer operations. We have to explicitly deal with the connection because the ICAP protocol is aware of keep alive and reconnects.
type ICAPConn struct {
tcp net.Conn
mu sync.Mutex
timeout time.Duration
}
// NewICAPConn creates a new connection to the icap server
func NewICAPConn(conf ICAPConnConfig) (*ICAPConn, error) {
return &ICAPConn{
timeout: conf.Timeout,
}, nil
}
// Connect connects to the icap server
func (c *ICAPConn) Connect(ctx context.Context, address string) error {
dialer := net.Dialer{Timeout: c.timeout}
conn, err := dialer.DialContext(ctx, "tcp", address)
if err != nil {
return err
}
c.tcp = conn
if dialer.Timeout == 0 {
return nil
}
deadline := time.Now().UTC().Add(dialer.Timeout)
if err := c.tcp.SetReadDeadline(deadline); err != nil {
return err
}
if err := c.tcp.SetWriteDeadline(deadline); err != nil {
return err
}
return nil
}
// Send sends a request to the icap server
func (c *ICAPConn) Send(in []byte) ([]byte, error) {
if !c.ok() {
return nil, syscall.EINVAL
}
c.mu.Lock()
defer c.mu.Unlock()
errChan := make(chan error)
resChan := make(chan []byte)
go func() {
// send the message to the server
_, err := c.tcp.Write(in)
if err != nil {
errChan <- err
}
}()
go func() {
data := make([]byte, 0)
for {
tmp := make([]byte, 1096)
// read the response from the server
n, err := c.tcp.Read(tmp)
// something went wrong while reading from the server,
// send the error and exit the routine to prevent
// sending the response to resChan
if err != nil && err != io.EOF {
errChan <- err
return
}
// EOF detected, an entire message is received
if err == io.EOF || n == 0 {
break
}
data = append(data, tmp[:n]...)
// explicitly breaking because the Read blocks for 100 continue message
if bytes.Equal(data, []byte(icap100ContinueMsg)) {
break
}
// EOF detected, double crlf indicates the end of the message
if bytes.HasSuffix(data, []byte(doubleCRLF)) {
break
}
// EOF detected, 204 no modifications and Double crlf indicate the end of the message
if bytes.Contains(data, []byte(icap204NoModsMsg)) {
break
}
}
resChan <- data
}()
select {
case err := <-errChan:
return nil, err
case res := <-resChan:
return res, nil
}
}
// Close closes the tcp connection
func (c *ICAPConn) Close() error {
if !c.ok() {
return syscall.EINVAL
}
return c.tcp.Close()
}
func (c *ICAPConn) ok() bool { return c != nil && c.tcp != nil }

View File

@@ -0,0 +1,20 @@
root = true
[*]
indent_style = space
indent_size = 2
tab_width = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.go]
indent_style = tab
[*.md]
trim_trailing_whitespace = false
indent_size = 1
[Makefile]
indent_style = tab

View File

@@ -0,0 +1 @@
* text=auto eol=lf

View File

@@ -11,10 +11,6 @@
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
vendor/
go.sum
# Misc
.DS_Store
logs.txt

View File

@@ -0,0 +1,76 @@
linters-settings:
gofmt:
simplify: true
rewrite-rules:
- pattern: interface{}
replacement: any
misspell:
locale: US
gofumpt:
extra-rules: true
forbidigo:
forbid:
- context\.WithCancel$
- ^print.*$
- panic
- ^log.Fatal().*$
errorlint:
errorf-multi: true
gci:
custom-order: true
sections:
- standard
- default
- prefix(github.com/opencloud-eu/woodpecker-ci-config-service)
godot:
scope: toplevel
period: true
linters:
disable-all: true
enable:
- bidichk
- errcheck
- gofmt
- gosimple
- goimports
- govet
- ineffassign
- misspell
- revive
- staticcheck
- typecheck
- unused
- gofumpt
- errorlint
- forbidigo
- zerologlint
- asciicheck
- dogsled
- durationcheck
- errchkjson
- goheader
- gomoddirectives
- gomodguard
- goprintffuncname
- importas
- makezero
- rowserrcheck
- sqlclosecheck
- usetesting
- unconvert
- unparam
- wastedassign
- whitespace
- gocritic
- nolintlint
- stylecheck
- contextcheck
- forcetypeassert
- gci
- godot
run:
timeout: 15m
build-tags:
- test

19
vendor/github.com/opencloud-eu/icap-client/Makefile generated vendored Normal file
View File

@@ -0,0 +1,19 @@
.PHONY: lint
lint:
go tool golangci-lint run
.PHONY: vendor
vendor:
go mod tidy
go mod vendor
format:
go tool gofumpt -extra -w .
.PHONY: clean
clean:
go clean -i ./...
.PHONY: test
test:
go test -race -cover -coverprofile coverage.out -timeout 60s .

18
vendor/github.com/opencloud-eu/icap-client/README.md generated vendored Normal file
View File

@@ -0,0 +1,18 @@
# icap-client
[![status-badge](https://ci.opencloud.eu/api/badges/21/status.svg)](https://ci.opencloud.eu/repos/21)
[![Matrix](https://img.shields.io/matrix/opencloud%3Amatrix.org?logo=matrix)](https://app.element.io/#/room/#opencloud:matrix.org)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
## Contributing
Please follow the [OpenCloud Contribution Guidelines](https://github.com/opencloud-eu/opencloud/blob/main/CONTRIBUTING.md), which apply to all projects in the OpenCloud organization.
## Acknowledgements
This project is forked from [@egirna/icap-client](https://github.com/egirna/icap-client).
Special thanks to [egirna](https://github.com/egirna) for the original implementation and ongoing inspiration.
### License
**icap-client** is licensed under the [Apache License](LICENSE).

View File

@@ -8,49 +8,43 @@ import (
"strings"
)
// Client represents the icap client who makes the icap server calls
// Client represents the ICAP client who makes the ICAP server calls.
type Client struct {
conn Conn
config Config // Store config for connection parameters
}
// NewClient creates a new icap client
// NewClient creates a new ICAP client (no persistent connection).
func NewClient(options ...ConfigOption) (Client, error) {
config := DefaultConfig()
for _, option := range options {
option(&config)
}
conn, err := NewICAPConn(config.ICAPConn)
if err != nil {
return Client{}, err
}
return Client{
conn: conn,
}, nil
return Client{config: config}, nil
}
// Do is the main function of the client that makes the ICAP request
func (c *Client) Do(req Request) (res Response, err error) {
// establish connection to the icap server
err = c.conn.Connect(req.ctx, req.URL.Host)
// Do make the ICAP request, creating and dropping a connection each time.
func (c Client) Do(req Request) (res Response, err error) {
conn, err := NewICAPConn(c.config.ICAPConn)
if err != nil {
return Response{}, err
}
if err := conn.Connect(req.ctx, req.URL.Host); err != nil {
return Response{}, err
}
defer func() {
err = errors.Join(err, c.conn.Close())
err = errors.Join(err, conn.Close())
}()
req.setDefaultRequestHeaders()
// convert the request to icap message
message, err := toICAPRequest(req)
if err != nil {
return Response{}, err
}
// send the icap message to the server
dataRes, err := c.conn.Send(message)
// send the ICAP message to the server
dataRes, err := conn.Send(message)
if err != nil {
return Response{}, err
}
@@ -60,25 +54,25 @@ func (c *Client) Do(req Request) (res Response, err error) {
return Response{}, err
}
// check if the message is fully done scanning or if it needs to be sent another chunk
// check if the message is fully done scanning or if it needs to be sent another chunk.
done := !(res.StatusCode == http.StatusContinue && !req.bodyFittedInPreview && req.previewSet)
if done {
return res, nil
}
// get the remaining body bytes
// get the remaining body bytes.
data := req.remainingPreviewBytes
if !bodyIsChunked(string(data)) {
data = []byte(addHexBodyByteNotations(string(data)))
}
// hydrate the icap message with closing doubleCRLF suffix
// hydrate the ICAP message with closing doubleCRLF suffix.
if !bytes.HasSuffix(data, []byte(doubleCRLF)) {
data = append(data, []byte(crlf)...)
}
// send the remaining body bytes to the server
dataRes, err = c.conn.Send(data)
// send the remaining body bytes to the server.
dataRes, err = conn.Send(data)
if err != nil {
return Response{}, err
}

View File

@@ -4,12 +4,12 @@ import (
"time"
)
// Config is the shared configuration for the icap client library
// Config is the shared configuration for the icap client library.
type Config struct {
ICAPConn ICAPConnConfig
}
// DefaultConfig returns the default configuration for the icap client library
// DefaultConfig returns the default configuration for the icap client library.
func DefaultConfig() Config {
return Config{
ICAPConn: ICAPConnConfig{
@@ -18,10 +18,10 @@ func DefaultConfig() Config {
}
}
// ConfigOption is a function that configures the icap client
// ConfigOption is a function that configures the icap client.
type ConfigOption func(*Config)
// WithICAPConnectionTimeout sets the timeout for the connection to the icap server
// WithICAPConnectionTimeout sets the timeout for the connection to the icap server.
func WithICAPConnectionTimeout(timeout time.Duration) ConfigOption {
return func(cfg *Config) {
if timeout <= 0 {

109
vendor/github.com/opencloud-eu/icap-client/conn.go generated vendored Normal file
View File

@@ -0,0 +1,109 @@
package icapclient
import (
"bytes"
"context"
"errors"
"io"
"net"
"sync"
"time"
)
// ICAPConnConfig is the configuration for the icap connection.
type ICAPConnConfig struct {
// Timeout is the maximum amount of time a connection will be kept open
Timeout time.Duration
}
// ICAPConn manages the transport layer for ICAP protocol.
type ICAPConn struct {
tcp net.Conn
mu sync.Mutex
timeout time.Duration
}
// NewICAPConn creates a new connection configuration.
func NewICAPConn(conf ICAPConnConfig) (*ICAPConn, error) {
return &ICAPConn{
timeout: conf.Timeout,
}, nil
}
// Connect connects to the ICAP server.
func (c *ICAPConn) Connect(ctx context.Context, address string) error {
dialer := net.Dialer{Timeout: c.timeout}
conn, err := dialer.DialContext(ctx, "tcp", address)
if err != nil {
return err
}
c.tcp = conn
if c.timeout > 0 {
deadline := time.Now().Add(c.timeout)
if err := c.tcp.SetDeadline(deadline); err != nil {
return err
}
}
return nil
}
// Send sends a request to the ICAP server and reads the response.
func (c *ICAPConn) Send(in []byte) ([]byte, error) {
if !c.ok() {
return nil, ErrInvalidConnection
}
c.mu.Lock()
defer c.mu.Unlock()
_, err := c.tcp.Write(in)
if err != nil {
return nil, err
}
var data []byte
buf := make([]byte, 4096)
for {
n, err := c.tcp.Read(buf)
if err != nil && !errors.Is(err, io.EOF) {
return nil, err
}
if errors.Is(err, io.EOF) || n == 0 {
break
}
data = append(data, buf[:n]...)
// Protocol checks for message termination
{
if bytes.Equal(data, []byte(icap100ContinueMsg)) {
break
}
if bytes.HasSuffix(data, []byte(doubleCRLF)) {
break
}
if bytes.Contains(data, []byte(icap204NoModsMsg)) {
break
}
}
}
return data, nil
}
// Close closes the TCP connection.
func (c *ICAPConn) Close() error {
if !c.ok() {
return ErrInvalidConnection
}
return c.tcp.Close()
}
func (c *ICAPConn) ok() bool {
return c != nil && c.tcp != nil
}

View File

@@ -13,41 +13,44 @@ import (
"strings"
)
// the icap request methods
// the icap request methods.
const (
MethodOPTIONS = "OPTIONS"
MethodRESPMOD = "RESPMOD"
MethodREQMOD = "REQMOD"
)
// shared errors
// shared errors.
var (
// ErrNoContext is used when no context is provided
// 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 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 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 is used when the host is invalid.
ErrInvalidHost = errors.New("the requested host is invalid")
// ErrInvalidTCPMsg is used when the tcp message is invalid
ErrInvalidTCPMsg = errors.New("invalid tcp message")
// ErrInvalidMsg is used when the tcp message is invalid.
ErrInvalidMsg = errors.New("invalid message")
// ErrREQMODWithoutReq is used when the request is nil for REQMOD method
// 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 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 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
// general constants required for the package.
const (
schemeICAP = "icap"
icapVersion = "ICAP/1.0"
@@ -63,20 +66,20 @@ const (
icap204NoModsMsg = "ICAP/1.0 204 Unmodified"
)
// Common ICAP headers
// Common ICAP headers.
const (
previewHeader = "Preview"
encapsulatedHeader = "Encapsulated"
)
// Conn represents the connection to the icap server
// 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
// Response represents the icap server response data.
type Response struct {
StatusCode int
Status string
@@ -86,10 +89,9 @@ type Response struct {
ContentResponse *http.Response
}
// getStatusWithCode prepares the status code and status text from two given strings
// 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
}
@@ -99,7 +101,7 @@ func getStatusWithCode(str1, str2 string) (int, string, error) {
return statusCode, status, nil
}
// getHeaderValue parses the header and its value from a tcp message string
// 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]
@@ -109,16 +111,15 @@ func getHeaderValue(str string) (string, string) {
}
return header, ""
}
// isRequestLine determines if the tcp message string is a request line, i.e., the first line of the message or not
// 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 string, httpReqStr, httpRespStr string) string {
// 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) {
@@ -173,15 +174,14 @@ func setEncapsulatedHeaderValue(icapReqStr string, httpReqStr, httpRespStr strin
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 string, uri, url string) string {
// 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 = "/"
}
@@ -189,12 +189,12 @@ func replaceRequestURIWithActualURL(str string, uri, url string) string {
return strings.Replace(str, uri, url, 1)
}
// addFullBodyInPreviewIndicator adds 0; ieof\r\n\r\n which indicates the entire body fitted in the preview
// 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
// splitBodyAndHeader separates header and body from a http message.
func splitBodyAndHeader(str string) (string, string, bool) {
ss := strings.SplitN(str, doubleCRLF, 2)
@@ -208,7 +208,7 @@ func splitBodyAndHeader(str string) (string, string, bool) {
return headerStr, bodyStr, true
}
// bodyIsChunked determines if the http body is already chunked from the origin server or not
// bodyIsChunked determines if the http body is already chunked from the origin server or not.
func bodyIsChunked(str string) bool {
_, bodyStr, ok := splitBodyAndHeader(str)
@@ -219,7 +219,7 @@ func bodyIsChunked(str string) bool {
return regexp.MustCompile(`\r\n0(\r\n)+$`).MatchString(bodyStr)
}
// parsePreviewBodyBytes parses the preview portion of the body and only keeps that in the message
// 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 {
@@ -229,21 +229,17 @@ func parsePreviewBodyBytes(str string, pb int) string {
return headerStr + doubleCRLF + bodyStr[:pb]
}
// addHexBodyByteNotations adds the hexadecimal byte notations to the string,
// for example, Hello World, becomes
// b
// Hello World
// 0
// 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
// 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
// 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)
@@ -262,7 +258,6 @@ func toICAPRequest(req Request) ([]byte, error) {
httpReqStr := ""
if req.HTTPRequest != nil {
b, err := httputil.DumpRequestOut(req.HTTPRequest, true)
if err != nil {
return nil, err
}
@@ -282,7 +277,6 @@ func toICAPRequest(req Request) ([]byte, error) {
httpReqStr = addHeaderAndBody(headerStr, bodyStr)
}
}
}
// if the HTTP Request message block doesn't end with a \r\n\r\n,
@@ -292,14 +286,12 @@ func toICAPRequest(req Request) ([]byte, error) {
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
}
@@ -321,13 +313,12 @@ func toICAPRequest(req Request) ([]byte, error) {
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
// populating the Encapsulated header of the ICAP message portion
reqStr = setEncapsulatedHeaderValue(reqStr, httpReqStr, httpRespStr)
}
@@ -345,7 +336,7 @@ func toICAPRequest(req Request) ([]byte, error) {
return data, nil
}
// toClientResponse reads an ICAP message and returns a Response
// toClientResponse reads an ICAP message and returns a Response.
func toClientResponse(b *bufio.Reader) (Response, error) {
resp := Response{
Header: make(map[string][]string),
@@ -354,14 +345,13 @@ func toClientResponse(b *bufio.Reader) (Response, error) {
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", ErrInvalidTCPMsg, currentMsg)
return Response{}, fmt.Errorf("%w: %s", ErrInvalidMsg, currentMsg)
}
// preparing the scheme below
@@ -409,7 +399,8 @@ func toClientResponse(b *bufio.Reader) (Response, error) {
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
// 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 {

View File

@@ -13,7 +13,7 @@ import (
"strings"
)
// Request represents the icap client request data
// Request represents the icap client request data.
type Request struct {
Method string
URL *url.URL
@@ -32,7 +32,6 @@ type Request struct {
// 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
}
@@ -128,8 +127,7 @@ func (r *Request) SetPreview(maxBytes int) (err error) {
return err
}
// setDefaultRequestHeaders is called by the client before sending the request
// to the ICAP server to ensure all required headers are set
// 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
@@ -141,10 +139,9 @@ func (r *Request) setDefaultRequestHeaders() {
}
}
// extendHeader extends the current ICAP Request header with a new header
// 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
}
@@ -174,7 +171,7 @@ func (r *Request) extendHeader(hdr http.Header) error {
return nil
}
// validate checks if the ICAP request is valid or not
// validate checks if the ICAP request is valid or not.
func (r *Request) validate() error {
var err error

View File

@@ -97,7 +97,6 @@ func respmodHandler(w icap.ResponseWriter, req *icap.Request) {
}
w.WriteHeader(status, nil, false)
}
}
@@ -134,7 +133,6 @@ func reqmodHandler(w icap.ResponseWriter, req *icap.Request) {
}
w.WriteHeader(status, nil, false)
}
}

7
vendor/modules.txt vendored
View File

@@ -419,9 +419,6 @@ github.com/ebitengine/purego/internal/strings
# github.com/egirna/icap v0.0.0-20181108071049-d5ee18bd70bc
## explicit
github.com/egirna/icap
# github.com/egirna/icap-client v0.1.1 => github.com/fschade/icap-client v0.0.0-20240802074440-aade4a234387
## explicit; go 1.21
github.com/egirna/icap-client
# github.com/emirpasic/gods v1.18.1
## explicit; go 1.2
github.com/emirpasic/gods/containers
@@ -1324,6 +1321,9 @@ github.com/open-policy-agent/opa/v1/types
github.com/open-policy-agent/opa/v1/util
github.com/open-policy-agent/opa/v1/util/decoding
github.com/open-policy-agent/opa/v1/version
# github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89
## explicit; go 1.24.6
github.com/opencloud-eu/icap-client
# github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20250724122329-41ba6b191e76
## explicit; go 1.18
github.com/opencloud-eu/libre-graph-api-go
@@ -2660,7 +2660,6 @@ sigs.k8s.io/yaml
## explicit; go 1.13
stash.kopano.io/kgol/rndm
# github.com/studio-b12/gowebdav => github.com/kobergj/gowebdav v0.0.0-20250102091030-aa65266db202
# github.com/egirna/icap-client => github.com/fschade/icap-client v0.0.0-20240802074440-aade4a234387
# github.com/unrolled/secure => github.com/DeepDiver1975/secure v0.0.0-20240611112133-abc838fb797c
# go-micro.dev/v4 => github.com/butonic/go-micro/v4 v4.11.1-0.20241115112658-b5d4de5ed9b3
# github.com/go-micro/plugins/v4/store/nats-js-kv => github.com/opencloud-eu/go-micro-plugins/v4/store/nats-js-kv v0.0.0-20250512152754-23325793059a