diff --git a/.gitignore b/.gitignore index 150f77b029..7a34ebc588 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,4 @@ go.work.sum .vscode/ # Generated files -data/ \ No newline at end of file +/data/ \ No newline at end of file diff --git a/Makefile b/Makefile index 9ba309902b..e19097b962 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ PROJECT_NAME=ponghub BINARY=bin/$(PROJECT_NAME) SRC=cmd/$(PROJECT_NAME)/*.go -.PHONY: all build run clean +.PHONY: all build run test clean all: build @@ -13,5 +13,8 @@ build: run: build $(BINARY) +test: + go test ./... + clean: del $(BINARY) diff --git a/README.md b/README.md index 396475fb01..0854271f0b 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,81 @@ Ensure that the environment variable is set in your GitHub repository settings u - `{{hash_short}}` - Short hash value (6-digit hexadecimal) - `{{hash_md5_like}}` - MD5-style long hash value (32-digit hexadecimal) +#### 🌐 Network and System Information Parameters + +- `{{local_ip}}` - Gets the local IP address of the system +- `{{hostname}}` - Gets the hostname of the system +- `{{user_agent}}` - Generates a random User-Agent string for HTTP requests +- `{{http_method}}` - Generates a random HTTP method (GET, POST, PUT, DELETE, etc.) + +#### 🔐 Encoding and Decoding Parameters + +- `{{base64(content)}}` - Base64 encodes the provided content + - Example: `{{base64(hello world)}}` - Encodes "hello world" to Base64 +- `{{url_encode(content)}}` - URL encodes the provided content + - Example: `{{url_encode(hello world)}}` - URL encodes "hello world" +- `{{json_escape(content)}}` - JSON escapes the provided content + - Example: `{{json_escape("test")}}` - Escapes quotes and special characters for JSON + +#### 🔢 Mathematical Operation Parameters + +- `{{add(a,b)}}` - Adds two numbers + - Example: `{{add(10,5)}}` - Returns 15 +- `{{sub(a,b)}}` - Subtracts two numbers + - Example: `{{sub(10,5)}}` - Returns 5 +- `{{mul(a,b)}}` - Multiplies two numbers + - Example: `{{mul(10,5)}}` - Returns 50 +- `{{div(a,b)}}` - Divides two numbers + - Example: `{{div(10,5)}}` - Returns 2 + +#### 📝 Text Processing Parameters + +- `{{upper(text)}}` - Converts text to uppercase + - Example: `{{upper(hello)}}` - Returns "HELLO" +- `{{lower(text)}}` - Converts text to lowercase + - Example: `{{lower(HELLO)}}` - Returns "hello" +- `{{reverse(text)}}` - Reverses the text + - Example: `{{reverse(hello)}}` - Returns "olleh" +- `{{substr(text,start,length)}}` - Extracts substring from text + - Example: `{{substr(hello world,0,5)}}` - Returns "hello" + +#### 🎨 Color Generation Parameters + +- `{{color_hex}}` - Generates a random hexadecimal color code + - Example: `#FF5733` +- `{{color_rgb}}` - Generates a random RGB color value + - Example: `rgb(255, 87, 51)` +- `{{color_hsl}}` - Generates a random HSL color value + - Example: `hsl(120, 50%, 75%)` + +#### 📁 File and MIME Type Parameters + +- `{{mime_type}}` - Generates a random MIME type + - Example: `application/json`, `image/png`, `text/html` +- `{{file_ext}}` - Generates a random file extension + - Example: `.jpg`, `.pdf`, `.txt` + +#### 👤 Fake Data Generation Parameters + +- `{{fake_email}}` - Generates a realistic fake email address + - Example: `john.smith@example.com` +- `{{fake_phone}}` - Generates a fake phone number + - Example: `+1-555-0123` +- `{{fake_name}}` - Generates a fake person name + - Example: `John Smith` +- `{{fake_domain}}` - Generates a fake domain name + - Example: `example-site.com` + +#### ⏰ Time Calculation Parameters + +- `{{time_add(duration)}}` - Adds duration to current time + - Example: `{{time_add(1h)}}` - Adds 1 hour to current time + - Example: `{{time_add(30m)}}` - Adds 30 minutes to current time + - Supported units: s (seconds), m (minutes), h (hours), d (days) +- `{{time_sub(duration)}}` - Subtracts duration from current time + - Example: `{{time_sub(1d)}}` - Subtracts 1 day from current time + - Example: `{{time_sub(2h30m)}}` - Subtracts 2 hours and 30 minutes + diff --git a/README_CN.md b/README_CN.md index c544d44b36..b2617fc992 100644 --- a/README_CN.md +++ b/README_CN.md @@ -162,6 +162,81 @@ ponghub 现已支持强大的参数化配置功能,允许在配置文件中使 - `{{hash_short}}` - 短哈希值(6位十六进制) - `{{hash_md5_like}}` - MD5风格的长哈希值(32位十六进制) +#### 🌐 网络和系统信息参数 + +- `{{local_ip}}` - 获取系统本地IP地址 +- `{{hostname}}` - 获取系统主机名 +- `{{user_agent}}` - 生成随机的User-Agent字符串 +- `{{http_method}}` - 生成随机的HTTP方法(GET、POST、PUT、DELETE等) + +#### 🔐 编码和解码参数 + +- `{{base64(内容)}}` - 对提供的内容进行Base64编码 + - 示例:`{{base64(hello world)}}` - 将"hello world"编码为Base64 +- `{{url_encode(内容)}}` - 对提供的内容进行URL编码 + - 示例:`{{url_encode(hello world)}}` - 对"hello world"进行URL编码 +- `{{json_escape(内容)}}` - 对提供的内容进行JSON转义 + - 示例:`{{json_escape("test")}}` - 转义引号和特殊字符以用于JSON + +#### 🔢 数学运算参数 + +- `{{add(a,b)}}` - 两数相加 + - 示例:`{{add(10,5)}}` - 返回15 +- `{{sub(a,b)}}` - 两数相减 + - 示例:`{{sub(10,5)}}` - 返回5 +- `{{mul(a,b)}}` - 两数相乘 + - 示例:`{{mul(10,5)}}` - 返回50 +- `{{div(a,b)}}` - 两数相除 + - 示例:`{{div(10,5)}}` - 返回2 + +#### 📝 文本处理参数 + +- `{{upper(文本)}}` - 将文本转换为大写 + - 示例:`{{upper(hello)}}` - 返回"HELLO" +- `{{lower(文本)}}` - 将文本转换为小写 + - 示例:`{{lower(HELLO)}}` - 返回"hello" +- `{{reverse(文本)}}` - 反转文本 + - 示例:`{{reverse(hello)}}` - 返回"olleh" +- `{{substr(文本,起始位置,长度)}}` - 从文本中提取子字符串 + - 示例:`{{substr(hello world,0,5)}}` - 返回"hello" + +#### 🎨 颜色生成参数 + +- `{{color_hex}}` - 生成随机的十六进制颜色代码 + - 示例:`#FF5733` +- `{{color_rgb}}` - 生成随机的RGB颜色值 + - 示例:`rgb(255, 87, 51)` +- `{{color_hsl}}` - 生成随机的HSL颜色值 + - 示例:`hsl(120, 50%, 75%)` + +#### 📁 文件和MIME类型参数 + +- `{{mime_type}}` - 生成随机的MIME类型 + - 示例:`application/json`、`image/png`、`text/html` +- `{{file_ext}}` - 生成随机的文件扩展名 + - 示例:`.jpg`、`.pdf`、`.txt` + +#### 👤 虚拟数据生成参数 + +- `{{fake_email}}` - 生成逼真的虚拟邮箱地址 + - 示例:`john.smith@example.com` +- `{{fake_phone}}` - 生成虚拟电话号码 + - 示例:`+1-555-0123` +- `{{fake_name}}` - 生成虚拟人名 + - 示例:`张三` +- `{{fake_domain}}` - 生成虚拟域名 + - 示例:`example-site.com` + +#### ⏰ 时间计算参数 + +- `{{time_add(时长)}}` - 在当前时间基础上增加指定时长 + - 示例:`{{time_add(1h)}}` - 在当前时间上增加1小时 + - 示例:`{{time_add(30m)}}` - 在当前时间上增加30分钟 + - 支持的单位:s(秒)、m(分钟)、h(小时)、d(天) +- `{{time_sub(时长)}}` - 在当前时间基础上减去指定时长 + - 示例:`{{time_sub(1d)}}` - 在当前时间上减去1天 + - 示例:`{{time_sub(2h30m)}}` - 在当前时间上减去2小时30分钟 + diff --git a/internal/checker/endpoints.go b/internal/checker/endpoints.go index c44e574729..d7e12571f8 100644 --- a/internal/checker/endpoints.go +++ b/internal/checker/endpoints.go @@ -10,7 +10,7 @@ import ( "strings" "time" - "github.com/wcy-dt/ponghub/internal/common" + "github.com/wcy-dt/ponghub/internal/common/params" "github.com/wcy-dt/ponghub/internal/types/structures/checker" "github.com/wcy-dt/ponghub/internal/types/structures/configure" ) @@ -33,7 +33,7 @@ func checkEndpoint(cfg *configure.Endpoint, timeout int, maxRetryTimes int, serv isCertExpired := false // Generate display URL for smart showing of template vs resolved URL - resolver := common.NewParameterResolver() + resolver := params.NewParameterResolver() displayURL, highlightSegments := resolver.HighlightChanges(cfg.OriginalURL) originalURL := cfg.OriginalURL if originalURL == "" { diff --git a/internal/common/params/constants.go b/internal/common/params/constants.go new file mode 100644 index 0000000000..cfeed171fa --- /dev/null +++ b/internal/common/params/constants.go @@ -0,0 +1,115 @@ +package params + +// Character sets for random string generation +const ( + DefaultCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + HexCharset = "0123456789abcdef" +) + +// TimeFormatReplacements Time format pattern replacements for strftime-like patterns to Go time format +var TimeFormatReplacements = map[string]string{ + "%Y": "2006", // 4-digit year + "%y": "06", // 2-digit year + "%m": "01", // month (01-12) + "%d": "02", // day (01-31) + "%H": "15", // hour (00-23) + "%M": "04", // minute (00-59) + "%S": "05", // second (00-59) + "%B": "January", // full month name + "%b": "Jan", // abbreviated month name + "%A": "Monday", // full weekday name + "%a": "Mon", // abbreviated weekday name + "%j": "002", // day of year (001-366) + "%U": "", // week of year (placeholder) + "%W": "", // week of year (placeholder) + "%w": "", // weekday (placeholder) + "%Z": "MST", // timezone name + "%z": "-0700", // timezone offset + "%s": "", // Unix timestamp (placeholder) +} + +// HTTPMethods HTTP methods for random HTTP method generation +var HTTPMethods = []string{ + "GET", + "POST", + "PUT", + "DELETE", + "PATCH", + "HEAD", + "OPTIONS", +} + +// SensitivePatterns Sensitive environment variable patterns for masking +var SensitivePatterns = []string{ + "key", + "secret", + "token", + "password", + "pass", + "pwd", + "auth", + "credential", + "private", + "api_key", + "access", + "jwt", + "bearer", + "signature", + "hash", + "salt", +} + +// UserAgents User agent strings for random user agent generation +var UserAgents = LoadUserAgents() + +// MimeTypes MIME types for random MIME type generation +var MimeTypes = []string{ + "image/jpeg", + "image/png", + "image/gif", + "image/webp", + "video/mp4", + "video/x-msvideo", + "video/x-flv", + "audio/mpeg", + "audio/ogg", + "application/pdf", + "application/zip", + "application/x-rar-compressed", + "text/html", + "text/css", + "text/javascript", + "application/json", + "application/xml", +} + +// FileExtensions File extensions for random file extension generation +var FileExtensions = []string{ + ".jpg", + ".jpeg", + ".png", + ".gif", + ".webp", + ".mp4", + ".avi", + ".flv", + ".mp3", + ".ogg", + ".pdf", + ".zip", + ".rar", + ".html", + ".css", + ".js", + ".json", + ".xml", +} + +// FirstNames First names for fake name generation +var FirstNames = LoadFirstNames() + +// LastNames Last names for fake name generation +var LastNames = LoadLastNames() + +// FakeDomains Domain names for fake domain generation +var FakeDomains = LoadFakeDomains() diff --git a/internal/common/params/data/fake_domains.txt b/internal/common/params/data/fake_domains.txt new file mode 100644 index 0000000000..45b3a18493 --- /dev/null +++ b/internal/common/params/data/fake_domains.txt @@ -0,0 +1,158 @@ +example.com +test.com +demo.com +sample.com +fake.org +mock.net +placeholder.io +dummy.co +testing.dev +sandbox.app +trial.tech +prototype.site +staging.pro +beta.digital +alpha.online +preview.web +development.local +localhost.test +tempmail.org +mailinator.com +10minutemail.com +guerrillamail.com +maildrop.cc +fakebox.com +throwaway.email +temp-mail.org +yopmail.com +dispostable.com +getnada.com +tempail.com +guerrillamailblock.com +sharklasers.com +grr.la +guerrillamail.info +guerrillamail.biz +guerrillamail.org +guerrillamail.de +spam4.me +bccto.me +chacuo.net +027168.com +027168.net +027168.org +027168.info +027168.biz +027168.us +027168.cn +027168.tk +027168.ml +027168.ga +027168.cf +disposableemailaddresses.com +fakeinbox.com +mailcatch.com +mailnesia.com +trashmail.com +tempinbox.com +emailondeck.com +mytrashmail.com +mailexpire.com +tempemail.net +deadaddress.com +emailtemporar.ro +tempsky.com +tempemailaddress.com +tempmailaddress.com +acme.com +widget.org +gadget.net +foobar.io +lorem.com +ipsum.org +dolor.net +sit.dev +amet.co +consectetur.app +adipiscing.tech +elit.site +sed.pro +eiusmod.digital +tempor.online +incididunt.web +labore.local +magna.test +aliqua.info +enim.biz +minim.us +veniam.cn +quis.tk +nostrud.ml +exercitation.ga +ullamco.cf +laboris.xyz +nisi.space +aliquip.club +commodo.store +consequat.shop +duis.blog +aute.news +irure.media +reprehenderit.live +voluptate.studio +velit.agency +esse.company +cillum.group +fugiat.team +nulla.works +pariatur.tools +excepteur.services +sint.solutions +occaecat.systems +cupidatat.network +proident.cloud +sunt.email +culpa.mail +officia.inbox +deserunt.post +mollit.message +anim.chat +laborum.talk +undefined.null +null.void +void.empty +empty.blank +blank.zero +zero.one +one.two +two.three +three.four +four.five +five.six +six.seven +seven.eight +eight.nine +nine.ten +alpha.beta +beta.gamma +gamma.delta +delta.epsilon +epsilon.zeta +zeta.eta +eta.theta +theta.iota +iota.kappa +kappa.lambda +lambda.mu +mu.nu +nu.xi +xi.omicron +omicron.pi +pi.rho +rho.sigma +sigma.tau +tau.upsilon +upsilon.phi +phi.chi +chi.psi +psi.omega \ No newline at end of file diff --git a/internal/common/params/data/first_names.txt b/internal/common/params/data/first_names.txt new file mode 100644 index 0000000000..24a708994e --- /dev/null +++ b/internal/common/params/data/first_names.txt @@ -0,0 +1,300 @@ +John +Jane +Alex +Emily +Michael +Sarah +David +Laura +James +Mary +Robert +Patricia +Jennifer +Linda +Elizabeth +Barbara +Susan +Jessica +Karen +Nancy +Lisa +Betty +Helen +Sandra +Donna +Carol +Ruth +Sharon +Michelle +Laura +Sarah +Kimberly +Deborah +Dorothy +Amy +Angela +Ashley +Brenda +Emma +Olivia +Cynthia +Marie +Janet +Catherine +Frances +Christine +Samantha +Debra +Rachel +Carolyn +Janet +Virginia +Maria +Heather +Diane +Julie +Joyce +Victoria +Kelly +Christina +Joan +Evelyn +Lauren +Judith +Megan +Cheryl +Andrea +Hannah +Jacqueline +Martha +Gloria +Teresa +Sara +Janice +Marie +Julia +Heather +Diane +Ruth +Julie +Joyce +Virginia +Victoria +Kelly +Christina +Joan +Evelyn +Lauren +Judith +Olivia +Sophia +Charlotte +Amelia +Ava +Harper +Evelyn +Abigail +Ella +Scarlett +Grace +Chloe +Victoria +Riley +Aria +Lily +Aubrey +Zoey +Penelope +Lillian +Addison +Layla +Natalie +Camila +Hannah +Brooklyn +Zoe +Nora +Leah +Savannah +Audrey +Claire +Eleanor +Skylar +Ellie +Samantha +Stella +Paisley +Violet +Mia +Allison +Aaliyah +Sophie +Kate +Madison +Lucy +Maya +Genesis +Ariana +Valentina +Naomi +Caroline +Serenity +Kennedy +Autumn +Kinsley +Piper +Ruby +Madeline +Bella +Eva +Hazel +Anna +Kaylee +Melanie +Mackenzie +Peyton +Hailey +Gianna +Alexis +Kayla +Jasmine +Julia +Alyssa +Destiny +Andrea +Kimberly +Brianna +Samantha +Paige +Jocelyn +Katherine +Danielle +Rebecca +Amber +Megan +Rachel +Michelle +Stephanie +Amanda +Courtney +Heather +Nicole +Amy +Angela +Ashley +Brenda +Crystal +Dawn +Deborah +Diana +Donna +Jacqueline +Janet +Jennifer +Joyce +Julie +Karen +Kathleen +Kelly +Kimberly +Laura +Linda +Lisa +Maria +Marie +Martha +Mary +Nancy +Patricia +Ruth +Sandra +Sharon +Susan +Teresa +Virginia +Carolyn +Cheryl +Frances +Gloria +Jean +Joan +Judith +Beverly +Catherine +Christine +Cynthia +Debra +Dorothy +Elizabeth +Helen +Janice +Lori +Pamela +Robin +Shirley +Tina +Wanda +Wayne +Timothy +Tyler +Vincent +Walter +Willie +Arthur +Bruce +Carl +Dennis +Eugene +Francis +Gerald +Harold +Jack +Jerry +Keith +Kenneth +Lawrence +Louis +Mark +Martin +Paul +Peter +Philip +Ralph +Raymond +Roger +Roy +Russell +Stephen +Terry +Todd +Albert +Anthony +Billy +Bobby +Brandon +Brian +Charles +Christopher +Daniel +David +Donald +Edward +Frank +George +Gregory +James +Jeffrey +John +Jose +Joseph +Joshua +Kevin +Matthew +Michael +Richard +Robert +Ronald +Steven +Thomas +William \ No newline at end of file diff --git a/internal/common/params/data/last_names.txt b/internal/common/params/data/last_names.txt new file mode 100644 index 0000000000..010fe50aec --- /dev/null +++ b/internal/common/params/data/last_names.txt @@ -0,0 +1,312 @@ +Smith +Doe +Brown +Johnson +Williams +Jones +Garcia +Miller +Davis +Rodriguez +Martinez +Hernandez +Lopez +Gonzalez +Wilson +Anderson +Thomas +Taylor +Moore +Jackson +Martin +Lee +Perez +Thompson +White +Harris +Sanchez +Clark +Ramirez +Lewis +Robinson +Walker +Young +Allen +King +Wright +Scott +Torres +Nguyen +Hill +Flores +Green +Adams +Nelson +Baker +Hall +Rivera +Campbell +Mitchell +Carter +Roberts +Gomez +Phillips +Evans +Turner +Diaz +Parker +Cruz +Edwards +Collins +Reyes +Stewart +Morris +Morales +Murphy +Cook +Rogers +Gutierrez +Ortiz +Morgan +Cooper +Peterson +Bailey +Reed +Kelly +Howard +Ramos +Kim +Cox +Ward +Richardson +Watson +Brooks +Chavez +Wood +James +Bennett +Gray +Mendoza +Ruiz +Hughes +Price +Alvarez +Castillo +Sanders +Patel +Myers +Long +Ross +Foster +Jimenez +Powell +Jenkins +Perry +Russell +Sullivan +Bell +Coleman +Butler +Henderson +Barnes +Gonzales +Fisher +Vasquez +Simmons +Romero +Jordan +Patterson +Alexander +Hamilton +Graham +Reynolds +Griffin +Wallace +Moreno +West +Cole +Hayes +Bryant +Herrera +Gibson +Ellis +Tran +Medina +Aguilar +Stevens +Murray +Ford +Castro +Marshall +Owen +Harrison +Burton +Kennedy +Lynch +Fox +Elliott +Palmer +Stephens +Knight +Hunt +Webb +Armstrong +Berry +Bishop +Black +Blake +Boyd +Bradley +Burke +Burns +Carpenter +Chapman +Chavez +Clarke +Crawford +Cross +Curtis +Davidson +Dean +Dixon +Duncan +Dunn +Ferguson +Fletcher +Freeman +Fuller +Gardner +Garrett +Gordon +Grant +Graves +Harper +Hart +Hawkins +Hayes +Holmes +Hopkins +Howe +Hudson +Hunter +Jackson +Jenkins +Jimenez +Jordan +Kelley +Kim +Lawrence +Lawson +Little +Long +Lynch +Mason +Matthews +May +Mcdonald +Mills +Montgomery +Morris +Murray +Oliver +Owens +Palmer +Parker +Payne +Pierce +Porter +Powers +Price +Ray +Reid +Reynolds +Rice +Richards +Riley +Rose +Sims +Simpson +Stone +Sullivan +Tucker +Ward +Warren +Washington +Wells +West +Wheeler +Willis +Woods +Wright +Zhang +Liu +Wang +Yang +Chen +Zhao +Li +Wu +Zhou +Sun +Xu +Ma +Zhu +Hu +Guo +Lin +He +Gao +Luo +Zheng +Liang +Xie +Tang +Song +Xu +Han +Deng +Feng +Cao +Peng +Zeng +Xiao +Tian +Dong +Pan +Yuan +Cai +Jiang +Yu +Du +Ye +Cheng +Wei +Ren +Zou +Qin +Yin +Shi +Xue +Meng +Hao +Hou +Lei +Luo +Qiu +Tan +Jiang +Fan +Chang +Xiong +Bai +Shi +Kang +Jia +Lu +Shao +Gu +Mao +Qian +Wen +Guan +Ni \ No newline at end of file diff --git a/internal/common/params/data/user_agents.txt b/internal/common/params/data/user_agents.txt new file mode 100644 index 0000000000..678d45af23 --- /dev/null +++ b/internal/common/params/data/user_agents.txt @@ -0,0 +1,7 @@ +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) Gecko/20100101 Firefox/89.0 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15 +Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1 +Mozilla/5.0 (iPad; CPU OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1 +Mozilla/5.0 (Linux; Android 10; Pixel 3 XL Build/QP1A.190711.020) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Mobile Safari/537.36 +Mozilla/5.0 (Linux; Android 10; SM-G973F Build/QP1A.190711.020) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Mobile Safari/537.36 \ No newline at end of file diff --git a/internal/common/params/file_loader.go b/internal/common/params/file_loader.go new file mode 100644 index 0000000000..0e1b2de9b3 --- /dev/null +++ b/internal/common/params/file_loader.go @@ -0,0 +1,79 @@ +package params + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "strings" +) + +// loadLinesFromFile loads lines from a text file +func loadLinesFromFile(filename string) []string { + // Get the path relative to the current package directory + filePath := filepath.Join("internal", "common", "params", "data", filename) + + file, err := os.Open(filePath) + if err != nil { + // Fallback to empty slice if file not found + return []string{} + } + defer func(file *os.File) { + if err := file.Close(); err != nil { + fmt.Println("Error closing file:", err) + } + }(file) + + var lines []string + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line != "" { + lines = append(lines, line) + } + } + return lines +} + +// LoadUserAgents loads user agent strings from file +func LoadUserAgents() []string { + agents := loadLinesFromFile("user_agents.txt") + if len(agents) == 0 { + // Fallback data if file not found + return []string{ + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Gecko/20100101 Firefox/89.0", + } + } + return agents +} + +// LoadFirstNames loads first names from file +func LoadFirstNames() []string { + names := loadLinesFromFile("first_names.txt") + if len(names) == 0 { + // Fallback data if file not found + return []string{"John", "Jane", "Alex", "Emily", "Michael", "Sarah"} + } + return names +} + +// LoadLastNames loads last names from file +func LoadLastNames() []string { + names := loadLinesFromFile("last_names.txt") + if len(names) == 0 { + // Fallback data if file not found + return []string{"Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia"} + } + return names +} + +// LoadFakeDomains loads fake domains from file +func LoadFakeDomains() []string { + domains := loadLinesFromFile("fake_domains.txt") + if len(domains) == 0 { + // Fallback data if file not found + return []string{"example.com", "test.com", "demo.com", "sample.com"} + } + return domains +} diff --git a/internal/common/params/handlers.go b/internal/common/params/handlers.go new file mode 100644 index 0000000000..ccbdb2e21f --- /dev/null +++ b/internal/common/params/handlers.go @@ -0,0 +1,256 @@ +package params + +import ( + "crypto/rand" + "encoding/base64" + "encoding/json" + "fmt" + "math/big" + "net" + "net/url" + "os" + "strconv" + "strings" + "time" +) + +// generateRandomString generates a random string of specified length +func (pr *ParameterResolver) generateRandomString(length int, charset string) string { + if charset == "" { + charset = DefaultCharset + } + + result := make([]byte, length) + for i := range result { + result[i] = charset[pr.randSource.Intn(len(charset))] + } + return string(result) +} + +// generateSecureRandomString generates a cryptographically secure random string +func (pr *ParameterResolver) generateSecureRandomString(length int) string { + result := make([]byte, length) + for i := range result { + num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(DefaultCharset)))) + result[i] = DefaultCharset[num.Int64()] + } + return string(result) +} + +// formatTimeWithPattern converts Go time format patterns to actual values +func (pr *ParameterResolver) formatTimeWithPattern(pattern string) string { + // Convert strftime-like patterns to Go time format + goFormat := pattern + for strftime, goFmt := range TimeFormatReplacements { + if goFmt != "" { + goFormat = strings.ReplaceAll(goFormat, strftime, goFmt) + } + } + + // Handle special cases that don't have direct Go equivalents + if strings.Contains(pattern, "%U") || strings.Contains(pattern, "%W") { + _, week := pr.currentTime.ISOWeek() + weekStr := fmt.Sprintf("%02d", week) + goFormat = strings.ReplaceAll(goFormat, "%U", weekStr) + goFormat = strings.ReplaceAll(goFormat, "%W", weekStr) + } + + if strings.Contains(pattern, "%w") { + weekday := int(pr.currentTime.Weekday()) + goFormat = strings.ReplaceAll(goFormat, "%w", fmt.Sprintf("%d", weekday)) + } + + if strings.Contains(pattern, "%s") { + timestamp := fmt.Sprintf("%d", pr.currentTime.Unix()) + goFormat = strings.ReplaceAll(goFormat, "%s", timestamp) + } + + return pr.currentTime.Format(goFormat) +} + +func (pr *ParameterResolver) getLocalIP() string { + adders, err := net.InterfaceAddrs() + if err != nil { + return "" + } + + for _, addr := range adders { + // Skip loopback and down interfaces + if ipNet, ok := addr.(*net.IPNet); ok && !ipNet.IP.IsLoopback() && ipNet.IP.To4() != nil { + return ipNet.IP.String() + } + } + + return "" +} + +func (pr *ParameterResolver) getHostname() string { + hostname, err := os.Hostname() + if err != nil { + return "" + } + return hostname +} + +func (pr *ParameterResolver) generateUserAgent() string { + return UserAgents[pr.randSource.Intn(len(UserAgents))] +} + +func (pr *ParameterResolver) base64Encode(input string) string { + return base64.StdEncoding.EncodeToString([]byte(input)) +} + +func (pr *ParameterResolver) urlEncode(input string) string { + return url.QueryEscape(input) +} + +func (pr *ParameterResolver) jsonEscape(input string) string { + jsonBytes, err := json.Marshal(input) + if err != nil { + return input + } + // Remove the surrounding quotes from JSON marshal + result := string(jsonBytes) + if len(result) >= 2 && result[0] == '"' && result[len(result)-1] == '"' { + return result[1 : len(result)-1] + } + return result +} + +func (pr *ParameterResolver) mathOperation(input, operation string) string { + parts := strings.Split(input, ",") + if len(parts) != 2 { + return "0" + } + + num1, err1 := strconv.ParseFloat(strings.TrimSpace(parts[0]), 64) + num2, err2 := strconv.ParseFloat(strings.TrimSpace(parts[1]), 64) + + if err1 != nil || err2 != nil { + return "0" + } + + var result float64 + switch operation { + case "add": + result = num1 + num2 + case "sub": + result = num1 - num2 + case "mul": + result = num1 * num2 + case "div": + if num2 != 0 { + result = num1 / num2 + } else { + return "0" + } + default: + return "0" + } + + return fmt.Sprintf("%f", result) +} + +func (pr *ParameterResolver) reverseString(input string) string { + runes := []rune(input) + for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 { + runes[i], runes[j] = runes[j], runes[i] + } + return string(runes) +} + +func (pr *ParameterResolver) subString(input string) string { + parts := strings.Split(input, ",") + if len(parts) != 3 { + return "" + } + + str := strings.TrimSpace(parts[0]) + start, err1 := strconv.Atoi(strings.TrimSpace(parts[1])) + length, err2 := strconv.Atoi(strings.TrimSpace(parts[2])) + + if err1 != nil || err2 != nil || start < 0 || length < 0 { + return "" + } + + if start+length > len(str) { + length = len(str) - start + } + + return str[start : start+length] +} + +func (pr *ParameterResolver) generateHexColor() string { + return fmt.Sprintf("#%06x", pr.randSource.Intn(0xFFFFFF)) +} + +func (pr *ParameterResolver) generateRGBColor() string { + r := pr.randSource.Intn(256) + g := pr.randSource.Intn(256) + b := pr.randSource.Intn(256) + return fmt.Sprintf("rgb(%d,%d,%d)", r, g, b) +} + +func (pr *ParameterResolver) generateHSLColor() string { + h := pr.randSource.Intn(360) + s := pr.randSource.Intn(101) + l := pr.randSource.Intn(101) + return fmt.Sprintf("hsl(%d,%d%%,%d%%)", h, s, l) +} + +func (pr *ParameterResolver) generateMimeType() string { + return MimeTypes[pr.randSource.Intn(len(MimeTypes))] +} + +func (pr *ParameterResolver) generateFileExtension() string { + return FileExtensions[pr.randSource.Intn(len(FileExtensions))] +} + +func (pr *ParameterResolver) generateFakeEmail() string { + return fmt.Sprintf("user%d@example.com", pr.randSource.Intn(10000)) +} + +func (pr *ParameterResolver) generateFakePhone() string { + return fmt.Sprintf("+1-800-%04d-%04d", pr.randSource.Intn(10000), pr.randSource.Intn(10000)) +} + +func (pr *ParameterResolver) generateFakeName() string { + return fmt.Sprintf("%s %s", FirstNames[pr.randSource.Intn(len(FirstNames))], LastNames[pr.randSource.Intn(len(LastNames))]) +} + +func (pr *ParameterResolver) generateFakeDomain() string { + return fmt.Sprintf("www.%s", FakeDomains[pr.randSource.Intn(len(FakeDomains))]) +} + +func (pr *ParameterResolver) timeCalculation(input, operation string) string { + parts := strings.Split(input, ",") + if len(parts) != 2 { + return "" + } + + timeStr := strings.TrimSpace(parts[0]) + valueStr := strings.TrimSpace(parts[1]) + + value, err := strconv.Atoi(valueStr) + if err != nil { + return "" + } + + layout := "2006-01-02 15:04:05" + t, err := time.Parse(layout, timeStr) + if err != nil { + return "" + } + + var result time.Time + switch operation { + case "add": + result = t.Add(time.Duration(value) * time.Second) + case "sub": + result = t.Add(-time.Duration(value) * time.Second) + default: + return "" + } + + return result.Format(layout) +} diff --git a/internal/common/params.go b/internal/common/params/params.go similarity index 58% rename from internal/common/params.go rename to internal/common/params/params.go index e740b4e392..6a1532574d 100644 --- a/internal/common/params.go +++ b/internal/common/params/params.go @@ -1,9 +1,7 @@ -package common +package params import ( - "crypto/rand" "fmt" - "math/big" mathrand "math/rand" "os" "regexp" @@ -29,90 +27,6 @@ func NewParameterResolver() *ParameterResolver { } } -// NewParameterResolverWithTime creates a new parameter resolver with specified time -func NewParameterResolverWithTime(t time.Time) *ParameterResolver { - return &ParameterResolver{ - currentTime: t, - randSource: mathrand.New(mathrand.NewSource(t.UnixNano())), - } -} - -// generateRandomString generates a random string of specified length -func (pr *ParameterResolver) generateRandomString(length int, charset string) string { - if charset == "" { - charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - } - - result := make([]byte, length) - for i := range result { - result[i] = charset[pr.randSource.Intn(len(charset))] - } - return string(result) -} - -// generateSecureRandomString generates a cryptographically secure random string -func (pr *ParameterResolver) generateSecureRandomString(length int) string { - const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - result := make([]byte, length) - for i := range result { - num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) - result[i] = charset[num.Int64()] - } - return string(result) -} - -// formatTimeWithPattern converts Go time format patterns to actual values -func (pr *ParameterResolver) formatTimeWithPattern(pattern string) string { - // Convert strftime-like patterns to Go time format - replacements := map[string]string{ - "%Y": "2006", // 4-digit year - "%y": "06", // 2-digit year - "%m": "01", // month (01-12) - "%d": "02", // day (01-31) - "%H": "15", // hour (00-23) - "%M": "04", // minute (00-59) - "%S": "05", // second (00-59) - "%B": "January", // full month name - "%b": "Jan", // abbreviated month name - "%A": "Monday", // full weekday name - "%a": "Mon", // abbreviated weekday name - "%j": "002", // day of year (001-366) - "%U": "", // week of year (placeholder) - "%W": "", // week of year (placeholder) - "%w": "", // weekday (placeholder) - "%Z": "MST", // timezone name - "%z": "-0700", // timezone offset - "%s": "", // Unix timestamp (placeholder) - } - - goFormat := pattern - for strftime, goFmt := range replacements { - if goFmt != "" { - goFormat = strings.ReplaceAll(goFormat, strftime, goFmt) - } - } - - // Handle special cases that don't have direct Go equivalents - if strings.Contains(pattern, "%U") || strings.Contains(pattern, "%W") { - _, week := pr.currentTime.ISOWeek() - weekStr := fmt.Sprintf("%02d", week) - goFormat = strings.ReplaceAll(goFormat, "%U", weekStr) - goFormat = strings.ReplaceAll(goFormat, "%W", weekStr) - } - - if strings.Contains(pattern, "%w") { - weekday := int(pr.currentTime.Weekday()) - goFormat = strings.ReplaceAll(goFormat, "%w", fmt.Sprintf("%d", weekday)) - } - - if strings.Contains(pattern, "%s") { - timestamp := fmt.Sprintf("%d", pr.currentTime.Unix()) - goFormat = strings.ReplaceAll(goFormat, "%s", timestamp) - } - - return pr.currentTime.Format(goFormat) -} - // resolveSpecialParameter resolves non-datetime special parameters func (pr *ParameterResolver) resolveSpecialParameter(param string) string { // Handle different types of special parameters @@ -157,9 +71,9 @@ func (pr *ParameterResolver) resolveSpecialParameter(param string) string { // Handle rand_hex(length) format lengthStr := param[9 : len(param)-1] if length, err := strconv.Atoi(strings.TrimSpace(lengthStr)); err == nil && length > 0 { - return pr.generateRandomString(length, "0123456789abcdef") + return pr.generateRandomString(length, HexCharset) } - return pr.generateRandomString(8, "0123456789abcdef") + return pr.generateRandomString(8, HexCharset) // Environment variables case strings.HasPrefix(param, "env(") && strings.HasSuffix(param, ")"): @@ -183,6 +97,80 @@ func (pr *ParameterResolver) resolveSpecialParameter(param string) string { case param == "hash_md5_like": return fmt.Sprintf("%032x", pr.currentTime.UnixNano()) + // Network and System Information + case param == "local_ip": + return pr.getLocalIP() + case param == "hostname": + return pr.getHostname() + case param == "user_agent": + return pr.generateUserAgent() + case param == "http_method": + return HTTPMethods[pr.randSource.Intn(len(HTTPMethods))] + + // Encoding and Decoding + case strings.HasPrefix(param, "base64(") && strings.HasSuffix(param, ")"): + content := param[7 : len(param)-1] + return pr.base64Encode(content) + case strings.HasPrefix(param, "url_encode(") && strings.HasSuffix(param, ")"): + content := param[11 : len(param)-1] + return pr.urlEncode(content) + case strings.HasPrefix(param, "json_escape(") && strings.HasSuffix(param, ")"): + content := param[12 : len(param)-1] + return pr.jsonEscape(content) + + // Mathematical Operations + case strings.HasPrefix(param, "add(") && strings.HasSuffix(param, ")"): + return pr.mathOperation(param[4:len(param)-1], "add") + case strings.HasPrefix(param, "sub(") && strings.HasSuffix(param, ")"): + return pr.mathOperation(param[4:len(param)-1], "sub") + case strings.HasPrefix(param, "mul(") && strings.HasSuffix(param, ")"): + return pr.mathOperation(param[4:len(param)-1], "mul") + case strings.HasPrefix(param, "div(") && strings.HasSuffix(param, ")"): + return pr.mathOperation(param[4:len(param)-1], "div") + + // Text Processing + case strings.HasPrefix(param, "upper(") && strings.HasSuffix(param, ")"): + content := param[6 : len(param)-1] + return strings.ToUpper(content) + case strings.HasPrefix(param, "lower(") && strings.HasSuffix(param, ")"): + content := param[6 : len(param)-1] + return strings.ToLower(content) + case strings.HasPrefix(param, "reverse(") && strings.HasSuffix(param, ")"): + content := param[8 : len(param)-1] + return pr.reverseString(content) + case strings.HasPrefix(param, "substr(") && strings.HasSuffix(param, ")"): + return pr.subString(param[7 : len(param)-1]) + + // Color and CSS + case param == "color_hex": + return pr.generateHexColor() + case param == "color_rgb": + return pr.generateRGBColor() + case param == "color_hsl": + return pr.generateHSLColor() + + // File and MIME types + case param == "mime_type": + return pr.generateMimeType() + case param == "file_ext": + return pr.generateFileExtension() + + // Fake Data Generation + case param == "fake_email": + return pr.generateFakeEmail() + case param == "fake_phone": + return pr.generateFakePhone() + case param == "fake_name": + return pr.generateFakeName() + case param == "fake_domain": + return pr.generateFakeDomain() + + // Time calculations + case strings.HasPrefix(param, "time_add(") && strings.HasSuffix(param, ")"): + return pr.timeCalculation(param[9:len(param)-1], "add") + case strings.HasPrefix(param, "time_sub(") && strings.HasSuffix(param, ")"): + return pr.timeCalculation(param[9:len(param)-1], "sub") + default: // If it's a time format, try to format it if strings.Contains(param, "%") { @@ -212,17 +200,10 @@ func (pr *ParameterResolver) resolveSpecialParameterForDisplay(param string) str // maskSensitiveValue masks sensitive environment variable values func (pr *ParameterResolver) maskSensitiveValue(value, envVar string) string { - // List of sensitive environment variable patterns - sensitivePatterns := []string{ - "key", "secret", "token", "password", "pass", "pwd", - "auth", "credential", "private", "api_key", "access", - "jwt", "bearer", "signature", "hash", "salt", - } - envVarLower := strings.ToLower(envVar) // Check if this environment variable name suggests it contains sensitive data - for _, pattern := range sensitivePatterns { + for _, pattern := range SensitivePatterns { if strings.Contains(envVarLower, pattern) { return pr.maskValue(value) } @@ -262,21 +243,74 @@ func (pr *ParameterResolver) maskValue(value string) string { // ResolveParameters resolves dynamic parameters in a string func (pr *ParameterResolver) ResolveParameters(input string) string { - // Use regex to find and replace parameters in {{...}} format - re := regexp.MustCompile(`\{\{([^}]+)}}`) + return pr.resolveParametersWithDepth(input, 0, make(map[string]bool)) +} - result := re.ReplaceAllStringFunc(input, func(match string) string { - // Extract the parameter from {{parameter}} - param := strings.TrimSpace(re.FindStringSubmatch(match)[1]) +// resolveParametersWithDepth resolves parameters with recursion depth control and cycle detection +func (pr *ParameterResolver) resolveParametersWithDepth(input string, depth int, resolved map[string]bool) string { + const maxDepth = 10 // Prevent infinite recursion - // Handle time format parameters (starting with %) - if strings.HasPrefix(param, "%") { - return pr.formatTimeWithPattern(param) + if depth >= maxDepth { + return input // Return as-is if max depth reached + } + + // Check for cycles in resolution + if resolved[input] { + return input // Return as-is if we've already processed this exact string + } + + // Use regex to find parameters in {{...}} format + re := regexp.MustCompile(`\{\{([^{}]+)}}`) + + // If no matches found, return the input + if !re.MatchString(input) { + return input + } + + // Mark this input as being resolved to detect cycles + resolved[input] = true + + // Find all matches and their positions + matches := re.FindAllStringSubmatchIndex(input, -1) + if len(matches) == 0 { + delete(resolved, input) + return input + } + + result := input + + // Process matches from right to left to preserve indices + for i := len(matches) - 1; i >= 0; i-- { + match := matches[i] + fullMatchStart, fullMatchEnd := match[0], match[1] + paramStart, paramEnd := match[2], match[3] + + // Extract the parameter content + param := strings.TrimSpace(input[paramStart:paramEnd]) + + // First, recursively resolve any nested parameters within this parameter + resolvedParam := pr.resolveParametersWithDepth(param, depth+1, resolved) + + // Then resolve the parameter itself + var resolvedValue string + if strings.HasPrefix(resolvedParam, "%") { + resolvedValue = pr.formatTimeWithPattern(resolvedParam) + } else { + resolvedValue = pr.resolveSpecialParameter(resolvedParam) } - // Handle other special parameters - return pr.resolveSpecialParameter(param) - }) + // Replace the match in the result + result = result[:fullMatchStart] + resolvedValue + result[fullMatchEnd:] + } + + // Remove this input from resolved map + delete(resolved, input) + + // Check if the result contains more parameters that need resolution + if re.MatchString(result) && result != input { + // Recursively resolve the result if it still contains parameters + return pr.resolveParametersWithDepth(result, depth, resolved) + } return result } diff --git a/internal/common/params/params_test.go b/internal/common/params/params_test.go new file mode 100644 index 0000000000..c67eb33142 --- /dev/null +++ b/internal/common/params/params_test.go @@ -0,0 +1,606 @@ +package params + +import ( + "fmt" + "os" + "regexp" + "strconv" + "strings" + "testing" +) + +func TestNewParameterResolver(t *testing.T) { + pr := NewParameterResolver() + if pr == nil { + t.Fatal("NewParameterResolver() returned nil") + } + if pr.randSource == nil { + t.Error("randSource should not be nil") + } + if pr.currentTime.IsZero() { + t.Error("currentTime should not be zero") + } +} + +func TestResolveSpecialParameter_UUID(t *testing.T) { + pr := NewParameterResolver() + + // Test UUID generation + uuid := pr.resolveSpecialParameter("uuid") + if len(uuid) != 36 { + t.Errorf("UUID length should be 36, got %d", len(uuid)) + } + if !regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`).MatchString(uuid) { + t.Errorf("Invalid UUID format: %s", uuid) + } + + // Test short UUID generation + uuidShort := pr.resolveSpecialParameter("uuid_short") + if len(uuidShort) != 32 { + t.Errorf("Short UUID length should be 32, got %d", len(uuidShort)) + } + if !regexp.MustCompile(`^[0-9a-f]{32}$`).MatchString(uuidShort) { + t.Errorf("Invalid short UUID format: %s", uuidShort) + } +} + +func TestResolveSpecialParameter_Random(t *testing.T) { + pr := NewParameterResolver() + + // Test basic random number + rand := pr.resolveSpecialParameter("rand") + if randInt, err := strconv.Atoi(rand); err != nil || randInt < 0 || randInt >= 1000000 { + t.Errorf("Random number should be between 0 and 999999, got %s", rand) + } + + // Test random int + randInt := pr.resolveSpecialParameter("rand_int") + if ri, err := strconv.Atoi(randInt); err != nil || ri < 0 || ri >= 2147483647 { + t.Errorf("Random int should be between 0 and 2147483646, got %s", randInt) + } + + // Test random range + randRange := pr.resolveSpecialParameter("rand(10,20)") + if rr, err := strconv.Atoi(randRange); err != nil || rr < 10 || rr >= 20 { + t.Errorf("Random range should be between 10 and 19, got %s", randRange) + } + + // Test invalid random range + invalidRange := pr.resolveSpecialParameter("rand(invalid,range)") + if ir, err := strconv.Atoi(invalidRange); err != nil || ir < 0 || ir >= 1000000 { + t.Errorf("Invalid random range should fallback to default range, got %s", invalidRange) + } +} + +func TestResolveSpecialParameter_RandomString(t *testing.T) { + pr := NewParameterResolver() + + // Test default random string + randStr := pr.resolveSpecialParameter("rand_str") + if len(randStr) != 8 { + t.Errorf("Random string length should be 8, got %d", len(randStr)) + } + + // Test secure random string + secureStr := pr.resolveSpecialParameter("rand_str_secure") + if len(secureStr) != 16 { + t.Errorf("Secure random string length should be 16, got %d", len(secureStr)) + } + + // Test custom length random string + customStr := pr.resolveSpecialParameter("rand_str(12)") + if len(customStr) != 12 { + t.Errorf("Custom random string length should be 12, got %d", len(customStr)) + } + + // Test hex random string + hexStr := pr.resolveSpecialParameter("rand_hex(8)") + if len(hexStr) != 8 { + t.Errorf("Hex string length should be 8, got %d", len(hexStr)) + } + if !regexp.MustCompile(`^[0-9a-f]+$`).MatchString(hexStr) { + t.Errorf("Hex string should only contain hex characters, got %s", hexStr) + } +} + +func TestResolveSpecialParameter_Environment(t *testing.T) { + pr := NewParameterResolver() + + // Set test environment variable + testKey := "TEST_PARAM_KEY" + testValue := "test_value" + if err := os.Setenv(testKey, testValue); err != nil { + return + } + defer func(key string) { + if err := os.Unsetenv(key); err != nil { + t.Errorf("Failed to unset environment variable %s: %v", key, err) + } + }(testKey) + + // Test environment variable resolution + envResult := pr.resolveSpecialParameter("env(" + testKey + ")") + if envResult != testValue { + t.Errorf("Environment variable should be %s, got %s", testValue, envResult) + } + + // Test non-existent environment variable + nonExistent := pr.resolveSpecialParameter("env(NON_EXISTENT_VAR)") + if nonExistent != "" { + t.Errorf("Non-existent environment variable should return empty string, got %s", nonExistent) + } +} + +func TestResolveSpecialParameter_Sequence(t *testing.T) { + pr := NewParameterResolver() + + // Test sequence number + seq := pr.resolveSpecialParameter("seq") + if _, err := strconv.Atoi(seq); err != nil { + t.Errorf("Sequence should be a valid integer, got %s", seq) + } + + // Test daily sequence + seqDaily := pr.resolveSpecialParameter("seq_daily") + if _, err := strconv.Atoi(seqDaily); err != nil { + t.Errorf("Daily sequence should be a valid integer, got %s", seqDaily) + } +} + +func TestResolveSpecialParameter_Hash(t *testing.T) { + pr := NewParameterResolver() + + // Test short hash + hashShort := pr.resolveSpecialParameter("hash_short") + if !regexp.MustCompile(`^[0-9a-f]+$`).MatchString(hashShort) { + t.Errorf("Short hash should be hex format, got %s", hashShort) + } + + // Test MD5-like hash + hashMD5 := pr.resolveSpecialParameter("hash_md5_like") + if len(hashMD5) != 32 { + t.Errorf("MD5-like hash should be 32 characters, got %d", len(hashMD5)) + } +} + +func TestResolveSpecialParameter_Network(t *testing.T) { + pr := NewParameterResolver() + + // Test local IP (maybe empty if no network interface) + localIP := pr.resolveSpecialParameter("local_ip") + if localIP != "" && !regexp.MustCompile(`^\d+\.\d+\.\d+\.\d+$`).MatchString(localIP) { + t.Errorf("Local IP should be valid IPv4 format or empty, got %s", localIP) + } + + // Test hostname (should not be empty) + hostname := pr.resolveSpecialParameter("hostname") + if hostname == "" { + t.Error("Hostname should not be empty") + } + + // Test user agent + userAgent := pr.resolveSpecialParameter("user_agent") + if userAgent == "" { + t.Error("User agent should not be empty") + } + + // Test HTTP method + httpMethod := pr.resolveSpecialParameter("http_method") + validMethods := map[string]bool{"GET": true, "POST": true, "PUT": true, "DELETE": true, "PATCH": true, "HEAD": true, "OPTIONS": true} + if !validMethods[httpMethod] { + t.Errorf("HTTP method should be valid, got %s", httpMethod) + } +} + +func TestResolveSpecialParameter_Encoding(t *testing.T) { + pr := NewParameterResolver() + + // Test Base64 encoding + base64Result := pr.resolveSpecialParameter("base64(hello)") + expected := "aGVsbG8=" + if base64Result != expected { + t.Errorf("Base64 encoding should be %s, got %s", expected, base64Result) + } + + // Test URL encoding + urlResult := pr.resolveSpecialParameter("url_encode(hello world)") + expectedURL := "hello+world" + if urlResult != expectedURL { + t.Errorf("URL encoding should be %s, got %s", expectedURL, urlResult) + } + + // Test JSON escape + jsonResult := pr.resolveSpecialParameter("json_escape(hello\"world)") + expectedJSON := "hello\\\"world" + if jsonResult != expectedJSON { + t.Errorf("JSON escape should be %s, got %s", expectedJSON, jsonResult) + } +} + +func TestResolveSpecialParameter_Math(t *testing.T) { + pr := NewParameterResolver() + + // Test addition + addResult := pr.resolveSpecialParameter("add(5,3)") + if !strings.Contains(addResult, "8") { + t.Errorf("Addition result should contain 8, got %s", addResult) + } + + // Test subtraction + subResult := pr.resolveSpecialParameter("sub(10,3)") + if !strings.Contains(subResult, "7") { + t.Errorf("Subtraction result should contain 7, got %s", subResult) + } + + // Test multiplication + mulResult := pr.resolveSpecialParameter("mul(4,3)") + if !strings.Contains(mulResult, "12") { + t.Errorf("Multiplication result should contain 12, got %s", mulResult) + } + + // Test division + divResult := pr.resolveSpecialParameter("div(15,3)") + if !strings.Contains(divResult, "5") { + t.Errorf("Division result should contain 5, got %s", divResult) + } + + // Test division by zero + divZeroResult := pr.resolveSpecialParameter("div(10,0)") + if divZeroResult != "0" { + t.Errorf("Division by zero should return 0, got %s", divZeroResult) + } +} + +func TestResolveSpecialParameter_TextProcessing(t *testing.T) { + pr := NewParameterResolver() + + // Test uppercase + upperResult := pr.resolveSpecialParameter("upper(hello)") + if upperResult != "HELLO" { + t.Errorf("Uppercase should be HELLO, got %s", upperResult) + } + + // Test lowercase + lowerResult := pr.resolveSpecialParameter("lower(HELLO)") + if lowerResult != "hello" { + t.Errorf("Lowercase should be hello, got %s", lowerResult) + } + + // Test reverse + reverseResult := pr.resolveSpecialParameter("reverse(hello)") + if reverseResult != "olleh" { + t.Errorf("Reverse should be olleh, got %s", reverseResult) + } + + // Test substring + substrResult := pr.resolveSpecialParameter("substr(hello,1,3)") + if substrResult != "ell" { + t.Errorf("Substring should be ell, got %s", substrResult) + } +} + +func TestResolveSpecialParameter_Colors(t *testing.T) { + pr := NewParameterResolver() + + // Test hex color + hexColor := pr.resolveSpecialParameter("color_hex") + if !regexp.MustCompile(`^#[0-9a-f]{6}$`).MatchString(hexColor) { + t.Errorf("Hex color should match pattern #xxxxxx, got %s", hexColor) + } + + // Test RGB color + rgbColor := pr.resolveSpecialParameter("color_rgb") + if !regexp.MustCompile(`^rgb\(\d+,\d+,\d+\)$`).MatchString(rgbColor) { + t.Errorf("RGB color should match pattern rgb(x,y,z), got %s", rgbColor) + } + + // Test HSL color + hslColor := pr.resolveSpecialParameter("color_hsl") + if !regexp.MustCompile(`^hsl\(\d+,\d+%,\d+%\)$`).MatchString(hslColor) { + t.Errorf("HSL color should match pattern hsl(x,y%%,z%%), got %s", hslColor) + } +} + +func TestResolveSpecialParameter_FileTypes(t *testing.T) { + pr := NewParameterResolver() + + // Test MIME type + mimeType := pr.resolveSpecialParameter("mime_type") + if mimeType == "" { + t.Error("MIME type should not be empty") + } + + // Test file extension + fileExt := pr.resolveSpecialParameter("file_ext") + if !strings.HasPrefix(fileExt, ".") { + t.Errorf("File extension should start with dot, got %s", fileExt) + } +} + +func TestResolveSpecialParameter_FakeData(t *testing.T) { + pr := NewParameterResolver() + + // Test fake email + fakeEmail := pr.resolveSpecialParameter("fake_email") + if !strings.Contains(fakeEmail, "@") { + t.Errorf("Fake email should contain @, got %s", fakeEmail) + } + + // Test fake phone + fakePhone := pr.resolveSpecialParameter("fake_phone") + if !strings.HasPrefix(fakePhone, "+1-800-") { + t.Errorf("Fake phone should start with +1-800-, got %s", fakePhone) + } + + // Test fake name + fakeName := pr.resolveSpecialParameter("fake_name") + if !strings.Contains(fakeName, " ") { + t.Errorf("Fake name should contain space, got %s", fakeName) + } + + // Test fake domain + fakeDomain := pr.resolveSpecialParameter("fake_domain") + if !strings.HasPrefix(fakeDomain, "www.") { + t.Errorf("Fake domain should start with www., got %s", fakeDomain) + } +} + +func TestResolveSpecialParameter_TimeFormat(t *testing.T) { + pr := NewParameterResolver() + + // Test year format + yearResult := pr.resolveSpecialParameter("%Y") + currentYear := strconv.Itoa(pr.currentTime.Year()) + if yearResult != currentYear { + t.Errorf("Year format should be %s, got %s", currentYear, yearResult) + } + + // Test month format + monthResult := pr.resolveSpecialParameter("%m") + expectedMonth := pr.currentTime.Format("01") + if monthResult != expectedMonth { + t.Errorf("Month format should be %s, got %s", expectedMonth, monthResult) + } +} + +func TestResolveParameters(t *testing.T) { + pr := NewParameterResolver() + + // Test simple parameter replacement + result := pr.ResolveParameters("Hello {{uuid}}") + if !strings.HasPrefix(result, "Hello ") { + t.Errorf("Result should start with 'Hello ', got %s", result) + } + + // Test multiple parameters + result = pr.ResolveParameters("{{%Y}}-{{%m}}-{{%d}}") + if !regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`).MatchString(result) { + t.Errorf("Date format should match YYYY-MM-DD, got %s", result) + } + + // Test no parameters + result = pr.ResolveParameters("No parameters here") + if result != "No parameters here" { + t.Errorf("String without parameters should remain unchanged, got %s", result) + } +} + +func TestMaskSensitiveValue(t *testing.T) { + pr := NewParameterResolver() + + // Test short value masking + result := pr.maskSensitiveValue("abc", "api_key") + if result != "***" { + t.Errorf("Short value should be fully masked, got %s", result) + } + + // Test longer value masking + result = pr.maskSensitiveValue("secret123456", "password") + if !strings.HasPrefix(result, "se") || !strings.HasSuffix(result, "56") { + t.Errorf("Long value should show first 2 and last 2 chars, got %s", result) + } + + // Test non-sensitive value + result = pr.maskSensitiveValue("normal_value", "normal_var") + if result != "normal_value" { + t.Errorf("Non-sensitive value should remain unchanged, got %s", result) + } +} + +func TestHighlightChanges(t *testing.T) { + pr := NewParameterResolver() + + // Test highlighting with parameters + result, segments := pr.HighlightChanges("Hello {{uuid}} world") + + if len(segments) != 3 { + t.Errorf("Should have 3 segments, got %d", len(segments)) + } + + if segments[0].Text != "Hello " || segments[0].IsHighlight { + t.Errorf("First segment should be 'Hello ' and not highlighted") + } + + if !segments[1].IsHighlight { + t.Errorf("Second segment should be highlighted") + } + + if segments[2].Text != " world" || segments[2].IsHighlight { + t.Errorf("Third segment should be ' world' and not highlighted") + } + + // Test no parameters + result, segments = pr.HighlightChanges("No parameters") + if result != "No parameters" || len(segments) != 0 { + t.Errorf("String without parameters should return original string and no segments") + } +} + +func TestGetResolvedValue(t *testing.T) { + pr := NewParameterResolver() + + result := pr.GetResolvedValue("{{%Y}}") + currentYear := strconv.Itoa(pr.currentTime.Year()) + if result != currentYear { + t.Errorf("Resolved value should be current year, got %s", result) + } +} + +func TestGetOriginalValue(t *testing.T) { + pr := NewParameterResolver() + + original := "{{uuid}}" + result := pr.GetOriginalValue(original) + if result != original { + t.Errorf("Original value should remain unchanged, got %s", result) + } +} + +func TestResolveSpecialParameterForDisplay(t *testing.T) { + pr := NewParameterResolver() + + // Set up test environment variable with sensitive name + testKey := "API_SECRET" + testValue := "very_secret_key_123456" + if err := os.Setenv(testKey, testValue); err != nil { + return + } + defer func(key string) { + if err := os.Unsetenv(key); err != nil { + t.Errorf("Failed to unset environment variable %s: %v", key, err) + } + }(testKey) + + // Test that sensitive env vars are masked for display + result := pr.resolveSpecialParameterForDisplay("env(" + testKey + ")") + if result == testValue { + t.Error("Sensitive environment variable should be masked for display") + } + if !strings.Contains(result, "*") { + t.Errorf("Masked value should contain asterisks, got %s", result) + } +} + +func TestTimeCalculation(t *testing.T) { + pr := NewParameterResolver() + + // Test time addition + result := pr.resolveSpecialParameter("time_add(2023-01-01 12:00:00,3600)") + if result != "2023-01-01 13:00:00" { + t.Errorf("Time addition should add 1 hour, got %s", result) + } + + // Test time subtraction + result = pr.resolveSpecialParameter("time_sub(2023-01-01 12:00:00,1800)") + if result != "2023-01-01 11:30:00" { + t.Errorf("Time subtraction should subtract 30 minutes, got %s", result) + } + + // Test invalid time format + result = pr.resolveSpecialParameter("time_add(invalid,3600)") + if result != "" { + t.Errorf("Invalid time format should return empty string, got %s", result) + } +} + +// Test nested tag resolution +func TestNestedTagResolution(t *testing.T) { + pr := NewParameterResolver() + + // Test simple nested tags + result := pr.ResolveParameters("{{base64({{uuid}})}}") + if len(result) == 0 { + t.Error("Nested base64(uuid) should produce a result") + } + + // Test multiple levels of nesting + result = pr.ResolveParameters("{{upper({{base64(test)}})}}") + expected := "DGVZDA==" // base64 of "test" in uppercase + if result != expected { + t.Errorf("Expected %s, got %s", expected, result) + } + + // Test nested with random content + result = pr.ResolveParameters("{{url_encode({{fake_email}})}}") + if !strings.Contains(result, "%40") { // @ symbol should be encoded as %40 + t.Error("URL encoded email should contain %40") + } + + // Test complex nesting + result = pr.ResolveParameters("{{base64({{upper({{fake_name}})}})}}") + if len(result) == 0 { + t.Error("Complex nested expression should produce a result") + } + + // Test mathematical operations with nested values + result = pr.ResolveParameters("{{add({{rand(1,10)}},{{rand(1,10)}})}}") + if len(result) == 0 { + t.Error("Nested mathematical operation should produce a result") + } + + // Test string manipulation with nested content + result = pr.ResolveParameters("{{substr({{uuid}},0,8)}}") + if len(result) != 8 { + t.Errorf("Substring of UUID should be 8 characters, got %d", len(result)) + } +} + +// Test cycle detection +func TestNestedTagCycleDetection(t *testing.T) { + pr := NewParameterResolver() + + // Test that cycles are properly detected and handled + result := pr.ResolveParameters("{{upper({{upper(test)}})}}") + expected := "TEST" + if result != expected { + t.Errorf("Expected %s, got %s", expected, result) + } + + // Test self-referencing prevention (should not cause infinite loop) + result = pr.ResolveParameters("test") + if result != "test" { + t.Errorf("Non-parameterized string should remain unchanged") + } +} + +// Test recursion depth limits +func TestNestedTagDepthLimit(t *testing.T) { + pr := NewParameterResolver() + + // Create a deeply nested structure + deepNested := "test" + for i := 0; i < 15; i++ { // More than maxDepth (10) + deepNested = fmt.Sprintf("{{upper(%s)}}", deepNested) + } + + result := pr.ResolveParameters(deepNested) + // Should not crash and should return some reasonable result + if len(result) == 0 { + t.Error("Deep nesting should not result in empty string") + } +} + +// Test mixed nested and non-nested parameters +func TestMixedNestedParameters(t *testing.T) { + pr := NewParameterResolver() + + // Mix nested and simple parameters + result := pr.ResolveParameters("ID: {{uuid}} - Encoded: {{base64({{fake_name}})}} - Time: {{%Y}}") + + parts := strings.Split(result, " - ") + if len(parts) != 3 { + t.Errorf("Expected 3 parts separated by ' - ', got %d", len(parts)) + } + + if !strings.HasPrefix(parts[0], "ID: ") { + t.Error("First part should start with 'ID: '") + } + + if !strings.HasPrefix(parts[1], "Encoded: ") { + t.Error("Second part should start with 'Encoded: '") + } + + if !strings.HasPrefix(parts[2], "Time: ") { + t.Error("Third part should start with 'Time: '") + } +} diff --git a/internal/configure/config.go b/internal/configure/config.go index 829b0f7809..25b13a567f 100644 --- a/internal/configure/config.go +++ b/internal/configure/config.go @@ -4,7 +4,7 @@ import ( "log" "os" - "github.com/wcy-dt/ponghub/internal/common" + "github.com/wcy-dt/ponghub/internal/common/params" "github.com/wcy-dt/ponghub/internal/types/structures/configure" "github.com/wcy-dt/ponghub/internal/types/types/default_config" @@ -27,7 +27,7 @@ func setDefaultConfigs(cfg *configure.Configure) { // resolveConfigParameters resolves dynamic parameters in configuration func resolveConfigParameters(cfg *configure.Configure) { - resolver := common.NewParameterResolver() + resolver := params.NewParameterResolver() for i := range cfg.Services { for j := range cfg.Services[i].Endpoints {