feat: Add /getall, logging to .NET, fix manager not getting traveling world (#554)

* fix: Fix world-db not getting current world properly when traveling

* fix: Stop redundant and exception-prone funcResult definition

* Fix: fetching current location

* refactor: Move constructing responseData to its own functions

* Fix: ignore own string requests

* feat: Add NLog dependency, and add some logging to .NET, mostly worlddb

* fix: I missed a semicolon

* refactor: Add more debug logging, change log format, archive less

* feat: Add /getall endpoint

---------

Co-authored-by: Natsumi <cmcooper123@hotmail.com>
This commit is contained in:
Teacup
2023-06-01 20:59:31 -04:00
committed by GitHub
parent 033e2691ae
commit 7d6ca28f86
7 changed files with 276 additions and 103 deletions

View File

@@ -16,6 +16,7 @@ namespace VRCX
public static WorldDBManager Instance;
private readonly HttpListener listener;
private readonly WorldDatabase worldDB;
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private string currentWorldId = null;
private string lastError = null;
@@ -34,49 +35,77 @@ namespace VRCX
{
listener.Start();
logger.Info("Listening for requests on {0}", listener.Prefixes.First());
while (true)
{
var context = await listener.GetContextAsync();
var request = context.Request;
var responseData = new WorldDataRequestResponse(false, null, null);
if (MainForm.Instance?.Browser == null || MainForm.Instance.Browser.IsLoading || !MainForm.Instance.Browser.CanExecuteJavascriptInMainFrame)
try
{
responseData.Error = "VRCX not yet initialized. Try again in a moment.";
responseData.StatusCode = 503;
SendJsonResponse(context.Response, responseData);
continue;
};
if (MainForm.Instance?.Browser == null || MainForm.Instance.Browser.IsLoading || !MainForm.Instance.Browser.CanExecuteJavascriptInMainFrame)
{
logger.Warn("Received a request to {0} while VRCX is still initializing the browser window. Responding with error 503.", request.Url);
switch (request.Url.LocalPath)
responseData.Error = "VRCX not yet initialized. Try again in a moment.";
responseData.StatusCode = 503;
SendJsonResponse(context.Response, responseData);
continue;
};
logger.Debug("Received a request to '{0}'", request.Url);
// TODO: Maybe an endpoint for getting a group of arbitrary keys by a group 'name'? eg; /getgroup?name=testgroup1 would return all keys with the column group set to 'testgroup1'
switch (request.Url.LocalPath)
{
case "/vrcx/data/init":
responseData = await HandleInitRequest(context);
SendJsonResponse(context.Response, responseData);
break;
case "/vrcx/data/get":
responseData = await HandleDataRequest(context);
SendJsonResponse(context.Response, responseData);
break;
case "/vrcx/data/getall":
responseData = await HandleAllDataRequest(context);
SendJsonResponse(context.Response, responseData);
break;
case "/vrcx/data/lasterror":
responseData.OK = lastError == null;
responseData.StatusCode = 200;
responseData.Data = lastError;
lastError = null;
SendJsonResponse(context.Response, responseData);
break;
case "/vrcx/data/getbulk":
responseData = await HandleBulkDataRequest(context);
SendJsonResponse(context.Response, responseData);
break;
case "/vrcx/status":
// Send a blank 200 response to indicate that the server is running.
context.Response.StatusCode = 200;
context.Response.Close();
break;
default:
responseData.Error = "Invalid VRCX endpoint.";
responseData.StatusCode = 404;
SendJsonResponse(context.Response, responseData);
break;
}
if (context.Response.StatusCode != 200)
{
logger.Warn("Received a request to '{0}' that returned a non-successful response. Error: {1} - {2}", request.Url, responseData.StatusCode, responseData.Error);
}
}
catch (Exception ex)
{
case "/vrcx/data/init":
responseData = await HandleInitRequest(context);
SendJsonResponse(context.Response, responseData);
break;
case "/vrcx/data/get":
responseData = await HandleDataRequest(context);
SendJsonResponse(context.Response, responseData);
break;
case "/vrcx/data/lasterror":
responseData.OK = lastError == null;
responseData.Data = lastError;
lastError = null;
SendJsonResponse(context.Response, responseData);
break;
case "/vrcx/data/getbulk":
responseData = await HandleBulkDataRequest(context);
SendJsonResponse(context.Response, responseData);
break;
case "/vrcx/status":
context.Response.StatusCode = 200;
context.Response.Close();
break;
default:
responseData.Error = "Invalid VRCX endpoint.";
responseData.StatusCode = 404;
SendJsonResponse(context.Response, responseData);
break;
logger.Error(ex, $"Exception while processing the url '{request.Url}'.");
responseData.Error = $"VRCX has encountered an exception while processing the url '{request.Url}': {ex.Message}";
responseData.StatusCode = 500;
SendJsonResponse(context.Response, responseData);
}
}
@@ -90,7 +119,6 @@ namespace VRCX
private async Task<WorldDataRequestResponse> HandleInitRequest(HttpListenerContext context)
{
var request = context.Request;
var responseData = new WorldDataRequestResponse(false, null, null);
if (request.QueryString["debug"] == "true")
{
@@ -101,19 +129,15 @@ namespace VRCX
}
currentWorldId = "wrld_12345";
responseData.OK = true;
responseData.StatusCode = 200;
responseData.Data = "12345";
return responseData;
return ConstructSuccessResponse("12345");
}
string worldId = await GetCurrentWorldID();
if (String.IsNullOrEmpty(worldId))
{
responseData.Error = "Failed to get/verify current world ID.";
responseData.StatusCode = 500;
return responseData;
return ConstructErrorResponse(500, "Failed to get/verify current world ID.");
}
currentWorldId = worldId;
@@ -131,10 +155,8 @@ namespace VRCX
connectionKey = worldDB.GetWorldConnectionKey(currentWorldId);
}
responseData.OK = true;
responseData.StatusCode = 200;
responseData.Data = connectionKey;
return responseData;
logger.Info("Initialized connection to world ID '{0}' with connection key '{1}'.", currentWorldId, connectionKey);
return ConstructSuccessResponse(connectionKey);
}
/// <summary>
@@ -145,14 +167,11 @@ namespace VRCX
private async Task<WorldDataRequestResponse> HandleDataRequest(HttpListenerContext context)
{
var request = context.Request;
var responseData = new WorldDataRequestResponse(false, null, null);
var key = request.QueryString["key"];
if (key == null)
{
responseData.Error = "Missing key parameter.";
responseData.StatusCode = 400;
return responseData;
return ConstructErrorResponse(400, "Missing key parameter.");
}
var worldIdOverride = request.QueryString["world"];
@@ -163,20 +182,12 @@ namespace VRCX
if (world == null)
{
responseData.OK = false;
responseData.Error = $"World ID '{worldIdOverride}' not initialized in this user's database.";
responseData.StatusCode = 200;
responseData.Data = null;
return responseData;
return ConstructErrorResponse(200, $"World ID '{worldIdOverride}' not initialized in this user's database.");
}
if (!world.AllowExternalRead)
{
responseData.OK = false;
responseData.Error = $"World ID '{worldIdOverride}' does not allow external reads.";
responseData.StatusCode = 200;
responseData.Data = null;
return responseData;
return ConstructErrorResponse(200, $"World ID '{worldIdOverride}' does not allow external reads.");
}
}
@@ -187,18 +198,64 @@ namespace VRCX
if (worldIdOverride == null && (String.IsNullOrEmpty(currentWorldId) || worldId != currentWorldId))
{
responseData.Error = "World ID not initialized.";
responseData.StatusCode = 400;
return responseData;
return ConstructErrorResponse(400, "World ID not initialized.");
}
var value = worldDB.GetDataEntry(worldId, key);
responseData.OK = true;
responseData.StatusCode = 200;
responseData.Error = null;
responseData.Data = value?.Value;
return responseData;
logger.Debug("Serving a request for data with key '{0}' from world ID '{1}'.", key, worldId);
// This is intended to be null if the key doesn't exist.
return ConstructSuccessResponse(value?.Value);
}
/// <summary>
/// Handles an HTTP listener request for all data from the world database for a given world.
/// </summary>
/// <param name="context">The HTTP listener context object.</param>
/// <returns>A <see cref="WorldDataRequestResponse"/> object containing the response data.</returns>
private async Task<WorldDataRequestResponse> HandleAllDataRequest(HttpListenerContext context)
{
var request = context.Request;
var worldIdOverride = request.QueryString["world"];
if (worldIdOverride != null)
{
var world = worldDB.GetWorld(worldIdOverride);
if (world == null)
{
return ConstructErrorResponse(200, $"World ID '{worldIdOverride}' not initialized in this user's database.");
}
if (!world.AllowExternalRead)
{
return ConstructErrorResponse(200, $"World ID '{worldIdOverride}' does not allow external reads.");
}
}
if (currentWorldId == "wrld_12345" && worldIdOverride == null)
worldIdOverride = "wrld_12345";
var worldId = worldIdOverride ?? await GetCurrentWorldID();
if (worldIdOverride == null && (String.IsNullOrEmpty(currentWorldId) || worldId != currentWorldId))
{
return ConstructErrorResponse(400, "World ID not initialized.");
}
var entries = worldDB.GetAllDataEntries(worldId);
logger.Debug("Serving a request for all data from world ID '{0}'.", worldId);
var data = new Dictionary<string, string>();
foreach (var entry in entries)
{
data.Add(entry.Key, entry.Value);
}
// This is intended to be null if the key doesn't exist.
return ConstructSuccessResponse(JsonConvert.SerializeObject(data));
}
/// <summary>
@@ -209,14 +266,11 @@ namespace VRCX
private async Task<WorldDataRequestResponse> HandleBulkDataRequest(HttpListenerContext context)
{
var request = context.Request;
var responseData = new WorldDataRequestResponse(false, null, null);
var keys = request.QueryString["keys"];
if (keys == null)
{
responseData.Error = "Missing/invalid keys parameter.";
responseData.StatusCode = 400;
return responseData;
return ConstructErrorResponse(400, "Missing/invalid keys parameter.");
}
var keyArray = keys.Split(',');
@@ -225,9 +279,7 @@ namespace VRCX
if (String.IsNullOrEmpty(currentWorldId) || (worldId != currentWorldId && currentWorldId != "wrld_12345"))
{
responseData.Error = "World ID not initialized.";
responseData.StatusCode = 400;
return responseData;
return ConstructErrorResponse(400, "World ID not initialized.");
}
var values = worldDB.GetDataEntries(currentWorldId, keyArray).ToList();
@@ -249,11 +301,8 @@ namespace VRCX
data.Add(dataKey, dataValue);
}
responseData.OK = true;
responseData.StatusCode = 200;
responseData.Error = null;
responseData.Data = JsonConvert.SerializeObject(data);
return responseData;
logger.Debug("Serving a request for bulk data with keys '{0}' from world ID '{1}'.", keys, currentWorldId);
return ConstructSuccessResponse(JsonConvert.SerializeObject(data));
}
/// <summary>
@@ -274,7 +323,7 @@ namespace VRCX
/// <returns>The ID of the current world as a string, or null if it could not be retrieved.</returns>
private async Task<string> GetCurrentWorldID()
{
JavascriptResponse funcResult = await MainForm.Instance.Browser.EvaluateScriptAsync("$app.API.actuallyGetCurrentLocation();", TimeSpan.FromSeconds(5));
JavascriptResponse funcResult;
try
{
@@ -282,6 +331,7 @@ namespace VRCX
}
catch (Exception ex)
{
logger.Error(ex, "Failed to evaluate actuallyGetCurrentLocation JS function to get current world ID.");
return null;
}
@@ -292,12 +342,36 @@ namespace VRCX
// implement
// wait what was i going to do here again
// seriously i forgot, hope it wasn't important
logger.Warn("actuallyGetCurrentLocation returned null or empty.");
return null;
}
return worldId;
}
private WorldDataRequestResponse ConstructSuccessResponse(string data = null)
{
var responseData = new WorldDataRequestResponse(true, null, null);
responseData.StatusCode = 200;
responseData.Error = null;
responseData.OK = true;
responseData.Data = data;
return responseData;
}
private WorldDataRequestResponse ConstructErrorResponse(int statusCode, string error)
{
var responseData = new WorldDataRequestResponse(true, null, null);
responseData.StatusCode = statusCode;
responseData.Error = error;
responseData.OK = false;
responseData.Data = null;
return responseData;
}
/// <summary>
/// Sends a JSON response to an HTTP listener request with the specified response data and status code.
/// </summary>
@@ -346,12 +420,14 @@ namespace VRCX
}
catch (JsonReaderException ex)
{
logger.Error(ex, json.ToString());
this.lastError = ex.Message;
// invalid json
return;
}
catch (Exception ex)
{
logger.Error(ex, json.ToString());
this.lastError = ex.Message;
// something else happened lol
return;
@@ -359,18 +435,21 @@ namespace VRCX
if (String.IsNullOrEmpty(request.Key))
{
logger.Warn("World {0} tried to store data with no key provided", request.Key);
this.lastError = "`key` is missing or null";
return;
}
if (String.IsNullOrEmpty(request.Value))
{
logger.Warn("World {0} tried to store data with no value provided", request.Key);
this.lastError = "`value` is missing or null";
return;
}
if (String.IsNullOrEmpty(request.ConnectionKey))
{
logger.Warn("World {0} tried to store data with no connection key provided", request.Key);
this.lastError = "`connectionKey` is missing or null";
return;
}
@@ -378,6 +457,7 @@ namespace VRCX
// Make sure the connection key is a valid GUID. No point in doing anything else if it's not.
if (!Guid.TryParse(request.ConnectionKey, out Guid _))
{
logger.Warn("World {0} tried to store data with an invalid GUID as a connection key {1}", request.Key, request.ConnectionKey);
this.lastError = "Invalid GUID provided as connection key";
// invalid guid
return;
@@ -387,6 +467,7 @@ namespace VRCX
string worldId = worldDB.GetWorldByConnectionKey(request.ConnectionKey);
if (worldId == null)
{
logger.Warn("World {0} tried to store data with invalid connection key {1}", request.Key, request.ConnectionKey);
this.lastError = "Invalid connection key";
// invalid connection key
return;
@@ -402,6 +483,7 @@ namespace VRCX
// This works, I tested it. Hopefully this prevents/limits any possible abuse.
if (newTotalDataSize > 1024 * 1024 * 10)
{
logger.Warn("World {0} exceeded 10MB total data size trying to store key {0}. {1}:{2} + {3} = {4}", request.Key, worldId, oldTotalDataSize - oldDataSize, newDataSize, newTotalDataSize);
this.lastError = $"You have hit the 10MB total data cap. The previous data entry was *not* stored. Your request was {newDataSize} bytes, your current shared byte total is {oldTotalDataSize} and you went over the table limit by {newTotalDataSize - (1024 * 1024 * 10)} bytes.";
// too much data
//throw new Exception("Too much data");
@@ -411,6 +493,8 @@ namespace VRCX
worldDB.AddDataEntry(worldId, request.Key, request.Value, newDataSize);
worldDB.UpdateWorldDataSize(worldId, newTotalDataSize);
logger.Info("World {0} stored data entry {1} with size {2} bytes", worldId, request.Key, newDataSize);
logger.Debug("{0} : {1}", request.Key, request.Value);
}
public void Stop()