Merge pull request 'v1.1.0' (#2) from develop into main

Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
2026-03-18 21:09:36 +05:00
14 changed files with 492 additions and 38 deletions

View File

@@ -89,29 +89,12 @@ func NewConfigZip(c Config) ConfigZip {
// 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.
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)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fileUrl, nil)
res, err := fetch(fileUrl, ctx, c.RequestTimeout)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
client := &http.Client{
Timeout: c.RequestTimeout,
}
res, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
return nil, err
}
defer func() {
_ = res.Body.Close()
@@ -124,32 +107,36 @@ func Get(fileUrl string, parser parser.Parser, c Config) (parser.IPs, error) {
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) {
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.Config.ContextTimeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fileUrl, nil)
res, err := fetch(fileUrl, ctx, c.Config.RequestTimeout)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
client := &http.Client{
Timeout: c.Config.RequestTimeout,
}
res, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
return nil, err
}
defer func() {
_ = res.Body.Close()
@@ -184,6 +171,49 @@ func GetZip(fileUrl string, parser parser.Parser, c ConfigZip) (parser.IPs, erro
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' &&
@@ -223,6 +253,37 @@ func parseZip(body []byte, p parser.Parser, c ConfigZip) (parser.IPs, error) {
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
@@ -246,3 +307,28 @@ func findArchiveFile(files []*zip.File) *zip.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
}

View File

@@ -23,12 +23,24 @@ func main() {
// 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
@@ -41,10 +53,22 @@ func main() {
// 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 := 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 := 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,12 +21,24 @@ func main() {
// 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)
/*
// You can also get a range of IP addresses from this service (from to)
url := "https://www.dshield.org/block.txt"
@@ -38,10 +50,22 @@ func main() {
// limit 0 - no limit
limit := uint(0)
config := blocklist.NewConfigWithValidator(limit, &parser.IPRangeValidator{})
// 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 := 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 := 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

@@ -32,9 +32,21 @@ func main() {
// 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

@@ -23,9 +23,21 @@ func main() {
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 := 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

@@ -63,3 +63,50 @@ func (p *jsonLinesParser) Parse(body io.Reader, validator IPValidator, limit uin
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 {
// Parse reads the body and returns a slice of IP addresses.
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.
type IPValidator interface {
// IsValid checks if the given IP address is valid.
IsValid(ip string) bool
IsValidAndReturnVersion(ip string) (bool, IPVersion)
}
type IPVersion int
const (
IPVersion4 IPVersion = iota
IPVersion6
)
// IPs is a slice of IP addresses.
type IPs []string
@@ -50,6 +61,48 @@ func (v *DefaultIPValidator) IsValid(value string) bool {
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.
// 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{}
@@ -89,3 +142,43 @@ func (v *IPRangeValidator) IsValid(value string) bool {
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
}
// 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
}
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
}