diff options
Diffstat (limited to 'src/main')
48 files changed, 1135 insertions, 383 deletions
diff --git a/src/main/java/org/traccar/EventLoopGroupFactory.java b/src/main/java/org/traccar/EventLoopGroupFactory.java index 482559253..7f596cd83 100644 --- a/src/main/java/org/traccar/EventLoopGroupFactory.java +++ b/src/main/java/org/traccar/EventLoopGroupFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 - 2018 Anton Tananaev (anton@traccar.org) + * Copyright 2012 - 2024 Anton Tananaev (anton@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,18 +20,18 @@ import io.netty.channel.nio.NioEventLoopGroup; public final class EventLoopGroupFactory { - private static EventLoopGroup bossGroup = new NioEventLoopGroup(); - private static EventLoopGroup workerGroup = new NioEventLoopGroup(); + private static final EventLoopGroup BOSS_GROUP = new NioEventLoopGroup(); + private static final EventLoopGroup WORKER_GROUP = new NioEventLoopGroup(); private EventLoopGroupFactory() { } public static EventLoopGroup getBossGroup() { - return bossGroup; + return BOSS_GROUP; } public static EventLoopGroup getWorkerGroup() { - return workerGroup; + return WORKER_GROUP; } } diff --git a/src/main/java/org/traccar/Main.java b/src/main/java/org/traccar/Main.java index 33fcf654f..a22194d9e 100644 --- a/src/main/java/org/traccar/Main.java +++ b/src/main/java/org/traccar/Main.java @@ -17,6 +17,7 @@ package org.traccar; import com.google.inject.Guice; import com.google.inject.Injector; +import com.google.inject.ProvisionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.traccar.broadcast.BroadcastService; @@ -51,23 +52,22 @@ public final class Main { public static void logSystemInfo() { try { OperatingSystemMXBean operatingSystemBean = ManagementFactory.getOperatingSystemMXBean(); - LOGGER.info("Operating system" - + " name: " + operatingSystemBean.getName() - + " version: " + operatingSystemBean.getVersion() - + " architecture: " + operatingSystemBean.getArch()); + LOGGER.info( + "Operating system name: {} version: {} architecture: {}", + operatingSystemBean.getName(), operatingSystemBean.getVersion(), operatingSystemBean.getArch()); RuntimeMXBean runtimeBean = ManagementFactory.getRuntimeMXBean(); - LOGGER.info("Java runtime" - + " name: " + runtimeBean.getVmName() - + " vendor: " + runtimeBean.getVmVendor() - + " version: " + runtimeBean.getVmVersion()); + LOGGER.info( + "Java runtime name: {} vendor: {} version: {}", + runtimeBean.getVmName(), runtimeBean.getVmVendor(), runtimeBean.getVmVersion()); MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean(); - LOGGER.info("Memory limit" - + " heap: " + memoryBean.getHeapMemoryUsage().getMax() / (1024 * 1024) + "mb" - + " non-heap: " + memoryBean.getNonHeapMemoryUsage().getMax() / (1024 * 1024) + "mb"); + LOGGER.info( + "Memory limit heap: {}mb non-heap: {}mb", + memoryBean.getHeapMemoryUsage().getMax() / (1024 * 1024), + memoryBean.getNonHeapMemoryUsage().getMax() / (1024 * 1024)); - LOGGER.info("Character encoding: " + Charset.defaultCharset().displayName()); + LOGGER.info("Character encoding: {}", Charset.defaultCharset().displayName()); } catch (Exception error) { LOGGER.warn("Failed to get system info"); @@ -115,7 +115,7 @@ public final class Main { try { injector = Guice.createInjector(new MainModule(configFile), new DatabaseModule(), new WebModule()); logSystemInfo(); - LOGGER.info("Version: " + Main.class.getPackage().getImplementationVersion()); + LOGGER.info("Version: {}", Main.class.getPackage().getImplementationVersion()); LOGGER.info("Starting server..."); var services = new ArrayList<LifecycleObject>(); @@ -142,8 +142,14 @@ public final class Main { } })); } catch (Exception e) { - LOGGER.error("Main method error", e); - throw new RuntimeException(e); + Throwable unwrapped; + if (e instanceof ProvisionException) { + unwrapped = e.getCause(); + } else { + unwrapped = e; + } + LOGGER.error("Main method error", unwrapped); + System.exit(1); } } diff --git a/src/main/java/org/traccar/MainModule.java b/src/main/java/org/traccar/MainModule.java index 791d61c61..66238ab44 100644 --- a/src/main/java/org/traccar/MainModule.java +++ b/src/main/java/org/traccar/MainModule.java @@ -66,7 +66,7 @@ import org.traccar.geocoder.MapmyIndiaGeocoder; import org.traccar.geocoder.NominatimGeocoder; import org.traccar.geocoder.OpenCageGeocoder; import org.traccar.geocoder.PositionStackGeocoder; -import org.traccar.geocoder.TestGeocoder; +import org.traccar.geocoder.PlusCodesGeocoder; import org.traccar.geocoder.TomTomGeocoder; import org.traccar.geolocation.GeolocationProvider; import org.traccar.geolocation.GoogleGeolocationProvider; @@ -79,7 +79,6 @@ import org.traccar.handler.GeolocationHandler; import org.traccar.handler.SpeedLimitHandler; import org.traccar.handler.TimeHandler; import org.traccar.helper.ObjectMapperContextResolver; -import org.traccar.helper.SanitizerModule; import org.traccar.helper.WebHelper; import org.traccar.mail.LogMailManager; import org.traccar.mail.MailManager; @@ -132,11 +131,8 @@ public class MainModule extends AbstractModule { @Singleton @Provides - public static ObjectMapper provideObjectMapper(Config config) { + public static ObjectMapper provideObjectMapper() { ObjectMapper objectMapper = new ObjectMapper(); - if (config.getBoolean(Keys.WEB_SANITIZE)) { - objectMapper.registerModule(new SanitizerModule()); - } objectMapper.registerModule(new JSONPModule()); objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); return objectMapper; @@ -191,7 +187,7 @@ public class MainModule extends AbstractModule { @Provides public static WebServer provideWebServer(Injector injector, Config config) { - if (config.hasKey(Keys.WEB_PORT)) { + if (config.getInteger(Keys.WEB_PORT) > 0) { return new WebServer(injector, config); } return null; @@ -201,7 +197,7 @@ public class MainModule extends AbstractModule { @Provides public static Geocoder provideGeocoder(Config config, Client client, StatisticsManager statisticsManager) { if (config.getBoolean(Keys.GEOCODER_ENABLE)) { - String type = config.getString(Keys.GEOCODER_TYPE, "google"); + String type = config.getString(Keys.GEOCODER_TYPE); String url = config.getString(Keys.GEOCODER_URL); String key = config.getString(Keys.GEOCODER_KEY); String language = config.getString(Keys.GEOCODER_LANGUAGE); @@ -211,8 +207,8 @@ public class MainModule extends AbstractModule { int cacheSize = config.getInteger(Keys.GEOCODER_CACHE_SIZE); Geocoder geocoder; switch (type) { - case "test": - geocoder = new TestGeocoder(); + case "pluscodes": + geocoder = new PlusCodesGeocoder(); break; case "nominatim": geocoder = new NominatimGeocoder(client, url, key, language, cacheSize, addressFormat); diff --git a/src/main/java/org/traccar/ProcessingHandler.java b/src/main/java/org/traccar/ProcessingHandler.java index fd048d127..bb040bfff 100644 --- a/src/main/java/org/traccar/ProcessingHandler.java +++ b/src/main/java/org/traccar/ProcessingHandler.java @@ -101,8 +101,8 @@ public class ProcessingHandler extends ChannelInboundHandlerAdapter implements B GeocoderHandler.class, SpeedLimitHandler.class, MotionHandler.class, - EngineHoursHandler.class, ComputedAttributesHandler.class, + EngineHoursHandler.class, CopyAttributesHandler.class, PositionForwardingHandler.class, DatabaseHandler.class) diff --git a/src/main/java/org/traccar/ServerManager.java b/src/main/java/org/traccar/ServerManager.java index 22af66b41..1b0c441cf 100644 --- a/src/main/java/org/traccar/ServerManager.java +++ b/src/main/java/org/traccar/ServerManager.java @@ -54,7 +54,7 @@ public class ServerManager implements LifecycleObject { for (Class<?> protocolClass : ClassScanner.findSubclasses(BaseProtocol.class, "org.traccar.protocol")) { String protocolName = BaseProtocol.nameFromClass(protocolClass); if (enabledProtocols == null || enabledProtocols.contains(protocolName)) { - if (config.hasKey(Keys.PROTOCOL_PORT.withPrefix(protocolName))) { + if (config.getInteger(Keys.PROTOCOL_PORT.withPrefix(protocolName)) > 0) { BaseProtocol protocol = (BaseProtocol) injector.getInstance(protocolClass); connectorList.addAll(protocol.getConnectorList()); protocolList.put(protocol.getName(), protocol); diff --git a/src/main/java/org/traccar/api/AsyncSocketServlet.java b/src/main/java/org/traccar/api/AsyncSocketServlet.java index cd2c1639e..e1e7ee977 100644 --- a/src/main/java/org/traccar/api/AsyncSocketServlet.java +++ b/src/main/java/org/traccar/api/AsyncSocketServlet.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 - 2022 Anton Tananaev (anton@traccar.org) + * Copyright 2015 - 2024 Anton Tananaev (anton@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.eclipse.jetty.websocket.server.JettyWebSocketServlet; import org.eclipse.jetty.websocket.server.JettyWebSocketServletFactory; import org.traccar.api.resource.SessionResource; +import org.traccar.api.security.LoginService; import org.traccar.config.Config; import org.traccar.config.Keys; import org.traccar.session.ConnectionManager; @@ -27,7 +28,12 @@ import org.traccar.storage.Storage; import jakarta.inject.Inject; import jakarta.inject.Singleton; import jakarta.servlet.http.HttpSession; +import org.traccar.storage.StorageException; + +import java.io.IOException; +import java.security.GeneralSecurityException; import java.time.Duration; +import java.util.List; @Singleton public class AsyncSocketServlet extends JettyWebSocketServlet { @@ -36,25 +42,37 @@ public class AsyncSocketServlet extends JettyWebSocketServlet { private final ObjectMapper objectMapper; private final ConnectionManager connectionManager; private final Storage storage; + private final LoginService loginService; @Inject public AsyncSocketServlet( - Config config, ObjectMapper objectMapper, ConnectionManager connectionManager, Storage storage) { + Config config, ObjectMapper objectMapper, ConnectionManager connectionManager, Storage storage, + LoginService loginService) { this.config = config; this.objectMapper = objectMapper; this.connectionManager = connectionManager; this.storage = storage; + this.loginService = loginService; } @Override public void configure(JettyWebSocketServletFactory factory) { factory.setIdleTimeout(Duration.ofMillis(config.getLong(Keys.WEB_TIMEOUT))); factory.setCreator((req, resp) -> { - if (req.getSession() != null) { - Long userId = (Long) ((HttpSession) req.getSession()).getAttribute(SessionResource.USER_ID_KEY); - if (userId != null) { - return new AsyncSocket(objectMapper, connectionManager, storage, userId); + Long userId = null; + List<String> tokens = req.getParameterMap().get("token"); + if (tokens != null && !tokens.isEmpty()) { + String token = tokens.iterator().next(); + try { + userId = loginService.login(token).getUser().getId(); + } catch (StorageException | GeneralSecurityException | IOException e) { + throw new RuntimeException(e); } + } else if (req.getSession() != null) { + userId = (Long) ((HttpSession) req.getSession()).getAttribute(SessionResource.USER_ID_KEY); + } + if (userId != null) { + return new AsyncSocket(objectMapper, connectionManager, storage, userId); } return null; }); diff --git a/src/main/java/org/traccar/api/resource/CommandResource.java b/src/main/java/org/traccar/api/resource/CommandResource.java index c23d91e77..66ec0f8a3 100644 --- a/src/main/java/org/traccar/api/resource/CommandResource.java +++ b/src/main/java/org/traccar/api/resource/CommandResource.java @@ -23,6 +23,7 @@ import org.traccar.BaseProtocol; import org.traccar.ServerManager; import org.traccar.api.ExtendedObjectResource; import org.traccar.database.CommandsManager; +import org.traccar.helper.LogAction; import org.traccar.helper.model.DeviceUtil; import org.traccar.model.Command; import org.traccar.model.Device; @@ -140,6 +141,8 @@ public class CommandResource extends ExtendedObjectResource<Command> { return Response.accepted(queuedCommand).build(); } } + + LogAction.command(getUserId(), groupId, entity.getDeviceId(), entity.getType()); return Response.ok(entity).build(); } diff --git a/src/main/java/org/traccar/api/resource/DeviceResource.java b/src/main/java/org/traccar/api/resource/DeviceResource.java index 56253152f..971c29c60 100644 --- a/src/main/java/org/traccar/api/resource/DeviceResource.java +++ b/src/main/java/org/traccar/api/resource/DeviceResource.java @@ -137,10 +137,8 @@ public class DeviceResource extends BaseObjectResource<Device> { @Path("{id}/accumulators") @PUT public Response updateAccumulators(DeviceAccumulators entity) throws Exception { - if (permissionsService.notAdmin(getUserId())) { - permissionsService.checkManager(getUserId()); - permissionsService.checkPermission(Device.class, getUserId(), entity.getDeviceId()); - } + permissionsService.checkEdit(getUserId(), Device.class, false); + permissionsService.checkPermission(Device.class, getUserId(), entity.getDeviceId()); Position position = storage.getObject(Position.class, new Request( new Columns.All(), new Condition.LatestPositions(entity.getDeviceId()))); @@ -171,7 +169,7 @@ public class DeviceResource extends BaseObjectResource<Device> { throw new IllegalArgumentException(); } - LogAction.resetDeviceAccumulators(getUserId(), entity.getDeviceId()); + LogAction.resetAccumulators(getUserId(), entity.getDeviceId()); return Response.noContent().build(); } diff --git a/src/main/java/org/traccar/api/resource/NotificationResource.java b/src/main/java/org/traccar/api/resource/NotificationResource.java index 43dc1fa98..a41d00cf3 100644 --- a/src/main/java/org/traccar/api/resource/NotificationResource.java +++ b/src/main/java/org/traccar/api/resource/NotificationResource.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 - 2023 Anton Tananaev (anton@traccar.org) + * Copyright 2016 - 2024 Anton Tananaev (anton@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,6 +46,8 @@ import java.util.ArrayList; import java.util.Collection; import java.util.LinkedList; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; @Path("notifications") @Produces(MediaType.APPLICATION_JSON) @@ -80,8 +82,11 @@ public class NotificationResource extends ExtendedObjectResource<Notification> { @GET @Path("notificators") - public Collection<Typed> getNotificators() { - return notificatorManager.getAllNotificatorTypes(); + public Collection<Typed> getNotificators(@QueryParam("announcement") boolean announcement) { + Set<String> announcementsUnsupported = Set.of("command", "web"); + return notificatorManager.getAllNotificatorTypes().stream() + .filter(typed -> !announcement || !announcementsUnsupported.contains(typed.getType())) + .collect(Collectors.toUnmodifiableSet()); } @POST @@ -120,7 +125,9 @@ public class NotificationResource extends ExtendedObjectResource<Notification> { } } for (User user : users) { - notificatorManager.getNotificator(notificator).send(user, message, null, null); + if (!user.getTemporary()) { + notificatorManager.getNotificator(notificator).send(user, message, null, null); + } } return Response.noContent().build(); } diff --git a/src/main/java/org/traccar/api/resource/ReportResource.java b/src/main/java/org/traccar/api/resource/ReportResource.java index 55a96fa90..81f409c0f 100644 --- a/src/main/java/org/traccar/api/resource/ReportResource.java +++ b/src/main/java/org/traccar/api/resource/ReportResource.java @@ -113,7 +113,7 @@ public class ReportResource extends SimpleObjectResource<Report> { @QueryParam("from") Date from, @QueryParam("to") Date to) throws StorageException { permissionsService.checkRestriction(getUserId(), UserRestrictions::getDisableReports); - LogAction.logReport(getUserId(), "combined", from, to, deviceIds, groupIds); + LogAction.report(getUserId(), false, "combined", from, to, deviceIds, groupIds); return combinedReportProvider.getObjects(getUserId(), deviceIds, groupIds, from, to); } @@ -125,7 +125,7 @@ public class ReportResource extends SimpleObjectResource<Report> { @QueryParam("from") Date from, @QueryParam("to") Date to) throws StorageException { permissionsService.checkRestriction(getUserId(), UserRestrictions::getDisableReports); - LogAction.logReport(getUserId(), "route", from, to, deviceIds, groupIds); + LogAction.report(getUserId(), false, "route", from, to, deviceIds, groupIds); return routeReportProvider.getObjects(getUserId(), deviceIds, groupIds, from, to); } @@ -140,7 +140,7 @@ public class ReportResource extends SimpleObjectResource<Report> { @QueryParam("mail") boolean mail) throws StorageException { permissionsService.checkRestriction(getUserId(), UserRestrictions::getDisableReports); return executeReport(getUserId(), mail, stream -> { - LogAction.logReport(getUserId(), "route", from, to, deviceIds, groupIds); + LogAction.report(getUserId(), false, "route", from, to, deviceIds, groupIds); routeReportProvider.getExcel(stream, getUserId(), deviceIds, groupIds, from, to); }); } @@ -166,7 +166,7 @@ public class ReportResource extends SimpleObjectResource<Report> { @QueryParam("from") Date from, @QueryParam("to") Date to) throws StorageException { permissionsService.checkRestriction(getUserId(), UserRestrictions::getDisableReports); - LogAction.logReport(getUserId(), "events", from, to, deviceIds, groupIds); + LogAction.report(getUserId(), false, "events", from, to, deviceIds, groupIds); return eventsReportProvider.getObjects(getUserId(), deviceIds, groupIds, types, from, to); } @@ -182,7 +182,7 @@ public class ReportResource extends SimpleObjectResource<Report> { @QueryParam("mail") boolean mail) throws StorageException { permissionsService.checkRestriction(getUserId(), UserRestrictions::getDisableReports); return executeReport(getUserId(), mail, stream -> { - LogAction.logReport(getUserId(), "events", from, to, deviceIds, groupIds); + LogAction.report(getUserId(), false, "events", from, to, deviceIds, groupIds); eventsReportProvider.getExcel(stream, getUserId(), deviceIds, groupIds, types, from, to); }); } @@ -209,7 +209,7 @@ public class ReportResource extends SimpleObjectResource<Report> { @QueryParam("to") Date to, @QueryParam("daily") boolean daily) throws StorageException { permissionsService.checkRestriction(getUserId(), UserRestrictions::getDisableReports); - LogAction.logReport(getUserId(), "summary", from, to, deviceIds, groupIds); + LogAction.report(getUserId(), false, "summary", from, to, deviceIds, groupIds); return summaryReportProvider.getObjects(getUserId(), deviceIds, groupIds, from, to, daily); } @@ -225,7 +225,7 @@ public class ReportResource extends SimpleObjectResource<Report> { @QueryParam("mail") boolean mail) throws StorageException { permissionsService.checkRestriction(getUserId(), UserRestrictions::getDisableReports); return executeReport(getUserId(), mail, stream -> { - LogAction.logReport(getUserId(), "summary", from, to, deviceIds, groupIds); + LogAction.report(getUserId(), false, "summary", from, to, deviceIds, groupIds); summaryReportProvider.getExcel(stream, getUserId(), deviceIds, groupIds, from, to, daily); }); } @@ -251,7 +251,7 @@ public class ReportResource extends SimpleObjectResource<Report> { @QueryParam("from") Date from, @QueryParam("to") Date to) throws StorageException { permissionsService.checkRestriction(getUserId(), UserRestrictions::getDisableReports); - LogAction.logReport(getUserId(), "trips", from, to, deviceIds, groupIds); + LogAction.report(getUserId(), false, "trips", from, to, deviceIds, groupIds); return tripsReportProvider.getObjects(getUserId(), deviceIds, groupIds, from, to); } @@ -266,7 +266,7 @@ public class ReportResource extends SimpleObjectResource<Report> { @QueryParam("mail") boolean mail) throws StorageException { permissionsService.checkRestriction(getUserId(), UserRestrictions::getDisableReports); return executeReport(getUserId(), mail, stream -> { - LogAction.logReport(getUserId(), "trips", from, to, deviceIds, groupIds); + LogAction.report(getUserId(), false, "trips", from, to, deviceIds, groupIds); tripsReportProvider.getExcel(stream, getUserId(), deviceIds, groupIds, from, to); }); } @@ -291,7 +291,7 @@ public class ReportResource extends SimpleObjectResource<Report> { @QueryParam("from") Date from, @QueryParam("to") Date to) throws StorageException { permissionsService.checkRestriction(getUserId(), UserRestrictions::getDisableReports); - LogAction.logReport(getUserId(), "stops", from, to, deviceIds, groupIds); + LogAction.report(getUserId(), false, "stops", from, to, deviceIds, groupIds); return stopsReportProvider.getObjects(getUserId(), deviceIds, groupIds, from, to); } @@ -306,7 +306,7 @@ public class ReportResource extends SimpleObjectResource<Report> { @QueryParam("mail") boolean mail) throws StorageException { permissionsService.checkRestriction(getUserId(), UserRestrictions::getDisableReports); return executeReport(getUserId(), mail, stream -> { - LogAction.logReport(getUserId(), "stops", from, to, deviceIds, groupIds); + LogAction.report(getUserId(), false, "stops", from, to, deviceIds, groupIds); stopsReportProvider.getExcel(stream, getUserId(), deviceIds, groupIds, from, to); }); } @@ -326,7 +326,7 @@ public class ReportResource extends SimpleObjectResource<Report> { @Path("devices/{type:xlsx|mail}") @GET @Produces(EXCEL) - public Response geDevicesExcel( + public Response getDevicesExcel( @PathParam("type") String type) throws StorageException { permissionsService.checkRestriction(getUserId(), UserRestrictions::getDisableReports); return executeReport(getUserId(), type.equals("mail"), stream -> { diff --git a/src/main/java/org/traccar/api/resource/ServerResource.java b/src/main/java/org/traccar/api/resource/ServerResource.java index 2a72d2773..eb10ac763 100644 --- a/src/main/java/org/traccar/api/resource/ServerResource.java +++ b/src/main/java/org/traccar/api/resource/ServerResource.java @@ -161,4 +161,11 @@ public class ServerResource extends BaseResource { return cacheManager.toString(); } + @Path("reboot") + @POST + public void reboot() throws StorageException { + permissionsService.checkAdmin(getUserId()); + System.exit(130); + } + } diff --git a/src/main/java/org/traccar/api/security/LoginService.java b/src/main/java/org/traccar/api/security/LoginService.java index 930c4fa46..507288c31 100644 --- a/src/main/java/org/traccar/api/security/LoginService.java +++ b/src/main/java/org/traccar/api/security/LoginService.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 - 2023 Anton Tananaev (anton@traccar.org) + * Copyright 2022 - 2024 Anton Tananaev (anton@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import org.traccar.api.signature.TokenManager; import org.traccar.config.Config; import org.traccar.config.Keys; import org.traccar.database.LdapProvider; +import org.traccar.helper.DataConverter; import org.traccar.helper.model.UserUtil; import org.traccar.model.User; import org.traccar.storage.Storage; @@ -32,6 +33,7 @@ import jakarta.annotation.Nullable; import jakarta.inject.Inject; import jakarta.inject.Singleton; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; @Singleton @@ -58,6 +60,20 @@ public class LoginService { forceOpenId = config.getBoolean(Keys.OPENID_FORCE); } + public LoginResult login( + String scheme, String credentials) throws StorageException, GeneralSecurityException, IOException { + switch (scheme.toLowerCase()) { + case "bearer": + return login(credentials); + case "basic": + byte[] decodedBytes = DataConverter.parseBase64(credentials); + String[] auth = new String(decodedBytes, StandardCharsets.US_ASCII).split(":", 2); + return login(auth[0], auth[1], null); + default: + throw new SecurityException("Unsupported authorization scheme"); + } + } + public LoginResult login(String token) throws StorageException, GeneralSecurityException, IOException { if (serviceAccountToken != null && serviceAccountToken.equals(token)) { return new LoginResult(new ServiceAccountUser()); diff --git a/src/main/java/org/traccar/api/security/SecurityRequestFilter.java b/src/main/java/org/traccar/api/security/SecurityRequestFilter.java index 12a5dbecf..07083e7a8 100644 --- a/src/main/java/org/traccar/api/security/SecurityRequestFilter.java +++ b/src/main/java/org/traccar/api/security/SecurityRequestFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 - 2023 Anton Tananaev (anton@traccar.org) + * Copyright 2015 - 2024 Anton Tananaev (anton@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.traccar.api.resource.SessionResource; import org.traccar.database.StatisticsManager; -import org.traccar.helper.DataConverter; import org.traccar.model.User; import org.traccar.storage.StorageException; @@ -36,7 +35,6 @@ import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.SecurityContext; import java.io.IOException; import java.lang.reflect.Method; -import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.util.Date; @@ -44,15 +42,6 @@ public class SecurityRequestFilter implements ContainerRequestFilter { private static final Logger LOGGER = LoggerFactory.getLogger(SecurityRequestFilter.class); - public static String[] decodeBasicAuth(String auth) { - auth = auth.replaceFirst("[B|b]asic ", ""); - byte[] decodedBytes = DataConverter.parseBase64(auth); - if (decodedBytes != null && decodedBytes.length > 0) { - return new String(decodedBytes, StandardCharsets.US_ASCII).split(":", 2); - } - return null; - } - @Context private HttpServletRequest request; @@ -83,13 +72,8 @@ public class SecurityRequestFilter implements ContainerRequestFilter { if (authHeader != null) { try { - LoginResult loginResult; - if (authHeader.startsWith("Bearer ")) { - loginResult = loginService.login(authHeader.substring(7)); - } else { - String[] auth = decodeBasicAuth(authHeader); - loginResult = loginService.login(auth[0], auth[1], null); - } + String[] auth = authHeader.split(" "); + LoginResult loginResult = loginService.login(auth[0], auth[1]); if (loginResult != null) { User user = loginResult.getUser(); statisticsManager.registerRequest(user.getId()); diff --git a/src/main/java/org/traccar/config/Config.java b/src/main/java/org/traccar/config/Config.java index 47e1f0707..2d76a5c70 100644 --- a/src/main/java/org/traccar/config/Config.java +++ b/src/main/java/org/traccar/config/Config.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 - 2022 Anton Tananaev (anton@traccar.org) + * Copyright 2015 - 2024 Anton Tananaev (anton@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,20 +41,10 @@ public class Config { @Inject public Config(@Named("configFile") String file) throws IOException { try { - Properties mainProperties = new Properties(); try (InputStream inputStream = new FileInputStream(file)) { - mainProperties.loadFromXML(inputStream); + properties.loadFromXML(inputStream); } - String defaultConfigFile = mainProperties.getProperty("config.default"); - if (defaultConfigFile != null) { - try (InputStream inputStream = new FileInputStream(defaultConfigFile)) { - properties.loadFromXML(inputStream); - } - } - - properties.putAll(mainProperties); // override defaults - useEnvironmentVariables = Boolean.parseBoolean(System.getenv("CONFIG_USE_ENVIRONMENT_VARIABLES")) || Boolean.parseBoolean(properties.getProperty("config.useEnvironmentVariables")); @@ -102,7 +92,13 @@ public class Config { } public boolean getBoolean(ConfigKey<Boolean> key) { - return Boolean.parseBoolean(getString(key.getKey())); + String value = getString(key.getKey()); + if (value != null) { + return Boolean.parseBoolean(value); + } else { + Boolean defaultValue = key.getDefaultValue(); + return Objects.requireNonNullElse(defaultValue, false); + } } public int getInteger(ConfigKey<Integer> key) { diff --git a/src/main/java/org/traccar/config/Keys.java b/src/main/java/org/traccar/config/Keys.java index d346084bd..91d5dac5d 100644 --- a/src/main/java/org/traccar/config/Keys.java +++ b/src/main/java/org/traccar/config/Keys.java @@ -23,7 +23,7 @@ public final class Keys { } /** - * Network interface for a the protocol. If not specified, server will bind all interfaces. + * Network interface for the protocol. If not specified, server will bind all interfaces. */ public static final ConfigSuffix<String> PROTOCOL_ADDRESS = new StringConfigSuffix( ".address", @@ -33,7 +33,7 @@ public final class Keys { * Port number for the protocol. Most protocols use TCP on the transport layer. Some protocols use UDP. Some * support both TCP and UDP. */ - public static final ConfigSuffix<Integer> PROTOCOL_PORT = new IntegerConfigSuffix( + public static final ConfigSuffix<Integer> PROTOCOL_PORT = new PortConfigSuffix( ".port", List.of(KeyType.CONFIG)); @@ -320,7 +320,8 @@ public final class Keys { */ public static final ConfigKey<String> SERVER_STATISTICS = new StringConfigKey( "server.statistics", - List.of(KeyType.CONFIG)); + List.of(KeyType.CONFIG), + "https://www.traccar.org/analytics/"); /** * Fuel drop threshold value. When fuel level drops from one position to another for more the value, an event is @@ -397,7 +398,8 @@ public final class Keys { */ public static final ConfigKey<Boolean> EVENT_IGNORE_DUPLICATE_ALERTS = new BooleanConfigKey( "event.ignoreDuplicateAlerts", - List.of(KeyType.CONFIG)); + List.of(KeyType.CONFIG), + true); /** * If set to true, invalid positions will be considered for motion logic. @@ -472,7 +474,8 @@ public final class Keys { */ public static final ConfigKey<String> DATABASE_CHANGELOG = new StringConfigKey( "database.changelog", - List.of(KeyType.CONFIG)); + List.of(KeyType.CONFIG), + "./schema/changelog-master.xml"); /** * Database connection pool size. Default value is defined by the HikariCP library. @@ -598,7 +601,7 @@ public final class Keys { "uid"); /** - * LDAP attribute used as user name. Default value is 'cn'. + * LDAP attribute used as username. Default value is 'cn'. */ public static final ConfigKey<String> LDAP_NAME_ATTRIBUTE = new StringConfigKey( "ldap.nameAttribute", @@ -671,7 +674,7 @@ public final class Keys { /** * OpenID Connect Authorization URL. * This can usually be found in the documentation of your identity provider or by using the well-known - * configuration endpoint, e.g. https://auth.example.com//.well-known/openid-configuration + * configuration endpoint, e.g. https://auth.example.com/.well-known/openid-configuration * Required to enable SSO if openid.issuerUrl is not set. */ public static final ConfigKey<String> OPENID_AUTH_URL = new StringConfigKey( @@ -736,7 +739,8 @@ public final class Keys { */ public static final ConfigKey<String> MEDIA_PATH = new StringConfigKey( "media.path", - List.of(KeyType.CONFIG)); + List.of(KeyType.CONFIG), + "./media"); /** * Optional parameter to specify network interface for web interface to bind to. By default server will bind to all @@ -747,12 +751,13 @@ public final class Keys { List.of(KeyType.CONFIG)); /** - * Web interface TCP port number. By default Traccar uses port 8082. To avoid specifying port in the browser you + * Web interface TCP port number. By default, Traccar uses port 8082. To avoid specifying port in the browser you * can set it to 80 (default HTTP port). */ public static final ConfigKey<Integer> WEB_PORT = new IntegerConfigKey( "web.port", - List.of(KeyType.CONFIG)); + List.of(KeyType.CONFIG), + 8082); /** * Maximum API requests per second. Above this limit requests and delayed and throttled. @@ -770,26 +775,20 @@ public final class Keys { 600); /** - * Sanitize all strings returned via API. This is needed to fix XSS issues in the old web interface. New React-based - * interface doesn't require this. - */ - public static final ConfigKey<Boolean> WEB_SANITIZE = new BooleanConfigKey( - "web.sanitize", - List.of(KeyType.CONFIG)); - - /** * Path to the web app folder. */ public static final ConfigKey<String> WEB_PATH = new StringConfigKey( "web.path", - List.of(KeyType.CONFIG)); + List.of(KeyType.CONFIG), + "./web"); /** * Path to a folder with overrides. It can be used for branding to keep custom logos in a separate place. */ public static final ConfigKey<String> WEB_OVERRIDE = new StringConfigKey( "web.override", - List.of(KeyType.CONFIG)); + List.of(KeyType.CONFIG), + "./override"); /** * WebSocket connection timeout in milliseconds. Default timeout is 5 minutes. @@ -1172,7 +1171,8 @@ public final class Keys { */ public static final ConfigKey<String> NOTIFICATOR_TYPES = new StringConfigKey( "notificator.types", - List.of(KeyType.CONFIG)); + List.of(KeyType.CONFIG), + "web,mail,command"); /** * If the event time is too old, we should not send notifications. This parameter is the threshold value in @@ -1261,6 +1261,13 @@ public final class Keys { List.of(KeyType.CONFIG)); /** + * Block notifications for specific users. The value should be a comma-separated list of internal user ids. + */ + public static final ConfigKey<String> NOTIFICATION_BLOCK_USERS = new StringConfigKey( + "notification.block.users", + List.of(KeyType.CONFIG)); + + /** * Maximum time period for reports in seconds. Can be useful to prevent users to request unreasonably long reports. * By default, there is no limit. */ @@ -1331,7 +1338,8 @@ public final class Keys { */ public static final ConfigKey<Boolean> FILTER_ENABLE = new BooleanConfigKey( "filter.enable", - List.of(KeyType.CONFIG)); + List.of(KeyType.CONFIG), + true); /** * Filter invalid (valid field is set to false) positions. @@ -1369,7 +1377,8 @@ public final class Keys { */ public static final ConfigKey<Long> FILTER_FUTURE = new LongConfigKey( "filter.future", - List.of(KeyType.CONFIG)); + List.of(KeyType.CONFIG), + 86400L); /** * Filter records with fix time in the past. The value is specified in seconds. Records that have fix time more @@ -1426,13 +1435,20 @@ public final class Keys { List.of(KeyType.CONFIG)); /** - * Filter position if the daily limit is exceeded for the device. + * Throttle positions if the daily limit is exceeded for the device. */ public static final ConfigKey<Integer> FILTER_DAILY_LIMIT = new IntegerConfigKey( "filter.dailyLimit", List.of(KeyType.CONFIG)); /** + * Throttling interval if the limit exceeded. The value is in seconds. + */ + public static final ConfigKey<Integer> FILTER_DAILY_LIMIT_INTERVAL = new IntegerConfigKey( + "filter.dailyLimitInterval", + List.of(KeyType.CONFIG)); + + /** * If false, the server expects all locations to come sequentially (for each device). Filter checks for duplicates, * distance, speed, or time period only against the location that was last received by server. * If true, the server expects locations to come at random order (since tracking device might go offline). @@ -1580,15 +1596,16 @@ public final class Keys { */ public static final ConfigKey<Boolean> GEOCODER_ENABLE = new BooleanConfigKey( "geocoder.enable", - List.of(KeyType.CONFIG)); + List.of(KeyType.CONFIG), + true); /** - * Reverse geocoder type. Check reverse geocoding documentation for more info. By default (if the value is not - * specified) server uses Google API. + * Reverse geocoder type. Check reverse geocoding documentation for more info. */ public static final ConfigKey<String> GEOCODER_TYPE = new StringConfigKey( "geocoder.type", - List.of(KeyType.CONFIG)); + List.of(KeyType.CONFIG), + "locationiq"); /** * Geocoder server URL. Applicable only to Nominatim and Gisgraphy providers. @@ -1602,7 +1619,8 @@ public final class Keys { */ public static final ConfigKey<String> GEOCODER_KEY = new StringConfigKey( "geocoder.key", - List.of(KeyType.CONFIG)); + List.of(KeyType.CONFIG), + "pk.689d849289c8c63708068b2ff1f63b2d"); /** * Language parameter for providers that support localization (e.g. Google and Nominatim). @@ -1630,7 +1648,8 @@ public final class Keys { */ public static final ConfigKey<Boolean> GEOCODER_IGNORE_POSITIONS = new BooleanConfigKey( "geocoder.ignorePositions", - List.of(KeyType.CONFIG)); + List.of(KeyType.CONFIG), + true); /** * Boolean flag to apply reverse geocoding to invalid positions. @@ -1652,7 +1671,8 @@ public final class Keys { */ public static final ConfigKey<Boolean> GEOCODER_ON_REQUEST = new BooleanConfigKey( "geocoder.onRequest", - List.of(KeyType.CONFIG)); + List.of(KeyType.CONFIG), + true); /** * Boolean flag to enable LBS location resolution. Some devices send cell towers information and WiFi point when GPS @@ -1853,7 +1873,8 @@ public final class Keys { */ public static final ConfigKey<String> LOGGER_FILE = new StringConfigKey( "logger.file", - List.of(KeyType.CONFIG)); + List.of(KeyType.CONFIG), + "./logs/tracker-server.log"); /** * Logging level. Default value is 'info'. @@ -1861,7 +1882,8 @@ public final class Keys { */ public static final ConfigKey<String> LOGGER_LEVEL = new StringConfigKey( "logger.level", - List.of(KeyType.CONFIG)); + List.of(KeyType.CONFIG), + "info"); /** * Print full exception traces. Useful for debugging. By default shortened traces are logged. @@ -1876,7 +1898,8 @@ public final class Keys { */ public static final ConfigKey<Boolean> LOGGER_ROTATE = new BooleanConfigKey( "logger.rotate", - List.of(KeyType.CONFIG)); + List.of(KeyType.CONFIG), + true); /** * Log file rotation interval, the default rotation interval is once a day. diff --git a/src/main/java/org/traccar/config/PortConfigSuffix.java b/src/main/java/org/traccar/config/PortConfigSuffix.java new file mode 100644 index 000000000..11d6f2dbb --- /dev/null +++ b/src/main/java/org/traccar/config/PortConfigSuffix.java @@ -0,0 +1,278 @@ +package org.traccar.config; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class PortConfigSuffix extends ConfigSuffix<Integer> { + + private static final Map<String, Integer> PORTS = new HashMap<>(); + + static { + PORTS.put("gps103", 5001); + PORTS.put("tk103", 5002); + PORTS.put("gl100", 5003); + PORTS.put("gl200", 5004); + PORTS.put("t55", 5005); + PORTS.put("xexun", 5006); + PORTS.put("totem", 5007); + PORTS.put("enfora", 5008); + PORTS.put("meiligao", 5009); + PORTS.put("trv", 5010); + PORTS.put("suntech", 5011); + PORTS.put("progress", 5012); + PORTS.put("h02", 5013); + PORTS.put("jt600", 5014); + PORTS.put("huabao", 5015); + PORTS.put("v680", 5016); + PORTS.put("pt502", 5017); + PORTS.put("tr20", 5018); + PORTS.put("navis", 5019); + PORTS.put("meitrack", 5020); + PORTS.put("skypatrol", 5021); + PORTS.put("gt02", 5022); + PORTS.put("gt06", 5023); + PORTS.put("megastek", 5024); + PORTS.put("navigil", 5025); + PORTS.put("gpsgate", 5026); + PORTS.put("teltonika", 5027); + PORTS.put("mta6", 5028); + PORTS.put("tzone", 5029); + PORTS.put("tlt2h", 5030); + PORTS.put("taip", 5031); + PORTS.put("wondex", 5032); + PORTS.put("cellocator", 5033); + PORTS.put("galileo", 5034); + PORTS.put("ywt", 5035); + PORTS.put("tk102", 5036); + PORTS.put("intellitrac", 5037); + PORTS.put("gpsmta", 5038); + PORTS.put("wialon", 5039); + PORTS.put("carscop", 5040); + PORTS.put("apel", 5041); + PORTS.put("manpower", 5042); + PORTS.put("globalsat", 5043); + PORTS.put("atrack", 5044); + PORTS.put("pt3000", 5045); + PORTS.put("ruptela", 5046); + PORTS.put("topflytech", 5047); + PORTS.put("laipac", 5048); + PORTS.put("aplicom", 5049); + PORTS.put("gotop", 5050); + PORTS.put("sanav", 5051); + PORTS.put("gator", 5052); + PORTS.put("noran", 5053); + PORTS.put("m2m", 5054); + PORTS.put("osmand", 5055); + PORTS.put("easytrack", 5056); + PORTS.put("gpsmarker", 5057); + PORTS.put("khd", 5058); + PORTS.put("piligrim", 5059); + PORTS.put("stl060", 5060); + PORTS.put("cartrack", 5061); + PORTS.put("minifinder", 5062); + PORTS.put("haicom", 5063); + PORTS.put("eelink", 5064); + PORTS.put("box", 5065); + PORTS.put("freedom", 5066); + PORTS.put("telic", 5067); + PORTS.put("trackbox", 5068); + PORTS.put("visiontek", 5069); + PORTS.put("orion", 5070); + PORTS.put("riti", 5071); + PORTS.put("ulbotech", 5072); + PORTS.put("tramigo", 5073); + PORTS.put("tr900", 5074); + PORTS.put("ardi01", 5075); + PORTS.put("xt013", 5076); + PORTS.put("autofon", 5077); + PORTS.put("gosafe", 5078); + PORTS.put("tt8850", 5079); + PORTS.put("bce", 5080); + PORTS.put("xirgo", 5081); + PORTS.put("calamp", 5082); + PORTS.put("mtx", 5083); + PORTS.put("tytan", 5084); + PORTS.put("avl301", 5085); + PORTS.put("castel", 5086); + PORTS.put("mxt", 5087); + PORTS.put("cityeasy", 5088); + PORTS.put("aquila", 5089); + PORTS.put("flextrack", 5090); + PORTS.put("blackkite", 5091); + PORTS.put("adm", 5092); + PORTS.put("watch", 5093); + PORTS.put("t800x", 5094); + PORTS.put("upro", 5095); + PORTS.put("auro", 5096); + PORTS.put("disha", 5097); + PORTS.put("thinkrace", 5098); + PORTS.put("pathaway", 5099); + PORTS.put("arnavi", 5100); + PORTS.put("nvs", 5101); + PORTS.put("kenji", 5102); + PORTS.put("astra", 5103); + PORTS.put("homtecs", 5104); + PORTS.put("fox", 5105); + PORTS.put("gnx", 5106); + PORTS.put("arknav", 5107); + PORTS.put("supermate", 5108); + PORTS.put("appello", 5109); + PORTS.put("idpl", 5110); + PORTS.put("huasheng", 5111); + PORTS.put("l100", 5112); + PORTS.put("granit", 5113); + PORTS.put("carcell", 5114); + PORTS.put("obddongle", 5115); + PORTS.put("hunterpro", 5116); + PORTS.put("raveon", 5117); + PORTS.put("cradlepoint", 5118); + PORTS.put("arknavx8", 5119); + PORTS.put("autograde", 5120); + PORTS.put("oigo", 5121); + PORTS.put("jpkorjar", 5122); + PORTS.put("cguard", 5123); + PORTS.put("fifotrack", 5124); + PORTS.put("smokey", 5125); + PORTS.put("extremtrac", 5126); + PORTS.put("trakmate", 5127); + PORTS.put("at2000", 5128); + PORTS.put("maestro", 5129); + PORTS.put("ais", 5130); + PORTS.put("gt30", 5131); + PORTS.put("tmg", 5132); + PORTS.put("pretrace", 5133); + PORTS.put("pricol", 5134); + PORTS.put("siwi", 5135); + PORTS.put("starlink", 5136); + PORTS.put("dmt", 5137); + PORTS.put("xt2400", 5138); + PORTS.put("dmthttp", 5139); + PORTS.put("alematics", 5140); + PORTS.put("gps056", 5141); + PORTS.put("flexcomm", 5142); + PORTS.put("vt200", 5143); + PORTS.put("owntracks", 5144); + PORTS.put("vtfms", 5145); + PORTS.put("tlv", 5146); + PORTS.put("esky", 5147); + PORTS.put("genx", 5148); + PORTS.put("flespi", 5149); + PORTS.put("dway", 5150); + PORTS.put("recoda", 5151); + PORTS.put("oko", 5152); + PORTS.put("ivt401", 5153); + PORTS.put("sigfox", 5154); + PORTS.put("t57", 5155); + PORTS.put("spot", 5156); + PORTS.put("m2c", 5157); + PORTS.put("austinnb", 5158); + PORTS.put("opengts", 5159); + PORTS.put("cautela", 5160); + PORTS.put("continental", 5161); + PORTS.put("egts", 5162); + PORTS.put("robotrack", 5163); + PORTS.put("pt60", 5164); + PORTS.put("telemax", 5165); + PORTS.put("sabertek", 5166); + PORTS.put("retranslator", 5167); + PORTS.put("svias", 5168); + PORTS.put("eseal", 5169); + PORTS.put("freematics", 5170); + PORTS.put("avema", 5171); + PORTS.put("autotrack", 5172); + PORTS.put("tek", 5173); + PORTS.put("wristband", 5174); + PORTS.put("lacak", 5175); + PORTS.put("milesmate", 5176); + PORTS.put("anytrek", 5177); + PORTS.put("smartsole", 5178); + PORTS.put("its", 5179); + PORTS.put("xrb28", 5180); + PORTS.put("c2stek", 5181); + PORTS.put("nyitech", 5182); + PORTS.put("neos", 5183); + PORTS.put("satsol", 5184); + PORTS.put("globalstar", 5185); + PORTS.put("sanul", 5186); + PORTS.put("minifinder2", 5187); + PORTS.put("radar", 5188); + PORTS.put("techtlt", 5189); + PORTS.put("starcom", 5190); + PORTS.put("mictrack", 5191); + PORTS.put("plugin", 5192); + PORTS.put("leafspy", 5193); + PORTS.put("naviset", 5194); + PORTS.put("racedynamics", 5195); + PORTS.put("rst", 5196); + PORTS.put("pt215", 5197); + PORTS.put("pacifictrack", 5198); + PORTS.put("topin", 5199); + PORTS.put("outsafe", 5200); + PORTS.put("solarpowered", 5201); + PORTS.put("motor", 5202); + PORTS.put("omnicomm", 5203); + PORTS.put("s168", 5204); + PORTS.put("vnet", 5205); + PORTS.put("blue", 5206); + PORTS.put("pst", 5207); + PORTS.put("dingtek", 5208); + PORTS.put("wli", 5209); + PORTS.put("niot", 5210); + PORTS.put("portman", 5211); + PORTS.put("moovbox", 5212); + PORTS.put("futureway", 5213); + PORTS.put("polte", 5214); + PORTS.put("net", 5215); + PORTS.put("mobilogix", 5216); + PORTS.put("swiftech", 5217); + PORTS.put("iotm", 5218); + PORTS.put("dolphin", 5219); + PORTS.put("ennfu", 5220); + PORTS.put("navtelecom", 5221); + PORTS.put("startek", 5222); + PORTS.put("gs100", 5223); + PORTS.put("mavlink2", 5224); + PORTS.put("uux", 5225); + PORTS.put("r12w", 5226); + PORTS.put("flexiblereport", 5227); + PORTS.put("thinkpower", 5228); + PORTS.put("stb", 5229); + PORTS.put("b2316", 5230); + PORTS.put("hoopo", 5231); + PORTS.put("dualcam", 5232); + PORTS.put("xexun2", 5233); + PORTS.put("techtocruz", 5234); + PORTS.put("flexapi", 5235); + PORTS.put("dsf22", 5236); + PORTS.put("jido", 5237); + PORTS.put("armoli", 5238); + PORTS.put("teratrack", 5239); + PORTS.put("envotech", 5240); + PORTS.put("bstpl", 5241); + PORTS.put("thuraya", 5242); + PORTS.put("ndtpv6", 5243); + PORTS.put("g1rus", 5244); + PORTS.put("rftrack", 5245); + PORTS.put("vlt", 5246); + PORTS.put("transync", 5247); + PORTS.put("t622iridium", 5248); + PORTS.put("pui", 5249); + PORTS.put("nto", 5250); + PORTS.put("ramac", 5251); + PORTS.put("positrex", 5252); + PORTS.put("dragino", 5253); + PORTS.put("fleetguide", 5254); + PORTS.put("valtrack", 5255); + PORTS.put("snapper", 5256); + } + + PortConfigSuffix(String key, List<KeyType> types) { + super(key, types, null); + } + + @Override + public ConfigKey<Integer> withPrefix(String protocol) { + return new IntegerConfigKey(protocol + keySuffix, types, PORTS.get(protocol)); + } +} diff --git a/src/main/java/org/traccar/database/NotificationManager.java b/src/main/java/org/traccar/database/NotificationManager.java index 65437f0a1..60b4f246b 100644 --- a/src/main/java/org/traccar/database/NotificationManager.java +++ b/src/main/java/org/traccar/database/NotificationManager.java @@ -42,8 +42,10 @@ import jakarta.annotation.Nullable; import jakarta.inject.Inject; import jakarta.inject.Singleton; import java.util.Arrays; +import java.util.HashSet; import java.util.Map; import java.util.Map.Entry; +import java.util.Set; import java.util.stream.Collectors; @Singleton @@ -59,6 +61,7 @@ public class NotificationManager { private final boolean geocodeOnRequest; private final long timeThreshold; + private final Set<Long> blockedUsers = new HashSet<>(); @Inject public NotificationManager( @@ -71,6 +74,12 @@ public class NotificationManager { this.geocoder = geocoder; geocodeOnRequest = config.getBoolean(Keys.GEOCODER_ON_REQUEST); timeThreshold = config.getLong(Keys.NOTIFICATOR_TIME_THRESHOLD); + String blockedUsersString = config.getString(Keys.NOTIFICATION_BLOCK_USERS); + if (blockedUsersString != null) { + for (String userIdString : blockedUsersString.split(",")) { + blockedUsers.add(Long.parseLong(userIdString)); + } + } } private void updateEvent(Event event, Position position) { @@ -122,6 +131,10 @@ public class NotificationManager { notifications.forEach(notification -> { cacheManager.getNotificationUsers(notification.getId(), event.getDeviceId()).forEach(user -> { + if (blockedUsers.contains(user.getId())) { + LOGGER.info("User {} notification blocked", user.getId()); + return; + } for (String notificator : notification.getNotificatorsTypes()) { try { notificatorManager.getNotificator(notificator).send(notification, user, event, position); diff --git a/src/main/java/org/traccar/database/StatisticsManager.java b/src/main/java/org/traccar/database/StatisticsManager.java index 445e53e7c..8711289a0 100644 --- a/src/main/java/org/traccar/database/StatisticsManager.java +++ b/src/main/java/org/traccar/database/StatisticsManager.java @@ -121,7 +121,7 @@ public class StatisticsManager { } String url = config.getString(Keys.SERVER_STATISTICS); - if (url != null) { + if (url != null && !url.isEmpty()) { String time = DateUtil.formatDate(statistics.getCaptureTime()); Form form = new Form(); diff --git a/src/main/java/org/traccar/geocoder/TestGeocoder.java b/src/main/java/org/traccar/geocoder/PlusCodesGeocoder.java index 259f13c6c..5d003192f 100644 --- a/src/main/java/org/traccar/geocoder/TestGeocoder.java +++ b/src/main/java/org/traccar/geocoder/PlusCodesGeocoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 Anton Tananaev (anton@traccar.org) + * Copyright 2022 - 2024 Anton Tananaev (anton@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,9 +15,10 @@ */ package org.traccar.geocoder; +import com.google.openlocationcode.OpenLocationCode; import org.traccar.database.StatisticsManager; -public class TestGeocoder implements Geocoder { +public class PlusCodesGeocoder implements Geocoder { @Override public void setStatisticsManager(StatisticsManager statisticsManager) { @@ -25,7 +26,7 @@ public class TestGeocoder implements Geocoder { @Override public String getAddress(double latitude, double longitude, ReverseGeocoderCallback callback) { - String address = String.format("Location %f, %f", latitude, longitude); + String address = new OpenLocationCode(latitude, longitude).getCode(); if (callback != null) { callback.onSuccess(address); return null; diff --git a/src/main/java/org/traccar/handler/ComputedAttributesHandler.java b/src/main/java/org/traccar/handler/ComputedAttributesHandler.java index 4293bd1fc..d286866a5 100644 --- a/src/main/java/org/traccar/handler/ComputedAttributesHandler.java +++ b/src/main/java/org/traccar/handler/ComputedAttributesHandler.java @@ -35,12 +35,14 @@ import org.traccar.session.cache.CacheManager; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.Date; +import java.util.stream.Collectors; public class ComputedAttributesHandler extends BasePositionHandler { @@ -63,7 +65,7 @@ public class ComputedAttributesHandler extends BasePositionHandler { sandbox.allow(Math.class.getName()); List.of( Double.class, Float.class, Integer.class, Long.class, Short.class, - Character.class, Boolean.class, String.class, Byte.class) + Character.class, Boolean.class, String.class, Byte.class, Date.class) .forEach((type) -> sandbox.allow(type.getName())); features = new JexlFeatures() .localVar(config.getBoolean(Keys.PROCESSING_COMPUTED_ATTRIBUTES_LOCAL_VARIABLES)) @@ -139,7 +141,9 @@ public class ComputedAttributesHandler extends BasePositionHandler { @Override public void handlePosition(Position position, Callback callback) { - Collection<Attribute> attributes = cacheManager.getDeviceObjects(position.getDeviceId(), Attribute.class); + var attributes = cacheManager.getDeviceObjects(position.getDeviceId(), Attribute.class).stream() + .sorted(Comparator.comparing(Attribute::getPriority).reversed()) + .collect(Collectors.toUnmodifiableList()); for (Attribute attribute : attributes) { if (attribute.getAttribute() != null) { Object result = null; diff --git a/src/main/java/org/traccar/handler/DatabaseHandler.java b/src/main/java/org/traccar/handler/DatabaseHandler.java index 5d96ebb34..4619e9d34 100644 --- a/src/main/java/org/traccar/handler/DatabaseHandler.java +++ b/src/main/java/org/traccar/handler/DatabaseHandler.java @@ -42,7 +42,7 @@ public class DatabaseHandler extends BasePositionHandler { try { position.setId(storage.addObject(position, new Request(new Columns.Exclude("id")))); - statisticsManager.messageStoredCount(position.getDeviceId()); + statisticsManager.registerMessageStored(position.getDeviceId(), position.getProtocol()); } catch (Exception error) { LOGGER.warn("Failed to store position", error); } diff --git a/src/main/java/org/traccar/handler/FilterHandler.java b/src/main/java/org/traccar/handler/FilterHandler.java index 796c302fb..700fdbc13 100644 --- a/src/main/java/org/traccar/handler/FilterHandler.java +++ b/src/main/java/org/traccar/handler/FilterHandler.java @@ -53,6 +53,7 @@ public class FilterHandler extends BasePositionHandler { private final int filterMaxSpeed; private final long filterMinPeriod; private final int filterDailyLimit; + private final long filterDailyLimitInterval; private final boolean filterRelative; private final long skipLimit; private final boolean skipAttributes; @@ -77,6 +78,7 @@ public class FilterHandler extends BasePositionHandler { filterMaxSpeed = config.getInteger(Keys.FILTER_MAX_SPEED); filterMinPeriod = config.getInteger(Keys.FILTER_MIN_PERIOD) * 1000L; filterDailyLimit = config.getInteger(Keys.FILTER_DAILY_LIMIT); + filterDailyLimitInterval = config.getInteger(Keys.FILTER_DAILY_LIMIT_INTERVAL) * 1000L; filterRelative = config.getBoolean(Keys.FILTER_RELATIVE); skipLimit = config.getLong(Keys.FILTER_SKIP_LIMIT) * 1000; skipAttributes = config.getBoolean(Keys.FILTER_SKIP_ATTRIBUTES_ENABLE); @@ -151,7 +153,7 @@ public class FilterHandler extends BasePositionHandler { if (filterMaxSpeed != 0 && last != null) { double distance = position.getDouble(Position.KEY_DISTANCE); double time = position.getFixTime().getTime() - last.getFixTime().getTime(); - return UnitsConverter.knotsFromMps(distance / (time / 1000)) > filterMaxSpeed; + return time > 0 && UnitsConverter.knotsFromMps(distance / (time / 1000)) > filterMaxSpeed; } return false; } @@ -164,9 +166,12 @@ public class FilterHandler extends BasePositionHandler { return false; } - private boolean filterDailyLimit(Position position) { - if (filterDailyLimit != 0) { - return statisticsManager.messageStoredCount(position.getDeviceId()) >= filterDailyLimit; + private boolean filterDailyLimit(Position position, Position last) { + if (filterDailyLimit != 0 + && statisticsManager.messageStoredCount(position.getDeviceId()) >= filterDailyLimit) { + long lastTime = last != null ? last.getFixTime().getTime() : 0; + long interval = position.getFixTime().getTime() - lastTime; + return filterDailyLimitInterval <= 0 || interval < filterDailyLimitInterval; } return false; } @@ -216,20 +221,18 @@ public class FilterHandler extends BasePositionHandler { if (filterApproximate(position)) { filterType.append("Approximate "); } - if (filterDailyLimit(position)) { - filterType.append("DailyLimit "); - } // filter out excessive data long deviceId = position.getDeviceId(); - if (filterDuplicate || filterStatic || filterDistance > 0 || filterMaxSpeed > 0 || filterMinPeriod > 0) { - Position preceding = null; + if (filterDuplicate || filterStatic + || filterDistance > 0 || filterMaxSpeed > 0 || filterMinPeriod > 0 || filterDailyLimit > 0) { + Position preceding; if (filterRelative) { try { Date newFixTime = position.getFixTime(); preceding = getPrecedingPosition(deviceId, newFixTime); } catch (StorageException e) { - LOGGER.warn("Error retrieving preceding position; fallbacking to last received position.", e); + LOGGER.warn("Error retrieving preceding position; fall backing to last received position.", e); preceding = cacheManager.getPosition(deviceId); } } else { @@ -250,6 +253,9 @@ public class FilterHandler extends BasePositionHandler { if (filterMinPeriod(position, preceding)) { filterType.append("MinPeriod "); } + if (filterDailyLimit(position, preceding)) { + filterType.append("DailyLimit "); + } } Device device = cacheManager.getObject(Device.class, deviceId); diff --git a/src/main/java/org/traccar/handler/GeocoderHandler.java b/src/main/java/org/traccar/handler/GeocoderHandler.java index b84237856..3744700da 100644 --- a/src/main/java/org/traccar/handler/GeocoderHandler.java +++ b/src/main/java/org/traccar/handler/GeocoderHandler.java @@ -43,11 +43,7 @@ public class GeocoderHandler extends BasePositionHandler { @Override public void handlePosition(Position position, Callback callback) { - if (!ignorePositions) { - callback.processed(false); - } - - if (processInvalidPositions || position.getValid()) { + if (!ignorePositions && (processInvalidPositions || position.getValid())) { if (reuseDistance != 0) { Position lastPosition = cacheManager.getPosition(position.getDeviceId()); if (lastPosition != null && lastPosition.getAddress() != null diff --git a/src/main/java/org/traccar/helper/LogAction.java b/src/main/java/org/traccar/helper/LogAction.java index b255b9206..3c4b69a2e 100644 --- a/src/main/java/org/traccar/helper/LogAction.java +++ b/src/main/java/org/traccar/helper/LogAction.java @@ -43,14 +43,17 @@ public final class LogAction { private static final String ACTION_LOGIN = "login"; private static final String ACTION_LOGOUT = "logout"; - private static final String ACTION_DEVICE_ACCUMULATORS = "resetDeviceAccumulators"; + private static final String ACTION_ACCUMULATORS = "accumulators"; + private static final String ACTION_COMMAND = "command"; private static final String PATTERN_OBJECT = "user: %d, action: %s, object: %s, id: %d"; private static final String PATTERN_LINK = "user: %d, action: %s, owner: %s, id: %d, property: %s, id: %d"; private static final String PATTERN_LOGIN = "user: %d, action: %s, from: %s"; private static final String PATTERN_LOGIN_FAILED = "login failed from: %s"; - private static final String PATTERN_DEVICE_ACCUMULATORS = "user: %d, action: %s, deviceId: %d"; - private static final String PATTERN_REPORT = "user: %d, report: %s, from: %s, to: %s, devices: %s, groups: %s"; + private static final String PATTERN_ACCUMULATORS = "user: %d, action: %s, deviceId: %d"; + private static final String PATTERN_COMMAND_DEVICE = "user: %d, action: %s, deviceId: %d, type: %s"; + private static final String PATTERN_COMMAND_GROUP = "user: %d, action: %s, groupId: %d, type: %s"; + private static final String PATTERN_REPORT = "user: %d, %s: %s, from: %s, to: %s, devices: %s, groups: %s"; public static void create(long userId, BaseModel object) { logObjectAction(ACTION_CREATE, userId, object.getClass(), object.getId()); @@ -87,9 +90,27 @@ public final class LogAction { LOGGER.info(String.format(PATTERN_LOGIN_FAILED, remoteAddress)); } - public static void resetDeviceAccumulators(long userId, long deviceId) { + public static void resetAccumulators(long userId, long deviceId) { LOGGER.info(String.format( - PATTERN_DEVICE_ACCUMULATORS, userId, ACTION_DEVICE_ACCUMULATORS, deviceId)); + PATTERN_ACCUMULATORS, userId, ACTION_ACCUMULATORS, deviceId)); + } + + public static void command(long userId, long groupId, long deviceId, String type) { + if (groupId > 0) { + LOGGER.info(String.format(PATTERN_COMMAND_GROUP, userId, ACTION_COMMAND, groupId, type)); + } else { + LOGGER.info(String.format(PATTERN_COMMAND_DEVICE, userId, ACTION_COMMAND, deviceId, type)); + } + } + + public static void report( + long userId, boolean scheduled, String report, + Date from, Date to, List<Long> deviceIds, List<Long> groupIds) { + DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm"); + LOGGER.info(String.format( + PATTERN_REPORT, userId, scheduled ? "scheduled" : "report", report, + dateFormat.format(from), dateFormat.format(to), + deviceIds.toString(), groupIds.toString())); } private static void logObjectAction(String action, long userId, Class<?> clazz, long objectId) { @@ -112,13 +133,4 @@ public final class LogAction { LOGGER.info(String.format(PATTERN_LOGIN, userId, action, remoteAddress)); } - public static void logReport( - long userId, String report, Date from, Date to, List<Long> deviceIds, List<Long> groupIds) { - DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm"); - LOGGER.info(String.format( - PATTERN_REPORT, userId, report, - dateFormat.format(from), dateFormat.format(to), - deviceIds.toString(), groupIds.toString())); - } - } diff --git a/src/main/java/org/traccar/helper/SanitizerModule.java b/src/main/java/org/traccar/helper/SanitizerModule.java deleted file mode 100644 index af9ac5c2b..000000000 --- a/src/main/java/org/traccar/helper/SanitizerModule.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2018 Anton Tananaev (anton@traccar.org) - * - * 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.helper; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.databind.ser.std.StdSerializer; -import org.owasp.encoder.Encode; - -import java.io.IOException; - -public class SanitizerModule extends SimpleModule { - - public static class SanitizerSerializer extends StdSerializer<String> { - - protected SanitizerSerializer() { - super(String.class); - } - - @Override - public void serialize(String value, JsonGenerator gen, SerializerProvider provider) throws IOException { - gen.writeString(Encode.forHtml(value)); - } - - } - - public SanitizerModule() { - addSerializer(new SanitizerSerializer()); - } - -} diff --git a/src/main/java/org/traccar/model/Attribute.java b/src/main/java/org/traccar/model/Attribute.java index 65f2e3881..573207170 100644 --- a/src/main/java/org/traccar/model/Attribute.java +++ b/src/main/java/org/traccar/model/Attribute.java @@ -61,4 +61,14 @@ public class Attribute extends BaseModel { this.type = type; } + private int priority; + + public int getPriority() { + return priority; + } + + public void setPriority(int priority) { + this.priority = priority; + } + } diff --git a/src/main/java/org/traccar/model/Calendar.java b/src/main/java/org/traccar/model/Calendar.java index 03f1995ba..76c9a2cc1 100644 --- a/src/main/java/org/traccar/model/Calendar.java +++ b/src/main/java/org/traccar/model/Calendar.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 - 2022 Anton Tananaev (anton@traccar.org) + * Copyright 2016 - 2024 Anton Tananaev (anton@traccar.org) * Copyright 2016 Andrey Kunitsyn (andrey@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -34,6 +34,7 @@ import java.time.Duration; import java.util.Collection; import java.util.Date; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; @StorageName("tc_calendars") @@ -78,10 +79,9 @@ public class Calendar extends ExtendedModel { } } - public Collection<Period> findPeriods(Date date) { - var calendarDate = new net.fortuna.ical4j.model.Date(date); + public Set<Period> findPeriods(Date date) { return findEvents(date).stream() - .flatMap((event) -> event.getConsumedTime(calendarDate, calendarDate).stream()) + .flatMap((e) -> e.calculateRecurrenceSet(new Period(new DateTime(date), Duration.ZERO)).stream()) .collect(Collectors.toSet()); } diff --git a/src/main/java/org/traccar/model/Server.java b/src/main/java/org/traccar/model/Server.java index 6442186b6..b29fe27cc 100644 --- a/src/main/java/org/traccar/model/Server.java +++ b/src/main/java/org/traccar/model/Server.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 - 2023 Anton Tananaev (anton@traccar.org) + * Copyright 2015 - 2024 Anton Tananaev (anton@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -126,16 +126,6 @@ public class Server extends ExtendedModel implements UserRestrictions { this.zoom = zoom; } - private boolean twelveHourFormat; - - public boolean getTwelveHourFormat() { - return twelveHourFormat; - } - - public void setTwelveHourFormat(boolean twelveHourFormat) { - this.twelveHourFormat = twelveHourFormat; - } - private boolean forceSettings; public boolean getForceSettings() { diff --git a/src/main/java/org/traccar/model/User.java b/src/main/java/org/traccar/model/User.java index 8cfee0f48..9b8ee3e53 100644 --- a/src/main/java/org/traccar/model/User.java +++ b/src/main/java/org/traccar/model/User.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 - 2022 Anton Tananaev (anton@traccar.org) + * Copyright 2013 - 2024 Anton Tananaev (anton@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -133,16 +133,6 @@ public class User extends ExtendedModel implements UserRestrictions, Disableable this.zoom = zoom; } - private boolean twelveHourFormat; - - public boolean getTwelveHourFormat() { - return twelveHourFormat; - } - - public void setTwelveHourFormat(boolean twelveHourFormat) { - this.twelveHourFormat = twelveHourFormat; - } - private String coordinateFormat; public String getCoordinateFormat() { diff --git a/src/main/java/org/traccar/protocol/EasyTrackProtocolDecoder.java b/src/main/java/org/traccar/protocol/EasyTrackProtocolDecoder.java index b10ff4c64..ff192807b 100644 --- a/src/main/java/org/traccar/protocol/EasyTrackProtocolDecoder.java +++ b/src/main/java/org/traccar/protocol/EasyTrackProtocolDecoder.java @@ -92,7 +92,7 @@ public class EasyTrackProtocolDecoder extends BaseProtocolDecoder { .number("C(d+);") // coolant temperature .number("L(d+.d);") // fuel level .number("[XY][MH]d+.d+;") - .number("M(d+);") // mileage + .number("Md+.?d*;") // mileage .number("F(d+.d+);") // fuel consumption .number("T(d+);") // engine time .any() @@ -264,7 +264,6 @@ public class EasyTrackProtocolDecoder extends BaseProtocolDecoder { position.set(Position.KEY_ENGINE_LOAD, parser.nextDouble()); position.set(Position.KEY_COOLANT_TEMP, parser.nextInt()); position.set(Position.KEY_FUEL_LEVEL, parser.nextDouble()); - position.set(Position.KEY_ODOMETER, parser.nextInt()); position.set(Position.KEY_FUEL_CONSUMPTION, parser.nextDouble()); position.set(Position.KEY_HOURS, parser.nextInt()); diff --git a/src/main/java/org/traccar/protocol/Gl200TextProtocolDecoder.java b/src/main/java/org/traccar/protocol/Gl200TextProtocolDecoder.java index 775e98401..8edce9346 100644 --- a/src/main/java/org/traccar/protocol/Gl200TextProtocolDecoder.java +++ b/src/main/java/org/traccar/protocol/Gl200TextProtocolDecoder.java @@ -974,6 +974,9 @@ public class Gl200TextProtocolDecoder extends BaseProtocolDecoder { if (model.startsWith("GV") && !model.startsWith("GV6")) { position.set(Position.PREFIX_ADC + 2, v[index++].isEmpty() ? null : Integer.parseInt(v[index - 1]) * 0.001); } + if (model.startsWith("GV355CEU")) { + index += 1; // reserved + } position.set(Position.KEY_BATTERY_LEVEL, v[index++].isEmpty() ? null : Integer.parseInt(v[index - 1])); if (model.startsWith("GL5")) { diff --git a/src/main/java/org/traccar/protocol/GlobalstarProtocolDecoder.java b/src/main/java/org/traccar/protocol/GlobalstarProtocolDecoder.java index b75e612b8..654071d22 100644 --- a/src/main/java/org/traccar/protocol/GlobalstarProtocolDecoder.java +++ b/src/main/java/org/traccar/protocol/GlobalstarProtocolDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 - 2023 Anton Tananaev (anton@traccar.org) + * Copyright 2019 - 2024 Anton Tananaev (anton@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,6 @@ import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpVersion; import org.traccar.BaseHttpProtocolDecoder; -import org.traccar.config.Keys; import org.traccar.session.DeviceSession; import org.traccar.NetworkMessage; import org.traccar.Protocol; @@ -65,12 +64,6 @@ public class GlobalstarProtocolDecoder extends BaseHttpProtocolDecoder { private final XPath xPath; private final XPathExpression messageExpression; - private boolean alternative; - - public void setAlternative(boolean alternative) { - this.alternative = alternative; - } - public GlobalstarProtocolDecoder(Protocol protocol) { super(protocol); try { @@ -89,11 +82,6 @@ public class GlobalstarProtocolDecoder extends BaseHttpProtocolDecoder { } } - @Override - protected void init() { - this.alternative = getConfig().getBoolean(Keys.PROTOCOL_ALTERNATIVE.withPrefix(getProtocolName())); - } - private void sendResponse(Channel channel, String messageId) throws TransformerException { Document document = documentBuilder.newDocument(); @@ -144,6 +132,7 @@ public class GlobalstarProtocolDecoder extends BaseHttpProtocolDecoder { DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, xPath.evaluate("esn", node)); if (deviceSession != null) { + boolean atlas = "AtlasTrax".equalsIgnoreCase(getDeviceModel(deviceSession)); Position position = new Position(getProtocolName()); position.setDeviceId(deviceSession.getDeviceId()); @@ -154,7 +143,7 @@ public class GlobalstarProtocolDecoder extends BaseHttpProtocolDecoder { int flags = buf.readUnsignedByte(); int type; - if (alternative) { + if (atlas) { type = BitUtil.to(flags, 1); position.setValid(true); position.set(Position.PREFIX_IN + 1, !BitUtil.check(flags, 1)); @@ -179,7 +168,7 @@ public class GlobalstarProtocolDecoder extends BaseHttpProtocolDecoder { position.setLongitude(longitude > 180 ? longitude - 360 : longitude); int speed = 0; - if (alternative) { + if (atlas) { speed = buf.readUnsignedByte(); position.setSpeed(UnitsConverter.knotsFromKph(speed)); position.set("batteryReplace", BitUtil.check(buf.readUnsignedByte(), 7)); diff --git a/src/main/java/org/traccar/protocol/Gt06ProtocolDecoder.java b/src/main/java/org/traccar/protocol/Gt06ProtocolDecoder.java index 6c0380278..a1a9c3773 100644 --- a/src/main/java/org/traccar/protocol/Gt06ProtocolDecoder.java +++ b/src/main/java/org/traccar/protocol/Gt06ProtocolDecoder.java @@ -410,7 +410,8 @@ public class Gt06ProtocolDecoder extends BaseProtocolDecoder { } } - private String decodeAlarm(short value) { + private String decodeAlarm(short value, String model) { + boolean modelLW = model != null && model.toUpperCase().startsWith("LW"); switch (value) { case 0x01: return Position.ALARM_SOS; @@ -427,7 +428,6 @@ public class Gt06ProtocolDecoder extends BaseProtocolDecoder { return Position.ALARM_OVERSPEED; case 0x0E: case 0x0F: - case 0x19: return Position.ALARM_LOW_BATTERY; case 0x11: return Position.ALARM_POWER_OFF; @@ -438,17 +438,21 @@ public class Gt06ProtocolDecoder extends BaseProtocolDecoder { case 0x14: return Position.ALARM_DOOR; case 0x18: - return Position.ALARM_REMOVING; - case 0x23: - return Position.ALARM_FALL_DOWN; + return modelLW ? Position.ALARM_ACCIDENT : Position.ALARM_REMOVING; + case 0x19: + return modelLW ? Position.ALARM_ACCELERATION : Position.ALARM_LOW_BATTERY; + case 0x1A: case 0x28: return Position.ALARM_BRAKING; - case 0x29: - return Position.ALARM_ACCELERATION; + case 0x1B: case 0x2A: case 0x2B: case 0x2E: return Position.ALARM_CORNERING; + case 0x23: + return Position.ALARM_FALL_DOWN; + case 0x29: + return Position.ALARM_ACCELERATION; case 0x2C: return Position.ALARM_ACCIDENT; case 0x30: @@ -831,7 +835,8 @@ public class Gt06ProtocolDecoder extends BaseProtocolDecoder { int satellites = BitUtil.between(signal, 10, 15) + BitUtil.between(signal, 5, 10); position.set(Position.KEY_SATELLITES, satellites); position.set(Position.KEY_RSSI, BitUtil.to(signal, 5)); - position.set(Position.KEY_ALARM, decodeAlarm(buf.readUnsignedByte())); + position.set(Position.KEY_ALARM, decodeAlarm( + buf.readUnsignedByte(), getDeviceModel(deviceSession))); buf.readUnsignedByte(); // language position.set(Position.KEY_BATTERY_LEVEL, buf.readUnsignedByte()); buf.readUnsignedByte(); // working mode @@ -842,7 +847,7 @@ public class Gt06ProtocolDecoder extends BaseProtocolDecoder { position.set(Position.KEY_RSSI, buf.readUnsignedByte()); short alarmExtension = buf.readUnsignedByte(); if (variant != Variant.VXT01) { - position.set(Position.KEY_ALARM, decodeAlarm(alarmExtension)); + position.set(Position.KEY_ALARM, decodeAlarm(alarmExtension, getDeviceModel(deviceSession))); } } } @@ -881,7 +886,8 @@ public class Gt06ProtocolDecoder extends BaseProtocolDecoder { decodeStatus(position, buf); position.set(Position.KEY_POWER, buf.readUnsignedShort() * 0.01); position.set(Position.KEY_RSSI, buf.readUnsignedByte()); - position.set(Position.KEY_ALARM, decodeAlarm(buf.readUnsignedByte())); + position.set(Position.KEY_ALARM, decodeAlarm( + buf.readUnsignedByte(), getDeviceModel(deviceSession))); position.set("oil", buf.readUnsignedShort()); int temperature = buf.readUnsignedByte(); if (BitUtil.check(temperature, 7)) { diff --git a/src/main/java/org/traccar/protocol/HuabaoProtocolDecoder.java b/src/main/java/org/traccar/protocol/HuabaoProtocolDecoder.java index 648c5fb42..d010a8fe0 100644 --- a/src/main/java/org/traccar/protocol/HuabaoProtocolDecoder.java +++ b/src/main/java/org/traccar/protocol/HuabaoProtocolDecoder.java @@ -492,6 +492,13 @@ public class HuabaoProtocolDecoder extends BaseProtocolDecoder { position.set(Position.KEY_BATTERY, Integer.parseInt(lockStatus.substring(2, 5)) * 0.01); } break; + case 0x51: + if (length == 16) { + for (int i = 1; i <= 8; i++) { + position.set(Position.PREFIX_TEMP + i, buf.readShort()); + } + } + break; case 0x56: position.set(Position.KEY_BATTERY_LEVEL, buf.readUnsignedByte() * 10); buf.readUnsignedByte(); // reserved @@ -668,6 +675,10 @@ public class HuabaoProtocolDecoder extends BaseProtocolDecoder { position.set("fuel2", Double.parseDouble( buf.readCharSequence(6, StandardCharsets.US_ASCII).toString())); break; + case 0x00B2: + position.set(Position.KEY_ICCID, ByteBufUtil.hexDump( + buf.readSlice(10)).replaceAll("f", "")); + break; case 0x00CE: position.set(Position.KEY_POWER, buf.readUnsignedShort() * 0.01); break; diff --git a/src/main/java/org/traccar/protocol/KhdProtocolDecoder.java b/src/main/java/org/traccar/protocol/KhdProtocolDecoder.java index e88b34478..2b234ab21 100644 --- a/src/main/java/org/traccar/protocol/KhdProtocolDecoder.java +++ b/src/main/java/org/traccar/protocol/KhdProtocolDecoder.java @@ -171,7 +171,7 @@ public class KhdProtocolDecoder extends BaseProtocolDecoder { } long status = buf.readUnsignedInt(); - position.set(Position.KEY_IGNITION, BitUtil.check(status, 7 + 3 * 8)); + position.set(Position.KEY_IGNITION, !BitUtil.check(status, 7 + 3 * 8)); position.set(Position.KEY_STATUS, status); buf.readUnsignedShort(); diff --git a/src/main/java/org/traccar/protocol/SnapperFrameDecoder.java b/src/main/java/org/traccar/protocol/SnapperFrameDecoder.java new file mode 100644 index 000000000..be45346a6 --- /dev/null +++ b/src/main/java/org/traccar/protocol/SnapperFrameDecoder.java @@ -0,0 +1,44 @@ +/* + * Copyright 2024 Anton Tananaev (anton@traccar.org) + * + * 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.protocol; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import org.traccar.BaseFrameDecoder; + +public class SnapperFrameDecoder extends BaseFrameDecoder { + + @Override + protected Object decode( + ChannelHandlerContext ctx, Channel channel, ByteBuf buf) throws Exception { + + byte header = buf.getByte(buf.readerIndex()); + if (header == 'P') { + if (buf.readableBytes() >= 2) { + return buf.readRetainedSlice(2); + } + } else if (buf.readableBytes() >= 16) { + int length = buf.getIntLE(buf.readerIndex() + 12) + 12 + 4 + 9; + if (buf.readableBytes() >= length) { + return buf.readRetainedSlice(length); + } + } + + return null; + } + +} diff --git a/src/main/java/org/traccar/protocol/SnapperProtocol.java b/src/main/java/org/traccar/protocol/SnapperProtocol.java new file mode 100644 index 000000000..25a11ed3a --- /dev/null +++ b/src/main/java/org/traccar/protocol/SnapperProtocol.java @@ -0,0 +1,37 @@ +/* + * Copyright 2024 Anton Tananaev (anton@traccar.org) + * + * 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.protocol; + +import jakarta.inject.Inject; +import org.traccar.BaseProtocol; +import org.traccar.PipelineBuilder; +import org.traccar.TrackerServer; +import org.traccar.config.Config; + +public class SnapperProtocol extends BaseProtocol { + + @Inject + public SnapperProtocol(Config config) { + addServer(new TrackerServer(config, getName(), false) { + @Override + protected void addProtocolHandlers(PipelineBuilder pipeline, Config config) { + pipeline.addLast(new SnapperFrameDecoder()); + pipeline.addLast(new SnapperProtocolDecoder(SnapperProtocol.this)); + } + }); + } + +} diff --git a/src/main/java/org/traccar/protocol/SnapperProtocolDecoder.java b/src/main/java/org/traccar/protocol/SnapperProtocolDecoder.java new file mode 100644 index 000000000..ef1a4426a --- /dev/null +++ b/src/main/java/org/traccar/protocol/SnapperProtocolDecoder.java @@ -0,0 +1,229 @@ +/* + * Copyright 2024 Anton Tananaev (anton@traccar.org) + * + * 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.protocol; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import org.traccar.BaseProtocolDecoder; +import org.traccar.NetworkMessage; +import org.traccar.Protocol; +import org.traccar.helper.BitUtil; +import org.traccar.helper.Checksum; +import org.traccar.model.Position; +import org.traccar.session.DeviceSession; + +import java.io.StringReader; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.TimeZone; + +public class SnapperProtocolDecoder extends BaseProtocolDecoder { + + public SnapperProtocolDecoder(Protocol protocol) { + super(protocol); + } + + public static final int MSG_HELLO = 0x01; + public static final int MSG_SENDING_START = 0x02; + public static final int MSG_SENDING_FINISH = 0x03; + public static final int MSG_SEND_EVENTS = 0x21; + public static final int MSG_SEND_TECH_INFO = 0x23; + public static final int MSG_UPDATE_CMS_NUM = 0x24; + public static final int MSG_SEND_SYSTEM_INFO = 0x26; + public static final int MSG_SEND_USER_PHONE_NUMBERS = 0x31; + public static final int MSG_SEND_GPS_DATA = 0x32; + public static final int MSG_SEND_LBS_DATA = 0x33; + public static final int MSG_SEND_SYSTEM_STATUS = 0x34; + public static final int MSG_SEND_TRANSIT_SETTINGS = 0x35; + public static final int MSG_GET_SETTINGS = 0x36; + public static final int MSG_SEND_CONCATENATED_PACKET = 0x37; + public static final int MSG_SEND_DEBUG_INFO = 0x38; + + private void sendResponse( + Channel channel, SocketAddress remoteAddress, int index, int type, String answer) { + + if (channel != null) { + ByteBuf response = Unpooled.buffer(); + response.writeByte('K'); + response.writeByte(3); // protocol version + response.writeLongLE(0); // reserved + response.writeShortLE(0); // encryption + response.writeIntLE(answer.length()); + response.writeIntLE(0); // reserved + response.writeShortLE(index); + response.writeByte(Checksum.sum(ByteBuffer.wrap(answer.getBytes(StandardCharsets.US_ASCII)))); + response.writeShortLE(type); + response.writeCharSequence(answer, StandardCharsets.US_ASCII); + channel.writeAndFlush(new NetworkMessage(response, remoteAddress)); + } + } + + private void decodeEvents(Position position, ByteBuf buf) { + + position.set(Position.KEY_EVENT, buf.readUnsignedByte()); + buf.readUnsignedByte(); // info 1 + buf.readUnsignedByte(); // info 2 + buf.readUnsignedIntLE(); // timestamp + buf.readUnsignedByte(); // timezone + } + + private void decodeTechInfo(Position position, ByteBuf buf) { + + for (int i = 0; i < 5; i++) { + buf.readUnsignedByte(); // index + int type = buf.readUnsignedByte(); + switch (type) { + case 0x00: + position.set(Position.KEY_POWER, buf.readUnsignedByte() * 0.1); + position.set(Position.KEY_DEVICE_TEMP, buf.readByte()); + position.set(Position.KEY_RSSI, buf.readUnsignedByte()); + break; + case 0x01: + position.set("interiorTemp", buf.readByte()); + position.set("engineTemp", buf.readByte()); + buf.readUnsignedByte(); // reserved + break; + default: + buf.skipBytes(3); + break; + } + } + } + + private void decodeGpsData(Position position, ByteBuf buf) throws ParseException { + + String content = buf.readCharSequence(buf.readableBytes(), StandardCharsets.US_ASCII).toString(); + JsonObject json = Json.createReader(new StringReader(content)).readObject(); + + int flags = Integer.parseInt(json.getString("f"), 16); + if (!BitUtil.check(flags, 3)) { + return; + } + + position.setValid(BitUtil.check(flags, 1)); + if (!BitUtil.check(flags, 6)) { + position.setLatitude(-position.getLatitude()); + } + if (!BitUtil.check(flags, 7)) { + position.setLongitude(-position.getLongitude()); + } + + DateFormat dateFormat = new SimpleDateFormat("ddMMyyHHmmss"); + dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + position.setTime(dateFormat.parse(json.getString("d") + json.getString("t").split("\\.")[0])); + + String lat = json.getString("la"); + position.setLatitude(Integer.parseInt(lat.substring(0, 2)) + Double.parseDouble(lat.substring(2)) / 60); + String lon = json.getString("lo"); + position.setLongitude(Integer.parseInt(lon.substring(0, 3)) + Double.parseDouble(lon.substring(3)) / 60); + + position.setAltitude(Double.parseDouble(json.getString("a"))); + position.setSpeed(Double.parseDouble(json.getString("s"))); + position.setCourse(Double.parseDouble(json.getString("c"))); + + position.set(Position.KEY_SATELLITES, Integer.parseInt(json.getString("sv"))); + } + + @Override + protected Object decode( + Channel channel, SocketAddress remoteAddress, Object msg) throws Exception { + + ByteBuf buf = (ByteBuf) msg; + + byte header = buf.readByte(); + if (header == 'P') { + if (channel != null) { + ByteBuf response = Unpooled.wrappedBuffer(new byte[] {0x50, 0x4f}); + channel.writeAndFlush(new NetworkMessage(response, remoteAddress)); + } + return null; + } + + buf.readUnsignedByte(); // protocol version + buf.readUnsignedIntLE(); // system bonus identifier + + String serialNumber = String.valueOf(buf.readUnsignedIntLE()); + DeviceSession deviceSession = getDeviceSession(channel, remoteAddress, serialNumber); + if (deviceSession == null) { + return null; + } + + buf.readUnsignedShortLE(); // encryption + int length = buf.readIntLE(); + buf.readUnsignedByte(); // flags + buf.readUnsignedMediumLE(); // reserved + int index = buf.readUnsignedShortLE(); + buf.readUnsignedByte(); // checksum + int type = buf.readUnsignedShortLE(); + + if (type == MSG_HELLO) { + sendResponse(channel, remoteAddress, index, type, "hello"); + } else { + sendResponse(channel, remoteAddress, index, type, "OK"); + } + + Position position = new Position(getProtocolName()); + position.setDeviceId(deviceSession.getDeviceId()); + + switch (type) { + case MSG_SEND_EVENTS: + decodeEvents(position, buf); + getLastLocation(position, null); // TODO read timestamp + return position; + case MSG_SEND_TECH_INFO: + decodeTechInfo(position, buf); + getLastLocation(position, null); + return position; + case MSG_SEND_GPS_DATA: + decodeGpsData(position, buf.readSlice(length)); + return position; + case MSG_SEND_CONCATENATED_PACKET: + int count = buf.readUnsignedShortLE(); + for (int i = 0; i < count; i++) { + int partType = buf.readUnsignedShortLE(); + int partLength = buf.readUnsignedShortLE(); + switch (partType) { + case MSG_SEND_EVENTS: + decodeEvents(position, buf); + break; + case MSG_SEND_TECH_INFO: + decodeTechInfo(position, buf); + break; + case MSG_SEND_GPS_DATA: + decodeGpsData(position, buf.readSlice(partLength)); + break; + default: + buf.skipBytes(partLength); + break; + } + } + if (position.getFixTime() == null) { + getLastLocation(position, null); + } + return position; + default: + return null; + } + } + +} diff --git a/src/main/java/org/traccar/protocol/SuntechProtocolDecoder.java b/src/main/java/org/traccar/protocol/SuntechProtocolDecoder.java index 53c4a5d02..c9d6f16ef 100644 --- a/src/main/java/org/traccar/protocol/SuntechProtocolDecoder.java +++ b/src/main/java/org/traccar/protocol/SuntechProtocolDecoder.java @@ -295,6 +295,60 @@ public class SuntechProtocolDecoder extends BaseProtocolDecoder { return position; } + private int decodeSerialData(Position position, String[] values, int index) { + + int remaining = Integer.parseInt(values[index++]); + double totalFuel = 0; + while (remaining > 0) { + String attribute = values[index++]; + if (attribute.startsWith("CabAVL")) { + String[] data = attribute.split(","); + double fuel1 = Double.parseDouble(data[2]); + if (fuel1 > 0) { + totalFuel += fuel1; + position.set("fuel1", fuel1); + } + double fuel2 = Double.parseDouble(data[3]); + if (fuel2 > 0) { + totalFuel += fuel2; + position.set("fuel2", fuel2); + } + } else if (attribute.startsWith("GTSL")) { + position.set(Position.KEY_DRIVER_UNIQUE_ID, attribute.split("\\|")[4]); + } else if (attribute.contains("=")) { + String[] pair = attribute.split("="); + if (pair.length >= 2) { + String value = pair[1].trim(); + if (value.contains(".")) { + value = value.substring(0, value.indexOf('.')); + } + switch (pair[0].charAt(0)) { + case 't': + position.set(Position.PREFIX_TEMP + pair[0].charAt(2), Integer.parseInt(value, 16)); + break; + case 'N': + int fuel = Integer.parseInt(value, 16); + totalFuel += fuel; + position.set("fuel" + pair[0].charAt(2), fuel); + break; + case 'Q': + position.set("drivingQuality", Integer.parseInt(value, 16)); + break; + default: + break; + } + } + } else { + position.set("serial", attribute.trim()); + } + remaining -= attribute.length() + 1; + } + if (totalFuel > 0) { + position.set(Position.KEY_FUEL_LEVEL, totalFuel); + } + return index + 1; // checksum + } + private Position decode2356( Channel channel, SocketAddress remoteAddress, String protocol, String[] values) throws ParseException { int index = 0; @@ -371,56 +425,7 @@ public class SuntechProtocolDecoder extends BaseProtocolDecoder { position.set(Position.KEY_ALARM, decodeAlert(Integer.parseInt(values[index++]))); break; case "UEX": - int remaining = Integer.parseInt(values[index++]); - double totalFuel = 0; - while (remaining > 0) { - String attribute = values[index++]; - if (attribute.startsWith("CabAVL")) { - String[] data = attribute.split(","); - double fuel1 = Double.parseDouble(data[2]); - if (fuel1 > 0) { - totalFuel += fuel1; - position.set("fuel1", fuel1); - } - double fuel2 = Double.parseDouble(data[3]); - if (fuel2 > 0) { - totalFuel += fuel2; - position.set("fuel2", fuel2); - } - } else if (attribute.startsWith("GTSL")) { - position.set(Position.KEY_DRIVER_UNIQUE_ID, attribute.split("\\|")[4]); - } else if (attribute.contains("=")) { - String[] pair = attribute.split("="); - if (pair.length >= 2) { - String value = pair[1].trim(); - if (value.contains(".")) { - value = value.substring(0, value.indexOf('.')); - } - switch (pair[0].charAt(0)) { - case 't': - position.set(Position.PREFIX_TEMP + pair[0].charAt(2), Integer.parseInt(value, 16)); - break; - case 'N': - int fuel = Integer.parseInt(value, 16); - totalFuel += fuel; - position.set("fuel" + pair[0].charAt(2), fuel); - break; - case 'Q': - position.set("drivingQuality", Integer.parseInt(value, 16)); - break; - default: - break; - } - } - } else { - position.set("serial", attribute.trim()); - } - remaining -= attribute.length() + 1; - } - if (totalFuel > 0) { - position.set(Position.KEY_FUEL_LEVEL, totalFuel); - } - index += 1; // checksum + index = decodeSerialData(position, values, index); break; default: break; @@ -482,7 +487,8 @@ public class SuntechProtocolDecoder extends BaseProtocolDecoder { String type = values[index++]; - if (!type.equals("STT") && !type.equals("ALT") && !type.equals("BLE") && !type.equals("RES")) { + if (!type.equals("STT") && !type.equals("ALT") && !type.equals("BLE") && !type.equals("RES") + && !type.equals("UEX")) { return null; } @@ -601,34 +607,40 @@ public class SuntechProtocolDecoder extends BaseProtocolDecoder { position.set(Position.KEY_OUTPUT, Integer.parseInt(values[index++])); } - if (type.equals("ALT")) { - if (BitUtil.check(mask, 19)) { - int alertId = Integer.parseInt(values[index++]); - position.set(Position.KEY_ALARM, decodeAlert(alertId)); - } - if (BitUtil.check(mask, 20)) { - position.set("alertModifier", values[index++]); - } - if (BitUtil.check(mask, 21)) { - position.set("alertData", values[index++]); - } - } else { - if (BitUtil.check(mask, 19)) { - position.set("mode", Integer.parseInt(values[index++])); - } - if (BitUtil.check(mask, 20)) { - position.set("reason", Integer.parseInt(values[index++])); - } - if (BitUtil.check(mask, 21)) { - position.set(Position.KEY_INDEX, Integer.parseInt(values[index++])); - } + switch (type) { + case "ALT": + if (BitUtil.check(mask, 19)) { + int alertId = Integer.parseInt(values[index++]); + position.set(Position.KEY_ALARM, decodeAlert(alertId)); + } + if (BitUtil.check(mask, 20)) { + position.set("alertModifier", values[index++]); + } + if (BitUtil.check(mask, 21)) { + position.set("alertData", values[index++]); + } + break; + case "UEX": + index = decodeSerialData(position, values, index); + break; + default: + if (BitUtil.check(mask, 19)) { + position.set("mode", Integer.parseInt(values[index++])); + } + if (BitUtil.check(mask, 20)) { + position.set("reason", Integer.parseInt(values[index++])); + } + if (BitUtil.check(mask, 21)) { + position.set(Position.KEY_INDEX, Integer.parseInt(values[index++])); + } + break; } if (BitUtil.check(mask, 22)) { index += 1; // reserved } - if (BitUtil.check(mask, 23)) { + if (BitUtil.check(mask, 23) && !type.equals("UEX")) { int assignMask = Integer.parseInt(values[index++], 16); for (int i = 0; i <= 30; i++) { if (BitUtil.check(assignMask, i)) { diff --git a/src/main/java/org/traccar/protocol/TeltonikaProtocolDecoder.java b/src/main/java/org/traccar/protocol/TeltonikaProtocolDecoder.java index 6197c6c13..de42031d7 100644 --- a/src/main/java/org/traccar/protocol/TeltonikaProtocolDecoder.java +++ b/src/main/java/org/traccar/protocol/TeltonikaProtocolDecoder.java @@ -199,7 +199,8 @@ public class TeltonikaProtocolDecoder extends BaseProtocolDecoder { var fmbXXX = Set.of( "FMB001", "FMB010", "FMB002", "FMB020", "FMB003", "FMB110", "FMB120", "FMB122", "FMB125", "FMB130", "FMB140", "FMU125", "FMB900", "FMB920", "FMB962", "FMB964", "FM3001", "FMB202", "FMB204", "FMB206", - "FMT100", "MTB100", "FMP100", "MSP500"); + "FMT100", "MTB100", "FMP100", "MSP500", "FMC125", "FMM125", "FMU130", "FMC130", "FMM130", "FMB150", + "FMC150", "FMM150"); register(1, null, (p, b) -> p.set(Position.PREFIX_IN + 1, b.readUnsignedByte() > 0)); register(2, null, (p, b) -> p.set(Position.PREFIX_IN + 2, b.readUnsignedByte() > 0)); @@ -231,7 +232,7 @@ public class TeltonikaProtocolDecoder extends BaseProtocolDecoder { register(75, fmbXXX, (p, b) -> p.set(Position.PREFIX_TEMP + 4, b.readInt() * 0.1)); register(78, null, (p, b) -> { long driverUniqueId = b.readLongLE(); - if (driverUniqueId > 0) { + if (driverUniqueId != 0) { p.set(Position.KEY_DRIVER_UNIQUE_ID, String.format("%016X", driverUniqueId)); } }); @@ -243,9 +244,9 @@ public class TeltonikaProtocolDecoder extends BaseProtocolDecoder { register(85, fmbXXX, (p, b) -> p.set(Position.KEY_RPM, b.readUnsignedShort())); register(87, fmbXXX, (p, b) -> p.set(Position.KEY_OBD_ODOMETER, b.readUnsignedInt())); register(89, fmbXXX, (p, b) -> p.set("fuelLevelPercentage", b.readUnsignedByte())); - register(90, null, (p, b) -> p.set(Position.KEY_DOOR, b.readUnsignedShort())); register(110, fmbXXX, (p, b) -> p.set(Position.KEY_FUEL_CONSUMPTION, b.readUnsignedShort() * 0.1)); register(113, fmbXXX, (p, b) -> p.set(Position.KEY_BATTERY_LEVEL, b.readUnsignedByte())); + register(115, fmbXXX, (p, b) -> p.set("engineTemp", b.readShort() * 0.1)); register(179, null, (p, b) -> p.set(Position.PREFIX_OUT + 1, b.readUnsignedByte() > 0)); register(180, null, (p, b) -> p.set(Position.PREFIX_OUT + 2, b.readUnsignedByte() > 0)); register(181, null, (p, b) -> p.set(Position.KEY_PDOP, b.readUnsignedShort() * 0.1)); @@ -292,6 +293,7 @@ public class TeltonikaProtocolDecoder extends BaseProtocolDecoder { } }); register(636, fmbXXX, (p, b) -> p.set("cid4g", b.readUnsignedInt())); + register(662, fmbXXX, (p, b) -> p.set(Position.KEY_DOOR, b.readUnsignedByte() > 0)); } private void decodeGh3000Parameter(Position position, int id, ByteBuf buf, int length) { diff --git a/src/main/java/org/traccar/protocol/TrvProtocolDecoder.java b/src/main/java/org/traccar/protocol/TrvProtocolDecoder.java index 02744f8ab..1187250f8 100644 --- a/src/main/java/org/traccar/protocol/TrvProtocolDecoder.java +++ b/src/main/java/org/traccar/protocol/TrvProtocolDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 - 2019 Anton Tananaev (anton@traccar.org) + * Copyright 2015 - 2024 Anton Tananaev (anton@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -64,6 +64,7 @@ public class TrvProtocolDecoder extends BaseProtocolDecoder { .number("(d+),") // mnc .number("(d+),") // lac .number("(d+)") // cell + .number(",(dd)").optional() // alarm .groupBegin() .text(",") .expression("(") @@ -77,7 +78,7 @@ public class TrvProtocolDecoder extends BaseProtocolDecoder { .any() .compile(); - private static final Pattern PATTERN_HEARTBEAT = new PatternBuilder() + private static final Pattern PATTERN_CP01 = new PatternBuilder() .expression("[A-Z]{2,3}") .text("CP01,") .number("(ddd)") // gsm @@ -99,7 +100,7 @@ public class TrvProtocolDecoder extends BaseProtocolDecoder { .any() .compile(); - private static final Pattern PATTERN_LBS = new PatternBuilder() + private static final Pattern PATTERN_AP02 = new PatternBuilder() .expression("[A-Z]{2,3}") .text("AP02,") .expression("[^,]+,") // language @@ -118,6 +119,19 @@ public class TrvProtocolDecoder extends BaseProtocolDecoder { .expression("(.*)") // wifi .compile(); + private static final Pattern PATTERN_AP03 = new PatternBuilder() + .expression("[A-Z]{2,3}") + .text("AP03,") + .number("(ddd)") // rssi + .number("(ddd)") // satellites + .number("(ddd)") // battery level + .number("d") // space + .number("xx") // fortification state + .number("dd,") // working mode + .number("(d+),") // steps + .number("d+") // rolls frequency + .compile(); + private Boolean decodeOptionalValue(Parser parser, int activeValue) { int value = parser.nextInt(); if (value != 0) { @@ -183,7 +197,7 @@ public class TrvProtocolDecoder extends BaseProtocolDecoder { if (type.equals("CP01")) { - Parser parser = new Parser(PATTERN_HEARTBEAT, sentence); + Parser parser = new Parser(PATTERN_CP01, sentence); if (!parser.matches()) { return null; } @@ -234,6 +248,20 @@ public class TrvProtocolDecoder extends BaseProtocolDecoder { parser.nextInt(), parser.nextInt(), parser.nextInt(), parser.nextInt())); if (parser.hasNext()) { + switch (parser.nextInt()) { + case 1: + position.set(Position.KEY_ALARM, Position.ALARM_SOS); + break; + case 5: + case 6: + position.set(Position.KEY_ALARM, Position.ALARM_FALL_DOWN); + break; + default: + break; + } + } + + if (parser.hasNext()) { decodeWifi(network, parser.next()); } @@ -243,7 +271,7 @@ public class TrvProtocolDecoder extends BaseProtocolDecoder { } else if (type.equals("AP02")) { - Parser parser = new Parser(PATTERN_LBS, sentence); + Parser parser = new Parser(PATTERN_AP02, sentence); if (!parser.matches()) { return null; } @@ -275,6 +303,61 @@ public class TrvProtocolDecoder extends BaseProtocolDecoder { return position; + } else if (type.equals("AP03")) { + + Parser parser = new Parser(PATTERN_AP03, sentence); + if (!parser.matches()) { + return null; + } + + Position position = new Position(getProtocolName()); + position.setDeviceId(deviceSession.getDeviceId()); + + getLastLocation(position, null); + + position.set(Position.KEY_RSSI, parser.nextInt()); + position.set(Position.KEY_SATELLITES, parser.nextInt()); + position.set(Position.KEY_BATTERY_LEVEL, parser.nextInt()); + position.set(Position.KEY_STEPS, parser.nextInt()); + + return position; + + } else if (type.equals("AP49") || type.equals("APHT") || type.equals("APHP") || type.equals("AP50")) { + + Position position = new Position(getProtocolName()); + position.setDeviceId(deviceSession.getDeviceId()); + + getLastLocation(position, null); + + String[] values = sentence.split(","); + + switch (type) { + case "AP49": + position.set(Position.KEY_HEART_RATE, Integer.parseInt(values[1])); + break; + case "APHT": + position.set(Position.KEY_HEART_RATE, Integer.parseInt(values[1])); + position.set("pressureSystolic", Integer.parseInt(values[2])); + position.set("pressureDiastolic", Integer.parseInt(values[3])); + break; + case "APHP": + position.set(Position.KEY_HEART_RATE, Integer.parseInt(values[1])); + position.set("pressureSystolic", Integer.parseInt(values[2])); + position.set("pressureDiastolic", Integer.parseInt(values[3])); + position.set("spo2", Integer.parseInt(values[4])); + position.set("bloodSugar", Double.parseDouble(values[5])); + position.set("temperature", Double.parseDouble(values[6])); + break; + case "AP50": + position.set("temperature", Double.parseDouble(values[1])); + position.set(Position.KEY_BATTERY_LEVEL, Integer.parseInt(values[2])); + break; + default: + break; + } + + return position; + } return null; diff --git a/src/main/java/org/traccar/reports/SummaryReportProvider.java b/src/main/java/org/traccar/reports/SummaryReportProvider.java index ffde0b067..5bd7e51b3 100644 --- a/src/main/java/org/traccar/reports/SummaryReportProvider.java +++ b/src/main/java/org/traccar/reports/SummaryReportProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 - 2023 Anton Tananaev (anton@traccar.org) + * Copyright 2016 - 2024 Anton Tananaev (anton@traccar.org) * Copyright 2016 Andrey Kunitsyn (andrey@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -105,16 +105,11 @@ public class SummaryReportProvider { result.setDistance(PositionUtil.calculateDistance(first, last, !ignoreOdometer)); result.setSpentFuel(reportUtils.calculateFuel(first, last)); - long durationMilliseconds; if (first.hasAttribute(Position.KEY_HOURS) && last.hasAttribute(Position.KEY_HOURS)) { - durationMilliseconds = last.getLong(Position.KEY_HOURS) - first.getLong(Position.KEY_HOURS); - result.setEngineHours(durationMilliseconds); - } else { - durationMilliseconds = last.getFixTime().getTime() - first.getFixTime().getTime(); - } - - if (durationMilliseconds > 0) { - result.setAverageSpeed(UnitsConverter.knotsFromMps(result.getDistance() * 1000 / durationMilliseconds)); + result.setStartHours(first.getLong(Position.KEY_HOURS)); + result.setEndHours(last.getLong(Position.KEY_HOURS)); + result.setAverageSpeed(UnitsConverter.knotsFromMps( + result.getDistance() * 1000 / result.getEngineHours())); } if (!ignoreOdometer @@ -142,15 +137,13 @@ public class SummaryReportProvider { if (daily) { while (from.truncatedTo(ChronoUnit.DAYS).isBefore(to.truncatedTo(ChronoUnit.DAYS))) { ZonedDateTime fromDay = from.truncatedTo(ChronoUnit.DAYS); - ZonedDateTime nextDay = fromDay.plus(1, ChronoUnit.DAYS); + ZonedDateTime nextDay = fromDay.plusDays(1); results.addAll(calculateDeviceResult( device, Date.from(from.toInstant()), Date.from(nextDay.toInstant()), fast)); from = nextDay; } - results.addAll(calculateDeviceResult(device, Date.from(from.toInstant()), Date.from(to.toInstant()), fast)); - } else { - results.addAll(calculateDeviceResult(device, Date.from(from.toInstant()), Date.from(to.toInstant()), fast)); } + results.addAll(calculateDeviceResult(device, Date.from(from.toInstant()), Date.from(to.toInstant()), fast)); return results; } diff --git a/src/main/java/org/traccar/reports/model/SummaryReportItem.java b/src/main/java/org/traccar/reports/model/SummaryReportItem.java index 44a15cf1d..b88cabe49 100644 --- a/src/main/java/org/traccar/reports/model/SummaryReportItem.java +++ b/src/main/java/org/traccar/reports/model/SummaryReportItem.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 - 2020 Anton Tananaev (anton@traccar.org) + * Copyright 2016 - 2024 Anton Tananaev (anton@traccar.org) * Copyright 2016 - 2017 Andrey Kunitsyn (andrey@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,13 +18,28 @@ package org.traccar.reports.model; public class SummaryReportItem extends BaseReportItem { - private long engineHours; // milliseconds - public long getEngineHours() { - return engineHours; + return endHours - startHours; + } + + private long startHours; // milliseconds + + public long getStartHours() { + return startHours; + } + + public void setStartHours(long startHours) { + this.startHours = startHours; } - public void setEngineHours(long engineHours) { - this.engineHours = engineHours; + private long endHours; // milliseconds + + public long getEndHours() { + return endHours; } + + public void setEndHours(long endHours) { + this.endHours = endHours; + } + } diff --git a/src/main/java/org/traccar/schedule/TaskExpirations.java b/src/main/java/org/traccar/schedule/TaskExpirations.java index 94f855c5f..e16dcd86c 100644 --- a/src/main/java/org/traccar/schedule/TaskExpirations.java +++ b/src/main/java/org/traccar/schedule/TaskExpirations.java @@ -111,7 +111,7 @@ public class TaskExpirations implements ScheduleTask { } if (config.getBoolean(Keys.NOTIFICATION_EXPIRATION_DEVICE)) { - long reminder = config.getLong(Keys.NOTIFICATION_EXPIRATION_USER_REMINDER); + long reminder = config.getLong(Keys.NOTIFICATION_EXPIRATION_DEVICE_REMINDER); var devices = storage.getObjects(Device.class, new Request(new Columns.All())); for (Device device : devices) { if (checkTimeTrigger(device, currentTime, 0)) { diff --git a/src/main/java/org/traccar/schedule/TaskReports.java b/src/main/java/org/traccar/schedule/TaskReports.java index e0fa6f8d6..070fa9d2b 100644 --- a/src/main/java/org/traccar/schedule/TaskReports.java +++ b/src/main/java/org/traccar/schedule/TaskReports.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 Anton Tananaev (anton@traccar.org) + * Copyright 2023 - 2024 Anton Tananaev (anton@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import com.google.inject.servlet.ServletScopes; import net.fortuna.ical4j.model.Period; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.traccar.helper.LogAction; import org.traccar.model.BaseModel; import org.traccar.model.Calendar; import org.traccar.model.Device; @@ -42,7 +43,9 @@ import org.traccar.storage.query.Request; import jakarta.inject.Inject; import java.util.Collections; import java.util.Date; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -80,8 +83,9 @@ public class TaskReports implements ScheduleTask { var lastEvents = calendar.findPeriods(lastCheck); var currentEvents = calendar.findPeriods(currentCheck); - if (!lastEvents.isEmpty() && currentEvents.isEmpty()) { - Period period = lastEvents.iterator().next(); + Set<Period> finishedEvents = new HashSet<>(lastEvents); + finishedEvents.removeAll(currentEvents); + for (Period period : finishedEvents) { RequestScoper scope = ServletScopes.scopeRequest(Collections.emptyMap()); try (RequestScoper.CloseableScope ignored = scope.open()) { executeReport(report, period.getStart(), period.getEnd()); @@ -110,6 +114,7 @@ public class TaskReports implements ScheduleTask { ReportMailer reportMailer = injector.getInstance(ReportMailer.class); for (User user : users) { + LogAction.report(user.getId(), true, report.getType(), from, to, deviceIds, groupIds); switch (report.getType()) { case "events": var eventsReportProvider = injector.getInstance(EventsReportProvider.class); diff --git a/src/main/java/org/traccar/storage/DatabaseModule.java b/src/main/java/org/traccar/storage/DatabaseModule.java index 9d9e5bd5e..cd66d7298 100644 --- a/src/main/java/org/traccar/storage/DatabaseModule.java +++ b/src/main/java/org/traccar/storage/DatabaseModule.java @@ -24,6 +24,7 @@ import liquibase.Liquibase; import liquibase.database.Database; import liquibase.database.DatabaseFactory; import liquibase.exception.LiquibaseException; +import liquibase.exception.LockException; import liquibase.resource.DirectoryResourceAccessor; import liquibase.resource.ResourceAccessor; import org.traccar.config.Config; @@ -78,22 +79,27 @@ public class DatabaseModule extends AbstractModule { DataSource dataSource = new HikariDataSource(hikariConfig); - if (config.hasKey(Keys.DATABASE_CHANGELOG)) { + String changelog = config.getString(Keys.DATABASE_CHANGELOG); + if (changelog != null && !changelog.isEmpty()) { ResourceAccessor resourceAccessor = new DirectoryResourceAccessor(new File(".")); - Database database = DatabaseFactory.getInstance().openDatabase( - config.getString(Keys.DATABASE_URL), - config.getString(Keys.DATABASE_USER), - config.getString(Keys.DATABASE_PASSWORD), - config.getString(Keys.DATABASE_DRIVER), - null, null, null, resourceAccessor); + System.setProperty("liquibase.changelogLockWaitTimeInMinutes", "1"); - String changelog = config.getString(Keys.DATABASE_CHANGELOG); - - try (Liquibase liquibase = new Liquibase(changelog, resourceAccessor, database)) { - liquibase.clearCheckSums(); - liquibase.update(new Contexts()); + try { + Database database = DatabaseFactory.getInstance().openDatabase( + config.getString(Keys.DATABASE_URL), + config.getString(Keys.DATABASE_USER), + config.getString(Keys.DATABASE_PASSWORD), + config.getString(Keys.DATABASE_DRIVER), + null, null, null, resourceAccessor); + + try (Liquibase liquibase = new Liquibase(changelog, resourceAccessor, database)) { + liquibase.clearCheckSums(); + liquibase.update(new Contexts()); + } + } catch (LockException e) { + throw new DatabaseLockException(); } } @@ -101,3 +107,12 @@ public class DatabaseModule extends AbstractModule { } } + +class DatabaseLockException extends RuntimeException { + DatabaseLockException() { + super("Database is in a locked state. " + + "It could be due to early service termination on a previous launch. " + + "To unlock you can run this query: 'UPDATE DATABASECHANGELOGLOCK SET locked = 0'. " + + "Make sure the schema is up to date before unlocking the database."); + } +} diff --git a/src/main/java/org/traccar/web/ModernDefaultServlet.java b/src/main/java/org/traccar/web/DefaultOverrideServlet.java index a7c8cdb29..14b441f86 100644 --- a/src/main/java/org/traccar/web/ModernDefaultServlet.java +++ b/src/main/java/org/traccar/web/DefaultOverrideServlet.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 Anton Tananaev (anton@traccar.org) + * Copyright 2023 - 2024 Anton Tananaev (anton@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,12 +24,12 @@ import jakarta.inject.Inject; import java.io.File; import java.io.IOException; -public class ModernDefaultServlet extends DefaultServlet { +public class DefaultOverrideServlet extends DefaultServlet { private Resource overrideResource; @Inject - public ModernDefaultServlet(Config config) { + public DefaultOverrideServlet(Config config) { String override = config.getString(Keys.WEB_OVERRIDE); if (override != null) { overrideResource = Resource.newResource(new File(override)); diff --git a/src/main/java/org/traccar/web/WebServer.java b/src/main/java/org/traccar/web/WebServer.java index 4759942b1..68fcf52de 100644 --- a/src/main/java/org/traccar/web/WebServer.java +++ b/src/main/java/org/traccar/web/WebServer.java @@ -139,7 +139,7 @@ public class WebServer implements LifecycleObject { } private void initWebApp(ServletContextHandler servletHandler) { - ServletHolder servletHolder = new ServletHolder(new ModernDefaultServlet(config)); + ServletHolder servletHolder = new ServletHolder(new DefaultOverrideServlet(config)); servletHolder.setInitParameter("resourceBase", new File(config.getString(Keys.WEB_PATH)).getAbsolutePath()); servletHolder.setInitParameter("dirAllowed", "false"); if (config.getBoolean(Keys.WEB_DEBUG)) { |