2. Semantics of Http/1.0

Timeless, 4-core components of HTTP

  • Method, URL(domain + path)

  • Header

  • Body

  • Status code

2.1. Simple form transfer (x-www-form-urlencoded)

form encode request:

#!/bin/sh

curl --http1.0 --data-urlencode title="Head First PHP & MySQL" \
--data-urlencode author="Lynn Beighley, Michael Morrison" \
http://$1 --verbose

output:

Content-Length: 71
Content-Type: application/x-www-form-urlencoded
User-Agent: curl/7.82.0

title=Head+First+PHP+%26+MySQL&author=Lynn+Beighley%2C+Michael+Morrison
URL encoding (wikipedia)
Percent-encodingURL encoding이라고도 알려져 있으며,
형식적인 데이터를 US-ASCII만을 허용하는 URI로 encode하는 방법이다.
일반적으로 주류 URI셋에서 사용되는데, application/x-www-form-urlencoded의 Media type 데이터의 준비작업에 사용되며 HTML form에 사용된다.

2.2. File transfer using form (multipart/form-data)

<form action="POST" enctype="multipart/form-data></form>
멀티파트를 이용하는 경우는 한번의 요청으로 복수의 파일을 받는 쪽에서 구분할 수 있어야합니다.
경계문자열(boundary)가 포함되어 브라우저가 독자적인 포멧을 랜덤하게 만들어냅니다.
Content-Type: multipart/form-data; boundary=-----WebkitFromBoundaryy0YfbccgoID172j7

Note

헤더에 Content-Disposition 항목이 포함됩니다.

2.2.1. Multipart form test

  • Request

    #!/bin/sh
    
    curl --http1.0 -F "file2<test.txt;type=text/html" -F "file1@test.txt;type=text/html" http://$1 --verbose
    
  • Client

    POST / HTTP/1.0
    > Host: 127.0.0.1:18888
    > User-Agent: curl/7.82.0
    > Accept: */*
    > Content-Length: 354
    > Content-Type: multipart/form-data; boundary=------------------------8eef5e797ff1fda2
    >
    * We are completely uploaded and fine
    * Mark bundle as not supporting multiuse
    * HTTP 1.0, assume close after body
    < HTTP/1.0 200 OK
    < Date: Wed, 13 Apr 2022 12:27:39 GMT
    < Content-Length: 32
    < Content-Type: text/html; charset=utf-8
    
  • Server

    POST / HTTP/1.0
    Host: 127.0.0.1:18888
    Connection: close
    Accept: */*
    Content-Length: 333
    Content-Type: multipart/form-data; boundary=------------------------8eef5e797ff1fda2
    User-Agent: curl/7.82.0
    
    --------------------------8eef5e797ff1fda2
    Content-Disposition: form-data; name="file2"
    Content-Type: text/html
    
    simple string
    
    --------------------------8eef5e797ff1fda2
    Content-Disposition: form-data; name="file1"; filename="test.txt"
    Content-Type: text/html
    
    simple string
    
    --------------------------8eef5e797ff1fda2--
    

Note

text/plain

form의 encoding type은 기본적으로 www-form-urlencoded, 파일을 전송하고 싶을 때: multipart/form-data.

text/plain은 개행을 통해 각 값을 구분해 전송합니다.

` title=The Art of community author=john bacon `

서비스쪽이 보안이 약할 때 문제를 일으키는 경우가 있다고 합니다.

2.3. Content negotiation

서버와 클라이언트간 통신을 최적화 하고자 하나의 요청에서 최고의 설정을 공유하는 시스템

Request Header

Response Header

negotiation target

Accept

Content-Type

MIME Type

Accept-Language

Content-Type / html tag

language

Accept-Charset

Content-Type

character set

Accept-Encoding

Content-Encoding

compression type

2.3.1. File type 결정

Accept: text/html,application/xml;q=0.9,image/webp,*/*;p=0.8

  • image/webp

  • */*;q=0.8

quality value는 0~1 사이의 수치로, 기본 1.0 경우에는 명시하지 않습니다.
0.9 우선순위로 webp를 요청하고, 그렇지 않으면 다른 포맷(0.8) 다음으로 두고 있는 표현입니다.

2.3.2. Charset 결정

Accept-Charset: window-949,utf-8;q=0.7,*;q=0.3

브라우저가 모든 charest-encoder를 내장하고 있기 때문에, 협상이 필요없어 졌습니다.

Content-Type: text/html; charset=UTF-8

HTML의 경우 문서 안에 쓸 수도 있습니다.

  1. <meta http-equiv="Content-Type" content="text/html; charset=UTF-8>

  2. <meta charset="UTF-8">

2.3.3. 압축을 이용한 통신속도 향상

통신에 걸리는 시간보다 압축과 해체가 짧은 시간에 이루어지므로, 압축을 함으로써 웹페이지를 표시할 때 걸리는 전체적인 처리시간을 줄일 수 있습니다.

PROS:

- 전력소비 감소
- 데이터 사용 비용 절감

압축에 대한 negotiation은 header안에서 명시합니다.

Accept-Encoding: deflate, gzip

#!/bin/sh

curl --http1.0 --compressed -H "Accept-Encoding: gzip, br" http://$1 --verbose
> GET / HTTP/1.0
> Host: 127.0.0.1:18888
> User-Agent: curl/7.82.0
> Accept: */*
> Accept-Encoding: gzip, br
>
* Mark bundle as not supporting multiuse
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Date: Fri, 15 Apr 2022 11:34:53 GMT
< Content-Length: 32
< Content-Type: text/html; charset=utf-8
  • 서버가 압축 content를 지원하지 않기 떄문에 차이가 발생하지 않았을 뿐 요청을 정상적으로 처리되었습니다.

  • 서버가 gzip을 지원했을 경우, 아래와 같은 응답을 반환합니다.

    < Content-Encoding: gzip
    < Content-Length: (zipped size of bytes)
    
  • gzip보다 효율이 좋은 Brotil는 구글의 proposal로 등장하였습니다

    HTTP헤더를 사용해 1왕복을 데이터 교환 단계에서 하위 호환성을 유지하면서도, 서로 최적의 통신을 할 수 있게 시스템이 정비되었습니다.

See also

sdch(Shared Dictionary Compressing for HTTP), 는 IANA비등록 압축알고리즘이며, 중복되는 문구를 공유 사전으로 사용하여 통신량을 대폭 줄일 수 있는 알고리즘입니다. 이런 공유 방식은 HTTP/2의 헤더부분 압축에도 사용됩니다.

2.5. authorization and session

2.5.1. Basic Auth

Basic 인증

유저명과 password를 BASE64인코딩 처리합니다. 변환가능하므로, 그대로 원본을 추출할 수 있습니다.

Caution

SSL/TLS 통신을 사용하지 않은 채로 감청시에 손쉽게 유출됩니다.

#!/bin/sh

curl --http1.0 --basic --user june:pwd1234 http://$1 --verbose
* Connected to 127.0.0.1 (127.0.0.1) port 18888 (#0)
* Server auth using Basic with user 'june'
> GET / HTTP/1.0
> Host: 127.0.0.1:18888
> Authorization: Basic anVuZTpwd2QxMjM0
> User-Agent: curl/7.82.0
> Accept: */*

2.5.2. Digest Auth

Digest 인증

hash function(암호화는 쉽지만 복호화는 어렵게)을 이용합니다. 보호된 영역에 접속을 시도할 시에, 복호화에 실패하면 401 Unauthorized의 응답을 반환합니다.

  • RESPONSE HEADER

    WWW-authenticated: Digest realm="영역명", nonce="1234567890", algorithm=MDS, qop="auth"

    DESC

    • realm: 보호 영역 명칭

    • nonce: 서버가 매번 생성하는 random data

    • qop: 보호수준

    클라이언트에서는 nonce와 주어진 값을 통해 생성한 cnonce로 다음과 같은 BODY를 구합니다.

  • RESPONSE BODY

    A1 = USERNAME ":" realm ":" "PASSWORD"
    A2 = HTTP METHOD ":" Content URI
    response = MD5( MD5(A1) ":" nonce ":" nc ":" cnonce ":" qop ":" MD5(A2)
    

    DESC

    • nc: 전송횟수로 16진수 8자리(4byte)로 표현됩니다.

    클라이언트에서는 생성한 cnonce와 계산으로 구한 response를 모아 다음과 같은 header를 포함하는 request를 구성합니다.

  • REQUEST HEADER

    Authorization: Digest username="username", realm="area name"
          nouce="1234567890", url"/scret.html", algorithm=MD5,
          qop=auth, nc=00000001, cnounce="0987654321",
          response="9d47a3f8b2d5c"
    

    DESC

    • 서버측에서도 이 헤더의 정보와 서버에 저장된 유저명 패스워드로 같은 계산을 실시하여 비교합니다.

      클라이언트가 재발송한 요청과 동일한 response body가 계산되면, 사용자가 정확하게 유저명과 패스워드릴 입력했음을 의미합니다.

    • 이 과정으로 유저명과 패스워드 자체를 요청에 포함하지 않고 사용자를 올바르게 인증하였습니다.

  • digest server and client

    package main
    
    import (
    	"fmt"
    	"io/ioutil"
    	"log"
    	"net/http"
    	"net/http/httputil"
    
    	"github.com/k0kubun/pp"
    )
    
    func digest_handler(writer http.ResponseWriter, request *http.Request) {
    	pp.Printf("URL: %s\n", request.URL.String())
    	pp.Printf("Query: %s\n", request.URL.Query())
    	pp.Printf("Proto: %s\n", request.Proto)
    	pp.Printf("Method: %s\n", request.Method)
    	pp.Printf("Header: %s\n", request.Header)
    
    	defer request.Body.Close()
    	body, _ := ioutil.ReadAll(request.Body)
    	fmt.Printf("--body--\n%s\n", string(body))
    
    	if _, ok := request.Header["Authorization"]; !ok {
    		writer.Header().Add("WWW-Authenticate",
    			`Digest
    				realm="Secret Zone",
    				nonce="TgLc25U2BQA=f410a2789473e18e6587be703c2e67fe2b04afd",
    				algorithm=MD5,
    				qop="auth"`)
    		writer.WriteHeader(http.StatusUnauthorized)
    	} else {
    		fmt.Fprintf(writer, "<html><body>secret page</body></html>\n")
    	}
    }
    
    func handler(writer http.ResponseWriter, request *http.Request) {
    	dump, err := httputil.DumpRequest(request, true)
    	if err != nil {
    		http.Error(writer, fmt.Sprint(err), http.StatusInternalServerError)
    		return
    	}
    	fmt.Println(string(dump))
    	cookie_response(writer, request)
    }
    
    func cookie_response(writer http.ResponseWriter, request *http.Request) {
    	writer.Header().Add("Set-Cookie", "VISIT=TRUE")
    	if _, ok := request.Header["Cookie"]; ok {
    		fmt.Fprintf(writer, "<html><body>VISITED~</body></html>\n")
    	} else {
    		fmt.Fprintf(writer, "<html><body>FIRST VISIT!!</body></html>\n")
    	}
    }
    
    func main() {
    	var httpServer http.Server
    	http.HandleFunc("/", handler)
    	http.HandleFunc("/digest", digest_handler)
    	log.Println("start http listening :18888")
    	httpServer.Addr = ":18888"
    	log.Println(httpServer.ListenAndServe())
    }
    
    #!/bin/sh
    
    curl --http1.0 -d message="hi" -d to="june" --digest -u june:pwd1234 http://127.0.0.1:$1/digest --verbose
    
    *   Trying 127.0.0.1:18888...
    * Connected to 127.0.0.1 (127.0.0.1) port 18888 (#0)
    * Server auth using Digest with user 'june'
    > POST /digest HTTP/1.0
    > Host: 127.0.0.1:18888
    > User-Agent: curl/7.82.0
    > Accept: */*
    > Content-Length: 0
    > Content-Type: application/x-www-form-urlencoded
    >
    * Mark bundle as not supporting multiuse
    * HTTP 1.0, assume close after body
    < HTTP/1.0 401 Unauthorized
    < Www-Authenticate: Digest                              realm="Secret Zone",                            nonce="TgLc25U2BQA=f410a2789473e18e6587be703c2e67fe2b04afd",                          algorithm=MD5,                          qop="auth"
    < Date: Sat, 16 Apr 2022 05:52:42 GMT
    < Content-Length: 0
    <
    * Closing connection 0
    * Issue another request to this URL: 'http://127.0.0.1:18888/digest'
    * Hostname 127.0.0.1 was found in DNS cache
    *   Trying 127.0.0.1:18888...
    * Connected to 127.0.0.1 (127.0.0.1) port 18888 (#1)
    * Server auth using Digest with user 'june'
    > POST /digest HTTP/1.0
    > Host: 127.0.0.1:18888
    > Authorization: Digest username="june", realm="Secret Zone", nonce="TgLc25U2BQA=f410a2789473e18e6587be703c2e67fe2b04afd", uri="/digest", cnonce="NmIwMjc5NDc5ODkxYmI4MzNiOWVmNTEyMjdjYjgyNDI=", nc=00000001, qop=auth, response="f6159fc829fdce222d65cffb4d9423b3", algorithm=MD5
    > User-Agent: curl/7.82.0
    > Accept: */*
    > Content-Length: 18
    > Content-Type: application/x-www-form-urlencoded
    >
    * Mark bundle as not supporting multiuse
    * HTTP 1.0, assume close after body
    < HTTP/1.0 200 OK
    < Date: Sat, 16 Apr 2022 05:52:42 GMT
    < Content-Length: 38
    < Content-Type: text/html; charset=utf-8
    <
    <html><body>secret page</body></html>
    * Closing connection 1
    
    1. 요청이 auth digest의 경우 아래와 같은 헤더를 부여받아 response를 받는다.

      Www-Authenticate: Digest realm="Secret Zone", nonce="TgLc25U2BQA=f410a2789473e18e6587be703c2e67fe2b04afd", algorithm=MD5, qop="auth"

    2. 다시 요청을 보낼때 아래와 같이 cnonce , reponse 를 추가하여 header.Authorization을 전송하는데, 코드상에서 이것을 해석하는 부분은 비어있지만, MD5알고리즘으로 정해진 임의 규칙에 따라 암호화 했다는 것을 알 수 있다.

      Authorization: Digest username="june", realm="Secret Zone", nonce="TgLc25U2BQA=f410a2789473e18e6587be703c2e67fe2b04afd", uri="/digest", cnonce="NmIwMjc5NDc5ODkxYmI4MzNiOWVmNTEyMjdjYjgyNDI=", nc=00000001, qop=auth, response="f6159fc829fdce222d65cffb4d9423b3", algorithm=MD5

2.5.3. 쿠키를 통한 세션 관리

지금은 BASIC/DIGEST Auth는 쓰여지지 않는 이유

  • 요청할 때마다 패스워드와 유저 ID를 매회 전송해야 Session을 유지하는 것처럼 보일 수 있다.

  • 사용자 Device를 식별 불가능 하다. (OS의 기능에 대해 분류할 방법이 없다.)

Form, cookie, session token을 활용하는 방식

  • DIGEST 인증과 달리 직접 유저정보를 전송하므로 SSL/TLS가 필수적입니다.

  • 서버는 세션토큰을 생성하고, 토큰을 클라이언트에게 전송합니다.

2.6. Proxy

2.6.1. wikipedia.Proxy

Proxy 서버는 client request와 server providing resource 중간에서 전달자 역할을 합니다. 직접 서버에 요청을 연결하는 대신에 프록시 서버에게 연결하도록 하여, request를 분석하고, 요구되는 네트워크 transaction을 수행합니다.

아래와 같은 효과를 가질 수 있습니다.

  • request의 복잡성을 조절하는 역할

  • 로드 밸런싱, 보안 등의 이점

Proxies는 분산 시스템에 encapsulation과 구조를 더하는 것으로 소개되었습니다.

따라서 중간의 자리에서, 암시적으로 request의 원형을 masking하는 효과를 가집니다.

프록시는 HTTP/1.0 규격에서 부터 언급되었습니다.

GET /helloworld
Host: localhost:18888
GET https://example.com/helloworld
Host: example.com

2.6.2. HEADER.Proxy-Authenticate

프록시 서버가 악용되지 않도록 인증을 이용해 프록시 서버를 보호하는 경우도 있습니다.

중계되는 프록시는 중간의 호스트 IP주소를 특정 Header에 기록해 갑니다.

X-Forwarded-For: client, proxy1, proxy2 (RFC 7239)

$ curl --http1.0 --proxy http://proxy1.com -U user:pass http://server/helloworld

HTTPS통신의 프록시 지원은 HTTP1.1에 추가된 CONNECT method를 사용합니다.

2.7. Cache

content의 diversity에 따른 데이터 전송량 증가에 따라, 컨텐츠가 변경되지 않았을 때,
로컬에 저장된 파일을 재사용함으로써 다운로드 데이터를 줄이고 성능을 높이는 cache 매커니즘이 등장했습니다.

Note

GET, HEAD메서드 이외에는 ‘기본적으로’ 캐시되지 않습니다.

2.7.1. 갱신 일자에 따른 캐시

HTTP/1.0 당시에는 정적 컨텐츠 위주여서 파일의 timestamp가 변경되었는지 비교하는 것으로 데이터 전송여부를 결정할 수 있었습니다.

Response Header

Last-Modified: Wed, 08 Jun 2016 15:23:45 GMT

웹 브라우저가 캐시된 URL을 다시 읽을 때에 서버에서 반환된 일시를 그대로 넣어 요청합니다.

REQUEST HEADER

If-Modified-Since: Wed, 08 Jun 2016 15:23:45 GMT

위 요청에 따라 데이터가 변경되었다면,

  • 200 OK, 데이터가 변경되어 정상적으로 응답

  • 304 Not Modified, 데이터가 변경되지 않았으니 캐시를 사용하라는 응답

2.7.2. Expires

갱신일시를 이용하는 캐시의 경우 명백히 시간이 너무 오래 지났는데 캐시의 유효성을 확인하기 위해 불필요한 통신이 발생하는 경우가 있습니다.

이 캐시 자체에 유효시간을 두어 fresh를 체크하는 Expires 헤더가 http/1.0에 도입되었습니다.

2.7.3. ETag

날짜와 시간을 이용한 캐시 비교만으로 해결할 수 없을 때가 있습니다.

사용자의 정보나 상태에 따라 부분적인 변경이 한 주소에 동적으로 일어나는 경우가 늘어나거나 하는 등의 경우에, 날짜와 시간을 근거로 캐시의 신선함을 판단하기가 어려워 집니다.

이럴 때 사용할 수 있는 것이 HTTP/1.1에서 추가된 ETag(Entity tag)입니다.

Wikipedia.ETag

파일의 데이터로 비교하여 캐시의 유효성을 검증하는 매커니즘

  • 클라이언트가 조건적인 요청을 전달하는 것을 가능하게 합니다.

  • 대상 URL에 대한 자원이 변경되면 새로운 Etag가 생성되고 전달됩니다.

ETag는 서버가 자유롭게 결정해서 반환할 수 있습니다.

  • 아마존 S3의 경우 컨텐츠 파일의 해시 값이 사용되는 것으로 추정됩니다.

2.7.4. Cache-Control

ETag와 같이 HTTP/1.1에 추가된 Cache-Control

아래와 같은 값을 가질 수 있습니다.

  • public: 같은 컴퓨터를 사용하는 사용자간 캐시 재사용을 허가

  • private: 같은 컴퓨터라도 다른 사용자라면 캐시를 재사용 하지 않음(유저 별로 컨텐츠가 변경되는 경우)

  • max-age=n: 캐시의 fresh를 초단위로 설정. n == 86400이면 하루동안 캐시가 유효하고 시간동안 서버에 문의 없이 캐시사용

  • s-maxage=n: max-age와 같으나 공유 캐시에 대한 설정 값

  • no-cache: 캐시가 유효한지 매번 ETAG를 포함하도록 요청

  • no-store: 캐시하지 않도록 요청

Todo

cache-Control(2) header for Proxy server manages

2.7.5. Vary

같은 URL이라도 클라이언트에 따라 반환결과가 다름을 나타내는 헤더 Vary

Cache-Control을 통해서 사용자가 다른 경우 등에 대해 캐시를 사용하는지 등을 결정하는 것이 가능하지만,
애초에 특정 상태에 따라 데이터가 달라진다는 명백한 규칙이 있다면 좀 더 수월한 의사결정이 가능합니다.

Vary: User-Agent, Accept-Language

  • 캐시데이터가 있고 신선하며, 그럴 경우 캐시를 사용하도록 권유하더라도 해당 데이터가 모바일 네트워크 요청이었고 현재는 그렇지 않다면 다른 결과를 기대할 수 있다.

  • 이래서 반응형이 짱이다.

2.7.6. Referer

  • 개인정보가 GET 파라메터로 표시되게 만들면 안됩니다. referer를 통해 외부 서비스로 전송되므로 유출과 직결됩니다.

  • 방어적인 용도로 이미지에 직접 링크되는 것을 막고자, 이미지 다운로드 시 referer가 설정되지 않은 요청은 거절하는 경우도 있었습니다.

referer 헤더에 대해서는 아래에 대한 옵션이 존재합니다.

  • no-referer: 전송하지 않음

  • no-referer-when-downgrade: HTTPS -> HTTP 일때는 전송하지 않음

  • same-origin: 동일 도메인 내의 링크에 대해서 전송

  • origin: index페이지에서 링크된 것으로 해 도메인 이름만 전송한다.

  • strict-origin: origin + downgrade시 비전송

  • origin-when-crossorigin: 같은 도메인 내에서는 온전한 referer, 다른 도메인에서는 도메인 이름만 전송

  • strict-origin-when-crossorigin: 위와 같으나 HTTPS -> HTTP에서는 전송하지 않는다.

  • unsafe-url: 항상 전송한다.