mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-01-07 12:50:21 -06:00
fix(antivirus): update icap-client library which fixes tcp socket reuse
This commit is contained in:
4
go.mod
4
go.mod
@@ -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
4
go.sum
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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.
|
||||
|
||||
19
vendor/github.com/egirna/icap-client/.travis.yml
generated
vendored
19
vendor/github.com/egirna/icap-client/.travis.yml
generated
vendored
@@ -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
vendor/github.com/egirna/icap-client/CONTRIBUTING.md
generated
vendored
9
vendor/github.com/egirna/icap-client/CONTRIBUTING.md
generated
vendored
@@ -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
vendor/github.com/egirna/icap-client/README.md
generated
vendored
84
vendor/github.com/egirna/icap-client/README.md
generated
vendored
@@ -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
vendor/github.com/egirna/icap-client/conn.go
generated
vendored
140
vendor/github.com/egirna/icap-client/conn.go
generated
vendored
@@ -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
vendor/github.com/opencloud-eu/icap-client/.editorconfig
generated
vendored
Normal file
20
vendor/github.com/opencloud-eu/icap-client/.editorconfig
generated
vendored
Normal 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
|
||||
1
vendor/github.com/opencloud-eu/icap-client/.gitattributes
generated
vendored
Normal file
1
vendor/github.com/opencloud-eu/icap-client/.gitattributes
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
||||
@@ -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
|
||||
76
vendor/github.com/opencloud-eu/icap-client/.golangci.yaml
generated
vendored
Normal file
76
vendor/github.com/opencloud-eu/icap-client/.golangci.yaml
generated
vendored
Normal 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
19
vendor/github.com/opencloud-eu/icap-client/Makefile
generated
vendored
Normal 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
18
vendor/github.com/opencloud-eu/icap-client/README.md
generated
vendored
Normal file
@@ -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).
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
109
vendor/github.com/opencloud-eu/icap-client/conn.go
generated
vendored
Normal 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
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -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
|
||||
|
||||
@@ -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
7
vendor/modules.txt
vendored
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user