/* This file is part of leafdigital leafChat. leafChat 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 3 of the License, or (at your option) any later version. leafChat 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 leafChat. If not, see <http://www.gnu.org/licenses/>. Copyright 2011 Samuel Marshall. */ package com.leafdigital.dcc; import java.io.*; import java.net.*; import java.util.*; import util.PlatformUtils; import util.xml.*; import com.leafdigital.irc.api.*; import com.leafdigital.ircui.api.*; import com.leafdigital.net.api.Network; import com.leafdigital.notification.api.NotificationListMsg; import com.leafdigital.prefs.api.Preferences; import com.leafdigital.prefsui.api.PreferencesUI; import com.leafdigital.ui.api.*; import leafchat.core.api.*; /** * Main plugin class for DCC handling. */ @UIHandler("dccprefs") public class DCCPlugin implements Plugin { private PluginContext context; private TransfersWindow tw; final static String NOTIFICATION_TRANSFERCOMPLETE="File transfer complete"; @Override public void init(PluginContext pc, PluginLoadReporter plr) throws GeneralException { this.context=pc; new DCCCommands(pc); pc.requestMessages(UserCTCPRequestIRCMsg.class,this); pc.requestMessages(NotificationListMsg.class,this); UI ui=pc.getSingle(UI.class); Page p = ui.createPage("dccprefs", this); downloadLocationUI.setText(getDownloadFolder().toString()); PreferencesUI preferencesUI=pc.getSingle(PreferencesUI.class); preferencesUI.registerPage(pc.getPlugin(),p); pc.requestMessages(IRCActionListMsg.class,this); } /** * Message: Notification list. (Adds 'DCC transfer finished' to the list of * things that users can be notified about.) * @param msg Message */ public void msg(NotificationListMsg msg) { msg.addType(NOTIFICATION_TRANSFERCOMPLETE, true); } /** * Message: CTCP request. Handles DCC SEND, DCC CHAT. * @param msg */ public void msg(UserCTCPRequestIRCMsg msg) { if(!msg.getRequest().equals("DCC")) return; byte[][] params=IRCMsg.splitBytes(msg.getText()); if(params.length<1) return; String command=IRCMsg.convertISO(params[0]).toUpperCase(); if((command.equals("SEND") || command.equals("CHAT")) && params.length>=4) { // << :Frog!frog@95913ea5.mant.adsl.78b9d041.com.hmsk PRIVMSG frog :DCC SEND grass1.jpg 4294967295 0 106538 2 // http://en.wikipedia.org/wiki/Direct_Client-to-Client#Reverse_.2F_Firewall_DCC try { long addressNum=Long.parseLong(IRCMsg.convertISO(params[2])); int port=Integer.parseInt(IRCMsg.convertISO(params[3])); InetAddress address=InetAddress.getByAddress( new byte[] { (byte)((addressNum>>24)&0xff), (byte)((addressNum>>16)&0xff), (byte)((addressNum>>8)&0xff), (byte)(addressNum&0xff)}); if(command.equals("SEND")) { context.logDebug("Received DCC SEND: "+msg.getLineISO()); long size=params.length==4 ? TransferProgress.SIZE_UNKNOWN : Long.parseLong(IRCMsg.convertISO(params[4])); new FileAcceptWindow(context,msg.getServer(),msg.getSourceUser(),address,port,params[1],size,msg); msg.markHandled(); } else if(command.equals("CHAT") && IRCMsg.convertISO(params[1]).equalsIgnoreCase("chat")) { context.logDebug("Received DCC CHAT: "+msg.getLineISO()); new DCCChatWindow(context,msg.getServer(),msg.getSourceUser().getNick(), address,port); msg.markHandled(); } else { // TODO Possibly support other DCC protocols } } catch(NumberFormatException nfe) { return; } catch(UnknownHostException uhe) { // Can't happen throw new Error(uhe); } } } /** * Message: IRC action list. (Adds menu items for DCC Send and DCC Chat when * you click on a single user.) * @param msg Message */ public void msg(IRCActionListMsg msg) { if(msg.hasSingleNick() && msg.notUs()) { msg.addIRCAction(new NickAction(context,"DCC: Send a file to "+msg.getSingleNick()+"...",IRCAction.CATEGORY_USER,100, "/dccsend %%NICK%%")); msg.addIRCAction(new NickAction(context,"DCC: Chat directly with "+msg.getSingleNick(),IRCAction.CATEGORY_USER,110, "/dccchat %%NICK%%")); } } final static String PREF_DOWNLOADFOLDER="download-folder"; File getDownloadFolder() { Preferences p=context.getSingle(Preferences.class); return new File( p.getGroup(context.getPlugin()).get(PREF_DOWNLOADFOLDER, PlatformUtils.getDownloadFolder())); } @Override public void close() throws GeneralException { // TODO Ought to cancel all transfers etc. here } @Override public String toString() { return "DCC plugin"; } void transfersWindowClosed() { tw=null; } synchronized void startDownload(String nick,InetAddress address,int port,File target,File targetPartial,long size,long resumePos) { if(tw==null) { tw=new TransfersWindow(context); } TransferProgress tp=new TransferProgress(tw,context,false,nick,target.getName(),size); new Downloader(context,tp,address,port,target,targetPartial,resumePos,size); } synchronized void startListen(Server s,String nick,File source) { if(tw==null) { tw=new TransfersWindow(context); } TransferProgress tp=new TransferProgress(tw,context,true,nick,source.getName(),source.length()); new Uploader(context,tp,s,nick,source); } synchronized void startChat(Server s,String nick) { new DCCChatWindow(context,s,nick); } // Prefs page /** * Label: Download location. */ public Label downloadLocationUI; /** * Action: User clicks to choose download location. */ @UIAction public void actionDownloadLocation() { UI ui=context.getSingle(UI.class); File f=ui.showFolderSelect(null,"Choose download folder", getDownloadFolder()); if(f==null) return; Preferences p=context.getSingle(Preferences.class); p.getGroup(context.getPlugin()).set(PREF_DOWNLOADFOLDER,f.getAbsolutePath(), PlatformUtils.getDesktopFolder()); downloadLocationUI.setText(XML.esc(getDownloadFolder().toString())); } /** Map of nickname -> address as IP string */ private Map<String, String> dccAddress = new HashMap<String, String>(); /** * Remembers a user's address for use with DCC via a proxy. * @param nick Nickname * @param address Address as IP string */ void setDCCAddress(String nick,String address) { synchronized(dccAddress) { dccAddress.put(nick.toLowerCase(),address); } } /** * @param nick Nickname * @return Previously-set IP address or null if none */ String getDCCAddress(String nick) { synchronized(dccAddress) { return dccAddress.get(nick.toLowerCase()); } } /** * DCC represents IP addresses as integers, for some godforesaken reason. * @param ia Internet address * @return Stupid number representing address * @throws GeneralException If the address is IPv6 (not supported) */ long getStupidIPNumber(InetAddress ia) throws GeneralException { long ipnumber; if(ia instanceof Inet6Address && !((Inet6Address)ia).isIPv4CompatibleAddress()) { throw new GeneralException("DCC does not support IPv6"); } byte[] addr=ia.getAddress(); int b1=addr[addr.length-4],b2=addr[addr.length-3], b3=addr[addr.length-2],b4=addr[addr.length-1]; ipnumber= (b4 & 0xff) | ((b3<<8) & 0xff00) | ((b2<<16) & 0xff0000) | ((b1<<24) & 0xff000000); return ipnumber; } /** * @param nick Target nickname (used only when going via proxy) * @return Suitable listening port for DCC * @throws GeneralException Any error */ Network.Port getDCCListenPort(String nick) throws GeneralException { Network n=context.getSingle(Network.class); // Listen Network.Port p; try { // Get address if needed if(n.needsListenTarget()) { String address=getDCCAddress(nick); if(address==null) { throw new GeneralException("Must use /dccaddress when using proxy"); } p=n.listen(address); } else { p=n.listen(); } } catch(IOException e) { throw new GeneralException("Error preparing port: "+e.getMessage()); } return p; } }