Skip to content

Commit

Permalink
Factor out creation vs login; add callback addon
Browse files Browse the repository at this point in the history
- Callback addon allows tests to introspect some mitmproxy things in
  a read-only way. They cannot modify the response.
- Factor out creation from login, so we can call client functions
  during the login process e.g when uploading OTKs.
  • Loading branch information
kegsay committed Jan 11, 2024
1 parent 294933e commit b13a69f
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 11 deletions.
2 changes: 2 additions & 0 deletions internal/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ type Client interface {
// If we get callbacks/events after this point, tests may panic if the callbacks
// log messages.
Close(t *testing.T)

Login(t *testing.T, opts ClientCreationOpts) error
// StartSyncing to begin syncing from sync v2 / sliding sync.
// Tests should call stopSyncing() at the end of the test.
// MUST BLOCK until the initial sync is complete.
Expand Down
15 changes: 12 additions & 3 deletions internal/api/js/js.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,17 @@ func NewJSClient(t *testing.T, opts api.ClientCreationOpts) (api.Client, error)
},
}
});`, opts.BaseURL, "true", opts.UserID, deviceID))

return &api.LoggedClient{Client: jsc}, nil
}

func (c *JSClient) Login(t *testing.T, opts api.ClientCreationOpts) error {
deviceID := "undefined"
if opts.DeviceID != "" {
deviceID = `"` + opts.DeviceID + `"`
}
// cannot use loginWithPassword as this generates a new device ID
chrome.MustRunAsyncFn[chrome.Void](t, browser.Ctx, fmt.Sprintf(`
chrome.MustRunAsyncFn[chrome.Void](t, c.browser.Ctx, fmt.Sprintf(`
await window.__client.login("m.login.password", {
user: "%s",
password: "%s",
Expand All @@ -122,15 +131,15 @@ func NewJSClient(t *testing.T, opts api.ClientCreationOpts) (api.Client, error)
await window.__client.initRustCrypto();`, opts.UserID, opts.Password, deviceID))

// any events need to log the control string so we get notified
chrome.MustRunAsyncFn[chrome.Void](t, browser.Ctx, fmt.Sprintf(`
chrome.MustRunAsyncFn[chrome.Void](t, c.browser.Ctx, fmt.Sprintf(`
window.__client.on("Event.decrypted", function(event) {
console.log("%s"+event.getRoomId()+"||"+JSON.stringify(event.getEffectiveEvent()));
});
window.__client.on("event", function(event) {
console.log("%s"+event.getRoomId()+"||"+JSON.stringify(event.getEffectiveEvent()));
});`, CONSOLE_LOG_CONTROL_STRING, CONSOLE_LOG_CONTROL_STRING))

return &api.LoggedClient{Client: jsc}, nil
return nil
}

// Close is called to clean up resources.
Expand Down
20 changes: 12 additions & 8 deletions internal/api/rust/rust.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,6 @@ func NewRustClient(t *testing.T, opts api.ClientCreationOpts, ssURL string) (api
if err != nil {
return nil, fmt.Errorf("ClientBuilder.Build failed: %s", err)
}
var deviceID *string
if opts.DeviceID != "" {
deviceID = &opts.DeviceID
}
err = client.Login(opts.UserID, opts.Password, nil, deviceID)
if err != nil {
return nil, fmt.Errorf("Client.Login failed: %s", err)
}
c := &RustClient{
userID: opts.UserID,
FFIClient: client,
Expand All @@ -68,6 +60,18 @@ func NewRustClient(t *testing.T, opts api.ClientCreationOpts, ssURL string) (api
return &api.LoggedClient{Client: c}, nil
}

func (c *RustClient) Login(t *testing.T, opts api.ClientCreationOpts) error {
var deviceID *string
if opts.DeviceID != "" {
deviceID = &opts.DeviceID
}
err := c.FFIClient.Login(opts.UserID, opts.Password, nil, deviceID)
if err != nil {
return fmt.Errorf("Client.Login failed: %s", err)
}
return nil
}

func (c *RustClient) Close(t *testing.T) {
t.Helper()
c.roomsMu.Lock()
Expand Down
2 changes: 2 additions & 0 deletions tests/addons/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from mitmproxy.addons import asgiapp

from callback import Callback
from status_code import StatusCode
from controller import MITM_DOMAIN_NAME, app

addons = [
asgiapp.WSGIApp(app, MITM_DOMAIN_NAME, 80), # requests to this host will be routed to the flask app
StatusCode(),
Callback(),
]
# testcontainers will look for this log line
print("loading complement crypto addons", flush=True)
84 changes: 84 additions & 0 deletions tests/addons/callback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
from typing import Optional
import json

from mitmproxy import ctx, flowfilter
from mitmproxy.http import Response
from controller import MITM_DOMAIN_NAME
from urllib.request import urlopen, Request
from urllib.error import HTTPError, URLError

# Callback will intercept a response and send a POST request to the provided callback_url, with
# the following JSON object. Supports filters: https://docs.mitmproxy.org/stable/concepts-filters/
# {
# method: "GET|PUT|...",
# access_token: "syt_11...",
# url: "http://hs1/_matrix/client/...",
# response_code: 200,
# }
# Currently this is a read-only callback. The response cannot be modified, but side-effects can be
# taken. For example, tests may wish to terminate a client prior to the delivery of a response but
# after the server has processed the request, or the test may wish to use the response as a
# synchronisation point for a Waiter.
class Callback:
def __init__(self):
self.reset()
self.matchall = flowfilter.parse(".")
self.filter: Optional[flowfilter.TFilter] = self.matchall

def reset(self):
self.config = {
"callback_url": "",
"filter": None,
}

def load(self, loader):
loader.add_option(
name="callback",
typespec=dict,
default={"callback_url": "", "filter": None},
help="Change the callback url, with an optional filter",
)

def configure(self, updates):
if "callback" not in updates:
self.reset()
return
if ctx.options.callback is None or ctx.options.callback["callback_url"] == "":
self.reset()
return
self.config = ctx.options.callback
new_filter = self.config.get('filter', None)
print(f"callback will hit {self.config['callback_url']} filter={new_filter}")
if new_filter:
self.filter = flowfilter.parse(new_filter)
else:
self.filter = self.matchall

def response(self, flow):
# always ignore the controller
if flow.request.pretty_host == MITM_DOMAIN_NAME:
return
if self.config["callback_url"] == "":
return # ignore responses if we aren't told a url
if flowfilter.match(self.filter, flow):
data = json.dumps({
"method": flow.request.method,
"access_token": flow.request.headers.get("Authorization", "").removeprefix("Bearer "),
"url": flow.request.url,
"response_code": flow.response.status_code,
})
request = Request(
self.config["callback_url"],
headers={"Content-Type": "application/json"},
data=data.encode("utf-8"),
)
try:
with urlopen(request, timeout=10) as response:
print(f"callback returned HTTP {response.status}")
return response.read(), response
except HTTPError as error:
print(f"ERR: callback returned {error.status} {error.reason}")
except URLError as error:
print(f"ERR: callback returned {error.reason}")
except TimeoutError:
print(f"ERR: callback request timed out")
2 changes: 2 additions & 0 deletions tests/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,12 @@ func MustLoginClient(t *testing.T, clientType api.ClientType, opts api.ClientCre
case api.ClientTypeRust:
c, err := rust.NewRustClient(t, opts, ssURL)
must.NotError(t, "NewRustClient: %s", err)
c.Login(t, opts)
return c
case api.ClientTypeJS:
c, err := js.NewJSClient(t, opts)
must.NotError(t, "NewJSClient: %s", err)
c.Login(t, opts)
return c
default:
t.Fatalf("unknown client type %v", clientType)
Expand Down

0 comments on commit b13a69f

Please sign in to comment.