Initial QSfera import
This commit is contained in:
+20
@@ -0,0 +1,20 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
tab_width = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.go]
|
||||
indent_style = tab
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
indent_size = 1
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
@@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
||||
@@ -0,0 +1,16 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
logs.txt
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
linters-settings:
|
||||
gofmt:
|
||||
simplify: true
|
||||
rewrite-rules:
|
||||
- pattern: interface{}
|
||||
replacement: any
|
||||
misspell:
|
||||
locale: US
|
||||
gofumpt:
|
||||
extra-rules: true
|
||||
forbidigo:
|
||||
forbid:
|
||||
- context\.WithCancel$
|
||||
- ^print.*$
|
||||
- panic
|
||||
- ^log.Fatal().*$
|
||||
errorlint:
|
||||
errorf-multi: true
|
||||
gci:
|
||||
custom-order: true
|
||||
sections:
|
||||
- standard
|
||||
- default
|
||||
- prefix(github.com/opencloud-eu/woodpecker-ci-config-service)
|
||||
godot:
|
||||
scope: toplevel
|
||||
period: true
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- bidichk
|
||||
- errcheck
|
||||
- gofmt
|
||||
- gosimple
|
||||
- goimports
|
||||
- govet
|
||||
- ineffassign
|
||||
- misspell
|
||||
- revive
|
||||
- staticcheck
|
||||
- typecheck
|
||||
- unused
|
||||
- gofumpt
|
||||
- errorlint
|
||||
- forbidigo
|
||||
- zerologlint
|
||||
- asciicheck
|
||||
- dogsled
|
||||
- durationcheck
|
||||
- errchkjson
|
||||
- goheader
|
||||
- gomoddirectives
|
||||
- gomodguard
|
||||
- goprintffuncname
|
||||
- importas
|
||||
- makezero
|
||||
- rowserrcheck
|
||||
- sqlclosecheck
|
||||
- usetesting
|
||||
- unconvert
|
||||
- unparam
|
||||
- wastedassign
|
||||
- whitespace
|
||||
- gocritic
|
||||
- nolintlint
|
||||
- stylecheck
|
||||
- contextcheck
|
||||
- forcetypeassert
|
||||
- gci
|
||||
- godot
|
||||
|
||||
run:
|
||||
timeout: 15m
|
||||
build-tags:
|
||||
- test
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
.PHONY: lint
|
||||
lint:
|
||||
go tool golangci-lint run
|
||||
|
||||
.PHONY: vendor
|
||||
vendor:
|
||||
go mod tidy
|
||||
go mod vendor
|
||||
|
||||
format:
|
||||
go tool gofumpt -extra -w .
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
go clean -i ./...
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
go test -race -cover -coverprofile coverage.out -timeout 60s .
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
# icap-client
|
||||
|
||||
[](https://ci.opencloud.eu/repos/21)
|
||||
[](https://app.element.io/#/room/#opencloud:matrix.org)
|
||||
[](https://opensource.org/licenses/Apache-2.0)
|
||||
|
||||
## Contributing
|
||||
|
||||
Please follow the [OpenCloud Contribution Guidelines](https://github.com/opencloud-eu/opencloud/blob/main/CONTRIBUTING.md), which apply to all projects in the OpenCloud organization.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
This project is forked from [@egirna/icap-client](https://github.com/egirna/icap-client).
|
||||
Special thanks to [egirna](https://github.com/egirna) for the original implementation and ongoing inspiration.
|
||||
|
||||
### License
|
||||
|
||||
**icap-client** is licensed under the [Apache License](LICENSE).
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package icapclient
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Client represents the ICAP client who makes the ICAP server calls.
|
||||
type Client struct {
|
||||
config Config // Store config for connection parameters
|
||||
}
|
||||
|
||||
// NewClient creates a new ICAP client (no persistent connection).
|
||||
func NewClient(options ...ConfigOption) (Client, error) {
|
||||
config := DefaultConfig()
|
||||
for _, option := range options {
|
||||
option(&config)
|
||||
}
|
||||
return Client{config: config}, nil
|
||||
}
|
||||
|
||||
// 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, conn.Close())
|
||||
}()
|
||||
|
||||
req.setDefaultRequestHeaders()
|
||||
|
||||
message, err := toICAPRequest(req)
|
||||
if err != nil {
|
||||
return Response{}, err
|
||||
}
|
||||
|
||||
// send the ICAP message to the server
|
||||
dataRes, err := conn.Send(message)
|
||||
if err != nil {
|
||||
return Response{}, err
|
||||
}
|
||||
|
||||
res, 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 := !(res.StatusCode == http.StatusContinue && !req.bodyFittedInPreview && req.previewSet)
|
||||
if done {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
if !bytes.HasSuffix(data, []byte(doubleCRLF)) {
|
||||
data = append(data, []byte(crlf)...)
|
||||
}
|
||||
|
||||
// send the remaining body bytes to the server.
|
||||
dataRes, err = conn.Send(data)
|
||||
if err != nil {
|
||||
return Response{}, err
|
||||
}
|
||||
|
||||
return toClientResponse(bufio.NewReader(strings.NewReader(string(dataRes))))
|
||||
}
|
||||
+33
@@ -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
|
||||
}
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
package icapclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ICAPConnConfig is the configuration for the icap connection.
|
||||
type ICAPConnConfig struct {
|
||||
// Timeout is the maximum amount of time a connection will be kept open
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
// ICAPConn manages the transport layer for ICAP protocol.
|
||||
type ICAPConn struct {
|
||||
tcp net.Conn
|
||||
mu sync.Mutex
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// NewICAPConn creates a new connection configuration.
|
||||
func NewICAPConn(conf ICAPConnConfig) (*ICAPConn, error) {
|
||||
return &ICAPConn{
|
||||
timeout: conf.Timeout,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Connect connects to the ICAP server.
|
||||
func (c *ICAPConn) Connect(ctx context.Context, address string) error {
|
||||
dialer := net.Dialer{Timeout: c.timeout}
|
||||
conn, err := dialer.DialContext(ctx, "tcp", address)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.tcp = conn
|
||||
|
||||
if c.timeout > 0 {
|
||||
deadline := time.Now().Add(c.timeout)
|
||||
if err := c.tcp.SetDeadline(deadline); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Send sends a request to the ICAP server and reads the response.
|
||||
func (c *ICAPConn) Send(in []byte) ([]byte, error) {
|
||||
if !c.ok() {
|
||||
return nil, ErrInvalidConnection
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
_, err := c.tcp.Write(in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var data []byte
|
||||
buf := make([]byte, 4096)
|
||||
for {
|
||||
n, err := c.tcp.Read(buf)
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if errors.Is(err, io.EOF) || n == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
data = append(data, buf[:n]...)
|
||||
|
||||
// Protocol checks for message termination
|
||||
{
|
||||
if bytes.Equal(data, []byte(icap100ContinueMsg)) {
|
||||
break
|
||||
}
|
||||
|
||||
if bytes.HasSuffix(data, []byte(doubleCRLF)) {
|
||||
break
|
||||
}
|
||||
|
||||
if bytes.Contains(data, []byte(icap204NoModsMsg)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Close closes the TCP connection.
|
||||
func (c *ICAPConn) Close() error {
|
||||
if !c.ok() {
|
||||
return ErrInvalidConnection
|
||||
}
|
||||
return c.tcp.Close()
|
||||
}
|
||||
|
||||
func (c *ICAPConn) ok() bool {
|
||||
return c != nil && c.tcp != nil
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
// Package icapclient is a client package for the ICAP protocol
|
||||
//
|
||||
// See https://github.com/egirna/icap-client/examples.
|
||||
package icapclient
|
||||
+432
@@ -0,0 +1,432 @@
|
||||
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")
|
||||
|
||||
// ErrInvalidMsg is used when the tcp message is invalid.
|
||||
ErrInvalidMsg = errors.New("invalid message")
|
||||
|
||||
// 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 = 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 = crlf + crlf
|
||||
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, 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, 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]
|
||||
}
|
||||
|
||||
// 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", ErrInvalidMsg, 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
|
||||
}
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
package icapclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Request represents the icap client request data.
|
||||
type Request struct {
|
||||
Method string
|
||||
URL *url.URL
|
||||
Header http.Header
|
||||
HTTPRequest *http.Request
|
||||
HTTPResponse *http.Response
|
||||
ChunkLength int
|
||||
PreviewBytes int
|
||||
ctx context.Context
|
||||
previewSet bool
|
||||
bodyFittedInPreview bool
|
||||
remainingPreviewBytes []byte
|
||||
}
|
||||
|
||||
// NewRequest returns a new Request given a context, method, url, http request and http response
|
||||
// todo: method iota
|
||||
func NewRequest(ctx context.Context, method, urlStr string, httpReq *http.Request, httpResp *http.Response) (Request, error) {
|
||||
u, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return Request{}, err
|
||||
}
|
||||
|
||||
req := Request{
|
||||
Method: strings.ToUpper(method),
|
||||
URL: u,
|
||||
Header: make(map[string][]string),
|
||||
HTTPRequest: httpReq,
|
||||
HTTPResponse: httpResp,
|
||||
ctx: ctx,
|
||||
}
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return Request{}, err
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// SetPreview sets the preview bytes in the icap header
|
||||
// todo: defer close error
|
||||
func (r *Request) SetPreview(maxBytes int) (err error) {
|
||||
var bodyBytes []byte
|
||||
var previewBytes int
|
||||
|
||||
// receiving the body bites to determine the preview bytes depending on the request ICAP method
|
||||
if r.Method == MethodREQMOD {
|
||||
if r.HTTPRequest == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if r.HTTPRequest.Body != nil {
|
||||
b, err := io.ReadAll(r.HTTPRequest.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bodyBytes = b
|
||||
|
||||
defer func() {
|
||||
err = errors.Join(err, r.HTTPRequest.Body.Close())
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
if r.Method == MethodRESPMOD {
|
||||
if r.HTTPResponse == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if r.HTTPResponse.Body != nil {
|
||||
b, err := io.ReadAll(r.HTTPResponse.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bodyBytes = b
|
||||
|
||||
defer func() {
|
||||
err = errors.Join(err, r.HTTPResponse.Body.Close())
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
previewBytes = len(bodyBytes)
|
||||
|
||||
// if the preview byte is 0 or less, there is no question of the body-fitting insides
|
||||
if previewBytes > 0 {
|
||||
r.bodyFittedInPreview = true
|
||||
}
|
||||
|
||||
// if the preview bytes are greater than what was mentioned by the ICAP Server (did not fit in the body)
|
||||
if previewBytes > maxBytes {
|
||||
previewBytes = maxBytes
|
||||
r.bodyFittedInPreview = false
|
||||
// storing the rest of the body byte which was not sent as preview for further operations
|
||||
r.remainingPreviewBytes = bodyBytes[maxBytes:]
|
||||
}
|
||||
|
||||
// set the body to the http message depending on the request method
|
||||
if r.Method == MethodREQMOD {
|
||||
r.HTTPRequest.Body = io.NopCloser(bytes.NewReader(bodyBytes))
|
||||
}
|
||||
|
||||
if r.Method == MethodRESPMOD {
|
||||
r.HTTPResponse.Body = io.NopCloser(bytes.NewReader(bodyBytes))
|
||||
}
|
||||
|
||||
// assign preview byte information to the header
|
||||
r.Header.Set("Preview", strconv.Itoa(previewBytes))
|
||||
r.PreviewBytes = previewBytes
|
||||
r.previewSet = true
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// to the ICAP server to ensure all required headers are set.
|
||||
func (r *Request) setDefaultRequestHeaders() {
|
||||
if _, exists := r.Header["Allow"]; !exists {
|
||||
r.Header.Add("Allow", "204") // assigning 204 by default if Allow not provided
|
||||
}
|
||||
|
||||
if _, exists := r.Header["Host"]; !exists {
|
||||
hostName, _ := os.Hostname()
|
||||
r.Header.Add("Host", hostName)
|
||||
}
|
||||
}
|
||||
|
||||
// extendHeader extends the current ICAP Request header with a new header.
|
||||
func (r *Request) extendHeader(hdr http.Header) error {
|
||||
for header, values := range hdr {
|
||||
if header == previewHeader && r.previewSet {
|
||||
continue
|
||||
}
|
||||
|
||||
if header == encapsulatedHeader {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, value := range values {
|
||||
if header == previewHeader {
|
||||
pb, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = r.SetPreview(pb)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
r.Header.Add(header, value)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validate checks if the ICAP request is valid or not.
|
||||
func (r *Request) validate() error {
|
||||
var err error
|
||||
|
||||
// check if the ICAP request has a context
|
||||
if r.ctx == nil {
|
||||
err = errors.Join(err, ErrNoContext)
|
||||
}
|
||||
|
||||
// check if the ICAP request method is allowed
|
||||
if methodAllowed := slices.Contains([]string{
|
||||
MethodOPTIONS,
|
||||
MethodRESPMOD,
|
||||
MethodREQMOD,
|
||||
}, r.Method); !methodAllowed {
|
||||
err = errors.Join(err, ErrMethodNotAllowed)
|
||||
}
|
||||
|
||||
// check if the ICAP url is valid and contains all required fields
|
||||
{
|
||||
if r.URL.Scheme != schemeICAP {
|
||||
err = errors.Join(err, ErrInvalidScheme)
|
||||
}
|
||||
|
||||
if r.URL.Host == "" {
|
||||
err = errors.Join(err, ErrInvalidHost)
|
||||
}
|
||||
}
|
||||
|
||||
// check if the ICAP request method is aligned with the http messages
|
||||
{
|
||||
if r.Method == MethodREQMOD && r.HTTPRequest == nil {
|
||||
err = errors.Join(err, ErrREQMODWithoutReq)
|
||||
}
|
||||
|
||||
if r.Method == MethodREQMOD && r.HTTPResponse != nil {
|
||||
err = errors.Join(err, ErrREQMODWithResp)
|
||||
}
|
||||
|
||||
if r.Method == MethodRESPMOD && r.HTTPResponse == nil {
|
||||
err = errors.Join(err, ErrRESPMODWithoutResp)
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
package icapclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/egirna/icap"
|
||||
)
|
||||
|
||||
var (
|
||||
stop = make(chan os.Signal, 1)
|
||||
port = 1344
|
||||
)
|
||||
|
||||
const (
|
||||
previewBytes = 24
|
||||
goodFileDetectStr = "GOOD FILE"
|
||||
badFileDetectStr = "BAD FILE"
|
||||
goodURL = "http://goodifle.com"
|
||||
badURL = "http://badfile.com"
|
||||
)
|
||||
|
||||
func startTestServer() {
|
||||
icap.HandleFunc("/respmod", respmodHandler)
|
||||
icap.HandleFunc("/reqmod", reqmodHandler)
|
||||
|
||||
log.Println("Starting ICAP test server...")
|
||||
|
||||
signal.Notify(stop, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT)
|
||||
|
||||
go func() {
|
||||
if err := icap.ListenAndServe(fmt.Sprintf(":%d", port), nil); err != nil {
|
||||
log.Println("Failed to start ICAP test server: ", err.Error())
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
|
||||
log.Printf("ICAP test server is running on localhost:%d\n...\n", port)
|
||||
<-stop
|
||||
|
||||
log.Println("ICAP test server is shut down!")
|
||||
}
|
||||
|
||||
func stopTestServer() {
|
||||
stop <- syscall.SIGKILL
|
||||
}
|
||||
|
||||
func respmodHandler(w icap.ResponseWriter, req *icap.Request) {
|
||||
h := w.Header()
|
||||
h.Set("ISTag", "ICAP-TEST")
|
||||
h.Set("Service", "ICAP-TEST-SERVICE")
|
||||
|
||||
switch req.Method {
|
||||
case "OPTIONS":
|
||||
h.Set("Methods", "RESPMOD")
|
||||
h.Set("Allow", "204")
|
||||
if previewBytes > 0 {
|
||||
h.Set("Preview", strconv.Itoa(previewBytes))
|
||||
}
|
||||
h.Set("Transfer-Preview", "*")
|
||||
w.WriteHeader(http.StatusOK, nil, false)
|
||||
case "RESPMOD":
|
||||
defer req.Response.Body.Close()
|
||||
|
||||
if val, exist := req.Header["Allow"]; !exist || (len(val) > 0 && val[0] != "204") {
|
||||
w.WriteHeader(http.StatusNoContent, nil, false)
|
||||
return
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
if _, err := io.Copy(buf, req.Response.Body); err != nil {
|
||||
log.Println("Failed to copy the response body to buffer: ", err.Error())
|
||||
w.WriteHeader(http.StatusNoContent, nil, false)
|
||||
return
|
||||
}
|
||||
|
||||
status := 0
|
||||
if strings.Contains(buf.String(), goodFileDetectStr) {
|
||||
status = http.StatusNoContent
|
||||
}
|
||||
|
||||
if strings.Contains(buf.String(), badFileDetectStr) {
|
||||
status = http.StatusOK
|
||||
}
|
||||
|
||||
w.WriteHeader(status, nil, false)
|
||||
}
|
||||
}
|
||||
|
||||
func reqmodHandler(w icap.ResponseWriter, req *icap.Request) {
|
||||
h := w.Header()
|
||||
h.Set("ISTag", "ICAP-TEST")
|
||||
h.Set("Service", "ICAP-TEST-SERVICE")
|
||||
|
||||
switch req.Method {
|
||||
case "OPTIONS":
|
||||
h.Set("Methods", "REQMOD")
|
||||
h.Set("Allow", "204")
|
||||
if previewBytes > 0 {
|
||||
h.Set("Preview", strconv.Itoa(previewBytes))
|
||||
}
|
||||
h.Set("Transfer-Preview", "*")
|
||||
w.WriteHeader(http.StatusOK, nil, false)
|
||||
case "REQMOD":
|
||||
|
||||
if val, exist := req.Header["Allow"]; !exist || (len(val) > 0 && val[0] != "204") {
|
||||
w.WriteHeader(http.StatusNoContent, nil, false)
|
||||
return
|
||||
}
|
||||
|
||||
fileURL := req.Request.RequestURI
|
||||
|
||||
status := 0
|
||||
if fileURL == goodURL {
|
||||
status = http.StatusNoContent
|
||||
}
|
||||
|
||||
if fileURL == badURL {
|
||||
status = http.StatusOK
|
||||
}
|
||||
|
||||
w.WriteHeader(status, nil, false)
|
||||
}
|
||||
}
|
||||
|
||||
func testServerRunning() bool {
|
||||
lstnr, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
lstnr.Close()
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user