mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2025-12-30 17:00:57 -06:00
enhancement: enable icap preview mode and fix client according to the spec (#8062)
* enhancement: enable icap preview mode and use a forked icap client which fixes tcp socket keepalive * enhancement: make use of human time for the icap timout config option * enhancement: update icap-client * enhancement: bump icap client library and deprecate ANTIVIRUS_ICAP_TIMEOUT env * chore: vendor icap library * enhancement: set preview size only if greater than 0 * Update services/antivirus/pkg/config/config.go Co-authored-by: Martin <github@diemattels.at> * enhancement: add changelog --------- Co-authored-by: Martin <github@diemattels.at>
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
Enhancement: Update antivirus service
|
||||
|
||||
We update the antivirus icap client library and optimize the antivirus scanning service.
|
||||
ANTIVIRUS_ICAP_TIMEOUT is now deprecated and ANTIVIRUS_ICAP_SCAN_TIMEOUT should be used instead.
|
||||
|
||||
ANTIVIRUS_ICAP_SCAN_TIMEOUT supports human durations like `1s`, `1m`, `1h` and `1d`.
|
||||
|
||||
https://github.com/owncloud/ocis/pull/8062
|
||||
https://github.com/owncloud/ocis/issues/6764
|
||||
2
go.mod
2
go.mod
@@ -353,3 +353,5 @@ require (
|
||||
replace github.com/go-micro/plugins/v4/store/nats-js-kv => github.com/kobergj/plugins/v4/store/nats-js-kv v0.0.0-20231207143248-4d424e3ae348
|
||||
|
||||
replace github.com/studio-b12/gowebdav => github.com/aduffeck/gowebdav v0.0.0-20231215102054-212d4a4374f6
|
||||
|
||||
replace github.com/egirna/icap-client => github.com/fschade/icap-client v0.0.0-20240105150744-9c2d8aff3ef2
|
||||
|
||||
4
go.sum
4
go.sum
@@ -1065,8 +1065,6 @@ github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1
|
||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||
github.com/egirna/icap v0.0.0-20181108071049-d5ee18bd70bc h1:6IxmRbXV8WXVkcYcTzkU219A3UZeNMX/e6X2sve1wXA=
|
||||
github.com/egirna/icap v0.0.0-20181108071049-d5ee18bd70bc/go.mod h1:FdVN2WHg7zOHhJ7kZQdDorfFhIfqZaHttjAzDDvAXHE=
|
||||
github.com/egirna/icap-client v0.1.1 h1:UURZRA7+36bBmMgJZHB+W4d3hfp20pDUA/QL794C0ck=
|
||||
github.com/egirna/icap-client v0.1.1/go.mod h1:6yHhnak1cKRyhDoRxnzlRKJbOTXPgh9Oe3tOs7Sq3vw=
|
||||
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
|
||||
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
@@ -1114,6 +1112,8 @@ github.com/foxcpp/go-mockdns v1.0.0 h1:7jBqxd3WDWwi/6WhDvacvH1XsN3rOLXyHM1uhvIx6
|
||||
github.com/foxcpp/go-mockdns v1.0.0/go.mod h1:lgRN6+KxQBawyIghpnl5CezHFGS9VLzvtVlwxvzXTQ4=
|
||||
github.com/frankban/quicktest v1.14.0 h1:+cqqvzZV87b4adx/5ayVOaYZ2CrvM4ejQvUdBzPPUss=
|
||||
github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og=
|
||||
github.com/fschade/icap-client v0.0.0-20240105150744-9c2d8aff3ef2 h1:PJERPsceXsS4uTJuDvHy/4rgrZyZWttbKesaacKmXiI=
|
||||
github.com/fschade/icap-client v0.0.0-20240105150744-9c2d8aff3ef2/go.mod h1:Curjbe9P7SKWAtoXuu/huL8VnqzuBzetEpEPt9TLToE=
|
||||
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.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
|
||||
@@ -2,6 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Config combines all available configuration parts.
|
||||
@@ -70,9 +71,10 @@ type ClamAV struct {
|
||||
Socket string `yaml:"socket" env:"ANTIVIRUS_CLAMAV_SOCKET" desc:"The socket clamav is running on. Note the default value is an example which needs adaption according your OS."`
|
||||
}
|
||||
|
||||
// ICAP provides configuration option for ICAP
|
||||
// ICAP provides configuration options for icap
|
||||
type ICAP struct {
|
||||
Timeout int64 `yaml:"timeout" env:"ANTIVIRUS_ICAP_TIMEOUT" desc:"Timeout for the ICAP client."`
|
||||
URL string `yaml:"url" env:"ANTIVIRUS_ICAP_URL" desc:"URL of the ICAP server."`
|
||||
Service string `yaml:"service" env:"ANTIVIRUS_ICAP_SERVICE" desc:"The name of the ICAP service."`
|
||||
DeprecatedTimeout int64 `yaml:"timeout" env:"ANTIVIRUS_ICAP_TIMEOUT" desc:"Timeout for the ICAP client." deprecationVersion:"5.0" removalVersion:"6.0" deprecationInfo:"Changing the envvar type for consistency reasons." deprecationReplacement:"ANTIVIRUS_ICAP_SCAN_TIMEOUT"`
|
||||
Timeout time.Duration `yaml:"scan_timeout" env:"ANTIVIRUS_ICAP_SCAN_TIMEOUT" desc:"Scan timeout for the ICAP client. Defaults to '5m' (5 minutes). See the Environment Variable Types description for more details."`
|
||||
URL string `yaml:"url" env:"ANTIVIRUS_ICAP_URL" desc:"URL of the ICAP server."`
|
||||
Service string `yaml:"service" env:"ANTIVIRUS_ICAP_SERVICE" desc:"The name of the ICAP service."`
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package defaults
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/owncloud/ocis/v2/services/antivirus/pkg/config"
|
||||
)
|
||||
|
||||
@@ -35,7 +37,7 @@ func DefaultConfig() *config.Config {
|
||||
ICAP: config.ICAP{
|
||||
URL: "icap://127.0.0.1:1344",
|
||||
Service: "avscan",
|
||||
Timeout: 300,
|
||||
Timeout: 5 * time.Minute,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package parser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
ociscfg "github.com/owncloud/ocis/v2/ocis-pkg/config"
|
||||
"github.com/owncloud/ocis/v2/services/antivirus/pkg/config"
|
||||
@@ -34,5 +36,10 @@ func ParseConfig(cfg *config.Config) error {
|
||||
|
||||
// Validate validates our little config
|
||||
func Validate(cfg *config.Config) error {
|
||||
if cfg.Scanner.ICAP.DeprecatedTimeout != 0 {
|
||||
cfg.Scanner.ICAP.Timeout = time.Duration(cfg.Scanner.ICAP.DeprecatedTimeout) * time.Second
|
||||
fmt.Println("ANTIVIRUS_ICAP_TIMEOUT is deprecated, use ANTIVIRUS_ICAP_SCAN_TIMEOUT instead")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package scanners
|
||||
|
||||
import (
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/dutchcoders/go-clamd"
|
||||
@@ -20,16 +19,16 @@ type ClamAV struct {
|
||||
}
|
||||
|
||||
// Scan to fulfill Scanner interface
|
||||
func (s ClamAV) Scan(file io.Reader) (ScanResult, error) {
|
||||
ch, err := s.clamd.ScanStream(file, make(chan bool))
|
||||
func (s ClamAV) Scan(in Input) (Result, error) {
|
||||
ch, err := s.clamd.ScanStream(in.Body, make(chan bool))
|
||||
if err != nil {
|
||||
return ScanResult{}, err
|
||||
return Result{}, err
|
||||
}
|
||||
|
||||
r := <-ch
|
||||
return ScanResult{
|
||||
return Result{
|
||||
Infected: r.Status == clamd.RES_FOUND,
|
||||
Description: r.Description,
|
||||
Scantime: time.Now(),
|
||||
ScanTime: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
package scanners
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/cs3org/reva/v2/pkg/mime"
|
||||
ic "github.com/egirna/icap-client"
|
||||
)
|
||||
|
||||
@@ -21,49 +23,71 @@ func NewICAP(icapURL string, icapService string, timeout time.Duration) (ICAP, e
|
||||
endpoint.Scheme = "icap"
|
||||
endpoint.Path = icapService
|
||||
|
||||
return ICAP{
|
||||
client: &ic.Client{
|
||||
Timeout: timeout,
|
||||
},
|
||||
endpoint: endpoint.String(),
|
||||
}, nil
|
||||
client, err := ic.NewClient(
|
||||
ic.WithICAPConnectionTimeout(timeout),
|
||||
)
|
||||
|
||||
return ICAP{client: client, url: *endpoint}, nil
|
||||
}
|
||||
|
||||
// ICAP is a Scanner talking to an ICAP server
|
||||
// ICAP is responsible for scanning files using an ICAP server
|
||||
type ICAP struct {
|
||||
client *ic.Client
|
||||
endpoint string
|
||||
client ic.Client
|
||||
url url.URL
|
||||
}
|
||||
|
||||
// Scan to fulfill Scanner interface
|
||||
func (s ICAP) Scan(file io.Reader) (ScanResult, error) {
|
||||
sr := ScanResult{}
|
||||
// Scan scans a file using the ICAP server
|
||||
func (s ICAP) Scan(in Input) (Result, error) {
|
||||
ctx := context.TODO()
|
||||
result := Result{}
|
||||
|
||||
httpReq, err := http.NewRequest(http.MethodGet, "http://localhost", file)
|
||||
httpReq, err := http.NewRequest(http.MethodPost, in.Url, in.Body)
|
||||
if err != nil {
|
||||
return sr, err
|
||||
return result, err
|
||||
}
|
||||
|
||||
req, err := ic.NewRequest(ic.MethodREQMOD, s.endpoint, httpReq, nil)
|
||||
if err != nil {
|
||||
return sr, err
|
||||
httpReq.ContentLength = in.Size
|
||||
if mt := mime.Detect(path.Ext(in.Name) == "", in.Name); mt != "" {
|
||||
httpReq.Header.Set("Content-Type", mt)
|
||||
}
|
||||
|
||||
resp, err := s.client.Do(req)
|
||||
optReq, err := ic.NewRequest(ctx, ic.MethodOPTIONS, s.url.String(), nil, nil)
|
||||
if err != nil {
|
||||
return sr, err
|
||||
return result, err
|
||||
}
|
||||
|
||||
// TODO: make header configurable. See oc10 documentation: https://doc.owncloud.com/server/10.12/admin_manual/configuration/server/virus-scanner-support.html
|
||||
if data, infected := resp.Header["X-Infection-Found"]; infected {
|
||||
sr.Infected = infected
|
||||
re := regexp.MustCompile(`Threat=(.*);`)
|
||||
match := re.FindStringSubmatch(fmt.Sprint(data))
|
||||
optRes, err := s.client.Do(optReq)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
if len(match) > 1 {
|
||||
sr.Description = match[1]
|
||||
req, err := ic.NewRequest(ctx, ic.MethodREQMOD, s.url.String(), httpReq, nil)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
if optRes.PreviewBytes > 0 {
|
||||
err = req.SetPreview(optRes.PreviewBytes)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
}
|
||||
|
||||
return sr, nil
|
||||
res, err := s.client.Do(req)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
// TODO: make header configurable. See oc10 documentation: https://doc.owncloud.com/server/10.12/admin_manual/configuration/server/virus-scanner-support.html
|
||||
if data, infected := res.Header["X-Infection-Found"]; infected {
|
||||
result.Infected = infected
|
||||
|
||||
match := regexp.MustCompile(`Threat=(.*);`).FindStringSubmatch(fmt.Sprint(data))
|
||||
|
||||
if len(match) > 1 {
|
||||
result.Description = match[1]
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -1,34 +1,21 @@
|
||||
package scanners
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/owncloud/ocis/v2/services/antivirus/pkg/config"
|
||||
)
|
||||
|
||||
// ScanResult is the common scan result to all scanners
|
||||
type ScanResult struct {
|
||||
// The Result is the common scan result to all scanners
|
||||
type Result struct {
|
||||
Infected bool
|
||||
Scantime time.Time
|
||||
ScanTime time.Time
|
||||
Description string
|
||||
}
|
||||
|
||||
// Scanner is an abstraction for the actual virus scan
|
||||
type Scanner interface {
|
||||
Scan(file io.Reader) (ScanResult, error)
|
||||
}
|
||||
|
||||
// New returns a new scanner from config
|
||||
func New(c *config.Config) (Scanner, error) {
|
||||
switch c.Scanner.Type {
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown av scanner: '%s'", c.Scanner.Type)
|
||||
case "clamav":
|
||||
return NewClamAV(c.Scanner.ClamAV.Socket), nil
|
||||
case "icap":
|
||||
return NewICAP(c.Scanner.ICAP.URL, c.Scanner.ICAP.Service, time.Duration(c.Scanner.ICAP.Timeout)*time.Second)
|
||||
}
|
||||
|
||||
// The Input is the common input to all scanners
|
||||
type Input struct {
|
||||
Body io.Reader
|
||||
Size int64
|
||||
Url string
|
||||
Name string
|
||||
}
|
||||
|
||||
@@ -16,14 +16,15 @@ import (
|
||||
"github.com/cs3org/reva/v2/pkg/events"
|
||||
"github.com/cs3org/reva/v2/pkg/events/stream"
|
||||
"github.com/cs3org/reva/v2/pkg/rhttp"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/v2/services/antivirus/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/services/antivirus/pkg/scanners"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrFatal is returned when a fatal error occurs and we want to exit.
|
||||
// ErrFatal is returned when a fatal error occurs, and we want to exit.
|
||||
ErrFatal = errors.New("fatal error")
|
||||
// ErrEvent is returned when something went wrong with a specific event.
|
||||
ErrEvent = errors.New("event error")
|
||||
@@ -31,18 +32,27 @@ var (
|
||||
|
||||
// Scanner is an abstraction for the actual virus scan
|
||||
type Scanner interface {
|
||||
Scan(file io.Reader) (scanners.ScanResult, error)
|
||||
Scan(body scanners.Input) (scanners.Result, error)
|
||||
}
|
||||
|
||||
// NewAntivirus returns a service implementation for Service.
|
||||
func NewAntivirus(c *config.Config, l log.Logger, tp trace.TracerProvider) (Antivirus, error) {
|
||||
av := Antivirus{c: c, l: l, tp: tp, client: rhttp.GetHTTPClient(rhttp.Insecure(true))}
|
||||
|
||||
var scanner Scanner
|
||||
var err error
|
||||
av.s, err = scanners.New(c)
|
||||
if err != nil {
|
||||
return av, err
|
||||
switch c.Scanner.Type {
|
||||
default:
|
||||
return Antivirus{}, fmt.Errorf("unknown av scanner: '%s'", c.Scanner.Type)
|
||||
case "clamav":
|
||||
scanner = scanners.NewClamAV(c.Scanner.ClamAV.Socket)
|
||||
case "icap":
|
||||
scanner, err = scanners.NewICAP(c.Scanner.ICAP.URL, c.Scanner.ICAP.Service, c.Scanner.ICAP.Timeout)
|
||||
}
|
||||
if err != nil {
|
||||
return Antivirus{}, err
|
||||
}
|
||||
|
||||
av := Antivirus{c: c, l: l, tp: tp, s: scanner, client: rhttp.GetHTTPClient(rhttp.Insecure(true))}
|
||||
|
||||
switch o := events.PostprocessingOutcome(c.InfectedFileHandling); o {
|
||||
case events.PPOutcomeContinue, events.PPOutcomeAbort, events.PPOutcomeDelete:
|
||||
@@ -199,11 +209,11 @@ func (av Antivirus) processEvent(e events.Event, s events.Publisher) error {
|
||||
}
|
||||
|
||||
// process the scan
|
||||
func (av Antivirus) process(ev events.StartPostprocessingStep) (scanners.ScanResult, error) {
|
||||
func (av Antivirus) process(ev events.StartPostprocessingStep) (scanners.Result, error) {
|
||||
if ev.Filesize == 0 || (0 < av.m && av.m < ev.Filesize) {
|
||||
av.l.Info().Str("uploadid", ev.UploadID).Uint64("limit", av.m).Uint64("filesize", ev.Filesize).Msg("Skipping file to be virus scanned because its file size is higher than the defined limit.")
|
||||
return scanners.ScanResult{
|
||||
Scantime: time.Now(),
|
||||
return scanners.Result{
|
||||
ScanTime: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -218,12 +228,12 @@ func (av Antivirus) process(ev events.StartPostprocessingStep) (scanners.ScanRes
|
||||
}
|
||||
if err != nil {
|
||||
av.l.Error().Err(err).Str("uploadid", ev.UploadID).Msg("error downloading file")
|
||||
return scanners.ScanResult{}, err
|
||||
return scanners.Result{}, err
|
||||
}
|
||||
defer rrc.Close()
|
||||
av.l.Debug().Str("uploadid", ev.UploadID).Msg("Downloaded file successfully, starting virusscan")
|
||||
|
||||
res, err := av.s.Scan(rrc)
|
||||
res, err := av.s.Scan(scanners.Input{Body: rrc, Size: int64(ev.Filesize), Url: ev.URL, Name: ev.Filename})
|
||||
if err != nil {
|
||||
av.l.Error().Err(err).Str("uploadid", ev.UploadID).Msg("error scanning file")
|
||||
}
|
||||
|
||||
2
vendor/github.com/egirna/icap-client/.travis.yml
generated
vendored
2
vendor/github.com/egirna/icap-client/.travis.yml
generated
vendored
@@ -6,7 +6,7 @@ env:
|
||||
- GO111MODULE=ON
|
||||
|
||||
go:
|
||||
- 1.12.x
|
||||
- 1.21.x
|
||||
|
||||
git:
|
||||
depth: 1
|
||||
|
||||
51
vendor/github.com/egirna/icap-client/README.md
generated
vendored
51
vendor/github.com/egirna/icap-client/README.md
generated
vendored
@@ -19,64 +19,53 @@ go get -u github.com/egirna/icap-client
|
||||
|
||||
```go
|
||||
import ic "github.com/egirna/icap-client"
|
||||
|
||||
```
|
||||
|
||||
**Making a simple RESPMOD call**
|
||||
|
||||
```go
|
||||
|
||||
req, err := ic.NewRequest(ic.MethodRESPMOD, "icap://<host>:<port>/<path>", httpReq, httpResp)
|
||||
|
||||
req, err := ic.NewRequest(context.Background(), MethodRESPMOD, "icap://<host>:<port>/<path>", httpReq, httpResp)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
client := &ic.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: ``httpReq`` & ``httpResp`` here are ``*http.Response`` & ``*http.Request`` respectively
|
||||
**Note**: `httpReq` & `httpResp` here are `*http.Response` & `*http.Request` respectively
|
||||
|
||||
**Setting preview obtained from OPTIONS call**
|
||||
|
||||
```go
|
||||
|
||||
optReq, err := ic.NewRequest(ic.MethodOPTIONS, "icap://<host>:<port>/<path>", nil, nil)
|
||||
|
||||
req, err := ic.NewRequest(context.Background(), ic.MethodOPTIONS, "icap://<host>:<port>/<path>", nil, nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return
|
||||
}
|
||||
|
||||
client := &ic.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
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)
|
||||
|
||||
```
|
||||
|
||||
**DEBUG Mode**
|
||||
|
||||
Turn on debug mode to inspect detailed & verbose logs to debug your code during development
|
||||
|
||||
```go
|
||||
ic.SetDebugMode(true)
|
||||
|
||||
```
|
||||
|
||||
By default the icap-client will dump the debugging logs to the standard output(stdout), but you can always add your custom writer
|
||||
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)
|
||||
@@ -88,7 +77,7 @@ For more details, see the [docs](https://godoc.org/github.com/egirna/icap-client
|
||||
|
||||
### Contributing
|
||||
|
||||
This package is still WIP, so totally open to suggestions. See the contributions guide [here](CONTRIBUTING.md).
|
||||
This package is still WIP, so totally open to suggestions. See the contribution guide [here](CONTRIBUTING.md).
|
||||
|
||||
### License
|
||||
|
||||
|
||||
150
vendor/github.com/egirna/icap-client/client.go
generated
vendored
150
vendor/github.com/egirna/icap-client/client.go
generated
vendored
@@ -1,115 +1,89 @@
|
||||
package icapclient
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Client represents the icap client who makes the icap server calls
|
||||
type Client struct {
|
||||
scktDriver *Driver
|
||||
Timeout time.Duration
|
||||
conn Conn
|
||||
}
|
||||
|
||||
// Do makes does everything required to make a call to the ICAP server
|
||||
func (c *Client) Do(req *Request) (*Response, error) {
|
||||
|
||||
if c.scktDriver == nil { // create a new socket driver if one wasn't explicitly created
|
||||
port, err := strconv.Atoi(req.URL.Port())
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.scktDriver = NewDriver(req.URL.Hostname(), port)
|
||||
// NewClient creates a new icap client
|
||||
func NewClient(options ...ConfigOption) (Client, error) {
|
||||
config := DefaultConfig()
|
||||
for _, option := range options {
|
||||
option(&config)
|
||||
}
|
||||
|
||||
c.setDefaultTimeouts() // assinging default timeouts if not set already
|
||||
|
||||
if req.ctx != nil { // connect with the given context if context is set
|
||||
if err := c.scktDriver.ConnectWithContext(*req.ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
if err := c.scktDriver.Connect(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
defer c.scktDriver.Close() // closing the socket connection
|
||||
|
||||
req.SetDefaultRequestHeaders() // assigning default headers if not set already
|
||||
|
||||
logDebug("The request headers: ")
|
||||
dumpDebug(req.Header)
|
||||
|
||||
d, err := DumpRequest(req) // getting the byte representation of the ICAP request
|
||||
|
||||
conn, err := NewICAPConn(config.ICAPConn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return Client{}, err
|
||||
}
|
||||
|
||||
if err := c.scktDriver.Send(d); err != nil { // sending the entire TCP message of the ICAP client to the server connected
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := c.scktDriver.Receive() // taking the response
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusContinue && !req.bodyFittedInPreview && req.previewSet { // this block suggests that the ICAP request contained preview body bytes and whole body did not fit in the preview, so the serber responded with 100 Continue and the client is to send the remaining body bytes only
|
||||
logDebug("Making request for the rest of the remaining body bytes after preview, as received 100 Continue from the server...")
|
||||
return c.DoRemaining(req)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
return Client{
|
||||
conn: conn,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DoRemaining requests an ICAP server with the remaining body bytes which did not fit in the preview in the original request
|
||||
func (c *Client) DoRemaining(req *Request) (*Response, error) {
|
||||
// Do is the main function of the client that makes the ICAP request
|
||||
func (c *Client) Do(req Request) (Response, error) {
|
||||
var err error
|
||||
|
||||
// establish connection to the icap server
|
||||
err = c.conn.Connect(req.ctx, req.URL.Host)
|
||||
if err != nil {
|
||||
return Response{}, err
|
||||
}
|
||||
defer func() {
|
||||
err = errors.Join(err, c.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)
|
||||
if err != nil {
|
||||
return Response{}, err
|
||||
}
|
||||
|
||||
resp, err := toClientResponse(bufio.NewReader(strings.NewReader(string(dataRes))))
|
||||
if err != nil {
|
||||
return Response{}, err
|
||||
}
|
||||
|
||||
// check if the message is fully done scanning or if it needs to be sent another chunk
|
||||
done := !(resp.StatusCode == http.StatusContinue && !req.bodyFittedInPreview && req.previewSet)
|
||||
if done {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// get the remaining body bytes
|
||||
data := req.remainingPreviewBytes
|
||||
|
||||
if !bodyAlreadyChunked(string(data)) { // if the body is not already chunke, then add the basic hexa body bytes notation
|
||||
ds := string(data)
|
||||
addHexaBodyByteNotations(&ds)
|
||||
data = []byte(ds)
|
||||
if !bodyIsChunked(string(data)) {
|
||||
data = []byte(addHexBodyByteNotations(string(data)))
|
||||
}
|
||||
|
||||
if err := c.scktDriver.Send(data); err != nil {
|
||||
return nil, err
|
||||
// hydrate the icap message with closing doubleCRLF suffix
|
||||
if !bytes.HasSuffix(data, []byte(doubleCRLF)) {
|
||||
data = append(data, []byte(crlf)...)
|
||||
}
|
||||
|
||||
resp, err := c.scktDriver.Receive()
|
||||
|
||||
// send the remaining body bytes to the server
|
||||
dataRes, err = c.conn.Send(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return Response{}, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// SetDriver sets a new socket driver with the client
|
||||
func (c *Client) SetDriver(d *Driver) {
|
||||
c.scktDriver = d
|
||||
}
|
||||
|
||||
func (c *Client) setDefaultTimeouts() {
|
||||
if c.Timeout == 0 {
|
||||
c.Timeout = defaultTimeout
|
||||
}
|
||||
|
||||
if c.scktDriver.DialerTimeout == 0 {
|
||||
c.scktDriver.DialerTimeout = c.Timeout
|
||||
}
|
||||
|
||||
if c.scktDriver.ReadTimeout == 0 {
|
||||
c.scktDriver.ReadTimeout = c.Timeout
|
||||
}
|
||||
|
||||
if c.scktDriver.WriteTimeout == 0 {
|
||||
c.scktDriver.WriteTimeout = c.Timeout
|
||||
}
|
||||
return toClientResponse(bufio.NewReader(strings.NewReader(string(dataRes))))
|
||||
}
|
||||
|
||||
33
vendor/github.com/egirna/icap-client/config.go
generated
vendored
Normal file
33
vendor/github.com/egirna/icap-client/config.go
generated
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
package icapclient
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// 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
|
||||
func DefaultConfig() Config {
|
||||
return Config{
|
||||
ICAPConn: ICAPConnConfig{
|
||||
Timeout: 15 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ConfigOption is a function that configures the icap client
|
||||
type ConfigOption func(*Config)
|
||||
|
||||
// WithICAPConnectionTimeout sets the timeout for the connection to the icap server
|
||||
func WithICAPConnectionTimeout(timeout time.Duration) ConfigOption {
|
||||
return func(cfg *Config) {
|
||||
if timeout <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
cfg.ICAPConn.Timeout = timeout
|
||||
}
|
||||
}
|
||||
141
vendor/github.com/egirna/icap-client/conn.go
generated
vendored
Normal file
141
vendor/github.com/egirna/icap-client/conn.go
generated
vendored
Normal file
@@ -0,0 +1,141 @@
|
||||
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
|
||||
// fixMe: still unclear why this is happening, find out and fix it
|
||||
if bytes.Equal(data, []byte(icap100ContinueMsg)) {
|
||||
break
|
||||
}
|
||||
|
||||
// EOF detected, 0 Double crlf indicates the end of the message
|
||||
if bytes.HasSuffix(data, []byte("0\r\n\r\n")) {
|
||||
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 }
|
||||
57
vendor/github.com/egirna/icap-client/consts.go
generated
vendored
57
vendor/github.com/egirna/icap-client/consts.go
generated
vendored
@@ -1,57 +0,0 @@
|
||||
package icapclient
|
||||
|
||||
import "time"
|
||||
|
||||
// the icap request methods
|
||||
const (
|
||||
MethodOPTIONS = "OPTIONS"
|
||||
MethodRESPMOD = "RESPMOD"
|
||||
MethodREQMOD = "REQMOD"
|
||||
)
|
||||
|
||||
// the error messages
|
||||
const (
|
||||
ErrInvalidScheme = "the url scheme must be icap://"
|
||||
ErrMethodNotRegistered = "the requested method is not registered"
|
||||
ErrInvalidHost = "the requested host is invalid"
|
||||
ErrConnectionNotOpen = "no open connection to close"
|
||||
ErrInvalidTCPMsg = "invalid tcp message"
|
||||
ErrREQMODWithNoReq = "http request cannot be nil for method REQMOD"
|
||||
ErrREQMODWithResp = "http response must be nil for method REQMOD"
|
||||
ErrRESPMODWithNoResp = "http response cannot be nil for method RESPMOD"
|
||||
)
|
||||
|
||||
// general constants required for the package
|
||||
const (
|
||||
SchemeICAP = "icap"
|
||||
ICAPVersion = "ICAP/1.0"
|
||||
HTTPVersion = "HTTP/1.1"
|
||||
SchemeHTTPReq = "http_request"
|
||||
SchemeHTTPResp = "http_response"
|
||||
CRLF = "\r\n"
|
||||
DoubleCRLF = "\r\n\r\n"
|
||||
LF = "\n"
|
||||
bodyEndIndicator = CRLF + "0" + CRLF
|
||||
fullBodyEndIndicatorPreviewMode = "; ieof" + DoubleCRLF
|
||||
icap100ContinueMsg = "ICAP/1.0 100 Continue" + DoubleCRLF
|
||||
icap204NoModsMsg = "ICAP/1.0 204 No modifications"
|
||||
defaultChunkLength = 512
|
||||
defaultTimeout = 15 * time.Second
|
||||
)
|
||||
|
||||
// Common ICAP headers
|
||||
const (
|
||||
PreviewHeader = "Preview"
|
||||
MethodsHeader = "Methods"
|
||||
AllowHeader = "Allow"
|
||||
EncapsulatedHeader = "Encapsulated"
|
||||
TransferPreviewHeader = "Transfer-Preview"
|
||||
ServiceHeader = "Service"
|
||||
ISTagHeader = "ISTag"
|
||||
OptBodyTypeHeader = "Opt-body-type"
|
||||
MaxConnectionsHeader = "Max-Connections"
|
||||
OptionsTTLHeader = "Options-TTL"
|
||||
ServiceIDHeader = "Service-ID"
|
||||
TransferIgnoreHeader = "Transfer-Ignore"
|
||||
TransferCompleteHeader = "Transfer-Complete"
|
||||
)
|
||||
55
vendor/github.com/egirna/icap-client/debug.go
generated
vendored
55
vendor/github.com/egirna/icap-client/debug.go
generated
vendored
@@ -1,55 +0,0 @@
|
||||
package icapclient
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
)
|
||||
|
||||
// the debug mode determiner & the writer to the write the debug output to
|
||||
var (
|
||||
DEBUG = false
|
||||
debugWriter io.Writer
|
||||
logger *log.Logger
|
||||
)
|
||||
|
||||
const (
|
||||
debugPrefix = "icap-client says: "
|
||||
)
|
||||
|
||||
// SetDebugMode sets the debug mode for the entire package depending on the bool
|
||||
func SetDebugMode(debug bool) {
|
||||
DEBUG = debug
|
||||
|
||||
if DEBUG { // setting os.Stdout as the default debug writer if debug mode is enabled & also the debug prefix
|
||||
debugWriter = os.Stdout
|
||||
logger = log.New(debugWriter, debugPrefix, log.LstdFlags)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// SetDebugOutput sets writer to write the debug outputs (default: os.Stdout)
|
||||
func SetDebugOutput(w io.Writer) {
|
||||
debugWriter = w
|
||||
logger.SetOutput(debugWriter)
|
||||
}
|
||||
|
||||
func logDebug(a ...interface{}) {
|
||||
if DEBUG {
|
||||
logger.Println(a...)
|
||||
}
|
||||
}
|
||||
|
||||
func logfDebug(s string, a ...interface{}) {
|
||||
if DEBUG {
|
||||
logger.Printf(s, a...)
|
||||
}
|
||||
}
|
||||
|
||||
func dumpDebug(a ...interface{}) {
|
||||
if DEBUG {
|
||||
spew.Fdump(debugWriter, a...)
|
||||
}
|
||||
}
|
||||
71
vendor/github.com/egirna/icap-client/doc.go
generated
vendored
71
vendor/github.com/egirna/icap-client/doc.go
generated
vendored
@@ -1,73 +1,4 @@
|
||||
//Package icapclient is a client package for the ICAP protocol
|
||||
// Package icapclient is a client package for the ICAP protocol
|
||||
//
|
||||
// Here is a basic example:
|
||||
// package main
|
||||
//
|
||||
// import (
|
||||
// "fmt"
|
||||
// "log"
|
||||
// "net/http"
|
||||
// "time"
|
||||
//
|
||||
// ic "github.com/egirna/icap-client"
|
||||
// )
|
||||
//
|
||||
// func main() {
|
||||
// /* preparing the http request required for the RESPMOD */
|
||||
// httpReq, err := http.NewRequest(http.MethodGet, "http://localhost:8000/sample.pdf", nil)
|
||||
//
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
//
|
||||
// /* making the http client & making the request call to get the response needed for the icap RESPMOD call */
|
||||
// httpClient := &http.Client{}
|
||||
//
|
||||
// httpResp, err := httpClient.Do(httpReq)
|
||||
//
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
//
|
||||
// /* making a icap request with OPTIONS method */
|
||||
// optReq, err := ic.NewRequest(ic.MethodOPTIONS, "icap://127.0.0.1:1344/respmod", nil, nil)
|
||||
//
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// /* making the icap client responsible for making the requests */
|
||||
// client := &ic.Client{
|
||||
// Timeout: 5 * time.Second,
|
||||
// }
|
||||
//
|
||||
// /* making the OPTIONS request call */
|
||||
// optResp, err := client.Do(optReq)
|
||||
//
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// /* making a icap request with RESPMOD method */
|
||||
// req, err := ic.NewRequest(ic.MethodRESPMOD, "icap://127.0.0.1:1344/respmod", httpReq, httpResp)
|
||||
//
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
//
|
||||
// req.SetPreview(optResp.PreviewBytes) // setting the preview bytes obtained from the OPTIONS call
|
||||
//
|
||||
// /* making the RESPMOD request call */
|
||||
// resp, err := client.Do(req)
|
||||
//
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
//
|
||||
// fmt.Println(resp.StatusCode)
|
||||
//
|
||||
// }
|
||||
// See https://github.com/egirna/icap-client/examples.
|
||||
package icapclient
|
||||
|
||||
99
vendor/github.com/egirna/icap-client/driver.go
generated
vendored
99
vendor/github.com/egirna/icap-client/driver.go
generated
vendored
@@ -1,99 +0,0 @@
|
||||
package icapclient
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Driver os the one responsible for driving the transport layer operations
|
||||
type Driver struct {
|
||||
Host string
|
||||
Port int
|
||||
DialerTimeout time.Duration
|
||||
ReadTimeout time.Duration
|
||||
WriteTimeout time.Duration
|
||||
tcp *transport
|
||||
}
|
||||
|
||||
// NewDriver is the factory function for Driver
|
||||
func NewDriver(host string, port int) *Driver {
|
||||
return &Driver{
|
||||
Host: host,
|
||||
Port: port,
|
||||
}
|
||||
}
|
||||
|
||||
// Connect fires up a tcp socket connection with the icap server
|
||||
func (d *Driver) Connect() error {
|
||||
|
||||
d.tcp = &transport{
|
||||
network: "tcp",
|
||||
addr: fmt.Sprintf("%s:%d", d.Host, d.Port),
|
||||
timeout: d.DialerTimeout,
|
||||
readTimeout: d.ReadTimeout,
|
||||
writeTimeout: d.WriteTimeout,
|
||||
}
|
||||
|
||||
return d.tcp.dial()
|
||||
}
|
||||
|
||||
// ConnectWithContext connects to the server satisfying the context
|
||||
func (d *Driver) ConnectWithContext(ctx context.Context) error {
|
||||
d.tcp = &transport{
|
||||
network: "tcp",
|
||||
addr: fmt.Sprintf("%s:%d", d.Host, d.Port),
|
||||
timeout: d.DialerTimeout,
|
||||
readTimeout: d.ReadTimeout,
|
||||
writeTimeout: d.WriteTimeout,
|
||||
}
|
||||
|
||||
return d.tcp.dialWithContext(ctx)
|
||||
}
|
||||
|
||||
// Close closes the socket connection
|
||||
func (d *Driver) Close() error {
|
||||
if d.tcp == nil {
|
||||
|
||||
return errors.New(ErrConnectionNotOpen)
|
||||
}
|
||||
|
||||
return d.tcp.close()
|
||||
}
|
||||
|
||||
// Send sends a request to the icap server
|
||||
func (d *Driver) Send(data []byte) error {
|
||||
|
||||
_, err := d.tcp.write(data)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// Receive returns the respone from the tcp socket connection
|
||||
func (d *Driver) Receive() (*Response, error) {
|
||||
|
||||
msg, err := d.tcp.read()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := ReadResponse(bufio.NewReader(strings.NewReader(msg)))
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logDebug("The final *ic.Response from tcp messages...")
|
||||
dumpDebug(resp)
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
124
vendor/github.com/egirna/icap-client/header.go
generated
vendored
124
vendor/github.com/egirna/icap-client/header.go
generated
vendored
@@ -1,124 +0,0 @@
|
||||
package icapclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// SetPreview sets the preview bytes in the icap header
|
||||
func (r *Request) SetPreview(maxBytes int) error {
|
||||
|
||||
bodyBytes := []byte{}
|
||||
|
||||
previewBytes := 0
|
||||
|
||||
// receiving the body bites to determine the preview bytes depending on the request ICAP method
|
||||
|
||||
if r.Method == MethodREQMOD {
|
||||
if r.HTTPRequest == nil {
|
||||
return nil
|
||||
}
|
||||
if r.HTTPRequest.Body != nil {
|
||||
var err error
|
||||
bodyBytes, err = ioutil.ReadAll(r.HTTPRequest.Body)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer r.HTTPRequest.Body.Close()
|
||||
}
|
||||
}
|
||||
|
||||
if r.Method == MethodRESPMOD {
|
||||
if r.HTTPResponse == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if r.HTTPResponse.Body != nil {
|
||||
var err error
|
||||
bodyBytes, err = ioutil.ReadAll(r.HTTPResponse.Body)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer r.HTTPResponse.Body.Close()
|
||||
}
|
||||
}
|
||||
|
||||
previewBytes = len(bodyBytes)
|
||||
|
||||
if previewBytes > 0 { // if the preview byte is 0 or less, there is no question of the body fitting insides
|
||||
r.bodyFittedInPreview = true
|
||||
}
|
||||
|
||||
if previewBytes > maxBytes { // if the preview bytes is greater than what was mentioned by the ICAP Server(did not fit in the body)
|
||||
previewBytes = maxBytes
|
||||
r.bodyFittedInPreview = false
|
||||
r.remainingPreviewBytes = bodyBytes[maxBytes:] // storing the rest of the body byte which were not sent as preview for further operations
|
||||
}
|
||||
|
||||
// returning the body back to the http message depending on the request method
|
||||
|
||||
if r.Method == MethodREQMOD {
|
||||
r.HTTPRequest.Body = ioutil.NopCloser(bytes.NewReader(bodyBytes))
|
||||
}
|
||||
|
||||
if r.Method == MethodRESPMOD {
|
||||
r.HTTPResponse.Body = ioutil.NopCloser(bytes.NewReader(bodyBytes))
|
||||
}
|
||||
|
||||
// finally assinging the preview informations including setting the header
|
||||
|
||||
r.Header.Set("Preview", strconv.Itoa(previewBytes))
|
||||
r.PreviewBytes = previewBytes
|
||||
r.previewSet = true
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// SetDefaultRequestHeaders assigns some of the headers with its default value if they are not set already
|
||||
func (r *Request) SetDefaultRequestHeaders() {
|
||||
if _, exists := r.Header["Allow"]; !exists {
|
||||
r.Header.Add("Allow", "204") // assigning 204 by default if Allow not provided
|
||||
}
|
||||
if _, exists := r.Header["Host"]; !exists {
|
||||
hostName, _ := os.Hostname()
|
||||
r.Header.Add("Host", hostName)
|
||||
}
|
||||
}
|
||||
|
||||
// ExtendHeader extends the current ICAP Request header with a new header
|
||||
func (r *Request) ExtendHeader(hdr http.Header) error {
|
||||
for header, values := range hdr {
|
||||
|
||||
if header == PreviewHeader && r.previewSet {
|
||||
continue
|
||||
}
|
||||
|
||||
if header == EncapsulatedHeader {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, value := range values {
|
||||
if header == PreviewHeader {
|
||||
pb, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.SetPreview(pb); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
r.Header.Add(header, value)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
441
vendor/github.com/egirna/icap-client/icap_client.go
generated
vendored
Normal file
441
vendor/github.com/egirna/icap-client/icap_client.go
generated
vendored
Normal file
@@ -0,0 +1,441 @@
|
||||
package icapclient
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// the icap request methods
|
||||
const (
|
||||
MethodOPTIONS = "OPTIONS"
|
||||
MethodRESPMOD = "RESPMOD"
|
||||
MethodREQMOD = "REQMOD"
|
||||
)
|
||||
|
||||
// shared errors
|
||||
var (
|
||||
// ErrNoContext is used when no context is provided
|
||||
ErrNoContext = errors.New("no context provided")
|
||||
|
||||
// ErrInvalidScheme is used when the url scheme is not icap://
|
||||
ErrInvalidScheme = errors.New("the url scheme must be icap://")
|
||||
|
||||
// ErrMethodNotAllowed is used when the method is not allowed
|
||||
ErrMethodNotAllowed = errors.New("the requested method is not registered")
|
||||
|
||||
// ErrInvalidHost is used when the host is invalid
|
||||
ErrInvalidHost = errors.New("the requested host is invalid")
|
||||
|
||||
// ErrInvalidTCPMsg is used when the tcp message is invalid
|
||||
ErrInvalidTCPMsg = errors.New("invalid tcp message")
|
||||
|
||||
// ErrREQMODWithoutReq is used when the request is nil for REQMOD method
|
||||
ErrREQMODWithoutReq = errors.New("http request cannot be nil for method REQMOD")
|
||||
|
||||
// ErrREQMODWithResp is used when the response is not nil for REQMOD method
|
||||
ErrREQMODWithResp = errors.New("http response must be nil for method REQMOD")
|
||||
|
||||
// ErrRESPMODWithoutResp is used when the response is nil for RESPMOD method
|
||||
ErrRESPMODWithoutResp = errors.New("http response cannot be nil for method RESPMOD")
|
||||
)
|
||||
|
||||
// general constants required for the package
|
||||
const (
|
||||
schemeICAP = "icap"
|
||||
icapVersion = "ICAP/1.0"
|
||||
httpVersion = "HTTP/1.1"
|
||||
schemeHTTPReq = "http_request"
|
||||
schemeHTTPResp = "http_response"
|
||||
crlf = "\r\n"
|
||||
doubleCRLF = "\r\n\r\n"
|
||||
lf = "\n"
|
||||
bodyEndIndicator = crlf + "0" + crlf
|
||||
fullBodyEndIndicatorPreviewMode = "; ieof" + doubleCRLF
|
||||
icap100ContinueMsg = "ICAP/1.0 100 Continue" + doubleCRLF
|
||||
icap204NoModsMsg = "ICAP/1.0 204 Unmodified"
|
||||
)
|
||||
|
||||
// Common ICAP headers
|
||||
const (
|
||||
previewHeader = "Preview"
|
||||
encapsulatedHeader = "Encapsulated"
|
||||
)
|
||||
|
||||
// Conn represents the connection to the icap server
|
||||
type Conn interface {
|
||||
io.Closer
|
||||
Connect(ctx context.Context, address string) error
|
||||
Send(in []byte) ([]byte, error)
|
||||
}
|
||||
|
||||
// Response represents the icap server response data
|
||||
type Response struct {
|
||||
StatusCode int
|
||||
Status string
|
||||
PreviewBytes int
|
||||
Header http.Header
|
||||
ContentRequest *http.Request
|
||||
ContentResponse *http.Response
|
||||
}
|
||||
|
||||
// getStatusWithCode prepares the status code and status text from two given strings
|
||||
func getStatusWithCode(str1, str2 string) (int, string, error) {
|
||||
statusCode, err := strconv.Atoi(str1)
|
||||
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
status := strings.TrimSpace(str2)
|
||||
|
||||
return statusCode, status, nil
|
||||
}
|
||||
|
||||
// getHeaderValue parses the header and its value from a tcp message string
|
||||
func getHeaderValue(str string) (string, string) {
|
||||
headerValues := strings.SplitN(str, ":", 2)
|
||||
header := headerValues[0]
|
||||
|
||||
if len(headerValues) >= 2 {
|
||||
return header, strings.TrimSpace(headerValues[1])
|
||||
}
|
||||
|
||||
return header, ""
|
||||
|
||||
}
|
||||
|
||||
// isRequestLine determines if the tcp message string is a request line, i.e., the first line of the message or not
|
||||
func isRequestLine(str string) bool {
|
||||
return strings.Contains(str, icapVersion) || strings.Contains(str, httpVersion)
|
||||
}
|
||||
|
||||
// setEncapsulatedHeaderValue generates the Encapsulated values and assigns to the ICAP request string
|
||||
func setEncapsulatedHeaderValue(icapReqStr string, httpReqStr, httpRespStr string) string {
|
||||
encVal := " "
|
||||
|
||||
if strings.HasPrefix(icapReqStr, MethodOPTIONS) {
|
||||
switch {
|
||||
// the most common case for OPTIONS method, no Encapsulated body
|
||||
case httpReqStr == "" && httpRespStr == "":
|
||||
encVal += "null-body=0"
|
||||
// if there is an Encapsulated body
|
||||
default:
|
||||
encVal += "opt-body=0"
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(icapReqStr, MethodREQMOD) || strings.HasPrefix(icapReqStr, MethodRESPMOD) {
|
||||
// looking for the match of the string \r\n\r\n,
|
||||
// as that is the expression that separates each block, i.e., headers and bodies
|
||||
re := regexp.MustCompile(doubleCRLF)
|
||||
|
||||
// getting the offsets of the matches, tells us the starting/ending point of headers and bodies
|
||||
reqIndices := re.FindAllStringIndex(httpReqStr, -1)
|
||||
|
||||
// is needed to calculate the response headers by adding the last offset of the request block
|
||||
reqEndsAt := 0
|
||||
|
||||
if reqIndices != nil {
|
||||
encVal += "req-hdr=0"
|
||||
reqEndsAt = reqIndices[0][1]
|
||||
|
||||
switch {
|
||||
// indicating there is a body present for the request block, as length would have been 1 for a single match of \r\n\r\n
|
||||
case len(reqIndices) > 1:
|
||||
encVal += fmt.Sprintf(", req-body=%d", reqIndices[0][1]) // assigning the starting point of the body
|
||||
reqEndsAt = reqIndices[1][1]
|
||||
case httpRespStr == "":
|
||||
encVal += fmt.Sprintf(", null-body=%d", reqIndices[0][1])
|
||||
}
|
||||
|
||||
if httpRespStr != "" {
|
||||
encVal += ", "
|
||||
}
|
||||
}
|
||||
|
||||
respIndices := re.FindAllStringIndex(httpRespStr, -1)
|
||||
|
||||
if respIndices != nil {
|
||||
encVal += fmt.Sprintf("res-hdr=%d", reqEndsAt)
|
||||
|
||||
switch {
|
||||
case len(respIndices) > 1:
|
||||
encVal += fmt.Sprintf(", res-body=%d", reqEndsAt+respIndices[0][1])
|
||||
default:
|
||||
encVal += fmt.Sprintf(", null-body=%d", reqEndsAt+respIndices[0][1])
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// formatting the ICAP request Encapsulated header with the value
|
||||
return fmt.Sprintf(icapReqStr, encVal)
|
||||
}
|
||||
|
||||
// replaceRequestURIWithActualURL replaces just the escaped portion of the url with the entire URL in the dumped request message
|
||||
func replaceRequestURIWithActualURL(str string, uri, url string) string {
|
||||
if uri == "" {
|
||||
uri = "/"
|
||||
}
|
||||
|
||||
return strings.Replace(str, uri, url, 1)
|
||||
}
|
||||
|
||||
// addFullBodyInPreviewIndicator adds 0; ieof\r\n\r\n which indicates the entire body fitted in the preview
|
||||
func addFullBodyInPreviewIndicator(str string) string {
|
||||
return strings.TrimSuffix(str, doubleCRLF) + fullBodyEndIndicatorPreviewMode
|
||||
}
|
||||
|
||||
// splitBodyAndHeader separates header and body from a http message
|
||||
func splitBodyAndHeader(str string) (string, string, bool) {
|
||||
ss := strings.SplitN(str, doubleCRLF, 2)
|
||||
|
||||
if len(ss) < 2 || ss[1] == "" {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
headerStr := ss[0]
|
||||
bodyStr := ss[1]
|
||||
|
||||
return headerStr, bodyStr, true
|
||||
}
|
||||
|
||||
// bodyIsChunked determines if the http body is already chunked from the origin server or not
|
||||
func bodyIsChunked(str string) bool {
|
||||
_, bodyStr, ok := splitBodyAndHeader(str)
|
||||
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return regexp.MustCompile(`\r\n0(\r\n)+$`).MatchString(bodyStr)
|
||||
}
|
||||
|
||||
// parsePreviewBodyBytes parses the preview portion of the body and only keeps that in the message
|
||||
func parsePreviewBodyBytes(str string, pb int) string {
|
||||
headerStr, bodyStr, ok := splitBodyAndHeader(str)
|
||||
if !ok {
|
||||
return str
|
||||
}
|
||||
|
||||
return headerStr + doubleCRLF + bodyStr[:pb]
|
||||
}
|
||||
|
||||
// addHexBodyByteNotations adds the hexadecimal byte notations to the string,
|
||||
// for example, Hello World, becomes
|
||||
// b
|
||||
// Hello World
|
||||
// 0
|
||||
func addHexBodyByteNotations(str string) string {
|
||||
return fmt.Sprintf("%x%s%s%s", len([]byte(str)), crlf, str, bodyEndIndicator)
|
||||
}
|
||||
|
||||
// addHeaderAndBody merges the header and body of the http message
|
||||
func addHeaderAndBody(headerStr, bodyStr string) string {
|
||||
return headerStr + doubleCRLF + bodyStr
|
||||
}
|
||||
|
||||
// toICAPRequest returns the given request in its ICAP/1.x wire
|
||||
func toICAPRequest(req Request) ([]byte, error) {
|
||||
// Making the ICAP message block
|
||||
reqStr := fmt.Sprintf("%s %s %s%s", req.Method, req.URL.String(), icapVersion, crlf)
|
||||
|
||||
for headerName, values := range req.Header {
|
||||
for _, value := range values {
|
||||
reqStr += fmt.Sprintf("%s: %s%s", headerName, value, crlf)
|
||||
}
|
||||
}
|
||||
|
||||
// will populate the Encapsulated header value after making the http Request & Response messages
|
||||
reqStr += "Encapsulated: %s" + crlf
|
||||
reqStr += crlf
|
||||
|
||||
// build the HTTP Request message block
|
||||
httpReqStr := ""
|
||||
if req.HTTPRequest != nil {
|
||||
b, err := httputil.DumpRequestOut(req.HTTPRequest, true)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpReqStr += string(b)
|
||||
httpReqStr = replaceRequestURIWithActualURL(httpReqStr, req.HTTPRequest.URL.EscapedPath(), req.HTTPRequest.URL.String())
|
||||
|
||||
if req.Method == MethodREQMOD {
|
||||
if req.previewSet {
|
||||
httpReqStr = parsePreviewBodyBytes(httpReqStr, req.PreviewBytes)
|
||||
}
|
||||
|
||||
if !bodyIsChunked(httpReqStr) {
|
||||
headerStr, bodyStr, ok := splitBodyAndHeader(httpReqStr)
|
||||
if ok {
|
||||
bodyStr = addHexBodyByteNotations(bodyStr)
|
||||
httpReqStr = addHeaderAndBody(headerStr, bodyStr)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// if the HTTP Request message block doesn't end with a \r\n\r\n,
|
||||
// then going to add one by force for better calculation of byte offsets
|
||||
if httpReqStr != "" {
|
||||
for !strings.HasSuffix(httpReqStr, doubleCRLF) {
|
||||
httpReqStr += crlf
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// build the HTTP Response message block
|
||||
httpRespStr := ""
|
||||
if req.HTTPResponse != nil {
|
||||
b, err := httputil.DumpResponse(req.HTTPResponse, true)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpRespStr += string(b)
|
||||
|
||||
if req.previewSet {
|
||||
httpRespStr = parsePreviewBodyBytes(httpRespStr, req.PreviewBytes)
|
||||
}
|
||||
|
||||
if !bodyIsChunked(httpRespStr) {
|
||||
headerStr, bodyStr, ok := splitBodyAndHeader(httpRespStr)
|
||||
if ok {
|
||||
bodyStr = addHexBodyByteNotations(bodyStr)
|
||||
httpRespStr = addHeaderAndBody(headerStr, bodyStr)
|
||||
}
|
||||
}
|
||||
|
||||
if httpRespStr != "" && !strings.HasSuffix(httpRespStr, doubleCRLF) { // if the HTTP Response message block doesn't end with a \r\n\r\n, then going to add one by force for better calculation of byte offsets
|
||||
httpRespStr += crlf
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if encVal := req.Header.Get(encapsulatedHeader); encVal != "" {
|
||||
reqStr = fmt.Sprintf(reqStr, encVal)
|
||||
} else {
|
||||
//populating the Encapsulated header of the ICAP message portion
|
||||
reqStr = setEncapsulatedHeaderValue(reqStr, httpReqStr, httpRespStr)
|
||||
}
|
||||
|
||||
// determining if the http message needs the full body fitted in the preview portion indicator or not
|
||||
if httpRespStr != "" && req.previewSet && req.bodyFittedInPreview {
|
||||
httpRespStr = addFullBodyInPreviewIndicator(httpRespStr)
|
||||
}
|
||||
|
||||
if req.Method == MethodREQMOD && req.previewSet && req.bodyFittedInPreview {
|
||||
httpReqStr = addFullBodyInPreviewIndicator(httpReqStr)
|
||||
}
|
||||
|
||||
data := []byte(reqStr + httpReqStr + httpRespStr)
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// toClientResponse reads an ICAP message and returns a Response
|
||||
func toClientResponse(b *bufio.Reader) (Response, error) {
|
||||
resp := Response{
|
||||
Header: make(map[string][]string),
|
||||
}
|
||||
|
||||
scheme := ""
|
||||
httpMsg := ""
|
||||
for currentMsg, err := b.ReadString('\n'); err == nil || currentMsg != ""; currentMsg, err = b.ReadString('\n') { // keep reading the buffer message which is the http response message
|
||||
|
||||
// if the current message line if the first line of the message portion(request line)
|
||||
if isRequestLine(currentMsg) {
|
||||
ss := strings.Split(currentMsg, " ")
|
||||
|
||||
// must contain 3 words, for example, "ICAP/1.0 200 OK" or "GET /something HTTP/1.1"
|
||||
if len(ss) < 3 {
|
||||
return Response{}, fmt.Errorf("%w: %s", ErrInvalidTCPMsg, currentMsg)
|
||||
}
|
||||
|
||||
// preparing the scheme below
|
||||
if ss[0] == icapVersion {
|
||||
scheme = schemeICAP
|
||||
|
||||
resp.StatusCode, resp.Status, err = getStatusWithCode(ss[1], strings.Join(ss[2:], " "))
|
||||
if err != nil {
|
||||
return Response{}, err
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if ss[0] == httpVersion {
|
||||
scheme = schemeHTTPResp
|
||||
httpMsg = ""
|
||||
}
|
||||
|
||||
// http request message scheme version should always be at the end,
|
||||
// for example, GET /something HTTP/1.1
|
||||
if strings.TrimSpace(ss[2]) == httpVersion {
|
||||
scheme = schemeHTTPReq
|
||||
httpMsg = ""
|
||||
}
|
||||
}
|
||||
|
||||
// preparing the header for ICAP & contents for the HTTP messages below
|
||||
if scheme == schemeICAP {
|
||||
// ignore the CRLF and the LF, shouldn't be counted
|
||||
if currentMsg == lf || currentMsg == crlf {
|
||||
continue
|
||||
}
|
||||
|
||||
header, val := getHeaderValue(currentMsg)
|
||||
if header == previewHeader {
|
||||
pb, _ := strconv.Atoi(val)
|
||||
resp.PreviewBytes = pb
|
||||
}
|
||||
|
||||
resp.Header.Add(header, val)
|
||||
}
|
||||
|
||||
if scheme == schemeHTTPReq {
|
||||
httpMsg += strings.TrimSpace(currentMsg) + crlf
|
||||
bufferEmpty := b.Buffered() == 0
|
||||
|
||||
// a crlf indicates the end of the HTTP message and the buffer check is just in case the buffer ended with one last message instead of a crlf
|
||||
if currentMsg == crlf || bufferEmpty {
|
||||
request, err := http.ReadRequest(bufio.NewReader(strings.NewReader(httpMsg)))
|
||||
if err != nil {
|
||||
return Response{}, err
|
||||
}
|
||||
resp.ContentRequest = request
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if scheme == schemeHTTPResp {
|
||||
httpMsg += strings.TrimSpace(currentMsg) + crlf
|
||||
bufferEmpty := b.Buffered() == 0
|
||||
|
||||
if currentMsg == crlf || bufferEmpty {
|
||||
response, err := http.ReadResponse(bufio.NewReader(strings.NewReader(httpMsg)), resp.ContentRequest)
|
||||
if err != nil {
|
||||
return Response{}, err
|
||||
}
|
||||
resp.ContentResponse = response
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
9
vendor/github.com/egirna/icap-client/methods.go
generated
vendored
9
vendor/github.com/egirna/icap-client/methods.go
generated
vendored
@@ -1,9 +0,0 @@
|
||||
package icapclient
|
||||
|
||||
var (
|
||||
registeredMethods = map[string]bool{
|
||||
MethodOPTIONS: true,
|
||||
MethodRESPMOD: true,
|
||||
MethodREQMOD: true,
|
||||
}
|
||||
)
|
||||
179
vendor/github.com/egirna/icap-client/parser.go
generated
vendored
179
vendor/github.com/egirna/icap-client/parser.go
generated
vendored
@@ -1,179 +0,0 @@
|
||||
package icapclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"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
|
||||
}
|
||||
|
||||
status := strings.TrimSpace(str2)
|
||||
|
||||
return statusCode, status, nil
|
||||
}
|
||||
|
||||
// getHeaderVal parses the header and its value from a tcp message string
|
||||
func getHeaderVal(str string) (string, string) {
|
||||
|
||||
headerVals := strings.SplitN(str, ":", 2)
|
||||
header := headerVals[0]
|
||||
val := ""
|
||||
|
||||
if len(headerVals) >= 2 {
|
||||
val = strings.TrimSpace(headerVals[1])
|
||||
}
|
||||
|
||||
return header, val
|
||||
|
||||
}
|
||||
|
||||
// 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) {
|
||||
encpVal := " "
|
||||
|
||||
if strings.HasPrefix(*icapReqStr, MethodOPTIONS) { // if the request method is OPTIONS
|
||||
if httpReqStr == "" && httpRespStr == "" { // the most common case for OPTIONS method, no Encapsulated body
|
||||
encpVal += "null-body=0"
|
||||
} else {
|
||||
encpVal += "opt-body=0" // if there is an Encapsulated body
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(*icapReqStr, MethodREQMOD) || strings.HasPrefix(*icapReqStr, MethodRESPMOD) { // if the request method is RESPMOD or REQMOD
|
||||
re := regexp.MustCompile(DoubleCRLF) // looking for the match of the string \r\n\r\n, as that is the expression that seperates each blocks, i.e headers and bodies
|
||||
reqIndices := re.FindAllStringIndex(httpReqStr, -1) // getting the offsets of the matches, tells us the starting/ending point of headers and bodies
|
||||
|
||||
reqEndsAt := 0 // this is needed to calculate the response headers by adding the last offset of the request block
|
||||
if reqIndices != nil {
|
||||
encpVal += "req-hdr=0"
|
||||
reqEndsAt = reqIndices[0][1]
|
||||
if len(reqIndices) > 1 { // indicating there is a body present for the request block, as length would have been 1 for a single match of \r\n\r\n
|
||||
encpVal += fmt.Sprintf(", req-body=%d", reqIndices[0][1]) // assigning the starting point of the body
|
||||
reqEndsAt = reqIndices[1][1]
|
||||
} else if httpRespStr == "" {
|
||||
encpVal += fmt.Sprintf(", null-body=%d", reqIndices[0][1])
|
||||
}
|
||||
if httpRespStr != "" {
|
||||
encpVal += ", "
|
||||
}
|
||||
}
|
||||
|
||||
respIndices := re.FindAllStringIndex(httpRespStr, -1)
|
||||
|
||||
if respIndices != nil {
|
||||
encpVal += fmt.Sprintf("res-hdr=%d", reqEndsAt)
|
||||
if len(respIndices) > 1 {
|
||||
encpVal += fmt.Sprintf(", res-body=%d", reqEndsAt+respIndices[0][1])
|
||||
} else {
|
||||
encpVal += fmt.Sprintf(", null-body=%d", reqEndsAt+respIndices[0][1])
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
*icapReqStr = fmt.Sprintf(*icapReqStr, encpVal) // formatting the ICAP request Encapsulated header with the value
|
||||
}
|
||||
|
||||
// replaceRequestURIWithActualURL replaces the just the escaped portion of the url with the entire URL in the dumped request message
|
||||
func replaceRequestURIWithActualURL(str *string, uri, url string) {
|
||||
if uri == "" {
|
||||
uri = "/"
|
||||
}
|
||||
*str = strings.Replace(*str, uri, url, 1)
|
||||
}
|
||||
|
||||
// addFullBodyInPreviewIndicator adds 0; ieof\r\n\r\n which indicates the entire body fitted in preview
|
||||
func addFullBodyInPreviewIndicator(str *string) {
|
||||
*str = strings.TrimSuffix(*str, DoubleCRLF)
|
||||
*str += fullBodyEndIndicatorPreviewMode
|
||||
}
|
||||
|
||||
// splitBodyAndHeader separates header and body from a http message
|
||||
func splitBodyAndHeader(str string) (string, string, bool) {
|
||||
ss := strings.SplitN(str, DoubleCRLF, 2)
|
||||
|
||||
if len(ss) < 2 || ss[1] == "" {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
headerStr := ss[0]
|
||||
bodyStr := ss[1]
|
||||
|
||||
return headerStr, bodyStr, true
|
||||
}
|
||||
|
||||
// bodyAlreadyChunked determines if the http body is already chunked from the origin server or not
|
||||
func bodyAlreadyChunked(str string) bool {
|
||||
_, bodyStr, ok := splitBodyAndHeader(str)
|
||||
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
r := regexp.MustCompile("\\r\\n0(\\r\\n)+$")
|
||||
return r.MatchString(bodyStr)
|
||||
|
||||
}
|
||||
|
||||
// parsePreviewBodyBytes parses the preview portion of the body and only keeps that in the message
|
||||
func parsePreviewBodyBytes(str *string, pb int) {
|
||||
|
||||
headerStr, bodyStr, ok := splitBodyAndHeader(*str)
|
||||
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
bodyStr = bodyStr[:pb]
|
||||
|
||||
*str = headerStr + DoubleCRLF + bodyStr
|
||||
}
|
||||
|
||||
// addHexaBodyByteNotations adds the hexadecimal byte notaions in the messages
|
||||
// for example: Hello World, becomes
|
||||
// b
|
||||
// Hello World
|
||||
// 0
|
||||
func addHexaBodyByteNotations(bodyStr *string) {
|
||||
|
||||
bodyBytes := []byte(*bodyStr)
|
||||
|
||||
*bodyStr = fmt.Sprintf("%x%s%s%s", len(bodyBytes), CRLF, *bodyStr, bodyEndIndicator)
|
||||
}
|
||||
|
||||
// mergeHeaderAndBody merges the header and body of the http message togather
|
||||
func mergeHeaderAndBody(src *string, headerStr, bodyStr string) {
|
||||
*src = headerStr + DoubleCRLF + bodyStr
|
||||
}
|
||||
|
||||
func chunkBodyByBytes(bdyByte []byte, cl int) []byte {
|
||||
|
||||
newBytes := []byte{}
|
||||
|
||||
for i := 0; i < len(bdyByte); i += cl {
|
||||
end := i + cl
|
||||
if end > len(bdyByte) {
|
||||
end = len(bdyByte)
|
||||
}
|
||||
|
||||
newBytes = append(newBytes, []byte(fmt.Sprintf("%x\r\n", len(bdyByte[i:end]))+string(bdyByte[i:end]))...)
|
||||
}
|
||||
|
||||
newBytes = append(newBytes, []byte(bodyEndIndicator)...)
|
||||
|
||||
return newBytes
|
||||
}
|
||||
269
vendor/github.com/egirna/icap-client/request.go
generated
vendored
269
vendor/github.com/egirna/icap-client/request.go
generated
vendored
@@ -1,11 +1,15 @@
|
||||
package icapclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -18,143 +22,202 @@ type Request struct {
|
||||
HTTPResponse *http.Response
|
||||
ChunkLength int
|
||||
PreviewBytes int
|
||||
ctx *context.Context
|
||||
ctx context.Context
|
||||
previewSet bool
|
||||
bodyFittedInPreview bool
|
||||
remainingPreviewBytes []byte
|
||||
}
|
||||
|
||||
// NewRequest is the factory function for Request
|
||||
func NewRequest(method, urlStr string, httpReq *http.Request, httpResp *http.Response) (*Request, error) {
|
||||
|
||||
method = strings.ToUpper(method)
|
||||
|
||||
// NewRequest returns a new Request given a context, method, url, http request and http response
|
||||
// todo: method iota
|
||||
func NewRequest(ctx context.Context, method, urlStr string, httpReq *http.Request, httpResp *http.Response) (Request, error) {
|
||||
u, err := url.Parse(urlStr)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return Request{}, err
|
||||
}
|
||||
|
||||
req := &Request{
|
||||
Method: method,
|
||||
req := Request{
|
||||
Method: strings.ToUpper(method),
|
||||
URL: u,
|
||||
Header: make(map[string][]string),
|
||||
HTTPRequest: httpReq,
|
||||
HTTPResponse: httpResp,
|
||||
ctx: ctx,
|
||||
}
|
||||
|
||||
if err := req.Validate(); err != nil {
|
||||
return nil, err
|
||||
if err := req.validate(); err != nil {
|
||||
return Request{}, err
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// DumpRequest returns the given request in its ICAP/1.x wire
|
||||
// representation.
|
||||
func DumpRequest(req *Request) ([]byte, error) {
|
||||
// SetPreview sets the preview bytes in the icap header
|
||||
// todo: defer close error
|
||||
func (r *Request) SetPreview(maxBytes int) error {
|
||||
var bodyBytes []byte
|
||||
var previewBytes int
|
||||
var err error
|
||||
|
||||
// Making the ICAP message block
|
||||
// receiving the body bites to determine the preview bytes depending on the request ICAP method
|
||||
if r.Method == MethodREQMOD {
|
||||
if r.HTTPRequest == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
reqStr := fmt.Sprintf("%s %s %s%s", req.Method, req.URL.String(), ICAPVersion, CRLF)
|
||||
if r.HTTPRequest.Body != nil {
|
||||
b, err := io.ReadAll(r.HTTPRequest.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bodyBytes = b
|
||||
|
||||
for headerName, vals := range req.Header {
|
||||
for _, val := range vals {
|
||||
reqStr += fmt.Sprintf("%s: %s%s", headerName, val, CRLF)
|
||||
defer func() {
|
||||
err = errors.Join(err, r.HTTPRequest.Body.Close())
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
reqStr += "Encapsulated: %s" + CRLF // will populate the Encapsulated header value after making the http Request & Response messages
|
||||
reqStr += CRLF
|
||||
|
||||
// Making the HTTP Request message block
|
||||
|
||||
httpReqStr := ""
|
||||
if req.HTTPRequest != nil {
|
||||
b, err := httputil.DumpRequestOut(req.HTTPRequest, true)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if r.Method == MethodRESPMOD {
|
||||
if r.HTTPResponse == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
httpReqStr += string(b)
|
||||
replaceRequestURIWithActualURL(&httpReqStr, req.HTTPRequest.URL.EscapedPath(), req.HTTPRequest.URL.String())
|
||||
|
||||
if req.Method == MethodREQMOD {
|
||||
if req.previewSet {
|
||||
parsePreviewBodyBytes(&httpReqStr, req.PreviewBytes)
|
||||
if r.HTTPResponse.Body != nil {
|
||||
b, err := io.ReadAll(r.HTTPResponse.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bodyBytes = b
|
||||
|
||||
if !bodyAlreadyChunked(httpReqStr) {
|
||||
headerStr, bodyStr, ok := splitBodyAndHeader(httpReqStr)
|
||||
if ok {
|
||||
addHexaBodyByteNotations(&bodyStr)
|
||||
mergeHeaderAndBody(&httpReqStr, headerStr, bodyStr)
|
||||
defer func() {
|
||||
err = errors.Join(err, r.HTTPResponse.Body.Close())
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
previewBytes = len(bodyBytes)
|
||||
|
||||
// if the preview byte is 0 or less, there is no question of the body-fitting insides
|
||||
if previewBytes > 0 {
|
||||
r.bodyFittedInPreview = true
|
||||
}
|
||||
|
||||
// if the preview bytes are greater than what was mentioned by the ICAP Server (did not fit in the body)
|
||||
if previewBytes > maxBytes {
|
||||
previewBytes = maxBytes
|
||||
r.bodyFittedInPreview = false
|
||||
// storing the rest of the body byte which was not sent as preview for further operations
|
||||
r.remainingPreviewBytes = bodyBytes[maxBytes:]
|
||||
}
|
||||
|
||||
// set the body to the http message depending on the request method
|
||||
if r.Method == MethodREQMOD {
|
||||
r.HTTPRequest.Body = io.NopCloser(bytes.NewReader(bodyBytes))
|
||||
}
|
||||
|
||||
if r.Method == MethodRESPMOD {
|
||||
r.HTTPResponse.Body = io.NopCloser(bytes.NewReader(bodyBytes))
|
||||
}
|
||||
|
||||
// assign preview byte information to the header
|
||||
r.Header.Set("Preview", strconv.Itoa(previewBytes))
|
||||
r.PreviewBytes = previewBytes
|
||||
r.previewSet = true
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// setDefaultRequestHeaders is called by the client before sending the request
|
||||
// to the ICAP server to ensure all required headers are set
|
||||
func (r *Request) setDefaultRequestHeaders() {
|
||||
if _, exists := r.Header["Allow"]; !exists {
|
||||
r.Header.Add("Allow", "204") // assigning 204 by default if Allow not provided
|
||||
}
|
||||
|
||||
if _, exists := r.Header["Host"]; !exists {
|
||||
hostName, _ := os.Hostname()
|
||||
r.Header.Add("Host", hostName)
|
||||
}
|
||||
}
|
||||
|
||||
// extendHeader extends the current ICAP Request header with a new header
|
||||
func (r *Request) extendHeader(hdr http.Header) error {
|
||||
for header, values := range hdr {
|
||||
|
||||
if header == previewHeader && r.previewSet {
|
||||
continue
|
||||
}
|
||||
|
||||
if header == encapsulatedHeader {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, value := range values {
|
||||
if header == previewHeader {
|
||||
pb, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = r.SetPreview(pb)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
r.Header.Add(header, value)
|
||||
}
|
||||
|
||||
if httpReqStr != "" { // if the HTTP Request message block doesn't end with a \r\n\r\n, then going to add one by force for better calculation of byte offsets
|
||||
for !strings.HasSuffix(httpReqStr, DoubleCRLF) {
|
||||
httpReqStr += CRLF
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Making the HTTP Response message block
|
||||
|
||||
httpRespStr := ""
|
||||
if req.HTTPResponse != nil {
|
||||
b, err := httputil.DumpResponse(req.HTTPResponse, true)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpRespStr += string(b)
|
||||
|
||||
if req.previewSet {
|
||||
parsePreviewBodyBytes(&httpRespStr, req.PreviewBytes)
|
||||
}
|
||||
|
||||
if !bodyAlreadyChunked(httpRespStr) {
|
||||
headerStr, bodyStr, ok := splitBodyAndHeader(httpRespStr)
|
||||
if ok {
|
||||
addHexaBodyByteNotations(&bodyStr)
|
||||
mergeHeaderAndBody(&httpRespStr, headerStr, bodyStr)
|
||||
}
|
||||
}
|
||||
|
||||
if httpRespStr != "" && !strings.HasSuffix(httpRespStr, DoubleCRLF) { // if the HTTP Response message block doesn't end with a \r\n\r\n, then going to add one by force for better calculation of byte offsets
|
||||
httpRespStr += CRLF
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if encpVal := req.Header.Get(EncapsulatedHeader); encpVal != "" {
|
||||
reqStr = fmt.Sprintf(reqStr, encpVal)
|
||||
} else {
|
||||
//populating the Encapsulated header of the ICAP message portion
|
||||
setEncapsulatedHeaderValue(&reqStr, httpReqStr, httpRespStr)
|
||||
}
|
||||
|
||||
// determining if the http message needs the full body fitted in the preview portion indicator or not
|
||||
if httpRespStr != "" && req.previewSet && req.bodyFittedInPreview {
|
||||
addFullBodyInPreviewIndicator(&httpRespStr)
|
||||
}
|
||||
|
||||
if req.Method == MethodREQMOD && req.previewSet && req.bodyFittedInPreview {
|
||||
addFullBodyInPreviewIndicator(&httpReqStr)
|
||||
}
|
||||
|
||||
data := []byte(reqStr + httpReqStr + httpRespStr)
|
||||
|
||||
return data, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetContext sets a context for the ICAP request
|
||||
func (r *Request) SetContext(ctx context.Context) { // TODO: make context take control over the whole operation
|
||||
r.ctx = &ctx
|
||||
// validate checks if the ICAP request is valid or not
|
||||
func (r *Request) validate() error {
|
||||
var err error
|
||||
|
||||
// check if the ICAP request has a context
|
||||
if r.ctx == nil {
|
||||
err = errors.Join(err, ErrNoContext)
|
||||
}
|
||||
|
||||
// check if the ICAP request method is allowed
|
||||
if methodAllowed := slices.Contains([]string{
|
||||
MethodOPTIONS,
|
||||
MethodRESPMOD,
|
||||
MethodREQMOD,
|
||||
}, r.Method); !methodAllowed {
|
||||
err = errors.Join(err, ErrMethodNotAllowed)
|
||||
}
|
||||
|
||||
// check if the ICAP url is valid and contains all required fields
|
||||
{
|
||||
if r.URL.Scheme != schemeICAP {
|
||||
err = errors.Join(err, ErrInvalidScheme)
|
||||
}
|
||||
|
||||
if r.URL.Host == "" {
|
||||
err = errors.Join(err, ErrInvalidHost)
|
||||
}
|
||||
}
|
||||
|
||||
// check if the ICAP request method is aligned with the http messages
|
||||
{
|
||||
if r.Method == MethodREQMOD && r.HTTPRequest == nil {
|
||||
err = errors.Join(err, ErrREQMODWithoutReq)
|
||||
}
|
||||
|
||||
if r.Method == MethodREQMOD && r.HTTPResponse != nil {
|
||||
err = errors.Join(err, ErrREQMODWithResp)
|
||||
}
|
||||
|
||||
if r.Method == MethodRESPMOD && r.HTTPResponse == nil {
|
||||
err = errors.Join(err, ErrRESPMODWithoutResp)
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
123
vendor/github.com/egirna/icap-client/response.go
generated
vendored
123
vendor/github.com/egirna/icap-client/response.go
generated
vendored
@@ -1,123 +0,0 @@
|
||||
package icapclient
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Response represents the icap server response data
|
||||
type Response struct {
|
||||
StatusCode int
|
||||
Status string
|
||||
PreviewBytes int
|
||||
Header http.Header
|
||||
ContentRequest *http.Request
|
||||
ContentResponse *http.Response
|
||||
}
|
||||
|
||||
var (
|
||||
optionValues = map[string]bool{
|
||||
PreviewHeader: true,
|
||||
MethodsHeader: true,
|
||||
AllowHeader: true,
|
||||
TransferPreviewHeader: true,
|
||||
ServiceHeader: true,
|
||||
ISTagHeader: true,
|
||||
OptBodyTypeHeader: true,
|
||||
MaxConnectionsHeader: true,
|
||||
OptionsTTLHeader: true,
|
||||
ServiceIDHeader: true,
|
||||
TransferIgnoreHeader: true,
|
||||
TransferCompleteHeader: true,
|
||||
}
|
||||
)
|
||||
|
||||
// ReadResponse converts a Reader to a icapclient Response
|
||||
func ReadResponse(b *bufio.Reader) (*Response, error) {
|
||||
|
||||
resp := &Response{
|
||||
Header: make(map[string][]string),
|
||||
}
|
||||
|
||||
scheme := ""
|
||||
httpMsg := ""
|
||||
for currentMsg, err := b.ReadString('\n'); err == nil || currentMsg != ""; currentMsg, err = b.ReadString('\n') { // keep reading the buffer message which is the http response message
|
||||
|
||||
if isRequestLine(currentMsg) { // if the current message line if the first line of the message portion(request line)
|
||||
ss := strings.Split(currentMsg, " ")
|
||||
|
||||
if len(ss) < 3 { // must contain 3 words, for example: "ICAP/1.0 200 OK" or "GET /something HTTP/1.1"
|
||||
return nil, errors.New(ErrInvalidTCPMsg + ":" + currentMsg)
|
||||
}
|
||||
|
||||
// preparing the scheme below
|
||||
|
||||
if ss[0] == ICAPVersion {
|
||||
scheme = SchemeICAP
|
||||
resp.StatusCode, resp.Status, err = getStatusWithCode(ss[1], strings.Join(ss[2:], " "))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if ss[0] == HTTPVersion {
|
||||
scheme = SchemeHTTPResp
|
||||
httpMsg = ""
|
||||
}
|
||||
|
||||
if strings.TrimSpace(ss[2]) == HTTPVersion { // for a http request message if the scheme version is always at last, for example: GET /something HTTP/1.1
|
||||
scheme = SchemeHTTPReq
|
||||
httpMsg = ""
|
||||
}
|
||||
}
|
||||
|
||||
// preparing the header for ICAP & contents for HTTP messages below
|
||||
|
||||
if scheme == SchemeICAP {
|
||||
if currentMsg == LF || currentMsg == CRLF { // don't want to count the Line Feed as header
|
||||
continue
|
||||
}
|
||||
header, val := getHeaderVal(currentMsg)
|
||||
if header == PreviewHeader {
|
||||
pb, _ := strconv.Atoi(val)
|
||||
resp.PreviewBytes = pb
|
||||
}
|
||||
resp.Header.Add(header, val)
|
||||
}
|
||||
|
||||
if scheme == SchemeHTTPReq {
|
||||
httpMsg += strings.TrimSpace(currentMsg) + CRLF
|
||||
bufferEmpty := b.Buffered() == 0
|
||||
if currentMsg == CRLF || bufferEmpty { // a CRLF indicates the end of a http message and the buffer check is just in case the buffer eneded with one last message instead of a CRLF
|
||||
var erR error
|
||||
resp.ContentRequest, erR = http.ReadRequest(bufio.NewReader(strings.NewReader(httpMsg)))
|
||||
if erR != nil {
|
||||
return nil, erR
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if scheme == SchemeHTTPResp {
|
||||
httpMsg += strings.TrimSpace(currentMsg) + CRLF
|
||||
bufferEmpty := b.Buffered() == 0
|
||||
if currentMsg == CRLF || bufferEmpty {
|
||||
var erR error
|
||||
resp.ContentResponse, erR = http.ReadResponse(bufio.NewReader(strings.NewReader(httpMsg)), resp.ContentRequest)
|
||||
if erR != nil {
|
||||
return nil, erR
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
|
||||
}
|
||||
6
vendor/github.com/egirna/icap-client/test_server.go
generated
vendored
6
vendor/github.com/egirna/icap-client/test_server.go
generated
vendored
@@ -36,7 +36,7 @@ func startTestServer() {
|
||||
|
||||
log.Println("Starting ICAP test server...")
|
||||
|
||||
signal.Notify(stop, syscall.SIGKILL, syscall.SIGINT, syscall.SIGQUIT)
|
||||
signal.Notify(stop, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT)
|
||||
|
||||
go func() {
|
||||
if err := icap.ListenAndServe(fmt.Sprintf(":%d", port), nil); err != nil {
|
||||
@@ -79,8 +79,6 @@ func respmodHandler(w icap.ResponseWriter, req *icap.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// log.Println("The preview data: ", string(req.Preview))
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
if _, err := io.Copy(buf, req.Response.Body); err != nil {
|
||||
@@ -124,8 +122,6 @@ func reqmodHandler(w icap.ResponseWriter, req *icap.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// log.Println("The preview data: ", string(req.Preview))
|
||||
|
||||
fileURL := req.Request.RequestURI
|
||||
|
||||
status := 0
|
||||
|
||||
123
vendor/github.com/egirna/icap-client/transport.go
generated
vendored
123
vendor/github.com/egirna/icap-client/transport.go
generated
vendored
@@ -1,123 +0,0 @@
|
||||
package icapclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// transport represents the transport layer data
|
||||
type transport struct {
|
||||
network string
|
||||
addr string
|
||||
timeout time.Duration
|
||||
readTimeout time.Duration
|
||||
writeTimeout time.Duration
|
||||
sckt net.Conn
|
||||
}
|
||||
|
||||
// dial fires up a tcp socket
|
||||
func (t *transport) dial() error {
|
||||
sckt, err := net.DialTimeout(t.network, t.addr, t.timeout)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := sckt.SetReadDeadline(time.Now().UTC().Add(t.readTimeout)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := sckt.SetWriteDeadline(time.Now().UTC().Add(t.writeTimeout)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.sckt = sckt
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// dialWithContext fires up a tcp socket
|
||||
func (t *transport) dialWithContext(ctx context.Context) error {
|
||||
sckt, err := (&net.Dialer{
|
||||
Timeout: t.timeout,
|
||||
}).DialContext(ctx, t.network, t.addr)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := sckt.SetReadDeadline(time.Now().UTC().Add(t.readTimeout)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := sckt.SetWriteDeadline(time.Now().UTC().Add(t.writeTimeout)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.sckt = sckt
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write writes data to the server
|
||||
func (t *transport) write(data []byte) (int, error) {
|
||||
logDebug("Dumping the message being sent to the server...")
|
||||
dumpDebug(string(data))
|
||||
return t.sckt.Write(data)
|
||||
}
|
||||
|
||||
// Read reads data from server
|
||||
func (t *transport) read() (string, error) {
|
||||
|
||||
data := make([]byte, 0)
|
||||
|
||||
logDebug("Dumping messages received from the server...")
|
||||
|
||||
for {
|
||||
tmp := make([]byte, 1096)
|
||||
|
||||
n, err := t.sckt.Read(tmp)
|
||||
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
logDebug("End of file detected from EOF error")
|
||||
break
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
if n == 0 {
|
||||
logDebug("End of file detected by 0 bytes")
|
||||
break
|
||||
}
|
||||
|
||||
data = append(data, tmp[:n]...)
|
||||
if string(data) == icap100ContinueMsg { // explicitly breaking because the Read blocks for 100 continue message // TODO: find out why
|
||||
logDebug("Stopping because got 100 Continue from the server")
|
||||
break
|
||||
}
|
||||
|
||||
if strings.HasSuffix(string(data), "0\r\n\r\n") {
|
||||
logDebug("End of the file detected by 0 Double CRLF indicator")
|
||||
break
|
||||
}
|
||||
|
||||
if strings.Contains(string(data), icap204NoModsMsg) {
|
||||
logDebug("End of file detected by 204 no modifications and Double CRLF at the end")
|
||||
break
|
||||
}
|
||||
|
||||
dumpDebug(string(tmp))
|
||||
|
||||
}
|
||||
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
// close closes the tcp connection
|
||||
func (t *transport) close() error {
|
||||
return t.sckt.Close()
|
||||
}
|
||||
63
vendor/github.com/egirna/icap-client/validate.go
generated
vendored
63
vendor/github.com/egirna/icap-client/validate.go
generated
vendored
@@ -1,63 +0,0 @@
|
||||
package icapclient
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// validMethod validates the ICAP method
|
||||
func validMethod(method string) (bool, error) {
|
||||
if _, registered := registeredMethods[method]; !registered {
|
||||
return false, errors.New(ErrMethodNotRegistered)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// validURL validates the Server URL provided
|
||||
func validURL(url *url.URL) (bool, error) {
|
||||
|
||||
if url.Scheme != SchemeICAP {
|
||||
return false, errors.New(ErrInvalidScheme)
|
||||
}
|
||||
|
||||
if url.Host == "" {
|
||||
return false, errors.New(ErrInvalidHost)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// validMethodWithHTTP validates if the ICAP request method and the http messages are alligned or not
|
||||
func validMethodWithHTTP(httpReq *http.Request, httpResp *http.Response, method string) (bool, error) {
|
||||
if method == MethodREQMOD && httpReq == nil {
|
||||
return false, errors.New(ErrREQMODWithNoReq)
|
||||
}
|
||||
if method == MethodREQMOD && httpResp != nil {
|
||||
return false, errors.New(ErrREQMODWithResp)
|
||||
}
|
||||
if method == MethodRESPMOD && httpResp == nil {
|
||||
return false, errors.New(ErrRESPMODWithNoResp)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Validate validates the ICAP request
|
||||
func (r *Request) Validate() error {
|
||||
|
||||
if valid, err := validMethod(r.Method); !valid {
|
||||
return err
|
||||
}
|
||||
|
||||
if valid, err := validURL(r.URL); !valid {
|
||||
return err
|
||||
}
|
||||
|
||||
if valid, err := validMethodWithHTTP(r.HTTPRequest, r.HTTPResponse, r.Method); !valid {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
5
vendor/modules.txt
vendored
5
vendor/modules.txt
vendored
@@ -763,8 +763,8 @@ github.com/dutchcoders/go-clamd
|
||||
# github.com/egirna/icap v0.0.0-20181108071049-d5ee18bd70bc
|
||||
## explicit
|
||||
github.com/egirna/icap
|
||||
# github.com/egirna/icap-client v0.1.1
|
||||
## explicit; go 1.12
|
||||
# github.com/egirna/icap-client v0.1.1 => github.com/fschade/icap-client v0.0.0-20240105150744-9c2d8aff3ef2
|
||||
## explicit; go 1.21
|
||||
github.com/egirna/icap-client
|
||||
# github.com/emirpasic/gods v1.18.1
|
||||
## explicit; go 1.2
|
||||
@@ -2318,3 +2318,4 @@ stash.kopano.io/kgol/oidc-go
|
||||
stash.kopano.io/kgol/rndm
|
||||
# github.com/go-micro/plugins/v4/store/nats-js-kv => github.com/kobergj/plugins/v4/store/nats-js-kv v0.0.0-20231207143248-4d424e3ae348
|
||||
# github.com/studio-b12/gowebdav => github.com/aduffeck/gowebdav v0.0.0-20231215102054-212d4a4374f6
|
||||
# github.com/egirna/icap-client => github.com/fschade/icap-client v0.0.0-20240105150744-9c2d8aff3ef2
|
||||
|
||||
Reference in New Issue
Block a user