Skip to content

Commit a7c0c9b

Browse files
authored
Merge pull request #102 from atb00ker/allow-changing-data-directory
Allow changing data directory
2 parents 0ff6de4 + 4eea529 commit a7c0c9b

File tree

7 files changed

+113
-58
lines changed

7 files changed

+113
-58
lines changed

.env.sample

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# General variables
2+
TZ="UTC"
3+
COLOR="blue-grey"
4+
HS_SERVER=http://localhost:8080
5+
KEY="GenerateYourOwnRandomKey"
6+
SCRIPT_NAME=/admin
7+
DATA_DIRECTORY=/data
8+
DOMAIN_NAME=http://localhost:8080
9+
AUTH_TYPE="Basic"
10+
LOG_LEVEL="Debug"
11+
12+
# BasicAuth variables
13+
BASIC_AUTH_USER="admin"
14+
BASIC_AUTH_PASS="admin"
15+
16+
# Flask OIDC Variables
17+
OIDC_AUTH_URL=https://localhost:8080
18+
OIDC_CLIENT_ID=Headscale-WebUI
19+
OIDC_CLIENT_SECRET=secret
20+
21+
# About section on the Settings page
22+
GIT_COMMIT=""
23+
GIT_BRANCH=""
24+
APP_VERSION=""
25+
BUILD_DATE=""
26+
HS_VERSION=""

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
__pycache__
22
.venv
3-
poetry.lock
3+
.env
4+
poetry.lock

SETUP.md

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,39 @@
55
* Containers are published to [GHCR](https://github.com/users/iFargle/packages/container/package/headscale-webui) and [Docker Hub](https://hub.docker.com/r/ifargle/headscale-webui)
66

77
# Contents
8+
* [Bare Metal](#bare-metal)
89
* [Docker Compose](#docker-compose)
910
* [Reverse Proxies](#reverse-proxies)
1011
* [Authentication](#authentication)
1112

1213
---
14+
# Bare Metal
15+
16+
1. Install dependencies:
17+
18+
```bash
19+
# Debian/Ubuntu
20+
apt install gcc python3-poetry --yes
21+
poetry install --only main
22+
```
23+
24+
2. Configurations: rename `.env.sample` -> `.env` and edit `.env` as per your requirements.
25+
26+
3. Run server
27+
28+
```bash
29+
poetry run gunicorn -b 0.0.0.0:5000 server:app
30+
```
31+
32+
That's it. Cheers.
33+
1334
# Docker Compose
1435
## Environment Settings
1536
* `TZ` - Set this to your current timezone. Example: `Asia/Tokyo`
1637
* `COLOR` Set this to your preferred color scheme. See the [MaterializeCSS docs](https://materializecss.github.io/materialize/color.html#palette) for examples. Only set the "base" color -- ie, instead of `blue-gray darken-1`, just use `blue-gray`.
1738
* `HS_SERVER` is the URL for your Headscale control server.
1839
* `SCRIPT_NAME` is your "Base Path" for hosting. For example, if you want to host on http://localhost/admin, set this to `/admin`, otherwise remove this variable entirely.
40+
* `DATA_DIRECTORY` is your "Data Path". This is there the application will create and store data. Only applicable for bare metal installations.
1941
* `KEY` is your encryption key. Set this to a random value generated from `openssl rand -base64 32`
2042
* `AUTH_TYPE` can be set to `Basic` or `OIDC`. See the [Authentication](#Authentication) section below for more information.
2143
* `LOG_LEVEL` can be one of `Debug`, `Info`, `Warning`, `Error`, or `Critical` for decreasing verbosity. Default is `Info` if removed from your Environment.
@@ -81,7 +103,7 @@ https://[DOMAIN] {
81103
reverse_proxy * [HS_SERVER]
82104
}
83105
```
84-
* Example:
106+
* Example:
85107
```
86108
https://example.com {
87109
reverse_proxy /admin* http://headscale-webui:5000
@@ -90,7 +112,7 @@ https://example.com {
90112
}
91113
```
92114

93-
---
115+
---
94116
# Authentication
95117
*If your OIDC provider isn't listed or doesn't work, please open up a [new issue](https://github.com/iFargle/headscale-webui/issues/new) and it will be worked on.*
96118

headscale.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55
from datetime import timedelta, date
66
from dateutil import parser
77
from flask import Flask
8+
from dotenv import load_dotenv
89

10+
load_dotenv()
911
LOG_LEVEL = os.environ["LOG_LEVEL"].replace('"', '').upper()
12+
DATA_DIRECTORY = os.environ["DATA_DIRECTORY"].replace('"', '') if os.environ["DATA_DIRECTORY"] else "/data"
1013
# Initiate the Flask application and logging:
1114
app = Flask(__name__, static_url_path="/static")
1215
match LOG_LEVEL:
@@ -39,7 +42,7 @@ def set_api_key(api_key):
3942
# User-set encryption key
4043
encryption_key = os.environ['KEY']
4144
# Key file on the filesystem for persistent storage
42-
key_file = open("/data/key.txt", "wb+")
45+
key_file = open(os.path.join(DATA_DIRECTORY, "key.txt"), "wb+")
4346
# Preparing the Fernet class with the key
4447
fernet = Fernet(encryption_key)
4548
# Encrypting the key
@@ -48,11 +51,11 @@ def set_api_key(api_key):
4851
return True if key_file.write(encrypted_key) else False
4952

5053
def get_api_key():
51-
if not os.path.exists("/data/key.txt"): return False
54+
if not os.path.exists(os.path.join(DATA_DIRECTORY, "key.txt")): return False
5255
# User-set encryption key
5356
encryption_key = os.environ['KEY']
5457
# Key file on the filesystem for persistent storage
55-
key_file = open("/data/key.txt", "rb+")
58+
key_file = open(os.path.join(DATA_DIRECTORY, "key.txt"), "rb+")
5659
# The encrypted key read from the file
5760
enc_api_key = key_file.read()
5861
if enc_api_key == b'': return "NULL"

helper.py

Lines changed: 51 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from flask import Flask
55

66
LOG_LEVEL = os.environ["LOG_LEVEL"].replace('"', '').upper()
7+
DATA_DIRECTORY = os.environ["DATA_DIRECTORY"].replace('"', '') if os.environ["DATA_DIRECTORY"] else "/data"
78
# Initiate the Flask application and logging:
89
app = Flask(__name__, static_url_path="/static")
910
match LOG_LEVEL:
@@ -45,7 +46,7 @@ def text_color_duration(duration):
4546
if days > 5: return "deep-orange-text text-lighten-1"
4647
if days > 1: return "deep-orange-text text-lighten-1"
4748
if hours > 12: return "orange-text "
48-
if hours > 1: return "orange-text text-lighten-2"
49+
if hours > 1: return "orange-text text-lighten-2"
4950
if hours == 1: return "yellow-text "
5051
if mins > 15: return "yellow-text text-lighten-2"
5152
if mins > 5: return "green-text text-lighten-3"
@@ -57,11 +58,11 @@ def key_check():
5758
api_key = headscale.get_api_key()
5859
url = headscale.get_url()
5960

60-
# Test the API key. If the test fails, return a failure.
61+
# Test the API key. If the test fails, return a failure.
6162
# AKA, if headscale returns Unauthorized, fail:
6263
app.logger.info("Testing API key validity.")
6364
status = headscale.test_api_key(url, api_key)
64-
if status != 200:
65+
if status != 200:
6566
app.logger.info("Got a non-200 response from Headscale. Test failed (Response: %i)", status)
6667
return False
6768
else:
@@ -128,7 +129,7 @@ def format_message(error_type, title, message):
128129
<ul class="collection">
129130
<li class="collection-item avatar">
130131
"""
131-
132+
132133
match error_type.lower():
133134
case "warning":
134135
icon = """<i class="material-icons circle yellow">priority_high</i>"""
@@ -143,7 +144,7 @@ def format_message(error_type, title, message):
143144
icon = """<i class="material-icons circle grey">help</i>"""
144145
title = """<span class="title">Information - """+title+"""</span>"""
145146

146-
content = content+icon+title+message
147+
content = content+icon+title+message
147148
content = content+"""
148149
</li>
149150
</ul>
@@ -158,12 +159,12 @@ def access_checks():
158159
# Return an error message if things fail.
159160
# Return a formatted error message for EACH fail.
160161
checks_passed = True # Default to true. Set to false when any checks fail.
161-
data_readable = False # Checks R permissions of /data
162-
data_writable = False # Checks W permissions of /data
162+
data_readable = False # Checks R permissions of DATA_DIRECTORY
163+
data_writable = False # Checks W permissions of DATA_DIRECTORY
163164
data_executable = False # Execute on directories allows file access
164-
file_readable = False # Checks R permissions of /data/key.txt
165-
file_writable = False # Checks W permissions of /data/key.txt
166-
file_exists = False # Checks if /data/key.txt exists
165+
file_readable = False # Checks R permissions of DATA_DIRECTORY/key.txt
166+
file_writable = False # Checks W permissions of DATA_DIRECTORY/key.txt
167+
file_exists = False # Checks if DATA_DIRECTORY/key.txt exists
167168
config_readable = False # Checks if the headscale configuration file is readable
168169

169170

@@ -176,32 +177,32 @@ def access_checks():
176177
checks_passed = False
177178
app.logger.critical("Headscale URL: Response 200: FAILED")
178179

179-
# Check: /data is rwx for 1000:1000:
180-
if os.access('/data/', os.R_OK): data_readable = True
180+
# Check: DATA_DIRECTORY is rwx for 1000:1000:
181+
if os.access(DATA_DIRECTORY, os.R_OK): data_readable = True
181182
else:
182-
app.logger.critical("/data READ: FAILED")
183+
app.logger.critical(f"{DATA_DIRECTORY} READ: FAILED")
183184
checks_passed = False
184-
if os.access('/data/', os.W_OK): data_writable = True
185+
if os.access(DATA_DIRECTORY, os.W_OK): data_writable = True
185186
else:
186-
app.logger.critical("/data WRITE: FAILED")
187+
app.logger.critical(f"{DATA_DIRECTORY} WRITE: FAILED")
187188
checks_passed = False
188-
if os.access('/data/', os.X_OK): data_executable = True
189+
if os.access(DATA_DIRECTORY, os.X_OK): data_executable = True
189190
else:
190-
app.logger.critical("/data EXEC: FAILED")
191+
app.logger.critical(f"{DATA_DIRECTORY} EXEC: FAILED")
191192
checks_passed = False
192193

193-
# Check: /data/key.txt exists and is rw:
194-
if os.access('/data/key.txt', os.F_OK):
194+
# Check: DATA_DIRECTORY/key.txt exists and is rw:
195+
if os.access(os.path.join(DATA_DIRECTORY, "key.txt"), os.F_OK):
195196
file_exists = True
196-
if os.access('/data/key.txt', os.R_OK): file_readable = True
197+
if os.access(os.path.join(DATA_DIRECTORY, "key.txt"), os.R_OK): file_readable = True
197198
else:
198-
app.logger.critical("/data/key.txt READ: FAILED")
199+
app.logger.critical(f"{os.path.join(DATA_DIRECTORY, 'key.txt')} READ: FAILED")
199200
checks_passed = False
200-
if os.access('/data/key.txt', os.W_OK): file_writable = True
201+
if os.access(os.path.join(DATA_DIRECTORY, "key.txt"), os.W_OK): file_writable = True
201202
else:
202-
app.logger.critical("/data/key.txt WRITE: FAILED")
203+
app.logger.critical(f"{os.path.join(DATA_DIRECTORY, 'key.txt')} WRITE: FAILED")
203204
checks_passed = False
204-
else: app.logger.error("/data/key.txt EXIST: FAILED - NO ERROR")
205+
else: app.logger.error(f"{os.path.join(DATA_DIRECTORY, 'key.txt')} EXIST: FAILED - NO ERROR")
205206

206207
# Check: /etc/headscale/config.yaml is readable:
207208
if os.access('/etc/headscale/config.yaml', os.R_OK): config_readable = True
@@ -219,7 +220,7 @@ def access_checks():
219220
if not server_reachable:
220221
app.logger.critical("Server is unreachable")
221222
message = """
222-
<p>Your headscale server is either unreachable or not properly configured.
223+
<p>Your headscale server is either unreachable or not properly configured.
223224
Please ensure your configuration is correct (Check for 200 status on
224225
"""+url+"""/api/v1 failed. Response: """+str(response.status_code)+""".)</p>
225226
"""
@@ -237,58 +238,58 @@ def access_checks():
237238
message_html += format_message("Error", "/etc/headscale/config.yaml not readable", message)
238239

239240
if not data_writable:
240-
app.logger.critical("/data folder is not writable")
241-
message = """
242-
<p>/data is not writable. Please ensure your
243-
permissions are correct. /data mount should be writable
241+
app.logger.critical(f"{DATA_DIRECTORY} folder is not writable")
242+
message = f"""
243+
<p>{DATA_DIRECTORY} is not writable. Please ensure your
244+
permissions are correct. {DATA_DIRECTORY} mount should be writable
244245
by UID/GID 1000:1000.</p>
245246
"""
246247

247-
message_html += format_message("Error", "/data not writable", message)
248+
message_html += format_message("Error", f"{DATA_DIRECTORY} not writable", message)
248249

249250
if not data_readable:
250-
app.logger.critical("/data folder is not readable")
251-
message = """
252-
<p>/data is not readable. Please ensure your
253-
permissions are correct. /data mount should be readable
251+
app.logger.critical(f"{DATA_DIRECTORY} folder is not readable")
252+
message = f"""
253+
<p>{DATA_DIRECTORY} is not readable. Please ensure your
254+
permissions are correct. {DATA_DIRECTORY} mount should be readable
254255
by UID/GID 1000:1000.</p>
255256
"""
256257

257-
message_html += format_message("Error", "/data not readable", message)
258+
message_html += format_message("Error", f"{DATA_DIRECTORY} not readable", message)
258259

259260
if not data_executable:
260-
app.logger.critical("/data folder is not readable")
261-
message = """
262-
<p>/data is not executable. Please ensure your
263-
permissions are correct. /data mount should be readable
261+
app.logger.critical(f"{DATA_DIRECTORY} folder is not readable")
262+
message = f"""
263+
<p>{DATA_DIRECTORY} is not executable. Please ensure your
264+
permissions are correct. {DATA_DIRECTORY} mount should be readable
264265
by UID/GID 1000:1000. (chown 1000:1000 /path/to/data && chmod -R 755 /path/to/data)</p>
265266
"""
266267

267-
message_html += format_message("Error", "/data not executable", message)
268+
message_html += format_message("Error", f"{DATA_DIRECTORY} not executable", message)
268269

269270

270271
if file_exists:
271272
# If it doesn't exist, we assume the user hasn't created it yet.
272273
# Just redirect to the settings page to enter an API Key
273274
if not file_writable:
274-
app.logger.critical("/data/key.txt is not writable")
275-
message = """
276-
<p>/data/key.txt is not writable. Please ensure your
277-
permissions are correct. /data mount should be writable
275+
app.logger.critical(f"{os.path.join(DATA_DIRECTORY, 'key.txt')} is not writable")
276+
message = f"""
277+
<p>{os.path.join(DATA_DIRECTORY, 'key.txt')} is not writable. Please ensure your
278+
permissions are correct. {DATA_DIRECTORY} mount should be writable
278279
by UID/GID 1000:1000.</p>
279280
"""
280281

281-
message_html += format_message("Error", "/data/key.txt not writable", message)
282+
message_html += format_message("Error", f"{os.path.join(DATA_DIRECTORY, 'key.txt')} not writable", message)
282283

283284
if not file_readable:
284-
app.logger.critical("/data/key.txt is not readable")
285-
message = """
286-
<p>/data/key.txt is not readable. Please ensure your
287-
permissions are correct. /data mount should be readable
285+
app.logger.critical(f"{os.path.join(DATA_DIRECTORY, 'key.txt')} is not readable")
286+
message = f"""
287+
<p>{os.path.join(DATA_DIRECTORY, 'key.txt')} is not readable. Please ensure your
288+
permissions are correct. {DATA_DIRECTORY} mount should be readable
288289
by UID/GID 1000:1000.</p>
289290
"""
290291

291-
message_html += format_message("Error", "/data/key.txt not readable", message)
292+
message_html += format_message("Error", f"{os.path.join(DATA_DIRECTORY, 'key.txt')} not readable", message)
292293

293294
return message_html
294295

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pyuwsgi = "^2.0.21"
1818
gunicorn = "^20.1.0"
1919
flask-basicauth = "^0.2.0"
2020
flask-providers-oidc = "^1.2.1"
21+
python-dotenv = "^1.0.0"
2122

2223
[tool.poetry.dev-dependencies]
2324

templates/settings.html

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
<i class="material-icons prefix">vpn_key</i>
1818
<input id="api_key" type="password">
1919
<label for="api_key">API Key</label>
20-
</div>
20+
</div>
2121
</div>
2222
<div class="card-action">
2323
<a href="#test_modal" class="modal-trigger" onclick="save_key()">Save</a>
@@ -57,6 +57,7 @@ <h4>Instructions</h4>
5757
<li>To generate your API key, run the command <a class="{{ COLOR_BTN }} white-text">headscale apikeys create</a> on your control server. Once you generate your first key, this UI will automatically renew the key near expiration.</li>
5858
<li>The Headscale server is configured via the <a class="{{ COLOR_BTN }} white-text">HS_SERVER</a> environment variable in Docker. Current server: <a class="{{ COLOR_BTN }} white-text"> {{url}} </a></li>
5959
<li>You must configure an encryption key via the <a class="{{ COLOR_BTN }} white-text">KEY</a> environment variable in Docker. One can be generated with the command <a class="{{ COLOR_BTN }} white-text">openssl rand -base64 32</a></li>
60+
<li>Enter the API key generated by headscale, press "Save" then "Test". Saving before using the "Test" button is important.</li>
6061
</ul>
6162
</div>
6263
<div class="modal-footer">
@@ -81,4 +82,4 @@ <h4>Web UI Theme Settings</h4>
8182
<a href="#!" class="modal-close btn-flat">Close</a>
8283
</div>
8384
</div>
84-
{% endblock %}
85+
{% endblock %}

0 commit comments

Comments
 (0)