openvidu/openvidu-server/src/main/java/io/openvidu/server/config/OpenviduConfig.java

1067 lines
30 KiB
Java

/*
* (C) Copyright 2017-2020 OpenVidu (https://openvidu.io)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.openvidu.server.config;
import java.io.File;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.UnknownHostException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.annotation.PostConstruct;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.Header;
import org.apache.http.message.BasicHeader;
import org.kurento.jsonrpc.JsonUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
import io.openvidu.java.client.OpenViduRole;
import io.openvidu.java.client.VideoCodec;
import io.openvidu.server.OpenViduServer;
import io.openvidu.server.cdr.CDREventName;
import io.openvidu.server.config.Dotenv.DotenvFormatException;
import io.openvidu.server.core.MediaServer;
import io.openvidu.server.recording.RecordingNotification;
import io.openvidu.server.rest.RequestMappings;
@Component
public class OpenviduConfig {
public static class Error {
private String property;
private String value;
private String message;
public Error(String property, String value, String message) {
super();
this.property = property;
this.value = value;
this.message = message;
}
public String getProperty() {
return property;
}
public String getValue() {
return value;
}
public String getMessage() {
return message;
}
@Override
public String toString() {
return "Error [property=" + property + ", value=" + value + ", message=" + message + "]";
}
}
protected static final Logger log = LoggerFactory.getLogger(OpenviduConfig.class);
private static final boolean SHOW_PROPERTIES_AS_ENV_VARS = true;
private List<Error> configErrors = new ArrayList<>();
private Map<String, String> configProps = new HashMap<>();
private List<String> userConfigProps;
protected Map<String, ?> propertiesSource;
@Autowired
protected Environment env;
@Value("#{'${spring.profiles.active:}'.length() > 0 ? '${spring.profiles.active:}'.split(',') : \"default\"}")
protected String springProfile;
// Config properties
private boolean openviduCdr;
private String openviduCdrPath;
private boolean openviduRecording;
private boolean openViduRecordingDebug;
private boolean openviduRecordingPublicAccess;
private Integer openviduRecordingAutostopTimeout;
private String openviduRecordingPath;
private RecordingNotification openviduRecordingNotification;
private String openviduRecordingCustomLayout;
private boolean openviduRecordingComposedBasicauth;
private String openviduRecordingVersion;
private Integer openviduStreamsVideoMaxRecvBandwidth;
private Integer openviduStreamsVideoMinRecvBandwidth;
private Integer openviduStreamsVideoMaxSendBandwidth;
private Integer openviduStreamsVideoMinSendBandwidth;
private String coturnIp;
private String coturnRedisIp;
private boolean openviduWebhookEnabled;
private String openviduWebhookEndpoint;
private List<Header> webhookHeadersList;
private List<CDREventName> webhookEventsList;
private List<String> kmsUrisList;
private String domainOrPublicIp;
private String openviduPublicUrl;
private Integer httpsPort;
private String openviduSecret;
private String openviduRecordingComposedUrl;
private String coturnRedisDbname;
private String coturnRedisPassword;
private String coturnRedisConnectTimeout;
private String certificateType;
protected int openviduSessionsGarbageInterval;
protected int openviduSessionsGarbageThreshold;
private VideoCodec openviduForcedCodec;
private boolean openviduAllowTranscoding;
private String dotenvPath;
// Media Nodes private IPs and Public IPs
// If defined, they will be configured as public IPs of Kurento or Mediasoup
// Key: Private IP
// Value: Public IP
private Map<String, String> mediaNodesPublicIps = new HashMap<>();
// Derived properties
public static String finalUrl;
private boolean isTurnadminAvailable = false;
// Plain config properties getters
public String getCoturnDatabaseDbname() {
return this.coturnRedisDbname;
}
public String getCoturnDatabasePassword() {
return this.coturnRedisPassword;
}
public List<String> getKmsUris() {
return kmsUrisList;
}
public String getDomainOrPublicIp() {
return this.domainOrPublicIp;
}
public String getOpenViduPublicUrl() {
return this.openviduPublicUrl;
}
public Integer getHttpsPort() {
return this.httpsPort;
}
public String getOpenViduSecret() {
return this.openviduSecret;
}
public boolean isCdrEnabled() {
return this.openviduCdr;
}
public String getOpenviduCdrPath() {
return this.openviduCdrPath;
}
public boolean isRecordingModuleEnabled() {
return this.openviduRecording;
}
public boolean isOpenViduRecordingDebug() {
return openViduRecordingDebug;
}
public boolean isRecordingComposedExternal() {
return false;
}
public MediaServer getMediaServer() {
return MediaServer.kurento;
}
public String getOpenViduRecordingPath() {
return this.openviduRecordingPath;
}
public String getOpenViduRemoteRecordingPath() {
return getOpenViduRecordingPath();
}
public boolean getOpenViduRecordingPublicAccess() {
return this.openviduRecordingPublicAccess;
}
public String getOpenviduRecordingCustomLayout() {
return this.openviduRecordingCustomLayout;
}
public boolean isOpenviduRecordingComposedBasicauth() {
return this.openviduRecordingComposedBasicauth;
}
public String getOpenViduRecordingVersion() {
return this.openviduRecordingVersion;
}
public int getOpenviduRecordingAutostopTimeout() {
return this.openviduRecordingAutostopTimeout;
}
public int getVideoMaxRecvBandwidth() {
return this.openviduStreamsVideoMaxRecvBandwidth;
}
public int getVideoMinRecvBandwidth() {
return this.openviduStreamsVideoMinRecvBandwidth;
}
public int getVideoMaxSendBandwidth() {
return this.openviduStreamsVideoMaxSendBandwidth;
}
public int getVideoMinSendBandwidth() {
return this.openviduStreamsVideoMinSendBandwidth;
}
public String getCoturnIp() {
return this.coturnIp;
}
public RecordingNotification getOpenViduRecordingNotification() {
return this.openviduRecordingNotification;
}
public String getOpenViduRecordingComposedUrl() {
return this.openviduRecordingComposedUrl;
}
public boolean isWebhookEnabled() {
return this.openviduWebhookEnabled;
}
public String getOpenViduWebhookEndpoint() {
return this.openviduWebhookEndpoint;
}
public List<Header> getOpenViduWebhookHeaders() {
return webhookHeadersList;
}
public List<CDREventName> getOpenViduWebhookEvents() {
return webhookEventsList;
}
public int getSessionGarbageInterval() {
return openviduSessionsGarbageInterval;
}
public int getSessionGarbageThreshold() {
return openviduSessionsGarbageThreshold;
}
public VideoCodec getOpenviduForcedCodec() {
return openviduForcedCodec;
}
public boolean isOpenviduAllowingTranscoding() {
return openviduAllowTranscoding;
}
public String getDotenvPath() {
return dotenvPath;
}
// Derived properties methods
public String getSpringProfile() {
return springProfile;
}
public String getFinalUrl() {
return finalUrl;
}
public void setFinalUrl(String finalUrlParam) {
finalUrl = finalUrlParam.endsWith("/") ? (finalUrlParam) : (finalUrlParam + "/");
}
public boolean isTurnadminAvailable() {
return this.isTurnadminAvailable;
}
public void setTurnadminAvailable(boolean available) {
this.isTurnadminAvailable = available;
}
public boolean areMediaNodesPublicIpsDefined() {
return !this.mediaNodesPublicIps.isEmpty();
}
public Map<String, String> getMediaNodesPublicIpsMap() {
return this.mediaNodesPublicIps;
}
public OpenViduRole[] getRolesFromRecordingNotification() {
OpenViduRole[] roles;
switch (this.openviduRecordingNotification) {
case none:
roles = new OpenViduRole[0];
break;
case moderator:
roles = new OpenViduRole[] { OpenViduRole.MODERATOR };
break;
case publisher_moderator:
roles = new OpenViduRole[] { OpenViduRole.PUBLISHER, OpenViduRole.MODERATOR };
break;
case all:
roles = new OpenViduRole[] { OpenViduRole.SUBSCRIBER, OpenViduRole.PUBLISHER, OpenViduRole.MODERATOR };
break;
default:
roles = new OpenViduRole[] { OpenViduRole.PUBLISHER, OpenViduRole.MODERATOR };
}
return roles;
}
public boolean isOpenViduSecret(String secret) {
return secret.equals(this.getOpenViduSecret());
}
public String getCoturnDatabaseString() {
return "\"ip=" + this.coturnRedisIp + " dbname=" + this.coturnRedisDbname + " password="
+ this.coturnRedisPassword + " connect_timeout=" + this.coturnRedisConnectTimeout + "\"";
}
public boolean openviduRecordingCustomLayoutChanged(String path) {
return !"/opt/openvidu/custom-layout".equals(path);
}
public String getOpenViduFrontendDefaultPath() {
return RequestMappings.FRONTEND_CE;
}
// Properties management methods
public OpenviduConfig deriveWithAdditionalPropertiesSource(Map<String, ?> propertiesSource) {
OpenviduConfig config = newOpenviduConfig();
config.propertiesSource = propertiesSource;
config.env = env;
return config;
}
protected OpenviduConfig newOpenviduConfig() {
return new OpenviduConfig();
}
public List<Error> getConfigErrors() {
return configErrors;
}
public Map<String, String> getConfigProps() {
return configProps;
}
public List<String> getUserProperties() {
return userConfigProps;
}
private String getValue(String property) {
return this.getValue(property, true);
}
protected String getValue(String property, boolean storeInConfigProps) {
String value = null;
if (propertiesSource != null) {
Object valueObj = propertiesSource.get(property);
if (valueObj != null) {
value = valueObj.toString();
}
}
if (value == null) {
value = env.getProperty(property);
}
if (storeInConfigProps) {
this.configProps.put(property, value);
}
return value;
}
public String getPropertyName(String propertyName) {
if (SHOW_PROPERTIES_AS_ENV_VARS) {
return propertyName.replace('.', '_').replace('-', '_').toUpperCase();
} else {
return propertyName;
}
}
protected void addError(String property, String msg) {
String value = null;
if (property != null) {
value = getValue(property);
}
this.configErrors.add(new Error(property, value, msg));
}
public void checkConfiguration(boolean loadDotenv) {
try {
this.checkConfigurationProperties(loadDotenv);
} catch (Exception e) {
log.error("Exception checking configuration", e);
addError(null, "Exception checking configuration." + e.getClass().getName() + ":" + e.getMessage());
}
userConfigProps = new ArrayList<>(configProps.keySet());
userConfigProps.removeAll(getNonUserProperties());
}
@PostConstruct
public void postConstruct() {
this.checkConfiguration(true);
}
protected List<String> getNonUserProperties() {
return Arrays.asList("server.port", "SERVER_PORT", "DOTENV_PATH", "COTURN_IP", "COTURN_REDIS_IP",
"COTURN_REDIS_DBNAME", "COTURN_REDIS_PASSWORD", "COTURN_REDIS_CONNECT_TIMEOUT");
}
// Properties
protected void checkConfigurationProperties(boolean loadDotenv) {
if (loadDotenv) {
dotenvPath = getValue("DOTENV_PATH");
this.populatePropertySourceFromDotenv();
}
checkHttpsPort();
checkDomainOrPublicIp();
populateSpringServerPort();
coturnRedisDbname = getValue("COTURN_REDIS_DBNAME");
coturnRedisPassword = getValue("COTURN_REDIS_PASSWORD");
coturnRedisConnectTimeout = getValue("COTURN_REDIS_CONNECT_TIMEOUT");
openviduSecret = asNonEmptyAlphanumericString("OPENVIDU_SECRET",
"Cannot be empty and must contain only alphanumeric characters [a-zA-Z0-9], hypens (\"-\") and underscores (\"_\")");
openviduCdr = asBoolean("OPENVIDU_CDR");
openviduCdrPath = openviduCdr ? asWritableFileSystemPath("OPENVIDU_CDR_PATH")
: asFileSystemPath("OPENVIDU_CDR_PATH");
openviduRecording = asBoolean("OPENVIDU_RECORDING");
openViduRecordingDebug = asBoolean("OPENVIDU_RECORDING_DEBUG");
openviduRecordingPath = openviduRecording ? asWritableFileSystemPath("OPENVIDU_RECORDING_PATH")
: asFileSystemPath("OPENVIDU_RECORDING_PATH");
openviduRecordingPublicAccess = asBoolean("OPENVIDU_RECORDING_PUBLIC_ACCESS");
openviduRecordingAutostopTimeout = asNonNegativeInteger("OPENVIDU_RECORDING_AUTOSTOP_TIMEOUT");
openviduRecordingCustomLayout = asFileSystemPath("OPENVIDU_RECORDING_CUSTOM_LAYOUT");
openviduRecordingComposedBasicauth = asBoolean("OPENVIDU_RECORDING_COMPOSED_BASICAUTH");
openviduRecordingVersion = asNonEmptyString("OPENVIDU_RECORDING_VERSION");
openviduRecordingComposedUrl = asOptionalURL("OPENVIDU_RECORDING_COMPOSED_URL");
checkOpenviduRecordingNotification();
openviduStreamsVideoMaxRecvBandwidth = asNonNegativeInteger("OPENVIDU_STREAMS_VIDEO_MAX_RECV_BANDWIDTH");
openviduStreamsVideoMinRecvBandwidth = asNonNegativeInteger("OPENVIDU_STREAMS_VIDEO_MIN_RECV_BANDWIDTH");
openviduStreamsVideoMaxSendBandwidth = asNonNegativeInteger("OPENVIDU_STREAMS_VIDEO_MAX_SEND_BANDWIDTH");
openviduStreamsVideoMinSendBandwidth = asNonNegativeInteger("OPENVIDU_STREAMS_VIDEO_MIN_SEND_BANDWIDTH");
openviduSessionsGarbageInterval = asNonNegativeInteger("OPENVIDU_SESSIONS_GARBAGE_INTERVAL");
openviduSessionsGarbageThreshold = asNonNegativeInteger("OPENVIDU_SESSIONS_GARBAGE_THRESHOLD");
openviduForcedCodec = asEnumValue("OPENVIDU_STREAMS_FORCED_VIDEO_CODEC", VideoCodec.class);
openviduAllowTranscoding = asBoolean("OPENVIDU_STREAMS_ALLOW_TRANSCODING");
// Load Public IPS
mediaNodesPublicIps = loadMediaNodePublicIps("MEDIA_NODES_PUBLIC_IPS");
kmsUrisList = checkKmsUris();
checkCoturnIp();
coturnRedisIp = asOptionalInetAddress("COTURN_REDIS_IP");
checkWebhook();
checkCertificateType();
}
private void checkCertificateType() {
String property = "CERTIFICATE_TYPE";
certificateType = asNonEmptyString(property);
if (certificateType != null && !certificateType.isEmpty()) {
List<String> validValues = Arrays.asList("selfsigned", "owncert", "letsencrypt");
if (!validValues.contains(certificateType)) {
addError(property, "Invalid value '" + certificateType + "'. Valid values are " + validValues);
}
}
}
private void checkCoturnIp() {
String property = "COTURN_IP";
coturnIp = asOptionalIPv4OrIPv6(property);
if (coturnIp == null || this.coturnIp.isEmpty()) {
try {
this.coturnIp = new URL(this.getFinalUrl()).getHost();
} catch (MalformedURLException e) {
log.error("Can't get Domain name from OpenVidu public Url: " + e.getMessage());
}
}
}
private void checkWebhook() {
openviduWebhookEnabled = asBoolean("OPENVIDU_WEBHOOK");
openviduWebhookEndpoint = asOptionalURL("OPENVIDU_WEBHOOK_ENDPOINT");
webhookHeadersList = checkWebhookHeaders();
webhookEventsList = getWebhookEvents();
if (openviduWebhookEnabled && (openviduWebhookEndpoint == null || openviduWebhookEndpoint.isEmpty())) {
addError("OPENVIDU_WEBHOOK_ENDPOINT", "With OPENVIDU_WEBHOOK=true, this property cannot be empty");
}
}
private void checkOpenviduRecordingNotification() {
String recordingNotif = asNonEmptyString("OPENVIDU_RECORDING_NOTIFICATION");
try {
openviduRecordingNotification = RecordingNotification.valueOf(recordingNotif);
} catch (IllegalArgumentException e) {
addError("OPENVIDU_RECORDING_NOTIFICATION",
"Must be one of the values " + Arrays.asList(RecordingNotification.values()));
}
}
private void checkDomainOrPublicIp() {
final String property = "DOMAIN_OR_PUBLIC_IP";
String domain = asOptionalInetAddress(property);
// TODO: remove when possible deprecated OPENVIDU_DOMAIN_OR_PUBLIC_IP
if (domain == null || domain.isEmpty()) {
domain = asOptionalInetAddress("OPENVIDU_DOMAIN_OR_PUBLIC_IP");
this.configProps.put("DOMAIN_OR_PUBLIC_IP", domain);
this.configProps.remove("OPENVIDU_DOMAIN_OR_PUBLIC_IP");
}
if (domain != null && !domain.isEmpty()) {
this.domainOrPublicIp = domain;
this.openviduPublicUrl = "https://" + domain;
if (this.httpsPort != null && this.httpsPort != 443) {
this.openviduPublicUrl += (":" + this.httpsPort);
}
calculatePublicUrl();
} else {
addError(property, "Cannot be empty");
}
}
private void checkHttpsPort() {
String property = "HTTPS_PORT";
String httpsPort = getValue(property);
if (httpsPort == null) {
addError(property, "Cannot be undefined");
}
int httpsPortNumber = 0;
try {
httpsPortNumber = Integer.parseInt(httpsPort);
} catch (NumberFormatException e) {
addError(property, "Is not a valid port. Must be an integer. " + e.getMessage());
return;
}
if (httpsPortNumber > 0 && httpsPortNumber <= 65535) {
this.httpsPort = httpsPortNumber;
} else {
addError(property, "Is not a valid port. Valid port range exceeded with value " + httpsPortNumber);
return;
}
}
/**
* Will add to collection of configuration properties the property "SERVER_PORT"
* only if property "SERVER_PORT" or "server.port" was explicitly defined. This
* doesn't mean this property won't have a default value if not explicitly
* defined (8080 is the default value given by Spring)
*/
private void populateSpringServerPort() {
String springServerPort = getValue("server.port", false);
if (springServerPort == null) {
springServerPort = getValue("SERVER_PORT", false);
}
if (springServerPort != null) {
this.configProps.put("SERVER_PORT", springServerPort);
}
}
private void calculatePublicUrl() {
final String publicUrl = this.getOpenViduPublicUrl();
if (publicUrl.startsWith("https://")) {
OpenViduServer.wsUrl = publicUrl.replace("https://", "wss://");
} else if (publicUrl.startsWith("http://")) {
OpenViduServer.wsUrl = publicUrl.replace("http://", "wss://");
}
if (OpenViduServer.wsUrl.endsWith("/")) {
OpenViduServer.wsUrl = OpenViduServer.wsUrl.substring(0, OpenViduServer.wsUrl.length() - 1);
}
String finalUrl = OpenViduServer.wsUrl.replaceFirst("wss://", "https://").replaceFirst("ws://", "http://");
this.setFinalUrl(finalUrl);
OpenViduServer.httpUrl = this.getFinalUrl();
}
public List<String> checkKmsUris() {
String property = "KMS_URIS";
return asKmsUris(property, getValue(property));
}
public List<String> asKmsUris(String property, String kmsUris) {
if (kmsUris == null || kmsUris.isEmpty()) {
return Arrays.asList();
}
kmsUris = kmsUris.replaceAll("\\s", ""); // Remove all white spaces
kmsUris = kmsUris.replaceAll("\\\\", ""); // Remove previous escapes
kmsUris = kmsUris.replaceAll("\"", ""); // Remove previous double quotes
kmsUris = kmsUris.replaceFirst("^\\[", "[\\\""); // Escape first char
kmsUris = kmsUris.replaceFirst("\\]$", "\\\"]"); // Escape last char
kmsUris = kmsUris.replaceAll(",", "\\\",\\\""); // Escape middle uris
List<String> kmsUrisArray = asJsonStringsArray(property);
for (String uri : kmsUrisArray) {
try {
this.checkWebsocketUri(uri);
} catch (Exception e) {
addError(property, uri + " is not a valid WebSocket URL");
}
}
return kmsUrisArray;
}
private List<Header> checkWebhookHeaders() {
String property = "OPENVIDU_WEBHOOK_HEADERS";
List<String> headers = asJsonStringsArray(property);
List<Header> headerList = new ArrayList<>();
for (String header : headers) {
String[] headerSplit = header.split(": ", 2);
if (headerSplit.length != 2) {
addError(property, "HTTP header '" + header
+ "' syntax is not correct. Must be 'HEADER_NAME: HEADER_VALUE'. For example: 'Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l'");
continue;
}
String headerName = headerSplit[0];
String headerValue = headerSplit[1];
if (headerName.isEmpty()) {
addError(property, "HTTP header '" + header + "' syntax is not correct. Header name cannot be empty");
}
if (headerValue.isEmpty()) {
addError(property, "HTTP header '" + header + "' syntax is not correct. Header value cannot be empty");
}
headerList.add(new BasicHeader(headerName, headerValue));
}
return headerList;
}
private List<CDREventName> getWebhookEvents() {
String property = "OPENVIDU_WEBHOOK_EVENTS";
List<String> events = asJsonStringsArray(property);
List<CDREventName> eventList = new ArrayList<>();
for (String event : events) {
try {
eventList.add(CDREventName.valueOf(event));
} catch (IllegalArgumentException e) {
addError(property, "Event '" + event + "' does not exist");
}
}
return eventList;
}
// -------------------------------------------------------
// Format Checkers
// -------------------------------------------------------
protected String asOptionalURL(String property) {
String optionalUrl = getValue(property);
try {
if (!optionalUrl.isEmpty()) {
checkUrl(optionalUrl);
}
return optionalUrl;
} catch (Exception e) {
addError(property, "Is not a valid URL. " + e.getMessage());
return null;
}
}
protected String asNonEmptyString(String property) {
String stringValue = getValue(property);
if (stringValue != null && !stringValue.isEmpty()) {
return stringValue;
} else {
addError(property, "Cannot be empty.");
return null;
}
}
protected String asNonEmptyAlphanumericString(String property, String errorMessage) {
final String REGEX = "^[a-zA-Z0-9_-]+$";
String stringValue = getValue(property);
if (stringValue != null && !stringValue.isEmpty() && stringValue.matches(REGEX)) {
return stringValue;
} else {
addError(property, errorMessage);
return null;
}
}
protected String asOptionalString(String property) {
return getValue(property);
}
protected boolean asBoolean(String property) {
String value = getValue(property);
if (value == null) {
addError(property, "Cannot be empty");
return false;
}
if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("false")) {
return Boolean.parseBoolean(value);
} else {
addError(property, "Is not a boolean (true or false)");
return false;
}
}
protected Integer asNonNegativeInteger(String property) {
try {
Integer integerValue = Integer.parseInt(getValue(property));
if (integerValue < 0) {
addError(property, "Is not a non negative integer");
}
return integerValue;
} catch (NumberFormatException e) {
addError(property, "Is not a non negative integer");
return 0;
}
}
/*
* This method checks all types of Internet addresses (IPv4, IPv6 and Domains)
*/
protected String asOptionalInetAddress(String property) {
String inetAddress = getValue(property);
if (inetAddress != null && !inetAddress.isEmpty()) {
try {
Inet6Address.getByName(inetAddress).getHostAddress();
} catch (UnknownHostException e) {
addError(property, "Is not a valid Internet Address (IP or Domain Name): " + e.getMessage());
}
}
return inetAddress;
}
protected String asOptionalIPv4OrIPv6(String property) {
String ip = getValue(property);
if (ip != null && !ip.isEmpty()) {
boolean isIP;
try {
final InetAddress inet = InetAddress.getByName(ip);
isIP = inet instanceof Inet4Address || inet instanceof Inet6Address;
if (isIP) {
ip = inet.getHostAddress();
}
} catch (final UnknownHostException e) {
isIP = false;
}
if (!isIP) {
addError(property, "Is not a valid IP Address (IPv4 or IPv6)");
}
}
return ip;
}
protected String asFileSystemPath(String property) {
try {
String stringPath = this.asNonEmptyString(property);
Paths.get(stringPath);
File f = new File(stringPath);
f.getCanonicalPath();
f.toURI().toString();
stringPath = stringPath.endsWith("/") ? stringPath : (stringPath + "/");
return stringPath;
} catch (Exception e) {
addError(property, "Is not a valid file system path. " + e.getMessage());
return null;
}
}
protected String asWritableFileSystemPath(String property) {
try {
String stringPath = this.asNonEmptyString(property);
Paths.get(stringPath);
File f = new File(stringPath);
f.getCanonicalPath();
f.toURI().toString();
if (!f.exists()) {
if (!f.mkdirs()) {
throw new Exception(
"The path does not exist and OpenVidu Server does not have enough permissions to create it");
}
}
if (!f.canWrite()) {
throw new Exception(
"OpenVidu Server does not have permissions to write on path " + f.getCanonicalPath());
}
stringPath = stringPath.endsWith("/") ? stringPath : (stringPath + "/");
return stringPath;
} catch (Exception e) {
addError(property, "Is not a valid writable file system path. " + e.getMessage());
return null;
}
}
protected List<String> asJsonStringsArray(String property) {
try {
Gson gson = new Gson();
JsonArray jsonArray = gson.fromJson(getValue(property), JsonArray.class);
List<String> list = JsonUtils.toStringList(jsonArray);
if (list.size() == 1 && list.get(0).isEmpty()) {
list = new ArrayList<>();
}
return list;
} catch (JsonSyntaxException e) {
addError(property, "Is not a valid strings array in JSON format. " + e.getMessage());
return Arrays.asList();
}
}
protected <E extends Enum<E>> E asEnumValue(String property, Class<E> enumType) {
String value = this.getValue(property);
try {
return Enum.valueOf(enumType, value);
} catch (IllegalArgumentException e) {
addError(property, "Must be one of " + Arrays.asList(enumType.getEnumConstants()));
return null;
}
}
protected Map<String, String> asOptionalStringMap(String property) {
Map<String, String> map = new HashMap<>();
String str = getValue(property);
if (str != null && !str.isEmpty()) {
try {
Gson gson = new Gson();
JsonObject jsonObject = gson.fromJson(str, JsonObject.class);
for (Entry<String, JsonElement> entry : jsonObject.entrySet()) {
map.put(entry.getKey(), entry.getValue().getAsString());
}
return map;
} catch (JsonSyntaxException e) {
addError(property, "Is not a valid map of strings. " + e.getMessage());
return map;
}
}
return map;
}
public URI checkWebsocketUri(String uri) throws Exception {
try {
if (!StringUtils.startsWithAny(uri, "ws://", "wss://")) {
throw new Exception("WebSocket protocol not found");
}
String parsedUri = uri.replaceAll("^ws://", "http://").replaceAll("^wss://", "https://");
return new URL(parsedUri).toURI();
} catch (Exception e) {
throw new RuntimeException(
"URI '" + uri + "' has not a valid WebSocket endpoint format: " + e.getMessage());
}
}
protected void checkUrl(String url) throws Exception {
try {
new URL(url).toURI();
} catch (MalformedURLException | URISyntaxException e) {
throw new Exception("String '" + url + "' has not a valid URL format: " + e.getMessage());
}
}
protected void populatePropertySourceFromDotenv() {
File dotenvFile = this.getDotenvFile();
if (dotenvFile != null) {
if (dotenvFile.canRead()) {
Dotenv dotenv = new Dotenv();
try {
dotenv.read(dotenvFile.toPath());
this.propertiesSource = dotenv.getAll();
log.info("Configuration properties read from file {}", dotenvFile.getAbsolutePath());
} catch (IOException | DotenvFormatException e) {
log.error("Error reading properties from .env file: {}", e.getMessage());
addError(null, e.getMessage());
}
} else {
log.error("OpenVidu does not have read permissions over .env file at {}", this.getDotenvPath());
}
}
}
public Path getDotenvFilePathFromDotenvPath(String dotenvPathProperty) {
if (dotenvPathProperty.endsWith(".env")) {
// Is file
return Paths.get(dotenvPathProperty);
} else if (dotenvPathProperty.endsWith("/")) {
// Is folder
return Paths.get(dotenvPathProperty + ".env");
} else {
// Is a folder not ending in "/"
return Paths.get(dotenvPathProperty + "/.env");
}
}
public File getDotenvFile() {
if (getDotenvPath() != null && !getDotenvPath().isEmpty()) {
Path path = getDotenvFilePathFromDotenvPath(getDotenvPath());
String normalizePath = FilenameUtils.normalize(path.toAbsolutePath().toString());
File file = new File(normalizePath);
if (file.exists()) {
return file;
} else {
log.error(".env file not found at {}", file.getAbsolutePath().toString());
}
} else {
log.warn("DOTENV_PATH configuration property is not defined");
}
return null;
}
private Map<String, String> loadMediaNodePublicIps(String propertyName) {
String mediaNodesPublicIpsRaw = this.asOptionalString(propertyName);
final Map<String, String> mediaNodesPublicIps = new HashMap<>();
if (mediaNodesPublicIpsRaw == null || mediaNodesPublicIpsRaw.isEmpty()) {
return mediaNodesPublicIps;
}
List<String> mediaNodesPublicIpsList = asJsonStringsArray(propertyName);
for(String ipPairStr: mediaNodesPublicIpsList) {
String[] ipPair = ipPairStr.trim().split(":");
if (ipPair.length != 2) {
addError(propertyName, "Not valid ip pair in " + propertyName + ": " + ipPairStr);
break;
}
String privateIp = ipPair[0];
String publicIp = ipPair[1];
isValidIp(propertyName, privateIp);
isValidIp(propertyName, publicIp);
mediaNodesPublicIps.put(privateIp, publicIp);
}
return mediaNodesPublicIps;
}
private void isValidIp(String propertyName, String ip) {
if (ip != null && !ip.isEmpty()) {
boolean isIP;
try {
final InetAddress inet = InetAddress.getByName(ip);
isIP = inet instanceof Inet4Address || inet instanceof Inet6Address;
if (isIP) {
ip = inet.getHostAddress();
}
} catch (final UnknownHostException e) {
isIP = false;
}
if (!isIP) {
addError(propertyName, "Is not a valid IP Address (IPv4 or IPv6): " + ip);
}
}
}
}