clockwerk-opensim – Blame information for rev 1
?pathlinks?
Rev | Author | Line No. | Line |
---|---|---|---|
1 | vero | 1 | /* |
2 | * Copyright (c) Contributors, http://opensimulator.org/ |
||
3 | * See CONTRIBUTORS.TXT for a full list of copyright holders. |
||
4 | * |
||
5 | * Redistribution and use in source and binary forms, with or without |
||
6 | * modification, are permitted provided that the following conditions are met: |
||
7 | * * Redistributions of source code must retain the above copyright |
||
8 | * notice, this list of conditions and the following disclaimer. |
||
9 | * * Redistributions in binary form must reproduce the above copyright |
||
10 | * notice, this list of conditions and the following disclaimer in the |
||
11 | * documentation and/or other materials provided with the distribution. |
||
12 | * * Neither the name of the OpenSimulator Project nor the |
||
13 | * names of its contributors may be used to endorse or promote products |
||
14 | * derived from this software without specific prior written permission. |
||
15 | * |
||
16 | * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY |
||
17 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
||
18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
||
19 | * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY |
||
20 | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
||
21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
||
22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND |
||
23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
||
24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||
25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||
26 | */ |
||
27 | |||
28 | using System; |
||
29 | using System.Collections; |
||
30 | using System.Collections.Specialized; |
||
31 | using System.Drawing; |
||
32 | using System.Drawing.Imaging; |
||
33 | using System.Reflection; |
||
34 | using System.IO; |
||
35 | using System.Web; |
||
36 | using log4net; |
||
37 | using Nini.Config; |
||
38 | using OpenMetaverse; |
||
39 | using OpenMetaverse.StructuredData; |
||
40 | using OpenMetaverse.Imaging; |
||
41 | using OpenSim.Framework; |
||
42 | using OpenSim.Framework.Servers; |
||
43 | using OpenSim.Framework.Servers.HttpServer; |
||
44 | using OpenSim.Region.Framework.Interfaces; |
||
45 | using OpenSim.Services.Interfaces; |
||
46 | using Caps = OpenSim.Framework.Capabilities.Caps; |
||
47 | |||
48 | namespace OpenSim.Capabilities.Handlers |
||
49 | { |
||
50 | public class GetTextureHandler : BaseStreamHandler |
||
51 | { |
||
52 | private static readonly ILog m_log = |
||
53 | LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); |
||
54 | private IAssetService m_assetService; |
||
55 | |||
56 | public const string DefaultFormat = "x-j2c"; |
||
57 | |||
58 | // TODO: Change this to a config option |
||
59 | private string m_RedirectURL = null; |
||
60 | |||
61 | public GetTextureHandler(string path, IAssetService assService, string name, string description, string redirectURL) |
||
62 | : base("GET", path, name, description) |
||
63 | { |
||
64 | m_assetService = assService; |
||
65 | m_RedirectURL = redirectURL; |
||
66 | if (m_RedirectURL != null && !m_RedirectURL.EndsWith("/")) |
||
67 | m_RedirectURL += "/"; |
||
68 | } |
||
69 | |||
70 | protected override byte[] ProcessRequest(string path, Stream request, IOSHttpRequest httpRequest, IOSHttpResponse httpResponse) |
||
71 | { |
||
72 | // Try to parse the texture ID from the request URL |
||
73 | NameValueCollection query = HttpUtility.ParseQueryString(httpRequest.Url.Query); |
||
74 | string textureStr = query.GetOne("texture_id"); |
||
75 | string format = query.GetOne("format"); |
||
76 | |||
77 | //m_log.DebugFormat("[GETTEXTURE]: called {0}", textureStr); |
||
78 | |||
79 | if (m_assetService == null) |
||
80 | { |
||
81 | m_log.Error("[GETTEXTURE]: Cannot fetch texture " + textureStr + " without an asset service"); |
||
82 | httpResponse.StatusCode = (int)System.Net.HttpStatusCode.NotFound; |
||
83 | } |
||
84 | |||
85 | UUID textureID; |
||
86 | if (!String.IsNullOrEmpty(textureStr) && UUID.TryParse(textureStr, out textureID)) |
||
87 | { |
||
88 | // m_log.DebugFormat("[GETTEXTURE]: Received request for texture id {0}", textureID); |
||
89 | |||
90 | string[] formats; |
||
91 | if (!string.IsNullOrEmpty(format)) |
||
92 | { |
||
93 | formats = new string[1] { format.ToLower() }; |
||
94 | } |
||
95 | else |
||
96 | { |
||
97 | formats = WebUtil.GetPreferredImageTypes(httpRequest.Headers.Get("Accept")); |
||
98 | if (formats.Length == 0) |
||
99 | formats = new string[1] { DefaultFormat }; // default |
||
100 | |||
101 | } |
||
102 | // OK, we have an array with preferred formats, possibly with only one entry |
||
103 | |||
104 | httpResponse.StatusCode = (int)System.Net.HttpStatusCode.NotFound; |
||
105 | foreach (string f in formats) |
||
106 | { |
||
107 | if (FetchTexture(httpRequest, httpResponse, textureID, f)) |
||
108 | break; |
||
109 | } |
||
110 | } |
||
111 | else |
||
112 | { |
||
113 | m_log.Warn("[GETTEXTURE]: Failed to parse a texture_id from GetTexture request: " + httpRequest.Url); |
||
114 | } |
||
115 | |||
116 | // m_log.DebugFormat( |
||
117 | // "[GETTEXTURE]: For texture {0} sending back response {1}, data length {2}", |
||
118 | // textureID, httpResponse.StatusCode, httpResponse.ContentLength); |
||
119 | |||
120 | return null; |
||
121 | } |
||
122 | |||
123 | /// <summary> |
||
124 | /// |
||
125 | /// </summary> |
||
126 | /// <param name="httpRequest"></param> |
||
127 | /// <param name="httpResponse"></param> |
||
128 | /// <param name="textureID"></param> |
||
129 | /// <param name="format"></param> |
||
130 | /// <returns>False for "caller try another codec"; true otherwise</returns> |
||
131 | private bool FetchTexture(IOSHttpRequest httpRequest, IOSHttpResponse httpResponse, UUID textureID, string format) |
||
132 | { |
||
133 | // m_log.DebugFormat("[GETTEXTURE]: {0} with requested format {1}", textureID, format); |
||
134 | AssetBase texture; |
||
135 | |||
136 | string fullID = textureID.ToString(); |
||
137 | if (format != DefaultFormat) |
||
138 | fullID = fullID + "-" + format; |
||
139 | |||
140 | if (!String.IsNullOrEmpty(m_RedirectURL)) |
||
141 | { |
||
142 | // Only try to fetch locally cached textures. Misses are redirected |
||
143 | texture = m_assetService.GetCached(fullID); |
||
144 | |||
145 | if (texture != null) |
||
146 | { |
||
147 | if (texture.Type != (sbyte)AssetType.Texture) |
||
148 | { |
||
149 | httpResponse.StatusCode = (int)System.Net.HttpStatusCode.NotFound; |
||
150 | return true; |
||
151 | } |
||
152 | WriteTextureData(httpRequest, httpResponse, texture, format); |
||
153 | } |
||
154 | else |
||
155 | { |
||
156 | string textureUrl = m_RedirectURL + "?texture_id="+ textureID.ToString(); |
||
157 | m_log.Debug("[GETTEXTURE]: Redirecting texture request to " + textureUrl); |
||
158 | httpResponse.StatusCode = (int)OSHttpStatusCode.RedirectMovedPermanently; |
||
159 | httpResponse.RedirectLocation = textureUrl; |
||
160 | return true; |
||
161 | } |
||
162 | } |
||
163 | else // no redirect |
||
164 | { |
||
165 | // try the cache |
||
166 | texture = m_assetService.GetCached(fullID); |
||
167 | |||
168 | if (texture == null) |
||
169 | { |
||
170 | // m_log.DebugFormat("[GETTEXTURE]: texture was not in the cache"); |
||
171 | |||
172 | // Fetch locally or remotely. Misses return a 404 |
||
173 | texture = m_assetService.Get(textureID.ToString()); |
||
174 | |||
175 | if (texture != null) |
||
176 | { |
||
177 | if (texture.Type != (sbyte)AssetType.Texture) |
||
178 | { |
||
179 | httpResponse.StatusCode = (int)System.Net.HttpStatusCode.NotFound; |
||
180 | return true; |
||
181 | } |
||
182 | if (format == DefaultFormat) |
||
183 | { |
||
184 | WriteTextureData(httpRequest, httpResponse, texture, format); |
||
185 | return true; |
||
186 | } |
||
187 | else |
||
188 | { |
||
189 | AssetBase newTexture = new AssetBase(texture.ID + "-" + format, texture.Name, (sbyte)AssetType.Texture, texture.Metadata.CreatorID); |
||
190 | newTexture.Data = ConvertTextureData(texture, format); |
||
191 | if (newTexture.Data.Length == 0) |
||
192 | return false; // !!! Caller try another codec, please! |
||
193 | |||
194 | newTexture.Flags = AssetFlags.Collectable; |
||
195 | newTexture.Temporary = true; |
||
196 | newTexture.Local = true; |
||
197 | m_assetService.Store(newTexture); |
||
198 | WriteTextureData(httpRequest, httpResponse, newTexture, format); |
||
199 | return true; |
||
200 | } |
||
201 | } |
||
202 | } |
||
203 | else // it was on the cache |
||
204 | { |
||
205 | // m_log.DebugFormat("[GETTEXTURE]: texture was in the cache"); |
||
206 | WriteTextureData(httpRequest, httpResponse, texture, format); |
||
207 | return true; |
||
208 | } |
||
209 | } |
||
210 | |||
211 | // not found |
||
212 | // m_log.Warn("[GETTEXTURE]: Texture " + textureID + " not found"); |
||
213 | httpResponse.StatusCode = (int)System.Net.HttpStatusCode.NotFound; |
||
214 | return true; |
||
215 | } |
||
216 | |||
217 | private void WriteTextureData(IOSHttpRequest request, IOSHttpResponse response, AssetBase texture, string format) |
||
218 | { |
||
219 | string range = request.Headers.GetOne("Range"); |
||
220 | |||
221 | if (!String.IsNullOrEmpty(range)) // JP2's only |
||
222 | { |
||
223 | // Range request |
||
224 | int start, end; |
||
225 | if (TryParseRange(range, out start, out end)) |
||
226 | { |
||
227 | // Before clamping start make sure we can satisfy it in order to avoid |
||
228 | // sending back the last byte instead of an error status |
||
229 | if (start >= texture.Data.Length) |
||
230 | { |
||
231 | // m_log.DebugFormat( |
||
232 | // "[GETTEXTURE]: Client requested range for texture {0} starting at {1} but texture has end of {2}", |
||
233 | // texture.ID, start, texture.Data.Length); |
||
234 | |||
235 | // Stricly speaking, as per http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html, we should be sending back |
||
236 | // Requested Range Not Satisfiable (416) here. However, it appears that at least recent implementations |
||
237 | // of the Linden Lab viewer (3.2.1 and 3.3.4 and probably earlier), a viewer that has previously |
||
238 | // received a very small texture may attempt to fetch bytes from the server past the |
||
239 | // range of data that it received originally. Whether this happens appears to depend on whether |
||
240 | // the viewer's estimation of how large a request it needs to make for certain discard levels |
||
241 | // (http://wiki.secondlife.com/wiki/Image_System#Discard_Level_and_Mip_Mapping), chiefly discard |
||
242 | // level 2. If this estimate is greater than the total texture size, returning a RequestedRangeNotSatisfiable |
||
243 | // here will cause the viewer to treat the texture as bad and never display the full resolution |
||
244 | // However, if we return PartialContent (or OK) instead, the viewer will display that resolution. |
||
245 | |||
246 | // response.StatusCode = (int)System.Net.HttpStatusCode.RequestedRangeNotSatisfiable; |
||
247 | // response.AddHeader("Content-Range", String.Format("bytes */{0}", texture.Data.Length)); |
||
248 | // response.StatusCode = (int)System.Net.HttpStatusCode.OK; |
||
249 | response.StatusCode = (int)System.Net.HttpStatusCode.PartialContent; |
||
250 | response.ContentType = texture.Metadata.ContentType; |
||
251 | } |
||
252 | else |
||
253 | { |
||
254 | // Handle the case where no second range value was given. This is equivalent to requesting |
||
255 | // the rest of the entity. |
||
256 | if (end == -1) |
||
257 | end = int.MaxValue; |
||
258 | |||
259 | end = Utils.Clamp(end, 0, texture.Data.Length - 1); |
||
260 | start = Utils.Clamp(start, 0, end); |
||
261 | int len = end - start + 1; |
||
262 | |||
263 | // m_log.Debug("Serving " + start + " to " + end + " of " + texture.Data.Length + " bytes for texture " + texture.ID); |
||
264 | |||
265 | // Always return PartialContent, even if the range covered the entire data length |
||
266 | // We were accidentally sending back 404 before in this situation |
||
267 | // https://issues.apache.org/bugzilla/show_bug.cgi?id=51878 supports sending 206 even if the |
||
268 | // entire range is requested, and viewer 3.2.2 (and very probably earlier) seems fine with this. |
||
269 | // |
||
270 | // We also do not want to send back OK even if the whole range was satisfiable since this causes |
||
271 | // HTTP textures on at least Imprudence 1.4.0-beta2 to never display the final texture quality. |
||
272 | // if (end > maxEnd) |
||
273 | // response.StatusCode = (int)System.Net.HttpStatusCode.OK; |
||
274 | // else |
||
275 | response.StatusCode = (int)System.Net.HttpStatusCode.PartialContent; |
||
276 | |||
277 | response.ContentLength = len; |
||
278 | response.ContentType = texture.Metadata.ContentType; |
||
279 | response.AddHeader("Content-Range", String.Format("bytes {0}-{1}/{2}", start, end, texture.Data.Length)); |
||
280 | |||
281 | response.Body.Write(texture.Data, start, len); |
||
282 | } |
||
283 | } |
||
284 | else |
||
285 | { |
||
286 | m_log.Warn("[GETTEXTURE]: Malformed Range header: " + range); |
||
287 | response.StatusCode = (int)System.Net.HttpStatusCode.BadRequest; |
||
288 | } |
||
289 | } |
||
290 | else // JP2's or other formats |
||
291 | { |
||
292 | // Full content request |
||
293 | response.StatusCode = (int)System.Net.HttpStatusCode.OK; |
||
294 | response.ContentLength = texture.Data.Length; |
||
295 | if (format == DefaultFormat) |
||
296 | response.ContentType = texture.Metadata.ContentType; |
||
297 | else |
||
298 | response.ContentType = "image/" + format; |
||
299 | response.Body.Write(texture.Data, 0, texture.Data.Length); |
||
300 | } |
||
301 | |||
302 | // if (response.StatusCode < 200 || response.StatusCode > 299) |
||
303 | // m_log.WarnFormat( |
||
304 | // "[GETTEXTURE]: For texture {0} requested range {1} responded {2} with content length {3} (actual {4})", |
||
305 | // texture.FullID, range, response.StatusCode, response.ContentLength, texture.Data.Length); |
||
306 | // else |
||
307 | // m_log.DebugFormat( |
||
308 | // "[GETTEXTURE]: For texture {0} requested range {1} responded {2} with content length {3} (actual {4})", |
||
309 | // texture.FullID, range, response.StatusCode, response.ContentLength, texture.Data.Length); |
||
310 | } |
||
311 | |||
312 | /// <summary> |
||
313 | /// Parse a range header. |
||
314 | /// </summary> |
||
315 | /// <remarks> |
||
316 | /// As per http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html, |
||
317 | /// this obeys range headers with two values (e.g. 533-4165) and no second value (e.g. 533-). |
||
318 | /// Where there is no value, -1 is returned. |
||
319 | /// FIXME: Need to cover the case where only a second value is specified (e.g. -4165), probably by returning -1 |
||
320 | /// for start.</remarks> |
||
321 | /// <returns></returns> |
||
322 | /// <param name='header'></param> |
||
323 | /// <param name='start'>Start of the range. Undefined if this was not a number.</param> |
||
324 | /// <param name='end'>End of the range. Will be -1 if no end specified. Undefined if there was a raw string but this was not a number.</param> |
||
325 | private bool TryParseRange(string header, out int start, out int end) |
||
326 | { |
||
327 | start = end = 0; |
||
328 | |||
329 | if (header.StartsWith("bytes=")) |
||
330 | { |
||
331 | string[] rangeValues = header.Substring(6).Split('-'); |
||
332 | |||
333 | if (rangeValues.Length == 2) |
||
334 | { |
||
335 | if (!Int32.TryParse(rangeValues[0], out start)) |
||
336 | return false; |
||
337 | |||
338 | string rawEnd = rangeValues[1]; |
||
339 | |||
340 | if (rawEnd == "") |
||
341 | { |
||
342 | end = -1; |
||
343 | return true; |
||
344 | } |
||
345 | else if (Int32.TryParse(rawEnd, out end)) |
||
346 | { |
||
347 | return true; |
||
348 | } |
||
349 | } |
||
350 | } |
||
351 | |||
352 | start = end = 0; |
||
353 | return false; |
||
354 | } |
||
355 | |||
356 | private byte[] ConvertTextureData(AssetBase texture, string format) |
||
357 | { |
||
358 | m_log.DebugFormat("[GETTEXTURE]: Converting texture {0} to {1}", texture.ID, format); |
||
359 | byte[] data = new byte[0]; |
||
360 | |||
361 | MemoryStream imgstream = new MemoryStream(); |
||
362 | Bitmap mTexture = new Bitmap(1, 1); |
||
363 | ManagedImage managedImage; |
||
364 | Image image = (Image)mTexture; |
||
365 | |||
366 | try |
||
367 | { |
||
368 | // Taking our jpeg2000 data, decoding it, then saving it to a byte array with regular data |
||
369 | |||
370 | imgstream = new MemoryStream(); |
||
371 | |||
372 | // Decode image to System.Drawing.Image |
||
373 | if (OpenJPEG.DecodeToImage(texture.Data, out managedImage, out image)) |
||
374 | { |
||
375 | // Save to bitmap |
||
376 | mTexture = new Bitmap(image); |
||
377 | |||
378 | EncoderParameters myEncoderParameters = new EncoderParameters(); |
||
379 | myEncoderParameters.Param[0] = new EncoderParameter(Encoder.Quality, 95L); |
||
380 | |||
381 | // Save bitmap to stream |
||
382 | ImageCodecInfo codec = GetEncoderInfo("image/" + format); |
||
383 | if (codec != null) |
||
384 | { |
||
385 | mTexture.Save(imgstream, codec, myEncoderParameters); |
||
386 | // Write the stream to a byte array for output |
||
387 | data = imgstream.ToArray(); |
||
388 | } |
||
389 | else |
||
390 | m_log.WarnFormat("[GETTEXTURE]: No such codec {0}", format); |
||
391 | |||
392 | } |
||
393 | } |
||
394 | catch (Exception e) |
||
395 | { |
||
396 | m_log.WarnFormat("[GETTEXTURE]: Unable to convert texture {0} to {1}: {2}", texture.ID, format, e.Message); |
||
397 | } |
||
398 | finally |
||
399 | { |
||
400 | // Reclaim memory, these are unmanaged resources |
||
401 | // If we encountered an exception, one or more of these will be null |
||
402 | if (mTexture != null) |
||
403 | mTexture.Dispose(); |
||
404 | |||
405 | if (image != null) |
||
406 | image.Dispose(); |
||
407 | |||
408 | if (imgstream != null) |
||
409 | { |
||
410 | imgstream.Close(); |
||
411 | imgstream.Dispose(); |
||
412 | } |
||
413 | } |
||
414 | |||
415 | return data; |
||
416 | } |
||
417 | |||
418 | // From msdn |
||
419 | private static ImageCodecInfo GetEncoderInfo(String mimeType) |
||
420 | { |
||
421 | ImageCodecInfo[] encoders; |
||
422 | encoders = ImageCodecInfo.GetImageEncoders(); |
||
423 | for (int j = 0; j < encoders.Length; ++j) |
||
424 | { |
||
425 | if (encoders[j].MimeType == mimeType) |
||
426 | return encoders[j]; |
||
427 | } |
||
428 | return null; |
||
429 | } |
||
430 | } |
||
431 | } |