OpenSim
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Events Macros
GetTextureRobustHandler.cs
Go to the documentation of this file.
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;
47 
48 namespace OpenSim.Capabilities.Handlers
49 {
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 GetTextureRobustHandler(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 
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 
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 }
long ContentLength
Boolean property indicating whether the content type property actively has been set.
override byte[] ProcessRequest(string path, Stream request, IOSHttpRequest httpRequest, IOSHttpResponse httpResponse)
OpenSim.Framework.Capabilities.Caps Caps
sbyte Type
(sbyte) AssetType enum
Definition: AssetBase.cs:198
OpenSim.Framework.Capabilities.Caps Caps
Asset class. All Assets are reference by this class or a class derived from this class ...
Definition: AssetBase.cs:49
GetTextureRobustHandler(string path, IAssetService assService, string name, string description, string redirectURL)
void AddHeader(string key, string value)
Add a header field and content to the response.
string ID
Asset MetaData ID (transferring from UUID to string ID)
Definition: AssetBase.cs:177
OSHttpStatusCode
HTTP status codes (almost) as defined by W3C in http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html and IETF in http://tools.ietf.org/html/rfc6585