aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
authoronefang2019-08-18 14:12:21 +1000
committeronefang2019-08-18 14:12:21 +1000
commit8f280962f019d46e0367b29246283a1e34ceb955 (patch)
tree987e5e5d561500288152bc5676f581d528193780
parentAdd HTTPS configs. (diff)
downloadopensim-SC-8f280962f019d46e0367b29246283a1e34ceb955.zip
opensim-SC-8f280962f019d46e0367b29246283a1e34ceb955.tar.gz
opensim-SC-8f280962f019d46e0367b29246283a1e34ceb955.tar.bz2
opensim-SC-8f280962f019d46e0367b29246283a1e34ceb955.tar.xz
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++
-rw-r--r--OpenSim/Server/Handlers/Web/WebServerConnector.cs514
1 files changed, 406 insertions, 108 deletions
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
24 // 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. 24 // 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.
25 private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); 25 private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
26 private IConfigSource m_Config; 26 private IConfigSource m_Config;
27
27 protected MySQLRaw m_database = null; 28 protected MySQLRaw m_database = null;
29
28 private Hashtable mime = new Hashtable(); 30 private Hashtable mime = new Hashtable();
29 private Hashtable ssi = new Hashtable(); 31 private Hashtable ssi = new Hashtable();
30 32
33 private IPAddress m_IP;
34 private IHttpServer m_server;
35 private IHttpServer m_SSLserver = null;
36 private string m_domain = "";
37 private uint m_http_port;
38 private uint m_https_port = 0;
39
40 private Dictionary<string, Hashtable> m_auth = new Dictionary<string, Hashtable>();
41
42 private static Dictionary<string, string> m_firstNames = new Dictionary<string, string>();
43 private static Dictionary<string, string> m_lastNames = new Dictionary<string, string>();
44 private static Dictionary<string, string> m_fullNames = new Dictionary<string, string>();
45
31 public WebServerConnector(IConfigSource config, IHttpServer server, string configName) : base(config, server, configName) 46 public WebServerConnector(IConfigSource config, IHttpServer server, string configName) : base(config, server, configName)
32 { 47 {
33 string dllName = String.Empty; 48 string dllName = String.Empty;
34 string connString = String.Empty; 49 string connString = String.Empty;
35 50
36 m_Config = config; 51 m_Config = config;
52 m_server = server;
53 m_IP = MainServer.Instance.ListenIPAddress;
54 m_http_port = server.Port;
37 55
38 // Try reading the [DatabaseService] section, if it exists 56 // Try reading the [DatabaseService] section, if it exists
39 IConfig dbConfig = m_Config.Configs["DatabaseService"]; 57 IConfig dbConfig = m_Config.Configs["DatabaseService"];
@@ -79,52 +97,104 @@ namespace OpenSim.Server.Handlers.Web
79// mime.Add(".markdown","text/markdown"); 97// mime.Add(".markdown","text/markdown");
80 mime.Add(".txt", "text/plain"); 98 mime.Add(".txt", "text/plain");
81 99
100 // Grab some info.
82 IConfig cfg = m_Config.Configs["GridInfoService"]; 101 IConfig cfg = m_Config.Configs["GridInfoService"];
83 string HomeURI = Util.GetConfigVarFromSections<string>(m_Config, "HomeURI", new string[] { "Startup", "Hypergrid" }, String.Empty); 102 string HomeURI = Util.GetConfigVarFromSections<string>(m_Config, "HomeURI", new string[] { "Startup", "Hypergrid" }, String.Empty);
84 ssi.Add("grid", cfg.GetString("gridname", "my grid")); 103 ssi.Add("grid", cfg.GetString("gridname", "my grid"));
85 ssi.Add("uri", cfg.GetString("login", HomeURI)); 104 ssi.Add("uri", cfg.GetString("login", HomeURI));
86 ssi.Add("version", VersionInfo.Version); 105 ssi.Add("version", VersionInfo.Version);
106 cfg = m_Config.Configs["Const"];
107 m_domain = cfg.GetString("HostName", "localhost");
87 108
109 // Copied from OpenSim/Region/OptionalModules/ViewerSupport/GodNamesModule.cs
110 cfg = m_Config.Configs["GodNames"];
111 if (null != cfg)
112 {
113 m_log.Info("[WEB SERVICE]: Loading god names.");
114 string conf_str = cfg.GetString("FullNames", String.Empty);
115 if (String.Empty != conf_str)
116 {
117 foreach (string strl in conf_str.Split(','))
118 {
119 string strlan = strl.Trim(" \t".ToCharArray());
120 m_log.InfoFormat("[WEB SERVICE]: Adding {0} as a god name", strlan);
121 m_fullNames.Add(strlan, strlan);
122 }
123 }
124
125 conf_str = cfg.GetString("FirstNames", String.Empty);
126 if (String.Empty != conf_str)
127 {
128 foreach (string strl in conf_str.Split(','))
129 {
130 string strlan = strl.Trim(" \t".ToCharArray());
131 m_log.InfoFormat("[WEB SERVICE]: Adding {0} as a god first name", strlan);
132 m_firstNames.Add(strlan, strlan);
133 }
134 }
135
136 conf_str = cfg.GetString("Surnames", String.Empty);
137 if (String.Empty != conf_str)
138 {
139 foreach (string strl in conf_str.Split(','))
140 {
141 string strlan = strl.Trim(" \t".ToCharArray());
142 m_log.InfoFormat("[WEB SERVICE]: Adding {0} as a god last name", strlan);
143 m_lastNames.Add(strlan, strlan);
144 }
145 }
146 }
147 else
148 m_log.Info("[WEB SERVICE]: No god names loaded.");
149
150 // Add the HTTP and HTTPS handlers.
88 server.AddHTTPHandler("/web/", WebRequestHandler); 151 server.AddHTTPHandler("/web/", WebRequestHandler);
89 IConfig networkConfig = m_Config.Configs["Network"]; 152 IConfig networkConfig = m_Config.Configs["Network"];
90 if (null != networkConfig) 153 if (null != networkConfig)
91 { 154 {
92 uint https_port = (uint) networkConfig.GetInt("https_port", 0); 155 m_https_port = (uint) networkConfig.GetInt("https_port", 0);
93 if (0 != https_port) 156 if (0 != m_https_port)
94 { 157 {
95 server = MainServer.GetHttpServer(https_port, null); 158 m_SSLserver = MainServer.GetHttpServer(m_https_port, null);
96 if (null != server) 159 if (null != m_SSLserver)
97 server.AddHTTPHandler("/web/", WebRequestHandler); 160 m_SSLserver.AddHTTPHandler("/web/", WebRequestHandlerSSL);
98 } 161 }
99 } 162 }
100 } 163 }
101 164
165 // 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.
166 private Hashtable WebRequestHandlerSSL(Hashtable request)
167 {
168 return Handler(request, true);
169 }
102 private Hashtable WebRequestHandler(Hashtable request) 170 private Hashtable WebRequestHandler(Hashtable request)
103 { 171 {
104 long locIn = m_database.Count("Presence", "RegionID != '00000000-0000-0000-0000-000000000000'"); // Locals online but not HGing, and HGers in world. 172 return Handler(request, false);
105 long HGin = m_database.Count("Presence", "UserID NOT IN (SELECT PrincipalID FROM UserAccounts)"); // HGers in world. 173 }
106 long locOut = m_database.Count("hg_traveling_data", "GridExternalName != '" + ssi["uri"] + "'"); // Locals that are HGing. 174 private Hashtable Handler(Hashtable request, bool usedSSL)
175 {
107 Hashtable reply = new Hashtable(); 176 Hashtable reply = new Hashtable();
108 Hashtable replyHeaders = new Hashtable(); 177 Hashtable replyHeaders = new Hashtable();
109 ssi["members"] = m_database.Count("UserAccounts").ToString(); 178 Hashtable cookies = new Hashtable();
110 ssi["sims"] = m_database.Count("regions").ToString(); 179 Hashtable fields = new Hashtable();
111 ssi["inworld"] = (locIn - HGin).ToString(); 180 List<string> errors = new List<string>();
112 ssi["outworld"] = locOut.ToString();
113 ssi["hgers"] = HGin.ToString();
114 ssi["month"] = m_database.Count("GridUser", "Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 2419200))").ToString();
115 181
116 string reqpath = (string) request["uri"]; 182 string reqpath = (string) request["uri"];
117 string[] query = (string[]) request["querystringkeys"];
118 Hashtable headers = (Hashtable) request["headers"];
119 string method = (string) request["http-method"]; 183 string method = (string) request["http-method"];
120 string type = (string) request["content-type"]; 184 string type = (string) request["content-type"];
121 string body = (string) request["body"]; 185 string body = (string) request["body"];
186 string[] query = (string[]) request["querystringkeys"];
187 Hashtable headers = (Hashtable) request["headers"];
188 Hashtable vars = (Hashtable) request["requestvars"];
122 string file = reqpath.Remove(0, 5); 189 string file = reqpath.Remove(0, 5);
123 string path = Path.Combine(Util.webDir(), file); 190 string path = Path.Combine(Util.webDir(), file);
124 191
192 m_log.InfoFormat("[WEB SERVICE]: {0} {1} {2} : {3} {4}, server IP {5} content type {6}, body {7}.",
193 headers["remote_addr"].ToString(), method, m_domain, (usedSSL ? m_https_port : m_http_port), reqpath, m_IP, type, body);
194
125 if (! Path.GetFullPath(path).StartsWith(Path.GetFullPath(Util.webDir()))) 195 if (! Path.GetFullPath(path).StartsWith(Path.GetFullPath(Util.webDir())))
126 { 196 {
127 m_log.ErrorFormat("[WEB SERVICE]: INVALID PATH {0} != {1}", Path.GetFullPath(path), Path.GetFullPath(Util.webDir())); 197 m_log.ErrorFormat("[WEB SERVICE]: INVALID PATH {0} != {1}", Path.GetFullPath(path), Path.GetFullPath(Util.webDir()));
128 reply["int_response_code"] = 404; 198 reply["int_response_code"] = 404;
129 reply["content_type"] = "text/html"; 199 reply["content_type"] = "text/html";
130 reply["str_response_string"] = "<html><title>404 Unknown page</title><head></head><body bgcolor=\"black\" text=\"white\" alink=\"red\" link=\"blue\" vlink=\"purple\">" + 200 reply["str_response_string"] = "<html><title>404 Unknown page</title><head></head><body bgcolor=\"black\" text=\"white\" alink=\"red\" link=\"blue\" vlink=\"purple\">" +
@@ -132,13 +202,59 @@ namespace OpenSim.Server.Handlers.Web
132 return reply; 202 return reply;
133 } 203 }
134 204
135 m_log.InfoFormat("[WEB SERVICE]: {0} method path {1} contont type {2} body {3}.", method, reqpath, type, body); 205 long locIn = m_database.Count("Presence", "RegionID != '00000000-0000-0000-0000-000000000000'"); // Locals online but not HGing, and HGers in world.
206 long HGin = m_database.Count("Presence", "UserID NOT IN (SELECT PrincipalID FROM UserAccounts)"); // HGers in world.
207 ssi["hgers"] = HGin.ToString();
208 ssi["inworld"] = (locIn - HGin).ToString();
209 ssi["outworld"] = m_database.Count("hg_traveling_data", "GridExternalName != '" + ssi["uri"] + "'").ToString(); // Locals that are HGing.
210 ssi["members"] = m_database.Count("UserAccounts").ToString();
211 ssi["month"] = m_database.Count("GridUser", "Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 2419200))").ToString();
212 ssi["sims"] = m_database.Count("regions").ToString();
213
136 foreach (DictionaryEntry h in headers) 214 foreach (DictionaryEntry h in headers)
137 m_log.InfoFormat("[WEB SERVICE]: {0} method path {1} header {2} = {3}", method, reqpath, (string) h.Key, (string) h.Value); 215 {
216 if ("cookie" == h.Key.ToString())
217 {
218 string[] cks = h.Value.ToString().Split(';');
219 foreach (String c in cks)
220 {
221 string[] ck = c.Split('=');
222 cookies[ck[0].Trim(' ')] = ck[1].Trim(' ');
223 }
224 }
225 }
226
227 if ("POST" == method)
228 {
229 string[] bdy = body.Split('&');
230 body = "";
231 foreach (String bd in bdy)
232 {
233 string[] b = bd.Split('=');
234 if (b.Length == 0)
235 continue;
236 string n = System.Web.HttpUtility.UrlDecode(b[0]);
237 string v = "";
238 if (b.Length > 1)
239 v = System.Web.HttpUtility.UrlDecode(b[1]);
240 fields[n] = bobbyTables(n, v);
241 body = body + "<p>" + n + " = " + v + "</p>\n";
242 }
243 }
244
138 foreach (String q in query) 245 foreach (String q in query)
139 m_log.InfoFormat("[WEB SERVICE]: {0} method path {1} query {2} value {3}", method, reqpath, q, (string) request[q]); 246 {
247 m_log.InfoFormat("[WEB SERVICE]: {0} {1} query {2} = {3}", method, reqpath, q, (string) request[q]);
248 fields[q] = bobbyTables(q, (string) request[q]);
249 }
250 foreach (DictionaryEntry h in headers)
251 m_log.DebugFormat("[WEB SERVICE]: {0} {1} header {2} = {3}", method, reqpath, (string) h.Key, (string) h.Value);
252 // I dunno what these vars are or where they come from, never actually seen them.
253 foreach (DictionaryEntry h in vars)
254 m_log.InfoFormat("[WEB SERVICE]: {0} {1} var {2} = {3}", method, reqpath, (string) h.Key, (string) h.Value);
140 255
141 reply["int_response_code"] = 200; 256 reply["int_response_code"] = 200;
257
142 if (("GET" == method) || ("HEAD" == method)) 258 if (("GET" == method) || ("HEAD" == method))
143 { 259 {
144 if (File.Exists(path)) 260 if (File.Exists(path))
@@ -157,19 +273,19 @@ namespace OpenSim.Server.Handlers.Web
157 if (0 >= DateTime.Compare(ifdt, dt)) 273 if (0 >= DateTime.Compare(ifdt, dt))
158 { 274 {
159 reply["int_response_code"] = 304; 275 reply["int_response_code"] = 304;
160 m_log.InfoFormat("[WEB SERVICE]: If-Modified-Since is earliar or equal to Last-Modified, from {0}", path); 276 m_log.InfoFormat("[WEB SERVICE]: If-Modified-Since is earliar or equal to Last-Modified, from {0}", reqpath);
161 reply["headers"] = replyHeaders; 277 reply["headers"] = replyHeaders;
162 if ("HEAD" == method) 278 if ("HEAD" == method)
163 { 279 {
164 reply["bin_response_data"] = null; 280 reply.Remove("bin_response_data");
165 reply["str_response_string"] = null; 281 reply.Remove("str_response_string");
166 } 282 }
167 return reply; 283 return reply;
168 } 284 }
169 } 285 }
170 catch (Exception) 286 catch (Exception)
171 { 287 {
172 m_log.ErrorFormat("[WEB SERVICE]: Invalid If-Modified-Since header, ignoring it, from {0}", path); 288 m_log.WarnFormat("[WEB SERVICE]: Invalid If-Modified-Since header, ignoring it, from {0} - {1}", reqpath, ifdtr);
173 } 289 }
174 } 290 }
175 replyHeaders["Last-Modified"] = dt.ToString("R"); 291 replyHeaders["Last-Modified"] = dt.ToString("R");
@@ -192,10 +308,21 @@ namespace OpenSim.Server.Handlers.Web
192 else 308 else
193 { 309 {
194 if ("account.html" == file) 310 if ("account.html" == file)
195 reply["str_response_string"] = loginPage(null, ""); 311 {
312 if (usedSSL)
313 reply["str_response_string"] = loginPage(null, "");
314 else // Force HTTPS by redirecting.
315 {
316 reply["int_response_code"] = 200;
317 reply["content_type"] = "text/html";
318 reply["str_response_string"] = "<html><title>404 Unknown page</title><head>" +
319 "<meta http-equiv=\"refresh\" content=\"0; URL=https://" + m_domain + ":" + m_https_port.ToString() + "/web/account.html\" />" +
320 "</head><body></body></html>";
321 }
322 }
196 else 323 else
197 { 324 {
198 m_log.ErrorFormat("[WEB SERVICE]: Unable to read {0}.", path); 325 m_log.ErrorFormat("[WEB SERVICE]: Unable to read {0}.", reqpath);
199 reply["int_response_code"] = 404; 326 reply["int_response_code"] = 404;
200 reply["content_type"] = "text/html"; 327 reply["content_type"] = "text/html";
201 reply["str_response_string"] = "<html><title>404 Unknown page</title><head></head><body bgcolor=\"black\" text=\"white\" alink=\"red\" link=\"blue\" vlink=\"purple\">" + 328 reply["str_response_string"] = "<html><title>404 Unknown page</title><head></head><body bgcolor=\"black\" text=\"white\" alink=\"red\" link=\"blue\" vlink=\"purple\">" +
@@ -205,124 +332,295 @@ namespace OpenSim.Server.Handlers.Web
205 } 332 }
206 else if ("POST" == method) 333 else if ("POST" == method)
207 { 334 {
208 Hashtable fields = new Hashtable();
209 string[] bdy = body.Split('&');
210 body = "";
211 foreach (String bd in bdy)
212 {
213 string[] b = bd.Split('=');
214 if (b.Length == 0)
215 continue;
216 String n = System.Web.HttpUtility.UrlDecode(b[0]);
217 String v = "";
218 if (b.Length > 1)
219 v = System.Web.HttpUtility.UrlDecode(b[1]);
220 if ((0 != String.Compare("password", n)) && (0 != String.Compare("psswrd", n)))
221 {
222 // Poor mans Bobby Tables protection.
223 v = v.Replace("'", "_");
224 v = v.Replace("\"", "_");
225 v = v.Replace(";", "_");
226 v = v.Replace("(", "_");
227 v = v.Replace(")", "_");
228 }
229 fields[n] = v;
230 body = body + "<p>" + n + " = " + v + "</p>\n";
231 }
232 335
233 if ("account.html" == file) 336 if ("account.html" == file)
234 { 337 {
235 string doit = fields["doit"].ToString(); 338 string doit = fields["doit"].ToString();
339 string toke_n_munchie = "";
236 replyHeaders["Cache-Control"] = "no-cache"; 340 replyHeaders["Cache-Control"] = "no-cache";
237 if ("logout" == doit) 341
238 reply["str_response_string"] = loginPage(null, "Logged out."); 342/* TODO -
239 else if (("create" == doit) || ("confirm" == doit)) 343 Switch to using prepared SQL statements.
344
345 Actually authenticate them.
346 Deal with dictionary attacks by slowing down access on password failures etc.
347
348 Regenerate token on authentication.
349 Store users UUID.
350
351 Invalidate token on logout and password reset.
352 Logout when invalidating tokens.
353
354 Deal with validation and password reset emails, likely with the same code.
355*/
356
357 if ((null == cookies["toke_n_munchie"]) || (null == fields["toke_n_munchie"])
358 || ("" == cookies["toke_n_munchie"].ToString()) || ("" == fields["toke_n_munchie"].ToString()))
359 toke_n_munchie = newSession(doit, headers, ref fields, ref replyHeaders);
360 else if (cookies["toke_n_munchie"].ToString() != fields["toke_n_munchie"].ToString())
361 errors.Add("Invalid session.");
362 else
240 { 363 {
241 Regex rgxName = new Regex("^[a-zA-Z0-9]+$"); 364 toke_n_munchie = cookies["toke_n_munchie"].ToString();
242 Regex rgxEmail = new Regex("^.+@.+\\..+$"); 365 Hashtable auth = m_auth[toke_n_munchie];
243 string[] names = fields["name"].ToString().Split(' '); 366 if (null == auth)
244 if ("" == fields["email"].ToString()) 367 {
245 reply["str_response_string"] = loginPage(fields, "Please supply an email address when creating an account."); 368 errors.Add("Null session.");
246 else if (!rgxEmail.IsMatch(fields["email"].ToString())) 369 m_log.InfoFormat("[WEB SERVICE]: Null session {0} - {1}.", toke_n_munchie, doit);
247 reply["str_response_string"] = loginPage(fields, "Please supply a valid email address when creating an account."); 370 }
248 else if (!Uri.IsWellFormedUriString("mailto:" + fields["email"].ToString(), System.UriKind.Absolute))
249 reply["str_response_string"] = loginPage(fields, "Please supply a valid email address when creating an account.");
250// TODO - the other test to do here is actually lookup the domain name, looking for any sort of record.
251 else if (2 != names.Length)
252 reply["str_response_string"] = loginPage(fields, "Please supply a two word name when creating an account.");
253 // 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.
254 // The userinfo table seems to be obsolete.
255 // Singularity at least limits the total name to 64.
256 // I can't find any limitations on characters allowed, but I only ever see letters and digits used. Case is stored, but not significant.
257 // OpenSims "create user" console command doesn't sanitize it at all, even crashing on some names.
258 else if (31 < names[0].Length)
259 reply["str_response_string"] = loginPage(fields, "First and last names are limited to 31 letters each.");
260 else if (31 < names[1].Length)
261 reply["str_response_string"] = loginPage(fields, "First and last names are limited to 31 letters each.");
262 else if (!rgxName.IsMatch(names[0]))
263 reply["str_response_string"] = loginPage(fields, "First and last names are limited to letters and digits.");
264 else if (!rgxName.IsMatch(names[1]))
265 reply["str_response_string"] = loginPage(fields, "First and last names are limited to letters and digits.");
266// TODO - check and disallow god names, those are done in the console.
267 else 371 else
268 { 372 {
269 if (("create" == doit)) 373// TODO - maybe check if session has expired, but how long should they last?
270 reply["str_response_string"] = accountCreationPage(body, fields); 374 if (auth["IP"].ToString() != headers["remote_addr"].ToString())
375 errors.Add("Wrong IP for session.");
271 else 376 else
272 { 377 {
273 if (0 != String.Compare(fields["psswrd"].ToString(), fields["password"].ToString())) 378 auth["time"] = DateTime.Now;
274 reply["str_response_string"] = loginPage(fields, "Passwords are not the same."); 379 m_auth[toke_n_munchie] = auth;
275 else 380 m_log.InfoFormat("[WEB SERVICE]: New timestamp for session {0} - {1}.", toke_n_munchie, doit);
276 reply["str_response_string"] = loggedOnPage(body, fields);
277 } 381 }
278 } 382 }
279 } 383 }
384
385 if (0 != errors.Count)
386 deleteSession(toke_n_munchie, doit, headers, ref fields, ref replyHeaders);
387
388 if (("https://" + m_domain + ":" + m_https_port.ToString() + "/web/account.html") != headers["referer"].ToString())
389 errors.Add("Invalid referer.");
390
391 validateName(false, fields, ref errors);
392
393 if ("logout" == doit)
394 {
395 deleteSession(toke_n_munchie, doit, headers, ref fields, ref replyHeaders);
396 errors.Add("Logged out.");
397 }
398 else if (("create" == doit) || ("confirm" == doit))
399 {
400 validateName(true, fields, ref errors);
401 validateEmail(fields, ref errors);
402 if ("confirm" == doit)
403 validatePassword(fields, ref errors);
404 if (0 == errors.Count)
405 {
406 // Check the account name doesn't exist yet.
407 // Which might be tricky, apparently names are not case sensitive on login, but stored with case in the database.
408 // I confirmed that, can log in no matter what case you use.
409 // UserAccounts FirstName and LastName fields are both varchar(64) utf8_general_ci.
410 // The MySQL docs say that the "_ci" bit means comparisons will be case insensitive. So that should work fine.
411 // No need for prepared SQL here, the names have already been checked.
412 string[] names = fields["name"].ToString().Split(' ');
413 long c = m_database.Count("UserAccounts", "FirstName = '" + names[0] + "' AND LastName = '" + names[1] + "'");
414 if (0 != c)
415 errors.Add("Pick a different name.");
416 else if (("create" == doit))
417 reply["str_response_string"] = accountCreationPage(fields, body);
418 else
419 reply["str_response_string"] = loggedOnPage(fields, body);
420 }
421 else
422 deleteSession(toke_n_munchie.ToString(), doit, headers, ref fields, ref replyHeaders);
423 }
280 else if ("cancel" == doit) 424 else if ("cancel" == doit)
281 { 425 {
282 reply["str_response_string"] = loginPage(null, "Cancelled."); 426 deleteSession(toke_n_munchie.ToString(), doit, headers, ref fields, ref replyHeaders);
427 errors.Add("Cancelled.");
283 } 428 }
284 else if ("list" == doit) 429 else if ("list" == doit)
285 { 430 {
431// TODO - should check if the user is a god before allowing this.
286 List< Hashtable > rows = m_database.Select("UserAccounts", 432 List< Hashtable > rows = m_database.Select("UserAccounts",
287 "CONCAT(FirstName,' ',LastName) as Name,UserTitle as Title,UserLevel as Level,UserFlags as Flags,PrincipalID as UUID", 433 "CONCAT(FirstName,' ',LastName) as Name,UserTitle as Title,UserLevel as Level,UserFlags as Flags,PrincipalID as UUID",
288 "", "Name"); 434 "", "Name");
289 reply["str_response_string"] = "<html><title>member accounts</title><head></head><body bgcolor=\"black\" text=\"white\" alink=\"red\" link=\"blue\" vlink=\"purple\">" + 435 reply["str_response_string"] = "<html><title>member accounts</title><head></head><body bgcolor=\"black\" text=\"white\" alink=\"red\" link=\"blue\" vlink=\"purple\">" +
290 table(rows, new string[5] {"Name", "Title", "Level", "Flags", "UUID"}, "member accounts", 436 table(rows, new string[5] {"Name", "Title", "Level", "Flags", "UUID"}, "member accounts",
291 "account.html?doit=edit&token=" + fields["token"].ToString(), "UUID") + "<p>" + button("my account") + "</p></body></html>"; 437 "account.html?doit=edit&token=" + fields["toke_n_munchie"].ToString(), "UUID") + "<p>" + button("my account") + "</p></body></html>";
292 } 438 }
293 else 439 else if ("login" == doit)
294 { 440 {
295 reply["str_response_string"] = loggedOnPage(body, fields); 441 if (0 != errors.Count)
442 deleteSession(toke_n_munchie.ToString(), doit, headers, ref fields, ref replyHeaders);
443 else
444 reply["str_response_string"] = loggedOnPage(fields, body);
296 } 445 }
446 else
447 reply["str_response_string"] = loggedOnPage(fields, body);
297 } 448 }
298 else 449 else // Not one of our dynamic pages.
299 { 450 {
300 m_log.ErrorFormat("[WEB SERVICE]: No such POST target {0}.", path); 451 m_log.ErrorFormat("[WEB SERVICE]: No such POST target {0}.", path);
301 reply["int_response_code"] = 404; 452 reply["int_response_code"] = 404;
302 reply["content_type"] = "text/html"; 453 reply["content_type"] = "text/html";
303 reply["str_response_string"] = "<html><title>404 Unknown page</title><head></head><body bgcolor=\"black\" text=\"white\" alink=\"red\" link=\"blue\" vlink=\"purple\">" + 454 reply["str_response_string"] = "<html><title>404 Unknown page</title><head></head><body bgcolor=\"black\" text=\"white\" alink=\"red\" link=\"blue\" vlink=\"purple\">" +
304 "404 error, can't find the " + reqpath + " page.<p>&nbsp;</p></body></html>"; 455 "404 error, can't find the " + reqpath + " page.<p>&nbsp;</p></body></html>";
305 } 456 }
306 } 457 }
307 else 458 else // Not one of our handled methods.
308 { 459 {
309 m_log.ErrorFormat("[WEB SERVICE]: UNKNOWN method {0} path {1}.", method, reqpath); 460 m_log.ErrorFormat("[WEB SERVICE]: UNKNOWN method {0} path {1}.", method, reqpath);
310 reply["int_response_code"] = 404; 461 reply["int_response_code"] = 404;
311 reply["content_type"] = "text/html"; 462 reply["content_type"] = "text/html";
312 reply["str_response_string"] = "<html><title>Unknown method</title><head></head><body bgcolor=\"black\" text=\"white\" alink=\"red\" link=\"blue\" vlink=\"purple\">" + 463 reply["str_response_string"] = "<html><title>Unknown method</title><head></head><body bgcolor=\"black\" text=\"white\" alink=\"red\" link=\"blue\" vlink=\"purple\">" +
313 "HUH! For " + reqpath + " page.<p>&nbsp;</p></body></html>"; 464 "HUH! For " + reqpath + " page.<p>&nbsp;</p></body></html>";
314 } 465 }
315 466
316 m_log.Info("[WEB SERVICE]: ");
317 reply["headers"] = replyHeaders; 467 reply["headers"] = replyHeaders;
468 if (0 != errors.Count)
469 {
470 string b = "";
471 foreach (string e in errors)
472 b = b + "<p>" + e + "</p>";
473 reply["str_response_string"] = loginPage(fields, b);
474 }
475
318 if ("HEAD" == method) 476 if ("HEAD" == method)
319 { 477 {
320 reply["bin_response_data"] = null; 478 reply.Remove("bin_response_data");
321 reply["str_response_string"] = null; 479 reply.Remove("str_response_string");
322 } 480 }
323 return reply; 481 return reply;
324 } 482 }
325 483
484 // Poor mans Bobby Tables protection.
485 private string bobbyTables(string n, string v)
486 {
487 if ((0 != String.Compare("password", n)) && (0 != String.Compare("psswrd", n)))
488 {
489 v = v.Replace("'", "_");
490 v = v.Replace("\"", "_");
491 v = v.Replace(";", "_");
492 v = v.Replace("(", "_");
493 v = v.Replace(")", "_");
494 }
495 return v;
496 }
497
498 private string newSession(string doit, Hashtable headers, ref Hashtable fields, ref Hashtable replyHeaders)
499 {
500 // This is a little over the top, but apparently best practices.
501 string toke_n_munchie = Guid.NewGuid().ToString();
502 string salt = Util.Md5Hash(UUID.Random().ToString());
503 string hash = Util.Md5Hash(Util.Md5Hash(toke_n_munchie) + ":" + salt);
504 Hashtable auth = new Hashtable();
505 auth["toke_n_munchie"] = toke_n_munchie;
506 auth["salt"] = salt;
507 auth["hash"] = hash;
508 auth["IP"] = headers["remote_addr"].ToString();
509 auth["time"] = DateTime.Now;
510 auth["UUID"] = UUID.Zero;
511 m_auth[hash] = auth;
512 // For some odd reason, __Host- only works if "Path=/", and if no "Domain=", per the spec.
513 //replyHeaders["Set-Cookie"] = "__Host-toke_n_munchie=" + toke_n_munchie + "; HttpOnly; Path=/web/; SameSite=Strict; Secure;";
514 replyHeaders["Set-Cookie"] = "toke_n_munchie=" + hash + "; HttpOnly; Path=/web/; SameSite=Strict; Secure;";
515 fields["toke_n_munchie"] = hash;
516 toke_n_munchie = hash;
517 m_log.InfoFormat("[WEB SERVICE]: {0} New session {1} - {2}.", headers["remote_addr"].ToString(), toke_n_munchie, doit);
518 return toke_n_munchie;
519 }
520
521 private void deleteSession(string session, string doit, Hashtable headers, ref Hashtable fields, ref Hashtable replyHeaders)
522 {
523 m_log.InfoFormat("[WEB SERVICE]: {0} Deleted session {1} - {2}.", headers["remote_addr"].ToString(), session, doit);
524 m_auth.Remove(session);
525 fields.Remove("toke_n_munchie");
526 replyHeaders["Set-Cookie"] = "toke_n_munchie=\"\"; HttpOnly; Path=/web/; SameSite=Strict; Secure; expires=Thu, 01 Jan 1970 00:00:00 GMT;";
527 }
528
529 private void validateEmail(Hashtable fields, ref List<string> errors)
530 {
531 Regex rgxEmail = new Regex("^.+@.+\\..+$");
532 if ((null == fields["email"]) || ("" == fields["email"].ToString()))
533 errors.Add("Please supply an email address.");
534 else if (!rgxEmail.IsMatch(fields["email"].ToString()))
535 errors.Add("Please supply a proper email address.");
536 else if (!Uri.IsWellFormedUriString("mailto:" + fields["email"].ToString(), System.UriKind.Absolute))
537 errors.Add("Please supply a valid email address.");
538
539 // Actually lookup the domain name, looking for any sort of record.
540 string e = fields["email"].ToString().Split('@')[1];
541 IPHostEntry ip = null;
542 try
543 {
544 ip = Dns.GetHostEntry(e);
545 }
546 catch(Exception)
547 {
548 }
549 if (null == ip)
550 errors.Add("Can't find that email server, try a different email address.");
551 }
552
553 private void validateName(bool godCheck, Hashtable fields, ref List<string> errors)
554 {
555 Regex rgxName = new Regex("^[a-zA-Z0-9]+$");
556 string[] names;
557 if ((null == fields["name"]) || ("" == fields["name"].ToString()))
558 errors.Add("Please supply an account name.");
559 else
560 {
561 names = fields["name"].ToString().Split(' ');
562 if (2 != names.Length)
563 errors.Add("Names have to be two words.");
564 // 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.
565 // The userinfo table seems to be obsolete.
566 // Singularity at least limits the total name to 64.
567 // I can't find any limitations on characters allowed, but I only ever see letters and digits used. Case is stored, but not significant.
568 // OpenSims "create user" console command doesn't sanitize it at all, even crashing on some names.
569 else
570 {
571 if ((31 < names[0].Length) || (31 < names[1].Length))
572 errors.Add("First and last names are limited to 31 letters each.");
573 if ((!rgxName.IsMatch(names[0])) || (!rgxName.IsMatch(names[1])))
574 errors.Add("First and last names are limited to letters and digits.");
575 if (godCheck)
576 {
577 // Check and disallow god names, those are done in the console.
578 bool f = false;
579 try
580 {
581 if (null != m_fullNames[names[0] + " " + names[1]])
582 {
583 f = true;
584 errors.Add("Pick another name.");
585 }
586 }
587 catch (Exception)
588 {
589 }
590 if (!f)
591 {
592 try
593 {
594 if (null != m_firstNames[names[0]])
595 errors.Add("Pick another first name.");
596 }
597 catch (Exception)
598 {
599 }
600 try
601 {
602 if (null != m_firstNames[names[1]])
603 errors.Add("Pick another last name.");
604 }
605 catch (Exception)
606 {
607 }
608 }
609 }
610 }
611 }
612 }
613
614 private void validatePassword(Hashtable fields, ref List<string> errors)
615 {
616 if ((null == fields["password"]) || ("" == fields["password"].ToString()))
617 errors.Add("Please supply a password.");
618 else if ((null == fields["psswrd"]) || ("" == fields["psswrd"].ToString()))
619 errors.Add("Please supply a password.");
620 else if (0 != String.Compare(fields["psswrd"].ToString(), fields["password"].ToString()))
621 errors.Add("Passwords are not the same.");
622 }
623
326 private string loginPage(Hashtable fields, string message) 624 private string loginPage(Hashtable fields, string message)
327 { 625 {
328 string n = ""; 626 string n = "";
@@ -345,29 +643,29 @@ namespace OpenSim.Server.Handlers.Web
345 + footer(); 643 + footer();
346 } 644 }
347 645
348 private string accountCreationPage(string body, Hashtable fields) 646 private string accountCreationPage(Hashtable fields, string message)
349 { 647 {
350 return header(ssi["grid"] + " account") 648 return header(ssi["grid"] + " account")
351 + "<h1>Creating " + ssi["grid"] + " account for " + fields["name"].ToString() + "</h1>" 649 + "<h1>Creating " + ssi["grid"] + " account for " + fields["name"].ToString() + "</h1>"
352 + form("account.html", fields["token"].ToString(), 650 + form("account.html", fields["toke_n_munchie"].ToString(),
353 hidden("name", fields["name"].ToString()) 651 hidden("name", fields["name"].ToString())
354 + hidden("psswrd", fields["password"].ToString()) 652 + hidden("psswrd", fields["password"].ToString())
355 + text("email", "An email will be sent to", "email", fields["email"].ToString(), 254, true) 653 + text("email", "An email will be sent to", "email", fields["email"].ToString(), 254, false)
356 + "&nbsp;to validate it, please double check this." 654 + "&nbsp;to validate it, please double check this."
357 + text("password", "Re-enter your password", "password", "", 0, true) 655 + text("password", "Re-enter your password", "password", "", 0, false)
358 + "Warning, the limit on password length is set by your viewer, some can't handle longer than 16 characters." 656 + "Warning, the limit on password length is set by your viewer, some can't handle longer than 16 characters."
359 + button("confirm") 657 + button("confirm")
360 + button("cancel") 658 + button("cancel")
361 ) 659 )
362 + body 660 + message
363 + footer(); 661 + footer();
364 } 662 }
365 663
366 private string loggedOnPage(string body, Hashtable fields) 664 private string loggedOnPage(Hashtable fields, string message)
367 { 665 {
368 return header(ssi["grid"] + " account") 666 return header(ssi["grid"] + " account")
369 + "<h1>" + ssi["grid"] + " account for " + fields["name"].ToString() + "</h1>" 667 + "<h1>" + ssi["grid"] + " account for " + fields["name"].ToString() + "</h1>"
370 + form("account.html", fields["token"].ToString(), 668 + form("account.html", fields["toke_n_munchie"].ToString(),
371 hidden("name", fields["name"].ToString()) 669 hidden("name", fields["name"].ToString())
372// + hidden("UUID", fields["UUID"].ToString()) 670// + hidden("UUID", fields["UUID"].ToString())
373 + text("email", "email", "email", fields["email"].ToString(), 254, true) 671 + text("email", "email", "email", fields["email"].ToString(), 254, true)
@@ -386,7 +684,7 @@ namespace OpenSim.Server.Handlers.Web
386// + button("read") 684// + button("read")
387 + button("update") 685 + button("update")
388 ) 686 )
389 + body 687 + message
390 + footer(); 688 + footer();
391 } 689 }
392 690
@@ -444,7 +742,7 @@ namespace OpenSim.Server.Handlers.Web
444 742
445 private string form(string action, string token, string form) 743 private string form(string action, string token, string form)
446 { 744 {
447 return " <form action=\"" + action + "\" method=\"POST\">\n" + hidden("token", token) + form + " </form>\n"; 745 return " <form action=\"" + action + "\" method=\"POST\">\n" + hidden("toke_n_munchie", token) + form + " </form>\n";
448 } 746 }
449 747
450 private string hidden(string name, string val) 748 private string hidden(string name, string val)