You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748
  1. /*
  2. * Copyright (c) 2006-2013 DMDirc Developers
  3. *
  4. * Permission is hereby granted, free of charge, to any person obtaining a copy
  5. * of this software and associated documentation files (the "Software"), to deal
  6. * in the Software without restriction, including without limitation the rights
  7. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  8. * copies of the Software, and to permit persons to whom the Software is
  9. * furnished to do so, subject to the following conditions:
  10. *
  11. * The above copyright notice and this permission notice shall be included in
  12. * all copies or substantial portions of the Software.
  13. *
  14. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  19. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  20. * SOFTWARE.
  21. */
  22. package com.dmdirc;
  23. import com.dmdirc.actions.ActionManager;
  24. import com.dmdirc.actions.CoreActionType;
  25. import com.dmdirc.actions.wrappers.AliasWrapper;
  26. import com.dmdirc.commandparser.CommandType;
  27. import com.dmdirc.commandparser.parsers.CommandParser;
  28. import com.dmdirc.config.ConfigManager;
  29. import com.dmdirc.config.Identity;
  30. import com.dmdirc.config.IdentityManager;
  31. import com.dmdirc.interfaces.AwayStateListener;
  32. import com.dmdirc.interfaces.CommandController;
  33. import com.dmdirc.interfaces.ConfigChangeListener;
  34. import com.dmdirc.interfaces.Connection;
  35. import com.dmdirc.interfaces.InviteListener;
  36. import com.dmdirc.logger.ErrorLevel;
  37. import com.dmdirc.logger.Logger;
  38. import com.dmdirc.parser.common.ChannelJoinRequest;
  39. import com.dmdirc.parser.common.DefaultStringConverter;
  40. import com.dmdirc.parser.common.IgnoreList;
  41. import com.dmdirc.parser.common.MyInfo;
  42. import com.dmdirc.parser.common.ParserError;
  43. import com.dmdirc.parser.common.ThreadedParser;
  44. import com.dmdirc.parser.interfaces.ChannelInfo;
  45. import com.dmdirc.parser.interfaces.ClientInfo;
  46. import com.dmdirc.parser.interfaces.EncodingParser;
  47. import com.dmdirc.parser.interfaces.Parser;
  48. import com.dmdirc.parser.interfaces.ProtocolDescription;
  49. import com.dmdirc.parser.interfaces.SecureParser;
  50. import com.dmdirc.parser.interfaces.StringConverter;
  51. import com.dmdirc.tls.CertificateManager;
  52. import com.dmdirc.tls.CertificateProblemListener;
  53. import com.dmdirc.ui.StatusMessage;
  54. import com.dmdirc.ui.WindowManager;
  55. import com.dmdirc.ui.core.components.StatusBarManager;
  56. import com.dmdirc.ui.core.components.WindowComponent;
  57. import com.dmdirc.ui.input.TabCompleter;
  58. import com.dmdirc.ui.input.TabCompletionType;
  59. import com.dmdirc.ui.messages.Formatter;
  60. import java.net.URI;
  61. import java.net.URISyntaxException;
  62. import java.security.cert.CertificateException;
  63. import java.security.cert.X509Certificate;
  64. import java.util.ArrayList;
  65. import java.util.Arrays;
  66. import java.util.Collection;
  67. import java.util.Collections;
  68. import java.util.Date;
  69. import java.util.HashSet;
  70. import java.util.List;
  71. import java.util.Map;
  72. import java.util.Random;
  73. import java.util.Set;
  74. import java.util.Timer;
  75. import java.util.TimerTask;
  76. import java.util.concurrent.ConcurrentSkipListMap;
  77. import java.util.concurrent.locks.ReadWriteLock;
  78. import java.util.concurrent.locks.ReentrantReadWriteLock;
  79. import javax.net.ssl.TrustManager;
  80. import lombok.extern.slf4j.Slf4j;
  81. /**
  82. * The Server class represents the client's view of a server. It maintains
  83. * a list of all channels, queries, etc, and handles parser callbacks pertaining
  84. * to the server.
  85. */
  86. @Slf4j
  87. public class Server extends WritableFrameContainer
  88. implements ConfigChangeListener, CertificateProblemListener, Connection {
  89. // <editor-fold defaultstate="collapsed" desc="Properties">
  90. // <editor-fold defaultstate="collapsed" desc="Static">
  91. /** The name of the general domain. */
  92. private static final String DOMAIN_GENERAL = "general".intern();
  93. /** The name of the profile domain. */
  94. private static final String DOMAIN_PROFILE = "profile".intern();
  95. /** The name of the server domain. */
  96. private static final String DOMAIN_SERVER = "server".intern();
  97. // </editor-fold>
  98. // <editor-fold defaultstate="collapsed" desc="Instance">
  99. /** Open channels that currently exist on the server. */
  100. private final Map<String, Channel> channels = new ConcurrentSkipListMap<String, Channel>();
  101. /** Open query windows on the server. */
  102. private final Map<String, Query> queries = new ConcurrentSkipListMap<String, Query>();
  103. /** The Parser instance handling this server. */
  104. private Parser parser;
  105. /** The Parser instance that used to be handling this server. */
  106. private Parser oldParser;
  107. /** The parser-supplied protocol description object. */
  108. private ProtocolDescription protocolDescription;
  109. /**
  110. * Object used to synchronise access to parser. This object should be
  111. * locked by anything requiring that the parser reference remains the same
  112. * for a duration of time, or by anything which is updating the parser
  113. * reference.
  114. *
  115. * If used in conjunction with myStateLock, the parserLock must always be
  116. * locked INSIDE the myStateLock to prevent deadlocks.
  117. */
  118. private final ReadWriteLock parserLock = new ReentrantReadWriteLock();
  119. /** The raw frame used for this server instance. */
  120. private Raw raw;
  121. /** The address of the server we're connecting to. */
  122. private URI address;
  123. /** The profile we're using. */
  124. private Identity profile;
  125. /** Object used to synchronise access to myState. */
  126. private final Object myStateLock = new Object();
  127. /** The current state of this server. */
  128. private final ServerStatus myState = new ServerStatus(this, myStateLock);
  129. /** The timer we're using to delay reconnects. */
  130. private Timer reconnectTimer;
  131. /** The timer we're using to send WHO requests. */
  132. private final Timer whoTimer;
  133. /** The tabcompleter used for this server. */
  134. private final TabCompleter tabCompleter = new TabCompleter();
  135. /** Our reason for being away, if any. */
  136. private String awayMessage;
  137. /** Our event handler. */
  138. private final ServerEventHandler eventHandler = new ServerEventHandler(this);
  139. /** A list of outstanding invites. */
  140. private final List<Invite> invites = new ArrayList<Invite>();
  141. /** A set of channels we want to join without focusing. */
  142. private final Set<String> backgroundChannels = new HashSet<String>();
  143. /** Our ignore list. */
  144. private final IgnoreList ignoreList = new IgnoreList();
  145. /** Our string convertor. */
  146. private StringConverter converter = new DefaultStringConverter();
  147. /** The certificate manager in use, if any. */
  148. private CertificateManager certificateManager;
  149. /** ParserFactory we use for creating parsers. */
  150. private final ParserFactory parserFactory;
  151. /** ServerManager that created us. */
  152. private final ServerManager manager;
  153. // </editor-fold>
  154. // </editor-fold>
  155. // <editor-fold defaultstate="collapsed" desc="Constructors">
  156. /**
  157. * Creates a new server which will connect to the specified URL with
  158. * the specified profile.
  159. *
  160. * @since 0.6.3
  161. * @param manager The server manager that owns this server.
  162. * @param configManager THe configuration manager to read config settings from.
  163. * @param commandParser The parser to use for commands in this server's window.
  164. * @param parserFactory The factory to use to generate parsers.
  165. * @param windowManager The window manager to register this server with.
  166. * @param aliasWrapper The actions wrapper to retrieve aliases from.
  167. * @param commandController The controller to use to retrieve commands.
  168. * @param uri The address of the server to connect to
  169. * @param profile The profile to use
  170. */
  171. public Server(
  172. final ServerManager manager,
  173. final ConfigManager configManager,
  174. final CommandParser commandParser,
  175. final ParserFactory parserFactory,
  176. final WindowManager windowManager,
  177. final AliasWrapper aliasWrapper,
  178. final CommandController commandController,
  179. final URI uri,
  180. final Identity profile) {
  181. super("server-disconnected",
  182. getHost(uri),
  183. getHost(uri),
  184. configManager,
  185. commandParser,
  186. Arrays.asList(WindowComponent.TEXTAREA.getIdentifier(),
  187. WindowComponent.INPUTFIELD.getIdentifier(),
  188. WindowComponent.CERTIFICATE_VIEWER.getIdentifier()));
  189. this.manager = manager;
  190. this.parserFactory = parserFactory;
  191. setConnectionDetails(uri, profile);
  192. manager.registerServer(this);
  193. windowManager.addWindow(this);
  194. // TODO: Server shouldn't have to know about the alias wrapper.
  195. tabCompleter.addEntries(TabCompletionType.COMMAND,
  196. aliasWrapper.getAliases());
  197. tabCompleter.addEntries(TabCompletionType.COMMAND,
  198. commandController.getCommandNames(CommandType.TYPE_SERVER));
  199. tabCompleter.addEntries(TabCompletionType.COMMAND,
  200. commandController.getCommandNames(CommandType.TYPE_GLOBAL));
  201. updateIcon();
  202. // TODO: Don't start timers in the constructor!
  203. whoTimer = new Timer("Server Who Timer");
  204. whoTimer.schedule(new TimerTask() {
  205. @Override
  206. public void run() {
  207. for (Channel channel : channels.values()) {
  208. channel.checkWho();
  209. }
  210. }
  211. }, 0, getConfigManager().getOptionInt(DOMAIN_GENERAL, "whotime"));
  212. if (getConfigManager().getOptionBool(DOMAIN_GENERAL, "showrawwindow")) {
  213. addRaw();
  214. }
  215. getConfigManager().addChangeListener("formatter", "serverName", this);
  216. getConfigManager().addChangeListener("formatter", "serverTitle", this);
  217. }
  218. // </editor-fold>
  219. // <editor-fold defaultstate="collapsed" desc="Connection, disconnection & reconnection">
  220. /**
  221. * Updates the connection details for this server. If the specified URI
  222. * does not define a port, the default port from the protocol description
  223. * will be used.
  224. *
  225. * @param uri The new URI that this server should connect to
  226. * @param profile The profile that this server should use
  227. */
  228. private void setConnectionDetails(final URI uri, final Identity profile) {
  229. this.address = uri;
  230. this.protocolDescription = parserFactory.getDescription(uri);
  231. this.profile = profile;
  232. if (uri.getPort() == -1 && protocolDescription != null) {
  233. try {
  234. this.address = new URI(uri.getScheme(), uri.getUserInfo(),
  235. uri.getHost(), protocolDescription.getDefaultPort(),
  236. uri.getPath(), uri.getQuery(), uri.getFragment());
  237. } catch (URISyntaxException ex) {
  238. Logger.appError(ErrorLevel.MEDIUM, "Unable to construct URI", ex);
  239. }
  240. }
  241. }
  242. /** {@inheritDoc} */
  243. @Override
  244. public void connect() {
  245. connect(address, profile);
  246. }
  247. /** {@inheritDoc} */
  248. @Override
  249. @Precondition({
  250. "The current parser is null or not connected",
  251. "The specified profile is not null"
  252. })
  253. @SuppressWarnings("fallthrough")
  254. public void connect(final URI address, final Identity profile) {
  255. assert profile != null;
  256. synchronized (myStateLock) {
  257. log.info("Connecting to {}, current state is {}", address,
  258. myState.getState());
  259. switch (myState.getState()) {
  260. case RECONNECT_WAIT:
  261. log.debug("Cancelling reconnection timer");
  262. reconnectTimer.cancel();
  263. break;
  264. case CLOSING:
  265. // Ignore the connection attempt
  266. return;
  267. case CONNECTED:
  268. case CONNECTING:
  269. disconnect(getConfigManager().getOption(DOMAIN_GENERAL, "quitmessage"));
  270. case DISCONNECTING:
  271. while (!myState.getState().isDisconnected()) {
  272. try {
  273. myStateLock.wait();
  274. } catch (InterruptedException ex) {
  275. return;
  276. }
  277. }
  278. break;
  279. default:
  280. // Do nothing
  281. break;
  282. }
  283. final URI connectAddress;
  284. try {
  285. parserLock.writeLock().lock();
  286. if (parser != null) {
  287. throw new IllegalArgumentException("Connection attempt while parser "
  288. + "is still connected.\n\nMy state:" + getState());
  289. }
  290. getConfigManager().migrate(address.getScheme(), "", "", address.getHost());
  291. setConnectionDetails(address, profile);
  292. updateTitle();
  293. updateIcon();
  294. parser = buildParser();
  295. if (parser == null) {
  296. addLine("serverUnknownProtocol", address.getScheme());
  297. return;
  298. }
  299. connectAddress = parser.getURI();
  300. } finally {
  301. parserLock.writeLock().unlock();
  302. }
  303. addLine("serverConnecting", connectAddress.getHost(), connectAddress.getPort());
  304. myState.transition(ServerState.CONNECTING);
  305. doCallbacks();
  306. updateAwayState(null);
  307. removeInvites();
  308. parser.connect();
  309. if (parser instanceof ThreadedParser) {
  310. ((ThreadedParser)parser).getControlThread().setName("Parser - " + connectAddress.getHost());
  311. }
  312. }
  313. ActionManager.getActionManager().triggerEvent(
  314. CoreActionType.SERVER_CONNECTING, null, this);
  315. }
  316. /** {@inheritDoc} */
  317. @Override
  318. public void reconnect(final String reason) {
  319. synchronized (myStateLock) {
  320. if (myState.getState() == ServerState.CLOSING) {
  321. return;
  322. }
  323. disconnect(reason);
  324. connect(address, profile);
  325. }
  326. }
  327. /** {@inheritDoc} */
  328. @Override
  329. public void reconnect() {
  330. reconnect(getConfigManager().getOption(DOMAIN_GENERAL, "reconnectmessage"));
  331. }
  332. /** {@inheritDoc} */
  333. @Override
  334. public void disconnect() {
  335. disconnect(getConfigManager().getOption(DOMAIN_GENERAL, "quitmessage"));
  336. }
  337. /** {@inheritDoc} */
  338. @Override
  339. public void disconnect(final String reason) {
  340. synchronized (myStateLock) {
  341. log.info("Disconnecting. Current state: {}", myState.getState());
  342. switch (myState.getState()) {
  343. case CLOSING:
  344. case DISCONNECTING:
  345. case DISCONNECTED:
  346. case TRANSIENTLY_DISCONNECTED:
  347. return;
  348. case RECONNECT_WAIT:
  349. log.debug("Cancelling reconnection timer");
  350. reconnectTimer.cancel();
  351. break;
  352. default:
  353. break;
  354. }
  355. clearChannels();
  356. backgroundChannels.clear();
  357. try {
  358. parserLock.readLock().lock();
  359. if (parser == null) {
  360. myState.transition(ServerState.DISCONNECTED);
  361. } else {
  362. myState.transition(ServerState.DISCONNECTING);
  363. removeInvites();
  364. updateIcon();
  365. parser.disconnect(reason);
  366. }
  367. } finally {
  368. parserLock.readLock().unlock();
  369. }
  370. if (getConfigManager().getOptionBool(DOMAIN_GENERAL,
  371. "closechannelsonquit")) {
  372. closeChannels();
  373. }
  374. if (getConfigManager().getOptionBool(DOMAIN_GENERAL,
  375. "closequeriesonquit")) {
  376. closeQueries();
  377. }
  378. }
  379. }
  380. /**
  381. * Schedules a reconnect attempt to be performed after a user-defiend delay.
  382. */
  383. @Precondition("The server state is transiently disconnected")
  384. private void doDelayedReconnect() {
  385. synchronized (myStateLock) {
  386. log.info("Performing delayed reconnect. State: {}", myState.getState());
  387. if (myState.getState() != ServerState.TRANSIENTLY_DISCONNECTED) {
  388. throw new IllegalStateException("doDelayedReconnect when not "
  389. + "transiently disconnected\n\nState: " + myState);
  390. }
  391. final int delay = Math.max(1000,
  392. getConfigManager().getOptionInt(DOMAIN_GENERAL, "reconnectdelay"));
  393. handleNotification("connectRetry", getAddress(), delay / 1000);
  394. reconnectTimer = new Timer("Server Reconnect Timer");
  395. reconnectTimer.schedule(new TimerTask() {
  396. @Override
  397. public void run() {
  398. reconnectTimer.cancel();
  399. synchronized (myStateLock) {
  400. log.debug("Reconnect task executing, state: {}",
  401. myState.getState());
  402. if (myState.getState() == ServerState.RECONNECT_WAIT) {
  403. myState.transition(ServerState.TRANSIENTLY_DISCONNECTED);
  404. reconnect();
  405. }
  406. }
  407. }
  408. }, delay);
  409. log.info("Scheduling reconnect task for delay of {}", delay);
  410. myState.transition(ServerState.RECONNECT_WAIT);
  411. updateIcon();
  412. }
  413. }
  414. // </editor-fold>
  415. // <editor-fold defaultstate="collapsed" desc="Child windows">
  416. /** {@inheritDoc} */
  417. @Override
  418. public boolean hasChannel(final String channel) {
  419. return channels.containsKey(converter.toLowerCase(channel));
  420. }
  421. /** {@inheritDoc} */
  422. @Override
  423. public Channel getChannel(final String channel) {
  424. return channels.get(converter.toLowerCase(channel));
  425. }
  426. /** {@inheritDoc} */
  427. @Override
  428. public List<String> getChannels() {
  429. return new ArrayList<String>(channels.keySet());
  430. }
  431. /** {@inheritDoc} */
  432. @Override
  433. public boolean hasQuery(final String host) {
  434. return queries.containsKey(converter.toLowerCase(parseHostmask(host)[0]));
  435. }
  436. /** {@inheritDoc} */
  437. @Override
  438. public Query getQuery(final String host) {
  439. return getQuery(host, false);
  440. }
  441. /** {@inheritDoc} */
  442. @Override
  443. public Query getQuery(final String host, final boolean focus) {
  444. synchronized (myStateLock) {
  445. if (myState.getState() == ServerState.CLOSING) {
  446. // Can't open queries while the server is closing
  447. return null;
  448. }
  449. }
  450. final String nick = parseHostmask(host)[0];
  451. final String lnick = converter.toLowerCase(nick);
  452. if (!queries.containsKey(lnick)) {
  453. final Query newQuery = new Query(this, host, focus);
  454. tabCompleter.addEntry(TabCompletionType.QUERY_NICK, nick);
  455. queries.put(lnick, newQuery);
  456. }
  457. return queries.get(lnick);
  458. }
  459. /** {@inheritDoc} */
  460. @Override
  461. public void updateQuery(final Query query, final String oldNick, final String newNick) {
  462. tabCompleter.removeEntry(TabCompletionType.QUERY_NICK, oldNick);
  463. tabCompleter.addEntry(TabCompletionType.QUERY_NICK, newNick);
  464. queries.put(converter.toLowerCase(newNick), query);
  465. queries.remove(converter.toLowerCase(oldNick));
  466. }
  467. /** {@inheritDoc} */
  468. @Override
  469. public Collection<Query> getQueries() {
  470. return Collections.unmodifiableCollection(queries.values());
  471. }
  472. /** {@inheritDoc} */
  473. @Override
  474. public void delQuery(final Query query) {
  475. tabCompleter.removeEntry(TabCompletionType.QUERY_NICK, query.getNickname());
  476. queries.remove(converter.toLowerCase(query.getNickname()));
  477. }
  478. /** {@inheritDoc} */
  479. @Override
  480. public void addRaw() {
  481. if (raw == null) {
  482. raw = new Raw(this);
  483. try {
  484. parserLock.readLock().lock();
  485. if (parser != null) {
  486. raw.registerCallbacks();
  487. }
  488. } finally {
  489. parserLock.readLock().unlock();
  490. }
  491. }
  492. }
  493. /** {@inheritDoc} */
  494. @Override
  495. public Raw getRaw() {
  496. return raw;
  497. }
  498. /** {@inheritDoc} */
  499. @Override
  500. public void delRaw() {
  501. raw = null; //NOPMD
  502. }
  503. /** {@inheritDoc} */
  504. @Override
  505. public void delChannel(final String chan) {
  506. tabCompleter.removeEntry(TabCompletionType.CHANNEL, chan);
  507. channels.remove(converter.toLowerCase(chan));
  508. }
  509. /** {@inheritDoc} */
  510. @Override
  511. public Channel addChannel(final ChannelInfo chan) {
  512. return addChannel(chan, !backgroundChannels.contains(chan.getName())
  513. || getConfigManager().getOptionBool(DOMAIN_GENERAL,
  514. "hidechannels"));
  515. }
  516. /** {@inheritDoc} */
  517. @Override
  518. public Channel addChannel(final ChannelInfo chan, final boolean focus) {
  519. synchronized (myStateLock) {
  520. if (myState.getState() == ServerState.CLOSING) {
  521. // Can't join channels while the server is closing
  522. return null;
  523. }
  524. }
  525. backgroundChannels.remove(chan.getName());
  526. if (hasChannel(chan.getName())) {
  527. getChannel(chan.getName()).setChannelInfo(chan);
  528. getChannel(chan.getName()).selfJoin();
  529. } else {
  530. final Channel newChan = new Channel(this, chan, focus);
  531. tabCompleter.addEntry(TabCompletionType.CHANNEL, chan.getName());
  532. channels.put(converter.toLowerCase(chan.getName()), newChan);
  533. }
  534. return getChannel(chan.getName());
  535. }
  536. /**
  537. * Closes all open channel windows associated with this server.
  538. */
  539. private void closeChannels() {
  540. for (Channel channel : new ArrayList<Channel>(channels.values())) {
  541. channel.close();
  542. }
  543. }
  544. /**
  545. * Clears the nicklist of all open channels.
  546. */
  547. private void clearChannels() {
  548. for (Channel channel : channels.values()) {
  549. channel.resetWindow();
  550. }
  551. }
  552. /**
  553. * Closes all open query windows associated with this server.
  554. */
  555. private void closeQueries() {
  556. for (Query query : new ArrayList<Query>(queries.values())) {
  557. query.close();
  558. }
  559. }
  560. // </editor-fold>
  561. // <editor-fold defaultstate="collapsed" desc="Miscellaneous methods">
  562. /**
  563. * Retrieves the host component of the specified URI, or throws a relevant
  564. * exception if this is not possible.
  565. *
  566. * @param uri The URI to be processed
  567. * @return The URI's host component, as returned by {@link URI#getHost()}.
  568. * @throws NullPointerException If <code>uri</code> is null
  569. * @throws IllegalArgumentException If the specified URI has no host
  570. * @since 0.6.4
  571. */
  572. private static String getHost(final URI uri) {
  573. if (uri.getHost() == null) {
  574. throw new IllegalArgumentException("URIs must have hosts");
  575. }
  576. return uri.getHost();
  577. }
  578. /**
  579. * Builds an appropriately configured {@link Parser} for this server.
  580. *
  581. * @return A configured parser.
  582. */
  583. private Parser buildParser() {
  584. final MyInfo myInfo = buildMyInfo();
  585. final Parser myParser = parserFactory.getParser(myInfo, address);
  586. if (myParser instanceof SecureParser) {
  587. certificateManager = new CertificateManager(address.getHost(), getConfigManager());
  588. final SecureParser secureParser = (SecureParser) myParser;
  589. secureParser.setTrustManagers(new TrustManager[]{certificateManager});
  590. secureParser.setKeyManagers(certificateManager.getKeyManager());
  591. certificateManager.addCertificateProblemListener(this);
  592. }
  593. if (myParser instanceof EncodingParser) {
  594. final EncodingParser encodingParser = (EncodingParser) myParser;
  595. encodingParser.setEncoder(new MessageEncoder(this, myParser));
  596. }
  597. if (myParser != null) {
  598. myParser.setIgnoreList(ignoreList);
  599. myParser.setPingTimerInterval(getConfigManager().getOptionInt(DOMAIN_SERVER,
  600. "pingtimer"));
  601. myParser.setPingTimerFraction((int) (getConfigManager().getOptionInt(DOMAIN_SERVER,
  602. "pingfrequency") / myParser.getPingTimerInterval()));
  603. if (getConfigManager().hasOptionString(DOMAIN_GENERAL, "bindip")) {
  604. myParser.setBindIP(getConfigManager().getOption(DOMAIN_GENERAL, "bindip"));
  605. }
  606. myParser.setProxy(buildProxyURI());
  607. }
  608. return myParser;
  609. }
  610. /**
  611. * Constructs a URI for the configured proxy for this server, if any.
  612. *
  613. * @return An appropriate URI or null if no proxy is configured
  614. */
  615. private URI buildProxyURI() {
  616. if (getConfigManager().hasOptionString(DOMAIN_SERVER, "proxy.address")) {
  617. final String type;
  618. if (getConfigManager().hasOptionString(DOMAIN_SERVER, "proxy.type")) {
  619. type = getConfigManager().getOption(DOMAIN_SERVER, "proxy.type");
  620. } else {
  621. type = "socks";
  622. }
  623. final int port;
  624. if (getConfigManager().hasOptionInt(DOMAIN_SERVER, "proxy.port")) {
  625. port = getConfigManager().getOptionInt(DOMAIN_SERVER, "proxy.port");
  626. } else {
  627. port = 8080;
  628. }
  629. final String host = getConfigManager().getOptionString(DOMAIN_SERVER, "proxy.address");
  630. final String userInfo;
  631. if (getConfigManager().hasOptionString(DOMAIN_SERVER, "proxy.username")
  632. && getConfigManager().hasOptionString(DOMAIN_SERVER, "proxy.password")) {
  633. userInfo = getConfigManager().getOption(DOMAIN_SERVER, "proxy.username")
  634. + getConfigManager().getOption(DOMAIN_SERVER, "proxy.password");
  635. } else {
  636. userInfo = "";
  637. }
  638. try {
  639. return new URI(type, userInfo, host, port, "", "", "");
  640. } catch (URISyntaxException ex) {
  641. Logger.appError(ErrorLevel.MEDIUM, "Unable to create proxy URI", ex);
  642. }
  643. }
  644. return null;
  645. }
  646. /** {@inheritDoc} */
  647. @Override
  648. public boolean compareURI(final URI uri) {
  649. if (parser != null) {
  650. return parser.compareURI(uri);
  651. }
  652. if (oldParser != null) {
  653. return oldParser.compareURI(uri);
  654. }
  655. return false;
  656. }
  657. /** {@inheritDoc} */
  658. @Override
  659. public String[] parseHostmask(final String hostmask) {
  660. return protocolDescription.parseHostmask(hostmask);
  661. }
  662. /**
  663. * Retrieves the MyInfo object used for the Parser.
  664. *
  665. * @return The MyInfo object for our profile
  666. */
  667. @Precondition({
  668. "The current profile is not null",
  669. "The current profile specifies at least one nickname"
  670. })
  671. private MyInfo buildMyInfo() {
  672. Logger.assertTrue(profile != null);
  673. Logger.assertTrue(!profile.getOptionList(DOMAIN_PROFILE, "nicknames").isEmpty());
  674. final MyInfo myInfo = new MyInfo();
  675. myInfo.setNickname(profile.getOptionList(DOMAIN_PROFILE, "nicknames").get(0));
  676. myInfo.setRealname(profile.getOption(DOMAIN_PROFILE, "realname"));
  677. if (profile.hasOptionString(DOMAIN_PROFILE, "ident")) {
  678. myInfo.setUsername(profile.getOption(DOMAIN_PROFILE, "ident"));
  679. }
  680. return myInfo;
  681. }
  682. /**
  683. * Updates this server's icon.
  684. */
  685. private void updateIcon() {
  686. final String icon = myState.getState() == ServerState.CONNECTED
  687. ? protocolDescription.isSecure(address)
  688. ? "secure-server" : "server" : "server-disconnected";
  689. setIcon(icon);
  690. }
  691. /**
  692. * Registers callbacks.
  693. */
  694. private void doCallbacks() {
  695. if (raw != null) {
  696. raw.registerCallbacks();
  697. }
  698. eventHandler.registerCallbacks();
  699. for (Query query : queries.values()) {
  700. query.reregister();
  701. }
  702. }
  703. /** {@inheritDoc} */
  704. @Override
  705. public void join(final ChannelJoinRequest ... requests) {
  706. join(true, requests);
  707. }
  708. /** {@inheritDoc} */
  709. @Override
  710. public void join(final boolean focus, final ChannelJoinRequest ... requests) {
  711. synchronized (myStateLock) {
  712. if (myState.getState() == ServerState.CONNECTED) {
  713. final List<ChannelJoinRequest> pending = new ArrayList<ChannelJoinRequest>();
  714. for (ChannelJoinRequest request : requests) {
  715. removeInvites(request.getName());
  716. final String name;
  717. if (parser.isValidChannelName(request.getName())) {
  718. name = request.getName();
  719. } else {
  720. name = parser.getChannelPrefixes().substring(0, 1)
  721. + request.getName();
  722. }
  723. if (!hasChannel(name) || !getChannel(name).isOnChannel()) {
  724. if (!focus) {
  725. backgroundChannels.add(name);
  726. }
  727. pending.add(request);
  728. }
  729. }
  730. parser.joinChannels(pending.toArray(new ChannelJoinRequest[pending.size()]));
  731. }
  732. // TODO: otherwise: address.getChannels().add(channel);
  733. }
  734. }
  735. /** {@inheritDoc} */
  736. @Override
  737. public void sendLine(final String line) {
  738. synchronized (myStateLock) {
  739. try {
  740. parserLock.readLock().lock();
  741. if (parser != null && !line.isEmpty()
  742. && myState.getState() == ServerState.CONNECTED) {
  743. parser.sendRawMessage(line);
  744. }
  745. } finally {
  746. parserLock.readLock().unlock();
  747. }
  748. }
  749. }
  750. /** {@inheritDoc} */
  751. @Override
  752. public int getMaxLineLength() {
  753. try {
  754. parserLock.readLock().lock();
  755. return parser == null ? -1 : parser.getMaxLength();
  756. } finally {
  757. parserLock.readLock().unlock();
  758. }
  759. }
  760. /** {@inheritDoc} */
  761. @Override
  762. public Parser getParser() {
  763. return parser;
  764. }
  765. /** {@inheritDoc} */
  766. @Override
  767. public Identity getProfile() {
  768. return profile;
  769. }
  770. /** {@inheritDoc} */
  771. @Override
  772. public String getChannelPrefixes() {
  773. try {
  774. parserLock.readLock().lock();
  775. return parser == null ? "#&" : parser.getChannelPrefixes();
  776. } finally {
  777. parserLock.readLock().unlock();
  778. }
  779. }
  780. /** {@inheritDoc} */
  781. @Override
  782. public String getAddress() {
  783. try {
  784. parserLock.readLock().lock();
  785. return parser == null ? address.getHost() : parser.getServerName();
  786. } finally {
  787. parserLock.readLock().unlock();
  788. }
  789. }
  790. /** {@inheritDoc} */
  791. @Override
  792. public String getNetwork() {
  793. try {
  794. parserLock.readLock().lock();
  795. if (parser == null) {
  796. throw new IllegalStateException("getNetwork called when "
  797. + "parser is null (state: " + getState() + ")");
  798. } else if (parser.getNetworkName().isEmpty()) {
  799. return getNetworkFromServerName(parser.getServerName());
  800. } else {
  801. return parser.getNetworkName();
  802. }
  803. } finally {
  804. parserLock.readLock().unlock();
  805. }
  806. }
  807. /** {@inheritDoc} */
  808. @Override
  809. public boolean isNetwork(final String target) {
  810. synchronized (myStateLock) {
  811. try {
  812. parserLock.readLock().lock();
  813. if (parser == null) {
  814. return false;
  815. } else {
  816. return getNetwork().equalsIgnoreCase(target);
  817. }
  818. } finally {
  819. parserLock.readLock().unlock();
  820. }
  821. }
  822. }
  823. /**
  824. * Calculates a network name from the specified server name. This method
  825. * implements parts 2-4 of the procedure documented at getNetwork().
  826. *
  827. * @param serverName The server name to parse
  828. * @return A network name for the specified server
  829. */
  830. protected static String getNetworkFromServerName(final String serverName) {
  831. final String[] parts = serverName.split("\\.");
  832. final String[] tlds = {"biz", "com", "info", "net", "org"};
  833. boolean isTLD = false;
  834. for (String tld : tlds) {
  835. if (serverName.endsWith("." + tld)) {
  836. isTLD = true;
  837. break;
  838. }
  839. }
  840. if (isTLD && parts.length > 2) {
  841. return parts[parts.length - 2] + "." + parts[parts.length - 1];
  842. } else if (parts.length > 2) {
  843. final StringBuilder network = new StringBuilder();
  844. for (int i = 1; i < parts.length; i++) {
  845. if (network.length() > 0) {
  846. network.append('.');
  847. }
  848. network.append(parts[i]);
  849. }
  850. return network.toString();
  851. } else {
  852. return serverName;
  853. }
  854. }
  855. /** {@inheritDoc} */
  856. @Override
  857. public String getIrcd() {
  858. return parser.getServerSoftwareType();
  859. }
  860. /** {@inheritDoc} */
  861. @Override
  862. public String getProtocol() {
  863. return address.getScheme();
  864. }
  865. /** {@inheritDoc} */
  866. @Override
  867. public boolean isAway() {
  868. return awayMessage != null;
  869. }
  870. /** {@inheritDoc} */
  871. @Override
  872. public String getAwayMessage() {
  873. return awayMessage;
  874. }
  875. /** {@inheritDoc} */
  876. @Override
  877. public TabCompleter getTabCompleter() {
  878. return tabCompleter;
  879. }
  880. /** {@inheritDoc} */
  881. @Override
  882. public ServerState getState() {
  883. return myState.getState();
  884. }
  885. /** {@inheritDoc} */
  886. @Override
  887. public ServerStatus getStatus() {
  888. return myState;
  889. }
  890. /** {@inheritDoc} */
  891. @Override
  892. public void windowClosing() {
  893. synchronized (myStateLock) {
  894. // 2: Remove any callbacks or listeners
  895. eventHandler.unregisterCallbacks();
  896. getConfigManager().removeListener(this);
  897. whoTimer.cancel();
  898. // 3: Trigger any actions neccessary
  899. disconnect();
  900. myState.transition(ServerState.CLOSING);
  901. }
  902. closeChannels();
  903. closeQueries();
  904. removeInvites();
  905. if (raw != null) {
  906. raw.close();
  907. }
  908. // 4: Trigger action for the window closing
  909. // 5: Inform any parents that the window is closing
  910. manager.unregisterServer(this);
  911. }
  912. /** {@inheritDoc} */
  913. @Override
  914. public void windowClosed() {
  915. // 7: Remove any references to the window and parents
  916. oldParser = null; //NOPMD
  917. parser = null; //NOPMD
  918. }
  919. /** {@inheritDoc} */
  920. @Override
  921. public void addLineToAll(final String messageType, final Date date,
  922. final Object... args) {
  923. for (Channel channel : channels.values()) {
  924. channel.addLine(messageType, date, args);
  925. }
  926. for (Query query : queries.values()) {
  927. query.addLine(messageType, date, args);
  928. }
  929. addLine(messageType, date, args);
  930. }
  931. /** {@inheritDoc} */
  932. @Override
  933. public void sendCTCPReply(final String source, final String type, final String args) {
  934. if (type.equalsIgnoreCase("VERSION")) {
  935. parser.sendCTCPReply(source, "VERSION", "DMDirc "
  936. + getConfigManager().getOption("version", "version")
  937. + " - http://www.dmdirc.com/");
  938. } else if (type.equalsIgnoreCase("PING")) {
  939. parser.sendCTCPReply(source, "PING", args);
  940. } else if (type.equalsIgnoreCase("CLIENTINFO")) {
  941. parser.sendCTCPReply(source, "CLIENTINFO", "VERSION PING CLIENTINFO");
  942. }
  943. }
  944. /** {@inheritDoc} */
  945. @Override
  946. public boolean isValidChannelName(final String channelName) {
  947. try {
  948. parserLock.readLock().lock();
  949. return hasChannel(channelName)
  950. || (parser != null && parser.isValidChannelName(channelName));
  951. } finally {
  952. parserLock.readLock().unlock();
  953. }
  954. }
  955. /** {@inheritDoc} */
  956. @Override
  957. public Server getServer() {
  958. return this;
  959. }
  960. /** {@inheritDoc} */
  961. @Override
  962. protected boolean processNotificationArg(final Object arg, final List<Object> args) {
  963. if (arg instanceof ClientInfo) {
  964. final ClientInfo clientInfo = (ClientInfo) arg;
  965. args.add(clientInfo.getNickname());
  966. args.add(clientInfo.getUsername());
  967. args.add(clientInfo.getHostname());
  968. return true;
  969. } else {
  970. return super.processNotificationArg(arg, args);
  971. }
  972. }
  973. /** {@inheritDoc} */
  974. @Override
  975. public void updateTitle() {
  976. synchronized (myStateLock) {
  977. if (myState.getState() == ServerState.CLOSING) {
  978. return;
  979. }
  980. try {
  981. parserLock.readLock().lock();
  982. final Object[] arguments = new Object[]{
  983. address.getHost(), parser == null ? "Unknown" : parser.getServerName(),
  984. address.getPort(), parser == null ? "Unknown" : getNetwork(),
  985. parser == null ? "Unknown" : parser.getLocalClient().getNickname(),
  986. };
  987. setName(Formatter.formatMessage(getConfigManager(),
  988. "serverName", arguments));
  989. setTitle(Formatter.formatMessage(getConfigManager(),
  990. "serverTitle", arguments));
  991. } finally {
  992. parserLock.readLock().unlock();
  993. }
  994. }
  995. }
  996. /** {@inheritDoc} */
  997. @Override
  998. public void configChanged(final String domain, final String key) {
  999. if ("formatter".equals(domain)) {
  1000. updateTitle();
  1001. }
  1002. }
  1003. // </editor-fold>
  1004. // <editor-fold defaultstate="collapsed" desc="Parser callbacks">
  1005. /**
  1006. * Called when the server says that the nickname we're trying to use is
  1007. * already in use.
  1008. *
  1009. * @param nickname The nickname that we were trying to use
  1010. */
  1011. public void onNickInUse(final String nickname) {
  1012. final String lastNick = parser.getLocalClient().getNickname();
  1013. // If our last nick is still valid, ignore the in use message
  1014. if (!converter.equalsIgnoreCase(lastNick, nickname)) {
  1015. return;
  1016. }
  1017. String newNick = lastNick + new Random().nextInt(10);
  1018. final List<String> alts = profile.getOptionList(DOMAIN_PROFILE, "nicknames");
  1019. int offset = 0;
  1020. // Loop so we can check case sensitivity
  1021. for (String alt : alts) {
  1022. offset++;
  1023. if (converter.equalsIgnoreCase(alt, lastNick)) {
  1024. break;
  1025. }
  1026. }
  1027. if (offset < alts.size() && !alts.get(offset).isEmpty()) {
  1028. newNick = alts.get(offset);
  1029. }
  1030. parser.getLocalClient().setNickname(newNick);
  1031. }
  1032. /**
  1033. * Called when the server sends a numeric event.
  1034. *
  1035. * @param numeric The numeric code for the event
  1036. * @param tokens The (tokenised) arguments of the event
  1037. */
  1038. public void onNumeric(final int numeric, final String[] tokens) {
  1039. String snumeric = String.valueOf(numeric);
  1040. if (numeric < 10) {
  1041. snumeric = "00" + snumeric;
  1042. } else if (numeric < 100) {
  1043. snumeric = "0" + snumeric;
  1044. }
  1045. final String sansIrcd = "numeric_" + snumeric;
  1046. StringBuffer target = new StringBuffer("");
  1047. if (getConfigManager().hasOptionString("formatter", sansIrcd)) {
  1048. target = new StringBuffer(sansIrcd);
  1049. } else if (getConfigManager().hasOptionString("formatter", "numeric_unknown")) {
  1050. target = new StringBuffer("numeric_unknown");
  1051. }
  1052. ActionManager.getActionManager().triggerEvent(
  1053. CoreActionType.SERVER_NUMERIC, target, this,
  1054. Integer.valueOf(numeric), tokens);
  1055. handleNotification(target.toString(), (Object[]) tokens);
  1056. }
  1057. /**
  1058. * Called when the socket has been closed.
  1059. */
  1060. public void onSocketClosed() {
  1061. log.info("Received socket closed event, state: {}", myState.getState());
  1062. if (Thread.holdsLock(myStateLock)) {
  1063. log.info("State lock contended: rerunning on a new thread");
  1064. new Thread(new Runnable() {
  1065. /** {@inheritDoc} */
  1066. @Override
  1067. public void run() {
  1068. onSocketClosed();
  1069. }
  1070. }, "Socket closed deferred thread").start();
  1071. return;
  1072. }
  1073. handleNotification("socketClosed", getAddress());
  1074. ActionManager.getActionManager().triggerEvent(
  1075. CoreActionType.SERVER_DISCONNECTED, null, this);
  1076. eventHandler.unregisterCallbacks();
  1077. synchronized (myStateLock) {
  1078. if (myState.getState() == ServerState.CLOSING
  1079. || myState.getState() == ServerState.DISCONNECTED) {
  1080. // This has been triggered via .disconnect()
  1081. return;
  1082. }
  1083. if (myState.getState() == ServerState.DISCONNECTING) {
  1084. myState.transition(ServerState.DISCONNECTED);
  1085. } else {
  1086. myState.transition(ServerState.TRANSIENTLY_DISCONNECTED);
  1087. }
  1088. clearChannels();
  1089. try {
  1090. parserLock.writeLock().lock();
  1091. oldParser = parser;
  1092. parser = null;
  1093. } finally {
  1094. parserLock.writeLock().unlock();
  1095. }
  1096. updateIcon();
  1097. if (getConfigManager().getOptionBool(DOMAIN_GENERAL,
  1098. "closechannelsondisconnect")) {
  1099. closeChannels();
  1100. }
  1101. if (getConfigManager().getOptionBool(DOMAIN_GENERAL,
  1102. "closequeriesondisconnect")) {
  1103. closeQueries();
  1104. }
  1105. removeInvites();
  1106. updateAwayState(null);
  1107. if (getConfigManager().getOptionBool(DOMAIN_GENERAL,
  1108. "reconnectondisconnect")
  1109. && myState.getState() == ServerState.TRANSIENTLY_DISCONNECTED) {
  1110. doDelayedReconnect();
  1111. }
  1112. }
  1113. }
  1114. /**
  1115. * Called when an error was encountered while connecting.
  1116. *
  1117. * @param errorInfo The parser's error information
  1118. */
  1119. @Precondition("The current server state is CONNECTING")
  1120. public void onConnectError(final ParserError errorInfo) {
  1121. synchronized (myStateLock) {
  1122. log.info("Received connect error event, state: {}; error: {}",
  1123. myState.getState(), errorInfo);
  1124. if (myState.getState() == ServerState.CLOSING
  1125. || myState.getState() == ServerState.DISCONNECTING) {
  1126. // Do nothing
  1127. return;
  1128. } else if (myState.getState() != ServerState.CONNECTING) {
  1129. // Shouldn't happen
  1130. throw new IllegalStateException("Connect error when not "
  1131. + "connecting\n\n" + getStatus().getTransitionHistory());
  1132. }
  1133. myState.transition(ServerState.TRANSIENTLY_DISCONNECTED);
  1134. try {
  1135. parserLock.writeLock().lock();
  1136. oldParser = parser;
  1137. parser = null;
  1138. } finally {
  1139. parserLock.writeLock().unlock();
  1140. }
  1141. updateIcon();
  1142. String description;
  1143. if (errorInfo.getException() == null) {
  1144. description = errorInfo.getData();
  1145. } else {
  1146. final Exception exception = errorInfo.getException();
  1147. if (exception instanceof java.net.UnknownHostException) {
  1148. description = "Unknown host (unable to resolve)";
  1149. } else if (exception instanceof java.net.NoRouteToHostException) {
  1150. description = "No route to host";
  1151. } else if (exception instanceof java.net.SocketTimeoutException) {
  1152. description = "Connection attempt timed out";
  1153. } else if (exception instanceof java.net.SocketException
  1154. || exception instanceof javax.net.ssl.SSLException) {
  1155. description = exception.getMessage();
  1156. } else {
  1157. Logger.appError(ErrorLevel.LOW, "Unknown socket error: "
  1158. + exception.getClass().getCanonicalName(),
  1159. new IllegalArgumentException(exception));
  1160. description = "Unknown error: " + exception.getMessage();
  1161. }
  1162. }
  1163. ActionManager.getActionManager().triggerEvent(
  1164. CoreActionType.SERVER_CONNECTERROR, null, this,
  1165. description);
  1166. handleNotification("connectError", getAddress(), description);
  1167. if (getConfigManager().getOptionBool(DOMAIN_GENERAL,
  1168. "reconnectonconnectfailure")) {
  1169. doDelayedReconnect();
  1170. }
  1171. }
  1172. }
  1173. /**
  1174. * Called when we fail to receive a ping reply within a set period of time.
  1175. */
  1176. public void onPingFailed() {
  1177. StatusBarManager.getStatusBarManager().setMessage(new StatusMessage(
  1178. "No ping reply from " + getName() + " for over "
  1179. + ((int) (Math.floor(parser.getPingTime() / 1000.0)))
  1180. + " seconds.", getConfigManager()));
  1181. ActionManager.getActionManager().triggerEvent(
  1182. CoreActionType.SERVER_NOPING, null, this,
  1183. Long.valueOf(parser.getPingTime()));
  1184. if (parser.getPingTime()
  1185. >= getConfigManager().getOptionInt(DOMAIN_SERVER, "pingtimeout")) {
  1186. log.warn("Server appears to be stoned, reconnecting");
  1187. handleNotification("stonedServer", getAddress());
  1188. reconnect();
  1189. }
  1190. }
  1191. /**
  1192. * Called after the parser receives the 005 headers from the server.
  1193. */
  1194. @Precondition("State is CONNECTING")
  1195. public void onPost005() {
  1196. synchronized (myStateLock) {
  1197. if (myState.getState() != ServerState.CONNECTING) {
  1198. // Shouldn't happen
  1199. throw new IllegalStateException("Received onPost005 while not "
  1200. + "connecting\n\n" + myState.getTransitionHistory());
  1201. }
  1202. if (myState.getState() != ServerState.CONNECTING) {
  1203. // We've transitioned while waiting for the lock. Just abort.
  1204. return;
  1205. }
  1206. myState.transition(ServerState.CONNECTED);
  1207. getConfigManager().migrate(address.getScheme(),
  1208. parser.getServerSoftwareType(), getNetwork(), parser.getServerName());
  1209. updateIcon();
  1210. updateTitle();
  1211. updateIgnoreList();
  1212. converter = parser.getStringConverter();
  1213. final List<ChannelJoinRequest> requests = new ArrayList<ChannelJoinRequest>();
  1214. if (getConfigManager().getOptionBool(DOMAIN_GENERAL, "rejoinchannels")) {
  1215. for (Channel chan : channels.values()) {
  1216. requests.add(new ChannelJoinRequest(chan.getName()));
  1217. }
  1218. }
  1219. join(requests.toArray(new ChannelJoinRequest[requests.size()]));
  1220. checkModeAliases();
  1221. }
  1222. ActionManager.getActionManager().triggerEvent(
  1223. CoreActionType.SERVER_CONNECTED, null, this);
  1224. }
  1225. /**
  1226. * Checks that we have the necessary mode aliases for this server.
  1227. */
  1228. private void checkModeAliases() {
  1229. // Check we have mode aliases
  1230. final String modes = parser.getBooleanChannelModes() + parser.getListChannelModes()
  1231. + parser.getParameterChannelModes() + parser.getDoubleParameterChannelModes();
  1232. final String umodes = parser.getUserModes();
  1233. final StringBuffer missingModes = new StringBuffer();
  1234. final StringBuffer missingUmodes = new StringBuffer();
  1235. for (char mode : modes.toCharArray()) {
  1236. if (!getConfigManager().hasOptionString(DOMAIN_SERVER, "mode" + mode)) {
  1237. missingModes.append(mode);
  1238. }
  1239. }
  1240. for (char mode : umodes.toCharArray()) {
  1241. if (!getConfigManager().hasOptionString(DOMAIN_SERVER, "umode" + mode)) {
  1242. missingUmodes.append(mode);
  1243. }
  1244. }
  1245. if (missingModes.length() + missingUmodes.length() > 0) {
  1246. final StringBuffer missing = new StringBuffer("Missing mode aliases: ");
  1247. if (missingModes.length() > 0) {
  1248. missing.append("channel: +");
  1249. missing.append(missingModes);
  1250. }
  1251. if (missingUmodes.length() > 0) {
  1252. if (missingModes.length() > 0) {
  1253. missing.append(' ');
  1254. }
  1255. missing.append("user: +");
  1256. missing.append(missingUmodes);
  1257. }
  1258. Logger.appError(ErrorLevel.LOW, missing.toString() + " ["
  1259. + parser.getServerSoftwareType() + "]",
  1260. new MissingModeAliasException(getNetwork(), parser,
  1261. getConfigManager().getOption("identity",
  1262. "modealiasversion"), missing.toString()));
  1263. }
  1264. }
  1265. // </editor-fold>
  1266. // <editor-fold defaultstate="collapsed" desc="Ignore lists">
  1267. /** {@inheritDoc} */
  1268. @Override
  1269. public IgnoreList getIgnoreList() {
  1270. return ignoreList;
  1271. }
  1272. /** {@inheritDoc} */
  1273. @Override
  1274. public void updateIgnoreList() {
  1275. ignoreList.clear();
  1276. ignoreList.addAll(getConfigManager().getOptionList("network", "ignorelist"));
  1277. }
  1278. /** {@inheritDoc} */
  1279. @Override
  1280. public void saveIgnoreList() {
  1281. getNetworkIdentity().setOption("network", "ignorelist", ignoreList.getRegexList());
  1282. }
  1283. // </editor-fold>
  1284. // <editor-fold defaultstate="collapsed" desc="Identity handling">
  1285. /** {@inheritDoc} */
  1286. @Override
  1287. public Identity getServerIdentity() {
  1288. return IdentityManager.getIdentityManager().createServerConfig(parser.getServerName());
  1289. }
  1290. /** {@inheritDoc} */
  1291. @Override
  1292. public Identity getNetworkIdentity() {
  1293. return IdentityManager.getIdentityManager().createNetworkConfig(getNetwork());
  1294. }
  1295. // </editor-fold>
  1296. // <editor-fold defaultstate="collapsed" desc="Invite handling">
  1297. /** {@inheritDoc} */
  1298. @Override
  1299. public void addInviteListener(final InviteListener listener) {
  1300. synchronized (listeners) {
  1301. listeners.add(InviteListener.class, listener);
  1302. }
  1303. }
  1304. /** {@inheritDoc} */
  1305. @Override
  1306. public void removeInviteListener(final InviteListener listener) {
  1307. synchronized (listeners) {
  1308. listeners.remove(InviteListener.class, listener);
  1309. }
  1310. }
  1311. /** {@inheritDoc} */
  1312. @Override
  1313. public void addInvite(final Invite invite) {
  1314. synchronized (invites) {
  1315. for (Invite oldInvite : new ArrayList<Invite>(invites)) {
  1316. if (oldInvite.getChannel().equals(invite.getChannel())) {
  1317. removeInvite(oldInvite);
  1318. }
  1319. }
  1320. invites.add(invite);
  1321. synchronized (listeners) {
  1322. for (InviteListener listener : listeners.get(InviteListener.class)) {
  1323. listener.inviteReceived(this, invite);
  1324. }
  1325. }
  1326. }
  1327. }
  1328. /** {@inheritDoc} */
  1329. @Override
  1330. public void acceptInvites(final Invite ... invites) {
  1331. final ChannelJoinRequest[] requests = new ChannelJoinRequest[invites.length];
  1332. for (int i = 0; i < invites.length; i++) {
  1333. requests[i] = new ChannelJoinRequest(invites[i].getChannel());
  1334. }
  1335. join(requests);
  1336. }
  1337. /** {@inheritDoc} */
  1338. @Override
  1339. public void acceptInvites() {
  1340. synchronized (invites) {
  1341. acceptInvites(invites.toArray(new Invite[invites.size()]));
  1342. }
  1343. }
  1344. /** {@inheritDoc} */
  1345. @Override
  1346. public void removeInvites(final String channel) {
  1347. for (Invite invite : new ArrayList<Invite>(invites)) {
  1348. if (invite.getChannel().equals(channel)) {
  1349. removeInvite(invite);
  1350. }
  1351. }
  1352. }
  1353. /** {@inheritDoc} */
  1354. @Override
  1355. public void removeInvites() {
  1356. for (Invite invite : new ArrayList<Invite>(invites)) {
  1357. removeInvite(invite);
  1358. }
  1359. }
  1360. /** {@inheritDoc} */
  1361. @Override
  1362. public void removeInvite(final Invite invite) {
  1363. synchronized (invites) {
  1364. invites.remove(invite);
  1365. synchronized (listeners) {
  1366. for (InviteListener listener : listeners.get(InviteListener.class)) {
  1367. listener.inviteExpired(this, invite);
  1368. }
  1369. }
  1370. }
  1371. }
  1372. /** {@inheritDoc} */
  1373. @Override
  1374. public List<Invite> getInvites() {
  1375. return invites;
  1376. }
  1377. // </editor-fold>
  1378. // <editor-fold defaultstate="collapsed" desc="Away state handling">
  1379. /** {@inheritDoc} */
  1380. @Override
  1381. public void addAwayStateListener(final AwayStateListener listener) {
  1382. synchronized (listeners) {
  1383. listeners.add(AwayStateListener.class, listener);
  1384. }
  1385. }
  1386. /** {@inheritDoc} */
  1387. @Override
  1388. public void removeAwayStateListener(final AwayStateListener listener) {
  1389. synchronized (listeners) {
  1390. listeners.remove(AwayStateListener.class, listener);
  1391. }
  1392. }
  1393. /** {@inheritDoc} */
  1394. @Override
  1395. public void updateAwayState(final String message) {
  1396. if ((awayMessage != null && awayMessage.equals(message))
  1397. || (awayMessage == null && message == null)) {
  1398. return;
  1399. }
  1400. awayMessage = message;
  1401. new Thread(new Runnable() {
  1402. /** {@inheritDoc} */
  1403. @Override
  1404. public void run() {
  1405. synchronized (listeners) {
  1406. if (message == null) {
  1407. for (AwayStateListener listener : listeners.get(AwayStateListener.class)) {
  1408. listener.onBack();
  1409. }
  1410. } else {
  1411. for (AwayStateListener listener : listeners.get(AwayStateListener.class)) {
  1412. listener.onAway(message);
  1413. }
  1414. }
  1415. }
  1416. }
  1417. }, "Away state listener runner").start();
  1418. }
  1419. // </editor-fold>
  1420. // <editor-fold defaultstate="collapsed" desc="TLS listener handling">
  1421. /** {@inheritDoc} */
  1422. @Override
  1423. public void addCertificateProblemListener(final CertificateProblemListener listener) {
  1424. listeners.add(CertificateProblemListener.class, listener);
  1425. if (certificateManager != null && !certificateManager.getProblems().isEmpty()) {
  1426. listener.certificateProblemEncountered(certificateManager.getChain(),
  1427. certificateManager.getProblems(), certificateManager);
  1428. }
  1429. }
  1430. /** {@inheritDoc} */
  1431. @Override
  1432. public void removeCertificateProblemListener(final CertificateProblemListener listener) {
  1433. listeners.remove(CertificateProblemListener.class, listener);
  1434. }
  1435. /** {@inheritDoc} */
  1436. @Override
  1437. public void certificateProblemEncountered(final X509Certificate[] chain,
  1438. final Collection<CertificateException> problems,
  1439. final CertificateManager certificateManager) {
  1440. for (CertificateProblemListener listener : listeners.get(CertificateProblemListener.class)) {
  1441. listener.certificateProblemEncountered(chain, problems, certificateManager);
  1442. }
  1443. }
  1444. /** {@inheritDoc} */
  1445. @Override
  1446. public void certificateProblemResolved(final CertificateManager manager) {
  1447. for (CertificateProblemListener listener : listeners.get(CertificateProblemListener.class)) {
  1448. listener.certificateProblemResolved(manager);
  1449. }
  1450. }
  1451. // </editor-fold>
  1452. }