001/*
002 *  Copyright 2023 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.runtime.request;
017
018import java.io.File;
019import java.io.IOException;
020import java.util.ArrayList;
021import java.util.Enumeration;
022import java.util.HashMap;
023import java.util.Hashtable;
024import java.util.List;
025import java.util.Map;
026
027import javax.servlet.http.HttpServletRequest;
028
029import org.apache.cocoon.servlet.multipart.MultipartException;
030import org.apache.cocoon.servlet.multipart.MultipartHttpServletRequest;
031import org.apache.cocoon.servlet.multipart.Part;
032import org.apache.cocoon.servlet.multipart.PartOnDisk;
033import org.apache.cocoon.servlet.multipart.RejectedPart;
034
035import org.ametys.runtime.config.ConfigManager;
036import org.ametys.runtime.servlet.AnalyseFileForVirusHelper;
037
038/**
039 * Extension of the cocoon request factory to allow antivirus scanning of uploaded file
040 */
041public class RequestFactory extends org.apache.cocoon.servlet.multipart.RequestFactory
042{
043    // check for antivirus activation at instantiation
044    private boolean _antivirusEnabled;
045
046    /**
047     * Default constructor see {@link org.apache.cocoon.servlet.multipart.RequestFactory} for documentation
048     * @param saveUploadedFilesToDisk to save files to disk
049     * @param uploadDirectory where to save files on disk
050     * @param allowOverwrite to allow overwrite
051     * @param silentlyRename to rename silently
052     * @param maxUploadSize the maximum size of an uploaded file
053     * @param defaultCharEncoding the default encoding
054     */
055    public RequestFactory(boolean saveUploadedFilesToDisk,
056            File uploadDirectory,
057            boolean allowOverwrite,
058            boolean silentlyRename,
059            int maxUploadSize,
060            String defaultCharEncoding)
061    {
062        super(saveUploadedFilesToDisk, uploadDirectory, allowOverwrite, silentlyRename, maxUploadSize, defaultCharEncoding);
063        
064        // Check antivirus activation
065        if (ConfigManager.getInstance().isComplete())
066        {
067            _antivirusEnabled = AnalyseFileForVirusHelper.isAntivirusEnabled();
068        }
069    }
070    
071    @SuppressWarnings("unchecked")
072    @Override
073    public HttpServletRequest getServletRequest(HttpServletRequest request) throws IOException, MultipartException
074    {
075        HttpServletRequest servletRequest = super.getServletRequest(request);
076        // Check the activation of the antivirus to ensure minimal impact if antivirus is disabled
077        if (servletRequest instanceof MultipartHttpServletRequest multipart && _antivirusEnabled)
078        {
079            Map<String, Object> rejectedParam = new HashMap<>();
080            Enumeration<String> parameterNames = multipart.getParameterNames();
081            while (parameterNames.hasMoreElements())
082            {
083                String name = parameterNames.nextElement();
084                Object value = multipart.get(name);
085                if (value instanceof PartOnDisk diskPart)
086                {
087                    if (_analyseAndRejectFile(diskPart, name))
088                    {
089                        // Record the name of param to remove
090                        rejectedParam.put(name, new RejectedPart(diskPart.getHeaders(), 0, 0, 0));
091                    }
092                }
093                else if (value instanceof List valueList && !valueList.isEmpty() && valueList.get(0) instanceof PartOnDisk)
094                {
095                    List<Part> replaceFiles = new ArrayList<>();
096                    for (PartOnDisk diskPart : (List<PartOnDisk>) valueList)
097                    {
098                        // Analyse the file and reject and delete it if it has a virus
099                        if (_analyseAndRejectFile(diskPart, name))
100                        {
101                            replaceFiles.add(new RejectedPart(diskPart.getHeaders(), 0, 0, 0));
102                        }
103                        else
104                        {
105                            replaceFiles.add(diskPart);
106                        }
107                    }
108                    
109                    // Record the name of param to remove
110                    rejectedParam.put(name, replaceFiles);
111                }
112            }
113            
114            if (!rejectedParam.isEmpty())
115            {
116                Hashtable<String, Object> params = new Hashtable<>();
117                // some part were rejected. 
118                // We have to copy the request parameters
119                parameterNames = multipart.getParameterNames();
120                while (parameterNames.hasMoreElements())
121                {
122                    String name = parameterNames.nextElement();
123                    if (rejectedParam.containsKey(name))
124                    {
125                        // We represent a contaminated part with a rejected part with max size = 0byte
126                        params.put(name, rejectedParam.get(name));
127                    }
128                    else
129                    {
130                        params.put(name, multipart.get(name));
131                    }
132                }
133                
134                // and wrap the request with the new parameters including the rejected parts
135                servletRequest = new MultipartHttpServletRequest(servletRequest, params);
136            }
137        }
138        return servletRequest;
139    }
140    
141    /**
142     * Analyse the file for viruses, and delete it if a virus has been detected
143     * @param diskPart The part to analyse
144     * @param name The name of the param containing the file
145     * @return a {@link Boolean}, true if a virus has been detected, false otherwise
146     */
147    private boolean _analyseAndRejectFile(PartOnDisk diskPart, String name)
148    {
149        File file = diskPart.getFile();
150        // Check file here
151        if (!AnalyseFileForVirusHelper.analysefile(file.getAbsolutePath()))
152        {
153            // Delete dangerous file instantly
154            file.delete();
155            
156            return true;
157        }
158        
159        return false;
160    }
161}