ip-filter-core 모듈은 아래와 같이 Config 객체를 이용해서 차단/허용 목록을 설정한다. (ip-filter의 사용법은 https://github.com/madvirus/ip-filter/wiki/HOME_kr 문서를 참고하기 바란다.)
Config config = new Config();
config.setAllowFirst(true);
config.setDefaultAllow(false);
config.allow("1.2.3.4"); // 허용 IP 추가
config.allow("10.20.30.40");
config.deny("101.102.103.104"); // 차단 IP 추가
IpFilter ipFilter = IpFilters.create(config);
ipFilter.accept("1.2.3.4"); // true
ipFilter.accept("101.102.103.104"); // false
코드로 설정하는 것이 필요할 때가 있지만, 아래와 같은 문자열을 이용할 수 있다면 파일이나 운영툴 등에서 쉽게 설정할 수 있을 것이다.
# 주석
order allow,deny
default true
allow from 1.2.3.4
allow from 1.2.5.* # 뒤에 주석
allow from 201.202.203.10/64
deny from all
위 문자열을 읽어와서 Config 객체를 생성하려면 문자열을 알맞게 파싱해 주어야 하는데, 파싱을 어떻게 할까 고민하다가 최근에 학습하고 있는 스카라(Scala)의 콤비네이터 파서(Conbinator parser)를 사용해 보기로 했다. 스카라는 자체적으로 Context-free grammer 에 따른 파싱 기능을 제공하고 있기 때문에, ANTLR과 같은 별도의 파서 생성기를 사용하지 않아도 된다.
스카라를 이용한 문자열 파서
음,, 먼저 스카라에 익숙하지 않은 분들은 아래 코드가 잘 이해되지 않을 것이다. 그래도 그냥 읽어보기 바란다. 설정 문자열을 파싱하기 위해 작성한 스카라 코드는 아래와 같다. (아래 코드는 order나 default를 여러 줄 입력해도 에러를 발생시키진 않는데, order나 default를 여러번 설정할 경우 오류를 발생시킬지의 여부는 고민 중에 있다.)
class Conf extends JavaTokenParsers {
override val whiteSpace = """[ \t]+""".r
def conf: Parser[Config] = repsep(confPart, eol) ^^ (
x => {
val config = new Config
x.foreach(part =>
part match {
case ("order", firstAllow: Boolean) => config.setAllowFirst(firstAllow)
case ("default", defaultAllow: Boolean) => config.setDefaultAllow(defaultAllow)
case ("allow", ip: String) => config.allow(ip)
case ("deny", ip: String) => config.deny(ip)
case _ =>
})
config
}
)
def confPart: Parser[Any] = commentPart | orderPart | defaultPart | allowOrDenyPart | emptyLine
def commentPart: Parser[String] = """#(.*)""".r ^^ (x => x)
def orderPart: Parser[Tuple2[String, Boolean]] =
"order" ~ orderValue ~ opt(commentPart) ^^ (x => ("order", x._1._2))
def orderValue: Parser[Boolean] = {
"allow" ~ "," ~ "deny" ^^ (x => true) |
"deny" ~ "," ~ "allow" ^^ (x => false)
}
def defaultPart: Parser[Tuple2[String, Boolean]] =
"default" ~ booleanValue ^^ (x => ("default", x._2))
def booleanValue: Parser[Boolean] = "true" ^^ (x => true) | "false" ^^ (x => false)
def allowOrDenyPart: Parser[Tuple2[String, String]] =
allow ^^ (x => ("allow", x)) | deny ^^ (x => ("deny", x))
def allow: Parser[String] = "allow" ~ "from" ~ ipPattern ~ opt(commentPart) ^^ (x => x._1._2)
def deny: Parser[String] = "deny" ~ "from" ~ ipPattern ~ opt(commentPart) ^^ (x => x._1._2)
def ipPattern: Parser[String] =
"all" ^^ (x => "*") |
"""(\d+\.){1,3}(\*)""".r ^^ (x => x) |
"""(\d+\.\d+\.\d+\.\d+\/\d+)""".r ^^ (x => x) |
"""(\d+\.\d+\.\d+\.\d+)""".r ^^ (x => x)
def emptyLine: Parser[String] = ""
def eol: Parser[String] = """(\r?\n)+""".r
}
class ConfParser extends Conf {
def parse(confText: String): Config = {
val result = parseAll(conf, confText)
if (result.successful)
result.get
else
throw new ConfParserException(result.toString)
}
}
Conf 클래스는 문자열을 파싱해서 Config 객체를 생성해주는 기능을 제공하며, ConfParser 클래스는 외부에 파싱 기능을 제공하는 parse() 메서드를 제공한다.
자바 코드에서는 ConfParser 클래스를 이용해서 문자열로부터 Config 객체를 생성할 수 있다.
public class UsingConfParserTestInJava {
@Test
public void useConfParser() {
String confValue =
"order deny,allow\n" +
"allow from 1.2.3.4\n" +
"deny from 10.20.30.40\n" +
"allow from 101.102.103.*\n" +
"allow from 201.202.203.10/64";
Config config = new ConfParser().parse(confValue);
assertFalse(config.isAllowFirst());
assertEquals(config.getAllowList().size(), 3);
}
응용
문자열로부터 Config 객체를 생성하는 파서를 만들었으니, 이제 파일이나 다른 곳에서 설정 데이터를 읽어와 Config 객체를 만들 수 있게 되었다. 실제 응용은 웹 어플리케이션에서 특정 IP 차단을 위해 작성한 ip-filter-web-simple 에서 사용하였다. (관련 코드는 https://github.com/madvirus/ip-filter/tree/master/ip-filter-web-simple 에서 확인 가능)
package org.chimi.ipfilter.web.impl;
import org.chimi.ipfilter.Config;
import org.chimi.ipfilter.parser.ConfParser;
import java.io.FileReader;
import java.io.IOException;
public class FileConfigFactory extends ConfigFactory {
@Override
public Config create(String value) {
return new ConfParser().parse(readFromFile(value));
}
private String readFromFile(String fileName) {
try {
return IOUtil.read(new FileReader(fileName));
} catch (IOException e) {
throw new ConfigFactoryException(e);
}
}
@Override
public boolean isReloadSupported() {
return true;
}
}