HTTP/1.1의 변경사항은 아래와 같다.

  • 통신 고속화
    • Keep-Alive가 기본적으로 유효
    • 파이프라이닝
  • TLS에 의한 암호화 통신 지원
  • 새 메서드 추가
    • PUT, DELETE가 필수 메서드
    • OPTION, TRACE, CONNECT 메서드 추가
  • 프로토콜 업그레이드
  • 이름을 사용한 가상 호스트 지원
  • 크기를 사전에 알 수 없는 콘텐츠의 청크 전송 인코딩 지원

 

1. 통신 고속화

이전 캐시에서 설명한 ETag와 Cache-Control은 HTTP/1.1의 기능이다. 캐시는 콘텐츠 리소스마다 통신을 최적화하는 기술이지만, Keep-Alive와 파이프 라이닝은 좀 더 범용적으로 모든 HTTP 통신을 고속화하는 기능이다.

 

브라우저에서 서버로 동시에 접속할 때 HTTP/1.0에서 권장하는 값은 4였다. 병렬로 동시에 접속하는 이 값이 HTTP/1.1에서는 2로 내려갔는데, Keep-Alive나 파이프라이닝의 효과를 고려한 결과라고 한다. 또한, 프로토콜 버전 업으로 속도가 개선되고 서버의 부하도 내려간다.

 

 

1.1 Keep-Alive

HTTP의 아래층인 TCP/IP 통신을 효율화하는 구조로, 요청마다 통신을 닫지않고 연속된 요청에는 연결된 connection을 재사용함
Why>
HTTP는 TCP를 사용하기 때문에 3-way-handshake를 통해 연결을 성립한다. 이후 request와 적절한 response를 받으며 통신을 하고 4-way-handshake를 통해 연결을 닫는다. 하지만, 1번의 request를 보내는 것이 아닌 연속적인 request를 보내야 할 경우 요청마다 3-way, 4-way handshake를 해야한다. 빛의 속도가 아무리 빠르다고 하지만, 지구 반대편까지 통신을 보내는 데 0.2초가 걸린다고 한다. 연속적인 request를 보낼 경우 이전에 사용한 3-way-handshake를 통해 연결된 connection을 재사용한다면 효율적인 통신이 오고 갈 수 있다. 이때 사용하는 것이 Keep-Alive이다.

Keep-Alive

이 기능은 HTTP/1.0에서 지원하지 않았지만, 몇몇 브라우저에서 이미 지원하고 있었다. HTTP/1.0에서 요청 헤더에 아래 헤더를 추가함으로써 Keep-Alive를 이용할 수 있었다.

Connection: Keep-Alive

 

이 헤더를 받아들인 서버가 Keep-Alive를 지원하면, 같은 헤더를 응답 헤더에 추가해서 반환한다.

HTTP/1.1에서는 이 동작이 기본으로 되어 있다. TLS 통신을 이용할 경우, 특히 통신 시간을 많이 줄여준다. (인증하는 시간이 추가되지 않아서 그런 것 같다.) HTTP 아래 계층의 프로토콜인 TCP/IP도 접속할 때는 1.5회 왕복의 통신을 필요로 한다. 패킷이 1회 왕복하는 시간을 1RTT(Round-Trip Time)로 부르며, TLS에서는 서버/클라이언트가 통신을 시작하기 전에 정보를 교환하는 핸드세이크과정에서 2RTT만큼 시간이 걸린다. 

 

Keep-Alive를 이용한 통신은 클라이언트나 서버 중 한 쪽이 아래 헤더를 부여해 접속을 끊거나 타임아웃될 때까지 연결이 유지된다.

Connection: Close

 

Keep-Alive 지속 시간은 클라이언트와 서버 모두 가지고 있다. 한 쪽이 TCP/IP 연결을 끊는 순간에 통신은 완료되므로, 어느 쪽이든 짧은 쪽이 사용된다. Chrome 300초, firefox 115초, edge 120초, safari 60초로 기본 타임아웃 시간을 가지고 있다. 

 

통신이 지속되는 동안 os의 자원을 계속 소비하므로, 실제로 통신이 전혀 이루어지지 않는데 접속을 유지하는 것은 바람직하지 않다. 짧은 시간에 접속을 끊는 것에 의미가 있다.

 

 

1.2. 파이프라이닝

최초의 요청이 완료되기 전에 다음 요청을 보내는 기술. 다음 요청까지의 대기 시간을 없앰으로써, 네트워크 가동률을 높이고 성능을 향상 시킨다. Keep-Alive 이용을 전제로, 서버는 요청이 들어온 순서대로 응답을 반환

파이프라이닝

그대로 동작한다면, 왕복 시간이 걸리는 모바일 통신에서 큰 효과를 기대할 수 있다. 하지만, 실제로는 파이프라이닝 기능을 구현하지 않았거나 구현했어도 기본 설정에서 꺼둔 브라우저도 있다. 크롬 또한 버전 18에서 지원했지만, 버전 26에선 파이프라이닝을 삭제했다. 현재 기본으로 활성화한 브라우저는 오페라와 iOS 5 이후의 사파리 정도이다.

호환성, 보안, 성능 이슈 등 파이프라이닝의 이점이 크지 않았다는 것이 이유인 것 같다. 최근에는 HTTP/2와 같은 새로운 프로토콜이 등장하며 파이프라이닝의 필요성이 더 낮아졌다고 한다. 
NOTE_스트림
그렇다고 파이프라이닝이 쓸모없는 사양이었다는 것은 아니다. 파이프라이닝은 여러 문제를 해결하고, HTTP/2에서 스트림이라는 새로운 구조로 다시 나왔다.
- HTTP/2 에서는 HTTPS 통신이 전제가 된다. HTTPS 이므로 기본적으로 프록시가 송수신되는 데이터 내부를 볼 수 없다. 프록시는 통신을 중계만 하게 됐으므로 최신 프로토콜을 해석할 수 없는 프록시가 도중에 방해할 일이 사라졌다.
- 통신 순서를 유지해야 한다는 제약이 HTTP/2에서는 사라졌다. (응답 순서가 파이프라이닝의 성능 이슈를 만들었었음.) HTTP/2에서는 하나의 세션 안에 복수의 스트림이 공존한다. 시분할로 각 스트림의 통신이 이루어지므로, 서버 측의 준비가 된 순서로 응답을 반환할 수 있다. 또한, 우선순위를 부여해 순서를 바꾸는 것도 가능하다.

 

 

2. 전송 계층 보안(TLS)

컴퓨터 네트워크에서 보안 통신을 제공하는 프로토콜로 기밀성과 데이터 무결성을 보장하여 민감한 정보의 안전한 전송을 도와준다.

 

HTTP/1.1과 함께 이용되는 것을 강하게 의식해서 만들어졌지만, TLS 암호화 자체는 HTTP 뿐만 아니라 다양한 형식의 데이터를 양방향으로 흘려보낼 수 있다. TLS는 기존 프로토콜에 통신 경로의 안정성을 추가해 새로운 프로토콜을 만들어낼 수 있는 범용적인 구조로 되어 있다. HTTP는 80번 port를 사용하고, HTTPS는 443번을 사용해 다른 서비스로 취급된다. HTTPS 이외에 메일 전송 프로토콜 SMTP(25번)의 TLS 버전인 SMTPS(465번) 등 기존 프로토콜의 버전업에도 이용된다.

 

HTTP/1.0과 1.1에서는 프록시 서버 등이 통신을 해석해 캐시함으로써 고속화 기능을 제공할 수 있었지만, 자신이 해석할 수 없는 프로토콜을 멈춰버리는 경우가 있었다. TLS를 사용하면 조작할 수 없는 안정된 통신로가 생기므로, HTML5에서 새로 도입된 웹소켓 같은 통신 프로토콜이나 HTTP/2 등 이전과 상위 호환성이 없는 수많은 새로운 시스템을 원만하게 도입하는 인프라가 됐다.

 

TLS는 몇 가지 버전이 있다. SSL 이라고 불리던 시절도 있었으며, 현재에도 TLS 부분을 많은 라이브러리로서 높은 점유율을 자랑하는 소프트웨어 이름이 OpenSSL이라는 등 TLS와 관련된 이름에 SSL이라는 글자가 들어간 경우가 있다. 다만 실제 SSL은 여러가지 취약성이 알려져 있어, RFC에서도 권장하지 않는다. 인터넷 서비스에서도 무효화된 것이 많고, 실제로 사용되는 것은 대부분 TLS 이다.

SSL/TLS 역사

 

실제 통신 내용은 암호화된 이진 데이터이므로 내부를 보는 것은 어렵지만, curl 커맨드로 액세스할 때는 URL을 https://로만 하면 된다. 아래 소개한 옵션으로 세밀하게 동작을 설정할 수 있다. 서버 설정이 맞는지, 각 버전에서 예상한 대로 동작하는지 확인하는 데 편리하다.

  • -1, --tlsv1
    • TLS로 접속
  • --tlsv1.0, --tlsv1.1, --tlsv1.2
    • TLS 니고에이션 시 지정한 버전으로 연결하도록 강제
  • --cert-status
    • 인증서를 확인
  • -k, --insecure
    • 자가 서명 인증서라도 오류가 되지 않는다.

--cert-status와 -v를 붙여 실행하면, 인증서 상태를 아래처럼 표시한다.

$ curl --cert-status -v https://example.com 
(생략)
* SSL certificate status: good (0)



 

2.1 해시 함수

입력 데이터를 규칙에 따라 집약해감으로써 해시 값으로 불리는 짧은 데이터를 만들어 내는 함수

해시는 '잘게 저민 조각'이라는 뜻으로, 해시드 포테이토나 해시드 비프의 해시와 같은 말이다. 

 

해시 함수에는 암호화 통신을 사용하는 데 편리한 수학적 특성이 있다. 해시 함수를 h(). 입력 데이터를 A,B...., 산출된 해시 값을 X, Y ...라고 하자. 길이는 len() 이라고 하자.

  • 같은 알고리즘과 같은 입력 데이터라면, 결과로서 생성되는 값은 같다. h(A)=X가 항상 성립한다.
  • 해시 값은 알고리즘이 같으면 길이가 고정된다. SHA-256 알고리즘에선 256비트(32바이트)다. 따라서 입력 데이터가 너무 작을 경우 해시 값이 더 커지지만, 기본적으로는 len(X)<len(A)가 된다.
  • 해시 값에서 원래 데이터를 유추하기 어렵다. h(A)=X의 X에서 A를 찾기 힘들다.
  • 같은 해시 값을 생성하는 다른 두 개의 데이터를 찾기 어렵다. h(A)=h(B)가 되는 임의의 데이터 A,B를 찾기가 힘들다.

해시 함수는 다양한 용도로 쓰인다. 예를 들어, 다운로드한 파일이 깨지지 않았는지 확인하는 방법으로 이용된다. 1바이트라도 데이터에 차이가 있으면 해시 값이 바뀌기 때문이다. 이런 용도로 사용될 때 해시 값을 checkSum 혹은 핑거프린트라고 불리기도 한다. 또한, Git에서는 파일을 관리할 때 파일명이 아니라 파일 내용을 바탕으로 한 해시 값을 사용하고, 이 해시 값을 키로 해서 데이터베이스에 파일을 저장한다. 같은 내용의 파일이 여러 개 있을 때, 데이터의 실체는 하나다. 커다란 데이터 파일을 모두 비교하지 않고 해시 값만 비교함으로써 재빨리 같은 파일인지 판정할 수 있다. 해시 값 충돌은 매우 드물게 일어나지만, 데이터양이 적으면 거의 일어날 수 없다.

 

유명한 해시 함수로는 MD5(128bit), SHA-1(160bit), SHA-2(SHA-224, 256, 384, 512, 512/224, 512/256) 등 이있다. SHA-1은 서버 인증서 서명에 사용하는 것을 추천하지 않는 등 다양한 이슈가 있다. 

 

해시 값이 어떤지 시험하는 데는 커맨드라인 툴을 사용하면 좋다. 보안 용도로 부적당한 MD5도 체크섬 용도로는 여전히 이요되고 있으며, 각 OS에 해당되는 유틸리티가 준비되어 있다. mac OS나 BSD 계열 OS에서는 md5 커맨드가 있고, 리눅스에서는 md5sum 커맨드가 있다. 파일을 1바이트만 수정해도 해시 값이 크게 달라지는 것을 알 수 있다.

md5 test

 

 

2.2 공통 키 암호와 공개 키 암호 그리고 디지털 서명

Why>
암호화는 비밀스러운 방법으로 문장을 해독할 수 없는 형식으로 바꿔(인코딩) 보내고, 받아보는 쪽에서 원래 문장으로 복원(디코딩)한다. 하지만, 인코딩/디코딩하는 변환 알고리즘을 알게된다면 모든 통신이 그대로 노출된다. 보다 안전한 통신 방법으로 공동키, 공개키가 만들어졌다.

암호화에서 중요한 것은 변환 알고리즘을 비밀로 하는 것이 아닌, 알고리즘이 알려져도 안전하게 통신할 수 있는 것이다. 현재 일반적으로 사용하는 방식은 암호화 알고리즘은 공개하고, 그 암호화에 사용하는 데이터(키)를 따로 준비하는 방식이다. TLS에서 사용되는 방식으로는 공통 키와 공개 키 방식 두 종류가 있다.

 

공통 키 방식은 자물쇠를 잠글 때와 열 때 모두 같은 열쇠를 사용하는 방식이다. 따라서 통신하는 사람끼리는 이 번호를 공유할 필요가 있다.  공통 키 방식은 대칭 암호라고도 불린다. TLS에서는 일반 통신의 암호화에 사용한다.

 

공개 키 방식은 비대칭 암호라고도 불린다. 공개 키 방식에서 필요한 것은 공개 키와 비밀 키이다. 공개 키는 이름 그대로 전 세계에 공개해도 문제 없지만, 비밀 키는 다른 사람에게 알려져선 안 된다. 공개 키 방식에선 집 열쇠와 달리 암호화 키와 암호해독 키가 따로 있다. 암호화하는 것이 공개 키고 해독하는 것은 비밀키다. 사물에 비유하면 공개 키는 자물쇠, 비밀 키는 열쇠이다. 

 

디지털 서명은 공개 키 방식을 응용한 예이다. 거꾸로 열쇠를 나누어주고 자물쇠를 비밀로 해두는 것과 같은 이미지이다. 편지 본문에 자물쇠로 잠근 데이터도 함께 첨부해 보낸다. 받는 사람이 공개된 키를 사용해 자물쇠를 열었을 때 본문과 동일한 것이 나오면 메시지가 조작되지 않은 것을 알 수 있다. 실제 디지털 서명은 본문 자체를 암호화하는 것이 아닌 해시화하고 그 결과를 암호화한다. 

 

암호화는 완벽하지 않다. 자물쇠에 비유하면 암호의 안정성은 알고리즘과 비트 수로 강도가 정해진다. 시간을 들여 열쇠의 울퉁불퉁한 패턴을 반복해서 테스트하면 진짜 열쇠를 만들어낼 수도 있다. 실제로는 상당한 계산량이 필요하지만, 현실적인 시간 내에서 해석이 가능한 경우 보안에 취약하다고 간주된다. 이미 몇몇 알고리즘은 CPU의 발달과 함께 권장하지 않게 되었다.

 

 

2.3 키 교환

키 교환은 클라이언트와 서버 사이에 키를 교환하는 것이다. 간단한 방법으로는 클라이언트에서 공통 키를 생성한 다음 전술한 서버 인증서의 공개 키로 암호화해 보내는 방법이 있고, 키 교환 전용 알고리즘도 있다. 책에서는 RFC 2631에 정의된 디피-헬먼(DH) 키 교환 알고리즘을 소개한다. 실제로는 해당 알고리즘에서 파생된 일시 디피-헬먼(DHE)를 사용한다.

 

알고리즘의 핵심은 키 자체를 교환하는게 아니라, 클라이언트와 서버에서 각각 키 재료를 만들어 서로 교환하고 각자 계산해서 같은 키를 얻는 것이다.

키 교환 모식도

TLS 상에서 서버가 계산에 사용할 값 p, g를 준비한다. 이 값들은 공개 정보로서 그대로 클라이언트어 넘겨줍니다. g는 법 p에 대한 원시근이라고 한다. 

원시근?
g^1, g^2 ..., g^(p-2)의 어느 수치도 q로 나눈 나머지가 1이 아니다

속도를 높이고자 p와 g의 조합을 미리 계산해서 TLS 라이브러리 내부에 목록으로 만들어두는 경우가 많다고 함.

 

또, 한 가지 값을 계산한다. 이 값을 Y라고 부르는데, 비밀이라고한다.(서버 내부에만 저장되는 값인 듯 하다.) 클라이언트에 보내는 Ys 값은 아래와 같이 계산한다.

// g=5, p=23, Y=6
Ys = (g^Y) % p = (5^6)%23 = 23

 

Server Key Exchange(서버 키 교환) 메시지의 인수로서 p, g, Ys를 클라이언트로 보낸다. 전송이 끝난 후 상태는 아래와 같다.

서버 키 교환 메시지 전송 후 상태 변화

클라이언트도 랜덤하게 값 X를 생성하고 Xs를 계산한다. X를 15라고 가정하면 아래와 같다.

Xs = (g^X)%p = (5^15)%23 = 19

Client key Exchange(클라이언트 키 교환) 메시지의 인수로서 Xs를 서버로 보낸다. 전송이 끝난 후 상태는 아래와 같다.

클라이언트 키 교환 메시지 전송 후 상태 변화

클라이언트가 직접 생성한 값 X와 서버가 보내준 Ys로 공통 키의 시드를 생성한다.

ZZ = (Ys^X)%p = (8^15)%23 = 2

서버도 직접 생성한 값 Y와 클라이언트가 보내준 값 Xs로 공통 키의 시드를 생성한다.

ZZ = (Xs^Y)%p = (19^6)%23 = 2

 

ZZ는 p로 나머지를 구하기 때문에 p를 넘어가지 않는다. 현재는 예시로 5비트 안에 들어가지만, 실제로는 1024비트, 2048비트와 같은 큰 길이를 사용한다. 이유는 생성되는 키가 작으면 취약성으로 이어지기 때문이다. 실제로 이 성질을 이용해, 생성되는 키의 비트 수를 작게 해서 보안 강도를 약화하는 로그 잼이라는 공격이 있다고 한다. 현재는 2048 bit를 권장한다고 함. 

 

 

2.4 공통 키 방식과 공개 키 방식을 구분해서 사용하는 이유

공개 키 방식이 복잡한만큼 안정성이 공통 키 방식에 비해 안정성이 높다. 안정성이 높은 방식을 계속 사용하면 안정적이지만 TLS는 양쪽 방식으 조합했다. TLS에서는 통신마다 한 번만 사용되는 공통 키를 만들어내고, 공개 키 방식을 사용해 통신 상대에게 신중히 키를 전달한 이후는 공통 키를 통해 고속으로 암호화하는 2단계 방식을 이용한다. 이유는 공개 키 방식이 안정성이 높지만, 키를 가지고 있어도 암호화와 복호화에 필요한 계산량이 공통 키 방식보다 너무 많기 때문이다.

 

"계산량이 많지만, 안전하게 좋다" 라는 사람도 있을 것이다. 한 번 두개의 키 방식의 성능을 측정해보자.

package main

import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/md5"
	"crypto/rand"
	"crypto/rsa"
	"io"
	"testing"
)

func prepareRSA() (sourceData, label []byte, privateKey *rsa.PrivateKey) {
	sourceData = make([]byte, 128)
	label = []byte("")
	io.ReadFull(rand.Reader, sourceData)
	privateKey, _ = rsa.GenerateKey(rand.Reader, 2048)
	return
}

func BenchmarkRSAEncryption(b *testing.B) {
	sourceData, label, privateKey := prepareRSA()
	publicKey := &privateKey.PublicKey
	md5hash := md5.New()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		rsa.EncryptOAEP(md5hash, rand.Reader, publicKey, sourceData, label)
	}
}

func BenchmarkRSADecrytion(b *testing.B) {
	sourceData, label, privateKey := prepareRSA()
	publicKey := &privateKey.PublicKey
	md5hash := md5.New()
	encrypted, _ := rsa.EncryptOAEP(md5hash, rand.Reader, publicKey, sourceData, label)
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		rsa.DecryptOAEP(md5hash, rand.Reader, privateKey, encrypted, label)
	}
}

func prepareAES() (sourceData, nonce []byte, gcm cipher.AEAD) {
	sourceData = make([]byte, 128)
	io.ReadFull(rand.Reader, sourceData)
	key := make([]byte, 32)
	io.ReadFull(rand.Reader, key)
	nonce = make([]byte, 12)
	io.ReadFull(rand.Reader, nonce)
	block, _ := aes.NewCipher(key)
	gcm, _ = cipher.NewGCM(block)
	return
}

func BenchmarkAESEncryption(b *testing.B) {
	sourceData, nonce, gcm := prepareAES()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		gcm.Seal(nil, nonce, sourceData, nil)
	}
}

func BenchmarkASEDecryption(b *testing.B) {
	sourceData, nonce, gcm := prepareAES()
	encrypted := gcm.Seal(nil, nonce, sourceData, nil)
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		gcm.Open(nil, nonce, encrypted, nil)
	}
}

 

공통 키는 AES, 공개 키는 RSA를 사용했고, 테스트를 하려면 cmd 창에서 아래 명령어를 입력하면 된다.

$ go test -bench .

결과는 아래와 같다.

시간 결과

128바이트 데이터를 암호화하고 복호화할 때 걸리는 처리 시간은 위의 결과 그댈외다. 암호화와 복호화가 다른 컴퓨터에서 진행되는 점을 감안하면 실제 산출량은 더 느린 수치가 될 수 있다. 공개 키 암호에서는 비교적 성능이 좋은 컴퓨터를 사용해도 PHS 회선 정도의 속도밖에 안나온다고 한다. AES로는 기가비트 광회선에서도 병목이 일어나지 않는 속도로 그 차이는 1만 5천배이다. AES는 Go 언어의 64비트 인텔 아키텍처용 구현으로 하드웨어 처리가 이루어져, 3~10배의 속도가 되는 것을 빼도 상당한 속도 차이가 있다.

 

 

2.5 TLS 통신 절차

TLS 통신은 크게 3개로 나눌 수 있다.

  1. handshake를 통해 통신 확립
  2. 레코드 프로토콜로 불리는 통신 단계
  3. SessionTicket 구조를 이용한 재접속 시의 고속 handshake

TLS 통신 순서

 

서버의 신뢰성 확인

서버의 신뢰성을 보증하는 구조는 공개 키를 보증하는 구조이기도 해서, 공개 키 기반구조(PKI)라고 불립니다. 브라우저는 서버에서 그 서버의 SSL 서버 인증서를 가져오는 것부터 시작합니다. 

 

인증서는 X.509 형식으로 기술된 파일입니다. 이 인증서에는 사이트 주체, 발행자, 소유자 서버의 공개 키, 유효 기한 등의 항목이 있습니다. 발행자는 인증기관(CA)이라고도 불립니다.

 

외부에 공개된 서비스라면 인증서는 누구나 취득할 수 있습니다. openssl 커맨드로 구글의 인증서를 가져와 내용을 표시해봅시다.

$ openssl x509 -in google.crt -noout -text  

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            90:76:89:18:e9:33:93:a0
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: OU=No SNI provided; please fix your client., CN=invalid2.invalid
        Validity
            Not Before: Jan  1 00:00:00 2015 GMT
            Not After : Jan  1 00:00:00 2030 GMT
        Subject: OU=No SNI provided; please fix your client., CN=invalid2.invalid
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (2048 bit)
                Modulus:
                    00:cd:62:4f:e5:c3:13:84:98:0c:05:e4:ef:44:a2:
                    a5:ec:de:99:71:90:1b:28:35:40:b4:d0:4d:9d:18:
...

위는 실제 서비스의 인증서입니다.

 

 

키 교환과 통신 시작

공개 키 암호를 사용하는 방법과 키 교환 전용 알고리즘을 사용하는 방법이 있다. 어느 쪽을 쓸 것인지는 최초의 Client Hello, Server Hello 니고시에이션에서 결정된다. 

 

클라이언트는 먼저 난수를 사용해 통신용 공통 키를 만든다. 난수도 패턴이 쉽게 읽히는 알고리즘으로는 아무리 암호해봐야 애초에 생성될 공통 키가 예측되거나 암호를 결정하는 알고리즘의 중간 경과가 추측될 우려가 있다. 

 

공개 키를 사용하는 방법은 간단하다. 서버 인증서에 첨부된 공개 키로 통신용 공통 키를 암호화해 그 키를 서버에 보낸다. 서버는 인증서의 공개 키에 대응하는 비밀 키를 갖고 있으므로 건네받은 데이터를 복호화해 공통 키를 꺼낼 수 있다.

 

순방향 비밀성이 우수하므로, 키 교환에서 주류가 되는 것은 키 교환 전용 알고리즘 방식이다. 

 

 

통신

통신을 할 때도 기밀성과 무결성을 위해 암호화를 한다. 암호화에는 클라이언트와 서버 모두 가지고 있는 공통 키 암호 방식 알고리즘 이용한다.

 

TLS 1.2 이전 버전에서는 통신 내용의 해시 값을 계산한 다음, 공통 키 암호로 암호화 하는 방법을 지원했다. 하지만, 이 기법에 대한 공격이 발견됐으므로 TLS 1.3 이후에는 AES+GCM, AES+CCM, ChaCha20-Poly1305 등의 인증 암호(AEAD)로 제한될 예정이다.

 

 

통신의 고속화

지금까지 설명한 절차는 가장 긴 경우인 신규 접속의 흐름이다. 일반적인 접속에서는 우선 HTTP로 연결하기 전 TCP/IP 단계에서 1.5RTT가 걸린다. 그 후 TLS 핸드세이크에서 2RTT, 그리고 HTTP의 요청에서 1RTT의 통신 시간이 걸린다. 단 TCP/IP 통신 마지막의 0.5RTT와 그 후 TLS의 최초 통신은 함께 이루어지므로 합계는 4RTT이다. TLS를 사용하지 않는다면 2RTT로 종료된다. 

 

4RTT라는 단위는 통신에서 전기 신호가 서버에 도달하고 응답이 되돌아오기까지 매우 긴 시간이다. 그래서 인터넷을 더 빠르게 하려면 왕복 시간을 줄이는 것이 중요하다. TLS와 HTTP에는 이를 위한 장치가 몇 가지 구현되어 있다.

 

우선 이 장 처음에 소개한 Keep-Alive입니다. Keep-Alive를 이용하면 세션이 지속되므로, 최초 요청 이후의 통신에서는 RTT가 1이 된다.

 

TLS 1.2에는 세션 재개 기능이 있어 최초의 핸드셰이크에서 전에 사용하던 세션ID(32bit)를 보내면 이후의 키 교환이 생략되므로 1RTT로 세션이 재개된다. 1.3에서는 사전에 키를 공유해 둠으로써 0RTT로 최초 요청부터 정보를 전송할 수 있게 된다.

 

TLS 1.3에서는 키 교환과 비밀 키 암호가 분리되어 암호화 스위트로 비밀 키 암호를 니고시에이션한 결과를 기다리지 않고, 최초의 Client Hello로 클라이언트 쪽에서 키를 교환할 수 있게 됩니다. 통신이 1왕복 줄어 1RTT로 인증이 완료된다.

 

TLS 아래 계층을 핸드셰이크가 필요한 세션형 TCP에서 재전송도 및 흐름 제어도 하지 않는 간이 데이터그램형 UDP로 대체해, 애플리케이션 계층에서 재전송하는 QUIC(Quic UDP Internet Connections)라는 통신 방식의 RFC화가 IETF에 제안됐다. 이미 구글의 서버가 QUIC을 지원하고, 크롬 브라우저에서도 이용된다. HTTP나 TLS 통신 이전에 전송 계층인 TCP 시점에서 핸드셰이크에 1RTT를 소비했지만, UDP는 핸드셰이크를 하지 않으므로 0RTT로 연결 할 수 있다. 현재 구현된 QUIC은 TLS에 해당하는 것을 자신이 갖는 등 거대해졌지만, 앞으로는 TLS 1.3으로 대체된다.

 

 

2.8 프로토콜 선택

TLS가 제공하는 기능 중 차세대 통신에 없어선 안 될 것이 애플리케이션 계층 프로토콜을 선택하는 확장 기능이다. 

 

처음에 구글이 NPN(Next Protocol negotiation) 확장을 제안해서 RFC화를 목표로 초안이 만들어졌다. 그러나 니고시에이션 흐름이 크게 달라져버렸고, 다른 방식인 ALPN(Applcation-Layer Protocol Negotiation) 확장 방식이 선택돼 RFC 7301이 됐다.

 

ALPN에서는 TLS의 최초 핸드셰이크 시 (Client Hello) 클라이언트가 서버에 '클라이언트가 이용할 수 있는 프로토콜 목록'을 첨부해서 보낸다. 서버는 그에 대한 응답 (Server Hello)으로 키 교환을 하고 인증서와 함께 선택한 프로토콜을 보낸다. 클라이언트가 보낸 목록에서 서버가 사용할 프로토콜을 하나 골라 반환하는 방법은 콘텐트 니고시에이션과 같다.

 

선택할 수 있는 프로토콜 목록은 IANA에서 관리한다. 현재 등록된 이름은 아래와 같다. 주로 HTTP 계열과 WebRTC 계열 프로토콜이 있다.

 

 

2.9 TLS가 지키는 것

TLS 1.3의 인증된 암호 모드 알고리즘은 통신 내부가 보이지 않게 하고, 조작도 사칭도 되지 않도록 보호한다. 여기서 중요한 것은 공통 키의 안전한 교환이다. 그렇기때문에 DHE, ECDHE 같은 키 교환 알고리즘을 이용하여 키를 찾기 힘들도록 한다. 다만 이 방법은 도중에 통신 내용을 바꿀 수 있는 중간자 공격에 약하기 때문에, 인증서 인증을 함께 사용하여 조작 위험성을 줄인다.

 

 

3.0 PUT 메서드와 DELETE 메서드와 표준화

HTTP/1.0에서 옵션이었던 PUT과 DELETE 메서드도 HTTP/1.1에서는 필수 메서드로 추가됐다. 이로써 DB에서 데이터를 다룰 때 사용하는 기본적인 CRUD가 갖추어져, HTTP는 데이터를 취급하는 프로토콜로도 이용할 수 있게 됐다.

데이터를 다루는 기본 메서드

 

데이터베이스의 경우 트랜잭션이라는 큰 테두리 안에서 데이터의 불일치가 일어나지 않도록 CRUD를 사용한 1회 액션으로 데이터를 갱신한다. HTTP에는 트랜재개션이 없고, 1회 액션에 해당하는 조작이 HTTP의 1 request이다. 

 

 

 

4.0 OPTIONS, TRACE, CONNECT 메서드 추가

HTTP/1.1에서는 OPTIONS, TRACE, CONNECT라는 새로운 메서드가 추가됐다. 이 중에서 CONNECT가 가장 자주 사용되는 새 메서드다.

 

4.1 OPTIONS

서버가 받아들일 수 있는 메서드 목록을 반환

command 창에서 curl을 이용해서 불러보자. google에 테스트를 해봤고 아래 결과를 받았다.

$ curl -X OPTIONS -v https://www.google.com
....


< HTTP/2 405 
< allow: GET, HEAD
< date: Thu, 22 Feb 2024 11:05:58 GMT
< content-type: text/html; charset=UTF-8

allow 헤더에 GET, HEAD만 받아들일 수 있다고 응답을 보낸 것을 확인할 수 있다. 참고로 405 Method Not Allowed를 반환했는데, google에서 OPTIONS라는 메서드는 받아들일 수 없기때문에 405를 반환한 것이다. 추가적으로 네이버는 404 Not Found를 반환한다. OPTIONS 헤더로 요청이 오면 404 Not Found를 반환하도록 되어있는 것 같다.

 

4.3 CONNECT

HTTP 프로토콜상에 다른 프로토콜의 패킷을 흘릴 수 있게 한다. 프록시 서버를 거쳐, 대상 서버에 접속하는 것을 목적으로 한다.

주로 https 통신을 중계하는 용도로 사용된다. (왜?) Squid의 CONNECT 설정에 관한 웹 문서를 보더라도 https 이외의 CONNECT 접속을 거부한다라는 설정을 소개한 페이지가 대부분이다. 

 

CONNECT 메서드를 이용하고 싶은 클라이언트는 다음과 같은 내용을 프록시 서버에 전송한다.

CONNECT example.con:8889 HTTP 1.1

CONNECT 메서드를 무조건 받아들이는 프록시는 아무 프로토콜이나 통과시켜버리므로, 맬웨어가 메일을 보내거나 하는 통신 경로로 사용될 위험이 있다. 

 

실제 프록시 서버인 squid를 사용해, 외부 사이트에 연결해보자. 로컬 3128 포트에서 squid를 시작한다.

$ docker run -d -p 3128:3128 --name squid poklet/squid

다음으로 squid를 프록시로 사용해 외부 https 서버에 접속해보자. 

$ curl -x http://localhost:3128 -v https://yahoo.com
*   Trying 127.0.0.1:3128...
* Connected to (nil) (127.0.0.1) port 3128 (#0)
* allocate connect buffer
* Establish HTTP proxy tunnel to yahoo.com:443
> CONNECT yahoo.com:443 HTTP/1.1
> Host: yahoo.com:443
> User-Agent: curl/7.86.0
> Proxy-Connection: Keep-Alive

< HTTP/1.0 200 Connection established

* Proxy replied 200 to CONNECT request
* CONNECT phase completed
* ALPN: offers h2
* ALPN: offers http/1.1
*  CAfile: /etc/ssl/cert.pem
*  CApath: none
* (304) (OUT), TLS handshake, Client hello (1):
* (304) (IN), TLS handshake, Server hello (2):
* (304) (IN), TLS handshake, Unknown (8):
* (304) (IN), TLS handshake, Certificate (11):
* (304) (IN), TLS handshake, CERT verify (15):
* (304) (IN), TLS handshake, Finished (20):
* (304) (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / AEAD-AES128-GCM-SHA256
* ALPN: server accepted h2
* Server certificate:
*  subject: C=US; ST=California; L=Sunnyvale; O=Oath Holdings Inc.; CN=yahoo.com
*  start date: Feb 20 00:00:00 2024 GMT
*  expire date: Aug 14 23:59:59 2024 GMT
*  subjectAltName: host "yahoo.com" matched cert's "yahoo.com"
*  issuer: C=US; O=DigiCert Inc; OU=www.digicert.com; CN=DigiCert SHA2 High Assurance Server CA
*  SSL certificate verify ok.
* Using HTTP2, server supports multiplexing
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* h2h3 [:method: GET]
* h2h3 [:path: /]
* h2h3 [:scheme: https]
* h2h3 [:authority: yahoo.com]
* h2h3 [user-agent: curl/7.86.0]
* h2h3 [accept: */*]
* Using Stream ID: 1 (easy handle 0x7fe53500d000)
> GET / HTTP/2
> Host: yahoo.com
> user-agent: curl/7.86.0
> accept: */*

< HTTP/2 301 
< date: Thu, 22 Feb 2024 15:53:06 GMT
< strict-transport-security: max-age=31536000
< server: ATS
< cache-control: no-store, no-cache
< content-type: text/html
< content-language: en
< x-frame-options: SAMEORIGIN
< referrer-policy: no-referrer-when-downgrade
< x-content-type-options: nosniff
< x-xss-protection: 1; mode=block
< location: https://www.yahoo.com/
< content-length: 8

* Connection #0 to host (nil) left intact

먼저 로컬 호스트의 3128 포트에 접속했지만, CONNECT 메서드로 yahoo.com의 https용 포트인 443 포트에 연결하러 가고 있는 것을 볼 수 있다.  HTTP/1.0 200 Connection established를 반환한 것은 프록시 서버이다. 실제 yahoo.com 서버는 www.yahoo.com으로 연결되길 원하므로, 301 Redirect를 반환했다. 이쪽은 프록시가 아닌 프록시 끝의 서버가 반환하는 내용이다.

 

확인을 다 했으면, 아래 명령어로 Squid 프록시 서버를 멈춰주자.

$ docker stop squid

 

 

 

5.0 프로토콜 업그레이드

HTTP/1.1 부터는 HTTP 이외의 프로토콜로 업그레이드가 가능하다. HTTP/1.0과 HTTP/1.1은 text 기반의 알기 쉬운 프로토콜이지만, 이 기능을 사용해 이진 프로토콜로 교체할 수 있다. 업그레이드는 클라이언트, 서버 측 모두 요청할 수 있다.

  • HTTP에서 TLS를 사용한 안전한 통신으로 업그레이드 (TLS/1.0, TLS/1.1, TLS/1.2)
  • HTTP에서 웹소켓을 사용한 양방향 통신으로 업그레이드(websocket)
  • HTTP에서 HTTP/2로 업그레이드(h2c)

HTTP에서 TLS로의 업그레이드는 RFC 2817에 설명되어 있다. 다만 이 방법으로 업그레이드해도 보안이 지켜지지 않는 문제가 있다. 현재는 모든 통신이 TLS화 되고 있으며, TLS 자체가 갖는 핸드세이크 시 프로토콜 선택 기능(ALPN)을 사용하도록 권장하고 있다. HTTP/2에서는 프로토콜 업그레이드 기능이 삭제 됐다.

 

HTTP/2 통신도 TLS를 전제로 하고, TLS의 ALPN 사용을 권장한다. 현재 프로토콜 업그레이드는 거의 웹소켓용 이다. 

요청 하는 방법은 167page 참고. 보통 클라이언트, 서버 측 모두 Upgrade와 Connection 헤더를 포함해서 요청한다.

 

 

6.0 가상 호스트 지원

HTTP/1.0은 한 대의 웹 서버로 하나의 도메인만 다루는 것이 전제였다. 하지만 웹사이트마다 서버를 따로 준비하는 것은 매우 힘든 일이다. 그렇기때문에 하나의 웹 서버로 여러 웹 서비스를 운영하는 방법이 HTTP/1.1에서 지원되기 시작했다.

 

http://example.com/hello라는 url에 접속하고 싶다고 가정해보자. example.com 부분을 꺼내서 도메인 네임 서버에 문의하면 도메인을 갖는 서버의 IP 주소를 알 수 있다. 다음에 http 부분 또는 도메인 이름 뒤에 포트 번호(8080)를 정해보자. HTTP/1.0까지는 실제의 서버가 받는 정보는 마지막 경로인 /hello 뿐이었다. 

 

HTTP/1.1에서는 클라이언트가 Host 헤더에 요청을 보내고자 하는 서버 이름을 기술할 의무가 생겼다. curl 커맨드도 아무런 설정을 하지 않아도 이 헤더를 부여한다. 같은 서버 같은 포트로 tokyo.example.com과 osaka.example.com이라는 두 개의 서비스가 호스트되고 있다고 가정하자. 요청 헤더의 Host 헤더를 보면, 서버는 어떤 서비스를 요청하는지를 판정할 수 있다.

 

아파치 웹 서버를 사용하면 호스트 이름에 따라서 해당하는 서비스의 콘텐츠를 가져와 반환할 수 있다.

NameVirtualHost *:80

<VirtualHost *:80>
    ServerName tokyo.example.com
    DocumentRoot /www/tokyo
</VirtualHost>

<VirtualHost *: 80>
    ServerName osaka.example.com
    DocumentRoot /www/osaka
</VirtualHost>

클라이언트에서는 Host를 붙이는 것 뿐이지만, 서버에서는 그 정보를 바탕으로 같은 서버에서 콘텐츠를 구분해 보낼 수 있게 된다.

 

 

7.0 청크

HTTP/1.1에서 지원되는 새로운 데이터 표현으로, 전체를 한꺼번에 전송하지 않고 작게 나눠 전송하는 청크방식이 있다. 청크를 사용하면 시간이 오래 걸리는 데이터 전송을 조금씩 앞당겨 시행할 수 있다. 청크 방식을 스트리밍 다운로드/업로드라고 부르는 경우도 있다. 

 

예를 들면 라이브 동영상을 배포하거나 시간이 걸리는 검색 결과를 전송할 때, 동영상의 앞부분부터 혹은 검색 엔진이 찾아낸 순서대로 반환할 수 있다. 클라이언트 측에서 처리할 때는 청크를 통합한 후 처리하지만, 서버 측에서는 전송에 필요한 블록만 메모리에 로드해 TCP 소켓에 데이터를 실어 보낼 수 있다. 따라서 1GB짜리 동영상 파일을 보내는 경우라도 메모리를 1GB 소비하는 일은 없다. 클라이언트 측의 장점으로는 서버 측에서 마지막 데이터 준비가 됐을 무렵엔 그 전까지의 데이터는 이미 전송이 끝났으므로 리드 타임을 짧게 할 수 있다. JPEG, GIF, PNG라면 다운로드된 부분만 표시하거나 인터레이스 방식 표시도 할 수 있으므로 사용자에 대한 응답속도도 빨라진다.

 

청크의 구조는 아래와 같다.

HTTP/1.1 200 OK
Date: Sun, 3 Mar 2024 00:50:21 GMT
Content-Type: video/webm
Transfer-Encoding: chunked

186a0
(100KB분의 데이터)
186a0
(100KB분의 데이터)
186a0
(100KB분의 데이터)
0

 

바디는 몇 개의 데이터 덩어리로 나뉘어 있다. 우선 16진수로 표시된 파일 크기가 표시되어 있고, 그 뒤로 지정한 크기만큼 데이터가 이어진다. Transfer-Encoding: chunked가 설정됐을 때는 Content-Length 헤더를 포함해선 안 된다고 RFC에 정의되어 있다. 데이터 크기는 지정된 크기의 합계가 된다. 마지막으로 0을 보내면 청크 전송이 모두 끝났다는 신호가 된다.

 

청크는 다운로드뿐만이 아니라 업로드에서도 사용할 수 있다. 업로드할 때도 형식은 똑같다.

 

 

7.1 메시지 끝에 헤더 추가

청크 형식으로 전송하는 경우에 청크된 메시지 끝에 헤더를 추가할 수 있게 됐다.

Trailer: Content-Type

'여기서 부여한 헤더는 바디를 보낸 후 전송된다'라고 알려준다. 청크 형식으로만 사용할 수 있다는 것은 청크 형식임을 사전에 알 수 있게 해야 하므로, 이를 위해 필요한 헤더는지정할 수 없다. 또한 Trailer 자신을 나중에 보낼 수 없다. 따라서 다음의 헤더는 지정할 수 없다.

  • Transfer-Encoding
  • Content-Length
  • Trailer

 

8.0 바디 전송 확인

클라이언트에서 서버로 한 번에 데이터를 보내는 게 아니라, 일단 받아들일 수 있는지 물어보고 나서 데이터를 보내는 2단계 전송을 할 수 있게 됐다.

 

우선 클라이언트는 다음 헤더와 바디를 제외한 모든 헤더를 지정해 문의한다. 파일이 없어도 Content-Length 헤더를 함께 보낸다.

Expect: 100-continue

만약 서버로부터 아래와 같은 응답이 돌아왔다면, 서버가 처리할 수 있다는 말이므로 바디를 붙여 다시 전송한다.

100 Continue

서버가 지원하지 않으면 417 EXPECTATION FAILED가 돌아오기도 한다.

 

curl 커맨드는 기본적으로 이 헤더를 전송해 2단계로 포스트한다. 전송할 콘텐츠의 크기가 1025 바이트 이상이면 이렇게 동작한다. 이를 억제하려면 아래와 같이 Expect 헤더를 비워서 보낸다.

curl -H "Expect:" --data-binary @bigfile.txt http://localhost:18888       

 

 

 

 

Reference

  • 리얼월드 HTTP : 역사와 코드로 배우는 인터넷과 웹 기술

 

Why>
무분별한 크롤링을 막기위한 방식이 필요하다.

인터넷은 브라우저를 이용해 문서를 열람하는 구조로 출발했지만, 점차 검색 엔진이 정보를 수집하는 자동 순회 프로그램이 많이 운용되게 됐다. 자동 순회 프로그램은 '크롤러', '로봇', '봇' 과 같은 이름으로 불린다. 정확히 자동 순회 프로그램은 봇이지만, 대부분 검색 엔진에서 정보를 수집(크롤)하는 용도로 운용되므로 거의 같은 뜻으로 사용된다. 

 

크롤러의 접근을 제어하는 방법으로 주로 아래와 같이 두 가지가 사용된다.

  • robots.txt
  • 사이트맵

1. robots.txt

서버 콘텐츠 제공자가 크롤러에게 접근 허가 여부를 전하기 위한 프로토콜

크롤러 개발자가 멤버로 참여한 메일링 리스트 내에서 논의되어 1994년 무렵네 정해졌다. 이 규칙을 읽고 해석해 실제로 접근을 중단하는 것은 크롤러 쪽이므로, 크롤러 개발자들의 신사협정이라 할 수 있다. 현재는 구글, 네이버, 빙 등 많은 검색 엔진의 크롤러가 이 텍스트를 해석할 수 있다.

 

robots.txt는 아래와 같은 형식으로 읽기를 금지할 크롤러의 이름과 장소를 지정한다.

User-agent: *
Disallow: /cgi-bin/
Disallow: /tmp/

여기서 모든 크롤러에 대해 /cgi-bin 폴더와 /tmp 폴더 접근을 금지했다. User-agent에 구글 봇처럼 개별적으로 지정할 수도 있다.

 

robots.txt와 비슷한 내용을 HTML의 메타 태그로도 기술할 수 있다. robots.txt가 우선하지만, 메타 태그로 더 자세히 지정할 수 있다. 아래는 메타 태그로 크롤러를 거부한다는 의미이다.

<meta name="robots" content="noindex" />

 

content 속성에는 다양한 디렉티브를 기술할 수 있다. 구글 봇이 해석하는 디렉티브에 관한 자세한 내용은 구글 사이트(https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag)에 기재되어 있다. 대표적으로는 아래와 같다.

구글 디렉티브

같은 디렉티브는 HTTP의 X-Robots-Tag 헤더에도 쓸 수 있다. 아래는 그 예시이다.

X-Robots-Tag: noindex, nofollow

 

 

2. robots.txt와 재판 결과

robots.txt는 1997년 RFC 드래프트 단계까지 도달했지만, 현 시즘에서 정식 RFC는 아니다. 그러나 사실상 표준으로 널리 인지되고 있고 HTML4 사양에서도 설명되고 있다. 게다가 법적으로 효력이 있는 판례가 나오고 있고, 2014년 개정된 일본 저작권법 시행 규칙에도 'robots.txt'라고 명시되어 있다.

 

2006년에는 구글의 캐시가 저작권을 침해한다는 이유로 열린 유명한 소송 '필드 대 구글 사건'이 있다. 이는 작가이자 변호사였던 블레이크 필드가 구글에 낸 소송이었는데, 결과적으로 구글의 주장이 인정되었다. 판결에 결정적인 것이 robots.txt였다. 원고인 필드는 robots.txt로 크롤러의 접속을 금지하는 방법을 알고 있었지만 그 방법을 쓰지 않았기 때문에 재판에서 저작권 침해를 주장할 수 없었다. 

 

robots.txt 웹사이트에서는 관련 판례를 몇 가지 소개하고 있다. 웹 서비스 제공자와 크롤러 제작자 사이에 계약서를 쓰지 않지만, robots.txt를 설치하면 웹 서비스 제공자가 명확히 의사를 표명한 것으로 봐야 하므로 크롤러는 이를 지켜야 한다. 또한, robots.txt를 설치해 거부하지 않는 콘텐츠를 크롤러가 이용하는 것은 '금반언원칙'을 어기는 것으로 필드 대 구글사건처럼 소송이 기각된다.

 

찾아보니 웹 서비스를 제공하는 유명한 회사들은 robots.txt를 제공한다.

네이버 robots.txt

위 robots.txt 파일의 의미는 사이트의 루트 페이지만 수집 허용으로 설정한다는 것이다. 이전 프로젝트를 진행하며 Indeed라는 공고 사이트에서 크롤링을 하려한 적이 있었는데, 크롤링을 하지 못하도록 막혀있었던 경험이 있다. http://www.indeed.com/robots.txt를 확인해보니 경로를 Disallow 해놓은 것을 확인할 수 있다.

 

3. 사이트맵

사이트맵은 웹사이트에 포함된 페이지 목록과 메타데이터를 제공하는 XML 파일로, 2005년에 구글이 개발해 야후나 마이크로소프트에서도 이용하게 됐다. robots.txt가 블랙리스트처럼 사용된다면, 사이트맵은 화이트리스트처럼 사용된다. 사이트맵은 구글, 야후, 마이크로소프트가 공동으로 운용하는 다음 사이트에 사양이 공개되어 있다.

사이트맵 사이트에는 기본 설정 항목이 정의되어 있지만, 해석하는 검색 엔진마다 다른 기능이 추가되기도 한다. 아래 예제처럼 XML 형식으로 기술한다.

<?xml version="1.0" encoding="UTF-8"?>

<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">

   <url>

      <loc>http://www.example.com/</loc>

      <lastmod>2005-01-01</lastmod>

      <changefreq>monthly</changefreq>

      <priority>0.8</priority>

   </url>

</urlset>

 

이 <url> 태그를 등록하고 싶은 페이지 수만큼 작성한다. <loc>는 절대 URL이다. xml 형식이 가장 많이 사용되지만 단순히 URL이 나열된 txt 파일이나 Rss 등의 형식도 사이트맵으로 사용할 수 있다.

 

사이트맵은 robots.txt에 쓸 수도 있다. 또한, 각 검색 엔진에 XML 파일을 업로드하는 방법도 있다. 아래는 robots.txt에 작성한 예시이다.

Sitemap: http://www.example.org/sitemap.xml

 

구글의 경우는 사이트맵을 사용해 웹 사이트의 메타데이터를 검색 엔진에 전달할 수 있다.

 

 

 

 

Reference

  • 리얼월드 HTTP : 역사와 코드로 배우는 인터넷과 웹 기술

 

Why>
클라이언트가 해당 사이트에 어떠한 경로로 접근하는지 알 수 있도록 해주는 헤더가 필요해서 만들어졌다.

 

사용자가 어느 경로로 웹사이트에 도달했는지 서버가 파악할 수 있도록 클라이언트가 서버에 보내는 헤더다. 추측이지만, Tistory 블로그 관리탭에 보면 유입 경로, 방문 통계에서 사용되는 헤더인 것 같다. 

 

클라이언트가 http://www.example.com/link.html의 링크를 클릭해서 다른 사이트로 이동할 때, 링크가 있는 페이지의 URL을 목적지 사이트의 서버에 아래와 같은 형식으로 전송한다.

Referer: http://www.example.com/link.html

철자가 referrer가 아닌 referer은 이유는 RFC 1945 제안 당시의 오자가 그대로 남아있기 때문이다.

 

만약 북마크에서 선택하거나 주소창에서 키보드로 직접 입력했을 때는 Referer 태그를 전송하지 않거나 Referer:about:blank를 전송한다.

 

 

 

 

Reference

  • 리얼월드 HTTP : 역사와 코드로 배우는 인터넷과 웹 기술

 

Cache

Why>
변경되지 않은 파일, 데이터, 값들을 다시 통신을 통해 가져올 필요가 있는가에 대한 고민으로 나온 기술이다. 자주 사용하는 데이터, 파일, 값을 미리 복사하여 사용하는 방식이다.

 

 

갱신 일자에 따른 캐시

What>
웹서버가 Last-Modified라는 헤더를 Response에 포함하여 클라이언트로 전송한다. 날짜는 RFC 1123이라는 형식으로 기술되며 Timezone은 GMT를 설정.

 

웹서버에서 보낸 헤더

Last-Modified: Web, 03 Jun 2023 15:23:45 GMT

웹 브라우저가 캐시된 URL을 다시 읽을 때는 서버에서 반환된 일시를 그대로 If-Modified-Since 헤더에 넣어서 요청

If-Modified-Since: Web, 03 Jun 2023 15:23:45 GMT

웹서버는 클라이언트의 요청을 받고 If-Modified-Since 헤더를 통해 일시를 확인한다. 만약, 웹 서버에 데이터가 변경되었다면 200 OK Status Code와 데이터를 Body에 넣어 보낸다. 그대로라면 304 Not Modified Status Code를 반환한다.

 

갱신 일자

 

 

Expires

Why>
갱신 일시를 이용하는 캐시의 경우 캐시의 유효성을 확인하기 위한 통신이 발생한다. 확인하기 위한 통신을 하지 않고, 캐시를 사용할 수 있도록 해주는 헤더

 

HTTP/1.0에 도입되었고, Expires Header를 이용하는 방법이다. Expires header에는 날짜와 시간이 들어간다. 클라이언트는 지정한 기한 내라면 캐시가 신선하다고 판단해 강제로 캐시를 이용한다. 

Expires: Web, 03 Jun 2023 15:23:45 GMT

Expires

 

 

Pragma: no-cache

Why>
요청을 보낼 때 저장된 캐시를 사용하지 않고 싶을 수도 있다. 그러기 위해서는 헤더에 저장된 캐시를 사용하지 않겠다는 것을 명시해야 한다. 이를위해 사용하는 것이 Pragma이다.

 

no-cache는 '요청한 컨텐츠가 이미 저장돼 있어도, 원래 서버에서 가져오라'고 프록시 서버에 지시하는 것이다. 참고로, no-cache는 HTTP/1.1에서 Cache-Control로 통합되었다. 

 

 

ETag 추가

Why>
날짜와 시간을 이용한 캐시 비교만으로 해결할 수 없을 때도 있다. 클라이언트마다 같은 사이트이지만, 각각 다른 화면을 동적으로 보여줘야한다면 말이다. 하지만, 동적으로 바뀌는 요소가 늘어날수록 어떤 날짜를 근거로 캐시의 유효성을 판단해야 하는지는 정말 어렵다. 따라서 하나의 수치로 귀착시키는 방법이 ETag이다.

 

RFC 2068의 HTTP/1.1에서 추가된 ETag는 순차적인 갱신 일시가 아니라 파일의 해시 값으로 비교한다. 웹서버에서 응답에 ETag 헤더를 부여한다. 클라이언트는 If-None-Match 헤더에 다운로드된 캐시에 들어있던 ETag 값을 추가해 요청한다. 서버는 보내려는 파일의 ETag와 비교해서 같으면 304 Not Modified Status Code를 반환한다. 

ETag

 

 

 

Cache-Control

Why>
보다 자세한 Cache Control을 위해 만들어졌다.

 

ETag와 같은 시기에 HTTP/1.1에서 추가된 것이 Cache-Control이다. Expires보다 우선해서 처리된다. 서버가 response로 보내는 헤더는 아래와 같다.

  • public
    • 같은 컴퓨터를 사용하는 복수의 사용자간 캐시 재사용을 허가한다. 
  • private
    • 같은 컴퓨터를 사용하는 다른 사용자 간 캐시를 재사용하지 않는다. 같은 URL에서 사용자마다 다른 콘텐츠가 들어오는 경우
  • max-age-n
    • 캐시의 신선도를 초단위로 설정한다. 86400을 지정하면 하루동안 캐시가 유효하고 서버에 요청하지 않고 캐시를 이용한다. 그 이후는 서버에 요청하고 304 Not Modified가 반환됐을 때만 캐시를 이용한다.
  • s-maxage-n
    • max-age-n과 같으나 공유 캐시에 대한 설정값이다.
  • no-cache
    • 캐시가 유효한지 매번 요청한다. max-age-0과 거의 같다.
    • Pragma: no-cache와 똑같이 캐시하지 않는 것은 아니다. 시간을 보고 서버에 접속하지 않은 채 콘텐츠를 재이용하는 것을 그만둘 뿐이다. 갱신 일자와 ETag를 사용하며, 서버가 304를 반환했을 때 이용하는 캐시는 유효하다.
  • no-store
    • 캐시하지 않는다.

위 헤더를 콤마로 구분해서 복수로 지정이 가능하지만, 내용면에서 아래와 같이 조합한다.

  • private, public 중 하나 혹은 설정하지 않는다. (default는 private)
  • max-age, s-maxge, no-cache, no-store 중 하나

cache-control

 

클라이언트 측에서 요청 헤더에 사용할 수 있는 설정 값은 아래와 같다.

  • no-cache
    • Pragma: no-cache와 같다.
  • no-store
    • 응답의 no-store와 같고, 프록시 서버에 캐시를 삭제하도록 요청
  • max-stale
    • 지정한 시간만큼 유지 기간이 지났어도 클라이언트는 지정한 시간 동안 저장된 캐시를 재사용하라고 프록시에 요청. 연장 시간은 생략할 수 있고, 그런 경우 영원히 유효하다는 의미 
  • min-fresh
    • 캐시의 수명이 지정된 시간 이상 남아있을 때, 캐시를 보내도 좋다고 프록시에 요청. 즉, 적어도 지정된 시간만큼은 신선해야한다.
  • no-transform
    • 프록시가 콘텐츠를 변형하지 않도록 요청 (게이트웨이와 같이 동작하라는 의미?)
  • only-if-cached
    • 캐시된 경우에만 응답을 반호나하고, 캐시된 콘텐츠가 없을 땐 504 Gateway Timeout status code를 반환하도록 프록시에 요청. 이 헤더가 설정되면 처음을 제외하고 오리진 서버에 전혀 액세스 하지 않음.

 

응답 헤더에서 서버가 프록시에 보내는 캐시 컨트롤 지시는 아래와 같다. 

  • no-transform
    • 프록시가 콘텐츠를 변경하는 것을 제어한다.
  • must-revalidate
    • no-cache와 비슷하지만 프록시 서버에 보내는 지시. 프록시 서버가 서버에 문의했을 때 서버의 응답이 없으면, 프록시 서버가 클라이언트에 504 Gateway Timeout이 반환되기를 기대.
  • proxy-revalidate
    • must-revalidate와 같지만, 공유 캐시에 요청

 

Vary

Why>
ETag는 같은 URL이라도 개인마다 달라지는 경우에 사용한다. 하지만, 같은 URL이라도 클라이언트에 따라 반환 결과가 달라야할 경우도 존재함. 이 경우에 사용하는 것이 Vary다.

 

예를들어, 사용자의 브라우저가 스마트폰용 일 때는 모바일용 페이지를 보여주는 등 언어에 내용이 바뀌는 경우를 들 수 있다. 이처럼 표시가 바뀌는 이유에 해당하는 헤더명을 Vary에 나열함으로써 잘못된 콘텐츠의 캐시로 사용되지 않게 합니다.

Vary: User-Agent, Accept-Language

 

로그인이 필요한 사이트라면 쿠키도 지시할 것이다.

 

Vary header는 검색 엔진용 힌트로도 사용된다. 브라우저 종류에 따라 콘텐츠가 바뀔 수 있다는 것은 모바일 버전은 다르게 보일수도 있다고 판단할 수 있는 재료가 된다. 그리고 영어 버전, 한국어 버전 등 언어별로 바르게 인덱스를 만드는 힌트도 된다.

 

모바일 브라우저인지 판정하는 방법은 User-Agent이다. 아래는 안드로이드 user-agent의 예시이다.

User-Agent: "Mozilla/5.0 (Linux; Android 8.0.0; SM-G960F Build/R16NW)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36"

아래는 윈도우 user-agent의 예시이다.

User-Agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246"

이처럼 User-Agent를 통해 판정할 수 있다. 서버에서는 이 정보를 바탕으로 콘텐츠를 나눠 내보낼 수 있다. 2017년 구글 가이드라인에서는 같은 콘텐츠를 모든 브라우저에 배포하고, 브라우저가 필요한 설정을 선택하는 반응형 웹 디자인을 권장한다. 

 

 

 

 

 

Reference

  • 리얼월드 HTTP : 역사와 코드로 배우는 인터넷과 웹 기술

 

 

 

+ Recent posts