Skip to content

Commit

Permalink
Implement protocol validation for different configurations
Browse files Browse the repository at this point in the history
  • Loading branch information
JLChnToZ committed Nov 18, 2023
1 parent 7594e7e commit 0063ac3
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 71 deletions.
157 changes: 101 additions & 56 deletions Packages/idv.jlchntoz.vvmw/Editor/Common/TrustedUrlUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,44 @@
using UdonSharp;
using UdonSharpEditor;

namespace JLChnToZ.VRC.VVMW {
public static class TrustedUrlUtils {
static readonly AsyncLazy<List<string>> getTrustedUrlsTask = UniTask.Lazy(GetTrustedUrlsLazy);
static readonly Dictionary<string, bool> trustedDomains = new Dictionary<string, bool>();
static readonly Dictionary<string, string> messageCache = new Dictionary<string, string>();
namespace JLChnToZ.VRC.VVMW.Editors {
public enum TrustedUrlTypes {
UnityVideo,
AVProDesktop,
AVProAndroid,
ImageUrl,
StringUrl,
}

public sealed class TrustedUrlUtils {
static readonly Dictionary<TrustedUrlTypes, TrustedUrlUtils> instances = new Dictionary<TrustedUrlTypes, TrustedUrlUtils>();
static readonly AsyncLazy getTrustedUrlsTask = UniTask.Lazy(GetTrustedUrlsLazy);
static GUIContent tempContent, warningContent;
static List<string> trustedUrls;

public static UniTask<List<string>> TrustedUrls => getTrustedUrlsTask.Task;
readonly Dictionary<string, bool> trustedDomains = new Dictionary<string, bool>();
readonly Dictionary<string, string> messageCache = new Dictionary<string, string>();
readonly HashSet<string> supportedProtocols;
List<string> trustedUrls;

static TrustedUrlUtils() {
var stringComparer = StringComparer.OrdinalIgnoreCase;
var supportedProtocolsCurl = new HashSet<string>(new [] {
"http", "https",
}, stringComparer);
// https://www.renderheads.com/content/docs/AVProVideo/articles/supportedmedia.html
// https://learn.microsoft.com/en-us/windows/win32/medfound/supported-protocols
var supportedProtocolsMF = new HashSet<string>(new [] {
"http", "https", "rtsp", "rtspt", "rtspu", "rtmp", "rtmps",
}, stringComparer);
// https://exoplayer.dev/supported-formats.html
var supportedProtocolsExo = new HashSet<string>(new [] {
"http", "https", "rtsp", "rtmp",
}, stringComparer);
instances[TrustedUrlTypes.UnityVideo] = new TrustedUrlUtils(supportedProtocolsCurl);
instances[TrustedUrlTypes.AVProDesktop] = new TrustedUrlUtils(supportedProtocolsMF);
instances[TrustedUrlTypes.AVProAndroid] = new TrustedUrlUtils(supportedProtocolsExo);
instances[TrustedUrlTypes.ImageUrl] = new TrustedUrlUtils(supportedProtocolsCurl);
instances[TrustedUrlTypes.StringUrl] = new TrustedUrlUtils(supportedProtocolsCurl);
}

static GUIContent GetContent(string label, string tooltip = null) {
if (tempContent == null) tempContent = new GUIContent();
Expand All @@ -34,32 +63,35 @@ static GUIContent GetWarningContent(string tooltip) {
return warningContent;
}

static async UniTask<List<string>> GetTrustedUrlsLazy() {
if (trustedUrls == null) {
var vrcsdkConfig = ConfigManager.RemoteConfig;
if (!vrcsdkConfig.IsInitialized()) {
Debug.Log("[VVMW] VRCSDK config is not initialized, initializing...");
var initState = new UniTaskCompletionSource();
vrcsdkConfig.Init(
() => initState.TrySetResult(),
() => initState.TrySetException(new Exception("Failed to initialize VRCSDK config."))
);
await initState.Task;
}
if (!vrcsdkConfig.HasKey("urlList")) {
Debug.LogWarning("[VVMW] Failed to fetch trusted url list.");
return null;
}
trustedUrls = vrcsdkConfig.GetList("urlList");
static async UniTask GetTrustedUrlsLazy() {
var vrcsdkConfig = ConfigManager.RemoteConfig;
if (!vrcsdkConfig.IsInitialized()) {
Debug.Log("[VVMW] VRCSDK config is not initialized, initializing...");
var initState = new UniTaskCompletionSource();
vrcsdkConfig.Init(
() => initState.TrySetResult(),
() => initState.TrySetException(new Exception("Failed to initialize VRCSDK config."))
);
await initState.Task;
}
return trustedUrls;
if (vrcsdkConfig.HasKey("urlList")) {
var trustedUrls = vrcsdkConfig.GetList("urlList");
instances[TrustedUrlTypes.UnityVideo].trustedUrls = trustedUrls;
instances[TrustedUrlTypes.AVProDesktop].trustedUrls = trustedUrls;
instances[TrustedUrlTypes.AVProAndroid].trustedUrls = trustedUrls;
}
if (vrcsdkConfig.HasKey("imageHostUrlList"))
instances[TrustedUrlTypes.ImageUrl].trustedUrls = vrcsdkConfig.GetList("imageHostUrlList");
if (vrcsdkConfig.HasKey("stringHostUrlList"))
instances[TrustedUrlTypes.StringUrl].trustedUrls = vrcsdkConfig.GetList("stringHostUrlList");
}

public static void CopyTrustedUrlsToStringArray(SerializedProperty stringArray) =>
CopyTrustedUrlsToStringArrayAsync(stringArray, true).Forget();
public static void CopyTrustedUrlsToStringArray(SerializedProperty stringArray, TrustedUrlTypes urlType) =>
CopyTrustedUrlsToStringArrayAsync(stringArray, urlType, true).Forget();

static async UniTask CopyTrustedUrlsToStringArrayAsync(SerializedProperty stringArray, bool applyChanges = true) {
var urlList = await TrustedUrls;
static async UniTask CopyTrustedUrlsToStringArrayAsync(SerializedProperty stringArray, TrustedUrlTypes urlType, bool applyChanges = true) {
await getTrustedUrlsTask.Task;
var urlList = instances[urlType].trustedUrls;
stringArray.arraySize = urlList.Count;
for (int i = 0; i < urlList.Count; i++) {
var url = urlList[i];
Expand All @@ -75,28 +107,57 @@ static async UniTask CopyTrustedUrlsToStringArrayAsync(SerializedProperty string
UdonSharpEditorUtility.CopyProxyToUdon(usharp);
}

public static void DrawUrlField(SerializedProperty urlProperty, params GUILayoutOption[] options) {
public static void DrawUrlField(SerializedProperty urlProperty, TrustedUrlTypes urlType, params GUILayoutOption[] options) {
var contentRect = EditorGUILayout.GetControlRect(true, EditorGUIUtility.singleLineHeight, options);
DrawUrlField(urlProperty, contentRect);
DrawUrlField(urlProperty, urlType, contentRect);
}

public static void DrawUrlField(SerializedProperty urlProperty, Rect rect) {
public static void DrawUrlField(SerializedProperty urlProperty, TrustedUrlTypes urlTypes, Rect rect) {
var content = GetContent(urlProperty.displayName, urlProperty.tooltip);
if (urlProperty.propertyType == SerializedPropertyType.Generic)
if (urlProperty.propertyType == SerializedPropertyType.Generic) // VRCUrl
urlProperty = urlProperty.FindPropertyRelative("url");
var url = urlProperty.stringValue;
using (new EditorGUI.PropertyScope(rect, content, urlProperty))
urlProperty.stringValue = DrawUrlField(url, rect, content);
urlProperty.stringValue = DrawUrlField(url, urlTypes, rect, content);
}

public static string DrawUrlField(string url, Rect rect, string propertyLabel = null, string propertyTooltip = null) =>
DrawUrlField(url, rect, GetContent(propertyLabel, propertyTooltip));
public static string DrawUrlField(string url, TrustedUrlTypes urlType, Rect rect, string propertyLabel = null, string propertyTooltip = null) =>
DrawUrlField(url, urlType, rect, GetContent(propertyLabel, propertyTooltip));

public static string DrawUrlField(string url, Rect rect, GUIContent content) {
public static string DrawUrlField(string url, TrustedUrlTypes urlType, Rect rect, GUIContent content) {
var instnace = instances[urlType];
var invalidMessage = instnace.GetValidateMessage(url);
var rect2 = rect;
if (!string.IsNullOrEmpty(invalidMessage)) {
var warnContent = GetWarningContent(invalidMessage);
var labelStyle = EditorStyles.miniLabel;
var warnSize = labelStyle.CalcSize(warnContent);
var warnRect = new Rect(rect2.xMax - warnSize.x, rect2.y, warnSize.x, rect2.height);
rect2.width -= warnSize.x;
GUI.Label(warnRect, warnContent, labelStyle);
}
using (var changed = new EditorGUI.ChangeCheckScope()) {
var newUrl = EditorGUI.TextField(rect2, content, url);
if (changed.changed) {
instnace.messageCache.Remove(url);
url = newUrl;
}
}
return url;
}

TrustedUrlUtils(HashSet<string> supportedProtocols) {
this.supportedProtocols = supportedProtocols;
}

string GetValidateMessage(string url) {
if (!messageCache.TryGetValue(url, out var invalidMessage)) {
invalidMessage = "";
if (Uri.TryCreate(url, UriKind.Absolute, out var uri)) {
if (trustedUrls == null) TrustedUrls.Forget(); // Force to fetch trusted urls.
if (!supportedProtocols.Contains(uri.Scheme))
invalidMessage = $"{uri.Scheme} is not a supported protocol.";
else if (trustedUrls == null)
getTrustedUrlsTask.Task.Forget(); // Force to fetch trusted urls.
else { // Check domains.
var domainName = uri.Host;
if (!trustedDomains.TryGetValue(domainName, out var trusted)) {
Expand All @@ -119,23 +180,7 @@ public static string DrawUrlField(string url, Rect rect, GUIContent content) {
invalidMessage = "This URL is invalid.";
messageCache[url] = invalidMessage;
}
var rect2 = rect;
if (!string.IsNullOrEmpty(invalidMessage)) {
var warnContent = GetWarningContent(invalidMessage);
var labelStyle = EditorStyles.miniLabel;
var warnSize = labelStyle.CalcSize(warnContent);
var warnRect = new Rect(rect2.xMax - warnSize.x, rect2.y, warnSize.x, rect2.height);
rect2.width -= warnSize.x;
GUI.Label(warnRect, warnContent, labelStyle);
}
using (var changed = new EditorGUI.ChangeCheckScope()) {
var newUrl = EditorGUI.TextField(rect2, content, url);
if (changed.changed) {
messageCache.Remove(url);
url = newUrl;
}
}
return url;
return invalidMessage;
}
}
}
29 changes: 16 additions & 13 deletions Packages/idv.jlchntoz.vvmw/Editor/VVMW/CoreEditor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using UnityEngine.SceneManagement;
using UnityEditor;
using UdonSharpEditor;
using VRC.Core;
using VRC.SDK3.Video.Components;
using VRC.SDK3.Video.Components.AVPro;

Expand Down Expand Up @@ -39,6 +38,7 @@ public class CoreEditor : VVMWEditorBase {
SerializedProperty avProPropertyNamesProperty;
ReorderableListUtils playerHandlersList, audioSourcesList, targetsList;
string[] playerNames;
bool[] playerTypes;
List<bool> screenTargetVisibilityState;
bool showTrustUrlList;

Expand Down Expand Up @@ -77,35 +77,38 @@ protected override void OnEnable() {
screenTargetVisibilityState = new List<bool>();
for (int i = 0, count = screenTargetsProperty.arraySize; i < count; i++)
screenTargetVisibilityState.Add(false);
TrustedUrlUtils.CopyTrustedUrlsToStringArray(trustedUrlDomainsProperty);
TrustedUrlUtils.CopyTrustedUrlsToStringArray(trustedUrlDomainsProperty, TrustedUrlTypes.AVProDesktop);
}

public override void OnInspectorGUI() {
base.OnInspectorGUI();
if (UdonSharpGUI.DrawDefaultUdonSharpBehaviourHeader(target, false, false)) return;
serializedObject.Update();
TrustedUrlUtils.DrawUrlField(defaultUrlProperty);
int autoPlayPlayerType = autoPlayPlayerTypeProperty.intValue - 1;
bool isAvPro = playerTypes != null && autoPlayPlayerType >= 0 && autoPlayPlayerType < playerTypes.Length && playerTypes[autoPlayPlayerType];
TrustedUrlUtils.DrawUrlField(defaultUrlProperty, isAvPro ? TrustedUrlTypes.AVProDesktop : TrustedUrlTypes.UnityVideo);
if (!string.IsNullOrEmpty(defaultUrlProperty.FindPropertyRelative("url").stringValue)) {
TrustedUrlUtils.DrawUrlField(defaultQuestUrlProperty);
TrustedUrlUtils.DrawUrlField(defaultQuestUrlProperty, isAvPro ? TrustedUrlTypes.AVProAndroid : TrustedUrlTypes.UnityVideo);
if (playerNames == null || playerNames.Length != playerHandlersProperty.arraySize)
playerNames = new string[playerHandlersProperty.arraySize];
if (playerTypes == null || playerTypes.Length != playerHandlersProperty.arraySize)
playerTypes = new bool[playerHandlersProperty.arraySize];
for (int i = 0; i < playerNames.Length; i++) {
var playerHandler = playerHandlersProperty.GetArrayElementAtIndex(i).objectReferenceValue as VideoPlayerHandler;
if (playerHandler == null)
playerNames[i] = "null";
else if (string.IsNullOrEmpty(playerHandler.playerName))
playerNames[i] = playerHandler.name;
else
playerNames[i] = playerHandler.playerName;
else {
playerNames[i] = string.IsNullOrEmpty(playerHandler.playerName) ? playerHandler.name : playerHandler.playerName;
playerTypes[i] = playerHandler.isAvPro;
}
}
int selectedIndex = autoPlayPlayerTypeProperty.intValue - 1;
var rect = EditorGUILayout.GetControlRect(true, EditorGUIUtility.singleLineHeight);
var content = GetTempContent(autoPlayPlayerTypeProperty);
using (new EditorGUI.PropertyScope(rect, content, autoPlayPlayerTypeProperty))
using (var changed = new EditorGUI.ChangeCheckScope()) {
rect = EditorGUI.PrefixLabel(rect, content);
selectedIndex = EditorGUI.Popup(rect, selectedIndex, playerNames);
if (changed.changed) autoPlayPlayerTypeProperty.intValue = selectedIndex + 1;
autoPlayPlayerType = EditorGUI.Popup(rect, autoPlayPlayerType, playerNames);
if (changed.changed) autoPlayPlayerTypeProperty.intValue = autoPlayPlayerType + 1;
}
}
EditorGUILayout.PropertyField(loopProperty);
Expand Down Expand Up @@ -143,7 +146,7 @@ public override void OnInspectorGUI() {
"The list of trusted URL domains from VRChat. This list is for display proper error message when the video URL is not trusted."
), true);
if (GUILayout.Button("Update from VRChat", GUILayout.ExpandWidth(false)))
TrustedUrlUtils.CopyTrustedUrlsToStringArray(trustedUrlDomainsProperty);
TrustedUrlUtils.CopyTrustedUrlsToStringArray(trustedUrlDomainsProperty, TrustedUrlTypes.AVProDesktop);
}
if (showTrustUrlList)
using (new EditorGUILayout.VerticalScope(GUI.skin.box))
Expand Down Expand Up @@ -513,7 +516,7 @@ static void AppendElement(SerializedProperty property, int value) {
static void UpdateTrustedUrlList() {
var cores = new List<Core>(SceneManager.GetActiveScene().IterateAllComponents<Core>());
using (var so = new SerializedObject(cores.ToArray()))
TrustedUrlUtils.CopyTrustedUrlsToStringArray(so.FindProperty("trustedUrlDomains"));
TrustedUrlUtils.CopyTrustedUrlsToStringArray(so.FindProperty("trustedUrlDomains"), TrustedUrlTypes.AVProDesktop);
}
}
}
10 changes: 8 additions & 2 deletions Packages/idv.jlchntoz.vvmw/Editor/VVMW/PlayListEditorWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class PlayListEditorWindow : EditorWindow {
FrontendHandler frontendHandler;
Core loadedCore;
string[] playerHandlerNames;
bool[] playerHandlerTypes;
int firstUnityPlayerIndex = -1, firstAvProPlayerIndex = -1;
[SerializeField] List<PlayList> playLists = new List<PlayList>();
ReorderableList playListView;
Expand Down Expand Up @@ -211,13 +212,14 @@ void DrawPlayListEntry(Rect rect, int index, bool isActive, bool isFocused) {
}
}
}
var isAvPro = playerHandlerTypes != null && entry.playerIndex >= 0 && entry.playerIndex < playerHandlerTypes.Length && playerHandlerTypes[entry.playerIndex];
var urlRect = rect;
urlRect.yMin = titleRect.yMax + EditorGUIUtility.standardVerticalSpacing;
urlRect.height = EditorGUIUtility.singleLineHeight;
using (var changed = new EditorGUI.ChangeCheckScope()) {
tempContent.text = "URL (PC)";
urlRect = EditorGUI.PrefixLabel(urlRect, tempContent);
var newUrl = TrustedUrlUtils.DrawUrlField(entry.url, urlRect, "");
var newUrl = TrustedUrlUtils.DrawUrlField(entry.url, isAvPro ? TrustedUrlTypes.AVProDesktop : TrustedUrlTypes.UnityVideo, urlRect, "");
if (changed.changed) {
entry.url = newUrl;
selectedPlayList.entries[index] = entry;
Expand All @@ -231,7 +233,7 @@ void DrawPlayListEntry(Rect rect, int index, bool isActive, bool isFocused) {
tempContent.text = "URL (Quest)";
urlQuestRect = EditorGUI.PrefixLabel(urlQuestRect, tempContent);
var newUrl = string.IsNullOrEmpty(entry.urlForQuest) ? entry.url : entry.urlForQuest;
newUrl = TrustedUrlUtils.DrawUrlField(newUrl, urlQuestRect, "");
newUrl = TrustedUrlUtils.DrawUrlField(newUrl, isAvPro ? TrustedUrlTypes.AVProAndroid : TrustedUrlTypes.UnityVideo, urlQuestRect, "");
if (changed.changed) {
entry.urlForQuest = newUrl == entry.url ? string.Empty : newUrl;
selectedPlayList.entries[index] = entry;
Expand Down Expand Up @@ -379,6 +381,8 @@ void UpdatePlayerHandlerInfos() {
var handlersCount = playerHandlersProperty.arraySize;
if (playerHandlerNames == null || playerHandlerNames.Length != handlersCount)
playerHandlerNames = new string[handlersCount];
if (playerHandlerTypes == null || playerHandlerTypes.Length != handlersCount)
playerHandlerTypes = new bool[handlersCount];
for (int i = 0; i < handlersCount; i++) {
var handler = playerHandlersProperty.GetArrayElementAtIndex(i).objectReferenceValue as VideoPlayerHandler;
if (handler == null) {
Expand All @@ -387,8 +391,10 @@ void UpdatePlayerHandlerInfos() {
}
playerHandlerNames[i] = string.IsNullOrEmpty(handler.playerName) ? handler.name : handler.playerName;
if (handler.isAvPro) {
playerHandlerTypes[i] = true;
if (firstAvProPlayerIndex < 0) firstAvProPlayerIndex = i;
} else {
playerHandlerTypes[i] = false;
if (firstUnityPlayerIndex < 0) firstUnityPlayerIndex = i;
}
}
Expand Down

0 comments on commit 0063ac3

Please sign in to comment.