Skip to content

Commit

Permalink
Merge pull request #4060 from mplattner/mapremote-addon
Browse files Browse the repository at this point in the history
add mapremote addon to modify request URLs
  • Loading branch information
mhils committed Jul 3, 2020
2 parents 73163c8 + cee4da4 commit cf15802
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 21 deletions.
1 change: 1 addition & 0 deletions CHANGELOG
Expand Up @@ -16,6 +16,7 @@ Unreleased: mitmproxy next
* Simplify Replacements with new ModifyBody addon (@mplattner)
* Rename SetHeaders addon to ModifyHeaders (@mplattner)
* mitmweb: "New -> File" menu option has been renamed to "Clear All" (@yogeshojha)
* Add new MapRemote addon to rewrite URLs of requests (@mplattner)

* --- TODO: add new PRs above this line ---

Expand Down
80 changes: 59 additions & 21 deletions docs/src/content/overview-features.md
Expand Up @@ -9,23 +9,17 @@ menu:
# Mitmproxy Core Features


- [Mitmproxy Core Features](#mitmproxy-core-features)
- [Anticache](#anticache)
- [Client-side replay](#client-side-replay)
- [Modify Body](#modify-body)
- [Examples](#examples)
- [Modify Headers](#modify-headers)
- [Examples](#examples-1)
- [Proxy Authentication](#proxy-authentication)
- [Server-side replay](#server-side-replay)
- [Response refreshing](#response-refreshing)
- [Replaying a session recorded in Reverse-proxy Mode](#replaying-a-session-recorded-in-reverse-proxy-mode)
- [Sticky auth](#sticky-auth)
- [Sticky cookies](#sticky-cookies)
- [Streaming](#streaming)
- [Customizing Streaming](#customizing-streaming)
- [Websockets](#websockets)
- [Upstream Certificates](#upstream-certificates)
- [Anticache](#anticache)
- [Client-side replay](#client-side-replay)
- [Map Remote](#map-remote)
- [Modify Body](#modify-body)
- [Modify Headers](#modify-headers)
- [Proxy Authentication](#proxy-authentication)
- [Server-side replay](#server-side-replay)
- [Sticky Auth](#sticky-auth)
- [Sticky Cookies](#sticky-cookies)
- [Streaming](#streaming)
- [Upstream Certificates](#upstream-certificates)


## Anticache
Expand All @@ -49,10 +43,54 @@ You may want to use client-side replay in conjunction with the `anticache`
option, to make sure the server responds with complete data.


## Map Remote

The `map_remote` option lets you specify an arbitrary number of patterns that
define replacements within HTTP request URLs before they are sent to a server.
The substituted URL is fetched instead of the original resource
and the corresponding HTTP response is returned transparently to the client.
Note that if the original destination uses HTTP2, the substituted destination
needs to support HTTP2 as well, otherwise the substituted request may fail.
`map_remote` patterns looks like this:

```
|flow-filter|regex|replacement
|flow-filter|regex|@file-path
|regex|replacement
|regex|@file-path
```

* **flow-filter** is an optional mitmproxy [filter expression]({{< relref "concepts-filters">}})
that defines which requests a replacement applies to.

* **regex** is a valid Python regular expression that defines what gets replaced in the URLs of requests.

* **replacement** is a string literal that is substituted in. If the replacement string
literal starts with `@` as in `@file-path`, it is treated as a **file path** from which the replacement is read.

The _separator_ is arbitrary, and is defined by the first character.

### Examples

Map all requests ending with `.jpg` to `https://placedog.net/640/480?random`.
Note that this might fail if the original HTTP request destination uses HTTP2 but the replaced
destination does not support HTTP2.

```
|.*\.jpg$|https://placedog.net/640/480?random
```

Re-route all GET requests from `example.org` to `mitmproxy.org` (using `|` as the separator):

```
|~m GET|//example.org/|//mitmproxy.org/
```


## Modify Body

The `modify_body` option lets you specify an arbitrary number of patterns that
define replacements within bodies of flows. `modify_body` patterns looks like this:
define replacements within bodies of flows. `modify_body` patterns look like this:

{{< highlight none >}}
/flow-filter/regex/replacement
Expand All @@ -62,14 +100,14 @@ define replacements within bodies of flows. `modify_body` patterns looks like th
{{< / highlight >}}

* **flow-filter** is an optional mitmproxy [filter expression]({{< relref "concepts-filters">}})
that defines which flows a replacement applies to
that defines which flows a replacement applies to.

* **regex** is a valid Python regular expression that defines what gets replaced
* **regex** is a valid Python regular expression that defines what gets replaced.

* **replacement** is a string literal that is substituted in. If the replacement string
literal starts with `@` as in `@file-path`, it is treated as a **file path** from which the replacement is read.

The _separator_ is arbitrary, and is defined by the first character.
The _separator_ is arbitrary, and is defined by the first character.

Modify hooks fire when either a client request or a server response is
received. Only the matching flow component is affected: so, for example,
Expand Down
2 changes: 2 additions & 0 deletions mitmproxy/addons/__init__.py
Expand Up @@ -13,6 +13,7 @@
from mitmproxy.addons import proxyauth
from mitmproxy.addons import script
from mitmproxy.addons import serverplayback
from mitmproxy.addons import mapremote
from mitmproxy.addons import modifybody
from mitmproxy.addons import modifyheaders
from mitmproxy.addons import stickyauth
Expand All @@ -39,6 +40,7 @@ def default_addons():
proxyauth.ProxyAuth(),
script.ScriptLoader(),
serverplayback.ServerPlayback(),
mapremote.MapRemote(),
modifybody.ModifyBody(),
modifyheaders.ModifyHeaders(),
stickyauth.StickyAuth(),
Expand Down
65 changes: 65 additions & 0 deletions mitmproxy/addons/mapremote.py
@@ -0,0 +1,65 @@
import os
import re
import typing

from mitmproxy import exceptions
from mitmproxy import ctx
from mitmproxy.addons.modifyheaders import parse_modify_spec, ModifySpec


class MapRemote:
def __init__(self):
self.replacements: typing.List[ModifySpec] = []

def load(self, loader):
loader.add_option(
"map_remote", typing.Sequence[str], [],
"""
Replacement pattern of the form "[/flow-filter]/regex/[@]replacement", where
the separator can be any character. The @ allows to provide a file path that
is used to read the replacement string.
"""
)

def configure(self, updated):
if "map_remote" in updated:
self.replacements = []
for option in ctx.options.map_remote:
try:
spec = parse_modify_spec(option)
try:
re.compile(spec.subject)
except re.error:
raise ValueError(f"Invalid regular expression: {spec.subject}")
except ValueError as e:
raise exceptions.OptionsError(
f"Cannot parse map_remote option {option}: {e}"
) from e

self.replacements.append(spec)

def request(self, flow):
if not flow.reply.has_message:
for spec in self.replacements:
if spec.matches(flow):
self.replace(flow.request, spec.subject, spec.replacement)

def replace(self, obj, search, repl):
"""
Replaces all matches of the regex search in the url of the request with repl.
Returns:
The number of replacements made.
"""
if repl.startswith(b"@"):
path = os.path.expanduser(repl[1:])
try:
with open(path, "rb") as f:
repl = f.read()
except IOError:
ctx.log.warn("Could not read replacement file: %s" % repl)
return

replacements = 0
obj.url, replacements = re.subn(search, repl, obj.pretty_url.encode("utf8", "surrogateescape"), flags=re.DOTALL)
return replacements
4 changes: 4 additions & 0 deletions mitmproxy/tools/cmdline.py
Expand Up @@ -81,6 +81,10 @@ def common_options(parser, opts):
opts.make_parser(group, "server_replay_nopop")
opts.make_parser(group, "server_replay_refresh")

# Map Remote
group = parser.add_argument_group("Map Remote")
opts.make_parser(group, "map_remote", metavar="PATTERN", short="M")

# Modify Body
group = parser.add_argument_group("Modify Body")
opts.make_parser(group, "modify_body", metavar="PATTERN", short="B")
Expand Down
72 changes: 72 additions & 0 deletions test/mitmproxy/addons/test_mapremote.py
@@ -0,0 +1,72 @@
import pytest

from mitmproxy.addons import mapremote
from mitmproxy.test import taddons
from mitmproxy.test import tflow


class TestMapRemote:

def test_configure(self):
mr = mapremote.MapRemote()
with taddons.context(mr) as tctx:
tctx.configure(mr, map_remote=["one/two/three"])
with pytest.raises(Exception, match="Cannot parse map_remote .* Invalid number"):
tctx.configure(mr, map_remote = ["/"])
with pytest.raises(Exception, match="Cannot parse map_remote .* Invalid filter"):
tctx.configure(mr, map_remote=["/~b/two/three"])
with pytest.raises(Exception, match="Cannot parse map_remote .* Invalid regular expression"):
tctx.configure(mr, map_remote=["/foo/+/three"])
tctx.configure(mr, map_remote=["/a/b/c/"])

def test_simple(self):
mr = mapremote.MapRemote()
with taddons.context(mr) as tctx:
tctx.configure(
mr,
map_remote=[
":example.org/images/:mitmproxy.org/img/",
]
)
f = tflow.tflow()
f.request.url = b"https://example.org/images/test.jpg"
mr.request(f)
assert f.request.url == "https://mitmproxy.org/img/test.jpg"


class TestMapRemoteFile:
def test_simple(self, tmpdir):
mr = mapremote.MapRemote()
with taddons.context(mr) as tctx:
tmpfile = tmpdir.join("replacement")
tmpfile.write("mitmproxy.org")
tctx.configure(
mr,
map_remote=[":example.org:@" + str(tmpfile)]
)
f = tflow.tflow()
f.request.url = b"https://example.org/test"
mr.request(f)
assert f.request.url == "https://mitmproxy.org/test"

@pytest.mark.asyncio
async def test_nonexistent(self, tmpdir):
mr = mapremote.MapRemote()
with taddons.context(mr) as tctx:
with pytest.raises(Exception, match="Invalid file path"):
tctx.configure(
mr,
map_remote=[":~q:example.org:@nonexistent"]
)

tmpfile = tmpdir.join("replacement")
tmpfile.write("mitmproxy.org")
tctx.configure(
mr,
map_remote=[":example.org:@" + str(tmpfile)]
)
tmpfile.remove()
f = tflow.tflow()
f.request.url = b"https://example.org/test"
mr.request(f)
assert await tctx.master.await_log("could not read")

0 comments on commit cf15802

Please sign in to comment.