package com.sound.ampache.net;
/* Copyright (c) 2008 Kevin James Purdy <purdyk@onid.orst.edu>
* Copyright (c) 2010 Jacob Alexander < haata@users.sf.net >
* Copyright (c) 2014 David Hrdina Nemecek <dejvino@gmail.com>
*
* +------------------------------------------------------------------------+
* | This program is free software; you can redistribute it and/or |
* | modify it under the terms of the GNU General Public License |
* | as published by the Free Software Foundation; either version 2 |
* | of the License, or (at your option) any later version. |
* | |
* | This program 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 General Public License |
* | along with this program; if not, write to the Free Software |
* | Foundation, Inc., 59 Temple Place - Suite 330, |
* | Boston, MA 02111-1307, USA. |
* +------------------------------------------------------------------------+
*/
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import com.sound.ampache.amdroid;
import com.sound.ampache.objects.Directive;
import com.sound.ampache.objects.ampacheObject;
import org.xml.sax.InputSource;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Date;
/**
* Communicator responsible for making network calls to the Ampache XML API interface.
*
* This class is NOT thread safe. Only a single thread should access this class
* (preferably from a non-UI thread, i.e. {@link com.sound.ampache.net.NetworkWorker}).
*/
public class AmpacheApiClient
{
public static String LOG_TAG = "Ampache_Amdroid_Comm";
private String authToken = "";
public int artists;
public int albums;
public int songs;
private String update;
private Context mCtxt;
public String lastErr;
private XMLReader reader;
private SharedPreferences prefs;
public AmpacheApiClient(SharedPreferences preferences, Context context) throws Exception
{
prefs = preferences;
mCtxt = context;
System.setProperty("org.xml.sax.driver", "org.xmlpull.v1.sax2.Driver");
reader = XMLReaderFactory.createXMLReader();
}
/**
* Performs a PING command to the server to check if a session is still valid and to extend its
* lifetime if it is.
*
* After calling this method, consider checking the output of {@link #isAuthenticated()}.
*/
public void ping()
{
AmpacheDataHandler hand = new AmpacheDataHandler();
reader.setContentHandler(hand);
try {
reader.parse(new InputSource(fetchFromServer("auth=" + this.authToken)));
if (hand.errorCode == 401) {
this.perform_auth_request();
}
} catch (MalformedURLException e) {
Log.e(LOG_TAG, "Operation PING failed due to invalid URL: " + e.getMessage(), e);
amdroid.logger.logCritical("Server PING failed", e.getLocalizedMessage());
} catch (Exception e) {
Log.e(LOG_TAG, "Operation PING failed: " + e.getMessage(), e);
amdroid.logger.logCritical("Server PING failed", "Error details: " + e.toString());
}
}
/**
* Performs an authentication request, creating a new session.
*
* After calling this method, consider checking the output of {@link #isAuthenticated()}.
*
* @throws Exception
*/
public void perform_auth_request() throws Exception
{
MessageDigest md = MessageDigest.getInstance("SHA-256");
/* Get the current time, and convert it to a string */
String time = Long.toString((new Date()).getTime() / 1000);
/* build our passphrase hash */
md.reset();
/* first hash the password */
String pwHash = prefs.getString("server_password_preference", "");
md.update(pwHash.getBytes(), 0, pwHash.length());
String preHash = time + asHex(md.digest());
/* then hash the timestamp in */
md.reset();
md.update(preHash.getBytes(), 0, preHash.length());
String hash = asHex(md.digest());
/* request server auth */
AmpacheAuthParser hand = new AmpacheAuthParser();
reader.setContentHandler(hand);
String user = prefs.getString("server_username_preference", "");
try {
reader.parse(new InputSource(fetchFromServer("action=handshake&auth=" + hash + "×tamp=" + time + "&version=350001&user=" + user)));
} catch (Exception e) {
Log.e(LOG_TAG, "Operation AUTH failed: " + e.getMessage(), e);
amdroid.logger.logCritical("Server AUTH failed", "Error Details: " + e.toString());
lastErr = "Could not connect to server";
}
if (hand.errorCode != 0) {
lastErr = hand.error;
amdroid.logger.logCritical("Server AUTH failed", "Error: " + lastErr);
} else {
amdroid.logger.logInfo("Server AUTH successful");
}
authToken = hand.token;
artists = hand.artists;
albums = hand.albums;
songs = hand.songs;
update = hand.update;
}
/**
* @return true when we have an AUTH token, false otherwise.
*/
public boolean isAuthenticated()
{
return authToken != null && !authToken.equals("");
}
/**
* @return Session identifier that is used when communicating with the server.
*/
public String getAuthToken()
{
return authToken;
}
/**
* A generic method for performing the API calls.
*
* @param append Query part of the URL.
* @return Input stream for reading the result.
* @throws Exception
*/
private InputStream fetchFromServer(String append) throws Exception
{
URL fullUrl = new URL(prefs.getString("server_url_preference", "") + "/server/xml.server.php?" + append);
return fullUrl.openStream();
}
public class ampacheRequestHandler extends Thread
{
private AmpacheDataReceiver recv = null;
private AmpacheDataHandler hand;
private Context mCtx;
private String type;
private String filter;
public Handler incomingRequestHandler;
public Boolean stop = false;
public void run()
{
Looper.prepare();
incomingRequestHandler = new Handler()
{
public void handleMessage(Message msg)
{
Directive directive = (Directive) msg.obj;
if (directive == null || directive.action == null) {
Log.e(LOG_TAG, "Cannot handle API request message, null directive or action.");
amdroid.logger.logWarning("Cannot handle API request message, null directive or action.");
return;
}
String error = null;
Message reply = this.obtainMessage();
InputSource dataIn = null;
String append = "action=" + directive.action.getKey();
switch (directive.action) {
case ARTISTS:
hand = new AmpacheArtistParser();
append += "&filter=" + directive.getFilterForUrl();
break;
case ARTIST_ALBUMS:
hand = new AmpacheAlbumParser();
append += "&filter=" + directive.getFilterForUrl();
break;
case ARTIST_SONGS:
hand = new AmpacheSongParser();
append += "&filter=" + directive.getFilterForUrl();
break;
case ALBUM_SONGS:
hand = new AmpacheSongParser();
append += "&filter=" + directive.getFilterForUrl();
break;
case PLAYLIST_SONGS:
hand = new AmpacheSongParser();
append += "&filter=" + directive.getFilterForUrl();
break;
case TAG_ARTISTS:
hand = new AmpacheArtistParser();
append += "&filter=" + directive.getFilterForUrl();
break;
case TAG_SONGS:
hand = new AmpacheSongParser();
append += "&filter=" + directive.getFilterForUrl();
break;
case ALBUMS:
hand = new AmpacheAlbumParser();
append += "&filter=" + directive.getFilterForUrl();
break;
case PLAYLISTS:
hand = new AmpachePlaylistParser();
append += "&filter=" + directive.getFilterForUrl();
break;
case SONGS:
hand = new AmpacheSongParser();
append += "&filter=" + directive.getFilterForUrl();
break;
case TAGS:
hand = new AmpacheTagParser();
append += "&filter=" + directive.getFilterForUrl();
break;
case VIDEOS:
hand = new AmpacheVideoParser();
break;
case SEARCH_SONGS:
hand = new AmpacheSongParser();
append += "&filter=" + directive.getFilterForUrl();
break;
case STATS:
hand = new AmpacheAlbumParser();
append += "&type=" + directive.getFilterForUrl();
break;
default:
throw new RuntimeException("Unhandled API action: " + directive.action);
}
if (msg.what == 0x1336 || msg.what == 0x1337) {
append += "&offset=" + msg.arg1 + "&limit=100";
reply.arg1 = msg.arg1;
reply.arg2 = msg.arg2;
}
append += "&auth=" + authToken;
if (stop == true) {
stop = false;
return;
}
/* now we fetch */
String urlText = prefs.getString("server_url_preference", "") + "/server/xml.server.php?" + append;
try {
URL url = new URL(urlText);
dataIn = new InputSource(url.openStream());
} catch (MalformedURLException e) {
Log.e(LOG_TAG, "Fetching #904 failed: " + e.getMessage(), e);
error = e.toString();
amdroid.logger.logCritical("Failed preparing server request, malformed URL",
"URL used: " + urlText + "\n" + "Error details: " + error);
} catch (Exception e) {
Log.e(LOG_TAG, "Fetching #904 failed: " + e.getMessage(), e);
error = e.toString();
amdroid.logger.logCritical("Failed preparing server request", "Error details: " + error);
}
if (stop == true) {
stop = false;
return;
}
Log.d(LOG_TAG, "Server request URL: " + urlText);
/* all done loading data, now to parse */
reader.setContentHandler(hand);
try {
reader.parse(dataIn);
} catch (Exception e) {
Log.e(LOG_TAG, "Parsing #995 failed: " + e.getMessage(), e);
error = e.toString();
amdroid.logger.logCritical("Failed parsing server response", "URL used: " + urlText + "\n"
+ "Error details: " + error);
}
if (hand.error != null) {
if (hand.errorCode == 401) {
try {
AmpacheApiClient.this.perform_auth_request();
this.sendMessage(msg);
} catch (Exception e) {
Log.e(LOG_TAG, "Operation AUTH #953 failed: " + e.getMessage(), e);
}
return;
}
error = hand.error;
}
if (stop == true) {
stop = false;
return;
}
if (error == null) {
reply.what = msg.what;
reply.obj = hand.data;
} else {
reply.what = 0x1338;
reply.obj = error;
}
try {
msg.replyTo.send(reply);
} catch (Exception e) {
Log.e(LOG_TAG, "Operation REPLY #958 failed: " + e.getMessage(), e);
//well shit, that sucks doesn't it
}
}
};
Looper.loop();
}
}
private static final char[] HEX_CHARS = "0123456789abcdef".toCharArray();
public static String asHex(byte[] buf)
{
char[] chars = new char[2 * buf.length];
for (int i = 0; i < buf.length; ++i) {
chars[2 * i] = HEX_CHARS[(buf[i] & 0xF0) >>> 4];
chars[2 * i + 1] = HEX_CHARS[buf[i] & 0x0F];
}
return new String(chars);
}
}