In the previous article, we learned about how to we can use Java and ClamAV to detect any virus in a file. In this, we will learn how we can run both Java and ClamAV inside a docker container and also a beautiful UI in react JS to access it.

  1. Spring Boot Application having File Upload Functionality
  2. Dockerized Application and add ClamAV inside it.
  3. Create Sample UI in ReactJS to communicate with service and show if the file contains a virus or not.

Spring Boot Application having File Upload Functionality

To create a Simple Spring Boot Application you can go to https://start.spring.io/ and create a maven project in Spring(Select Spring web dependency). Once the project is created download it and open it in IntelliJ and build and run it. In the below image, you can find the configuration.

On top of it add below dependencies in pom.xml. Springfox is used to generate Swagger Documentation and common io is used to perform any file operations.

<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.4</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>

Once done we have to add Dockerfile. As we are running multiple processes inside docker we have to make some changes in Dockerfile. We have to use “supervised” in docker which helps in running multiple processes inside docker. Below is the architecture of how service will work. We can see Service will communicate to ClamAV for virus scanning. VirusDB contains metadata and all file formats it can scan related to ClamAV and Freshclam is responsible for updating it. Freshclam connects to the internet and goes to a specific site to fetch updates so it is advised in production use you should avoid running Freshclam daemon thread.

By default, ClamAV has clamd.conf and Feshclam configuration so we are going to provide our own with different TCP port and file size. It is always good to avoid using the default port of any services. In the above diagram, ClamAV will run inside docker and won’t be exposed to outside. Spring application will communicate over TCP.

For Supervisord we provide below config. “/etc/supervisor.d/*.conf” this contains all services execution command. (https://github.com/ragnar-lothbrok/clamav-spring-reactjs/tree/master/etc)

[supervisord]
nodaemon=true
user=root

[include]
files = /etc/supervisor.d/*.conf

Dockerfile

FROM azul/zulu-openjdk-alpine:11

RUN apk --no-cache add clamav-daemon freshclam clamav-libunrar supervisor

RUN sed -i 's/^Foreground .*$/Foreground true/g' /etc/clamav/clamd.conf && \
echo 'TCPSocket 3310' >> /etc/clamav/clamd.conf && \
sed -i 's/^Foreground .*$/Foreground true/g' /etc/clamav/freshclam.conf

# this is to bootstrap the virus definitions, as trying to start clamd without
# any just causes the process to die, upon the container starting it updates
# these immediately again
RUN freshclam

RUN mkdir /run/clamav && chown clamav:clamav /run/clamav
COPY etc /etc/

COPY
target/*.jar app.jar
EXPOSE 3310/tcp
CMD ["supervisord", "-c", "/etc/supervisord.conf"]

Create End Points

File Scan End Point

@CrossOrigin
@RestController
@RequestMapping("/api/v1/scan")
public class FileScanController {

private static Logger LOGGER = LoggerFactory.getLogger(FileScanController.class);

@Autowired
private FileScanService fileScanService;

@PostMapping("/upload")
public ResponseDto<List<FileScanResponseDto>> uploadFiles(@RequestParam("files") MultipartFile files) {
return new ResponseDto<List<FileScanResponseDto>>(fileScanService.scanFiles(new MultipartFile[] {files}));
}
}

Health End Point

In this endpoint, we will send “Ping” to ClamAV and if it returns ok we send “Pong” as health response. This endpoint will decide if the service is healthy or not.

@RestController
@RequestMapping("/clamd/health")
public class HealthController {

private static Logger LOGGER = LoggerFactory.getLogger(HealthController.class);

@Autowired
private AppProperties appProperties;

@Autowired
private ClamAVClient clamAVClient;

@GetMapping
public String healthCheck() {
boolean isOk = false;
try {
isOk = clamAVClient.ping();
} catch (IOException ex) {
LOGGER.error("Exception occurred while pinging clamav = {} ",ex.getMessage());
}
return isOk ? "pong" : "ping";
}
}

In the service layer, we will send Input Stream we receive to ClamAV and format response. The service layer sends InputStream to ClamAV using TCP and gets a response. We are also calculating the MD5 hash of InputSream and returning in response.

public List<FileScanResponseDto> scanFiles(MultipartFile[] files) {
LOGGER.info("File received = {} ", (files != null ? files.length : null));
return Arrays.stream(files).map(multipartFile -> {
FileScanResponseDto fileScanResponseDto = new FileScanResponseDto();
long startTime = System.currentTimeMillis();
fileScanResponseDto.setUploadTime(startTime);
try {
byte[] response = clamAVClient.scan(multipartFile.getInputStream(), fileScanResponseDto);
Boolean status = ClamAVClient.isCleanReply(response);
fileScanResponseDto.setDetected(status != null ? !status : status);
LOGGER.info("File Scanned = {} Clam AV Response = {} ", multipartFile.getOriginalFilename(), (status != null ? status : null));
} catch(ClamAVSizeLimitException exception) {
LOGGER.error("Exception occurred while scanning using clam av = {} ",exception.getMessage());
fileScanResponseDto.setErrorMessage(exception.getMessage());
} catch (Exception e) {
LOGGER.error("Exception occurred while scanning using clam av = {} ",e.getMessage());
fileScanResponseDto.setErrorMessage(e.getMessage());
}
fileScanResponseDto.setFileName(multipartFile.getOriginalFilename());
fileScanResponseDto.setFileType(FilenameUtils.getExtension(multipartFile.getOriginalFilename()));
fileScanResponseDto.setSize(multipartFile.getSize());
fileScanResponseDto.setScanTimeInMilliSec(System.currentTimeMillis() - startTime);
return fileScanResponseDto;
}).collect(Collectors.toList());
}

Building and Running Spring Application inside docker

bash$ docker build -t clamav-scanner .bash$ docker image ls -a | grep clamav-scannerclamav-scanner latest 9286d673c9ec 10 minutes ago 522MBbash$ docker run -p 8080:8080  clamav-scanner

The above command will run the Spring Boot application inside the docker. Once Services are running successfully you can access swagger using http://localhost:8080/swagger-ui.html.

Health API

File Scan API

Curl Request

curl -X POST “http://localhost:8080/api/v1/scan/upload" -H “accept: */*” -H “Content-Type: multipart/form-data” -F “files=@sampe.json;type=application/json”

Response

{
"data": [
{
"fileName": "sampe.json",
"detected": false,
"fileType": "json",
"size": 9079,
"scanTimeInMilliSec": 14,
"hash": "e589eedd8ffda2f3fc74f4793a45a0b3",
"uploadTime": 1577021303104
}
]
}

In the next article, we will integrate it with ReactJS.

Thanks.

Subscribe to FAUN topics and get your weekly curated email of the must-read tech stories, news, and tutorials 🗞️

Follow us on Twitter 🐦 and Facebook 👥 and Instagram 📷 and join our Facebook and Linkedin Groups 💬

If this post was helpful, please click the clap 👏 button below a few times to show your support for the author! ⬇

--

--