fix: security headers, XSS, prototype pollution, focus trap, accessibility
This commit is contained in:
parent
696118f17b
commit
a86afced2d
13 changed files with 154 additions and 85 deletions
|
|
@ -133,7 +133,7 @@ Noch nicht umgesetzt — bitte nicht als vorhanden dokumentieren, bevor es wirkl
|
||||||
- [ ] Automatischer Server-Neustart (`systemd` oder `pm2`).
|
- [ ] Automatischer Server-Neustart (`systemd` oder `pm2`).
|
||||||
- [ ] SSL/HTTPS für Produktionsbetrieb.
|
- [ ] SSL/HTTPS für Produktionsbetrieb.
|
||||||
- [ ] Docker-Containerisierung.
|
- [ ] Docker-Containerisierung.
|
||||||
- [ ] Path-Traversal-Schutz und Auth für den PUT-Endpoint.
|
- [ ] Auth für den PUT-Endpoint.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,7 @@ python scripts/validate.py pfad/zum/template.json
|
||||||
python scripts/validate.py --all
|
python scripts/validate.py --all
|
||||||
|
|
||||||
# Nur JSON-Templates
|
# Nur JSON-Templates
|
||||||
python scripts/validate.py --type-json
|
python scripts/validate.py --all --type-json
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -25,4 +25,4 @@ Dieser Ordner enthält alle technischen und prozessbezogenen Dokumentationen fü
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Letzte Aktualisierung: `$(date +%Y-%m-%d)`*
|
*Letzte Aktualisierung: 2026-05-03*
|
||||||
|
|
|
||||||
|
|
@ -10,25 +10,6 @@ from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
def read_file(filepath: str) -> Optional[str]:
|
|
||||||
"""
|
|
||||||
Liest eine Datei und gibt den Inhalt als String zurück.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
filepath: Absoluter Pfad zur Datei
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dateiinhalt als UTF-8 String, oder None bei Fehler
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
with open(filepath, "r", encoding="utf-8") as f:
|
|
||||||
return f.read()
|
|
||||||
except FileNotFoundError:
|
|
||||||
return None
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def read_file_binary(filepath: str) -> Optional[bytes]:
|
def read_file_binary(filepath: str) -> Optional[bytes]:
|
||||||
"""
|
"""
|
||||||
Liest eine Datei im Binärmodus.
|
Liest eine Datei im Binärmodus.
|
||||||
|
|
@ -71,11 +52,6 @@ def write_file(filepath: str, content: bytes) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def file_exists(filepath: str) -> bool:
|
|
||||||
"""Prüft, ob eine Datei existiert."""
|
|
||||||
return os.path.isfile(filepath)
|
|
||||||
|
|
||||||
|
|
||||||
def directory_exists(dirpath: str) -> bool:
|
def directory_exists(dirpath: str) -> bool:
|
||||||
"""Prüft, ob ein Verzeichnis existiert."""
|
"""Prüft, ob ein Verzeichnis existiert."""
|
||||||
return os.path.isdir(dirpath)
|
return os.path.isdir(dirpath)
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,26 @@ class Handler(http.server.SimpleHTTPRequestHandler):
|
||||||
def __init__(self, *args, directory=None, **kwargs):
|
def __init__(self, *args, directory=None, **kwargs):
|
||||||
super().__init__(*args, directory=directory or self.directory, **kwargs)
|
super().__init__(*args, directory=directory or self.directory, **kwargs)
|
||||||
|
|
||||||
|
def send_response(self, code, message=None):
|
||||||
|
if self.request_version != 'HTTP/0.9':
|
||||||
|
if message is None:
|
||||||
|
if code in self.responses:
|
||||||
|
message = self.responses[code][0]
|
||||||
|
else:
|
||||||
|
message = ''
|
||||||
|
if not hasattr(self, '_headers_buffer'):
|
||||||
|
self._headers_buffer = []
|
||||||
|
self._headers_buffer.append(("%s %d %s\r\n" %
|
||||||
|
(self.protocol_version, code, message)).encode('latin-1', 'strict'))
|
||||||
|
# Write Server and Date headers (mimicking base class behavior)
|
||||||
|
self.send_header('Server', self.version_string())
|
||||||
|
self.send_header('Date', self.date_time_string())
|
||||||
|
# Security headers
|
||||||
|
self.send_header('X-Content-Type-Options', 'nosniff')
|
||||||
|
self.send_header('X-Frame-Options', 'DENY')
|
||||||
|
self.send_header('Content-Security-Policy',
|
||||||
|
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 'none'")
|
||||||
|
|
||||||
def do_PUT(self):
|
def do_PUT(self):
|
||||||
"""Speichert eine Template-Datei."""
|
"""Speichert eine Template-Datei."""
|
||||||
file_path = self.validator.resolve_template_path(urlparse(self.path).path)
|
file_path = self.validator.resolve_template_path(urlparse(self.path).path)
|
||||||
|
|
@ -77,7 +97,7 @@ class Handler(http.server.SimpleHTTPRequestHandler):
|
||||||
self.wfile.write(b"File saved successfully")
|
self.wfile.write(b"File saved successfully")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Failed to save file: %s", e)
|
logger.error("Failed to save file: %s", e)
|
||||||
self.send_error(500, f"Failed to save file: {e}")
|
self.send_error(500, "Failed to save file")
|
||||||
|
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
"""Liefert Dateien aus."""
|
"""Liefert Dateien aus."""
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Toast -->
|
<!-- Toast -->
|
||||||
<div class="toast" id="toast"></div>
|
<div class="toast" id="toast" role="status" aria-live="polite"></div>
|
||||||
|
|
||||||
<!-- Tune Modal -->
|
<!-- Tune Modal -->
|
||||||
<div class="modal-overlay" id="tune-modal" role="dialog" aria-label="Prompt tunen">
|
<div class="modal-overlay" id="tune-modal" role="dialog" aria-label="Prompt tunen">
|
||||||
|
|
@ -90,7 +90,7 @@
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="filter-bar">
|
<div class="filter-bar">
|
||||||
<input type="text" id="search" placeholder="Suche Templates... (Name, Beschreibung, Tags)">
|
<input type="text" id="search" placeholder="Suche Templates... (Name, Beschreibung, Tags)" aria-label="Templates durchsuchen">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ async function scanTemplates() {
|
||||||
format: 'json'
|
format: 'json'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) { console.error('scanTemplates error:', e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
const userFiles = ['email_draft', 'brainstorming'];
|
const userFiles = ['email_draft', 'brainstorming'];
|
||||||
|
|
@ -110,7 +110,7 @@ async function scanTemplates() {
|
||||||
format: 'md'
|
format: 'md'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) { console.error('scanTemplates error:', e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
return templates;
|
return templates;
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@
|
||||||
* die Werte zurück in ein JSON-Objekt.
|
* die Werte zurück in ein JSON-Objekt.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
let editContainerRef = null;
|
|
||||||
let currentIndent = 2;
|
let currentIndent = 2;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -81,6 +80,9 @@ function createJsonEditUI(container, jsonData, editPathHint = '') {
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
Object.keys(data).forEach(key => {
|
Object.keys(data).forEach(key => {
|
||||||
|
if (key.includes('__proto__') || key.includes('constructor') || key.includes('prototype')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const fullKey = prefix ? `${prefix}.${key}` : key;
|
const fullKey = prefix ? `${prefix}.${key}` : key;
|
||||||
const value = data[key];
|
const value = data[key];
|
||||||
const isObject = typeof value === 'object' && value !== null && !Array.isArray(value);
|
const isObject = typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||||
|
|
@ -264,30 +266,39 @@ function extractJsonFromForm(formDiv) {
|
||||||
// Sort by key to ensure stable parent-first construction
|
// Sort by key to ensure stable parent-first construction
|
||||||
inputEntries.sort((a,b) => a.key.localeCompare(b.key));
|
inputEntries.sort((a,b) => a.key.localeCompare(b.key));
|
||||||
|
|
||||||
const result = {};
|
const result = Object.create(null);
|
||||||
inputEntries.forEach(({ key, value }) => {
|
inputEntries.forEach(({ key, value }) => {
|
||||||
const parts = key.split('.');
|
const parts = key.split('.');
|
||||||
let cur = result;
|
let cur = result;
|
||||||
for (let i = 0; i < parts.length; i++) {
|
for (let i = 0; i < parts.length; i++) {
|
||||||
const part = parts[i];
|
const part = parts[i];
|
||||||
const isLast = i === parts.length - 1;
|
const isLast = i === parts.length - 1;
|
||||||
const m = part.match(/^([^\[\]]+)\[(\d+)\]$/);
|
const lastBracket = part.lastIndexOf('[');
|
||||||
if (m && !isLast) {
|
const lastCloseBracket = part.lastIndexOf(']');
|
||||||
|
let m = null;
|
||||||
|
if (lastBracket !== -1 && lastCloseBracket !== -1 && lastCloseBracket > lastBracket) {
|
||||||
|
const beforeBracket = part.substring(0, lastBracket);
|
||||||
|
const between = part.substring(lastBracket + 1, lastCloseBracket);
|
||||||
|
const afterBracket = part.substring(lastCloseBracket + 1);
|
||||||
|
if (afterBracket === '' && /^\d+$/.test(between)) {
|
||||||
|
m = [part, beforeBracket, between];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (m && isLast) {
|
||||||
const base = m[1];
|
const base = m[1];
|
||||||
const idx = Number(m[2]);
|
const idx = Number(m[2]);
|
||||||
if (!cur[base]) cur[base] = [];
|
if (!cur[base]) cur[base] = Object.create(null);
|
||||||
if (!cur[base][idx]) cur[base][idx] = {};
|
|
||||||
cur = cur[base][idx];
|
|
||||||
} else if (m && isLast) {
|
|
||||||
// Array leaf
|
|
||||||
const base = m[1];
|
|
||||||
const idx = Number(m[2]);
|
|
||||||
if (!cur[base]) cur[base] = [];
|
|
||||||
cur[base][idx] = value;
|
cur[base][idx] = value;
|
||||||
|
} else if (m && !isLast) {
|
||||||
|
const base = m[1];
|
||||||
|
const idx = Number(m[2]);
|
||||||
|
if (!cur[base]) cur[base] = [];
|
||||||
|
if (!cur[base][idx]) cur[base][idx] = Object.create(null);
|
||||||
|
cur = cur[base][idx];
|
||||||
} else if (isLast) {
|
} else if (isLast) {
|
||||||
cur[part] = value;
|
cur[part] = value;
|
||||||
} else {
|
} else {
|
||||||
if (!cur[part]) cur[part] = {};
|
if (!cur[part]) cur[part] = Object.create(null);
|
||||||
cur = cur[part];
|
cur = cur[part];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -520,7 +531,13 @@ async function applyTunedContent() {
|
||||||
* Schließe Tune-Modal
|
* Schließe Tune-Modal
|
||||||
*/
|
*/
|
||||||
function closeTuneModal() {
|
function closeTuneModal() {
|
||||||
document.getElementById('tune-modal').classList.remove('active');
|
window._lastFocusedElement = document.activeElement;
|
||||||
|
var modal = document.getElementById('tune-modal');
|
||||||
|
modal.classList.remove('active');
|
||||||
|
_setAriaHidden('tune-modal', true);
|
||||||
|
if (window._lastFocusedElement && window._lastFocusedElement.focus) {
|
||||||
|
window._lastFocusedElement.focus();
|
||||||
|
}
|
||||||
window._tuneState = { path: '', rawPath: '', template: '' };
|
window._tuneState = { path: '', rawPath: '', template: '' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,26 @@ document.getElementById('search').addEventListener('input', (e) => {
|
||||||
document.getElementById('modal').addEventListener('click', (e) => {
|
document.getElementById('modal').addEventListener('click', (e) => {
|
||||||
if (e.target === e.currentTarget) closeModal();
|
if (e.target === e.currentTarget) closeModal();
|
||||||
});
|
});
|
||||||
|
document.getElementById('edit-modal').addEventListener('click', (e) => {
|
||||||
|
if (e.target === e.currentTarget) closeEditModal();
|
||||||
|
});
|
||||||
|
document.getElementById('tune-modal').addEventListener('click', (e) => {
|
||||||
|
if (e.target === e.currentTarget) closeTuneModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Focus trap for edit-modal
|
||||||
|
document.getElementById('edit-modal').addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Tab') { _trapFocus(e, document.getElementById('edit-modal')); }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Focus trap for tune-modal
|
||||||
|
document.getElementById('tune-modal').addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Tab') { _trapFocus(e, document.getElementById('tune-modal')); }
|
||||||
|
});
|
||||||
|
|
||||||
// Escape key closes modal
|
// Escape key closes modal
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Escape') { closeEditModal(); closeModal(); }
|
if (e.key === 'Escape') { closeEditModal(); closeModal(); closeTuneModal(); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// Hash -> type filter
|
// Hash -> type filter
|
||||||
|
|
@ -38,6 +54,11 @@ document.querySelectorAll('.nav a').forEach(link => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set initial aria-hidden on all modals
|
||||||
|
_setAriaHidden('modal', true);
|
||||||
|
_setAriaHidden('edit-modal', true);
|
||||||
|
_setAriaHidden('tune-modal', true);
|
||||||
|
|
||||||
// Initial load
|
// Initial load
|
||||||
loadTemplates().then(t => {
|
loadTemplates().then(t => {
|
||||||
allTemplates = t;
|
allTemplates = t;
|
||||||
|
|
|
||||||
|
|
@ -2,29 +2,87 @@
|
||||||
* Modal-Management für View und Edit Modals.
|
* Modal-Management für View und Edit Modals.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
var _lastFocusedElement = null;
|
||||||
|
|
||||||
|
function _getFocusableElements(container) {
|
||||||
|
var selectors = 'button:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
||||||
|
return Array.from(container.querySelectorAll(selectors)).filter(function(el) {
|
||||||
|
return !el.closest('.hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function _trapFocus(e, container) {
|
||||||
|
if (e.key !== 'Tab') return;
|
||||||
|
var focusable = _getFocusableElements(container);
|
||||||
|
if (focusable.length === 0) return;
|
||||||
|
var first = focusable[0];
|
||||||
|
var last = focusable[focusable.length - 1];
|
||||||
|
if (e.shiftKey) {
|
||||||
|
if (document.activeElement === first) {
|
||||||
|
e.preventDefault();
|
||||||
|
last.focus();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (document.activeElement === last) {
|
||||||
|
e.preventDefault();
|
||||||
|
first.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _setAriaHidden(id, hidden) {
|
||||||
|
var el = document.getElementById(id);
|
||||||
|
if (el) {
|
||||||
|
if (hidden) {
|
||||||
|
el.setAttribute('aria-hidden', 'true');
|
||||||
|
} else {
|
||||||
|
el.removeAttribute('aria-hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Zeige View-Modal mit Titel und Inhalt
|
* Zeige View-Modal mit Titel und Inhalt
|
||||||
* @param {string} title - Titel des Modals
|
* @param {string} title - Titel des Modals
|
||||||
* @param {string} content - Inhalt als Text
|
* @param {string} content - Inhalt als Text
|
||||||
*/
|
*/
|
||||||
function showModal(title, content) {
|
function showModal(title, content) {
|
||||||
|
_lastFocusedElement = document.activeElement;
|
||||||
document.getElementById('modal-title').textContent = title;
|
document.getElementById('modal-title').textContent = title;
|
||||||
document.getElementById('modal-content').textContent = content;
|
document.getElementById('modal-content').textContent = content;
|
||||||
document.getElementById('modal').classList.add('active');
|
var modal = document.getElementById('modal');
|
||||||
|
modal.classList.add('active');
|
||||||
|
_setAriaHidden('modal', false);
|
||||||
|
var focusable = _getFocusableElements(modal);
|
||||||
|
if (focusable.length > 0) {
|
||||||
|
focusable[0].focus();
|
||||||
|
}
|
||||||
|
modal.addEventListener('keydown', function(e) { _trapFocus(e, modal); });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schließe View-Modal
|
* Schließe View-Modal
|
||||||
*/
|
*/
|
||||||
function closeModal() {
|
function closeModal() {
|
||||||
document.getElementById('modal').classList.remove('active');
|
var modal = document.getElementById('modal');
|
||||||
|
modal.classList.remove('active');
|
||||||
|
_setAriaHidden('modal', true);
|
||||||
|
if (_lastFocusedElement && _lastFocusedElement.focus) {
|
||||||
|
_lastFocusedElement.focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schließe Edit-Modal
|
* Schließe Edit-Modal
|
||||||
*/
|
*/
|
||||||
function closeEditModal() {
|
function closeEditModal() {
|
||||||
document.getElementById('edit-modal').classList.remove('active');
|
_lastFocusedElement = document.activeElement;
|
||||||
|
var modal = document.getElementById('edit-modal');
|
||||||
|
modal.classList.remove('active');
|
||||||
|
_setAriaHidden('edit-modal', true);
|
||||||
|
if (_lastFocusedElement && _lastFocusedElement.focus) {
|
||||||
|
_lastFocusedElement.focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -35,11 +93,4 @@ function wasViewModalOpen() {
|
||||||
return window._wasViewModalOpen || false;
|
return window._wasViewModalOpen || false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Merke dass View-Modal offen war
|
|
||||||
*/
|
|
||||||
function rememberViewModalOpen() {
|
|
||||||
window._wasViewModalOpen = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// exported functions are global (loaded as <script> tags)
|
// exported functions are global (loaded as <script> tags)
|
||||||
|
|
|
||||||
|
|
@ -34,10 +34,10 @@ function renderTemplates(templates) {
|
||||||
${t.tags.map(tag => `<span class="tag">${esc(tag)}</span>`).join('')}
|
${t.tags.map(tag => `<span class="tag">${esc(tag)}</span>`).join('')}
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="btn btn-icon btn-view" data-path="${t.path}">Anzeigen</button>
|
<button class="btn btn-icon btn-view" data-path="${t.path}" aria-label="Anzeigen: ${esc(t.name)}">Anzeigen</button>
|
||||||
<button class="btn btn-icon btn-edit" data-path="${t.path}">📝 Bearbeiten</button>
|
<button class="btn btn-icon btn-edit" data-path="${t.path}" aria-label="Bearbeiten: ${esc(t.name)}">📝 Bearbeiten</button>
|
||||||
<button class="btn btn-icon btn-copy" data-path="${t.path}">Inhalt kopieren</button>
|
<button class="btn btn-icon btn-copy" data-path="${t.path}" aria-label="Inhalt kopieren: ${esc(t.name)}">Inhalt kopieren</button>
|
||||||
<button class="btn btn-icon btn-tune" data-path="${t.path}">🎯 Tunen</button>
|
<button class="btn btn-icon btn-tune" data-path="${t.path}" aria-label="Tunen: ${esc(t.name)}">🎯 Tunen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
`).join('');
|
||||||
|
|
|
||||||
|
|
@ -66,14 +66,16 @@ class PathValidator:
|
||||||
# Vollständigen Pfad konstruieren
|
# Vollständigen Pfad konstruieren
|
||||||
full_path = os.path.join(self.root_dir, "templates", normalized_rel)
|
full_path = os.path.join(self.root_dir, "templates", normalized_rel)
|
||||||
|
|
||||||
# Explizite Prüfung, dass der Pfad innerhalb von ROOT_DIR/templates/ liegt
|
templates_base = os.path.realpath(os.path.join(self.root_dir, "templates"))
|
||||||
templates_base = os.path.abspath(os.path.join(self.root_dir, "templates"))
|
full_path_resolved = os.path.realpath(full_path)
|
||||||
full_path_abs = os.path.abspath(full_path)
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
not full_path_abs.startswith(templates_base + os.sep)
|
not full_path_resolved.startswith(templates_base + os.sep)
|
||||||
and full_path_abs != templates_base
|
and full_path_resolved != templates_base
|
||||||
):
|
):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return full_path_abs
|
if os.path.islink(full_path_resolved):
|
||||||
|
return None
|
||||||
|
|
||||||
|
return full_path_resolved
|
||||||
|
|
|
||||||
18
web/serve.py
18
web/serve.py
|
|
@ -28,24 +28,6 @@ Handler.validator = validator
|
||||||
Handler.directory = DIRECTORY
|
Handler.directory = DIRECTORY
|
||||||
|
|
||||||
|
|
||||||
def find_free_port(start_port=9000):
|
|
||||||
"""Finde einen freien Port ab start_port.
|
|
||||||
|
|
||||||
Für zukünftige Smoke-Test-Integration.
|
|
||||||
"""
|
|
||||||
import socket
|
|
||||||
|
|
||||||
port = start_port
|
|
||||||
while port < 10000:
|
|
||||||
try:
|
|
||||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
||||||
s.bind(("", port))
|
|
||||||
return port
|
|
||||||
except OSError:
|
|
||||||
port += 1
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
PORT = 8081
|
PORT = 8081
|
||||||
logging.info("Serving on http://localhost:%s", PORT)
|
logging.info("Serving on http://localhost:%s", PORT)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue