/**
* Copyright (c) 2000-present Liferay, Inc. All rights reserved.
*
* This library is free software; you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; either version 2.1 of the License, or (at your option)
* any later version.
*
* This library 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 Lesser General Public License for more
* details.
*/
package com.liferay.sync.engine.lan.server.file;
import static io.netty.handler.codec.http.HttpMethod.GET;
import static io.netty.handler.codec.http.HttpMethod.HEAD;
import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND;
import static io.netty.handler.codec.http.HttpResponseStatus.OK;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
import com.liferay.sync.engine.lan.util.LanTokenUtil;
import com.liferay.sync.engine.model.SyncAccount;
import com.liferay.sync.engine.model.SyncFile;
import com.liferay.sync.engine.service.SyncAccountService;
import com.liferay.sync.engine.service.SyncFileService;
import com.liferay.sync.engine.util.GetterUtil;
import com.liferay.sync.engine.util.OSDetector;
import com.liferay.sync.engine.util.PropsValues;
import com.liferay.sync.engine.util.Validator;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.DecoderResult;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.DefaultHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpChunkedInput;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.traffic.TrafficCounter;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.FileTime;
import java.util.List;
import javax.activation.MimetypesFileTypeMap;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Dennis Ju
*/
public class LanFileServerHandler
extends SimpleChannelInboundHandler<FullHttpRequest> {
public LanFileServerHandler(
SyncTrafficShapingHandler syncTrafficShapingHandler) {
_syncTrafficShapingHandler = syncTrafficShapingHandler;
_trafficCounter = _syncTrafficShapingHandler.trafficCounter();
}
@Override
public void channelRead0(
ChannelHandlerContext channelHandlerContext,
FullHttpRequest fullHttpRequest)
throws Exception {
DecoderResult decoderResult = fullHttpRequest.decoderResult();
if (!decoderResult.isSuccess()) {
_sendError(channelHandlerContext, BAD_REQUEST);
return;
}
if (fullHttpRequest.method() == GET) {
processGetRequest(channelHandlerContext, fullHttpRequest);
}
else if (fullHttpRequest.method() == HEAD) {
processHeadRequest(channelHandlerContext, fullHttpRequest);
}
else {
_sendError(channelHandlerContext, BAD_REQUEST);
}
}
@Override
public void exceptionCaught(
ChannelHandlerContext channelHandlerContext, Throwable exception) {
String message = exception.getMessage();
Channel channel = channelHandlerContext.channel();
if (!message.startsWith("An established connection was aborted") &&
!message.startsWith(
"An existing connectionn was forcibly closed") &&
!message.startsWith("Connection reset by peer")) {
_logger.error(
"Client {}: {}", channel.remoteAddress(),
exception.getMessage(), exception);
}
channel.close();
}
protected void processGetRequest(
ChannelHandlerContext channelHandlerContext,
FullHttpRequest fullHttpRequest)
throws Exception {
if (_logger.isTraceEnabled()) {
Channel channel = channelHandlerContext.channel();
_logger.trace(
"Client {}: processing get request {}", channel.remoteAddress(),
fullHttpRequest.uri());
}
HttpHeaders requestHttpHeaders = fullHttpRequest.headers();
String lanToken = requestHttpHeaders.get("lanToken");
if (Validator.isBlank(lanToken)) {
Channel channel = channelHandlerContext.channel();
_logger.error(
"Client {}: did not send token", channel.remoteAddress());
_sendError(channelHandlerContext, NOT_FOUND);
return;
}
if (!LanTokenUtil.containsLanToken(lanToken)) {
Channel channel = channelHandlerContext.channel();
_logger.error(
"Client {}: token not found or expired",
channel.remoteAddress());
_sendError(channelHandlerContext, NOT_FOUND);
return;
}
SyncFile syncFile = _getSyncFile(fullHttpRequest);
if (syncFile == null) {
if (_logger.isTraceEnabled()) {
Channel channel = channelHandlerContext.channel();
_logger.trace(
"Client {}: SyncFile not found. uri: {}",
channel.remoteAddress(), fullHttpRequest.uri());
}
_sendError(channelHandlerContext, NOT_FOUND);
return;
}
if (_syncTrafficShapingHandler.getConnectionsCount() >=
PropsValues.SYNC_LAN_SERVER_MAX_CONNECTIONS) {
_sendError(channelHandlerContext, NOT_FOUND);
return;
}
_syncTrafficShapingHandler.incrementConnectionsCount();
try {
sendFile(channelHandlerContext, fullHttpRequest, syncFile);
}
catch (Exception e) {
_syncTrafficShapingHandler.decrementConnectionsCount();
throw e;
}
LanTokenUtil.removeLanToken(lanToken);
}
protected void processHeadRequest(
ChannelHandlerContext channelHandlerContext,
FullHttpRequest fullHttpRequest) {
if (_logger.isTraceEnabled()) {
Channel channel = channelHandlerContext.channel();
_logger.trace(
"Client {}: processing head request {}",
channel.remoteAddress(), fullHttpRequest.uri());
}
SyncFile syncFile = _getSyncFile(fullHttpRequest);
if (syncFile == null) {
_sendError(channelHandlerContext, NOT_FOUND);
return;
}
String lanTokenKey = syncFile.getLanTokenKey();
if ((lanTokenKey == null) || lanTokenKey.isEmpty()) {
_sendError(channelHandlerContext, NOT_FOUND);
return;
}
String encryptedToken = null;
try {
encryptedToken = LanTokenUtil.createEncryptedToken(lanTokenKey);
}
catch (Exception e) {
_sendError(channelHandlerContext, NOT_FOUND);
return;
}
HttpResponse httpResponse = new DefaultFullHttpResponse(HTTP_1_1, OK);
HttpHeaders httpHeaders = httpResponse.headers();
httpHeaders.set(
"connectionsCount",
_syncTrafficShapingHandler.getConnectionsCount());
httpHeaders.set("downloadRate", _trafficCounter.lastWrittenBytes());
httpHeaders.set("encryptedToken", encryptedToken);
httpHeaders.set(
"maxConnections", PropsValues.SYNC_LAN_SERVER_MAX_CONNECTIONS);
channelHandlerContext.writeAndFlush(httpResponse);
}
protected void sendFile(
final ChannelHandlerContext channelHandlerContext,
FullHttpRequest fullHttpRequest, SyncFile syncFile)
throws Exception {
Path path = Paths.get(syncFile.getFilePathName());
if (Files.notExists(path)) {
_syncTrafficShapingHandler.decrementConnectionsCount();
if (_logger.isTraceEnabled()) {
Channel channel = channelHandlerContext.channel();
_logger.trace(
"Client {}: file not found {}", channel.remoteAddress(),
path);
}
_sendError(channelHandlerContext, NOT_FOUND);
return;
}
if (_logger.isDebugEnabled()) {
Channel channel = channelHandlerContext.channel();
_logger.debug(
"Client {}: sending file {}", channel.remoteAddress(), path);
}
long modifiedTime = syncFile.getModifiedTime();
long previousModifiedTime = syncFile.getPreviousModifiedTime();
if (OSDetector.isApple()) {
modifiedTime = modifiedTime / 1000 * 1000;
previousModifiedTime = previousModifiedTime / 1000 * 1000;
}
FileTime currentFileTime = Files.getLastModifiedTime(
path, LinkOption.NOFOLLOW_LINKS);
long currentTime = currentFileTime.toMillis();
if ((currentTime != modifiedTime) &&
(currentTime != previousModifiedTime)) {
_syncTrafficShapingHandler.decrementConnectionsCount();
Channel channel = channelHandlerContext.channel();
_logger.error(
"Client {}: file modified {}, currentTime {}, modifiedTime " +
"{}, previousModifiedTime {}",
channel.remoteAddress(), path, currentTime, modifiedTime,
previousModifiedTime);
_sendError(channelHandlerContext, NOT_FOUND);
return;
}
HttpResponse httpResponse = new DefaultHttpResponse(HTTP_1_1, OK);
long size = Files.size(path);
HttpUtil.setContentLength(httpResponse, size);
HttpHeaders httpHeaders = httpResponse.headers();
MimetypesFileTypeMap mimetypesFileTypeMap = new MimetypesFileTypeMap();
httpHeaders.set(
HttpHeaderNames.CONTENT_TYPE,
mimetypesFileTypeMap.getContentType(syncFile.getName()));
if (HttpUtil.isKeepAlive(fullHttpRequest)) {
httpHeaders.set(
HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
}
channelHandlerContext.write(httpResponse);
SyncChunkedFile syncChunkedFile = new SyncChunkedFile(
path, size, 4 * 1024 * 1024, currentTime);
ChannelFuture channelFuture = channelHandlerContext.writeAndFlush(
new HttpChunkedInput(syncChunkedFile),
channelHandlerContext.newProgressivePromise());
channelFuture.addListener(
new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture)
throws Exception {
_syncTrafficShapingHandler.decrementConnectionsCount();
if (channelFuture.isSuccess()) {
return;
}
Throwable exception = channelFuture.cause();
Channel channel = channelHandlerContext.channel();
_logger.error(
"Client {}: {}", channel.remoteAddress(),
exception.getMessage(), exception);
channelHandlerContext.close();
}
});
if (!HttpUtil.isKeepAlive(fullHttpRequest)) {
channelFuture.addListener(ChannelFutureListener.CLOSE);
}
}
private SyncFile _getSyncFile(FullHttpRequest fullHttpRequest) {
String[] pathArray = StringUtils.split(fullHttpRequest.uri(), "/");
if (pathArray.length != 4) {
return null;
}
String lanServerUuid = pathArray[0];
long repositoryId = GetterUtil.getLong(pathArray[1]);
long typePK = GetterUtil.getLong(pathArray[2]);
long versionId = GetterUtil.getLong(pathArray[3]);
if (lanServerUuid.isEmpty() || (repositoryId == 0) || (typePK == 0) ||
(versionId == 0)) {
return null;
}
List<SyncAccount> syncAccounts = SyncAccountService.findSyncAccounts(
lanServerUuid);
for (SyncAccount syncAccount : syncAccounts) {
SyncFile syncFile = SyncFileService.fetchSyncFile(
repositoryId, syncAccount.getSyncAccountId(), typePK,
versionId);
if ((syncFile != null) &&
(syncFile.getState() == SyncFile.STATE_SYNCED)) {
return syncFile;
}
}
return null;
}
private void _sendError(
ChannelHandlerContext channelHandlerContext,
HttpResponseStatus httpResponseStatus) {
ChannelFuture channelFuture = channelHandlerContext.writeAndFlush(
new DefaultFullHttpResponse(HTTP_1_1, httpResponseStatus));
channelFuture.addListener(ChannelFutureListener.CLOSE);
}
private static final Logger _logger = LoggerFactory.getLogger(
LanFileServerHandler.class);
private final SyncTrafficShapingHandler _syncTrafficShapingHandler;
private final TrafficCounter _trafficCounter;
}