Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

Server.java 35KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020
  1. /*
  2. * Copyright (c) 2006-2017 DMDirc Developers
  3. *
  4. * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
  5. * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
  6. * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
  7. * permit persons to whom the Software is furnished to do so, subject to the following conditions:
  8. *
  9. * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
  10. * Software.
  11. *
  12. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
  13. * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
  14. * OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
  15. * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  16. */
  17. package com.dmdirc;
  18. import com.dmdirc.config.profiles.Profile;
  19. import com.dmdirc.events.FrameClosingEvent;
  20. import com.dmdirc.events.ServerConnectErrorEvent;
  21. import com.dmdirc.events.ServerConnectedEvent;
  22. import com.dmdirc.events.ServerConnectingEvent;
  23. import com.dmdirc.events.ServerDisconnectedEvent;
  24. import com.dmdirc.events.ServerReconnectScheduledEvent;
  25. import com.dmdirc.events.ServerUnknownProtocolEvent;
  26. import com.dmdirc.interfaces.Connection;
  27. import com.dmdirc.interfaces.GroupChatManager;
  28. import com.dmdirc.interfaces.InviteManager;
  29. import com.dmdirc.interfaces.PrivateChat;
  30. import com.dmdirc.interfaces.User;
  31. import com.dmdirc.interfaces.WindowModel;
  32. import com.dmdirc.config.provider.ConfigChangeListener;
  33. import com.dmdirc.config.provider.ConfigProvider;
  34. import com.dmdirc.config.provider.ConfigProviderMigrator;
  35. import com.dmdirc.interfaces.config.IdentityFactory;
  36. import com.dmdirc.parser.common.DefaultStringConverter;
  37. import com.dmdirc.parser.common.IgnoreList;
  38. import com.dmdirc.parser.common.ParserError;
  39. import com.dmdirc.parser.common.ThreadedParser;
  40. import com.dmdirc.parser.interfaces.ClientInfo;
  41. import com.dmdirc.parser.interfaces.EncodingParser;
  42. import com.dmdirc.parser.interfaces.Parser;
  43. import com.dmdirc.parser.interfaces.ProtocolDescription;
  44. import com.dmdirc.parser.interfaces.SecureParser;
  45. import com.dmdirc.parser.interfaces.StringConverter;
  46. import com.dmdirc.tls.CertificateManager;
  47. import com.dmdirc.ui.input.TabCompletionType;
  48. import com.dmdirc.ui.messages.Formatter;
  49. import com.dmdirc.ui.messages.HighlightManager;
  50. import com.google.common.net.InternetDomainName;
  51. import java.net.NoRouteToHostException;
  52. import java.net.SocketException;
  53. import java.net.SocketTimeoutException;
  54. import java.net.URI;
  55. import java.net.UnknownHostException;
  56. import java.util.ArrayList;
  57. import java.util.Collection;
  58. import java.util.Collections;
  59. import java.util.Map;
  60. import java.util.Optional;
  61. import java.util.concurrent.ConcurrentSkipListMap;
  62. import java.util.concurrent.ScheduledExecutorService;
  63. import java.util.concurrent.ScheduledFuture;
  64. import java.util.concurrent.TimeUnit;
  65. import java.util.concurrent.locks.ReadWriteLock;
  66. import java.util.concurrent.locks.ReentrantReadWriteLock;
  67. import java.util.function.Function;
  68. import javax.annotation.Nonnull;
  69. import javax.annotation.Nullable;
  70. import javax.net.ssl.SSLException;
  71. import net.engio.mbassy.listener.Handler;
  72. import org.slf4j.Logger;
  73. import org.slf4j.LoggerFactory;
  74. import static com.dmdirc.util.LogUtils.APP_ERROR;
  75. import static com.google.common.base.Preconditions.checkNotNull;
  76. /**
  77. * The Server class represents the client's view of a server. It maintains a list of all channels,
  78. * queries, etc, and handles parser callbacks pertaining to the server.
  79. */
  80. public class Server implements Connection {
  81. private static final Logger LOG = LoggerFactory.getLogger(Server.class);
  82. /** The name of the general domain. */
  83. private static final String DOMAIN_GENERAL = "general";
  84. /** Manager of group chats. */
  85. private final GroupChatManagerImpl groupChatManager;
  86. /** Manager of invites. */
  87. private final InviteManager inviteManager;
  88. /** Open query windows on the server. */
  89. private final Map<String, Query> queries = new ConcurrentSkipListMap<>();
  90. /** The user manager to retrieve users from. */
  91. private final UserManager userManager;
  92. /** The Parser instance handling this server. */
  93. @Nonnull
  94. private Optional<Parser> parser = Optional.empty();
  95. /** The Parser instance that used to be handling this server. */
  96. @Nonnull
  97. private Optional<Parser> oldParser = Optional.empty();
  98. /** The parser-supplied protocol description object. */
  99. @Nonnull
  100. private Optional<ProtocolDescription> protocolDescription = Optional.empty();
  101. /**
  102. * Object used to synchronise access to parser. This object should be locked by anything
  103. * requiring that the parser reference remains the same for a duration of time, or by anything
  104. * which is updating the parser reference.
  105. *
  106. * If used in conjunction with myStateLock, the parserLock must always be locked INSIDE the
  107. * myStateLock to prevent deadlocks.
  108. */
  109. private final ReadWriteLock parserLock = new ReentrantReadWriteLock();
  110. /** Object used to synchronise access to myState. */
  111. private final Object myStateLock = new Object();
  112. /** The current state of this server. */
  113. private final ServerStatus myState = new ServerStatus(this, myStateLock);
  114. /** The address of the server we're connecting to. */
  115. @Nonnull
  116. private URI address;
  117. /** The profile we're using. */
  118. @Nonnull
  119. private Profile profile;
  120. /** Our reason for being away, if any. */
  121. private Optional<String> awayMessage;
  122. /** Our event handler. */
  123. private final ServerEventHandler eventHandler;
  124. /** Our ignore list. */
  125. private final IgnoreList ignoreList = new IgnoreList();
  126. /** Our string converter. */
  127. private StringConverter converter = new DefaultStringConverter();
  128. /** ParserFactory we use for creating parsers. */
  129. private final ParserFactory parserFactory;
  130. /** Factory to use to create new identities. */
  131. private final IdentityFactory identityFactory;
  132. /** The migrator to use to change our config provider. */
  133. private final ConfigProviderMigrator configMigrator;
  134. /** Factory to use for creating queries. */
  135. private final QueryFactory queryFactory;
  136. /** The config provider to write user settings to. */
  137. private final ConfigProvider userSettings;
  138. /** Executor service to use to schedule repeated events. */
  139. private final ScheduledExecutorService executorService;
  140. /** The message encoder factory to create a message encoder with. */
  141. private final MessageEncoderFactory messageEncoderFactory;
  142. /** The manager to use for highlighting. */
  143. private final HighlightManager highlightManager;
  144. /** Listener to use for config changes. */
  145. private final ConfigChangeListener configListener = (domain, key) -> updateTitle();
  146. private final WindowModel windowModel;
  147. /** The future used when a reconnect timer is scheduled. */
  148. private ScheduledFuture<?> reconnectTimerFuture;
  149. /**
  150. * Creates a new server which will connect to the specified URL with the specified profile.
  151. */
  152. public Server(
  153. final WindowModel windowModel,
  154. final ConfigProviderMigrator configMigrator,
  155. final ParserFactory parserFactory,
  156. final IdentityFactory identityFactory,
  157. final QueryFactory queryFactory,
  158. final MessageEncoderFactory messageEncoderFactory,
  159. final ConfigProvider userSettings,
  160. final GroupChatManagerImplFactory groupChatManagerFactory,
  161. final ScheduledExecutorService executorService,
  162. @Nonnull final URI uri,
  163. @Nonnull final Profile profile,
  164. final UserManager userManager) {
  165. this.windowModel = windowModel;
  166. this.parserFactory = parserFactory;
  167. this.identityFactory = identityFactory;
  168. this.configMigrator = configMigrator;
  169. this.queryFactory = queryFactory;
  170. this.executorService = executorService;
  171. this.userSettings = userSettings;
  172. this.messageEncoderFactory = messageEncoderFactory;
  173. this.userManager = userManager;
  174. this.groupChatManager = groupChatManagerFactory.create(this);
  175. this.inviteManager = new InviteManagerImpl(this);
  176. awayMessage = Optional.empty();
  177. eventHandler = new ServerEventHandler(this, groupChatManager, windowModel.getEventBus());
  178. this.address = uri;
  179. this.profile = profile;
  180. setConnectionDetails(uri, profile);
  181. updateIcon();
  182. windowModel.getConfigManager().addChangeListener("formatter", "serverName", configListener);
  183. windowModel.getConfigManager().addChangeListener("formatter", "serverTitle", configListener);
  184. highlightManager = new HighlightManager(windowModel);
  185. windowModel.getEventBus().subscribe(highlightManager);
  186. windowModel.getEventBus().subscribe(groupChatManager);
  187. windowModel.getEventBus().subscribe(this);
  188. }
  189. /**
  190. * Updates the connection details for this server.
  191. *
  192. * @param uri The new URI that this server should connect to
  193. * @param profile The profile that this server should use
  194. */
  195. private void setConnectionDetails(final URI uri, final Profile profile) {
  196. this.address = checkNotNull(uri);
  197. this.protocolDescription = Optional.ofNullable(parserFactory.getDescription(uri));
  198. this.profile = profile;
  199. }
  200. @Override
  201. public void connect() {
  202. connect(address, profile);
  203. }
  204. @Override
  205. @Precondition({
  206. "The current parser is null or not connected",
  207. "The specified profile is not null"
  208. })
  209. @SuppressWarnings("fallthrough")
  210. public void connect(final URI address, final Profile profile) {
  211. checkNotNull(address);
  212. checkNotNull(profile);
  213. synchronized (myStateLock) {
  214. LOG.info("Connecting to {}, current state is {}", address, myState.getState());
  215. switch (myState.getState()) {
  216. case RECONNECT_WAIT:
  217. LOG.debug("Cancelling reconnection timer");
  218. if (reconnectTimerFuture != null) {
  219. reconnectTimerFuture.cancel(false);
  220. }
  221. break;
  222. case CLOSING:
  223. // Ignore the connection attempt
  224. return;
  225. case CONNECTED:
  226. case CONNECTING:
  227. disconnect(windowModel.getConfigManager()
  228. .getOption(DOMAIN_GENERAL, "quitmessage"));
  229. case DISCONNECTING:
  230. while (!myState.getState().isDisconnected()) {
  231. try {
  232. myStateLock.wait();
  233. } catch (InterruptedException ex) {
  234. return;
  235. }
  236. }
  237. break;
  238. default:
  239. // Do nothing
  240. break;
  241. }
  242. final URI connectAddress;
  243. final Parser newParser;
  244. try {
  245. parserLock.writeLock().lock();
  246. if (parser.isPresent()) {
  247. throw new IllegalArgumentException("Connection attempt while parser "
  248. + "is still connected.\n\nMy state:" + getState());
  249. }
  250. configMigrator.migrate(address.getScheme(), "", "", address.getHost());
  251. setConnectionDetails(address, profile);
  252. updateTitle();
  253. updateIcon();
  254. parser = Optional.ofNullable(buildParser());
  255. if (!parser.isPresent()) {
  256. windowModel.getEventBus().publishAsync(
  257. new ServerUnknownProtocolEvent(this, address.getScheme()));
  258. return;
  259. }
  260. newParser = parser.get();
  261. connectAddress = newParser.getURI();
  262. } finally {
  263. parserLock.writeLock().unlock();
  264. }
  265. myState.transition(ServerState.CONNECTING);
  266. doCallbacks();
  267. updateAwayState(Optional.empty());
  268. inviteManager.removeInvites();
  269. newParser.connect();
  270. if (newParser instanceof ThreadedParser) {
  271. ((ThreadedParser) newParser).getControlThread()
  272. .setName("Parser - " + connectAddress.getHost());
  273. }
  274. }
  275. windowModel.getEventBus().publish(new ServerConnectingEvent(this, address));
  276. }
  277. @Override
  278. public void reconnect(final String reason) {
  279. synchronized (myStateLock) {
  280. if (myState.getState() == ServerState.CLOSING) {
  281. return;
  282. }
  283. disconnect(reason);
  284. connect(address, profile);
  285. }
  286. }
  287. @Override
  288. public void reconnect() {
  289. reconnect(windowModel.getConfigManager().getOption(DOMAIN_GENERAL, "reconnectmessage"));
  290. }
  291. @Override
  292. public void disconnect() {
  293. disconnect(windowModel.getConfigManager().getOption(DOMAIN_GENERAL, "quitmessage"));
  294. }
  295. @Override
  296. public void disconnect(final String reason) {
  297. synchronized (myStateLock) {
  298. LOG.info("Disconnecting. Current state: {}", myState.getState());
  299. switch (myState.getState()) {
  300. case CLOSING:
  301. case DISCONNECTING:
  302. case DISCONNECTED:
  303. case TRANSIENTLY_DISCONNECTED:
  304. return;
  305. case RECONNECT_WAIT:
  306. LOG.debug("Cancelling reconnection timer");
  307. if (reconnectTimerFuture != null) {
  308. reconnectTimerFuture.cancel(false);
  309. }
  310. break;
  311. default:
  312. break;
  313. }
  314. groupChatManager.handleDisconnect();
  315. try {
  316. parserLock.readLock().lock();
  317. if (parser.isPresent()) {
  318. myState.transition(ServerState.DISCONNECTING);
  319. inviteManager.removeInvites();
  320. updateIcon();
  321. parser.get().disconnect(reason);
  322. } else {
  323. myState.transition(ServerState.DISCONNECTED);
  324. }
  325. } finally {
  326. parserLock.readLock().unlock();
  327. }
  328. if (windowModel.getConfigManager()
  329. .getOptionBool(DOMAIN_GENERAL, "closequeriesonquit")) {
  330. closeQueries();
  331. }
  332. }
  333. }
  334. /**
  335. * Schedules a reconnect attempt to be performed after a user-defined delay.
  336. */
  337. @Precondition("The server state is transiently disconnected")
  338. private void doDelayedReconnect() {
  339. synchronized (myStateLock) {
  340. LOG.info("Performing delayed reconnect. State: {}", myState.getState());
  341. if (myState.getState() != ServerState.TRANSIENTLY_DISCONNECTED) {
  342. throw new IllegalStateException("doDelayedReconnect when not "
  343. + "transiently disconnected\n\nState: " + myState);
  344. }
  345. final int delay = Math.max(1000,
  346. windowModel.getConfigManager().getOptionInt(DOMAIN_GENERAL, "reconnectdelay"));
  347. windowModel.getEventBus().publishAsync(
  348. new ServerReconnectScheduledEvent(this, delay / 1000));
  349. reconnectTimerFuture = executorService.schedule(() -> {
  350. synchronized (myStateLock) {
  351. LOG.debug("Reconnect task executing, state: {}", myState.getState());
  352. if (myState.getState() == ServerState.RECONNECT_WAIT) {
  353. myState.transition(ServerState.TRANSIENTLY_DISCONNECTED);
  354. reconnect();
  355. }
  356. }
  357. }, delay, TimeUnit.MILLISECONDS);
  358. LOG.info("Scheduling reconnect task for delay of {}", delay);
  359. myState.transition(ServerState.RECONNECT_WAIT);
  360. updateIcon();
  361. }
  362. }
  363. @Override
  364. public boolean hasQuery(final String host) {
  365. return queries.containsKey(converter.toLowerCase(getUser(host).getNickname()));
  366. }
  367. @Override
  368. public Optional<User> getLocalUser() {
  369. return parser.map(Parser::getLocalClient)
  370. .map(client -> userManager.getUserFromClientInfo(client, this));
  371. }
  372. @Override
  373. public User getUser(final String details) {
  374. return parser.map(p -> p.getClient(details))
  375. .map(client -> userManager.getUserFromClientInfo(client, this)).get();
  376. }
  377. @Override
  378. public PrivateChat getQuery(final String host) {
  379. return getQuery(host, false);
  380. }
  381. @Override
  382. public PrivateChat getQuery(final String host, final boolean focus) {
  383. synchronized (myStateLock) {
  384. if (myState.getState() == ServerState.CLOSING) {
  385. // Can't open queries while the server is closing
  386. return null;
  387. }
  388. }
  389. final String nick = getUser(host).getNickname();
  390. final String lnick = converter.toLowerCase(nick);
  391. if (!queries.containsKey(lnick)) {
  392. final Query newQuery = queryFactory.getQuery(this, getUser(host));
  393. if (!getState().isDisconnected()) {
  394. newQuery.reregister();
  395. }
  396. windowModel.getInputModel().get().getTabCompleter()
  397. .addEntry(TabCompletionType.QUERY_NICK, nick);
  398. queries.put(lnick, newQuery);
  399. }
  400. return queries.get(lnick);
  401. }
  402. /**
  403. * Updates tab completer and queries after a user changes their nickname.
  404. *
  405. * @param client The client that changed nickname
  406. * @param oldNick The old nickname they used.
  407. */
  408. void handleNickChange(final ClientInfo client, final String oldNick) {
  409. if (queries.containsKey(converter.toLowerCase(oldNick))) {
  410. windowModel.getInputModel().get().getTabCompleter()
  411. .removeEntry(TabCompletionType.QUERY_NICK, oldNick);
  412. windowModel.getInputModel().get().getTabCompleter()
  413. .addEntry(TabCompletionType.QUERY_NICK, client.getNickname());
  414. queries.put(
  415. converter.toLowerCase(client.getNickname()),
  416. queries.remove(converter.toLowerCase(oldNick)));
  417. }
  418. }
  419. @Override
  420. public Collection<PrivateChat> getQueries() {
  421. return Collections.unmodifiableCollection(queries.values());
  422. }
  423. @Override
  424. public void delQuery(final PrivateChat query) {
  425. windowModel.getInputModel().get().getTabCompleter().removeEntry(
  426. TabCompletionType.QUERY_NICK, query.getNickname());
  427. queries.remove(converter.toLowerCase(query.getNickname()));
  428. }
  429. /**
  430. * Closes all open query windows associated with this server.
  431. */
  432. private void closeQueries() {
  433. new ArrayList<>(queries.values()).forEach(Query::close);
  434. }
  435. /**
  436. * Builds an appropriately configured {@link Parser} for this server.
  437. *
  438. * @return A configured parser.
  439. */
  440. @Nullable
  441. private Parser buildParser() {
  442. final Parser myParser = parserFactory
  443. .getParser(profile, address, windowModel.getConfigManager())
  444. .orElse(null);
  445. if (myParser != null) {
  446. myParser.setIgnoreList(ignoreList);
  447. }
  448. if (myParser instanceof SecureParser) {
  449. final CertificateManager certificateManager =
  450. new CertificateManager(this, address.getHost(), windowModel.getConfigManager(),
  451. userSettings, windowModel.getEventBus());
  452. final SecureParser secureParser = (SecureParser) myParser;
  453. secureParser.setTrustManagers(certificateManager);
  454. secureParser.setKeyManagers(certificateManager.getKeyManager());
  455. }
  456. if (myParser instanceof EncodingParser) {
  457. final EncodingParser encodingParser = (EncodingParser) myParser;
  458. encodingParser.setEncoder(messageEncoderFactory.getMessageEncoder(this, myParser));
  459. }
  460. return myParser;
  461. }
  462. @Override
  463. public boolean compareURI(final URI uri) {
  464. return parser.map(p -> p.compareURI(uri)).orElse(
  465. oldParser.map(op -> op.compareURI(uri)).orElse(false));
  466. }
  467. /**
  468. * Updates this server's icon.
  469. */
  470. private void updateIcon() {
  471. final String icon = myState.getState() == ServerState.CONNECTED
  472. ? protocolDescription.get().isSecure(address)
  473. ? "secure-server" : "server" : "server-disconnected";
  474. windowModel.setIcon(icon);
  475. }
  476. /**
  477. * Registers callbacks.
  478. */
  479. private void doCallbacks() {
  480. eventHandler.registerCallbacks();
  481. queries.values().forEach(Query::reregister);
  482. }
  483. @Override
  484. public void sendLine(final String line) {
  485. synchronized (myStateLock) {
  486. try {
  487. parserLock.readLock().lock();
  488. parser.ifPresent(p -> {
  489. if (!line.isEmpty() && myState.getState() == ServerState.CONNECTED) {
  490. p.sendRawMessage(line);
  491. }
  492. });
  493. } finally {
  494. parserLock.readLock().unlock();
  495. }
  496. }
  497. }
  498. @Override
  499. public void sendMessage(final String target, final String message) {
  500. if (!message.isEmpty()) {
  501. parser.ifPresent(p -> p.sendMessage(target, message));
  502. }
  503. }
  504. public int getMaxLineLength() {
  505. return withParserReadLock(Parser::getMaxLength, -1);
  506. }
  507. @Override
  508. @Nonnull
  509. public Optional<Parser> getParser() {
  510. return parser;
  511. }
  512. @Nonnull
  513. @Override
  514. public Profile getProfile() {
  515. return profile;
  516. }
  517. @Override
  518. public String getAddress() {
  519. return withParserReadLock(Parser::getServerName, address.getHost());
  520. }
  521. @Override
  522. public String getNetwork() {
  523. try {
  524. parserLock.readLock().lock();
  525. return parser.map(p -> p.getNetworkName().isEmpty()
  526. ? getNetworkFromServerName(p.getServerName()) : p.getNetworkName())
  527. .orElseThrow(() -> new IllegalStateException(
  528. "getNetwork called when parser is null (state: " + getState() + ')'));
  529. } finally {
  530. parserLock.readLock().unlock();
  531. }
  532. }
  533. @Override
  534. public boolean isNetwork(final String target) {
  535. synchronized (myStateLock) {
  536. return withParserReadLock(p -> getNetwork().equalsIgnoreCase(target), false);
  537. }
  538. }
  539. /**
  540. * Calculates a network name from the specified server name.
  541. *
  542. * @param serverName The server name to parse
  543. * @return A network name for the specified server
  544. */
  545. protected static String getNetworkFromServerName(final String serverName) {
  546. try {
  547. return InternetDomainName.from(serverName).topPrivateDomain().toString();
  548. } catch (IllegalArgumentException | IllegalStateException ex) {
  549. // Either couldn't parse it as a domain name, or it didn't have a public suffix.
  550. // Just use the server name as-is.
  551. return serverName;
  552. }
  553. }
  554. @Override
  555. public String getIrcd() {
  556. return parser.get().getServerSoftwareType();
  557. }
  558. @Override
  559. public String getProtocol() {
  560. return address.getScheme();
  561. }
  562. @Override
  563. public boolean isAway() {
  564. return awayMessage.isPresent();
  565. }
  566. @Override
  567. public String getAwayMessage() {
  568. return awayMessage.orElse(null);
  569. }
  570. @Override
  571. public ServerState getState() {
  572. return myState.getState();
  573. }
  574. public ServerStatus getStatus() {
  575. return myState;
  576. }
  577. @Handler
  578. private void handleClose(final FrameClosingEvent event) {
  579. if (event.getSource().equals(windowModel)) {
  580. synchronized (myStateLock) {
  581. eventHandler.unregisterCallbacks();
  582. windowModel.getConfigManager().removeListener(configListener);
  583. windowModel.getEventBus().unsubscribe(groupChatManager);
  584. windowModel.getEventBus().unsubscribe(highlightManager);
  585. executorService.shutdown();
  586. if (parser.isPresent()) {
  587. parser.get().shutdown();
  588. }
  589. disconnect();
  590. myState.transition(ServerState.CLOSING);
  591. }
  592. groupChatManager.closeAll();
  593. closeQueries();
  594. inviteManager.removeInvites();
  595. windowModel.getEventBus().unsubscribe(this);
  596. }
  597. }
  598. @Override
  599. public WindowModel getWindowModel() {
  600. return windowModel;
  601. }
  602. @Override
  603. public void sendCTCPReply(final String source, final String type, final String args) {
  604. if ("VERSION".equalsIgnoreCase(type)) {
  605. parser.get().sendCTCPReply(source, "VERSION",
  606. "DMDirc " + windowModel.getConfigManager().getOption("version", "version") +
  607. " - https://www.dmdirc.com/");
  608. } else if ("PING".equalsIgnoreCase(type)) {
  609. parser.get().sendCTCPReply(source, "PING", args);
  610. } else if ("CLIENTINFO".equalsIgnoreCase(type)) {
  611. parser.get().sendCTCPReply(source, "CLIENTINFO", "VERSION PING CLIENTINFO");
  612. }
  613. }
  614. @Override
  615. public void updateTitle() {
  616. synchronized (myStateLock) {
  617. if (myState.getState() == ServerState.CLOSING) {
  618. return;
  619. }
  620. try {
  621. parserLock.readLock().lock();
  622. final Object[] arguments = {
  623. address.getHost(), parser.map(Parser::getServerName).orElse("Unknown"),
  624. address.getPort(), parser.map(p -> getNetwork()).orElse("Unknown"),
  625. getLocalUser().map(User::getNickname).orElse("Unknown")
  626. };
  627. windowModel.setName(Formatter.formatMessage(windowModel.getConfigManager(),
  628. "serverName", arguments));
  629. windowModel.setTitle(Formatter.formatMessage(windowModel.getConfigManager(),
  630. "serverTitle", arguments));
  631. } finally {
  632. parserLock.readLock().unlock();
  633. }
  634. }
  635. }
  636. /**
  637. * Called when the socket has been closed.
  638. */
  639. public void onSocketClosed() {
  640. LOG.info("Received socket closed event, state: {}", myState.getState());
  641. if (Thread.holdsLock(myStateLock)) {
  642. LOG.info("State lock contended: rerunning on a new thread");
  643. executorService.schedule(this::onSocketClosed, 0, TimeUnit.SECONDS);
  644. return;
  645. }
  646. windowModel.getEventBus().publish(new ServerDisconnectedEvent(this));
  647. eventHandler.unregisterCallbacks();
  648. synchronized (myStateLock) {
  649. if (myState.getState() == ServerState.CLOSING
  650. || myState.getState() == ServerState.DISCONNECTED) {
  651. // This has been triggered via .disconnect()
  652. return;
  653. }
  654. if (myState.getState() == ServerState.DISCONNECTING) {
  655. myState.transition(ServerState.DISCONNECTED);
  656. } else {
  657. myState.transition(ServerState.TRANSIENTLY_DISCONNECTED);
  658. }
  659. groupChatManager.handleSocketClosed();
  660. try {
  661. parserLock.writeLock().lock();
  662. oldParser = parser;
  663. if (oldParser.isPresent()) {
  664. oldParser.get().shutdown();
  665. }
  666. parser = Optional.empty();
  667. } finally {
  668. parserLock.writeLock().unlock();
  669. }
  670. updateIcon();
  671. if (windowModel.getConfigManager()
  672. .getOptionBool(DOMAIN_GENERAL, "closequeriesondisconnect")) {
  673. closeQueries();
  674. }
  675. inviteManager.removeInvites();
  676. updateAwayState(Optional.empty());
  677. if (windowModel.getConfigManager()
  678. .getOptionBool(DOMAIN_GENERAL, "reconnectondisconnect")
  679. && myState.getState() == ServerState.TRANSIENTLY_DISCONNECTED) {
  680. doDelayedReconnect();
  681. }
  682. }
  683. }
  684. /**
  685. * Called when an error was encountered while connecting.
  686. *
  687. * @param errorInfo The parser's error information
  688. */
  689. @Precondition("The current server state is CONNECTING")
  690. public void onConnectError(final ParserError errorInfo) {
  691. synchronized (myStateLock) {
  692. LOG.info("Received connect error event, state: {}; error: {}", myState.getState(),
  693. errorInfo);
  694. if (myState.getState() == ServerState.CLOSING
  695. || myState.getState() == ServerState.DISCONNECTING) {
  696. // Do nothing
  697. return;
  698. } else if (myState.getState() != ServerState.CONNECTING) {
  699. // Shouldn't happen
  700. throw new IllegalStateException("Connect error when not "
  701. + "connecting\n\n" + getStatus().getTransitionHistory());
  702. }
  703. myState.transition(ServerState.TRANSIENTLY_DISCONNECTED);
  704. try {
  705. parserLock.writeLock().lock();
  706. oldParser = parser;
  707. if (oldParser.isPresent()) {
  708. oldParser.get().shutdown();
  709. }
  710. parser = Optional.empty();
  711. } finally {
  712. parserLock.writeLock().unlock();
  713. }
  714. updateIcon();
  715. windowModel.getEventBus().publish(new ServerConnectErrorEvent(this,
  716. getErrorDescription(errorInfo)));
  717. if (windowModel.getConfigManager()
  718. .getOptionBool(DOMAIN_GENERAL, "reconnectonconnectfailure")) {
  719. doDelayedReconnect();
  720. }
  721. }
  722. }
  723. /**
  724. * Gets a user-readable description of the specified error.
  725. *
  726. * @param errorInfo The parser error to get a description for.
  727. * @return A user-readable error description.
  728. */
  729. private static String getErrorDescription(final ParserError errorInfo) {
  730. final String description;
  731. if (errorInfo.getException() == null) {
  732. description = errorInfo.getData();
  733. } else {
  734. final Exception exception = errorInfo.getException();
  735. if (exception instanceof UnknownHostException) {
  736. description = "Unknown host (unable to resolve)";
  737. } else if (exception instanceof NoRouteToHostException) {
  738. description = "No route to host";
  739. } else if (exception instanceof SocketTimeoutException) {
  740. description = "Connection attempt timed out";
  741. } else if (exception instanceof SocketException
  742. || exception instanceof SSLException) {
  743. description = exception.getMessage();
  744. } else {
  745. LOG.info(APP_ERROR, "Unknown socket error: {}",
  746. exception.getClass().getCanonicalName(), exception);
  747. description = "Unknown error: " + exception.getMessage();
  748. }
  749. }
  750. return description;
  751. }
  752. /**
  753. * Called after the parser receives the 005 headers from the server.
  754. */
  755. @Precondition("State is CONNECTING")
  756. public void onPost005() {
  757. synchronized (myStateLock) {
  758. if (myState.getState() != ServerState.CONNECTING) {
  759. // Shouldn't happen
  760. throw new IllegalStateException("Received onPost005 while not "
  761. + "connecting\n\n" + myState.getTransitionHistory());
  762. }
  763. myState.transition(ServerState.CONNECTED);
  764. configMigrator.migrate(address.getScheme(),
  765. parser.get().getServerSoftwareType(), getNetwork(), parser.get().getServerName());
  766. updateIcon();
  767. updateTitle();
  768. updateIgnoreList();
  769. converter = parser.get().getStringConverter();
  770. groupChatManager.handleConnected();
  771. }
  772. windowModel.getEventBus().publish(new ServerConnectedEvent(this));
  773. }
  774. @Override
  775. public IgnoreList getIgnoreList() {
  776. return ignoreList;
  777. }
  778. @Override
  779. public void updateIgnoreList() {
  780. ignoreList.clear();
  781. ignoreList.addAll(windowModel.getConfigManager().getOptionList("network", "ignorelist"));
  782. }
  783. @Override
  784. public void saveIgnoreList() {
  785. getNetworkIdentity().setOption("network", "ignorelist", ignoreList.getRegexList());
  786. }
  787. @Override
  788. public ConfigProvider getServerIdentity() {
  789. return identityFactory.createServerConfig(parser.get().getServerName());
  790. }
  791. @Override
  792. public ConfigProvider getNetworkIdentity() {
  793. return identityFactory.createNetworkConfig(getNetwork());
  794. }
  795. @Override
  796. public void updateAwayState(final Optional<String> message) {
  797. checkNotNull(message);
  798. if (awayMessage.equals(message)) {
  799. return;
  800. }
  801. awayMessage = message;
  802. }
  803. @Override
  804. public String getUserModes() {
  805. return getParser().map(Parser::getChannelUserModes).orElse("");
  806. }
  807. @Override
  808. public String getBooleanModes() {
  809. return getParser().map(Parser::getBooleanChannelModes).orElse("");
  810. }
  811. @Override
  812. public String getListModes() {
  813. return getParser().map(Parser::getListChannelModes).orElse("");
  814. }
  815. @Override
  816. public String getParameterModes() {
  817. return getParser().map(Parser::getParameterChannelModes).orElse("");
  818. }
  819. @Override
  820. public String getDoubleParameterModes() {
  821. return getParser().map(Parser::getDoubleParameterChannelModes).orElse("");
  822. }
  823. @Override
  824. public int getMaxListModes(final char mode) {
  825. return getParser().map(p -> p.getMaxListModes(mode)).orElse(-1);
  826. }
  827. @Override
  828. public GroupChatManager getGroupChatManager() {
  829. return groupChatManager;
  830. }
  831. @Override
  832. public InviteManager getInviteManager() {
  833. return inviteManager;
  834. }
  835. @Override
  836. public void setNickname(final String nickname) {
  837. parser.map(Parser::getLocalClient).ifPresent(c -> c.setNickname(nickname));
  838. }
  839. @Override
  840. public Optional<String> getNickname() {
  841. return parser.map(Parser::getLocalClient).map(ClientInfo::getNickname);
  842. }
  843. @Override
  844. public void requestUserInfo(final User user) {
  845. parser.ifPresent(p -> p.sendWhois(user.getNickname()));
  846. }
  847. /**
  848. * Utility method to get a result from the parser while holding the {@link #parserLock}
  849. * read lock.
  850. *
  851. * @param func The function to use to retrieve information from the parser.
  852. * @param orElse The value to return if the parser is otherwise not present.
  853. * @param <T> The type of result returned.
  854. * @return The value returned by {@code func}, if the parser is present, otherwise the
  855. * {@code orElse} value.
  856. */
  857. private <T> T withParserReadLock(
  858. final Function<Parser, T> func,
  859. final T orElse) {
  860. try {
  861. parserLock.readLock().lock();
  862. return parser.map(func).orElse(orElse);
  863. } finally {
  864. parserLock.readLock().unlock();
  865. }
  866. }
  867. }