링크 미리보기 HTTP GET이 사양을 위반합니다

아직 보고되지 않은 것으로 보이는 문제를 꽤 오래 겪고 있습니다. 움직이는 부품이 다소 많고 복잡한 구조라 죄송하지만, 간결하게 설명해 보겠습니다.

요약하자면, 메시지에 링크를 붙여넣으면 해당 URL에서 임베드 데이터를 찾기 위해 HTTP GET 요청을 보내는 Ruby Gem이, 일부 HTTP 프록시에서 사양에 따라 무효로 간주되는 HTTP 요청을 보내게 됩니다. 이로 인해 일부 경우 미리보기가 작동하지 않습니다:

조금 더 자세한 설명은 다음과 같습니다. 우리는 문서용으로 Gitbook.io라는 편리한 서비스를 사용하고 있습니다. Gitbook은 호스팅 솔루션이며, 사이트 내부 리다이렉트에 Cloudflare Workers를 사용합니다. 그들의 Cloudflare Workers 중 일부는 Node Fetch API를 사용하여 HTTP 요청을 프록시합니다. Node fetch 개발자들은 사양 준수를 매우 엄격하게 따르며, HTTP 본문이 있거나, 그 헤더가 0으로 설정되어 있더라도 Content-Length 헤더가 있는 모든 GET 요청을 거부합니다.

그리고 바로 그 일이 일어나고 있습니다. HTTP 요청을 보내는 Ruby Gem이

Content-Length: 0

요청 헤더를 보내며, 이는 node fetch 프록시를 매우 화나게 하여 원격 서버에서 요청이 거부되도록 만듭니다. HTTP 사양상 GET 요청에 본문을 포함하거나 단순히 content length 헤더만 있는 것이 유효한지에 대해 다양한 포럼에서 많은 논쟁이 있었습니다. 저는 이에 대해 이의를 제기하지 않지만, 이는 Node fetch 개발자들이 그러한 의미를 허용해 달라고 요청하는 모든 이슈를 닫는 것을 막지 못했습니다.

불행히도 저는 이 상황에서 끼어 있습니다.

  • Node Fetch 프로젝트는 이러한 HTTP 요청을 유효한 것으로 간주하기를 거부합니다.
  • Cloudflare 지원 팀은 제가 해당 Node 기반 Workers를 제어할 권한이 없기 때문에 도움을 줄 수 없다고 합니다.
  • Gitbook 지원 팀은 Fetch 개발자들의 의견에 동의하기 때문에(그리고 그들이 정말로 신경을 쓰는지도 모르겠지만) 도움을 줄 수 없다고 합니다.
  • 그리고 HTTPrb 라이브러리는 헤더를 제거하기를 거부합니다. 그 이유는 그것이 완벽하게 유효하다고 생각하기 때문입니다.

따라서 저는 여기에서 링크 미리보기에 사용되는 HTTP GET 요청을 제어하거나 변경하여, Node Fetch와 같이 매우 엄격한 라이브러리를 사용하는 프록시가 이러한 요청을 거부하지 않도록 허용 가능한 HTTP 헤더 세트를 포함할 수 있는 방법이 있는지 문의하게 되었습니다.

시도해 보고 싶으시다면, Gitbook 서버에 호스팅되어 있으며 Node Fetch 기반 Cloudflare Worker를 사용하는 예시 URL은 다음과 같습니다.

6개의 좋아요

@jamie.wilson / @techAPJ 요청 시 Content-Length가 0으로 전송되는 이유가 무엇인지 혹시 아시는 분 계신가요? 이 동작을 확인해 주실 수 있을까요? HEAD 요청에서는 그럴 수 있지만, GET 요청에서는 좀 이상한 것 같습니다.

2개의 좋아요

안녕하세요 @sam. HTTP 요청은 httprb라는 Ruby 라이브러리에 의해 이루어지는 것으로 보이며, 이 라이브러리는 해당 동작을 하고 있습니다. “HTTPrb 라이브러리는 헤더를 제거하는 것을 거부합니다. 왜냐하면 그것은 완전히 유효하다고 생각하기 때문입니다.” 항목의 링크를 확인해 보면, 해당 라이브러리의 개발자가 HTTP 스펙을 왜곡하면서도 위반하지 않는 이유에 대해 논리를 펼치고 있는 것을 볼 수 있습니다.

인터넷 이곳저곳을 뒤져서 누군가와 합의점을 찾으려 노력하던 중, httprb에 이 문제를 해결할 수 있는 풀 리퀘스트를 보내는 데 성공했습니다.

저는 Ruby 개발자가 아니기 때문에 이를 테스트하는 방법조차 모릅니다. 결국 이 Gem이 수정된 버전을 릴리스하고, 그 후 Discourse가 해당 버전을 사용하도록 업데이트할 것으로 추정합니다. 누군가 이것이 작동하는지 테스트할 수 있는 방법이 있다면 좋겠습니다. 재현 케이스는 매우 간단합니다 – 위에서 언급한 링크를 제 Gitbook URL에 붙여넣고 미리보기가 거부되는지 확인하면 됩니다.

1개의 좋아요

다음과 같은 내용을 확인했습니다(첫 번째 게시물에 있는 이미지와 일치합니다):

'Getting Started Guide’라는 텍스트는 요청이 성공적으로 처리되었음을 보여줍니다. 해당 문자열은 og:title 메타 태그에서 가져오고 있습니다:

<meta property="og:title" content="Getting Started Guide" data-react-helmet="true">

설명이 누락되었다는 오류/경고도 정확합니다. 페이지의 콘텐츠는 다음과 같습니다:

<meta property="og:description" content="" data-react-helmet="true">

이미지 URL은 og:image 태그에서 가져오며, 해당 태그는 다음과 같습니다:

<meta data-react-helmet="true" property="og:image" content="https://app.gitbook.com/share/space/thumbnail/-LA-UVvV3_TgzQyCXMWK.png">

https://app.gitbook.com/share/space/thumbnail/-LA-UVvV3_TgzQyCXMWK.png를 복사하여 브라우저(마카OS의 최신 Safari)에 붙여넣으면 다음 오류 메시지가 표시됩니다:

Error: could not handle the request

curl을 통해 동일한 요청을 수행해도 동일한 응답이 반환됩니다:

curl -v https://app.gitbook.com/share/space/thumbnail/-LA-UVvV3_TgzQyCXMWK.png
*   Trying 104.18.8.111...
* TCP_NODELAY set
* Connected to app.gitbook.com (104.18.8.111) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/cert.pem
  CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-ECDSA-CHACHA20-POLY1305
* ALPN, server accepted to use h2
* Server certificate:
*  subject: C=US; ST=California; L=San Francisco; O=Cloudflare, Inc.; CN=sni.cloudflaressl.com
*  start date: Jun 16 00:00:00 2021 GMT
*  expire date: Jun 15 23:59:59 2022 GMT
*  subjectAltName: host "app.gitbook.com" matched cert's "*.gitbook.com"
*  issuer: C=US; O=Cloudflare, Inc.; CN=Cloudflare Inc ECC CA-3
*  SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x142809200)
> GET /share/space/thumbnail/-LA-UVvV3_TgzQyCXMWK.png HTTP/2
> Host: app.gitbook.com
> User-Agent: curl/7.64.1
> Accept: */*
>
* Connection state changed (MAX_CONCURRENT_STREAMS == 256)!
< HTTP/2 500
< date: Mon, 23 Aug 2021 17:40:04 GMT
< content-type: text/plain; charset=utf-8
< content-length: 36
< cf-ray: 68361fb8ea9b4009-YYZ
< age: 432
< vary: Accept-Encoding
< via: magic cache
< cf-cache-status: HIT
< expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
< x-cache: HIT
< x-cloud-trace-context: 9d4cbd24a15138451c88b2ced35a32f1;o=1
< x-content-type-options: nosniff
< x-magic-hash: f46ac4bf6b6dc125a68e9ad566b48481631bb27eec2165532a7c0f538e93c4f6
< x-release: gitbook-28427-6.25.14
< server: cloudflare
<
Error: could not handle the request
* Connection #0 to host app.gitbook.com left intact
* Closing connection 0

og:image URL을 복사하여 브라우저에 붙여넣으면 이미지를 볼 수 있습니까?

요약하면: 원본 URL의 응답을 기준으로 볼 때, Onebox 미리보기는 예상대로 작동하는 것으로 보입니다.

3개의 좋아요

@jamie.wilson 이 문제를 조사해 주셔서 감사합니다. 그런데 위에서 언급하신 테스트가 해당 풀 리퀘스트를 포함하는 최신 버전의 httprb 젬으로 수행된 것인지, 아니면 라이브러리의 이전 버전으로 수행된 것인지 명확히 해 주실 수 있을까요?

원래 Onebox 미리보기에서 제가 보던 오류는 대상 URL이 500 상태 코드를 반환한다는 것이었습니다. 이 글을 올리기 전 어느 시점부터 Onebox 미리보기는 500 오류 대신 opengraph 메타데이터가 누락되었다는 알림을 표시하기 시작했습니다. Gitbook 지원팀에 이 글을 올리기 전 몇 달간 이 문제를 해결하려고 노력해 왔기 때문에, 그 사이에 무언가가 변경되었을 가능성이 있습니다.

만약 Gitbook URL이 실제로 로드되고 있고 단순히 일부 메타데이터나 이미지가 누락된 상태라면, 이는 요청이 거부되는 경우와 다릅니다. 다만, Content-Length: 0 HTTP 요청 헤더를 포함하여 제가 직접 보내는 모든 요청은 원격 서버의 CloudFlare 워커에 의해 거부된다는 점은 확실히 알고 있습니다. 어쩌면 Discourse에서 요청을 수행하는 데 사용되는 HTTP 클라이언트가 변경되었을까요? 저는 Discourse 소스 코드에 대해 아무것도 알지 못하며, httprb 라이브러리가 HTTP 요청의 실제 출처인지 100% 확신하지도 못합니다.

우리는 httprb gem을 전혀 사용하지 않는 것으로 보입니다. Oneboxing 프로세스(즉, 링크 미리보기를 생성하는 기능)는 ruby stdlib의 Net::HTTP와 플로우의 일부로 Excon gem을 사용합니다.

더 자세히 살펴보니, 때때로 Content-Length: 0 헤더를 가진 요청을 생성하는 것을 확인할 수 있습니다. 그러나 적어도 제공된 URL의 경우, 이는 Onebox 생성에 영향을 미치지 않습니다.

소수 버전이 업데이트되었을 수 있지만, 요청 방식을 재설계하거나 사용하는 라이브러리를 변경하는 것과 같은 큰 변화는 없었습니다.

Oneboxing을 전반적으로 더 견고하게 만들기 위한 몇 가지 변경 사항이 있었는데, 이는 이전에 500 오류를 반환하던 URL이 이제 Oneboxing에 성공하는 이유를 설명할 수 있습니다.

현재 Oneboxing 중 오류를 반환하거나(또는 Discourse의 다른 부분에서 의도대로 작동하지 않는) URL이 있으시면 자유롭게 저에게 보내주세요!

3개의 좋아요

아, 이 정보는 정말 유용하네요. 이 시점에서 어떤 라이브러리가 관련되어 있는지 막연하게 추측만 하고 있었는데, CloudFlare 프록시를 유지보수하는 Gitbook 팀에서 거의 도움을 받지 못했기 때문입니다.

알겠습니다. 위에서 공유하지 않았던 것 같은데, Gitbook에서 얻을 수 있었던 유일한 정보는 CloudFlare 오류 로그에서 Discourse의 미리보기 요청을 거부한 오류가 다음과 같다는 것이었습니다:

GET 또는 HEAD 메서드의 요청에는 바디(body)가 있을 수 없습니다.

불분명한 점은 Discourse에서发出的 GET 요청에 실제로 바디가 포함되어 있었는지, 아니면 단순히 Content-Length: 0 헤더만 있었는지입니다. 어느 쪽이든, 일부(그중에는 Cloudflare의 직원들도 포함)는 이것이 Fetch 사양을 위반한다고 주장합니다.

네, 어떤 시점에서 Onebox 오류가 일반적인 500 오류에서 이제 일부 데이터를 포함하는 것으로 변경된 것 같습니다. 어떤 라이브러리가 업데이트되었을지 알 수 없으며(이 기간 동안 Discourse를 업데이트했습니다). 저는 Discourse에서 어떤 헤더가 전송되는지 정확히 캡처할 방법이 없습니다. http://httpbin.org/get 같은 URL에 요청을 보내도, 결과가 Onebox에 의해 완전히 소비되고 제가 아는 한 어디에도 로그로 기록되지 않기 때문에 반환되는 내용을 "확인"할 방법이 없습니다.

빈 content-length 헤더가 이제 사라졌다면, 저는 적어도 Gitbook과 협력하여 그들의 임베드(embed) 문제를 수정할 수 있을 것입니다(현재 그들은 전체 앱을 처음부터 다시 작성하고 있으며 기존 버그에 대한 대응을 거부하고 있으므로 이는 일어나지 않을 것입니다 :confused: 하지만 적어도 이는 Discourse의 문제는 아닙니다).

먼저 말씀드리자면, 위에 작성된 내용 중 상당 부분이 제 수준을 훨씬 벗어나지만, 여기서 발버둥을 치고 있는 셈입니다. 제가 잘못된 주제에 끼어들고 있다면 제가 멍청하다고 편하게 말씀해 주셔도 괜찮습니다.

저희 커뮤니티에서는 헬프 센터(지식 베이스) 링크를 자주 게시하기 때문에 이 문제를 꽤 자주 겪고 있습니다.

원박싱이 실패하는 링크의 몇 가지 예시는 다음과 같습니다:

https://help.republicwireless.com/hc/en-us/articles/115014150828--How-to-Add-an-E911-Address

입력 중 미리보기 패널에서:

1개의 좋아요

더 깊이 살펴본 결과, Content-Length: 0을 추가하는 것은 Excon gem이었으나, GET 요청에는 적용되지 않았습니다.

그러나 해당 코드는 8~9년 동안 존재해 왔으므로, 이것이 문제의 원인은 아니었을 가능성이 높습니다.

Gemfile.lock 파일에서 코어 Discourse가 사용하는 gem들을 확인할 수 있습니다.

2개의 좋아요

이 사이트는 Cloudflare 캡차 뒤에 있어 Discourse가 정보를 가져오는 것을 차단합니다 :slightly_frowning_face:

2개의 좋아요

브라우저로 이 페이지를 열면 Onebox를 구성하는 데 필요한 메타 태그가 포함되어 있습니다. 그러나 해당 URL을 가져오려고 하면 오류가 발생하는 것 같습니다!

oneboxer preview url: https://help.republicwireless.com/hc/en-us/articles/115014150828--How-to-Add-an-E911-Address
headers: {"User-Agent"=>"Discourse Forum Onebox v2.8.0.beta4"}
helpers response code: 403

이는 "Discourse Forum Onebox v2.8.0.beta4"라는 User Agent로 해당 URL을 요청했지만, 원격 웹 서버가 403 상태 코드를 반환했다는 의미입니다.

마찬가지로, 커맨드 라인 도구 wget을 사용해도:

wget https://help.republicwireless.com/hc/en-us/articles/115014150828--How-to-Add-an-E911-Address
--2021-08-23 17:38:30--  https://help.republicwireless.com/hc/en-us/articles/115014150828--How-to-Add-an-E911-Address
Resolving help.republicwireless.com (help.republicwireless.com)... 104.16.53.111, 104.16.51.111
Connecting to help.republicwireless.com (help.republicwireless.com)|104.16.53.111|:443... connected.
HTTP request sent, awaiting response... 403 Forbidden

동일한 결과를 보여줍니다… 우리는 유효한 요청을 보내고 있지만, 원격 웹 서버가 결과를 반환하는 것을 거부하고 있습니다. help.republicwireless.com을 관리하는 담당자가 이러한 유효한 요청의 차단 해제(block 해제)가 가능한가요?

이 두 사이트에는 OpenGraph 제목/설명 태그가 없지만, Onebox가 폴백으로 사용할 수 있는 다른 제목/설명은 있습니다. 이는 수정해야 할 사항으로 검토해 보아야 합니다.

2개의 좋아요

:confused: 그런데 몇 년 동안 잘 작동하고 있었거든요.

같은 사이트의 링크가 유효한 Onebox를 제공하는 예시입니다: https://forums.republicwireless.com/t/4-digit-pin-which-i-have-forgot/37655/2

이 링크는 https://help.republicwireless.com/hc/en-us/articles/115012101188-Can-t-Get-Past-the-Screen-Lock-on-the-Phone 로 연결됩니다.

1개의 좋아요

Cloudflare는 로봇 탐지 알고리즘을 지속적으로 변경하고 있으므로, Discourse가 차단되지 않도록 하려면 Cloudflare 지원팀에 문의하여 해당 요청이 차단되는 이유를 문의하는 것이 좋습니다.

5개의 좋아요

죄송합니다, 이 이슈를 오래 방치되어 폐쇄합니다. 문제를 재현할 수 있는 새로운 보고가 필요합니다.