/*
* Copyright 2004 - 2008 Christian Sprajc. All rights reserved.
*
* This file is part of PowerFolder.
*
* PowerFolder 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.
*
* PowerFolder 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 PowerFolder. If not, see <http://www.gnu.org/licenses/>.
*
* $Id$
*/
package de.dal33t.powerfolder;
import java.awt.Frame;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URLDecoder;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;
import jwf.WizardPanel;
import de.dal33t.powerfolder.clientserver.ServerClient;
import de.dal33t.powerfolder.disk.Folder;
import de.dal33t.powerfolder.disk.FolderRepository;
import de.dal33t.powerfolder.disk.FolderSettings;
import de.dal33t.powerfolder.disk.SyncProfile;
import de.dal33t.powerfolder.light.FileInfo;
import de.dal33t.powerfolder.light.FileInfoFactory;
import de.dal33t.powerfolder.light.FolderInfo;
import de.dal33t.powerfolder.light.MemberInfo;
import de.dal33t.powerfolder.message.Invitation;
import de.dal33t.powerfolder.security.FolderCreatePermission;
import de.dal33t.powerfolder.task.CreateFolderOnServerTask;
import de.dal33t.powerfolder.ui.dialog.DialogFactory;
import de.dal33t.powerfolder.ui.dialog.GenericDialogType;
import de.dal33t.powerfolder.ui.wizard.ChooseDiskLocationPanel;
import de.dal33t.powerfolder.ui.wizard.FolderCreatePanel;
import de.dal33t.powerfolder.ui.wizard.PFWizard;
import de.dal33t.powerfolder.ui.wizard.TextPanelPanel;
import de.dal33t.powerfolder.ui.wizard.WizardContextAttributes;
import de.dal33t.powerfolder.util.Base64;
import de.dal33t.powerfolder.util.BrowserLauncher;
import de.dal33t.powerfolder.util.Convert;
import de.dal33t.powerfolder.util.FileUtils;
import de.dal33t.powerfolder.util.IdGenerator;
import de.dal33t.powerfolder.util.InvitationUtil;
import de.dal33t.powerfolder.util.LoginUtil;
import de.dal33t.powerfolder.util.StringUtils;
import de.dal33t.powerfolder.util.Translation;
import de.dal33t.powerfolder.util.Util;
import de.dal33t.powerfolder.util.Waiter;
/**
* The remote command processor is responsible for binding on a socket and
* receive and process any remote control commands. e.g. Load invitation file,
* process powerfolder links or exit powerfolder.
* <p>
* Supported links:
* <p>
* <code>
* Folder links:
* PowerFolder://|folder|<foldername>|<P or S>|<folderid>|<size>|<numFiles>
* <P or S> P = public, S = secret
* PowerFolder://|folder|test|S|[test-AAgwZXFLgigj222]|99900000|1000
*
* File links:
* PowerFolder://|file|<foldername>|<P or S>|<folderid>|<fullpath_filename>
* <P or S> P = public, S = secret
* PowerFolder://|folder|test|S|[test-AAgwZXFLgigj222]|/test/New_text_docuement.txt
* </code>
*
* @author <a href="mailto:totmacher@powerfolder.com">Christian Sprajc </a>
* @version $Revision: 1.10 $
*/
public class RemoteCommandManager extends PFComponent implements Runnable {
// Parameters according to
// http://www.powerfolder.com/wiki/Script_configuration
private static final String FOLDER_SCRIPT_CONFIG_ID = "id";
private static final String FOLDER_SCRIPT_CONFIG_NAME = "name";
private static final String FOLDER_SCRIPT_CONFIG_DIR = "dir";
private static final String FOLDER_SCRIPT_CONFIG_BACKUP_BY_SERVER = "backup_by_server";
private static final String FOLDER_SCRIPT_CONFIG_SYNC_PROFILE = "syncprofile";
private static final String FOLDER_SCRIPT_CONFIG_SILENT = "silent";
private static final String FOLDER_SCRIPT_CONFIG_DL_SCRIPT = "dlscript";
private static final Logger log = Logger
.getLogger(RemoteCommandManager.class.getName());
// The default prefix for all rcon commands
private static final String REMOTECOMMAND_PREFIX = "PowerFolder_RCON_COMMAND";
// The default encoding
private static final String ENCODING = "UTF8";
// All possible commands
public static final String QUIT = "QUIT";
public static final String OPEN = "OPEN;";
public static final String MAKEFOLDER = "MAKEFOLDER;";
public static final String REMOVEFOLDER = "REMOVEFOLDER;";
public static final String COPYLINK = "COPYLINK;";
// Private vars
private ServerSocket serverSocket;
private Thread myThread;
/**
* Initialization
*
* @param controller
*/
public RemoteCommandManager(Controller controller) {
super(controller);
}
/**
* Checks if there is a running instance of RemoteCommandManager. Determines
* this by opening a server socket port on the default remote command port.
*
* @return true if port already taken
*/
public static boolean hasRunningInstance() {
return hasRunningInstance(Integer
.valueOf(ConfigurationEntry.NET_RCON_PORT.getDefaultValue()));
}
/**
* Checks if there is a running instance of RemoteComamndManager. Determines
* this by opening a server socket port.
*
* @param port
* the port to check
* @return true if port already taken
*/
public static boolean hasRunningInstance(int port) {
ServerSocket testSocket = null;
try {
// Only bind to localhost
testSocket = new ServerSocket(port, 0,
InetAddress.getByName("127.0.0.1"));
// Server socket can be opened, no instance of PowerFolder running
log.fine("No running instance found");
return false;
} catch (UnknownHostException e) {
} catch (IOException e) {
} finally {
if (testSocket != null) {
try {
testSocket.close();
} catch (IOException e) {
log.log(Level.SEVERE,
"Unable to close already running test socket. "
+ testSocket, e);
}
}
}
log.warning("Running instance found");
return true;
}
/**
* Sends a remote command to a running instance of PowerFolder
*
* @param command
* the command
* @return true if succeeded, otherwise false
*/
public static boolean sendCommand(String command) {
return sendCommand(
Integer.valueOf(ConfigurationEntry.NET_RCON_PORT.getDefaultValue()),
command);
}
/**
* Sends a remote command to a running instance of PowerFolder
*
* @param port
* the port to send this to.
* @param command
* the command
* @return true if succeeded, otherwise false
*/
public static boolean sendCommand(int port, String command) {
try {
log.log(Level.INFO, "Sending remote command '" + command + '\'');
Socket socket = new Socket("127.0.0.1", port);
PrintWriter writer = new PrintWriter(new OutputStreamWriter(
socket.getOutputStream(), ENCODING));
writer.println(REMOTECOMMAND_PREFIX + ';' + command);
writer.flush();
writer.close();
socket.close();
return true;
} catch (IOException e) {
log.log(Level.SEVERE, "Unable to send remote command", e);
}
return false;
}
/**
* Starts the remote command processor
*/
public void start() {
Integer port = ConfigurationEntry.NET_RCON_PORT
.getValueInt(getController());
try {
// Only bind to localhost
serverSocket = new ServerSocket(port, 0,
InetAddress.getByName("127.0.0.1"));
// Start thread
myThread = new Thread(this, "Remote command Manager");
myThread.start();
} catch (UnknownHostException e) {
log.warning("Unable to open remote command manager on port " + port
+ ": " + e);
log.log(Level.FINER, "UnknownHostException", e);
} catch (IOException e) {
log.warning("Unable to open remote command manager on port " + port
+ ": " + e);
log.log(Level.FINER, "IOException", e);
}
}
/**
* Shuts down the rcon manager
*/
public void shutdown() {
if (myThread != null) {
myThread.interrupt();
}
if (serverSocket != null) {
try {
serverSocket.close();
} catch (IOException e) {
log.log(Level.FINER, "Unable to close rcon socket", e);
}
}
}
public void run() {
log.fine("Listening for remote commands on port "
+ serverSocket.getLocalPort());
while (!Thread.currentThread().isInterrupted()) {
Socket socket;
try {
socket = serverSocket.accept();
} catch (IOException e) {
log.log(Level.FINER, "Rcon socket closed, stopping", e);
break;
}
log.finer("Remote command from " + socket);
try {
String address = socket.getInetAddress().getHostAddress();
if (address.equals("localhost") || address.equals("127.0.0.1"))
{
BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream(), ENCODING));
String line = reader.readLine();
if (line == null) {
logFine("Did not receive valid remote request");
} else if (line.startsWith(REMOTECOMMAND_PREFIX)) {
processCommand(line.substring(REMOTECOMMAND_PREFIX
.length() + 1));
} else if (line.startsWith("GET")
|| line.startsWith("POST"))
{
processWebRequest(line, socket.getOutputStream());
} else {
logWarning("Unknown remote command: " + line);
}
}
// socket.close();
} catch (Exception e) {
logWarning("Problems parsing remote command from " + socket
+ ". " + e);
logFiner(e);
} finally {
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
}
}
}
}
}
private void processWebRequest(String line, OutputStream out)
throws IOException
{
// System.err.println(line);
out = new BufferedOutputStream(out);
Writer w = new OutputStreamWriter(out, Convert.UTF8);
w.write("HTTP/1.1 200 OK\n");
// w.write("Transfer-Encoding: chunked\n");
if (line.contains("/info")) {
w.write("Content-Type: text/javascript; charset=utf-8\n");
w.write("\n");
w.write("_jqjsp(\"");
// JSON Start
w.write("{");
w.write("'nodeId':'"
+ getController().getMySelf().getId().replace("'", "\\'") + '\'');
w.write(",");
w.write("'nodeName':'"
+ getController().getMySelf().getNick().replace("'", "\\'")
+ '\'');
w.write("}");
// JSON End
w.write("\");");
w.close();
} else if (line.contains("/open/")) {
// TODO Error handling
int start = line.indexOf("/open/");
int end = line.indexOf(" HTTP");
String addr = line.substring(start + 6, end);
int fIdEnd = addr.indexOf('/');
String fId64 = addr.substring(0, fIdEnd);
String folderId = Base64.decodeString(fId64);
Folder folder = getController().getFolderRepository().getFolder(
folderId);
String relativeName = addr.substring(fIdEnd + 1, addr.length());
try {
relativeName = URLDecoder.decode(relativeName, "UTF-8");
relativeName = relativeName.replace("%20", " ");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Encoding UTF-8 not found", e);
}
FileInfo lookupFile = FileInfoFactory.lookupInstance(
folder.getInfo(), relativeName);
File file = lookupFile.getDiskFile(getController()
.getFolderRepository());
logInfo("Opening file: " + file);
FileUtils.openFile(file);
}
w.close();
}
/**
* Processes a remote command
*
* @param command
*/
private void processCommand(String command) {
if (StringUtils.isBlank(command)) {
log.severe("Received a empty remote command");
return;
}
logFine("Processing remote command: '" + command + '\'');
if (command.startsWith(MAKEFOLDER) || command.startsWith(COPYLINK)) {
// Wait for hook up.
Waiter w = new Waiter(60000);
while (!w.isTimeout()
&& !getController().getOSClient().isLoggedIn()
&& Feature.OS_CLIENT.isEnabled())
{
w.waitABit();
}
}
if (QUIT.equalsIgnoreCase(command)) {
getController().exit(0);
} else if (command.startsWith(OPEN)) {
// Open files
String fileStr = command.substring(OPEN.length());
// Open all files in remote command
StringTokenizer nizer = new StringTokenizer(fileStr, ";");
while (nizer.hasMoreTokens()) {
String token = nizer.nextToken();
// Must be a file
File file = new File(token);
openFile(file);
}
} else if (command.startsWith(MAKEFOLDER)) {
String folderConfig = command.substring(MAKEFOLDER.length());
if (getController().isUIOpen()) {
// Popup application
getController().getUIController().getMainFrame()
.getUIComponent().setVisible(true);
getController().getUIController().getMainFrame()
.getUIComponent().setExtendedState(Frame.NORMAL);
}
// Old style configuration was simply the Directory, e.g.
// C:\The_path
boolean oldStyle = !folderConfig.contains("dir=");
if (oldStyle) {
logWarning("Converted old style folder "
+ "make command for directory " + folderConfig);
// Convert to new style
folderConfig = "dir=" + folderConfig;
}
// New style configuration
// dir=%BASE_DIR%\IPAKI\BACKUP;name=IPAKI/BACKUP/%COMPUTERNAME%;syncprofile=true,true,true,true,5,false,12,0,m,Auto-sync;backup_by_server=true
final String finalFolderConfig = folderConfig;
getController().schedule(new Runnable() {
public void run() {
makeFolder(finalFolderConfig);
}
}, 0);
} else if (command.startsWith(REMOVEFOLDER)) {
final String folderConfig = command
.substring(REMOVEFOLDER.length());
getController().schedule(new Runnable() {
public void run() {
removeFolder(folderConfig);
}
}, 0);
} else if (command.startsWith(COPYLINK)) {
final String filename = command.substring(COPYLINK.length());
getController().schedule(new Runnable() {
public void run() {
copyLink(filename);
}
}, 0);
} else {
log.warning("Remote command not recognizable '" + command + '\'');
}
}
protected void copyLink(String filename) {
File file = new File(filename);
String absPath = file.getAbsolutePath();
for (Folder folder : getController().getFolderRepository().getFolders())
{
if (absPath.startsWith(folder.getLocalBase().getAbsolutePath())) {
final ServerClient client = getController().getOSClient();
final FileInfo fInfo = FileInfoFactory.lookupInstance(folder,
file);
if (client.isConnected()) {
getController().getIOProvider().startIO(new Runnable() {
public void run() {
// COOL MODE: Directly get file link without web
// browser.
String altURL = client.getFolderService()
.getFileLink(fInfo);
Util.setClipboardContents(altURL);
}
});
}
final String linkURL = client.getFileLinkURL(fInfo);
try {
BrowserLauncher.openURL(linkURL);
} catch (IOException e) {
logWarning("Unable to open in browser: " + linkURL);
}
return;
}
}
logWarning("Unable to copy file link. File not contained in a shared folder");
if (getController().isUIEnabled()) {
getController().getUIController().getMainFrame().toFront();
DialogFactory.genericDialog(getController(),
Translation.getTranslation("remote_command_manager.copy_link.error_title"),
Translation.getTranslation("remote_command_manager.copy_link.error_message", filename),
GenericDialogType.ERROR);
}
}
/**
* Opens a file and processes its content
*
* @param file
*/
private void openFile(File file) {
if (!file.exists()) {
log.warning("File not found " + file.getAbsolutePath());
return;
}
if (file.getName().endsWith(".invitation")) {
// Load invitation file
Invitation invitation = InvitationUtil.load(file);
if (invitation != null) {
getController().invitationReceived(invitation);
}
} else if (file.getName().endsWith(".nodes")) {
// Load nodes file
MemberInfo[] nodes = loadNodesFile(file);
// Enqueue new nodes
if (nodes != null) {
getController().getNodeManager().queueNewNodes(nodes);
}
}
}
private void removeFolder(String folderConfig) {
Map<String, String> config = parseFolderConfig(folderConfig);
String id = config.get(FOLDER_SCRIPT_CONFIG_ID);
String name = config.get(FOLDER_SCRIPT_CONFIG_NAME);
String dirStr = config.get(FOLDER_SCRIPT_CONFIG_DIR);
File dir = StringUtils.isBlank(dirStr) ? null : new File(dirStr);
logFine("Remove folder command received. Config: " + folderConfig);
if (StringUtils.isBlank(id) && StringUtils.isBlank(name) && dir == null)
{
logSevere("Unable to remove folder. Wrong parameters: "
+ folderConfig);
return;
}
for (Folder candidate : getController().getFolderRepository()
.getFolders())
{
if (StringUtils.isNotBlank(id)) {
if (!candidate.getId().equals(id)) {
// ID given, but no match. Skip
continue;
}
}
if (StringUtils.isNotBlank(name)) {
if (!candidate.getName().equalsIgnoreCase(name)) {
// name given, but no match. Skip
continue;
}
}
if (dir != null) {
try {
if (!candidate.getLocalBase().equals(dir)
&& !candidate.getLocalBase().getCanonicalPath()
.equals(dir.getCanonicalPath()))
{
// path given, but no match. Skip
continue;
}
} catch (Exception e) {
logWarning("Unable to check by directory: " + candidate
+ ". Dir: " + dir + ". " + e, e);
}
}
logInfo("Removing folder: " + candidate + ". Matched by: "
+ folderConfig);
// Ok this candidate matches! Remove it.
getController().getFolderRepository().removeFolder(candidate, true);
}
}
private void makeFolder(String folderConfig) {
Map<String, String> config = parseFolderConfig(folderConfig);
// Directory
if (StringUtils.isBlank(config.get(FOLDER_SCRIPT_CONFIG_DIR))) {
logSevere("Unable to parse make folder command. directory missing. "
+ folderConfig);
return;
}
FolderRepository repository = getController().getFolderRepository();
File dir = new File(config.get(FOLDER_SCRIPT_CONFIG_DIR));
// Name
String name;
if (StringUtils.isNotBlank(config.get(FOLDER_SCRIPT_CONFIG_NAME))) {
name = config.get(FOLDER_SCRIPT_CONFIG_NAME);
} else {
name = FileUtils.getSuggestedFolderName(dir);
}
if (ConfigurationEntry.SECURITY_PERMISSIONS_STRICT
.getValueBoolean(getController()))
{
if (!getController().getOSClient().getAccount()
.hasPermission(FolderCreatePermission.INSTANCE))
{
if (getController().isUIEnabled()) {
getController().getUIController().getMainFrame().toFront();
DialogFactory.genericDialog(getController(),
Translation.getTranslation("remote_command_manager.make_folder.error_title"),
Translation.getTranslation("remote_command_manager.make_folder.error_message", name),
GenericDialogType.ERROR);
}
return;
}
}
// Show user?
boolean silent = "true".equalsIgnoreCase(config
.get(FOLDER_SCRIPT_CONFIG_SILENT));
// ID
String id = config.get(FOLDER_SCRIPT_CONFIG_ID);
boolean createInvitationFile = false;
if (StringUtils.isEmpty(id)) {
id = '[' + IdGenerator.makeId() + ']';
createInvitationFile = true;
}
if (ConfigurationEntry.FOLDER_CREATE_AVOID_DUPES
.getValueBoolean(getController()))
{
Folder oldFolder = repository.findExistingFolder(dir);
if (oldFolder != null) {
oldFolder = repository.findExistingFolder(name);
}
if (oldFolder != null) {
// Re-use old ID to prevent breaking existing setup.
id = oldFolder.getId();
logWarning("Deleting folder: " + oldFolder + " at "
+ oldFolder.getLocalBase() + ". Replacing it new one at "
+ dir);
repository.removeFolder(oldFolder, true);
}
}
String syncProfileFieldList = config
.get(FOLDER_SCRIPT_CONFIG_SYNC_PROFILE);
SyncProfile syncProfile = syncProfileFieldList != null ? SyncProfile
.getSyncProfileByFieldList(syncProfileFieldList) : null;
boolean backupByServer = "true".equals(config
.get(FOLDER_SCRIPT_CONFIG_BACKUP_BY_SERVER));
FolderInfo foInfo = new FolderInfo(name, id);
String dlScript = config.get(FOLDER_SCRIPT_CONFIG_DL_SCRIPT);
if (!silent && getController().isUIEnabled()) {
PFWizard wizard = new PFWizard(getController(),
Translation.getTranslation("wizard.pfwizard.folder_title"));
wizard.getWizardContext().setAttribute(
WizardContextAttributes.INITIAL_FOLDER_NAME, name);
if (syncProfile != null) {
wizard.getWizardContext()
.setAttribute(
WizardContextAttributes.SYNC_PROFILE_ATTRIBUTE,
syncProfile);
}
wizard.getWizardContext().setAttribute(
WizardContextAttributes.BACKUP_ONLINE_STOARGE,
backupByServer
|| StringUtils.isBlank(config
.get(FOLDER_SCRIPT_CONFIG_BACKUP_BY_SERVER)));
wizard.getWizardContext().setAttribute(
WizardContextAttributes.FOLDERINFO_ATTRIBUTE, foInfo);
WizardPanel nextPanel = new FolderCreatePanel(getController());
// Setup success panel of this wizard path
TextPanelPanel successPanel = new TextPanelPanel(getController(),
Translation.getTranslation("wizard.setup_success"),
Translation.getTranslation("wizard.success_join"));
wizard.getWizardContext().setAttribute(PFWizard.SUCCESS_PANEL, successPanel);
ChooseDiskLocationPanel panel = new ChooseDiskLocationPanel(
getController(), dir.getAbsolutePath(), nextPanel);
wizard.open(panel);
} else {
if (syncProfile == null) {
syncProfile = SyncProfile.getDefault(getController());
}
FolderSettings settings = new FolderSettings(dir, syncProfile,
createInvitationFile,
false, dlScript,
ConfigurationEntry.DEFAULT_ARCHIVE_VERSIONS
.getValueInt(getController()), true);
repository.createFolder(foInfo, settings);
if (backupByServer) {
new CreateFolderOnServerTask(foInfo, null)
.scheduleTask(getController());
}
}
}
private Map<String, String> parseFolderConfig(String folderConfig) {
Map<String, String> config = new HashMap<String, String>();
StringTokenizer nizer = new StringTokenizer(folderConfig, ";");
while (nizer.hasMoreTokens()) {
String keyValuePair = nizer.nextToken();
int equal = keyValuePair.indexOf('=');
if (equal <= 0) {
logSevere("Unable to parse make folder command: '"
+ folderConfig + '\'');
continue;
}
String key = keyValuePair.substring(0, equal);
String value = keyValuePair.substring(equal + 1);
config.put(key, value);
}
return config;
}
/**
* Tries to load a list of nodes from a nodes file. Returns null if wasn't
* able to read the file
*
* @param file
* The file to load from
* @return array of MemberInfo, null if failed
*/
@SuppressWarnings({"unchecked"})
private static MemberInfo[] loadNodesFile(File file) {
try {
InputStream fIn = new BufferedInputStream(new FileInputStream(file));
ObjectInputStream oIn = new ObjectInputStream(fIn);
// Load nodes
List<MemberInfo> nodes = (List<MemberInfo>) oIn.readObject();
log.warning("Loaded " + nodes.size() + " nodes");
MemberInfo[] nodesArrary = new MemberInfo[nodes.size()];
nodes.toArray(nodesArrary);
return nodesArrary;
} catch (IOException e) {
log.log(Level.SEVERE, "Unable to load nodes from file '" + file
+ "'.", e);
} catch (ClassCastException e) {
log.log(Level.SEVERE, "Illegal format of nodes file '" + file
+ "'.", e);
} catch (ClassNotFoundException e) {
log.log(Level.SEVERE, "Illegal format of nodes file '" + file
+ "'.", e);
}
return null;
}
}