/*
* Aphelion
* Copyright (c) 2013 Joris van der Wel
*
* This file is part of Aphelion
*
* Aphelion is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* Aphelion is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Aphelion. If not, see <http://www.gnu.org/licenses/>.
*
* In addition, the following supplemental terms apply, based on section 7 of
* the GNU Affero General Public License (version 3):
* a) Preservation of all legal notices and author attributions
* b) Prohibition of misrepresentation of the origin of this material, and
* modified versions are required to be marked in reasonable ways as
* different from the original version (for example by appending a copyright notice).
*
* Linking this library statically or dynamically with other modules is making a
* combined work based on this library. Thus, the terms and conditions of the
* GNU Affero General Public License cover the whole combination.
*
* As a special exception, the copyright holders of this library give you
* permission to link this library with independent modules to produce an
* executable, regardless of the license terms of these independent modules,
* and to copy and distribute the resulting executable under terms of your
* choice, provided that you also meet, for each linked independent module,
* the terms and conditions of the license of that module. An independent
* module is a module which is not derived from or based on this library.
*/
package aphelion.server.http;
import aphelion.server.http.HttpDateUtils.DateParseException;
import aphelion.server.http.HttpUtil.METHOD;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Date;
import java.util.HashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
/**
* @author Joris
*/
class HttpResponse
{
private static final Logger log = Logger.getLogger("aphelion.server.http");
METHOD requestMethod;
HashMap<String, String> requestHeaders;
int status;
String statusMessage;
boolean sendStatusAsContent = true;
boolean close;
File file;
ByteBuffer headers;
RandomAccessFile raf;
long fileBytesSent = 0;
ByteBuffer fileBuffer;
boolean range = false;
long rangeStart = 0;
long rangeEnd = 0;
long rangeLength = 0;
public HttpResponse(METHOD requestMethod, HashMap<String, String> requestHeaders, int status, String statusMessage, boolean close, File file)
{
this.requestMethod = requestMethod;
this.requestHeaders = requestHeaders;
this.status = status;
this.statusMessage = statusMessage;
this.close = close;
this.file = file;
}
public void prepare()
{
assert this.headers == null;
long fileLength = 0;
StringBuilder headerString = new StringBuilder();
boolean sendFile = false;
if (file != null)
{
if (file.isDirectory())
{
file = HttpUtil.findDirectoryIndex(file);
if (file != null)
{
sendFile = true;
}
}
else if (file.isFile())
{
sendFile = true;
}
else
{
if (status == 200)
{
status = 404;
statusMessage = "File Not Found";
}
}
}
Date lastModified = null;
if (sendFile)
{
lastModified = new Date(file.lastModified());
String ifModifiedSince = requestHeaders.get("if-modified-since");
if (ifModifiedSince != null)
{
try
{
Date ifModifiedSinceDate = HttpDateUtils.parseDate(ifModifiedSince);
if (lastModified.after(ifModifiedSinceDate))
{
sendFile = false;
status = 304;
statusMessage = "Not Modified";
sendStatusAsContent = false;
}
}
catch (DateParseException ex)
{
}
}
}
if (sendFile)
{
sendFile = false;
try
{
raf = new RandomAccessFile(file, "r");
fileLength = raf.length();
sendFile = true;
}
catch (FileNotFoundException ex)
{
try
{
raf.close();
}
catch (IOException ex2)
{
}
raf = null;
if (status == 200)
{
status = 404;
statusMessage = "File Not Found";
}
log.log(Level.INFO, "File not found", ex);
}
catch (IOException ex)
{
try
{
raf.close();
}
catch (IOException ex2)
{
}
raf = null;
if (status == 200)
{
status = 404;
statusMessage = "Error reading file";
}
log.log(Level.SEVERE, "Error reading file", ex);
}
String rangeValue = requestHeaders.get("range");
if (sendFile && rangeValue != null)
{
// only 1 range is supported
Matcher rangeMatcher = HttpUtil.simpleRange.matcher(rangeValue);
if (rangeMatcher.matches())
{
range = true;
try
{
String start = rangeMatcher.group(1);
String end = rangeMatcher.group(2);
if (start != null && start.isEmpty())
{
start = null;
}
if (end != null && end.isEmpty())
{
end = null;
}
if (start == null && end == null)
{
throw new NumberFormatException(); // invalid range
}
// The final 500 bytes (byte offsets 9500-9999, inclusive): bytes=-500
if (start == null)
{
rangeStart = fileLength - Long.parseLong(end, 10);
rangeEnd = fileLength - 1;
}
// Or bytes=9500-
else if (end == null)
{
rangeStart = Long.parseLong(start, 10);
rangeEnd = fileLength - 1;
}
else
{
rangeStart = Long.parseLong(start, 10);
rangeEnd = Long.parseLong(end, 10);
}
if (rangeEnd > fileLength)
{
rangeEnd = fileLength - 1;
}
if (rangeEnd < rangeStart)
{
range = false;
}
rangeLength = rangeEnd - rangeStart + 1;
raf.seek(rangeStart);
}
catch (NumberFormatException ex)
{
log.log(Level.WARNING, "", ex);
range = false;
}
catch (IOException ex)
{
range = false;
}
}
}
if (range)
{
status = 206;
}
}
// HTTP/1.1 200 OK\r\n
headerString.append("HTTP/1.1 ");
headerString.append(status);
headerString.append(" ");
headerString.append(statusMessage);
headerString.append("\r\n");
if (close)
{
headerString.append("Connection: close\r\n");
}
if (status == 405)
{
headerString.append("Allow: GET, HEAD\r\n");
}
headerString.append("Server: Aphelion\r\n");
headerString.append("X-Frame-Options: SAMEORIGIN\r\n");
headerString.append("Date: ");
headerString.append(HttpDateUtils.formatDate(new Date()));
headerString.append("\r\n");
if (!sendFile)
{
headerString.append("Content-Type: text/plain; charset=UTF-8\r\n");
if (sendStatusAsContent)
{
headerString.append("Content-Length: ");
headerString.append(HttpUtil.binarySizeUTF8(statusMessage));
headerString.append("\r\n");
}
headerString.append("\r\n"); // end of headers
if (requestMethod != METHOD.HEAD)
{
if (sendStatusAsContent)
{
headerString.append(statusMessage);
}
}
}
else
{
if (range)
{
headerString.append("Content-Range: bytes ");
headerString.append(rangeStart);
headerString.append("-");
headerString.append(rangeEnd);
headerString.append("/");
headerString.append(fileLength);
headerString.append("\r\n");
headerString.append("Content-Length: ");
headerString.append(rangeEnd - rangeStart + 1);
headerString.append("\r\n");
}
else
{
headerString.append("Content-Length: ");
headerString.append(fileLength);
headerString.append("\r\n");
}
assert lastModified != null;
headerString.append("Last-Modified: ");
headerString.append(HttpDateUtils.formatDate(lastModified));
headerString.append("\r\n");
headerString.append("Content-Type: ");
headerString.append(HttpMime.getMime(file));
headerString.append("\r\n");
headerString.append("Accept-Ranges: bytes\r\n");
headerString.append("\r\n");
}
//System.out.println(headerString.toString());
this.headers = ByteBuffer.wrap(headerString.toString().getBytes(HttpUtil.UTF8));
}
/**
* Attempt to write some http resonse stuff on a socket channel.
*
* @return true if there is nothing more to write
*/
public boolean write(SocketChannel channel) throws IOException
{
if (headers != null)
{
if (headers.hasRemaining())
{
if (channel.write(headers) < 0)
{
throw new IOException("closed");
}
}
if (headers.hasRemaining()) // unable to write further, try again later
{
return false;
}
else
{
headers = null;
}
}
if (raf != null)
{
if (fileBuffer == null)
{
fileBuffer = ByteBuffer.wrap(new byte[1024]);
fileBuffer.position(fileBuffer.limit()); // so that hasRemaining returns false
}
while (true)
{
if (!fileBuffer.hasRemaining())
{
int read = raf.read(fileBuffer.array());
if (read == -1)
{
raf.close();
return true;
}
fileBuffer.position(0);
fileBuffer.limit(read);
}
if (fileBuffer.hasRemaining())
{
if (range)
{
long limit = rangeLength - fileBytesSent;
if (fileBuffer.limit() > limit)
{
fileBuffer.limit((int) limit);
}
}
fileBytesSent += channel.write(fileBuffer);
if (range && fileBytesSent >= rangeLength)
{
raf.close();
return true; // done
}
if (fileBuffer.hasRemaining()) // unable to write further, try again later
{
return false;
}
}
}
}
return true; // done
}
}