diff options
Diffstat (limited to 'src/main/java/org/traccar/database')
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; |