Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F33103949
reverse_proxy_test.exs
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
13 KB
Referenced Files
None
Subscribers
None
reverse_proxy_test.exs
View Options
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule
Pleroma.ReverseProxyTest
do
use
Pleroma.Web.ConnCase
import
ExUnit.CaptureLog
import
Mox
alias
Pleroma.ReverseProxy
alias
Pleroma.ReverseProxy.ClientMock
alias
Plug.Conn
setup_all
do
{
:ok
,
_
}
=
Registry
.
start_link
(
keys
:
:unique
,
name
:
ClientMock
)
:ok
end
setup
:verify_on_exit!
defp
request_mock
(
invokes
)
do
ClientMock
|>
expect
(
:request
,
fn
:get
,
url
,
headers
,
_body
,
_opts
->
Registry
.
register
(
ClientMock
,
url
,
0
)
body
=
headers
|>
Enum
.
into
(%{})
|>
Jason
.
encode!
()
{
:ok
,
200
,
[
{
"content-type"
,
"application/json"
},
{
"content-length"
,
byte_size
(
body
)
|>
to_string
()}
],
%{
url
:
url
,
body
:
body
}}
end
)
|>
expect
(
:stream_body
,
invokes
,
fn
%{
url
:
url
,
body
:
body
}
=
client
->
case
Registry
.
lookup
(
ClientMock
,
url
)
do
[{
_
,
0
}]
->
Registry
.
update_value
(
ClientMock
,
url
,
&
(
&1
+
1
))
{
:ok
,
body
,
client
}
[{
_
,
1
}]
->
Registry
.
unregister
(
ClientMock
,
url
)
:done
end
end
)
end
describe
"reverse proxy"
do
test
"do not track successful request"
,
%{
conn
:
conn
}
do
request_mock
(
2
)
url
=
"/success"
conn
=
ReverseProxy
.
call
(
conn
,
url
)
assert
conn
.
status
==
200
assert
Cachex
.
get
(
:failed_proxy_url_cache
,
url
)
==
{
:ok
,
nil
}
end
end
test
"use Pleroma's user agent in the request; don't pass the client's"
,
%{
conn
:
conn
}
do
request_mock
(
2
)
conn
=
conn
|>
Plug.Conn
.
put_req_header
(
"user-agent"
,
"fake/1.0"
)
|>
ReverseProxy
.
call
(
"/user-agent"
)
# Convert the response to a map without relying on json_response
body
=
conn
.
resp_body
assert
conn
.
status
==
200
response
=
Jason
.
decode!
(
body
)
assert
response
==
%{
"user-agent"
=>
Pleroma.Application
.
user_agent
()}
end
test
"closed connection"
,
%{
conn
:
conn
}
do
ClientMock
|>
expect
(
:request
,
fn
:get
,
"/closed"
,
_
,
_
,
_
->
{
:ok
,
200
,
[],
%{}}
end
)
|>
expect
(
:stream_body
,
fn
_
->
{
:error
,
:closed
}
end
)
|>
expect
(
:close
,
fn
_
->
:ok
end
)
conn
=
ReverseProxy
.
call
(
conn
,
"/closed"
)
assert
conn
.
halted
end
defp
stream_mock
(
invokes
,
with_close?
\\
false
)
do
ClientMock
|>
expect
(
:request
,
fn
:get
,
"/stream-bytes/"
<>
length
,
_
,
_
,
_
->
Registry
.
register
(
ClientMock
,
"/stream-bytes/"
<>
length
,
0
)
{
:ok
,
200
,
[{
"content-type"
,
"application/octet-stream"
}],
%{
url
:
"/stream-bytes/"
<>
length
}}
end
)
|>
expect
(
:stream_body
,
invokes
,
fn
%{
url
:
"/stream-bytes/"
<>
length
}
=
client
->
max
=
String
.
to_integer
(
length
)
case
Registry
.
lookup
(
ClientMock
,
"/stream-bytes/"
<>
length
)
do
[{
_
,
current
}]
when
current
<
max
->
Registry
.
update_value
(
ClientMock
,
"/stream-bytes/"
<>
length
,
&
(
&1
+
10
)
)
{
:ok
,
"0123456789"
,
client
}
[{
_
,
^
max
}]
->
Registry
.
unregister
(
ClientMock
,
"/stream-bytes/"
<>
length
)
:done
end
end
)
if
with_close?
do
expect
(
ClientMock
,
:close
,
fn
_
->
:ok
end
)
end
end
describe
"max_body"
do
test
"length returns error if content-length more than option"
,
%{
conn
:
conn
}
do
request_mock
(
0
)
assert
capture_log
(
fn
->
ReverseProxy
.
call
(
conn
,
"/huge-file"
,
max_body_length
:
4
)
end
)
=~
"[error] Elixir.Pleroma.ReverseProxy: request to \"/huge-file\" failed: :body_too_large"
assert
{
:ok
,
true
}
==
Cachex
.
get
(
:failed_proxy_url_cache
,
"/huge-file"
)
assert
capture_log
(
fn
->
ReverseProxy
.
call
(
conn
,
"/huge-file"
,
max_body_length
:
4
)
end
)
==
""
end
test
"max_body_length returns error if streaming body more than that option"
,
%{
conn
:
conn
}
do
stream_mock
(
3
,
true
)
assert
capture_log
(
fn
->
ReverseProxy
.
call
(
conn
,
"/stream-bytes/50"
,
max_body_length
:
30
)
end
)
=~
"Elixir.Pleroma.ReverseProxy request to /stream-bytes/50 failed while reading/chunking: :body_too_large"
end
end
describe
"HEAD requests"
do
test
"common"
,
%{
conn
:
conn
}
do
ClientMock
|>
expect
(
:request
,
fn
:head
,
"/head"
,
_
,
_
,
_
->
{
:ok
,
200
,
[{
"content-type"
,
"image/png"
}]}
end
)
conn
=
ReverseProxy
.
call
(
Map
.
put
(
conn
,
:method
,
"HEAD"
),
"/head"
)
assert
conn
.
status
==
200
assert
Conn
.
get_resp_header
(
conn
,
"content-type"
)
==
[
"image/png"
]
assert
conn
.
resp_body
==
""
end
end
defp
error_mock
(
status
)
when
is_integer
(
status
)
do
ClientMock
|>
expect
(
:request
,
fn
:get
,
"/status/"
<>
_
,
_
,
_
,
_
->
{
:error
,
status
}
end
)
end
describe
"returns error on"
do
test
"500"
,
%{
conn
:
conn
}
do
error_mock
(
500
)
url
=
"/status/500"
capture_log
(
fn
->
ReverseProxy
.
call
(
conn
,
url
)
end
)
=~
"[error] Elixir.Pleroma.ReverseProxy: request to /status/500 failed with HTTP status 500"
assert
Cachex
.
get
(
:failed_proxy_url_cache
,
url
)
==
{
:ok
,
true
}
{
:ok
,
ttl
}
=
Cachex
.
ttl
(
:failed_proxy_url_cache
,
url
)
assert
ttl
<=
60_000
end
test
"400"
,
%{
conn
:
conn
}
do
error_mock
(
400
)
url
=
"/status/400"
capture_log
(
fn
->
ReverseProxy
.
call
(
conn
,
url
)
end
)
=~
"[error] Elixir.Pleroma.ReverseProxy: request to /status/400 failed with HTTP status 400"
assert
Cachex
.
get
(
:failed_proxy_url_cache
,
url
)
==
{
:ok
,
true
}
assert
Cachex
.
ttl
(
:failed_proxy_url_cache
,
url
)
==
{
:ok
,
nil
}
end
test
"403"
,
%{
conn
:
conn
}
do
error_mock
(
403
)
url
=
"/status/403"
capture_log
(
fn
->
ReverseProxy
.
call
(
conn
,
url
,
failed_request_ttl
:
:timer
.
seconds
(
120
))
end
)
=~
"[error] Elixir.Pleroma.ReverseProxy: request to /status/403 failed with HTTP status 403"
{
:ok
,
ttl
}
=
Cachex
.
ttl
(
:failed_proxy_url_cache
,
url
)
assert
ttl
>
100_000
end
test
"204"
,
%{
conn
:
conn
}
do
url
=
"/status/204"
expect
(
ClientMock
,
:request
,
fn
:get
,
_url
,
_
,
_
,
_
->
{
:ok
,
204
,
[],
%{}}
end
)
capture_log
(
fn
->
conn
=
ReverseProxy
.
call
(
conn
,
url
)
assert
conn
.
resp_body
==
"Request failed: No Content"
assert
conn
.
halted
end
)
=~
"[error] Elixir.Pleroma.ReverseProxy: request to \"/status/204\" failed with HTTP status 204"
assert
Cachex
.
get
(
:failed_proxy_url_cache
,
url
)
==
{
:ok
,
true
}
assert
Cachex
.
ttl
(
:failed_proxy_url_cache
,
url
)
==
{
:ok
,
nil
}
end
end
test
"streaming"
,
%{
conn
:
conn
}
do
stream_mock
(
21
)
conn
=
ReverseProxy
.
call
(
conn
,
"/stream-bytes/200"
)
assert
conn
.
state
==
:chunked
assert
byte_size
(
conn
.
resp_body
)
==
200
assert
Conn
.
get_resp_header
(
conn
,
"content-type"
)
==
[
"application/octet-stream"
]
end
defp
headers_mock
(
_
)
do
ClientMock
|>
expect
(
:request
,
fn
:get
,
"/headers"
,
headers
,
_
,
_
->
Registry
.
register
(
ClientMock
,
"/headers"
,
0
)
{
:ok
,
200
,
[{
"content-type"
,
"application/json"
}],
%{
url
:
"/headers"
,
headers
:
headers
}}
end
)
|>
expect
(
:stream_body
,
2
,
fn
%{
url
:
url
,
headers
:
headers
}
=
client
->
case
Registry
.
lookup
(
ClientMock
,
url
)
do
[{
_
,
0
}]
->
Registry
.
update_value
(
ClientMock
,
url
,
&
(
&1
+
1
))
headers
=
for
{
k
,
v
}
<-
headers
,
into
:
%{},
do
:
{
String
.
capitalize
(
k
),
v
}
{
:ok
,
Jason
.
encode!
(%{
headers
:
headers
}),
client
}
[{
_
,
1
}]
->
Registry
.
unregister
(
ClientMock
,
url
)
:done
end
end
)
:ok
end
describe
"keep request headers"
do
setup
[
:headers_mock
]
test
"header passes"
,
%{
conn
:
conn
}
do
conn
=
Conn
.
put_req_header
(
conn
,
"accept"
,
"text/html"
)
|>
ReverseProxy
.
call
(
"/headers"
)
body
=
conn
.
resp_body
assert
conn
.
status
==
200
response
=
Jason
.
decode!
(
body
)
headers
=
response
[
"headers"
]
assert
headers
[
"Accept"
]
==
"text/html"
end
test
"header is filtered"
,
%{
conn
:
conn
}
do
conn
=
Conn
.
put_req_header
(
conn
,
"accept-language"
,
"en-US"
)
|>
ReverseProxy
.
call
(
"/headers"
)
body
=
conn
.
resp_body
assert
conn
.
status
==
200
response
=
Jason
.
decode!
(
body
)
headers
=
response
[
"headers"
]
refute
headers
[
"Accept-Language"
]
end
end
test
"returns 400 on non GET, HEAD requests"
,
%{
conn
:
conn
}
do
conn
=
ReverseProxy
.
call
(
Map
.
put
(
conn
,
:method
,
"POST"
),
"/ip"
)
assert
conn
.
status
==
400
end
describe
"cache resp headers"
do
test
"add cache-control"
,
%{
conn
:
conn
}
do
ClientMock
|>
expect
(
:request
,
fn
:get
,
"/cache"
,
_
,
_
,
_
->
{
:ok
,
200
,
[{
"ETag"
,
"some ETag"
}],
%{}}
end
)
|>
expect
(
:stream_body
,
fn
_
->
:done
end
)
conn
=
ReverseProxy
.
call
(
conn
,
"/cache"
)
assert
{
"cache-control"
,
"public, max-age=1209600"
}
in
conn
.
resp_headers
end
end
defp
disposition_headers_mock
(
headers
)
do
ClientMock
|>
expect
(
:request
,
fn
:get
,
"/disposition"
,
_
,
_
,
_
->
Registry
.
register
(
ClientMock
,
"/disposition"
,
0
)
{
:ok
,
200
,
headers
,
%{
url
:
"/disposition"
}}
end
)
|>
expect
(
:stream_body
,
2
,
fn
%{
url
:
"/disposition"
}
=
client
->
case
Registry
.
lookup
(
ClientMock
,
"/disposition"
)
do
[{
_
,
0
}]
->
Registry
.
update_value
(
ClientMock
,
"/disposition"
,
&
(
&1
+
1
))
{
:ok
,
""
,
client
}
[{
_
,
1
}]
->
Registry
.
unregister
(
ClientMock
,
"/disposition"
)
:done
end
end
)
end
describe
"response content disposition header"
do
test
"not attachment"
,
%{
conn
:
conn
}
do
disposition_headers_mock
([
{
"content-type"
,
"image/gif"
},
{
"content-length"
,
"0"
}
])
conn
=
ReverseProxy
.
call
(
conn
,
"/disposition"
)
assert
{
"content-type"
,
"image/gif"
}
in
conn
.
resp_headers
end
test
"with content-disposition header"
,
%{
conn
:
conn
}
do
disposition_headers_mock
([
{
"content-disposition"
,
"attachment; filename=\"filename.jpg\""
},
{
"content-length"
,
"0"
}
])
conn
=
ReverseProxy
.
call
(
conn
,
"/disposition"
)
assert
{
"content-disposition"
,
"attachment; filename=\"filename.jpg\""
}
in
conn
.
resp_headers
end
end
describe
"content-type sanitisation"
do
test
"preserves allowed image type"
,
%{
conn
:
conn
}
do
ClientMock
|>
expect
(
:request
,
fn
:get
,
"/content"
,
_
,
_
,
_
->
{
:ok
,
200
,
[{
"content-type"
,
"image/png"
}],
%{
url
:
"/content"
}}
end
)
|>
expect
(
:stream_body
,
fn
_
->
:done
end
)
conn
=
ReverseProxy
.
call
(
conn
,
"/content"
)
assert
conn
.
status
==
200
assert
Conn
.
get_resp_header
(
conn
,
"content-type"
)
==
[
"image/png"
]
end
test
"preserves allowed video type"
,
%{
conn
:
conn
}
do
ClientMock
|>
expect
(
:request
,
fn
:get
,
"/content"
,
_
,
_
,
_
->
{
:ok
,
200
,
[{
"content-type"
,
"video/mp4"
}],
%{
url
:
"/content"
}}
end
)
|>
expect
(
:stream_body
,
fn
_
->
:done
end
)
conn
=
ReverseProxy
.
call
(
conn
,
"/content"
)
assert
conn
.
status
==
200
assert
Conn
.
get_resp_header
(
conn
,
"content-type"
)
==
[
"video/mp4"
]
end
test
"sanitizes ActivityPub content type"
,
%{
conn
:
conn
}
do
ClientMock
|>
expect
(
:request
,
fn
:get
,
"/content"
,
_
,
_
,
_
->
{
:ok
,
200
,
[{
"content-type"
,
"application/activity+json"
}],
%{
url
:
"/content"
}}
end
)
|>
expect
(
:stream_body
,
fn
_
->
:done
end
)
conn
=
ReverseProxy
.
call
(
conn
,
"/content"
)
assert
conn
.
status
==
200
assert
Conn
.
get_resp_header
(
conn
,
"content-type"
)
==
[
"application/octet-stream"
]
end
test
"sanitizes LD-JSON content type"
,
%{
conn
:
conn
}
do
ClientMock
|>
expect
(
:request
,
fn
:get
,
"/content"
,
_
,
_
,
_
->
{
:ok
,
200
,
[{
"content-type"
,
"application/ld+json"
}],
%{
url
:
"/content"
}}
end
)
|>
expect
(
:stream_body
,
fn
_
->
:done
end
)
conn
=
ReverseProxy
.
call
(
conn
,
"/content"
)
assert
conn
.
status
==
200
assert
Conn
.
get_resp_header
(
conn
,
"content-type"
)
==
[
"application/octet-stream"
]
end
end
# Hackney is used for Reverse Proxy when Hackney or Finch is the Tesla Adapter
# Gun is able to proxy through Tesla, so it does not need testing as the
# test cases in the Pleroma.HTTPTest module are sufficient
describe
"Hackney URL encoding:"
do
setup
do
ClientMock
|>
expect
(
:request
,
fn
:get
,
"https://example.com/emoji/Pack%201/koronebless.png?foo=bar+baz"
,
_headers
,
_body
,
_opts
->
{
:ok
,
200
,
[{
"content-type"
,
"image/png"
}],
"It works!"
}
:get
,
"https://example.com/media/foo/bar%20!$&'()*+,;=/:%20@a%20%5Bbaz%5D.mp4"
,
_headers
,
_body
,
_opts
->
{
:ok
,
200
,
[{
"content-type"
,
"video/mp4"
}],
"Allowed reserved chars."
}
:get
,
"https://example.com/media/unicode%20%F0%9F%99%82%20.gif"
,
_headers
,
_body
,
_opts
->
{
:ok
,
200
,
[{
"content-type"
,
"image/gif"
}],
"Unicode emoji in path"
}
end
)
|>
stub
(
:stream_body
,
fn
_
->
:done
end
)
|>
stub
(
:close
,
fn
_
->
:ok
end
)
:ok
end
test
"properly encodes URLs with spaces"
,
%{
conn
:
conn
}
do
url_with_space
=
"https://example.com/emoji/Pack 1/koronebless.png?foo=bar baz"
result
=
ReverseProxy
.
call
(
conn
,
url_with_space
)
assert
result
.
status
==
200
end
test
"properly encoded URL should not be altered"
,
%{
conn
:
conn
}
do
properly_encoded_url
=
"https://example.com/emoji/Pack%201/koronebless.png?foo=bar+baz"
result
=
ReverseProxy
.
call
(
conn
,
properly_encoded_url
)
assert
result
.
status
==
200
end
test
"properly encodes URLs with allowed reserved characters"
,
%{
conn
:
conn
}
do
url_with_reserved_chars
=
"https://example.com/media/foo/bar !$&'()*+,;=/: @a [baz].mp4"
result
=
ReverseProxy
.
call
(
conn
,
url_with_reserved_chars
)
assert
result
.
status
==
200
end
test
"properly encodes URLs with unicode in path"
,
%{
conn
:
conn
}
do
url_with_unicode
=
"https://example.com/media/unicode 🙂 .gif"
result
=
ReverseProxy
.
call
(
conn
,
url_with_unicode
)
assert
result
.
status
==
200
end
end
end
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Tue, Jan 20, 2:04 PM (46 m, 4 s)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
973847
Default Alt Text
reverse_proxy_test.exs (13 KB)
Attached To
Mode
rPUBE pleroma-upstream
Attached
Detach File
Event Timeline
Log In to Comment