CometVisu Backend for openHAB affected by RCE through path traversal
Description
openHAB, a provider of open-source home automation software, has add-ons including the visualization add-on CometVisu. Prior to version 4.2.1, CometVisu's file system endpoints don't require authentication and additionally the endpoint to update an existing file is susceptible to path traversal. This makes it possible for an attacker to overwrite existing files on the openHAB instance. If the overwritten file is a shell script that is executed at a later time, this vulnerability can allow remote code execution by an attacker. Users should upgrade to version 4.2.1 to receive a patch.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
CometVisu add-on in openHAB prior to 4.2.1 has unauthenticated file system endpoints with path traversal, allowing file overwrite leading to remote code execution.
Vulnerability
Description
In openHAB's CometVisu visualization add-on, the file system endpoints are unauthenticated, and the update endpoint is vulnerable to path traversal [2][4]. This allows an attacker to overwrite arbitrary files on the server by manipulating file paths, bypassing intended directory restrictions.
Exploitation
An attacker with network access to the CometVisu backend can send crafted requests without any authentication [2]. By using path traversal sequences (e.g., "../"), the attacker can target files outside the intended mount points, such as shell scripts or other executable files. The vulnerability exists because the path validation only checks for ".." in the target but not properly sanitizes the source path [3]. No special privileges are required beyond network access.
Impact
If an attacker overwrites a shell script that is later executed (e.g., by a cron job or system service), this can lead to remote code execution with the privileges of the openHAB process [4]. This compromises the confidentiality, integrity, and availability of the system.
Mitigation
The issue is fixed in openHAB version 4.2.1 [2]. The commit [3] introduces authentication requirements and improves path validation to prevent path traversal. Users are strongly advised to upgrade immediately; there are no known workarounds.
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.openhab.ui.bundles:org.openhab.ui.cometvisuMaven | < 4.2.1 | 4.2.1 |
Affected products
1- Range: < 4.2.1
Patches
1630e8525835c[cometvisu] Security fixes & cleanup for cometvisu backend (#2671)
24 files changed · +104 −1318
bundles/org.openhab.ui.cometvisu/src/main/java/org/openhab/ui/cometvisu/internal/backend/model/ConfigBean.java+0 −27 removed@@ -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; -}
bundles/org.openhab.ui.cometvisu/src/main/java/org/openhab/ui/cometvisu/internal/backend/model/LoginBean.java+0 −26 removed@@ -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; -}
bundles/org.openhab.ui.cometvisu/src/main/java/org/openhab/ui/cometvisu/internal/backend/model/ResourcesBean.java+0 −27 removed@@ -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; -}
bundles/org.openhab.ui.cometvisu/src/main/java/org/openhab/ui/cometvisu/internal/backend/model/rest/RestBackendEnvironmentState.java+5 −2 modified@@ -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 @@ * @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(); }
bundles/org.openhab.ui.cometvisu/src/main/java/org/openhab/ui/cometvisu/internal/backend/model/StateBean.java+0 −25 removed@@ -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; -}
bundles/org.openhab.ui.cometvisu/src/main/java/org/openhab/ui/cometvisu/internal/backend/model/SuccessBean.java+0 −24 removed@@ -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; -}
bundles/org.openhab.ui.cometvisu/src/main/java/org/openhab/ui/cometvisu/internal/backend/rest/ChartResource.java+0 −330 removed@@ -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; - } -}
bundles/org.openhab.ui.cometvisu/src/main/java/org/openhab/ui/cometvisu/internal/backend/rest/CheckResource.java+1 −1 modified@@ -60,7 +60,7 @@ 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 */
bundles/org.openhab.ui.cometvisu/src/main/java/org/openhab/ui/cometvisu/internal/backend/rest/ConfigResource.java+10 −42 modified@@ -19,6 +19,7 @@ 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.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 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 @@ @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 static HiddenConfig loadHiddenConfig() { 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 @@ private static HiddenConfig loadJson(String content) { 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); } }
bundles/org.openhab.ui.cometvisu/src/main/java/org/openhab/ui/cometvisu/internal/backend/rest/FsResource.java+22 −14 modified@@ -19,6 +19,7 @@ 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.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 Response createBinary(@Context HttpServletRequest request, @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 Response read( } @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); } } }
bundles/org.openhab.ui.cometvisu/src/main/java/org/openhab/ui/cometvisu/internal/backend/rest/LoginResource.java+0 −92 removed@@ -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(); - } -}
bundles/org.openhab.ui.cometvisu/src/main/java/org/openhab/ui/cometvisu/internal/backend/rest/MoveResource.java+3 −0 modified@@ -15,6 +15,7 @@ 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.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 @@ @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
bundles/org.openhab.ui.cometvisu/src/main/java/org/openhab/ui/cometvisu/internal/backend/rest/ProxyResource.java+29 −1 modified@@ -21,6 +21,9 @@ 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,17 +81,19 @@ 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, @Parameter(description = "optional authorization token", content = @Content(schema = @Schema(implementation = String.class, defaultValue = ""))) @QueryParam("auth-type") @Nullable String authType, @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 Response proxy( } } 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);
bundles/org.openhab.ui.cometvisu/src/main/java/org/openhab/ui/cometvisu/internal/backend/rest/ReadResource.java+0 −254 removed@@ -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()); - } -}
bundles/org.openhab.ui.cometvisu/src/main/java/org/openhab/ui/cometvisu/internal/backend/rest/WriteResource.java+0 −116 removed@@ -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(); - } - } -}
bundles/org.openhab.ui.cometvisu/src/main/java/org/openhab/ui/cometvisu/internal/backend/sitemap/ConfigHelper.java+1 −1 modified@@ -767,7 +767,7 @@ public String getLabel(Widget widget) { 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));
bundles/org.openhab.ui.cometvisu/src/main/java/org/openhab/ui/cometvisu/internal/listeners/ItemRegistryEventListener.java+0 −77 removed@@ -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(); - } -}
bundles/org.openhab.ui.cometvisu/src/main/java/org/openhab/ui/cometvisu/internal/listeners/StateEventListener.java+0 −87 removed@@ -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); - } - } - } - } -}
bundles/org.openhab.ui.cometvisu/src/main/java/org/openhab/ui/cometvisu/internal/ManagerSettings.java+11 −9 modified@@ -109,16 +109,18 @@ private void refreshMounts() { 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); } } }
bundles/org.openhab.ui.cometvisu/src/main/java/org/openhab/ui/cometvisu/internal/servlet/CometVisuServlet.java+17 −2 modified@@ -20,7 +20,6 @@ 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.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 @@ * * @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 final void init(@Nullable ServletConfig config) throws ServletException { @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 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se } } } + 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 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se } } - 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 @@ protected File getRequestedFile(HttpServletRequest req) throws UnsupportedEncodi 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
bundles/org.openhab.ui.cometvisu/src/main/java/org/openhab/ui/cometvisu/internal/StateBeanMessageBodyWriter.java+0 −89 removed@@ -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; - } -}
bundles/org.openhab.ui.cometvisu/src/main/java/org/openhab/ui/cometvisu/internal/util/FsUtil.java+1 −1 modified@@ -287,7 +287,7 @@ public FsEntry getEntry(MountedFile mfile, boolean recursive) { } 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) {
bundles/org.openhab.ui.cometvisu/src/main/java/org/openhab/ui/cometvisu/internal/util/MountedFile.java+4 −0 modified@@ -82,6 +82,10 @@ public boolean exists() { return toFile().exists(); } + public boolean canWrite() { + return toFile().canWrite(); + } + public boolean isDirectory() { return toFile().isDirectory(); }
bundles/org.openhab.ui.cometvisu/src/main/java/org/openhab/ui/cometvisu/internal/util/SseUtil.java+0 −71 removed@@ -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); - } -}
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4- github.com/advisories/GHSA-f729-58x4-gqgfghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-42469ghsaADVISORY
- github.com/openhab/openhab-webui/commit/630e8525835c698cf58856aa43782d92b18087f2ghsax_refsource_MISCWEB
- github.com/openhab/openhab-webui/security/advisories/GHSA-f729-58x4-gqgfghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.