Changeset 79ac955


Ignore:
Timestamp:
Jul 14, 2011 8:06:31 PM (9 years ago)
Author:
zzz <zzz@…>
Branches:
master
Children:
55bfd6a
Parents:
252f1047
Message:
  • I2PTunnelIRCClient:
    • Big refactoring into multiple class files
    • Allow AWAY and CAP messages
    • First cut at DCC support - not for SOCKS (yet)
Location:
apps/i2ptunnel/java/src/net/i2p/i2ptunnel
Files:
7 added
4 edited

Legend:

Unmodified
Added
Removed
  • apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java

    r252f1047 r79ac955  
    194194        // no need to load the netDb with leaseSets for destinations that will never
    195195        // be looked up
    196         tunnel.getClientOptions().setProperty("i2cp.dontPublishLeaseSet", "true");
     196        boolean dccEnabled = (this instanceof I2PTunnelIRCClient) &&
     197                      Boolean.valueOf(tunnel.getClientOptions().getProperty(I2PTunnelIRCClient.PROP_DCC)).booleanValue();
     198        if (!dccEnabled)
     199            tunnel.getClientOptions().setProperty("i2cp.dontPublishLeaseSet", "true");
    197200       
    198201        boolean openNow = !Boolean.valueOf(tunnel.getClientOptions().getProperty("i2cp.delayOpen")).booleanValue();
  • apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCClient.java

    r252f1047 r79ac955  
    1111
    1212import net.i2p.client.streaming.I2PSocket;
     13import net.i2p.data.Base32;
    1314import net.i2p.data.Destination;
     15import net.i2p.i2ptunnel.irc.DCCClientManager;
     16import net.i2p.i2ptunnel.irc.DCCHelper;
     17import net.i2p.i2ptunnel.irc.I2PTunnelDCCServer;
     18import net.i2p.i2ptunnel.irc.IrcInboundFilter;
     19import net.i2p.i2ptunnel.irc.IrcOutboundFilter;
    1420import net.i2p.util.EventDispatcher;
    1521import net.i2p.util.I2PAppThread;
     
    1925 * Todo: Can we extend I2PTunnelClient instead and remove some duplicated code?
    2026 */
    21 public class I2PTunnelIRCClient extends I2PTunnelClientBase implements Runnable {
     27public class I2PTunnelIRCClient extends I2PTunnelClientBase implements DCCHelper {
    2228
    2329    /** used to assign unique IDs to the threads / clients.  no logic or functionality */
     
    2834    private static final long DEFAULT_READ_TIMEOUT = 5*60*1000; // -1
    2935    protected long readTimeout = DEFAULT_READ_TIMEOUT;
     36    private final boolean _dccEnabled;
     37    private I2PTunnelDCCServer _DCCServer;
     38    private DCCClientManager _DCCClientManager;
     39
     40    /**
     41     *  @since 0.8.9
     42     */
     43    public static final String PROP_DCC = "i2ptunnel.ircclient.enableDCC";
    3044
    3145    /**
     
    7690        setName("IRC Client on " + tunnel.listenHost + ':' + localPort);
    7791
     92        _dccEnabled = Boolean.valueOf(tunnel.getClientOptions().getProperty(PROP_DCC)).booleanValue();
     93        // TODO add some prudent tunnel options (or is it too late?)
     94
    7895        startRunning();
    7996
     
    90107            i2ps.setReadTimeout(readTimeout);
    91108            StringBuffer expectedPong = new StringBuffer();
    92             Thread in = new I2PAppThread(new IrcInboundFilter(s,i2ps, expectedPong, _log), "IRC Client " + __clientId + " in", true);
     109            Thread in = new I2PAppThread(new IrcInboundFilter(s,i2ps, expectedPong, _log, this), "IRC Client " + __clientId + " in", true);
    93110            in.start();
    94             Thread out = new I2PAppThread(new IrcOutboundFilter(s,i2ps, expectedPong, _log), "IRC Client " + __clientId + " out", true);
     111            Thread out = new I2PAppThread(new IrcOutboundFilter(s,i2ps, expectedPong, _log, this), "IRC Client " + __clientId + " out", true);
    95112            out.start();
    96113        } catch (Exception ex) {
     
    121138    }
    122139
    123     /*************************************************************************
    124      *
    125      */
    126     public static class IrcInboundFilter implements Runnable {
    127        
    128         private final Socket local;
    129         private final I2PSocket remote;
    130         private final StringBuffer expectedPong;
    131         private final Log _log;
    132                
    133         public IrcInboundFilter(Socket _local, I2PSocket _remote, StringBuffer pong, Log log) {
    134             local=_local;
    135             remote=_remote;
    136             expectedPong=pong;
    137             _log = log;
    138         }
    139 
    140         public void run() {
    141             // Todo: Don't use BufferedReader - IRC spec limits line length to 512 but...
    142             BufferedReader in;
    143             OutputStream output;
    144             try {
    145                 in = new BufferedReader(new InputStreamReader(remote.getInputStream(), "ISO-8859-1"));
    146                 output=local.getOutputStream();
    147             } catch (IOException e) {
    148                 if (_log.shouldLog(Log.ERROR))
    149                     _log.error("IrcInboundFilter: no streams",e);
    150                 return;
    151             }
    152             if (_log.shouldLog(Log.DEBUG))
    153                 _log.debug("IrcInboundFilter: Running.");
    154             try {
    155                 while(true)
    156                 {
    157                     try {
    158                         String inmsg = in.readLine();
    159                         if(inmsg==null)
    160                             break;
    161                         if(inmsg.endsWith("\r"))
    162                             inmsg=inmsg.substring(0,inmsg.length()-1);
    163                         if (_log.shouldLog(Log.DEBUG))
    164                             _log.debug("in: [" + inmsg + "]");
    165                         String outmsg = inboundFilter(inmsg, expectedPong);
    166                         if(outmsg!=null)
    167                         {
    168                             if(!inmsg.equals(outmsg)) {
    169                                 if (_log.shouldLog(Log.WARN)) {
    170                                     _log.warn("inbound FILTERED: "+outmsg);
    171                                     _log.warn(" - inbound was: "+inmsg);
    172                                 }
    173                             } else {
    174                                 if (_log.shouldLog(Log.INFO))
    175                                     _log.info("inbound: "+outmsg);
    176                             }
    177                             outmsg=outmsg+"\r\n";   // rfc1459 sec. 2.3
    178                             output.write(outmsg.getBytes("ISO-8859-1"));
    179                             // probably doesn't do much but can't hurt
    180                             output.flush();
    181                         } else {
    182                             if (_log.shouldLog(Log.WARN))
    183                                 _log.warn("inbound BLOCKED: "+inmsg);
    184                         }
    185                     } catch (IOException e1) {
    186                         if (_log.shouldLog(Log.WARN))
    187                             _log.warn("IrcInboundFilter: disconnected",e1);
    188                         break;
    189                     }
    190                 }
    191             } catch (RuntimeException re) {
    192                 _log.error("Error filtering inbound data", re);
    193             } finally {
    194                 try { local.close(); } catch (IOException e) {}
    195             }
    196             if(_log.shouldLog(Log.DEBUG))
    197                 _log.debug("IrcInboundFilter: Done.");
    198             }
    199            
    200         }
    201                
    202         /*************************************************************************
    203          *
    204          */
    205         public static class IrcOutboundFilter implements Runnable {
    206                    
    207             private final Socket local;
    208             private final I2PSocket remote;
    209             private final StringBuffer expectedPong;
    210             private final Log _log;
    211                
    212             public IrcOutboundFilter(Socket _local, I2PSocket _remote, StringBuffer pong, Log log) {
    213                 local=_local;
    214                 remote=_remote;
    215                 expectedPong=pong;
    216                 _log = log;
    217             }
    218                
    219             public void run() {
    220                 // Todo: Don't use BufferedReader - IRC spec limits line length to 512 but...
    221                 BufferedReader in;
    222                 OutputStream output;
    223                 try {
    224                     in = new BufferedReader(new InputStreamReader(local.getInputStream(), "ISO-8859-1"));
    225                     output=remote.getOutputStream();
    226                 } catch (IOException e) {
    227                     if (_log.shouldLog(Log.ERROR))
    228                         _log.error("IrcOutboundFilter: no streams",e);
    229                     return;
    230                 }
    231                 if (_log.shouldLog(Log.DEBUG))
    232                     _log.debug("IrcOutboundFilter: Running.");
    233                 try {
    234                     while(true)
    235                     {
    236                         try {
    237                             String inmsg = in.readLine();
    238                             if(inmsg==null)
    239                                 break;
    240                             if(inmsg.endsWith("\r"))
    241                                 inmsg=inmsg.substring(0,inmsg.length()-1);
    242                             if (_log.shouldLog(Log.DEBUG))
    243                                 _log.debug("out: [" + inmsg + "]");
    244                             String outmsg = outboundFilter(inmsg, expectedPong);
    245                             if(outmsg!=null)
    246                             {
    247                                 if(!inmsg.equals(outmsg)) {
    248                                     if (_log.shouldLog(Log.WARN)) {
    249                                         _log.warn("outbound FILTERED: "+outmsg);
    250                                         _log.warn(" - outbound was: "+inmsg);
    251                                     }
    252                                 } else {
    253                                     if (_log.shouldLog(Log.INFO))
    254                                         _log.info("outbound: "+outmsg);
    255                                 }
    256                                 outmsg=outmsg+"\r\n";   // rfc1459 sec. 2.3
    257                                 output.write(outmsg.getBytes("ISO-8859-1"));
    258                                 // save 250 ms in streaming
    259                                 output.flush();
    260                             } else {
    261                                 if (_log.shouldLog(Log.WARN))
    262                                     _log.warn("outbound BLOCKED: "+"\""+inmsg+"\"");
    263                             }
    264                         } catch (IOException e1) {
    265                             if (_log.shouldLog(Log.WARN))
    266                                 _log.warn("IrcOutboundFilter: disconnected",e1);
    267                             break;
    268                         }
    269                     }
    270                 } catch (RuntimeException re) {
    271                     _log.error("Error filtering outbound data", re);
    272                 } finally {
    273                     try { remote.close(); } catch (IOException e) {}
    274                 }
    275                 if (_log.shouldLog(Log.DEBUG))
    276                     _log.debug("IrcOutboundFilter: Done.");
     140    @Override
     141    public boolean close(boolean forced) {
     142        synchronized(this) {
     143            if (_DCCServer != null) {
     144                _DCCServer.close(forced);
     145                _DCCServer = null;
    277146            }
    278147        }
     148        return super.close(forced);
     149    }
    279150
    280    
    281     /*************************************************************************
    282      *
    283      */
    284    
    285     public static String inboundFilter(String s, StringBuffer expectedPong) {
    286        
    287         String field[]=s.split(" ",4);
    288         String command;
    289         int idx=0;
    290         final String[] allowedCommands =
    291         {
    292                 // "NOTICE", // can contain CTCP
    293                 //"PING",
    294                 //"PONG",
    295                 "MODE",
    296                 "JOIN",
    297                 "NICK",
    298                 "QUIT",
    299                 "PART",
    300                 "WALLOPS",
    301                 "ERROR",
    302                 "KICK",
    303                 "H", // "hide operator status" (after kicking an op)
    304                 "TOPIC"
    305         };
    306        
    307         if(field[0].charAt(0)==':')
    308             idx++;
     151    //
     152    //  Start of the DCCHelper interface
     153    //
    309154
    310         try { command = field[idx++]; }
    311          catch (IndexOutOfBoundsException ioobe) // wtf, server sent borked command?
    312         {
    313            //_log.warn("Dropping defective message: index out of bounds while extracting command.");
    314            return null;
     155    public boolean isEnabled() {
     156        return _dccEnabled;
     157    }
     158
     159    public String getB32Hostname() {
     160        return Base32.encode(sockMgr.getSession().getMyDestination().calculateHash().getData()) + ".b32.i2p";
     161    }
     162
     163
     164    public int newOutgoing(byte[] ip, int port, String type) {
     165        I2PTunnelDCCServer server;
     166        synchronized(this) {
     167            if (_DCCServer == null) {
     168                if (_log.shouldLog(Log.INFO))
     169                    _log.info("Starting DCC Server");
     170                _DCCServer = new I2PTunnelDCCServer(sockMgr, l, this, getTunnel());
     171                // TODO add some prudent tunnel options (or is it too late?)
     172                _DCCServer.startRunning();
     173            }
     174            server = _DCCServer;
    315175        }
     176        int rv = server.newOutgoing(ip, port, type);
     177        if (_log.shouldLog(Log.INFO))
     178            _log.info("New outgoing " + type + ' ' + port + " returns " + rv);
     179        return rv;
     180    }
    316181
    317         idx++; //skip victim
    318 
    319         // Allow numerical responses
    320         try {
    321             new Integer(command);
    322             return s;
    323         } catch(NumberFormatException nfe){}
    324 
    325        
    326         if ("PING".equalsIgnoreCase(command))
    327             return "PING 127.0.0.1"; // no way to know what the ircd to i2ptunnel server con is, so localhost works
    328         if ("PONG".equalsIgnoreCase(command)) {
    329             // Turn the received ":irc.freshcoffee.i2p PONG irc.freshcoffee.i2p :127.0.0.1"
    330             // into ":127.0.0.1 PONG 127.0.0.1 " so that the caller can append the client's extra parameter
    331             // though, does 127.0.0.1 work for irc clients connecting remotely?  and for all of them?  sure would
    332             // be great if irc clients actually followed the RFCs here, but i guess thats too much to ask.
    333             // If we haven't PINGed them, or the PING we sent isn't something we know how to filter, this
    334             // is blank.
    335             //
    336             // String pong = expectedPong.length() > 0 ? expectedPong.toString() : null;
    337             // If we aren't going to rewrite it, pass it through
    338             String pong = expectedPong.length() > 0 ? expectedPong.toString() : s;
    339             expectedPong.setLength(0);
    340             return pong;
     182    public int newIncoming(String b32, int port, String type) {
     183        DCCClientManager tracker;
     184        synchronized(this) {
     185            if (_DCCClientManager == null) {
     186                if (_log.shouldLog(Log.INFO))
     187                    _log.info("Starting DCC Client");
     188                _DCCClientManager = new DCCClientManager(sockMgr, l, this, getTunnel());
     189            }
     190            tracker = _DCCClientManager;
    341191        }
    342        
    343         // Allow all allowedCommands
    344         for(int i=0;i<allowedCommands.length;i++) {
    345             if(allowedCommands[i].equalsIgnoreCase(command))
    346                 return s;
    347         }
    348        
    349         // Allow PRIVMSG, but block CTCP.
    350         if("PRIVMSG".equalsIgnoreCase(command) || "NOTICE".equalsIgnoreCase(command))
    351         {
    352             String msg;
    353             msg = field[idx++];
    354        
    355             if(msg.indexOf(0x01) >= 0) // CTCP marker ^A can be anywhere, not just immediately after the ':'
    356             {
    357                 // CTCP
    358                 msg=msg.substring(2);
    359                 if(msg.startsWith("ACTION ")) {
    360                     // /me says hello
    361                     return s;
    362                 }
    363                 return null; // Block all other ctcp
    364             }
    365             return s;
    366         }
    367        
    368         // Block the rest
    369         return null;
    370     }
    371    
    372     public static String outboundFilter(String s, StringBuffer expectedPong) {
    373        
    374         String field[]=s.split(" ",3);
    375         String command;
    376         final String[] allowedCommands =
    377         {
    378                 // "NOTICE", // can contain CTCP
    379                 "MODE",
    380                 "JOIN",
    381                 "NICK",
    382                 "WHO",
    383                 "WHOIS",
    384                 "LIST",
    385                 "NAMES",
    386                 "NICK",
    387                 // "QUIT", // replace with a filtered QUIT to hide client quit messages
    388                 "SILENCE",
    389                 "MAP", // seems safe enough, the ircd should protect themselves though
    390                 // "PART", // replace with filtered PART to hide client part messages
    391                 "OPER",
    392                 // "PONG", // replaced with a filtered PING/PONG since some clients send the server IP (thanks aardvax!)
    393                 // "PING",
    394                 "KICK",
    395                 "HELPME",
    396                 "RULES",
    397                 "TOPIC",
    398                 "ISON",    // jIRCii uses this for a ping (response is 303)
    399                 "INVITE"
    400         };
    401 
    402         if(field[0].length()==0)
    403             return null; // W T F?
    404        
    405        
    406         if(field[0].charAt(0)==':')
    407             return null; // wtf
    408        
    409         command = field[0].toUpperCase();
    410 
    411         if ("PING".equals(command)) {
    412             // Most clients just send a PING and are happy with any old PONG.  Others,
    413             // like BitchX, actually expect certain behavior.  It sends two different pings:
    414             // "PING :irc.freshcoffee.i2p" and "PING 1234567890 127.0.0.1" (where the IP is the proxy)
    415             // the PONG to the former seems to be "PONG 127.0.0.1", while the PONG to the later is
    416             // ":irc.freshcoffee.i2p PONG irc.freshcoffe.i2p :1234567890".
    417             // We don't want to send them our proxy's IP address, so we need to rewrite the PING
    418             // sent to the server, but when we get a PONG back, use what we expected, rather than
    419             // what they sent.
    420             //
    421             // Yuck.
    422 
    423             String rv = null;
    424             expectedPong.setLength(0);
    425             if (field.length == 1) { // PING
    426                 rv = "PING";
    427                 // If we aren't rewriting the PING don't rewrite the PONG
    428                 // expectedPong.append("PONG 127.0.0.1");
    429             } else if (field.length == 2) { // PING nonce
    430                 rv = "PING " + field[1];
    431                 // If we aren't rewriting the PING don't rewrite the PONG
    432                 // expectedPong.append("PONG ").append(field[1]);
    433             } else if (field.length == 3) { // PING nonce serverLocation
    434                 rv = "PING " + field[1];
    435                 expectedPong.append("PONG ").append(field[2]).append(" :").append(field[1]); // PONG serverLocation nonce
    436             } else {
    437                 //if (_log.shouldLog(Log.ERROR))
    438                 //    _log.error("IRC client sent a PING we don't understand, filtering it (\"" + s + "\")");
    439                 rv = null;
    440             }
    441            
    442             //if (_log.shouldLog(Log.WARN))
    443             //    _log.warn("sending ping [" + rv + "], waiting for [" + expectedPong + "] orig was [" + s  + "]");
    444            
    445             return rv;
    446         }
    447         if ("PONG".equals(command))
    448             return "PONG 127.0.0.1"; // no way to know what the ircd to i2ptunnel server con is, so localhost works
    449 
    450         // Allow all allowedCommands
    451         for(int i=0;i<allowedCommands.length;i++)
    452         {
    453             if(allowedCommands[i].equals(command))
    454                 return s;
    455         }
    456        
    457         // mIRC sends "NOTICE user :DCC Send file (IP)"
    458         // in addition to the CTCP version
    459         if("NOTICE".equals(command))
    460         {
    461             String msg = field[2];
    462             if(msg.startsWith(":DCC "))
    463                 return null;
    464             // fall through
    465         }
    466        
    467         // Allow PRIVMSG, but block CTCP (except ACTION).
    468         if("PRIVMSG".equals(command) || "NOTICE".equals(command))
    469         {
    470             String msg;
    471             msg = field[2];
    472        
    473             if(msg.indexOf(0x01) >= 0) // CTCP marker ^A can be anywhere, not just immediately after the ':'
    474             {
    475                     // CTCP
    476                 msg=msg.substring(2);
    477                 if(msg.startsWith("ACTION ")) {
    478                     // /me says hello
    479                     return s;
    480                 }
    481                 return null; // Block all other ctcp
    482             }
    483             return s;
    484         }
    485        
    486         if("USER".equals(command)) {
    487             int idx = field[2].lastIndexOf(":");
    488             if(idx<0)
    489                 return "USER user hostname localhost :realname";
    490             String realname = field[2].substring(idx+1);
    491             String ret = "USER "+field[1]+" hostname localhost :"+realname;
    492             return ret;
    493         }
    494 
    495         if ("PART".equals(command)) {
    496             // hide client message
    497             return "PART " + field[1] + " :leaving";
    498         }
    499        
    500         if ("QUIT".equals(command)) {
    501             return "QUIT :leaving";
    502         }
    503        
    504         // Block the rest
    505         return null;
     192        // The tracker starts our client
     193        int rv = tracker.newIncoming(b32, port, type);
     194        if (_log.shouldLog(Log.INFO))
     195            _log.info("New incoming " + type + ' ' + b32 + ' ' + port + " returns " + rv);
     196        return rv;
    506197    }
    507198}
  • apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelServer.java

    r252f1047 r79ac955  
    4848    private boolean _usePool;
    4949
    50     private Logging l;
     50    protected Logging l;
    5151
    5252    private static final long DEFAULT_READ_TIMEOUT = -1; // 3*60*1000;
     
    7070    private ThreadPoolExecutor _executor;
    7171
     72    /** unused? port should always be specified */
    7273    private int DEFAULT_LOCALPORT = 4488;
    7374    protected int localPort = DEFAULT_LOCALPORT;
    7475
    7576    /**
     77     * @param privData Base64-encoded private key data,
     78     *                 format is specified in {@link net.i2p.data.PrivateKeyFile PrivateKeyFile}
    7679     * @throws IllegalArgumentException if the I2CP configuration is b0rked so
    7780     *                                  badly that we cant create a socketManager
     
    8588
    8689    /**
     90     * @param privkey file containing the private key data,
     91     *                format is specified in {@link net.i2p.data.PrivateKeyFile PrivateKeyFile}
     92     * @param privkeyname the name of the privKey file, not clear why we need this too
    8793     * @throws IllegalArgumentException if the I2CP configuration is b0rked so
    8894     *                                  badly that we cant create a socketManager
     
    106112
    107113    /**
     114     * @param privData stream containing the private key data,
     115     *                 format is specified in {@link net.i2p.data.PrivateKeyFile PrivateKeyFile}
     116     * @param privkeyname the name of the privKey file, not clear why we need this too
    108117     * @throws IllegalArgumentException if the I2CP configuration is b0rked so
    109118     *                                  badly that we cant create a socketManager
     
    115124    }
    116125
     126    /**
     127     *  @param sktMgr the existing socket manager
     128     *  @since 0.8.9
     129     */
     130    public I2PTunnelServer(InetAddress host, int port, I2PSocketManager sktMgr,
     131                           Logging l, EventDispatcher notifyThis, I2PTunnel tunnel) {
     132        super("Server at " + host + ':' + port, notifyThis, tunnel);
     133        this.l = l;
     134        this.remoteHost = host;
     135        this.remotePort = port;
     136        _log = tunnel.getContext().logManager().getLog(getClass());
     137        sockMgr = sktMgr;
     138        open = true;
     139    }
     140
    117141    private static final int RETRY_DELAY = 20*1000;
    118142    private static final int MAX_RETRIES = 4;
    119143
    120144    /**
     145     * @param privData stream containing the private key data,
     146     *                 format is specified in {@link net.i2p.data.PrivateKeyFile PrivateKeyFile}
     147     * @param privkeyname the name of the privKey file, not clear why we need this too
    121148     * @throws IllegalArgumentException if the I2CP configuration is b0rked so
    122149     *                                  badly that we cant create a socketManager
  • apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSIRCTunnel.java

    r252f1047 r79ac955  
    1212import net.i2p.client.streaming.I2PSocket;
    1313import net.i2p.i2ptunnel.I2PTunnel;
    14 import net.i2p.i2ptunnel.I2PTunnelIRCClient;
     14import net.i2p.i2ptunnel.irc.IrcInboundFilter;
     15import net.i2p.i2ptunnel.irc.IrcOutboundFilter;
    1516import net.i2p.i2ptunnel.Logging;
    1617import net.i2p.util.EventDispatcher;
     
    5152            I2PSocket destSock = serv.getDestinationI2PSocket(this);
    5253            StringBuffer expectedPong = new StringBuffer();
    53             Thread in = new I2PAppThread(new I2PTunnelIRCClient.IrcInboundFilter(clientSock, destSock, expectedPong, _log),
     54            Thread in = new I2PAppThread(new IrcInboundFilter(clientSock, destSock, expectedPong, _log),
    5455                                         "SOCKS IRC Client " + (++__clientId) + " in", true);
    5556            in.start();
    56             Thread out = new I2PAppThread(new I2PTunnelIRCClient.IrcOutboundFilter(clientSock, destSock, expectedPong, _log),
     57            Thread out = new I2PAppThread(new IrcOutboundFilter(clientSock, destSock, expectedPong, _log),
    5758                                          "SOCKS IRC Client " + __clientId + " out", true);
    5859            out.start();
Note: See TracChangeset for help on using the changeset viewer.