Commit daf84b84 authored by James Westman's avatar James Westman

Initial commit

parents
.DS_Store
thumbs.db
__pycache__
This diff is collapsed.
# GeoClueless
A location spoofer for GeoClue2
## What it does
GeoClueless allows you to fake your computer's geographical location for
debugging purposes. While it is running, applications that use GeoClue can
be fed coordinates of your choosing rather than your actual location.
One of the most useful features is GPX track replaying. This allows you to
record a track on your phone with an app like OsmAnd, then copy it to your
computer and replay it in real time. This way, you can pretend to drive the
same route over and over again without leaving your chair.
## How it works
GeoClueless implements the GeoClue D-Bus API, but with debug data instead of
real data. When you run it, it kills the existing GeoClue process if necessary,
then sets itself up on the `org.freedesktop.GeoClue2` name.
## Requirements/Installation
- Python 3
- [PyGObject](https://pypi.org/project/PyGObject/) (probably also available
through your distro's package repositories)
To install, simply clone this repository with git. Then run the program with
`sudo ./geoclueless.py`.
## Usage
### Data Sources
You can manually input coordinates, source them from a CSV file, or replay a
GPX track file.
#### Manually
`sudo ./geoclueless.py coords <lat> <lon>`
Other options:
- `--altitude`: The altitude (meters) of the point
- `--accuracy`: The accuracy (meters) to report
- `--heading`: The heading to report, in degrees clockwise from due north
#### CSV
`sudo ./geoclueless.py csv <file>`
Other options:
- `--rate`: Number of points per second
#### GPX
`sudo ./geoclueless.py gpx <file>`
Other options:
- `--speed`: The speed at which to replay the track. 1 is real time, .5 is half
speed, etc.
## Notes
- For CSV and GPX options, replay will not start until at least one client has
connected on D-Bus.
- You must run GeoClueless with `sudo` so it can register itself the system bus.
- GeoClueless does not perfectly replicate the behavior of GeoClue. In
particular, it ignores the authorization and accuracy scrambling features of
GeoClue. If you need these features emulated in GeoClueless, please file an
issue or (preferably) a merge request.
- So far, I've only tested the GPX feature with tracks generated in OsmAnd. If
you have a different app and it doesn't work, please file an issue.
## License
GeoClueless is licensed under the GNU General Public License, version 3 or
later. See COPYING.
GeoClueless contains a few files adapted from the original GeoClue. See
geoclueless/interfaces/README.md.
Untangle is a simple XML parsing library for Python. Its source code has been
copied to geoclueless/untangle.py. Untangle is licensed under the MIT license
and its source code is at https://github.com/stchris/untangle.
#!/usr/bin/env python3
from geoclueless import main
main.main()
from gi.repository import GLib, Gio, Geoclue
from geoclueless.interfaces.client import CLIENT
from geoclueless.dbus_location import DBusLocation
from geoclueless.main import BUS_NAME
class DBusClient:
def __init__(self, manager, sender, num):
self.nodeinfo = Gio.DBusNodeInfo.new_for_xml(CLIENT)
self.manager = manager
self.sender = sender
self.num = num
self.path = "/org/freedesktop/GeoClue2/Client/" + str(num)
self.distance_threshold = 0
self.time_threshold = 0
self.active = False
self.location = None
self.old_location = None
self.registration_id = 0
def register(self, conn):
self.conn = conn
self.registration_id = conn.register_object(
interface_info=self.nodeinfo.interfaces[0],
object_path=self.path,
method_call_closure=self.on_method_call,
get_property_closure=self.on_get_property,
set_property_closure=self.on_set_property
)
def unregister(self):
self.conn.unregister_object(self.registration_id)
self.location.unregister(self.conn)
def update(self, location):
old_location = self.location
new_location = DBusLocation(location)
new_location.register(self.conn)
if self.old_location is not None: self.old_location.unregister(self.conn)
self.location = new_location
self.old_location = old_location
self.conn.emit_signal(
None, self.path, "org.freedesktop.GeoClue2.Client",
"LocationUpdated",
GLib.Variant("(oo)",
(old_location.path if old_location is not None else "/",
new_location.path)
)
)
def on_method_call(self, conn, sender, obj_path, interface, method, params, invocation):
if method == "Start":
self.active = True
if self.manager.current_location is not None:
self.update(self.manager.current_location)
elif method == "Stop":
self.active = False
invocation.return_value(None)
def on_get_property(self, conn, sender, obj_path, interface, property):
if property == "Location":
if self.location is None:
return GLib.Variant("o", "/")
else:
return GLib.Variant("o", self.location.path)
elif property == "DistanceThreshold":
return GLib.Variant("u", self.distance_threshold)
elif property == "TimeThreshold":
return GLib.Variant("u", self.distance_threshold)
elif property == "DesktopId":
return GLib.Variant("s", "")
elif property == "RequestedAccuracyLevel":
return GLib.Variant("u", Geoclue.AccuracyLevel.EXACT)
elif property == "Active":
return GLib.Variant("b", True)
def on_set_property(self, *args):
return True # yeah sure whatever
import time, math
from gi.repository import GLib, Gio
from geoclueless.interfaces.location import LOCATION
num = 0
class DBusLocation:
def __init__(self, location):
global num
self.nodeinfo = Gio.DBusNodeInfo.new_for_xml(LOCATION)
self.num = num
num += 1
self.path = "/org/freedesktop/GeoClue2/Location/" + str(self.num)
self.location = location
t = time.time()
self.timestamp = (math.floor(t), t % 1)
self.registration_id = 0
def register(self, conn):
self.registration_id = conn.register_object(
interface_info=self.nodeinfo.interfaces[0],
object_path=self.path,
method_call_closure=self.on_method_call,
get_property_closure=self.on_get_property,
set_property_closure=self.on_set_property
)
def unregister(self, conn):
conn.unregister_object(self.registration_id)
def on_method_call(self, conn, sender, obj_path, interface, method, params, invocation):
pass
def on_get_property(self, conn, sender, obj_path, interface, property):
if property == "Latitude":
return GLib.Variant("d", self.location.lat)
elif property == "Longitude":
return GLib.Variant("d", self.location.lon)
elif property == "Accuracy":
return GLib.Variant("d", self.location.accuracy)
elif property == "Altitude":
return GLib.Variant("d", self.location.altitude)
elif property == "Speed":
return GLib.Variant("d", self.location.speed)
elif property == "Heading":
return GLib.Variant("d", self.location.heading)
elif property == "Description":
return GLib.Variant("s", "")
elif property == "Timestamp":
return GLib.Variant("(tt)", self.timestamp)
def on_set_property(self, *args):
return True
from gi.repository import GLib, Gio, Geoclue
from geoclueless.interfaces.manager import MANAGER
from geoclueless.dbus_client import DBusClient
from geoclueless import providers
from geoclueless.main import get_args
class DBusManager:
def __init__(self):
self.nodeinfo = Gio.DBusNodeInfo.new_for_xml(MANAGER)
self.client_num = 0
self.clients = {}
self.provider = iter(providers.get_provider())
self.current_location = None
self.quiet = get_args().quiet
self.in_use = False
def register(self, conn):
conn.register_object(
interface_info=self.nodeinfo.interfaces[0],
object_path="/org/freedesktop/GeoClue2/Manager",
method_call_closure=self.on_method_call,
get_property_closure=self.on_get_property,
set_property_closure=self.on_set_property
)
def on_method_call(self, conn, sender, obj_path, interface, method, params, invocation):
if method == "AddAgent":
invocation.return_value(None)
elif method == "CreateClient":
client = self.create_client(conn, sender)
invocation.return_value(GLib.Variant("(o)", (client.path,)))
elif method == "GetClient":
for client in self.clients.values():
if client.sender == sender:
invocation.return_value(GLib.Variant("(o)", (client.path,)))
client = self.create_client(conn, sender)
invocation.return_value(GLib.Variant("(o)", (client.path,)))
elif method == "DeleteClient":
client = self.clients.get(params[0])
if client is not None:
client.unregister()
self.clients.pop(params[0])
invocation.return_value(None)
def on_get_property(self, conn, sender, obj_path, interface, property):
if property == "InUse":
return GLib.Variant("b", self.in_use)
elif property == "AvailableAccuracyLevel":
return GLib.Variant("u", Geoclue.AccuracyLevel.EXACT)
def on_set_property(self, *args):
return True
def create_client(self, conn, sender):
client = DBusClient(self, sender, self.client_num)
client.register(conn)
self.client_num += 1
self.clients[client.path] = client
if not self.in_use:
self.in_use = True
self.next_location()
return client
def next_location(self):
location = None
def on_timeout():
if not self.quiet: print(location)
self.current_location = location
for client in self.clients.values():
client.update(location)
self.next_location()
try:
location = next(self.provider)
GLib.timeout_add(int(location.delay * 1000), on_timeout)
except StopIteration:
pass
# GeoClue DBus Interfaces
These XML files have been taken from the GeoClue source code at
https://gitlab.freedesktop.org/geoclue/geoclue/tree/master/interface.
GeoClue is licensed under the GPL version 2 or later.
CLIENT = """<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<!--
GeoClue 2.0 Interface Specification
Copyright 2013 Red Hat, Inc.
-->
<node>
<!--
org.freedesktop.GeoClue2.Client:
@short_description: The Application-specific client API
This is the interface you use to retrieve location information and receive
location update signals from GeoClue service. You get the client object to
use this interface on from
<link linkend="gdbus-method-org-freedesktop-GeoClue2-Manager.GetClient">
org.freedesktop.GeoClue2.Manager.GetClient()</link> method.
-->
<interface name="org.freedesktop.GeoClue2.Client">
<!--
Location:
Current location as path to a
<link linkend="gdbus-org.freedesktop.GeoClue2.Location">
org.freedesktop.GeoClue2.Location</link> object. Please note that this
property will be set to "/" (D-Bus equivalent of null) initially, until
Geoclue finds user's location. You want to delay reading this property
until your callback to #org.freedesktop.GeoClue2.Client::LocationUpdated
signal is called for the first time after starting the client.
-->
<property name="Location" type="o" access="read"/>
<!--
DistanceThreshold:
Contains the current distance threshold in meters. This value is used
by the service when it gets new location info. If the distance moved is
below the threshold, it won't emit the LocationUpdated signal.
The default value is 0. When TimeThreshold is zero, it always emits
the signal.
-->
<property name="DistanceThreshold" type="u" access="readwrite">
<annotation name="org.freedesktop.Accounts.DefaultValue" value="0"/>
</property>
<!--
TimeThreshold:
Contains the current time threshold in seconds. This value is used
by the service when it gets new location info. If the time since the
last update is below the threshold, it won't emit the LocationUpdated
signal. The default value is 0. When TimeThreshold is zero, it always
emits the signal.
-->
<property name="TimeThreshold" type="u" access="readwrite">
<annotation name="org.freedesktop.Accounts.DefaultValue" value="0"/>
</property>
<!--
DesktopId:
The desktop file id (the basename of the desktop file). This property
must be set by applications for authorization to work.
-->
<property name="DesktopId" type="s" access="readwrite"/>
<!--
RequestedAccuracyLevel:
The level of accuracy requested by client, as
<link linkend="GClueAccuracyLevel">GClueAccuracyLevel</link>.
Please keep in mind that the actual accuracy of location information is
dependent on available hardware on your machine, external resources
and/or how much accuracy user agrees to be confortable with.
-->
<property name="RequestedAccuracyLevel" type="u" access="readwrite"/>
<!--
Active:
If client is active, i-e started successfully using
org.freedesktop.GeoClue2.Client.Start() and receiving location updates.
Please keep in mind that geoclue can at any time stop and start the
client on user (agent) request. Applications that are interested in
in these changes, should watch for changes in this property.
-->
<property name="Active" type="b" access="read"/>
<!--
Start:
Start receiving events about the current location. Applications should
hook-up to #org.freedesktop.GeoClue2.Client::LocationUpdated signal
before calling this method.
-->
<method name="Start"/>
<!--
Stop:
Stop receiving events about the current location.
-->
<method name="Stop"/>
<!--
LocationUpdated:
@old: old location as path to a #org.freedesktop.GeoClue2.Location object
@new: new location as path to a #org.freedesktop.GeoClue2.Location object
The signal is emitted every time the location changes.
The client should set the DistanceThreshold property to control how
often this signal is emitted.
-->
<signal name="LocationUpdated">
<arg name="old" type="o"/>
<arg name="new" type="o"/>
</signal>
</interface>
</node>
"""
LOCATION = """<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<!--
GeoClue 2.0 Interface Specification
Copyright 2013 Red Hat, Inc.
-->
<node>
<!--
org.freedesktop.GeoClue2.Location:
@short_description: The Location interface
This is the interface you use on location objects.
-->
<interface name="org.freedesktop.GeoClue2.Location">
<!--
Latitude:
The latitude of the location, in degrees.
-->
<property name="Latitude" type="d" access="read"/>
<!--
Longitude:
The longitude of the location, in degrees.
-->
<property name="Longitude" type="d" access="read"/>
<!--
Accuracy:
The accuracy of the location fix, in meters.
-->
<property name="Accuracy" type="d" access="read"/>
<!--
Altitude:
The altitude of the location fix, in meters. When unknown, its set to
minimum double value, -1.7976931348623157e+308.
-->
<property name="Altitude" type="d" access="read"/>
<!--
Speed:
The speed in meters per second. When unknown, it's set to -1.0.
-->
<property name="Speed" type="d" access="read"/>
<!--
Heading:
The heading direction in degrees with respect to North direction, in
clockwise order. That means North becomes 0 degree, East: 90 degrees,
South: 180 degrees, West: 270 degrees and so on. When unknown,
it's set to -1.0.
-->
<property name="Heading" type="d" access="read"/>
<!--
Description:
A human-readable description of the location, if available.
WARNING: Applications should not rely on this property since not all
sources provide a description. If you really need a description (or
more details) about current location, use a reverse-geocoding API, e.g
geocode-glib.
-->
<property name="Description" type="s" access="read"/>
<!--
Timestamp:
The timestamp when the location was determined, in seconds and
microseconds since the Epoch. This is the time of measurement if the
backend provided that information, otherwise the time when GeoClue
received the new location.
Note that GeoClue can't guarantee that the timestamp will always
monotonically increase, as a backend may not respect that.
Also note that a timestamp can be very old, e.g. because of a cached
location.
-->
<property name="Timestamp" type="(tt)" access="read"/>
</interface>
</node>
"""
MANAGER = """<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<!--
GeoClue 2.0 Interface Specification
Copyright 2013 Red Hat, Inc.
-->
<node>
<!--
org.freedesktop.GeoClue2.Manager:
@short_description: The GeoClue service manager
This is the interface you use to talk to main GeoClue2 manager object at
path "/org/freedesktop/GeoClue2/Manager". The only thing you do with this
interface is to call org.freedesktop.GeoClue2.Manager.GetClient() or
org.freedesktop.GeoClue2.Manager.CreateClient() on it to get your
application specific client object(s).
-->
<interface name="org.freedesktop.GeoClue2.Manager">
<!--
InUse:
Whether service is currently is use by any application.
-->
<property name="InUse" type="b" access="read"/>
<!--
AvailableAccuracyLevel:
The level of available accuracy, as
<link linkend="GClueAccuracyLevel">GClueAccuracyLevel</link>.
-->
<property name="AvailableAccuracyLevel" type="u" access="read"/>
<!--
GetClient:
@client: The path for the client object
Retrieves a client object which can only be used by the calling
application only. On the first call from a specific D-Bus peer, this
method will create the client object but subsequent calls will return
the path of the existing client.
-->
<method name="GetClient">
<arg name="client" type="o" direction="out"/>
</method>
<!--
CreateClient:
@client: The path for the newly created client object
Creates and retrieves a client object which can only be used by the
calling application only. Unlike
org.freedesktop.GeoClue2.Manager.GetClient(), thid method always
creates a new client.
-->
<method name="CreateClient">
<arg name="client" type="o" direction="out"/>
</method>
<!--
DeleteClient:
@client: The path of the client object to delete
Use this method to explicitly destroy a client, created using
org.freedesktop.GeoClue2.Manager.GetClient() or
org.freedesktop.GeoClue2.Manager.CreateClient().
Long-running applications, should either use this to delete associated
client(s) when not needed, or disconnect from the D-Bus connection used
for communicating with Geoclue (which is implicit on client process
termination).
-->
<method name="DeleteClient">
<arg name="client" type="o" direction="in"/>
</method>
<!--
AddAgent:
@id: The Desktop ID (excluding .desktop) of the agent
An API for user authorization agents to register themselves. Each agent
is responsible for the user it is running as. Application developers
can and should simply ignore this API.
-->
<method name="AddAgent">
<arg name="id" type="s" direction="in"/>
</method>
</interface>
</node>
"""
import sys
class Location:
def __init__(self, lat, lon, accuracy=0, altitude=sys.float_info.min,
speed=-1, heading=-1, delay=0):
self.lat = lat
self.lon = lon
self.accuracy = accuracy
self.altitude = altitude
self.speed = speed
self.heading = heading
self.delay = delay
@staticmethod
def from_args(args):
return Location(args.latitude, args.longitude, 0)
def __str__(self):
return "{}, {}".format(self.lat, self.lon)
import subprocess, argparse
import gi
gi.require_version("Geoclue", "2.0")
from gi.repository import Gio, GLib
BUS_NAME = "org.freedesktop.GeoClue2"
ARGS = None
def get_args():
return ARGS
from geoclueless.dbus_manager import DBusManager
def main():
collect_args()
# Kill any existing GeoClue process
kill_existing_geoclue()
# Start the main loop
GLib.MainLoop.new(None, True).run()
def collect_args():
global ARGS
parser = argparse.ArgumentParser(description="Spoof your GPS location")
subparsers = parser.add_subparsers(help="data sources", required=True, metavar="{coords|csv|gpx}", dest="datasource")
parser.add_argument("-q", "--quiet", action="store_true", help="Do not output coordinates as they are replayed")
coords = subparsers.add_parser("coords", description="Set location to a specific point")
coords.add_argument("latitude", type=float)
coords.add_argument("longitude", type=float)
coords.add_argument("--altitude", type=float, help="Altitude in meters")
coords.add_argument("--accuracy", type=float, help="Accuracy in meters")
coords.add_argument("--heading", type=float, help="Heading in degrees clockwise from due north")
csv = subparsers.add_parser("csv", description="Set location to points defined in a CSV file or stream")
csv.add_argument("file", type=argparse.FileType(), help="CSV file to read")
csv.add_argument("--rate", type=float, default=1, help="Number of points to read per second")
gpx = subparsers.add_parser("gpx", description="Replay a GPX track")
gpx.add_argument("file", type=argparse.FileType(), help="GPX file to read, or - for stdin")
gpx.add_argument("--speed", type=float, default=1, help="Speed at which to replay track. 1 is realtime.")
ARGS = parser.parse_args()
def start_server():
""" Start the custom GeoClue2 server and set up the Manager object. """
def bus_acquired(conn, name):
pass
def name_acquired(conn, name):
manager = DBusManager()
manager.register(conn)
print("Waiting for DBus client...")
def name_lost(conn, name):
print("Could not aqcuire name. Make sure to run with sudo.")
exit(1)
Gio.bus_own_name(
Gio.BusType.SYSTEM,
BUS_NAME,
Gio.BusNameOwnerFlags.DO_NOT_QUEUE,
bus_acquired,
name_acquired,
name_lost
)
def kill_existing_geoclue():
""" Find the process that currently owns the org.freedesktop.GeoClue2 bus
name, and kill it so we can replace it (if it exists). """
def on_get_pid(proxy, pid, userdata):
p = subprocess.Popen(["kill", str(pid)])
p.wait()