package org.lognavigator.controller;
import static org.lognavigator.util.Constants.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.UnsupportedCharsetException;
import java.text.MessageFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import net.schmizz.sshj.common.IOUtils;
import org.lognavigator.bean.Breadcrumb;
import org.lognavigator.bean.CommandLine;
import org.lognavigator.bean.DisplayType;
import org.lognavigator.bean.FileInfo;
import org.lognavigator.bean.LogAccessConfig.LogAccessType;
import org.lognavigator.bean.TableCell;
import org.lognavigator.exception.AuthorizationException;
import org.lognavigator.exception.LogAccessException;
import org.lognavigator.service.ConfigService;
import org.lognavigator.service.LogAccessService;
import org.lognavigator.util.BreadcrumbFactory;
import org.lognavigator.util.CommandLineParser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.view.UrlBasedViewResolver;
@Controller
public class CommandController {
@Autowired
@Qualifier("facade")
LogAccessService logAccessService;
@Autowired
ConfigService configService;
@Autowired
ListController listController;
@RequestMapping("/logs/{logAccessConfigId}/command")
public String executeCommand(Model model,
HttpServletRequest request,
@PathVariable String logAccessConfigId,
@RequestParam(value="cmd", required=false, defaultValue=DEFAULT_LIST_COMMAND) String cmd,
@RequestParam(value="encoding", required=false) String encoding,
@RequestParam(value="displayType", required=false) DisplayType displayType
) throws AuthorizationException, LogAccessException, IOException {
// Parse command line
CommandLine commandLine = CommandLineParser.parseCommandLine(cmd);
// Is command forbidden ?
checkForbiddenCommand(commandLine);
// Forward to 'list' action, if command is 'ls'
if ((displayType == null || displayType == DisplayType.TABLE) && commandLine.getCommand().equals(DEFAULT_LIST_COMMAND) && !cmd.contains("|")) {
if (commandLine.hasParams()) {
return UrlBasedViewResolver.FORWARD_URL_PREFIX + FOLDER_VIEW_URL_PREFIX + commandLine.getParam(0);
}
else {
return UrlBasedViewResolver.FORWARD_URL_PREFIX + LOGS_LIST_URL;
}
}
// Define default encoding when not given by client
if (encoding == null) {
encoding = configService.getDefaultEncoding(logAccessConfigId);
}
// Define default displayType when not given by client
if (displayType == null) {
if (cmd.startsWith(TAR_GZ_FILE_VIEW_COMMAND_START) || cmd.endsWith(TAR_GZ_FILE_VIEW_COMMAND_END)) {
displayType = DisplayType.TABLE;
}
else {
displayType = DisplayType.RAW;
}
}
// Add options to model
request.setAttribute(SHOW_OPTIONS_KEY, true);
request.setAttribute(ENCODING_KEY, encoding);
request.setAttribute(DISPLAY_TYPE_KEY, displayType);
// Generate Breadcrumbs
generateBreadcrumbs(logAccessConfigId, commandLine, request);
// Execute the command
InputStream resultStream = logAccessService.executeCommand(logAccessConfigId, cmd);
BufferedReader resultReader = new BufferedReader(new InputStreamReader(resultStream, encoding));
// Process the result lines for raw display
if (displayType == DisplayType.RAW) {
model.addAttribute(RAW_CONTENT_KEY, resultReader);
return VIEW_RAW;
}
// Process the result lines for html table display
else {
try {
if (cmd.startsWith(TAR_GZ_FILE_VIEW_COMMAND_START) || cmd.endsWith(TAR_GZ_FILE_VIEW_COMMAND_END)) {
return processTarGzList(resultReader, model, cmd);
}
else {
processOtherCommand(resultReader, model);
}
}
finally {
IOUtils.closeQuietly(resultReader);
}
return VIEW_TABLE;
}
}
/**
* Checks that command line doesn't contain any forbidden command (for security reasons)
* @param commandLine command line to check
* @throws AuthorizationException if command line contains a forbidden command
*/
private void checkForbiddenCommand(CommandLine commandLine) throws AuthorizationException {
String forbiddenCommandsRegex = "(" + configService.getForbiddenCommands().replace(',', '|') + ")";
String forbiddenCommandLineRegex = MessageFormat.format(FORBIDDEN_COMMANDLINE_REGEX, forbiddenCommandsRegex);
if (commandLine.getLine().matches(forbiddenCommandLineRegex)
|| commandLine.getCommand().matches(forbiddenCommandsRegex)
|| commandLine.getCommand().matches(">|>>")
|| commandLine.getParams().contains(">") ||
commandLine.getParams().contains(">>")
) {
throw new AuthorizationException("This command is forbidden (" + configService.getForbiddenCommands() + ",>,>>)");
}
}
/**
* Display the result of a "tar.gz list" command as a convenient html table
* @return view name to display
*/
private String processTarGzList(BufferedReader resultReader, Model model, String cmd) throws IOException {
// Compute archive filename
Matcher matcher = Pattern.compile("[^ ]+\\.tar\\.gz").matcher(cmd);
matcher.find();
String targzFileName = matcher.group();
Set<FileInfo> archiveEntryList = new TreeSet<FileInfo>();
// Compute archive contents list
StringBuilder potentialErrorMessage = new StringBuilder();
String line;
int remainingFileCount = configService.getFileListMaxCount();
SimpleDateFormat targzDateFormat = new SimpleDateFormat(TAR_GZ_DATE_FORMAT);
LogAccessType logAccessType = cmd.startsWith(HTTPD_FILE_VIEW_COMMAND_START) ? LogAccessType.HTTPD : LogAccessType.LOCAL;
try {
while ( (line = resultReader.readLine()) != null && remainingFileCount > 0) {
potentialErrorMessage.append(line).append("\n");
// directories are ignored
boolean isDirectory = line.startsWith(DIRECTORY_RIGHT);
if (isDirectory) {
continue;
}
--remainingFileCount;
line = line.replaceAll(" +", " ");
StringTokenizer stLine = new StringTokenizer(line, " ");
// Skip 2 first columns
stLine.nextToken();
stLine.nextToken();
// Get columns to display
Long fileSize = Long.parseLong(stLine.nextToken());
String sFileDate = stLine.nextToken() + " " + stLine.nextToken();
Date fileDate = targzDateFormat.parse(sFileDate);
String filePath = stLine.nextToken();
String fileName = filePath.replaceAll(".*/", "");
// Construct FileInfo bean
FileInfo fileInfo = new FileInfo();
fileInfo.setFileName(fileName);
fileInfo.setRelativePath(targzFileName + "!" + filePath);
fileInfo.setDirectory(false);
fileInfo.setFileSize(fileSize);
fileInfo.setLastModified(fileDate);
fileInfo.setLogAccessType(logAccessType);
archiveEntryList.add(fileInfo);
}
}
catch (RuntimeException e) {
potentialErrorMessage.append(FileCopyUtils.copyToString(resultReader));
throw new IOException("Error while listing tar.gz entries.\n" + potentialErrorMessage.toString(), e);
}
catch (ParseException e) {
throw new IOException("Error while listing tar.gz entries. " + e.getMessage(), e);
}
// Render archive entry list in HTML
return listController.renderFileList(model, targzFileName, archiveEntryList);
}
/**
* Display the result of any command as a default html table
*/
private void processOtherCommand(BufferedReader resultReader, Model model) throws IOException {
List<List<TableCell>> tableLines = new ArrayList<List<TableCell>>();
String line;
int lineNumber = 0;
while ( (line = resultReader.readLine()) != null) {
++lineNumber;
tableLines.add(Arrays.asList(new TableCell(String.valueOf(lineNumber)), new TableCell(line)));
}
model.addAttribute(TABLE_HEADERS_KEY, Arrays.asList(LINE_NUMBER_TABLE_HEADER, LINE_CONTENT_TABLE_HEADER));
model.addAttribute(TABLE_LINES_KEY, tableLines);
model.addAttribute(TABLE_LAYOUT_CLASS_KEY, TABLE_LAYOUT_FULL_WIDTH);
}
/**
* Generate Breadcrumbs containing path to current file (for navigation)
* @param logAccessConfigId current logAccessConfigId
* @param commandLine current executed command
* @param request request where to add breadcrumbs as a request attribute
*/
private void generateBreadcrumbs(String logAccessConfigId, CommandLine commandLine, HttpServletRequest request) {
List<Breadcrumb> breadcrumbs = BreadcrumbFactory.createBreadCrumbs(logAccessConfigId);
String filePath = null;
boolean lastElementIsLink = false;
String targzFilePath = null;
String targzSubFilename = null;
// first argument
if (commandLine.getParams().size() >= 1) {
String firstParam = commandLine.getParam(0);
if (commandLine.getLine().startsWith(TAR_GZ_CONTENT_FILE_VIEW_COMMAND_START)) {
filePath = firstParam.contains("/") ? firstParam.substring(0, firstParam.lastIndexOf('/')) : null;
lastElementIsLink = true;
targzFilePath = firstParam;
}
else if (!commandLine.getCommand().matches(TWO_PARAMS_COMMAND_REGEX)) {
filePath = firstParam;
}
}
// second argument
if (commandLine.getParams().size() >= 2) {
String secondParam = commandLine.getParam(1);
if (commandLine.getLine().startsWith(TAR_GZ_CONTENT_FILE_VIEW_COMMAND_START)) {
targzSubFilename = secondParam.contains("/") ? secondParam.substring(secondParam.lastIndexOf('/') + 1) : secondParam;
}
else if (commandLine.getCommand().matches(TWO_PARAMS_COMMAND_REGEX)) {
filePath = secondParam;
}
}
// special case : view content file included in a tar.gz archive, using curl
if (commandLine.getLine().matches(TAR_GZ_CONTENT_FILE_VIEW_COMMAND_USING_CURL_REGEX)) {
targzFilePath = filePath;
filePath = targzFilePath.contains("/") ? targzFilePath.substring(0, targzFilePath.lastIndexOf('/')) : null;
lastElementIsLink = true;
targzSubFilename = commandLine.getLine().replaceFirst(TAR_GZ_CONTENT_FILE_VIEW_COMMAND_USING_CURL_REGEX, "$2");
targzSubFilename = targzSubFilename.contains("/") ? targzSubFilename.substring(targzSubFilename.lastIndexOf('/') + 1) : targzSubFilename;
}
if (filePath != null) {
BreadcrumbFactory.addSubPath(breadcrumbs, filePath, lastElementIsLink);
}
if (targzFilePath != null) {
try {
int filenameIndex = targzFilePath.lastIndexOf("/") + 1;
String targzFilename = filenameIndex > 0 ? targzFilePath.substring(filenameIndex) : targzFilePath;
String command = MessageFormat.format(TAR_GZ_FILE_VIEW_COMMAND, targzFilePath);
if (commandLine.getLine().startsWith(HTTPD_FILE_VIEW_COMMAND_START)) {
command = HTTPD_FILE_VIEW_COMMAND_START + targzFilePath + TAR_GZ_FILE_VIEW_COMMAND_END;
}
String link = FILE_VIEW_URL_PREFIX + URLEncoder.encode(command, URL_ENCODING);
breadcrumbs.add(new Breadcrumb(targzFilename, link));
breadcrumbs.add(new Breadcrumb(targzSubFilename));
}
catch (UnsupportedEncodingException e) {
throw new UnsupportedCharsetException(URL_ENCODING);
}
}
request.setAttribute(BREADCRUMBS_KEY, breadcrumbs);
}
}