/**
* OLAT - Online Learning and Training<br>
* http://www.olat.org
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); <br>
* you may not use this file except in compliance with the License.<br>
* You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing,<br>
* software distributed under the License is distributed on an "AS IS" BASIS, <br>
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
* See the License for the specific language governing permissions and <br>
* limitations under the License.
* <p>
* Copyright (c) since 2004 at Multimedia- & E-Learning Services (MELS),<br>
* University of Zurich, Switzerland.
* <hr>
* <a href="http://www.openolat.org">
* OpenOLAT - Online Learning and Training</a><br>
* This file has been modified by the OpenOLAT community. Changes are licensed
* under the Apache 2.0 license as the original file.
* <p>
*/
package org.olat.core.gui.media;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.StringTokenizer;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils;
import org.olat.core.CoreSpringFactory;
import org.olat.core.gui.Windows;
import org.olat.core.gui.render.StringOutput;
import org.olat.core.gui.util.bandwidth.SlowBandWidthSimulator;
import org.olat.core.helpers.Settings;
import org.olat.core.logging.AssertException;
import org.olat.core.logging.OLog;
import org.olat.core.logging.Tracing;
import org.olat.core.util.FileUtils;
import org.olat.core.util.StringHelper;
import org.olat.core.util.session.UserSessionManager;
/**
* @author Felix Jost
*/
public class ServletUtil {
private static final OLog log = Tracing.createLoggerFor(ServletUtil.class);
public static void printOutRequestParameters(HttpServletRequest request) {
for(Enumeration<String> names=request.getParameterNames(); names.hasMoreElements(); ) {
String name = names.nextElement();
log.info(name + " :: " + request.getParameter(name));
}
}
public static void printOutRequestHeaders(HttpServletRequest request) {
for(Enumeration<String> headers=request.getHeaderNames(); headers.hasMoreElements(); ) {
String header = headers.nextElement();
log.info(header + " :: " + request.getHeader(header));
}
}
public static boolean acceptJson(HttpServletRequest request) {
boolean acceptJson = false;
for(Enumeration<String> headers=request.getHeaders("Accept"); headers.hasMoreElements(); ) {
String accept = headers.nextElement();
if(accept.contains("application/json")) {
acceptJson = true;
}
}
return acceptJson;
}
/**
* @param httpReq
* @param httpResp
* @param mr
*/
public static void serveResource(HttpServletRequest httpReq, HttpServletResponse httpResp, MediaResource mr) {
boolean debug = log.isDebug();
try {
Long lastModified = mr.getLastModified();
if (lastModified != null) {
// give browser a chance to cache images
long ifModifiedSince = httpReq.getDateHeader("If-Modified-Since");
// TODO: if no such header, what is the return value
long lastMod = lastModified.longValue();
if (ifModifiedSince >= (lastMod / 1000L) * 1000L) {
httpResp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
return;
}
httpResp.setDateHeader("Last-Modified", lastModified.longValue());
}
if (isFlashPseudoStreaming(httpReq, mr)) {
httpResp.setContentType("video/x-flv");
pseudoStreamFlashResource(httpReq, httpResp, mr);
} else {
String mime = mr.getContentType();
if (mime != null) {
httpResp.setContentType(mime);
}
serveFullResource(httpReq, httpResp, mr);
}
// else there is no stream, but probably just headers
// like e.g. in case of a 302 http-redirect
} catch (Exception e) {
if (debug) {
log.warn("client browser abort when serving media resource", e);
}
} finally {
try {
mr.release();
} catch (Exception e) {
//we did our best here to clean up
}
}
}
private static boolean isFlashPseudoStreaming(HttpServletRequest httpReq, MediaResource mr) {
//exclude some mappers which cannot be flash
if(mr instanceof JSONMediaResource) {
return false;
}
String start = httpReq.getParameter("undefined");
if(StringHelper.containsNonWhitespace(start)) {
return true;
}
start = httpReq.getParameter("start");
if(StringHelper.containsNonWhitespace(start)) {
return true;
}
return false;
}
private static void serveFullResource(HttpServletRequest httpReq, HttpServletResponse httpResp, MediaResource mr) {
boolean debug = log.isDebug();
InputStream in = null;
OutputStream out = null;
BufferedInputStream bis = null;
try {
Long size = mr.getSize();
Long lastModified = mr.getLastModified();
//fxdiff FXOLAT-118: accept range to deliver videos for iPad (implementation based on Tomcat)
List<Range> ranges = parseRange(httpReq, httpResp, (lastModified == null ? -1 : lastModified.longValue()), (size == null ? 0 : size.longValue()));
if(ranges != null && mr.acceptRanges()) {
httpResp.setHeader("Accept-Ranges", "bytes");
}
// maybe some more preparations
mr.prepare(httpResp);
in = mr.getInputStream();
// serve the Resource
if (in != null) {
long rstart = 0;
if (debug) {
rstart = System.currentTimeMillis();
}
if (Settings.isDebuging()) {
SlowBandWidthSimulator sbs = Windows.getWindows(CoreSpringFactory.getImpl(UserSessionManager.class).getUserSession(httpReq)).getSlowBandWidthSimulator();
out = sbs.wrapOutputStream(httpResp.getOutputStream());
} else {
out = httpResp.getOutputStream();
}
if (ranges != null && ranges.size() == 1) {
Range range = ranges.get(0);
httpResp.addHeader("Content-Range", "bytes " + range.start + "-" + range.end + "/" + range.length);
long length = range.end - range.start + 1;
if (length < Integer.MAX_VALUE) {
httpResp.setContentLengthLong(length);
} else {
// Set the content-length as String to be able to use a long
httpResp.setHeader("content-length", "" + length);
}
httpResp.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
try {
httpResp.setBufferSize(2048);
} catch (IllegalStateException e) {
// Silent catch
}
copy(out, in, range);
} else {
if (size != null) {
httpResp.setContentLengthLong(size.longValue());
}
int bufferSize = httpResp.getBufferSize();
// buffer input stream
bis = new BufferedInputStream(in, bufferSize);
IOUtils.copyLarge(bis, out, new byte[bufferSize]);
}
if (debug) {
long rstop = System.currentTimeMillis();
log.debug("time to serve (mr="+mr.getClass().getName()+") "+ (size == null ? "n/a" : "" + size) + " bytes: " + (rstop - rstart));
}
}
} catch (IOException e) {
FileUtils.closeSafely(out);
String className = e.getClass().getSimpleName();
if("ClientAbortException".equals(className)) {
if(log.isDebug()) {//video generate a lot of these errors
log.warn("client browser probably abort when serving media resource", e);
}
} else {
log.error("client browser probably abort when serving media resource", e);
}
} finally {
IOUtils.closeQuietly(bis);
IOUtils.closeQuietly(in);
}
}
//fxdiff FXOLAT-118: accept range to deliver videos for iPad
protected static void copy(OutputStream ostream, InputStream resourceInputStream, Range range) throws IOException {
IOException exception = null;
InputStream istream = new BufferedInputStream(resourceInputStream, 2048);
exception = copyRange(istream, ostream, range.start, range.end);
// Clean up the input stream
istream.close();
// Rethrow any exception that has occurred
if (exception != null) throw exception;
}
//fxdiff FXOLAT-118: accept range to deliver videos for iPad
protected static IOException copyRange(InputStream istream, OutputStream ostream, long start, long end) {
try {
istream.skip(start);
} catch (IOException e) {
return e;
}
IOException exception = null;
long bytesToRead = end - start + 1;
byte buffer[] = new byte[2048];
int len = buffer.length;
while ((bytesToRead > 0) && (len >= buffer.length)) {
try {
len = istream.read(buffer);
if (bytesToRead >= len) {
ostream.write(buffer, 0, len);
bytesToRead -= len;
} else {
ostream.write(buffer, 0, (int) bytesToRead);
bytesToRead = 0;
}
} catch (IOException e) {
exception = e;
len = -1;
}
if (len < buffer.length) break;
}
return exception;
}
//fxdiff FXOLAT-118: accept range to deliver videos for iPad
protected static List<Range> parseRange(HttpServletRequest request, HttpServletResponse response, long lastModified, long fileLength)
throws IOException {
String headerValue = request.getHeader("If-Range");
if (headerValue != null) {
long headerValueTime = (-1L);
try {
headerValueTime = request.getDateHeader("If-Range");
} catch (IllegalArgumentException e) {
//
}
if (headerValueTime != (-1L)) {
// If the timestamp of the entity the client got is older than
// the last modification date of the entity, the entire entity
// is returned.
if (lastModified > (headerValueTime + 1000))
return Collections.emptyList();
}
}
if (fileLength == 0) return null;
// Retrieving the range header (if any is specified
String rangeHeader = request.getHeader("Range");
if (rangeHeader == null) return null;
// bytes is the only range unit supported (and I don't see the point
// of adding new ones).
if (!rangeHeader.startsWith("bytes")) {
response.addHeader("Content-Range", "bytes */" + fileLength);
response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
return null;
}
rangeHeader = rangeHeader.substring(6);
// Vector which will contain all the ranges which are successfully
// parsed.
List<Range> result = new ArrayList<Range>();
StringTokenizer commaTokenizer = new StringTokenizer(rangeHeader, ",");
// Parsing the range list
while (commaTokenizer.hasMoreTokens()) {
String rangeDefinition = commaTokenizer.nextToken().trim();
Range currentRange = new Range();
currentRange.length = fileLength;
int dashPos = rangeDefinition.indexOf('-');
if (dashPos == -1) {
response.addHeader("Content-Range", "bytes */" + fileLength);
response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
return null;
}
if (dashPos == 0) {
try {
long offset = Long.parseLong(rangeDefinition);
currentRange.start = fileLength + offset;
currentRange.end = fileLength - 1;
} catch (NumberFormatException e) {
response.addHeader("Content-Range", "bytes */" + fileLength);
response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
return null;
}
} else {
try {
currentRange.start = Long.parseLong(rangeDefinition.substring(0, dashPos));
if (dashPos < rangeDefinition.length() - 1) currentRange.end = Long.parseLong(rangeDefinition.substring(dashPos + 1,
rangeDefinition.length()));
else currentRange.end = fileLength - 1;
} catch (NumberFormatException e) {
response.addHeader("Content-Range", "bytes */" + fileLength);
response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
return null;
}
}
if (!currentRange.validate()) {
response.addHeader("Content-Range", "bytes */" + fileLength);
response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
return null;
}
result.add(currentRange);
}
return result;
}
private static void pseudoStreamFlashResource(HttpServletRequest httpReq, HttpServletResponse httpResp, MediaResource mr) {
Long range = getRange(httpReq);
long seekPos = range == null ? 0l : range.longValue();
long fileSize = mr.getSize() - ((seekPos > 0) ? seekPos + 1 : 0);
InputStream s = null;
OutputStream out = null;
try {
s = new BufferedInputStream(mr.getInputStream());
out = httpResp.getOutputStream();
if(seekPos == 0) {
httpResp.addHeader("Content-Length", Long.toString(fileSize));
} else {
httpResp.addHeader("Content-Length", Long.toString(fileSize + 13));
byte[] flvHeader = new byte[] {70, 76, 86, 1, 1, 0, 0, 0, 9, 0, 0, 0, 9};
out.write(flvHeader);
}
s.skip(seekPos);
final int bufferSize = 1024 * 10;
long left = fileSize;
while (left > 0) {
int howMuch = bufferSize;
if (howMuch > left) {
howMuch = (int) left;
}
byte[] buf = new byte[howMuch];
int numRead = s.read(buf);
out.write(buf, 0, numRead);
httpResp.flushBuffer();
if (numRead == -1) {
break;
}
left -= numRead;
}
}
catch (Exception e) {
log.error("", e);
if (e.getClass().getName().contains("Eof")) {
//ignore
} else {
throw new RuntimeException(e);
}
} finally {
FileUtils.closeSafely(s);
}
}
private static Long getRange(HttpServletRequest httpReq) {
if (httpReq.getParameter("start") != null) {
return Long.parseLong(httpReq.getParameter("start"));
} else if (httpReq.getParameter("undefined") != null) {
return Long.parseLong(httpReq.getParameter("undefined"));
}
return null;
}
/**
* @param response
* @param result
*/
public static void serveStringResource(HttpServletRequest httpReq, HttpServletResponse response, String result) {
setStringResourceHeaders(response);
// log the response headers prior to sending the output
boolean isDebug = log.isDebug();
if (isDebug) {
log.debug("\nResponse headers (some)\ncontent type:" + response.getContentType() + "\ncharacterencoding:"
+ response.getCharacterEncoding() + "\nlocale:" + response.getLocale());
}
try {
long rstart = 0;
if (isDebug) {
rstart = System.currentTimeMillis();
}
// make a ByteArrayOutputStream to be able to determine the length.
// buffer size: assume average length of a char in bytes is max 2
ByteArrayOutputStream baos = new ByteArrayOutputStream(result.length() * 2);
// we ignore the accept-charset from the request and always write in
// utf-8:
// we have lots of different languages (content) in one application to
// support, and more importantly,
// a blend of olat translations and content by authors which can be in
// different languages.
OutputStreamWriter osw = new OutputStreamWriter(baos, "utf-8");
osw.write(result);
osw.close();
// the data is now utf-8 encoded in the bytearray -> push it into the outputstream
int encLen = baos.size();
response.setContentLength(encLen);
OutputStream os;
if (Settings.isDebuging()) {
SlowBandWidthSimulator sbs = Windows.getWindows(CoreSpringFactory.getImpl(UserSessionManager.class).getUserSession(httpReq)).getSlowBandWidthSimulator();
os = sbs.wrapOutputStream(response.getOutputStream());
} else {
os = response.getOutputStream();
}
byte[] bout = baos.toByteArray();
os.write(bout);
os.close();
if (isDebug) {
long rstop = System.currentTimeMillis();
log.debug("time to serve inline-resource " + result.length() + " chars / " + encLen + " bytes: "
+ (rstop - rstart));
}
} catch (IOException e) {
if (isDebug) {
log.warn("client browser abort when serving inline", e);
}
}
}
public static void serveStringResource(HttpServletResponse response, StringOutput result) {
setStringResourceHeaders(response);
// log the response headers prior to sending the output
boolean isDebug = log.isDebug();
if (isDebug) {
log.debug("\nResponse headers (some)\ncontent type:" + response.getContentType() + "\ncharacterencoding:"
+ response.getCharacterEncoding() + "\nlocale:" + response.getLocale());
}
try {
long rstart = 0;
if (isDebug || true) {
rstart = System.currentTimeMillis();
}
// make a ByteArrayOutputStream to be able to determine the length.
// buffer size: assume average length of a char in bytes is max 2
int encLen = result.length();
Reader reader = result.getReader();
//response.setContentLength(encLen); set the number of characters, must be number of bytes
PrintWriter os = response.getWriter();
IOUtils.copy(reader, os);
os.close();
if (isDebug) {
log.debug("time to serve inline-resource " + result.length() + " chars / " + encLen + " bytes: "
+ (System.currentTimeMillis() - rstart));
}
} catch (IOException e) {
if (isDebug) {
log.warn("client browser abort when serving inline", e);
}
}
}
public static void setStringResourceHeaders(HttpServletResponse response) {
// we ignore the accept-charset from the request and always write in utf-8
// -> see comment below
response.setContentType("text/html;charset=utf-8");
// never allow to cache pages since they contain a timestamp valid only once
// HTTP 1.1
response.setHeader("Cache-Control", "private, no-cache, no-store, must-revalidate, proxy-revalidate, s-maxage=0, max-age=0");
// HTTP 1.0
response.setHeader("Pragma", "no-cache");
response.setDateHeader("Expires", 0);
}
public static void setJSONResourceHeaders(HttpServletResponse response) {
// we ignore the accept-charset from the request and always write in utf-8
// -> see comment below
//response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
// never allow to cache pages since they contain a timestamp valid only once
// HTTP 1.1
response.setHeader("Cache-Control", "private, no-cache, no-store, must-revalidate, proxy-revalidate, s-maxage=0, max-age=0");
// HTTP 1.0
response.setHeader("Pragma", "no-cache");
response.setDateHeader("Expires", 0);
}
/**
* Return a context-relative path, beginning with a "/", that represents the
* canonical version of the specified path
* <p>
* ".." and "." elements are resolved out. If the specified path attempts to
* go outside the boundaries of the current context (i.e. too many ".." path
* elements are present), return <code>null</code> instead.
* <p>
*
* @author Mike Stock
*
* @param path Path to be normalized
* @return the normalized path
*/
public static String normalizePath(String path) {
if (path == null) return null;
// Create a place for the normalized path
String normalized = path;
try { // we need to decode potential UTF-8 characters in the URL
normalized = new String(normalized.getBytes(), "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new AssertException("utf-8 encoding must be supported on all java platforms...");
}
if (normalized.equals("/.")) return "/";
// Normalize the slashes and add leading slash if necessary
if (normalized.indexOf('\\') >= 0) normalized = normalized.replace('\\', '/');
if (!normalized.startsWith("/")) normalized = "/" + normalized;
// Resolve occurrences of "//" in the normalized path
while (true) {
int index = normalized.indexOf("//");
if (index < 0) break;
normalized = normalized.substring(0, index) + normalized.substring(index + 1);
}
// Resolve occurrences of "/./" in the normalized path
while (true) {
int index = normalized.indexOf("/./");
if (index < 0) break;
normalized = normalized.substring(0, index) + normalized.substring(index + 2);
}
// Resolve occurrences of "/../" in the normalized path
while (true) {
int index = normalized.indexOf("/../");
if (index < 0) break;
if (index == 0) return (null); // Trying to go outside our context
int index2 = normalized.lastIndexOf('/', index - 1);
normalized = normalized.substring(0, index2) + normalized.substring(index + 3);
}
// Return the normalized path that we have completed
return (normalized);
}
/**
* Extracts a quoted value from a header that has a given key. For instance if the header is
* <p>
* content-disposition=form-data; name="my field"
* and the key is name then "my field" will be returned without the quotes.
*
* @param header The header
* @param key The key that identifies the token to extract
* @return The token, or null if it was not found
*/
public static String extractQuotedValueFromHeader(final String header, final String key) {
int keypos = 0;
int pos = -1;
boolean inQuotes = false;
for (int i = 0; i < header.length() - 1; ++i) { //-1 because we need room for the = at the end
//TODO: a more efficient matching algorithm
char c = header.charAt(i);
if (inQuotes) {
if (c == '"') {
inQuotes = false;
}
} else {
if (key.charAt(keypos) == c) {
keypos++;
} else if (c == '"') {
keypos = 0;
inQuotes = true;
} else {
keypos = 0;
}
if (keypos == key.length()) {
if (header.charAt(i + 1) == '=') {
pos = i + 2;
break;
} else {
keypos = 0;
}
}
}
}
if (pos == -1) {
return null;
}
int end;
int start = pos;
if (header.charAt(start) == '"') {
start++;
for (end = start; end < header.length(); ++end) {
char c = header.charAt(end);
if (c == '"') {
break;
}
}
return header.substring(start, end);
} else {
//no quotes
for (end = start; end < header.length(); ++end) {
char c = header.charAt(end);
if (c == ' ' || c == '\t' || c == ';') {
break;
}
}
return header.substring(start, end);
}
}
//fxdiff FXOLAT-118: accept range to deliver videos for iPad
protected static class Range {
public long start;
public long end;
public long length;
/**
* Validate range.
*/
public boolean validate() {
if (end >= length)
end = length - 1;
return ( (start >= 0) && (end >= 0) && (start <= end)
&& (length > 0) );
}
public void recycle() {
start = 0;
end = 0;
length = 0;
}
@Override
public String toString() {
return start + "-" + end + "/" + length;
}
}
}