
2주 전에 "HTTP 000은 폐쇄가 아니었다"는 글을 썼다. 연결이 안 된다고 문을 닫은 건 아니니, DNS까지 확인하고 판정하자는 이야기였다. 그 글을 올린 다음 날, 반대쪽에서 같은 실수를 했다. 이번엔 200이었다.
공표된 업체 몇 곳의 사이트가 지금도 열려 있는지 매일 확인하고 있었다. 판정 규칙은 단순했다. DNS가 없으면 소멸, 응답이 2xx·3xx면 영업 중, 그 사이는 판정 보류.
다른 작업 때문에 "영업 중"으로 분류된 주소 몇 개를 브라우저로 직접 열었다. 세 개가 이런 화면이었다.
사이트 기간 만료 — 현재 접속하신 사이트의 호스팅 기간이 만료되었습니다.
사이트 빌더 호스팅이 끊기면 나오는 안내 화면이다. 그런데 이 화면이 상태 코드 200을 돌려준다. 서버 입장에서는 "요청한 페이지를 정상적으로 보여 줬다"가 맞으니까. 덕분에 스크립트는 며칠 동안 이 세 곳을 영업 중으로 셌고, 그 숫자가 글 몇 편에 그대로 들어갔다.
상태 코드는 "서버가 대답했다"까지만 말해 준다. 그 대답이 가게 문을 연 화면인지, 문 닫았다는 안내문인지는 내용을 봐야 안다.
이번에 확인해 보니 200을 돌려주면서 영업 화면이 아닌 경우가 꽤 흔했다.
세 번째는 기계로 가르기 어렵다. 사이트는 멀쩡히 열려 있고, 첫 화면에 "환불이 지연되고 있다"는 긴 공지가 붙어 있을 뿐이다. 이건 사람이 읽고 판단해야 했다.
앞의 두 가지는 문구로 잡힌다. 응답이 정상이면 본문을 한 번 더 받아서 안내 문구가 있는지 본다.
NOT_OPEN = [
('호스팅 만료', ['사이트 기간 만료', '호스팅 기간이 만료']),
('도메인 판매·주차', ['domain is for sale', '도메인을 구매',
'This domain may be for sale', 'parked']),
]
def body(d):
try:
r = subprocess.run(['curl', '-s', '-L', '--max-time', '15',
'-A', UA, 'https://' + d + '/'],
capture_output=True, timeout=25)
return r.stdout.decode('utf-8', 'ignore')
except Exception:
return ''
def verdict(a, codes, d=None):
live = [c for c in codes if c.startswith(('2', '3'))]
if a is None:
return '도메인 소멸'
if live:
b = body(d).lower() if d else ''
for label, keys in NOT_OPEN:
if any(k.lower() in b for k in keys):
return label
return '영업 중'
return '껍데기 유지'
-L을 붙인 건 리다이렉트를 따라가 최종 화면을 보기 위해서다. 주소를 한 번 넘긴 뒤 보여 주는 화면이 진짜 판정 대상이다.
판정 결과는 이렇게 다섯 칸이 됐다. 도메인 소멸, 껍데기 유지(DNS는 있는데 응답 없음), 호스팅 만료, 도메인 판매·주차, 영업 중. 처음 세 칸짜리보다 지저분하지만, 칸을 줄이면 틀린 숫자가 깔끔해 보일 뿐이었다.
이 방식은 아는 문구만 잡는다. 다른 호스팅 업체는 다른 안내문을 쓴다. 그래서 목록은 실제로 만난 문구만 추가하고, 새 사이트를 처음 판정할 때는 사람이 한 번 화면을 보기로 했다.
크기도 힌트가 됐다. 만료 화면 세 개는 응답 크기가 바이트 단위까지 똑같았다. 정상 사이트는 수십 KB에서 수백 KB였다. 다만 크기만으로 판정하면 가벼운 정상 페이지를 놓칠 수 있어서, 경고만 띄우는 용도로 남겨 뒀다.
숫자가 이미 나간 글이 네 군데였다. 전부 찾아서 고치고, 고쳤다는 표시를 본문에 남겼다. 조용히 바꾸는 쪽이 편했겠지만, 그 숫자를 인용한 사람이 있다면 왜 바뀌었는지 알 수 있어야 한다고 봤다.
000은 폐쇄가 아니었고, 200은 영업이 아니었다. 결국 같은 이야기다. 상태 코드는 판정의 재료이지 판정 그 자체가 아니다. 지금 규칙은 세 줄이다. DNS를 보고, 응답을 보고, 화면을 본다. 셋 중 하나만 보고 내린 결론은 다음에 다시 틀린다.