package device import ( "crypto/rand" "encoding/hex" "io" "log" "net" "net/http" "strings" "git.autistici.org/id/auth" "github.com/gorilla/sessions" "github.com/mssola/user_agent" ) func randomDeviceID() string { b := make([]byte, 8) if _, err := io.ReadFull(rand.Reader, b[:]); err != nil { panic(err) } return hex.EncodeToString(b) } // Manager can provide DeviceInfo entries for incoming HTTP requests. type Manager struct { store sessions.Store geodb *geoIPDb } // Config stores options for the device info manager. type Config struct { AuthKey string `yaml:"auth_key"` GeoIPDataFiles []string `yaml:"geo_ip_data_files"` } // New returns a new Manager with the given configuration. func New(config *Config) (*Manager, error) { if config == nil { config = &Config{} } geodb, err := newGeoIP(config.GeoIPDataFiles) if err != nil { log.Printf("Warning: GeoIP disabled: %v", err) } return &Manager{ geodb: geodb, store: newStore([]byte(config.AuthKey)), }, nil } const deviceIDSessionName = "_dev" // GetDeviceInfoFromRequest will retrieve or create a DeviceInfo // object for the given request. It will always return a valid object. // The ResponseWriter is needed to store the unique ID on the client // when a new device info object is created. func (m *Manager) GetDeviceInfoFromRequest(w http.ResponseWriter, req *http.Request) *auth.DeviceInfo { session, _ := m.store.Get(req, deviceIDSessionName) devID, ok := session.Values["id"].(string) if !ok || devID == "" { // Generate a new Device ID and save it on the client. devID = randomDeviceID() session.Values["id"] = devID if err := session.Save(req, w); err != nil { // This is likely a misconfiguration issue, so // we want to know about it. log.Printf("error saving device manager session: %v", err) } } uaStr := req.UserAgent() ua := user_agent.New(uaStr) browser, _ := ua.Browser() d := auth.DeviceInfo{ ID: devID, UserAgent: uaStr, Mobile: ua.Mobile(), OS: ua.OS(), Browser: browser, } // Special check for .onion HTTP hosts - sets the zone to // "onion" and skips the IP-based checks. if req.Host != "" && strings.HasSuffix(req.Host, ".onion") { d.RemoteZone = "onion" } else { // If we have an IP address, we can add more information. if ip := getIPFromRequest(req); ip != nil { d.RemoteAddr = ip.String() // Country lookup. if m.geodb != nil { if zone, err := m.geodb.getZoneForIP(ip); err == nil { d.RemoteZone = zone } } } } return &d } func getIPFromRequest(req *http.Request) net.IP { // Parse the RemoteAddr Request field, for starters. host, _, err := net.SplitHostPort(req.RemoteAddr) if err != nil { host = req.RemoteAddr } return net.ParseIP(host) }