/*
* 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.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 {
User user = clients.stream().filter(c -> c.session.equals(session)).map(c -> c.visitor)
.findFirst().orElse(AnonymousUser.INSTANCE);
String command = message.getPayload().trim();
if (StringUtils.isNotEmpty(command)) {
CommandResult result = commandsManager.processCommand(user, command, URI.create(""));
session.sendMessage(new TextMessage(result.getText()));
}
}
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;
}
}
}