path)
{
path = path.Trim().TrimEnd('/');
if (path[0] == '/')
return path;
return ("/" + path.ToString()).AsSpan();
}
///
/// Checks if we have an Exact path in the LLSD handlers for the path provided
///
/// URI of the request
/// true if we have one, false if not
private bool DoWeHaveALLSDHandler(string path)
{
if(m_llsdHandlers.Count == 0)
{
return false;
}
var searchquery = CleanSearchPath(path.AsSpan());
lock (m_llsdHandlers)
{
foreach (string pattern in m_llsdHandlers.Keys)
{
if (searchquery.Length >= pattern.Length && searchquery.StartsWith(pattern))
return true;
}
}
return false;
}
///
/// Checks if we have an Exact path in the HTTP handlers for the path provided
///
/// URI of the request
/// true if we have one, false if not
private bool DoWeHaveAHTTPHandler(string path)
{
var searchquery = CleanSearchPath(path.AsSpan());
//m_log.DebugFormat("[BASE HTTP HANDLER]: Checking if we have an HTTP handler for {0}", searchquery);
lock (m_HTTPHandlers)
{
foreach (string pattern in m_HTTPHandlers.Keys)
{
if (searchquery.Length >= pattern.Length && searchquery.StartsWith(pattern))
return true;
}
}
return false;
}
private bool TryGetLLSDHandler(string path, out LLSDMethod llsdHandler)
{
if(m_llsdHandlers.Count == 0)
{
llsdHandler = null;
return false;
}
// Pull out the first part of the path
// splitting the path by '/' means we'll get the following return..
// {0}/{1}/{2}
// where {0} isn't something we really control 100%
var searchquery = CleanSearchPath(path.AsSpan());
// while the matching algorithm below doesn't require it, we're expecting a query in the form
//
// [] = optional
// /resource/UUID/action[/action]
//
// now try to get the closest match to the reigstered path
// at least for OGP, registered path would probably only consist of the /resource/
string bestMatch = null;
bool nomatch = true;
lock (m_llsdHandlers)
{
foreach (string pattern in m_llsdHandlers.Keys)
{
if (searchquery.StartsWith(pattern, StringComparison.InvariantCultureIgnoreCase))
{
if (nomatch || searchquery.Length > bestMatch.Length)
{
bestMatch = pattern;
nomatch = false;
}
}
}
if (nomatch)
{
llsdHandler = null;
return false;
}
if (bestMatch == "/" && searchquery != "/")
{
llsdHandler = null;
return false;
}
llsdHandler = m_llsdHandlers[bestMatch];
return true;
}
}
// legacy should go
public byte[] HandleHTTPRequest(OSHttpRequest request, OSHttpResponse response)
{
// m_log.DebugFormat(
// "[BASE HTTP SERVER]: HandleHTTPRequest for request to {0}, method {1}",
// request.RawUrl, request.HttpMethod);
if (!TryGetHTTPHandlerPathBased(request.RawUrl, out GenericHTTPMethod requestprocessor))
{
return SendHTML404(response);
}
// m_log.DebugFormat("[BASE HTTP SERVER]: HandleContentVerbs for request to {0}", request.RawUrl);
// This is a test. There's a workable alternative.. as this way sucks.
// We'd like to put this into a text file parhaps that's easily editable.
//
// For this test to work, I used the following secondlife.exe parameters
// "C:\Program Files\SecondLifeWindLight\SecondLifeWindLight.exe" -settings settings_windlight.xml -channel "Second Life WindLight" -set SystemLanguage en-us -loginpage http://10.1.1.2:8002/?show_login_form=TRUE -loginuri http://10.1.1.2:8002 -user 10.1.1.2
//
// Even after all that, there's still an error, but it's a start.
//
// I depend on show_login_form being in the secondlife.exe parameters to figure out
// to display the form, or process it.
// a better way would be nifty.
string requestBody;
using(StreamReader reader = new StreamReader(request.InputStream, Encoding.UTF8))
requestBody = reader.ReadToEnd();
Hashtable keysvals = new Hashtable();
Hashtable headervals = new Hashtable();
Hashtable requestVars = new Hashtable();
string host = string.Empty;
string[] querystringkeys = request.QueryString.AllKeys;
string[] rHeaders = request.Headers.AllKeys;
keysvals.Add("body", requestBody);
keysvals.Add("uri", request.RawUrl);
keysvals.Add("content-type", request.ContentType);
keysvals.Add("http-method", request.HttpMethod);
foreach (string queryname in querystringkeys)
{
//m_log.DebugFormat(
// "[BASE HTTP SERVER]: Got query paremeter {0}={1}", queryname, request.QueryString[queryname]);
if(!string.IsNullOrEmpty(queryname))
{
keysvals.Add(queryname, request.QueryString[queryname]);
requestVars.Add(queryname, keysvals[queryname]);
}
}
foreach (string headername in rHeaders)
{
//m_log.Debug("[BASE HTTP SERVER]: " + headername + "=" + request.Headers[headername]);
headervals[headername] = request.Headers[headername];
}
keysvals.Add("headers", headervals);
keysvals.Add("querystringkeys", querystringkeys);
keysvals.Add("requestvars", requestVars);
//keysvals.Add("form", request.Form);
Hashtable responsedata2 = requestprocessor(keysvals);
return DoHTTPGruntWork(responsedata2, response);
}
private bool TryGetHTTPHandlerPathBased(string path, out GenericHTTPMethod httpHandler)
{
if(m_HTTPHandlers.Count == 0)
{
httpHandler = null;
return false;
}
var searchquery = CleanSearchPath(path);
string bestMatch = null;
bool nomatch = true;
//m_log.DebugFormat(
// "[BASE HTTP HANDLER]: TryGetHTTPHandlerPathBased() looking for HTTP handler to match {0}", searchquery);
lock (m_HTTPHandlers)
{
foreach (string pattern in m_HTTPHandlers.Keys)
{
if (searchquery.StartsWith(pattern, StringComparison.InvariantCultureIgnoreCase))
{
if (nomatch || searchquery.Length > bestMatch.Length)
{
bestMatch = pattern;
nomatch = false;
}
}
}
if (nomatch)
{
httpHandler = null;
return false;
}
if (bestMatch == "/" && searchquery != "/")
{
httpHandler = null;
return false;
}
httpHandler = m_HTTPHandlers[bestMatch];
return true;
}
}
internal static byte[] DoHTTPGruntWork(Hashtable responsedata, OSHttpResponse response)
{
int responsecode;
string responseString = string.Empty;
byte[] responseData = null;
string contentType;
if (responsedata is null)
{
responsecode = 500;
responseString = "No response could be obtained";
contentType = "text/plain";
responsedata = new Hashtable();
}
else
{
try
{
//m_log.Info("[BASE HTTP SERVER]: Doing HTTP Grunt work with response");
responsecode = (int)responsedata["int_response_code"];
contentType = (string)responsedata["content_type"];
if (responsedata["bin_response_data"] is byte[] b)
responseData = b;
else
responseString = (string)responsedata["str_response_string"];
}
catch
{
responsecode = 500;
responseString = "No response could be obtained";
contentType = "text/plain";
responsedata = new Hashtable();
}
}
if (responsedata.ContainsKey("error_status_text"))
response.StatusDescription = (string)responsedata["error_status_text"];
if (responsedata.ContainsKey("http_protocol_version"))
response.ProtocolVersion = (string)responsedata["http_protocol_version"];
if (responsedata.ContainsKey("keepalive"))
{
bool keepalive = (bool)responsedata["keepalive"];
response.KeepAlive = keepalive;
}
// Cross-Origin Resource Sharing with simple requests
if (responsedata.ContainsKey("access_control_allow_origin"))
response.AddHeader("Access-Control-Allow-Origin", (string)responsedata["access_control_allow_origin"]);
//Even though only one other part of the entire code uses HTTPHandlers, we shouldn't expect this
//and should check for NullReferenceExceptions
if (string.IsNullOrEmpty(contentType))
{
contentType = "text/html";
}
// The client ignores anything but 200 here for web login, so ensure that this is 200 for that
response.StatusCode = responsecode;
if (responsecode == (int)HttpStatusCode.Moved)
{
response.Redirect((string)responsedata["str_redirect_location"], HttpStatusCode.Moved);
}
response.AddHeader("Content-Type", contentType);
if (responsedata.ContainsKey("headers"))
{
Hashtable headerdata = (Hashtable)responsedata["headers"];
foreach (string header in headerdata.Keys)
response.AddHeader(header, headerdata[header].ToString());
}
byte[] buffer;
if (responseData is not null)
{
buffer = responseData;
}
else
{
if(string.IsNullOrEmpty(responseString))
return null;
if (!(contentType.Contains("image")
|| contentType.Contains("x-shockwave-flash")
|| contentType.Contains("application/x-oar")
|| contentType.Contains("application/vnd.ll.mesh")))
{
// Text
buffer = Encoding.UTF8.GetBytes(responseString);
}
else
{
// Binary!
buffer = Convert.FromBase64String(responseString);
}
response.ContentLength64 = buffer.Length;
response.ContentEncoding = Encoding.UTF8;
}
return buffer;
}
public byte[] SendHTML404(OSHttpResponse response)
{
response.StatusCode = 404;
response.ContentType = "text/html";
string responseString = GetHTTP404();
byte[] buffer = Encoding.UTF8.GetBytes(responseString);
response.ContentLength64 = buffer.Length;
response.ContentEncoding = Encoding.UTF8;
return buffer;
}
public void Start()
{
Start(true, true);
}
///
/// Start the http server
///
///
/// If true then poll responses are performed asynchronsly.
/// Option exists to allow regression tests to perform processing synchronously.
///
public void Start(bool performPollResponsesAsync, bool runPool)
{
m_log.Info($"[BASE HTTP SERVER]: Starting HTTP{(UseSSL ? "S" : "")} server on port {Port}");
try
{
//m_httpListener = new HttpListener();
if (!m_ssl)
{
m_httpListener = tinyHTTPListener.Create(m_listenIPAddress, (int)m_port);
m_httpListener.ExceptionThrown += httpServerException;
if (DebugLevel > 0)
{
m_httpListener.LogWriter = httpserverlog;
httpserverlog.DebugLevel = 1;
}
// Uncomment this line in addition to those in HttpServerLogWriter
// if you want more detailed trace information from the HttpServer
//m_httpListener2.DisconnectHandler = httpServerDisconnectMonitor;
}
else
{
m_httpListener = tinyHTTPListener.Create(IPAddress.Any, (int)m_port, m_cert);
if(m_certificateValidationCallback is not null)
m_httpListener.CertificateValidationCallback = m_certificateValidationCallback;
m_httpListener.ExceptionThrown += httpServerException;
if (DebugLevel > 0)
{
m_httpListener.LogWriter = httpserverlog;
httpserverlog.DebugLevel = 1;
}
}
m_httpListener.RequestReceived += OnRequest;
m_httpListener.Start(64);
lock(m_generalLock)
{
if (runPool)
{
m_pollServiceManager ??= new PollServiceRequestManager(performPollResponsesAsync, 2, 25000);
m_pollServiceManager.Start();
}
}
HTTPDRunning = true;
}
catch (Exception e)
{
m_log.Error("[BASE HTTP SERVER]: Error - " + e.Message);
m_log.Error("[BASE HTTP SERVER]: Tip: Do you have permission to listen on port " + m_port + "?");
// We want this exception to halt the entire server since in current configurations we aren't too
// useful without inbound HTTP.
throw;
}
m_requestsProcessedStat = new Stat(
"HTTPRequestsServed",
"Number of inbound HTTP requests processed",
"",
"requests",
"httpserver",
Port.ToString(),
StatType.Pull,
MeasuresOfInterest.AverageChangeOverTime,
stat => stat.Value = RequestNumber,
StatVerbosity.Debug);
StatsManager.RegisterStat(m_requestsProcessedStat);
}
public static void httpServerException(object source, Exception exception)
{
if (source.ToString().Equals("HttpServer.HttpListener") && exception.ToString().StartsWith("Mono.Security.Protocol.Tls.TlsException"))
return;
m_log.ErrorFormat("[BASE HTTP SERVER]: {0} had an exception {1}", source.ToString(), exception.ToString());
}
public void Stop(bool stopPool = false)
{
HTTPDRunning = false;
StatsManager.DeregisterStat(m_requestsProcessedStat);
try
{
lock(m_generalLock)
{
if (stopPool && m_pollServiceManager != null)
m_pollServiceManager.Stop();
}
m_httpListener.ExceptionThrown -= httpServerException;
//m_httpListener2.DisconnectHandler = null;
m_httpListener.LogWriter = null;
m_httpListener.RequestReceived -= OnRequest;
m_httpListener.Stop();
}
catch (NullReferenceException)
{
m_log.Warn("[BASE HTTP SERVER]: Null Reference when stopping HttpServer.");
}
}
public void RemoveStreamHandler(string httpMethod, string path)
{
if (m_streamHandlers.TryRemove(path, out _))
return;
string handlerKey = GetHandlerKey(httpMethod, path);
//m_log.DebugFormat("[BASE HTTP SERVER]: Removing handler key {0}", handlerKey);
m_streamHandlers.TryRemove(handlerKey, out _);
}
public void RemoveStreamHandler(string path)
{
m_streamHandlers.TryRemove(path, out IRequestHandler _);
}
public void RemoveSimpleStreamHandler(string path)
{
if(m_simpleStreamHandlers.TryRemove(path, out _))
return;
m_simpleStreamVarPath.TryRemove(path, out _);
}
public void RemoveHTTPHandler(string httpMethod, string path)
{
if (string.IsNullOrEmpty(path))
return; // Caps module isn't loaded, tries to remove handler where path = null
lock (m_HTTPHandlers)
{
if (httpMethod is not null && httpMethod.Length == 0)
{
m_HTTPHandlers.Remove(path);
return;
}
m_HTTPHandlers.Remove(GetHandlerKey(httpMethod, path));
}
}
public void RemovePollServiceHTTPHandler(string httpMethod, string path)
{
if(!m_pollHandlers.TryRemove(path, out _))
m_pollHandlersVarPath.TryRemove(path, out _);
}
public void RemovePollServiceHTTPHandler(string path)
{
if(!m_pollHandlers.TryRemove(path, out _))
m_pollHandlersVarPath.TryRemove(path, out _);
}
//public bool RemoveAgentHandler(string agent, IHttpAgentHandler handler)
//{
// lock (m_agentHandlers)
// {
// IHttpAgentHandler foundHandler;
// if (m_agentHandlers.TryGetValue(agent, out foundHandler) && foundHandler == handler)
// {
// m_agentHandlers.Remove(agent);
// return true;
// }
// }
//
// return false;
//}
public void RemoveXmlRPCHandler(string method)
{
lock (m_rpcHandlers)
m_rpcHandlers.Remove(method);
}
public void RemoveJsonRPCHandler(string method)
{
lock(jsonRpcHandlers)
jsonRpcHandlers.Remove(method);
}
public bool RemoveLLSDHandler(string path, LLSDMethod handler)
{
lock (m_llsdHandlers)
{
if (m_llsdHandlers.TryGetValue(path, out LLSDMethod foundHandler) && foundHandler == handler)
{
m_llsdHandlers.Remove(path);
return true;
}
}
return false;
}
// Fallback HTTP responses in case the HTTP error response files don't exist
private static string getDefaultHTTP404()
{
return "404 Page not found
Ooops!
The page you requested has been obsconded with by knomes. Find hippos quick!
";
}
public void SetHTTP404()
{
string file = Path.Combine(".", "http_404.html");
try
{
if (File.Exists(file))
{
using (StreamReader sr = File.OpenText(file))
HTTP404 = sr.ReadToEnd();
if(string.IsNullOrWhiteSpace(HTTP404))
HTTP404 = getDefaultHTTP404();
return;
}
}
catch { }
HTTP404 = getDefaultHTTP404();
}
public string GetHTTP404()
{
return HTTP404;
}
}
public class HttpServerContextObj
{
public IHttpClientContext context = null;
public IHttpRequest req = null;
public OSHttpRequest oreq = null;
public OSHttpResponse oresp = null;
public HttpServerContextObj(IHttpClientContext contxt, IHttpRequest reqs)
{
context = contxt;
req = reqs;
}
public HttpServerContextObj(OSHttpRequest osreq, OSHttpResponse osresp)
{
oreq = osreq;
oresp = osresp;
}
}
///
/// Relays HttpServer log messages to our own logging mechanism.
///
/// To use this you must uncomment the switch section
///
/// You may also be able to get additional trace information from HttpServer if you uncomment the UseTraceLogs
/// property in StartHttp() for the HttpListener
///
public class HttpServerLogWriter : ILogWriter
{
private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
public int DebugLevel {get; set;} = (int)LogPrio.Error;
public void Write(object source, LogPrio priority, string message)
{
if((int)priority < DebugLevel)
return;
switch (priority)
{
case LogPrio.Trace:
m_log.DebugFormat("[{0}]: {1}", source, message);
break;
case LogPrio.Debug:
m_log.DebugFormat("[{0}]: {1}", source, message);
break;
case LogPrio.Error:
m_log.ErrorFormat("[{0}]: {1}", source, message);
break;
case LogPrio.Info:
m_log.InfoFormat("[{0}]: {1}", source, message);
break;
case LogPrio.Warning:
m_log.WarnFormat("[{0}]: {1}", source, message);
break;
case LogPrio.Fatal:
m_log.ErrorFormat("[{0}]: FATAL! - {1}", source, message);
break;
default:
break;
}
return;
}
}
public class IndexPHPHandler : SimpleStreamHandler
{
readonly BaseHttpServer m_server;
public IndexPHPHandler(BaseHttpServer server)
: base("/index.php")
{
m_server = server;
}
protected override void ProcessRequest(IOSHttpRequest httpRequest, IOSHttpResponse httpResponse)
{
httpResponse.KeepAlive = false;
if (m_server is null || !m_server.HTTPDRunning)
{
httpResponse.StatusCode = (int)HttpStatusCode.NotFound;
return;
}
if (httpRequest.QueryString.Count == 0)
{
httpResponse.Redirect("http://opensimulator.org");
return;
}
if (httpRequest.QueryFlags.Contains("about"))
{
httpResponse.Redirect("http://opensimulator.org/wiki/0.9.3.1_Release");
return;
}
if (!httpRequest.QueryAsDictionary.TryGetValue("method", out string method) || string.IsNullOrWhiteSpace(method))
{
httpResponse.StatusCode = (int)HttpStatusCode.NotFound;
return;
}
int indx = method.IndexOf(',');
if(indx > 0)
method = method[..indx];
if (string.IsNullOrWhiteSpace(method))
{
httpResponse.StatusCode = (int)HttpStatusCode.NotFound;
return;
}
SimpleStreamMethod sh = m_server.TryGetIndexPHPMethodHandler(method);
if (sh is null)
{
httpResponse.StatusCode = (int)HttpStatusCode.NotFound;
return;
}
try
{
sh?.Invoke(httpRequest, httpResponse);
}
catch
{
httpResponse.StatusCode = (int)HttpStatusCode.InternalServerError;
}
}
}
}