mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 04:50:52 +08:00
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>
177 lines
4.4 KiB
Go
177 lines
4.4 KiB
Go
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
|
|
}
|