diff options
author | gareth | 2007-03-22 10:11:15 +0000 |
---|---|---|
committer | gareth | 2007-03-22 10:11:15 +0000 |
commit | 7daa3955bc3a1918e40962851f9e8d38597a245e (patch) | |
tree | bee3e1372a7eed0c1b220a8a49f7bee7d29a6b91 /OpenSim.RegionServer/UserServer | |
parent | Load XML for neighbourinfo from grid (diff) | |
download | opensim-SC_OLD-7daa3955bc3a1918e40962851f9e8d38597a245e.zip opensim-SC_OLD-7daa3955bc3a1918e40962851f9e8d38597a245e.tar.gz opensim-SC_OLD-7daa3955bc3a1918e40962851f9e8d38597a245e.tar.bz2 opensim-SC_OLD-7daa3955bc3a1918e40962851f9e8d38597a245e.tar.xz |
brought zircon branch into trunk
Diffstat (limited to 'OpenSim.RegionServer/UserServer')
-rw-r--r-- | OpenSim.RegionServer/UserServer/LocalUserProfileManager.cs | 76 | ||||
-rw-r--r-- | OpenSim.RegionServer/UserServer/LoginServer.cs | 414 |
2 files changed, 490 insertions, 0 deletions
diff --git a/OpenSim.RegionServer/UserServer/LocalUserProfileManager.cs b/OpenSim.RegionServer/UserServer/LocalUserProfileManager.cs new file mode 100644 index 0000000..83e340b --- /dev/null +++ b/OpenSim.RegionServer/UserServer/LocalUserProfileManager.cs | |||
@@ -0,0 +1,76 @@ | |||
1 | using System; | ||
2 | using System.Collections.Generic; | ||
3 | using System.Collections; | ||
4 | using System.Text; | ||
5 | using OpenSim.Framework.User; | ||
6 | using OpenSim.Framework.Grid; | ||
7 | using OpenSim.Framework.Inventory; | ||
8 | using OpenSim.Framework.Interfaces; | ||
9 | using libsecondlife; | ||
10 | |||
11 | namespace OpenSim.UserServer | ||
12 | { | ||
13 | class LocalUserProfileManager : UserProfileManager | ||
14 | { | ||
15 | private IGridServer _gridServer; | ||
16 | |||
17 | public LocalUserProfileManager(IGridServer gridServer) | ||
18 | { | ||
19 | _gridServer = gridServer; | ||
20 | } | ||
21 | |||
22 | public override void InitUserProfiles() | ||
23 | { | ||
24 | // TODO: need to load from database | ||
25 | } | ||
26 | |||
27 | public override void CustomiseResponse(ref System.Collections.Hashtable response, UserProfile theUser) | ||
28 | { | ||
29 | uint circode = (uint)response["circuit_code"]; | ||
30 | theUser.AddSimCircuit(circode, LLUUID.Random()); | ||
31 | response["home"] = "{'region_handle':[r" + (997 * 256).ToString() + ",r" + (996 * 256).ToString() + "], 'position':[r" + theUser.homepos.X.ToString() + ",r" + theUser.homepos.Y.ToString() + ",r" + theUser.homepos.Z.ToString() + "], 'look_at':[r" + theUser.homelookat.X.ToString() + ",r" + theUser.homelookat.Y.ToString() + ",r" + theUser.homelookat.Z.ToString() + "]}"; | ||
32 | response["sim_port"] = OpenSimRoot.Instance.Cfg.IPListenPort; | ||
33 | response["sim_ip"] = OpenSimRoot.Instance.Cfg.IPListenAddr; | ||
34 | response["region_y"] = (Int32)996 * 256; | ||
35 | response["region_x"] = (Int32)997* 256; | ||
36 | |||
37 | string first; | ||
38 | string last; | ||
39 | if (response.Contains("first")) | ||
40 | { | ||
41 | first = (string)response["first"]; | ||
42 | } | ||
43 | else | ||
44 | { | ||
45 | first = "test"; | ||
46 | } | ||
47 | |||
48 | if (response.Contains("last")) | ||
49 | { | ||
50 | last = (string)response["last"]; | ||
51 | } | ||
52 | else | ||
53 | { | ||
54 | last = "User"; | ||
55 | } | ||
56 | |||
57 | ArrayList InventoryList = (ArrayList)response["inventory-skeleton"]; | ||
58 | Hashtable Inventory1 = (Hashtable)InventoryList[0]; | ||
59 | |||
60 | Login _login = new Login(); | ||
61 | //copy data to login object | ||
62 | _login.First = first; | ||
63 | _login.Last = last; | ||
64 | _login.Agent = new LLUUID((string)response["agent_id"]) ; | ||
65 | _login.Session = new LLUUID((string)response["session_id"]); | ||
66 | _login.BaseFolder = null; | ||
67 | _login.InventoryFolder = new LLUUID((string)Inventory1["folder_id"]); | ||
68 | |||
69 | //working on local computer if so lets add to the gridserver's list of sessions? | ||
70 | if (OpenSimRoot.Instance.GridServers.GridServer.GetName() == "Local") | ||
71 | { | ||
72 | ((LocalGridBase)this._gridServer).AddNewSession(_login); | ||
73 | } | ||
74 | } | ||
75 | } | ||
76 | } | ||
diff --git a/OpenSim.RegionServer/UserServer/LoginServer.cs b/OpenSim.RegionServer/UserServer/LoginServer.cs new file mode 100644 index 0000000..86b098a --- /dev/null +++ b/OpenSim.RegionServer/UserServer/LoginServer.cs | |||
@@ -0,0 +1,414 @@ | |||
1 | /* | ||
2 | * Copyright (c) OpenSim project, http://sim.opensecondlife.org/ | ||
3 | * | ||
4 | * Redistribution and use in source and binary forms, with or without | ||
5 | * modification, are permitted provided that the following conditions are met: | ||
6 | * * Redistributions of source code must retain the above copyright | ||
7 | * notice, this list of conditions and the following disclaimer. | ||
8 | * * Redistributions in binary form must reproduce the above copyright | ||
9 | * notice, this list of conditions and the following disclaimer in the | ||
10 | * documentation and/or other materials provided with the distribution. | ||
11 | * * Neither the name of the <organization> nor the | ||
12 | * names of its contributors may be used to endorse or promote products | ||
13 | * derived from this software without specific prior written permission. | ||
14 | * | ||
15 | * THIS SOFTWARE IS PROVIDED BY <copyright holder> ``AS IS'' AND ANY | ||
16 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
17 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
18 | * DISCLAIMED. IN NO EVENT SHALL <copyright holder> BE LIABLE FOR ANY | ||
19 | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
20 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
21 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND | ||
22 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
23 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
24 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
25 | * | ||
26 | */ | ||
27 | |||
28 | using Nwc.XmlRpc; | ||
29 | using System; | ||
30 | using System.IO; | ||
31 | using System.Net; | ||
32 | using System.Net.Sockets; | ||
33 | using System.Text; | ||
34 | using System.Text.RegularExpressions; | ||
35 | using System.Threading; | ||
36 | using System.Collections; | ||
37 | using System.Security.Cryptography; | ||
38 | using System.Xml; | ||
39 | using libsecondlife; | ||
40 | using OpenSim; | ||
41 | using OpenSim.Framework.Interfaces; | ||
42 | using OpenSim.Framework.Grid; | ||
43 | using OpenSim.Framework.Inventory; | ||
44 | using OpenSim.Framework.User; | ||
45 | using OpenSim.Framework.Utilities; | ||
46 | |||
47 | namespace OpenSim.UserServer | ||
48 | { | ||
49 | |||
50 | /// <summary> | ||
51 | /// When running in local (default) mode , handles client logins. | ||
52 | /// </summary> | ||
53 | public class LoginServer : LoginService , IUserServer | ||
54 | { | ||
55 | private IGridServer _gridServer; | ||
56 | private ushort _loginPort = 8080; | ||
57 | public IPAddress clientAddress = IPAddress.Loopback; | ||
58 | public IPAddress remoteAddress = IPAddress.Any; | ||
59 | private Socket loginServer; | ||
60 | private int NumClients; | ||
61 | private string _defaultResponse; | ||
62 | private bool userAccounts = false; | ||
63 | private string _mpasswd; | ||
64 | private bool _needPasswd = false; | ||
65 | private LocalUserProfileManager userManager; | ||
66 | |||
67 | public LoginServer(IGridServer gridServer) | ||
68 | { | ||
69 | _gridServer = gridServer; | ||
70 | } | ||
71 | |||
72 | // InitializeLogin: initialize the login | ||
73 | private void InitializeLogin() | ||
74 | { | ||
75 | this._needPasswd = false; | ||
76 | //read in default response string | ||
77 | StreamReader SR; | ||
78 | string lines; | ||
79 | SR = File.OpenText("new-login.dat"); | ||
80 | |||
81 | //lines=SR.ReadLine(); | ||
82 | |||
83 | while (!SR.EndOfStream) | ||
84 | { | ||
85 | lines = SR.ReadLine(); | ||
86 | _defaultResponse += lines; | ||
87 | //lines = SR.ReadLine(); | ||
88 | } | ||
89 | SR.Close(); | ||
90 | this._mpasswd = EncodePassword("testpass"); | ||
91 | |||
92 | userManager = new LocalUserProfileManager(this._gridServer); | ||
93 | userManager.InitUserProfiles(); | ||
94 | userManager.SetKeys("", "", "", "Welcome to OpenSim"); | ||
95 | |||
96 | loginServer = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); | ||
97 | loginServer.Bind(new IPEndPoint(remoteAddress, _loginPort)); | ||
98 | loginServer.Listen(1); | ||
99 | } | ||
100 | |||
101 | public void Startup() | ||
102 | { | ||
103 | this.InitializeLogin(); | ||
104 | Thread runLoginProxy = new Thread(new ThreadStart(RunLogin)); | ||
105 | runLoginProxy.IsBackground = true; | ||
106 | runLoginProxy.Start(); | ||
107 | } | ||
108 | |||
109 | private void RunLogin() | ||
110 | { | ||
111 | Console.WriteLine("Starting Login Server"); | ||
112 | try | ||
113 | { | ||
114 | for (; ; ) | ||
115 | { | ||
116 | Socket client = loginServer.Accept(); | ||
117 | IPEndPoint clientEndPoint = (IPEndPoint)client.RemoteEndPoint; | ||
118 | |||
119 | |||
120 | NetworkStream networkStream = new NetworkStream(client); | ||
121 | StreamReader networkReader = new StreamReader(networkStream); | ||
122 | StreamWriter networkWriter = new StreamWriter(networkStream); | ||
123 | |||
124 | try | ||
125 | { | ||
126 | LoginRequest(networkReader, networkWriter); | ||
127 | } | ||
128 | catch (Exception e) | ||
129 | { | ||
130 | Console.WriteLine(e.Message); | ||
131 | } | ||
132 | |||
133 | networkWriter.Close(); | ||
134 | networkReader.Close(); | ||
135 | networkStream.Close(); | ||
136 | |||
137 | client.Close(); | ||
138 | |||
139 | // send any packets queued for injection | ||
140 | |||
141 | } | ||
142 | } | ||
143 | catch (Exception e) | ||
144 | { | ||
145 | Console.WriteLine(e.Message); | ||
146 | Console.WriteLine(e.StackTrace); | ||
147 | } | ||
148 | } | ||
149 | |||
150 | // ProxyLogin: proxy a login request | ||
151 | private void LoginRequest(StreamReader reader, StreamWriter writer) | ||
152 | { | ||
153 | lock (this) | ||
154 | { | ||
155 | string line; | ||
156 | int contentLength = 0; | ||
157 | // read HTTP header | ||
158 | do | ||
159 | { | ||
160 | // read one line of the header | ||
161 | line = reader.ReadLine(); | ||
162 | |||
163 | // check for premature EOF | ||
164 | if (line == null) | ||
165 | throw new Exception("EOF in client HTTP header"); | ||
166 | |||
167 | // look for Content-Length | ||
168 | Match match = (new Regex(@"Content-Length: (\d+)$")).Match(line); | ||
169 | if (match.Success) | ||
170 | contentLength = Convert.ToInt32(match.Groups[1].Captures[0].ToString()); | ||
171 | } while (line != ""); | ||
172 | |||
173 | // read the HTTP body into a buffer | ||
174 | char[] content = new char[contentLength]; | ||
175 | reader.Read(content, 0, contentLength); | ||
176 | |||
177 | if (this.userAccounts) | ||
178 | { | ||
179 | //ask the UserProfile Manager to process the request | ||
180 | string reply = this.userManager.ParseXMLRPC(new String(content)); | ||
181 | // forward the XML-RPC response to the client | ||
182 | writer.WriteLine("HTTP/1.0 200 OK"); | ||
183 | writer.WriteLine("Content-type: text/xml"); | ||
184 | writer.WriteLine(); | ||
185 | writer.WriteLine(reply); | ||
186 | } | ||
187 | else | ||
188 | { | ||
189 | //handle ourselves | ||
190 | XmlRpcRequest request = (XmlRpcRequest)(new XmlRpcRequestDeserializer()).Deserialize(new String(content)); | ||
191 | if (request.MethodName == "login_to_simulator") | ||
192 | { | ||
193 | this.ProcessXmlRequest(request, writer); | ||
194 | } | ||
195 | else | ||
196 | { | ||
197 | XmlRpcResponse PresenceErrorResp = new XmlRpcResponse(); | ||
198 | Hashtable PresenceErrorRespData = new Hashtable(); | ||
199 | PresenceErrorRespData["reason"] = "XmlRequest"; ; | ||
200 | PresenceErrorRespData["message"] = "Unknown Rpc request"; | ||
201 | PresenceErrorRespData["login"] = "false"; | ||
202 | PresenceErrorResp.Value = PresenceErrorRespData; | ||
203 | string reply = Regex.Replace(XmlRpcResponseSerializer.Singleton.Serialize(PresenceErrorResp), " encoding=\"utf-16\"", ""); | ||
204 | writer.WriteLine("HTTP/1.0 200 OK"); | ||
205 | writer.WriteLine("Content-type: text/xml"); | ||
206 | writer.WriteLine(); | ||
207 | writer.WriteLine(reply); | ||
208 | } | ||
209 | } | ||
210 | } | ||
211 | } | ||
212 | |||
213 | public bool ProcessXmlRequest(XmlRpcRequest request, StreamWriter writer) | ||
214 | { | ||
215 | Hashtable requestData = (Hashtable)request.Params[0]; | ||
216 | string first; | ||
217 | string last; | ||
218 | string passwd; | ||
219 | LLUUID Agent; | ||
220 | LLUUID Session; | ||
221 | |||
222 | //get login name | ||
223 | if (requestData.Contains("first")) | ||
224 | { | ||
225 | first = (string)requestData["first"]; | ||
226 | } | ||
227 | else | ||
228 | { | ||
229 | first = "test"; | ||
230 | } | ||
231 | |||
232 | if (requestData.Contains("last")) | ||
233 | { | ||
234 | last = (string)requestData["last"]; | ||
235 | } | ||
236 | else | ||
237 | { | ||
238 | last = "User" + NumClients.ToString(); | ||
239 | } | ||
240 | |||
241 | if (requestData.Contains("passwd")) | ||
242 | { | ||
243 | passwd = (string)requestData["passwd"]; | ||
244 | } | ||
245 | else | ||
246 | { | ||
247 | passwd = "notfound"; | ||
248 | } | ||
249 | |||
250 | if (!Authenticate(first, last, passwd)) | ||
251 | { | ||
252 | XmlRpcResponse PresenceErrorResp = new XmlRpcResponse(); | ||
253 | Hashtable PresenceErrorRespData = new Hashtable(); | ||
254 | PresenceErrorRespData["reason"] = "key"; ; | ||
255 | PresenceErrorRespData["message"] = "You have entered an invalid name/password combination. Check Caps/lock."; | ||
256 | PresenceErrorRespData["login"] = "false"; | ||
257 | PresenceErrorResp.Value = PresenceErrorRespData; | ||
258 | string reply = Regex.Replace(XmlRpcResponseSerializer.Singleton.Serialize(PresenceErrorResp), " encoding=\"utf-16\"", ""); | ||
259 | writer.WriteLine("HTTP/1.0 200 OK"); | ||
260 | writer.WriteLine("Content-type: text/xml"); | ||
261 | writer.WriteLine(); | ||
262 | writer.WriteLine(reply); | ||
263 | return false; | ||
264 | } | ||
265 | |||
266 | NumClients++; | ||
267 | |||
268 | //create a agent and session LLUUID | ||
269 | Agent = GetAgentId(first, last); | ||
270 | int SessionRand = Util.RandomClass.Next(1, 999); | ||
271 | Session = new LLUUID("aaaabbbb-0200-" + SessionRand.ToString("0000") + "-8664-58f53e442797"); | ||
272 | LLUUID secureSess = LLUUID.Random(); | ||
273 | //create some login info | ||
274 | Hashtable LoginFlagsHash = new Hashtable(); | ||
275 | LoginFlagsHash["daylight_savings"] = "N"; | ||
276 | LoginFlagsHash["stipend_since_login"] = "N"; | ||
277 | LoginFlagsHash["gendered"] = "Y"; | ||
278 | LoginFlagsHash["ever_logged_in"] = "Y"; | ||
279 | ArrayList LoginFlags = new ArrayList(); | ||
280 | LoginFlags.Add(LoginFlagsHash); | ||
281 | |||
282 | Hashtable GlobalT = new Hashtable(); | ||
283 | GlobalT["sun_texture_id"] = "cce0f112-878f-4586-a2e2-a8f104bba271"; | ||
284 | GlobalT["cloud_texture_id"] = "fc4b9f0b-d008-45c6-96a4-01dd947ac621"; | ||
285 | GlobalT["moon_texture_id"] = "fc4b9f0b-d008-45c6-96a4-01dd947ac621"; | ||
286 | ArrayList GlobalTextures = new ArrayList(); | ||
287 | GlobalTextures.Add(GlobalT); | ||
288 | |||
289 | XmlRpcResponse response = (XmlRpcResponse)(new XmlRpcResponseDeserializer()).Deserialize(this._defaultResponse); | ||
290 | Hashtable responseData = (Hashtable)response.Value; | ||
291 | |||
292 | responseData["sim_port"] = OpenSimRoot.Instance.Cfg.IPListenPort; | ||
293 | responseData["sim_ip"] = OpenSimRoot.Instance.Cfg.IPListenAddr; | ||
294 | responseData["agent_id"] = Agent.ToStringHyphenated(); | ||
295 | responseData["session_id"] = Session.ToStringHyphenated(); | ||
296 | responseData["secure_session_id"]= secureSess.ToStringHyphenated(); | ||
297 | responseData["circuit_code"] = (Int32)(Util.RandomClass.Next()); | ||
298 | responseData["seconds_since_epoch"] = (Int32)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds; | ||
299 | responseData["login-flags"] = LoginFlags; | ||
300 | responseData["global-textures"] = GlobalTextures; | ||
301 | |||
302 | //inventory | ||
303 | ArrayList InventoryList = (ArrayList)responseData["inventory-skeleton"]; | ||
304 | Hashtable Inventory1 = (Hashtable)InventoryList[0]; | ||
305 | Hashtable Inventory2 = (Hashtable)InventoryList[1]; | ||
306 | LLUUID BaseFolderID = LLUUID.Random(); | ||
307 | LLUUID InventoryFolderID = LLUUID.Random(); | ||
308 | Inventory2["name"] = "Base"; | ||
309 | Inventory2["folder_id"] = BaseFolderID.ToStringHyphenated(); | ||
310 | Inventory2["type_default"] = 0; | ||
311 | Inventory1["folder_id"] = InventoryFolderID.ToStringHyphenated(); | ||
312 | |||
313 | ArrayList InventoryRoot = (ArrayList)responseData["inventory-root"]; | ||
314 | Hashtable Inventoryroot = (Hashtable)InventoryRoot[0]; | ||
315 | Inventoryroot["folder_id"] = InventoryFolderID.ToStringHyphenated(); | ||
316 | |||
317 | CustomiseLoginResponse(responseData, first, last); | ||
318 | |||
319 | Login _login = new Login(); | ||
320 | //copy data to login object | ||
321 | _login.First = first; | ||
322 | _login.Last = last; | ||
323 | _login.Agent = Agent; | ||
324 | _login.Session = Session; | ||
325 | _login.SecureSession = secureSess; | ||
326 | _login.BaseFolder = BaseFolderID; | ||
327 | _login.InventoryFolder = InventoryFolderID; | ||
328 | |||
329 | //working on local computer if so lets add to the gridserver's list of sessions? | ||
330 | if (OpenSimRoot.Instance.GridServers.GridServer.GetName() == "Local") | ||
331 | { | ||
332 | ((LocalGridBase)this._gridServer).AddNewSession(_login); | ||
333 | } | ||
334 | |||
335 | // forward the XML-RPC response to the client | ||
336 | writer.WriteLine("HTTP/1.0 200 OK"); | ||
337 | writer.WriteLine("Content-type: text/xml"); | ||
338 | writer.WriteLine(); | ||
339 | |||
340 | XmlTextWriter responseWriter = new XmlTextWriter(writer); | ||
341 | XmlRpcResponseSerializer.Singleton.Serialize(responseWriter, response); | ||
342 | responseWriter.Close(); | ||
343 | |||
344 | return true; | ||
345 | } | ||
346 | |||
347 | protected virtual void CustomiseLoginResponse(Hashtable responseData, string first, string last) | ||
348 | { | ||
349 | } | ||
350 | |||
351 | protected virtual LLUUID GetAgentId(string firstName, string lastName) | ||
352 | { | ||
353 | LLUUID Agent; | ||
354 | int AgentRand = Util.RandomClass.Next(1, 9999); | ||
355 | Agent = new LLUUID("99998888-0100-" + AgentRand.ToString("0000") + "-8ec1-0b1d5cd6aead"); | ||
356 | return Agent; | ||
357 | } | ||
358 | |||
359 | protected virtual bool Authenticate(string first, string last, string passwd) | ||
360 | { | ||
361 | if (this._needPasswd) | ||
362 | { | ||
363 | //every user needs the password to login | ||
364 | string encodedPass = passwd.Remove(0, 3); //remove $1$ | ||
365 | if (encodedPass == this._mpasswd) | ||
366 | { | ||
367 | return true; | ||
368 | } | ||
369 | else | ||
370 | { | ||
371 | return false; | ||
372 | } | ||
373 | } | ||
374 | else | ||
375 | { | ||
376 | //do not need password to login | ||
377 | return true; | ||
378 | } | ||
379 | } | ||
380 | |||
381 | private static string EncodePassword(string passwd) | ||
382 | { | ||
383 | Byte[] originalBytes; | ||
384 | Byte[] encodedBytes; | ||
385 | MD5 md5; | ||
386 | |||
387 | md5 = new MD5CryptoServiceProvider(); | ||
388 | originalBytes = ASCIIEncoding.Default.GetBytes(passwd); | ||
389 | encodedBytes = md5.ComputeHash(originalBytes); | ||
390 | |||
391 | return Regex.Replace(BitConverter.ToString(encodedBytes), "-", "").ToLower(); | ||
392 | } | ||
393 | |||
394 | //IUserServer implementation | ||
395 | public AgentInventory RequestAgentsInventory(LLUUID agentID) | ||
396 | { | ||
397 | AgentInventory aInventory = null; | ||
398 | if (this.userAccounts) | ||
399 | { | ||
400 | aInventory = this.userManager.GetUsersInventory(agentID); | ||
401 | } | ||
402 | |||
403 | return aInventory; | ||
404 | } | ||
405 | |||
406 | public void SetServerInfo(string ServerUrl, string SendKey, string RecvKey) | ||
407 | { | ||
408 | |||
409 | } | ||
410 | |||
411 | } | ||
412 | |||
413 | |||
414 | } | ||