Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
7338ada
Store all requests to pages hosted by BugHog and minor fixes
GJFR Oct 24, 2024
bd0e23b
Custom .py experiment files (#34)
szymsza Oct 29, 2024
1da8973
WiP - interaction script
szymsza Oct 30, 2024
5a516ba
Connect browser interaction to terminal automation
szymsza Oct 30, 2024
6cef759
Install pip requirements in dev container
szymsza Oct 30, 2024
688948f
Simple interaction browser identification
szymsza Oct 30, 2024
b2f1f82
Fix browser log reading
szymsza Oct 30, 2024
e8f56a2
Browser interaction commands interpreter
szymsza Oct 30, 2024
c0e4790
Basic BiDi implementation
szymsza Oct 30, 2024
796bf93
Remove print
szymsza Oct 30, 2024
c2c5580
Simple AutoGUI test experiment
szymsza Oct 31, 2024
b8c02dd
Install AutoGUI
szymsza Oct 31, 2024
1444ff2
Simple AutoGUI usage
szymsza Oct 31, 2024
0acc9c2
Remove debugging ports
szymsza Oct 31, 2024
840190a
Fix opening Firefox
szymsza Oct 31, 2024
c85fa2a
Fix opening Firefox
szymsza Oct 31, 2024
e7cfbd9
Improve PoC editor
GJFR Oct 31, 2024
44e5dac
Editing config files through the web UI
szymsza Oct 31, 2024
025c8ec
Interaction script - support blank lines and comments
szymsza Oct 31, 2024
32a6149
Interaction script editor code highlighting
szymsza Oct 31, 2024
b56b17c
Add new Sleep command
szymsza Oct 31, 2024
4775796
Clicking on element IDs
szymsza Oct 31, 2024
641f122
Implement clicking by screen percentage
szymsza Oct 31, 2024
3ede69e
Implement clicking by screen percentage
szymsza Oct 31, 2024
8e1043a
Save screenshots to filesystem
szymsza Oct 31, 2024
63ab107
Fix collector notifications status code
szymsza Nov 1, 2024
dcbc707
Remove redundant styles
szymsza Nov 1, 2024
2fc829b
Add default experiment file values
szymsza Nov 1, 2024
5fbb38b
Refactor experiment
szymsza Nov 1, 2024
6e366bd
Better selectors styling
szymsza Nov 1, 2024
45710c2
Adding config files through the frontend - Vue part
szymsza Nov 1, 2024
acdec3c
Adding config files through the frontend - Python part
szymsza Nov 1, 2024
3ee4d20
More interaction commands
szymsza Nov 1, 2024
52211d0
Styling fix
szymsza Nov 1, 2024
639441c
Final interaction improvements
szymsza Nov 1, 2024
8e4af95
Remove old experiment
szymsza Nov 1, 2024
e815589
Merge branch 'dev' into dev-auto-gui
GJFR Nov 12, 2024
a2e61c2
Fix button to add script
GJFR Nov 13, 2024
792447e
Rename interaction_script.cmd to script.md and small button fix
GJFR Nov 13, 2024
2d1ad6d
AutoGUI interaction (#36)
GJFR Nov 13, 2024
69c758c
Remove test experiment
GJFR Nov 13, 2024
2a34025
Add button to create a new project
GJFR Nov 13, 2024
f6e34cc
Automatically create empty collection when trying to retrieve non-exi…
GJFR Nov 13, 2024
301da7e
Fix issue where conditional /report/if endpoints couldn't be reached
GJFR Nov 13, 2024
cf43c7d
Fix creation of python server file
GJFR Nov 15, 2024
6c275f4
Clear editor when changing poc or project
GJFR Nov 15, 2024
849a6f2
Fix two typos
GJFR Nov 15, 2024
968dfc3
Enable various advanced options for release version evaluation
GJFR Nov 22, 2024
1e24fb0
Abandon hacky nginx request splitting in favor of the `mirror` direct…
GJFR Nov 22, 2024
1272591
Remove pseudo-interface for master.py + small fixes + cleanup
GJFR Nov 25, 2024
7572c70
Add option to remove datapoints by SHIFT clicking squares and dots in…
GJFR Nov 26, 2024
c00eb5b
Fix SHIFT down checking for Gantt chart
GJFR Nov 26, 2024
fd30b0b
Isolate browser sessions and clean Downloads folder between tries.
GJFR Nov 26, 2024
1cdecf7
Fix bug where states would be evaluated more than once
GJFR Nov 28, 2024
0ebaf3c
Update BokehJS
GJFR Nov 28, 2024
c5e563e
Upgrade python dependencies
GJFR Nov 28, 2024
4a9a5a5
Add convenience scripts for updating pip and npm dependencies
GJFR Nov 28, 2024
2d19a14
Update node dependencies
GJFR Nov 28, 2024
8dcb3f0
Fix test failing due to changed internals
GJFR Nov 28, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@
"Vue.volar"
]
}
}
},

// Install pip requirements
"postCreateCommand": "pip install -r requirements.txt"

// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
Expand All @@ -35,9 +38,6 @@
// Uncomment the next line if you want to keep your containers running after VS Code shuts down.
// "shutdownAction": "none",

// Uncomment the next line to run commands after the container is created.
// "postCreateCommand": "cat /etc/os-release",

// Configure tool-specific properties.
// "customizations": {},
}
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ nginx/ssl/keys/*
!**/.gitkeep
**/node_modules
**/junit.xml

# Screenshots
logs/screenshots/*
!logs/screenshots/.gitkeep

# Fish shell
$HOME

# JetBrains IDEs
.idea

# Created by https://www.toptal.com/developers/gitignore/api/intellij,python,flask,macos
# Edit at https://www.toptal.com/developers/gitignore?templates=intellij,python,flask,macos

Expand Down
4 changes: 4 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,11 @@ RUN cp /app/scripts/daemon/xvfb /etc/init.d/xvfb
# Install python packages
COPY requirements.txt /app/requirements.txt
RUN pip install --user -r /app/requirements.txt
RUN apt-get install python3-tk python3-xlib gnome-screenshot -y

# Initiate PyAutoGUI
RUN touch /root/.Xauthority && \
xauth add ${HOST}:0 . $(xxd -l 16 -p /dev/urandom)

FROM base AS core
# Copy rest of source code
Expand Down
8 changes: 4 additions & 4 deletions bci/analysis/plot_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
class PlotFactory:

@staticmethod
def get_plot_revision_data(params: PlotParameters, db: MongoDB) -> dict:
revision_docs = db.get_documents_for_plotting(params)
def get_plot_revision_data(params: PlotParameters) -> dict:
revision_docs = MongoDB().get_documents_for_plotting(params)
revision_results = PlotFactory.__add_outcome_info(params, revision_docs)
return revision_results

@staticmethod
def get_plot_version_data(params: PlotParameters, db: MongoDB) -> dict:
version_docs = db.get_documents_for_plotting(params, releases=True)
def get_plot_version_data(params: PlotParameters) -> dict:
version_docs = MongoDB().get_documents_for_plotting(params, releases=True)
version_results = PlotFactory.__add_outcome_info(params, version_docs)
return version_results

Expand Down
17 changes: 13 additions & 4 deletions bci/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,37 @@
from flask import Flask
from flask_sock import Sock

from bci.main import Main as bci_api
from bci.configuration import Global, Loggers
from bci.main import Main

sock = Sock()


def create_app():
bci_api.initialize()
Loggers.configure_loggers()

if not Global.check_required_env_parameters():
raise Exception('Not all required environment variables are available')

# Instantiate main object and add to global flask context
main = Main()

# Blueprint modules are only imported after loggers are configured
from bci.web.blueprints.api import api
from bci.web.blueprints.experiments import exp

app = Flask(__name__)
# We don't store anything sensitive in the session, so we can use a simple secret key
app.config['main'] = main
app.secret_key = 'secret_key'

app.register_blueprint(api)
app.register_blueprint(exp)
sock.init_app(app)

# Configure signal handlers
signal.signal(signal.SIGTERM, bci_api.sigint_handler)
signal.signal(signal.SIGINT, bci_api.sigint_handler)
signal.signal(signal.SIGTERM, main.sigint_handler)
signal.signal(signal.SIGINT, main.sigint_handler)

return app

Expand Down
30 changes: 17 additions & 13 deletions bci/browser/automation/terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,35 @@


class TerminalAutomation:

@staticmethod
def run(url: str, args: list[str], seconds_per_visit: int):
logger.debug("Starting browser process...")
def visit_url(url: str, args: list[str], seconds_per_visit: int):
args.append(url)
proc = TerminalAutomation.open_browser(args)
logger.debug(f'Visiting the page for {seconds_per_visit}s')
time.sleep(seconds_per_visit)
TerminalAutomation.terminate_browser(proc, args)

@staticmethod
def open_browser(args: list[str]) -> subprocess.Popen:
logger.debug('Starting browser process...')
logger.debug(f'Command string: \'{" ".join(args)}\'')
with open('/tmp/browser.log', 'a') as file:
proc = subprocess.Popen(
args,
stdout=file,
stderr=file
)
with open('/tmp/browser.log', 'a+') as file:
proc = subprocess.Popen(args, stdout=file, stderr=file)
return proc

time.sleep(seconds_per_visit)
@staticmethod
def terminate_browser(proc: subprocess.Popen, args: list[str]) -> None:
logger.debug('Terminating browser process using SIGINT...')

logger.debug(f'Terminating browser process after {seconds_per_visit}s using SIGINT...')
# Use SIGINT and SIGTERM to end process such that cookies remain saved.
proc.send_signal(signal.SIGINT)
proc.send_signal(signal.SIGTERM)

try:
stdout, stderr = proc.communicate(timeout=5)
except subprocess.TimeoutExpired:
logger.info("Browser process did not terminate after 5s. Killing process through pkill...")
logger.info('Browser process did not terminate after 5s. Killing process through pkill...')
subprocess.run(['pkill', '-2', args[0].split('/')[-1]])

proc.wait()
logger.debug("Browser process terminated.")
logger.debug('Browser process terminated.')
44 changes: 39 additions & 5 deletions bci/browser/configuration/browser.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import os
import subprocess
from abc import abstractmethod

import bci.browser.binary.factory as binary_factory
Expand All @@ -15,9 +16,13 @@


class Browser:
process: subprocess.Popen | None

def __init__(self, browser_config: BrowserConfiguration, eval_config: EvaluationConfiguration, binary: Binary) -> None:
def __init__(
self, browser_config: BrowserConfiguration, eval_config: EvaluationConfiguration, binary: Binary
) -> None:
self.browser_config = browser_config
self.process = None
self.eval_config = eval_config
self.binary = binary
self.state = binary.state
Expand All @@ -34,10 +39,22 @@ def visit(self, url: str):
match self.eval_config.automation:
case 'terminal':
args = self._get_terminal_args()
TerminalAutomation.run(url, args, self.eval_config.seconds_per_visit)
TerminalAutomation.visit_url(url, args, self.eval_config.seconds_per_visit)
case _:
raise AttributeError('Not implemented')

def open(self, url: str) -> None:
args = self._get_terminal_args()
args.append(url)
self.process = TerminalAutomation.open_browser(args)

def terminate(self):
if self.process is None:
return

TerminalAutomation.terminate_browser(self.process, self._get_terminal_args())
self.process = None

def pre_evaluation_setup(self):
self.__fetch_binary()

Expand All @@ -46,11 +63,16 @@ def post_evaluation_cleanup(self):

def pre_test_setup(self):
self.__prepare_execution_folder()
self._prepare_profile_folder()

def post_test_cleanup(self):
self.__remove_execution_folder()

def pre_try_setup(self):
self._prepare_profile_folder()

def post_try_cleanup(self):
self.__remove_profile_folder()
self.__empty_downloads_folder()

def __fetch_binary(self):
self.binary.fetch_binary()
Expand All @@ -70,21 +92,33 @@ def __remove_execution_folder(self):
util.rmtree(self.__get_execution_folder_path())

def __remove_profile_folder(self):
if self._profile_path is None:
return
remove_profile_execution_folder(self._profile_path)
self._profile_path = None

def __empty_downloads_folder(self):
download_folder = '/root/Downloads'
util.remove_all_in_folder(download_folder)

def __get_execution_folder_path(self) -> str:
return os.path.join(EXECUTION_PARENT_FOLDER, str(self.state.name))

def _get_executable_file_path(self) -> str:
return os.path.join(self.__get_execution_folder_path(), self.binary.executable_name)

@abstractmethod
def _get_terminal_args(self):
def _get_terminal_args(self) -> list[str]:
pass

@abstractmethod
def get_navigation_sleep_duration(self) -> int:
pass

@staticmethod
def get_browser(browser_config: BrowserConfiguration, eval_config: EvaluationConfiguration, state: State) -> Browser:
def get_browser(
browser_config: BrowserConfiguration, eval_config: EvaluationConfiguration, state: State
) -> Browser:
from bci.browser.configuration.chromium import Chromium
from bci.browser.configuration.firefox import Firefox

Expand Down
3 changes: 3 additions & 0 deletions bci/browser/configuration/chromium.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@

class Chromium(Browser):

def get_navigation_sleep_duration(self) -> int:
return 1

def _get_terminal_args(self) -> list[str]:
assert self._profile_path is not None

Expand Down
10 changes: 7 additions & 3 deletions bci/browser/configuration/firefox.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@

from bci import cli
from bci.browser.configuration.browser import Browser
from bci.browser.configuration.options import Default, BlockThirdPartyCookies, PrivateBrowsing, TrackingProtection
from bci.browser.configuration.options import BlockThirdPartyCookies, Default, PrivateBrowsing, TrackingProtection
from bci.browser.configuration.profile import prepare_firefox_profile


SUPPORTED_OPTIONS = [
Default(),
BlockThirdPartyCookies(),
Expand All @@ -21,11 +20,15 @@

class Firefox(Browser):

def get_navigation_sleep_duration(self) -> int:
return 2

def _get_terminal_args(self) -> list[str]:
assert self._profile_path is not None

args = [self._get_executable_file_path()]
args.extend(['-profile', self._profile_path])
args.append('-setDefaultBrowser')
user_prefs = []

def add_user_pref(key: str, value: str | int | bool):
Expand All @@ -45,6 +48,7 @@ def add_user_pref(key: str, value: str | int | bool):
# add_user_pref('network.proxy.type', 1)

add_user_pref('app.update.enabled', False)
add_user_pref('browser.shell.checkDefaultBrowser', False)
if 'default' in self.browser_config.browser_setting:
pass
elif 'btpc' in self.browser_config.browser_setting:
Expand Down Expand Up @@ -92,7 +96,7 @@ def _prepare_profile_folder(self):
if 'tp' in self.browser_config.browser_setting:
self._profile_path = prepare_firefox_profile('tp-67')
else:
self._profile_path = prepare_firefox_profile('default-67')
self._profile_path = prepare_firefox_profile()

# Make Firefox trust the bughog CA

Expand Down
16 changes: 11 additions & 5 deletions bci/browser/configuration/profile.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,40 @@
import os
from typing import Optional

from bci import cli

PROFILE_STORAGE_FOLDER = '/app/browser/profiles'
PROFILE_EXECUTION_FOLDER = '/tmp/profiles'


def prepare_chromium_profile(profile_name: str = None) -> str:
def prepare_chromium_profile(profile_name: Optional[str] = None) -> str:
# Create new execution profile folder
profile_execution_path = os.path.join(PROFILE_EXECUTION_FOLDER, 'new_profile')
profile_execution_path = __create_folder(profile_execution_path)

# Copy profile from storage to execution folder if profile_name is given
if profile_name:
if not os.path.exists(os.path.join(PROFILE_STORAGE_FOLDER, 'chromium', profile_name)):
raise Exception(f'Profile \'{profile_name}\' does not exist')
raise Exception(f"Profile '{profile_name}' does not exist")
profile_storage_path = os.path.join(PROFILE_STORAGE_FOLDER, profile_name, 'Default')
cli.execute(f'cp -r {profile_storage_path} {profile_execution_path}')
return profile_execution_path


def prepare_firefox_profile(profile_name: str = None) -> str:
def prepare_firefox_profile(profile_name: Optional[str] = None) -> str:
""""
Create a temporary profile folder, based on the provided name of the premade profile.

:param profile_name: Reference to the premade profile folder used for creating the temporary profile.
"""
# Create new execution profile folder
profile_execution_path = os.path.join(PROFILE_EXECUTION_FOLDER, 'new_profile')
profile_execution_path = __create_folder(profile_execution_path)

# Copy profile from storage to execution folder if profile_name is given
if profile_name is None:
if profile_name is not None:
if not os.path.exists(os.path.join(PROFILE_STORAGE_FOLDER, 'firefox', profile_name)):
raise Exception(f'Profile \'{profile_name}\' does not exist')
raise Exception(f"Profile '{profile_name}' does not exist")
profile_storage_path = os.path.join(PROFILE_STORAGE_FOLDER, profile_name)
cli.execute(f'cp -r {profile_storage_path} {profile_execution_path}')
return profile_execution_path
Expand Down
Empty file.
Binary file added bci/browser/interaction/elements/five.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added bci/browser/interaction/elements/four.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added bci/browser/interaction/elements/one.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added bci/browser/interaction/elements/six.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added bci/browser/interaction/elements/three.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added bci/browser/interaction/elements/two.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading