pftp 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pftp/__init__.py +7 -0
- pftp/__main__.py +6 -0
- pftp/cli.py +782 -0
- pftp/config.py +152 -0
- pftp/constants.py +32 -0
- pftp/docker_manager.py +295 -0
- pftp-2.0.0.dist-info/METADATA +86 -0
- pftp-2.0.0.dist-info/RECORD +11 -0
- pftp-2.0.0.dist-info/WHEEL +5 -0
- pftp-2.0.0.dist-info/entry_points.txt +2 -0
- pftp-2.0.0.dist-info/top_level.txt +1 -0
pftp/config.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Configuration management for PFTP"""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, asdict, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional, Dict
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
8
|
+
from .constants import (
|
|
9
|
+
DEFAULT_HOST,
|
|
10
|
+
DEFAULT_PORT,
|
|
11
|
+
DEFAULT_DATA_DIR,
|
|
12
|
+
DEFAULT_DOCKER_IMAGE,
|
|
13
|
+
CONTAINER_NAME,
|
|
14
|
+
FTP_PORT,
|
|
15
|
+
FTP_PASSIVE_START,
|
|
16
|
+
FTP_PASSIVE_END,
|
|
17
|
+
SMB_PORT,
|
|
18
|
+
SMB_NETBIOS_PORT,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class Config:
|
|
24
|
+
"""PFTP configuration v2.0 with multi-protocol support"""
|
|
25
|
+
|
|
26
|
+
# Server settings
|
|
27
|
+
host: str = DEFAULT_HOST
|
|
28
|
+
port: int = DEFAULT_PORT
|
|
29
|
+
|
|
30
|
+
# v2.0 Protocol configurations
|
|
31
|
+
protocols: Dict = field(default_factory=lambda: {
|
|
32
|
+
'http': {'enabled': True, 'port': DEFAULT_PORT},
|
|
33
|
+
'ftp': {'enabled': True, 'port': FTP_PORT, 'passive_start': FTP_PASSIVE_START, 'passive_end': FTP_PASSIVE_END},
|
|
34
|
+
'smb': {'enabled': True, 'port': SMB_PORT, 'netbios_port': SMB_NETBIOS_PORT}
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
# Authentication
|
|
38
|
+
auth_enabled: bool = False
|
|
39
|
+
auth_username: Optional[str] = None
|
|
40
|
+
auth_password_hash: Optional[str] = None
|
|
41
|
+
|
|
42
|
+
# Docker and directories
|
|
43
|
+
docker_image: str = DEFAULT_DOCKER_IMAGE
|
|
44
|
+
data_dir: Path = DEFAULT_DATA_DIR
|
|
45
|
+
tools_dir: Optional[Path] = None
|
|
46
|
+
uploads_dir: Optional[Path] = None
|
|
47
|
+
ignore_dirs: str = ".git,__pycache__,.vscode"
|
|
48
|
+
debug: bool = False
|
|
49
|
+
|
|
50
|
+
# Docker restart policy: 'no', 'always', 'unless-stopped', 'on-failure'
|
|
51
|
+
restart_policy: str = "unless-stopped"
|
|
52
|
+
|
|
53
|
+
def __post_init__(self):
|
|
54
|
+
"""Initialize paths"""
|
|
55
|
+
self.data_dir = Path(self.data_dir).expanduser()
|
|
56
|
+
if not self.tools_dir:
|
|
57
|
+
self.tools_dir = self.data_dir / "tools"
|
|
58
|
+
if not self.uploads_dir:
|
|
59
|
+
self.uploads_dir = self.data_dir / "uploads"
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def load(cls, config_path: Path) -> 'Config':
|
|
63
|
+
"""Load configuration from YAML file with v1.0 to v2.0 migration"""
|
|
64
|
+
if not config_path.exists():
|
|
65
|
+
return cls()
|
|
66
|
+
|
|
67
|
+
with open(config_path, 'r') as f:
|
|
68
|
+
data = yaml.safe_load(f) or {}
|
|
69
|
+
|
|
70
|
+
version = data.get('version', '1.0')
|
|
71
|
+
|
|
72
|
+
# v1.0 config - migrate to v2.0
|
|
73
|
+
if version == '1.0':
|
|
74
|
+
http_port = data.get('server', {}).get('port', DEFAULT_PORT)
|
|
75
|
+
config = cls(
|
|
76
|
+
host=data.get('server', {}).get('host', DEFAULT_HOST),
|
|
77
|
+
port=http_port,
|
|
78
|
+
docker_image=data.get('docker', {}).get('image', DEFAULT_DOCKER_IMAGE),
|
|
79
|
+
data_dir=Path(data.get('directories', {}).get('data_dir', DEFAULT_DATA_DIR)),
|
|
80
|
+
ignore_dirs=data.get('advanced', {}).get('ignore_dirs', ".git,__pycache__,.vscode"),
|
|
81
|
+
debug=data.get('advanced', {}).get('debug', False)
|
|
82
|
+
)
|
|
83
|
+
# Update HTTP port to match legacy config
|
|
84
|
+
config.protocols['http']['port'] = http_port
|
|
85
|
+
# Auto-save as v2.0
|
|
86
|
+
config.save(config_path)
|
|
87
|
+
return config
|
|
88
|
+
|
|
89
|
+
# v2.0 config
|
|
90
|
+
tools_dir_str = data.get('directories', {}).get('tools_dir')
|
|
91
|
+
uploads_dir_str = data.get('directories', {}).get('uploads_dir')
|
|
92
|
+
|
|
93
|
+
return cls(
|
|
94
|
+
host=data.get('server', {}).get('host', DEFAULT_HOST),
|
|
95
|
+
port=data.get('server', {}).get('port', DEFAULT_PORT),
|
|
96
|
+
protocols=data.get('protocols', cls().protocols),
|
|
97
|
+
auth_enabled=data.get('authentication', {}).get('enabled', False),
|
|
98
|
+
auth_username=data.get('authentication', {}).get('username'),
|
|
99
|
+
auth_password_hash=data.get('authentication', {}).get('password_hash'),
|
|
100
|
+
docker_image=data.get('docker', {}).get('image', DEFAULT_DOCKER_IMAGE),
|
|
101
|
+
data_dir=Path(data.get('directories', {}).get('data_dir', DEFAULT_DATA_DIR)),
|
|
102
|
+
tools_dir=Path(tools_dir_str) if tools_dir_str else None,
|
|
103
|
+
uploads_dir=Path(uploads_dir_str) if uploads_dir_str else None,
|
|
104
|
+
ignore_dirs=data.get('advanced', {}).get('ignore_dirs', ".git,__pycache__,.vscode"),
|
|
105
|
+
debug=data.get('advanced', {}).get('debug', False),
|
|
106
|
+
restart_policy=data.get('docker', {}).get('restart_policy', 'unless-stopped')
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def save(self, config_path: Path):
|
|
110
|
+
"""Save configuration to YAML file in v2.0 format"""
|
|
111
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
112
|
+
|
|
113
|
+
data = {
|
|
114
|
+
'version': '2.0',
|
|
115
|
+
'server': {
|
|
116
|
+
'host': self.host,
|
|
117
|
+
'port': self.port
|
|
118
|
+
},
|
|
119
|
+
'protocols': self.protocols,
|
|
120
|
+
'authentication': {
|
|
121
|
+
'enabled': self.auth_enabled,
|
|
122
|
+
'username': self.auth_username,
|
|
123
|
+
'password_hash': self.auth_password_hash
|
|
124
|
+
},
|
|
125
|
+
'docker': {
|
|
126
|
+
'image': self.docker_image,
|
|
127
|
+
'container_name': CONTAINER_NAME,
|
|
128
|
+
'restart_policy': self.restart_policy
|
|
129
|
+
},
|
|
130
|
+
'directories': {
|
|
131
|
+
'data_dir': str(self.data_dir),
|
|
132
|
+
'tools_dir': str(self.tools_dir),
|
|
133
|
+
'uploads_dir': str(self.uploads_dir),
|
|
134
|
+
'permissions': {
|
|
135
|
+
'tools': 'ro',
|
|
136
|
+
'uploads': 'rw'
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
'advanced': {
|
|
140
|
+
'ignore_dirs': self.ignore_dirs,
|
|
141
|
+
'debug': self.debug
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
with open(config_path, 'w') as f:
|
|
146
|
+
yaml.dump(data, f, default_flow_style=False)
|
|
147
|
+
|
|
148
|
+
def merge_cli_args(self, **kwargs):
|
|
149
|
+
"""Merge CLI arguments into configuration"""
|
|
150
|
+
for key, value in kwargs.items():
|
|
151
|
+
if value is not None and hasattr(self, key):
|
|
152
|
+
setattr(self, key, value)
|
pftp/constants.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Constants and default values for PFTP"""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
# Default configuration values
|
|
6
|
+
DEFAULT_PORT = 1234
|
|
7
|
+
DEFAULT_HOST = "0.0.0.0"
|
|
8
|
+
DEFAULT_DATA_DIR = Path.home() / ".pftp"
|
|
9
|
+
DEFAULT_DOCKER_IMAGE = "ahmadalawneh3/pftp:latest"
|
|
10
|
+
|
|
11
|
+
# Protocol ports
|
|
12
|
+
FTP_PORT = 21
|
|
13
|
+
FTP_PASSIVE_START = 60000
|
|
14
|
+
FTP_PASSIVE_END = 60100
|
|
15
|
+
SMB_PORT = 445
|
|
16
|
+
SMB_NETBIOS_PORT = 139
|
|
17
|
+
|
|
18
|
+
# Container and config settings
|
|
19
|
+
CONTAINER_NAME = "pftp"
|
|
20
|
+
CONFIG_FILE = "config.yaml"
|
|
21
|
+
CONFIG_DIR = "config"
|
|
22
|
+
TOOLS_DIR = "tools"
|
|
23
|
+
UPLOADS_DIR = "uploads"
|
|
24
|
+
|
|
25
|
+
# Docker environment variables
|
|
26
|
+
ENV_PROTOCOL = "PROTOCOL"
|
|
27
|
+
ENV_HOST = "HOST"
|
|
28
|
+
ENV_PORT = "PORT"
|
|
29
|
+
ENV_DEBUG = "DEBUG"
|
|
30
|
+
ENV_UPLOAD_FOLDER = "UPLOAD_FOLDER"
|
|
31
|
+
ENV_TOOLS_FOLDER = "TOOLS_FOLDER"
|
|
32
|
+
ENV_IGNORE_DIRS = "IGNORE_DIRS"
|
pftp/docker_manager.py
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""Docker container management for PFTP"""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
import docker
|
|
5
|
+
from docker.errors import DockerException, ImageNotFound, NotFound
|
|
6
|
+
from typing import Dict, Optional
|
|
7
|
+
|
|
8
|
+
from .config import Config
|
|
9
|
+
from .constants import CONTAINER_NAME
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DockerManager:
|
|
13
|
+
"""Manage PFTP Docker container"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, config: Config):
|
|
16
|
+
"""Initialize Docker manager
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
config: PFTP configuration
|
|
20
|
+
"""
|
|
21
|
+
self.config = config
|
|
22
|
+
try:
|
|
23
|
+
self.client = docker.from_env()
|
|
24
|
+
except DockerException as e:
|
|
25
|
+
click.echo(f"Error: Cannot connect to Docker daemon. Is Docker running?", err=True)
|
|
26
|
+
click.echo(f"Details: {e}", err=True)
|
|
27
|
+
raise
|
|
28
|
+
|
|
29
|
+
def pull_image(self, image_name: str) -> bool:
|
|
30
|
+
"""Pull Docker image from registry
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
image_name: Full image name (e.g., 'ahmadalawneh3/pftp:latest')
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
True if successful, False otherwise
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
click.echo(f"Pulling image {image_name}...")
|
|
40
|
+
self.client.images.pull(image_name)
|
|
41
|
+
click.echo(f"✓ Successfully pulled {image_name}")
|
|
42
|
+
return True
|
|
43
|
+
except DockerException as e:
|
|
44
|
+
click.echo(f"Error pulling image: {e}", err=True)
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
def is_running(self) -> bool:
|
|
48
|
+
"""Check if container is running
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
True if container exists and is running
|
|
52
|
+
"""
|
|
53
|
+
try:
|
|
54
|
+
container = self.client.containers.get(CONTAINER_NAME)
|
|
55
|
+
return container.status == 'running'
|
|
56
|
+
except NotFound:
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
def start_container(self) -> bool:
|
|
60
|
+
"""Start PFTP container
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
True if successful, False otherwise
|
|
64
|
+
"""
|
|
65
|
+
# Check if container exists
|
|
66
|
+
try:
|
|
67
|
+
container = self.client.containers.get(CONTAINER_NAME)
|
|
68
|
+
if container.status == 'running':
|
|
69
|
+
click.echo(f"Container '{CONTAINER_NAME}' is already running")
|
|
70
|
+
return True
|
|
71
|
+
# Container exists but not running - start it
|
|
72
|
+
container.start()
|
|
73
|
+
click.echo(f"✓ Started container '{CONTAINER_NAME}'")
|
|
74
|
+
return True
|
|
75
|
+
except NotFound:
|
|
76
|
+
# Create new container
|
|
77
|
+
return self._create_and_start()
|
|
78
|
+
|
|
79
|
+
def _create_and_start(self) -> bool:
|
|
80
|
+
"""Create and start new container
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
True if successful, False otherwise
|
|
84
|
+
"""
|
|
85
|
+
try:
|
|
86
|
+
# Prepare volume mappings
|
|
87
|
+
volumes = {
|
|
88
|
+
str(self.config.tools_dir): {'bind': '/app/data/tools', 'mode': 'rw'},
|
|
89
|
+
str(self.config.uploads_dir): {'bind': '/app/data/uploads', 'mode': 'rw'},
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# Prepare environment variables
|
|
93
|
+
environment = {
|
|
94
|
+
# General
|
|
95
|
+
'PROTOCOL': 'http',
|
|
96
|
+
'HOST': self.config.host,
|
|
97
|
+
'PORT': str(self.config.port),
|
|
98
|
+
'DEBUG': 'false',
|
|
99
|
+
'UPLOAD_FOLDER': 'data/uploads',
|
|
100
|
+
'TOOLS_FOLDER': 'data/tools',
|
|
101
|
+
'IGNORE_DIRS': '.git,__pycache__,.vscode',
|
|
102
|
+
|
|
103
|
+
# HTTP
|
|
104
|
+
'HTTP_ENABLED': str(self.config.protocols.get('http', {}).get('enabled', True)).lower(),
|
|
105
|
+
'HTTP_PORT': str(self.config.protocols.get('http', {}).get('port', 1234)),
|
|
106
|
+
|
|
107
|
+
# FTP
|
|
108
|
+
'FTP_ENABLED': str(self.config.protocols.get('ftp', {}).get('enabled', True)).lower(),
|
|
109
|
+
'FTP_PORT': str(self.config.protocols.get('ftp', {}).get('port', 21)),
|
|
110
|
+
'FTP_PASSIVE_START': str(self.config.protocols.get('ftp', {}).get('passive_start', 60000)),
|
|
111
|
+
'FTP_PASSIVE_END': str(self.config.protocols.get('ftp', {}).get('passive_end', 60100)),
|
|
112
|
+
|
|
113
|
+
# SMB
|
|
114
|
+
'SMB_ENABLED': str(self.config.protocols.get('smb', {}).get('enabled', True)).lower(),
|
|
115
|
+
'SMB_PORT': str(self.config.protocols.get('smb', {}).get('port', 445)),
|
|
116
|
+
'SMB_NETBIOS_PORT': str(self.config.protocols.get('smb', {}).get('netbios_port', 139)),
|
|
117
|
+
|
|
118
|
+
# Authentication
|
|
119
|
+
'AUTH_ENABLED': str(self.config.auth_enabled).lower(),
|
|
120
|
+
'AUTH_USERNAME': self.config.auth_username or '',
|
|
121
|
+
'AUTH_PASSWORD_HASH': self.config.auth_password_hash or '',
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# Prepare restart policy
|
|
125
|
+
restart_policy_name = self.config.restart_policy or 'unless-stopped'
|
|
126
|
+
restart_policy = {'Name': restart_policy_name}
|
|
127
|
+
if restart_policy_name == 'on-failure':
|
|
128
|
+
restart_policy['MaximumRetryCount'] = 5
|
|
129
|
+
|
|
130
|
+
# Create and start container
|
|
131
|
+
container = self.client.containers.run(
|
|
132
|
+
self.config.docker_image,
|
|
133
|
+
name=CONTAINER_NAME,
|
|
134
|
+
volumes=volumes,
|
|
135
|
+
environment=environment,
|
|
136
|
+
network_mode='host', # Required for IP detection
|
|
137
|
+
restart_policy=restart_policy,
|
|
138
|
+
detach=True,
|
|
139
|
+
stdin_open=True,
|
|
140
|
+
tty=True
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
click.echo(click.style(f"✓ Container created and started: {container.short_id}", fg='green'))
|
|
144
|
+
|
|
145
|
+
# Try to get actual IPs
|
|
146
|
+
try:
|
|
147
|
+
import subprocess
|
|
148
|
+
import re
|
|
149
|
+
result = subprocess.run(['ip', 'addr', 'show'],
|
|
150
|
+
capture_output=True, text=True, timeout=5)
|
|
151
|
+
if result.returncode == 0:
|
|
152
|
+
ips = []
|
|
153
|
+
current_interface = None
|
|
154
|
+
|
|
155
|
+
for line in result.stdout.split('\n'):
|
|
156
|
+
if_match = re.match(r'^\d+:\s+(\S+):', line)
|
|
157
|
+
if if_match:
|
|
158
|
+
current_interface = if_match.group(1)
|
|
159
|
+
|
|
160
|
+
ip_match = re.search(r'inet\s+(\d+\.\d+\.\d+\.\d+)', line)
|
|
161
|
+
if ip_match and current_interface:
|
|
162
|
+
ip = ip_match.group(1)
|
|
163
|
+
if ip != '127.0.0.1':
|
|
164
|
+
# Priority system:
|
|
165
|
+
# 0 = tun interfaces (VPN/pentest)
|
|
166
|
+
# 1 = eth interfaces (ethernet)
|
|
167
|
+
# 2 = wlan interfaces (wifi)
|
|
168
|
+
# 3 = other interfaces
|
|
169
|
+
if current_interface.startswith('tun'):
|
|
170
|
+
priority = 0
|
|
171
|
+
elif current_interface.startswith('eth'):
|
|
172
|
+
priority = 1
|
|
173
|
+
elif current_interface.startswith('wlan'):
|
|
174
|
+
priority = 2
|
|
175
|
+
else:
|
|
176
|
+
priority = 3
|
|
177
|
+
|
|
178
|
+
ips.append((priority, ip, current_interface))
|
|
179
|
+
|
|
180
|
+
ips.sort()
|
|
181
|
+
ip_list = [ip for _, ip, _ in ips]
|
|
182
|
+
|
|
183
|
+
if ip_list:
|
|
184
|
+
click.echo(click.style(f"✓ Server running at:", fg='green', bold=True))
|
|
185
|
+
for ip in ip_list:
|
|
186
|
+
click.echo(f" {click.style(f'http://{ip}:{self.config.port}', fg='cyan', bold=True)}")
|
|
187
|
+
else:
|
|
188
|
+
click.echo(click.style(f"✓ Server running on http://<your-ip>:{self.config.port}", fg='green'))
|
|
189
|
+
else:
|
|
190
|
+
click.echo(click.style(f"✓ Server running on http://<your-ip>:{self.config.port}", fg='green'))
|
|
191
|
+
except:
|
|
192
|
+
click.echo(click.style(f"✓ Server running on http://<your-ip>:{self.config.port}", fg='green'))
|
|
193
|
+
|
|
194
|
+
return True
|
|
195
|
+
|
|
196
|
+
except ImageNotFound:
|
|
197
|
+
click.echo(f"Error: Docker image '{self.config.docker_image}' not found", err=True)
|
|
198
|
+
click.echo("Run 'pftp install' to pull the image", err=True)
|
|
199
|
+
return False
|
|
200
|
+
except DockerException as e:
|
|
201
|
+
click.echo(f"Error starting container: {e}", err=True)
|
|
202
|
+
return False
|
|
203
|
+
|
|
204
|
+
def stop_container(self) -> bool:
|
|
205
|
+
"""Stop container
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
True if successful, False otherwise
|
|
209
|
+
"""
|
|
210
|
+
try:
|
|
211
|
+
container = self.client.containers.get(CONTAINER_NAME)
|
|
212
|
+
container.stop(timeout=10)
|
|
213
|
+
click.echo(f"✓ Container '{CONTAINER_NAME}' stopped")
|
|
214
|
+
return True
|
|
215
|
+
except NotFound:
|
|
216
|
+
click.echo(f"Container '{CONTAINER_NAME}' not found")
|
|
217
|
+
return False
|
|
218
|
+
except DockerException as e:
|
|
219
|
+
click.echo(f"Error stopping container: {e}", err=True)
|
|
220
|
+
return False
|
|
221
|
+
|
|
222
|
+
def remove_container(self) -> bool:
|
|
223
|
+
"""Remove container
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
True if successful, False otherwise
|
|
227
|
+
"""
|
|
228
|
+
try:
|
|
229
|
+
container = self.client.containers.get(CONTAINER_NAME)
|
|
230
|
+
container.remove(force=True)
|
|
231
|
+
click.echo(f"✓ Container '{CONTAINER_NAME}' removed")
|
|
232
|
+
return True
|
|
233
|
+
except NotFound:
|
|
234
|
+
# Already removed
|
|
235
|
+
return True
|
|
236
|
+
except DockerException as e:
|
|
237
|
+
click.echo(f"Error removing container: {e}", err=True)
|
|
238
|
+
return False
|
|
239
|
+
|
|
240
|
+
def get_logs(self, follow: bool = True, lines: int = 50):
|
|
241
|
+
"""Get container logs
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
follow: Stream logs (like tail -f)
|
|
245
|
+
lines: Number of lines to show
|
|
246
|
+
"""
|
|
247
|
+
try:
|
|
248
|
+
container = self.client.containers.get(CONTAINER_NAME)
|
|
249
|
+
if follow:
|
|
250
|
+
click.echo(click.style(f"Following logs from '{CONTAINER_NAME}' (Ctrl+C to stop)...", fg='cyan'))
|
|
251
|
+
buffer = b''
|
|
252
|
+
for chunk in container.logs(stream=True, follow=True, tail=lines):
|
|
253
|
+
if isinstance(chunk, bytes):
|
|
254
|
+
buffer += chunk
|
|
255
|
+
else:
|
|
256
|
+
buffer += chunk.encode('utf-8')
|
|
257
|
+
while b'\n' in buffer:
|
|
258
|
+
line, buffer = buffer.split(b'\n', 1)
|
|
259
|
+
click.echo(line.decode('utf-8', errors='replace').rstrip())
|
|
260
|
+
if buffer:
|
|
261
|
+
click.echo(buffer.decode('utf-8', errors='replace').rstrip())
|
|
262
|
+
else:
|
|
263
|
+
# Get logs as bytes, decode to string
|
|
264
|
+
logs_bytes = container.logs(tail=lines)
|
|
265
|
+
if isinstance(logs_bytes, bytes):
|
|
266
|
+
click.echo(logs_bytes.decode('utf-8', errors='replace'))
|
|
267
|
+
else:
|
|
268
|
+
click.echo(str(logs_bytes))
|
|
269
|
+
except NotFound:
|
|
270
|
+
click.echo(click.style(f"Container '{CONTAINER_NAME}' not found", fg='red'), err=True)
|
|
271
|
+
except KeyboardInterrupt:
|
|
272
|
+
click.echo(click.style("\n✓ Stopped following logs", fg='green'))
|
|
273
|
+
except DockerException as e:
|
|
274
|
+
click.echo(click.style(f"Error getting logs: {e}", fg='red'), err=True)
|
|
275
|
+
|
|
276
|
+
def get_status(self) -> Optional[Dict]:
|
|
277
|
+
"""Get container status information
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
Dictionary with container info or None if not found
|
|
281
|
+
"""
|
|
282
|
+
try:
|
|
283
|
+
container = self.client.containers.get(CONTAINER_NAME)
|
|
284
|
+
return {
|
|
285
|
+
'status': container.status,
|
|
286
|
+
'id': container.short_id,
|
|
287
|
+
'image': container.image.tags[0] if container.image.tags else 'unknown',
|
|
288
|
+
'created': container.attrs['Created'],
|
|
289
|
+
'ports': container.attrs.get('NetworkSettings', {}).get('Ports', {})
|
|
290
|
+
}
|
|
291
|
+
except NotFound:
|
|
292
|
+
return {'status': 'not_found'}
|
|
293
|
+
except DockerException as e:
|
|
294
|
+
click.echo(f"Error getting status: {e}", err=True)
|
|
295
|
+
return None
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pftp
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: PFTP - Pentest File Transfer Protocols - CLI Tool
|
|
5
|
+
Author-email: Ahmad Alawneh <a.3alawneh@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: pentest,file-transfer,security,hacking,ctf,ftp,CTF
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Information Technology
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Topic :: Security
|
|
17
|
+
Requires-Python: >=3.8
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: click>=8.0
|
|
20
|
+
Requires-Dist: docker>=6.0
|
|
21
|
+
Requires-Dist: pyyaml>=6.0
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: build; extra == "dev"
|
|
24
|
+
Requires-Dist: twine; extra == "dev"
|
|
25
|
+
|
|
26
|
+
# PFTP CLI
|
|
27
|
+
|
|
28
|
+
CLI tool for managing the PFTP (Pentest File Transfer Protocols) server.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pipx install pftp
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Or from source:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
cd cli
|
|
40
|
+
pipx install .
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Install and configure
|
|
47
|
+
pftp install
|
|
48
|
+
|
|
49
|
+
# Start the server
|
|
50
|
+
pftp start
|
|
51
|
+
|
|
52
|
+
# Check status
|
|
53
|
+
pftp status
|
|
54
|
+
|
|
55
|
+
# Add tools
|
|
56
|
+
pftp add-tool /path/to/linpeas.sh
|
|
57
|
+
|
|
58
|
+
# View logs
|
|
59
|
+
pftp logs
|
|
60
|
+
|
|
61
|
+
# Stop the server
|
|
62
|
+
pftp stop
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Commands
|
|
66
|
+
|
|
67
|
+
- `pftp install` - Install and configure pftp
|
|
68
|
+
- `pftp configure` - Reconfigure settings
|
|
69
|
+
- `pftp start` - Start the server
|
|
70
|
+
- `pftp stop` - Stop the server
|
|
71
|
+
- `pftp restart` - Restart the server
|
|
72
|
+
- `pftp status` - Show status and configuration
|
|
73
|
+
- `pftp logs` - View server logs
|
|
74
|
+
- `pftp update` - Update to latest Docker image
|
|
75
|
+
- `pftp remove` - Uninstall pftp
|
|
76
|
+
- `pftp add-tool` - Add files to tools directory
|
|
77
|
+
- `pftp version` - Show version
|
|
78
|
+
|
|
79
|
+
## Requirements
|
|
80
|
+
|
|
81
|
+
- Python 3.8+
|
|
82
|
+
- Docker
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
MIT
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
pftp/__init__.py,sha256=V9qkxhFeWzkx2PC5yjNuNREk-LwtdRWT0DDbKoOp6TQ,173
|
|
2
|
+
pftp/__main__.py,sha256=OgFKe0aksr0dwPtmMSbvtSRrRaPrhWrtLmLhjIWdLGA,108
|
|
3
|
+
pftp/cli.py,sha256=EjaOnWCERNMkKj3shEHI2DVBYxuJOGEsizL51Djrn5M,31422
|
|
4
|
+
pftp/config.py,sha256=9NBYpcd2xL021JHAymi_FANvodJfgOf8QEMZixEYfRE,5642
|
|
5
|
+
pftp/constants.py,sha256=X9F7MdIP0FKVAY1GkKrAWOz08KxDr1i4KBtFRHGd7Sk,758
|
|
6
|
+
pftp/docker_manager.py,sha256=q6L5wd52g3EbtlvC40ThsNb0zkgkCpO2W9fjVtU7u00,11812
|
|
7
|
+
pftp-2.0.0.dist-info/METADATA,sha256=RYlrFffAcq7KgBN2CU4G8k_XuC0kJh2tebGdGxSLojc,1816
|
|
8
|
+
pftp-2.0.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
9
|
+
pftp-2.0.0.dist-info/entry_points.txt,sha256=Hp7IlQLGb30u4zTq3huSYls15Ecy9vw8tAxu5qf-79U,38
|
|
10
|
+
pftp-2.0.0.dist-info/top_level.txt,sha256=Dn7MXqEDhB-cK4jiVjMlh0Xu1pq1yuO3_BTNwyd7TSk,5
|
|
11
|
+
pftp-2.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pftp
|