1. PUT/DELETE 기능 구현한 웹서버 동작

1) PUT기능 구현

  • put_paste 함수는 PUT 메서드를 처리한다. PUT /paste/번호 요청이 들어오면 실행된다. 여기서 paste_id는 URL에서 받은 번호, paste는 요청 본문(JSON)을 Data model 클래스 Paste 모델로 검증 후 받는 것이다.
  • 만약 찾으려는 번호가 db 내에 있는 경우(db가 list라 인덱스로 찾음), 해당 번호에 저장된 객체가 있다는 뜻이다. 기존 데이터를 새로 들어온 요청 내용으로 대체한 후 return을 통해 성공했음을 알리고, 현재 저장되어있는 paste 데이터를 JSON으로 돌려준다. 이때 내용을 통째로 대체하기 때문에 멱등성을 만족한다.
  • 과제 의도는 ‘해당 번호가 없을 시’ 생성을 하지 않고 넘어가는 것인데, else문에서 pass로 처리하려다가 코드드의 통일성을 위해 return 하는 방식으로 작성 했다. 찾아보니 이렇게 처리를 해주지 않더라도(pass만 사용) 자동으로 200 OK와 null을 받는다고 한다. FastAPI는 Python 객체를 문자열로 바꿔서 클라이언트에게 전송할 때 None을 JSON의 null로 변환해주니, 이렇게 작성해도 ‘결과적으로는’ 현재 구현과의 차이가 크게는 없었을 것이다.

2) DELETE 기능 구현

  • delete_paste 함수는 DELETE 메서드를 처리한다. DELETE /paste/번호 요청이 들어오면 실행된다. 여기서 paste_id는 URL에서 받은 번호이고, response는 상태 코드를 조정하기 위한 FastAPI 객체이다.
  • 만약 삭제하려는 번호가 db 내에 있는 경우(db가 list라 인덱스로 찾음), 해당 번호 위치의 데이터를 물리적으로 삭제하지 않고 단순히 실제 db 리스트에서 해당 요소를 None으로 바꾸는 처리를 한다. 이후 status code를 200으로 바꾼다(이 코드 넣지 않아도 200이 기본 반환됨). 마지막으로 JSON 형태로 { “paste_id”: 번호, “paste”: None }을 반환한다.
  • 만약 삭제하려는 번호가 db 범위에 존재하지 않는 경우, 즉 인덱스가 유효하지 않은 경우에는 response.status_code를 404로 명시하여 클라이언트에게 존재하지 않음을 알린다. 이때도 return 값은 동일한 형태({ “paste_id”: 번호, “paste”: null })이지만 상태 코드가 다르므로 클라이언트는 이를 통해 요청이 실패했음을 구분할 수 있다.
  • DELETE 요청은 멱등성을 가지는데, 동일한 요청을 여러 번 보내도 최종 결과는 동일하게 해당 리소스가 없는 상태로 유지된다. 따라서 한 번 삭제된 리소스에 대해 DELETE를 반복 호출하더라도 서버의 상태는 변하지 않고, 클라이언트는 항상 삭제된 상태(null)를 확인할 수 있다.

3) 기타 서버 전반 구현

구현 코드

PUT, DELETE 메서드 제외 구현 내용

  • 앞서 구현한 PUT, DELETE 앞에 오는 코드로, GET, POST와 Paste모델이 구현되어있다. (강의자료 실습코드)

request.sh 구현 내용

  • curl 로 서버에 차례차례 요청 보내서 API 동작을 확인하는 스크립트이다. 줄 끝의 echo; 는 요청 사이에 줄바꿈을 넣기 위한 것이다.

웹 캡쳐

main.py 내부에 정의된 API 엔드포인트

  • GET / → Root
  • GET /paste/{paste_id} → 특정 paste 조회
  • PUT /paste/{paste_id} → 특정 paste 수정
  • DELETE /paste/{paste_id} → 특정 paste 삭제
  • POST /paste/ → paste 생성

⇒ 기능들이 잘 들어갔음을 확인할 수 있다.

엔드포인트 호출로 기능 테스트(request.sh)

request.sh 실행 및 서버 로그

  • 실습 코드에 구현한 PUT, DELETE 메서드를 채워넣었다. 또한 강의자료의 request.sh 내용을 통해 쉽게 테스트했다.

[ 로그 해석 ]

  • 추가로, 클라이언트 포트는 연결할 때마다 OS가 랜덤으로 정해주는 것을 사용하므로 서버 로그에 보이는 클라이언트의 IP:Port에서 계속 포트번호가 바뀌는 것을 발견할 수 있었다.

2. Web서버에 TLS 적용

1) 사설 인증서 생성

  • Dockerfile을 통해 Openssl을 설치했다.
# 개인키 생성
openssl genrsa -out server.key 2048

# 인증서 서명 요청 파일 생성
openssl req -new -key server.key -out server.csr

# self-signed 인증서 발급
openssl x509 -req -in server.csr -signkey server.key -out server.crt

(1) 내 로컬 디렉토리를 docker 디렉토리와 mount

  • certs 폴더를 생성하고 내 certs 디렉토리를 /work/certs에 마운트하면 docker에서 만들어진 파일이 docker cp 없이 바로 내 디렉토리에 생성된다.

(2) server.key 생성

openssl genrsa -out server.key 2048

  • 서버만 가지는 비밀 키를 만든다. RSA(Rivest–Shamir–Adleman) 알고리즘으로 만든 2048비트 키이다.

[ 키를 왜 만드는가?: 통신 암호화 ]

(3) 인증서 서명 요청(CSR) 파일 만들기

openssl req -new -key server.key -out server.csr

  • Certificate Signing Request(CSR)
  • 인증에 들어갈 신원 정보를 받는 것인데, 실습 용도로 하는 것이므로 각 정보를 아무렇게나 기입했다.

(4) self-signed cert 발급

openssl x509 -req -in server.csr -signkey server.key -out server.crt

  • (3)에서 만든 CSR을 server.key로 직접 서명해서 server.crt를 만든다. 결과적으로 도커 컨테이너의 /work/certs(호스트의 경우 /certs)에 server.key, server.csr, server.crt가 생긴다. (ls certs로 확인)

2) HTTPS 가동 및 호스트에서 TLS 검증

  • 지금까지 server.key 등을 만든 것은 HTTPS 서버를 띄우기 위함이다.

HTTPS 가동

  • Docker 컨테이너 실행

2) HTTPS 가동 및 호스트에서 TLS 검증

docker container의 curl -v -k를 통한 TLS검증

  • v: TLS 핸드셰이크 로그를 확인하기 위한 옵션
  • k: 테스트용이므로 self-signed 인증서 검증 생략

host터미널

  • 호스트에서 curl을 통해 TLS를 검증한다. 도커 컨테이너 실행 시 -p 8443:8443 옵션을 줬으므로, 호스트에서 https://localhost:8443로 요청하면, Docker가 자동으로 해당 요청을 컨테이너 안 uvicorn 서버(0.0.0.0:8443) 로 전달한다.
  • 또한 아까 컨테이너 내부에서 HTTPS서버를 실행했으므로, curl -vk로 handshake 후 암호화된 통신을 맺는다. 로그에 출력된 TLS 관련 로그를 통해 확인할 수 있다
  • 또한 Wireshark로 진행한 패킷 캡쳐에서도 TLS 관련 내용을 확인할 수 있었다.

만든 웹 접속

웹 접속 결과

  • not secure는 self-signed cert를 썼기 때문에 뜨는 것이다. 로컬 테스트 및 실습용이라 상관이 크게 없을 것 같다.

Leave a comment