latest
This commit is contained in:
168
geolocate/geolocate.go
Normal file
168
geolocate/geolocate.go
Normal file
@@ -0,0 +1,168 @@
|
||||
// Command geolocate estimates the machine's physical location by scanning
|
||||
// nearby Wi-Fi access points and submitting their BSSIDs to a public
|
||||
// Wi-Fi-to-location service, then records the result under ~/.data/location.
|
||||
//
|
||||
// This is a macOS port of the original Linux script. Two things changed since
|
||||
// the original was written:
|
||||
//
|
||||
// - The original scanned access points with nmcli (NetworkManager). macOS
|
||||
// has no equivalent, and Apple removed the `airport` CLI in macOS 14.4.
|
||||
// On macOS 26, CoreWLAN only returns real BSSIDs to a Developer-ID-signed,
|
||||
// notarized binary that the user has granted Location Services access. We
|
||||
// get that via the macwifi package, which ships such a signed helper.
|
||||
//
|
||||
// - The original queried Mozilla Location Services, which shut down in 2024.
|
||||
// We use BeaconDB instead: a community-run successor that speaks the same
|
||||
// Ichnaea API (identical request/response shape).
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/jaisonerick/macwifi"
|
||||
)
|
||||
|
||||
// geolocateURL is BeaconDB's Ichnaea-compatible geolocate endpoint, the
|
||||
// drop-in replacement for the defunct location.services.mozilla.com.
|
||||
const geolocateURL = "https://api.beacondb.net/v1/geolocate"
|
||||
|
||||
// accessPoint is one observed Wi-Fi access point in the Ichnaea request
|
||||
// schema. Only macAddress is required; signal strength and channel improve
|
||||
// the position estimate when available.
|
||||
type accessPoint struct {
|
||||
MacAddress string `json:"macAddress"`
|
||||
SignalStrength int `json:"signalStrength,omitempty"`
|
||||
Channel int `json:"channel,omitempty"`
|
||||
}
|
||||
|
||||
type geolocateRequest struct {
|
||||
WifiAccessPoints []accessPoint `json:"wifiAccessPoints,omitempty"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := run(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "geolocate:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run() error {
|
||||
ts := time.Now().Unix()
|
||||
|
||||
// The first scan triggers macOS's Location Services prompt for the
|
||||
// bundled helper; allow generous time for a user to approve it.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Wi-Fi scanning requires Location Services. If it is disabled (or the
|
||||
// scan otherwise fails), fall back to IP-based geolocation rather than
|
||||
// aborting — matching the original, which posted an empty request when
|
||||
// it couldn't find enough access points.
|
||||
aps, err := listAccessPoints(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr,
|
||||
"geolocate: wifi scan unavailable (%v); falling back to IP geolocation\n", err)
|
||||
aps = nil
|
||||
}
|
||||
|
||||
loc, err := geolocate(aps)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
loc["timestamp"] = ts
|
||||
|
||||
out, err := json.Marshal(loc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dir := filepath.Join(home, ".data", "location")
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := writeFile(filepath.Join(dir, "latest.json"), out); err != nil {
|
||||
return err
|
||||
}
|
||||
return writeFile(filepath.Join(dir, fmt.Sprintf("%d.json", ts)), out)
|
||||
}
|
||||
|
||||
func writeFile(path string, content []byte) error {
|
||||
return os.WriteFile(path, content, 0o644)
|
||||
}
|
||||
|
||||
// listAccessPoints scans nearby Wi-Fi networks via CoreWLAN (through the
|
||||
// macwifi helper) and returns their BSSIDs. It returns nil if fewer than two
|
||||
// usable access points are found, mirroring the original script: the
|
||||
// geolocation service needs at least two observations to triangulate.
|
||||
func listAccessPoints(ctx context.Context) ([]accessPoint, error) {
|
||||
networks, err := macwifi.Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scanning wifi: %w", err)
|
||||
}
|
||||
|
||||
seen := make(map[string]bool)
|
||||
var aps []accessPoint
|
||||
for _, n := range networks {
|
||||
// BSSID is empty when Location Services access has not been
|
||||
// granted, or for saved-but-not-visible networks.
|
||||
if n.BSSID == "" || seen[n.BSSID] {
|
||||
continue
|
||||
}
|
||||
seen[n.BSSID] = true
|
||||
aps = append(aps, accessPoint{
|
||||
MacAddress: n.BSSID,
|
||||
SignalStrength: n.RSSI,
|
||||
Channel: n.Channel,
|
||||
})
|
||||
}
|
||||
|
||||
if len(aps) < 2 {
|
||||
return nil, nil
|
||||
}
|
||||
return aps, nil
|
||||
}
|
||||
|
||||
// geolocate posts the observed access points to BeaconDB and returns the
|
||||
// decoded response (e.g. {"location":{"lat":..,"lng":..},"accuracy":..}).
|
||||
// With no access points it degrades to a GeoIP estimate, matching the
|
||||
// original behaviour of posting an empty request.
|
||||
func geolocate(aps []accessPoint) (map[string]any, error) {
|
||||
body, err := json.Marshal(geolocateRequest{WifiAccessPoints: aps})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := http.Post(geolocateURL, "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("geolocation service returned %s: %s",
|
||||
resp.Status, bytes.TrimSpace(data))
|
||||
}
|
||||
|
||||
var result map[string]any
|
||||
if err := json.Unmarshal(data, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
5
geolocate/go.mod
Normal file
5
geolocate/go.mod
Normal file
@@ -0,0 +1,5 @@
|
||||
module sneak.berlin/go/geolocate
|
||||
|
||||
go 1.26.1
|
||||
|
||||
require github.com/jaisonerick/macwifi v1.0.0
|
||||
2
geolocate/go.sum
Normal file
2
geolocate/go.sum
Normal file
@@ -0,0 +1,2 @@
|
||||
github.com/jaisonerick/macwifi v1.0.0 h1:Jy86C7BBR/J7u1cxFWOWi+YlwYjiMMjYLUt6RGFD1Mc=
|
||||
github.com/jaisonerick/macwifi v1.0.0/go.mod h1:AtCf/s2Y4izU7IYcBSPoGe2lp2Oj53T7rz9D08uqSRM=
|
||||
Reference in New Issue
Block a user