/********************************************************************** * This file is part of iDempiere ERP Open Source * * http://www.idempiere.org * * * * Copyright (C) Contributors * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 2 * * 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 General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the Free Software * * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * * MA 02110-1301, USA. * * * * Contributors: * * - Trek Global Corporation * * - Heng Sin Low * **********************************************************************/ package com.trekglobal.idempiere.rest.api.v1.resource.impl; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.util.Base64; import java.util.logging.Level; import javax.activation.MimetypesFileTypeMap; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import org.compiere.model.MUser; import org.compiere.util.CLogger; import org.compiere.util.Env; import org.compiere.util.MimeType; import org.compiere.util.Util; import org.idempiere.distributed.IClusterMember; import org.idempiere.distributed.IClusterService; import com.google.gson.JsonObject; import com.trekglobal.idempiere.rest.api.json.ResponseUtils; import com.trekglobal.idempiere.rest.api.util.ClusterUtil; import com.trekglobal.idempiere.rest.api.util.ErrorBuilder; import com.trekglobal.idempiere.rest.api.v1.resource.FileResource; import com.trekglobal.idempiere.rest.api.v1.resource.file.FileAccess; import com.trekglobal.idempiere.rest.api.v1.resource.file.FileInfo; import com.trekglobal.idempiere.rest.api.v1.resource.file.FileStreamingOutput; import com.trekglobal.idempiere.rest.api.v1.resource.file.GetFileInfoCallable; import com.trekglobal.idempiere.rest.api.v1.resource.file.RemoteFileStreamingOutput; /** * * @author hengsin * */ public class FileResourceImpl implements FileResource { protected static final int BLOCK_SIZE = 1024 * 1024 * 5; private static final CLogger log = CLogger.getCLogger(FileResourceImpl.class); public FileResourceImpl() { } @Override public Response getFile(String fileName, long length, String nodeId, String asJson) { MUser user = MUser.get(Env.getCtx()); if (!user.isAdministrator()) return forbidden("Access denied", "Access denied for get file request"); /* TODO: allow access to files generated by processes in a more secure way * the length is not enough for security because a hacker can get access with trial/error * maybe adding an md5sum in the response of the process, and validating it here can make the implementation safer */ if (Util.isEmpty(nodeId)) { return getLocalFile(fileName, true, length, asJson); } else { IClusterService service = ClusterUtil.getClusterService(); if (service == null) return getLocalFile(fileName, true, length, asJson); IClusterMember local = service.getLocalMember(); if (local != null && local.getId().equals(nodeId)) return getLocalFile(fileName, true, length, asJson); return getRemoteFile(fileName, true, length, nodeId, asJson); } } /** * * @param fileName * @param nodeId * @return response */ public Response getFile(String fileName, String nodeId, String asJson) { if (Util.isEmpty(nodeId)) { return getLocalFile(fileName, false, 0, asJson); } else { IClusterService service = ClusterUtil.getClusterService(); if (service == null) return getLocalFile(fileName, false, 0, asJson); IClusterMember local = service.getLocalMember(); if (local != null && local.getId().equals(nodeId)) return getLocalFile(fileName, false, 0, asJson); return getRemoteFile(fileName, false, 0, nodeId, asJson); } } private Response getLocalFile(String fileName, boolean verifyLength, long length, String asJson) { File file = new File(fileName); if (file.exists() && file.isFile()) { if (!file.canRead() || !FileAccess.isAccessible(file)) { return Response.status(Status.FORBIDDEN) .entity(new ErrorBuilder().status(Status.FORBIDDEN).title("File not readable").append("File not readable: ").append(fileName).build().toString()) .build(); } else if (!verifyLength || file.length()==length) { try { if (asJson == null) { String contentType = null; String lfn = fileName.toLowerCase(); if (lfn.endsWith(".html") || lfn.endsWith(".htm")) { contentType = MediaType.TEXT_HTML; } else if (lfn.endsWith(".csv") || lfn.endsWith(".ssv") || lfn.endsWith(".log")) { contentType = MediaType.TEXT_PLAIN; } else { contentType = MimeType.getMimeType(file.getName()); } if (Util.isEmpty(contentType, true)) contentType = MediaType.APPLICATION_OCTET_STREAM; FileStreamingOutput fso = new FileStreamingOutput(file); return Response.ok(fso, contentType).build(); } else { JsonObject json = new JsonObject(); byte[] binaryData = Files.readAllBytes(file.toPath()); String data = Base64.getEncoder().encodeToString(binaryData); json.addProperty("data", data); return Response.ok(json.toString()).build(); } } catch (IOException ex) { return ResponseUtils.getResponseErrorFromException(ex, "IO error"); } } else { return Response.status(Status.FORBIDDEN) .entity(new ErrorBuilder().status(Status.FORBIDDEN).title("Access denied").append("Access denied for file: ").append(fileName).build().toString()) .build(); } } else { return Response.status(Status.NOT_FOUND) .entity(new ErrorBuilder().status(Status.NOT_FOUND).title("File not found").append("File not found: ").append(fileName).build().toString()) .build(); } } private Response getRemoteFile(String fileName, boolean verifyLength, long length, String nodeId, String asJson) { IClusterService service = ClusterUtil.getClusterService(); IClusterMember member = ClusterUtil.getClusterMember(nodeId); if (member == null) { return Response.status(Status.NOT_FOUND) .entity(new ErrorBuilder().status(Status.NOT_FOUND).title("Invalid Node Id").append("No match found for node id: ").append(nodeId).build().toString()) .build(); } try { GetFileInfoCallable infoCallable = new GetFileInfoCallable(null, fileName, BLOCK_SIZE); FileInfo fileInfo = service.execute(infoCallable, member).get(); if (fileInfo == null) { return Response.status(Status.NOT_FOUND) .entity(new ErrorBuilder().status(Status.NOT_FOUND).title("Invalid File Name").append("File does not exists or not readable: ").append(fileName).build().toString()) .build(); } if (verifyLength && length != fileInfo.getLength()) { return Response.status(Status.FORBIDDEN) .entity(new ErrorBuilder().status(Status.FORBIDDEN).title("Access denied").append("Access denied for file: ").append(fileName).build().toString()) .build(); } RemoteFileStreamingOutput rfso = new RemoteFileStreamingOutput(fileInfo, member); if (asJson == null) { String contentType = null; String lfn = fileName.toLowerCase(); if (lfn.endsWith(".html") || lfn.endsWith(".htm")) { contentType = MediaType.TEXT_HTML; } else if (lfn.endsWith(".csv") || lfn.endsWith(".ssv") || lfn.endsWith(".log")) { contentType = MediaType.TEXT_PLAIN; } else { MimetypesFileTypeMap map = new MimetypesFileTypeMap(); contentType = map.getContentType(fileInfo.getFileName()); } if (Util.isEmpty(contentType, true)) contentType = MediaType.APPLICATION_OCTET_STREAM; return Response.ok(rfso, contentType).build(); } else { try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) { rfso.write(byteArrayOutputStream); JsonObject json = new JsonObject(); byte[] binaryData = byteArrayOutputStream.toByteArray(); String data = Base64.getEncoder().encodeToString(binaryData); json.addProperty("data", data); return Response.ok(json.toString()).build(); } } } catch (Exception ex) { log.log(Level.SEVERE, ex.getMessage(), ex); return Response.status(Status.INTERNAL_SERVER_ERROR) .entity(new ErrorBuilder().status(Status.INTERNAL_SERVER_ERROR).title("Server error").append("Server error with exception: ").append(ex.getMessage()).build().toString()) .build(); } } private Response forbidden(String title, String detail) { return Response.status(Status.FORBIDDEN) .entity(new ErrorBuilder().status(Status.FORBIDDEN).title(title).append(detail).build().toString()) .build(); } }