mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 04:50:52 +08:00
feat(claude): add native request cloaking for non-claude-code clients
integrate claude-cloak functionality to disguise api requests: - add CloakConfig with mode (auto/always/never) and strict-mode options - generate fake user_id in claude code format (user_[hex]_account__session_[uuid]) - inject claude code system prompt (configurable strict mode) - obfuscate sensitive words with zero-width characters - auto-detect claude code clients via user-agent 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -134,6 +134,15 @@ ws-auth: false
|
|||||||
# - "claude-3-*" # wildcard matching prefix (e.g. claude-3-7-sonnet-20250219)
|
# - "claude-3-*" # wildcard matching prefix (e.g. claude-3-7-sonnet-20250219)
|
||||||
# - "*-thinking" # wildcard matching suffix (e.g. claude-opus-4-5-thinking)
|
# - "*-thinking" # wildcard matching suffix (e.g. claude-opus-4-5-thinking)
|
||||||
# - "*haiku*" # wildcard matching substring (e.g. claude-3-5-haiku-20241022)
|
# - "*haiku*" # wildcard matching substring (e.g. claude-3-5-haiku-20241022)
|
||||||
|
# cloak: # optional: request cloaking for non-Claude-Code clients
|
||||||
|
# mode: "auto" # "auto" (default): cloak only when client is not Claude Code
|
||||||
|
# # "always": always apply cloaking
|
||||||
|
# # "never": never apply cloaking
|
||||||
|
# strict-mode: false # false (default): prepend Claude Code prompt to user system messages
|
||||||
|
# # true: strip all user system messages, keep only Claude Code prompt
|
||||||
|
# sensitive-words: # optional: words to obfuscate with zero-width characters
|
||||||
|
# - "API"
|
||||||
|
# - "proxy"
|
||||||
|
|
||||||
# OpenAI compatibility providers
|
# OpenAI compatibility providers
|
||||||
# openai-compatibility:
|
# openai-compatibility:
|
||||||
|
|||||||
@@ -236,6 +236,25 @@ type PayloadModelRule struct {
|
|||||||
Protocol string `yaml:"protocol" json:"protocol"`
|
Protocol string `yaml:"protocol" json:"protocol"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CloakConfig configures request cloaking for non-Claude-Code clients.
|
||||||
|
// Cloaking disguises API requests to appear as originating from the official Claude Code CLI.
|
||||||
|
type CloakConfig struct {
|
||||||
|
// Mode controls cloaking behavior: "auto" (default), "always", or "never".
|
||||||
|
// - "auto": cloak only when client is not Claude Code (based on User-Agent)
|
||||||
|
// - "always": always apply cloaking regardless of client
|
||||||
|
// - "never": never apply cloaking
|
||||||
|
Mode string `yaml:"mode,omitempty" json:"mode,omitempty"`
|
||||||
|
|
||||||
|
// StrictMode controls how system prompts are handled when cloaking.
|
||||||
|
// - false (default): prepend Claude Code prompt to user system messages
|
||||||
|
// - true: strip all user system messages, keep only Claude Code prompt
|
||||||
|
StrictMode bool `yaml:"strict-mode,omitempty" json:"strict-mode,omitempty"`
|
||||||
|
|
||||||
|
// SensitiveWords is a list of words to obfuscate with zero-width characters.
|
||||||
|
// This can help bypass certain content filters.
|
||||||
|
SensitiveWords []string `yaml:"sensitive-words,omitempty" json:"sensitive-words,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// ClaudeKey represents the configuration for a Claude API key,
|
// ClaudeKey represents the configuration for a Claude API key,
|
||||||
// including the API key itself and an optional base URL for the API endpoint.
|
// including the API key itself and an optional base URL for the API endpoint.
|
||||||
type ClaudeKey struct {
|
type ClaudeKey struct {
|
||||||
@@ -260,6 +279,9 @@ type ClaudeKey struct {
|
|||||||
|
|
||||||
// ExcludedModels lists model IDs that should be excluded for this provider.
|
// ExcludedModels lists model IDs that should be excluded for this provider.
|
||||||
ExcludedModels []string `yaml:"excluded-models,omitempty" json:"excluded-models,omitempty"`
|
ExcludedModels []string `yaml:"excluded-models,omitempty" json:"excluded-models,omitempty"`
|
||||||
|
|
||||||
|
// Cloak configures request cloaking for non-Claude-Code clients.
|
||||||
|
Cloak *CloakConfig `yaml:"cloak,omitempty" json:"cloak,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClaudeModel describes a mapping between an alias and the actual upstream model name.
|
// ClaudeModel describes a mapping between an alias and the actual upstream model name.
|
||||||
|
|||||||
@@ -67,9 +67,10 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
|
|||||||
// Inject thinking config based on model metadata for thinking variants
|
// Inject thinking config based on model metadata for thinking variants
|
||||||
body = e.injectThinkingConfig(model, req.Metadata, body)
|
body = e.injectThinkingConfig(model, req.Metadata, body)
|
||||||
|
|
||||||
if !strings.HasPrefix(model, "claude-3-5-haiku") {
|
// Apply cloaking (system prompt injection, fake user ID, sensitive word obfuscation)
|
||||||
body = checkSystemInstructions(body)
|
// based on client type and configuration
|
||||||
}
|
body = applyCloaking(ctx, e.cfg, auth, body, model)
|
||||||
|
|
||||||
body = applyPayloadConfigWithRoot(e.cfg, model, to.String(), "", body, originalTranslated)
|
body = applyPayloadConfigWithRoot(e.cfg, model, to.String(), "", body, originalTranslated)
|
||||||
|
|
||||||
// Disable thinking if tool_choice forces tool use (Anthropic API constraint)
|
// Disable thinking if tool_choice forces tool use (Anthropic API constraint)
|
||||||
@@ -181,7 +182,11 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
|
|||||||
body, _ = sjson.SetBytes(body, "model", model)
|
body, _ = sjson.SetBytes(body, "model", model)
|
||||||
// Inject thinking config based on model metadata for thinking variants
|
// Inject thinking config based on model metadata for thinking variants
|
||||||
body = e.injectThinkingConfig(model, req.Metadata, body)
|
body = e.injectThinkingConfig(model, req.Metadata, body)
|
||||||
body = checkSystemInstructions(body)
|
|
||||||
|
// Apply cloaking (system prompt injection, fake user ID, sensitive word obfuscation)
|
||||||
|
// based on client type and configuration
|
||||||
|
body = applyCloaking(ctx, e.cfg, auth, body, model)
|
||||||
|
|
||||||
body = applyPayloadConfigWithRoot(e.cfg, model, to.String(), "", body, originalTranslated)
|
body = applyPayloadConfigWithRoot(e.cfg, model, to.String(), "", body, originalTranslated)
|
||||||
|
|
||||||
// Disable thinking if tool_choice forces tool use (Anthropic API constraint)
|
// Disable thinking if tool_choice forces tool use (Anthropic API constraint)
|
||||||
@@ -770,3 +775,164 @@ func checkSystemInstructions(payload []byte) []byte {
|
|||||||
}
|
}
|
||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getClientUserAgent extracts the client User-Agent from the gin context.
|
||||||
|
func getClientUserAgent(ctx context.Context) string {
|
||||||
|
if ginCtx, ok := ctx.Value("gin").(*gin.Context); ok && ginCtx != nil && ginCtx.Request != nil {
|
||||||
|
return ginCtx.GetHeader("User-Agent")
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCloakConfigFromAuth extracts cloak configuration from auth attributes.
|
||||||
|
// Returns (cloakMode, strictMode, sensitiveWords).
|
||||||
|
func getCloakConfigFromAuth(auth *cliproxyauth.Auth) (string, bool, []string) {
|
||||||
|
if auth == nil || auth.Attributes == nil {
|
||||||
|
return "auto", false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cloakMode := auth.Attributes["cloak_mode"]
|
||||||
|
if cloakMode == "" {
|
||||||
|
cloakMode = "auto"
|
||||||
|
}
|
||||||
|
|
||||||
|
strictMode := strings.ToLower(auth.Attributes["cloak_strict_mode"]) == "true"
|
||||||
|
|
||||||
|
var sensitiveWords []string
|
||||||
|
if wordsStr := auth.Attributes["cloak_sensitive_words"]; wordsStr != "" {
|
||||||
|
sensitiveWords = strings.Split(wordsStr, ",")
|
||||||
|
for i := range sensitiveWords {
|
||||||
|
sensitiveWords[i] = strings.TrimSpace(sensitiveWords[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cloakMode, strictMode, sensitiveWords
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveClaudeKeyCloakConfig finds the matching ClaudeKey config and returns its CloakConfig.
|
||||||
|
func resolveClaudeKeyCloakConfig(cfg *config.Config, auth *cliproxyauth.Auth) *config.CloakConfig {
|
||||||
|
if cfg == nil || auth == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
apiKey, baseURL := claudeCreds(auth)
|
||||||
|
if apiKey == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range cfg.ClaudeKey {
|
||||||
|
entry := &cfg.ClaudeKey[i]
|
||||||
|
cfgKey := strings.TrimSpace(entry.APIKey)
|
||||||
|
cfgBase := strings.TrimSpace(entry.BaseURL)
|
||||||
|
|
||||||
|
// Match by API key
|
||||||
|
if strings.EqualFold(cfgKey, apiKey) {
|
||||||
|
// If baseURL is specified, also check it
|
||||||
|
if baseURL != "" && cfgBase != "" && !strings.EqualFold(cfgBase, baseURL) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return entry.Cloak
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// injectFakeUserID generates and injects a fake user ID into the request metadata.
|
||||||
|
func injectFakeUserID(payload []byte) []byte {
|
||||||
|
metadata := gjson.GetBytes(payload, "metadata")
|
||||||
|
if !metadata.Exists() {
|
||||||
|
payload, _ = sjson.SetBytes(payload, "metadata.user_id", generateFakeUserID())
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
existingUserID := gjson.GetBytes(payload, "metadata.user_id").String()
|
||||||
|
if existingUserID == "" || !isValidUserID(existingUserID) {
|
||||||
|
payload, _ = sjson.SetBytes(payload, "metadata.user_id", generateFakeUserID())
|
||||||
|
}
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkSystemInstructionsWithMode injects Claude Code system prompt.
|
||||||
|
// In strict mode, it replaces all user system messages.
|
||||||
|
// In non-strict mode (default), it prepends to existing system messages.
|
||||||
|
func checkSystemInstructionsWithMode(payload []byte, strictMode bool) []byte {
|
||||||
|
system := gjson.GetBytes(payload, "system")
|
||||||
|
claudeCodeInstructions := `[{"type":"text","text":"You are Claude Code, Anthropic's official CLI for Claude."}]`
|
||||||
|
|
||||||
|
if strictMode {
|
||||||
|
// Strict mode: replace all system messages with Claude Code prompt only
|
||||||
|
payload, _ = sjson.SetRawBytes(payload, "system", []byte(claudeCodeInstructions))
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-strict mode (default): prepend Claude Code prompt to existing system messages
|
||||||
|
if system.IsArray() {
|
||||||
|
if gjson.GetBytes(payload, "system.0.text").String() != "You are Claude Code, Anthropic's official CLI for Claude." {
|
||||||
|
system.ForEach(func(_, part gjson.Result) bool {
|
||||||
|
if part.Get("type").String() == "text" {
|
||||||
|
claudeCodeInstructions, _ = sjson.SetRaw(claudeCodeInstructions, "-1", part.Raw)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
payload, _ = sjson.SetRawBytes(payload, "system", []byte(claudeCodeInstructions))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
payload, _ = sjson.SetRawBytes(payload, "system", []byte(claudeCodeInstructions))
|
||||||
|
}
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyCloaking applies cloaking transformations to the payload based on config and client.
|
||||||
|
// Cloaking includes: system prompt injection, fake user ID, and sensitive word obfuscation.
|
||||||
|
func applyCloaking(ctx context.Context, cfg *config.Config, auth *cliproxyauth.Auth, payload []byte, model string) []byte {
|
||||||
|
clientUserAgent := getClientUserAgent(ctx)
|
||||||
|
|
||||||
|
// Get cloak config from ClaudeKey configuration
|
||||||
|
cloakCfg := resolveClaudeKeyCloakConfig(cfg, auth)
|
||||||
|
|
||||||
|
// Determine cloak settings
|
||||||
|
var cloakMode string
|
||||||
|
var strictMode bool
|
||||||
|
var sensitiveWords []string
|
||||||
|
|
||||||
|
if cloakCfg != nil {
|
||||||
|
cloakMode = cloakCfg.Mode
|
||||||
|
strictMode = cloakCfg.StrictMode
|
||||||
|
sensitiveWords = cloakCfg.SensitiveWords
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to auth attributes if no config found
|
||||||
|
if cloakMode == "" {
|
||||||
|
attrMode, attrStrict, attrWords := getCloakConfigFromAuth(auth)
|
||||||
|
cloakMode = attrMode
|
||||||
|
if !strictMode {
|
||||||
|
strictMode = attrStrict
|
||||||
|
}
|
||||||
|
if len(sensitiveWords) == 0 {
|
||||||
|
sensitiveWords = attrWords
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if cloaking should be applied
|
||||||
|
if !shouldCloak(cloakMode, clientUserAgent) {
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip system instructions for claude-3-5-haiku models
|
||||||
|
if !strings.HasPrefix(model, "claude-3-5-haiku") {
|
||||||
|
payload = checkSystemInstructionsWithMode(payload, strictMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject fake user ID
|
||||||
|
payload = injectFakeUserID(payload)
|
||||||
|
|
||||||
|
// Apply sensitive word obfuscation
|
||||||
|
if len(sensitiveWords) > 0 {
|
||||||
|
matcher := buildSensitiveWordMatcher(sensitiveWords)
|
||||||
|
payload = obfuscateSensitiveWords(payload, matcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
176
internal/runtime/executor/cloak_obfuscate.go
Normal file
176
internal/runtime/executor/cloak_obfuscate.go
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
package executor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
"github.com/tidwall/sjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
// zeroWidthSpace is the Unicode zero-width space character used for obfuscation.
|
||||||
|
const zeroWidthSpace = "\u200B"
|
||||||
|
|
||||||
|
// SensitiveWordMatcher holds the compiled regex for matching sensitive words.
|
||||||
|
type SensitiveWordMatcher struct {
|
||||||
|
regex *regexp.Regexp
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildSensitiveWordMatcher compiles a regex from the word list.
|
||||||
|
// Words are sorted by length (longest first) for proper matching.
|
||||||
|
func buildSensitiveWordMatcher(words []string) *SensitiveWordMatcher {
|
||||||
|
if len(words) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter and normalize words
|
||||||
|
var validWords []string
|
||||||
|
for _, w := range words {
|
||||||
|
w = strings.TrimSpace(w)
|
||||||
|
if utf8.RuneCountInString(w) >= 2 && !strings.Contains(w, zeroWidthSpace) {
|
||||||
|
validWords = append(validWords, w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(validWords) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by length (longest first) for proper matching
|
||||||
|
sort.Slice(validWords, func(i, j int) bool {
|
||||||
|
return len(validWords[i]) > len(validWords[j])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Escape and join
|
||||||
|
escaped := make([]string, len(validWords))
|
||||||
|
for i, w := range validWords {
|
||||||
|
escaped[i] = regexp.QuoteMeta(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
pattern := "(?i)" + strings.Join(escaped, "|")
|
||||||
|
re, err := regexp.Compile(pattern)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &SensitiveWordMatcher{regex: re}
|
||||||
|
}
|
||||||
|
|
||||||
|
// obfuscateWord inserts a zero-width space after the first grapheme.
|
||||||
|
func obfuscateWord(word string) string {
|
||||||
|
if strings.Contains(word, zeroWidthSpace) {
|
||||||
|
return word
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get first rune
|
||||||
|
r, size := utf8.DecodeRuneInString(word)
|
||||||
|
if r == utf8.RuneError || size >= len(word) {
|
||||||
|
return word
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(r) + zeroWidthSpace + word[size:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// obfuscateText replaces all sensitive words in the text.
|
||||||
|
func (m *SensitiveWordMatcher) obfuscateText(text string) string {
|
||||||
|
if m == nil || m.regex == nil {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
return m.regex.ReplaceAllStringFunc(text, obfuscateWord)
|
||||||
|
}
|
||||||
|
|
||||||
|
// obfuscateSensitiveWords processes the payload and obfuscates sensitive words
|
||||||
|
// in system blocks and message content.
|
||||||
|
func obfuscateSensitiveWords(payload []byte, matcher *SensitiveWordMatcher) []byte {
|
||||||
|
if matcher == nil || matcher.regex == nil {
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obfuscate in system blocks
|
||||||
|
payload = obfuscateSystemBlocks(payload, matcher)
|
||||||
|
|
||||||
|
// Obfuscate in messages
|
||||||
|
payload = obfuscateMessages(payload, matcher)
|
||||||
|
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
// obfuscateSystemBlocks obfuscates sensitive words in system blocks.
|
||||||
|
func obfuscateSystemBlocks(payload []byte, matcher *SensitiveWordMatcher) []byte {
|
||||||
|
system := gjson.GetBytes(payload, "system")
|
||||||
|
if !system.Exists() {
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
if system.IsArray() {
|
||||||
|
modified := false
|
||||||
|
system.ForEach(func(key, value gjson.Result) bool {
|
||||||
|
if value.Get("type").String() == "text" {
|
||||||
|
text := value.Get("text").String()
|
||||||
|
obfuscated := matcher.obfuscateText(text)
|
||||||
|
if obfuscated != text {
|
||||||
|
path := "system." + key.String() + ".text"
|
||||||
|
payload, _ = sjson.SetBytes(payload, path, obfuscated)
|
||||||
|
modified = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if modified {
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
} else if system.Type == gjson.String {
|
||||||
|
text := system.String()
|
||||||
|
obfuscated := matcher.obfuscateText(text)
|
||||||
|
if obfuscated != text {
|
||||||
|
payload, _ = sjson.SetBytes(payload, "system", obfuscated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
// obfuscateMessages obfuscates sensitive words in message content.
|
||||||
|
func obfuscateMessages(payload []byte, matcher *SensitiveWordMatcher) []byte {
|
||||||
|
messages := gjson.GetBytes(payload, "messages")
|
||||||
|
if !messages.Exists() || !messages.IsArray() {
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
messages.ForEach(func(msgKey, msg gjson.Result) bool {
|
||||||
|
content := msg.Get("content")
|
||||||
|
if !content.Exists() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
msgPath := "messages." + msgKey.String()
|
||||||
|
|
||||||
|
if content.Type == gjson.String {
|
||||||
|
// Simple string content
|
||||||
|
text := content.String()
|
||||||
|
obfuscated := matcher.obfuscateText(text)
|
||||||
|
if obfuscated != text {
|
||||||
|
payload, _ = sjson.SetBytes(payload, msgPath+".content", obfuscated)
|
||||||
|
}
|
||||||
|
} else if content.IsArray() {
|
||||||
|
// Array of content blocks
|
||||||
|
content.ForEach(func(blockKey, block gjson.Result) bool {
|
||||||
|
if block.Get("type").String() == "text" {
|
||||||
|
text := block.Get("text").String()
|
||||||
|
obfuscated := matcher.obfuscateText(text)
|
||||||
|
if obfuscated != text {
|
||||||
|
path := msgPath + ".content." + blockKey.String() + ".text"
|
||||||
|
payload, _ = sjson.SetBytes(payload, path, obfuscated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
return payload
|
||||||
|
}
|
||||||
47
internal/runtime/executor/cloak_utils.go
Normal file
47
internal/runtime/executor/cloak_utils.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package executor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// userIDPattern matches Claude Code format: user_[64-hex]_account__session_[uuid-v4]
|
||||||
|
var userIDPattern = regexp.MustCompile(`^user_[a-fA-F0-9]{64}_account__session_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)
|
||||||
|
|
||||||
|
// generateFakeUserID generates a fake user ID in Claude Code format.
|
||||||
|
// Format: user_[64-hex-chars]_account__session_[UUID-v4]
|
||||||
|
func generateFakeUserID() string {
|
||||||
|
hexBytes := make([]byte, 32)
|
||||||
|
_, _ = rand.Read(hexBytes)
|
||||||
|
hexPart := hex.EncodeToString(hexBytes)
|
||||||
|
uuidPart := uuid.New().String()
|
||||||
|
return "user_" + hexPart + "_account__session_" + uuidPart
|
||||||
|
}
|
||||||
|
|
||||||
|
// isValidUserID checks if a user ID matches Claude Code format.
|
||||||
|
func isValidUserID(userID string) bool {
|
||||||
|
return userIDPattern.MatchString(userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// shouldCloak determines if request should be cloaked based on config and client User-Agent.
|
||||||
|
// Returns true if cloaking should be applied.
|
||||||
|
func shouldCloak(cloakMode string, userAgent string) bool {
|
||||||
|
switch strings.ToLower(cloakMode) {
|
||||||
|
case "always":
|
||||||
|
return true
|
||||||
|
case "never":
|
||||||
|
return false
|
||||||
|
default: // "auto" or empty
|
||||||
|
// If client is Claude Code, don't cloak
|
||||||
|
return !strings.HasPrefix(userAgent, "claude-cli")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isClaudeCodeClient checks if the User-Agent indicates a Claude Code client.
|
||||||
|
func isClaudeCodeClient(userAgent string) bool {
|
||||||
|
return strings.HasPrefix(userAgent, "claude-cli")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user