use of me.retrodaredevil.solarthing.type.alter.packets.ScheduledCommandData in project solarthing by wildmountainfarms.
the class AlterManagerAction method onUpdate.
@Override
protected void onUpdate() {
super.onUpdate();
// Supply queried solarthing_open packets to the security packet receiver, which may execute code to put commands requested to be scheduled in solarthing_alter
List<StoredPacketGroup> packets = openDatabaseCache.getAllCachedPackets();
securityPacketReceiver.receivePacketGroups(packets);
// Check packets in solarthing_alter and see if we need to send a command because of a scheduled command packet
Instant now = Instant.now();
List<VersionedPacket<StoredAlterPacket>> alterPackets = alterPacketsProvider.getPackets();
if (alterPackets == null) {
LOGGER.info("alterPackets is null. Maybe query failed? Maybe additional info in previous logs?");
} else {
for (VersionedPacket<StoredAlterPacket> versionedPacket : alterPackets) {
AlterPacket packet = versionedPacket.getPacket().getPacket();
if (packet instanceof ScheduledCommandPacket) {
ScheduledCommandPacket scheduledCommandPacket = (ScheduledCommandPacket) packet;
ScheduledCommandData data = scheduledCommandPacket.getData();
if (data.getScheduledTimeMillis() <= now.toEpochMilli()) {
if (now.toEpochMilli() - data.getScheduledTimeMillis() > Duration.ofMinutes(5).toMillis()) {
LOGGER.warn("Not going to send a command scheduled for more than 5 minutes ago! data: " + data);
} else {
doSendCommand(versionedPacket, scheduledCommandPacket);
}
}
} else if (packet instanceof FlagPacket) {
FlagPacket flagPacket = (FlagPacket) packet;
FlagData data = flagPacket.getFlagData();
ActivePeriod activePeriod = data.getActivePeriod();
if (activePeriod instanceof TimeRangeActivePeriod) {
// We only try to "manage" flags that use this type of ActivePeriod
TimeRangeActivePeriod period = (TimeRangeActivePeriod) activePeriod;
TimeRange timeRange = period.getTimeRange();
Instant endTime = timeRange.getEndTime();
if (endTime != null && endTime.compareTo(now) < 0) {
// If there is an end time, and it is in the past, then we should remove the flag
executorService.execute(() -> {
try {
database.getAlterDatabase().delete(versionedPacket);
} catch (SolarThingDatabaseException e) {
LOGGER.error("Could not delete a FlagPacket with an expired time", e);
// If we cannot delete it, no need to try again, it'll still be here next time around
}
});
}
}
}
}
}
}
use of me.retrodaredevil.solarthing.type.alter.packets.ScheduledCommandData in project solarthing by wildmountainfarms.
the class ScheduleCommandChatBotHandler method schedule.
private void schedule(MessageSender messageSender, Instant now, Instant targetTime, AvailableCommand availableCommand) {
if (now.plus(Duration.ofMinutes(1)).isAfter(targetTime)) {
messageSender.sendMessage("Cannot schedule a command less than one minute from now.");
return;
}
if (now.plus(Duration.ofHours(72)).isBefore(targetTime)) {
messageSender.sendMessage("Cannot schedule a command more than 72 hours from now.");
return;
}
UUID uniqueId = UUID.randomUUID();
PacketCollectionCreator creator = commandHelper.getCommandManager().makeCreator(sourceId, zoneId, // We don't have an InstanceTargetPacket because scheduling commands is not handled by a program with a fragment ID // also look at PacketGroups.parseToTargetPacketGroup() for interpretation without a TargetInstancePacket
null, new ImmutableScheduleCommandPacket(new ScheduledCommandData(targetTime.toEpochMilli(), availableCommand.getCommandInfo().getName(), Collections.singleton(availableCommand.getFragmentId())), uniqueId), PacketCollectionIdGenerator.Defaults.UNIQUE_GENERATOR);
PacketCollection packetCollection = creator.create(now);
messageSender.sendMessage("Scheduling " + availableCommand.getCommandInfo().getDisplayName() + " at " + TimeUtil.instantToSlackDateSeconds(targetTime));
executorService.execute(() -> {
boolean success = false;
try {
database.getOpenDatabase().uploadPacketCollection(packetCollection, null);
success = true;
} catch (SolarThingDatabaseException e) {
LOGGER.error("Could not upload schedule command packet collection", e);
}
if (success) {
messageSender.sendMessage("Successfully requested schedule. ID: " + uniqueId);
} else {
messageSender.sendMessage("Could not upload schedule command request.");
}
});
}
use of me.retrodaredevil.solarthing.type.alter.packets.ScheduledCommandData in project solarthing by wildmountainfarms.
the class CommandOpenPacketsTest method test.
@Test
void test() throws JsonProcessingException {
PacketTestUtil.testJson(new ImmutableRequestCommandPacket("GEN OFF"), CommandOpenPacket.class, true);
PacketTestUtil.testJson(new ImmutableRequestFlagPacket(new FlagData("disable_automations", new TimeRangeActivePeriod(TimeRange.create(Instant.parse("2021-10-05T04:53:22.877307Z"), Instant.parse("2021-10-05T04:53:44.305146Z"))))), CommandOpenPacket.class, true);
PacketTestUtil.testJson(new ImmutableScheduleCommandPacket(new ScheduledCommandData(System.currentTimeMillis(), "GEN OFF", Collections.singleton(1)), UUID.randomUUID()), CommandOpenPacket.class, true);
PacketTestUtil.testJson(new ImmutableDeleteAlterPacket("my_document_id", new RevisionUpdateToken("46-9ab7d71841380a36e1ec9e367deae36e")), CommandOpenPacket.class, true);
PacketTestUtil.testJson(new ImmutableRequestHeartbeatPacket(new HeartbeatData("Hourly Mate Ping", "heartbeat.ping.mate.hourly", Duration.ofHours(1), Duration.ofMinutes(5)), UUID.randomUUID()), CommandOpenPacket.class, true);
}
use of me.retrodaredevil.solarthing.type.alter.packets.ScheduledCommandData in project solarthing by wildmountainfarms.
the class AlterManagerAction method isDocumentMadeByUs.
private boolean isDocumentMadeByUs(Instant now, ScheduledCommandData scheduledCommandData, StoredPacketGroup existingDocument) {
LargeIntegrityPacket largeIntegrityPacket = (LargeIntegrityPacket) existingDocument.getPackets().stream().filter(p -> p instanceof LargeIntegrityPacket).findAny().orElse(null);
if (largeIntegrityPacket == null) {
LOGGER.warn(SolarThingConstants.SUMMARY_MARKER, "The stored document did not have a LargeIntegrity packet. Someone must be trying to stop a scheduled command!");
return false;
}
String sender = largeIntegrityPacket.getSender();
if (!commandManager.getSender().equals(sender)) {
LOGGER.info(SolarThingConstants.SUMMARY_MARKER, "The sender of the large integrity packet we are inspecting is not us (" + commandManager.getSender() + "). It is " + sender + ". Might be a malicious actor, might be a bad setup.");
return false;
}
String encryptedHash = largeIntegrityPacket.getEncryptedHash();
String data;
try {
synchronized (CIPHER) {
data = Decrypt.decrypt(CIPHER, commandManager.getKeyPair().getPublic(), encryptedHash);
}
} catch (InvalidKeyException e) {
throw new RuntimeException("Should be a valid key!", e);
} catch (DecryptException e) {
LOGGER.warn(SolarThingConstants.SUMMARY_MARKER, "The document we are inspecting had a large integrity packet with the sender: " + sender + ", but that's us and we couldn't decrypt their payload. Likely a malicious actor", e);
return false;
}
final String[] split = data.split(",", 2);
LOGGER.debug("decrypted data: " + data);
if (split.length != 2) {
LOGGER.warn(SolarThingConstants.SUMMARY_MARKER, "split.length: " + split.length + " split: " + Arrays.asList(split));
return false;
}
String hexMillis = split[0];
// String message = split[1]; We don't care what the message is. We don't even care enough to check if it matches the payload's hash
long dateMillis;
try {
dateMillis = Long.parseLong(hexMillis, 16);
} catch (NumberFormatException e) {
LOGGER.error(SolarThingConstants.SUMMARY_MARKER, "Error parsing hex date millis", e);
return false;
}
if (dateMillis < scheduledCommandData.getScheduledTimeMillis()) {
LOGGER.warn(SolarThingConstants.SUMMARY_MARKER, "The dateMillis for this is less than the command's scheduled execution time! This must be a malicious actor!");
return false;
}
if (dateMillis > now.toEpochMilli()) {
LOGGER.warn(SolarThingConstants.SUMMARY_MARKER, "The dateMillis for this is greater than now! This should never ever happen.");
return false;
}
return true;
}
use of me.retrodaredevil.solarthing.type.alter.packets.ScheduledCommandData in project solarthing by wildmountainfarms.
the class AlterManagerAction method doSendCommand.
private void doSendCommand(VersionedPacket<StoredAlterPacket> versionedPacket, ScheduledCommandPacket scheduledCommandPacket) {
LOGGER.info(SolarThingConstants.SUMMARY_MARKER, "Sending command from data: " + scheduledCommandPacket.getData());
ScheduledCommandData data = scheduledCommandPacket.getData();
RequestCommandPacket requestCommandPacket = new ImmutableRequestCommandPacket(data.getCommandName());
// Having a document ID based off of the StoredAlterPacket's _id helps make sure we don't process it twice in case we are unable to delete it.
// -- if there's an update conflict while uploading, we know we already processed it
String documentId = "scheduled-request-" + versionedPacket.getPacket().getDbId();
PacketCollectionCreator creator = commandManager.makeCreator(sourceId, zoneId, InstanceTargetPackets.create(data.getTargetFragmentIds()), requestCommandPacket, zonedDateTime -> documentId);
executorService.execute(() -> {
Instant uploadingNow = Instant.now();
PacketCollection packetCollection = creator.create(uploadingNow);
boolean shouldDeleteAlter = false;
try {
database.getOpenDatabase().uploadPacketCollection(packetCollection, null);
LOGGER.info(SolarThingConstants.SUMMARY_MARKER, "Successfully uploaded packet collection that schedules command from data: " + scheduledCommandPacket.getData() + " document ID: " + documentId);
shouldDeleteAlter = true;
} catch (UpdateConflictSolarThingDatabaseException e) {
LOGGER.error("Got update conflict exception while uploading document ID: " + documentId + ". Will inspect existing document and overwrite if it's a malicious actor...", e);
VersionedPacket<StoredPacketGroup> existingDocument = null;
try {
existingDocument = database.getOpenDatabase().getPacketCollection(documentId);
} catch (SolarThingDatabaseException ex) {
LOGGER.error("Could not retrieve document with document ID: " + documentId, ex);
}
if (existingDocument != null) {
if (isDocumentMadeByUs(uploadingNow, data, existingDocument.getPacket())) {
LOGGER.info(SolarThingConstants.SUMMARY_MARKER, "False alarm everyone. The packet in the database was made by us and its timestamp is reasonable. document ID: " + documentId);
shouldDeleteAlter = true;
} else {
LOGGER.warn(SolarThingConstants.SUMMARY_MARKER, "The packet in the database with document ID: " + documentId + " was not made by us. Could be a malicious actor. We will overwrite that packet.");
try {
database.getOpenDatabase().uploadPacketCollection(packetCollection, existingDocument.getUpdateToken());
shouldDeleteAlter = true;
} catch (SolarThingDatabaseException ex) {
LOGGER.error("Could not overwrite malicious packet. Will likely try again. document ID: " + documentId, ex);
}
}
}
} catch (SolarThingDatabaseException e) {
LOGGER.error("Failed to upload our request command packet. documentId: " + documentId, e);
}
if (shouldDeleteAlter) {
try {
database.getAlterDatabase().delete(versionedPacket);
} catch (SolarThingDatabaseException e) {
LOGGER.error("Error while deleting an alter document. document ID: " + versionedPacket.getPacket().getDbId() + " update token: " + versionedPacket.getUpdateToken(), e);
}
}
});
}
Aggregations