aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/org/traccar/database
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/org/traccar/database')
-rw-r--r--src/main/java/org/traccar/database/CommandsManager.java29
-rw-r--r--src/main/java/org/traccar/database/DeviceLookupService.java6
-rw-r--r--src/main/java/org/traccar/database/MediaManager.java4
-rw-r--r--src/main/java/org/traccar/database/NotificationManager.java35
-rw-r--r--src/main/java/org/traccar/database/OpenIdProvider.java204
-rw-r--r--src/main/java/org/traccar/database/StatisticsManager.java29
6 files changed, 278 insertions, 29 deletions
diff --git a/src/main/java/org/traccar/database/CommandsManager.java b/src/main/java/org/traccar/database/CommandsManager.java
index df399cd7a..90180b989 100644
--- a/src/main/java/org/traccar/database/CommandsManager.java
+++ b/src/main/java/org/traccar/database/CommandsManager.java
@@ -22,6 +22,7 @@ import org.traccar.broadcast.BroadcastInterface;
import org.traccar.broadcast.BroadcastService;
import org.traccar.model.Command;
import org.traccar.model.Device;
+import org.traccar.model.Event;
import org.traccar.model.Position;
import org.traccar.model.QueuedCommand;
import org.traccar.session.ConnectionManager;
@@ -34,10 +35,12 @@ import org.traccar.storage.query.Condition;
import org.traccar.storage.query.Order;
import org.traccar.storage.query.Request;
-import javax.annotation.Nullable;
-import javax.inject.Inject;
-import javax.inject.Singleton;
+import jakarta.annotation.Nullable;
+import jakarta.inject.Inject;
+import jakarta.inject.Singleton;
import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
import java.util.stream.Collectors;
@Singleton
@@ -48,20 +51,23 @@ public class CommandsManager implements BroadcastInterface {
private final SmsManager smsManager;
private final ConnectionManager connectionManager;
private final BroadcastService broadcastService;
+ private final NotificationManager notificationManager;
@Inject
public CommandsManager(
Storage storage, ServerManager serverManager, @Nullable SmsManager smsManager,
- ConnectionManager connectionManager, BroadcastService broadcastService) {
+ ConnectionManager connectionManager, BroadcastService broadcastService,
+ NotificationManager notificationManager) {
this.storage = storage;
this.serverManager = serverManager;
this.smsManager = smsManager;
this.connectionManager = connectionManager;
this.broadcastService = broadcastService;
+ this.notificationManager = notificationManager;
broadcastService.registerListener(this);
}
- public boolean sendCommand(Command command) throws Exception {
+ public QueuedCommand sendCommand(Command command) throws Exception {
long deviceId = command.getDeviceId();
if (command.getTextChannel()) {
if (smsManager == null) {
@@ -84,12 +90,13 @@ public class CommandsManager implements BroadcastInterface {
if (deviceSession != null && deviceSession.supportsLiveCommands()) {
deviceSession.sendCommand(command);
} else {
- storage.addObject(QueuedCommand.fromCommand(command), new Request(new Columns.Exclude("id")));
+ QueuedCommand queuedCommand = QueuedCommand.fromCommand(command);
+ queuedCommand.setId(storage.addObject(queuedCommand, new Request(new Columns.Exclude("id"))));
broadcastService.updateCommand(true, deviceId);
- return false;
+ return queuedCommand;
}
}
- return true;
+ return null;
}
public Collection<Command> readQueuedCommands(long deviceId) {
@@ -102,10 +109,16 @@ public class CommandsManager implements BroadcastInterface {
new Columns.All(),
new Condition.Equals("deviceId", deviceId),
new Order("id", false, count)));
+ Map<Event, Position> events = new HashMap<>();
for (var command : commands) {
storage.removeObject(QueuedCommand.class, new Request(
new Condition.Equals("id", command.getId())));
+
+ Event event = new Event(Event.TYPE_QUEUED_COMMAND_SENT, command.getDeviceId());
+ event.set("id", command.getId());
+ events.put(event, null);
}
+ notificationManager.updateEvents(events);
return commands.stream().map(QueuedCommand::toCommand).collect(Collectors.toList());
} catch (StorageException e) {
throw new RuntimeException(e);
diff --git a/src/main/java/org/traccar/database/DeviceLookupService.java b/src/main/java/org/traccar/database/DeviceLookupService.java
index 583b2ae35..90d23531e 100644
--- a/src/main/java/org/traccar/database/DeviceLookupService.java
+++ b/src/main/java/org/traccar/database/DeviceLookupService.java
@@ -29,8 +29,8 @@ import org.traccar.storage.query.Columns;
import org.traccar.storage.query.Condition;
import org.traccar.storage.query.Request;
-import javax.inject.Inject;
-import javax.inject.Singleton;
+import jakarta.inject.Inject;
+import jakarta.inject.Singleton;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
@@ -49,7 +49,7 @@ public class DeviceLookupService {
private final boolean throttlingEnabled;
- private static class IdentifierInfo {
+ private static final class IdentifierInfo {
private long lastQuery;
private long delay;
private Timeout timeout;
diff --git a/src/main/java/org/traccar/database/MediaManager.java b/src/main/java/org/traccar/database/MediaManager.java
index c1ef810ee..2f2369c96 100644
--- a/src/main/java/org/traccar/database/MediaManager.java
+++ b/src/main/java/org/traccar/database/MediaManager.java
@@ -21,8 +21,8 @@ import org.slf4j.LoggerFactory;
import org.traccar.config.Config;
import org.traccar.config.Keys;
-import javax.inject.Inject;
-import javax.inject.Singleton;
+import jakarta.inject.Inject;
+import jakarta.inject.Singleton;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
diff --git a/src/main/java/org/traccar/database/NotificationManager.java b/src/main/java/org/traccar/database/NotificationManager.java
index cb971b082..65437f0a1 100644
--- a/src/main/java/org/traccar/database/NotificationManager.java
+++ b/src/main/java/org/traccar/database/NotificationManager.java
@@ -23,12 +23,12 @@ import org.traccar.config.Keys;
import org.traccar.forward.EventData;
import org.traccar.forward.EventForwarder;
import org.traccar.geocoder.Geocoder;
+import org.traccar.helper.DateUtil;
import org.traccar.model.Calendar;
import org.traccar.model.Device;
import org.traccar.model.Event;
import org.traccar.model.Geofence;
import org.traccar.model.Maintenance;
-import org.traccar.model.Notification;
import org.traccar.model.Position;
import org.traccar.notification.MessageException;
import org.traccar.notification.NotificatorManager;
@@ -38,9 +38,9 @@ import org.traccar.storage.StorageException;
import org.traccar.storage.query.Columns;
import org.traccar.storage.query.Request;
-import javax.annotation.Nullable;
-import javax.inject.Inject;
-import javax.inject.Singleton;
+import jakarta.annotation.Nullable;
+import jakarta.inject.Inject;
+import jakarta.inject.Singleton;
import java.util.Arrays;
import java.util.Map;
import java.util.Map.Entry;
@@ -58,6 +58,7 @@ public class NotificationManager {
private final Geocoder geocoder;
private final boolean geocodeOnRequest;
+ private final long timeThreshold;
@Inject
public NotificationManager(
@@ -69,6 +70,7 @@ public class NotificationManager {
this.notificatorManager = notificatorManager;
this.geocoder = geocoder;
geocodeOnRequest = config.getBoolean(Keys.GEOCODER_ON_REQUEST);
+ timeThreshold = config.getLong(Keys.NOTIFICATOR_TIME_THRESHOLD);
}
private void updateEvent(Event event, Position position) {
@@ -78,7 +80,14 @@ public class NotificationManager {
LOGGER.warn("Event save error", error);
}
- var notifications = cacheManager.getDeviceObjects(event.getDeviceId(), Notification.class).stream()
+ forwardEvent(event, position);
+
+ if (System.currentTimeMillis() - event.getEventTime().getTime() > timeThreshold) {
+ LOGGER.info("Skipping notifications for old event");
+ return;
+ }
+
+ var notifications = cacheManager.getDeviceNotifications(event.getDeviceId()).stream()
.filter(notification -> notification.getType().equals(event.getType()))
.filter(notification -> {
if (event.getType().equals(Event.TYPE_ALARM)) {
@@ -98,6 +107,14 @@ public class NotificationManager {
})
.collect(Collectors.toUnmodifiableList());
+ Device device = cacheManager.getObject(Device.class, event.getDeviceId());
+ LOGGER.info(
+ "Event id: {}, time: {}, type: {}, notifications: {}",
+ device.getUniqueId(),
+ DateUtil.formatDate(event.getEventTime(), false),
+ event.getType(),
+ notifications.size());
+
if (!notifications.isEmpty()) {
if (position != null && position.getAddress() == null && geocodeOnRequest && geocoder != null) {
position.setAddress(geocoder.getAddress(position.getLatitude(), position.getLongitude(), null));
@@ -107,16 +124,14 @@ public class NotificationManager {
cacheManager.getNotificationUsers(notification.getId(), event.getDeviceId()).forEach(user -> {
for (String notificator : notification.getNotificatorsTypes()) {
try {
- notificatorManager.getNotificator(notificator).send(user, event, position);
- } catch (MessageException | InterruptedException exception) {
+ notificatorManager.getNotificator(notificator).send(notification, user, event, position);
+ } catch (MessageException exception) {
LOGGER.warn("Notification failed", exception);
}
}
});
});
}
-
- forwardEvent(event, position);
}
private void forwardEvent(Event event, Position position) {
@@ -146,7 +161,7 @@ public class NotificationManager {
try {
cacheManager.addDevice(event.getDeviceId());
updateEvent(event, position);
- } catch (StorageException e) {
+ } catch (Exception e) {
throw new RuntimeException(e);
} finally {
cacheManager.removeDevice(event.getDeviceId());
diff --git a/src/main/java/org/traccar/database/OpenIdProvider.java b/src/main/java/org/traccar/database/OpenIdProvider.java
new file mode 100644
index 000000000..93297f7ab
--- /dev/null
+++ b/src/main/java/org/traccar/database/OpenIdProvider.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright 2023 Daniel Raper (me@danr.uk)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.database;
+
+import org.traccar.config.Config;
+import org.traccar.config.Keys;
+import org.traccar.api.resource.SessionResource;
+import org.traccar.api.security.LoginService;
+import org.traccar.model.User;
+import org.traccar.storage.StorageException;
+import org.traccar.helper.LogAction;
+import org.traccar.helper.WebHelper;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.security.GeneralSecurityException;
+import java.util.List;
+import java.util.Map;
+import java.io.IOException;
+import jakarta.servlet.http.HttpServletRequest;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.inject.Inject;
+
+import com.nimbusds.oauth2.sdk.http.HTTPResponse;
+import com.nimbusds.oauth2.sdk.AuthorizationCode;
+import com.nimbusds.oauth2.sdk.ResponseType;
+import com.nimbusds.oauth2.sdk.Scope;
+import com.nimbusds.oauth2.sdk.AuthorizationGrant;
+import com.nimbusds.oauth2.sdk.TokenRequest;
+import com.nimbusds.oauth2.sdk.TokenResponse;
+import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.oauth2.sdk.AuthorizationResponse;
+import com.nimbusds.oauth2.sdk.auth.Secret;
+import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
+import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
+import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
+import com.nimbusds.oauth2.sdk.id.State;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
+import com.nimbusds.openid.connect.sdk.OIDCTokenResponseParser;
+import com.nimbusds.openid.connect.sdk.UserInfoResponse;
+import com.nimbusds.openid.connect.sdk.UserInfoRequest;
+import com.nimbusds.openid.connect.sdk.AuthenticationRequest;
+import com.nimbusds.openid.connect.sdk.claims.UserInfo;
+
+public class OpenIdProvider {
+ private final Boolean force;
+ private final ClientID clientId;
+ private final ClientAuthentication clientAuth;
+ private final URI callbackUrl;
+ private final URI authUrl;
+ private final URI tokenUrl;
+ private final URI userInfoUrl;
+ private final URI baseUrl;
+ private final String adminGroup;
+ private final String allowGroup;
+
+ private final LoginService loginService;
+
+ @Inject
+ public OpenIdProvider(Config config, LoginService loginService, HttpClient httpClient, ObjectMapper objectMapper)
+ throws InterruptedException, IOException, URISyntaxException {
+
+ this.loginService = loginService;
+
+ force = config.getBoolean(Keys.OPENID_FORCE);
+ clientId = new ClientID(config.getString(Keys.OPENID_CLIENT_ID));
+ clientAuth = new ClientSecretBasic(clientId, new Secret(config.getString(Keys.OPENID_CLIENT_SECRET)));
+
+ baseUrl = new URI(WebHelper.retrieveWebUrl(config));
+ callbackUrl = new URI(WebHelper.retrieveWebUrl(config) + "/api/session/openid/callback");
+
+ if (config.hasKey(Keys.OPENID_ISSUER_URL)) {
+ HttpRequest httpRequest = HttpRequest.newBuilder(
+ URI.create(config.getString(Keys.OPENID_ISSUER_URL) + "/.well-known/openid-configuration"))
+ .header("Accept", "application/json")
+ .build();
+
+ String httpResponse = httpClient.send(httpRequest, BodyHandlers.ofString()).body();
+
+ Map<String, Object> discoveryMap = objectMapper.readValue(httpResponse, new TypeReference<>() {
+ });
+
+ authUrl = new URI((String) discoveryMap.get("authorization_endpoint"));
+ tokenUrl = new URI((String) discoveryMap.get("token_endpoint"));
+ userInfoUrl = new URI((String) discoveryMap.get("userinfo_endpoint"));
+ } else {
+ authUrl = new URI(config.getString(Keys.OPENID_AUTH_URL));
+ tokenUrl = new URI(config.getString(Keys.OPENID_TOKEN_URL));
+ userInfoUrl = new URI(config.getString(Keys.OPENID_USERINFO_URL));
+ }
+
+ adminGroup = config.getString(Keys.OPENID_ADMIN_GROUP);
+ allowGroup = config.getString(Keys.OPENID_ALLOW_GROUP);
+ }
+
+ public URI createAuthUri() {
+ Scope scope = new Scope("openid", "profile", "email");
+
+ if (adminGroup != null) {
+ scope.add("groups");
+ }
+
+ AuthenticationRequest.Builder request = new AuthenticationRequest.Builder(
+ new ResponseType("code"),
+ scope,
+ clientId,
+ callbackUrl);
+
+ return request.endpointURI(authUrl)
+ .state(new State())
+ .build()
+ .toURI();
+ }
+
+ private OIDCTokenResponse getToken(AuthorizationCode code)
+ throws IOException, ParseException, GeneralSecurityException {
+ AuthorizationGrant codeGrant = new AuthorizationCodeGrant(code, callbackUrl);
+ TokenRequest tokenRequest = new TokenRequest(tokenUrl, clientAuth, codeGrant);
+
+ HTTPResponse tokenResponse = tokenRequest.toHTTPRequest().send();
+ TokenResponse token = OIDCTokenResponseParser.parse(tokenResponse);
+ if (!token.indicatesSuccess()) {
+ throw new GeneralSecurityException("Unable to authenticate with the OpenID Connect provider.");
+ }
+
+ return (OIDCTokenResponse) token.toSuccessResponse();
+ }
+
+ private UserInfo getUserInfo(BearerAccessToken token) throws IOException, ParseException, GeneralSecurityException {
+ HTTPResponse httpResponse = new UserInfoRequest(userInfoUrl, token)
+ .toHTTPRequest()
+ .send();
+
+ UserInfoResponse userInfoResponse = UserInfoResponse.parse(httpResponse);
+
+ if (!userInfoResponse.indicatesSuccess()) {
+ throw new GeneralSecurityException(
+ "Failed to access OpenID Connect user info endpoint. Please contact your administrator.");
+ }
+
+ return userInfoResponse.toSuccessResponse().getUserInfo();
+ }
+
+ public URI handleCallback(URI requestUri, HttpServletRequest request)
+ throws StorageException, ParseException, IOException, GeneralSecurityException {
+
+ AuthorizationResponse response = AuthorizationResponse.parse(requestUri);
+
+ if (!response.indicatesSuccess()) {
+ throw new GeneralSecurityException(response.toErrorResponse().getErrorObject().getDescription());
+ }
+
+ AuthorizationCode authCode = response.toSuccessResponse().getAuthorizationCode();
+
+ if (authCode == null) {
+ throw new GeneralSecurityException("Malformed OpenID callback.");
+ }
+
+ OIDCTokenResponse tokens = getToken(authCode);
+
+ BearerAccessToken bearerToken = tokens.getOIDCTokens().getBearerAccessToken();
+
+ UserInfo userInfo = getUserInfo(bearerToken);
+
+ List<String> userGroups = userInfo.getStringListClaim("groups");
+ boolean administrator = adminGroup != null && userGroups.contains(adminGroup);
+
+ if (!(administrator || allowGroup == null || userGroups.contains(allowGroup))) {
+ throw new GeneralSecurityException("Your OpenID Groups do not permit access to Traccar.");
+ }
+
+ User user = loginService.login(
+ userInfo.getEmailAddress(), userInfo.getName(), administrator).getUser();
+
+ request.getSession().setAttribute(SessionResource.USER_ID_KEY, user.getId());
+ LogAction.login(user.getId(), WebHelper.retrieveRemoteAddress(request));
+
+ return baseUrl;
+ }
+
+ public boolean getForce() {
+ return force;
+ }
+}
diff --git a/src/main/java/org/traccar/database/StatisticsManager.java b/src/main/java/org/traccar/database/StatisticsManager.java
index e0995dabc..445e53e7c 100644
--- a/src/main/java/org/traccar/database/StatisticsManager.java
+++ b/src/main/java/org/traccar/database/StatisticsManager.java
@@ -19,6 +19,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.traccar.api.security.ServiceAccountUser;
import org.traccar.config.Config;
import org.traccar.config.Keys;
import org.traccar.helper.DateUtil;
@@ -28,11 +29,11 @@ import org.traccar.storage.StorageException;
import org.traccar.storage.query.Columns;
import org.traccar.storage.query.Request;
-import javax.inject.Inject;
-import javax.inject.Singleton;
-import javax.ws.rs.client.Client;
-import javax.ws.rs.client.Entity;
-import javax.ws.rs.core.Form;
+import jakarta.inject.Inject;
+import jakarta.inject.Singleton;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.Form;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
@@ -57,6 +58,7 @@ public class StatisticsManager {
private final Set<Long> users = new HashSet<>();
private final Map<Long, String> deviceProtocols = new HashMap<>();
+ private final Map<Long, Integer> deviceMessages = new HashMap<>();
private int requests;
private int messagesReceived;
@@ -98,8 +100,11 @@ public class StatisticsManager {
statistics.setProtocols(protocols);
}
+ statistics.set("modern", config.getString(Keys.WEB_PATH).contains("modern"));
+
users.clear();
deviceProtocols.clear();
+ deviceMessages.clear();
requests = 0;
messagesReceived = 0;
messagesStored = 0;
@@ -138,6 +143,13 @@ public class StatisticsManager {
LOGGER.warn("Failed to serialize protocols", e);
}
}
+ if (!statistics.getAttributes().isEmpty()) {
+ try {
+ form.param("attributes", objectMapper.writeValueAsString(statistics.getAttributes()));
+ } catch (JsonProcessingException e) {
+ LOGGER.warn("Failed to serialize attributes", e);
+ }
+ }
client.target(url).request().async().post(Entity.form(form));
}
@@ -147,7 +159,7 @@ public class StatisticsManager {
public synchronized void registerRequest(long userId) {
checkSplit();
requests += 1;
- if (userId != 0) {
+ if (userId != 0 && userId != ServiceAccountUser.ID) {
users.add(userId);
}
}
@@ -162,9 +174,14 @@ public class StatisticsManager {
messagesStored += 1;
if (deviceId != 0) {
deviceProtocols.put(deviceId, protocol);
+ deviceMessages.merge(deviceId, 1, Integer::sum);
}
}
+ public synchronized int messageStoredCount(long deviceId) {
+ return deviceMessages.getOrDefault(deviceId, 0);
+ }
+
public synchronized void registerMail() {
checkSplit();
mailSent += 1;