aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/OpenSim/Server/Handlers/Web/WebServerConnector.cs
diff options
context:
space:
mode:
Diffstat (limited to 'OpenSim/Server/Handlers/Web/WebServerConnector.cs')
-rw-r--r--OpenSim/Server/Handlers/Web/WebServerConnector.cs878
1 files changed, 878 insertions, 0 deletions
diff --git a/OpenSim/Server/Handlers/Web/WebServerConnector.cs b/OpenSim/Server/Handlers/Web/WebServerConnector.cs
new file mode 100644
index 0000000..5389c6e
--- /dev/null
+++ b/OpenSim/Server/Handlers/Web/WebServerConnector.cs
@@ -0,0 +1,878 @@
1using System;
2using System.Collections;
3using System.Collections.Generic;
4using System.IO;
5using System.Net;
6using System.Net.Mail;
7using System.Reflection;
8using System.Security;
9using System.Text;
10using System.Text.RegularExpressions;
11using log4net;
12using Nini.Config;
13using OpenMetaverse;
14using OpenMetaverse.StructuredData;
15using OpenSim.Data.MySQL;
16using OpenSim.Framework;
17using OpenSim.Framework.Servers;
18using OpenSim.Framework.Servers.HttpServer;
19using OpenSim.Server.Handlers.Base;
20
21namespace OpenSim.Server.Handlers.Web
22{
23 public class WebServerConnector : ServiceConnector
24 {
25 // 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.
26 private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
27 private IConfigSource m_Config;
28
29 protected MySQLRaw m_database = null;
30
31 private Hashtable mime = new Hashtable();
32 private Hashtable ssi = new Hashtable();
33
34 private IPAddress m_IP;
35 private IHttpServer m_server;
36// private IHttpServer m_SSLserver = null;
37 private string m_domain = "";
38 private uint m_http_port;
39// private uint m_https_port = 0;
40
41 private Dictionary<string, Hashtable> m_auth = new Dictionary<string, Hashtable>();
42
43 private static Dictionary<string, string> m_firstNames = new Dictionary<string, string>();
44 private static Dictionary<string, string> m_lastNames = new Dictionary<string, string>();
45 private static Dictionary<string, string> m_fullNames = new Dictionary<string, string>();
46
47/* TODO - shelved for now, rewrite it in Lua for lighttpd after the release.
48 private string m_SMTP_server;
49 private string m_SMTP_port;
50 private string m_SMTP_user;
51 private string m_SMTP_password;
52 private string m_email_from;
53 private SmtpClient m_smtp;
54*/
55
56 public WebServerConnector(IConfigSource config, IHttpServer server, string configName) : base(config, server, configName)
57 {
58 string dllName = String.Empty;
59 string connString = String.Empty;
60
61 m_Config = config;
62 m_server = server;
63 m_IP = MainServer.Instance.ListenIPAddress;
64 m_http_port = server.Port;
65
66 // Try reading the [DatabaseService] section, if it exists
67 IConfig dbConfig = m_Config.Configs["DatabaseService"];
68 if (dbConfig != null)
69 {
70 if (dllName == String.Empty)
71 dllName = dbConfig.GetString("StorageProvider", String.Empty);
72 if (connString == String.Empty)
73 connString = dbConfig.GetString("ConnectionString", String.Empty);
74 }
75 if (dllName.Equals(String.Empty))
76 throw new Exception("No StorageProvider configured");
77
78//// TODO - Should do the plugin thing to pick between database backends.
79//// Or not, we are all using MariaDB anyway.
80// m_Database = LoadPlugin<SQLGenericHandler>(dllName, new Object[] { connString });
81
82 m_database = new MySQLRaw(connString);
83
84 mime.Add(".gz", "application/gzip");
85 mime.Add(".js", "application/javascript");
86 mime.Add(".json", "application/json");
87 mime.Add(".pdf", "application/pdf");
88 mime.Add(".rtf", "application/rtf");
89 mime.Add(".zip", "application/zip");
90 mime.Add(".xz", "application/x-xz");
91 mime.Add(".gif", "image/gif");
92 mime.Add(".png", "image/png");
93 mime.Add(".jp2", "image/jp2");
94 mime.Add(".jpg2", "image/jp2");
95 mime.Add(".jpe", "image/jpeg");
96 mime.Add(".jpg", "image/jpeg");
97 mime.Add(".jpeg", "image/jpeg");
98 mime.Add(".svg", "image/svg+xml");
99 mime.Add(".svgz", "image/svg+xml");
100 mime.Add(".tif", "image/tiff");
101 mime.Add(".tiff", "image/tiff");
102 mime.Add(".css", "text/css");
103 mime.Add(".html", "text/html");
104 mime.Add(".htm", "text/html");
105 mime.Add(".shtml", "text/html");
106// mime.Add(".md", "text/markdown");
107// mime.Add(".markdown","text/markdown");
108 mime.Add(".txt", "text/plain");
109
110 // Grab some info.
111 IConfig cfg = m_Config.Configs["GridInfoService"];
112 string HomeURI = Util.GetConfigVarFromSections<string>(m_Config, "HomeURI", new string[] { "Startup", "Hypergrid" }, String.Empty);
113 ssi.Add("grid", cfg.GetString("gridname", "my grid"));
114 ssi.Add("uri", cfg.GetString("login", HomeURI));
115 ssi.Add("version", VersionInfo.Version);
116 cfg = m_Config.Configs["Const"];
117 m_domain = cfg.GetString("HostName", "localhost");
118
119/* TODO - shelved for now, rewrite it in Lua for lighttpd after the release.
120 // Copied from OpenSim/Region/OptionalModules/ViewerSupport/GodNamesModule.cs
121 cfg = m_Config.Configs["GodNames"];
122 if (null != cfg)
123 {
124 m_log.Info("[WEB SERVICE]: Loading god names.");
125 string conf_str = cfg.GetString("FullNames", 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 name", strlan);
132 m_fullNames.Add(strlan, strlan);
133 }
134 }
135
136 conf_str = cfg.GetString("FirstNames", 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 first name", strlan);
143 m_firstNames.Add(strlan, strlan);
144 }
145 }
146
147 conf_str = cfg.GetString("Surnames", String.Empty);
148 if (String.Empty != conf_str)
149 {
150 foreach (string strl in conf_str.Split(','))
151 {
152 string strlan = strl.Trim(" \t".ToCharArray());
153 m_log.InfoFormat("[WEB SERVICE]: Adding {0} as a god last name", strlan);
154 m_lastNames.Add(strlan, strlan);
155 }
156 }
157 }
158 else
159 m_log.Info("[WEB SERVICE]: No god names loaded.");
160
161 // Add the email client.
162 cfg = m_Config.Configs["SMTP"];
163 if (null != cfg)
164 {
165 m_log.Info("[WEB SERVICE]: Loading email configuration.");
166 m_SMTP_server = cfg.GetString("SMTP_SERVER_HOSTNAME", "127.0.0.1");
167 m_SMTP_port = cfg.GetString("SMTP_SERVER_PORT", "25");
168 m_SMTP_user = cfg.GetString("SMTP_SERVER_LOGIN", "");
169 m_SMTP_password = cfg.GetString("SMTP_SERVER_PASSWORD", "");
170 m_email_from = cfg.GetString("host_domain_header_from", "grid@localhost");
171
172 m_smtp = new SmtpClient
173 {
174 Host = m_SMTP_server,
175 Port = Convert.ToInt16(m_SMTP_port),
176 EnableSsl = false,
177 DeliveryMethod = SmtpDeliveryMethod.Network,
178 Credentials = new NetworkCredential(m_SMTP_user, m_SMTP_password),
179 Timeout = 20000
180 };
181 }
182*/
183
184 // Add the HTTP and HTTPS handlers.
185 server.AddHTTPHandler("/web/", WebRequestHandler);
186/* TODO - shelved for now, rewrite it in Lua for lighttpd after the release.
187 IConfig networkConfig = m_Config.Configs["Network"];
188 if (null != networkConfig)
189 {
190 m_https_port = (uint) networkConfig.GetInt("https_port", 0);
191 if (0 != m_https_port)
192 {
193 m_SSLserver = MainServer.GetHttpServer(m_https_port, null);
194 if (null != m_SSLserver)
195 m_SSLserver.AddHTTPHandler("/web/", WebRequestHandlerSSL);
196 }
197 }
198*/
199 }
200
201 // 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.
202/* TODO - shelved for now, rewrite it in Lua for lighttpd after the release.
203 private Hashtable WebRequestHandlerSSL(Hashtable request)
204 {
205 return Handler(request, true);
206 }
207*/
208 private Hashtable WebRequestHandler(Hashtable request)
209 {
210 return Handler(request, false);
211 }
212 private Hashtable Handler(Hashtable request, bool usedSSL)
213 {
214 Hashtable reply = new Hashtable();
215 Hashtable replyHeaders = new Hashtable();
216 Hashtable cookies = new Hashtable();
217 Hashtable fields = new Hashtable();
218 List<string> errors = new List<string>();
219
220 string reqpath = (string) request["uri"];
221 string method = (string) request["http-method"];
222 string type = (string) request["content-type"];
223 string body = (string) request["body"];
224 string[] query = (string[]) request["querystringkeys"];
225 Hashtable headers = (Hashtable) request["headers"];
226 Hashtable vars = (Hashtable) request["requestvars"];
227 string file = reqpath.Remove(0, 5);
228 string path = Path.Combine(Util.webDir(), file);
229
230// m_log.InfoFormat("[WEB SERVICE]: {0} {1} {2} : {3} {4}, server IP {5} content type {6}, body {7}.",
231// headers["remote_addr"].ToString(), method, m_domain, (usedSSL ? m_https_port : m_http_port), reqpath, m_IP, type, body);
232 m_log.InfoFormat("[WEB SERVICE]: {0} {1} {2} : {3} {4}, server IP {5} content type {6}, body {7}.",
233 headers["remote_addr"].ToString(), method, m_domain, m_http_port, reqpath, m_IP, type, body);
234
235 if (! Path.GetFullPath(path).StartsWith(Path.GetFullPath(Util.webDir())))
236 {
237 m_log.ErrorFormat("[WEB SERVICE]: INVALID PATH {0} != {1}", Path.GetFullPath(path), Path.GetFullPath(Util.webDir()));
238 reply["int_response_code"] = 404;
239 reply["content_type"] = "text/html";
240 reply["str_response_string"] = "<html><title>404 Unknown page</title><head><link rel=\"shortcut icon\" href=\"SledjHamrIconSmall.png\"></head><body bgcolor=\"black\" text=\"white\" alink=\"red\" link=\"blue\" vlink=\"purple\">" +
241 "404 error, can't find the " + reqpath + " page.<p>&nbsp;</p></body></html>";
242 return reply;
243 }
244
245 long locIn = m_database.Count("Presence", "RegionID != '00000000-0000-0000-0000-000000000000'"); // Locals online but not HGing, and HGers in world.
246 long HGin = m_database.Count("Presence", "UserID NOT IN (SELECT PrincipalID FROM UserAccounts)"); // HGers in world.
247 ssi["hgers"] = HGin.ToString();
248 ssi["inworld"] = (locIn - HGin).ToString();
249 ssi["outworld"] = m_database.Count("hg_traveling_data", "GridExternalName != '" + ssi["uri"] + "'").ToString(); // Locals that are HGing.
250 ssi["members"] = m_database.Count("UserAccounts").ToString();
251 ssi["sims"] = m_database.Count("regions").ToString();
252 ssi["onlineSims"] = m_database.Count("regions", "sizeX != 0").ToString();
253 ssi["varRegions"] = m_database.Count("regions", "sizeX > 256 or sizeY > 256").ToString();
254 ssi["singleSims"] = m_database.Count("regions", "sizeX = 256 and sizeY = 256").ToString();
255 ssi["offlineSims"] = m_database.Count("regions", "sizeX = 0").ToString();
256
257 // Calculate grid area.
258 long simSize = 0;
259 List< Hashtable > rows = m_database.Select("regions", "sizeX,sizeY", "sizeX != 0", "");
260 foreach (Hashtable row in rows)
261 {
262 simSize = simSize + Convert.ToInt32(row["sizeX"]) * Convert.ToInt32(row["sizeY"]);
263 }
264 ssi["simsSize"] = simSize.ToString();
265
266 // Count local and HG visitors for the last 30 and 60 days.
267 HGin = m_database.Count("GridUser", "Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 2419200))");
268 rows = m_database.Join("GridUser", "GridUser.UserID", "INNER JOIN UserAccounts ON GridUser.UserID = UserAccounts.PrincipalID",
269 "Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 2419200))", "");
270 locIn = rows.Count;
271 ssi["locDay30"] = locIn.ToString();
272 ssi["day30"] = HGin.ToString();
273 HGin = HGin - locIn;
274 ssi["HGday30"] = HGin.ToString();
275
276 HGin = m_database.Count("GridUser", "Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 4838400))");
277 rows = m_database.Join("GridUser", "GridUser.UserID", "INNER JOIN UserAccounts ON GridUser.UserID = UserAccounts.PrincipalID",
278 "Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 4838400))", "");
279 locIn = rows.Count;
280 ssi["locDay60"] = locIn.ToString();
281 ssi["day60"] = HGin.ToString();
282 HGin = HGin - locIn;
283 ssi["HGday60"] = HGin.ToString();
284
285 foreach (DictionaryEntry h in headers)
286 {
287 if ("cookie" == h.Key.ToString())
288 {
289 string[] cks = h.Value.ToString().Split(';');
290 foreach (String c in cks)
291 {
292 string[] ck = c.Split('=');
293 cookies[ck[0].Trim(' ')] = ck[1].Trim(' ');
294 }
295 }
296 }
297
298 if ("POST" == method)
299 {
300 string[] bdy = body.Split('&');
301 body = "";
302 foreach (String bd in bdy)
303 {
304 string[] b = bd.Split('=');
305 if (b.Length == 0)
306 continue;
307 string n = System.Web.HttpUtility.UrlDecode(b[0]);
308 string v = "";
309 if (b.Length > 1)
310 v = System.Web.HttpUtility.UrlDecode(b[1]);
311 fields[n] = bobbyTables(n, v);
312 body = body + "<p>" + n + " = " + v + "</p>\n";
313 }
314 }
315
316 foreach (String q in query)
317 {
318// m_log.InfoFormat("[WEB SERVICE]: {0} {1} query {2} = {3}", method, reqpath, q, (string) request[q]);
319 fields[q] = bobbyTables(q, (string) request[q]);
320 }
321// foreach (DictionaryEntry h in headers)
322// m_log.DebugFormat("[WEB SERVICE]: {0} {1} header {2} = {3}", method, reqpath, (string) h.Key, (string) h.Value);
323 // I dunno what these vars are or where they come from, never actually seen them.
324 // Ah, viewers send them, and they seem to be identical to the query that viewers also send.
325// foreach (DictionaryEntry h in vars)
326// m_log.InfoFormat("[WEB SERVICE]: {0} {1} var {2} = {3}", method, reqpath, (string) h.Key, (string) h.Value);
327
328 reply["int_response_code"] = 200;
329
330 if (("GET" == method) || ("HEAD" == method))
331 {
332 if (File.Exists(path))
333 {
334 DateTime dt = File.GetLastWriteTimeUtc(path);
335 string m = (string) mime[Path.GetExtension(path).ToLower()];
336 reply["content_type"] = m;
337 if ((null == m) || ("text/" != m.Substring(0, 5)))
338 {
339 string ifdtr = (string) headers["if-modified-since"];
340 if (null != ifdtr)
341 {
342 try
343 {
344 DateTime ifdt = DateTime.Parse(ifdtr, System.Globalization.CultureInfo.InvariantCulture);
345 if (0 >= DateTime.Compare(ifdt, dt))
346 {
347 reply["int_response_code"] = 304;
348 m_log.InfoFormat("[WEB SERVICE]: If-Modified-Since is earlier or equal to Last-Modified, from {0}", reqpath);
349 reply["headers"] = replyHeaders;
350 if ("HEAD" == method)
351 {
352 reply.Remove("bin_response_data");
353 reply.Remove("str_response_string");
354 }
355 return reply;
356 }
357 }
358 catch (Exception)
359 {
360 m_log.WarnFormat("[WEB SERVICE]: Invalid If-Modified-Since header, ignoring it, from {0} - {1}", reqpath, ifdtr);
361 }
362 }
363 replyHeaders["Last-Modified"] = dt.ToString("R");
364 reply["bin_response_data"] = File.ReadAllBytes(path);
365 }
366 else
367 {
368 replyHeaders["Cache-Control"] = "no-cache";
369 StreamReader csr = File.OpenText(path);
370 string content = csr.ReadToEnd();
371 // Slow and wasteful, but I'm expecting only tiny web files, not accessed very often.
372 foreach (DictionaryEntry v in ssi)
373 {
374 content = content.Replace("<!--#echo var=\"" + ((string) v.Key) + "\" -->", (string) v.Value);
375 }
376 reply["str_response_string"] = content;
377 csr.Close();
378 }
379 }
380/* TODO - shelved for now, rewrite it in Lua for lighttpd after the release.
381 else
382 {
383 if ("account.html" == file)
384 {
385 if (usedSSL)
386 reply["str_response_string"] = loginPage(null, "");
387 else // Force HTTPS by redirecting.
388 {
389 reply["int_response_code"] = 200;
390 reply["content_type"] = "text/html";
391 reply["str_response_string"] = "<html><title>404 Unknown page</title><head>" +
392 "<meta http-equiv=\"refresh\" content=\"0; URL=https://" + m_domain + ":" + m_https_port.ToString() + "/web/account.html\" />" +
393 "</head><body></body></html>";
394 }
395 }
396 else
397 {
398 m_log.ErrorFormat("[WEB SERVICE]: Unable to read {0}.", reqpath);
399 reply["int_response_code"] = 404;
400 reply["content_type"] = "text/html";
401 reply["str_response_string"] = "<html><title>404 Unknown page</title><head><link rel=\"shortcut icon\" href=\"SledjHamrIconSmall.png\"></head><body bgcolor=\"black\" text=\"white\" alink=\"red\" link=\"blue\" vlink=\"purple\">" +
402 "404 error, can't find the " + reqpath + " page.<p>&nbsp;</p></body></html>";
403 }
404 }
405*/
406 }
407 else if ("POST" == method)
408 {
409
410// if ("account.html" == file)
411// {
412// string doit = fields["doit"].ToString();
413// string toke_n_munchie = "";
414// replyHeaders["Cache-Control"] = "no-cache";
415
416/* TODO -
417 Switch to using prepared SQL statements.
418
419 Actually authenticate them.
420 Deal with dictionary attacks by slowing down access on password failures etc.
421
422 Regenerate token on authentication.
423 Store users UUID.
424
425 Invalidate token on logout and password reset.
426 Logout when invalidating tokens.
427
428 Deal with validation and password reset emails, likely with the same code.
429
430 Deal with editing yourself.
431 Deal with editing others, but only as god.
432*/
433/* TODO - shelved for now, rewrite it in Lua for lighttpd after the release.
434 if ((null == cookies["toke_n_munchie"]) || (null == fields["toke_n_munchie"])
435 || ("" == cookies["toke_n_munchie"].ToString()) || ("" == fields["toke_n_munchie"].ToString()))
436 toke_n_munchie = newSession(doit, headers, ref fields, ref replyHeaders);
437 else if (cookies["toke_n_munchie"].ToString() != fields["toke_n_munchie"].ToString())
438 errors.Add("Invalid session.");
439 else
440 {
441 toke_n_munchie = cookies["toke_n_munchie"].ToString();
442 Hashtable auth = m_auth[toke_n_munchie];
443 if (null == auth)
444 {
445 errors.Add("Null session.");
446 m_log.InfoFormat("[WEB SERVICE]: Null session {0} - {1}.", toke_n_munchie, doit);
447 }
448 else
449 {
450// TODO - maybe check if session has expired, but how long should they last?
451 if (auth["IP"].ToString() != headers["remote_addr"].ToString())
452 errors.Add("Wrong IP for session.");
453 else
454 {
455 auth["time"] = DateTime.Now;
456 m_auth[toke_n_munchie] = auth;
457 m_log.InfoFormat("[WEB SERVICE]: New timestamp for session {0} - {1}.", toke_n_munchie, doit);
458 }
459 }
460 }
461
462 if (0 != errors.Count)
463 deleteSession(toke_n_munchie, doit, headers, ref fields, ref replyHeaders);
464
465 if (("https://" + m_domain + ":" + m_https_port.ToString() + "/web/account.html") != headers["referer"].ToString())
466 errors.Add("Invalid referer.");
467
468 // Include a check for god names if we are creating a new account.
469 string[] names = validateName(("create" == doit) || ("confirm" == doit), fields, ref errors);
470
471 if ("logout" == doit)
472 {
473 deleteSession(toke_n_munchie, doit, headers, ref fields, ref replyHeaders);
474 errors.Add("Logged out.");
475 }
476 else if (("create" == doit) || ("confirm" == doit))
477 {
478 validateEmail(fields, ref errors);
479 if ("confirm" == doit)
480 validatePassword(fields, ref errors);
481 if (0 == errors.Count)
482 {
483 // Check the account name doesn't exist yet.
484 // Which might be tricky, apparently names are not case sensitive on login, but stored with case in the database.
485 // I confirmed that, can log in no matter what case you use.
486 // UserAccounts FirstName and LastName fields are both varchar(64) utf8_general_ci.
487 // The MySQL docs say that the "_ci" bit means comparisons will be case insensitive. So that should work fine.
488 // No need for prepared SQL here, the names have already been checked.
489 if (0 != m_database.Count("UserAccounts", "FirstName = '" + names[0] + "' AND LastName = '" + names[1] + "'"))
490 errors.Add("Pick a different name.");
491 else if (("create" == doit))
492 reply["str_response_string"] = accountCreationPage(fields, body);
493 else
494 {
495 var fromAddress = new MailAddress(m_email_from, (string) ssi["grid"]);
496 var toAddress = new MailAddress((string) fields["email"], (string) fields["name"]);
497 using (var message = new MailMessage(fromAddress, toAddress)
498 {
499 Subject = "validation email",
500 Body = "Should be a linky around here somewhere."
501 })
502 {
503 m_smtp.Send(message);
504 }
505 reply["str_response_string"] = loggedOnPage(fields, body);
506 }
507 }
508 else
509 deleteSession(toke_n_munchie.ToString(), doit, headers, ref fields, ref replyHeaders);
510 }
511 else if ("cancel" == doit)
512 {
513 deleteSession(toke_n_munchie.ToString(), doit, headers, ref fields, ref replyHeaders);
514 errors.Add("Cancelled.");
515 }
516 else if ("list" == doit)
517 {
518// TODO - should check if the user is a god before allowing this.
519 List< Hashtable > rows = m_database.Select("UserAccounts",
520 "CONCAT(FirstName,' ',LastName) as Name,UserTitle as Title,UserLevel as Level,UserFlags as Flags,PrincipalID as UUID",
521 "", "Name");
522 reply["str_response_string"] = "<html><title>member accounts</title><head><link rel=\"shortcut icon\" href=\"SledjHamrIconSmall.png\"></head><body bgcolor=\"black\" text=\"white\" alink=\"red\" link=\"blue\" vlink=\"purple\">" +
523 table(rows, new string[5] {"Name", "Title", "Level", "Flags", "UUID"}, "member accounts",
524 "account.html?doit=edit&token=" + fields["toke_n_munchie"].ToString(), "UUID") + "<p>" + button("my account") + "</p></body></html>";
525 }
526 else if ("login" == doit)
527 {
528 if (0 != errors.Count)
529 deleteSession(toke_n_munchie.ToString(), doit, headers, ref fields, ref replyHeaders);
530 else
531 reply["str_response_string"] = loggedOnPage(fields, body);
532 }
533 else
534 reply["str_response_string"] = loggedOnPage(fields, body);
535*/
536// }
537// else // Not one of our dynamic pages.
538// {
539 m_log.ErrorFormat("[WEB SERVICE]: No such POST target {0}.", path);
540 reply["int_response_code"] = 404;
541 reply["content_type"] = "text/html";
542 reply["str_response_string"] = "<html><title>404 Unknown page</title><head><link rel=\"shortcut icon\" href=\"SledjHamrIconSmall.png\"></head><body bgcolor=\"black\" text=\"white\" alink=\"red\" link=\"blue\" vlink=\"purple\">" +
543 "404 error, can't find the " + reqpath + " page.<p>&nbsp;</p></body></html>";
544// }
545 }
546 else // Not one of our handled methods.
547 {
548 m_log.ErrorFormat("[WEB SERVICE]: UNKNOWN method {0} path {1}.", method, reqpath);
549 reply["int_response_code"] = 404;
550 reply["content_type"] = "text/html";
551 reply["str_response_string"] = "<html><title>Unknown method</title><head><link rel=\"shortcut icon\" href=\"SledjHamrIconSmall.png\"></head><body bgcolor=\"black\" text=\"white\" alink=\"red\" link=\"blue\" vlink=\"purple\">" +
552 "HUH! For " + reqpath + " page.<p>&nbsp;</p></body></html>";
553 }
554
555 reply["headers"] = replyHeaders;
556 if (0 != errors.Count)
557 {
558 string b = "";
559 foreach (string e in errors)
560 b = b + "<p>" + e + "</p>";
561 reply["str_response_string"] = loginPage(fields, b);
562 }
563
564 if ("HEAD" == method)
565 {
566 reply.Remove("bin_response_data");
567 reply.Remove("str_response_string");
568 }
569 return reply;
570 }
571
572 // Poor mans Bobby Tables protection.
573 private string bobbyTables(string n, string v)
574 {
575 if ((0 != String.Compare("password", n)) && (0 != String.Compare("psswrd", n)))
576 {
577 v = v.Replace("'", "_");
578 v = v.Replace("\"", "_");
579 v = v.Replace(";", "_");
580 v = v.Replace("(", "_");
581 v = v.Replace(")", "_");
582 }
583 return v;
584 }
585
586 private string newSession(string doit, Hashtable headers, ref Hashtable fields, ref Hashtable replyHeaders)
587 {
588 // This is a little over the top, but apparently best practices.
589 string toke_n_munchie = Guid.NewGuid().ToString();
590 string salt = Util.Md5Hash(UUID.Random().ToString());
591 string hash = Util.Md5Hash(Util.Md5Hash(toke_n_munchie) + ":" + salt);
592 Hashtable auth = new Hashtable();
593 auth["toke_n_munchie"] = toke_n_munchie;
594 auth["salt"] = salt;
595 auth["hash"] = hash;
596 auth["IP"] = headers["remote_addr"].ToString();
597 auth["time"] = DateTime.Now;
598 auth["UUID"] = UUID.Zero;
599 m_auth[hash] = auth;
600 // For some odd reason, __Host- only works if "Path=/", and if no "Domain=", per the spec.
601 //replyHeaders["Set-Cookie"] = "__Host-toke_n_munchie=" + toke_n_munchie + "; HttpOnly; Path=/web/; SameSite=Strict; Secure;";
602 replyHeaders["Set-Cookie"] = "toke_n_munchie=" + hash + "; HttpOnly; Path=/web/; SameSite=Strict; Secure;";
603 fields["toke_n_munchie"] = hash;
604 toke_n_munchie = hash;
605 m_log.InfoFormat("[WEB SERVICE]: {0} New session {1} - {2}.", headers["remote_addr"].ToString(), toke_n_munchie, doit);
606 return toke_n_munchie;
607 }
608
609 private void deleteSession(string session, string doit, Hashtable headers, ref Hashtable fields, ref Hashtable replyHeaders)
610 {
611 m_log.InfoFormat("[WEB SERVICE]: {0} Deleted session {1} - {2}.", headers["remote_addr"].ToString(), session, doit);
612 m_auth.Remove(session);
613 fields.Remove("toke_n_munchie");
614 replyHeaders["Set-Cookie"] = "toke_n_munchie=\"\"; HttpOnly; Path=/web/; SameSite=Strict; Secure; expires=Thu, 01 Jan 1970 00:00:00 GMT;";
615 }
616
617 private void validateEmail(Hashtable fields, ref List<string> errors)
618 {
619 Regex rgxEmail = new Regex("^.+@.+\\..+$");
620 if ((null == fields["email"]) || ("" == fields["email"].ToString()))
621 errors.Add("Please supply an email address.");
622 else if (!rgxEmail.IsMatch(fields["email"].ToString()))
623 errors.Add("Please supply a proper email address.");
624 else if (!Uri.IsWellFormedUriString("mailto:" + fields["email"].ToString(), System.UriKind.Absolute))
625 errors.Add("Please supply a valid email address.");
626
627 // Actually lookup the domain name, looking for any sort of record.
628 string e = fields["email"].ToString().Split('@')[1];
629 IPHostEntry ip = null;
630 try
631 {
632 ip = Dns.GetHostEntry(e);
633 }
634 catch(Exception)
635 {
636 }
637 if (null == ip)
638 errors.Add("Can't find that email server, try a different email address.");
639 }
640
641 private string[] validateName(bool godCheck, Hashtable fields, ref List<string> errors)
642 {
643 Regex rgxName = new Regex("^[a-zA-Z0-9]+$");
644 string[] names = {"", ""};
645 if ((null == fields["name"]) || ("" == fields["name"].ToString()))
646 errors.Add("Please supply an account name.");
647 else
648 {
649 names = fields["name"].ToString().Split(' ');
650 if (2 != names.Length)
651 errors.Add("Names have to be two words.");
652 // 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.
653 // The userinfo table seems to be obsolete.
654 // Singularity at least limits the total name to 64.
655 // I can't find any limitations on characters allowed, but I only ever see letters and digits used. Case is stored, but not significant.
656 // OpenSims "create user" console command doesn't sanitize it at all, even crashing on some names.
657 else
658 {
659 if ((31 < names[0].Length) || (31 < names[1].Length))
660 errors.Add("First and last names are limited to 31 letters each.");
661 if ((!rgxName.IsMatch(names[0])) || (!rgxName.IsMatch(names[1])))
662 errors.Add("First and last names are limited to letters and digits.");
663 if (godCheck)
664 {
665 // Check and disallow god names, those are done in the console.
666 bool f = false;
667 try
668 {
669 if (null != m_fullNames[names[0] + " " + names[1]])
670 {
671 f = true;
672 errors.Add("Pick another name.");
673 }
674 }
675 catch (Exception)
676 {
677 }
678 if (!f)
679 {
680 try
681 {
682 if (null != m_firstNames[names[0]])
683 errors.Add("Pick another first name.");
684 }
685 catch (Exception)
686 {
687 }
688 try
689 {
690 if (null != m_firstNames[names[1]])
691 errors.Add("Pick another last name.");
692 }
693 catch (Exception)
694 {
695 }
696 }
697 }
698 }
699 }
700 return names;
701 }
702
703 private void validatePassword(Hashtable fields, ref List<string> errors)
704 {
705 if ((null == fields["password"]) || ("" == fields["password"].ToString()))
706 errors.Add("Please supply a password.");
707 else if ((null == fields["psswrd"]) || ("" == fields["psswrd"].ToString()))
708 errors.Add("Please supply a password.");
709 else if (0 != String.Compare(fields["psswrd"].ToString(), fields["password"].ToString()))
710 errors.Add("Passwords are not the same.");
711 }
712
713 private string loginPage(Hashtable fields, string message)
714 {
715 string n = "";
716 string e = "";
717 if (null != fields)
718 {
719 n = fields["name"].ToString();
720 e = fields["email"].ToString();
721 }
722 return header(ssi["grid"] + " account")
723 + form("account.html", "",
724 text("text", "name", "name", n, 63, true)
725 + text("email", "email", "email", e, 254, false)
726 + text("password", "password", "password", "", 0, true)
727 + "Warning, the limit on password length is set by your viewer, some can't handle longer than 16 characters."
728 + button("create")
729 + button("login")
730 )
731 + "<p>" + message + "</p>"
732 + footer();
733 }
734
735 private string accountCreationPage(Hashtable fields, string message)
736 {
737 return header(ssi["grid"] + " account")
738 + "<h1>Creating " + ssi["grid"] + " account for " + fields["name"].ToString() + "</h1>"
739 + form("account.html", fields["toke_n_munchie"].ToString(),
740 hidden("name", fields["name"].ToString())
741 + hidden("psswrd", fields["password"].ToString())
742 + text("email", "An email will be sent to", "email", fields["email"].ToString(), 254, false)
743 + "&nbsp;to validate it, please double check this."
744 + text("password", "Re-enter your password", "password", "", 0, false)
745 + "Warning, the limit on password length is set by your viewer, some can't handle longer than 16 characters."
746 + button("confirm")
747 + button("cancel")
748 )
749 + message
750 + footer();
751 }
752
753 private string loggedOnPage(Hashtable fields, string message)
754 {
755 return header(ssi["grid"] + " account")
756 + "<h1>" + ssi["grid"] + " account for " + fields["name"].ToString() + "</h1>"
757 + form("account.html", fields["toke_n_munchie"].ToString(),
758 hidden("name", fields["name"].ToString())
759// + hidden("UUID", fields["UUID"].ToString())
760 + text("email", "email", "email", fields["email"].ToString(), 254, true)
761 + text("password", "password", "password", "", 0, false)
762 + "Warning, the limit on password length is set by your viewer, some can't handle longer than 16 characters."
763// + text("title", "text", "title", fields["title"].ToString(), 64, false)
764 + select("type", "type",
765 option("", false)
766 + option("approved", true)
767 + option("disabled", false)
768 + option("god", false)
769 )
770 + button("delete")
771 + button("list")
772 + button("logout")
773// + button("read")
774 + button("update")
775 )
776 + message
777 + footer();
778 }
779
780 private string header(string title)
781 {
782 return "<html>\n <head>\n <title>" + title + "</title>\n </head>\n <body>\n";
783 }
784
785 private string table(List< Hashtable > rows, string[] fields, string caption, string URL, string id)
786 {
787 string tbl = "<table border=\"1\"><caption>" + caption + "</caption>";
788 bool head = true;
789 string address = "";
790 string addrend = "";
791 foreach (Hashtable row in rows)
792 {
793 if (0 == fields.Length)
794 {
795 int c = 0;
796 foreach (DictionaryEntry r in row)
797 c++;
798 fields = new string[c];
799 c = 0;
800 foreach (DictionaryEntry r in row)
801 fields[c++] = (string) r.Key;
802 }
803 string line = "<tr>";
804 address = "";
805 if ("" != URL)
806 {
807 address = "<a href=\"" + URL;
808 addrend = "</a>";
809 }
810 if ("" != id)
811 address = address + "&" + id + "=" + row[id] + "\">";
812 if (head)
813 {
814 foreach (string s in fields)
815 line = line + "<th>" + s + "</th>";
816 tbl = tbl + line + "</tr>\n";
817 head = false;
818 }
819 line = "<tr>";
820 foreach (string s in fields)
821 {
822 if (s == id)
823 line = line + "<td>" + address + row[s] + addrend + "</td>";
824 else
825 line = line + "<td>" + row[s] + "</td>";
826 }
827 tbl = tbl + line + "</tr>\n";
828 }
829 return tbl + "</table>";
830 }
831
832 private string form(string action, string token, string form)
833 {
834 return " <form action=\"" + action + "\" method=\"POST\">\n" + hidden("toke_n_munchie", token) + form + " </form>\n";
835 }
836
837 private string hidden(string name, string val)
838 {
839 return " <input type=\"hidden\" name=\"" + name + "\" value=\"" + val + "\">\n";
840 }
841
842 private string text(string type, string title, string name, string val, int max, bool required)
843 {
844 string extra = "";
845 if (0 < max)
846 extra = extra + " maxlength=\"" + max.ToString() + "\"";
847 if (required)
848 extra = extra + " required";
849 if ("" != val)
850 val = "value=\"" + val + "\"";
851 return " <p>" + title + " : <input type=\"" + type + "\" name=\"" + name + "\"" + val + extra + "></p>\n";
852 }
853
854 private string select(string title, string name, string options)
855 {
856 return " <p>" + title + " : \n <select name=\"" + name + "\">\n" + options + " </select>\n </p>\n";
857 }
858
859 private string option(string title, bool selected)
860 {
861 string sel = "";
862 if (selected)
863 sel = " selected";
864 return " <option value=\"" + title + "\"" + sel + ">" + title + "</option>\n";
865 }
866
867 private string button(string title)
868 {
869 return " <button type=\"submit\" name=\"doit\" value=\"" + title + "\">" + title + "</button>\n";
870 }
871
872 private string footer()
873 {
874 return " </body>\n</html>\n";
875 }
876
877 }
878}