/** * Standalone signaling server for the Nextcloud Spreed app. * Copyright (C) 2019 struktur AG * * @author Joachim Bauch * * @license GNU AGPL version 3 or any later version * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package signaling import ( "archive/tar" "compress/gzip" "fmt" "io" "io/ioutil" "log" "net" "net/http" "net/url" "os" "strings" "sync" "time" "github.com/oschwald/maxminddb-golang" ) var ( ErrDatabaseNotInitialized = fmt.Errorf("GeoIP database not initialized yet") ) func GetGeoIpDownloadUrl(license string) string { if license == "" { return "" } result := "https://download.maxmind.com/app/geoip_download" result += "?edition_id=GeoLite2-Country" result += "&license_key=" + url.QueryEscape(license) result += "&suffix=tar.gz" return result } type GeoLookup struct { url string isFile bool client http.Client mu sync.Mutex lastModifiedHeader string lastModifiedTime time.Time reader *maxminddb.Reader } func NewGeoLookupFromUrl(url string) (*GeoLookup, error) { geoip := &GeoLookup{ url: url, } return geoip, nil } func NewGeoLookupFromFile(filename string) (*GeoLookup, error) { geoip := &GeoLookup{ url: filename, isFile: true, } if err := geoip.Update(); err != nil { geoip.Close() return nil, err } return geoip, nil } func (g *GeoLookup) Close() { g.mu.Lock() if g.reader != nil { g.reader.Close() g.reader = nil } g.mu.Unlock() } func (g *GeoLookup) Update() error { if g.isFile { return g.updateFile() } else { return g.updateUrl() } } func (g *GeoLookup) updateFile() error { info, err := os.Stat(g.url) if err != nil { return err } if info.ModTime().Equal(g.lastModifiedTime) { return nil } reader, err := maxminddb.Open(g.url) if err != nil { return err } if err := reader.Verify(); err != nil { return err } metadata := reader.Metadata log.Printf("Using %s GeoIP database from %s (built on %s)", metadata.DatabaseType, g.url, time.Unix(int64(metadata.BuildEpoch), 0).UTC()) g.mu.Lock() if g.reader != nil { g.reader.Close() } g.reader = reader g.lastModifiedTime = info.ModTime() g.mu.Unlock() return nil } func (g *GeoLookup) updateUrl() error { request, err := http.NewRequest("GET", g.url, nil) if err != nil { return err } if g.lastModifiedHeader != "" { request.Header.Add("If-Modified-Since", g.lastModifiedHeader) } response, err := g.client.Do(request) if err != nil { return err } defer response.Body.Close() if response.StatusCode == http.StatusNotModified { log.Printf("GeoIP database at %s has not changed", g.url) return nil } else if response.StatusCode/100 != 2 { return fmt.Errorf("downloading %s returned an error: %s", g.url, response.Status) } body := response.Body if strings.HasSuffix(g.url, ".gz") { body, err = gzip.NewReader(body) if err != nil { return err } } tarfile := tar.NewReader(body) var geoipdata []byte for { header, err := tarfile.Next() if err == io.EOF { break } else if err != nil { return err } if !strings.HasSuffix(header.Name, ".mmdb") { continue } geoipdata, err = ioutil.ReadAll(tarfile) if err != nil { return err } break } if len(geoipdata) == 0 { return fmt.Errorf("did not find MaxMind database in tarball from %s", g.url) } reader, err := maxminddb.FromBytes(geoipdata) if err != nil { return err } if err := reader.Verify(); err != nil { return err } metadata := reader.Metadata log.Printf("Using %s GeoIP database from %s (built on %s)", metadata.DatabaseType, g.url, time.Unix(int64(metadata.BuildEpoch), 0).UTC()) g.mu.Lock() if g.reader != nil { g.reader.Close() } g.reader = reader g.lastModifiedHeader = response.Header.Get("Last-Modified") g.mu.Unlock() return nil } func (g *GeoLookup) LookupCountry(ip net.IP) (string, error) { var record struct { Country struct { ISOCode string `maxminddb:"iso_code"` } `maxminddb:"country"` } g.mu.Lock() if g.reader == nil { g.mu.Unlock() return "", ErrDatabaseNotInitialized } err := g.reader.Lookup(ip, &record) g.mu.Unlock() if err != nil { return "", err } return record.Country.ISOCode, nil } func LookupContinents(country string) []string { if continents, found := ContinentMap[country]; !found { return nil } else { return continents } } func IsValidContinent(continent string) bool { switch continent { case "AF": // Africa fallthrough case "AN": // Antartica fallthrough case "AS": // Asia fallthrough case "EU": // Europe fallthrough case "NA": // North America fallthrough case "SA": // South America fallthrough case "OC": // Oceania return true default: return false } }