diff options
Diffstat (limited to '')
-rw-r--r-- | OpenSim/Server/Handlers/Web/WebServerConnector.cs | 514 |
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> </p></body></html>"; | 455 | "404 error, can't find the " + reqpath + " page.<p> </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> </p></body></html>"; | 464 | "HUH! For " + reqpath + " page.<p> </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 | + " to validate it, please double check this." | 654 | + " 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) |