[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
Tobias Bräutigam 2024-07-17 22:00:00 +02:00 committed by Kai Kreuzer
parent 091d0edc06
commit 630e852583
24 changed files with 104 additions and 1318 deletions

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -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
*/

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}

View File

@ -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

View File

@ -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);

View File

@ -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());
}
}

View File

@ -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();
}
}
}

View File

@ -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));

View File

@ -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();
}
}

View File

@ -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);
}
}
}
}
}

View File

@ -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

View 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) {

View File

@ -82,6 +82,10 @@ public class MountedFile {
return toFile().exists();
}
public boolean canWrite() {
return toFile().canWrite();
}
public boolean isDirectory() {
return toFile().isDirectory();
}

View File

@ -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);
}
}