harbor/src/pkg/scan/rest/v1/client.go

293 lines
8.1 KiB
Go

// Copyright Project Harbor Authors
//
// 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.
package v1
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"time"
"github.com/goharbor/harbor/src/jobservice/logger"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/pkg/scan/rest/auth"
)
const (
// defaultRefreshInterval is the default interval with seconds of refreshing report
defaultRefreshInterval = 5
// refreshAfterHeader provides the refresh interval value
refreshAfterHeader = "Refresh-After"
)
// Client defines the methods to access the adapter services that
// implement the REST API specs
type Client interface {
// GetMetadata gets the metadata of the given scanner
//
// Returns:
// *ScannerAdapterMetadata : metadata of the given scanner
// error : non nil error if any errors occurred
GetMetadata() (*ScannerAdapterMetadata, error)
// SubmitScan initiates a scanning of the given artifact.
// Returns `nil` if the request was accepted, a non `nil` error otherwise.
//
// Arguments:
// req *ScanRequest : request including the registry and artifact data
//
// Returns:
// *ScanResponse : response with UUID for tracking the scan results
// error : non nil error if any errors occurred
SubmitScan(req *ScanRequest) (*ScanResponse, error)
// GetScanReport gets the scan result for the corresponding ScanRequest identifier.
// Note that this is a blocking method which either returns a non `nil` scan report or error.
// A caller is supposed to cast the returned interface{} to a structure that corresponds
// to the specified MIME type.
//
// Arguments:
// scanRequestID string : the ID of the scan submitted before
// reportMIMEType string : the report mime type
// Returns:
// string : the scan report of the given artifact
// error : non nil error if any errors occurred
GetScanReport(scanRequestID, reportMIMEType string, urlParameter string) (string, error)
}
// basicClient is default implementation of the Client interface
type basicClient struct {
httpClient *http.Client
spec *Spec
authorizer auth.Authorizer
}
// NewClient news a basic client
func NewClient(url, authType, accessCredential string, skipCertVerify bool) (Client, error) {
transport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
MaxIdleConns: 100,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: skipCertVerify,
},
}
authorizer, err := auth.GetAuthorizer(authType, accessCredential)
if err != nil {
return nil, errors.Wrap(err, "new v1 client")
}
return &basicClient{
httpClient: &http.Client{
Timeout: time.Second * 5,
Transport: transport,
CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
return http.ErrUseLastResponse
},
},
spec: NewSpec(url),
authorizer: authorizer,
}, nil
}
// GetMetadata ...
func (c *basicClient) GetMetadata() (*ScannerAdapterMetadata, error) {
def := c.spec.Metadata()
request, err := http.NewRequest(http.MethodGet, def.URL, nil)
if err != nil {
return nil, errors.Wrap(err, "v1 client: get metadata")
}
// Resolve header
def.Resolver(request)
// Send request
respData, err := c.send(request, generalResponseHandler(http.StatusOK))
if err != nil {
return nil, errors.Wrap(err, "v1 client: get metadata")
}
// Unmarshal data
meta := &ScannerAdapterMetadata{}
if err := json.Unmarshal(respData, meta); err != nil {
return nil, errors.Wrap(err, "v1 client: get metadata")
}
return meta, nil
}
// SubmitScan ...
func (c *basicClient) SubmitScan(req *ScanRequest) (*ScanResponse, error) {
if req == nil {
return nil, errors.New("nil request")
}
data, err := json.Marshal(req)
if err != nil {
return nil, errors.Wrap(err, "v1 client: submit scan")
}
def := c.spec.SubmitScan()
request, err := http.NewRequest(http.MethodPost, def.URL, bytes.NewReader(data))
if err != nil {
return nil, errors.Wrap(err, "v1 client: submit scan")
}
// Resolve header
def.Resolver(request)
respData, err := c.send(request, generalResponseHandler(http.StatusAccepted))
if err != nil {
return nil, errors.Wrap(err, "v1 client: submit scan")
}
resp := &ScanResponse{}
if err := json.Unmarshal(respData, resp); err != nil {
return nil, errors.Wrap(err, "v1 client: submit scan")
}
return resp, nil
}
// GetScanReport ...
func (c *basicClient) GetScanReport(scanRequestID, reportMIMEType string, urlParameter string) (string, error) {
if len(scanRequestID) == 0 {
return "", errors.New("empty scan request ID")
}
if len(reportMIMEType) == 0 {
return "", errors.New("missing report mime type")
}
def := c.spec.GetScanReport(scanRequestID, reportMIMEType)
reportURL := def.URL
if len(urlParameter) > 0 {
reportURL = fmt.Sprintf("%s?%s", def.URL, urlParameter)
}
req, err := http.NewRequest(http.MethodGet, reportURL, nil)
if err != nil {
return "", errors.Wrap(err, "v1 client: get scan report")
}
// Resolve header
def.Resolver(req)
respData, err := c.send(req, reportResponseHandler())
if err != nil {
// This error should not be wrapped
return "", err
}
return string(respData), nil
}
func (c *basicClient) send(req *http.Request, h responseHandler) ([]byte, error) {
if c.authorizer != nil {
if err := c.authorizer.Authorize(req); err != nil {
return nil, errors.Wrap(err, "send: authorization")
}
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer func() {
if err := resp.Body.Close(); err != nil {
// Just logged
logger.Errorf("close response body error: %s", err)
}
}()
return h(resp.StatusCode, resp)
}
// responseHandlerFunc is a handler func template for handling the http response data,
// especially the error part.
type responseHandler func(code int, resp *http.Response) ([]byte, error)
// generalResponseHandler create a general response handler to cover the common cases.
func generalResponseHandler(expectedCode int) responseHandler {
return func(code int, resp *http.Response) ([]byte, error) {
return generalRespHandlerFunc(expectedCode, code, resp)
}
}
// reportResponseHandler creates response handler for get report special case.
func reportResponseHandler() responseHandler {
return func(code int, resp *http.Response) ([]byte, error) {
if code == http.StatusFound {
// Set default
retryAfter := defaultRefreshInterval // seconds
// Read `retry after` info from header
v := resp.Header.Get(refreshAfterHeader)
if len(v) > 0 {
if i, err := strconv.ParseInt(v, 10, 8); err == nil {
retryAfter = int(i)
} else {
// log error
logger.Errorf("Parse `%s` error: %s", refreshAfterHeader, err)
}
}
return nil, &ReportNotReadyError{RetryAfter: retryAfter}
}
return generalRespHandlerFunc(http.StatusOK, code, resp)
}
}
// generalRespHandlerFunc is a handler to cover the general cases
func generalRespHandlerFunc(expectedCode, code int, resp *http.Response) ([]byte, error) {
buf, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if code != expectedCode {
if len(buf) > 0 {
// Try to read error response
eResp := &ErrorResponse{
Err: &Error{},
}
err := json.Unmarshal(buf, eResp)
if err != nil {
return nil, errors.Wrap(err, "general response handler")
}
// Append more contexts
eResp.Err.Message = fmt.Sprintf(
"%s: general response handler: unexpected status code: %d, expected: %d",
eResp.Err.Message,
code,
expectedCode,
)
return nil, eResp
}
return nil, errors.Errorf("general response handler: unexpected status code: %d, expected: %d", code, expectedCode)
}
return buf, nil
}