diff --git a/CHANGELOG.md b/CHANGELOG.md
index eaa254a..1f2e2c8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,8 @@
## CHANGELOG
+### [25.1.1] - Jan 1, 2025
+- Rewritten Web Log authentication so it will now prompt you for a password
+
### [25.1.0] - Dec 29, 2024
- Added support for nested templates within `template.j2` using Jinja2 include syntax
- Added support for encrypted DataTemplates using Vaulty (ChaCha20-Poly1305 encryption)
@@ -365,6 +368,7 @@
- Initial release
+[25.1.1]: https://github.com/cmason3/jinjafx_server/compare/25.1.0...25.1.1
[25.1.0]: https://github.com/cmason3/jinjafx_server/compare/24.12.1...25.1.0
[24.12.1]: https://github.com/cmason3/jinjafx_server/compare/24.12.0...24.12.1
[24.12.0]: https://github.com/cmason3/jinjafx_server/compare/24.10.1...24.12.0
diff --git a/README.md b/README.md
index 4e689ab..2c9f991 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ python3 -m pip install --upgrade --user jinjafx-server
### JinjaFx Server Usage
-Once JinjaFx Server has been started with the `-s` argument then point your web browser at http://localhost:8080 and you will be presented with a web page that allows you to specify `data.csv`, `template.j2` and `vars.yml` and then generate outputs. If you click on "Export" then it will present you with an output that can be pasted back into any pane of JinjaFx to restore the values.
+Once JinjaFx Server has been started with the `-s` argument then point your web browser at http://localhost:8080 and you will be presented with a web page that allows you to specify "data.csv", "template.j2" and "vars.yml" and then generate outputs. If you click on "Export" then it will present you with an output that can be pasted back into any pane of JinjaFx to restore the values.
```
jinjafx_server -s [-l
] [-p ]
@@ -59,7 +59,8 @@ Description=JinjaFx Server
[Service]
Environment="VIRTUAL_ENV=/opt/jinjafx"
-ExecStart=/opt/jinjafx/bin/python3 -u -m jinjafx_server -s -l 127.0.0.1 -p 8080
+Environment="JFX_WEBLOG_KEY=''"
+ExecStart=/opt/jinjafx/bin/python3 -u -m jinjafx_server -s -l 127.0.0.1 -p 8080 -weblog
SyslogIdentifier=jinjafx_server
TimeoutStartSec=60
Restart=always
@@ -71,11 +72,11 @@ EOF
sudo systemctl enable --now jinjafx
```
-The "-r", "-s3" or "-github" arguments (mutually exclusive) allow you to specify a repository ("-r" is a local directory, "-s3" is an AWS S3 URL and "-github" is a GitHub repository) that will be used to store DataTemplates on the server via the "Get Link" and "Update Link" buttons. The generated link is guaranteed to be unique and a different link will be created every time - version 1.3.0 changed the behaviour, where previously the same link was always generated for the same DataTemplate, but this made it difficult to update DataTemplates without the link changing as it was basically a cryptographic hash of your DataTemplate. If you use an AWS S3 bucket then you will also need to provide some credentials via the two environment variables which has read and write permissions to the S3 URL.
+The `-r`, `-s3` or `-github` arguments (mutually exclusive) allow you to specify a repository (`-r` is a local directory, `-s3` is an AWS S3 URL and `-github` is a GitHub repository) that will be used to store DataTemplates on the server via the "Get Link" and "Update Link" buttons. The generated link is guaranteed to be unique and a different link will be created every time. If you use an AWS S3 bucket then you will also need to provide some credentials via the two environment variables which has read and write permissions to the S3 URL.
-The "-rl" argument is used to provide an optional rate limit of the source IP - the "rate" is how many requests are permitted and the "limit" is the interval in which those requests are permitted - it can be specified in "s", "m" or "h" (e.g. "5/30s", "10/1m" or "30/1h"). This is currently only applied to "Get Link" and Web Log authentication.
+The `-rl` argument is used to provide an optional rate limit of the source IP - the "rate" is how many requests are permitted and the "limit" is the interval in which those requests are permitted - it can be specified in "s", "m" or "h" (e.g. "5/30s", "10/1m" or "30/1h"). This is currently only applied to "Get Link" and Web Log authentication.
-The "-weblog" argument in combination with the "JFX_WEBLOG_KEY" environment variable enables the Web Log interface to view the current application logs - this can be accessed from a web browser using the URL `/logs?key=`.
+The `-weblog` argument in combination with the `JFX_WEBLOG_KEY` environment variable enables the Web Log interface to view the current application logs - this can be accessed from a web browser using the URL `/logs` - the user will be prompted for the key or they can provide it via a query string of `?key=`.
### Shortcut Keys
diff --git a/jinjafx_server.py b/jinjafx_server.py
index 35cb5e3..71e8d18 100755
--- a/jinjafx_server.py
+++ b/jinjafx_server.py
@@ -19,16 +19,14 @@
if sys.version_info < (3, 9):
sys.exit('Requires Python >= 3.9')
-from http.cookies import SimpleCookie
from http.server import HTTPServer, BaseHTTPRequestHandler
-from urllib.parse import urlparse, parse_qs
from jinja2 import __version__ as jinja2_version
import jinjafx, os, io, socket, signal, threading, yaml, json, base64, time, datetime, resource
import re, argparse, hashlib, traceback, glob, hmac, uuid, struct, binascii, gzip, requests, ctypes, subprocess
import cmarkgfm, emoji
-__version__ = '25.1.0'
+__version__ = '25.1.1'
llock = threading.RLock()
rlock = threading.RLock()
@@ -91,12 +89,12 @@ def log_message(self, format, *args):
else:
ansi = '31'
- if (args[1] != '204' and args[1] != '404' and args[1] != '501' and not path.startswith('/output.html') and not '/dt/' in path and (not path.startswith('/logs') or (args[1] != '200' and args[1] != '304'))) or self.critical or verbose:
+ if (args[1] != '204' and args[1] != '404' and args[1] != '501' and not path.startswith('/output.html') and not '/dt/' in path and (path != '/get_logs' or (args[1] != '200' and args[1] != '304'))) or self.critical or verbose:
src = str(self.client_address[0])
proto_ver = ''
ctype = ''
- if path.startswith('/logs') and args[1] == '302':
+ if path == '/get_logs' and args[1] == '302':
ansi = '32'
if hasattr(self, 'headers'):
@@ -217,55 +215,32 @@ def do_GET(self, head=False, cache=True, versioned=False):
r = [ 'text/plain', 200, 'OK\r\n'.encode('utf-8'), sys._getframe().f_lineno ]
elif fpath == '/logs' and jfx_weblog_key is not None:
- qs = parse_qs(urlparse(self.path).query, keep_blank_values=True)
- key = None
+ self.path = re.sub(r'key=[^&]*', 'key', self.path, flags=re.IGNORECASE)
- if 'key' in qs:
- for kv in qs['key']:
- self.path = self.path.replace('key=' + kv, 'key=*')
+ with open(base + '/www/logs.html', 'rb') as f:
+ r = [ 'text/html', 200, f.read(), sys._getframe().f_lineno ]
- key = qs['key'][-1]
-
- elif hasattr(self, 'headers'):
- cookies = SimpleCookie(self.headers.get('Cookie'))
- if 'jfx_weblog_key' in cookies:
- key = cookies['jfx_weblog_key'].value
-
- if key == jfx_weblog_key:
- if 'key' in qs:
- cheaders['Set-Cookie'] = 'jfx_weblog_key=' + key + '; path=/logs'
- if 'raw' in qs:
- cheaders['Location'] = '/logs?raw'
- else:
- cheaders['Location'] = '/logs'
- r = [ 'text/plain', 302, '302 Found\r\n'.encode('utf-8'), sys._getframe().f_lineno ]
-
- else:
- if not self.ratelimit(remote_addr, 3, True):
- if 'raw' in qs:
- with llock:
- logs = '\r\n'.join(logring)
+ elif fpath == '/get_logs' and jfx_weblog_key is not None:
+ if hasattr(self, 'headers') and 'X-WebLog-Password' in self.headers:
+ if self.headers['X-WebLog-Password'] == jfx_weblog_key:
+ with llock:
+ logs = '\r\n'.join(logring)
- logs = logs.replace('&', '&').replace('<', '<').replace('>', '>')
- logs = logs.replace('\033[1;31m', '')
- logs = logs.replace('\033[1;32m', '')
- logs = logs.replace('\033[1;33m', '')
- logs = logs.replace('\033[0m', '')
- r = [ 'text/plain', 200, logs.encode('utf-8'), sys._getframe().f_lineno ]
-
- else:
- with open(base + '/www/logs.html', 'rb') as f:
- r = [ 'text/html', 200, f.read(), sys._getframe().f_lineno ]
+ logs = logs.replace('&', '&').replace('<', '<').replace('>', '>')
+ logs = logs.replace('\033[1;31m', '')
+ logs = logs.replace('\033[1;32m', '')
+ logs = logs.replace('\033[1;33m', '')
+ logs = logs.replace('\033[0m', '')
+ r = [ 'text/plain', 200, logs.encode('utf-8'), sys._getframe().f_lineno ]
+ else:
+ if not self.ratelimit(remote_addr, 3, False):
+ r = [ 'text/plain', 401, '401 Unauthorized\r\n'.encode('utf-8'), sys._getframe().f_lineno ]
else:
r = [ 'text/plain', 429, '429 Too Many Requests\r\n'.encode('utf-8'), sys._getframe().f_lineno ]
else:
- cheaders['Set-Cookie'] = 'jfx_weblog_key=; path=/logs; max-age=0'
- if not self.ratelimit(remote_addr, 3, False):
- r = [ 'text/plain', 401, '401 Unauthorized\r\n'.encode('utf-8'), sys._getframe().f_lineno ]
- else:
- r = [ 'text/plain', 429, '429 Too Many Requests\r\n'.encode('utf-8'), sys._getframe().f_lineno ]
+ r = [ 'text/plain', 401, '401 Unauthorized\r\n'.encode('utf-8'), sys._getframe().f_lineno ]
else:
if fpath == '/':
diff --git a/www/jinjafx_logs.css b/www/jinjafx_logs.css
index 53d5024..6cd0a1c 100644
--- a/www/jinjafx_logs.css
+++ b/www/jinjafx_logs.css
@@ -1,8 +1,8 @@
body {
- color: white;
background: #000040;
}
pre {
+ color: white;
height: 100%;
font-variant-ligatures: none;
white-space: pre-wrap;
diff --git a/www/jinjafx_logs.js b/www/jinjafx_logs.js
index 1b027df..2aa5216 100644
--- a/www/jinjafx_logs.js
+++ b/www/jinjafx_logs.js
@@ -1,5 +1,7 @@
(function() {
let interval = 60;
+ let auth_ok = false;
+ let key = '';
function scroll() {
let e = document.getElementById('container');
@@ -8,14 +10,21 @@
function update() {
var xHR = new XMLHttpRequest();
- xHR.open("GET", '/logs?raw', true);
+ xHR.open("GET", '/get_logs', true);
xHR.onload = function() {
if (this.status == 200) {
+ auth_ok = true;
+ sessionStorage.setItem('jfx_weblog_key', key);
document.getElementById('container').innerHTML = xHR.responseText;
scroll();
setTimeout(update, interval * 1000);
}
+ else if ((this.status == 401) && !auth_ok) {
+ new bootstrap.Modal(document.getElementById('password_input'), {
+ keyboard: false
+ }).show();
+ }
else {
document.getElementById('container').innerHTML = 'HTTP ERROR ' + this.status;
}
@@ -32,11 +41,47 @@
};
xHR.timeout = 3000;
+ xHR.setRequestHeader("X-WebLog-Password", key);
xHR.send();
}
window.onresize = scroll;
window.onload = function() {
- update();
+ document.getElementById('password_input').addEventListener('shown.bs.modal', function (e) {
+ document.getElementById("in_password").focus();
+ });
+
+ document.getElementById('password_input').addEventListener('hidden.bs.modal', function (e) {
+ document.getElementById("in_password").value = '';
+ });
+
+ document.getElementById('ml-password-ok').addEventListener('click', function (e) {
+ key = document.getElementById("in_password").value;
+ update();
+ });
+
+ document.getElementById('in_password').addEventListener('keyup', function(e) {
+ if (e.which == 13) {
+ document.getElementById('ml-password-ok').click();
+ }
+ });
+
+ let qs = new URLSearchParams(window.location.search);
+
+ if (qs.has('key')) {
+ key = qs.get('key');
+ window.history.replaceState(null, null, window.location.pathname);
+ sessionStorage.removeItem('jfx_weblog_key');
+ update();
+ }
+ else if (sessionStorage.getItem('jfx_weblog_key')) {
+ key = sessionStorage.getItem('jfx_weblog_key');
+ update();
+ }
+ else {
+ new bootstrap.Modal(document.getElementById('password_input'), {
+ keyboard: false
+ }).show();
+ }
};
})();
diff --git a/www/logs.html b/www/logs.html
index 6f6d729..1ed4487 100644
--- a/www/logs.html
+++ b/www/logs.html
@@ -7,10 +7,30 @@
-
-
+
+
+
+