Jeen - Yet anothere techlog

STFUAWSC

Export Github Issues

회사에서는 한달에 $20 인가 지불하면서 Github 을 쓰고 있습니다.

$20 플랜에서는 한정적인 Private Repo 밖에 만들지 못합니다. 그래서 완료가 된 프로젝트에 대해서는 Github Repo 를 닫고 사내 Git Repo 로 옮기는 방식으로 사용하고 있습니다.

그럴거면 그냥 사내 Git Repo 를 사용할 것이지 왜 돈내가면서 Github 를 쓰느냐고 물으신다면, Github 에서 제공해주는 Issue 관리라든가, Wiki 라든가 여러모로 통합이 잘되어 있고 깔끔해서 사용하기 편하다고 그냥 그렇게 말하렵니다.

아무튼 그러면서, 프로젝트 뿐 아니라 회사내에 발생하는 다양한 이슈들은 Bugzilla 에 기록해놓습니다.

다 끝나서 닫아야할 프로젝트들을 사내 Repo 로 옮기는 것은 좋지만, Issues 안에 적어둔 깨알같은 내용들과 코멘트들 등등을 고스란히 다 허공으로 날려버려야 된다는 것은 뼈아픈 일입니다.

그래서 잠깐 시간을 내서 Issues 에 있는 내용(이슈타이틀,내용,코멘트, Git commit, 이슈 참조 등등)을 뽑아서 Bugzilla 에 담아놓기 좋게 코드를 써봤습니다.

~~~ perl export-github-issues.pl use strict; use warnings; use Text::Xslate; use Data::Section::Simple; use Net::GitHub::V3; use Config::Pit;

my $pit = pit_get(‘github.com’, require => {

login => "Your login",
pass  => "Your pass",

});

my $github = Net::GitHub::V3->new(

login => $pit->{login},
pass  => $pit->{pass},

);

my ($user, $repo) = @ARGV; unless ($user && $repo) {

print "Usage: perl p.pl silexkr Donnenwa\n";
exit;

}

my $tx = Text::Xslate->new(

syntax => 'TTerse',
module => \[ 'Text::Xslate::Bridge::TT2Like' \],
path => \[
    Data::Section::Simple->new()->get_data_section()
\],

);

my $issue = $github->issue; my @issues = $issue->repos_issues($user, $repo, { state => ‘closed’ }); while($issue->has_next_page) {

push @issues, $issue->next_page;

}

for my $iss (@issues) {

my @comments = ();
my @events   = ();
eval { @comments = $issue->comments('silexkr', $repo, $iss->{number}) };
eval { @events   = $issue->events('silexkr', $repo, $iss->{number}) };
print $tx->render('text.tx', { %{ $iss }, comments => \@comments, events => \@events });

}

DATA

@@ text.tx

[% number %].[% title %] [% FOREACH label IN labels %][% label.name %] [% END %] – [% user.login %] – [% created_at %] ~ [% closed_at %] [% IF body %] // [% body %] [% END %] [% FOREACH comment IN comments %] \= [% comment.user.login %] @ [% comment.updated_at %] — [% comment.body %] [% END %] [% FOREACH event IN events %][% NEXT UNLESS event.commit_id %] \= [% event.commit_id %][% END %] ~~~

Net::GitHub 모듈에서 뭔가 코멘트를 뽑아내려는 데 없으면 계속 죽어버리는 문제 등등이 있어서 뭐 그냥 eval {} 로 묶어버렸구요.

Text::Xslate 에서 path

perl Data::Section::Simple->new()–>get_data_section();

으로 해서 쉽게 ->render() 의 첫번째 인수를 템플릿이름으로 지정할 수 있더군요.

Net::GitHub->next_page 가 최근 최근에 사용된 API 기준으로 동작을 하다보니, while() 안에서 events, comments 를 혼용해서 사용해버리면 의도한 대로 동작을 하지 않아서, @issues 안에 일단 전체 이슈를 뽑고서 시작을 했습니다.

아무튼 뭐 얼기설기 그냥 놔두기도 그렇고 혹여나 해서 일단 올려봅니다.

HTTPS-SSL-LWP-SOAP and Perl

tl;dr: I hate Java and SOAP, use Perl

지난 주에 고객으로부터 요구를 받았는데, DB특정 항목에 대치되는 어떤 값을 가지고 모 API를 통해서 결과를 받아서 뿌려주는 간단한 것이었습니다.

그 API 제공회사에서 날라온 API 명세서는 뭘 해도 자바였었죠. 샘플이라고 널려놓은 두세네개 코드도 전부 자바였습니다. 해야하는 건 결국 뭐 SOAP 정도였죠.

명세서에는 Axis 라는 것을 깔아서 뭐 붙여서 어떻게 뭐 wsdl 붙이고 어쩌고 복잡하게 써놨는데…

결론부터 말하면 endpoint 와 보낼 값들만 필요하면 자바가 무슨 소용이냐 싶어서 집어냈습니다.

~~~ perl use strict; use warnings; use LWP::UserAgent; use Data::Dumper;

my $q = { <?xml version=“1.0” encoding=“UTF-8”?> <soapenv:Envelope xmlns:soapenv=“http://schemas.xmlsoap.org/soap/envelope/” xmlns:xsd=“http://www.w3.org/2001/XMLSchema” xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance”> <soapenv:Body> ……… </soapenv:Body> };

my $ua = LWP::UserAgent->new; my $res = $ua->post(‘https://……..’, ‘Content-Type’ => ‘text/xml; charset=utf-8’, Content => $q, );

print Dumper $res->content; ~~~

우선 SOAP XML 내용 및 endpoint 는 해당업체 비밀자료이기에 표시하지 않습니다.

위의 코드의 결과는

~~~ bash Can\’t connect to demo-asp.ysdasp-service.ne.jp:443 (certificate verify failed)

LWP::Protocol::https::Socket: SSL connect attempt failed with unknown error error:14090086:SSL routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed at …/perl-5.14.2-llvm/lib/site_perl/5.14.2/LWP/Protocol/http.pm line 51. ~~~

이라고 나온 것이었습니다. 유효하지 않은 인증서라는 데, 브라우저에서는 그냥 무시하고 지나가버리면 되는 데, Perl 의 경우에서는 어떻게 할까… 결론은 구글해봤습니다.

perl $ENV{‘PERL_LWP_SSL_VERIFY_HOSTNAME’} = 0;

PERL_LWP_SSL_VERIFY_HOSTNAME 이라는 환경변수의 값을 0으로 지정함으로써 LWP 가 인증서의 유효성 체크를 생략시켜버립니다.

이제는 되겠지 하고 실행을 시켜보니…

xml <?xml version=“1.0” encoding=“UTF-8”?> <soapenv:Envelope xmlns:soapenv=“http://schemas.xmlsoap.org/soap/envelope/” xmlns:xsd=“http://www.w3.org/2001/XMLSchema” xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance”> <soapenv:Body> <soapenv:Fault> ns1:Client.NoSOAPAction no SOAPAction header! </soapenv:Fault> </soapenv:Body> </soapenv:Envelope>

이라는 결과가 나옵니다. SOAPAction 이라는 헤더가 없다고 하는데… SOAP 통신에는 SOAPAction 이라는 헤더를 넣는다고… 그냥 해보니 SOAPAction 에 그냥 빈값이라도 헤더이름만 넣고 돌려보면 되겠구나 라는 생각이 들었습니다.

perl my $res = $ua->post(‘https://……..’, ‘Content-Type’ => ‘text/xml; charset=utf-8’, SOAPAction => ‘“”’, Content => $q, );

POST 시에 SOAPAction 헤더를 넣어주고 다시 돌려보았습니다.

~~~ xml <?xml version=“1.0” encoding=“UTF-8”?> <soapenv:Envelope xmlns:soapenv=“http://schemas.xmlsoap.org/soap/envelope/” xmlns:xsd=“http://www.w3.org/2001/XMLSchema” xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance”> <soapenv:Body> <soapenv:Fault> soapenv:Server.userException org.xml.sax.SAXParseException: The processing instruction target matching "[xX][mM][lL]" is not allowed. </soapenv:Fault> </soapenv:Body> </soapenv:Envelope> “`

다시 해당 faultstring 으로 검색을 해본 결과, 이유는 <?xml …?> 앞에 빈 공백값이 있으면 안된다는 것이었습니다.

perl my $q = q{<?xml version=“1.0” encoding=“UTF-8”?> <soapenv:Envelope xmlns:soapenv=“http://schemas.xmlsoap.org/soap/envelope/” xmlns:xsd=“http://www.w3.org/2001/XMLSchema” xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance”> <soapenv:Body> ……… </soapenv:Body> };

맨 위의 코드에서 q{ 다음에 개행을 넣은 게 문제가 되어서 다시 지우고 돌려본 결과 좌르르르륵 펴쳐져 나오는 기나긴 XML의 향연…

아무튼 이걸로 API 제공업체에서 JAVA 를 기준으로 작성된 길고 긴 명세서들과 설치요령 등등의 문서들을 제쳐두고 해야할 일에만 집중할 수 있게 되었습니다.

Regexp::Wildcards

저같은 평범한 개발자야 어떤 패턴을 확인하고 정규표현식을 쓰면 되는데, 사실 일반 사용자들에게 정규표현식을 어떤 설정의 입력값으로 밀어넣는 것은 일반 사용자에게 배려가 부족하지 않나 하고 생각했었습니다.

  • /path/a/b
  • /path/a/c
  • /path/a/d

/path/a/ 디렉토리 아래의 b,c,d 파일들을 삭제한다고 합니다. 물론 b,c,d 밖에 없다고 합니다. 이럴때 보통 모두가 대개 와일드카드를 사용하죠.

bash $ rm -rf /path/a/*

라고 말이죠.

예를들어서 어떤 관리자페이지 아래에서 특정 유저그룹의 접속권한에 대해서 다룬다고 해봅니다. 특정그룹은 고정IP를 통해서 접근해올 수 있고, 또 다른 그룹은 특정 IP대역에서 찾아온다고 합니다. 이런 것들을 복잡한 설정파일을 통해서가 아니라 유저가 관리하기 쉬운 인터페이스를 제공해야 할 필요도 있죠.

일반적인 입력예는 아래와 같습니다.

  • 211.123.123.1-211.123.123.254
  • 211.123.122.5
  • 211.123.121.4, 211.123.121.8

적당한 IP대역이거나, 혹은 단일 IP라거나, 혹은 몇몇 패턴을 알 수 없는 복수개의 IP라거나…

  • 211.123.126.0/24

혹은 네트워크 관리자라면 CIDR 스타일을 사용할지도 모르겠습니다.

  • 211.123.128.\*
  • 211.123.121.10\*

아니면 위에서 말한 대로 와일드카드를 사용할 수 있겠죠.

우선 위의 네개째까지는 Net::IPAddrRanges 라는 모듈을 사용하면 편하게 이용할 수 있습니다.

~~~ perl use strict; use warnings; use Net::IPAddrRanges;

my $ranges = Net::IPAddrRanges->new; $ranges->add(qw{

211.123.123.1-211.123.254
211.123.122.5
211.123.121.4 211.123.121.8
211.123.126.0/24

});

$ranges->find(‘211.123.123.5’); # OK; ~~~

이런저런 다양한 입력값들을 받아서 Net::IPAddrRanges 모듈에 넘겨주고 ->find 메소드를 통해서 접속해온 IP(211.123.123.5)가 접속가능한 IP인지를 위처럼 간단하게 확인할 수 있죠.

와일드카드 사용시에는 이와는 좀 다릅니다.

~~~ perl use strict; use warnings; use Regexp::Wildcards;

my $rw = Regexp::Wildcards->new( type => ‘unix’ );

my $user_ip = ‘211.123.121.108’;

if (scalar grep { $user_ip =~ $_ }

       map { $rw->convert($_) } qw{ 211.123.128.* 211.123.121.10\* }) {
print "OK";

} ~~~

뭐 사실 이런 코드를 적당한 메소드로 만들어서 사용하고 있습니다. IP제한같은 시나리오에는 사실 Net::IPAddrRanges 를 사용하는 게 좋아보이기는 합니다만… 최근에 Regexp::Wildcards 를 사용하는 경우가 있었는데…

유저의 권한별로 페이지의 접근을 제어하고 싶다라는 것이었죠.

yaml ROLE1: [ /admin/user/list, /admin/user/*/download ] ROLE2: [ /admin/user/* ]

ROLE1 의 경우에는 admin/user 라는 컨트롤러중에 유저 리스트(/admin/user/list) 와 각 유저별로 관련정보를 다운로드를 할 수 있습니다. 그 이외에는 전부 접근을 차단한다는 것이구요.

ROLE2 의 경우에는 admin/user 의 모든 액션에 접근을 가능하게 합니다.

이때는 카탈리스트를 사용하고 있다는 전제아래, Admin.pm 이라는 컨드롤러파일의 auto 아래 다음과 같은 코드를 추가했습니다.

~~~ perl sub auto :Private { ….

my $rw = Regexp::Wildcards->new( type => 'unix' );

my $ACL = ...; # 어떻게 샤바샤바해서 유저권한에 따른 접근가능 목록을 뽑아옵니다

unless (scalar grep { $c->action =~ $_ } map { $rw->convert($_); } @{ $ACL }) {
    $c->flash( warn_messages => \[ '접속할 수 없어요.' \] );
    $c->res->redirect($c->uri_for("/admin"));
    return 1;
}

…. } ~~~

네 아무튼 뭐 사실 결론은 굳이 할 수 있다면 간단하게 설정할 수 있는데, 왜 설정파일에까지 정규표현식이 끼어들어야 하느냐 라는 생각아래에서 시작했었지요. 정규표현식으로 난무할 이런저런 상황들을 적당하게 와일드카드로 커버할 수 있다면 더 직관적으로 이해하고 사용할 수 있지 않을까 하는 생각이 듭니다.