[FTP] (2) 클라이언트 구현하기

네트워크로그 2011. 3. 9. 02:22



다음은 c언어로 구현한  open, get, put, ls, cd, pwd, quit/bye, hash 명령을  구현한 간단한 FTP 클라이언트 예제이다. 

에러처리와 소켓의 recv, send 함수의 동작방식을 정확히 고려하지 않았기 때문에, 정확히 동작하지 않을 수 있다.   

데이터 수신시 하나의 명령에 대한 메시지의 끝을 구분해 줄 수 있는 방법이 추가로 필요하다. 

(메시지 끝을 의미하는 \r\n 로 구분)




FtpCommand.h

 : Ftp 커맨드 및 상수를  정의한 헤더파일 

#define CMD_OPEN "open"
#define CMD_LIST "ls"
#define CMD_GET "get"
#define CMD_PUT "put"
#define CMD_PWD "pwd"
#define CMD_CD "cd"
#define CMD_QUIT "quit"
#define CMD_BYE "bye"
#define CMD_HASH "hash"
#define CMD_SHELL "!"

#define MODE_DEBUG 1
#define MODE_NORMAL 0

#define FTP_PORT 21

extern int mode;

void debug(char *msg) {
        if (mode == MODE_DEBUG) {
                printf("[debug] : %s \n", msg);
        }
}



ClientSocket.h

: Ftp Client 구현 파일로 단일 프로세스만을 사용하였음 

#include <sys/types.h>
#include <sys/socket.h> 
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h> 
#include <sys/types.h> 
#include <sys/stat.h>
#include <fcntl.h>

#define TEMP_BUFFER_SIZE 1024

int connectServer(char *serverIp, short port) {
        int sock;
        struct sockaddr_in servAddr;
        
        if ((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
                perror("sock failed");
                exit(1);
        }
        
        memset(&servAddr, 0, sizeof(servAddr));
        servAddr.sin_family = AF_INET;
        servAddr.sin_addr.s_addr = inet_addr(serverIp);
        servAddr.sin_port = htons(port);
        
        if (connect(sock, (struct sockaddr*)&servAddr, sizeof(servAddr)) == -1) {
                perror("connect failed");
                exit(1);
        }
        
        return sock;
}


void sendProtocol(int sock, char *protocol) {
        if (send(sock, protocol, strlen(protocol), 0) != strlen(protocol)) {
                perror("send failed");
                exit(1);
        }
	if (MODE_DEBUG == mode) {
		printf("send: %s", protocol);
	}
}

void recvProtocol(int sock, char *recvBuffer, int bufferSize) {
        int recvLen;
        
        if ((recvLen = recv(sock, recvBuffer, bufferSize-1, 0)) <= 0) {
                perror("recv failed");
                exit(1);
        }
        recvBuffer[recvLen] = '\0';

	if (MODE_DEBUG == mode) {
		printf("recv: %s", recvBuffer);
	}
}


unsigned int downloadFile(int sock, char *filePath, unsigned int fileSize, int hashFlag) {
        char readBuffer[TEMP_BUFFER_SIZE];
        unsigned int readBytes, totalBytes, numHash;
        
        int fd = open(filePath, O_WRONLY | O_CREAT, 0744);
        
        totalBytes = numHash = 0;
        while (totalBytes < fileSize) {
                if ((readBytes = read(sock, readBuffer, TEMP_BUFFER_SIZE)) <= 0) {
                        close(fd);
                        return totalBytes;
                }
                write(fd, readBuffer, readBytes);
                totalBytes += readBytes;
                
                if (hashFlag) {
                        if ((totalBytes/TEMP_BUFFER_SIZE) > numHash) {
                                numHash++;
                                printf("#");
                        }
                }
        }
        close(fd);
        printf("\n");
        
        return totalBytes;
}

unsigned int uploadFile(int sock, char *filePath, int hashFlag) {
        char readBuffer[TEMP_BUFFER_SIZE];
        unsigned int readBytes, totalBytes, numHash;
        
        int fd = open(filePath, O_RDONLY);
        
        totalBytes = numHash = 0;
        while ((readBytes = read(fd, readBuffer, TEMP_BUFFER_SIZE)) > 0) {
                write(sock, readBuffer, readBytes);
                totalBytes += readBytes;
                
                if (hashFlag) {
                        if ((totalBytes/TEMP_BUFFER_SIZE) > numHash) {
                                numHash++;
                                printf("#");
                        }
                }
        }
        close(fd);
        printf("\n");
        
        return totalBytes;
}



FtpClient.c

:  Ftp Client 구현 파일로 단일 프로세스만을 사용하였음

#include <stdio.h>
#include <string.h> 
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h> 
#include <netinet/in.h> 
#include <arpa/inet.h> 
#include <unistd.h> 
#include "FtpCommand.h"
#include "ClientSocket.h"

#define COMMAND_MAX_SIZE 1024
#define BUFFER_SIZE 1024
#define FILENAME_SIZE 256
#define END_OF_PROTOCOL "\r\n"

void initializeFtpClient();
void startFtpClient(char *ip, char *port);
void commandHandle(char *cmd); 
void defaultHandler(char *cmd);
int modeCheck(const char *option);
void printMessage(char *msg);

void openCon(char *cmd);
void list(char *listCmd);
void get(char *getCmd);
void put(char *putCmd);
void cd(char *cdCmd);
void quit(char *quitCmd);
void pwd(char *pwdCmd);
void bye(char *byeCmd);
void hash(char *hashCmd);
void passiveMode(char *ip, int *port);
void shellEscape(char *shellCmd);

unsigned int downloadFile(int sock, char *filePath, unsigned int fileSize, int hashFlag);
unsigned int uploadFile(int sock, char *filePath, int hashFlag);


typedef struct _FtpCmdHandler {
        char cmd[5];
        void (*handler)(char* arg);
        
} FtpCmdHandler;

// Map Ftp Command to Handler
FtpCmdHandler ftpCmdHandler[] = {
        { CMD_OPEN, openCon },
        { CMD_LIST, list },
        { CMD_PUT, put },
        { CMD_GET, get },
        { CMD_CD, cd },
        { CMD_PWD, pwd },
        { CMD_HASH, hash },
        { CMD_QUIT, quit },
        { CMD_BYE, bye },
        { CMD_SHELL, shellEscape },
        
};

// sock - PI socket ,  dtpSock - DTP socket
int sock, dtpSock;
int state;      
int mode;
int hashFlag;


int main (int argc, char const *argv[])
{
 	if (argc == 3) {
	// argv[1] == ip, argv[2] == port
		startFtpClient(argv[1], argv[2]);
	} else if (argc == 4 && modeCheck(argv[1]) == MODE_DEBUG) {
	// argv[1] == -d, argv[2] = ip, argv[3] == port
		startFtpClient(argv[2], argv[3]);
	}  else {
		fprintf(stderr, "Usage: %s [-d]  \n", argv[0]);
	}
                
        return 0;
}

int modeCheck(const char *option) {
        if (!strcmp(option, "-d")) {
                // debug mode
                mode = MODE_DEBUG;
        } else {
                mode = MODE_NORMAL;
        }
        return mode;
}

// initialize ftp client
void initializeFtpClient() {
        hashFlag = 1;
        state = INITIAL_STATE;  
        debug("initialized");
}



// ftp client start 
void startFtpClient(char *ip, char *port) {   
        char cmd[COMMAND_MAX_SIZE];

        initializeFtpClient();
        
        while (1) { 
                // input user command
                if (ip == 0 && port == 0) {
                        printMessage("ftp>");
                        fgets(cmd, COMMAND_MAX_SIZE, stdin);
                } else {
                        sprintf(cmd, "open %s %s", ip, port);
                        startCmd = 0;
                }       
                // call handler 
                commandHandle(cmd);
        }

}

// map command to handler 
void commandHandle(char *cmd) {
        int i;
        int numCmd = sizeof(ftpCmdHandler)/sizeof(FtpCmdHandler);
        
        for (i = 0; i < numCmd; i++) {
                if (!strncmp(cmd, ftpCmdHandler[i].cmd, strlen(ftpCmdHandler[i].cmd))) {
                        (*(ftpCmdHandler[i].handler))(cmd);
                        break;
                }
        }       
}


void defaultHandler(char *cmd) {
        printf("default handler: %s\n", cmd);
}

// ftp server connect 
void openCon(char *openCmd) {
        char serverIp[16], serverPort[16];
        char cmd[BUFFER_SIZE];
        char sendBuffer[BUFFER_SIZE];
        char recvBuffer[BUFFER_SIZE];
        
        sscanf(openCmd,"%*s %s %s%*c", serverIp, serverPort);
        debug(serverIp);
        
        // connect to server
        sock = connectServer(serverIp, atoi(serverPort));
        recvProtocol(sock, recvBuffer, BUFFER_SIZE-1);
        
        // send user name
        printf("Name: ");
        fgets(cmd, COMMAND_MAX_SIZE, stdin);
        sprintf(sendBuffer, "User %s", cmd);
        sendProtocol(sock, sendBuffer);
        recvProtocol(sock, recvBuffer, BUFFER_SIZE-1);
        printMessage(recvBuffer);

        // send password
        printf("Password: ");
        fgets(cmd, COMMAND_MAX_SIZE, stdin);
        sprintf(sendBuffer, "PASS %s", cmd);
        sendProtocol(sock, sendBuffer);
        recvProtocol(sock, recvBuffer, BUFFER_SIZE-1);
        printMessage(recvBuffer);
                
        // get server os information    
        sprintf(sendBuffer, "SYST%s", END_OF_PROTOCOL);
        sendProtocol(sock, sendBuffer);
        recvProtocol(sock, recvBuffer, BUFFER_SIZE-1);
        printMessage(recvBuffer);

}


// send EPSV or PASS to Server
void passiveMode(char *ip, int *port) {
        char sendBuffer[BUFFER_SIZE];
        char recvBuffer[BUFFER_SIZE];
        int host0, host1, host2, host3;
        int port0, port1;
        
        sprintf(sendBuffer, "PASV%s", END_OF_PROTOCOL);
        sendProtocol(sock, sendBuffer);
        recvProtocol(sock, recvBuffer, BUFFER_SIZE-1);
        printMessage(recvBuffer);
        
        sscanf(strchr(recvBuffer, '(')+1, "%d,%d,%d,%d,%d,%d", &host0, &host1, &host2, &host3, &port0, &port1);
        sprintf(ip, "%d.%d.%d.%d", host0, host1, host2, host3);

        *port = port0*256 + port1;
        
        debug(ip);
        printf("dtp port : %d\n", *port);
}


// get remote working directory file list
void list(char *listCmd) {
        int port;
        char ip[16];
        char sendBuffer[BUFFER_SIZE];
        char recvBuffer[BUFFER_SIZE*8];
        
        debug("list");

        // recv server response and parsing 
        passiveMode(ip, &port);
        
        // connect to DTP  
        dtpSock = connectServer(ip, port);
        
        // send LIST command to PI server
        sprintf(sendBuffer, "LIST%s", END_OF_PROTOCOL);
        sendProtocol(sock, sendBuffer);
        recvProtocol(sock, recvBuffer, BUFFER_SIZE);
        printMessage(recvBuffer);
        
        // recv file list from DTP
        recvProtocol(dtpSock, recvBuffer, BUFFER_SIZE*8);
        printMessage(recvBuffer);
        
        // recv complete message from PI server
        recvProtocol(sock, recvBuffer, BUFFER_SIZE);
        printMessage(recvBuffer);
        
        close(dtpSock);
}


// file download
void get(char *getCmd) {
        int port;
        unsigned int fileSize;
        char ip[16], filePath[FILENAME_SIZE], fileName[50];
        char sendBuffer[BUFFER_SIZE];
        char recvBuffer[BUFFER_SIZE];
        
        // get local current working directory 
        getcwd(filePath, FILENAME_SIZE);
        sscanf(getCmd, "%*s %s%*c", fileName);
        sprintf(filePath, "%s/%s", filePath, fileName);

        debug("get");
        printf("fileName: %s\n", fileName);
        printf("filePath: %s\n", filePath);

        passiveMode(ip, &port);
        
        // connect to DTP
        dtpSock = connectServer(ip, port);
        
        // request server for transfer start - RETR fileName
        sprintf(sendBuffer, "RETR %s%s", fileName, END_OF_PROTOCOL);
        sendProtocol(sock, sendBuffer);
        recvProtocol(sock, recvBuffer, BUFFER_SIZE);
        printMessage(recvBuffer);
        
        // extract fileSize 
        sscanf(strchr(recvBuffer, '(')+1, "%u", &fileSize);
        printf("fileSize: %u\n", fileSize);
        
        // download file from DTP 
        downloadFile(dtpSock, filePath, fileSize, hashFlag);
        
        // recv complete message from PI server
        recvProtocol(sock, recvBuffer, BUFFER_SIZE);
        printMessage(recvBuffer);
        
        close(dtpSock);
}




// file upload
void put(char *putCmd) {
        int port;
        unsigned int fileSize;
        char ip[16], filePath[FILENAME_SIZE], fileName[50];
        char sendBuffer[BUFFER_SIZE];
        char recvBuffer[BUFFER_SIZE];
        
        sscanf(putCmd, "%*s %s%*c", fileName);
        
        // get local current working directory
        getcwd(filePath, FILENAME_SIZE);
        sscanf(putCmd, "%*s %s%*c", fileName);
        sprintf(filePath, "%s/%s", filePath, fileName);

        debug("put");
        debug(filePath);
        
        passiveMode(ip, &port);
        
        // connect to DTP
        dtpSock = connectServer(ip, port);
        
        // request server for transfer start - STOR fileName
        sprintf(sendBuffer, "STOR %s%s", fileName, END_OF_PROTOCOL);
        sendProtocol(sock, sendBuffer);
        recvProtocol(sock, recvBuffer, BUFFER_SIZE);
        printMessage(recvBuffer);
        
        // file upload to DTP 
        fileSize = uploadFile(dtpSock, filePath, hashFlag);
        
        close(dtpSock);
        
        /// recv complete message from PI server
        recvProtocol(sock, recvBuffer, BUFFER_SIZE);
        printMessage(recvBuffer);
}


// change remote working directory
void cd(char *cdCmd) {
        char sendBuffer[BUFFER_SIZE];
        char recvBuffer[BUFFER_SIZE];
        debug("cd");

        sscanf(cdCmd, "%*s %s%*c", recvBuffer);
        debug(recvBuffer);

        sprintf(sendBuffer, "CWD %s%s", recvBuffer, END_OF_PROTOCOL);
        sendProtocol(sock, sendBuffer);
        recvProtocol(sock, recvBuffer, BUFFER_SIZE);
        printMessage(recvBuffer);
}

// ftp client exit 
void quit(char *quitCmd) {
        char sendBuffer[BUFFER_SIZE];
        char recvBuffer[BUFFER_SIZE];
        debug("quit");
        
        sprintf(sendBuffer, "QUIT%s", END_OF_PROTOCOL);
        sendProtocol(sock, sendBuffer);
        recvProtocol(sock, recvBuffer, BUFFER_SIZE);
        printMessage(recvBuffer);
        
        close(sock);
        exit(0);
}

// same quit  
void bye(char *byeCmd) {
        quit(0);
}

// get remote working directory
void pwd(char *pwdCmd) {
        char sendBuffer[BUFFER_SIZE];
        char recvBuffer[BUFFER_SIZE];
        debug("pwd");
        
        sprintf(sendBuffer, "PWD%s", END_OF_PROTOCOL);
        sendProtocol(sock, sendBuffer);
        recvProtocol(sock, recvBuffer, BUFFER_SIZE);
        printMessage(recvBuffer);
}


// hash option on/off
void hash(char *hashCmd) {
        debug("hash");
        hashFlag = !hashFlag;
        
        if (hashFlag == 0) {
                printMessage("hash off");
        } else {
                printMessage("hash on");
        }
}

// shell command - not implemented
void shellEscape(char *shellCmd) {
        printMessage("not implemented");
}



void printMessage(char *msg) {
        printf("%s", msg);
}





[FTP] (1) FTP(File Transmission Protocol) 프로토콜 이해

네트워크로그 2011. 3. 9. 01:44



응용계층 프로토콜에 한 종류인 FTP에 대해 알아보고,  C언어를 사용하여 간단한 FTP 프로그램을 만들어보자.  

 


1. FTP 란? 

FTP 는 2대의 컴퓨터 간에 파일 전송을 위한 애플리케이션 프로토콜로 RFC 959에 정의되어 있다. 

Transport  계층 프로토콜로 TCP를 사용하며,  클라이언트/서버 모델 구성을 가지고 있으며,  FTP 서버는

21번 포트에서 FTP 클라이언트의 접속을 기다린다. FTP는 텔넷과는 달리,  클라이언트와 서버간에 2개의 

커넥션을 맺어 통신을 한다. 제어 명령을 송수신하는 PI(Protocol Interpreter) 프로세스와, 데이터를 송수신하는

DTP(Data Transmission Process) 로 구성된다. 





2. Active 모드 vs Passive 모드 

데이터를 전송하는 방식에 따라 Active 모드와 Passive 모드가 존재한다. 서버를 기준으로 했을 때,  서버가 

클라이언트에 접속하여 데이터를 송수신하는 것을  Active 모드라 하며,  클라이언트가 서버에 접속하여 

데이터를 송수신하는 것이 Passive 모드이다. 

 




3.  FTP  애플리케이션의 구조 

FTP 애플리케이션의 구조는 다음과 같다. 



 

 알툴즈의  알 FTP 와 같이 그래픽 유저 인터페이스를 제공하는 FTP 클라이언트도 있는가 하면, 유닉스/리눅스에서와 같이   

터미널에서 동작하는  텍스트 인터페이스의 FTP 클라이트도 존재한다. 하지만 어떤 클라이언트 형태는 RFC959에 정의된 

FTP 프로토콜에 따라 내부적으로 FTP 서버와 통신하게 된다. 

 

FTP 클라이언트 인테페이스를 통해, FTP 명령을 치면 클라이언 PI와 서버 PI간에 통신이 이루어진다. 작업디렉토리에 파일목록

출력과 데이터 업로드, 다운로드와 같은 작업들은 PI 를 통해 초기화가 이루어지면, 실제 데이터 전송은 서버와 클라이언트의 DTP 

간에 별도의 커넥션을 통해 이루어진다.

 PI와 DTP는 구조상에  개념이며, 반드시 FTP 클라이어트가 2개의 프로세스로 구성되어야  하는 것은 아니다. 

한 개의 프로세스여도, 프로토콜 전송 주소와 포트, 절차만을 준수하면 이상없이 서버와 통신할 수 있다. 

 




4.  FTP 명령 / 명령을 완료하기 위해 전송되는 프로토콜


FTP 클라이언트에서 사용할 수 있는 명령들은 다음과 같다. 더 많지만 자주 사용되는 명령들만을 간추려 보았다. 
 

커맨드 기능 프로토콜 
 open
<아이피 주소 또는 호스트명>
 해당 주소의 FTP 서버로 접속한다. 접속이 성공하면 Name과 Password를
입력하여  로그인할 수 있다.

 커넥트

 USER

 PASS

 SYST

 pwd  현재 원격(서버) 작업 디렉토리를 얻어온다.  PWD
 cd <디렉토리>  원격 작업 디렉토리를 변경한다   CWD
 ls  현재 원격 작업디렉토리의 파일목록을 얻어온다.

 PASV

 LIST

 hash  파일 업로드/다운로드시 1024바이트 단위로 해시문자 출력 옵셥을 on/off  한다.    
 get <파일명>   서버에 파일을 로컬로 다운로드한다. 

 PASV

 GET

 put <파일명>  로컬 파일으로 서버로 업로드한다. 

 PASV

 PUT

 quit/bye  FTP를 종료한다.   QUIT
 !<쉘명령>  셀 이스케이프. 쉘 명령을 수행하고, FTP 로 되돌아온다.   
ascii / binary  파일을 아스키 또는 바이너리로 전송한다.   TYPE I./A
 ?  FTP 명령 사용법을 출력한다.   

 

  커맨드는 텍스트 기반 FTP 클라이언트에서 사용자게 터미널을 통해 입력받는 명령이며, 이러한 명령의 기능을 수행하기 위해

 클라이언트는 서버에게  한개 이상의 프로토콜을  전송하게 된다. 클라이언트가 서버에게 전송하는 프로토콜은 텍스트이며, 

프토토콜의 종결문자로 \r\n을 사용한다.  다음은 몇가지 FTP 프로토콜이다. 

 


4.1 FTP 프로토콜

커맨드 내용 비고
 USER 사용자명  FTP 계정에 사용자 명을 전송한다   
 PASS 비밀번호  FTP 계정에 사용자 비밀번호를 전송한다  
 SYST  FTP 서버에 운영체제 종류를 문의한다.   
 PWD  현재 작업디렉토리 경로를 얻어온다.   
 CWD 디렉토리  인자로 주어진 디렉토리로 현재 작업디렉토리를 변경한다    
 PASV  passive 모드로 전환한다. 서버는 클라이언트의 접속을 기다릴 주소와 포트를 쉼표를 구분자로 하여 (127,0,0,1,234,13) 의 포맷으로 응답해준다.   
 LIST  현재 작업 디렉토리의 파일목록을 얻어온다.   
 GET 파일명  원격 작업디렉토리 상에 파일을 로컬로 다운로드 한다.   
 PUT 파일명   로컬의 파일을 원격 작업디렉토리로 업로드 한다.   
 RETR  로컬로 파일 다운로드시 클라이언트가 서버에게 데이터 전송시작을 요청한다.   
 STOR  파일 업로드시 클라이언트가 서버에게 데이터 전송시작을 요청한다.   
 PORT h0,h1,h2,h3,p0,p1   Active 모드로 데이터 전송시  서버에게 클라이언트가 대기할 주소와 포트를 쉼표를 구분자로 전송한다. p0, p1은 2바이트 포트의 각 바이트의 십진수값이다  
 QUIT  연결을 종료한다.   


 


4.2  데이터 전송모드

데이터 전송은 passive 모드와  active 모드로 전송할 수 있으며, 그 개념상에 차이는 이전에 설명했다. active 모드로 데이터 

전송시  클라이언트는   PORT 명령을 통해 서버가 접속할 수 있도록 클라이언트가 대기할 주소와 포트번호를 전송한다. 


 

PORT 202,13,180,230,4,103 


 앞에 2개의 수는 IP 를 구성하고 있는 수들이며, 4, 103은 2바이트 포트번호의 각 바이트를 십진수로 표현한 값(4x256 + 103)이다.

 즉 위 프로토콜을 해석하면  202.13.180.230 주소, 1127 포트에서 대기할 테니 여기로 서버는 접속하시오 ~~ 라는 의미가 

되겠다.  이렇게  서버가 클라이언트에게 접속하면, 클라이언트는 바로 데이터를 전송하면 된다. 

 passive 모드에서는 클라이언트는 PASV 프르토콜을 전송하여 passive 모드로 데이터를 전송받겠다는 것을 서버에게 알리면, 

서버는 위와 통일한 포맷으로 서버가 대기할 주소와 포트번호를 알려준다. 클라이언트는 이 값을 읽어들여 해당 주소로 접속한다. 

이후 LIST, RETR, STOR 을 전송하여 실제 데이터 전송을 시작해달라고 요청하게 된다. 

 

다음은 이를 나타내고 있는 그림이다. 



 




바이트 오더링(byte ordering)

네트워크로그 2011. 3. 8. 15:04



바이트 오더는 데이터가 바이트 단위로 메모리에 저장되는 순서를 의미하며 각 CPU 벤더 의존적인 특징을 가지고 있다.
크게 Big endian과 Little   endian 방식이 존재하며,  Little Endian을 사용하는 대표적인 벤더가 인텔이며, Big endian을 사용하는
벤더가 AMD 이다.  이기종 간에 통신을 하는 네트워크 프로그래밍에서는  두 종단 간에 올바로 통신하기 위해서는 통일된
방식이 필요하게 되는데, 네트워크 바이트 오더 표준은 Big Endian 방식이다. 네트웍을 통한 데이터 전송은 Big Endian을 통해
전송하며, 데이터를 송수신 하는 곳에서는 각자의 플랫폼에 맞는 바이트 오더의 변환이 필요하게 된다.  물론 바이트 오더는
2바이트 이상의 프리미티브 타입에 적용되는 내용이다. 1바이트 단위의 문자열 데이터는 바이트 오더의 변경이 불필요하기
때문이다.



Big endian과 Little endian 간의 차이를 다음 그림을 통해 알아보자 

Big-endian은 가장 최상위 바이트(0A)가 가장 낮은 메모리 주소에 저장되는 방식이다.




Little-endian은 가장 최상위 바이트(0A)가 가장 높은 메모리 주소에 저장되는 방식이다. 




다음은 위의 개념을 이용하여 공용체로 바이트 오더를 알아보는 간단한 프로그램이다.  
 

#include <stdio.h>

int main(int argc, char **argv){    
    union {      
        short s;        
        char c[sizeof(short)];    
    } un;  

    un.s = 0x0102;      

    if (sizeof(short) == 2) {        
        if (un.c[0] == 1 && un.c[1] == 2)           
            printf("big-endian\n");        
        else if (un.c[0] == 2 && un.c[1] == 1)            
            printf("little-endian\n");       
        else            
            printf("unknown\n");   
    } else{      
            printf("sizeof(short) = %d\n", sizeof(short));   
    }   
    return 0;
}




OAuth의 세부사항

네트워크로그 2011. 1. 1. 03:05




지난 포스팅에 이어 OAuth의 세부사항에 대해 알아보자

① Request Token 요청 / ③ Access Token 요청 / ④ Protected Resources 요청 단계에서 적절한 oauth 파라미터를 함께 전달해주어야 
하는데 이 파라미터들에 대해서 알아보자. Consumer는 ServiceProvider 에게 3가지 방법 중 하나를  통해 파라미터를 전달할 수 있다. 
1) HTTP 헤더에 Authorization 헤더를 통해 전달한다. 
2) HTTP 헤더의 POST request body를 통해 전달한다. (이때 Content-Type 헤더는 application/x-www-form-urlencoded로 설정함)
3) URL의 쿼리 파트를 통해 전달한다. 

어느 방법을 선택해도 되지만, 이 포스팅에서는 첫번째 방법을 설명하겠다.  전체적인 흐름을 나타낸 그림은 다음과 같다. 



oauth 파라미터
oauth  파라미터는 OAuth 스펙에서 요구하는 "oauth_ " 접두어를 갖는 파라미터를 의미한다. oauth 파라미터의 종류와 그 의미는 
다음과 같다. 

  oauth_version  OAuth 프로토콜의 버전으로 1.0으로 설정하며, 선택적인
 파라미터 값이다
 oauth_consumer_key   ServiceProvider 에서 제공하는 Consumer 를 고유하게 식별하기 위한 값
 oauth_token   request token 값으로 Request token 요청시에는 사용되지 않으며,
  Access token 요청과 자원조회시에는 사용된다. 
 oauth_timestamp   GMT 1970년 1월 1일 00:00:00 이후 현재까지의 흐른 초로,  밀리초가 아닌 초 단위 임에 주의하자 
 oauth_nonce  reply attack 을 예방하기 위해,  동일한 timestamp 를 갖는 요청마다 유일한  랜덤값이다. 
 oauth_signature_method  oauth 파라미터 값들이 도중에 변경되지 않았음을 보장하기 위해 사용하는 해시 알고리즘의 종류로, HAC-SHA1,  RSA-SHA1
등이 있다. 이 포스팅에서는  HAC-SHA1 를 사용한다.  
 oauth_signature  HMAC-SHA1 를 사용하여 생성한 알고리즘의 결과값이다.
 이 알고리즘 적용에 필요한 키와 데이터에 대해서는 아래에서 자세히 다루겠다. 
 oauth_callback  Authorize URL 에 접속하여 사용자의 허용을 받고 리다이렉트
시킬 URL 이다. Consumer가 웹서비스가 아닌 애플리케이션이면
 "oob"  로 설정해야 한다. (oob는 out of band의 약자이다.)
 oauth_verifier  사용자 허용을 통해 응답받은 값으로, Access token 요청시에만 설정한다.  

각 단계별로 필요한 파라미터들을 요약한 결과는 다음과 같다. 
Request token URL  요청: oauth_consumer_key,  oauth_callback, oauth_timestamp, oauth_nonce, oauth_signature_method, oauth_signature
                                            oauth_version
Access token  URL  요청: oauth_consumer_key,  oauth_callback, oauth_timestamp, oauth_nonce, oauth_signature_method, oauth_signature,                                                oauth_version, oauth_token, oauth_verifier
Protected Resources 요청: oauth_consumer_key,  oauth_callback, oauth_timestamp, oauth_nonce, oauth_signature_method, oauth_signature,                                                oauth_version, oauth_token


oauth_signature 생성하기
HAC-SHA1 해시알고리즘을 사용하여 시그너처를 생성하는 방법을 알아보자.  각 파라미터들은 percent 인코딩 또는 Base64 인코딩을 하기전에 반드시 UTF-8 로 인코딩해야함을 주의하자.

이 알고리즘은 키를 바탕으로 데이터에 대한 해시 값을 생성하게 되는데,  data는 Signature Base String 이 되며,  key 는 
Consumer Secret 와 Token Secret 를 & 문자로 연결한 값이 된다. 

HMAC-SHA1의 데이터 생성 ( Signature Base String 생성)
생성 절차는 좀 까다로운데 절차는 다음과 같다. 
① oauth_signature 파라미터를 제외한 oauth 파라미터들의 키와 값을 각각 percent 인코딩을 하고 =문자로 연결한다. 
     (HTTP request의  post body값이 존재하면 이 값도 동일한 연산을 수행한다)
② oauth 파라미터( 키=값) 쌍을 키의 알파벳순으로 정렬한 후  &문자로 연결하여 정규화된 문자열을 생성한다 
    (키가 동일하면, 값의 알파벳순으로 정렬함)
③  Request URL을 percent 인코딩한다. 
④ HTTP request method(대문자) 와 인코딩된  Request URL, 그리고 정규화된 문자열을 & 문자로 연결하고, percent  인코딩하여
    Signature Base String을 생성한다.   
 

HMAC-SHA1의 키 생성 
① consumer secret를 percent 인코딩한다 
② token_secret를 percent 인코딩한다.  (token_secret 은 Request Token 요청시에는 공백값이며, Access Token 요청시에는 
     Request token의 secret이며,  자원 요청시에는 Access Token의 secret 이 된다. )
③ 인코딩된 consumer secret 값과 token secret 값을  & 문자로 연결하여 최종 데이터를 생성한다. 

Base64 인코딩으로 바이너리 값을 아스키 값으로 변경 
위에서 생성한 키와 데이터를 바탕으로 HMAC-SHA1 알고리즘을 적용하면, signature 의 바이너리 값을 얻을 수 있다.
이 데이터에 Base64 인코딩을 적용하여, 아스키코드값으로 변경하고, 다시 percent 인코딩을 적용하여 최종 signature 값을 
생성한다. 

지금까지 생성한 oauth 파라미터드를 HTTP  request의 헤더에 설정하고, 요청만 하면된다. 하지만 아직 한단계가 더 남아있다. 
HTTP  request 의 Authorization 헤더값를 생성하는 것이다. 

Authorization  헤더값 생성
 "OAuth  " (공백이 있다는 것에 주의) 문자열과 모든 oauth 파라미터의 키와 쌍따옴표로 감싼 값쌍을 =문자료 연결하고 다시 각각을  ,(쉼표)문자로 연결한다 
최종헤더 값은 다음과 같은 형태일 것이다. 
OAuth oauth_callback="oob", oauth_timestamp="13882494", ......


이제는 거의 마지막 과정까지 왔다. 각각 생성한 oauth 파라미터의 키와 값, Authorization 헤더의 키와 값을 HTTP 헤더에 설정하고, 
http 요청을 하면 된다.   HTTP 헤더와 HMAC-SHA1의 키와 데이터를 표현한 그림은 다음과 같다. 

HMAC-SHA1 알고리즘 키와 데이터에 인코딩 적용여부는 그림에 표현하지 않았다.  









OAuth의 개념과 대략적인 흐름

네트워크로그 2010. 12. 31. 19:41




트위터 관련 앱을 개발하면서 정보수집을 하는중에 트위터 서비스에 있는 특정 데이터를 사용하기 위해서는 
OAuth 란 인증과정을 거쳐야 된다는 것을 알게되었다. 트위터 뿐만이 아니라,  스프링 노트와 페이스북 등 대부분의
웹서비스에서 OAuth를 사용하여 써드파티들이 데이터에 접근할 수 있도록 되어 있었다.  각 웹서비스에서는 OAuth 인증과정을
나름대로 설명해주고 있지만,  그 정보만을 가지고 구현하기는 생각보다 쉽지 않아보였다.  공개된 많은 소스와 OAuth 스펙을 참고
하여 나름대로 정리해 보았다. SNS 써드파티에 관심있는 사람들에게 조금이라도 도움이 되었으면 한다. 

OAuth
OAuth 는 Open Authorization 의 약자로 API를 사용하여 자원에 접근하기 위한 오픈 표준이다. 즉
SNS 와 같은 웹서비스에 있는 자원(데이터)을 사용자의 인증정보 없이도 써드파티에서 사용할 있도록 하기위한 오픈 표준이다.  사용자의 아이디와 비밀번호를 통한 명시적인 인증정보 대신 사용자가 개인자원에 접근을 허용했다는 표시인 토큰을 발행하여,  써드파티에서 접근할 수 있게하는 것이다. 

OAuth 프로토콜은 2007년 초반경에 처음 초안이 작성되어 2010 년 4월에 RFC 5849 : The OAuth 1.0 Protocol 로 등록이 되었다. 

OAuth를 통해 써드파티에서 궁극적으로 획득하는 것은 사용자의 자원에 대한 접근허용을 표시하는 액세스 토큰이다. 
이점을 염두하고 자주 언급되는 용어에 대해 알아보자.  트위터를 예로 들어 설명하겠다. 


OAuth 용어
Service  Provider : 서비스 프로바이더는 자원을 제공하는 웹서비스를 의미한다.  트위터 또는 페이스북 서비스를 의미한다. 
Consumer : 서비스 프로바이터의 자원에 접근하는 써드파티 웹사이트/애플리케이션을 의미한다. 
Protected Resource :  트윗글과 팔로워 목록과 같은 서비스 프로바이더가 보유한 데이터를 의미한다. 
Consumer Key/Secret :  서비스 프로바이더는 자신들의 서비스에 써드파티들이 접근할 수 있도록 하는데, 이때 각 써드파티들을 고유하게 식별하기 위한 값이 Consumer Key 이며,  그에 대한 확인을 위한 값이 Consumer Secret  이다. (아이디/비밀번호와 유사)
Request Token : 액세스 토콘과 교환하기 위한 사용자의 승인이 떨어지지 않은 토큰이다. 토큰은 key와 secret으로 구성된다. 
Access Token : 사용자에 의해 승인된 토큰으로,  이 토큰을 소유하면 해당사용자의 자원을 사용할 수 있게 된다. 
OAuth Parameter : OAuth 인증을 위해 필요한 파라미터 값으로, oauth_ 라는 접두어로 시작한다.  


OAuth 의 간략한 흐름 
크게 3가지 단계로 나눌 수 있다. 
① ServiceProvider 가 제공하는 Request token URL에 접속하여, Request Token을 요청하여 얻는다. 
    이 값은 Access Token과 교환하기 위한 값이며, 아직은 사용자의 승인이 되지 않은 토큰 값이다. 
② ServiceProvider 가 제공하는 Authorize URL에 접속하면서  Request Token 값을 전달하면, 사용자의 인증페이지를
    볼수 있게된다. 사용자가 (로그인을 하고)  접근허용을 하면, Verifier 값을 얻을 수 있게된다. 
   (이 때,  Consumer 가 웹서비스 또는 애플리케이션인지에 따라 Verifier 값을 다르게 수신하게 된다. 
    웹서비스라면, ServiceProvider 에게  제공한 callback 주소로 리다이렉트 되면서 Verifier가 전달되며, 
    애플리케이션이면,  리다이렉션 없이 pincode를 발급해준다. 이 pincode를 추출하여 Verifier 로 사용한다. )
③ ServiceProvider 가 제공하는 Access token URL로 접속하여, Request Token 과 Verifier를 함께 전달하면 
    Access Token을 얻을 수 있게된다. 
④ 획득한 Access Token을 사용자의 계정에 따라 저장하여, 이후에 자원에 접근하기 위한 값으로 사용할 수 있다. 

OAuth 인증과정은 4가지 과정으로 단순하다. 하지만 인증과정을 어렵게 보이게 하는 것은 ②번 과정을 제외한 나머지 
과정에서 URL 요청시 적절한 oauth 파라미터 값들을 생성하여, 보내주어야 하기 때문이다.  이 하나하나 파라미터 값들을
올바로 생성하기 위해서는 ServiceProvider 와 스펙에서 요구하는 사항을 준수하여야 한다. 지금은 세부 사항들은 무시하고, 
크게 4가지 단계를 거치게 된다는 것만 이해하자. 위 흐름을 간략히 그림으로 나타내 보았다. 



사용자의 계정별로 ①,②,③ 번 과정은 한번만 수행하면 되며,  이후 ④ 을 조회하기 위해 사전에 획득한 Access Token을 
사용하게 된다.  (물론 ServiceProvider에 따라 Access Token을 특정기간 동안에만 유효하게 할 수 있는데, 그럴 경우 일정주기마다 
①,②,③ 번 과정을 다시 거쳐야 한다. )



지금까지 OAuth 의 개념과 대략적인 흐름을 알아보았다. 다음 포스팅에서는 각 단계별로 필요한 oauth 파라미터들과 파라미터를
생성하는 방법, 그리고 실제 구현까지 해보겠다.