/*******************************************************************************
* Copyright (c) 2011, 2016 Eurotech and/or its affiliates
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Eurotech
*******************************************************************************/
package org.eclipse.kura.web.server.servlet;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.FileCleanerCleanup;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.io.FileCleaningTracker;
import org.apache.commons.io.IOUtils;
import org.eclipse.kura.configuration.ComponentConfiguration;
import org.eclipse.kura.configuration.ConfigurationService;
import org.eclipse.kura.core.configuration.ComponentConfigurationImpl;
import org.eclipse.kura.core.configuration.XmlComponentConfigurations;
import org.eclipse.kura.core.configuration.util.XmlUtil;
import org.eclipse.kura.deployment.agent.DeploymentAgentService;
import org.eclipse.kura.system.SystemService;
import org.eclipse.kura.web.Console;
import org.eclipse.kura.web.server.KuraRemoteServiceServlet;
import org.eclipse.kura.web.server.util.ServiceLocator;
import org.eclipse.kura.web.shared.GwtKuraErrorCode;
import org.eclipse.kura.web.shared.GwtKuraException;
import org.eclipse.kura.web.shared.model.GwtXSRFToken;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.service.metatype.MetaTypeInformation;
import org.osgi.service.metatype.MetaTypeService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class FileServlet extends HttpServlet {
private static final long serialVersionUID = -5016170117606322129L;
private static Logger s_logger = LoggerFactory.getLogger(FileServlet.class);
private static final int BUFFER = 1024;
private static int tooBig = 0x6400000; // Max size of unzipped data, 100MB
private static int tooMany = 1024; // Max number of files
private DiskFileItemFactory m_diskFileItemFactory;
private FileCleaningTracker m_fileCleaningTracker;
@Override
public void destroy() {
super.destroy();
s_logger.info("Servlet {} destroyed", getServletName());
if (this.m_fileCleaningTracker != null) {
s_logger.info("Number of temporary files tracked: " + this.m_fileCleaningTracker.getTrackCount());
}
}
@Override
public void init() throws ServletException {
super.init();
s_logger.info("Servlet {} initialized", getServletName());
ServletContext ctx = getServletContext();
this.m_fileCleaningTracker = FileCleanerCleanup.getFileCleaningTracker(ctx);
getZipUploadSizeMax();
getZipUploadCountMax();
int sizeThreshold = getFileUploadInMemorySizeThreshold();
File repository = new File(System.getProperty("java.io.tmpdir"));
s_logger.info("DiskFileItemFactory.DEFAULT_SIZE_THRESHOLD: {}", DiskFileItemFactory.DEFAULT_SIZE_THRESHOLD);
s_logger.info("DiskFileItemFactory: using size threshold of: {}", sizeThreshold);
this.m_diskFileItemFactory = new DiskFileItemFactory(sizeThreshold, repository);
this.m_diskFileItemFactory.setFileCleaningTracker(this.m_fileCleaningTracker);
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("image/jpeg");
String reqPathInfo = req.getPathInfo();
if (reqPathInfo == null) {
s_logger.error("Request path info not found");
throw new ServletException("Request path info not found");
}
s_logger.debug("req.getRequestURI(): {}", req.getRequestURI());
s_logger.debug("req.getRequestURL(): {}", req.getRequestURL());
s_logger.debug("req.getPathInfo(): {}", req.getPathInfo());
if (reqPathInfo.startsWith("/icon")) {
doGetIcon(req, resp);
} else {
s_logger.error("Unknown request path info: " + reqPathInfo);
throw new ServletException("Unknown request path info: " + reqPathInfo);
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html");
String reqPathInfo = req.getPathInfo();
if (reqPathInfo == null) {
s_logger.error("Request path info not found");
throw new ServletException("Request path info not found");
}
s_logger.debug("req.getRequestURI(): {}", req.getRequestURI());
s_logger.debug("req.getRequestURL(): {}", req.getRequestURL());
s_logger.debug("req.getPathInfo(): {}", req.getPathInfo());
if (reqPathInfo.startsWith("/deploy")) {
doPostDeploy(req, resp);
} else if (reqPathInfo.equals("/configuration/snapshot")) {
doPostConfigurationSnapshot(req, resp);
} else if (reqPathInfo.equals("/command")) {
doPostCommand(req, resp);
} else if (reqPathInfo.equals("/certificate")) {
return;
} else {
s_logger.error("Unknown request path info: " + reqPathInfo);
throw new ServletException("Unknown request path info: " + reqPathInfo);
}
}
private void doGetIcon(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String queryString = req.getQueryString();
if (queryString == null) {
s_logger.error("Error parsing query string.");
throw new ServletException("Error parsing query string.");
}
// Parse the query string
Map<String, String> pairs;
try {
pairs = parseQueryString(queryString);
} catch (UnsupportedEncodingException e) {
s_logger.error("Error parsing query string.");
throw new ServletException("Error parsing query string: " + e.getLocalizedMessage());
}
// Check for malformed request
if (pairs == null || pairs.size() != 1) {
s_logger.error("Error parsing query string.");
throw new ServletException("Error parsing query string.");
}
String pid = pairs.get("pid");
if (pid != null && pid.length() > 0) {
BundleContext ctx = Console.getBundleContext();
Bundle[] bundles = ctx.getBundles();
ServiceLocator locator = ServiceLocator.getInstance();
// Iterate over bundles to find PID
for (Bundle b : bundles) {
MetaTypeService mts;
try {
mts = locator.getService(MetaTypeService.class);
} catch (GwtKuraException e1) {
s_logger.error("Error parsing query string.");
throw new ServletException("Error parsing query string.");
}
MetaTypeInformation mti = mts.getMetaTypeInformation(b);
String[] pids = mti.getPids();
for (String p : pids) {
if (p.equals(pid)) {
try {
InputStream is = mti.getObjectClassDefinition(pid, null).getIcon(32);
if (is == null) {
s_logger.error("Error reading icon file.");
throw new ServletException("Error reading icon file.");
}
OutputStream os = resp.getOutputStream();
byte[] buffer = new byte[1024];
for (int length = 0; (length = is.read(buffer)) > 0;) {
os.write(buffer, 0, length);
}
is.close();
os.close();
} catch (IOException e) {
s_logger.error("Error reading icon file.");
throw new IOException("Error reading icon file.");
}
}
}
}
} else {
s_logger.error("Error parsing query string.");
throw new ServletException("Error parsing query string.");
}
}
private Map<String, String> parseQueryString(String queryString) throws UnsupportedEncodingException {
Map<String, String> qp = new HashMap<String, String>();
String[] pairs = queryString.split("&");
for (String p : pairs) {
int index = p.indexOf("=");
qp.put(URLDecoder.decode(p.substring(0, index), "UTF-8"),
URLDecoder.decode(p.substring(index + 1), "UTF-8"));
}
return qp;
}
private void doPostCommand(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
UploadRequest upload = new UploadRequest(this.m_diskFileItemFactory);
try {
upload.parse(req);
} catch (FileUploadException e) {
s_logger.error("Error parsing the file upload request");
throw new ServletException("Error parsing the file upload request", e);
}
// BEGIN XSRF - Servlet dependent code
Map<String, String> formFields = upload.getFormFields();
try {
GwtXSRFToken token = new GwtXSRFToken(formFields.get("xsrfToken"));
KuraRemoteServiceServlet.checkXSRFToken(req, token);
} catch (Exception e) {
throw new ServletException("Security error: please retry this operation correctly.", e);
}
// END XSRF security check
List<FileItem> fileItems = null;
InputStream is = null;
File localFolder = new File(System.getProperty("java.io.tmpdir"));
OutputStream os = null;
try {
fileItems = upload.getFileItems();
if (fileItems.size() > 0) {
FileItem item = fileItems.get(0);
is = item.getInputStream();
byte[] bytes = IOUtils.toByteArray(is);
ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(bytes));
int entries = 0;
long total = 0;
ZipEntry ze = zis.getNextEntry();
while (ze != null) {
byte[] buffer = new byte[BUFFER];
String expectedFilePath = new StringBuilder(localFolder.getPath()).append(File.separator)
.append(ze.getName()).toString();
String fileName = validateFileName(expectedFilePath, localFolder.getPath());
File newFile = new File(fileName);
if (newFile.isDirectory()) {
newFile.mkdirs();
ze = zis.getNextEntry();
continue;
}
if (newFile.getParent() != null) {
File parent = new File(newFile.getParent());
parent.mkdirs();
}
FileOutputStream fos = new FileOutputStream(newFile);
int len;
while (total + BUFFER <= tooBig && (len = zis.read(buffer)) > 0) {
fos.write(buffer, 0, len);
total += len;
}
fos.flush();
fos.close();
entries++;
if (entries > tooMany) {
throw new IllegalStateException("Too many files to unzip.");
}
if (total > tooBig) {
throw new IllegalStateException("File being unzipped is too big.");
}
ze = zis.getNextEntry();
}
zis.closeEntry();
zis.close();
}
} catch (IOException e) {
throw e;
} catch (GwtKuraException e) {
throw new ServletException("File is outside extraction target directory.");
} finally {
if (os != null) {
try {
os.close();
} catch (IOException e) {
s_logger.warn("Cannot close output stream", e);
}
}
if (is != null) {
try {
is.close();
} catch (IOException e) {
s_logger.warn("Cannot close input stream", e);
}
}
if (fileItems != null) {
for (FileItem fileItem : fileItems) {
fileItem.delete();
}
}
}
}
private String validateFileName(String zipFileName, String intendedDir) throws IOException, GwtKuraException {
File zipFile = new File(zipFileName);
String filePath = zipFile.getCanonicalPath();
File iD = new File(intendedDir);
String canonicalID = iD.getCanonicalPath();
if (filePath.startsWith(canonicalID)) {
return filePath;
} else {
throw new GwtKuraException(GwtKuraErrorCode.ILLEGAL_ACCESS);
}
}
private void doPostConfigurationSnapshot(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
UploadRequest upload = new UploadRequest(this.m_diskFileItemFactory);
try {
upload.parse(req);
} catch (FileUploadException e) {
s_logger.error("Error parsing the file upload request");
throw new ServletException("Error parsing the file upload request", e);
}
// BEGIN XSRF - Servlet dependent code
Map<String, String> formFields = upload.getFormFields();
try {
GwtXSRFToken token = new GwtXSRFToken(formFields.get("xsrfToken"));
KuraRemoteServiceServlet.checkXSRFToken(req, token);
} catch (Exception e) {
throw new ServletException("Security error: please retry this operation correctly.", e);
}
// END XSRF security check
List<FileItem> fileItems = upload.getFileItems();
if (fileItems.size() != 1) {
s_logger.error("expected 1 file item but found {}", fileItems.size());
throw new ServletException("Wrong number of file items");
}
FileItem fileItem = fileItems.get(0);
byte[] data = fileItem.get();
String xmlString = new String(data, "UTF-8");
XmlComponentConfigurations xmlConfigs;
try {
xmlConfigs = XmlUtil.unmarshal(xmlString, XmlComponentConfigurations.class);
} catch (Exception e) {
s_logger.error("Error unmarshaling device configuration", e);
throw new ServletException("Error unmarshaling device configuration", e);
}
ServiceLocator locator = ServiceLocator.getInstance();
try {
ConfigurationService cs = locator.getService(ConfigurationService.class);
List<ComponentConfigurationImpl> configImpls = xmlConfigs.getConfigurations();
List<ComponentConfiguration> configs = new ArrayList<ComponentConfiguration>();
configs.addAll(configImpls);
cs.updateConfigurations(configs);
//
// Add an additional delay after the configuration update
// to give the time to the device to apply the received
// configuration
SystemService ss = locator.getService(SystemService.class);
long delay = Long.parseLong(ss.getProperties().getProperty("console.updateConfigDelay", "5000"));
if (delay > 0) {
Thread.sleep(delay);
}
} catch (Exception e) {
s_logger.error("Error updating device configuration: {}", e);
throw new ServletException("Error updating device configuration", e);
}
}
private void doPostDeployUpload(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
ServiceLocator locator = ServiceLocator.getInstance();
DeploymentAgentService deploymentAgentService;
try {
deploymentAgentService = locator.getService(DeploymentAgentService.class);
} catch (GwtKuraException e) {
s_logger.error("Error locating DeploymentAgentService", e);
throw new ServletException("Error locating DeploymentAgentService", e);
}
// Check that we have a file upload request
boolean isMultipart = ServletFileUpload.isMultipartContent(req);
if (!isMultipart) {
s_logger.error("Not a file upload request");
throw new ServletException("Not a file upload request");
}
UploadRequest upload = new UploadRequest(this.m_diskFileItemFactory);
try {
upload.parse(req);
} catch (FileUploadException e) {
s_logger.error("Error parsing the file upload request", e);
throw new ServletException("Error parsing the file upload request", e);
}
// BEGIN XSRF - Servlet dependent code
Map<String, String> formFields = upload.getFormFields();
try {
GwtXSRFToken token = new GwtXSRFToken(formFields.get("xsrfToken"));
KuraRemoteServiceServlet.checkXSRFToken(req, token);
} catch (Exception e) {
throw new ServletException("Security error: please retry this operation correctly.", e);
}
// END XSRF security check
List<FileItem> fileItems = null;
InputStream is = null;
File localFile = null;
OutputStream os = null;
boolean successful = false;
try {
fileItems = upload.getFileItems();
if (fileItems.size() != 1) {
s_logger.error("expected 1 file item but found {}", fileItems.size());
throw new ServletException("Wrong number of file items");
}
FileItem item = fileItems.get(0);
String filename = item.getName();
is = item.getInputStream();
String filePath = System.getProperty("java.io.tmpdir") + File.separator + filename;
localFile = new File(filePath);
if (localFile.exists()) {
if (localFile.delete()) {
s_logger.error("Cannot delete file: {}", filePath);
throw new ServletException("Cannot delete file: " + filePath);
}
}
try {
localFile.createNewFile();
localFile.deleteOnExit();
} catch (IOException e) {
s_logger.error("Cannot create file: {}", filePath, e);
throw new ServletException("Cannot create file: " + filePath);
}
try {
os = new FileOutputStream(localFile);
} catch (FileNotFoundException e) {
s_logger.error("Cannot find file: {}", filePath, e);
throw new ServletException("Cannot find file: " + filePath, e);
}
s_logger.info("Copying uploaded package file to file: {}", filePath);
try {
IOUtils.copy(is, os);
} catch (IOException e) {
s_logger.error("Failed to copy deployment package file: {}", filename, e);
throw new ServletException("Failed to copy deployment package file: " + filename, e);
}
try {
os.close();
} catch (IOException e) {
s_logger.warn("Cannot close output stream", e);
}
URL url = localFile.toURI().toURL();
String sUrl = url.toString();
s_logger.info("Installing package...");
try {
deploymentAgentService.installDeploymentPackageAsync(sUrl);
successful = true;
} catch (Exception e) {
s_logger.error("Package installation failed", e);
throw new ServletException("Package installation failed", e);
}
} catch (IOException e) {
throw e;
} catch (ServletException e) {
throw e;
} finally {
if (os != null) {
try {
os.close();
} catch (IOException e) {
s_logger.warn("Cannot close output stream", e);
}
}
if (localFile != null && !successful) {
try {
localFile.delete();
} catch (Exception e) {
s_logger.warn("Cannot delete file");
}
}
if (is != null) {
try {
is.close();
} catch (IOException e) {
s_logger.warn("Cannot close input stream", e);
}
}
if (fileItems != null) {
for (FileItem fileItem : fileItems) {
fileItem.delete();
}
}
}
}
private void doPostDeploy(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
ServiceLocator locator = ServiceLocator.getInstance();
DeploymentAgentService deploymentAgentService;
try {
deploymentAgentService = locator.getService(DeploymentAgentService.class);
} catch (GwtKuraException e) {
s_logger.error("Error locating DeploymentAgentService", e);
throw new ServletException("Error locating DeploymentAgentService", e);
}
String reqPathInfo = req.getPathInfo();
if (reqPathInfo.endsWith("url")) {
String packageDownloadUrl = req.getParameter("packageUrl");
if (packageDownloadUrl == null) {
s_logger.error("Deployment package URL parameter missing");
throw new ServletException("Deployment package URL parameter missing");
}
// BEGIN XSRF - Servlet dependent code
String tokenId = req.getParameter("xsrfToken");
try {
GwtXSRFToken token = new GwtXSRFToken(tokenId);
KuraRemoteServiceServlet.checkXSRFToken(req, token);
} catch (Exception e) {
throw new ServletException("Security error: please retry this operation correctly.", e);
}
// END XSRF security check
try {
s_logger.info("Installing package...");
deploymentAgentService.installDeploymentPackageAsync(packageDownloadUrl);
} catch (Exception e) {
s_logger.error("Failed to install package at URL {}", packageDownloadUrl, e);
throw new ServletException("Error installing deployment package", e);
}
} else if (reqPathInfo.endsWith("upload")) {
doPostDeployUpload(req, resp);
} else {
s_logger.error("Unsupported package deployment request");
throw new ServletException("Unsupported package deployment request");
}
}
private void getZipUploadSizeMax() {
ServiceLocator locator = ServiceLocator.getInstance();
try {
SystemService systemService = locator.getService(SystemService.class);
int sizeInMB = systemService.getFileCommandZipMaxUploadSize();
int sizeInBytes = sizeInMB * 1024 * 1024;
tooBig = sizeInBytes;
} catch (GwtKuraException e) {
s_logger.error("Error locating SystemService", e);
}
}
private void getZipUploadCountMax() {
ServiceLocator locator = ServiceLocator.getInstance();
try {
SystemService systemService = locator.getService(SystemService.class);
tooMany = systemService.getFileCommandZipMaxUploadNumber();
} catch (GwtKuraException e) {
s_logger.error("Error locating SystemService", e);
}
}
static long getFileUploadSizeMax() {
ServiceLocator locator = ServiceLocator.getInstance();
long sizeMax = -1;
try {
SystemService systemService = locator.getService(SystemService.class);
sizeMax = Long.parseLong(systemService.getProperties().getProperty("file.upload.size.max", "-1"));
} catch (GwtKuraException e) {
s_logger.error("Error locating SystemService", e);
}
return sizeMax;
}
static private int getFileUploadInMemorySizeThreshold() {
ServiceLocator locator = ServiceLocator.getInstance();
int sizeThreshold = DiskFileItemFactory.DEFAULT_SIZE_THRESHOLD;
try {
SystemService systemService = locator.getService(SystemService.class);
sizeThreshold = Integer
.parseInt(systemService.getProperties().getProperty("file.upload.in.memory.size.threshold",
String.valueOf(DiskFileItemFactory.DEFAULT_SIZE_THRESHOLD)));
} catch (GwtKuraException e) {
s_logger.error("Error locating SystemService", e);
}
return sizeThreshold;
}
}
class UploadRequest extends ServletFileUpload {
private static Logger s_logger = LoggerFactory.getLogger(UploadRequest.class);
Map<String, String> formFields;
List<FileItem> fileItems;
public UploadRequest(DiskFileItemFactory diskFileItemFactory) {
super(diskFileItemFactory);
setSizeMax(FileServlet.getFileUploadSizeMax());
this.formFields = new HashMap<String, String>();
this.fileItems = new ArrayList<FileItem>();
}
@SuppressWarnings("unchecked")
public void parse(HttpServletRequest req) throws FileUploadException {
s_logger.debug("upload.getFileSizeMax(): {}", getFileSizeMax());
s_logger.debug("upload.getSizeMax(): {}", getSizeMax());
// Parse the request
List<FileItem> items = null;
items = parseRequest(req);
// Process the uploaded items
Iterator<FileItem> iter = items.iterator();
while (iter.hasNext()) {
FileItem item = iter.next();
if (item.isFormField()) {
String name = item.getFieldName();
String value = item.getString();
s_logger.debug("Form field item name: {}, value: {}", name, value);
this.formFields.put(name, value);
} else {
String fieldName = item.getFieldName();
String fileName = item.getName();
String contentType = item.getContentType();
boolean isInMemory = item.isInMemory();
long sizeInBytes = item.getSize();
s_logger.debug("File upload item name: {}, fileName: {}, contentType: {}, isInMemory: {}, size: {}",
new Object[] { fieldName, fileName, contentType, isInMemory, sizeInBytes });
this.fileItems.add(item);
}
}
}
public Map<String, String> getFormFields() {
return this.formFields;
}
public List<FileItem> getFileItems() {
return this.fileItems;
}
}