├── .gitignore ├── LICENSE ├── README.md ├── client └── client.go ├── cmd ├── config.go ├── subtype.go └── toml.go ├── config.toml ├── go.mod ├── go.sum ├── log └── log.go └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | /dist 3 | /*.bat 4 | /*.sh 5 | ip.txt 6 | config.debug.toml -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Josh Baker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # YesCloudflare 2 | 3 | 查询Cloudflare反代节点小工具 4 | 5 | 程序原型由 [Joey Huang](https://t.me/Joeyblog/) 开发 6 | 7 | Telegram反馈群: https://t.me/+ft-zI76oovgwNmRh/ 8 | 9 | ### 用法 Usage 10 | 11 | ``` 12 | Usage of ShadowObj/yescloudflare: 13 | 14 | -c/-config string 15 | 指定配置文件 (默认config.toml) 16 | 注意: 指定配置文件不会覆盖命令行参数。 17 | -o/-output ip.txt 18 | 指定输出文件 (默认ip.txt) 19 | -A/-auto 20 | 自动获取下一页内容 (默认需要确认) 21 | -key apikey 22 | 指定APIKEY 23 | -norepeat 24 | 自动去除重复IP (默认不去除) 25 | -port port 26 | 指定端口 (默认全部, 可用英文逗号分隔) 27 | -asn asn1,asn2 28 | 指定ASN (默认全部, 可用英文逗号分隔) 29 | -region CN,HK,JP,KR,TW 30 | 指定地区ISO3166二字码 31 | (默认全部, 可用英文逗号分隔) 32 | -page 1-10 33 | 指定需要查询的页面 34 | (默认1-10, 范围应为第1到100页) 35 | ``` 36 | 37 | 附: 38 | 1. [config.toml 配置文件完整示例](https://github.com/ShadowObj/YesCloudflare/blob/main/config.toml) 39 | 2. [小白向 - 如何获取Cloudflare反代IP列表指北](https://telegra.ph/%E5%A6%82%E4%BD%95%E8%8E%B7%E5%8F%96Cloudflare%E5%8F%8D%E4%BB%A3IP%E5%88%97%E8%A1%A8%E6%8C%87%E5%8C%97-08-21) 40 | 41 | ### 如何获取APIKEY 42 | 43 | 进入censys的Search API汇总页 44 | https://search.censys.io/api#/hosts/searchHosts 45 | 46 | 如果未注册: 47 | 48 | 点击右上角Register,按流程进行(需要常见邮箱,不支持临时邮箱),进行邮箱验证,**登入时选择"Censys Search"**,重新点击上方网址 49 | 50 | 如果已注册:进入上方网址 51 | 52 | 在上述流程完成后,来到下方任意一个API块,点击Try it out,再点击Execute,在Responses中会出现用于curl的一串命令,其中"Authorization: Basic"后的一长串字符即为APIKEY。 53 | 54 | *注意:不要复制额外的空格哟!(^U^)ノ~* 55 | -------------------------------------------------------------------------------- /client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "os" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/ShadowObj/yescloudflare/cmd" 13 | "github.com/tidwall/gjson" 14 | ) 15 | 16 | type Censys struct { 17 | Key string 18 | Client *http.Client 19 | } 20 | 21 | type Hit struct { 22 | IP string 23 | Port int 24 | } 25 | 26 | // Exec performs a task with censys client 27 | func (c *Censys) Exec(conf *cmd.Config) { 28 | var ( 29 | f *os.File 30 | err error 31 | hits *[]*Hit 32 | ) 33 | logger := conf.GetLogger() 34 | if f, err = os.Create(conf.Output); err != nil { 35 | logger.Fatalf("Open %s failed: %v", conf.Output, err) 36 | } 37 | defer f.Close() 38 | writer := bufio.NewWriter(f) 39 | defer writer.Flush() 40 | for i := conf.Page.Start; i < (conf.Page.End + 1); i++ { 41 | if !conf.Auto { 42 | inputT := "" 43 | logger.Printf("继续获取第 %d 页内容? (Y/N, Default Y): ", i) 44 | fmt.Scanln(&inputT) 45 | if inputT == "N" { 46 | break 47 | } 48 | } 49 | logger.Printf("正在获取第 %d 页内容...\n", i) 50 | if hits, err = c.get(i, conf.GetQuery(), conf.Norepeat); err != nil { 51 | logger.Fatalf("Get failed: %v\n(page: %d, query: %s)", err, i, conf.GetQuery()) 52 | } 53 | logger.Printf("在第 %d 页中发现了 %d 个节点.\n", i, len(*hits)) 54 | for _, v := range *hits { 55 | ip := v.IP 56 | if strings.Contains(v.IP, ":") { 57 | ip = "[" + v.IP + "]" 58 | } 59 | if !conf.Port.Contains(v.Port) { 60 | continue 61 | } 62 | if _, err = writer.WriteString(ip + ":" + strconv.Itoa(v.Port) + "\n"); err != nil { 63 | logger.Fatalf("Unable to write into buffer: %v", err) 64 | } 65 | } 66 | } 67 | } 68 | 69 | func (c *Censys) get(page int, query string, norepeat bool) (*[]*Hit, error) { 70 | var ( 71 | err error 72 | hits []*Hit 73 | req *http.Request 74 | resp *http.Response 75 | ) 76 | 77 | if req, err = c.newRequest(page, query); err != nil { 78 | return nil, err 79 | } 80 | if resp, err = c.Client.Do(req); err != nil { 81 | return nil, err 82 | } 83 | respData, _ := io.ReadAll(resp.Body) 84 | resp.Body.Close() 85 | for _, v := range gjson.Get(string(respData), "result.hits").Array() { 86 | ip := v.Get("ip").String() 87 | if norepeat { 88 | hits = append(hits, &Hit{ 89 | IP: ip, 90 | Port: int(v.Get("services.#(service_name==\"HTTP\").port").Int()), 91 | }) 92 | } else { 93 | for _, port := range v.Get("services.#(service_name==\"HTTP\")#.port").Array() { 94 | hits = append(hits, &Hit{ 95 | IP: ip, 96 | Port: int(port.Int()), 97 | }) 98 | } 99 | } 100 | } 101 | return &hits, nil 102 | } 103 | 104 | func (c *Censys) newRequest(page int, query string) (*http.Request, error) { 105 | req, err := http.NewRequest("GET", "https://search.censys.io/api/v2/hosts/search?per_page=100&virtual_hosts=EXCLUDE&sort=RELEVANCE", strings.NewReader("")) 106 | req.Header.Set("Accept", "application/json") 107 | req.Header.Set("Authorization", "Basic "+c.Key) 108 | q := req.URL.Query() 109 | q.Add("q", query) 110 | q.Add("page", strconv.Itoa(page)) 111 | req.URL.RawQuery = q.Encode() 112 | return req, err 113 | } 114 | -------------------------------------------------------------------------------- /cmd/config.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "strings" 7 | //"github.com/ShadowObj/yescloudflare/log" 8 | ) 9 | 10 | const ( 11 | defaultKeyLen = 92 12 | defaultPageStart = 1 13 | defaultPageEnd = 10 14 | defaultOutput = "ip.txt" 15 | ) 16 | 17 | type Config struct { 18 | Key string 19 | Port PortList 20 | ASN ASNList 21 | Region RegionList 22 | query string 23 | Output string 24 | Config string 25 | Auto bool 26 | Norepeat bool 27 | Page PageRange 28 | 29 | logger *log.Logger 30 | } 31 | 32 | // Check checks whether the config is vaild and generate the query 33 | func (c *Config) Check() { 34 | c.logger = log.Default() 35 | if data, err := os.ReadFile(c.Config); err == nil { 36 | tc := &tomlConfig{} 37 | if err := tc.Parse(string(data)); err != nil { 38 | c.logger.Fatalf("配置文件 %s 格式不正确.", c.Config) 39 | } 40 | if err = tc.Pairing(c); err != nil { 41 | c.logger.Fatalf("配置文件 %s 格式不正确: %s", c.Config, err) 42 | } 43 | c.logger.Printf("使用配置文件 %s 中的配置.", c.Config) 44 | } 45 | if c.Key == "" || len(c.Key) != defaultKeyLen { 46 | c.logger.Fatalf("Correct APIKEY is Required! (-key APIKEY)") 47 | } 48 | c.logger.Printf("APIKEY: %s****%s\n", c.Key[:2], c.Key[len(c.Key)-2:]) 49 | c.query = "NOT autonomous_system.asn={13335,209242} and services.software.vendor='CloudFlare'" 50 | if len(c.Port.intS) > 0 { 51 | c.query += " and services.port={" + strings.Join(c.Port.strS, ",") + "}" 52 | } else { 53 | c.logger.Printf("未指定端口,不会有端口被过滤.\n") 54 | } 55 | if len(c.ASN) > 0 { 56 | c.query += " and autonomous_system.asn={" + strings.Join([]string(c.ASN), ",") + "}" 57 | } 58 | if len(c.Region) > 0 { 59 | c.query += " and location.country_code={" + strings.Join([]string(c.Region), ",") + "}" 60 | } 61 | if c.Page.Start == 0 || c.Page.End == 0 { 62 | c.Page.Start, c.Page.End = defaultPageStart, defaultPageEnd 63 | } 64 | if c.Output == "" { 65 | c.Output = defaultOutput 66 | } 67 | } 68 | 69 | func (c *Config) GetQuery() string { 70 | return c.query 71 | } 72 | 73 | func (c *Config) GetLogger() *log.Logger { 74 | return c.logger 75 | } 76 | -------------------------------------------------------------------------------- /cmd/subtype.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | const ( 9 | defaultItemSep = "," 10 | defaultRangeSep = "-" 11 | ) 12 | 13 | type PortList struct { 14 | intS []int 15 | strS []string 16 | } 17 | 18 | func (l *PortList) String() string { 19 | return strings.Join(l.strS, defaultItemSep) 20 | } 21 | 22 | func (l *PortList) Set(c string) error { 23 | for _, v := range strings.Split(c, defaultItemSep) { 24 | p, err := strconv.Atoi(v) 25 | if err == nil && p > 0 && p < 65535 { 26 | l.intS = append(l.intS, p) 27 | l.strS = append(l.strS, v) 28 | } 29 | } 30 | return nil 31 | } 32 | 33 | func (l *PortList) Contains(num int) bool { 34 | for _, v := range l.intS { 35 | if v == num { 36 | return true 37 | } 38 | } 39 | return false 40 | } 41 | 42 | type ASNList []string 43 | 44 | func (l *ASNList) String() string { 45 | return strings.Join(*l, defaultItemSep) 46 | } 47 | 48 | func (l *ASNList) Set(c string) error { 49 | for _, v := range strings.Split(c, defaultItemSep) { 50 | p, err := strconv.Atoi(v) 51 | if err == nil && p > 0 { 52 | *l = append(*l, v) 53 | } 54 | } 55 | return nil 56 | } 57 | 58 | type RegionList []string 59 | 60 | func (l *RegionList) String() string { 61 | return strings.Join(*l, defaultItemSep) 62 | } 63 | 64 | func (l *RegionList) Set(c string) error { 65 | for _, v := range strings.Split(c, defaultItemSep) { 66 | if len(v) == 2 { 67 | *l = append(*l, v) 68 | } 69 | } 70 | return nil 71 | } 72 | 73 | type PageRange struct { 74 | Start int 75 | End int 76 | } 77 | 78 | func (r *PageRange) String() string { 79 | return strconv.Itoa(r.Start) + defaultRangeSep + strconv.Itoa(r.End) 80 | } 81 | 82 | func (r *PageRange) Set(c string) error { 83 | strL := strings.Split(c, defaultRangeSep) 84 | if len(strL) > 1 { 85 | var err error 86 | if r.Start, err = strconv.Atoi(strL[0]); err != nil { 87 | r.Start, r.End = 0, 0 88 | } 89 | if r.End, err = strconv.Atoi(strL[1]); err != nil { 90 | r.Start, r.End = 0, 0 91 | } 92 | if r.Start > r.End || r.Start < 1 || r.End > 100 { 93 | r.Start, r.End = 0, 0 94 | } 95 | } 96 | return nil 97 | } 98 | -------------------------------------------------------------------------------- /cmd/toml.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/BurntSushi/toml" 7 | ) 8 | 9 | var once sync.Once 10 | 11 | type tomlConfig struct { 12 | Config 13 | Port string 14 | ASN string 15 | Region string 16 | Page string 17 | } 18 | 19 | func (tc *tomlConfig) Parse(data string) error { 20 | var err error 21 | once.Do(func() { _, err = toml.Decode(data, &tc) }) 22 | if err != nil { 23 | return err 24 | } 25 | return nil 26 | } 27 | 28 | func (tc *tomlConfig) Pairing(conf *Config) error { 29 | conf.Key = tc.Key 30 | conf.Output = tc.Output 31 | conf.Auto = tc.Auto 32 | conf.Norepeat = tc.Norepeat 33 | conf.Port.Set(tc.Port) 34 | conf.ASN.Set(tc.ASN) 35 | conf.Region.Set(tc.Region) 36 | conf.Page.Set(tc.Page) 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /config.toml: -------------------------------------------------------------------------------- 1 | key = "" #输入你的APIKEY 2 | port = "" #指定端口 (默认全部,可用英文逗号分隔) 3 | asn = "" #指定ASN (默认全部,可用英文逗号分隔) 4 | region = "" #指定地区ISO3166二字码 (默认全部,可用英文逗号分隔) 5 | output = "" #指定输出文件 (默认ip.txt) 6 | page = "" #指定需要查询的页面 (默认1-10,范围应为第1到100页) 7 | 8 | # 以下选项为开关项,true为开启,false为关闭,不要加双引号 9 | norepeat = false # 自动去除重复IP (默认不去除) 10 | auto = false # 自动获取下一页内容 (默认需要确认) -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ShadowObj/yescloudflare 2 | 3 | go 1.23.0 4 | 5 | require github.com/tidwall/gjson v1.17.3 // direct 6 | 7 | require github.com/BurntSushi/toml v1.4.0 8 | 9 | require ( 10 | github.com/tidwall/match v1.1.1 // indirect 11 | github.com/tidwall/pretty v1.2.1 // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= 2 | github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 3 | github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94= 4 | github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 5 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 6 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 7 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 8 | github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= 9 | github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 10 | -------------------------------------------------------------------------------- /log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | ) 7 | 8 | type Logger struct { 9 | logger *log.Logger 10 | logChan chan<- string 11 | } 12 | 13 | func Default(ch chan<- string) *Logger { 14 | return &Logger{ 15 | logger: log.Default(), 16 | logChan: ch, 17 | } 18 | } 19 | 20 | func (l *Logger) Fatalf(format string, v ...any) { 21 | f := fmt.Sprintf(format, v...) 22 | l.logChan <- f 23 | l.logger.Fatal(f) 24 | } 25 | 26 | func (l *Logger) Printf(format string, v ...any) { 27 | f := fmt.Sprintf(format, v...) 28 | l.logChan <- f 29 | l.logger.Print(f) 30 | } 31 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/ShadowObj/yescloudflare/client" 9 | "github.com/ShadowObj/yescloudflare/cmd" 10 | ) 11 | 12 | const ( 13 | show_welcome = `# ShadowObj/yescloudflare 14 | 查询Cloudflare反代节点小工具 15 | 程序原型由 Joey Huang 开发 16 | Telegram反馈群: https://t.me/+ft-zI76oovgwNmRh 17 | ` 18 | version = "1.1.0" 19 | help = ` 20 | Usage of YesCloudflare: 21 | -c/-config string 22 | 指定配置文件 (默认config.toml) 23 | 注意: 指定配置文件不会覆盖命令行参数。 24 | -o/-output ip.txt 25 | 指定输出文件 (默认ip.txt) 26 | -A/-auto 27 | 自动获取下一页内容 (默认需要确认) 28 | -key apikey 29 | 指定APIKEY 30 | -norepeat 31 | 自动去除重复IP (默认不去除) 32 | -port port 33 | 指定端口 (默认全部, 可用英文逗号分隔) 34 | -asn asn1,asn2 35 | 指定ASN (默认全部, 可用英文逗号分隔) 36 | -region CN,HK,JP,KR,TW 37 | 指定地区ISO3166二字码 38 | (默认全部, 可用英文逗号分隔) 39 | -page 1-10 40 | 指定需要查询的页面 41 | (默认1-10, 范围应为第1到100页) 42 | ` 43 | ) 44 | 45 | func parseConf(conf *cmd.Config) { 46 | flag.StringVar(&conf.Config, "c", "ip.txt", "") 47 | flag.StringVar(&conf.Config, "config", "config.toml", "") 48 | flag.StringVar(&conf.Output, "o", "ip.txt", "") 49 | flag.StringVar(&conf.Output, "output", "ip.txt", "") 50 | flag.StringVar(&conf.Key, "key", "", "") 51 | flag.BoolVar(&conf.Auto, "A", false, "") 52 | flag.BoolVar(&conf.Auto, "auto", false, "") 53 | flag.BoolVar(&conf.Norepeat, "norepeat", false, "") 54 | flag.Var(&conf.Port, "port", "") 55 | flag.Var(&conf.ASN, "asn", "") 56 | flag.Var(&conf.Region, "region", "") 57 | flag.Var(&conf.Page, "page", "") 58 | flag.Usage = func() { fmt.Print(help) } 59 | flag.Parse() 60 | } 61 | 62 | func main() { 63 | fmt.Print(show_welcome) 64 | fmt.Print("版本 VERSION: ", version, "\n") 65 | conf := &cmd.Config{} 66 | parseConf(conf) 67 | conf.Check() 68 | cli := &client.Censys{Key: conf.Key, Client: &http.Client{}} 69 | cli.Exec(conf) 70 | } 71 | --------------------------------------------------------------------------------