Go - анализ исходного кода http.Client

Go

1. Причина

анализироватьhttp.ClientПричина реализации исходного кода заключается в том, что при использовании следующих шагов для имитации входа на веб-сайт возникает проблема, см.Зная - go net/http.Client обрабатывает перенаправление:

  1. POSTпароль от учетной записи и другие параметры для входа

  2. Изданныйtoken, этоtokenпройти черезcookieИзданный

  3. Перенаправить на домашнюю страницу/

проходя черезhttp.PostСделайте запрос, не ждите перенаправления, можете получить его напрямуюcookieзначение, но на самом деле go обрабатывает перенаправление для нас, потерянноеcookieценность

Проанализировав исходный код, эту проблему можно легко решить:

// 请求http.calabash.top将被301重定向到https

myClient := http.Client{
    CheckRedirect: func(req *http.Request, via []*http.Request) error {
        return http.ErrUseLastResponse
    },
}
respWithNoRedirect, _ := myClient.Get("http://blog.calabash.top")
respWithRedirect, _ := http.Get("http://blog.calabash.top")

fmt.Println(respWithNoRedirect.StatusCode) // 301
fmt.Println(respWithRedirect.StatusCode)   // 200

2. Client

HTTP-клиент, нулевое значение которого равноDefaultClient, ядро ​​анализа здесь лежит в способе обработки перенаправления

type Client struct {
    // 分析范围之外
    Transport RoundTripper
    // 重定向策略
    CheckRedirect func(req *Request, via []*Request) error
    // Cookie的存储, 单纯的get/set方法
    Jar CookieJar
    // 超时
    Timeout time.Duration
}

var DefaultClient = &Client{}

При обнаружении перенаправления, за исключением следующих случаев,ClientВсе начальные заголовки запроса будут перенаправлены:

  • Когда адрес перенаправления и начальный адресdomainразные и неsub domain, в заголовке запросаcookie, authorizationи другие чувствительные поля будут игнорироваться

  • перенаправления могут изменитьcookieзначение, так впередcookieЗаголовки запросов будут игнорироваться при любых измененияхcookie

2.1 Правила переадресации:redirectBehavior

func redirectBehavior(reqMethod string, resp *Response, ireq *Request) (redirectMethod string, shouldRedirect, includeBody bool) {
	switch resp.StatusCode {
	case 301, 302, 303:
		redirectMethod = reqMethod
		shouldRedirect = true
		includeBody = false

		if reqMethod != "GET" && reqMethod != "HEAD" {
			redirectMethod = "GET"
		}
	case 307, 308:
		redirectMethod = reqMethod
		shouldRedirect = true
		includeBody = true

		if resp.Header.Get("Location") == "" {
			shouldRedirect = false
			break
		}
		if ireq.GetBody == nil && ireq.outgoingLength() != 0 {
			shouldRedirect = false
		}
	}
	return redirectMethod, shouldRedirect, includeBody
}

Из исходного кода можно получить следующую информацию:

  • Для кодов статуса 301, 302, 303 перенаправление не идет с телом запроса, а для не-GET/HEADМетод запроса будет вынужден измениться наGETМетод запроса

  • Для кода состояния 307, 308 перенаправление не меняет метод запроса на изменение и повторно использует тело запроса.Locationпустой илиBodyЕсли пусто, не следует перенаправлять

Ссылаться наMDN - Http Status Code, который имеет описание

301: Хотя стандарт требует, чтобы браузер не модифицировал метод и тело http при получении ответа и перенаправлении, у некоторых браузеров могут возникнуть проблемы. Поэтому лучше использовать 301 при работе с методами GET или HEAD и использовать 308 вместо 301 в других случаях.

302: даже если регуляторные требования к обеспечению того, чтобы метод запроса браузера и запрашивать тот же предмет во время перенаправления, но не все пользовательские агенты последуют этому Рекомендуется использовать только код состояния 302 в ответ на методы получения или главы, другие альтернативы с использованием 307 301

303: Обычно в качестве возвращаемого результата операции PUT или POST указывает, что ссылка перенаправления указывает не на вновь загруженный ресурс, а на другую страницу. Метод запроса перенаправленной страницы всегда должен использовать GET

307: Единственное различие между кодами состояния 307 и 302 заключается в том, что при отправке запроса на перенаправление код состояния 307 гарантирует, что метод запроса и тело сообщения не изменятся.

308: В процессе перенаправления метод запроса и тело сообщения не меняются.Однако, когда возвращается код состояния 301, метод запроса иногда неправильно изменяется клиентом на метод GET.

в состоянии обнаружитьMDNописание иredirectBehaviorДизайн исходного кода очень последователен

2.2 Стратегия проверки для перенаправления:checkRedirect

когда не указаноCheckRedirectфункция,ClientБудет использоватьсяdefaultCheckRedirectСтратегия:По умолчанию до десяти перенаправлений

func (c *Client) checkRedirect(req *Request, via []*Request) error {
	fn := c.CheckRedirect
	if fn == nil {
		fn = defaultCheckRedirect
	}
	return fn(req, via)
}

func defaultCheckRedirect(req *Request, via []*Request) error {
	if len(via) >= 10 {
		return errors.New("stopped after 10 redirects")
	}
	return nil
}

2.3 Обработка заголовков запроса на перенаправление:makeHeadersCopier

Этот метод проходитЗакрытиеспособ завершения обработки упомянутого выше заголовка запроса:

  • Когда адрес перенаправления и исходный адрес имеют разные доменные имена и не являются поддоменами, заголовок запросаcookie, authorizationи другие чувствительные поля будут игнорироваться

  • перенаправления могут изменитьcookieзначение, так впередcookieЗаголовки запросов будут игнорироваться при любых измененияхcookie

func (c *Client) makeHeadersCopier(ireq *Request) func(*Request) {
	// 克隆一份header
	var (
		ireqhdr  = ireq.Header.Clone()
		icookies map[string][]*Cookie
	)
	// 再维护一个cookie哈希表
	if c.Jar != nil && ireq.Header.Get("Cookie") != "" {
		icookies = make(map[string][]*Cookie)
		for _, c := range ireq.Cookies() {
			icookies[c.Name] = append(icookies[c.Name], c)
		}
	}
	// The previous request
	preq := ireq 
	
	// 调用返回一个接受req的函数, 用于对拷贝header进行处理
	return func(req *Request) {
		if c.Jar != nil && icookies != nil {
			var changed bool
			resp := req.Response
			// 如果响应中的set-cookie操作设定的cookie名称存在于cookie哈希表中, 从哈希表中删除它
			for _, c := range resp.Cookies() {
				if _, ok := icookies[c.Name]; ok {
					delete(icookies, c.Name)
					changed = true
				}
			}
			// 忽略所有变化的cookie, 重新组装cookie请求头字段
			if changed {
				ireqhdr.Del("Cookie")
				var ss []string
				for _, cs := range icookies {
					for _, c := range cs {
						ss = append(ss, c.Name+"="+c.Value)
					}
				}
				sort.Strings(ss) // Ensure deterministic headers
				ireqhdr.Set("Cookie", strings.Join(ss, "; "))
			}
		}

		for k, vv := range ireqhdr {
		        // 对于非同域或子域, 敏感请求头的处理
			if shouldCopyHeaderOnRedirect(k, preq.URL, req.URL) {
				req.Header[k] = vv
			}
		}
		// Update previous Request with the current request
		preq = req 
	}
}

Кстати, для разных доменов или поддоменов обработка конфиденциальных заголовков запроса относительно проста и понятна.

func shouldCopyHeaderOnRedirect(headerKey string, initial, dest *url.URL) bool {
	switch CanonicalHeaderKey(headerKey) {
	case "Authorization", "Www-Authenticate", "Cookie", "Cookie2":
		ihost := canonicalAddr(initial)
		dhost := canonicalAddr(dest)
		return isDomainOrSubdomain(dhost, ihost)
	}
	// All other headers are copied:
	return true
}

func isDomainOrSubdomain(sub, parent string) bool {
	if sub == parent {
		return true
	}
	if !strings.HasSuffix(sub, parent) {
		return false
	}
	return sub[len(sub)-len(parent)-1] == '.'
}

2.4 http.Getза методом

можно найти в исходном коде,http.Getи т.д. методыDefaultClientслой после упаковки

var DefaultClient = &Client{}

func Get(url string) (resp *Response, err error) {
	return DefaultClient.Get(url)
}

func Post(url, contentType string, body io.Reader) (resp *Response, err error) {
	return DefaultClient.Post(url, contentType, body)
}

func PostForm(url string, data url.Values) (resp *Response, err error) {
	return DefaultClient.PostForm(url, data)
}

func Head(url string) (resp *Response, err error) {
	return DefaultClient.Head(url)
}

И эти методы в конечном итоге называютсяClient.DoСпособ, эту часть можно сказать очень аппетитный

func (c *Client) Get(url string) (resp *Response, err error) {
	req, err := NewRequest("GET", url, nil)
	if err != nil {
		return nil, err
	}
	return c.Do(req)
}

func (c *Client) Post(url, contentType string, body io.Reader) (resp *Response, err error) {
	req, err := NewRequest("POST", url, body)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", contentType)
	return c.Do(req)
}

func (c *Client) PostForm(url string, data url.Values) (resp *Response, err error) {
	return c.Post(url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
}

func (c *Client) Head(url string) (resp *Response, err error) {
	req, err := NewRequest("HEAD", url, nil)
	if err != nil {
		return nil, err
	}
	return c.Do(req)
}

2.5 Client.Doметод

func (c *Client) Do(req *Request) (*Response, error) {
	return c.do(req)
}

начать анализClient.doметод, этот метод имеет около 200 строк, чтобы избежать влияния на анализ процесса, часть кода обработки ошибок будет удалена, а упрощенный код выглядит следующим образом.

func (c *Client) do(req *Request) (retres *Response, reterr error) {
	var (
		deadline      = c.deadline()
		reqs          []*Request
		resp          *Response
		copyHeaders   = c.makeHeadersCopier(req)
		reqBodyClosed = false
		redirectMethod string
		includeBody    bool
	)
	for {
		if len(reqs) > 0 {
			loc := resp.Header.Get("Location")
			ireq := reqs[0]
			req = &Request{
				Method:   redirectMethod,
				Response: resp,
				URL:      u,
				Header:   make(Header),
				Host:     host,
				Cancel:   ireq.Cancel,
				ctx:      ireq.ctx,
			}
			if includeBody && ireq.GetBody != nil {
				req.Body, err = ireq.GetBody()
				req.ContentLength = ireq.ContentLength
			}

			copyHeaders(req)

			err = c.checkRedirect(req, reqs)
			if err == ErrUseLastResponse {
				return resp, nil
			}
			resp.Body.Close()
		}

		reqs = append(reqs, req)
		var err error
		var didTimeout func() bool
		resp, didTimeout, err = c.send(req, deadline)

		var shouldRedirect bool
		redirectMethod, shouldRedirect, includeBody = redirectBehavior(req.Method, resp, reqs[0])
		if !shouldRedirect {
			return resp, nil
		}

		req.closeBody()
	}
}
  1. копироватьreqвсе вheaders, возвращает функциюcopyHeaders, при перенаправлении некоторые поля заголовка запроса обрабатываются по правилам

  2. reqs = append(reqs, req), зарегистрируйте запрос наreqsв массиве первыйreqДолжен быть самый оригинальный запрос, последнийreqДолжен быть перенаправлен

  3. resp = c.send(req), Серьезно инициируйте запрос, получитеresp

  4. redirectBehavior(req.Method, resp, reqs[0]), по ответу определить требуется ли перенаправление, если нет, то процесс завершается, при необходимости продолжить вниз

  5. Входитьif len(reqs) > 0ветка, начать обработку редиректа

  6. отrespполучено вLocationполя в сочетании с исходным запросомreqs[0]Соберите новый запрос на перенаправление и назначьте егоreq

  7. copyHeaders(req), Сравнениеreqs[0]иreq, удалить определенные поля в соответствии с двумя правилами, упомянутыми выше

  8. c.checkRedirect(req, reqs)Определить, соблюдается ли политика перенаправления, если нет, вернуться к последнему ответу и не продолжать перенаправление

  9. Выполните шаги 1-4 еще раз

3. Резюме

Адрес источника

Несмотря на то, что это интерфейс, я впервые читаю исходный код Go, опыт все равно очень классный, 800 строк кода, 400 строк комментариев и не очень большой объем QAQ.

Обобщить опыт:

  1. Посмотрите исходник с вопросом, легко определить направление

  2. Проанализируйте основную ветку, удалите/пропустите некоторые боковые ветки и код обработки ошибок.

  3. DEBUG — самый быстрый способ определить процесс