15 Commits

Author SHA1 Message Date
7331ed2b2b Merge pull request 'v1.1.0' (#2) from develop into main
Reviewed-on: #2
2026-03-18 21:09:36 +05:00
a8364f98ed Add examples for fetching and displaying separated IPv4 and IPv6 addresses from blocklist services 2026-03-18 21:05:30 +05:00
a2dff48936 Refactor request logic with reusable fetch function and add methods to handle separated IP parsing for both regular and ZIP responses. 2026-03-18 21:05:11 +05:00
e09411f871 Add method to parse IPs by version (IPv4/IPv6) with validation and limit support 2026-03-18 21:04:33 +05:00
1b16ae243f Add support for parsing IPs by version (IPv4 and IPv6) with validation logic 2026-03-18 21:03:50 +05:00
70fb7a3aa8 Merge pull request 'v1.0.0' (#1) from develop into main
Reviewed-on: #1
2026-03-15 20:35:59 +05:00
cfbf4ce504 Add StopForumSpam example link to README 2026-03-15 20:28:36 +05:00
17115d97c6 Add StopForumSpam example for fetching and parsing IP blocklist data 2026-03-15 20:27:58 +05:00
1dc7a8b22c Add ZIP archive handling support for fetching and parsing blocklist data 2026-03-15 20:27:31 +05:00
e4fc07871b Add GreenSnow example link to README 2026-03-15 19:06:26 +05:00
faedb345df Add Greensnow example for fetching and parsing IP blocklist data 2026-03-15 19:05:16 +05:00
ee0e36b40d Add Blocklist.de example link to README 2026-03-15 18:59:25 +05:00
790073aa95 Add Blocklist.de example for fetching and parsing IP blocklist data 2026-03-15 18:58:19 +05:00
e18859e78f Add a link to Daniel Gerzo's BruteforceBlocker list usage example to your README file. 2026-03-15 18:18:05 +05:00
aeb855c0b8 Add BruteforceBlocker example for fetching and parsing IP blocklist data 2026-03-15 18:07:40 +05:00
15 changed files with 768 additions and 19 deletions

View File

@@ -13,6 +13,10 @@ go get git.kor-elf.net/kor-elf-shield/blocklist
- [Пример получения списка IP адресов от HONEYPOT](/examples/honeypot.go) - [Пример получения списка IP адресов от HONEYPOT](/examples/honeypot.go)
- [Пример получения списка IP адресов от Tor](/examples/tor.go) - [Пример получения списка IP адресов от Tor](/examples/tor.go)
- [Пример получения списка IP адресов от CIARMY](/examples/ciarmy.go) - [Пример получения списка IP адресов от CIARMY](/examples/ciarmy.go)
- [Пример получения списка IP адресов от Daniel Gerzo (BruteforceBlocker)](/examples/bruteforceblocker.go)
- [Пример получения списка IP адресов от Blocklist.de](/examples/blocklist.go)
- [Пример получения списка IP адресов от GreenSnow](/examples/greensnow.go)
- [Пример получения списка IP адресов от StopForumSpam](/examples/stopforumspam.go)
## Лицензия ## Лицензия

View File

@@ -1,10 +1,14 @@
package blocklist package blocklist
import ( import (
"archive/zip"
"bytes"
"context" "context"
"fmt" "fmt"
"io"
"net/http" "net/http"
"net/url" "net/url"
"strings"
"time" "time"
"git.kor-elf.net/kor-elf-shield/blocklist/parser" "git.kor-elf.net/kor-elf-shield/blocklist/parser"
@@ -16,6 +20,12 @@ const (
// requestTimeout defines the maximum duration for request operations before timing out. // requestTimeout defines the maximum duration for request operations before timing out.
requestTimeout = 20 * time.Second requestTimeout = 20 * time.Second
// maxDownloadSize defines the maximum allowed size of the downloaded file in bytes.
maxDownloadSize int64 = 20 << 20 // 20 MiB
// maxArchiveFileSize defines the maximum allowed size of the extracted file from ZIP in bytes.
maxArchiveFileSize uint64 = 50 << 20 // 50 MiB
) )
// Config defines the configuration for the blocklist. // Config defines the configuration for the blocklist.
@@ -34,6 +44,17 @@ type Config struct {
RequestTimeout time.Duration RequestTimeout time.Duration
} }
type ConfigZip struct {
// Config is the configuration for the blocklist.
Config Config
// MaxDownloadSize defines the maximum allowed size of the downloaded file in bytes.
MaxDownloadSize int64
// MaxArchiveFileSize defines the maximum allowed size of the extracted file from ZIP in bytes.
MaxArchiveFileSize uint64
}
// NewConfig creates a new Config with default values. // NewConfig creates a new Config with default values.
// limit is the maximum number of items to process or validate. 0 means no limit. // limit is the maximum number of items to process or validate. 0 means no limit.
func NewConfig(limit uint) Config { func NewConfig(limit uint) Config {
@@ -57,32 +78,23 @@ func NewConfigWithValidator(limit uint, validator parser.IPValidator) Config {
} }
} }
func NewConfigZip(c Config) ConfigZip {
return ConfigZip{
Config: c,
MaxDownloadSize: maxDownloadSize,
MaxArchiveFileSize: maxArchiveFileSize,
}
}
// Get fetches data from the given URL, parses the response using the provided parser, and applies the given configuration. // Get fetches data from the given URL, parses the response using the provided parser, and applies the given configuration.
// It returns the parsed IPs and any errors that occurred during the process. // It returns the parsed IPs and any errors that occurred during the process.
func Get(fileUrl string, parser parser.Parser, c Config) (parser.IPs, error) { func Get(fileUrl string, parser parser.Parser, c Config) (parser.IPs, error) {
parsedURL, err := url.Parse(fileUrl)
if err != nil {
return nil, fmt.Errorf("invalid url: %w", err)
}
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
return nil, fmt.Errorf("invalid url scheme: %s", parsedURL.Scheme)
}
ctx, cancel := context.WithTimeout(context.Background(), c.ContextTimeout) ctx, cancel := context.WithTimeout(context.Background(), c.ContextTimeout)
defer cancel() defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fileUrl, nil) res, err := fetch(fileUrl, ctx, c.RequestTimeout)
if err != nil { if err != nil {
return nil, fmt.Errorf("create request: %w", err) return nil, err
}
client := &http.Client{
Timeout: c.RequestTimeout,
}
res, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
} }
defer func() { defer func() {
_ = res.Body.Close() _ = res.Body.Close()
@@ -94,3 +106,229 @@ func Get(fileUrl string, parser parser.Parser, c Config) (parser.IPs, error) {
return parser.Parse(res.Body, c.Validator, c.Limit) return parser.Parse(res.Body, c.Validator, c.Limit)
} }
// GetSeparatedIPs fetches data from the given URL, parses the response using the provided parser, and applies the given configuration.
// It returns the parsed IPs and any errors that occurred during the process.
func GetSeparatedIPs(fileUrl string, parser parser.Parser, c Config) (ipV4 parser.IPs, ipV6 parser.IPs, err error) {
ctx, cancel := context.WithTimeout(context.Background(), c.ContextTimeout)
defer cancel()
res, err := fetch(fileUrl, ctx, c.RequestTimeout)
if err != nil {
return nil, nil, err
}
defer func() {
_ = res.Body.Close()
}()
if res.StatusCode != http.StatusOK {
return nil, nil, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
return parser.ParseIPsByVersion(res.Body, c.Validator, c.Limit)
}
// GetZip fetches data from the given URL, parses the response using the provided parser, and applies the given configuration.
// It returns the parsed IPs and any errors that occurred during the process.
func GetZip(fileUrl string, parser parser.Parser, c ConfigZip) (parser.IPs, error) {
ctx, cancel := context.WithTimeout(context.Background(), c.Config.ContextTimeout)
defer cancel()
res, err := fetch(fileUrl, ctx, c.Config.RequestTimeout)
if err != nil {
return nil, err
}
defer func() {
_ = res.Body.Close()
}()
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
if c.MaxDownloadSize > 0 && res.ContentLength > c.MaxDownloadSize {
return nil, fmt.Errorf("downloaded file is too large: content-length %d exceeds limit %d", res.ContentLength, c.MaxDownloadSize)
}
reader := res.Body
if c.MaxDownloadSize > 0 {
reader = io.NopCloser(io.LimitReader(res.Body, c.MaxDownloadSize+1))
}
body, err := io.ReadAll(reader)
if err != nil {
return nil, fmt.Errorf("read response body: %w", err)
}
if c.MaxDownloadSize > 0 && int64(len(body)) > c.MaxDownloadSize {
return nil, fmt.Errorf("downloaded file exceeds limit %d bytes", c.MaxDownloadSize)
}
if !isZip(body) {
return nil, fmt.Errorf("invalid zip archive")
}
return parseZip(body, parser, c)
}
// GetZipSeparatedIPs fetches data from the given URL, parses the response using the provided parser, and applies the given configuration.
// It returns the parsed IPs and any errors that occurred during the process.
func GetZipSeparatedIPs(fileUrl string, parser parser.Parser, c ConfigZip) (ipV4 parser.IPs, ipV6 parser.IPs, err error) {
ctx, cancel := context.WithTimeout(context.Background(), c.Config.ContextTimeout)
defer cancel()
res, err := fetch(fileUrl, ctx, c.Config.RequestTimeout)
if err != nil {
return nil, nil, err
}
defer func() {
_ = res.Body.Close()
}()
if res.StatusCode != http.StatusOK {
return nil, nil, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
if c.MaxDownloadSize > 0 && res.ContentLength > c.MaxDownloadSize {
return nil, nil, fmt.Errorf("downloaded file is too large: content-length %d exceeds limit %d", res.ContentLength, c.MaxDownloadSize)
}
reader := res.Body
if c.MaxDownloadSize > 0 {
reader = io.NopCloser(io.LimitReader(res.Body, c.MaxDownloadSize+1))
}
body, err := io.ReadAll(reader)
if err != nil {
return nil, nil, fmt.Errorf("read response body: %w", err)
}
if c.MaxDownloadSize > 0 && int64(len(body)) > c.MaxDownloadSize {
return nil, nil, fmt.Errorf("downloaded file exceeds limit %d bytes", c.MaxDownloadSize)
}
if !isZip(body) {
return nil, nil, fmt.Errorf("invalid zip archive")
}
return parseZipSeparatedIPs(body, parser, c)
}
func isZip(body []byte) bool {
return len(body) >= 4 &&
body[0] == 'P' &&
body[1] == 'K' &&
body[2] == 0x03 &&
body[3] == 0x04
}
func parseZip(body []byte, p parser.Parser, c ConfigZip) (parser.IPs, error) {
reader, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
if err != nil {
return nil, fmt.Errorf("open zip archive: %w", err)
}
file := findArchiveFile(reader.File)
if file == nil {
return nil, fmt.Errorf("zip archive does not contain a supported file")
}
if c.MaxArchiveFileSize > 0 && file.UncompressedSize64 > c.MaxArchiveFileSize {
return nil, fmt.Errorf("file %q in zip is too large: %d exceeds limit %d", file.Name, file.UncompressedSize64, c.MaxArchiveFileSize)
}
rc, err := file.Open()
if err != nil {
return nil, fmt.Errorf("open file %q from zip: %w", file.Name, err)
}
defer func() {
_ = rc.Close()
}()
var zipReader io.Reader = rc
if c.MaxArchiveFileSize > 0 {
zipReader = io.LimitReader(rc, int64(c.MaxArchiveFileSize)+1)
}
return p.Parse(zipReader, c.Config.Validator, c.Config.Limit)
}
func parseZipSeparatedIPs(body []byte, p parser.Parser, c ConfigZip) (ipV4 parser.IPs, ipV6 parser.IPs, err error) {
reader, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
if err != nil {
return nil, nil, fmt.Errorf("open zip archive: %w", err)
}
file := findArchiveFile(reader.File)
if file == nil {
return nil, nil, fmt.Errorf("zip archive does not contain a supported file")
}
if c.MaxArchiveFileSize > 0 && file.UncompressedSize64 > c.MaxArchiveFileSize {
return nil, nil, fmt.Errorf("file %q in zip is too large: %d exceeds limit %d", file.Name, file.UncompressedSize64, c.MaxArchiveFileSize)
}
rc, err := file.Open()
if err != nil {
return nil, nil, fmt.Errorf("open file %q from zip: %w", file.Name, err)
}
defer func() {
_ = rc.Close()
}()
var zipReader io.Reader = rc
if c.MaxArchiveFileSize > 0 {
zipReader = io.LimitReader(rc, int64(c.MaxArchiveFileSize)+1)
}
return p.ParseIPsByVersion(zipReader, c.Config.Validator, c.Config.Limit)
}
func findArchiveFile(files []*zip.File) *zip.File {
var fallback *zip.File
for _, file := range files {
if file.FileInfo().IsDir() {
continue
}
if fallback == nil {
fallback = file
}
name := strings.ToLower(file.Name)
if strings.HasSuffix(name, ".txt") ||
strings.HasSuffix(name, ".json") ||
strings.HasSuffix(name, ".xml") ||
strings.HasSuffix(name, ".rss") {
return file
}
}
return fallback
}
func fetch(fileUrl string, ctx context.Context, requestTimeout time.Duration) (*http.Response, error) {
parsedURL, err := url.Parse(fileUrl)
if err != nil {
return nil, fmt.Errorf("invalid url: %w", err)
}
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
return nil, fmt.Errorf("invalid url scheme: %s", parsedURL.Scheme)
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fileUrl, nil)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
client := &http.Client{
Timeout: requestTimeout,
}
res, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
return res, nil
}

74
examples/blocklist.go Normal file
View File

@@ -0,0 +1,74 @@
package main
import (
"fmt"
"git.kor-elf.net/kor-elf-shield/blocklist"
"git.kor-elf.net/kor-elf-shield/blocklist/parser"
)
/**
* An example of how to get a list of IP addresses from a service https://www.blocklist.de/en/export.html
*/
func main() {
// Getting a list of IP addresses that were entered in the last hour
// time=seconds
url := "https://api.blocklist.de/getlast.php?time=3600"
extract := parser.NewDefaultTextExtract(0, "\t")
pars, err := parser.NewText(extract)
if err != nil {
panic(err)
}
// limit 0 - no limit
limit := uint(0)
config := blocklist.NewConfig(limit)
// Get IPv4 and IPv6 addresses in one list
ips, err := blocklist.Get(url, pars, config)
if err != nil {
panic(err)
}
fmt.Println(ips)
// Get IPv4 and IPv6 addresses in two lists
ipsV4, ipsV6, err := blocklist.GetSeparatedIPs(url, pars, config)
if err != nil {
panic(err)
}
fmt.Println("IPv4")
fmt.Println(ipsV4)
fmt.Println("IPv6")
fmt.Println(ipsV6)
/*
// This second list retrieves all the IP addresses added in the last 48 hours and is usually a
// very large list (over 10000 entries), so be sure that you have the resources available to use it
url := "http://lists.blocklist.de/lists/all.txt"
extract := parser.NewDefaultTextExtract(0, "\t")
pars, err := parser.NewText(extract)
if err != nil {
panic(err)
}
// limit 0 - no limit
limit := uint(0)
config := blocklist.NewConfig(limit)
// Get IPv4 and IPv6 addresses in one list
ips, err := blocklist.Get(url, pars, config)
if err != nil {
panic(err)
}
fmt.Println(ips)
// Get IPv4 and IPv6 addresses in two lists
ipsV4, ipsV6, err := blocklist.GetSeparatedIPs(url, pars, config)
if err != nil {
panic(err)
}
fmt.Println("IPv4")
fmt.Println(ipsV4)
fmt.Println("IPv6")
fmt.Println(ipsV6)
*/
}

View File

@@ -0,0 +1,41 @@
package main
import (
"fmt"
"git.kor-elf.net/kor-elf-shield/blocklist"
"git.kor-elf.net/kor-elf-shield/blocklist/parser"
)
/**
* An example of how to get a list of IP addresses from a service https://danger.rulez.sk/index.php/bruteforceblocker/
*/
func main() {
url := "https://danger.rulez.sk/projects/bruteforceblocker/blist.php"
extract := parser.NewDefaultTextExtract(0, "\t")
pars, err := parser.NewText(extract)
if err != nil {
panic(err)
}
// limit 0 - no limit
limit := uint(0)
config := blocklist.NewConfig(limit)
// Get IPv4 and IPv6 addresses in one list
ips, err := blocklist.Get(url, pars, config)
if err != nil {
panic(err)
}
fmt.Println(ips)
// Get IPv4 and IPv6 addresses in two lists
ipsV4, ipsV6, err := blocklist.GetSeparatedIPs(url, pars, config)
if err != nil {
panic(err)
}
fmt.Println("IPv4")
fmt.Println(ipsV4)
fmt.Println("IPv6")
fmt.Println(ipsV6)
}

View File

@@ -21,9 +21,21 @@ func main() {
// limit 0 - no limit // limit 0 - no limit
limit := uint(0) limit := uint(0)
config := blocklist.NewConfig(limit) config := blocklist.NewConfig(limit)
// Get IPv4 and IPv6 addresses in one list
ips, err := blocklist.Get(url, pars, config) ips, err := blocklist.Get(url, pars, config)
if err != nil { if err != nil {
panic(err) panic(err)
} }
fmt.Println(ips) fmt.Println(ips)
// Get IPv4 and IPv6 addresses in two lists
ipsV4, ipsV6, err := blocklist.GetSeparatedIPs(url, pars, config)
if err != nil {
panic(err)
}
fmt.Println("IPv4")
fmt.Println(ipsV4)
fmt.Println("IPv6")
fmt.Println(ipsV6)
} }

View File

@@ -21,12 +21,24 @@ func main() {
// limit 0 - no limit // limit 0 - no limit
limit := uint(0) limit := uint(0)
config := blocklist.NewConfig(limit) config := blocklist.NewConfig(limit)
// Get IPv4 and IPv6 addresses in one list
ips, err := blocklist.Get(url, pars, config) ips, err := blocklist.Get(url, pars, config)
if err != nil { if err != nil {
panic(err) panic(err)
} }
fmt.Println(ips) fmt.Println(ips)
// Get IPv4 and IPv6 addresses in two lists
ipsV4, ipsV6, err := blocklist.GetSeparatedIPs(url, pars, config)
if err != nil {
panic(err)
}
fmt.Println("IPv4")
fmt.Println(ipsV4)
fmt.Println("IPv6")
fmt.Println(ipsV6)
/* /*
// You can also get a range of IP addresses from this service (from to) // You can also get a range of IP addresses from this service (from to)
url := "https://www.dshield.org/block.txt" url := "https://www.dshield.org/block.txt"
@@ -38,10 +50,22 @@ func main() {
// limit 0 - no limit // limit 0 - no limit
limit := uint(0) limit := uint(0)
config := blocklist.NewConfigWithValidator(limit, &parser.IPRangeValidator{}) config := blocklist.NewConfigWithValidator(limit, &parser.IPRangeValidator{})
// Get IPv4 and IPv6 addresses in one list
ips, err := blocklist.Get(url, pars, config) ips, err := blocklist.Get(url, pars, config)
if err != nil { if err != nil {
panic(err) panic(err)
} }
fmt.Println(ips) fmt.Println(ips)
// Get IPv4 and IPv6 addresses in two lists
ipsV4, ipsV6, err := blocklist.GetSeparatedIPs(url, pars, config)
if err != nil {
panic(err)
}
fmt.Println("IPv4")
fmt.Println(ipsV4)
fmt.Println("IPv6")
fmt.Println(ipsV6)
*/ */
} }

41
examples/greensnow.go Normal file
View File

@@ -0,0 +1,41 @@
package main
import (
"fmt"
"git.kor-elf.net/kor-elf-shield/blocklist"
"git.kor-elf.net/kor-elf-shield/blocklist/parser"
)
/**
* An example of how to get a list of IP addresses from a service https://greensnow.co/
*/
func main() {
url := "https://blocklist.greensnow.co/greensnow.txt"
extract := parser.NewDefaultTextExtract(0, " ")
pars, err := parser.NewText(extract)
if err != nil {
panic(err)
}
// limit 0 - no limit
limit := uint(0)
config := blocklist.NewConfig(limit)
// Get IPv4 and IPv6 addresses in one list
ips, err := blocklist.Get(url, pars, config)
if err != nil {
panic(err)
}
fmt.Println(ips)
// Get IPv4 and IPv6 addresses in two lists
ipsV4, ipsV6, err := blocklist.GetSeparatedIPs(url, pars, config)
if err != nil {
panic(err)
}
fmt.Println("IPv4")
fmt.Println(ipsV4)
fmt.Println("IPv6")
fmt.Println(ipsV6)
}

View File

@@ -45,9 +45,21 @@ func main() {
// limit 0 - no limit // limit 0 - no limit
limit := uint(0) limit := uint(0)
config := blocklist.NewConfig(limit) config := blocklist.NewConfig(limit)
// Get IPv4 and IPv6 addresses in one list
ips, err := blocklist.Get(url, pars, config) ips, err := blocklist.Get(url, pars, config)
if err != nil { if err != nil {
panic(err) panic(err)
} }
fmt.Println(ips) fmt.Println(ips)
// Get IPv4 and IPv6 addresses in two lists
ipsV4, ipsV6, err := blocklist.GetSeparatedIPs(url, pars, config)
if err != nil {
panic(err)
}
fmt.Println("IPv4")
fmt.Println(ipsV4)
fmt.Println("IPv6")
fmt.Println(ipsV6)
} }

View File

@@ -32,9 +32,21 @@ func main() {
// limit 0 - no limit // limit 0 - no limit
limit := uint(0) limit := uint(0)
config := blocklist.NewConfig(limit) config := blocklist.NewConfig(limit)
// Get IPv4 and IPv6 addresses in one list
ips, err := blocklist.Get(url, pars, config) ips, err := blocklist.Get(url, pars, config)
if err != nil { if err != nil {
panic(err) panic(err)
} }
fmt.Println(ips) fmt.Println(ips)
// Get IPv4 and IPv6 addresses in two lists
ipsV4, ipsV6, err := blocklist.GetSeparatedIPs(url, pars, config)
if err != nil {
panic(err)
}
fmt.Println("IPv4")
fmt.Println(ipsV4)
fmt.Println("IPv6")
fmt.Println(ipsV6)
} }

43
examples/stopforumspam.go Normal file
View File

@@ -0,0 +1,43 @@
package main
import (
"fmt"
"git.kor-elf.net/kor-elf-shield/blocklist"
"git.kor-elf.net/kor-elf-shield/blocklist/parser"
)
/**
* An example of how to get a list of IP addresses from a service https://www.stopforumspam.com/downloads
*/
func main() {
url := "https://www.stopforumspam.com/downloads/listed_ip_1.zip"
//url := "https://www.stopforumspam.com/downloads/listed_ip_1_ipv6.zip"
extract := parser.NewDefaultTextExtract(0, " ")
pars, err := parser.NewText(extract)
if err != nil {
panic(err)
}
// limit 0 - no limit
limit := uint(0)
config := blocklist.NewConfig(limit)
configZip := blocklist.NewConfigZip(config)
// Get IPv4 and IPv6 addresses in one list
ips, err := blocklist.GetZip(url, pars, configZip)
if err != nil {
panic(err)
}
fmt.Println(ips)
// Get IPv4 and IPv6 addresses in two lists
ipsV4, ipsV6, err := blocklist.GetZipSeparatedIPs(url, pars, configZip)
if err != nil {
panic(err)
}
fmt.Println("IPv4")
fmt.Println(ipsV4)
fmt.Println("IPv6")
fmt.Println(ipsV6)
}

View File

@@ -21,9 +21,21 @@ func main() {
// limit 0 - no limit // limit 0 - no limit
limit := uint(0) limit := uint(0)
config := blocklist.NewConfig(limit) config := blocklist.NewConfig(limit)
// Get IPv4 and IPv6 addresses in one list
ips, err := blocklist.Get(url, pars, config) ips, err := blocklist.Get(url, pars, config)
if err != nil { if err != nil {
panic(err) panic(err)
} }
fmt.Println(ips) fmt.Println(ips)
// Get IPv4 and IPv6 addresses in two lists
ipsV4, ipsV6, err := blocklist.GetSeparatedIPs(url, pars, config)
if err != nil {
panic(err)
}
fmt.Println("IPv4")
fmt.Println(ipsV4)
fmt.Println("IPv6")
fmt.Println(ipsV6)
} }

View File

@@ -63,3 +63,50 @@ func (p *jsonLinesParser) Parse(body io.Reader, validator IPValidator, limit uin
return ips, nil return ips, nil
} }
// ParseIPsByVersion parses the JSON Lines data from the given reader
// and returns a slice of IP addresses for each IP version.
// It also returns any errors that occurred during the process.
func (p *jsonLinesParser) ParseIPsByVersion(body io.Reader, validator IPValidator, limit uint) (ipV4 IPs, ipV6 IPs, err error) {
decoder := json.NewDecoder(body)
ipV4 = make(IPs, 0)
ipV6 = make(IPs, 0)
for {
var item json.RawMessage
if err := decoder.Decode(&item); err != nil {
if err == io.EOF {
break
}
return nil, nil, fmt.Errorf("decode json item: %w", err)
}
if item == nil {
continue
}
ip, err := p.extract(item)
if err != nil {
return nil, nil, fmt.Errorf("extract ip: %w", err)
}
ip = strings.TrimSpace(ip)
isValid, ipVersion := validator.IsValidAndReturnVersion(ip)
if !isValid {
continue
}
if ipVersion == IPVersion4 {
ipV4 = append(ipV4, ip)
} else if ipVersion == IPVersion6 {
ipV6 = append(ipV6, ip)
} else {
continue
}
if limit > 0 && uint(len(ipV4))+uint(len(ipV6)) >= limit {
break
}
}
return ipV4, ipV6, nil
}

View File

@@ -11,14 +11,25 @@ import (
type Parser interface { type Parser interface {
// Parse reads the body and returns a slice of IP addresses. // Parse reads the body and returns a slice of IP addresses.
Parse(body io.Reader, validator IPValidator, limit uint) (IPs, error) Parse(body io.Reader, validator IPValidator, limit uint) (IPs, error)
ParseIPsByVersion(body io.Reader, validator IPValidator, limit uint) (ipV4 IPs, ipV6 IPs, err error)
} }
// IPValidator interface defines the contract for validating IP addresses. // IPValidator interface defines the contract for validating IP addresses.
type IPValidator interface { type IPValidator interface {
// IsValid checks if the given IP address is valid. // IsValid checks if the given IP address is valid.
IsValid(ip string) bool IsValid(ip string) bool
IsValidAndReturnVersion(ip string) (bool, IPVersion)
} }
type IPVersion int
const (
IPVersion4 IPVersion = iota
IPVersion6
)
// IPs is a slice of IP addresses. // IPs is a slice of IP addresses.
type IPs []string type IPs []string
@@ -50,6 +61,48 @@ func (v *DefaultIPValidator) IsValid(value string) bool {
return false return false
} }
// IsValidAndReturnVersion checks if the given IP address is valid and returns the IP version.
// It returns true if the IP address is not a loopback address and the IP version is either IPv4 or IPv6.
func (v *DefaultIPValidator) IsValidAndReturnVersion(value string) (bool, IPVersion) {
if value == "" {
return false, IPVersion4
}
if ip := net.ParseIP(value); ip != nil {
if ip.IsLoopback() {
return false, IPVersion4
}
if ip.To4() != nil {
return true, IPVersion4
}
if ip.To16() != nil {
return true, IPVersion6
}
return false, IPVersion4
}
if ip, _, err := net.ParseCIDR(value); err == nil {
if ip.IsLoopback() {
return false, IPVersion4
}
if ip.To4() != nil {
return true, IPVersion4
}
if ip.To16() != nil {
return true, IPVersion6
}
return false, IPVersion4
}
return false, IPVersion4
}
// IPRangeValidator implements IPValidator interface. // IPRangeValidator implements IPValidator interface.
// It validates IP ranges by parsing them using net.ParseIP and checking if the start and end IPs are in the same network. // It validates IP ranges by parsing them using net.ParseIP and checking if the start and end IPs are in the same network.
type IPRangeValidator struct{} type IPRangeValidator struct{}
@@ -89,3 +142,43 @@ func (v *IPRangeValidator) IsValid(value string) bool {
return false return false
} }
} }
func (v *IPRangeValidator) IsValidAndReturnVersion(value string) (bool, IPVersion) {
if value == "" {
return false, IPVersion4
}
parts := strings.Split(value, "-")
if len(parts) != 2 {
return false, IPVersion4
}
start := net.ParseIP(strings.TrimSpace(parts[0]))
end := net.ParseIP(strings.TrimSpace(parts[1]))
if start == nil || end == nil {
return false, IPVersion4
}
start4 := start.To4()
end4 := end.To4()
switch {
case start4 != nil && end4 != nil:
if bytes.Compare(start4, end4) <= 0 {
return true, IPVersion4
}
return false, IPVersion4
case start4 == nil && end4 == nil:
start16 := start.To16()
end16 := end.To16()
if start16 == nil || end16 == nil {
return false, IPVersion4
}
if bytes.Compare(start16, end16) <= 0 {
return true, IPVersion6
}
return false, IPVersion4
default:
return false, IPVersion4
}
}

View File

@@ -66,3 +66,53 @@ func (p *rssParser) Parse(body io.Reader, validator IPValidator, limit uint) (IP
return ips, nil return ips, nil
} }
// ParseIPsByVersion parses the RSS data from the given reader
// and returns a slice of IP addresses for each IP version.
// It also returns any errors that occurred during the process.
func (p *rssParser) ParseIPsByVersion(body io.Reader, validator IPValidator, limit uint) (ipV4 IPs, ipV6 IPs, err error) {
decoder := xml.NewDecoder(body)
ipV4 = make(IPs, 0)
ipV6 = make(IPs, 0)
for {
token, err := decoder.Token()
if err != nil {
if err == io.EOF {
break
}
return nil, nil, fmt.Errorf("parse rss: %w", err)
}
start, ok := token.(xml.StartElement)
if !ok {
continue
}
ip, err := p.extract(decoder, start)
if err != nil {
return nil, nil, fmt.Errorf("extract rss ip: %w", err)
}
ip = strings.TrimSpace(ip)
isValid, ipVersion := validator.IsValidAndReturnVersion(ip)
if !isValid {
continue
}
if ipVersion == IPVersion4 {
ipV4 = append(ipV4, ip)
} else if ipVersion == IPVersion6 {
ipV6 = append(ipV6, ip)
} else {
continue
}
if limit > 0 && uint(len(ipV4))+uint(len(ipV6)) >= limit {
break
}
}
return ipV4, ipV6, nil
}

View File

@@ -158,3 +158,49 @@ func (p *textParser) Parse(body io.Reader, validator IPValidator, limit uint) (I
return ips, nil return ips, nil
} }
func (p *textParser) ParseIPsByVersion(body io.Reader, validator IPValidator, limit uint) (ipV4 IPs, ipV6 IPs, err error) {
ipV4 = make(IPs, 0)
ipV6 = make(IPs, 0)
scanner := bufio.NewScanner(body)
buf := make([]byte, 0, 64*1024)
scanner.Buffer(buf, 1024*1024)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, ";") || strings.HasPrefix(line, "#") {
continue
}
ip, isFound := p.textExtract.Extract(line)
if !isFound {
continue
}
ip = strings.TrimSpace(ip)
isValid, ipVersion := validator.IsValidAndReturnVersion(ip)
if !isValid {
continue
}
if ipVersion == IPVersion4 {
ipV4 = append(ipV4, ip)
} else if ipVersion == IPVersion6 {
ipV6 = append(ipV6, ip)
} else {
continue
}
if limit > 0 && uint(len(ipV4))+uint(len(ipV6)) >= limit {
break
}
}
if err := scanner.Err(); err != nil {
return nil, nil, fmt.Errorf("read response: %w", err)
}
return ipV4, ipV6, nil
}