feat(canary): envfile generator (shuffled bait + embedded canary URL)

Generator builds the artifact per spec §9.6:

  - extractIncludeKeys(t.Metadata) reads include_keys from JSON
    metadata, defaults to ["aws", "db"] on missing/malformed metadata
    (defensive fallback covers empty raw, invalid JSON, wrong type,
    null value, empty array, missing field, all-whitespace entries)
  - buildSections iterates recipe keys, appending the canary section
    last: INTERNAL_METRICS_ENDPOINT=<baseURL>/c/<t.ID> +
    INTERNAL_METRICS_TOKEN=tok_live_<32-char alnum>
  - shuffleSections uses crypto/rand-driven Fisher-Yates so the canary
    section can land at any position. Spec §9.6 line 1301 uses
    math/rand.Shuffle; we use crypto/rand to stay consistent with the
    Phase 0 supplement choice and the recipes package
  - renderSections writes the fixed "# Production environment / NODE_ENV
    / PORT" header followed by each section's comment + key=value lines
  - Artifact: KindText, ContentType "text/plain; charset=utf-8",
    Filename default ".env"

Trigger is a byte-identical content-copy of webbug/docx/pdf: 200 +
pixel.Clone() GIF + cache headers + realIP triplet + nil-token-returns-
GIF defense-in-depth.

Tests (~30 cases):
  - Generator: Type, KindText, ContentType, Filename defaulting (4),
    header presence, canary URL embedded, canary token format
    (tok_live_+32 alnum regex), trailing-slash trim, default
    include_keys vs metadata-driven include_keys, unknown keys
    skipped, 6 malformed-metadata fallback subcases, empty-string
    filtering in keys list, shuffle variability (30 invocations →
    canary at multiple positions), distinct outputs (random bait +
    random shuffle), token id appears exactly once in output
  - Trigger: GIF response shape, source-IP precedence (11 subcases
    matching docx/pdf/kubeconfig), missing UA/Referer → nil pointers,
    response body independence (mutate one, other unchanged),
    nil-token still returns GIF + nil event

Registry wire-up next.
This commit is contained in:
CarterPerez-dev 2026-05-13 14:36:06 -04:00
parent 003aa9970c
commit 2a2a3c4d8f
2 changed files with 838 additions and 0 deletions

View File

@ -0,0 +1,244 @@
// ©AngelaMos | 2026
// generator.go
package envfile
import (
"bytes"
"context"
"crypto/rand"
"encoding/json"
"fmt"
"math/big"
"net"
"net/http"
"strings"
"github.com/CarterPerez-dev/cybersecurity-projects/canary-token-generator/backend/internal/event"
"github.com/CarterPerez-dev/cybersecurity-projects/canary-token-generator/backend/internal/token"
"github.com/CarterPerez-dev/cybersecurity-projects/canary-token-generator/backend/internal/token/generators"
"github.com/CarterPerez-dev/cybersecurity-projects/canary-token-generator/backend/internal/token/generators/envfile/recipes"
"github.com/CarterPerez-dev/cybersecurity-projects/canary-token-generator/backend/internal/token/generators/pixel"
)
const (
headerCFConnectingIP = "CF-Connecting-IP"
headerXForwardedFor = "X-Forwarded-For"
headerXRealIP = "X-Real-IP"
headerReferer = "Referer"
headerCacheControl = "Cache-Control"
headerPragma = "Pragma"
cacheControlNoStore = "no-store, no-cache, must-revalidate, max-age=0"
pragmaNoCache = "no-cache"
triggerPathPrefix = "/c/"
metadataIncludeKey = "include_keys"
contentType = "text/plain; charset=utf-8"
defaultFilename = ".env"
canaryComment = "Internal monitoring (Datadog-style integration)"
canaryEndpointKey = "INTERNAL_METRICS_ENDPOINT"
canaryTokenKey = "INTERNAL_METRICS_TOKEN"
canaryTokenPrefix = "tok_live_"
canaryTokenLength = 32
envfileHeader = "# Production environment\n" +
"NODE_ENV=production\n" +
"PORT=8080\n\n"
)
var defaultIncludeKeys = []string{"aws", "db"}
type Generator struct{}
func New() *Generator { return &Generator{} }
func (g *Generator) Type() token.Type { return token.TypeEnvfile }
func (g *Generator) Generate(
_ context.Context,
t *token.Token,
baseURL string,
) (generators.Artifact, error) {
keys := extractIncludeKeys(t.Metadata)
triggerURL := strings.TrimRight(baseURL, "/") + triggerPathPrefix + t.ID
sections := buildSections(keys, triggerURL)
if err := shuffleSections(sections); err != nil {
return generators.Artifact{}, fmt.Errorf(
"envfile: shuffle sections: %w",
err,
)
}
body := renderSections(sections)
return generators.Artifact{
Kind: generators.KindText,
Filename: resolveFilename(t.Filename),
Content: body,
ContentType: contentType,
}, nil
}
func (g *Generator) Trigger(
_ context.Context,
t *token.Token,
r *http.Request,
) (*event.Event, *generators.TriggerResponse, error) {
resp := &generators.TriggerResponse{
StatusCode: http.StatusOK,
ContentType: pixel.ContentType,
Body: pixel.Clone(),
ExtraHeaders: map[string]string{
headerCacheControl: cacheControlNoStore,
headerPragma: pragmaNoCache,
},
}
if t == nil {
return nil, resp, nil
}
evt := &event.Event{
TokenID: t.ID,
SourceIP: realIP(r),
UserAgent: optionalHeader(r.UserAgent()),
Referer: optionalHeader(r.Header.Get(headerReferer)),
}
return evt, resp, nil
}
func buildSections(keys []string, triggerURL string) [][]recipes.EnvLine {
sections := make([][]recipes.EnvLine, 0, len(keys)+1)
for _, k := range keys {
if r, ok := recipes.Get(k); ok {
sections = append(sections, r.Generate())
}
}
sections = append(sections, []recipes.EnvLine{
{Comment: canaryComment},
{Key: canaryEndpointKey, Value: triggerURL},
{
Key: canaryTokenKey,
Value: canaryTokenPrefix +
recipes.RandomAlnumMixed(canaryTokenLength),
},
})
return sections
}
func shuffleSections(sections [][]recipes.EnvLine) error {
for i := len(sections) - 1; i > 0; i-- {
jBig, err := rand.Int(rand.Reader, big.NewInt(int64(i+1)))
if err != nil {
return fmt.Errorf("crypto/rand.Int: %w", err)
}
j := int(jBig.Int64())
sections[i], sections[j] = sections[j], sections[i]
}
return nil
}
func renderSections(sections [][]recipes.EnvLine) []byte {
var buf bytes.Buffer
buf.WriteString(envfileHeader)
for _, s := range sections {
for _, l := range s {
if l.Comment != "" {
fmt.Fprintf(&buf, "# %s\n", l.Comment)
}
if l.Key != "" {
fmt.Fprintf(&buf, "%s=%s\n", l.Key, l.Value)
}
}
buf.WriteString("\n")
}
return buf.Bytes()
}
func extractIncludeKeys(metadata json.RawMessage) []string {
if len(metadata) == 0 {
return cloneStrings(defaultIncludeKeys)
}
var m map[string]json.RawMessage
if err := json.Unmarshal(metadata, &m); err != nil {
return cloneStrings(defaultIncludeKeys)
}
raw, ok := m[metadataIncludeKey]
if !ok {
return cloneStrings(defaultIncludeKeys)
}
var keys []string
if err := json.Unmarshal(raw, &keys); err != nil || len(keys) == 0 {
return cloneStrings(defaultIncludeKeys)
}
out := make([]string, 0, len(keys))
for _, k := range keys {
k = strings.TrimSpace(k)
if k == "" {
continue
}
out = append(out, k)
}
if len(out) == 0 {
return cloneStrings(defaultIncludeKeys)
}
return out
}
func cloneStrings(in []string) []string {
out := make([]string, len(in))
copy(out, in)
return out
}
func resolveFilename(name *string) string {
if name == nil {
return defaultFilename
}
trimmed := strings.TrimSpace(*name)
if trimmed == "" {
return defaultFilename
}
return trimmed
}
func optionalHeader(v string) *string {
v = strings.TrimSpace(v)
if v == "" {
return nil
}
return &v
}
func realIP(r *http.Request) string {
if v := strings.TrimSpace(r.Header.Get(headerCFConnectingIP)); v != "" {
return v
}
if v := lastNonEmptyXFF(r.Header.Get(headerXForwardedFor)); v != "" {
return v
}
if v := strings.TrimSpace(r.Header.Get(headerXRealIP)); v != "" {
return v
}
if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
return host
}
return r.RemoteAddr
}
func lastNonEmptyXFF(header string) string {
if header == "" {
return ""
}
parts := strings.Split(header, ",")
for i := len(parts) - 1; i >= 0; i-- {
if v := strings.TrimSpace(parts[i]); v != "" {
return v
}
}
return ""
}

View File

@ -0,0 +1,594 @@
// ©AngelaMos | 2026
// generator_test.go
package envfile_test
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/CarterPerez-dev/cybersecurity-projects/canary-token-generator/backend/internal/token"
"github.com/CarterPerez-dev/cybersecurity-projects/canary-token-generator/backend/internal/token/generators"
"github.com/CarterPerez-dev/cybersecurity-projects/canary-token-generator/backend/internal/token/generators/envfile"
"github.com/CarterPerez-dev/cybersecurity-projects/canary-token-generator/backend/internal/token/generators/pixel"
)
const (
testBaseURL = "https://canary.example.com"
envfileContentTypeMIME = "text/plain; charset=utf-8"
defaultFilenameValue = ".env"
cacheControlNoStoreValue = "no-store, no-cache, must-revalidate, max-age=0"
pragmaNoCacheValue = "no-cache"
gifByteLength = 43
)
func newEnvfileToken(id string) *token.Token {
return &token.Token{
ID: id,
ManageID: "manage-" + id,
Type: token.TypeEnvfile,
Memo: "unit test envfile",
AlertChannel: token.ChannelWebhook,
Enabled: true,
}
}
func newEnvfileTokenWithFilename(id, filename string) *token.Token {
tok := newEnvfileToken(id)
tok.Filename = &filename
return tok
}
func newEnvfileTokenWithIncludeKeys(keys []string) *token.Token {
tok := newEnvfileToken("abc")
raw, err := json.Marshal(map[string]any{"include_keys": keys})
if err != nil {
panic(err)
}
tok.Metadata = raw
return tok
}
func newEnvfileTokenWithRawMetadata(metadata string) *token.Token {
tok := newEnvfileToken("abc")
tok.Metadata = json.RawMessage(metadata)
return tok
}
func envLines(content []byte) []string {
return strings.Split(string(content), "\n")
}
func TestGenerator_TypeIsEnvfile(t *testing.T) {
g := envfile.New()
require.Equal(t, token.TypeEnvfile, g.Type())
}
func TestGenerate_ArtifactKindIsText(t *testing.T) {
g := envfile.New()
art, err := g.Generate(
context.Background(),
newEnvfileToken("abc"),
testBaseURL,
)
require.NoError(t, err)
require.Equal(t, generators.KindText, art.Kind)
}
func TestGenerate_ContentTypeIsTextPlain(t *testing.T) {
g := envfile.New()
art, err := g.Generate(
context.Background(),
newEnvfileToken("abc"),
testBaseURL,
)
require.NoError(t, err)
require.Equal(t, envfileContentTypeMIME, art.ContentType)
}
func TestGenerate_Filename(t *testing.T) {
g := envfile.New()
t.Run("nil Filename defaults to .env", func(t *testing.T) {
art, err := g.Generate(
context.Background(),
newEnvfileToken("abc"),
testBaseURL,
)
require.NoError(t, err)
require.Equal(t, defaultFilenameValue, art.Filename)
})
t.Run("empty Filename pointer defaults to .env", func(t *testing.T) {
art, err := g.Generate(
context.Background(),
newEnvfileTokenWithFilename("abc", ""),
testBaseURL,
)
require.NoError(t, err)
require.Equal(t, defaultFilenameValue, art.Filename)
})
t.Run("whitespace Filename defaults to .env", func(t *testing.T) {
art, err := g.Generate(
context.Background(),
newEnvfileTokenWithFilename("abc", " "),
testBaseURL,
)
require.NoError(t, err)
require.Equal(t, defaultFilenameValue, art.Filename)
})
t.Run("set Filename is preserved (trimmed)", func(t *testing.T) {
art, err := g.Generate(
context.Background(),
newEnvfileTokenWithFilename("abc", " .env.production "),
testBaseURL,
)
require.NoError(t, err)
require.Equal(t, ".env.production", art.Filename)
})
}
func TestGenerate_ContainsHeader(t *testing.T) {
g := envfile.New()
art, err := g.Generate(
context.Background(),
newEnvfileToken("abc"),
testBaseURL,
)
require.NoError(t, err)
body := string(art.Content)
require.Contains(t, body, "# Production environment")
require.Contains(t, body, "NODE_ENV=production")
require.Contains(t, body, "PORT=8080")
}
func TestGenerate_EmbedsCanaryURL(t *testing.T) {
g := envfile.New()
art, err := g.Generate(
context.Background(),
newEnvfileToken("token42"),
testBaseURL,
)
require.NoError(t, err)
body := string(art.Content)
require.Contains(
t,
body,
"INTERNAL_METRICS_ENDPOINT=https://canary.example.com/c/token42",
"envfile must contain the canary URL as the INTERNAL_METRICS_ENDPOINT line",
)
}
func TestGenerate_EmbedsCanaryToken(t *testing.T) {
g := envfile.New()
art, err := g.Generate(
context.Background(),
newEnvfileToken("abc"),
testBaseURL,
)
require.NoError(t, err)
body := string(art.Content)
require.Regexp(
t,
`INTERNAL_METRICS_TOKEN=tok_live_[A-Za-z0-9]{32}`,
body,
"INTERNAL_METRICS_TOKEN must follow the tok_live_ + 32-alnum format",
)
}
func TestGenerate_TriggerURLTrailingSlashTrim(t *testing.T) {
g := envfile.New()
artA, err := g.Generate(
context.Background(),
newEnvfileToken("tk1"),
"https://canary.example.com",
)
require.NoError(t, err)
artB, err := g.Generate(
context.Background(),
newEnvfileToken("tk1"),
"https://canary.example.com/",
)
require.NoError(t, err)
require.Contains(
t,
string(artA.Content),
"INTERNAL_METRICS_ENDPOINT=https://canary.example.com/c/tk1",
)
require.Contains(
t,
string(artB.Content),
"INTERNAL_METRICS_ENDPOINT=https://canary.example.com/c/tk1",
)
}
func TestGenerate_DefaultIncludeKeysAreAWSAndDB(t *testing.T) {
g := envfile.New()
art, err := g.Generate(
context.Background(),
newEnvfileToken("abc"),
testBaseURL,
)
require.NoError(t, err)
body := string(art.Content)
require.Contains(
t,
body,
"AWS_ACCESS_KEY_ID=",
"default include_keys should include aws",
)
require.Contains(
t,
body,
"DATABASE_URL=postgres://",
"default include_keys should include db",
)
require.NotContains(
t,
body,
"STRIPE_SECRET_KEY=",
"default include_keys must NOT include stripe",
)
require.NotContains(
t,
body,
"GITHUB_TOKEN=",
"default include_keys must NOT include github",
)
}
func TestGenerate_IncludeKeysFromMetadata(t *testing.T) {
g := envfile.New()
tok := newEnvfileTokenWithIncludeKeys([]string{"stripe", "github"})
art, err := g.Generate(context.Background(), tok, testBaseURL)
require.NoError(t, err)
body := string(art.Content)
require.Contains(t, body, "STRIPE_SECRET_KEY=")
require.Contains(t, body, "GITHUB_TOKEN=")
require.NotContains(t, body, "AWS_ACCESS_KEY_ID=")
require.NotContains(t, body, "DATABASE_URL=postgres://")
}
func TestGenerate_UnknownKeyInMetadataSkipped(t *testing.T) {
g := envfile.New()
tok := newEnvfileTokenWithIncludeKeys(
[]string{"aws", "nonexistent", "stripe"},
)
art, err := g.Generate(context.Background(), tok, testBaseURL)
require.NoError(t, err)
body := string(art.Content)
require.Contains(t, body, "AWS_ACCESS_KEY_ID=")
require.Contains(t, body, "STRIPE_SECRET_KEY=")
require.NotContains(t, body, "nonexistent")
}
func TestGenerate_MalformedMetadataFallsBackToDefaults(t *testing.T) {
g := envfile.New()
cases := []struct {
name string
metadata string
}{
{"empty raw", ""},
{"invalid json", "{not valid"},
{"include_keys is not array", `{"include_keys": "aws"}`},
{"include_keys is null", `{"include_keys": null}`},
{"include_keys is empty array", `{"include_keys": []}`},
{"missing include_keys field", `{"other_field": "value"}`},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
tok := newEnvfileTokenWithRawMetadata(tc.metadata)
art, err := g.Generate(context.Background(), tok, testBaseURL)
require.NoError(t, err)
body := string(art.Content)
require.Contains(
t,
body,
"AWS_ACCESS_KEY_ID=",
"malformed metadata must fall back to aws default",
)
require.Contains(
t,
body,
"DATABASE_URL=postgres://",
"malformed metadata must fall back to db default",
)
})
}
}
func TestGenerate_IncludeKeysFiltersEmptyStrings(t *testing.T) {
g := envfile.New()
tok := newEnvfileTokenWithIncludeKeys(
[]string{"", " ", "aws"},
)
art, err := g.Generate(context.Background(), tok, testBaseURL)
require.NoError(t, err)
require.Contains(t, string(art.Content), "AWS_ACCESS_KEY_ID=")
}
func TestGenerate_IncludeKeysAllWhitespaceFallsBackToDefaults(t *testing.T) {
g := envfile.New()
tok := newEnvfileTokenWithIncludeKeys([]string{"", " "})
art, err := g.Generate(context.Background(), tok, testBaseURL)
require.NoError(t, err)
body := string(art.Content)
require.Contains(t, body, "AWS_ACCESS_KEY_ID=")
require.Contains(t, body, "DATABASE_URL=postgres://")
}
func TestGenerate_SectionShuffleProducesVariableOrder(t *testing.T) {
g := envfile.New()
tok := newEnvfileTokenWithIncludeKeys(
[]string{"aws", "stripe", "github", "db"},
)
canaryPositions := make(map[int]struct{})
for range 30 {
art, err := g.Generate(context.Background(), tok, testBaseURL)
require.NoError(t, err)
lines := envLines(art.Content)
for i, line := range lines {
if strings.HasPrefix(line, "INTERNAL_METRICS_ENDPOINT=") {
canaryPositions[i] = struct{}{}
break
}
}
}
require.Greater(
t,
len(canaryPositions),
1,
"shuffle must place the canary section at varying positions across 30 invocations",
)
}
func TestGenerate_DistinctInvocationsProduceDistinctOutputs(t *testing.T) {
g := envfile.New()
seen := make(map[string]struct{})
for range 10 {
art, err := g.Generate(
context.Background(),
newEnvfileToken("abc"),
testBaseURL,
)
require.NoError(t, err)
seen[string(art.Content)] = struct{}{}
}
require.Greater(
t,
len(seen),
8,
"random bait + random canary token + shuffle should produce near-unique outputs",
)
}
func TestGenerate_TokenIDDoesNotLeakOutsideCanaryLine(t *testing.T) {
g := envfile.New()
art, err := g.Generate(
context.Background(),
newEnvfileToken("uniqueprobe"),
testBaseURL,
)
require.NoError(t, err)
body := string(art.Content)
require.Equal(
t,
1,
strings.Count(body, "uniqueprobe"),
"token id must appear exactly once (in the canary URL), never in bait lines",
)
}
func TestTrigger_ReturnsGIFLikeWebbug(t *testing.T) {
g := envfile.New()
tok := newEnvfileToken("abc")
r := httptest.NewRequest(http.MethodGet, "/c/abc", nil)
_, resp, err := g.Trigger(context.Background(), tok, r)
require.NoError(t, err)
require.NotNil(t, resp)
require.Equal(t, http.StatusOK, resp.StatusCode)
require.Equal(t, pixel.ContentType, resp.ContentType)
require.Len(t, resp.Body, gifByteLength)
require.Equal(t, pixel.Clone(), resp.Body)
require.Equal(
t,
cacheControlNoStoreValue,
resp.ExtraHeaders["Cache-Control"],
)
require.Equal(t, pragmaNoCacheValue, resp.ExtraHeaders["Pragma"])
}
func TestTrigger_RecordsEventWithRequestMetadata(t *testing.T) {
g := envfile.New()
tok := newEnvfileToken("token1")
t.Run(
"captures token id, source ip, user agent, referer",
func(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/c/token1", nil)
r.Header.Set("CF-Connecting-IP", "203.0.113.50")
r.Header.Set("User-Agent", "curl/8.0.0")
r.Header.Set("Referer", "https://victim.example.com/")
evt, _, err := g.Trigger(context.Background(), tok, r)
require.NoError(t, err)
require.NotNil(t, evt)
require.Equal(t, "token1", evt.TokenID)
require.Equal(t, "203.0.113.50", evt.SourceIP)
require.NotNil(t, evt.UserAgent)
require.Equal(t, "curl/8.0.0", *evt.UserAgent)
require.NotNil(t, evt.Referer)
require.Equal(t, "https://victim.example.com/", *evt.Referer)
},
)
t.Run("source ip precedence", func(t *testing.T) {
cases := []struct {
name string
headers map[string]string
remote string
wantIP string
}{
{
name: "CF wins over XFF and XRI",
headers: map[string]string{
"CF-Connecting-IP": "203.0.113.10",
"X-Forwarded-For": "198.51.100.1, 198.51.100.2",
"X-Real-IP": "192.0.2.99",
},
remote: "127.0.0.1:9999",
wantIP: "203.0.113.10",
},
{
name: "XFF rightmost wins over XRI when no CF",
headers: map[string]string{
"X-Forwarded-For": "198.51.100.1, 198.51.100.7",
"X-Real-IP": "192.0.2.99",
},
remote: "127.0.0.1:9999",
wantIP: "198.51.100.7",
},
{
name: "XFF trailing-comma falls through to last non-empty",
headers: map[string]string{
"X-Forwarded-For": "198.51.100.1, ",
"X-Real-IP": "192.0.2.99",
},
remote: "127.0.0.1:9999",
wantIP: "198.51.100.1",
},
{
name: "XFF entirely empty entries fall through to XRI",
headers: map[string]string{
"X-Forwarded-For": ", ,",
"X-Real-IP": "192.0.2.99",
},
remote: "127.0.0.1:9999",
wantIP: "192.0.2.99",
},
{
name: "XRI when no CF or XFF",
headers: map[string]string{
"X-Real-IP": "192.0.2.99",
},
remote: "127.0.0.1:9999",
wantIP: "192.0.2.99",
},
{
name: "RemoteAddr IPv4 strips port",
headers: nil,
remote: "127.0.0.1:9999",
wantIP: "127.0.0.1",
},
{
name: "RemoteAddr IPv6 strips brackets and port",
headers: nil,
remote: "[2001:db8::1]:54321",
wantIP: "2001:db8::1",
},
{
name: "RemoteAddr loopback IPv6 strips brackets and port",
headers: nil,
remote: "[::1]:9999",
wantIP: "::1",
},
{
name: "RemoteAddr without port falls back to raw value",
headers: nil,
remote: "127.0.0.1",
wantIP: "127.0.0.1",
},
{
name: "XFF IPv6 rightmost",
headers: map[string]string{
"X-Forwarded-For": "198.51.100.1, 2001:db8::dead",
},
remote: "127.0.0.1:9999",
wantIP: "2001:db8::dead",
},
{
name: "CF value is trimmed of whitespace",
headers: map[string]string{
"CF-Connecting-IP": " 203.0.113.10 ",
},
remote: "127.0.0.1:9999",
wantIP: "203.0.113.10",
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/c/token1", nil)
for k, v := range tc.headers {
r.Header.Set(k, v)
}
r.RemoteAddr = tc.remote
evt, _, err := g.Trigger(context.Background(), tok, r)
require.NoError(t, err)
require.NotNil(t, evt)
require.Equal(t, tc.wantIP, evt.SourceIP)
})
}
})
t.Run(
"missing user agent and referer record as nil pointers",
func(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/c/token1", nil)
r.Header.Del("User-Agent")
r.Header.Del("Referer")
r.Header.Set("CF-Connecting-IP", "203.0.113.5")
evt, _, err := g.Trigger(context.Background(), tok, r)
require.NoError(t, err)
require.NotNil(t, evt)
require.Nil(t, evt.UserAgent)
require.Nil(t, evt.Referer)
},
)
}
func TestTrigger_ResponseBodyIsIndependentCopyPerCall(t *testing.T) {
g := envfile.New()
tok := newEnvfileToken("abc")
r := httptest.NewRequest(http.MethodGet, "/c/abc", nil)
_, resp1, err := g.Trigger(context.Background(), tok, r)
require.NoError(t, err)
_, resp2, err := g.Trigger(context.Background(), tok, r)
require.NoError(t, err)
resp1.Body[0] = 0x00
require.Equal(t, byte(0x47), resp2.Body[0])
}
func TestTrigger_TokenNotFound_StillReturnsGIF(t *testing.T) {
g := envfile.New()
r := httptest.NewRequest(http.MethodGet, "/c/does-not-exist", nil)
r.Header.Set("CF-Connecting-IP", "203.0.113.100")
r.Header.Set("User-Agent", "curl/8.0.0")
evt, resp, err := g.Trigger(context.Background(), nil, r)
require.NoError(t, err)
require.NotNil(t, resp)
require.Equal(t, http.StatusOK, resp.StatusCode)
require.Equal(t, pixel.ContentType, resp.ContentType)
require.Equal(t, pixel.Clone(), resp.Body)
require.Nil(t, evt)
}