Deduplicate concurrent credential verification requests via singleflight (#4314)
* Cache verification info for reuse * updated hashing * tried fixing concurrent verification issue * Added singleflight to avoid concurrent verification * resolved linter * Ok I tried something new for a much simpler detector * some enhancements * Added test case * re-added tags * Deduplicate concurrent credential verification requests via singleflight * Enforce dedup key via DoWithDedup, remove public WithDedupKey * remove unused func * Remove dead io.Copy after io.ReadAll error in singleflight transport * Preserve deadline on shared singleflight request after WithoutCancel * Added test cases and moved existing ones to http_test.go * fixed linter
This commit is contained in:
@@ -18,7 +18,7 @@ type Scanner struct{}
|
|||||||
var _ detectors.Detector = (*Scanner)(nil)
|
var _ detectors.Detector = (*Scanner)(nil)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
client = common.SaneHttpClient()
|
client = detectors.NewClientWithDedup(common.SaneHttpClient())
|
||||||
|
|
||||||
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
|
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
|
||||||
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"cliengo"}) + `\b([0-9a-f]{8}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{12})\b`)
|
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"cliengo"}) + `\b([0-9a-f]{8}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{12})\b`)
|
||||||
@@ -50,7 +50,7 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
res, err := client.Do(req)
|
res, err := detectors.DoWithDedup(client, detector_typepb.DetectorType_Cliengo, resMatch, req)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode >= 200 && res.StatusCode < 300 {
|
if res.StatusCode >= 200 && res.StatusCode < 300 {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ type Scanner struct{}
|
|||||||
var _ detectors.Detector = (*Scanner)(nil)
|
var _ detectors.Detector = (*Scanner)(nil)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
client = common.SaneHttpClient()
|
client = detectors.NewClientWithDedup(common.SaneHttpClient())
|
||||||
|
|
||||||
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
|
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
|
||||||
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"clockify"}) + `\b([a-zA-Z0-9]{48})\b`)
|
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"clockify"}) + `\b([a-zA-Z0-9]{48})\b`)
|
||||||
@@ -52,7 +52,7 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
|||||||
}
|
}
|
||||||
req.Header.Add("content-type", "application/json")
|
req.Header.Add("content-type", "application/json")
|
||||||
req.Header.Add("X-Api-Key", resMatch)
|
req.Header.Add("X-Api-Key", resMatch)
|
||||||
res, err := client.Do(req)
|
res, err := detectors.DoWithDedup(client, detector_typepb.DetectorType_Clockify, resMatch, req)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode >= 200 && res.StatusCode < 300 {
|
if res.StatusCode >= 200 && res.StatusCode < 300 {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ type Scanner struct{}
|
|||||||
var _ detectors.Detector = (*Scanner)(nil)
|
var _ detectors.Detector = (*Scanner)(nil)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
client = common.SaneHttpClient()
|
client = detectors.NewClientWithDedup(common.SaneHttpClient())
|
||||||
|
|
||||||
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives
|
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives
|
||||||
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"databox"}) + common.BuildRegex(common.RegexPattern, "", 21))
|
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"databox"}) + common.BuildRegex(common.RegexPattern, "", 21))
|
||||||
@@ -65,7 +65,7 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
|||||||
req.Header.Add("Content-Type", "application/json")
|
req.Header.Add("Content-Type", "application/json")
|
||||||
req.Header.Add("Accept", "application/vnd.databox.v2+json")
|
req.Header.Add("Accept", "application/vnd.databox.v2+json")
|
||||||
req.Header.Add("Authorization", fmt.Sprintf("Basic %s", sEnc))
|
req.Header.Add("Authorization", fmt.Sprintf("Basic %s", sEnc))
|
||||||
res, err := client.Do(req)
|
res, err := detectors.DoWithDedup(client, detector_typepb.DetectorType_Databox, resMatch, req)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode >= 200 && res.StatusCode < 300 {
|
if res.StatusCode >= 200 && res.StatusCode < 300 {
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"math/big"
|
"math/big"
|
||||||
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
@@ -335,3 +337,20 @@ func ParseURLAndStripPathAndParams(u string) (*url.URL, error) {
|
|||||||
parsedURL.RawQuery = ""
|
parsedURL.RawQuery = ""
|
||||||
return parsedURL, nil
|
return parsedURL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type dedupKeyContextKey struct{}
|
||||||
|
|
||||||
|
func withDedupKey(ctx context.Context, detType detector_typepb.DetectorType, credential string) context.Context {
|
||||||
|
key := fmt.Sprintf("%d:%s", int32(detType), credential)
|
||||||
|
return context.WithValue(ctx, dedupKeyContextKey{}, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DoWithDedup executes req through client, coalescing concurrent requests that share
|
||||||
|
// the same detector type and credential into a single network call via singleflight.
|
||||||
|
// The response body is fully buffered and replayed to every waiting caller.
|
||||||
|
//
|
||||||
|
// Use this instead of client.Do for all verification requests on a client created
|
||||||
|
// with NewClientWithDedup or WithDedup — it is the only way to activate deduplication.
|
||||||
|
func DoWithDedup(client *http.Client, detType detector_typepb.DetectorType, credential string, req *http.Request) (*http.Response, error) {
|
||||||
|
return client.Do(req.WithContext(withDedupKey(req.Context(), detType, credential)))
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ type Scanner struct{}
|
|||||||
var _ detectors.Detector = (*Scanner)(nil)
|
var _ detectors.Detector = (*Scanner)(nil)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
client = common.SaneHttpClient()
|
client = detectors.NewClientWithDedup(common.SaneHttpClient())
|
||||||
|
|
||||||
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
|
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
|
||||||
keyPat = regexp.MustCompile(`\b(API_KEY[0-9A-Z]{32})\b`)
|
keyPat = regexp.MustCompile(`\b(API_KEY[0-9A-Z]{32})\b`)
|
||||||
@@ -52,7 +52,7 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
req.Header.Add("Content-Type", "application/json")
|
req.Header.Add("Content-Type", "application/json")
|
||||||
res, err := client.Do(req)
|
res, err := detectors.DoWithDedup(client, detector_typepb.DetectorType_Finage, resMatch, req)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode >= 200 && res.StatusCode < 300 {
|
if res.StatusCode >= 200 && res.StatusCode < 300 {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ type Scanner struct{}
|
|||||||
var _ detectors.Detector = (*Scanner)(nil)
|
var _ detectors.Detector = (*Scanner)(nil)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
client = common.SaneHttpClient()
|
client = detectors.NewClientWithDedup(common.SaneHttpClient())
|
||||||
|
|
||||||
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
|
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
|
||||||
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"geocode"}) + `\b([a-z0-9]{28})\b`)
|
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"geocode"}) + `\b([a-z0-9]{28})\b`)
|
||||||
@@ -52,7 +52,7 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
res, err := client.Do(req)
|
res, err := detectors.DoWithDedup(client, detector_typepb.DetectorType_Geocode, resMatch, req)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
bodyBytes, err := io.ReadAll(res.Body)
|
bodyBytes, err := io.ReadAll(res.Body)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ type Scanner struct{}
|
|||||||
var _ detectors.Detector = (*Scanner)(nil)
|
var _ detectors.Detector = (*Scanner)(nil)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
client = common.SaneHttpClient()
|
client = detectors.NewClientWithDedup(common.SaneHttpClient())
|
||||||
|
|
||||||
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
|
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
|
||||||
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"gitter"}) + `\b([a-z0-9-]{40})\b`)
|
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"gitter"}) + `\b([a-z0-9-]{40})\b`)
|
||||||
@@ -52,7 +52,7 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch))
|
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch))
|
||||||
res, err := client.Do(req)
|
res, err := detectors.DoWithDedup(client, detector_typepb.DetectorType_Gitter, resMatch, req)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode >= 200 && res.StatusCode < 300 {
|
if res.StatusCode >= 200 && res.StatusCode < 300 {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ type Scanner struct{}
|
|||||||
var _ detectors.Detector = (*Scanner)(nil)
|
var _ detectors.Detector = (*Scanner)(nil)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
client = common.SaneHttpClient()
|
client = detectors.NewClientWithDedup(common.SaneHttpClient())
|
||||||
|
|
||||||
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
|
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
|
||||||
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"holidayapi"}) + `\b([a-z0-9-]{36})\b`)
|
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"holidayapi"}) + `\b([a-z0-9-]{36})\b`)
|
||||||
@@ -51,7 +51,7 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
res, err := client.Do(req)
|
res, err := detectors.DoWithDedup(client, detector_typepb.DetectorType_HolidayAPI, resMatch, req)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode >= 200 && res.StatusCode < 300 {
|
if res.StatusCode >= 200 && res.StatusCode < 300 {
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
package detectors
|
package detectors
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"slices"
|
"slices"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/sync/singleflight"
|
||||||
|
|
||||||
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
|
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
|
||||||
"github.com/trufflesecurity/trufflehog/v3/pkg/feature"
|
"github.com/trufflesecurity/trufflehog/v3/pkg/feature"
|
||||||
)
|
)
|
||||||
@@ -175,3 +180,93 @@ func NewDetectorHttpClient(opts ...ClientOption) *http.Client {
|
|||||||
client.Transport = common.NewInstrumentedTransport(client.Transport)
|
client.Transport = common.NewInstrumentedTransport(client.Transport)
|
||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// bufferedResponse holds a fully-read HTTP response so it can be replayed to
|
||||||
|
// every goroutine that was coalesced by singleflight.
|
||||||
|
type bufferedResponse struct {
|
||||||
|
statusCode int
|
||||||
|
header http.Header
|
||||||
|
body []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// singleflightTransport is an http.RoundTripper that coalesces concurrent requests
|
||||||
|
// sharing the same deduplication key into a single network call. It is a no-op for
|
||||||
|
// requests whose context does not carry a dedup key.
|
||||||
|
type singleflightTransport struct {
|
||||||
|
base http.RoundTripper
|
||||||
|
group singleflight.Group
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *singleflightTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
|
key, ok := req.Context().Value(dedupKeyContextKey{}).(string)
|
||||||
|
if !ok || key == "" {
|
||||||
|
return t.base.RoundTrip(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DoChan is used instead of Do so each caller can independently respect its
|
||||||
|
// own context cancellation without blocking on the shared in-flight call.
|
||||||
|
ch := t.group.DoChan(key, func() (any, error) {
|
||||||
|
// Detach the in-flight request from the first caller's cancellation so
|
||||||
|
// that one goroutine timing out doesn't abort the shared network call
|
||||||
|
// and propagate an error to all coalesced waiters.
|
||||||
|
//
|
||||||
|
// context.WithoutCancel also strips any deadline (e.g. from
|
||||||
|
// http.Client.Timeout), so we re-attach the original deadline if
|
||||||
|
// present. Without this the shared request has no timeout and a
|
||||||
|
// hanging server would leak the goroutine and pin the singleflight
|
||||||
|
// key indefinitely.
|
||||||
|
sharedCtx := context.WithoutCancel(req.Context())
|
||||||
|
if deadline, ok := req.Context().Deadline(); ok {
|
||||||
|
var cancel context.CancelFunc
|
||||||
|
sharedCtx, cancel = context.WithDeadline(sharedCtx, deadline)
|
||||||
|
defer cancel()
|
||||||
|
}
|
||||||
|
sharedReq := req.WithContext(sharedCtx)
|
||||||
|
resp, err := t.base.RoundTrip(sharedReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &bufferedResponse{
|
||||||
|
statusCode: resp.StatusCode,
|
||||||
|
header: resp.Header.Clone(),
|
||||||
|
body: body,
|
||||||
|
}, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
select {
|
||||||
|
case result := <-ch:
|
||||||
|
if result.Err != nil {
|
||||||
|
return nil, result.Err
|
||||||
|
}
|
||||||
|
br := result.Val.(*bufferedResponse)
|
||||||
|
return &http.Response{
|
||||||
|
StatusCode: br.statusCode,
|
||||||
|
Status: fmt.Sprintf("%d %s", br.statusCode, http.StatusText(br.statusCode)),
|
||||||
|
Header: br.header.Clone(),
|
||||||
|
Body: io.NopCloser(bytes.NewReader(br.body)),
|
||||||
|
}, nil
|
||||||
|
case <-req.Context().Done():
|
||||||
|
return nil, req.Context().Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClientWithDedup wraps base with a transport that deduplicates concurrent
|
||||||
|
// verification requests sharing the same key. Detectors opt in per credential by
|
||||||
|
// calling WithDedupKey on the request context before client.Do — no other changes
|
||||||
|
// to request building or response reading are needed.
|
||||||
|
func NewClientWithDedup(base *http.Client) *http.Client {
|
||||||
|
clone := *base
|
||||||
|
transport := base.Transport
|
||||||
|
if transport == nil {
|
||||||
|
transport = http.DefaultTransport
|
||||||
|
}
|
||||||
|
clone.Transport = &singleflightTransport{base: transport}
|
||||||
|
return &clone
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,11 +2,18 @@ package detectors
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestWithNoLocalIP(t *testing.T) {
|
func TestWithNoLocalIP(t *testing.T) {
|
||||||
@@ -95,6 +102,213 @@ func TestWithNoLocalIP(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestDoWithDedup_Singleflight verifies that concurrent DoWithDedup calls sharing the
|
||||||
|
// same detector type and credential are coalesced into one network call. Each request
|
||||||
|
// the server receives returns a distinct body, so all goroutines should observe the
|
||||||
|
// body from exactly one actual server-side request.
|
||||||
|
func TestDoWithDedup_Singleflight(t *testing.T) {
|
||||||
|
var requestCount int32
|
||||||
|
|
||||||
|
// The 20 ms sleep keeps the first request in-flight long enough for all
|
||||||
|
// goroutines to call DoWithDedup before the result is ready.
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
n := atomic.AddInt32(&requestCount, 1)
|
||||||
|
time.Sleep(20 * time.Millisecond)
|
||||||
|
fmt.Fprintf(w, `{"request":%d}`, n)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := NewClientWithDedup(server.Client())
|
||||||
|
|
||||||
|
const goroutines = 5
|
||||||
|
bodies := make([]string, goroutines)
|
||||||
|
statuses := make([]int, goroutines)
|
||||||
|
errs := make([]error, goroutines)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for i := range goroutines {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(i int) {
|
||||||
|
defer wg.Done()
|
||||||
|
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, server.URL, http.NoBody)
|
||||||
|
if err != nil {
|
||||||
|
errs[i] = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := DoWithDedup(client, detector_typepb.DetectorType_Meraki, "test-credential", req)
|
||||||
|
if err != nil {
|
||||||
|
errs[i] = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
var buf [512]byte
|
||||||
|
n, _ := resp.Body.Read(buf[:])
|
||||||
|
bodies[i] = string(buf[:n])
|
||||||
|
statuses[i] = resp.StatusCode
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
for _, err := range errs {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
for _, s := range statuses {
|
||||||
|
assert.Equal(t, http.StatusOK, s)
|
||||||
|
}
|
||||||
|
assert.Equal(t, int32(1), atomic.LoadInt32(&requestCount),
|
||||||
|
"singleflight should coalesce all concurrent calls into one HTTP request")
|
||||||
|
for i := 1; i < goroutines; i++ {
|
||||||
|
assert.Equal(t, bodies[0], bodies[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDoWithDedup_WaiterContextCancelled verifies that a waiter whose context is
|
||||||
|
// cancelled bails out with an error while other waiters still receive the response,
|
||||||
|
// and only one HTTP request is made.
|
||||||
|
func TestDoWithDedup_WaiterContextCancelled(t *testing.T) {
|
||||||
|
var requestCount int32
|
||||||
|
// inFlight is closed by the server once it starts handling the request,
|
||||||
|
// giving us a reliable signal to cancel one waiter mid-flight.
|
||||||
|
inFlight := make(chan struct{})
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
atomic.AddInt32(&requestCount, 1)
|
||||||
|
close(inFlight)
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := NewClientWithDedup(server.Client())
|
||||||
|
|
||||||
|
ctx1, cancel1 := context.WithCancel(context.Background())
|
||||||
|
defer cancel1()
|
||||||
|
|
||||||
|
type result struct {
|
||||||
|
status int
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
results := make([]result, 3)
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
for i, ctx := range []context.Context{ctx1, context.Background(), context.Background()} {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(i int, ctx context.Context) {
|
||||||
|
defer wg.Done()
|
||||||
|
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, server.URL, http.NoBody)
|
||||||
|
resp, err := DoWithDedup(client, detector_typepb.DetectorType_Meraki, "cred", req)
|
||||||
|
if err != nil {
|
||||||
|
results[i] = result{err: err}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
results[i] = result{status: resp.StatusCode}
|
||||||
|
}(i, ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
<-inFlight
|
||||||
|
cancel1()
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
assert.ErrorIs(t, results[0].err, context.Canceled, "cancelled waiter should get context error")
|
||||||
|
assert.NoError(t, results[1].err)
|
||||||
|
assert.Equal(t, http.StatusOK, results[1].status)
|
||||||
|
assert.NoError(t, results[2].err)
|
||||||
|
assert.Equal(t, http.StatusOK, results[2].status)
|
||||||
|
assert.Equal(t, int32(1), atomic.LoadInt32(&requestCount), "only one HTTP request should be made")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDoWithDedup_FirstCallerContextCancelled verifies that cancelling the first
|
||||||
|
// caller's context does not abort the shared in-flight HTTP call: the second caller
|
||||||
|
// should still receive a valid response, and only one HTTP request is made.
|
||||||
|
func TestDoWithDedup_FirstCallerContextCancelled(t *testing.T) {
|
||||||
|
var requestCount int32
|
||||||
|
inFlight := make(chan struct{})
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
atomic.AddInt32(&requestCount, 1)
|
||||||
|
close(inFlight)
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := NewClientWithDedup(server.Client())
|
||||||
|
|
||||||
|
ctx1, cancel1 := context.WithCancel(context.Background())
|
||||||
|
defer cancel1()
|
||||||
|
|
||||||
|
var firstErr error
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
req, _ := http.NewRequestWithContext(ctx1, http.MethodGet, server.URL, http.NoBody)
|
||||||
|
resp, err := DoWithDedup(client, detector_typepb.DetectorType_Meraki, "cred", req)
|
||||||
|
if err != nil {
|
||||||
|
firstErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Cancel the first caller once the server is processing, then immediately
|
||||||
|
// start a second caller that should coalesce into the still-running call.
|
||||||
|
<-inFlight
|
||||||
|
cancel1()
|
||||||
|
|
||||||
|
var secondStatus int
|
||||||
|
var secondErr error
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, server.URL, http.NoBody)
|
||||||
|
resp, err := DoWithDedup(client, detector_typepb.DetectorType_Meraki, "cred", req)
|
||||||
|
if err != nil {
|
||||||
|
secondErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
secondStatus = resp.StatusCode
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
assert.ErrorIs(t, firstErr, context.Canceled, "first caller should get context error")
|
||||||
|
assert.NoError(t, secondErr, "second caller should succeed despite first caller's cancellation")
|
||||||
|
assert.Equal(t, http.StatusOK, secondStatus)
|
||||||
|
assert.Equal(t, int32(1), atomic.LoadInt32(&requestCount), "only one HTTP request should be made")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDoWithDedup_DeadlinePreserved verifies that the client timeout still applies
|
||||||
|
// to the shared in-flight call after context.WithoutCancel strips cancellation.
|
||||||
|
// A hanging server must not cause an indefinite leak.
|
||||||
|
func TestDoWithDedup_DeadlinePreserved(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
select {
|
||||||
|
case <-time.After(10 * time.Second):
|
||||||
|
case <-r.Context().Done():
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
base := server.Client()
|
||||||
|
base.Timeout = 75 * time.Millisecond
|
||||||
|
client := NewClientWithDedup(base)
|
||||||
|
|
||||||
|
req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, server.URL, http.NoBody)
|
||||||
|
start := time.Now()
|
||||||
|
resp, err := DoWithDedup(client, detector_typepb.DetectorType_Meraki, "cred", req)
|
||||||
|
if err == nil {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
elapsed := time.Since(start)
|
||||||
|
|
||||||
|
assert.Error(t, err, "request to hanging server should fail")
|
||||||
|
assert.Less(t, elapsed, time.Second, "timeout should be enforced by client deadline, not run indefinitely")
|
||||||
|
}
|
||||||
|
|
||||||
func TestIsLocalIP(t *testing.T) {
|
func TestIsLocalIP(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ type Scanner struct {
|
|||||||
var _ detectors.Detector = (*Scanner)(nil)
|
var _ detectors.Detector = (*Scanner)(nil)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
defaultClient = common.SaneHttpClient()
|
defaultClient = detectors.NewClientWithDedup(common.SaneHttpClient())
|
||||||
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
|
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
|
||||||
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"ipinfo"}) + `\b([a-f0-9]{14})\b`)
|
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"ipinfo"}) + `\b([a-f0-9]{14})\b`)
|
||||||
)
|
)
|
||||||
@@ -56,7 +56,7 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
res, err := client.Do(req)
|
res, err := detectors.DoWithDedup(client, detector_typepb.DetectorType_IPInfo, resMatch, req)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode >= 200 && res.StatusCode < 300 {
|
if res.StatusCode >= 200 && res.StatusCode < 300 {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ type Scanner struct{}
|
|||||||
var _ detectors.Detector = (*Scanner)(nil)
|
var _ detectors.Detector = (*Scanner)(nil)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
client = common.SaneHttpClient()
|
client = detectors.NewClientWithDedup(common.SaneHttpClient())
|
||||||
|
|
||||||
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
|
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
|
||||||
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"loadmill"}) + `\b([0-9a-zA-Z]{40})\b`)
|
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"loadmill"}) + `\b([0-9a-zA-Z]{40})\b`)
|
||||||
@@ -52,7 +52,7 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
|||||||
}
|
}
|
||||||
req.Header.Add("Accept", "application/vnd.loadmill+json; version=3")
|
req.Header.Add("Accept", "application/vnd.loadmill+json; version=3")
|
||||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch))
|
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch))
|
||||||
res, err := client.Do(req)
|
res, err := detectors.DoWithDedup(client, detector_typepb.DetectorType_Loadmill, resMatch, req)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode >= 200 && res.StatusCode < 300 {
|
if res.StatusCode >= 200 && res.StatusCode < 300 {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ type merakiOrganizations struct {
|
|||||||
var _ detectors.Detector = (*Scanner)(nil)
|
var _ detectors.Detector = (*Scanner)(nil)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
defaultClient = common.SaneHttpClient()
|
defaultClient = detectors.NewClientWithDedup(common.SaneHttpClient())
|
||||||
|
|
||||||
apiKey = regexp.MustCompile(detectors.PrefixRegex([]string{"meraki"}) + `([0-9a-f]{40})`)
|
apiKey = regexp.MustCompile(detectors.PrefixRegex([]string{"meraki"}) + `([0-9a-f]{40})`)
|
||||||
)
|
)
|
||||||
@@ -57,9 +57,7 @@ func (s Scanner) Description() string {
|
|||||||
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
|
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
|
||||||
dataStr := string(data)
|
dataStr := string(data)
|
||||||
|
|
||||||
// uniqueMatches will hold unique match values and ensure we only process unique matches found in the data string
|
uniqueMatches := make(map[string]struct{})
|
||||||
var uniqueMatches = make(map[string]struct{})
|
|
||||||
|
|
||||||
for _, match := range apiKey.FindAllStringSubmatch(dataStr, -1) {
|
for _, match := range apiKey.FindAllStringSubmatch(dataStr, -1) {
|
||||||
uniqueMatches[match[1]] = struct{}{}
|
uniqueMatches[match[1]] = struct{}{}
|
||||||
}
|
}
|
||||||
@@ -107,11 +105,9 @@ func verifyMerakiApiKey(ctx context.Context, client *http.Client, match string)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// set the required auth header
|
|
||||||
req.Header.Set("X-Cisco-Meraki-API-Key", match)
|
req.Header.Set("X-Cisco-Meraki-API-Key", match)
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
resp, err := detectors.DoWithDedup(client, detector_typepb.DetectorType_Meraki, match, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
@@ -122,12 +118,10 @@ func verifyMerakiApiKey(ctx context.Context, client *http.Client, match string)
|
|||||||
|
|
||||||
switch resp.StatusCode {
|
switch resp.StatusCode {
|
||||||
case http.StatusOK:
|
case http.StatusOK:
|
||||||
// in case token is verified, capture the organization id's and name which are accessible via token.
|
|
||||||
var organizations []merakiOrganizations
|
var organizations []merakiOrganizations
|
||||||
if err = json.NewDecoder(resp.Body).Decode(&organizations); err != nil {
|
if err = json.NewDecoder(resp.Body).Decode(&organizations); err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return organizations, true, nil
|
return organizations, true, nil
|
||||||
case http.StatusUnauthorized:
|
case http.StatusUnauthorized:
|
||||||
return nil, false, nil
|
return nil, false, nil
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ type Scanner struct{}
|
|||||||
var _ detectors.Detector = (*Scanner)(nil)
|
var _ detectors.Detector = (*Scanner)(nil)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
client = common.SaneHttpClient()
|
client = detectors.NewClientWithDedup(common.SaneHttpClient())
|
||||||
|
|
||||||
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
|
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
|
||||||
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"mixmax"}) + `\b([a-zA-Z0-9_-]{36})\b`)
|
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"mixmax"}) + `\b([a-zA-Z0-9_-]{36})\b`)
|
||||||
@@ -51,7 +51,7 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Add("X-API-Token", resMatch)
|
req.Header.Add("X-API-Token", resMatch)
|
||||||
res, err := client.Do(req)
|
res, err := detectors.DoWithDedup(client, detector_typepb.DetectorType_Mixmax, resMatch, req)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode >= 200 && res.StatusCode < 300 {
|
if res.StatusCode >= 200 && res.StatusCode < 300 {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ type Scanner struct{}
|
|||||||
var _ detectors.Detector = (*Scanner)(nil)
|
var _ detectors.Detector = (*Scanner)(nil)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
client = common.SaneHttpClient()
|
client = detectors.NewClientWithDedup(common.SaneHttpClient())
|
||||||
|
|
||||||
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
|
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
|
||||||
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"opencagedata"}) + `\b([a-z0-9]{32})\b`)
|
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"opencagedata"}) + `\b([a-z0-9]{32})\b`)
|
||||||
@@ -50,7 +50,7 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
res, err := client.Do(req)
|
res, err := detectors.DoWithDedup(client, detector_typepb.DetectorType_OpenCageData, resMatch, req)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode >= 200 && res.StatusCode < 300 {
|
if res.StatusCode >= 200 && res.StatusCode < 300 {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ type Scanner struct{}
|
|||||||
var _ detectors.Detector = (*Scanner)(nil)
|
var _ detectors.Detector = (*Scanner)(nil)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
client = common.SaneHttpClient()
|
client = detectors.NewClientWithDedup(common.SaneHttpClient())
|
||||||
|
|
||||||
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
|
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
|
||||||
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"proxycrawl"}) + `\b([a-zA-Z0-9_]{22})\b`)
|
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"proxycrawl"}) + `\b([a-zA-Z0-9_]{22})\b`)
|
||||||
@@ -53,7 +53,7 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
res, err := client.Do(req)
|
res, err := detectors.DoWithDedup(client, detector_typepb.DetectorType_ProxyCrawl, resMatch, req)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode >= 200 && res.StatusCode < 300 {
|
if res.StatusCode >= 200 && res.StatusCode < 300 {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ type Scanner struct{}
|
|||||||
var _ detectors.Detector = (*Scanner)(nil)
|
var _ detectors.Detector = (*Scanner)(nil)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
client = common.SaneHttpClient()
|
client = detectors.NewClientWithDedup(common.SaneHttpClient())
|
||||||
|
|
||||||
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives
|
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives
|
||||||
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"tickettailor"}) + `\b(sk_[0-9]{4}_[0-9]{6}_[a-f0-9]{32})`)
|
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"tickettailor"}) + `\b(sk_[0-9]{4}_[0-9]{6}_[a-f0-9]{32})`)
|
||||||
@@ -77,7 +77,7 @@ func verifyTicketTailor(ctx context.Context, client *http.Client, apiKey string)
|
|||||||
req.Header.Add("Accept", "application/json")
|
req.Header.Add("Accept", "application/json")
|
||||||
// as per API docs we only need to use apiKey as username in basic auth and leave password as empty: https://developers.tickettailor.com/#authentication
|
// as per API docs we only need to use apiKey as username in basic auth and leave password as empty: https://developers.tickettailor.com/#authentication
|
||||||
req.SetBasicAuth(apiKey, "")
|
req.SetBasicAuth(apiKey, "")
|
||||||
resp, err := client.Do(req)
|
resp, err := detectors.DoWithDedup(client, detector_typepb.DetectorType_Tickettailor, apiKey, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ type Scanner struct{}
|
|||||||
var _ detectors.Detector = (*Scanner)(nil)
|
var _ detectors.Detector = (*Scanner)(nil)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
client = common.SaneHttpClient()
|
client = detectors.NewClientWithDedup(common.SaneHttpClient())
|
||||||
|
|
||||||
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
|
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
|
||||||
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"toggl"}) + `\b([0-9Aa-z]{32})\b`)
|
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"toggl"}) + `\b([0-9Aa-z]{32})\b`)
|
||||||
@@ -55,7 +55,7 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
req.Header.Add("Authorization", fmt.Sprintf("Basic %s", sEnc))
|
req.Header.Add("Authorization", fmt.Sprintf("Basic %s", sEnc))
|
||||||
res, err := client.Do(req)
|
res, err := detectors.DoWithDedup(client, detector_typepb.DetectorType_TogglTrack, resMatch, req)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode >= 200 && res.StatusCode < 300 {
|
if res.StatusCode >= 200 && res.StatusCode < 300 {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ type Scanner struct{}
|
|||||||
var _ detectors.Detector = (*Scanner)(nil)
|
var _ detectors.Detector = (*Scanner)(nil)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
client = common.SaneHttpClient()
|
client = detectors.NewClientWithDedup(common.SaneHttpClient())
|
||||||
|
|
||||||
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
|
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
|
||||||
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"veriphone"}) + `\b([0-9A-Z]{32})\b`)
|
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"veriphone"}) + `\b([0-9A-Z]{32})\b`)
|
||||||
@@ -50,7 +50,7 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
res, err := client.Do(req)
|
res, err := detectors.DoWithDedup(client, detector_typepb.DetectorType_Veriphone, resMatch, req)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode >= 200 && res.StatusCode < 300 {
|
if res.StatusCode >= 200 && res.StatusCode < 300 {
|
||||||
|
|||||||
+3
-1
@@ -57,9 +57,11 @@ func (p *PlainPrinter) Print(_ context.Context, r *detectors.ResultWithMetadata)
|
|||||||
yellowPrinter.Printf("Verification issue: %s\n", out.VerificationError)
|
yellowPrinter.Printf("Verification issue: %s\n", out.VerificationError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.VerificationFromCache {
|
if r.VerificationFromCache {
|
||||||
cyanPrinter.Print("(Verification info cached)\n")
|
cyanPrinter.Print("(🔍 Using cached verification)\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
printer.Printf("Detector Type: %s\n", out.DetectorType)
|
printer.Printf("Detector Type: %s\n", out.DetectorType)
|
||||||
printer.Printf("Decoder Type: %s\n", out.DecoderType)
|
printer.Printf("Decoder Type: %s\n", out.DecoderType)
|
||||||
printer.Printf("Raw result: %s\n", whitePrinter.Sprint(out.Raw))
|
printer.Printf("Raw result: %s\n", whitePrinter.Sprint(out.Raw))
|
||||||
|
|||||||
Reference in New Issue
Block a user