Skip to content

Commit

Permalink
Feat: support maxmindMMDB type as input format
Browse files Browse the repository at this point in the history
  • Loading branch information
Loyalsoldier committed Sep 17, 2022
1 parent bd553de commit 55929d3
Show file tree
Hide file tree
Showing 3 changed files with 258 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ These two concepts are notable: `input` and `output`. The `input` is the data so
- **private**:局域网和私有网络 CIDR(例如:`192.168.0.0/16` 和 `127.0.0.0/8`)
- **cutter**:用于裁剪前置步骤中的数据
- **v2rayGeoIPDat**:V2Ray GeoIP dat 格式(`geoip.dat`)
- **maxmindMMDB**:MaxMind mmdb 数据格式(`GeoLite2-Country.mmdb`)
- **maxmindGeoLite2CountryCSV**:MaxMind GeoLite2 country CSV 数据(`GeoLite2-Country-CSV.zip`)
- **clashRuleSetClassical**:[classical 类型的 Clash RuleSet](https://github.com/Dreamacro/clash/wiki/premium-core-features#classical)
- **clashRuleSet**:[ipcidr 类型的 Clash RuleSet](https://github.com/Dreamacro/clash/wiki/premium-core-features#ipcidr)
Expand Down Expand Up @@ -209,6 +210,7 @@ $ ./geoip -c config.json
$ ./geoip -l
All available input formats:
- v2rayGeoIPDat (Convert V2Ray GeoIP dat to other formats)
- maxmindMMDB (Convert MaxMind mmdb database to other formats)
- maxmindGeoLite2CountryCSV (Convert MaxMind GeoLite2 country CSV data to other formats)
- private (Convert LAN and private network CIDR to other formats)
- text (Convert plaintext IP & CIDR to other formats)
Expand Down
29 changes: 29 additions & 0 deletions config-example.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,35 @@
"wantedList": ["cn", "us"]
}
},
{
"type": "maxmindMMDB",
"action": "add"
},
{
"type": "maxmindMMDB",
"action": "add",
"args": {
"uri": "./path/to/your/mmdb/file",
"wantedList": ["cn", "us"],
"onlyIPType": "ipv4"
}
},
{
"type": "maxmindMMDB",
"action": "add",
"args": {
"uri": "https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb"
}
},
{
"type": "maxmindMMDB",
"action": "remove",
"args": {
"uri": "https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb",
"wantedList": ["jp"],
"onlyIPType": "ipv6"
}
},
{
"type": "cutter",
"action": "remove",
Expand Down
227 changes: 227 additions & 0 deletions plugin/maxmind/mmdb_in.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
package maxmind

import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"

"github.com/Loyalsoldier/geoip/lib"
"github.com/oschwald/maxminddb-golang"
)

const (
typeMaxmindMMDBIn = "maxmindMMDB"
descMaxmindMMDBIn = "Convert MaxMind mmdb database to other formats"
)

var (
defaultMMDBFile = filepath.Join("./", "geolite2", "GeoLite2-Country.mmdb")
tempMMDBPath = filepath.Join("./", "tmp")
tempMMDBFile = filepath.Join(tempMMDBPath, "input.mmdb")
)

func init() {
lib.RegisterInputConfigCreator(typeMaxmindMMDBIn, func(action lib.Action, data json.RawMessage) (lib.InputConverter, error) {
return newMaxmindMMDBIn(action, data)
})
lib.RegisterInputConverter(typeMaxmindMMDBIn, &maxmindMMDBIn{
Description: descMaxmindMMDBIn,
})
}

func newMaxmindMMDBIn(action lib.Action, data json.RawMessage) (lib.InputConverter, error) {
var tmp struct {
URI string `json:"uri"`
Want []string `json:"wantedList"`
OnlyIPType lib.IPType `json:"onlyIPType"`
}

if len(data) > 0 {
if err := json.Unmarshal(data, &tmp); err != nil {
return nil, err
}
}

if tmp.URI == "" {
tmp.URI = defaultMMDBFile
}

return &maxmindMMDBIn{
Type: typeMaxmindMMDBIn,
Action: action,
Description: descMaxmindMMDBIn,
URI: tmp.URI,
Want: tmp.Want,
OnlyIPType: tmp.OnlyIPType,
}, nil
}

type maxmindMMDBIn struct {
Type string
Action lib.Action
Description string
URI string
Want []string
OnlyIPType lib.IPType
}

func (g *maxmindMMDBIn) GetType() string {
return g.Type
}

func (g *maxmindMMDBIn) GetAction() lib.Action {
return g.Action
}

func (g *maxmindMMDBIn) GetDescription() string {
return g.Description
}

func (g *maxmindMMDBIn) Input(container lib.Container) (lib.Container, error) {
var fd io.ReadCloser
var err error
switch {
case strings.HasPrefix(g.URI, "http://"), strings.HasPrefix(g.URI, "https://"):
fd, err = g.downloadFile(g.URI)
default:
fd, err = os.Open(g.URI)
}

if err != nil {
return nil, err
}

err = g.moveFile(fd)
if err != nil {
return nil, err
}

entries := make(map[string]*lib.Entry)
err = g.generateEntries(entries)
if err != nil {
return nil, err
}

if len(entries) == 0 {
return nil, fmt.Errorf("❌ [type %s | action %s] no entry is newly generated", typeMaxmindMMDBIn, g.Action)
}

var ignoreIPType lib.IgnoreIPOption
switch g.OnlyIPType {
case lib.IPv4:
ignoreIPType = lib.IgnoreIPv6
case lib.IPv6:
ignoreIPType = lib.IgnoreIPv4
}

// Filter want list
wantList := make(map[string]bool)
for _, want := range g.Want {
if want = strings.ToUpper(strings.TrimSpace(want)); want != "" {
wantList[want] = true
}
}

for _, entry := range entries {
name := entry.GetName()
if len(wantList) > 0 && !wantList[name] {
continue
}

switch g.Action {
case lib.ActionAdd:
if err := container.Add(entry, ignoreIPType); err != nil {
return nil, err
}
case lib.ActionRemove:
container.Remove(name, ignoreIPType)
}
}

return container, nil
}

func (g *maxmindMMDBIn) downloadFile(url string) (io.ReadCloser, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}

if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to get remote file %s, http status code %d", url, resp.StatusCode)
}

return resp.Body, nil
}

func (g *maxmindMMDBIn) moveFile(src io.ReadCloser) error {
defer src.Close()

err := os.MkdirAll(tempMMDBPath, 0755)
if err != nil {
return err
}

out, err := os.Create(tempMMDBFile)
if err != nil {
return err
}
defer out.Close()

_, err = io.Copy(out, src)

return err
}

func (g *maxmindMMDBIn) generateEntries(entries map[string]*lib.Entry) error {
db, err := maxminddb.Open(tempMMDBFile)
if err != nil {
return err
}
defer db.Close()

record := struct {
Country struct {
IsoCode string `maxminddb:"iso_code"`
} `maxminddb:"country"`
}{}

networks := db.Networks(maxminddb.SkipAliasedNetworks)
for networks.Next() {
subnet, err := networks.Network(&record)
if err != nil {
continue
}

var entry *lib.Entry
name := strings.ToUpper(record.Country.IsoCode)
if theEntry, found := entries[name]; found {
entry = theEntry
} else {
entry = lib.NewEntry(name)
}

switch g.Action {
case lib.ActionAdd:
if err := entry.AddPrefix(subnet); err != nil {
return err
}
case lib.ActionRemove:
if err := entry.RemovePrefix(subnet.String()); err != nil {
return err
}
}

entries[name] = entry
}

if networks.Err() != nil {
return networks.Err()
}

return nil
}

0 comments on commit 55929d3

Please sign in to comment.