fix(antivirus): update icap-client library which fixes tcp socket reuse
This commit is contained in:
@@ -18,7 +18,6 @@ require (
|
|||||||
github.com/davidbyttow/govips/v2 v2.16.0
|
github.com/davidbyttow/govips/v2 v2.16.0
|
||||||
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8
|
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8
|
||||||
github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e
|
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/gabriel-vasile/mimetype v1.4.10
|
||||||
github.com/ggwhite/go-masker v1.1.0
|
github.com/ggwhite/go-masker v1.1.0
|
||||||
github.com/go-chi/chi/v5 v5.2.3
|
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/ginkgo/v2 v2.25.3
|
||||||
github.com/onsi/gomega v1.38.2
|
github.com/onsi/gomega v1.38.2
|
||||||
github.com/open-policy-agent/opa v1.9.0
|
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/libre-graph-api-go v1.0.8-0.20250724122329-41ba6b191e76
|
||||||
github.com/opencloud-eu/reva/v2 v2.38.1-0.20250924125540-eaa2437c36b2
|
github.com/opencloud-eu/reva/v2 v2.38.1-0.20250924125540-eaa2437c36b2
|
||||||
github.com/opensearch-project/opensearch-go/v4 v4.5.0
|
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/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 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
|
replace go-micro.dev/v4 => github.com/butonic/go-micro/v4 v4.11.1-0.20241115112658-b5d4de5ed9b3
|
||||||
|
|||||||
@@ -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/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 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI=
|
||||||
github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
|
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.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.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
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/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 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/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 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/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=
|
github.com/opencloud-eu/reva/v2 v2.38.1-0.20250924125540-eaa2437c36b2 h1:e3B6KbWMjloKpqoTwTwvBLoCETRyyCDkQsqwRQMUdxc=
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
|
|
||||||
"github.com/opencloud-eu/reva/v2/pkg/mime"
|
"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
|
// Scanner is the interface that wraps the basic Do method
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
"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"
|
||||||
"github.com/opencloud-eu/opencloud/services/antivirus/pkg/scanners/mocks"
|
"github.com/opencloud-eu/opencloud/services/antivirus/pkg/scanners/mocks"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
package mocks
|
package mocks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/egirna/icap-client"
|
"github.com/opencloud-eu/icap-client"
|
||||||
mock "github.com/stretchr/testify/mock"
|
"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.
|
// 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.
|
||||||
|
|||||||
-19
@@ -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 .
|
|
||||||
-9
@@ -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
|
|
||||||
-84
@@ -1,84 +0,0 @@
|
|||||||
# icap-client
|
|
||||||
|
|
||||||
[](LICENSE)
|
|
||||||
[](https://github.com/egirna/icap-client/releases)
|
|
||||||
[](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).
|
|
||||||
-140
@@ -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 }
|
|
||||||
+20
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
-4
@@ -11,10 +11,6 @@
|
|||||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
*.out
|
*.out
|
||||||
|
|
||||||
# Dependency directories (remove the comment below to include it)
|
|
||||||
vendor/
|
|
||||||
go.sum
|
|
||||||
|
|
||||||
# Misc
|
# Misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
logs.txt
|
logs.txt
|
||||||
+76
@@ -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
|
||||||
Generated
Vendored
+19
@@ -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
@@ -0,0 +1,18 @@
|
|||||||
|
# icap-client
|
||||||
|
|
||||||
|
[](https://ci.opencloud.eu/repos/21)
|
||||||
|
[](https://app.element.io/#/room/#opencloud:matrix.org)
|
||||||
|
[](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).
|
||||||
Generated
Vendored
+19
-25
@@ -8,49 +8,43 @@ import (
|
|||||||
"strings"
|
"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 {
|
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) {
|
func NewClient(options ...ConfigOption) (Client, error) {
|
||||||
config := DefaultConfig()
|
config := DefaultConfig()
|
||||||
for _, option := range options {
|
for _, option := range options {
|
||||||
option(&config)
|
option(&config)
|
||||||
}
|
}
|
||||||
|
return Client{config: config}, nil
|
||||||
conn, err := NewICAPConn(config.ICAPConn)
|
|
||||||
if err != nil {
|
|
||||||
return Client{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return Client{
|
|
||||||
conn: conn,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do is the main function of the client that makes the ICAP request
|
// Do make the ICAP request, creating and dropping a connection each time.
|
||||||
func (c *Client) Do(req Request) (res Response, err error) {
|
func (c Client) Do(req Request) (res Response, err error) {
|
||||||
// establish connection to the icap server
|
conn, err := NewICAPConn(c.config.ICAPConn)
|
||||||
err = c.conn.Connect(req.ctx, req.URL.Host)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Response{}, err
|
return Response{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := conn.Connect(req.ctx, req.URL.Host); err != nil {
|
||||||
|
return Response{}, err
|
||||||
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
err = errors.Join(err, c.conn.Close())
|
err = errors.Join(err, conn.Close())
|
||||||
}()
|
}()
|
||||||
|
|
||||||
req.setDefaultRequestHeaders()
|
req.setDefaultRequestHeaders()
|
||||||
|
|
||||||
// convert the request to icap message
|
|
||||||
message, err := toICAPRequest(req)
|
message, err := toICAPRequest(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Response{}, err
|
return Response{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// send the icap message to the server
|
// send the ICAP message to the server
|
||||||
dataRes, err := c.conn.Send(message)
|
dataRes, err := conn.Send(message)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Response{}, err
|
return Response{}, err
|
||||||
}
|
}
|
||||||
@@ -60,25 +54,25 @@ func (c *Client) Do(req Request) (res Response, err error) {
|
|||||||
return Response{}, err
|
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)
|
done := !(res.StatusCode == http.StatusContinue && !req.bodyFittedInPreview && req.previewSet)
|
||||||
if done {
|
if done {
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the remaining body bytes
|
// get the remaining body bytes.
|
||||||
data := req.remainingPreviewBytes
|
data := req.remainingPreviewBytes
|
||||||
if !bodyIsChunked(string(data)) {
|
if !bodyIsChunked(string(data)) {
|
||||||
data = []byte(addHexBodyByteNotations(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)) {
|
if !bytes.HasSuffix(data, []byte(doubleCRLF)) {
|
||||||
data = append(data, []byte(crlf)...)
|
data = append(data, []byte(crlf)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// send the remaining body bytes to the server
|
// send the remaining body bytes to the server.
|
||||||
dataRes, err = c.conn.Send(data)
|
dataRes, err = conn.Send(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Response{}, err
|
return Response{}, err
|
||||||
}
|
}
|
||||||
Generated
Vendored
+4
-4
@@ -4,12 +4,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config is the shared configuration for the icap client library
|
// Config is the shared configuration for the icap client library.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
ICAPConn ICAPConnConfig
|
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 {
|
func DefaultConfig() Config {
|
||||||
return Config{
|
return Config{
|
||||||
ICAPConn: ICAPConnConfig{
|
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)
|
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 {
|
func WithICAPConnectionTimeout(timeout time.Duration) ConfigOption {
|
||||||
return func(cfg *Config) {
|
return func(cfg *Config) {
|
||||||
if timeout <= 0 {
|
if timeout <= 0 {
|
||||||
+109
@@ -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
|
||||||
|
}
|
||||||
Generated
Vendored
Generated
Vendored
+37
-46
@@ -13,41 +13,44 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// the icap request methods
|
// the icap request methods.
|
||||||
const (
|
const (
|
||||||
MethodOPTIONS = "OPTIONS"
|
MethodOPTIONS = "OPTIONS"
|
||||||
MethodRESPMOD = "RESPMOD"
|
MethodRESPMOD = "RESPMOD"
|
||||||
MethodREQMOD = "REQMOD"
|
MethodREQMOD = "REQMOD"
|
||||||
)
|
)
|
||||||
|
|
||||||
// shared errors
|
// shared errors.
|
||||||
var (
|
var (
|
||||||
// ErrNoContext is used when no context is provided
|
// ErrNoContext is used when no context is provided.
|
||||||
ErrNoContext = errors.New("no context 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://")
|
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")
|
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")
|
ErrInvalidHost = errors.New("the requested host is invalid")
|
||||||
|
|
||||||
// ErrInvalidTCPMsg is used when the tcp message is invalid
|
// ErrInvalidMsg is used when the tcp message is invalid.
|
||||||
ErrInvalidTCPMsg = errors.New("invalid tcp message")
|
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")
|
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")
|
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")
|
ErrRESPMODWithoutResp = errors.New("http response cannot be nil for method RESPMOD")
|
||||||
)
|
)
|
||||||
|
|
||||||
// general constants required for the package
|
// general constants required for the package.
|
||||||
const (
|
const (
|
||||||
schemeICAP = "icap"
|
schemeICAP = "icap"
|
||||||
icapVersion = "ICAP/1.0"
|
icapVersion = "ICAP/1.0"
|
||||||
@@ -63,20 +66,20 @@ const (
|
|||||||
icap204NoModsMsg = "ICAP/1.0 204 Unmodified"
|
icap204NoModsMsg = "ICAP/1.0 204 Unmodified"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Common ICAP headers
|
// Common ICAP headers.
|
||||||
const (
|
const (
|
||||||
previewHeader = "Preview"
|
previewHeader = "Preview"
|
||||||
encapsulatedHeader = "Encapsulated"
|
encapsulatedHeader = "Encapsulated"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Conn represents the connection to the icap server
|
// Conn represents the connection to the icap server.
|
||||||
type Conn interface {
|
type Conn interface {
|
||||||
io.Closer
|
io.Closer
|
||||||
Connect(ctx context.Context, address string) error
|
Connect(ctx context.Context, address string) error
|
||||||
Send(in []byte) ([]byte, error)
|
Send(in []byte) ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Response represents the icap server response data
|
// Response represents the icap server response data.
|
||||||
type Response struct {
|
type Response struct {
|
||||||
StatusCode int
|
StatusCode int
|
||||||
Status string
|
Status string
|
||||||
@@ -86,10 +89,9 @@ type Response struct {
|
|||||||
ContentResponse *http.Response
|
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) {
|
func getStatusWithCode(str1, str2 string) (int, string, error) {
|
||||||
statusCode, err := strconv.Atoi(str1)
|
statusCode, err := strconv.Atoi(str1)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, "", err
|
return 0, "", err
|
||||||
}
|
}
|
||||||
@@ -99,7 +101,7 @@ func getStatusWithCode(str1, str2 string) (int, string, error) {
|
|||||||
return statusCode, status, nil
|
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) {
|
func getHeaderValue(str string) (string, string) {
|
||||||
headerValues := strings.SplitN(str, ":", 2)
|
headerValues := strings.SplitN(str, ":", 2)
|
||||||
header := headerValues[0]
|
header := headerValues[0]
|
||||||
@@ -109,16 +111,15 @@ func getHeaderValue(str string) (string, string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return header, ""
|
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 {
|
func isRequestLine(str string) bool {
|
||||||
return strings.Contains(str, icapVersion) || strings.Contains(str, httpVersion)
|
return strings.Contains(str, icapVersion) || strings.Contains(str, httpVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
// setEncapsulatedHeaderValue generates the Encapsulated values and assigns to the ICAP request string
|
// setEncapsulatedHeaderValue generates the Encapsulated values and assigns to the ICAP request string.
|
||||||
func setEncapsulatedHeaderValue(icapReqStr string, httpReqStr, httpRespStr string) string {
|
func setEncapsulatedHeaderValue(icapReqStr, httpReqStr, httpRespStr string) string {
|
||||||
encVal := " "
|
encVal := " "
|
||||||
|
|
||||||
if strings.HasPrefix(icapReqStr, MethodOPTIONS) {
|
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])
|
encVal += fmt.Sprintf(", null-body=%d", reqEndsAt+respIndices[0][1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatting the ICAP request Encapsulated header with the value
|
// formatting the ICAP request Encapsulated header with the value
|
||||||
return fmt.Sprintf(icapReqStr, encVal)
|
return fmt.Sprintf(icapReqStr, encVal)
|
||||||
}
|
}
|
||||||
|
|
||||||
// replaceRequestURIWithActualURL replaces just the escaped portion of the url with the entire URL in the dumped request message
|
// 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 {
|
func replaceRequestURIWithActualURL(str, uri, url string) string {
|
||||||
if uri == "" {
|
if uri == "" {
|
||||||
uri = "/"
|
uri = "/"
|
||||||
}
|
}
|
||||||
@@ -189,12 +189,12 @@ func replaceRequestURIWithActualURL(str string, uri, url string) string {
|
|||||||
return strings.Replace(str, uri, url, 1)
|
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 {
|
func addFullBodyInPreviewIndicator(str string) string {
|
||||||
return strings.TrimSuffix(str, doubleCRLF) + fullBodyEndIndicatorPreviewMode
|
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) {
|
func splitBodyAndHeader(str string) (string, string, bool) {
|
||||||
ss := strings.SplitN(str, doubleCRLF, 2)
|
ss := strings.SplitN(str, doubleCRLF, 2)
|
||||||
|
|
||||||
@@ -208,7 +208,7 @@ func splitBodyAndHeader(str string) (string, string, bool) {
|
|||||||
return headerStr, bodyStr, true
|
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 {
|
func bodyIsChunked(str string) bool {
|
||||||
_, bodyStr, ok := splitBodyAndHeader(str)
|
_, bodyStr, ok := splitBodyAndHeader(str)
|
||||||
|
|
||||||
@@ -219,7 +219,7 @@ func bodyIsChunked(str string) bool {
|
|||||||
return regexp.MustCompile(`\r\n0(\r\n)+$`).MatchString(bodyStr)
|
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 {
|
func parsePreviewBodyBytes(str string, pb int) string {
|
||||||
headerStr, bodyStr, ok := splitBodyAndHeader(str)
|
headerStr, bodyStr, ok := splitBodyAndHeader(str)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -229,21 +229,17 @@ func parsePreviewBodyBytes(str string, pb int) string {
|
|||||||
return headerStr + doubleCRLF + bodyStr[:pb]
|
return headerStr + doubleCRLF + bodyStr[:pb]
|
||||||
}
|
}
|
||||||
|
|
||||||
// addHexBodyByteNotations adds the hexadecimal byte notations to the string,
|
// 0.
|
||||||
// for example, Hello World, becomes
|
|
||||||
// b
|
|
||||||
// Hello World
|
|
||||||
// 0
|
|
||||||
func addHexBodyByteNotations(str string) string {
|
func addHexBodyByteNotations(str string) string {
|
||||||
return fmt.Sprintf("%x%s%s%s", len([]byte(str)), crlf, str, bodyEndIndicator)
|
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 {
|
func addHeaderAndBody(headerStr, bodyStr string) string {
|
||||||
return headerStr + doubleCRLF + bodyStr
|
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) {
|
func toICAPRequest(req Request) ([]byte, error) {
|
||||||
// Making the ICAP message block
|
// Making the ICAP message block
|
||||||
reqStr := fmt.Sprintf("%s %s %s%s", req.Method, req.URL.String(), icapVersion, crlf)
|
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 := ""
|
httpReqStr := ""
|
||||||
if req.HTTPRequest != nil {
|
if req.HTTPRequest != nil {
|
||||||
b, err := httputil.DumpRequestOut(req.HTTPRequest, true)
|
b, err := httputil.DumpRequestOut(req.HTTPRequest, true)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -282,7 +277,6 @@ func toICAPRequest(req Request) ([]byte, error) {
|
|||||||
httpReqStr = addHeaderAndBody(headerStr, bodyStr)
|
httpReqStr = addHeaderAndBody(headerStr, bodyStr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the HTTP Request message block doesn't end with a \r\n\r\n,
|
// 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
|
httpReqStr += crlf
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// build the HTTP Response message block
|
// build the HTTP Response message block
|
||||||
httpRespStr := ""
|
httpRespStr := ""
|
||||||
if req.HTTPResponse != nil {
|
if req.HTTPResponse != nil {
|
||||||
b, err := httputil.DumpResponse(req.HTTPResponse, true)
|
b, err := httputil.DumpResponse(req.HTTPResponse, true)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
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
|
httpRespStr += crlf
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if encVal := req.Header.Get(encapsulatedHeader); encVal != "" {
|
if encVal := req.Header.Get(encapsulatedHeader); encVal != "" {
|
||||||
reqStr = fmt.Sprintf(reqStr, encVal)
|
reqStr = fmt.Sprintf(reqStr, encVal)
|
||||||
} else {
|
} else {
|
||||||
//populating the Encapsulated header of the ICAP message portion
|
// populating the Encapsulated header of the ICAP message portion
|
||||||
reqStr = setEncapsulatedHeaderValue(reqStr, httpReqStr, httpRespStr)
|
reqStr = setEncapsulatedHeaderValue(reqStr, httpReqStr, httpRespStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,7 +336,7 @@ func toICAPRequest(req Request) ([]byte, error) {
|
|||||||
return data, nil
|
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) {
|
func toClientResponse(b *bufio.Reader) (Response, error) {
|
||||||
resp := Response{
|
resp := Response{
|
||||||
Header: make(map[string][]string),
|
Header: make(map[string][]string),
|
||||||
@@ -354,14 +345,13 @@ func toClientResponse(b *bufio.Reader) (Response, error) {
|
|||||||
scheme := ""
|
scheme := ""
|
||||||
httpMsg := ""
|
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
|
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 the current message line if the first line of the message portion(request line)
|
||||||
if isRequestLine(currentMsg) {
|
if isRequestLine(currentMsg) {
|
||||||
ss := strings.Split(currentMsg, " ")
|
ss := strings.Split(currentMsg, " ")
|
||||||
|
|
||||||
// must contain 3 words, for example, "ICAP/1.0 200 OK" or "GET /something HTTP/1.1"
|
// must contain 3 words, for example, "ICAP/1.0 200 OK" or "GET /something HTTP/1.1"
|
||||||
if len(ss) < 3 {
|
if len(ss) < 3 {
|
||||||
return Response{}, fmt.Errorf("%w: %s", ErrInvalidTCPMsg, currentMsg)
|
return Response{}, fmt.Errorf("%w: %s", ErrInvalidMsg, currentMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// preparing the scheme below
|
// preparing the scheme below
|
||||||
@@ -409,7 +399,8 @@ func toClientResponse(b *bufio.Reader) (Response, error) {
|
|||||||
httpMsg += strings.TrimSpace(currentMsg) + crlf
|
httpMsg += strings.TrimSpace(currentMsg) + crlf
|
||||||
bufferEmpty := b.Buffered() == 0
|
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 {
|
if currentMsg == crlf || bufferEmpty {
|
||||||
request, err := http.ReadRequest(bufio.NewReader(strings.NewReader(httpMsg)))
|
request, err := http.ReadRequest(bufio.NewReader(strings.NewReader(httpMsg)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Generated
Vendored
+4
-7
@@ -13,7 +13,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Request represents the icap client request data
|
// Request represents the icap client request data.
|
||||||
type Request struct {
|
type Request struct {
|
||||||
Method string
|
Method string
|
||||||
URL *url.URL
|
URL *url.URL
|
||||||
@@ -32,7 +32,6 @@ type Request struct {
|
|||||||
// todo: method iota
|
// todo: method iota
|
||||||
func NewRequest(ctx context.Context, method, urlStr string, httpReq *http.Request, httpResp *http.Response) (Request, error) {
|
func NewRequest(ctx context.Context, method, urlStr string, httpReq *http.Request, httpResp *http.Response) (Request, error) {
|
||||||
u, err := url.Parse(urlStr)
|
u, err := url.Parse(urlStr)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Request{}, err
|
return Request{}, err
|
||||||
}
|
}
|
||||||
@@ -128,8 +127,7 @@ func (r *Request) SetPreview(maxBytes int) (err error) {
|
|||||||
return err
|
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() {
|
func (r *Request) setDefaultRequestHeaders() {
|
||||||
if _, exists := r.Header["Allow"]; !exists {
|
if _, exists := r.Header["Allow"]; !exists {
|
||||||
r.Header.Add("Allow", "204") // assigning 204 by default if Allow not provided
|
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 {
|
func (r *Request) extendHeader(hdr http.Header) error {
|
||||||
for header, values := range hdr {
|
for header, values := range hdr {
|
||||||
|
|
||||||
if header == previewHeader && r.previewSet {
|
if header == previewHeader && r.previewSet {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -174,7 +171,7 @@ func (r *Request) extendHeader(hdr http.Header) error {
|
|||||||
return nil
|
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 {
|
func (r *Request) validate() error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
Generated
Vendored
-2
@@ -97,7 +97,6 @@ func respmodHandler(w icap.ResponseWriter, req *icap.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(status, nil, false)
|
w.WriteHeader(status, nil, false)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +133,6 @@ func reqmodHandler(w icap.ResponseWriter, req *icap.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(status, nil, false)
|
w.WriteHeader(status, nil, false)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Vendored
+3
-4
@@ -419,9 +419,6 @@ github.com/ebitengine/purego/internal/strings
|
|||||||
# github.com/egirna/icap v0.0.0-20181108071049-d5ee18bd70bc
|
# github.com/egirna/icap v0.0.0-20181108071049-d5ee18bd70bc
|
||||||
## explicit
|
## explicit
|
||||||
github.com/egirna/icap
|
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
|
# github.com/emirpasic/gods v1.18.1
|
||||||
## explicit; go 1.2
|
## explicit; go 1.2
|
||||||
github.com/emirpasic/gods/containers
|
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
|
||||||
github.com/open-policy-agent/opa/v1/util/decoding
|
github.com/open-policy-agent/opa/v1/util/decoding
|
||||||
github.com/open-policy-agent/opa/v1/version
|
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
|
# github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20250724122329-41ba6b191e76
|
||||||
## explicit; go 1.18
|
## explicit; go 1.18
|
||||||
github.com/opencloud-eu/libre-graph-api-go
|
github.com/opencloud-eu/libre-graph-api-go
|
||||||
@@ -2660,7 +2660,6 @@ sigs.k8s.io/yaml
|
|||||||
## explicit; go 1.13
|
## explicit; go 1.13
|
||||||
stash.kopano.io/kgol/rndm
|
stash.kopano.io/kgol/rndm
|
||||||
# github.com/studio-b12/gowebdav => github.com/kobergj/gowebdav v0.0.0-20250102091030-aa65266db202
|
# 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
|
# 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
|
# 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
|
# 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
|
||||||
|
|||||||
Reference in New Issue
Block a user