using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Net; using System.Reflection; using System.Security; using System.Text; using System.Text.RegularExpressions; using log4net; using Nini.Config; using OpenMetaverse; using OpenMetaverse.StructuredData; using OpenSim.Data.MySQL; using OpenSim.Framework; using OpenSim.Framework.Servers; using OpenSim.Framework.Servers.HttpServer; using OpenSim.Server.Handlers.Base; namespace OpenSim.Server.Handlers.Web { public class WebServerConnector : ServiceConnector { // This is all slow and clunky, it's not a real web server, just something to use if you don't want a real one. private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); private IConfigSource m_Config; protected MySQLRaw m_database = null; private Hashtable mime = new Hashtable(); private Hashtable ssi = new Hashtable(); public WebServerConnector(IConfigSource config, IHttpServer server, string configName) : base(config, server, configName) { string dllName = String.Empty; string connString = String.Empty; m_Config = config; // Try reading the [DatabaseService] section, if it exists IConfig dbConfig = m_Config.Configs["DatabaseService"]; if (dbConfig != null) { if (dllName == String.Empty) dllName = dbConfig.GetString("StorageProvider", String.Empty); if (connString == String.Empty) connString = dbConfig.GetString("ConnectionString", String.Empty); } if (dllName.Equals(String.Empty)) throw new Exception("No StorageProvider configured"); //// TODO - Should do the plugin thing to pick between database backends. //// Or not, we are all using MariaDB anyway. // m_Database = LoadPlugin(dllName, new Object[] { connString }); m_database = new MySQLRaw(connString); mime.Add(".gz", "application/gzip"); mime.Add(".js", "application/javascript"); mime.Add(".json", "application/json"); mime.Add(".pdf", "application/pdf"); mime.Add(".rtf", "application/rtf"); mime.Add(".zip", "application/zip"); mime.Add(".xz", "application/x-xz"); mime.Add(".gif", "image/gif"); mime.Add(".png", "image/png"); mime.Add(".jp2", "image/jp2"); mime.Add(".jpg2", "image/jp2"); mime.Add(".jpe", "image/jpeg"); mime.Add(".jpg", "image/jpeg"); mime.Add(".jpeg", "image/jpeg"); mime.Add(".svg", "image/svg+xml"); mime.Add(".svgz", "image/svg+xml"); mime.Add(".tif", "image/tiff"); mime.Add(".tiff", "image/tiff"); mime.Add(".css", "text/css"); mime.Add(".html", "text/html"); mime.Add(".htm", "text/html"); mime.Add(".shtml", "text/html"); // mime.Add(".md", "text/markdown"); // mime.Add(".markdown","text/markdown"); mime.Add(".txt", "text/plain"); IConfig cfg = m_Config.Configs["GridInfoService"]; string HomeURI = Util.GetConfigVarFromSections(m_Config, "HomeURI", new string[] { "Startup", "Hypergrid" }, String.Empty); ssi.Add("grid", cfg.GetString("gridname", "my grid")); ssi.Add("uri", cfg.GetString("login", HomeURI)); ssi.Add("version", VersionInfo.Version); server.AddHTTPHandler("/web/", WebRequestHandler); IConfig networkConfig = m_Config.Configs["Network"]; if (null != networkConfig) { uint https_port = (uint) networkConfig.GetInt("https_port", 0); if (0 != https_port) { server = MainServer.GetHttpServer(https_port, null); if (null != server) server.AddHTTPHandler("/web/", WebRequestHandler); } } } private Hashtable WebRequestHandler(Hashtable request) { long locIn = m_database.Count("Presence", "RegionID != '00000000-0000-0000-0000-000000000000'"); // Locals online but not HGing, and HGers in world. long HGin = m_database.Count("Presence", "UserID NOT IN (SELECT PrincipalID FROM UserAccounts)"); // HGers in world. long locOut = m_database.Count("hg_traveling_data", "GridExternalName != '" + ssi["uri"] + "'"); // Locals that are HGing. Hashtable reply = new Hashtable(); Hashtable replyHeaders = new Hashtable(); ssi["members"] = m_database.Count("UserAccounts").ToString(); ssi["sims"] = m_database.Count("regions").ToString(); ssi["inworld"] = (locIn - HGin).ToString(); ssi["outworld"] = locOut.ToString(); ssi["hgers"] = HGin.ToString(); ssi["month"] = m_database.Count("GridUser", "Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 2419200))").ToString(); string reqpath = (string) request["uri"]; string[] query = (string[]) request["querystringkeys"]; Hashtable headers = (Hashtable) request["headers"]; string method = (string) request["http-method"]; string type = (string) request["content-type"]; string body = (string) request["body"]; string file = reqpath.Remove(0, 5); string path = Path.Combine(Util.webDir(), file); if (! Path.GetFullPath(path).StartsWith(Path.GetFullPath(Util.webDir()))) { m_log.ErrorFormat("[WEB SERVICE]: INVALID PATH {0} != {1}", Path.GetFullPath(path), Path.GetFullPath(Util.webDir())); reply["int_response_code"] = 404; reply["content_type"] = "text/html"; reply["str_response_string"] = "404 Unknown page" + "404 error, can't find the " + reqpath + " page.

 

"; return reply; } m_log.InfoFormat("[WEB SERVICE]: {0} method path {1} contont type {2} body {3}.", method, reqpath, type, body); foreach (DictionaryEntry h in headers) m_log.InfoFormat("[WEB SERVICE]: {0} method path {1} header {2} = {3}", method, reqpath, (string) h.Key, (string) h.Value); foreach (String q in query) m_log.InfoFormat("[WEB SERVICE]: {0} method path {1} query {2} value {3}", method, reqpath, q, (string) request[q]); reply["int_response_code"] = 200; if (("GET" == method) || ("HEAD" == method)) { if (File.Exists(path)) { DateTime dt = File.GetLastWriteTimeUtc(path); string m = (string) mime[Path.GetExtension(path).ToLower()]; reply["content_type"] = m; if ((null == m) || ("text/" != m.Substring(0, 5))) { string ifdtr = (string) headers["if-modified-since"]; if (null != ifdtr) { try { DateTime ifdt = DateTime.Parse(ifdtr, System.Globalization.CultureInfo.InvariantCulture); if (0 >= DateTime.Compare(ifdt, dt)) { reply["int_response_code"] = 304; m_log.InfoFormat("[WEB SERVICE]: If-Modified-Since is earliar or equal to Last-Modified, from {0}", path); reply["headers"] = replyHeaders; if ("HEAD" == method) { reply["bin_response_data"] = null; reply["str_response_string"] = null; } return reply; } } catch (Exception) { m_log.ErrorFormat("[WEB SERVICE]: Invalid If-Modified-Since header, ignoring it, from {0}", path); } } replyHeaders["Last-Modified"] = dt.ToString("R"); reply["bin_response_data"] = File.ReadAllBytes(path); } else { replyHeaders["Cache-Control"] = "no-cache"; StreamReader csr = File.OpenText(path); string content = csr.ReadToEnd(); // Slow and wasteful, but I'm expecting only tiny web files, not accessed very often. foreach (DictionaryEntry v in ssi) { content = content.Replace("", (string) v.Value); } reply["str_response_string"] = content; csr.Close(); } } else { if ("account.html" == file) reply["str_response_string"] = loginPage(null, ""); else { m_log.ErrorFormat("[WEB SERVICE]: Unable to read {0}.", path); reply["int_response_code"] = 404; reply["content_type"] = "text/html"; reply["str_response_string"] = "404 Unknown page" + "404 error, can't find the " + reqpath + " page.

 

"; } } } else if ("POST" == method) { Hashtable fields = new Hashtable(); string[] bdy = body.Split('&'); body = ""; foreach (String bd in bdy) { string[] b = bd.Split('='); if (b.Length == 0) continue; String n = System.Web.HttpUtility.UrlDecode(b[0]); String v = ""; if (b.Length > 1) v = System.Web.HttpUtility.UrlDecode(b[1]); if ((0 != String.Compare("password", n)) && (0 != String.Compare("psswrd", n))) { // Poor mans Bobby Tables protection. v = v.Replace("'", "_"); v = v.Replace("\"", "_"); v = v.Replace(";", "_"); v = v.Replace("(", "_"); v = v.Replace(")", "_"); } fields[n] = v; body = body + "

" + n + " = " + v + "

\n"; } if ("account.html" == file) { string doit = fields["doit"].ToString(); replyHeaders["Cache-Control"] = "no-cache"; if ("logout" == doit) reply["str_response_string"] = loginPage(null, "Logged out."); else if (("create" == doit) || ("confirm" == doit)) { Regex rgxName = new Regex("^[a-zA-Z0-9]+$"); Regex rgxEmail = new Regex("^.+@.+\\..+$"); string[] names = fields["name"].ToString().Split(' '); if ("" == fields["email"].ToString()) reply["str_response_string"] = loginPage(fields, "Please supply an email address when creating an account."); else if (!rgxEmail.IsMatch(fields["email"].ToString())) reply["str_response_string"] = loginPage(fields, "Please supply a valid email address when creating an account."); else if (!Uri.IsWellFormedUriString("mailto:" + fields["email"].ToString(), System.UriKind.Absolute)) reply["str_response_string"] = loginPage(fields, "Please supply a valid email address when creating an account."); // TODO - the other test to do here is actually lookup the domain name, looking for any sort of record. else if (2 != names.Length) reply["str_response_string"] = loginPage(fields, "Please supply a two word name when creating an account."); // SL docs say 31 characters each for first and last name. UserAccounts table is varchar(64) each. userinfo has varchar(50) for the combined name. // The userinfo table seems to be obsolete. // Singularity at least limits the total name to 64. // I can't find any limitations on characters allowed, but I only ever see letters and digits used. Case is stored, but not significant. // OpenSims "create user" console command doesn't sanitize it at all, even crashing on some names. else if (31 < names[0].Length) reply["str_response_string"] = loginPage(fields, "First and last names are limited to 31 letters each."); else if (31 < names[1].Length) reply["str_response_string"] = loginPage(fields, "First and last names are limited to 31 letters each."); else if (!rgxName.IsMatch(names[0])) reply["str_response_string"] = loginPage(fields, "First and last names are limited to letters and digits."); else if (!rgxName.IsMatch(names[1])) reply["str_response_string"] = loginPage(fields, "First and last names are limited to letters and digits."); // TODO - check and disallow god names, those are done in the console. else { if (("create" == doit)) reply["str_response_string"] = accountCreationPage(body, fields); else { if (0 != String.Compare(fields["psswrd"].ToString(), fields["password"].ToString())) reply["str_response_string"] = loginPage(fields, "Passwords are not the same."); else reply["str_response_string"] = loggedOnPage(body, fields); } } } else if ("cancel" == doit) { reply["str_response_string"] = loginPage(null, "Cancelled."); } else if ("list" == doit) { List< Hashtable > rows = m_database.Select("UserAccounts", "CONCAT(FirstName,' ',LastName) as Name,UserTitle as Title,UserLevel as Level,UserFlags as Flags,PrincipalID as UUID", "", "Name"); reply["str_response_string"] = "member accounts" + table(rows, new string[5] {"Name", "Title", "Level", "Flags", "UUID"}, "member accounts", "account.html?doit=edit&token=" + fields["token"].ToString(), "UUID") + "

" + button("my account") + "

"; } else { reply["str_response_string"] = loggedOnPage(body, fields); } } else { m_log.ErrorFormat("[WEB SERVICE]: No such POST target {0}.", path); reply["int_response_code"] = 404; reply["content_type"] = "text/html"; reply["str_response_string"] = "404 Unknown page" + "404 error, can't find the " + reqpath + " page.

 

"; } } else { m_log.ErrorFormat("[WEB SERVICE]: UNKNOWN method {0} path {1}.", method, reqpath); reply["int_response_code"] = 404; reply["content_type"] = "text/html"; reply["str_response_string"] = "Unknown method" + "HUH! For " + reqpath + " page.

 

"; } m_log.Info("[WEB SERVICE]: "); reply["headers"] = replyHeaders; if ("HEAD" == method) { reply["bin_response_data"] = null; reply["str_response_string"] = null; } return reply; } private string loginPage(Hashtable fields, string message) { string n = ""; string e = ""; if (null != fields) { n = fields["name"].ToString(); e = fields["email"].ToString(); } return header(ssi["grid"] + " account") + form("account.html", "", text("text", "name", "name", n, 63, true) + text("email", "email", "email", e, 254, false) + text("password", "password", "password", "", 0, true) + "Warning, the limit on password length is set by your viewer, some can't handle longer than 16 characters." + button("create") + button("login") ) + "

" + message + "

" + footer(); } private string accountCreationPage(string body, Hashtable fields) { return header(ssi["grid"] + " account") + "

Creating " + ssi["grid"] + " account for " + fields["name"].ToString() + "

" + form("account.html", fields["token"].ToString(), hidden("name", fields["name"].ToString()) + hidden("psswrd", fields["password"].ToString()) + text("email", "An email will be sent to", "email", fields["email"].ToString(), 254, true) + " to validate it, please double check this." + text("password", "Re-enter your password", "password", "", 0, true) + "Warning, the limit on password length is set by your viewer, some can't handle longer than 16 characters." + button("confirm") + button("cancel") ) + body + footer(); } private string loggedOnPage(string body, Hashtable fields) { return header(ssi["grid"] + " account") + "

" + ssi["grid"] + " account for " + fields["name"].ToString() + "

" + form("account.html", fields["token"].ToString(), hidden("name", fields["name"].ToString()) // + hidden("UUID", fields["UUID"].ToString()) + text("email", "email", "email", fields["email"].ToString(), 254, true) + text("password", "password", "password", "", 0, false) + "Warning, the limit on password length is set by your viewer, some can't handle longer than 16 characters." // + text("title", "text", "title", fields["title"].ToString(), 64, false) + select("type", "type", option("", false) + option("approved", true) + option("disabled", false) + option("god", false) ) + button("delete") + button("list") + button("logout") // + button("read") + button("update") ) + body + footer(); } private string header(string title) { return "\n \n " + title + "\n \n \n"; } private string table(List< Hashtable > rows, string[] fields, string caption, string URL, string id) { string tbl = ""; bool head = true; string address = ""; string addrend = ""; foreach (Hashtable row in rows) { if (0 == fields.Length) { int c = 0; foreach (DictionaryEntry r in row) c++; fields = new string[c]; c = 0; foreach (DictionaryEntry r in row) fields[c++] = (string) r.Key; } string line = ""; address = ""; if ("" != URL) { address = ""; if (head) { foreach (string s in fields) line = line + ""; tbl = tbl + line + "\n"; head = false; } line = ""; foreach (string s in fields) { if (s == id) line = line + ""; else line = line + ""; } tbl = tbl + line + "\n"; } return tbl + "
" + caption + "
" + s + "
" + address + row[s] + addrend + "" + row[s] + "
"; } private string form(string action, string token, string form) { return "
\n" + hidden("token", token) + form + "
\n"; } private string hidden(string name, string val) { return " \n"; } private string text(string type, string title, string name, string val, int max, bool required) { string extra = ""; if (0 < max) extra = extra + " maxlength=\"" + max.ToString() + "\""; if (required) extra = extra + " required"; if ("" != val) val = "value=\"" + val + "\""; return "

" + title + " :

\n"; } private string select(string title, string name, string options) { return "

" + title + " : \n \n

\n"; } private string option(string title, bool selected) { string sel = ""; if (selected) sel = " selected"; return " \n"; } private string button(string title) { return " \n"; } private string footer() { return " \n\n"; } } }