/* * Copyright (C) 2008-2017, Juick * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package com.juick.server; import com.fasterxml.jackson.databind.ObjectMapper; import com.juick.User; import com.juick.server.helpers.AnonymousUser; import com.juick.server.helpers.CommandResult; import com.juick.server.util.HttpBadRequestException; import com.juick.server.util.HttpForbiddenException; import com.juick.server.util.HttpNotFoundException; import com.juick.service.MessagesService; import com.juick.service.UserService; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.math.NumberUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.PingMessage; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.handler.ConcurrentWebSocketSessionDecorator; import org.springframework.web.socket.handler.TextWebSocketHandler; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; import javax.inject.Inject; import java.io.IOException; import java.net.URI; import java.time.Instant; import java.util.Collections; import java.util.LinkedList; import java.util.List; /** * Created by vitalyster on 28.06.2016. */ @Component public class WebsocketManager extends TextWebSocketHandler { private static final Logger logger = LoggerFactory.getLogger(WebsocketManager.class); private final List clients = Collections.synchronizedList(new LinkedList<>()); @Inject private UserService userService; @Inject private MessagesService messagesService; @Value("${service_user:juick}") private String serviceUser; @Inject private ObjectMapper jsonMapper; @Inject private CommandsManager commandsManager; @Override public void afterConnectionEstablished(WebSocketSession session) { URI hLocation; String hXRealIP; hLocation = session.getUri(); HttpHeaders headers = session.getHandshakeHeaders(); hXRealIP = headers.getOrDefault("X-Real-IP", Collections.singletonList(session.getRemoteAddress().toString())).get(0); // Auth User visitor = AnonymousUser.INSTANCE; UriComponents uriComponents = UriComponentsBuilder.fromUri(hLocation).build(); List hash = uriComponents.getQueryParams().get("hash"); if (hash != null && hash.get(0).length() == 16) { visitor = userService.getUserByHash(hash.get(0)); } else { logger.debug("wrong hash for {} from {}", visitor.getUid(), hXRealIP); } int MID = 0; SocketSubscribed sockSubscr = null; if (hLocation.getPath().equals("/ws/")) { logger.debug("user {} connected", visitor.getUid()); sockSubscr = new SocketSubscribed(session, hXRealIP, visitor, false); } else if (hLocation.getPath().equals("/ws/_all")) { logger.debug("user {} connected to legacy _all ({})", visitor.getUid(), hLocation.getPath()); sockSubscr = new SocketSubscribed(session, hXRealIP, visitor, true); sockSubscr.allMessages = true; } else if (hLocation.getPath().equals("/ws/_replies")) { logger.debug("user {} connected to legacy _replies ({})", visitor.getUid(), hLocation.getPath()); sockSubscr = new SocketSubscribed(session, hXRealIP, visitor, true); sockSubscr.allReplies = true; } else if (hLocation.getPath().matches("^/ws/(\\d)+$")) { MID = NumberUtils.toInt(hLocation.getPath().substring(4), 0); if (MID > 0) { if (messagesService.canViewThread(MID, visitor.getUid())) { logger.debug("user {} connected to legacy thread ({}) from {}", visitor.getUid(), MID, hXRealIP); sockSubscr = new SocketSubscribed(session, hXRealIP, visitor, true); sockSubscr.MID = MID; } else { throw new HttpForbiddenException(); } } } else { throw new HttpNotFoundException(); } if (sockSubscr != null) { synchronized (clients) { clients.add(sockSubscr); logger.debug("{} clients connected", clients.size()); } } else { throw new HttpBadRequestException(); } } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { synchronized (clients) { logger.debug("session closed with status {}: {}", status.getCode(), status.getReason()); clients.removeIf(c -> c.session.getDelegate().getId().equals(session.getId())); logger.debug("{} clients connected", clients.size()); } } @Scheduled(fixedRate = 30000) public void ping() { clients.forEach(c -> { try { if (c.session.isOpen()) { c.session.sendMessage(new PingMessage()); } } catch (IOException e) { logger.error("WebSocket PING exception", e); } }); } @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { SocketSubscribed ws = clients.stream().filter(c -> c.session.getDelegate().equals(session)) .findFirst().orElseThrow(IllegalStateException::new); if (!ws.visitor.isAnonymous()) { String command = message.getPayload().trim(); if (StringUtils.isNotEmpty(command)) { CommandResult result = commandsManager.processCommand(ws.visitor, command, URI.create("")); ws.session.sendMessage(new TextMessage(result.getText())); } } else { ws.session.sendMessage(new TextMessage("Authorization required")); } } public List getClients() { return clients; } class SocketSubscribed { ConcurrentWebSocketSessionDecorator session; String clientName; User visitor; int MID; boolean allMessages; boolean allReplies; Instant tsConnected; Instant tsLastData; boolean legacy; public SocketSubscribed(WebSocketSession session, String clientName, User visitor, boolean legacy) { this.session = new ConcurrentWebSocketSessionDecorator(session, 60000, 65536); this.clientName = clientName; this.visitor = visitor; tsConnected = tsLastData = Instant.now(); this.legacy = legacy; } } }