게임 서비스와 HTTP CORS

들어가며

게임 서비스는 2000년대 초반의 PC / 콘솔 환경에서 주로 서비스했습니다. 현재는 PC와 콘솔을 넘어서 모바일 게임이 시대의 주류가 되었고, 많은 게이머들이 출퇴근 시간의 버스나 지하철 안에서 게임을 즐길 수 있게 되었습니다.
게임 개발자들은 언제나 더 새로운, 더 많은 유저에게 다가갈 수 있는 플랫폼을 찾아다니고 있는데, 현재 그 대상 중 하나가 웹입니다.

웹의 대두

현재 가장 광범위하게 사용하는 플랫폼은 웹입니다. 실제로 유저가 접속하는게 웹 페이지인 경우도 많고, 유저가 사용하는 프로그램/앱이 단순히 웹 페이지를 연결하는 경우도 많습니다. 이 경우 사실상 프로그램을 실행하는 주체는 웹브라우저와 웹 서버 그리고 그 위에서 동작하는 웹 서비스입니다.

그리고 이러한 경향은 최근 더 가속화하고 있습니다.

Emscripten

Emscripten은 LLVM 컴파일러 툴체인을 이용해서, C/C++ 응용 프로그램을
JavaScript 로 빌드하는 프로젝트입니다. 또한 OpenGL ES 2.0/3.0 중의 일부를 지원합니다 — 정확히는 OpenGL ES 2.0/3.0 중에서 웹 브라우저를 위한 WebGL 1/2 명세에 포함된 부분을 쓸 수 있습니다.
그리고 웹 브라우저에서 추가로 지원 작업이 진행 중인 더 효율적인 스크립트 포맷인 WebAssembly 도 지원합니다.

그래서 무엇을 할 수 있을까요?

OpenGL을 사용한 C/C++ 게임을 웹 브라우저에서 구동하게 변경할 수 있습니다. 이 방법으로 Quake 3 같은 3D 게임을 브라우저에서 실행해볼 수 있습니다.
혹은, 게임 개발할 때 지원할 추가 플랫폼으로, 웹 브라우저를 생각해볼 수도 있습니다. Unity 3D 엔진에서 말하는 WebGL 지원이 바로 이에 해당하는데,
웹 브라우저용 빌드를 하면 JavaScript 혹은 WebAssembly 형태로 빌드를 만들게 됩니다.

PC나 콘솔 환경 혹은 모바일처럼 독립된 클라이언트 응용 프로그램이 있는 경우를 넘어서, 웹 브라우저에서 동작하는 게임을
만들면 무엇이 바뀌어야 할까요? 게임 개발자 입장에서 어떤 부분을 더 고려해야할까요?

과거의 시도: Adobe Flash

현재는 사실상 사장되어가는 중이지만, Adobe Flash 가 한창 널리 쓰이던 2000년대 중반까지는 이 기술을 활용해서 게임 클라이언트를 만들기도 했습니다.
Adobe Flash 로 만든 클라이언트가 게임 서버에 접속할 때는 Adobe Flash Socket Policy 라는 XML 파일을 전달받아서 게임 서버에 접속해도 되는지를 확인합니다. 아래와 같은 과정을 거칩니다.

  1. 웹 브라우저 -> 게임 서버의 843번 포트: TCP 요청으로 <policy-file-request/>\0 라는 메시지 전송
  2. 게임 서버의 843번 포트 -> 웹 브라우저: XML 응답. 만약 모든 위치에서 허용이거나 응답하지 않은 경우 경우 3으로. 특정 위치 허용 응답인경우 종료.
  3. Security.loadPolicyFile() 에서 지정한 위치에서 정책 가져오고, 없다면 4로.
  4. 게임 서버의 게임 서버용 포트에 접속. 그리고 다시 <policy-file-request/>\0 라는 메시지 전송.
    응답으로 XML을 받으면 진행, 그렇지 않다면 연결 실패로 종료.

이 파일 (혹은 서버 응답) 은 대략 아래와 같이 생겼습니다.

<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE cross-domain-policy
    SYSTEM "/xml/dtds/cross-domain-policy.dtd">

<cross-domain-policy>
  <site-control
      permitted-cross-domain-policies="master-only"/>
  <allow-access-from
      domain='flash-cdn.example.com'
      to-ports="8012,8013,8000-8010"/>
</cross-domain-policy>

대략, allow-access-from-domain 에서 다운로드한 플래시 파일이면, 게임 서버의 8012, 8013 혹은 8000번에서 8010 사이의 포트에 접속해도 된다라는 의미입니다.
즉, “특정 위치에서 다운로드한 파일이면”, 게임 서버의 “특정 포트” 로 접속해도 좋다라고 선언하는 방식입니다.

현재: 웹 어셈블리, WebGL, HTML5 앱

Adobe Flash는 브라우저가 직접 실행하는 것이 아니고, 브라우저 위에서 동작하는 플러그인이 실행합니다. 이렇게 플러그인 구조라서 어떤 동작을 할 수 있는지는 플러그인 제작사가 (거의) 정할 수 있습니다.
반면에 현재의 웹 앱들은 표준 HTTP 프로토콜을 이용하기 때문에, 표준에 있는 기능들과 웹 브라우저 제작사에서 정한 제약 사항을 따라야 합니다.

이제 표준과 표준이 제약하는 내용에 대해서 간략히 얘기해보겠습니다. 접근 방식은 Adobe Flash 의 경우와 상당히 유사하지만, 웹의 특성이 어느 정도 녹아 있는 부분을 관심있게 보시면 더 좋습니다.

HTTP 동일 출처 정책

웹 브라우저에서 로드한 HTML문서나 JavaScript 등에서 “어디에 있는 다른 자원 (문서, 이미지, CSS, JavaScript, …)” 에 접근할 수 있는지를 제약하는 가장 기본적인 정책이 “동일 출처 정책 (Same Origin Policy)” 입니다.

우선 문서나 스크립트의 출처 (origin) 를 정의합니다.

  • scheme: httphttps 같은 접속 방법을 말합니다.
  • domain: example.com 이나 app.example.com 같은 도메인 주소를 말합니다.
  • port: example.com:443example.com:80 처럼 도메인 주소 이하의 포트 번호입니다.

여기의 (scheme, domain, port) 를 출처(origin) 이라고 부르며, 이 출처가 같은 경우에 접근을 허용합니다. 다만 아래 조건을 모두 만족하는 경우 출처가 달라도 허용하며, 이를 실제로 사용하는 웹 페이지에 대응시켜 보면 대부분의 웹 페이지에서 사용하는 HTTP 요청이 이를 만족합니다.

  • HTTP GET, POST, HEAD 만 허용.
  • 브라우저가 직접 설정하는 것 (Connection, User-Agent, …) 을 제외하고는 Accept,
    Content-Language, Viewport-Width 같은 일부 헤더만 허용.
  • Content-Type 은 일반적인 HTML 폼 요청인 application/x-www-form-urlencoded
    multipart/form-data 그리고 text/plain 만 허용.

HTTP 동일 출처 정책 넘어서: HTTP 교차 출처 자원 공유

게임 클라이언트를 웹 플랫폼 용으로 빌드했다고 해서 용량이 작은 것은 아닙니다. 그래서 대부분 CDN을 통해서 클라이언트를 배포하게 되는데, 이럴 때는 “게임 클라이언트를 다운로드한 출처” 와 “게임 서비스의 API 주소” 가 서로 다른 도메인을 사용하게 됩니다. 그래서 이런 경우에는 어떻게 진행하는지 살펴보겠습니다.

위에서 언급한 일반적인 요청 — HTTP form 채우기, 이미지나 CSS 파일 등의 단순 가져오기 — 를 제외하고는 HTTP 교차 출처 자원 공유 (HTTP Cross-Origin Resource Sharing; 이하 HTTP CORS)에 따라 처리합니다. 게임 서버와 클라이언트가 통신하는 경우, 대부분 JSON을 메시지 포맷으로 사용하고 몇 가지 추가적인 HTTP 헤더를 설정하는 경우가 많습니다. 이런 경우에는 단순한 요청 이 아닌 것으로 취급해서 HTTP CORS 정책에 따라 처리하게 됩니다.

이 때 다음과 같은 메시지 교환이 이뤄집니다. 만약 일부 메시지를 게임 서비스에서 제대로 처리하지 않는다면 웹 브라우저가 응답을 받았는지 / 게임 서비스에 연결이 가능한지 게임 클라이언트가 알 방법이 없습니다.

  1. 웹브라우저 (게임 클라이언트) -> 게임 서버: HTTP POST 요청 전송 (JSON 메시지)
    이 메시지는 바로 전송되지 않고 대기 상태가 됩니다.
  2. 웹브라우저 (게임 클라이언트가 아닌 웹 브라우저 자체) -> 게임 서버: HTTP OPTIONS 요청. 이 메시지를 HTTP CORS preflight 요청이라고 부릅니다. 여기엔
    “해당 스크립트(클라이언트)를 다운로드한 출처 (origin) 이 무엇인지”, “요청하려는 HTTP 방식은 무엇인지 (여기선 POST)”,
    “안전 목록에 없는 HTTP 헤더 중 뭘 더 쓰는지” 를 전송합니다.
  3. 게임 서버 -> 웹 브라우저: Access-Control- 로 시작하는 몇 개의 헤더를 포함한 응답을 보냅니다.
    여기에서 앞으로 얼마동안 이렇게 해도 되는지 (Max-Age), 어떤 헤더를 써도 되는지 (Allow-Header)
    / 응답에서 어떤 헤더를 게임 클라이언트에게 전달해도 좋은지 (Expose-Header) 등을 결정합니다.
    그리고, “이 출처에서 받은 스크립트는 접속해도 좋다 (Allow-Origin)” 를 보냅니다.
  4. 웹 브라우저 -> 게임 서버: 1에서 보류했던 HTTP POST 요청을 실제로 보냅니다.
  5. 게임 서버 -> 웹 브라우저: 4에 대한 응답을 보냅니다. 이 때에도 Acess-Control-Allow-Origin 헤더는 필요합니다.

실제 구현을 한다면 지연 시간 감소를 위해서 꼭 Access-Control-Max-Age 를 preflight 응답으로 전달해줘야 합니다.
해당 시간 만큼 preflight 응답을 캐싱하며, 캐싱한 결과가 없다면 모든 HTTP 요청을 보내기 전에 preflight 요청을 보내서
응답 지연 시간이 크게 길어집니다. (네트워크 지연 시간의 2배가 아니라 4배가 소요됩니다)

예: WebGL + HTTP CORS

아이펀팩토리에서 개발하는 아이펀 엔진에서는 웹 브라우저에서 WebGL 클라이언트로 연결하는 경우 아래와 같이 응답합니다. Unity3d 로 빌드한 WebGL 클라이언트를 써서 게임 서버에 접속하고, 이 때 주고 받은 메시지를 크롬 개발자 도구로 확인했습니다.

실제로 HTTP CORS가 필요한 형태의 게임 서버 – 클라이언트간 연동을 한다면 아래 응답을 참고해서 작성하신다면 문제 없이 통신할 수 있습니다.

1단계: 웹 브라우저 -> 게임 서버: HTTP CORS preflight 요청 전송

Unity3D를사용한WebGL클라이언트 (Google Chrome; macOS에서실행)
HTTP CORS preflight 요청(Google Chrome 개발자도구)

2단계: 웹 브라우저 -> 게임 서버: 게임용 HTTP 요청 전송

HTTP CORS응답을받고나서보내는(보통의) HTTP요청

한계

실시간 게임에서 주로 쓰게되는 웹 소켓의 경우, 애초에 이런 HTTP CORS 의 제한을 받지 않습니다.

그리고 공격자가 웹 브라우저를 거치지 않고 HTTP 요청을 (임의로) 만들어서 보낸다면, HTTP CORS로 “여기에 있는 스크립크만 허용했는데” 라고 가정한게 쓸모없어질 수 있습니다.

결론

많은 게임 서비스가 웹 서비스의 형태로 바뀌었고, 이제는 게임 클라이언트도 웹 브라우저에서 구동하는 경우가 늘어나고 있습니다. 이런 경우에 HTTP 접근 제어가 어떤 형태로 이뤄지는지를 알면 게임 서비스 구성하는게 편리합니다. 나아가, 서비스에 문제가 있는 경우에도 어떤 의미인지 파악하고 분석할 수 있게 됩니다.

그리고 Google API나 Facebook API 혹은 LINE이나 위챗 혹은 QQ같은 메신저 플랫폼의 거의 모든 제3자 서비스도 웹 서비스의 형태를 하고 있습니다. 그래서 이런 서비스들과 연동할 때에도 HTTP same-origin policy 나 CORS 에 대해서 이해한다면 좀 더 쉽게 접근할 수 있으리라 생각합니다.

답글 남기기

댓글을 게시하려면 다음의 방법 중 하나를 사용하여 로그인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Google+ photo

Google+의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

%s에 연결하는 중

This site uses Akismet to reduce spam. Learn how your comment data is processed.