1 //
2 // ClientSessionCache.cs: Client-side cache for re-using sessions
3 //
4 // Author:
5 //	Sebastien Pouliot  <sebastien@ximian.com>
6 //
7 // Copyright (C) 2006 Novell (http://www.novell.com)
8 //
9 // Permission is hereby granted, free of charge, to any person obtaining
10 // a copy of this software and associated documentation files (the
11 // "Software"), to deal in the Software without restriction, including
12 // without limitation the rights to use, copy, modify, merge, publish,
13 // distribute, sublicense, and/or sell copies of the Software, and to
14 // permit persons to whom the Software is furnished to do so, subject to
15 // the following conditions:
16 //
17 // The above copyright notice and this permission notice shall be
18 // included in all copies or substantial portions of the Software.
19 //
20 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
24 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
25 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
26 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27 //
28 
29 using System;
30 using System.Collections;
31 
32 namespace Mono.Security.Protocol.Tls {
33 
34 	internal class ClientSessionInfo : IDisposable {
35 
36 		// (by default) we keep this item valid for 3 minutes (if unused)
37 		private const int DefaultValidityInterval = 3 * 60;
38 		private static readonly int ValidityInterval;
39 
40 		private bool disposed;
41 		private DateTime validuntil;
42 		private string host;
43 
44 		// see RFC2246 - Section 7
45 		private byte[] sid;
46 		private byte[] masterSecret;
47 
ClientSessionInfo()48 		static ClientSessionInfo ()
49 		{
50 			string user_cache_timeout = Environment.GetEnvironmentVariable ("MONO_TLS_SESSION_CACHE_TIMEOUT");
51 			if (user_cache_timeout == null) {
52 				ValidityInterval = DefaultValidityInterval;
53 			} else {
54 				try {
55 					ValidityInterval = Int32.Parse (user_cache_timeout);
56 				}
57 				catch {
58 					ValidityInterval = DefaultValidityInterval;
59 				}
60 			}
61 		}
62 
ClientSessionInfo(string hostname, byte[] id)63 		public ClientSessionInfo (string hostname, byte[] id)
64 		{
65 			host = hostname;
66 			sid = id;
67 			KeepAlive ();
68 		}
69 
~ClientSessionInfo()70 		~ClientSessionInfo ()
71 		{
72 			Dispose (false);
73 		}
74 
75 
76 		public string HostName {
77 			get { return host; }
78 		}
79 
80 		public byte[] Id {
81 			get { return sid; }
82 		}
83 
84 		public bool Valid {
85 			get { return ((masterSecret != null) && (validuntil > DateTime.UtcNow)); }
86 		}
87 
88 
GetContext(Context context)89 		public void GetContext (Context context)
90 		{
91 			CheckDisposed ();
92 			if (context.MasterSecret != null)
93 				masterSecret = (byte[]) context.MasterSecret.Clone ();
94 		}
95 
SetContext(Context context)96 		public void SetContext (Context context)
97 		{
98 			CheckDisposed ();
99 			if (masterSecret != null)
100 				context.MasterSecret = (byte[]) masterSecret.Clone ();
101 		}
102 
KeepAlive()103 		public void KeepAlive ()
104 		{
105 			CheckDisposed ();
106 			validuntil = DateTime.UtcNow.AddSeconds (ValidityInterval);
107 		}
108 
Dispose()109 		public void Dispose ()
110 		{
111 			Dispose (true);
112 			GC.SuppressFinalize (this);
113 		}
114 
Dispose(bool disposing)115 		private void Dispose (bool disposing)
116 		{
117 			if (!disposed) {
118 				validuntil = DateTime.MinValue;
119 				host = null;
120 				sid = null;
121 
122 				if (masterSecret != null) {
123 					Array.Clear (masterSecret, 0, masterSecret.Length);
124 					masterSecret = null;
125 				}
126 			}
127 			disposed = true;
128 		}
129 
CheckDisposed()130 		private void CheckDisposed ()
131 		{
132 			if (disposed) {
133 				string msg = Locale.GetText ("Cache session information were disposed.");
134 				throw new ObjectDisposedException (msg);
135 			}
136 		}
137 	}
138 
139 	// note: locking is aggressive but isn't used often (and we gain much more :)
140 	internal class ClientSessionCache {
141 
142 		static Hashtable cache;
143 		static object locker;
144 
ClientSessionCache()145 		static ClientSessionCache ()
146 		{
147 			cache = new Hashtable ();
148 			locker = new object ();
149 		}
150 
151 		// note: we may have multiple connections with a host, so
152 		// possibly multiple entries per host (each with a different
153 		// id), so we do not use the host as the hashtable key
Add(string host, byte[] id)154 		static public void Add (string host, byte[] id)
155 		{
156 			lock (locker) {
157 				string uid = BitConverter.ToString (id);
158 				ClientSessionInfo si = (ClientSessionInfo) cache[uid];
159 				if (si == null) {
160 					cache.Add (uid, new ClientSessionInfo (host, id));
161 				} else if (si.HostName == host) {
162 					// we already have this and it's still valid
163 					// on the server, so we'll keep it a little longer
164 					si.KeepAlive ();
165 				} else {
166 					// it's very unlikely but the same session id
167 					// could be used by more than one host. In this
168 					// case we replace the older one with the new one
169 					si.Dispose ();
170 					cache.Remove (uid);
171 					cache.Add (uid, new ClientSessionInfo (host, id));
172 				}
173 			}
174 		}
175 
176 		// return the first session us
FromHost(string host)177 		static public byte[] FromHost (string host)
178 		{
179 			lock (locker) {
180 				foreach (ClientSessionInfo si in cache.Values) {
181 					if (si.HostName == host) {
182 						if (si.Valid) {
183 							// ensure it's still valid when we really need it
184 							si.KeepAlive ();
185 							return si.Id;
186 						}
187 					}
188 				}
189 				return null;
190 			}
191 		}
192 
193 		// only called inside the lock
FromContext(Context context, bool checkValidity)194 		static private ClientSessionInfo FromContext (Context context, bool checkValidity)
195 		{
196 			if (context == null)
197 				return null;
198 
199 			byte[] id = context.SessionId;
200 			if ((id == null) || (id.Length == 0))
201 				return null;
202 
203 			// do we have a session cached for this host ?
204 			string uid = BitConverter.ToString (id);
205 
206 			ClientSessionInfo si = (ClientSessionInfo) cache[uid];
207 			if (si == null)
208 				return null;
209 
210 			// In the unlikely case of multiple hosts using the same
211 			// session id, we just act like we do not know about it
212 			if (context.ClientSettings.TargetHost != si.HostName)
213 				return null;
214 
215 			// yes, so what's its status ?
216 			if (checkValidity && !si.Valid) {
217 				si.Dispose ();
218 				cache.Remove (uid);
219 				return null;
220 			}
221 
222 			// ok, it make sense
223 			return si;
224 		}
225 
SetContextInCache(Context context)226 		static public bool SetContextInCache (Context context)
227 		{
228 			lock (locker) {
229 				// Don't check the validity because the masterKey of the ClientSessionInfo
230 				// can still be null when this is called the first time
231 				ClientSessionInfo csi = FromContext (context, false);
232 				if (csi == null)
233 					return false;
234 
235 				csi.GetContext (context);
236 				csi.KeepAlive ();
237 				return true;
238 			}
239 		}
240 
SetContextFromCache(Context context)241 		static public bool SetContextFromCache (Context context)
242 		{
243 			lock (locker) {
244 				ClientSessionInfo csi = FromContext (context, true);
245 				if (csi == null)
246 					return false;
247 
248 				csi.SetContext (context);
249 				csi.KeepAlive ();
250 				return true;
251 			}
252 		}
253 	}
254 }
255