Prepare repository for Public Release

pull/28/head
Marc Egerton 2020-06-10 12:43:10 +01:00
commit d4cd34b3a5
343 changed files with 124188 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
__pycache__/
.DS_Store
*.pyc
venv
.idea/*
replacer.sh
create.sh
*.tar.gz
node_modules/

View File

View File

@ -0,0 +1 @@
from pineapple.helpers.helpers import *

View File

@ -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''

View File

@ -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')

View File

@ -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/')

View File

@ -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

View File

@ -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')

View File

@ -0,0 +1,3 @@
from pineapple.jobs.job import Job
from pineapple.jobs.job_runner import JobRunner
from pineapple.jobs.job_manager import JobManager

View File

@ -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()

View File

@ -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}

View File

@ -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()

View File

@ -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

View File

@ -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)

View File

@ -0,0 +1,2 @@
from pineapple.modules.module import Module
from pineapple.modules.request import Request

View File

@ -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)

View File

@ -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__)

6
Libraries/README.md Normal file
View File

@ -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.

11
README.md Normal file
View File

@ -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)

13
cabinet/.editorconfig Normal file
View File

@ -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

46
cabinet/.gitignore vendored Normal file
View File

@ -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

47
cabinet/angular.json Normal file
View File

@ -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"
}

42
cabinet/build.sh Executable file
View File

@ -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 generated Normal file

File diff suppressed because it is too large Load Diff

52
cabinet/package.json Normal file
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"
}
}

View File

@ -0,0 +1,7 @@
{
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../dist/cabinet",
"lib": {
"entryFile": "src/public-api.ts"
}
}

View File

@ -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"
}
}

View File

@ -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 { }

View File

@ -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;
}

View File

@ -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>

View File

@ -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,
private dialog: MatDialog) { }
humanFileSize(byteLength: number): string {
const kiloBytes = 1024;
const megaBytes = kiloBytes * kiloBytes;
const gigaBytes = megaBytes * megaBytes;
if (byteLength >= kiloBytes && byteLength < megaBytes) {
return Math.round(byteLength / kiloBytes) + ' KB';
} else if (byteLength >= megaBytes && byteLength < gigaBytes) {
return Math.round(byteLength / megaBytes) + ' MB';
} else if (byteLength >= gigaBytes) {
return Math.round(byteLength / gigaBytes) + ' GB';
} else {
return byteLength + ' bytes';
}
}
getDirectoryContents(path: string, getParent: boolean = false): void {
this.isBusy = true;
this.API.request({
module: 'cabinet',
action: 'list_directory',
directory: path,
get_parent: getParent
}, (response) => {
this.isBusy = false;
if (response.error !== undefined) {
this.showErrorDialog(response.error);
return
}
this.currentDirectory = response.working_directory;
this.directoryContents = response.contents.map((item) => {
item['size'] = this.humanFileSize(item['size']);
return item;
});
});
}
deleteItem(path: string): void {
this.API.request({
module: 'cabinet',
action: 'delete_item',
file_to_delete: path
}, (response) => {
if (response.error !== undefined) {
this.showErrorDialog(response.error);
return
}
this.getDirectoryContents(this.currentDirectory);
})
}
createDirectory(name: string): void {
this.API.request({
module: 'cabinet',
action: 'create_directory',
path: this.currentDirectory,
name: name
}, (response) => {
if (response.error !== undefined) {
this.showErrorDialog(response.error);
return
}
this.getDirectoryContents(this.currentDirectory);
})
}
writeFile(path: string, content: string): void {
this.API.request({
module: 'cabinet',
action: 'write_file',
file: path,
content: content
}, (response) => {
if (response.error !== undefined) {
this.showErrorDialog(response.error);
return
}
this.getDirectoryContents(this.currentDirectory);
})
}
showDeleteConfirmation(item): void {
this.dialog.open(CabinetDeleteDialogComponent, {
hasBackdrop: true,
width: '900px',
data: {
item: item,
onDelete: () => {
console.log('DELETING ITEM: ' + item.path);
this.deleteItem(item.path);
}
}
});
}
showCreateDirectory(): void {
this.dialog.open(NewFolderDialogComponent, {
hasBackdrop: true,
width: '900px',
data: {
path: this.currentDirectory,
onCreate: (name) => {
this.createDirectory(name);
}
}
})
}
showEditDialog(item): void {
const data = (item === null) ? {
path: this.currentDirectory,
fileName: null,
isNew: true,
onSave: (path, content) => {
this.writeFile(path, content);
}
} : {
path: item.path,
fileName: item.name,
isNew: false,
onSave: (path, content) => {
this.writeFile(path, content);
}
};
this.dialog.open(FileEditorDialogComponent, {
hasBackdrop: true,
width: '900px',
data: data
})
}
showErrorDialog(msg: string): void {
this.dialog.closeAll();
this.dialog.open(CabinetErrorDialogComponent, {
hasBackdrop: true,
width: '900px',
data: {
errorMessage: msg
}
});
}
ngOnInit(): void {
this.getDirectoryContents('/');
}
}

View File

@ -0,0 +1,24 @@
.delete-file-title-container {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-self: flex-start;
}
.delete-file-title {
font-size: 24px;
}
.delete-file-body-container {
height: 500px;
display: flex;
flex-direction: column;
}
.delete-file-buttons {
margin-top: 16px;
}
.delete-file-button-first {
margin-right: 10px;
}

View File

@ -0,0 +1,25 @@
<div class="delete-file-title-container">
<h1 mat-dialog-title>Delete {{ path }}?</h1>
<span fxFlex></span>
<button mat-icon-button (click)="closeDialog();">
<mat-icon>close</mat-icon>
</button>
</div>
<mat-divider></mat-divider>
<div>
<p *ngIf="!isDirectory">You are about to delete a file named `{{ name }}`. This action can not be undone.</p>
<p *ngIf="isDirectory">You are about to delete a directory named `{{ name }}` and all of its contents. This action not not be undone.</p>
<p>Are you sure you want to continue?</p>
<div class="delete-file-buttons">
<span fxFlex></span>
<button mat-raised-button class="delete-file-button-first" (click)="closeDialog()">
Cancel
</button>
<button mat-raised-button color="warn" (click)="preformDelete()">
Delete
</button>
</div>
</div>

View File

@ -0,0 +1,34 @@
import {Component, Inject, OnInit} from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
@Component({
selector: 'lib-cabinet-delete-dialog-component',
templateUrl: './cabinet-delete-dialog.component.html',
styleUrls: ['./cabinet-delete-dialog.component.css']
})
export class CabinetDeleteDialogComponent implements OnInit {
constructor(public dialogRef: MatDialogRef<CabinetDeleteDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any) {
this.path = data.item.path;
this.name = data.item.name;
this.isDirectory = data.item.is_directory;
}
public path: string;
public name: string;
public isDirectory: boolean;
preformDelete(): void {
const onDelete = this.data.onDelete;
onDelete();
this.closeDialog();
}
closeDialog(): void {
this.dialogRef.close()
}
ngOnInit() {
}
}

View File

@ -0,0 +1,24 @@
.error-dialog-title-container {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-self: flex-start;
}
.error-dialog-title {
font-size: 24px;
}
.error-dialog-body-container {
height: 500px;
display: flex;
flex-direction: column;
}
.error-dialog-buttons {
margin-top: 16px;
}
.error-dialog-button-first {
margin-right: 10px;
}

View File

@ -0,0 +1,19 @@
<div class="error-dialog-title-container">
<h1 mat-dialog-title>Error</h1>
<span fxFlex></span>
<button mat-icon-button (click)="closeDialog();">
<mat-icon>close</mat-icon>
</button>
</div>
<mat-divider></mat-divider>
<div>
<p>{{ errorMessage }}</p>
<div class="error-dialog-buttons">
<span fxFlex></span>
<button mat-raised-button color="accent" class="error-dialog-button-first" (click)="closeDialog();">
Okay
</button>
</div>
</div>

View File

@ -0,0 +1,23 @@
import {Component, Inject, OnInit} from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
@Component({
selector: 'lib-cabinet-error-dialog-component',
templateUrl: './cabinet-error-dialog.component.html',
styleUrls: ['./cabinet-error-dialog.component.css']
})
export class CabinetErrorDialogComponent implements OnInit {
constructor(public dialogRef: MatDialogRef<CabinetErrorDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any) {
this.errorMessage = data.errorMessage;
}
public errorMessage: string;
closeDialog(): void {
this.dialogRef.close()
}
ngOnInit() {
}
}

View File

@ -0,0 +1,37 @@
.file-editor-title-container {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-self: flex-start;
}
.file-editor-title {
font-size: 24px;
}
.file-editor-body-container {
height: 500px;
display: flex;
flex-direction: column;
}
.new-portal-buttons {
margin-top: 16px;
}
.new-portal-button-first {
margin-right: 10px;
}
.file-editor-text-area {
width: 100%;
min-height: 320px;
max-height: 400px;
resize: none;
margin: 0 0 5px;
padding: 0;
background-color: #efefef;
border-radius: 2px;
border-color: #cecece;
max-lines: 50;
}

View File

@ -0,0 +1,30 @@
<div class="file-editor-title-container">
<h1 mat-dialog-title *ngIf="error == null">{{ title }}</h1>
<h1 style="color: red;" *ngIf="error != null">{{ error }}</h1>
<span fxFlex></span>
<button mat-icon-button (click)="closeDialog();">
<mat-icon>close</mat-icon>
</button>
</div>
<mat-divider></mat-divider>
<div>
<form>
<mat-form-field style="width: 100%;">
<mat-label>File Name</mat-label>
<input style="width: 100%" matInput [(ngModel)]="fileName" name="fileName" [disabled]="!isNew"/>
</mat-form-field>
</form>
<textarea placeholder="File Content" class="file-editor-text-area" matTextareaAutosize [(ngModel)]="fileContent" name="fileContent" (keydown)="handleTabKey($event);"></textarea>
<div class="file-editor-buttons">
<span fxFlex></span>
<button mat-raised-button class="file-editor-button-first" (click)="closeDialog();">
Cancel
</button>
<button mat-raised-button color="accent" (click)="preformSave();">
Save
</button>
</div>
</div>

View File

@ -0,0 +1,69 @@
import {Component, Inject, OnInit} from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
import {ApiService} from '../../../services/api.service';
@Component({
selector: 'lib-cabinet-error-dialog-component',
templateUrl: './cabinet-file-editor-dialog.component.html',
styleUrls: ['./cabinet-file-editor-dialog.component.css']
})
export class FileEditorDialogComponent implements OnInit {
constructor(public dialogRef: MatDialogRef<FileEditorDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any,
private API: ApiService) {
this.path = data.path;
this.fileName = data.fileName;
this.isNew = data.isNew;
}
public path: string = null;
public isNew = false;
public title = '';
public fileName = '';
public fileContent = '';
public error: string = null;
loadFileContent(): void {
this.API.request({
module: 'cabinet',
action: 'read_file',
file: this.path
}, (response) => {
if (response.error !== undefined) {
this.error = response.error;
return
}
this.fileContent = response;
})
}
preformSave(): void {
const fileToSave = (this.isNew) ? this.path + '/' + this.fileName : this.path;
const onSave = this.data.onSave;
onSave(fileToSave, this.fileContent);
this.closeDialog();
}
handleTabKey(e: KeyboardEvent): boolean {
if (e.code.toLowerCase() === 'tab') {
e.preventDefault();
const target = e.target as HTMLTextAreaElement;
const start = target.selectionStart;
const end = target.selectionEnd;
this.fileContent = this.fileContent.substring(0, start) + ' ' + this.fileContent.substring(end);
return false;
}
}
closeDialog(): void {
this.dialogRef.close();
}
ngOnInit() {
this.title = (this.isNew) ? 'Create New File' : 'Edit File';
if (!this.isNew) {
this.loadFileContent();
}
}
}

View File

@ -0,0 +1,24 @@
.new-folder-title-container {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-self: flex-start;
}
.new-folder-title {
font-size: 24px;
}
.new-folder-body-container {
height: 500px;
display: flex;
flex-direction: column;
}
.new-folder-buttons {
margin-top: 16px;
}
.new-folder-button-first {
margin-right: 10px;
}

View File

@ -0,0 +1,31 @@
<div class="delete-file-title-container">
<h1 mat-dialog-title>Create New Directory</h1>
<span fxFlex></span>
<button mat-icon-button (click)="closeDialog();">
<mat-icon>close</mat-icon>
</button>
</div>
<mat-divider></mat-divider>
<br/>
<div>
<form>
<mat-form-field style="width: 100%;">
<mat-label>Directory Name</mat-label>
<input style="width: 100%;" matInput [(ngModel)]="directoryName" name="directoryName" />
<br/>
</mat-form-field>
</form>
<div class="new-folder-buttons">
<span fxFlex></span>
<button mat-raised-button class="new-folder-button-first" (click)="closeDialog()">
Cancel
</button>
<button mat-raised-button color="accent" (click)="preformCreate()">
Done
</button>
</div>
</div>

View File

@ -0,0 +1,31 @@
import {Component, Inject, OnInit} from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
import {ApiService} from '../../../services/api.service';
@Component({
selector: 'lib-cabinet-error-dialog-component',
templateUrl: './cabinet-new-folder-dialog.component.html',
styleUrls: ['./cabinet-new-folder-dialog.component.css']
})
export class NewFolderDialogComponent implements OnInit {
constructor(public dialogRef: MatDialogRef<NewFolderDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any) {
this.path = data.path;
}
public path: string;
public directoryName = '';
preformCreate(): void {
const onCreate = this.data.onCreate;
onCreate(this.directoryName);
this.closeDialog();
}
closeDialog(): void {
this.dialogRef.close();
}
ngOnInit() {
}
}

View File

@ -0,0 +1,107 @@
/*
* Copyright (c) 2018 Hak5 LLC.
*/
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { A11yModule } from '@angular/cdk/a11y';
import { BidiModule } from '@angular/cdk/bidi';
import { ObserversModule } from '@angular/cdk/observers';
import { OverlayModule } from '@angular/cdk/overlay';
import { PlatformModule } from '@angular/cdk/platform';
import { PortalModule } from '@angular/cdk/portal';
import { CdkStepperModule } from '@angular/cdk/stepper';
import { CdkTableModule } from '@angular/cdk/table';
import { CdkTreeModule } from '@angular/cdk/tree';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatBadgeModule } from '@angular/material/badge';
import { MatBottomSheetModule } from '@angular/material/bottom-sheet';
import { MatButtonModule } from '@angular/material/button';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { MatCardModule } from '@angular/material/card';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatChipsModule } from '@angular/material/chips';
import { MatNativeDateModule, MatRippleModule } from '@angular/material/core';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatDialogModule } from '@angular/material/dialog';
import { MatDividerModule } from '@angular/material/divider';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatListModule } from '@angular/material/list';
import { MatMenuModule } from '@angular/material/menu';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatRadioModule } from '@angular/material/radio';
import { MatSelectModule } from '@angular/material/select';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatSliderModule } from '@angular/material/slider';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatSortModule } from '@angular/material/sort';
import { MatStepperModule } from '@angular/material/stepper';
import { MatTableModule } from '@angular/material/table';
import { MatTabsModule } from '@angular/material/tabs';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatTreeModule } from '@angular/material/tree';
@NgModule({
imports: [ CommonModule],
exports: [
// CDK
A11yModule,
BidiModule,
ObserversModule,
OverlayModule,
PlatformModule,
PortalModule,
CdkStepperModule,
CdkTableModule,
CdkTreeModule,
// Material
MatAutocompleteModule,
MatBadgeModule,
MatBottomSheetModule,
MatButtonModule,
MatButtonToggleModule,
MatCardModule,
MatCheckboxModule,
MatChipsModule,
MatDatepickerModule,
MatDialogModule,
MatDividerModule,
MatExpansionModule,
MatFormFieldModule,
MatGridListModule,
MatIconModule,
MatInputModule,
MatListModule,
MatMenuModule,
MatNativeDateModule,
MatPaginatorModule,
MatProgressBarModule,
MatProgressSpinnerModule,
MatRadioModule,
MatRippleModule,
MatSelectModule,
MatSidenavModule,
MatSliderModule,
MatSlideToggleModule,
MatSnackBarModule,
MatSortModule,
MatStepperModule,
MatTableModule,
MatTabsModule,
MatToolbarModule,
MatTooltipModule,
MatTreeModule,
],
declarations: []
})
export class MaterialModule {
}

View File

@ -0,0 +1,182 @@
import {Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {Router} from '@angular/router';
@Injectable({
providedIn: 'root'
})
export class ApiService {
public static totalRequests = 0;
apiModuleBusy = document.getElementById('ApiModuleBusy');
constructor(private http: HttpClient,
private router: Router) {}
emptyResponse = {error: 'Request returned empty response'};
unauth(): void {
localStorage.removeItem('authToken');
if (this.router.url !== '/Login' && this.router.url !== '/Setup') {
this.router.navigateByUrl('/Login');
}
}
setBusy(): void {
this.apiModuleBusy.style.display = 'block';
}
setNotBusy(): void {
this.apiModuleBusy.style.display = 'none';
}
request(payload: any, callback: (any) => void) {
this.setBusy();
let resp;
this.http.post('/api/module/request', payload).subscribe((r: any) => {
if (r === undefined || r === null) {
resp = this.emptyResponse;
}
resp = r.payload;
}, (err) => {
resp = err.error;
if (err.status === 401) {
this.unauth();
}
this.setNotBusy();
callback(resp);
}, () => {
this.setNotBusy();
callback(resp);
});
ApiService.totalRequests++;
}
APIGet(path: string, callback: (any) => void): any {
ApiService.totalRequests++;
let resp;
this.http.get(path).subscribe((r) => {
if (r === undefined || r === null) {
r = this.emptyResponse;
}
resp = r;
}, (err) => {
resp = err.error;
if (err.status === 401) {
this.unauth();
}
callback(resp);
}, () => {
callback(resp);
});
}
async APIGetAsync(path: string): Promise<any> {
ApiService.totalRequests++;
return await this.http.get(path).toPromise();
}
APIPut(path: string, body: any, callback: (any) => void): any {
ApiService.totalRequests++;
let resp;
this.http.put(path, body).subscribe((r) => {
if (r === undefined || r === null) {
r = this.emptyResponse;
}
resp = r;
}, (err) => {
resp = err.error;
if (err.status === 401) {
this.unauth();
}
callback(resp);
}, () => {
callback(resp);
});
}
async APIPutAsync(path: string, body: any): Promise<any> {
return await this.http.put(path, body).toPromise();
}
APIPost(path: string, body: any, callback: (any) => void): any {
ApiService.totalRequests++;
let resp;
this.http.post(path, body).subscribe((r) => {
if (r === undefined || r === null) {
resp = this.emptyResponse;
}
resp = r;
}, (err) => {
resp = err.error;
if (err.status === 401) {
this.unauth();
}
callback(resp);
}, () => {
callback(resp);
});
}
async APIPostAsync(path: string, body: any): Promise<any> {
return await this.http.post(path, body).toPromise();
}
APIDelete(path: string, body: any, callback: (any) => void): any {
ApiService.totalRequests++;
const opts = {
headers: null,
body: body
};
let resp;
this.http.delete(path, opts).subscribe((r) => {
if (r === undefined || r === null) {
r = this.emptyResponse;
}
resp = r;
}, (err) => {
resp = err.error;
if (err.status === 401) {
this.unauth();
}
callback(resp);
}, () => {
callback(resp);
});
}
async APIDeleteAsync(path: string, body: any): Promise<any> {
return await this.http.delete(path, body).toPromise();
}
APIDownload(fullpath: string, filename: string): void {
ApiService.totalRequests++;
const body = {
filename: fullpath
};
this.http.post('/api/download', body, {responseType: 'blob'}).subscribe((r) => {
const url = window.URL.createObjectURL(r);
const a = document.createElement('a');
document.body.appendChild(a);
a.setAttribute('style', 'display: none');
a.href = url;
a.download = filename;
a.click();
window.URL.revokeObjectURL(url);
a.remove();
});
}
}

View File

@ -0,0 +1,8 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class cabinetService {
constructor() {}
}

View File

@ -0,0 +1,7 @@
{
"name": "cabinet",
"title": "Cabinet",
"author": "newbi3",
"description": "A simple browser based file manager for the WiFi Pineapple.",
"version": "1.0"
}

View File

@ -0,0 +1,115 @@
#!/usr/bin/env python3
from typing import List
import logging
import pathlib
import os
from pineapple.modules import Module, Request
module = Module('cabinet', logging.DEBUG)
def _get_directory_contents(directory: pathlib.Path) -> List[dict]:
return [
{
'name': item.name,
'path': str(item),
'size': item.lstat().st_size,
'permissions': oct(item.lstat().st_mode)[-3:],
'is_directory': item.is_dir()
}
for item in sorted(directory.iterdir(), key=lambda i: not i.is_dir())
]
def _delete_directory_tree(directory: pathlib.Path) -> bool:
if not directory.is_dir():
return False
for item in directory.iterdir():
module.logger.debug(f'DELETING ITEM: {str(item)}')
if item.is_dir():
_delete_directory_tree(item)
item.rmdir()
else:
item.unlink()
return True
@module.handles_action('list_directory')
def list_directory(request: Request):
get_parent: bool = request.get_parent
directory: pathlib.Path = pathlib.Path(request.directory) if not get_parent else pathlib.Path(request.directory).parent
if not directory:
return 'Directory not found.', False
if not os.path.isdir(directory):
return 'Directory not found.', False
return {'working_directory': str(directory), 'contents': _get_directory_contents(directory)}
@module.handles_action('delete_item')
def delete_item(request: Request):
path = pathlib.Path(request.file_to_delete)
module.logger.debug(f'DELETING: {request.file_to_delete}')
if not path.exists():
return 'File or directory does not exist.', False
if not path.is_absolute():
return 'Absolute path expected.', False
if path.is_dir():
success = _delete_directory_tree(path)
if success:
path.rmdir()
else:
return 'An error occurred deleting the directory.', False
else:
path.unlink()
return f'{path} has been deleted.'
@module.handles_action('write_file')
def write_file(request: Request):
path = pathlib.Path(request.file)
content = request.content
with open(str(path), 'w') as f:
f.write(content)
return 'File saved.'
@module.handles_action('read_file')
def read_file(request: Request):
path = pathlib.Path(request.file)
if not path.exists() or not path.is_file():
return 'Unable to open file.'
with open(str(path), 'r') as f:
return f.read()
@module.handles_action('create_directory')
def create_directory(request: Request):
path = pathlib.Path(f'{request.path}/{request.name}')
module.logger.debug(f'CREATE FOLDER {request.name} IN PATH {request.path}. SHOULD BE {str(path)}')
try:
path.mkdir()
return "Directory created."
except FileExistsError:
return "A file by that name already exists.", False
if __name__ == '__main__':
module.start()

View File

@ -0,0 +1 @@
<svg id="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><path d="M0,39.54V12A3.85,3.85,0,0,1,1.1,8.92l2.52-3s.47-1.07,2.74-1.07h12.2s2.17-.25,2.88,1.3l1.92,1.93s1.07,1.43,2.15,1.43H45.15A2.88,2.88,0,0,1,48,12.62V40s.23,3.13-3.35,3.13H3.14S0,43.5,0,39.54Z"/></svg>

After

Width:  |  Height:  |  Size: 277 B

View File

@ -0,0 +1,7 @@
/*
* Public API Surface of cabinet
*/
export * from './lib/services/cabinet.service';
export * from './lib/components/cabinet.component';
export * from './lib/cabinet.module';

View File

@ -0,0 +1,25 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/lib",
"target": "es2015",
"declaration": true,
"inlineSources": true,
"types": [],
"lib": [
"dom",
"es2018"
]
},
"angularCompilerOptions": {
"skipTemplateCodegen": true,
"strictMetadataEmit": true,
"fullTemplateTypeCheck": true,
"strictInjectionParameters": true,
"enableResourceInlining": true
},
"exclude": [
"src/test.ts",
"**/*.spec.ts"
]
}

View File

@ -0,0 +1,6 @@
{
"extends": "./tsconfig.lib.json",
"angularCompilerOptions": {
"enableIvy": false
}
}

View File

@ -0,0 +1,17 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/spec",
"types": [
"jasmine",
"node"
]
},
"files": [
"src/test.ts"
],
"include": [
"**/*.spec.ts",
"**/*.d.ts"
]
}

View File

@ -0,0 +1,17 @@
{
"extends": "../../tslint.json",
"rules": {
"directive-selector": [
true,
"attribute",
"lib",
"camelCase"
],
"component-selector": [
true,
"element",
"lib",
"kebab-case"
]
}
}

34
cabinet/tsconfig.json Normal file
View File

@ -0,0 +1,34 @@
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"module": "esnext",
"moduleResolution": "node",
"importHelpers": true,
"target": "es2015",
"typeRoots": [
"node_modules/@types"
],
"lib": [
"es2018",
"dom"
],
"paths": {
"cabinet": [
"dist/cabinet"
],
"cabinet/*": [
"dist/cabinet/*"
]
}
},
"angularCompilerOptions": {
"fullTemplateTypeCheck": true,
"strictInjectionParameters": true
}
}

79
cabinet/tslint.json Normal file
View File

@ -0,0 +1,79 @@
{
"extends": "tslint:recommended",
"rulesDirectory": [
"codelyzer"
],
"rules": {
"array-type": false,
"arrow-parens": false,
"deprecation": {
"severity": "warning"
},
"import-blacklist": [
true,
"rxjs/Rx"
],
"interface-name": false,
"max-classes-per-file": false,
"max-line-length": [
true,
140
],
"member-access": false,
"member-ordering": [
true,
{
"order": [
"static-field",
"instance-field",
"static-method",
"instance-method"
]
}
],
"no-consecutive-blank-lines": false,
"no-console": [
true,
"debug",
"info",
"time",
"timeEnd",
"trace"
],
"no-empty": false,
"no-inferrable-types": [
true,
"ignore-params"
],
"no-non-null-assertion": true,
"no-redundant-jsdoc": true,
"no-switch-case-fall-through": true,
"no-var-requires": false,
"object-literal-key-quotes": [
true,
"as-needed"
],
"object-literal-sort-keys": false,
"ordered-imports": false,
"quotemark": [
true,
"single"
],
"trailing-comma": false,
"component-class-suffix": true,
"contextual-lifecycle": true,
"directive-class-suffix": true,
"no-conflicting-lifecycle": true,
"no-host-metadata-property": true,
"no-input-rename": true,
"no-inputs-metadata-property": true,
"no-output-native": true,
"no-output-on-prefix": true,
"no-output-rename": true,
"no-outputs-metadata-property": true,
"template-banana-in-box": true,
"template-no-negated-async": true,
"use-lifecycle-interface": true,
"use-pipe-transform-interface": true
}
}

111
create.sh Executable file
View File

@ -0,0 +1,111 @@
#!/bin/bash
# _ _ _ _____
# | | | | | | | ____|
# | |__| | __ _| | _| |__
# | __ |/ _` | |/ /___ \
# | | | | (_| | < ___) |
# |_| |_|\__,_|_|\_\____/
MODULE_NAME="NOTSPECIFIED"
MODULE_TITLE="NOTSPECIFIED"
MODULE_AUTHOR="NOTSPECIFIED"
MODULE_DESC="NOTSPECIFIED"
print_banner() {
echo " __ ___ ______ _ _____ _ _ "
echo " \ \ / (_) ____(_) | __ (_) | | "
echo " \ \ /\ / / _| |__ _ | |__) | _ __ ___ __ _ _ __ _ __ | | ___ "
echo " \ \/ \/ / | | __| | | | ___/ | '_ \ / _ \/ _\` | '_ \| '_ \| |/ _ \ "
echo " \ /\ / | | | | | | | | | | | | __/ (_| | |_) | |_) | | __/ "
echo " \/ \/ |_|_| |_| |_| |_|_| |_|\___|\__,_| .__/| .__/|_|\___| Mark 7"
echo " Module Creation Helper | | | | "
echo " Version 1.0 |_| |_| "
echo " "
echo " "
}
get_info() {
read -p "[*] Module Name: " MODULE_NAME
read -p "[*] Module Title: " MODULE_TITLE
read -p "[*] Module Author: " MODULE_AUTHOR
read -p "[*] Module Short Description: " MODULE_DESC
sanitize_info
}
sanitize_info() {
# Remove All Spaces from Module Name
MODULE_NAME=${MODULE_NAME// /}
}
create_template() {
if [[ ! -d "examplemodule" ]]; then
echo "[!!] The template module seems to be missing. Please re-clone this repository and try again."
exit 1
fi
echo "[*] Creating New Module ($MODULE_NAME)."
cp -r examplemodule $MODULE_NAME
grep -rl examplemodule $MODULE_NAME/ | xargs sed -i "s/examplemodule/$MODULE_NAME/g"
grep -rl example-module $MODULE_NAME/ | xargs sed -i "s/example-module/$MODULE_NAME/g"
grep -rl example-service $MODULE_NAME/ | xargs sed -i "s/example-service/$MODULE_NAME/g"
grep -rl ExampleModuleComponent $MODULE_NAME/ | xargs sed -i "s/ExampleModuleComponent/${MODULE_NAME}Component/g"
grep -rl ExampleServiceService $MODULE_NAME/ | xargs sed -i "s/ExampleServiceService/${MODULE_NAME}Service/g"
grep -rl ExampleModuleModule $MODULE_NAME/ | xargs sed -i "s/ExampleModuleModule/${MODULE_NAME}Module/g"
grep -rl "the Example Module!" $MODULE_NAME/ | xargs sed -i "s/the Example Module!/${MODULE_NAME}/g"
grep -rl ": \"Example Module" $MODULE_NAME/ | xargs sed -i "s/: \"Example Module/: \"${MODULE_TITLE}/g"
grep -rl "An example module!" $MODULE_NAME/ | xargs sed -i "s/An example module!/${MODULE_DESC}/g"
grep -rl ": \"Hak5" $MODULE_NAME/ | xargs sed -i "s/: \"Hak5/: \"${MODULE_AUTHOR}/g"
mv $MODULE_NAME/projects/examplemodule $MODULE_NAME/projects/$MODULE_NAME
mv $MODULE_NAME/projects/$MODULE_NAME/src/lib/components/example-module.component.html $MODULE_NAME/projects/$MODULE_NAME/src/lib/components/$MODULE_NAME.component.html
mv $MODULE_NAME/projects/$MODULE_NAME/src/lib/components/example-module.component.css $MODULE_NAME/projects/$MODULE_NAME/src/lib/components/$MODULE_NAME.component.css
mv $MODULE_NAME/projects/$MODULE_NAME/src/lib/components/example-module.component.ts $MODULE_NAME/projects/$MODULE_NAME/src/lib/components/$MODULE_NAME.component.ts
mv $MODULE_NAME/projects/$MODULE_NAME/src/lib/services/example-service.service.ts $MODULE_NAME/projects/$MODULE_NAME/src/lib/services/$MODULE_NAME.service.ts
mv $MODULE_NAME/projects/$MODULE_NAME/src/lib/example-module.module.ts $MODULE_NAME/projects/$MODULE_NAME/src/lib/$MODULE_NAME.module.ts
}
ask_prepare_node() {
while true; do
read -p "[!] Would you like to prepare the Angular workspace? [Y/n] " yn
case $yn in
[Yy]* ) prepare_node; break;;
[Nn]* ) return;;
* ) prepare_node; break;;
esac
done
}
prepare_node() {
echo "[*] Preparing the Angular workspace."
cd $MODULE_NAME
if ! command -v npm &> /dev/null; then
echo "[!] NPM does not appear to be installed on this system. Failed to create workspace."
return
fi
npm install
echo "[*] Prepared the Angular workspace."
cd -
}
finish() {
echo "[*] A new module has been created! Exiting."
}
main() {
print_banner
get_info
create_template
ask_prepare_node
finish
}
main

13
evilportal/.editorconfig Normal file
View File

@ -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

46
evilportal/.gitignore vendored Normal file
View File

@ -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

47
evilportal/angular.json Normal file
View File

@ -0,0 +1,47 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"evilportal": {
"projectType": "library",
"root": "projects/evilportal",
"sourceRoot": "projects/evilportal/src",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-ng-packagr:build",
"options": {
"tsConfig": "projects/evilportal/tsconfig.lib.json",
"project": "projects/evilportal/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "projects/evilportal/tsconfig.lib.prod.json"
}
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "projects/evilportal/src/test.ts",
"tsConfig": "projects/evilportal/tsconfig.spec.json",
"karmaConfig": "projects/evilportal/karma.conf.js"
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"projects/evilportal/tsconfig.lib.json",
"projects/evilportal/tsconfig.spec.json"
],
"exclude": [
"**/node_modules/**"
]
}
}
}
}},
"defaultProject": "evilportal"
}

42
evilportal/build.sh Executable file
View File

@ -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

19244
evilportal/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

53
evilportal/package.json Normal file
View File

@ -0,0 +1,53 @@
{
"name": "evilportal",
"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",
"angular-cli": "^1.0.0-beta.28.3",
"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"
}
}

View File

@ -0,0 +1,7 @@
{
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../dist/evilportal",
"lib": {
"entryFile": "src/public-api.ts"
}
}

View File

@ -0,0 +1,11 @@
{
"name": "evilportal",
"version": "0.0.1",
"peerDependencies": {
"@angular/common": "^8.2.14",
"@angular/core": "^8.2.14"
},
"scripts": {
"build": "ng build --prod"
}
}

View File

@ -0,0 +1,49 @@
<?php namespace evilportal;
class API
{
private $request;
private $error;
public function __construct()
{
$this->request = (object)$_POST;
}
public function route()
{
$portalPath = "/www/MyPortal.php";
$portalClass = "evilportal\\MyPortal";
if (!file_exists($portalPath)) {
$this->error = "MyPortal.php does not exist in {$portalPath}";
return;
}
require_once("Portal.php");
require_once($portalPath);
if (!class_exists($portalClass)) {
$this->error = "The class {$portalClass} does not exist in {$portalPath}";
return;
}
$portal = new $portalClass($this->request);
$portal->handleAuthorization();
$this->response = $portal->getResponse();
}
public function finalize()
{
if ($this->error) {
return json_encode(array("error" => $this->error));
} elseif ($this->response) {
return json_encode($this->response);
}
}
public function go()
{
$this->route();
}
}

View File

@ -0,0 +1,131 @@
<?php namespace evilportal;
abstract class Portal
{
protected $request;
protected $response;
protected $error;
protected $AUTHORIZED_CLIENTS_FILE = "/tmp/EVILPORTAL_CLIENTS.txt";
public function __construct($request)
{
$this->request = $request;
}
public function getResponse()
{
if (empty($this->error) && !empty($this->response)) {
return $this->response;
} elseif (empty($this->error) && empty($this->response)) {
return array('error' => 'API returned empty response');
} else {
return array('error' => $this->error);
}
}
/**
* Run a command in the background and don't wait for it to finish.
* @param $command: The command to run
*/
protected final function execBackground($command)
{
exec("echo \"{$command}\" | at now");
}
/**
* Send notifications to the web UI.
* @param $message: The notification message
*/
protected final function notify($message)
{
$this->execBackground("notify {$message}");
}
/**
* Write a log to the portals log file.
* These logs can be retrieved from the web UI for .logs in the portals directory.
* The log file is automatically appended to so there is no reason to add new line characters to your message.
* @param $message: The message to write to the log file.
*/
protected final function writeLog($message)
{
try {
$reflector = new \ReflectionClass(get_class($this));
$logPath = dirname($reflector->getFileName());
file_put_contents("{$logPath}/.logs", "{$message}\n", FILE_APPEND);
} catch (\ReflectionException $e) {
// do nothing.
}
}
/**
* Creates an iptables rule allowing the client to access the internet and writes them to the authorized clients.
* Override this method to add other authorization steps validation.
* @param $clientIP: The IP address of the client to authorize
* @return bool: True if the client was successfully authorized otherwise false.
*/
protected function authorizeClient($clientIP)
{
if (!$this->isClientAuthorized($clientIP)) {
exec("iptables -t nat -I PREROUTING -s {$clientIP} -j ACCEPT");
file_put_contents($this->AUTHORIZED_CLIENTS_FILE, "{$clientIP}\n", FILE_APPEND);
}
return true;
}
/**
* Handle client authorization here.
* By default it just checks that the redirection target is in the request.
* Override this to perform your own validation.
*/
protected function handleAuthorization()
{
if (isset($this->request->target)) {
$this->authorizeClient($_SERVER['REMOTE_ADDR']);
$this->onSuccess();
$this->redirect();
} elseif ($this->isClientAuthorized($_SERVER['REMOTE_ADDR'])) {
$this->redirect();
} else {
$this->showError();
}
}
/**
* Where to redirect to on successful authorization.
*/
protected function redirect()
{
header("Location: {$this->request->target}", true, 302);
}
/**
* Override this to do something when the client is successfully authorized.
* By default it just notifies the Web UI.
*/
protected function onSuccess()
{
$this->notify("New client authorized through EvilPortal!");
}
/**
* If an error occurs then do something here.
* Override to provide your own functionality.
*/
protected function showError()
{
echo "You have not been authorized.";
}
/**
* Checks if the client has been authorized.
* @param $clientIP: The IP of the client to check.
* @return bool|int: True if the client is authorized else false.
*/
protected function isClientAuthorized($clientIP)
{
$authorizeClients = file_get_contents($this->AUTHORIZED_CLIENTS_FILE);
return strpos($authorizeClients, $clientIP);
}
}

View File

@ -0,0 +1,10 @@
<?php namespace evilportal;
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
header('Content-Type: application/json');
require_once("API.php");
$api = new API();
echo $api->go();

View File

@ -0,0 +1,52 @@
user root root;
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
index index.php;
default_type text/html;
sendfile on;
keepalive_timeout 65;
gzip on;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_http_version 1.0;
gzip_comp_level 2;
gzip_types text/plain application/x-javascript text/css application/xml;
gzip_vary on;
server {
listen 80; # Port, make sure it is not in conflict with another http daemon.
server_name www; # Change this, reference -> http://nginx.org/en/docs/http/server_names.html
error_page 404 =200 /index.php;
error_log /root/elog;
access_log /dev/null;
fastcgi_connect_timeout 300;
fastcgi_send_timeout 300;
fastcgi_read_timeout 300;
fastcgi_buffer_size 32k;
fastcgi_buffers 4 32k;
fastcgi_busy_buffers_size 32k;
fastcgi_temp_file_write_size 32k;
client_body_timeout 10;
client_header_timeout 10;
send_timeout 60; # 60 sec should be enough, if experiencing alof of timeouts, increase this.
output_buffers 1 32k;
postpone_output 1460;
root /www;
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/var/run/php7-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
if (-f $request_filename) {
# Only throw it at PHP-FPM if the file exists (prevents some PHP exploits)
fastcgi_pass unix:/var/run/php7-fpm.sock; # The upstream determined above
}
}
error_page 404 =200 /index.php;
}
}

View File

@ -0,0 +1,311 @@
[PHP]
zend.ze1_compatibility_mode = Off
; Language Options
engine = On
short_open_tag = On
precision = 12
y2k_compliance = On
output_buffering = Off
;output_handler =
zlib.output_compression = Off
;zlib.output_compression_level = -1
;zlib.output_handler =
implicit_flush = Off
unserialize_callback_func =
serialize_precision = 100
;open_basedir =
disable_functions =
disable_classes =
; Colors for Syntax Highlighting mode. Anything that's acceptable in
; <span style="color: ???????"> would work.
;highlight.string = #DD0000
;highlight.comment = #FF9900
;highlight.keyword = #007700
;highlight.bg = #FFFFFF
;highlight.default = #0000BB
;highlight.html = #000000
;ignore_user_abort = On
;realpath_cache_size = 16k
;realpath_cache_ttl = 120
; Miscellaneous
expose_php = On
; Resource Limits
max_execution_time = 30 ; Maximum execution time of each script, in seconds.
max_input_time = 60 ; Maximum amount of time each script may spend parsing request data.
;max_input_nesting_level = 64
memory_limit = 10M ; Maximum amount of memory a script may consume.
; Error handling and logging
; Error Level Constants:
; E_ALL - All errors and warnings (includes E_STRICT as of PHP 6.0.0)
; E_ERROR - fatal run-time errors
; E_RECOVERABLE_ERROR - almost fatal run-time errors
; E_WARNING - run-time warnings (non-fatal errors)
; E_PARSE - compile-time parse errors
; E_NOTICE - run-time notices (these are warnings which often result
; from a bug in your code, but it's possible that it was
; intentional (e.g., using an uninitialized variable and
; relying on the fact it's automatically initialized to an
; empty string)
; E_STRICT - run-time notices, enable to have PHP suggest changes
; to your code which will ensure the best interoperability
; and forward compatibility of your code
; E_CORE_ERROR - fatal errors that occur during PHP's initial startup
; E_CORE_WARNING - warnings (non-fatal errors) that occur during PHP's
; initial startup
; E_COMPILE_ERROR - fatal compile-time errors
; E_COMPILE_WARNING - compile-time warnings (non-fatal errors)
; E_USER_ERROR - user-generated error message
; E_USER_WARNING - user-generated warning message
; E_USER_NOTICE - user-generated notice message
; E_DEPRECATED - warn about code that will not work in future versions
; of PHP
; E_USER_DEPRECATED - user-generated deprecation warnings
;
; Common Values:
; E_ALL & ~E_NOTICE (Show all errors, except for notices and coding standards warnings.)
; E_ALL & ~E_NOTICE | E_STRICT (Show all errors, except for notices)
; E_COMPILE_ERROR|E_RECOVERABLE_ERROR|E_ERROR|E_CORE_ERROR (Show only errors)
; E_ALL | E_STRICT (Show all errors, warnings and notices including coding standards.)
; Default Value: E_ALL & ~E_NOTICE
error_reporting = E_ALL & ~E_NOTICE & ~E_STRICT
display_errors = Off
display_startup_errors = Off
log_errors = Off
log_errors_max_len = 1024
ignore_repeated_errors = Off
ignore_repeated_source = Off
report_memleaks = On
;report_zend_debug = 0
track_errors = Off
;html_errors = Off
;docref_root = "/phpmanual/"
;docref_ext = .html
;error_prepend_string = "<font color=#ff0000>"
;error_append_string = "</font>"
; Log errors to specified file.
;error_log = /var/log/php_errors.log
; Log errors to syslog.
;error_log = syslog
; Data Handling
;arg_separator.output = "&amp;"
;arg_separator.input = ";&"
variables_order = "EGPCS"
request_order = "GP"
register_globals = Off
register_long_arrays = Off
register_argc_argv = On
auto_globals_jit = On
post_max_size = 8M
;magic_quotes_gpc = Off
magic_quotes_runtime = Off
magic_quotes_sybase = Off
default_mimetype = "text/html"
;default_charset = "iso-8859-1"
always_populate_raw_post_data = "-1"
; Paths and Directories
; UNIX: "/path1:/path2"
;include_path = ".:/php/includes"
doc_root = ""
user_dir =
extension_dir = "/usr/lib/php"
enable_dl = On
;cgi.force_redirect = 1
;cgi.nph = 1
;cgi.redirect_status_env = ;
cgi.fix_pathinfo=1
;fastcgi.impersonate = 1;
;fastcgi.logging = 0
;cgi.rfc2616_headers = 0
; File Uploads
file_uploads = On
upload_tmp_dir = "/tmp"
upload_max_filesize = 2M
max_file_uploads = 20
; Fopen wrappers
allow_url_fopen = On
allow_url_include = Off
;from="john@doe.com"
;user_agent="PHP"
default_socket_timeout = 60
;auto_detect_line_endings = Off
; Dynamic Extensions
;extension=ctype.so
;extension=curl.so
;extension=dom.so
;extension=exif.so
;extension=ftp.so
;extension=gd.so
;extension=gmp.so
;extension=hash.so
;extension=iconv.so
;extension=json.so
;extension=ldap.so
;extension=mbstring.so
;extension=mcrypt.so
;extension=mysql.so
;extension=openssl.so
;extension=pcre.so
;extension=pdo.so
;extension=pdo-mysql.so
;extension=pdo-pgsql.so
;extension=pdo_sqlite.so
;extension=pgsql.so
;extension=session.so
;extension=soap.so
;extension=sockets.so
;extension=sqlite.so
;extension=tokenizer.so
;extension=xml.so
;extension=xmlreader.so
;extension=xmlwriter.so
; Module Settings
[APC]
apc.enabled = 1
apc.shm_segments = 1 ;The number of shared memory segments to allocate for the compiler cache.
apc.shm_size = 4M ;The size of each shared memory segment.
[Date]
;date.timezone =
;date.default_latitude = 31.7667
;date.default_longitude = 35.2333
;date.sunrise_zenith = 90.583333
;date.sunset_zenith = 90.583333
[filter]
;filter.default = unsafe_raw
;filter.default_flags =
[iconv]
;iconv.input_encoding = ISO-8859-1
;iconv.internal_encoding = ISO-8859-1
;iconv.output_encoding = ISO-8859-1
[sqlite]
;sqlite.assoc_case = 0
[sqlite3]
;sqlite3.extension_dir =
[Pdo_mysql]
pdo_mysql.cache_size = 2000
pdo_mysql.default_socket=
[MySQL]
mysql.allow_local_infile = On
mysql.allow_persistent = On
mysql.cache_size = 2000
mysql.max_persistent = -1
mysql.max_links = -1
mysql.default_port =
mysql.default_socket =
mysql.default_host =
mysql.default_user =
mysql.default_password =
mysql.connect_timeout = 60
mysql.trace_mode = Off
[PostgresSQL]
pgsql.allow_persistent = On
pgsql.auto_reset_persistent = Off
pgsql.max_persistent = -1
pgsql.max_links = -1
pgsql.ignore_notice = 0
pgsql.log_notice = 0
[Session]
session.save_handler = files
session.save_path = "/tmp"
session.use_cookies = 1
;session.cookie_secure =
session.use_only_cookies = 1
session.name = PHPSESSID
session.auto_start = 0
session.cookie_lifetime = 0
session.cookie_path = /
session.cookie_domain =
session.cookie_httponly = 1
session.serialize_handler = php
session.gc_probability = 1
session.gc_divisor = 100
session.gc_maxlifetime = 1440
session.bug_compat_42 = On
session.bug_compat_warn = On
session.referer_check =
session.entropy_length = 0
;session.entropy_file = /dev/urandom
session.entropy_file =
;session.entropy_length = 16
session.cache_limiter = nocache
session.cache_expire = 180
session.use_trans_sid = 0
session.hash_function = 0
session.hash_bits_per_character = 4
url_rewriter.tags = "a=href,area=href,frame=src,input=src,form=,fieldset="
[mbstring]
;mbstring.language = Japanese
;mbstring.internal_encoding = EUC-JP
;mbstring.http_input = auto
;mbstring.http_output = SJIS
;mbstring.encoding_translation = Off
;mbstring.detect_order = auto
;mbstring.substitute_character = none;
;mbstring.func_overload = 0
;mbstring.strict_detection = Off
;mbstring.http_output_conv_mimetype=
;mbstring.script_encoding=
[gd]
;gd.jpeg_ignore_warning = 0
[exif]
;exif.encode_unicode = ISO-8859-15
;exif.decode_unicode_motorola = UCS-2BE
;exif.decode_unicode_intel = UCS-2LE
;exif.encode_jis =
;exif.decode_jis_motorola = JIS
;exif.decode_jis_intel = JIS
[soap]
soap.wsdl_cache_enabled=1
soap.wsdl_cache_dir="/tmp"
soap.wsdl_cache_ttl=86400
soap.wsdl_cache_limit = 5
[sysvshm]
;sysvshm.init_mem = 10000
[ldap]
ldap.max_links = -1
[mcrypt]
;mcrypt.algorithms_dir=
;mcrypt.modes_dir=
cgi.force_redirect = 1
cgi.redirect_status_env = "yes";

View File

@ -0,0 +1,19 @@
#!/bin/sh /etc/rc.common
START=50
PROG=/usr/bin/php-fpm
CONFIG=/etc/php7-fpm.conf
SERVICE_PID_FILE=/var/run/php7-fpm.pid
start_instance() {
local section="$1"
local enabled
config_get_bool enabled "$section" 'enabled' 0
[ $enabled -gt 0 ] || return 1
service_start $PROG -y $CONFIG -g $SERVICE_PID_FILE -R
}
start() {
config_load 'php7-fpm'
config_foreach start_instance 'php7-fpm'
}
stop() {
service_stop $PROG
}

View File

@ -0,0 +1,122 @@
;;;;;;;;;;;;;;;;;;;;;
; FPM Configuration ;
;;;;;;;;;;;;;;;;;;;;;
; All relative paths in this configuration file are relative to PHP's install
; prefix (/usr). This prefix can be dynamically changed by using the
; '-p' argument from the command line.
; Include one or more files. If glob(3) exists, it is used to include a bunch of
; files from a glob(3) pattern. This directive can be used everywhere in the
; file.
; Relative path can also be used. They will be prefixed by:
; - the global prefix if it's been set (-p argument)
; - /usr otherwise
;include=/etc/php7/fpm/*.conf
;;;;;;;;;;;;;;;;;;
; Global Options ;
;;;;;;;;;;;;;;;;;;
[global]
; Pid file
; Note: the default prefix is /var
; Default Value: none
pid = /var/run/php7-fpm.pid
; Error log file
; If it's set to "syslog", log is sent to syslogd instead of being written
; in a local file.
; Note: the default prefix is /var
; Default Value: log/php-fpm.log
; error_log = /var/log/php7-fpm.log
error_log = syslog
; syslog_facility is used to specify what type of program is logging the
; message. This lets syslogd specify that messages from different facilities
; will be handled differently.
; See syslog(3) for possible values (ex daemon equiv LOG_DAEMON)
; Default Value: daemon
;syslog.facility = daemon
; syslog_ident is prepended to every message. If you have multiple FPM
; instances running on the same server, you can change the default value
; which must suit common needs.
; Default Value: php-fpm
;syslog.ident = php-fpm
; Log level
; Possible Values: alert, error, warning, notice, debug
; Default Value: notice
;log_level = notice
; If this number of child processes exit with SIGSEGV or SIGBUS within the time
; interval set by emergency_restart_interval then FPM will restart. A value
; of '0' means 'Off'.
; Default Value: 0
;emergency_restart_threshold = 0
; Interval of time used by emergency_restart_interval to determine when
; a graceful restart will be initiated. This can be useful to work around
; accidental corruptions in an accelerator's shared memory.
; Available Units: s(econds), m(inutes), h(ours), or d(ays)
; Default Unit: seconds
; Default Value: 0
;emergency_restart_interval = 0
; Time limit for child processes to wait for a reaction on signals from master.
; Available units: s(econds), m(inutes), h(ours), or d(ays)
; Default Unit: seconds
; Default Value: 0
;process_control_timeout = 0
; The maximum number of processes FPM will fork. This has been design to control
; the global number of processes when using dynamic PM within a lot of pools.
; Use it with caution.
; Note: A value of 0 indicates no limit
; Default Value: 0
; process.max = 128
; Specify the nice(2) priority to apply to the master process (only if set)
; The value can vary from -19 (highest priority) to 20 (lower priority)
; Note: - It will only work if the FPM master process is launched as root
; - The pool process will inherit the master process priority
; unless it specified otherwise
; Default Value: no set
; process.priority = -19
; Send FPM to background. Set to 'no' to keep FPM in foreground for debugging.
; Default Value: yes
;daemonize = yes
; Set open file descriptor rlimit for the master process.
; Default Value: system defined value
;rlimit_files = 1024
; Set max core size rlimit for the master process.
; Possible Values: 'unlimited' or an integer greater or equal to 0
; Default Value: system defined value
;rlimit_core = 0
; Specify the event mechanism FPM will use. The following is available:
; - select (any POSIX os)
; - poll (any POSIX os)
; - epoll (linux >= 2.5.44)
; - kqueue (FreeBSD >= 4.1, OpenBSD >= 2.9, NetBSD >= 2.0)
; - /dev/poll (Solaris >= 7)
; - port (Solaris >= 10)
; Default Value: not set (auto detection)
; events.mechanism = epoll
;;;;;;;;;;;;;;;;;;;;
; Pool Definitions ;
;;;;;;;;;;;;;;;;;;;;
; Multiple pools of child processes may be started with different listening
; ports and different management options. The name of the pool will be
; used in logs and stats. There is no limitation on the number of pools which
; FPM can handle. Your system will tell you anyway :)
; To configure the pools it is recommended to have one .conf file per
; pool in the following directory:
include=/etc/php7-fpm.d/*.conf

View File

@ -0,0 +1,392 @@
; Start a new pool named 'www'.
; the variable $pool can we used in any directive and will be replaced by the
; pool name ('www' here)
[www]
; Per pool prefix
; It only applies on the following directives:
; - 'slowlog'
; - 'listen' (unixsocket)
; - 'chroot'
; - 'chdir'
; - 'php_values'
; - 'php_admin_values'
; When not set, the global prefix (or /usr) applies instead.
; Note: This directive can also be relative to the global prefix.
; Default Value: none
;prefix = /path/to/pools/$pool
; Unix user/group of processes
; Note: The user is mandatory. If the group is not set, the default user's group
; will be used.
user = root
group = root
; The address on which to accept FastCGI requests.
; Valid syntaxes are:
; 'ip.add.re.ss:port' - to listen on a TCP socket to a specific address on
; a specific port;
; 'port' - to listen on a TCP socket to all addresses on a
; specific port;
; '/path/to/unix/socket' - to listen on a unix socket.
; Note: This value is mandatory.
listen = /var/run/php7-fpm.sock
; Set listen(2) backlog.
; Default Value: 128 (-1 on FreeBSD and OpenBSD)
;listen.backlog = 128
; Set permissions for unix socket, if one is used. In Linux, read/write
; permissions must be set in order to allow connections from a web server. Many
; BSD-derived systems allow connections regardless of permissions.
; Default Values: user and group are set as the running user
; mode is set to 0666
;listen.owner = www-data
;listen.group = www-data
;listen.mode = 0666
; List of ipv4 addresses of FastCGI clients which are allowed to connect.
; Equivalent to the FCGI_WEB_SERVER_ADDRS environment variable in the original
; PHP FCGI (5.2.2+). Makes sense only with a tcp listening socket. Each address
; must be separated by a comma. If this value is left blank, connections will be
; accepted from any ip address.
; Default Value: any
;listen.allowed_clients = 127.0.0.1
; Specify the nice(2) priority to apply to the pool processes (only if set)
; The value can vary from -19 (highest priority) to 20 (lower priority)
; Note: - It will only work if the FPM master process is launched as root
; - The pool processes will inherit the master process priority
; unless it specified otherwise
; Default Value: no set
; priority = -19
; Choose how the process manager will control the number of child processes.
; Possible Values:
; static - a fixed number (pm.max_children) of child processes;
; dynamic - the number of child processes are set dynamically based on the
; following directives. With this process management, there will be
; always at least 1 children.
; pm.max_children - the maximum number of children that can
; be alive at the same time.
; pm.start_servers - the number of children created on startup.
; pm.min_spare_servers - the minimum number of children in 'idle'
; state (waiting to process). If the number
; of 'idle' processes is less than this
; number then some children will be created.
; pm.max_spare_servers - the maximum number of children in 'idle'
; state (waiting to process). If the number
; of 'idle' processes is greater than this
; number then some children will be killed.
; ondemand - no children are created at startup. Children will be forked when
; new requests will connect. The following parameter are used:
; pm.max_children - the maximum number of children that
; can be alive at the same time.
; pm.process_idle_timeout - The number of seconds after which
; an idle process will be killed.
; Note: This value is mandatory.
pm = dynamic
; The number of child processes to be created when pm is set to 'static' and the
; maximum number of child processes when pm is set to 'dynamic' or 'ondemand'.
; This value sets the limit on the number of simultaneous requests that will be
; served. Equivalent to the ApacheMaxClients directive with mpm_prefork.
; Equivalent to the PHP_FCGI_CHILDREN environment variable in the original PHP
; CGI. The below defaults are based on a server without much resources. Don't
; forget to tweak pm.* to fit your needs.
; Note: Used when pm is set to 'static', 'dynamic' or 'ondemand'
; Note: This value is mandatory.
pm.max_children = 5
; The number of child processes created on startup.
; Note: Used only when pm is set to 'dynamic'
; Default Value: min_spare_servers + (max_spare_servers - min_spare_servers) / 2
pm.start_servers = 2
; The desired minimum number of idle server processes.
; Note: Used only when pm is set to 'dynamic'
; Note: Mandatory when pm is set to 'dynamic'
pm.min_spare_servers = 1
; The desired maximum number of idle server processes.
; Note: Used only when pm is set to 'dynamic'
; Note: Mandatory when pm is set to 'dynamic'
pm.max_spare_servers = 3
; The number of seconds after which an idle process will be killed.
; Note: Used only when pm is set to 'ondemand'
; Default Value: 10s
;pm.process_idle_timeout = 10s;
; The number of requests each child process should execute before respawning.
; This can be useful to work around memory leaks in 3rd party libraries. For
; endless request processing specify '0'. Equivalent to PHP_FCGI_MAX_REQUESTS.
; Default Value: 0
;pm.max_requests = 500
; The URI to view the FPM status page. If this value is not set, no URI will be
; recognized as a status page. It shows the following informations:
; pool - the name of the pool;
; process manager - static, dynamic or ondemand;
; start time - the date and time FPM has started;
; start since - number of seconds since FPM has started;
; accepted conn - the number of request accepted by the pool;
; listen queue - the number of request in the queue of pending
; connections (see backlog in listen(2));
; max listen queue - the maximum number of requests in the queue
; of pending connections since FPM has started;
; listen queue len - the size of the socket queue of pending connections;
; idle processes - the number of idle processes;
; active processes - the number of active processes;
; total processes - the number of idle + active processes;
; max active processes - the maximum number of active processes since FPM
; has started;
; max children reached - number of times, the process limit has been reached,
; when pm tries to start more children (works only for
; pm 'dynamic' and 'ondemand');
; Value are updated in real time.
; Example output:
; pool: www
; process manager: static
; start time: 01/Jul/2011:17:53:49 +0200
; start since: 62636
; accepted conn: 190460
; listen queue: 0
; max listen queue: 1
; listen queue len: 42
; idle processes: 4
; active processes: 11
; total processes: 15
; max active processes: 12
; max children reached: 0
;
; By default the status page output is formatted as text/plain. Passing either
; 'html', 'xml' or 'json' in the query string will return the corresponding
; output syntax. Example:
; http://www.foo.bar/status
; http://www.foo.bar/status?json
; http://www.foo.bar/status?html
; http://www.foo.bar/status?xml
;
; By default the status page only outputs short status. Passing 'full' in the
; query string will also return status for each pool process.
; Example:
; http://www.foo.bar/status?full
; http://www.foo.bar/status?json&full
; http://www.foo.bar/status?html&full
; http://www.foo.bar/status?xml&full
; The Full status returns for each process:
; pid - the PID of the process;
; state - the state of the process (Idle, Running, ...);
; start time - the date and time the process has started;
; start since - the number of seconds since the process has started;
; requests - the number of requests the process has served;
; request duration - the duration in µs of the requests;
; request method - the request method (GET, POST, ...);
; request URI - the request URI with the query string;
; content length - the content length of the request (only with POST);
; user - the user (PHP_AUTH_USER) (or '-' if not set);
; script - the main script called (or '-' if not set);
; last request cpu - the %cpu the last request consumed
; it's always 0 if the process is not in Idle state
; because CPU calculation is done when the request
; processing has terminated;
; last request memory - the max amount of memory the last request consumed
; it's always 0 if the process is not in Idle state
; because memory calculation is done when the request
; processing has terminated;
; If the process is in Idle state, then informations are related to the
; last request the process has served. Otherwise informations are related to
; the current request being served.
; Example output:
; ************************
; pid: 31330
; state: Running
; start time: 01/Jul/2011:17:53:49 +0200
; start since: 63087
; requests: 12808
; request duration: 1250261
; request method: GET
; request URI: /test_mem.php?N=10000
; content length: 0
; user: -
; script: /home/fat/web/docs/php/test_mem.php
; last request cpu: 0.00
; last request memory: 0
;
; Note: There is a real-time FPM status monitoring sample web page available
; It's available in: ${prefix}/share/fpm/status.html
;
; Note: The value must start with a leading slash (/). The value can be
; anything, but it may not be a good idea to use the .php extension or it
; may conflict with a real PHP file.
; Default Value: not set
;pm.status_path = /status
; The ping URI to call the monitoring page of FPM. If this value is not set, no
; URI will be recognized as a ping page. This could be used to test from outside
; that FPM is alive and responding, or to
; - create a graph of FPM availability (rrd or such);
; - remove a server from a group if it is not responding (load balancing);
; - trigger alerts for the operating team (24/7).
; Note: The value must start with a leading slash (/). The value can be
; anything, but it may not be a good idea to use the .php extension or it
; may conflict with a real PHP file.
; Default Value: not set
;ping.path = /ping
; This directive may be used to customize the response of a ping request. The
; response is formatted as text/plain with a 200 response code.
; Default Value: pong
;ping.response = pong
; The access log file
; Default: not set
;access.log = log/$pool.access.log
; The access log format.
; The following syntax is allowed
; %%: the '%' character
; %C: %CPU used by the request
; it can accept the following format:
; - %{user}C for user CPU only
; - %{system}C for system CPU only
; - %{total}C for user + system CPU (default)
; %d: time taken to serve the request
; it can accept the following format:
; - %{seconds}d (default)
; - %{miliseconds}d
; - %{mili}d
; - %{microseconds}d
; - %{micro}d
; %e: an environment variable (same as $_ENV or $_SERVER)
; it must be associated with embraces to specify the name of the env
; variable. Some exemples:
; - server specifics like: %{REQUEST_METHOD}e or %{SERVER_PROTOCOL}e
; - HTTP headers like: %{HTTP_HOST}e or %{HTTP_USER_AGENT}e
; %f: script filename
; %l: content-length of the request (for POST request only)
; %m: request method
; %M: peak of memory allocated by PHP
; it can accept the following format:
; - %{bytes}M (default)
; - %{kilobytes}M
; - %{kilo}M
; - %{megabytes}M
; - %{mega}M
; %n: pool name
; %o: ouput header
; it must be associated with embraces to specify the name of the header:
; - %{Content-Type}o
; - %{X-Powered-By}o
; - %{Transfert-Encoding}o
; - ....
; %p: PID of the child that serviced the request
; %P: PID of the parent of the child that serviced the request
; %q: the query string
; %Q: the '?' character if query string exists
; %r: the request URI (without the query string, see %q and %Q)
; %R: remote IP address
; %s: status (response code)
; %t: server time the request was received
; it can accept a strftime(3) format:
; %d/%b/%Y:%H:%M:%S %z (default)
; %T: time the log has been written (the request has finished)
; it can accept a strftime(3) format:
; %d/%b/%Y:%H:%M:%S %z (default)
; %u: remote user
;
; Default: "%R - %u %t \"%m %r\" %s"
;access.format = "%R - %u %t \"%m %r%Q%q\" %s %f %{mili}d %{kilo}M %C%%"
; The log file for slow requests
; Default Value: not set
; Note: slowlog is mandatory if request_slowlog_timeout is set
;slowlog = log/$pool.log.slow
; The timeout for serving a single request after which a PHP backtrace will be
; dumped to the 'slowlog' file. A value of '0s' means 'off'.
; Available units: s(econds)(default), m(inutes), h(ours), or d(ays)
; Default Value: 0
;request_slowlog_timeout = 0
; The timeout for serving a single request after which the worker process will
; be killed. This option should be used when the 'max_execution_time' ini option
; does not stop script execution for some reason. A value of '0' means 'off'.
; Available units: s(econds)(default), m(inutes), h(ours), or d(ays)
; Default Value: 0
;request_terminate_timeout = 0
; Set open file descriptor rlimit.
; Default Value: system defined value
;rlimit_files = 1024
; Set max core size rlimit.
; Possible Values: 'unlimited' or an integer greater or equal to 0
; Default Value: system defined value
;rlimit_core = 0
; Chroot to this directory at the start. This value must be defined as an
; absolute path. When this value is not set, chroot is not used.
; Note: you can prefix with '$prefix' to chroot to the pool prefix or one
; of its subdirectories. If the pool prefix is not set, the global prefix
; will be used instead.
; Note: chrooting is a great security feature and should be used whenever
; possible. However, all PHP paths will be relative to the chroot
; (error_log, sessions.save_path, ...).
; Default Value: not set
;chroot =
; Chdir to this directory at the start.
; Note: relative path can be used.
; Default Value: current directory or / when chroot
chdir = /
; Redirect worker stdout and stderr into main error log. If not set, stdout and
; stderr will be redirected to /dev/null according to FastCGI specs.
; Note: on highloaded environement, this can cause some delay in the page
; process time (several ms).
; Default Value: no
;catch_workers_output = yes
; Limits the extensions of the main script FPM will allow to parse. This can
; prevent configuration mistakes on the web server side. You should only limit
; FPM to .php extensions to prevent malicious users to use other extensions to
; exectute php code.
; Note: set an empty value to allow all extensions.
; Default Value: .php
;security.limit_extensions = .php .php3 .php4 .php5
; Pass environment variables like LD_LIBRARY_PATH. All $VARIABLEs are taken from
; the current environment.
; Default Value: clean env
;env[HOSTNAME] = $HOSTNAME
;env[PATH] = /usr/local/bin:/usr/bin:/bin
;env[TMP] = /tmp
;env[TMPDIR] = /tmp
;env[TEMP] = /tmp
; Additional php.ini defines, specific to this pool of workers. These settings
; overwrite the values previously defined in the php.ini. The directives are the
; same as the PHP SAPI:
; php_value/php_flag - you can set classic ini defines which can
; be overwritten from PHP call 'ini_set'.
; php_admin_value/php_admin_flag - these directives won't be overwritten by
; PHP call 'ini_set'
; For php_*flag, valid values are on, off, 1, 0, true, false, yes or no.
; Defining 'extension' will load the corresponding shared extension from
; extension_dir. Defining 'disable_functions' or 'disable_classes' will not
; overwrite previously defined php.ini values, but will append the new value
; instead.
; Note: path INI options can be relative and will be expanded with the prefix
; (pool, global or /usr)
; Default Value: nothing is defined by default except the values in php.ini and
; specified at startup with the -d argument
;php_admin_value[sendmail_path] = /usr/sbin/sendmail -t -i -f www@my.domain.com
;php_flag[display_errors] = off
;php_admin_value[error_log] = /var/log/fpm-php.www.log
;php_admin_flag[log_errors] = on
;php_admin_value[memory_limit] = 32M

View File

@ -0,0 +1,46 @@
#!/bin/sh /etc/rc.common
# This is the auto-start script for EvilPortal
START=200
start() {
# Enable ip forward.
echo 1 > /proc/sys/net/ipv4/ip_forward
# Remove old authorized clients list
rm /tmp/EVILPORTAL_CLIENTS.txt
/etc/init.d/php7-fpm start
/etc/init.d/nginx start
# Start DNS MASQ to spoof * for unauthorized clients
dnsmasq --no-hosts --no-resolv --address=/#/172.16.42.1 -p 5353
# Symlink evilportal portal api
rm /www/captiveportal
ln -s /pineapple/ui/modules/evilportal/assets/api /www/captiveportal
# Run iptables commands
iptables -t nat -A PREROUTING -i br-lan -p tcp --dport 443 -j DNAT --to-destination 172.16.42.1:80
iptables -t nat -A PREROUTING -i br-lan -p tcp --dport 80 -j DNAT --to-destination 172.16.42.1:80
iptables -t nat -A PREROUTING -i br-lan -p tcp --dport 53 -j DNAT --to-destination 172.16.42.1:5353
iptables -t nat -A PREROUTING -i br-lan -p udp --dport 53 -j DNAT --to-destination 172.16.42.1:5353
}
stop() {
/etc/init.d/php7-fpm stop
/etc/init.d/nginx stop
kill $(netstat -plant | grep 5353 | awk '{print $NF}' | sed 's/\/dnsmasq//g' | head -n 1)
rm /www/captiveportal
iptables -t nat -D PREROUTING -i br-lan -p tcp --dport 443 -j DNAT --to-destination 172.16.42.1:80
iptables -t nat -D PREROUTING -i br-lan -p tcp --dport 80 -j DNAT --to-destination 172.16.42.1:80
iptables -t nat -D PREROUTING -i br-lan -p tcp --dport 53 -j DNAT --to-destination 172.16.42.1:5353
iptables -t nat -D PREROUTING -i br-lan -p udp --dport 53 -j DNAT --to-destination 172.16.42.1:5353
}
disable() {
rm /etc/rc.d/*evilportal
}

View File

@ -0,0 +1 @@
172.16.42.42

View File

@ -0,0 +1,4 @@
#!/bin/bash
# Commands in this file are ran when a portal is de-activated.
# You can use any interpreter you want to, the default is bash.

View File

@ -0,0 +1,4 @@
#!/bin/bash
# Commands in this file are ran when a portal is activated and when Evil Portal startsup on boot.
# You can use any interpreter you want to, the default is bash.

View File

@ -0,0 +1,33 @@
<?php namespace evilportal;
class MyPortal extends Portal
{
public function handleAuthorization()
{
// handle form input or other extra things there
// Call parent to handle basic authorization first
parent::handleAuthorization();
}
/**
* Override this to do something when the client is successfully authorized.
* By default it just notifies the Web UI.
*/
public function onSuccess()
{
// Calls default success message
parent::onSuccess();
}
/**
* If an error occurs then do something here.
* Override to provide your own functionality.
*/
public function showError()
{
// Calls default error message
parent::showError();
}
}

View File

@ -0,0 +1,55 @@
<?php
/**
* getClientMac
* Gets the mac address of a client by the IP address
* Returns the mac address as a string
* @param $clientIP : The clients IP address
* @return string
*/
function getClientMac($clientIP)
{
return trim(exec("grep " . escapeshellarg($clientIP) . " /tmp/dhcp.leases | awk '{print $2}'"));
}
/**
* getClientSSID
* Gets the SSID a client is associated by the IP address
* Returns the SSID as a string
* @param $clientIP : The clients IP address
* @return string
*/
function getClientSSID($clientIP)
{
if (file_exists("/tmp/log.db"))
{
// Get the clients mac address. We need this to get the SSID
$mac = strtoupper(getClientMac($clientIP));
$db = new SQLite3("/tmp/log.db");
$results = $db->query("select ssid from log WHERE mac = '{$mac}' AND log_type = 0 ORDER BY updated_at DESC LIMIT 1;");
$ssid = '';
while($row = $results->fetchArray())
{
$ssid = $row['ssid'];
break;
}
$db->close();
return $ssid;
}
return '';
}
/**
* getClientHostName
* Gets the host name of the connected client by the IP address
* Returns the host name as a string
* @param $clientIP : The clients IP address
* @return string
*/
function getClientHostName($clientIP)
{
return trim(exec("grep " . escapeshellarg($clientIP) . " /tmp/dhcp.leases | awk '{print $4}'"));
}

View File

@ -0,0 +1,39 @@
<?php
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
$destination = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]";
require_once('helper.php');
?>
<HTML>
<HEAD>
<title>Evil Portal</title>
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<meta name="viewport" content="width=device-width, initial-scale=1">
</HEAD>
<BODY>
<div style="text-align: center;">
<h1>Evil Portal</h1>
<p>This is the default Evil Portal page.</p>
<p>The SSID you are connected to is <?=getClientSSID($_SERVER['REMOTE_ADDR']);?></p>
<p>Your host name is <?=getClientHostName($_SERVER['REMOTE_ADDR']);?></p>
<p>Your MAC Address is <?=getClientMac($_SERVER['REMOTE_ADDR']);?></p>
<p>Your internal IP address is <?=$_SERVER['REMOTE_ADDR'];?></p>
<form method="POST" action="/captiveportal/index.php">
<input type="hidden" name="target" value="<?=$destination?>">
<button type="submit">Authorize</button>
</form>
</div>
</BODY>
</HTML>

View File

@ -0,0 +1,4 @@
{
"name": null,
"type": "basic"
}

View File

@ -0,0 +1,4 @@
#!/bin/bash
# Commands in this file are ran when a portal is de-activated.
# You can use any interpreter you want to, the default is bash.

View File

@ -0,0 +1,4 @@
#!/bin/bash
# Commands in this file are ran when a portal is activated and when Evil Portal startsup on boot.
# You can use any interpreter you want to, the default is bash.

View File

@ -0,0 +1,33 @@
<?php namespace evilportal;
class MyPortal extends Portal
{
public function handleAuthorization()
{
// handle form input or other extra things there
// Call parent to handle basic authorization first
parent::handleAuthorization();
}
/**
* Override this to do something when the client is successfully authorized.
* By default it just notifies the Web UI.
*/
public function onSuccess()
{
// Calls default success message
parent::onSuccess();
}
/**
* If an error occurs then do something here.
* Override to provide your own functionality.
*/
public function showError()
{
// Calls default error message
parent::showError();
}
}

View File

@ -0,0 +1,35 @@
<?php
$destination = "http://" . $_SERVER['HTTP_HOST'] . $_SERVER['HTTP_URI'] . "";
require_once('helper.php');
?>
<HTML>
<HEAD>
<title>Evil Portal</title>
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<script type="text/javascript">
function redirect() { setTimeout(function(){window.location = "/captiveportal/index.php";},100);}
</script>
</HEAD>
<BODY>
<div style="text-align: center;">
<h1>Evil Portal</h1>
<p>This is the default Evil Portal page.</p>
<p>The SSID you are connected to is <?=getClientSSID($_SERVER['REMOTE_ADDR']);?></p>
<p>Your host name is <?=getClientHostName($_SERVER['REMOTE_ADDR']);?></p>
<p>Your MAC Address is <?=getClientMac($_SERVER['REMOTE_ADDR']);?></p>
<p>Your internal IP address is <?=$_SERVER['REMOTE_ADDR'];?></p>
<form method="POST" action="/captiveportal/index.php" onsubmit="redirect()">
<input type="hidden" name="target" value="<?=$destination?>">
<button type="submit">Authorize</button>
</form>
</div>
</BODY>
</HTML>

View File

@ -0,0 +1,54 @@
<?php
/**
* getClientMac
* Gets the mac address of a client by the IP address
* Returns the mac address as a string
* @param $clientIP : The clients IP address
* @return string
*/
function getClientMac($clientIP)
{
return trim(exec("grep " . escapeshellarg($clientIP) . " /tmp/dhcp.leases | awk '{print $2}'"));
}
/**
* getClientSSID
* Gets the SSID a client is associated by the IP address
* Returns the SSID as a string
* @param $clientIP : The clients IP address
* @return string
*/
function getClientSSID($clientIP)
{
if (file_exists("/tmp/log.db"))
{
// Get the clients mac address. We need this to get the SSID
$mac = strtoupper(getClientMac($clientIP));
$db = new SQLite3("/tmp/log.db");
$results = $db->query("SELECT ssid FROM log WHERE mac = '{$mac}' AND log_type = 0 ORDER BY updated_at DESC LIMIT 1;");
$ssid = '';
while($row = $results->fetchArray())
{
$ssid = $row['ssid'];
break;
}
$db->close();
return $ssid;
}
return '';
}
/**
* getClientHostName
* Gets the host name of the connected client by the IP address
* Returns the host name as a string
* @param $clientIP : The clients IP address
* @return string
*/
function getClientHostName($clientIP)
{
return trim(exec("grep " . escapeshellarg($clientIP) . " /tmp/dhcp.leases | awk '{print $4}'"));
}

View File

@ -0,0 +1,88 @@
<?php
// Make sure this page is never cached.
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
require_once('helper.php');
/**
*
* DO NOT MODIFY THIS FILE
* I highly recommend against modifying this file unless you know what you are doing!
*
* This file handles determining the destination file a client should see based on the conditions set in the json.
*
*/
// The value for this variable needs to be set when an new instance of a portal is created
// EvilPortal does this automatically when a targeted portal is created by running:
// sed -i 's/"portal_name_here"/"{portalName}"/g' index.php
$PORTAL_NAME = "portal_name_here";
// Get the information about the client and map
// it to the rule key specified in the {$PORTAL_NAME}.ep file
$MAPPED_RULES = [
"mac" => getClientMac($_SERVER['REMOTE_ADDR']),
"ssid" => getClientSSID($_SERVER['REMOTE_ADDR']),
"hostname" => getClientHostName($_SERVER['REMOTE_ADDR']),
"useragent" => $_SERVER['HTTP_USER_AGENT']
];
// Read the json
$jsonData = json_decode(file_get_contents("{$PORTAL_NAME}.ep"), true);
$routeData = $jsonData['targeted_rules'];
// This variable represents the page to include
$includePage = null;
// Check rules to find the page
foreach ($routeData['rule_order'] as $key) {
$includePage = handle_rule($routeData['rules'][$key], $MAPPED_RULES[$key]);
if ($includePage != null) {
include $includePage;
break;
}
}
// We have to display something.
// If the includePage variable is still null after checking the rules
// then include the default page.
if ($includePage == null) {
include $routeData['default'];
}
/**
* Checks if a given rule matches a given value
* @param $rules: The rules to check the client data against
* @param $client_data: The data to check if the rules match
* @return string: If a rule matches it returns the page to include, null otherwise
*/
function handle_rule($rules, $client_data) {
$return_value = null;
foreach ($rules as $key => $val) {
switch($key) {
case "exact": // exact matches
if (isset($val[$client_data])) {
$return_value = $val[$client_data];
break 2; // break out of the loop
}
break 1;
case "regex": // regex matches
foreach($val as $expression => $destination) {
if (preg_match($expression, $client_data)) {
$return_value = $destination;
break 1; // match was found. Exit this loop
}
if ($return_value != null)
break 2; // break out of the main loop
}
break 1;
}
}
return $return_value;
}

View File

@ -0,0 +1,34 @@
{
"name": null,
"type": "targeted",
"targeted_rules": {
"default": "default.php",
"rule_order": ["mac", "ssid", "hostname", "useragent"],
"rules": {
"mac": {
"exact": {
},
"regex": {
}
},
"ssid": {
"exact": {
},
"regex": {
}
},
"hostname": {
"exact": {
},
"regex": {
}
},
"useragent": {
"exact": {
},
"regex": {
}
}
}
}
}

View File

@ -0,0 +1,76 @@
.dependency-card {
max-width: 600px;
min-height: 246px;
}
.dependency-card-centered {
display: flex;
justify-content: center;
align-self: center;
}
.dependency-card-button {
width: 75%;
display: flex;
justify-content: center;
align-items: center;
}
.spinner-padding {
margin-top: 6px;
margin-bottom: 7px;
}
.controls-container {
display: flex;
width: 100%;
flex-wrap: wrap;
justify-content: space-between;
}
.card-container {
display: flex;
}
.right-left-split-container {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-self: flex-start;
}
.control-item {
flex: 1;
}
.client-textarea {
width: 100%;
height: 150px;
min-height: 50px;
max-height: 200px;
resize: vertical;
margin: 0 0 5px;
padding: 0;
background-color: #efefef;
border-radius: 2px;
border-color: #cecece;
max-lines: 50;
}
.output-textarea {
width: 100%;
height: 300px;
min-height: 200px;
max-height: 600px;
resize: vertical;
margin: 0 0 5px;
padding: 0;
background-color: #efefef;
border-radius: 2px;
border-color: #cecece;
max-lines: 50;
}
.control-button {
margin-right: 4px;
}

View File

@ -0,0 +1,235 @@
<div class="dependency-card-centered" *ngIf="!hasDependencies">
<mat-card class="dependency-card">
<mat-card-content>
<mat-card-title>Welcome To Evil Portal</mat-card-title>
<mat-card-subtitle>Lets get started.</mat-card-subtitle>
<mat-divider></mat-divider>
<br />
<p>You need to install some dependencies before you can use this module.</p>
<p>This will make an request to the internet.</p>
<br/>
<br/>
<div class="dependency-card-centered">
<button mat-raised-button color="accent"
class="dependency-card-button"
(click)="installDependencies()"
[disabled]="isInstalling">
<span *ngIf="isInstalling">
<mat-spinner [diameter]="20" class="spinner-padding" color="accent"></mat-spinner>
</span>
<span *ngIf="!isInstalling">
Install Dependencies
</span>
</button>
</div>
</mat-card-content>
</mat-card>
</div>
<div class="controls-container" fxLayoutGap="20px" fxLayout="row" fxLayout.xs="column" fxLayout.sm="column" *ngIf="hasDependencies">
<mat-card class="control-item">
<mat-card-content>
<div class="right-left-split-container">
<mat-card-title>Evil Portal</mat-card-title>
<span *ngIf="controlState.isBusy"><mat-spinner [diameter]="24" color="accent" style="margin-left: 8px"></mat-spinner></span>
<span fxFlex></span>
<mat-menu #evilportalMenu>
<button mat-menu-item (click)="loadControlState();">Refresh</button>
<button mat-menu-item (click)="showUninstallDependencies();">Uninstall Dependencies</button>
</mat-menu>
<button mat-icon-button [matMenuTriggerFor]="evilportalMenu">
<mat-icon>more_vert</mat-icon>
</button>
</div>
<br/><mat-divider></mat-divider><br/>
<button mat-raised-button color="accent" style="width: 100%;" *ngIf="!controlState.running" (click)="toggleEvilPortal();" [disabled]="controlState.isBusy">Start</button>
<button mat-raised-button color="warn" style="width: 100%;" *ngIf="controlState.running" (click)="toggleEvilPortal()" [disabled]="controlState.isBusy">Stop</button>
<br/><br/>
<button mat-raised-button color="accent" style="width: 100%;" *ngIf="!controlState.webserver" (click)="toggleWebserver();" [disabled]="controlState.isBusy">Start Web Server</button>
<button mat-raised-button color="warn" style="width: 100%;" *ngIf="controlState.webserver" (click)="toggleWebserver();" [disabled]="controlState.isBusy">Stop Web Server</button>
<br/><br/>
<button mat-raised-button color="accent" style="width: 100%;" *ngIf="!controlState.autoStart" [disabled]="controlState.isBusy" (click)="toggleAutostart();">Enable on boot</button>
<button mat-raised-button color="warn" style="width: 100%;" *ngIf="controlState.autoStart" [disabled]="controlState.isBusy" (click)="toggleAutostart();">Disable on boot</button>
</mat-card-content>
</mat-card>
<mat-card class="control-item">
<mat-card-content>
<div class="right-left-split-container">
<mat-card-title>Permanent Clients</mat-card-title>
<mat-card-subtitle style="color: red">{{ permanentClientState.error }}</mat-card-subtitle>
<span fxFlex></span>
<button mat-icon-button *ngIf="!permanentClientState.isBusy" (click)="loadPermanentClients();"><mat-icon>refresh</mat-icon></button>
<span *ngIf="permanentClientState.isBusy"><mat-spinner [diameter]="24" color="accent" style="margin-left: 8px"></mat-spinner></span>
</div>
<br/><mat-divider></mat-divider><br/>
<textarea #permanentClients
class="client-textarea"
[(ngModel)]="permanentClientState.clients"
(mouseup)="getLineNumber('permanentClients');"
readonly></textarea>
<div class="right-left-split-container">
<form>
<mat-form-field>
<mat-label>
Client IP
</mat-label>
<input matInput [(ngModel)]="permanentClientState.selected" name="selectedPermanent" />
</mat-form-field>
</form>
<span fxFlex></span>
<button
mat-button
color="accent"
[disabled]="permanentClientState.isBusy"
(click)="updateClientList(permanentClientState.selected, 'permanentClients', true);">Add</button>
<button
mat-button
color="warn"
[disabled]="permanentClientState.isBusy"
(click)="updateClientList(permanentClientState.selected, 'permanentClients', false);">Remove</button>
</div>
</mat-card-content>
</mat-card>
<mat-card class="control-item">
<mat-card-content>
<div class="right-left-split-container">
<mat-card-title>Allowed Clients</mat-card-title>
<mat-card-subtitle style="color: red;">{{ allowedClientState.error }}</mat-card-subtitle>
<span fxFlex></span>
<button mat-icon-button *ngIf="!allowedClientState.isBusy" (click)="loadAllowedClients();"><mat-icon>refresh</mat-icon></button>
<span *ngIf="allowedClientState.isBusy"><mat-spinner [diameter]="24" color="accent" style="margin-left: 8px"></mat-spinner></span>
</div>
<br/><mat-divider></mat-divider><br/>
<textarea #allowedClients
class="client-textarea"
[(ngModel)]="allowedClientState.clients"
(mouseup)="getLineNumber('allowedClients');"
readonly></textarea>
<div class="right-left-split-container">
<form>
<mat-form-field>
<mat-label>
Client IP
</mat-label>
<input matInput [(ngModel)]="allowedClientState.selected" [disabled]="!controlState.running" name="selectedAllowed" />
</mat-form-field>
</form>
<span fxFlex></span>
<button mat-button color="accent"
[disabled]="!controlState.running || allowedClientState.isBusy"
(click)="updateClientList(allowedClientState.selected, 'allowedClients', true);">
Add
</button>
<button mat-button color="warn"
[disabled]="!controlState.running || allowedClientState.isBusy"
(click)="updateClientList(allowedClientState.selected, 'allowedClients', false);">
Remove
</button>
</div>
</mat-card-content>
</mat-card>
</div>
<br/>
<mat-card *ngIf="hasDependencies && libraryState.showLibrary">
<mat-card-content>
<div class="right-left-split-container">
<mat-card-title>Portal Library</mat-card-title>
<span *ngIf="libraryState.isBusy"><mat-spinner [diameter]="24" color="accent" style="margin-left: 8px"></mat-spinner></span>
<span fxFlex></span>
<button mat-raised-button color="accent" (click)="showNewPortalDialog();">New Portal</button>
</div>
<br/><mat-divider></mat-divider><br/>
<mat-table style="display: none">
<mat-header-row *matHeaderRowDef="[]"></mat-header-row>
</mat-table>
<table *ngIf="libraryState.portals.length > 0"class="mat-table" style="min-width: 100%; overflow-x: auto; justify-content: left;">
<thead>
<tr class="mat-header-row">
<th class="mat-header-cell">Portal Name</th>
<th class="mat-header-cell">Portal Type</th>
<th class="mat-header-cell">Activate</th>
<th class="mat-header-cell">Actions</th>
</tr>
</thead>
<tbody>
<ng-container *ngFor="let portal of libraryState.portals">
<tr class="mat-row">
<td class="mat-cell">
<button mat-button color="accent" style="text-transform: capitalize" (click)="loadPortal(portal);">{{ portal.title }}</button>
</td>
<td class="mat-cell" style="text-transform: capitalize">{{ portal.portal_type }}</td>
<td class="mat-cell">
<button mat-button color="accent" *ngIf="!portal.active" (click)="togglePortal(portal.title);">Activate</button>
<button mat-button color="warn" *ngIf="portal.active" (click)="togglePortal(portal.title);">Deactivate</button>
</td>
<td class="mat-cell">
<button mat-raised-button color="accent" class="control-button" (click)="downloadPortal(portal);">Download</button>
<button mat-raised-button color="warn" class="control-button" *ngIf="!portal.active" (click)="showDeletePortalDialog(portal.location);">Delete</button>
<button mat-raised-button color="accent" class="control-button" *ngIf="portal.active" (click)="showPreviewDialog();">Preview</button>
<button mat-raised-button color="accent" class="control-button" *ngIf="portal.active" class="control-button" (click)="editFile(portal, '.logs', true);">View Log</button>
</td>
</tr>
</ng-container>
</tbody>
</table>
<p *ngIf="libraryState.portals.length == 0 && libraryState.isBusy"><i>Loading Portal Library...</i></p>
<p *ngIf="libraryState.portals.length == 0 && !libraryState.isBusy"><i>No portals to display.</i></p>
</mat-card-content>
</mat-card>
<mat-card *ngIf="hasDependencies && !libraryState.showLibrary">
<mat-card-content>
<div class="right-left-split-container">
<mat-card-title>Work Bench: <i>{{ workbenchState.portal.title }}</i></mat-card-title>
<span *ngIf="workbenchState.isBusy"><mat-spinner [diameter]="24" color="accent" style="margin-left: 8px"></mat-spinner></span>
<span fxFlex></span>
<div>
<button mat-raised-button color="accent" class="control-button" (click)="libraryState.showLibrary = true;">Back</button>
<button mat-raised-button color="accent" class="control-button" (click)="editFile(workbenchState.portal, '.logs', true);">View Log</button>
<button mat-raised-button color="accent" class="control-button" (click)="editFile(workbenchState.portal);">New File</button>
<button mat-raised-button color="accent" class="control-button" *ngIf="workbenchState.portal.portal_type === 'targeted';" (click)="showPortalRules(workbenchState.portal);">Target Rule Editor</button>
<button mat-raised-button color="accent" class="control-button" (click)="loadPortal(workbenchState.portal);">Refresh</button>
</div>
</div>
<br/><mat-divider></mat-divider><br/>
<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;">
<thead>
<tr class="mat-header-row">
<th class="mat-header-cell">Name</th>
<th class="mat-header-cell">Size</th>
<th class="mat-header-cell">Edit</th>
<th class="mat-header-cell">Delete</th>
</tr>
</thead>
<tbody>
<ng-container *ngFor="let file of workbenchState.dirContents">
<tr class="mat-row">
<td class="mat-cell">{{ file.name }}</td>
<td class="mat-cell">{{ file.size }}</td>
<td class="mat-cell"><button mat-raised-button color="accent" *ngIf="!file.directory" (click)="editFile(workbenchState.portal, file.name);">Edit</button></td>
<td class="mat-cell"><button mat-raised-button color="warn" *ngIf="file.deletable" (click)="showDeleteItemDialog(file.path);">Delete</button></td>
</tr>
</ng-container>
</tbody>
</table>
</mat-card-content>
</mat-card>

View File

@ -0,0 +1,493 @@
import {Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
import { ApiService } from '../services/api.service';
import {ErrorDialogComponent} from "./helpers/error-dialog/error-dialog.component";
import {MatDialog} from "@angular/material/dialog";
import {JobResultDTO} from "../interfaces/jobresult.interface";
import {PortalInfoDTO} from "../interfaces/portalinfo.interface";
import {ControlState} from "../interfaces/controlstate.interface";
import {NewPortalDialogComponent} from "./helpers/new-portal-dialog/new-portal-dialog.component";
import {PreviewDialogComponent} from "./helpers/preview-dialog/preview-dialog.component";
import {ConfirmationDialogComponent} from "./helpers/confirmation-dialog/confirmation-dialog.component";
import {DirectoryDTO} from "../interfaces/directorydto.interface";
import {WorkBenchState} from "../interfaces/workbenchstate.interface";
import {LibraryState} from "../interfaces/librarystate.interface";
import {EditFileDialogComponent} from "./helpers/edit-file-dialog/edit-file-dialog.component";
import {RuleEditorDialogComponent} from "./helpers/rule-editor-dialog/rule-editor-dialog.component";
import {ClientListState} from "../interfaces/clientliststate.interface";
import {UninstallDialogComponent} from "./helpers/uninstall-dialog/uninstall-dialog.component";
@Component({
selector: 'lib-evilportal',
templateUrl: './evilportal.component.html',
styleUrls: ['./evilportal.component.css']
})
export class EvilPortalComponent implements OnInit, OnDestroy {
@ViewChild('allowedClients', {static: false}) macFilterPoolRef;
@ViewChild('permanentClients', {static: false}) ssidFilterPoolRef;
public controlState: ControlState = { isBusy: false, running: false, webserver: false, autoStart: false };
public permanentClientState: ClientListState = { isBusy: false, clients: '', selected: '', error: null };
public allowedClientState: ClientListState = { isBusy: false, clients: '', selected: '', error: null };
public libraryState: LibraryState = { showLibrary: true, isBusy: false, portals: [] };
public workbenchState: WorkBenchState = { isBusy: false, portal: null, dirContents: [], inRoot: true, rootDirectory: null };
public hasDependencies: boolean = true;
public isInstalling: boolean = false;
private backgroundJobInterval = null;
constructor(private API: ApiService,
private dialog: MatDialog) { }
private handleError(msg: string): void {
this.dialog.closeAll();
this.dialog.open(ErrorDialogComponent, {
hasBackdrop: true,
width: '500px',
data: {
message: msg
}
});
}
private pollBackgroundJob<T>(jobId: string, onComplete: (result: JobResultDTO<T>) => void, onInterval?: Function): void {
this.backgroundJobInterval = setInterval(() => {
this.API.request({
module: 'evilportal',
action: 'poll_job',
job_id: jobId
}, (response: JobResultDTO<T>) => {
if (response.is_complete) {
onComplete(response);
clearInterval(this.backgroundJobInterval);
} else if (onInterval) {
onInterval();
}
});
}, 5000);
}
private delete(path: string, onSuccess: () => void): void {
this.API.request({
module: 'evilportal',
action: 'delete',
file_path: path
}, (response) => {
if (response.error !== undefined) {
this.handleError(response.error);
return;
}
console.log('CALLING CALLBACK!');
onSuccess();
});
}
private monitorDependencies(jobId: string) {
this.isInstalling = true;
this.pollBackgroundJob(jobId, (result: JobResultDTO<boolean>) => {
this.isInstalling = false;
if (result.job_error !== null) {
this.handleError(result.job_error);
}
this.checkForDependencies();
});
}
private loadDirectoryContent(path: string, onSuccess: (data: Array<DirectoryDTO>) => void) {
this.API.request({
module: 'evilportal',
action: 'load_directory',
path: path
}, (response) => {
if (response.error !== undefined) {
this.handleError(response.error);
return;
}
onSuccess(response);
});
}
private loadFile(path: string, onComplete: (success: boolean, content: string, error: string) => void) {
this.API.request({
module: 'evilportal',
action: 'load_file',
path: path
}, (response) => {
if (response === undefined || response.error !== undefined) {
let error = (response !== undefined) ? response.error : '';
onComplete(false, undefined, error);
return
}
onComplete(true, response, undefined);
});
}
getLineNumber(target: string): void {
if (target === 'allowedClients') {
const el = this.macFilterPoolRef.nativeElement;
const lineNumber = el.value.substr(0, el.selectionStart).split('\n').length;
this.allowedClientState.selected = el.value.split('\n')[lineNumber - 1].trim();
} else if (target === 'permanentClients') {
const el = this.ssidFilterPoolRef.nativeElement;
const lineNumber = el.value.substr(0, el.selectionStart).split('\n').length;
this.permanentClientState.selected = el.value.split('\n')[lineNumber - 1].trim();
}
}
updateClientList(client: string, list: string, add: boolean): void {
if (list === 'allowedClients') {
this.allowedClientState.isBusy = true;
} else if (list == 'permanentClients') {
this.permanentClientState.isBusy = true;
} else {
return;
}
this.API.request({
module: 'evilportal',
action: 'update_client_list',
add: add,
client: client,
list: list
}, (response) => {
if (list === 'allowedClients') {
this.allowedClientState.isBusy = false;
this.allowedClientState.selected = '';
this.loadAllowedClients();
} else if (list == 'permanentClients') {
this.permanentClientState.isBusy = false;
this.permanentClientState.selected = '';
this.loadPermanentClients();
}
if (response.error !== undefined) {
this.handleError(response.error);
return;
}
});
}
showPreviewDialog(): void {
if (!this.controlState.webserver) {
this.handleError('You can not preview the portal unless the webserver is running.\n Please start it and try again.');
return;
}
this.dialog.open(PreviewDialogComponent, {
hasBackdrop: true,
data: {}
});
}
togglePortal(portalName: string): void {
this.libraryState.isBusy = true;
this.API.request({
module: 'evilportal',
action: 'toggle_portal',
portal: portalName
}, (response) => {
this.libraryState.isBusy = false;
if (response.error !== undefined) {
this.handleError(response.error);
return;
}
this.loadPortals();
});
}
createPortal(portalName: string, portalType: string): void {
this.libraryState.isBusy = true;
this.API.request({
module: 'evilportal',
action: 'new_portal',
name: portalName,
type: portalType
}, (response) => {
this.libraryState.isBusy = false;
if (response.error !== undefined) {
this.handleError(response.error);
return;
}
this.loadPortals();
});
}
showDeleteItemDialog(path: string): void {
this.dialog.open(ConfirmationDialogComponent, {
hasBackdrop: true,
width: '500px',
data: {
title: "Delete",
message: "Are you sure you want to delete this this? This action can not be undone.",
handleResponse: (affimative: boolean) => {
if (affimative) {
this.workbenchState.isBusy = true;
this.delete(path, () => {
this.loadPortal(this.workbenchState.portal);
});
}
}
}
});
}
showDeletePortalDialog(path: string): void {
this.dialog.open(ConfirmationDialogComponent, {
hasBackdrop: true,
width: '500px',
data: {
title: "Delete Portal?",
message: "Are you sure you want to delete this portal? This action can not be undone.",
handleResponse: (affimative: boolean) => {
if (affimative) {
this.libraryState.isBusy = true;
this.delete(path, () => {
this.loadPortals();
});
}
}
}
});
}
showNewPortalDialog(): void {
this.dialog.open(NewPortalDialogComponent, {
hasBackdrop: true,
width: '900px',
data: {
onCreate: (portalName: string, portalType: string) => {
this.createPortal(portalName, portalType);
this.dialog.closeAll();
}
}
});
}
editFile(portal: PortalInfoDTO, fileName = null, readonly = false): void {
this.dialog.open(EditFileDialogComponent, {
hasBackdrop: true,
width: '900px',
data: {
readonly: readonly,
path: portal.location,
fileName: fileName,
isNew: fileName == null,
onSaved: () => {
this.loadPortal(this.workbenchState.portal);
}
}
});
}
showPortalRules(portal: PortalInfoDTO): void {
this.dialog.open(RuleEditorDialogComponent, {
hasBackdrop: true,
data: {
portal: portal.title
}
});
}
toggleWebserver(): void {
this.controlState.isBusy = true;
this.API.request({
module: 'evilportal',
action: 'toggle_webserver'
}, (response) => {
this.controlState.isBusy = false;
if (response.error !== undefined) {
this.handleError(response.error);
return;
}
this.loadControlState();
});
}
toggleEvilPortal(): void {
this.controlState.isBusy = true;
this.API.request({
module: 'evilportal',
action: 'toggle_evilportal'
}, (response) => {
this.controlState.isBusy = false;
if (response.error !== undefined) {
this.handleError(response.error)
return;
}
this.loadControlState();
this.loadAllowedClients();
});
}
toggleAutostart(): void {
this.controlState.isBusy = true;
this.API.request({
module: 'evilportal',
action: 'toggle_autostart'
}, (response) => {
this.controlState.isBusy = false;
if (response.error !== undefined) {
this.handleError(response.error);
return;
}
this.loadControlState();
});
}
downloadPortal(portal: PortalInfoDTO): void {
this.API.request({
module: 'evilportal',
action: 'archive_portal',
portal: portal.title
}, (response) => {
if (response.error !== undefined) {
this.handleError(response.error);
return;
}
this.API.APIDownload(response, portal.title + '.tar.gz');
this.delete(response, () => {});
});
}
loadPortal(portal: PortalInfoDTO): void {
this.workbenchState.isBusy = true;
this.loadDirectoryContent(portal.location, (data) => {
this.workbenchState = {
isBusy: false,
portal: portal,
dirContents: data,
inRoot: true,
rootDirectory: portal.location
};
this.libraryState.showLibrary = false;
});
}
loadPortals(): void {
this.libraryState.isBusy = true;
this.API.request({
module: 'evilportal',
action: 'list_portals'
}, (response) => {
this.libraryState.isBusy = false;
if (response.error !== undefined) {
this.handleError(response.error);
return;
}
this.libraryState.portals = response;
})
}
loadAllowedClients(): void {
this.allowedClientState.isBusy = true;
this.loadFile('/tmp/EVILPORTAL_CLIENTS.txt', (success, content, error) => {
this.allowedClientState.isBusy = false;
if (!success) {
this.allowedClientState.error = error;
this.allowedClientState.clients = '';
return;
}
this.allowedClientState.error = null;
this.allowedClientState.clients = content;
});
}
loadPermanentClients(): void {
this.permanentClientState.isBusy = true;
this.loadFile('/pineapple/ui/modules/evilportal/assets/permanentclients.txt', (success, content, error) => {
this.permanentClientState.isBusy = false;
if (!success) {
this.permanentClientState.error = error;
this.permanentClientState.clients = '';
return;
}
this.permanentClientState.clients = content;
});
}
loadControlState(): void {
this.controlState.isBusy = true;
this.API.request({
module: 'evilportal',
action: 'status'
}, (response) => {
this.controlState.isBusy = false;
if (response.error !== undefined) {
this.handleError(response.error);
return;
}
this.controlState = {
isBusy: false,
running: response.running,
webserver: response.webserver,
autoStart: response.start_on_boot
}
})
}
showUninstallDependencies(): void {
this.dialog.open(UninstallDialogComponent, {
disableClose: true,
hasBackdrop: true,
width: '700px',
data: {
onComplete: () => {
this.checkForDependencies();
}
}
})
}
installDependencies(): void {
this.API.request({
module: 'evilportal',
action: 'manage_dependencies',
install: true
}, (response) => {
if (response.error !== undefined && response.error !== null) {
this.handleError(response.error);
return;
}
this.monitorDependencies(response.job_id);
});
}
checkForDependencies(): void {
this.API.request({
module: 'evilportal',
action: 'check_dependencies'
}, (response) => {
if (response.error !== undefined) {
this.handleError(response.error);
return;
}
this.hasDependencies = response.installed;
if (response.installing) {
this.isInstalling = true;
this.monitorDependencies(response.job_id);
}
});
}
ngOnInit() {
this.checkForDependencies();
this.loadControlState();
this.loadPortals();
this.loadAllowedClients();
this.loadPermanentClients();
}
ngOnDestroy() {
clearInterval(this.backgroundJobInterval);
}
}

View File

@ -0,0 +1,12 @@
.confirmation-dialog-body-container {
display: flex;
flex-direction: column;
}
.confirmation-dialog-buttons {
margin-top: 16px;
}
.confirmation-dialog-button-first {
margin-right: 10px;
}

View File

@ -0,0 +1,19 @@
<div class="right-left-split-container">
<h1 mat-dialog-title>{{ title }}</h1>
<span fxFlex></span>
<button mat-icon-button (click)="closeDialog();">
<mat-icon>close</mat-icon>
</button>
</div>
<mat-divider></mat-divider>
<br/>
<div class="confirmation-dialog-body-container">
<p>{{ message }}</p>
<div class="confirmation-dialog-buttons">
<span fxFlex></span>
<button mat-raised-button class="confirmation-dialog-button-first" (click)="handleResponse(false);">No</button>
<button mat-raised-button color="warn" (click)="handleResponse(true);">Yes</button>
</div>
</div>

View File

@ -0,0 +1,33 @@
import {Component, Inject, OnInit} from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {ConfirmationDialogDelegate} from "../../../interfaces/confirmationdialogdelegate.interface";
@Component({
selector: 'lib-confirmation-dialog',
templateUrl: './confirmation-dialog.component.html',
styleUrls: ['./confirmation-dialog.component.css']
})
export class ConfirmationDialogComponent implements OnInit {
constructor(public dialogRef: MatDialogRef<ConfirmationDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: ConfirmationDialogDelegate) {
this.title = data.title;
this.message = data.message;
}
public title: string;
public message: string;
closeDialog(): void {
this.dialogRef.close();
}
handleResponse(affirmative: boolean): void {
this.closeDialog();
this.data.handleResponse(affirmative);
}
ngOnInit(): void {
}
}

View File

@ -0,0 +1,37 @@
.file-editor-title-container {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-self: flex-start;
}
.file-editor-title {
font-size: 24px;
}
.file-editor-body-container {
height: 500px;
display: flex;
flex-direction: column;
}
.new-portal-buttons {
margin-top: 16px;
}
.new-portal-button-first {
margin-right: 10px;
}
.file-editor-text-area {
width: 100%;
min-height: 320px;
max-height: 400px;
resize: none;
margin: 0 0 5px;
padding: 0;
background-color: #efefef;
border-radius: 2px;
border-color: #cecece;
max-lines: 50;
}

View File

@ -0,0 +1,32 @@
<div class="file-editor-title-container">
<h1 mat-dialog-title *ngIf="error === null || error === undefined">{{ title }}</h1>
<h1 style="color: red;" *ngIf="error != null">{{ error }}</h1>
<span *ngIf="isBusy"><mat-spinner [diameter]="24" color="accent" style="margin-left: 8px"></mat-spinner></span>
<span fxFlex></span>
<button mat-icon-button (click)="closeDialog();">
<mat-icon>close</mat-icon>
</button>
</div>
<mat-divider></mat-divider>
<div>
<form>
<mat-form-field style="width: 100%;">
<mat-label>File Name</mat-label>
<input style="width: 100%" matInput [(ngModel)]="fileName" name="fileName" [disabled]="!isNew"/>
</mat-form-field>
</form>
<textarea placeholder="File Content" class="file-editor-text-area" matTextareaAutosize [(ngModel)]="fileContent" name="fileContent" (keydown)="handleTabKey($event);" [readOnly]="readonly"></textarea>
<div class="file-editor-buttons">
<span fxFlex></span>
<button mat-raised-button class="file-editor-button-first" (click)="closeDialog();" [disabled]="isBusy">
Cancel
</button>
<button mat-raised-button color="accent" (click)="preformSave();" [disabled]="isBusy">
Save
</button>
</div>
</div>

View File

@ -0,0 +1,96 @@
import {Component, Inject, OnInit} from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {ApiService} from "../../../services/api.service";
@Component({
selector: 'lib-edit-file-dialog',
templateUrl: './edit-file-dialog.component.html',
styleUrls: ['./edit-file-dialog.component.css']
})
export class EditFileDialogComponent implements OnInit {
constructor(public dialogRef: MatDialogRef<EditFileDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any,
private API: ApiService) {
this.readonly = data.readonly;
this.path = data.path;
this.fileName = data.fileName;
this.isNew = data.isNew;
}
public readonly: boolean = false;
public isBusy: boolean = false;
public path: string = null;
public isNew = false;
public title = '';
public fileName = '';
public fileContent = '';
public error: string = null;
private loadFileContent(path: string): void {
this.isBusy = true;
this.API.request({
module: 'evilportal',
action: 'load_file',
path: path
}, (response) => {
this.isBusy = false;
if (response.error !== undefined) {
this.error = response.error;
return
}
this.fileContent = response;
});
}
private saveFileContent(path: string): void {
this.isBusy = true;
this.API.request({
module: 'evilportal',
action: 'save_file',
path: path,
content: this.fileContent
}, (response) => {
this.isBusy = false;
if (response.error !== undefined) {
this.error = response.error;
return;
}
this.closeDialog();
this.data.onSaved();
});
}
preformSave(): void {
const fileToSave = this.path + '/' + this.fileName;
this.saveFileContent(fileToSave);
}
handleTabKey(e: KeyboardEvent): boolean {
if (e.code.toLowerCase() === 'tab') {
e.preventDefault();
const target = e.target as HTMLTextAreaElement;
const start = target.selectionStart;
const end = target.selectionEnd;
this.fileContent = this.fileContent.substring(0, start) + ' ' + this.fileContent.substring(end);
return false;
}
}
closeDialog(): void {
if (this.isBusy) {
return;
}
this.dialogRef.close();
}
ngOnInit() {
this.title = (this.isNew) ? 'Create New File' : 'Edit File';
if (!this.isNew) {
this.loadFileContent(this.path + '/' + this.fileName);
}
}
}

View File

@ -0,0 +1,3 @@
.error-dialog-buttons {
margin-top: 16px;
}

View File

@ -0,0 +1,19 @@
<div class="right-left-split-container">
<mat-card-title>Error</mat-card-title>
<span fxFlex></span>
<button mat-icon-button>
<mat-icon (click)="closeDialog();">close</mat-icon>
</button>
</div>
<mat-divider></mat-divider>
<div>
<p>{{ message }}</p>
<div class="error-dialog-buttons">
<span fxFlex></span>
<button mat-raised-button color="accent" class="error-dialog-button-first" (click)="closeDialog();">
Okay
</button>
</div>
</div>

View File

@ -0,0 +1,25 @@
import {Component, Inject, OnInit} from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
@Component({
selector: 'lib-error-dialog',
templateUrl: './error-dialog.component.html',
styleUrls: ['./error-dialog.component.css']
})
export class ErrorDialogComponent implements OnInit {
constructor(public dialogRef: MatDialogRef<ErrorDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any) {
this.message = data.message;
}
public message: string;
closeDialog(): void {
this.dialogRef.close();
}
ngOnInit(): void {
}
}

Some files were not shown because too many files have changed in this diff Show More