tiny

zeo·2021년 9월 22일
0
//tiny.c - a simple, iterative HTTP/1.0 Web server that uses the GET method to serve static and dynamic content

#include "csapp.h"

void doit(int fd);
void read_requesthdrs(rio_t *rp);
int parse_uri(char *uri, char *filename, char *cgiargs);
void serve_static(int fd, char *filename, int filesize);
void get_filetype(char *filename, char *filetype);
void serve_dynamic(int fd, char *filename, char *cgiargs);
void clienterror(int fd, char *cause, char *errnum, char *shortmsg, char *longmsg);


// main 요약) Open_listenfd 함수를 사용해 listenfd를 생성하고,
// accept함수를 통해 연결 요청한 clientfd와 연결
// accept 함수에서 clientaddr에 저장한 client의 주소를 getnameinfo 함수 인자로 넣어
// 서버의 ip 주소와 포트 번호를 얻고 이를 print
// 다음으로 client에서 받은 요청을 처리하는 doit 함수 진행
int main(int argc, char **argv) {
    int listenfd, connfd;
    char hostname[MAXLINE], port[MAXLINE];
    socklen_t clientlen;
    struct sockaddr_storage clientaddr;

    // check command-line args
    if (argc != 2) {
        fprintf(stderr, "usage: %s <port>\n", argv[0]);
        exit(1);
    }

    listenfd = Open_listenfd(argv[1]); //인자로 받은 port에 listenfd 생성
    while (1) {
        clientlen = sizeof(clientaddr);
        // listenfd에 연결 요청한 client의 주소를 sockaddr_storage에 저장
        // client의 주소, 크기를 받아 저장할 곳의 포인터를 인자로 받음
        // accept의 세번째 인자는 일단 addr의 크기를 설정하고(input) 접속이 완료되면
        // 실제로 addr에 설정된 접속한 client의 주소 정보의 크기를 저장
        connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
        
        // accept 함수에서 clientaddr에 저장한 client 주소를 getnameinfo 함수 인자로 넣어
        // 서버의 ip 주소와 port 번호를 얻고 이를 출력함
        Getnameinfo((SA *) &clientaddr, clientlen, hostname, MAXLINE, port, MAXLINE, 0);
        printf("Accepted connection from (%s, %s)\n", hostname, port);
        // client 에서 받은 요청을 처리하는 doit 함수 진행
        doit(connfd);
        Close(connfd);
    }
}

// doit 함수는 1개의 HTTP 트랜잭션 처리
// 즉, 1개의 client 요청을 처리해 client에게 컨텐츠 제공
// 1. client의 HTTP 요청에서 요청 라인만 읽음 (Rio_readlineb을 통해 요청 텍스트의 제일 위 한줄(요청 라인)을 읽음)
// 요청 라인 -> GET/ HTTP /1.1 (method, uri, 요청이 준수하는 http 버전)
// 2. GET 메소드인지 확인(Tiny 서버에서는 최소한의 기능만 충족하도록 GET 메소드만 지원하므로!)
// 3. 요청 헤더는 사용하지 않을 것이기 때문에 읽고 무시
// 4. uri를 분해하여, uri, filename, cigiargs로 나누고 client가 정적 컨텐츠를 원하는지 동적 컨텐츠를 원하는지 확인
// 5. 실행을 원하는 파일의 stat 구조체의 st_mode를 이용해 파일이 읽기 권한과 실행 권한이 있는지 확인

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;

    // read request line and headers
    Rio_readinitb(&rio, fd);
    Rio_readlineb(&rio, buf, MAXLINE); //request line, header 읽기
    printf("Request headers:\n");
    printf("%s", buf);
    //buf에서 공백문자로 구분된 문자열 3개 읽어 각자 method, uri, version에 저장 (method, uri, 요청이 준수하는 http 버전)
    sscanf(buf, "%s %s %s", method, uri, version); 
    
    //GET요청인지 확인
    if (strcasecmp(method, "GET")) { //strcasecmp - 대소문자 구분없이 문자열 비교 함수
        clienterror(fd, method, "501", "Not implemented", "Tiny does not implement this method");
        return;
    }

    // 요청 헤더는 무시
    read_requesthdrs(&rio);

    // URI를 분해하여 URI, filename, cgiargs로 나눔
    // parse URI from GET request
    is_static = parse_uri(uri, filename, cgiargs);
    // stat (파일명 또는 파일 상대/절대 경로, 파일 상태 및 정보를 저장할 buf 구조체)
    // 즉 stat 구조체는, 파일의 정보를 저장하는 구조체로 stat(파일 이름, 정보를 저장할 주소) 함수 실행을 통해 얻을 수 있음

    if (stat(filename, &sbuf) < 0) {
        clienterror(fd, filename, "404", "Not found", "Tiny couldn't find this file");
        return;
    } // 해당 filename이 유효한지 확인
    
    // 정적 컨텐츠 
    if (is_static) { //serve static content
        // 실행 가능한지 확인하는 조건문 -> 일반 파일인지, 읽기 권한을 갖고 있는지 확인
        // S_ISREG -> isregular : 일반 파일인지 확인하는 macro
        // st_mode는 파일의 유형값으로 bit& 연산으로 파일의 유형 확인 가능
        // S_IRUSR -> 읽기 권한이 있는지 
        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);
    }
    // 동적 컨텐츠
    // S_ISREG -> isregular : 일반 파일인지 확인하는 macro
    // S_IXUSR -> 실행 권한이 있는지
    else { //serve dynamic content
        if (!(S_ISREG(sbuf.st_mode)) || ! (S_IXUSR & sbuf.st_mode)) {
            clienterror(fd, filename, "403", "Forbidden", "Tiny couldn't run the CGI program");
            return;
        }
        serve_dynamic(fd, filename, cgiargs);
    }
}


// 일부 명백한 에러에 대해 client에게 HTTP 응답 보냄
void clienterror(int fd, char *cause, char *errnum, char *shortmsg, char *longmsg) {
    char buf[MAXLINE], body[MAXBUF];

    // build the HTTP response body
    sprintf(body, "<html><title>Tiny Error</title>"); // body에 문자열 저장
    sprintf(body, "%s<body bgcolor=""ffffff"">\r\n", body); // 기존 body + 문자열 내용 -> body에 저장
    sprintf(body, "%s%s: %s\r\n", body, errnum, shortmsg); 
    sprintf(body, "%s<p>%s: %s\r\n", body, longmsg, cause);
    sprintf(body, "%s<hr><em>The Tiny Web server</em>\r\n", body);

    //print the HTTP response
    sprintf(buf, "HTTP/1.0 %s %s\r\n", errnum, shortmsg);
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "Content-type: text/html\r\n");
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "Content-length: %d\r\n\r\n", (int)strlen(body));
    Rio_writen(fd, buf, strlen(buf));
    Rio_writen(fd, body, strlen(body));
}

// tiny 웹서버는 요청 헤더 내의 어떤 정보도 사용하지 않음
// 따라서 요청 헤더를 종료하는 빈 텍스트줄(\r\n)이 나올 때까지 요청 헤더를 모두 읽어들임
void read_requesthdrs(rio_t *rp) {
    char buf[MAXLINE];

    Rio_realineb(rp, buf, MAXLINE);
    while(strcmp(buf, "\r\n")) {
        Rio_readlineb(rp, buf, MAXLINE);
        printf("%s", buf);
    }
    return;
}

// parse_uri) URI 분해하는 함수 -> domain, path, cgiargs로 나눔
// cgiargs는, 동적 컨텐츠의 실행 파일에 들어갈 인자
// tiny 서버의 모든 동적 컨텐츠를 위한 실행파일은 cgi-bin이라는 디렉토리에 넣는 고전 방식으로 정적 컨텐츠와 분리시킴
// 따라서 URI에 cgi-bin이라는 경로가 있는지 확인하여 정적컨텐츠와 동적컨텐츠 중 어떤 것을 보낼 것인지 판단 가능

int parse_uri(char *uri, char *filename, char *cgiargs) {
    char *ptr;

    if(!strstr(uri, "cgi-bin")) { //static content
        //strcpy : 문자열 복사 
        //strcat : 문자열 결합
        strcpy(cgiargs, ""); // uri에 cgi-bin과 일치하는 문자열이 없으면,
        // cgiargs에 빈 문자열 저장
        strcpy(filename, "."); // 아래 줄과 더불어 상대 리눅스 경로이름으로 변환(./index.html)
        strcat(filename, uri);
        if (uri[strlen(uri)-1] == '/') // uri가 '/' 문자로 끝난다면 기본 파일 이름 추가
            strcat(filename, "home.html");
        return 1;
    }
    else {
        //dynamic content
        ptr = index(uri, '?');
        if (ptr) { //cgi 인자 추출
            strcpy(cgiargs, ptr+1);
            *ptr = '\0';
        }
        else
        strcpy(cgiargs, "");
        strcpy(filename, ".");
        strcat(filename, uri);
        return 0;
    }
}


// 서버의 정적 컨텐츠(디스크 파일) 처리
// 1. 컨텐츠를 전달하기 전, 전달할 컨텐츠의 내용 및 크기 등 내역을 포함한 response header를 보냄
// 2. 요청한 파일을 읽기 전용으로 열고 파일의 내용을 가상메모리 영역에 저장
// 3. 가상메모리에 저장된 내용을 client와 연결된 연결식별자에 작성하여 컨텐츠를 client에게 보냄

void serve_static(int fd, char *filename, int filesize) {
    int srcfd;
    char *srcp, filetype[MAXLINE], buf[MAXBUF];

    //send response headers to client
    get_filetype(filename, filetype);
    sprintf(buf, "HTTP/1.0 200 OK\r\n");
    sprintf(buf, "%sServer: Tiny Web Server\r\n", buf);
    sprintf(buf, "%sConnection: close\r\n", buf);
    sprintf(buf, "%sContent-length: %d\r\n", buf, filesize);
    sprintf(buf, "%sContent-type: %s\r\n\r\n", buf, filetype);
    Rio_written(fd, buf, strlen(buf));
    printf("Response headers:\n");
    printf("%s", buf);

    //Send response body to client
    // O_RDONLY -> 파일을 읽기 전용으로 열기
    // O_WRONLY -> 파일을 쓰기 전용으로 열기
    // O_RDWR -> O_RDONLY와 O_WRONLY 합치기
    srcfd = Open(filename, O_RDONLY, 0);

    // mmap는 요청한 파일을 가상메모리 영역으로 매핑함
    // Mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset)
    // fd로 지정된 파일에서 offset을 시작으로 length 바이트 만큼 start주소로 대응시키도록 함
    // start 주소는 단지 그 주소를 사용했으면 좋겠다는 정도 이므로 보통 0으로 지정
    // mmap는 지정된 영역에 대응된 실제 시작위치를 반환
    // prot 인자는 원하는 메모리 보호모드(:12)를 설정
    // -> PROT_EXEC - 실행가능, PROT_READ - 읽기 가능, NONE- 접근 불가, WRITE - 쓰기 가능
    // flags는 대응된 객체의 타입, 대응 옵션, 대응된 페이지 복사본에 대한 수정이 
    // 그 프로세스에서만 보일 것인지, 다른 참조하는 프로세스와 공유할 것인지 설정
    // MAP_FIXED - 지정한 주소만 사용, 사용 못할 경우 실패
    // MAP_SHARED - 대응된 객체를 다른 모든 프로세스와 공유
    // MAP_PRIVATE - 다른 프로세스와 대응 영역 공유하지 않음
    srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);
    Close(srcfd);
    Rio_writen(fd, srcp, filesize);
    // 파일을 client에게 전송 -> 주소 srcp에서 시작하는 filesize 바이트를 클라이언트의 연결 식별자로 복사
    Munmap(srcp, filesize);
    // int munmap(void *addr, size_t len);
    // munmap 함수는 addr이 가리키는 영역에 len 크기만큼 할당하여 매핑한 메모리를 해제함
    // 매핑된 가상메모리 주소 반환
    // 파일을 메모리에 매핑한 후 더이상 이 식별자는 필요하지 않으므로 이 파일을 닫아줘야함(메모리 누수 방지)
}
// response header에 들어갈 내용인 클라이언트가 요청한 파일의 타입을 확인
// get_filetype - Derive file type from filename
void get_filetype(char *filename, char *filetype) 
{
    if (strstr(filename, ".html"))
        strcpy(filetype, "text/html");
    else if (strstr(filename, ".gif"))
        strcpy(filetype, "image/gif");
    else if (strstr(filename, ".png"))
        strcpy(filetype, "image/png");
    else if (strstr(filename, ".jpg"))
        strcpy(filetype, "image/jpeg");
    else
        strcpy(filetype, "text/plain");
}

// 동적컨텐츠 처리
// 1. 부모는 메모리를 물려받는 자식 프로세스를 만들고, 새로운 프로세스를 실행할 준비
// 2. exec는 fork와 달리 메모리를 물려받지 않기 때문에 전달하고 싶은 변수는 환경변수로 저장해야함
//    따라서, cgiargs를 query_string이라는 환경변수에 저장
// 3. client와 연결된 connfd를 표준 출력으로 재설정
//    이를 통해 CGI 프로그램이 표준 출력에 쓰는 모든 것은 직접 client 프로세스로 전달됨
// 4. 실행파일(CGI 프로그램)을 새로운 프로세스로 실행. 이때 마지막 인자로 environ을 넣어주면 프로그램 내에서
//    getenv 함수를 통해 기존 설정한 query_string 변수 사용 가능

void serve_dynamic(int fd, char *filename, char *cgiargs) {
    char buf[MAXLINE], *emptylist[] = { NULL };

    //return first part of HTTP response
    sprintf(buf, "HTTP/1.0 200 OK\r\n");
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "Server: Tiny Web Server\r\n");
    Rio_writen(fd, buf, strlen(buf));

    // fork 함수를 호출하는 프로세스는 부모 프로세스가 되고, 새로 생성되는 프로세스는 자식 프로세스가 됨
    // fork 함수에 의해 생성된 자식 프로세스는 부모 프로세스의 메모리를 그대로 복사하여 갖게 됨
    // fork 함수 호출 이후 코드부터 각자의 메모리를 사용해 실행
    // fork()의 반환값 = 부모는 자식프로세스의 PID(프로세스아이디)값, 자식프로세스는 0
    // fork()의 값을 어디 변수에 저장해 놓으면 조건문으로 부모와 자식 프로세스에서 원하는 것 따로 실행 가능
    // pid_t pid = fork()/ if (pid > 0) (=부모){~~} else if (pid == 0) (=자식) {~~}

    if (Fork() == 0) { // 자식 프로세스  //child
        // real server would set all CGI vars here
        // setenv(const char* name, const char* value, int overwrite)
        // 환경변수 "name"을 현재의 환경 리스트에 삽입 또는 재설정
        // overwrite가 0이면 재설정되지 않음, 그 외의 경우 주어진 값에 재설정됨
        // exec 호출하면 명령줄 인수, 환경변수만 전달받음
        // exec 호출하면 코드 영역에 있는 내용을 지우고, 새로운 코드로 바꿈
        // 또한 데이터 영역이 새로운 변수로 채워지고 스택 영역이 리셋됨
        setenv("QUERY_STRING", cgiargs, 1);

        // dup2(fd, fd2) = fd의 값을 fd2로 지정함-> connfd를 STDOUT_FILENO로 바꿈(연결 바꿈)
        Dup2(fd, STDOUT_FILENO); 
        //redirect stdout to client -> 프로세스가 로드되기 전 표준 출력을 클라이언트와 연관된 연결식별자로 재지정
        // 1) 자식은 자신의 표준 출력을 연결 파일 식별자로 재지정하고


        // 경로 또는 파일 이름으로 지정한 실행 파일을 실행하여 프로세스 생성
        // 부모와 자식 다른 작업하며 양쪽 모두 살아있도록 하기 위해 사용
        // execve(실행파일 or 명령어, argv(맨 뒤를 NULL로 넣어줘야 함(argc를 전달할 수 없기 때문)), 전달할 환경변수 - environ에 넣으면 기존에 설정한 환경변수 사용)
        Execve(filename, emptylist, environ); // Run CGI program
        // 2) 자식의 연결 파일 식별자 재지정 후, CGI 프로그램을 로드하고 실행
        // 3) CGI 프로그램이 자식 컨텍스트에서 실행되기 때문에 execve 함수를 호출하기 전에 존재하던 열린 파일들과
        //    환경변수에도 동일하게 접근 가능. 그래서 CGI 프로그램이 표준 출력에 쓰는 모든 것은 
        //    직접 클라이언트 프로세스로 부모 프로세스의 어떤 간섭도 없이 전달됨
    
    }
    Wait(NULL); //parent waits for and reaps child 자식프로세스가 종료되어 정리되기 기다림
}```

0개의 댓글