728x90

프록시 서버는 인터넷 사용 시 '중계자'로서 중요한 역할을 한다. 이를 통해 온라인 활동은 보다 안전하고 효율적으로 이루어진다. 프록시 서버에는 주로 두 가지 종류가 있다.

 

 

포워드 프록시는 사용자의 온라인 신원을 숨기는 역할을 한다. 인터넷에 접속할 때 직접 서버로 연결하지 않고 프록시 서버를 거쳐서, 사용자의 IP 주소와 같은 개인 정보의 노출을 막는다. 이는 마치 온라인에서 가면을 쓰고 다니는 것과 같아 사용자를 보호한다.

 

 

리버스 프록시는 서버 측에서 사용되며, 외부의 요청을 받아 내부 서버로 전달하는 역할을 한다. 이를 통해 보안을 강화하고, 여러 서버가 요청을 효율적으로 처리할 수 있도록 돕는다. 실제 서버들은 내부망에 숨어 있으며, 외부에서는 리버스 프록시를 통해서만 접근할 수 있다.

 

프록시 체이닝은 여러 프록시 서버를 연속적으로 사용하는 기술로, 사용자의 IP 주소를 더욱 효과적으로 숨길 수 있다. 해외 프록시를 포함하여 여러 단계의 프록시를 거치면 추적이 더 어려워진다.

 

프록시 서버는 다음과 같은 주요 기능을 제공한다.

  • 방화벽: 외부의 위협으로부터 내부 네트워크를 보호한다.
  • 익명화: 사용자의 신원을 숨기고 익명으로 인터넷을 사용할 수 있도록 한다. 프록시는 요청에서 식별 정보를 제거하여 이를 가능하게 한다.
  • 캐시: 자주 접속하는 웹 사이트의 데이터를 저장해 놓고, 다음 접속 시 빠르게 로드할 수 있도록 도와준다.

proxy.c(main)

int main(int argc,char **argv)
{
    int listenfd,connfd;
    socklen_t  clientlen;
    char hostname[MAXLINE],port[MAXLINE];
    struct sockaddr_storage clientaddr;
    if(argc != 2){
        fprintf(stderr,"usage :%s <port> \n",argv[0]);
        exit(1);
    }
    listenfd = Open_listenfd(argv[1]);
    while(1){
        clientlen = sizeof(clientaddr);
        connfd = Accept(listenfd,(SA *)&clientaddr,&clientlen);
        Getnameinfo((SA*)&clientaddr,clientlen,hostname,MAXLINE,port,MAXLINE,0);
        printf("Accepted connection from (%s %s).\n",hostname,port);
        doit(connfd);
        Close(connfd);
    }
    return 0;
}

 

1. Open_listenfd 함수를 호출해 주어진 포트 번호에 대한 리슨 소켓을 열고, 클라이언트의 연결 요청을 기다릴 준비를 한다.

 

2. 프로그램은 무한히 실행되며, while(1) 루프 안에서 Accept 함수를 사용하여 클라이언트의 연결 요청을 기다리고 수락한다. 연결 요청이 들어오면, 클라이언트와 통신하기 위한 새로운 소켓(connfd)을 얻는다.

 

3. 연결된 클라이언트의 IP 주소와 포트 번호를 얻기 위해 Getnameinfo 함수를 사용한다. 이 정보는 연결이 수락되었음을 나타내는 메시지에 사용된다.

 

4. doit 함수를 호출하여 클라이언트로부터 받은 요청을 처리한다.

 

5. 클라이언트의 요청을 처리한 후, Close 함수를 사용하여 클라이언트와의 연결을 닫는다. 이는 서버가 사용한 리소스를 해제하고 다음 연결 요청을 준비하는 단계이다.

 

proxy.c(doit)

void doit(int connfd){
  int end_serverfd;
  char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
  char endserver_http_header[MAXLINE];
  char hostname[MAXLINE], path[MAXLINE];
  int port;
  rio_t rio,rio_endserver;
  Rio_readinitb( & rio, connfd);
  Rio_readlineb( & rio, buf, MAXLINE);
  printf("Request headers:\n");
  printf("%s", buf);
  sscanf(buf, "%s %s %s", method, uri, version);
  if (strcasecmp(method, "GET")) {
    printf("Error code: 501\n");
    return;
  }
  parse_uri(uri,hostname,path,&port);
  build_http_header(endserver_http_header,hostname,path,port,&rio);
  end_serverfd = connect_endServer(hostname,port,endserver_http_header);
  if(end_serverfd<0){
    printf("connection failed\n");
    return;
  }
  Rio_readinitb(&rio_endserver,end_serverfd);
  Rio_writen(end_serverfd,endserver_http_header,strlen(endserver_http_header));
  size_t n;
  while((n=Rio_readlineb(&rio_endserver,buf,MAXLINE))!=0){
    printf("Proxy received %d bytes,then send\n",n);
    Rio_writen(connfd,buf,n);
  }
  Close(end_serverfd);
}

 

1. 필요한 변수를 초기화하고, 클라이언트 연결 파일 디스크립터(connfd)를 사용하여 클라이언트로부터 HTTP 요청 헤더를 읽는다.

 

2. 요청에서 HTTP 메소드(GET 등)를 추출하고, 이 메소드가 GET이 아닐 경우 오류 코드 501(Not Implemented)를 출력하고 함수에서 빠져나온다.

 

3. 요청된 URI에서 호스트 이름, 경로(path), 포트를 추출한다. 이 정보는 엔드 서버로의 연결 설정에 필요하다.

 

4. 엔드 서버로 전송할 새로운 HTTP 헤더를 구성한다. 이 과정에서 호스트 이름, 경로, 포트 정보 등을 사용하여 헤더를 작성한다.

 

5. 추출된 호스트 이름과 포트를 사용하여 엔드 서버에 연결을 시도하고, 연결에 성공하면 엔드 서버와의 통신을 위한 새로운 파일 디스크립터(end_serverfd)를 얻는다. 연결 실패 시 오류 메시지를 출력하고 함수에서 빠져나온다.

 

6. 구성한 HTTP 헤더를 엔드 서버로 전송하고 엔드 서버로부터 응답을 받아 클라이언트에게 전달한다. 이 과정에서 프록시 서버는 엔드 서버로부터 받은 데이터를 그대로 클라이언트에게 전송한다.

 

7. 엔드 서버와의 연결을 종료한다.

 

proxy.c(build_http_header)

void build_http_header(char *http_header,char *hostname,char *path,int port,rio_t *client_rio){
  char buf[MAXLINE],request_hdr[MAXLINE],other_hdr[MAXLINE],host_hdr[MAXLINE];
  sprintf(request_hdr,requestlint_hdr_format,path);
  while(Rio_readlineb(client_rio,buf,MAXLINE)>0){
    if(strcmp(buf,endof_hdr)==0) break;
    if(!strncasecmp(buf,host_key,strlen(host_key))){
      strcpy(host_hdr,buf);
      continue;
    }
    if(!strncasecmp(buf,connection_key,strlen(connection_key))&&
    !strncasecmp(buf,proxy_connection_key,strlen(proxy_connection_key))&&
    !strncasecmp(buf,user_agent_key,strlen(user_agent_key))){
      strcat(other_hdr,buf);
    }
  }
  if(strlen(host_hdr)==0){
    sprintf(host_hdr,host_hdr_format,hostname);
  }
  sprintf(http_header,"%s%s%s%s%s%s%s",request_hdr,host_hdr,conn_hdr,prox_hdr,user_agent_hdr,other_hdr,endof_hdr);
  return;
}

 

1. 필요한 헤더 문자열을 저장할 버퍼들을 초기화한다.

 

2. 클라이언트로부터 받은 URI의 경로(path) 정보를 사용하여 HTTP 요청 라인을 생성한다. 예를 들어, GET /path HTTP/1.1과 같은 형태이다.

 

3. 함수를 사용하여 클라이언트로부터 한 줄씩 헤더를 읽는다. 헤더의 끝을 나타내는 빈 줄을 만날 때까지 이 작업을 반복한다.

 

4. 헤더를 처리하고 처리할 때 생성한 요청 라인, Host 헤더, 필수 헤더(Connection, Proxy-Connection, User-Agent), 그리고 다른 헤더들을 하나의 HTTP 요청 헤더로 조합한다.

 

5. 최종적으로 조합된 HTTP 요청 헤더를 http_header에 저장하여 함수를 종료한다.

 

위 코드는 해당 URL yunsejin/proxy 브랜치에 있다.

 

GitHub - yunsejin/Webproxy-lab

Contribute to yunsejin/Webproxy-lab development by creating an account on GitHub.

github.com

728x90

'서버' 카테고리의 다른 글

Express.js Clean Architecture 구성하기  (0) 2024.04.12
echo server, tiny server 구현  (2) 2024.03.24
Socket, Redirection, Pipe  (1) 2024.03.23
Parsing, Caching, Filtering, Load Balancing, MTU, NAT  (1) 2024.03.23

+ Recent posts