
commit
d4cd34b3a5
343 changed files with 124188 additions and 0 deletions
-
9.gitignore
-
0Libraries/Python3/pineapple/__init__.py
-
1Libraries/Python3/pineapple/helpers/__init__.py
-
36Libraries/Python3/pineapple/helpers/command_helpers.py
-
16Libraries/Python3/pineapple/helpers/helpers.py
-
34Libraries/Python3/pineapple/helpers/network_helpers.py
-
42Libraries/Python3/pineapple/helpers/notification_helpers.py
-
167Libraries/Python3/pineapple/helpers/opkg_helpers.py
-
3Libraries/Python3/pineapple/jobs/__init__.py
-
42Libraries/Python3/pineapple/jobs/job.py
-
162Libraries/Python3/pineapple/jobs/job_manager.py
-
53Libraries/Python3/pineapple/jobs/job_runner.py
-
27Libraries/Python3/pineapple/logger/__init__.py
-
25Libraries/Python3/pineapple/logger/pretty_formatter.py
-
2Libraries/Python3/pineapple/modules/__init__.py
-
364Libraries/Python3/pineapple/modules/module.py
-
9Libraries/Python3/pineapple/modules/request.py
-
6Libraries/README.md
-
11README.md
-
13cabinet/.editorconfig
-
46cabinet/.gitignore
-
47cabinet/angular.json
-
42cabinet/build.sh
-
14727cabinet/package-lock.json
-
52cabinet/package.json
-
7cabinet/projects/cabinet/ng-package.json
-
11cabinet/projects/cabinet/package.json
-
43cabinet/projects/cabinet/src/lib/cabinet.module.ts
-
25cabinet/projects/cabinet/src/lib/components/cabinet.component.css
-
88cabinet/projects/cabinet/src/lib/components/cabinet.component.html
-
176cabinet/projects/cabinet/src/lib/components/cabinet.component.ts
-
24cabinet/projects/cabinet/src/lib/components/helpers/delete-dialog/cabinet-delete-dialog.component.css
-
25cabinet/projects/cabinet/src/lib/components/helpers/delete-dialog/cabinet-delete-dialog.component.html
-
34cabinet/projects/cabinet/src/lib/components/helpers/delete-dialog/cabinet-delete-dialog.component.ts
-
24cabinet/projects/cabinet/src/lib/components/helpers/error-dialog/cabinet-error-dialog.component.css
-
19cabinet/projects/cabinet/src/lib/components/helpers/error-dialog/cabinet-error-dialog.component.html
-
23cabinet/projects/cabinet/src/lib/components/helpers/error-dialog/cabinet-error-dialog.component.ts
-
37cabinet/projects/cabinet/src/lib/components/helpers/file-editor-dialog/cabinet-file-editor-dialog.component.css
-
30cabinet/projects/cabinet/src/lib/components/helpers/file-editor-dialog/cabinet-file-editor-dialog.component.html
-
69cabinet/projects/cabinet/src/lib/components/helpers/file-editor-dialog/cabinet-file-editor-dialog.component.ts
-
24cabinet/projects/cabinet/src/lib/components/helpers/new-folder-dialog/cabinet-new-folder-dialog.component.css
-
31cabinet/projects/cabinet/src/lib/components/helpers/new-folder-dialog/cabinet-new-folder-dialog.component.html
-
31cabinet/projects/cabinet/src/lib/components/helpers/new-folder-dialog/cabinet-new-folder-dialog.component.ts
-
107cabinet/projects/cabinet/src/lib/modules/material/material.module.ts
-
182cabinet/projects/cabinet/src/lib/services/api.service.ts
-
8cabinet/projects/cabinet/src/lib/services/cabinet.service.ts
-
7cabinet/projects/cabinet/src/module.json
-
115cabinet/projects/cabinet/src/module.py
-
1cabinet/projects/cabinet/src/module.svg
-
7cabinet/projects/cabinet/src/public-api.ts
-
25cabinet/projects/cabinet/tsconfig.lib.json
-
6cabinet/projects/cabinet/tsconfig.lib.prod.json
-
17cabinet/projects/cabinet/tsconfig.spec.json
-
17cabinet/projects/cabinet/tslint.json
-
34cabinet/tsconfig.json
-
79cabinet/tslint.json
-
111create.sh
-
13evilportal/.editorconfig
-
46evilportal/.gitignore
-
47evilportal/angular.json
-
42evilportal/build.sh
-
19244evilportal/package-lock.json
-
53evilportal/package.json
-
7evilportal/projects/evilportal/ng-package.json
-
11evilportal/projects/evilportal/package.json
-
49evilportal/projects/evilportal/src/assets/api/API.php
-
131evilportal/projects/evilportal/src/assets/api/Portal.php
-
10evilportal/projects/evilportal/src/assets/api/index.php
-
52evilportal/projects/evilportal/src/assets/configs/nginx.conf
-
311evilportal/projects/evilportal/src/assets/configs/php.ini
-
19evilportal/projects/evilportal/src/assets/configs/php7-fpm
-
122evilportal/projects/evilportal/src/assets/configs/php7-fpm.conf
-
392evilportal/projects/evilportal/src/assets/configs/www.conf
-
46evilportal/projects/evilportal/src/assets/evilportal.sh
-
1evilportal/projects/evilportal/src/assets/permanentclients.txt
-
4evilportal/projects/evilportal/src/assets/skeleton/.disable
-
4evilportal/projects/evilportal/src/assets/skeleton/.enable
-
33evilportal/projects/evilportal/src/assets/skeleton/MyPortal.php
-
55evilportal/projects/evilportal/src/assets/skeleton/helper.php
-
39evilportal/projects/evilportal/src/assets/skeleton/index.php
-
4evilportal/projects/evilportal/src/assets/skeleton/portalinfo.json
-
4evilportal/projects/evilportal/src/assets/targeted_skeleton/.disable
-
4evilportal/projects/evilportal/src/assets/targeted_skeleton/.enable
-
33evilportal/projects/evilportal/src/assets/targeted_skeleton/MyPortal.php
-
35evilportal/projects/evilportal/src/assets/targeted_skeleton/default.php
-
54evilportal/projects/evilportal/src/assets/targeted_skeleton/helper.php
-
88evilportal/projects/evilportal/src/assets/targeted_skeleton/index.php
-
34evilportal/projects/evilportal/src/assets/targeted_skeleton/portalinfo.json
-
76evilportal/projects/evilportal/src/lib/components/evilportal.component.css
-
235evilportal/projects/evilportal/src/lib/components/evilportal.component.html
-
493evilportal/projects/evilportal/src/lib/components/evilportal.component.ts
-
12evilportal/projects/evilportal/src/lib/components/helpers/confirmation-dialog/confirmation-dialog.component.css
-
19evilportal/projects/evilportal/src/lib/components/helpers/confirmation-dialog/confirmation-dialog.component.html
-
33evilportal/projects/evilportal/src/lib/components/helpers/confirmation-dialog/confirmation-dialog.component.ts
-
37evilportal/projects/evilportal/src/lib/components/helpers/edit-file-dialog/edit-file-dialog.component.css
-
32evilportal/projects/evilportal/src/lib/components/helpers/edit-file-dialog/edit-file-dialog.component.html
-
96evilportal/projects/evilportal/src/lib/components/helpers/edit-file-dialog/edit-file-dialog.component.ts
-
3evilportal/projects/evilportal/src/lib/components/helpers/error-dialog/error-dialog.component.css
-
19evilportal/projects/evilportal/src/lib/components/helpers/error-dialog/error-dialog.component.html
-
25evilportal/projects/evilportal/src/lib/components/helpers/error-dialog/error-dialog.component.ts
@ -0,0 +1,9 @@ |
|||
__pycache__/ |
|||
.DS_Store |
|||
*.pyc |
|||
venv |
|||
.idea/* |
|||
replacer.sh |
|||
create.sh |
|||
*.tar.gz |
|||
node_modules/ |
@ -0,0 +1 @@ |
|||
from pineapple.helpers.helpers import * |
@ -0,0 +1,36 @@ |
|||
import subprocess |
|||
from typing import List |
|||
|
|||
|
|||
def grep_output(command: str, grep_text: str, grep_options: List[str] = None) -> bytes: |
|||
""" |
|||
Run a command and pipe it to grep for some output. |
|||
The output is returned. |
|||
|
|||
For example this command: |
|||
ps -aux | grep pineap |
|||
Looks like this: |
|||
grep_output('ps -aux', 'pineap') |
|||
|
|||
:param command: The initial command to run. |
|||
:param grep_text: The text to grep for |
|||
:param grep_options: Any options to be passed to grep. |
|||
:return: The output as bytes. |
|||
""" |
|||
cmd = command.split(' ') |
|||
|
|||
grep_options = grep_options if grep_options else [] |
|||
grep = ['grep'] + grep_options |
|||
grep.append(grep_text) |
|||
|
|||
ps = subprocess.Popen(cmd, stdout=subprocess.PIPE) |
|||
return subprocess.run(grep, stdin=ps.stdout, capture_output=True).stdout |
|||
|
|||
|
|||
def check_for_process(process_name) -> bool: |
|||
""" |
|||
Check if a process is running by its name. |
|||
:param process_name: The name of the process to look for |
|||
:return: True if the process is running, False if it is not. |
|||
""" |
|||
return subprocess.run(['pgrep', '-l', process_name], capture_output=True).stdout != b'' |
@ -0,0 +1,16 @@ |
|||
import json |
|||
|
|||
|
|||
def json_to_bytes(message) -> bytes: |
|||
""" |
|||
json deserialize a message and then decode it. |
|||
Use this to convert your json message to bytes before publishing it over the socket. |
|||
:param message: A json serializable list or a dict. |
|||
:return: bytes |
|||
""" |
|||
if not (type(message) is list or type(message) is dict): |
|||
raise TypeError(f'Expected a list or dict but got {type(message)} instead.') |
|||
|
|||
d = json.dumps(message) |
|||
|
|||
return d.encode('utf-8') |
@ -0,0 +1,34 @@ |
|||
from typing import Optional, List |
|||
from logging import Logger |
|||
import urllib.request |
|||
import ssl |
|||
import os |
|||
|
|||
|
|||
def check_for_internet(url: str = 'https://downloads.hak5.org/internet', timout: int = 10, logger: Optional[Logger] = None) -> bool: |
|||
""" |
|||
Attempt to connect to a given url. If a connection was established then assume there is an internet connection. |
|||
If the connection fails to establish or times out then assume there is not internet. |
|||
:param url: The url to attempt to connect to. Default is https://downloads.hak5.org/internet. |
|||
:param timout: The amount of time in seconds to wait before giving up. Default is 10. |
|||
:param logger: An optional instance of Logger use to log any exceptions while trying to establish a connection. |
|||
:return: True if there is an internet connection, false if there is not |
|||
""" |
|||
try: |
|||
if url[:5] == 'https': |
|||
context = ssl.SSLContext() |
|||
urllib.request.urlopen(url, timeout=timout, context=context) |
|||
else: |
|||
urllib.request.urlopen(url, timeout=timout) |
|||
return True |
|||
except Exception as e: |
|||
if logger: |
|||
logger.error(e) |
|||
return False |
|||
|
|||
|
|||
def get_interfaces() -> List[str]: |
|||
""" |
|||
:return: A list of network interfaces available on the device. |
|||
""" |
|||
return os.listdir('/sys/class/net/') |
@ -0,0 +1,42 @@ |
|||
import socket |
|||
|
|||
from pineapple.helpers import json_to_bytes |
|||
|
|||
|
|||
INFO = 0 |
|||
WARN = 1 |
|||
ERROR = 2 |
|||
OTHER = 3 |
|||
|
|||
|
|||
def send_notification(message: str, module_name: str, level: int = INFO) -> bool: |
|||
""" |
|||
Send a notification over the WiFi Pineapples notification socket |
|||
|
|||
:param message: Notification message |
|||
:param module_name: The name of the module the notification is from. |
|||
:param level: Notification level |
|||
:return: bool |
|||
""" |
|||
|
|||
notify_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) |
|||
notify_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) |
|||
notify_socket_path = '/tmp/notifications.sock' |
|||
|
|||
module_notification = {'level': level, 'message': message, 'module_name': module_name} |
|||
socket_message = json_to_bytes(module_notification) |
|||
status = True |
|||
|
|||
try: |
|||
notify_socket.connect(notify_socket_path) |
|||
except ValueError: |
|||
return False |
|||
|
|||
try: |
|||
notify_socket.sendall(socket_message) |
|||
except ValueError: |
|||
status = False |
|||
|
|||
notify_socket.close() |
|||
|
|||
return status |
@ -0,0 +1,167 @@ |
|||
from logging import Logger |
|||
from typing import Optional, Tuple, List, Union |
|||
import subprocess |
|||
import os |
|||
|
|||
from pineapple.helpers.network_helpers import check_for_internet |
|||
from pineapple.jobs.job import Job |
|||
|
|||
|
|||
def update_repository(logger: Optional[Logger] = None) -> Tuple[bool, str]: |
|||
""" |
|||
Update the opkg package repository. |
|||
:param logger: An optional instance of logger to log output from opkg as debug. |
|||
:return: True if the update was successful, False if it was not. |
|||
""" |
|||
if not check_for_internet(logger=logger): |
|||
return False, 'Could not connect to internet.' |
|||
|
|||
out = subprocess.run(['opkg', 'update']) |
|||
|
|||
if logger: |
|||
logger.debug(out.stdout) |
|||
|
|||
if out.returncode == 0: |
|||
return True, 'Success' |
|||
else: |
|||
return False, f'Opkg update failed with code {out.returncode}' |
|||
|
|||
|
|||
def check_if_installed(package: str, logger: Optional[Logger] = None) -> bool: |
|||
""" |
|||
Check if a package is already installed via opkg. |
|||
:param package: The name of the package to search for. |
|||
:param logger: An optional instance of logger to log output from opkg as debug. |
|||
:return: True if the package is installed, False if it is not. |
|||
""" |
|||
out = subprocess.run(['opkg', 'status', package], capture_output=True) |
|||
|
|||
if logger: |
|||
logger.debug(out.stdout) |
|||
|
|||
return out.stdout != b'' and out.returncode == 0 |
|||
|
|||
|
|||
def install_dependency(package: str, logger: Optional[Logger] = None, skip_repo_update: bool = False) -> [bool, str]: |
|||
""" |
|||
Install a package via opkg if its not currently installed. |
|||
:param package: The name of the package to install. |
|||
:param logger: An optional instance of logger to log output from opkg as debug. |
|||
:param skip_repo_update: True to skip running `opkg update`. An internet connection will still be checked for. |
|||
:return: True if the package installed successfully, False if it did not. |
|||
""" |
|||
if check_if_installed(package, logger): |
|||
return True, 'Package is already installed' |
|||
|
|||
if not skip_repo_update: |
|||
update_successful, msg = update_repository(logger) |
|||
if not update_successful: |
|||
return False, msg |
|||
else: |
|||
has_internet = check_for_internet() |
|||
if not has_internet: |
|||
return False, 'Could not connect to internet.' |
|||
|
|||
out = subprocess.run(['opkg', 'install', package], capture_output=True) |
|||
|
|||
if logger: |
|||
logger.debug(out.stdout) |
|||
|
|||
is_installed = check_if_installed(package, logger) |
|||
message = 'Package installed successfully' if is_installed else 'Unable to install package.' |
|||
return is_installed, message |
|||
|
|||
|
|||
def uninstall_dependency(package: str, logger: Optional[Logger] = None) -> [bool, str]: |
|||
""" |
|||
Uninstall a package via opkg if its currently installed. |
|||
:param package: The name of the package to uninstall. |
|||
:param logger: An optional instance of logger to log output from opkg as debug. |
|||
:return: True if the package uninstalled successfully, False if it did not. |
|||
""" |
|||
if not check_if_installed(package, logger): |
|||
return True, 'Package is not installed' |
|||
|
|||
out = subprocess.run(['opkg', 'remove', package], capture_output=True) |
|||
|
|||
if logger: |
|||
logger.debug(out.stdout) |
|||
|
|||
is_installed = check_if_installed(package, logger) |
|||
message = 'Package uninstalled successfully' if not is_installed else 'Unable to uninstall package' |
|||
return not is_installed, message |
|||
|
|||
|
|||
class OpkgJob(Job[bool]): |
|||
""" |
|||
A job to be used with the background JobManager that installs or uninstalls dependencies. |
|||
""" |
|||
|
|||
def __init__(self, package: Union[str, List[str]], install: bool): |
|||
""" |
|||
:param package: The name of the package or list of packages to be installed/uninstalled |
|||
:param install: True if installing the package, False if uninstalling. |
|||
""" |
|||
super().__init__() |
|||
self.package: Union[str, List[str]] = package |
|||
self.install = install |
|||
|
|||
def _install_or_remove(self, pkg: str, logger: Logger, skip_repo_update: bool = False) -> bool: |
|||
""" |
|||
If `self.install` is True: |
|||
Call `install_dependency` and pass the package and logger to it. |
|||
If the result of `install_dependency` is False then set `self.error` equal to the message from the call. |
|||
return the True if `install_dependency` returned True, otherwise return False. |
|||
|
|||
If `self.install` is False: |
|||
Call `uninstall_dependency` and pass the package and logger to it. |
|||
If the result of `uninstall_dependency` is False then set `self.error` equal to the message from the call. |
|||
return the True if `uninstall_dependency` returned True, otherwise return False |
|||
|
|||
:param pkg: The name of the package to install/uninstall. |
|||
:param logger: An instance of a logger to provide insight. |
|||
:return: True if call there were no errors, otherwise False. |
|||
:return: |
|||
""" |
|||
if self.install: |
|||
success, msg = install_dependency(package=pkg, logger=logger, skip_repo_update=skip_repo_update) |
|||
if not success: |
|||
if not self.error: |
|||
self.error = msg |
|||
else: |
|||
self.error += f'{msg}\n' |
|||
return success |
|||
else: |
|||
success, msg = uninstall_dependency(package=pkg, logger=logger) |
|||
if not success: |
|||
if not self.error: |
|||
self.error = msg |
|||
else: |
|||
self.error += f'{msg}\n' |
|||
return success |
|||
|
|||
def do_work(self, logger: Logger) -> bool: |
|||
""" |
|||
If `self.package` is a List: |
|||
Attempt to install each every package in the list. If a single package fails to install then this method |
|||
will return False. |
|||
|
|||
:param logger: An instance of a logger to provide insight. |
|||
:return: True if call there were no errors, otherwise False. |
|||
""" |
|||
if isinstance(self.package, list): |
|||
update_repository(logger) |
|||
results = [self._install_or_remove(pkg, logger, True) for pkg in self.package] |
|||
return False not in results |
|||
elif isinstance(self.package, str): |
|||
return self._install_or_remove(self.package, logger) |
|||
else: |
|||
raise TypeError(f'Package is expected to be a list of strings or a single string. Got {type(self.package)} instead.') |
|||
|
|||
def stop(self): |
|||
""" |
|||
Kill the opkg process if it is running. |
|||
:return: |
|||
""" |
|||
if not self.is_complete: |
|||
os.system('killall -9 opkg') |
@ -0,0 +1,3 @@ |
|||
from pineapple.jobs.job import Job |
|||
from pineapple.jobs.job_runner import JobRunner |
|||
from pineapple.jobs.job_manager import JobManager |
@ -0,0 +1,42 @@ |
|||
from typing import TypeVar, Generic, Optional |
|||
from logging import Logger |
|||
import abc |
|||
|
|||
TResult = TypeVar('TResult') |
|||
|
|||
|
|||
class Job(Generic[TResult]): |
|||
|
|||
def __init__(self): |
|||
self.is_complete: bool = False |
|||
self.result: Optional[TResult] = None |
|||
self.error: Optional[str] = None |
|||
|
|||
@property |
|||
def was_successful(self) -> bool: |
|||
""" |
|||
Checks if the job complete without an error. |
|||
If the job has not completed or if it complete with no errors return True. |
|||
If the job completed with an error then return False. |
|||
:return: True if the job completed without an error, otherwise False |
|||
""" |
|||
return self.error is None and self.is_complete |
|||
|
|||
@abc.abstractmethod |
|||
def do_work(self, logger: Logger) -> TResult: |
|||
""" |
|||
Override this method and implement a long running job. |
|||
This function should return whatever the result of the work is. |
|||
|
|||
:param logger: An instance of a logger that may be used to provide insight. |
|||
:return: The result of the work. |
|||
""" |
|||
raise NotImplementedError() |
|||
|
|||
@abc.abstractmethod |
|||
def stop(self): |
|||
""" |
|||
Override this method and implement a way to stop the running jub. |
|||
:return: |
|||
""" |
|||
raise NotImplementedError() |
@ -0,0 +1,162 @@ |
|||
from typing import Dict, Optional, List, Callable, Tuple, Union |
|||
from uuid import uuid4 |
|||
|
|||
from pineapple.modules.module import Module |
|||
from pineapple.modules.request import Request |
|||
from pineapple.jobs.job import Job |
|||
from pineapple.jobs.job_runner import JobRunner |
|||
from pineapple.logger import * |
|||
|
|||
|
|||
class JobManager: |
|||
|
|||
def __init__(self, name: str, log_level: int = logging.ERROR, module: Optional[Module] = None): |
|||
""" |
|||
:param name: The name of the job manager. |
|||
:param log_level: Optional level for logging. Default is ERROR |
|||
:param module: Optional instance of Module. If given some action and shutdown handlers will be registered. |
|||
Checkout `_setup_with_module` for more details. |
|||
""" |
|||
self.name = name |
|||
self.logger = get_logger(name, log_level) |
|||
self.jobs: Dict[str, JobRunner] = {} |
|||
self._setup_with_module(module) |
|||
|
|||
def get_job(self, job_id: str, remove_if_complete: bool = True) -> Optional[Job]: |
|||
""" |
|||
Attempt to get a job by its id. If the job_id doesn't exist then None is returned. |
|||
If `remove_if_complete` is True the job will be deleted from memory only if it is completed. |
|||
This is the default behavior to prevent JobManager from tacking up unnecessary memory. |
|||
|
|||
:param job_id: The id of the job to find. |
|||
:param remove_if_complete: True to delete the job from memory after its complete. (Default: True) |
|||
:return: an instance of Job if found, else None |
|||
""" |
|||
job_runner = self.jobs.get(job_id) |
|||
|
|||
if not job_runner: |
|||
self.logger.debug(f'No job found matching id {job_id}.') |
|||
return None |
|||
|
|||
job = job_runner.job |
|||
|
|||
if remove_if_complete and job.is_complete: |
|||
self.logger.debug(f'Removing completed job: {job_id}.') |
|||
self.remove_job(job_id) |
|||
|
|||
return job |
|||
|
|||
def prune_completed_jobs(self): |
|||
""" |
|||
Removes all completed jobs from memory. |
|||
""" |
|||
self.logger.debug('Pruning jobs...') |
|||
|
|||
running_jobs: Dict[str, JobRunner] = {} |
|||
current_jobs = len(self.jobs) |
|||
|
|||
for job_id, job in self.jobs: |
|||
if job.is_complete: |
|||
self.remove_job(job_id) |
|||
|
|||
self.logger.debug(f'Pruned {current_jobs - len(running_jobs)} jobs.') |
|||
|
|||
def remove_job(self, job_id: str): |
|||
""" |
|||
Remove a job from memory based on its id. |
|||
This will remove the job regardless of its completion status. |
|||
|
|||
:param job_id: The id of the job to delete. |
|||
:return: |
|||
""" |
|||
del self.jobs[job_id] |
|||
self.logger.debug(f'Removed job {job_id}.') |
|||
|
|||
def execute_job(self, job: Job, callbacks: List[Callable[[Job], None]] = None) -> str: |
|||
""" |
|||
Assign an id to a job and execute it in a background thread. |
|||
The id will be returned and the job can be tracked by calling `get_job` and providing it the id. |
|||
|
|||
:param job: an instance of Job to start running. |
|||
:param callbacks: An optional list of functions that take `job` as a parameter to be called when completed. |
|||
These will be called regardless if `job` raises an exception or not. |
|||
:return: The id of the running job. |
|||
""" |
|||
job_id = str(uuid4()) |
|||
self.logger.debug(f'Assign job the id: {job_id}') |
|||
|
|||
job_runner = JobRunner(job, self.logger, callbacks) |
|||
self.jobs[job_id] = job_runner |
|||
|
|||
self.logger.debug('Starting job...') |
|||
job_runner.setDaemon(True) |
|||
job_runner.start() |
|||
self.logger.debug('Job started!') |
|||
|
|||
return job_id |
|||
|
|||
def stop_job(self, job: Optional[Job] = None, job_id: Optional[str] = None): |
|||
""" |
|||
Call the `stop` method on a job. |
|||
Either an instance of the Job to stop or id of the job is expected. |
|||
The job will not automatically be removed from memory on completion. |
|||
|
|||
:param job: An instance of Job |
|||
:param job_id: The id of te job to stop |
|||
""" |
|||
if not job and not job_id: |
|||
raise Exception('A job or job_id is expected.') |
|||
|
|||
if not job: |
|||
job = self.get_job(job_id, remove_if_complete=False) |
|||
|
|||
if isinstance(job, Job): |
|||
job.stop() |
|||
|
|||
def _setup_with_module(self, module: Optional[Module]): |
|||
""" |
|||
If module is not None and is an instance of Module then register the following action handlers: |
|||
action: `poll_job` | handler: `self.poll_job` |
|||
|
|||
And register _on_module_shutdown as a shutdown handler. |
|||
|
|||
:param module: an instance of Module |
|||
""" |
|||
if not module or not isinstance(module, Module): |
|||
return |
|||
|
|||
module.register_action_handler('poll_job', self._poll_job) |
|||
module.register_shutdown_handler(self._on_module_shutdown) |
|||
|
|||
def _on_module_shutdown(self, signal: int): |
|||
""" |
|||
A shutdown handler to be registered is `self.module` is not None. |
|||
This will stop all currently running jobs. |
|||
|
|||
:param signal: The signal given |
|||
""" |
|||
for job_id, runner in self.jobs.items(): |
|||
self.stop_job(job_id=job_id) |
|||
|
|||
def _poll_job(self, request: Request) -> Union[dict, Tuple[str, bool]]: |
|||
""" |
|||
A module action handler to be used for checking the status of a background job. |
|||
The request object must contain string `job_id` which is used to lookup the running job. |
|||
Optionally, the request can contain boolean `remove_if_complete`. If this is True then the job will |
|||
be deleted from memory if it is completed. If this value is False then the job will remain until manually deleted. |
|||
This default value is True. |
|||
|
|||
:param request: An instance of Request |
|||
""" |
|||
job_id = request.__dict__.get('job_id') |
|||
remove_if_complete = request.__dict__.get('remove_if_complete', True) |
|||
|
|||
if not job_id: |
|||
return 'job_id was not found in request.', False |
|||
|
|||
job = self.get_job(job_id, remove_if_complete) |
|||
|
|||
if not job: |
|||
return 'No job found by that id.', False |
|||
|
|||
return {'is_complete': job.is_complete, 'result': job.result, 'job_error': job.error} |
@ -0,0 +1,53 @@ |
|||
from typing import Callable, List |
|||
from threading import Thread |
|||
from logging import Logger |
|||
|
|||
from pineapple.jobs.job import Job |
|||
|
|||
|
|||
class JobRunner(Thread): |
|||
|
|||
def __init__(self, job: Job, logger: Logger, callbacks: List[Callable[[Job], None]] = None): |
|||
""" |
|||
:param job: An instance of Job to run on a background thread. |
|||
:param logger: An instance of Logger to provide insight. |
|||
:param callbacks: An optional list of functions that take `job` as a parameter to be called when completed. |
|||
These will be called regardless if `job` raises an exception or not. |
|||
""" |
|||
super().__init__() |
|||
self.logger = logger |
|||
self.job: Job = job |
|||
self.running: bool = False |
|||
self._callbacks: List[Callable[[Job], None]] = callbacks if callbacks else list() |
|||
|
|||
def run(self): |
|||
""" |
|||
Call the `do_work` method on `self.job` and assign the results to `self.job.result`. |
|||
If an exception is raised by the `do_work` method, catch it and set `self.job.error` equal to it. |
|||
After `do_work` finishes set `self.job.is_complete` equal to True. |
|||
""" |
|||
self.running = True |
|||
try: |
|||
self.job.result = self.job.do_work(self.logger) |
|||
except Exception as e: |
|||
self.logger.error(f'Running job encountered a {type(e)} error: {e}') |
|||
self.job.error = str(e) |
|||
|
|||
self.job.is_complete = True |
|||
|
|||
try: |
|||
if isinstance(self._callbacks, list) and len(self._callbacks) > 0: |
|||
for callback in self._callbacks: |
|||
callback(self.job) |
|||
except Exception as e: |
|||
self.logger.error(f'Callback failed with a {type(e)} error: {e}') |
|||
|
|||
self.running = False |
|||
|
|||
def stop(self): |
|||
""" |
|||
Call the `stop` method on `self.job` if the job is running. |
|||
:return: |
|||
""" |
|||
if self.running: |
|||
self.job.stop() |
@ -0,0 +1,27 @@ |
|||
from pineapple.logger.pretty_formatter import PrettyFormatter |
|||
|
|||
from logging.handlers import RotatingFileHandler |
|||
from logging import Logger |
|||
import logging |
|||
|
|||
|
|||
def get_logger(name: str, level: int, log_to_file: bool = True, console_logger_level: int = logging.DEBUG) -> Logger: |
|||
logger = logging.getLogger(name) |
|||
logger.setLevel(level) |
|||
|
|||
if logger.hasHandlers(): |
|||
logger.handlers.clear() |
|||
|
|||
if log_to_file: |
|||
log_format = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s (%(filename)s:%(lineno)d)") |
|||
file_handler = RotatingFileHandler(f'/tmp/modules/{name}.log', maxBytes=1024*1024) |
|||
file_handler.setFormatter(log_format) |
|||
file_handler.setLevel(level) |
|||
logger.addHandler(file_handler) |
|||
|
|||
if level <= console_logger_level: |
|||
console_logger = logging.StreamHandler() |
|||
console_logger.setFormatter(PrettyFormatter()) |
|||
logger.addHandler(console_logger) |
|||
|
|||
return logger |
@ -0,0 +1,25 @@ |
|||
import logging |
|||
|
|||
|
|||
class PrettyFormatter(logging.Formatter): |
|||
|
|||
grey = "\x1b[38;21m" |
|||
yellow = "\x1b[33;21m" |
|||
red = "\x1b[31;21m" |
|||
bold_red = "\x1b[31;1m" |
|||
reset = "\x1b[0m" |
|||
light_blue = "\x1b[1;34m" |
|||
format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s (%(filename)s:%(lineno)d)" |
|||
|
|||
FORMATS = { |
|||
logging.DEBUG: grey + format + reset, |
|||
logging.INFO: light_blue + format + reset, |
|||
logging.WARNING: yellow + format + reset, |
|||
logging.ERROR: red + format + reset, |
|||
logging.CRITICAL: bold_red + format + reset |
|||
} |
|||
|
|||
def format(self, record): |
|||
log_fmt = self.FORMATS.get(record.levelno) |
|||
formatter = logging.Formatter(log_fmt) |
|||
return formatter.format(record) |
@ -0,0 +1,2 @@ |
|||
from pineapple.modules.module import Module |
|||
from pineapple.modules.request import Request |
@ -0,0 +1,364 @@ |
|||
import os |
|||
import socket |
|||
import json |
|||
import logging |
|||
import signal |
|||
from typing import Tuple, Any, Callable, Optional, Dict, Union, List |
|||
|
|||
from pineapple.logger import get_logger |
|||
from pineapple.modules.request import Request |
|||
from pineapple.helpers import json_to_bytes |
|||
import pineapple.helpers.notification_helpers as notifier |
|||
|
|||
|
|||
class Module: |
|||
|
|||
def __init__(self, name: str, log_level: int = logging.WARNING): |
|||
""" |
|||
:param name: The name of the module. Example `cabinet` |
|||
:param log_level: The level of logging you wish to show. Default WARNING |
|||
""" |
|||
self.logger = get_logger(name, log_level) # logger for feedback. |
|||
self.name = name # the name of the module |
|||
|
|||
self.logger.debug(f'Initializing module {name}.') |
|||
|
|||
# A list of functions to called when module is started. |
|||
self._startup_handlers: List[Callable[[], None]] = [] |
|||
|
|||
# A list of functions to be called when module is stopped. |
|||
self._shutdown_handlers: List[Callable[[int], None]] = [] |
|||
|
|||
# A dictionary mapping an action to a function. |
|||
self._action_handlers: Dict[str, Callable[[Request], Union[Any, Tuple[bool, Any]]]] = {} |
|||
|
|||
self._running: bool = False # set to False to stop the module loop |
|||
|
|||
# api requests will be received over this socket |
|||
self._module_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) |
|||
self._module_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) |
|||
self._module_socket_path = f'/tmp/modules/{name}.sock' # apth to the socket |
|||
self._buffer_size = 10485760 |
|||
|
|||
# if the socket already exists attempt to delete it. |
|||
try: |
|||
os.unlink(self._module_socket_path) |
|||
except OSError: |
|||
if os.path.exists(self._module_socket_path): |
|||
self.logger.error('Could not remove existing socket!') |
|||
raise FileExistsError('Could not remove existing socket!') |
|||
|
|||
# If a SIGINT is received preform a clean shutdown by calling `shutdown()` |
|||
signal.signal(signal.SIGINT, self.shutdown) |
|||
signal.signal(signal.SIGTERM, self.shutdown) |
|||
|
|||
def _receive(self) -> Optional[dict]: |
|||
""" |
|||
Receive data over a socket and attempt to json deserialize it. |
|||
If the deserialization fails, None will be returned |
|||
:return: A dictionary containing the data received over the socket or None if json deserialization fails. |
|||
""" |
|||
connection, _ = self._module_socket.accept() |
|||
data = connection.recv(self._buffer_size) |
|||
decoded_data = data.decode('utf-8') |
|||
|
|||
try: |
|||
return json.loads(decoded_data) |
|||
except ValueError: |
|||
self.logger.warning('Non-JSON Received') |
|||
|
|||
return None |
|||
|
|||
def _publish(self, message: bytes): |
|||
""" |
|||
Publish a message `message` to over `_module_socket`. |
|||
Call this method to respond to a request. |
|||
:param message: Bytes of a message that should be sent |
|||
:return: None |
|||
""" |
|||
self.logger.debug('Accepting on module socket') |
|||
connection, _ = self._module_socket.accept() |
|||
|
|||
try: |
|||
self.logger.debug(f'Sending response {str(message, "utf-8")}') |
|||
connection.sendall(message) |
|||
except ValueError: |
|||
self.logger.error('Could not send response!') |
|||
|
|||
def _handle_request(self, request: Request): |
|||
""" |
|||
Attempt to find an handle for the requests actions and call it. |
|||
If there is no action registered for the request `request`, an error will be sent back over `module_socket`. |
|||
|
|||
If there is a handler registered the following will happen: |
|||
* the action handler will be called |
|||
* if the action handler returns an error, an error will be sent back over `module_socket` |
|||
* if the action handler returns success, the data will be sent back over `module_socket` |
|||
:param request: The request instance to handle |
|||
:return: None |
|||
""" |
|||
handler: Callable[[Request], Union[Any, Tuple[Any, bool]]] = self._action_handlers.get(request.action) |
|||
|
|||
if not handler: |
|||
self._publish(json_to_bytes({'error': f'No action handler registered for action {request.action}'})) |
|||
self.logger.error(f'No action handler registered for action {request.action}') |
|||
return |
|||
|
|||
try: |
|||
self.logger.debug(f'Calling handler for action {request.action} and passing {request.__dict__}') |
|||
result = handler(request) |
|||
except Exception as e: |
|||
self.logger.error(f'Handler raised exception: {e}') |
|||
self._publish(json_to_bytes({'error': f'Handler raised exception: {e}'})) |
|||
return |
|||
|
|||
if isinstance(result, tuple): |
|||
if len(result) > 2: |
|||
self.logger.error(f'Action handler `{request.action}` returned to many values.') |
|||
self._publish(json_to_bytes({'error': f'Action handler `{request.action}` returned to many values.'})) |
|||
return |
|||
|
|||
if not isinstance(result[1], bool): |
|||
self.logger.error(f'{request.action}: second value expected to be a bool but got {type(result[1])} instead.') |
|||
self._publish(json_to_bytes({ |
|||
'error': f'{request.action}: second value expected to be a bool but got {type(result[1])} instead.' |
|||
})) |
|||
return |
|||
|
|||
data, success = result |
|||
else: |
|||
success = True |
|||
data = result |
|||
|
|||
if success: |
|||
response_dict = {'payload': data} |
|||
else: |
|||
response_dict = {'error': data} |
|||
|
|||
message_bytes = json_to_bytes(response_dict) |
|||
|
|||
# if the message is to big to be sent over the socket - return an error instead. |
|||
if len(message_bytes) > self._buffer_size: |
|||
self.logger.error(f'Response of {len(message_bytes)} bytes exceeds limit of {self._buffer_size}') |
|||
message_bytes = json_to_bytes({ |
|||
'error': 'Response of {len(message_bytes)} bytes exceeds limit of {self._buffer_size}' |
|||
}) |
|||
|
|||
self._publish(message_bytes) |
|||
|
|||
def shutdown(self, sig=None, frame=None): |
|||
""" |
|||
Attempt to clean shutdown the module. |
|||
If your module has anything it needs to close or otherwise cleanup upon shutdown, please override this |
|||
and do what you need to here. Be sure you call `super.shutdown()` in your new implementation. |
|||
|
|||
This method may also be called to handle signals such as SIGINT. If it was called as a signal handler the |
|||
signal `sig` and frame `frame` will be passed into this method. |
|||
:param sig: Optional signal that triggered a signal handler |
|||
:param frame: Optional frame |
|||
:return: None |
|||
""" |
|||
|
|||
self.logger.debug(f'Calling {len(self._shutdown_handlers)} shutdown handlers.') |
|||
try: |
|||
for handler in self._shutdown_handlers: |
|||
handler(sig) |
|||
except Exception as e: |
|||
self.logger.warning(f'Shutdown handler raised an exception: {str(e)}') |
|||
|
|||
try: |
|||
os.unlink(f'/tmp/modules/{self.name}.sock') |
|||
os.unlink(f'/tmp/modules/{self.name}.pid') |
|||
except Exception as e: |
|||
self.logger.warning(f'Error deleting socket or pid file: {str(e)}') |
|||
|
|||
self.logger.info(f'Shutting down module. Signal: {sig}') |
|||
self._running = False |
|||
self._module_socket.close() |
|||
|
|||
def start(self): |
|||
""" |
|||
Main loop for the module which will run as long as `_running` is True. |
|||
This will listen for data coming over `_module_socket` and deserialize it to a `Request` object. |
|||
That object is then passed to `handle_request` for further processing. |
|||
|
|||
If an exception is thrown, this loop will stop working and attempt to do a clean shutdown of the module by |
|||
calling `shutdown`. |
|||
:return: None |
|||
""" |
|||
self.logger.info('Starting module...') |
|||
|
|||
self.logger.debug(f'Binding to socket {self._module_socket_path}') |
|||
self._module_socket.bind(self._module_socket_path) |
|||
self._module_socket.listen(1) |
|||
self.logger.debug('Listening on socket!') |
|||
|
|||
self.logger.debug(f'Calling {len(self._startup_handlers)} startup handlers.') |
|||
for handler in self._startup_handlers: |
|||
try: |
|||
handler() |
|||
except Exception as e: |
|||
self.logger.warning(f'Startup handler raised an exception: {str(e)}') |
|||
|
|||
self._running = True |
|||
while self._running: |
|||
try: |
|||
request_dict: Optional[dict] = self._receive() |
|||
if not request_dict: |
|||
self.logger.debug("Received non-json data over the socket.") |
|||
continue |
|||
|
|||
self.logger.debug('Processing request.') |
|||
request = Request() |
|||
request.__dict__ = request_dict |
|||
self._handle_request(request) |
|||
except OSError as os_error: |
|||
self.logger.warning(f'An os error occurred: {os_error}') |
|||
except Exception as e: |
|||
self.logger.critical(f'A fatal `{type(e)}` exception was thrown: {e}') |
|||
self.shutdown() |
|||
|
|||
def register_action_handler(self, action: str, handler: Callable[[Request], Union[Any, Tuple[Any, bool]]]): |
|||
""" |
|||
Manually register an function `handler` to handle an action `action`. |
|||
This function will be called anytime a request with the matching action is received. |
|||
The action handler must take a positional argument of type `Request`. This must be the first argument. |
|||
|
|||
Usage Example: |
|||
module = Module('example') |
|||
|
|||
def save_file(request: Request) -> Union[Any, Tuple[Any, bool]]: |
|||
... |
|||
|
|||
module.register_action_handler(save_file) |
|||
|
|||
:param action: The request action to handle |
|||
:param handler: A function that takes `Request` that gets called when the matching `action` is received. |
|||
""" |
|||
self._action_handlers[action] = handler |
|||
|
|||
def handles_action(self, action: str): |
|||
""" |
|||
A decorator that registers a function as an handler for a given action `action` in a request. |
|||
The decorated function is expected take an instance of `Request` as its first argument and can return either |
|||
Any or a tuple with two values - Any, bool - in that order. |
|||
|
|||
If the function does not return a tuple, The response is assumed to be successful and the returned value |
|||
will be json serialized and placed into the 'payload' of the response body. |
|||
|
|||
Example Function: |
|||
@handles_action('save_file') |
|||
def save_file(request: Request) -> str: |
|||
... |
|||
return 'Filed saved successfully!' |
|||
|
|||
Example Response: |
|||
{ "payload": "File saved successfully!" } |
|||
|
|||
If a tuple is returned, the first value in the tuple will the data sent back to the user. The second value |
|||
must be a boolean that indicates whether the function was successful (True) or not (False). If this |
|||
value is True, the data in the first index will be sent back in the response payload. |
|||
|
|||
Example Function: |
|||
@handles_action('save_file') |
|||
def save_file(request: Request) -> Tuple[str, bool]: |
|||
... |
|||
return 'Filed saved successfully!', True |
|||
|
|||
Example Response: |
|||
{ "payload": "File saved successfully!" } |
|||
|
|||
However, if this value is False, The data in the first index will be sent back as an error. |
|||
|
|||
Example Function: |
|||
@handles_action('save_file') |
|||
def save_file(request: Request) -> Tuple[str, bool]: |
|||
... |
|||
return 'There was an issue saving the file.', False |
|||
|
|||
Example Response: |
|||
{ "error": There was an issue saving the file." } |
|||
|
|||
:param action: The request action to handle |
|||
""" |
|||
def wrapper(func: Callable[[Request], Union[Any, Tuple[Any, bool]]]): |
|||
self.register_action_handler(action, func) |
|||
return func |
|||
return wrapper |
|||
|
|||
def register_shutdown_handler(self, handler: Callable[[Optional[int]], None]): |
|||
""" |
|||
Manually register a function `handler` to be called on the module shutdown lifecycle event. |
|||
This handler function must take an integer as a parameter which may be the kill signal sent to the application. |
|||
Depending on how the module is shutdown, the signal value may be None. |
|||
|
|||
Example: |
|||
module = Module('example') |
|||
|
|||
def stop_all_tasks(signal: int): |
|||
... |
|||
|
|||
module.register_shutdown_handler(stop_all_tasks) |
|||
|
|||
:param handler: A function to be called on shutdown lifecycle event. |
|||
""" |
|||
self._shutdown_handlers.append(handler) |
|||
|
|||
def on_shutdown(self): |
|||
""" |
|||
A decorator that registers a function as a shutdown handler to be called on the shutdown lifecycle event. |
|||
In the example below, the function `stop_all_tasks` will be called when the module process is terminated. |
|||
|
|||
Example: |
|||
@module.on_shutdown() |
|||
def stop_all_tasks(signal: int): |
|||
... |
|||
""" |
|||
def wrapper(func: Callable[[int], None]): |
|||
self.register_shutdown_handler(func) |
|||
return func |
|||
return wrapper |
|||
|
|||
def register_startup_handler(self, handler: Callable[[], None]): |
|||
""" |
|||
Manually register a function `handler` to be called on the module start lifecycle event. |
|||
This handler function most not take any arguments. |
|||
|
|||
Example: |
|||
module = Module('example') |
|||
|
|||
def copy_configs(): |
|||
... |
|||
|
|||
module.register_startup_handler(copy_configs) |
|||
|
|||
:param handler: |
|||
:return: |
|||
""" |
|||
self._startup_handlers.append(handler) |
|||
|
|||
def on_start(self): |
|||
""" |
|||
A decorator that registers a function as a startup handler to be called on the start lifecycle event. |
|||
In the example below, the function `copy_configs` will be called when the modules `start` method is called. |
|||
|
|||
Example: |
|||
@module.on_start() |
|||
def copy_configs(): |
|||
... |
|||
:return: |
|||
""" |
|||
def wrapper(func: Callable[[], None]): |
|||
self.register_startup_handler(func) |
|||
return func |
|||
return wrapper |
|||
|
|||
def send_notification(self, message: str, level: int) -> bool: |
|||
""" |
|||
Send a notification over the WiFi Pineapples notification socket |
|||
|
|||
:param message: Notification message |
|||
:param level: Notification level |
|||
:return: bool |
|||
""" |
|||
return notifier.send_notification(message, self.name, level) |
@ -0,0 +1,9 @@ |
|||
import json |
|||
|
|||
class Request: |
|||
def __init__(self): |
|||
self.module: str = "" |
|||
self.action: str = "" |
|||
|
|||
def __repr__(self): |
|||
return json.dumps(self.__dict__) |
@ -0,0 +1,6 @@ |
|||
# WiFi Pineapple Mark 7 Module Libraries |
|||
|
|||
This section of the GitHub repository holds the code for the Python module library that exposes helpers and frameworks to enable you to develop modules much easier. |
|||
|
|||
More language support is planned in the future, and contributions are welcome. |
|||
|
@ -0,0 +1,11 @@ |
|||
# WiFi Pineapple Mark 7 Modules |
|||
|
|||
This repository contains modules for the WiFi Pineapple Mark 7. All the community developed modules are here, and developers should create pull requests for any changes to modules, and to submit new modules. |
|||
|
|||
## Documentation |
|||
|
|||
* [WiFi Pineapple Mark 7 Modules Guide](https://docs.hak5.org/hc/en-us/articles/360052162434) |
|||
* [WiFi Pineapple Mark 7 REST API](https://docs.hak5.org/hc/en-us/articles/360049854174-WiFi-Pineapple-Mark-VII-REST-API) |
|||
* [WiFi Pineapple Mark 7 TypeScript API](https://docs.hak5.org/hc/en-us/articles/360058059233) |
|||
* [Contributing to the WiFi Pineapple Mark 7 Modules Repository](https://docs.hak5.org/hc/en-us/articles/360056213714) |
|||
|
@ -0,0 +1,13 @@ |
|||
# Editor configuration, see https://editorconfig.org |
|||
root = true |
|||
|
|||
[*] |
|||
charset = utf-8 |
|||
indent_style = space |
|||
indent_size = 4 |
|||
insert_final_newline = true |
|||
trim_trailing_whitespace = true |
|||
|
|||
[*.md] |
|||
max_line_length = off |
|||
trim_trailing_whitespace = false |
@ -0,0 +1,46 @@ |
|||
# See http://help.github.com/ignore-files/ for more about ignoring files. |
|||
|
|||
# compiled output |
|||
/dist |
|||
/tmp |
|||
/out-tsc |
|||
# Only exists if Bazel was run |
|||
/bazel-out |
|||
|
|||
# dependencies |
|||
/node_modules |
|||
|
|||
# profiling files |
|||
chrome-profiler-events*.json |
|||
speed-measure-plugin*.json |
|||
|
|||
# IDEs and editors |
|||
/.idea |
|||
.project |
|||
.classpath |
|||
.c9/ |
|||
*.launch |
|||
.settings/ |
|||
*.sublime-workspace |
|||
|
|||
# IDE - VSCode |
|||
.vscode/* |
|||
!.vscode/settings.json |
|||
!.vscode/tasks.json |
|||
!.vscode/launch.json |
|||
!.vscode/extensions.json |
|||
.history/* |
|||
|
|||
# misc |
|||
/.sass-cache |
|||
/connect.lock |
|||
/coverage |
|||
/libpeerconnection.log |
|||
npm-debug.log |
|||
yarn-error.log |
|||
testem.log |
|||
/typings |
|||
|
|||
# System Files |
|||
.DS_Store |
|||
Thumbs.db |
@ -0,0 +1,47 @@ |
|||
{ |
|||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json", |
|||
"version": 1, |
|||
"newProjectRoot": "projects", |
|||
"projects": { |
|||
"cabinet": { |
|||
"projectType": "library", |
|||
"root": "projects/cabinet", |
|||
"sourceRoot": "projects/cabinet/src", |
|||
"prefix": "lib", |
|||
"architect": { |
|||
"build": { |
|||
"builder": "@angular-devkit/build-ng-packagr:build", |
|||
"options": { |
|||
"tsConfig": "projects/cabinet/tsconfig.lib.json", |
|||
"project": "projects/cabinet/ng-package.json" |
|||
}, |
|||
"configurations": { |
|||
"production": { |
|||
"tsConfig": "projects/cabinet/tsconfig.lib.prod.json" |
|||
} |
|||
} |
|||
}, |
|||
"test": { |
|||
"builder": "@angular-devkit/build-angular:karma", |
|||
"options": { |
|||
"main": "projects/cabinet/src/test.ts", |
|||
"tsConfig": "projects/cabinet/tsconfig.spec.json", |
|||
"karmaConfig": "projects/cabinet/karma.conf.js" |
|||
} |
|||
}, |
|||
"lint": { |
|||
"builder": "@angular-devkit/build-angular:tslint", |
|||
"options": { |
|||
"tsConfig": [ |
|||
"projects/cabinet/tsconfig.lib.json", |
|||
"projects/cabinet/tsconfig.spec.json" |
|||
], |
|||
"exclude": [ |
|||
"**/node_modules/**" |
|||
] |
|||
} |
|||
} |
|||
} |
|||
}}, |
|||
"defaultProject": "cabinet" |
|||
} |
@ -0,0 +1,42 @@ |
|||
#!/bin/bash |
|||
|
|||
# Step 1: Build the Angular module |
|||
ng build --prod > /dev/null 2>&1 |
|||
RET=$? |
|||
|
|||
if [[ $RET -ne 0 ]]; then |
|||
echo "[!] Angular Build Failed: Run 'ng build --prod' to figure out why." |
|||
exit 1 |
|||
else |
|||
echo "[*] Angular Build Succeeded" |
|||
fi |
|||
|
|||
# Step 2: Copy the required files to the build output |
|||
MODULENAME=$(basename $PWD) |
|||
cp -r projects/$MODULENAME/src/module.svg dist/$MODULENAME/bundles/ |
|||
cp -r projects/$MODULENAME/src/module.json dist/$MODULENAME/bundles/ |
|||
cp -r projects/$MODULENAME/src/module.py dist/$MODULENAME/bundles/ > /dev/null 2>&1 |
|||
cp -r projects/$MODULENAME/src/module.php dist/$MODULENAME/bundles/ > /dev/null 2>&1 |
|||
cp -r projects/$MODULENAME/src/assets/ dist/$MODULENAME/bundles/ > /dev/null 2>&1 |
|||
|
|||
# Step 3: Clean up |
|||
rm -rf dist/$MODULENAME/bundles/*.map |
|||
rm -rf dist/$MODULENAME/bundles/*.min* |
|||
rm -rf bundletmp |
|||
mv dist/$MODULENAME/bundles/ bundletmp |
|||
rm -rf dist/$MODULENAME/* |
|||
mv bundletmp/* dist/$MODULENAME/ |
|||
rm -rf bundletmp |
|||
|
|||
# Step 4: Package (Optional) |
|||
if [[ $1 == "package" ]]; then |
|||
VERS=$(cat dist/$MODULENAME/module.json | grep "version" | awk '{split($0, a, ": "); gsub("\"", "", a[2]); gsub(",", "", a[2]); print a[2]}') |
|||
rm -rf $MODULENAME-$VERS.tar.gz |
|||
echo "[*] Packaging $MODULENAME (Version $VERS)" |
|||
cd dist/ |
|||
tar -pczf $MODULENAME-$VERS.tar.gz $MODULENAME |
|||
mv $MODULENAME-$VERS.tar.gz ../ |
|||
cd ../ |
|||
else |
|||
echo "[*] Skipping Packaging (Run ./build.sh package to generate)" |
|||
fi |
14727
cabinet/package-lock.json
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,52 @@ |
|||
{ |
|||
"name": "cabinet", |
|||
"version": "0.0.0", |
|||
"scripts": { |
|||
"ng": "ng", |
|||
"start": "ng serve", |
|||
"build": "ng build", |
|||
"test": "ng test", |
|||
"lint": "ng lint", |
|||
"e2e": "ng e2e" |
|||
}, |
|||
"private": true, |
|||
"dependencies": { |
|||
"@angular/animations": "~9.1.11", |
|||
"@angular/cdk": "^9.2.4", |
|||
"@angular/common": "~9.1.11", |
|||
"@angular/compiler": "~9.1.11", |
|||
"@angular/core": "~9.1.11", |
|||
"@angular/flex-layout": "^9.0.0-beta.31", |
|||
"@angular/forms": "~9.1.11", |
|||
"@angular/material": "^9.2.4", |
|||
"@angular/platform-browser": "~9.1.11", |
|||
"@angular/platform-browser-dynamic": "~9.1.11", |
|||
"@angular/router": "~9.1.11", |
|||
"rxjs": "~6.5.5", |
|||
"tslib": "^1.10.0", |
|||
"zone.js": "~0.10.2" |
|||
}, |
|||
"devDependencies": { |
|||
"@angular-devkit/build-angular": "~0.901.8", |
|||
"@angular-devkit/build-ng-packagr": "~0.901.8", |
|||
"@angular/cli": "~9.1.8", |
|||
"@angular/compiler-cli": "~9.1.11", |
|||
"@angular/language-service": "~9.1.11", |
|||
"@types/jasmine": "~3.5.10", |
|||
"@types/jasminewd2": "~2.0.3", |
|||
"@types/node": "^12.11.1", |
|||
"codelyzer": "^5.1.2", |
|||
"jasmine-core": "~3.5.0", |
|||
"jasmine-spec-reporter": "~5.0.2", |
|||
"karma": "~5.1.0", |
|||
"karma-chrome-launcher": "~3.1.0", |
|||
"karma-coverage-istanbul-reporter": "~3.0.3", |
|||
"karma-jasmine": "~3.3.1", |
|||
"karma-jasmine-html-reporter": "^1.4.0", |
|||
"ng-packagr": "^9.0.0", |
|||
"protractor": "~7.0.0", |
|||
"ts-node": "~8.10.2", |
|||
"tslint": "~6.1.2", |
|||
"typescript": "^3.6.5" |
|||
} |
|||
} |
@ -0,0 +1,7 @@ |
|||
{ |
|||
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json", |
|||
"dest": "../../dist/cabinet", |
|||
"lib": { |
|||
"entryFile": "src/public-api.ts" |
|||
} |
|||
} |
@ -0,0 +1,11 @@ |
|||
{ |
|||
"name": "cabinet", |
|||
"version": "0.0.1", |
|||
"peerDependencies": { |
|||
"@angular/common": "^8.2.14", |
|||
"@angular/core": "^8.2.14" |
|||
}, |
|||
"scripts": { |
|||
"build": "ng build --prod" |
|||
} |
|||
} |
@ -0,0 +1,43 @@ |
|||
import { NgModule } from '@angular/core'; |
|||
import {CommonModule} from '@angular/common'; |
|||
import { cabinetComponent } from './components/cabinet.component'; |
|||
import { RouterModule, Routes } from '@angular/router'; |
|||
import {FlexLayoutModule} from '@angular/flex-layout'; |
|||
import {FormsModule, ReactiveFormsModule} from '@angular/forms'; |
|||
|
|||
import {MaterialModule} from './modules/material/material.module'; |
|||
|
|||
import {CabinetDeleteDialogComponent} from './components/helpers/delete-dialog/cabinet-delete-dialog.component'; |
|||
import {CabinetErrorDialogComponent} from './components/helpers/error-dialog/cabinet-error-dialog.component'; |
|||
import {FileEditorDialogComponent} from './components/helpers/file-editor-dialog/cabinet-file-editor-dialog.component'; |
|||
import {NewFolderDialogComponent} from './components/helpers/new-folder-dialog/cabinet-new-folder-dialog.component'; |
|||
|
|||
const routes: Routes = [ |
|||
{ path: '', component: cabinetComponent } |
|||
]; |
|||
|
|||
@NgModule({ |
|||
declarations: [ |
|||
cabinetComponent, |
|||
CabinetDeleteDialogComponent, |
|||
NewFolderDialogComponent, |
|||
FileEditorDialogComponent, |
|||
CabinetErrorDialogComponent |
|||
], |
|||
imports: [ |
|||
RouterModule.forChild(routes), |
|||
MaterialModule, |
|||
CommonModule, |
|||
FormsModule, |
|||
FlexLayoutModule, |
|||
ReactiveFormsModule |
|||
], |
|||
exports: [cabinetComponent], |
|||
entryComponents: [ |
|||
CabinetDeleteDialogComponent, |
|||
NewFolderDialogComponent, |
|||
FileEditorDialogComponent, |
|||
CabinetErrorDialogComponent |
|||
], |
|||
}) |
|||
export class cabinetModule { } |
@ -0,0 +1,25 @@ |
|||
.cabinet-control-container { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: flex-start; |
|||
align-self: flex-start; |
|||
} |
|||
|
|||
.control-button { |
|||
margin-top: 16px; |
|||
margin-right: 4px; |
|||
} |
|||
|
|||
.action-button { |
|||
margin-top: 4px; |
|||
margin-right: 4px; |
|||
margin-bottom: 4px; |
|||
} |
|||
|
|||
.cabinet-loading-centered { |
|||
display: flex; |
|||
justify-content: center; |
|||
align-self: center; |
|||
margin-top: 16px; |
|||
margin-bottom: 16px; |
|||
} |
@ -0,0 +1,88 @@ |
|||
<mat-card> |
|||
<mat-card-content> |
|||
<div class="cabinet-control-container"> |
|||
<div> |
|||
<mat-card-title>Cabinet</mat-card-title> |
|||
<mat-card-subtitle>Current Directory <i>{{ currentDirectory }}</i></mat-card-subtitle> |
|||
</div> |
|||
<span fxFlex></span> |
|||
<div class="cabinet-control-container"> |
|||
<mat-spinner [diameter]="24" color="accent" class="control-button" *ngIf="isBusy"></mat-spinner> |
|||
<button mat-raised-button |
|||
color="accent" |
|||
class="control-button" |
|||
(click)="getDirectoryContents(currentDirectory, true);" |
|||
[disabled]="currentDirectory == '/' || isBusy">Back |
|||
</button> |
|||
<button mat-raised-button |
|||
color="accent" |
|||
class="control-button" |
|||
(click)="showCreateDirectory();" |
|||
[disabled]="isBusy">New Folder |
|||
</button> |
|||
<button mat-raised-button |
|||
color="accent" |
|||
class="control-button" |
|||
(click)="showEditDialog(null);" |
|||
[disabled]="isBusy">New File</button> |
|||
<button mat-raised-button |
|||
color="accent" |
|||
class="control-button" |
|||
(click)="getDirectoryContents(currentDirectory);" |
|||
[disabled]="isBusy">Refresh |
|||
</button> |
|||
</div> |
|||
</div> |
|||
|
|||
<mat-divider></mat-divider> |
|||
|
|||
<div class="cabinet-loading-centered" *ngIf="isBusy && directoryContents.length == 0"> |
|||
<i>Loading...</i> |
|||
<mat-spinner [diameter]="18" color="accent" style="margin-left: 8px"></mat-spinner> |
|||
</div> |
|||
|
|||
<div class="cabinet-loading-centered" *ngIf="!isBusy && directoryContents.length == 0"> |
|||
<span> |
|||
<p>Directory <i>{{ currentDirectory }}</i> appears to be empty</p> |
|||
<button mat-flat-button |
|||
color="accent" |
|||
style="width: 100%" |
|||
(click)="getDirectoryContents(currentDirectory, true);">Back</button> |
|||
</span> |
|||
</div> |
|||
|
|||
<mat-table style="display: none"> |
|||
<mat-header-row *matHeaderRowDef="[]"></mat-header-row> |
|||
</mat-table> |
|||
<table class="mat-table" style="min-width: 100%; overflow-x: auto; justify-content: left" *ngIf="directoryContents.length > 0"> |
|||
<thead> |
|||
<tr class="mat-header-row"> |
|||
<th class="mat-header-cell">File Name</th> |
|||
<th class="mat-header-cell">Location</th> |
|||
<th class="mat-header-cell">Permissions</th> |
|||
<th class="mat-header-cell">Size</th> |
|||
<th class="mat-header-cell">Actions</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
<ng-container *ngFor="let item of directoryContents"> |
|||
<tr class="mat-row"> |
|||
<td class="mat-cell" *ngIf="!item.is_directory">{{ item.name }}</td> |
|||
<td class="mat-cell" *ngIf="item.is_directory"> |
|||
<button mat-button color="accent" (click)="getDirectoryContents(item.path);"> |
|||
{{ item.name }} |
|||
</button> |
|||
</td> |
|||
<td class="mat-cell">{{ item.path }}</td> |
|||
<td class="mat-cell">{{ item.permissions }}</td> |
|||
<td class="mat-cell">{{ item.size }}</td> |
|||
<td class="mat-cell"> |
|||
<button mat-flat-button color="warn" class="action-button" (click)="showDeleteConfirmation(item);">Delete</button> |
|||
<button mat-flat-button color="accent" class="action-button" *ngIf="!item.is_directory" (click)="showEditDialog(item)">Edit</button> |
|||
</td> |
|||
</tr> |
|||
</ng-container> |
|||
</tbody> |
|||
</table> |
|||
</mat-card-content> |
|||
</mat-card> |
@ -0,0 +1,176 @@ |
|||
import { Component, OnInit } from '@angular/core'; |
|||
import {MatDialog } from '@angular/material/dialog'; |
|||
|
|||
import {ApiService} from '../services/api.service'; |
|||
|
|||
import {CabinetDeleteDialogComponent} from './helpers/delete-dialog/cabinet-delete-dialog.component'; |
|||
import {NewFolderDialogComponent} from './helpers/new-folder-dialog/cabinet-new-folder-dialog.component'; |
|||
import {FileEditorDialogComponent} from './helpers/file-editor-dialog/cabinet-file-editor-dialog.component'; |
|||
import {CabinetErrorDialogComponent} from './helpers/error-dialog/cabinet-error-dialog.component'; |
|||
|
|||
@Component({ |
|||
selector: 'lib-cabinet', |
|||
templateUrl: 'cabinet.component.html', |
|||
styleUrls: ['cabinet.component.css'], |
|||
}) |
|||
export class cabinetComponent implements OnInit { |
|||
|
|||
public isBusy: boolean = false; |
|||
public currentDirectory: string = '/'; |
|||
public directoryContents: Array<object> = []; |
|||
|
|||
constructor(private API: ApiService, |
|||