From e40b26908c32560c06c9fe47e9ab42f15054c586 Mon Sep 17 00:00:00 2001 From: Anton Tananaev Date: Thu, 23 Nov 2023 13:30:04 -0800 Subject: [PATCH 01/11] Support null timezone --- src/main/java/org/traccar/BaseProtocolDecoder.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/traccar/BaseProtocolDecoder.java b/src/main/java/org/traccar/BaseProtocolDecoder.java index 69ca0ccc65b..97762e8ca38 100644 --- a/src/main/java/org/traccar/BaseProtocolDecoder.java +++ b/src/main/java/org/traccar/BaseProtocolDecoder.java @@ -125,12 +125,13 @@ protected TimeZone getTimeZone(long deviceId) { } protected TimeZone getTimeZone(long deviceId, String defaultTimeZone) { - TimeZone result = TimeZone.getTimeZone(defaultTimeZone); String timeZoneName = AttributeUtil.lookup(cacheManager, Keys.DECODER_TIMEZONE, deviceId); if (timeZoneName != null) { - result = TimeZone.getTimeZone(timeZoneName); + return TimeZone.getTimeZone(timeZoneName); + } else if (defaultTimeZone != null) { + return TimeZone.getTimeZone(defaultTimeZone); } - return result; + return null; } public DeviceSession getDeviceSession(Channel channel, SocketAddress remoteAddress, String... uniqueIds) { From c4d2cf73f5a66759f7940ba4c11ec63976c05367 Mon Sep 17 00:00:00 2001 From: Anton Tananaev Date: Thu, 23 Nov 2023 13:37:36 -0800 Subject: [PATCH 02/11] Improve GT06 timezone handling --- .../traccar/protocol/Gt06ProtocolDecoder.java | 31 +++++++------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/traccar/protocol/Gt06ProtocolDecoder.java b/src/main/java/org/traccar/protocol/Gt06ProtocolDecoder.java index cf7cd12d302..7ee47dd8653 100644 --- a/src/main/java/org/traccar/protocol/Gt06ProtocolDecoder.java +++ b/src/main/java/org/traccar/protocol/Gt06ProtocolDecoder.java @@ -480,28 +480,21 @@ private Object decodeBasic(Channel channel, SocketAddress remoteAddress, ByteBuf buf.readUnsignedShort(); // type deviceSession = getDeviceSession(channel, remoteAddress, imei); - if (deviceSession != null && !deviceSession.contains(DeviceSession.KEY_TIMEZONE)) { - deviceSession.set(DeviceSession.KEY_TIMEZONE, getTimeZone(deviceSession.getDeviceId())); - } - - if (dataLength > 10) { - int extensionBits = buf.readUnsignedShort(); - int hours = (extensionBits >> 4) / 100; - int minutes = (extensionBits >> 4) % 100; - int offset = (hours * 60 + minutes) * 60; - if ((extensionBits & 0x8) != 0) { - offset = -offset; - } - if (deviceSession != null) { - TimeZone timeZone = deviceSession.get(DeviceSession.KEY_TIMEZONE); - if (timeZone.getRawOffset() == 0) { - timeZone.setRawOffset(offset * 1000); - deviceSession.set(DeviceSession.KEY_TIMEZONE, timeZone); + if (deviceSession != null) { + TimeZone timeZone = getTimeZone(deviceSession.getDeviceId(), null); + if (timeZone == null && dataLength > 10) { + int extensionBits = buf.readUnsignedShort(); + int hours = (extensionBits >> 4) / 100; + int minutes = (extensionBits >> 4) % 100; + int offset = (hours * 60 + minutes) * 60; + if ((extensionBits & 0x8) != 0) { + offset = -offset; } + timeZone = TimeZone.getTimeZone("UTC"); + timeZone.setRawOffset(offset * 1000); } - } + deviceSession.set(DeviceSession.KEY_TIMEZONE, timeZone); - if (deviceSession != null) { sendResponse(channel, false, type, buf.getShort(buf.writerIndex() - 6), null); } From 41b5577dd8574309104c880191a35ba3bca600d4 Mon Sep 17 00:00:00 2001 From: Anton Tananaev Date: Thu, 23 Nov 2023 14:06:02 -0800 Subject: [PATCH 03/11] Fix RST ignition value --- .../traccar/protocol/RstProtocolDecoder.java | 23 +++++++++++-------- .../protocol/RstProtocolDecoderTest.java | 4 ++++ 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/traccar/protocol/RstProtocolDecoder.java b/src/main/java/org/traccar/protocol/RstProtocolDecoder.java index fcc96fbf10d..d53675b7fcd 100644 --- a/src/main/java/org/traccar/protocol/RstProtocolDecoder.java +++ b/src/main/java/org/traccar/protocol/RstProtocolDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 - 2021 Anton Tananaev (anton@traccar.org) + * Copyright 2019 - 2023 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. @@ -42,7 +42,7 @@ public RstProtocolDecoder(Protocol protocol) { .expression("(.{5});") // firmware .number("(d{9});") // serial number .number("(d+);") // index - .number("(d+);") // type + .number("d+;") // type .groupBegin() .number("(dd)-(dd)-(dddd) ") // event date .number("(dd):(dd):(dd);") // event time @@ -87,7 +87,6 @@ protected Object decode( String firmware = parser.next(); String serial = parser.next(); int index = parser.nextInt(); - int type = parser.nextInt(); if (channel != null) { String response = "RST;A;" + model + ";" + firmware + ";" + serial + ";" + index + ";6;FIM;"; @@ -115,9 +114,16 @@ protected Object decode( position.set(Position.KEY_SATELLITES, parser.nextInt()); position.set(Position.KEY_HDOP, parser.nextInt()); - position.set(Position.PREFIX_IN + 1, parser.nextHexInt()); - position.set(Position.PREFIX_IN + 2, parser.nextHexInt()); - position.set(Position.PREFIX_IN + 3, parser.nextHexInt()); + + int inputs1 = parser.nextHexInt(); + int inputs2 = parser.nextHexInt(); + int inputs3 = parser.nextHexInt(); + position.set(Position.PREFIX_IN + 1, inputs1); + position.set(Position.PREFIX_IN + 2, inputs2); + position.set(Position.PREFIX_IN + 3, inputs3); + + position.set(Position.KEY_IGNITION, BitUtil.check(inputs2, 7)); + position.set(Position.PREFIX_OUT + 1, parser.nextHexInt()); position.set(Position.PREFIX_OUT + 2, parser.nextHexInt()); position.set(Position.KEY_POWER, parser.nextDouble()); @@ -125,10 +131,7 @@ protected Object decode( position.set(Position.KEY_ODOMETER, parser.nextInt()); position.set(Position.KEY_RSSI, parser.nextInt()); position.set(Position.PREFIX_TEMP + 1, (int) parser.nextHexInt().byteValue()); - - int status = (parser.nextHexInt() << 8) + parser.nextHexInt(); - position.set(Position.KEY_IGNITION, BitUtil.check(status, 7)); - position.set(Position.KEY_STATUS, status); + position.set(Position.KEY_STATUS, (parser.nextHexInt() << 8) + parser.nextHexInt()); return position; diff --git a/src/test/java/org/traccar/protocol/RstProtocolDecoderTest.java b/src/test/java/org/traccar/protocol/RstProtocolDecoderTest.java index 13095765b8a..0e8aefe518e 100644 --- a/src/test/java/org/traccar/protocol/RstProtocolDecoderTest.java +++ b/src/test/java/org/traccar/protocol/RstProtocolDecoderTest.java @@ -14,6 +14,10 @@ public void testDecode() throws Exception { verifyNull(decoder, text( "RST;A;RST-MINIv2;V7.04;008051261;124;29;04-04-2021 17:27:26;04-04-2021 17:27:26;-1.280811;-47.931755;7353;79;1;14;7315;26;10;0;1855;0;0;0;0;5;5;-1.280821;-47.931747;04-04-2021 17:52:23;6;-1.280863;-47.931770;04-04-2021 18:12:19;5;-1.280844;-47.931763;04-04-2021 17:28:02;5;-1.280900;-47.931770;04-04-2021 19:04:27;4;-1.280843;-47.931747;04-04-2021 18:21:45;04-04-2021 19:29:59;04-04-2021 19:29:59;-1.280770;-47.931595;1;15;0;0;0;0;FIM;")); + verifyAttribute(decoder, text( + "RST;A;RST-MINI-4Gv3;V9.10;009521405;13;1;21-11-2023 20:04:18;21-11-2023 20:04:18;-12.923627;-38.388287;1;165;29;1;5;2;00;B0;00;1A;02;11.89;3.90;73;31;FE;0000;01;40;00800061;0;184;2;4;4;6;434.0000;2;0;-49;1815;37391;724;255;263;00000000;FIM;"), + Position.KEY_IGNITION, true); + verifyPosition(decoder, text( "RST;L;RST-MINIv2;V7.02;008068078;61;1;27-01-2020 21:36:33;27-01-2020 21:36:33;-16.696159;-49.284275;0;67;786;1;15;0;00;B0;00;19;06;12.42;4.16;79;20;FE;0000;01;E0;00800020;0;467;FIM;")); From 25c5e09b02ae9e498562f3fe9a80bd6d7463a11b Mon Sep 17 00:00:00 2001 From: Anton Tananaev Date: Sat, 25 Nov 2023 08:06:25 -0800 Subject: [PATCH 04/11] Support JT705A additional data --- .../protocol/HuabaoProtocolDecoder.java | 43 +++++++++++++++++-- .../protocol/Jt600ProtocolDecoder.java | 12 ++---- .../protocol/HuabaoProtocolDecoderTest.java | 10 +++++ 3 files changed, 53 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/traccar/protocol/HuabaoProtocolDecoder.java b/src/main/java/org/traccar/protocol/HuabaoProtocolDecoder.java index 6e837337354..88120912081 100644 --- a/src/main/java/org/traccar/protocol/HuabaoProtocolDecoder.java +++ b/src/main/java/org/traccar/protocol/HuabaoProtocolDecoder.java @@ -770,12 +770,15 @@ private Position decodeLocation2(DeviceSession deviceSession, ByteBuf buf, int t int battery = buf.readUnsignedByte(); if (battery <= 100) { position.set(Position.KEY_BATTERY_LEVEL, battery); - } else if (battery == 0xAA) { + } else if (battery == 0xAA || battery == 0xAB) { position.set(Position.KEY_CHARGE, true); } - position.setNetwork(new Network(CellTower.fromCidLac( - getConfig(), buf.readUnsignedInt(), buf.readUnsignedShort()))); + long cid = buf.readUnsignedInt(); + int lac = buf.readUnsignedShort(); + if (cid > 0 && lac > 0) { + position.setNetwork(new Network(CellTower.fromCidLac(getConfig(), cid, lac))); + } int product = buf.readUnsignedByte(); int status = buf.readUnsignedShort(); @@ -787,6 +790,9 @@ private Position decodeLocation2(DeviceSession deviceSession, ByteBuf buf, int t } } else if (product == 3) { position.set(Position.KEY_BLOCKED, BitUtil.check(status, 5)); + if (BitUtil.check(alarm, 0)) { + position.set(Position.KEY_ALARM, Position.ALARM_OVERSPEED); + } if (BitUtil.check(alarm, 1)) { position.set(Position.KEY_ALARM, Position.ALARM_LOW_POWER); } @@ -796,6 +802,12 @@ private Position decodeLocation2(DeviceSession deviceSession, ByteBuf buf, int t if (BitUtil.check(alarm, 3)) { position.set(Position.KEY_ALARM, Position.ALARM_LOW_BATTERY); } + if (BitUtil.check(alarm, 5)) { + position.set(Position.KEY_ALARM, Position.ALARM_GEOFENCE_ENTER); + } + if (BitUtil.check(alarm, 6)) { + position.set(Position.KEY_ALARM, Position.ALARM_GEOFENCE_EXIT); + } } position.set(Position.KEY_STATUS, status); @@ -807,6 +819,28 @@ private Position decodeLocation2(DeviceSession deviceSession, ByteBuf buf, int t case 0x02: position.setAltitude(buf.readShort()); break; + case 0x10: + position.set("wakeSource", buf.readUnsignedByte()); + break; + case 0x0A: + if (length == 3) { + buf.readUnsignedShort(); // mcc + buf.readUnsignedByte(); // mnc + } else { + buf.skipBytes(length); + } + break; + case 0x0B: + position.set("lockCommand", buf.readUnsignedByte()); + if (length >= 5 && length <= 6) { + position.set("lockCard", buf.readUnsignedInt()); + } else if (length >= 7) { + position.set("lockPassword", buf.readCharSequence(6, StandardCharsets.US_ASCII).toString()); + } + if (length % 2 == 0) { + position.set("unlockResult", buf.readUnsignedByte()); + } + break; case 0x0C: int x = buf.readUnsignedShort(); if (x > 0x8000) { @@ -822,6 +856,9 @@ private Position decodeLocation2(DeviceSession deviceSession, ByteBuf buf, int t } position.set("tilt", String.format("[%d,%d,%d]", x, y, z)); break; + case 0xFC: + position.set(Position.KEY_GEOFENCE, buf.readUnsignedByte()); + break; default: buf.skipBytes(length); break; diff --git a/src/main/java/org/traccar/protocol/Jt600ProtocolDecoder.java b/src/main/java/org/traccar/protocol/Jt600ProtocolDecoder.java index 1b6d471b4c3..eca7e2d1127 100644 --- a/src/main/java/org/traccar/protocol/Jt600ProtocolDecoder.java +++ b/src/main/java/org/traccar/protocol/Jt600ProtocolDecoder.java @@ -105,15 +105,9 @@ static void decodeBinaryLocation(ByteBuf buf, Position position) { double longitude = convertCoordinate(BcdUtil.readInteger(buf, 9)); byte flags = buf.readByte(); - position.setValid((flags & 0x1) == 0x1); - if ((flags & 0x2) == 0) { - latitude = -latitude; - } - position.setLatitude(latitude); - if ((flags & 0x4) == 0) { - longitude = -longitude; - } - position.setLongitude(longitude); + position.setValid(BitUtil.check(flags, 0)); + position.setLatitude(BitUtil.check(flags, 1) ? latitude : -latitude); + position.setLongitude(BitUtil.check(flags, 2) ? longitude : -longitude); position.setSpeed(BcdUtil.readInteger(buf, 2)); position.setCourse(buf.readUnsignedByte() * 2.0); diff --git a/src/test/java/org/traccar/protocol/HuabaoProtocolDecoderTest.java b/src/test/java/org/traccar/protocol/HuabaoProtocolDecoderTest.java index 22c34c7ac71..164635109d0 100644 --- a/src/test/java/org/traccar/protocol/HuabaoProtocolDecoderTest.java +++ b/src/test/java/org/traccar/protocol/HuabaoProtocolDecoderTest.java @@ -11,6 +11,16 @@ public void testDecode() throws Exception { var decoder = inject(new HuabaoProtocolDecoder(null)); + verifyAttribute(decoder, binary( + "7e55018c418560090010174701022106242122348476113550490700001c06000000074e0000000000000308100000100102020200000a030000000b0803363839363037650c0600000001000afa7e"), + "unlockResult", 0x65); + + verifyPosition(decoder, binary( + "7e55018c378580120300032a06052117594022348474113550560705981e0400000002370ac30c0c28660308000000100101020200000a0301cc000c0600a2ffa7ff5e1b7e")); + + verifyPosition(decoder, binary( + "7e55028c37850011000200c008052106305122348621113550170700001e080000000aaa0000000000000300000000100100020200140a030000000c06003bffa8ffc3c77e")); + verifyAttribute(decoder, binary( "7E020000C2000000001862003F0000000400108042015A322206C869480017007B012E230918081550661D01CC1A0000099DC25727B0130000099DC26B27B00100000C40F89427B0711438393836303436393130323139303037383135336A0114503FF8AF05826BC8CA24E124F732B7C780C5484D6318C0A840412262D4BCEC26CA0234CCB85455D5114F12B650FA841FBEC0B03C67A21F501CAE6C384595028DA76B010060110065000012345678903930303137333833630B0012345678900E5C3080006804000003D46902016F6D7E"), Position.KEY_DRIVER_UNIQUE_ID, "\u00909001738"); From a59a6d19f575d8b593085ce19458c8fff18a6360 Mon Sep 17 00:00:00 2001 From: Anton Tananaev Date: Sat, 25 Nov 2023 14:19:23 -0800 Subject: [PATCH 05/11] Add device sharing (fix #3789, fix #4936, fix #5025) --- schema/changelog-5.11.xml | 17 ++++++ schema/changelog-master.xml | 1 + .../traccar/api/resource/DeviceResource.java | 37 +++++++++++ src/main/java/org/traccar/model/User.java | 10 +++ .../org/traccar/schedule/ScheduleManager.java | 1 + .../traccar/schedule/TaskDeleteTemporary.java | 61 +++++++++++++++++++ .../org/traccar/schedule/TaskReports.java | 2 +- 7 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 schema/changelog-5.11.xml create mode 100644 src/main/java/org/traccar/schedule/TaskDeleteTemporary.java diff --git a/schema/changelog-5.11.xml b/schema/changelog-5.11.xml new file mode 100644 index 00000000000..e59df924988 --- /dev/null +++ b/schema/changelog-5.11.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + diff --git a/schema/changelog-master.xml b/schema/changelog-master.xml index 183b3fd9323..559d90923d9 100644 --- a/schema/changelog-master.xml +++ b/schema/changelog-master.xml @@ -41,5 +41,6 @@ + diff --git a/src/main/java/org/traccar/api/resource/DeviceResource.java b/src/main/java/org/traccar/api/resource/DeviceResource.java index 61a70bac030..ebc40a9b10b 100644 --- a/src/main/java/org/traccar/api/resource/DeviceResource.java +++ b/src/main/java/org/traccar/api/resource/DeviceResource.java @@ -15,12 +15,15 @@ */ package org.traccar.api.resource; +import jakarta.ws.rs.FormParam; import org.traccar.api.BaseObjectResource; +import org.traccar.api.signature.TokenManager; import org.traccar.broadcast.BroadcastService; import org.traccar.database.MediaManager; import org.traccar.helper.LogAction; import org.traccar.model.Device; import org.traccar.model.DeviceAccumulators; +import org.traccar.model.Permission; import org.traccar.model.Position; import org.traccar.model.User; import org.traccar.session.ConnectionManager; @@ -46,7 +49,9 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.security.GeneralSecurityException; import java.util.Collection; +import java.util.Date; import java.util.LinkedList; import java.util.List; @@ -67,6 +72,9 @@ public class DeviceResource extends BaseObjectResource { @Inject private MediaManager mediaManager; + @Inject + private TokenManager tokenManager; + public DeviceResource() { super(Device.class); } @@ -183,4 +191,33 @@ public Response uploadImage( return Response.status(Response.Status.NOT_FOUND).build(); } + @Path("share") + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + @POST + public String shareDevice( + @FormParam("deviceId") long deviceId, + @FormParam("expiration") Date expiration) throws StorageException, GeneralSecurityException, IOException { + + User user = permissionsService.getUser(getUserId()); + + Device device = storage.getObject(Device.class, new Request( + new Columns.All(), + new Condition.And( + new Condition.Equals("id", deviceId), + new Condition.Permission(User.class, user.getId(), Device.class)))); + + User share = new User(); + share.setName(device.getName()); + share.setEmail(user.getEmail() + ":" + device.getUniqueId()); + share.setExpirationTime(expiration); + share.setTemporary(true); + share.setReadonly(true); + + share.setId(storage.addObject(share, new Request(new Columns.Exclude("id")))); + + storage.addPermission(new Permission(User.class, share.getId(), Device.class, deviceId)); + + return tokenManager.generateToken(share.getId(), expiration); + } + } diff --git a/src/main/java/org/traccar/model/User.java b/src/main/java/org/traccar/model/User.java index 757064ba221..8cfee0f48bb 100644 --- a/src/main/java/org/traccar/model/User.java +++ b/src/main/java/org/traccar/model/User.java @@ -261,6 +261,16 @@ public void setTotpKey(String totpKey) { this.totpKey = totpKey; } + private boolean temporary; + + public boolean getTemporary() { + return temporary; + } + + public void setTemporary(boolean temporary) { + this.temporary = temporary; + } + @QueryIgnore public String getPassword() { return null; diff --git a/src/main/java/org/traccar/schedule/ScheduleManager.java b/src/main/java/org/traccar/schedule/ScheduleManager.java index 07cdb1fe1a8..38e8f281c41 100644 --- a/src/main/java/org/traccar/schedule/ScheduleManager.java +++ b/src/main/java/org/traccar/schedule/ScheduleManager.java @@ -39,6 +39,7 @@ public ScheduleManager(Injector injector) { public void start() { executor = Executors.newSingleThreadScheduledExecutor(); var tasks = List.of( + TaskDeleteTemporary.class, TaskReports.class, TaskDeviceInactivityCheck.class, TaskWebSocketKeepalive.class, diff --git a/src/main/java/org/traccar/schedule/TaskDeleteTemporary.java b/src/main/java/org/traccar/schedule/TaskDeleteTemporary.java new file mode 100644 index 00000000000..0cead59fbae --- /dev/null +++ b/src/main/java/org/traccar/schedule/TaskDeleteTemporary.java @@ -0,0 +1,61 @@ +/* + * Copyright 2023 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.schedule; + +import jakarta.inject.Inject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.traccar.model.User; +import org.traccar.storage.Storage; +import org.traccar.storage.StorageException; +import org.traccar.storage.query.Condition; +import org.traccar.storage.query.Request; + +import java.util.Date; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class TaskDeleteTemporary implements ScheduleTask { + + private static final Logger LOGGER = LoggerFactory.getLogger(TaskDeleteTemporary.class); + + private static final long CHECK_PERIOD_HOURS = 1; + + private final Storage storage; + + @Inject + public TaskDeleteTemporary(Storage storage) { + this.storage = storage; + } + + @Override + public void schedule(ScheduledExecutorService executor) { + executor.scheduleAtFixedRate(this, CHECK_PERIOD_HOURS, CHECK_PERIOD_HOURS, TimeUnit.HOURS); + } + + @Override + public void run() { + try { + storage.removeObject(User.class, new Request( + new Condition.And( + new Condition.Equals("temporary", true), + new Condition.Compare("expirationTime", "<", "time", new Date())))); + } catch (StorageException e) { + LOGGER.warn("Failed to delete temporary users", e); + } + } + +} diff --git a/src/main/java/org/traccar/schedule/TaskReports.java b/src/main/java/org/traccar/schedule/TaskReports.java index 30f20f4373e..e0fa6f8d678 100644 --- a/src/main/java/org/traccar/schedule/TaskReports.java +++ b/src/main/java/org/traccar/schedule/TaskReports.java @@ -51,7 +51,7 @@ public class TaskReports implements ScheduleTask { private static final Logger LOGGER = LoggerFactory.getLogger(TaskReports.class); - private static final long CHECK_PERIOD_MINUTES = 1; + private static final long CHECK_PERIOD_MINUTES = 15; private final Storage storage; private final Injector injector; From 08cb38a2126e91ccde44e9f91d2ad1f149c7e6b5 Mon Sep 17 00:00:00 2001 From: Anton Tananaev Date: Sun, 26 Nov 2023 08:03:53 -0800 Subject: [PATCH 06/11] Add login result class --- .../traccar/api/resource/SessionResource.java | 6 ++--- .../org/traccar/api/security/LoginResult.java | 25 +++++++++++++++++++ .../traccar/api/security/LoginService.java | 18 ++++++------- .../api/security/SecurityRequestFilter.java | 7 +++--- .../org/traccar/database/OpenIdProvider.java | 3 ++- 5 files changed, 43 insertions(+), 16 deletions(-) create mode 100644 src/main/java/org/traccar/api/security/LoginResult.java diff --git a/src/main/java/org/traccar/api/resource/SessionResource.java b/src/main/java/org/traccar/api/resource/SessionResource.java index 90f0ceadedb..3e80e002093 100644 --- a/src/main/java/org/traccar/api/resource/SessionResource.java +++ b/src/main/java/org/traccar/api/resource/SessionResource.java @@ -82,7 +82,7 @@ public class SessionResource extends BaseResource { public User get(@QueryParam("token") String token) throws StorageException, IOException, GeneralSecurityException { if (token != null) { - User user = loginService.login(token); + User user = loginService.login(token).getUser(); if (user != null) { request.getSession().setAttribute(USER_ID_KEY, user.getId()); LogAction.login(user.getId(), WebHelper.retrieveRemoteAddress(request)); @@ -109,7 +109,7 @@ public User get(@QueryParam("token") String token) throws StorageException, IOEx } } if (email != null && password != null) { - User user = loginService.login(email, password, null); + User user = loginService.login(email, password, null).getUser(); if (user != null) { request.getSession().setAttribute(USER_ID_KEY, user.getId()); LogAction.login(user.getId(), WebHelper.retrieveRemoteAddress(request)); @@ -148,7 +148,7 @@ public User add( @FormParam("code") Integer code) throws StorageException { User user; try { - user = loginService.login(email, password, code); + user = loginService.login(email, password, code).getUser(); } catch (CodeRequiredException e) { Response response = Response .status(Response.Status.UNAUTHORIZED) diff --git a/src/main/java/org/traccar/api/security/LoginResult.java b/src/main/java/org/traccar/api/security/LoginResult.java new file mode 100644 index 00000000000..66c35bbedd3 --- /dev/null +++ b/src/main/java/org/traccar/api/security/LoginResult.java @@ -0,0 +1,25 @@ +package org.traccar.api.security; + +import org.traccar.model.User; + +import java.util.Date; + +public class LoginResult { + + private final User user; + private final Date expiration; + + public LoginResult(User user) { + this.user = user; + expiration = null; + } + + public User getUser() { + return user; + } + + public Date getExpiration() { + return expiration; + } + +} diff --git a/src/main/java/org/traccar/api/security/LoginService.java b/src/main/java/org/traccar/api/security/LoginService.java index 829f5d2fa95..6246d249435 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 Anton Tananaev (anton@traccar.org) + * Copyright 2022 - 2023 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. @@ -58,9 +58,9 @@ public LoginService( forceOpenId = config.getBoolean(Keys.OPENID_FORCE); } - public User login(String token) throws StorageException, GeneralSecurityException, IOException { + public LoginResult login(String token) throws StorageException, GeneralSecurityException, IOException { if (serviceAccountToken != null && serviceAccountToken.equals(token)) { - return new ServiceAccountUser(); + return new LoginResult(new ServiceAccountUser()); } long userId = tokenManager.verifyToken(token); User user = storage.getObject(User.class, new Request( @@ -68,10 +68,10 @@ public User login(String token) throws StorageException, GeneralSecurityExceptio if (user != null) { checkUserEnabled(user); } - return user; + return new LoginResult(user); } - public User login(String email, String password, Integer code) throws StorageException { + public LoginResult login(String email, String password, Integer code) throws StorageException { if (forceOpenId) { return null; } @@ -87,20 +87,20 @@ public User login(String email, String password, Integer code) throws StorageExc || !forceLdap && user.isPasswordValid(password)) { checkUserCode(user, code); checkUserEnabled(user); - return user; + return new LoginResult(user); } } else { if (ldapProvider != null && ldapProvider.login(email, password)) { user = ldapProvider.getUser(email); user.setId(storage.addObject(user, new Request(new Columns.Exclude("id")))); checkUserEnabled(user); - return user; + return new LoginResult(user); } } return null; } - public User login(String email, String name, boolean administrator) throws StorageException { + public LoginResult login(String email, String name, boolean administrator) throws StorageException { User user = storage.getObject(User.class, new Request( new Columns.All(), new Condition.Equals("email", email))); @@ -115,7 +115,7 @@ public User login(String email, String name, boolean administrator) throws Stora user.setId(storage.addObject(user, new Request(new Columns.Exclude("id")))); } checkUserEnabled(user); - return user; + return new LoginResult(user); } private void checkUserEnabled(User user) throws SecurityException { diff --git a/src/main/java/org/traccar/api/security/SecurityRequestFilter.java b/src/main/java/org/traccar/api/security/SecurityRequestFilter.java index cb523177e3e..e308024da10 100644 --- a/src/main/java/org/traccar/api/security/SecurityRequestFilter.java +++ b/src/main/java/org/traccar/api/security/SecurityRequestFilter.java @@ -82,13 +82,14 @@ public void filter(ContainerRequestContext requestContext) { if (authHeader != null) { try { - User user; + LoginResult loginResult; if (authHeader.startsWith("Bearer ")) { - user = loginService.login(authHeader.substring(7)); + loginResult = loginService.login(authHeader.substring(7)); } else { String[] auth = decodeBasicAuth(authHeader); - user = loginService.login(auth[0], auth[1], null); + loginResult = loginService.login(auth[0], auth[1], null); } + User user = loginResult.getUser(); if (user != null) { statisticsManager.registerRequest(user.getId()); securityContext = new UserSecurityContext(new UserPrincipal(user.getId())); diff --git a/src/main/java/org/traccar/database/OpenIdProvider.java b/src/main/java/org/traccar/database/OpenIdProvider.java index 1f5a2f48149..93297f7ab37 100644 --- a/src/main/java/org/traccar/database/OpenIdProvider.java +++ b/src/main/java/org/traccar/database/OpenIdProvider.java @@ -189,7 +189,8 @@ public URI handleCallback(URI requestUri, HttpServletRequest request) throw new GeneralSecurityException("Your OpenID Groups do not permit access to Traccar."); } - User user = loginService.login(userInfo.getEmailAddress(), userInfo.getName(), administrator); + User user = loginService.login( + userInfo.getEmailAddress(), userInfo.getName(), administrator).getUser(); request.getSession().setAttribute(SessionResource.USER_ID_KEY, user.getId()); LogAction.login(user.getId(), WebHelper.retrieveRemoteAddress(request)); From acdc451e3e2ab94f548f078c4bd49985b3c2f01d Mon Sep 17 00:00:00 2001 From: Anton Tananaev Date: Sun, 26 Nov 2023 08:07:52 -0800 Subject: [PATCH 07/11] Set expiration from token --- .../api/resource/PasswordResource.java | 2 +- .../org/traccar/api/security/LoginResult.java | 6 +++++- .../traccar/api/security/LoginService.java | 6 +++--- .../traccar/api/signature/TokenManager.java | 20 +++++++++++++------ 4 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/traccar/api/resource/PasswordResource.java b/src/main/java/org/traccar/api/resource/PasswordResource.java index 029e63a0c8e..22b3f0cd3ee 100644 --- a/src/main/java/org/traccar/api/resource/PasswordResource.java +++ b/src/main/java/org/traccar/api/resource/PasswordResource.java @@ -75,7 +75,7 @@ public Response update( @FormParam("token") String token, @FormParam("password") String password) throws StorageException, GeneralSecurityException, IOException { - long userId = tokenManager.verifyToken(token); + long userId = tokenManager.verifyToken(token).getUserId(); User user = storage.getObject(User.class, new Request( new Columns.All(), new Condition.Equals("id", userId))); if (user != null) { diff --git a/src/main/java/org/traccar/api/security/LoginResult.java b/src/main/java/org/traccar/api/security/LoginResult.java index 66c35bbedd3..1fccc36d1ee 100644 --- a/src/main/java/org/traccar/api/security/LoginResult.java +++ b/src/main/java/org/traccar/api/security/LoginResult.java @@ -10,8 +10,12 @@ public class LoginResult { private final Date expiration; public LoginResult(User user) { + this(user, null); + } + + public LoginResult(User user, Date expiration) { this.user = user; - expiration = null; + this.expiration = expiration; } public User getUser() { diff --git a/src/main/java/org/traccar/api/security/LoginService.java b/src/main/java/org/traccar/api/security/LoginService.java index 6246d249435..930c4fa46f3 100644 --- a/src/main/java/org/traccar/api/security/LoginService.java +++ b/src/main/java/org/traccar/api/security/LoginService.java @@ -62,13 +62,13 @@ public LoginResult login(String token) throws StorageException, GeneralSecurityE if (serviceAccountToken != null && serviceAccountToken.equals(token)) { return new LoginResult(new ServiceAccountUser()); } - long userId = tokenManager.verifyToken(token); + TokenManager.TokenData tokenData = tokenManager.verifyToken(token); User user = storage.getObject(User.class, new Request( - new Columns.All(), new Condition.Equals("id", userId))); + new Columns.All(), new Condition.Equals("id", tokenData.getUserId()))); if (user != null) { checkUserEnabled(user); } - return new LoginResult(user); + return new LoginResult(user, tokenData.getExpiration()); } public LoginResult login(String email, String password, Integer code) throws StorageException { diff --git a/src/main/java/org/traccar/api/signature/TokenManager.java b/src/main/java/org/traccar/api/signature/TokenManager.java index 3019e12b96e..824433b08c3 100644 --- a/src/main/java/org/traccar/api/signature/TokenManager.java +++ b/src/main/java/org/traccar/api/signature/TokenManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 Anton Tananaev (anton@traccar.org) + * Copyright 2022 - 2023 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. @@ -35,11 +35,19 @@ public class TokenManager { private final ObjectMapper objectMapper; private final CryptoManager cryptoManager; - public static class Data { + public static class TokenData { @JsonProperty("u") private long userId; @JsonProperty("e") private Date expiration; + + public long getUserId() { + return userId; + } + + public Date getExpiration() { + return expiration; + } } @Inject @@ -54,7 +62,7 @@ public String generateToken(long userId) throws IOException, GeneralSecurityExce public String generateToken( long userId, Date expiration) throws IOException, GeneralSecurityException, StorageException { - Data data = new Data(); + TokenData data = new TokenData(); data.userId = userId; if (expiration != null) { data.expiration = expiration; @@ -65,13 +73,13 @@ public String generateToken( return Base64.encodeBase64URLSafeString(cryptoManager.sign(encoded)); } - public long verifyToken(String token) throws IOException, GeneralSecurityException, StorageException { + public TokenData verifyToken(String token) throws IOException, GeneralSecurityException, StorageException { byte[] encoded = cryptoManager.verify(Base64.decodeBase64(token)); - Data data = objectMapper.readValue(encoded, Data.class); + TokenData data = objectMapper.readValue(encoded, TokenData.class); if (data.expiration.before(new Date())) { throw new SecurityException("Token has expired"); } - return data.userId; + return data; } } From fc8678b22929026e6c62284add8ff1cbca247f20 Mon Sep 17 00:00:00 2001 From: Anton Tananaev Date: Sun, 26 Nov 2023 08:18:32 -0800 Subject: [PATCH 08/11] Store session expiration --- .../org/traccar/api/resource/SessionResource.java | 6 +++++- .../traccar/api/security/SecurityRequestFilter.java | 7 +++++-- .../java/org/traccar/api/security/UserPrincipal.java | 11 +++++++++-- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/traccar/api/resource/SessionResource.java b/src/main/java/org/traccar/api/resource/SessionResource.java index 3e80e002093..0435f4f928b 100644 --- a/src/main/java/org/traccar/api/resource/SessionResource.java +++ b/src/main/java/org/traccar/api/resource/SessionResource.java @@ -17,6 +17,7 @@ import org.traccar.api.BaseResource; import org.traccar.api.security.CodeRequiredException; +import org.traccar.api.security.LoginResult; import org.traccar.api.security.LoginService; import org.traccar.api.signature.TokenManager; import org.traccar.database.OpenIdProvider; @@ -61,6 +62,7 @@ public class SessionResource extends BaseResource { public static final String USER_ID_KEY = "userId"; + public static final String EXPIRATION_KEY = "expiration"; public static final String USER_COOKIE_KEY = "user"; public static final String PASS_COOKIE_KEY = "password"; @@ -82,9 +84,11 @@ public class SessionResource extends BaseResource { public User get(@QueryParam("token") String token) throws StorageException, IOException, GeneralSecurityException { if (token != null) { - User user = loginService.login(token).getUser(); + LoginResult loginResult = loginService.login(token); + User user = loginResult.getUser(); if (user != null) { request.getSession().setAttribute(USER_ID_KEY, user.getId()); + request.getSession().setAttribute(EXPIRATION_KEY, loginResult.getExpiration()); LogAction.login(user.getId(), WebHelper.retrieveRemoteAddress(request)); return user; } diff --git a/src/main/java/org/traccar/api/security/SecurityRequestFilter.java b/src/main/java/org/traccar/api/security/SecurityRequestFilter.java index e308024da10..c33a800154f 100644 --- a/src/main/java/org/traccar/api/security/SecurityRequestFilter.java +++ b/src/main/java/org/traccar/api/security/SecurityRequestFilter.java @@ -38,6 +38,7 @@ import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; +import java.util.Date; public class SecurityRequestFilter implements ContainerRequestFilter { @@ -92,7 +93,8 @@ public void filter(ContainerRequestContext requestContext) { User user = loginResult.getUser(); if (user != null) { statisticsManager.registerRequest(user.getId()); - securityContext = new UserSecurityContext(new UserPrincipal(user.getId())); + securityContext = new UserSecurityContext( + new UserPrincipal(user.getId(), loginResult.getExpiration())); } } catch (StorageException | GeneralSecurityException | IOException e) { throw new WebApplicationException(e); @@ -101,12 +103,13 @@ public void filter(ContainerRequestContext requestContext) { } else if (request.getSession() != null) { Long userId = (Long) request.getSession().getAttribute(SessionResource.USER_ID_KEY); + Date expiration = (Date) request.getSession().getAttribute(SessionResource.EXPIRATION_KEY); if (userId != null) { User user = injector.getInstance(PermissionsService.class).getUser(userId); if (user != null) { user.checkDisabled(); statisticsManager.registerRequest(userId); - securityContext = new UserSecurityContext(new UserPrincipal(userId)); + securityContext = new UserSecurityContext(new UserPrincipal(userId, expiration)); } } diff --git a/src/main/java/org/traccar/api/security/UserPrincipal.java b/src/main/java/org/traccar/api/security/UserPrincipal.java index 18b84a0e194..83bd06fe99c 100644 --- a/src/main/java/org/traccar/api/security/UserPrincipal.java +++ b/src/main/java/org/traccar/api/security/UserPrincipal.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 - 2020 Anton Tananaev (anton@traccar.org) + * Copyright 2015 - 2023 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. @@ -16,19 +16,26 @@ package org.traccar.api.security; import java.security.Principal; +import java.util.Date; public class UserPrincipal implements Principal { private final long userId; + private final Date expiration; - public UserPrincipal(long userId) { + public UserPrincipal(long userId, Date expiration) { this.userId = userId; + this.expiration = expiration; } public Long getUserId() { return userId; } + public Date getExpiration() { + return expiration; + } + @Override public String getName() { return null; From b73c8246c2023feae9eb5332a69f0ab8a1cd4e3d Mon Sep 17 00:00:00 2001 From: Anton Tananaev Date: Sun, 26 Nov 2023 08:21:33 -0800 Subject: [PATCH 09/11] Limit token expiration extension --- src/main/java/org/traccar/api/resource/SessionResource.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/org/traccar/api/resource/SessionResource.java b/src/main/java/org/traccar/api/resource/SessionResource.java index 0435f4f928b..02c9837f0a1 100644 --- a/src/main/java/org/traccar/api/resource/SessionResource.java +++ b/src/main/java/org/traccar/api/resource/SessionResource.java @@ -181,6 +181,10 @@ public Response remove() { @POST public String requestToken( @FormParam("expiration") Date expiration) throws StorageException, GeneralSecurityException, IOException { + Date currentExpiration = (Date) request.getSession().getAttribute(EXPIRATION_KEY); + if (currentExpiration != null && currentExpiration.before(expiration)) { + expiration = currentExpiration; + } return tokenManager.generateToken(getUserId(), expiration); } From a943126d3cdb4d2e8c4c314d487736267daf171e Mon Sep 17 00:00:00 2001 From: Anton Tananaev Date: Sun, 26 Nov 2023 08:38:55 -0800 Subject: [PATCH 10/11] Remove cookie password login --- .../traccar/api/resource/SessionResource.java | 36 +------------------ 1 file changed, 1 insertion(+), 35 deletions(-) diff --git a/src/main/java/org/traccar/api/resource/SessionResource.java b/src/main/java/org/traccar/api/resource/SessionResource.java index 02c9837f0a1..dc517277eb9 100644 --- a/src/main/java/org/traccar/api/resource/SessionResource.java +++ b/src/main/java/org/traccar/api/resource/SessionResource.java @@ -21,7 +21,6 @@ import org.traccar.api.security.LoginService; import org.traccar.api.signature.TokenManager; import org.traccar.database.OpenIdProvider; -import org.traccar.helper.DataConverter; import org.traccar.helper.LogAction; import org.traccar.helper.WebHelper; import org.traccar.model.User; @@ -34,7 +33,6 @@ import jakarta.annotation.Nullable; import jakarta.annotation.security.PermitAll; import jakarta.inject.Inject; -import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; @@ -50,8 +48,6 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import java.io.IOException; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.util.Date; import java.net.URI; @@ -63,8 +59,6 @@ public class SessionResource extends BaseResource { public static final String USER_ID_KEY = "userId"; public static final String EXPIRATION_KEY = "expiration"; - public static final String USER_COOKIE_KEY = "user"; - public static final String PASS_COOKIE_KEY = "password"; @Inject private LoginService loginService; @@ -95,39 +89,11 @@ public User get(@QueryParam("token") String token) throws StorageException, IOEx } Long userId = (Long) request.getSession().getAttribute(USER_ID_KEY); - if (userId == null) { - - Cookie[] cookies = request.getCookies(); - String email = null, password = null; - if (cookies != null) { - for (Cookie cookie : cookies) { - if (cookie.getName().equals(USER_COOKIE_KEY)) { - byte[] emailBytes = DataConverter.parseBase64( - URLDecoder.decode(cookie.getValue(), StandardCharsets.US_ASCII)); - email = new String(emailBytes, StandardCharsets.UTF_8); - } else if (cookie.getName().equals(PASS_COOKIE_KEY)) { - byte[] passwordBytes = DataConverter.parseBase64( - URLDecoder.decode(cookie.getValue(), StandardCharsets.US_ASCII)); - password = new String(passwordBytes, StandardCharsets.UTF_8); - } - } - } - if (email != null && password != null) { - User user = loginService.login(email, password, null).getUser(); - if (user != null) { - request.getSession().setAttribute(USER_ID_KEY, user.getId()); - LogAction.login(user.getId(), WebHelper.retrieveRemoteAddress(request)); - return user; - } - } - - } else { - + if (userId != null) { User user = permissionsService.getUser(userId); if (user != null) { return user; } - } throw new WebApplicationException(Response.status(Response.Status.NOT_FOUND).build()); From e0d67dd1771e7b265e98bb18ab5359fc335ca9c4 Mon Sep 17 00:00:00 2001 From: Anton Tananaev Date: Tue, 28 Nov 2023 07:10:00 -0800 Subject: [PATCH 11/11] Support Glonass and GNSS NMEA --- .../java/org/traccar/protocol/T55ProtocolDecoder.java | 10 ++++++---- .../org/traccar/protocol/T55ProtocolDecoderTest.java | 9 +++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/traccar/protocol/T55ProtocolDecoder.java b/src/main/java/org/traccar/protocol/T55ProtocolDecoder.java index b18359b3f2f..9e7518ce5a7 100644 --- a/src/main/java/org/traccar/protocol/T55ProtocolDecoder.java +++ b/src/main/java/org/traccar/protocol/T55ProtocolDecoder.java @@ -41,7 +41,8 @@ public T55ProtocolDecoder(Protocol protocol) { } private static final Pattern PATTERN_GPRMC = new PatternBuilder() - .text("$GPRMC,") + .text("$") + .expression("G[PLN]RMC,") .number("(dd)(dd)(dd).?d*,") // time (hhmmss) .expression("([AV]),") // validity .number("(dd)(dd.d+),") // latitude @@ -64,7 +65,8 @@ public T55ProtocolDecoder(Protocol protocol) { .compile(); private static final Pattern PATTERN_GPGGA = new PatternBuilder() - .text("$GPGGA,") + .text("$") + .expression("G[PLN]GGA,") .number("(dd)(dd)(dd).?d*,") // time (hhmmss) .number("(d+)(dd.d+),") // latitude .expression("([NS]),") @@ -444,9 +446,9 @@ protected Object decode( } } else if (sentence.matches("^[0-9A-F]+$")) { getDeviceSession(channel, remoteAddress, sentence); - } else if (sentence.startsWith("$GPRMC")) { + } else if (sentence.startsWith("RMC", 3)) { return decodeGprmc(deviceSession, sentence, remoteAddress, channel); - } else if (sentence.startsWith("$GPGGA") && deviceSession != null) { + } else if (sentence.startsWith("GGA", 3) && deviceSession != null) { return decodeGpgga(deviceSession, sentence); } else if (sentence.startsWith("$GPRMA") && deviceSession != null) { return decodeGprma(deviceSession, sentence); diff --git a/src/test/java/org/traccar/protocol/T55ProtocolDecoderTest.java b/src/test/java/org/traccar/protocol/T55ProtocolDecoderTest.java index 7b9841d6819..91020714cfd 100644 --- a/src/test/java/org/traccar/protocol/T55ProtocolDecoderTest.java +++ b/src/test/java/org/traccar/protocol/T55ProtocolDecoderTest.java @@ -17,6 +17,15 @@ public void testDecode() throws Exception { verifyPosition(decoder, text( "$PUBX,00,130209.00,3650.51159,N,01346.10602,E,785.947,D3,4.1,5.2,0.163,87.43,-0.054,7.0,0.88,1.21,0.88,24,01012,0*6D")); + verifyPosition(decoder, text( + "$GNRMC,164414.90,A,4650.5156500,N,01246.1059604,E,0.018,,091123,,,A,V*15")); + + verifyPosition(decoder, text( + "$GNGGA,164414.90,4650.5156500,N,01246.1059604,E,1,12,0.84,740.729,M,44.804,M,,*4E")); + + verifyNull(decoder, text( + "$GNGLL,4650.5156500,N,01246.1059604,E,164414.90,A,A*77")); + verifyPosition(decoder, text( "QZE,868994033976700,35,28062020,113553,22.13673,114.57263,0,22,A,0"));