/**
* Copyright 2009 Marc Stogaitis and Mimi Sun
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.gmote.server;
import java.awt.Container;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.MediaTracker;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.Socket;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import org.gmote.common.FileInfo;
import org.gmote.common.MimeTypeResolver;
import org.gmote.common.FileInfo.FileType;
import org.gmote.server.settings.BaseMediaPaths;
import org.gmote.server.settings.SupportedFiletypeSettings;
import com.sun.image.codec.jpeg.ImageFormatException;
import com.sun.image.codec.jpeg.JPEGCodec;
import com.sun.image.codec.jpeg.JPEGEncodeParam;
import com.sun.image.codec.jpeg.JPEGImageEncoder;
/**
* A server that responds to HTTP requests. It doesn't listen for connections
* like a typical server as we instead share the port used by GmoteServer.
* TcpConnection.java will handle connection routing between HTTP packets and
* Gmote java packets. It will call this class when it notices an HTTP
* connection.
*
* @author Marc Stogaitis
*/
public class GmoteHttpServer {
private static final Logger LOGGER = Logger.getLogger(GmoteHttpServer.class.getName());
private static final int HTTP_OK = 200;
private static final int HTTP_NOT_FOUND = 404;
private Socket connectionSocket;
public GmoteHttpServer(Socket connectionSocket) {
this.connectionSocket = connectionSocket;
}
public void handleHttpRequestAsync(List<String> latestSessionIds) {
HttpConnectionHandler conHandler = new HttpConnectionHandler(latestSessionIds);
new Thread(conHandler).start();
}
/**
*
* @param latestSessionIds
* List of the last 5 session ids that we have seen. We keep more
* than once since there are cases where the client could request a
* song from the media player, re-connect, and then seek to furthur
* in the song, which would cause the media player to do an http
* request with the old session id.
* @throws InterruptedException
* @throws ImageFormatException
*/
private void handleHttpRequest(List<String> latestSessionIds) throws ImageFormatException, InterruptedException {
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(connectionSocket.getInputStream()));
List<String> header = extractHeader(reader);
String requestedUrl = extractFile(header.get(0));
String[] urlSplit = requestedUrl.split("\\?");
if (urlSplit.length < 2 || urlSplit[1].indexOf("=") < 0) {
LOGGER.warning("Encountered a malformed url. It's missing a session param. Ignoring request: " + requestedUrl);
return;
}
String sessionId = getParamValue("sessionId", urlSplit[1]);
if (sessionId == null || !latestSessionIds.contains(sessionId)) {
LOGGER.warning("Encountered a malformed url. It has an incorrect session param. Ignoring request: " + requestedUrl + " -- expected: " + latestSessionIds);
return;
}
File file = new File(urlSplit[0]);
if (!file.exists()) {
throw new FileNotFoundException("The file was not found: " + file.getName());
}
if (!downloadOfFileIsAllowed(file)) {
throw new FileNotFoundException("The user is not authorized to download this type of file. Please make sure that the file is in the base-paths and that the file type of the file is in the supported_filetypes.txt file");
}
long startingByte = extractRange(header);
PrintWriter ps = new PrintWriter(connectionSocket.getOutputStream());
printHeaders(file, startingByte, ps);
if (SupportedFiletypeSettings.fileNameToFileType(file.getName()) == FileType.IMAGE) {
sendImage(file, new BufferedOutputStream(connectionSocket.getOutputStream()));
} else {
sendFile(file, startingByte, new BufferedOutputStream(connectionSocket.getOutputStream()));
}
} catch (UnsupportedEncodingException e) {
LOGGER.log(Level.SEVERE, e.getMessage(), e);
} catch (IOException e) {
LOGGER.log(Level.SEVERE, e.getMessage(), e);
}
}
/**
* Returns true if, and only if, the file meets the following conditions: 1.
* The file must be in a directory that is a child of 'base paths' 2. The file
* must be of a file type that is in the supporte_filetypes.
*
* This is based on the least privilege principle. It helps ensure that
* potential intruders will only have access to media files, and that these
* files are only
*/
private boolean downloadOfFileIsAllowed(File file) {
if (SupportedFiletypeSettings.fileNameToFileType(file.getName()) == FileType.UNKNOWN) {
return false;
}
for (FileInfo path : BaseMediaPaths.getInstance().getBasePaths()) {
// Make sure that we only return paths that exist.
if (file.getAbsolutePath().toLowerCase().startsWith(path.getAbsolutePath().toLowerCase())) {
return true;
}
}
return false;
}
private long extractRange(List<String> headers) {
String headerValue = getHeaderValue("Range", headers);
if (headerValue == null) {
return 0;
}
if (headerValue.startsWith("bytes=")) {
headerValue = headerValue.substring("bytes=".length());
String fields[] = headerValue.split("-");
try {
return Long.parseLong(fields[0]);
} catch (NumberFormatException e) {
LOGGER.log(Level.SEVERE, e.getMessage(), e);
return 0;
}
}
return 0;
}
private String getParamValue(String paramName, String fullParam) {
String[] paramSplit = fullParam.split("=");
if (paramSplit.length != 2) {
return null;
}
if (paramSplit[0].equalsIgnoreCase(paramName)) {
return paramSplit[1];
}
return null;
}
private String getHeaderValue(String fieldName, List<String> headers) {
for (String header : headers) {
LOGGER.info("Header: " + header);
if (header.startsWith(fieldName + ":") ) {
return header.substring(header.indexOf(":") + 1).trim();
}
}
return null;
}
private List<String> extractHeader(BufferedReader reader) throws IOException {
String line = null;
List<String> header = new ArrayList<String>();
while ((line = reader.readLine()) != null && !(line.length()==0)) {
header.add(line);
}
return header;
}
private String extractFile(String fileNameHeaderLine) throws IOException, UnsupportedEncodingException {
LOGGER.info("Extracting file path from: " + fileNameHeaderLine);
String[] fields = fileNameHeaderLine.split(" ");
if (fields.length < 2) {
throw new MalformedURLException("Invalid url. Did not find file name: " + fileNameHeaderLine);
}
String fileName = URLDecoder.decode(fields[0], "UTF-8");
if (!fileName.startsWith("/files/")) {
fileName = URLDecoder.decode(fields[1], "UTF-8");
if (!fileName.startsWith("/files/")) {
LOGGER.warning("Invalid url. Ignoring connection request: " + fileName);
throw new MalformedURLException("Invalid url. Url doesn't start with /files: " + fileName);
}
}
fileName = fileName.substring("/files/".length());
return fileName;
}
void sendFile(File targ, long startingByte, BufferedOutputStream dataOut) throws IOException {
LOGGER.info("Sending file: " + targ.getAbsolutePath() + " offset: " + startingByte);
byte[] buf = new byte[2048];
InputStream is = null;
if (targ.isDirectory()) {
// listDirectory(targ, ps);
return;
} else {
is = new FileInputStream(targ.getAbsolutePath());
if (startingByte != 0) {
long bytesSkipped = is.skip(startingByte);
LOGGER.info("bytesSkipped = " + bytesSkipped);
}
}
try {
int n;
while ((n = is.read(buf)) >= 0) {
dataOut.write(buf, 0, n);
}
} finally {
LOGGER.info("Done sending file");
is.close();
}
dataOut.close();
LOGGER.info("Print stream closed");
}
private void sendImage(File originalImagePath, BufferedOutputStream dataOut) throws InterruptedException, ImageFormatException, IOException {
LOGGER.info("Converting image to smaller scale");
// load image from INFILE
Image image = Toolkit.getDefaultToolkit().getImage(originalImagePath.getAbsolutePath());
MediaTracker mediaTracker = new MediaTracker(new Container());
mediaTracker.addImage(image, 0);
mediaTracker.waitForID(0);
// determine thumbnail size from WIDTH and HEIGHT
int imageWidth = image.getWidth(null);
int imageHeight = image.getHeight(null);
int thumbWidth = imageWidth;
int thumbHeight = imageHeight;
int MAX_SIZE = 500;
if (imageWidth > MAX_SIZE || imageHeight > MAX_SIZE) {
double imageRatio = (double)imageWidth / (double)imageHeight;
if (imageWidth > imageHeight) {
thumbWidth = MAX_SIZE;
thumbHeight = (int) (thumbWidth / imageRatio);
} else {
thumbHeight = MAX_SIZE;
thumbWidth = (int) (thumbHeight * imageRatio);
}
}
// draw original image to thumbnail image object and
// scale it to the new size on-the-fly
BufferedImage thumbImage;
thumbImage = new BufferedImage(thumbWidth,
thumbHeight, BufferedImage.TYPE_INT_RGB);
Graphics2D graphics2D = thumbImage.createGraphics();
graphics2D.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BILINEAR);
graphics2D.drawImage(image, 0, 0, thumbWidth, thumbHeight, null);
if (PlatformUtil.isLinux()) {
ImageIO.write(thumbImage, "JPEG", dataOut);
} else {
JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(dataOut);
JPEGEncodeParam param = encoder.getDefaultJPEGEncodeParam(thumbImage);
float quality = 80;
param.setQuality(quality / 100.0f, false);
encoder.encode(thumbImage, param);
}
dataOut.close();
LOGGER.info("Done sending image");
}
public static BufferedImage shrink(BufferedImage image, int n) {
int w = image.getWidth() / n;
int h = image.getHeight() / n;
BufferedImage shrunkImage =
new BufferedImage(w, h, image.getType());
for (int y=0; y < h; ++y)
for (int x=0; x < w; ++x)
shrunkImage.setRGB(x, y, image.getRGB(x*n, y*n));
return shrunkImage;
}
boolean printHeaders(File targ, long startingByte, PrintWriter pw) throws IOException {
boolean ret = false;
if (!targ.exists()) {
pw.println("HTTP/1.0 " + HTTP_NOT_FOUND + " not found");
ret = false;
} else {
pw.println("HTTP/1.0 " + HTTP_OK + " OK");
ret = true;
}
pw.println("Server: GmoteHttpServer");
pw.println("Date: " + (new Date()));
if (ret) {
long fileLength = targ.length();
if (startingByte != 0) {
pw.println("Content-range: bytes" + startingByte + "-" + (fileLength - 1) + "/" + fileLength);
}
pw.println("Content-length: " + (fileLength - startingByte));
pw.println("Last Modified: " + (new Date(targ.lastModified())));
String name = targ.getName();
String ct = MimeTypeResolver.findMimeType(name);
if (ct.equals(MimeTypeResolver.UNKNOWN_MIME_TYPE)) {
FileType type = SupportedFiletypeSettings.fileNameToFileType(name);
if (type == FileType.MUSIC) {
ct = "audio/unknown";
} else if (type == FileType.VIDEO) {
ct = "video/unknown";
} else {
ct = MimeTypeResolver.findMimeTypeSlow(targ);
}
}
LOGGER.info("Mime type is: " + ct);
pw.println("Content-type: " + ct);
}
pw.println();
pw.flush();
return ret;
}
public class HttpConnectionHandler implements Runnable {
private List<String> latestSessionIds;
public HttpConnectionHandler(List<String> latestSessionIds) {
this.latestSessionIds = latestSessionIds;
}
public void run() {
try {
handleHttpRequest(latestSessionIds);
LOGGER.info("Done handlerequest(). Closing connection.");
} catch (Exception ex) {
// Catching all exceptions since this is the top layer of our app.
LOGGER.log(Level.SEVERE, ex.getMessage(), ex);
try {
PrintWriter ps = new PrintWriter(connectionSocket.getOutputStream());
ps.println("HTTP/1.0 " + HTTP_NOT_FOUND + " not found " + ex.getMessage());
} catch (IOException e) {
LOGGER.log(Level.SEVERE, e.getMessage(), e);
}
} finally {
LOGGER.info("Closing http connection");
try {
connectionSocket.close();
} catch (IOException e) {
LOGGER.log(Level.SEVERE, e.getMessage(), e);
}
}
}
}
}