aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
authorJohn Hurliman2010-03-03 14:38:00 -0800
committerJohn Hurliman2010-03-03 14:38:00 -0800
commit61a63ff85124d932cca5a2aeba9ae685fa7f146f (patch)
treeae5337063e486e32131a188c3d6b50b4695b1f09
parent* Adjusted the significant movement magic value from 0.5m to 2.0m and added a... (diff)
downloadopensim-SC-61a63ff85124d932cca5a2aeba9ae685fa7f146f.zip
opensim-SC-61a63ff85124d932cca5a2aeba9ae685fa7f146f.tar.gz
opensim-SC-61a63ff85124d932cca5a2aeba9ae685fa7f146f.tar.bz2
opensim-SC-61a63ff85124d932cca5a2aeba9ae685fa7f146f.tar.xz
* Added three new helper utility files to OpenSim.Framework. MultipartForm is used for constructing multipart/form-data requests. UntrustedWebRequest places sanity checks and policy on requests to HTTP endpoints that are not in the same trust domain (useful for Hypergrid, OpenID, etc). WebUtil contains misc. functions for managing URLs and network streams
-rw-r--r--OpenSim/Framework/MultipartForm.cs117
-rw-r--r--OpenSim/Framework/UntrustedWebRequest.cs203
-rw-r--r--OpenSim/Framework/WebUtil.cs330
-rw-r--r--prebuild.xml1
4 files changed, 651 insertions, 0 deletions
diff --git a/OpenSim/Framework/MultipartForm.cs b/OpenSim/Framework/MultipartForm.cs
new file mode 100644
index 0000000..8ba6d22
--- /dev/null
+++ b/OpenSim/Framework/MultipartForm.cs
@@ -0,0 +1,117 @@
1using System;
2using System.Collections.Generic;
3using System.Net;
4using System.IO;
5using System.Text;
6
7namespace OpenSim.Framework
8{
9 public static class MultipartForm
10 {
11 #region Helper Classes
12
13 public abstract class Element
14 {
15 public string Name;
16 }
17
18 public class File : Element
19 {
20 public string Filename;
21 public string ContentType;
22 public byte[] Data;
23
24 public File(string name, string filename, string contentType, byte[] data)
25 {
26 Name = name;
27 Filename = filename;
28 ContentType = contentType;
29 Data = data;
30 }
31 }
32
33 public class Parameter : Element
34 {
35 public string Value;
36
37 public Parameter(string name, string value)
38 {
39 Name = name;
40 Value = value;
41 }
42 }
43
44 #endregion Helper Classes
45
46 public static HttpWebResponse Post(HttpWebRequest request, List<Element> postParameters)
47 {
48 string boundary = Boundary();
49
50 // Set up the request properties
51 request.Method = "POST";
52 request.ContentType = "multipart/form-data; boundary=" + boundary;
53
54 #region Stream Writing
55
56 using (MemoryStream formDataStream = new MemoryStream())
57 {
58 foreach (var param in postParameters)
59 {
60 if (param is File)
61 {
62 File file = (File)param;
63
64 // Add just the first part of this param, since we will write the file data directly to the Stream
65 string header = string.Format("--{0}\r\nContent-Disposition: form-data; name=\"{1}\"; filename=\"{2}\";\r\nContent-Type: {3}\r\n\r\n",
66 boundary,
67 file.Name,
68 !String.IsNullOrEmpty(file.Filename) ? file.Filename : "tempfile",
69 file.ContentType);
70
71 formDataStream.Write(Encoding.UTF8.GetBytes(header), 0, header.Length);
72 formDataStream.Write(file.Data, 0, file.Data.Length);
73 }
74 else
75 {
76 Parameter parameter = (Parameter)param;
77
78 string postData = string.Format("--{0}\r\nContent-Disposition: form-data; name=\"{1}\"\r\n\r\n{2}\r\n",
79 boundary,
80 parameter.Name,
81 parameter.Value);
82 formDataStream.Write(Encoding.UTF8.GetBytes(postData), 0, postData.Length);
83 }
84 }
85
86 // Add the end of the request
87 byte[] footer = Encoding.UTF8.GetBytes("\r\n--" + boundary + "--\r\n");
88 formDataStream.Write(footer, 0, footer.Length);
89
90 request.ContentLength = formDataStream.Length;
91
92 // Copy the temporary stream to the network stream
93 formDataStream.Seek(0, SeekOrigin.Begin);
94 using (Stream requestStream = request.GetRequestStream())
95 formDataStream.CopyTo(requestStream, (int)formDataStream.Length);
96 }
97
98 #endregion Stream Writing
99
100 return request.GetResponse() as HttpWebResponse;
101 }
102
103 private static string Boundary()
104 {
105 Random rnd = new Random();
106 string formDataBoundary = String.Empty;
107
108 while (formDataBoundary.Length < 15)
109 formDataBoundary = formDataBoundary + rnd.Next();
110
111 formDataBoundary = formDataBoundary.Substring(0, 15);
112 formDataBoundary = "-----------------------------" + formDataBoundary;
113
114 return formDataBoundary;
115 }
116 }
117}
diff --git a/OpenSim/Framework/UntrustedWebRequest.cs b/OpenSim/Framework/UntrustedWebRequest.cs
new file mode 100644
index 0000000..1af7c41
--- /dev/null
+++ b/OpenSim/Framework/UntrustedWebRequest.cs
@@ -0,0 +1,203 @@
1using System;
2using System.Collections.Generic;
3using System.IO;
4using System.Net;
5using System.Net.Security;
6using System.Text;
7using log4net;
8
9namespace OpenSim.Framework
10{
11 /// <summary>
12 /// Used for requests to untrusted endpoints that may potentially be
13 /// malicious
14 /// </summary>
15 public static class UntrustedHttpWebRequest
16 {
17 /// <summary>Setting this to true will allow HTTP connections to localhost</summary>
18 private const bool DEBUG = true;
19
20 private static readonly ILog m_log =
21 LogManager.GetLogger(
22 System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
23
24 private static readonly ICollection<string> allowableSchemes = new List<string> { "http", "https" };
25
26 /// <summary>
27 /// Creates an HttpWebRequest that is hardened against malicious
28 /// endpoints after ensuring the given Uri is safe to retrieve
29 /// </summary>
30 /// <param name="uri">Web location to request</param>
31 /// <returns>A hardened HttpWebRequest if the uri was determined to be safe</returns>
32 /// <exception cref="ArgumentNullException">If uri is null</exception>
33 /// <exception cref="ArgumentException">If uri is unsafe</exception>
34 public static HttpWebRequest Create(Uri uri)
35 {
36 return Create(uri, DEBUG, 1000 * 5, 1000 * 20, 10);
37 }
38
39 /// <summary>
40 /// Creates an HttpWebRequest that is hardened against malicious
41 /// endpoints after ensuring the given Uri is safe to retrieve
42 /// </summary>
43 /// <param name="uri">Web location to request</param>
44 /// <param name="allowLoopback">True to allow connections to localhost, otherwise false</param>
45 /// <param name="readWriteTimeoutMS">Read write timeout, in milliseconds</param>
46 /// <param name="timeoutMS">Connection timeout, in milliseconds</param>
47 /// <param name="maximumRedirects">Maximum number of allowed redirects</param>
48 /// <returns>A hardened HttpWebRequest if the uri was determined to be safe</returns>
49 /// <exception cref="ArgumentNullException">If uri is null</exception>
50 /// <exception cref="ArgumentException">If uri is unsafe</exception>
51 public static HttpWebRequest Create(Uri uri, bool allowLoopback, int readWriteTimeoutMS, int timeoutMS, int maximumRedirects)
52 {
53 if (uri == null)
54 throw new ArgumentNullException("uri");
55
56 if (!IsUriAllowable(uri, allowLoopback))
57 throw new ArgumentException("Uri " + uri + " was rejected");
58
59 HttpWebRequest httpWebRequest = (HttpWebRequest)HttpWebRequest.Create(uri);
60 httpWebRequest.MaximumAutomaticRedirections = maximumRedirects;
61 httpWebRequest.ReadWriteTimeout = readWriteTimeoutMS;
62 httpWebRequest.Timeout = timeoutMS;
63 httpWebRequest.KeepAlive = false;
64
65 return httpWebRequest;
66 }
67
68 public static string PostToUntrustedUrl(Uri url, string data)
69 {
70 try
71 {
72 byte[] requestData = System.Text.Encoding.UTF8.GetBytes(data);
73
74 HttpWebRequest request = Create(url);
75 request.Method = "POST";
76 request.ContentLength = requestData.Length;
77 request.ContentType = "application/x-www-form-urlencoded";
78
79 using (Stream requestStream = request.GetRequestStream())
80 requestStream.Write(requestData, 0, requestData.Length);
81
82 using (WebResponse response = request.GetResponse())
83 {
84 using (Stream responseStream = response.GetResponseStream())
85 return responseStream.GetStreamString();
86 }
87 }
88 catch (Exception ex)
89 {
90 m_log.Warn("POST to untrusted URL " + url + " failed: " + ex.Message);
91 return null;
92 }
93 }
94
95 public static string GetUntrustedUrl(Uri url)
96 {
97 try
98 {
99 HttpWebRequest request = Create(url);
100
101 using (WebResponse response = request.GetResponse())
102 {
103 using (Stream responseStream = response.GetResponseStream())
104 return responseStream.GetStreamString();
105 }
106 }
107 catch (Exception ex)
108 {
109 m_log.Warn("GET from untrusted URL " + url + " failed: " + ex.Message);
110 return null;
111 }
112 }
113
114 /// <summary>
115 /// Determines whether a URI is allowed based on scheme and host name.
116 /// No requireSSL check is done here
117 /// </summary>
118 /// <param name="allowLoopback">True to allow loopback addresses to be used</param>
119 /// <param name="uri">The URI to test for whether it should be allowed.</param>
120 /// <returns>
121 /// <c>true</c> if [is URI allowable] [the specified URI]; otherwise, <c>false</c>.
122 /// </returns>
123 private static bool IsUriAllowable(Uri uri, bool allowLoopback)
124 {
125 if (!allowableSchemes.Contains(uri.Scheme))
126 {
127 m_log.WarnFormat("Rejecting URL {0} because it uses a disallowed scheme.", uri);
128 return false;
129 }
130
131 // Try to interpret the hostname as an IP address so we can test for internal
132 // IP address ranges. Note that IP addresses can appear in many forms
133 // (e.g. http://127.0.0.1, http://2130706433, http://0x0100007f, http://::1
134 // So we convert them to a canonical IPAddress instance, and test for all
135 // non-routable IP ranges: 10.*.*.*, 127.*.*.*, ::1
136 // Note that Uri.IsLoopback is very unreliable, not catching many of these variants.
137 IPAddress hostIPAddress;
138 if (IPAddress.TryParse(uri.DnsSafeHost, out hostIPAddress))
139 {
140 byte[] addressBytes = hostIPAddress.GetAddressBytes();
141
142 // The host is actually an IP address.
143 switch (hostIPAddress.AddressFamily)
144 {
145 case System.Net.Sockets.AddressFamily.InterNetwork:
146 if (!allowLoopback && (addressBytes[0] == 127 || addressBytes[0] == 10))
147 {
148 m_log.WarnFormat("Rejecting URL {0} because it is a loopback address.", uri);
149 return false;
150 }
151 break;
152 case System.Net.Sockets.AddressFamily.InterNetworkV6:
153 if (!allowLoopback && IsIPv6Loopback(hostIPAddress))
154 {
155 m_log.WarnFormat("Rejecting URL {0} because it is a loopback address.", uri);
156 return false;
157 }
158 break;
159 default:
160 m_log.WarnFormat("Rejecting URL {0} because it does not use an IPv4 or IPv6 address.", uri);
161 return false;
162 }
163 }
164 else
165 {
166 // The host is given by name. We require names to contain periods to
167 // help make sure it's not an internal address.
168 if (!allowLoopback && !uri.Host.Contains("."))
169 {
170 m_log.WarnFormat("Rejecting URL {0} because it does not contain a period in the host name.", uri);
171 return false;
172 }
173 }
174
175 return true;
176 }
177
178 /// <summary>
179 /// Determines whether an IP address is the IPv6 equivalent of "localhost/127.0.0.1".
180 /// </summary>
181 /// <param name="ip">The ip address to check.</param>
182 /// <returns>
183 /// <c>true</c> if this is a loopback IP address; <c>false</c> otherwise.
184 /// </returns>
185 private static bool IsIPv6Loopback(IPAddress ip)
186 {
187 if (ip == null)
188 throw new ArgumentNullException("ip");
189
190 byte[] addressBytes = ip.GetAddressBytes();
191 for (int i = 0; i < addressBytes.Length - 1; i++)
192 {
193 if (addressBytes[i] != 0)
194 return false;
195 }
196
197 if (addressBytes[addressBytes.Length - 1] != 1)
198 return false;
199
200 return true;
201 }
202 }
203}
diff --git a/OpenSim/Framework/WebUtil.cs b/OpenSim/Framework/WebUtil.cs
new file mode 100644
index 0000000..d9782ff
--- /dev/null
+++ b/OpenSim/Framework/WebUtil.cs
@@ -0,0 +1,330 @@
1using System;
2using System.Collections.Generic;
3using System.Collections.Specialized;
4using System.IO;
5using System.Net;
6using System.Net.Security;
7using System.Reflection;
8using System.Text;
9using System.Web;
10using log4net;
11using OpenSim.Framework.Servers.HttpServer;
12using OpenMetaverse.StructuredData;
13
14namespace OpenSim.Framework
15{
16 /// <summary>
17 /// Miscellaneous static methods and extension methods related to the web
18 /// </summary>
19 public static class WebUtil
20 {
21 private static readonly ILog m_log =
22 LogManager.GetLogger(
23 MethodBase.GetCurrentMethod().DeclaringType);
24
25 /// <summary>
26 /// Send LLSD to an HTTP client in application/llsd+json form
27 /// </summary>
28 /// <param name="response">HTTP response to send the data in</param>
29 /// <param name="body">LLSD to send to the client</param>
30 public static void SendJSONResponse(OSHttpResponse response, OSDMap body)
31 {
32 byte[] responseData = Encoding.UTF8.GetBytes(OSDParser.SerializeJsonString(body));
33
34 response.ContentEncoding = Encoding.UTF8;
35 response.ContentLength = responseData.Length;
36 response.ContentType = "application/llsd+json";
37 response.Body.Write(responseData, 0, responseData.Length);
38 }
39
40 /// <summary>
41 /// Send LLSD to an HTTP client in application/llsd+xml form
42 /// </summary>
43 /// <param name="response">HTTP response to send the data in</param>
44 /// <param name="body">LLSD to send to the client</param>
45 public static void SendXMLResponse(OSHttpResponse response, OSDMap body)
46 {
47 byte[] responseData = OSDParser.SerializeLLSDXmlBytes(body);
48
49 response.ContentEncoding = Encoding.UTF8;
50 response.ContentLength = responseData.Length;
51 response.ContentType = "application/llsd+xml";
52 response.Body.Write(responseData, 0, responseData.Length);
53 }
54
55 /// <summary>
56 /// Make a GET or GET-like request to a web service that returns LLSD
57 /// or JSON data
58 /// </summary>
59 public static OSDMap ServiceRequest(string url, string httpVerb)
60 {
61 string errorMessage;
62
63 try
64 {
65 HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(url);
66 request.Method = httpVerb;
67
68 using (WebResponse response = request.GetResponse())
69 {
70 using (Stream responseStream = response.GetResponseStream())
71 {
72 try
73 {
74 string responseStr = responseStream.GetStreamString();
75 OSD responseOSD = OSDParser.Deserialize(responseStr);
76 if (responseOSD.Type == OSDType.Map)
77 return (OSDMap)responseOSD;
78 else
79 errorMessage = "Response format was invalid.";
80 }
81 catch
82 {
83 errorMessage = "Failed to parse the response.";
84 }
85 }
86 }
87 }
88 catch (Exception ex)
89 {
90 m_log.Warn("GET from URL " + url + " failed: " + ex.Message);
91 errorMessage = ex.Message;
92 }
93
94 return new OSDMap { { "Message", OSD.FromString("Service request failed. " + errorMessage) } };
95 }
96
97 /// <summary>
98 /// POST URL-encoded form data to a web service that returns LLSD or
99 /// JSON data
100 /// </summary>
101 public static OSDMap PostToService(string url, NameValueCollection data)
102 {
103 string errorMessage;
104
105 try
106 {
107 string queryString = BuildQueryString(data);
108 byte[] requestData = System.Text.Encoding.UTF8.GetBytes(queryString);
109
110 HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(url);
111 request.Method = "POST";
112 request.ContentLength = requestData.Length;
113 request.ContentType = "application/x-www-form-urlencoded";
114
115 using (Stream requestStream = request.GetRequestStream())
116 requestStream.Write(requestData, 0, requestData.Length);
117
118 using (WebResponse response = request.GetResponse())
119 {
120 using (Stream responseStream = response.GetResponseStream())
121 {
122 try
123 {
124 string responseStr = responseStream.GetStreamString();
125 OSD responseOSD = OSDParser.Deserialize(responseStr);
126 if (responseOSD.Type == OSDType.Map)
127 return (OSDMap)responseOSD;
128 else
129 errorMessage = "Response format was invalid.";
130 }
131 catch
132 {
133 errorMessage = "Failed to parse the response.";
134 }
135 }
136 }
137 }
138 catch (Exception ex)
139 {
140 m_log.Warn("POST to URL " + url + " failed: " + ex.Message);
141 errorMessage = ex.Message;
142 }
143
144 return new OSDMap { { "Message", OSD.FromString("Service request failed. " + errorMessage) } };
145 }
146
147 #region Uri
148
149 /// <summary>
150 /// Combines a Uri that can contain both a base Uri and relative path
151 /// with a second relative path fragment
152 /// </summary>
153 /// <param name="uri">Starting (base) Uri</param>
154 /// <param name="fragment">Relative path fragment to append to the end
155 /// of the Uri</param>
156 /// <returns>The combined Uri</returns>
157 /// <remarks>This is similar to the Uri constructor that takes a base
158 /// Uri and the relative path, except this method can append a relative
159 /// path fragment on to an existing relative path</remarks>
160 public static Uri Combine(this Uri uri, string fragment)
161 {
162 string fragment1 = uri.Fragment;
163 string fragment2 = fragment;
164
165 if (!fragment1.EndsWith("/"))
166 fragment1 = fragment1 + '/';
167 if (fragment2.StartsWith("/"))
168 fragment2 = fragment2.Substring(1);
169
170 return new Uri(uri, fragment1 + fragment2);
171 }
172
173 /// <summary>
174 /// Combines a Uri that can contain both a base Uri and relative path
175 /// with a second relative path fragment. If the fragment is absolute,
176 /// it will be returned without modification
177 /// </summary>
178 /// <param name="uri">Starting (base) Uri</param>
179 /// <param name="fragment">Relative path fragment to append to the end
180 /// of the Uri, or an absolute Uri to return unmodified</param>
181 /// <returns>The combined Uri</returns>
182 public static Uri Combine(this Uri uri, Uri fragment)
183 {
184 if (fragment.IsAbsoluteUri)
185 return fragment;
186
187 string fragment1 = uri.Fragment;
188 string fragment2 = fragment.ToString();
189
190 if (!fragment1.EndsWith("/"))
191 fragment1 = fragment1 + '/';
192 if (fragment2.StartsWith("/"))
193 fragment2 = fragment2.Substring(1);
194
195 return new Uri(uri, fragment1 + fragment2);
196 }
197
198 /// <summary>
199 /// Appends a query string to a Uri that may or may not have existing
200 /// query parameters
201 /// </summary>
202 /// <param name="uri">Uri to append the query to</param>
203 /// <param name="query">Query string to append. Can either start with ?
204 /// or just containg key/value pairs</param>
205 /// <returns>String representation of the Uri with the query string
206 /// appended</returns>
207 public static string AppendQuery(this Uri uri, string query)
208 {
209 if (String.IsNullOrEmpty(query))
210 return uri.ToString();
211
212 if (query[0] == '?' || query[0] == '&')
213 query = query.Substring(1);
214
215 string uriStr = uri.ToString();
216
217 if (uriStr.Contains("?"))
218 return uriStr + '&' + query;
219 else
220 return uriStr + '?' + query;
221 }
222
223 #endregion Uri
224
225 #region NameValueCollection
226
227 /// <summary>
228 /// Convert a NameValueCollection into a query string. This is the
229 /// inverse of HttpUtility.ParseQueryString()
230 /// </summary>
231 /// <param name="parameters">Collection of key/value pairs to convert</param>
232 /// <returns>A query string with URL-escaped values</returns>
233 public static string BuildQueryString(NameValueCollection parameters)
234 {
235 List<string> items = new List<string>(parameters.Count);
236
237 foreach (string key in parameters.Keys)
238 {
239 foreach (string value in parameters.GetValues(key))
240 items.Add(String.Concat(key, "=", HttpUtility.UrlEncode(value ?? String.Empty)));
241 }
242
243 return String.Join("&", items.ToArray());
244 }
245
246 /// <summary>
247 ///
248 /// </summary>
249 /// <param name="collection"></param>
250 /// <param name="key"></param>
251 /// <returns></returns>
252 public static string GetOne(this NameValueCollection collection, string key)
253 {
254 string[] values = collection.GetValues(key);
255 if (values != null && values.Length > 0)
256 return values[0];
257
258 return null;
259 }
260
261 #endregion NameValueCollection
262
263 #region Stream
264
265 /// <summary>
266 /// Copies the contents of one stream to another, starting at the
267 /// current position of each stream
268 /// </summary>
269 /// <param name="copyFrom">The stream to copy from, at the position
270 /// where copying should begin</param>
271 /// <param name="copyTo">The stream to copy to, at the position where
272 /// bytes should be written</param>
273 /// <param name="maximumBytesToCopy">The maximum bytes to copy</param>
274 /// <returns>The total number of bytes copied</returns>
275 /// <remarks>
276 /// Copying begins at the streams' current positions. The positions are
277 /// NOT reset after copying is complete.
278 /// </remarks>
279 public static int CopyTo(this Stream copyFrom, Stream copyTo, int maximumBytesToCopy)
280 {
281 byte[] buffer = new byte[4096];
282 int readBytes;
283 int totalCopiedBytes = 0;
284
285 while ((readBytes = copyFrom.Read(buffer, 0, Math.Min(4096, maximumBytesToCopy))) > 0)
286 {
287 int writeBytes = Math.Min(maximumBytesToCopy, readBytes);
288 copyTo.Write(buffer, 0, writeBytes);
289 totalCopiedBytes += writeBytes;
290 maximumBytesToCopy -= writeBytes;
291 }
292
293 return totalCopiedBytes;
294 }
295
296 /// <summary>
297 /// Converts an entire stream to a string, regardless of current stream
298 /// position
299 /// </summary>
300 /// <param name="stream">The stream to convert to a string</param>
301 /// <returns></returns>
302 /// <remarks>When this method is done, the stream position will be
303 /// reset to its previous position before this method was called</remarks>
304 public static string GetStreamString(this Stream stream)
305 {
306 string value = null;
307
308 if (stream != null && stream.CanRead)
309 {
310 long rewindPos = -1;
311
312 if (stream.CanSeek)
313 {
314 rewindPos = stream.Position;
315 stream.Seek(0, SeekOrigin.Begin);
316 }
317
318 StreamReader reader = new StreamReader(stream);
319 value = reader.ReadToEnd();
320
321 if (rewindPos >= 0)
322 stream.Seek(rewindPos, SeekOrigin.Begin);
323 }
324
325 return value;
326 }
327
328 #endregion Stream
329 }
330}
diff --git a/prebuild.xml b/prebuild.xml
index 09d78e9..98a471b 100644
--- a/prebuild.xml
+++ b/prebuild.xml
@@ -157,6 +157,7 @@
157 <Reference name="System.Xml"/> 157 <Reference name="System.Xml"/>
158 <Reference name="System.Data"/> 158 <Reference name="System.Data"/>
159 <Reference name="System.Drawing"/> 159 <Reference name="System.Drawing"/>
160 <Reference name="System.Web"/>
160 <Reference name="BclExtras.dll"/> 161 <Reference name="BclExtras.dll"/>
161 <Reference name="OpenMetaverseTypes.dll"/> 162 <Reference name="OpenMetaverseTypes.dll"/>
162 <Reference name="OpenMetaverse.dll"/> 163 <Reference name="OpenMetaverse.dll"/>