/*
* Digital Audio Access Protocol (DAAP) Library
* Copyright (C) 2004-2010 Roger Kapsi
*
* 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.ardverk.daap;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Locale;
import java.util.NoSuchElementException;
import java.util.StringTokenizer;
import org.apache.commons.codec.binary.Base64;
import org.apache.http.Header;
import org.ardverk.daap.chunks.Chunk;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* DaapRequestProcessor processes a DaapRequest and generates the appropriate
* DaapResponse.
*
* @author Roger Kapsi
*/
public class DaapRequestProcessor {
private static final Logger LOG
= LoggerFactory.getLogger(DaapRequestProcessor.class);
private DaapResponseFactory factory;
/** Creates a new instance of DaapRequestProcessor */
public DaapRequestProcessor(DaapResponseFactory factory) {
this.factory = factory;
}
/**
* Processes the request and returns the appropiate DaapResponse (note: can
* be null which is valid and means basically <tt>do nothing</tt>). Invalid
* requests and all other errors throw an IOException which should result in
* an immediate disconnect!
*
* @param request
* @throws IOException
* @return a DaapResponse for the request
*/
public DaapResponse process(DaapRequest request) throws IOException {
if (request == null || request.isUnknownRequest()) {
throw new IOException("Unknown request: " + request);
}
// Unlock code in processUpdateRequest()
request.getConnection().lock();
if (request.isSongRequest()) {
return processSongRequest(request);
} else if (request.isServerInfoRequest()) {
return processServerInfoRequest(request);
} else if (request.isLogoutRequest()) {
return processLogoutRequest(request);
} else {
if (!isAuthenticated(request)) {
return factory.createAuthResponse(request);
}
if (request.isContentCodesRequest()) {
return processContentCodesRequest(request);
} else if (request.isLoginRequest()) {
return processLoginRequest(request);
// The following requests require a vaild
// session id
} else if (validateSessionId(request)) {
if (request.isUpdateRequest()) {
return processUpdateRequest(request);
} else if (request.isDatabasesRequest()) {
return processDatabasesRequest(request);
} else if (request.isDatabaseSongsRequest()) {
return processDatabaseSongsRequest(request);
} else if (request.isDatabasePlaylistsRequest()) {
return processDatabasePlaylistsRequest(request);
} else if (request.isPlaylistSongsRequest()) {
return processPlaylistSongsRequest(request);
} else if (request.isResolveRequest()) {
return processResolveRequest(request);
}
} else {
throw new IOException("Invalid session-id: " + request);
}
}
throw new IOException("Unhandled request: " + request);
}
/**
* Returns <tt>true</tt> if request is authenticated or if no authentication
* is required (if disabled).
*/
private boolean isAuthenticated(DaapRequest request)
throws UnsupportedEncodingException {
if (request.isServerSideRequest()) {
return true;
}
DaapConnection connection = request.getConnection();
DaapServer<?> server = request.getServer();
DaapConfig config = server.getConfig();
DaapAuthenticator authenticator = server.getAuthenticator();
if (authenticator == null) {
return true;
}
if (config.getAuthenticationMethod().equals(DaapConfig.NO_PASSWORD)) {
return true;
}
Header authHeader = request.getHeader(DaapRequest.AUTHORIZATION);
if (authHeader == null) {
if (LOG.isInfoEnabled()) {
LOG.info(DaapRequest.AUTHORIZATION + " header is not set");
}
return false;
}
String authValue = authHeader.getValue();
Object scheme = config.getAuthenticationScheme();
if (scheme.equals(DaapConfig.BASIC_SCHEME)) {
StringTokenizer tok = new StringTokenizer(authHeader.getValue(),
" ");
if (tok.nextToken().equals("Basic") == false) {
if (LOG.isInfoEnabled()) {
LOG.info("Schemes mismatch");
}
return false;
}
byte[] logpass = Base64.decodeBase64(tok.nextToken().getBytes(
DaapUtil.ISO_8859_1));
int q = 0;
for (; q < logpass.length && logpass[q] != ':'; q++)
;
String username = new String(logpass, 0, q, DaapUtil.UTF_8);
q++;
String password = new String(logpass, q, logpass.length - q,
DaapUtil.UTF_8);
// Success!
return authenticator.authenticate(username, password, null, null);
} else {
if (!authValue.startsWith("Digest")) {
if (LOG.isInfoEnabled()) {
LOG.info("Schemes mismatch");
}
return false;
}
int beginIndex = "Digest".length() + 1;
if (beginIndex >= authValue.length()) {
if (LOG.isInfoEnabled()) {
LOG.info("Illegal Authorization Header");
}
return false;
}
// TODO: better/safer splitting
String[] values = authValue.substring(beginIndex).split(", ");
String username = null;
String nonce = null;
String uri = null;
String response = null;
for (int i = 0; i < values.length; i++) {
String[] kv = values[i].split("=", 2);
if (kv.length != 2) {
if (LOG.isInfoEnabled()) {
LOG.info("Illegal Authorization Header: " + values[i]);
}
return false;
}
if (!kv[1].startsWith("\"") || !kv[1].endsWith("\"")) {
if (LOG.isInfoEnabled()) {
LOG.info("Illegal Authorization Header: " + values[i]);
}
return false;
}
String key = kv[0].trim().toLowerCase(Locale.US);
String value = kv[1].substring(1, kv[1].length() - 1);
if (key.equals("username")) {
username = value;
} else if (key.equals("nonce")) {
nonce = value;
} else if (key.equals("uri")) {
uri = value;
} else if (key.equals("response")) {
response = value;
}
if (username != null && nonce != null && uri != null
&& response != null) {
break;
}
}
if (username == null) {
LOG.info("Username is null");
return false;
}
if (nonce == null) {
LOG.info("Nonce is null");
return false;
}
if (uri == null) {
LOG.info("URI is null");
return false;
}
if (response == null) {
LOG.info("Response is null");
return false;
}
String currentNonce = connection.getNonce();
if (currentNonce == null || !currentNonce.equals(nonce)) {
if (LOG.isInfoEnabled()) {
LOG.info("Nonce mismatch: " + currentNonce + " vs. "
+ nonce);
}
return false;
}
/*
* byte[] password = authenticator.getPassword(username,
* DaapConfig.DIGEST_SCHEME); if (password == null) { if
* (LOG.isInfoEnabled()) { LOG.info("Password is null"); } return
* false; }
*
* String ha1 = DaapUtil.toHexString(password); String ha2 =
* DaapUtil.calculateHA2(uri);
*
* String digest = DaapUtil.digest(ha1, ha2, nonce);
*
* // Success? return digest.equalsIgnoreCase(response);
*/
return authenticator.authenticate(username, response, uri, nonce);
}
}
/**
* Checks if the SessionId of the request is valid
*/
private boolean validateSessionId(DaapRequest request) {
DaapConnection connection = request.getConnection();
DaapSession session = connection.getSession(false);
if (session != null) {
return session.getSessionId().equals(request.getSessionId());
}
return false;
}
/**
*
* @param request
* @throws IOException
* @return
*/
protected DaapResponse processServerInfoRequest(DaapRequest request)
throws IOException {
Library library = request.getHeadLibrary();
if (library == null) {
throw new IOException("Connection is not associated with a Library");
}
Chunk chunk = (Chunk) library.select(request);
if (chunk == null) {
// request was either illegal or the protocol version
// is not supported
throw new IOException(
"library.select(ServerInfoRequest) returned null");
}
byte[] data = DaapUtil.serialize(chunk, request.isGZIPSupported());
return factory.createChunkResponse(request, data);
}
/**
*
* @param request
* @throws IOException
* @return
*/
protected DaapResponse processContentCodesRequest(DaapRequest request)
throws IOException {
Library library = request.getHeadLibrary();
if (library == null) {
throw new IOException("Connection is not associated with a Library");
}
Chunk chunk = (Chunk) library.select(request);
if (chunk == null) {
// in theory not possible
throw new IOException(
"library.select(ContentCodesRequest) returned null");
}
byte[] data = DaapUtil.serialize(chunk, request.isGZIPSupported());
return factory.createChunkResponse(request, data);
}
/**
*
* @param request
* @throws IOException
* @return
*/
protected DaapResponse processLoginRequest(DaapRequest request)
throws IOException {
if (!request.getSessionId().equals(SessionId.INVALID)) {
throw new IOException("Session ID cannot exist: "
+ request.getSessionId());
}
Library library = request.getHeadLibrary();
if (library == null) {
throw new IOException("Connection is not associated with a Library");
}
// Create session...
DaapConnection connection = request.getConnection();
DaapSession session = connection.getSession(true);
request.setSessionId(session.getSessionId());
Chunk chunk = (Chunk) library.select(request);
if (chunk == null) {
throw new IOException("library.select(LoginRequest) returned null");
}
byte[] data = DaapUtil.serialize(chunk, request.isGZIPSupported());
return factory.createChunkResponse(request, data);
}
/**
*
* @param request
* @throws IOException
* @return
*/
protected DaapResponse processLogoutRequest(DaapRequest request)
throws IOException {
DaapConnection connection = request.getConnection();
DaapSession session = connection.getSession(false);
if (request.isKeepConnectionAlive() && session != null) {
return factory.createNoContentResponse(request);
}
// Do nothing, just throw a IOE which will disconnect the client
throw new IOException("Logout");
}
/**
*
* @return
* @param request
* @throws IOException
*/
protected DaapResponse processUpdateRequest(DaapRequest request)
throws IOException {
Library library = request.nextLibrary();
if (library == null) {
throw new IOException("Connection is not associated with a Library");
}
if (library.getRevision() == request.getDelta()) {
request.getConnection().unlock();
DaapSession session = request.getConnection().getSession(false);
if (session == null) {
throw new IOException(
"Connection is not associated with a Session");
}
session.setAttribute("CLIENT_REVISION",
library.getRevision());
return null;
}
Chunk chunk = (Chunk) library.select(request);
if (chunk == null) {
throw new IOException("library.select(UpdateRequest) returned null");
}
byte[] data = DaapUtil.serialize(chunk, request.isGZIPSupported());
return factory.createChunkResponse(request, data);
}
/**
*
* @param request
* @throws IOException
* @return
*/
protected DaapResponse processDatabasesRequest(DaapRequest request)
throws IOException {
Library library = request.getHeadLibrary();
if (library == null) {
throw new IOException("Connection is not associated with a Library");
}
Chunk serverDatabases = (Chunk) library.select(request);
if (serverDatabases == null) {
// request was either illegal or the requested revision
// is no longer available (server updateded to fast and
// this client couldn't keep up)
throw new IOException(
"library.select(DatabasesRequest) returned null");
}
byte[] data = DaapUtil.serialize(serverDatabases, request
.isGZIPSupported());
return factory.createChunkResponse(request, data);
}
/**
*
* @param request
* @throws IOException
* @return
*/
protected DaapResponse processDatabaseSongsRequest(DaapRequest request)
throws IOException {
Library library = request.getHeadLibrary();
if (library == null) {
throw new IOException("Connection is not associated with a Library");
}
Chunk databaseSongs = (Chunk) library.select(request);
if (databaseSongs == null) {
// see processDatabasesRequest()
throw new IOException(
"library.select(DatabaseSongsRequest) returned null");
}
byte[] data = DaapUtil.serialize(databaseSongs, request
.isGZIPSupported());
return factory.createChunkResponse(request, data);
}
/**
*
* @param request
* @throws IOException
* @return
*/
protected DaapResponse processDatabasePlaylistsRequest(DaapRequest request)
throws IOException {
Library library = request.getHeadLibrary();
if (library == null) {
throw new IOException("Connection is not associated with a Library");
}
Chunk databasePlaylists = (Chunk) library.select(request);
if (databasePlaylists == null) {
// see processDatabasesRequest()
throw new IOException(
"library.select(DatabasePlaylists) returned null");
}
byte[] data = DaapUtil.serialize(databasePlaylists, request
.isGZIPSupported());
return factory.createChunkResponse(request, data);
}
/**
*
* @param request
* @throws IOException
* @return
*/
protected DaapResponse processPlaylistSongsRequest(DaapRequest request)
throws IOException {
Library library = request.getHeadLibrary();
if (library == null) {
throw new IOException("Connection is not associated with a Library");
}
Chunk playlistSongs = (Chunk) library.select(request);
if (playlistSongs == null) {
// see processDatabasesRequest()
throw new IOException("library.select(PlaylistSongs) returned null");
}
byte[] data = DaapUtil.serialize(playlistSongs, request
.isGZIPSupported());
return factory.createChunkResponse(request, data);
}
/**
* Isn't implemented
*
* @param request
* @throws IOException
* @return
*/
protected DaapResponse processResolveRequest(DaapRequest request)
throws IOException {
throw new IOException("Resolve is not implemented");
}
/**
*
* @param request
* @throws IOException
* @return
*/
protected DaapResponse processSongRequest(DaapRequest request)
throws IOException {
Library library = request.getLibrary();
if (library == null) {
throw new IOException("Connection is not associated with a Library");
}
DaapConnection connection = request.getConnection();
DaapServer<?> server = connection.getServer();
DaapStreamSource streamSource = server.getStreamSource();
if (streamSource != null) {
long[] range = getRange(request);
if (range == null) {
throw new IOException("getRange returned null");
}
long pos = range[0];
long end = range[1];
Song song = (Song) library.select(request);
if (song == null) {
throw new IOException(
"Library returned null-Song for request: " + request);
}
if (end == -1) {
end = song.getSize();
}
Object src = streamSource.getSource(song);
if (src instanceof File) {
return factory.createAudioResponse(request, song, (File) src,
pos, end);
} else if (src instanceof FileInputStream) {
return factory.createAudioResponse(request, song,
(FileInputStream) src, pos, end);
}/*
* else if (src instanceof FileChannel) { return
* factory.createAudioResponse(request, song, (FileChannel)src,
* pos, end); }
*/else {
throw new IOException("Unknown source [" + src + "] for Song: "
+ song);
}
}
return null;
}
/*
* Returns the range which should be streamed.
*
* @param request
*
* @throws IOException
*
* @return
*/
private long[] getRange(DaapRequest request) throws IOException {
Header rangeHeader = request.getHeader("Range");
if (rangeHeader != null) {
try {
StringTokenizer tok = new StringTokenizer(rangeHeader
.getValue(), "=");
String key = tok.nextToken().trim();
if (key.equals("bytes") == false) {
if (LOG.isInfoEnabled())
LOG.info("Unknown range type: " + key);
return null;
}
byte[] range = tok.nextToken().getBytes(DaapUtil.ISO_8859_1);
int q = 0;
for (; q < range.length && range[q] != '-'; q++)
;
long pos = -1;
long end = -1;
pos = Long.parseLong(new String(range, 0, q));
q++;
if (range.length - q != 0) {
end = Long
.parseLong(new String(range, q, range.length - q));
}
return (new long[] { pos, end });
} catch (NoSuchElementException err) {
// not critical, we can recover...
LOG.error("NoSuchElementException", err);
} catch (NumberFormatException err) {
// not critical, we can recover...
LOG.error("NumberFormatException", err);
}
}
// play from begin to end
return (new long[] { 0, -1 });
}
}