웹 프록시는 웹 브라우저와 서버 사이의 중개자 역할을 하는 프로그램입니다. 웹 페이지 파일을 서버에 직접 요청하지 않고, 브라우저는 프록시와 연결한 후 서버에 요청을 대신 보내도록 시킵니다. 서버가 프록시에 응답을 주면, 프록시는 브라우저에게 응답을 전달합니다.
프록시는 다양한 목적을 위해 유용하게 쓰입니다. 서버에 접속하는 루트를 프록시로 제한하여 방화벽처럼 사용하기도 하고, 익명화를 수행하도록 할 수도 있습니다.
때로는 프록시를 통해 모든 식별 정보를 제거함으로써 서버가 브라우저를 식별할 수 없도록 익명 브라우저를 만들기도 합니다. 또한 프록시는 서버에서 받은 객체들의 사본을 저장해놓고 캐시 저장소처럼 사용될 수도 있습니다. (이미 한번 중개했던 객체는 캐시해두었다가 이후 클라이언트가 요청하면 프록시가 직접 보내준다.)
이번 과제에서 여러분은 웹 객체를 캐싱하는 심플한 HTTP 프록시를 구현하게 됩니다.
첫번째 파트는 에서는 들어오는 요청을 수락하고 읽고 파싱(해석 및 분할)한 후에 서버에 요청을 전달하고, 다시 서버로부터 받은 응답을 읽고, 최종적으로 클라이언트에 전달하는 프록시를 만드는 것입니다. 이 첫번째 파트는 기본적인 HTTP 오퍼레이션 네트워크 통신 프로그램을 구현하기 위해 소켓을 사용하는 방법을 익히는 파트입니다.
두번째 파트에서는 여러분이 구현한 프록시가 다수의 동시적인 요청을 처리할 수 있도록 업그레이드하게 될 것입니다. 이 과제를 통해 여러분은 매우 중요한 시스템 개념인 동시성을 다루게 될 것입니다.
마지막 세번째 파트에서는 프록시에 최근에 엑세스한 웹 컨텐츠를 캐싱하는 기능을 추가할 것입니다.
이것은 개인 프로젝트입니다.
시퀀셜 웹 프록시 ( 한번에 하나씩 요청을 처리하는 프록시 )
첫번째 단계는 HTTP/1.0 GET 요청을 처리하는 기본적인 시퀀셜 프록시를 구현하는 것입니다. POST와 같은 다른 요청 처리는 선택사항입니다.
프록시가 실행된 이후에는 프록시는 커맨드라인에 명시된 포트로부터 들어오는 요청을 항상 대기하고 있어야 합니다. 연결이 맺어지면 프록시는 클라이언트의 요청 을 읽고 파싱(해석 및 분할)해야 합니다. 그리고 클라이언트가 올바른 HTTP요청을 보냈는지 검증을 해야 합니다. 만약 올바른 요청으로 판단되면 적절한 웹서버와 연결을 맺은 후 클라이언트로 받은 요청을 전달합니다. 최종적으로 프록시는 서버의 응답을 읽고 클라이언트에 전달합니다.
(클라이언트 단의) 사용자가 https://www.cmu.edu/hub/index.html 과 같은 URL을 접속하면 브라우저는 HTTP 요청을 프록시에 보냅니다. 그 요청은 이런식으로 구성됩니다.
GET http://www.cmu.edu/hub/index.html HTTP/1.1
그러면 프록시는 이 요청을 다음과 같은 필드 구성으로 파싱해야 합니다:
호스트 네임 : www.cmu.edu
쿼리, path 등: /hub/index.html
이렇게 함으로써 프록시는 www.cmu.edu 에게 다음과 같은 HTTP 요청을 보내야 함을 파악할 수 있습니다.
GET /hub/index.html HTTP/1.0
명심하세요. HTTP 요청의 모든 라인들은 각각 맨 뒤에 리턴문자(\r) 와 개행문자(\n)가 붙습니다. 또한 모든 HTTP 요청은 반드시 ‘\r\n’ 으로 끝나야 합니다.
위의 예시에서 프록시의 요청라인은 ‘HTTP/1.0’ 으로 끝나는 반면 웹브라우저의 요청라인은 ‘HTTP/1.1’ 로 끝난다는 것을 파악했을 것입니다. 현대의 웹 브라우저들은 HTTP/1.1 요청을 생성하지만, 여러분의 프록시는 HTTP/1.0 요청으로 전달해야 합니다.
HTTP/1.0 GET 요청만 하더라도 엄청나게 복잡합니다. 교재에서는 HTTP 트랜잭션의 특정 세부 사항을 설명하고 있지만, HTTP/1.0 스펙을 온전히 이해하기 위해서는 RFC 1945 를 참고해야 할 것입니다. 이상적으로는 여러분의 HTTP 요청 파서가 RFC 1945 에 정의된 필드를 모두 처리할 수 있도록 견고해야 하지만, 예외가 있습니다: 스펙 상으로 여러줄의 요청필드가 들어오는 것을 허용하고 있지만 여러분의 프록시는 그것들을 모두 처리할 필요는 없습니다. 당연하게도, 잘못된 요청이 들어오더라도 여러분의 프록시가 결코 중단되어서는 안됩니다.
이번 과제에서 중요한 요청 헤더들은 다음과 같습니다.
: Host, User-Agent, Connection, Proxy-Connection 헤더
Host 헤더는 서버의 호스트이름을 담고 있습니다. 예를 들어서 http://www.cmu.eduhub/index.html 에 접근하려면 프록시는 다음과 같은 헤더를 붙여서 보내야 합니다.
Host: [www.cmu.edu](http://www.cmu.edu/)
때로는 웹브라우저가 HTTP 요청을 보낼 때 직접 Host 헤더를 붙여서 보내올 수도 있습니다. 그런 경우에 proxy는 브라우저가 보낸 Host 헤더를 그대로 사용해야 합니다.
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:10.0.3) Gecko/20120305 Firefox/10.0.3
이 헤더는 쓰기 편리상 두줄로 표현되었지만 프록시에서는 한줄로 보내야 합니다.
User-Agent 헤더는 클라이언트(운영시스템과 브라우저)를 식별하고, 웹서버는 식별정보에 따라 응답할 내용을 변경합니다. 이 특정한 User-Agent 헤더 문자열을 전송하면, 텔넷 스타일 테스팅 중에 응답받는 리소스가 내용과 다양성 측면에서 개선됩니다.
Connection: close
Proxy-Connection: close
커넥션 헤더와 프록시 커넥션 헤더는 이 연결이 첫번째 요청/응답이 완료된 이후에도 지속적으로 살아있어야 하는지를 결정하는 데 사용됩니다. 여러분의 프록시가 매번 요청을 받을 때마다 새로운 연결을 맺도록 해도 괜찮습니다(그리고 권장합니다) close 로 헤더 값을 설정해놓으면 웹서버는 첫번째 요청/응답 이후 프록시와의 연결을 종료할 것입니다.
편의를 위해, User-Agent 헤더는 proxy.c 파일에 정의되어있습니다.
마지막으로, 만약 브라우저가 추가적인 요청헤더를 HTTP 요청에 담아서 보낸다면 여러분의 프록시는 그것들을 모두 바꾸지 않은 채로 서버에 전달해야 합니다.
이번 과제에서 사용하는 두개의 중요한 포트 클래스가 있습니다.
: HTTP 요청 포트와 프록시의 리스닝 포트입니다.
HTTP 요청 포트는 HTTP 요청 중 URL 에 선택적으로 들어있는 필드입니다. 다시 말해,
‘ http://www.cmu.edu:8080/hub/index.html ‘이렇게 생긴 URL은 프록시가 ' www.cmu.edu ’ 라는 호스트와 연결을 맺을 때 HTTP 기본 포트인 80포트 대신에 8080 포트로 연결을 맺어야 한다는 의미입니다. 여러분의 프록시는 포트 번호가가 URL에 포함되어 있든 포함되어있지 않든 정상적으로 동작해야 합니다.
프록시의 리스닝 포트는 프록시가 자신에게 들어오는 연결 요청들을 대기하고 있는 포트입니다. 프록시는 커맨드 라인 인자로 받은 리스닝 포트를 수락해야 합니다. 예를 들어 다음과 같은 커맨드를 받으면 프록시는 15213 포트의 연결을 대기해야 합니다.
linux> ./proxy 15213
다른 프로세스에서 사용되지 않는다면,비선점형 리스닝 포트 ( 1024보다 크고 65536보다는 작은 번호의 포트들) 을 선택해도 됩니다. 각각의 프록시가 반드시 고유한 리스닝 포트를 사용해야 하는데 (이번 과제에서) 많은 사람들이 동시에 각자의 기계를 돌릴 것이기 때문에, port-for-user.pl 스크립트가 제공됩니다. 이 스크립트는 여러분이 개별적인 포트 넘버를 사용할 수 있게 도와줄 것입니다. 이 스크립트에 자신의 user ID를 넣어 고유 포트 번호를 생성하세요.
linux> ./port-for-user.pl droh
droh: 45806
port 를 의미하는 p 는 port-for-user.pl 에 의해 리턴되며 언제나 짝수입니다. 그러므로 만약 여러분이 예를 들어 Tiny Server를 위해 또다른 포트를 사용하고 싶다면 p와 p+1 번호의 포트를 사용하면 안전합니다.
임의로 포트번호를 선택하지 마세요. 그렇게 하면 다른 사람을 방해할 수 있습니다.
동시다발적인 요청 처리하기
시퀀셜 프록시를 잘 구현했다면 동시다발적인 요청을 처리할 수 있도록 개선해보세요. 동시성 서버를 구현하는 가장 단순한 방법은 각각의 새로운 요청에 대해서 새로운 스레드를 생성하는 것입니다. 교재 12.5.5에 나오는 prethreaded (사전스레드) 서버와 같은 다른 디자인패턴을 사용해도 됩니다.
웹 객체 캐싱하기
이 마지막 파트에서, 여러분은 프록시에 캐시를 추가할 것입니다. 이 캐시는 최근에 사용된 웹 객체를 메모리에 저장합니다. HTTP 는 실제로는 웹서버가 객체를 캐시하고 클라이언트가 캐시를 사용하는 방법에 대해 복잡한 지침을 가지고 있습니다. 하지만, 여러분의 프록시는 단순한 접근방법을 채택해도 괜찮습니다.
프록시가 웹 객체를 서버로부터 받으면 그것을 클라이언트에 전송하면서 메모리에도 따로 캐싱해두어야 합니다. 만약 다른 클라이언트가 같은 객체를 요청해오면, 프록시는 서버에 다시 연결할 필요 없이 단지 캐시된 객체를 보내주면 됩니다.
분명히, 여러분의 프록시가 매번 요청시마다 요청받은 모든 객체를 캐싱하려고 하면 무한대의 메모리가 필요할 것입니다. 더욱이, 어떤 웹 객체는 유난히 커서, 하나의 커다란 객체가 전체 캐시를 다 잡아먹어 다른 객체는 전혀 캐시되지 못하는 경우도 있을 수 있습니다. 이런 문제를 피하기 위해, 여러분의 프록시는 최대 캐시 사이즈와 캐시할 수 있는 객체의 최대사이즈를 가지고 있어야 합니다.
프록시의 최대 캐시 메모리 사이즈는 다음과 같아야 합니다.
MAX_CACHE_SIZE = 1 MiB
캐시 사이즈를 계산할 떄, 프록시는 실제 웹 객체를 저장한 바이트만을 가지고 계산해야 합니다. 메타데이터와 같은 관계없는 바이트들은 무시해도 됩니다.
프록시가 캐시할 수 있는 객체의 최대 사이즈는 다음을 초과해서는 안됩니다.
MAX_OBJECT_SIZE = 100 KiB
편의를 위해, 이 두개의 사이즈 리밋이 proxy.c 파일에 매크로로 정의되어 있습니다.
올바른 캐시를 구현하는 가장 쉬운 방법은, 활성상태인 연결에 일정한 버퍼를 할당하고 서버에서 데이터를 받을 때마다 버퍼에 쌓아두는 것입니다. 만약 버퍼의 사이즈가 최대 객체 사이즈를 넘으면 버퍼는 폐기됩니다. 만약 전체 웹 서버 응답(파일)을 다 읽었는데도 아직 최대 객체 사이즈에 도달하지 않았다면, 객체가 캐시될 수 있습니다. 이런 구조를 활용하여서, 프록시가 웹 객체를 위해 쓸 수 있는 최대 데이터 사이즈는 다음과 같이 계산됩니다. 여기에서 T 는 활성상태인 연결의 최대 개수입니다.
MAX_CACHE_SIZE + T * MAX_OBJECT_SIZE
프록시 캐시는 LRU 삭제 정책을 가지고 있어야 합니다. (사용한 지 가장 오래된 항목부터 버리는 방식)
엄격한 LRU 일 필요는 없지만 이와 거의 유사해야 합니다. 객체를 읽고 쓰는 것 모두 객체를 사용하는 행위에 해당한다는 것을 명심하세요.
캐시 접근은 스레드세이프(여러스레드에서 동시에 접근해도 프로그램이 안전해야 함) 해야 하며, 캐시 액세스에 경쟁 조건이 없도록 보장하는 것은 이번 과제에서 무엇보다 흥미로운 부분이 될 것입니다.
사실, 특별한 요구사항이 있습니다. 다수의 스레드가 동시에 캐시를 읽을 수 있어야 합니다. 물론 캐시 메모리에 쓸 수 있는 것은 한번에 한 스레드뿐입니다. 하지만 이 때에 다른 스레드들도 캐시를 읽을 수는 있어야 합니다.
그러므로 캐시 접근을 완전히 배타적인 잠금을 통해 보호하는 것은 적절한 솔루션이 아닙니다. Pthreads readers-writers locks 이나 semaphores 를 사용하여 여러분만의 읽기 쓰기 솔루션을 탐색해보세요. 두 케이스 모두, 엄격한 LRU 삭제정첵을 따를 필요는 없다는 점 덕분에 다수의 읽기 스레드들을 유연하게 서포트( 쓰기 권한은 안 주더라도 읽는 권한을 주는 ) 할 수 있을 것입니다.
driver.sh
파일이 자동채점 프로그램입니다. 교수는 이 프로그램으로 자동채점을 돌릴 것입니다.
linux> ./driver.sh
driver 파일은 리눅스에서 실행해야 합니다.
언제나 그렇듯, 프로그램은 에러나 잘못된 형식의 요청이나 악의적인 요청까지도 견고하게 잡아내야 합니다. 서버는 전형적으로 길게 실행되는 프로세스를 운영하며 웹 프록시도 예외는 아닙니다. 길게 실행되는 프로세스가 다양한 타입의 에러에 어떻게 대응해야 할지 생각해보세요. 다양한 에러들에 대해, 프록시가 즉시 종료되도록 하는건 분명 부적절할 것입니다.
견고성에는 다른 요소들도 포함됩니다. 세그먼트 폴트나 메모리 부족, 파일 디스크립터 누수같은 에러에 대해 취약하지 않도록 설계하는 것이 이에 해당됩니다.
단순 자동채점기를 제외하면, 구현을 테스트해볼 수 있는 샘플 요청이나 테스트 프로그램이 없을 것입니다. 코드를 잘 구현했는지 확인해볼 수 있는 테스트케이스와 테스트 도구를 직접 고안해내세요. 예상되는 모든 동작방식을 미리 알기 어려운 경우가 많은 현실세계에서 이것은 굉장히 중요한 스킬입니다.
다행히 여러분의 프록시를 디버그할 수 있는 많은 툴들은 이미 있습니다. 코드 경로를 모두 실행해보고, 대표적인 인풋, 전형적인 인풋, 엣지케이스를 테스트해보세요.
Tiny 디렉토리에 Tiny 웹서버의 소스코드가 있습니다. thttpd 만큼 강력하지는 않지만, 이 웹서버는 원하는대로 쉽게 수정할 수 있습니다. 이것은 프록시 코드를 만드는 괜찮은 시작점이 될 것입니다. 이 서버는 driver 코드가 페이지를 fetch해오는 데 사용하는 서버입니다.
교재 11.5.3 에 나와있듯, 프록시와 연결하고 HTTP요청을 하기 위해 telnet을 사용할 수 있습니다.
curl 을 통해 어떤 서버(당신의 프록시를 포함)에 보낼 HTTP 요청을 생성할 수 있습니다. 이건 굉장히 유용한 디버깅 툴입니다. 예를 들어, 만약 당신의 프록시와 Tiny가 로컬머신에서 둘다 실행중이라면Tiny는 15213 포트에서 대기하고 프록시는 15214 포트에서 대기할 것입니다. 그러면 당신은 다음의 curl 커맨드를 쳐서 Tiny에게 프록시를 통해 페이지를 요청할 수 있습니다.
linux> curl -v --proxy http://localhost:15214 http://localhost:15213/home.html
* About to connect() to proxy localhost port 15214 (#0)
* Trying 127.0.0.1... connected
* Connected to localhost (127.0.0.1) port 15214 (#0)
> GET http://localhost:15213/home.html HTTP/1.1
> User-Agent: curl/7.19.7 (x86_64-redhat-linux-gnu)...
> Host: localhost:15213
> Accept: */*
> Proxy-Connection: Keep-Alive
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Server: Tiny Web Server
< Content-length: 120
< Content-type: text/html
<
<html>
<head><title>test</title></head>
<body>
<img align="middle" src="godzilla.gif">
Dave O’Hallaron
</body>
</html>
* Closing connection #0
nc라고 알려진 netcat은 굉장히 가변적인 네트워크 유틸리티입니다. netcat을 telnet처럼 서버와의 연결에 이용할 수 있습니다.그러므로, 만약 프록시가 catshark 에서 12345포트로 실행중이라면 다음과 같이 프록시를 테스트할 수 있습니다.
sh> nc catshark.ics.cs.cmu.edu 12345
GET http://www.cmu.edu/hub/index.html HTTP/1.0
HTTP/1.1 200 OK
...
웹서버에 연결하는 것뿐 아니라, netcat은 직접 서버로서 동작할 수 있습니다. 다음 커맨드를 통해 netcat 서버를 12345 포트에서 실행할 수 있습니다.
sh> nc -l 12345
netcat 서버를 세팅한 후에, 프록시를 통한 가상요청을 생성할 수 있습니다. 이를 통해 proxy가 netcat에 실제 요청을 보내는지 검사할 수 있습니다.
당신은 Mozilla Firefox 최신버전으로 당신의 프록시를 테스트해야 합니다. About Firefox 페이지를 방문하면 자동으로 브라우저가 최신버전으로 업데이트될 것입니다. Firefox에서 프록시를 설정하려면 여기서 하세요.
Preferences>Advanced>Network>Settings
실제 웹 브라우저에서 당신이 만든 프록시가 동작하는 걸 보는 일은 굉장히 짜릿할 것입니다. 당신의 프록시의 기능은 제한적일 테지만 프록시를 통해 방대한 양의 웹사이트를 탐색할 수 있다는 것을 목격할 것입니다. 웹브라우저를 사용하여 캐싱할 때 조심해야 합니다. 모든 현대 웹 브라우저에는 자체 캐시가 있으므로 프록시를 테스트하기 전에 이것을 비활성화해야 합니다.
생략