diff options
Diffstat (limited to 'OpenSim/Server/Handlers/Web/WebServerConnector.cs')
-rw-r--r-- | OpenSim/Server/Handlers/Web/WebServerConnector.cs | 878 |
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 @@ | |||
1 | using System; | ||
2 | using System.Collections; | ||
3 | using System.Collections.Generic; | ||
4 | using System.IO; | ||
5 | using System.Net; | ||
6 | using System.Net.Mail; | ||
7 | using System.Reflection; | ||
8 | using System.Security; | ||
9 | using System.Text; | ||
10 | using System.Text.RegularExpressions; | ||
11 | using log4net; | ||
12 | using Nini.Config; | ||
13 | using OpenMetaverse; | ||
14 | using OpenMetaverse.StructuredData; | ||
15 | using OpenSim.Data.MySQL; | ||
16 | using OpenSim.Framework; | ||
17 | using OpenSim.Framework.Servers; | ||
18 | using OpenSim.Framework.Servers.HttpServer; | ||
19 | using OpenSim.Server.Handlers.Base; | ||
20 | |||
21 | namespace 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> </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> </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> </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> </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 | + " 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 | } | ||