From 8f280962f019d46e0367b29246283a1e34ceb955 Mon Sep 17 00:00:00 2001 From: onefang Date: Sun, 18 Aug 2019 14:12:21 +1000 Subject: Various additions to the web account manager. Track if we are accessed via HTTP or HTTPS, and the server name. Track cookies. HEAD method. Various security clean ups. Force HTTPS for account.html. Poor mans Bobby Tables protection. Security token. Validate inputs. Looking up the DNS records for email domain name. Don't allow creation of accounts with god names, leave that for the console. Check if created user name exists already. Double check the passwords and emails. Error messages on dynamic pages. Various clean ups. TODO++ --- OpenSim/Server/Handlers/Web/WebServerConnector.cs | 514 +++++++++++++++++----- 1 file changed, 406 insertions(+), 108 deletions(-) (limited to 'OpenSim/Server/Handlers/Web/WebServerConnector.cs') diff --git a/OpenSim/Server/Handlers/Web/WebServerConnector.cs b/OpenSim/Server/Handlers/Web/WebServerConnector.cs index c18e09e..67cb8da 100644 --- a/OpenSim/Server/Handlers/Web/WebServerConnector.cs +++ b/OpenSim/Server/Handlers/Web/WebServerConnector.cs @@ -24,16 +24,34 @@ namespace OpenSim.Server.Handlers.Web // 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(); + private IPAddress m_IP; + private IHttpServer m_server; + private IHttpServer m_SSLserver = null; + private string m_domain = ""; + private uint m_http_port; + private uint m_https_port = 0; + + private Dictionary m_auth = new Dictionary(); + + private static Dictionary m_firstNames = new Dictionary(); + private static Dictionary m_lastNames = new Dictionary(); + private static Dictionary m_fullNames = new Dictionary(); + public WebServerConnector(IConfigSource config, IHttpServer server, string configName) : base(config, server, configName) { string dllName = String.Empty; string connString = String.Empty; m_Config = config; + m_server = server; + m_IP = MainServer.Instance.ListenIPAddress; + m_http_port = server.Port; // Try reading the [DatabaseService] section, if it exists IConfig dbConfig = m_Config.Configs["DatabaseService"]; @@ -79,52 +97,104 @@ namespace OpenSim.Server.Handlers.Web // mime.Add(".markdown","text/markdown"); mime.Add(".txt", "text/plain"); + // Grab some info. 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); + cfg = m_Config.Configs["Const"]; + m_domain = cfg.GetString("HostName", "localhost"); + // Copied from OpenSim/Region/OptionalModules/ViewerSupport/GodNamesModule.cs + cfg = m_Config.Configs["GodNames"]; + if (null != cfg) + { + m_log.Info("[WEB SERVICE]: Loading god names."); + string conf_str = cfg.GetString("FullNames", String.Empty); + if (String.Empty != conf_str) + { + foreach (string strl in conf_str.Split(',')) + { + string strlan = strl.Trim(" \t".ToCharArray()); + m_log.InfoFormat("[WEB SERVICE]: Adding {0} as a god name", strlan); + m_fullNames.Add(strlan, strlan); + } + } + + conf_str = cfg.GetString("FirstNames", String.Empty); + if (String.Empty != conf_str) + { + foreach (string strl in conf_str.Split(',')) + { + string strlan = strl.Trim(" \t".ToCharArray()); + m_log.InfoFormat("[WEB SERVICE]: Adding {0} as a god first name", strlan); + m_firstNames.Add(strlan, strlan); + } + } + + conf_str = cfg.GetString("Surnames", String.Empty); + if (String.Empty != conf_str) + { + foreach (string strl in conf_str.Split(',')) + { + string strlan = strl.Trim(" \t".ToCharArray()); + m_log.InfoFormat("[WEB SERVICE]: Adding {0} as a god last name", strlan); + m_lastNames.Add(strlan, strlan); + } + } + } + else + m_log.Info("[WEB SERVICE]: No god names loaded."); + + // Add the HTTP and HTTPS handlers. 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) + m_https_port = (uint) networkConfig.GetInt("https_port", 0); + if (0 != m_https_port) { - server = MainServer.GetHttpServer(https_port, null); - if (null != server) - server.AddHTTPHandler("/web/", WebRequestHandler); + m_SSLserver = MainServer.GetHttpServer(m_https_port, null); + if (null != m_SSLserver) + m_SSLserver.AddHTTPHandler("/web/", WebRequestHandlerSSL); } - } - } + } + } + // AAARGGGH, in the request we don't get the HTTP/S, domain name, nor port number we were called from. So we have to fake it, sorta. + private Hashtable WebRequestHandlerSSL(Hashtable request) + { + return Handler(request, true); + } 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. + return Handler(request, false); + } + private Hashtable Handler(Hashtable request, bool usedSSL) + { 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(); + Hashtable cookies = new Hashtable(); + Hashtable fields = new Hashtable(); + List errors = new List(); 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[] query = (string[]) request["querystringkeys"]; + Hashtable headers = (Hashtable) request["headers"]; + Hashtable vars = (Hashtable) request["requestvars"]; string file = reqpath.Remove(0, 5); string path = Path.Combine(Util.webDir(), file); + m_log.InfoFormat("[WEB SERVICE]: {0} {1} {2} : {3} {4}, server IP {5} content type {6}, body {7}.", + headers["remote_addr"].ToString(), method, m_domain, (usedSSL ? m_https_port : m_http_port), reqpath, m_IP, type, body); + 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())); + 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" + @@ -132,13 +202,59 @@ namespace OpenSim.Server.Handlers.Web return reply; } - m_log.InfoFormat("[WEB SERVICE]: {0} method path {1} contont type {2} body {3}.", method, reqpath, type, body); + 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. + ssi["hgers"] = HGin.ToString(); + ssi["inworld"] = (locIn - HGin).ToString(); + ssi["outworld"] = m_database.Count("hg_traveling_data", "GridExternalName != '" + ssi["uri"] + "'").ToString(); // Locals that are HGing. + ssi["members"] = m_database.Count("UserAccounts").ToString(); + ssi["month"] = m_database.Count("GridUser", "Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 2419200))").ToString(); + ssi["sims"] = m_database.Count("regions").ToString(); + 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); + { + if ("cookie" == h.Key.ToString()) + { + string[] cks = h.Value.ToString().Split(';'); + foreach (String c in cks) + { + string[] ck = c.Split('='); + cookies[ck[0].Trim(' ')] = ck[1].Trim(' '); + } + } + } + + if ("POST" == method) + { + 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]); + fields[n] = bobbyTables(n, v); + body = body + "

" + n + " = " + v + "

\n"; + } + } + foreach (String q in query) - m_log.InfoFormat("[WEB SERVICE]: {0} method path {1} query {2} value {3}", method, reqpath, q, (string) request[q]); + { + m_log.InfoFormat("[WEB SERVICE]: {0} {1} query {2} = {3}", method, reqpath, q, (string) request[q]); + fields[q] = bobbyTables(q, (string) request[q]); + } + foreach (DictionaryEntry h in headers) + m_log.DebugFormat("[WEB SERVICE]: {0} {1} header {2} = {3}", method, reqpath, (string) h.Key, (string) h.Value); + // I dunno what these vars are or where they come from, never actually seen them. + foreach (DictionaryEntry h in vars) + m_log.InfoFormat("[WEB SERVICE]: {0} {1} var {2} = {3}", method, reqpath, (string) h.Key, (string) h.Value); reply["int_response_code"] = 200; + if (("GET" == method) || ("HEAD" == method)) { if (File.Exists(path)) @@ -157,19 +273,19 @@ namespace OpenSim.Server.Handlers.Web 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); + m_log.InfoFormat("[WEB SERVICE]: If-Modified-Since is earliar or equal to Last-Modified, from {0}", reqpath); reply["headers"] = replyHeaders; if ("HEAD" == method) { - reply["bin_response_data"] = null; - reply["str_response_string"] = null; + reply.Remove("bin_response_data"); + reply.Remove("str_response_string"); } return reply; } } catch (Exception) { - m_log.ErrorFormat("[WEB SERVICE]: Invalid If-Modified-Since header, ignoring it, from {0}", path); + m_log.WarnFormat("[WEB SERVICE]: Invalid If-Modified-Since header, ignoring it, from {0} - {1}", reqpath, ifdtr); } } replyHeaders["Last-Modified"] = dt.ToString("R"); @@ -192,10 +308,21 @@ namespace OpenSim.Server.Handlers.Web else { if ("account.html" == file) - reply["str_response_string"] = loginPage(null, ""); + { + if (usedSSL) + reply["str_response_string"] = loginPage(null, ""); + else // Force HTTPS by redirecting. + { + reply["int_response_code"] = 200; + reply["content_type"] = "text/html"; + reply["str_response_string"] = "404 Unknown page" + + "" + + ""; + } + } else { - m_log.ErrorFormat("[WEB SERVICE]: Unable to read {0}.", path); + m_log.ErrorFormat("[WEB SERVICE]: Unable to read {0}.", reqpath); reply["int_response_code"] = 404; reply["content_type"] = "text/html"; reply["str_response_string"] = "404 Unknown page" + @@ -205,124 +332,295 @@ namespace OpenSim.Server.Handlers.Web } 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(); + string toke_n_munchie = ""; replyHeaders["Cache-Control"] = "no-cache"; - if ("logout" == doit) - reply["str_response_string"] = loginPage(null, "Logged out."); - else if (("create" == doit) || ("confirm" == doit)) + +/* TODO - + Switch to using prepared SQL statements. + + Actually authenticate them. + Deal with dictionary attacks by slowing down access on password failures etc. + + Regenerate token on authentication. + Store users UUID. + + Invalidate token on logout and password reset. + Logout when invalidating tokens. + + Deal with validation and password reset emails, likely with the same code. +*/ + + if ((null == cookies["toke_n_munchie"]) || (null == fields["toke_n_munchie"]) + || ("" == cookies["toke_n_munchie"].ToString()) || ("" == fields["toke_n_munchie"].ToString())) + toke_n_munchie = newSession(doit, headers, ref fields, ref replyHeaders); + else if (cookies["toke_n_munchie"].ToString() != fields["toke_n_munchie"].ToString()) + errors.Add("Invalid session."); + else { - 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. + toke_n_munchie = cookies["toke_n_munchie"].ToString(); + Hashtable auth = m_auth[toke_n_munchie]; + if (null == auth) + { + errors.Add("Null session."); + m_log.InfoFormat("[WEB SERVICE]: Null session {0} - {1}.", toke_n_munchie, doit); + } else { - if (("create" == doit)) - reply["str_response_string"] = accountCreationPage(body, fields); +// TODO - maybe check if session has expired, but how long should they last? + if (auth["IP"].ToString() != headers["remote_addr"].ToString()) + errors.Add("Wrong IP for session."); 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); + auth["time"] = DateTime.Now; + m_auth[toke_n_munchie] = auth; + m_log.InfoFormat("[WEB SERVICE]: New timestamp for session {0} - {1}.", toke_n_munchie, doit); } } } + + if (0 != errors.Count) + deleteSession(toke_n_munchie, doit, headers, ref fields, ref replyHeaders); + + if (("https://" + m_domain + ":" + m_https_port.ToString() + "/web/account.html") != headers["referer"].ToString()) + errors.Add("Invalid referer."); + + validateName(false, fields, ref errors); + + if ("logout" == doit) + { + deleteSession(toke_n_munchie, doit, headers, ref fields, ref replyHeaders); + errors.Add("Logged out."); + } + else if (("create" == doit) || ("confirm" == doit)) + { + validateName(true, fields, ref errors); + validateEmail(fields, ref errors); + if ("confirm" == doit) + validatePassword(fields, ref errors); + if (0 == errors.Count) + { + // Check the account name doesn't exist yet. + // Which might be tricky, apparently names are not case sensitive on login, but stored with case in the database. + // I confirmed that, can log in no matter what case you use. + // UserAccounts FirstName and LastName fields are both varchar(64) utf8_general_ci. + // The MySQL docs say that the "_ci" bit means comparisons will be case insensitive. So that should work fine. + // No need for prepared SQL here, the names have already been checked. + string[] names = fields["name"].ToString().Split(' '); + long c = m_database.Count("UserAccounts", "FirstName = '" + names[0] + "' AND LastName = '" + names[1] + "'"); + if (0 != c) + errors.Add("Pick a different name."); + else if (("create" == doit)) + reply["str_response_string"] = accountCreationPage(fields, body); + else + reply["str_response_string"] = loggedOnPage(fields, body); + } + else + deleteSession(toke_n_munchie.ToString(), doit, headers, ref fields, ref replyHeaders); + } else if ("cancel" == doit) { - reply["str_response_string"] = loginPage(null, "Cancelled."); + deleteSession(toke_n_munchie.ToString(), doit, headers, ref fields, ref replyHeaders); + errors.Add("Cancelled."); } else if ("list" == doit) { +// TODO - should check if the user is a god before allowing this. List< Hashtable > rows = m_database.Select("UserAccounts", - "CONCAT(FirstName,' ',LastName) as Name,UserTitle as Title,UserLevel as Level,UserFlags as Flags,PrincipalID as UUID", + "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") + "

"; + "account.html?doit=edit&token=" + fields["toke_n_munchie"].ToString(), "UUID") + "

" + button("my account") + "

"; } - else + else if ("login" == doit) { - reply["str_response_string"] = loggedOnPage(body, fields); + if (0 != errors.Count) + deleteSession(toke_n_munchie.ToString(), doit, headers, ref fields, ref replyHeaders); + else + reply["str_response_string"] = loggedOnPage(fields, body); } + else + reply["str_response_string"] = loggedOnPage(fields, body); } - else + else // Not one of our dynamic pages. { - m_log.ErrorFormat("[WEB SERVICE]: No such POST target {0}.", path); + 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 + else // Not one of our handled methods. { - m_log.ErrorFormat("[WEB SERVICE]: UNKNOWN method {0} path {1}.", method, reqpath); + 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 (0 != errors.Count) + { + string b = ""; + foreach (string e in errors) + b = b + "

" + e + "

"; + reply["str_response_string"] = loginPage(fields, b); + } + if ("HEAD" == method) { - reply["bin_response_data"] = null; - reply["str_response_string"] = null; + reply.Remove("bin_response_data"); + reply.Remove("str_response_string"); } return reply; } + // Poor mans Bobby Tables protection. + private string bobbyTables(string n, string v) + { + if ((0 != String.Compare("password", n)) && (0 != String.Compare("psswrd", n))) + { + v = v.Replace("'", "_"); + v = v.Replace("\"", "_"); + v = v.Replace(";", "_"); + v = v.Replace("(", "_"); + v = v.Replace(")", "_"); + } + return v; + } + + private string newSession(string doit, Hashtable headers, ref Hashtable fields, ref Hashtable replyHeaders) + { + // This is a little over the top, but apparently best practices. + string toke_n_munchie = Guid.NewGuid().ToString(); + string salt = Util.Md5Hash(UUID.Random().ToString()); + string hash = Util.Md5Hash(Util.Md5Hash(toke_n_munchie) + ":" + salt); + Hashtable auth = new Hashtable(); + auth["toke_n_munchie"] = toke_n_munchie; + auth["salt"] = salt; + auth["hash"] = hash; + auth["IP"] = headers["remote_addr"].ToString(); + auth["time"] = DateTime.Now; + auth["UUID"] = UUID.Zero; + m_auth[hash] = auth; + // For some odd reason, __Host- only works if "Path=/", and if no "Domain=", per the spec. + //replyHeaders["Set-Cookie"] = "__Host-toke_n_munchie=" + toke_n_munchie + "; HttpOnly; Path=/web/; SameSite=Strict; Secure;"; + replyHeaders["Set-Cookie"] = "toke_n_munchie=" + hash + "; HttpOnly; Path=/web/; SameSite=Strict; Secure;"; + fields["toke_n_munchie"] = hash; + toke_n_munchie = hash; + m_log.InfoFormat("[WEB SERVICE]: {0} New session {1} - {2}.", headers["remote_addr"].ToString(), toke_n_munchie, doit); + return toke_n_munchie; + } + + private void deleteSession(string session, string doit, Hashtable headers, ref Hashtable fields, ref Hashtable replyHeaders) + { + m_log.InfoFormat("[WEB SERVICE]: {0} Deleted session {1} - {2}.", headers["remote_addr"].ToString(), session, doit); + m_auth.Remove(session); + fields.Remove("toke_n_munchie"); + replyHeaders["Set-Cookie"] = "toke_n_munchie=\"\"; HttpOnly; Path=/web/; SameSite=Strict; Secure; expires=Thu, 01 Jan 1970 00:00:00 GMT;"; + } + + private void validateEmail(Hashtable fields, ref List errors) + { + Regex rgxEmail = new Regex("^.+@.+\\..+$"); + if ((null == fields["email"]) || ("" == fields["email"].ToString())) + errors.Add("Please supply an email address."); + else if (!rgxEmail.IsMatch(fields["email"].ToString())) + errors.Add("Please supply a proper email address."); + else if (!Uri.IsWellFormedUriString("mailto:" + fields["email"].ToString(), System.UriKind.Absolute)) + errors.Add("Please supply a valid email address."); + + // Actually lookup the domain name, looking for any sort of record. + string e = fields["email"].ToString().Split('@')[1]; + IPHostEntry ip = null; + try + { + ip = Dns.GetHostEntry(e); + } + catch(Exception) + { + } + if (null == ip) + errors.Add("Can't find that email server, try a different email address."); + } + + private void validateName(bool godCheck, Hashtable fields, ref List errors) + { + Regex rgxName = new Regex("^[a-zA-Z0-9]+$"); + string[] names; + if ((null == fields["name"]) || ("" == fields["name"].ToString())) + errors.Add("Please supply an account name."); + else + { + names = fields["name"].ToString().Split(' '); + if (2 != names.Length) + errors.Add("Names have to be two words."); + // 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) || (31 < names[1].Length)) + errors.Add("First and last names are limited to 31 letters each."); + if ((!rgxName.IsMatch(names[0])) || (!rgxName.IsMatch(names[1]))) + errors.Add("First and last names are limited to letters and digits."); + if (godCheck) + { + // Check and disallow god names, those are done in the console. + bool f = false; + try + { + if (null != m_fullNames[names[0] + " " + names[1]]) + { + f = true; + errors.Add("Pick another name."); + } + } + catch (Exception) + { + } + if (!f) + { + try + { + if (null != m_firstNames[names[0]]) + errors.Add("Pick another first name."); + } + catch (Exception) + { + } + try + { + if (null != m_firstNames[names[1]]) + errors.Add("Pick another last name."); + } + catch (Exception) + { + } + } + } + } + } + } + + private void validatePassword(Hashtable fields, ref List errors) + { + if ((null == fields["password"]) || ("" == fields["password"].ToString())) + errors.Add("Please supply a password."); + else if ((null == fields["psswrd"]) || ("" == fields["psswrd"].ToString())) + errors.Add("Please supply a password."); + else if (0 != String.Compare(fields["psswrd"].ToString(), fields["password"].ToString())) + errors.Add("Passwords are not the same."); + } + private string loginPage(Hashtable fields, string message) { string n = ""; @@ -345,29 +643,29 @@ namespace OpenSim.Server.Handlers.Web + footer(); } - private string accountCreationPage(string body, Hashtable fields) + private string accountCreationPage(Hashtable fields, string message) { return header(ssi["grid"] + " account") + "

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

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

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

" - + form("account.html", fields["token"].ToString(), + + form("account.html", fields["toke_n_munchie"].ToString(), hidden("name", fields["name"].ToString()) // + hidden("UUID", fields["UUID"].ToString()) + text("email", "email", "email", fields["email"].ToString(), 254, true) @@ -386,7 +684,7 @@ namespace OpenSim.Server.Handlers.Web // + button("read") + button("update") ) - + body + + message + footer(); } @@ -444,7 +742,7 @@ namespace OpenSim.Server.Handlers.Web private string form(string action, string token, string form) { - return "
\n" + hidden("token", token) + form + "
\n"; + return "
\n" + hidden("toke_n_munchie", token) + form + "
\n"; } private string hidden(string name, string val) -- cgit v1.1