본문 바로가기
정글

WIL(7주차) Proxy 구현

by 진득한진드기 2023. 5. 25.

이번주는 좀 힘겹게 지나간거 같다.

 

몸도 아파서 정글에 왔을때중 가장 힘들지 않았나 싶다.

 

Pintos 주에 이렇게 안아픈게 다행이라고도 생각한다 ㅋㅋㅋㅋ

 

먼저 이번주는 C언어로 간단한 서버를 만들어보는여서 네트워크에 대한 것들을 공부하고 기초 코드들을 가지고 Proxy를 구현해보았다.

 

기초적인 코드인 tiny서버의 코드는 CSAPP책에서 제공하기 때문에 그것을 가공하면된다.

 

이번주의 최고의 해결

 

간단하게만 보자면 아래는 아래 doit 함수는 main 함수 내에서 무한루프를 돌면서 실행되는 함수이다.

 

* doit - HTTP request/response를 다룬다.
 */
/* $begin doit */
void doit(int fd) 
{
    int is_static;
    struct stat sbuf;
    char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
    char filename[MAXLINE], cgiargs[MAXLINE];
    rio_t rio;

    /* 헤더라인 읽기 */
    Rio_readinitb(&rio, fd);
    printf("%s", buf);
    sscanf(buf, "%s %s %s", method, uri, version);
    if (strcasecmp(method, "GET")) {                     // Get 메소드 아니면 return
        clienterror(fd, method, "501", "Not Implemented",
                    "Tiny does not implement this method");
        return;
    }                                             
    read_requesthdrs(&rio);                              // 헤더 읽기

    /* Get 요청 파싱 */
    is_static = parse_uri(uri, filename, cgiargs);       // staic 인지 체크하기
    if (stat(filename, &sbuf) < 0) {                     // 요청 없으면
	clienterror(fd, filename, "404", "Not found",
		    "Tiny couldn't find this file");
	return;
    }                                                    // 요청있을때

    if (is_static) { /* Serve static content */          
	if (!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR & sbuf.st_mode)) { // 유효성 검사 -> 읽기 권한이 있는지 체크
	    clienterror(fd, filename, "403", "Forbidden",
			"Tiny couldn't read the file");
	    return;
	}
	serve_static(fd, filename, sbuf.st_size);        // static 보내기
    }
    else { /* Serve dynamic content */
	if (!(S_ISREG(sbuf.st_mode)) || !(S_IXUSR & sbuf.st_mode)) { //line:netp:doit:executable
	    clienterror(fd, filename, "403", "Forbidden",
			"Tiny couldn't run the CGI program");
	    return;
	}
	serve_dynamic(fd, filename, cgiargs);            // 다이나믹 CGI 실행
    }
}

 

근데 책에 등장하는 tiny의 이 doit 함수에는 큰 오류가 있다.

 

바로 우리가 컨텐츠를 생성한 컨텐츠를 만지게되면 buf가 남아있게되어 main 무한루프 때문에 printf가 실행되는것이다.

 

대부분 원인이 뭔지 모르고 그냥 지나가지만 생각해보면 그냥 요청이 없으면 출력을 안하고 넘기면 된다.

 

위에서 헤더라인을 읽는 함수 Rio_readinitb(&rio, fd); 이후 요청확인을 하고 아래와 같이 return 해준다.

if (!Rio_readlineb(&rio, buf, MAXLINE))  // 요청이 없으면 return
        return;

 


 

네트워크의 이해

사실상 이번주는 네트워크의 흐름을 코드기준으로 이해하는 시간이 아니었나 싶다.

 

단순히 string으로 보내고 자동으로 json 형식으로 보내고 그러고 넘어가는 부분이 아니라

 

직접 문자열을 받고 socket연결 과정과 어떻게 I/O스트림이 흘러가는지 TCP/IP로 통신할때 어떠한 과정을 가지게 되는지 

 

C언어에서는 어떠한 인터페이스를 제공하는지 하나하나 설명하자면 사실상 책과 다르지 않기 때문에 간단하게 적어놓겠다.

 

흐름은 아래 그림과 같다.

RIO 패키지

 

버퍼 없는 입력 및 출력함수

rio_readn(int fd, void *usrbuf,size_t n), rio_writen(int fd, void *usrbuf,size_t n) : 메모리와 파일 간에 직접 데이터를 전송하는 함수

 

버퍼를 통한 입력 함수

rio_readlienb(rio_t *rp,int fd) :  다음 텍스트 줄을 파일rp에서 읽고 메모리 위치 usrbuf로 복사하고 널문자로 종료시킨다.

 

rio_readnb(rio_t *rp,void *ustbuf, size_t n) : 최대 n 바이트를 파일 rp로 부터 메모리 위치 ustbuf로 읽는다.

 

이 둘의 호출은 동일한 식별자에 대해서 임의로 중첩되어서는 안된다.

 

 

Socket 인터페이스

socket : 소켓 식별자 생성

 

connect: 소켓 주소 addr의 서버와 인터넷 연결을 시도

 

bind: 커널에게 addr에 있는 서버의 소켓 주소를 소켓 식별자 sockfd와 연결하라고 물어봄

 

listen:  sockfd를 능동 소켓에서 듣기 소켓으로 변환하며, 듣기 소켓은 클라이언트로부터 연결 요청을 승락할 수 있다.

 

accpet : 클라이언트로 부터의 연결 요청이 듣기 식별자 listenfd에 도달하기를 기다리고, 그 후에 addr 내의 클라이언트의 소켓 주소를 채우고, 클라이언트와 통신하기 위해 사용될 수 있는 연결 식별자를 리턴

 


 

Proxy(프록시)는 이름 그대로 대체자이다.

 

간단하게 보면 대체자의 역할을 하는 서버를 하나 만드는 것이다.

 

클라이언트와 Web 서버 중간에 위치하고 있어서, 통신을 받아 주는 서버이다.

 

프록시 서버를 왜 사용할까?

당장 간단한 서버를 구현하는 것이라 추가과제인 cache와 hashmap을 구현하지는 못했지만

 

캐시

클라이언트에게 온 요청을 프록시 서버에 저장하여 다시 동일한 페이지를 요청했을때 캐시에 남은 정보를 클라이언트에게 주는 것이가능하다.

 

URL 보호

외부의 엑세스 프록시 서버를 경유하므로 외부 웹사이트로의 엑세스를 필터링 할 수 있다.

 

정보보호

hashmap을 사용해서 정보를 보호하여 바로 Web서버로 갔을때의 정보의 침해를 예방할 수 있게 된다.

 

Proxy 구현

#include <stdio.h>
#include "csapp.h"

/* 권장되는 최대 캐시 및 오브젝트 크기 */
#define MAX_CACHE_SIZE 1049000
#define MAX_OBJECT_SIZE 102400

static const char *user_agent_hdr =
    "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:10.0.3) Gecko/20120305 "
    "Firefox/10.0.3\r\n";
static const char *new_version = "HTTP/1.0";

/* 함수 프로토타입 */
void *thread_func(void *arg);
void handle_request(int proxy_connfd);
void send_request(int p_clientfd, char *method, char *uri_ptos, char *host);
void handle_response(int p_connfd, int p_clientfd);
int parse_uri(char *uri, char *uri_ptos, char *host, char *port);

int main(int argc, char **argv)
{
  int listenfd;
  char hostname[MAXLINE], port[MAXLINE];
  socklen_t clientlen;
  struct sockaddr_storage clientaddr;
  pthread_t tid;

  /* 명령행 인수 확인 */
  if (argc != 2)
  {
    fprintf(stderr, "사용법: %s <포트>\n", argv[0]);
    exit(1);
  }
  /* 지정된 포트에 대한 수신 소켓 생성 */
  listenfd = Open_listenfd(argv[1]);

  while (1)
  {
    clientlen = sizeof(clientaddr);
    int *connfdp = malloc(sizeof(int));
    *connfdp = Accept(listenfd, (SA *)&clientaddr, &clientlen);

    /* 각 클라이언트 연결마다 새로운 스레드 생성 */
    pthread_create(&tid, NULL, thread_func, connfdp);
  }
  return 0;
}

void *thread_func(void *arg)
{
  int p_connfd = *((int *)arg);
  pthread_detach(pthread_self());
  Free(arg);

  handle_request(p_connfd);
  Close(p_connfd);

  return NULL;
}

/*
파싱 전 (클라이언트로부터 받은 요청 라인)
=> GET http://www.google.com:80/index.html HTTP/1.1
​
파싱 결과
=> host = www.google.com
=> port = 80
=> uri_ptos = /index.html
​
파싱 후 (서버로 보낼 요청 라인)
=> GET /index.html HTTP/1.0
*/

void handle_request(int proxy_connfd)
{
  int server_connfd;
  char buf[MAXLINE], host[MAXLINE], port[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
  char transformed_uri[MAXLINE];
  rio_t rio;

  /* 클라이언트로부터 요청 라인과 헤더 읽기 */
  Rio_readinitb(&rio, proxy_connfd); // rio 버퍼를 프록시의 연결 파일 디스크립터(proxy_connfd)와 연결
  Rio_readlineb(&rio, buf, MAXLINE); // rio(프록시의 연결 파일 디스크립터)에서 한 줄(요청 라인)을 읽고 buf에 저장
  printf("프록시로부터의 요청 헤더:\n");
  printf("%s", buf);
  sscanf(buf, "%s %s %s", method, uri, version); // buf에서 세 개의 문자열을 읽어서 각각 method, uri, version에 저장

  /* GET 요청에서 URI 파싱 */
  parse_uri(uri, transformed_uri, host, port);

  server_connfd = Open_clientfd(host, port);                  // 서버에 연결하고 서버의 연결 파일 디스크립터(server_connfd)를 가져옴
  send_request(server_connfd, method, transformed_uri, host); // 서버의 연결 파일 디스크립터에 요청 헤더를 보내고 동시에 서버의 연결 파일 디스크립터에도 씀
  handle_response(proxy_connfd, server_connfd);
  Close(server_connfd); // 서버 연결 파일 디스크립터 닫기
}

/* send_request: 프록시 => 서버 */
void send_request(int p_clientfd, char *method, char *uri_ptos, char *host)
{
  char buf[MAXLINE];
  printf("서버로 보내는 요청 헤더: \n");
  printf("%s %s %s\n", method, uri_ptos, new_version);

  /* 요청 헤더 읽기 */
  sprintf(buf, "GET %s %s\r\n", uri_ptos, new_version);   // GET /index.html HTTP/1.0
  sprintf(buf, "%sHost: %s\r\n", buf, host);              // Host: www.google.com
  sprintf(buf, "%s%s", buf, user_agent_hdr);              // User-Agent: ~(bla bla)
  sprintf(buf, "%sConnections: close\r\n", buf);          // Connections: close
  sprintf(buf, "%sProxy-Connection: close\r\n\r\n", buf); // Proxy-Connection: close

  /* Rio_writen: buf에서 p_clientfd로 strlen(buf)바이트 전송 */
  Rio_writen(p_clientfd, buf, (size_t)strlen(buf)); // => 요청을 보내는 행위 자체
}

/* handle_response: 서버 => 프록시 */
void handle_response(int p_connfd, int p_clientfd)
{
  char buf[MAX_CACHE_SIZE];
  ssize_t n;
  rio_t rio;

  Rio_readinitb(&rio, p_clientfd);           //
  n = Rio_readnb(&rio, buf, MAX_CACHE_SIZE); // 최대 MAXLINE까지 안전하게 모두 읽음
  Rio_writen(p_connfd, buf, n);
}
/* parse_uri: (클라이언트로부터 받은) GET 요청에서 URI 파싱, 서버로의 GET 요청을 위해 필요 */
int parse_uri(char *uri, char *uri_ptos, char *host, char *port)
{
  char *ptr = strstr(uri, "://");
  if (!ptr)
    return -1;
  ptr += 3;
  sscanf(ptr, "%[^:/]:%[^/]%s", host, port, uri_ptos);
  if (strcmp(port, "") == 0)
    strcpy(port, "80");
  if (strcmp(uri_ptos, "") == 0)
    strcpy(uri_ptos, "/");
  return 0;
}

'정글' 카테고리의 다른 글

(Pintos 1주차) Priority scheduling  (0) 2023.05.31
Pintos(1일차 후기) 쓰레드  (0) 2023.05.28
TIL(27일차) 네트워크  (0) 2023.05.23
(TIL26일차) 동시성 프로그래밍  (1) 2023.05.20
WIL(6주차) Malloc-lab 구현  (1) 2023.05.20