웹페이지 개선

2018-05-25

들어가며

약 한 달 전부터 웹페이지가 조금씩 바뀌어 가고 있다. 전체적으로 아무 디자인도 없었던 페이지에 조금씩 새로 디자인을 입히고, 굉장히 플랫하게, 내가 별로 디자인할 것은 없이 프레임워크가 알아서 해 주도록 설정하고 있다. 개인정보취급방침도 새로 만들었고, 메인과 인소야닷컴 등 일부 단순 html들, 그리고 Apache directory listing의 결과에 디자인을 입혔다.

Markdown

사실 예전 Course 서비스를 만들 때부터, html을 직접 만드는 것은 너무나도 귀찮았다. 그 이후 알게 된 Markdown 문법만을 활용해 내가 페이지를 만들고 서버에서 알아서 html로 변환하여 보내주도록 한다면 정말 편할 것이라는 생각이 들었다.

처음에는 Showdown.js를 사용했다. 서버 사이드에서 해 줄 것은 하나도 없었다. md 파일과 해당하는 파일을 XHR로 불러오도록 하는 html만 제작하여 업로드하면 됐다. 그러나 이 방법에는 문제점들이 있었다. html 자체에 문서 내용이 포함된 것이 아니므로 외부에서 메타데이터를 가져가는 것이 불가능하다. 제목조차도 없었다. 처음에 이 방법으로 메인 index를 만들었더니 구글에서 크롤링을 못 하게 되는 상황이 발생했다. XHR로 요청을 하므로 약간의 시간이 더 필요한 것은 덤이었다. 변할 필요성이 있었다.

다음으로 생각한 것은 PHP를 통해 파싱해서 보내주는 것이다. Parsedown을 활용해 구현했다. 별도의 footer html을 만들고 아래에 붙이는 것까지 했다. Medium에서 쓰는 이미지 확대 퍼포먼스가 신기해서 해당 기능을 따라한 zoom.js도 적용했다.

한편 Parsedown은 표에서 colspan 속성을 지원하지 않는다. Course의 Grade 페이지에서는 해당 기능이 필요한데, 이를 지원하는 파서는 Multimarkdown이 있다. 아래와 같은 문법이다.

|>|a|

Multimarkdown은 하나의 독립적인 실행 파일로, 소스를 받아서 서버에서 컴파일해 보려고 했지만 그러려면 CMake가 필요했고 이게 메모리 문제인지 제대로 컴파일이 되지 않아서 실패했다. 만약 성공한다면 PHP에서 캐싱하는 기능을 만들어 보려고 했었다.

2018-06-14 추가

코드 하이라이팅을 해 보기로 했다. 코드 펜스 블록의 첫 줄에 언어를 써 주면 Parsedown에서는 이를 "language-" 뒤에 붙여서 class로 만들어 준다. 이 형식은 highlight.js에서 지원하는 형식과 완전히 동일하다. GitHub 테마를 적용할까 했지만 VS 테마가 더 예쁜 것 같다. VS 2015가 더 예쁘지만, 배경색을 바꾸는 방법을 잘 알 수 없어서 기본 VS 테마를 쓰기로 했다. 내가 코드 한 줄을 굉장히 길게 쓰는 습관이 있어서, wrap이라는 클래스를 추가하여 임의로 줄 바꿈을 추가할 수 있도록 했다. Parsedown에서 언어 이름에 공백이 들어가는지 신경을 안 쓰고 첫 줄에 있는 내용을 그대로 class에 붙이기에 가능했다.

2018-06-22 추가

한국정보올림피아드 개선방안 공청회에 오니 옆에 앉아계신 분이 날짜 형식을 보더니, ISO 표준에 맞지 않는다고 지적해주셨다. 찾아보니 ISO 8601에서 규정하기를 YYYY-MM-DD를 사용해야 한다. 따라서 CV, 블로그의 모든 글에서 해당 부분을 바꾸었다.

2018-07-15 추가

글에서 참조하는 파일들이 점점 늘어감에 따라 이를 한 폴더에 모두 몰아넣는 것이 이상해졌다. 블로그의 모든 글을 각각 폴더로 분리하기로 했다. 다행히 지금까지 쓴 글이 그렇게 많지 않아 일일이 손으로 한다. 이에 따라 글의 주소가 바뀐다. 다른 블로그 양식처럼 연도/월/일 형태의 주소에 대해서는 여전히 글쎄올시다 상태이다.

2018-07-26 추가

2018 SCPC 2차 예선 문제 아카이빙#을 하면서 MathJax 지원을 추가했다. 그냥 스크립트 하나만 추가하고 줄 내부이면 \(\), 별도 줄이면 \[\]로 감싸면 된다. 간편하다. 한편 저 백슬래시가 마크다운을 거쳐서 나와야 하므로 실제 파일에는 백슬래시를 두 개 써 줘야 한다. 예시는 다음과 같다.

\[x = {-b \pm \sqrt{b^2-4ac} \over 2a}\]

2018-08-14 추가

글을 대부분 Typora에서 쓰다 보니 아래아한글에서 뭔가를 쓰던 시절보다 맞춤법에 대한 개념이 희박해졌다. 사실 이전에 페이스북에 쓸 때부터 그랬다. Typora에 맞춤법 엔진을 어쩌고 하는 건 너무 어려우니 쓰고 나서라도 맞춤법 검사를 하기 위해 localhost에서만 body의 내용을 모두 긁어서 한국어 맞춤법/문법 검사기에 던져주는 버튼을 만들었다. 만들고 전체적으로 한 번 손을 본 결과 띄어쓰기가 엄청 많이 틀렸다. 아무래도 블로그 특성상 일반 글도 있지만, 전문 단어나 영어 단어를 쓸 일이 많은데 그런 것들까지 계속 지적하는 점은 좀 불편하다.

2018-08-29 추가

어쩌다 보니 거의 보름에 한 번꼴로 기능 업데이트를 하고 있다. 이제 콜론 형태의 이모지를 지원한다. 😄 js-emoji를 사용하여 구현했다. 그런데 이름에서 볼 수 있듯이 Javascript단이라 변환되기 전 원본 텍스트가 잠시 보이는 문제가 있다. PHP에서 바로 바꿔주면 좋겠지만 관련 라이브러리를 찾기 어렵다.

2018-10-07 추가

Parsedown에 rowspan, colspan 속성을 지원하는 extension을 만들었다. 관련 내용은 해당 글#에 있다.

2018-10-09 추가

이번에는 댓글 시스템을 추가했다. 관련 내용의 양이 좀 되어 페이지를 분리한다. 해당 글#에서 내용을 찾을 수 있다.

2018-11-13 추가

케이웹 스터디와 연계하여 AMP 버전을 추가했다. [이 페이지의 AMP 버전 보기] 관련 글#의 1부 내용이다.

2019-01-30 추가

PHP Simple HTML DOM Parser가 Selector에 >를 지원하지 않는다는 것을 깨달아서 고쳤다. 기여하고 싶기는 하지만 영 귀찮아 보인다.

2019-02-08 추가

그림에 캡션을 다는 기능을 추가했다. 자동으로 번호까지 매겨지므로 내가 일반 워드프로세서에서 글 쓸 때와 비슷하게 사용할 수 있다.

2019-02-20 추가

캡션을 그냥 <p> 태그로 달고 있었는데 HTML5를 활용하여 밖을 <figure>로 감싸고 <figcaption> 태그를 사용하도록 변경했다. AMP에서 뭔가 이상해서 여러 가지 시도해보니 <amp-img/>가 아닌 <amp-img></amp-img>로 반드시 써야 한다는 것을 알아냈다.

이모지 변환하는 부분을 Javascript단에서 PHP단으로 당겼다. 😃 그래서 이제 AMP에서도 이모지를 볼 수 있다. 여전히 좋은 라이브러리는 못 찾았다. 일단 데모 페이지#와 배열 파일#을 거의 그대로 가져다 썼다. 이제 AMP와 일반 페이지가 다른 것은 syntax highlighting과 utterances 댓글뿐이다.

2019-02-21 추가

이렇게 자주, 큰 규모로 업데이트한 것은 처음이다. 새로운 글을 쓰며 체크박스 기능이 필요해졌다. 이를 지원하는 ParsedownCheckbox라는 extension이 있으나, 내가 자체적으로 만든 ParsedownTablespan과 다중 상속 문제가 있어서 바로 사용하지 못했다. 이에 PHP의 trait 문법을 사용하여 해결했다. 다음과 같이 하면 된다.

trait ParsedownTablespanTrait
{
    protected function blockTableCompleteTrait(array $Block)
    {
        $md=new ParsedownTablespan();
        return $md->blockTableComplete($Block);
    }
}
trait ParsedownCheckboxTrait
{
    protected function blockListCompleteTrait(array $block)
    {
        $md=new ParsedownCheckbox();
        return $md->blockListComplete($block);
    }
}
class ParsedownFinal extends ParsedownExtreme
{
    use ParsedownTablespanTrait,ParsedownCheckboxTrait;
    protected function blockTableComplete(array $Block)
    {
        return $this->blockTableCompleteTrait($Block);
    }
    protected function blockListComplete(array $block)
    {
        return $this->blockListCompleteTrait($block);
    }
}

또는 고전적으로 다음과 같이 해도 된다.

class ParsedownFinal extends ParsedownExtreme
{
    private $tablespan,$checkbox;
    public function __construct()
    {
        $this->tablespan=new ParsedownTablespan();
        $this->checkbox=new ParsedownCheckbox();
    }
    protected function blockTableComplete(array $Block)
    {
        return $this->tablespan->blockTableComplete($Block);
    }
    protected function blockListComplete(array $block)
    {
        return $this->checkbox->blockListComplete($block);
    }
}

코드에도 있듯이 이번 기회에 아예 기반 클래스를 ParsedownExtra에서 ParsedownExtreme으로 바꾸었다. 이제 composer 관련해서 처리하면 공개하려 한다.

2019-02-24 추가

충분한 테스트를 거치지 않았더니 버그가 있었다. ParsedownExtreme으로 바꾸면서 Parsedown과 ParsedownExtra의 버전을 올렸더니 ParsedownTablespan이 제대로 동작하지 않았고, protected 함수에 대한 호출도 문제가 되었다. 일단 롤백하고 다음과 같이 바꾸었다. 역시 어느 언어에서든 Reflection은 대단하다.

class ParsedownFinal extends ParsedownExtra
{
    private $tablespan_object,$tablespan_method,$checkbox_object,$checkbox_method;
    public function __construct()
    {
        $this->tablespan_object=new ParsedownTablespan();
        $this->tablespan_method=new ReflectionMethod('ParsedownTablespan','blockTableComplete');
        $this->tablespan_method->setAccessible(true);
        $this->checkbox_object=new ParsedownCheckbox();
        $this->checkbox_method=new ReflectionMethod('ParsedownCheckbox','blockListComplete');
        $this->checkbox_method->setAccessible(true);
    }
    protected function blockTableComplete(array $Block)
    {
        return $this->tablespan_method->invoke($this->tablespan_object,$Block);
    }
    protected function blockListComplete(array $block)
    {
        return $this->checkbox_method->invoke($this->checkbox_object,$block);
    }
}

날짜를 그동안은 그냥 <div> 안에 써 놓았는데 <date> 안에 쓰도록 하고 CSS를 입혔다. 그동안 쓴 글을 일괄적으로 바꾸기 위해서 아래와 같은 명령을 사용했다.

grep -rl "<div style=""text-align: right"">" . | xargs sed -i "s/<div style=""text-align: right"">\(201.-..-..\)<\/div>/<date>\1<\/date>/" *.md

2021-07-17 추가

한참 글감만 마련해두고 작성하지 않다가 2021 SCPC 1차 예선 풀이 글#을 작성하며 몇 가지 업데이트했다.

일단 추가 날짜 표시를 드디어 ISO로 바꿨다. 2년 만의 업데이트라 월일만 적으면 헷갈릴 때가 되었다.

아무래도 개발 관련 내용이 많다 보니 code fence를 쓸 일이 많고 여기는 코드가 많이 들어가는데, localhost에서 맞춤법 검사를 해보려고 할 때 code fence의 내용은 맞춤법 검사를 할 일이 단 하나도 없어 제외하도록 변경했다. 전에는 한 번에 어절 500개씩은 보여줬던 것 같은데 지금은 300개라 코드 한 번 들어가면 너무 페이지가 많아진다.

var child=document.getElementsByClassName("markdown-body")[0].firstChild;
while(child)
{
    if(child.nodeType===Node.ELEMENT_NODE&&child.tagName!=="PRE")text1=text1+child.innerText;
    else if(child.nodeType===Node.TEXT_NODE)text1=text1+child.data;
    child=child.nextSibling;
}

<details><summary>를 알고는 있었는데, 그 안에 보통 넣고 싶은 것이 code fence였는데 넣으면 항상 깨졌다. HTML 구조를 Parsedown에서 파싱하면서 깨지는 것으로 추정되는데 고치려면 복잡할 것 같아서 편법을 쓰기로 했다. <details> 블록 안에 들어가는 것이 문제라 <summary>는 그대로 사용하고 그 바로 앞에서 <details>가 열린다고 가정했다. </details>는 닫고 싶은 곳에 그대로 쓰되, 파싱 결과 <p> 안에 감싸지므로 이를 다시 풀어줬다.

$html=str_replace("<summary>","<details><summary>",$html);
$html=str_replace("<p></details></p>","</details>",$html);

2022-02-13 추가

어느새 도메인 만료 시기가 다가와 옮길 준비를 하면서 git으로 관리하기 시작했다. Dependency들을 일단 현 상황으로 한 번 commit 해두고, git submodule로 옮겼다. 그 과정에서 ParsedownTablespan이 Parsedown 1.8.0 Beta와 호환되도록 옛날에 준비해뒀던 것을 배포하지 않았다는 것을 깨달았다.

그 후 살펴보니 ParsedownExtended(구 ParsedownExtreme)가 ParsedownTablespan의 코드도 포함하고 있었다. 호환 패치도 내가 아마 더 먼저 했을 텐데 릴리즈가 늦어 증명할 방법은 없다. 역시 배포는 제때제때 해야 한다. 이 라이브러리를 쓰면 checkbox와 이모지까지 한 방에 해결되어 편리해 보이는데 리스트에서 링크를 제대로 못 보여주는 문제가 있어 아직 사용은 못 하고 있다.

기존에는 URL을 포함할 때 반드시 링크 형태인 http://XXX 처럼 적어주고 있었는데 Typora 및 여러 parser에서 자동 링크를 지원함에 따라 번거로운 작업을 없애기로 했다. 기존 글 마이그레이션을 위해 다음 명령을 사용했다.

find -iname '*.md' | tr '\n' '\0' | xargs -0 sed -i -E 's/\[(http.*)]\(\1\)/\1/g'

2022-03-20 추가

드디어 마무리. 도메인 만료까지 5일이 남아 있는 상황에서 겨우 끝냈다.

일단 서버 세팅부터 이야기하자면 Oracle Cloud에서 무료 제공하는 ARM 인스턴스에 CentOS 계열의 Oracle Linux 8을 사용 중이다. Apache2와 PHP를 직접 깔지는 않겠다는 의지 하에 podman(docker와 비슷)을 활용했다.

처음에는 httpd + php 2개로 하려고 httpd를 일부 세팅했는데, php 페이지를 열고 읽어보니 php-apache 이미지가 있어서 httpd 세팅을 버리고 다시 php-apache를 설정했다. HTTPS까지 적당히 a2enmod로 설정하긴 했는데, 이게 mpm_prefork라 HTTP2 활성화가 안되고 mpm_worker나 mpm_event로 바꾸면 PHP threading 설정과 맞지 않아서 실행하지 못하는 상황이 되었다. 이에 다시 버리고 httpd + php-fpm으로 사용했다. 다시 httpd 세팅하려니 이번에는 a2enmod가 없어서 설정 파일을 일일이 구성하는 번거로움이 조금 있었다. socket 공유도 권한 문제로 조금 헤맸지만 잘 되었다.

직접 제공하니 이제 SSL 인증서 체인도 잘 제공할 수 있었고 SSL Server Test에서도 A 받을 수 있도록 상세 조정했다. 잘은 모르겠지만 A+ 받으려면 HSTS 설정해야 하는 느낌이던데, 그다지 좋아하는 옵션은 아니라 넘겼다.

상세 설정 스크립트 보기
## php-apache (deprecated)
# sudo mkdir /www
# sudo chown opc:opc /www

# podman run --rm -v /www/:/tmp/:z php:8.1.4-apache cp -r /etc/apache2 /tmp
# #podman run --rm -v /www/apache2/:/etc/apache2/:z php:8.1.4-apache a2dismod mpm_prefork
# podman run --rm -v /www/apache2/:/etc/apache2/:z php:8.1.4-apache a2enmod ssl #mpm_event http2
# podman run --rm -v /www/apache2/:/etc/apache2/:z php:8.1.4-apache a2ensite default-ssl
# podman run --rm -v /www/:/tmp/:z php:8.1.4-apache cp -r /usr/local/etc/php /tmp
# cp /www/php/php.ini-production /www/php/php.ini

# # https://ssl-config.mozilla.org/#server=apache&version=2.4.52&config=intermediate&openssl=1.1.1n&hsts=false&ocsp=false&guideline=5.6
# sed -i -e 's|^\(.*SSLProtocol.*\)all -SSLv3$|\1all -SSLv3 -TLSv1 -TLSv1.1|' \
#     -e 's|^\(.*SSLCipherSuite.*\)HIGH:!aNULL$|\1ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384|' \
#     /www/apache2/mods-available/ssl.conf
# sed -i -e 's|^\(.*SSLCertificateFile.*\)/etc/ssl/certs/ssl-cert-snakeoil.pem$|\1/etc/ssl/certs/cert.pem|' \
#     -e 's|^\(.*SSLCertificateKeyFile.*\)/etc/ssl/private/ssl-cert-snakeoil.key$|\1/etc/ssl/private/privkey.pem|' \
#     -e 's|^\(.*\)#\(SSLCertificateChainFile.*\)$|\1\2|' \
#     /www/apache2/sites-available/default-ssl.conf

# podman run -d --name php-apache -p 8080:80 -p 8443:443 -v /www/apache2/:/etc/apache2/:z -v /www/php/:/usr/local/etc/php/:z -v /www/ssl/cert.pem:/etc/ssl/certs/cert.pem -v /www/ssl/privkey.pem:/etc/ssl/private/privkey.pem -v /www/ssl/chain.pem:/etc/apache2/ssl.crt/server-ca.crt --restart unless-stopped php:8.1.4-apache

sudo mkdir /httpd-data
sudo chown opc:opc /httpd-data

# php-fpm
podman run --rm -v /httpd-data/:/tmp/:z php:8.1.4-fpm cp -r /usr/local/etc/php-fpm.d /tmp
sed -i 's|^listen = .*$|listen = /var/run/php-fpm.sock|' /httpd-data/php-fpm.d/zz-docker.conf
echo 'listen.mode = 0666' >>/httpd-data/php-fpm.d/zz-docker.conf

podman volume create phpfpm
podman run -d --name svc-phpfpm -v /httpd-data/php-fpm.d/:/usr/local/etc/php-fpm.d/:z -v /httpd-data/htdocs/:/usr/local/apache2/htdocs/:z -v phpfpm:/var/run --restart always php:8.1.4-fpm php-fpm

# httpd
podman run --rm -v /httpd-data/:/tmp/:z httpd:2.4.53 cp -r /usr/local/apache2/conf /tmp

sed -i -e 's/^#\(LoadModule .*mod_rewrite.so\)/\1/' \
    -e 's/^#\(LoadModule .*mod_proxy.so\)/\1/' \
    -e 's/^#\(LoadModule .*mod_proxy_fcgi.so\)/\1/' \
    -e 's/^#\(Include .*httpd-ssl.conf\)/\1/' \
    -e 's/^#\(LoadModule .*mod_ssl.so\)/\1/' \
    -e 's/^#\(LoadModule .*mod_socache_shmcb.so\)/\1/' \
    -e 's/^#\(LoadModule .*mod_http2.so\)/\1/' \
    /httpd-data/conf/httpd.conf
cat >>/httpd-data/conf/httpd.conf <<EOF
<FilesMatch \.php$>
    SetHandler "proxy:unix:/var/run/php-fpm.sock|fcgi://localhost"
</FilesMatch>

DirectoryIndex disabled
DirectoryIndex index.php index.html

<Directory /usr/local/apache2/htdocs/>
    Options -Indexes
    AllowOverride All
</Directory>

Protocols h2 h2c http/1.1
EOF

sed -i -e 's|^\(.*SSLProtocol.*\)all -SSLv3$|\1all -SSLv3 -TLSv1 -TLSv1.1|' \
    -e 's|^\(.*SSLCipherSuite.*\)HIGH:MEDIUM:!MD5:!RC4:!3DES$|\1ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384|' \
    -e 's|^\(.*\)#\(SSLCertificateChainFile.*\)$|\1\2|' \
    /httpd-data/conf/extra/httpd-ssl.conf

podman run -d --name svc-httpd -p 8080:80 -p 8443:443 -v /httpd-data/conf/:/usr/local/apache2/conf/:z -v /httpd-data/htdocs/:/usr/local/apache2/htdocs/:z -v phpfpm:/var/run --restart always httpd:2.4.53

당연히 포트 80과 443이 바로 열리지는 않는다. 열자고 root를 줄 수는 없고, sysctl net.ipv4.ip_unprivileged_port_start=80 을 시도했던 기억은 있지만, root 주는 것과 크게 다르지 않다고 생각하여 금방 접었다. setcap cap_net_bind_service=+ep /usr/bin/podman 을 시도해봤는데, 컨테이너로 띄운 것에는 잘 안 되어 포기하고 포트포워딩으로 해결했다.

sudo firewall-cmd --zone=public --permanent --add-service=http
sudo firewall-cmd --zone=public --permanent --add-service=https
sudo firewall-cmd --zone=public --permanent --add-forward-port=port=80:proto=tcp:toport=8080
sudo firewall-cmd --zone=public --permanent --add-forward-port=port=443:proto=tcp:toport=8443
sudo firewall-cmd --reload

하지만 이렇게만 해주면 큰 문제가 있다. 결국 중간 네트워크를 타고 들어오기에 컨테이너 내부 프로세스 입장에서는 외부 IP를 알 수 없다. 실제로 로그 찍어보면 10.0.2.100이 나왔다. 이에 --network=host 를 쓰고, listen 포트도 맞춰 바꿔주고, 로그 추가까지 한 스크립트는 아래와 같다.

sed -i -e 's/^\(.*Listen \)80.*$/\18080/' \
    -e 's|^\(.*ErrorLog \).*$|\1"logs/error_log"|' \
    -e 's/^\(.*\)\(CustomLog .*common\)/\1#\2/' \
    -e 's/^\(.*\)#\(CustomLog .*combined\)/\1\2/' \
    -e 's/^#\(LoadModule .*mod_rewrite.so\)/\1/' \
    -e 's/^#\(LoadModule .*mod_proxy.so\)/\1/' \
    -e 's/^#\(LoadModule .*mod_proxy_fcgi.so\)/\1/' \
    -e 's/^#\(Include .*httpd-ssl.conf\)/\1/' \
    -e 's/^#\(LoadModule .*mod_ssl.so\)/\1/' \
    -e 's/^#\(LoadModule .*mod_socache_shmcb.so\)/\1/' \
    -e 's/^#\(LoadModule .*mod_http2.so\)/\1/' \
    /httpd-data/conf/httpd.conf

# https://ssl-config.mozilla.org/#server=apache&version=2.4.53&config=intermediate&openssl=1.1.1k&hsts=false&ocsp=false&guideline=5.6
sed -i -e 's/^\(.*Listen \)443.*$/\18443/' \
    -e 's/^\(.*_default_:\)443\(.*\)$/\18443\2/' \
    -e 's|^\(.*ErrorLog \).*$|\1"logs/error_log"|' \
    -e 's|^\(.*TransferLog \).*$|\1"logs/access_log"|' \
    -e 's|^\(.*CustomLog \).* \\$|\1"logs/ssl_request_log" \\|' \
    -e 's/^\(.*SSLProtocol.*\)all -SSLv3$/\1all -SSLv3 -TLSv1 -TLSv1.1/' \
    -e 's/^\(.*SSLCipherSuite.*\)HIGH:MEDIUM:!MD5:!RC4:!3DES$/\1ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384/' \
    -e 's/^\(.*\)#\(SSLCertificateChainFile.*\)$/\1\2/' \
    /httpd-data/conf/extra/httpd-ssl.conf

podman run -d --name svc-httpd --network=host -v /httpd-data/conf/:/usr/local/apache2/conf/:z -v /httpd-data/htdocs/:/usr/local/apache2/htdocs/:z -v /httpd-data/logs/:/usr/local/apache2/logs/:z -v phpfpm:/var/run --restart always httpd:2.4.53

로그도 나오니 이제 예전 웹호스팅 때처럼 Webalizer를 세팅하면 좋을텐데... NAS 개념으로 Windows 굴리면서 한 번 세팅해봤던 기억은 있지만, 상세 자료가 어디 있는지를 도저히 모르겠다. 일단 로그 쌓기부터 시작하고 rotate나 분석은 좀 나중에 해야겠다.

리스트에서 링크를 제대로 못 보여주는 문제를 알긴 하지만 이대로 놔두면 앞으로 나갈 수 없지 싶어서, 계속 보고 notify 되면서 고치기 위해 일단 문제 발생하는 상태로 옮겼다. 고쳐 보려고 조금 더 재현을 시도해보니 : 문자가 들어가 있으면 그런 것으로 보여서, emojis flag를 켜고 꺼 보는 것으로 증명했다. 코드 보니 고치는 것은 어렵지 않아서 바로 PR 제출했고# 금방 merge 되었으며 바로 반영했다.

한편으로 ParsedownExtended를 쓰게 되어 h 태그들에 자동으로 id가 생기게 되어 해당 위치로 링크를 만들 수 있는데, GitHub처럼 만들어보고 싶다는 생각도 생겼다.

네임서버는 DigitalOcean 쪽으로 옮겨두었고, 전파 고려하여 주초에 고객센터 문의 넣고 기관이전 진행하면 또 몇 년간은 걱정 끝! container가 자꾸 죽기는 하던데 원인은 잘 모르겠고 --restart always만 걸어줬다. 이제 SSL 인증서 갱신할 때 CLI로만 작업할 수 있게 되기를 기대해본다.

2022-04-01 추가

Oracle Cloud로부터 "Oracle Cloud Infrastructure Compute - Virtual Machine Instance Reboot Detected" 라는 메일을 받았다. 혹시나 하고 홈페이지를 켜보니 접속되지 않았다. 분명 restart 걸어뒀는데 뭔가 싶어서 ssh로 붙었더니, 그 즉시 홈페이지가 살아났다. 즉 뭔가 container를 띄운 유저로 한 번은 접속해야 실행되는 것 같았다. 아래 세팅을 해줘도 크게 달라지는 것은 없었다.

mkdir -p ~/.config/systemd/user
podman generate systemd --name svc-httpd > ~/.config/systemd/user/container-svc-httpd.service
podman generate systemd --name svc-phpfpm > ~/.config/systemd/user/container-svc-phpfpm.service
systemctl --user daemon-reload
systemctl --user enable --now container-svc-httpd.service
systemctl --user enable --now container-svc-phpfpm.service

조금 더 찾아보니 systemctl --user 로 설정해서 그런 것이고, systemctl --global 이 있다는데 이건 잘은 안 되었다. 최종적으로는 이슈 댓글#을 참고하여 아래와 같이 완성했다.

systemctl --user enable --now podman-restart
loginctl enable-linger opc

2022-05-07 추가

옮기면서 대충 세팅했던 인증서 만료 시점이 되어 드디어 certbot DNS 플러그인을 써보기로 했다.

기본적으로 DigitalOcean의 가이드#를 따라간다. 다만 certbot 설치할 때 snapd를 사용하는 것이 ARM 환경에서 잘 안되어서# yum을 사용했다.

sudo yum install -y epel-release
# sudo yum install -y snapd
# sudo systemctl enable --now snapd.socket
# sudo ln -s /var/lib/snapd/snap /snap
# sudo snap install core; sudo snap refresh core
# sudo snap install --classic certbot
# sudo ln -s /snap/bin/certbot /usr/bin/certbot
sudo yum install -y certbot python3-certbot-dns-digitalocean

vim ~/certbot-creds.ini
chmod 600 ~/certbot-creds.ini

sudo certbot certonly --dns-digitalocean --dns-digitalocean-credentials ~/certbot-creds.ini -d 'kennysoft.kr,*.kennysoft.kr'
sudo cp /etc/letsencrypt/live/kennysoft.kr/cert.pem /httpd-data/conf/server.crt
sudo cp /etc/letsencrypt/live/kennysoft.kr/privkey.pem /httpd-data/conf/server.key
sudo cp /etc/letsencrypt/live/kennysoft.kr/chain.pem /httpd-data/conf/server-ca.crt

Requesting a certificate for kennysoft.kr and *.kennysoft.kr
Waiting 10 seconds for DNS changes to propagate

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/kennysoft.kr/fullchain.pem
Key is saved at: /etc/letsencrypt/live/kennysoft.kr/privkey.pem
This certificate expires on 2022-08-05.
These files will be updated when the certificate renews.
Certbot has set up a scheduled task to automatically renew this certificate in the background.


If you like Certbot, please consider supporting our work by:


스케줄이 어디에 등록되었는지는 잘 모르겠다. 만료 다가왔을 때 잘 갱신되는지 확인해봐야겠다.

2022-07-17 추가

스케줄이 없었어서 인증서 만료 예정 메일을 받았다. 공식 가이드#를 참고하여 다음과 같이 구성했다.

echo "0 0,12 * * * root certbot renew -q" | sudo tee -a /etc/crontab > /dev/null
sudo tee /etc/letsencrypt/renewal-hooks/post/post.sh > /dev/null <<EOF
#!/bin/bash
cp /etc/letsencrypt/live/kennysoft.kr/cert.pem /httpd-data/conf/server.crt
cp /etc/letsencrypt/live/kennysoft.kr/privkey.pem /httpd-data/conf/server.key
cp /etc/letsencrypt/live/kennysoft.kr/chain.pem /httpd-data/conf/server-ca.crt
chown opc:opc /httpd-data/conf/server.crt /httpd-data/conf/server.key /httpd-data/conf/server-ca.crt
su opc -c 'podman exec svc-httpd apachectl -k graceful'
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/post/post.sh

sudo cat으로는 안되는데 sudo tee로는 되는 이유는 잘 모르겠다.

2022-11-21 추가

su opc -c 'podman exec svc-httpd apachectl -k graceful' 빼고 다 돌아서 인증서 재발급 및 파일 복사도 잘 됐는데 단지 재시작만 안 됐다.

사실 인증서 만료됐음을 인지한 것은 30일쯤 됐는데 확인해볼 심적인 여유가 없어서 많이 늦었다. 그동안 트래픽은 다 떨어졌다. 파일 다 잘 있는 것 확인하고 저 명령만 실행해서 금방 복구했다.

혹시나 PATH를 못 잡아줬나 싶어 저 명령어의 podman/usr/bin/podman으로 바꿔주었고, 몇 달쯤 후에 잘 갱신되나 봐야 한다.

2022-12-04 추가

build.gradle 관련 글#을 썼는데 Groovy 코드라고 설정하니 Syntax Highlighting이 되지 않아서 언어 추가 겸 버전들을 올렸다. 여백이 중복으로 적용되어 고친 부분도 있고, MathJax 올렸더니 드래그가 안 되는 변경도 보인다.

2023-02-14 추가

또 갱신 실패했다.

Letsencrypt Logs
그림 1 Letsencrypt Logs

이번에는 로그를 좀 살펴보았다. 시간당 1회 갱신 시도를 하다 보니 상당히 파일이 많았고 log rotation이 파일 1000개로 걸려있다는 것을 알 수 있었는데, 다행히도 만료 1달 전부터 갱신할 수 있다 보니 1000시간 이내에 갱신되어서 로그가 남아있었다.

2023-01-13 07:29:21,972:INFO:certbot.compat.misc:Running post-hook command: /etc/letsencrypt/renewal-hooks/post/post.sh
2023-01-13 07:29:22,038:WARNING:certbot.display.ops:Hook 'post-hook' reported error code 1
2023-01-13 07:29:22,038:WARNING:certbot.display.ops:Hook 'post-hook' ran with error output:
cannot chdir to /root: Permission denied

Renewal hook을 실행하는 계정이 root가 아닌가 본 데, 일단 sudo를 앞에 추가하긴 했지만, 잘될 것 같지는 않다. 일단 또 몇 개월 후에 살펴보려 한다.

2023-02-20 추가

QRcode CGI 데모 페이지에서 이미지 생성에 실패해서 추가 작업했다.

Fatal error: Uncaught Error: Call to undefined function ImageCreate() in /usr/local/apache2/htdocs/qr_ko/qr_img0.50i/php/qr_img.php:609 Stack trace: #0 {main} thrown in /usr/local/apache2/htdocs/qr_ko/qr_img0.50i/php/qr_img.php on line 609

PHP에 gd module이 없어서 그런 것으로 모듈을 설치#해주어야 한다. 이미지를 하나 더 만들어두기는 싫어서 시작 시 초기 대기 시간을 감수하고 스크립트 실행하는 부분만 바꾸어 설정했다. php-fpm의 podman run 부분을 다음과 같이 바꾸면 된다.

podman run -d --name svc-phpfpm -v /httpd-data/php-fpm.d/:/usr/local/etc/php-fpm.d/:z -v /httpd-data/htdocs/:/usr/local/apache2/htdocs/:z -v /httpd-data/log/fpm-php.www.log:/var/log/fpm-php.www.log -v phpfpm:/var/run --restart always php:8.1.4-fpm /bin/bash -c 'apt update && apt install -y libpng-dev && docker-php-ext-configure gd && docker-php-ext-install -j$(nproc) gd && docker-php-entrypoint && php-fpm'

Apache directory listing

Apache의 기본 directory listing 기능은 정말 못생겼다.

Apache default directory listing
그림 2 Apache default directory listing

그래서 좀 바꿔보기로 했다. 처음에는 Markdown과 통일성을 주려고 Autoindex-Strapdown을 써 보려고 했으나, 테마 적용 시 이미지 비율 등의 문제로 포기했다. 한편 여기서 알게 된 Strapdown.js는 Showdown.js보다 더 신기했다.

위 프로젝트의 테마를 설정하려고 하다가 알게 된 Bootswatch의 다른 테마를 활용하고 싶었다. autoindex의 Header와 Readme를 다른 파일로 설정하는 방법을 통해 Materia 테마를 이용했다. Navbar를 생성하고, body 너비 조정과 표 class만 주었는데 페이지의 느낌이 매우 달라졌다. 한편 아이콘이 없어져서 파일 타입을 구분하기 조금 어려워진 점은 있다. 폴더는 별도 배경색으로 구분하기로 했다.

Apache directory listing with Bootstrap
그림 3 Apache directory listing with Bootstrap

그런데 누가 웹 셸 등으로 뚫으려고 시도를 하는 것인지 이상한 파일들이 꽤 업로드되어 있다.

여기서도 기술적인 얘기를 조금 하자면, autoindex 모듈이 자동으로 생성해주는 title 태그는 그대로 가져가고 싶어서 SuppressHTMLPreamble 옵션을 사용하지 않았다. 따라서 css를 head에 포함할 수 없게 되었는데, 이는 Javascript로 동적으로 태그를 생성하도록 했다. Markdown에서 구현한 footer를 별도의 html을 불러와서 포함하는 기능은 PHP 없이 link import를 활용하는 방법으로 구현했다. 마지막으로 폴더의 경로 요소들을 navbar의 하나의 메뉴 항목으로 만들고 링크를 걸어줌으로써 실제 파일 탐색기 같은 활용이 조금 더 될 수 있도록 했다.

2018-07-23 추가

Javascript로 css를 import하려니 꽤 지연이 심하다. 검색하다 찾아낸 다른 방법을 이용하기로 했다. bodystyle에서 @import url()을 이용하는 방법이다.# 여전히 표에 클래스 씌우는 것은 느리지만 일단 헤더 등은 처음부터 잘 보인다.

2018-09-03 추가

결국 뚫렸다. 자세한 내용은 해당 글#에서 볼 수 있다.

블로그#

여러 정보 페이지도 많이 만들어야겠지만, 블로그도 해 보고 싶었다. 네이버 블로그는 이미 방치된 지 오래였고, 어차피 Markdown 기반 렌더링 엔진(?)도 구축했기에 이를 통해 간단하게 글들을 써 보려고 한다. 댓글 시스템은 좀 너무 스케일이 커질 것 같아서 그냥 내 개인 일기장 정도로 생각하면 되겠다. 지금 당장, 이 글이 보이는, 바로 이 서비스이다.

나가며

Markdown은 정말 어디에 딱 정해진 문법이 없는 것 같아서 파서마다 구현이 정말 제각각이다. 참조를 달 때 위첨자로 다는 스타일을 좋아하는데 그러면 기본적으로는 sup 태그를 사용해야 한다. 좀 불편하다. ^(내용)^으로 하는 방법이 있기는 한데 지원하는 파서가 많지 않다.

2022-02-13 추가

ParsedownExtended를 쓰면 위첨자에 더불어 아래첨자까지 지원된다. 다만 테스트해보니 그 안에서 링크가 걸리지는 않아서 일단 그대로 둔다.

돌아가기


댓글