웹서비스에 한글 아이디를 허용하게 되면 난감해지는 HTTP적인 이유

그건 Basic Authentication 때문이다.

웹서비스에 한글 아이디를 허용하면 Basic Authentication을 적용하기 어려워진다.

뭐가 문제인가

HTTP의 Basic Authentication은 매우 단순한 인증 체계이다. 클라이언트가 사용자의 아이디와 비밀번호를 “아이디:비밀번호” 꼴로 만들어 base64 인코딩하여 전송하면, 서버가 확인 후 틀렸으면 401 Unauthorized를 돌려주고 맞았으면 요청을 적절히 처리한다.

보통 웹브라우저로 사이트에 들어가는데 팝업이 짠 뜨면서 로그인을 요구하면 대개 이 Basic Authentication이라고 보면 된다. 좀 더 복잡한 Digest Authentication이란 것도 있긴 한데 있긴 한데 별로 인기가 없다. 어차피 안전하지 않은 건 둘 다 마찬가지라 그럴바엔 그냥 쉬운 것을 선호하게 되는 모양이다.

근데 이 Basic Authentication 체계에서는 한글 아이디나 비밀번호를 사용할 수 없다.

Basic Authentication를 정의한 RFC 2617에 따르면, 아이디는 “:”를 제외한 0자 이상의 TEXT이고 비밀번호는 그냥 0자 이상의 TEXT다. 그럼 대체 TEXT는 뭘까?

The TEXT rule is only used for descriptive field contents and values
that are not intended to be interpreted by the message parser. Words
of *TEXT MAY contain characters from character sets other than ISO-
8859-1 [22] only when encoded according to the rules of RFC 2047

HTTP/1.1을 정의한 RFC 2616에 따르면 TEXT는 ISO-8859-1로 정의된 문자집합에 속한 문자만을 사용해야하며, 그렇지 않은 경우에는 RFC 2047의 인코딩 규칙을 따라야 한다.

RFC 2047의 인코딩 규칙이란 대략 요렇게 생긴 것들이다. 아마 웹브라우저로 파일 다운로드 받다 뭔가 잘못되었을 때 종종 봤을 것이다. (( 웹브라우저마다 파일 이름을 코딩하는 방식이 제각각이라 그런 문제가 종종 생긴다. 자세한 것은 여기를 보라. ))

=?iso-8859-1?q?this is some text?=

자 그럼, 저 요상하게 생긴 방식으로 인코딩을 하면 아무 문제없이 한글 아이디를 써도 Basic Authentication이 가능할 것 같은 기분이 든다.

근데 아니다.

예전에 Stackoverflow에 Basic Authentication의 아이디와 패스워드는 대체 뭘로 디코딩해야 하는가에 대한 질문이 올라온 일이 있었는데, 답변은 “ISO-8859-1” 흑은 “undefined” 니까 네가 하고 싶은대로 해라.였다.

아니 스펙에 분명 RFC 2047도 된다고 하는데 이 답변자는 뭐하시는 분이길래 하고 봤더니만 Julian Reschke였다. HTTP/1.1을 고쳐쓰는 httpbis의 저자 중 한명이다. 그에 따르면 httpbis에선 RFC 2047을 적용 가능함에 대한 언급이 빠진다고 한다.

실제 구현들은 어떠한가

“ISO-8859-1” 혹은 “undefined” 라니 참으로 애매해다. 이런 모호한 상황을 유명 웹서버들은 어떻게 해결했을까. 구현을 들여다보자.

다음은 Apache Tomcat의 구현이다.

username = new String(buf, 0, colon);
password = new String(buf, colon + 1,
        authorizationCC.getEnd() - colon - 1);

인코딩을 정해주지 않았다. 이런 경우 시스템 설정을 따라갈 것이다.

jetty6은 ISO-8859-1로 디코딩한다.

credentials = B64Code.decode(credentials,StringUtil.__ISO_8859_1);

cpython은 ascii로 디코딩한다. ISO-8859-1 보다 작은 문자집합이다.

authorization = base64.decodebytes(authorization).
                decode('ascii')

한번 자신이 애용하는 웹서버의 소스코드를 들여다보라. 아마 UTF-8로 디코딩하는 경우는 거의 없을 것이다.

결론

웹애플리케이션을 만들면서 아이디를 한글도 가능하게 하려고 생각하고 있다면 다시 한번 생각해보길 바란다. Basic Authentication을 붙일 일이 생기면 골치아파질 수 있다.

Advertisements

"HTTP/1.1 200" 은 틀리고 "HTTP/1.1 200 " 은 맞는 이유

HTTP 응답은 항상 다음과 같은 Status-Line 으로 시작한다.

HTTP/1.1 200 OK

왼쪽부터 순서대로 HTTP-Version, Status-Code, Reason-Phrase가 온다.

그런데 만약 응답을 해줘야 하는 HTTP 서버가, 적절한 이유구문을 결정하지 못해 Reason-Phrase 없이 응답을 보내려면 어떻게 해야 할까?

그냥 “HTTP/1.1 200” 로 보내면 될 것 같지만, 정답은 “HTTP/1.1 200 “이다. 끝에 공백문자를 하나 넣어줘야 한다.

이유가 뭘까? RFC 2616에 정의되어 있는 Status-Line의 문법을 보자.

Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF

흠. 반드시 Reason-Phrase를 넣게 되어있다. 그럼 Reason-Phrase는 어떻게 정의되어 있을까?

Reason-Phrase  = *<TEXT, excluding CR, LF>

Reason-Phrase는 CR이나 LF를 제외한 문자로 된 0자 이상의 문자열이다. “0자 이상” 이기 때문에 당연히 빈 문자열도 문제없다. 따라서 “HTTP/1.1 200 “과 같은 Status-Line은 문법상 틀리지 않다. 그러나 “HTTP/1.1 200″은 틀렸다. Status-Code 200 뒤에 공백(SP)가 빠졌기 때문이다.

이렇게 되어버린 것은 사실 실수라고 한다. Reason-Phrase를 생략하고 싶은 경우엔 “HTTP/1.1 200” 식으로 응답할 수 있게 했어야 했다. 하지만 이제와서 되돌릴 순 없으니 앞으로도 계속 이렇게 사용할 것이다. HTTP/2.0이 나와도 계속.

HTTP status code 실험

요즘 회사에서 HTTP 스터디중이다.

HTTP/1.1의 다양한 Status Code들에 대해 공부한 뒤, 실제 웹브라우저가 Status Code에 대해 어떻게 대응하는지 궁금해졌다. 그래서 구글 크롬을 가지고서 테스트 해 보았다.

일단 status code를 원하는대로 만들어서 응답해주는 코드를 간단히 작성했다.

header("HTTP/1.1 ".$_REQUEST['status']." ".$_REQUEST['reason']);

if (isset($_REQUEST['body'])) {
        echo $_REQUEST['body'];
}

예를 들어 http://npcode.com/test/http-resonse.php?status=201 이렇게 요청하면 201로 응답해준다.

이걸로 구글크롬을 테스트 해 보았다.

특이한 반응을 보인 Status Code

대부분의 status code에 대해 평이하게 반응했지만 몇가지 특이한 것들이 있었다.

204 No Content 마치 response를 받지 않은 것 처럼 처리되는 것으로 보인다. 크롬 개발자 도구(CTRL+SHIFT+I)로 확인해보면 request만 있고 response는 없는 것으로 나온다. 심지어 주소입력창에 입력한 url도 원래대로 돌려놓는다.

205 Reset Content 204와 똑같이 동작한다. 그런데 205 Reset Content라면 form을 지워줘야 HTTP/1.1 스펙대로가 아닌가 싶은데… 어째선지 전혀 form이 지워지지 않는다.

304 Not Modified 다른 redirect 관련 reponse와는 달리 이 응답만은 에러페이지를 내보낸다. body가 있거나 없거나 무조건 “해당 웹페이지를 찾을 수 없습니다.” 페이지.

4xx의 경우는 보통 body를 그대로 출력하지만, 예외인 status code가 딱 3개 있다.

403 Forbidden body가 있으면 body 출력, 없으면 “웹페이지 억세스가 거부되었습니다” 에러 페이지

404 Not Found body가 있거나 없거나 “죄송합니다. 링크가 깨진 것 같습니다” 에러 페이지

407 Proxy Authentication Required body가 있거나 없거나 “해당 웹페이지를 사용할 수 없습니다” 에러페이지

5xx의 경우엔 body가 있으면 body 출력, 없으면 “서버 오류” 에러 페이지를 보여준다. 에러 세부내용은 각 status code에 맞게 나타난다.

RFC 2616(HTTP/1.1)에는 없지만 크롬이 이해하는 Status Code

reason pharse를 빼놓고 보낸 경우에도, 개발자도구로 응답 헤더를 들여다보면 reason pharse가 채워져있다. 기본적으로 HTTP/1.1 스펙에 나온대로 채워주고, 스펙에 없는 몇몇에 대해서도 아래와 같이 몇몇 채워넣는 것들이 있다.

  • 418~421 unused
  • 422 Unprocessable Entity
  • 423 Locked
  • 424 Failed Dependency
  • 425 No code
  • 426 Upgrade Required
  • 506 Variant Also Negotiates
  • 507 Insufficient Storage
  • 508, 509 unused
  • 510 Not Extended

418이 I’m a teapot이었으면 조금 재미있었을지도… (RFC2324) 근데 생각해보니 scheme을 coffee가 아닌 http로 해서 요청했으니 될 리가 없겠구나.

정의되지 않은 Status Code에 대한 크롬의 대응

기타 정의되지 않은 status code로 응답한 경우엔, reason pharse와 body 유무에 따라 다음과 같이 동작한다.

  • reason pharse가 없는 경우 => 500 Internel server error
    • body가 없으면 브라우저 자체 에러 페이지 “웹사이트에서 오류가 발생했습니다” 출력
    • body가 있으면 body 출력
  • reason pharse가 있는 경우 => 응답한 status code를 그대로 사용

결론

HTTP response의 status code에 대한구글크롬의 반응은, 뭔가 일관성이 있기 보다는 그냥 각 status code에 맞게 적절하게 대응하는 것으로 보인다. 파이어폭스도 조금 해봤는데 크게 다르지는 않았다.