v0.1.0 #1
@@ -0,0 +1,65 @@
|
||||
# geoip2
|
||||
|
||||
Получение данных с сервиса maxmind.com. Изначально пакет пишется для сервиса https://git.kor-elf.net/kor-elf-shield/kor-elf-shield.
|
||||
|
||||
## Установка
|
||||
```sh
|
||||
go get git.kor-elf.net/kor-elf-shield/geoip2
|
||||
```
|
||||
|
||||
## Примеры использования
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/geoip2/service/maxmind/mmdb"
|
||||
)
|
||||
|
||||
type Logger struct{}
|
||||
|
||||
func (l *Logger) Error(err error) {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Для получения username и password ознакомьтесь тут: https://support.maxmind.com/knowledge-base/articles/create-a-maxmind-account
|
||||
username := "username"
|
||||
password := "password"
|
||||
download, err := mmdb.NewDownload(mmdb.DownloadURLCityLite, username, password, mmdb.DefaultDownloadConfig())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
city := mmdb.NewCity(download, &Logger{}, "tmp", mmdb.LanguageRussian)
|
||||
defer func() {
|
||||
_ = city.Close()
|
||||
}()
|
||||
|
||||
/*
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// Если хотите обновить данные
|
||||
if err := city.Refresh(ctx); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
*/
|
||||
|
||||
// Вместо 127.0.0.1 укажите интересующиеся IP адрес
|
||||
ip, err := netip.ParseAddr("127.0.0.1")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
info, err := city.Info(ip)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println(info.ToString())
|
||||
}
|
||||
```
|
||||
## Лицензия
|
||||
|
||||
[MIT](https://git.kor-elf.net/kor-elf-shield/geoip2/src/branch/main/LICENSE)
|
||||
@@ -0,0 +1,78 @@
|
||||
package geoip2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ErrNotFound is returned when the database does not contain the IP address.
|
||||
const ErrNotFound = "not found"
|
||||
|
||||
// Info is a structure that contains information about the IP address.
|
||||
type Info struct {
|
||||
// IP is the IP address.
|
||||
IP netip.Addr
|
||||
// ISOCode is the ISO code of the country.
|
||||
ISOCode string
|
||||
// Continent is the continent.
|
||||
Continent string
|
||||
// Country is the country.
|
||||
Country string
|
||||
// City is the city.
|
||||
City string
|
||||
// CitySubdivisions is the list of subdivisions.
|
||||
CitySubdivisions []string
|
||||
// TimeZone is the time zone.
|
||||
TimeZone string
|
||||
}
|
||||
|
||||
// ToString returns a string representation of the Info structure.
|
||||
func (i Info) ToString() string {
|
||||
var data []string
|
||||
|
||||
if i.Continent != "" {
|
||||
data = append(data, i.Continent)
|
||||
}
|
||||
|
||||
if i.ISOCode != "" {
|
||||
data = append(data, i.ISOCode)
|
||||
}
|
||||
|
||||
if i.Country != "" {
|
||||
data = append(data, i.Country)
|
||||
}
|
||||
|
||||
if len(i.CitySubdivisions) > 0 {
|
||||
data = append(data, strings.Join(i.CitySubdivisions, ", "))
|
||||
}
|
||||
|
||||
if i.City != "" {
|
||||
data = append(data, i.City)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s (%s) time zone: %s",
|
||||
i.IP.String(),
|
||||
strings.Join(data, "/"),
|
||||
i.TimeZone,
|
||||
)
|
||||
}
|
||||
|
||||
// GeoIP2 is an interface for geoip2.
|
||||
type GeoIP2 interface {
|
||||
Info(ip netip.Addr) (Info, error)
|
||||
}
|
||||
|
||||
// RefreshableGeoIP2 is an interface for geoip2 with refresh and close functionality.
|
||||
type RefreshableGeoIP2 interface {
|
||||
GeoIP2
|
||||
Refresh(ctx context.Context) error
|
||||
Close() error
|
||||
}
|
||||
|
||||
// Logger is an interface for logging.
|
||||
type Logger interface {
|
||||
// Error logs an error.
|
||||
Error(err error)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
module git.kor-elf.net/kor-elf-shield/geoip2
|
||||
|
||||
go 1.25
|
||||
|
||||
require github.com/oschwald/geoip2-golang/v2 v2.1.0
|
||||
|
||||
require (
|
||||
github.com/oschwald/maxminddb-golang/v2 v2.1.1 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
)
|
||||
@@ -0,0 +1,14 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/oschwald/geoip2-golang/v2 v2.1.0 h1:DjnLhNJu9WHwTrmoiQFvgmyJoczhdnm7LB23UBI2Amo=
|
||||
github.com/oschwald/geoip2-golang/v2 v2.1.0/go.mod h1:qdVmcPgrTJ4q2eP9tHq/yldMTdp2VMr33uVdFbHBiBc=
|
||||
github.com/oschwald/maxminddb-golang/v2 v2.1.1 h1:lA8FH0oOrM4u7mLvowq8IT6a3Q/qEnqRzLQn9eH5ojc=
|
||||
github.com/oschwald/maxminddb-golang/v2 v2.1.1/go.mod h1:PLdx6PR+siSIoXqqy7C7r3SB3KZnhxWr1Dp6g0Hacl8=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -0,0 +1,124 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Dir struct {
|
||||
Path string
|
||||
PathCurrentDir string
|
||||
PathBackupDir string
|
||||
}
|
||||
|
||||
// NewDir creates a new Dir instance.
|
||||
func NewDir(dir, nameCurrentDir string) *Dir {
|
||||
return &Dir{
|
||||
Path: dir,
|
||||
PathCurrentDir: filepath.Join(dir, nameCurrentDir),
|
||||
PathBackupDir: filepath.Join(dir, nameCurrentDir+".bak"),
|
||||
}
|
||||
}
|
||||
|
||||
// CreateRandomTmpDir creates a temporary directory in the specified directory with a random name.
|
||||
// The directory is created with permissions 0750.
|
||||
// If the directory already exists, an error is returned.
|
||||
// @return string - path to the temporary directory
|
||||
// @return error - an error if the directory could not be created
|
||||
func (d *Dir) CreateRandomTmpDir() (string, error) {
|
||||
tmpDir := filepath.Join(d.Path, time.Now().Format(time.RFC3339)+"_"+rand.Text()+".tmp")
|
||||
if _, err := os.Stat(tmpDir); err == nil {
|
||||
return "", fmt.Errorf("directory already exists: %s", tmpDir)
|
||||
}
|
||||
if err := os.MkdirAll(tmpDir, 0750); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return tmpDir, nil
|
||||
}
|
||||
|
||||
// ReplaceDirToCurrent replaces the current directory with a new directory.
|
||||
// If the new directory already exists, it is renamed to a backup directory.
|
||||
// If the backup directory already exists, it is removed.
|
||||
// @return error - an error if the directory could not be replaced
|
||||
func (d *Dir) ReplaceDirToCurrent(newDir string) error {
|
||||
if err := os.RemoveAll(d.PathBackupDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := os.Stat(d.PathCurrentDir); err == nil {
|
||||
if err := os.Rename(d.PathCurrentDir, d.PathBackupDir); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.Rename(newDir, d.PathCurrentDir); err != nil {
|
||||
if _, statErr := os.Stat(d.PathBackupDir); statErr == nil {
|
||||
_ = os.Rename(d.PathBackupDir, d.PathCurrentDir)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.RemoveAll(d.PathBackupDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MovingFiles moves all files from dirFrom to dirTo.
|
||||
// If dirTo does not exist, it is created.
|
||||
// @param dirFrom - path to the directory containing the files to move
|
||||
// @param dirTo - path to the directory where the files will be moved
|
||||
// @return error - an error if the files could not be moved
|
||||
func MovingFiles(dirFrom, dirTo string) error {
|
||||
if _, err := os.Stat(dirTo); err != nil {
|
||||
if err := os.MkdirAll(dirTo, 0750); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(dirFrom)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
if err := MovingFiles(filepath.Join(dirFrom, entry.Name()), dirTo); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
src := filepath.Join(dirFrom, entry.Name())
|
||||
dst := filepath.Join(dirTo, entry.Name())
|
||||
if err := os.Rename(src, dst); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindMMDBFile finds the .mmdb file in the specified directory.
|
||||
// @param dir - path to the directory to search
|
||||
// @return string - path to the .mmdb file
|
||||
// @return error - an error if the .mmdb file could not be found
|
||||
func FindMMDBFile(dir string) (string, error) {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
if filepath.Ext(entry.Name()) == ".mmdb" {
|
||||
return filepath.Join(dir, entry.Name()), nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no .mmdb file found in %s", dir)
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
package mmdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/geoip2"
|
||||
"git.kor-elf.net/kor-elf-shield/geoip2/internal/pkg"
|
||||
|
||||
oschwaldGeoip2 "github.com/oschwald/geoip2-golang/v2"
|
||||
)
|
||||
|
||||
// download is an interface for downloading MaxMind GeoIP2 database.
|
||||
type download interface {
|
||||
// Download downloads MaxMind GeoIP2 database.
|
||||
Download(dir string, ctx context.Context) error
|
||||
}
|
||||
|
||||
type service struct {
|
||||
// download is an interface for downloading MaxMind GeoIP2 database.
|
||||
download download
|
||||
// info is a function that returns information about the IP address.
|
||||
info info
|
||||
logger geoip2.Logger
|
||||
// dir is a directory for storing MaxMind GeoIP2 database.
|
||||
dir *pkg.Dir
|
||||
|
||||
// isRefreshing is a flag that indicates whether the database is being refreshed.
|
||||
isRefreshing bool
|
||||
// mu is a mutex for synchronizing access to the reader.
|
||||
mu sync.RWMutex
|
||||
// reader is a MaxMind GeoIP2 database reader.
|
||||
reader *oschwaldGeoip2.Reader
|
||||
}
|
||||
|
||||
// info is a function that returns information about the IP address.
|
||||
// @param ip - IP address
|
||||
// @param reader - MaxMind GeoIP2 database reader
|
||||
// @return geoip2.Info - information about the IP address
|
||||
type info func(ip netip.Addr, reader *oschwaldGeoip2.Reader) (geoip2.Info, error)
|
||||
|
||||
// NewMMDB creates a new MaxMind GeoIP2 database service.
|
||||
// @param download - an interface for downloading MaxMind GeoIP2 database
|
||||
// @param info - a function that returns information about the IP address
|
||||
// @param logger - a logger
|
||||
// @param dir - a directory for storing MaxMind GeoIP2 database
|
||||
// @return geoip2.RefreshableGeoIP2 - a MaxMind GeoIP2 database service
|
||||
func NewMMDB(download download, info info, logger geoip2.Logger, dir *pkg.Dir) geoip2.RefreshableGeoIP2 {
|
||||
s := &service{
|
||||
download: download,
|
||||
logger: logger,
|
||||
dir: dir,
|
||||
info: info,
|
||||
|
||||
mu: sync.RWMutex{},
|
||||
}
|
||||
s.init()
|
||||
return s
|
||||
}
|
||||
|
||||
// Info returns information about the IP address.
|
||||
// @param ip - IP address
|
||||
// @return geoip2.Info - information about the IP address
|
||||
// @return error - an error if the IP address could not be found
|
||||
func (s *service) Info(ip netip.Addr) (geoip2.Info, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
if s.reader == nil {
|
||||
return geoip2.Info{}, errors.New("geoip reader is not ready")
|
||||
}
|
||||
|
||||
return s.info(ip, s.reader)
|
||||
}
|
||||
|
||||
// Refresh refreshes the MaxMind GeoIP2 database.
|
||||
// @param ctx - a context
|
||||
// @return error - an error if the database could not be refreshed
|
||||
func (s *service) Refresh(ctx context.Context) error {
|
||||
if s.isRefreshing {
|
||||
return fmt.Errorf("process is refreshing")
|
||||
}
|
||||
s.isRefreshing = true
|
||||
defer func() {
|
||||
s.isRefreshing = false
|
||||
}()
|
||||
|
||||
if err := s.fetch(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newReader, err := s.openReader()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
oldReader := s.reader
|
||||
s.reader = newReader
|
||||
s.mu.Unlock()
|
||||
|
||||
if oldReader != nil {
|
||||
_ = oldReader.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes the MaxMind GeoIP2 database.
|
||||
// @return error - an error if the database could not be closed
|
||||
func (s *service) Close() error {
|
||||
if s.reader != nil {
|
||||
return s.reader.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// init initializes the MaxMind GeoIP2 database service.
|
||||
func (s *service) init() {
|
||||
reader, err := s.openReader()
|
||||
if err != nil {
|
||||
go func() {
|
||||
// If there is no data yet, then we try to get the data.
|
||||
if err := s.Refresh(context.Background()); err != nil {
|
||||
s.logger.Error(err)
|
||||
}
|
||||
}()
|
||||
return
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.reader = reader
|
||||
}
|
||||
|
||||
// openReader opens the MaxMind GeoIP2 database reader.
|
||||
// @return (*oschwaldGeoip2.Reader, error) - a MaxMind GeoIP2 database reader and an error if the database could not be opened
|
||||
// 1. If the database is already open, then it is returned.
|
||||
// 2. If the database is not open, then it is opened and returned.
|
||||
func (s *service) openReader() (*oschwaldGeoip2.Reader, error) {
|
||||
path, err := pkg.FindMMDBFile(s.dir.PathCurrentDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return oschwaldGeoip2.Open(path)
|
||||
}
|
||||
|
||||
// fetch downloads the MaxMind GeoIP2 database.
|
||||
// @param ctx - a context
|
||||
// @return error - an error if the database could not be downloaded
|
||||
func (s *service) fetch(ctx context.Context) error {
|
||||
tmpDir, err := s.dir.CreateRandomTmpDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err := os.RemoveAll(tmpDir); err != nil {
|
||||
s.logger.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := s.download.Download(tmpDir, ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmpDirForFiles, err := s.dir.CreateRandomTmpDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err := os.RemoveAll(tmpDir); err != nil {
|
||||
s.logger.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := pkg.MovingFiles(tmpDir, tmpDirForFiles); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.dir.ReplaceDirToCurrent(tmpDirForFiles); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
package mmdb
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/netip"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/geoip2"
|
||||
"git.kor-elf.net/kor-elf-shield/geoip2/internal/pkg"
|
||||
"git.kor-elf.net/kor-elf-shield/geoip2/internal/service/maxmind/mmdb"
|
||||
|
||||
oschwaldGeoip2 "github.com/oschwald/geoip2-golang/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
DownloadURLCityLite = "https://download.maxmind.com/geoip/databases/GeoLite2-City/download?suffix=tar.gz"
|
||||
NameDirNameCityLite = "GeoLite2-City"
|
||||
)
|
||||
|
||||
// City is a structure that contains information about the IP address.
|
||||
type City struct {
|
||||
// lang is a language.
|
||||
lang language
|
||||
}
|
||||
|
||||
// NewCity creates a new City instance.
|
||||
// @param download - an interface for downloading MaxMind GeoIP2 database
|
||||
// @param logger - a logger
|
||||
// @param dir - a directory for storing MaxMind GeoIP2 database
|
||||
// @return geoip2.RefreshableGeoIP2 - a MaxMind GeoIP2 database service
|
||||
func NewCity(download *Download, logger geoip2.Logger, dir string, language language) geoip2.RefreshableGeoIP2 {
|
||||
city := &City{
|
||||
lang: language,
|
||||
}
|
||||
|
||||
newDir := pkg.NewDir(dir, NameDirNameCityLite)
|
||||
return mmdb.NewMMDB(download, city.infoCity, logger, newDir)
|
||||
}
|
||||
|
||||
// infoCity returns information about the IP address.
|
||||
// @param ip - IP address
|
||||
// @param reader - MaxMind GeoIP2 database reader
|
||||
// @return geoip2.Info - information about the IP address
|
||||
// @return error - an error if the IP address could not be found
|
||||
func (c *City) infoCity(ip netip.Addr, reader *oschwaldGeoip2.Reader) (geoip2.Info, error) {
|
||||
record, err := reader.City(ip)
|
||||
if err != nil {
|
||||
return geoip2.Info{}, err
|
||||
}
|
||||
if !record.HasData() {
|
||||
return geoip2.Info{}, errors.New(geoip2.ErrNotFound)
|
||||
}
|
||||
|
||||
var timeZone string
|
||||
if record.Location.HasData() {
|
||||
timeZone = record.Location.TimeZone
|
||||
}
|
||||
|
||||
return geoip2.Info{
|
||||
IP: ip,
|
||||
ISOCode: record.Country.ISOCode,
|
||||
Country: c.country(record.Country),
|
||||
City: c.city(record.City),
|
||||
CitySubdivisions: c.subdivisions(record.Subdivisions),
|
||||
TimeZone: timeZone,
|
||||
Continent: c.continent(record.Continent),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *City) continent(continent oschwaldGeoip2.Continent) string {
|
||||
if !continent.HasData() {
|
||||
return ""
|
||||
}
|
||||
|
||||
switch c.lang {
|
||||
case LanguageRussian:
|
||||
return continent.Names.Russian
|
||||
case LanguageEnglish:
|
||||
return continent.Names.English
|
||||
case LanguageBrazilianPortuguese:
|
||||
return continent.Names.BrazilianPortuguese
|
||||
case LanguageSimplifiedChinese:
|
||||
return continent.Names.SimplifiedChinese
|
||||
case LanguageJapanese:
|
||||
return continent.Names.Japanese
|
||||
case LanguageGerman:
|
||||
return continent.Names.German
|
||||
case LanguageFrench:
|
||||
return continent.Names.French
|
||||
case LanguageSpanish:
|
||||
return continent.Names.Spanish
|
||||
default:
|
||||
return continent.Names.English
|
||||
}
|
||||
}
|
||||
|
||||
func (c *City) country(country oschwaldGeoip2.CountryRecord) string {
|
||||
if !country.HasData() {
|
||||
return ""
|
||||
}
|
||||
|
||||
switch c.lang {
|
||||
case LanguageRussian:
|
||||
return country.Names.Russian
|
||||
case LanguageEnglish:
|
||||
return country.Names.English
|
||||
case LanguageBrazilianPortuguese:
|
||||
return country.Names.BrazilianPortuguese
|
||||
case LanguageSimplifiedChinese:
|
||||
return country.Names.SimplifiedChinese
|
||||
case LanguageJapanese:
|
||||
return country.Names.Japanese
|
||||
case LanguageGerman:
|
||||
return country.Names.German
|
||||
case LanguageFrench:
|
||||
return country.Names.French
|
||||
case LanguageSpanish:
|
||||
return country.Names.Spanish
|
||||
default:
|
||||
return country.Names.English
|
||||
}
|
||||
}
|
||||
|
||||
func (c *City) city(city oschwaldGeoip2.CityRecord) string {
|
||||
if !city.HasData() {
|
||||
return ""
|
||||
}
|
||||
|
||||
switch c.lang {
|
||||
case LanguageRussian:
|
||||
return city.Names.Russian
|
||||
case LanguageEnglish:
|
||||
return city.Names.English
|
||||
case LanguageBrazilianPortuguese:
|
||||
return city.Names.BrazilianPortuguese
|
||||
case LanguageSimplifiedChinese:
|
||||
return city.Names.SimplifiedChinese
|
||||
case LanguageJapanese:
|
||||
return city.Names.Japanese
|
||||
case LanguageGerman:
|
||||
return city.Names.German
|
||||
case LanguageFrench:
|
||||
return city.Names.French
|
||||
case LanguageSpanish:
|
||||
return city.Names.Spanish
|
||||
default:
|
||||
return city.Names.English
|
||||
}
|
||||
}
|
||||
|
||||
func (c *City) subdivisions(subdivisions []oschwaldGeoip2.CitySubdivision) []string {
|
||||
var citySubdivisions []string
|
||||
|
||||
if len(subdivisions) == 0 {
|
||||
return citySubdivisions
|
||||
}
|
||||
|
||||
for _, subdivision := range subdivisions {
|
||||
citySubdivisions = append(citySubdivisions, c.subdivision(subdivision))
|
||||
}
|
||||
|
||||
return citySubdivisions
|
||||
}
|
||||
|
||||
func (c *City) subdivision(subdivision oschwaldGeoip2.CitySubdivision) string {
|
||||
if !subdivision.HasData() {
|
||||
return ""
|
||||
}
|
||||
|
||||
switch c.lang {
|
||||
case LanguageRussian:
|
||||
return subdivision.Names.Russian
|
||||
case LanguageEnglish:
|
||||
return subdivision.Names.English
|
||||
case LanguageBrazilianPortuguese:
|
||||
return subdivision.Names.BrazilianPortuguese
|
||||
case LanguageSimplifiedChinese:
|
||||
return subdivision.Names.SimplifiedChinese
|
||||
case LanguageJapanese:
|
||||
return subdivision.Names.Japanese
|
||||
case LanguageGerman:
|
||||
return subdivision.Names.German
|
||||
case LanguageFrench:
|
||||
return subdivision.Names.French
|
||||
case LanguageSpanish:
|
||||
return subdivision.Names.Spanish
|
||||
default:
|
||||
return subdivision.Names.English
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
package mmdb
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// requestTimeout defines the maximum duration for request operations before timing out.
|
||||
requestTimeout = 60 * time.Second
|
||||
|
||||
// maxDownloadSize defines the maximum allowed size of the downloaded file in bytes.
|
||||
maxDownloadSize int64 = 200 << 20 // 200 MiB
|
||||
|
||||
// maxArchiveFileSize defines the maximum allowed size of the extracted file from ZIP in bytes.
|
||||
maxArchiveFileSize uint64 = 500 << 20 // 500 MiB
|
||||
)
|
||||
|
||||
// DownloadConfig defines the configuration for downloading MaxMind GeoIP2 database.
|
||||
type DownloadConfig struct {
|
||||
// RequestTimeout defines the maximum duration for request operations before timing out.
|
||||
RequestTimeout time.Duration
|
||||
// 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
|
||||
}
|
||||
|
||||
// DefaultDownloadConfig returns the default configuration for downloading MaxMind GeoIP2 database.
|
||||
func DefaultDownloadConfig() DownloadConfig {
|
||||
return DownloadConfig{
|
||||
RequestTimeout: requestTimeout,
|
||||
MaxDownloadSize: maxDownloadSize,
|
||||
MaxArchiveFileSize: maxArchiveFileSize,
|
||||
}
|
||||
}
|
||||
|
||||
// Download is a struct for downloading MaxMind GeoIP2 database.
|
||||
type Download struct {
|
||||
// url is a URL for downloading MaxMind GeoIP2 database.
|
||||
url string
|
||||
// username is a username for downloading MaxMind GeoIP2 database.
|
||||
username string
|
||||
// password is a password for downloading MaxMind GeoIP2 database.
|
||||
password string
|
||||
// Cfg is a configuration for downloading MaxMind GeoIP2 database.
|
||||
Cfg DownloadConfig
|
||||
}
|
||||
|
||||
// NewDownload creates a new Download struct.
|
||||
// @param downloadURL - a URL for downloading MaxMind GeoIP2 database
|
||||
// @param username - a username for downloading MaxMind GeoIP2 database
|
||||
// @param password - a password for downloading MaxMind GeoIP2 database
|
||||
// @param cfg - a configuration for downloading MaxMind GeoIP2 database
|
||||
// @return (*Download, error) - a Download struct and an error if the download URL is invalid
|
||||
func NewDownload(downloadURL, username, password string, cfg DownloadConfig) (*Download, error) {
|
||||
parsedURL, err := url.Parse(downloadURL)
|
||||
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)
|
||||
}
|
||||
|
||||
return &Download{
|
||||
url: downloadURL,
|
||||
username: username,
|
||||
password: password,
|
||||
Cfg: cfg,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Download downloads MaxMind GeoIP2 database.
|
||||
// @param dir - a directory to save the downloaded database
|
||||
// @param ctx - a context for cancelling the download
|
||||
// @return error - an error if the download failed
|
||||
func (d *Download) Download(dir string, ctx context.Context) error {
|
||||
if err := createDir(dir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, d.url, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
req.SetBasicAuth(d.username, d.password)
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: d.Cfg.RequestTimeout,
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("download archive: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("download archive: unexpected status %s", resp.Status)
|
||||
}
|
||||
|
||||
filename := rand.Text() + ".tar.gz"
|
||||
archivePath := filepath.Join(dir, filename)
|
||||
file, err := os.Create(archivePath)
|
||||
if err != nil {
|
||||
if os.IsExist(err) {
|
||||
return fmt.Errorf("archive file already exists: %w", err)
|
||||
}
|
||||
return fmt.Errorf("create archive file: %w", err)
|
||||
}
|
||||
|
||||
limitedReader := io.LimitReader(resp.Body, d.Cfg.MaxDownloadSize+1)
|
||||
written, copyErr := io.Copy(file, limitedReader)
|
||||
closeErr := file.Close()
|
||||
if copyErr != nil {
|
||||
if removeErr := os.Remove(archivePath); removeErr != nil {
|
||||
return fmt.Errorf("remove archive: %w", removeErr)
|
||||
}
|
||||
return fmt.Errorf("save archive: %w", copyErr)
|
||||
}
|
||||
if closeErr != nil {
|
||||
if removeErr := os.Remove(archivePath); removeErr != nil {
|
||||
return fmt.Errorf("remove archive: %w", removeErr)
|
||||
}
|
||||
return fmt.Errorf("close archive file: %w", closeErr)
|
||||
}
|
||||
if written > d.Cfg.MaxDownloadSize {
|
||||
if removeErr := os.Remove(archivePath); removeErr != nil {
|
||||
return fmt.Errorf("remove archive: %w", removeErr)
|
||||
}
|
||||
return fmt.Errorf("download archive too large: %d bytes", written)
|
||||
}
|
||||
|
||||
if err := extractTarGz(archivePath, dir, d.Cfg.MaxArchiveFileSize); err != nil {
|
||||
if removeErr := os.Remove(archivePath); removeErr != nil {
|
||||
return fmt.Errorf("remove archive: %w", removeErr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if removeErr := os.Remove(archivePath); removeErr != nil {
|
||||
return fmt.Errorf("remove archive: %w", removeErr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createDir creates a directory if it does not exist.
|
||||
// @param dir - a directory to create
|
||||
// @return error - an error if the directory could not be created
|
||||
func createDir(dir string) error {
|
||||
if err := os.MkdirAll(dir, 0750); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// extractTarGz extracts a tar.gz archive to a specified directory.
|
||||
// @param archivePath - path to the archive to extract
|
||||
// @param dir - path to the directory to extract the archive to
|
||||
// @param maxExtractSize - maximum size of the extracted archive in bytes
|
||||
// @return error - an error if the archive could not be extracted
|
||||
func extractTarGz(archivePath string, dir string, maxExtractSize uint64) error {
|
||||
file, err := os.Open(archivePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open archive: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = file.Close()
|
||||
}()
|
||||
|
||||
gzr, err := gzip.NewReader(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create gzip reader: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = gzr.Close()
|
||||
}()
|
||||
|
||||
tr := tar.NewReader(gzr)
|
||||
|
||||
var extractedSize uint64
|
||||
|
||||
for {
|
||||
header, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("read tar archive: %w", err)
|
||||
}
|
||||
|
||||
targetPath := filepath.Join(dir, header.Name)
|
||||
|
||||
switch header.Typeflag {
|
||||
case tar.TypeDir:
|
||||
if err := os.MkdirAll(targetPath, os.FileMode(header.Mode)); err != nil {
|
||||
return fmt.Errorf("create directory %q: %w", targetPath, err)
|
||||
}
|
||||
|
||||
case tar.TypeReg:
|
||||
if err := os.MkdirAll(filepath.Dir(targetPath), 0750); err != nil {
|
||||
return fmt.Errorf("create parent directory %q: %w", targetPath, err)
|
||||
}
|
||||
|
||||
outFile, err := os.OpenFile(targetPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(header.Mode))
|
||||
if err != nil {
|
||||
return fmt.Errorf("create file %q: %w", targetPath, err)
|
||||
}
|
||||
|
||||
n, err := io.Copy(outFile, tr)
|
||||
closeErr := outFile.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("write file %q: %w", targetPath, err)
|
||||
}
|
||||
if closeErr != nil {
|
||||
return fmt.Errorf("close file %q: %w", targetPath, closeErr)
|
||||
}
|
||||
|
||||
extractedSize += uint64(n)
|
||||
if extractedSize > maxExtractSize {
|
||||
return fmt.Errorf("extracted archive too large: %d bytes", extractedSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package mmdb
|
||||
|
||||
type language string
|
||||
|
||||
const (
|
||||
LanguageRussian = language("Russian")
|
||||
LanguageEnglish = language("English")
|
||||
LanguageSpanish = language("Spanish")
|
||||
LanguageFrench = language("French")
|
||||
LanguageGerman = language("German")
|
||||
LanguageJapanese = language("Japanese")
|
||||
|
||||
LanguageBrazilianPortuguese = language("BrazilianPortuguese")
|
||||
LanguageSimplifiedChinese = language("SimplifiedChinese")
|
||||
)
|
||||
Reference in New Issue
Block a user