서론
최근에 코로나 밀접접촉자 판정을 받으면서 약 일주일 동안 재택근무했다. 좋긴 좋은데 집에서 하니 뭔가 집중도 안되고 카페를 못 가니 답답했다. 암튼, 소스코드 외부 반출이 안돼서 집에 있는 동안 확장자 체크 라이브러리 tika 좀 알아보라는 업무를 받았다. 업무를 하면서 알게된 내용을 정리해보려 한다.
본론
MIME(Multipurpose Internet Mail Extensions)
tika는 MIME를 이용해서 파일 타입을 체크한다. MIME이 뭘까?
MIME는 SMTP 프로토콜에서 이메일을 보낼때 파일을 확인하기 위해 생성된 표준인데, 이제는 많은 프로토콜에서 같은 목적으로 MIME를 사용하기 시작했다. 그래서 "Internet Media Type"이라고 부르기도 한다.
대부분의 프로토콜에서 header에 MIME를 Content-type이란 key로 사용한다.
MIME는 type/subtype으로 이루어져있는데 간혹 subtype 중에 vnd, x- 라는 prefix가 붙은 걸 볼 수 있다.
vnd는 vendor라는 뜻으로 파일 생성 회사를 특정할 수 있다는 뜻이다.
x-는 IANA에 등록되지 않은 표준 MIME 타입이 아니라는 뜻이다.
Tika Library
tika는 2가지 라이브러리를 가지고 있다.
- tika-core
- tika-parsers
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId>
<version>1.22</version>
</dependency>
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-parsers</artifactId>
<version>1.22</version>
</dependency>
tika-core는 tika의 parser 패키지가 제외된 버전이라고 보면 된다. 단순히 파일 타입 체크만 한다면 tika-core만으로도 충분하고, 파일 안의 내용을 알 필요가 있으면 tika-parsers가 필요하다. 목적에 맞게 하나를 선택해서 사용하면 된다.
다만, 파일 타입 체크를 할 때 tika-core를 사용할 경우 tika-parsers를 사용하는 것보다 속도는 빠른 대신 약간 부정확할 수도 있다. 이 내용은 밑에서 다시 설명하겠다.
나는 확장자 체크 기능만 필요했으므로 tika-core를 사용했다.
Tika File Detect
MediaType detect(java.io.InputStream input,
Metadata metadata) throws java.io.IOException
tika는 파일 형식을 확인할 때 Detector라는 인터페이스를 구현한 객체의 detect 메서드를 사용한다. 두 번째 인자에 MetaData는 Map형식이다.
파일 형식 확인을 위해서 사용하는 key는 일반적으로 2개로 Metadata.RESOURCE_NAME_KEY, Metadata.CONTENT_TYPE가 있다.
Metadata.RESOURCE_NAME_KEY는 파일명의 확장자, Metadata.CONTENT_TYPE은 파일의 content-type이다.
tika는 Metadata에 담긴 이 두가지 정보를 활용해서 파일 형식을 체크한다.
그런데 만약, 사용자가 확장자를 임의로 조작한 경우를 생각해보자. 웹에서 java.class파일을 java.txt 파일로 변경해 content-type=application/octet-stream으로 보낼 경우 어떻게 될까?
metadata 정보만으로는 class파일임을 판별하기 어려울 것이다. 위변조를 확인하기 위해서 tika는 magic 패턴을 사용해서 파일 형식을 확인한다.
MIME Magic Detection
tika는 파일 시작 부분의 byte magic pattern을 읽어서 MIME 타입을 확인한다. 대부분의 파일은 이 방식으로 파일의 진짜 형식을 확인할 수 있다. 그러나 물론 예외도 존재한다.
tika에서는 파일 종류를 5가지로 구분한다.
- Simple Document
- Structured Document
- Compound Document
- Simple Container
- Container with Text
이 중 Compound Document(xlsx...)와 Simple Container(zip...)는 파일 내부적으로 여러 content type을 가질 수 있는 구조이기 때문에 단순히 파일의 시작부분을 읽는 tika-core magic Detection으로는 부정확하다. 파일 내용을 확인할 필요가 있다는 뜻이다. 고로, 정확한 Compound Document, Container 파일 타입 확인을 위해서는 위에서 언급했던 tika-parsers 라이브러리가 필요하다.
@Test
@DisplayName("확장자 변경되지 않은 Compound 파일의 미디어 타입을 정확히 읽을 수 있다.")
void t3() throws IOException {
File pptFile = new ClassPathResource("powerpoint.pptx").getFile();
String detectedMediaType = tika.detect(pptFile);
assertThat(detectedMediaType).isEqualToIgnoringCase("application/vnd.openxmlformats-officedocument.presentationml.presentation");
}
@Test
@DisplayName("확장자 위조된 Compound 파일(.png)의 실제 미디어 타입(.pptx)은 대략 읽을 수 있다.")
void t4() throws IOException {
File pptFile = new ClassPathResource("powerpoint.png").getFile();
String detectedMediaType = tika.detect(pptFile);
assertThat(detectedMediaType).isNotEqualToIgnoringCase("application/vnd.openxmlformats-officedocument.presentationml.presentation");
assertThat(detectedMediaType).isEqualToIgnoringCase("application/x-tika-ooxml");
}
위의 테스트 코드처럼 tika-core만 사용할 경우 확장자 위조된 pptx파일을 application/x-tika-ooxml이라는 다소 광범위한 tika mime타입을 반환한다. tika-parsers 라이브러리를 쓸 경우 위조된 파일 mime타입 또한 application/vnd.openxmlformats-officedocument.presentationml.presentation을 반환한다.
Default Tika Detector
위의 테스트 코드에서 tika.detect()라는 메소드를 사용했는데, tika에서 Facade 패턴으로 제공해주는 메서드다. 이 메서드 인자는 File, InputStream, byte 등 다양하게 오버 로딩되어있다. 그래서 사용자는 단순히 new Tika()로 인스턴스 생성 후에 detect 메서드를 사용하면 된다.
public Tika() {
this(TikaConfig.getDefaultConfig());
}
public String detect(File file) throws IOException {
Metadata metadata = new Metadata();
try (InputStream stream = TikaInputStream.get(file, metadata)) {
return detect(stream, metadata);
}
}
그럼 Tika 생성자에서 자동으로 Default Tika Detector를 생성해주고, 사용자는 별다른 설정 없이 바로 파일 형식을 확인할 수 있는 것이다.
기본적으로 tika-core에서는 DefaultDectector에는 Mime Magic Detector, Resource Name Detector 총 2개의 Detector가 사용된다. 이 Detector들은 MimeTypes 클래스를 통해 파일 타입 확인을 위한 설정 파일을 읽어오는데 tika에서 기본으로 정의한 tika-mimetypes와 사용자가 정의하는 custom-mimetypes를 읽는다.
types = MimeTypesFactory.create(
"tika-mimetypes.xml", "custom-mimetypes.xml", classLoader);
<!--tika-mimetypes.xml-->
<mime-type type="image/png">
<acronym>PNG</acronym>
<_comment>Portable Network Graphics</_comment>
<magic priority="50">
<match value="\x89PNG\x0d\x0a\x1a\x0a" type="string" offset="0"/>
</magic>
<glob pattern="*.png"/>
</mime-type>
xml 파일에 정의된 정보를 바탕으로 파일 형식을 확인하는 순서는 아래와 같다.
- Mime Magic Detector로 <magic> 규칙 일치 여부 확인
- Metadata에 RESOURCE_NAME_KEY 있으면 Resource Name Detector로 <glob> 패턴 일치 여부 확인
- Metadata에 CONTENT_TYPE이 있으면 도출된 <mime-type>과 상응하는 확인
Custom MIME Type
custom-mimetypes.xml에서 사용자 임의의 MIME 타입을 추가할 수도 있다.
아래한글파일(*.hwp)의 경우 한국에서만 쓰이기 때문에 IANA에 등록되어 있지 않다.
아래한글파일을 검사할 경우 application/x-tika-msoffice으로 나오는데 이를 application/hwp로 바꿔보려 한다.
<?xml version="1.0" encoding="UTF-8"?>
<mime-info>
<mime-type type="application/hwp">
<glob pattern="*.hwp"/>
<sub-class-of type="application/x-tika-msoffice"/>
</mime-type>
</mime-info>
커스텀하는 것은 간단하다. classpath:/org/apache/tika/mime 경로에 custom-mimetypes.xml을 생성하고 위와 같이 작성하면 된다. 간단히 설명하자면 tika-mimetypes.xml에서 MagicDetector와 NameDetector로 <mime-type> type이 "application/x-tika-msoffice"을 확인한 파일 중에 확장자가 hwp라면 application/hwp를 반환한다.
@Test
@DisplayName("custom-mimetype애 추가한 hwp 파일을 읽을 수 있다.")
void t6() throws IOException {
File hwpFile = new ClassPathResource("한글.hwp").getFile();
String detectedMediaType = tika.detect(hwpFile);
assertThat(detectedMediaType).isEqualToIgnoringCase("application/hwp");
}
@Test
@DisplayName("한셀 파일을 읽을 수 있다.")
void t7() throws IOException {
File hanCellFile = new ClassPathResource("한셀.Cell").getFile();
String detectedMediaType = tika.detect(hanCellFile);
assertThat(detectedMediaType).isEqualToIgnoringCase("application/x-tika-ooxml");
}
'Backend > Java' 카테고리의 다른 글
Spring Boot 2.4.x 이슈 (0) | 2022.07.08 |
---|---|
UUID, 정말 안전할까? (2) | 2021.11.06 |
YAML) @PropertySource에서 EnvironmentPostProcessor까지 (0) | 2021.07.03 |
SpringBoot profile logback (0) | 2021.06.24 |
Java Stream과 Multi Thread (2) | 2021.04.17 |