request_builder_test.go 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912
  1. // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
  2. // SPDX-License-Identifier: Apache-2.0
  3. package fetcher // import "miniflux.app/v2/internal/reader/fetcher"
  4. import (
  5. "net/http"
  6. "net/http/httptest"
  7. "net/url"
  8. "strings"
  9. "testing"
  10. "time"
  11. "miniflux.app/v2/internal/config"
  12. "miniflux.app/v2/internal/proxyrotator"
  13. )
  14. func TestNewRequestBuilder(t *testing.T) {
  15. builder := NewRequestBuilder()
  16. if builder == nil {
  17. t.Fatal("NewRequestBuilder should not return nil")
  18. }
  19. if builder.clientTimeout != defaultHTTPClientTimeout {
  20. t.Errorf("Expected default timeout %d, got %d", defaultHTTPClientTimeout, builder.clientTimeout)
  21. }
  22. if builder.headers == nil {
  23. t.Fatal("Headers should be initialized")
  24. }
  25. }
  26. func TestRequestBuilder_WithHeader(t *testing.T) {
  27. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  28. if r.Header.Get("Custom-Header") != "custom-value" {
  29. t.Errorf("Expected Custom-Header to be 'custom-value', got '%s'", r.Header.Get("Custom-Header"))
  30. }
  31. w.WriteHeader(http.StatusOK)
  32. }))
  33. defer server.Close()
  34. builder := NewRequestBuilder()
  35. resp, err := builder.WithHeader("Custom-Header", "custom-value").ExecuteRequest(server.URL)
  36. if err != nil {
  37. t.Fatalf("Expected no error, got %v", err)
  38. }
  39. defer resp.Body.Close()
  40. }
  41. func TestRequestBuilder_WithETag(t *testing.T) {
  42. tests := []struct {
  43. name string
  44. etag string
  45. expected string
  46. }{
  47. {"with etag", "test-etag", "test-etag"},
  48. {"empty etag", "", ""},
  49. }
  50. for _, tt := range tests {
  51. t.Run(tt.name, func(t *testing.T) {
  52. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  53. if r.Header.Get("If-None-Match") != tt.expected {
  54. t.Errorf("Expected If-None-Match to be '%s', got '%s'", tt.expected, r.Header.Get("If-None-Match"))
  55. }
  56. w.WriteHeader(http.StatusOK)
  57. }))
  58. defer server.Close()
  59. builder := NewRequestBuilder()
  60. resp, err := builder.WithETag(tt.etag).ExecuteRequest(server.URL)
  61. if err != nil {
  62. t.Fatalf("Expected no error, got %v", err)
  63. }
  64. defer resp.Body.Close()
  65. })
  66. }
  67. }
  68. func TestRequestBuilder_WithLastModified(t *testing.T) {
  69. tests := []struct {
  70. name string
  71. lastModified string
  72. expected string
  73. }{
  74. {"with last modified", "Mon, 02 Jan 2006 15:04:05 GMT", "Mon, 02 Jan 2006 15:04:05 GMT"},
  75. {"empty last modified", "", ""},
  76. }
  77. for _, tt := range tests {
  78. t.Run(tt.name, func(t *testing.T) {
  79. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  80. if r.Header.Get("If-Modified-Since") != tt.expected {
  81. t.Errorf("Expected If-Modified-Since to be '%s', got '%s'", tt.expected, r.Header.Get("If-Modified-Since"))
  82. }
  83. w.WriteHeader(http.StatusOK)
  84. }))
  85. defer server.Close()
  86. builder := NewRequestBuilder()
  87. resp, err := builder.WithLastModified(tt.lastModified).ExecuteRequest(server.URL)
  88. if err != nil {
  89. t.Fatalf("Expected no error, got %v", err)
  90. }
  91. defer resp.Body.Close()
  92. })
  93. }
  94. }
  95. func TestRequestBuilder_WithUserAgent(t *testing.T) {
  96. tests := []struct {
  97. name string
  98. userAgent string
  99. defaultAgent string
  100. expectedHeader string
  101. }{
  102. {"custom user agent", "CustomAgent/1.0", "DefaultAgent/1.0", "CustomAgent/1.0"},
  103. {"default user agent", "", "DefaultAgent/1.0", "DefaultAgent/1.0"},
  104. }
  105. for _, tt := range tests {
  106. t.Run(tt.name, func(t *testing.T) {
  107. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  108. if r.Header.Get("User-Agent") != tt.expectedHeader {
  109. t.Errorf("Expected User-Agent to be '%s', got '%s'", tt.expectedHeader, r.Header.Get("User-Agent"))
  110. }
  111. w.WriteHeader(http.StatusOK)
  112. }))
  113. defer server.Close()
  114. builder := NewRequestBuilder()
  115. resp, err := builder.WithUserAgent(tt.userAgent, tt.defaultAgent).ExecuteRequest(server.URL)
  116. if err != nil {
  117. t.Fatalf("Expected no error, got %v", err)
  118. }
  119. defer resp.Body.Close()
  120. })
  121. }
  122. }
  123. func TestRequestBuilder_WithCookie(t *testing.T) {
  124. tests := []struct {
  125. name string
  126. cookie string
  127. expected string
  128. }{
  129. {"with cookie", "session=abc123; lang=en", "session=abc123; lang=en"},
  130. {"empty cookie", "", ""},
  131. }
  132. for _, tt := range tests {
  133. t.Run(tt.name, func(t *testing.T) {
  134. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  135. if r.Header.Get("Cookie") != tt.expected {
  136. t.Errorf("Expected Cookie to be '%s', got '%s'", tt.expected, r.Header.Get("Cookie"))
  137. }
  138. w.WriteHeader(http.StatusOK)
  139. }))
  140. defer server.Close()
  141. builder := NewRequestBuilder()
  142. resp, err := builder.WithCookie(tt.cookie, server.URL).ExecuteRequest(server.URL)
  143. if err != nil {
  144. t.Fatalf("Expected no error, got %v", err)
  145. }
  146. defer resp.Body.Close()
  147. })
  148. }
  149. }
  150. func TestRequestBuilder_WithCookieOnlySentToSameSite(t *testing.T) {
  151. // The proxy receives the requests sent to any host, so no DNS is needed.
  152. receivedCookies := make(chan string, 1)
  153. proxyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  154. select {
  155. case receivedCookies <- r.Header.Get("Cookie"):
  156. default:
  157. }
  158. w.WriteHeader(http.StatusOK)
  159. }))
  160. defer proxyServer.Close()
  161. proxyURL, err := url.Parse(proxyServer.URL)
  162. if err != nil {
  163. t.Fatalf("Unable to parse proxy URL: %v", err)
  164. }
  165. // The same builder is used for all requests, like when scraping the entries of a feed.
  166. builder := NewRequestBuilder().
  167. WithCustomApplicationProxyURL(proxyURL).
  168. UseCustomApplicationProxyURL(true).
  169. WithCookie("session=secret", "https://feeds.example.org/rss.xml")
  170. tests := []struct {
  171. requestURL string
  172. expected string
  173. }{
  174. {"http://feeds.example.org/rss.xml", "session=secret"},
  175. {"http://www.example.org/article.html", "session=secret"},
  176. {"http://example.org/favicon.ico", "session=secret"},
  177. {"http://example.net/article.html", ""},
  178. {"http://feeds.example.org.example.net/rss.xml", ""},
  179. {"http://cdn.example.com/icon.png", ""},
  180. }
  181. for _, tt := range tests {
  182. resp, err := builder.ExecuteRequest(tt.requestURL)
  183. if err != nil {
  184. t.Fatalf("Expected no error for %q, got %v", tt.requestURL, err)
  185. }
  186. resp.Body.Close()
  187. select {
  188. case cookie := <-receivedCookies:
  189. if cookie != tt.expected {
  190. t.Errorf("Expected Cookie for %q to be %q, got %q", tt.requestURL, tt.expected, cookie)
  191. }
  192. default:
  193. t.Fatalf("Expected request to %q to be sent through the proxy", tt.requestURL)
  194. }
  195. }
  196. }
  197. func TestRequestBuilder_WithCookieNotForwardedOnRedirectToAnotherSite(t *testing.T) {
  198. type receivedRequest struct {
  199. host string
  200. cookie string
  201. }
  202. receivedRequests := make(chan receivedRequest, 2)
  203. proxyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  204. select {
  205. case receivedRequests <- receivedRequest{r.Host, r.Header.Get("Cookie")}:
  206. default:
  207. }
  208. if r.Host == "www.example.org" {
  209. http.Redirect(w, r, "http://example.net/article.html", http.StatusFound)
  210. return
  211. }
  212. w.WriteHeader(http.StatusOK)
  213. }))
  214. defer proxyServer.Close()
  215. proxyURL, err := url.Parse(proxyServer.URL)
  216. if err != nil {
  217. t.Fatalf("Unable to parse proxy URL: %v", err)
  218. }
  219. builder := NewRequestBuilder().
  220. WithCustomApplicationProxyURL(proxyURL).
  221. UseCustomApplicationProxyURL(true).
  222. WithCookie("session=secret", "https://feeds.example.org/rss.xml")
  223. resp, err := builder.ExecuteRequest("http://www.example.org/article.html")
  224. if err != nil {
  225. t.Fatalf("Expected no error, got %v", err)
  226. }
  227. resp.Body.Close()
  228. if len(receivedRequests) != 2 {
  229. t.Fatalf("Expected the redirect to be followed, got %d requests", len(receivedRequests))
  230. }
  231. if request := <-receivedRequests; request.cookie != "session=secret" {
  232. t.Errorf("Expected Cookie for %q to be %q, got %q", request.host, "session=secret", request.cookie)
  233. }
  234. if request := <-receivedRequests; request.host != "example.net" || request.cookie != "" {
  235. t.Errorf("Expected no Cookie to be forwarded to example.net, got %q for %q", request.cookie, request.host)
  236. }
  237. }
  238. func TestIsSameSite(t *testing.T) {
  239. tests := []struct {
  240. host string
  241. siteHost string
  242. expected bool
  243. }{
  244. {"example.org", "example.org", true},
  245. {"www.example.org", "example.org", true},
  246. {"example.org", "www.example.org", true},
  247. {"www.example.org", "feeds.example.org", true},
  248. {"WWW.Example.ORG", "feeds.example.org", true},
  249. {"news.example.co.uk", "www.example.co.uk", true},
  250. {"xn--bcher-kva.example", "www.bücher.example", true},
  251. {"example.net", "example.org", false},
  252. {"notexample.org", "example.org", false},
  253. {"example.org.example.net", "example.org", false},
  254. {"attacker.co.uk", "example.co.uk", false},
  255. {"attacker.CO.UK", "www.example.CO.UK", false},
  256. {"bob.github.io", "alice.github.io", false},
  257. {"127.0.0.1", "127.0.0.1", true},
  258. {"10.0.0.1", "192.168.0.1", false},
  259. {"::1", "::1", true},
  260. {"localhost", "localhost", true},
  261. {"intranet", "localhost", false},
  262. {"", "example.org", false},
  263. {"example.org", "", false},
  264. {"", "", false},
  265. }
  266. for _, tt := range tests {
  267. if got := isSameSite(tt.host, tt.siteHost); got != tt.expected {
  268. t.Errorf("isSameSite(%q, %q) = %v, expected %v", tt.host, tt.siteHost, got, tt.expected)
  269. }
  270. }
  271. }
  272. func TestRequestBuilder_WithUsernameAndPassword(t *testing.T) {
  273. tests := []struct {
  274. name string
  275. username string
  276. password string
  277. expected string
  278. }{
  279. {"with credentials", "test", "password", "Basic dGVzdDpwYXNzd29yZA=="}, // base64 of "test:password"
  280. {"empty username", "", "password", ""},
  281. {"empty password", "test", "", ""},
  282. {"both empty", "", "", ""},
  283. }
  284. for _, tt := range tests {
  285. t.Run(tt.name, func(t *testing.T) {
  286. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  287. if r.Header.Get("Authorization") != tt.expected {
  288. t.Errorf("Expected Authorization to be '%s', got '%s'", tt.expected, r.Header.Get("Authorization"))
  289. }
  290. w.WriteHeader(http.StatusOK)
  291. }))
  292. defer server.Close()
  293. builder := NewRequestBuilder()
  294. resp, err := builder.WithUsernameAndPassword(tt.username, tt.password).ExecuteRequest(server.URL)
  295. if err != nil {
  296. t.Fatalf("Expected no error, got %v", err)
  297. }
  298. defer resp.Body.Close()
  299. })
  300. }
  301. }
  302. func TestRequestBuilder_DefaultAcceptHeader(t *testing.T) {
  303. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  304. if r.Header.Get("Accept") != defaultAcceptHeader {
  305. t.Errorf("Expected Accept to be '%s', got '%s'", defaultAcceptHeader, r.Header.Get("Accept"))
  306. }
  307. w.WriteHeader(http.StatusOK)
  308. }))
  309. defer server.Close()
  310. builder := NewRequestBuilder()
  311. resp, err := builder.ExecuteRequest(server.URL)
  312. if err != nil {
  313. t.Fatalf("Expected no error, got %v", err)
  314. }
  315. defer resp.Body.Close()
  316. }
  317. func TestRequestBuilder_CustomAcceptHeaderNotOverridden(t *testing.T) {
  318. customAccept := "application/json"
  319. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  320. if r.Header.Get("Accept") != customAccept {
  321. t.Errorf("Expected Accept to be '%s', got '%s'", customAccept, r.Header.Get("Accept"))
  322. }
  323. w.WriteHeader(http.StatusOK)
  324. }))
  325. defer server.Close()
  326. builder := NewRequestBuilder()
  327. resp, err := builder.WithHeader("Accept", customAccept).ExecuteRequest(server.URL)
  328. if err != nil {
  329. t.Fatalf("Expected no error, got %v", err)
  330. }
  331. defer resp.Body.Close()
  332. }
  333. func TestRequestBuilder_WithTimeout(t *testing.T) {
  334. builder := NewRequestBuilder()
  335. builder = builder.WithTimeout(30 * time.Second)
  336. if builder.clientTimeout != 30*time.Second {
  337. t.Errorf("Expected timeout to be 30, got %d", builder.clientTimeout)
  338. }
  339. }
  340. func TestRequestBuilder_WithoutRedirects(t *testing.T) {
  341. // Create a redirect server
  342. redirectServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  343. w.WriteHeader(http.StatusOK)
  344. }))
  345. defer redirectServer.Close()
  346. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  347. http.Redirect(w, r, redirectServer.URL, http.StatusFound)
  348. }))
  349. defer server.Close()
  350. builder := NewRequestBuilder()
  351. resp, err := builder.WithoutRedirects().ExecuteRequest(server.URL)
  352. if err != nil {
  353. t.Fatalf("Expected no error, got %v", err)
  354. }
  355. defer resp.Body.Close()
  356. if resp.StatusCode != http.StatusFound {
  357. t.Errorf("Expected status code %d, got %d", http.StatusFound, resp.StatusCode)
  358. }
  359. }
  360. func TestRequestBuilder_Clone(t *testing.T) {
  361. original := NewRequestBuilder().WithHeader("X-Shared", "value")
  362. clone := original.Clone().WithoutRedirects()
  363. clone.WithHeader("X-Clone-Only", "value")
  364. if original.withoutRedirects {
  365. t.Error("Mutating the clone should not disable redirects on the original")
  366. }
  367. if original.headers.Get("X-Clone-Only") != "" {
  368. t.Error("Mutating the clone's headers should not affect the original")
  369. }
  370. if clone.headers.Get("X-Shared") != "value" {
  371. t.Error("Expected the clone to inherit the original headers")
  372. }
  373. if clone.clientTimeout != original.clientTimeout {
  374. t.Error("Expected the clone to inherit the original timeout")
  375. }
  376. }
  377. func TestRequestBuilder_DisableHTTP2(t *testing.T) {
  378. builder := NewRequestBuilder()
  379. builder = builder.DisableHTTP2(true)
  380. if !builder.disableHTTP2 {
  381. t.Error("Expected disableHTTP2 to be true")
  382. }
  383. }
  384. func TestRequestBuilder_IgnoreTLSErrors(t *testing.T) {
  385. builder := NewRequestBuilder()
  386. builder = builder.IgnoreTLSErrors(true)
  387. if !builder.ignoreTLSErrors {
  388. t.Error("Expected ignoreTLSErrors to be true")
  389. }
  390. }
  391. func TestRequestBuilder_WithoutCompression(t *testing.T) {
  392. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  393. if r.Header.Get("Accept-Encoding") != "identity" {
  394. t.Errorf("Expected Accept-Encoding to be 'identity', got '%s'", r.Header.Get("Accept-Encoding"))
  395. }
  396. w.WriteHeader(http.StatusOK)
  397. }))
  398. defer server.Close()
  399. builder := NewRequestBuilder()
  400. resp, err := builder.WithoutCompression().ExecuteRequest(server.URL)
  401. if err != nil {
  402. t.Fatalf("Expected no error, got %v", err)
  403. }
  404. defer resp.Body.Close()
  405. }
  406. func TestRequestBuilder_WithCompression(t *testing.T) {
  407. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  408. if r.Header.Get("Accept-Encoding") != "br,gzip" {
  409. t.Errorf("Expected Accept-Encoding to be 'br,gzip', got '%s'", r.Header.Get("Accept-Encoding"))
  410. }
  411. w.WriteHeader(http.StatusOK)
  412. }))
  413. defer server.Close()
  414. builder := NewRequestBuilder()
  415. resp, err := builder.ExecuteRequest(server.URL)
  416. if err != nil {
  417. t.Fatalf("Expected no error, got %v", err)
  418. }
  419. defer resp.Body.Close()
  420. }
  421. func TestRequestBuilder_ConnectionCloseHeader(t *testing.T) {
  422. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  423. if r.Header.Get("Connection") != "close" {
  424. t.Errorf("Expected Connection to be 'close', got '%s'", r.Header.Get("Connection"))
  425. }
  426. w.WriteHeader(http.StatusOK)
  427. }))
  428. defer server.Close()
  429. builder := NewRequestBuilder()
  430. resp, err := builder.ExecuteRequest(server.URL)
  431. if err != nil {
  432. t.Fatalf("Expected no error, got %v", err)
  433. }
  434. defer resp.Body.Close()
  435. }
  436. func TestRequestBuilder_WithCustomApplicationProxyURL(t *testing.T) {
  437. proxyURL, _ := url.Parse("http://proxy.example.com:8080")
  438. builder := NewRequestBuilder()
  439. builder = builder.WithCustomApplicationProxyURL(proxyURL)
  440. if builder.clientProxyURL != proxyURL {
  441. t.Error("Expected clientProxyURL to be set")
  442. }
  443. }
  444. func TestRequestBuilder_UseCustomApplicationProxyURL(t *testing.T) {
  445. builder := NewRequestBuilder()
  446. builder = builder.UseCustomApplicationProxyURL(true)
  447. if !builder.useClientProxy {
  448. t.Error("Expected useClientProxy to be true")
  449. }
  450. }
  451. func TestRequestBuilder_WithCustomFeedProxyURL(t *testing.T) {
  452. proxyURL := "http://feed-proxy.example.com:8080"
  453. builder := NewRequestBuilder()
  454. builder = builder.WithCustomFeedProxyURL(proxyURL)
  455. if builder.feedProxyURL != proxyURL {
  456. t.Errorf("Expected feedProxyURL to be '%s', got '%s'", proxyURL, builder.feedProxyURL)
  457. }
  458. }
  459. func TestRequestBuilder_ChainedMethods(t *testing.T) {
  460. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  461. // Check multiple headers
  462. if r.Header.Get("User-Agent") != "TestAgent/1.0" {
  463. t.Errorf("Expected User-Agent to be 'TestAgent/1.0', got '%s'", r.Header.Get("User-Agent"))
  464. }
  465. if r.Header.Get("Cookie") != "test=value" {
  466. t.Errorf("Expected Cookie to be 'test=value', got '%s'", r.Header.Get("Cookie"))
  467. }
  468. if r.Header.Get("If-None-Match") != "etag123" {
  469. t.Errorf("Expected If-None-Match to be 'etag123', got '%s'", r.Header.Get("If-None-Match"))
  470. }
  471. w.WriteHeader(http.StatusOK)
  472. }))
  473. defer server.Close()
  474. builder := NewRequestBuilder()
  475. resp, err := builder.
  476. WithUserAgent("TestAgent/1.0", "DefaultAgent/1.0").
  477. WithCookie("test=value", server.URL).
  478. WithETag("etag123").
  479. WithTimeout(10 * time.Second).
  480. ExecuteRequest(server.URL)
  481. if err != nil {
  482. t.Fatalf("Expected no error, got %v", err)
  483. }
  484. defer resp.Body.Close()
  485. }
  486. func TestRequestBuilder_InvalidURL(t *testing.T) {
  487. builder := NewRequestBuilder()
  488. _, err := builder.ExecuteRequest("invalid-url")
  489. if err == nil {
  490. t.Error("Expected error for invalid URL")
  491. }
  492. }
  493. func TestRequestBuilder_RefusePrivateNetworkByDefault(t *testing.T) {
  494. configureFetcherAllowPrivateNetworksOption(t, "0")
  495. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  496. w.WriteHeader(http.StatusOK)
  497. }))
  498. defer server.Close()
  499. builder := NewRequestBuilder()
  500. _, err := builder.ExecuteRequest(server.URL)
  501. if err == nil {
  502. t.Fatal("Expected private network request to be rejected")
  503. }
  504. if !strings.Contains(err.Error(), "refusing to access private network host") {
  505. t.Fatalf("Unexpected error for private network request: %v", err)
  506. }
  507. }
  508. func TestRequestBuilder_AllowPrivateNetworkWhenEnabled(t *testing.T) {
  509. configureFetcherAllowPrivateNetworksOption(t, "1")
  510. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  511. w.WriteHeader(http.StatusOK)
  512. }))
  513. defer server.Close()
  514. builder := NewRequestBuilder()
  515. resp, err := builder.ExecuteRequest(server.URL)
  516. if err != nil {
  517. t.Fatalf("Expected private network request to succeed when enabled: %v", err)
  518. }
  519. defer resp.Body.Close()
  520. }
  521. func TestRequestBuilder_AllowPrivateConfiguredProxy(t *testing.T) {
  522. configureFetcherAllowPrivateNetworksOption(t, "0")
  523. tests := []struct {
  524. name string
  525. configure func(t *testing.T, builder *RequestBuilder, proxyURL string) *RequestBuilder
  526. }{
  527. {
  528. name: "application proxy",
  529. configure: func(t *testing.T, builder *RequestBuilder, proxyURL string) *RequestBuilder {
  530. t.Helper()
  531. parsedProxyURL, err := url.Parse(proxyURL)
  532. if err != nil {
  533. t.Fatalf("Unable to parse proxy URL: %v", err)
  534. }
  535. return builder.WithCustomApplicationProxyURL(parsedProxyURL).UseCustomApplicationProxyURL(true)
  536. },
  537. },
  538. {
  539. name: "proxy rotator",
  540. configure: func(t *testing.T, builder *RequestBuilder, proxyURL string) *RequestBuilder {
  541. t.Helper()
  542. rotator, err := proxyrotator.NewProxyRotator([]string{proxyURL})
  543. if err != nil {
  544. t.Fatalf("Unable to create proxy rotator: %v", err)
  545. }
  546. return builder.WithProxyRotator(rotator)
  547. },
  548. },
  549. }
  550. for _, tt := range tests {
  551. t.Run(tt.name, func(t *testing.T) {
  552. targetURL := "http://feed.invalid/rss.xml"
  553. proxyRequests := make(chan string, 1)
  554. proxyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  555. select {
  556. case proxyRequests <- r.URL.String():
  557. default:
  558. }
  559. w.WriteHeader(http.StatusOK)
  560. }))
  561. defer proxyServer.Close()
  562. builder := tt.configure(t, NewRequestBuilder(), proxyServer.URL)
  563. resp, err := builder.ExecuteRequest(targetURL)
  564. if err != nil {
  565. t.Fatalf("Expected private proxy request to succeed: %v", err)
  566. }
  567. defer resp.Body.Close()
  568. select {
  569. case gotURL := <-proxyRequests:
  570. if gotURL != targetURL {
  571. t.Fatalf("Expected proxy request URL to be %q, got %q", targetURL, gotURL)
  572. }
  573. default:
  574. t.Fatal("Expected request to be sent through the proxy")
  575. }
  576. })
  577. }
  578. }
  579. func TestRequestBuilder_RefusePrivateFeedProxyByDefault(t *testing.T) {
  580. configureFetcherPrivateNetworkOptions(t, "0", "0")
  581. proxyRequests := make(chan string, 1)
  582. proxyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  583. select {
  584. case proxyRequests <- r.URL.String():
  585. default:
  586. }
  587. w.WriteHeader(http.StatusOK)
  588. }))
  589. defer proxyServer.Close()
  590. _, err := NewRequestBuilder().
  591. WithCustomFeedProxyURL(proxyServer.URL).
  592. ExecuteRequest("http://feed.invalid/rss.xml")
  593. if err == nil {
  594. t.Fatal("Expected private feed proxy request to be rejected")
  595. }
  596. if !strings.Contains(err.Error(), "refusing to access private network host") {
  597. t.Fatalf("Unexpected error for private feed proxy request: %v", err)
  598. }
  599. select {
  600. case gotURL := <-proxyRequests:
  601. t.Fatalf("Expected private feed proxy to receive no request, got %q", gotURL)
  602. default:
  603. }
  604. }
  605. func TestRequestBuilder_AllowPrivateFeedProxyWhenEnabled(t *testing.T) {
  606. configureFetcherPrivateNetworkOptions(t, "0", "1")
  607. targetURL := "http://feed.invalid/rss.xml"
  608. proxyRequests := make(chan string, 1)
  609. proxyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  610. select {
  611. case proxyRequests <- r.URL.String():
  612. default:
  613. }
  614. w.WriteHeader(http.StatusOK)
  615. }))
  616. defer proxyServer.Close()
  617. resp, err := NewRequestBuilder().
  618. WithCustomFeedProxyURL(proxyServer.URL).
  619. ExecuteRequest(targetURL)
  620. if err != nil {
  621. t.Fatalf("Expected private feed proxy request to succeed: %v", err)
  622. }
  623. defer resp.Body.Close()
  624. select {
  625. case gotURL := <-proxyRequests:
  626. if gotURL != targetURL {
  627. t.Fatalf("Expected proxy request URL to be %q, got %q", targetURL, gotURL)
  628. }
  629. default:
  630. t.Fatal("Expected request to be sent through the private feed proxy")
  631. }
  632. }
  633. func TestRequestBuilder_AllowPrivateEnvironmentProxy(t *testing.T) {
  634. configureFetcherAllowPrivateNetworksOption(t, "0")
  635. targetURL := "http://feed.invalid/rss.xml"
  636. proxyRequests := make(chan string, 1)
  637. proxyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  638. select {
  639. case proxyRequests <- r.URL.String():
  640. default:
  641. }
  642. w.WriteHeader(http.StatusOK)
  643. }))
  644. defer proxyServer.Close()
  645. t.Setenv("HTTP_PROXY", proxyServer.URL)
  646. resp, err := NewRequestBuilder().ExecuteRequest(targetURL)
  647. if err != nil {
  648. t.Fatalf("Expected request through the environment proxy to succeed: %v", err)
  649. }
  650. defer resp.Body.Close()
  651. select {
  652. case gotURL := <-proxyRequests:
  653. if gotURL != targetURL {
  654. t.Fatalf("Expected proxy request URL to be %q, got %q", targetURL, gotURL)
  655. }
  656. default:
  657. t.Fatal("Expected request to be sent through the environment proxy")
  658. }
  659. }
  660. func TestRequestBuilder_RefusePrivateNetworkWhenExcludedFromEnvironmentProxy(t *testing.T) {
  661. configureFetcherAllowPrivateNetworksOption(t, "0")
  662. privateServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  663. w.WriteHeader(http.StatusOK)
  664. }))
  665. defer privateServer.Close()
  666. proxyRequests := make(chan string, 1)
  667. proxyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  668. select {
  669. case proxyRequests <- r.URL.String():
  670. default:
  671. }
  672. w.WriteHeader(http.StatusOK)
  673. }))
  674. defer proxyServer.Close()
  675. t.Setenv("HTTP_PROXY", proxyServer.URL)
  676. t.Setenv("NO_PROXY", "feed.invalid")
  677. _, err := NewRequestBuilder().ExecuteRequest("http://feed.invalid/rss.xml")
  678. if err == nil {
  679. t.Fatal("Expected request excluded from the environment proxy to fail")
  680. }
  681. select {
  682. case gotURL := <-proxyRequests:
  683. t.Fatalf("Expected request to bypass the environment proxy, but the proxy received %q", gotURL)
  684. default:
  685. }
  686. }
  687. func TestRequestBuilder_AllowPrivateEnvironmentProxyOnRedirect(t *testing.T) {
  688. configureFetcherAllowPrivateNetworksOption(t, "0")
  689. connectRequests := make(chan string, 1)
  690. httpsProxyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  691. select {
  692. case connectRequests <- r.Host:
  693. default:
  694. }
  695. http.Error(w, "tunneling is not supported by this test proxy", http.StatusBadGateway)
  696. }))
  697. defer httpsProxyServer.Close()
  698. httpProxyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  699. http.Redirect(w, r, "https://secure.invalid/rss.xml", http.StatusFound)
  700. }))
  701. defer httpProxyServer.Close()
  702. t.Setenv("HTTP_PROXY", httpProxyServer.URL)
  703. t.Setenv("HTTPS_PROXY", httpsProxyServer.URL)
  704. if _, err := NewRequestBuilder().ExecuteRequest("http://feed.invalid/rss.xml"); err != nil {
  705. if strings.Contains(err.Error(), "refusing to access private network host") {
  706. t.Fatalf("Expected the proxy selected after the redirect to be reachable: %v", err)
  707. }
  708. }
  709. select {
  710. case gotHost := <-connectRequests:
  711. if gotHost != "secure.invalid:443" {
  712. t.Fatalf("Expected the HTTPS proxy to receive a CONNECT request for %q, got %q", "secure.invalid:443", gotHost)
  713. }
  714. default:
  715. t.Fatal("Expected the redirect to be sent through the HTTPS environment proxy")
  716. }
  717. }
  718. func TestRequestBuilder_RefusePrivateNetworkOnRedirect(t *testing.T) {
  719. configureFetcherAllowPrivateNetworksOption(t, "0")
  720. // Target server on a loopback address (private).
  721. privateServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  722. w.WriteHeader(http.StatusOK)
  723. }))
  724. defer privateServer.Close()
  725. // Redirector that sends the client to the private server.
  726. // Because the Control callback checks the IP at connection time, the
  727. // redirect target is also validated (unlike a pre-flight DNS check).
  728. redirectServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  729. http.Redirect(w, r, privateServer.URL, http.StatusFound)
  730. }))
  731. defer redirectServer.Close()
  732. builder := NewRequestBuilder()
  733. _, err := builder.ExecuteRequest(redirectServer.URL)
  734. if err == nil {
  735. t.Fatal("Expected redirect to private network to be rejected")
  736. }
  737. if !strings.Contains(err.Error(), "refusing to access private network host") {
  738. t.Fatalf("Unexpected error for redirected private network request: %v", err)
  739. }
  740. }
  741. func TestRequestBuilder_TimeoutConfiguration(t *testing.T) {
  742. // Create a slow server that blocks until the client disconnects, so
  743. // server.Close() does not have to wait for a fixed sleep to elapse.
  744. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  745. <-r.Context().Done()
  746. }))
  747. defer server.Close()
  748. builder := NewRequestBuilder()
  749. start := time.Now()
  750. _, err := builder.WithTimeout(100 * time.Millisecond).ExecuteRequest(server.URL)
  751. duration := time.Since(start)
  752. if err == nil {
  753. t.Error("Expected timeout error")
  754. }
  755. // Should timeout around 100ms, allow some margin
  756. if duration > 500*time.Millisecond {
  757. t.Errorf("Expected timeout around 100ms, took %v", duration)
  758. }
  759. }
  760. func configureFetcherAllowPrivateNetworksOption(t *testing.T, value string) {
  761. configureFetcherPrivateNetworkOptions(t, value, "0")
  762. }
  763. func configureFetcherPrivateNetworkOptions(t *testing.T, privateNetworks, privateFeedProxy string) {
  764. t.Helper()
  765. t.Setenv("FETCHER_ALLOW_PRIVATE_NETWORKS", privateNetworks)
  766. t.Setenv("FETCHER_ALLOW_PRIVATE_FEED_PROXY", privateFeedProxy)
  767. configParser := config.NewConfigParser()
  768. parsedOptions, err := configParser.ParseEnvironmentVariables()
  769. if err != nil {
  770. t.Fatalf("Unable to configure test options: %v", err)
  771. }
  772. previousOptions := config.Opts
  773. config.Opts = parsedOptions
  774. t.Cleanup(func() {
  775. config.Opts = previousOptions
  776. })
  777. }