/* 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 util.StringUtils; import com.leafdigital.irc.api.*; import com.leafdigital.net.api.Network; import leafchat.core.api.*; /** * Thread that handles DCC file uploads. */ public class Uploader extends Thread { private File source; private Server server; private String nick; private byte[] sendName; private int port; private long startPos=0; private PluginContext context; private TransferProgress tp; private boolean cancelled; /** * @param context Plugin context * @param tp Transfer progress display window * @param s Server * @param nick Nickname * @param source Source file on disk */ public Uploader(PluginContext context,TransferProgress tp,Server s,String nick,File source) { this.source=source; this.server=s; this.nick=nick; this.tp=tp; this.context=context; try { sendName=source.getName().replaceAll("(\\s|[/\\\\:*?\"<>|])","_").getBytes("UTF-8"); } catch(UnsupportedEncodingException e) { throw new BugException(e); } // For RESUME requests context.requestMessages(UserCTCPRequestIRCMsg.class,this,Msg.PRIORITY_EARLY); tp.setUploader(this); // Start listening thread start(); } void cancel() { cancelled=true; context.unrequestMessages(null,this,PluginContext.ALLREQUESTS); } /** * Message: DCC RESUME request. * @param msg Message */ public void msg(UserCTCPRequestIRCMsg msg) { if(msg.getServer()!=server || !msg.getSourceUser().getNick().equals(nick) || !msg.getRequest().equals("DCC")) return; byte[][] params=IRCMsg.splitBytes(msg.getText()); if(params.length<4) return; // RESUME file port pos String command=IRCMsg.convertISO(params[0]).toUpperCase(); if(!command.equals("RESUME")) return; int specifiedPort; long resumePos; try { specifiedPort=Integer.parseInt(IRCMsg.convertISO(params[2])); resumePos=Long.parseLong(IRCMsg.convertISO(params[3])); } catch(NumberFormatException e) { return; } context.logDebug("Received DCC RESUME: "+msg.getLineISO()); // I made it not check the filename as apparently some clients // send invalid filenames if(port!=specifiedPort) return; // OK, a valid request and for us! msg.markHandled(); startPos=Math.min(source.length(),resumePos); // Send response ByteArrayOutputStream baos=new ByteArrayOutputStream(); try { baos.write(IRCMsg.constructBytes( "PRIVMSG "+nick+" :\u0001DCC ACCEPT ")); baos.write(sendName); baos.write(IRCMsg.constructBytes(" "+port+" "+startPos+"\u0001")); } catch(IOException e) { throw new BugException(e); } server.sendLine(baos.toByteArray()); context.logDebug("Sent DCC ACCEPT: "+IRCMsg.convertISO(baos.toByteArray())); tp.status("Resuming from "+StringUtils.displayBytes(startPos)); } @Override public void run() { tp.status("Setting up"); Network.Port p; InetAddress ia; long ipnumber; try { p=((DCCPlugin)context.getPlugin()).getDCCListenPort(nick); ia=p.getPublicAddress(); ipnumber=((DCCPlugin)context.getPlugin()).getStupidIPNumber(ia); } catch(GeneralException e) { tp.error(e.getMessage()); return; } port=p.getPublicPort(); long size=source.length(); Socket s=null; FileInputStream fis=null; try { // OK, we have a port and stupid-version IP, let's send the DCC message. try { ByteArrayOutputStream baos=new ByteArrayOutputStream(); baos.write(IRCMsg.constructBytes( "PRIVMSG "+nick+" :\u0001DCC SEND ")); baos.write(sendName); baos.write(IRCMsg.constructBytes(" "+ipnumber+" "+port+" "+size+"\u0001")); server.sendLine(baos.toByteArray()); context.logDebug("Sent DCC SEND: "+IRCMsg.convertISO(baos.toByteArray())); } catch(IOException e) { throw new BugException(e); } tp.status("Waiting for connection"); // Now listen try { context.logDebug("Listening on server socket"); context.log("DCC send: "+source.getName()+" on "+ia.getHostAddress()+":"+port); while(true) { try { s=p.accept(1000); s.setSoTimeout(0); // Just in case this is needed context.logDebug("Got connection on server socket"); context.log("DCC send: connected from "+s.getInetAddress().getHostAddress()); break; } catch(SocketTimeoutException e) { if(cancelled) return; } } } catch(IOException e) { tp.error("Problem with local socket",e); return; } // No more need to listen for resume! context.unrequestMessages(null,this,PluginContext.ALLREQUESTS); // Open file and skip start if requested try { fis=new FileInputStream(source); if(startPos>0) { context.logDebug("Skipping "+startPos+" bytes"); long remaining=startPos-fis.skip(startPos); while(remaining>0) { if(fis.read()==-1) { tp.error("Unexpected error scanning file"); return; } remaining--; } } } catch(IOException e) { tp.error("Error opening local file",e); return; } tp.status("Sending..."); context.logDebug("Beginning send"); try { OutputStream os=s.getOutputStream(); InputStream is=s.getInputStream(); byte[] buffer=new byte[BLOCKSIZE],ackBuffer=new byte[BLOCKSIZE]; long sent=startPos; while(sent<size) { // Read but ignore any acknowledgements while(true) { int ack=is.available(); if(ack==0) break; ack=Math.max(ack,ackBuffer.length); context.logDebug("Reading and ignoring ack"); is.read(ackBuffer,0,ack); } // Get new data from file and write it int read; try { read=fis.read(buffer); } catch(IOException e) { tp.error("Error reading local file",e); return; } if(cancelled) return; context.logDebug("Sending "+read+" bytes"); os.write(buffer,0,read); os.flush(); if(cancelled) return; sent+=read; tp.setTransferred(sent); } tp.setFinished(); // Wait for CLOSEDELAY milliseconds after last received ack, then close // the socket long closeAfter=System.currentTimeMillis()+CLOSEDELAY; while(true) { long now=System.currentTimeMillis(); if(now>closeAfter) break; int ack=is.available(); if(ack!=0) { ack=Math.max(ack,ackBuffer.length); context.logDebug("Reading ack"); is.read(ackBuffer,0,ack); closeAfter=now+CLOSEDELAY; } try { sleep(250); } catch(InterruptedException e) { throw new BugException(e); } } context.logDebug("Closing socket"); context.log("DCC send: complete"); os.close(); s.close(); } catch(IOException e) { tp.error("Connection error",e); return; } } finally { try { if(fis!=null) fis.close(); } catch(IOException e) {} try { if(p!=null) p.close(); } catch(IOException e) {} try { if(s!=null) s.close(); } catch(IOException e) {} } } private final static int BLOCKSIZE=4096; private final static long CLOSEDELAY=3000L; }