[cometvisu] Security fixes & cleanup for cometvisu backend (#2671)
add required authentication for some rest endpoints, add some sanity checks to improve security. Remove code that has been marked as deprecated. --------- Signed-off-by: Tobias Bräutigam <tbraeutigam@gmail.com>4.2.x
parent
091d0edc06
commit
630e852583
|
@ -109,16 +109,18 @@ public class ManagerSettings implements IConfigChangeListener {
|
|||
for (final String target : Config.mountPoints.keySet()) {
|
||||
if (!target.contains("..") && !"demo".equalsIgnoreCase(target)) {
|
||||
String value = (String) Config.mountPoints.get(target);
|
||||
String[] parts = value.split(":");
|
||||
String source = parts[0];
|
||||
if (!source.contains("..") || (allowLookup && lookupMount.matcher(source).find())) {
|
||||
boolean writeable = parts.length > 1 && parts[1].contains("w");
|
||||
boolean showSubDirs = parts.length > 1 && parts[1].contains("s");
|
||||
if (source.startsWith(File.separator)) {
|
||||
source = source.substring(1);
|
||||
if (value != null) {
|
||||
String[] parts = value.split(":");
|
||||
String source = parts[0];
|
||||
if (!source.contains("..") || (allowLookup && lookupMount.matcher(source).find())) {
|
||||
boolean writeable = parts.length > 1 && parts[1].contains("w");
|
||||
boolean showSubDirs = parts.length > 1 && parts[1].contains("s");
|
||||
if (source.startsWith(File.separator)) {
|
||||
source = source.substring(1);
|
||||
}
|
||||
MountPoint mount = new MountPoint(Paths.get(target), Paths.get(source), showSubDirs, writeable);
|
||||
mounts.add(mount);
|
||||
}
|
||||
MountPoint mount = new MountPoint(Paths.get(target), Paths.get(source), showSubDirs, writeable);
|
||||
mounts.add(mount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,89 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.ui.cometvisu.internal;
|
||||
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.ext.MessageBodyWriter;
|
||||
import javax.ws.rs.ext.Provider;
|
||||
|
||||
import org.openhab.ui.cometvisu.internal.backend.model.StateBean;
|
||||
|
||||
/**
|
||||
* {@link StateBeanMessageBodyWriter} is used to serialize state update messages
|
||||
* for the CometVisu client
|
||||
*
|
||||
* @author Tobias Bräutigam - Initial contribution
|
||||
*/
|
||||
@Provider
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public class StateBeanMessageBodyWriter implements MessageBodyWriter<Object> {
|
||||
|
||||
@Override
|
||||
public long getSize(Object arg0, Class<?> arg1, Type arg2, Annotation[] arg3, MediaType arg4) {
|
||||
// deprecated by JAX-RS 2.0 and ignored by Jersey runtime
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWriteable(Class<?> type, Type genericType, Annotation[] arg2, MediaType arg3) {
|
||||
return (type == StateBean.class || genericType == StateBean.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTo(Object stateBean, Class<?> type, Type genericType, Annotation[] annotations,
|
||||
MediaType mediaType, MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream)
|
||||
throws IOException, WebApplicationException {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(serialize(stateBean));
|
||||
try (DataOutputStream dos = new DataOutputStream(entityStream)) {
|
||||
dos.writeUTF(sb.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param bean
|
||||
* - StateBean or List<StateBean>
|
||||
* @return String
|
||||
* - CV-Protocol state update json format {d:{item:state,...}}
|
||||
*/
|
||||
public String serialize(Object bean) {
|
||||
String msg = "{\"d\":{";
|
||||
if (bean instanceof StateBean stateBean) {
|
||||
msg += "\"" + stateBean.name + "\":\"" + stateBean.state + "\"";
|
||||
} else if (bean instanceof List<?>) {
|
||||
List<String> states = new ArrayList<>();
|
||||
for (Object bo : (List<?>) bean) {
|
||||
if (bo instanceof StateBean stateBean) {
|
||||
states.add("\"" + stateBean.name + "\":\"" + stateBean.state + "\"");
|
||||
}
|
||||
}
|
||||
if (!states.isEmpty()) {
|
||||
msg += String.join(",", states);
|
||||
}
|
||||
}
|
||||
msg += "}}";
|
||||
return msg;
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.ui.cometvisu.internal.backend.model;
|
||||
|
||||
/**
|
||||
* This is a java bean that is used with JAXB to define the backend configurationfor the
|
||||
* Cometvisu client.
|
||||
*
|
||||
* @author Tobias Bräutigam - Initial contribution
|
||||
*
|
||||
*/
|
||||
public class ConfigBean {
|
||||
public String name = "openhab2";
|
||||
public String transport = "sse";
|
||||
public String baseURL = "/rest/cv/";
|
||||
public ResourcesBean resources;
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.ui.cometvisu.internal.backend.model;
|
||||
|
||||
/**
|
||||
* This is a java bean that is used with JAXB to define the login entry
|
||||
* page of the Cometvisu interface.
|
||||
*
|
||||
* @author Tobias Bräutigam - Initial contribution
|
||||
*
|
||||
*/
|
||||
public class LoginBean {
|
||||
public String v;
|
||||
public String s;
|
||||
public ConfigBean c;
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.ui.cometvisu.internal.backend.model;
|
||||
|
||||
/**
|
||||
* This is a java bean that is used with JAXB to define the resources of backend configuration for the
|
||||
* Cometvisu client.
|
||||
*
|
||||
* @author Tobias Bräutigam - Initial contribution
|
||||
*
|
||||
*/
|
||||
public class ResourcesBean {
|
||||
public String read;
|
||||
public String rrd;
|
||||
public String write;
|
||||
public String rest;
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.ui.cometvisu.internal.backend.model;
|
||||
|
||||
/**
|
||||
* Item bean for broadcasted item states.
|
||||
*
|
||||
* @author Tobias Bräutigam - Initial Contribution and API
|
||||
*/
|
||||
public class StateBean {
|
||||
|
||||
public String name;
|
||||
|
||||
public String state;
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.ui.cometvisu.internal.backend.model;
|
||||
|
||||
/**
|
||||
* This is a java bean that is used with JAXB to define the login entry
|
||||
* page of the Cometvisu interface.
|
||||
*
|
||||
* @author Tobias Bräutigam - Initial contribution
|
||||
*
|
||||
*/
|
||||
public class SuccessBean {
|
||||
public int success;
|
||||
}
|
|
@ -12,6 +12,7 @@
|
|||
*/
|
||||
package org.openhab.ui.cometvisu.internal.backend.model.rest;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.OpenHAB;
|
||||
|
||||
/**
|
||||
|
@ -21,16 +22,18 @@ import org.openhab.core.OpenHAB;
|
|||
* @author Tobias Bräutigam - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class RestBackendEnvironmentState {
|
||||
// as we are just simulating we use a fixed version here to tell that we are compatible
|
||||
public int PHP_VERSION_ID = 80100;
|
||||
public String phpversion = "8.1.0";
|
||||
|
||||
public String SERVER_SIGNATURE;
|
||||
public String SERVER_SOFTWARE;
|
||||
public String SERVER_SIGNATURE = "";
|
||||
public String SERVER_SOFTWARE = "";
|
||||
public String required_php_version = ">=7.4";
|
||||
|
||||
// openHAB specific values
|
||||
public boolean isOpenHab = true;
|
||||
public boolean requiresAuth = true;
|
||||
public String server_release = "openHAB " + OpenHAB.getVersion();
|
||||
}
|
||||
|
|
|
@ -1,330 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.ui.cometvisu.internal.backend.rest;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.NumberFormat;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.TimeZone;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.HttpHeaders;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.OpenHAB;
|
||||
import org.openhab.core.io.rest.RESTConstants;
|
||||
import org.openhab.core.io.rest.RESTResource;
|
||||
import org.openhab.core.items.GroupItem;
|
||||
import org.openhab.core.items.Item;
|
||||
import org.openhab.core.items.ItemNotFoundException;
|
||||
import org.openhab.core.items.ItemRegistry;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.persistence.FilterCriteria;
|
||||
import org.openhab.core.persistence.FilterCriteria.Ordering;
|
||||
import org.openhab.core.persistence.HistoricItem;
|
||||
import org.openhab.core.persistence.PersistenceService;
|
||||
import org.openhab.core.persistence.QueryablePersistenceService;
|
||||
import org.openhab.ui.cometvisu.internal.Config;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.osgi.service.component.annotations.ReferenceCardinality;
|
||||
import org.osgi.service.component.annotations.ReferencePolicy;
|
||||
import org.osgi.service.jaxrs.whiteboard.JaxrsWhiteboardConstants;
|
||||
import org.osgi.service.jaxrs.whiteboard.propertytypes.JSONRequired;
|
||||
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsApplicationSelect;
|
||||
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsName;
|
||||
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
|
||||
import org.rrd4j.ConsolFun;
|
||||
import org.rrd4j.core.FetchData;
|
||||
import org.rrd4j.core.FetchRequest;
|
||||
import org.rrd4j.core.RrdDb;
|
||||
import org.rrd4j.core.Util;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* handles requests for chart series data from the CometVisu client
|
||||
* used by the diagram plugin
|
||||
*
|
||||
* @author Tobias Bräutigam - Initial contribution
|
||||
* @author Wouter Born - Migrated to JAX-RS Whiteboard Specification
|
||||
* @author Wouter Born - Migrated to OpenAPI annotations
|
||||
*
|
||||
* @deprecated CometVisu (>=0.12) is using openHAB's native REST API, a special backend implementation is obsolete now
|
||||
*/
|
||||
@Component
|
||||
@JaxrsResource
|
||||
@JaxrsName(Config.COMETVISU_BACKEND_ALIAS + "/" + Config.COMETVISU_BACKEND_CHART_ALIAS)
|
||||
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
|
||||
@JSONRequired
|
||||
@Path(Config.COMETVISU_BACKEND_ALIAS + "/" + Config.COMETVISU_BACKEND_CHART_ALIAS)
|
||||
@Tag(name = Config.COMETVISU_BACKEND_ALIAS + "/" + Config.COMETVISU_BACKEND_CHART_ALIAS)
|
||||
@NonNullByDefault
|
||||
@Deprecated(since = "3.4", forRemoval = true)
|
||||
public class ChartResource implements RESTResource {
|
||||
private final Logger logger = LoggerFactory.getLogger(ChartResource.class);
|
||||
|
||||
// pattern RRDTool uses to format doubles in XML files
|
||||
private static final String PATTERN = "0.0000000000E00";
|
||||
|
||||
private static final DecimalFormat DECIMAL_FORMAT;
|
||||
|
||||
protected static final String RRD_FOLDER = OpenHAB.getUserDataFolder() + File.separator + "persistence"
|
||||
+ File.separator + "rrd4j";
|
||||
|
||||
static {
|
||||
DECIMAL_FORMAT = (DecimalFormat) NumberFormat.getNumberInstance(Locale.ENGLISH);
|
||||
synchronized (DECIMAL_FORMAT) {
|
||||
DECIMAL_FORMAT.applyPattern(PATTERN);
|
||||
}
|
||||
}
|
||||
|
||||
private final Map<String, QueryablePersistenceService> persistenceServices = new HashMap<>();
|
||||
|
||||
private final ItemRegistry itemRegistry;
|
||||
|
||||
@Activate
|
||||
public ChartResource(final @Reference ItemRegistry itemRegistry) {
|
||||
this.itemRegistry = itemRegistry;
|
||||
}
|
||||
|
||||
private @Context @NonNullByDefault({}) UriInfo uriInfo;
|
||||
|
||||
@Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
|
||||
public void addPersistenceService(PersistenceService service) {
|
||||
if (service instanceof QueryablePersistenceService) {
|
||||
persistenceServices.put(service.getId(), (QueryablePersistenceService) service);
|
||||
}
|
||||
}
|
||||
|
||||
public void removePersistenceService(PersistenceService service) {
|
||||
persistenceServices.remove(service.getId());
|
||||
}
|
||||
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(summary = "returns chart data from persistence service for an item", responses = {
|
||||
@ApiResponse(responseCode = "200", description = "OK"),
|
||||
@ApiResponse(responseCode = "500", description = "Server error") })
|
||||
public Response getChartSeries(@Context HttpHeaders headers, @QueryParam("rrd") String itemName,
|
||||
@QueryParam("ds") String consFunction, @QueryParam("start") String start, @QueryParam("end") String end,
|
||||
@QueryParam("res") long resolution) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Received GET request at '{}' for rrd '{}'.", uriInfo.getPath(), itemName);
|
||||
}
|
||||
String responseType = MediaType.APPLICATION_JSON;
|
||||
|
||||
// RRD specific: no equivalent in PersistenceService known
|
||||
ConsolFun consilidationFunction = ConsolFun.valueOf(consFunction);
|
||||
|
||||
// read the start/end time as they are provided in the RRD-way, we use
|
||||
// the RRD4j to read them
|
||||
long[] times = Util.getTimestamps(start, end);
|
||||
Date startTime = new Date();
|
||||
startTime.setTime(times[0] * 1000L);
|
||||
Date endTime = new Date();
|
||||
endTime.setTime(times[1] * 1000L);
|
||||
|
||||
if (itemName.endsWith(".rrd")) {
|
||||
itemName = itemName.substring(0, itemName.length() - 4);
|
||||
}
|
||||
String[] parts = itemName.split(":");
|
||||
String service = "rrd4j";
|
||||
|
||||
if (parts.length == 2) {
|
||||
itemName = parts[1];
|
||||
service = parts[0];
|
||||
}
|
||||
|
||||
Item item;
|
||||
try {
|
||||
item = itemRegistry.getItem(itemName);
|
||||
logger.debug("item '{}' found ", item);
|
||||
|
||||
// Prefer RRD-Service
|
||||
QueryablePersistenceService persistenceService = persistenceServices.get(service);
|
||||
// Fallback to first persistenceService from list
|
||||
if (persistenceService == null) {
|
||||
Iterator<Entry<String, QueryablePersistenceService>> pit = persistenceServices.entrySet().iterator();
|
||||
if (pit.hasNext()) {
|
||||
persistenceService = pit.next().getValue();
|
||||
logger.debug("required persistence service ({}) not found, using {} instead", service,
|
||||
persistenceService.getId());
|
||||
} else {
|
||||
throw new IllegalArgumentException("No Persistence service found.");
|
||||
}
|
||||
} else {
|
||||
logger.debug("using {} persistence for item {}", persistenceService.getId(), itemName);
|
||||
}
|
||||
Object data = null;
|
||||
if (persistenceService.getId().equals("rrd4j")) {
|
||||
data = getRrdSeries(persistenceService, item, consilidationFunction, startTime, endTime, resolution);
|
||||
} else {
|
||||
data = getPersistenceSeries(persistenceService, item, startTime, endTime, resolution);
|
||||
}
|
||||
return Response.ok(data, responseType).build();
|
||||
} catch (ItemNotFoundException e1) {
|
||||
logger.error("Item '{}' not found error while requesting series data.", itemName);
|
||||
}
|
||||
return Response.serverError().build();
|
||||
}
|
||||
|
||||
public Object getPersistenceSeries(QueryablePersistenceService persistenceService, Item item, Date timeBegin,
|
||||
Date timeEnd, long resolution) {
|
||||
Map<Long, List<String>> data = new HashMap<>();
|
||||
|
||||
// Define the data filter
|
||||
FilterCriteria filter = new FilterCriteria();
|
||||
filter.setBeginDate(ZonedDateTime.ofInstant(timeBegin.toInstant(), TimeZone.getDefault().toZoneId()));
|
||||
filter.setEndDate(ZonedDateTime.ofInstant(timeEnd.toInstant(), TimeZone.getDefault().toZoneId()));
|
||||
filter.setItemName(item.getName());
|
||||
filter.setOrdering(Ordering.ASCENDING);
|
||||
|
||||
// Get the data from the persistence store
|
||||
Iterable<HistoricItem> result = persistenceService.query(filter);
|
||||
Iterator<HistoricItem> it = result.iterator();
|
||||
|
||||
// Iterate through the data
|
||||
int dataCounter = 0;
|
||||
while (it.hasNext()) {
|
||||
dataCounter++;
|
||||
HistoricItem historicItem = it.next();
|
||||
org.openhab.core.types.State state = historicItem.getState();
|
||||
if (state instanceof DecimalType) {
|
||||
List<String> vals = new ArrayList<>();
|
||||
vals.add(formatDouble(((DecimalType) state).doubleValue(), "null", true));
|
||||
data.put(historicItem.getTimestamp().toInstant().toEpochMilli(), vals);
|
||||
}
|
||||
}
|
||||
logger.debug("'{}' querying item '{}' from '{}' to '{}' => '{}' results", persistenceService.getId(),
|
||||
filter.getItemName(), filter.getBeginDate(), filter.getEndDate(), dataCounter);
|
||||
return convertToRrd(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns a rrd series data, an array of [[timestamp,data1,data2,...]]
|
||||
*
|
||||
* @param persistenceService
|
||||
* @param item
|
||||
* @param consilidationFunction
|
||||
* @param timeBegin
|
||||
* @param timeEnd
|
||||
* @param resolution
|
||||
* @return
|
||||
*/
|
||||
public Object getRrdSeries(QueryablePersistenceService persistenceService, Item item,
|
||||
ConsolFun consilidationFunction, Date timeBegin, Date timeEnd, long resolution) {
|
||||
Map<Long, List<String>> data = new TreeMap<>();
|
||||
try {
|
||||
List<String> itemNames = new ArrayList<>();
|
||||
|
||||
if (item instanceof GroupItem groupItem) {
|
||||
for (Item member : groupItem.getMembers()) {
|
||||
itemNames.add(member.getName());
|
||||
}
|
||||
} else {
|
||||
itemNames.add(item.getName());
|
||||
}
|
||||
for (String itemName : itemNames) {
|
||||
addRrdData(data, itemName, consilidationFunction, timeBegin, timeEnd, resolution);
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
// rrd file does not exist, fallback to generic persistence service
|
||||
logger.debug("no rrd file found '{}'", (RRD_FOLDER + File.separator + item.getName() + ".rrd"));
|
||||
return getPersistenceSeries(persistenceService, item, timeBegin, timeEnd, resolution);
|
||||
} catch (Exception e) {
|
||||
logger.error("{}: fallback to generic persistance service", e.getLocalizedMessage());
|
||||
return getPersistenceSeries(persistenceService, item, timeBegin, timeEnd, resolution);
|
||||
}
|
||||
return convertToRrd(data);
|
||||
}
|
||||
|
||||
private List<Object> convertToRrd(Map<Long, List<String>> data) {
|
||||
// sort data by key
|
||||
Map<Long, List<String>> treeMap = new TreeMap<>(data);
|
||||
List<Object> rrd = new ArrayList<>();
|
||||
for (Long time : treeMap.keySet()) {
|
||||
Object[] entry = new Object[2];
|
||||
entry[0] = time;
|
||||
entry[1] = data.get(time);
|
||||
rrd.add(entry);
|
||||
}
|
||||
return rrd;
|
||||
}
|
||||
|
||||
private Map<Long, List<String>> addRrdData(Map<Long, List<String>> data, String itemName,
|
||||
ConsolFun consilidationFunction, Date timeBegin, Date timeEnd, long resolution) throws IOException {
|
||||
RrdDb rrdDb = new RrdDb(RRD_FOLDER + File.separator + itemName + ".rrd");
|
||||
FetchRequest fetchRequest = rrdDb.createFetchRequest(consilidationFunction, Util.getTimestamp(timeBegin),
|
||||
Util.getTimestamp(timeEnd), resolution);
|
||||
FetchData fetchData = fetchRequest.fetchData();
|
||||
long[] timestamps = fetchData.getTimestamps();
|
||||
double[][] values = fetchData.getValues();
|
||||
|
||||
logger.debug("RRD fetch returned '{}' rows and '{}' columns", fetchData.getRowCount(),
|
||||
fetchData.getColumnCount());
|
||||
|
||||
for (int row = 0; row < fetchData.getRowCount(); row++) {
|
||||
// change to microseconds
|
||||
long time = timestamps[row] * 1000;
|
||||
|
||||
if (!data.containsKey(time)) {
|
||||
data.put(time, new ArrayList<>());
|
||||
}
|
||||
List<String> vals = data.get(time);
|
||||
int indexOffset = vals.size();
|
||||
for (int dsIndex = 0; dsIndex < fetchData.getColumnCount(); dsIndex++) {
|
||||
vals.add(dsIndex + indexOffset, formatDouble(values[dsIndex][row], "null", true));
|
||||
}
|
||||
}
|
||||
rrdDb.close();
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
static String formatDouble(double x, String nanString, boolean forceExponents) {
|
||||
if (Double.isNaN(x)) {
|
||||
return nanString;
|
||||
}
|
||||
if (forceExponents) {
|
||||
synchronized (DECIMAL_FORMAT) {
|
||||
return DECIMAL_FORMAT.format(x);
|
||||
}
|
||||
}
|
||||
return "" + x;
|
||||
}
|
||||
}
|
|
@ -60,7 +60,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||
public class CheckResource implements RESTResource {
|
||||
|
||||
/**
|
||||
* Checks some files and folders for existance and access rights.
|
||||
* Checks some files and folders for existence and access rights.
|
||||
*
|
||||
* @return the check result that contains a bitfield with check results for each entity
|
||||
*/
|
||||
|
|
|
@ -19,6 +19,7 @@ import java.util.Objects;
|
|||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.annotation.security.RolesAllowed;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.DELETE;
|
||||
import javax.ws.rs.GET;
|
||||
|
@ -32,6 +33,7 @@ import javax.ws.rs.core.Response;
|
|||
import javax.ws.rs.core.Response.Status;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.auth.Role;
|
||||
import org.openhab.core.io.rest.RESTConstants;
|
||||
import org.openhab.core.io.rest.RESTResource;
|
||||
import org.openhab.ui.cometvisu.internal.Config;
|
||||
|
@ -55,6 +57,7 @@ import com.google.gson.GsonBuilder;
|
|||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
|
@ -69,6 +72,8 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||
@JaxrsName(Config.COMETVISU_BACKEND_ALIAS + "/" + Config.COMETVISU_BACKEND_CONFIG_ALIAS)
|
||||
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
|
||||
@JSONRequired
|
||||
@RolesAllowed({ Role.ADMIN })
|
||||
@SecurityRequirement(name = "oauth2", scopes = { "admin" })
|
||||
@Path(Config.COMETVISU_BACKEND_ALIAS + "/" + Config.COMETVISU_BACKEND_CONFIG_ALIAS)
|
||||
@Tag(name = Config.COMETVISU_BACKEND_ALIAS + "/" + Config.COMETVISU_BACKEND_CONFIG_ALIAS)
|
||||
@NonNullByDefault
|
||||
|
@ -263,18 +268,7 @@ public class ConfigResource implements RESTResource {
|
|||
java.nio.file.Path hiddenConfigPath = ManagerSettings.getInstance().getConfigPath().resolve("hidden.php");
|
||||
if (hiddenConfigPath.toFile().exists()) {
|
||||
List<String> content = Files.readAllLines(hiddenConfigPath);
|
||||
boolean isPhpVersion = true;
|
||||
for (int i = content.size() - 1; i >= 0; i++) {
|
||||
if (content.get(i).contains("json_decode")) {
|
||||
isPhpVersion = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isPhpVersion) {
|
||||
return loadPhpConfig(config, content);
|
||||
} else {
|
||||
return loadJson(String.join("\n", content));
|
||||
}
|
||||
return loadJson(String.join("\n", content));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
}
|
||||
|
@ -289,41 +283,15 @@ public class ConfigResource implements RESTResource {
|
|||
return Objects.requireNonNull(gson.fromJson(rawContent, HiddenConfig.class));
|
||||
}
|
||||
|
||||
private static HiddenConfig loadPhpConfig(HiddenConfig config, List<String> content) {
|
||||
boolean inHidden = false;
|
||||
|
||||
for (final String line : content) {
|
||||
if (!inHidden) {
|
||||
if ("$hidden = array(".equalsIgnoreCase(line)) {
|
||||
inHidden = true;
|
||||
}
|
||||
} else if (");".equalsIgnoreCase(line)) {
|
||||
break;
|
||||
} else {
|
||||
Matcher m = sectionPattern.matcher(line);
|
||||
if (m.find()) {
|
||||
boolean commented = m.group(1) != null;
|
||||
if (!commented) {
|
||||
String options = m.group(3);
|
||||
Matcher om = optionPattern.matcher(options);
|
||||
ConfigSection section = new ConfigSection();
|
||||
while (om.find()) {
|
||||
section.put(om.group(1), om.group(2));
|
||||
}
|
||||
config.put(m.group(2), section);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
private void writeHiddenConfig(HiddenConfig hidden) throws IOException {
|
||||
java.nio.file.Path hiddenConfigPath = ManagerSettings.getInstance().getConfigPath().resolve("hidden.php");
|
||||
Gson gson = new GsonBuilder().setPrettyPrinting().create();
|
||||
StringBuilder content = new StringBuilder().append("<?php\n")
|
||||
.append("// File for configurations that shouldn't be shared with the user\n").append("$data = '")
|
||||
.append(gson.toJson(hidden)).append("';\n").append("$hidden = json_decode($data, true);\n");
|
||||
.append(gson.toJson(hidden)).append("';\n").append("try {\n")
|
||||
.append(" $hidden = json_decode($data, true, 512, JSON_THROW_ON_ERROR);\n")
|
||||
.append("} catch (JsonException $e) {\n")
|
||||
.append(" $hidden = [\"error\" => $e->getMessage(), \"data\" => $data];\n").append("}\n");
|
||||
Files.writeString(hiddenConfigPath, content);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import java.nio.file.Files;
|
|||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
|
||||
import javax.annotation.security.RolesAllowed;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.DELETE;
|
||||
|
@ -36,6 +37,7 @@ import javax.ws.rs.core.Response;
|
|||
import javax.ws.rs.core.Response.Status;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.auth.Role;
|
||||
import org.openhab.core.io.rest.RESTConstants;
|
||||
import org.openhab.core.io.rest.RESTResource;
|
||||
import org.openhab.ui.cometvisu.internal.Config;
|
||||
|
@ -79,6 +81,7 @@ public class FsResource implements RESTResource {
|
|||
private final Logger logger = LoggerFactory.getLogger(FsResource.class);
|
||||
|
||||
@POST
|
||||
@RolesAllowed({ Role.USER, Role.ADMIN })
|
||||
@Consumes("text/*")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(summary = "Create a text file", responses = { @ApiResponse(responseCode = "200", description = "OK"),
|
||||
|
@ -153,6 +156,7 @@ public class FsResource implements RESTResource {
|
|||
|
||||
@DELETE
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@RolesAllowed({ Role.USER, Role.ADMIN })
|
||||
@Operation(summary = "Deletes a file/folder", responses = { @ApiResponse(responseCode = "200", description = "OK"),
|
||||
@ApiResponse(responseCode = "403", description = "not allowed"),
|
||||
@ApiResponse(responseCode = "404", description = "File/Folder not found"),
|
||||
|
@ -242,33 +246,37 @@ public class FsResource implements RESTResource {
|
|||
}
|
||||
|
||||
@PUT
|
||||
@RolesAllowed({ Role.USER, Role.ADMIN })
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes({ MediaType.TEXT_PLAIN, MediaType.TEXT_XML })
|
||||
@Operation(summary = "Update an existing file", responses = {
|
||||
@ApiResponse(responseCode = "200", description = "OK"),
|
||||
@ApiResponse(responseCode = "403", description = "not allowed"),
|
||||
@ApiResponse(responseCode = "403", description = "forbidden"),
|
||||
@ApiResponse(responseCode = "404", description = "File does not exist") })
|
||||
public Response update(
|
||||
@Parameter(description = "Relative path inside the config folder", required = true) @QueryParam("path") String path,
|
||||
@Parameter(description = "file content") String body,
|
||||
@Parameter(description = "CRC32 hash value of the file content", content = @Content(schema = @Schema(implementation = String.class, defaultValue = "ignore"))) @DefaultValue("ignore") @QueryParam("hash") String hash) {
|
||||
File target = new File(
|
||||
ManagerSettings.getInstance().getConfigFolder().getAbsolutePath() + File.separator + path);
|
||||
if (target.exists()) {
|
||||
if (target.canWrite()) {
|
||||
try {
|
||||
FsUtil.getInstance().saveFile(target, body, hash);
|
||||
return Response.ok().build();
|
||||
} catch (FileOperationException e) {
|
||||
return FsUtil.createErrorResponse(e);
|
||||
} catch (Exception e) {
|
||||
try {
|
||||
MountedFile target = new MountedFile(path);
|
||||
if (target.exists()) {
|
||||
if (target.canWrite()) {
|
||||
try {
|
||||
FsUtil.getInstance().saveFile(target.toFile(), body, hash);
|
||||
return Response.ok().build();
|
||||
} catch (FileOperationException e) {
|
||||
return FsUtil.createErrorResponse(e);
|
||||
} catch (Exception e) {
|
||||
return FsUtil.createErrorResponse(Status.FORBIDDEN, "forbidden");
|
||||
}
|
||||
} else {
|
||||
return FsUtil.createErrorResponse(Status.FORBIDDEN, "forbidden");
|
||||
}
|
||||
} else {
|
||||
return FsUtil.createErrorResponse(Status.FORBIDDEN, "forbidden");
|
||||
return FsUtil.createErrorResponse(Status.NOT_FOUND, "not found");
|
||||
}
|
||||
} else {
|
||||
return FsUtil.createErrorResponse(Status.NOT_FOUND, "not found");
|
||||
} catch (FileOperationException e) {
|
||||
return FsUtil.createErrorResponse(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,92 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.ui.cometvisu.internal.backend.rest;
|
||||
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.HttpHeaders;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.io.rest.RESTConstants;
|
||||
import org.openhab.core.io.rest.RESTResource;
|
||||
import org.openhab.ui.cometvisu.internal.Config;
|
||||
import org.openhab.ui.cometvisu.internal.backend.model.ConfigBean;
|
||||
import org.openhab.ui.cometvisu.internal.backend.model.LoginBean;
|
||||
import org.openhab.ui.cometvisu.internal.backend.model.ResourcesBean;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.jaxrs.whiteboard.JaxrsWhiteboardConstants;
|
||||
import org.osgi.service.jaxrs.whiteboard.propertytypes.JSONRequired;
|
||||
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsApplicationSelect;
|
||||
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsName;
|
||||
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* handles login request from the CometVisu client
|
||||
* currently this is just a placeholder and does no real authentification
|
||||
*
|
||||
* @author Tobias Bräutigam - Initial contribution
|
||||
* @author Wouter Born - Migrated to JAX-RS Whiteboard Specification
|
||||
* @author Wouter Born - Migrated to OpenAPI annotations
|
||||
*
|
||||
* @deprecated CometVisu (>=0.12) is using openHAB's native REST API, a special backend implementation is obsolete now
|
||||
*/
|
||||
@Component
|
||||
@JaxrsResource
|
||||
@JaxrsName(Config.COMETVISU_BACKEND_ALIAS + "/" + Config.COMETVISU_BACKEND_LOGIN_ALIAS)
|
||||
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
|
||||
@JSONRequired
|
||||
@Path(Config.COMETVISU_BACKEND_ALIAS + "/" + Config.COMETVISU_BACKEND_LOGIN_ALIAS)
|
||||
@Tag(name = Config.COMETVISU_BACKEND_ALIAS + "/" + Config.COMETVISU_BACKEND_LOGIN_ALIAS)
|
||||
@NonNullByDefault
|
||||
@Deprecated(since = "3.4", forRemoval = true)
|
||||
public class LoginResource implements RESTResource {
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(summary = "returns the login response with backend configuration information", responses = {
|
||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoginBean.class))) })
|
||||
public Response getLogin(@Context UriInfo uriInfo, @Context HttpHeaders headers, @QueryParam("u") String user,
|
||||
@QueryParam("p") String password, @QueryParam("d") String device) {
|
||||
LoginBean bean = new LoginBean();
|
||||
bean.v = "0.0.1";
|
||||
bean.s = "0"; // Session-ID not needed with SSE
|
||||
ConfigBean conf = new ConfigBean();
|
||||
ResourcesBean res = new ResourcesBean();
|
||||
String origin = headers.getHeaderString("Origin");
|
||||
String serverHost = uriInfo.getBaseUri().getScheme() + "://" + uriInfo.getBaseUri().getHost();
|
||||
if (uriInfo.getBaseUri().getPort() != 80) {
|
||||
serverHost += ":" + uriInfo.getBaseUri().getPort();
|
||||
}
|
||||
String host = origin == null || serverHost.compareToIgnoreCase(origin) == 0 ? "" : serverHost;
|
||||
|
||||
conf.baseURL = host + "/rest/" + Config.COMETVISU_BACKEND_ALIAS + "/";
|
||||
conf.resources = res;
|
||||
res.read = Config.COMETVISU_BACKEND_READ_ALIAS;
|
||||
res.rrd = Config.COMETVISU_BACKEND_CHART_ALIAS;
|
||||
res.write = Config.COMETVISU_BACKEND_WRITE_ALIAS;
|
||||
res.rest = conf.baseURL.substring(0, conf.baseURL.length() - 1); // no trailing slash
|
||||
bean.c = conf;
|
||||
return Response.ok(bean, MediaType.APPLICATION_JSON).build();
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@ package org.openhab.ui.cometvisu.internal.backend.rest;
|
|||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
|
||||
import javax.annotation.security.RolesAllowed;
|
||||
import javax.ws.rs.PUT;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
|
@ -24,6 +25,7 @@ import javax.ws.rs.core.Response;
|
|||
import javax.ws.rs.core.Response.Status;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.auth.Role;
|
||||
import org.openhab.core.io.rest.RESTConstants;
|
||||
import org.openhab.core.io.rest.RESTResource;
|
||||
import org.openhab.ui.cometvisu.internal.Config;
|
||||
|
@ -56,6 +58,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||
@JaxrsName(Config.COMETVISU_BACKEND_ALIAS + "/fs/move")
|
||||
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
|
||||
@JSONRequired
|
||||
@RolesAllowed({ Role.USER, Role.ADMIN })
|
||||
@Path(Config.COMETVISU_BACKEND_ALIAS + "/fs/move")
|
||||
@Tag(name = Config.COMETVISU_BACKEND_ALIAS + "/fs/move")
|
||||
@NonNullByDefault
|
||||
|
|
|
@ -21,6 +21,9 @@ import java.net.http.HttpResponse;
|
|||
import java.net.http.HttpResponse.BodyHandlers;
|
||||
import java.time.Duration;
|
||||
import java.util.Base64;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
|
@ -78,7 +81,9 @@ public class ProxyResource implements RESTResource {
|
|||
@Produces({ MediaType.APPLICATION_JSON, MediaType.MEDIA_TYPE_WILDCARD })
|
||||
@Operation(summary = "proxy a request", responses = { @ApiResponse(responseCode = "200", description = "OK"),
|
||||
@ApiResponse(responseCode = "400", description = "Bad request"),
|
||||
@ApiResponse(responseCode = "403", description = "Forbidden"),
|
||||
@ApiResponse(responseCode = "404", description = "Not found"),
|
||||
@ApiResponse(responseCode = "406", description = "Not Acceptable"),
|
||||
@ApiResponse(responseCode = "500", description = "Internal server error") })
|
||||
public Response proxy(
|
||||
@Parameter(description = "URL this request should be sent to", content = @Content(schema = @Schema(implementation = String.class, defaultValue = ""))) @QueryParam("url") @Nullable String url,
|
||||
|
@ -86,9 +91,9 @@ public class ProxyResource implements RESTResource {
|
|||
@Parameter(description = "use information from hidden config section", content = @Content(schema = @Schema(implementation = String.class, defaultValue = ""))) @QueryParam("config-section") @Nullable String configSection) {
|
||||
ConfigSection sec = null;
|
||||
String queryUrl = url != null ? url : "";
|
||||
HiddenConfig config = ConfigResource.loadHiddenConfig();
|
||||
if (configSection != null && !configSection.isBlank()) {
|
||||
// read URI and further information
|
||||
HiddenConfig config = ConfigResource.loadHiddenConfig();
|
||||
sec = config.get(configSection);
|
||||
if (sec != null) {
|
||||
String configUrl = sec.get("uri");
|
||||
|
@ -101,6 +106,29 @@ public class ProxyResource implements RESTResource {
|
|||
}
|
||||
} else if (url == null || url.isBlank()) {
|
||||
return Response.status(Status.BAD_REQUEST).build();
|
||||
} else {
|
||||
ConfigSection whiteList = config.get("proxy.whitelist");
|
||||
boolean allowed = false;
|
||||
if (whiteList != null) {
|
||||
for (Map.Entry<String, String> entry : whiteList.entrySet()) {
|
||||
String value = entry.getValue();
|
||||
if (value.startsWith("/") && value.endsWith("/")) {
|
||||
Pattern pattern = Pattern.compile(value.substring(1, value.length() - 1),
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
Matcher matcher = pattern.matcher(queryUrl);
|
||||
if (matcher.find()) {
|
||||
allowed = true;
|
||||
break;
|
||||
}
|
||||
} else if (value.equalsIgnoreCase(queryUrl)) {
|
||||
allowed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!allowed) {
|
||||
return Response.status(Status.FORBIDDEN).build();
|
||||
}
|
||||
}
|
||||
logger.info("proxying request to {}", queryUrl);
|
||||
|
||||
|
|
|
@ -1,254 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.ui.cometvisu.internal.backend.rest;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.sse.Sse;
|
||||
import javax.ws.rs.sse.SseEventSink;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.io.rest.RESTConstants;
|
||||
import org.openhab.core.io.rest.RESTResource;
|
||||
import org.openhab.core.io.rest.SseBroadcaster;
|
||||
import org.openhab.core.items.GenericItem;
|
||||
import org.openhab.core.items.Item;
|
||||
import org.openhab.core.items.ItemFactory;
|
||||
import org.openhab.core.items.ItemNotFoundException;
|
||||
import org.openhab.core.items.ItemRegistry;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.ui.cometvisu.internal.Config;
|
||||
import org.openhab.ui.cometvisu.internal.backend.model.StateBean;
|
||||
import org.openhab.ui.cometvisu.internal.listeners.StateEventListener;
|
||||
import org.openhab.ui.cometvisu.internal.util.SseUtil;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Deactivate;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.osgi.service.jaxrs.whiteboard.JaxrsWhiteboardConstants;
|
||||
import org.osgi.service.jaxrs.whiteboard.propertytypes.JSONRequired;
|
||||
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsApplicationSelect;
|
||||
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsName;
|
||||
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* handles read request from the CometVisu client every request initializes a
|
||||
* SSE communication
|
||||
*
|
||||
* @author Tobias Bräutigam - Initial contribution
|
||||
* @author Wouter Born - Migrated to JAX-RS Whiteboard Specification
|
||||
* @author Wouter Born - Migrated to OpenAPI annotations
|
||||
*
|
||||
* @deprecated CometVisu (>=0.12) is using openHAB's native REST API, a special backend implementation is obsolete now
|
||||
*/
|
||||
@Component(immediate = true)
|
||||
@JaxrsResource
|
||||
@JaxrsName(Config.COMETVISU_BACKEND_ALIAS + "/" + Config.COMETVISU_BACKEND_READ_ALIAS)
|
||||
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
|
||||
@JSONRequired
|
||||
@Path(Config.COMETVISU_BACKEND_ALIAS + "/" + Config.COMETVISU_BACKEND_READ_ALIAS)
|
||||
@Tag(name = Config.COMETVISU_BACKEND_ALIAS + "/" + Config.COMETVISU_BACKEND_READ_ALIAS)
|
||||
@NonNullByDefault
|
||||
@Deprecated(since = "3.4", forRemoval = true)
|
||||
public class ReadResource implements EventBroadcaster, RESTResource {
|
||||
private final Logger logger = LoggerFactory.getLogger(ReadResource.class);
|
||||
|
||||
private SseBroadcaster<SseSinkInfo> broadcaster = new SseBroadcaster<>();
|
||||
|
||||
private final ExecutorService executorService = Executors.newSingleThreadExecutor();
|
||||
|
||||
private final ItemRegistry itemRegistry;
|
||||
|
||||
private final StateEventListener stateEventListener = new StateEventListener(this);
|
||||
|
||||
private List<String> itemNames = new ArrayList<>();
|
||||
private Map<Item, Map<String, @Nullable Class<? extends State>>> items = new HashMap<>();
|
||||
|
||||
private @NonNullByDefault({}) Sse sse;
|
||||
|
||||
private Collection<ItemFactory> itemFactories = new CopyOnWriteArrayList<>();
|
||||
|
||||
@Activate
|
||||
public ReadResource(@Reference ItemRegistry itemRegistry) {
|
||||
this.itemRegistry = itemRegistry;
|
||||
}
|
||||
|
||||
@Deactivate
|
||||
public void deactivate() {
|
||||
broadcaster.close();
|
||||
}
|
||||
|
||||
@Context
|
||||
public void setSse(final Sse sse) {
|
||||
this.sse = sse;
|
||||
}
|
||||
|
||||
protected void addItemFactory(ItemFactory itemFactory) {
|
||||
itemFactories.add(itemFactory);
|
||||
}
|
||||
|
||||
protected void removeItemFactory(ItemFactory itemFactory) {
|
||||
itemFactories.remove(itemFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes the connecting client to the stream of events filtered by the
|
||||
* given eventFilter.
|
||||
*
|
||||
* @throws IOException
|
||||
* @throws InterruptedException
|
||||
*/
|
||||
@GET
|
||||
@Produces(MediaType.SERVER_SENT_EVENTS)
|
||||
@Operation(summary = "Creates the SSE stream for item states, sends all requested states once and then only changes states", responses = {
|
||||
@ApiResponse(responseCode = "200", description = "OK") })
|
||||
public void getStates(@Context final SseEventSink sseEventSink, @QueryParam("a") List<String> itemNames,
|
||||
@QueryParam("i") long index, @QueryParam("t") long time) throws IOException, InterruptedException {
|
||||
this.itemNames = itemNames;
|
||||
|
||||
broadcaster.add(sseEventSink, new SseSinkInfo(itemNames, index, time));
|
||||
|
||||
// get all requested items and send their states to the client
|
||||
items = new HashMap<>();
|
||||
// send the current states of all items to the client
|
||||
List<StateBean> states = new ArrayList<>();
|
||||
for (String cvItemName : itemNames) {
|
||||
try {
|
||||
String[] parts = cvItemName.split(":");
|
||||
String ohItemName = cvItemName;
|
||||
Class<? extends State> stateClass = null;
|
||||
if (parts.length == 2) {
|
||||
String classPrefix = parts[0].toLowerCase();
|
||||
if (Config.itemTypeMapper.containsKey(classPrefix)) {
|
||||
stateClass = Config.itemTypeMapper.get(classPrefix);
|
||||
classPrefix += ":";
|
||||
} else {
|
||||
logger.debug("no type found for '{}'", classPrefix);
|
||||
classPrefix = "";
|
||||
}
|
||||
ohItemName = parts[1];
|
||||
}
|
||||
Item item = this.itemRegistry.getItem(ohItemName);
|
||||
if (!items.containsKey(item)) {
|
||||
items.put(item, new HashMap<>());
|
||||
}
|
||||
items.get(item).put(cvItemName, stateClass);
|
||||
StateBean itemState = new StateBean();
|
||||
itemState.name = cvItemName;
|
||||
|
||||
if (stateClass != null) {
|
||||
itemState.state = item.getStateAs(stateClass).toString();
|
||||
logger.trace("get state of '{}' as '{}' == '{}'", item, stateClass, itemState.state);
|
||||
} else {
|
||||
itemState.state = item.getState().toString();
|
||||
}
|
||||
states.add(itemState);
|
||||
} catch (ItemNotFoundException e) {
|
||||
logger.error("{}", e.getLocalizedMessage());
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("initially broadcasting {}/{} item states", states.size(), itemNames.size());
|
||||
broadcaster.send(SseUtil.buildEvent(sse.newEventBuilder(), states));
|
||||
|
||||
// listen to state changes of the requested items
|
||||
registerItems();
|
||||
}
|
||||
|
||||
/**
|
||||
* listen for state changes from the requested items
|
||||
*/
|
||||
@Override
|
||||
public void registerItems() {
|
||||
for (Item item : items.keySet()) {
|
||||
if (item instanceof GenericItem) {
|
||||
((GenericItem) item).addStateChangeListener(stateEventListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* listens to state changes of the given item, if it is part of the requested items
|
||||
*
|
||||
* @param item the new item, that should be listened to
|
||||
*/
|
||||
@Override
|
||||
public void registerItem(Item item) {
|
||||
if (items.containsKey(item) || !itemNames.contains(item.getName())) {
|
||||
return;
|
||||
}
|
||||
if (item instanceof GenericItem) {
|
||||
((GenericItem) item).addStateChangeListener(stateEventListener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* listens to state changes of the given item, if it is part of the
|
||||
* requested items
|
||||
*
|
||||
* @param item the new item, that should be listened to
|
||||
*/
|
||||
@Override
|
||||
public void unregisterItem(Item item) {
|
||||
if (items.containsKey(item) || !itemNames.contains(item.getName())) {
|
||||
return;
|
||||
}
|
||||
if (item instanceof GenericItem) {
|
||||
((GenericItem) item).removeStateChangeListener(stateEventListener);
|
||||
items.remove(item);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts an event described by the given parameters to all currently
|
||||
* listening clients.
|
||||
*
|
||||
* @param eventObject bean that can be converted to a JSON object.
|
||||
*/
|
||||
@Override
|
||||
public void broadcastEvent(final Object eventObject) {
|
||||
if (sse == null) {
|
||||
logger.trace("broadcast skipped (no one listened since activation)");
|
||||
return;
|
||||
}
|
||||
|
||||
executorService.execute(() -> broadcaster.send(SseUtil.buildEvent(sse.newEventBuilder(), eventObject)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, @Nullable Class<? extends State>> getClientItems(Item item) {
|
||||
return Objects.requireNonNullElse(items.get(item), Map.of());
|
||||
}
|
||||
}
|
|
@ -1,116 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.ui.cometvisu.internal.backend.rest;
|
||||
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.HttpHeaders;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.Response.Status;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.events.EventPublisher;
|
||||
import org.openhab.core.io.rest.RESTConstants;
|
||||
import org.openhab.core.io.rest.RESTResource;
|
||||
import org.openhab.core.items.Item;
|
||||
import org.openhab.core.items.ItemNotFoundException;
|
||||
import org.openhab.core.items.ItemRegistry;
|
||||
import org.openhab.core.items.events.ItemEventFactory;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.TypeParser;
|
||||
import org.openhab.ui.cometvisu.internal.Config;
|
||||
import org.openhab.ui.cometvisu.internal.backend.model.SuccessBean;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.osgi.service.jaxrs.whiteboard.JaxrsWhiteboardConstants;
|
||||
import org.osgi.service.jaxrs.whiteboard.propertytypes.JSONRequired;
|
||||
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsApplicationSelect;
|
||||
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsName;
|
||||
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* handles state updates send by the CometVisu client and forwars them to the EventPublisher
|
||||
*
|
||||
* @author Tobias Bräutigam - Initial contribution
|
||||
* @author Wouter Born - Migrated to JAX-RS Whiteboard Specification
|
||||
* @author Wouter Born - Migrated to OpenAPI annotations
|
||||
*
|
||||
* @deprecated CometVisu (>=0.12) is using openHAB's native REST API, a special backend implementation is obsolete now
|
||||
*/
|
||||
@Component
|
||||
@JaxrsResource
|
||||
@JaxrsName(Config.COMETVISU_BACKEND_ALIAS + "/" + Config.COMETVISU_BACKEND_WRITE_ALIAS)
|
||||
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
|
||||
@JSONRequired
|
||||
@Path(Config.COMETVISU_BACKEND_ALIAS + "/" + Config.COMETVISU_BACKEND_WRITE_ALIAS)
|
||||
@Tag(name = Config.COMETVISU_BACKEND_ALIAS + "/" + Config.COMETVISU_BACKEND_WRITE_ALIAS)
|
||||
@NonNullByDefault
|
||||
@Deprecated(since = "3.4", forRemoval = true)
|
||||
public class WriteResource implements RESTResource {
|
||||
private final Logger logger = LoggerFactory.getLogger(WriteResource.class);
|
||||
|
||||
private final EventPublisher eventPublisher;
|
||||
private final ItemRegistry itemRegistry;
|
||||
|
||||
private @Context @NonNullByDefault({}) UriInfo uriInfo;
|
||||
|
||||
@Activate
|
||||
public WriteResource(final @Reference EventPublisher eventPublisher, final @Reference ItemRegistry itemRegistry) {
|
||||
this.eventPublisher = eventPublisher;
|
||||
this.itemRegistry = itemRegistry;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(summary = "starts defined actions e.g. downloading the CometVisu client", responses = {
|
||||
@ApiResponse(responseCode = "200", description = "OK"),
|
||||
@ApiResponse(responseCode = "404", description = "Item not found") })
|
||||
public Response setState(@Context HttpHeaders headers,
|
||||
@Parameter(description = "Item name", required = true) @QueryParam("a") String itemName,
|
||||
@Parameter(description = "Item value", required = true) @QueryParam("v") String value,
|
||||
@Parameter(description = "timestamp") @QueryParam("ts") long timestamp) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Received CV write request at '{}' for item '{}' with value '{}'.", uriInfo.getPath(),
|
||||
itemName, value);
|
||||
}
|
||||
Item item;
|
||||
try {
|
||||
item = itemRegistry.getItem(itemName);
|
||||
Command command = TypeParser.parseCommand(item.getAcceptedCommandTypes(), value);
|
||||
SuccessBean bean = new SuccessBean();
|
||||
if (command != null) {
|
||||
eventPublisher.post(ItemEventFactory.createCommandEvent(item.getName(), command));
|
||||
bean.success = 1;
|
||||
} else {
|
||||
bean.success = 0;
|
||||
}
|
||||
return Response.ok(bean, MediaType.APPLICATION_JSON).build();
|
||||
} catch (ItemNotFoundException e) {
|
||||
logger.error("{}", e.getLocalizedMessage());
|
||||
return Response.status(Status.NOT_FOUND).build();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -767,7 +767,7 @@ public class ConfigHelper {
|
|||
public void addSeparatorToNavbar(Page page, NavbarPositionType position, boolean ifNotEmpty) {
|
||||
Navbar navbar = getNavbar(page, position);
|
||||
if (navbar != null) {
|
||||
if (!ifNotEmpty || navbar.getPageOrGroupOrLine().size() > 0) {
|
||||
if (!ifNotEmpty || !navbar.getPageOrGroupOrLine().isEmpty()) {
|
||||
Line line = new Line();
|
||||
line.setLayout(createLayout(0));
|
||||
navbar.getPageOrGroupOrLine().add(factory.createNavbarLine(line));
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.ui.cometvisu.internal.listeners;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import org.openhab.core.items.Item;
|
||||
import org.openhab.core.items.ItemRegistry;
|
||||
import org.openhab.core.items.ItemRegistryChangeListener;
|
||||
import org.openhab.ui.cometvisu.internal.backend.rest.EventBroadcaster;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
|
||||
/**
|
||||
* Listener responsible for notifying the CometVisu backend about changes
|
||||
* in the ItemRegistry
|
||||
*
|
||||
* @author Tobias Bräutigam - Initial Contribution and API
|
||||
*/
|
||||
@Component(immediate = true)
|
||||
public class ItemRegistryEventListener implements ItemRegistryChangeListener {
|
||||
private ItemRegistry itemRegistry;
|
||||
|
||||
private EventBroadcaster eventBroadcaster;
|
||||
|
||||
@Reference
|
||||
protected void setEventBroadcaster(EventBroadcaster eventBroadcaster) {
|
||||
this.eventBroadcaster = eventBroadcaster;
|
||||
}
|
||||
|
||||
protected void unsetEventBroadcaster(EventBroadcaster eventBroadcaster) {
|
||||
this.eventBroadcaster = null;
|
||||
}
|
||||
|
||||
@Reference
|
||||
protected void setItemRegistry(ItemRegistry itemRegistry) {
|
||||
this.itemRegistry = itemRegistry;
|
||||
this.itemRegistry.addRegistryChangeListener(this);
|
||||
}
|
||||
|
||||
protected void unsetItemRegistry(ItemRegistry itemRegistry) {
|
||||
this.itemRegistry.removeRegistryChangeListener(this);
|
||||
this.itemRegistry = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void added(Item element) {
|
||||
eventBroadcaster.registerItem(element);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removed(Item element) {
|
||||
eventBroadcaster.unregisterItem(element);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updated(Item oldElement, Item element) {
|
||||
eventBroadcaster.unregisterItem(oldElement);
|
||||
eventBroadcaster.registerItem(element);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void allItemsChanged(Collection<String> oldItemNames) {
|
||||
// All items have changed, StateListener needs to be registered to the new Items
|
||||
eventBroadcaster.registerItems();
|
||||
}
|
||||
}
|
|
@ -1,87 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.ui.cometvisu.internal.listeners;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.items.GroupItem;
|
||||
import org.openhab.core.items.Item;
|
||||
import org.openhab.core.items.StateChangeListener;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.ui.cometvisu.internal.backend.model.StateBean;
|
||||
import org.openhab.ui.cometvisu.internal.backend.rest.EventBroadcaster;
|
||||
|
||||
/**
|
||||
* listens to state changes on items and send them to an EventBroadcaster
|
||||
*
|
||||
* @author Tobias Bräutigam - Initial contribution
|
||||
*/
|
||||
public class StateEventListener implements StateChangeListener {
|
||||
|
||||
private EventBroadcaster eventBroadcaster;
|
||||
|
||||
public StateEventListener(EventBroadcaster eventBroadcaster) {
|
||||
this.eventBroadcaster = eventBroadcaster;
|
||||
}
|
||||
|
||||
public void setEventBroadcaster(EventBroadcaster eventBroadcaster) {
|
||||
this.eventBroadcaster = eventBroadcaster;
|
||||
}
|
||||
|
||||
protected void unsetEventBroadcaster(EventBroadcaster eventBroadcaster) {
|
||||
this.eventBroadcaster = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stateChanged(Item item, State oldState, State newState) {
|
||||
Map<String, @Nullable Class<? extends State>> clientItems = eventBroadcaster.getClientItems(item);
|
||||
if (!clientItems.isEmpty()) {
|
||||
for (String cvItemName : clientItems.keySet()) {
|
||||
Class<? extends State> stateClass = clientItems.get(cvItemName);
|
||||
StateBean stateBean = new StateBean();
|
||||
stateBean.name = cvItemName;
|
||||
if (stateClass != null) {
|
||||
stateBean.state = item.getStateAs(stateClass).toString();
|
||||
} else {
|
||||
stateBean.state = item.getState().toString();
|
||||
}
|
||||
eventBroadcaster.broadcastEvent(stateBean);
|
||||
}
|
||||
} else {
|
||||
StateBean stateBean = new StateBean();
|
||||
stateBean.name = item.getName();
|
||||
stateBean.state = newState.toString();
|
||||
eventBroadcaster.broadcastEvent(stateBean);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stateUpdated(Item item, State state) {
|
||||
if (item instanceof GroupItem) {
|
||||
// group item update could be relevant for the client, although the state of switch group does not change
|
||||
// wenn more the one are on, the number-groupFunction changes
|
||||
Map<String, @Nullable Class<? extends State>> clientItems = eventBroadcaster.getClientItems(item);
|
||||
for (String cvItemName : clientItems.keySet()) {
|
||||
Class<? extends State> stateClass = clientItems.get(cvItemName);
|
||||
if (stateClass != null) {
|
||||
StateBean stateBean = new StateBean();
|
||||
stateBean.name = cvItemName;
|
||||
stateBean.state = item.getStateAs(stateClass).toString();
|
||||
|
||||
eventBroadcaster.broadcastEvent(stateBean);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,7 +20,6 @@ import java.io.InputStream;
|
|||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.DateFormat;
|
||||
|
@ -45,6 +44,7 @@ import javax.servlet.http.HttpServletRequest;
|
|||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.OpenHAB;
|
||||
import org.openhab.core.items.Item;
|
||||
|
@ -72,6 +72,7 @@ import com.google.gson.Gson;
|
|||
*
|
||||
* @author Tobias Bräutigam - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class CometVisuServlet extends HttpServlet {
|
||||
private static final long serialVersionUID = 4448918908615003303L;
|
||||
private final Logger logger = LoggerFactory.getLogger(CometVisuServlet.class);
|
||||
|
@ -138,6 +139,10 @@ public class CometVisuServlet extends HttpServlet {
|
|||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
|
||||
File requestedFile = getRequestedFile(req);
|
||||
if (requestedFile == null) {
|
||||
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
|
||||
String path = req.getPathInfo();
|
||||
if (path == null) {
|
||||
|
@ -168,6 +173,10 @@ public class CometVisuServlet extends HttpServlet {
|
|||
}
|
||||
}
|
||||
}
|
||||
if (requestedFile.getName().equalsIgnoreCase("version")) {
|
||||
// tell client that its been served by openhab
|
||||
resp.setHeader("X-CometVisu-Backend-Name", "openhab");
|
||||
}
|
||||
if (requestedFile.getName().equalsIgnoreCase("hidden.php")) {
|
||||
// do not deliver the hidden php
|
||||
resp.sendError(HttpServletResponse.SC_FORBIDDEN);
|
||||
|
@ -178,7 +187,7 @@ public class CometVisuServlet extends HttpServlet {
|
|||
}
|
||||
}
|
||||
|
||||
protected File getRequestedFile(HttpServletRequest req) throws UnsupportedEncodingException {
|
||||
protected @Nullable File getRequestedFile(HttpServletRequest req) throws IOException {
|
||||
String requestedFile = req.getPathInfo();
|
||||
File file = null;
|
||||
|
||||
|
@ -188,12 +197,18 @@ public class CometVisuServlet extends HttpServlet {
|
|||
requestedFile = requestedFile.substring(0, requestedFile.length() - 1);
|
||||
}
|
||||
file = new File(userFileFolder, URLDecoder.decode(requestedFile, StandardCharsets.UTF_8));
|
||||
if (!file.getCanonicalPath().startsWith(userFileFolder.getCanonicalPath() + File.separator)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// serve the file from the cometvisu src directory
|
||||
if (file == null || !file.exists() || file.isDirectory()) {
|
||||
file = requestedFile != null
|
||||
? new File(rootFolder, URLDecoder.decode(requestedFile, StandardCharsets.UTF_8))
|
||||
: rootFolder;
|
||||
if (!file.getCanonicalPath().startsWith(rootFolder.getCanonicalPath() + File.separator)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (file.isDirectory()) {
|
||||
// search for an index file
|
||||
|
|
|
@ -287,7 +287,7 @@ public class FsUtil {
|
|||
}
|
||||
|
||||
public static Response createErrorResponse(FileOperationException e) {
|
||||
return FsUtil.createErrorResponse(e.getStatus(), e.getCause().toString());
|
||||
return FsUtil.createErrorResponse(e.getStatus(), e.getMessage());
|
||||
}
|
||||
|
||||
public static Response createErrorResponse(Status status, String message) {
|
||||
|
|
|
@ -82,6 +82,10 @@ public class MountedFile {
|
|||
return toFile().exists();
|
||||
}
|
||||
|
||||
public boolean canWrite() {
|
||||
return toFile().canWrite();
|
||||
}
|
||||
|
||||
public boolean isDirectory() {
|
||||
return toFile().isDirectory();
|
||||
}
|
||||
|
|
|
@ -1,71 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.ui.cometvisu.internal.util;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.sse.OutboundSseEvent;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.ui.cometvisu.internal.StateBeanMessageBodyWriter;
|
||||
import org.openhab.ui.cometvisu.internal.backend.model.StateBean;
|
||||
|
||||
/**
|
||||
* Utility class containing helper methods for the SSE implementation.
|
||||
*
|
||||
* @author Tobias Bräutigam - Initial Contribution and API
|
||||
* @author Wouter Born - Migrated to JAX-RS Whiteboard Specification
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SseUtil {
|
||||
|
||||
/**
|
||||
* Creates a new {@link OutboundSseEvent} object containing an
|
||||
* {@link StateBean} created for the given eventType, objectIdentifier,
|
||||
* eventObject.
|
||||
*
|
||||
* @param eventBuilder the builder used for building the event
|
||||
* @param eventObject the eventObject to be included
|
||||
* @return a new OutboundSseEvent
|
||||
*/
|
||||
public static OutboundSseEvent buildEvent(OutboundSseEvent.Builder eventBuilder, Object eventObject) {
|
||||
StateBeanMessageBodyWriter writer = new StateBeanMessageBodyWriter();
|
||||
Date date = new Date();
|
||||
return eventBuilder.mediaType(MediaType.TEXT_PLAIN_TYPE).data(writer.serialize(eventObject))
|
||||
.id(String.valueOf(date.getTime())).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to mark our current thread(request processing) that SSE blocking
|
||||
* should be enabled.
|
||||
*/
|
||||
private static ThreadLocal<Boolean> blockingSseEnabled = ThreadLocal.withInitial(() -> false);
|
||||
|
||||
/**
|
||||
* Returns true if the current thread is processing an SSE request that
|
||||
* should block.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static boolean shouldAsyncBlock() {
|
||||
return blockingSseEnabled.get().booleanValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the current thread as processing a blocking sse request.
|
||||
*/
|
||||
public static void enableBlockingSse() {
|
||||
blockingSseEnabled.set(true);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue