diff options
author | onefang | 2019-08-18 14:12:21 +1000 |
---|---|---|
committer | onefang | 2019-08-18 14:12:21 +1000 |
commit | 8f280962f019d46e0367b29246283a1e34ceb955 (patch) | |
tree | 987e5e5d561500288152bc5676f581d528193780 | |
parent | Add HTTPS configs. (diff) | |
download | opensim-SC-8f280962f019d46e0367b29246283a1e34ceb955.zip opensim-SC-8f280962f019d46e0367b29246283a1e34ceb955.tar.gz opensim-SC-8f280962f019d46e0367b29246283a1e34ceb955.tar.bz2 opensim-SC-8f280962f019d46e0367b29246283a1e34ceb955.tar.xz |
Various additions to the web account manager.
Track if we are accessed via HTTP or HTTPS, and the server name.
Track cookies.
HEAD method.
Various security clean ups.
Force HTTPS for account.html.
Poor mans Bobby Tables protection.
Security token.
Validate inputs. Looking up the DNS records for email domain name.
Don't allow creation of accounts with god names, leave that for the
console. Check if created user name exists already. Double check the
passwords and emails.
Error messages on dynamic pages.
Various clean ups.
TODO++
-rw-r--r-- | OpenSim/Server/Handlers/Web/WebServerConnector.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) |