Skip to content

Commit

Permalink
Merge pull request #55 from cmason3/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
cmason3 authored Jan 1, 2025
2 parents 50b4e18 + deb0bec commit f8310e0
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 56 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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
Expand Down
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <address>] [-p <port>]
Expand Down Expand Up @@ -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='<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
Expand All @@ -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=<JFX_WEBLOG_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=<JFX_WEBLOG_KEY>`.

### Shortcut Keys

Expand Down
67 changes: 21 additions & 46 deletions jinjafx_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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'):
Expand Down Expand Up @@ -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('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
logs = logs.replace('\033[1;31m', '<span class="text-danger">')
logs = logs.replace('\033[1;32m', '<span class="text-success">')
logs = logs.replace('\033[1;33m', '<span class="text-warning">')
logs = logs.replace('\033[0m', '</span>')
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('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
logs = logs.replace('\033[1;31m', '<span class="text-danger">')
logs = logs.replace('\033[1;32m', '<span class="text-success">')
logs = logs.replace('\033[1;33m', '<span class="text-warning">')
logs = logs.replace('\033[0m', '</span>')
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 == '/':
Expand Down
2 changes: 1 addition & 1 deletion www/jinjafx_logs.css
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
body {
color: white;
background: #000040;
}
pre {
color: white;
height: 100%;
font-variant-ligatures: none;
white-space: pre-wrap;
Expand Down
49 changes: 47 additions & 2 deletions www/jinjafx_logs.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
(function() {
let interval = 60;
let auth_ok = false;
let key = '';

function scroll() {
let e = document.getElementById('container');
Expand All @@ -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;
}
Expand All @@ -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();
}
};
})();
24 changes: 22 additions & 2 deletions www/logs.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,30 @@
<link rel="shortcut icon" href="/874f2915/jinjafx.png">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css" integrity="sha512-jnSuA4Ss2PkkikSOLtYs8BlYIeeIK1h99ty4YfvRPAlzr377vr3CXDb7sb7eEEBYjDtcYj+AjBH3FLv5uSJuXg==" crossorigin="anonymous" referrerpolicy="no-referrer">
<link rel="stylesheet" href="/bb176715/jinjafx.css">
<link rel="stylesheet" href="/28fdf760/jinjafx_logs.css">
<script src="/4f5b7922/jinjafx_logs.js"></script>
<link rel="stylesheet" href="/53007a23/jinjafx_logs.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/js/bootstrap.bundle.min.js" integrity="sha512-7Pi/otdlbbCR+LnW+F7PwFcSDJOuUJB3OxtEHbg4vSMvzvJjde4Po1v4BR9Gdc9aXNUNFVUY+SK51wWT8WF0Gg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="/78bc24cc/jinjafx_logs.js"></script>
</head>
<body>
<pre id="container" class="p-3"></pre>
<div id="password_input" class="modal fade" role="dialog">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h4 class="mt-0">JinjaFx Logs</h4>
</div>
<div class="form-group row modal-body">
<label id="lb_password" for="in_password" class="col-sm-12 col-form-label">Enter Password</label>
<div class="col-sm-12">
<input type="password" class="form-control" id="in_password">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button id="ml-password-ok" type="button" class="btn btn-primary" data-bs-dismiss="modal">OK</button>
</div>
</div>
</div>
</div>
</body>
</html>

0 comments on commit f8310e0

Please sign in to comment.