diff options
Diffstat (limited to 'plugin')
| -rw-r--r-- | plugin/maxmind/country_csv.go | 216 | ||||
| -rw-r--r-- | plugin/maxmind/mmdb.go | 198 | ||||
| -rw-r--r-- | plugin/plaintext/clash_in.go | 36 | ||||
| -rw-r--r-- | plugin/plaintext/clash_out.go | 36 | ||||
| -rw-r--r-- | plugin/plaintext/common_in.go | 106 | ||||
| -rw-r--r-- | plugin/plaintext/common_out.go | 170 | ||||
| -rw-r--r-- | plugin/plaintext/surge_in.go | 26 | ||||
| -rw-r--r-- | plugin/plaintext/surge_out.go | 26 | ||||
| -rw-r--r-- | plugin/plaintext/text_in.go | 202 | ||||
| -rw-r--r-- | plugin/plaintext/text_out.go | 78 | ||||
| -rw-r--r-- | plugin/special/private.go | 94 | ||||
| -rw-r--r-- | plugin/special/test.go | 76 | ||||
| -rw-r--r-- | plugin/v2ray/dat.go | 227 |
13 files changed, 1491 insertions, 0 deletions
diff --git a/plugin/maxmind/country_csv.go b/plugin/maxmind/country_csv.go new file mode 100644 index 00000000..6394e375 --- /dev/null +++ b/plugin/maxmind/country_csv.go @@ -0,0 +1,216 @@ +package maxmind + +import ( + "encoding/csv" + "encoding/json" + "errors" + "os" + "path/filepath" + "strings" + + "github.com/Loyalsoldier/geoip/lib" +) + +const ( + typeCountryCSV = "maxmindGeoLite2CountryCSV" + descCountryCSV = "Convert MaxMind GeoLite2 country CSV data to other formats" +) + +var ( + defaultCCFile = filepath.Join("./", "geolite2", "GeoLite2-Country-Locations-en.csv") + defaultIPv4File = filepath.Join("./", "geolite2", "GeoLite2-Country-Blocks-IPv4.csv") + defaultIPv6File = filepath.Join("./", "geolite2", "GeoLite2-Country-Blocks-IPv6.csv") +) + +func init() { + lib.RegisterInputConfigCreator(typeCountryCSV, func(action lib.Action, data json.RawMessage) (lib.InputConverter, error) { + return newGeoLite2CountryCSV(action, data) + }) + lib.RegisterInputConverter(typeCountryCSV, &geoLite2CountryCSV{ + Description: descCountryCSV, + }) +} + +func newGeoLite2CountryCSV(action lib.Action, data json.RawMessage) (lib.InputConverter, error) { + var tmp struct { + CountryCodeFile string `json:"country"` + IPv4File string `json:"ipv4"` + IPv6File string `json:"ipv6"` + 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.CountryCodeFile == "" { + tmp.CountryCodeFile = defaultCCFile + } + + if tmp.IPv4File == "" { + tmp.IPv4File = defaultIPv4File + } + + if tmp.IPv6File == "" { + tmp.IPv6File = defaultIPv6File + } + + return &geoLite2CountryCSV{ + Type: typeCountryCSV, + Action: action, + Description: descCountryCSV, + CountryCodeFile: tmp.CountryCodeFile, + IPv4File: tmp.IPv4File, + IPv6File: tmp.IPv6File, + Want: tmp.Want, + OnlyIPType: tmp.OnlyIPType, + }, nil +} + +type geoLite2CountryCSV struct { + Type string + Action lib.Action + Description string + CountryCodeFile string + IPv4File string + IPv6File string + Want []string + OnlyIPType lib.IPType +} + +func (g *geoLite2CountryCSV) GetType() string { + return g.Type +} + +func (g *geoLite2CountryCSV) GetAction() lib.Action { + return g.Action +} + +func (g *geoLite2CountryCSV) GetDescription() string { + return g.Description +} + +func (g *geoLite2CountryCSV) Input(container lib.Container) (lib.Container, error) { + ccMap, err := g.getCountryCode() + if err != nil { + return nil, err + } + + entries := make(map[string]*lib.Entry) + + if g.IPv4File != "" { + if err := g.process(g.IPv4File, ccMap, entries); err != nil { + return nil, err + } + } + + if g.IPv6File != "" { + if err := g.process(g.IPv6File, ccMap, entries); err != nil { + return nil, err + } + } + + var ignoreIPType lib.IgnoreIPOption + switch g.OnlyIPType { + case lib.IPv4: + ignoreIPType = lib.IgnoreIPv6 + case lib.IPv6: + ignoreIPType = lib.IgnoreIPv4 + } + + for name, entry := range entries { + switch g.Action { + case lib.ActionAdd: + if err := container.Add(entry, ignoreIPType); err != nil { + return nil, err + } + case lib.ActionRemove: + container.Remove(name, ignoreIPType) + case lib.ActionReplace: + container.Replace(entry, ignoreIPType) + default: + return nil, lib.ErrUnknownAction + } + } + + return container, nil +} + +func (g *geoLite2CountryCSV) getCountryCode() (map[string]string, error) { + ccReader, err := os.Open(g.CountryCodeFile) + if err != nil { + return nil, err + } + defer ccReader.Close() + + reader := csv.NewReader(ccReader) + lines, err := reader.ReadAll() + if err != nil { + return nil, err + } + + ccMap := make(map[string]string) + for _, line := range lines[1:] { + id := strings.TrimSpace(line[0]) + countryCode := strings.TrimSpace(line[4]) + if id == "" || countryCode == "" { + continue + } + ccMap[id] = strings.ToUpper(countryCode) + } + return ccMap, nil +} + +func (g *geoLite2CountryCSV) process(file string, ccMap map[string]string, entries map[string]*lib.Entry) error { + if len(ccMap) == 0 { + return errors.New("country code list must be specified") + } + if entries == nil { + entries = make(map[string]*lib.Entry) + } + + fReader, err := os.Open(file) + if err != nil { + return err + } + defer fReader.Close() + + reader := csv.NewReader(fReader) + lines, err := reader.ReadAll() + if err != nil { + return err + } + + // 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 _, line := range lines[1:] { + ccID := strings.TrimSpace(line[1]) + if countryCode, found := ccMap[ccID]; found { + if len(wantList) > 0 { + if _, found := wantList[countryCode]; !found { + continue + } + } + cidrStr := strings.ToLower(strings.TrimSpace(line[0])) + entry, found := entries[countryCode] + if !found { + entry = lib.NewEntry(countryCode) + } + if err := entry.AddPrefix(cidrStr); err != nil { + return err + } + entries[countryCode] = entry + } + } + + return nil +} diff --git a/plugin/maxmind/mmdb.go b/plugin/maxmind/mmdb.go new file mode 100644 index 00000000..64f79178 --- /dev/null +++ b/plugin/maxmind/mmdb.go @@ -0,0 +1,198 @@ +package maxmind + +import ( + "encoding/json" + "fmt" + "log" + "net" + "os" + "path/filepath" + "strings" + + "github.com/Loyalsoldier/geoip/lib" + "github.com/maxmind/mmdbwriter" + "github.com/maxmind/mmdbwriter/mmdbtype" +) + +const ( + typeMaxmindMMDB = "maxmindMMDB" + descMaxmindMMDB = "Convert data to MaxMind mmdb database format" +) + +var ( + defaultOutputName = "Country.mmdb" + defaultOutputDir = filepath.Join("./", "output", "maxmind") +) + +func init() { + lib.RegisterOutputConfigCreator(typeMaxmindMMDB, func(action lib.Action, data json.RawMessage) (lib.OutputConverter, error) { + return newMMDB(action, data) + }) + lib.RegisterOutputConverter(typeMaxmindMMDB, &mmdb{ + Description: descMaxmindMMDB, + }) +} + +func newMMDB(action lib.Action, data json.RawMessage) (lib.OutputConverter, error) { + var tmp struct { + OutputName string `json:"outputName"` + OutputDir string `json:"outputDir"` + 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.OutputName == "" { + tmp.OutputName = defaultOutputName + } + + if tmp.OutputDir == "" { + tmp.OutputDir = defaultOutputDir + } + + return &mmdb{ + Type: typeMaxmindMMDB, + Action: action, + Description: descMaxmindMMDB, + OutputName: tmp.OutputName, + OutputDir: tmp.OutputDir, + Want: tmp.Want, + OnlyIPType: tmp.OnlyIPType, + }, nil +} + +type mmdb struct { + Type string + Action lib.Action + Description string + OutputName string + OutputDir string + Want []string + OnlyIPType lib.IPType +} + +func (m *mmdb) GetType() string { + return m.Type +} + +func (m *mmdb) GetAction() lib.Action { + return m.Action +} + +func (m *mmdb) GetDescription() string { + return m.Description +} + +func (m *mmdb) Output(container lib.Container) error { + // Filter want list + wantList := make(map[string]bool) + for _, want := range m.Want { + if want = strings.ToUpper(strings.TrimSpace(want)); want != "" { + wantList[want] = true + } + } + + writer, err := mmdbwriter.New( + mmdbwriter.Options{ + DatabaseType: "GeoIP2-Country", + RecordSize: 24, + IncludeReservedNetworks: true, + }, + ) + if err != nil { + return err + } + + updated := false + switch len(wantList) { + case 0: + for entry := range container.Loop() { + if err := m.marshalData(writer, entry); err != nil { + return err + } + updated = true + } + + default: + for name := range wantList { + entry, found := container.GetEntry(name) + if !found { + log.Printf("entry %s not found", name) + continue + } + if err := m.marshalData(writer, entry); err != nil { + return err + } + updated = true + } + } + + if updated { + if err := m.writeFile(m.OutputName, writer); err != nil { + return err + } + } else { + return fmt.Errorf("type %s | action %s failed to write file", m.Type, m.Action) + } + + return nil +} + +func (m *mmdb) marshalData(writer *mmdbwriter.Tree, entry *lib.Entry) error { + var entryCidr []string + var err error + switch m.OnlyIPType { + case lib.IPv4: + entryCidr, err = entry.MarshalText(lib.IgnoreIPv6) + case lib.IPv6: + entryCidr, err = entry.MarshalText(lib.IgnoreIPv4) + default: + entryCidr, err = entry.MarshalText() + } + if err != nil { + return err + } + + record := mmdbtype.Map{ + "country": mmdbtype.Map{ + "iso_code": mmdbtype.String(entry.GetName()), + }, + } + + for _, cidr := range entryCidr { + _, network, err := net.ParseCIDR(cidr) + if err != nil { + return err + } + if err := writer.Insert(network, record); err != nil { + return err + } + } + + return nil +} + +func (m *mmdb) writeFile(filename string, writer *mmdbwriter.Tree) error { + if err := os.MkdirAll(m.OutputDir, 0755); err != nil { + return err + } + + f, err := os.OpenFile(filepath.Join(m.OutputDir, filename), os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644) + if err != nil { + return err + } + + _, err = writer.WriteTo(f) + if err != nil { + return err + } + + log.Printf("✅ [%s] %s --> %s", m.Type, filename, m.OutputDir) + + return nil +} diff --git a/plugin/plaintext/clash_in.go b/plugin/plaintext/clash_in.go new file mode 100644 index 00000000..7cf36338 --- /dev/null +++ b/plugin/plaintext/clash_in.go @@ -0,0 +1,36 @@ +package plaintext + +import ( + "encoding/json" + + "github.com/Loyalsoldier/geoip/lib" +) + +/* +The types in this file extend the type `typeTextIn`, +which make it possible to support more formats for the project. +*/ + +const ( + typeClashRuleSetClassicalIn = "clashRuleSetClassical" + descClashClassicalIn = "Convert classical type of Clash RuleSet to other formats (just processing IP & CIDR lines)" + + typeClashRuleSetIPCIDRIn = "clashRuleSet" + descClashRuleSetIn = "Convert ipcidr type of Clash RuleSet to other formats" +) + +func init() { + lib.RegisterInputConfigCreator(typeClashRuleSetClassicalIn, func(action lib.Action, data json.RawMessage) (lib.InputConverter, error) { + return newTextIn(typeClashRuleSetClassicalIn, action, data) + }) + lib.RegisterInputConverter(typeClashRuleSetClassicalIn, &textIn{ + Description: descClashClassicalIn, + }) + + lib.RegisterInputConfigCreator(typeClashRuleSetIPCIDRIn, func(action lib.Action, data json.RawMessage) (lib.InputConverter, error) { + return newTextIn(typeClashRuleSetIPCIDRIn, action, data) + }) + lib.RegisterInputConverter(typeClashRuleSetIPCIDRIn, &textIn{ + Description: descClashRuleSetIn, + }) +} diff --git a/plugin/plaintext/clash_out.go b/plugin/plaintext/clash_out.go new file mode 100644 index 00000000..a7feab75 --- /dev/null +++ b/plugin/plaintext/clash_out.go @@ -0,0 +1,36 @@ +package plaintext + +import ( + "encoding/json" + + "github.com/Loyalsoldier/geoip/lib" +) + +/* +The types in this file extend the type `typeTextOut`, +which make it possible to support more formats for the project. +*/ + +const ( + typeClashRuleSetClassicalOut = "clashRuleSetClassical" + descClashClassicalOut = "Convert data to classical type of Clash RuleSet" + + typeClashRuleSetIPCIDROut = "clashRuleSet" + descClashRuleSetOut = "Convert data to ipcidr type of Clash RuleSet" +) + +func init() { + lib.RegisterOutputConfigCreator(typeClashRuleSetClassicalOut, func(action lib.Action, data json.RawMessage) (lib.OutputConverter, error) { + return newTextOut(typeClashRuleSetClassicalOut, action, data) + }) + lib.RegisterOutputConverter(typeClashRuleSetClassicalOut, &textOut{ + Description: descClashClassicalOut, + }) + + lib.RegisterOutputConfigCreator(typeClashRuleSetIPCIDROut, func(action lib.Action, data json.RawMessage) (lib.OutputConverter, error) { + return newTextOut(typeClashRuleSetIPCIDROut, action, data) + }) + lib.RegisterOutputConverter(typeClashRuleSetIPCIDROut, &textOut{ + Description: descClashRuleSetOut, + }) +} diff --git a/plugin/plaintext/common_in.go b/plugin/plaintext/common_in.go new file mode 100644 index 00000000..c8bfe367 --- /dev/null +++ b/plugin/plaintext/common_in.go @@ -0,0 +1,106 @@ +package plaintext + +import ( + "bufio" + "io" + "strings" + + "github.com/Loyalsoldier/geoip/lib" + "gopkg.in/yaml.v2" +) + +type textIn struct { + Type string + Action lib.Action + Description string + Name string + URI string + InputDir string + OnlyIPType lib.IPType +} + +func (t *textIn) scanFile(reader io.Reader, entry *lib.Entry) error { + var err error + switch t.Type { + case typeTextIn: + err = t.scanFileForTextIn(reader, entry) + case typeClashRuleSetClassicalIn: + err = t.scanFileForClashClassicalRuleSetInAndSurgeIn(reader, entry) + case typeClashRuleSetIPCIDRIn: + err = t.scanFileForClashRuleSetIn(reader, entry) + case typeSurgeRuleSetIn: + err = t.scanFileForClashClassicalRuleSetInAndSurgeIn(reader, entry) + default: + return lib.ErrNotSupportedFormat + } + + return err +} + +func (t *textIn) scanFileForTextIn(reader io.Reader, entry *lib.Entry) error { + scanner := bufio.NewScanner(reader) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" { + continue + } + if err := entry.AddPrefix(line); err != nil { + return err + } + } + if err := scanner.Err(); err != nil { + return err + } + + return nil +} + +func (t *textIn) scanFileForClashRuleSetIn(reader io.Reader, entry *lib.Entry) error { + var payload struct { + Payload []string `yaml:"payload"` + } + + data, err := io.ReadAll(reader) + if err != nil { + return err + } + + if err := yaml.Unmarshal(data, &payload); err != nil { + return err + } + + for _, cidrStr := range payload.Payload { + if err := entry.AddPrefix(strings.TrimSpace(cidrStr)); err != nil { + return err + } + } + + return nil +} + +func (t *textIn) scanFileForClashClassicalRuleSetInAndSurgeIn(reader io.Reader, entry *lib.Entry) error { + scanner := bufio.NewScanner(reader) + for scanner.Scan() { + line := strings.ToLower(strings.TrimSpace(scanner.Text())) + if line == "" { + continue + } + + switch { + case strings.HasPrefix(line, "ip-cidr,"), strings.HasPrefix(line, "ip-cidr6,"): + parts := strings.Split(line, ",") + if len(parts) > 1 { + if err := entry.AddPrefix(strings.TrimSpace(parts[1])); err != nil { + return err + } + } + default: + continue + } + } + if err := scanner.Err(); err != nil { + return err + } + + return nil +} diff --git a/plugin/plaintext/common_out.go b/plugin/plaintext/common_out.go new file mode 100644 index 00000000..ff4ab63b --- /dev/null +++ b/plugin/plaintext/common_out.go @@ -0,0 +1,170 @@ +package plaintext + +import ( + "bytes" + "encoding/json" + "log" + "net" + "os" + "path/filepath" + + "github.com/Loyalsoldier/geoip/lib" +) + +var ( + defaultOutputDirForTextOut = filepath.Join("./", "output", "text") + defaultOutputDirForClashRuleSetClassicalOut = filepath.Join("./", "output", "clash", "classical") + defaultOutputDirForClashRuleSetIPCIDROut = filepath.Join("./", "output", "clash", "ipcidr") + defaultOutputDirForSurgeRuleSetOut = filepath.Join("./", "output", "surge") +) + +type textOut struct { + Type string + Action lib.Action + Description string + OutputDir string + Want []string + OnlyIPType lib.IPType +} + +func newTextOut(iType string, action lib.Action, data json.RawMessage) (lib.OutputConverter, error) { + var tmp struct { + OutputDir string `json:"outputDir"` + 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.OutputDir == "" { + switch iType { + case typeTextOut: + tmp.OutputDir = defaultOutputDirForTextOut + case typeClashRuleSetClassicalOut: + tmp.OutputDir = defaultOutputDirForClashRuleSetClassicalOut + case typeClashRuleSetIPCIDROut: + tmp.OutputDir = defaultOutputDirForClashRuleSetIPCIDROut + case typeSurgeRuleSetOut: + tmp.OutputDir = defaultOutputDirForSurgeRuleSetOut + } + } + + return &textOut{ + Type: iType, + Action: action, + Description: descTextOut, + OutputDir: tmp.OutputDir, + Want: tmp.Want, + OnlyIPType: tmp.OnlyIPType, + }, nil +} + +func (t *textOut) marshalBytes(entry *lib.Entry) ([]byte, error) { + var err error + + var entryCidr []string + switch t.OnlyIPType { + case lib.IPv4: + entryCidr, err = entry.MarshalText(lib.IgnoreIPv6) + case lib.IPv6: + entryCidr, err = entry.MarshalText(lib.IgnoreIPv4) + default: + entryCidr, err = entry.MarshalText() + } + if err != nil { + return nil, err + } + + var buf bytes.Buffer + switch t.Type { + case typeTextOut: + err = t.marshalBytesForTextOut(&buf, entryCidr) + case typeClashRuleSetClassicalOut: + err = t.marshalBytesForClashRuleSetClassicalOut(&buf, entryCidr) + case typeClashRuleSetIPCIDROut: + err = t.marshalBytesForClashRuleSetIPCIDROut(&buf, entryCidr) + case typeSurgeRuleSetOut: + err = t.marshalBytesForSurgeRuleSetOut(&buf, entryCidr) + default: + return nil, lib.ErrNotSupportedFormat + } + if err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +func (t *textOut) marshalBytesForTextOut(buf *bytes.Buffer, entryCidr []string) error { + for _, cidr := range entryCidr { + buf.WriteString(cidr) + buf.WriteString("\n") + } + return nil +} + +func (t *textOut) marshalBytesForClashRuleSetClassicalOut(buf *bytes.Buffer, entryCidr []string) error { + buf.WriteString("payload:\n") + for _, cidr := range entryCidr { + ip, _, err := net.ParseCIDR(cidr) + if err != nil { + return err + } + if ip.To4() != nil { + buf.WriteString(" - IP-CIDR,") + } else { + buf.WriteString(" - IP-CIDR6,") + } + buf.WriteString(cidr) + buf.WriteString("\n") + } + + return nil +} + +func (t *textOut) marshalBytesForClashRuleSetIPCIDROut(buf *bytes.Buffer, entryCidr []string) error { + buf.WriteString("payload:\n") + for _, cidr := range entryCidr { + buf.WriteString(" - '") + buf.WriteString(cidr) + buf.WriteString("'\n") + } + + return nil +} + +func (t *textOut) marshalBytesForSurgeRuleSetOut(buf *bytes.Buffer, entryCidr []string) error { + for _, cidr := range entryCidr { + ip, _, err := net.ParseCIDR(cidr) + if err != nil { + return err + } + if ip.To4() != nil { + buf.WriteString("IP-CIDR,") + } else { + buf.WriteString("IP-CIDR6,") + } + buf.WriteString(cidr) + buf.WriteString("\n") + } + + return nil +} + +func (t *textOut) writeFile(filename string, data []byte) error { + if err := os.MkdirAll(t.OutputDir, 0755); err != nil { + return err + } + + if err := os.WriteFile(filepath.Join(t.OutputDir, filename), data, 0644); err != nil { + return err + } + + log.Printf("✅ [%s] %s --> %s", t.Type, filename, t.OutputDir) + + return nil +} diff --git a/plugin/plaintext/surge_in.go b/plugin/plaintext/surge_in.go new file mode 100644 index 00000000..d409a782 --- /dev/null +++ b/plugin/plaintext/surge_in.go @@ -0,0 +1,26 @@ +package plaintext + +import ( + "encoding/json" + + "github.com/Loyalsoldier/geoip/lib" +) + +/* +The types in this file extend the type `typeTextIn`, +which make it possible to support more formats for the project. +*/ + +const ( + typeSurgeRuleSetIn = "surgeRuleSet" + descSurgeRuleSetIn = "Convert Surge RuleSet to other formats (just processing IP & CIDR lines)" +) + +func init() { + lib.RegisterInputConfigCreator(typeSurgeRuleSetIn, func(action lib.Action, data json.RawMessage) (lib.InputConverter, error) { + return newTextIn(typeSurgeRuleSetIn, action, data) + }) + lib.RegisterInputConverter(typeSurgeRuleSetIn, &textIn{ + Description: descSurgeRuleSetIn, + }) +} diff --git a/plugin/plaintext/surge_out.go b/plugin/plaintext/surge_out.go new file mode 100644 index 00000000..c3868423 --- /dev/null +++ b/plugin/plaintext/surge_out.go @@ -0,0 +1,26 @@ +package plaintext + +import ( + "encoding/json" + + "github.com/Loyalsoldier/geoip/lib" +) + +/* +The types in this file extend the type `typeTextOut`, +which make it possible to support more formats for the project. +*/ + +const ( + typeSurgeRuleSetOut = "surgeRuleSet" + descSurgeRuleSetOut = "Convert data to Surge RuleSet" +) + +func init() { + lib.RegisterOutputConfigCreator(typeSurgeRuleSetOut, func(action lib.Action, data json.RawMessage) (lib.OutputConverter, error) { + return newTextOut(typeSurgeRuleSetOut, action, data) + }) + lib.RegisterOutputConverter(typeSurgeRuleSetOut, &textOut{ + Description: descSurgeRuleSetOut, + }) +} diff --git a/plugin/plaintext/text_in.go b/plugin/plaintext/text_in.go new file mode 100644 index 00000000..64ad3ad3 --- /dev/null +++ b/plugin/plaintext/text_in.go @@ -0,0 +1,202 @@ +package plaintext + +import ( + "encoding/json" + "fmt" + "net/http" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/Loyalsoldier/geoip/lib" +) + +const ( + typeTextIn = "text" + descTextIn = "Convert plaintext IP & CIDR to other formats" +) + +func init() { + lib.RegisterInputConfigCreator(typeTextIn, func(action lib.Action, data json.RawMessage) (lib.InputConverter, error) { + return newTextIn(typeTextIn, action, data) + }) + lib.RegisterInputConverter(typeTextIn, &textIn{ + Description: descTextIn, + }) +} + +func newTextIn(iType string, action lib.Action, data json.RawMessage) (lib.InputConverter, error) { + var tmp struct { + Name string `json:"name"` + URI string `json:"uri"` + InputDir string `json:"inputDir"` + OnlyIPType lib.IPType `json:"onlyIPType"` + } + + if strings.TrimSpace(iType) == "" { + return nil, fmt.Errorf("type is required") + } + + if len(data) > 0 { + if err := json.Unmarshal(data, &tmp); err != nil { + return nil, err + } + } + + if tmp.Name == "" && tmp.URI == "" && tmp.InputDir == "" { + return nil, fmt.Errorf("type %s | action %s missing inputdir or name or uri", typeTextIn, action) + } + + if (tmp.Name != "" && tmp.URI == "") || (tmp.Name == "" && tmp.URI != "") { + return nil, fmt.Errorf("type %s | action %s name & uri must be specified together", typeTextIn, action) + } + + return &textIn{ + Type: iType, + Action: action, + Description: descTextIn, + Name: tmp.Name, + URI: tmp.URI, + InputDir: tmp.InputDir, + OnlyIPType: tmp.OnlyIPType, + }, nil +} + +func (t *textIn) GetType() string { + return t.Type +} + +func (t *textIn) GetAction() lib.Action { + return t.Action +} + +func (t *textIn) GetDescription() string { + return t.Description +} + +func (t *textIn) Input(container lib.Container) (lib.Container, error) { + entries := make(map[string]*lib.Entry) + var err error + + switch { + case t.InputDir != "": + err = t.walkDir(t.InputDir, entries) + case t.Name != "" && t.URI != "": + switch { + case strings.HasPrefix(t.URI, "http://"), strings.HasPrefix(t.URI, "https://"): + err = t.walkRemoteFile(t.URI, t.Name, entries) + default: + err = t.walkLocalFile(t.URI, t.Name, entries) + } + default: + return nil, fmt.Errorf("config missing argument inputDir or name or uri") + } + + if err != nil { + return nil, err + } + + var ignoreIPType lib.IgnoreIPOption + switch t.OnlyIPType { + case lib.IPv4: + ignoreIPType = lib.IgnoreIPv6 + case lib.IPv6: + ignoreIPType = lib.IgnoreIPv4 + } + + if len(entries) == 0 { + return nil, fmt.Errorf("type %s | action %s no entry are generated", t.Type, t.Action) + } + + for _, entry := range entries { + switch t.Action { + case lib.ActionAdd: + if err := container.Add(entry, ignoreIPType); err != nil { + return nil, err + } + case lib.ActionRemove: + container.Remove(entry.GetName(), ignoreIPType) + case lib.ActionReplace: + container.Replace(entry, ignoreIPType) + } + } + + return container, nil +} + +func (t *textIn) walkDir(dir string, entries map[string]*lib.Entry) error { + err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + + if err := t.walkLocalFile(path, "", entries); err != nil { + return err + } + + return nil + }) + + return err +} + +func (t *textIn) walkLocalFile(path, name string, entries map[string]*lib.Entry) error { + name = strings.TrimSpace(name) + var filename string + if name != "" { + filename = name + } else { + filename = filepath.Base(path) + } + + // check filename + if !regexp.MustCompile(`^[a-zA-Z0-9_.\-]+$`).MatchString(filename) { + return fmt.Errorf("filename %s cannot be entry name, please remove special characters in it", filename) + } + dotIndex := strings.LastIndex(filename, ".") + if dotIndex > 0 { + filename = filename[:dotIndex] + } + + if _, found := entries[filename]; found { + return fmt.Errorf("found duplicated file %s", filename) + } + + entry := lib.NewEntry(filename) + file, err := os.Open(path) + if err != nil { + return err + } + defer file.Close() + if err := t.scanFile(file, entry); err != nil { + return err + } + + entries[filename] = entry + + return nil +} + +func (t *textIn) walkRemoteFile(url, name string, entries map[string]*lib.Entry) error { + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return fmt.Errorf("failed to get remote file %s, http status code %d", url, resp.StatusCode) + } + + entry := lib.NewEntry(name) + if err := t.scanFile(resp.Body, entry); err != nil { + return err + } + + entries[name] = entry + return nil +} diff --git a/plugin/plaintext/text_out.go b/plugin/plaintext/text_out.go new file mode 100644 index 00000000..7bdfaf42 --- /dev/null +++ b/plugin/plaintext/text_out.go @@ -0,0 +1,78 @@ +package plaintext + +import ( + "encoding/json" + "log" + "strings" + + "github.com/Loyalsoldier/geoip/lib" +) + +const ( + typeTextOut = "text" + descTextOut = "Convert data to plaintext CIDR format" +) + +func init() { + lib.RegisterOutputConfigCreator(typeTextOut, func(action lib.Action, data json.RawMessage) (lib.OutputConverter, error) { + return newTextOut(typeTextOut, action, data) + }) + lib.RegisterOutputConverter(typeTextOut, &textOut{ + Description: descTextOut, + }) +} + +func (t *textOut) GetType() string { + return t.Type +} + +func (t *textOut) GetAction() lib.Action { + return t.Action +} + +func (t *textOut) GetDescription() string { + return t.Description +} + +func (t *textOut) Output(container lib.Container) error { + // Filter want list + wantList := make(map[string]bool) + for _, want := range t.Want { + if want = strings.ToUpper(strings.TrimSpace(want)); want != "" { + wantList[want] = true + } + } + + switch len(wantList) { + case 0: + for entry := range container.Loop() { + data, err := t.marshalBytes(entry) + if err != nil { + return err + } + filename := strings.ToLower(entry.GetName()) + ".txt" + if err := t.writeFile(filename, data); err != nil { + return err + } + } + + default: + for name := range wantList { + entry, found := container.GetEntry(name) + if !found { + log.Printf("entry %s not found", name) + continue + } + data, err := t.marshalBytes(entry) + if err != nil { + return err + } + filename := strings.ToLower(entry.GetName()) + ".txt" + if err := t.writeFile(filename, data); err != nil { + return err + } + } + } + + return nil +} diff --git a/plugin/special/private.go b/plugin/special/private.go new file mode 100644 index 00000000..e8f231ef --- /dev/null +++ b/plugin/special/private.go @@ -0,0 +1,94 @@ +package special + +import ( + "encoding/json" + + "github.com/Loyalsoldier/geoip/lib" +) + +const ( + entryNamePrivate = "private" + typePrivate = "private" + descPrivate = "Convert LAN and private network CIDR to other formats" +) + +var privateCIDRs = []string{ + "0.0.0.0/8", + "10.0.0.0/8", + "100.64.0.0/10", + "127.0.0.0/8", + "169.254.0.0/16", + "172.16.0.0/12", + "192.0.0.0/24", + "192.0.2.0/24", + "192.88.99.0/24", + "192.168.0.0/16", + "198.18.0.0/15", + "198.51.100.0/24", + "203.0.113.0/24", + "224.0.0.0/4", + "240.0.0.0/4", + "255.255.255.255/32", + "::1/128", + "fc00::/7", + "fe80::/10", +} + +func init() { + lib.RegisterInputConfigCreator(typePrivate, func(action lib.Action, data json.RawMessage) (lib.InputConverter, error) { + return newPrivate(action, data) + }) + lib.RegisterInputConverter(typePrivate, &private{ + Description: descPrivate, + }) +} + +func newPrivate(action lib.Action, data json.RawMessage) (lib.InputConverter, error) { + return &private{ + Type: typePrivate, + Action: action, + Description: descPrivate, + }, nil +} + +type private struct { + Type string + Action lib.Action + Description string +} + +func (p *private) GetType() string { + return p.Type +} + +func (p *private) GetAction() lib.Action { + return p.Action +} + +func (p *private) GetDescription() string { + return p.Description +} + +func (p *private) Input(container lib.Container) (lib.Container, error) { + entry := lib.NewEntry(entryNamePrivate) + for _, cidr := range privateCIDRs { + if err := entry.AddPrefix(cidr); err != nil { + return nil, err + } + } + + switch p.Action { + case lib.ActionAdd: + if err := container.Add(entry); err != nil { + return nil, err + } + case lib.ActionRemove: + container.Remove(entryNamePrivate) + case lib.ActionReplace: + container.Replace(entry) + default: + return nil, lib.ErrUnknownAction + } + + return container, nil +} diff --git a/plugin/special/test.go b/plugin/special/test.go new file mode 100644 index 00000000..2700b61e --- /dev/null +++ b/plugin/special/test.go @@ -0,0 +1,76 @@ +package special + +import ( + "encoding/json" + + "github.com/Loyalsoldier/geoip/lib" +) + +const ( + entryNameTest = "test" + typeTest = "test" + descTest = "Convert specific CIDR to other formats (for test only)" +) + +var testCIDRs = []string{ + "127.0.0.0/8", +} + +func init() { + lib.RegisterInputConfigCreator(typeTest, func(action lib.Action, data json.RawMessage) (lib.InputConverter, error) { + return newTest(action, data) + }) + lib.RegisterInputConverter(typeTest, &test{ + Description: descTest, + }) +} + +func newTest(action lib.Action, data json.RawMessage) (lib.InputConverter, error) { + return &test{ + Type: typeTest, + Action: action, + Description: descTest, + }, nil +} + +type test struct { + Type string + Action lib.Action + Description string +} + +func (t *test) GetType() string { + return t.Type +} + +func (t *test) GetAction() lib.Action { + return t.Action +} + +func (t *test) GetDescription() string { + return t.Description +} + +func (t *test) Input(container lib.Container) (lib.Container, error) { + entry := lib.NewEntry(entryNameTest) + for _, cidr := range testCIDRs { + if err := entry.AddPrefix(cidr); err != nil { + return nil, err + } + } + + switch t.Action { + case lib.ActionAdd: + if err := container.Add(entry); err != nil { + return nil, err + } + case lib.ActionRemove: + container.Remove(entryNameTest) + case lib.ActionReplace: + container.Replace(entry) + default: + return nil, lib.ErrUnknownAction + } + + return container, nil +} diff --git a/plugin/v2ray/dat.go b/plugin/v2ray/dat.go new file mode 100644 index 00000000..b33d6090 --- /dev/null +++ b/plugin/v2ray/dat.go @@ -0,0 +1,227 @@ +package v2ray + +import ( + "encoding/json" + "fmt" + "log" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/Loyalsoldier/geoip/lib" + "github.com/v2fly/v2ray-core/v4/app/router" + "github.com/v2fly/v2ray-core/v4/infra/conf/rule" + "google.golang.org/protobuf/proto" +) + +const ( + typeGeoIPdat = "v2rayGeoIPDat" + descGeoIPdat = "Convert data to V2Ray GeoIP dat format" +) + +var ( + defaultOutputName = "geoip.dat" + defaultOutputDir = filepath.Join("./", "output", "dat") +) + +func init() { + lib.RegisterOutputConfigCreator(typeGeoIPdat, func(action lib.Action, data json.RawMessage) (lib.OutputConverter, error) { + return newGeoIPDat(action, data) + }) + lib.RegisterOutputConverter(typeGeoIPdat, &geoIPDat{ + Description: descGeoIPdat, + }) +} + +func newGeoIPDat(action lib.Action, data json.RawMessage) (lib.OutputConverter, error) { + var tmp struct { + OutputName string `json:"outputName"` + OutputDir string `json:"outputDir"` + Want []string `json:"wantedList"` + OneFilePerList bool `json:"oneFilePerList"` + OnlyIPType lib.IPType `json:"onlyIPType"` + } + + if len(data) > 0 { + if err := json.Unmarshal(data, &tmp); err != nil { + return nil, err + } + } + + if tmp.OutputName == "" { + tmp.OutputName = defaultOutputName + } + + if tmp.OutputDir == "" { + tmp.OutputDir = defaultOutputDir + } + + return &geoIPDat{ + Type: typeGeoIPdat, + Action: action, + Description: descGeoIPdat, + OutputName: tmp.OutputName, + OutputDir: tmp.OutputDir, + Want: tmp.Want, + OneFilePerList: tmp.OneFilePerList, + OnlyIPType: tmp.OnlyIPType, + }, nil +} + +type geoIPDat struct { + Type string + Action lib.Action + Description string + OutputName string + OutputDir string + Want []string + OneFilePerList bool + OnlyIPType lib.IPType +} + +func (g *geoIPDat) GetType() string { + return g.Type +} + +func (g *geoIPDat) GetAction() lib.Action { + return g.Action +} + +func (g *geoIPDat) GetDescription() string { + return g.Description +} + +func (g *geoIPDat) Output(container lib.Container) error { + // Filter want list + wantList := make(map[string]bool) + for _, want := range g.Want { + if want = strings.ToUpper(strings.TrimSpace(want)); want != "" { + wantList[want] = true + } + } + + geoIPList := new(router.GeoIPList) + geoIPList.Entry = make([]*router.GeoIP, 0, 300) + updated := false + switch len(wantList) { + case 0: + for entry := range container.Loop() { + geoIP, err := g.generateGeoIP(entry) + if err != nil { + return err + } + geoIPList.Entry = append(geoIPList.Entry, geoIP) + updated = true + + if g.OneFilePerList { + geoIPBytes, err := proto.Marshal(geoIPList) + if err != nil { + return err + } + filename := strings.ToLower(entry.GetName()) + ".dat" + if err := g.writeFile(filename, geoIPBytes); err != nil { + return err + } + geoIPList.Entry = nil + } + } + + default: + for name := range wantList { + entry, found := container.GetEntry(name) + if !found { + log.Printf("entry %s not found", name) + continue + } + geoIP, err := g.generateGeoIP(entry) + if err != nil { + return err + } + geoIPList.Entry = append(geoIPList.Entry, geoIP) + updated = true + + if g.OneFilePerList { + geoIPBytes, err := proto.Marshal(geoIPList) + if err != nil { + return err + } + filename := strings.ToLower(entry.GetName()) + ".dat" + if err := g.writeFile(filename, geoIPBytes); err != nil { + return err + } + geoIPList.Entry = nil + } + } + } + + // Sort to make reproducible builds + g.sort(geoIPList) + + if !g.OneFilePerList && updated { + geoIPBytes, err := proto.Marshal(geoIPList) + if err != nil { + return err + } + if err := g.writeFile(g.OutputName, geoIPBytes); err != nil { + return err + } + } + + return nil +} + +func (g *geoIPDat) generateGeoIP(entry *lib.Entry) (*router.GeoIP, error) { + var entryCidr []string + var err error + switch g.OnlyIPType { + case lib.IPv4: + entryCidr, err = entry.MarshalText(lib.IgnoreIPv6) + case lib.IPv6: + entryCidr, err = entry.MarshalText(lib.IgnoreIPv4) + default: + entryCidr, err = entry.MarshalText() + } + if err != nil { + return nil, err + } + + v2rayCIDR := make([]*router.CIDR, 0, 1024) + for _, cidrStr := range entryCidr { + cidr, err := rule.ParseIP(cidrStr) + if err != nil { + return nil, err + } + v2rayCIDR = append(v2rayCIDR, cidr) + } + + if len(v2rayCIDR) > 0 { + return &router.GeoIP{ + CountryCode: entry.GetName(), + Cidr: v2rayCIDR, + }, nil + } + + return nil, fmt.Errorf("entry %s has no CIDR", entry.GetName()) +} + +// Sort by country code to make reproducible builds +func (g *geoIPDat) sort(list *router.GeoIPList) { + sort.SliceStable(list.Entry, func(i, j int) bool { + return list.Entry[i].CountryCode < list.Entry[j].CountryCode + }) +} + +func (g *geoIPDat) writeFile(filename string, geoIPBytes []byte) error { + if err := os.MkdirAll(g.OutputDir, 0755); err != nil { + return err + } + + if err := os.WriteFile(filepath.Join(g.OutputDir, filename), geoIPBytes, 0644); err != nil { + return err + } + + log.Printf("✅ [%s] %s --> %s", g.Type, filename, g.OutputDir) + + return nil +} |
