1# GeoClue 2 daemon.
2{
3 config,
4 lib,
5 pkgs,
6 ...
7}:
8let
9 cfg = config.services.geoclue2;
10
11 appConfigModule = lib.types.submodule (
12 { name, ... }:
13 {
14 options = {
15 desktopID = lib.mkOption {
16 type = lib.types.str;
17 description = "Desktop ID of the application.";
18 };
19
20 isAllowed = lib.mkOption {
21 type = lib.types.bool;
22 description = ''
23 Whether the application will be allowed access to location information.
24 '';
25 };
26
27 isSystem = lib.mkOption {
28 type = lib.types.bool;
29 description = ''
30 Whether the application is a system component or not.
31 '';
32 };
33
34 users = lib.mkOption {
35 type = lib.types.listOf lib.types.str;
36 default = [ ];
37 description = ''
38 List of UIDs of all users for which this application is allowed location
39 info access, Defaults to an empty string to allow it for all users.
40 '';
41 };
42 };
43
44 config.desktopID = lib.mkDefault name;
45 }
46 );
47
48 appConfigToINICompatible =
49 _:
50 {
51 desktopID,
52 isAllowed,
53 isSystem,
54 users,
55 ...
56 }:
57 {
58 name = desktopID;
59 value = {
60 allowed = isAllowed;
61 system = isSystem;
62 users = lib.concatStringsSep ";" users;
63 };
64 };
65
66in
67{
68
69 ###### interface
70
71 options = {
72
73 services.geoclue2 = {
74
75 enable = lib.mkOption {
76 type = lib.types.bool;
77 default = false;
78 description = ''
79 Whether to enable GeoClue 2 daemon, a DBus service
80 that provides location information for accessing.
81 '';
82 };
83 whitelistedAgents = lib.mkOption {
84 type = lib.types.listOf lib.types.str;
85 default = [
86 "gnome-shell"
87 "io.elementary.desktop.agent-geoclue2"
88 ];
89 description = ''
90 Desktop IDs (without the .desktop extension) of whitelisted agents.
91 '';
92 };
93
94 enableDemoAgent = lib.mkOption {
95 type = lib.types.bool;
96 default = true;
97 description = ''
98 Whether to use the GeoClue demo agent. This should be
99 overridden by desktop environments that provide their own
100 agent.
101 '';
102 };
103
104 enableNmea = lib.mkOption {
105 type = lib.types.bool;
106 default = true;
107 description = ''
108 Whether to fetch location from NMEA sources on local network.
109 '';
110 };
111
112 enable3G = lib.mkOption {
113 type = lib.types.bool;
114 default = true;
115 description = ''
116 Whether to enable 3G source.
117 '';
118 };
119
120 enableCDMA = lib.mkOption {
121 type = lib.types.bool;
122 default = true;
123 description = ''
124 Whether to enable CDMA source.
125 '';
126 };
127
128 enableModemGPS = lib.mkOption {
129 type = lib.types.bool;
130 default = true;
131 description = ''
132 Whether to enable Modem-GPS source.
133 '';
134 };
135
136 enableWifi = lib.mkOption {
137 type = lib.types.bool;
138 default = true;
139 description = ''
140 Whether to enable WiFi source.
141 '';
142 };
143
144 enableStatic = lib.mkOption {
145 type = lib.types.bool;
146 default = false;
147 description = ''
148 Whether to enable the static source. This source defines a fixed
149 location using the `staticLatitude`, `staticLongitude`,
150 `staticAltitude`, and `staticAccuracy` options.
151
152 Setting `enableStatic` to true will disable all other sources, to
153 prevent conflicts. Use `lib.mkForce true` when enabling other sources
154 if for some reason you want to override this.
155 '';
156 };
157
158 staticLatitude = lib.mkOption {
159 type = lib.types.numbers.between (-90) 90;
160 description = ''
161 Latitude to use for the static source. Defaults to `location.latitude`.
162 '';
163 };
164
165 staticLongitude = lib.mkOption {
166 type = lib.types.numbers.between (-180) 180;
167 description = ''
168 Longitude to use for the static source. Defaults to `location.longitude`.
169 '';
170 };
171
172 staticAltitude = lib.mkOption {
173 type = lib.types.number;
174 description = ''
175 Altitude in meters to use for the static source.
176 '';
177 };
178
179 staticAccuracy = lib.mkOption {
180 type = lib.types.numbers.positive;
181 description = ''
182 Accuracy radius in meters to use for the static source.
183 '';
184 };
185
186 geoProviderUrl = lib.mkOption {
187 type = lib.types.str;
188 default = "https://api.beacondb.net/v1/geolocate";
189 example = "https://www.googleapis.com/geolocation/v1/geolocate?key=YOUR_KEY";
190 description = ''
191 The url to the wifi GeoLocation Service.
192 '';
193 };
194
195 package = lib.mkOption {
196 type = lib.types.package;
197 default = pkgs.geoclue2;
198 defaultText = lib.literalExpression "pkgs.geoclue2";
199 apply =
200 pkg:
201 pkg.override {
202 # the demo agent isn't built by default, but we need it here
203 withDemoAgent = cfg.enableDemoAgent;
204 };
205 description = "The geoclue2 package to use";
206 };
207
208 submitData = lib.mkOption {
209 type = lib.types.bool;
210 default = false;
211 description = ''
212 Whether to submit data to a GeoLocation Service.
213 '';
214 };
215
216 submissionUrl = lib.mkOption {
217 type = lib.types.str;
218 default = "https://api.beacondb.net/v2/geosubmit";
219 description = ''
220 The url to submit data to a GeoLocation Service.
221 '';
222 };
223
224 submissionNick = lib.mkOption {
225 type = lib.types.str;
226 default = "geoclue";
227 description = ''
228 A nickname to submit network data with.
229 Must be 2-32 characters long.
230 '';
231 };
232
233 appConfig = lib.mkOption {
234 type = lib.types.attrsOf appConfigModule;
235 default = { };
236 example = lib.literalExpression ''
237 "com.github.app" = {
238 isAllowed = true;
239 isSystem = true;
240 users = [ "300" ];
241 };
242 '';
243 description = ''
244 Specify extra settings per application.
245 '';
246 };
247
248 };
249
250 };
251
252 ###### implementation
253 config = lib.mkIf cfg.enable {
254
255 environment.systemPackages = [ cfg.package ];
256
257 services.dbus.packages = [ cfg.package ];
258
259 systemd.packages = [ cfg.package ];
260
261 # we cannot use DynamicUser as we need the the geoclue user to exist for the
262 # dbus policy to work
263 users = {
264 users.geoclue = {
265 isSystemUser = true;
266 home = "/var/lib/geoclue";
267 group = "geoclue";
268 description = "Geoinformation service";
269 };
270
271 groups.geoclue = { };
272 };
273
274 services.geoclue2 = {
275 enable3G = lib.mkIf cfg.enableStatic false;
276 enableCDMA = lib.mkIf cfg.enableStatic false;
277 enableModemGPS = lib.mkIf cfg.enableStatic false;
278 enableNmea = lib.mkIf cfg.enableStatic false;
279 enableWifi = lib.mkIf cfg.enableStatic false;
280 staticLatitude = lib.mkDefault config.location.latitude;
281 staticLongitude = lib.mkDefault config.location.longitude;
282 };
283
284 systemd.services.geoclue = {
285 wants = lib.optionals cfg.enableWifi [ "network-online.target" ];
286 after = lib.optionals cfg.enableWifi [ "network-online.target" ];
287 # restart geoclue service when the configuration changes
288 restartTriggers = [
289 config.environment.etc."geoclue/geoclue.conf".source
290 ];
291 serviceConfig.StateDirectory = "geoclue";
292 };
293
294 # this needs to run as a user service, since it's associated with the
295 # user who is making the requests
296 systemd.user.services = lib.mkIf cfg.enableDemoAgent {
297 geoclue-agent = {
298 description = "Geoclue agent";
299 # this should really be `partOf = [ "geoclue.service" ]`, but
300 # we can't be part of a system service, and the agent should
301 # be okay with the main service coming and going
302 wantedBy = [ "default.target" ];
303 wants = lib.optionals cfg.enableWifi [ "network-online.target" ];
304 after = lib.optionals cfg.enableWifi [ "network-online.target" ];
305 unitConfig.ConditionUser = "!@system";
306 serviceConfig = {
307 Type = "exec";
308 ExecStart = "${cfg.package}/libexec/geoclue-2.0/demos/agent";
309 Restart = "on-failure";
310 PrivateTmp = true;
311 };
312 };
313 };
314
315 services.geoclue2.appConfig.epiphany = {
316 isAllowed = true;
317 isSystem = false;
318 };
319
320 services.geoclue2.appConfig.firefox = {
321 isAllowed = true;
322 isSystem = false;
323 };
324
325 environment.etc."geoclue/geoclue.conf".text = lib.generators.toINI { } (
326 {
327 agent = {
328 whitelist = lib.concatStringsSep ";" (
329 lib.lists.unique (
330 cfg.whitelistedAgents
331 ++ lib.optionals config.services.geoclue2.enableDemoAgent [ "geoclue-demo-agent" ]
332 )
333 );
334 };
335 network-nmea = {
336 enable = cfg.enableNmea;
337 };
338 "3g" = {
339 enable = cfg.enable3G;
340 };
341 cdma = {
342 enable = cfg.enableCDMA;
343 };
344 modem-gps = {
345 enable = cfg.enableModemGPS;
346 };
347 wifi = {
348 enable = cfg.enableWifi;
349 }
350 // lib.optionalAttrs cfg.enableWifi {
351 url = cfg.geoProviderUrl;
352 submit-data = lib.boolToString cfg.submitData;
353 submission-url = cfg.submissionUrl;
354 submission-nick = cfg.submissionNick;
355 };
356 static-source = {
357 enable = cfg.enableStatic;
358 };
359 }
360 // lib.mapAttrs' appConfigToINICompatible cfg.appConfig
361 );
362
363 environment.etc.geolocation = lib.mkIf cfg.enableStatic {
364 mode = "0440";
365 group = "geoclue";
366 text = ''
367 ${toString cfg.staticLatitude}
368 ${toString cfg.staticLongitude}
369 ${toString cfg.staticAltitude}
370 ${toString cfg.staticAccuracy}
371 '';
372 };
373 };
374
375 meta = with lib; {
376 maintainers = with maintainers; [ ] ++ teams.pantheon.members;
377 };
378}